MediaWiki:Gadget-ImageAnnotator.js: Difference between revisions

Content deleted Content added
Per request
 
(17 intermediate revisions by 11 users not shown)
Line 1:
// <source lang="javascript">
 
/*
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
Line 26 ⟶ 20:
*/
 
//* Global:global importScript, importScriptURI (wiki.js),
LAPI, Tooltip, Tooltips, TextCleaner, UIElements, Buttons,
// Global: wgPageName, wgCurRevisionId, wgUserGroups, wgRestrictionEdit (inline script on the page)
ImageAnnotator, ImageAnnotator_disable */
// Global: wgAction, wgNamespaceNumber, wgUserLanguage, wgContentLanguage, stylepath (inline script)
/* eslint-disable one-var, vars-on-top, camelcase, no-use-before-define,
// Global: wgNamespaceIds (inline script)
eqeqeq, no-alert, no-loop-func, no-inner-declarations */
/*jshint eqnull:true, laxbreak:true, laxcomma:true */
 
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 var( result !== b.area(0 ) -{ a.area()return result; }
// Just to make sure the order is complete
if (result !== 0) return result;
return a.model.id - b.model.id; // Just to make sure the order is complete
};
 
ImageAnnotation.prototype = {
// Rectangle to be displayed on image: a div with pos and size
{
view: null,
view : null, // Rectangle to be displayed on image: a div with pos and size
model : null, // Internal representation of the annotation
tooltip model: null,
// Tooltip to display the annotation
content tooltip: null,
// Content of the tooltip
viewer 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;
var is_new = false;
this.viewer = viewer;
var view_w = 0, view_h = 0, view_x = 0, view_y = 0;
if ( LAPI.DOM.hasClass( node, IA.annotation_class ) ) {
this.viewer = viewer;
// Extract the info we need
if (LAPI.DOM.hasClass(node, IA.annotation_class)) {
var x = IA.getIntItem( 'view_x_' + id, viewer.scope );
// Extract the info we need
var xy = IA.getIntItem( 'view_x_view_y_' + id, viewer.scope );
var yw = IA.getIntItem( 'view_y_view_w_' + id, viewer.scope );
var wh = IA.getIntItem( 'view_w_view_h_' + id, viewer.scope );
var hhtml = IA.getIntItemgetRawItem( 'view_h_content_' + id, viewer.scope );
if ( x === null || y === null || w === null || h === null || html === null ) { throw new Error( 'Invalid note' ); }
var html = IA.getRawItem('content_' + id, viewer.scope);
if ( x < 0 || x >= viewer.full_img.width || y < 0 || y >= viewer.full_img.height ) { throw new Error( 'Invalid note: origin invalid on note ' + id ); }
if (x === null || y === null || w === null || h === null || html === null)
if (
throw new Error ('Invalid note');
if ( x <+ 0 || xw >= viewer.full_img.width ||+ y < 010 || y >= viewer.full_img.height)
y + h > viewer.full_img.height + 10
throw new Error ('Invalid note: origin invalid on note ' + id);
) {
if ( x + w > viewer.full_img.width + 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);
{
this.view =
position: 'absolute',
LAPI.make(
display: 'none',
'div', null
lineHeight: '0px', // IE
, { position : 'absolute'
fontSize: '0px', // IE
,display : 'none'
top: String( view_y ) + 'px',
,lineHeight : '0px' // IE
left: String( view_x ) + 'px',
,fontSize : '0px' // IE
width: String( view_w ) + 'px',
,top : '' + view_y + 'px'
height: String( view_h ) + 'px'
,left : '' + view_x + 'px'
}
,width : '' + view_w + 'px'
);
,height : '' + view_h + 'px'
// We'll add the view to the DOM once we've loaded all notes
}
this.model = {
);
id: id,
// We'll add the view to the DOM once we've loaded all notes
dimension: { x: x, y: y, w: w, h: h },
this.model =
wiki: '',
{ id : id
html: html.cloneNode( true )
,dimension: {x: x, y: y, w: w, h: h}
};
,wiki : ''
} else {
,html : html.cloneNode(true)
is_new = true;
};
this.view = node;
} else {
this.model = {
is_new = true;
id: -1,
this.view = node;
dimension: null,
this.model =
wiki: '',
{ id : -1
,dimension html: null
};
,wiki : ''
view_w = this.view.offsetWidth - 2; // Subtract cumulated border widths
,html : null
view_h = this.view.offsetHeight - 2;
};
view_x = this.view.offsetLeft;
view_w = this.view.offsetWidth - 2; // Subtract cumulated border widths
view_h view_y = this.view.offsetHeight - 2offsetTop;
}
view_x = this.view.offsetLeft;
// Enforce a minimum size of the view. Center the 6x6px square over the center of the old view.
view_y = this.view.offsetTop;
// 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; }
// Enforce a minimum size of the view. Center the 6x6px square over the center of the old view.
if ( view_h < 6 ) { view_y = Math.floor( view_y + view_h / 2 - 3 ); view_h = 6; }
// If we overlap the image boundary, adjustRectangleSize will take care of it later.
Object.merge(
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; }
left: String( view_x ) + 'px',
Object.merge(
top: String( view_y ) + 'px',
{ left: '' + view_x + 'px', top: '' + view_y + 'px'
, width: '' +String( view_w ) + 'px', height: '' + view_h + 'px'}
height: String( view_h ) + 'px'
, this.view.style
},
);
this.view.style.zIndex = 500; // Below tooltips
);
try {
this.view.style.borderzIndex = '1px500; solid// 'Below + this.viewer.outer_border;tooltips
try {
} catch (ex) {
this.view.style.border = '1px solid ' + IAthis.viewer.outer_border;
} catch ( ex ) {
}
this.view.style.border = '1px solid ' + IA.outer_border;
this.view.appendChild(
}
LAPI.make(
this.view.appendChild(
'div', null
LAPI.make(
, { lineHeight : '0px' // IE
'div', null
,fontSize : '0px' // IE
, { lineHeight: '0px', // IE
,width : '' + Math.max(view_w - 2, 0) + 'px' // -2 to leave space for the border
fontSize: '0px', // IE
,height : '' + Math.max(view_h - 2, 0) + 'px'
width: String( Math.max( view_w - 2, 0 ) ) + 'px', // -2 to leave space for the border
}
height: String( Math.max( view_h - 2, 0 ) ) + 'px'
)
}
// width=100% doesn't work right: inner div's border appears outside on right and bottom on FF.
)
);
// width=100% doesn't work right: inner div's border appears outside on right and bottom on FF.
try {
);
this.view.firstChild.style.border = '1px solid ' + this.viewer.inner_border;
try {
} catch (ex) {
this.view.firstChild.style.border = '1px solid ' + IAthis.viewer.inner_border;
} catch ( ex ) {
}
this.view.firstChild.style.border = '1px solid ' + IA.inner_border;
if (is_new) viewer.adjustRectangleSize(this.view);
}
// IE somehow passes through event to the view even if covered by our cover, displaying the tooltips
if ( is_new ) { viewer.adjustRectangleSize( this.view ); }
// when drawing a new rectangle, which is confusing and produces a selection nightmare. Hence we just
// IE somehow passes through event to the view even if covered by our cover, displaying the tooltips
// display raw rectangles without any tooltips attached while drawing. Yuck.
// when drawing a new rectangle, which is confusing and produces a selection nightmare. Hence we just
this.dummy = this.view.cloneNode(true);
// display raw rectangles without any tooltips attached while drawing. Yuck.
viewer.img_div.appendChild(this.dummy);
this.dummy = this.view.cloneNode( true );
if (!is_new) {
viewer.img_div.appendChild( this.dummy );
// New notes get their tooltip only once the editor has saved, otherwise IE may try to
if ( !is_new ) {
// open them if the mouse moves onto the view even though there is the cover above them!
// New notes get their tooltip only once the editor has saved, otherwise IE may try to
this.setTooltip();
// 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
if (this.tooltip || !this.view) return; // Already set, or corrupt
// targets and we have to show and hide them ourselves through a mousemove listener in the viewer
// Note: on IE, don't have tooltips appear automatically. IE doesn't do it right for transparent
// anyway. The occasional event that IE sends to the tooltip may then lead to ugly flickering.
// targets and we have to show and hide them ourselves through a mousemove listener in the viewer
this.tooltip = new Tooltip(
// anyway. The occasional event that IE sends to the tooltip may then lead to ugly flickering.
this.view.firstChild,
this.tooltip = new Tooltip
this.display.bind( this ),
( this.view.firstChild
{
, this.display.bind(this)
, { activate : ( LAPI.DOM.is_ie ? Tooltip.NONE : Tooltip.HOVER ),
, deactivate : ( LAPI.DOM.is_ie ? Tooltip.ESCAPE : Tooltip.LEAVE ),
, close_button : null,
, mode : Tooltip.MOUSE,
, mouse_offset : { x: -5, y: -5, dx: ( IA.is_rtl ? -1 : 1 ), dy: 1 },
, open_delay : 0,
, hide_delay : 0,
, onclose : ( function ( tooltip, evt ) {
if ( this.view ) {
try {
try {
this.view.style.border = '1px solid ' + this.viewer.outer_border;
} catch ( ex ) {
this.view.style.border = '1px solid ' + IA.outer_border;
}
}
}
}
if ( this.viewer.tip == tooltip ) { this.viewer.tip = null; }
// Hide all boxes if we're outside the image. Relies on hide checking the
// coordinates! (Otherwise, we'd always hide...)
if ( evt ) { this.viewer.hide( evt ); }
} }).bind( this ),
, onopen : ( function ( tooltip ) {
if ( this.view ) {
try {
try {
this.view.style.border = '1px solid ' + this.viewer.active_border;
} catch ( ex ) {
this.view.style.border = '1px solid ' + IA.active_border;
}
}
}
}
this.viewer.tip = tooltip;
} }).bind( this )
},
}
, IA.tooltip_styles
);
},
 
