MediaWiki:Gadget-ImageAnnotator.js: Difference between revisions

Content deleted Content added
Remove cache busting. MediaWiki handles this natively now as long as the url follows the exact format title/action/ctype in that order. It's cached properly and purged when the page is edited.
eslint-config-wikimedia autofix
Line 2:
 
/*
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:global importScript, importScriptURI (wiki.js),
LAPI, Tooltip, Tooltips, TextCleaner, UIElements, Buttons,
/*jshint eqnull:true, laxbreak:true, laxcomma:true */
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 (
if (x < 0 || x >= viewer.full_img.width || y < 0 || y >= viewer.full_img.height)
x + w > viewer.full_img.width + 10 ||
throw new Error('Invalid note: origin invalid on note ' + id);
if ( x y + wh > viewer.full_img.widthheight + 10 ) {
throw new Error( 'Invalid note: size extends beyond image on note ' + id );
|| y + h > viewer.full_img.height + 10)
}
{
// Notes written by early versions may be slightly too large, whence the + 10 above. Fix this.
throw new Error('Invalid note: size extends beyond image on note ' + id);
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; }
// Notes written by early versions may be slightly too large, whence the + 10 above. Fix this.
view_w = Math.floor( w / viewer.factors.dx );
if (x + w > viewer.full_img.width) w = viewer.full_img.width - x;
view_h = Math.floor( h / viewer.factors.dy );
if (y + h > viewer.full_img.height) h = viewer.full_img.height - y;
view_w view_x = Math.floor(w x / viewer.factors.dx );
view_h view_y = Math.floor(h y / viewer.factors.dy );
this.view = LAPI.make(
view_x = Math.floor(x / viewer.factors.dx);
'div', null, {
view_y = Math.floor(y / viewer.factors.dy);
position: 'absolute',
this.view =
display: 'none',
LAPI.make(
lineHeight: '0px', // IE
'div', null
fontSize: '0px', // IE
, { position: 'absolute'
top: String( view_y ) + 'px',
,display: 'none'
left: String( view_x ) + 'px',
,lineHeight: '0px' // IE
width: String( view_w ) + 'px',
,fontSize: '0px' // IE
,top height: ''String( +view_h view_y) + 'px'
}
,left: '' + view_x + 'px'
);
,width: '' + view_w + 'px'
// We'll add the view to the DOM once we've loaded all notes
,height: '' + view_h + 'px'
this.model = {
}
id: id,
);
dimension: { x: x, y: y, w: w, h: h },
// We'll add the view to the DOM once we've loaded all notes
wiki: '',
this.model =
html: html.cloneNode( true )
{ id: id
};
,dimension: {x: x, y: y, w: w, h: h}
} else {
,wiki: ''
is_new = true;
,html: html.cloneNode(true)
this.view = node;
};
this.model = {
} else {
id: -1,
is_new = true;
dimension: null,
this.view = node;
wiki: '',
this.model =
html: null
{ id: -1
};
,dimension: null
view_w = this.view.offsetWidth - 2; // Subtract cumulated border widths
,wiki: ''
view_h = this.view.offsetHeight - 2;
,html: null
view_x = this.view.offsetLeft;
};
view_y = this.view.offsetTop;
view_w = this.view.offsetWidth - 2; // Subtract cumulated border widths
}
view_h = this.view.offsetHeight - 2;
// Enforce a minimum size of the view. Center the 6x6px square over the center of the old view.
view_x = this.view.offsetLeft;
// If we overlap the image boundary, adjustRectangleSize will take care of it later.
view_y = this.view.offsetTop;
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; }
// Enforce a minimum size of the view. Center the 6x6px square over the center of the old view.
Object.merge(
// 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; }
left: String( view_x ) + 'px',
if (view_h < 6) {view_y = Math.floor(view_y + view_h / 2 - 3); view_h = 6; }
top: String( view_y ) + 'px',
Object.merge(
width: String( view_w ) + 'px',
{
left height: ''String( +view_h view_x) + 'px',
},
top: '' + view_y + 'px',
this.view.style
width: '' + view_w + 'px',
);
height: '' + view_h + 'px'
this.view.style.zIndex = 500; // Below tooltips
},
try {
this.view.style
this.view.style.border = '1px solid ' + this.viewer.outer_border;
);
} catch ( ex ) {
this.view.style.zIndex = 500; // Below tooltips
this.view.style.border = '1px solid ' + IA.outer_border;
try {
}
this.view.style.border = '1px solid ' + this.viewer.outer_border;
this.view.appendChild(
} catch (ex) {
LAPI.make(
this.view.style.border = '1px solid ' + IA.outer_border;
'div', null, {
}
lineHeight: '0px', // IE
this.view.appendChild(
fontSize: '0px', // IE
LAPI.make(
width: String( Math.max( view_w - 2, 0 ) ) + 'px', // -2 to leave space for the border
'div', null
height: String( Math.max( view_h - 2, 0 ) ) + 'px'
, { lineHeight: '0px' // IE
}
,fontSize: '0px' // IE
)
,width: '' + Math.max(view_w - 2, 0) + 'px' // -2 to leave space for the border
// width=100% doesn't work right: inner div's border appears outside on right and bottom on FF.
,height: '' + Math.max(view_h - 2, 0) + 'px'
);
}
try {
)
this.view.firstChild.style.border = '1px solid ' + this.viewer.inner_border;
// width=100% doesn't work right: inner div's border appears outside on right and bottom on FF.
} catch ( ex ); {
this.view.firstChild.style.border = '1px solid ' + IA.inner_border;
try {
}
this.view.firstChild.style.border = '1px solid ' + this.viewer.inner_border;
if ( is_new ) { viewer.adjustRectangleSize( this.view ); }
} catch (ex) {
// IE somehow passes through event to the view even if covered by our cover, displaying the tooltips
this.view.firstChild.style.border = '1px solid ' + IA.inner_border;
// 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.
if (is_new) viewer.adjustRectangleSize(this.view);
this.dummy = this.view.cloneNode( true );
// IE somehow passes through event to the view even if covered by our cover, displaying the tooltips
viewer.img_div.appendChild( this.dummy );
// when drawing a new rectangle, which is confusing and produces a selection nightmare. Hence we just
if ( !is_new ) {
// display raw rectangles without any tooltips attached while drawing. Yuck.
// New notes get their tooltip only once the editor has saved, otherwise IE may try to
this.dummy = this.view.cloneNode(true);
// open them if the mouse moves onto the view even though there is the cover above them!
viewer.img_div.appendChild(this.dummy);
this.setTooltip();
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 ) { return; } // Already set, or corrupt
// 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 activate: ( LAPI.DOM.is_ie ? Tooltip.ESCAPENONE : Tooltip.LEAVEHOVER ),
deactivate: ( LAPI.DOM.is_ie ? Tooltip.ESCAPE : Tooltip.LEAVE ),
,close_button: null
close_button: null,
,mode: Tooltip.MOUSE
mode: Tooltip.MOUSE,
,mouse_offset : {x: -5, y: -5, dx: (IA.is_rtl ? -1 : 1), dy: 1}
mouse_offset: { x: -5, y: -5, dx: ( IA.is_rtl ? -1 : 1 ), dy: 1 },
,open_delay: 0
open_delay: 0,
,hide_delay: 0
hide_delay: 0,
,onclose: (function (tooltip, evt) {
onclose: ( function ( tooltip, evt ) {
if (this.view) {
if ( this.view ) {
try {
try {
this.view.style.border = '1px solid ' + this.viewer.outer_border;
this.view.style.border = '1px solid ' + this.viewer.outer_border;
} catch (ex) {
} catch ( ex ) {
this.view.style.border = '1px solid ' + IA.outer_border;
this.view.style.border = '1px solid ' + IA.outer_border;
}
}
}
}
if (this.viewer.tip == tooltip) this.viewer.tip = null;
if ( this.viewer.tip == tooltip ) {
// Hide all boxes if we're outside the image. Relies on hide checking the
this.viewer.tip = null;
// coordinates! (Otherwise, we'd always hide...)
}
if (evt) this.viewer.hide(evt);
// Hide all boxes if we're outside the image. Relies on hide checking the
}).bind(this)
// coordinates! (Otherwise, we'd always hide...)
,onopen: (function (tooltip) {
if ( evt ) {
if (this.view) {
this.viewer.hide( evt );
try {
}
this.view.style.border = '1px solid ' + this.viewer.active_border;
} ).bind( this ),
} catch (ex) {
onopen: ( function ( tooltip ) {
this.view.style.border = '1px solid ' + IA.active_border;
if ( this.view ) {
}
try {
}
this.view.style.border = '1px solid ' + this.viewer.active_border;
this.viewer.tip = tooltip;
} catch ( ex ) {
}).bind(this)
this.view.style.border = '1px solid ' + IA.active_border;
}
}
, IA.tooltip_styles
}
);
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',
LAPI.make(
null,
'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,
, 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,
, 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 ) { return false; } // Cancelled
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;
var self = this;
if ( !textbox ) { throw new Error( '#Server replied with invalid edit page.' ); }
var spinnerId = 'image_annotation_delete_' + this.model.id;
var pagetext = textbox.value.replace( /\r\n/g, '\n' );
LAPI.Ajax.injectSpinner(this.content.button_section.lastChild, spinnerId);
// Normalize different end-of-line handling. Opera and IE may use \r\n, whereas other
if (this.tooltip) this.tooltip.size_change();
// browsers just use '\n'. Note that all browsers do the right thing if a '\n' is added.
LAPI.Ajax.editPage(
// We normally don't care, but here we need this to make sure we don't leave extra line
mw.config.get('wgPageName')
// breaks when we remove the note.
, 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.');
 
IA.setWikitext( pagetext );
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.
 
var span = IA.setWikitextfindNote( 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;
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();
}
);
 
destroy: function () {
return true;
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;
},
 
destroy area: function () {
if ( !this.view)model LAPI.DOM.removeNode(|| !this.viewmodel.dimension ) { return 0; }
if return ( this.dummy) LAPImodel.DOMdimension.removeNode(w * this.dummymodel.dimension.h );
},
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 cannotEdit: function () {
if (! this.modelcontent ||&& !this.modelcontent.dimensionbutton_section ) return 0;{
LAPI.DOM.removeNode( this.content.button_section );
return (this.model.dimension.w * this.model.dimension.h);
this.content.button_section = null;
},
if ( this.tooltip ) { this.tooltip.size_change(); }
}
}
 
}; // end ImageAnnotation
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();
}
}
 
var ImageAnnotationEditor = function () { this.initialize.apply( this, arguments ); };
}; // end ImageAnnotation
 
