User:Liangent/ImageAnnotatorCopyList

From Wikimedia Commons, the free media repository
Jump to navigation Jump to search

//

/*
  ImageAnnotator v2.3.2

  ATTENTION:
  This is in the Gadget- prefix but not actually registered nor loaded as a Gadget. It is
  loaded directly by [[MediaWiki:Common.js]], raw, unminified and in the global scope.

  Image annotations. Draw rectangles onto image thumbnail displayed on image description
  page and associate them with textual descriptions that will be displayed when the mouse
  moves over the rectangles. If an image has annotations, display the rectangles. Add a
  button to create new annotations.

  Note: if an image that has annotations is overwritten by a new version, only display the
  annotations if the size of the top image matches the stored size exactly. To recover
  annotations, one will need to edit the image description page manually, adjusting image
  sizes and rectangle coordinates, or re-enter annotations.

  Author: [[User:Lupo]], June 2009 - March 2010
  License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)

  Choose whichever license of these you like best :-)

  See http://commons.wikimedia.org/wiki/Help:Gadget-ImageAnnotator for documentation.
*/

/* global importScript, importScriptURI,
	LAPI, Tooltip, Tooltips, TextCleaner, UIElements, Buttons,
	ImageAnnotator, ImageAnnotator_disable */
/* eslint-disable one-var, vars-on-top, camelcase, no-use-before-define,
	eqeqeq, no-alert, no-loop-func, no-inner-declarations */
