MediaWiki:LAPI.js: Difference between revisions
Content deleted Content added
No edit summary |
per tper |
||
(9 intermediate revisions by 4 users not shown) | |||
Line 1:
/*
Small JS library containing stuff I use often.
Line 24 ⟶ 22:
*/
// Global: importScript (from wiki.js, for MediaWiki:AjaxSubmit.js)
Line 31 ⟶ 28:
// Remember to double-escape the backslash.
if (typeof (LAPI_file_store) == 'undefined')
var LAPI_file_store = "
// Some basic routines, mainly enhancements of the String, Array, and Function objects.
Line 40 ⟶ 37:
// Note: adding these to the prototype may break other code that assumes that
// {} has no properties at all.
if (!Object.clone) {
Object.clone = function (source, includeInherited) {
if (!source) return null;
var result = {};
for (var key in source) {
if (includeInherited || source.hasOwnProperty (key)) result[key] = source[key];
}
return result;
};
}
if (!Object.merge) {
Object.merge = function (from, into, includeInherited) {
if (!from) return into;
for (var key in from) {
if (includeInherited || from.hasOwnProperty (key)) into[key] = from[key];
}
return into;
};
}
if (!Object.mergeSome) {
Object.mergeSome = function (from, into, includeInherited, predicate) {
if (!from) return into;
if (typeof (predicate) == 'undefined')
return Object.merge (from, into, includeInherited);
for (var key in from) {
if ((includeInherited || from.hasOwnProperty (key)) && predicate (from, into, key))
into[key] = from[key];
}
return into;
};
}
if (!Object.mergeSet) {
Object.mergeSet = function (from, into, includeInherited)
{
return Object.mergeSome
(from, into, includeInherited, function (src, tgt, key) {return src[key] !== null;});
};
}
/** String enhancements (Javascript 1.6) ************/
Line 96 ⟶ 100:
};
}
if (!String.prototype.trimFront)
String.prototype.trimFront = String.prototype.trimLeft; // Synonym // Removes given characters from the end of the string.
Line 106 ⟶ 111:
};
}
if (!String.prototype.trimEnd)
String.prototype.trimEnd = String.prototype.trimRight; // Synonym /** Further String enhancements ************/
// Returns true if the string begins with prefix.
if (!String.prototype.startsWith
return this.indexOf (prefix) === 0;
};
}
// Returns true if the string ends in suffix
if (!String.prototype.endsWith
String.prototype.endsWith = function (suffix) {
var last = this.lastIndexOf (suffix);
return last !== -1 && last + suffix.length == this.length;
};
}
// Returns true if the string contains s.
if (!String.prototype.contains
String.prototype.contains = function (s) {
return this.indexOf (s) >= 0;
};
}
// Replace all occurrences of a string pattern by replacement.
if (!String.prototype.replaceAll) {
String.prototype.replaceAll = function (pattern, replacement) { return this.split (pattern).join (replacement);
};
}
// Escape all backslashes and single or double quotes such that the result can
// be used in Javascript inside quotes or double quotes.
if (!String.prototype.stringifyJS
String.prototype.stringifyJS = function () {
return
.replace (/\n/g, '\\n');
};
}
// Escape all RegExp special characters such that the result can be safely used
// in a RegExp as a literal.
if (!String.prototype.escapeRE
String.prototype.escapeRE = function () {
return this.replace (/([\\{}()|.?*+^$\[\]])/g, "\\$1");
};
}
if (!String.prototype.escapeXML
String.prototype.escapeXML = function (quot, apos) {
var s =
.replace (/
.replace (/
.replace (/>/g, '>');
if (quot) s = s.replace (/\"/g, '"'); // " // Fix syntax coloring if (apos) s = s.replace (/\'/g, '''); // ' // Fix syntax coloring
return s;
};
}
if (!String.prototype.decodeXML
String.prototype.decodeXML = function () {
return
.replace(/&
.replace(/&
.replace(/&
.replace(/&
.replace(/&/g, '&');
};
}
if (!String.prototype.capitalizeFirst
String.prototype.capitalizeFirst = function () {
return this.substring (0, 1).toUpperCase() + this.substring (1);
};
}
if (!String.prototype.lowercaseFirst
String.prototype.lowercaseFirst = function () {
return this.substring (0, 1).toLowerCase() + this.substring (1);
};
}
// This is actually a function on URLs, but since URLs typically are strings in
// Javascript, let's include this one here, too.
if (!String.prototype.getParamValue
String.prototype.getParamValue = function (param) {
var re = new RegExp ('[&?]' + param.escapeRE () + '=([^&#]*)');
var m = re.exec (this);
if (m && m.length >= 2) return decodeURIComponent (m[1]); return null;
};
}
if (!String.getParamValue) {
String.getParamValue = function (param, url)
{
if (typeof (url) == 'undefined' || url === null) url = document.___location.href;
try {
return url.getParamValue (param);
} catch (e) {
return null;
}
};
}
/** Function enhancements ************/
if (!Function.prototype.bind) {
// Return a function that calls the function with 'this' bound to 'thisObject'
Function.prototype.bind = function (thisObject) {
var f = this, obj = thisObject, slice = Array.prototype.slice, prefixedArgs = slice.call (arguments, 1);
return function () { return f.apply (obj, prefixedArgs.concat (slice.call (arguments))); };
};
}
/** Array enhancements (Javascript 1.6) ************/
Line 224 ⟶ 257:
};
}
if (!Array.select)
Array.select = Array.filter; // Synonym // Calls iterator on all elements of the array
Line 261 ⟶ 295:
};
}
if (!Array.forAll)
Array.forAll = Array.every; // Synonym // Returns true if predicate is true for at least one element of the array, false otherwise.
Line 279 ⟶ 314:
};
}
if (!Array.exists)
Array.exists = Array.some; // Synonym // Returns a new array built by applying mapper to all elements.
Line 322 ⟶ 358:
{
if (target === null) return -1;
if (typeof (target.
if (typeof (target.length) == 'undefined') return -1;
var l = target.length;
Line 338 ⟶ 374:
/** Additional Array enhancements ************/
if (!Array.remove) {
Array.remove = function (target, elem) { var i = Array.indexOf (target, elem);
if (i >= 0) target.splice (i, 1);
};
}
if (!Array.contains) {
Array.contains = function (target, elem) { return Array.indexOf (target, elem) >= 0;
};
}
if (!Array.flatten
Array.flatten = function (target) {
var result = [];
Array.forEach (target, function (elem) {result = result.concat (elem);}); return result;
};
}
// Calls selector on the array elements until it returns a non-null object
// and then returns that object. If selector always returns null, any also
// returns null. See also Array.map.
if (!Array.any) {
Array.any = function (target, selector, thisObject) {
if (target === null) return null;
if (typeof (selector) != 'function')
throw new Error ('Array.any: selector must be a function');
var l = target.length;
var result = null;
if (thisObject) selector = selector.bind (thisObject);
for (var i=0; l && i < l; i++) {
if (i in target) {
result = selector (target[i], i, target);
if (result != null) return result;
}
}
return null;
}
// Return a contiguous array of the contents of source, which may be an array or pseudo-array,
// basically anything that has a length and can be indexed. (E.g. live HTMLCollections, but also
// Strings, or objects, or the arguments "variable".
if (!Array.make)
Array.make = function (source)
{
if (!source || typeof (source.length) == 'undefined') return null;
for (var i=0; i < l; i++) {
if (i in source) result[result.length] = source[i]; }
return result;
};
}
if (typeof (window.LAPI) == 'undefined') {
Ajax :
{
Line 468 ⟶ 514:
if (name || msg) {
if (!asDOM) {
return (
'Exception ' + name + ': ' + msg
+ (file ? '\nFile ' + file + (line ? ' (' + line + ')' : "") : "")
);
} else {
var ex_msg = LAPI.make ('div');
Line 700 ⟶ 746:
parseHTML : function (str, sanity_check)
{
//
return LAPI.DOM.fakeHTMLDocument (str);
},
Line 886 ⟶ 919:
{ // Gecko etc.
if (property == 'cssFloat') property = 'float';
return element.ownerDocument.defaultView.getComputedStyle (element, null).getPropertyValue (property);
} else {
var result;
Line 1,037 ⟶ 1,069:
if (node.nodeName.toLowerCase () == tag) res[res.length] = node;
var curr = node.firstChild;
while (curr) { traverse (curr, tag); curr = curr.nextSibling; }
}
traverse (this.body, tag.toLowerCase ());
Line 1,098 ⟶ 1,130:
if (!file_hist) return result;
try {
var $file_curr =
// Did they change the column order here? It once was nextSibling.nextSibling... but somehow
// the thumbnails seem to be gone... Right:
// http://svn.wikimedia.org/viewvc/mediawiki/trunk/phase3/includes/ImagePage.php?r1=52385&r2=53130
file_hist = LAPI.DOM.getInnerText ($file_curr[0].nextSibling);
if (!file_hist.contains ('×')) {
file_hist = LAPI.DOM.getInnerText ($file_curr[0].nextSibling.nextSibling);
if (!file_hist.contains ('×')) file_hist = null;
}
Line 1,131 ⟶ 1,163:
if (!file_div) return null; // Catch page without file...
var imgs = file_div.getElementsByTagName ('img');
title = title || mw.config.get('wgTitle');
for (var i = 0; i < imgs.length; i++) {
var src = decodeURIComponent (imgs[i].getAttribute ('src', 2)).replace ('%26', '&');
if (src.search (new RegExp ('^' + LAPI_file_store + '.*/' + title.replace (/ /g, '_
return imgs[i];
}
Line 1,145 ⟶ 1,177:
var href = lk.getAttribute ('href', 2);
if (!href) return null;
// This is a bit tricky to get right, because wgScript can be a substring prefix of
// wgArticlePath, or vice versa.
var script = mw.config.get('wgScript') + '?';
if (href.startsWith (script) || href.startsWith (mw.config.get('wgServer') + script) || mw.config.get('wgServer').startsWith('//') && href.startsWith (document.___location.protocol + mw.config.get('wgServer') + script)) {
// href="/w/index.php?title=..."
return href.getParamValue ('title');
}
// Now try wgArticlePath: href="/wiki/..."
var prefix = mw.config.get('wgArticlePath').replace ('$1', "");
if (!href.startsWith (prefix)) prefix = mw.config.get('wgServer') + prefix; // Fully expanded URL?
if (!href.startsWith (prefix) && prefix.startsWith ('//')) prefix = document.___location.protocol + prefix; // Protocol-relative wgServer?
if (href.startsWith (prefix))
return decodeURIComponent (href.substring (prefix.length));
// Do we have variants?
var variants = mw.config.get('wgVariantArticlePath');
if (variants && variants.length > 0)
{
var re =
new RegExp (
var m = re.exec (href);
if (m && m.length > 1) return decodeURIComponent (m[m.length-1]);
}
// Finally alternative action paths
var actions = mw.config.get('wgActionPaths');
if (actions) {
for (var i=0; i < actions.length; i++) {
var p = actions[i];
if (p && p.length > 0) {
p = p.replace('$1', "");
if (!href.startsWith (p)) p = mw.config.get('wgServer') + p;
if (!href.startsWith (p) && p.startsWith('//')) p = document.___location.protocol + p;
if (href.startsWith (p))
return decodeURIComponent (href.substring (p.length));
}
}
}
return null;
},
revisionFromHtml : function (htmlOfPage)
{
var revision_id = null;
if (window.mediaWiki) { // MW 1.17+
revision_id = htmlOfPage.match (/(mediaWiki|mw).config.set\(\{.*"wgCurRevisionId"\s*:\s*(\d+),/);
if (revision_id) revision_id = parseInt (revision_id[2], 10);
} else { // MW < 1.17
revision_id = htmlOfPage.match (/wgCurRevisionId\s*=\s*(\d+)[;,]/);
if (revision_id) revision_id = parseInt (revision_id[1], 10);
}
return revision_id;
}
Line 1,201 ⟶ 1,266:
// may fail to find elements known to exist.
var doc = null;
// Always use our own parser instead of responseXML; that doesn't work right with HTML5. (It did work with XHTML, though.)
// if ( request.responseXML && request.responseXML.documentElement
// &&
// && (!sanity_check || request.responseXML.getElementById (sanity_check) != null)
//
// {
// doc = request.responseXML;
// } else {
try {
doc = LAPI.DOM.parseHTML (request.responseText, sanity_check);
Line 1,215 ⟶ 1,281:
doc = null;
}
// }
if (doc) {
try {
Line 1,222 ⟶ 1,288:
if (typeof (failureFunc) == 'function') failureFunc (request, ex);
doc = null;
}
}
if (doc === null) return doc;
// We've gotten XML. There is a subtle difference between XML and (X)HTML concerning leading newlines in textareas:
// XML is required to pass through any whitespace (http://www.w3.org/TR/2004/REC-xml-20040204/#sec-white-space), whereas
// HTML may or must not (e.g. http://www.w3.org/TR/html4/appendix/notes.html#h-B.3.1, though it is unclear whether that
// really applies to the content of a textarea, but the draft HTML 5 spec explicitly says that the first newline in a
// <textarea> is swallowed in HTML:
// http://www.whatwg.org/specs/web-apps/current-work/multipage/syntax.html#element-restrictions).
// Because of the latter MW1.18+ adds a newline after the <textarea> start tag if the value starts with a newline. That
// solves bug 12130 (leading newlines swallowed), but since XML passes us this extra newline, we might end up adding a
// leading newline upon each edit.
// Let's try to make sure that all textarea's values are as they should be in HTML.
// Note: since the above change to always use our own parser, which always returns a faked HTML document, this should be
// unnecessary since doc.isFake should always be true.
if (typeof (LAPI.Ajax.getHTML.extraNewlineRE) == 'undefined') {
// Feature detection. Compare value after parsing with value after .innerHTML.
LAPI.Ajax.getHTML.extraNewlineRE = null; // Don't know; hence do nothing
try {
var testTA = '<textarea id="test">\nTest</textarea>';
var testString = '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n'
+ '<html xmlns="http://www.w3.org/1999/xhtml" lang="en" dir="ltr">\n'
+ '<head><title>Test</title></head><body><form>' + testTA + '</form></body>\n'
+ '</html>';
var testDoc = LAPI.DOM.parseHTML (testString, 'test');
var testVal = "" + testDoc.getElementById ('test').value;
if (testDoc.dispose) testDoc.dispose();
var testDiv = LAPI.make ('div', null, {display: 'none'});
document.body.appendChild (testDiv);
testDiv.innerHTML = testTA;
if (testDiv.firstChild.value != testVal) {
LAPI.Ajax.getHTML.extraNewlineRE = /^\r?\n/;
if (testDiv.firstChild.value != testVal.replace(LAPI.Ajax.getHTML.extraNewlineRE, "")) {
// Huh? Not the expected difference: go back to "don't know" mode
LAPI.Ajax.getHTML.extraNewlineRE = null;
}
}
LAPI.DOM.removeNode (testDiv);
} catch (any) {
LAPI.Ajax.getHTML.extraNewlineRE = null;
}
}
if (!doc.isFake && LAPI.Ajax.getHTML.extraNewlineRE !== null) {
// If have a "fake" doc, then we did parse through .innerHTML anyway. No need to fix anything.
// (Hm. Maybe we should just always use a fake doc?)
var tas = doc.getElementsByTagName ('textarea');
for (var i = 0, l = tas.length; i < l; i++) {
tas[i].value = tas[i].value.replace(LAPI.Ajax.getHTML.extraNewlineRE, "");
}
}
Line 1,260 ⟶ 1,374:
}
var method;
if (uri.startsWith ('//')) uri = document.___location.protocol + uri; // Avoid protocol-relative URIs (IE7 bug)
if (uri.length + args.length + 1 < (LAPI.Browser.is_ie ? 2040 : 4080)) {
// Both browsers and web servers may have limits on URL length. IE has a limit of 2083 characters
Line 1,308 ⟶ 1,423:
LAPI.Ajax.getPage = function (page, action, params, success, failure)
{
var uri = mw.config.get('wgServer') + mw.config.get('wgScript') + '?title=' + encodeURIComponent (page)
+ (action ? '&action=' + action : "");
LAPI.Ajax.get (uri, params, success, failure, {overrideMimeType : 'application/xml'});
Line 1,339 ⟶ 1,454:
if (!the_form) throw new Error ('#Server reply does not contain mandatory form.');
}
revision_id =
} catch (ex) {
failureFunc (request, ex);
Line 1,422 ⟶ 1,536:
}
}
var uri = mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php' + (action ? '?action=' + action : "");
LAPI.Ajax.get (
uri, params
, function (request, failureFunc) {
if (is_json && request.responseText.trimLeft().charAt (0) != '{') {
failureFunc (request);
} else {
success (
request
, (is_json ? eval ('(' + request.responseText.trimLeft() + ')') : null)
, original_failure
);
Line 1,445 ⟶ 1,559:
if (!success || typeof (success) != 'function')
throw new Error ('No success function supplied for parseWikitext');
if (!wikitext && !on_page)
throw new Error ('No wikitext or page supplied for parseWikitext');
var params =
if (!wikitext) {
params = {pst: null, page: on_page};
} else {
params =
{ pst : null // Do the pre-save-transform: Pipe magic, tilde expansion,
,text
+ '\<div class="previewnote"\>'
+ '\{\{MediaWiki:Previewnote/' + (user_language || mw.config.get('wgUserLanguage')) +'\}\}'
+ '\<\/div>\<div\>\n'
: "")
+ wikitext
+ (as_preview ? '\<\/div\>\<div style="clear:both;"\>\<\/div\>\<\/div\>' : "")
,title: on_page || mw.config.get('wgPageName') || "API"
};
}
params.prop = 'text';
params.uselang = user_language || mw.config.get('wgUserLanguage'); // see bugzilla 22764
if (cache && /^\d+$/.test(cache=cache.toString())) {
params.maxage = cache;
Line 1,478 ⟶ 1,600:
}; // end LAPI.Ajax.parseWikitext
// Throbber backward-compatibility
LAPI.Ajax.injectSpinner = function (elementBefore, id) {}; // No-op, replaced as appropriate below.
LAPI.Ajax.removeSpinner = function (id) {}; // No-op, replaced as appropriate below.
if (typeof window.jQuery == 'undefined' || typeof window.mediaWiki == 'undefined' || typeof window.mediaWiki.loader == 'undefined') {
// Assume old-stlye
if (typeof window.injectSpinner != 'undefined') {
LAPI.Ajax.injectSpinner = window.injectSpinner;
}
if (typeof window.removeSpinner != 'undefined') {
LAPI.Ajax.removeSpinner = window.removeSpinner;
}
} else {
window.mediaWiki.loader.using('jquery.spinner', function () {
LAPI.Ajax.injectSpinner = function (elementBefore, id) {
window.jQuery(elementBefore).injectSpinner(id);
}
LAPI.Ajax.removeSpinner = function (id) {
window.jQuery.removeSpinner(id);
}
});
}
} // end if (guard)
Line 1,915 ⟶ 2,058:
}
, true
, mw.config.get('wgUserLanguage') || null
, mw.config.get('wgPageName') || null
);
return true;
Line 1,968 ⟶ 2,111:
} // end if (guard)
|