var ImageAnnotationEditor.prototype = function () {this.initialize.apply(this, arguments);};
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 () {
Array.forEach( IA.viewers, ( function ( viewer ) {
if ( viewer != this.viewer && viewer.visible ) { viewer.hide(); }
} ).bind( this ) );
}
);
},
 
get_editor: function () {
ImageAnnotationEditor.prototype =
return this.box;
{
},
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 editNote: function ( note ) {
var same_note = ( returnnote == this.boxnote );
this.note = note;
},
this.viewer = this.note.viewer;
 
var cover = IA.get_cover();
editNote: function (note) {
cover.style.cursor = 'auto';
var same_note = (note == this.note);
IA.show_cover();
this.note = note;
this.viewer = this.note.viewer;
 
if ( note.tooltip ) { note.tooltip.hide_now(); }
var cover = IA.get_cover();
cover.style.cursor = 'auto';
IA.show_cover();
 
IA.is_editing = true;
if (note.tooltip) note.tooltip.hide_now();
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 ) {
IA.is_editing = true;
this.editor.hidePreview();
if (note.content && !IA.wiki_read) {
if ( !same_note || this.editor.textarea.readOnly ) {
// Existing note, and we don't have the wikitext yet: go get it
// Different note, or save error last time
var self = this;
this.editor.setText( this.note.model.wiki );
LAPI.Ajax.apiGet(
}
'query'
this.editor.enable(
, { prop: 'revisions'
LAPI.Edit.SAVE + LAPI.Edit.PREVIEW + LAPI.Edit.REVERT + LAPI.Edit.CANCEL
,titles: mw.config.get('wgPageName')
);
,rvlimit: 1
this.editor.textarea.readOnly = false;
,rvstartid : mw.config.get('wgCurRevisionId')
this.editor.textarea.style.backgroundColor = 'white';
,rvprop: 'ids|content'
// Set the position relative to the note's view.
}
var view_pos = LAPI.Pos.position( this.note.view );
, function (request, json_result) {
var origin = LAPI.Pos.position( cover );
if (json_result && json_result.query && json_result.query.pages) {
this.tooltip.options.fixed_offset.x =
// Should have only one page here
view_pos.x - origin.x + this.tooltip.options.mouse_offset.x;
for (var page in json_result.query.pages) {
this.tooltip.options.fixed_offset.y =
var p = json_result.query.pages[page];
view_pos.y - origin.y + this.tooltip.options.mouse_offset.y;
if (p && p.revisions && p.revisions.length) {
this.tooltip.options.fixed_offset.dx = 1;
var rev = p.revisions[0];
this.tooltip.options.fixed_offset.dy = 1;
if (rev.revid == mw.config.get('wgCurRevisionId') && rev["*"] && rev["*"].length)
// Make sure mouse event listeners are removed, especially on IE.
IA.setWikitext(rev["*"]);
this.dim = { x: this.note.view.offsetLeft, y: this.note.view.offsetTop,
}
w: this.note.view.offsetWidth, h: this.note.view.offsetHeight };
break;
this.viewer.setShowHideEvents( false );
}
this.viewer.hide(); // Make sure notes are hidden
}
this.viewer.toggle( true ); // Show all note rectangles (but only the dummies)
// TODO: What upon a failure?
// Now show the editor
self.open_editor(same_note, cover);
this.tooltip.show_tip( null, false );
}
var tpos = LAPI.Pos.position( this.editor.textarea );
, function (request) {
var ppos = LAPI.Pos.position( this.tooltip.popup );
// TODO: What upon a failure?
tpos = tpos.x - ppos.x;
self.open_editor(same_note, cover);
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...
} else {
this.editor.textarea.style.width = this.editor.textarea.offsetWidth + 'px';
this.open_editor(same_note, cover);
}
}
this.visible = true;
},
},
 
open_editor hide_editor: function (same_note, coverevt ) {
if ( !this.visible ) { return; }
this.editor.hidePreview();
this.visible = false;
if (!same_note || this.editor.textarea.readOnly)
IA.is_editing = false;
// Different note, or save error last time
this.tooltip.hide_now( evt );
this.editor.setText(this.note.model.wiki);
if ( evt && evt.type == 'keydown' && !this.saving ) {
this.editor.enable
// ESC pressed on new note before a save attempt
(LAPI.Edit.SAVE + LAPI.Edit.PREVIEW + LAPI.Edit.REVERT + LAPI.Edit.CANCEL);
this.cancel();
this.editor.textarea.readOnly = false;
}
this.editor.textarea.style.backgroundColor = 'white';
IA.hide_cover();
// Set the position relative to the note's view.
this.viewer.setDefaultMsg();
var view_pos = LAPI.Pos.position(this.note.view);
this.viewer.setShowHideEvents( true );
var origin = LAPI.Pos.position(cover);
this.viewer.hide();
this.tooltip.options.fixed_offset.x =
this.viewer.show(); // Make sure we get the real views again.
view_pos.x - origin.x + this.tooltip.options.mouse_offset.x;
// FIXME in Version 2.1: Unfortunately, we don't have a mouse position here, so sometimes we
this.tooltip.options.fixed_offset.y =
// may show the note rectangles even though the mouse is now outside the image. (It was
view_pos.y - origin.y + this.tooltip.options.mouse_offset.y;
// somewhere inside the editor in most cases (if an editor button was clicked), but if ESC was
this.tooltip.options.fixed_offset.dx = 1;
// pressed, it may actually be anywhere.)
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 save: function (evt editor ) {
var data = editor.getText();
if (!this.visible) return;
if ( !data || !data.length ) {
this.visible = false;
// Empty text
IA.is_editing = false;
if ( this.tooltipnote.hide_nowremove(evt); ) {
this.hide_editor();
if (evt && evt.type == 'keydown' && !this.saving) {
this.cancel();
// ESC pressed on new note before a save attempt
this.note = null;
this.cancel();
} else {
}
this.hide_editor();
IA.hide_cover();
this.viewer.setDefaultMsgcancel();
}
this.viewer.setShowHideEvents(true);
return;
this.viewer.hide();
} else if ( data == this.note.model.wiki ) {
this.viewer.show(); // Make sure we get the real views again.
// Text unchanged
// FIXME in Version 2.1: Unfortunately, we don't have a mouse position here, so sometimes we
this.hide_editor();
// may show the note rectangles even though the mouse is now outside the image. (It was
this.cancel();
// somewhere inside the editor in most cases (if an editor button was clicked), but if ESC was
return;
// pressed, it may actually be anywhere.)
}
},
// 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
save: function (editor) {
var datatextbox = editoreditForm.getText()wpTextbox1;
if ( !textbox ) { throw new Error( '#Server replied with invalid edit page.' ); }
if (!data || !data.length) {
var pagetext = textbox.value;
// 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.');
 
IA.setWikitext( pagetext );
// Modify the page
var textbox = editForm.wpTextbox1;
if (!textbox) throw new Error('#Server replied with invalid edit page.');
var pagetext = textbox.value;
 
var span = null;
IA.setWikitext(pagetext);
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 ) {
var span = null;
if ( this.tooltip ) { this.tooltip.size_change(); }
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 cancel: function ( editor ) {
if ( !this.tooltipnote ) this.tooltip.size_change(){ return; }
if ( !this.note.content ) {
},
// No content: Cancel and remove this note!
this.note.destroy();
this.note = null;
}
if ( editor ) { this.hide_editor(); }
},
 
cancel close_tooltip: function (editor tooltip, evt ) {
this.hide_editor( evt );
if (!this.note) return;
if (! this.note.contentcancel() {;
}
// 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 ); };
};
 
var ImageNotesViewer.prototype = function () {this.initialize.apply(this, arguments); };
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 ) {
ImageNotesViewer.prototype = {
this.setup();
initialize: function (descriptor, may_edit) {
} else {
Object.merge(descriptor, this);
// Normalize the namespace of the realName to 'File' to account for images possibly stored at
this.annotations = [];
// a foreign repository (the Commons). Otherwise a later information load might fail because
this.max_id = 0;
// the link is local and might actually be given as "Bild:Foo.jpg". If that page doesn't exist
this.main_div = null;
// locally, we want to ask at the Commons about "File:Foo.jpg". The Commons doesn't understand
this.msg = null;
// the localized namespace names of other wikis, but the canonical namespace name 'File' works
this.may_edit = may_edit;
// also locally.
this.setup_done = false;
this.realName = 'File:' + this.realName.substring( this.realName.indexOf( ':' ) + 1 );
this.tip = null;
}
this.icon = null;
},
this.factors = {
dx : this.full_img.width / this.thumb.width,
dy : this.full_img.height / this.thumb.height
};
 
setup: function ( onlyIcon ) {
if (!this.isThumbnail && !this.isOther) {
this.setup_done = true;
this.setup();
var name = this.realName;
} else {
if ( this.isThumbnail || this.scope == document || this.may_edit || !IA.haveAjax ) {
// Normalize the namespace of the realName to 'File' to account for images possibly stored at
this.imgName = this.realName;
// a foreign repository (the Commons). Otherwise a later information load might fail because
this.realName = '';
// the link is local and might actually be given as "Bild:Foo.jpg". If that page doesn't exist
} else {
// locally, we want to ask at the Commons about "File:Foo.jpg". The Commons doesn't understand
name = getElementsByClassName( this.scope, '*', 'wpImageAnnotatorFullName' );
// the localized namespace names of other wikis, but the canonical namespace name 'File' works
this.realName = ( ( name && name.length ) ? LAPI.DOM.getInnerText( name[ 0 ] ) : '' );
// also locally.
this.realNameimgName = 'File:' + this.realName.substring(this.realName.indexOf(':') + 1);
}
}
},
 
var annotations = getElementsByClassName( this.scope, 'div', IA.annotation_class );
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;
}
 
if ( !this.may_edit && ( !annotations || annotations.length === 0 ) ) { return; } // Nothing to do
var annotations = getElementsByClassName (this.scope, 'div', IA.annotation_class);
 
// A div inserted around the image. It ensures that everything we add is positioned properly
if (!this.may_edit && (!annotations || annotations.length === 0))
// over the image, even if the browser window size changes and re-layouts occur.
return; // Nothing to do
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 );
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' } );
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 ) { this.icon = this.icon.firstChild; } // Skip the message container span or div
// 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
// 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: '' + 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: '' + this.thumb.width + 'px'
,position: 'relative' // Fixes IE layout bugs...
}
);
floater.appendChild(this.img_div);
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: '' + this.thumb.width + 'px'});
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: '' + 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: '' + 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) this.icon = this.icon.firstChild; // Skip the message container span or div
// 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);
}
 