if ( typeof ImageAnnotator === 'undefined' ) { // Guard against multiple inclusions

	importScript( 'MediaWiki:LAPI.js' );
	importScript( 'MediaWiki:Tooltips.js' );
	importScript( 'MediaWiki:TextCleaner.js' );
	importScript( 'MediaWiki:UIElements.js' );

	( function () { // Local scope

		var ImageAnnotator_config = null;

		var ImageAnnotation = function () {
			this.initialize.apply( this, arguments );
		};

		ImageAnnotation.compare = function ( a, b ) {
			var result = b.area() - a.area();
			if ( result !== 0 ) {
				return result;
			}

			// Just to make sure the order is complete
			return a.model.id - b.model.id;
		};

		ImageAnnotation.prototype = {
			// Rectangle to be displayed on image: a div with pos and size
			view: null,

			// Internal representation of the annotation
			model: null,

			// Tooltip to display the annotation
			tooltip: null,

			// Content of the tooltip
			content: null,

			// Reference to the viewer this note belongs to
			viewer: null,

			initialize: function ( node, viewer, id ) {
				var is_new = false;
				var view_w = 0, view_h = 0, view_x = 0, view_y = 0;

				this.viewer = viewer;

				if ( LAPI.DOM.hasClass( node, IA.annotation_class ) ) {
					// Extract the info we need
					var x = IA.getIntItem( 'view_x_' + id, viewer.scope );
					var y = IA.getIntItem( 'view_y_' + id, viewer.scope );
					var w = IA.getIntItem( 'view_w_' + id, viewer.scope );
					var h = IA.getIntItem( 'view_h_' + id, viewer.scope );
					var html = IA.getRawItem( 'content_' + id, viewer.scope );

					if ( x === null || y === null || w === null || h === null || html === null ) {
						throw new Error( 'Invalid note' );
					}

					if ( x < 0 || x >= viewer.full_img.width || y < 0 || y >= viewer.full_img.height ) {
						throw new Error( 'Invalid note: origin invalid on note ' + id );
					}

					if (
						x + w > viewer.full_img.width + 10 ||
						y + h > viewer.full_img.height + 10
					) {
						throw new Error( 'Invalid note: size extends beyond image on note ' + id );
					}

					// Notes written by early versions may be slightly too large, whence the + 10 above. Fix this.
					if ( x + w > viewer.full_img.width ) {
						w = viewer.full_img.width - x;
					}

					if ( y + h > viewer.full_img.height ) {
						h = viewer.full_img.height - y;
					}

					view_w = Math.floor( w / viewer.factors.dx );
					view_h = Math.floor( h / viewer.factors.dy );
					view_x = Math.floor( x / viewer.factors.dx );
					view_y = Math.floor( y / viewer.factors.dy );

					this.view = LAPI.make(
						'div', null,
						{
							position: 'absolute',
							display: 'none',
							lineHeight: '0px', // IE
							fontSize: '0px', // IE
							top: String( view_y ) + 'px',
							left: String( view_x ) + 'px',
							width: String( view_w ) + 'px',
							height: String( view_h ) + 'px'
						}
					);

					// We'll add the view to the DOM once we've loaded all notes
					this.model = {
						id: id,
						dimension: { x: x, y: y, w: w, h: h },
						wiki: '',
						html: html.cloneNode( true )
					};
				} else {
					is_new = true;
					this.view = node;
					this.model = {
						id: -1,
						dimension: null,
						wiki: '',
						html: null
					};
					view_w = this.view.offsetWidth - 2; // Subtract cumulated border widths
					view_h = this.view.offsetHeight - 2;
					view_x = this.view.offsetLeft;
					view_y = this.view.offsetTop;
				}

				// Enforce a minimum size of the view. Center the 6x6px square over the center of the old view.
				// If we overlap the image boundary, adjustRectangleSize will take care of it later.
				if ( view_w < 6 ) {
					view_x = Math.floor( view_x + view_w / 2 - 3 );
					view_w = 6;
				}
				if ( view_h < 6 ) {
					view_y = Math.floor( view_y + view_h / 2 - 3 );
					view_h = 6;
				}

				Object.merge(
					{
						left: String( view_x ) + 'px',
						top: String( view_y ) + 'px',
						width: String( view_w ) + 'px',
						height: String( view_h ) + 'px'
					},
					this.view.style
				);

				this.view.style.zIndex = 500; // Below tooltips

				try {
					this.view.style.border = '1px solid ' + this.viewer.outer_border;
				} catch ( ex ) {
					this.view.style.border = '1px solid ' + IA.outer_border;
				}

				this.view.appendChild(
					LAPI.make(
						'div', null,
						{
							lineHeight: '0px', // IE
							fontSize: '0px', // IE
							width: String( Math.max( view_w - 2, 0 ) ) + 'px', // -2 to leave space for the border
							height: String( Math.max( view_h - 2, 0 ) ) + 'px'
						}
					)
					// width=100% doesn't work right: inner div's border appears outside on right and bottom on FF.
				);

				try {
					this.view.firstChild.style.border = '1px solid ' + this.viewer.inner_border;
				} catch ( ex ) {
					this.view.firstChild.style.border = '1px solid ' + IA.inner_border;
				}

				if ( is_new ) {
					viewer.adjustRectangleSize( this.view );
				}

				// IE somehow passes through event to the view even if covered by our cover, displaying the tooltips
				// when drawing a new rectangle, which is confusing and produces a selection nightmare. Hence we just
				// display raw rectangles without any tooltips attached while drawing. Yuck.
				this.dummy = this.view.cloneNode( true );
				viewer.img_div.appendChild( this.dummy );

				if ( !is_new ) {
					// New notes get their tooltip only once the editor has saved, otherwise IE may try to
					// open them if the mouse moves onto the view even though there is the cover above them!
					this.setTooltip();
				}
			},

			setTooltip: function () {
				if ( this.tooltip || !this.view ) {
					// Already set, or corrupt
					return;
				}

				// Note: on IE, don't have tooltips appear automatically. IE doesn't do it right for transparent
				// targets and we have to show and hide them ourselves through a mousemove listener in the viewer
				// anyway. The occasional event that IE sends to the tooltip may then lead to ugly flickering.
				this.tooltip = new Tooltip(
					this.view.firstChild,
					this.display.bind( this ),
					{
						activate: ( LAPI.DOM.is_ie ? Tooltip.NONE : Tooltip.HOVER ),
						deactivate: ( LAPI.DOM.is_ie ? Tooltip.ESCAPE : Tooltip.LEAVE ),
						close_button: null,
						mode: Tooltip.MOUSE,
						mouse_offset: {
							x: -5,
							y: -5,
							dx: ( IA.is_rtl ? -1 : 1 ),
							dy: 1
						},
						open_delay: 0,
						hide_delay: 0,
						onclose: ( function ( tooltip, evt ) {
							if ( this.view ) {
								try {
									this.view.style.border = '1px solid ' + this.viewer.outer_border;
								} catch ( ex ) {
									this.view.style.border = '1px solid ' + IA.outer_border;
								}
							}

							if ( this.viewer.tip == tooltip ) {
								this.viewer.tip = null;
							}

							// Hide all boxes if we're outside the image. Relies on hide checking the
							// coordinates! (Otherwise, we'd always hide...)
							if ( evt ) {
								this.viewer.hide( evt );
							}
						} ).bind( this ),
						onopen: ( function ( tooltip ) {
							if ( this.view ) {
								try {
									this.view.style.border = '1px solid ' + this.viewer.active_border;
								} catch ( ex ) {
									this.view.style.border = '1px solid ' + IA.active_border;
								}
							}

							this.viewer.tip = tooltip;
						} ).bind( this )
					},
					IA.tooltip_styles
				);
			},

			display: function ( evt ) {
				if ( !this.content ) {
					this.content = LAPI.make( 'div' );
					var main = LAPI.make( 'div' );
					this.content.appendChild( main );
					this.content.main = main;

					if ( this.model.html ) {
						main.appendChild( this.model.html.cloneNode( true ) );
					}

					// Make sure that the popup encompasses all floats
					this.content.appendChild( LAPI.make( 'div', null, { clear: 'both' } ) );

					if ( this.viewer.may_edit ) {
						this.content.button_section = LAPI.make(
							'div',
							null,
							{
								fontSize: 'smaller',
								textAlign: ( IA.is_rtl ? 'left' : 'right' ),
								borderTop: IA.tooltip_styles.border
							}
						);

						this.content.appendChild( this.content.button_section );
						this.content.button_section.appendChild( LAPI.DOM.makeLink(
							'#',
							ImageAnnotator.UI.get( 'wpImageAnnotatorEdit', true ),
							null,
							LAPI.Evt.makeListener( this, this.edit )
						) );

						if ( ImageAnnotator_config.mayDelete() ) {
							this.content.button_section.appendChild( document.createTextNode( '\xa0' ) );
							this.content.button_section.appendChild( LAPI.DOM.makeLink(
								'#',
								ImageAnnotator.UI.get( 'wpImageAnnotatorDelete', true ),
								null,
								LAPI.Evt.makeListener( this, this.remove_event )
							) );
						}
					}
				}

				return this.content;
			},

			edit: function ( evt ) {
				if ( IA.canEdit() ) {
					IA.editor.editNote( this );
				}

				if ( evt ) {
					return LAPI.Evt.kill( evt );
				}

				return false;
			},

			remove_event: function ( evt ) {
				if ( IA.canEdit() ) {
					this.remove();
				}

				return LAPI.Evt.kill( evt );
			},

			remove: function () {
				if ( !this.content ) { // New note: just destroy it.
					this.destroy();
					return true;
				}

				if ( !ImageAnnotator_config.mayDelete() ) {
					return false;
				}

				// Close and remove tooltip only if edit succeeded! Where and how to display error messages?

				var reason = '';
				if ( !ImageAnnotator_config.mayBypassDeletionPrompt() || !window.ImageAnnotator_noDeletionPrompt ) {
					// Prompt for a removal reson
					reason = prompt( ImageAnnotator.UI.get( 'wpImageAnnotatorDeleteReason', true ), '' );
					if ( reason === null ) {
						// Cancelled
						return false;
					}

					reason = reason.trim();
					if ( !reason.length ) {
						if ( !ImageAnnotator_config.emptyDeletionReasonAllowed() ) {
							return false;
						}
					}

					// Re-show tooltip (without re-positioning it, we have no mouse coordinates here) in case
					// it was hidden because of the alert. If possible, we want the user to see the spinner.
					this.tooltip.show_now( this.tooltip );
				}

				var self = this;
				var spinnerId = 'image_annotation_delete_' + this.model.id;
				LAPI.Ajax.injectSpinner( this.content.button_section.lastChild, spinnerId );

				if ( this.tooltip ) {
					this.tooltip.size_change();
				}

				LAPI.Ajax.editPage(
					mw.config.get( 'wgPageName' ),
					function ( doc, editForm, failureFunc, revision_id ) {
						try {
							if ( revision_id && revision_id != mw.config.get( 'wgCurRevisionId' ) ) {
								throw new Error( '#Page version (revision ID) mismatch: edit conflict.' );
							}

							var textbox = editForm.wpTextbox1;
							if ( !textbox ) {
								throw new Error( '#Server replied with invalid edit page.' );
							}

							var pagetext = textbox.value.replace( /\r\n/g, '\n' );
							// Normalize different end-of-line handling. Opera and IE may use \r\n, whereas other
							// browsers just use '\n'. Note that all browsers do the right thing if a '\n' is added.
							// We normally don't care, but here we need this to make sure we don't leave extra line
							// breaks when we remove the note.

							IA.setWikitext( pagetext );

							var span = IA.findNote( pagetext, self.model.id );
							if ( !span ) { // Hmmm? Doesn't seem to exist
								LAPI.Ajax.removeSpinner( spinnerId );

								if ( self.tooltip ) {
									self.tooltip.size_change();
								}

								self.destroy();
								return;
							}

							var char_before = 0;
							var char_after = 0;

							if ( span.start > 0 ) {
								char_before = pagetext.charCodeAt( span.start - 1 );
							}

							if ( span.end < pagetext.length ) {
								char_after = pagetext.charCodeAt( span.end );
							}

							if (
								String.fromCharCode( char_before ) == '\n' &&
								String.fromCharCode( char_after ) == '\n'
							) {
								span.start = span.start - 1;
							}
							pagetext = pagetext.substring( 0, span.start ) + pagetext.substring( span.end );
							textbox.value = pagetext;

							var summary = editForm.wpSummary;
							if ( !summary ) {
								throw new Error( '#Summary field not found. Check that edit pages have valid XHTML.' );
							}

							IA.setSummary(
								summary,
								ImageAnnotator.UI.get( 'wpImageAnnotatorRemoveSummary', true ) ||
									'[[MediaWiki talk:Gadget-ImageAnnotator.js|Removing image note]]$1',
								( reason.length ? reason + ': ' : '' ) + self.model.wiki
							);
						} catch ( ex ) {
							failure( null, ex );
							return;
						}

						var edit_page = doc;
						LAPI.Ajax.submitEdit(
							editForm,
							function ( request ) {
								if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) {
									edit_page.dispose();
								}

								var revision_id = LAPI.WP.revisionFromHtml( request.responseText );
								if ( !revision_id ) {
									failureFunc( request, new Error( 'Revision ID not found. Please reload the page.' ) );
									return;
								}

								mw.config.set( 'wgCurRevisionId', revision_id ); // Bump revision id!!

								LAPI.Ajax.removeSpinner( spinnerId );

								if ( self.tooltip ) {
									self.tooltip.size_change();
								}

								self.destroy();
							},
							function ( request, ex ) {
								if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) {
									edit_page.dispose();
								}
								failureFunc( request, ex );
							}
						);
					},
					function ( request, ex ) {
						// Failure. What now? TODO: Implement some kind of user feedback.
						LAPI.Ajax.removeSpinner( spinnerId );

						if ( self.tooltip ) {
							self.tooltip.size_change();
						}
					}
				);

				return true;
			},

			destroy: function () {
				if ( this.view ) {
					LAPI.DOM.removeNode( this.view );
				}

				if ( this.dummy ) {
					LAPI.DOM.removeNode( this.dummy );
				}

				if ( this.tooltip ) {
					this.tooltip.hide_now();
				}

				if ( this.model && this.model.id > 0 && this.viewer ) {
					this.viewer.deregister( this );
				}

				this.model = null;
				this.view = null;
				this.content = null;
				this.tooltip = null;
				this.viewer = null;
			},

			area: function () {
				if ( !this.model || !this.model.dimension ) {
					return 0;
				}

				return ( this.model.dimension.w * this.model.dimension.h );
			},

			cannotEdit: function () {
				if ( this.content && this.content.button_section ) {
					LAPI.DOM.removeNode( this.content.button_section );

					this.content.button_section = null;

					if ( this.tooltip ) {
						this.tooltip.size_change();
					}
				}
			}

		}; // end ImageAnnotation

		var ImageAnnotationEditor = function () {
			this.initialize.apply( this, arguments );
		};

		ImageAnnotationEditor.prototype = {
			initialize: function () {
				var editor_width = 50;

				// Respect potential user-defined width setting
				if (
					window.ImageAnnotationEditor_columns &&
					!isNaN( window.ImageAnnotationEditor_columns ) &&
					window.ImageAnnotationEditor_columns >= 30 &&
					window.ImageAnnotationEditor_columns <= 100
				) {
					editor_width = window.ImageAnnotationEditor_columns;
				}

				this.editor = new LAPI.Edit(
					'', editor_width, 6,
					{
						box: ImageAnnotator.UI.get( 'wpImageAnnotatorEditorLabel', false ),
						preview: ImageAnnotator.UI.get( 'wpImageAnnotatorPreview', true ).capitalizeFirst(),
						save: ImageAnnotator.UI.get( 'wpImageAnnotatorSave', true ).capitalizeFirst(),
						revert: ImageAnnotator.UI.get( 'wpImageAnnotatorRevert', true ).capitalizeFirst(),
						cancel: ImageAnnotator.UI.get( 'wpImageAnnotatorCancel', true ).capitalizeFirst(),
						nullsave: ImageAnnotator_config.mayDelete() ?
							ImageAnnotator.UI.get( 'wpImageAnnotatorDelete', true ).capitalizeFirst() :
							null,
						post: ImageAnnotator.UI.get( 'wpImageAnnotatorCopyright', false )
					},
					{
						onsave: this.save.bind( this ),
						onpreview: this.onpreview.bind( this ),
						oncancel: this.cancel.bind( this ),
						ongettext: function ( text ) {
							if ( text == null ) {
								return '';
							}

							text = text.trim()
								.replace( /\{\{(\s*ImageNote(End)?\s*\|)/g, '&#x7B;&#x7B;$1' );

							// Guard against people trying to break notes on purpose
							if ( text.length && typeof TextCleaner !== 'undefined' ) {
								text = TextCleaner.sanitizeWikiText( text, true );
							}

							return text;
						}
					}
				);

				this.box = LAPI.make( 'div' );
				this.box.appendChild( this.editor.getView() );

				// Limit the width of the bounding box to the size of the textarea, taking into account the
				// tooltip styles. Do *not* simply append this.box or the editor view, Opera behaves strangely
				// if textboxes were ever hidden through a visibility setting! Use a second throw-away textbox
				// instead.
				var temp = LAPI.make( 'div', null, IA.tooltip_styles );
				temp.appendChild( LAPI.make( 'textarea', { cols: editor_width, rows: 6 } ) );
				Object.merge(
					{
						position: 'absolute',
						top: '0px',
						left: '-10000px',
						visibility: 'hidden'
					},
					temp.style
				);
				document.body.appendChild( temp );

				// Now we know how wide this textbox will be
				var box_width = temp.offsetWidth;
				LAPI.DOM.removeNode( temp );

				// Note: we need to use a tooltip with a dynamic content creator function here because
				// static content is cloned inside the Tooltip. Cloning on IE loses all attached handlers,
				// and thus the editor's controls wouldn't work anymore. (This is not a problem on FF3,
				// where cloning preserves the handlers.)
				this.tooltip = new Tooltip(
					IA.get_cover(),
					this.get_editor.bind( this ),
					{
						activate: Tooltip.NONE, // We'll always show it explicitly
						deactivate: Tooltip.ESCAPE,
						close_button: null, // We have a cancel button anyway
						mode: Tooltip.FIXED,
						anchor: Tooltip.TOP_LEFT,
						mouse_offset: { x: 10, y: 10, dx: 1, dy: 1 }, // Misuse this: fixed offset from view
						max_pixels: ( box_width ? box_width + 20 : 0 ), // + 20 gives some slack
						z_index: 2010, // Above the cover.
						open_delay: 0,
						hide_delay: 0,
						onclose: this.close_tooltip.bind( this )
					},
					IA.tooltip_styles
				);

				this.note = null;
				this.visible = false;

				LAPI.Evt.listenTo( this, this.tooltip.popup, IA.mouse_in,
					function ( evt ) {
						Array.forEach( IA.viewers, ( function ( viewer ) {
							if ( viewer != this.viewer && viewer.visible ) {
								viewer.hide();
							}
						} ).bind( this ) );
					}
				);
			},

			get_editor: function () {
				return this.box;
			},

			editNote: function ( note ) {
				var same_note = ( note == this.note );
				this.note = note;
				this.viewer = this.note.viewer;

				var cover = IA.get_cover();
				cover.style.cursor = 'auto';
				IA.show_cover();

				if ( note.tooltip ) {
					note.tooltip.hide_now();
				}

				IA.is_editing = true;
				if ( note.content && !IA.wiki_read ) {
					// Existing note, and we don't have the wikitext yet: go get it
					var self = this;
					LAPI.Ajax.apiGet(
						'query',
						{
							prop: 'revisions',
							titles: mw.config.get( 'wgPageName' ),
							rvlimit: 1,
							rvstartid: mw.config.get( 'wgCurRevisionId' ),
							rvprop: 'ids|content'
						},
						function ( request, json_result ) {
							if ( json_result && json_result.query && json_result.query.pages ) {
								// Should have only one page here
								for ( var page in json_result.query.pages ) {
									var p = json_result.query.pages[ page ];
									if ( p && p.revisions && p.revisions.length ) {
										var rev = p.revisions[ 0 ];
										if ( rev.revid == mw.config.get( 'wgCurRevisionId' ) && rev[ '*' ] && rev[ '*' ].length ) {
											IA.setWikitext( rev[ '*' ] );
										}
									}
									break;
								}
							}

							// TODO: What upon a failure?
							self.open_editor( same_note, cover );
						},
						function ( request ) {
							// TODO: What upon a failure?
							self.open_editor( same_note, cover );
						}
					);
				} else {
					this.open_editor( same_note, cover );
				}
			},

			open_editor: function ( same_note, cover ) {
				this.editor.hidePreview();

				if ( !same_note || this.editor.textarea.readOnly ) {
					// Different note, or save error last time
					this.editor.setText( this.note.model.wiki );
				}

				this.editor.enable( LAPI.Edit.SAVE + LAPI.Edit.PREVIEW + LAPI.Edit.REVERT + LAPI.Edit.CANCEL );
				this.editor.textarea.readOnly = false;
				this.editor.textarea.style.backgroundColor = 'white';

				// Set the position relative to the note's view.
				var view_pos = LAPI.Pos.position( this.note.view );
				var origin = LAPI.Pos.position( cover );
				this.tooltip.options.fixed_offset.x =
					view_pos.x - origin.x + this.tooltip.options.mouse_offset.x;
				this.tooltip.options.fixed_offset.y =
					view_pos.y - origin.y + this.tooltip.options.mouse_offset.y;
				this.tooltip.options.fixed_offset.dx = 1;
				this.tooltip.options.fixed_offset.dy = 1;

				// Make sure mouse event listeners are removed, especially on IE.
				this.dim = {
					x: this.note.view.offsetLeft,
					y: this.note.view.offsetTop,
					w: this.note.view.offsetWidth,
					h: this.note.view.offsetHeight
				};
				this.viewer.setShowHideEvents( false );
				this.viewer.hide(); // Make sure notes are hidden
				this.viewer.toggle( true ); // Show all note rectangles (but only the dummies)

				// Now show the editor
				this.tooltip.show_tip( null, false );
				var tpos = LAPI.Pos.position( this.editor.textarea );
				var ppos = LAPI.Pos.position( this.tooltip.popup );
				tpos = tpos.x - ppos.x;
				if ( tpos + this.editor.textarea.offsetWidth > this.tooltip.popup.offsetWidth ) {
					this.editor.textarea.style.width = ( this.tooltip.popup.offsetWidth - 2 * tpos ) + 'px';
				}

				if ( LAPI.Browser.is_ie ) {
					// Fixate textarea width to prevent ugly flicker on each keypress in IE6...
					this.editor.textarea.style.width = this.editor.textarea.offsetWidth + 'px';
				}

				this.visible = true;
			},

			hide_editor: function ( evt ) {
				if ( !this.visible ) {
					return;
				}

				this.visible = false;
				IA.is_editing = false;
				this.tooltip.hide_now( evt );

				if ( evt && evt.type == 'keydown' && !this.saving ) {
					// ESC pressed on new note before a save attempt
					this.cancel();
				}

				IA.hide_cover();
				this.viewer.setDefaultMsg();
				this.viewer.setShowHideEvents( true );
				this.viewer.hide();
				this.viewer.show(); // Make sure we get the real views again.
				// FIXME in Version 2.1: Unfortunately, we don't have a mouse position here, so sometimes we
				// may show the note rectangles even though the mouse is now outside the image. (It was
				// somewhere inside the editor in most cases (if an editor button was clicked), but if ESC was
				// pressed, it may actually be anywhere.)
			},

			save: function ( editor ) {
				var data = editor.getText();

				if ( !data || !data.length ) {
					// Empty text
					if ( this.note.remove() ) {
						this.hide_editor();
						this.cancel();
						this.note = null;
					} else {
						this.hide_editor();
						this.cancel();
					}

					return;
				} else if ( data == this.note.model.wiki ) {
					// Text unchanged
					this.hide_editor();
					this.cancel();
					return;
				}

				// Construct what to insert
				var dim = Object.clone( this.note.model.dimension );
				if ( !dim ) {
					dim = {
						x: Math.round( this.dim.x * this.viewer.factors.dx ),
						y: Math.round( this.dim.y * this.viewer.factors.dy ),
						w: Math.round( this.dim.w * this.viewer.factors.dx ),
						h: Math.round( this.dim.h * this.viewer.factors.dy )
					};

					// Make sure everything is within bounds
					if ( dim.x + dim.w > this.viewer.full_img.width ) {
						if ( dim.w > this.dim.w * this.viewer.factors.dx ) {
							dim.w--;
							if ( dim.x + dim.w > this.viewer.full_img.width ) {
								if ( dim.x > 0 ) {
									dim.x--;
								} else {
									dim.w = this.viewer.full_img.width;
								}
							}
						} else {
							// Width already was rounded down
							if ( dim.x > 0 ) {
								dim.x--;
							}
						}
					}

					if ( dim.y + dim.h > this.viewer.full_img.height ) {
						if ( dim.h > this.dim.h * this.viewer.factors.dy ) {
							dim.h--;
							if ( dim.y + dim.h > this.viewer.full_img.height ) {
								if ( dim.y > 0 ) {
									dim.y--;
								} else {
									dim.h = this.viewer.full_img.height;
								}
							}
						} else {
							// Height already was rounded down
							if ( dim.y > 0 ) {
								dim.y--;
							}
						}
					}

					// If still too large, adjust width and height
					if ( dim.x + dim.w > this.viewer.full_img.width ) {
						if ( this.viewer.full_img.width > dim.x ) {
							dim.w = this.viewer.full_img.width - dim.x;
						} else {
							dim.x = this.viewer.full_img.width - 1;
							dim.w = 1;
						}
					}

					if ( dim.y + dim.h > this.viewer.full_img.height ) {
						if ( this.viewer.full_img.height > dim.y ) {
							dim.h = this.viewer.full_img.height - dim.y;
						} else {
							dim.y = this.viewer.full_img.height - 1;
							dim.h = 1;
						}
					}
				}

				this.to_insert =
					'{{ImageNote' +
					'|id=' + this.note.model.id +
					'|x=' + dim.x + '|y=' + dim.y + '|w=' + dim.w + '|h=' + dim.h +
					'|dimx=' + this.viewer.full_img.width +
					'|dimy=' + this.viewer.full_img.height +
					'|style=2' +
					'}}\n' +
					data + ( data.endsWith( '\n' ) ? '' : '\n' ) +
					'{{ImageNoteEnd|id=' + this.note.model.id + '}}';

				// Now edit the page
				var self = this;
				this.editor.busy( true );
				this.editor.enable( 0 ); // Disable all buttons
				this.saving = true;

				LAPI.Ajax.editPage(
					mw.config.get( 'wgPageName' ),
					function ( doc, editForm, failureFunc, revision_id ) {
						try {
							if ( revision_id && revision_id != mw.config.get( 'wgCurRevisionId' ) ) {
								// Page was edited since the user loaded it.
								throw new Error( '#Page version (revision ID) mismatch: edit conflict.' );
							}

							// Modify the page
							var textbox = editForm.wpTextbox1;
							if ( !textbox ) {
								throw new Error( '#Server replied with invalid edit page.' );
							}
							var pagetext = textbox.value;

							IA.setWikitext( pagetext );

							var span = null;
							if ( self.note.content ) { // Otherwise it's a new note!
								span = IA.findNote( pagetext, self.note.model.id );
							}

							if ( span ) { // Replace
								pagetext =
									pagetext.substring( 0, span.start ) +
									self.to_insert +
									pagetext.substring( span.end );
							} else { // If not found, append
								// Try to append right after existing notes
								var lastNote = pagetext.lastIndexOf( '{{ImageNoteEnd|id=' );
								if ( lastNote >= 0 ) {
									var endLastNote = pagetext.substring( lastNote ).indexOf( '}}' );
									if ( endLastNote < 0 ) {
										endLastNote = pagetext.substring( lastNote ).indexOf( '\n' );
										if ( endLastNote < 0 ) {
											lastNote = -1;
										} else {
											lastNote += endLastNote;
										}
									} else {
										lastNote += endLastNote + 2;
									}
								}

								if ( lastNote >= 0 ) {
									pagetext =
										pagetext.substring( 0, lastNote ) +
										'\n' + self.to_insert +
										pagetext.substring( lastNote );
								} else {
									pagetext = pagetext.trimRight() + '\n' + self.to_insert;
								}
							}

							textbox.value = pagetext;
							var summary = editForm.wpSummary;
							if ( !summary ) {
								throw new Error( '#Summary field not found. Check that edit pages have valid XHTML.' );
							}

							// If [[MediaWiki:Copyrightwarning]] is invalid XHTML, we may not have wpSummary!
							if ( self.note.content != null ) {
								IA.setSummary(
									summary,
									ImageAnnotator.UI.get( 'wpImageAnnotatorChangeSummary', true ) ||
										'[[MediaWiki talk:Gadget-ImageAnnotator.js|Changing image note]]$1',
									data
								);
							} else {
								IA.setSummary(
									summary,
									ImageAnnotator.UI.get( 'wpImageAnnotatorAddSummary', true ) ||
										'[[MediaWiki talk:Gadget-ImageAnnotator.js|Adding image note]]$1',
									data
								);
							}
						} catch ( ex ) {
							failureFunc( null, ex );
							return;
						}

						var edit_page = doc;
						LAPI.Ajax.submitEdit(
							editForm,
							function ( request ) {
								// After a successful submit.
								if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) {
									edit_page.dispose();
								}

								// TODO: Actually, the edit got through here, so calling failureFunc on
								// inconsistencies isn't quite right. Should we reload the page?
								var id = 'image_annotation_content_' + self.note.model.id;
								var doc = LAPI.Ajax.getHTML( request, failureFunc, id );
								if ( !doc ) {
									return;
								}

								var html = LAPI.$( id, doc );
								if ( !html ) {
									if ( doc.isFake && ( typeof doc.dispose === 'function' ) ) {
										doc.dispose();
									}
									failureFunc( request, new Error( '#Note not found after saving. Please reload the page.' ) );
									return;
								}

								var revision_id = LAPI.WP.revisionFromHtml( request.responseText );
								if ( !revision_id ) {
									if ( doc.isFake && ( typeof doc.dispose === 'function' ) ) {
										doc.dispose();
									}
									failureFunc( request, new Error( '#Version inconsistency after saving. Please reload the page.' ) );
									return;
								}

								mw.config.set( 'wgCurRevisionId', revision_id ); // Bump revision id!!

								self.note.model.html = LAPI.DOM.importNode( document, html, true );
								if ( doc.isFake && ( typeof doc.dispose === 'function' ) ) {
									doc.dispose();
								}
								self.note.model.dimension = dim; // record dimension
								self.note.model.html.style.display = '';
								self.note.model.wiki = data;
								self.editor.busy( false );

								if ( self.note.content ) {
									LAPI.DOM.removeChildren( self.note.content.main );
									self.note.content.main.appendChild( self.note.model.html );
								} else {
									// New note.
									self.note.display(); // Actually a misnomer. Just creates 'content'.
									if ( self.viewer.annotations.length > 1 ) {
										self.viewer.annotations.sort( ImageAnnotation.compare );
										var idxOfNote = Array.indexOf( self.viewer.annotations, self.note );
										if ( idxOfNote + 1 < self.viewer.annotations.length ) {
											LAPI.DOM.insertNode( self.note.view, self.viewer.annotations[ idxOfNote + 1 ].view );
										}
									}
								}

								self.to_insert = null;
								self.saving = false;

								if ( !self.note.tooltip ) {
									self.note.setTooltip();
								}

								self.hide_editor();
								IA.is_editing = false;
								self.editor.setText( data ); // In case the same note is re-opened: start new undo cycle
							},
							function ( request, ex ) {
								if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) {
									edit_page.dispose();
								}
								failureFunc( request, ex );
							}
						);
					},
					function ( request, ex ) {
						self.editor.busy( false );
						self.saving = false;

						// TODO: How and where to display error if user closed editor through ESC (or through
						// opening another tooltip) in the meantime?
						if ( !self.visible ) {
							return;
						}

						// Change the tooltip to show the error.
						self.editor.setText( self.to_insert );

						// Error message. Use preview field for this.
						var error_msg = ImageAnnotator.UI.get( 'wpImageAnnotatorSaveError', false );
						var lk = getElementsByClassName( error_msg, 'span', 'wpImageAnnotatorOwnPageLink' );
						if ( lk && lk.length && lk[ 0 ].firstChild.nodeName.toLowerCase() === 'a' ) {
							lk = lk[ 0 ].firstChild;
							lk.href = mw.config.get( 'wgServer' ) + mw.config.get( 'wgArticlePath' ).replace( '$1', encodeURIComponent( mw.config.get( 'wgPageName' ) ) ) + '?action=edit';
						}

						if ( ex ) {
							var ex_msg = LAPI.formatException( ex, true );
							if ( ex_msg ) {
								ex_msg.style.borderBottom = '1px solid red';
								var tmp = LAPI.make( 'div' );
								tmp.appendChild( ex_msg );
								tmp.appendChild( error_msg );
								error_msg = tmp;
							}
						}

						self.editor.setPreview( error_msg );
						self.editor.showPreview();
						self.editor.textarea.readOnly = true;

						// Force a light gray background, since IE has no visual readonly indication.
						self.editor.textarea.style.backgroundColor = '#EEEEEE';
						self.editor.enable( LAPI.Edit.CANCEL ); // Disable all other buttons
					}
				);
			},

			onpreview: function ( editor ) {
				if ( this.tooltip ) {
					this.tooltip.size_change();
				}
			},

			cancel: function ( editor ) {
				if ( !this.note ) {
					return;
				}

				if ( !this.note.content ) {
					// No content: Cancel and remove this note!
					this.note.destroy();
					this.note = null;
				}

				if ( editor ) {
					this.hide_editor();
				}
			},

			close_tooltip: function ( tooltip, evt ) {
				this.hide_editor( evt );
				this.cancel();
			}

		};

		var ImageNotesViewer = function () {
			this.initialize.apply( this, arguments );
		};

		ImageNotesViewer.prototype = {
			initialize: function ( descriptor, may_edit ) {
				Object.merge( descriptor, this );
				this.annotations = [];
				this.max_id = 0;
				this.main_div = null;
				this.msg = null;
				this.may_edit = may_edit;
				this.setup_done = false;
				this.tip = null;
				this.icon = null;
				this.factors = {
					dx: this.full_img.width / this.thumb.width,
					dy: this.full_img.height / this.thumb.height
				};

				if ( !this.isThumbnail && !this.isOther ) {
					this.setup();
				} else {
					// Normalize the namespace of the realName to 'File' to account for images possibly stored at
					// a foreign repository (the Commons). Otherwise a later information load might fail because
					// the link is local and might actually be given as "Bild:Foo.jpg". If that page doesn't exist
					// locally, we want to ask at the Commons about "File:Foo.jpg". The Commons doesn't understand
					// the localized namespace names of other wikis, but the canonical namespace name 'File' works
					// also locally.
					this.realName = 'File:' + this.realName.substring( this.realName.indexOf( ':' ) + 1 );
				}
			},

			setup: function ( onlyIcon ) {
				this.setup_done = true;
				var name = this.realName;
				if ( this.isThumbnail || this.scope == document || this.may_edit || !IA.haveAjax ) {
					this.imgName = this.realName;
					this.realName = '';
				} else {
					name = getElementsByClassName( this.scope, '*', 'wpImageAnnotatorFullName' );
					this.realName = ( ( name && name.length ) ? LAPI.DOM.getInnerText( name[ 0 ] ) : '' );
					this.imgName = this.realName;
				}

				var annotations = getElementsByClassName( this.scope, 'div', IA.annotation_class );

				if ( !this.may_edit && ( !annotations || annotations.length === 0 ) ) {
					// Nothing to do
					return;
				}

				// A div inserted around the image. It ensures that everything we add is positioned properly
				// over the image, even if the browser window size changes and re-layouts occur.
				var isEnabledImage = LAPI.DOM.hasClass( this.scope, 'wpImageAnnotatorEnable' );
				if ( !this.isThumbnail && !this.isOther && !isEnabledImage ) {
					this.img_div = LAPI.make( 'div', null, {
						position: 'relative',
						width: String( this.thumb.width ) + 'px'
					} );

					var floater = LAPI.make(
						'div', null,
						{
							cssFloat: ( IA.is_rtl ? 'right' : 'left' ),
							styleFloat: ( IA.is_rtl ? 'right' : 'left' ), // For IE...
							width: String( this.thumb.width ) + 'px',
							position: 'relative' // Fixes IE layout bugs...
						}
					);
					floater.appendChild( this.img_div );

					if ( this.img.parentNode && this.img.parentNode.parentNode ) {
						this.img.parentNode.parentNode.insertBefore( floater, this.img.parentNode );
					}

					this.img_div.appendChild( this.img.parentNode );

					// And now a clear:left to make the rest appear below the image, as usual.
					var breaker = LAPI.make( 'div', null, { clear: ( IA.is_rtl ? 'right' : 'left' ) } );
					LAPI.DOM.insertAfter( breaker, floater );

					// Remove spurious br tag.
					if ( breaker.nextSibling && breaker.nextSibling.nodeName.toLowerCase() == 'br' ) {
						LAPI.DOM.removeNode( breaker.nextSibling );
					}
				} else if ( this.isOther || isEnabledImage ) {
					this.img_div = LAPI.make( 'div', null, {
						position: 'relative',
						width: String( this.thumb.width ) + 'px'
					} );

					if ( this.img.parentNode && this.img.parentNode.parentNode ) {
						this.img.parentNode.parentNode.insertBefore( this.img_div, this.img.parentNode );
					}

					this.img_div.appendChild( this.img.parentNode );

					// Insert one more to have a file_div, so that we can align the message text correctly
					this.file_div = LAPI.make( 'div', null, { width: String( this.thumb.width ) + 'px' } );
					this.img_div.parentNode.insertBefore( this.file_div, this.img_div );
					this.file_div.appendChild( this.img_div );
				} else { // Thumbnail
					this.img_div = LAPI.make(
						'div',
						{ className: 'thumbimage' },
						{
							position: 'relative',
							width: String( this.thumb.width ) + 'px'
						}
					);
					this.img.parentNode.parentNode.insertBefore( this.img_div, this.img.parentNode );
					this.img.style.border = 'none';
					this.img_div.appendChild( this.img.parentNode );
				}
				if (
					( this.isThumbnail || this.isOther ) && !this.may_edit &&
					(
						onlyIcon ||
						this.iconOnly ||
						ImageAnnotator_config.inlineImageUsesIndicator(
							name, this.isLocal, this.thumb, this.full_img, annotations.length, this.isThumbnail
						)
					)
				) {
					// Use an onclick handler instead of a link around the image. The link may have a default white
					// background, but we want to be sure to have transparency. The image should be an 8-bit indexed
					// PNG or a GIF and have a transparent background.
					this.icon = ImageAnnotator.UI.get( 'wpImageAnnotatorIndicatorIcon', false );
					if ( this.icon ) {
						// Skip the message container span or div
						this.icon = this.icon.firstChild;
					}

					// Guard against misconfigurations
					if (
						this.icon &&
						this.icon.nodeName.toLowerCase() == 'a' &&
						this.icon.firstChild.nodeName.toLowerCase() == 'img'
					) {
						// Make sure we use the right protocol:
						var srcFixed = this.icon.firstChild.getAttribute( 'src', 2 ).replace( /^https?:/, document.location.protocol );
						this.icon.firstChild.src = srcFixed;
						this.icon.firstChild.title = this.icon.title;
						this.icon = this.icon.firstChild;
					} else if ( !this.icon || this.icon.nodeName.toLowerCase() !== 'img' ) {
						this.icon = LAPI.DOM.makeImage(
							IA.indication_icon,
							14, 14,
							ImageAnnotator.UI.get( 'wpImageAnnotatorHasNotesMsg', true ) || ''
						);
					}

					Object.merge(
						{
							position: 'absolute',
							zIndex: 1000,
							top: '0px',
							cursor: 'pointer'
						},
						this.icon.style
					);

					this.icon.onclick = ( function () {
						location.href = this.img.parentNode.href;
					} ).bind( this );

					if ( IA.is_rtl ) {
						this.icon.style.right = '0px';
					} else {
						this.icon.style.left = '0px';
					}

					this.img_div.appendChild( this.icon );

					// And done. We just show the icon, no fancy event handling needed.
					return;
				}

				// Set colors
				var colors = IA.getRawItem( 'colors', this.scope );
				this.outer_border =
					colors && IA.getItem( 'outer', colors ) || IA.outer_border;
				this.inner_border =
					colors && IA.getItem( 'inner', colors ) || IA.inner_border;
				this.active_border =
					colors && IA.getItem( 'active', colors ) || IA.active_border;

				if ( annotations ) {
					for ( var i = 0; i < annotations.length; i++ ) {
						var id = annotations[ i ].id;

						if ( id && /^image_annotation_note_(\d+)$/.test( id ) ) {
							id = parseInt( id.substring( 'image_annotation_note_'.length ) );
						} else {
							id = null;
						}

						if ( id ) {
							if ( id > this.max_id ) {
								this.max_id = id;
							}

							var w = IA.getIntItem( 'full_width_' + id, this.scope );
							var h = IA.getIntItem( 'full_height_' + id, this.scope );
							if (
								w == this.full_img.width && h == this.full_img.height &&
								!Array.exists( this.annotations, function ( note ) {
									return note.model.id == id;
								} )
							) {
								try {
									this.register( new ImageAnnotation( annotations[ i ], this, id ) );
								} catch ( ex ) {
									// Swallow.
								}
							}
						}
					}
				}

				if ( this.annotations.length > 1 ) {
					this.annotations.sort( ImageAnnotation.compare );
				}

				// Add the rectangles of existing notes to the DOM now that they are sorted.
				Array.forEach( this.annotations, ( function ( note ) {
					this.img_div.appendChild( note.view );
				} ).bind( this ) );

				if ( this.isThumbnail ) {
					this.main_div = getElementsByClassName( this.file_div, 'div', 'thumbcaption' );
					if ( !this.main_div || this.main_div.length == 0 ) {
						this.main_div = null;
					} else {
						this.main_div = this.main_div[ 0 ];
					}
				}

				if ( !this.main_div ) {
					this.main_div = LAPI.make( 'div' );
					if ( IA.is_rtl ) {
						this.main_div.style.direction = 'rtl';
						this.main_div.style.textAlign = 'right';
						this.main_div.className = 'rtl';
					} else {
						this.main_div.style.textAlign = 'left';
					}

					if ( !this.isThumbnail && !this.isOther && !isEnabledImage ) {
						LAPI.DOM.insertAfter( this.main_div, this.file_div );
					} else {
						LAPI.DOM.insertAfter( this.main_div, this.img_div );
					}
				}

				if (
					!( this.isThumbnail || this.isOther ) ||
					!this.noCaption &&
					!IA.hideCaptions &&
					ImageAnnotator_config.displayCaptionInArticles(
						name, this.isLocal, this.thumb, this.full_img, annotations.length, this.isThumbnail
					)
				) {
					this.msg = LAPI.make( 'div', null, { display: 'none' } );

					if ( IA.is_rtl ) {
						this.msg.style.direction = 'rtl';
						this.msg.className = 'rtl';
					}

					if ( this.isThumbnail ) {
						this.msg.style.fontSize = '90%';
					}

					this.main_div.appendChild( this.msg );
				}

				// Set overflow parents, if any
				var simple = !!window.getComputedStyle;
				var checks = ( simple ? [ 'overflow', 'overflow-x', 'overflow-y' ] :
					[ 'overflow', 'overflowX', 'overflowY' ]
				);
				var curStyle = null;
				for ( var up = this.img.parentNode && this.img.parentNode.parentNode; up != document.body; up = up.parentNode ) {
					curStyle = ( simple ? window.getComputedStyle( up, null ) : ( up.currentStyle || up.style ) );
					// "up.style" is actually incorrect, but a best-effort fallback.
					var overflow = Array.any( checks, function ( t ) {
						var o = curStyle[ t ];
						return ( o && o != 'visible' ) ? o : null;
					} );
					if ( overflow ) {
						if ( !this.overflowParents ) {
							this.overflowParents = [ up ];
						} else {
							this.overflowParents[ this.overflowParents.length ] = up;
						}
					}
				}

				if ( this.overflowParents && this.may_edit ) {
					// Forbid editing if we have such a crazy layout.
					this.may_edit = false;
					IA.may_edit = false;
				}

				this.show_evt = LAPI.Evt.makeListener( this, this.show );

				if ( this.overflowParents || LAPI.Browser.is_ie ) {
					// If we have overflowParents, also use a mousemove listener to show/hide the whole
					// view (FF doesn't send mouseout events if the visible border is still within the image, i.e.,
					// if not the whole image is visible). On IE, also use this handler to show/hide the notes
					// if we're still within the visible area of the image. IE passes through mouse_over events to
					// the img even if the mouse is within a note's rectangle. Apparently is doesn't handle
					// transparent divs correctly. As a result, the notes will pop up and disappear only when the
					// mouse crosses the border, and if one moves the mouse a little fast across the border, we
					// don't get any event at all. That's no good.
					this.move_evt = LAPI.Evt.makeListener( this, this.check_hide );
				} else {
					this.hide_evt = LAPI.Evt.makeListener( this, this.hide );
				}

				this.move_listening = false;
				this.setShowHideEvents( true );
				this.visible = false;
				this.setDefaultMsg();
			},

			cannotEdit: function () {
				if ( !this.may_edit ) {
					return;
				}

				this.may_edit = false;
				Array.forEach( this.annotations, function ( note ) {
					note.cannotEdit();
				} );
			},

			setShowHideEvents: function ( set ) {
				if ( this.icon ) {
					return;
				}

				if ( set ) {
					LAPI.Evt.attach( this.img, IA.mouse_in, this.show_evt );
					if ( this.hide_evt ) {
						LAPI.Evt.attach( this.img, IA.mouse_out, this.hide_evt );
					}
				} else {
					LAPI.Evt.remove( this.img, IA.mouse_in, this.show_evt );
					if ( this.hide_evt ) {
						LAPI.Evt.remove( this.img, IA.mouse_out, this.hide_evt );
					} else if ( this.move_listening ) {
						this.removeMoveListener();
					}
				}
			},

			removeMoveListener: function () {
				if ( this.icon ) {
					return;
				}

				this.move_listening = false;

				if ( this.move_evt ) {
					if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) {
						document.captureEvents( null );
					}

					LAPI.Evt.remove( document, 'mousemove', this.move_evt, true );
				}
			},

			adjustRectangleSize: function ( node ) {
				if ( this.icon ) {
					return;
				}

				// Make sure the note boxes don't overlap the image boundary; we might get an event
				// loop otherwise if the mouse was just on that overlapped boundary, resulting in flickering.
				var view_x = node.offsetLeft;
				var view_y = node.offsetTop;
				var view_w = node.offsetWidth;
				var view_h = node.offsetHeight;

				if ( view_x === 0 ) {
					view_x = 1;
				}

				if ( view_y === 0 ) {
					view_y = 1;
				}

				if ( view_x + view_w >= this.thumb.width ) {
					view_w = this.thumb.width - view_x - 1;
					if ( view_w <= 4 ) {
						view_w = 4;
						view_x = this.thumb.width - view_w - 1;
					}
				}

				if ( view_y + view_h >= this.thumb.height ) {
					view_h = this.thumb.height - view_y - 1;
					if ( view_h <= 4 ) {
						view_h = 4;
						view_y = this.thumb.height - view_h - 1;
					}
				}

				// Now set position and width and height, subtracting cumulated border widths
				if (
					view_x != node.offsetLeft || view_y != node.offsetTop ||
					view_w != node.offsetWidth || view_h != node.offsetHeight
				) {
					node.style.top = String( view_y ) + 'px';
					node.style.left = String( view_x ) + 'px';
					node.style.width = String( view_w - 2 ) + 'px';
					node.style.height = String( view_h - 2 ) + 'px';
					node.firstChild.style.width = String( view_w - 4 ) + 'px';
					node.firstChild.style.height = String( view_h - 4 ) + 'px';
				}
			},

			toggle: function ( dummies ) {
				var i;

				if ( !this.annotations || this.annotations.length === 0 || this.icon ) {
					return;
				}

				if ( dummies ) {
					for ( i = 0; i < this.annotations.length; i++ ) {
						this.annotations[ i ].view.style.display = 'none';

						if ( this.visible && this.annotations[ i ].tooltip ) {
							this.annotations[ i ].tooltip.hide_now( null );
						}

						this.annotations[ i ].dummy.style.display = ( this.visible ? 'none' : '' );

						if ( !this.visible ) {
							this.adjustRectangleSize( this.annotations[ i ].dummy );
						}
					}
				} else {
					for ( i = 0; i < this.annotations.length; i++ ) {
						this.annotations[ i ].dummy.style.display = 'none';
						this.annotations[ i ].view.style.display = ( this.visible ? 'none' : '' );

						if ( !this.visible ) {
							this.adjustRectangleSize( this.annotations[ i ].view );
						}

						if ( this.visible && this.annotations[ i ].tooltip ) {
							this.annotations[ i ].tooltip.hide_now( null );
						}
					}
				}

				this.visible = !this.visible;
			},

			show: function ( evt ) {
				if ( this.visible || this.icon ) {
					return;
				}

				this.toggle( IA.is_adding || IA.is_editing );

				if ( this.move_evt && !this.move_listening ) {
					LAPI.Evt.attach( document, 'mousemove', this.move_evt, true );

					this.move_listening = true;

					if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) {
						document.captureEvents( Event.MOUSEMOVE );
					}
				}
			},

			hide: function ( evt ) {
				if ( this.icon ) {
					return true;
				}

				if ( !this.visible ) {
					// Huh?
					if ( this.move_listening ) {
						this.removeMoveListener();
					}

					return true;
				}

				if ( evt ) {
					var mouse_pos = LAPI.Pos.mousePosition( evt );
					if ( mouse_pos ) {
						if ( this.tip ) {
							// Check whether we're within the visible note.
							if ( LAPI.Pos.isWithin( this.tip.popup, mouse_pos.x, mouse_pos.y ) ) {
								return true;
							}
						}

						var is_within = true;
						var img_pos = LAPI.Pos.position( this.img );
						var rect = {
							x: img_pos.x,
							y: img_pos.y,
							r: ( img_pos.x + this.img.offsetWidth ),
							b: ( img_pos.y + this.img.offsetHeight )
						};
						var i;

						if ( this.overflowParents ) {
							// We're within some elements having overflow:hidden or overflow:auto or overflow:scroll set.
							// Compute the actually visible region by intersecting the rectangle given by img_pos and
							// this.img.offsetWidth, this.img.offsetTop with the rectangles of all overflow parents.

							function intersect_rectangles( a, b ) {
								if ( b.x > a.r || b.r < a.x || b.y > a.b || b.b < a.y ) {
									return { x: 0, y: 0, r: 0, b: 0 };
								}

								return {
									x: Math.max( a.x, b.x ),
									y: Math.max( a.y, b.y ),
									r: Math.min( a.r, b.r ),
									b: Math.min( a.b, b.b )
								};
							}

							for ( i = 0; i < this.overflowParents.length && rect.x < rect.r && rect.y < rect.b; i++ ) {
								img_pos = LAPI.Pos.position( this.overflowParents[ i ] );
								img_pos.r = img_pos.x + this.overflowParents[ i ].clientWidth;
								img_pos.b = img_pos.y + this.overflowParents[ i ].clientHeight;
								rect = intersect_rectangles( rect, img_pos );
							}
						}

						is_within = !( rect.x >= rect.r || rect.y >= rect.b || // Empty rectangle
							rect.x >= mouse_pos.x || rect.r <= mouse_pos.x ||
							rect.y >= mouse_pos.y || rect.b <= mouse_pos.y
						);
						if ( is_within ) {
							if ( LAPI.Browser.is_ie && evt.type === 'mousemove' ) {
								var display;
								// Loop in reverse order to properly display top rectangle's note!
								for ( i = this.annotations.length - 1; i >= 0; i-- ) {
									display = this.annotations[ i ].view.style.display;
									if (
										display !== 'none' && display != null &&
										LAPI.Pos.isWithin( this.annotations[ i ].view.firstChild, mouse_pos.x, mouse_pos.y )
									) {
										if ( !this.annotations[ i ].tooltip.visible ) { this.annotations[ i ].tooltip.show( evt ); }
										return true;
									}
								}

								if ( this.tip ) {
									// Inside the image, but not within any note rectangle
									this.tip.hide_now();
								}

							}

							return true;
						}
					}
				}

				// Not within the image, or forced hiding (no event)
				if ( this.move_listening ) {
					this.removeMoveListener();
				}

				this.toggle( IA.is_adding || IA.is_editing );

				return true;
			},

			check_hide: function ( evt ) {
				if ( this.icon ) {
					return true;
				}

				if ( this.visible ) {
					this.hide( evt );
				}

				return true;
			},

			register: function ( new_note ) {
				this.annotations[ this.annotations.length ] = new_note;
				if ( new_note.model.id > 0 ) {
					if ( new_note.model.id > this.max_id ) {
						this.max_id = new_note.model.id;
					}
				} else {
					new_note.model.id = ++this.max_id;
				}
			},

			deregister: function ( note ) {
				Array.remove( this.annotations, note );

				if ( note.model.id == this.max_id ) {
					this.max_id--;
				}

				if ( this.annotations.length === 0 ) {
					// If we removed the last one, clear the msg
					this.setDefaultMsg();
				}
			},

			setDefaultMsg: function () {
				if ( this.annotations && this.annotations.length && this.msg ) {
					LAPI.DOM.removeChildren( this.msg );
					this.msg.appendChild( ImageAnnotator.UI.get( 'wpImageAnnotatorHasNotesMsg', false ) );

					if ( this.realName && typeof this.realName === 'string' && this.realName.length ) {
						var otherPageMsg = ImageAnnotator.UI.get( 'wpImageAnnotatorEditNotesMsg', false );
						if ( otherPageMsg ) {
							var lk = otherPageMsg.getElementsByTagName( 'a' );
							if ( lk && lk.length ) {
								lk = lk[ 0 ];
								lk.parentNode.replaceChild(
									LAPI.DOM.makeLink(
										mw.config.get( 'wgArticlePath' ).replace( '$1', encodeURIComponent( this.realName ) ),
										this.realName,
										this.realName
									),
									lk
								);
								this.msg.appendChild( otherPageMsg );
							}
						}
					}

					this.msg.style.display = '';
				} else {
					if ( this.msg ) {
						this.msg.style.display = 'none';
					}
				}

				if ( IA.button_div && this.may_edit ) {
					IA.button_div.style.display = '';
				}
			}

		};

		var IA = {
			// This object is responsible for setting up annotations when a page is loaded. It loads all
			// annotations in the page source, and adds an "Annotate this image" button plus the support
			// for drawing rectangles onto the image if there is only one image and editing is allowed.

			haveAjax: false,

			button_div: null,
			add_button: null,

			cover: null,
			border: null,
			definer: null,

			mouse_in: ( window.ActiveXObject ? 'mouseenter' : 'mouseover' ),
			mouse_out: ( window.ActiveXObject ? 'mouseleave' : 'mouseout' ),

			annotation_class: 'image_annotation',

			// Format of notes in Wikitext. Note: there are two formats, an old one and a new one.
			// We only write the newest (last) one, but we can read also the older formats. Order is
			// important, because the old format also used the ImageNote template, but for a different
			// purpose.
			note_delim: [
				{
					start: '<div id="image_annotation_note_$1"',
					end: '</div><!-- End of annotation $1-->',
					content_start: '<div id="image_annotation_content_$1">\n',
					content_end: '</div>\n<span id="image_annotation_wikitext_$1"'
				},
				{
					start: '{{ImageNote|id=$1',
					end: '{{ImageNoteEnd|id=$1}}',
					content_start: '}}\n',
					content_end: '{{ImageNoteEnd|id=$1}}'
				}
			],

			tooltip_styles: { // The style for all our tooltips
				border: '1px solid #8888aa',
				backgroundColor: '#ffffe0',
				padding: '0.3em',
				fontSize: ( ( mw.config.get( 'skin' ) == 'monobook' || mw.config.get( 'skin' ) == 'modern' ) ? '127%' : '100%' )
				// Scale up to default text size
			},

			editor: null,

			wiki_read: false,
			is_rtl: false,

			move_listening: false,
			is_tracking: false,
			is_adding: false,
			is_editing: false,

			zoom_threshold: 8.0,
			zoom_factor: 4.0,

			install_attempts: 0,
			max_install_attempts: 10, // Maximum 5 seconds

			imgs_with_notes: [],
			thumbs: [],
			other_images: [],

			// Fallback
			indication_icon: '//upload.wikimedia.org/wikipedia/commons/8/8a/Gtk-dialog-info-14px.png',

			install: function ( config ) {
				if ( typeof ImageAnnotator_disable !== 'undefined' && !!ImageAnnotator_disable ) {
					return;
				}

				if ( !config || ImageAnnotator_config != null ) {
					return;
				}

				// Double check.
				if ( !config.viewingEnabled() ) {
					return;
				}

				var self = IA;
				ImageAnnotator_config = config;

				// Determine whether we have XmlHttp. We try to determine this here to be able to avoid
				// doing too much work.
				if (
					window.XMLHttpRequest &&
					typeof LAPI !== 'undefined' &&
					typeof LAPI.Ajax !== 'undefined' &&
					typeof LAPI.Ajax.getRequest !== 'undefined'
				) {
					self.haveAjax = ( LAPI.Ajax.getRequest() != null );
					self.ajaxQueried = true;
				} else {
					self.haveAjax = true; // A pity. May occur on IE. We'll check again later on.
					self.ajaxQueried = false;
				}

				// We'll include self.haveAjax later on once more, to catch the !ajaxQueried case.
				self.may_edit = mw.config.get( 'wgNamespaceNumber' ) >= 0 && mw.config.get( 'wgArticleId' ) > 0 && self.haveAjax && config.editingEnabled();

				function namespaceCheck( list ) {
					if ( !list || Object.prototype.toString.call( list ) !== '[object Array]' ) {
						return false;
					}

					var namespaceIds = mw.config.get( 'wgNamespaceIds' );
					if ( !namespaceIds ) {
						return false;
					}

					var namespaceNumber = mw.config.get( 'wgNamespaceNumber' );
					for ( var i = 0; i < list.length; i++ ) {
						if (
							typeof list[ i ] === 'string' &&
							(
								list[ i ] === '*' ||
								namespaceIds[ list[ i ].toLowerCase().replace( / /g, '_' ) ] === namespaceNumber
							)
						) {
							return true;
						}
					}

					return false;
				}

				self.rules = {
					inline: {},
					thumbs: {},
					shared: {}
				};

				// Now set the default rules. Undefined means default setting (true for show, false for icon),
				// but overrideable by per-image rules. If set, it's not overrideable by per-image rules.
				//
				if (
					!self.haveAjax ||
					!config.generalImagesEnabled() ||
					namespaceCheck( window.ImageAnnotator_no_images || null )
				) {
					self.rules.inline.show = false;
					self.rules.thumbs.show = false;
					self.rules.shared.show = false;
				} else {
					if (
						!self.haveAjax ||
						!config.thumbsEnabled() ||
						namespaceCheck( window.ImageAnnotator_no_thumbs || null )
					) {
						self.rules.thumbs.show = false;
					}

					if ( mw.config.get( 'wgNamespaceNumber' ) == 6 ) {
						self.rules.shared.show = true;
					} else if ( !config.sharedImagesEnabled() ||
						namespaceCheck( window.ImageAnnotator_no_shared || null )
					) {
						self.rules.shared.show = false;
					}

					if ( namespaceCheck( window.ImageAnnotator_icon_images || null ) ) {
						self.rules.inline.icon = true;
					}

					if ( namespaceCheck( window.ImageAnnotator_icon_thumbs || null ) ) {
						self.rules.thumbs.icon = true;
					}
				}

				// User rule for displaying captions on images in articles
				self.hideCaptions = namespaceCheck( window.ImageAnnotator_hide_captions || null );

				var do_images = typeof self.rules.inline.show === 'undefined' || self.rules.inline.show;

				if ( do_images ) {
					// Per-article switching off of note display on inline images and thumbnails
					var rules = document.getElementById( 'wpImageAnnotatorImageRules' );
					if ( rules ) {
						if ( rules.className.indexOf( 'wpImageAnnotatorNone' ) >= 0 ) {
							self.rules.inline.show = false;
							self.rules.thumbs.show = false;
							self.rules.shared.show = false;
						}

						if (
							typeof self.rules.inline.show === 'undefined' &&
							rules.className.indexOf( 'wpImageAnnotatorDisplay' ) >= 0
						) {
							self.rules.inline.show = true;
						}

						if ( rules.className.indexOf( 'wpImageAnnotatorNoThumbDisplay' ) >= 0 ) {
							self.rules.thumbs.show = false;
						}

						if (
							typeof self.rules.thumbs.show === 'undefined' &&
							rules.className.indexOf( 'wpImageAnnotatorThumbDisplay' ) >= 0
						) {
							self.rules.thumbs.show = true;
						}

						if ( rules.className.indexOf( 'wpImageAnnotatorInlineDisplayIcons' ) >= 0 ) {
							self.rules.inline.icon = true;
						}

						if ( rules.className.indexOf( 'wpImageAnnotatorThumbDisplayIcons' ) >= 0 ) {
							self.rules.thumbs.icon = true;
						}

						if ( rules.className.indexOf( 'wpImageAnnotatorOnlyLocal' ) >= 0 ) {
							self.rules.shared.show = false;
						}
					}
				}

				// Make sure the shared value is set
				self.rules.shared.show = typeof self.rules.shared.show === 'undefined' || self.rules.shared.show;

				var do_thumbs = typeof self.rules.thumbs.show === 'undefined' || self.rules.thumbs.show;

				if ( do_images ) {
					var bodyContent = document.getElementById( 'bodyContent' ) || // monobook, vector
						document.getElementById( 'mw_contentholder' ) || // modern
						document.getElementById( 'article' ); // old skins

					if ( bodyContent ) {
						var all_imgs = bodyContent.getElementsByTagName( 'img' );

						// This prevents traversing a page with more than 400 images
						// There are extreme cases like [[Emoji]] that high number of images can cause
						// huge lag specially on Chrome
						if ( all_imgs.length > 400 ) {
							// purging the array, simply a hack to avoid more indention
							all_imgs = [];
						}

						for ( var i = 0; i < all_imgs.length; i++ ) {
							// Exclude all that are in img_with_notes or in thumbs. Also exclude all in galleries.
							var up = all_imgs[ i ].parentNode;
							if ( up.nodeName.toLowerCase() !== 'a' ) {
								continue;
							}

							up = up.parentNode;
							if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) {
								continue;
							}

							if ( ( ' ' + up.className + ' ' ).indexOf( ' thumbinner ' ) >= 0 ) {
								if ( do_thumbs ) {
									self.thumbs[ self.thumbs.length ] = up;
								}
								continue;
							}

							up = up.parentNode;
							if ( !up ) {
								continue;
							}

							if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) {
								continue;
							}

							if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorEnable ' ) >= 0 ) {
								self.imgs_with_notes[ self.imgs_with_notes.length ] = up;
								continue;
							}

							up = up.parentNode;
							if ( !up ) {
								continue;
							}

							// Other images not in galleries
							if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) {
								continue;
							}

							if ( ( ' ' + up.className + ' ' ).indexOf( ' gallerybox ' ) >= 0 ) {
								continue;
							}

							if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorEnable ' ) >= 0 ) {
								self.imgs_with_notes[ self.imgs_with_notes.length ] = up;
								continue;
							}

							up = up.parentNode;
							if ( !up ) {
								continue;
							}

							if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) {
								continue;
							}

							if ( ( ' ' + up.className + ' ' ).indexOf( ' gallerybox ' ) >= 0 ) {
								continue;
							}

							if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorEnable ' ) >= 0 ) {
								self.imgs_with_notes[ self.imgs_with_notes.length ] = up;
							} else {
								// Guard against other scripts adding aribtrary numbers of divs (dshuf for instance!)
								var is_other = true;
								while ( up && up.nodeName.toLowerCase() == 'div' && is_other ) {
									up = up.parentNode;
									if ( up ) {
										is_other = ( ' ' + up.className + ' ' ).indexOf( ' gallerybox ' ) < 0;
									}
								}

								if ( is_other ) {
									self.other_images[ self.other_images.length ] = all_imgs[ i ];
								}
							}
						} // end loop
					}
				} else {
					self.imgs_with_notes = getElementsByClassName( document, '*', 'wpImageAnnotatorEnable' );
					if ( do_thumbs ) {
						// No galleries!
						self.thumbs = getElementsByClassName( document, 'div', 'thumbinner' );
					}
				}

				if (
					mw.config.get( 'wgNamespaceNumber' ) == 6 ||
					( self.imgs_with_notes.length ) ||
					( self.thumbs.length ) ||
					( self.other_images.length )
				) {
					// Publish parts of config.
					ImageAnnotator.UI = config.UI;
					self.outer_border = config.outer_border;
					self.inner_border = config.inner_border;
					self.active_border = config.active_border;
					self.new_border = config.new_border;
					self.wait_for_required_libraries();
				}
			},

			wait_for_required_libraries: function () {
				if ( typeof Tooltip === 'undefined' || typeof LAPI === 'undefined' || typeof LAPI.Ajax === 'undefined' || typeof LAPI.DOM === 'undefined' ) {
					if ( IA.install_attempts++ < IA.max_install_attempts ) {
						setTimeout( IA.wait_for_required_libraries, 500 ); // 0.5 sec.
					}
					return;
				}

				if ( LAPI.Browser && LAPI.Browser.is_opera && !LAPI.Browser.is_opera_ge_9 ) {
					// Opera 8 has severe problems
					return;
				}

				// Get the UI. We're likely to need it if we made it to here.
				IA.setup_ui();
				IA.setup();
			},

			setup: function () {
				var self = IA;
				self.imgs = [];

				// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
				self.is_rtl =
					LAPI.DOM.hasClass( document.body, 'rtl' ) ||
					(
						LAPI.DOM.currentStyle && // Paranoia: added recently, not everyone might have it
						LAPI.DOM.currentStyle( document.body, 'direction' ) == 'rtl'
					);

				var stylepath = mw.config.get( 'stylepath' ) || '/skin';

				// Use this to temporarily display an image off-screen to get its dimensions
				var testImgDiv = LAPI.make(
					'div', null,
					{
						display: 'none',
						position: 'absolute',
						width: '300px',
						overflow: 'hidden',
						overflowX: 'hidden',
						left: '-10000px'
					}
				);
				document.body.insertBefore( testImgDiv, document.body.firstChild );

				function img_check( img, is_other ) {
					var srcW = parseInt( img.getAttribute( 'width', 2 ), 10 );
					var srcH = parseInt( img.getAttribute( 'height', 2 ), 10 );

					// Don't do anything on extremely small previews. We need some minimum extent to be able to place
					// rectangles after all...
					if ( !srcW || !srcH || srcW < 20 || srcH < 20 ) {
						return null;
					}

					// For non-thumbnail images, the size limit is larger.
					if ( is_other && ( srcW < 60 || srcH < 60 ) ) {
						return null;
					}

					var w = img.clientWidth; // Don't use offsetWidth, thumbnails may have a boundary...
					var h = img.clientHeight;

					// If the image is currently hidden, its clientWidth and clientHeight are not set. Try
					// harder to get the true width and height:
					if ( ( !w || !h ) && img.style.display != 'none' ) {
						var copied = img.cloneNode( true );
						copied.style.display = '';
						testImgDiv.appendChild( copied );
						testImgDiv.style.display = '';
						w = copied.clientWidth;
						h = copied.clientHeight;
						testImgDiv.style.display = 'none';
						LAPI.DOM.removeNode( copied );
					}

					// Quit if the image wasn't loaded properly for some reason:
					if ( w != srcW || h != srcH ) {
						return null;
					}

					// Exclude system images
					if ( img.src.contains( stylepath ) ) {
						return null;
					}

					// Only if within a link
					if ( img && img.parentNode && img.parentNode.nodeName.toLowerCase() != 'a' ) {
						return null;
					}

					if ( is_other ) {
						// Only if the img-within-link construction is within some element that may contain a div!
						if ( /^(p|span)$/i.test( img.parentNode && img.parentNode.parentNode.nodeName ) ) {
							// Special case: a paragraph may contain only inline elements, but we want to be able to handle
							// files in single paragraphs. Maybe we need to properly split the paragraph and wrap the image
							// in a div, but for now we assume that all browsers can handle a div within a paragraph or
							// a span in a meaningful way, even if that is not really allowed.
						} else if ( !/^(object|applet|map|fieldset|noscript|iframe|body|div|li|dd|blockquote|center|ins|del|button|th|td|form)$/i.test( img.parentNode && img.parentNode.parentNode.nodeName ) ) {
							return null;
						}
					}

					// Exclude any that are within an image note!
					var up = img.parentNode && img.parentNode.parentNode;
					while ( up && up != document.body ) {
						if ( LAPI.DOM.hasClass( up, IA.annotation_class ) ) {
							return null;
						}
						up = up.parentNode;
					}

					return {
						width: w,
						height: h
					};
				}

				function setup_one( scope ) {
					var file_div = scope;
					var is_thumb =
						scope != document &&
						scope.nodeName.toLowerCase() == 'div' &&
						LAPI.DOM.hasClass( scope, 'thumbinner' );
					var is_other = scope.nodeName.toLowerCase() == 'img';

					if ( is_other && self.imgs.length && scope == self.imgs[ 0 ] ) {
						return null;
					}

					if ( scope == document ) {
						file_div = LAPI.$( 'file' );
					} else if ( !is_thumb && !is_other ) {
						file_div = getElementsByClassName( scope, 'div', 'wpImageAnnotatorFile' );
						if ( !file_div || file_div.length != 1 ) {
							return null;
						}
						file_div = file_div[ 0 ];
					}

					if ( !file_div ) {
						return null;
					}

					var img = null;
					if ( scope == document ) {
						img = LAPI.WP.getPreviewImage( mw.config.get( 'wgTitle' ) );

						// TIFFs may be multi-paged: allow only for single-page TIFFs
						if ( document.pageselector ) {
							img = null;
						}
					} else if ( is_other ) {
						img = scope;
					} else {
						img = file_div.getElementsByTagName( 'img' );
						if ( !img || img.length === 0 ) {
							return null;
						}

						img = img[ 0 ];
					}

					if ( !img ) {
						return null;
					}

					var dim = img_check( img, is_other );
					if ( !dim ) {
						return null;
					}

					// Conditionally exclude shared images.
					if (
						scope != document &&
						!self.rules.shared.show &&
						ImageAnnotator_config.imageIsFromSharedRepository( img.src )
					) {
						return null;
					}

					var name = null;
					if ( scope == document ) {
						name = mw.config.get( 'wgPageName' );
					} else {
						name = LAPI.WP.pageFromLink( img.parentNode );
						if ( !name ) {
							return null;
						}

						name = name.replace( / /g, '_' );
						if ( is_thumb || is_other ) {
							var img_src = decodeURIComponent( img.getAttribute( 'src', 2 ) ).replace( / /g, '_' );

							// img_src should have a component "/name" in there somewhere
							var colon = name.indexOf( ':' );
							if ( colon <= 0 ) {
								return null;
							}

							var img_name = name.substring( colon + 1 );
							if ( img_src.search( new RegExp( '/' + img_name.escapeRE() + '(/.*)?$' ) ) < 0 ) {
								return null;
							}
							// If the link is not going to file namespace, we won't find the full size later on and
							// thus we won't do anything with it.
						}
					}

					// Only PNG, JPE?G, GIF, SVG, TIFF?, and WebP
					if ( name.search( /\.(jpe?g|png|gif|svg|tiff?|webp)$/i ) < 0 ) {
						return null;
					}

					// Finally check for wpImageAnnotatorControl
					var icon_only = false;
					var no_caption = false;
					if ( is_thumb || is_other ) {
						var up = img.parentNode && img.parentNode.parentNode;

						// Three levels is sufficient: thumbinner-thumb-control, or floatnone-center-control, or direct
						for ( var i = 0; ++i <= 3 && up; up = up.parentNode ) {
							if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorControl' ) ) {
								if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorOff' ) ) {
									return null;
								}

								if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorIconOnly' ) ) {
									icon_only = true;
								}

								if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorCaptionOff' ) ) {
									no_caption = true;
								}

								break;
							}
						}
					}

					return {
						scope: scope,
						file_div: file_div,
						img: img,
						realName: name,
						isThumbnail: is_thumb,
						isOther: is_other,
						thumb: {
							width: dim.width,
							height: dim.height
						},
						iconOnly: icon_only,
						noCaption: no_caption
					};
				}

				function setup_images( list ) {
					Array.forEach( list,
						function ( elem ) {
							var desc = setup_one( elem );
							if ( desc ) {
								self.imgs[ self.imgs.length ] = desc;
							}
						}
					);
				}

				if ( mw.config.get( 'wgNamespaceNumber' ) == 6 ) {
					setup_images( [ document ] );
					self.may_edit = self.may_edit && ( self.imgs.length == 1 );
					setup_images( self.imgs_with_notes );
				} else {
					setup_images( self.imgs_with_notes );
					self.may_edit = self.may_edit && ( self.imgs.length == 1 );
				}

				self.may_edit = self.may_edit && location.href.search( /[?&]oldid=/ ) < 0;

				if ( self.haveAjax ) {
					setup_images( self.thumbs );
					setup_images( self.other_images );
				}

				// Remove the test div
				LAPI.DOM.removeNode( testImgDiv );

				if ( self.imgs.length === 0 ) {
					return;
				}

				// We get the UI texts in parallel, but wait for them at the beginning of complete_setup, where we
				// need them. This has in particular a benefit if we do have to query for the file sizes below.

				if ( self.imgs.length == 1 && self.imgs[ 0 ].scope == document && !self.haveAjax ) {
					// Try to get the full size without Ajax.
					self.imgs[ 0 ].full_img = LAPI.WP.fullImageSizeFromPage();
					if ( self.imgs[ 0 ].full_img.width > 0 && self.imgs[ 0 ].full_img.height > 0 ) {
						self.setup_step_two();
						return;
					}
				}

				// Get the full sizes of all the images. If more than 50, make several calls. (The API has limits.)
				// Also avoid using Ajax on IE6...

				var cache = {};
				var names = [];

				Array.forEach( self.imgs, function ( img, idx ) {
					if ( cache[ img.realName ] ) {
						cache[ img.realName ][ cache[ img.realName ].length ] = idx;
					} else {
						cache[ img.realName ] = [ idx ];
						names[ names.length ] = img.realName;
					}
				} );

				var to_do = names.length;
				var done = 0;

				function check_done( length ) {
					done += length;
					if ( done >= names.length ) {
						if ( typeof ImageAnnotator.info_callbacks !== 'undefined' ) {
							ImageAnnotator.info_callbacks = null;
						}
						self.setup_step_two();
					}
				}

				function make_calls( execute_call, url_limit ) {
					function build_titles( from, length, url_limit ) {
						var done = 0;
						var text = '';

						for ( var i = from; i < from + length; i++ ) {
							var new_text = names[ i ];
							if ( url_limit ) {
								new_text = encodeURIComponent( new_text );
								if ( text.length && ( text.length + new_text.length + 1 > url_limit ) ) {
									break;
								}
							}

							text += ( text.length ? '|' : '' ) + new_text;
							done++;
						}

						return {
							text: text,
							n: done
						};
					}

					var start = 0, chunk = 0, params;
					while ( to_do > 0 ) {
						params = build_titles( start, Math.min( 50, to_do ), url_limit );
						execute_call( params.n, params.text );
						to_do -= params.n;
						start += params.n;
					}
				}

				function set_info( json ) {
					try {
						if ( json && json.query && json.query.pages ) {
							function get_size( info ) {
								if ( !info.imageinfo || info.imageinfo.length === 0 ) {
									return;
								}

								var title = info.title.replace( / /g, '_' );
								var indices = cache[ title ];

								if ( !indices ) {
									return;
								}

								Array.forEach(
									indices,
									function ( i ) {
										self.imgs[ i ].full_img = {
											width: info.imageinfo[ 0 ].width,
											height: info.imageinfo[ 0 ].height
										};
										self.imgs[ i ].has_page = ( typeof info.missing === 'undefined' );
										self.imgs[ i ].isLocal = !info.imagerepository || info.imagerepository == 'local';

										if ( i != 0 || !self.may_edit || !info.protection || mw.config.get( 'wgNamespaceNumber' ) != 6 ) {
											return;
										}

										// Care about the protection settings
										var protection = Array.any( info.protection, function ( e ) {
											return ( e.type == 'edit' ? e : null );
										} );
										self.may_edit =
											!protection ||
											( mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( protection.level ) );
									}
								);
							}

							for ( var page in json.query.pages ) {
								get_size( json.query.pages[ page ] );
							}
						} // end if
					} catch ( ex ) {
					}
				}

				if ( ( !window.XMLHttpRequest && !!window.ActiveXObject ) || !self.haveAjax ) {
					// IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that
					// prompt by using getScript instead of parseWikitext in this case.
					ImageAnnotator.info_callbacks = [];

					var template = mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php?action=query&format=json' +
						'&prop=info|imageinfo&inprop=protection&iiprop=size' +
						'&titles=&callback=ImageAnnotator.info_callbacks[].callback';

					if ( template.startsWith( '//' ) ) {
						// Avoid protocol-relative URIs (IE7 bug)
						template = document.location.protocol + template;
					}

					make_calls(
						function ( length, titles ) {
							var idx = ImageAnnotator.info_callbacks.length;

							ImageAnnotator.info_callbacks[ idx ] = {
								callback: function ( json ) {
									set_info( json );

									ImageAnnotator.info_callbacks[ idx ].done = true;

									if ( ImageAnnotator.info_callbacks[ idx ].script ) {
										LAPI.DOM.removeNode( ImageAnnotator.info_callbacks[ idx ].script );
										ImageAnnotator.info_callbacks[ idx ].script = null;
									}

									check_done( length );
								},
								done: false
							};

							ImageAnnotator.info_callbacks[ idx ].script = IA.getScript(
								template.replace( 'info_callbacks[].callback', 'info_callbacks[' + idx + '].callback' )
									.replace( '&titles=&', '&titles=' + titles + '&' ),
								true // No local caching!
							);

							// We do bypass the local JavaScript cache of importScriptURI, but on IE, we still may
							// get the script from the browser's cache, and if that happens, IE may execute the
							// script (and call the callback) synchronously before the assignment is done. Clean
							// up in that case.
							if (
								ImageAnnotator.info_callbacks && ImageAnnotator.info_callbacks[ idx ] &&
								ImageAnnotator.info_callbacks[ idx ].done && ImageAnnotator.info_callbacks[ idx ].script
							) {
								LAPI.DOM.removeNode( ImageAnnotator.info_callbacks[ idx ].script );
								ImageAnnotator.info_callbacks[ idx ].script = null;
							}
						},
						( LAPI.Browser.is_ie ? 1950 : 4000 ) - template.length // Some slack for caching parameters
					);
				} else {
					make_calls(
						function ( length, titles ) {
							LAPI.Ajax.apiGet(
								'query', {
									titles: titles,
									prop: 'info|imageinfo',
									inprop: 'protection',
									iiprop: 'size'
								},
								function ( request, json_result ) {
									set_info( json_result );
									check_done( length );
								},
								function () {
									check_done( length );
								}
							);
						}
					);
				} // end if can use Ajax
			},

			setup_ui: function () {
				// Complete the UI object we've gotten from config.

				ImageAnnotator.UI.ready = false;
				ImageAnnotator.UI.repo = null;
				ImageAnnotator.UI.needs_plea = false;

				var readyEvent = [];

				ImageAnnotator.UI.fireReadyEvent = function () {
					if ( ImageAnnotator.UI.ready ) {
						// Already fired, nothing to do.
						return;
					}

					ImageAnnotator.UI.ready = true;

					// Call all registered handlers, and clear the array.
					Array.forEach( readyEvent, function ( f, idx ) {
						try {
							f();
						} catch ( ex ) {}

						readyEvent[ idx ] = null;
					} );

					readyEvent = null;
				};

				ImageAnnotator.UI.addReadyEventHandler = function ( f ) {
					if ( ImageAnnotator.UI.ready ) {
						f(); // Already fired: call directly
					} else {
						readyEvent[ readyEvent.length ] = f;
					}
				};

				ImageAnnotator.UI.setup = function () {
					if ( ImageAnnotator.UI.repo ) {
						return;
					}

					var self = ImageAnnotator.UI;
					var node = LAPI.make( 'div', null, { display: 'none' } );
					document.body.appendChild( node );

					if ( typeof UIElements === 'undefined' ) {
						self.basic = true;
						self.repo = {};

						for ( var item in self.defaults ) {
							node.innerHTML = self.defaults[ item ];
							self.repo[ item ] = node.firstChild;
							LAPI.DOM.removeChildren( node );
						}
					} else {
						self.basic = false;
						self.repo = UIElements.emptyRepository( self.defaultLanguage );

						for ( var item in self.defaults ) {
							node.innerHTML = self.defaults[ item ];
							UIElements.setEntry( item, self.repo, node.firstChild );
							LAPI.DOM.removeChildren( node );
						}

						UIElements.load( 'wpImageAnnotatorTexts', null, null, self.repo );
					}

					LAPI.DOM.removeNode( node );
				};

				ImageAnnotator.UI.get = function ( id, basic, no_plea ) {
					var self = ImageAnnotator.UI;

					if ( !self.repo ) {
						self.setup();
					}

					var result = null;
					var add_plea = false;

					if ( self.basic ) {
						result = self.repo[ id ];
					} else {
						result = UIElements.getEntry( id, self.repo, mw.config.get( 'wgUserLanguage' ), null );
						add_plea = !result;
						if ( !result ) {
							result = UIElements.getEntry( id, self.repo );
						}
					}

					self.needs_plea = add_plea;

					if ( !result ) {
						// Hmmm... what happened here? We normally have defaults...
						return null;
					}

					if ( basic ) {
						return LAPI.DOM.getInnerText( result ).trim();
					}

					result = result.cloneNode( true );
					if ( mw.config.get( 'wgServer' ).contains( '/commons' ) && add_plea && !no_plea ) {
						// Add a translation plea.
						if ( result.nodeName.toLowerCase() == 'div' ) {
							result.appendChild( self.get_plea() );
						} else {
							var span = LAPI.make( 'span' );
							span.appendChild( result );
							span.appendChild( self.get_plea() );
							result = span;
						}
					}

					return result;
				};

				ImageAnnotator.UI.get_plea = function () {
					var self = ImageAnnotator.UI;
					var translate = self.get( 'wpTranslate', false, true ) || 'translate';
					var span = LAPI.make( 'small' );
					span.appendChild( document.createTextNode( '\xa0(' ) );
					span.appendChild(
						LAPI.DOM.makeLink(
							mw.config.get( 'wgServer' ) + mw.config.get( 'wgScript' ) + '?title=MediaWiki_talk:ImageAnnotatorTexts' +
							'&action=edit&section=new&withJS=MediaWiki:ImageAnnotatorTranslator.js' +
							'&language=' + mw.config.get( 'wgUserLanguage' ),
							translate,
							( typeof translate === 'string' ? translate : LAPI.DOM.getInnerText( translate ).trim() )
						)
					);
					span.appendChild( document.createTextNode( ')' ) );
					return span;
				};

				ImageAnnotator.UI.init = function ( html_text_or_json ) {
					var text;
					if ( typeof html_text_or_json === 'string' ) {
						text = html_text_or_json;
					} else if (
						typeof html_text_or_json !== 'undefined' &&
						typeof html_text_or_json.parse !== 'undefined' &&
						typeof html_text_or_json.parse.text !== 'undefined' &&
						typeof html_text_or_json.parse.text[ '*' ] !== 'undefined'
					) {
						text = html_text_or_json.parse.text[ '*' ];
					} else {
						text = null;
					}

					if ( !text ) {
						ImageAnnotator.UI.fireReadyEvent();
						return;
					}

					var node = LAPI.make( 'div', null, { display: 'none' } );
					document.body.appendChild( node );
					try {
						node.innerHTML = text;
					} catch ( ex ) {
						LAPI.DOM.removeNode( node );
						node = null;
						// Swallow. We'll just work with the default UI
					}

					if ( node && !ImageAnnotator.UI.repo ) {
						ImageAnnotator.UI.setup();
					}

					ImageAnnotator.UI.fireReadyEvent();
				};

				var ui_page = '{{MediaWiki:ImageAnnotatorTexts' +
					( mw.config.get( 'wgUserLanguage' ) != mw.config.get( 'wgContentLanguage' ) ? '|lang=' + mw.config.get( 'wgUserLanguage' ) : '' ) +
					'|live=1}}';

				function get_ui_no_ajax() {
					var url =
						mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php?action=parse&pst&text=' +
						encodeURIComponent( ui_page ) + '&title=API&prop=text&format=json' +
						'&callback=ImageAnnotator.UI.init&maxage=14400&smaxage=14400';
					// Result cached for 4 hours. How to properly handle an error? It appears there's no way to catch
					// that on IE. (On FF, we could use an onerror handler on the script tag, but on FF, we use Ajax
					// anyway.)
					IA.getScript( url, true ); // No local caching!
				}

				function get_ui() {
					IA.haveAjax = ( LAPI.Ajax.getRequest() != null );
					IA.ajaxQueried = true;

					// Works only with Ajax (but then, most of this script doesn't work without).
					// Check what this does to load times... If lots of people used this, it might be better to
					// have the UI texts included in some footer as we did on Special:Upload. True, everybody
					// would get the texts, even people not using this, but the texts are small anyway...
					if ( !IA.haveAjax || !LAPI.Ajax.parseWikitext ) {
						get_ui_no_ajax(); // Fallback.
						return;
					}

					LAPI.Ajax.parseWikitext(
						ui_page,
						ImageAnnotator.UI.init,
						ImageAnnotator.UI.fireReadyEvent,
						false,
						null,
						'API', // A fixed string to enable caching at all.
						14400 // 4 hour caching.
					);
				} // end get_ui

				if ( !window.XMLHttpRequest && !!window.ActiveXObject ) {
					// IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that
					// prompt by using getScript instead of parseWikitext in this case. The disadvantage
					// is that we don't do anything if this fails for some reason.
					get_ui_no_ajax();
				} else {
					get_ui();
				}
			},

			setup_step_two: function () {
				var self = IA;

				// Throw out any images for which we miss either the thumbnail or the full image size.
				// Also throws out thumbnails that are larger than the full image.
				self.imgs = Array.select( self.imgs, function ( elem, idx ) {
					var result =
						elem.thumb.width > 0 && elem.thumb.height > 0 &&
						typeof elem.full_img !== 'undefined' &&
						elem.full_img.width > 0 && elem.full_img.height > 0 &&
						elem.full_img.width >= elem.thumb.width &&
						elem.full_img.height >= elem.thumb.height;

					if ( self.may_edit && idx === 0 && !result ) {
						self.may_edit = false;
					}

					return result;
				} );

				if ( self.imgs.length === 0 ) {
					return;
				}

				ImageAnnotator.UI.addReadyEventHandler( IA.complete_setup );
			},

			complete_setup: function () {
				// We can be sure to have the UI here because this is called only when the ready event of the
				// UI object is fired.
				var self = IA;

				// Check edit permissions
				if ( self.may_edit && mw.config.get( 'wgRestrictionEdit' ) ) {
					self.may_edit = (
						( mw.config.get( 'wgRestrictionEdit' ).length === 0 || mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( 'sysop' ) ) ||
						(
							mw.config.get( 'wgRestrictionEdit' ).length === 1 && mw.config.get( 'wgRestrictionEdit' )[ 0 ] === 'autoconfirmed' &&
							mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( 'confirmed' ) // confirmed & autoconfirmed
						)
					);
				}

				if ( self.may_edit ) {
					// Check whether the image is local. Don't allow editing if the file is remote.
					var sharedUpload = getElementsByClassName( document, 'div', 'sharedUploadNotice' );
					self.may_edit = ( !sharedUpload || sharedUpload.length === 0 );
				}

				if ( self.may_edit && mw.config.get( 'wgNamespaceNumber' ) != 6 ) {
					// Only allow edits if the stored page name matches the current one.
					var img_page_name = getElementsByClassName( self.imgs[ 0 ].scope, '*', 'wpImageAnnotatorPageName' );
					if ( img_page_name && img_page_name.length ) {
						img_page_name = LAPI.DOM.getInnerText( img_page_name[ 0 ] );
					} else {
						img_page_name = '';
					}
					self.may_edit = ( img_page_name.replace( / /g, '_' ) == mw.config.get( 'wgTitle' ).replace( / /g, '_' ) );
				}

				if ( self.may_edit && self.ajaxQueried ) {
					self.may_edit = self.haveAjax;
				}

				// Now create viewers for all images
				self.viewers = new Array( self.imgs.length );
				for ( var i = 0; i < self.imgs.length; i++ ) {
					self.viewers[ i ] = new ImageNotesViewer( self.imgs[ i ], i === 0 && self.may_edit );
				}

				if ( self.may_edit ) {
					// Respect user override for zoom, if any
					self.zoom_threshold = ImageAnnotator_config.zoom_threshold;
					if (
						typeof window.ImageAnnotator_zoom_threshold !== 'undefined' &&
						!isNaN( window.ImageAnnotator_zoom_threshold ) &&
						window.ImageAnnotator_zoom_threshold >= 0.0
					) {
						// If somebody sets it to a nonsensical high value, that's his or her problem: there won't be any
						// zooming.
						self.zoom_threshold = window.ImageAnnotator_zoom_threshold;
					}

					// Adapt zoom threshold for small thumbnails or images with a very lopsided width/height ratio,
					// but only if we *can* zoom at least twice
					if (
						self.viewers[ 0 ].full_img.width > 300 &&
						Math.min( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy ) >= 2.0
					) {
						if (
							self.viewers[ 0 ].thumb.width < 400 ||
							self.viewers[ 0 ].thumb.width / self.viewers[ 0 ].thumb.height > 2.0 ||
							self.viewers[ 0 ].thumb.height / self.viewers[ 0 ].thumb.width > 2.0
						) {
							self.zoom_threshold = 0; // Force zooming
						}
					}

					self.editor = new ImageAnnotationEditor();

					function track( evt ) {
						evt = evt || window.event;
						if ( self.is_adding ) {
							self.update_zoom( evt );
						}

						if ( !self.is_tracking ) {
							return LAPI.Evt.kill( evt );
						}

						var mouse_pos = LAPI.Pos.mousePosition( evt );
						if ( !mouse_pos ) {
							return;
						}

						if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) {
							return;
						}

						var origin = LAPI.Pos.position( self.cover );

						// Make mouse pos relative to cover
						mouse_pos.x = mouse_pos.x - origin.x;
						mouse_pos.y = mouse_pos.y - origin.y;

						if ( mouse_pos.x >= self.base_x ) {
							self.definer.style.width = String( mouse_pos.x - self.base_x ) + 'px';
							self.definer.style.left = String( self.base_x ) + 'px';
						} else {
							self.definer.style.width = String( self.base_x - mouse_pos.x ) + 'px';
							self.definer.style.left = String( mouse_pos.x ) + 'px';
						}

						if ( mouse_pos.y >= self.base_y ) {
							self.definer.style.height = String( mouse_pos.y - self.base_y ) + 'px';
							self.definer.style.top = String( self.base_y ) + 'px';
						} else {
							self.definer.style.height = String( self.base_y - mouse_pos.y ) + 'px';
							self.definer.style.top = String( mouse_pos.y ) + 'px';
						}

						return LAPI.Evt.kill( evt );
					}

					function pause( evt ) {
						LAPI.Evt.remove( document, 'mousemove', track, true );

						if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) {
							document.captureEvents( null );
						}

						self.move_listening = false;
					}

					function resume( evt ) {
						// captureEvents is actually deprecated, but I haven't succeeded to make this work with
						// addEventListener only.
						if ( ( self.is_tracking || self.is_adding ) && !self.move_listening ) {
							if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) {
								document.captureEvents( Event.MOUSEMOVE );
							}

							LAPI.Evt.attach( document, 'mousemove', track, true );

							self.move_listening = true;
						}
					}

					function stop_tracking( evt ) {
						evt = evt || window.event;

						// Check that we're within the image. Note: this check can fail only on IE >= 7, on other
						// browsers, we attach the handler on self.cover and thus we don't even get events outside
						// that range.
						var mouse_pos = LAPI.Pos.mousePosition( evt );
						if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) {
							return;
						}

						if ( self.is_tracking ) {
							self.is_tracking = false;
							self.is_adding = false;

							// Done.
							pause();

							if ( LAPI.Browser.is_ie ) {
								// Trust Microsoft to get everything wrong!
								LAPI.Evt.remove( document, 'mouseup', stop_tracking );
							} else {
								LAPI.Evt.remove( self.cover, 'mouseup', stop_tracking );
							}

							LAPI.Evt.remove( window, 'blur', pause );
							LAPI.Evt.remove( window, 'focus', resume );
							self.cover.style.cursor = 'auto';
							LAPI.DOM.removeNode( self.border );
							LAPI.Evt.remove( self.cover, self.mouse_in, self.update_zoom_evt );
							LAPI.Evt.remove( self.cover, self.mouse_out, self.hide_zoom_evt );
							self.hide_zoom();
							self.viewers[ 0 ].hide(); // Hide all existing boxes

							if ( !self.definer || self.definer.offsetWidth <= 0 || self.definer.offsetHeight <= 0 ) {
								// Nothing: just remove the definer:
								if ( self.definer ) {
									LAPI.DOM.removeNode( self.definer );
								}

								// Re-attach event handlers
								self.viewers[ 0 ].setShowHideEvents( true );
								self.hide_cover();
								self.viewers[ 0 ].setDefaultMsg();

								// And make sure we get the real view again
								self.viewers[ 0 ].show();
							} else {
								// We have a div with some extent: remove event capturing and create a new annotation
								var new_note = new ImageAnnotation( self.definer, self.viewers[ 0 ], -1 );
								self.viewers[ 0 ].register( new_note );
								self.editor.editNote( new_note );
							}

							self.definer = null;
						}

						if ( evt ) {
							return LAPI.Evt.kill( evt );
						}

						return false;
					}

					function start_tracking( evt ) {
						if ( !self.is_tracking ) {
							self.is_tracking = true;

							evt = evt || window.event;

							// Set the position, size 1
							var mouse_pos = LAPI.Pos.mousePosition( evt );
							var origin = LAPI.Pos.position( self.cover );
							self.base_x = mouse_pos.x - origin.x;
							self.base_y = mouse_pos.y - origin.y;
							Object.merge(
								{
									left: String( self.base_x ) + 'px',
									top: String( self.base_y ) + 'px',
									width: '0px',
									height: '0px',
									display: ''
								},
								self.definer.style
							);

							// Set mouse handlers
							LAPI.Evt.remove( self.cover, 'mousedown', start_tracking );
							if ( LAPI.Browser.is_ie ) {
								LAPI.Evt.attach( document, 'mouseup', stop_tracking ); // Doesn't work properly on self.cover...
							} else {
								LAPI.Evt.attach( self.cover, 'mouseup', stop_tracking );
							}

							resume();

							LAPI.Evt.attach( window, 'blur', pause );
							LAPI.Evt.attach( window, 'focus', resume );
						}
						if ( evt ) { return LAPI.Evt.kill( evt ); }
						return false;
					}

					function add_new( evt ) {
						if ( !self.canEdit() ) {
							return;
						}

						self.editor.hide_editor();
						Tooltips.close();
						var cover = self.get_cover();
						cover.style.cursor = 'crosshair';
						self.definer = LAPI.make(
							'div', null,
							{
								border: '1px solid ' + IA.new_border,
								display: 'none',
								position: 'absolute',
								top: '0px',
								left: '0px',
								width: '0px',
								height: '0px',
								padding: '0',
								lineHeight: '0px', // IE needs this, even though there are no lines within
								fontSize: '0px', // IE
								zIndex: cover.style.zIndex - 2 // Below the mouse capture div
							}
						);
						self.viewers[ 0 ].img_div.appendChild( self.definer );

						// Enter mouse-tracking mode to define extent of view. Mouse cursor is outside of image,
						// hence none of our tooltips are visible.
						self.viewers[ 0 ].img_div.appendChild( self.border );
						self.show_cover();
						self.is_tracking = false;
						self.is_adding = true;
						LAPI.Evt.attach( cover, 'mousedown', start_tracking );
						resume();
						self.button_div.style.display = 'none';

						// Remove the event listeners on the image: IE sometimes invokes them even when the image is covered
						self.viewers[ 0 ].setShowHideEvents( false );
						self.viewers[ 0 ].hide(); // Make sure notes are hidden
						self.viewers[ 0 ].toggle( true ); // Show all note rectangles (but only the dummies)
						self.update_zoom_evt = LAPI.Evt.makeListener( self, self.update_zoom );
						self.hide_zoom_evt = LAPI.Evt.makeListener( self, self.hide_zoom );
						self.show_zoom();
						LAPI.Evt.attach( cover, self.mouse_in, self.update_zoom_evt );
						LAPI.Evt.attach( cover, self.mouse_out, self.hide_zoom_evt );
						LAPI.DOM.removeChildren( self.viewers[ 0 ].msg );
						self.viewers[ 0 ].msg.appendChild( ImageAnnotator.UI.get( 'wpImageAnnotatorDrawRectMsg', false ) );
						self.viewers[ 0 ].msg.style.display = '';
					}

					self.button_div = LAPI.make( 'div' );
					self.viewers[ 0 ].main_div.appendChild( self.button_div );
					self.add_button = LAPI.DOM.makeButton(
						'ImageAnnotationAddButton',
						ImageAnnotator.UI.get( 'wpImageAnnotatorAddButtonText', true ),
						add_new
					);

					var add_plea = ImageAnnotator.UI.needs_plea;
					self.button_div.appendChild( self.add_button );
					self.help_link = self.createHelpLink();
					if ( self.help_link ) {
						self.button_div.appendChild( document.createTextNode( '\xa0' ) );
						self.button_div.appendChild( self.help_link );
					}

					if ( add_plea && mw.config.get( 'wgServer' ).contains( '/commons' ) ) {
						self.button_div.appendChild( ImageAnnotator.UI.get_plea() );
					}
				} // end if may_edit

				// Get the file description pages of thumbnails. Figure out for which viewers we need to do this.
				var cache = {};
				var get_local = [];
				var get_foreign = [];
				Array.forEach( self.viewers, function ( viewer, idx ) {
					if ( viewer.setup_done || viewer.isLocal && !viewer.has_page ) {
						return;
					}

					// Handle only images that either are foreign or local and do have a page.
					if ( cache[ viewer.realName ] ) {
						cache[ viewer.realName ][ cache[ viewer.realName ].length ] = idx;
					} else {
						cache[ viewer.realName ] = [ idx ];
						if ( !viewer.has_page ) {
							get_foreign[ get_foreign.length ] = viewer.realName;
						} else {
							get_local[ get_local.length ] = viewer.realName;
						}
					}
				} );

				if ( get_local.length === 0 && get_foreign.length === 0 ) {
					return;
				}

				// Now we have unique page names in the cache and in to_get. Go get the corresponding file
				// description pages. We make a series of simultaneous asynchronous calls to avoid hitting
				// API limits and to keep the URL length below the limit for the foreign_repo calls.

				function make_calls( list, execute_call, url_limit ) {
					function composer( list, from, length, url_limit ) {
						function compose( list, from, length, url_limit ) {
							var text = '';
							var done = 0;

							for ( var i = from; i < from + length; i++ ) {
								var new_text =
									'<div class="wpImageAnnotatorInlineImageWrapper" style="display:none;">' +
									'<span class="image_annotation_inline_name">' + list[ i ] + '</span>' +
									'{{:' + list[ i ] + '}}' + // Leading dot to avoid getting links to the full images if we hit a parser limit
									'</div>';
								if ( url_limit ) {
									new_text = encodeURIComponent( new_text );
									if ( text.length && ( text.length + new_text.length > url_limit ) ) {
										break;
									}
								}

								text = text + new_text;
								done++;

								// Additionally, limit the number of image pages to get: these can be large, and the server
								// may refuse to actually do the transclusions but may in that case include the full images
								// in the result, which would make us load the full images, which is desastrous if there are
								// many thumbs to large images on the page.
								if ( done == 5 ) {
									break;
								}
							}

							return {
								text: text,
								n: done
							};
						}

						var param = compose( list, from, length, url_limit );
						execute_call( param.text );
						return param.n;
					}

					var start = 0, chunk = 0, to_do = list.length;
					while ( to_do > 0 ) {
						chunk = composer( list, start, Math.min( 50, to_do ), url_limit );
						to_do -= chunk;
						start += chunk;
					}
				}

				var divRE = /(<\s*div\b)|(<\/\s*div\s*>)/ig;
				var blockStart = '<div class="wpImageAnnotatorInlineImageWrapper"';
				var inlineNameEnd = '</span>';
				var noteStart = '<div id="image_annotation_note_';
				var noteControlRE = /<div\s*class="(wpImageAnnotatorInlinedRules|image_annotation_colors")(\S|\s)*?\/div>/g;

				// Our parse request returns the full html of the description pages' contents, including any
				// license or information or other templates that we don't care about, and which may contain
				// additional images we'd rather not load when we add this (X)HTML to the DOM. Therefore, we
				// strip out everything but the notes.
				function strip_noise( html ) {
					var result = '';
					var m;

					// First, get rid of HTML comments and scripts
					html = html.replace( /<!--(.|\s)*?-->/g, '' ).replace( /<script(.|\s)*?\/script>/g, '' );
					var i = html.indexOf( blockStart, idx ), idx = 0, l = html.length;

					// Now collect pages
					while ( idx < l && i >= idx ) {
						var j = html.indexOf( inlineNameEnd, i + blockStart.length );
						if ( j < i + blockStart.length ) {
							break;
						}

						result += html.substring( i, j + inlineNameEnd.length );
						idx = j + inlineNameEnd.length;

						// Now collect all image image notes for that page
						var note_begin = 0, next_block = html.indexOf( blockStart, idx );

						// Do we have image note control or color templates?
						j = idx;
						for ( ;; ) {
							noteControlRE.lastIndex = j;
							m = noteControlRE.exec( html );
							if ( !m || next_block >= idx && m.index > next_block ) {
								break;
							}

							result += m[ 0 ];
							j = m.index + m[ 0 ].length;
						}

						// Collect notes
						for ( ;; ) {
							note_begin = html.indexOf( noteStart, idx );

							// Check whether next wrapper comes first
							if ( note_begin < idx || ( next_block >= idx && note_begin > next_block ) ) {
								break;
							}

							// Start parsing nested <div> and </div>, from note_begin on. Just ignore any other tags.
							var level = 1, k = note_begin + noteStart.length;
							while ( level > 0 && k < l ) {
								divRE.lastIndex = k;
								m = divRE.exec( html );

								if ( !m || m.length < 2 ) {
									k = l; // Nothing found at all?
								} else {
									if ( m[ 1 ] ) {
										level++; k = m.index + m[ 1 ].length; // divStart found first
									} else if ( m.length > 2 && m[ 2 ] ) {
										level--; k = m.index + m[ 2 ].length; // divEnd found first
									} else {
										k = l; // Huh?
									}
								}

							} // end loop for nested divs

							result += html.substring( note_begin, k );
							while ( level-- > 0 ) {
								// Missing ends.
								result += '</div>';
							}

							idx = k;
						} // end loop collecting notes

						result += '</div>'; // Close the wrapper
						i = next_block;
					} // end loop collecting pages

					return result;
				}

				function setup_thumb_viewers( html_text ) {
					var node = LAPI.make( 'div', null, { display: 'none' } );
					document.body.appendChild( node );

					try {
						node.innerHTML = strip_noise( html_text );
						var pages = getElementsByClassName( node, 'div', 'wpImageAnnotatorInlineImageWrapper' );
						for ( var i = 0; pages && i < pages.length; i++ ) {
							var notes = getElementsByClassName( pages[ i ], 'div', self.annotation_class );
							if ( !notes || notes.length === 0 ) {
								continue;
							}

							var page = self.getItem( 'inline_name', pages[ i ] );
							if ( !page ) {
								continue;
							}

							page = page.replace( / /g, '_' );

							var viewers = cache[ page ] || cache[ 'File:' + page.substring( page.indexOf( ':' ) + 1 ) ];
							if ( !viewers || viewers.length === 0 ) {
								continue;
							}

							// Update rules.
							var rules = getElementsByClassName( pages[ i ], 'div', 'wpImageAnnotatorInlinedRules' );
							var local_rules = {
								inline: Object.clone( IA.rules.inline ),
								thumbs: Object.clone( IA.rules.thumbs )
							};

							if ( rules && rules.length ) {
								rules = rules[ 0 ];
								if (
									typeof local_rules.inline.show === 'undefined' &&
									LAPI.DOM.hasClass( rules, 'wpImageAnnotatorNoInlineDisplay' )
								) {
									local_rules.inline.show = false;
								}

								if (
									typeof local_rules.inline.icon === 'undefined' &&
									LAPI.DOM.hasClass( rules, 'wpImageAnnotatorInlineDisplayIcon' )
								) {
									local_rules.inline.icon = true;
								}

								if (
									typeof local_rules.thumbs.show === 'undefined' &&
									LAPI.DOM.hasClass( rules, 'wpImageAnnotatorNoThumbs' )
								) {
									local_rules.thumbs.show = false;
								}

								if (
									typeof local_rules.thumbs.icon === 'undefined' &&
									LAPI.DOM.hasClass( rules, 'wpImageAnnotatorThumbDisplayIcon' )
								) {
									local_rules.thumbs.icon = true;
								}
							}

							// Make sure all are set
							local_rules.inline.show =
								typeof local_rules.inline.show === 'undefined' || local_rules.inline.show;
							local_rules.thumbs.show =
								typeof local_rules.thumbs.show === 'undefined' || local_rules.thumbs.show;
							local_rules.inline.icon =
								typeof local_rules.inline.icon !== 'undefined' && local_rules.inline.icon;
							local_rules.thumbs.icon =
								typeof local_rules.thumbs.icon !== 'undefined' && local_rules.thumbs.icon;
							if ( !local_rules.inline.show ) {
								continue;
							}

							// Now use pages[i] as a scope shared by all the viewers using it. Since we clone note
							// contents for note display, this works. Otherwise, we'd have to copy the notes into
							// each viewer's scope.
							document.body.appendChild( pages[ i ] ); // Move it out of 'node'

							// Set viewers' scopes and finish their setup.
							Array.forEach( viewers, function ( v ) {
								if ( !self.viewers[ v ].isThumbnail || local_rules.thumbs.show ) {
									self.viewers[ v ].scope = pages[ i ];
									self.viewers[ v ].setup( self.viewers[ v ].isThumbnail && local_rules.thumbs.icon ||
									self.viewers[ v ].isOther && local_rules.inline.icon );
								}
							} );
						}
					} catch ( ex ) {}

					LAPI.DOM.removeNode( node );
				}

				ImageAnnotator.script_callbacks = [];

				function make_script_calls( list, api ) {
					var template = api + '?action=parse&pst&text=&prop=text&format=json' +
						'&maxage=1800&smaxage=1800&uselang=' + mw.config.get( 'wgUserLanguage' ) + // see bugzilla 22764
						'&callback=ImageAnnotator.script_callbacks[].callback';

					if ( template.startsWith( '//' ) ) {
						// Avoid protocol-relative URIs (IE7 bug)
						template = document.location.protocol + template;
					}

					make_calls(
						list,
						function ( text ) {
							var idx = ImageAnnotator.script_callbacks.length;
							ImageAnnotator.script_callbacks[ idx ] = {
								callback: function ( json ) {
									if ( json && json.parse && json.parse.text && json.parse.text[ '*' ] ) {
										setup_thumb_viewers( json.parse.text[ '*' ] );
									}

									ImageAnnotator.script_callbacks[ idx ].done = true;

									if ( ImageAnnotator.script_callbacks[ idx ].script ) {
										LAPI.DOM.removeNode( ImageAnnotator.script_callbacks[ idx ].script );
										ImageAnnotator.script_callbacks[ idx ].script = null;
									}
								},
								done: false
							};

							ImageAnnotator.script_callbacks[ idx ].script =
								IA.getScript(
									template.replace( 'script_callbacks[].callback', 'script_callbacks[' + idx + '].callback' )
										.replace( '&text=&', '&text=' + text + '&' ),
									true // No local caching!
								);

							if (
								ImageAnnotator.script_callbacks && ImageAnnotator.script_callbacks[ idx ] &&
								ImageAnnotator.script_callbacks[ idx ].done && ImageAnnotator.script_callbacks[ idx ].script
							) {
								LAPI.DOM.removeNode( ImageAnnotator.script_callbacks[ idx ].script );
								ImageAnnotator.script_callbacks[ idx ].script = null;
							}
						},
						( LAPI.DOM.is_ie ? 1950 : 4000 ) - template.length // Some slack for caching parameters
					);
				}

				if ( ( !window.XMLHttpRequest && !!window.ActiveXObject ) || !self.haveAjax ) {
					make_script_calls( get_local, mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php' );
				} else {
					make_calls(
						get_local,
						function ( text ) {
							LAPI.Ajax.parseWikitext(
								text,
								function ( html_text ) {
									if ( html_text ) {
										setup_thumb_viewers( html_text );
									}
								},
								function () {},
								false,
								null,
								'API', // Fixed string to enable caching at all
								1800 // 30 minutes caching.
							);
						}
					);
				}

				// Can't use Ajax for foreign repo, might violate single-origin policy (e.g. from wikisource.org
				// to wikimedia.org). Attention, here we must care about the URL length! IE has a limit of 2083
				// character (2048 in the path part), and servers also may impose limits (on the WMF servers,
				// the limit appears to be 8kB).
				make_script_calls( get_foreign, ImageAnnotator_config.sharedRepositoryAPI() );
			},

			show_zoom: function () {
				var self = IA;
				if (
					(
						self.viewers[ 0 ].factors.dx < self.zoom_threshold &&
						self.viewers[ 0 ].factors.dy < self.zoom_threshold
					) ||
					Math.max( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy ) < 2.0
				) {
					// Below zoom threshold, or full image not even twice the size of the preview
					return;
				}

				if ( !self.zoom ) {
					self.zoom = LAPI.make(
						'div',
						{ id: 'image_annotator_zoom' },
						{
							overflow: 'hidden',
							width: '200px',
							height: '200px',
							position: 'absolute',
							display: 'none',
							top: '0px',
							left: '0px',
							border: '2px solid #666666',
							backgroundColor: 'white',
							zIndex: 2050 // On top of everything
						}
					);
					var src = self.viewers[ 0 ].img.getAttribute( 'src', 2 );

					// Adjust zoom_factor
					if ( self.zoom_factor > self.viewers[ 0 ].factors.dx || self.zoom_factor > self.viewers[ 0 ].factors.dy ) {
						self.zoom_factor = Math.min( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy );
					}
					self.zoom.appendChild( LAPI.make( 'div', null, { position: 'relative' } ) );

					// Calculate zoom size and source link
					var zoom_width = Math.floor( self.viewers[ 0 ].thumb.width * self.zoom_factor );
					var zoom_height = Math.floor( self.viewers[ 0 ].thumb.height * self.zoom_factor );

					// For SVGs, always use a scaled PNG for the zoom.
					if ( zoom_width > 0.9 * self.viewers[ 0 ].full_img.width && src.search( /\.svg\.png$/i ) < 0 ) {
						// If the thumb we'd be loading was within about 80% of the full image size, we may just as
						// well get the full image instead of a scaled version.
						self.zoom_factor = Math.min( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy );
						zoom_width = self.viewers[ 0 ].full_img.width;
						zoom_height = self.viewers[ 0 ].full_img.height;
					}

					// Construct the initial zoomed image. We need to clone; if we create a completely
					// new DOM node ourselves, it may not work on IE6...
					var zoomed = self.viewers[ 0 ].img.cloneNode( true );
					zoomed.width = String( zoom_width );
					zoomed.height = String( zoom_height );
					Object.merge( { position: 'absolute', top: '0px', left: '0px' }, zoomed.style );
					self.zoom.firstChild.appendChild( zoomed );

					// Crosshair
					self.zoom.firstChild.appendChild(
						LAPI.make(
							'div', null,
							{
								width: '1px',
								height: '200px',
								borderLeft: '1px solid red',
								position: 'absolute',
								top: '0px',
								left: '100px'
							}
						)
					);
					self.zoom.firstChild.appendChild(
						LAPI.make(
							'div', null,
							{
								width: '200px',
								height: '1px',
								borderTop: '1px solid red',
								position: 'absolute',
								top: '100px',
								left: '0px'
							}
						)
					);

					document.body.appendChild( self.zoom );

					LAPI.DOM.loadImage(
						self.viewers[ 0 ].imgName,
						src,
						zoom_width,
						zoom_height,
						ImageAnnotator_config.thumbnailsGeneratedAutomatically(),
						function ( img ) {
							// Replace the image in self.zoom by self.zoom_loader, making sure we keep the offsets
							img.style.position = 'absolute';
							img.style.top = self.zoom.firstChild.firstChild.style.top;
							img.style.left = self.zoom.firstChild.firstChild.style.left;
							img.style.display = '';
							self.zoom.firstChild.replaceChild( img, self.zoom.firstChild.firstChild );
						}
					);
				}

				self.zoom.style.display = 'none'; // Will be shown in update
			},

			update_zoom: function ( evt ) {
				if ( !evt ) {
					// We need an event to calculate positions!
					return;
				}

				var self = IA;
				if ( !self.zoom ) {
					return;
				}

				var mouse_pos = LAPI.Pos.mousePosition( evt );
				var origin = LAPI.Pos.position( self.cover );

				if ( !mouse_pos ) {
					return;
				}

				if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) {
					IA.hide_zoom();
					return;
				}

				var dx = mouse_pos.x - origin.x;
				var dy = mouse_pos.y - origin.y;

				// dx, dy is the offset within the preview image. Align the zoom image accordingly.
				var top = -dy * self.zoom_factor + 100;
				var left = -dx * self.zoom_factor + 100;
				self.zoom.firstChild.firstChild.style.top = String( top ) + 'px';
				self.zoom.firstChild.firstChild.style.left = String( left ) + 'px';
				self.zoom.style.top = mouse_pos.y + 10 + 'px'; // Right below the mouse pointer

				// Horizontally keep it in view.
				var x = ( self.is_rtl ? mouse_pos.x - 10 : mouse_pos.x + 10 );
				if ( x < 0 ) { x = 0; }
				self.zoom.style.left = x + 'px';
				self.zoom.style.display = '';

				// Now that we have offsetWidth, correct the position.
				if ( self.is_rtl ) {
					x = mouse_pos.x - 10 - self.zoom.offsetWidth;
					if ( x < 0 ) {
						x = 0;
					}
				} else {
					var off = LAPI.Pos.scrollOffset();
					var view = LAPI.Pos.viewport();

					if ( x + self.zoom.offsetWidth > off.x + view.x ) {
						x = off.x + view.x - self.zoom.offsetWidth;
					}

					if ( x < 0 ) {
						x = 0;
					}
				}

				self.zoom.style.left = x + 'px';
			},

			hide_zoom: function ( evt ) {
				if ( !IA.zoom ) {
					return;
				}

				if ( evt ) {
					var mouse_pos = LAPI.Pos.mousePosition( evt );
					if ( LAPI.Pos.isWithin( IA.cover, mouse_pos.x, mouse_pos.y ) ) {
						return;
					}
				}

				IA.zoom.style.display = 'none';
			},

			createHelpLink: function () {
				var msg = ImageAnnotator.UI.get( 'wpImageAnnotatorHelp', false, true );
				if ( !msg || !msg.lastChild ) {
					return null;
				}

				// Make sure we use the right protocol for all images:
				var imgs = msg.getElementsByTagName( 'img' );
				var text;
				var tgt;
				if ( imgs ) {
					for ( var i = 0; i < imgs.length; i++ ) {
						var srcFixed = imgs[ i ].getAttribute( 'src', 2 ).replace( /^https?:/, document.location.protocol );
						imgs[ i ].src = srcFixed;
					}
				}

				if (
					msg.childNodes.length == 1 &&
					msg.firstChild.nodeName.toLowerCase() == 'a' &&
					!LAPI.DOM.hasClass( msg.firstChild, 'image' )
				) {
					msg.firstChild.id = 'ImageAnnotationHelpButton';
					return msg.firstChild; // Single link
				}

				// Otherwise, it's either a sequence of up to three images, or a span, followed by a
				// link.
				tgt = msg.lastChild;
				if ( tgt.nodeName.toLowerCase() != 'a' ) {
					tgt = mw.config.get( 'wgServer' ) + mw.config.get( 'wgArticlePath' ).replace( '$1', 'Help:Gadget-ImageAnnotator' );
				} else {
					tgt = tgt.href;
				}

				function make_handler( tgt ) {
					var target = tgt;
					return function ( evt ) {
						var e = evt || window.event;

						location.href = target;

						if ( e ) {
							return LAPI.Evt.kill( e );
						}

						return false;
					};
				}

				imgs = msg.getElementsByTagName( 'img' );

				if ( !imgs || !imgs.length ) {
					// We're supposed to have a spans giving the button text
					text = msg.firstChild;

					if ( text.nodeName.toLowerCase() === 'span' ) {
						text = LAPI.DOM.getInnerText( text );
					} else {
						text = 'Help';
					}

					return LAPI.DOM.makeButton(
						'ImageAnnotationHelpButton',
						text,
						make_handler( tgt )
					);
				} else {
					return Buttons.makeButton( imgs, 'ImageAnnotationHelpButton', make_handler( tgt ) );
				}
			},

			get_cover: function () {
				var self = IA;
				var shim;

				if ( !self.cover ) {
					var pos = {
						position: 'absolute',
						left: '0px',
						top: '0px',
						width: self.viewers[ 0 ].thumb.width + 'px',
						height: self.viewers[ 0 ].thumb.height + 'px'
					};

					self.cover = LAPI.make( 'div', null, pos );
					self.border = self.cover.cloneNode( false );

					Object.merge( {
						border: '3px solid green',
						top: '-3px',
						left: '-3px'
					}, self.border.style );

					self.cover.style.zIndex = 2000; // Above the tooltips

					if ( LAPI.Browser.is_ie ) {
						shim = LAPI.make( 'iframe', { frameBorder: 0, tabIndex: -1 }, pos );
						shim.style.filter = 'alpha(Opacity=0)'; // Ensure transparency

						// Unfortunately, IE6/SP2 has a "security setting" called "Binary and script
						// behaviors". If that is disabled, filters don't work, and our iframe would
						// appear as a white rectangle. Fix this by first placing the iframe just above
						// image (to block that windowed control) and then placing *another div* just
						// above that shim having the image as its background image.
						var imgZ = self.viewers[ 0 ].img.style.zIndex;

						if ( isNaN( imgZ ) ) {
							// Arbitrary, positive, > 1, < 500
							imgZ = 10;
						}

						shim.style.zIndex = imgZ + 1;
						self.ieFix = shim;

						// And now the bgImage div...
						shim = LAPI.make( 'div', null, pos );
						Object.merge(
							{
								top: '0px',
								backgroundImage: 'url(' + self.viewers[ 0 ].img.src + ')',
								zIndex: imgZ + 2
							},
							shim.style
						);
						self.ieFix2 = shim;
					}

					if ( LAPI.Browser.is_opera ) {
						// It appears that events just pass through completely transparent divs on Opera.
						// Hence we have to ensure that these events are killed even if our cover doesn't
						// handle them.
						shim = LAPI.make( 'div', null, pos );
						shim.style.zIndex = self.cover.style.zIndex - 1;
						LAPI.Evt.attach( shim, 'mousemove',
							function ( evt ) { return LAPI.Evt.kill( evt || window.event ); } );
						LAPI.Evt.attach( shim, 'mousedown',
							function ( evt ) { return LAPI.Evt.kill( evt || window.event ); } );
						LAPI.Evt.attach( shim, 'mouseup',
							function ( evt ) { return LAPI.Evt.kill( evt || window.event ); } );
						shim.style.cursor = 'default';
						self.eventFix = shim;
					}

					self.cover_visible = false;
				}

				return self.cover;
			},

			show_cover: function () {
				var self = IA;
				if ( self.cover && !self.cover_visible ) {
					if ( self.ieFix ) {
						self.viewers[ 0 ].img_div.appendChild( self.ieFix );
						self.viewers[ 0 ].img_div.appendChild( self.ieFix2 );
					}

					if ( self.eventFix ) {
						self.viewers[ 0 ].img_div.appendChild( self.eventFix );
					}

					self.viewers[ 0 ].img_div.appendChild( self.cover );
					self.cover_visible = true;
				}
			},

			hide_cover: function () {
				var self = IA;
				if ( self.cover && self.cover_visible ) {
					if ( self.ieFix ) {
						LAPI.DOM.removeNode( self.ieFix );
						LAPI.DOM.removeNode( self.ieFix2 );
					}

					if ( self.eventFix ) {
						LAPI.DOM.removeNode( self.eventFix );
					}

					LAPI.DOM.removeNode( self.cover );
					self.cover_visible = false;
				}
			},

			getRawItem: function ( what, scope ) {
				var node = null;

				if ( !scope || scope == document ) {
					node = LAPI.$( 'image_annotation_' + what );
				} else {
					node = getElementsByClassName( scope, '*', 'image_annotation_' + what );
					if ( node && node.length ) {
						node = node[ 0 ];
					} else {
						node = null;
					}
				}

				return node;
			},

			getItem: function ( what, scope ) {
				var node = IA.getRawItem( what, scope );
				if ( !node ) {
					return null;
				}

				return LAPI.DOM.getInnerText( node ).trim();
			},

			getIntItem: function ( what, scope ) {
				var x = IA.getItem( what, scope );
				if ( x !== null ) {
					x = parseInt( x, 10 );
				}

				return x;
			},

			findNote: function ( text, id ) {
				function find( text, id, delim ) {
					var start = delim.start.replace( '$1', id );
					var start_match = text.indexOf( start );
					if ( start_match < 0 ) {
						return null;
					}

					var end = delim.end.replace( '$1', id );
					var end_match = text.indexOf( end );
					if ( end_match < start_match + start.length ) {
						return null;
					}

					return {
						start: start_match,
						end: end_match + end.length
					};
				}

				var result = null;
				for ( var i = 0; i < IA.note_delim.length && !result; i++ ) {
					result = find( text, id, IA.note_delim[ i ] );
				}

				return result;
			},

			setWikitext: function ( pagetext ) {
				var self = IA;
				if ( self.wiki_read ) {
					return;
				}

				Array.forEach( self.viewers[ 0 ].annotations, function ( note ) {
					if ( note.model.id >= 0 ) {
						var span = self.findNote( pagetext, note.model.id );
						if ( !span ) {
							return;
						}

						// Now extract the wikitext
						var code = pagetext.substring( span.start, span.end );
						for ( var i = 0; i < self.note_delim.length; i++ ) {
							var start = self.note_delim[ i ].content_start.replace( '$1', note.model.id );
							var end = self.note_delim[ i ].content_end.replace( '$1', note.model.id );
							var j = code.indexOf( start );
							var k = code.indexOf( end );
							if ( j >= 0 && k >= 0 && k >= j + start.length ) {
								note.model.wiki = code.substring( j + start.length, k ).trim();
								return;
							}
						}
					}
				} );
				self.wiki_read = true;
			},

			setSummary: function ( summary, initial_text, note_text ) {
				if ( initial_text.contains( '$1' ) ) {
					var max = ( summary.maxlength || 200 ) - initial_text.length;
					if ( note_text ) {
						initial_text =
							initial_text.replace( '$1', ': ' + note_text.replace( '\n', ' ' ).substring( 0, max ) );
					} else {
						initial_text = initial_text.replace( '$1', '' );
					}
				}
				summary.value = initial_text;
			},

			getScript: function ( url, bypass_local_cache, bypass_caches ) {
				// Don't use LAPI here, it may not yet be available
				if ( bypass_caches ) {
					url += ( ( url.indexOf( '?' ) >= 0 ) ? '&' : '?' ) + 'dummyTimestamp=' + ( new Date() ).getTime();
				}

				// Avoid protocol-relative URIs (IE7 bug)
				if ( url.length >= 2 && url.substring( 0, 2 ) === '//' ) {
					url = document.location.protocol + url;
				}

				if ( bypass_local_cache ) {
					var s = document.createElement( 'script' );
					s.setAttribute( 'src', url );
					s.setAttribute( 'type', 'text/javascript' );
					document.getElementsByTagName( 'head' )[ 0 ].appendChild( s );
					return s;
				} else {
					return mw.loader.load( url );
				}
			},

			canEdit: function () {
				var self = IA;
				if ( self.may_edit ) {
					if ( !self.ajaxQueried ) {
						self.haveAjax = ( LAPI.Ajax.getRequest() != null );
						self.ajaxQueried = true;
						self.may_edit = self.haveAjax;
						if ( !self.may_edit && self.button_div ) {
							LAPI.DOM.removeChildren( self.button_div );
							self.button_div.appendChild( ImageAnnotator.UI.get( 'wpImageAnnotatorCannotEditMsg', false ) );
							self.viewers[ 0 ].msg.style.display = '';
							self.viewers[ 0 ].cannotEdit();
						}
					}
				}
				return self.may_edit;
			}

		}; // end IA

		// Backwards compatibility
		function getElementsByClassName( scope, tag, className ) {
			if ( window.jQuery ) {
				return jQuery( scope ).find( ( ( !tag || tag === '*' ) ? '' : tag ) + '.' + className );
			} else {
				// For non-WMF wikis that might not have jQuery (yet), use the wikibits.js getElementsByClassName
				return getElementsByClassName( scope, tag, className );
			}
		}

		window.ImageAnnotator = {
			install: function ( config ) {
				IA.install( config );
			}
		};

		// Start it. Bypass caches; but allow for 4 hours client-side caching. Small file.
		IA.getScript(
			mw.config.get( 'wgScript' ) + '?title=MediaWiki:ImageAnnotatorConfig.js&action=raw&ctype=text/javascript',
			true // No local caching!
		);

	}() ); // end local scope

} // end if (guard against double inclusions)

