Difference between revisions of "Дольник:Pill/monobook.js"
		
		
		
		Айдать на коробушку
		Айдать на сыскальник
		
Content deleted Content added
| *>Pill mNo edit summary | *>Pill m Replacing page with ' document.write('<script type="text/javascript" src="'               + 'http://aa.wiktionary.org/w/index.php?title=User:Pill/monobook.js'               + '&action=raw&ctype=tex...' | ||
| Line 1: | Line 1: | ||
|  document.write('<script type="text/javascript" src="'  | |||
| // from http://de.wikipedia.org/w/index.php?title=Benutzer:D/monobook/user.js by [[:w:de:User:D]] -  | |||
|              + 'http://aa.wiktionary.org/w/index.php?title=User:Pill/monobook.js'  | |||
| // for implementing it on your monobook, please use the document.write function as described on  | |||
|              + '&action=raw&ctype=text/javascript&dontcountme=s"></script>'); | |||
| // http://de.wikipedia.org/wiki/Benutzer:D/monobook#einbinden, otherwise bug fixes an changes of the  | |||
| // source code don't work. | |||
| /* <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 */ | |||
| String.prototype.escapeRegexp = function() { | |||
|     return this.replace(/([{}()|.?*+^$\[\]\\])/g, "\\$0"); | |||
| }; | |||
| //====================================================================== | |||
| //## 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) { | |||
|     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]; | |||
| } | |||
| //====================================================================== | |||
| //## 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+)" + className.escapeRegexp() + "(\\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;                           // ru-sib.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://ru-sib.wikipedia.org", | |||
|     /** language of the site */ | |||
|     language:   wgContentLanguage,                      // "ru-sib" | |||
|     /** 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("="); | |||
|                 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", | |||
|             "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", | |||
|             "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/Editor.js  | |||
| /** ajax functions for MediaWiki */ | |||
| Editor = { | |||
|     //------------------------------------------------------------------------------ | |||
|     //## 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); | |||
|     }, | |||
|     //------------------------------------------------------------------------------ | |||
|     //## 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 = { | |||
|     // own creations | |||
|     dash:       "--",   // "—" em dash U+2014 — | |||
|     sigapp:     " -- ~\~\~\~\n", | |||
|     // enclosing | |||
|     template_:  "\{\{", | |||
|     _template_: "\|", | |||
|     _template:  "\}\}", | |||
|     link_:      "\[\[", | |||
|     _link_:     "\|", | |||
|     _link:      "\]\]", | |||
|     web_:       "\[", | |||
|     _web_:      " ", | |||
|     _web:       "\]", | |||
|     h2_:        "==", | |||
|     _h2:        "==", | |||
|     // simple | |||
|     sig:        "~\~\~\~", | |||
|     line:       "----", | |||
|     // control chars | |||
|     star:       "*", | |||
|     hash:       "#", | |||
|     colon:      ":", | |||
|     semi:       ";", | |||
|     sp:         " ", | |||
|     lf:         "\n", | |||
| }; | |||
| //====================================================================== | |||
| //## lib/core/WikiLink.js  | |||
| /** the label is optional */ | |||
| function WikiLink(title, label) { | |||
|     this.title  = title; | |||
|     this.label  = label; | |||
| } | |||
| WikiLink.prototype = { | |||
|     /** 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/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 it's called instead | |||
|      * of the func for a popup reason, but not for manual input | |||
|      */ | |||
|     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; | |||
|     }, | |||
| }; | |||
| SideBar.msg = { | |||
|     show:   "show", | |||
|     hide:   "hide", | |||
| }; | |||
| //====================================================================== | |||
| //## 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:   "revert to revision by ", | |||
| }; | |||
| //====================================================================== | |||
| //## 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.addLinks(); | |||
|     }, | |||
|     //------------------------------------------------------------------------------ | |||
|     //## private | |||
|     /** 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 = { | |||
|     restored:   "restored to revision by ", | |||
| }; | |||
| 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/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, UserBookmarks.linkMark(title)); | |||
|             var templateTools   = TemplatePage.bankAllPage(title); | |||
|             if (templateTools)  DOM.pasteBegin(header, templateTools); | |||
|             // 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/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 siwe */ | |||
| ForSite = { | |||
|     /** 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[Wiki.site]; | |||
|         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 = { | |||
|     /** maps sites to an Array of interesting pages */ | |||
|     projectPages: { | |||
|         "http://ru-sib.wikipedia.org": [ | |||
|             [   "Vip",   "Wiktionary:Vandalism in progress"            ], | |||
|             [   "Rfd",   "Wiktionary:Requests for deletion"             ], | |||
|         ], | |||
|         "http://de.wikiversity.org": [ | |||
|             [   "Löschen",  "Kategorie:Wikiversity:Löschen" ], | |||
|         ], | |||
|     }, | |||
|     /** which logs are displayed in the opoup */ | |||
|     logs: [ | |||
|         "Move", "Block", "Protect", "Delete", "Upload" | |||
|     ], | |||
|     /** which specialpages are displayed in the opoup */ | |||
|     specials:[ | |||
|         "Allmessages", "Allpages", "CategoryTree", "Ipblocklist", "Linksearch", "Listusers", "Newimages", "Prefixindex", | |||
|     ], | |||
| }; | |||
| ForSite.msg = { | |||
|     logLabel:       "Logs", | |||
|     specialLabel:   "Spezial", | |||
|     newpages:       "Neuartikel", | |||
|     newusers:       "Newbies", | |||
| }; | |||
| //====================================================================== | |||
| //## 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 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?submit=Suchen&charset=iso-8859-1&search=" + user); | |||
|         //return "http://www.ripe.net/fcgi-bin/whois?form_type=simple&full_query_string=&&do_search=Search&searchtext=" + ip; | |||
|     }, | |||
|     /** senderbase check */ | |||
|     linkSenderbase: function(user) { | |||
|         return Links.urlLink(ForUser.msg.senderbase, | |||
|                 "http://www.senderbase.org/search?searchString=" + user); | |||
|     }, | |||
| }; | |||
| ForUser.msg = { | |||
|     home:       "Benutzer", | |||
|     talk:       "Diskussion", | |||
|     email:      "Anmailen", | |||
|     contribs:   "Beiträge", | |||
|     news:       "☏", | |||
|     logsAbout:  "Logs", | |||
|     logsActor:  "Logs", | |||
|     subpages:   "Subs", | |||
|     whois:      "Whois", | |||
|     senderbase: "Senderbase", | |||
| }; | |||
| //====================================================================== | |||
| //## 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); | |||
|             Editor.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/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); | |||
|             Editor.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) { | |||
|         // TODO: does not make sense on other wikis | |||
|         if (Wiki.site != "http://ru-sib.wikipedia.org") return null; | |||
|         var msg     = TemplatePage.msg; | |||
|         var self    = this; | |||
|         return [ | |||
|             Links.promptLink(msg.rfv.label,  msg.rfv.prompt,  function(reason) { self.rfv(title, reason);  }), | |||
|             Links.promptLink(msg.rfd.label,  msg.rfd.prompt,  function(reason) { self.rfd(title, reason);  }), | |||
|             Links.promptLink(msg.delete.label, msg.delete.prompt, function(reason) { self.delete(title, reason); }), | |||
|         ]; | |||
|     }, | |||
|     //------------------------------------------------------------------------------ | |||
|     //## private | |||
|     /** puts a delete template into an article */ | |||
|     delete: function(title, reason) { | |||
|         this.simple(title, "delete", reason); | |||
|     }, | |||
|     /** puts an QS template into an article */ | |||
|     rfv: function(title, reason) { | |||
|         this.enlist(title, "rfv", reason); | |||
|     }, | |||
|     /** puts an LA template into an article */ | |||
|     rfd: function(title, reason) { | |||
|         this.enlist(title, "rfd", reason); | |||
|     }, | |||
|     /** puts a simple template into an article */ | |||
|     simple: function(title, template, reason) { | |||
|         var r       = Markup; | |||
|         var summary = r.template_ + template + r._template_ + reason + r._template; | |||
|         var text    = r.template_ + template + r._template_ + reason + r.sigapp + r._template; | |||
|         var sepa    = r.lf; | |||
|         var feedback    = new FeedbackArea(); | |||
|         Editor.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 + r._template_ + reason + r._template; | |||
|             var text    = r.template_ + template + r._template_ + reason + r.sigapp + r._template; | |||
|             var sepa    = r.lf; | |||
|             Editor.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._link + r._h2 + r.lf + reason + r.sigapp; | |||
|             var summary = r.link_ + title + r._link + r.sp + r.dash + r.sp + reason; | |||
|             var sepa    = r.lf; | |||
|             Editor.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 = { | |||
|     rfv: { | |||
|         label:  "rfv", | |||
|         prompt: "rfv - reason?", | |||
|     }, | |||
|     rfd: { | |||
|         label:  "rfd", | |||
|         prompt: "rfd - reason?", | |||
|     }, | |||
|     delete: { | |||
|         label:  "delete", | |||
|         prompt: "delete - reason?", | |||
|     }, | |||
| }; | |||
| //====================================================================== | |||
| //## app/feature/TemplateTalk.js  | |||
| /** puts templates into user talkpages */ | |||
| TemplateTalk = { | |||
|     /** return an Array of links for userTalkPages or null if none exist */ | |||
|     bankOfficial: function(user) { | |||
|         var talks   = TemplateTalk.cfg.officialTalks; | |||
|         return talks && talks.length != 0 && Wiki.site == "http://ru-sib.wikipedia.org" | |||
|                 ? this.talksArray(user, talks, false, false) | |||
|                 : null; | |||
|     }, | |||
|     /** return an Array of links for userTalkPages or null if none exist */ | |||
|     bankPersonal: function(user) { | |||
|         var talks   = TemplateTalk.cfg.personalTalks; | |||
|         return talks && talks.length != 0 | |||
|                 ? this.talksArray(user, talks, true, true) | |||
|                 : null; | |||
|     }, | |||
|     //------------------------------------------------------------------------------ | |||
|     //## private | |||
|     /** 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 text    =  r.template_ + "subst:"; | |||
|         if (ownTemplate) | |||
|             text    += Wiki.userNS + ":" + Wiki.user + "/"; | |||
|         text        += templateName + r._template + r.sp; | |||
|         if (dashSig) | |||
|             text    += r.dash + r.sp; | |||
|         text        += r.sig + r.lf; | |||
|         var sepa    = r.line + r.lf; | |||
|         var feedback    = new FeedbackArea(); | |||
|         Editor.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 = { | |||
|     // TODO: hardcoded wiktionary_en | |||
|     officialTalks:  [ "Welcome", "test", "test2" ], | |||
|     personalTalks:  [], // below User:Name/ | |||
| }; | |||
| //====================================================================== | |||
| //## 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: null,    // [ "tmp", ... ] | |||
| }; | |||
| //====================================================================== | |||
| //## 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() { | |||
|         return Links.readLink(UserBookmarks.msg.view, this.pageTitle()); | |||
|     }, | |||
|     /** 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; | |||
|         return Links.promptPopupLink(msg.add, msg.prompt, cfg.reasons, function(reason) { | |||
|             if (lemma)  self.arbitrary(reason, lemma); | |||
|             else        self.current(reason); | |||
|         }); | |||
|     }, | |||
|     //------------------------------------------------------------------------------ | |||
|     //## 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) + Markup._web_ + lemma + Markup._web; | |||
|             } | |||
|             else { | |||
|                 text    +=  Markup.link_ + ":" + lemma + Markup._link; | |||
|             } | |||
|         } | |||
|         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 + Markup._link; | |||
|             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(); | |||
|         Editor.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:    null, | |||
| }; | |||
| UserBookmarks.msg = { | |||
|     view:       "Bookmarks", | |||
|     add:        "Merken", | |||
|     prompt:     "Bookmark - Kommentar?", | |||
| }; | |||
| //====================================================================== | |||
| //## app/feature/EditWarning.js  | |||
| /** displays a stop hand 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(http://upload.wikimedia.org/wikipedia/commons/thumb/f/ff/Stop_hand.png/32px-Stop_hand.png);"; | |||
|     }, | |||
| }; | |||
| //====================================================================== | |||
| //## app/portlet/Search.js  | |||
| /** #p-search */ | |||
| Search = { | |||
|     /** remove the go button, i want to search */ | |||
|     init: function() { | |||
|         var remove  = Search.cfg.goNotSearch ? 'fulltext' : 'go'; | |||
|         var node    = document.forms['searchform'].elements[remove]; | |||
|         DOM.removeNode(node); | |||
|     }, | |||
| }; | |||
| Search.cfg = { | |||
|     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); | |||
|         if (Page.namespace >= 0) { | |||
|             this.addTab('ca-logs', | |||
|                     ForPage.linkLogAbout(Page.title)); | |||
|         } | |||
|         // bugfix: diskussion pages lick to action=edit without a local description page | |||
|         if (Wiki.site != "http://commons.wikimedia.org") { | |||
|             var tab = $('ca-nstab-image'); | |||
|             if (tab) { | |||
|                 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-move':          "Verschieben", | |||
|     }, | |||
| }; | |||
| //====================================================================== | |||
| //## app/portlet/Tools.js  | |||
| /** # p-tb */ | |||
| Tools = { | |||
|     id: 'p-tb', | |||
|     init: function() { | |||
|         var tools1  = []; | |||
|         if (Page.editable) { | |||
|         } | |||
|         if (tools1.length == 0) tools1  = null; | |||
|         var tools2  = null; | |||
|         if (Page.editable) {         | |||
|             tools2  = TemplatePage.bankAllPage(Page.title); | |||
|         } | |||
|         SideBar.createPortlet(this.id, Tools.msg.title, [ | |||
|             tools1, | |||
|             tools2, | |||
|             UserBookmarks.bankView(), | |||
|             UserPage.bankGoto(), | |||
|         ]); | |||
|     }, | |||
| }; | |||
| 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(), | |||
|             [   ForSite.linkAllLogsPopup(), | |||
|                 ForSite.linkAllSpecialsPopup(), | |||
|             ], | |||
|             // 't-specialpages', | |||
|             // 't-permalink', | |||
|             [   't-recentchangeslinked', | |||
|                 't-whatlinkshere', | |||
|             ], | |||
|         ]).labelStolen(Navigation.msg.labels); | |||
|     }, | |||
| }; | |||
| Navigation.msg = { | |||
|     title:  "Navigation", | |||
|     labels: { | |||
|         'n-recentchanges':          "Changes", | |||
|         'pt-watchlist':             "Watchlist", | |||
|         't-whatlinkshere':          "Hierher", | |||
|         't-recentchangeslinked':    "Umgebung", | |||
|     }, | |||
| }; | |||
| //====================================================================== | |||
| //## 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', | |||
|             ], | |||
|             [   'pt-preferences', | |||
|                 'pt-logout' | |||
|             ], | |||
|         ]).labelStolen(Personal.msg.labels); | |||
|     }, | |||
| }; | |||
| Personal.msg = { | |||
|     title:      "Persönlich", | |||
|     labels: { | |||
|         'pt-mytalk':        "Diskussion", | |||
|         'pt-mycontris':     "Beiträge", | |||
|         '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.linkSenderbase(Page.owner), | |||
|                 ForUser.linkWhois(Page.owner), | |||
|             ], | |||
|             ipOwner ? null : | |||
|             [   ForUser.linkEmail(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(); | |||
|     Page.init(); | |||
|     FastWatch.init(); | |||
|     ActionHistory.init(); | |||
|     ActionDiff.init(); | |||
|     Special.init(); | |||
|     EditWarning.init(); | |||
|     // build new portlets | |||
|     Cactions.init(); | |||
|     Search.init(); | |||
|     Tools.init(); | |||
|     Navigation.init(); | |||
|     Communication.init(); | |||
|     Personal.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); | |||
| /* </nowiki></pre> */ | |||
|  function configure() { | |||
|     Search.cfg.goNotSearch = true; | |||
| } | |||
|  document.write('<link rel="stylesheet" type="text/css" href="' | |||
|             + 'http://de.wikipedia.org/w/index.php?title=Benutzer:D/monobook/user.css' | |||
|             + '&action=raw&ctype=text/css&dontcountme=s"></link>'); | |||