var simple = !!window.getComputedStyle;
// Set overflow parents, if any
var checks = ( simple ? [ 'overflow', 'overflow-x', 'overflow-y' ] :
[ 'overflow', 'overflowX', 'overflowY' ]
);
var curStyle = null;
for ( var up = 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 );
var simple = !!window.getComputedStyle;
if ( this.overflowParents || LAPI.Browser.is_ie ) {
var checks = (simple ? ['overflow', 'overflow-x', 'overflow-y']
// If we have overflowParents, also use a mousemove listener to show/hide the whole
: ['overflow', 'overflowX', 'overflowY']
// 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
var curStyle = null;
// if we're still within the visible area of the image. IE passes through mouse_over events to
for (var up = this.img.parentNode.parentNode; up != document.body; up = up.parentNode) {
// the img even if the mouse is within a note's rectangle. Apparently is doesn't handle
curStyle = (simple ? window.getComputedStyle(up, null) : (up.currentStyle || up.style));
// transparent divs correctly. As a result, the notes will pop up and disappear only when the
// "up.style" is actually incorrect, but a best-effort fallback.
// mouse crosses the border, and if one moves the mouse a little fast across the border, we
var overflow =
// don't get any event at all. That's no good.
Array.any(checks, function (t) {
this.move_evt = LAPI.Evt.makeListener( this, this.check_hide );
var o = curStyle[t];
} else { this.hide_evt = LAPI.Evt.makeListener( this, this.hide ); }
return (o && o != 'visible') ? o : null;
this.move_listening = false;
});
this.setShowHideEvents( true );
if (overflow) {
this.visible = false;
if (!this.overflowParents)
this.setDefaultMsg();
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;
}
 
cannotEdit: function () {
this.show_evt = LAPI.Evt.makeListener(this, this.show);
if ( !this.may_edit ) { return; }
if (this.overflowParents || LAPI.Browser.is_ie) {
this.may_edit = false;
// If we have overflowParents, also use a mousemove listener to show/hide the whole
Array.forEach( this.annotations, function ( note ) { note.cannotEdit(); } );
// 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 setShowHideEvents: function ( set ) {
if (! this.may_editicon ) { return; }
if ( set ) {
this.may_edit = false;
LAPI.Evt.attach( this.img, IA.mouse_in, this.show_evt );
Array.forEach(this.annotations, function (note) { note.cannotEdit(); });
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(); }
}
},
 
setShowHideEvents removeMoveListener: function (set) {
if ( this.icon ) { return; }
this.move_listening = false;
if (set) {
if ( this.move_evt ) {
LAPI.Evt.attach(this.img, IA.mouse_in, this.show_evt);
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( null ); }
if (this.hide_evt) LAPI.Evt.attach(this.img, IA.mouse_out, this.hide_evt);
LAPI.Evt.remove( document, 'mousemove', this.move_evt, true );
} 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 adjustRectangleSize: function ( node ) {
if ( this.icon ) { return; }
// Make sure the note boxes don't overlap the image boundary; we might get an event
this.move_listening = false;
// loop otherwise if the mouse was just on that overlapped boundary, resulting in flickering.
if (this.move_evt) {
var view_x = node.offsetLeft;
if (!LAPI.Browser.is_ie && typeof document.captureEvents === 'function')
var view_y = node.offsetTop;
document.captureEvents(null);
var view_w = node.offsetWidth;
LAPI.Evt.remove(document, 'mousemove', this.move_evt, true);
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';
}
},
 
adjustRectangleSize toggle: function (node dummies ) {
var i;
if (this.icon) return;
if ( !this.annotations || this.annotations.length === 0 || this.icon ) { return; }
// Make sure the note boxes don't overlap the image boundary; we might get an event
if ( dummies ) {
// loop otherwise if the mouse was just on that overlapped boundary, resulting in flickering.
for ( i = 0; i < this.annotations.length; i++ ) {
var view_x = node.offsetLeft;
this.annotations[ i ].view.style.display = 'none';
var view_y = node.offsetTop;
if ( this.visible && this.annotations[ i ].tooltip ) { this.annotations[ i ].tooltip.hide_now( null ); }
var view_w = node.offsetWidth;
this.annotations[ i ].dummy.style.display = ( this.visible ? 'none' : '' );
var view_h = node.offsetHeight;
if ( !this.visible ) { this.adjustRectangleSize( this.annotations[ i ].dummy ); }
if (view_x === 0) view_x = 1;
}
if (view_y === 0) view_y = 1;
} else {
if (view_x + view_w >= this.thumb.width) {
for ( i = 0; i view_w =< this.thumbannotations.widthlength; -i++ view_x) - 1;{
this.annotations[ i ].dummy.style.display = 'none';
if (view_w <= 4) { view_w = 4; view_x = this.thumb.width - view_w - 1;}
this.annotations[ i ].view.style.display = ( this.visible ? 'none' : '' );
}
if ( !this.visible ) { this.adjustRectangleSize( this.annotations[ i ].view ); }
if (view_y + view_h >= this.thumb.height) {
if ( this.visible && this.annotations[ i ].tooltip ) { this.annotations[ i ].tooltip.hide_now( null ); }
view_h = this.thumb.height - view_y - 1;
}
if (view_h <= 4) { view_h = 4; view_y = this.thumb.height - view_h - 1;}
}
}
this.visible = !this.visible;
// 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 = '' + view_y + 'px';
node.style.left = '' + view_x + 'px';
node.style.width = '' + (view_w - 2) + 'px';
node.style.height = '' + (view_h - 2) + 'px';
node.firstChild.style.width = '' + (view_w - 4) + 'px';
node.firstChild.style.height = '' + (view_h - 4) + 'px';
}
},
 
toggle show: function (dummies evt ) {
if ( this.visible || this.icon ) { return; }
var i;
this.toggle( IA.is_adding || IA.is_editing );
if (!this.annotations || this.annotations.length === 0 || this.icon) return;
if ( this.move_evt && !this.move_listening ) {
if (dummies) {
LAPI.Evt.attach( document, 'mousemove', this.move_evt, true );
for (i = 0; i < this.annotations.length; i++) {
this.move_listening = true;
this.annotations[i].view.style.display = 'none';
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( Event.MOUSEMOVE ); }
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 hide: function ( evt ) {
if (this.visible || this.icon ) { return true; }
if ( !this.visible ) {
this.toggle(IA.is_adding || IA.is_editing);
// Huh?
if (this.move_evt && !this.move_listening) {
if ( this.move_listening ) { this.removeMoveListener(); }
LAPI.Evt.attach(document, 'mousemove', this.move_evt, true);
return true;
this.move_listening = true;
}
if (!LAPI.Browser.is_ie && typeof document.captureEvents === 'function')
if ( evt ) {
document.captureEvents(Event.MOUSEMOVE);
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 ) {
hide: function (evt) {
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 }; }
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.
 
return {
function intersect_rectangles (a, b) {
x: Math.max( a.x, b.x ),
if (b.x > a.r || b.r < a.x || b.y > a.b || b.b < a.y)
y: Math.max( a.y, b.y ),
return { x:0, y:0, r:0, b:0 };
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++ ) {
return {
img_pos = LAPI.Pos.position( this.overflowParents[ i ] );
x: Math.max(a.x, b.x),
img_pos.r = img_pos.x + this.overflowParents[ i ].clientWidth;
y: Math.max(a.y, b.y),
img_pos.b = img_pos.y + this.overflowParents[ i ].clientHeight;
r: Math.min(a.r, b.r),
rect = intersect_rectangles( rect, img_pos );
b: Math.min(a.b, b.b)
}
};
}
}
 
is_within = for !(i = 0; i < this.overflowParents.length && rect.x <>= rect.r &&|| rect.y <>= rect.b; i++)|| // Empty {rectangle
rect.x >= mouse_pos.x || rect.r <= mouse_pos.x ||
img_pos = LAPI.Pos.position(this.overflowParents[i]);
rect.y >= mouse_pos.y || rect.b <= mouse_pos.y
img_pos.r = img_pos.x + this.overflowParents[i].clientWidth;
);
img_pos.b = img_pos.y + this.overflowParents[i].clientHeight;
if ( is_within ) {
rect = intersect_rectangles (rect, img_pos);
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 ) { this.tip.hide_now(); } // Inside the image, but not within any note rectangle
}
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 ) {
is_within = !( rect.x >= rect.r || rect.y >= rect.b // Empty rectangle
if ( this.icon ) { return true; }
|| rect.x >= mouse_pos.x || rect.r <= mouse_pos.x
if ( this.visible ) { this.hide( evt ); }
|| rect.y >= mouse_pos.y || rect.b <= mouse_pos.y
return true;
);
},
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) this.tip.hide_now(); // Inside the image, but not within any note rectangle
}
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 register: function (evt new_note ) {
this.annotations[ this.annotations.length ] = new_note;
if (this.icon) return true;
if ( new_note.model.id > 0 ) {
if (this.visible)
if ( new_note.model.id > this.max_id ) { this.max_id = new_note.model.id; }
this.hide(evt);
} else {
return true;
new_note.model.id = ++this.max_id;
},
}
},
 
register deregister: function (new_note note ) {
Array.remove( this.annotations[this.annotations.length], =note new_note);
if (new_note note.model.id >== this.max_id 0) { this.max_id--; }
if ( this.annotations.length === 0 ) { this.setDefaultMsg(); } // If we removed the last one, clear the msg
if (new_note.model.id > this.max_id) this.max_id = new_note.model.id;
},
} else {
new_note.model.id = ++this.max_id;
}
},
 
deregister setDefaultMsg: function (note) {
if ( this.annotations && Array.remove(this.annotations,.length && this.msg note); {
LAPI.DOM.removeChildren( this.msg );
if (note.model.id == this.max_id) this.max_id--;
this.msg.appendChild(
if (this.annotations.length === 0) this.setDefaultMsg(); //If we removed the last one, clear the msg
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 = ''; }
}
 
};
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,
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.
 
button_div: null,
haveAjax: false,
add_button: null,
 
button_div cover: null,
add_button border: null,
definer: null,
 
mouse_in: ( window.ActiveXObject ? 'mouseenter' : 'mouseover' ),
cover: null,
mouse_out: ( window.ActiveXObject ? 'mouseleave' : 'mouseout' ),
border: null,
definer: null,
 
annotation_class: 'image_annotation',
mouse_in: (window.ActiveXObject ? 'mouseenter' : 'mouseover'),
mouse_out: (window.ActiveXObject ? 'mouseleave' : 'mouseout'),
 
// Format of notes in Wikitext. Note: there are two formats, an old one and a new one.
annotation_class : 'image_annotation',
// 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,
// 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}}'
}
],
 
wiki_read: false,
tooltip_styles : // The style for all our tooltips
is_rtl: false,
{ 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
},
 