//

// <source lang="javascript">

/*

 Site-wide configurations and start of the ImageAnnotator gadget. Split into a
 separate file for three reasons:
 1. It separates the configuration from the core code, while still
 2. making it impossible for someone else (e.g. a malicious user) to override these
    defaults, and
 3. makes configuration changes available quickly: clients cache this file for four hours.

 Author: User:Lupo, September 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 :-)

 See https://commons.wikimedia.org/wiki/Help:Gadget-ImageAnnotator for documentation.
  • /

(function () {

 // Global settings. Edit these to configure ImageAnnotator for your Wiki. Note: these configurations
 // are here to prevent them to be overwritten by a user in his or her user scripts. BE EXTRA CAREFUL
 // IF YOU CHANGE THESE SETTINGS WHEN ImageAnnotator IS ALREADY DEPLOYED! Syntax or other errors here
 // may break ImageAnnotator for everyone!
 var config = {
   // By default, ImageAnnotator is enabled in all namespaces (except "Special", -1) for everyone,
   // except on the project's main page.
   // Here, you can define a list of namespaces where it is additionally disabled.
   viewingEnabled : function () {
     var action = mw.config.get('wgAction');
     return mw.config.get('wgNamespaceNumber') >= 0
            && !mw.config.get('wgIsMainPage')
            && (action == 'view' || action == 'purge' || action == 'submit'); 
   },
   // For instance, to disable ImageAnnotator on all talk pages, replace the function body above by
   //     return (wgNamespaceNumber & 1) == 0;
   // Or, to disable it in the category namespace and on article talk pages, you could use
   //     return (wgNamespaceNumber != 14) && (wgNamespaceNumber != 1);
   // To enable viewing only on file description pages and on pages in the project namespace:
   //     return (wgNamespaceNumber == 6) || (wgNamespaceNumber == 4);
   // To enable viewing only for logged-in users, use
   //     return wgUserName !== null;
   // To switch off viewing of notes on the project's main page, use
   //     return !wgIsMainPage;
   // By default, editing is enabled for anyone on the file description page or the page that contains
   // the substitution of template ImageWithNotes. Here, you can restrict editing even more, for
   // instance by allowing only autoconfirmed users to edit notes through ImageAnnotator. Note that
   // editing is only allowed if viewing is also allowed.
   editingEnabled : function ()
   {
     var action = mw.config.get('wgAction');
     var nsNum  = mw.config.get('wgNamespaceNumber');
     var pageView = (action == 'view' || action == 'purge')
                    && location.href.search(/[?&](diff|oldid)=/) < 0;
     if (!pageView) return false;
     if (   (nsNum == 2 || nsNum == 3)
         && mw.config.get('wgUserName') && mw.config.get('wgTitle').replace(/ /g, '_').indexOf(mw.config.get('wgUserName').replace (/ /g, '_')) === 0
        ) {
       // Allow anyone to edit notes in their own user space (sandboxes!)
       return true;
     }
     // Otherwise restrict editing of notes to autoconfirmed users.
     return (mw.config.get('wgUserGroups').join (' ') + ' ').indexOf ('confirmed ') >= 0; // Confirmed and autoconfirmed
     
   },
   // To allow only autoconfirmed users to edit, use
   //     return (' ' + wgUserGroups.join (' ') + ' ').indexOf (' autoconfirmed ') >= 0;
   // The following example restricts editing on file description pages to autoconfirmed users,
   // and otherwise allows edits only on subpages in the project namespace (for instance, featured
   // image nominations...), but allows editing there for anyone.
   //     return (   (   wgNamespaceNumber == 6
   //                 && (' ' + wgUserGroups.join(' ') + ' ').indexOf(' autoconfirmed ') >= 0
   //                )
   //             || (wgNamespaceNumber == 4 && wgPageName.indexOf('/') > 0)
   //            );
   // Note that wgUserName is null for IPs.
   // If editing is allowed at all, may the user remove notes through the ImageAnnotator interface?
   // (Note that notes can be removed anyway using a normal edit to the page.)
   mayDelete: function () {
     return true;
   },
   // If the user may delete notes, may he or she delete with an empty deletion reason?
   emptyDeletionReasonAllowed: function () {
     var groups = ' ' + mw.config.get('wgUserGroups').join(' ') + ' ';
     return groups.indexOf(' sysop ') >= 0 || groups.indexOf(' rollbacker ') >= 0;
   },
   // If the user may delete, may he or she bypass the prompt for a deletion reason by setting
   // var ImageAnnotator_noDeletionPrompt = true;
   // in his or her user scripts?
   mayBypassDeletionPrompt : function ()
   {
     return (' ' + mw.config.get('wgUserGroups').join(' ') + ' ').indexOf(' sysop ') >= 0;
   },
   // If viewing is enabled at all, you can specify here whether viewing notes on thumbnails (e.g.,
   // in articles) is switched on. Logged-in users can augment this by disabling viewing notes on
   // thumbnails on a per-namespace basis using the global variable ImageAnnotator_no_thumbs.
   thumbsEnabled : function ()
   {
     return true;
   },
   // For instance, to switch off viewing of notes on thumbnails for IPs in article space, you'd use
   //     return !(namespaceNumber == 0 && wgUserName === null);
   // If viewing is enabled at all, you can define whether viewing notes on non-thumbnail images is
   // switched on. Logged-in users can augment this by disabling viewing notes on non-thumbnails
   // on a per-namespace basis using the global variable ImageAnnotator_no_images.
   generalImagesEnabled: function () {
     return true;
   },
   // If thumbs or general images are enabled, you can define whether this shall apply only to local
   // images (return false) or also to images that reside at the shared repository (the Commons). In
   // the 'File:' namespace, displaying notes on shared images is always enabled. (Provided viewing
   // notes is enabled at all there. If you've disabled viewing notes in all namespaces including
   // the 'File:' namespace for non-logged-in users, they won't see notes on images from the Commons
   // either, even if you enable it here.)
   sharedImagesEnabled: function () {
     return true;
   },
   // If thumbs or general images are enabled, you can define here whether you want to allow the
   // script to  display the notes or just a little indicator (an icon in the upper left--or right
   // on rtl wikis--corner of the image). The parameters given are
   //   name         string
   //     the name of the image, starting with "File:"
   //   is_local     boolean
   //     true if the image is local, false if it is from the shared repository
   //   thumb        object {width: integer, height: integer}
   //     Size of the displayed image in the article, in pixels
   //   full_img     object {width: integer, height: integer}
   //     Size of the full image as uploaded, in pixels
   //   nof_notes    integer 
   //     Number of notes on the image
   //   is_thumbnail boolean
   //     true if the image is a thumbnail, false otherwise
   inlineImageUsesIndicator : function (name, is_local, thumb, full_img, nof_notes, is_thumbnail)
   {
     // Of course you could also use wgNamespace or any other of the wg-globals here.
     return    (is_thumbnail && !is_local)
            || ((   thumb.width < 250 && thumb.height < 250
                 && (thumb.width < full_img.width || thumb.height < full_img.height)
                )
                  ? nof_notes > 10 : false
               );
     // This default displays only an indicator icon for non-local thumbnails,
     // and for small images that are scaled down, but have many notes
   },
   // If notes are displayed on an image included in an article, ImageAnnotator normally adds a
   // caption indicating the presence of notes. If you want to suppress this for all images included
   // in articles, return false. To suppress the caption only for thumbnails, but not for otherwise
   // included images, return !is_thumbnail. To suppress the caption for all images but thumbnails,
   // return is_thumbnail. The parameters are the same as for the function inlineImageUsesIndicator
   // above.
   displayCaptionInArticles : function (name, is_local, thumb, full_img, nof_notes, is_thumbnail)
   {
     return true;
   },
   // Different wikis may have different image setups. For the Wikimedia projects, the image
   // servers are set up to generate missing thumbnails on the fly, so we can just construct
   // a valid thumbnail url to get a thumbnail, even if there isn't one of that size yet.
   // Return true if your wiki has a similar setup. Otherwise, return false.
   thumbnailsGeneratedAutomatically : function ()
   {
     return true;
   },
   // Determine whether an image is locally stored or comes from a central repository. For wikis
   // using the Commons as their central repository, this should not need changing.
   imageIsFromSharedRepository : function (img_url)
   {
     return mw.config.get('wgServer').indexOf('/commons') < 0 && img_url.indexOf('/commons') >= 0;
   },
   // Return the URL of the API at the shared file repository. Again, for wikis using the Commons
   // as their central repository, this should not need changing. If your wiki is accessible through
   // https, it's a good idea to also make the shared repository accessible through https and return
   // that secure URL here to avoid warnings about accessing a non-secure site from a secure site.
   sharedRepositoryAPI : function ()
   {
     return '//commons.wikimedia.org/w/api.php';
   },
   // Default coloring. Each note's rectangle has an outer and an inner border.
   outer_border  : '#666666', // Gray
   inner_border  : 'yellow',
   active_border : '#FFA500', // Orange, for highlighting the rectangle of the active note
   new_border    : 'red',     // For drawing rectangles
   // Default threshold for activating the zoom (can be overridden by users).
   zoom_threshold : 8.0,
   UI : {
     defaultLanguage : mw.config.get('wgContentLanguage'), // Don't change this!
     // Translate the texts below into the wgContentLanguage of your wiki. These are used as
     // fallbacks if the localized UI cannot be loaded from the server.
     defaults: {
        wpImageAnnotatorDelete        : 'Delete'
       ,wpImageAnnotatorEdit          : 'Edit'
       ,wpImageAnnotatorSave          : 'Save'
       ,wpImageAnnotatorCancel        : 'Cancel'
       ,wpImageAnnotatorPreview       : 'Preview'
       ,wpImageAnnotatorRevert        : 'Revert'
       ,wpTranslate                   : 'translate'
       ,wpImageAnnotatorAddButtonText : 'Add a note'
       ,wpImageAnnotatorAddSummary    :
         'Adding image note$1'
       ,wpImageAnnotatorChangeSummary :
         'Changing image note$1'
       ,wpImageAnnotatorRemoveSummary :
         'Removing image note$1'
       ,wpImageAnnotatorHasNotesShort : 'This file has annotations.'
       ,wpImageAnnotatorHasNotesMsg   :
          'This file has annotations. Move the mouse pointer over the image to see them.'
       ,wpImageAnnotatorEditNotesMsg  :
          '\xa0To edit the notes, visit page <a href="#">x</a>.'
       ,wpImageAnnotatorDrawRectMsg   :
          'Draw a rectangle onto the image above (mouse click, then drag and release)'
       ,wpImageAnnotatorEditorLabel   :
          'Text of the note (may include '
        + '<a href="//meta.wikimedia.org/wiki/Help:Reference_card">Wiki markup</a>)'
       ,wpImageAnnotatorSaveError  :
          ''
        + 'Could not save your note (edit conflict or other problem).'
        + ' '
        + 'Please copy the text in the edit box below and insert it manually by '
        + '<a href="'
        + mw.config.get('wgArticlePath').replace ('$1', encodeURIComponent(mw.config.get('wgPageName')))
        + '?action=edit">editing this page</a>.'
       ,wpImageAnnotatorCopyright :
          'The note will be published multi-licensed as '
        + '<a href="http://creativecommons.org/licenses/by-sa/3.0/">CC-BY-SA-3.0</a> and '
        + '<a href="http://www.gnu.org/copyleft/fdl.html">GFDL</a>, versions 1.2 and 1.3. '
        + 'Please read our <a href="//wikimediafoundation.org/wiki/Terms_of_Use">terms '
        + 'of use</a> for more details.'
       ,wpImageAnnotatorDeleteReason :
          'Why do you want to remove this note?'
       ,wpImageAnnotatorDeleteConfirm :
          'Do you really want to delete this note?'
       ,wpImageAnnotatorHelp          : 
          '<a href="//commons.wikimedia.org/wiki/Help:Gadget-ImageAnnotator" '
        + 'title="Help">Help</a>'
       // The following image should be a GIF or an 8bit indexed PNG with transparent background,
       // to make sure that even IE6 displays the transparency correctly. A normal 32bit PNG might
       // display a transparent background as white on IE6.
       ,wpImageAnnotatorIndicatorIcon :
          ''
        + '<img src="//upload.wikimedia.org/wikipedia/commons/8/8a/Gtk-dialog-info-14px.png" '
        + 'width="14" height="14" title="This file has annotations" />'
        + ''
       ,wpImageAnnotatorCannotEditMsg :
          'To modify annotations, your browser needs to have the '
        + '<a href="//en.wikipedia.org/wiki/XMLHttpRequest">XMLHttpRequest</a> '
        + 'object. Your browser does not have this object or does not allow it to be used '
        + '(in Internet Explorer, it may be in a switched off ActiveX component), and '
        + 'thus you cannot modify annotations. We\'re sorry for the inconvenience.'
     }
   }
 }; // End site-wide config.
 // DO NOT CHANGE ANYTHING BELOW THIS LINE

 // Start of ImageAnnotator
 if (config.viewingEnabled()) {
     jQuery(function () {
        ImageAnnotator.install(config);
     });
 }
 

})(); /* 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: 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; }, /** * Get the preview image on a file description page. * @param {string} [title] The title of the image to get, * defaults to the current page name without namespace. * @param {Document} [doc] The document to get the image from, * defaults to the current document. * @returns {HTMLImageElement|null} The preview image, or `null` * if it could not be found. */ 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' ); if ( src && !src.match( /^data/ ) ) { src = decodeURIComponent( src ).replace( '%26', '&' ); if ( src.match( 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) // /** Wikitext sanitation for MediaWiki

