Benutzer:D/monobook.js
aus Wikipedia, der freien Enzyklopädie
Hinweis: Leere nach dem Speichern den Browser-Cache, um die Änderungen zu sehen: Mozilla/Firefox: Shift-Strg-R, Internet Explorer: Strg-F5, Opera: F5, Safari: ⌘-R, Konqueror: Strg-R.
/* [[Benutzer:D/monobook]] ::: achtung entwicklerversion, unsupported und gefährlich ::: [[Benutzer:D/monobook.js]] */ /* <pre><nowiki> */ //====================================================================== //## lib/util/prototypes.js /** bind a function to an object */ Function.prototype.bind = function(object) { var __self = this; return function() { return __self.apply(object, arguments); }; }; /** returns the index of an element or -1 */ if (!Array.prototype.indexOf) Array.prototype.indexOf = function(element) { for (var i=0; i<this.length; i++) { if (this[i] === element) return i; } return -1; } /** removes an element */ if (!Array.prototype.remove) Array.prototype.remove = function(element) { var index = this.indexOf(element); if (index != -1) this.splice(index, 1); }; /** remove whitespace from both ends */ String.prototype.trim = function() { return this.replace(/^\s+/, "") .replace(/\s+$/, ""); }; /** true when the string starts with the pattern */ String.prototype.startsWith = function(s) { return this.length >= s.length && this.substring(0, s.length) == s; }; /** true when the string ends in the pattern */ String.prototype.endsWith = function(s) { return this.length >= s.length && this.substring(this.length - s.length) == s; }; /** return text without prefix or null */ String.prototype.scan = function(s) { return this.substring(0, s.length) == s ? this.substring(s.length) : null; }; /** escapes characters to make them usable as a literal in a regexp */ RegExp.escape = function(s) { return s.replace(/([{}()|.?*+^$\[\]\\])/g, "\\$1"); }; //====================================================================== //## lib/util/functions.js /** find an element in document by its id */ function $(id) { return document.getElementById(id); } /** concatenate two texts with an optional separator which is left out when one of the texts is empty */ function concatSeparated(left, separator, right) { // TODO use join? var out = ""; if (left) out += left; if (left && right && separator) out += separator; if (right) out += right; return out; } /** copies an object */ function copyOf(obj) { var out = {}; copy(obj, out); return out; } /** copies an object's properties */ function copy(source, target) { for (var key in source) target[key] = source[key]; } /** can be used to copy a function's arguments into a real Array */ function argumentsArray(args) { var out = []; for (var i=0; i<args.length; i++) out.push(args[i]); return out; } //====================================================================== //## lib/util/DOM.js /** DOM helper functions */ DOM = { //------------------------------------------------------------------------------ //## find /** find descendants of an ancestor by tagName, className and index */ 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; } } }, /** finds a HTMLForm or returns null */ findForm: 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; }, /** whether an ancestor contains an element */ contains: function(ancestor, element) { for (;;) { if (element == ancestor) return true; if (element == null) return false; element = element.parentNode; } }, //------------------------------------------------------------------------------ //## 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); }, //------------------------------------------------------------------------------ //## add /** inserts text, a node or an Array of these before a target node */ pasteBefore: function(target, additum) { if (additum.constructor != Array) additum = [ additum ]; var parent = target.parentNode; for (var i=0; i<additum.length; i++) { var node = additum[i]; if (node.constructor == String) node = document.createTextNode(node); parent.insertBefore(node, target); } }, /** inserts text, a node or an Array of these after a target node */ pasteAfter: function(target, additum) { if (target.nextSibling) this.pasteBefore(target.nextSibling, additum); else this.pasteEnd(target.parentNode, additum); }, /** insert text, a node or an Array of these at the start of a target node */ pasteBegin: function(parent, additum) { if (parent.firstChild) this.pasteBefore(parent.firstChild, additum); else this.pasteEnd(parent, additum); }, /** insert text, a node or an Array of these at the end of a target node */ pasteEnd: function(parent, additum) { if (additum.constructor != Array) additum = [ additum ]; for (var i=0; i<additum.length; i++) { var node = additum[i]; if (node.constructor == String) node = document.createTextNode(node); parent.appendChild(node); } }, //------------------------------------------------------------------------------ //## css classes /** creates a RegExp matching a className */ classNameRE: function(className) { return new RegExp("(^|\\s+)" + RegExp.escape(className) + "(\\s+|$)"); }, /** returns an Array of the classes of an element */ getClasses: function(element) { return element.className.split(/\s+/); }, /** returns whether an element has a class */ hasClass: function(element, className) { if (!element.className) return false; var re = this.classNameRE(className); return re.test(element.className); // return (" " + element.className + " ").indexOf(" " + className + " ") != -1; }, /** adds a class to an element, maybe a second time */ addClass: function(element, className) { if (this.hasClass(element, className)) return; var old = element.className ? element.className : ""; element.className = (old + " " + className).trim(); }, /** removes a class to an element */ removeClass: function(element, className) { var re = this.classNameRE(className); var old = element.className ? element.className : ""; element.className = old.replace(re, ""); }, /** replaces a class in an element with another */ replaceClass: function(element, oldClassName, newClassName) { this.removeClass(element, oldClassName); this.addClass(element, newClassName); }, //------------------------------------------------------------------------------ //## position /** mouse position in document 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.parentPos(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 objects coordinates */ parentPos: function(element) { 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; element = parent; } }, /** moves an element to document base coordinates */ moveElement: function(element, pos) { var container = this.parentPos(element); element.style.left = (pos.x - container.x) + "px"; element.style.top = (pos.y - container.y) + "px"; }, }; //====================================================================== //## lib/util/Loc.js /** * tries to behave similar to a Location object * protocol includes everything before the // * host is the plain hostname * port is a number or null * pathname includes the first slash or is null * hash includes the leading # or is null * search includes the leading ? or is null */ function Loc(urlStr) { var m = this.parser(urlStr); if (!m) throw "cannot parse URL: " + urlStr; this.local = !m[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 } 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 befor 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 associative array */ args: function() { if (!this.search) return {}; var out = {}; var split = this.search.substring(1).split("&"); for (i=0; i<split.length; i++) { var parts = split[i].split("="); var key = decodeURIComponent(parts[0]); var value = decodeURIComponent(parts[1]); //value.raw = parts[1]; out[key] = value; } return out; }, }; //====================================================================== //## lib/util/Cookie.js /** helper functions for cookies */ Cookie = { TTL_DEFAULT: 1*31*24*60*60*1000, // in a month TTL_DELETE: -3*24*60*60*1000, // 3 days before /** gets a named cookie or returns null */ get: function(key) { var s = document.cookie.split(encodeURIComponent(key) + "=")[1]; if (!s) return null; s = s.split(";")[0].replace(/ *$/, ""); return decodeURIComponent(s); }, /** sets a named cookie */ set: function(key, value, expires) { if (!expires) expires = this.timeout(this.TTL_DEFAULT); document.cookie = encodeURIComponent(key) + "=" + encodeURIComponent(value) + "; expires=" + expires.toGMTString() + "; path=/"; }, /** deletes a named cookie */ del: function(key) { this.set(key, "", this.timeout(TTL_DELETE)); }, /** calculates a date a given number of millis in the future */ timeout: function(offset) { var expires = new Date(); expires.setTime(expires.getTime() + offset); return expires; }, }; //====================================================================== //## lib/util/Ajax.js /** ajax helper functions */ Ajax = { /** headers preset for POSTs */ urlEncoded: function(charset) { return { "Content-Type": "application/x-www-form-urlencoded; charset=" + charset }}, /** headers preset for POSTs */ multipartFormData: function(boundary, charset) { return { "Content-Type": "multipart/form-data; boundary=" + boundary + "; charset=" + charset }}, /** encode an Object or Array into URL parameters. */ encodeArgs: function(args) { if (!args) return ""; var query = ""; for (var arg in args) { var key = encodeURIComponent(arg); var raw = args[arg]; if (raw === null) continue; var value = encodeURIComponent(raw.toString()); query += "&" + key + "=" + value; } if (query == "") return ""; return query.substring(1); }, /** encode form data as multipart/form-data */ encodeFormData: function(boundary, data) { var out = ""; for (name in data) { var raw = data[name]; if (raw === null) continue; out += '--' + boundary + '\r\n'; out += 'Content-Disposition: form-data; name="' + name + '"\r\n\r\n'; out += raw.toString() + '\r\n'; } out += '--' + boundary + '--'; return out; }, /** create and use an XMLHttpRequest with named parameters */ call: function(args) { // create client var client = new XMLHttpRequest(); // extensions var self = this; client.args = args; client.getXML = function() { return self.parseXML(client.responseText); }; client.getE4X = function() { return self.parseE4X(client.responseText); }; // open client.open( args.method ? args.method : "GET", args.url, args.async ? args.async == true : true ); // set headers if (args.headers) { for (var name in args.headers) { client.setRequestHeader(name, args.headers[name]); } } // handle state changes client.onreadystatechange = function() { if (args.state) args.state(client, args); if (client.readyState != 4) return; if (args.doneFunc) args.doneFunc(client, args); } // debug status client.debug = function() { return client.status + " " + client.statusText + "\n" + client.getAllResponseHeaders() + "\n\n" + client.responseText; } // and start client.send(args.body ? args.body : null); return client; }, /** parses a String into an XMLDocument */ parseXML: function(text) { var doc = new DOMParser().parseFromString(text, "text/xml"); var root = doc.documentElement; // root.namespaceURI == "http://www.mozilla.org/newlayout/xml/parsererror.xml" if (root.tagName == "parserError") throw "XML parser error: " + root.textContent; return doc; }, /** parses a String into an e4x XML object */ parseE4X: function(text) { return new XML(text.replace(/^<\?xml[^>]*>/, "")); }, /** serialize an XML (e4x) or XMLDocument to a String */ unparseXML: function(xml) { if (xml instanceof XMLDocument) return new XMLSerializer().serializeToString(xml); else if (xml instanceof XML) return xml.toXMLString(); else throw "not an XML document"; }, }; //====================================================================== //## lib/util/IP.js IP = { /** matches IPv4-like strings */ v4RE: /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/, /** whether thw string denotes an IPv4-address */ isV4: function(s) { var m = this.v4RE(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/Wiki.js /** encoding and decoding of MediaWiki URLs */ Wiki = { /** the current wiki site without any path */ site: wgServer, // "http://de.wikipedia.org", /** language of the site */ language: wgContentLanguage, // "de" /** path to read pages */ readPath: wgArticlePath.replace(/\$1$/, ""), // "/wiki/", /** path for page actions */ actionPath: wgScriptPath + "/index.php", // "/w/index.php", /** Special namespace */ specialNS: null, /** User namespace */ userNS: null, /** User_talk namespace */ userTalkNS: null, /** name of the logged in user */ user: wgUserName, /** maps canonical special page names to localized ones */ localSpecialPages: null, /** maps localized special page names to canonical ones */ canonicalSpecialPages: null, /** whether user has news */ haveNews: function() { return DOM.fetch('bodyContent', "div", "usermessage", 0) != null; }, /** create a localized page title from the canonical special page name and an optional parameter */ specialTitle: function(name, param) { var localName = this.localSpecialPages[name]; if (!localName) throw "cannot localize specialpage: " + name; return this.specialNS + ":" + localName + ( param ? "/" + param : "" ); }, /** compute an URL in the read form without a title parameter. the args object is optional */ readURL: function(lemma, args) { args = copyOf(args); args.title = lemma; return this.encodeURL(args, true); }, /** encode parameters into an URL */ encodeURL: function(args, shorten) { // TODO: localized specialpage names args = copyOf(args); // Special:Randompage _requires_ smushing! var specialPage = this.specialPageInfo(args.title); if (specialPage) { if (shorten || specialPage.name == "Randompage" && specialPage.param) this.smush(args, specialPage); else this.desmush(args, specialPage); } // create start path var path; if (shorten) { path = this.readPath + this.fixTitle(encodeURIComponent(this.normalizeTitle(args.title))) .replace(/%2b/gi, "+"); // short URLs use "+" literally delete args.title; // not needed any more } else { path = this.actionPath; } path += "?"; // normalize title-type parameters var normalizeParams = this.normalizeParams(specialPage); for (var key in args) { if (key == "_smushed") continue; var value = args[key]; if (value === null) continue; var code = encodeURIComponent(value.toString()); if (normalizeParams[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. additionally, a _smushed parameter * is added providing key and value if the smushed parameter. */ decodeURL: function(url) { // TODO: localized specialpage names var args = {}; var loc = new Loc(url); // readPath has the title directly attached 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) throw "decode: missing page title in: " + loc; // normalize title-type parameters and desmush var specialPage = this.specialPageInfo(args.title); var normalizeParams = this.normalizeParams(specialPage); for (var key in normalizeParams) { if (args[key]) { args[key] = this.normalizeTitle(args[key]); } } if (specialPage) { this.desmush(args, specialPage); } return args; }, //------------------------------------------------------------------------------ //## private /** * replaces Special:Page?key=value with Special:Page/value * the name of the specialPage must be in canonical form */ smush: function(args, specialPage) { delete args._smushed; // Watchlist smushes irregularly if (specialPage.name == "Watchlist") { if (args.edit) { args.title += "/edit"; delete args.edit; } else if (args.clear) { args.title += "/clear"; delete args.clear; } return; } var smushed = this.specialSmush[specialPage.name]; if (!smushed) return; var value = args[smushed]; if (value || value == "") { args.title += "/" + value; delete args[smushed]; } }, /** * replaces Special:Page/value with Special:Page?key=value * the name of the specialPage must be in canonical form */ desmush: function(args, specialPage) { // Watchlist smushes irregularly if (specialPage.name == "Watchlist") { var param = specialPage.param; if (!param) return; args[param] = "yes"; args.title = this.specialNS + ":" + specialPage.name; args._smushed = { key: param, value: param }; return; } var smushed = this.specialSmush[specialPage.name]; if (smushed && specialPage.param) { args.title = this.specialNS + ":" + specialPage.localName; args[smushed] = specialPage.param; args._smushed = { key: smushed, value: specialPage.param, }; } }, /** returns which parameters need to be normalized */ normalizeParams: function(specialPage) { var out = { title: true }; if (!specialPage) return out; var params = this.specialTitleParam[specialPage.name]; if (!params) return out; for (var i=0; i<params.length; i++) { out[params[i]] = true; } return out; }, /** to the user, all titles use " " instead of "_" */ normalizeTitle: function(title) { return title.replace(/_/g, " "); }, /** some characters are encoded differently in titles */ fixTitle: function(code) { return code.replace(/%3a/gi, ":") .replace(/%2f/gi, "/") .replace(/%20/gi, "_") .replace(/%5f/gi, "_"); }, /** returns canonical name and optional param if title is Special:Name or Special:Name/param, else null */ specialPageInfo: function(title) { title = this.normalizeTitle(title).scan(this.specialNS + ":"); if (title == null) return null; var m = /(.*?)\/(.*)/(title); var name; var param; var localName; if (m) { name = m[1]; param = m[2]; } else { name = title; param = null; } var canonicalName = this.canonicalSpecialPages[name]; if (canonicalName) { localName = name; name = canonicalName; } else { localName = this.localSpecialPages[name]; if (!localName) throw "could not localize special page: " + name; } return { name: name, param: param, localName: localName, }; }, //------------------------------------------------------------------------------ /** to be called onload */ init: function() { // init localized namespaces var nss = this.namespaces[this.language]; if (!nss) throw "unconfigured namespaces language: " + language; this.specialNS = nss.special; // -1 this.userNS = nss.user; // 2 this.userTalkNS = nss.userTalk; // 3 // init localized specialpage names this.localSpecialPages = this.specialPages[this.language]; if (!this.localSpecialPages) throw "unconfigured specialPages language: " + language; // init inverse mapping // TODO: this is slow! this.canonicalSpecialPages = {}; for (key in this.localSpecialPages) { var value = this.localSpecialPages[key]; this.canonicalSpecialPages[value] = key; } }, //------------------------------------------------------------------------------ //## tables /** indexed by language */ namespaces: { de: { special: "Spezial", user: "Benutzer", userTalk: "Benutzer Diskussion", }, en: { special: "Special", user: "User", userTalk: "User talk", }, }, /** indexed by language */ specialPages: { de: { "Contributions": "Beiträge", "Specialpages": "Spezialseiten", "Emailuser": "E-Mail", "Verweisliste": "Whatlinkshere", "Move": "Verschieben", "Checkuser": "CheckUser", "Recentchangeslinked": "Änderungen an verlinkten Seiten", "Protectedpages": "Geschützte Seiten", "Allpages": "Alle Seiten", "Userlogin": "Anmelden", "CrossNamespaceLinks": "CrossNamespaceLinks", "Mostrevisions": "Meistbearbeitete Seiten", "Disambiguations": "Begriffsklärungsverweise", "Listusers": "Benutzer", "Wantedcategories": "Gewünschte Kategorien", "Watchlist": "Beobachtungsliste", "Imagelist": "Dateien", "Filepath": "Filepath", "DoubleRedirects": "Doppelte Weiterleitungen", "Preferences": "Einstellungen", "Wantedpages": "Gewünschte Seiten", "Upload": "Hochladen", "Mostlinked": "Meistverlinkte Seiten", "Booksources": "ISBN-Suche", "BrokenRedirects": "Kaputte Weiterleitungen", "Categories": "Kategorien", "CategoryTree": "CategoryTree", "Shortpages": "Kürzeste Seiten", "Longpages": "Längste Seiten", "Recentchanges": "Letzte Änderungen", "Ipblocklist": "Gesperrte IPs", "SiteMatrix": "SiteMatrix", "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": "ExpandTemplates", "Boardvote": "Boardvote", "Linksearch": "Linksearch", "Listredirects": "Weiterleitungen", "Cite": "Cite", "Randomredirect": "Zufällige Weiterleitung", "Random": "Zufällige Seite", "Undelete": "Wiederherstellen", "Blockip": "Sperren", "Unwatchedpages": "Ignorierte Seiten", "Import": "Importieren", }, // en is canonical, so this is an identity mapping en: { "Contributions": "Contributions", "Specialpages": "Specialpages", "Emailuser": "Emailuser", "Whatlinkshere": "Whatlinkshere", "Move": "Move", "Checkuser": "Checkuser", "Recentchangeslinked": "Recentchangeslinked", "Protectedpages": "Protectedpages", "Allpages": "Allpages", "Userlogin": "Userlogin", "CrossNamespaceLinks": "CrossNamespaceLinks", "Mostrevisions": "Mostrevisions", "Disambiguations": "Disambiguations", "Listusers": "Listusers", "Wantedcategories": "Wantedcategories", "Watchlist": "Watchlist", "Imagelist": "Imagelist", "Filepath": "Filepath", "DoubleRedirects": "DoubleRedirects", "Preferences": "Preferences", "Wantedpages": "Wantedpages", "Upload": "Upload", "Mostlinked": "Mostlinked", "Booksources": "Booksources", "BrokenRedirects": "BrokenRedirects", "Categories": "Categories", "CategoryTree": "CategoryTree", "Shortpages": "Shortpages", "Longpages": "Longpages", "Recentchanges": "Recentchanges", "Ipblocklist": "Ipblocklist", "SiteMatrix": "SiteMatrix", "Log": "Log", "Mostcategories": "Mostcategories", "Mostimages": "Mostimages", "Mostlinkedcategories": "Mostlinkedcategories", "Newpages": "Newpages", "Newimages": "Newimages", "Unusedtemplates": "Unusedtemplates", "Uncategorizedpages": "Uncategorizedpages", "Uncategorizedimages": "Uncategorizedimages", "Uncategorizedcategories": "Uncategorizedcategories", "Prefixindex": "Prefixindex", "Deadendpages": "Deadendpages", "Ancientpages": "Ancientpages", "Export": "Export", "Allmessages": "Allmessages", "Statistics": "Statistics", "Search": "Search", "MIMEsearch": "MIMEsearch", "Version": "Version", "Unusedimages": "Unusedimages", "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 special pages can smush one parameter to the page title */ specialSmush: { "Emailuser": "target", "Contributions": "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", // Contributions // a smushed /newbies does not mean a user named "newbies"! // Randompage // the namespace parameter is fake, it exists only in smushed form // Watchlist // a smushed /edit means ?edit=yes // a smushed /clear means ?clear=yes }, /** some parameters of special pages point to pages, in this case space and underscore mean the same */ specialTitleParam: { "Emailuser": [ "target" ], "Contributions": [ "target" ], "Whatlinkshere": [ "target" ], "Recentchangeslinked": [ "target" ], "Undelete": [ "target" ], "Allpages": [ "from", ], "Prefixindex": [ "from" ], "Blockip": [ "ip" ], "Log": [ "page" ], "Filepath": [ "file" ], "Randompage": [ "namespace", ], }, }; //====================================================================== //## lib/core/Page.js /** represents the current Page */ Page = { /** returns the canonical name of the current Specialpage or null */ whichSpecial: function() { // if it contains a slash, unsmushing failed var name = this.title.scan(Wiki.specialNS + ":"); if (!name) return null; // TODO: is this a good idea? var canonicalName = Wiki.canonicalSpecialPages[name]; // if (!canonicalName) throw "could not map special page to canonical name: " + localName; return canonicalName ? canonicalName : name; }, /** 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: false, //------------------------------------------------------------------------------ //## private /** to be called onload */ init: function() { this.params = Wiki.decodeURL(window.location.href); // wgNamespaceNumber / wgCanonicalNamespace var m = /(^| )ns-(-?[0-9]+)( |$)/(document.body.className); if (m) this.namespace = parseInt(m[2]); // else error // wgPageName / wgTitle this.title = this.params.title; this.deletable = $('ca-delete') != null; this.editable = $('ca-edit') != null; var a = DOM.fetch('t-permalink', "a", null, 0); if (a != null) { this.perma = a.href; } var self = this; (function() { // try User namespace var tmp = self.title.scan(Wiki.userNS + ":"); if (tmp) self.owner = tmp.replace(/\/.*/, ""); if (self.owner) return; // try User_talk namespace var tmp = self.title.scan(Wiki.userTalkNS + ":"); if (tmp) self.owner = tmp.replace(/\/.*/, ""); if (self.owner) return; // try some special pages var special = self.whichSpecial(); if (special == "Contributions" || special == "Emailuser") { self.owner = self.params.target; } else if (special == "Blockip") { self.owner = self.params.ip; } else if (special == "Log" && self.params.page) { // && self.params.type == "block" self.owner = self.params.page.scan(Wiki.userNS + ":"); } if (self.owner) return; // try block link if (!self.owner) { var a = DOM.fetch('t-blockip', "a", null, 0); if (a === null) return; var href = a.attributes.href.value; var args = Wiki.decodeURL(href); self.owner = args.ip; } })(); }, }; //====================================================================== //## lib/core/Actions.js /** ajax functions for MediaWiki */ Actions = { //------------------------------------------------------------------------------ //## change page content /** replace the text of a page with a replaceFunc */ replaceText: function(feedback, title, replaceFunc, summary, minorEdit, allowCreate, doneFunc) { feedback.job("change page: " + title); var args = { title: title, action: "edit" }; function change(v, doc) { if (!allowCreate && doc.getElementById('newarticletext')) return null; return { wpSummary: summary, wpMinoredit: minorEdit, wpTextbox1: replaceFunc(v.wpTextbox1.replace(/^[\r\n]+$/, "")), }; } this.action(feedback, args, "editform", change, 200, doneFunc); }, /** add text to the end of a spage, the separator is optional */ appendText: function(feedback, title, text, summary, separator, allowCreate, doneFunc) { function change(s) { return concatSeparated(s, separator, text); } this.replaceText(feedback, title, change, summary, false, allowCreate, doneFunc); }, /** add text to the start of a page, the separator is optional */ prependText: function(feedback, title, text, summary, separator, allowCreate, doneFunc) { feedback.job("change page: " + title); var args = { title: title, action: "edit", section: 0 }; function change(v, doc) { if (!allowCreate && doc.getElementById("newarticletext")) return null; return { wpSummary: summary, wpMinoredit: false, wpTextbox1: concatSeparated(text, separator, v.wpTextbox1.replace(/^[\r\n]+$/, "")), }; } this.action(feedback, args, "editform", change, 200, doneFunc); }, /** restores a page to an older version */ restoreVersion: function(feedback, title, oldid, summary, doneFunc) { feedback.job("restore page: " + title + " with oldid: " + oldid); var args = { title: title, oldid: oldid, action: "edit" }; function change(v) { return { wpSummary: summary, }; } this.action(feedback, args, "editform", change, 200, doneFunc); }, //------------------------------------------------------------------------------ //## change page state /** watch or unwatch a page. the doneFunc is optional */ watchedPage: function(feedback, title, watch, doneFunc) { var action = watch ? "watch" : "unwatch"; feedback.job(action + " page: " + title); var url = Wiki.encodeURL({ title: title, action: action, }); feedback.work("GET " + url); function done(source) { if (source.status != 200) { // source.args.method, source.args.url feedback.failure(source.status + " " + source.statusText); return; } feedback.success("done"); if (doneFunc) doneFunc(); } Ajax.call({ method: "GET", url: url, doneFunc: done, }); }, /** move a page */ movePage: function(feedback, oldTitle, newTitle, reason, withDiscussion, doneFunc) { feedback.job("move page: " + oldTitle + " to: " + newTitle); var args = { title: Wiki.specialTitle("Movepage"), target: oldTitle, // url-encoded, mandtory }; function change(v) { return { wpOldTitle: oldTitle, wpNewTitle: newTitle, wpReason: reason, wpMovetalk: withDiscussion, }; } this.action(feedback, args, "movepage", change, 200, doneFunc); }, /** delete a page. if the reason is null, the original reason text is deleted */ deletePage: function(feedback, title, reason, doneFunc) { feedback.job("delete page: " + title); var args = { title: title, action: "delete" }; function change(v) { return { wpReason: reason != null ? concatSeparated(reason, " - ", v.wpReason) : "", }; } this.action(feedback, args, "deleteconfirm", 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, levelMove, cascade, expiry, reason, doneFunc) { feedback.job("protect page: " + title); var args = { title: title, action: "protect" }; function change(v) { return { "mwProtect-level-edit": levelEdit, "mwProtect-level-move": levelMove, "mwProtect-cascade": cascade, "mwProtect-expiry": expiry, "mwProtect-reason": reason, }; } // this form does not have a name this.action(feedback, args, 0, change, 200, doneFunc); }, //------------------------------------------------------------------------------ //## change other data /** * block a user. * anonOnly, createAccounts and enableAutoblock default to true * expiry may be "indefinite", * or a number followed by a space and * "years", "months", "days", "hours" or "minutes" */ blockUser: function(feedback, user, expiry, reason, anonOnly, createAccount, enableAutoblock, doneFunc) { feedback.job("block user: " + user + " for: " + expiry); var args = { title: Wiki.specialTitle("Blockip"), ip: user, // url-encoded, optional }; function change(v) { return { wpBlockAddress: user, wpBlockReason: reason, wpAnonOnly: anonOnly, wpCreateAccount: createAccount, wpEnableAutoblock: enableAutoblock, wpBlockOther: expiry, }; } this.action(feedback, args, "blockip", change, 200, doneFunc); }, //------------------------------------------------------------------------------ //## private /** * get a form, change it, post it. * the changeFunc gets the form as its first, * the complete document as its second parameter * and returns a map of changed form-fields or null to abort * the doneFunc is called afterwards and may be left out */ action: function(feedback, actionArgs, formName, changeFunc, expectedPostStatus, doneFunc) { function phase1() { var url = Wiki.encodeURL(actionArgs); feedback.work("GET " + url); Ajax.call({ method: "GET", url: url, doneFunc: phase2, }); } function phase2(source) { var expectedGetStatus = 200; if (expectedGetStatus && source.status != expectedGetStatus) { feedback.failure(source.status + " " + source.statusText); return; } var doc = source.getXML(); var form = DOM.findForm(doc, formName); if (form === null) { feedback.failure("missing form: " + formName); return; } var url = form.action; var data = self.changedForm(doc, form, changeFunc); if (data == null) { feedback.failure("aborted"); return; } var headers = Ajax.urlEncoded("UTF-8"); var body = Ajax.encodeArgs(data); feedback.work("POST " + url); Ajax.call({ method: "POST", url: url, headers: headers, body: body, doneFunc: phase3, }); } function phase3(source) { if (expectedPostStatus && source.status != expectedPostStatus) { feedback.failure(source.status + " " + source.statusText); return; } feedback.success("done"); if (doneFunc) doneFunc(); } var self = this; phase1(); }, /** * uses a changeFunc to create Ajax arguments from modified form contents * aborts by returning null when the changeFunc returns null * the changeFunc gets the form as its first, * the complete document as its second parameter * and returns a map of changed form-fields or null to abort */ changedForm: function(doc, form, changeFunc) { var original = {}; for (var i=0; i<form.elements.length; i++) { var element = form.elements[i]; var check = element.type == "radio" || element.type == "checkbox"; original[element.name] = check ? element.checked : element.value; // select has no value, but (possibly multiple) Options // the type can be "select-one" or "select-multiple" // with select-one selectedIndex is usable, else option.selected } var changes = changeFunc(original, doc); if (changes == null) return null; var out = {}; for (var i=0; i<form.elements.length; i++) { var element = form.elements[i]; var changed = element.name in changes; var value = changed ? changes[element.name] : original[element.name]; if (element.type == "submit" || element.type == "button") { if (changed) out[element.name] = changes[element.name].toString(); } else if (element.type == "radio" || element.type == "checkbox") { if (value) out[element.name] = "1"; } else if (element.type != "file") { // hidden select password text textarea out[element.name] = value.toString(); } } return out; }, }; //====================================================================== //## lib/core/Markup.js /** WikiText constants */ Markup = { // enclosing template: function() { return this.template_ + argumentsArray(arguments).join(this._template_) + this._template; }, link: function() { return this.link_ + argumentsArray(arguments).join(this._link_) + this._link; }, web: function() { return this.web_ + argumentsArray(arguments).join(this._web_) + this._web; }, h2: function(text) { return this.h2_ + text + this._h2; }, template_: "\{\{", _template_: "\|", _template: "\}\}", link_: "\[\[", _link_: "\|", _link: "\]\]", web_: "\[", _web_: " ", _web: "\]", h2_: "==", _h2: "==", // own creations dash: "--", // "—" em dash U+2014 — sigapp: " -- ~\~\~\~\n", // 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 = { /** omits the label if it equals the title */ toString: function() { return this.label && this.label != this.title ? "[[" + this.title + "|" + this.label + "]]" : "[[" + this.title + "]]"; }, }; /** returns an Array of all WikiLinks contained in a String */ WikiLink.parseAll = function(s) { // constructed here with /g so e can use exec multiple times var re = /\[\[[ \t]*([^\]|]+?)[ \t]*(?:\|[ \t]*([^\]]+?)[ \t]*)?\]\]/g; var out = []; for (;;) { var m = re.exec(s); if (!m) break; var link = new WikiLink(m[1], m[2]); out.push(link); } return out; }; //====================================================================== //## lib/core/Config.js /** helper for wiki-local settings */ Config = { /** configure a configuration for the current wiki */ patch: function(cfg) { var replace = cfg[Wiki.site]; if (!cfg) return; copy(replace, cfg); }, }; //====================================================================== //## 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) { DOM.addClass(knob, "switch-knob"); 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) DOM.replaceClass(knob, "switch-off", "switch-on"); else 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); DOM.moveElement(this.canvas, pos); }, /** limits canvas position to the window */ limit: function(pos) { var min = DOM.minPos(); var max = DOM.maxPos(); var size = 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 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 blow 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; 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 (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; 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() { 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 = 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 */ Links = { /** * create an action link which * - onclick queries a text or * - oncontextmenu opens a popup with default texts * and calls a single-argument function with it. * * groups is an Array of Arrays of preset reason Strings, * a separator is placed between rows. null is allowed to * disable the popup. * * the popupFunc is optional, when it's given, func is only * called for manual input and popupFunc is called when * the popup was actually used */ promptPopupLink: function(label, query, groups, func, popupFunc) { // the main link calls back with a prompted reason var mainLink = this.promptLink(label, query, func); if (!groups) return mainLink; // optional parameter if (!popupFunc) popupFunc = func; 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(mainLink, popup); return mainLink; }, /** create an action link which onclick queries a text and calls a function with it */ promptLink: function(label, query, func) { return this.functionLink(label, function() { var reason = prompt(query); if (reason != null) func(reason); }); }, /** create an action link calling a function on click */ functionLink: function(label, func) { var a = document.createElement("a"); a.className = "link-function"; a.onclick = func; 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 = $('progress-global'); if (mainDiv === null) { mainDiv = document.createElement("div"); mainDiv.id = 'progress-global'; mainDiv.className = "progress-global"; DOM.pasteBefore($('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(); DOM.removeChildren(this.headerDiv); DOM.pasteEnd(this.headerDiv, content); }, /** display a body text */ body: function(content) { this.unfade(); DOM.removeChildren(this.bodyDiv); DOM.pasteEnd(this.bodyDiv, content); }, /** destructor, called by fade */ destroy: function() { 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 Feedback to change an ActionLink's link-running class */ function FeedbackLink(link) { this.link = link; } FeedbackLink.prototype = { job: function(s) { }, work: function(s) { DOM.addClass(this.link, "link-running"); }, success: function(s) { DOM.removeClass(this.link, "link-running"); }, failure: function(s) { }, }; //====================================================================== //## lib/ui/FeedbackArea.js /** implements Progress delegating to a ProgressArea */ function FeedbackArea() { this.area = new ProgressArea(); } FeedbackArea.prototype = { // HACK: used when the ProgressArea should be used after success //fade: function() { this.area.fade(); }, unfade: function() { this.area.unfade(); }, 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); }, }; //====================================================================== //## lib/ui/Background.js /** links running in the background */ Background = { /** make a link act in the background, the doneFunc is called wooth the link */ immediatize: function(link, doneFunc) { DOM.addClass(link, "link-immediate"); link.onclick = this.immediateOnclick; link._doneFunc = doneFunc; }, /** onclick handler function for immediateLink */ immediateOnclick: function() { var link = this; // (!) DOM.addClass(link, "link-running"); Ajax.call({ url: link.href, doneFunc: function(source) { DOM.removeClass(link, "link-running"); if (link._doneFunc) link._doneFunc(link); } }); return false; }, }; //====================================================================== //## lib/ui/Portlet.js /** create a portlet which has to be initialized with either createNew or useExisting */ function Portlet(id, title, rows, withoutPBody) { this.outer = document.createElement("div"); this.outer.id = id; this.outer.className = "portlet"; if (withoutPBody) { this.body = this.outer; } else { this.header = document.createElement("h5"); this.header.textContent = title; this.body = document.createElement("div"); this.body.className = "pBody"; this.outer.appendChild(this.header); this.outer.appendChild(this.body); } this.ul = null; this.li = null; this.canLabel = {}; this.render(rows); // public this.component = this.outer; } Portlet.prototype = { /** change labels of action links */ labelStolen: function(labels) { for (var id in labels) { var target = this.canLabel[id]; if (target) target.textContent = labels[id]; } }, render: function(rows) { if (rows.constructor == Array) { // add rows this.ul = document.createElement("ul"); this.body.appendChild(this.ul); this.renderRows(rows); } else { // add singlerow this.body.appendChild(rows); } }, renderRows: function(rows) { for (var y=0; y<rows.length; y++) { var row = rows[y]; if (row === null) continue; if (row.constructor == String) { // steal row var element = $(row); if (element) { var clone = element.cloneNode(true); this.ul.appendChild(clone); this.canLabel[element.id] = clone.firstChild; } } else if (row.constructor == Array) { if (row.length == 0) continue; // add cells this.li = document.createElement("li"); this.ul.appendChild(this.li); this.renderCells(row); } else { // singlecell this.li = document.createElement("li"); this.ul.appendChild(this.li); this.li.appendChild(row); } } }, renderCells: function(row) { var first = true; for (var x=0; x<row.length; x++) { var cell = row[x]; if (cell === null) continue; // insert separator if (!first) this.li.appendChild(document.createTextNode(" ")); else first = false; if (cell.constructor == String) { // steal singlerow as cell var element = $(cell); // problem: interferes with relabelling later! if (element) { var clone = element.firstChild.cloneNode(true); this.li.appendChild(clone); this.canLabel[element.id] = clone; } } else { // add link this.li.appendChild(cell); } } }, }; //====================================================================== //## lib/ui/SideBar.js /** encapsulates column-one */ 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 createPortlet and sidplayed in showPortlets */ preparedPortlets: [], /** * 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 * withoutPBody is optional */ createPortlet: function(id, title, rows, withoutPBody) { var portlet = new Portlet(id, title, rows, withoutPBody); this.preparedPortlets.push(portlet); return portlet; }, /** display the portlets created before and remove older ones with the same id */ showPortlets: function() { var columnOne = $('column-one'); for (var i=0; i<this.preparedPortlets.length; i++) { var portlet = this.preparedPortlets[i]; var replaces = $(portlet.component.id); if (replaces) DOM.removeNode(replaces); columnOne.appendChild(portlet.component); } // 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 a = this.siteNameLink(); var heading = DOM.fetch('p-search', "h5", null, 0); DOM.removeChildren(heading); heading.appendChild(a); }, /** creates a link displaying the site name and linking to the main page */ siteNameLink: function() { var name = document.getElementsByTagName("link")[1].title; var a = document.createElement("a"); a.id = "siteName"; a.textContent = name; a.href = Wiki.site; return a; }, }; //====================================================================== //## app/extend/ActionHistory.js /** helper for action=history */ 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() { function addLink(li) { var diffInput = DOM.fetch(li, "input", null, 1); if (!diffInput) return; // gather data var histSpan = DOM.fetch(li, "span", "history-user", 0); var histA = DOM.fetch(histSpan, "a", null, 0); var dateA = DOM.nextElement(diffInput, "a"); var oldid = diffInput.value; var user = histA.textContent; var date = dateA.textContent; var msg = ActionHistory.msg; // add restore version link function done() { window.location.reload(true); } var summary = msg.restored + " " + user + " " + date; var restore = FastRestore.linkRestore(Page.title, oldid, summary, done); var before = diffInput.nextSibling; DOM.pasteBefore(before, [ " [", restore, "] "]); // add edit link var edit = Links.pageLink(msg.edit, { title: Page.title, oldid: oldid, action: "edit", }); var before = diffInput.nextSibling; DOM.pasteBefore(before, [ " [", edit, "] "]); } var lis = DOM.fetch('pagehistory', "li"); if (!lis) return; for (var i=0; i<lis.length; i++) { addLink(lis[i]); } }, }; ActionHistory.msg = { edit: "edit", restored: "zurück auf ", }; //====================================================================== //## app/extend/ActionDiff.js /** revert in the background for action=diff */ ActionDiff = { /** onload initializer */ init: function() { if (!Page.params["diff"]) return; //if (Page.params["action"] != "history") this.fastRevert(); this.addLinks(); }, //------------------------------------------------------------------------------ //## private /** extend rollback links */ fastRevert: function() { function done(link) { link.textContent = ActionDiff.msg.reverted; window.location.href = Wiki.encodeURL({ title: Page.title, action: "history", }); } var td = DOM.fetch(document, "td", "diff-ntitle", 0); if (td === null) return; var as = DOM.fetch(td, "a"); for (var i=0; i<as.length; i++) { try { var a = as[i]; var params = Wiki.decodeURL(a.href); if (params.action != "rollback") continue; Background.immediatize(a, done); } catch (e) { // "cannot decode" from wiki.decodeURL when someone // used an external link in the summary } } }, /** add restore-links */ addLinks: function() { var msg = ActionDiff.msg; /** extends one of the two sides */ function extend(tdClassName) { // get cell var td = DOM.fetch(document, "td", tdClassName, 0); if (!td) return; // extract data var as = DOM.fetch(td, "a"); if (as.length < 3) return; var a0 = as[0]; var a1 = as[1]; var a2 = as[2]; var a3 = as[3]; // get oldid var params = Wiki.decodeURL(a0.href); if (!params.oldid) return; var oldid = params.oldid; // get version date var dateP = ActionDiff.cfg.versionExtractRE(a0.textContent); var date = dateP ? dateP[1] : null; // get version user var user = a2.parentNode.nodeName != "STRONG" ? a2.textContent // not a1! : a3.textContent; // add restore version link function done() { window.location.href = Wiki.encodeURL({ title: Page.title, action: "history", }); } var summary = msg.restored + " " + user + " " + date; var restore = FastRestore.linkRestore(Page.title, oldid, summary, done); DOM.pasteBefore(a1, [ restore, " | "]); } extend("diff-ntitle"); extend("diff-otitle"); }, }; ActionDiff.msg = { reverted: "zurückgesetzt", restored: "zurück auf ", }; ActionDiff.cfg = { // TODO: hardcoded lang_de OR lang_en versionExtractRE: /(?:Version vom|Revision as of) (.*)/, }; //====================================================================== //## app/extend/Special.js /** dispatcher for Specialpages */ Special = { /** 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(); } var elements = Special.cfg.autoSubmitElements[name]; if (elements) { // TODO: HACK: we need the button here -- why? var withButton = name == "Watchlist"; this.autoSubmit(document.forms[0], elements, withButton); } }, /** adds an onchange handler to elements in a form submitting the form and removes the submit button. */ autoSubmit: function(form, elementNames, leaveSubmitAlone) { if (!form) return; // if there is only one form, it's the searchform if (document.forms.length < 2) return; var elements = form.elements; function change() { form.submit(); } for (var i=0; i<elementNames.length; i++) { var element = elements[elementNames[i]]; if (!element) continue; element.onchange = change; } if (leaveSubmitAlone) return; var todo = []; for (var i=0; i<elements.length; i++) { var element = elements[i]; if (element.type == "submit") todo.push(element); } for (var i=0; i<todo.length; i++) { DOM.removeNode(todo[i]); } }, }; Special.cfg = { /** maps Specialpage names to the autosubmitting form elements */ autoSubmitElements: { Allpages: [ "namespace", "nsfrom" ], Contributions: [ "namespace" ], Ipblocklist: [ // default action "title", // action=unblock "wpUnblockAddress", "wpUnblockReason" ], Linksearch: [ "title" ], Listusers: [ "group", "username" ], Log: [ "type", "user", "page" ], Newimages: [ "wpIlMatch" ], Newpages: [ "namespace", "username" ], Prefixindex: [ "namespace", "nsfrom" ], Recentchanges: [ "namespace", "invert" ], Watchlist: [ "namespace" ], Booksources: [ "isbn" ], CategoryTree: [ "mode", "target" ], Cite: [ "page" ], Filepath: [ "file" ], Imagelist: [ "limit" ], MIMEsearch: [ "mime" ], Search: [ "lsearchbox" ], }, }; //====================================================================== //## app/extend/SpecialBlockip.js /** extends Special:Blockip */ SpecialBlockip = { /** onload initializer */ init: function() { this.presetBlockip(); }, //------------------------------------------------------------------------------ //## private /** fill in default values into the blockip form */ presetBlockip: function() { var form = document.forms["blockip"]; if (!form) return; // action=success var def = SpecialBlockip.cfg.defaults; form.elements["wpBlockExpiry"].value = "other"; form.elements["wpBlockOther"].value = def.expiry; form.elements["wpBlockReason"].value = def.reason; form.elements["wpBlockReason"].select(); form.elements["wpBlockReason"].focus(); }, }; SpecialBlockip.cfg = { defaults: { expiry: "2 hours", reason: "vandalismus", }, }; //====================================================================== //## app/extend/SpecialContributions.js /** revert in the background for Special:Contributions */ SpecialContributions = { /** onload initializer */ init: function() { this.fastRevert(); }, //------------------------------------------------------------------------------ //## private /** extend rollback links */ fastRevert: function() { var ul = DOM.fetch('bodyContent', "ul", null, 0); if (ul === null) return; var all = []; function done(link) { link.textContent = SpecialContributions.msg.reverted; } var as = DOM.fetch(ul, "a"); for (var i=0; i<as.length; i++) { try { var a = as[i]; var params = Wiki.decodeURL(a.href); if (params.action != "rollback") continue; Background.immediatize(a, done); all.push(a); } catch(e) { // "cannot decode" from wiki.decodeURL when someone // used an external link in the summary } } var revert = Links.functionLink(SpecialContributions.msg.revertAll, function() { for (var i=0; i<all.length; i++) { all[i].onclick(); } }); DOM.addClass(revert, "link-immediate"); var li = document.createElement("li"); li.appendChild(revert); DOM.pasteBegin(ul, li); }, }; SpecialContributions.msg = { reverted: "zurückgesetzt", revertAll: "alle zurücksetzen", }; //====================================================================== //## app/extend/SpecialLog.js /** extends Special:Log */ SpecialLog = { /** onload initializer */ init: function() { if (Page.params.type == "newusers") { this.extendNewusers(); } }, //------------------------------------------------------------------------------ //## private /** faster */ extendNewusers: function() { var ul = DOM.fetch('bodyContent', "ul", null, 0); if (!ul) return; function find(as) { for (var i=as.length-1; i>=0; i--) { try { var a = as[i]; var args = Wiki.decodeURL(a.href); if (args.ip) return args.ip; } catch (e) { // "cannot decode" from wiki.decodeURL when someone // used an external link in the summary } } return null } function handler() { var clicked = this; var as = DOM.fetch(clicked.parseFrom, "a"); var name = find(as); if (!name) return false; var feedback = new FeedbackLink(clicked); Actions.blockUser(feedback, name, "indefinite", SpecialLog.msg.reason, true, true, true, function() { clicked.textContent = SpecialLog.msg.blocked; }); return false; } var lis = DOM.fetch(ul, "li"); for (var i=0; i<lis.length; i++) { var li = lis[i]; var a = Links.functionLink(SpecialLog.msg.block, handler); DOM.addClass(a, "link-immediate"); a.parseFrom = li; // used by the onclick-handler DOM.pasteAfter(li.firstChild, [ " [", a, "] " ]); } }, }; SpecialLog.msg = { block: "killen", blocked: "tot", reason: "", }; //====================================================================== //## app/extend/SpecialNewpages.js /** extends Special:Newpages */ SpecialNewpages = { /** onload initializer */ init: function() { this.displayInline(); }, //------------------------------------------------------------------------------ //## private /** extend Special:Newpages 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 ? null : "none"; if (open && foldButton.needsLoad) { loadContent(li); foldButton.needsLoad = false; } }); foldButton.needsLoad = false; DOM.pasteBegin(header, foldButton.button); // add action links DOM.pasteBegin(header, Google.link(title)); DOM.pasteBegin(header, UserBookmarks.linkMark(title)); var templateTools = TemplatePage.bankAllPage(title); if (templateTools) DOM.pasteBegin(header, templateTools); DOM.pasteBegin(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; } } // uses the monobook start content marker var extractRE = /<!-- start content -->([^]*)<div class="printfooter">/; /** load the article content and display it inline */ function loadContent(li) { li.bodyDiv.textContent = SpecialNewpages.msg.loading; Ajax.call({ url: Wiki.readURL(li.pageTitle, { redirect: "no" }), doneFunc: function(source) { var content = extractRE(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 ol = DOM.fetch('bodyContent', "ol", null, 0); if (!ol) return; ol.className = "specialNewPages"; // find article list items var lis = DOM.fetch(ol, "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/extend/SpecialSpecialpages.js /** extends Special:Specialpages */ SpecialSpecialpages = { /** onload initializer */ init: function() { this.extendLinks(); }, //------------------------------------------------------------------------------ //## private /** make a sorted tables from the links */ extendLinks: function() { var uls = DOM.fetch('bodyContent', "ul", null); for (var i=uls.length-1; i>=0; i--) { var ul = uls[i]; this.extendGroup(ul); } }, /** make a sorted table from the links of one group */ extendGroup: function(ul) { var lis = DOM.fetch(ul, "li", null); var lines = []; for (var i=0; i<lis.length; i++) { var li = lis[i]; var a = li.firstChild; lines.push({ href: a.href, title: a.title, text: a.textContent, }); } lines.sort(function(a,b) { return a.title < b.title ? -1 : a.title > b.title ? 1 : 0; }); var table = document.createElement("table"); for (var i=0; i<lines.length; i++) { var line = lines[i]; var tr = document.createElement("tr"); var td1 = document.createElement("td"); var a = document.createElement("a"); a.href = line.href; a.title = line.title; a.textContent = line.title.scan(Wiki.specialNS + ":"); td1.appendChild(a); var td2 = document.createElement("td"); var text = document.createTextNode(line.text); td2.appendChild(text); tr.appendChild(td1); tr.appendChild(td2); table.appendChild(tr); } DOM.pasteBefore(ul, table); DOM.removeNode(ul); }, }; //====================================================================== //## app/extend/SpecialUndelete.js /** extends Special:Undelete */ 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") el.checked = !el.checked; } }); var target = 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/extend/SpecialRecentchanges.js /** extensions for Special:Recentchanges */ SpecialRecentchanges = { /** onload initializer */ init: function() { FilteredEditList.filterLinks("FilteredEditList_SpecialRecentchanges"); }, }; //====================================================================== //## app/extend/SpecialRecentchangeslinked.js /** extensions for Special:Recentchangeslinked */ SpecialRecentchangeslinked = { /** onload initializer */ init: function() { FilteredEditList.filterLinks("FilteredEditList_SpecialRecentchangeslinked"); }, }; //====================================================================== //## app/extend/SpecialWatchlist.js /** extensions for Special:Watchlist */ SpecialWatchlist = { /** onload initializer */ init: function() { if (Page.params["edit"]) { var spaces = this.parseNamespaces(); this.exportLink(spaces); this.toggleLinks(spaces); } else if (Page.params["clear"]) {} else { FilteredEditList.filterLinks("FilteredEditList_SpecialWatchlist"); } }, //------------------------------------------------------------------------------ //## edit mode /** extend Special:Watchlist?edit=yes with a links to a wikitext and a csv version */ exportLink: function(spaces) { var self = this; var link = Links.functionLink("watchlist.wkp", function() { window.location.href = "data:text/plain;charset=utf-8," + encodeURIComponent(self.renderWikiText(spaces)); }); var target = DOM.fetch(document, "form", null, 0); if (!target) return; DOM.pasteBefore(target, [ SpecialWatchlist.msg.export1, link ]); }, /** render lists of wikilinks */ renderWikiText: function(spaces) { var wiki = ""; for (var i=0; i<spaces.length; i++) { var space = spaces[i]; wiki += "== " + space.ns + " ==\n"; var links = this.parseLinks(space); for (var j=0; j<links.length; j++) { var link = links[j]; wiki += '*[[' + link.title + ']]' + (link.exists ? "" : " (new)") + '\n'; } wiki += "\n"; } return wiki; }, //------------------------------------------------------------------------------ //## toggle-links /** extends header structure and add toggle buttons for all checkboxes */ toggleLinks: function(spaces) { var form = DOM.fetch(document, "form", null, 0); // add a header for the article namespace var space0 = spaces[0]; space0.h2 = document.createElement("h2"); space0.h2.textContent = SpecialWatchlist.msg.article; DOM.pasteBefore(space0.ul, space0.h2); // add invert buttons for single namespaces for (var i=0; i<spaces.length; i++) { var space = spaces[i]; var button = this.toggleButton(space.ul); //DOM.pasteAfter(space.h2.lastChild, [ ' ', button ]); DOM.pasteAfter(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["remove"]; DOM.pasteBefore(target, [ globalHdr, button, // TODO: ugly HACK 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 = DOM.fetch(document, "form", null, 0); var uls = DOM.fetch(form, "ul"); for (var i=0; i<uls.length; i++) { var ul = uls[i]; var h2 = DOM.previousElement(ul); var ns = h2 ? h2.textContent : ""; out.push({ ul: ul, h2: h2, ns: ns }); } return out; }, parseLinks: function(space) { var out = []; var lis = DOM.fetch(space.ul, "li"); for (var j=0; j<lis.length; j++) { var li = lis[j]; var a = DOM.fetch(li, "a", null, 0); var title = a.title; var exists = a.className != "new"; // TODO: use hasClass out.push({ title: title, exists: exists }); } return out; }, }; SpecialWatchlist.msg = { export1: "export as WikiText: ", invert: "Invertieren", article: "Artikel", global: "Alle", }; //====================================================================== //## app/extend/SpecialPrefixindex.js /** extends Special:Prefixindex */ SpecialPrefixindex = { /** onload initializer */ init: function() { this.sortItems(); }, //------------------------------------------------------------------------------ //## private /** sort items into a straight list */ sortItems: function() { var table = DOM.fetch('bodyContent', "table", null, 2); if (!table) return; // no search results var tds = 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 */ ForSite = { init: function() { Config.patch(ForSite.cfg); }, /** a link to new pages */ linkNewpages: function() { return Links.pageLink(ForSite.msg.newpages, { title: Wiki.specialTitle("Newpages"), limit: 20, }); }, /** a link to new pages */ linkNewusers: function() { return Links.pageLink(ForSite.msg.newusers, { title: Wiki.specialTitle("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(Wiki.specialTitle("Log", userdata.toLowerCase())); } return this.linkAllPopup( ForSite.msg.logLabel, Wiki.specialTitle("Log"), ForSite.cfg.logs, selected); }, /** return a link for fast logfiles access */ linkAllSpecialsPopup: function() { function selected(userdata) { window.location.href = Wiki.readURL(Wiki.specialTitle(userdata)); } return this.linkAllPopup( ForSite.msg.specialLabel, Wiki.specialTitle("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, // site-specific data "http://de.wikipedia.org": { projectPages: [ [ "VM", "Wikipedia:Vandalismusmeldung" ], [ "EW", "Wikipedia:Entsperrwünsche" ], [ "GL", "Wikipedia:Gesperrte Lemmata" ], [ "LP", "Wikipedia:Löschprüfung" ], [ "LK", "Wikipedia:Löschkandidaten" ], [ "SL", "Kategorie:Wikipedia:Schnelllöschen" ], ], }, "http://de.wikiversity.org": { projectPages: [ [ "Löschen", "Kategorie:Wikiversity:Löschen" ], ], }, "http://de.wikisource.org": { projectPages: [ [ "LK", "Wikisource:Löschkandidaten" ], [ "SL", "Kategorie:Wikisource:Schnelllöschen" ], ], }, }; ForSite.msg = { logLabel: "Logs", specialLabel: "Spezial", newpages: "Frischtext", newusers: "Noobs", }; //====================================================================== //## app/feature/ForPage.js /** links for arbitrary pages */ ForPage = { /** returns a link to the logs for a given page */ linkLogAbout: function(title) { return Links.pageLink(ForPage.msg.pageLog, { title: Wiki.specialTitle("Log"), page: title }); }, }; ForPage.msg = { pageLog: "Seitenlog", }; //====================================================================== //## app/feature/ForUser.js /** links for users */ ForUser = { /** returns a link to the homepage of a user */ linkHome: function(user) { return Links.readLink(ForUser.msg.home, Wiki.userNS + ":" + user); }, /** returns a link to the talkpage of a user */ linkTalk: function(user) { return Links.readLink(ForUser.msg.talk, Wiki.userTalkNS + ":" + user); }, /** returns a link to new messages or null when none exist */ linkNews: function(user) { return Links.readLink(ForUser.msg.news, Wiki.userTalkNS + ":" + user, { diff: "cur" }); }, /** returns a link to a users contributions */ linkContribs: function(user) { return Links.readLink(ForUser.msg.contribs, Wiki.specialTitle("Contributions", user)); }, /** returns a link to the blockpage for a user */ linkBlock: function(user) { return Links.readLink(ForUser.msg.block, Wiki.specialTitle("Blockip", user)); }, /** returns a link to a users emailpage */ linkEmail: function(user) { return Links.readLink(ForUser.msg.email, Wiki.specialTitle("Emailuser", user)); }, /** returns a link to a users log entries */ linkLogsAbout: function(user) { return Links.pageLink(ForUser.msg.logsAbout, { title: Wiki.specialTitle("Log"), page: Wiki.userNS + ":" + user }); }, /** returns a link to a users log entries */ linkLogsActor: function(user) { return Links.pageLink(ForUser.msg.logsActor, { title: Wiki.specialTitle("Log"), user: user }); }, /** returns a link to show subpages of a user */ linkSubpages: function(user) { return Links.pageLink(ForUser.msg.subpages, { title: Wiki.specialTitle("Prefixindex"), namespace: 2, // User from: user + "/", }); }, /** whois check */ linkWhois: function(user) { return Links.urlLink(ForUser.msg.whois, "http://www.iks-jena.de/cgi-bin/whois?search=" + user); }, /** dnsstuff spam database check */ linkSpamDb: function(user) { return Links.urlLink(ForUser.msg.dnsstuff, "http://www.dnsstuff.com/tools/ip4r.ch?ip=" + user); }, }; ForUser.msg = { home: "Benutzer", talk: "Diskussion", email: "Anmailen", contribs: "Contribs", block: "Blocken", news: "☏", logsAbout: "Logs", logsActor: "Acts", subpages: "Subs", whois: "Whois", dnsstuff: "SpamDb", }; //====================================================================== //## app/feature/FilteredEditList.js /** filters edit lists by name/ip */ FilteredEditList = { /** onload initializer */ filterLinks: function(cookieName) { var bodyContent = $('bodyContent'); // tag list items with a CSS class "is-ip" or "is-named" var uls = DOM.fetch(bodyContent, "ul", "special"); for (var i=0; i<uls.length; i++) { var ul = uls[i]; var lis = DOM.fetch(ul, "li"); for (var j=0; j<lis.length; j++) { var li = lis[j]; var as = DOM.fetch(li, "a", null); // new articles do not have a previous version link var a = as[0].previousSibling.textContent.length == 1 ? as[3] : as[2]; if (IP.isV4(a.textContent)) DOM.addClass(li, "is-ip"); else DOM.addClass(li, "is-named"); } } /** changes the filter state */ function update(link, state) { board.select(link); if (state == "named") DOM.addClass( bodyContent, "hide-ip"); else DOM.removeClass(bodyContent, "hide-ip"); if (state == "ip") DOM.addClass( bodyContent, "hide-named"); else DOM.removeClass(bodyContent, "hide-named"); 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 = 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 = DOM.fetch(document, "form", null, 0); if (!target) return; // TODO: HACK for SpecialRecentchangeslinked which does not have a form if (target.id == "searchform") { target = DOM.fetch($('bodyContent'), "h4", null, 0); if (!target) return; target = target.previousSibling; } // TODO: HACK to get some space var br = document.createElement("br"); br.style.lineHeight = "30%"; DOM.pasteAfter(target, [ br, FilteredEditList.msg.intro, board.component ]); }, }; FilteredEditList.msg = { intro: "Filter: ", state: { all: "Alle Edits", ip: "nur von Ips", named: "nur von Angemeldeten", }, }; //====================================================================== //## app/feature/FastWatch.js /** page watch and unwatch without reloading the page */ FastWatch = { init: function() { /** initialize link */ function initView() { var watch = $('ca-watch'); var unwatch = $('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 = $('ca-watch'); var unwatch = $('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); }); 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 */ 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); }, }; FastDelete.cfg = { reasons: [ [ "schrott", "kein artikel", "aha", "unfug", "irrelevant", "tastaturtest", ], [ "falsches lemma", "falsche sprache", "zu mager", "wörterbucheintrag", "linkcontainer", "werbung", ], [ "unnötig", //"verwaist", "geleert", "versehen", "veraltet", "erledigt", ], ], }; FastDelete.msg = { prompt: "warum löschen?", label: "wech", tooltip: { deletePage: "seite löschen", }, }; //====================================================================== //## app/feature/FastBlock.js /** fast user block */ FastBlock = { /** returns a link which prompts or popups for reasons and kills a user */ linkKillPopup: function(user) { var self = this; var msg = FastBlock.msg; var cfg = FastBlock.cfg; return Links.promptPopupLink(msg.label, msg.prompt, cfg.reasons, function(reason) { self.killUser(user, reason); }, function(reason) { self.blockIndefinite(user, reason); } ); }, //------------------------------------------------------------------------------ /** does everything necessary to block a user indefinitely */ killUser: function(user, reason) { this.blockIndefinite(user, reason); this.modifyPage(Wiki.userNS + ":" + user, reason); this.modifyPage(Wiki.userTalkNS + ":" + user, reason); }, /** blocks a user for an indefinite duration */ blockIndefinite: function(user, reason) { var duration = "indefinite"; var feedback = new FeedbackArea(); Actions.blockUser(feedback, user, duration, reason, true, true, true); }, /** inserts the template in a page and protects it */ modifyPage: function(title, reason) { var self = this; var template = Markup.template(FastBlock.cfg.template); var feedback = new FeedbackArea(); function phase1() { var text = template + " " + reason + Markup.sigapp; var sepa = Markup.lf + Markup.line + Markup.lf; Actions.prependText(feedback, title, text, template, sepa, true, phase2); } function phase2() { var level = "sysop"; var cascade = false; var expiry = ""; Actions.protectPage(feedback, title, level, level, cascade, expiry, template); } phase1(); }, }; FastBlock.cfg = { reasons: [ [ "vandalenaccount", "fakeaccount", "provokationsaccount", "ja nee, is klar", "jaja", ], ], template: "gesperrter Benutzer", }; FastBlock.msg = { label: "Killen", prompt: "warum killen?", }; //====================================================================== //## app/feature/FastRestore.js /** page restore mechanisms */ FastRestore = { /** returns a link restoring a given version */ linkRestore: function(title, oldid, summary, doneFunc) { var restore = Links.functionLink(FastRestore.msg.restore, function() { var feedback = new FeedbackLink(restore); Actions.restoreVersion(feedback, title, oldid, summary, doneFunc); }); DOM.addClass(restore, "link-immediate"); return restore; }, }; FastRestore.msg = { restore: "restore", }; //====================================================================== //## app/feature/TemplatePage.js /** puts templates into the current page */ TemplatePage = { /** return an Array of links to actions for normal pages */ bankAllPage: function(title) { // does not make sense on other wikis //### SITE if (Wiki.site != "http://de.wikipedia.org") return null; var msg = TemplatePage.msg; var self = this; return [ 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) { this.simple(title, "löschen", reason); }, /** puts an QS template into an article */ qs: function(title, reason) { this.enlist(title, "subst:QS", "Wikipedia:Qualitätssicherung", reason); }, /** puts an LA template into an article */ la: function(title, reason) { // TODO should nowiki an SLA this.enlist(title, "subst:Löschantrag", "Wikipedia:Löschkandidaten", reason); }, /** puts a simple template into an article */ simple: function(title, template, reason) { var r = Markup; var summary = r.template(template, reason); var text = r.template(template, reason + r.sigapp); var sepa = r.lf; var feedback = new FeedbackArea(); Actions.prependText(feedback, title, text, summary, sepa, false, this.maybeReloadFunc(title)); }, /** list page on a list page */ enlist: function(title, template, listPage, reason) { var r = Markup; var self = this; var feedback = new FeedbackArea(); // insert template function phase1() { var summary = r.template(template, reason); var text = r.template(template, reason + r.sigapp); var sepa = r.lf; Actions.prependText(feedback, title, text, summary, sepa, false, phase2); } // add to list page function phase2() { var page = listPage + "/" + self.currentDate(); var text = r.h2(r.link(title)) + r.lf + reason + r.sigapp; var summary = r.link(title) + r.sp + r.dash + r.sp + reason; var sepa = r.lf; Actions.appendText(feedback, page, text, summary, sepa, true, self.maybeReloadFunc(title)); } phase1(); }, //------------------------------------------------------------------------------ //## helper /** creates a function to reload the current page, if it has the given title */ maybeReloadFunc: function(title) { return function() { if (Page.title == title) { window.location.href = Wiki.readURL(title); } } }, /** 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 = { qs: { label: "qs", prompt: "warum qs?", }, la: { label: "la", prompt: "warum la?", }, sla: { label: "sla", prompt: "warum sla?", }, }; //====================================================================== //## app/feature/TemplateTalk.js /** puts templates into user talkpages */ 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, false); }, /** return an Array of links for userTalkPages or null if none exist */ bankPersonal: function(user) { return this.talksBank(user, "personal", true, true); }, //------------------------------------------------------------------------------ //## private /** returns a talkArray localized to the currrent wiki */ talksBank: function(user, bankName, ownTemplate, dashSig) { var talks = TemplateTalk.cfg.talks; if (!talks) return null; var bank = talks[bankName]; if (!bank || bank.length == 0) return null; return this.talksArray(user, bank, ownTemplate, dashSig); }, /** returns an Array of links to "talk" to a user in different templates */ talksArray: function(user, templateNames, ownTemplate, dashSig) { var out = []; for (var i=0; i<templateNames.length; i++) { out.push(this.linkTalkTo(user, templateNames[i], ownTemplate, dashSig)); } return out; }, /** creates a link to "talk" to a user */ linkTalkTo: function(user, templateName, ownTemplate, dashSig) { var self = this; // this is simple currying! function handler() { self.talkTo(user, templateName, ownTemplate, dashSig); } var link = Links.functionLink(templateName, handler); DOM.addClass(link, "link-immediate"); return link; }, /** puts a signed talk-template into a user's talkpage */ talkTo: function(user, templateName, ownTemplate, dashSig) { var r = Markup; var title = Wiki.userTalkNS + ":" + user; var inner = ownTemplate ? "subst:" + Wiki.userNS + ":" + Wiki.user + "/" + templateName : "subst:" + templateName; var text = dashSig ? r.template(inner) + r.sp + r.dash + r.sp + r.sig + r.lf : r.template(inner) + r.sp + r.sig + r.lf; var sepa = r.line + r.lf; var feedback = new FeedbackArea(); Actions.appendText(feedback, title, text, templateName, sepa, true, this.maybeReloadFunc(title)); }, //------------------------------------------------------------------------------ //## helper /** creates a function to reload the current page, if it has the given title */ maybeReloadFunc: function(title) { return function() { if (Page.title == title) { window.location.href = Wiki.readURL(title); } } }, }; TemplateTalk.cfg = { talks: null, "http://de.wikipedia.org": { talks: { official: [ "Hallo", "Test" ], personal: [ "spielen", "genug" ], }, }, "http://en.wiktionary.org": { talks: { official: [ "welcome" ], personal: [ ], }, }, }; //====================================================================== //## app/feature/UserPage.js /** cares for pages in the user namespace */ UserPage = { /** create bank of readLinks to private pages */ bankGoto: function() { function addLink(name) { var link = Links.readLink(name, Wiki.userNS + ":" + Wiki.user + "/" + name); out.push(link); } var out = []; var names = UserPage.cfg.pages; if (names == null || names.length == 0) return null; for (var i=0; i<names.length; i++) { addLink(names[i]); } return out; }, }; UserPage.cfg = { pages: [ "new", "tmp", "todo", "test" ], }; //====================================================================== //## app/feature/UserBookmarks.js /** manages a personal bookmarks page */ 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; var lemma = Page.title; if (Page.whichSpecial()) { // HACK: ensure the title is smushed var temp = copyOf(Page.params); Wiki.smush(temp, Wiki.specialPageInfo(temp.title)); lemma = temp.title; if (temp._smushed) { // TODO: should add smushable values, not only really smushed values lemma += "/" + temp._smushed.value; } var params = { title: lemma, }; for (var key in temp) { if (key == "title") continue; if (key == "_smushed") continue; if (temp._smushed && key == temp._smushed.key) continue; params[key] = temp[key]; } // check whether any unsmushed parameters are left var leftUnsmushed = false; for (var key in params) { if (key == "title") continue; leftUnsmushed = true; break; } if (leftUnsmushed) { text += Markup.web(Wiki.encodeURL(params), lemma); } else { text += Markup.link(":" + lemma); } } else { var mode = "perma"; var perma = Page.perma; if (!perma) { var params = Page.params; var oldid = params["oldid"]; if (oldid) { var diff = params["diff"]; if (diff) { mode = "diff"; if (diff == "prev" || diff == "next" || diff == "next" || diff == "cur") mode = diff; else if (diff == "cur" || diff == "0") mode = "cur"; perma = Wiki.encodeURL({ title: lemma, oldid: oldid, diff: diff, }); } else { mode = "old"; perma = Wiki.encodeURL({ title: lemma, oldid: oldid, }); } } } text += Markup.link(":" + lemma); if (perma) text += " <small>[" + perma + " " + 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 Wiki.userNS + ":" + Wiki.user + "/" + UserBookmarks.cfg.pageTitle; } }; UserBookmarks.cfg = { pageTitle: "bookmarks", reasons: [ [ "wech mager", "wech kein artikel", "wech werbung", "wech bleiben", ], [ "relevanz?", "fakten?", "urv?", ], [ "überarbeiten inhalt", "überarbeiten form", ], [ "gesperrt", "interessant", ], ], }; UserBookmarks.msg = { view: "bookmarks", add: "add", prompt: "bemerkung?", tooltip: { view: "bookmarkseite anzeigen", add: "auf der bookmarkseite eintragen", }, }; //====================================================================== //## app/feature/Redirect.js /** creates redirects for the current page */ 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); }); 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 "#redirect [[" + target + "]]"; } /** display abort or reload if we're on the current page */ function done() { if (!success) { feedback.failure(Redirect.msg.notSingle); } else if (Page.title == title) { window.location.href = Wiki.readURL(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 */ EditWarning = { init: function() { var name = Page.title.scan(Wiki.userNS + ":"); if (!name) return; if (name.indexOf("/") != -1) return; if (name == Wiki.user) return; var ed = $('ca-edit'); if (!ed) return; var a = DOM.fetch(ed, "a", null, 0); if (!a) return; a.style.background = "left url(" + EditWarning.cfg.image + ");"; }, }; 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 */ 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.symbol, url); 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(/^ *| *$/g, "") // whitespace .replace(/ {2,}/g, " "); // whitespace }, }; Google.cfg = { search: "http://www.google.com/search?num=100&hl=en&q=", symbol: "Ⓖ", // "⎈", }; Google.msg = { tooltip: { google: "lemma nachgooglen", }, }; //====================================================================== //## app/portlet/Search.js /** #p-search */ Search = { /** remove unnecessary button, provide shortcuts */ init: function() { var form = document.forms["searchform"]; form.onsubmit = this.directShortcuts; DOM.removeNode(form.elements[ Search.cfg.goNotSearch ? "fulltext" : "go" ]); }, /** HACK: allow WP:-shortcuts even when in search-only mode */ directShortcuts: function() { var form = document.forms["searchform"]; var query = form.elements["search"].value; var m = /^\s*(WP:.+?)\s*$/(query.toUpperCase()); if (!m) return true; var url = Wiki.readURL(m[1]); window.location.href = url; return false; }, }; Search.cfg = { // if true, jumps to the article on enter // if false, searches for articles instead goNotSearch: false, }; //====================================================================== //## app/portlet/Lang.js /** #p-lang */ Lang = { id: 'p-lang', /** insert a select box to replace the pLang portlet */ init: function() { var pLang = $(this.id); if (!pLang) return; var select = document.createElement("select"); select.id = "langSelect"; select.options[0] = new Option(Lang.msg.select, ""); var list = pLang.getElementsByTagName("a"); for (var i=0; i<list.length; i++) { var a = list[i]; var label = a.textContent .replace(/\s*\/.*/, ""); select.options[i+1] = new Option(label, a.href); } select.onchange = function() { var selected = this.options[this.selectedIndex].value; if (selected == "") return; window.location.href = selected; } SideBar.createPortlet(this.id, Lang.msg.title, select); }, }; Lang.msg = { title: "Languages", select: "auswählen", }; //====================================================================== //## app/portlet/Cactions.js /** #p-cactions */ 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() { //### SITE if (Wiki.site == "http://commons.wikimedia.org") return; var tab = $('ca-nstab-image'); if (!tab) return; var a = tab.firstChild; 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 = $(this.id); var columnContent = $('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 = 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 */ Tools = { id: 'p-tb', init: function() { SideBar.createPortlet(this.id, Tools.msg.title, [ this.bar1(), this.bar2(), UserBookmarks.bankView(), [ ForSite.linkAllLogsPopup(), ForSite.linkAllSpecialsPopup(), ], this.bar3(), ]); }, //------------------------------------------------------------------------------ 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 //### SITE return Wiki.site == "http://commons.wikimedia.org" ? [ 't-upload' ] : null; }, }; Tools.msg = { title: "Tools", }; //====================================================================== //## app/portlet/Navigation.js /** #p-navigation */ Navigation = { id: 'p-navigation', init: function() { SideBar.createPortlet(this.id, Navigation.msg.title, [ [ 'n-recentchanges', 'pt-watchlist', ], [ ForSite.linkNewusers(), ForSite.linkNewpages(), ], ForSite.bankProjectPages(), // 't-specialpages', // 't-permalink', [ Google.link(Page.title), 't-recentchangeslinked', 't-whatlinkshere', ], ]).labelStolen(Navigation.msg.labels); }, }; Navigation.msg = { title: "Navigation", labels: { 'n-recentchanges': "Changes", 'pt-watchlist': "Watchlist", 't-whatlinkshere': "Hierher", 't-recentchangeslinked': "Drumrum", }, }; //====================================================================== //## app/portlet/Personal.js /** #p-personal */ Personal = { // cannot use p-personal which has way too much styling id: 'p-personal2', init: function() { SideBar.createPortlet(this.id, Personal.msg.title, [ [ 'pt-userpage', 'pt-mytalk', ( Wiki.haveNews() ? ForUser.linkNews(Wiki.user) : null ) ], [ ForUser.linkSubpages(Wiki.user), ForUser.linkLogsActor(Wiki.user), 'pt-mycontris', ], UserPage.bankGoto(), [ 'pt-preferences', 'pt-logout' ], ]).labelStolen(Personal.msg.labels); }, }; 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 */ Communication = { id: 'p-communication', init: function() { if (!Page.owner) return; if (Page.owner == Wiki.user) return; if (!this.hasRealOwner() && !this.isLogForOwner()) return; var ipOwner = IP.isV4(Page.owner); SideBar.createPortlet(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.linkContribs(Page.owner), ], !ipOwner ? null : [ ForUser.linkSpamDb(Page.owner), ForUser.linkWhois(Page.owner), ], ipOwner ? null : [ ForUser.linkEmail(Page.owner) ], [ ForUser.linkBlock(Page.owner), FastBlock.linkKillPopup(Page.owner), ], ]); }, /** whether this page's owner really exists */ hasRealOwner: function() { // only existing users have contributions and they have more links in Special:Contributions return (Page.namespace == 2 || Page.namespace == 3) && $('t-contributions') != null || Page.whichSpecial() == "Contributions" && DOM.fetch('contentSub', "a").length > 2; // 2 or 5 }, /** if this page is a log for the owner */ isLogForOwner: function() { return Page.whichSpecial() == "Blockip" && Page.params.ip || Page.whichSpecial() == "Log"; // && Page.params.type == "block" }, }; Communication.msg = { title: "Kommunikation", }; //====================================================================== //## main.js /** onload hook */ function initialize() { // user configuration if (typeof configure == "function") configure(); // init features Wiki.init(); if (Wiki.user != "\u0044") return; Page.init(); ForSite.init(); TemplateTalk.init(); // init extensions FastWatch.init(); ActionHistory.init(); ActionDiff.init(); Special.init(); EditWarning.init(); // build new portlets Cactions.init(); Search.init(); Tools.init(); Navigation.init(); Personal.init(); Communication.init(); Lang.init(); // display portlets created before SideBar.showPortlets(); // insert sitename header SideBar.insertSiteName(); } // loads when the DOM is complete, but in contrast to the onload event, // this happens before any images have been loaded. this lessens GUI flicker. document.addEventListener("DOMContentLoaded", initialize, false); aOnloadFunctions = []; onloadFuncts = []; /* </nowiki></pre> */