move_listening: false,
editor: null,
is_tracking: false,
is_adding: false,
is_editing: false,
 
zoom_threshold: 8.0,
wiki_read: false,
zoom_factor: 4.0,
is_rtl: false,
 
install_attempts: 0,
move_listening : false,
max_install_attempts: 10, // Maximum 5 seconds
is_tracking: false,
is_adding: false,
is_editing: false,
 
imgs_with_notes: [],
zoom_threshold : 8.0,
thumbs: [],
zoom_factor: 4.0,
other_images: [],
 
// Fallback
install_attempts: 0,
indication_icon: '//upload.wikimedia.org/wikipedia/commons/8/8a/Gtk-dialog-info-14px.png',
max_install_attempts : 10, // Maximum 5 seconds
 
install: function ( config ) {
imgs_with_notes : [],
if ( typeof ImageAnnotator_disable !== 'undefined' && !!ImageAnnotator_disable ) { return; }
thumbs: [],
if ( !config || ImageAnnotator_config != null ) { return; }
other_images: [],
 
// Double check.
// Fallback
if ( !config.viewingEnabled() ) { return; }
indication_icon : '//upload.wikimedia.org/wikipedia/commons/8/8a/Gtk-dialog-info-14px.png',
 
var self = IA;
install: function (config) {
ImageAnnotator_config = config;
if (typeof ImageAnnotator_disable !== 'undefined' && !!ImageAnnotator_disable) return;
if (!config || ImageAnnotator_config != null) return;
 
// Determine whether we have XmlHttp. We try to determine this here to be able to avoid
// Double check.
// doing too much work.
if (!config.viewingEnabled()) return;
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.
var self = IA;
self.may_edit = mw.config.get( 'wgNamespaceNumber' ) >= 0 && mw.config.get( 'wgArticleId' ) > 0 && self.haveAjax && config.editingEnabled();
ImageAnnotator_config = config;
 
function namespaceCheck( list ) {
// Determine whether we have XmlHttp. We try to determine this here to be able to avoid
if ( !list || Object.prototype.toString.call( list ) !== '[object Array]' ) { return false; }
// doing too much work.
var namespaceIds = mw.config.get( 'wgNamespaceIds' );
if ( window.XMLHttpRequest
if ( !namespaceIds ) { return false; }
&& typeof LAPI !== 'undefined'
var namespaceNumber = mw.config.get( 'wgNamespaceNumber' );
&& typeof LAPI.Ajax !== 'undefined'
for ( var i = 0; i < list.length; i++ ) {
&& typeof LAPI.Ajax.getRequest !== 'undefined'
if (
)
typeof list[ i ] === 'string' &&
{
( list[ i ] === '*' ||
self.haveAjax = (LAPI.Ajax.getRequest() != null);
namespaceIds[ list[ i ].toLowerCase().replace( / /g, '_' ) ] === namespaceNumber
self.ajaxQueried = true;
)
} else {
) {
self.haveAjax = true; // A pity. May occur on IE. We'll check again later on.
return true;
self.ajaxQueried = false;
}
}
}
return false;
}
 
self.rules = { inline: {}, thumbs: {}, shared: {} };
// 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();
 
// Now set the default rules. Undefined means default setting (true for show, false for icon),
function namespaceCheck (list)
// but overrideable by per-image rules. If set, it's not overrideable by per-image rules.
{
//
if (!list || Object.prototype.toString.call(list) !== '[object Array]') return false;
if (
var namespaceIds = mw.config.get('wgNamespaceIds');
!self.haveAjax ||
if(!namespaceIds) return false;
!config.generalImagesEnabled() ||
var namespaceNumber = mw.config.get('wgNamespaceNumber');
namespaceCheck( window.ImageAnnotator_no_images || null )
for (var i = 0; i < list.length; i++) {
) {
if (typeof list[i] === 'string'
self.rules.inline.show = false;
&& (list[i] === '*'
self.rules.thumbs.show = false;
|| namespaceIds[list[i].toLowerCase().replace(/ /g, '_')] === namespaceNumber
self.rules.shared.show = false;
)
} else {
)
if (
return true;
!self.haveAjax ||
}
!config.thumbsEnabled() ||
return false;
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.rules = { inline: {}, thumbs: {}, shared : {} };
self.hideCaptions = namespaceCheck( window.ImageAnnotator_hide_captions || null );
 
var do_images = typeof self.rules.inline.show === 'undefined' || self.rules.inline.show;
// 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;
}
 
if ( do_images ) {
// User rule for displaying captions on images in articles
// Per-article switching off of note display on inline images and thumbnails
self.hideCaptions = namespaceCheck(window.ImageAnnotator_hide_captions || null);
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
var do_images = typeof self.rules.inline.show === 'undefined' || self.rules.inline.show;
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) {
// 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;
}
}
}
 
if ( do_images ) {
// Make sure the shared value is set
var bodyContent = document.getElementById( 'bodyContent' ) || // monobook, vector
self.rules.shared.show =
document.getElementById( 'mw_contentholder' ) || // modern
typeof self.rules.shared.show === 'undefined' || self.rules.shared.show;
document.getElementById( 'article' ); // old skins
if ( bodyContent ) {
var all_imgs = bodyContent.getElementsByTagName( 'img' );
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 ) { self.thumbs = getElementsByClassName( document, 'div', 'thumbinner' ); } // No galleries!
}
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 () {
do_images = typeof self.rules.inline.show === 'undefined' || self.rules.inline.show;
if ( typeof Tooltip var=== do_thumbs'undefined' =|| typeof self.rules.thumbs.showLAPI === 'undefined' ||) self.rules.thumbs.show;{
if ( IA.install_attempts++ < IA.max_install_attempts ) {
setTimeout( IA.wait_for_required_libraries, 500 ); // 0.5 sec.
}
return;
}
if ( LAPI.Browser.is_opera && !LAPI.Browser.is_opera_ge_9 ) { return; } // Opera 8 has severe problems
// Get the UI. We're likely to need it if we made it to here.
IA.setup_ui();
IA.setup();
},
 
setup: function () {
if (do_images) {
var self = IA;
var bodyContent = document.getElementById('bodyContent') // monobook, vector
self.imgs = [];
|| document.getElementById('mw_contentholder') // modern
|| document.getElementById('article') // old skins
;
if (bodyContent) {
var all_imgs = bodyContent.getElementsByTagName('img');
 
// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
// This prevents traversing a page with more than 400 images
self.is_rtl =
// There are extreme cases like [[Emoji]] that high number of images can cause
LAPI.DOM.hasClass( document.body, 'rtl' ) ||
// huge lag specially on Chrome
(
if (all_imgs.length > 400) {
LAPI.DOM.currentStyle && // Paranoia: added recently, not everyone might have it
// purging the array, simply a hack to avoid more indention
LAPI.DOM.currentStyle( document.body, 'direction' ) == 'rtl'
all_imgs = [];
);
}
 
var stylepath = mw.config.get( 'stylepath' ) || '/skin';
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)
self.thumbs = getElementsByClassName (document, 'div', 'thumbinner'); // No galleries!
}
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();
}
},
 
// Use this to temporarily display an image off-screen to get its dimensions
wait_for_required_libraries: function () {
var testImgDiv =
if (typeof Tooltip == 'undefined' || typeof LAPI == 'undefined') {
LAPI.make( 'div', null, {
if (IA.install_attempts++ < IA.max_install_attempts) {
display: 'none', position: 'absolute', width: '300px',
setTimeout(IA.wait_for_required_libraries, 500); // 0.5 sec.
overflow: 'hidden', overflowX: 'hidden', left: '-10000px'
}
} );
return;
document.body.insertBefore( testImgDiv, document.body.firstChild );
}
if (LAPI.Browser.is_opera && !LAPI.Browser.is_opera_ge_9) return; // Opera 8 has severe problems
// Get the UI. We're likely to need it if we made it to here.
IA.setup_ui();
IA.setup();
},
 
function img_check( img, is_other ) {
setup: function () {
var srcW = parseInt( img.getAttribute( 'width', 2 ), 10 );
var self = IA;
var srcH = parseInt( img.getAttribute( 'height', 2 ), 10 );
self.imgs = [];
// 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.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.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.parentNode.nodeName ) ) { return null; }
}
// Exclude any that are within an image note!
var up = img.parentNode.parentNode;
while ( 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 ) {
// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
var file_div = scope;
self.is_rtl =
var is_thumb =
LAPI.DOM.hasClass(document.body, 'rtl')
scope != document &&
|| ( LAPI.DOM.currentStyle // Paranoia: added recently, not everyone might have it
scope.nodeName.toLowerCase() == 'div' &&
&& LAPI.DOM.currentStyle(document.body, 'direction') == 'rtl'
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.
}
}
if ( name.search( /\.(jpe?g|png|gif|svg|tiff?)$/i ) < 0 ) { return null; } // Only PNG, JPE?G, GIF, SVG, and TIFF?
// Finally check for wpImageAnnotatorControl
var icon_only = false;
var no_caption = false;
if ( is_thumb || is_other ) {
var up = 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 ) {
var stylepath = mw.config.get('stylepath') || '/skin';
Array.forEach( list,
function ( elem ) {
var desc = setup_one( elem );
if ( desc ) { self.imgs[ self.imgs.length ] = desc; }
}
);
}
 
if ( mw.config.get( 'wgNamespaceNumber' ) == 6 ) {
// Use this to temporarily display an image off-screen to get its dimensions
setup_images( [ document ] );
var testImgDiv =
self.may_edit = self.may_edit && ( self.imgs.length == 1 );
LAPI.make('div', null,
setup_images( self.imgs_with_notes );
{ display: 'none', position: 'absolute', width: '300px'
} else {
, overflow: 'hidden', overflowX: 'hidden', left: '-10000px'
setup_images( self.imgs_with_notes );
}
self.may_edit = self.may_edit && ( self.imgs.length == 1 );
);
}
document.body.insertBefore(testImgDiv, document.body.firstChild);
 
self.may_edit = self.may_edit && ___location.href.search( /[?&]oldid=/ ) < 0;
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.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.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.parentNode.nodeName))
return null;
}
// Exclude any that are within an image note!
var up = img.parentNode.parentNode;
while (up != document.body) {
if (LAPI.DOM.hasClass(up, IA.annotation_class)) return null;
up = up.parentNode;
}
return {width: w, height: h};
}
 
if ( self.haveAjax ) {
function setup_one (scope) {
setup_images( self.thumbs );
var file_div = scope;
setup_images( self.other_images );
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.
}
}
if (name.search(/\.(jpe?g|png|gif|svg|tiff?)$/i) < 0) return null; // Only PNG, JPE?G, GIF, SVG, and TIFF?
// Finally check for wpImageAnnotatorControl
var icon_only = false;
var no_caption = false;
if (is_thumb || is_other) {
var up = 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
};
}
 