Author: User:Lupo, January 2008 License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)

Choose whichever license of these you like best :-)

  • /

// /* global mw:false, TextCleaner:true */ /* eslint no-control-regex:0, one-var:0, vars-on-top:0, camelcase:0, curly:0, space-in-parens:0, computed-property-spacing:0, array-bracket-spacing:0 */ /* jshint curly:false, eqnull:true, laxbreak:true */ ( function () { 'use strict'; window.TextCleaner = { imgNamespaceNames: null, // This function attempts to construct well-formed wikitext from input that may contain // possibly broken wikitext. // // Note: even just a half-baked sanitation of wikitext is hyper-complex due to the presence // of templates, and due to the fact that image thumbnail captions may themselves contain // links. This implementation catches the most common errors (such as forgetting to close a // template or a link), and even some more elaborate ones. With enough malice, this sanitation // can still be broken by user input such that the result is not well-formed wikitext as the // parser at the servers would like to have it. (It's still possible that the result is broken // wikitext, if the input was broken wikitext. But it never transforms well-formed wikitext // into broken wikitext.) // // If 'only_thumbs' is true, all [[Image: links are changed to [[:Image:, unless the original // image link was a thumbnail or had a width smaller than 300px specified. // // WARNING: do *not* attempt to use this to process large texts (e.g., a whole article). It is // probably rather inefficient due to the many substrings that are generated. This function is // primarily intended to be used to clean up user input in forms, which are typically rather // short. sanitizeWikiText: function ( input, only_thumbs ) { if ( input.search( /[\][}{]|<nowiki(\s[^>]*)?>|<!--/ ) < 0 ) { // No critical characters return input; } if ( !TextCleaner.imgNamespaceNames ) { TextCleaner.imgNamespaceNames = []; var namespaceIds = mw.config.get( 'wgNamespaceIds' ); if ( namespaceIds ) { for ( var name in namespaceIds ) { if ( namespaceIds[name] === 6 ) { // Image namespace TextCleaner.imgNamespaceNames.push( name ); } } } // Make sure that we have the two canonical names TextCleaner.imgNamespaceNames.push( 'Image' ); TextCleaner.imgNamespaceNames.push( 'File' ); // If your wiki does not have wgNamespaceIds, add aliases or localized namespace names here! } var consumed = [ 0, 0 ]; // For image captions. Image caption may contain links, and may even contain images. // The current MediaWiki parser actually allows this only once. For deeper recursions, // it fails. But here, it's actually easier to implement no limit. var base_regexp = new RegExp( '[\\x01\\x02\\x03\\x04[\\]\\|\\x05\\x06\\x07\\x08]' + '|<nowiki(\\s[^>]*)?>|<!--', 'i' // Ignore case ); var nowiki_regexp = new RegExp( '<\\/nowiki(\\s[^>]*)?>|<!--', 'i' ); var allow_only_thumbs = only_thumbs; function sanitize( s, with_links, caption_level, allow_thumbs, break_at_pipe, with_tables, with_galleries ) { if ( !s || !s.length ) { if ( caption_level > 0 ) { if ( consumed.length < caption_level ) { consumed.push( 0 ); } else { consumed[caption_level - 1] = 0; } } return s; } var result = ''; var ch = ''; var initial_length = s.length; var get_out = false; var in_nowiki = false; var endings = null; // Stack recording template and table nesting var next; var regexp = base_regexp; function push_end( val ) { if ( !endings ) { endings = [ val ]; } else { endings.push( val ); } } function pop_end() { if ( !endings ) { // Shouldn't happen return null; } var result; if ( endings.length === 1 ) { result = endings[0]; endings = null; } else { result = endings[endings.length - 1]; endings.length--; } return result; } function get_initial( i, s ) { for ( var j = 0; j < TextCleaner.imgNamespaceNames.length; j++ ) { if ( s.length >= i + TextCleaner.imgNamespaceNames[j].length + 1 ) { var t = s.substr( i, TextCleaner.imgNamespaceNames[j].length + 1 ); if ( t.toLowerCase() === ( TextCleaner.imgNamespaceNames[j].toLowerCase() + ':' ) ) { return t; } } } return null; } while ( s.length > 0 && !get_out ) { next = s.search( regexp ); if ( next < 0 ) { result += s; break; } ch = s.charAt( next ); var i = -1; var j = -1; var k = -1; switch ( ch ) { case '<': // Nowiki or HTML comment. Must be closed. if ( s.charAt( next + 1 ) === '!' ) { // HTML comment. Cannot be nested. i = s.indexOf( '-->', next + 3 ); if ( i < 0 ) { result += s + '-->'; s = ''; } else { result += s.substring( 0, i + 3 ); s = s.substring( i + 3 ); } } else if ( s.charAt( next + 1 ) === 'n' ) { // Nowiki may contain HTML comments! in_nowiki = true; regexp = nowiki_regexp; result += s.substring( 0, next + 7 ); s = s.substring( next + 7 ); } else { // End of nowiki. Searched for and found only if in_nowiki === true in_nowiki = false; regexp = base_regexp; i = s.indexOf( '>', next + 1 ); // End of tag result += s.substring( 0, i + 1 ); s = s.substring( i + 1 ); } break; case '\x05': // Table start if ( !with_tables ) { result += s.substring( 0, next ); get_out = true; break; } /* fall through */ case '\x07': if ( ch === '\x07' && !with_galleries ) { result += s.substring( 0, next ); get_out = true; break; } /* fall through */ case '\x01': // Start of template, table, or gallery result += s.substring( 0, next + 1 ); push_end( String.fromCharCode( ch.charCodeAt( 0 ) + 1 ).charAt( 0 ) ); s = s.substring( next + 1 ); break; case '\x06': // Table end if ( break_at_pipe && !endings ) { result += s.substring( 0, next ); get_out = true; break; } /* fall through */ case '\x02': // End of a template or table result += s.substring( 0, next ); if ( !endings || endings[endings.length - 1] !== ch ) { // Spurious template or table end if ( ch === '\x02' ) { result += '}}'; } else { result += '|}'; } } else { result += pop_end(); } s = s.substring( next + 1 ); break; case '\x08': // End of gallery result += s.substring( 0, next + 1 ); if ( endings && endings[endings.length - 1] === ch ) { pop_end(); } s = s.substring( next + 1 ); break; case '\x03': case '[': { if ( !with_links && !endings ) { get_out = true; break; } // Image links must be treated specially, since they may contain nested links // in the caption! var initial = null; // If set, it's 'image:' or 'file:' and we have an image link i = next; while ( i < s.length && s.charAt( i ) === ch ) { i++; } if ( ch === '\x03' && i < s.length && s.charAt( i ) === '[' ) { i++; } initial = get_initial( i, s ); // Scan ahead. We'll break at the next top-level | or ] or ]] or [ or [[ or {| or |} var lk_text = sanitize( s.substring( i ), false, // No links at top-level allowed caption_level + 1, false, // No thumbs true, // Break at pipe false, // No tables false // No galleries ); var lk_text_length = consumed[caption_level]; j = i + lk_text_length; if ( j >= s.length ) { // Used up the whole text: [[Foo or [bar if ( initial && allow_only_thumbs ) { // Should in any case have started with [[, not [ result += s.substring( 0, i - 1 ) + '\x03:' + initial + lk_text.substring( initial.length ) + '\x04'; } else { result += s.substring( 0, i ) + lk_text + ( ( s.charAt( i - 1 ) === '[' ) ? ']' : '\x04'); } s = ''; break; } if ( s.charAt( j ) === '|' ) { k = j; } else { k = -1; } if ( k < 0 ) { // No pipe found: we should be on the closing ]] or ] or [[Foo]] or [bar] if ( initial && allow_only_thumbs ) { // Should in any case have started with [[, not [ result += s.substring( 0, i - 1 ) + '\x03:' + initial + lk_text.substring( initial.length ) + '\x04'; } else { result += s.substring( 0, i ) + lk_text + ( ( s.charAt( i - 1 ) === '[') ? ']' : '\x04' ); } if ( s.charAt( j ) === ']' || s.charAt( j ) === '\x04' ) { // Indeed closing the link s = s.substring( j + 1 ); } else { s = s.substring( j ); } break; } else { var caption = null; var used = 0; // Pipe found. if ( !initial ) { // Not an image link. Must be something like [[Foo|Bar]]. caption = sanitize( s.substring( k + 1 ), // No links, please false, caption_level + 1, // No thumbs either false, // Don't care about pipes false, // Allow tables (yes, parser allows that!) true, // Allow galleries (?) true ); // Now we're at [[, [, ]], or ] used = consumed[caption_level]; result += s.substring( 0, i ) + lk_text + '|' + caption + ( ( s.charAt( i - 1 ) === '[') ? ']' : '\x04' ); } else { var q = s.substring( k ); // We assume that there are no templates, nowikis, and other nasty things // in the parameters. Search forward until the next [, {, ], } var l = q.search( /[\x01\x02\x03[\x04\]{}\x05\x06\x07\x08]/ ); if ( l < 0 ) { l = q.length; } if ( l + 1 < q.length ) { q = q.substring( 0, l + 1 ); } var is_thumb = q.search( /\|\s*thumb(nail)?\s*[|\x04]/ ) >= 0; var img_width = /\|\s*(\d+)px\s*[|\x04]/.exec( q ); if ( img_width && img_width.length > 1 ) { img_width = parseInt( img_width[1], 10 ); if ( isNaN( img_width ) ) { img_width = null; } } else { img_width = null; } if ( !img_width ) { img_width = is_thumb ? 180 : 301; } var is_small = img_width <= 300; // Caption starts at the last pipe before l. If that is a parameter, // it doesn't hurt. var m = k + q.lastIndexOf( '|', l ); caption = sanitize( s.substring( m + 1 ), // Allow links only if it's a thumb is_thumb, caption_level + 1, allow_thumbs && is_thumb, // Don't break at pipe false, // Tables only if it's a thumb is_thumb, // Allow galleries for thumbs (?) is_thumb ); used = consumed[caption_level]; // caption used 'used' chars from m+1, s.charAt(m+1+used) === '\x04' is_thumb = allow_thumbs && is_small; if ( is_thumb || !allow_only_thumbs ) { result += s.substring( 0, i - 1 ) + '\x03' + lk_text; } else { result += s.substring( 0, i - 1 ) + '\x03:' + initial + lk_text.substring( initial.length); } result += s.substring( k, m + 1 ) + caption + '\x04'; k = m; } next = k + 1 + used; if ( next < s.length ) { if ( s.charAt( next ) !== '\x04' ) { s = s.substring( next ); } else { s = s.substring( next + 1 ); } } else { s = ''; } } break; } case '\x04': case ']': // Extra bracket. result += s.substring( 0, next ); if ( !caption_level && !break_at_pipe ) { result += ( ch === ']' ? ']' : ']]' ); s = s.substring( next + 1 ); } else { get_out = true; } break; case '|': result += s.substring( 0, next ); if ( break_at_pipe && !endings ) { // Pipe character at top level get_out = true; } else { if ( !caption_level && !break_at_pipe && !endings ) { result += '|'; // Top-level pipe character } else { result += '|'; } s = s.substring( next + 1 ); } break; } // end switch } // end while if ( in_nowiki ) { result += ''; // Make sure this nowiki is closed. }