display : function ( evt ) {
if ( !this.content ) {
{
this.content = LAPI.make( 'div' );
if (!this.content) {
var this.contentmain = LAPI.make( 'div' );
this.content.appendChild( main );
var main = LAPI.make('div');
this.content.appendChild(main) = main;
if ( this.model.html ) { main.appendChild( this.model.html.cloneNode( true ) ); }
this.content.main = main;
// Make sure that the popup encompasses all floats
if (this.model.html) main.appendChild(this.model.html.cloneNode(true));
this.content.appendChild( LAPI.make( 'div', null, { clear: 'both' } ) );
// Make sure that the popup encompasses all floats
if ( this.viewer.may_edit ) {
this.content.appendChild(LAPI.make('div', null, { clear: 'both' }));
this.content.button_section = LAPI.make(
if (this.viewer.may_edit) {
'div',
this.content.button_section =
null,
LAPI.make(
{
'div'
fontSize: 'smaller',
,null
textAlign: ( IA.is_rtl ? 'left' : 'right' ),
,{ fontSize : 'smaller'
borderTop: IA.tooltip_styles.border
,textAlign: (IA.is_rtl ? 'left' : 'right')
}
,borderTop: IA.tooltip_styles.border
);
}
this.content.appendChild( this.content.button_section );
);
this.content.button_section.appendChild(this LAPI.contentDOM.button_section);makeLink(
'#',
this.content.button_section.appendChild(LAPI.DOM.makeLink(
ImageAnnotator.UI.get( 'wpImageAnnotatorEdit', true ),
'#'
null,
, ImageAnnotator.UI.get('wpImageAnnotatorEdit', true)
LAPI.Evt.makeListener( this, this.edit )
, 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(
if (ImageAnnotator_config.mayDelete()) {
'#',
this.content.button_section.appendChild(document.createTextNode('\xa0'));
ImageAnnotator.UI.get( 'wpImageAnnotatorDelete', true ),
this.content.button_section.appendChild
null,
(LAPI.DOM.makeLink
LAPI.Evt.makeListener( this, this.remove_event )
( '#'
) );
, ImageAnnotator.UI.get('wpImageAnnotatorDelete', true)
}
, null
}
, LAPI.Evt.makeListener(this, this.remove_event)
}
)
return this.content;
);
},
}
}
}
return this.content;
},
 
edit : function ( evt ) {
if ( IA.canEdit() ) { IA.editor.editNote( this ); }
{
if ( evt ) { return LAPI.Evt.kill( evt ); }
if (IA.canEdit()) IA.editor.editNote(this);
if (evt) return LAPI.Evt.kill(evt)false;
},
return false;
},
 
remove_event : function ( evt ) {
if ( IA.canEdit() ) { this.remove(); }
{
return LAPI.Evt.kill( evt );
if (IA.canEdit()) this.remove();
},
return LAPI.Evt.kill(evt);
},
 
remove : function () {
if ( !this.content ) { // New note: just destroy it.
{
this.destroy();
if (!this.content) { // New note: just destroy it.
return true;
this.destroy();
}
return true;
if ( !ImageAnnotator_config.mayDelete() ) { return false; }
}
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
wgPageName
// breaks when we remove the note.
, function (doc, editForm, failureFunc, revision_id)
{
try {
if (revision_id && revision_id != 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;
}
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.model || !this.model.dimension ) { return 0; }
{
if return ( this.view) LAPImodel.DOMdimension.removeNode(w * this.viewmodel.dimension.h );
},
if (this.dummy) LAPI.DOM.removeNode(this.dummy);
if (this.tooltip) this.tooltip.hide_now();
if (this.model && this.model.id > 0 && this.viewer) this.viewer.deregister(this);
this.model = null;
this.view = null;
this.content = null;
this.tooltip = null;
this.viewer = null;
},
 
area cannotEdit: function () {
if ( this.content && this.content.button_section ) {
{
LAPI.DOM.removeNode( this.content.button_section );
if (!this.model || !this.model.dimension) return 0;
this.content.button_section = null;
return (this.model.dimension.w * this.model.dimension.h);
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 ( evt ) {
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';
{
IA.show_cover();
var same_note = (note == this.note);
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( LAPI.Edit.SAVE + LAPI.Edit.PREVIEW + LAPI.Edit.REVERT + LAPI.Edit.CANCEL );
, { prop : 'revisions'
this.editor.textarea.readOnly = false;
,titles : wgPageName
this.editor.textarea.style.backgroundColor = 'white';
,rvlimit : 1
// Set the position relative to the note's view.
,rvstartid : wgCurRevisionId
var view_pos = LAPI.Pos.position( this.note.view );
,rvprop : 'ids|content'
var origin = LAPI.Pos.position( cover );
}
this.tooltip.options.fixed_offset.x =
, function (request, json_result) {
view_pos.x - origin.x + this.tooltip.options.mouse_offset.x;
if (json_result && json_result.query && json_result.query.pages) {
this.tooltip.options.fixed_offset.y =
// Should have only one page here
view_pos.y - origin.y + this.tooltip.options.mouse_offset.y;
for (var page in json_result.query.pages) {
this.tooltip.options.fixed_offset.dx = 1;
var p = json_result.query.pages[page];
this.tooltip.options.fixed_offset.dy = 1;
if (p && p.revisions && p.revisions.length) {
// Make sure mouse event listeners are removed, especially on IE.
var rev = p.revisions[0];
this.dim = { x: this.note.view.offsetLeft, y: this.note.view.offsetTop,
if (rev.revid == wgCurRevisionId && rev["*"] && rev["*"].length)
w: this.note.view.offsetWidth, h: this.note.view.offsetHeight };
IA.setWikitext(rev["*"]);
this.viewer.setShowHideEvents( false );
}
this.viewer.hide(); // Make sure notes are hidden
break;
this.viewer.toggle( true ); // Show all note rectangles (but only the dummies)
}
// Now show the editor
}
this.tooltip.show_tip( null, false );
// TODO: What upon a failure?
var tpos = LAPI.Pos.position( this.editor.textarea );
self.open_editor(same_note, cover);
var ppos = LAPI.Pos.position( this.tooltip.popup );
}
tpos = tpos.x - ppos.x;
, function (request) {
if ( tpos + this.editor.textarea.offsetWidth > this.tooltip.popup.offsetWidth ) { this.editor.textarea.style.width = ( this.tooltip.popup.offsetWidth - 2 * tpos ) + 'px'; }
// TODO: What upon a failure?
if ( LAPI.Browser.is_ie ) {
self.open_editor(same_note, cover);
// Fixate textarea width to prevent ugly flicker on each keypress in IE6...
}
this.editor.textarea.style.width = this.editor.textarea.offsetWidth + 'px';
);
}
} else {
this.visible = true;
this.open_editor(same_note, cover);
},
}
},
 
open_editor hide_editor: function (same_note, coverevt ) {
if ( !this.visible ) { return; }
{
this.visible = false;
this.editor.hidePreview();
IA.is_editing = false;
if (!same_note || this.editor.textarea.readOnly)
this.tooltip.hide_now( evt );
// Different note, or save error last time
if ( evt && evt.type == 'keydown' && !this.saving ) {
this.editor.setText(this.note.model.wiki);
// ESC pressed on new note before a save attempt
this.editor.enable
this.cancel();
(LAPI.Edit.SAVE + LAPI.Edit.PREVIEW + LAPI.Edit.REVERT + LAPI.Edit.CANCEL);
}
this.editor.textarea.readOnly = false;
IA.hide_cover();
this.editor.textarea.style.backgroundColor = 'white';
this.viewer.setDefaultMsg();
// Set the position relative to the note's view.
this.viewer.setShowHideEvents( true );
var view_pos = LAPI.Pos.position(this.note.view);
this.viewer.hide();
var origin = LAPI.Pos.position(cover);
this.viewer.show(); // Make sure we get the real views again.
this.tooltip.options.fixed_offset.x =
// FIXME in Version 2.1: Unfortunately, we don't have a mouse position here, so sometimes we
view_pos.x - origin.x + this.tooltip.options.mouse_offset.x;
// may show the note rectangles even though the mouse is now outside the image. (It was
this.tooltip.options.fixed_offset.y =
// somewhere inside the editor in most cases (if an editor button was clicked), but if ESC was
view_pos.y - origin.y + this.tooltip.options.mouse_offset.y;
// pressed, it may actually be anywhere.)
this.tooltip.options.fixed_offset.dx = 1;
},
this.tooltip.options.fixed_offset.dy = 1;
// Make sure mouse event listeners are removed, especially on IE.
this.dim = { x : this.note.view.offsetLeft, y : this.note.view.offsetTop
,w : this.note.view.offsetWidth, h : this.note.view.offsetHeight };
this.viewer.setShowHideEvents(false);
this.viewer.hide(); // Make sure notes are hidden
this.viewer.toggle(true); // Show all note rectangles (but only the dummies)
// Now show the editor
this.tooltip.show_tip(null, false);
var tpos = LAPI.Pos.position(this.editor.textarea);
var ppos = LAPI.Pos.position(this.tooltip.popup);
tpos = tpos.x - ppos.x;
if (tpos + this.editor.textarea.offsetWidth > this.tooltip.popup.offsetWidth)
this.editor.textarea.style.width = (this.tooltip.popup.offsetWidth - 2 * tpos) + 'px';
if (LAPI.Browser.is_ie) {
// Fixate textarea width to prevent ugly flicker on each keypress in IE6...
this.editor.textarea.style.width = this.editor.textarea.offsetWidth + 'px';
}
this.visible = true;
},
 
hide_editor save: function (evt editor ) {
var data = editor.getText();
{
if ( !data || !data.length ) {
if (!this.visible) return;
// Empty text
this.visible = false;
if ( this.note.remove() ) {
IA.is_editing = false;
this.tooltip.hide_nowhide_editor(evt);
this.cancel();
if (evt && evt.type == 'keydown' && !this.saving) {
this.note = null;
// ESC pressed on new note before a save attempt
} else {
this.cancel();
this.hide_editor();
}
this.cancel();
IA.hide_cover();
}
this.viewer.setDefaultMsg();
return;
this.viewer.setShowHideEvents(true);
} else if ( data == this.viewernote.hide(model.wiki ); {
// Text unchanged
this.viewer.show(); // Make sure we get the real views again.
this.hide_editor();
// FIXME in Version 2.1: Unfortunately, we don't have a mouse position here, so sometimes we
this.cancel();
// may show the note rectangles even though the mouse is now outside the image. (It was
return;
// somewhere inside the editor in most cases (if an editor button was clicked), but if ESC was
}
// 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 textbox = editForm.wpTextbox1;
{
if ( !textbox ) { throw new Error( '#Server replied with invalid edit page.' ); }
var data = editor.getText();
var pagetext = textbox.value;
if (!data || !data.length) {
// Empty text
if (this.note.remove()) {
this.hide_editor();
this.cancel();
this.note = null;
} else {
this.hide_editor();
this.cancel();
}
return;
} else if (data == this.note.model.wiki) {
// Text unchanged
this.hide_editor();
this.cancel();
return;
}
// Construct what to insert
var dim = Object.clone(this.note.model.dimension);
if (!dim) {
dim = {
x : Math.round(this.dim.x * this.viewer.factors.dx),
y : Math.round(this.dim.y * this.viewer.factors.dy),
w : Math.round(this.dim.w * this.viewer.factors.dx),
h : Math.round(this.dim.h * this.viewer.factors.dy)
};
// Make sure everything is within bounds
if (dim.x + dim.w > this.viewer.full_img.width) {
if (dim.w > this.dim.w * this.viewer.factors.dx) {
dim.w--;
if (dim.x + dim.w > this.viewer.full_img.width) {
if (dim.x > 0) dim.x--; else dim.w = this.viewer.full_img.width;
}
} else {
// Width already was rounded down
if (dim.x > 0) dim.x--;
}
}
if (dim.y + dim.h > this.viewer.full_img.height) {
if (dim.h > this.dim.h * this.viewer.factors.dy) {
dim.h--;
if (dim.y + dim.h > this.viewer.full_img.height) {
if (dim.y > 0) dim.y--; else dim.h = this.viewer.full_img.height;
}
} else {
// Height already was rounded down
if (dim.y > 0) dim.y--;
}
}
// If still too large, adjust width and height
if (dim.x + dim.w > this.viewer.full_img.width) {
if (this.viewer.full_img.width > dim.x) {
dim.w = this.viewer.full_img.width - dim.x;
} else {
dim.x = this.viewer.full_img.width - 1;
dim.w = 1;
}
}
if (dim.y + dim.h > this.viewer.full_img.height) {
if (this.viewer.full_img.height > dim.y) {
dim.h = this.viewer.full_img.height - dim.y;
} else {
dim.y = this.viewer.full_img.height - 1;
dim.h = 1;
}
}
}
this.to_insert =
'{{ImageNote'
+ '|id=' + this.note.model.id
+ '|x=' + dim.x + '|y=' + dim.y + '|w=' + dim.w + '|h=' + dim.h
+ '|dimx=' + this.viewer.full_img.width
+ '|dimy=' + this.viewer.full_img.height
+ '|style=2'
+ '}}\n'
+ data + (data.endsWith('\n') ? '' : '\n')
+ '{{ImageNoteEnd|id=' + this.note.model.id + '}}';
// Now edit the page
var self = this;
this.editor.busy(true);
this.editor.enable(0); // Disable all buttons
this.saving = true;
LAPI.Ajax.editPage(
wgPageName
, function (doc, editForm, failureFunc, revision_id)
{
try {
if (revision_id && revision_id != 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;
}
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 = wgServer + wgArticlePath.replace('$1', encodeURIComponent(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.note ) { return; }
{
if (this.tooltip) !this.tooltipnote.size_change(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.notecancel() return;
}
if (!this.note.content) {
// No content: Cancel and remove this note!
this.note.destroy();
this.note = null;
}
if (editor) this.hide_editor();
},
 
};
close_tooltip : function (tooltip, evt)
{
this.hide_editor(evt);
this.cancel();
}
 
var ImageNotesViewer = function () { this.initialize.apply( this, arguments ); };
};
 
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();
{
} else {
initialize : function (descriptor, may_edit)
// Normalize the namespace of the realName to 'File' to account for images possibly stored at
{
// a foreign repository (the Commons). Otherwise a later information load might fail because
Object.merge(descriptor, this);
// the link is local and might actually be given as "Bild:Foo.jpg". If that page doesn't exist
this.annotations = [];
// locally, we want to ask at the Commons about "File:Foo.jpg". The Commons doesn't understand
this.max_id = 0;
// the localized namespace names of other wikis, but the canonical namespace name 'File' works
this.main_div = null;
// also locally.
this.msg = null;
this.realName = 'File:' + this.realName.substring( this.realName.indexOf( ':' ) + 1 );
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
};
 
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.icon ) { return; }
{
if ( set ) {
if (!this.may_edit) return;
LAPI.Evt.attach( this.img, IA.mouse_in, this.show_evt );
this.may_edit = false;
if ( this.hide_evt ) { LAPI.Evt.attach( this.img, IA.mouse_out, this.hide_evt ); }
Array.forEach(this.annotations, function (note) { note.cannotEdit(); });
} 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 (this.icon) return;
if ( this.move_evt ) {
if (set) {
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( null ); }
LAPI.Evt.attach(this.img, IA.mouse_in, this.show_evt);
if (this.hide_evt) LAPI.Evt.attachremove(this.img document, IA.mouse_out'mousemove', this.hide_evtmove_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
if (this.icon) return;
// loop otherwise if the mouse was just on that overlapped boundary, resulting in flickering.
this.move_listening = false;
var view_x = node.offsetLeft;
if (this.move_evt) {
var view_y = node.offsetTop;
if (!LAPI.Browser.is_ie && typeof document.captureEvents === 'function')
var view_w = node.offsetWidth;
document.captureEvents(null);
var view_h = node.offsetHeight;
LAPI.Evt.remove(document, 'mousemove', this.move_evt, true);
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.annotations || ifthis.annotations.length === 0 || (this.icon ) { return; }
if ( dummies ) {
// Make sure the note boxes don't overlap the image boundary; we might get an event
for ( i = 0; i < this.annotations.length; i++ ) {
// loop otherwise if the mouse was just on that overlapped boundary, resulting in flickering.
this.annotations[ i ].view.style.display = 'none';
var view_x = node.offsetLeft;
if ( this.visible && this.annotations[ i ].tooltip ) { this.annotations[ i ].tooltip.hide_now( null ); }
var view_y = node.offsetTop;
this.annotations[ i ].dummy.style.display = ( this.visible ? 'none' : '' );
var view_w = node.offsetWidth;
if ( !this.visible ) { this.adjustRectangleSize( this.annotations[ i ].dummy ); }
var view_h = node.offsetHeight;
}
if (view_x === 0) view_x = 1;
} else {
if (view_y === 0) view_y = 1;
for ( i = if0; (view_xi + view_w >=< this.thumbannotations.widthlength; i++ ) {
this.annotations[ i ].dummy.style.display = 'none';
view_w = this.thumb.width - view_x - 1;
this.annotations[ i ].view.style.display = ( this.visible ? 'none' : '' );
if (view_w <= 4) { view_w = 4; view_x = this.thumb.width - view_w - 1;}
if ( !this.visible ) { this.adjustRectangleSize( this.annotations[ i ].view ); }
}
if ( this.visible && this.annotations[ i ].tooltip ) { this.annotations[ i ].tooltip.hide_now( null ); }
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;}
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; }
{
this.toggle( IA.is_adding || IA.is_editing );
var i;
if (! this.annotationsmove_evt ||&& !this.annotations.lengthmove_listening === 0 || this.icon) return;{
LAPI.Evt.attach( document, 'mousemove', this.move_evt, true );
if (dummies) {
this.move_listening = true;
for (i = 0; i < this.annotations.length; i++) {
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( Event.MOUSEMOVE ); }
this.annotations[i].view.style.display = 'none';
}
if (this.visible && this.annotations[i].tooltip)
},
this.annotations[i].tooltip.hide_now(null);
this.annotations[i].dummy.style.display = (this.visible ? 'none' : '');
if (!this.visible) this.adjustRectangleSize(this.annotations[i].dummy);
}
} else {
for (i = 0; i < this.annotations.length; i++) {
this.annotations[i].dummy.style.display = 'none';
this.annotations[i].view.style.display = (this.visible ? 'none' : '');
if (!this.visible) this.adjustRectangleSize(this.annotations[i].view);
if (this.visible && this.annotations[i].tooltip)
this.annotations[i].tooltip.hide_now(null);
}
}
this.visible = !this.visible;
},
 
show hide: function ( evt ) {
if ( this.icon ) { return true; }
{
if ( !this.visible || this.icon) return;{
// Huh?
this.toggle(IA.is_adding || IA.is_editing);
if ( this.move_evtmove_listening &&) { !this.move_listeningremoveMoveListener(); {}
return true;
LAPI.Evt.attach(document, 'mousemove', this.move_evt, true);
}
this.move_listening = true;
if ( evt ) {
if (!LAPI.Browser.is_ie && typeof document.captureEvents === 'function')
var mouse_pos = LAPI.Pos.mousePosition( evt );
document.captureEvents(Event.MOUSEMOVE);
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 ( new_note.model.id > 0 ) {
if (this.icon) return true;
if ( new_note.model.id > this.max_id ) { this.max_id = new_note.model.id; }
if (this.visible)
} else {
this.hide(evt);
new_note.model.id = ++this.max_id;
return true;
}
},
},
 
register deregister: function (new_note note ) {
Array.remove( this.annotations, note );
{
if ( note.model.id == this.max_id ) { this.max_id--; }
this.annotations[this.annotations.length] = new_note;
if ( this.annotations.length === 0 ) { this.setDefaultMsg(); } // If we removed the last one, clear the msg
if (new_note.model.id > 0) {
},
if (new_note.model.id > this.max_id) this.max_id = new_note.model.id;
} else {
new_note.model.id = ++this.max_id;
}
},
 
deregister setDefaultMsg: function (note) {
if ( this.annotations && this.annotations.length && this.msg ) {
{
LAPI.DOM.removeChildren( this.msg );
Array.remove(this.annotations, note);
this.msg.appendChild( ImageAnnotator.UI.get( 'wpImageAnnotatorHasNotesMsg', false ) );
if (note.model.id == this.max_id) this.max_id--;
if ( this.realName && typeof this.realName === 'string' && this.realName.length ) {
if (this.annotations.length === 0) this.setDefaultMsg(); //If we removed the last one, clear the msg
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(
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
// Format of notes in Wikitext. Note: there are two formats, an old one and a new one.
border: '1px solid #8888aa',
// We only write the newest (last) one, but we can read also the older formats. Order is
backgroundColor: '#ffffe0',
// important, because the old format also used the ImageNote template, but for a different
padding: '0.3em',
// purpose.
fontSize: ( ( mw.config.get( 'skin' ) == 'monobook' || mw.config.get( 'skin' ) == 'modern' ) ? '127%' : '100%' )
note_delim :
// Scale up to default text size
[
},
{ 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}}'
}
],
 
editor: null,
tooltip_styles : // The style for all our tooltips
{ border : '1px solid #8888aa'
, backgroundColor : '#ffffe0'
, padding : '0.3em'
, fontSize : ((skin && (skin == 'monobook' || skin == 'modern')) ? '127%' : '100%')
// Scale up to default text size
},
 
wiki_read: false,
editor : null,
is_rtl: false,
 
wiki_read move_listening: false,
is_rtl is_tracking: false,
is_adding: false,
is_editing: false,
 
zoom_threshold: 8.0,
move_listening : false,
zoom_factor: 4.0,
is_tracking : false,
is_adding : false,
is_editing : false,
 
install_attempts: 0,
zoom_threshold : 8.0,
max_install_attempts: 10, // Maximum 5 seconds
zoom_factor : 4.0,
 
imgs_with_notes: [],
install_attempts : 0,
thumbs: [],
max_install_attempts : 10, // Maximum 5 seconds
other_images: [],
 
// Fallback
imgs_with_notes : [],
indication_icon: '//upload.wikimedia.org/wikipedia/commons/8/8a/Gtk-dialog-info-14px.png',
thumbs : [],
other_images : [],
 
install: function ( config ) {
// Fallback
if ( typeof ImageAnnotator_disable !== 'undefined' && !!ImageAnnotator_disable ) { return; }
indication_icon : '//upload.wikimedia.org/wikipedia/commons/8/8a/Gtk-dialog-info-14px.png',
if ( !config || ImageAnnotator_config != null ) { return; }
 
// Double check.
install : function (config)
if ( !config.viewingEnabled() ) { return; }
{
if (typeof ImageAnnotator_disable !== 'undefined' && !!ImageAnnotator_disable) return;
if (!config || ImageAnnotator_config != null) return;
 
var self = IA;
// Double check.
ImageAnnotator_config = config;
if (!config.viewingEnabled()) return;
 
// Determine whether we have XmlHttp. We try to determine this here to be able to avoid
var self = IA;
// doing too much work.
ImageAnnotator_config = config;
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.
// Determine whether we have XmlHttp. We try to determine this here to be able to avoid
self.may_edit = mw.config.get( 'wgNamespaceNumber' ) >= 0 && mw.config.get( 'wgArticleId' ) > 0 && self.haveAjax && config.editingEnabled();
// doing too much work.
if ( window.XMLHttpRequest
&& typeof LAPI !== 'undefined'
&& typeof LAPI.Ajax !== 'undefined'
&& typeof LAPI.Ajax.getRequest !== 'undefined'
)
{
self.haveAjax = (LAPI.Ajax.getRequest() != null);
self.ajaxQueried = true;
} else {
self.haveAjax = true; // A pity. May occur on IE. We'll check again later on.
self.ajaxQueried = false;
}
 
function namespaceCheck( list ) {
// We'll include self.haveAjax later on once more, to catch the !ajaxQueried case.
if ( !list || Object.prototype.toString.call( list ) !== '[object Array]' ) { return false; }
self.may_edit = wgNamespaceNumber >= 0 && wgArticleId > 0 && self.haveAjax && config.editingEnabled();
var namespaceIds = mw.config.get( 'wgNamespaceIds' );
if ( !namespaceIds ) { return false; }
var namespaceNumber = mw.config.get( 'wgNamespaceNumber' );
for ( var i = 0; i < list.length; i++ ) {
if (
typeof list[ i ] === 'string' &&
(
list[ i ] === '*' ||
namespaceIds[ list[ i ].toLowerCase().replace( / /g, '_' ) ] === namespaceNumber
)
) {
return true;
}
}
return false;
}
 
self.rules = { inline: {}, thumbs: {}, shared: {} };
function namespaceCheck (list)
{
if (!list || Object.prototype.toString.call(list) !== '[object Array]') return false;
for (var i = 0; i < list.length; i++) {
if (wgNamespaceIds
&& typeof list[i] === 'string'
&& (list[i] === '*'
|| wgNamespaceIds[list[i].toLowerCase().replace(/ /g, '_')] === wgNamespaceNumber
)
)
return true;
}
return false;
}
 
// Now set the default rules. Undefined means default setting (true for show, false for icon),
self.rules = { inline: {}, thumbs: {}, shared : {} };
// but overrideable by per-image rules. If set, it's not overrideable by per-image rules.
//
if (
!self.haveAjax ||
!config.generalImagesEnabled() ||
namespaceCheck( window.ImageAnnotator_no_images || null )
) {
self.rules.inline.show = false;
self.rules.thumbs.show = false;
self.rules.shared.show = false;
} else {
if (
!self.haveAjax ||
!config.thumbsEnabled() ||
namespaceCheck( window.ImageAnnotator_no_thumbs || null )
) {
self.rules.thumbs.show = false;
}
if ( mw.config.get( 'wgNamespaceNumber' ) == 6 ) {
self.rules.shared.show = true;
} else if ( !config.sharedImagesEnabled() ||
namespaceCheck( window.ImageAnnotator_no_shared || null )
) {
self.rules.shared.show = false;
}
if ( namespaceCheck( window.ImageAnnotator_icon_images || null ) ) { self.rules.inline.icon = true; }
if ( namespaceCheck( window.ImageAnnotator_icon_thumbs || null ) ) { self.rules.thumbs.icon = true; }
}
 
// User rule for displaying captions on images in articles
// Now set the default rules. Undefined means default setting (true for show, false for icon),
self.hideCaptions = namespaceCheck( window.ImageAnnotator_hide_captions || null );
// 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 (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;
}
 
var do_images = typeof self.rules.inline.show === 'undefined' || self.rules.inline.show;
// User rule for displaying captions on images in articles
self.hideCaptions = namespaceCheck(window.ImageAnnotator_hide_captions || null);
 
if ( do_images ) {
var do_images = typeof self.rules.inline.show === 'undefined' || self.rules.inline.show;
// Per-article switching off of note display on inline images and thumbnails
var rules = document.getElementById( 'wpImageAnnotatorImageRules' );
if ( rules ) {
if ( rules.className.indexOf( 'wpImageAnnotatorNone' ) >= 0 ) {
self.rules.inline.show = false;
self.rules.thumbs.show = false;
self.rules.shared.show = false;
}
if (
typeof self.rules.inline.show === 'undefined' &&
rules.className.indexOf( 'wpImageAnnotatorDisplay' ) >= 0
) {
self.rules.inline.show = true;
}
if ( rules.className.indexOf( 'wpImageAnnotatorNoThumbDisplay' ) >= 0 ) {
self.rules.thumbs.show = false;
}
if (
typeof self.rules.thumbs.show === 'undefined' &&
rules.className.indexOf( 'wpImageAnnotatorThumbDisplay' ) >= 0
) {
self.rules.thumbs.show = true;
}
if ( rules.className.indexOf( 'wpImageAnnotatorInlineDisplayIcons' ) >= 0 ) {
self.rules.inline.icon = true;
}
if ( rules.className.indexOf( 'wpImageAnnotatorThumbDisplayIcons' ) >= 0 ) {
self.rules.thumbs.icon = true;
}
if ( rules.className.indexOf( 'wpImageAnnotatorOnlyLocal' ) >= 0 ) {
self.rules.shared.show = false;
}
}
}
 
// Make sure the shared value is set
if (do_images) {
self.rules.shared.show = typeof self.rules.shared.show === 'undefined' || self.rules.shared.show;
// 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;
}
}
}
 
var do_thumbs = typeof self.rules.thumbs.show === 'undefined' || self.rules.thumbs.show;
// Make sure the shared value is set
self.rules.shared.show =
typeof self.rules.shared.show === 'undefined' || self.rules.shared.show;
 
if ( do_images ) {
do_images = typeof self.rules.inline.show === 'undefined' || self.rules.inline.show;
var bodyContent = document.getElementById( 'bodyContent' ) || // monobook, vector
var do_thumbs = typeof self.rules.thumbs.show === 'undefined' || self.rules.thumbs.show;
document.getElementById( 'mw_contentholder' ) || // modern
document.getElementById( 'article' ); // old skins
 
if ( bodyContent ) {
if (do_images) {
var all_imgs = bodyContent.getElementsByTagName( 'img' );
var bodyContent = document.getElementById('bodyContent') // monobook, vector
|| document.getElementById('mw_contentholder') // modern
|| document.getElementById('article') // old skins
;
if (bodyContent) {
var all_imgs = bodyContent.getElementsByTagName('img');
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 ( 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();
}
},
 
// This prevents traversing a page with more than 400 images
wait_for_required_libraries : function ()
// There are extreme cases like [[Emoji]] that high number of images can cause
{
// huge lag specially on Chrome
if (typeof Tooltip == 'undefined' || typeof LAPI == 'undefined') {
if ( all_imgs.length > 400 ) {
if (IA.install_attempts++ < IA.max_install_attempts) {
// purging the array, simply a hack to avoid more indention
setTimeout(IA.wait_for_required_libraries, 500); // 0.5 sec.
all_imgs = [];
}
}
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();
},
 
for ( var i = 0; i < all_imgs.length; i++ ) {
setup: function ()
// Exclude all that are in img_with_notes or in thumbs. Also exclude all in galleries.
{
var up = all_imgs[ i ].parentNode;
var self = IA;
if ( up.nodeName.toLowerCase() !== 'a' ) { continue; }
self.imgs = [];
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 () {
// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
if ( typeof Tooltip === 'undefined' || typeof LAPI === 'undefined' ) {
self.is_rtl =
if ( IA.install_attempts++ < IA.max_install_attempts ) {
LAPI.DOM.hasClass(document.body, 'rtl')
setTimeout( IA.wait_for_required_libraries, 500 ); // 0.5 sec.
|| ( LAPI.DOM.currentStyle // Paranoia: added recently, not everyone might have it
}
&& LAPI.DOM.currentStyle(document.body, 'direction') == 'rtl'
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 () {
var stylepath = mw.config.get('stylepath') || '/skin';
var self = IA;
self.imgs = [];
 
// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
// Use this to temporarily display an image off-screen to get its dimensions
self.is_rtl =
var testImgDiv =
LAPI.DOM.hasClass( document.body, 'rtl' ) ||
LAPI.make('div', null,
(
{ display: 'none', position: 'absolute', width: '300px'
LAPI.DOM.currentStyle && // Paranoia: added recently, not everyone might have it
, overflow: 'hidden', overflowX: 'hidden', left: '-10000px'
LAPI.DOM.currentStyle( document.body, 'direction' ) == 'rtl'
}
);
document.body.insertBefore(testImgDiv, document.body.firstChild);
 
var stylepath = mw.config.get( 'stylepath' ) || '/skin';
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};
}
 
// Use this to temporarily display an image off-screen to get its dimensions
function setup_one (scope) {
var file_divtestImgDiv = scope;LAPI.make(
'div', null,
var is_thumb =
{
scope != document
display: 'none', position: 'absolute', width: '300px',
&& scope.nodeName.toLowerCase() == 'div'
overflow: 'hidden', overflowX: 'hidden', left: '-10000px'
&& LAPI.DOM.hasClass(scope, 'thumbinner')
}
;
);
var is_other = scope.nodeName.toLowerCase() == 'img';
document.body.insertBefore( testImgDiv, document.body.firstChild );
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(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 = 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 img_check( img, is_other ) {
function setup_images (list)
var srcW = parseInt( img.getAttribute( 'width', 2 ), 10 );
{
var srcH = parseInt( img.getAttribute( 'height', 2 ), 10 );
Array.forEach(list,
// Don't do anything on extremely small previews. We need some minimum extent to be able to place
function (elem) {
// rectangles after all...
var desc = setup_one (elem);
if ( !srcW || !srcH || srcW < 20 || srcH < 20 ) { return null; }
if (desc) self.imgs[self.imgs.length] = desc;
// 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 ) {
if (wgNamespaceNumber == 6) {
var file_div = scope;
setup_images ([document]);
var is_thumb =
self.may_edit = self.may_edit && (self.imgs.length == 1);
scope != document &&
setup_images (self.imgs_with_notes);
scope.nodeName.toLowerCase() == 'div' &&
} else {
LAPI.DOM.hasClass( scope, 'thumbinner' );
setup_images (self.imgs_with_notes);
var is_other = scope.nodeName.toLowerCase() == 'img';
self.may_edit = self.may_edit && (self.imgs.length == 1);
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?|webp)$/i ) < 0 ) { return null; } // Only PNG, JPE?G, GIF, SVG, TIFF?, and WebP
// 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 ) {
self.may_edit = self.may_edit && ___location.href.search(/[?&]oldid=/) < 0;
Array.forEach( list,
function ( elem ) {
var desc = setup_one( elem );
if ( desc ) { self.imgs[ self.imgs.length ] = desc; }
}
);
}
 
if ( mw.config.get( 'wgNamespaceNumber' ) == 6 ) {
if (self.haveAjax) {
setup_images( [ document ] );
setup_images (self.thumbs);
self.may_edit = self.may_edit && ( self.imgs.length == 1 );
setup_images (self.other_images);
setup_images( self.imgs_with_notes );
}
} else {
setup_images( self.imgs_with_notes );
self.may_edit = self.may_edit && ( self.imgs.length == 1 );
}
 
self.may_edit = self.may_edit && ___location.href.search( /[?&]oldid=/ ) < 0;
// Remove the test div
LAPI.DOM.removeNode(testImgDiv);
 
if ( self.imgs.length ===haveAjax 0) return;{
setup_images( self.thumbs );
setup_images( self.other_images );
}
 
// Remove the test div
// We get the UI texts in parallel, but wait for them at the beginning of complete_setup, where we
LAPI.DOM.removeNode( testImgDiv );
// need them. This has in particular a benefit if we do have to query for the file sizes below.
 
if ( self.imgs.length === 1 && self.imgs[0].scope ==) document{ &&return; !self.haveAjax) {}
// Try to get the full size without Ajax.
self.imgs[0].full_img = LAPI.WP.fullImageSizeFromPage();
if (self.imgs[0].full_img.width > 0 && self.imgs[0].full_img.height > 0) {
self.setup_step_two();
return;
}
}
 
// We get the UI texts in parallel, but wait for them at the beginning of complete_setup, where we
// Get the full sizes of all the images. If more than 50, make several calls. (The API has limits.)
// need them. This has in particular a benefit if we do have to query for the file sizes below.
// Also avoid using Ajax on IE6...
 
if ( self.imgs.length == 1 && self.imgs[ 0 ].scope == document && !self.haveAjax ) {
var cache = {};
// Try to get the full size without Ajax.
var names = [];
self.imgs[ 0 ].full_img = LAPI.WP.fullImageSizeFromPage();
if ( self.imgs[ 0 ].full_img.width > 0 && self.imgs[ 0 ].full_img.height > 0 ) {
self.setup_step_two();
return;
}
}
 
// Get the full sizes of all the images. If more than 50, make several calls. (The API has limits.)
Array.forEach(self.imgs, function (img, idx) {
// Also avoid using Ajax on IE6...
if (cache[img.realName]) {
cache[img.realName][cache[img.realName].length] = idx;
} else {
cache[img.realName] = [idx];
names[names.length] = img.realName;
}
});
 
var to_docache = names.length{};
var done names = 0[];
 
Array.forEach( self.imgs, function ( img, idx ) {
function check_done (length)
if ( cache[ img.realName ] ) {
{
cache[ img.realName ][ cache[ img.realName ].length ] = idx;
done += length;
} else {
if (done >= names.length) {
cache[ img.realName ] = [ idx ];
if (typeof ImageAnnotator.info_callbacks !== 'undefined') ImageAnnotator.info_callbacks = null;
names[ names.length ] = img.realName;
self.setup_step_two();
}
}
} );
}
 
var to_do = names.length;
function make_calls (execute_call, url_limit)
var done = 0;
{
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};
}
 
function check_done( length ) {
var start = 0, chunk = 0, params;
done += length;
while (to_do > 0) {
if ( done >= names.length ) {
params = build_titles (start, Math.min(50, to_do), url_limit);
if ( typeof ImageAnnotator.info_callbacks !== 'undefined' ) { ImageAnnotator.info_callbacks = null; }
execute_call (params.n, params.text);
self.setup_step_two();
to_do -= params.n;
}
start += params.n;
}
}
}
 
function make_calls( execute_call, url_limit ) {
function set_info (json)
function build_titles( from, length, url_limit ) {
{
var done = 0;
try {
var text = '';
if (json && json.query && json.query.pages) {
for ( var i = from; i < from + functionlength; get_sizei++ (info) {
var new_text = names[ i ];
if (!info.imageinfo || info.imageinfo.length === 0) return;
if ( url_limit ) {
var title = info.title.replace(/ /g, '_');
new_text = encodeURIComponent( new_text );
var indices = cache[title];
if ( text.length && ( text.length + new_text.length + 1 > url_limit ) ) { break; }
if (!indices) return;
}
Array.forEach(
text += ( text.length ? '|' : '' ) + new_text;
indices
done++;
, function (i) {
}
self.imgs[i].full_img = { width : info.imageinfo[0].width
return { text: text, n: done };
,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 || 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
|| (wgUserGroups && wgUserGroups.join(' ').contains(protection.level))
;
}
);
}
for (var page in json.query.pages) {
get_size (json.query.pages[page]);
}
} // end if
} catch (ex) {
}
}
 
var start = 0, chunk = 0, params;
if ((!window.XMLHttpRequest && !!window.ActiveXObject) || !self.haveAjax) {
while ( to_do > 0 ) {
// IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that
params = build_titles( start, Math.min( 50, to_do ), url_limit );
// prompt by using getScript instead of parseWikitext in this case.
execute_call( params.n, params.text );
ImageAnnotator.info_callbacks = [];
to_do -= params.n;
var template = wgServer + wgScriptPath + '/api.php?action=query&format=json'
start += params.n;
+ '&prop=info|imageinfo&inprop=protection&iiprop=size'
}
+ '&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
},
 
function set_info( json ) {
setup_ui : function ()
try {
{
if ( json && json.query && json.query.pages ) {
// Complete the UI object we've gotten from config.
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 ) {
ImageAnnotator.UI.ready = false;
// IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that
ImageAnnotator.UI.repo = null;
// prompt by using getScript instead of parseWikitext in this case.
ImageAnnotator.UI.needs_plea = false;
ImageAnnotator.info_callbacks = [];
var template = mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php?action=query&format=json' +
'&prop=info|imageinfo&inprop=protection&iiprop=size' +
'&titles=&callback=ImageAnnotator.info_callbacks[].callback';
if ( template.startsWith( '//' ) ) { 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 () {
var readyEvent = [];
// Complete the UI object we've gotten from config.
 
ImageAnnotator.UI.fireReadyEventready = function () {false;
if ( ImageAnnotator.UI.ready)repo return= null; // Already fired, nothing to do.
ImageAnnotator.UI.readyneeds_plea = truefalse;
// Call all registered handlers, and clear the array.
Array.forEach( readyEvent , function (f, idx) {
try {f ();} catch (ex) {}
readyEvent[idx] = null;
});
readyEvent = null;
};
 
var readyEvent = [];
ImageAnnotator.UI.addReadyEventHandler = function (f) {
if (ImageAnnotator.UI.ready) {
f (); // Already fired: call directly
} else {
readyEvent[readyEvent.length] = f;
}
};
 
ImageAnnotator.UI.setupfireReadyEvent = function () {
if ( ImageAnnotator.UI.ready ) { return; } // Already fired, nothing to do.
{
if ( ImageAnnotator.UI.repo)ready = returntrue;
// Call all registered handlers, and clear the array.
var self = ImageAnnotator.UI;
Array.forEach( readyEvent, function ( f, idx ) {
var node = LAPI.make('div', null, { display: 'none' });
try { f(); } catch ( ex ) {}
document.body.appendChild(node);
readyEvent[ idx ] = null;
if (typeof UIElements === 'undefined') {
} );
self.basic = true;
readyEvent = null;
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.getaddReadyEventHandler = function (id, basic,f no_plea) {
if ( ImageAnnotator.UI.ready ) {
{
f(); // Already fired: call directly
var self = ImageAnnotator.UI;
} else {
if (!self.repo) self.setup();
readyEvent[ readyEvent.length ] = f;
var result = null;
}
var add_plea = false;
};
if (self.basic) {
result = self.repo[id];
} else {
result = UIElements.getEntry(id, self.repo, 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 (wgServer.contains('/commons') && add_plea && !no_plea) {
// Add a translation plea.
if (result.nodeName.toLowerCase() == 'div') {
result.appendChild(self.get_plea());
} else {
var span = LAPI.make('span');
span.appendChild(result);
span.appendChild(self.get_plea());
result = span;
}
}
return result;
};
 
ImageAnnotator.UI.get_pleasetup = function () {
if ( ImageAnnotator.UI.repo ) { return; }
{
var self = ImageAnnotator.UI;
var translatenode = selfLAPI.getmake( 'wpTranslatediv', falsenull, true){ ||display: 'translatenone' } );
document.body.appendChild( node );
var span = LAPI.make('small');
if ( typeof UIElements === 'undefined' ) {
span.appendChild(document.createTextNode('\xa0('));
self.basic = true;
span.appendChild(
self.repo = {};
LAPI.DOM.makeLink(
for ( var item in self.defaults ) {
wgServer + wgScript + '?title=MediaWiki_talk:ImageAnnotatorTexts'
node.innerHTML = self.defaults[ item ];
+ '&action=edit&section=new&withJS=MediaWiki:ImageAnnotatorTranslator.js'
self.repo[ item ] = node.firstChild;
+ '&language=' + wgUserLanguage
LAPI.DOM.removeChildren( node );
, translate
}
, (typeof translate === 'string' ? translate : LAPI.DOM.getInnerText(translate).trim())
} else {
)
self.basic = false;
);
self.repo = UIElements.emptyRepository( self.defaultLanguage );
span.appendChild(document.createTextNode(')'));
for ( var item in self.defaults ) {
return span;
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.initget = function (html_text_or_json id, basic, no_plea ) {
var self = ImageAnnotator.UI;
{
if ( !self.repo ) { self.setup(); }
var text;
var result = null;
if (typeof html_text_or_json === 'string')
var add_plea = false;
text = html_text_or_json;
if ( self.basic ) {
else if ( typeof html_text_or_json !== 'undefined'
result = self.repo[ id ];
&& typeof html_text_or_json.parse !== 'undefined'
} else {
&& typeof html_text_or_json.parse.text !== 'undefined'
result = UIElements.getEntry( id, self.repo, mw.config.get( 'wgUserLanguage' ), null );
&& typeof html_text_or_json.parse.text['*'] !== 'undefined'
add_plea = !result;
)
if ( !result ) { result = UIElements.getEntry( id, self.repo ); }
text = html_text_or_json.parse.text['*'];
}
else
self.needs_plea = add_plea;
text = null;
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.get_plea = function () {
if (!text) {
var self = ImageAnnotator.UI.fireReadyEvent();
var translate = self.get( 'wpTranslate', false, true ) || 'translate';
return;
var span = LAPI.make( 'small' );
}
span.appendChild( document.createTextNode( '\xa0(' ) );
span.appendChild(
LAPI.DOM.makeLink(
mw.config.get( 'wgServer' ) + mw.config.get( 'wgScript' ) + '?title=MediaWiki_talk:ImageAnnotatorTexts' +
'&action=edit&section=new&withJS=MediaWiki:ImageAnnotatorTranslator.js' +
'&language=' + mw.config.get( 'wgUserLanguage' ),
translate,
( typeof translate === 'string' ? translate : LAPI.DOM.getInnerText( translate ).trim() )
)
);
span.appendChild( document.createTextNode( ')' ) );
return span;
};
 
ImageAnnotator.UI.init = function ( html_text_or_json ) {
var node = LAPI.make('div', null, {display: 'none'});
var text;
document.body.appendChild(node);
if ( typeof html_text_or_json === 'string' ) {
try {
text = html_text_or_json;
node.innerHTML = text;
} else if (
} catch (ex) {
typeof html_text_or_json !== 'undefined' &&
LAPI.DOM.removeNode(node);
typeof html_text_or_json.parse !== 'undefined' &&
node = null;
typeof html_text_or_json.parse.text !== 'undefined' &&
// Swallow. We'll just work with the default UI
typeof html_text_or_json.parse.text[ '*' ] !== 'undefined'
}
) {
if (node && !ImageAnnotator.UI.repo) ImageAnnotator.UI.setup();
text = html_text_or_json.parse.text[ '*' ];
ImageAnnotator.UI.fireReadyEvent();
} else {
};
text = null;
}
 
if ( !text ) {
var ui_page = '{{MediaWiki:ImageAnnotatorTexts'
ImageAnnotator.UI.fireReadyEvent();
+ (wgUserLanguage != wgContentLanguage ? '|lang=' + wgUserLanguage : '')
return;
+ '|live=1}}';
}
 
var node = LAPI.make( 'div', null, { display: 'none' } );
function get_ui_no_ajax ()
document.body.appendChild( node );
{
try {
var url =
node.innerHTML = text;
wgServer + wgScriptPath + '/api.php?action=parse&pst&text='
} catch ( ex ) {
+ encodeURIComponent(ui_page) + '&title=API&prop=text&format=json'
LAPI.DOM.removeNode( node );
+ '&callback=ImageAnnotator.UI.init&maxage=14400&smaxage=14400'
node = null;
;
// Swallow. We'll just work with the default 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
if ( node && !ImageAnnotator.UI.repo ) { ImageAnnotator.UI.setup(); }
// anyway.)
ImageAnnotator.UI.fireReadyEvent();
IA.getScript(url, true); // No local caching!
};
}
 
var ui_page = '{{MediaWiki:ImageAnnotatorTexts' +
function get_ui ()
( mw.config.get( 'wgUserLanguage' ) != mw.config.get( 'wgContentLanguage' ) ? '|lang=' + mw.config.get( 'wgUserLanguage' ) : '' ) +
{
'|live=1}}';
IA.haveAjax = (LAPI.Ajax.getRequest() != null);
IA.ajaxQueried = true;
 
function get_ui_no_ajax() {
// Works only with Ajax (but then, most of this script doesn't work without).
var url =
// Check what this does to load times... If lots of people used this, it might be better to
mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php?action=parse&pst&text=' +
// have the UI texts included in some footer as we did on Special:Upload. True, everybody
encodeURIComponent( ui_page ) + '&title=API&prop=text&format=json' +
// would get the texts, even people not using this, but the texts are small anyway...
'&callback=ImageAnnotator.UI.init&maxage=14400&smaxage=14400';
if (!IA.haveAjax) {
// Result cached for 4 hours. How to properly handle an error? It appears there's no way to catch
get_ui_no_ajax (); // Fallback.
// that on IE. (On FF, we could use an onerror handler on the script tag, but on FF, we use Ajax
return;
// anyway.)
}
IA.getScript( url, true ); // No local caching!
}
 
function get_ui() {
LAPI.Ajax.parseWikitext(
IA.haveAjax = ( LAPI.Ajax.getRequest() != null );
ui_page
IA.ajaxQueried = true;
, ImageAnnotator.UI.init
, ImageAnnotator.UI.fireReadyEvent
, false
, null
, "API" // A fixed string to enable caching at all.
, 14400 // 4 hour caching.
);
} // end get_ui
 
// Works only with Ajax (but then, most of this script doesn't work without).
if (!window.XMLHttpRequest && !!window.ActiveXObject) {
// Check what this does to load times... If lots of people used this, it might be better to
// IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that
// have the UI texts included in some footer as we did on Special:Upload. True, everybody
// prompt by using getScript instead of parseWikitext in this case. The disadvantage
// would get the texts, even people not using this, but the texts are small anyway...
// is that we don't do anything if this fails for some reason.
if ( !IA.haveAjax ) {
get_ui_no_ajax ();
get_ui_no_ajax(); // Fallback.
} else {
return;
get_ui ();
}
}
},
 
LAPI.Ajax.parseWikitext(
setup_step_two : function ()
ui_page,
{
ImageAnnotator.UI.init,
var self = IA;
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 ) {
// Throw out any images for which we miss either the thumbnail or the full image size.
// IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that
// Also throws out thumbnails that are larger than the full image.
// prompt by using getScript instead of parseWikitext in this case. The disadvantage
self.imgs = Array.select(self.imgs, function (elem, idx) {
// is that we don't do anything if this fails for some reason.
var result =
get_ui_no_ajax();
elem.thumb.width > 0 && elem.thumb.height > 0
} else {
&& typeof elem.full_img !== 'undefined'
get_ui();
&& 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;
});
 
setup_step_two: function () {
if (self.imgs.length === 0) return;
var self = IA;
 
// Throw out any images for which we miss either the thumbnail or the full image size.
ImageAnnotator.UI.addReadyEventHandler(IA.complete_setup);
// Also throws out thumbnails that are larger than the full image.
},
self.imgs = Array.select( self.imgs, function ( elem, idx ) {
var result =
elem.thumb.width > 0 && elem.thumb.height > 0 &&
typeof elem.full_img !== 'undefined' &&
elem.full_img.width > 0 && elem.full_img.height > 0 &&
elem.full_img.width >= elem.thumb.width &&
elem.full_img.height >= elem.thumb.height;
if ( self.may_edit && idx === 0 && !result ) { self.may_edit = false; }
return result;
} );
 
if ( self.imgs.length === 0 ) { return; }
complete_setup : function ()
{
// We can be sure to have the UI here because this is called only when the ready event of the
// UI object is fired.
var self = IA;
 
ImageAnnotator.UI.addReadyEventHandler( IA.complete_setup );
// Check edit permissions
},
if (self.may_edit && typeof wgRestrictionEdit !== 'undefined' ) {
self.may_edit =
( (wgRestrictionEdit.length === 0 || wgUserGroups && wgUserGroups.join(' ').contains('sysop'))
|| ( wgRestrictionEdit.length === 1 && wgRestrictionEdit[0] === 'autoconfirmed'
&& wgUserGroups && wgUserGroups.join(' ').contains('confirmed') // confirmed & autoconfirmed
)
);
}
 
complete_setup: function () {
if (self.may_edit) {
// We can be sure to // Check whetherhave the imageUI ishere local.because Don'tthis allowis editingcalled ifonly when the fileready isevent remote.of the
// UI object is fired.
var sharedUpload = getElementsByClassName (document, 'div', 'sharedUploadNotice');
var self = IA;
self.may_edit = (!sharedUpload || sharedUpload.length === 0);
}
if (self.may_edit && 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, '_') == wgTitle.replace(/ /g, '_'));
}
 
// Check edit permissions
if (self.may_edit && self.ajaxQueried) self.may_edit = self.haveAjax;
if ( self.may_edit && mw.config.get( 'wgRestrictionEdit' ) ) {
self.may_edit = (
( mw.config.get( 'wgRestrictionEdit' ).length === 0 || mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( 'sysop' ) ) ||
(
mw.config.get( 'wgRestrictionEdit' ).length === 1 && mw.config.get( 'wgRestrictionEdit' )[ 0 ] === 'autoconfirmed' &&
mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( 'confirmed' ) // confirmed & autoconfirmed
)
);
}
 
if ( self.may_edit ) {
// Now create viewers for all images
// Check whether the image is local. Don't allow editing if the file is remote.
self.viewers = new Array (self.imgs.length);
var sharedUpload = getElementsByClassName( document, 'div', 'sharedUploadNotice' );
for (var i = 0; i < self.imgs.length; i++) {
self.viewers[i]may_edit = new( ImageNotesViewer!sharedUpload (self|| sharedUpload.imgs[i], ilength === 0 && self.may_edit);
}
}
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; }
 
// RespectNow usercreate overrideviewers for zoom, ifall anyimages
self.viewers = new Array( self.imgs.length );
self.zoom_threshold = ImageAnnotator_config.zoom_threshold;
for ( var i = 0; i < self.imgs.length; i++ ) {
if ( typeof window.ImageAnnotator_zoom_threshold !== 'undefined'
self.viewers[ i ] = new ImageNotesViewer( self.imgs[ i ], i === 0 && self.may_edit );
&& !isNaN (window.ImageAnnotator_zoom_threshold)
}
&& window.ImageAnnotator_zoom_threshold >= 0.0
)
{
// If somebody sets it to a nonsensical high value, that's his or her problem: there won't be any
// zooming.
self.zoom_threshold = window.ImageAnnotator_zoom_threshold;
}
// Adapt zoom threshold for small thumbnails or images with a very lopsided width/height ratio,
// but only if we *can* zoom at least twice
if ( self.viewers[0].full_img.width > 300
&& Math.min(self.viewers[0].factors.dx, self.viewers[0].factors.dy) >= 2.0
)
{
if ( self.viewers[0].thumb.width < 400
|| self.viewers[0].thumb.width / self.viewers[0].thumb.height > 2.0
|| self.viewers[0].thumb.height / self.viewers[0].thumb.width > 2.0
)
{
self.zoom_threshold = 0; // Force zooming
}
}
 
if ( self.may_edit ) {
self.editor = new ImageAnnotationEditor ();
 
// Respect user override for zoom, if any
function track (evt) {
self.zoom_threshold = ImageAnnotator_config.zoom_threshold;
evt = evt || window.event;
if (
if (self.is_adding) self.update_zoom(evt);
typeof window.ImageAnnotator_zoom_threshold !== 'undefined' &&
if (!self.is_tracking) return LAPI.Evt.kill(evt);
!isNaN( window.ImageAnnotator_zoom_threshold ) &&
var mouse_pos = LAPI.Pos.mousePosition(evt);
window.ImageAnnotator_zoom_threshold >= 0.0
if (!LAPI.Pos.isWithin(self.cover, mouse_pos.x, mouse_pos.y)) return;
) {
var origin = LAPI.Pos.position(self.cover);
// If somebody sets it to a nonsensical high value, that's his or her problem: there won't be any
// Make mouse pos relative to cover
// zooming.
mouse_pos.x = mouse_pos.x - origin.x;
self.zoom_threshold = window.ImageAnnotator_zoom_threshold;
mouse_pos.y = mouse_pos.y - origin.y;
}
if (mouse_pos.x >= self.base_x) {
// Adapt zoom threshold for small thumbnails or images with a very lopsided width/height ratio,
self.definer.style.width = '' + (mouse_pos.x - self.base_x) + 'px';
// but only if we *can* zoom at least twice
self.definer.style.left = '' + self.base_x + 'px';
if (
} else {
self.viewers[ 0 ].full_img.width > 300 &&
self.definer.style.width = '' + (self.base_x - mouse_pos.x) + 'px';
Math.min( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy ) >= 2.0
self.definer.style.left = '' + mouse_pos.x + 'px';
) {
}
if (
if (mouse_pos.y >= self.base_y) {
self.viewers[ 0 ].thumb.width < 400 ||
self.definer.style.height = '' + (mouse_pos.y - self.base_y) + 'px';
self.viewers[ 0 ].thumb.width / self.viewers[ 0 ].thumb.height > 2.0 ||
self.definer.style.top = '' + self.base_y + 'px';
self.viewers[ 0 ].thumb.height / self.viewers[ 0 ].thumb.width > 2.0
} else {
) {
self.definer.style.height = '' + (self.base_y - mouse_pos.y) + 'px';
self.zoom_threshold = 0; // Force zooming
self.definer.style.top = '' + mouse_pos.y + 'px';
}
}
}
return LAPI.Evt.kill(evt);
}
 
self.editor = new ImageAnnotationEditor();
function pause (evt) {
LAPI.Evt.remove(document, 'mousemove', track, true);
if (!LAPI.Browser.is_ie && typeof document.captureEvents === 'function')
document.captureEvents(null);
self.move_listening = false;
}
 
function resumetrack( (evt ) {
evt = evt || window.event;
// captureEvents is actually deprecated, but I haven't succeeded to make this work with
if ( self.is_adding ) { self.update_zoom( evt ); }
// addEventListener only.
if (( !self.is_tracking ||) self{ return LAPI.Evt.is_adding)kill( &&evt !self.move_listening); {}
var mouse_pos = LAPI.Pos.mousePosition( evt );
if (!LAPI.Browser.is_ie && typeof document.captureEvents === 'function')
if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) { return; }
document.captureEvents(Event.MOUSEMOVE);
var origin = LAPI.Pos.position( self.cover );
LAPI.Evt.attach(document, 'mousemove', track, true);
// Make mouse pos relative to cover
self.move_listening = true;
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 stop_trackingpause( (evt ) {
LAPI.Evt.remove( document, 'mousemove', track, true );
evt = evt || window.event;
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( null ); }
// Check that we're within the image. Note: this check can fail only on IE >= 7, on other
self.move_listening = false;
// browsers, we attach the handler on self.cover and thus we don't even get events outside
}
// that range.
var mouse_pos = LAPI.Pos.mousePosition(evt);
if (!LAPI.Pos.isWithin(self.cover, mouse_pos.x, mouse_pos.y)) return;
if (self.is_tracking) {
self.is_tracking = false;
self.is_adding = false;
// Done.
pause ();
if (LAPI.Browser.is_ie) {
//Trust Microsoft to get everything wrong!
LAPI.Evt.remove(document, 'mouseup', stop_tracking);
} else {
LAPI.Evt.remove(self.cover, 'mouseup', stop_tracking);
}
LAPI.Evt.remove(window, 'blur', pause);
LAPI.Evt.remove(window, 'focus', resume);
self.cover.style.cursor = 'auto';
LAPI.DOM.removeNode(self.border);
LAPI.Evt.remove(self.cover, self.mouse_in, self.update_zoom_evt);
LAPI.Evt.remove(self.cover, self.mouse_out, self.hide_zoom_evt);
self.hide_zoom();
self.viewers[0].hide(); // Hide all existing boxes
if (!self.definer || self.definer.offsetWidth <= 0 || self.definer.offsetHeight <= 0) {
// Nothing: just remove the definer:
if (self.definer) LAPI.DOM.removeNode(self.definer);
// Re-attach event handlers
self.viewers[0].setShowHideEvents(true);
self.hide_cover();
self.viewers[0].setDefaultMsg();
// And make sure we get the real view again
self.viewers[0].show();
} else {
// We have a div with some extent: remove event capturing and create a new annotation
var new_note = new ImageAnnotation (self.definer, self.viewers[0], -1);
self.viewers[0].register(new_note);
self.editor.editNote(new_note);
}
self.definer = null;
}
if (evt) return LAPI.Evt.kill(evt);
return false;
}
 
function start_trackingresume( (evt ) {
// captureEvents is actually deprecated, but I haven't succeeded to make this work with
{
// addEventListener only.
if (!self.is_tracking) {
if ( ( self.is_tracking || self.is_adding ) && !self.move_listening ) {
self.is_tracking = true;
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( Event.MOUSEMOVE ); }
evt = evt || window.event;
LAPI.Evt.attach( document, 'mousemove', track, true );
// Set the position, size 1
self.move_listening = true;
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;
}
 
function add_newstop_tracking( (evt ) {
evt = evt || window.event;
if (!self.canEdit()) return;
// Check that we're within the image. Note: this check can fail only on IE >= 7, on other
// browsers, we attach the handler on self.cover and thus we don't even get events outside
// that range.
var mouse_pos = LAPI.Pos.mousePosition( evt );
if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) { return; }
if ( self.is_tracking ) {
self.is_tracking = false;
self.is_adding = false;
// Done.
pause();
if ( LAPI.Browser.is_ie ) {
// Trust Microsoft to get everything wrong!
LAPI.Evt.remove( document, 'mouseup', stop_tracking );
} else {
LAPI.Evt.remove( self.cover, 'mouseup', stop_tracking );
}
LAPI.Evt.remove( window, 'blur', pause );
LAPI.Evt.remove( window, 'focus', resume );
self.cover.style.cursor = 'auto';
LAPI.DOM.removeNode( self.border );
LAPI.Evt.remove( self.cover, self.mouse_in, self.update_zoom_evt );
LAPI.Evt.remove( self.cover, self.mouse_out, self.hide_zoom_evt );
self.hide_zoom();
self.viewers[ 0 ].hide(); // Hide all existing boxes
if ( !self.definer || self.definer.offsetWidth <= 0 || self.definer.offsetHeight <= 0 ) {
// Nothing: just remove the definer:
if ( self.definer ) { LAPI.DOM.removeNode( self.definer ); }
// Re-attach event handlers
self.viewers[ 0 ].setShowHideEvents( true );
self.hide_cover();
self.viewers[ 0 ].setDefaultMsg();
// And make sure we get the real view again
self.viewers[ 0 ].show();
} else {
// We have a div with some extent: remove event capturing and create a new annotation
var new_note = new ImageAnnotation( self.definer, self.viewers[ 0 ], -1 );
self.viewers[ 0 ].register( new_note );
self.editor.editNote( new_note );
}
self.definer = null;
}
if ( evt ) { return LAPI.Evt.kill( evt ); }
return false;
}
 
function start_tracking( evt ) {
self.editor.hide_editor();
if ( !self.is_tracking ) {
Tooltips.close();
self.is_tracking = true;
var cover = self.get_cover();
evt = evt || window.event;
cover.style.cursor = 'crosshair';
// Set the position, size 1
self.definer =
var mouse_pos = LAPI.Pos.mousePosition( evt );
LAPI.make(
var origin = LAPI.Pos.position( self.cover );
'div', null
self.base_x = mouse_pos.x - origin.x;
,{ border : '1px solid ' + IA.new_border
self.base_y = mouse_pos.y - origin.y;
,display : 'none'
Object.merge(
,position : 'absolute'
{ left: String( self.base_x ) + 'px',
,top : '0px'
top: String( self.base_y ) + 'px',
,left : '0px'
, width : '0px',
, height : '0px',
display: ''
,padding : '0'
}
,lineHeight : '0px' // IE needs this, even though there are no lines within
, self.definer.style
,fontSize : '0px' // IE
);
,zIndex : cover.style.zIndex - 2 // Below the mouse capture div
// Set mouse handlers
}
LAPI.Evt.remove( self.cover, 'mousedown', start_tracking );
);
if ( LAPI.Browser.is_ie ) {
self.viewers[0].img_div.appendChild(self.definer);
LAPI.Evt.attach( document, 'mouseup', stop_tracking ); // Doesn't work properly on self.cover...
// Enter mouse-tracking mode to define extent of view. Mouse cursor is outside of image,
} else {
// hence none of our tooltips are visible.
LAPI.Evt.attach( self.cover, 'mouseup', stop_tracking );
self.viewers[0].img_div.appendChild(self.border);
}
self.show_cover();
resume();
self.is_tracking = false;
LAPI.Evt.attach( window, 'blur', pause );
self.is_adding = true;
LAPI.Evt.attach(cover window, 'mousedownfocus', start_trackingresume );
}
resume ();
if ( evt ) { return LAPI.Evt.kill( evt ); }
self.button_div.style.display = 'none';
return false;
// 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 = '';
}
 
function add_new( evt ) {
self.button_div = LAPI.make('div');
if ( !self.canEdit() ) { return; }
self.viewers[0].main_div.appendChild(self.button_div);
self.add_button =
LAPI.DOM.makeButton(
'ImageAnnotationAddButton'
, ImageAnnotator.UI.get('wpImageAnnotatorAddButtonText', true)
, add_new
);
var add_plea = ImageAnnotator.UI.needs_plea;
self.button_div.appendChild(self.add_button);
self.help_link = self.createHelpLink();
if (self.help_link) {
self.button_div.appendChild(document.createTextNode('\xa0'));
self.button_div.appendChild(self.help_link);
}
if (add_plea && wgServer.contains('/commons'))
self.button_div.appendChild(ImageAnnotator.UI.get_plea());
 
self.editor.hide_editor();
} // end if may_edit
Tooltips.close();
var cover = self.get_cover();
cover.style.cursor = 'crosshair';
self.definer = LAPI.make(
'div', null,
{
border: '1px solid ' + IA.new_border,
display: 'none',
position: 'absolute',
top: '0px',
left: '0px',
width: '0px',
height: '0px',
padding: '0',
lineHeight: '0px', // IE needs this, even though there are no lines within
fontSize: '0px', // IE
zIndex: cover.style.zIndex - 2 // Below the mouse capture div
}
);
self.viewers[ 0 ].img_div.appendChild( self.definer );
// Enter mouse-tracking mode to define extent of view. Mouse cursor is outside of image,
// hence none of our tooltips are visible.
self.viewers[ 0 ].img_div.appendChild( self.border );
self.show_cover();
self.is_tracking = false;
self.is_adding = true;
LAPI.Evt.attach( cover, 'mousedown', start_tracking );
resume();
self.button_div.style.display = 'none';
// Remove the event listeners on the image: IE sometimes invokes them even when the image is covered
self.viewers[ 0 ].setShowHideEvents( false );
self.viewers[ 0 ].hide(); // Make sure notes are hidden
self.viewers[ 0 ].toggle( true ); // Show all note rectangles (but only the dummies)
self.update_zoom_evt = LAPI.Evt.makeListener( self, self.update_zoom );
self.hide_zoom_evt = LAPI.Evt.makeListener( self, self.hide_zoom );
self.show_zoom();
LAPI.Evt.attach( cover, self.mouse_in, self.update_zoom_evt );
LAPI.Evt.attach( cover, self.mouse_out, self.hide_zoom_evt );
LAPI.DOM.removeChildren( self.viewers[ 0 ].msg );
self.viewers[ 0 ].msg.appendChild( ImageAnnotator.UI.get( 'wpImageAnnotatorDrawRectMsg', false ) );
self.viewers[ 0 ].msg.style.display = '';
}
 
self.button_div = LAPI.make( 'div' );
// Get the file description pages of thumbnails. Figure out for which viewers we need to do this.
self.viewers[ 0 ].main_div.appendChild( self.button_div );
var cache = {};
self.add_button = LAPI.DOM.makeButton(
var get_local = [];
'ImageAnnotationAddButton',
var get_foreign = [];
ImageAnnotator.UI.get( 'wpImageAnnotatorAddButtonText', true ),
Array.forEach(self.viewers, function (viewer, idx) {
add_new
if (viewer.setup_done || viewer.isLocal && !viewer.has_page) return;
);
// Handle only images that either are foreign or local and do have a page.
var add_plea = ImageAnnotator.UI.needs_plea;
if (cache[viewer.realName]) {
self.button_div.appendChild( self.add_button );
cache[viewer.realName][cache[viewer.realName].length] = idx;
self.help_link = self.createHelpLink();
} else {
if ( self.help_link ) {
cache[viewer.realName] = [idx];
self.button_div.appendChild( document.createTextNode( '\xa0' ) );
if (!viewer.has_page) {
self.button_div.appendChild( self.help_link );
get_foreign[get_foreign.length] = viewer.realName;
}
} else {
if ( add_plea && mw.config.get( 'wgServer' ).contains( '/commons' ) ) { self.button_div.appendChild( ImageAnnotator.UI.get_plea() ); }
get_local[get_local.length] = viewer.realName;
}
}
});
 
} // end if may_edit
if (get_local.length === 0 && get_foreign.length === 0) return;
 
// Get the file description pages of thumbnails. Figure out for which viewers we need to do this.
// Now we have unique page names in the cache and in to_get. Go get the corresponding file
var cache = {};
// description pages. We make a series of simultaneous asynchronous calls to avoid hitting
var get_local = [];
// API limits and to keep the URL length below the limit for the foreign_repo calls.
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; }
function make_calls (list, execute_call, url_limit)
{
function composer (list, from, length, url_limit)
{
function compose (list, from, length, url_limit)
{
var text = '';
var done = 0;
for (var i = from; i < from + length; i++) {
var new_text =
'<div class="wpImageAnnotatorInlineImageWrapper" style="display:none;">'
+ '<span class="image_annotation_inline_name">' + list[i] + '</span>'
+ '{{:' + list[i] + '}}' // Leading dot to avoid getting links to the full images if we hit a parser limit
+ '</div>'
;
if (url_limit) {
new_text = encodeURIComponent(new_text);
if (text.length && (text.length + new_text.length > url_limit)) break;
}
text = text + new_text;
done++;
// Additionally, limit the number of image pages to get: these can be large, and the server
// may refuse to actually do the transclusions but may in that case include the full images
// in the result, which would make us load the full images, which is desastrous if there are
// many thumbs to large images on the page.
if (done == 5) break;
}
return {text: text, n: done};
}
 
// Now we have unique page names in the cache and in to_get. Go get the corresponding file
var param = compose (list, from, length, url_limit);
// description pages. We make a series of simultaneous asynchronous calls to avoid hitting
execute_call (param.text);
// API limits and to keep the URL length below the limit for the foreign_repo calls.
return param.n;
}
 
function make_calls( list, execute_call, url_limit ) {
var start = 0, chunk = 0, to_do = list.length;
function composer( list, from, length, url_limit ) {
while (to_do > 0) {
function compose( list, from, length, url_limit ) {
chunk = composer (list, start, Math.min(50, to_do), url_limit);
var text = '';
to_do -= chunk;
var done = 0;
start += chunk;
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 );
var divRE = /(<\s*div\b)|(<\/\s*div\s*>)/ig;
execute_call( param.text );
var blockStart = '<div class="wpImageAnnotatorInlineImageWrapper"';
return param.n;
var inlineNameEnd = '</span>';
}
var noteStart = '<div id="image_annotation_note_';
var noteControlRE = /<div\s*class="(wpImageAnnotatorInlinedRules|image_annotation_colors")(\S|\s)*?\/div>/g;
 
var start = 0, chunk = 0, to_do = list.length;
// Our parse request returns the full html of the description pages' contents, including any
while ( to_do > 0 ) {
// license or information or other templates that we don't care about, and which may contain
chunk = composer( list, start, Math.min( 50, to_do ), url_limit );
// additional images we'd rather not load when we add this (X)HTML to the DOM. Therefore, we
to_do -= chunk;
// strip out everything but the notes.
start += chunk;
function strip_noise (html) {
}
var result = '';
}
var m;
// First, get rid of HTML comments and scripts
html = html.replace(/<\!--(.|\s)*?--\>/g, '').replace(/<script(.|\s)*?\/script>/g, '');
var i = html.indexOf(blockStart, idx), idx = 0, l = html.length;
// Now collect pages
while (idx < l && i >= idx) {
var j = html.indexOf(inlineNameEnd, i+blockStart.length);
if (j < i+blockStart.length) break;
result += html.substring(i, j+inlineNameEnd.length);
idx = j+inlineNameEnd.length;
// Now collect all image image notes for that page
var note_begin = 0, next_block = html.indexOf(blockStart, idx);
// Do we have image note control or color templates?
j = idx;
for (;;) {
noteControlRE.lastIndex = j;
m = noteControlRE.exec(html);
if (!m || next_block >= idx && m.index > next_block) break;
result += m[0];
j = m.index + m[0].length;
}
// Collect notes
for (;;) {
note_begin = html.indexOf(noteStart, idx);
// Check whether next wrapper comes first
if (note_begin < idx || (next_block >= idx && note_begin > next_block)) break;
// Start parsing nested <div> and </div>, from note_begin on. Just ignore any other tags.
var level = 1, k = note_begin + noteStart.length;
while (level > 0 && k < l) {
divRE.lastIndex = k;
m = divRE.exec(html);
if (!m || m.length < 2) {
k = l; // Nothing found at all?
} else {
if (m[1]) {
level++; k = m.index + m[1].length; // divStart found first
} else if (m.length > 2 && m[2]) {
level--; k = m.index + m[2].length; // divEnd found first
} else {
k = l; // Huh?
}
}
} // end loop for nested divs
result += html.substring(note_begin, k);
while (level-- > 0) result += '</div>'; // Missing ends.
idx = k;
} // end loop collecting notes
result += '</div>'; // Close the wrapper
i = next_block;
} // end loop collecting pages
return result;
}
 
var divRE = /(<\s*div\b)|(<\/\s*div\s*>)/ig;
function setup_thumb_viewers (html_text)
var blockStart = '<div class="wpImageAnnotatorInlineImageWrapper"';
{
var inlineNameEnd = '</span>';
var node = LAPI.make('div', null, {display: 'none'});
var noteStart = '<div id="image_annotation_note_';
document.body.appendChild(node);
var noteControlRE = /<div\s*class="(wpImageAnnotatorInlinedRules|image_annotation_colors")(\S|\s)*?\/div>/g;
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);
}
 
// Our parse request returns the full html of the description pages' contents, including any
ImageAnnotator.script_callbacks = [];
// license or information or other templates that we don't care about, and which may contain
// additional images we'd rather not load when we add this (X)HTML to the DOM. Therefore, we
// strip out everything but the notes.
function strip_noise( html ) {
var result = '';
var m;
// First, get rid of HTML comments and scripts
html = html.replace( /<!--(.|\s)*?-->/g, '' ).replace( /<script(.|\s)*?\/script>/g, '' );
var i = html.indexOf( blockStart, idx ), idx = 0, l = html.length;
// Now collect pages
while ( idx < l && i >= idx ) {
var j = html.indexOf( inlineNameEnd, i + blockStart.length );
if ( j < i + blockStart.length ) { break; }
result += html.substring( i, j + inlineNameEnd.length );
idx = j + inlineNameEnd.length;
// Now collect all image image notes for that page
var note_begin = 0, next_block = html.indexOf( blockStart, idx );
// Do we have image note control or color templates?
j = idx;
for ( ;; ) {
noteControlRE.lastIndex = j;
m = noteControlRE.exec( html );
if ( !m || next_block >= idx && m.index > next_block ) { break; }
result += m[ 0 ];
j = m.index + m[ 0 ].length;
}
// Collect notes
for ( ;; ) {
note_begin = html.indexOf( noteStart, idx );
// Check whether next wrapper comes first
if ( note_begin < idx || ( next_block >= idx && note_begin > next_block ) ) { break; }
// Start parsing nested <div> and </div>, from note_begin on. Just ignore any other tags.
var level = 1, k = note_begin + noteStart.length;
while ( level > 0 && k < l ) {
divRE.lastIndex = k;
m = divRE.exec( html );
if ( !m || m.length < 2 ) {
k = l; // Nothing found at all?
} else {
if ( m[ 1 ] ) {
level++; k = m.index + m[ 1 ].length; // divStart found first
} else if ( m.length > 2 && m[ 2 ] ) {
level--; k = m.index + m[ 2 ].length; // divEnd found first
} else {
k = l; // Huh?
}
}
} // end loop for nested divs
result += html.substring( note_begin, k );
while ( level-- > 0 ) { 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 make_script_calls setup_thumb_viewers(list, apihtml_text ) {
var node = LAPI.make( 'div', null, { display: 'none' } );
{
document.body.appendChild( node );
var template = api + '?action=parse&pst&text=&prop=text&format=json'
try {
+ '&maxage=1800&smaxage=1800&uselang=' + wgUserLanguage //see bugzilla 22764
node.innerHTML = strip_noise( html_text );
+ '&callback=ImageAnnotator.script_callbacks[].callback';
var pages = getElementsByClassName( node, 'div', 'wpImageAnnotatorInlineImageWrapper' );
if (template.startsWith('//')) template = document.___location.protocol + template; // Avoid protocol-relative URIs (IE7 bug)
for ( var i = 0; pages && i < pages.length; i++ ) {
make_calls (
var notes = getElementsByClassName( pages[ i ], 'div', self.annotation_class );
list
if ( !notes || notes.length === 0 ) {
, function (text) {
continue;
var idx = ImageAnnotator.script_callbacks.length;
}
ImageAnnotator.script_callbacks[idx] =
var page = self.getItem( 'inline_name', pages[ i ] );
{ callback : function (json) {
if ( !page ) { continue; }
if (json && json.parse && json.parse.text && json.parse.text['*']) {
page = page.replace( / /g, '_' );
setup_thumb_viewers (json.parse.text['*']);
var viewers = cache[ page ] || cache[ 'File:' + page.substring( page.indexOf( ':' ) + 1 ) ];
}
if ( !viewers || viewers.length === 0 ) {
ImageAnnotator.script_callbacks[idx].done = true;
continue;
if (ImageAnnotator.script_callbacks[idx].script) {
}
LAPI.DOM.removeNode(ImageAnnotator.script_callbacks[idx].script);
// Update rules.
ImageAnnotator.script_callbacks[idx].script = null;
var rules = getElementsByClassName( pages[ i ], 'div', 'wpImageAnnotatorInlinedRules' );
}
var local_rules = {
}
inline: Object.clone( IA.rules.inline ),
,done : false
thumbs: Object.clone( IA.rules.thumbs )
};
};
ImageAnnotator.script_callbacks[idx].script =
if ( rules && rules.length ) {
IA.getScript(
rules = rules[ 0 ];
template.replace('script_callbacks[].callback', 'script_callbacks[' + idx + '].callback')
if (
.replace('&text=&', '&text=' + text + '&')
typeof local_rules.inline.show === 'undefined' &&
, true // No local caching!
LAPI.DOM.hasClass( rules, 'wpImageAnnotatorNoInlineDisplay' )
);
) {
if ( ImageAnnotator.script_callbacks && ImageAnnotator.script_callbacks[idx]
local_rules.inline.show = false;
&& ImageAnnotator.script_callbacks[idx].done && ImageAnnotator.script_callbacks[idx].script)
}
{
if (
LAPI.DOM.removeNode(ImageAnnotator.script_callbacks[idx].script);
typeof local_rules.inline.icon === 'undefined' &&
ImageAnnotator.script_callbacks[idx].script = null;
LAPI.DOM.hasClass( rules, 'wpImageAnnotatorInlineDisplayIcon' )
}
) {
}
local_rules.inline.icon = true;
, (LAPI.DOM.is_ie ? 1950 : 4000) - template.length // Some slack for caching parameters
}
);
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 = [];
if ((!window.XMLHttpRequest && !!window.ActiveXObject) || !self.haveAjax) {
make_script_calls(get_local, wgServer + wgScriptPath + '/api.php');
} else {
make_calls(
get_local
, function (text) {
LAPI.Ajax.parseWikitext(
text
, function (html_text) {if (html_text) setup_thumb_viewers(html_text);}
, function () {}
, false
, null
, 'API' // Fixed string to enable caching at all
, 1800 // 30 minutes caching.
);
}
);
}
 
function make_script_calls( list, api ) {
// Can't use Ajax for foreign repo, might violate single-origin policy (e.g. from wikisource.org
var template = api + '?action=parse&pst&text=&prop=text&format=json' +
// to wikimedia.org). Attention, here we must care about the URL length! IE has a limit of 2083
'&maxage=1800&smaxage=1800&uselang=' + mw.config.get( 'wgUserLanguage' ) + // see bugzilla 22764
// character (2048 in the path part), and servers also may impose limits (on the WMF servers,
'&callback=ImageAnnotator.script_callbacks[].callback';
// the limit appears to be 8kB).
if ( template.startsWith( '//' ) ) { template = document.___location.protocol + template; } // Avoid protocol-relative URIs (IE7 bug)
make_script_calls (get_foreign, ImageAnnotator_config.sharedRepositoryAPI());
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 ) {
show_zoom : function ()
make_script_calls( get_local, mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php' );
{
} else {
var self = IA;
make_calls(
if ( ( self.viewers[0].factors.dx < self.zoom_threshold
get_local,
&& self.viewers[0].factors.dy < self.zoom_threshold
function ( text ) {
)
LAPI.Ajax.parseWikitext(
|| Math.max(self.viewers[0].factors.dx, self.viewers[0].factors.dy) < 2.0
text,
)
function ( html_text ) { if ( html_text ) { setup_thumb_viewers( html_text ); } },
{
function () {},
// Below zoom threshold, or full image not even twice the size of the preview
false,
return;
null,
}
'API', // Fixed string to enable caching at all
if (!self.zoom) {
1800 // 30 minutes caching.
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
},
 
// Can't use Ajax for foreign repo, might violate single-origin policy (e.g. from wikisource.org
update_zoom : function (evt)
// 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,
if (!evt) return; // We need an event to calculate positions!
// the limit appears to be 8kB).
var self = IA;
make_script_calls( get_foreign, ImageAnnotator_config.sharedRepositoryAPI() );
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';
},
 
hide_zoom show_zoom: function (evt) {
var self = IA;
{
if (
if (!IA.zoom) return;
(
if (evt) {
self.viewers[ 0 ].factors.dx < self.zoom_threshold &&
var mouse_pos = LAPI.Pos.mousePosition(evt);
self.viewers[ 0 ].factors.dy < self.zoom_threshold
if (LAPI.Pos.isWithin(IA.cover, mouse_pos.x, mouse_pos.y)) return;
) ||
}
Math.max( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy ) < 2.0
IA.zoom.style.display = 'none';
) {
},
// 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
},
 
createHelpLink update_zoom: function ( evt ) {
if ( !evt ) { return; } // We need an event to calculate positions!
{
var self = IA;
var msg = ImageAnnotator.UI.get('wpImageAnnotatorHelp', false, true);
if (!msg || !msgself.lastChildzoom ) { return; null;}
var mouse_pos = LAPI.Pos.mousePosition( evt );
// Make sure we use the right protocol for all images:
var origin = LAPI.Pos.position( self.cover );
var imgs = msg.getElementsByTagName('img');
if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) {
var text;
IA.hide_zoom();
var tgt;
return;
if (imgs) {
}
for (var i = 0; i < imgs.length; i++) {
var dx = mouse_pos.x - origin.x;
var srcFixed = imgs[i].getAttribute('src', 2).replace(/^https?\:/, document.___location.protocol);
var dy = mouse_pos.y - origin.y;
imgs[i].src = srcFixed;
// 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;
if ( msg.childNodes.length == 1
&& msg self.zoom.firstChild.nodeNamefirstChild.toLowerCasestyle.top = String( top ) ==+ 'apx';
self.zoom.firstChild.firstChild.style.left = String( left ) + 'px';
&& !LAPI.DOM.hasClass(msg.firstChild, 'image')
self.zoom.style.top = mouse_pos.y + 10 + 'px'; // Right below the mouse pointer
) {
// Horizontally keep it in view.
msg.firstChild.id = 'ImageAnnotationHelpButton';
var x = ( self.is_rtl ? mouse_pos.x - 10 : mouse_pos.x + 10 );
return msg.firstChild; // Single link
if ( x < 0 ) { x = 0; }
}
self.zoom.style.left = x + 'px';
// Otherwise, it's either a sequence of up to three images, or a span, followed by a
self.zoom.style.display = '';
// link.
// Now that we have offsetWidth, correct the position.
tgt = msg.lastChild;
if ( self.is_rtl ) {
if (tgt.nodeName.toLowerCase() != 'a')
x = mouse_pos.x - 10 - self.zoom.offsetWidth;
tgt = wgServer + wgArticlePath.replace('$1', 'Help:Gadget-ImageAnnotator');
if ( x < 0 ) { x = 0; }
else
} else {
tgt = tgt.href;
var off = LAPI.Pos.scrollOffset();
var view = LAPI.Pos.viewport();
if ( x + self.zoom.offsetWidth > off.x + view.x ) { x = off.x + view.x - self.zoom.offsetWidth; }
if ( x < 0 ) { x = 0; }
}
self.zoom.style.left = x + 'px';
},
 
hide_zoom: function ( evt ) {
function make_handler (tgt) {
if ( !IA.zoom ) { return; }
var target = tgt;
if ( evt ) {
return function (evt) {
var mouse_pos = LAPI.Pos.mousePosition( evt );
var e = evt || window.event;
if ( LAPI.Pos.isWithin( IA.cover, mouse_pos.x, mouse_pos.y ) ) { return; }
___location.href = target;
}
if (e) return LAPI.Evt.kill(e);
IA.zoom.style.display = 'none';
return false;
},
};
}
 
createHelpLink: function () {
imgs = msg.getElementsByTagName('img');
var msg = ImageAnnotator.UI.get( 'wpImageAnnotatorHelp', false, true );
if ( !msg || !msg.lastChild ) { return null; }
// Make sure we use the right protocol for all images:
var imgs = msg.getElementsByTagName( 'img' );
var text;
var tgt;
if ( imgs ) {
for ( var i = 0; i < imgs.length; i++ ) {
var srcFixed = imgs[ i ].getAttribute( 'src', 2 ).replace( /^https?:/, document.___location.protocol );
imgs[ i ].src = srcFixed;
}
}
if (
msg.childNodes.length == 1 &&
msg.firstChild.nodeName.toLowerCase() == 'a' &&
!LAPI.DOM.hasClass( msg.firstChild, 'image' )
) {
msg.firstChild.id = 'ImageAnnotationHelpButton';
return msg.firstChild; // Single link
}
// Otherwise, it's either a sequence of up to three images, or a span, followed by a
// link.
tgt = msg.lastChild;
if ( tgt.nodeName.toLowerCase() != 'a' ) { tgt = mw.config.get( 'wgServer' ) + mw.config.get( 'wgArticlePath' ).replace( '$1', 'Help:Gadget-ImageAnnotator' ); } else { tgt = tgt.href; }
 
function make_handler( tgt ) {
if (!imgs || !imgs.length) {
var target = tgt;
// We're supposed to have a spans giving the button text
return function ( evt ) {
text = msg.firstChild;
var e = evt || window.event;
if (text.nodeName.toLowerCase() === 'span')
___location.href = target;
text = LAPI.DOM.getInnerText(text);
if ( e ) { return LAPI.Evt.kill( e ); }
else
return false;
text = 'Help';
};
return LAPI.DOM.makeButton(
}
'ImageAnnotationHelpButton'
, text
, make_handler(tgt)
);
} else {
return Buttons.makeButton(imgs, 'ImageAnnotationHelpButton', make_handler(tgt));
}
},
 
imgs = msg.getElementsByTagName( 'img' );
get_cover : function ()
{
var self = IA;
var shim;
if (!self.cover) {
var pos = { position : 'absolute'
,left : '0px'
,top : '0px'
,width : self.viewers[0].thumb.width + 'px'
,height : self.viewers[0].thumb.height + 'px'
};
self.cover = LAPI.make('div', null, pos);
self.border = self.cover.cloneNode(false);
Object.merge(
{border: '3px solid green', top: '-3px', left: '-3px'}, self.border.style);
self.cover.style.zIndex = 2000; // Above the tooltips
if (LAPI.Browser.is_ie) {
shim = LAPI.make('iframe', {frameBorder: 0, tabIndex: -1}, pos);
shim.style.filter = 'alpha(Opacity=0)'; // Ensure transparency
// Unfortunately, IE6/SP2 has a "security setting" called "Binary and script
// behaviors". If that is disabled, filters don't work, and our iframe would
// appear as a white rectangle. Fix this by first placing the iframe just above
// image (to block that windowed control) and then placing *another div* just
// above that shim having the image as its background image.
var imgZ = self.viewers[0].img.style.zIndex;
if (isNaN (imgZ)) 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;
},
 
if ( !imgs || !imgs.length ) {
show_cover : function ()
// We're supposed to have a spans giving the button text
{
text = msg.firstChild;
var self = IA;
if ( text.nodeName.toLowerCase() === 'span' ) { text = LAPI.DOM.getInnerText( text ); } else { text = 'Help'; }
if (self.cover && !self.cover_visible) {
return LAPI.DOM.makeButton(
if (self.ieFix) {
'ImageAnnotationHelpButton'
self.viewers[0].img_div.appendChild(self.ieFix);
, text
self.viewers[0].img_div.appendChild(self.ieFix2);
, make_handler( tgt )
}
);
if (self.eventFix) self.viewers[0].img_div.appendChild(self.eventFix);
} else {
self.viewers[0].img_div.appendChild(self.cover);
return Buttons.makeButton( imgs, 'ImageAnnotationHelpButton', make_handler( tgt ) );
self.cover_visible = true;
}
}
},
 
hide_cover get_cover: function () {
var self = IA;
{
var self = IAshim;
if ( !self.cover && self.cover_visible) {
var pos = { position: 'absolute',
if (self.ieFix) {
left: '0px',
LAPI.DOM.removeNode(self.ieFix);
top: '0px',
LAPI.DOM.removeNode(self.ieFix2);
width: self.viewers[ 0 ].thumb.width + 'px',
}
height: self.viewers[ 0 ].thumb.height + 'px'
if (self.eventFix) LAPI.DOM.removeNode(self.eventFix);
};
LAPI.DOM.removeNode(self.cover);
self.cover = LAPI.make( 'div', null, pos );
self.cover_visible = false;
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;
},
 
getRawItem show_cover: function (what,) scope){
var self = IA;
{
if ( self.cover && !self.cover_visible ) {
var node = null;
if ( self.ieFix ) {
if (!scope || scope == document) {
self.viewers[ 0 ].img_div.appendChild( self.ieFix );
node = LAPI.$ ('image_annotation_' + what);
self.viewers[ 0 ].img_div.appendChild( self.ieFix2 );
} else {
}
node = getElementsByClassName (scope, '*', 'image_annotation_' + what);
if ( self.eventFix ) { self.viewers[ 0 ].img_div.appendChild( self.eventFix ); }
if (node && node.length) node = node[0]; else node = null;
self.viewers[ 0 ].img_div.appendChild( self.cover );
}
self.cover_visible = true;
return node;
}
},
},
 
getItem hide_cover: function (what,) scope){
var self = IA;
{
if ( self.cover && self.cover_visible ) {
var node = IA.getRawItem(what, scope);
if ( self.ieFix ) {
if (!node) return null;
return LAPI.DOM.getInnerTextremoveNode(node) self.trim(ieFix );
LAPI.DOM.removeNode( self.ieFix2 );
},
}
if ( self.eventFix ) { LAPI.DOM.removeNode( self.eventFix ); }
LAPI.DOM.removeNode( self.cover );
self.cover_visible = false;
}
},
 
getIntItem getRawItem: function ( what, scope ) {
var node = null;
{
if ( !scope || scope == document ) {
var x = IA.getItem(what, scope);
node = LAPI.$( 'image_annotation_' + what );
if (x !== null) x = parseInt (x, 10);
} else {
return x;
node = getElementsByClassName( scope, '*', 'image_annotation_' + what );
},
if ( node && node.length ) { node = node[ 0 ]; } else { node = null; }
}
return node;
},
 
findNote getItem: function (text what, idscope ) {
var node = IA.getRawItem( what, scope );
{
if ( !node ) { return null; }
function find (text, id, delim) {
return LAPI.DOM.getInnerText( node ).trim();
var start = delim.start.replace('$1', id);
},
var start_match = text.indexOf(start);
if (start_match < 0) return null;
var end = delim.end.replace('$1', id);
var end_match = text.indexOf(end);
if (end_match < start_match + start.length) return null;
return {start: start_match, end: end_match + end.length};
}
 
getIntItem: function ( what, scope ) {
var result = null;
var x = IA.getItem( what, scope );
for (var i=0; i < IA.note_delim.length && !result; i++) {
if ( x !== null ) result{ x = find parseInt(text, idx, IA.note_delim[i]10 ); }
return x;
}
},
return result;
},
 
setWikitext findNote: function (pagetext text, id ) {
function find( text, id, delim ) {
{
var start = delim.start.replace( '$1', id );
var self = IA;
var start_match = text.indexOf( start );
if (self.wiki_read) return;
if ( start_match < 0 ) { return null; }
Array.forEach(self.viewers[0].annotations, function (note) {
var end = delim.end.replace( '$1', id );
if (note.model.id >= 0) {
var end_match = text.indexOf( end );
var span = self.findNote(pagetext, note.model.id);
if ( end_match < start_match + start.length if (!span) { return null; }
return { start: start_match, end: end_match + end.length };
// 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;
},
 
var result = null;
setSummary : function (summary, initial_text, note_text)
for ( var i = 0; i < IA.note_delim.length && !result; i++ ) {
{
result = find( text, id, IA.note_delim[ i ] );
if (initial_text.contains('$1')) {
}
var max = (summary.maxlength || 200) - initial_text.length;
return result;
if (note_text)
},
initial_text =
initial_text.replace('$1', ': ' + note_text.replace('\n', ' ').substring(0, max));
else
initial_text = initial_text.replace('$1', '');
}
summary.value = initial_text;
},
 
getScript setWikitext: function (url, bypass_local_cache,pagetext bypass_caches) {
var self = IA;
{
if ( self.wiki_read ) { return; }
// Don't use LAPI here, it may not yet be available
Array.forEach( self.viewers[ 0 ].annotations, function ( note ) {
if (bypass_caches) {
if ( note.model.id >= 0 ) {
url += ((url.indexOf('?') >= 0) ? '&' : '?') + 'dummyTimestamp=' + (new Date()).getTime();
var span = self.findNote( pagetext, note.model.id );
}
if ( !span ) { return; }
// Avoid protocol-relative URIs (IE7 bug)
// Now extract the wikitext
if (url.length >= 2 && url.substring(0, 2) === '//') url = document.___location.protocol + url;
var code = pagetext.substring( span.start, span.end );
if (bypass_local_cache) {
for ( var i = 0; i < self.note_delim.length; i++ ) {
var s = document.createElement('script');
var start = self.note_delim[ i ].content_start.replace( '$1', note.model.id );
s.setAttribute('src', url);
var end = self.note_delim[ i ].content_end.replace( '$1', note.model.id );
s.setAttribute('type', 'text/javascript');
var j = code.indexOf( start );
document.getElementsByTagName('head')[0].appendChild(s);
var k = code.indexOf( end );
return s;
if ( j >= 0 && k >= 0 && k >= j + start.length ) {
} else {
note.model.wiki = code.substring( j + start.length, k ).trim();
return importScriptURI (url);
return;
}
}
},
}
}
} );
self.wiki_read = true;
},
 
canEdit setSummary: function ( summary, initial_text, note_text ) {
if ( initial_text.contains( '$1' ) ) {
{
var max = ( summary.maxlength || 200 ) - initial_text.length;
var self = IA;
if ( note_text ) {
if (self.may_edit) {
initial_text =
if (!self.ajaxQueried) {
initial_text.replace( '$1', ': ' + note_text.replace( '\n', ' ' ).substring( 0, max ) );
self.haveAjax = (LAPI.Ajax.getRequest() != null);
} else {
self.ajaxQueried = true;
initial_text = initial_text.replace( '$1', '' );
self.may_edit = self.haveAjax;
}
if (!self.may_edit && self.button_div) {
}
LAPI.DOM.removeChildren(self.button_div);
summary.value = initial_text;
self.button_div.appendChild
},
(ImageAnnotator.UI.get('wpImageAnnotatorCannotEditMsg', false));
self.viewers[0].msg.style.display = '';
self.viewers[0].cannotEdit();
}
}
}
return self.may_edit;
}
 
getScript: function ( url, bypass_local_cache, bypass_caches ) {
}; // end IA
// Don't use LAPI here, it may not yet be available
if ( bypass_caches ) {
url += ( ( url.indexOf( '?' ) >= 0 ) ? '&' : '?' ) + 'dummyTimestamp=' + ( new Date() ).getTime();
}
// Avoid protocol-relative URIs (IE7 bug)
if ( url.length >= 2 && url.substring( 0, 2 ) === '//' ) { url = document.___location.protocol + url; }
if ( bypass_local_cache ) {
var s = document.createElement( 'script' );
s.setAttribute( 'src', url );
s.setAttribute( 'type', 'text/javascript' );
document.getElementsByTagName( 'head' )[ 0 ].appendChild( s );
return s;
} else {
return mw.loader.load( url );
}
},
 
canEdit: function () {
// Backwards compatibility
var self = IA;
function getElementsByClassName (scope, tag, className) {
if (window self.jQuerymay_edit ) {
if ( !self.ajaxQueried ) {
return jQuery(scope).find(((!tag || tag === '*') ? '' : tag) + '.' + className);
self.haveAjax = ( LAPI.Ajax.getRequest() != null );
} else {
self.ajaxQueried = true;
// For non-WMF wikis that might not have jQuery (yet), use the wikibits.js getElementsByClassName
self.may_edit = self.haveAjax;
return getElementsByClassName(scope, tag, className);
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
window.ImageAnnotator = {
install: function (config) { IA.install(config); }
};
 
// Backwards compatibility
// Start it. Bypass caches; but allow for 4 hours client-side caching. Small file.
function getElementsByClassName( scope, tag, className ) {
IA.getScript(
if ( window.jQuery ) {
wgScript + '?title=MediaWiki:ImageAnnotatorConfig.js&action=raw&ctype=text/javascript'
return jQuery( scope ).find( ( ( !tag || tag === '*' ) ? '' : tag ) + '.' + className );
+ '&dummy=' + Math.floor((new Date()).getTime() / (14400 * 1000)) // 4 hours
} else {
, true // No local caching!
// For non-WMF wikis that might not have jQuery (yet), use the wikibits.js getElementsByClassName
);
return getElementsByClassName( scope, tag, className );
}
}
 
window.ImageAnnotator = {
})(); // end local scope
install: function ( config ) { IA.install( config ); }
};
 
// Start it. Bypass caches; but allow for 4 hours client-side caching. Small file.
} // end if (guard against double inclusions)
IA.getScript(
mw.config.get( 'wgScript' ) + '?title=MediaWiki:ImageAnnotatorConfig.js&action=raw&ctype=text/javascript',
true // No local caching!
);
 
}() ); // end local scope
// </source>
 
} // end if (guard against double inclusions)