// Remove the test div
function setup_images (list)
LAPI.DOM.removeNode( testImgDiv );
{
Array.forEach(list,
function (elem) {
var desc = setup_one (elem);
if (desc) self.imgs[self.imgs.length] = desc;
}
);
}
 
if ( self.imgs.length === 0 ) { return; }
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);
}
 
// We get the UI texts in parallel, but wait for them at the beginning of complete_setup, where we
self.may_edit = self.may_edit && ___location.href.search(/[?&]oldid=/) < 0;
// need them. This has in particular a benefit if we do have to query for the file sizes below.
 
if ( self.imgs.length == if1 && (self.imgs[ 0 ].scope == document && !self.haveAjax ) {
// Try to get the full size without Ajax.
setup_images (self.thumbs);
self.imgs[ 0 ].full_img = LAPI.WP.fullImageSizeFromPage();
setup_images (self.other_images);
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.)
// Remove the test div
// Also avoid using Ajax on IE6...
LAPI.DOM.removeNode(testImgDiv);
 
var cache = {};
if (self.imgs.length === 0) return;
var names = [];
 
Array.forEach( self.imgs, function ( img, idx ) {
// We get the UI texts in parallel, but wait for them at the beginning of complete_setup, where we
if ( cache[ img.realName ] ) {
// need them. This has in particular a benefit if we do have to query for the file sizes below.
cache[ img.realName ][ cache[ img.realName ].length ] = idx;
} else {
cache[ img.realName ] = [ idx ];
names[ names.length ] = img.realName;
}
} );
 
var to_do = names.length;
if (self.imgs.length == 1 && self.imgs[0].scope == document && !self.haveAjax) {
var done = 0;
// 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;
}
}
 
function check_done( length ) {
// Get the full sizes of all the images. If more than 50, make several calls. (The API has limits.)
done += length;
// Also avoid using Ajax on IE6...
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 ) {
var cache = {};
function build_titles( from, length, url_limit ) {
var names = [];
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;
Array.forEach(self.imgs, function (img, idx) {
while ( to_do > 0 ) {
if (cache[img.realName]) {
params = build_titles( start, Math.min( 50, to_do ), url_limit );
cache[img.realName][cache[img.realName].length] = idx;
execute_call( params.n, params.text );
} else {
to_do -= params.n;
cache[img.realName] = [idx];
start += params.n;
names[names.length] = img.realName;
}
}
}
});
 
function set_info( json ) {
var to_do = names.length;
try {
var done = 0;
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 ) {
function check_done (length)
// 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.
done += length;
ImageAnnotator.info_callbacks = [];
if (done >= names.length) {
var template = mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php?action=query&format=json' +
if (typeof ImageAnnotator.info_callbacks !== 'undefined') ImageAnnotator.info_callbacks = null;
'&prop=info|imageinfo&inprop=protection&iiprop=size' +
self.setup_step_two();
'&titles=&callback=ImageAnnotator.info_callbacks[].callback';
}
if ( template.startsWith( '//' ) ) { template = document.___location.protocol + template; } // Avoid protocol-relative URIs (IE7 bug)
}
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 make_calls (execute_call,) url_limit){
// Complete the UI object we've gotten from config.
{
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};
}
 
ImageAnnotator.UI.ready = false;
var start = 0, chunk = 0, params;
ImageAnnotator.UI.repo = null;
while (to_do > 0) {
ImageAnnotator.UI.needs_plea = false;
params = build_titles (start, Math.min(50, to_do), url_limit);
execute_call (params.n, params.text);
to_do -= params.n;
start += params.n;
}
}
 
var readyEvent = [];
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) {
}
}
 
ImageAnnotator.UI.fireReadyEvent = function () {
if ((!window.XMLHttpRequest && !!window.ActiveXObject) || !self.haveAjax) {
if ( ImageAnnotator.UI.ready ) { return; } // Already fired, nothing to do.
// IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that
ImageAnnotator.UI.ready = true;
// prompt by using getScript instead of parseWikitext in this case.
// Call all registered handlers, and clear the array.
ImageAnnotator.info_callbacks = [];
Array.forEach( readyEvent, function ( f, idx ) {
var template = mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php?action=query&format=json'
try { f(); } catch ( ex ) {}
+ '&prop=info|imageinfo&inprop=protection&iiprop=size'
readyEvent[ idx ] = null;
+ '&titles=&callback=ImageAnnotator.info_callbacks[].callback';
} );
if (template.startsWith('//')) template = document.___location.protocol + template; // Avoid protocol-relative URIs (IE7 bug)
readyEvent = null;
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
},
 
ImageAnnotator.UI.addReadyEventHandler setup_ui:= function ( f ) {
if ( ImageAnnotator.UI.ready ) {
// Complete the UI object we've gotten from config.
f(); // Already fired: call directly
} else {
readyEvent[ readyEvent.length ] = f;
}
};
 
ImageAnnotator.UI.readysetup = function () = false;{
if ( ImageAnnotator.UI.repo ) { return; = null;}
var self = ImageAnnotator.UI.needs_plea = false;
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 readyEvent = [];
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 ) { return null; } // Hmmm... what happened here? We normally have defaults...
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.fireReadyEventget_plea = function () {
var self = if (ImageAnnotator.UI.ready) return; // Already fired, nothing to do.
var translate = self.get( 'wpTranslate', false, true ) || 'translate';
ImageAnnotator.UI.ready = true;
var span = LAPI.make( 'small' );
// Call all registered handlers, and clear the array.
span.appendChild( document.createTextNode( '\xa0(' ) );
Array.forEach( readyEvent , function (f, idx) {
span.appendChild(
try {f ();} catch (ex) {}
LAPI.DOM.makeLink(
readyEvent[idx] = null;
mw.config.get( 'wgServer' ) + mw.config.get( 'wgScript' ) + '?title=MediaWiki_talk:ImageAnnotatorTexts' +
});
'&action=edit&section=new&withJS=MediaWiki:ImageAnnotatorTranslator.js' +
readyEvent = null;
'&language=' + mw.config.get( 'wgUserLanguage' ),
};
translate,
( typeof translate === 'string' ? translate : LAPI.DOM.getInnerText( translate ).trim() )
)
);
span.appendChild( document.createTextNode( ')' ) );
return span;
};
 