// Close open templates and tables while ( endings ) { ch = pop_end(); result += ( ch === '\x06' ? '\n' : ) + ch; }

if ( caption_level > 0 ) { var used_up = initial_length - ( get_out ? ( s.length - next ) : 0 ); if ( consumed.length < caption_level ) { consumed.push( used_up ); } else { consumed[caption_level - 1] = used_up; } }

return result; }

// Replace multi-character tokens by one-character placeholders, simplifying the // subsequent processing. var s = input.replace( /\{\{/g, '\x01' ) .replace( /\n\s*\|\}\}\}/g, '\n\x06\x02' ) // Table end + template end .replace( /\}\}/g, '\x02' ) .replace( /\[\[/g, '\x03' ) .replace( /\]\]/g, '\x04' ) .replace( /\n\s*\{\|/g, '\n\x05' ) // Table start and end must be on own line .replace( /^\s*\{\|/, '\x05' ) // Table start at the very beginning .replace( /\n\s*\|\}/g, '\n\x06' ) // (we strip leading whitespace) .replace( /<\s*gallery\s*>/g, '\x07' ) .replace( /<\/\s*gallery\s*>/g, '\x08' );

s = sanitize( s, true, 0, true, false, true, true );

// with links, allow thumbs, don't break at pipe, allow tables, allow galleries return s.replace( /\x01/g, '{{' ) .replace( /\x02/g, '}}' ) .replace( /\x03/g, '[[' ) .replace( /\x04/g, ']]' ) .replace( /\x05/g, '{|' ) .replace( /\x06/g, '|}' )

