Benutzer:Notebook/monobook.js
Zur Navigation springen
Zur Suche springen
Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.
- Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
- Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
- Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
/* <pre><nowiki> */
//======================================================================
//## core/prototypes.js
/** bind a function to an object */
Function.prototype.bind = function(object) {
var __self = this;
return function() {
__self.apply(object, arguments);
};
}
/** remove whitespace from both ends */
String.prototype.trim = function() {
return this.replace(/^\s+/, "")
.replace(/\s+$/, "");
}
/** true when the string starts with the pattern */
String.prototype.startsWith = function(s) {
return this.length >= s.length
&& this.substring(0, s.length) == s;
}
/** true when the string ends in the pattern */
String.prototype.endsWith = function(s) {
return this.length >= s.length
&& this.substring(this.length - s.length) == s;
}
/** return text without prefix or null */
String.prototype.scan = function(s) {
return this.substring(0, s.length) == s
? this.substring(s.length)
: null;
}
/** escapes characters to make them usable as a literal in a regexp */
String.prototype.escapeRegexp = function() {
return this.replace(/([{}()|.?*+^$\[\]\\])/g, "\\$0");
}
//======================================================================
//## core/functions.js
/** find an element in document by its id */
function $(id) {
return document.getElementById(id);
}
/** find descendants of an ancestor by tagName, className and index */
function descendants(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 = new Array();
for (var i=0; i<elements.length; i++) {
if (elements[i].className == className) {
tmp.push(elements[i]);
}
}
elements = tmp;
}
if (typeof index == "undefined") return elements;
if (index >= elements.length) return null;
return elements[index];
}
/** find the next element from el which has a given nodeName or is non-text */
function nextElement(el, nodeName) {
for (;;) {
el = el.nextSibling; if (!el) return null;
if (nodeName) { if (el.nodeName.toUpperCase() == nodeName.toUpperCase()) 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 */
function previousElement(el, nodeName) {
for (;;) {
el = el.previousSibling; if (!el) return null;
if (nodeName) { if (el.nodeName.toUpperCase() == nodeName.toUpperCase()) return el; }
else { if (el.nodeName.toUpperCase() != "#TEXT") return el; }
}
}
/** remove a node from its parent node */
function removeNode(node) {
node.parentNode.removeChild(node);
}
/** removes all children of a node */
function removeChildren(node) {
while (node.lastChild) node.removeChild(node.lastChild);
}
/** inserts an element before another one. allows an Array for multiple elements and string for textNodes */
function pasteBefore(targetIdOrElement, element) {
var target = targetIdOrElement.constructor == String
? document.getElementById(targetIdOrElement)
: targetIdOrElement;
if (!target) return;
function add(element) {
if (element.constructor == String) element = document.createTextNode(element);
target.parentNode.insertBefore(element, target);
}
if (element.constructor == Array) {
for (var i=0; i<element.length; i++) { add(element[i]); }
}
else {
add(element);
}
}
/** inserts an element before another one. allows an Array for multiple elements and string for textNodes */
function pasteAfter(targetIdOrElement, element) {
var target = targetIdOrElement.constructor == String
? document.getElementById(targetIdOrElement)
: targetIdOrElement;
if (!target) return;
function addInsert(element) {
if (element.constructor == String) element = document.createTextNode(element);
target.parentNode.insertBefore(element, next);
}
function addAppend(element) {
if (element.constructor == String) element = document.createTextNode(element);
target.parentNode.appendChild(element);
}
var next = target.nextSibling;
var add = next ? addInsert : addAppend;
if (element.constructor == Array) {
for (var i=0; i<element.length; i++) { add(element[i]); }
}
else {
add(element);
}
}
/** insert text, element or elements at the start of a target */
function pasteBegin(targetIdOrElement, element) {
var target = targetIdOrElement.constructor == String
? document.getElementById(targetIdOrElement)
: targetIdOrElement;
if (!target) return;
function add(element) {
if (element.constructor == String) element = document.createTextNode(element);
if (target.firstChild) target.insertBefore(element, target.firstChild);
else target.appendChild(element);
}
if (element.constructor == Array) {
for (var i=0; i<element.length; i++) { add(element[i]); }
}
else {
add(element);
}
}
/** insert text, element or elements at the end of a target */
function pasteEnd(targetIdOrElement, element) {
var target = targetIdOrElement.constructor == String
? document.getElementById(targetIdOrElement)
: targetIdOrElement;
if (!target) return;
function add(element) {
if (element.constructor == String) element = document.createTextNode(element);
target.appendChild(element);
}
if (element.constructor == Array) {
for (var i=0; i<element.length; i++) { add(element[i]); }
}
else {
add(element);
}
}
/** adds or removes className parts from a String */
function className(state, add, remove) {
// put existing into a map
var stateSplit = state.split(/\s+/);
var stateMap = new Array();
for (var i=0; i<stateSplit.length; i++) {
var stateClass = stateSplit[i].trim();
if (stateClass.length == 0) continue;
stateMap[stateClass] = 1;
}
// remove parts
var remSplit = remove.split(/\s+/);
for (var i=0; i<remSplit.length; i++) {
var name = remSplit[i];
delete stateMap[name];
}
// add parts
var addSplit = add.split(/\s+/);
for (var i=0; i<addSplit.length; i++) {
var name = addSplit[i];
stateMap[name] = 1;
}
// join parts
var newStr = "";
for (var newClass in stateMap) {
newStr += " " + newClass;
}
if (newStr != "") newStr = newStr.substring(1);
return newStr;
}
/** add an OnLoad event handler */
function doOnLoad(callback) {
//.. gecko, safari, konqueror and standard
if (typeof window.addEventListener != 'undefined')
window.addEventListener('load', callback, false);
//.. opera 7
else if (typeof document.addEventListener != 'undefined')
document.addEventListener('load', callback, false);
//.. win/ie
else if (typeof window.attachEvent != 'undefined')
window.attachEvent('onload', callback);
// mac/ie5 and other crap fails here. on purpose.
}
/** concatenate two texts with an optional separator which is left out when one of the texts is empty */
function concatSeparated(left, separator, right) {
var out = "";
if (left) out += left;
if (left && right && separator) out += separator;
if (right) out += right;
return out;
}
//======================================================================
//## core/Page.js
/** represents the current Page */
Page = { init: function() {
//------------------------------------------------------------------------------
//## public info
/** the current wiki site without any path */
this.site = location.protocol + "//" + location.host
+ (location.port ? ":" + location.port : "");
/** search string of the current location decoded into an Array */
this.params = location.search
? decodeArgs(location.search.substring(1))
: {};
/** path to read pages */
this.readPath = null;
/** path for page actions */
this.actionPath = "/w/index.php"; //### generalize
/** decoded Special namespace */
this.specialNS = null;
/** decoded User namespace */
this.userNS = null;
/** decoded User_talk namespace */
this.userTalkNS = null;
/** name of the logged in user or null (should never happen) */
this.user = null;
/** the namespace of the current page */
this.namespace = null;
/** lemma for the current URL ignoring redirects */
this.lemma = null;
/** permalink to the current page if one exists or null */
this.perma = null;
/** the user a User or User_talk or Special:Contributions page belongs to */
this.owner = null;
/** whether a this page could be deleted */
this.deletable = false;
//------------------------------------------------------------------------------
//## public methods
/** compute an URL in action form which may have a title parameter */
this.actionURL = function(args) {
var url = this.site + this.actionPath;
return appendArgs(url, args);
}
/** compute an URL in the read form without a title parameter. args object and target string are optional */
this.readURL = function(lemma, args, target) {
var url = this.site
+ this.readPath
+ encodeTitle(lemma)
+ (target ? "/" + encodeTitle(target) : "");
return appendArgs(url, args);
}
/**
* returns an object containing the name and optionally argument
* when this Page is the named SpecialPage or returns null if not
*/
this.isSpecial = function(testName) {
// remove Special-namespace or fail
var path = this.lemma.scan(this.specialNS + ":");
if (!path) return null;
// check page or fail
var match = /([^\/]+)(\/(.+))?/(path);
var name = match[1];
var arg = match[3];
if (name != testName) return null;
// found it: return name and optional arg
return {
name: match[1],
argument: match[3],
};
}
//------------------------------------------------------------------------------
//## private url encoding and decoding functions
/** append arguments to an url with "?" or "&" depending on context */
function appendArgs(url, args) {
var code = encodeArgs(args);
if (code == "") return url;
return url
+ (url.indexOf("?") == -1 ? "?" : "&")
+ code;
}
/** encode an Object or Array into URL parameters. */
function encodeArgs(args) {
if (!args) return "";
var query = "";
for (arg in args) {
var key = encodeURIComponent(arg);
var raw = args[arg];
if (raw == null) continue;
var value = arg == "title" || arg == "target"
? encodeTitle(raw)
: encodeURIComponent(raw.toString());
query += "&" + key + "=" + value;
}
if (query == "") return "";
return query.substring(1);
}
/** convert URL-parameters into an Array. */
function decodeArgs(search) {
var out = new Object();
if (search == "") return out;
var split = search.split("&");
for (i=0; i<split.length; i++) {
var parts = split[i].split("=");
var key = decodeURIComponent(parts[0]);
var value = key == "title" || key == "target"
? decodeTitle(parts[1])
: decodeURIComponent(parts[1]);
out[key] = value;
}
return out;
}
/** encode a MediaWiki title into URL form */
function encodeTitle(title) {
return encodeURIComponent(title)
.replace(/%3[aA]/g, ":")
.replace(/%2[fF]/g, "/")
.replace(/ /g, "_");
}
/** decode a MediaWiki title from an URL */
function decodeTitle(title) {
// MediaWiki allows "+" for "+" instead of " " since 05jan06
title = title.replace(/\+/g, "%2b");
return decodeURIComponent(title)
.replace(/_/g, " ");
}
//------------------------------------------------------------------------------
//## page info intilialization
// scoping helper
var self = this;
// setup readPath and specialNS
(function() {
var search = $('searchform').action;
var title = search.scan(self.site);
var match = /(.*\/)(.*):.*/(title ? title : search);
self.readPath = match[1];
self.specialNS = decodeTitle(match[2]);
})();
// setup user and userNS
(function() {
// <li id="pt-userpage"><a href="/wiki/Benutzer:D">D</a></li>
// <li id="ca-nstab-user"><a href="/wiki/Benutzer:D">Benutzerseite</a></li>
var a = descendants('pt-userpage', "a", null, 0);
if (a == null) return;
var href = a.attributes.href.value;
var split = href.split(self.readPath);
if (split.length < 2) return;
var title = decodeTitle(split[1]);
var match = /([^:]+):(.*)/(title);
self.userNS = match[1];
self.user = match[2];
})();
// setup userTalkNS
(function() {
//<li id="pt-mytalk"><a href="/wiki/Benutzer_Diskussion:D">Eigene Diskussion</a></li>
var a = descendants('pt-mytalk', "a", null, 0);
if (a == null) return;
var href = a.attributes.href.value;
var split = href.split(self.readPath);
if (split.length < 2) return;
var title = decodeTitle(split[1]);
var match = /([^:]+):(.*)/(title);
self.userTalkNS = match[1];
})();
// setup lemma
(function() {
if (self.params.title) {
if (self.params.target) self.lemma = self.params.title + "/" + self.params.target;
else self.lemma = self.params.title;
}
else {
var scanned = this.location.pathname.scan(self.readPath);
if (scanned != null) self.lemma = decodeTitle(scanned);
else self.lemma = null; //### error!
}
})();
// setup perma
(function() {
// <li id="t-permalink"><a href="/w/index.php?title=Benutzer_Diskussion:D&oldid=12706568">Permanentlink</a></li>
var a = descendants('t-permalink', "a", null, 0);
if (a == null) return;
// to get the oldid use this:
// .replace(/^.*&oldid=([0-9]+).*/, "$1");
self.perma = a.href;
})();
// setup owner
(function() {
// try Special:Contributions
self.owner = self.lemma.scan(self.specialNS + ":Contributions/");
if (self.owner) return;
// try Special:Blockip
self.owner = self.lemma.scan(self.specialNS + ":Blockip/");
if (self.owner) return;
// try Special:Emailuser
self.owner = self.lemma.scan(self.specialNS + ":Emailuser/");
if (self.owner) return;
// try blocklog
(function() {
var log = self.isSpecial("Log");
if (!log) return; // require Special:Log
if (log.argument != "block"
&& self.params["type"] != "block") return; // require Special:Log/block
if (!self.isSpecial("Log")) return; // or Special:Log?type=block
var page = self.params["page"];
if (!page) return; // require &page=User:Name
var title = decodeTitle(page); //### correct??
var user = title.scan(Page.userNS + ":");
if (!user) return;
self.owner = user;
})();
if (self.owner) return;
// <li id="t-blockip"><a href="/wiki/Spezial:Blockip/D">Benutzer blockieren</a></li>
var a = descendants('t-blockip', "a", null, 0);
if (a == null) return;
var href = a.attributes.href.value;
var split = href.split(self.readPath);
if (split.length < 2) return;
var full = decodeTitle(split[1]);
var split2 = full.split(/\//);
if (split2.length < 2) return;
self.owner = split2[1];
})();
// setup contributor, deletable and namespace
(function() {
self.deletable = $('ca-delete') != null;
self.namespace = parseInt(document.body.className.substring("ns-".length));
})();
} };
//======================================================================
//## core/URLs.js
/** generates mediawiki-urls */
URLs = {
/** User:Name */
userHome: function(user) {
var title = Page.userNS + ":" + user;
return Page.readURL(title);
},
/** User_talk:Name */
userTalk: function(user) {
var title = Page.userTalkNS + ":" + user;
return Page.readURL(title);
},
/** User:Name/Subtitle */
userPage: function(user, subTitle) {
var title = Page.userNS + ":" + user;
return Page.readURL(title, null, subTitle);
},
/** Special:Emailuser/Name */
userEmail: function(user) {
var title = Page.specialNS + ":Emailuser";
return Page.readURL(title, null, user);
},
/** Special:Contributions/Name */
userContribs: function(user) {
var title = Page.specialNS + ":Contributions";
return Page.readURL(title, null, user);
},
/** Special:Blockip/Name */
userBlock: function(user) {
var title = Page.specialNS + ":Blockip";
return Page.readURL(title, null, user);
},
/** ?title=Spezial:Log&type=block&user=&page=User%3AName */
userBlocklog: function(user) {
var title = Page.specialNS + ":Log";
var victim = Page.userNS + ":" + user;
return Page.actionURL({
title: title,
type: "block",
user: "",
page: victim
});
},
/** ?title=Special:Log&user=user */
userAllLogs: function(user) {
var title = Page.specialNS + ":Log";
return Page.actionURL({
title: title,
user: user
});
},
//------------------------------------------------------------------------------
/** ?title=Title&oldid=oldid&action=edit */
pageEdit: function(title, oldid) {
if (!oldid) oldid = null; // oldid is optional
return Page.actionURL({
title: Page.lemma,
oldid: oldid,
action: "edit",
});
},
/** ?title=Special:Newpages&limit=limit */
newPages: function(limit) {
if (!limit) limit = 10; // limit is optional
var title = Page.specialNS + ":Newpages";
return Page.actionURL({
title: title,
limit: 20
// , inline: "yes"
});
},
/** ?title=Special:Log */
allLogs: function() {
var title = Page.specialNS + ":Log";
return Page.actionURL({
title: title,
});
},
};
//======================================================================
//## core/Ajax.js
/** ajax helper functions */
Ajax = {
/** headers preset for POSTs */
urlEncoded: function(charset) { return {
"Content-Type": "application/x-www-form-urlencoded; charset=" + charset
}},
/** headers preset for POSTs */
multipartFormData: function(boundary, charset) { return {
"Content-Type": "multipart/form-data; boundary=" + boundary + "; charset=" + charset
}},
/** encode an Object or Array into URL parameters. */
encodeArgs: function(args) {
if (!args) return "";
var query = "";
for (var arg in args) {
var key = encodeURIComponent(arg);
var raw = args[arg];
if (raw == null) continue;
var value = encodeURIComponent(raw.toString());
query += "&" + key + "=" + value;
}
if (query == "") return "";
return query.substring(1);
},
/** encode form data as multipart/form-data */
encodeFormData: function(boundary, data) {
var out = "";
for (name in data) {
var raw = data[name];
if (raw == null) continue;
out += '--' + boundary + '\r\n';
out += 'Content-Disposition: form-data; name="' + name + '"\r\n\r\n';
out += raw.toString() + '\r\n';
}
out += '--' + boundary + '--';
return out;
},
/** create and use an XMLHttpRequest with named parameters */
call: function(args) {
// create
var client = new XMLHttpRequest();
client.args = args;
// open
client.open(
args.method ? args.method : "GET",
args.url,
args.async ? args.async == true : true
);
// set headers
if (args.headers) {
for (var name in args.headers) {
client.setRequestHeader(name, args.headers[name]);
}
}
// handle state changes
client.onreadystatechange = function() {
if (args.state) args.state(client, args);
if (client.readyState != 4) return;
if (args.doneState) args.doneState(client, args);
}
// debug status
client.debug = function() {
return client.status + " " + client.statusText + "\n"
+ client.getAllResponseHeaders() + "\n\n"
+ client.responseText;
}
// and start
client.send(args.body ? args.body : null);
return client;
},
/** parse text into an XML DOM */
parseXML: function(text) {
var parser = new DOMParser();
return parser.parseFromString(text, "text/xml");
},
};
//======================================================================
//## core/Editor.js
/** ajax functions for MediaWiki */
function Editor(progress) {
if (progress) this.indicator = this.progressIndicator(progress);
else this.indicator = this.quietIndicator;
}
Editor.prototype = {
//------------------------------------------------------------------------------
//## simple actions without form
/** watch or unwatch a page. the doneFunc gets the new state and is optional */
watchedPage: function(title, watch, doneFunc) {
var self = this;
var action = watch ? "watch" : "unwatch";
self.indicator.header(action + "ing " + title);
var url = Page.actionURL({
title: title,
action: action,
});
self.indicator.getting(url);
Ajax.call({
method: "GET",
url: url,
doneState: function(source) {
if (source.status != 200) {
self.indicator.failed(source, 200);
return;
}
if (doneFunc) doneFunc(watch);
self.indicator.finished();
},
});
},
//------------------------------------------------------------------------------
//## complex actions with form
/** add text to the start of a page, the separator is optional */
prependText: function(title, text, summary, separator, doneFunc) {
this.indicator.header("prepending to " + title);
this.action(
// section=0 does _not_ insert the summary as a headline
{ title: title, action: "edit", section: 0 },
200,
"editform",
function(f) {
var oldText = f.wpTextbox1.value.replace(/^[\r\n]+$/, "");
var newText = concatSeparated(text, separator, oldText);
return {
wpSection: f.wpSection.value,
wpStarttime: f.wpStarttime.value,
wpEdittime: f.wpEdittime.value,
wpScrolltop: f.wpScrolltop.value,
wpSummary: summary,
wpWatchthis: f.wpWatchthis.checked ? "1" : null,
wpMinoredit: f.wpMinoredit.checked ? "1" : null,
wpSave: f.wpSave.value,
wpEditToken: f.wpEditToken.value,
wpTextbox1: newText,
};
},
200,
doneFunc
);
},
/** add text to the end of a spage, the separator is optional */
appendText: function(title, text, summary, separator, doneFunc) {
this.indicator.header("appending to " + title);
this.action(
// section=new inserts the summary as a headline
{ title: title, action: "edit" },
200,
"editform",
function(f) {
var oldText = f.wpTextbox1.value.replace(/^[\r\n]+$/, "");
var newText = concatSeparated(oldText, separator, text);
return {
wpSection: f.wpSection.value,
wpStarttime: f.wpStarttime.value,
wpEdittime: f.wpEdittime.value,
wpScrolltop: f.wpScrolltop.value,
wpSummary: summary,
wpWatchthis: f.wpWatchthis.checked ? "1" : null,
wpMinoredit: f.wpMinoredit.checked ? "1" : null,
wpSave: f.wpSave.value,
wpEditToken: f.wpEditToken.value,
wpTextbox1: newText,
};
},
200,
doneFunc
);
},
/** delete a page. if the reason is null, the original reason text is deleted */
deletePage: function(title, reason, doneFunc) {
this.indicator.header("deleting " + title);
this.action(
{ title: title, action: "delete" },
200,
"deleteconfirm",
function(f) {
var separator = " - "; //### hardcoded
var rs = reason != null
? concatSeparated(reason, separator, f.wpReason.value)
: "";
return {
wpReason: rs,
wpConfirmB: f.wpConfirmB.value,
wpEditToken: f.wpEditToken.value,
};
},
200,
doneFunc
);
},
/** block a user, anonOnly defaults to false, createAccounts defaults to true */
blockUser: function(user, duration, reason, anonOnly, createAccount, doneFunc) {
this.indicator.header("blocking " + user);
var title = Page.specialNS + ":Blockip";
this.action(
{ title: title, target: user },
200,
"blockip",
function(f) { return {
wpBlockAddress: user,
wpBlockReason: reason,
wpAnonOnly: anonOnly ? "1" : null,
wpCreateAccount: createAccount ? "1" : null,
wpBlockExpiry: f.wpBlockExpiry.value,
wpBlockOther: duration,
wpEditToken: f.wpEditToken.value,
wpBlock: f.wpBlock.value,
}},
200,
doneFunc
);
},
/** move a page */
movePage: function(oldTitle, newTitle, reason, withDiscussion, doneFunc) {
this.indicator.header("moving " + oldTitle + " to " + newTitle);
var title = Page.specialNS + ":Movepage";
this.action(
{ title: title, target: oldTitle },
200,
"movepage",
function(f) { return {
wpOldTitle: oldTitle,
wpNewTitle: newTitle,
wpReason: reason,
wpMovetalk: withDiscussion ? "1" : null,
wpEditToken: f.wpEditToken.value,
wpMove: f.wpMove.value,
}},
200,
doneFunc
);
},
/**
* change a page's protection state
* allowed values for the levels are "", "autoconfirmed" and "sysop"
*/
protectPage: function(title, levelEdit, levelMove, reason, doneFunc) {
this.indicator.header("protecting " + title);
this.action(
{ title: title, action: "protect" },
200,
0, // this form does not have a name. arrgh.
function(f) {
return {
// nice names. second arrgh.
"mwProtect-level-edit": levelEdit,
"mwProtect-level-move": levelMove,
"mwProtect-reason": reason, // f["mwProtect-reason"].value,
wpEditToken: f.wpEditToken.value,
};
},
200,
doneFunc
);
},
//------------------------------------------------------------------------------
//## action helper
/**
* get a form, change it, post it.
* makeData gets form.elements to create a Map
* the doneFunc is called afterwards and may be left out
*/
action: function(actionArgs, expectedGetStatus,
formName, makeData, expectedPostStatus,
doneFunc) {
function phase1() {
var url = Page.actionURL(actionArgs);
self.indicator.getting(url);
Ajax.call({
method: "GET",
url: url,
doneState: phase2,
});
}
function phase2(source) {
if (expectedGetStatus && source.status != expectedGetStatus) {
self.indicator.failed(source, expectedGetStatus);
return;
}
var doc = Ajax.parseXML(source.responseText);
var form = self.findForm(doc, formName);
if (form == null) { self.indicator.missingForm(source, formName); return; }
var url = form.action;
var data = makeData(form.elements);
var headers = Ajax.urlEncoded("UTF-8"); // Ajax.multipartFormData(boundary, "UTF-8")
var body = Ajax.encodeArgs(data); // Ajax.encodeFormData(boundary, data)
// bug #246651 - is this still open?
//headers["Connection"] = "close";
//var boundary = "134j5hkvgnarw4t82raflfjl3aklsjdfhsdlkhflkqe";
//var headers = Ajax.multipartFormData(boundary, "UTF-8")
//var body = Ajax.encodeFormData(boundary, data)
self.indicator.posting(url);
Ajax.call({
method: "POST",
url: url,
headers: headers,
body: body,
doneState: phase3,
});
}
function phase3(source) {
if (expectedPostStatus && source.status != expectedPostStatus) {
self.indicator.failed(source, expectedPostStatus);
return;
}
self.indicator.finished();
if (doneFunc) doneFunc();
}
var self = this;
phase1();
},
/** finds a HTMLForm within an XMLDocument (!) */
findForm: function(doc, nameOrIdOrIndex) {
// firefox does _not_ provide document.forms,
// but within the form we get HTMLInputElements (!)
var forms = doc.getElementsByTagName("form");
if (typeof nameOrIdOrIndex == "number") {
if (nameOrIdOrIndex >= 0
&& nameOrIdOrIndex < forms.length) return forms[nameOrIdOrIndex];
else return null;
}
for (var i=0; i<forms.length; i++) {
var form = forms[i];
if (this.elementNameOrId(form) == nameOrIdOrIndex) return form;
}
return null;
},
/** finds the name or id of an element */
elementNameOrId: function(element) {
return element.name ? element.name
: element.id ? element.id
: null;
},
//------------------------------------------------------------------------------
//## progress
/** progress indicator */
progressIndicator: function(progress) {
return {
header: function(text) { progress.header(text); },
getting: function(url) { progress.body("getting " + url); },
posting: function(url) { progress.body("posting " + url); },
finished: function() { progress.body("done"); progress.fade(); },
missingForm: function(client, name) { progress.body("form not found: " + name); },
failed: function(client, expectedStatus) {
progress.body(
client.args.method + " " + client.args.url + " "
+ client.status + " " + client.statusText + " "
+ " (expected " + expectedStatus + ")"
);
},
};
},
/** progress indicator */
quietIndicator: {
header: function(text) {},
body: function(text) {},
getting: function(url) {},
posting: function(url) {},
finished: function() {},
missingForm: function(client, name) { throw "form not found: " + name; },
failed: function(client, expectedStatus) { throw "status unexpected\n" + client.debug(); }
},
};
//======================================================================
//## ui/ProgressArea.js
/** uses a messageArea to display ajax progress */
function ProgressArea() {
var close = closeButton(this.destroy.bind(this));
var headerDiv = document.createElement("div");
headerDiv.className = "progress-header";
var bodyDiv = document.createElement("div");
bodyDiv.className = "progress-body";
var outerDiv = document.createElement("div");
outerDiv.className = "progress-area";
outerDiv.appendChild(close);
outerDiv.appendChild(headerDiv);
outerDiv.appendChild(bodyDiv);
var mainDiv = $('progress-global');
if (mainDiv == null) {
mainDiv = document.createElement("div");
mainDiv.id = 'progress-global';
mainDiv.className = "progress-global";
//var bc = $('bodyContent');
//bc.insertBefore(mainDiv, bc.firstChild);
pasteBefore('bodyContent', mainDiv);
}
mainDiv.appendChild(outerDiv);
this.headerDiv = headerDiv;
this.bodyDiv = bodyDiv;
this.outerDiv = outerDiv;
}
ProgressArea.prototype = {
/** destructor, called by fade */
destroy: function() {
removeNode(this.outerDiv);
},
/** display a header text */
header: function(content) {
removeChildren(this.headerDiv);
pasteEnd(this.headerDiv, content);
},
/** display a body text */
body: function(content) {
removeChildren(this.bodyDiv);
pasteEnd(this.bodyDiv, content);
},
/** fade out */
fade: function() {
var self = this;
setTimeout(function() { self.destroy(); }, 1500);
},
};
//======================================================================
//## ui/closeButton.js
/** creates a close button calling a function on click */
function closeButton(closeFunc) {
var button = document.createElement("input");
button.type = "submit";
button.value = "x";
button.className = "closeButton";
if (closeFunc) button.onclick = closeFunc;
return button;
}
//======================================================================
//## ui/Action.js
/** creates links */
Action = {
/**
* create an action link which
* - onclick queries a text or
* - oncontextmenu opens a popup with default texts
* and calls a single-argument function with it.
* the groups are an Array of preset string Arrays.
* a separator is placed between rows.
*/
promptPopupLink: function(label, query, groups, func) {
// the main link calls back with a prompted reason
var mainLink = this.promptLink(label, query, func);
// create the menu
var menu = document.createElement("div");
menu.className = "popup-menu hidden";
/** add a preset link to the menu */
function addPreset(preset) {
var link = self.functionLink(preset, null);
var item = document.createElement("div");
item.className = "popup-menu-item";
item.preset = preset; // user data
item.appendChild(link);
menu.appendChild(item);
}
/** add a separator to the menu */
function addSeparator() {
var separator = document.createElement("hr");
separator.className = "popup-menu-separator";
menu.appendChild(separator);
}
// setup groups of items
var self = this;
for (var i=0; i<groups.length; i++) {
var group = groups[i]; // maybe skip null groups
if (i != 0) addSeparator();
for (var j=0; j<group.length; j++) {
var preset = group[j];
addPreset(preset);
}
}
//### is displayed at the wrong position if not inserted at document root
//mainLink.appendChild(menu);
document.body.appendChild(menu);
// intialize popup
function selected(item) { func(item.preset); }
popup(mainLink, menu, selected, null);
return mainLink;
},
/** create an action link which onclick queries a text and calls a function with it */
promptLink: function(label, query, func) {
return this.functionLink(label, function() {
var reason = prompt(query);
if (reason != null) func(reason);
});
},
/** create an action link calling a function on click */
functionLink: function(label, func) {
var a = document.createElement("a");
a.className = "functionLink";
a.onclick = func;
a.textContent = label;
return a;
},
/** create a link to an url within the current list item */
urlLink: function(label, url) {
var a = document.createElement("a");
a.href = url;
a.textContent = label;
return a;
},
};
//======================================================================
//## ui/Portlet.js
/** create a portlet which has to be initialized with either createNew or useExisting */
function Portlet() {}
Portlet.prototype = {
//------------------------------------------------------------------------------
//## initialization
/** create a new portlet, but do not yet display */
createNew: function(id) {
this.outer = document.createElement("div");
this.outer.id = id;
this.outer.className = "portlet";
this.header = document.createElement("h5");
this.body = document.createElement("div");
this.body.className = "pBody";
this.outer.appendChild(this.header);
this.outer.appendChild(this.body);
this.ul = null;
this.li = null;
return this;
},
/** init from a MediaWiki-provided portlet and hide it */
useExisting: function(id) {
this.outer = $(id);
this.header = descendants(this.outer, "h5", null, 0);
this.body = descendants(this.outer, "div", "pBody", 0);
this.ul = descendants(this.body, "ul", null, 0);
this.li = null;
removeNode(this.outer);
return this;
},
/**
* display in the sidebar.
* has to be called after useExisting and createNew and after
* all other methods. this is way faster than messing with nodes
* connected to the DOM-tree
*/
show: function() {
SideBar.appendPortlet(this.outer);
return this;
},
//------------------------------------------------------------------------------
//## properties
/** get the header text */
getTitle: function() {
return this.header.textContent;
},
/** set the header text */
setTitle: function(text) {
this.header.textContent = text;
return this;
},
/** get the inner node */
getInner: function() {
return this.body.firstChild;
},
/** set the inner node */
setInner: function(newChild) {
removeChildren(this.body);
this.body.appendChild(newChild);
return this;
},
//------------------------------------------------------------------------------
//## content builing
/**
* render an array of arrays of links.
* the outer array may contains strings to steal list items
* null items in the outer array are legal and skipped
*/
build: function(rows) {
this.list();
for (var y=0; y<rows.length; y++) {
var row = rows[y];
// null rows are silently skipped
if (row == null) continue;
// String objects are ids of elements to be stolen
if (row.constructor == String) {
this.steal(row);
continue;
}
this.item();
for (var x=0; x<row.length; x++) {
var cell = row[x];
if (x > 0) this.space();
this.link(cell);
}
}
return this;
},
//------------------------------------------------------------------------------
//## private
/** make a list */
list: function() {
if (this.ul) return this;
this.ul = document.createElement("ul");
this.setInner(this.ul);
return this;
},
/** insert a list item, content is optional */
item: function(content) {
this.li = document.createElement("li");
if (content)
this.li.appendChild(content);
this.ul.appendChild(this.li);
return this;
},
/** steal a list item from another portlet */
steal: function(id) {
var element = $(id);
if (!element) return this;
removeNode(element);
this.ul.appendChild(element);
return this;
},
/** append a link element created by the Action */
link: function(link) {
this.li.appendChild(link);
return this;
},
/** create a small space within the current list item */
space: function() {
var s = document.createTextNode(" ");
this.li.appendChild(s);
return this;
},
};
//======================================================================
//## ui/SideBar.js
/** encapsulates column-one */
SideBar = {
//------------------------------------------------------------------------------
//## public methods
/** change labels of item links. data is an Array of name/label Arrays */
labelItems: function(data) {
function sa(action, text) {
var el = $(action);
if (!el) return;
var a = el.getElementsByTagName("a")[0];
if (!a) return;
a.textContent = text;
}
for (var i=0; i<data.length; i++) {
sa(data[i][0], data[i][1]);
}
},
/** remove the go button, i want to search */
removeSearchGoButton: function() {
var node = document.forms['searchform'].elements['fulltext'];
removeNode(node);
},
/** move p-cactions out of column-one so it does not inherit its position:fixed */
unfixCactions: function() {
var pCactions = $('p-cactions');
var columnContent = $('column-content');
pCactions.parentNode.removeChild(pCactions);
columnContent.insertBefore(pCactions, columnContent.firstChild);
},
/** insert a select box to replace the pLang portlet */
langSelect: function() {
var pLang = $('p-lang');
if (!pLang) return;
var select = document.createElement("select");
select.id = "langSelect";
select.options[0] = new Option(SideBar.msg.select, "");
var list = pLang.getElementsByTagName("a");
for (var i=0; i<list.length; i++) {
var a = list[i];
select.options[i+1] = new Option(a.firstChild.textContent, a.href);
}
select.onchange = function() {
var selected = this.options[this.selectedIndex].value;
if (selected == "") return;
location.href = selected;
}
// replace portlet contents
this.usePortlet('p-lang').setInner(select).show();
},
//------------------------------------------------------------------------------
/** return a portlet object for an existing portlet */
usePortlet: function(id) {
return new Portlet().useExisting(id);
},
/** create a new portlet object */
newPortlet: function(id) {
return new Portlet().createNew(id);
},
/** append a portlet to column-one, called by Portlet.show */
appendPortlet: function(portlet) {
var columnOne = $('column-one');
columnOne.appendChild(portlet);
// navigation.parentNode.insertBefore(search, navigation);
},
/** move a single or multiple (shown) portlets portlets to the end of the toolbar */
moveDown: function(id) {
if (id.constructor == Array) {
for (var i=0; i<id.length; i++) {
this.moveDown(id[i]);
}
return;
}
var element = $(id);
if (!element) return;
removeNode(element);
this.appendPortlet(element);
},
};
SideBar.msg = {
select: "auswählen",
};
//======================================================================
//## ui/FoldButton.js
/** FoldButton class */
function FoldButton(initiallyOpen, reactor) {
var self = this;
this.button = document.createElement("span");
this.button.className = "folding-button";
this.button.onclick = function() { self.flip(); }
this.open = initiallyOpen ? true : false;
this.reactor = reactor;
this.display();
}
FoldButton.prototype = {
/** flip the state and tell the reactor */
flip: function() {
this.change(!this.open);
return this;
},
/** change state and tell the reactor when changed */
change: function(open) {
if (open == this.open) return;
this.open = open;
if (this.reactor) this.reactor(open);
this.display();
return this;
},
/** change the displayed state */
display: function() {
this.button.innerHTML = this.open
? "▼"
: "►";
return this;
},
};
//======================================================================
//## ui/popup.js
/** display a prefab popup menu */
function popup(sourceOrId, menuOrId, selectListener, abortListener) {
// init
var menu = typeof menuOrId == "string" ? document.getElementById(menuOrId) : menuOrId;
var source = typeof sourceOrId == "string" ? document.getElementById(sourceOrId) : sourceOrId;
// initially hide popup menu
menu.className = "popup-menu hidden";
var visible = false;
//------------------------------------------------------------------------------
//## actions
/** show the popup at mouse position */
function showMenu(mouse) {
//TODO: does not work whith an arbitrary parent, the menu must be top-level in the document
var leftPos = window.pageXOffset + mouse.x;
var topPos = window.pageYOffset + mouse.y;
var rightEdge = window.innerWidth - mouse.x;
var bottomEdge = window.innerHeight - mouse.y;
if (menu.offsetWidth + mouse.x > window.innerWidth) leftPos -= menu.offsetWidth;
if (menu.offsetHeight + mouse.y > window.innerHeight) topPos -= menu.offsetHeight;
menu.style.left = leftPos + "px";
menu.style.top = topPos + "px";
menu.className = className(menu.className, "popup-menu visible", "hidden");
visible = true;
}
/** hide the popup */
function hideMenu() {
menu.className = className(menu.className, "popup-menu hidden", "visible");
visible = false;
}
/** the user selected an item */
function selectItem(item) {
hideMenu();
if (selectListener) selectListener(item, menu, source);
}
/** the user possibly aborted selection */
function finish() {
if (!visible) return;
hideMenu();
if (abortListener) abortListener(menu, source);
}
//------------------------------------------------------------------------------
//## wiring
function maybeSelectItem(ev) {
// target is within the item
// currentTarget is the menu
// this is ???
var target = ev.target;
for (;;) {
if (target.className
&& target.className.search(/\bpopup-menu-item\b/) != -1) {
selectItem(target);
return;
}
target = target.parentNode;
if (!target) return;
}
}
source.oncontextmenu = function(ev) {
showMenu({ x: ev.clientX, y: ev.clientY});
return false;
}
document.addEventListener("click",
function(ev) {
if (visible) finish();
return false;
},
false
);
menu.onclick = function(ev) {
maybeSelectItem(ev);
return false;
}
menu.onmouseup = function(ev) {
if (ev.button == 2) maybeSelectItem(ev);
return false;
}
}
//======================================================================
//## Template.js
/** puts templates into the current page */
Template = new function() {
/** return an Array of links to actions for normal pages */
this.allPageActions = function(lemma) {
var msg = Template.msg;
return [
Action.promptLink(msg.qs.label, msg.qs.prompt, function(reason) { qs(lemma, reason); }),
Action.promptLink(msg.la.label, msg.la.prompt, function(reason) { la(lemma, reason); }),
Action.promptLink(msg.sla.label, msg.sla.prompt, function(reason) { sla(lemma, reason); }),
];
};
/** return an Array of links for userTalkPages */
this.userTalkPageActions = function(user, userTemplateNames) {
var out = new Array();
out.push(this.doTest(user));
if (userTemplateNames) {
for (var i=0; i<userTemplateNames.length; i++) {
out.push(this.doPersonal(user, userTemplateNames[i]));
}
}
return out;
}
/** link inserting Vorlage:Test */
this.doTest = function(user) {
var lemma = Page.userTalkNS + ":" + user;
return Action.functionLink("Test", function() { test(lemma); });
};
/** link inserting User:FooBar/name template */
this.doPersonal = function(user, name) {
var lemma = Page.userTalkNS + ":" + user;
return Action.functionLink(name, function() { personal(lemma, name); });
};
//------------------------------------------------------------------------------
//## text constants
var r = {
template: function(title) { return "{" + "{" + title + "}" + "}"; },
link: function(title) { return "[" + "[" + title + "]" + "]"; },
link2: function(title, label) { return "[" + "[" + title + "|" + label + "]" + "]"; },
header: function(text) { return "==" + text + "=="; },
dash: "--", // "—" em dash U+2014 —
sig: "~~" + "~~",
sigapp: " -- ~~" + "~~\n",
line: "----",
sp: " ",
lf: "\n",
};
//------------------------------------------------------------------------------
//## append templates without a reason
/** returns a function that puts an Test template into an article */
function test(lemma) {
var template = "subst:Test";
var editor = new Editor(new ProgressArea());
editor.appendText(
lemma,
r.template(template) + r.sp + r.sig + r.lf,
r.template(template),
r.line + r.lf
);
}
/** returns a function that puts a named user template into an article */
function personal(lemma, name) {
var template = "subst:" + Page.userNS + ":" + Page.user + "/" + name;
var editor = new Editor(new ProgressArea());
editor.appendText(
lemma,
r.template(template) + r.sigapp,
name,
r.line + r.lf
);
}
//------------------------------------------------------------------------------
//## prepend wikipedia templates with a reason
/** puts an QS template into an article */
function qs(lemma, reason) {
enlist(lemma, "subst:QS", "Wikipedia:Qualitätssicherung", reason);
}
/** puts an LA template into an article */
function la(lemma, reason) {
enlist(lemma, "subst:Löschantrag", "Wikipedia:Löschkandidaten", reason);
}
/** puts an SLA template into an article */
function sla(lemma, reason) {
stamp(lemma, "löschen", reason);
}
/** put template in a page */
function stamp(title, template, reason) {
var editor = new Editor(new ProgressArea());
editor.prependText(
title,
r.template(template) + r.sp + reason + r.sigapp,
r.template(template) + r.sp + reason,
r.line + r.lf
);
}
/** list page on a list page */
function enlist(title, template, listPage, reason) {
stamp(title, template, reason);
var editor = new Editor(new ProgressArea());
editor.appendText(
listPage + "/" + currentDate(),
r.header(r.link(title)) + r.lf + reason + r.sigapp,
r.link(title) + r.sp + r.dash + r.sp + reason,
r.lf
);
}
/** returns the current date in the format the LKs are organized */
function currentDate() {
var months = [ "Januar", "Februar", "März", "April", "Mai", "Juni", "Juli",
"August", "September", "Oktober", "November", "Dezember" ];
var now = new Date();
var year = now.getYear();
if (year < 999) year += 1900;
return now.getDate() + ". " + months[now.getMonth()] + " " + year;
}
};
Template.msg = {
qs: {
label: "qs",
prompt: "warum qs?",
},
la: {
label: "la",
prompt: "warum la?",
},
sla: {
label: "sla",
prompt: "warum sla?",
},
};
//======================================================================
//## FastDelete.js
/** one-click delete */
FastDelete = {
/** returns a link which prompts or popups reasons and then deletes */
doDeletePopup: function(title) {
var self = this;
var msg = FastDelete.msg;
return Action.promptPopupLink(msg.label, msg.prompt, msg.reasons, function(reason) {
self.fastDelete(title, reason);
});
},
/** return whole banks of delete links in an array of arrays */
doDeleteBanks: function(title) {
var inp1 = this.msg.reasons;
var out1 = new Array();
for (var i=0; i<inp1.length; i++) {
var inp2 = inp1[i];
var out2 = new Array();
for (var j=0; j<inp2.length; j++) {
var reason = inp2[j];
out2.push(this.doDeleteLink(title, reason));
}
out1.push(out2);
}
return out1;
},
/** return a single delete link */
doDeleteLink: function(title, reason) {
var self = this;
return Action.functionLink(reason, function() {
self.fastDelete(title, reason);
});
},
/** delete an article with a reason*/
fastDelete: function(title, reason) {
var editor = new Editor(new ProgressArea());
editor.deletePage(title, reason);
},
};
FastDelete.msg = {
label: "wech",
prompt: "warum löschen?",
reasons: [
[ "schrott", "kein artikel",
"irrelevant", "unfug",
"tastaturtest", "zu mager",
"linkcontainer", "werbung",
],
[ "wörterbucheintrag",
"falsche sprache",
"falsches lemma",
"unnötiger redirect",
"versehen",
],
[ "veraltet", "erledigt"
],
],
};
//======================================================================
//## FastBlock.js
/** one-click user block, the hard way */
FastBlock = {
/** returns a link which prompts a reason and then kills a user */
doUserKill: function(user) {
var self = this;
var msg = FastBlock.msg;
return Action.promptLink(msg.label, msg.prompt, function(reason) {
self.killUser(user, reason);
});
},
/** does everythin necessary to block a user indefinitely */
killUser: function(user, reason) {
this.blockIndefinite(user, reason);
this.modifyPage(Page.userNS + ":" + user, reason);
this.modifyPage(Page.userTalkNS + ":" + user, reason);
},
/** blocks a user for an indefinite duration */
blockIndefinite: function(user, reason) {
var duration = "indefinite";
var editor = new Editor(new ProgressArea());
editor.blockUser(user, duration, reason, false, true);
},
/** inserts the template in a page and protects it */
modifyPage: function(title, reason) {
var self = this;
var msg = FastBlock.msg;
var editor = new Editor(new ProgressArea());
var text = msg.template + " " + reason + msg.sigapp;
editor.prependText(title, text, msg.template, msg.separator, function() {
var level = "sysop";
var editor = new Editor(new ProgressArea());
editor.protectPage(title, level, level, msg.template);
});
},
};
FastBlock.msg = {
label: "Killen",
prompt: "warum killen?",
template: "{{gesperrter Benutzer}}",
sigapp: " -- ~~" + "~~\n",
separator: "\n----\n",
//standardReason: "vandalismus",
};
//======================================================================
//## FoldHeaders.js
/** manages folding with a button */
FoldHeaders = {
/** onload initializer */
init: function() {
if (Page.params["action"]) return;
if (Page.params["diff"]) return;
if ($('autofold')) this.install();
},
/** a link folding the current page */
doFold: function() {
return Action.functionLink(FoldHeaders.msg.fold, this.install.bind(this));
},
//------------------------------------------------------------------------------
/** provide an icon for each heading to fold or unfold it */
install: function() {
/** a folder attaching a folder span to a header. end is inclusive. returns a Folder */
function Folder(header, end, even) {
// make body
var body = document.createElement("div");
body.className = "foldHeaders folding-body";
body.style.display = "none";
// fill body
var range = document.createRange();
range.setStartAfter(header);
range.setEndAfter(end);
var contents = range.extractContents();
body.appendChild(contents);
// clear body
var clear = document.createElement("div");
clear.className = "visualClear";
body.appendChild(clear);
// remove the header
var targetParent = header.parentNode;
var targetSibling = header.previousSibling;
targetParent.removeChild(header);
// create a FoldButton
var self = this;
var foldButton = new FoldButton(false, function(open) {
header.style.marginBottom = open ? null : "0";
body.style.display = open ? null : "none";
// fold children injected by recurse
if (self.children) {
for (var i=0; i<self.children.length; i++) {
self.children[i].foldButton.change(open);
}
// HACK: remove children, so only the first open is executed
self.children = null;
}
});
// change the header
header.className = "foldHeaders folding-header";
header.insertBefore(foldButton.button, header.firstChild);
// insert filled container instead of the header
var container = document.createElement("div");
container.className = "foldHeaders folding-container "
+ (even ? "folding-even" : "folding-odd");
container.appendChild(header);
container.appendChild(body);
// replace the old header
if (targetSibling) targetParent.insertBefore(container, targetSibling.nextSibling);
else targetParent.appendChild(container);
this.body = body;
this.foldButton = foldButton;
}
/** attach folders to all header tags of a given depth and below. parent is given children, if present. */
function recurse(parent, depth) {
var element = parent.body ? parent.body : parent;
var tagName = "H" + depth;
var even = depth % 2 == 0;
// inserts a synthetic header if a lower-level header after an editsection is found
function fake() {
// find first editsection
var maybe = descendants(element, "div", "editsection", 0);
if (!maybe) return;
// find link paragraph
maybe = nextElement(maybe);
if (!maybe) return;
if (maybe.nodeName != "P") return;
// find header tag
maybe = nextElement(maybe);
var re = new RegExp("H[" + (depth+1) + "-7]");
if (!re(maybe.nodeName)) return;
var faked = document.createElement(tagName);
var target = element.firstChild;
// hack: in level 2, skip the contentsub
if (depth == 2) target = $('contentSub').nextSibling;
element.insertBefore(faked, target);
}
fake();
var headers = element.getElementsByTagName(tagName);
var folders = new Array();
for (var i=0; i<headers.length; i++) {
var header = headers[i];
// skip some headers we do not want to modify
if (depth == 3 && header.id == "siteSub") continue;
//if (header.parentNode.id == "toctitle") continue;
// TODO: remove <h2>Aktuelle Version</h2>
var end = i < headers.length-1
? contentEnd(headers[i+1])
: element.lastChild;
// in level2 we may need to use #catliks instead of element.lastChild
if (!end) continue;
var folder = new Folder(header, end, even);
if (depth < 6) recurse(folder, depth+1);
folders.push(folder);
}
// inject children into Folder object
if (parent.body) parent.children = folders;
return folders;
}
/** (backwards) find a (inclusive) endpoint for folder content */
function contentEnd(next) {
var maybe = next;
// skip previous paragraph
maybe = previousElement(maybe); if (!maybe) return null;
if (maybe.nodeName != "P") return next.previousSibling;
// skip editsection div
maybe = previousElement(maybe); if (!maybe) return null;
if (maybe.nodeName != "DIV"
|| maybe.className != "editsection") return next.previousSibling;
return maybe.previousSibling;
}
var bodyContent = $('bodyContent');
// prevent double installation
if (bodyContent.foldersInstalled) return;
bodyContent.foldersInstalled = true;
// rip out toc
var toc = $('toc');
if (toc) removeNode(toc); // toc.style.display = "none";
// attach folders at level 2
var pageFolders = recurse(bodyContent, 2);
// attach a meta FoldButton for the page
var pageTitle = descendants('content', "h1", "firstHeading", 0);
var pageButton = new FoldButton(false, function(open) {
for (var i=0; i<pageFolders.length; i++) {
pageFolders[i].foldButton.change(open);
}
});
pageTitle.insertBefore(pageButton.button, pageTitle.firstChild);
},
}
FoldHeaders.msg = {
fold: "fold",
};
//======================================================================
//## Usermessage.js
/** changes usermessages */
Usermessage = {
/** onload initializer */
init: function() {
this.historyLink();
},
/** modify the new usermessages to contain only a link to the history of the talkpage */
historyLink: function() {
var um = descendants('bodyContent', "div", "usermessage", 0);
var a = descendants(um, "a", null, 1);
if (!a) return;
a.href = a.href.replace(/&diff=cur$/, "&action=history");
a.textContent = Usermessage.msg.history;
}
};
Usermessage.msg = {
history: "History",
};
//======================================================================
//## UserPage.js
/** cares for pages below the user namespace */
UserPage = {
/** create bank of readLinks to private pages */
doGotoBank: function() {
var names = UserPage.msg.pages;
function addLink(name) {
var link = Action.urlLink(name, URLs.userPage(Page.user, name));
out.push(link);
}
var out = new Array();
for (var i=0; i<names.length; i++) addLink(names[i]);
return out;
},
};
UserPage.msg = {
pages: [ "new", "tmp", "todo", "test" ],
};
//======================================================================
//## UserBookmarks.js
/** manages a personal bookmarks page */
UserBookmarks = {
/** return an Array of links for a lemma */
actions: function(lemma) {
return [ this.doView(), this.doMark(lemma) ];
},
/** return the absolute page link */
doView: function() {
return Action.urlLink(UserBookmarks.msg.view, URLs.userPage(Page.user, this.PAGE_TITLE));
},
/** add a bookmark on a user's bookmark page. if the page is left out, the current is added */
doMark: function(lemma) {
var self = this;
var msg = UserBookmarks.msg;
return Action.promptPopupLink(msg.add, msg.prompt, msg.reasons, function(reason) {
if (lemma) self.arbitrary(reason, lemma);
else self.current(reason);
});
},
//------------------------------------------------------------------------------
/** user page name */
PAGE_TITLE: "bookmarks",
/** add a bookmark on a user's bookmark page */
current: function(remark) {
var lemma = Page.lemma;
var mode = "perma";
var perma = Page.perma;
if (!perma) {
var params = Page.params;
var oldid = params["oldid"];
var target = params["target"];
if (oldid) {
var diff = params["diff"];
if (diff) {
mode = "diff";
if (diff == "prev"
|| diff == "next"
|| diff == "next"
|| diff == "cur") mode = diff;
else
if (diff == "cur"
|| diff == "0") mode = "cur";
perma = Page.actionURL({ title: lemma, oldid: oldid, diff: diff});
}
else {
mode = "old";
perma = Page.actionURL({ title: lemma, oldid: oldid});
}
}
}
var text = "*[[:" + lemma + "]]";
if (perma) text += " <small>[" + perma + " " + mode + "]</small>";
if (remark) text += " " + remark;
text += "\n";
this.prepend(text);
},
/** add a bookmark for an arbitrary page */
arbitrary: function(remark, lemma) {
var text = "*[[:" + lemma + "]]";
if (remark) text += " " + remark;
text += "\n";
this.prepend(text);
},
/** add text to the bookmarks page */
prepend: function(text) {
var title = Page.userNS + ":" + Page.user + "/" + this.PAGE_TITLE;
var editor = new Editor(new ProgressArea());
editor.prependText(title, text, "");
},
};
UserBookmarks.msg = {
view: "bookmarks",
add: "add",
prompt: "bemerkung?",
reasons: [
[ "wech mager",
"wech kein artikel",
"relevanz?",
"urv?",
],
[ "überarbeiten inhalt",
"überarbeiten form",
],
[ "gesperrt",
"interessant",
"wech bleiben",
"faktencheck",
],
],
};
//======================================================================
//## Communication.js
/** communication with Page.owner */
Communication = {
/** generate banks of links */
doCommunicationBanks: function() {
var msg = Communication.msg;
var talkActions = Template.userTalkPageActions(Page.owner, msg.talkActions);
var userContribsAction = [ Action.urlLink(msg.userContribsAction, URLs.userContribs(Page.owner)) ];
var userRipeAction = [ Action.urlLink(msg.userRipeAction, this.ripeURL(Page.owner)) ];
var userBlocklogAction = [ Action.urlLink(msg.userBlocklogAction, URLs.userBlocklog(Page.owner)) ];
var userBlockAction = [ Action.urlLink(msg.userBlockAction, URLs.userBlock(Page.owner)) ];
var userEmailAction = [ Action.urlLink(msg.userEmailAction, URLs.userEmail(Page.owner)) ];
var userHomeAction = [ Action.urlLink(msg.userHomeAction, URLs.userHome(Page.owner)) ];
var userTalkAction = [ Action.urlLink(msg.userTalkAction, URLs.userTalk(Page.owner)) ];
var userKillAction = [ FastBlock.doUserKill(Page.owner) ];
// remove email for IP-users and ripe for non-IP-users
var ipOwner = this.isIP(Page.owner);
if (ipOwner) userEmailAction = null;
else userRipeAction = null;
return [
talkActions,
userHomeAction,
userTalkAction,
userContribsAction,
userRipeAction,
userBlocklogAction,
userBlockAction,
userEmailAction,
userKillAction,
];
},
/** true when the name String denotes an v4 IP-address */
isIP: function(ip) {
if (!ip.match(/^(\d{1,3}\.){3}\d{1,3}$/)) return false;
var parts = ip.split(/\./);
if (parts.length != 4) return false;
for (var i=0; i<parts.length; i++) {
var byt = parseInt(parts[i]);
if (byt < 0 || byt > 255) return false;
}
return true;
},
/** ripe check URL */
ripeURL: function(ip) {
return "http://www.ripe.net/fcgi-bin/whois?form_type=simple&full_query_string=&&do_search=Search&searchtext=" + ip;
},
};
Communication.msg = {
talkActions: [ "spielen", "genug" ],
userRipeAction: "Ripe",
userBlocklogAction: "Blocklog",
userBlockAction: "Blockieren",
userKillAction: "Killen",
userHomeAction: "Benutzerseite",
userTalkAction: "Diskussion",
userEmailAction: "Anmailen",
userContribsAction: "Beiträge",
};
//======================================================================
//## ActionHistory.js
/** helper for action=history */
ActionHistory = {
/** onload initializer */
init: function() {
if (Page.params["action"] != "history") return;
this.addBlockAndEditLinks();
},
//------------------------------------------------------------------------------
/** add an edit link and a block link every version in a page history */
addBlockAndEditLinks: function() {
function addLink(li) {
var diffInput = descendants(li, "input", null, 1);
if (!diffInput) return;
// gather data
var histSpan = descendants(li, "span", "history-user", 0);
var histA = descendants(histSpan, "a", null, 0);
var dateA = nextElement(diffInput, "a");
var oldid = diffInput.value;
var user = histA.textContent;
var date = dateA.textContent;
// add edit link
var edit = Action.urlLink(ActionHistory.msg.edit, URLs.pageEdit(Page.lemma, oldid));
var before = diffInput.nextSibling; // li.firstChild;
pasteBefore(before, [ " [", edit, "] "]);
// add block link
var block = Action.urlLink(ActionHistory.msg.block, URLs.userBlock(user));
pasteBefore(histSpan, [ " [", block, "] "]);
}
var lis = descendants('pagehistory', "li");
if (!lis) return;
for (var i=0; i<lis.length; i++) {
addLink(lis[i]);
}
},
};
ActionHistory.msg = {
edit: "edit",
block: "blocken",
};
//======================================================================
//## ActionWatch.js
/** page watch and unwatch without reloading the page */
ActionWatch = {
//<li id="ca-unwatch"><a href="/w/index.php?title=Benutzer:D/test&action=unwatch">Nicht mehr beobachten</a></li>
//<li id="ca-watch"><a href="/w/index.php?title=Benutzer:D/test&action=watch">Beobachten</a></li>
init: function() {
/** initialize link */
function initView() {
var watch = $('ca-watch');
var unwatch = $('ca-unwatch');
if (watch) exchangeItem(watch, true);
else if (unwatch) exchangeItem(unwatch, false);
}
/** show we are talking to the server */
function progressView() {
var watch = descendants('ca-watch', "a", null, 0);
var unwatch = descendants('ca-unwatch', "a", null, 0);
if (watch) watch.className = "active";
if (unwatch) unwatch.className = "active";
}
/** talk to the server */
function changeRemote(watched) {
var editor = new Editor(); // new ProgressArea()
editor.watchedPage(Page.lemma, watched, updateView);
}
/** replace link */
function updateView(watched) {
var watch = $('ca-watch');
var unwatch = $('ca-unwatch');
if ( watched && watch ) exchangeItem(watch, false);
if (!watched && unwatch) exchangeItem(unwatch, true);
}
/** create a li with a link in it */
function exchangeItem(target, watchable) {
var li = document.createElement("li");
li.id = watchable ? "ca-watch" : "ca-unwatch";
var label = watchable ? ActionWatch.msg.watch : ActionWatch.msg.unwatch;
var a = Action.functionLink(label, function() {
progressView();
changeRemote(watchable);
});
li.appendChild(a);
target.parentNode.replaceChild(li, target);
}
initView();
},
};
ActionWatch.msg = {
watch: "Beobachten",
unwatch: "Vergessen",
};
//======================================================================
//## SpecialNewpages.js
/** extends Special:Newpages */
SpecialNewpages = {
/** onload initializer */
init: function() {
// early exit, wrong page
if (!Page.isSpecial("Newpages")) return;
//if (Page.params["inline"] != "yes") return;
this.displayInline();
},
/** a link to new pages */
doNewpages: function() {
return Action.urlLink(SpecialNewpages.msg.newpages, URLs.newPages(20));
},
//------------------------------------------------------------------------------
/** extend Special:Newpages with the content of the articles */
displayInline: function() {
// maximum number of bytes an article may have to be loaded immediately
var maxSize = 2048;
/** parse one list item and insert its content */
function extendItem(li) {
// fetch data
var a = li.getElementsByTagName("a")[0];
var title = a.title;
//var byteStr = /^[^0-9]*([0-9\.]+)/(a.nextSibling.textContent)[1].replace(/\./g, "");
var byteStr = li.innerHTML.replace(/.*\[([0-9.]+) Bytes\].*/, "$1").replace(/\./, "");
var bytes = parseInt(byteStr);
// HACK: modify link in the header
//a.href = a.href + "?action=history";
// make header
var header = document.createElement("div");
header.className = "folding-header";
header.innerHTML = li.innerHTML;
// make body
var body = document.createElement("div");
body.className = "folding-body";
// add a FoldButton to the header
var foldButton = new FoldButton(true, function(open) {
body.style.display = open ? null : "none";
});
pasteBegin(header, foldButton.button);
// add action links
pasteBegin(header, UserBookmarks.doMark(title));
pasteBegin(header, Template.allPageActions(title));
pasteBegin(header, FastDelete.doDeletePopup(title));
// change listitem
li.pageTitle = title;
li.contentBytes = bytes;
li.headerDiv = header;
li.bodyDiv = body;
//TODO: set folding-even and folding-odd
li.className = "folding-container";
li.innerHTML = "";
li.appendChild(header);
li.appendChild(body);
if (li.contentBytes <= maxSize) {
loadContent(li);
}
else {
//### BÄH
var inner = [
SpecialNewpages.msg.longer1,
""+maxSize,
SpecialNewpages.msg.longer2,
SpecialNewpages.msg.load1,
Action.functionLink(SpecialNewpages.msg.loadX,
function() { loadContent(li); }),
SpecialNewpages.msg.load2,
];
for (var i=0; i<inner.length; i++) {
var node = inner[i];
if (node.constructor == String) body.appendChild(document.createTextNode(node));
else body.appendChild(node);
}
}
}
function loadContent(li) {
// load the article content and display it inline
Ajax.call({
url: Page.readURL(li.pageTitle, { redirect: "no" }),
doneState: function(source) {
var content = /<!-- start content -->([^]*)<div class="printfooter">/(source.responseText);
if (content) li.bodyDiv.innerHTML = content[1] + '<div class="visualClear" />';
}
});
}
// find article list
var ol = descendants('bodyContent', "ol", null, 0);
ol.className = "specialNewPages";
// find article list items
var lis = descendants(ol, "li");
for (var i=0; i<lis.length; i++) {
extendItem(lis[i]);
}
},
};
SpecialNewpages.msg = {
newpages: "Neue Artikel",
longer1: "länger als ",
longer2: " bytes. ",
load1: "trotzdem ",
loadX: "laden",
load2: ".",
};
//======================================================================
//## SpecialBlockip.js
/** extends Special:Blockip */
SpecialBlockip = {
/** onload initializer */
init: function() {
if (!Page.isSpecial("Blockip")) return;
this.presetBlockip();
},
//------------------------------------------------------------------------------
/** fill in default values into the blockip form */
presetBlockip: function() {
var form = document.forms["blockip"];
if (!form) return; // action=success
form.elements["wpBlockExpiry"].value = "other";
form.elements["wpBlockOther"].value = "1 hour";
form.elements["wpBlockReason"].value = SpecialBlockip.msg.standardReason;
form.elements["wpBlockReason"].select();
form.elements["wpBlockReason"].focus();
},
};
SpecialBlockip.msg = {
standardReason: "vandalismus",
};
//======================================================================
//## SpecialUndelete.js
/** extends Special:Undelete */
SpecialUndelete = {
/** onload initializer */
init: function() {
if (!Page.isSpecial("Undelete")) return;
this.toggleAll();
},
//------------------------------------------------------------------------------
/** add an invert button for all checkboxes */
toggleAll: function() {
// damn, this form no longer has a name
// var form = document.forms["undelete"];
var form = document.forms[0];
if (!form) return;
var button = document.createElement("input");
button.type = "button";
button.value = SpecialUndelete.msg.invert;
button.onclick = function() {
var els = form.elements;
for (var i=0; i<els.length; i++) {
var el = els[i];
if (el.type == "checkbox")
el.checked = !el.checked;
}
}
//var target = descendants('undelete', "ul", null, 1);
var target = descendants(form, "ul", null, 2);
if (target == null) return; // no list if there is only one deleted version
target.parentNode.insertBefore(button, target);
},
};
SpecialUndelete.msg = {
invert: "Invertieren",
};
//======================================================================
//## SpecialWatchlist.js
/** extensions for Special:Watchlist */
SpecialWatchlist = {
/** onload initializer */
init: function() {
var watchlist = Page.isSpecial("Watchlist");
if (!watchlist) return;
if (Page.params["edit"] == "yes"
|| watchlist.argument == "edit") {
this.exportLinks(); // call before extendHeaders!
this.toggleLinks();
}
else {
this.filterLinks();
}
},
//------------------------------------------------------------------------------
//## normal mode
/** change the watchlist to make it filterable */
filterLinks: function() {
if (!Page.isSpecial("Watchlist")) return;
if (Page.params["edit"]) return;
var ip = /^(\d{1,3}\.){3}\d{1,3}$/;
/** set a source-ip or source-name class on every list item in every ul-special */
function init() {
var uls = descendants(document, "ul", "special");
for (var i=0; i<uls.length; i++) {
var ul = uls[i];
var lis = descendants(ul, "li");
for (var j=0; j<lis.length; j++) {
var li = lis[j];
var a = descendants(li, "a", null, 3);
li.className = ip(a.textContent)
? "source-ip"
: "source-name";
}
}
}
/** show all list items */
function showAll() {
mode("source-all", "source-ip source-name");
}
/** show all list items from ip users */
function showIp() {
mode("source-ip", "source-all source-name");
}
/** show all list items from logged in users */
function showName() {
mode("source-name", "source-all source-ip");
}
/**
* add and remove classNames from the bodyContent.
* this is used to display or hide li-tags setup in init with CSS
*/
function mode(add,sub) {
var bodyContent = $('bodyContent');
bodyContent.className = className(bodyContent.className, add, sub);
}
// change list items and add buttons
init();
// add buttons to the bodyContent
// TODO: show current state
var target = nextElement($('jump-to-nav'), "h4");
pasteBefore(target, [
SpecialWatchlist.msg.show1,
Action.functionLink(SpecialWatchlist.msg.all, showAll), ' | ',
Action.functionLink(SpecialWatchlist.msg.names, showName), ' | ',
Action.functionLink(SpecialWatchlist.msg.ips, showIp),
SpecialWatchlist.msg.show2,
]);
},
//------------------------------------------------------------------------------
//## edit mode
/** extend Special:Watchlist?edit=yes with a link to a text/plain version */
exportLinks: function() {
// parse and generate wiki and csv text
var wiki = "";
var csv = '"title","namespace","exists"\n';
var ns = "";
var form = descendants(document, "form", null, 0);
var uls = descendants(form, "ul");
for (var i=0; i<uls.length; i++) {
var ul = uls[i];
var h2 = previousElement(ul);
if (h2) ns = h2.textContent;
wiki += "== " + ns + " ==\n";
var lis = descendants(ul, "li");
for (var j=0; j<lis.length; j++) {
var li = lis[j];
var as = descendants(li, "a");
var a = as[0];
var title = a.title;
var exists = a.className != "new";
wiki += '*[[' + title + ']]'
+ (exists ? "" : " (new)")
+ '\n';
csv += '"' + title.replace(/"/g, '""') + '"' + ','
+ '"' + ns.replace(/"/g, '""') + '"' + ','
+ '"' + (exists ? "yes": "no") + '"' + '\n';
}
}
// create wiki link
var wikiLink = document.createElement("a");
wikiLink.textContent = "watchlist.wkp";
wikiLink.title = "Markup";
wikiLink.href = "data:text/plain;charset=utf-8," + encodeURIComponent(wiki);
// create csv link
var csvLink = document.createElement("a");
csvLink.textContent = "watchlist.csv";
csvLink.title = "CSV";
csvLink.href = "data:text/csv;charset=utf-8," + encodeURIComponent(csv);
// insert links
var target = nextElement($('jump-to-nav'), "form");
pasteBefore(target, [
"export as ", wikiLink, " (Markup) ",
"or as ", csvLink, " (CSV).",
]);
},
/** extends header structure and add toggle buttons for all checkboxes */
toggleLinks: function() {
var form = descendants(document, "form", null, 0)
// be folding-friendly: add a header for the article namespace
var ul = descendants(form, "ul", null, 0);
var articleHdr = document.createElement("h2");
articleHdr.textContent = SpecialWatchlist.msg.article;
pasteBefore(ul, articleHdr);
// add invert buttons for single namespaces
var uls = descendants(form, "ul");
for (var i=0; i<uls.length; i++) {
var ul = uls[i];
var button = this.toggleButton(ul);
var target = previousElement(ul, "h2");
pasteAfter(target.lastChild, [ ' ', button ]);
}
// be folding-friendly: add a header for the global controls
var globalHdr = document.createElement("h2");
globalHdr.textContent = SpecialWatchlist.msg.global;
var target = form.elements["remove"];
pasteBefore(target, globalHdr);
// add a gobal invert button
var button = this.toggleButton(form);
pasteAfter(globalHdr.lastChild, [ ' ', button ]);
},
/** creates a toggle button for all input children of an element */
toggleButton: function(container) {
return Action.functionLink(SpecialWatchlist.msg.invert, function() {
var inputs = container.getElementsByTagName("input");
for (var i=0; i<inputs.length; i++) {
var el = inputs[i];
if (el.type == "checkbox")
el.checked = !el.checked;
}
});
},
};
SpecialWatchlist.msg = {
invert: "Invertieren",
article: "Artikel",
global: "Alle",
show1: "Änderungen ",
all: "von allen Nutzern",
ips: "nur von Ips",
names: "nur von Angemeldeten",
show2: " anzeigen.",
};
//======================================================================
//## _private.js
/** onload hook */
function initialize() {
//------------------------------------------------------------------------------
//## init functions
Page.init(); // gather globally used information
Usermessage.init(); // replace diff=cur with action=history
ActionWatch.init(); // one-click watch and unwatch
ActionHistory.init(); // add edit and block link for every version
SpecialUndelete.init(); // add invert button toggling all checkboxes
SpecialNewpages.init(); // display article content inline, a link is added to the sideBar later
SpecialBlockip.init(); // set default values for a blockip page, a link is addded to the sideBar later
SpecialWatchlist.init(); // add export links to the top of the page and an toggle button for all checkboxes
FoldHeaders.init(); // fold all headers when a div with id autofold is found
//------------------------------------------------------------------------------
//## p-search
// remove the go button, i want to search on enter
SideBar.removeSearchGoButton();
//------------------------------------------------------------------------------
//## p-cactions
// cactions should move with the page
SideBar.unfixCactions();
SideBar.labelItems([
[ 'ca-talk', "Diskussion" ],
[ 'ca-edit', "Bearbeiten" ],
[ 'ca-viewsource', "Source" ],
[ 'ca-history', "History" ],
[ 'ca-protect', "Schützen" ],
[ 'ca-unprotect', "Freigeben" ],
[ 'ca-delete', "Löschen" ],
[ 'ca-move', "Verschieben" ],
// done in ActionWatch
//[ 'ca-watch', "Beobachten" ],
//[ 'ca-unwatch', "Vergessen" ],
]);
//------------------------------------------------------------------------------
//## p-tb
SideBar.labelItems([
[ 't-whatlinkshere', "Links hierher" ],
[ 't-recentchangeslinked', "Nahe Änderungen" ],
[ 't-emailuser', "Anmailen" ],
]);
var tools1 = [ FoldHeaders.doFold() ];
if (Page.deletable) {
var multi = FastDelete.doDeletePopup(Page.lemma);
tools1.unshift(multi);
}
SideBar.usePortlet('p-tb').setTitle("Tools").build([
tools1,
Template.allPageActions(Page.lemma),
UserBookmarks.actions(),
UserPage.doGotoBank(),
]).show();
//------------------------------------------------------------------------------
//## portlet-delete
if (Page.deletable)
SideBar.newPortlet("portlet-delete").setTitle("Wech").build(
FastDelete.doDeleteBanks(Page.lemma)
).show();
//------------------------------------------------------------------------------
//## p-navigation
SideBar.usePortlet('p-navigation').setTitle("Navigation").build([
'n-recentchanges',
[ SpecialNewpages.doNewpages() ],
't-specialpages',
[ Action.urlLink("Logbücher", URLs.allLogs()) ], //TODO: wrap up
't-permalink',
'pt-watchlist',
't-recentchangeslinked',
't-whatlinkshere',
]).show();
//------------------------------------------------------------------------------
//## portlet-communication
if (Page.owner && Page.owner != Page.user) {
var banks = Communication.doCommunicationBanks();
// build portlet
SideBar.newPortlet('portlet-communication').setTitle("Kommunikation").build(
Communication.doCommunicationBanks()
).show();
}
//------------------------------------------------------------------------------
//## p-personal
SideBar.labelItems([
[ 'pt-mytalk', "Diskussion" ],
[ 'pt-mycontris', "Beiträge" ],
]);
// cannot use p-personal which has too much styling
SideBar.newPortlet('portlet-personal').setTitle("Persönlich").build([
'pt-userpage',
'pt-mytalk',
'pt-mycontris',
[ Action.urlLink("Adminlog", URLs.userAllLogs(Page.user)) ], //TODO: wrap up
'pt-preferences',
'pt-logout',
// 'pt-watchlist'
]).show();
//------------------------------------------------------------------------------
//## p-lang
// transform list into a select box
SideBar.langSelect();
}
doOnLoad(initialize);
/* </nowiki></pre> */