ImageAnnotator.UI.addReadyEventHandlerinit = function (f html_text_or_json ) {
var text;
if (ImageAnnotator.UI.ready) {
if (
f (); // Already fired: call directly
typeof html_text_or_json === 'string' ) { text = html_text_or_json; } else if ( typeof html_text_or_json !== 'undefined' &&
} else {
typeof html_text_or_json.parse !== 'undefined' &&
readyEvent[readyEvent.length] = f;
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.setup = function () {
if ( ImageAnnotator.UI.repofireReadyEvent() return;
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);
};
 
var node = LAPI.make( 'div', null, { display: 'none' } );
ImageAnnotator.UI.get = function (id, basic, no_plea) {
document.body.appendChild( node );
var self = ImageAnnotator.UI;
try {
if (!self.repo) self.setup();
node.innerHTML = text;
var result = null;
} catch ( ex ) {
var add_plea = false;
LAPI.DOM.removeNode( node );
if (self.basic) {
node = null;
result = self.repo[id];
// Swallow. We'll just work with the default UI
} else {
}
result = UIElements.getEntry(id, self.repo, mw.config.get('wgUserLanguage'), null);
if ( node && !ImageAnnotator.UI.repo ) { ImageAnnotator.UI.setup(); }
add_plea = !result;
ImageAnnotator.UI.fireReadyEvent();
if (!result) result = UIElements.getEntry(id, self.repo);
};
}
self.needs_plea = add_plea;
if (!result) return null; // Hmmm... what happened here? We normally have defaults...
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;
};
 
var ui_page = '{{MediaWiki:ImageAnnotatorTexts' +
ImageAnnotator.UI.get_plea = function () {
( mw.config.get( 'wgUserLanguage' ) != mw.config.get( 'wgContentLanguage' ) ? '|lang=' + mw.config.get( 'wgUserLanguage' ) : '' ) +
var self = ImageAnnotator.UI;
'|live=1}}';
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;
};
 
function get_ui_no_ajax() {
ImageAnnotator.UI.init = function (html_text_or_json) {
var url =
var text;
mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php?action=parse&pst&text=' +
if (typeof html_text_or_json === 'string')
encodeURIComponent( ui_page ) + '&title=API&prop=text&format=json' +
text = html_text_or_json;
'&callback=ImageAnnotator.UI.init&maxage=14400&smaxage=14400';
else if ( typeof html_text_or_json !== 'undefined'
// Result cached for 4 hours. How to properly handle an error? It appears there's no way to catch
&& typeof html_text_or_json.parse !== 'undefined'
// that on IE. (On FF, we could use an onerror handler on the script tag, but on FF, we use Ajax
&& typeof html_text_or_json.parse.text !== 'undefined'
// anyway.)
&& typeof html_text_or_json.parse.text['*'] !== 'undefined'
IA.getScript( url, true ); // No local caching!
)
}
text = html_text_or_json.parse.text['*'];
else
text = null;
 
function get_ui() {
if (!text) {
IA.haveAjax = ( LAPI.Ajax.getRequest() != null );
ImageAnnotator.UI.fireReadyEvent();
IA.ajaxQueried = true;
return;
}
 
// Works only with Ajax (but then, most of this script doesn't work without).
var node = LAPI.make('div', null, {display: 'none'});
// Check what this does to load times... If lots of people used this, it might be better to
document.body.appendChild(node);
// have the UI texts included in some footer as we did on Special:Upload. True, everybody
try {
// would get the texts, even people not using this, but the texts are small anyway...
node.innerHTML = text;
if ( !IA.haveAjax ) {
} catch (ex) {
get_ui_no_ajax(); // Fallback.
LAPI.DOM.removeNode(node);
return;
node = null;
}
// Swallow. We'll just work with the default UI
}
if (node && !ImageAnnotator.UI.repo) ImageAnnotator.UI.setup();
ImageAnnotator.UI.fireReadyEvent();
};
 
LAPI.Ajax.parseWikitext(
var ui_page = '{{MediaWiki:ImageAnnotatorTexts'
ui_page,
+ (mw.config.get('wgUserLanguage') != mw.config.get('wgContentLanguage') ? '|lang=' + mw.config.get('wgUserLanguage') : '')
ImageAnnotator.UI.init,
+ '|live=1}}';
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 ) {
function get_ui_no_ajax ()
// 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
var url =
// is that we don't do anything if this fails for some reason.
mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php?action=parse&pst&text='
get_ui_no_ajax();
+ encodeURIComponent(ui_page) + '&title=API&prop=text&format=json'
} else {
+ '&callback=ImageAnnotator.UI.init&maxage=14400&smaxage=14400'
get_ui();
;
}
// 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!
}
 
setup_step_two: function get_ui () {
var self = IA;
{
IA.haveAjax = (LAPI.Ajax.getRequest() != null);
IA.ajaxQueried = true;
 
// Throw out any images for which we miss either the thumbnail or the full image size.
// Works only with Ajax (but then, most of this script doesn't work without).
// Also throws out thumbnails that are larger than the full image.
// Check what this does to load times... If lots of people used this, it might be better to
self.imgs = Array.select( self.imgs, function ( elem, idx ) {
// have the UI texts included in some footer as we did on Special:Upload. True, everybody
var result =
// would get the texts, even people not using this, but the texts are small anyway...
elem.thumb.width > 0 && elem.thumb.height > 0 &&
if (!IA.haveAjax) {
typeof elem.full_img !== 'undefined' &&
get_ui_no_ajax (); // Fallback.
elem.full_img.width > 0 && elem.full_img.height > 0 &&
return;
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; }
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
 
ImageAnnotator.UI.addReadyEventHandler( IA.complete_setup );
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 complete_setup: function () {
// We can be sure to have the UI here because this is called only when the ready event of the
var self = IA;
// UI object is fired.
var self = IA;
 
// Check edit permissions
// Throw out any images for which we miss either the thumbnail or the full image size.
if ( self.may_edit && mw.config.get( 'wgRestrictionEdit' ) ) {
// Also throws out thumbnails that are larger than the full image.
self.may_edit = (
self.imgs = Array.select(self.imgs, function (elem, idx) {
( mw.config.get( 'wgRestrictionEdit' ).length === 0 || mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( 'sysop' ) ) ||
var result =
(
elem.thumb.width > 0 && elem.thumb.height > 0
mw.config.get( 'wgRestrictionEdit' ).length === 1 && mw.config.get( 'wgRestrictionEdit' )[ 0 ] === 'autoconfirmed' &&
&& typeof elem.full_img !== 'undefined'
mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( 'confirmed' ) // confirmed & autoconfirmed
&& 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 ===may_edit 0) return;{
// 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; }
ImageAnnotator.UI.addReadyEventHandler(IA.complete_setup);
},
 
// Now create viewers for all images
complete_setup: function () {
self.viewers = new Array( self.imgs.length );
// We can be sure to have the UI here because this is called only when the ready event of the
for ( var i = 0; i < self.imgs.length; i++ ) {
// UI object is fired.
self.viewers[ i ] = new ImageNotesViewer( self.imgs[ i ], i === 0 && self.may_edit );
var self = IA;
}
 
if ( self.may_edit ) {
// 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
)
);
}
 
// Respect user override for zoom, if any
if (self.may_edit) {
self.zoom_threshold = ImageAnnotator_config.zoom_threshold;
// Check whether the image is local. Don't allow editing if the file is remote.
if (
var sharedUpload = getElementsByClassName (document, 'div', 'sharedUploadNotice');
typeof window.ImageAnnotator_zoom_threshold !== 'undefined' &&
self.may_edit = (!sharedUpload || sharedUpload.length === 0);
!isNaN( window.ImageAnnotator_zoom_threshold ) &&
}
window.ImageAnnotator_zoom_threshold >= 0.0
if (self.may_edit && mw.config.get('wgNamespaceNumber') != 6) {
) {
// Only allow edits if the stored page name matches the current one.
// If somebody sets it to a nonsensical high value, that's his or her problem: there won't be any
var img_page_name =
// zooming.
getElementsByClassName (self.imgs[0].scope, '*', 'wpImageAnnotatorPageName');
self.zoom_threshold = window.ImageAnnotator_zoom_threshold;
if (img_page_name && img_page_name.length)
}
img_page_name = LAPI.DOM.getInnerText(img_page_name[0]);
// Adapt zoom threshold for small thumbnails or images with a very lopsided width/height ratio,
else
// but only if we *can* zoom at least twice
img_page_name = '';
if ( self.viewers[ 0 ].full_img.width > 300 &&
self.may_edit = (img_page_name.replace(/ /g, '_') == mw.config.get('wgTitle').replace(/ /g, '_'));
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();
if (self.may_edit && self.ajaxQueried) self.may_edit = self.haveAjax;
 
function track( evt ) {
// Now create viewers for all images
evt = evt || window.event;
self.viewers = new Array (self.imgs.length);
for if (var iself.is_adding =) 0; i <{ self.imgs.length;update_zoom( evt i++); {}
if ( !self.is_tracking ) { return LAPI.Evt.kill( evt ); }
self.viewers[i] = new ImageNotesViewer (self.imgs[i], i === 0 && self.may_edit);
var mouse_pos = LAPI.Pos.mousePosition( evt );
}
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 ) {
if (self.may_edit) {
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 ) {
// Respect user override for zoom, if any
// captureEvents is actually deprecated, but I haven't succeeded to make this work with
self.zoom_threshold = ImageAnnotator_config.zoom_threshold;
// addEventListener only.
if ( typeof window.ImageAnnotator_zoom_threshold !== 'undefined'
if ( ( self.is_tracking || self.is_adding ) && !self.move_listening ) {
&& !isNaN (window.ImageAnnotator_zoom_threshold)
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( Event.MOUSEMOVE ); }
&& window.ImageAnnotator_zoom_threshold >= 0.0
LAPI.Evt.attach( document, 'mousemove', track, true );
)
self.move_listening = true;
{
}
// 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
}
}
 
function stop_tracking( evt ) {
self.editor = new ImageAnnotationEditor ();
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 trackstart_tracking( (evt ) {
if ( !self.is_tracking ) {
evt = evt || window.event;
self.is_tracking = true;
if (self.is_adding) self.update_zoom(evt);
evt = evt || window.event;
if (!self.is_tracking) return LAPI.Evt.kill(evt);
// Set the position, size 1
var mouse_pos = LAPI.Pos.mousePosition(evt);
var mouse_pos = LAPI.Pos.mousePosition( evt );
if (!LAPI.Pos.isWithin(self.cover, mouse_pos.x, mouse_pos.y)) return;
var origin = LAPI.Pos.position( self.cover );
self.base_x = mouse_pos.x - origin.x;
// Make mouse pos relative to cover
mouse_pos self.xbase_y = mouse_pos.xy - origin.xy;
Object.merge(
mouse_pos.y = mouse_pos.y - origin.y;
{
if (mouse_pos.x >= self.base_x) {
left: self.definer.style.width = '' + String(mouse_pos.x - self.base_x ) + 'px';,
top: String( self.definer.style.leftbase_y = '' + self.base_x) + 'px';,
width: '0px',
} else {
height: '0px',
self.definer.style.width = '' + (self.base_x - mouse_pos.x) + 'px';
display: ''
self.definer.style.left = '' + mouse_pos.x + 'px';
}, self.definer.style
}
);
if (mouse_pos.y >= self.base_y) {
// Set mouse handlers
self.definer.style.height = '' + (mouse_pos.y - self.base_y) + 'px';
LAPI.Evt.remove( self.cover, 'mousedown', start_tracking );
self.definer.style.top = '' + self.base_y + 'px';
if ( LAPI.Browser.is_ie ) {
} else {
LAPI.Evt.attach( document, 'mouseup', stop_tracking ); // Doesn't work properly on self.cover...
self.definer.style.height = '' + (self.base_y - mouse_pos.y) + 'px';
} else {
self.definer.style.top = '' + mouse_pos.y + 'px';
LAPI.Evt.attach( self.cover, 'mouseup', stop_tracking );
}
}
return LAPI.Evt.kill(evt);
resume();
}
LAPI.Evt.attach( window, 'blur', pause );
LAPI.Evt.attach( window, 'focus', resume );
}
if ( evt ) { return LAPI.Evt.kill( evt ); }
return false;
}
 
function pauseadd_new( (evt ) {
if ( !self.canEdit() ) { return; }
LAPI.Evt.remove(document, 'mousemove', track, true);
if (!LAPI.Browser.is_ie && typeof document.captureEvents === 'function')
document.captureEvents(null);
self.move_listening = false;
}
 
self.editor.hide_editor();
function resume (evt) {
Tooltips.close();
// captureEvents is actually deprecated, but I haven't succeeded to make this work with
var cover = self.get_cover();
// addEventListener only.
cover.style.cursor = 'crosshair';
if ((self.is_tracking || self.is_adding) && !self.move_listening) {
self.definer = LAPI.make(
if (!LAPI.Browser.is_ie && typeof document.captureEvents === 'function')
'div', null,
document.captureEvents(Event.MOUSEMOVE);
{
LAPI.Evt.attach(document, 'mousemove', track, true);
border: '1px solid ' + IA.new_border,
self.move_listening = true;
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' );
function stop_tracking (evt) {
self.viewers[ 0 ].main_div.appendChild( self.button_div );
evt = evt || window.event;
self.add_button = LAPI.DOM.makeButton(
// Check that we're within the image. Note: this check can fail only on IE >= 7, on other
'ImageAnnotationAddButton',
// browsers, we attach the handler on self.cover and thus we don't even get events outside
ImageAnnotator.UI.get( 'wpImageAnnotatorAddButtonText', true ),
// that range.
add_new
var mouse_pos = LAPI.Pos.mousePosition(evt);
);
if (!LAPI.Pos.isWithin(self.cover, mouse_pos.x, mouse_pos.y)) return;
var add_plea = ImageAnnotator.UI.needs_plea;
if (self.is_tracking) {
self.button_div.appendChild( self.add_button );
self.is_tracking = false;
self.help_link = self.createHelpLink();
self.is_adding = false;
if ( self.help_link ) {
// Done.
self.button_div.appendChild( document.createTextNode( '\xa0' ) );
pause ();
self.button_div.appendChild( self.help_link );
if (LAPI.Browser.is_ie) {
}
//Trust Microsoft to get everything wrong!
if ( add_plea && mw.config.get( 'wgServer' ).contains( '/commons' ) ) { self.button_div.appendChild( ImageAnnotator.UI.get_plea() ); }
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;
}
 
} // end if may_edit
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: '' + self.base_x + 'px'
,top: '' + 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;
}
 
// Get the file description pages of thumbnails. Figure out for which viewers we need to do this.
function add_new (evt) {
var cache = {};
if (!self.canEdit()) return;
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; }
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 = '';
}
 
// Now we have unique page names in the cache and in to_get. Go get the corresponding file
self.button_div = LAPI.make('div');
// description pages. We make a series of simultaneous asynchronous calls to avoid hitting
self.viewers[0].main_div.appendChild(self.button_div);
// API limits and to keep the URL length below the limit for the foreign_repo calls.
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());
 
function make_calls( list, execute_call, url_limit ) {
} // end if may_edit
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 );
// Get the file description pages of thumbnails. Figure out for which viewers we need to do this.
execute_call( param.text );
var cache = {};
return param.n;
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;
}
}
});
 
var start = 0, if (get_local.lengthchunk === 0, &&to_do get_foreign= list.length === 0) return;
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;
// Now we have unique page names in the cache and in to_get. Go get the corresponding file
var blockStart = '<div class="wpImageAnnotatorInlineImageWrapper"';
// description pages. We make a series of simultaneous asynchronous calls to avoid hitting
var inlineNameEnd = '</span>';
// API limits and to keep the URL length below the limit for the foreign_repo calls.
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
function make_calls (list, execute_call, url_limit)
// 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
function composer (list, from, length, url_limit)
// strip out everything but the notes.
{
function compose strip_noise(list, from,html length,) url_limit){
var result = '';
{
var text = ''m;
// First, get rid of HTML comments and scripts
var done = 0;
html = html.replace( /<!--(.|\s)*?-->/g, '' ).replace( /<script(.|\s)*?\/script>/g, '' );
for (var i = from; i < from + length; i++) {
var i = html.indexOf( blockStart, idx ), idx = 0, l = html.length;
var new_text =
// Now collect pages
'<div class="wpImageAnnotatorInlineImageWrapper" style="display:none;">'
while ( idx < l && i >= idx ) {
+ '<span class="image_annotation_inline_name">' + list[i] + '</span>'
var j = html.indexOf( inlineNameEnd, i + blockStart.length );
+ '{{:' + list[i] + '}}' // Leading dot to avoid getting links to the full images if we hit a parser limit
if ( j < i + blockStart.length ) { break; }
+ '</div>'
result += html.substring( i, j + inlineNameEnd.length );
;
idx = j + inlineNameEnd.length;
if (url_limit) {
// Now collect all image image notes for that page
new_text = encodeURIComponent(new_text);
var note_begin = 0, next_block = html.indexOf( blockStart, idx );
if (text.length && (text.length + new_text.length > url_limit)) break;
// Do we have image note control or color templates?
}
j = idx;
text = text + new_text;
for ( ;; ) {
done++;
noteControlRE.lastIndex = j;
// Additionally, limit the number of image pages to get: these can be large, and the server
m = noteControlRE.exec( html );
// may refuse to actually do the transclusions but may in that case include the full images
if ( !m || next_block >= idx && m.index > next_block ) { break; }
// in the result, which would make us load the full images, which is desastrous if there are
result += m[ 0 ];
// many thumbs to large images on the page.
j = m.index + m[ 0 ].length;
if (done == 5) break;
}
}
// Collect notes
return {text: text, n: done};
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 ) { result += '</div>'; } // Missing ends.
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 param = compose (list, from, length, url_limit);
var node = LAPI.make( 'div', null, { display: 'none' } );
execute_call (param.text);
document.body.appendChild( node );
return param.n;
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 = [];
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;
}
}
 
function make_script_calls( list, api ) {
var divRE = /(<\s*div\b)|(<\/\s*div\s*>)/ig;
var template = api + '?action=parse&pst&text=&prop=text&format=json' +
var blockStart = '<div class="wpImageAnnotatorInlineImageWrapper"';
'&maxage=1800&smaxage=1800&uselang=' + mw.config.get( 'wgUserLanguage' ) + // see bugzilla 22764
var inlineNameEnd = '</span>';
'&callback=ImageAnnotator.script_callbacks[].callback';
var noteStart = '<div id="image_annotation_note_';
if ( template.startsWith( '//' ) ) { template = document.___location.protocol + template; } // Avoid protocol-relative URIs (IE7 bug)
var noteControlRE = /<div\s*class="(wpImageAnnotatorInlinedRules|image_annotation_colors")(\S|\s)*?\/div>/g;
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 ) {
// Our parse request returns the full html of the description pages' contents, including any
make_script_calls( get_local, mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php' );
// license or information or other templates that we don't care about, and which may contain
} else {
// additional images we'd rather not load when we add this (X)HTML to the DOM. Therefore, we
make_calls(
// strip out everything but the notes.
get_local,
function strip_noise (html) {
function ( text ) {
var result = '';
LAPI.Ajax.parseWikitext(
var m;
text,
// First, get rid of HTML comments and scripts
function ( html_text ) { if ( html_text ) { setup_thumb_viewers( html_text ); } },
html = html.replace(/<\!--(.|\s)*?--\>/g, '').replace(/<script(.|\s)*?\/script>/g, '');
function () {},
var i = html.indexOf(blockStart, idx), idx = 0, l = html.length;
false,
// Now collect pages
null,
while (idx < l && i >= idx) {
'API', // Fixed string to enable caching at all
var j = html.indexOf(inlineNameEnd, i+blockStart.length);
1800 // 30 minutes caching.
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) result += '</div>'; // Missing ends.
idx = k;
} // end loop collecting notes
result += '</div>'; // Close the wrapper
i = next_block;
} // end loop collecting pages
return result;
}
 
// Can't use Ajax for foreign repo, might violate single-origin policy (e.g. from wikisource.org
function setup_thumb_viewers (html_text)
// 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,
var node = LAPI.make('div', null, {display: 'none'});
// the limit appears to be 8kB).
document.body.appendChild(node);
make_script_calls( get_foreign, ImageAnnotator_config.sharedRepositoryAPI() );
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);
}
 
show_zoom: function () {
ImageAnnotator.script_callbacks = [];
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 make_script_calls (list, apievt ) {
if ( !evt ) { return; } // We need an event to calculate positions!
{
var self = IA;
var template = api + '?action=parse&pst&text=&prop=text&format=json'
if ( !self.zoom ) { return; }
+ '&maxage=1800&smaxage=1800&uselang=' + mw.config.get('wgUserLanguage') //see bugzilla 22764
var mouse_pos = LAPI.Pos.mousePosition( evt );
+ '&callback=ImageAnnotator.script_callbacks[].callback';
var origin = LAPI.Pos.position( self.cover );
if (template.startsWith('//')) template = document.___location.protocol + template; // Avoid protocol-relative URIs (IE7 bug)
if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) {
make_calls (
IA.hide_zoom();
list
return;
, function (text) {
}
var idx = ImageAnnotator.script_callbacks.length;
var dx = mouse_pos.x - origin.x;
ImageAnnotator.script_callbacks[idx] =
var dy = mouse_pos.y - origin.y;
{ callback: function (json) {
// dx, dy is the offset within the preview image. Align the zoom image accordingly.
if (json && json.parse && json.parse.text && json.parse.text['*']) {
var top = -dy * self.zoom_factor + 100;
setup_thumb_viewers (json.parse.text['*']);
var left = -dx * self.zoom_factor + 100;
}
self.zoom.firstChild.firstChild.style.top = String( top ) + 'px';
ImageAnnotator.script_callbacks[idx].done = true;
self.zoom.firstChild.firstChild.style.left = String( left ) + 'px';
if (ImageAnnotator.script_callbacks[idx].script) {
self.zoom.style.top = mouse_pos.y + 10 + 'px'; // Right below the mouse pointer
LAPI.DOM.removeNode(ImageAnnotator.script_callbacks[idx].script);
// Horizontally keep it in view.
ImageAnnotator.script_callbacks[idx].script = null;
var x = ( self.is_rtl ? mouse_pos.x - 10 : mouse_pos.x + 10 );
}
if ( x < 0 ) { x = 0; }
self.zoom.style.left = x + 'px';
,done: false
self.zoom.style.display = '';
};
// Now that we have offsetWidth, correct the position.
ImageAnnotator.script_callbacks[idx].script =
if ( self.is_rtl ) {
IA.getScript(
x = mouse_pos.x - 10 - self.zoom.offsetWidth;
template.replace('script_callbacks[].callback', 'script_callbacks[' + idx + '].callback')
if ( x < 0 ) { x = 0; }
.replace('&text=&', '&text=' + text + '&')
} else {
, true // No local caching!
var off = LAPI.Pos.scrollOffset();
);
var view = LAPI.Pos.viewport();
if ( ImageAnnotator.script_callbacks && ImageAnnotator.script_callbacks[idx]
if ( x + self.zoom.offsetWidth > off.x + view.x ) { x = off.x + view.x - self.zoom.offsetWidth; }
&& ImageAnnotator.script_callbacks[idx].done && ImageAnnotator.script_callbacks[idx].script)
if ( x < 0 ) { x = 0; {}
}
LAPI.DOM.removeNode(ImageAnnotator.script_callbacks[idx].script);
self.zoom.style.left = x + 'px';
ImageAnnotator.script_callbacks[idx].script = null;
},
}
}
, (LAPI.DOM.is_ie ? 1950 : 4000) - template.length // Some slack for caching parameters
);
}
 
hide_zoom: function ( evt ) {
if ((!window.XMLHttpRequest && !!window.ActiveXObject) || !self.haveAjax) {
if ( !IA.zoom ) { return; }
make_script_calls(get_local, mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php');
if ( evt } else) {
var mouse_pos = LAPI.Pos.mousePosition( evt );
make_calls(
if ( LAPI.Pos.isWithin( IA.cover, mouse_pos.x, mouse_pos.y ) ) { return; }
get_local
}
, function (text) {
IA.zoom.style.display = 'none';
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.
);
}
);
}
 
createHelpLink: function () {
// Can't use Ajax for foreign repo, might violate single-origin policy (e.g. from wikisource.org
var msg = ImageAnnotator.UI.get( 'wpImageAnnotatorHelp', false, true );
// to wikimedia.org). Attention, here we must care about the URL length! IE has a limit of 2083
if ( !msg || !msg.lastChild ) { return null; }
// character (2048 in the path part), and servers also may impose limits (on the WMF servers,
// Make sure we //use the limitright appearsprotocol tofor beall 8kB).images:
var imgs = msg.getElementsByTagName( 'img' );
make_script_calls (get_foreign, ImageAnnotator_config.sharedRepositoryAPI());
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 ) {
show_zoom: function () {
var selftarget = IAtgt;
return function ( evt ) {
if ( ( self.viewers[0].factors.dx < self.zoom_threshold
var e = evt || window.event;
&& self.viewers[0].factors.dy < self.zoom_threshold
___location.href = target;
)
if ( e ) { return LAPI.Evt.kill( e ); }
|| Math.max(self.viewers[0].factors.dx, self.viewers[0].factors.dy) < 2.0
return false;
)
};
{
}
// 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);
var zoom_src = null;
// 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 = '' + zoom_width;
zoomed.height = '' + 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
},
 
imgs = msg.getElementsByTagName( 'img' );
update_zoom: function (evt) {
if (!evt) return; // We need an event to calculate positions!
var self = IA;
if (!self.zoom) return;
var mouse_pos = LAPI.Pos.mousePosition(evt);
var origin = LAPI.Pos.position(self.cover);
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 = '' + top + 'px';
self.zoom.firstChild.firstChild.style.left = '' + 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';
},
 
if ( !imgs || !imgs.length ) {
hide_zoom: function (evt) {
// We're supposed to have a spans giving the button text
if (!IA.zoom) return;
text = msg.firstChild;
if (evt) {
if ( text.nodeName.toLowerCase() === 'span' ) { text = LAPI.DOM.getInnerText( text ); } else { text = 'Help'; }
var mouse_pos = LAPI.Pos.mousePosition(evt);
return LAPI.DOM.makeButton(
if (LAPI.Pos.isWithin(IA.cover, mouse_pos.x, mouse_pos.y)) return;
'ImageAnnotationHelpButton',
}
text,
IA.zoom.style.display = 'none';
make_handler( tgt )
},
);
} else {
return Buttons.makeButton( imgs, 'ImageAnnotationHelpButton', make_handler( tgt ) );
}
},
 
createHelpLink get_cover: function () {
var self = IA;
var msg = ImageAnnotator.UI.get('wpImageAnnotatorHelp', false, true);
var shim;
if (!msg || !msg.lastChild) return null;
if ( !self.cover ) {
// Make sure we use the right protocol for all images:
var pos = {
var imgs = msg.getElementsByTagName('img');
position: 'absolute',
var text;
left: '0px',
var tgt;
top: '0px',
if (imgs) {
width: self.viewers[ 0 ].thumb.width + 'px',
for (var i = 0; i < imgs.length; i++) {
height: self.viewers[ 0 ].thumb.height + 'px'
var srcFixed = imgs[i].getAttribute('src', 2).replace(/^https?\:/, document.___location.protocol);
};
imgs[i].src = srcFixed;
self.cover = LAPI.make( 'div', null, pos );
}
self.border = self.cover.cloneNode( false );
}
Object.merge(
if ( msg.childNodes.length == 1
{ border: '3px solid green', top: '-3px', left: '-3px' },
&& msg.firstChild.nodeName.toLowerCase() == 'a'
self.border.style
&& !LAPI.DOM.hasClass(msg.firstChild, 'image')
);
) {
self.cover.style.zIndex = 2000; // Above the tooltips
msg.firstChild.id = 'ImageAnnotationHelpButton';
if ( LAPI.Browser.is_ie ) {
return msg.firstChild; // Single link
shim = LAPI.make( 'iframe', { frameBorder: 0, tabIndex: -1 }, pos );
}
shim.style.filter = 'alpha(Opacity=0)'; // Ensure transparency
// Otherwise, it's either a sequence of up to three images, or a span, followed by a
// Unfortunately, IE6/SP2 has a "security setting" called "Binary and script
// link.
// behaviors". If that is disabled, filters don't work, and our iframe would
tgt = msg.lastChild;
// appear as a white rectangle. Fix this by first placing the iframe just above
if (tgt.nodeName.toLowerCase() != 'a')
// image (to block that windowed control) and then placing *another div* just
tgt = mw.config.get('wgServer') + mw.config.get('wgArticlePath').replace('$1', 'Help:Gadget-ImageAnnotator');
// above that shim having the image as its background image.
else
var imgZ = self.viewers[ 0 ].img.style.zIndex;
tgt = tgt.href;
if ( isNaN( imgZ ) ) { imgZ = 10; } // Arbitrary, positive, > 1, < 500
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 make_handler (tgt) {
var targetself = tgtIA;
if ( self.cover && !self.cover_visible ) {
return function (evt) {
if ( self.ieFix ) {
var e = evt || window.event;
self.viewers[ 0 ].img_div.appendChild( self.ieFix );
___location.href = target;
self.viewers[ 0 ].img_div.appendChild( self.ieFix2 );
if (e) return LAPI.Evt.kill(e);
}
return false;
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 () {
imgs = msg.getElementsByTagName('img');
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 ) {
if (!imgs || !imgs.length) {
var node = null;
// We're supposed to have a spans giving the button text
if ( !scope || scope == document ) {
text = msg.firstChild;
node = LAPI.$( 'image_annotation_' + what );
if (text.nodeName.toLowerCase() === 'span')
} else {
text = LAPI.DOM.getInnerText(text);
node = getElementsByClassName( scope, '*', 'image_annotation_' + what );
else
if ( node && node.length ) { node = node[ 0 ]; } else { node = null; }
text = 'Help';
}
return LAPI.DOM.makeButton(
return node;
'ImageAnnotationHelpButton'
},
, text
, make_handler(tgt)
);
} else {
return Buttons.makeButton(imgs, 'ImageAnnotationHelpButton', make_handler(tgt));
}
},
 
get_cover getItem: function ( what, scope ) {
var node = IA.getRawItem( what, scope );
var self = IA;
if ( !node ) { return null; }
var shim;
return LAPI.DOM.getInnerText( node ).trim();
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)) imgZ = 10; // Arbitrary, positive, > 1, < 500
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 getIntItem: function ( what, scope ) {
var x = IA.getItem( what, scope );
var self = IA;
if ( x !== null ) { x = parseInt( x, 10 ); }
if (self.cover && !self.cover_visible) {
return x;
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 findNote: function ( text, id ) {
function find( text, id, delim ) {
var self = IA;
var start = delim.start.replace( '$1', id );
if (self.cover && self.cover_visible) {
var start_match = text.indexOf( start );
if (self.ieFix) {
if ( start_match < 0 ) { return null; }
LAPI.DOM.removeNode(self.ieFix);
var end = delim.end.replace( '$1', id );
LAPI.DOM.removeNode(self.ieFix2);
var end_match = text.indexOf( end );
}
if ( end_match < start_match + start.length ) { return null; }
if (self.eventFix) LAPI.DOM.removeNode(self.eventFix);
return { start: start_match, end: end_match + end.length };
LAPI.DOM.removeNode(self.cover);
}
self.cover_visible = false;
}
},
 
var result = null;
getRawItem: function (what, scope) {
for ( var i = 0; i < IA.note_delim.length && !result; i++ ) {
var node = null;
result = find( text, id, IA.note_delim[ i ] );
if (!scope || scope == document) {
}
node = LAPI.$ ('image_annotation_' + what);
return result;
} else {
},
node = getElementsByClassName (scope, '*', 'image_annotation_' + what);
if (node && node.length) node = node[0]; else node = null;
}
return node;
},
 
getItem setWikitext: function (what, scopepagetext ) {
var nodeself = IA.getRawItem(what, scope);
if ( self.wiki_read ) if (!node){ return; null;}
Array.forEach( self.viewers[ 0 ].annotations, function ( note ) {
return LAPI.DOM.getInnerText(node).trim();
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;
},
 
getIntItem setSummary: function (what summary, scopeinitial_text, note_text ) {
if ( initial_text.contains( '$1' ) ) {
var x = IA.getItem(what, scope);
var max = ( summary.maxlength || 200 ) - initial_text.length;
if (x !== null) x = parseInt (x, 10);
if ( note_text ) {
return x;
initial_text =
},
initial_text.replace( '$1', ': ' + note_text.replace( '\n', ' ' ).substring( 0, max ) );
} else { initial_text = initial_text.replace( '$1', '' ); }
}
summary.value = initial_text;
},
 
findNote getScript: function (text url, idbypass_local_cache, bypass_caches ) {
// Don't use LAPI here, it may not yet be available
function find (text, id, delim) {
if ( bypass_caches ) {
var start = delim.start.replace('$1', id);
url += ( ( url.indexOf( '?' ) >= 0 ) ? '&' : '?' ) + 'dummyTimestamp=' + ( new Date() ).getTime();
var start_match = text.indexOf(start);
}
if (start_match < 0) return null;
// Avoid protocol-relative URIs (IE7 bug)
var end = delim.end.replace('$1', id);
if ( url.length >= 2 && url.substring( 0, 2 ) === '//' ) { url = document.___location.protocol + url; }
var end_match = text.indexOf(end);
if ( bypass_local_cache ) {
if (end_match < start_match + start.length) return null;
var s = document.createElement( 'script' );
return {start: start_match, end: end_match + end.length};
s.setAttribute( 'src', url );
}
s.setAttribute( 'type', 'text/javascript' );
document.getElementsByTagName( 'head' )[ 0 ].appendChild( s );
return s;
} else {
return importScriptURI( url );
}
},
 
canEdit: function () {
var result = null;
var self = IA;
for (var i=0; i < IA.note_delim.length && !result; i++) {
if ( self.may_edit ) {
result = find (text, id, IA.note_delim[i]);
if ( !self.ajaxQueried ) {
}
self.haveAjax = ( LAPI.Ajax.getRequest() != null );
return result;
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
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;
},
 
// Backwards compatibility
setSummary: function (summary, initial_text, note_text) {
function getElementsByClassName( scope, tag, className ) {
if (initial_text.contains('$1')) {
if ( window.jQuery ) {
var max = (summary.maxlength || 200) - initial_text.length;
return jQuery( scope ).find( ( ( !tag || tag === '*' ) ? '' : tag ) + '.' + className );
if (note_text)
} else {
initial_text =
// For non-WMF wikis that might not have jQuery (yet), use the wikibits.js getElementsByClassName
initial_text.replace('$1', ': ' + note_text.replace('\n', ' ').substring(0, max));
return getElementsByClassName( scope, tag, className );
else
}
initial_text = initial_text.replace('$1', '');
}
}
summary.value = initial_text;
},
 
window.ImageAnnotator = {
getScript: function (url, bypass_local_cache, bypass_caches) {
install: function ( config ) { IA.install( config ); }
// 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 importScriptURI (url);
}
},
 
// Start it. Bypass caches; but allow for 4 hours client-side caching. Small file.
canEdit: function () {
IA.getScript(
var self = IA;
mw.config.get( 'wgScript' ) + '?title=MediaWiki:ImageAnnotatorConfig.js&action=raw&ctype=text/javascript' +
if (self.may_edit) {
'&dummy=' + Math.floor( ( new Date() ).getTime() / ( 14400 * 1000 ) ), // 4 hours
if (!self.ajaxQueried) {
true // No local caching!
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 IAlocal scope
 
// 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)