MediaWiki:LAPI.js
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Internet Explorer / Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5
- Opera: Press Ctrl-F5.
/* 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) */ // <nowiki> // Global: importScript (from wiki.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. /* global importScript, LAPI, ajaxSubmit */ /* jshint unused:false, laxcomma:true, smarttabs:true, loopfunc:true, forin:false */ /* eslint-disable one-var, vars-on-top, camelcase, no-use-before-define, eqeqeq, no-bitwise */ if ( window.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 ( !predicate ) { 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 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. if (!String.prototype.startsWith) { String.prototype.startsWith = function ( prefix ) { return this.indexOf( prefix ) === 0; }; } // Returns true if the string ends in suffix if ( !String.prototype.endsWith ) { 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() + '=([^&#]*)' ), m = re.exec( this ); if ( m && m.length >= 2 ) { return decodeURIComponent( m[1] ); } return null; }; String.getParamValue = function ( param, url ) { url = url || document.location.href; try { return url.getParamValue( param ); } catch ( e ) { return null; } }; /** Function enhancements (JavaScript 1.8.5.) ************/ 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, result = []; if ( thisObject ) { predicate = predicate.bind( thisObject ); } for ( var i = 0; 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; 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; 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; 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, result = []; if ( thisObject ) { mapper = mapper.bind( thisObject ); } for ( var i = 0; 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 ( !target.length ) { 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; }; } /** 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, result = null; if ( thisObject ) { selector = selector.bind( thisObject ); } for ( var i = 0; 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 || !source.length ) { return null; } var result = [], l = source.length; for ( var i = 0; i < l; i++ ) { if ( i in source ) { result[result.length] = source[i]; } } return result; }; if ( !window.LAPI ) { var LAPI = window.LAPI = { Ajax: { getRequest: function () { var request = null; try { request = new XMLHttpRequest(); } catch ( anything ) { request = null; if ( window.ActiveXObject ) { if ( !LAPI.Ajax.getRequest.msXMLHttpID ) { 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 ) { return null; } doc = doc || document; if ( typeof selector === 'string' ) { if ( selector[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 ) { 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 || '', msg = ex.message || '', file = null, line = null; if ( msg && msg.length > 0 && msg[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 ( !LAPI.Browser ) { // 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 ( !LAPI.DOM ) { 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 ( !LAPI.DOM.parse.msDOMDocumentID ) { // 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*/ ) { // Always use a faked document; parsing as XML and then treating the result as HTML doesn't work right with HTML5. return LAPI.DOM.fakeHTMLDocument( str ); }, fakeHTMLDocument: function ( str ) { var body_tag = /<body.*?>/.exec( str ); if ( !body_tag || !body_tag.length ) { 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 ( doc.parseError ) { // 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 ( 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: String( width ), height: String( 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 ( elem, 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 ( elem.ownerDocument.defaultView && elem.ownerDocument.defaultView.getComputedStyle ) { // Gecko etc. if ( property === 'cssFloat' ) { property = 'float'; } return elem.ownerDocument.defaultView.getComputedStyle( elem, null ).getPropertyValue( property ); } else { var result; if ( elem.currentStyle ) { // IE, has subtle differences to getComputedStyle result = elem.currentStyle[property] || elem.currentStyle[normalize( property )]; } else { // Not exactly right, but best effort result = elem.style[property] || elem.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 ) && elem.runtimeStyle ) { var style = elem.style.left, runtimeStyle = elem.runtimeStyle.left; elem.runtimeStyle.left = elem.currentStyle.left; elem.style.left = result || 0; result = elem.style.pixelLeft + 'px'; elem.style.left = style; elem.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 = String( width ); } if ( height ) { img_loader.height = String( 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' ), 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, 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 ( !LAPI.WP ) { 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 // "\u200f (1,806 × 1,341 بكسل، حجم الملف: 996 كيلوبايت، نوع الملف: image/jpeg) and with uselang=en, // it is at ar-WP "\u200f (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 }, file_hist = LAPI.$( 'mw-imagepage-section-filehistory', doc ); if ( !file_hist ) { return result; } try { var $file_curr = $ ? $( file_hist ).find( 'td.filehistory-selected' ) : document.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 ) { // Catch page without file... return null; } var imgs = file_div.getElementsByTagName( 'img' ); title = title || mw.config.get( 'wgTitle' ); for ( var i = 0; i < imgs.length; i++ ) { var src = imgs[i].getAttribute( 'src', 2 ); if ( src && src.search( /^data/ ) ) { src = decodeURIComponent( src ).replace( '%26', '&' ); if ( !src.search( new RegExp( '^' + LAPI_file_store + '.*/' + title.replace( / /g, '_' ).escapeRE() + '(/.*)?$' ) ) ) { 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 // article path, 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 article path: href="/wiki/..." var prefix = mw.config.get( 'wgArticlePath' ).replace( '$1', '' ); if ( !href.startsWith( prefix ) ) { // Fully expanded URL? prefix = mw.config.get( 'wgServer') + prefix; } if ( !href.startsWith( prefix ) && prefix.startsWith( '//' ) ) { // Protocol-relative 'wgServer'? prefix = document.location.protocol + prefix; } 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( variants.escapeRE().replace( '\\$2', '[^\\/]*' ).replace( '\\$1', '(.*)' ) ), 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.RLCONF ) { // MW 1.32+ revision_id = htmlOfPage.match( /RLCONF=\{.*"wgCurRevisionId"\s*:\s*(\d+),/ ); } else if ( window.mediaWiki ) { // MW 1.17+ revision_id = htmlOfPage.match( /(?:mediaWiki|mw).config.set\(\{.*"wgCurRevisionId"\s*:\s*(\d+),/ ); } 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 ( !LAPI.Ajax.doAction ) { importScript( 'MediaWiki:AjaxSubmit.js' ); // 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; // 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 // && 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; } } 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 ( !LAPI.Ajax.getHTML.extraNewlineRE ) { // 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>', 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>', testDoc = LAPI.DOM.parseHTML( testString, 'test' ), testVal = String( 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, '' ); } } 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 = '', 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[0] === '&' || params[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( '//' ) ) { // Avoid protocol-relative URIs (IE7 bug) uri = document.location.protocol + uri; } 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 { // We'll lose caching, but at least we can make the request. method = 'POST'; } 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, the_form = null, 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.' ); } the_form.wpWatchthis.checked = !!document.getElementById( 'ca-unwatch' ); } 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'; } // Exclude jsonfm, which actually serves XHTML is_json = /format=json(&|$)/.test( params ); } else { if ( typeof params.format !== 'string' || !params.format.length ) { params.format = 'json'; } is_json = params.format === 'json'; } } 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()[0] !== '{' ) { failureFunc( request ); } else { var json; try { json = ( is_json ? eval( '(' + request.responseText.trimLeft() + ')' ) : null ); } catch ( e ) { json = null; } success( request, json, 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 || 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; 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 // 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 ( !window.jQuery || !window.mediaWiki || !mw.loader ) { // Assume old-style if ( window.injectSpinner ) { LAPI.Ajax.injectSpinner = window.injectSpinner; } if ( window.removeSpinner ) { LAPI.Ajax.removeSpinner = window.removeSpinner; } } else { mw.loader.using( 'jquery.spinner', function () { LAPI.Ajax.injectSpinner = function ( elementBefore, id ) { $( elementBefore ).injectSpinner( id ); }; LAPI.Ajax.removeSpinner = function ( id ) { $.removeSpinner( id ); }; } ); } } // end if (guard) if ( !LAPI.Pos ) { LAPI.Pos = { // Returns the global coordinates of the mouse pointer within the document. mousePosition: function ( evt ) { if ( !evt || ( !evt.pageX && !evt.clientX ) ) { // No way to calculate a mouse pointer position return null; } if ( evt.pageX.evt ) { return { x: evt.pageX, y: evt.pageY }; } var offset = LAPI.Pos.scrollOffset(), mouse_delta = LAPI.Pos.mouse_offset(), coor_x = evt.clientX + offset.x - mouse_delta.x, 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, container = document.createElement( 'div' ), 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>', 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, checkDiv = innerDiv.firstChild, 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(), scroll = LAPI.Pos.scrollOffset(); return { x: ( box.left + scroll.x ), y: ( box.top + scroll.y ) }; } if ( !data ) { jQuery_init(); } var elem = node, offsetParent = elem.offsetParent, // prevOffsetParent = elem, doc = elem.ownerDocument, prevComputedStyle = doc.defaultView.getComputedStyle( elem, null ), computedStyle, top = elem.offsetTop, 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 ( !LAPI.Evt ) { 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 ( evt.cancelBubble.evt ) { // IE... evt.cancelBubble = true; } return false; // Don't follow the link (IE) } }; // end LAPI.Evt } // end if (guard) if ( !LAPI.Edit ) { 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 #88a', 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 ) && 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, mw.config.get( '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 ) { this.save.value = ( length > 0 ) ? this.labels.save : 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) // </nowiki>