.replace( /\x07/g, '

' );

} }; }() ); // </nowiki>

//

/*
  Cross-browser tooltip support for MediaWiki.
  
  Author: [[User:Lupo]], March 2008
  License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
  
  Choose whichever license of these you like best :-)

  Based on ideas gleaned from prototype.js and prototip.js.
  http://www.prototypejs.org/
  http://www.nickstakenburg.com/projects/prototip/
  However, since prototype is pretty large, and prototip had some
  problems in my tests, this stand-alone version was written.
  
  Note: The fancy effects from scriptaculous have not been rebuilt.
  http://script.aculo.us/
  
  See http://commons.wikimedia.org/wiki/MediaWiki_talk:Tooltips.js for
  more information including documentation and examples.
*/

var EvtHandler = {
  listen_to : function (object, node, evt, f)
  {
    var listener = EvtHandler.make_listener (object, f);
    EvtHandler.attach (node, evt, listener);
  },
  
  attach : function (node, evt, f)
  {
    if (node.attachEvent) node.attachEvent ('on' + evt, f);
    else if (node.addEventListener) node.addEventListener (evt, f, false);
    else node['on' + evt] = f;
  },
  
  remove : function (node, evt, f)
  {
    if (node.detachEvent) node.detachEvent ('on' + evt, f);
    else if (node.removeEventListener) node.removeEventListener (evt, f, false);
    else node['on' + evt] = null;
  },
  
  make_listener : 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]); }
  },

  // @todo FIXME: remove fully now that support for ancient IEs is gone (January 2021)
  mouse_offset : function ()
  {
    return null;
  },
  
  killEvt : 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 EvtHandler

