MediaWiki:LAPI.js
Hinweis: Leere nach dem Veröffentlichen den Browser-Cache, um die Änderungen sehen zu können.
- Firefox/Safari: Umschalttaste drücken und gleichzeitig Aktualisieren anklicken oder entweder Strg+F5 oder Strg+R (⌘+R auf dem Mac) drücken
- Google Chrome: Umschalttaste+Strg+R (⌘+Umschalttaste+R auf dem Mac) drücken
- Internet Explorer/Edge: Strg+F5 drücken oder Strg drücken und gleichzeitig Aktualisieren anklicken
- Opera: Strg+F5
// <source lang=javascript">
/*
Small JS library containing stuff I use often.
Author: [[User:Lupo]], June 2009
License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
Choose whichever license of these you like best :-)
Includes the following components:
- Object enhancements (clone, merge)
- String enhancements (trim, ...)
- Array enhancements (JS 1.6)
- Function enhancements (bind)
- LAPI Most basic DOM functions: $ (getElementById), make
- LAPI.Ajax Ajax request implementation, tailored for MediaWiki/WMF sites
- LAPI.Browser Browser detection (general)
- LAPI.DOM DOM helpers, including a cross-browser DOM parser
- LAPI.WP MediaWiki/WMF-specific DOM routines
- LAPI.Edit Simple editor implementation with save, cancel, preview (for WMF sites)
- LAPI.Evt Event handler routines (general)
- LAPI.Pos Position calculations (general)
*/
// Global: wgServer, wgScript, wgUserLanguage, injectSpinner, removeSpinner (from wiki.js)
// Global: importScript (from wikibits.js, for MediaWiki:AjaxSubmit.js)
// Configuration: set this to the URL of your image server. The value is a string representation
// of a regular expression. For instance, for Wikia, use "http://images\\d\\.wikia\\.nocookie\\.net".
// Remember to double-escape the backslash.
if ( typeof( LAPI_file_store ) == 'undefined' ) {
var LAPI_file_store = "(https?:)?//upload\\.wikimedia\\.org/";
}
// Some basic routines, mainly enhancements of the String, Array, and Function objects.
// Some taken from JavaScript 1.6, some own.
/** Object enhancements ************/
// Note: adding these to the prototype may break other code that assumes that
// {} has no properties at all.
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;
};
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;
};
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;
};
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) ************/
// Removes given characters from both ends of the string.
// If no characters are given, defaults to removing whitespace.
if ( !String.prototype.trim ) {
String.prototype.trim = function( chars ) {
if ( !chars ) {
return this.replace( /^\s+|\s+$/g, '' );
}
return this.trimRight( chars ).trimLeft( chars );
};
}
// Removes given characters from the beginning of the string.
// If no characters are given, defaults to removing whitespace.
if ( !String.prototype.trimLeft ) {
String.prototype.trimLeft = function( chars ) {
if ( !chars ) {
return this.replace( /^\s\s*/, '' );
}
return this.replace( new RegExp( '^[' + chars.escapeRE() + ']+' ), '' );
};
}
String.prototype.trimFront = String.prototype.trimLeft; // Synonym
// Removes given characters from the end of the string.
// If no characters are given, defaults to removing whitespace.
if ( !String.prototype.trimRight ) {
String.prototype.trimRight = function( chars ) {
if ( !chars ) {
return this.replace( /\s\s*$/, '' );
}
return this.replace( new RegExp( '[' + chars.escapeRE() + ']+$' ), '' );
};
}
String.prototype.trimEnd = String.prototype.trimRight; // Synonym
/** Further String enhancements ************/
// Returns true if the string begins with prefix.
String.prototype.startsWith = function( prefix ) {
return this.indexOf( prefix ) == 0;
};
// Returns true if the string ends in suffix
String.prototype.endsWith = function( suffix ) {
return this.lastIndexOf( suffix ) + suffix.length == this.length;
};
// Returns true if the string contains s.
String.prototype.contains = function( s ) {
return this.indexOf( s ) >= 0;
};
// Replace all occurrences of a string pattern by replacement.
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.
String.prototype.stringifyJS = function() {
return this.replace( /([\\\'\"]|%5C|%27|%22)/g, '\\$1' ) // ' // Fix syntax coloring
.replace( /\n/g, '\\n' );
}
// Escape all RegExp special characters such that the result can be safely used
// in a RegExp as a literal.
String.prototype.escapeRE = function() {
return this.replace( /([\\{}()|.?*+^$\[\]])/g, "\\$1" );
};
String.prototype.escapeXML = function( quot, apos ) {
var s = this.replace( /&/g, '&' )
.replace( /\xa0/g, ' ' )
.replace( /</g, '<' )
.replace( />/g, '>' );
if ( quot ) {
s = s.replace( /\"/g, '"' ); // " // Fix syntax coloring
}
if ( apos ) {
s = s.replace( /\'/g, ''' ); // ' // Fix syntax coloring
}
return s;
};
String.prototype.decodeXML = function() {
return this.replace( /"/g, '"' )
.replace( /'/g, "'" )
.replace( />/g, '>' )
.replace( /</g, '<' )
.replace( / /g, '\xa0' )
.replace( /&/g, '&' );
};
String.prototype.capitalizeFirst = function() {
return this.substring( 0, 1 ).toUpperCase() + this.substring( 1 );
};
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.
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;
};
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) ************/
// Note that contrary to JS 1.6, we treat the thisObject as optional.
// Don't add to the prototype, that would break for (var key in array) loops!
// Returns a new array containing only those elements for which predicate
// is true.
if ( !Array.filter ) {
Array.filter = function( target, predicate, thisObject ) {
if ( target === null ) {
return null;
}
if ( typeof( target.filter ) == 'function' ) {
return target.filter( predicate, thisObject );
}
if ( typeof( predicate ) != 'function' ) {
throw new Error( 'Array.filter: predicate must be a function' );
}
var l = target.length;
var result = [];
if ( thisObject ) {
predicate = predicate.bind( thisObject );
}
for ( var i = 0; l && i < l; i++ ) {
if ( i in target ) {
var curr = target[i];
if ( predicate( curr, i, target ) ) {
result[result.length] = curr;
}
}
}
return result;
};
}
Array.select = Array.filter; // Synonym
// Calls iterator on all elements of the array
if ( !Array.forEach ) {
Array.forEach = function( target, iterator, thisObject ) {
if ( target === null ) {
return;
}
if ( typeof( target.forEach ) == 'function' ) {
target.forEach( iterator, thisObject );
return;
}
if ( typeof( iterator ) != 'function' ) {
throw new Error( 'Array.forEach: iterator must be a function' );
}
var l = target.length;
if ( thisObject ) {
iterator = iterator.bind( thisObject );
}
for ( var i = 0; l && i < l; i++ ) {
if ( i in target ) {
iterator( target[i], i, target );
}
}
};
}
// Returns true if predicate is true for every element of the array, false otherwise
if ( !Array.every ) {
Array.every = function( target, predicate, thisObject ) {
if ( target === null ) {
return true;
}
if ( typeof( target.every ) == 'function' ) {
return target.every( predicate, thisObject );
}
if ( typeof( predicate ) != 'function' ) {
throw new Error( 'Array.every: predicate must be a function' );
}
var l = target.length;
if ( thisObject ) {
predicate = predicate.bind( thisObject );
}
for ( var i = 0; l && i < l; i++ ) {
if ( i in target && !predicate( target[i], i, target ) ) {
return false;
}
}
return true;
};
}
Array.forAll = Array.every; // Synonym
// Returns true if predicate is true for at least one element of the array, false otherwise.
if ( !Array.some ) {
Array.some = function( target, predicate, thisObject ) {
if ( target === null ) {
return false;
}
if ( typeof( target.some ) == 'function' ) {
return target.some( predicate, thisObject );
}
if ( typeof( predicate ) != 'function' ) {
throw new Error( 'Array.some: predicate must be a function' );
}
var l = target.length;
if ( thisObject ) {
predicate = predicate.bind( thisObject );
}
for ( var i = 0; l && i < l; i++ ) {
if ( i in target && predicate( target[i], i, target ) ) {
return true;
}
}
return false;
};
}
Array.exists = Array.some; // Synonym
// Returns a new array built by applying mapper to all elements.
if ( !Array.map ) {
Array.map = function( target, mapper, thisObject ) {
if ( target === null ) {
return null;
}
if ( typeof( target.map ) == 'function' ) {
return target.map( mapper, thisObject );
}
if ( typeof( mapper ) != 'function' ) {
throw new Error( 'Array.map: mapper must be a function' );
}
var l = target.length;
var result = [];
if ( thisObject ) {
mapper = mapper.bind( thisObject );
}
for ( var i = 0; l && i < l; i++ ) {
if ( i in target ) {
result[i] = mapper( target[i], i, target );
}
}
return result;
};
}
if ( !Array.indexOf ) {
Array.indexOf = function( target, elem, from ) {
if ( target === null ) {
return -1;
}
if ( typeof( target.indexOf ) == 'function' ) {
return target.indexOf( elem, from );
}
if ( typeof( target.length ) == 'undefined' ) {
return -1;
}
var l = target.length;
if ( isNaN( from ) ) {
from = 0;
} else {
from = from || 0;
}
from = ( from < 0 ) ? Math.ceil( from ) : Math.floor( from );
if ( from < 0 ) {
from += l;
}
if ( from < 0 ) {
from = 0;
}
while ( from < l ) {
if ( from in target && target[from] === elem ) {
return from;
}
from += 1;
}
return -1;
};
}
if ( !Array.lastIndexOf ) {
Array.lastIndexOf = function( target, elem, from ) {
if ( target === null ) {
return -1;
}
if ( typeof( target.lastIndexOf ) == 'function' ) {
return target.lastIndexOf( elem, from );
}
if ( typeof( target.length ) == 'undefined' ) {
return -1;
}
var l = target.length;
if ( isNaN( from ) ) {
from = l - 1;
} else {
from = from || ( l - 1 );
}
from = ( from < 0 ) ? Math.ceil( from ) : Math.floor( from );
if ( from < 0 ) {
from += l;
} else if ( from >= l ) {
from = l - 1;
}
while ( from >= 0 ) {
if ( from in target && target[from] === elem ) {
return from;
}
from -= 1;
}
return -1;
};
}
/** Additional Array enhancements ************/
Array.remove = function( target, elem ) {
var i = Array.indexOf( target, elem );
if ( i >= 0 ) {
target.splice( i, 1 );
}
};
Array.contains = function( target, elem ) {
return Array.indexOf( target, elem ) >= 0;
};
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.
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".
Array.make = function( source ) {
if ( !source || typeof( source.length ) == 'undefined' ) {
return null;
}
var result = [];
var l = source.length;
for ( var i = 0; i < l; i++ ) {
if ( i in source ) {
result[result.length] = source[i];
}
}
return result;
};
if ( typeof( window.LAPI) == 'undefined' ) {
window.LAPI = {
Ajax: {
getRequest: function() {
var request = null;
try {
request = new XMLHttpRequest();
} catch ( anything ) {
request = null;
if ( !!window.ActiveXObject ) {
if ( typeof( LAPI.Ajax.getRequest.msXMLHttpID ) == 'undefined' ) {
var XHR_ids = [
'MSXML2.XMLHTTP.6.0', 'MSXML2.XMLHTTP.3.0',
'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP'
];
for ( var i = 0; i < XHR_ids.length && !request; i++ ) {
try {
request = new ActiveXObject( XHR_ids[i] );
if ( request ) {
LAPI.Ajax.getRequest.msXMLHttpID = XHR_ids[i];
}
} catch ( ex ) {
request = null;
}
}
if ( !request ) {
LAPI.Ajax.getRequest.msXMLHttpID = null;
}
} else if ( LAPI.Ajax.getRequest.msXMLHttpID ) {
request = new ActiveXObject( LAPI.Ajax.getRequest.msXMLHttpID );
}
} // end if IE
} // end try-catch
return request;
}
},
$: function( selector, doc, multi ) {
if ( !selector || selector.length == 0 ) {
return null;
}
doc = doc || document;
if ( typeof( selector ) == 'string' ) {
if ( selector.charAt( 0 ) == '#' ) {
selector = selector.substring( 1 );
}
if ( selector.length > 0 ) {
return doc.getElementById( selector );
}
return null;
} else {
if ( multi ) {
return Array.map( selector, function( id ) { return LAPI.$( id, doc ); } );
}
return Array.any( selector, function( id ) { return LAPI.$( id, doc ); } );
}
},
make: function( tag, attribs, css, doc ) {
doc = doc || document;
if ( !tag || tag.length == 0 ) {
throw new Error ('No tag for LAPI.make');
}
var result = doc.createElement( tag );
Object.mergeSet( attribs, result );
Object.mergeSet( css, result.style );
if (
/^(form|input|button|select|textarea)$/.test( tag ) &&
result.id && result.id.length > 0 && !result.name
)
{
result.name = result.id;
}
return result;
},
formatException: function( ex, asDOM ) {
var name = ex.name || '';
var msg = ex.message || '';
var file = null;
var line = null;
if ( msg && msg.length > 0 && msg.charAt( 0 ) == '#' ) {
// User msg: don't confuse users with error locations. (Note: could also use
// custom exception types, but that doesn't work right on IE6.)
msg = msg.substring( 1 );
} else {
file = ex.fileName || ex.sourceURL || null; // Gecko, Webkit, others
line = ex.lineNumber || ex.line || null; // Gecko, Webkit, others
}
if ( name || msg ) {
if ( !asDOM ) {
return 'Exception ' + name + ': ' + msg +
( file ? '\nFile ' + file + ( line ? ' (' + line + ')' : '' ) : '' );
} else {
var ex_msg = LAPI.make( 'div' );
ex_msg.appendChild( document.createTextNode( 'Exception ' + name + ': ' + msg ) );
if ( file ) {
ex_msg.appendChild( LAPI.make( 'br' ) );
ex_msg.appendChild(
document.createTextNode(
'File ' + file + (line ? ' (' + line + ')' : '' )
)
);
}
return ex_msg;
}
} else {
return null;
}
}
};
} // end if (guard)
if ( typeof( LAPI.Browser ) == 'undefined' ) {
// Yes, usually it's better to test for available features. But sometimes there's no
// way around testing for specific browsers (differences in dimensions, layout errors,
// etc.)
LAPI.Browser = ( function( agent ) {
var result = {};
result.client = agent;
var m = agent.match( /applewebkit\/(\d+)/ );
result.is_webkit = ( m != null );
result.is_safari = result.is_webkit && !agent.contains( 'spoofer' );
result.webkit_version = ( m ? parseInt( m[1] ) : 0 );
result.is_khtml = navigator.vendor == 'KDE' ||
( document.childNodes && !document.all &&
!navigator.taintEnabled && navigator.accentColorName );
result.is_gecko = agent.contains( 'gecko' ) &&
!/khtml|spoofer|netscape\/7\.0/.test( agent );
result.is_ff_1 = agent.contains( 'firefox/1' );
result.is_ff_2 = agent.contains( 'firefox/2' );
result.is_ff_ge_2 = /firefox\/[2-9]|minefield\/3/.test( agent );
result.is_ie = agent.contains( 'msie' ) || !!window.ActiveXObject;
result.is_ie_lt_7 = false;
if ( result.is_ie ) {
var version = /msie ((\d|\.)+)/.exec( agent );
result.is_ie_lt_7 = ( version != null && ( parseFloat( version[1] ) < 7 ) );
}
result.is_opera = agent.contains( 'opera' );
result.is_opera_ge_9 = false;
result.is_opera_95 = false;
if ( result.is_opera ) {
m = /opera\/((\d|\.)+)/.exec( agent );
result.is_opera_95 = m && ( parseFloat( m[1] ) >= 9.5 );
result.is_opera_ge_9 = m && ( parseFloat( m[1] ) >= 9.0 );
}
result.is_mac = agent.contains( 'mac' );
return result;
})( navigator.userAgent.toLowerCase() );
} // end if (guard)
if ( typeof( LAPI.DOM ) == 'undefined' ) {
LAPI.DOM = {
// IE6 doesn't have these Node constants in Node, so put them here
ELEMENT_NODE : 1,
ATTRIBUTE_NODE : 2,
TEXT_NODE : 3,
CDATA_SECTION_NODE : 4,
ENTITY_REFERENCE_NODE : 5,
ENTITY_NODE : 6,
PROCESSING_INSTRUCTION_NODE : 7,
COMMENT_NODE : 8,
DOCUMENT_NODE : 9,
DOCUMENT_TYPE_NODE : 10,
DOCUMENT_FRAGMENT_NODE : 11,
NOTATION_NODE : 12,
cleanAttributeName: function( attr_name ) {
if ( !LAPI.Browser.is_ie ) {
return attr_name;
}
if ( !LAPI.DOM.cleanAttributeName._names ) {
LAPI.DOM.cleanAttributeName._names = {
'class': 'className',
'cellspacing': 'cellSpacing',
'cellpadding': 'cellPadding',
'colspan': 'colSpan',
'maxlength': 'maxLength',
'readonly': 'readOnly',
'rowspan': 'rowSpan',
'tabindex': 'tabIndex',
'valign': 'vAlign'
};
}
var cleaned = attr_name.toLowerCase();
return LAPI.DOM.cleanAttributeName._names[cleaned] || cleaned;
},
importNode: function( into, node, deep ) {
if ( !node ) {
return null;
}
if ( into.importNode ) {
return into.importNode( node, deep );
}
if ( node.ownerDocument == into ) {
return node.cloneNode( deep );
}
var new_node = null;
switch ( node.nodeType ) {
case LAPI.DOM.ELEMENT_NODE:
new_node = into.createElement( node.nodeName );
Array.forEach(
node.attributes,
function( attr ) {
if ( attr && attr.nodeValue && attr.nodeValue.length > 0 ) {
new_node.setAttribute(
LAPI.DOM.cleanAttributeName( attr.name ),
attr.nodeValue
);
}
}
);
new_node.style.cssText = node.style.cssText;
if ( deep ) {
Array.forEach(
node.childNodes,
function( child ) {
var copy = LAPI.DOM.importNode( into, child, true );
if ( copy ) {
new_node.appendChild( copy );
}
}
);
}
return new_node;
case LAPI.DOM.TEXT_NODE:
return into.createTextNode( node.nodeValue );
case LAPI.DOM.CDATA_SECTION_NODE:
return ( into.createCDATASection
? into.createCDATASection( node.nodeValue )
: into.createTextNode( node.nodeValue )
);
case LAPI.DOM.COMMENT_NODE:
return into.createComment( node.nodeValue );
default:
return null;
} // end switch
},
parse: function( str, content_type ) {
function getDocument( str, content_type ) {
if ( typeof( DOMParser ) != 'undefined' ) {
var parser = new DOMParser();
if ( parser && parser.parseFromString ) {
return parser.parseFromString( str, content_type );
}
}
// We don't have DOMParser
if ( LAPI.Browser.is_ie ) {
var doc = null;
// Apparently, these can be installed side-by-side. Try to get the newest one available.
// Unfortunately, one finds a variety of version strings on the net. I have no idea which
// ones are correct.
if ( typeof( LAPI.DOM.parse.msDOMDocumentID ) == 'undefined' ) {
// If we find a parser, we cache it.
// If we cannot find one, we also remember that.
var parsers = [
'MSXML6.DOMDocument', 'MSXML5.DOMDocument',
'MSXML4.DOMDocument', 'MSXML3.DOMDocument',
'MSXML2.DOMDocument.5.0', 'MSXML2.DOMDocument.4.0',
'MSXML2.DOMDocument.3.0', 'MSXML2.DOMDocument',
'MSXML.DomDocument', 'Microsoft.XmlDom'
];
for ( var i = 0; i < parsers.length && !doc; i++ ) {
try {
doc = new ActiveXObject( parsers[i] );
if ( doc ) {
LAPI.DOM.parse.msDOMDocumentID = parsers[i];
}
} catch ( ex ) {
doc = null;
}
}
if ( !doc ) {
LAPI.DOM.parse.msDOMDocumentID = null;
}
} else if ( LAPI.DOM.parse.msDOMDocumentID ) {
doc = new ActiveXObject( LAPI.DOM.parse.msDOMDocumentID );
}
if ( doc ) {
doc.async = false;
doc.loadXML( str );
return doc;
}
}
// Try using a "data" URI (http://www.ietf.org/rfc/rfc2397).
// Reported to work on older Safaris.
content_type = content_type || 'application/xml';
var req = LAPI.Ajax.getRequest();
if ( req ) {
// Synchronous is OK, since "data" URIs are local
req.open(
'GET',
'data:' + content_type + ';charset=utf-8,' +
encodeURIComponent( str ),
false
);
if ( req.overrideMimeType ) {
req.overrideMimeType( content_type );
}
req.send( null );
return req.responseXML;
}
return null;
} // end getDocument
var doc = null;
try {
doc = getDocument( str, content_type );
} catch ( ex ) {
doc = null;
}
if (
(
( !doc || !doc.documentElement ) &&
(
str.search( /^\s*(<xml[^>]*>\s*)?<!doctype\s+html/i ) >= 0 ||
str.search( /^\s*<html/i ) >= 0
)
)
||
( doc && ( LAPI.Browser.is_ie &&
( !doc.documentElement &&
doc.parseError && doc.parseError.errorCode != 0 &&
doc.parseError.reason.contains( 'Error processing resource' ) &&
doc.parseError.reason.contains( 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd' )
)
)
)
)
{
// Either the text specified an (X)HTML document, but we failed to get a Document, or we
// hit the walls of the single-origin policy on IE which tries to get the DTD from the
// URI specified... Let's fake a document:
doc = LAPI.DOM.fakeHTMLDocument( str );
}
return doc;
},
parseHTML: function( str, sanity_check ) {
// Simplified from the above, for cases where we *know* up front that the text is (X)HTML.
var doc = null;
if ( typeof( DOMParser ) != 'undefined' ) {
var parser = new DOMParser();
if ( parser && parser.parseFromString ) {
doc = parser.parseFromString( str, 'text/xml' );
}
}
if (
!doc || !doc.documentElement ||
/^parsererror$/i.test( doc.documentElement.tagName ) ||
( sanity_check && doc.getElementById( sanity_check ) == null )
)
{
// We had an error, or the sanity check (looking for an element known to be there) failed.
// (Happens on Konqueror 4.2.3/4.2.4 upon the very first call...)
doc = LAPI.DOM.fakeHTMLDocument( str );
}
return doc;
},
fakeHTMLDocument: function( str ) {
var body_tag = /<body.*?>/.exec( str );
if ( !body_tag || body_tag.length == 0 ) {
return null;
}
body_tag = body_tag.index + body_tag[0].length; // Index after the opening body tag
var body_end = str.lastIndexOf( '</body>' );
if ( body_end < 0 ) {
return null;
}
var content = str.substring( body_tag, body_end ); // Anything in between
content = content.replace( /<script(.|\s)*?\/script>/g, '' ); // Sanitize: strip scripts
return new LAPI.DOM.DocumentFacade( content );
},
isValid: function( doc ) {
if ( !doc ) {
return doc;
}
if ( typeof( doc.parseError ) != 'undefined' ) { // IE
if ( doc.parseError.errorCode != 0 ) {
throw new Error(
'XML parse error: ' + doc.parseError.reason +
' line ' + doc.parseError.line +
' col ' + doc.parseError.linepos +
'\nsrc = ' + doc.parseError.srcText
);
}
} else {
// FF... others?
var root = doc.documentElement;
if ( /^parsererror$/i.test( root.tagName ) ) {
throw new Error( 'XML parse error: ' + root.getInnerText() );
}
}
return doc;
},
hasClass: function( node, className ) {
if ( !node ) {
return false;
}
return ( ' ' + node.className + ' ' ).contains( ' ' + className + ' ' );
},
setContent: function( node, content ) {
if ( content == null ) {
return node;
}
LAPI.DOM.removeChildren( node );
if ( content.nodeName ) { // presumably a DOM tree, like a span or a document fragment
node.appendChild( content );
} else if ( typeof( node.innerHTML ) != 'undefined' ) {
node.innerHTML = content.toString();
} else {
node.appendChild( document.createTextNode( content.toString() ) );
}
return node;
},
makeImage: function( src, width, height, title, doc ) {
return LAPI.make(
'img',
{
src: src,
width: '' + width,
height: '' + height,
title: title
},
doc
);
},
makeButton: function( id, text, f, submit, doc ) {
return LAPI.make(
'input',
{
id: id || '',
type: ( submit ? 'submit' : 'button' ),
value: text,
onclick: f
},
doc
);
},
makeLabel: function( id, text, for_elem, doc ) {
var label = LAPI.make( 'label', {id: id || '', htmlFor: for_elem}, null, doc );
return LAPI.DOM.setContent( label, text );
},
makeLink: function( url, text, tooltip, onclick, doc ) {
var lk = LAPI.make( 'a', {href: url, title: tooltip, onclick: onclick}, null, doc );
return LAPI.DOM.setContent( lk, text || url );
},
// Unfortunately, extending Node.prototype may not work on some browsers,
// most notably (you've guessed it) IE...
getInnerText: function( node ) {
if ( node.textContent ) {
return node.textContent;
}
if ( node.innerText ) {
return node.innerText;
}
var result = '';
if ( node.nodeType == LAPI.DOM.TEXT_NODE ) {
result = node.nodeValue;
} else {
Array.forEach(
node.childNodes,
function( elem ) {
switch ( elem.nodeType ) {
case LAPI.DOM.ELEMENT_NODE:
result += LAPI.DOM.getInnerText( elem );
break;
case LAPI.DOM.TEXT_NODE:
result += elem.nodeValue;
break;
}
}
);
}
return result;
},
removeNode: function( node ) {
if ( node.parentNode ) {
node.parentNode.removeChild( node );
}
return node;
},
removeChildren: function( node ) {
// if ( typeof( node.innerHTML ) != 'undefined' ) node.innerHTML = '';
// Not a good idea. On IE this destroys all contained nodes, even if they're still referenced
// from JavaScript! Can't have that...
while ( node.firstChild ) node.removeChild( node.firstChild );
return node;
},
insertNode: function( node, before ) {
before.parentNode.insertBefore( node, before );
return node;
},
insertAfter: function( node, after ) {
var next = after.nextSibling;
after.parentNode.insertBefore( node, next );
return node;
},
replaceNode: function( node, newNode ) {
node.parentNode.replaceChild( node, newNode );
return newNode;
},
isParentOf: function( parent, child ) {
while ( child && child != parent && child.parentNode ) child = child.parentNode;
return child == parent;
},
// Property is to be in CSS style, e.g. 'background-color', not in JS style ('backgroundColor')!
// Use standard 'cssFloat' for float property.
currentStyle: function( element, property ) {
function normalize( prop ) {
// Don't use a regexp with a lambda function(available only in JS 1.3)... and I once had a
// case where IE6 goofed grossly with a lambda function. Since then I try to avoid those
// (though they're neat).
if ( prop == 'cssFloat' ) {
return 'styleFloat'; // We'll try both variants below, standard first...
}
var result = prop.split( '-' );
result = Array.map( result, function( s ) {
if ( s ) {
return s.capitalizeFirst();
} else {
return s;
}
} );
result = result.join( '' );
return result.lowercaseFirst();
}
if (
element.ownerDocument.defaultView &&
element.ownerDocument.defaultView.getComputedStyle
)
{ // Gecko etc.
if ( property == 'cssFloat' ) {
property = 'float';
}
return
element.ownerDocument.defaultView.getComputedStyle( element, null ).getPropertyValue( property );
} else {
var result;
if ( element.currentStyle ) { // IE, has subtle differences to getComputedStyle
result = element.currentStyle[property] || element.currentStyle[normalize( property )];
} else { // Not exactly right, but best effort
result = element.style[property] || element.style[normalize( property )];
}
// Convert em etc. to pixels. Kudos to Dean Edwards; see
// http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291
if ( !/^\d+(px)?$/i.test( result ) && /^\d/.test( result ) && element.runtimeStyle ) {
var style = element.style.left;
var runtimeStyle = element.runtimeStyle.left;
element.runtimeStyle.left = element.currentStyle.left;
element.style.left = result || 0;
result = elem.style.pixelLeft + 'px';
element.style.left = style;
element.runtimeStyle.left = runtimeStyle;
}
}
},
// Load a given image in a given size. Parameters:
// title
// Full title of the image, including the "File:" namespace
// url
// If != null, URL of an existing thumb for that image. If width is null, may contain the url
// of the full image.
// width
// If != null, desired width of the image, otherwise load the full image
// height
// If width != null, height should also be set.
// auto_thumbs
// True if missing thumbnails are generated automatically.
// success
// Function to be called once the image is loaded. Takes one parameter: the IMG-tag of
// the loaded image
// failure
// Function to be called if the image cannot be loaded. Takes one parameter: a string
// containing an error message.
loadImage: function( title, url, width, height, auto_thumbs, success, failure ) {
if ( auto_thumbs && url ) {
// MediaWiki-style with 404 handler. Set condition to false if your wiki does not have such a
// setup.
var img_src = null;
if ( width ) {
var i = url.lastIndexOf( '/' );
if ( i >= 0 ) {
img_src = url.substring( 0, i ) +
url.substring( i ).replace( /^\/\d+px-/, '/' + width + 'px-' );
}
} else if ( url ) {
img_src = url;
}
if ( !img_src ) {
failure( 'Cannot load image from url ' + url );
return;
}
var img_loader = LAPI.make(
'img',
{src: img_src},
{
position: 'absolute',
top: '0px',
left: '0px',
display: 'none'
}
);
if ( width ) {
img_loader.width = '' + width;
}
if ( height ) {
img_loader.height = '' + height;
}
LAPI.Evt.attach( img_loader, 'load', function() { success( img_loader ); } );
document.body.appendChild( img_loader ); // Now the browser goes loading the image
} else {
// No URL to work with. Use parseWikitext to have a thumb generated an to get its URL.
LAPI.Ajax.parseWikitext(
'[[' + title + ( width ? '|' + width + 'px' : '' ) + ']]',
function( html, failureFunc ) {
var dummy = LAPI.make(
'div',
null,
{
position: 'absolute',
top: '0px',
left: '0px',
display: 'none'
}
);
document.body.appendChild( dummy ); // Now start loading the image
dummy.innerHTML = html;
var imgs = dummy.getElementsByTagName( 'img' );
LAPI.Evt.attach(
imgs[0], 'load',
function() {
success( imgs[0] );
LAPI.DOM.removeNode( dummy );
}
);
},
function( request, json_result ) {
failure( 'Image loading failed: ' + request.status + ' ' + request.statusText );
},
false, // Not as preview
null, // user language: don't care
null, // on page: don't care
3600 // Cache for an hour
);
}
}
}; // end LAPI.DOM
LAPI.DOM.DocumentFacade = function() { this.initialize.apply( this, arguments ); };
LAPI.DOM.DocumentFacade.prototype = {
initialize: function( text ) {
// It's not a real document, but it will behave like one for our purposes.
this.documentElement = LAPI.make( 'div', null, {display: 'none', position: 'absolute'} );
this.body = LAPI.make( 'div', null, {position: 'relative'} );
this.documentElement.appendChild( this.body );
document.body.appendChild( this.documentElement );
this.body.innerHTML = text;
// Find all forms
var forms = document.getElementsByTagName( 'form' );
var self = this;
this.forms = Array.select( forms, function( f ) { return LAPI.DOM.isParentOf( self.body, f ); } );
// Konqueror 4.2.3/4.2.4 clears form.elements when the containing div is removed from the
// parent document?!
if ( !LAPI.Browser.is_khtml ) {
LAPI.DOM.removeNode( this.documentElement );
} else {
this.dispose = function() { LAPI.DOM.removeNode( this.documentElement ); };
// Since we must leave the stuff *in* the original document on Konqueror, we'll also need a
// dispose routine... what an ugly hack.
}
this.allIDs = {};
this.isFake = true;
},
createElement: function( tag ) { return document.createElement( tag ); },
createDocumentFragment: function() { return document.createDocumentFragment(); },
createTextNode: function( text ) { return document.createTextNode( text ); },
createComment: function( text ) { return document.createComment( text ); },
createCDATASection: function( text ) { return document.createCDATASection( text ); },
createAttribute: function( name ) { return document.createAttribute( name ); },
createEntityReference: function( name) { return document.createEntityReference( name ); },
createProcessingInstruction: function( target, data ) { return document.createProcessingInstruction( target, data ); },
getElementsByTagName: function( tag ) {
// Grossly inefficient, but deprecated anyway
var res = [];
function traverse( node, tag ) {
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() );
return res;
},
getElementById: function( id ) {
function traverse( elem, id ) {
if ( elem.id == id ) {
return elem;
}
var res = null;
var curr = elem.firstChild;
while ( curr && !res ) {
res = traverse( curr, id );
curr = curr.nextSibling;
}
return res;
}
if ( !this.allIDs[id] ) {
this.allIDs[id] = traverse( this.body, id );
}
return this.allIDs[id];
}
// ...NS operations omitted
}; // end DocumentFacade
if ( document.importNode ) {
LAPI.DOM.DocumentFacade.prototype.importNode =
function( node, deep ) { document.importNode( node, deep ); };
}
} // end if (guard)
if ( typeof( LAPI.WP ) == 'undefined' ) {
LAPI.WP = {
getContentDiv: function( doc ) {
// Monobook, modern, classic skins
return LAPI.$( ['bodyContent', 'mw_contentholder', 'article'], doc );
},
fullImageSizeFromPage: function( doc ) {
// Get the full img size. This is screenscraping :-( but there are times where you don't
// want to get this info from the server using an Ajax call.
// Note: we get the size from the file history table because the text just below the image
// is all scrambled on RTL wikis. For instance, on ar-WP, it is
// " (1,806 × 1,341 بكسل، حجم الملف: 996 كيلوبايت، نوع الملف: image/jpeg) and with uselang=en,
// it is at ar-WP " (1,806 × 1,341 pixels, file size: 996 KB, MIME type: image/jpeg)"
// However, in the file history table, it looks good no matter the language and writing
// direction.
// Update: this fails on e.g. ar-WP because someone had the great idea to use localized
// numerals, but the digit transform table is empty!
var result = {width: 0, height: 0};
var file_hist = LAPI.$( 'mw-imagepage-section-filehistory', doc );
if ( !file_hist ) {
return result;
}
try {
var file_curr = getElementsByClassName( file_hist, 'td', 'filehistory-selected' );
// 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;
}
}
} catch ( ex ) {
return result;
}
// Now we have "number×number" followed by something arbitrary
if ( file_hist ) {
file_hist = file_hist.split( '×', 2 );
result.width = parseInt( file_hist.shift().replace( /[^0-9]/g, '' ), 10 );
// Height is a bit more difficult because e.g. uselang=eo uses a space as the thousands
// separator. Hence we have to extract this more carefully
file_hist = file_hist.pop(); // Everything after the "×"
// Remove any white space embedded between digits
file_hist = file_hist.replace( /(\d)\s*(\d)/g, '$1$2' );
file_hist = file_hist.split( ' ', 2 ).shift().replace( /[^0-9]/g, '' );
result.height = parseInt( file_hist, 10 );
if ( isNaN( result.width ) || isNaN( result.height ) ) {
result = {width: 0, height: 0};
}
}
return result;
},
getPreviewImage: function( title, doc ) {
var file_div = LAPI.$( 'file', doc );
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, '_' ).replace( /(\.svg)$/i, '$1.png' ).escapeRE() + '$' ) ) == 0 ) {
return imgs[i];
}
}
return null;
},
pageFromLink: function( lk ) {
if ( !lk ) {
return null;
}
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?
if (
typeof( wgVariantArticlePath ) != 'undefined' &&
wgVariantArticlePath && wgVariantArticlePath.length > 0
)
{
var re = new RegExp( wgVariantArticlePath.escapeRE().replace( '\\$2', "[^\\/]*" ).replace( '\\$1', "(.*)" ) );
var m = re.exec( href );
if ( m && m.length > 1 ) {
return decodeURIComponent( m[m.length - 1] );
}
}
// Finally alternative action paths
if ( typeof( wgActionPaths ) != 'undefined' && wgActionPaths ) {
for ( var i = 0; i < wgActionPaths.length; i++ ) {
var p = wgActionPaths[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;
}
}; // end LAPI.WP
} // end if (guard)
if ( typeof( LAPI.Ajax.doAction ) == 'undefined' ) {
mw.loader.load( '/w/index.php?title=MediaWiki:AjaxSubmit.js&action=raw&ctype=text/javascript' ); // Legacy code: ajaxSubmit
LAPI.Ajax.getXML = function( request, failureFunc ) {
var doc = null;
if ( request.responseXML && request.responseXML.documentElement ) {
doc = request.responseXML;
} else {
try {
doc = LAPI.DOM.parse( request.responseText, 'text/xml' );
} catch ( ex ) {
if ( typeof( failureFunc ) == 'function' ) {
failureFunc( request, ex );
}
doc = null;
}
}
if ( doc ) {
try {
doc = LAPI.DOM.isValid( doc );
} catch ( ex ) {
if ( typeof( failureFunc ) == 'function' ) {
failureFunc( request, ex );
}
doc = null;
}
}
return doc;
};
LAPI.Ajax.getHTML = function( request, failureFunc, sanity_check ) {
// Konqueror sometimes has severe problems with responseXML.
// It does set it, but getElementById may fail to find elements known to exist.
var doc = null;
if (
request.responseXML && request.responseXML.documentElement &&
request.responseXML.documentElement.tagName == 'HTML' &&
( !sanity_check || request.responseXML.getElementById( sanity_check ) != null )
)
{
doc = request.responseXML;
} else {
try {
doc = LAPI.DOM.parseHTML( request.responseText, sanity_check );
if ( !doc ) {
throw new Error( '#Could not understand request result' );
}
} catch ( ex ) {
if ( typeof( failureFunc ) == 'function' ) {
failureFunc( request, ex );
}
doc = null;
}
}
if ( doc ) {
try {
doc = LAPI.DOM.isValid( doc );
} catch ( ex ) {
if ( typeof( failureFunc ) == 'function' ) {
failureFunc( request, ex );
}
doc = null;
}
}
return doc;
};
LAPI.Ajax.get = function( uri, params, success, failure, config ) {
var original_failure = failure;
if ( !failure || typeof( failure ) != 'function' ) {
failure = function() {};
}
if ( !success || typeof( success ) != 'function' ) {
throw new Error(
'No success function supplied for LAPI.Ajax.get ' + uri +
' with arguments ' + params.toString()
);
}
var request = LAPI.Ajax.getRequest();
if ( !request ) {
failure( request );
return;
}
var args = '';
var question_mark = uri.indexOf( '?' );
if ( question_mark ) {
args = uri.substring( question_mark + 1 );
uri = uri.substring( 0, question_mark );
}
if ( params != null ) {
if ( typeof( params ) == 'string' && params.length > 0 ) {
args += ( args.length > 0 ? '&' : '' ) +
( ( params.charAt( 0 ) == '&' || params.charAt( 0 ) == '?' )
? params.substring( 1 )
: params
); // Must already be encoded!
} else {
for ( var param in params ) {
args += ( args.length > 0 ? '&' : '' ) + param;
if ( params[param] != null ) {
args += '=' + encodeURIComponent( params[param] );
}
}
}
}
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
// (2048 in the path part), and the WMF servers seem to impose a limit of 4kB.
method = 'GET';
uri += '?' + args;
args = null;
} else {
method = 'POST'; // We'll lose caching, but at least we can make the request.
}
request.open( method, uri, true );
request.setRequestHeader( 'Pragma', 'cache=yes' );
request.setRequestHeader(
'Cache-Control',
'no-transform'
+ ( params && params.maxage ? ', max-age=' + params.maxage : '' )
+ ( params && params.smaxage ? ', s-maxage=' + params.smaxage : '' )
);
if ( config ) {
for ( var conf in config ) {
if ( conf == 'overrideMimeType' ) {
if ( config[conf] && config[conf].length > 0 && request.overrideMimeType ) {
request.overrideMimeType( config[conf] );
}
} else {
request.setRequestHeader( conf, config[conf] );
}
}
}
if ( args ) {
request.setRequestHeader( 'Content-Type', 'application/x-www-form-urlencoded' );
}
request.onreadystatechange = function() {
if ( request.readyState != 4 ) {
return; // Wait until the request has completed.
}
try {
if ( request.status != 200 ) {
throw new Error(
'#Request to server failed. Status: ' +
request.status + ' ' + request.statusText + ' URI: ' + uri
);
}
if ( !request.responseText ) {
throw new Error( '#Empty response from server for request ' + uri );
}
} catch ( ex ) {
failure( request, ex );
return;
}
success( request, original_failure );
};
request.send( args );
};
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'} );
};
// modify is supposed to save the changes at the end, e.g. using LAPI.Ajax.submit.
// modify is called with three parameters: the document, possibly the form, and the optional
// failure function. The failure function is called with the request as the first parameter,
// and possibly an exception as the second parameter.
LAPI.Ajax.doAction = function( page, action, form, modify, failure ) {
if ( !page || !action || !modify || typeof( modify ) != 'function' ) {
throw new Error( 'Parameter inconsistency in LAPI.Ajax.doAction.' );
}
var original_failure = failure;
if ( !failure || typeof( failure ) != 'function') {
failure = function() {};
}
LAPI.Ajax.getPage(
page, action, null, // No additional parameters
function( request, failureFunc ) {
var doc = null;
var the_form = null;
var revision_id = null;
try {
// Convert responseText into DOM tree.
doc = LAPI.Ajax.getHTML( request, failureFunc, form );
if ( !doc ) {
return;
}
var err_msg = LAPI.$ ('permissions-errors', doc );
if ( err_msg ) {
throw new Error( '#' + LAPI.DOM.getInnerText( err_msg ) );
}
if ( form ) {
the_form = LAPI.$( form, doc );
if ( !the_form ) {
throw new Error( '#Server reply does not contain mandatory form.' );
}
}
revision_id = LAPI.WP.revisionFromHtml( request.responseText );
} catch ( ex ) {
failureFunc( request, ex );
return;
}
modify( doc, the_form, original_failure, revision_id )
},
failure
);
}; // end LAPI.Ajax.doAction
LAPI.Ajax.submit = function( form, after_submit ) {
try {
ajaxSubmit( form, null, after_submit, true ); // Legacy code from MediaWiki:AjaxSubmit
} catch ( ex ) {
after_submit( null, ex );
}
}; // end LAPI.Ajax.submit
LAPI.Ajax.editPage = function( page, modify, failure ) {
LAPI.Ajax.doAction( page, 'edit', 'editform', modify, failure );
}; // end LAPI.Ajax.editPage
LAPI.Ajax.checkEdit = function( request ) {
if (!request) return true;
// Check for previews (session token lost?) or edit forms (edit conflict).
try {
var doc = LAPI.Ajax.getHTML( request, function() { throw new Error( 'Cannot check HTML' ); } );
if ( !doc ) {
return false;
}
return LAPI.$( ['wikiPreview', 'editform'], doc ) == null;
} catch ( anything ) {
return false;
}
}; // end LAPI.Ajax.checkEdit
LAPI.Ajax.submitEdit = function( form, success, failure ) {
if ( !success || typeof( success ) != 'function' ) {
success = function() {};
}
if ( !failure || typeof( failure ) != 'function' ) {
failure = function() {};
}
LAPI.Ajax.submit(
form,
function( request, ex ) {
if ( ex ) {
failure( request, ex );
} else {
var successful = false;
try {
successful = request && request.status == 200 && LAPI.Ajax.checkEdit( request );
} catch ( some_error ) {
failure( request, some_error );
return;
}
if ( successful ) {
success( request );
} else {
failure( request );
}
}
}
);
}; // end LAPI.Ajax.submitEdit
LAPI.Ajax.apiGet = function( action, params, success, failure ) {
var original_failure = failure;
if ( !failure || typeof( failure ) != 'function' ) {
failure = function() {};
}
if ( !success || typeof( success ) != 'function' ) {
throw new Error(
'No success function supplied for LAPI.Ajax.apiGet ' + action +
' with arguments ' + params.toString()
);
}
var is_json = false;
if ( params != null) {
if ( typeof( params ) == 'string') {
if ( !/format=[^&]+/.test( params ) ) {
params += '&format=json';
}
is_json = /format=json(&|$)/.test( params ); // Exclude jsonfm, which actually serves XHTML
} else {
if ( typeof( params['format'] ) != 'string' || params.format.length == 0 ) {
params.format = 'json';
}
is_json = params.format == 'json';
}
}
var uri = mw.config.get('wgServer') + 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
);
}
},
failure
);
}; // end LAPI.Ajax.apiGet
LAPI.Ajax.parseWikitext = function( wikitext, success, failure, as_preview, user_language, on_page, cache ) {
if ( !failure || typeof( failure ) != 'function' ) {
failure = function() {};
}
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 = null;
if ( !wikitext ) {
params = {pst: null, page: on_page};
} else {
params = {
pst: null, // Do the pre-save-transform: Pipe magic, tilde expansion, etc.
text: ( as_preview ? '\<div style="border:1px solid red; padding:0.5em;"\>'
+ '\<div class="previewnote"\>'
+ '\{\{MediaWiki:Previewnote/' + ( user_language || 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 || wgUserLanguage; // see bugzilla 22764
if ( cache && /^\d+$/.test( cache = cache.toString() ) ) {
params.maxage = cache;
params.smaxage = cache;
}
LAPI.Ajax.apiGet(
'parse',
params,
function( req, json_result, failureFunc ) {
// Success.
if ( !json_result || !json_result.parse || !json_result.parse.text ) {
failureFunc( req, json_result );
return;
}
success( json_result.parse.text['*'], failureFunc );
},
failure
);
}; // end LAPI.Ajax.parseWikitext
LAPI.Ajax.injectSpinner = injectSpinner;
LAPI.Ajax.removeSpinner = removeSpinner;
} // end if (guard)
if ( typeof( LAPI.Pos ) == 'undefined' ) {
LAPI.Pos = {
// Returns the global coordinates of the mouse pointer within the document.
mousePosition: function( evt ) {
if ( !evt || ( typeof( evt.pageX ) == 'undefined' && typeof( evt.clientX ) == 'undefined' ) ) {
// No way to calculate a mouse pointer position
return null;
}
if ( typeof( evt.pageX ) != 'undefined' ) {
return { x: evt.pageX, y: evt.pageY };
}
var offset = LAPI.Pos.scrollOffset();
var mouse_delta = LAPI.Pos.mouse_offset();
var coor_x = evt.clientX + offset.x - mouse_delta.x;
var coor_y = evt.clientY + offset.y - mouse_delta.y;
return { x: coor_x, y: coor_y };
},
// Operations on document level:
// Returns the scroll offset of the whole document (in other words, the coordinates
// of the top left corner of the viewport).
scrollOffset: function() {
return {x: LAPI.Pos.getScroll( 'Left' ), y: LAPI.Pos.getScroll( 'Top' ) };
},
getScroll: function( what ) {
var s = 'scroll' + what;
return ( document.documentElement ? document.documentElement[s] : 0 )
|| document.body[s] || 0;
},
// Returns the size of the viewport (result.x is the width, result.y the height).
viewport: function() {
return {x: LAPI.Pos.getViewport( 'Width' ), y: LAPI.Pos.getViewport( 'Height' ) };
},
getViewport: function( what ) {
if (
LAPI.Browser.is_opera_95 && what == 'Height' ||
LAPI.Browser.is_safari && !document.evaluate
)
{
return window['inner' + what];
}
var s = 'client' + what;
if ( LAPI.Browser.is_opera ) {
return document.body[s];
}
return ( document.documentElement ? document.documentElement[s] : 0 )
|| document.body[s] || 0;
},
// Operations on DOM nodes
position: ( function() {
// The following is the jQuery.offset implementation. We cannot use jQuery yet in globally
// activated scripts (it has strange side effects for Opera 8 users who can't log in anymore,
// and it breaks the search box for some users). Note that jQuery does not support Opera 8.
// Until the WMF servers serve jQuery by default, this copy from the jQuery 1.3.2 sources is
// needed here. If and when we have jQuery available officially, the whole thing here can be
// replaced by "var tmp = jQuery (node).offset(); return {x:tmp.left, y:tmp.top};"
// Kudos to the jQuery development team. Any errors in this adaptation are my own. (Lupo,
// 2009-08-24).
var data = null;
function jQuery_init() {
data = {};
// Capability check from jQuery.
var body = document.body;
var container = document.createElement( 'div' );
var html =
'<div style="position:absolute;top:0;left:0;margin:0;border:5px solid #000;'
+ 'padding:0;width:1px;height:1px;"><div></div></div><table style="position:absolute;'
+ 'top:0;left:0;margin:0;border:5px solid #000;padding:0;width:1px;height:1px;" '
+ 'cellpadding="0" cellspacing="0"><tr><td></td></tr></table>';
var rules = {
position: 'absolute',
visibility: 'hidden',
top: 0,
left: 0,
margin: 0,
border: 0,
width: '1px',
height: '1px'
};
Object.merge( rules, container.style );
container.innerHTML = html;
body.insertBefore( container, body.firstChild );
var innerDiv = container.firstChild;
var checkDiv = innerDiv.firstChild;
var td = innerDiv.nextSibling.firstChild.firstChild;
data.doesNotAddBorder = ( checkDiv.offsetTop !== 5 );
data.doesAddBorderForTableAndCells = ( td.offsetTop === 5 );
innerDiv.style.overflow = 'hidden', innerDiv.style.position = 'relative';
data.subtractsBorderForOverflowNotVisible = ( checkDiv.offsetTop === -5 );
var bodyMarginTop = body.style.marginTop;
body.style.marginTop = '1px';
data.doesNotIncludeMarginInBodyOffset = ( body.offsetTop === 0 );
body.style.marginTop = bodyMarginTop;
body.removeChild( container );
};
function jQuery_offset( node ) {
if ( node === node.ownerDocument.body ) {
return jQuery_bodyOffset( node );
}
if ( node.getBoundingClientRect ) {
var box = node.getBoundingClientRect();
var scroll = LAPI.Pos.scrollOffset();
return {x: ( box.left + scroll.x ), y: ( box.top + scroll.y )};
}
if ( !data ) {
jQuery_init();
}
var elem = node;
var offsetParent = elem.offsetParent;
var prevOffsetParent = elem;
var doc = elem.ownerDocument;
var prevComputedStyle = doc.defaultView.getComputedStyle( elem, null );
var computedStyle;
var top = elem.offsetTop;
var left = elem.offsetLeft;
while ( ( elem = elem.parentNode ) && elem !== doc.body && elem !== doc.documentElement ) {
computedStyle = doc.defaultView.getComputedStyle( elem, null );
top -= elem.scrollTop, left -= elem.scrollLeft;
if ( elem === offsetParent ) {
top += elem.offsetTop, left += elem.offsetLeft;
if (
data.doesNotAddBorder &&
!( data.doesAddBorderForTableAndCells && /^t(able|d|h)$/i.test( elem.tagName ) )
)
{
top += parseInt( computedStyle.borderTopWidth, 10 ) || 0;
left += parseInt( computedStyle.borderLeftWidth, 10 ) || 0;
}
prevOffsetParent = offsetParent;
offsetParent = elem.offsetParent;
}
if ( data.subtractsBorderForOverflowNotVisible && computedStyle.overflow !== 'visible' ) {
top += parseInt( computedStyle.borderTopWidth, 10 ) || 0;
left += parseInt( computedStyle.borderLeftWidth, 10 ) || 0;
}
prevComputedStyle = computedStyle;
}
if ( prevComputedStyle.position === 'relative' || prevComputedStyle.position === 'static' ) {
top += doc.body.offsetTop;
left += doc.body.offsetLeft;
}
if ( prevComputedStyle.position === 'fixed' ) {
top += Math.max( doc.documentElement.scrollTop, doc.body.scrollTop );
left += Math.max( doc.documentElement.scrollLeft, doc.body.scrollLeft );
}
return {x: left, y: top};
}
function jQuery_bodyOffset( body ) {
if ( !data ) {
jQuery_init();
}
var top = body.offsetTop, left = body.offsetLeft;
if ( data.doesNotIncludeMarginInBodyOffset ) {
top += parseInt( LAPI.DOM.currentStyle( body, 'margin-top' ), 10 ) || 0;
left += parseInt( LAPI.DOM.currentStyle( body, 'margin-left' ), 10 ) || 0;
}
return {x: left, y: top};
}
return jQuery_offset;
})(),
isWithin: function( node, x, y ) {
if ( !node || !node.parentNode ) {
return false;
}
var pos = LAPI.Pos.position( node );
return ( x == null || x > pos.x && x < pos.x + node.offsetWidth ) &&
( y == null || y > pos.y && y < pos.y + node.offsetHeight );
},
// Private:
// IE has some strange offset...
mouse_offset: function() {
if ( LAPI.Browser.is_ie ) {
var doc_elem = document.documentElement;
if ( doc_elem ) {
if ( typeof( doc_elem.getBoundingClientRect ) == 'function' ) {
var tmp = doc_elem.getBoundingClientRect();
return {x: tmp.left, y: tmp.top};
} else {
return {x: doc_elem.clientLeft, y: doc_elem.clientTop};
}
}
}
return {x: 0, y: 0};
}
}; // end LAPI.Pos
} // end if (guard)
if ( typeof( LAPI.Evt ) == 'undefined' ) {
LAPI.Evt = {
listenTo: function( object, node, evt, f, capture ) {
var listener = LAPI.Evt.makeListener( object, f );
LAPI.Evt.attach( node, evt, listener, capture );
},
attach: function( node, evt, f, capture ) {
if ( node.attachEvent ) {
node.attachEvent( 'on' + evt, f );
} else if ( node.addEventListener ) {
node.addEventListener( evt, f, capture );
} else {
node['on' + evt] = f;
}
},
remove: function( node, evt, f, capture ) {
if ( node.detachEvent ) {
node.detachEvent( 'on' + evt, f );
} else if ( node.removeEventListener ) {
node.removeEventListener( evt, f, capture );
} else {
node['on' + evt] = null;
}
},
makeListener: function( obj, listener ) {
// Some hacking around to make sure 'this' is set correctly
var object = obj, f = listener;
return function( evt ) { return f.apply( object, [evt || window.event] ); }
// Alternative implementation:
// var f = listener.bind( obj );
// return function( evt ) { return f ( evt || window.event ); };
},
kill: function( evt ) {
if ( typeof( evt.preventDefault ) == 'function' ) {
evt.stopPropagation();
evt.preventDefault(); // Don't follow the link
} else if ( typeof( evt.cancelBubble ) != 'undefined' ) { // IE...
evt.cancelBubble = true;
}
return false; // Don't follow the link (IE)
}
}; // end LAPI.Evt
} // end if (guard)
if ( typeof( LAPI.Edit ) == 'undefined' ) {
LAPI.Edit = function() { this.initialize.apply( this, arguments ); };
LAPI.Edit.SAVE = 1;
LAPI.Edit.PREVIEW = 2;
LAPI.Edit.REVERT = 4;
LAPI.Edit.CANCEL = 8;
LAPI.Edit.prototype = {
initialize: function( initial_text, columns, rows, labels, handlers ) {
var my_labels = {
box: null,
preview: null,
save: 'Save',
cancel: 'Cancel',
nullsave: null,
revert: null,
post: null
};
if ( labels ) {
my_labels = Object.merge( labels, my_labels );
}
this.labels = my_labels;
this.timestamp = ( new Date() ).getTime();
this.id = 'simpleedit_' + this.timestamp;
this.view = LAPI.make( 'div', {id: this.id}, {marginRight: '1em'} );
// Somehow, the textbox extends beyond the bounding box of the view. Don't know why, but
// adding a small margin fixes the layout more or less.
this.form = LAPI.make(
'form',
{
id: this.id + '_form',
action : '',
onsubmit: ( function() {} )
}
);
if ( my_labels.box ) {
var label = LAPI.make( 'div' );
label.appendChild( LAPI.DOM.makeLabel(
this.id + '_label',
my_labels.box, this.id + '_text'
) );
this.form.appendChild( label );
}
this.textarea = LAPI.make(
'textarea',
{
id: this.id + '_text',
cols: columns,
rows: rows,
value: ( initial_text ? initial_text.toString() : '' )
}
);
LAPI.Evt.attach( this.textarea, 'keyup', LAPI.Evt.makeListener( this, this.text_changed ) );
// Catch cut/copy/paste through the context menu. Some browsers support oncut, oncopy,
// onpaste events for this, but since that's only IE, FF 3, Safari 3, and Chrome, we
// cannot rely on this. Instead, we check again as soon as we leave the textarea. Only
// minor catch is that on FF 3, the next focus target is determined before the blur event
// fires. Since in practice save will always be enabled, this shouldn't be a problem.
LAPI.Evt.attach( this.textarea, 'mouseout', LAPI.Evt.makeListener( this, this.text_changed ) );
LAPI.Evt.attach( this.textarea, 'blur', LAPI.Evt.makeListener( this, this.text_changed ) );
this.form.appendChild( this.textarea );
this.form.appendChild( LAPI.make( 'br' ) );
this.preview_section = LAPI.make( 'div', null, {borderBottom: '1px solid #8888aa', display: 'none'} );
this.view.insertBefore( this.preview_section, this.view.firstChild );
this.save = LAPI.DOM.makeButton(
this.id + '_save',
my_labels.save,
LAPI.Evt.makeListener( this, this.do_save )
);
this.form.appendChild( this.save );
if ( my_labels.preview ) {
this.preview = LAPI.DOM.makeButton(
this.id + '_preview',
my_labels.preview,
LAPI.Evt.makeListener( this, this.do_preview )
);
this.form.appendChild( this.preview );
}
this.cancel = LAPI.DOM.makeButton(
this.id + '_cancel',
my_labels.cancel,
LAPI.Evt.makeListener( this, this.do_cancel )
);
this.form.appendChild( this.cancel );
this.view.appendChild( this.form );
if ( my_labels.post ) {
this.post_text = LAPI.DOM.setContent( LAPI.make( 'div' ), my_labels.post );
this.view.appendChild( this.post_text );
}
if ( handlers ) {
Object.merge( handlers, this );
}
if ( typeof( this.ongettext ) != 'function' ) {
this.ongettext = function( text ) { return text; }; // Default: no modifications
}
this.current_mask = LAPI.Edit.SAVE + LAPI.Edit.PREVIEW + LAPI.Edit.REVERT + LAPI.Edit.CANCEL;
if ( ( !initial_text || initial_text.trim().length == 0 ) && this.preview ) {
this.preview.disabled = true;
}
if ( my_labels.revert ) {
this.revert = LAPI.DOM.makeButton(
this.id + '_revert',
my_labels.revert,
LAPI.Evt.makeListener( this, this.do_revert )
);
this.form.insertBefore( this.revert, this.cancel );
}
this.original_text = '';
},
getView: function() {
return this.view;
},
getText: function() {
return this.ongettext( this.textarea.value );
},
setText: function( text ) {
this.textarea.value = text;
this.original_text = text;
this.text_changed();
},
changeText: function( text ) {
this.textarea.value = text;
this.text_changed();
},
hidePreview: function() {
this.preview_section.style.display = 'none';
if ( this.onpreview ) {
this.onpreview( this );
}
},
showPreview: function() {
this.preview_section.style.display = '';
if ( this.onpreview ) {
this.onpreview( this );
}
},
setPreview: function( html ) {
if ( html.nodeName ) {
LAPI.DOM.removeChildren( this.preview_section );
this.preview_section.appendChild( html );
} else {
this.preview_section.innerHTML = html;
}
},
busy: function( show ) {
if ( show ) {
LAPI.Ajax.injectSpinner( this.cancel, this.id + '_spinner' );
} else {
LAPI.Ajax.removeSpinner( this.id + '_spinner' );
}
},
do_save: function( evt ) {
if ( this.onsave ) {
this.onsave( this );
}
return true;
},
do_revert: function( evt ) {
this.changeText( this.original_text );
return true;
},
do_cancel: function( evt ) {
if ( this.oncancel ) {
this.oncancel( this );
}
return true;
},
do_preview: function( evt ) {
var self = this;
this.busy( true );
LAPI.Ajax.parseWikitext(
this.getText(),
function( text, failureFunc ) {
self.busy( false );
self.setPreview( text );
self.showPreview();
},
function( req, json_result ) {
// Error. TODO: user feedback?
self.busy( false );
},
true,
wgUserLanguage || null,
mw.config.get('wgPageName') || null
);
return true;
},
enable: function( bit_set ) {
var call_text_changed = false;
this.current_mask = bit_set;
this.save.disabled = ( ( bit_set & LAPI.Edit.SAVE ) == 0 );
this.cancel.disabled = ( ( bit_set & LAPI.Edit.CANCEL ) == 0 );
if ( this.preview ) {
if ( ( bit_set & LAPI.Edit.PREVIEW ) == 0 ) {
this.preview.disabled = true;
} else {
call_text_changed = true;
}
}
if ( this.revert ) {
if ( ( bit_set & LAPI.Edit.REVERT ) == 0 ) {
this.revert.disabled = true;
} else {
call_text_changed = true;
}
}
if ( call_text_changed ) {
this.text_changed();
}
},
text_changed: function( evt ) {
var text = this.textarea.value;
text = text.trim();
var length = text.length;
if ( this.preview && ( this.current_mask & LAPI.Edit.PREVIEW ) != 0 ) {
// Preview is basically enabled
this.preview.disabled = ( length <= 0 );
}
if ( this.labels.nullsave ) {
if ( length > 0 ) {
this.save.value = this.labels.save;
} else {
this.save.value = this.labels.nullsave;
}
}
if ( this.revert ) {
this.revert.disabled =
( text == this.original_text || this.textarea.value == this.original_text );
}
return true;
}
}; // end LAPI.Edit
} // end if (guard)
// </source>