Benutzer:Paulis/monobook/admin.js
/* w:Benutzer:D/monobook ::: version für admins ::: w:Benutzer:D/monobook/admin.js */
/*
*/ //====================================================================== //## 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); } }//, }; SpecialContributions.msg = { reverted: "zurückgesetzt", }; //====================================================================== //## 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: "Neuartikel", newusers: "Newbies", }; //====================================================================== //## 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: null//, }; FastDelete.msg = { prompt: "Warum löschen?", label: "löschen", tooltip: { deletePage: "Seite löschen"//, }//, }; //====================================================================== //## 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; }, reasons: null, "de": {}, "en": {}//, }; //====================================================================== //## 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: "QS - Begründung?"//, }, la: { label: "LA", prompt: "LA - Begründung?"//, }, sla: { label: "SLA", prompt: "SLA - Begründung?"//, }//, }; //====================================================================== //## 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: [ ]//, }//, }, "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: null, // [ "tmp", ... ] }; //====================================================================== //## 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: null//, }; UserBookmarks.msg = { view: "Bookmarks", add: "Merken", prompt: "Bookmark - Kommentar?", 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), ], ]); }//, }; Communication.msg = { title: "Kommunikation"//, }; //====================================================================== //## main.js /** onload hook */ function initialize() { try { // user configuration if (typeof configure === "function") configure(); // init features Wiki.init(); 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); /*
*/