„Benutzer:D/monobook.js“ – Versionsunterschied
Inhalt gelöscht Inhalt hinzugefügt
D (Diskussion | Beiträge) Keine Bearbeitungszusammenfassung | D (Diskussion | Beiträge) kaputt Markierung: Geleert | ||
Zeile 1: | Zeile 1: | ||
/* [[Benutzer:D/monobook]] ::: achtung entwicklerversion, unsupported und gefährlich ::: [[Benutzer:D/monobook.js]] */ | |||
/* <pre><nowiki> */ | |||
//====================================================================== | |||
//## util/Object.js | |||
var jsutil = jsutil || {}; | |||
// NOTE: these do _not_ break for (foo in bar) | |||
/** Object helper functions */ | |||
jsutil.Object = { | |||
/** return a type-indicating string */ | |||
type: function(obj) { | |||
return obj === null ? "null" : | |||
obj == null ? "undefined" : | |||
Object.prototype.toString.call(obj).match(/(\w+)\]/)[1]; | |||
}, | |||
/** returns all keys as an Array */ | |||
keys: function(obj) { | |||
var out = []; | |||
for (var key in obj) | |||
if (obj.hasOwnProperty(key)) | |||
out.push(key); | |||
return out; | |||
}, | |||
/** returns the value behind every key */ | |||
values: function(obj) { | |||
var out = []; | |||
for (var key in obj) | |||
if (obj.hasOwnProperty(key)) | |||
out.push(obj[key]); | |||
return out; | |||
}, | |||
/** copies an Object's properties into an new Object */ | |||
clone: function(obj) { | |||
var out = {}; | |||
for (var key in obj) | |||
if (obj.hasOwnProperty(key)) | |||
out[key] = obj[key]; | |||
return out; | |||
}, | |||
/** 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([key, obj[key]]); | |||
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[i]; | |||
out[pair[0]] = pair[1]; | |||
} | |||
return out; | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## util/Function.js | |||
var jsutil = jsutil || {}; | |||
/** can be used to copy a function's arguments into a real Array */ | |||
jsutil.Function = { | |||
identity: function(x) { return x; }, | |||
constant: function(c) { return function(v) { return c; }; } | |||
}; | |||
//====================================================================== | |||
//## util/Number.js | |||
var jsutil = jsutil || {}; | |||
/** can be used to copy a function's arguments into a real Array */ | |||
jsutil.Number = { | |||
range: function(from, to) { | |||
var out = []; | |||
for (var i=from; i<to; i++) out.push(i); | |||
return out; | |||
} | |||
}; | |||
//====================================================================== | |||
//## util/Text.js | |||
var jsutil = jsutil || {}; | |||
/** text utilities */ | |||
jsutil.Text = { | |||
/** | |||
* 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[i]; | |||
search.push(pair[0].escapeRE()); | |||
replace[pair[0]] = pair[1]; | |||
} | |||
var regexp = new RegExp(search.join("|"), "gm"); | |||
return function(s) { | |||
return s.replace(regexp, function(dollar0) { | |||
return replace[dollar0]; }); }; | |||
}, | |||
/** 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[i]; | |||
if (value === null || value === "") continue; | |||
filtered.push(value); | |||
} | |||
return filtered.join(separator ? separator : ""); | |||
}, | |||
/** 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 jsutil.Text.joinPrintable(separator, [ prefix, suffix ]); | |||
}; | |||
}, | |||
/** make a function adding a given suffix */ | |||
suffixFunc: function(separator, suffix) { | |||
return function(prefix) { | |||
return jsutil.Text.joinPrintable(separator, [ prefix, suffix ]); | |||
}; | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## util/XML.js | |||
var jsutil = jsutil || {}; | |||
/** XML utility functions */ | |||
jsutil.XML = { | |||
//------------------------------------------------------------------------------ | |||
//## DOM | |||
/** parses a String into an XMLDocument */ | |||
parseXML: function(text) { | |||
// TODO text/html does work on firefox, but not on webkit | |||
var doc = new DOMParser().parseFromString(text, "text/html"); | |||
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 new Error("XML parser error: " + root.textContent); | |||
return doc; | |||
}, | |||
/** serialize an XML (e4x) or XMLDocument to a String */ | |||
unparseXML: function(xml) { | |||
return new XMLSerializer().serializeToString(xml); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## 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 | |||
// @depends jsutil/Object | |||
var jsutil = jsutil || {}; | |||
/** | |||
* 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 | |||
*/ | |||
jsutil.Loc = function(urlStr) { | |||
var m = this.parser.exec(urlStr); | |||
if (!m) throw new Error("cannot parse URL: " + urlStr); | |||
this.local = !m[1]; | |||
this.protocol = m[2] ? m[2] : null; // http: | |||
this.host = m[3] ? m[3] : null; // de.wikipedia.org | |||
this.port = m[4] ? parseInt(m[4].substring(1)) : null; // 80 | |||
this.pathname = m[5] ? m[5] : ""; // /wiki/Test | |||
this.hash = m[6] ? m[6] : ""; // #Industry | |||
this.search = m[7] ? m[7] : ""; // ?action=edit | |||
}; | |||
jsutil.Loc.prototype = { | |||
/** matches a global or local URL */ | |||
parser: /((.+?)\/\/([^:\/]+)(:[0-9]+)?)?([^#?]+)?(#[^?]*)?(\?.*)?/, | |||
/** 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[i].split("="); | |||
out.push([ | |||
decodeURIComponent(parts[0]), | |||
decodeURIComponent(parts[1]) | |||
]); | |||
} | |||
return out; | |||
}, | |||
/** converts the searchString into a hash. */ | |||
argsMap: function() { | |||
var pairs = this.args(); | |||
var out = {}; | |||
for (var i=0; i<pairs.length; i++) { | |||
var pair = pairs[i]; | |||
var key = pair[0]; | |||
var value = pair[1]; | |||
// if (out.hasOwnProperty(key)) throw new Error("duplicate argument: " + key); | |||
out[key] = value; | |||
} | |||
return out; | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## util/DOM.js | |||
var jsutil = jsutil || {}; | |||
/** DOM helper functions */ | |||
jsutil.DOM = { | |||
//------------------------------------------------------------------------------ | |||
//## events | |||
/** executes a function when the DOM is loaded */ | |||
onLoad: function(func) { | |||
window.addEventListener("DOMContentLoaded", func, false); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## find | |||
/** checks if obj is a DOM node */ | |||
isNode: function(obj) { | |||
return !!(obj && obj.nodeType); | |||
}, | |||
/** 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[i], 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 */ | |||
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; | |||
} | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## single insertions | |||
// BETTER implement something like an insertion point? | |||
/** insert a node as the first child of the parent */ | |||
insertBegin: function(parent, node) { | |||
parent.insertBefore(node, parent.firstChild); | |||
}, | |||
/** insert a node as the last child of the parent */ | |||
insertEnd: function(parent, node) { | |||
parent.appendChild(node); | |||
}, | |||
/** insert a node before target */ | |||
insertBefore: function(target, node) { | |||
target.parentNode.insertBefore(node, target); | |||
}, | |||
/** insert a node after target */ | |||
insertAfter: function(target, node) { | |||
target.parentNode.insertBefore(node, target.nextSibling); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## multi insertions | |||
/** insert many nodes at the front of the children of the parent */ | |||
insertBeginMany: function(parent, nodes) { | |||
var reference = parent.firstChild; | |||
for (var i=0; i<nodes.length; i++) { | |||
parent.insertBefore(nodes[i], reference); | |||
} | |||
}, | |||
/** insert many nodes at the end of the children of the parent */ | |||
insertEndMany: function(parent, nodes) { | |||
for (var i=0; i<nodes.length; i++) { | |||
parent.appendChild(nodes[i]); | |||
} | |||
}, | |||
/** insert many nodes before the target */ | |||
insertBeforeMany: function(target, nodes) { | |||
var parent = target.parentNode; | |||
for (var i=0; i<nodes.length; i++) { | |||
parent.insertBefore(nodes[i], target); | |||
} | |||
}, | |||
/** insert many nodes after the target */ | |||
insertAfterMany: function(target, nodes) { | |||
var parent = target.parentNode; | |||
var reference = target.nextSibling; | |||
for (var i=0; i<nodes.length; i++) { | |||
parent.insertBefore(nodes[i], reference); | |||
} | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## insertion helpers | |||
/** turn String into text node, pass through everything else */ | |||
textAsNode: function(it) { | |||
return it.constructor === String | |||
? document.createTextNode(it) | |||
: it; | |||
}, | |||
/** textAsNode lifted to an Array */ | |||
textAsNodeMany: function(its) { | |||
var out = []; | |||
for (var i=0; i<its.length; i++) { | |||
out.push(this.textAsNode(its[i])); | |||
} | |||
return out; | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## 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 target with a replacement node */ | |||
replaceNode: function(target, replacement) { | |||
target.parentNode.replaceChild(replacement, target); | |||
}, | |||
/** replace a target with many replacement nodes */ | |||
replaceNodeMany: function(target, replacements) { | |||
var parent = target.parentNode; | |||
for (var i=0; i<replacements.length; i++) { | |||
parent.insertBefore(replacements[i], target); | |||
} | |||
parent.removeChild(target); | |||
}, | |||
/** replace all children of a target node */ | |||
replaceChildren: function(target, replacements) { | |||
this.removeChildren(target); | |||
this.insertEndMany(target, replacements); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## css classes | |||
// BETTER use StringSet here | |||
/** returns an Array of the classes of an element */ | |||
getClasses: function(element) { | |||
var raw = (element.className || "").trim(); | |||
return raw ? raw.split(/\s+/) : []; | |||
}, | |||
/** sets all classes of an element from an Array of names */ | |||
setClasses: function(element, classNames) { | |||
element.className = classNames.join(" "); | |||
}, | |||
/** pass the class names array to a function modifying it */ | |||
modifyClasses: function(element, func) { | |||
this.setClasses(element, func(this.getClasses(element))); | |||
}, | |||
/** returns whether an element has a class */ | |||
hasClass: function(element, className) { | |||
return this.getClasses(element).contains(className); | |||
}, | |||
/** adds a class to an element */ | |||
addClass: function(element, className) { | |||
var set = this.getClasses(element); | |||
var ok = !set.contains(className); | |||
if (ok) { | |||
set.push(className); | |||
this.setClasses(element, set); | |||
} | |||
return ok; | |||
}, | |||
/** removes a class to an element */ | |||
removeClass: function(element, className) { | |||
var set = this.getClasses(element); | |||
var ok = set.contains(className); | |||
if (ok) { | |||
set.remove(className); | |||
this.setClasses(element, set); | |||
} | |||
return ok; | |||
}, | |||
/** replaces a class in an element with another */ | |||
replaceClass: function(element, oldClassName, newClassName) { | |||
var set = this.getClasses(element); | |||
if (set.contains(oldClassName)) { | |||
set.remove(oldClassName); | |||
if (!set.contains(newClassName)) { | |||
set.push(newClassName); | |||
} | |||
} | |||
this.setClasses(element, set); | |||
}, | |||
/** sets or unsets a class on an element */ | |||
updateClass: function(element, className, active) { | |||
if (active) this.addClass(element, className); | |||
else this.removeClass(element, className); | |||
}, | |||
/** switches between two different classes */ | |||
switchClass: function(element, condition, trueClassName, falseClassName) { | |||
if (condition) this.replaceClass(element, falseClassName, trueClassName); | |||
else this.replaceClass(element, trueClassName, falseClassName); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## position | |||
/** analog to element.scrollTop, but from the bottom */ | |||
getScrollBottom: function(element) { | |||
return element.scrollHeight - element.offsetHeight - element.scrollTop; | |||
}, | |||
/** analog to element.scrollTop, but from the bottom */ | |||
setScrollBottom: function(element, scrollBottom) { | |||
element.scrollTop = element.scrollHeight - element.offsetHeight - scrollBottom; | |||
}, | |||
/** 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 | |||
var jsutil = jsutil || {}; | |||
/** helper functions for cookies */ | |||
jsutil.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)[1]; | |||
if (!s) return null; | |||
s = s.split(";")[0].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.toUTCString() + | |||
"; 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 | |||
var jsutil = jsutil || {}; | |||
/** ajax helper */ | |||
jsutil.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 before the more specific functions | |||
* noSuccessFunc called in all non-successful cases | |||
* | |||
* 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 new Error("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 (args.completeFunc) args.completeFunc(client); | |||
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 (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) { | |||
if (!args.headers.hasOwnProperty(key)) continue; | |||
client.setRequestHeader(key, args.headers[key]); | |||
} | |||
} | |||
// 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); | |||
return { | |||
client: client, | |||
aborted: false, | |||
abort: function() { | |||
if (client.timer) clearTimeout(client.timer); | |||
this.aborted = true; | |||
try { client.abort(); } | |||
catch (e) {} // TODO log this somewhere | |||
}//, | |||
}; | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## 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[i]; | |||
if (pair.constructor !== Array) throw new Error("expected a Pair: " + pair); | |||
if (pair.length !== 2) throw new Error("expected a Pair: " + pair); | |||
if (pair[1] === null) continue; | |||
out += "&" + encodeFunc(pair[0]) | |||
+ "=" + encodeFunc(pair[1]); | |||
} | |||
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) { | |||
if (!map.hasOwnProperty(key)) continue; | |||
var value = map[key]; | |||
if (value === null) continue; | |||
if (value.constructor === Array) { | |||
for (var i=0; i<value.length;i++) { | |||
var subValue = value[i]; | |||
if (subValue === null) continue; | |||
out.push([ key, subValue ]); | |||
} | |||
continue; | |||
} | |||
out.push([ key, value.toString() ]); | |||
} | |||
return out; | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## util/Form.js | |||
var jsutil = jsutil || {}; | |||
/** HTMLFormElement helper functions */ | |||
jsutil.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[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; | |||
}, | |||
/** 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 jsutil.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[i]; | |||
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 [ [ name, value ] ]; | |||
else return [ [ name, "Submit Query" ] ]; | |||
} | |||
else if (type === "button" || type === "image") { | |||
if (value) return [ [ name, value ] ]; | |||
else return [ [ name, "" ] ]; | |||
} | |||
else if (type === "checkbox" || type === "radio") { | |||
if (!element.checked) return []; | |||
else if (value !== null) return [ [ name, value ] ]; | |||
else return [ [ name, "on" ] ]; | |||
} | |||
else if (type === "select-one") { | |||
if (element.selectedIndex !== -1) return [ [ name, value ] ]; | |||
else return []; | |||
} | |||
else if (type === "select-multiple") { | |||
var pairs = []; | |||
for (var i=0; i<element.options.length; i++) { | |||
var opt = element.options[i]; | |||
if (!opt.selected) continue; | |||
pairs.push([ name, opt.value ]); | |||
} | |||
return pairs; | |||
} | |||
else if (type === "text" || type === "password" || type === "hidden" || type === "textarea") { | |||
if (value) return [ [ name, value ] ]; | |||
else return [ [ name, "" ] ]; | |||
} | |||
else if (type === "file") { | |||
// NOTE: can't do anything here :( | |||
return []; | |||
} | |||
else { | |||
throw new Error("field: " + name + " has the unknown type: " + type); | |||
} | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## util/ext/function.js | |||
/** | |||
* call this thunk after some millis. | |||
* optionally call the given continuation with the result afterwards. | |||
* returns an object with an cancel method to prevent this thunk from being called. | |||
* the cancel method returns whether cancellation was successful | |||
*/ | |||
Function.prototype.callAfter = function(millis, continuation) { | |||
var self = this; | |||
var running = false; | |||
function execute() { | |||
running = true; | |||
var out = self.call(); | |||
if (continuation) continuation(out); | |||
} | |||
var timer = window.setTimeout(execute, millis); | |||
function cancel() { | |||
window.clearTimeout(timer); | |||
return !running; | |||
} | |||
return { | |||
cancel: cancel | |||
}; | |||
}; | |||
//====================================================================== | |||
//## util/ext/array.js | |||
//------------------------------------------------------------------------------ | |||
//## mutating | |||
/** mutating operation: removes an element */ | |||
Array.prototype.remove = function(element) { | |||
var index = this.indexOf(element); | |||
if (index === -1) return false; | |||
this.splice(index, 1); | |||
return true; | |||
}; | |||
//------------------------------------------------------------------------------ | |||
//## not mutating | |||
/** returns a new Array without the given element */ | |||
Array.prototype.cloneRemove = function(element) { | |||
var index = this.indexOf(element); | |||
if (index === -1) return this; | |||
var out = [].concat(this); | |||
out.splice(index, 1); | |||
return out; | |||
}; | |||
/** filter with an inverted predicate */ | |||
Array.prototype.filterNot = function(predicate, thisVal) { | |||
var len = this.length; | |||
var out = new Array(); | |||
for (var i=0; i<len; i++) { | |||
var it = this[i]; | |||
if (!predicate.call(thisVal, it, i, this)) { | |||
out.push(it); | |||
} | |||
} | |||
return out; | |||
}; | |||
/** zip with another array */ | |||
Array.prototype.zip = function(that) { | |||
var thisLen = this.length; | |||
var thatLen = that.length; | |||
var out = new Array(); | |||
for (var i=0; i<thisLen && i<thatLen; i++) { | |||
out.push([ this[i], that[i] ]); | |||
} | |||
return out; | |||
}; | |||
/** whether this array contains an element */ | |||
Array.prototype.contains = function(element) { | |||
return this.indexOf(element) !== -1; | |||
}; | |||
/** two partitions in a 2-element Array, first the partition where the predicate returned true */ | |||
Array.prototype.partition = function(predicate) { | |||
var yes = []; | |||
var no = []; | |||
for (var i=0; i<this.length; i++) { | |||
var item = this[i]; | |||
(predicate(item) ? yes : no).push(item); | |||
} | |||
return [ yes, no ]; | |||
}; | |||
/** flatten an Array of Arrays into a simple Array */ | |||
Array.prototype.flatten = function() { | |||
var out = []; | |||
for (var i=0; i<this.length; i++) { | |||
out = out.concat(this[i]); | |||
} | |||
return out; | |||
}; | |||
/** map every element to an Array and concat the resulting Arrays */ | |||
Array.prototype.flatMap = function(func, thisVal) { | |||
var out = []; | |||
for (var i=0; i<this.length; i++) { | |||
out = out.concat(func.call(thisVal, this[i], i, this)); | |||
} | |||
return out; | |||
}; | |||
/** returns a copy of this Array */ | |||
Array.prototype.clone = function() { | |||
return [].concat(this); | |||
}; | |||
/** returns a reverse copy of this Array */ | |||
Array.prototype.cloneReverse = function() { | |||
var out = [].concat(this); | |||
out.reverse(); | |||
return out; | |||
}; | |||
/** return a new Array with a separator inserted between every element of the Array */ | |||
Array.prototype.intersperse = function(separator) { | |||
var out = []; | |||
for (var i=0; i<this.length; i++) { | |||
out.push(this[i]); | |||
out.push(separator); | |||
} | |||
out.pop(); | |||
return out; | |||
}; | |||
/** optionally insert an element between every two elements and boundaries */ | |||
Array.prototype.inject = function(func, thisVal) { | |||
var out = []; | |||
for (var i=0; i<=this.length; i++) { | |||
var a = i > 0 ? this[i-1] : null; | |||
var b = i < this.length ? this[i] : null; | |||
var tmp = func.call(thisVal, a, b); | |||
if (tmp !== null) out.push(tmp); | |||
if (i < this.length) out.push(this[i]); | |||
} | |||
return out; | |||
}; | |||
/** use a function to extract keys and build an Object */ | |||
Array.prototype.mapBy = function(keyFunc) { | |||
var out = {}; | |||
for (var i=0; i<this.length; i++) { | |||
var item = this[i]; | |||
out[keyFunc(item)] = item; | |||
} | |||
return out; | |||
}; | |||
//====================================================================== | |||
//## util/ext/string.js | |||
/** 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; | |||
}; | |||
/** escapes characters to make them usable as a literal in a RegExp */ | |||
String.prototype.escapeRE = function() { | |||
return this.replace(/([{}()|.?*+^$\[\]\\])/g, "\\$1"); | |||
}; | |||
/** 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() + "([a-zA-Z]+?)" + suffix.escapeRE(), "g"); | |||
return this.replace(re, function($0, $1) { | |||
var arg = args[$1]; | |||
return arg !== undefined ? arg : $0; | |||
}); | |||
}; | |||
//====================================================================== | |||
//## util/fill/nonstandard.js | |||
/** remove whitespace from the left */ | |||
if (!String.prototype.trimLeft) | |||
String.prototype.trimLeft = function() { | |||
return this.replace(/^\s+/gm, ""); | |||
}; | |||
/** remove whitespace from the right */ | |||
if (!String.prototype.trimRight) | |||
String.prototype.trimRight = function() { | |||
return this.replace(/\s+$/gm, ""); | |||
}; | |||
//====================================================================== | |||
//## util/fill/es6.js | |||
//------------------------------------------------------------------------------ | |||
//## Object | |||
/** copies an object's properties into another object */ | |||
if (!Object.assign) | |||
Object.assign = function(target, source) { | |||
for (var key in source) { | |||
if (source.hasOwnProperty(key)) { | |||
target[key] = source[key]; | |||
} | |||
} | |||
}; | |||
//------------------------------------------------------------------------------ | |||
//## Array | |||
/** can be used to copy a function's arguments into a real Array */ | |||
if (!Array.from) | |||
Array.from = function(args) { | |||
return Array.prototype.slice.apply(args); | |||
}; | |||
if (!Array.of) | |||
Array.of = function(args) { | |||
return Array.prototype.slice.call(args); | |||
}; | |||
if (!Array.prototype.find) | |||
Array.prototype.find = function(predicate, thisVal) { | |||
var len = this.length; | |||
for (var i=0; i<len; i++) { | |||
var it = this[i]; | |||
if (predicate.call(thisVal, it, i, this)) return it; | |||
} | |||
return undefined; | |||
}; | |||
if (!Array.prototype.findIndex) | |||
Array.prototype.findIndex = function(predicate, thisVal) { | |||
var len = this.length; | |||
for (var i=0; i<len; i++) { | |||
var it = this[i]; | |||
if (predicate.call(thisVal, it, i, this)) return i; | |||
} | |||
return undefined; | |||
}; | |||
//------------------------------------------------------------------------------ | |||
//## String | |||
/** whether this string starts with another */ | |||
if (!String.prototype.startsWith) | |||
String.prototype.startsWith = function(s, position) { | |||
var pos = position || 0; | |||
return this.indexOf(s, pos) === pos; | |||
}; | |||
/** whether this string ends with another */ | |||
if (!String.prototype.endsWith) | |||
String.prototype.endsWith = function(s, position) { | |||
var pos = (position || this.length) - s.length; | |||
var idx = this.lastIndexOf(s); | |||
return idx !== -1 && idx === pos; | |||
}; | |||
/** whether this string contains another */ | |||
if (!String.prototype.contains) | |||
String.prototype.contains = function(s, start) { | |||
return this.indexOf(s, start) !== -1; | |||
}; | |||
/** repeat this string count times */ | |||
if (!String.prototype.repeat) | |||
String.prototype.repeat = function(count) { | |||
if (count < 0) throw new Error("count must be greater or equal than zero"); | |||
else if (count === 0) return ""; | |||
else { | |||
var out = ""; | |||
for (var i=0; i<count; i++) { | |||
out += this; | |||
} | |||
return out; | |||
} | |||
}; | |||
//====================================================================== | |||
//## lib/core/Wiki.js | |||
/** encoding and decoding of MediaWiki URLs */ | |||
var Wiki = { | |||
//## config | |||
/** the current wiki site without any path */ | |||
site: null, // "http://de.wikipedia.org", | |||
/** the protocol of the current site */ | |||
protocol: null, // "http://" | |||
/** the current wiki domain */ | |||
domain: null, // "de.wikipedia.org", | |||
/** language of the site */ | |||
language: null, // "de" | |||
/** name of the logged in user */ | |||
user: null, | |||
/** private: path to read pages */ | |||
readPath: null, // "/wiki/", | |||
/** private: path for page actions */ | |||
actionPath: null, // "/w/index.php", | |||
//## info | |||
/** whether user has news */ | |||
haveNews: function() { | |||
// li#pt-mytalk a.mw-echo-unread-notifications.mw-echo-notifications-badge | |||
return jsutil.DOM.fetch("pt-mytalk", null, "mw-echo-alert", 0) !== null; | |||
}, | |||
/** | |||
* the bodyContent-equivalent for all skins. | |||
* parent is optional, for null the document is used | |||
*/ | |||
bodyContent: function(parent) { | |||
parent = parent || document; | |||
return parent.getElementById('bodyContent') | |||
|| parent.getElementById('mw_contentholder') | |||
|| parent.getElementById('article'); | |||
}, | |||
//## URLs | |||
/** compute an URL in the read form without a title parameter. the args object is optional */ | |||
readURL: function(title, args) { | |||
args = jsutil.Object.clone(args); | |||
args.title = title; | |||
return this.encodeURL(args, true); | |||
}, | |||
/** encode parameters into an URL */ | |||
encodeURL: function(args, shorten) { | |||
args = jsutil.Object.clone(args); | |||
args.title = this.normalizeTitle(args.title); | |||
// HACK: Special:Randompage _requires_ smushing! | |||
var specialInfo = Special.pageInfo(args.title); | |||
if (specialInfo) { | |||
if (shorten | |||
|| specialInfo.canonicalName == "Randompage" && specialInfo.smushValue) | |||
this.smush(args, specialInfo); | |||
else | |||
this.desmush(args, specialInfo); | |||
} | |||
// create start path | |||
var path; | |||
if (shorten) { | |||
// "+" means "+" here! | |||
path = this.readPath | |||
+ this.fixPlus(this.fixTitle(encodeURIComponent(args.title))); | |||
// not needed any more | |||
delete args.title; | |||
} | |||
else { | |||
path = this.actionPath; | |||
} | |||
path += "?"; | |||
// normalize title-type parameters | |||
var normalizeParams = ["title"]; | |||
if (specialInfo) { | |||
normalizeParams = normalizeParams.concat(specialInfo.titleParams); | |||
} | |||
for (var key in args) { | |||
var value = args[key]; | |||
if (value === null) continue; | |||
var code = encodeURIComponent(value.toString()); | |||
if (normalizeParams.contains(key)) { | |||
code = this.fixTitle(code); | |||
} | |||
path += encodeURIComponent(key) | |||
+ "=" + code + "&"; | |||
} | |||
return this.site + path.replace(/[?&]$/, ""); | |||
}, | |||
/** | |||
* decode an URL or path into a map of parameters. all titles are normalized. | |||
* if a specialpage has a smushed parameter, it is removed from the title | |||
* and handled like any other parameter. | |||
*/ | |||
decodeURL: function(url, titleFallback) { | |||
var args = {}; | |||
var loc = new jsutil.Loc(url); | |||
// TODO check protocol, host and port | |||
// readPath has the title directly attached, "+" means "+" here! | |||
if (loc.pathname !== this.actionPath) { | |||
var read = loc.pathname.scan(this.readPath); | |||
if (!read) throw "cannot decode: " + url; | |||
args.title = decodeURIComponent(read); | |||
} | |||
// decode all parameters, "+" means " " | |||
if (loc.search) { | |||
var split = loc.search.substring(1).split("&"); | |||
for (i=0; i<split.length; i++) { | |||
var parts = split[i].split("="); | |||
if (parts.length !== 2) continue; | |||
var key = decodeURIComponent(parts[0]); | |||
var code = parts[1].replace(/\+/g, "%20"); | |||
args[key] = decodeURIComponent(code) | |||
} | |||
} | |||
if (!args.title) args.title = titleFallback; | |||
if (!args.title) throw "decode: missing page title in: " + loc; | |||
args.title = this.normalizeTitle(args.title); | |||
// normalize title-type parameters and desmush | |||
var specialInfo = Special.pageInfo(args.title); | |||
if (specialInfo) { | |||
var normalizeParams = specialInfo.titleParams; | |||
for (var i=0; i<normalizeParams.length; i++) { | |||
var key = normalizeParams[i]; | |||
var value = args[key]; | |||
if (value) args[key] = this.normalizeTitle(value); | |||
} | |||
this.desmush(args, specialInfo); | |||
} | |||
return args; | |||
}, | |||
//## hacks | |||
/** returns a smushed copy of a params object */ | |||
smushedParams: function(args) { | |||
var out = jsutil.Object.clone(args); | |||
var specialInfo = Special.pageInfo(this.normalizeTitle(args.title)); | |||
this.smush(out, specialInfo); | |||
return out; | |||
}, | |||
/** returns the owner for a Page */ | |||
owner: function(args) { | |||
var self = this; | |||
function names(title) { | |||
if (!title) return null; | |||
var tmp = Namespace.scan(2, title) || Namespace.scan(3, title); | |||
if (!tmp) return null; | |||
return tmp.replace(/\/.*/, ""); | |||
} | |||
function special() { | |||
var specialInfo = Special.pageInfo(self.normalizeTitle(args.title)); | |||
if (!specialInfo) return null; | |||
var desmushed = jsutil.Object.clone(args); | |||
self.desmush(desmushed, specialInfo); | |||
if (specialInfo.canonicalName === "EmailUser") return desmushed.target; | |||
if (specialInfo.canonicalName === "Contributions") return desmushed.target; | |||
if (specialInfo.canonicalName === "DeletedContributions") return desmushed.target; | |||
if (specialInfo.canonicalName === "BlockIP") return desmushed.ip; | |||
if (specialInfo.canonicalName === "WhatLinksHere") return names(desmushed.target); | |||
if (specialInfo.canonicalName === "RecentChangesLinked") return names(desmushed.target); | |||
if (specialInfo.canonicalName === "Undelete") return names(desmushed.target); | |||
if (specialInfo.canonicalName === "Log") return names(desmushed.page); | |||
// TODO Userrights MovePage CheckUser UserLogin Preferences Export | |||
return null; | |||
} | |||
return names(args.title) || special(); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** replaces Special:Page?key=value with Special:Page/value */ | |||
smush: function(args, specialInfo) { | |||
var param = specialInfo.smushParam; | |||
if (param && args.hasOwnProperty(param)) { | |||
args.title += "/" + args[param]; | |||
delete args[param]; | |||
} | |||
}, | |||
/** replaces Special:Page/value with Special:Page?key=value */ | |||
desmush: function(args, specialInfo) { | |||
var param = specialInfo.smushParam; | |||
var value = specialInfo.smushValue; | |||
if (param && value) { | |||
args.title = Titles.specialPage(specialInfo.localName); | |||
args[param] = value; | |||
} | |||
}, | |||
/** to the user, all titles use " " instead of "_" */ | |||
normalizeTitle: function(title) { | |||
return title.replace(/_/g, " "); | |||
}, | |||
/** some characters are encoded differently in titles */ | |||
fixTitle: jsutil.Text.recoder([ | |||
[ "%%", "%%" ], | |||
[ "%3a", ":" ], | |||
[ "%3A", ":" ], | |||
[ "%2f", "/" ], | |||
[ "%2F", "/" ], | |||
[ "%20", "_" ], | |||
[ "%5f", "_" ], | |||
[ "%5F", "_" ]//, | |||
]), | |||
/** in read urls the title uses literal "+" */ | |||
fixPlus: jsutil.Text.recoder([ | |||
[ "%%", "%%" ], | |||
[ "%2b", "+" ], | |||
[ "%2B", "+" ]//, | |||
]), | |||
//------------------------------------------------------------------------------ | |||
/** to be called onload */ | |||
init: function() { | |||
// for some reason the http: prefix is missing on commons | |||
var hackServer = wgServer.replace(/^\/\//, location.protocol + "//"); | |||
this.site = hackServer; | |||
var m = /([^\/]+\/+)(.*)/.exec(hackServer); | |||
this.protocol = m[1]; | |||
this.domain = m[2]; | |||
this.language = wgContentLanguage; | |||
this.user = wgUserName; | |||
this.readPath = wgArticlePath.replace(/\$1$/, ""); | |||
this.actionPath = wgScriptPath + "/index.php"; | |||
Special.init(this.language); | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## lib/core/Titles.js | |||
// NOTE: named Titles instead of Title to avoid a nameclash with lupin's popups | |||
var Titles = { | |||
/** generates a page title. nsIndex and subPage are optional */ | |||
page: function(nsIndex, page, subPage) { | |||
var title = page; | |||
if (nsIndex !== null) title = Namespace.add(nsIndex, title); | |||
if (subPage) title = title + "/" + subPage; | |||
return title; | |||
}, | |||
/** create a localized page title from the canonical special page name and an optional parameter */ | |||
specialPage: function(name, param) { | |||
var specialNames = Special.pageNames(name); | |||
if (!specialNames) throw "cannot localize specialpage: " + name; | |||
return this.page(-1, specialNames.localName, param); | |||
}, | |||
/** generates a user page title. subPage is optional */ | |||
userPage: function(user, subPage) { | |||
return this.page(2, user, subPage); | |||
}, | |||
/** generates a user talk page title. subPage is optional */ | |||
userTalkPage: function(user, subPage) { | |||
return this.page(3, user, subPage); | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## lib/core/Page.js | |||
/** represents the current Page */ | |||
var Page = { | |||
/** search string of the current location decoded into an Array */ | |||
params: null, | |||
/** the namespace of the current page */ | |||
namespace: null, | |||
/** title for the current URL ignoring redirects */ | |||
title: null, | |||
/** permalink to the current page if one exists or null */ | |||
perma: null, | |||
/** whether this page could be deleted */ | |||
deletable: false, | |||
/** whether this page could be edited */ | |||
editable: false, | |||
/** the user a User or User_talk or Special:Contributions page belongs to */ | |||
owner: null, | |||
/** the existing user this page belongs to, or null */ | |||
ownerExists: null, | |||
//------------------------------------------------------------------------------ | |||
//## public | |||
/** returns the canonical name of the current Specialpage or null */ | |||
whichSpecial: function() { | |||
return this.specialInfo && this.specialInfo.canonicalName; | |||
}, | |||
/** other languages of this page */ | |||
languages: function() { | |||
var languages = []; | |||
var pLang = jsutil.DOM.get('p-lang'); | |||
if (!pLang) return languages; | |||
var lis = pLang.getElementsByTagName("li"); | |||
for (var i=0; i<lis.length; i++) { | |||
var li = lis[i]; | |||
var a = li.firstChild; | |||
if (!a) continue; | |||
languages.push({ | |||
code: li.className.replace(/^interwiki-/, ""), | |||
name: a.textContent, | |||
href: a.href//, | |||
}); | |||
} | |||
return languages; | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** SpecialInfo object */ | |||
specialInfo: null, | |||
/** to be called onload */ | |||
init: function() { | |||
this.params = Wiki.decodeURL(window.location.href, wgPageName); | |||
// wgNamespaceNumber / wgCanonicalNamespace | |||
var m = /(^| )ns-(-?[0-9]+)( |$)/.exec(document.body.className); | |||
if (m) this.namespace = parseInt(m[2]); | |||
// else error | |||
// wgPageName / wgTitle | |||
this.title = this.params.title; | |||
this.deletable = jsutil.DOM.get('ca-delete') !== null; | |||
this.editable = jsutil.DOM.get('ca-edit') !== null; | |||
// #t-permalink | |||
var tPermalink = jsutil.DOM.fetch('t-permalink', "a", null, 0); | |||
if (tPermalink !== null) this.perma = tPermalink.href; | |||
// title is already normalized | |||
this.specialInfo = Special.pageInfo(this.params.title); | |||
// standard way | |||
this.owner = Wiki.owner(this.params); | |||
// try to find a really existing owner | |||
var self = this; | |||
this.ownerExists = (function() { | |||
var tBlockip = jsutil.DOM.fetch('t-blockip', "a", null, 0); | |||
if (tBlockip) return Wiki.decodeURL(tBlockip.href).ip; | |||
var tContributions = jsutil.DOM.fetch('t-contributions', "a", null, 0); | |||
if (tContributions) return Wiki.decodeURL(tContributions.href).target; | |||
if (!self.specialInfo) return null; | |||
if (self.specialInfo.canonicalName !== "Contributions" | |||
&& self.specialInfo.canonicalName !== "DeletedContributions") return null; | |||
// block and deleted exist for admins only | |||
// nonexisting: _ talk logs deleted | |||
// ip: _ talk block blocklog logs deleted | |||
// user: user talk block blocklog logs deleted | |||
// if a blocklog-link exists, the user exists, too. | |||
var as = jsutil.DOM.fetch('contentSub', "a"); | |||
for (var i=0; i<as.length; i++) { | |||
var logA = as[i]; | |||
var logArgs = Wiki.decodeURL(logA.href); | |||
var logInfo = Special.pageInfo(logArgs.title); | |||
if (!logInfo) continue; | |||
if (logInfo.canonicalName === "Log" | |||
&& logArgs.type === "block") return self.params.target; | |||
} | |||
return null; | |||
})(); | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## lib/core/Namespace.js | |||
/** namespaces of the current Wiki */ | |||
var Namespace = { | |||
// TODO check canonical | |||
// TODO allow spaces around the colon (?) | |||
/** the local name of a namespace */ | |||
name: function(nsIndex) { | |||
if (!wgFormattedNamespaces.hasOwnProperty(nsIndex)) throw "unknown Namespace: " + nsIndex; | |||
return wgFormattedNamespaces[nsIndex]; | |||
}, | |||
/** put a page into a namespace */ | |||
add: function(nsIndex, page) { | |||
return this.name(nsIndex) + ":" + page; | |||
}, | |||
/** calculate page title without the namespace or return null */ | |||
scan: function(nsIndex, title) { | |||
return title.scanNoCase(this.name(nsIndex) + ":"); | |||
}, | |||
/** returns an object with internal nsIndex and page */ | |||
split: function(title) { | |||
var split = title.split(/:/); | |||
if (split.length > 1) { | |||
var prefix = split[0].toLowerCase(); | |||
if (wgNamespaceIds.hasOwnProperty(prefix)) { | |||
return { | |||
nsIndex: wgNamespaceIds[prefix], | |||
page: title.replace(/.*?:/, "")//, | |||
}; | |||
} | |||
} | |||
return { | |||
nsIndex: 0, | |||
page: title//, | |||
}; | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## lib/core/Special.js | |||
/** special page specialities */ | |||
var Special = { | |||
/** | |||
* returns an information object for titles like Special:Name or Special:Name/param | |||
* or null for unknown specialPages or non-specialPages. | |||
* | |||
* localName the localized name | |||
* canonicalName the canonical name | |||
* smushParam which parameter may be sushed to this specialPages's title | |||
* param the value of the smushed parameter, or null if none | |||
* titleParams an Array of parameter names for page titles | |||
*/ | |||
pageInfo: function(normalizedTitle) { | |||
var title = Namespace.scan(-1, normalizedTitle); | |||
if (title === null) return null; | |||
// split after the first slash | |||
var m = /(.*?)\/(.*)/.exec(title); | |||
var name; | |||
var param; | |||
var localName; | |||
if (m) { | |||
name = m[1]; | |||
param = m[2]; | |||
} | |||
else { | |||
name = title; | |||
param = null; | |||
} | |||
// NOTE: name may be canonical or local | |||
var names = this.pageNames(name); | |||
if (names == null) throw "could not localize special page: " + name + " for title: " + normalizedTitle; | |||
var localName = names.localName; | |||
var canonicalName = names.canonicalName; | |||
var titleParams = this.titleParams[canonicalName] || []; | |||
var smushParam = this.smush[canonicalName]; | |||
return { | |||
localName: localName, | |||
canonicalName: canonicalName, | |||
smushParam: smushParam, | |||
smushValue: param, | |||
titleParams: titleParams | |||
}; | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** maps lowercased canonical special page names to localized name */ | |||
localPages: null, | |||
/** maps lowercased localized special page names to canonical name */ | |||
canonicalPages: null, | |||
init: function(language) { | |||
var pages = this.pages[language]; | |||
if (!pages) throw "unconfigured pages language: " + language; | |||
// init forward and inverse name maps | |||
this.localPages = {}; | |||
this.canonicalPages = {}; | |||
for (key in pages) { | |||
var value = pages[key]; | |||
this.localPages[key.toLowerCase()] = value; | |||
this.canonicalPages[value.toLowerCase()] = key; | |||
} | |||
}, | |||
/** returns an object containing localName and canonicalName or null for unknown pages */ | |||
pageNames: function(name) { | |||
var canonicalName = this.canonicalPages[name.toLowerCase()]; | |||
var localName = this.localPages[name.toLowerCase()]; | |||
if (!canonicalName && !localName) return null; | |||
if (!canonicalName) canonicalName = this.canonicalPages[localName.toLowerCase()]; | |||
if (!localName) localName = this.localPages[canonicalName.toLowerCase()]; | |||
return { | |||
localName: localName, | |||
canonicalName: canonicalName//, | |||
}; | |||
}, | |||
/** maps language to wgCanonicalSpecialPageName to wgTitle */ | |||
pages: { | |||
de: { | |||
// TODO "Benutzerkonto anlegen" redirects to "Anmelden/signup" | |||
"CreateAccount": "Benutzerkonto anlegen", | |||
"DisambiguationPages": "Begriffsklärungsseiten", | |||
"Nearby": "In der Nähe", | |||
"PagesWithProp": "Seiten mit Eigenschaften", | |||
"DisambiguationPageLinks": "Begriffsklärungslinks", | |||
"Notifications": "Benachrichtigungen", | |||
"OAuthListConsumers": "Verbraucher auflisten", | |||
"OAuthManageMyGrants": "Meine Berechtigungen verwalten", | |||
"Thanks": "Danke", | |||
"ValidationStatistics": "Sichtungsstatistik", | |||
"ArticleFeedbackv5": "Artikelrückmeldungen v5", | |||
"QualityOversight": "QualityOversight", | |||
"PendingChanges": "PendingChanges", | |||
"SimpleSurvey": "SimpleSurvey", | |||
"ArticleFeedback": "ArticleFeedback", | |||
"FeedbackDashboard": "FeedbackDashboard", | |||
"ApiSandbox": "API-Spielwiese", | |||
"PasswordReset": "Passwort neu vergeben", | |||
"Hieroglyphs": "Hieroglyphen", | |||
"Interwiki": "Interwikitabelle", | |||
"ZeroRatedMobileAccess": "ZeroRatedMobileAccess", | |||
"ChangeEmail": "E-Mail-Adresse ändern", | |||
"Unblock": "Freigeben", | |||
// "UploadWizard": "UploadWizard", // does not exist on de.wikipedia | |||
"Protectedtitles": "Geschützte Titel", | |||
"CentralAuth": "Verwaltung Benutzerkonten-Zusammenführung", | |||
"WikiSets": "Wikigruppen", | |||
"ComparePages": "Seiten vergleichen", | |||
"ConfiguredPages": "ConfiguredPages", | |||
"Revisiondelete": "Versionslöschung", | |||
"ProblemChanges": "Seiten mit problematischen Versionen", | |||
"PrefStats": "PrefStats", | |||
"Mypage": "Meine Benutzerseite", | |||
"Nuke": "Massenlöschung", | |||
"Activeusers": "Aktive Benutzer", | |||
"PrefSwitch": "UsabilityInitiativePrefSwitch", | |||
// "UsabilityInitiativeOptIn": "UsabilityInitiativeOptIn", | |||
"GlobalGroupPermissions": "Globale Gruppenrechte", | |||
"Wantedfiles": "Gewünschte Dateien", | |||
"Wantedtemplates": "Gewünschte Vorlagen", | |||
"Resetpass": "Passwort ändern", | |||
"Tags": "Markierungen", | |||
"AbuseLog": "Missbrauchsfilter-Logbuch", | |||
"Book": "Buch", | |||
"UnstablePages": "Unstabile Seiten", | |||
"AbuseFilter": "Missbrauchsfilter", | |||
"ValidationStatistics": "Markierungsstatistik", | |||
"GlobalBlockStatus": "Ausnahme von globaler Sperre", | |||
"AutoLogin": "AutoLogin", | |||
"Captcha": "Captcha", | |||
"MergeHistory": "Versionsgeschichten vereinen", | |||
"GlobalBlockList": "Liste globaler Sperren", | |||
"Userrights": "Benutzerrechte", | |||
"ReviewedPages": "Gesichtete Seiten", | |||
"StablePages": "Konfigurierte Seiten", | |||
"QualityOversight": "Markierungsübersicht", | |||
"OldReviewedPages": "Seiten mit ungesichteten Versionen", | |||
"RevisionReview": "RevisionReview", | |||
"UnreviewedPages": "Ungesichtete Seiten", | |||
"DepreciationOversight": "Zurückgezogene Markierungen", | |||
"Listgrouprights": "Gruppenrechte", | |||
"Mostlinkedtemplates": "Meistbenutzte Vorlagen", | |||
"Uncategorizedtemplates": "Nicht kategorisierte Vorlagen", | |||
"Gadgets": "Helferlein", | |||
"GlobalUsers": "Globale Benutzerliste", | |||
"ParserDiffTest": "Parser-Differenz-Test", //### the link title on Special:SpecialPages is ParserDiffTest on wp:de | |||
"MergeAccount": "Benutzerkonten zusammenführen", | |||
"FileDuplicateSearch": "Dateiduplikatsuche", | |||
"DeletedContributions": "Gelöschte Beiträge", | |||
"Contributions": "Beiträge", | |||
"SpecialPages": "Spezialseiten", | |||
"Emailuser": "E-Mail senden", | |||
"Whatlinkshere": "Linkliste", | |||
"MovePage": "Verschieben", | |||
"CheckUser": "CheckUser", | |||
"Recentchangeslinked": "Änderungen an verlinkten Seiten", | |||
"Protectedpages": "Geschützte Seiten", | |||
"Fewestrevisions": "Wenigstbearbeitete Seiten", | |||
"Withoutinterwiki": "Fehlende Interwikis", | |||
"Allpages": "Alle Seiten", | |||
"Userlogin": "Anmelden", | |||
"CrossNamespaceLinks": "Seiten mit Links in andere Namensräume", | |||
"Mostrevisions": "Meistbearbeitete Seiten", | |||
"Disambiguations": "Begriffsklärungsverweise", | |||
"Listusers": "Benutzer", | |||
"Wantedcategories": "Gewünschte Kategorien", | |||
"Watchlist": "Beobachtungsliste", | |||
"Listfiles": "Dateien", | |||
"Filepath": "Dateipfad", | |||
"DoubleRedirects": "Doppelte Weiterleitungen", | |||
"Preferences": "Einstellungen", | |||
"Wantedpages": "Gewünschte Seiten", | |||
"Upload": "Hochladen", | |||
"Mostlinked": "Meistverlinkte Seiten", | |||
"Booksources": "ISBN-Suche", | |||
"BrokenRedirects": "Defekte Weiterleitungen", | |||
"Categories": "Kategorien", | |||
"CategoryTree": "Kategorienbaum", | |||
"Shortpages": "Kürzeste Seiten", | |||
"LongPages": "Längste Seiten", | |||
"Recentchanges": "Letzte Änderungen", | |||
"Ipblocklist": "Liste der Sperren", | |||
"SiteMatrix": "Liste der Wikimedia-Wikis", | |||
"Log": "Logbuch", | |||
"Mostcategories": "Meistkategorisierte Seiten", | |||
"Mostimages": "Meistbenutzte Dateien", | |||
"Mostlinkedcategories": "Meistbenutzte Kategorien", | |||
"Newpages": "Neue Seiten", | |||
"Newimages": "Neue Dateien", | |||
"Unusedtemplates": "Unbenutzte Vorlagen", | |||
"Uncategorizedpages": "Nicht kategorisierte Seiten", | |||
"Uncategorizedimages": "Nicht kategorisierte Dateien", | |||
"Uncategorizedcategories": "Nicht kategorisierte Kategorien", | |||
"Prefixindex": "Präfixindex", | |||
"Deadendpages": "Sackgassenseiten", | |||
"Ancientpages": "Älteste Seiten", | |||
"Export": "Exportieren", | |||
"Allmessages": "MediaWiki-Systemnachrichten", | |||
"Statistics": "Statistik", | |||
"Search": "Suche", | |||
"MIMEsearch": "MIME-Typ-Suche", | |||
"Version": "Version", | |||
"Unusedimages": "Unbenutzte Dateien", | |||
"Unusedcategories": "Unbenutzte Kategorien", | |||
"Lonelypages": "Verwaiste Seiten", | |||
"ExpandTemplates": "Vorlagen expandieren", | |||
"Boardvote": "Boardvote", | |||
"LinkSearch": "Weblinksuche", | |||
"Listredirects": "Weiterleitungen", | |||
"Cite": "Zitierhilfe", | |||
"RandomRedirect": "Zufällige Weiterleitung", | |||
"Random": "Zufällige Seite", | |||
"Undelete": "Wiederherstellen", | |||
"BlockIP": "Sperren", | |||
"UnwatchedPages": "Ignorierte Seiten", | |||
"Import": "Importieren"//, | |||
}, | |||
// en is (mostly) canonical, so this is almost an identity mapping | |||
en: { | |||
// TODO "???" redirects to "Userlogin/signup" | |||
"CreateAccount": "CreateAccount", | |||
"DisambiguationPages": "DisambiguationPages", | |||
"Nearby": "Nearby", | |||
"PagesWithProp": "PagesWithProp", | |||
"DisambiguationPageLinks": "DisambiguationPageLinks", | |||
"Notifications": "Notifications", | |||
"OAuthListConsumers": "OAuthListConsumers", | |||
"OAuthManageMyGrants": "OAuthManageMyGrants", | |||
"Thanks": "Thanks", | |||
"ValidationStatistics": "ValidationStatistics", | |||
"ArticleFeedbackv5": "ArticleFeedbackv5", | |||
"QualityOversight": "AdvancedReviewLog", | |||
"PendingChanges": "PendingChanges", | |||
"SimpleSurvey": "SimpleSurvey", | |||
"ArticleFeedback": "ArticleFeedback", | |||
"FeedbackDashboard": "FeedbackDashboard", | |||
"ApiSandbox": "ApiSandbox", | |||
"PasswordReset": "PasswordReset", | |||
"Hieroglyphs": "Hieroglyphs", | |||
"Interwiki": "Interwiki", | |||
"ZeroRatedMobileAccess": "ZeroRatedMobileAccess", | |||
"ChangeEmail": "ChangeEmail", | |||
"Unblock": "Unblock", | |||
"UploadWizard": "UploadWizard", | |||
"CentralAuth": "CentralAuth", | |||
"WikiSets": "WikiSets", | |||
"ComparePages": "ComparePages", | |||
"ConfiguredPages": "ConfiguredPages", | |||
"Revisiondelete": "Revisiondelete", | |||
"ProblemChanges": "ProblemChanges", | |||
"PrefStats": "PrefStats", | |||
"Mypage": "Mypage", | |||
"Nuke": "Nuke", | |||
"Activeusers": "Activeusers", | |||
"PrefSwitch": "UsabilityInitiativePrefSwitch", | |||
"UsabilityInitiativeOptIn": "UsabilityInitiativeOptIn", | |||
"GlobalGroupPermissions": "GlobalGroupPermissions", | |||
"Wantedfiles": "WantedFiles", | |||
"Wantedtemplates": "Wantedtemplates", | |||
"Resetpass": "ChangePassword", | |||
"Tags": "Tags", | |||
"AbuseLog": "AbuseLog", | |||
"Book": "Book", | |||
"UnstablePages": "UnstablePages", | |||
"AbuseFilter": "AbuseFilter", | |||
"ValidationStatistics": "ValidationStatistics", | |||
"GlobalBlockStatus": "GlobalBlockStatus", | |||
"AutoLogin": "AutoLogin", | |||
"Captcha": "Captcha", | |||
"MergeHistory": "Versionsgeschichten vereinen", | |||
"GlobalBlockList": "GlobalBlockList", | |||
"Userrights": "Userrights", | |||
"ReviewedPages": "ReviewedPages", | |||
"StablePages": "StablePages", | |||
"QualityOversight": "QualityOversight", | |||
"OldReviewedPages": "OldReviewedPages", | |||
"RevisionReview": "RevisionReview", | |||
"UnreviewedPages": "UnreviewedPages", | |||
"DepreciationOversight": "DepreciationOversight", | |||
"Protectedtitles": "ProtectedTitles", // attention, not equal | |||
"Listgrouprights": "ListGroupRights", // attention, not equal | |||
"Mostlinkedtemplates": "MostLinkedTemplates", // attention, not equal | |||
"Uncategorizedtemplates": "UncategorizedTemplates", // attention, not equal | |||
"Gadgets": "Gadgets", | |||
"GlobalUsers": "GlobalUsers", | |||
"ParserDiffTest": "ParserDiffTest", | |||
"MergeAccount": "MergeAccount", | |||
"FileDuplicateSearch": "FileDuplicateSearch", | |||
"DeletedContributions": "DeletedContributions", | |||
"Contributions": "Contributions", | |||
"SpecialPages": "SpecialPages", | |||
"Emailuser": "EmailUser", | |||
"Whatlinkshere": "WhatLinksHere", | |||
"MovePage": "MovePage", | |||
"CheckUser": "CheckUser", | |||
"Recentchangeslinked": "RecentChangesLinked", | |||
"Protectedpages": "ProtectedPages", | |||
"Fewestrevisions": "FewestRevisions", | |||
"Withoutinterwiki": "WithoutInterwiki", | |||
"Allpages": "AllPages", | |||
"Userlogin": "UserLogin", | |||
"CrossNamespaceLinks": "CrossNamespaceLinks", | |||
"Mostrevisions": "MostRevisions", | |||
"Disambiguations": "Disambiguations", | |||
"Listusers": "ListUsers", | |||
"Wantedcategories": "WantedCategories", | |||
"Watchlist": "Watchlist", | |||
"Listfiles": "ListFiles", // attention, not equal | |||
"Filepath": "FilePath", | |||
"DoubleRedirects": "DoubleRedirects", | |||
"Preferences": "Preferences", | |||
"Wantedpages": "WantedPages", | |||
"Upload": "Upload", | |||
"Mostlinked": "MostLinkedPages", | |||
"Booksources": "BookSources", | |||
"BrokenRedirects": "BrokenRedirects", | |||
"Categories": "Categories", | |||
"CategoryTree": "CategoryTree", | |||
"Shortpages": "ShortPages", | |||
"LongPages": "LongPages", | |||
"Recentchanges": "RecentChanges", | |||
"Ipblocklist": "BlockList", | |||
"SiteMatrix": "SiteMatrix", | |||
"Log": "Log", | |||
"Mostcategories": "MostCategories", | |||
"Mostimages": "MostLinkedFiles", | |||
"Mostlinkedcategories": "MostLinkedCategories", | |||
"Newpages": "NewPages", | |||
"Newimages": "NewFiles", // attention, not equal | |||
"Unusedtemplates": "UnusedTemplates", | |||
"Uncategorizedpages": "UncategorizedPages", | |||
"Uncategorizedimages": "UncategorizedFiles", | |||
"Uncategorizedcategories": "UncategorizedCategories", | |||
"Prefixindex": "PrefixIndex", | |||
"Deadendpages": "DeadendPages", | |||
"Ancientpages": "AncientPages", | |||
"Export": "Export", | |||
"Allmessages": "AllMessages", | |||
"Statistics": "Statistics", | |||
"Search": "Search", | |||
"MIMEsearch": "MIMESearch", | |||
"Version": "Version", | |||
"Unusedimages": "UnusedFiles", | |||
"Unusedcategories": "UnusedCategories", | |||
"Lonelypages": "LonelyPages", | |||
"ExpandTemplates": "ExpandTemplates", | |||
"Boardvote": "Boardvote", | |||
"LinkSearch": "Linksearch", | |||
"Listredirects": "ListRedirects", | |||
"Cite": "Cite", | |||
"RandomRedirect": "RandomRedirect", | |||
"Random": "Random", | |||
"Undelete": "Undelete", | |||
"BlockIP": "BlockIP", | |||
"UnwatchedPages": "UnwatchedPages", | |||
"Import": "Import"//, | |||
}//, | |||
}, | |||
/** some parameters of special pages point to pages, in this case space and underscore mean the same */ | |||
titleParams: { | |||
"EmailUser": [ "target" ], | |||
"Contributions": [ "target" ], | |||
"DeletedContributions": [ "target" ], | |||
"Whatlinkshere": [ "target" ], | |||
"Recentchangeslinked": [ "target" ], | |||
"Undelete": [ "target" ], | |||
"Allpages": [ "from" ], | |||
"Prefixindex": [ "from" ], | |||
"BlockIP": [ "ip" ], | |||
"Log": [ "page" ], | |||
"Filepath": [ "file" ], | |||
"Randompage": [ "namespace" ]//, | |||
}, | |||
/** some special pages can smush one parameter to the page title */ | |||
smush: { | |||
"EmailUser": "target", | |||
"Contributions": "target", | |||
"DeletedContributions": "target", | |||
"Whatlinkshere": "target", | |||
"Recentchangeslinked": "target", | |||
"Undelete": "target", | |||
"LinkSearch": "target", | |||
"Newpages": "limit", | |||
"Newimages": "limit", | |||
"Wantedpages": "limit", | |||
"Recentchanges": "limit", | |||
"Allpages": "from", | |||
"Prefixindex": "from", | |||
"Log": "type", | |||
"BlockIP": "ip", | |||
"Listusers": "group", | |||
"Filepath": "file", | |||
"Randompage": "namespace", | |||
"Watchlist": "action", | |||
// Contributions | |||
// a smushed /newbies does not mean a user named "newbies"! | |||
// Randompage | |||
// the namespace parameter is fake, it exists only in smushed form | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## 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 = jsutil.Text.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 = jsutil.Text.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 = jsutil.Function.identity; | |||
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["wpSummary"].value = summary; | |||
} | |||
if (minor !== null && !!form.elements["wpMinoredit"]) { | |||
form.elements["wpMinoredit"].checked = minor; | |||
} | |||
var text = form.elements["wpTextbox1"].value; | |||
if (textFunc) { | |||
text = text.replace(/^[\r\n]+$/, ""); | |||
text = textFunc(text); | |||
if (text === null) { feedback.failure("aborted"); return false; } | |||
} | |||
form.elements["wpTextbox1"].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[key]; } | |||
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[i]; | |||
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 args = { | |||
title: title, | |||
action: action//, | |||
}; | |||
function change(form, doc) { | |||
return true; | |||
} | |||
this.action(feedback, args, 0, change, 200, doneFunc); | |||
}, | |||
/** 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["wpOldTitle"].value = oldTitle; | |||
form.elements["wpNewTitle"].value = newTitle; | |||
form.elements["wpReason"].value = reason; | |||
if (form.elements["wpMovetalk"]) | |||
form.elements["wpMovetalk"].checked = moveTalk; | |||
if (form.elements["wpMovesubpages"]) | |||
form.elements["wpMovesubpages"].checked = moveSub; | |||
if (form.elements["wpLeaveRedirect"]) | |||
form.elements["wpLeaveRedirect"].checked = leaveRedirect; | |||
form.elements["wpWatch"].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 " + 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(); | |||
} | |||
jsutil.Ajax.call({ | |||
method: "GET", | |||
url: 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 = jsutil.Text.joinPrintable(" - ", [ | |||
reason, | |||
form.elements["wpReason"].value ]); | |||
} | |||
else { | |||
reason = ""; | |||
} | |||
form.elements["wpReason"].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 = jsutil.Text.joinPrintable(" - ", [ | |||
reason, | |||
form.elements["wpReason"].value ]); | |||
} | |||
else { | |||
reason = ""; | |||
} | |||
form.elements["wpReason"].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["mwProtect-level-edit"]) | |||
form.elements["mwProtect-level-edit"].value = levelEdit; // plus mwProtectExpirySelection-edit named wpProtectExpirySelection-edit | |||
if (form.elements["mwProtect-edit-expires"]) | |||
form.elements["mwProtect-edit-expires"].value = expiryEdit; // named mwProtect-expiry-edit | |||
// for existing pages | |||
if (form.elements["mwProtect-level-move"]) | |||
form.elements["mwProtect-level-move"].value = levelMove; // plus mwProtectExpirySelection-move named wpProtectExpirySelection-move | |||
if(form.elements["mwProtect-move-expires"]) | |||
form.elements["mwProtect-move-expires"].value = expiryMove; // named mwProtect-expiry-move | |||
// for deleted pages | |||
if (form.elements["mwProtect-level-create"]) | |||
form.elements["mwProtect-level-create"].value = levelCreate; // plus mwProtectExpirySelection-create named wpProtectExpirySelection-create | |||
if (form.elements["mwProtect-create-expires"]) | |||
form.elements["mwProtect-create-expires"].value = expiryMove; // named mwProtect-expiry-create | |||
// for both deleted and existing pages | |||
form.elements["mwProtect-cascade"].checked = cascade; | |||
form.elements["mwProtect-reason"].value = reason; // plus wpProtectReasonSelection | |||
form.elements["mwProtectWatch"].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. | |||
* createAccount defaults to true | |||
* hardBlock, autoBlock, disableEmail and disableUTEdit default to false | |||
* hardBlock makes sense for ip users, autoBlock for logged in users | |||
* expiry may be "indefinite", | |||
* or a number followed by a space and | |||
* "years", "months", "days", "hours" or "minutes" | |||
*/ | |||
blockUser: function(feedback, user, expiry, reason, autoBlock, hardBlock, createAccount, disableEmail, disableUTEdit, watchUT, 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) { | |||
var hasError = form.getElementsByClassName("error").length !== 0; | |||
if (!allowChange && hasError) return false; | |||
form.elements["wpTarget"].value = user; | |||
form.elements["wpExpiry-other"].value = expiry; | |||
form.elements["wpReason-other"].value = reason; | |||
form.elements["wpCreateAccount"].checked = createAccount; | |||
form.elements["wpDisableEmail"].checked = disableEmail; | |||
form.elements["wpDisableUTEdit"].checked = disableUTEdit; | |||
form.elements["wpAutoBlock"].checked = autoBlock; | |||
form.elements["wpHardBlock"].checked = hardBlock; | |||
form.elements["wpWatch"].checked = watchUT; | |||
// TODO wpConfirm to true for self-blocking | |||
return true; | |||
} | |||
this.action(feedback, args, 0, 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["wpSubject"].value = subject; | |||
form.elements["wpText"].value = body; | |||
form.elements["wpCCMe"].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 = jsutil.XML.parseXML(text); | |||
} | |||
catch (e) { | |||
feedback.failure("cannot parse XML: " + e); | |||
return; | |||
} | |||
if (doc.getElementById('wikiPreview')) { | |||
feedback.failure("cannot save, preview detected"); | |||
return; | |||
} | |||
var form = jsutil.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 " + wgScript + " with " + self.debugArgsString(actionArgs)); | |||
jsutil.Ajax.call({ | |||
method: "GET", | |||
url: 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 = jsutil.XML.parseXML(source.responseText); | |||
} | |||
catch (e) { | |||
feedback.failure("cannot parse XML: " + e); | |||
return; | |||
} | |||
// get form | |||
var form = jsutil.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.getAttribute("action"); | |||
var data = jsutil.Form.serialize(form); | |||
feedback.work("POST " + url); | |||
jsutil.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 = wgScriptPath + "/api.php"; | |||
var bodyParams = { | |||
format: "json"//, | |||
}; | |||
Object.assign(bodyParams, args); | |||
feedback.work("POST " + apiPath); | |||
jsutil.Ajax.call({ | |||
url: apiPath, | |||
method: "POST", | |||
bodyParams: bodyParams, | |||
successFunc: phase2, | |||
noSuccessFunc: failure//, | |||
}); | |||
} | |||
function phase2(source) { | |||
var text = source.responseText; | |||
var json; | |||
try { | |||
json = JSON.parse(text); | |||
} | |||
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[key]; | |||
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); | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## lib/core/Markup.js | |||
/** WikiText generator */ | |||
var Markup = { | |||
//------------------------------------------------------------------------------ | |||
//## complex | |||
/** make a link to a user page */ | |||
userLink: function(user, label) { | |||
return this.link(Titles.userPage(user), label); | |||
}, | |||
/** make a link to a user's talkpage */ | |||
talkLink: function(user, label) { | |||
return this.link(Titles.userTalkPage(user), label); | |||
}, | |||
/** make a link to a user's contributions */ | |||
contribsLink: function(user, label) { | |||
return this.link(Titles.specialPage("Contributions", user), label); | |||
}, | |||
/** make a link to a lemma preventing inclusion */ | |||
referenceLink: function(title) { | |||
var split = Namespace.split(title); | |||
if (split.nsIndex === 6 | |||
|| split.nsIndex === 10 | |||
|| split.nsIndex === 14) { | |||
return this.link(":" + title, title); | |||
} | |||
else { | |||
return this.link(title); | |||
} | |||
}, | |||
/** make a redirect link */ | |||
redirect: function(title) { | |||
return "#redirect " + this.link(title); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## enclosing | |||
// enclosing | |||
template: function() { return this.TEMPLATE_ + Array.from(arguments).join(this._TEMPLATE_) + this._TEMPLATE; }, | |||
link: function() { return this.LINK_ + Array.from(arguments).join(this._LINK_) + this._LINK; }, | |||
web: function() { return this.WEB_ + Array.from(arguments).join(this._WEB_) + this._WEB; }, | |||
h2: function(text) { return this.H2_ + text + this._H2; }, | |||
//------------------------------------------------------------------------------ | |||
//## composite | |||
SIGAPP: " -- ~\~\~\~\n", | |||
DASH: "--", // "—" em dash U+2014 — | |||
//------------------------------------------------------------------------------ | |||
//## tokens | |||
// enclosing | |||
TEMPLATE_: "\{\{", | |||
_TEMPLATE_: "\|", | |||
_TEMPLATE: "\}\}", | |||
LINK_: "\[\[", | |||
_LINK_: "\|", | |||
_LINK: "\]\]", | |||
WEB_: "\[", | |||
_WEB_: " ", | |||
_WEB: "\]", | |||
H2_: "==", | |||
_H2: "==", | |||
// simple | |||
SIG: "~\~\~\~", | |||
LINE: "----", | |||
// control chars | |||
STAR: "*", | |||
HASH: "#", | |||
COLON: ":", | |||
SEMI: ";", | |||
SP: " ", | |||
LF: "\n"//, | |||
}; | |||
//====================================================================== | |||
//## lib/core/WikiLink.js | |||
/** the label is optional */ | |||
function WikiLink(title, label) { | |||
this.title = title; | |||
this.label = label; | |||
} | |||
WikiLink.prototype = { | |||
/** the title with spaces instead of underscores */ | |||
normalizedTitle: function() { | |||
return title.replace(/_/g, " "); | |||
}, | |||
/** the label if set, the normalized title if not */ | |||
displayedLabel: function() { | |||
return label ? label : this.normalizedTitle(); | |||
}, | |||
/** omits the label if it equals the title */ | |||
toPrettyString: function() { | |||
var title = this.normalizedTitle(); | |||
var label = this.displayedLabel(); | |||
return title !== label | |||
? Markup.link(title, label) | |||
: Markup.link(title); | |||
}, | |||
/** simple variant, omits the label if it's null */ | |||
toString: function() { | |||
return this.label !== null | |||
? Markup.link(this.title, this.label) | |||
: Markup.link(this.title); | |||
}//, | |||
}; | |||
/** mangle all WikiLinks in a WikiText */ | |||
WikiLink.changeAll = function(wikiText, changeFunc) { | |||
var re = /\[\[[ \t]*([^\]|]+?)[ \t]*(\|[ \t]*([^\]]*?)[ \t]*)?\]\]/g; | |||
return wikiText.replace(re, function($0, $1, $2, $3, pos, string) { | |||
var title = $1; | |||
var second = $2; | |||
var label = $3; | |||
if (!second) label = null; | |||
var orig = new WikiLink(title, label); | |||
var changed = changeFunc(orig); | |||
return changed !== null ? changed.toString() : $0; | |||
}); | |||
} | |||
/** returns an Array of all WikiLinks contained in a WikiText */ | |||
WikiLink.parseAll = function(wikiText) { | |||
var out = []; | |||
this.changeAll(wikiText, function(wikiLink) { | |||
out.push(wikiLink); | |||
return null; | |||
}); | |||
return out; | |||
} | |||
//====================================================================== | |||
//## lib/core/Config.js | |||
/** helper for wiki-local settings */ | |||
var Config = { | |||
/** configure a configuration for the current wiki domain or language */ | |||
patch: function(cfg) { | |||
var language = cfg[Wiki.language]; | |||
var domain = cfg[Wiki.domain]; | |||
if (language) Object.assign(cfg, language); | |||
if (domain) Object.assign(cfg, domain); | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## lib/core/IP.js | |||
// @see mw.util.isIPv4Address | |||
// @see mw.util.isIPv6Address | |||
var IP = { | |||
/** matches IPv4-like strings */ | |||
v4RE: /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/, | |||
/** whether the string denotes an IPv4-address */ | |||
isV4: function(s) { | |||
var m = this.v4RE.exec(s); | |||
if (!m) return false; | |||
for (var i=1; i<=4; i++) { | |||
var byt = parseInt(m[i]); | |||
if (byt < 0 || byt > 255) return false; | |||
} | |||
return true; | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## lib/core/Afterwards.js | |||
/** creates functions to be used after user actions */ | |||
var Afterwards = { | |||
/** returns a function reloading the page if changed and current page are the same */ | |||
maybeReloadFunc: function(title) { | |||
return this.maybeReload.bind(this, title); | |||
}, | |||
/** returns a function loading the history of a page if changed and current page are the same */ | |||
maybeHistoryFunc: function(title) { | |||
return this.maybeHistory.bind(this, title); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
/** reload a page if it is the current page */ | |||
maybeReload: function(title) { | |||
if (title !== Page.title) return; | |||
window.location.href = Wiki.readURL(title); | |||
}, | |||
/** show a pages history if it is the current page */ | |||
maybeHistory: function(title) { | |||
if (title !== Page.title) return; | |||
window.location.href = Wiki.encodeURL({ | |||
title: title, | |||
action: "history"//, | |||
}); | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## lib/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; | |||
} | |||
//====================================================================== | |||
//## lib/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; | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## lib/ui/SwitchBoard.js | |||
/** contains a number of on/off-switches */ | |||
function SwitchBoard() { | |||
this.knobs = []; | |||
this.board = document.createElement("span"); | |||
this.board.className = "switch-board"; | |||
// public | |||
this.component = this.board; | |||
} | |||
SwitchBoard.prototype = { | |||
/** add a knob and set its className */ | |||
add: function(knob) { | |||
jsutil.DOM.addClass(knob, "switch-knob"); | |||
jsutil.DOM.addClass(knob, "switch-off"); | |||
this.knobs.push(knob); | |||
this.board.appendChild(knob); | |||
}, | |||
/** selects a single knob */ | |||
select: function(knob) { | |||
this.changeAll(false); | |||
this.change(knob, true); | |||
}, | |||
/** changes selection state of one knob */ | |||
change: function(knob, selected) { | |||
if (selected) jsutil.DOM.replaceClass(knob, "switch-off", "switch-on"); | |||
else jsutil.DOM.replaceClass(knob, "switch-on", "switch-off"); | |||
}, | |||
/** changes selection state of all knobs */ | |||
changeAll: function(selected) { | |||
for (var i=0; i<this.knobs.length; i++) { | |||
this.change(this.knobs[i], selected); | |||
} | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## lib/ui/Floater.js | |||
/** a Floater is a small area floating over the document */ | |||
function Floater(id, limited) { | |||
this.limited = limited; | |||
// public | |||
this.canvas = document.createElement("div"); | |||
this.canvas.id = id; | |||
this.canvas.className = "floater"; | |||
// shortcut | |||
this.style = this.canvas.style; | |||
// attaching to a node below body leads to clipping: overflow:visible maybe? | |||
//this.source.appendChild(this.canvas); | |||
document.body.appendChild(this.canvas); | |||
Floater.instances.push(this); | |||
this.style.zIndex = this.minimumZ + Floater.instances.length - 1; | |||
} | |||
Floater.prototype = { | |||
/** z-index for the lowest Floater */ | |||
minimumZ: 1000, | |||
/** removes this Floater from the view */ | |||
destroy: function() { | |||
Floater.instances.remove(this); | |||
// TODO: change all other fields like it was removed | |||
document.body.removeChild(this.canvas); | |||
}, | |||
/** locates the div near a mouse position */ | |||
locate: function(pos) { | |||
// helps with https://bugzilla.mozilla.org/show_bug.cgi?id=324819 | |||
// display is necessary for position, visibility is not | |||
this.style.display = "block"; | |||
if (this.limited) pos = this.limit(pos); | |||
jsutil.DOM.moveElement(this.canvas, pos); | |||
}, | |||
/** limits canvas position to the window */ | |||
limit: function(pos) { | |||
var min = jsutil.DOM.minPos(); | |||
var max = jsutil.DOM.maxPos(); | |||
var size = jsutil.DOM.elementSize(this.canvas); | |||
// HACK: why does the menu go too far to the right without this? | |||
size.x += 16; | |||
pos = { x: pos.x, y: pos.y }; | |||
if (pos.x < min.x) pos.x = min.x; | |||
if (pos.y < min.y) pos.y = min.y; | |||
if (pos.x + size.x > max.x) pos.x = max.x - size.x; | |||
if (pos.y + size.y > max.y) pos.y = max.y - size.y; | |||
return pos; | |||
}, | |||
/** returns the current location */ | |||
location: function() { | |||
// display is necessary for position, visibility is not | |||
this.style.display = "block"; | |||
return jsutil.DOM.elementPos(this.canvas); | |||
}, | |||
/** displays the div */ | |||
show: function() { | |||
this.style.display = "block"; | |||
this.style.visibility = "visible"; | |||
}, | |||
/** hides the div */ | |||
hide: function() { | |||
this.style.display = "none"; | |||
this.style.visibility = "hidden"; | |||
}, | |||
/** raises the div above all other Floaters */ | |||
raise: function() { | |||
var all = Floater.instances; | |||
var idx = all.indexOf(this); | |||
if (idx === -1) return; | |||
all.splice(idx, 1); | |||
all.push(this); | |||
for (var i=idx; i<all.length; i++) { | |||
all[i].style.zIndex = i + this.minimumZ; | |||
} | |||
}, | |||
/** lower the div below all other Floaters */ | |||
lower: function() { | |||
var all = Floater.instances; | |||
var idx = all.indexOf(this); | |||
if (idx === -1) return; | |||
all.splice(idx, 1); | |||
all.unshift(this); | |||
for (var i=idx; i>= 0; i++) { | |||
all[i].style.zIndex = i + this.minimumZ; | |||
} | |||
}//, | |||
}; | |||
/** all instances z-ordered starting with the lowest */ | |||
Floater.instances = []; | |||
//====================================================================== | |||
//## lib/ui/PopupMenu.js | |||
/** a PopupMenu display a number of items and call a selectFunc when one of the items is selected */ | |||
function PopupMenu(selectFunc) { | |||
this.selectFunc = selectFunc; | |||
this.floater = new Floater(null, true); | |||
this.canvas = this.floater.canvas; | |||
jsutil.DOM.addClass(this.canvas, "popup-menu-window"); | |||
this.canvas.onmouseup = this.maybeSelectItem.bind(this); | |||
} | |||
PopupMenu.prototype = { | |||
/** removes this menu */ | |||
destroy: function() { | |||
this.canvas.onmouseup = null; | |||
this.floater.destroy(); | |||
}, | |||
/** opens at a given position */ | |||
showAt: function(pos) { | |||
this.floater.locate(pos); | |||
this.floater.raise(); | |||
this.floater.show(); | |||
}, | |||
/** closes the menu */ | |||
hide: function() { | |||
this.floater.hide(); | |||
}, | |||
/** adds an item, its userdata will be supplied to the selectFunc */ | |||
item: function(label, userdata) { | |||
var item = document.createElement("div"); | |||
item.className = "popup-menu-item"; | |||
item.textContent = label; | |||
item.userdata = userdata; | |||
this.canvas.appendChild(item); | |||
}, | |||
/** adds a separator */ | |||
separator: function() { | |||
var separator = document.createElement("hr"); | |||
separator.className = "popup-menu-separator"; | |||
this.canvas.appendChild(separator); | |||
}, | |||
/** calls the selectFunc with the userData of the selected item */ | |||
maybeSelectItem: function(ev) { | |||
var target = ev.target; | |||
for (;;) { | |||
if (jsutil.DOM.hasClass(target, "popup-menu-item")) { | |||
if (this.selectFunc) { | |||
this.selectFunc(target.userdata); | |||
} | |||
return; | |||
} | |||
target = target.parentNode; | |||
if (!target) return; | |||
} | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## lib/ui/PopupSource.js | |||
/** makes a source open a Floater as context-menu */ | |||
function PopupSource(source, menu) { | |||
this.source = source; | |||
this.menu = menu; | |||
jsutil.DOM.addClass(source, "popup-source"); | |||
source.oncontextmenu = this.contextMenu.bind(this); | |||
this.boundMouseUp = this.mouseUp.bind(this); | |||
document.addEventListener("mouseup", this.boundMouseUp, false); | |||
} | |||
PopupSource.prototype = { | |||
mouseupCloseDelay: 250, | |||
/** removes all listeners */ | |||
destroy: function() { | |||
jsutil.DOM.removeClass(this.source, "popup-source"); | |||
this.source.oncontextmenu = null; | |||
document.removeEventListener("mouseup", this.boundMouseUp, false); | |||
}, | |||
/** opens the Floater near the mouse cursor */ | |||
contextMenu: function(ev) { | |||
if (ev.target !== this.source) return; | |||
var mouse = jsutil.DOM.mousePos(ev); | |||
// so the document does not get a mouseup shortly after | |||
mouse.x ++; | |||
this.menu.showAt(mouse); | |||
// delay closing so the popup stays open after a short klick | |||
this.abortable = false; | |||
var self = this; | |||
window.setTimeout( | |||
function() { self.abortable = true; }, | |||
this.mouseupCloseDelay); | |||
// old-style, stop propagation | |||
return false; | |||
}, | |||
/** closes the Floater except within a short time after opening */ | |||
mouseUp: function(ev) { | |||
if (this.abortable) { | |||
this.menu.hide(); | |||
} | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## lib/ui/Links.js | |||
/** creates links to action functions and pages */ | |||
var Links = { | |||
/** | |||
* create an action link which | |||
* - onclick queries a text and | |||
* - oncontextmenu opens a popup with default texts | |||
* and calls the single-argument selectFunc with it. | |||
* | |||
* promptLabel may be null to indicate no prompt is | |||
* wanted, the selecteFunc is called with null in this case. | |||
* | |||
* groups is an Array of Arrays of preset reason Strings. | |||
* a separator is placed between rows. null is allowed to | |||
* disable the popup. | |||
*/ | |||
promptPopupLink: function(label, promptLabel, groups, selectFunc) { | |||
var link = promptLabel !== null | |||
? this.promptLink(label, promptLabel, selectFunc) | |||
: this.functionLink(label, selectFunc.bind(this, null)); | |||
if (groups) this.popupOnLink(link, groups, selectFunc); | |||
return link; | |||
}, | |||
/** add a popup calling a function to an existing link */ | |||
popupOnLink: function(link, groups, popupFunc) { | |||
var popup = new PopupMenu(popupFunc); | |||
// setup groups of items | |||
for (var i=0; i<groups.length; i++) { | |||
var group = groups[i]; | |||
if (i !== 0) popup.separator(); | |||
for (var j=0; j<group.length; j++) { | |||
var preset = group[j]; | |||
popup.item(preset, preset); | |||
} | |||
} | |||
new PopupSource(link, popup); | |||
}, | |||
/** create an action link which onclick queries a text and calls a function with it */ | |||
promptLink: function(label, promptLabel, okFunc) { | |||
return this.functionLink(label, function() { | |||
var reason = prompt(promptLabel); | |||
if (reason !== null) okFunc(reason); | |||
}); | |||
}, | |||
/** create an action link calling a function on click */ | |||
functionLink: function(label, clickFunc) { | |||
var a = document.createElement("a"); | |||
a.className = "link-function"; | |||
a.onclick = clickFunc; | |||
a.textContent = label; | |||
return a; | |||
}, | |||
/** create a link to a readURL */ | |||
readLink: function(label, title, args) { | |||
return this.urlLink(label, Wiki.readURL(title, args)); | |||
}, | |||
/** create a link to an actionURL */ | |||
pageLink: function(label, args) { | |||
return this.urlLink(label, Wiki.encodeURL(args)); | |||
}, | |||
/** 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; | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## lib/ui/ProgressArea.js | |||
/** uses a ProgressArea 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); | |||
// the mainDiv is a singleton | |||
var mainDiv = jsutil.DOM.get('progress-global'); | |||
if (mainDiv === null) { | |||
mainDiv = document.createElement("div"); | |||
mainDiv.id = 'progress-global'; | |||
mainDiv.className = "progress-global"; | |||
jsutil.DOM.insertBefore(jsutil.DOM.get('bodyContent'), mainDiv); | |||
} | |||
mainDiv.appendChild(outerDiv); | |||
this.headerDiv = headerDiv; | |||
this.bodyDiv = bodyDiv; | |||
this.outerDiv = outerDiv; | |||
this.timeout = null; | |||
} | |||
ProgressArea.prototype = { | |||
/** display a header text */ | |||
header: function(content) { | |||
this.unfade(); | |||
jsutil.DOM.removeChildren(this.headerDiv); | |||
jsutil.DOM.insertEnd(this.headerDiv, document.createTextNode(content)); | |||
}, | |||
/** display a body text */ | |||
body: function(content) { | |||
this.unfade(); | |||
jsutil.DOM.removeChildren(this.bodyDiv); | |||
jsutil.DOM.insertEnd(this.bodyDiv, document.createTextNode(content)); | |||
}, | |||
/** destructor, called by fade */ | |||
destroy: function() { | |||
jsutil.DOM.removeNode(this.outerDiv); | |||
}, | |||
/** fade out */ | |||
fade: function() { | |||
this.timeout = setTimeout(this.destroy.bind(this), ProgressArea.cfg.fadeTime); | |||
}, | |||
/** inihibit fade */ | |||
unfade: function() { | |||
if (this.timeout !== null) { | |||
clearTimeout(this.timeout); | |||
this.timeout = null; | |||
} | |||
}//, | |||
}; | |||
ProgressArea.cfg = { | |||
fadeTime: 750, // fade delay in millis | |||
}; | |||
//====================================================================== | |||
//## lib/ui/FeedbackLink.js | |||
/** | |||
* implements the Feedback interface defined in Actions.js | |||
* change an ActionLink's CSS-class link-running | |||
*/ | |||
function FeedbackLink(link) { | |||
this.link = link; | |||
} | |||
FeedbackLink.prototype = { | |||
job: function(s) { }, | |||
work: function(s) { jsutil.DOM.addClass(this.link, "link-running"); }, | |||
success: function(s) { jsutil.DOM.removeClass(this.link, "link-running"); }, | |||
failure: function(s) { jsutil.DOM.replaceClass(this.link, "link-running", "link-failed" ); } | |||
}; | |||
//====================================================================== | |||
//## lib/ui/FeedbackArea.js | |||
/** | |||
* implements the Feedback interface defined in Actions.js | |||
* delegates to a ProgressArea | |||
*/ | |||
function FeedbackArea() { | |||
this.area = new ProgressArea(); | |||
} | |||
FeedbackArea.prototype = { | |||
job: function(s) { this.area.header(s); }, | |||
work: function(s) { this.area.body(s); }, | |||
success: function(s) { this.area.body(s); this.area.fade(); }, | |||
failure: function(s) { this.area.body(s); }, // #body unfades | |||
}; | |||
//====================================================================== | |||
//## lib/ui/SideBar.js | |||
/** encapsulates column-one */ | |||
var SideBar = { | |||
/** | |||
* change labels of action links | |||
* root is a common parent of all items, f.e. document | |||
* labels is a Map from id to label | |||
*/ | |||
labelItems: function(labels) { | |||
for (var id in labels) { | |||
var el = document.getElementById(id); | |||
if (!el) continue; | |||
var a = el.getElementsByTagName("a")[0]; | |||
if (!a) continue; | |||
a.textContent = labels[id]; | |||
} | |||
}, | |||
//------------------------------------------------------------------------------ | |||
/** the portlets remembered in addSimplePortlet/addComplexPortlet and displayed in showPortlets */ | |||
preparedPortlets: [], | |||
/** add a portlet from an element */ | |||
addSimplePortlet: function(id, title, content) { | |||
this.preparedPortlets.push( | |||
this.createPortlet(id, title, content)); | |||
}, | |||
/** | |||
* render an array of arrays of links. | |||
* the outer array may contains strings to steal list items | |||
* null items in the outer array or inner are legal and skipped | |||
*/ | |||
addComplexPortlet: function(id, title, labels, rows) { | |||
this.addSimplePortlet(id, title, | |||
this.renderPortletContent(rows, labels)); | |||
}, | |||
/** | |||
* render a complex portlet's content element from an Array of Arrays | |||
* containing elements or ids of elements to steal | |||
*/ | |||
renderPortletContent: function(rows, labels) { | |||
// remove nulls, steal nodes | |||
var newRows = []; | |||
for (var i=0; i<rows.length; i++) { | |||
var cells = rows[i]; | |||
if (!cells) continue; | |||
var newCells = []; | |||
for (var j=0; j<cells.length; j++) { | |||
var cell = cells[j]; | |||
if (!cell) continue; | |||
if (cell.constructor === String) { | |||
var element = jsutil.DOM.get(cell); | |||
if (!element) continue; | |||
var label = labels[cell]; | |||
cell = element.firstChild.cloneNode(true); | |||
if (label) { | |||
cell.textContent = label; | |||
} | |||
} | |||
newCells.push(cell); | |||
} | |||
if (newCells.length === 0) continue; | |||
newRows.push(newCells); | |||
} | |||
//if (newRows.length === 0) return null; | |||
// compile into ul/li/node | |||
var ul = document.createElement("ul"); | |||
for (var i=0; i<newRows.length; i++) { | |||
var newCells = newRows[i]; | |||
var li = document.createElement("li"); | |||
for (var j=0; j<newCells.length; j++) { | |||
if (j !== 0) { | |||
li.appendChild(document.createTextNode(" ")); | |||
} | |||
var newCell = newCells[j]; | |||
li.appendChild(newCell); | |||
} | |||
ul.appendChild(li); | |||
} | |||
return ul; | |||
}, | |||
/** create a portlet div */ | |||
createPortlet: function(id, title, content) { | |||
var header = document.createElement("h3"); | |||
header.textContent = title; | |||
var body = document.createElement("div"); | |||
body.className = "pBody"; | |||
body.appendChild(content); | |||
var portlet = document.createElement("div"); | |||
portlet.id = id; | |||
portlet.className = "portlet"; | |||
portlet.appendChild(header); | |||
portlet.appendChild(body); | |||
return portlet; | |||
}, | |||
/** display the portlets created before and remove older ones with the same id */ | |||
showPortlets: function() { | |||
var columnOne = jsutil.DOM.get('column-one'); | |||
for (var i=0; i<this.preparedPortlets.length; i++) { | |||
var portlet = this.preparedPortlets[i]; | |||
var replaces = jsutil.DOM.get(portlet.id); | |||
if (replaces) jsutil.DOM.removeNode(replaces); | |||
columnOne.appendChild(portlet); | |||
} | |||
// HACK for speedup, hidden in sideBar.css | |||
columnOne.style.visibility = "visible"; | |||
}, | |||
//------------------------------------------------------------------------------ | |||
/** adds a div with the site name at the top of the sidebar */ | |||
insertSiteName: function() { | |||
var heading = jsutil.DOM.fetch('p-search', "h3", null, 0); | |||
jsutil.DOM.removeChildren(heading); | |||
var name = this.siteName(); | |||
if (name == null) return; | |||
var a = document.createElement("a"); | |||
a.id = "siteName"; | |||
a.textContent = name; | |||
a.href = Wiki.site; | |||
heading.appendChild(a); | |||
}, | |||
/** find the name of the current wiki */ | |||
siteName: function() { | |||
var links = document.getElementsByTagName("link"); | |||
for (var i=0; i<links.length; i++) { | |||
var link = links[i]; | |||
if (link.rel == "search") return link.title; | |||
} | |||
return null; | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/extension/ActionHistory.js | |||
/** helper for action=history */ | |||
var ActionHistory = { | |||
/** onload initializer */ | |||
init: function() { | |||
if (Page.params["action"] !== "history") return; | |||
this.addLinks(); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** additional links for every version in a page history */ | |||
addLinks: function() { | |||
var lis = jsutil.DOM.fetch('pagehistory', "li"); | |||
if (!lis) return; | |||
for (var i=0; i<lis.length; i++) { | |||
var li = lis[i]; | |||
this.editAndRestore(li); | |||
this.immediatize(li); | |||
} | |||
}, | |||
/** add edit and restore links to an item */ | |||
editAndRestore: function(li) { | |||
var diffInput = jsutil.DOM.fetch(li, "input", null, 1); | |||
if (!diffInput) return; | |||
// gather data | |||
var histSpan = jsutil.DOM.fetch(li, "span", "history-user", 0); | |||
var histA = jsutil.DOM.fetch(histSpan, "a", null, 0); | |||
// (Username or IP removed) | |||
if (!histA) return; | |||
var dateA = jsutil.DOM.nextElement(diffInput, "a"); | |||
if (!dateA) return; | |||
var oldid = diffInput.value; | |||
var user = histA.textContent; | |||
var date = dateA.textContent; | |||
var msg = ActionHistory.msg; | |||
// add edit and restore version link | |||
function done() { window.location.reload(true); } | |||
var restore = FastRestore.linkRestore(Page.title, oldid, user, date, done); | |||
var edit = Links.pageLink(msg.edit, { | |||
title: Page.title, | |||
oldid: oldid, | |||
action: "edit"//, | |||
}); | |||
jsutil.DOM.insertBeforeMany(dateA, | |||
jsutil.DOM.textAsNodeMany([ | |||
" [", edit, "] ", | |||
" [", restore, "] " | |||
]) | |||
); | |||
}, | |||
/** extend undo and rollback links */ | |||
immediatize: function(li) { | |||
// TODO unify with ActionDiff | |||
var undoSpan = jsutil.DOM.fetch(li, "span", 'mw-history-undo', 0); | |||
if (undoSpan) { | |||
var undoA = jsutil.DOM.fetch(undoSpan, "a", null, 0); | |||
FastUndo.patchLink(undoA, | |||
Afterwards.maybeHistoryFunc(Page.title)); | |||
} | |||
// TODO add a revert-button for users without rollback | |||
var rollbackSpan = jsutil.DOM.fetch(li, "span", 'mw-rollback-link', 0); | |||
if (rollbackSpan) { | |||
var rollbackA = jsutil.DOM.fetch(rollbackSpan, "a", null, 0); | |||
FastRollback.patchLink(rollbackA, | |||
Afterwards.maybeHistoryFunc(Page.title)); | |||
} | |||
} | |||
}; | |||
ActionHistory.msg = { | |||
edit: "edit"//, | |||
}; | |||
//====================================================================== | |||
//## app/extension/ActionDiff.js | |||
/** revert in the background for action=diff */ | |||
var ActionDiff = { | |||
/** onload initializer */ | |||
init: function() { | |||
if (!Page.params["diff"]) return; | |||
this.fastUndo(); | |||
this.fastRevert(); | |||
this.addLinks("diff-ntitle"); | |||
this.addLinks("diff-otitle"); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
hasRollback: false, | |||
/** extend undo links */ | |||
fastUndo: function() { | |||
var newVersion = jsutil.DOM.get('mw-diff-ntitle1'); | |||
if (!newVersion) return; | |||
var as = jsutil.DOM.fetch(newVersion, "a"); | |||
if (!as.length) return; | |||
var a = as[as.length-1]; // the last link | |||
FastUndo.patchLink(a, | |||
Afterwards.maybeHistoryFunc(Page.title)); | |||
}, | |||
/** extend rollback links */ | |||
fastRevert: function() { | |||
var newVersion = jsutil.DOM.get('mw-diff-ntitle2'); | |||
if (!newVersion) return; | |||
var span = jsutil.DOM.fetch(newVersion, "span", "mw-rollback-link", 0); | |||
if (!span) return; | |||
var a = jsutil.DOM.fetch(span, "a", null, 0); | |||
FastRollback.patchLink(a, | |||
Afterwards.maybeHistoryFunc(Page.title)); | |||
this.hasRollback = true; | |||
}, | |||
/** extends one of the two sides */ | |||
addLinks: function(tdClassName) { | |||
// get container | |||
var td = jsutil.DOM.fetch(document, "td", tdClassName, 0); | |||
if (!td) return; | |||
// get lines | |||
var divs = td.getElementsByTagName("div"); | |||
// extract revision info | |||
var revisionA = jsutil.DOM.fetch(divs[0], "a", null, 0); | |||
if (!revisionA) return; | |||
var params = Wiki.decodeURL(revisionA.href); | |||
var oldid = params.oldid; | |||
var dateP = ActionDiff.cfg.versionExtractRE.exec(revisionA.textContent); | |||
var date = dateP ? dateP[1] : null; | |||
// extract user info | |||
var userA = jsutil.DOM.fetch(divs[1], "a", null, 0); | |||
// (Username or IP removed) | |||
if (!userA) return; | |||
var user = userA.textContent; | |||
var done = Afterwards.maybeHistoryFunc(Page.title); | |||
// if an oldid exists: add a restore revision link | |||
var restorable = params.oldid; | |||
if (restorable) { | |||
var restore = FastRestore.linkRestore(Page.title, oldid, user, date, done); | |||
var targetA = jsutil.DOM.fetch(divs[0], "a", null, 1); | |||
if (targetA) { | |||
jsutil.DOM.insertBeforeMany(targetA, | |||
jsutil.DOM.textAsNodeMany([ restore, ") ("]) | |||
); | |||
} | |||
} | |||
// note: some users have rollback, others don't | |||
if (!this.hasRollback && divs.length > 3) { | |||
// latest revision: add a revert link | |||
var div3 = divs[3]; | |||
var latest = div3.id === "mw-diff-ntitle4" && ( | |||
div3.innerHTML === " " || // firefox | |||
div3.innerHTML === "\u00a0" ); // safari | |||
if (latest) { | |||
var revert = UserRevert.linkRevert(Page.title, user, done); | |||
jsutil.DOM.insertEndMany(divs[1], | |||
jsutil.DOM.textAsNodeMany([ " [", revert, "]" ]) | |||
); | |||
} | |||
} | |||
}//, | |||
}; | |||
ActionDiff.cfg = { | |||
// TODO: hardcoded lang_de OR lang_en | |||
versionExtractRE: /(?:Version vom|Revision as of) (.*)/ //, | |||
}; | |||
//====================================================================== | |||
//## app/extension/SpecialAny.js | |||
/** dispatcher for SpecialPages */ | |||
var SpecialAny = { | |||
/** dispatches calls to Special* objects */ | |||
init: function() { | |||
var name = Page.whichSpecial(); | |||
if (!name) return; | |||
var feature = window["Special" + name]; | |||
if (feature && feature.init) { | |||
feature.init(); | |||
} | |||
this.mangleForm(name); | |||
}, | |||
mangleForm: function(specialPage) { | |||
// if there is only one form, it's the searchform | |||
if (document.forms.length < 2) return; | |||
var form = document.forms[0]; | |||
if (!form) return; | |||
// only pages listed below are mangled | |||
var elementNames = SpecialAny.cfg.autoSubmitElements[specialPage]; | |||
if (!elementNames) return; | |||
this.autoSubmit(form, elementNames); | |||
// HACK: in Watchlist the submit-button is necessary | |||
if (specialPage === "Watchlist" | |||
&& Page.params["action"]) | |||
return; | |||
// HACK: in the Undelete sometimes the buttons are necessary. or at least a restore-parameter | |||
if (specialPage === "Undelete") { | |||
if (Page.params["timestamp"] || Page.params["action"] === "submit") | |||
return; | |||
form.action += "&restore=yes"; | |||
} | |||
this.removeButtons(form); | |||
}, | |||
/** adds an onchange handler to elements in a form submitting the form */ | |||
autoSubmit: function(form, elementNames) { | |||
function change() { form.submit(); } | |||
var elements = form.elements; | |||
for (var i=0; i<elementNames.length; i++) { | |||
var elementName = elementNames[i]; | |||
var element = elements[elementName]; | |||
if (!element) continue; | |||
// radioButtons return an array, not a single element, | |||
// but only when accessed by name (!) | |||
if (element.type) { | |||
element.onchange = change; | |||
} | |||
} | |||
}, | |||
/** removes submit button(s) */ | |||
removeButtons: function(form) { | |||
var elements = form.elements; | |||
for (var i=0; i<elements.length; i++) { | |||
var element = elements[i]; | |||
if (element.type !== "submit" | |||
&& element.type !== "reset") continue; | |||
element.style.display = "none"; | |||
} | |||
}//, | |||
}; | |||
SpecialAny.cfg = { | |||
/** | |||
* maps Specialpage names to the autosubmitting form elements. | |||
* only SpecialPages listed here have their submit-buttons hidden. | |||
*/ | |||
autoSubmitElements: { | |||
AbuseFilter: [ // "deletedfilters", | |||
"mw-abusefilter-deletedfilters-show", | |||
"mw-abusefilter-deletedfilters-hide", | |||
"mw-abusefilter-deletedfilters-only", | |||
"hidedisabled", "limit" ], | |||
StablePages: [ "namespace" ], | |||
GlobalUsers: [ "group", "username" ], | |||
Allpages: [ "namespace", "nsfrom" ], | |||
Contributions: [ "namespace", "month" ], // contribs is a bit useless | |||
Ipblocklist: [ "title", // default action | |||
"wpUnblockAddress", "wpUnblockReason" // action=unblock | |||
], | |||
LinkSearch: [ "title" ], | |||
Listusers: [ "group", "username" ], | |||
Log: [ "type", "user", "page", | |||
"year", "month" ], | |||
Newimages: [ "wpIlMatch" ], | |||
Newpages: [ "namespace", "username" ], | |||
Prefixindex: [ "namespace", "nsfrom" ], | |||
Recentchanges: [ "namespace", "invert" ], | |||
Watchlist: [ "namespace" ], | |||
Whatlinkshere: [ "namespace" ], | |||
Booksources: [ "isbn" ], | |||
CategoryTree: [ "mode", "target" ], | |||
Cite: [ "page" ], | |||
Filepath: [ "file" ], | |||
Listfiles: [ "limit" ], | |||
MIMEsearch: [ "mime" ], | |||
Search: [ "lsearchbox" ], | |||
Undelete: [ | |||
], | |||
DeletedContributions: | |||
[ "namespace", "target" ], | |||
ReviewedPages: [ "level" ], | |||
Userrights: [ "user" ], | |||
Recentchangeslinked: | |||
[ "recentchangeslinked-target", "showlinkedto" ]//, | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/extension/SpecialBlockIP.js | |||
/** extends Special:BlockIP */ | |||
var SpecialBlockIP = { | |||
/** onload initializer */ | |||
init: function() { | |||
this.presetBlockIP(); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** fill in default values into the blockip form */ | |||
presetBlockIP: function() { | |||
// if there is only one form, it's the searchform | |||
if (document.forms.length < 2) return; | |||
var form = document.forms[0]; // was blockip | |||
if (!form) return; // action=success | |||
var def = SpecialBlockIP.cfg.defaults; | |||
form.elements["mw-input-wpExpiry"].value = "other"; | |||
form.elements["mw-input-wpExpiry-other"].value = def.expiry; | |||
form.elements["mw-input-wpReason-other"].value = def.reason; | |||
form.elements["mw-input-wpReason-other"].select(); | |||
form.elements["mw-input-wpReason-other"].focus(); | |||
}//, | |||
}; | |||
SpecialBlockIP.cfg = { | |||
defaults: { | |||
expiry: "2 hours", | |||
reason: "vandalismus"//, | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/extension/SpecialContributions.js | |||
/** revert in the background for Special:Contributions */ | |||
var SpecialContributions = { | |||
/** onload initializer */ | |||
init: function() { | |||
this.fastRevert(); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** extend rollback links */ | |||
fastRevert: function() { | |||
var warning = jsutil.DOM.fetch('bodyContent', "div", "mw-warning-with-logexcerpt", 0); | |||
var ul = jsutil.DOM.fetch('bodyContent', "ul", null, warning ? 1 : 0); | |||
if (ul === null) return; | |||
function changeLink(link) { | |||
link.textContent = SpecialContributions.msg.reverted; | |||
} | |||
var all = []; | |||
var spans = jsutil.DOM.fetch(ul, "span", "mw-rollback-link"); | |||
for (var i=0; i<spans.length; i++) { | |||
var span = spans[i]; | |||
var a = jsutil.DOM.fetch(span, "a", null, 0); | |||
var p = FastRollback.patchLink(a, changeLink); | |||
all.push(p); | |||
} | |||
var revert = Links.functionLink(SpecialContributions.msg.revertAll, function() { | |||
for (var i=0; i<all.length; i++) { all[i].onclick(); } | |||
}); | |||
jsutil.DOM.addClass(revert, "link-immediate"); | |||
var li = document.createElement("li"); | |||
li.appendChild(revert); | |||
jsutil.DOM.insertBegin(ul, li); | |||
}//, | |||
}; | |||
SpecialContributions.msg = { | |||
reverted: "zurückgesetzt", | |||
revertAll: "alle zurücksetzen", | |||
}; | |||
//====================================================================== | |||
//## app/extension/SpecialLog.js | |||
/** extends Special:Log */ | |||
var SpecialLog = { | |||
/** onload initializer */ | |||
init: function() { | |||
if (Page.params.type === "newusers") { | |||
this.extendNewusers(); | |||
} | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** faster */ | |||
extendNewusers: function() { | |||
var ul = jsutil.DOM.fetch('mw-log-deleterevision-submit', "ul", null, 0); | |||
if (!ul) return; | |||
function find(as) { | |||
for (var i=0; i<as.length; i++) { | |||
try { | |||
var a = as[i]; | |||
var args = Wiki.decodeURL(a.href); | |||
if (args.ip) return args.ip; | |||
} | |||
catch (e) { | |||
// "cannot decode" from wiki.decodeURL when someone | |||
// used an external link in the summary | |||
} | |||
} | |||
return null | |||
} | |||
// TODO use FastBlock | |||
function handler() { | |||
var clicked = this; | |||
var as = jsutil.DOM.fetch(clicked.parseFrom, "a"); | |||
var name = find(as); | |||
if (!name) return false; | |||
var feedback = new FeedbackLink(clicked); | |||
var expiry = "indefinite"; | |||
var reason = SpecialLog.msg.reason; | |||
var autoBlock = true; | |||
var hardBlock = true; | |||
var createAccount = true; | |||
var disableEmail = true; | |||
var disableUTEdit = true; | |||
var watchUser = false; | |||
var allowChange = false; | |||
Actions.blockUser(feedback, name, expiry, reason, autoBlock, hardBlock, createAccount, disableEmail, disableUTEdit, watchUser, allowChange, function() { | |||
clicked.textContent = SpecialLog.msg.blocked; | |||
}); | |||
return false; | |||
} | |||
var lis = jsutil.DOM.fetch(ul, "li"); | |||
for (var i=0; i<lis.length; i++) { | |||
var li = lis[i]; | |||
var a = Links.functionLink(SpecialLog.msg.block, handler); | |||
jsutil.DOM.addClass(a, "link-immediate"); | |||
a.parseFrom = li; // used by the onclick-handler | |||
jsutil.DOM.insertAfterMany(li.firstChild, | |||
jsutil.DOM.textAsNodeMany([ " [", a, "] " ]) | |||
); | |||
} | |||
}//, | |||
}; | |||
SpecialLog.msg = { | |||
block: "killen", | |||
blocked: "tot", | |||
reason: ""//, | |||
}; | |||
//====================================================================== | |||
//## app/extension/SpecialNewpages.js | |||
/** extends Special: */ | |||
var SpecialNewpages = { | |||
/** onload initializer */ | |||
init: function() { | |||
this.displayInline(); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** extend Special: with the content of the articles */ | |||
displayInline: function() { | |||
var openCount = 0; | |||
/** parse one list item, then add folding and the inline view to it */ | |||
function extendItem(li) { | |||
// fetch data | |||
var a = li.getElementsByTagName("a")[0]; | |||
var title = a.title; | |||
var byteStr = li.innerHTML | |||
.replace(SpecialNewpages.cfg.bytesExtractRE, "$1") | |||
.replace(SpecialNewpages.cfg.bytesStripRE, ""); | |||
var bytes = parseInt(byteStr); | |||
// 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"; | |||
// a FoldButton for the header | |||
var foldButton = new FoldButton(true, function(open) { | |||
body.style.display = open ? "" : "none"; | |||
if (open && foldButton.needsLoad) { | |||
loadContent(li); | |||
foldButton.needsLoad = false; | |||
} | |||
}); | |||
foldButton.needsLoad = false; | |||
jsutil.DOM.insertBegin(header, foldButton.button); | |||
// add action links | |||
jsutil.DOM.insertBegin(header, Google.link(title)); | |||
jsutil.DOM.insertBegin(header, UserBookmarks.linkMark(title)); | |||
var templateTools = TemplatePage.bankAllPage(title); | |||
if (templateTools) jsutil.DOM.insertBeginMany(header, templateTools); | |||
jsutil.DOM.insertBegin(header, FastDelete.linkDeletePopup(title)); | |||
// change listitem | |||
li.pageTitle = title; | |||
li.contentBytes = bytes; | |||
li.headerDiv = header; | |||
li.bodyDiv = body; | |||
li.className = "folding-container"; | |||
li.innerHTML = ""; | |||
li.appendChild(header); | |||
li.appendChild(body); | |||
if (li.contentBytes <= SpecialNewpages.cfg.sizeLimit | |||
&& openCount < SpecialNewpages.cfg.maxArticles) { | |||
loadContent(li); | |||
openCount++; | |||
} | |||
else { | |||
foldButton.change(false); | |||
foldButton.needsLoad = true; | |||
} | |||
} | |||
/** load the article content and display it inline */ | |||
function loadContent(li) { | |||
li.bodyDiv.textContent = SpecialNewpages.msg.loading; | |||
jsutil.Ajax.call({ | |||
url: Wiki.readURL(li.pageTitle, { redirect: "no" }), | |||
successFunc: function(source) { | |||
// uses the monobook start content marker | |||
// firefox, safari: [^] works, div is lower case | |||
// opera: [^] does not work, div is upper case | |||
var extractRE = /<!-- start content -->([\s\S]*)<(div|DIV) class="printfooter">/; | |||
var content = extractRE.exec(source.responseText); | |||
if (!content) throw "could not extract article content"; | |||
li.bodyDiv.innerHTML = content[1] + '<div class="visualClear" />'; | |||
// <div class="noarticletext"> | |||
} | |||
}); | |||
} | |||
// find article list | |||
var ul = jsutil.DOM.fetch('bodyContent', "ul", null, 0); | |||
if (!ul) return; | |||
ul.className = "SpecialNewpages"; | |||
// find article list items | |||
var lis = jsutil.DOM.fetch(ul, "li"); | |||
for (var i=0; i<lis.length; i++) { | |||
extendItem(lis[i], i); | |||
} | |||
}//, | |||
}; | |||
SpecialNewpages.cfg = { | |||
maxArticles: 100, | |||
sizeLimit: 2048, | |||
// TODO: hardcoded lang_de OR lang_en | |||
bytesExtractRE: /.*\[([0-9.,]+) [Bb]ytes\].*/, | |||
bytesStripRE: /[.,]/g//, | |||
}; | |||
SpecialNewpages.msg = { | |||
loading: "lade seite.."//, | |||
}; | |||
//====================================================================== | |||
//## app/extension/SpecialSpecialPages.js | |||
/** extends Special:SpecialPages */ | |||
var SpecialSpecialPages = { | |||
/** onload initializer */ | |||
init: function() { | |||
this.extendLinks(); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** make a sorted tables from the links */ | |||
extendLinks: function() { | |||
var tables = jsutil.DOM.fetch('bodyContent', "table", "mw-specialpages-table"); | |||
for (var i=0; i<tables.length; i++) { | |||
var group = tables[i]; | |||
this.extendGroup(group); | |||
} | |||
}, | |||
/** make a sorted table from the links of one group */ | |||
extendGroup: function(group) { | |||
var as = jsutil.DOM.fetch(group, "a"); | |||
for (var i=0; i<as.length; i++) { | |||
var a = as[i]; | |||
var name = Namespace.scan(-1, a.title); | |||
var descr = a.textContent; | |||
a.title = descr; | |||
a.textContent = name; | |||
var hint = document.createElement("span"); | |||
hint.className = "specialSpecialPagesHint"; | |||
jsutil.DOM.insertAfter(a, hint); | |||
var specialNames = Special.pageNames(name); | |||
if (!specialNames) { | |||
hint.textContent = "(missing)"; | |||
continue; | |||
} | |||
var canonical = specialNames.canonicalName; | |||
if (canonical === name) { | |||
hint.textContent = "(canonical)"; | |||
continue; | |||
} | |||
hint.textContent = canonical; | |||
} | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/extension/SpecialUndelete.js | |||
/** extends Special:Undelete */ | |||
var SpecialUndelete = { | |||
/** onload initializer */ | |||
init: function() { | |||
this.toggleAll(); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** add an invert button for all checkboxes */ | |||
toggleAll: function() { | |||
var form = document.forms[0]; | |||
if (!form) return; | |||
var button = Links.functionLink(SpecialUndelete.msg.invert, function() { | |||
var els = form.elements; | |||
for (var i=0; i<els.length; i++) { | |||
var el = els[i]; | |||
if (el.type !== "checkbox") continue; | |||
el.checked = !el.checked; | |||
} | |||
}); | |||
var target = jsutil.DOM.fetch(form, "ul", null, 2); | |||
// no list if there is only one deleted version | |||
if (target === null) return; | |||
target.parentNode.insertBefore(button, target); | |||
}//, | |||
}; | |||
SpecialUndelete.msg = { | |||
invert: "Invertieren"//, | |||
}; | |||
//====================================================================== | |||
//## app/extension/SpecialRecentchanges.js | |||
/** extensions for Special:Recentchanges */ | |||
var SpecialRecentchanges = { | |||
/** onload initializer */ | |||
init: function() { | |||
FilteredEditList.filterLinks("FilteredEditList_SpecialRecentchanges"); | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/extension/SpecialRecentchangeslinked.js | |||
/** extensions for Special:Recentchangeslinked */ | |||
var SpecialRecentchangeslinked = { | |||
/** onload initializer */ | |||
init: function() { | |||
FilteredEditList.filterLinks("FilteredEditList_SpecialRecentchangeslinked"); | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/extension/SpecialWatchlist.js | |||
/** extensions for Special:Watchlist */ | |||
var SpecialWatchlist = { | |||
/** onload initializer */ | |||
init: function() { | |||
var action = Page.params["action"]; | |||
if (!action) { | |||
FilteredEditList.filterLinks("FilteredEditList_SpecialWatchlist"); | |||
} | |||
if (action === "edit") { | |||
var spaces = this.parseNamespaces(); | |||
this.toggleLinks(spaces); | |||
} | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## toggle-links | |||
/** extends header structure and add toggle buttons for all checkboxes */ | |||
toggleLinks: function(spaces) { | |||
var form = jsutil.DOM.fetch(document, "form", null, 0); | |||
// add invert buttons for single namespaces | |||
for (var i=0; i<spaces.length; i++) { | |||
var space = spaces[i]; | |||
var button = this.toggleButton(space.ul); | |||
jsutil.DOM.insertAfter(space.h2, button ); | |||
} | |||
// add gobal invert button with header | |||
var globalHdr = document.createElement("h2"); | |||
globalHdr.textContent = SpecialWatchlist.msg.global; | |||
var button = this.toggleButton(form); | |||
var target = form.elements[form.elements.length-1]; | |||
jsutil.DOM.insertBeforeMany(target, [ | |||
globalHdr, button, | |||
// HACK to get some space | |||
document.createElement("br"), | |||
document.createElement("br")//, | |||
]); | |||
}, | |||
/** creates a toggle button for all input children of an element */ | |||
toggleButton: function(container) { | |||
return Links.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; | |||
} | |||
}); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## list parser | |||
parseNamespaces: function() { | |||
var out = []; | |||
var form = jsutil.DOM.fetch(document, "form", null, 0); | |||
var uls = jsutil.DOM.fetch(form, "ul"); | |||
for (var i=0; i<uls.length; i++) { | |||
var ul = uls[i]; | |||
var h2 = jsutil.DOM.previousElement(ul); | |||
var ns = h2 ? h2.textContent : ""; | |||
out.push({ ul: ul, h2: h2, ns: ns }); | |||
} | |||
return out; | |||
}//, | |||
}; | |||
SpecialWatchlist.msg = { | |||
invert: "Invertieren", | |||
article: "Artikel", | |||
global: "Alle"//, | |||
}; | |||
//====================================================================== | |||
//## app/extension/SpecialPrefixindex.js | |||
/** extends Special:PrefixIndex */ | |||
var SpecialPrefixindex = { | |||
/** onload initializer */ | |||
init: function() { | |||
this.sortItems(); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** sort items into a straight list */ | |||
sortItems: function() { | |||
var table = jsutil.DOM.fetch('bodyContent', "table", null, 2); | |||
if (!table) return; // no search results | |||
var tds = jsutil.DOM.fetch(table, "td"); | |||
var ol = document.createElement("ol"); | |||
for (var i=0; i<tds.length; i++) { | |||
var td = tds[i]; | |||
var li = document.createElement("li"); | |||
var c = td.firstChild.cloneNode(true) | |||
li.appendChild(c); | |||
ol.appendChild(li); | |||
} | |||
table.parentNode.replaceChild(ol, table); | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/ForSite.js | |||
/** links for the whole site */ | |||
var ForSite = { | |||
init: function() { | |||
Config.patch(ForSite.cfg); | |||
}, | |||
/** a link to new pages */ | |||
linkNewPages: function() { | |||
return Links.pageLink(ForSite.msg.newpages, { | |||
title: Titles.specialPage("NewPages"), | |||
limit: 20//, | |||
}); | |||
}, | |||
/** a link to new pages */ | |||
linkNewusers: function() { | |||
return Links.pageLink(ForSite.msg.newusers, { | |||
title: Titles.specialPage("Log"), | |||
type: "newusers", | |||
limit: 50//, | |||
}); | |||
}, | |||
/** a bank of links to interesting pages */ | |||
bankProjectPages: function() { | |||
var pages = ForSite.cfg.projectPages; | |||
if (!pages) return null; | |||
var out = []; | |||
for (var i=0; i<pages.length; i++) { | |||
var page = pages[i]; | |||
var link = Links.readLink(page[0], page[1]); | |||
out.push(link); | |||
} | |||
return out; | |||
}, | |||
/** return a link for fast logfiles access */ | |||
linkAllLogsPopup: function() { | |||
function selected(userdata) { | |||
window.location.href = Wiki.readURL(Titles.specialPage("Log", userdata.toLowerCase())); | |||
} | |||
return this.linkAllPopup( | |||
ForSite.msg.logLabel, | |||
Titles.specialPage("Log"), | |||
ForSite.cfg.logs, | |||
selected); | |||
}, | |||
/** return a link for fast logfiles access */ | |||
linkAllSpecialsPopup: function() { | |||
function selected(userdata) { | |||
window.location.href = Wiki.readURL(Titles.specialPage(userdata)); | |||
} | |||
return this.linkAllPopup( | |||
ForSite.msg.specialLabel, | |||
Titles.specialPage("SpecialPages"), | |||
ForSite.cfg.specials, | |||
selected); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** returns a linkPopup */ | |||
linkAllPopup: function(linkLabel, mainPage, pages, selectFunc) { | |||
var mainLink = Links.readLink(linkLabel, mainPage); | |||
var popup = new PopupMenu(selectFunc); | |||
for (var i=0; i<pages.length; i++) { | |||
var page = pages[i]; | |||
popup.item(page, page); // the page is the userdata | |||
} | |||
new PopupSource(mainLink, popup); | |||
return mainLink; | |||
}//, | |||
} | |||
ForSite.cfg = { | |||
/** which logs are displayed in the popup */ | |||
logs: [ | |||
"Move", "Block", "Protect", "Delete", "Upload" | |||
], | |||
/** which specialpages are displayed in the opoup */ | |||
specials:[ | |||
"AllMessages", "AllPages", "CategoryTree", "IPBlockList", "Linksearch", "ListUsers", "NewImages", "PrefixIndex", | |||
], | |||
/** useful pages in this wiki */ | |||
projectPages: null, | |||
// domain-specific data | |||
"de.wikipedia.org": { | |||
projectPages: [ | |||
[ "VM", "Wikipedia:Vandalismusmeldung" ], | |||
[ "EW", "Wikipedia:Entsperrwünsche" ], | |||
[ "SP", "Wikipedia:Sperrprüfung" ], | |||
[ "LP", "Wikipedia:Löschprüfung" ], | |||
[ "LK", "Wikipedia:Löschkandidaten" ], | |||
[ "SL", "Kategorie:Wikipedia:Schnelllöschen" ], | |||
]//, | |||
}, | |||
"de.wikiversity.org": { | |||
projectPages: [ | |||
[ "Löschen", "Kategorie:Wikiversity:Löschen" ]//, | |||
]//, | |||
}, | |||
"de.wikisource.org": { | |||
projectPages: [ | |||
[ "LK", "Wikisource:Löschkandidaten" ], | |||
[ "SL", "Kategorie:Wikisource:Schnelllöschen" ]//, | |||
]//, | |||
}//, | |||
}; | |||
ForSite.msg = { | |||
logLabel: "Logs", | |||
specialLabel: "Spezial", | |||
newpages: "Frischtext", | |||
newusers: "Noobs", | |||
}; | |||
//====================================================================== | |||
//## app/feature/ForPage.js | |||
/** links for arbitrary pages */ | |||
var ForPage = { | |||
/** returns a link to the logs for a given page */ | |||
linkLogAbout: function(title) { | |||
return Links.pageLink(ForPage.msg.pageLog, { | |||
title: Titles.specialPage("Log"), | |||
page: title | |||
}); | |||
}//, | |||
}; | |||
ForPage.msg = { | |||
pageLog: "Seitenlog"//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/ForUser.js | |||
/** links for users */ | |||
var ForUser = { | |||
/** returns a link to the homepage of a user */ | |||
linkHome: function(user) { | |||
return Links.readLink(ForUser.msg.home, Titles.userPage(user)); | |||
}, | |||
/** returns a link to the talkpage of a user */ | |||
linkTalk: function(user) { | |||
return Links.readLink(ForUser.msg.talk, Titles.userTalkPage(user)); | |||
}, | |||
/** returns a link to new messages or null when none exist */ | |||
linkNews: function(user) { | |||
return Links.readLink(ForUser.msg.news, Titles.userTalkPage(user), { diff: "cur" }); | |||
}, | |||
/** returns a link to a users contributions */ | |||
linkContribs: function(user) { | |||
return Links.readLink(ForUser.msg.contribs, Titles.specialPage("Contributions", user)); | |||
}, | |||
/** returns a link to the blockpage for a user */ | |||
linkBlock: function(user) { | |||
return Links.readLink(ForUser.msg.block, Titles.specialPage("BlockIP", user)); | |||
}, | |||
/** returns a link to a users emailpage */ | |||
linkEmail: function(user) { | |||
return Links.readLink(ForUser.msg.email, Titles.specialPage("EmailUser", user)); | |||
}, | |||
/** returns a link to a users log entries */ | |||
linkLogsAbout: function(user) { | |||
return Links.pageLink(ForUser.msg.logsAbout, { | |||
title: Titles.specialPage("Log"), | |||
page: Titles.userPage(user)//, | |||
}); | |||
}, | |||
/** returns a link to a users log entries */ | |||
linkLogsActor: function(user) { | |||
return Links.pageLink(ForUser.msg.logsActor, { | |||
title: Titles.specialPage("Log"), | |||
user: user//, | |||
}); | |||
}, | |||
/** returns a link to show subpages of a user */ | |||
linkSubpages: function(user) { | |||
return Links.pageLink(ForUser.msg.subpages, { | |||
title: Titles.specialPage("PrefixIndex"), | |||
namespace: 2, // User | |||
from: user + "/"//, | |||
}); | |||
}, | |||
/** various checks for anonymous users */ | |||
bankIP: function(ip) { | |||
return ForUser.cfg.ipChecks.map(function(def) { | |||
return Links.urlLink(def.label, def.template.template({ip:ip})); | |||
}); | |||
}//, | |||
}; | |||
ForUser.msg = { | |||
home: "Benutzer", | |||
talk: "Diskussion", | |||
email: "Anmailen", | |||
contribs: "Contribs", | |||
block: "Sperren", | |||
news: "☏", | |||
logsAbout: "Log", | |||
logsActor: "Act", | |||
subpages: "Sub", | |||
}; | |||
ForUser.cfg = { | |||
ipChecks: [ | |||
{ label: "Honey", template: "http://www.projecthoneypot.org/ip_${ip}" }, | |||
{ label: "Whois", template: "http://whois.domaintools.com/${ip}" }//, | |||
]//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/FilteredEditList.js | |||
/** filters edit lists by name/ip */ | |||
var FilteredEditList = { | |||
/** onload initializer */ | |||
filterLinks: function(cookieName) { | |||
var bodyContent = jsutil.DOM.get('bodyContent'); | |||
// tag list items with a CSS class "is-ip" or "is-named" | |||
var uls = jsutil.DOM.fetch(bodyContent, "ul", "special"); | |||
for (var i=0; i<uls.length; i++) { | |||
var ul = uls[i]; | |||
var lis = jsutil.DOM.fetch(ul, "li"); | |||
for (var j=0; j<lis.length; j++) { | |||
var li = lis[j]; | |||
var a = jsutil.DOM.fetch(li, "a", "mw-userlink", 0); | |||
if (!a) continue; | |||
if (IP.isV4(a.textContent)) jsutil.DOM.addClass(li, "is-ip"); | |||
else jsutil.DOM.addClass(li, "is-named"); | |||
} | |||
} | |||
/** changes the filter state */ | |||
function update(link, state) { | |||
board.select(link); | |||
if (state === "named") jsutil.DOM.addClass( bodyContent, "hide-ip"); | |||
else jsutil.DOM.removeClass( bodyContent, "hide-ip"); | |||
if (state === "ip") jsutil.DOM.addClass( bodyContent, "hide-named"); | |||
else jsutil.DOM.removeClass( bodyContent, "hide-named"); | |||
jsutil.Cookie.set(cookieName, state); | |||
} | |||
/** adds a filter-change link to the switchBoard */ | |||
function action(state) { | |||
var link = Links.functionLink( | |||
FilteredEditList.msg.state[state], | |||
function() { update(link, state); } | |||
); | |||
board.add(link); | |||
if (state === initial) update(link, state); | |||
} | |||
// create state switchboard | |||
var initial = jsutil.Cookie.get(cookieName); | |||
if (!initial) initial = "all"; | |||
var states = [ "all", "named", "ip" ]; | |||
var board = new SwitchBoard(); | |||
for (var i=0; i<states.length; i++) action(states[i]); | |||
var target = jsutil.DOM.fetch(document, "form", null, 0); | |||
if (!target) return; | |||
// HACK to get some space | |||
var br = document.createElement("br"); | |||
br.style.lineHeight = "30%"; | |||
var after = document.createElement("div"); | |||
after.className = "visualClear"; | |||
jsutil.DOM.insertAfterMany(target, | |||
jsutil.DOM.textAsNodeMany([ | |||
br, | |||
FilteredEditList.msg.intro, | |||
board.component, | |||
after | |||
]) | |||
); | |||
}//, | |||
}; | |||
FilteredEditList.msg = { | |||
intro: "Filter: ", | |||
state: { | |||
all: "Alle", | |||
ip: "Ips", | |||
named: "Angemeldete"//, | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/FastWatch.js | |||
/** page watch and unwatch without reloading the page */ | |||
var FastWatch = { | |||
init: function() { | |||
/** initialize link */ | |||
function initView() { | |||
var watch = jsutil.DOM.get('ca-watch'); | |||
var unwatch = jsutil.DOM.get('ca-unwatch'); | |||
if (watch) exchangeItem(watch, true); | |||
if (unwatch) exchangeItem(unwatch, false); | |||
} | |||
/** talk to the server, then updates the UI */ | |||
function changeState(link, watched) { | |||
function update() { | |||
var watch = jsutil.DOM.get('ca-watch'); | |||
var unwatch = jsutil.DOM.get('ca-unwatch'); | |||
if ( watched && watch ) exchangeItem(watch, false); | |||
if (!watched && unwatch) exchangeItem(unwatch, true); | |||
} | |||
var feedback = new FeedbackLink(link); | |||
Actions.watchedPage(feedback, Page.title, watched, update); | |||
} | |||
/** 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 ? FastWatch.msg.watch : FastWatch.msg.unwatch; | |||
var a = Links.functionLink(label, function() { | |||
changeState(a, watchable); | |||
}); | |||
jsutil.DOM.addClass(a, "link-immediate"); | |||
li.appendChild(a); | |||
target.parentNode.replaceChild(li, target); | |||
} | |||
initView(); | |||
}//, | |||
}; | |||
FastWatch.msg = { | |||
watch: "Beobachten", | |||
unwatch: "Entobachten"//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/FastDelete.js | |||
/** one-click delete */ | |||
var FastDelete = { | |||
/** returns a link which prompts or popups reasons and then deletes */ | |||
linkDeletePopup: function(title) { | |||
var self = this; | |||
var msg = FastDelete.msg; | |||
var cfg = FastDelete.cfg; | |||
var link = Links.promptPopupLink(msg.label, msg.prompt, cfg.reasons, function(reason) { | |||
self.fastDelete(title, reason); | |||
}); | |||
link.title = msg.tooltip.deletePage; | |||
return link; | |||
}, | |||
/** delete an article with a reason */ | |||
fastDelete: function(title, reason) { | |||
var feedback = new FeedbackArea(); | |||
Actions.deletePage(feedback, title, reason, Afterwards.maybeReloadFunc(title)); | |||
}//, | |||
}; | |||
FastDelete.cfg = { | |||
reasons: [ | |||
[ "schrott", | |||
"kein artikel", | |||
"aha", | |||
"unfug", | |||
"wirr", | |||
"irrelevant", | |||
"tastaturtest"//, | |||
], | |||
[ "zu mager", | |||
"falsches lemma", | |||
"falsche sprache", | |||
"wörterbucheintrag", | |||
"linkcontainer", | |||
"werbung"//, | |||
], | |||
[ "unnötig", | |||
//"verwaist", | |||
"geleert", | |||
"versehen", | |||
"veraltet", | |||
"erledigt"//, | |||
]//, | |||
]//, | |||
}; | |||
FastDelete.msg = { | |||
prompt: "warum löschen?", | |||
label: "Wech", | |||
tooltip: { | |||
deletePage: "seite löschen"//, | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/FastBlock.js | |||
/** fast user block */ | |||
var FastBlock = { | |||
/** | |||
* returns a link to kill a user with full templating. | |||
* a simple klickpromps for a reason, | |||
* a context menu provides preset reasons. | |||
*/ | |||
linkKillPopup: function(user) { | |||
var self = this; | |||
var msg = FastBlock.msg; | |||
var cfg = FastBlock.cfg; | |||
return Links.promptPopupLink(msg.label, msg.prompt, cfg.reasons, | |||
function(reason) { self.killUser(user, reason); } | |||
); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
/** does everything necessary to block a user indefinitely */ | |||
killUser: function(user, reason) { | |||
// TODO split this into two separate actions? | |||
this.blockIndefinite(user, reason); | |||
this.templateBlockedUser(user, reason); | |||
}, | |||
/** blocks a user for an indefinite duration */ | |||
blockIndefinite: function(user, reason) { | |||
var feedback = new FeedbackArea(); | |||
var expiry = "indefinite"; | |||
var autoBlock = false; | |||
var hardBlock = false; | |||
var createAccount = true; | |||
var disableEmail = true; | |||
var disableUTEdit = true; | |||
var watchUser = false; | |||
Actions.blockUser(feedback, user, expiry, reason, autoBlock, hardBlock, createAccount, disableEmail, disableUTEdit, watchUser, true); | |||
}, | |||
/** inserts blocked templates in a users's home- and talkpage */ | |||
templateBlockedUser: function(user, reason) { | |||
this.modifyUserHomePage(user, reason); | |||
this.modifyUserTalkPage(user, reason); | |||
}, | |||
/** inserts redirect to the talkpage and protects */ | |||
modifyUserHomePage: function(user, reason) { | |||
var self = this; | |||
var title = Titles.userPage(user); | |||
var template = Markup.template(FastBlock.cfg.template); | |||
var feedback = new FeedbackArea(); | |||
function phase1() { | |||
var text = "#redirect [[" + Titles.userTalkPage(user) + "]]"; | |||
var minorEdit = false; | |||
var allowCreate = true; | |||
Actions.replaceText(feedback, title, jsutil.Function.constant(text), template, minorEdit, allowCreate, phase2); | |||
} | |||
function phase2() { | |||
var level = "sysop"; | |||
var cascade = false; | |||
var expiry = ""; | |||
var watch = false; | |||
Actions.protectPage(feedback, title, level, expiry, level, expiry, level, expiry, template, cascade, watch); | |||
} | |||
phase1(); | |||
}, | |||
/** inserts the template in a page and protects */ | |||
modifyUserTalkPage: function(user, reason) { | |||
var self = this; | |||
var title = Titles.userTalkPage(user); | |||
var template = Markup.template(FastBlock.cfg.template); | |||
var feedback = new FeedbackArea(); | |||
function phase1() { | |||
var text = template + " " + reason + Markup.SIGAPP; | |||
var sepa = Markup.LF + Markup.LINE + Markup.LF; | |||
var allowCreate = true; | |||
Actions.prependText(feedback, title, text, template, sepa, allowCreate, phase2); | |||
} | |||
function phase2() { | |||
var level = "sysop"; | |||
var cascade = false; | |||
var expiry = ""; | |||
var watch = false; | |||
Actions.protectPage(feedback, title, level, expiry, level, expiry, level, expiry, template, cascade, watch); | |||
} | |||
phase1(); | |||
}//, | |||
}; | |||
FastBlock.cfg = { | |||
reasons: [ | |||
[ "vandalenaccount", | |||
"fakeaccount", | |||
"provokationsaccount", | |||
"ja nee, is klar", | |||
"jaja"//, | |||
]//, | |||
], | |||
template: "gesperrter Benutzer"//, | |||
}; | |||
FastBlock.msg = { | |||
label: "Killen", | |||
prompt: "warum killen?"//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/FastRestore.js | |||
/** old revision restore */ | |||
var FastRestore = { | |||
init: function() { | |||
Config.patch(FastRestore.cfg); | |||
}, | |||
/** returns a link restoring a given version */ | |||
linkRestore: function(title, oldid, user, date, doneFunc) { | |||
var summary = FastRestore.cfg.summary(title, oldid, user, date); | |||
var restore = Links.functionLink(FastRestore.msg.label, function() { | |||
var feedback = new FeedbackLink(restore); | |||
Actions.restoreVersion(feedback, title, oldid, summary, doneFunc); | |||
}); | |||
jsutil.DOM.addClass(restore, "link-immediate"); | |||
return restore; | |||
}//, | |||
}; | |||
FastRestore.msg = { | |||
label: "restore"//, | |||
}; | |||
FastRestore.cfg = { | |||
"de": { | |||
/** compile a summary for a restore-action */ | |||
summary: function(title, oldid, user, date) { | |||
return "zurück auf " + Markup.userLink(user, user) + " " + date + " (" + oldid + ")"; | |||
}//, | |||
}, | |||
"en": { | |||
/** compile a summary for a restore-action */ | |||
summary: function(title, oldid, user, date) { | |||
return "back to " + Markup.userLink(user, user) + " " + date + " (" + oldid + ")"; | |||
}//, | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/FastUndo.js | |||
/** ajax undo */ | |||
var FastUndo = { | |||
/** extend an undo link */ | |||
patchLink: function(link, doneFunc) { | |||
var params = Wiki.decodeURL(link.href); | |||
if (!params.undo) return; // TODO necessary? | |||
jsutil.DOM.addClass(link, "link-immediate"); | |||
link.onclick = function() { | |||
var feedback = new FeedbackLink(link); | |||
Actions.undoVersion(feedback, params.title, params.undo, params.undoafter, doneFunc); | |||
return false; | |||
}; | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/FastRollback.js | |||
/** ajax rollback */ | |||
var FastRollback = { | |||
init: function() { | |||
Config.patch(FastRollback.cfg); | |||
}, | |||
/** extend a rollback link */ | |||
patchLink: function(link, doneFunc) { | |||
var params = Wiki.decodeURL(link.href); | |||
if (!params.token) return null; // TODO necessary? | |||
var msg = FastRollback.msg; | |||
var cfg = FastRollback.cfg; | |||
var promptLabel = cfg.requireReason ? msg.promptLabel : null; | |||
var mainLink = Links.promptPopupLink(msg.label, promptLabel, cfg.reasons, function(reason) { | |||
var feedback = new FeedbackLink(mainLink); | |||
var summary = cfg.commentFunc(params.from, reason); | |||
Actions.rollbackEdit(feedback, params.title, params.from, params.token, summary, doneFunc); | |||
return false; | |||
}); | |||
//link.title = msg.tooltip.deletePage; | |||
link.parentNode.replaceChild(mainLink, link); | |||
return mainLink; | |||
}, | |||
}; | |||
FastRollback.msg = { | |||
label: "rollback", | |||
promptLabel: "Rollback - grund?"//, | |||
}; | |||
FastRollback.cfg = { | |||
requireReason: false, | |||
commentFunc: function(victim, reason) { return reason === null ? null : "revert " + Markup.contribsLink(victim, victim) + ": " + reason; }, | |||
"de": { | |||
reasons: [ | |||
[ "vandalismus", "unfug", "ach", "flame" ], | |||
[ "warum?", "quelle?", "häh?" ], | |||
[ "glaskugelei", "pov" ] | |||
], | |||
}, | |||
"en": { | |||
reasons: null | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/UserRevert.js | |||
/** add a revert-button for non-admins */ | |||
var UserRevert = { | |||
init: function() { | |||
Config.patch(UserRevert.cfg); | |||
}, | |||
/** returns a link revertung edits by a given user */ | |||
linkRevert: function(title, badUser, doneFunc) { | |||
var self = this; | |||
var revert = Links.functionLink(UserRevert.msg.revert, function() { | |||
self.revert(feedback, title, badUser, Afterwards.maybeHistoryFunc(title)); | |||
}); | |||
var feedback = new FeedbackLink(revert); | |||
jsutil.DOM.addClass(revert, "link-immediate"); | |||
return revert; | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** reverts new edits of a given user */ | |||
revert: function(feedback, title, badUser, doneFunc) { | |||
function phase1() { | |||
Actions.newEdits(feedback, title, badUser, phase2); | |||
} | |||
function phase2(title, user, previousUser, revid, timestamp) { | |||
var summary = UserRevert.cfg.summary(title, user, previousUser, revid, timestamp); | |||
Actions.restoreVersion(feedback, title, revid, summary, doneFunc); | |||
} | |||
phase1(); | |||
}//, | |||
}; | |||
UserRevert.msg = { | |||
revert: "revert"//, | |||
}; | |||
UserRevert.cfg = { | |||
"de": { | |||
summary: function(title, user, previousUser, revid, timestamp) { | |||
// msg:revertpage | |||
// Änderungen von [[{{ns:user}}:$2|$2]] ([[{{ns:special}}:Contributions/$2|Beiträge]]) rückgängig gemacht und letzte Version von $1 wiederhergestellt | |||
return "Änderungen von " + | |||
Markup.userLink(user, user) + " " + | |||
"(" + Markup.contribsLink(user, "Beiträge") + ") " + | |||
"rückgängig gemacht und letzte Version von " + | |||
previousUser + " wiederhergestellt"; | |||
}//, | |||
}, | |||
"en": { | |||
summary: function(title, user, previousUser, revid, timestamp) { | |||
// msg:revertpage | |||
// Reverted edits by [[Special:Contributions/$2|$2]] ([[User talk:$2|talk]]) to last version by $1 | |||
return "Reverted edits by " + | |||
Markup.contribsLink(user, user) + " " + | |||
"(" + Markup.talkLink(user, "talk") + ") " + | |||
"to last version by " + previousUser; | |||
}//, | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/TemplatePage.js | |||
/** puts templates into the current page */ | |||
var TemplatePage = { | |||
/** return an Array of links to actions for normal pages */ | |||
bankAllPage: function(title) { | |||
// HACK does not make sense on other wikis | |||
if (Wiki.domain !== "de.wikipedia.org") return null; | |||
var msg = TemplatePage.msg; | |||
var self = this; | |||
return [ | |||
Links.promptLink(msg.urv.label, msg.urv.prompt, function(source) { self.urv(title, source); }), | |||
Links.promptLink(msg.qs.label, msg.qs.prompt, function(reason) { self.qs(title, reason); }), | |||
Links.promptLink(msg.la.label, msg.la.prompt, function(reason) { self.la(title, reason); }), | |||
Links.promptLink(msg.sla.label, msg.sla.prompt, function(reason) { self.sla(title, reason); })//, | |||
]; | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** puts an SLA template into an article */ | |||
sla: function(title, reason) { | |||
var template = "löschen"; | |||
// reason as parameter | |||
var summary = Markup.template(template, reason); | |||
var text = Markup.template(template, reason + Markup.SIGAPP) | |||
var sepa = Markup.LF; | |||
var feedback = new FeedbackArea(); | |||
Actions.prependText(feedback, title, text, summary, sepa, false, Afterwards.maybeReloadFunc(title)); | |||
}, | |||
/** puts an LA template into an article */ | |||
la: function(title, reason) { | |||
// TODO should automatically nowiki an SLA | |||
var template = "subst:Löschantrag"; | |||
var listPage = "Wikipedia:Löschkandidaten"; | |||
// images have a separate page | |||
var split = Namespace.split(title); | |||
if (split.nsIndex === 6) { | |||
// not necessary | |||
// template = "subst:Löschantrag_Bild"; | |||
listPage = "Wikipedia:Löschkandidaten/Bilder"; | |||
} | |||
var self = this; | |||
var feedback = new FeedbackArea(); | |||
// insert template | |||
function phase1() { | |||
// reason as parameter | |||
var summary = Markup.template(template, reason); | |||
var text = Markup.template(template, reason + Markup.SIGAPP); | |||
var sepa = Markup.LF; | |||
Actions.prependText(feedback, title, text, summary, sepa, false, phase2); | |||
} | |||
// add to list page | |||
function phase2() { | |||
// daily page | |||
var page = listPage + "/" + self.currentDate(); | |||
var text = Markup.h2(Markup.referenceLink(title)) + Markup.LF + reason + Markup.SIGAPP; | |||
var summary = Markup.link(title) + Markup.SP + Markup.DASH + Markup.SP + reason; | |||
var sepa = Markup.LF; | |||
Actions.appendText(feedback, page, text, summary, sepa, true, Afterwards.maybeReloadFunc(title)); | |||
} | |||
phase1(); | |||
}, | |||
/** puts an QS template into an article and links to the article from the list page */ | |||
qs: function(title, reason) { | |||
var template = "subst:QS"; | |||
var listPage = "Wikipedia:Qualitätssicherung"; | |||
var self = this; | |||
var feedback = new FeedbackArea(); | |||
// insert template | |||
function phase1() { | |||
// reason as parameter | |||
var summary = Markup.template(template, reason); | |||
var text = Markup.template(template, reason + Markup.SIGAPP); | |||
var sepa = Markup.LF; | |||
Actions.prependText(feedback, title, text, summary, sepa, false, phase2); | |||
} | |||
// add to list page | |||
function phase2() { | |||
// daily page | |||
var page = listPage + "/" + self.currentDate(); | |||
var text = Markup.h2(Markup.referenceLink(title)) + Markup.LF + reason + Markup.SIGAPP; | |||
var summary = Markup.link(title) + Markup.SP + Markup.DASH + Markup.SP + reason; | |||
var sepa = Markup.LF; | |||
Actions.appendText(feedback, page, text, summary, sepa, true, Afterwards.maybeReloadFunc(title)); | |||
} | |||
phase1(); | |||
}, | |||
/** puts an URV template into an article */ | |||
urv: function(title, source) { | |||
var template = "URV"; | |||
var listPage = "Wikipedia:Löschkandidaten/Urheberrechtsverletzungen"; | |||
var self = this; | |||
var feedback = new FeedbackArea(); | |||
// insert template | |||
function phase1() { | |||
// reason behind template | |||
var summary = Markup.template(template) + Markup.SP + "von" + Markup.SP + Markup.web(source); | |||
var text = Markup.template(template) + Markup.SP + Markup.web(source) + Markup.SIGAPP; | |||
// replace complete text | |||
var replaceFunc = jsutil.Function.constant(text); | |||
Actions.replaceText(feedback, title, replaceFunc, summary, false, false, phase2); | |||
} | |||
// add to list page | |||
function phase2() { | |||
// single page with ordered list | |||
var page = listPage; | |||
var text = "# " + Markup.referenceLink(title) + Markup.SP + "von" + Markup.SP + Markup.web(source) + Markup.SIGAPP; | |||
var summary = Markup.link(title) + Markup.SP + "von" + Markup.SP + Markup.web(source); | |||
function replace(t) { return t.replace(/\s+$/, "") + Markup.LF + text; } | |||
Actions.replaceText(feedback, page, replace, summary, false, true, Afterwards.maybeReloadFunc(title)); | |||
} | |||
phase1(); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## helper | |||
/** returns the current date in the format the LKs are organized */ | |||
currentDate: function() { | |||
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; | |||
}//, | |||
}; | |||
TemplatePage.msg = { | |||
urv: { | |||
label: "URV", | |||
prompt: "urv-quelle?"//, | |||
}, | |||
qs: { | |||
label: "QS", | |||
prompt: "warum qs?"//, | |||
}, | |||
la: { | |||
label: "LA", | |||
prompt: "warum la?"//, | |||
}, | |||
sla: { | |||
label: "SLA", | |||
prompt: "warum sla?"//, | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/TemplateTalk.js | |||
/** puts templates into user talkpages */ | |||
var TemplateTalk = { | |||
init: function() { | |||
Config.patch(TemplateTalk.cfg); | |||
}, | |||
/** return an Array of links for userTalkPages or null if none exist */ | |||
bankOfficial: function(user) { | |||
return this.talksBank(user, "official", false); | |||
}, | |||
/** return an Array of links for userTalkPages or null if none exist */ | |||
bankPersonal: function(user) { | |||
return this.talksBank(user, "personal", true); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** returns a talkArray localized to the currrent wiki */ | |||
talksBank: function(user, bankName, ownTemplate) { | |||
var talks = TemplateTalk.cfg.talks; | |||
if (!talks) return null; | |||
var bank = talks[bankName]; | |||
if (!bank || bank.length === 0) return null; | |||
var out = []; | |||
for (var i=0; i<bank.length; i++) { | |||
var template = bank[i]; | |||
out.push(this.linkTalkTo( | |||
user, template, ownTemplate)); | |||
} | |||
return out; | |||
}, | |||
/** creates a link to "talk" to a user */ | |||
linkTalkTo: function(user, template, ownTemplate) { | |||
if (template.constructor === String) { | |||
var handler = this.talkToOld.bind(this, user, template, ownTemplate); | |||
var link = Links.functionLink(template, handler); | |||
jsutil.DOM.addClass(link, "link-immediate"); | |||
return link; | |||
} | |||
else { | |||
var handler = this.talkToNew.bind(this, user, template.subst, template.separator, template.summary); | |||
var link = Links.functionLink(template.label, handler); | |||
jsutil.DOM.addClass(link, "link-immediate"); | |||
return link; | |||
} | |||
}, | |||
/** puts a signed talk-template into a user's talkpage */ | |||
talkToOld: function(user, template, ownTemplate) { | |||
var title = Titles.userTalkPage(user); | |||
var inner = ownTemplate | |||
? "subst:" + Titles.userPage(Wiki.user, template) | |||
: "subst:" + template; | |||
var text = Markup.template(inner) + Markup.SP + Markup.SIGAPP + Markup.LF; | |||
var sepa = Markup.LINE + Markup.LF; | |||
var feedback = new FeedbackArea(); | |||
Actions.appendText(feedback, title, text, template, sepa, true, Afterwards.maybeReloadFunc(title)); | |||
}, | |||
/** puts a talk-template into a user's talkpage */ | |||
talkToNew: function(user, subst, separator, summary) { | |||
var title = Titles.userTalkPage(user); | |||
var home = Titles.userPage(Wiki.user); | |||
var text = subst.template({ home: home }); | |||
var feedback = new FeedbackArea(); | |||
Actions.appendText(feedback, title, text, summary, separator, true, Afterwards.maybeReloadFunc(title)); | |||
}//, | |||
}; | |||
TemplateTalk.cfg = { | |||
talks: null, | |||
"de.wikipedia.org": { | |||
talks: { | |||
//official: [ "Hallo", "Test" ], | |||
official: [ | |||
{ | |||
label: "Hallo", | |||
subst: "{{subst:Hallo}}", | |||
summary: "Vorlage:Hallo", | |||
separator: "\n----\n" | |||
}, | |||
{ | |||
label: "Test", | |||
subst: "{{subst:Test}}", | |||
summary: "Vorlage:Test", | |||
separator: "\n----\n" | |||
}//, | |||
], | |||
//personal: [ "spielen", "genug", "stop" ]//, | |||
personal: [ | |||
{ | |||
label: "spielen", | |||
subst: "{{subst:${home}/spielen}} -- ~~~~", | |||
summary: "spielen", | |||
separator: "\n----\n" | |||
}, | |||
{ | |||
label: "genug", | |||
subst: "{{subst:${home}/genug}} -- ~~~~", | |||
summary: "genug", | |||
separator: "\n----\n" | |||
}, | |||
{ | |||
label: "stop", | |||
subst: "{{subst:${home}/stop}} -- ~~~~", | |||
summary: "stop", | |||
separator: "\n----\n" | |||
}//, | |||
]//, | |||
}//, | |||
}, | |||
"en.wiktionary.org": { | |||
talks: { | |||
// official: [ "welcome" ], | |||
official: [ | |||
{ | |||
label: "welcome", | |||
subst: "{{subst:Welcome}} -- ~~~~", | |||
summary: "Template:Welcome", | |||
separator: "\n----\n" | |||
}//, | |||
], | |||
personal: [ ]//, | |||
}//, | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/UserPage.js | |||
/** cares for pages in the user namespace */ | |||
var UserPage = { | |||
/** create bank of readLinks to private pages */ | |||
bankGoto: function() { | |||
function addLink(name) { | |||
var link = Links.readLink(name, Titles.userPage(Wiki.user, name)); | |||
out.push(link); | |||
} | |||
var out = []; | |||
var names = UserPage.cfg.pages; | |||
if (names === null | |||
|| names.length === 0) return null; | |||
for (var i=0; i<names.length; i++) { | |||
addLink(names[i]); | |||
} | |||
return out; | |||
}//, | |||
}; | |||
UserPage.cfg = { | |||
pages: [ "new", "tmp", "todo", "test" ]//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/UserBookmarks.js | |||
/** manages a personal bookmarks page */ | |||
var UserBookmarks = { | |||
/** return an Array of links for a lemma. if it's left out, uses the current page */ | |||
bankView: function(lemma) { | |||
return [ this.linkView(), this.linkMark(lemma) ]; | |||
}, | |||
/** return the absolute page link */ | |||
linkView: function() { | |||
var link = Links.readLink(UserBookmarks.msg.view, this.pageTitle()); | |||
link.title = UserBookmarks.msg.tooltip.view; | |||
return link; | |||
}, | |||
/** add a bookmark on a user's bookmark page. if the page is left out, the current is added */ | |||
linkMark: function(lemma) { | |||
var self = this; | |||
var msg = UserBookmarks.msg; | |||
var cfg = UserBookmarks.cfg; | |||
var link = Links.promptPopupLink(msg.add, msg.prompt, cfg.reasons, function(reason) { | |||
if (lemma) self.arbitrary(reason, lemma); | |||
else self.current(reason); | |||
}); | |||
link.title = msg.tooltip.add; | |||
return link; | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** add a bookmark for an arbitrary page */ | |||
arbitrary: function(remark, lemma) { | |||
var text = "*\[\[:" + lemma + "\]\]"; | |||
if (remark) text += " " + remark; | |||
text += "\n"; | |||
this.prepend(text); | |||
}, | |||
/** add a bookmark on a user's bookmark page */ | |||
current: function(remark) { | |||
var text = Markup.STAR; | |||
if (Page.whichSpecial()) { | |||
var temp = Wiki.smushedParams(Page.params); | |||
var complete = true; | |||
for (var key in temp) { | |||
complete = key === "title"; | |||
if (!complete) break; | |||
} | |||
if (complete) { | |||
text += Markup.referenceLink(temp.title); | |||
} | |||
else { | |||
text += Markup.web(Wiki.encodeURL(temp), temp.title); | |||
} | |||
} | |||
else { | |||
var lemma = Page.title; | |||
var perma = Page.perma; | |||
var oldid = Page.params["oldid"]; | |||
var diff = Page.params["diff"]; | |||
var mode; | |||
var extra; | |||
if (oldid && diff) { | |||
if (diff === "prev" | |||
|| diff === "next" | |||
|| diff === "next" | |||
|| diff === "cur") mode = diff; | |||
else | |||
if (diff === "cur" | |||
|| diff === "0") mode = "cur"; | |||
else mode = "diff"; | |||
extra = Wiki.encodeURL({ | |||
title: lemma, | |||
oldid: oldid, | |||
diff: diff//, | |||
}); | |||
} | |||
else if (oldid) { | |||
mode = "old"; | |||
extra = Wiki.encodeURL({ | |||
title: lemma, | |||
oldid: oldid//, | |||
}); | |||
} | |||
else if (perma) { | |||
mode = "perma"; | |||
extra = perma; | |||
} | |||
else { | |||
mode = "none"; | |||
extra = null; | |||
} | |||
text += Markup.referenceLink(lemma); | |||
if (extra) text += " <small>[" + extra + " " + mode + "]</small>"; | |||
} | |||
if (remark) text += " " + remark; | |||
text += Markup.LF; | |||
this.prepend(text); | |||
}, | |||
/** add text to the bookmarks page */ | |||
prepend: function(text) { | |||
var feedback = new FeedbackArea(); | |||
Actions.prependText(feedback, this.pageTitle(), text, "", null, true); | |||
}, | |||
/** the title of the current user's bookmarks page */ | |||
pageTitle: function() { | |||
return Titles.userPage(Wiki.user, UserBookmarks.cfg.pageTitle); | |||
} | |||
}; | |||
UserBookmarks.cfg = { | |||
pageTitle: "bookmarks", | |||
reasons: [ | |||
[ "wech mager", | |||
"wech kein artikel", | |||
"wech werbung", | |||
"wech bleiben"//, | |||
], | |||
[ "relevanz?", | |||
"fakten?", | |||
"urv?"//, | |||
], | |||
[ "überarbeiten inhalt", | |||
"überarbeiten form"//, | |||
], | |||
[ "gesperrt", | |||
"interessant"//, | |||
]//, | |||
]//, | |||
}; | |||
UserBookmarks.msg = { | |||
view: "Bookmarks", | |||
add: "Merken", | |||
prompt: "bemerkung?", | |||
tooltip: { | |||
view: "bookmarkseite anzeigen", | |||
add: "auf der bookmarkseite eintragen"//, | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/Redirect.js | |||
/** creates redirects for the current page */ | |||
var Redirect = { | |||
/** returns a link changing a page into a redirect to its first link */ | |||
linkModify: function(title) { | |||
var self = this; | |||
var msg = Redirect.msg; | |||
var modify = Links.functionLink(msg.modify, function() { | |||
self.modify(title); | |||
}); | |||
jsutil.DOM.addClass(modify, "link-immediate"); | |||
modify.title =msg.tooltip.redirect; | |||
return modify; | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** changes a page into a redirect to its first link */ | |||
modify: function(title) { | |||
var feedback = new FeedbackArea(); | |||
var success = true; | |||
/** replace page with redirect */ | |||
function change(s) { | |||
var links = WikiLink.parseAll(s); | |||
if (links.length !== 1) { | |||
// do nothing | |||
success = false; | |||
return s; | |||
} | |||
var target = links[0].title; | |||
return Markup.redirect(target); | |||
} | |||
/** display abort or reload if we're on the current page */ | |||
function done() { | |||
if (!success) { | |||
feedback.failure(Redirect.msg.notSingle); | |||
return; | |||
} | |||
Afterwards.maybeReload(title); | |||
} | |||
Actions.replaceText(feedback, title, change, "", true, false, done); | |||
}//, | |||
}; | |||
Redirect.msg = { | |||
modify: "Redir", | |||
notSingle: "erwartete einen einzigen link", | |||
tooltip: { | |||
redirect: "seite in einen redirect umwandeln"//, | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/EditWarning.js | |||
/** displays an image behind the edit link on other people's user page */ | |||
var EditWarning = { | |||
init: function() { | |||
var name = Namespace.scan(2, Page.title); | |||
if (!name) return; | |||
if (name.indexOf("/") !== -1) return; | |||
if (name === Wiki.user) return; | |||
var ed = jsutil.DOM.get('ca-edit'); | |||
if (!ed) return; | |||
var a = jsutil.DOM.fetch(ed, "a", null, 0); | |||
if (!a) return; | |||
ed.style.background = "left url(" + EditWarning.cfg.image + ");"; | |||
a.style.background = "transparent"; | |||
}//, | |||
}; | |||
EditWarning.cfg = { | |||
image: "http://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Stop_hand.png/32px-Stop_hand.png"//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/Google.js | |||
/** provides links to google for article lemmata */ | |||
var Google = { | |||
/** returns a link searching google for a given title */ | |||
link: function(title) { | |||
var search = this.searchString(title); | |||
var url = Google.cfg.search + encodeURIComponent(search); | |||
var link = Links.urlLink(Google.cfg.label, url); | |||
jsutil.DOM.addClass(link, "google"); | |||
link.title = Google.msg.tooltip.google; | |||
return link; | |||
}, | |||
/** compiles the search terms into a search string */ | |||
searchString: function(title) { | |||
// search for non-article pages literally | |||
//### HACK not every colon is a namespace separator | |||
if (title.indexOf(":") !== -1) { | |||
return title.indexOf(" ") !== -1 | |||
? '"' + title + '"' | |||
: title; | |||
} | |||
var parts = this.titleParts(title); | |||
var out = ""; | |||
for (var i=0; i<parts.length; i++) { | |||
var part = parts[i]; | |||
var spaced = part.indexOf(" ") !== -1; | |||
if (spaced) out += '"' + part + '"'; | |||
else out += part; | |||
out += " "; | |||
} | |||
// don't search wikipedia (and its mirrors) | |||
return out.trim() + " -wikipedia"; | |||
}, | |||
/** splits the title into a list of search terms */ | |||
titleParts: function(title) { | |||
var parts = title.split(/[()]/); | |||
var out = []; | |||
for (var i=0; i<parts.length; i++) { | |||
var part = this.normalizePart(parts[i]); | |||
if (part === "") continue; | |||
out.push(part); | |||
} | |||
return out; | |||
}, | |||
/** removes unusable parts from a search term */ | |||
normalizePart: function(title) { | |||
return title.replace(/"/g, " ") // metacharacters | |||
.replace(/^[+-]/g, "") // metacharacters | |||
.replace(/ {2,}/g, " ") // whitespace | |||
.trim(); // whitespace | |||
}//, | |||
}; | |||
Google.cfg = { | |||
search: "http://www.google.com/search?num=100&hl=en&q=", | |||
label: "Google", // "Ⓖ", "⎈", font-size: 90%; | |||
}; | |||
Google.msg = { | |||
tooltip: { | |||
google: "lemma nachgooglen"//, | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/feature/SectionEdit.js | |||
/** beautify sectionedit links */ | |||
var SectionEdit = { | |||
init: function() { | |||
if (SectionEdit.cfg.section0) { | |||
this.section0(); | |||
} | |||
this.sectionN(); | |||
}, | |||
section0: function() { | |||
if (!Page.editable) return; | |||
var firstHeading = jsutil.DOM.get('firstHeading'); | |||
if (!firstHeading) return; | |||
var a = Links.pageLink(SectionEdit.cfg.symbol, { | |||
title: Page.title, | |||
action: "edit", | |||
section: 0 | |||
}); | |||
var span = document.createElement("span"); | |||
span.id = "firstsectionedit"; | |||
span.className = "mw-editsection"; | |||
span.appendChild(a); | |||
jsutil.DOM.insertBegin(firstHeading, span); | |||
}, | |||
sectionN: function() { | |||
var bodyContent = jsutil.DOM.get('bodyContent'); | |||
for (j=1; j<=6; j++) { | |||
var tag = "h" + j; | |||
var hdrs = jsutil.DOM.fetch(bodyContent, tag); | |||
for (var i=0; i<hdrs.length; i++) { | |||
var hdr = hdrs[i]; | |||
// replace link text with symbol | |||
var section = jsutil.DOM.fetch(hdr, "span", "mw-editsection", 0); | |||
if (!section) continue; | |||
var a = jsutil.DOM.fetch(section, "a", null, 0); | |||
a.textContent = SectionEdit.cfg.symbol; | |||
// remove cruft around the link | |||
while (section.firstChild && section.firstChild !== a) | |||
section.removeChild(section.firstChild); | |||
while (section.lastChild && section.lastChild !== a) | |||
section.removeChild(section.lastChild); | |||
} | |||
} | |||
}//, | |||
} | |||
SectionEdit.cfg = { | |||
symbol: "✍", // ✎ ✍ ✐ ✑ ✒ | |||
section0: false//, | |||
}; | |||
//====================================================================== | |||
//## app/portlet/Search.js | |||
/** #p-search */ | |||
Search = { | |||
/** remove strange whitespace messing up FF */ | |||
init: function() { | |||
var form = document.forms["searchform"]; | |||
var nodes = form.childNodes; | |||
for (var i=nodes.length-1; i>=0; i--) { | |||
var node = nodes[i]; | |||
if (node.nodeType !== Node.TEXT_NODE) continue; | |||
jsutil.DOM.removeNode(node); | |||
} | |||
} | |||
}; | |||
//====================================================================== | |||
//## app/portlet/Lang.js | |||
/** #p-lang */ | |||
var Lang = { | |||
id: 'p-lang', | |||
/** replace the #p-lang portlet with a shorter variant */ | |||
init: function() { | |||
var langs = Page.languages(); | |||
if (!langs.length) return; | |||
var content = document.createElement("div"); | |||
content.id = "langSelect"; | |||
for (var i=0; i<langs.length; i++) { | |||
if (i !== 0) { | |||
content.appendChild( | |||
document.createTextNode( | |||
Lang.msg.separator)); | |||
} | |||
var lang = langs[i]; | |||
var a = document.createElement("a"); | |||
a.className = "interwiki-" + lang.code; | |||
a.href = lang.href; | |||
a.title = lang.name; | |||
a.textContent = lang.code; | |||
content.appendChild(a); | |||
} | |||
SideBar.addSimplePortlet(this.id, Lang.msg.title, content); | |||
}//, | |||
}; | |||
Lang.msg = { | |||
title: "Sprachen", | |||
separator: " ", // " · ", | |||
}; | |||
//====================================================================== | |||
//## app/portlet/Cactions.js | |||
/** #p-cactions */ | |||
var Cactions = { | |||
id: "p-cactions", | |||
init: function() { | |||
this.unfix(); | |||
SideBar.labelItems(Cactions.msg.labels); | |||
this.addPageLogTab(); | |||
this.fixImagePageLink(); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
//## private | |||
/** add a tab with logs for the current page */ | |||
addPageLogTab: function() { | |||
if (Page.namespace < 0) return; | |||
this.addTab('ca-logs', | |||
ForPage.linkLogAbout(Page.title)); | |||
}, | |||
/** bugfix: discussion pages link to action=edit without a local description page */ | |||
fixImagePageLink: function() { | |||
if (Wiki.domain === "commons.wikimedia.org") return; | |||
var tab = jsutil.DOM.get('ca-nstab-image'); | |||
if (!tab) return; | |||
var a = tab.firstChild; | |||
if (!a.href) return; | |||
a.href = a.href.replace(/&action=edit$/, ""); | |||
}, | |||
/** move p-cactions out of column-one so it does not inherit its position:fixed */ | |||
unfix: function() { | |||
var pCactions = jsutil.DOM.get(this.id); | |||
var columnContent = jsutil.DOM.get('column-content'); // belongs to the SideBar somehow.. | |||
pCactions.parentNode.removeChild(pCactions); | |||
columnContent.insertBefore(pCactions, columnContent.firstChild); | |||
}, | |||
/** adds a tab */ | |||
addTab: function(id, content) { | |||
// ta[id] = ['g', 'Show logs for this page']; | |||
var li = document.createElement("li"); | |||
li.id = id; | |||
li.appendChild(content); | |||
var tabs = jsutil.DOM.fetch(this.id, "ul", null, 0); | |||
tabs.appendChild(li); | |||
}//, | |||
}; | |||
Cactions.msg = { | |||
labels: { | |||
'ca-talk': "Diskussion", | |||
'ca-edit': "Bearbeiten", | |||
'ca-viewsource': "Source", | |||
'ca-history': "History", | |||
'ca-protect': "Schützen", | |||
'ca-unprotect': "Freigeben", | |||
'ca-delete': "Löschen", | |||
'ca-undelete': "Entlöschen", | |||
'ca-move': "Verschieben", | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/portlet/Tools.js | |||
/** # p-tb */ | |||
var Tools = { | |||
id: 'p-tb', | |||
init: function() { | |||
SideBar.addComplexPortlet(this.id, Tools.msg.title, Tools.msg.labels, [ | |||
this.bar1(), | |||
this.bar2(), | |||
UserBookmarks.bankView(), | |||
// 't-specialpages', | |||
[ Google.link(Page.title), | |||
't-permalink'//, | |||
], | |||
this.bar3(), | |||
[ 't-recentchangeslinked', | |||
't-whatlinkshere'//, | |||
] | |||
]); | |||
}, | |||
//------------------------------------------------------------------------------ | |||
bar1: function() { | |||
if (!Page.editable) return null; | |||
var bar = []; | |||
bar.push(Redirect.linkModify(Page.title)); | |||
if (Page.deletable) { | |||
bar.push(FastDelete.linkDeletePopup(Page.title)); | |||
} | |||
return bar.length !== 0 ? bar : null; | |||
}, | |||
bar2: function() { | |||
if (!Page.editable) return null; | |||
return TemplatePage.bankAllPage(Page.title); | |||
}, | |||
bar3: function() { | |||
// does not make sense on other wikis | |||
return Wiki.domain === "commons.wikimedia.org" | |||
? [ 't-upload' ] : null; | |||
}//, | |||
}; | |||
Tools.msg = { | |||
title: "Tools", | |||
labels: { | |||
't-permalink': "Perma", | |||
't-whatlinkshere': "Hierher", | |||
't-recentchangeslinked': "Drumrum"//, | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/portlet/Navigation.js | |||
/** #p-navigation */ | |||
var Navigation = { | |||
id: 'p-navigation', | |||
init: function() { | |||
SideBar.addComplexPortlet(this.id, Navigation.msg.title, Navigation.msg.labels, [ | |||
[ 'n-recentchanges', | |||
'pt-watchlist'//, | |||
], | |||
[ ForSite.linkNewusers(), | |||
ForSite.linkNewPages()//, | |||
], | |||
ForSite.bankProjectPages(), | |||
[ ForSite.linkAllLogsPopup(), | |||
ForSite.linkAllSpecialsPopup()//, | |||
]//, | |||
]); | |||
}//, | |||
}; | |||
Navigation.msg = { | |||
title: "Navigation", | |||
labels: { | |||
'n-recentchanges': "Changes", | |||
'pt-watchlist': "Watchlist"//, | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/portlet/Personal.js | |||
/** #p-personal */ | |||
var Personal = { | |||
// cannot use p-personal which has way too much styling | |||
id: 'p-personal2', | |||
init: function() { | |||
SideBar.addComplexPortlet(this.id, Personal.msg.title, Personal.msg.labels, [ | |||
[ 'pt-userpage', | |||
'pt-mytalk', | |||
( Wiki.haveNews() ? ForUser.linkNews(Wiki.user) : null ) | |||
], | |||
[ ForUser.linkSubpages(Wiki.user), | |||
ForUser.linkLogsAbout(Wiki.user), | |||
ForUser.linkLogsActor(Wiki.user), | |||
'pt-mycontris'//, | |||
], | |||
UserPage.bankGoto(), | |||
[ 'pt-preferences', | |||
'pt-logout' | |||
]//, | |||
]); | |||
}//, | |||
}; | |||
Personal.msg = { | |||
title: "Persönlich", | |||
labels: { | |||
'pt-mytalk': "Diskussion", | |||
'pt-mycontris': "Contribs", | |||
'pt-preferences': "Prefs", | |||
'pt-logout': "Logout"//, | |||
}//, | |||
}; | |||
//====================================================================== | |||
//## app/portlet/Communication.js | |||
/** #p-communication: communication with Page.owner */ | |||
var Communication = { | |||
id: 'p-communication', | |||
init: function() { | |||
if (!Page.owner) return; | |||
if (Page.owner === Wiki.user) return; | |||
// TODO display shadowed | |||
if (!Page.ownerExists) return; | |||
var ipOwner = IP.isV4(Page.owner); | |||
SideBar.addComplexPortlet(this.id, Communication.msg.title, {}, [ | |||
TemplateTalk.bankOfficial(Page.owner), | |||
TemplateTalk.bankPersonal(Page.owner), | |||
[ ForUser.linkHome(Page.owner), | |||
ForUser.linkTalk(Page.owner)//, | |||
], | |||
[ ForUser.linkSubpages(Page.owner), | |||
ForUser.linkLogsAbout(Page.owner), | |||
ForUser.linkLogsActor(Page.owner), | |||
ForUser.linkContribs(Page.owner)//, | |||
], | |||
!ipOwner ? null : ForUser.bankIP(Page.owner), | |||
ipOwner ? null : | |||
[ ForUser.linkEmail(Page.owner)//, | |||
], | |||
[ ForUser.linkBlock(Page.owner), | |||
FastBlock.linkKillPopup(Page.owner), | |||
], | |||
]); | |||
}//, | |||
}; | |||
Communication.msg = { | |||
title: "Kommunikation"//, | |||
}; | |||
//====================================================================== | |||
//## main.js | |||
/** onload hook */ | |||
function initialize() { | |||
try { | |||
// user configuration | |||
if (typeof configure === "function") configure(); | |||
// init features | |||
Wiki.init(); | |||
if (Wiki.user !== "\u0044") return; | |||
Page.init(); | |||
ForSite.init(); | |||
TemplateTalk.init(); | |||
// init extensions | |||
FastWatch.init(); | |||
FastRestore.init(); | |||
FastRollback.init(); | |||
UserRevert.init(); | |||
ActionHistory.init(); | |||
ActionDiff.init(); | |||
SpecialAny.init(); | |||
EditWarning.init(); | |||
SectionEdit.init(); | |||
// build new portlets | |||
Search.init(); | |||
Cactions.init(); | |||
Tools.init(); | |||
Navigation.init(); | |||
Personal.init(); | |||
Communication.init(); | |||
// TODO ugly hacks: | |||
// move the sister projects, export and language portlets downwards | |||
var sisterprojects = jsutil.DOM.get("p-sisterprojects"); | |||
if (sisterprojects) { | |||
jsutil.DOM.removeNode(sisterprojects); | |||
SideBar.preparedPortlets.push(sisterprojects); | |||
} | |||
var collPrintExport = jsutil.DOM.get("p-coll-print_export"); | |||
if (collPrintExport) { | |||
jsutil.DOM.removeNode(collPrintExport); | |||
SideBar.preparedPortlets.push(collPrintExport); | |||
} | |||
var pLang = jsutil.DOM.get("p-lang"); | |||
if (pLang) { | |||
jsutil.DOM.removeNode(pLang); | |||
SideBar.preparedPortlets.push(pLang); | |||
} | |||
// display portlets created before | |||
SideBar.showPortlets(); | |||
// insert sitename header | |||
SideBar.insertSiteName(); | |||
} | |||
catch (e) { | |||
if (window.console) console.error("cannot initialize", e, e.stack); | |||
} | |||
} | |||
$(document).ready(initialize); | |||
/* </nowiki></pre> */ |