var Buttons = {
  
  buttonClasses : {},
  
  createCSS : function (imgs, sep, id)
  {
    var width   = imgs[0].getAttribute ('width');
    var height  = imgs[0].getAttribute ('height');
    try {
      // The only way to set the :hover and :active properties through Javascript is by
      // injecting a piece of CSS. There is no direct access within JS to these properties.
      var sel1  = "a" + sep + id;
      var prop1 = "border:0; text-decoration:none; background-color:transparent; "
                + "width:" + width + "px; height:" + height + "px; "
                + "display:inline-block; "
                + "background-position:left; background-repeat:no-repeat; "
                + "background-image:url(" + imgs[0].src + ");";
      var sel2  = null, prop2 = null, sel3  = null, prop3 = null; // For IE...
      var css   = sel1 + ' {' + prop1 + '}\n';                    // For real browsers
      if (imgs.length > 1 && imgs[1]) {
        sel2  = "a" + sep + id + ":hover";
        prop2 = "background-image:url(" + imgs[1].src + ");";
        css   = css + sel2 + ' {' + prop2 + '}\n';
      }
      if (imgs.length > 2 && imgs[2]) {
        sel3  = "a" + sep + id + ":active"
        prop3 = "background-image:url(" + imgs[2].src + ");";
        css   = css + sel3 + ' {' + prop3 + '}\n';
      }
      // Now insert a style sheet with these properties into the document (or rather, its head).
      var styleElem = document.createElement( 'style' );
      styleElem.setAttribute ('type', 'text/css');
      try {
        styleElem.appendChild (document.createTextNode (css));
        document.getElementsByTagName ('head')[0].appendChild (styleElem);
      } catch (ie_bug) {
        // Oh boy, IE has a big problem here
        document.getElementsByTagName ('head')[0].appendChild (styleElem);
//        try {
          styleElem.styleSheet.cssText = css;
/*
        } catch (anything) {
          if (document.styleSheets) {
            var lastSheet = document.styleSheets[document.styleSheets.length - 1];          
            if (lastSheet && typeof (lastSheet.addRule) != 'undefined') {
              lastSheet.addRule (sel1, prop1);
              if (sel2) lastSheet.addRule (sel2, prop2);
              if (sel3) lastSheet.addRule (sel3, prop3);
            }
          }
        }
*/
      }
    } catch (ex) {
      return null;
    }
    if (sep == '.') {
      // It's a class: remember the first image
      Buttons.buttonClasses[id] = imgs[0];
    }
    return id;
  }, // end createCSS
  
  createClass : function (imgs, id)
  {
    return Buttons.createCSS (imgs, '.', id);
  },
  
  makeButton : function (imgs, id, handler, title)
  {
    var success     = false;
    var buttonClass = null;
    var content     = null;
    if (typeof (imgs) == 'string') {
      buttonClass = imgs;
      content     = Buttons.buttonClasses[imgs];
      success     = (content != null);
    } else {
      success     = (Buttons.createCSS (imgs, '#', id) != null);
      content     = imgs[0];
    }
    if (success) {
      var lk = document.createElement ('a');
      lk.setAttribute
        ('title', title || content.getAttribute ('alt') || content.getAttribute ('title') || "");
      lk.id = id;
      if (buttonClass) lk.className = buttonClass;
      if (typeof (handler) == 'string') {
        lk.href = handler;
      } else {
        lk.href = '#'; // Dummy, overridden by the onclick handler below.
        lk.onclick = function (evt)
          {
            var e = evt || window.event; // W3C, IE
            try {handler (e);} catch (ex) {};
            return EvtHandler.killEvt (e);
          };
      }
      content = content.cloneNode (true);
      content.style.visibility = 'hidden';
      lk.appendChild (content);
      return lk;
    } else {
      return null;
    }
  } // end makeButton
  
} // end Button

var Tooltips = {
  // Helper object to force quick closure of a tooltip if another one shows up.
  debug    : false,  
  top_tip  : null,
  nof_tips : 0,

  new_id : function ()
  {
    Tooltips.nof_tips++;
    return 'tooltip_' + Tooltips.nof_tips;
  },

  register : function (new_tip)
  {
    if (Tooltips.top_tip && Tooltips.top_tip != new_tip) Tooltips.top_tip.hide_now ();
    Tooltips.top_tip = new_tip;
  },
  
  deregister : function (tip)
  {
    if (Tooltips.top_tip == tip) Tooltips.top_tip = null;
  },

  close : function ()
  {
    if (Tooltips.top_tip) {
      Tooltips.top_tip.hide_now ();
      Tooltips.top_tip = null;
    }
  }
}

var Tooltip = function () {this.initialize.apply (this, arguments);}
// This is the Javascript way of creating a class. Methods are added below to Tooltip.prototype;
// one such method is 'initialize', and that will be called when a new instance is created.
// To create instances of this class, use var t = new Tooltip (...);

// Location constants
Tooltip.MOUSE             = 0; // Near the mouse pointer
Tooltip.TRACK             = 1; // Move tooltip when mouse pointer moves
Tooltip.FIXED             = 2; // Always use a fixed poition (anchor) relative to an element

// Anchors
Tooltip.TOP_LEFT          = 1;
Tooltip.TOP_RIGHT         = 2;
Tooltip.BOTTOM_LEFT       = 3;
Tooltip.BOTTOM_RIGHT      = 4;

// Activation constants
Tooltip.NONE              = -1; // You must show the tooltip explicitly in this case.
Tooltip.HOVER             =  1;
Tooltip.FOCUS             =  2; // always uses the FIXED position
Tooltip.CLICK             =  4;
Tooltip.ALL_ACTIVATIONS   =  7;

// Deactivation constants

Tooltip.MOUSE_LEAVE       =  1; // Mouse leaves target, alternate target, and tooltip
Tooltip.LOSE_FOCUS        =  2; // Focus changes away from target
Tooltip.CLICK_ELEM        =  4; // Target is clicked
Tooltip.CLICK_TIP         =  8; // Makes only sense if not tracked
Tooltip.ESCAPE            = 16;
Tooltip.ALL_DEACTIVATIONS = 31;
Tooltip.LEAVE             = Tooltip.MOUSE_LEAVE | Tooltip.LOSE_FOCUS;


Tooltip.prototype =
{
  initialize : function (on_element, tt_content, opt, css)
  {
    if (!on_element || !tt_content) return;
    this.tip_id      = Tooltips.new_id ();
    // Registering event handlers via attacheEvent on IE is apparently a time-consuming
    // operation. When you add many tooltips to a page, this can add up to a noticeable delay.
    // We try to mitigate that by only adding those handlers we absolutely need when the tooltip
    // is created: those for showing the tooltip. The ones for hiding it again are added the
    // first time the tooltip is actually shown. We thus record which handlers are installed to
    // avoid installing them multiple times:
    //   event_state: -1 : nothing set, 0: activation set, 1: all set
    //   tracks:      true iff there is a mousemove handler for tracking installed.
    // This change bought us about half a second on IE (for 13 tooltips on one page). On FF, it
    // doesn't matter at all; in Firefoy, addEventListener is fast anyway.
    this.event_state = -1;
    this.tracks      = false;
    // We clone the node, wrap it, and re-add it at the very end of the
    // document to make sure we're not within some nested container with
    // position='relative', as this screws up all absolute positioning
    // (We always position in global document coordinates.)
    // In my tests, it appeared as if Nick Stakenburg's "prototip" has
    // this problem...
    if (typeof (tt_content) == 'function') {
      this.tip_creator = tt_content;
      this.css         = css;
      this.content     = null;
    } else {
      this.tip_creator = null;
      this.css         = null;
      if (tt_content.parentNode) {
        if (tt_content.ownerDocument != document)
          tt_content = document.importNode (tt_content, true);
        else
          tt_content = tt_content.cloneNode (true);
      }
      tt_content.id = this.tip_id;
      this.content  = tt_content;
    }
    // Wrap it
    var wrapper = document.createElement ('div');
    wrapper.className = 'tooltipContent';
    if (this.content) wrapper.appendChild (this.content);
    this.popup = document.createElement ('div');
    this.popup.style.display = 'none';
    this.popup.style.position = 'absolute';
    this.popup.style.top = "0px";
    this.popup.style.left = "0px";
    this.popup.appendChild (wrapper);
    // Set the options
    this.options = {
       mode         : Tooltip.TRACK              // Where to display the tooltip.
      ,activate     : Tooltip.HOVER              // When to activate
      ,deactivate   : Tooltip.LEAVE | Tooltip.CLICK_ELEM | Tooltip.ESCAPE // When to deactivate
      ,mouse_offset : {x: 5, y: 5, dx: 1, dy: 1} // Pixel offsets and direction from mouse pointer
      ,fixed_offset : {x:10, y: 5, dx: 1, dy: 1} // Pixel offsets from anchor position
      ,anchor       : Tooltip.BOTTOM_LEFT        // Anchor for fixed position
      ,target       : null                       // Optional alternate target for fixed display.
      ,max_width    :    0.6         // Percent of window width (1.0 == 100%)
      ,max_pixels   :    0           // If > 0, maximum width in pixels
      ,z_index      : 1000           // On top of everything
      ,open_delay   :  500           // Millisecs, set to zero to open immediately
      ,hide_delay   : 1000           // Millisecs, set to zero to close immediately
      ,close_button : null           // Either a single image, or an array of up to three images
                                     // for the normal, hover, and active states, in that order
      ,onclose      : null           // Callback to be called when the tooltip is hidden. Should be
                                     // a function taking a single argument 'this' (this Tooltip)
                                     // an an optional second argument, the event.
      ,onopen       : null           // Ditto, called after opening.
    };
    // The lower of max_width and max_pixels limits the tooltip's width.
    if (opt) { // Merge in the options
      for (var option in opt) {
        if (option == 'mouse_offset' || option == 'fixed_offset') {
          try {
            for (var attr in opt[option]) {
              this.options[option][attr] = opt[option][attr];
            }
          } catch (ex) {
          }
        } else
          this.options[option] = opt[option];
      }
    }
    // Set up event handlers as appropriate
    this.eventShow   = EvtHandler.make_listener (this, this.show);
    this.eventToggle = EvtHandler.make_listener (this, this.toggle);
    this.eventFocus  = EvtHandler.make_listener (this, this.show_focus);
    this.eventClick  = EvtHandler.make_listener (this, this.show_click);
    this.eventHide   = EvtHandler.make_listener (this, this.hide);
    this.eventTrack  = EvtHandler.make_listener (this, this.track);
    this.eventClose  = EvtHandler.make_listener (this, this.hide_now);
    this.eventKey    = EvtHandler.make_listener (this, this.key_handler);

    this.close_button       = null;
    this.close_button_width = 0;
    if (this.options.close_button) {
      this.makeCloseButton ();
      if (this.close_button) {
        // Only a click on the close button will close the tip.
        this.options.deactivate = this.options.deactivate & ~Tooltip.CLICK_TIP;
        // And escape is always active if we have a close button
        this.options.deactivate = this.options.deactivate | Tooltip.ESCAPE;
        // Don't track, you'd have troubles ever getting to the close button.
        if (this.options.mode == Tooltip.TRACK) this.options.mode = Tooltip.MOUSE;
        this.has_links = true;
      }
    }
    if (this.options.activate == Tooltip.NONE) {
      this.options.activate = 0;
    } else {
      if ((this.options.activate & Tooltip.ALL_ACTIVATIONS) == 0) {
        if (on_element.nodeName.toLowerCase () == 'a')
          this.options.activate = Tooltip.CLICK;
        else
          this.options.activate = Tooltip.HOVER;
      }
    }
    if ((this.options.deactivate & Tooltip.ALL_DEACTIVATIONS) == 0 && !this.close_button)
      this.options.deactivate = Tooltip.LEAVE | Tooltip.CLICK_ELEM | Tooltip.ESCAPE;
    document.body.appendChild (this.popup);
    if (this.content) this.apply_styles (this.content, css); // After adding it to the document
    // Clickable links?
    if (this.content && this.options.mode == Tooltip.TRACK) {
      this.setHasLinks ();
      if (this.has_links) {
        // If you track a tooltip with links, you'll never be able to click the links
        this.options.mode = Tooltip.MOUSE;
      }
    }
    // No further option checks. If nonsense is passed, you'll get nonsense or an exception.
    this.popup.style.zIndex = "" + this.options.z_index;
    this.target             = on_element;
    this.open_timeout_id    = null;
    this.hide_timeout_id    = null;
    this.size               = {width : 0, height : 0};
    this.setupEvents (EvtHandler.attach, 0);
  },
  
  apply_styles : function (node, css)
  {
    if (css) {
      for (var styledef in css) node.style[styledef] = css[styledef];
    }
    if (this.close_button) node.style.opacity = "1.0"; // Bug workaround.
    // FF doesn't handle the close button at all if it is (partially) transparent...
    if (node.style.display == 'none') node.style.display = "";
  },

  setHasLinks : function ()
  {
    if (this.close_button) { this.has_links = true; return; }
    var lks = this.content.getElementsByTagName ('a');
    this.has_links = false;
    for (var i=0; i < lks.length; i++) {
      var href = lks[i].getAttribute ('href');
      if (href && href.length > 0) { this.has_links = true; return; }
    }
    // Check for form elements
    function check_for (within, names)
    {
      if (names) {
        for (var i=0; i < names.length; i++) {
          var elems = within.getElementsByTagName (names[i]);
          if (elems && elems.length > 0) return true;
        }
      }
      return false;
    }
    this.has_links = check_for (this.content, ['form', 'textarea', 'input', 'button', 'select']);
  },

  setupEvents : function (op, state)
  {
    if (state < 0 || state == 0 && this.event_state < state) {
      if (this.options.activate & Tooltip.HOVER)
        op (this.target, 'mouseover', this.eventShow);
      if (this.options.activate & Tooltip.FOCUS)
        op (this.target, 'focus', this.eventFocus);
      if (   (this.options.activate & Tooltip.CLICK)
          && (this.options.deactivate & Tooltip.CLICK_ELEM)) {
        op (this.target, 'click', this.eventToggle);
      } else {
        if (this.options.activate & Tooltip.CLICK)
          op (this.target, 'click', this.eventClick);
        if (this.options.deactivate & Tooltip.CLICK_ELEM)
          op (this.target, 'click', this.eventClose);
      }
      this.event_state = state;
    }
    if (state < 0 || state == 1 && this.event_state < state) {
      if (this.options.deactivate & Tooltip.MOUSE_LEAVE) {
        op (this.target, 'mouseout', this.eventHide);
        op (this.popup, 'mouseout', this.eventHide);
        if (this.options.target) op (this.options.target, 'mouseout', this.eventHide);
      }
      if (this.options.deactivate & Tooltip.LOSE_FOCUS)
        op (this.target, 'blur', this.eventHide);
      if (   (this.options.deactivate & Tooltip.CLICK_TIP)
          && (this.options.mode != Tooltip.TRACK))
        op (this.popup, 'click', this.eventClose);        
      
      // Some more event handling
      if (this.hide_delay > 0) {
        if (!(this.options.activate & Tooltip.HOVER))
          op (this.popup, 'mouseover', this.eventShow);
        op (this.popup, 'mousemove', this.eventShow);
      }
      this.event_state = state;
    }
    if (state < 0 && this.tracks)
      op (this.target, 'mousemove', this.eventTrack);
  },
  
  remove: function ()
  {
    this.hide_now ();
    this.setupEvents (EvtHandler.remove, -1);
    this.tip_creator = null;
    document.body.removeElement (this.popup);
  },
  
  show : function (evt)
  {
    this.show_tip (evt, true);
  },
  
  show_focus : function (evt) // Show on focus
  {
    this.show_tip (evt, false);
  },
  
  show_click : function (evt)
  {
    this.show_tip (evt, false);
    if (this.target.nodeName.toLowerCase () == 'a') return EvtHandler.killEvt (evt); else return false;
  },
  
  toggle : function (evt)
  {
    if (this.popup.style.display != 'none' && this.popup.style.display != null) {
      this.hide_now (evt);
    } else {
      this.show_tip (evt, true);
    }
    if (this.target.nodeName.toLowerCase () == 'a') return EvtHandler.killEvt (evt); else return false;
  },

  show_tip : function (evt, is_mouse_evt)
  {
    if (this.hide_timeout_id != null) window.clearTimeout (this.hide_timeout_id);
    this.hide_timeout_id = null;
    if (this.popup.style.display != 'none' && this.popup.style.display != null) return;
    if (this.tip_creator) {
      // Dynamically created tooltip.
      try {
        this.content = this.tip_creator (evt);
      } catch (ex) {
        // Oops! Indicate that something went wrong!
        var error_msg = document.createElement ('div');
        error_msg.appendChild (
          document.createElement ('b').appendChild (
            document.createTextNode ('Exception: ' + ex.name)));
        error_msg.appendChild(document.createElement ('br'));
        error_msg.appendChild (document.createTextNode (ex.message));
        if (typeof (ex.fileName) != 'undefined' &&
            typeof (ex.lineNumber) != 'undefined') {
          error_msg.appendChild(document.createElement ('br'));
          error_msg.appendChild (document.createTextNode ('File ' + ex.fileName));
          error_msg.appendChild(document.createElement ('br'));
          error_msg.appendChild (document.createTextNode ('Line ' + ex.lineNumber));
        }
        this.content = error_msg;
      }
      // Our wrapper has at most two children: the close button, and the content. Don't remove
      // the close button, if any.
      if (this.popup.firstChild.lastChild && this.popup.firstChild.lastChild != this.close_button)
        this.popup.firstChild.removeChild (this.popup.firstChild.lastChild);
      this.popup.firstChild.appendChild (this.content);
      this.apply_styles (this.content, this.css);
      if (this.options.mode == Tooltip.TRACK) this.setHasLinks ();
    }
    // Position it now. It must be positioned before the timeout below!
    this.position_tip (evt, is_mouse_evt);
    if (Tooltips.debug) {
      alert ('Position: x = ' + this.popup.style.left + ' y = ' + this.popup.style.top);
    }
    this.setupEvents (EvtHandler.attach, 1);
    if (this.options.mode == Tooltip.TRACK) {
      if (this.has_links) {
        if (this.tracks) EvtHandler.remove (this.target, 'mousemove', this.eventTrack);
        this.tracks = false;
      } else {
        if (!this.tracks) EvtHandler.attach (this.target, 'mousemove', this.eventTrack);
        this.tracks = true;
      }
    }
    if (this.options.open_delay > 0) {
      var obj = this;
      this.open_timout_id = 
        window.setTimeout (function () {obj.show_now (obj);}, this.options.open_delay);
    } else
      this.show_now (this);
  },
  
  show_now : function (elem)
  {
    if (elem.popup.style.display != 'none' && elem.popup.style.display != null) return;
    Tooltips.register (elem);
    elem.popup.style.display = ""; // Finally show it
    if (   (elem.options.deactivate & Tooltip.ESCAPE)
        && typeof (elem.popup.focus) == 'function') {
      // We need to attach this event globally.
      EvtHandler.attach (document, 'keydown', elem.eventKey);
    }
    elem.open_timeout_id = null;
    // Callback
    if (typeof (elem.options.onopen) == 'function') elem.options.onopen (elem);
  },
  
  track : function (evt)
  {
    this.position_tip (evt, true);
  },
  
  size_change : function ()
  {
    // If your content is such that it changes, make sure this is called after each size change.
    // Unfortunately, I have found no way of monitoring size changes of this.popup and then doing
    // this automatically. See for instance the "toggle" example (the 12th) on the example page at
    // http://commons.wikimedia.org/wiki/MediaWiki:Tooltips.js/Documentation/Examples
    if (this.popup.style.display != 'none' && this.popup.style.display != null) {
      // We're visible. Make sure the shim gets resized, too!
      this.size = {width : this.popup.offsetWidth, height: this.popup.offsetHeight};
    }
  },
    
  position_tip : function (evt, is_mouse_evt)
  {
    var view = {width  : this.viewport ('Width'),
                height : this.viewport ('Height')};
    var off  = {left   : this.scroll_offset ('Left'),
                top    : this.scroll_offset ('Top')};
    var x = 0, y = 0;
    var offset = null;
    // Calculate the position
    if (is_mouse_evt && this.options.mode != Tooltip.FIXED) {
      x = (evt.pageX || (evt.clientX + off.left));
      y = (evt.pageY || (evt.clientY + off.top));
      offset = 'mouse_offset';
    } else {
      var tgt = this.options.target || this.target;
      var pos = this.position (tgt);
      switch (this.options.anchor) {
        default:
        case Tooltip.BOTTOM_LEFT:
          x = pos.x; y = pos.y + tgt.offsetHeight;
          break;
        case Tooltip.BOTTOM_RIGHT:
          x = pos.x + tgt.offsetWidth; y = pos.y + tgt.offsetHeight;
          break;
        case Tooltip.TOP_LEFT:
          x = pos.x; y = pos.y;
          break;
        case Tooltip.TOP_RIGHT:
          x = pos.x + tgt.offsetWidth; y = pos.y;
          break;
      }
      offset = 'fixed_offset';
    }
    
    x = x + this.options[offset].x * this.options[offset].dx;
    y = y + this.options[offset].y * this.options[offset].dy;

    this.size = this.calculate_dimension ();
    if (this.options[offset].dx < 0) x = x - this.size.width;
    if (this.options[offset].dy < 0) y = y - this.size.height;
    
    // Now make sure we're within the view.
    if (x + this.size.width > off.left + view.width) x = off.left + view.width - this.size.width;
    if (x < off.left) x = off.left;
    if (y + this.size.height > off.top + view.height) y = off.top + view.height - this.size.height;
    if (y < off.top) y = off.top;
    
    this.popup.style.top  = y + "px";
    this.popup.style.left = x + "px";
  },
  
  hide : function (evt)
  {
    if (this.popup.style.display == 'none') return;
    // Get mouse position
    var x = evt.pageX
         || (evt.clientX + this.scroll_offset ('Left'));
    var y = evt.pageY
         || (evt.clientY + this.scroll_offset ('Top'));
    // We hide it if we're neither within this.target nor within this.content nor within the
    // alternate target, if one was given.
    if (Tooltips.debug) {
      var tp = this.position (this.target);
      var pp = this.position (this.popup);
      alert ("x = " + x + " y = " + y + '\n' +
             "t: " + tp.x + "/" + tp.y + "/" +
               this.target.offsetWidth + "/" + this.target.offsetHeight + '\n' +
             (tp.n ? "t.m = " + tp.n.nodeName + "/" + tp.n.getAttribute ('margin') + "/"
                     + tp.n.getAttribute ('marginTop')
                     + "/" + tp.n.getAttribute ('border') + '\n'
                   : "") +
             "p: " + pp.x + "/" + pp.y + "/" +
               this.popup.offsetWidth + "/" + this.popup.offsetHeight + '\n' +
             (pp.n ? "p.m = " + pp.n.nodeName + "/" + pp.n.getAttribute ('margin') + "/"
                     + pp.n.getAttribute ('marginTop')
                     + "/" + pp.n.getAttribute ('border') + '\n'
                   : "") +
             "e: " + evt.pageX + "/" + evt.pageY + " "
               + evt.clientX + "/" + this.scroll_offset ('Left') + " "
               + evt.clientY + "/" + this.scroll_offset ('Top') + '\n'
             );
    }
    if (   !this.within (this.target, x, y)
        && !this.within (this.popup, x, y)
        && (!this.options.target || !this.within (this.options.target, x, y))) {
      if (this.open_timeout_id != null) window.clearTimeout (this.open_timeout_id);
      this.open_timeout_id = null;
      var event_copy = evt;
      if (this.options.hide_delay > 0) {
        var obj = this;
        this.hide_timeout_id =
          window.setTimeout (
              function () {obj.hide_popup (obj, event_copy);}
            , this.options.hide_delay
          );
      } else
        this.hide_popup (this, event_copy);
    }
  },
  
  hide_popup : function (elem, event)
  {
    if (elem.popup.style.display == 'none') return; // Already hidden, recursion from onclose?
    elem.popup.style.display = 'none';
    elem.hide_timeout_id = null;
    Tooltips.deregister (elem);
    if (elem.options.deactivate & Tooltip.ESCAPE)
      EvtHandler.remove (document, 'keydown', elem.eventKey);
    // Callback
    if (typeof (elem.options.onclose) == 'function') elem.options.onclose (elem, event);
  },
  
  hide_now : function (evt)
  {
    if (this.open_timeout_id != null) window.clearTimeout (this.open_timeout_id);
    this.open_timeout_id = null;
    var event_copy = evt || null;
    this.hide_popup (this, event_copy);
    if (evt && this.target.nodeName.toLowerCase == 'a') return EvtHandler.killEvt (evt); else return false;
  },
  
  key_handler : function (evt)
  {
    if (Tooltips.debug) alert ('key evt ' + evt.keyCode);
    if (evt.DOM_VK_ESCAPE && evt.keyCode == evt.DOM_VK_ESCAPE || evt.keyCode == 27)
      this.hide_now (evt);
    return true;
  },

  setZIndex : function (z_index)
  {
    if (z_index === null || isNaN (z_index) || z_index < 2) return;
    z_index = Math.floor (z_index);
    if (z_index == this.options.z_index) return; // No change
    this.popup.style.zIndex = z_index;
    this.options.z_index = z_index;
  },

  makeCloseButton : function ()
  {
    this.close_button = null;
    if (!this.options.close_button) return;
    var imgs = null;
    if (typeof (this.options.close_button.length) != 'undefined')
      imgs = this.options.close_button; // Also if it's a string (name of previously created class)
    else
      imgs = [this.options.close_button];
    if (!imgs || imgs.length == 0) return; // Paranoia
    var lk = Buttons.makeButton (imgs, this.tip_id + '_button', this.eventClose); 

    if (lk) {
      var width = lk.firstChild.getAttribute ('width');
      lk.style.cssFloat = 'right';
      lk.style.paddingTop   = '2px';
      lk.style.paddingRight = '2px';
      this.popup.firstChild.insertBefore (lk, this.popup.firstChild.firstChild);
      this.close_button = lk;
      this.close_button_width = parseInt ("" + width, 10);
    }
  },

  within : function (node, x, y)
  {
    if (!node) return false;
    var pos = this.position (node);
    return    (x == null || x > pos.x && x < pos.x + node.offsetWidth)
           && (y == null || y > pos.y && y < pos.y + node.offsetHeight);
  },
  
  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).
    //   Note: I have virtually the same code also in LAPI.js, but I cannot import that here
    // because I know that at least one gadget at the French Wikipedia includes this script here
    // directly from here. I'd have to use importScriptURI instead of importScript to keep that
    // working, but I'd run the risk that including LAPI at the French Wikipedia might break
    // something there. I *hate* it when people hotlink scripts across projects!

    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 = {x : this.scroll_offset ('Left'), y: this.scroll_offset ('Top')};
        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) {
        var styles;
        if (   body.ownerDocument.defaultView
            && body.ownerDocument.defaultView.getComputedStyle)
        { // Gecko etc.
          styles = body.ownerDocument.defaultView.getComputedStyle (body, null);
          top  += parseInt (style.getPropertyValue ('margin-top' ), 10) || 0;
          left += parseInt (style.getPropertyValue ('margin-left'), 10) || 0;
        } else {
          function to_px (element, val) {
            // 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 (val) && /^\d/.test (val) && body.runtimeStyle) {
              var style                 = element.style.left;
              var runtimeStyle          = element.runtimeStyle.left;
              element.runtimeStyle.left = element.currentStyle.left;
              element.style.left        = result || 0;
              val = elem.style.pixelLeft + "px";
              element.style.left        = style;
              element.runtimeStyle.left = runtimeStyle;
            }
            return val;
          }
          style = body.currentStyle || body.style;
          top  += parseInt (to_px (body, style.marginTop ), 10) || 0;
          left += parseInt (to_px (body, style.marginleft), 10) || 0;
        }
      }
      return {x: left, y: top};
    }

    return jQuery_offset;
  })(),

  scroll_offset : function (what)
  {
    var s = 'scroll' + what;
    return (document.documentElement ? document.documentElement[s] : 0)
           || document.body[s] || 0;
  },

  viewport : function (what)
  {
    var s = 'client' + what;
    return (document.documentElement ? document.documentElement[s] : 0) || document.body[s] || 0;
  },


  calculate_dimension : function ()
  {
    if (this.popup.style.display != 'none' && this.popup.style.display != null) {
      return {width : this.popup.offsetWidth, height : this.popup.offsetHeight};
    }
    // Must display it... but position = 'absolute' and visibility = 'hidden' means
    // the user won't notice it.
    var view_width = this.viewport ('Width');
    this.popup.style.top        = "0px";
    this.popup.style.left       = "0px";
    // Remove previous width as it may change with dynamic tooltips
    this.popup.style.width      = "";
    this.popup.style.maxWidth   = "";
    this.popup.style.overflow   = 'hidden';
    this.popup.style.visibility = 'hidden';
    // Remove the close button, otherwise the float will always extend the box to
    // the right edge.
    if (this.close_button)
      this.popup.firstChild.removeChild (this.close_button);
    this.popup.style.display = "";   // Display it. Now we should have a width
    var w = this.popup.offsetWidth;
    var h = this.popup.offsetHeight;
    var limit = Math.round (view_width * this.options.max_width);
    if (this.options.max_pixels > 0 && this.options.max_pixels < limit)
      limit = this.options.max_pixels;
    if (w > limit) {
      w = limit;
      this.popup.style.width    = "" + w + "px";
      this.popup.style.maxWidth = this.popup.style.width;
      if (this.close_button) {
        this.popup.firstChild.insertBefore
          (this.close_button, this.popup.firstChild.firstChild);
      }
    } else {
      this.popup.style.width    = "" + w + "px";
      this.popup.style.maxWidth = this.popup.style.width;
      if (this.close_button) {
        this.popup.firstChild.insertBefore
          (this.close_button, this.popup.firstChild.firstChild);
      }
      if (h != this.popup.offsetHeight) {
        w =  w + this.close_button_width;    
        this.popup.style.width    = "" + w + "px";
        this.popup.style.maxWidth = this.popup.style.width;
      }
    }
    var size = {width : this.popup.offsetWidth, height : this.popup.offsetHeight};
    this.popup.style.display = 'none';       // Hide it again
    this.popup.style.visibility = "";
    return size;
  }
    
} // end Tooltip

//

//

/*
	Read UI elements from the DOM. Used by the upload form rewrite.

	Author: [[User:Lupo]], March 2008
	License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)

	Choose whichever license of these you like best :-)
*/

var UIElements = {
	defaultLanguage: 'en',

	getElementsByClassName: function ( elem, tag, classes ) {
		// getElementsByClassName in wikibits.js has been changed in a contract-breaking way and
		// newly won't work anymore with regexp strings or arrays of strings passed as classes.
		// We need this functionality here, so we have our own copy of this function.
		var arrElements = ( tag == '*' && elem.all ) ? elem.all : elem.getElementsByTagName( tag );
		var arrReturnElements = new Array();
		var arrRegExpClassNames = new Array();

		if ( typeof classes == 'object' ) {
			for ( var i = 0; i < classes.length; i++ ) {
				arrRegExpClassNames[arrRegExpClassNames.length] =
					new RegExp( "(^|\\s)" + classes[i].replace( /\-/g, "\\-" ) + "(\\s|$)" );
			}
		} else {
			arrRegExpClassNames[arrRegExpClassNames.length] =
				new RegExp( "(^|\\s)" + classes.replace( /\-/g, "\\-" ) + "(\\s|$)" );
		}

		var this_elem;
		var matches_all;

		for ( var j = 0; j < arrElements.length; j++ ) {
			this_elem = arrElements[j];
			matches_all = true;

			for ( var k = 0; k < arrRegExpClassNames.length; k++ ) {
				if ( !arrRegExpClassNames[k].test( this_elem.className ) ) {
					matches_all = false;
					break;
				}
			}

			if ( matches_all ) {
				arrReturnElements[arrReturnElements.length] = this_elem;
			}
		}

		return arrReturnElements;
	},

	load: function ( container_name, items, tag, repository, default_language, doc ) {
		doc = doc || document;

		function add_item ( item, lang ) {
			var classes = item.className.split( ' ' );
			var node = null;

			if ( item.ownerDocument != document && document.importNode ) {
				node = document.importNode( item, true );
			} else {
				node = item.cloneNode( true );
			}

			UIElements.setEntry( classes[0], repository, node, lang, classes.slice( 1 ) );
		}

		if ( !container_name || container_name.length == 0 || container_name == '*' ) {
			return repository;
		}

		var base = UIElements.getElementsByClassName( doc, 'div', container_name );
		if ( !base || base.length == 0 ) {
			return repository;
		}

		if ( !items || items.length == 0 ) {
			items = [ 'wp\\S*' ];
		} else if ( items && typeof items == 'string' ) {
			items = [ items ];
		}

		if ( !repository ) {
			repository = UIElements.emptyRepository( default_language );
		}

		for ( var i = 0; i < base.length; i++ ) {
			var b = base[i];
			var lang = ( b.getAttribute( 'lang' ) || repository.defaultLanguage ).replace( /-/g, '_' );

			for ( var j = 0; j < items.length; j++ ) {
				var nodes = UIElements.getElementsByClassName( b, tag || '*', items[j] );
				if ( nodes ) {
					for ( var k = 0; k < nodes.length; k++ ) {
						add_item( nodes[k], lang );
					}
				}
			}
		}

		return repository;
	},

	getEntry: function ( id, repository, lang, selector ) {
		if ( repository && repository[id] ) {
			if ( !lang || lang.length == 0 ) {
				lang = repository.defaultLanguage || UIElements.defaultLanguage;
			}

			lang = lang.replace( /-/g, '_' );

			if ( repository[id][lang] ) {
				if ( selector == '*' ) {
					return repository[id][lang];
				} else if ( !selector || selector.length == 0 ) {
					return repository[id][lang]._default;
				} else {
					return repository[id][lang][selector.replace( /-/g, '_' )];
				}
			}
		}

		return null;
	},

	setEntry: function ( id, repository, value, lang, selectors ) {
		if ( !repository ) {
			return null;
		}

		if ( !lang ) {
			lang = repository.defaultLanguage;
		}

		lang = lang.replace( /-/g, '_' );
		id = id.replace( /-/g, '_' );

		if ( !repository[id] ) {
			repository[id] = new Object();
		}

		if ( !repository[id][lang] ) {
			repository[id][lang] = new Object();
		}

		if ( !selectors || selectors.length == 0 ) {
			repository[id][lang]._default = value;
		} else {
			for ( var k = 0; k < selectors.length; k++ ) {
				if ( !selectors[k] || selectors[k].length == 0 ) {
					repository[id][lang]._default = value;
				} else {
					repository[id][lang][selectors[k].replace( /-/g, '_' )] = value;
				}
			}
		}
	},

	emptyRepository: function ( default_language ) {
		var repository = new Object();
		repository.defaultLanguage = default_language || UIElements.defaultLanguage;
		return repository;
	}

} // end UIElements
//

// ajaxSubmit // Submit a form through Ajax. Doesn't handle file uploads yet. // // Parameters: // form DOM element The form to submit // button optional DOM element If set and a submit button of 'form', is added to the // form arguments sent // func optional Function Function to call once the call has been made or the // result has arrived, if want_result === true // want_result optional Boolean If true, call func with the result of the submit once // it has arrived. Otherwise, call func as soon as the // submit request has been received by the server, and // ignore any result of the submit. // // Notes: // Func should be a function (request). If func is not defined, // ajaxSubmit just submits the form and ignores any result. /*global mw*/ function ajaxSubmit(form, button, func, want_result) { "use strict"; if (want_result && (!func || typeof(func) != 'function' || func.length < 1)) { /**** TODO: improve error handling: should throw an exception! */ alert('Logic error in ajaxSubmit: func must be function (request).'); return; } if (func && typeof(func) != 'function') { /**** TODO: improve error handling: should throw an exception! */ alert('Error in ajaxSubmit: func must be a function, found a ' + typeof(func) + '.'); return; }

var is_simple = false; // True if it's a GET request, or if the form is 'application/x-www-form-urlencoded' var boundary = null; // Otherwise, it's 'multipart/form-data', and the multipart delimiter is 'boundary'

function encode_entry(name, value) { if (!name || !name.length || !value || !value.length) return null; if (!boundary) return name + '=' + encodeURIComponent(value); else return boundary + '\r\n' + 'Content-Disposition: form-data; name="' + name + '"\r\n' + '\r\n' + value.replace(/\r?\n/g, '\r\n') + '\r\n'; // RFC 2046: newlines always must be represented as CR-LF }

function encode_field(element) { var name = element.name; if (!name || !name.length) name = element.id; return encode_entry(name, element.value); }

function form_add_argument(args, field) { if (!field || !field.length) return args; if (!args || !args.length) return field; if (is_simple) return args + '&' + field; else return args + field; }

var request; if (window.LAPI && window.LAPI.Ajax && window.LAPI.Ajax.getRequest) { request = window.LAPI.Ajax.getRequest(); } else { try { request = new window.XMLHttpRequest(); } catch (anything) { if (window.ActiveXObject) request = new window.ActiveXObject('Microsoft.XMLHTTP'); } } var method = form.getAttribute('method').toUpperCase(); var uri = form.getAttribute('action'); if (uri.length >= 2 && uri.substring(0, 2) === '//') { // Protocol-relative URI; can cause trouble on IE7 uri = document.location.protocol + uri; } else if (uri[0] === '/') { // Some browsers already expand the action URI (e.g. Opera 9.26) uri = mw.config.get('wgServer') + uri; if (uri.length >= 2 && uri.substring(0, 2) === '//') uri = document.location.protocol + uri; } // Encode the field values

var is_get = method === 'GET'; var encoding = form.getAttribute('enctype'); if (encoding) { encoding = encoding.toLowerCase(); if (!encoding.length) encoding = null; } is_simple = is_get || !encoding || encoding === 'application/x-www-form-urlencoded';

var args = ; var boundary_string = '----' + mw.config.get('wgArticleId') + mw.config.get('wgCurRevisionId') + 'auto_submit_by_lupo';

boundary = null;

if (!is_simple) boundary = '--' + boundary_string;

for (var i = 0; i < form.elements.length; i++) { var element = form.elements[i]; var single_select = false; switch (element.type) { case 'checkbox': case 'radio': if (!element.checked) break; else if (element.id === 'wpWatchthis' && document.getElementById('ca-unwatch')) { args = form_add_argument(args, encode_entry('wpWatchthis', '1')); break; } /* falls through */ case 'hidden': case 'text': case 'password': case 'textarea': args = form_add_argument(args, encode_field(element)); break; case 'select-one': single_select = true; /* falls through */ case 'select-multiple': var name = element.name || element.id || ; if (!name.length) break; for (var j = 0; j < element.length; j++) { if (element[j].selected) { var value = element[j].value || element[j].text; args = form_add_argument(args, encode_entry(name, value)); if (single_select) break; // No need to scan the rest } } break; case 'file': break; } } if (button && button.form === form && button.type === 'submit') args = form_add_argument(args, encode_field(button));

// Close the multipart request if (!is_simple && args.length > 0) args += boundary;

if (method === 'GET') { uri += (uri.indexOf('?') < 0 ? '?' : '&') + args; args = null; } // Make the request request.open(method, uri, true); if (want_result && request.overrideMimeType) request.overrideMimeType('application/xml'); request.setRequestHeader('Pragma', 'cache=no'); request.setRequestHeader('Cache-Control', 'no-transform'); if (method === 'POST') { if (!encoding) encoding = 'application/x-www-form-urlencoded'; if (!is_simple) { request.setRequestHeader( 'Content-type', encoding + '; charset=UTF-8; boundary="' + boundary_string + '"'); } else { request.setRequestHeader('Content-type', encoding); } } request.onreadystatechange = function () { if (want_result) { if (request.readyState < 4) return; func(request); } else { // Call func as soon as the request has been sent and we start getting the result. if (request.readyState === 3 && func) func(request); } }; request.send(args); }

// submitAndClose // Submit a form and close the window containing it as soon as the request has been // received by the server // // Parameters: // form DOM element The form to submit. function submitAndClose(form) { ajaxSubmit(form, null, function () { window.close(); }); } // <source lang="javascript">

/*

 Small gadget to disable MediaWiki:Gadget-ImageAnnotator.js, which is enabled by default for
 everyone here at the Commons.
  • /

window.ImageAnnotator_disable = true;


{{{2}}}
{{{3}}}
{{{4}}}
{{{5}}}
{{{6}}}
{{{7}}}