MediaWiki:Gadget-ImageAnnotator.js: Difference between revisions

Content deleted Content added
&rawcontinue=
Per request
 
(8 intermediate revisions by 8 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,
/*jshint eqnull:true, laxbreak:true, laxcomma:true */
ImageAnnotator, ImageAnnotator_disable */
/* eslint-disable one-var, vars-on-top, camelcase, no-use-before-define,
eqeqeq, no-alert, no-loop-func, no-inner-declarations */
 
if ( typeof ImageAnnotator === 'undefined' ) { // Guard against multiple inclusions
 
importScript( 'MediaWiki:LAPI.js' );
importScript( 'MediaWiki:Tooltips.js' );
importScript( 'MediaWiki:TextCleaner.js' );
importScript( 'MediaWiki:UIElements.js' );
 
( function () { // Local scope
 
var ImageAnnotator_config = null;
 
var ImageAnnotation = function () {
this.initialize.apply( this, arguments );
};
 
ImageAnnotation.compare = function ( a, b ) {
var result = b.area() - a.area();
if ( result !== 0 ) { return result; }
// Just to make sure the order is complete
return a.model.id - b.model.id;
};
 
ImageAnnotation.prototype = {
// Rectangle to be displayed on image: a div with pos and size
view: null,
// Internal representation of the annotation
model: null,
// Tooltip to display the annotation
tooltip : null,
// Content of the tooltip
content : null,
// Reference to the viewer this note belongs to
viewer: null,
 
initialize: function ( node, viewer, id ) {
var is_new = false;
var view_w = 0, view_h = 0, view_x = 0, view_y = 0;
this.viewer = viewer;
if ( LAPI.DOM.hasClass( node, IA.annotation_class ) ) {
// Extract the info we need
var x = IA.getIntItem( 'view_x_' + id, viewer.scope );
var y = IA.getIntItem( 'view_y_' + id, viewer.scope );
var w = IA.getIntItem( 'view_w_' + id, viewer.scope );
var h = IA.getIntItem( 'view_h_' + id, viewer.scope );
var html = IA.getRawItem( 'content_' + id, viewer.scope );
if ( x === null || y === null || w === null || h === null || html === null ) { throw new Error( 'Invalid note' ); }
if ( x < 0 || x >= viewer.full_img.width || y < 0 || y >= viewer.full_img.height ) { throw new Error( 'Invalid note: origin invalid on note ' + id ); }
if (
if (x < 0 || x >= viewer.full_img.width || y < 0 || y >= viewer.full_img.height)
x + w > viewer.full_img.width + 10 ||
throw new Error('Invalid note: origin invalid on note ' + id);
if ( x y + wh > viewer.full_img.widthheight + 10
) {
|| y + h > viewer.full_img.height + 10)
throw new Error( 'Invalid note: size extends beyond image on note ' + id );
{
}
throw new Error('Invalid note: size extends beyond image on note ' + id);
// Notes written by early versions may be slightly too large, whence the + 10 above. Fix this.
}
if ( x + w > viewer.full_img.width ) { w = viewer.full_img.width - x; }
// Notes written by early versions may be slightly too large, whence the + 10 above. Fix this.
if (x y + wh > viewer.full_img.widthheight ) w{ h = viewer.full_img.widthheight - xy; }
view_w = Math.floor( w / viewer.factors.dx );
if (y + h > viewer.full_img.height) h = viewer.full_img.height - y;
view_w view_h = Math.floor(w h / viewer.factors.dxdy );
view_h view_x = Math.floor(h x / viewer.factors.dydx );
view_x view_y = Math.floor(x y / viewer.factors.dxdy );
this.view = LAPI.make(
view_y = Math.floor(y / viewer.factors.dy);
'div', null,
this.view =
{
LAPI.make(
position: 'absolute',
'div', null
display: 'none',
, { position: 'absolute'
lineHeight: '0px', // IE
,display: 'none'
,lineHeight fontSize: '0px', // IE
top: String( view_y ) + 'px',
,fontSize: '0px' // IE
,top left: ''String( +view_x view_y) + 'px',
,left width: ''String( +view_w view_x) + 'px',
,width height: ''String( +view_h view_w) + 'px'
}
,height: '' + view_h + 'px'
);
}
// We'll add the view to the DOM once we've loaded all notes
);
this.model = {
// We'll add the view to the DOM once we've loaded all notes
id: id,
this.model =
dimension: { x: x, y: y, w: w, { idh: idh },
wiki: '',
,dimension: {x: x, y: y, w: w, h: h}
html: html.cloneNode( true )
,wiki: ''
};
,html: html.cloneNode(true)
} else {
};
is_new = true;
} else {
this.view = node;
is_new = true;
this.viewmodel = node;{
id: -1,
this.model =
dimension: null,
{ id: -1
wiki: '',
,dimension: null
html: null
,wiki: ''
};
,html: null
view_w = this.view.offsetWidth - 2; // Subtract cumulated border widths
};
view_w view_h = this.view.offsetWidthoffsetHeight - 2; // Subtract cumulated border widths
view_h view_x = this.view.offsetHeight - 2offsetLeft;
view_x view_y = this.view.offsetLeftoffsetTop;
}
view_y = this.view.offsetTop;
// Enforce a minimum size of the view. Center the 6x6px square over the center of the old view.
}
// If we overlap the image boundary, adjustRectangleSize will take care of it later.
// Enforce a minimum size of the view. Center the 6x6px square over the center of the old view.
if ( view_w < 6 ) { view_x = Math.floor( view_x + view_w / 2 - 3 ); view_w = 6; }
// If we overlap the image boundary, adjustRectangleSize will take care of it later.
if (view_w view_h < 6 ) {view_x view_y = Math.floor(view_x view_y + view_wview_h / 2 - 3 ); view_wview_h = 6; }
Object.merge(
if (view_h < 6) {view_y = Math.floor(view_y + view_h / 2 - 3); view_h = 6; }
{
Object.merge(
left: String( view_x ) + 'px',
{
left top: ''String( +view_y view_x) + 'px',
top width: ''String( +view_w view_y) + 'px',
width height: ''String( +view_h view_w) + 'px',
},
height: '' + view_h + 'px'
this.view.style
},
);
this.view.style
this.view.style.zIndex = 500; // Below tooltips
);
try {
this.view.style.zIndex = 500; // Below tooltips
this.view.style.border = '1px solid ' + this.viewer.outer_border;
try {
} catch ( ex ) {
this.view.style.border = '1px solid ' + this.viewer.outer_border;
this.view.style.border = '1px solid ' + IA.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: 'div0px', null// IE
, { lineHeight fontSize: '0px', // IE
width: String( Math.max( view_w - 2, 0 ) ) + 'px', // -2 to leave space for the border
,fontSize: '0px' // IE
,width height: '' +String( Math.max(view_w view_h - 2, 0 ) ) + 'px' // -2 to leave space for the border
}
,height: '' + 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 ' + this.viewer.inner_border;
this.view.firstChild.style.border = '1px solid ' + IA.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
// targets and we have to show and hide them ourselves through a mousemove listener in the viewer
// anyway. The occasional event that IE sends to the tooltip may then lead to ugly flickering.
this.tooltip = new Tooltip(
( this.view.firstChild,
, this.display.bind( this ),
{
, { activate: (LAPI.DOM.is_ie ? Tooltip.NONE : Tooltip.HOVER)
,deactivate activate: ( LAPI.DOM.is_ie ? Tooltip.ESCAPENONE : Tooltip.LEAVEHOVER ),
deactivate: ( LAPI.DOM.is_ie ? Tooltip.ESCAPE : Tooltip.LEAVE ),
,close_button: null
close_button: null,
,mode: Tooltip.MOUSE
mode: Tooltip.MOUSE,
,mouse_offset : {x: -5, y: -5, dx: (IA.is_rtl ? -1 : 1), dy: 1}
mouse_offset: { x: -5, y: -5, dx: ( IA.is_rtl ? -1 : 1 ), dy: 1 },
,open_delay: 0
open_delay: 0,
,hide_delay: 0
hide_delay: 0,
,onclose: (function (tooltip, evt) {
onclose: ( function ( tooltip, evt ) {
if (this.view) {
if ( this.view ) {
try {
try {
this.view.style.border = '1px solid ' + this.viewer.outer_border;
this.view.style.border = '1px solid ' + this.viewer.outer_border;
} catch (ex) {
} catch ( ex ) {
this.view.style.border = '1px solid ' + IA.outer_border;
this.view.style.border = '1px solid ' + IA.outer_border;
}
}
}
}
if (this.viewer.tip == tooltip) this.viewer.tip = null;
if ( this.viewer.tip == tooltip ) { this.viewer.tip = null; }
// Hide all boxes if we're outside the image. Relies on hide checking the
// Hide all boxes if we're outside the image. Relies on hide checking the
// coordinates! (Otherwise, we'd always hide...)
// coordinates! (Otherwise, we'd always hide...)
if (evt) this.viewer.hide(evt);
if ( evt ) { this.viewer.hide( evt ); }
}).bind(this)
} ).bind( this ),
,onopen: (function (tooltip) {
onopen: ( function ( tooltip ) {
if (this.view) {
if ( this.view ) {
try {
try {
this.view.style.border = '1px solid ' + this.viewer.active_border;
this.view.style.border = '1px solid ' + this.viewer.active_border;
} catch (ex) {
} catch ( ex ) {
this.view.style.border = '1px solid ' + IA.active_border;
this.view.style.border = '1px solid ' + IA.active_border;
}
}
}
}
this.viewer.tip = tooltip;
this.viewer.tip = tooltip;
}).bind(this)
} ).bind( this )
}
},
, IA.tooltip_styles
IA.tooltip_styles
);
);
},
},
 
display: function ( evt ) {
if ( !this.content ) {
this.content = LAPI.make( 'div' );
var main = LAPI.make( 'div' );
this.content.appendChild( main );
this.content.main = main;
if ( this.model.html ) { main.appendChild( this.model.html.cloneNode( true ) ); }
// Make sure that the popup encompasses all floats
this.content.appendChild( LAPI.make( 'div', null, { clear: 'both' } ) );
if ( this.viewer.may_edit ) {
this.content.button_section = LAPI.make(
'div',
LAPI.make(
null,
'div'
{
,null
,{ fontSize : 'smaller',
, textAlign: ( IA.is_rtl ? 'left' : 'right' ),
, borderTop: IA.tooltip_styles.border
}
}
);
);
this.content.appendChild( this.content.button_section );
this.content.button_section.appendChild( LAPI.DOM.makeLink(
'#',
'#'
, ImageAnnotator.UI.get( 'wpImageAnnotatorEdit', true ),
null,
, null
, LAPI.Evt.makeListener( this, this.edit )
) );
)
if ( ImageAnnotator_config.mayDelete() ) {
);
this.content.button_section.appendChild( document.createTextNode( '\xa0' ) );
if (ImageAnnotator_config.mayDelete()) {
this.content.button_section.appendChild(document LAPI.DOM.createTextNodemakeLink('\xa0'));
'#',
this.content.button_section.appendChild
ImageAnnotator.UI.get( 'wpImageAnnotatorDelete', true ),
(LAPI.DOM.makeLink
null,
( '#'
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 ); }
return false;
},
 
remove_event: function ( evt ) {
if ( IA.canEdit() ) { this.remove(); }
return LAPI.Evt.kill( evt );
},
 
remove: function () {
if ( !this.content ) { // New note: just destroy it.
this.destroy();
return true;
}
}
if ( !ImageAnnotator_config.mayDelete() ) { return false; }
 
// Close and remove tooltip only if edit succeeded! Where and how to display error messages?
 
var reason = '';
if ( !ImageAnnotator_config.mayBypassDeletionPrompt() || !window.ImageAnnotator_noDeletionPrompt ) {
// Prompt for a removal reson
reason = prompt( ImageAnnotator.UI.get( 'wpImageAnnotatorDeleteReason', true ), '' );
if ( reason === null ) { return false; } // Cancelled
reason = reason.trim();
if ( !reason.length ) {
if ( !ImageAnnotator_config.emptyDeletionReasonAllowed() ) { return false; }
}
}
// Re-show tooltip (without re-positioning it, we have no mouse coordinates here) in case
// it was hidden because of the alert. If possible, we want the user to see the spinner.
this.tooltip.show_now( this.tooltip );
}
}
 
var self = this;
var spinnerId = 'image_annotation_delete_' + this.model.id;
LAPI.Ajax.injectSpinner( this.content.button_section.lastChild, spinnerId );
if ( this.tooltip ) { this.tooltip.size_change(); }
LAPI.Ajax.editPage(
mw.config.get( 'wgPageName' ),
function ( doc, editForm, failureFunc, revision_id ) {
try {
if ( revision_id && revision_id != mw.config.get( 'wgCurRevisionId' ) ) { throw new Error( '#Page version (revision ID) mismatch: edit conflict.' ); }
 
var textbox = editForm.wpTextbox1;
var self = this;
if ( !textbox ) { throw new Error( '#Server replied with invalid edit page.' ); }
var spinnerId = 'image_annotation_delete_' + this.model.id;
var pagetext = textbox.value.replace( /\r\n/g, '\n' );
LAPI.Ajax.injectSpinner(this.content.button_section.lastChild, spinnerId);
// Normalize different end-of-line handling. Opera and IE may use \r\n, whereas other
if (this.tooltip) this.tooltip.size_change();
// browsers just use '\n'. Note that all browsers do the right thing if a '\n' is added.
LAPI.Ajax.editPage(
// We normally don't care, but here we need this to make sure we don't leave extra line
mw.config.get('wgPageName')
// breaks when we remove the note.
, function (doc, editForm, failureFunc, revision_id) {
try {
if (revision_id && revision_id != mw.config.get('wgCurRevisionId'))
throw new Error('#Page version (revision ID) mismatch: edit conflict.');
 
IA.setWikitext( pagetext );
var textbox = editForm.wpTextbox1;
if (!textbox) throw new Error('#Server replied with invalid edit page.');
var pagetext = textbox.value.replace(/\r\n/g, '\n');
// Normalize different end-of-line handling. Opera and IE may use \r\n, whereas other
// browsers just use '\n'. Note that all browsers do the right thing if a '\n' is added.
// We normally don't care, but here we need this to make sure we don't leave extra line
// breaks when we remove the note.
 
var span = IA.setWikitextfindNote( pagetext, self.model.id );
if ( !span ) { // Hmmm? Doesn't seem to exist
LAPI.Ajax.removeSpinner( spinnerId );
if ( self.tooltip ) { self.tooltip.size_change(); }
self.destroy();
return;
}
var char_before = 0;
var char_after = 0;
if ( span.start > 0 ) { char_before = pagetext.charCodeAt( span.start - 1 ); }
if ( span.end < pagetext.length ) { char_after = pagetext.charCodeAt( span.end ); }
if (
String.fromCharCode( char_before ) == '\n' &&
String.fromCharCode( char_after ) == '\n'
) {
span.start = span.start - 1;
}
pagetext = pagetext.substring( 0, span.start ) + pagetext.substring( span.end );
textbox.value = pagetext;
var summary = editForm.wpSummary;
if ( !summary ) {
throw new Error( '#Summary field not found. Check that edit pages have valid XHTML.' );
}
IA.setSummary(
summary,
ImageAnnotator.UI.get( 'wpImageAnnotatorRemoveSummary', true ) ||
'[[MediaWiki talk:Gadget-ImageAnnotator.js|Removing image note]]$1',
( reason.length ? reason + ': ' : '' ) + self.model.wiki
);
} catch ( ex ) {
failure( null, ex );
return;
}
var edit_page = doc;
LAPI.Ajax.submitEdit(
editForm,
function ( request ) {
if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) { edit_page.dispose(); }
var revision_id = LAPI.WP.revisionFromHtml( request.responseText );
if ( !revision_id ) {
failureFunc( request, new Error( 'Revision ID not found. Please reload the page.' ) );
return;
}
mw.config.set( 'wgCurRevisionId', revision_id ); // Bump revision id!!
LAPI.Ajax.removeSpinner( spinnerId );
if ( self.tooltip ) { self.tooltip.size_change(); }
self.destroy();
},
function ( request, ex ) {
if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) { edit_page.dispose(); }
failureFunc( request, ex );
}
);
},
function ( request, ex ) {
// Failure. What now? TODO: Implement some kind of user feedback.
LAPI.Ajax.removeSpinner( spinnerId );
if ( self.tooltip ) { self.tooltip.size_change(); }
}
);
 
return true;
var span = IA.findNote(pagetext, self.model.id);
},
if (!span) { // Hmmm? Doesn't seem to exist
LAPI.Ajax.removeSpinner(spinnerId);
if (self.tooltip) self.tooltip.size_change();
self.destroy();
return;
}
var char_before = 0;
var char_after = 0;
if (span.start > 0) char_before = pagetext.charCodeAt(span.start - 1);
if (span.end < pagetext.length) char_after = pagetext.charCodeAt(span.end);
if ( String.fromCharCode(char_before) == '\n'
&& String.fromCharCode(char_after) == '\n')
span.start = span.start - 1;
pagetext = pagetext.substring(0, span.start) + pagetext.substring(span.end);
textbox.value = pagetext;
var summary = editForm.wpSummary;
if (!summary)
throw new Error('#Summary field not found. Check that edit pages have valid XHTML.');
IA.setSummary(
summary
, ImageAnnotator.UI.get('wpImageAnnotatorRemoveSummary', true)
|| '[[MediaWiki talk:Gadget-ImageAnnotator.js|Removing image note]]$1'
, (reason.length ? reason + ': ' : '') + self.model.wiki
);
} catch (ex) {
failure (null, ex);
return;
}
var edit_page = doc;
LAPI.Ajax.submitEdit(
editForm
, function (request) {
if (edit_page.isFake && (typeof edit_page.dispose === 'function'))
edit_page.dispose();
var revision_id = LAPI.WP.revisionFromHtml(request.responseText);
if (!revision_id) {
failureFunc (request, new Error('Revision ID not found. Please reload the page.'));
return;
}
mw.config.set('wgCurRevisionId', revision_id); // Bump revision id!!
LAPI.Ajax.removeSpinner(spinnerId);
if (self.tooltip) self.tooltip.size_change();
self.destroy();
}
, function (request, ex) {
if (edit_page.isFake && (typeof edit_page.dispose === 'function'))
edit_page.dispose();
failureFunc (request, ex);
}
);
}
, function (request, ex) {
// Failure. What now? TODO: Implement some kind of user feedback.
LAPI.Ajax.removeSpinner(spinnerId);
if (self.tooltip) self.tooltip.size_change();
}
);
 
destroy: function () {
return true;
if ( this.view ) { LAPI.DOM.removeNode( this.view ); }
},
if ( this.dummy ) { LAPI.DOM.removeNode( this.dummy ); }
if ( this.tooltip ) { this.tooltip.hide_now(); }
if ( this.model && this.model.id > 0 && this.viewer ) { this.viewer.deregister( this ); }
this.model = null;
this.view = null;
this.content = null;
this.tooltip = null;
this.viewer = null;
},
 
destroy area: function () {
if ( !this.view)model LAPI.DOM.removeNode(|| !this.viewmodel.dimension ) { return 0; }
if return ( this.dummy) LAPImodel.DOMdimension.removeNode(w * this.dummymodel.dimension.h );
},
if (this.tooltip) this.tooltip.hide_now();
if (this.model && this.model.id > 0 && this.viewer) this.viewer.deregister(this);
this.model = null;
this.view = null;
this.content = null;
this.tooltip = null;
this.viewer = null;
},
 
area cannotEdit: function () {
if (! this.modelcontent ||&& !this.modelcontent.dimensionbutton_section ) return 0;{
LAPI.DOM.removeNode( this.content.button_section );
return (this.model.dimension.w * this.model.dimension.h);
this.content.button_section = null;
},
if ( this.tooltip ) { this.tooltip.size_change(); }
}
}
 
}; // end ImageAnnotation
cannotEdit: function () {
if (this.content && this.content.button_section) {
LAPI.DOM.removeNode(this.content.button_section);
this.content.button_section = null;
if (this.tooltip) this.tooltip.size_change();
}
}
 
var ImageAnnotationEditor = function () { this.initialize.apply( this, arguments ); };
}; // end ImageAnnotation
 
var ImageAnnotationEditor.prototype = function () {this.initialize.apply(this, arguments);};
initialize: function () {
var editor_width = 50;
// Respect potential user-defined width setting
if ( window.ImageAnnotationEditor_columns &&
!isNaN( window.ImageAnnotationEditor_columns ) &&
window.ImageAnnotationEditor_columns >= 30 &&
window.ImageAnnotationEditor_columns <= 100 ) {
editor_width = window.ImageAnnotationEditor_columns;
}
this.editor = new LAPI.Edit(
'', editor_width, 6,
{
box: ImageAnnotator.UI.get( 'wpImageAnnotatorEditorLabel', false ),
preview: ImageAnnotator.UI.get( 'wpImageAnnotatorPreview', true ).capitalizeFirst(),
save: ImageAnnotator.UI.get( 'wpImageAnnotatorSave', true ).capitalizeFirst(),
revert: ImageAnnotator.UI.get( 'wpImageAnnotatorRevert', true ).capitalizeFirst(),
cancel: ImageAnnotator.UI.get( 'wpImageAnnotatorCancel', true ).capitalizeFirst(),
nullsave: ImageAnnotator_config.mayDelete() ?
ImageAnnotator.UI.get( 'wpImageAnnotatorDelete', true ).capitalizeFirst() :
null,
post: ImageAnnotator.UI.get( 'wpImageAnnotatorCopyright', false )
},
{
onsave: this.save.bind( this ),
onpreview: this.onpreview.bind( this ),
oncancel: this.cancel.bind( this ),
ongettext: function ( text ) {
if ( text == null ) { return ''; }
text = text.trim()
.replace( /\{\{(\s*ImageNote(End)?\s*\|)/g, '&#x7B;&#x7B;$1' );
// Guard against people trying to break notes on purpose
if ( text.length && typeof TextCleaner !== 'undefined' ) { text = TextCleaner.sanitizeWikiText( text, true ); }
return text;
}
}
);
this.box = LAPI.make( 'div' );
this.box.appendChild( this.editor.getView() );
// Limit the width of the bounding box to the size of the textarea, taking into account the
// tooltip styles. Do *not* simply append this.box or the editor view, Opera behaves strangely
// if textboxes were ever hidden through a visibility setting! Use a second throw-away textbox
// instead.
var temp = LAPI.make( 'div', null, IA.tooltip_styles );
temp.appendChild( LAPI.make( 'textarea', { cols: editor_width, rows: 6 } ) );
Object.merge(
{
position: 'absolute',
top: '0px',
left: '-10000px',
visibility: 'hidden'
},
temp.style
);
document.body.appendChild( temp );
// Now we know how wide this textbox will be
var box_width = temp.offsetWidth;
LAPI.DOM.removeNode( temp );
// Note: we need to use a tooltip with a dynamic content creator function here because
// static content is cloned inside the Tooltip. Cloning on IE loses all attached handlers,
// and thus the editor's controls wouldn't work anymore. (This is not a problem on FF3,
// where cloning preserves the handlers.)
this.tooltip = new Tooltip(
IA.get_cover(),
this.get_editor.bind( this ),
{
activate: Tooltip.NONE, // We'll always show it explicitly
deactivate: Tooltip.ESCAPE,
close_button: null, // We have a cancel button anyway
mode: Tooltip.FIXED,
anchor: Tooltip.TOP_LEFT,
mouse_offset: { x: 10, y: 10, dx: 1, dy: 1 }, // Misuse this: fixed offset from view
max_pixels: ( box_width ? box_width + 20 : 0 ), // + 20 gives some slack
z_index: 2010, // Above the cover.
open_delay: 0,
hide_delay: 0,
onclose: this.close_tooltip.bind( this )
},
IA.tooltip_styles
);
this.note = null;
this.visible = false;
LAPI.Evt.listenTo( this, this.tooltip.popup, IA.mouse_in,
function ( 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';
var same_note = (note == this.note);
IA.show_cover();
this.note = note;
this.viewer = this.note.viewer;
 
if ( note.tooltip ) { note.tooltip.hide_now(); }
var cover = IA.get_cover();
cover.style.cursor = 'auto';
IA.show_cover();
 
IA.is_editing = true;
if (note.tooltip) note.tooltip.hide_now();
if ( note.content && !IA.wiki_read ) {
// Existing note, and we don't have the wikitext yet: go get it
var self = this;
LAPI.Ajax.apiGet(
'query',
{
prop: 'revisions',
titles: mw.config.get( 'wgPageName' ),
rvlimit: 1,
rvstartid: mw.config.get( 'wgCurRevisionId' ),
rvprop: 'ids|content'
},
function ( request, json_result ) {
if ( json_result && json_result.query && json_result.query.pages ) {
// Should have only one page here
for ( var page in json_result.query.pages ) {
var p = json_result.query.pages[ page ];
if ( p && p.revisions && p.revisions.length ) {
var rev = p.revisions[ 0 ];
if ( rev.revid == mw.config.get( 'wgCurRevisionId' ) && rev[ '*' ] && rev[ '*' ].length ) { IA.setWikitext( rev[ '*' ] ); }
}
break;
}
}
// TODO: What upon a failure?
self.open_editor( same_note, cover );
},
function ( request ) {
// TODO: What upon a failure?
self.open_editor( same_note, cover );
}
);
} else {
this.open_editor( same_note, cover );
}
},
 
open_editor: function ( same_note, cover ) {
IA.is_editing = true;
this.editor.hidePreview();
if (note.content && !IA.wiki_read) {
if ( !same_note || this.editor.textarea.readOnly ) {
// Existing note, and we don't have the wikitext yet: go get it
// Different note, or save error last time
var self = this;
this.editor.setText( this.note.model.wiki );
LAPI.Ajax.apiGet(
}
'query'
this.editor.enable( LAPI.Edit.SAVE + LAPI.Edit.PREVIEW + LAPI.Edit.REVERT + LAPI.Edit.CANCEL );
, { prop: 'revisions'
this.editor.textarea.readOnly = false;
,titles: mw.config.get('wgPageName')
this.editor.textarea.style.backgroundColor = 'white';
,rvlimit: 1
// Set the position relative to the note's view.
,rvstartid : mw.config.get('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 == mw.config.get('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.editor.hidePreview();
this.visible = false;
if (!same_note || this.editor.textarea.readOnly)
IA.is_editing = false;
// Different note, or save error last time
this.tooltip.hide_now( evt );
this.editor.setText(this.note.model.wiki);
if ( evt && evt.type == 'keydown' && !this.saving ) {
this.editor.enable
// ESC pressed on new note before a save attempt
(LAPI.Edit.SAVE + LAPI.Edit.PREVIEW + LAPI.Edit.REVERT + LAPI.Edit.CANCEL);
this.cancel();
this.editor.textarea.readOnly = false;
}
this.editor.textarea.style.backgroundColor = 'white';
IA.hide_cover();
// Set the position relative to the note's view.
this.viewer.setDefaultMsg();
var view_pos = LAPI.Pos.position(this.note.view);
this.viewer.setShowHideEvents( true );
var origin = LAPI.Pos.position(cover);
this.viewer.hide();
this.tooltip.options.fixed_offset.x =
this.viewer.show(); // Make sure we get the real views again.
view_pos.x - origin.x + this.tooltip.options.mouse_offset.x;
// FIXME in Version 2.1: Unfortunately, we don't have a mouse position here, so sometimes we
this.tooltip.options.fixed_offset.y =
// may show the note rectangles even though the mouse is now outside the image. (It was
view_pos.y - origin.y + this.tooltip.options.mouse_offset.y;
// somewhere inside the editor in most cases (if an editor button was clicked), but if ESC was
this.tooltip.options.fixed_offset.dx = 1;
// pressed, it may actually be anywhere.)
this.tooltip.options.fixed_offset.dy = 1;
},
// Make sure mouse event listeners are removed, especially on IE.
this.dim = { x : this.note.view.offsetLeft, y : this.note.view.offsetTop
,w : this.note.view.offsetWidth, h : this.note.view.offsetHeight };
this.viewer.setShowHideEvents(false);
this.viewer.hide(); // Make sure notes are hidden
this.viewer.toggle(true); // Show all note rectangles (but only the dummies)
// Now show the editor
this.tooltip.show_tip(null, false);
var tpos = LAPI.Pos.position(this.editor.textarea);
var ppos = LAPI.Pos.position(this.tooltip.popup);
tpos = tpos.x - ppos.x;
if (tpos + this.editor.textarea.offsetWidth > this.tooltip.popup.offsetWidth)
this.editor.textarea.style.width = (this.tooltip.popup.offsetWidth - 2 * tpos) + 'px';
if (LAPI.Browser.is_ie) {
// Fixate textarea width to prevent ugly flicker on each keypress in IE6...
this.editor.textarea.style.width = this.editor.textarea.offsetWidth + 'px';
}
this.visible = true;
},
 
hide_editor save: function (evt editor ) {
var data = editor.getText();
if (!this.visible) return;
if ( !data || !data.length ) {
this.visible = false;
// Empty text
IA.is_editing = false;
if ( this.tooltipnote.hide_nowremove(evt); ) {
this.hide_editor();
if (evt && evt.type == 'keydown' && !this.saving) {
this.cancel();
// ESC pressed on new note before a save attempt
this.note = null;
this.cancel();
} else {
}
this.hide_editor();
IA.hide_cover();
this.viewer.setDefaultMsgcancel();
}
this.viewer.setShowHideEvents(true);
return;
this.viewer.hide();
} else if ( data == this.note.model.wiki ) {
this.viewer.show(); // Make sure we get the real views again.
// Text unchanged
// FIXME in Version 2.1: Unfortunately, we don't have a mouse position here, so sometimes we
this.hide_editor();
// may show the note rectangles even though the mouse is now outside the image. (It was
this.cancel();
// somewhere inside the editor in most cases (if an editor button was clicked), but if ESC was
return;
// pressed, it may actually be anywhere.)
}
},
// Construct what to insert
var dim = Object.clone( this.note.model.dimension );
if ( !dim ) {
dim = {
x: Math.round( this.dim.x * this.viewer.factors.dx ),
y: Math.round( this.dim.y * this.viewer.factors.dy ),
w: Math.round( this.dim.w * this.viewer.factors.dx ),
h: Math.round( this.dim.h * this.viewer.factors.dy )
};
// Make sure everything is within bounds
if ( dim.x + dim.w > this.viewer.full_img.width ) {
if ( dim.w > this.dim.w * this.viewer.factors.dx ) {
dim.w--;
if ( dim.x + dim.w > this.viewer.full_img.width ) {
if ( dim.x > 0 ) { dim.x--; } else { dim.w = this.viewer.full_img.width; }
}
} else {
// Width already was rounded down
if ( dim.x > 0 ) { dim.x--; }
}
}
if ( dim.y + dim.h > this.viewer.full_img.height ) {
if ( dim.h > this.dim.h * this.viewer.factors.dy ) {
dim.h--;
if ( dim.y + dim.h > this.viewer.full_img.height ) {
if ( dim.y > 0 ) { dim.y--; } else { dim.h = this.viewer.full_img.height; }
}
} else {
// Height already was rounded down
if ( dim.y > 0 ) { dim.y--; }
}
}
// If still too large, adjust width and height
if ( dim.x + dim.w > this.viewer.full_img.width ) {
if ( this.viewer.full_img.width > dim.x ) {
dim.w = this.viewer.full_img.width - dim.x;
} else {
dim.x = this.viewer.full_img.width - 1;
dim.w = 1;
}
}
if ( dim.y + dim.h > this.viewer.full_img.height ) {
if ( this.viewer.full_img.height > dim.y ) {
dim.h = this.viewer.full_img.height - dim.y;
} else {
dim.y = this.viewer.full_img.height - 1;
dim.h = 1;
}
}
}
this.to_insert =
'{{ImageNote' +
'|id=' + this.note.model.id +
'|x=' + dim.x + '|y=' + dim.y + '|w=' + dim.w + '|h=' + dim.h +
'|dimx=' + this.viewer.full_img.width +
'|dimy=' + this.viewer.full_img.height +
'|style=2' +
'}}\n' +
data + ( data.endsWith( '\n' ) ? '' : '\n' ) +
'{{ImageNoteEnd|id=' + this.note.model.id + '}}';
// Now edit the page
var self = this;
this.editor.busy( true );
this.editor.enable( 0 ); // Disable all buttons
this.saving = true;
LAPI.Ajax.editPage(
mw.config.get( 'wgPageName' ),
function ( doc, editForm, failureFunc, revision_id ) {
try {
if ( revision_id && revision_id != mw.config.get( 'wgCurRevisionId' ) ) {
// Page was edited since the user loaded it.
throw new Error( '#Page version (revision ID) mismatch: edit conflict.' );
}
 
// Modify the page
save: function (editor) {
var datatextbox = editoreditForm.getText()wpTextbox1;
if ( !textbox ) { throw new Error( '#Server replied with invalid edit page.' ); }
if (!data || !data.length) {
var pagetext = textbox.value;
// Empty text
if (this.note.remove()) {
this.hide_editor();
this.cancel();
this.note = null;
} else {
this.hide_editor();
this.cancel();
}
return;
} else if (data == this.note.model.wiki) {
// Text unchanged
this.hide_editor();
this.cancel();
return;
}
// Construct what to insert
var dim = Object.clone(this.note.model.dimension);
if (!dim) {
dim = {
x : Math.round(this.dim.x * this.viewer.factors.dx),
y : Math.round(this.dim.y * this.viewer.factors.dy),
w : Math.round(this.dim.w * this.viewer.factors.dx),
h : Math.round(this.dim.h * this.viewer.factors.dy)
};
// Make sure everything is within bounds
if (dim.x + dim.w > this.viewer.full_img.width) {
if (dim.w > this.dim.w * this.viewer.factors.dx) {
dim.w--;
if (dim.x + dim.w > this.viewer.full_img.width) {
if (dim.x > 0) dim.x--; else dim.w = this.viewer.full_img.width;
}
} else {
// Width already was rounded down
if (dim.x > 0) dim.x--;
}
}
if (dim.y + dim.h > this.viewer.full_img.height) {
if (dim.h > this.dim.h * this.viewer.factors.dy) {
dim.h--;
if (dim.y + dim.h > this.viewer.full_img.height) {
if (dim.y > 0) dim.y--; else dim.h = this.viewer.full_img.height;
}
} else {
// Height already was rounded down
if (dim.y > 0) dim.y--;
}
}
// If still too large, adjust width and height
if (dim.x + dim.w > this.viewer.full_img.width) {
if (this.viewer.full_img.width > dim.x) {
dim.w = this.viewer.full_img.width - dim.x;
} else {
dim.x = this.viewer.full_img.width - 1;
dim.w = 1;
}
}
if (dim.y + dim.h > this.viewer.full_img.height) {
if (this.viewer.full_img.height > dim.y) {
dim.h = this.viewer.full_img.height - dim.y;
} else {
dim.y = this.viewer.full_img.height - 1;
dim.h = 1;
}
}
}
this.to_insert =
'{{ImageNote'
+ '|id=' + this.note.model.id
+ '|x=' + dim.x + '|y=' + dim.y + '|w=' + dim.w + '|h=' + dim.h
+ '|dimx=' + this.viewer.full_img.width
+ '|dimy=' + this.viewer.full_img.height
+ '|style=2'
+ '}}\n'
+ data + (data.endsWith('\n') ? '' : '\n')
+ '{{ImageNoteEnd|id=' + this.note.model.id + '}}';
// Now edit the page
var self = this;
this.editor.busy(true);
this.editor.enable(0); // Disable all buttons
this.saving = true;
LAPI.Ajax.editPage(
mw.config.get('wgPageName')
, function (doc, editForm, failureFunc, revision_id) {
try {
if (revision_id && revision_id != mw.config.get('wgCurRevisionId'))
// Page was edited since the user loaded it.
throw new Error('#Page version (revision ID) mismatch: edit conflict.');
 
IA.setWikitext( pagetext );
// Modify the page
var textbox = editForm.wpTextbox1;
if (!textbox) throw new Error('#Server replied with invalid edit page.');
var pagetext = textbox.value;
 
var span = null;
IA.setWikitext(pagetext);
if ( self.note.content ) { // Otherwise it's a new note!
span = IA.findNote( pagetext, self.note.model.id );
}
if ( span ) { // Replace
pagetext =
pagetext.substring( 0, span.start ) +
self.to_insert +
pagetext.substring( span.end );
} else { // If not found, append
// Try to append right after existing notes
var lastNote = pagetext.lastIndexOf( '{{ImageNoteEnd|id=' );
if ( lastNote >= 0 ) {
var endLastNote = pagetext.substring( lastNote ).indexOf( '}}' );
if ( endLastNote < 0 ) {
endLastNote = pagetext.substring( lastNote ).indexOf( '\n' );
if ( endLastNote < 0 ) { lastNote = -1; } else { lastNote += endLastNote; }
} else { lastNote += endLastNote + 2; }
}
if ( lastNote >= 0 ) {
pagetext =
pagetext.substring( 0, lastNote ) +
'\n' + self.to_insert +
pagetext.substring( lastNote );
} else { pagetext = pagetext.trimRight() + '\n' + self.to_insert; }
}
textbox.value = pagetext;
var summary = editForm.wpSummary;
if ( !summary ) { throw new Error( '#Summary field not found. Check that edit pages have valid XHTML.' ); }
// If [[MediaWiki:Copyrightwarning]] is invalid XHTML, we may not have wpSummary!
if ( self.note.content != null ) {
IA.setSummary(
summary,
ImageAnnotator.UI.get( 'wpImageAnnotatorChangeSummary', true ) ||
'[[MediaWiki talk:Gadget-ImageAnnotator.js|Changing image note]]$1',
data
);
} else {
IA.setSummary(
summary,
ImageAnnotator.UI.get( 'wpImageAnnotatorAddSummary', true ) ||
'[[MediaWiki talk:Gadget-ImageAnnotator.js|Adding image note]]$1',
data
);
}
} catch ( ex ) {
failureFunc( null, ex );
return;
}
var edit_page = doc;
LAPI.Ajax.submitEdit(
editForm,
function ( request ) {
// After a successful submit.
if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) { edit_page.dispose(); }
// TODO: Actually, the edit got through here, so calling failureFunc on
// inconsistencies isn't quite right. Should we reload the page?
var id = 'image_annotation_content_' + self.note.model.id;
var doc = LAPI.Ajax.getHTML( request, failureFunc, id );
if ( !doc ) { return; }
var html = LAPI.$( id, doc );
if ( !html ) {
if ( doc.isFake && ( typeof doc.dispose === 'function' ) ) { doc.dispose(); }
failureFunc( request, new Error( '#Note not found after saving. Please reload the page.' ) );
return;
}
var revision_id = LAPI.WP.revisionFromHtml( request.responseText );
if ( !revision_id ) {
if ( doc.isFake && ( typeof doc.dispose === 'function' ) ) { doc.dispose(); }
failureFunc( request, new Error( '#Version inconsistency after saving. Please reload the page.' ) );
return;
}
mw.config.set( 'wgCurRevisionId', revision_id ); // Bump revision id!!
self.note.model.html = LAPI.DOM.importNode( document, html, true );
if ( doc.isFake && ( typeof doc.dispose === 'function' ) ) { doc.dispose(); }
self.note.model.dimension = dim; // record dimension
self.note.model.html.style.display = '';
self.note.model.wiki = data;
self.editor.busy( false );
if ( self.note.content ) {
LAPI.DOM.removeChildren( self.note.content.main );
self.note.content.main.appendChild( self.note.model.html );
} else {
// New note.
self.note.display(); // Actually a misnomer. Just creates 'content'.
if ( self.viewer.annotations.length > 1 ) {
self.viewer.annotations.sort( ImageAnnotation.compare );
var idxOfNote = Array.indexOf( self.viewer.annotations, self.note );
if ( idxOfNote + 1 < self.viewer.annotations.length ) {
LAPI.DOM.insertNode( self.note.view, self.viewer.annotations[ idxOfNote + 1 ].view );
}
}
}
self.to_insert = null;
self.saving = false;
if ( !self.note.tooltip ) { self.note.setTooltip(); }
self.hide_editor();
IA.is_editing = false;
self.editor.setText( data ); // In case the same note is re-opened: start new undo cycle
}
, function ( request, ex ) {
if ( edit_page.isFake && ( typeof edit_page.dispose === 'function' ) ) { edit_page.dispose(); }
failureFunc( request, ex );
}
);
},
function ( request, ex ) {
self.editor.busy( false );
self.saving = false;
// TODO: How and where to display error if user closed editor through ESC (or through
// opening another tooltip) in the meantime?
if ( !self.visible ) { return; }
// Change the tooltip to show the error.
self.editor.setText( self.to_insert );
// Error message. Use preview field for this.
var error_msg = ImageAnnotator.UI.get( 'wpImageAnnotatorSaveError', false );
var lk = getElementsByClassName( error_msg, 'span', 'wpImageAnnotatorOwnPageLink' );
if ( lk && lk.length && lk[ 0 ].firstChild.nodeName.toLowerCase() === 'a' ) {
lk = lk[ 0 ].firstChild;
lk.href = mw.config.get( 'wgServer' ) + mw.config.get( 'wgArticlePath' ).replace( '$1', encodeURIComponent( mw.config.get( 'wgPageName' ) ) ) + '?action=edit';
}
if ( ex ) {
var ex_msg = LAPI.formatException( ex, true );
if ( ex_msg ) {
ex_msg.style.borderBottom = '1px solid red';
var tmp = LAPI.make( 'div' );
tmp.appendChild( ex_msg );
tmp.appendChild( error_msg );
error_msg = tmp;
}
}
self.editor.setPreview( error_msg );
self.editor.showPreview();
self.editor.textarea.readOnly = true;
// Force a light gray background, since IE has no visual readonly indication.
self.editor.textarea.style.backgroundColor = '#EEEEEE';
self.editor.enable( LAPI.Edit.CANCEL ); // Disable all other buttons
}
);
},
 
onpreview: function ( editor ) {
var span = null;
if ( this.tooltip ) { this.tooltip.size_change(); }
if (self.note.content) // Otherwise it's a new note!
},
span = IA.findNote(pagetext, self.note.model.id);
if (span) { // Replace
pagetext =
pagetext.substring(0, span.start)
+ self.to_insert
+ pagetext.substring(span.end)
;
} else { // If not found, append
// Try to append right after existing notes
var lastNote = pagetext.lastIndexOf('{{ImageNoteEnd|id=');
if (lastNote >= 0) {
var endLastNote = pagetext.substring(lastNote).indexOf('}}');
if (endLastNote < 0) {
endLastNote = pagetext.substring(lastNote).indexOf('\n');
if (endLastNote < 0) lastNote = -1; else lastNote += endLastNote;
} else
lastNote += endLastNote + 2;
}
if (lastNote >= 0) {
pagetext =
pagetext.substring(0, lastNote)
+ '\n' + self.to_insert
+ pagetext.substring(lastNote)
;
} else
pagetext = pagetext.trimRight() + '\n' + self.to_insert;
}
textbox.value = pagetext;
var summary = editForm.wpSummary;
if (!summary)
throw new Error('#Summary field not found. Check that edit pages have valid XHTML.');
// If [[MediaWiki:Copyrightwarning]] is invalid XHTML, we may not have wpSummary!
if (self.note.content != null) {
IA.setSummary(
summary
, ImageAnnotator.UI.get('wpImageAnnotatorChangeSummary', true)
|| '[[MediaWiki talk:Gadget-ImageAnnotator.js|Changing image note]]$1'
, data
);
} else {
IA.setSummary(
summary
, ImageAnnotator.UI.get('wpImageAnnotatorAddSummary', true)
|| '[[MediaWiki talk:Gadget-ImageAnnotator.js|Adding image note]]$1'
, data
);
}
} catch (ex) {
failureFunc (null, ex);
return;
}
var edit_page = doc;
LAPI.Ajax.submitEdit(
editForm
, function (request) {
// After a successful submit.
if (edit_page.isFake && (typeof edit_page.dispose === 'function'))
edit_page.dispose();
// TODO: Actually, the edit got through here, so calling failureFunc on
// inconsistencies isn't quite right. Should we reload the page?
var id = 'image_annotation_content_' + self.note.model.id;
var doc = LAPI.Ajax.getHTML(request, failureFunc, id);
if (!doc) return;
var html = LAPI.$ (id, doc);
if (!html) {
if (doc.isFake && (typeof doc.dispose === 'function')) doc.dispose();
failureFunc
(request, new Error('#Note not found after saving. Please reload the page.'));
return;
}
var revision_id = LAPI.WP.revisionFromHtml(request.responseText);
if (!revision_id) {
if (doc.isFake && (typeof doc.dispose === 'function')) doc.dispose();
failureFunc
(request, new Error('#Version inconsistency after saving. Please reload the page.'));
return;
}
mw.config.set('wgCurRevisionId', revision_id); // Bump revision id!!
self.note.model.html = LAPI.DOM.importNode(document, html, true);
if (doc.isFake && (typeof doc.dispose === 'function')) doc.dispose();
self.note.model.dimension = dim; // record dimension
self.note.model.html.style.display = '';
self.note.model.wiki = data;
self.editor.busy(false);
if (self.note.content) {
LAPI.DOM.removeChildren(self.note.content.main);
self.note.content.main.appendChild(self.note.model.html);
} else {
// New note.
self.note.display(); // Actually a misnomer. Just creates 'content'.
if (self.viewer.annotations.length > 1) {
self.viewer.annotations.sort(ImageAnnotation.compare);
var idxOfNote = Array.indexOf(self.viewer.annotations, self.note);
if (idxOfNote+1 < self.viewer.annotations.length)
LAPI.DOM.insertNode
(self.note.view, self.viewer.annotations [idxOfNote+1].view);
}
}
self.to_insert = null;
self.saving = false;
if (!self.note.tooltip) self.note.setTooltip();
self.hide_editor();
IA.is_editing = false;
self.editor.setText(data); // In case the same note is re-opened: start new undo cycle
}
, function (request, ex) {
if (edit_page.isFake && (typeof edit_page.dispose === 'function'))
edit_page.dispose();
failureFunc (request, ex);
}
);
}
, function (request, ex) {
self.editor.busy(false);
self.saving = false;
// TODO: How and where to display error if user closed editor through ESC (or through
// opening another tooltip) in the meantime?
if (!self.visible) return;
// Change the tooltip to show the error.
self.editor.setText(self.to_insert);
// Error message. Use preview field for this.
var error_msg = ImageAnnotator.UI.get('wpImageAnnotatorSaveError', false);
var lk = getElementsByClassName(error_msg, 'span', 'wpImageAnnotatorOwnPageLink');
if (lk && lk.length && lk[0].firstChild.nodeName.toLowerCase() === 'a') {
lk = lk[0].firstChild;
lk.href = mw.config.get('wgServer') + mw.config.get('wgArticlePath').replace('$1', encodeURIComponent(mw.config.get('wgPageName'))) + '?action=edit';
}
if (ex) {
var ex_msg = LAPI.formatException(ex, true);
if (ex_msg) {
ex_msg.style.borderBottom = '1px solid red';
var tmp = LAPI.make('div');
tmp.appendChild(ex_msg);
tmp.appendChild(error_msg);
error_msg = tmp;
}
}
self.editor.setPreview(error_msg);
self.editor.showPreview();
self.editor.textarea.readOnly = true;
// Force a light gray background, since IE has no visual readonly indication.
self.editor.textarea.style.backgroundColor = '#EEEEEE';
self.editor.enable(LAPI.Edit.CANCEL); // Disable all other buttons
}
);
},
 
onpreview cancel: function ( editor ) {
if ( !this.tooltipnote ) this.tooltip.size_change(){ return; }
if ( !this.note.content ) {
},
// No content: Cancel and remove this note!
this.note.destroy();
this.note = null;
}
if ( editor ) { this.hide_editor(); }
},
 
cancel close_tooltip: function (editor tooltip, evt ) {
this.hide_editor( evt );
if (!this.note) return;
if (! this.note.contentcancel() {;
}
// No content: Cancel and remove this note!
this.note.destroy();
this.note = null;
}
if (editor) this.hide_editor();
},
 
};
close_tooltip: function (tooltip, evt) {
this.hide_editor(evt);
this.cancel();
}
 
var ImageNotesViewer = function () { this.initialize.apply( this, arguments ); };
};
 
var ImageNotesViewer.prototype = function () {this.initialize.apply(this, arguments); };
initialize: function ( descriptor, may_edit ) {
Object.merge( descriptor, this );
this.annotations = [];
this.max_id = 0;
this.main_div = null;
this.msg = null;
this.may_edit = may_edit;
this.setup_done = false;
this.tip = null;
this.icon = null;
this.factors = {
dx: this.full_img.width / this.thumb.width,
dy: this.full_img.height / this.thumb.height
};
 
if ( !this.isThumbnail && !this.isOther ) {
ImageNotesViewer.prototype = {
this.setup();
initialize: function (descriptor, may_edit) {
} else {
Object.merge(descriptor, this);
// Normalize the namespace of the realName to 'File' to account for images possibly stored at
this.annotations = [];
// a foreign repository (the Commons). Otherwise a later information load might fail because
this.max_id = 0;
// the link is local and might actually be given as "Bild:Foo.jpg". If that page doesn't exist
this.main_div = null;
// locally, we want to ask at the Commons about "File:Foo.jpg". The Commons doesn't understand
this.msg = null;
// the localized namespace names of other wikis, but the canonical namespace name 'File' works
this.may_edit = may_edit;
// also locally.
this.setup_done = false;
this.realName = 'File:' + this.realName.substring( this.realName.indexOf( ':' ) + 1 );
this.tip = null;
}
this.icon = null;
},
this.factors = {
dx : this.full_img.width / this.thumb.width,
dy : this.full_img.height / this.thumb.height
};
 
setup: function ( onlyIcon ) {
if (!this.isThumbnail && !this.isOther) {
this.setup_done = true;
this.setup();
var name = this.realName;
} else {
if ( this.isThumbnail || this.scope == document || this.may_edit || !IA.haveAjax ) {
// Normalize the namespace of the realName to 'File' to account for images possibly stored at
this.imgName = this.realName;
// a foreign repository (the Commons). Otherwise a later information load might fail because
this.realName = '';
// the link is local and might actually be given as "Bild:Foo.jpg". If that page doesn't exist
} else {
// locally, we want to ask at the Commons about "File:Foo.jpg". The Commons doesn't understand
name = getElementsByClassName( this.scope, '*', 'wpImageAnnotatorFullName' );
// the localized namespace names of other wikis, but the canonical namespace name 'File' works
this.realName = ( ( name && name.length ) ? LAPI.DOM.getInnerText( name[ 0 ] ) : '' );
// also locally.
this.realNameimgName = 'File:' + this.realName.substring(this.realName.indexOf(':') + 1);
}
}
},
 
var annotations = getElementsByClassName( this.scope, 'div', IA.annotation_class );
setup: function (onlyIcon) {
this.setup_done = true;
var name = this.realName;
if (this.isThumbnail || this.scope == document || this.may_edit || !IA.haveAjax) {
this.imgName = this.realName;
this.realName = '';
} else {
name = getElementsByClassName (this.scope, '*', 'wpImageAnnotatorFullName');
this.realName = ((name && name.length) ? LAPI.DOM.getInnerText(name[0]) : '');
this.imgName = this.realName;
}
 
if ( !this.may_edit && ( !annotations || annotations.length === 0 ) ) { return; } // Nothing to do
var annotations = getElementsByClassName (this.scope, 'div', IA.annotation_class);
 
// A div inserted around the image. It ensures that everything we add is positioned properly
if (!this.may_edit && (!annotations || annotations.length === 0))
// over the image, even if the browser window size changes and re-layouts occur.
return; // Nothing to do
var isEnabledImage = LAPI.DOM.hasClass( this.scope, 'wpImageAnnotatorEnable' );
if ( !this.isThumbnail && !this.isOther && !isEnabledImage ) {
this.img_div = LAPI.make( 'div', null, { position: 'relative', width: String( this.thumb.width ) + 'px' } );
var floater = LAPI.make(
'div', null,
{
cssFloat: ( IA.is_rtl ? 'right' : 'left' ),
styleFloat: ( IA.is_rtl ? 'right' : 'left' ), // For IE...
width: String( this.thumb.width ) + 'px',
position: 'relative' // Fixes IE layout bugs...
}
);
floater.appendChild( this.img_div );
this.img.parentNode.parentNode.insertBefore( floater, this.img.parentNode );
this.img_div.appendChild( this.img.parentNode );
// And now a clear:left to make the rest appear below the image, as usual.
var breaker = LAPI.make( 'div', null, { clear: ( IA.is_rtl ? 'right' : 'left' ) } );
LAPI.DOM.insertAfter( breaker, floater );
// Remove spurious br tag.
if ( breaker.nextSibling && breaker.nextSibling.nodeName.toLowerCase() == 'br' ) { LAPI.DOM.removeNode( breaker.nextSibling ); }
} else if ( this.isOther || isEnabledImage ) {
this.img_div = LAPI.make( 'div', null, { position: 'relative', width: String( this.thumb.width ) + 'px' } );
this.img.parentNode.parentNode.insertBefore( this.img_div, this.img.parentNode );
this.img_div.appendChild( this.img.parentNode );
// Insert one more to have a file_div, so that we can align the message text correctly
this.file_div = LAPI.make( 'div', null, { width: String( this.thumb.width ) + 'px' } );
this.img_div.parentNode.insertBefore( this.file_div, this.img_div );
this.file_div.appendChild( this.img_div );
} else { // Thumbnail
this.img_div = LAPI.make(
'div',
{ className: 'thumbimage' },
{ position: 'relative', width: String( this.thumb.width ) + 'px' }
);
this.img.parentNode.parentNode.insertBefore( this.img_div, this.img.parentNode );
this.img.style.border = 'none';
this.img_div.appendChild( this.img.parentNode );
}
if (
( this.isThumbnail || this.isOther ) && !this.may_edit &&
(
onlyIcon ||
this.iconOnly ||
ImageAnnotator_config.inlineImageUsesIndicator(
name, this.isLocal, this.thumb, this.full_img, annotations.length, this.isThumbnail
)
)
) {
// Use an onclick handler instead of a link around the image. The link may have a default white
// background, but we want to be sure to have transparency. The image should be an 8-bit indexed
// PNG or a GIF and have a transparent background.
this.icon = ImageAnnotator.UI.get( 'wpImageAnnotatorIndicatorIcon', false );
if ( this.icon ) { this.icon = this.icon.firstChild; } // Skip the message container span or div
// Guard against misconfigurations
if (
this.icon &&
this.icon.nodeName.toLowerCase() == 'a' &&
this.icon.firstChild.nodeName.toLowerCase() == 'img'
) {
// Make sure we use the right protocol:
var srcFixed = this.icon.firstChild.getAttribute( 'src', 2 ).replace( /^https?:/, document.___location.protocol );
this.icon.firstChild.src = srcFixed;
this.icon.firstChild.title = this.icon.title;
this.icon = this.icon.firstChild;
} else if ( !this.icon || this.icon.nodeName.toLowerCase() !== 'img' ) {
this.icon = LAPI.DOM.makeImage(
IA.indication_icon,
14, 14,
ImageAnnotator.UI.get( 'wpImageAnnotatorHasNotesMsg', true ) || ''
);
}
Object.merge(
{ position: 'absolute', zIndex: 1000, top: '0px', cursor: 'pointer' }
, this.icon.style
);
this.icon.onclick = ( function () { ___location.href = this.img.parentNode.href; } ).bind( this );
if ( IA.is_rtl ) { this.icon.style.right = '0px'; } else { this.icon.style.left = '0px'; }
this.img_div.appendChild( this.icon );
// And done. We just show the icon, no fancy event handling needed.
return;
}
// Set colors
var colors = IA.getRawItem( 'colors', this.scope );
this.outer_border =
colors && IA.getItem( 'outer', colors ) || IA.outer_border;
this.inner_border =
colors && IA.getItem( 'inner', colors ) || IA.inner_border;
this.active_border =
colors && IA.getItem( 'active', colors ) || IA.active_border;
if ( annotations ) {
for ( var i = 0; i < annotations.length; i++ ) {
var id = annotations[ i ].id;
if ( id && /^image_annotation_note_(\d+)$/.test( id ) ) {
id = parseInt( id.substring( 'image_annotation_note_'.length ) );
} else { id = null; }
if ( id ) {
if ( id > this.max_id ) { this.max_id = id; }
var w = IA.getIntItem( 'full_width_' + id, this.scope );
var h = IA.getIntItem( 'full_height_' + id, this.scope );
if (
w == this.full_img.width && h == this.full_img.height &&
!Array.exists( this.annotations, function ( note ) { return note.model.id == id; } )
) {
try {
this.register( new ImageAnnotation( annotations[ i ], this, id ) );
} catch ( ex ) {
// Swallow.
}
}
}
}
}
if ( this.annotations.length > 1 ) { this.annotations.sort( ImageAnnotation.compare ); }
// Add the rectangles of existing notes to the DOM now that they are sorted.
Array.forEach( this.annotations, ( function ( note ) { this.img_div.appendChild( note.view ); } ).bind( this ) );
if ( this.isThumbnail ) {
this.main_div = getElementsByClassName( this.file_div, 'div', 'thumbcaption' );
if ( !this.main_div || this.main_div.length == 0 ) { this.main_div = null; } else { this.main_div = this.main_div[ 0 ]; }
}
if ( !this.main_div ) {
this.main_div = LAPI.make( 'div' );
if ( IA.is_rtl ) {
this.main_div.style.direction = 'rtl';
this.main_div.style.textAlign = 'right';
this.main_div.className = 'rtl';
} else {
this.main_div.style.textAlign = 'left';
}
if ( !this.isThumbnail && !this.isOther && !isEnabledImage ) {
LAPI.DOM.insertAfter( this.main_div, this.file_div );
} else {
LAPI.DOM.insertAfter( this.main_div, this.img_div );
}
}
if (
!( this.isThumbnail || this.isOther ) ||
!this.noCaption &&
!IA.hideCaptions &&
ImageAnnotator_config.displayCaptionInArticles(
name, this.isLocal, this.thumb, this.full_img, annotations.length, this.isThumbnail
)
) {
this.msg = LAPI.make( 'div', null, { display: 'none' } );
if ( IA.is_rtl ) {
this.msg.style.direction = 'rtl';
this.msg.className = 'rtl';
}
if ( this.isThumbnail ) { this.msg.style.fontSize = '90%'; }
this.main_div.appendChild( this.msg );
}
 
// Set overflow parents, if any
// A div inserted around the image. It ensures that everything we add is positioned properly
// over the image, even if the browser window size changes and re-layouts occur.
var isEnabledImage = LAPI.DOM.hasClass(this.scope, 'wpImageAnnotatorEnable');
if (!this.isThumbnail && !this.isOther && !isEnabledImage) {
this.img_div =
LAPI.make('div', null, {position: 'relative', width: '' + this.thumb.width + 'px'});
var floater =
LAPI.make(
'div', null
, { cssFloat: (IA.is_rtl ? 'right' : 'left')
,styleFloat: (IA.is_rtl ? 'right' : 'left') // For IE...
,width: '' + this.thumb.width + 'px'
,position: 'relative' // Fixes IE layout bugs...
}
);
floater.appendChild(this.img_div);
this.img.parentNode.parentNode.insertBefore(floater, this.img.parentNode);
this.img_div.appendChild(this.img.parentNode);
// And now a clear:left to make the rest appear below the image, as usual.
var breaker = LAPI.make('div', null, {clear: (IA.is_rtl ? 'right' : 'left')});
LAPI.DOM.insertAfter(breaker, floater);
// Remove spurious br tag.
if (breaker.nextSibling && breaker.nextSibling.nodeName.toLowerCase() == 'br')
LAPI.DOM.removeNode(breaker.nextSibling);
} else if (this.isOther || isEnabledImage) {
this.img_div =
LAPI.make('div', null, {position: 'relative', width: '' + this.thumb.width + 'px'});
this.img.parentNode.parentNode.insertBefore(this.img_div, this.img.parentNode);
this.img_div.appendChild(this.img.parentNode);
// Insert one more to have a file_div, so that we can align the message text correctly
this.file_div = LAPI.make('div', null, {width: '' + this.thumb.width + 'px'});
this.img_div.parentNode.insertBefore(this.file_div, this.img_div);
this.file_div.appendChild(this.img_div);
} else { // Thumbnail
this.img_div =
LAPI.make(
'div'
, {className: 'thumbimage'}
, {position: 'relative', width: '' + this.thumb.width + 'px'}
);
this.img.parentNode.parentNode.insertBefore(this.img_div, this.img.parentNode);
this.img.style.border = 'none';
this.img_div.appendChild(this.img.parentNode);
}
if ( (this.isThumbnail || this.isOther) && !this.may_edit
&& ( onlyIcon
|| this.iconOnly
|| ImageAnnotator_config.inlineImageUsesIndicator
(name, this.isLocal, this.thumb, this.full_img, annotations.length, this.isThumbnail)
)
)
{
// Use an onclick handler instead of a link around the image. The link may have a default white
// background, but we want to be sure to have transparency. The image should be an 8-bit indexed
// PNG or a GIF and have a transparent background.
this.icon = ImageAnnotator.UI.get('wpImageAnnotatorIndicatorIcon', false);
if (this.icon) this.icon = this.icon.firstChild; // Skip the message container span or div
// Guard against misconfigurations
if ( this.icon
&& this.icon.nodeName.toLowerCase() == 'a'
&& this.icon.firstChild.nodeName.toLowerCase() == 'img'
)
{
// Make sure we use the right protocol:
var srcFixed = this.icon.firstChild.getAttribute('src', 2).replace(/^https?\:/, document.___location.protocol);
this.icon.firstChild.src = srcFixed;
this.icon.firstChild.title = this.icon.title;
this.icon = this.icon.firstChild;
} else if (!this.icon || this.icon.nodeName.toLowerCase() !== 'img') {
this.icon =
LAPI.DOM.makeImage(
IA.indication_icon
, 14, 14
, ImageAnnotator.UI.get('wpImageAnnotatorHasNotesMsg', true) || ''
);
}
Object.merge(
{position: 'absolute', zIndex: 1000, top: '0px', cursor: 'pointer'}
, this.icon.style
);
this.icon.onclick = (function () { ___location.href = this.img.parentNode.href; }).bind(this);
if (IA.is_rtl)
this.icon.style.right = '0px';
else
this.icon.style.left = '0px';
this.img_div.appendChild(this.icon);
// And done. We just show the icon, no fancy event handling needed.
return;
}
// Set colors
var colors = IA.getRawItem('colors', this.scope);
this.outer_border =
colors && IA.getItem('outer', colors) || IA.outer_border;
this.inner_border =
colors && IA.getItem('inner', colors) || IA.inner_border;
this.active_border =
colors && IA.getItem('active', colors) || IA.active_border;
if (annotations) {
for (var i = 0; i < annotations.length; i++) {
var id = annotations[i].id;
if (id && /^image_annotation_note_(\d+)$/.test(id)) {
id = parseInt (id.substring('image_annotation_note_'.length));
} else
id = null;
if (id) {
if (id > this.max_id) this.max_id = id;
var w = IA.getIntItem('full_width_' + id, this.scope);
var h = IA.getIntItem('full_height_' + id, this.scope);
if ( w == this.full_img.width && h == this.full_img.height
&& !Array.exists(this.annotations, function (note) { return note.model.id == id; })
)
{
try {
this.register(new ImageAnnotation(annotations[i], this, id));
} catch (ex) {
// Swallow.
}
}
}
}
}
if (this.annotations.length > 1) this.annotations.sort(ImageAnnotation.compare);
// Add the rectangles of existing notes to the DOM now that they are sorted.
Array.forEach(this.annotations, (function (note) {this.img_div.appendChild(note.view);}).bind(this));
if (this.isThumbnail) {
this.main_div = getElementsByClassName (this.file_div, 'div', 'thumbcaption');
if (!this.main_div || this.main_div.length == 0)
this.main_div = null;
else
this.main_div = this.main_div[0];
}
if (!this.main_div) {
this.main_div = LAPI.make('div');
if (IA.is_rtl) {
this.main_div.style.direction = 'rtl';
this.main_div.style.textAlign = 'right';
this.main_div.className = 'rtl';
} else {
this.main_div.style.textAlign = 'left';
}
if (!this.isThumbnail && !this.isOther && !isEnabledImage) {
LAPI.DOM.insertAfter(this.main_div, this.file_div);
} else {
LAPI.DOM.insertAfter(this.main_div, this.img_div);
}
}
if ( !(this.isThumbnail || this.isOther)
|| !this.noCaption
&& !IA.hideCaptions
&& ImageAnnotator_config.displayCaptionInArticles
(name, this.isLocal, this.thumb, this.full_img, annotations.length, this.isThumbnail)
)
{
this.msg = LAPI.make('div', null, {display: 'none'});
if (IA.is_rtl) {
this.msg.style.direction = 'rtl';
this.msg.className = 'rtl';
}
if (this.isThumbnail) this.msg.style.fontSize = '90%';
this.main_div.appendChild(this.msg);
}
 
var simple = !!window.getComputedStyle;
// Set overflow parents, if any
var checks = ( simple ? [ 'overflow', 'overflow-x', 'overflow-y' ] :
[ 'overflow', 'overflowX', 'overflowY' ]
);
var curStyle = null;
for ( var up = this.img.parentNode.parentNode; up != document.body; up = up.parentNode ) {
curStyle = ( simple ? window.getComputedStyle( up, null ) : ( up.currentStyle || up.style ) );
// "up.style" is actually incorrect, but a best-effort fallback.
var overflow = Array.any( checks, function ( t ) {
var o = curStyle[ t ];
return ( o && o != 'visible' ) ? o : null;
} );
if ( overflow ) {
if ( !this.overflowParents ) { this.overflowParents = [ up ]; } else { this.overflowParents[ this.overflowParents.length ] = up; }
}
}
if ( this.overflowParents && this.may_edit ) {
// Forbid editing if we have such a crazy layout.
this.may_edit = false;
IA.may_edit = false;
}
 
this.show_evt = LAPI.Evt.makeListener( this, this.show );
var simple = !!window.getComputedStyle;
if ( this.overflowParents || LAPI.Browser.is_ie ) {
var checks = (simple ? ['overflow', 'overflow-x', 'overflow-y']
// If we have overflowParents, also use a mousemove listener to show/hide the whole
: ['overflow', 'overflowX', 'overflowY']
// view (FF doesn't send mouseout events if the visible border is still within the image, i.e.,
);
// if not the whole image is visible). On IE, also use this handler to show/hide the notes
var curStyle = null;
// if we're still within the visible area of the image. IE passes through mouse_over events to
for (var up = this.img.parentNode.parentNode; up != document.body; up = up.parentNode) {
// the img even if the mouse is within a note's rectangle. Apparently is doesn't handle
curStyle = (simple ? window.getComputedStyle(up, null) : (up.currentStyle || up.style));
// transparent divs correctly. As a result, the notes will pop up and disappear only when the
// "up.style" is actually incorrect, but a best-effort fallback.
// mouse crosses the border, and if one moves the mouse a little fast across the border, we
var overflow =
// don't get any event at all. That's no good.
Array.any(checks, function (t) {
this.move_evt = LAPI.Evt.makeListener( this, this.check_hide );
var o = curStyle[t];
} else { this.hide_evt = LAPI.Evt.makeListener( this, this.hide ); }
return (o && o != 'visible') ? o : null;
this.move_listening = false;
});
this.setShowHideEvents( true );
if (overflow) {
this.visible = false;
if (!this.overflowParents)
this.setDefaultMsg();
this.overflowParents = [up];
},
else
this.overflowParents[this.overflowParents.length] = up;
}
}
if (this.overflowParents && this.may_edit) {
// Forbid editing if we have such a crazy layout.
this.may_edit = false;
IA.may_edit = false;
}
 
cannotEdit: function () {
this.show_evt = LAPI.Evt.makeListener(this, this.show);
if ( !this.may_edit ) { return; }
if (this.overflowParents || LAPI.Browser.is_ie) {
this.may_edit = false;
// If we have overflowParents, also use a mousemove listener to show/hide the whole
Array.forEach( this.annotations, function ( note ) { note.cannotEdit(); } );
// view (FF doesn't send mouseout events if the visible border is still within the image, i.e.,
},
// if not the whole image is visible). On IE, also use this handler to show/hide the notes
// if we're still within the visible area of the image. IE passes through mouse_over events to
// the img even if the mouse is within a note's rectangle. Apparently is doesn't handle
// transparent divs correctly. As a result, the notes will pop up and disappear only when the
// mouse crosses the border, and if one moves the mouse a little fast across the border, we
// don't get any event at all. That's no good.
this.move_evt = LAPI.Evt.makeListener(this, this.check_hide);
} else
this.hide_evt = LAPI.Evt.makeListener(this, this.hide);
this.move_listening = false;
this.setShowHideEvents(true);
this.visible = false;
this.setDefaultMsg();
},
 
cannotEdit setShowHideEvents: function ( set ) {
if (! this.may_editicon ) { return; }
if ( set ) {
this.may_edit = false;
LAPI.Evt.attach( this.img, IA.mouse_in, this.show_evt );
Array.forEach(this.annotations, function (note) { note.cannotEdit(); });
if ( this.hide_evt ) { LAPI.Evt.attach( this.img, IA.mouse_out, this.hide_evt ); }
},
} else {
LAPI.Evt.remove( this.img, IA.mouse_in, this.show_evt );
if ( this.hide_evt ) {
LAPI.Evt.remove( this.img, IA.mouse_out, this.hide_evt );
} else if ( this.move_listening ) { this.removeMoveListener(); }
}
},
 
setShowHideEvents removeMoveListener: function (set) {
if ( this.icon ) { return; }
this.move_listening = false;
if (set) {
if ( this.move_evt ) {
LAPI.Evt.attach(this.img, IA.mouse_in, this.show_evt);
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( null ); }
if (this.hide_evt) LAPI.Evt.attach(this.img, IA.mouse_out, this.hide_evt);
LAPI.Evt.remove( document, 'mousemove', this.move_evt, true );
} else {
}
LAPI.Evt.remove(this.img, IA.mouse_in, this.show_evt);
},
if (this.hide_evt) {
LAPI.Evt.remove(this.img, IA.mouse_out, this.hide_evt);
} else if (this.move_listening)
this.removeMoveListener();
}
},
 
removeMoveListener adjustRectangleSize: function ( node ) {
if ( this.icon ) { return; }
// Make sure the note boxes don't overlap the image boundary; we might get an event
this.move_listening = false;
// loop otherwise if the mouse was just on that overlapped boundary, resulting in flickering.
if (this.move_evt) {
var view_x = node.offsetLeft;
if (!LAPI.Browser.is_ie && typeof document.captureEvents === 'function')
var view_y = node.offsetTop;
document.captureEvents(null);
var view_w = node.offsetWidth;
LAPI.Evt.remove(document, 'mousemove', this.move_evt, true);
var view_h = node.offsetHeight;
}
if ( view_x === 0 ) { view_x = 1; }
},
if ( view_y === 0 ) { view_y = 1; }
if ( view_x + view_w >= this.thumb.width ) {
view_w = this.thumb.width - view_x - 1;
if ( view_w <= 4 ) { view_w = 4; view_x = this.thumb.width - view_w - 1; }
}
if ( view_y + view_h >= this.thumb.height ) {
view_h = this.thumb.height - view_y - 1;
if ( view_h <= 4 ) { view_h = 4; view_y = this.thumb.height - view_h - 1; }
}
// Now set position and width and height, subtracting cumulated border widths
if (
view_x != node.offsetLeft || view_y != node.offsetTop ||
view_w != node.offsetWidth || view_h != node.offsetHeight
) {
node.style.top = String( view_y ) + 'px';
node.style.left = String( view_x ) + 'px';
node.style.width = String( view_w - 2 ) + 'px';
node.style.height = String( view_h - 2 ) + 'px';
node.firstChild.style.width = String( view_w - 4 ) + 'px';
node.firstChild.style.height = String( view_h - 4 ) + 'px';
}
},
 
adjustRectangleSize toggle: function (node dummies ) {
var i;
if (this.icon) return;
if ( !this.annotations || this.annotations.length === 0 || this.icon ) { return; }
// Make sure the note boxes don't overlap the image boundary; we might get an event
if ( dummies ) {
// loop otherwise if the mouse was just on that overlapped boundary, resulting in flickering.
for ( i = 0; i < this.annotations.length; i++ ) {
var view_x = node.offsetLeft;
this.annotations[ i ].view.style.display = 'none';
var view_y = node.offsetTop;
if ( this.visible && this.annotations[ i ].tooltip ) { this.annotations[ i ].tooltip.hide_now( null ); }
var view_w = node.offsetWidth;
this.annotations[ i ].dummy.style.display = ( this.visible ? 'none' : '' );
var view_h = node.offsetHeight;
if ( !this.visible ) { this.adjustRectangleSize( this.annotations[ i ].dummy ); }
if (view_x === 0) view_x = 1;
}
if (view_y === 0) view_y = 1;
} else {
if (view_x + view_w >= this.thumb.width) {
for ( i = 0; i view_w =< this.thumbannotations.widthlength; -i++ view_x) - 1;{
this.annotations[ i ].dummy.style.display = 'none';
if (view_w <= 4) { view_w = 4; view_x = this.thumb.width - view_w - 1;}
this.annotations[ i ].view.style.display = ( this.visible ? 'none' : '' );
}
if ( !this.visible ) { this.adjustRectangleSize( this.annotations[ i ].view ); }
if (view_y + view_h >= this.thumb.height) {
if ( this.visible && this.annotations[ i ].tooltip ) { this.annotations[ i ].tooltip.hide_now( null ); }
view_h = this.thumb.height - view_y - 1;
}
if (view_h <= 4) { view_h = 4; view_y = this.thumb.height - view_h - 1;}
}
}
this.visible = !this.visible;
// Now set position and width and height, subtracting cumulated border widths
},
if ( view_x != node.offsetLeft || view_y != node.offsetTop
|| view_w != node.offsetWidth || view_h != node.offsetHeight)
{
node.style.top = '' + view_y + 'px';
node.style.left = '' + view_x + 'px';
node.style.width = '' + (view_w - 2) + 'px';
node.style.height = '' + (view_h - 2) + 'px';
node.firstChild.style.width = '' + (view_w - 4) + 'px';
node.firstChild.style.height = '' + (view_h - 4) + 'px';
}
},
 
toggle show: function (dummies evt ) {
if ( this.visible || this.icon ) { return; }
var i;
this.toggle( IA.is_adding || IA.is_editing );
if (!this.annotations || this.annotations.length === 0 || this.icon) return;
if ( this.move_evt && !this.move_listening ) {
if (dummies) {
LAPI.Evt.attach( document, 'mousemove', this.move_evt, true );
for (i = 0; i < this.annotations.length; i++) {
this.move_listening = true;
this.annotations[i].view.style.display = 'none';
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( Event.MOUSEMOVE ); }
if (this.visible && this.annotations[i].tooltip)
}
this.annotations[i].tooltip.hide_now(null);
},
this.annotations[i].dummy.style.display = (this.visible ? 'none' : '');
if (!this.visible) this.adjustRectangleSize(this.annotations[i].dummy);
}
} else {
for (i = 0; i < this.annotations.length; i++) {
this.annotations[i].dummy.style.display = 'none';
this.annotations[i].view.style.display = (this.visible ? 'none' : '');
if (!this.visible) this.adjustRectangleSize(this.annotations[i].view);
if (this.visible && this.annotations[i].tooltip)
this.annotations[i].tooltip.hide_now(null);
}
}
this.visible = !this.visible;
},
 
show hide: function ( evt ) {
if (this.visible || this.icon ) { return true; }
if ( !this.visible ) {
this.toggle(IA.is_adding || IA.is_editing);
// Huh?
if (this.move_evt && !this.move_listening) {
if ( this.move_listening ) { this.removeMoveListener(); }
LAPI.Evt.attach(document, 'mousemove', this.move_evt, true);
return true;
this.move_listening = true;
}
if (!LAPI.Browser.is_ie && typeof document.captureEvents === 'function')
if ( evt ) {
document.captureEvents(Event.MOUSEMOVE);
var mouse_pos = LAPI.Pos.mousePosition( evt );
}
if ( mouse_pos ) {
},
if ( this.tip ) {
// Check whether we're within the visible note.
if ( LAPI.Pos.isWithin( this.tip.popup, mouse_pos.x, mouse_pos.y ) ) { return true; }
}
var is_within = true;
var img_pos = LAPI.Pos.position( this.img );
var rect = {
x: img_pos.x,
y: img_pos.y,
r: ( img_pos.x + this.img.offsetWidth ),
b: ( img_pos.y + this.img.offsetHeight )
};
var i;
if ( this.overflowParents ) {
// We're within some elements having overflow:hidden or overflow:auto or overflow:scroll set.
// Compute the actually visible region by intersecting the rectangle given by img_pos and
// this.img.offsetWidth, this.img.offsetTop with the rectangles of all overflow parents.
 
function intersect_rectangles( a, b ) {
hide: function (evt) {
if ( b.x > a.r || b.r < a.x || b.y > a.b || b.b < a.y ) { return { x: 0, y: 0, r: 0, b: 0 }; }
if (this.icon) return true;
if (!this.visible) {
// Huh?
if (this.move_listening) this.removeMoveListener();
return true;
}
if (evt) {
var mouse_pos = LAPI.Pos.mousePosition(evt);
if (mouse_pos) {
if (this.tip) {
// Check whether we're within the visible note.
if (LAPI.Pos.isWithin(this.tip.popup, mouse_pos.x, mouse_pos.y)) return true;
}
var is_within = true;
var img_pos = LAPI.Pos.position(this.img);
var rect = {
x: img_pos.x,
y: img_pos.y,
r: (img_pos.x + this.img.offsetWidth),
b: (img_pos.y + this.img.offsetHeight)
};
var i;
if (this.overflowParents) {
// We're within some elements having overflow:hidden or overflow:auto or overflow:scroll set.
// Compute the actually visible region by intersecting the rectangle given by img_pos and
// this.img.offsetWidth, this.img.offsetTop with the rectangles of all overflow parents.
 
return {
function intersect_rectangles (a, b) {
x: Math.max( a.x, b.x ),
if (b.x > a.r || b.r < a.x || b.y > a.b || b.b < a.y)
y: Math.max( a.y, b.y ),
return { x:0, y:0, r:0, b:0 };
r: Math.min( a.r, b.r ),
b: Math.min( a.b, b.b )
};
}
 
for ( i = 0; i < this.overflowParents.length && rect.x < rect.r && rect.y < rect.b; i++ ) {
return {
img_pos = LAPI.Pos.position( this.overflowParents[ i ] );
x: Math.max(a.x, b.x),
img_pos.r = img_pos.x + this.overflowParents[ i ].clientWidth;
y: Math.max(a.y, b.y),
img_pos.b = img_pos.y + this.overflowParents[ i ].clientHeight;
r: Math.min(a.r, b.r),
rect = intersect_rectangles( rect, img_pos );
b: Math.min(a.b, b.b)
}
};
}
}
 
is_within = for !(i = 0; i < this.overflowParents.length && rect.x <>= rect.r &&|| rect.y <>= rect.b; i++)|| // Empty {rectangle
rect.x >= mouse_pos.x || rect.r <= mouse_pos.x ||
img_pos = LAPI.Pos.position(this.overflowParents[i]);
rect.y >= mouse_pos.y || rect.b <= mouse_pos.y
img_pos.r = img_pos.x + this.overflowParents[i].clientWidth;
);
img_pos.b = img_pos.y + this.overflowParents[i].clientHeight;
if ( is_within ) {
rect = intersect_rectangles (rect, img_pos);
if ( LAPI.Browser.is_ie && evt.type === 'mousemove' ) {
}
var display;
}
// Loop in reverse order to properly display top rectangle's note!
for ( i = this.annotations.length - 1; i >= 0; i-- ) {
display = this.annotations[ i ].view.style.display;
if (
display !== 'none' && display != null &&
LAPI.Pos.isWithin( this.annotations[ i ].view.firstChild, mouse_pos.x, mouse_pos.y )
) {
if ( !this.annotations[ i ].tooltip.visible ) { this.annotations[ i ].tooltip.show( evt ); }
return true;
}
}
if ( this.tip ) { this.tip.hide_now(); } // Inside the image, but not within any note rectangle
}
return true;
}
}
}
// Not within the image, or forced hiding (no event)
if ( this.move_listening ) { this.removeMoveListener(); }
this.toggle( IA.is_adding || IA.is_editing );
return true;
},
 
check_hide: function ( evt ) {
is_within = !( rect.x >= rect.r || rect.y >= rect.b // Empty rectangle
if ( this.icon ) { return true; }
|| rect.x >= mouse_pos.x || rect.r <= mouse_pos.x
if ( this.visible ) { this.hide( evt ); }
|| rect.y >= mouse_pos.y || rect.b <= mouse_pos.y
return true;
);
},
if (is_within) {
if (LAPI.Browser.is_ie && evt.type === 'mousemove') {
var display;
// Loop in reverse order to properly display top rectangle's note!
for (i = this.annotations.length - 1; i >= 0; i--) {
display = this.annotations[i].view.style.display;
if ( display !== 'none' && display != null
&& LAPI.Pos.isWithin(this.annotations[i].view.firstChild, mouse_pos.x, mouse_pos.y)
)
{
if (!this.annotations[i].tooltip.visible) this.annotations[i].tooltip.show(evt);
return true;
}
}
if (this.tip) this.tip.hide_now(); // Inside the image, but not within any note rectangle
}
return true;
}
}
}
// Not within the image, or forced hiding (no event)
if (this.move_listening) this.removeMoveListener();
this.toggle(IA.is_adding || IA.is_editing);
return true;
},
 
check_hide register: function (evt new_note ) {
this.annotations[ this.annotations.length ] = new_note;
if (this.icon) return true;
if ( new_note.model.id > 0 ) {
if (this.visible)
if ( new_note.model.id > this.max_id ) { this.max_id = new_note.model.id; }
this.hide(evt);
} else {
return true;
new_note.model.id = ++this.max_id;
},
}
},
 
register deregister: function (new_note note ) {
Array.remove( this.annotations[this.annotations.length], =note new_note);
if (new_note note.model.id >== this.max_id 0) { this.max_id--; }
if ( this.annotations.length === 0 ) { this.setDefaultMsg(); } // If we removed the last one, clear the msg
if (new_note.model.id > this.max_id) this.max_id = new_note.model.id;
},
} else {
new_note.model.id = ++this.max_id;
}
},
 
deregister setDefaultMsg: function (note) {
if ( this.annotations && Array.remove(this.annotations,.length && this.msg note); {
LAPI.DOM.removeChildren( this.msg );
if (note.model.id == this.max_id) this.max_id--;
this.msg.appendChild( ImageAnnotator.UI.get( 'wpImageAnnotatorHasNotesMsg', false ) );
if (this.annotations.length === 0) this.setDefaultMsg(); //If we removed the last one, clear the msg
if ( this.realName && typeof this.realName === 'string' && this.realName.length ) {
},
var otherPageMsg = ImageAnnotator.UI.get( 'wpImageAnnotatorEditNotesMsg', false );
if ( otherPageMsg ) {
var lk = otherPageMsg.getElementsByTagName( 'a' );
if ( lk && lk.length ) {
lk = lk[ 0 ];
lk.parentNode.replaceChild(
LAPI.DOM.makeLink(
mw.config.get( 'wgArticlePath' ).replace( '$1', encodeURIComponent( this.realName ) ),
this.realName,
this.realName
),
lk
);
this.msg.appendChild( otherPageMsg );
}
}
}
this.msg.style.display = '';
} else {
if ( this.msg ) { this.msg.style.display = 'none'; }
}
if ( IA.button_div && this.may_edit ) { IA.button_div.style.display = ''; }
}
 
};
setDefaultMsg: function () {
if (this.annotations && this.annotations.length && this.msg) {
LAPI.DOM.removeChildren(this.msg);
this.msg.appendChild
(ImageAnnotator.UI.get('wpImageAnnotatorHasNotesMsg', false));
if (this.realName && typeof this.realName === 'string' && this.realName.length) {
var otherPageMsg = ImageAnnotator.UI.get('wpImageAnnotatorEditNotesMsg', false);
if (otherPageMsg) {
var lk = otherPageMsg.getElementsByTagName('a');
if (lk && lk.length) {
lk = lk[0];
lk.parentNode.replaceChild(
LAPI.DOM.makeLink(
mw.config.get('wgArticlePath').replace('$1', encodeURIComponent(this.realName))
, this.realName
, this.realName
)
, lk
);
this.msg.appendChild(otherPageMsg);
}
}
}
this.msg.style.display = '';
} else {
if (this.msg) this.msg.style.display = 'none';
}
if (IA.button_div && this.may_edit) IA.button_div.style.display = '';
}
 
var IA = {
};
// This object is responsible for setting up annotations when a page is loaded. It loads all
// annotations in the page source, and adds an "Annotate this image" button plus the support
// for drawing rectangles onto the image if there is only one image and editing is allowed.
 
haveAjax: false,
var IA = {
// This object is responsible for setting up annotations when a page is loaded. It loads all
// annotations in the page source, and adds an "Annotate this image" button plus the support
// for drawing rectangles onto the image if there is only one image and editing is allowed.
 
button_div: null,
haveAjax: false,
add_button: null,
 
button_div cover: null,
add_button border: null,
definer: null,
 
mouse_in: ( window.ActiveXObject ? 'mouseenter' : 'mouseover' ),
cover: null,
mouse_out: ( window.ActiveXObject ? 'mouseleave' : 'mouseout' ),
border: null,
definer: null,
 
annotation_class: 'image_annotation',
mouse_in: (window.ActiveXObject ? 'mouseenter' : 'mouseover'),
mouse_out: (window.ActiveXObject ? 'mouseleave' : 'mouseout'),
 
// Format of notes in Wikitext. Note: there are two formats, an old one and a new one.
annotation_class : 'image_annotation',
// We only write the newest (last) one, but we can read also the older formats. Order is
// important, because the old format also used the ImageNote template, but for a different
// purpose.
note_delim: [
{
start: '<div id="image_annotation_note_$1"',
end: '</div><!-- End of annotation $1-->',
content_start: '<div id="image_annotation_content_$1">\n',
content_end: '</div>\n<span id="image_annotation_wikitext_$1"'
},
{
start: '{{ImageNote|id=$1',
end: '{{ImageNoteEnd|id=$1}}',
content_start: '}}\n',
content_end: '{{ImageNoteEnd|id=$1}}'
}
],
 
tooltip_styles: { // The style for all our tooltips
// 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: ((mw.config.get('skin') == 'monobook' || mw.config.get('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 = mw.config.get('wgNamespaceNumber') >= 0 && mw.config.get('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;
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;
}
 
// 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 (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;
}
 
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');
 
// This prevents traversing a page with more than 400 images
// There are extreme cases like [[Emoji]] that high number of images can cause
// huge lag specially on Chrome
if ( all_imgs.length > 400 ) {
// purging the array, simply a hack to avoid more indention
all_imgs = [];
}
}
 
for ( var i = 0; i < all_imgs.length; i++ ) {
// Exclude all that are in img_with_notes or in thumbs. Also exclude all in galleries.
var up = all_imgs[ i ].parentNode;
if ( up.nodeName.toLowerCase() !== 'a' ) { continue; }
up = up.parentNode;
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' thumbinner ' ) >= 0 ) {
if ( do_thumbs ) { self.thumbs[ self.thumbs.length ] = up; }
continue;
}
}
up = up.parentNode;
if ( !up ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorEnable ' ) >= 0 ) {
self.imgs_with_notes[ self.imgs_with_notes.length ] = up;
continue;
}
}
up = up.parentNode;
if ( !up ) { continue; }
// Other images not in galleries
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' gallerybox ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorEnable ' ) >= 0 ) {
self.imgs_with_notes[ self.imgs_with_notes.length ] = up;
continue;
}
}
up = up.parentNode;
if ( !up ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' gallerybox ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorEnable ' ) >= 0 ) {
self.imgs_with_notes[ self.imgs_with_notes.length ] = up;
} else {
// Guard against other scripts adding aribtrary numbers of divs (dshuf for instance!)
var is_other = true;
while ( up && up.nodeName.toLowerCase() == 'div' && is_other ) {
up = up.parentNode;
if ( up ) { is_other = ( ' ' + up.className + ' ' ).indexOf( ' gallerybox ' ) < 0; }
}
}
if ( is_other ) { self.other_images[ self.other_images.length ] = all_imgs[ i ]; }
}
}
} // end loop
}
}
} else {
self.imgs_with_notes = getElementsByClassName( (document, '*', 'wpImageAnnotatorEnable' );
if ( do_thumbs ) { self.thumbs = getElementsByClassName( document, 'div', 'thumbinner' ); } // No galleries!
if (do_thumbs)
}
self.thumbs = getElementsByClassName (document, 'div', 'thumbinner'); // No galleries!
if (
}
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;
// Publish parts of config.
ImageAnnotator self.UI outer_border = config.UIouter_border;
self.outer_border inner_border = config.outer_borderinner_border;
self.inner_border active_border = config.inner_borderactive_border;
self.active_bordernew_border = config.active_bordernew_border;
self.wait_for_required_libraries();
self.new_border = config.new_border;
}
self.wait_for_required_libraries();
},
}
},
 
wait_for_required_libraries: function () {
if ( typeof Tooltip === 'undefined' || typeof LAPI === 'undefined' ) {
if ( IA.install_attempts++ < IA.max_install_attempts ) {
setTimeout( IA.wait_for_required_libraries, 500 ); // 0.5 sec.
}
}
return;
}
}
if ( LAPI.Browser.is_opera && !LAPI.Browser.is_opera_ge_9 ) { return; } // Opera 8 has severe problems
// Get the UI. We're likely to need it if we made it to here.
IA.setup_ui();
IA.setup();
},
 
setup: function () {
var self = IA;
self.imgs = [];
 
// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
self.is_rtl =
LAPI.DOM.hasClass( document.body, 'rtl' ) ||
(
|| ( LAPI.DOM.currentStyle // Paranoia: added recently, not everyone might have it
LAPI.DOM.currentStyle && // Paranoia: added recently, not everyone might have it
&& LAPI.DOM.currentStyle(document.body, 'direction') == 'rtl'
LAPI.DOM.currentStyle( document.body, 'direction' ) == 'rtl'
)
);
;
 
var stylepath = mw.config.get( 'stylepath' ) || '/skin';
 
// Use this to temporarily display an image off-screen to get its dimensions
var testImgDiv = LAPI.make(
LAPI.make( 'div', null,
{
{ display: 'none', position: 'absolute', width: '300px'
, overflow display: 'hiddennone', overflowXposition: 'hiddenabsolute', leftwidth: '-10000px300px',
overflow: 'hidden', overflowX: 'hidden', left: '-10000px'
}
}
);
);
document.body.insertBefore(testImgDiv, document.body.firstChild);
document.body.insertBefore( testImgDiv, document.body.firstChild );
 
function img_check( (img, is_other ) {
var srcW = parseInt( img.getAttribute( 'width', 2 ), 10 );
{
var srcWsrcH = parseInt ( img.getAttribute( 'widthheight', 2 ), 10 );
// Don't do anything on extremely small previews. We need some minimum extent to be able to place
var srcH = parseInt (img.getAttribute('height', 2), 10);
// rectangles after all...
// Don't do anything on extremely small previews. We need some minimum extent to be able to place
if ( !srcW || !srcH || srcW < 20 || srcH < 20 ) { return null; }
// rectangles after all...
// For non-thumbnail images, the size limit is larger.
if (!srcW || !srcH || srcW < 20 || srcH < 20) return null;
if ( is_other && ( srcW < 60 || srcH < 60 ) ) { return null; }
// For non-thumbnail images, the size limit is larger.
var w = img.clientWidth; // Don't use offsetWidth, thumbnails may have a boundary...
if (is_other && (srcW < 60 || srcH < 60)) return null;
var h = img.clientHeight;
var w = img.clientWidth; // Don't use offsetWidth, thumbnails may have a boundary...
// If the image is currently varhidden, hits =clientWidth and img.clientHeight; are not set. Try
// harder to get the true width and height:
// If the image is currently hidden, its clientWidth and clientHeight are not set. Try
if ( ( !w || !h ) && img.style.display != 'none' ) {
// harder to get the true width and height:
var copied = img.cloneNode( true );
if ((!w || !h) && img.style.display != 'none') {
copied.style.display = '';
var copied = img.cloneNode(true);
testImgDiv.appendChild( copied );
copied.style.display = '';
testImgDiv.style.display = '';
testImgDiv.appendChild(copied);
w = copied.clientWidth;
testImgDiv.style.display = '';
w h = copied.clientWidthclientHeight;
testImgDiv.style.display = 'none';
h = copied.clientHeight;
LAPI.DOM.removeNode( copied );
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; }
// Quit if the image wasn't loaded properly for some reason:
// Exclude system images
if (w != srcW || h != srcH) return null;
if ( img.src.contains( stylepath ) ) { return null; }
// Exclude system images
// Only if within a link
if (img.src.contains(stylepath)) return null;
if ( img.parentNode.nodeName.toLowerCase() != 'a' ) { return null; }
// Only if within a link
if ( is_other ) {
if (img.parentNode.nodeName.toLowerCase() != 'a') return null;
// Only if the img-within-link construction is within some element that may contain a div!
if (is_other) {
if ( /^(p|span)$/i.test( img.parentNode.parentNode.nodeName ) ) {
// Only if the img-within-link construction is within some element that may contain a div!
// Special case: a paragraph may contain only inline elements, but we want to be able to handle
if (/^(p|span)$/i.test(img.parentNode.parentNode.nodeName)) {
// files in single paragraphs. Maybe we need to properly split the paragraph and wrap the image
// Special case: a paragraph may contain only inline elements, but we want to be able to handle
// in a div, but for now we assume that all browsers can handle a div within a paragraph or
// files in single paragraphs. Maybe we need to properly split the paragraph and wrap the image
// a span in a meaningful way, even if that is not really allowed.
// in a div, but for now we assume that all browsers can handle a div within a paragraph or
} 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; }
// 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))
// Exclude any that are within an image note!
return null;
var up = img.parentNode.parentNode;
}
while ( up != document.body ) {
// Exclude any that are within an image note!
if ( LAPI.DOM.hasClass( up, IA.annotation_class ) ) { return null; }
var up = img.parentNode.parentNode;
up = up.parentNode;
while (up != document.body) {
}
if (LAPI.DOM.hasClass(up, IA.annotation_class)) return null;
return { width: w, height: h };
up = up.parentNode;
}
}
return {width: w, height: h};
}
 
function setup_one( (scope ) {
var file_div = scope;
var is_thumb =
scope != document &&
&& scope.nodeName.toLowerCase() == 'div' &&
&& LAPI.DOM.hasClass( scope, 'thumbinner' );
var is_other = scope.nodeName.toLowerCase() == 'img';
;
if ( is_other && self.imgs.length && scope == self.imgs[ 0 ] ) { return null; }
var is_other = scope.nodeName.toLowerCase() == 'img';
if (is_other && self.imgs.length && scope == self.imgs[0]document ) return null;{
file_div = LAPI.$( 'file' );
if (scope == document) {
} else if ( !is_thumb && !is_other ) {
file_div = LAPI.$('file');
file_div = getElementsByClassName( scope, 'div', 'wpImageAnnotatorFile' );
} else if (!is_thumb && !is_other) {
if ( !file_div || file_div.length != 1 ) { return null; }
file_div = getElementsByClassName(scope, 'div', 'wpImageAnnotatorFile');
if (! file_div ||= file_div.length != 1)[ return0 null];
}
file_div = file_div[0];
if ( !file_div ) { return null; }
}
var img = null;
if (!file_div) return null;
if ( scope == document ) {
var img = null;
img = LAPI.WP.getPreviewImage( mw.config.get( 'wgTitle' ) );
if (scope == document) {
// TIFFs may be multi-paged: allow only for single-page TIFFs
img = LAPI.WP.getPreviewImage(mw.config.get('wgTitle'));
if ( document.pageselector ) { img = null; }
// TIFFs may be multi-paged: allow only for single-page TIFFs
} else if ( is_other ) {
if ( document.pageselector ) img = null;
img = scope;
} else if (is_other) {
} else {
img = scope;
img = file_div.getElementsByTagName( 'img' );
} else {
if ( !img || img.length === 0 ) { return null; }
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 (!img) return null;
if ( !dim ) { return null; }
var dim = img_check (img, is_other);
// Conditionally exclude shared images.
if (!dim) return null;
if (
// Conditionally exclude shared images.
if ( scope != document &&
&& !self.rules.shared.show &&
&& ImageAnnotator_config.imageIsFromSharedRepository( img.src )
) { return null; }
)
var name return= null;
if ( scope == document ) {
var name = null;
name = mw.config.get( 'wgPageName' );
if (scope == document) {
} else {
name = mw.config.get('wgPageName');
name = LAPI.WP.pageFromLink( img.parentNode );
} else {
if ( !name ) { return null; }
name = LAPI.WP.pageFromLink(img.parentNode);
name = name.replace( / /g, '_' );
if (!name) return null;
if ( is_thumb || is_other ) {
name = name.replace(/ /g, '_');
var img_src = decodeURIComponent( img.getAttribute( 'src', 2 ) ).replace( / /g, '_' );
if (is_thumb || is_other) {
// img_src should have a component "/name" in there somewhere
var img_src = decodeURIComponent(img.getAttribute('src', 2)).replace(/ /g, '_');
var colon = name.indexOf( ':' );
// img_src should have a component "/name" in there somewhere
if ( colon <= 0 ) { return null; }
var colon = name.indexOf(':');
var img_name = name.substring( colon + 1 );
if (colon <= 0) return null;
if ( img_src.search( new RegExp( '/' + img_name.escapeRE() + '(/.*)?$' ) ) < 0 ) { return null; }
var img_name = name.substring(colon + 1);
// If the link is not going to file namespace, we won't find the full size later on and
if (img_src.search(new RegExp('/' + img_name.escapeRE() + '(/.*)?$')) < 0)
// thus we won't do anything with it.
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;
if (name.search(/\.(jpe?g|png|gif|svg|tiff?)$/i) < 0) return null; // Only PNG, JPE?G, GIF, SVG, and TIFF?
var no_caption = false;
// Finally check for wpImageAnnotatorControl
if ( is_thumb || is_other ) {
var icon_only = false;
var up = img.parentNode.parentNode;
var no_caption = false;
// Three levels is sufficient: thumbinner-thumb-control, or floatnone-center-control, or direct
if (is_thumb || is_other) {
for ( var i = 0; ++i <= var3 && up; up = imgup.parentNode.parentNode; ) {
if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorControl' ) ) {
// Three levels is sufficient: thumbinner-thumb-control, or floatnone-center-control, or direct
if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorOff' ) ) { return null; }
for (var i = 0; ++i <= 3 && up; up = up.parentNode) {
if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorControlwpImageAnnotatorIconOnly' ) ) { icon_only = true; }
if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorOffwpImageAnnotatorCaptionOff' ) ) return{ nullno_caption = true; }
break;
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,
return { scope: scope
realName: name,
,file_div: file_div
isThumbnail: is_thumb,
,img: img
isOther: is_other,
,realName: name
thumb: { width: dim.width, height: dim.height },
,isThumbnail: is_thumb
iconOnly: icon_only,
,isOther: is_other
noCaption: no_caption
,thumb: {width: dim.width, height: dim.height}
};
,iconOnly: icon_only
}
,noCaption: no_caption
};
}
 
function setup_images( (list ) {
Array.forEach( list,
{
function ( elem ) {
Array.forEach(list,
var desc = setup_one( elem );
function (elem) {
if ( desc ) { self.imgs[ self.imgs.length ] = desc; }
var desc = setup_one (elem);
}
if (desc) self.imgs[self.imgs.length] = desc;
);
}
}
);
}
 
if ( mw.config.get( 'wgNamespaceNumber' ) == 6 ) {
setup_images( ([ document ] );
self.may_edit = self.may_edit && ( self.imgs.length == 1 );
setup_images( (self.imgs_with_notes );
} else {
setup_images( (self.imgs_with_notes );
self.may_edit = self.may_edit && ( self.imgs.length == 1 );
}
}
 
self.may_edit = self.may_edit && ___location.href.search( /[?&]oldid=/ ) < 0;
 
if ( self.haveAjax ) {
setup_images( (self.thumbs );
setup_images( (self.other_images );
}
}
 
// Remove the test div
LAPI.DOM.removeNode( testImgDiv );
 
if ( self.imgs.length === 0 ) { return; }
 
// We get the UI texts in parallel, but wait for them at the beginning of complete_setup, where we
// need them. This has in particular a benefit if we do have to query for the file sizes below.
 
if ( self.imgs.length == 1 && self.imgs[ 0 ].scope == document && !self.haveAjax ) {
// Try to get the full size without Ajax.
self.imgs[ 0 ].full_img = LAPI.WP.fullImageSizeFromPage();
if ( self.imgs[ 0 ].full_img.width > 0 && self.imgs[ 0 ].full_img.height > 0 ) {
self.setup_step_two();
return;
}
}
}
}
 
// Get the full sizes of all the images. If more than 50, make several calls. (The API has limits.)
// Also avoid using Ajax on IE6...
 
var cache = {};
var names = [];
 
Array.forEach( self.imgs, function ( img, idx ) {
if ( cache[ img.realName ] ) {
cache[ img.realName ][ cache[ img.realName ].length ] = idx;
} else {
cache[ img.realName ] = [ idx ];
names[ names.length ] = img.realName;
}
}
} });
 
var to_do = names.length;
var done = 0;
 
function check_done( (length ) {
done += length;
{
if ( done +>= names.length; ) {
if ( typeof ImageAnnotator.info_callbacks !== 'undefined' ) { ImageAnnotator.info_callbacks = null; }
if (done >= names.length) {
self.setup_step_two();
if (typeof ImageAnnotator.info_callbacks !== 'undefined') ImageAnnotator.info_callbacks = null;
}
self.setup_step_two();
}
}
}
 
function make_calls( (execute_call, url_limit ) {
function build_titles( from, length, url_limit ) {
{
var done = 0;
function build_titles (from, length, url_limit)
var text = '';
{
for ( var i = from; i < from + length; i++ ) {
var done = 0;
var new_text = names[ i ];
var text = '';
if ( url_limit ) {
for (var i = from; i < from + length; i++) {
new_text = varencodeURIComponent( new_text = names[i]);
if ( text.length && ( text.length + new_text.length + 1 if> (url_limit ) ) { break; }
}
new_text = encodeURIComponent(new_text);
text if+= (text.length && (text.length +? new_text.length'|' + 1: >'' url_limit)) break+ new_text;
done++;
}
}
text += (text.length ? '|' : '') + new_text;
return { text: text, n: done };
done++;
}
}
return {text: text, n: done};
}
 
var start = 0, chunk = 0, params;
while ( to_do > 0 ) {
params = build_titles( (start, Math.min( 50, to_do ), url_limit );
execute_call( (params.n, params.text );
to_do -= params.n;
start += params.n;
}
}
}
}
 
function set_info( (json ) {
try {
{
if ( json && json.query && json.query.pages ) {
try {
function get_size( info ) {
if (json && json.query && json.query.pages) {
if ( !info.imageinfo || info.imageinfo.length === 0 ) { return; }
function get_size (info) {
var title = info.title.replace( / /g, '_' );
if (!info.imageinfo || info.imageinfo.length === 0) return;
var titleindices = info.cache[ title.replace(/ /g, '_')];
if ( !indices ) { return; }
var indices = cache[title];
Array.forEach(
if (!indices) return;
indices
Array.forEach(
, function ( i ) {
indices
self.imgs[ i ].full_img = { width: info.imageinfo[ 0 ].width,
, function (i) {
self.imgs[i].full_img = { width height: info.imageinfo[ 0 ].widthheight };
self.imgs[ i ].has_page = ( typeof info.missing === 'undefined' );
,height: info.imageinfo[0].height};
self.imgs[ i ].has_pageisLocal = (typeof!info.imagerepository || info.missingimagerepository === 'undefinedlocal');
if ( i != 0 || !self.may_edit || !info.protection || mw.config.get( 'wgNamespaceNumber' ) != 6 ) { return; }
self.imgs[i].isLocal = !info.imagerepository || info.imagerepository == 'local';
// Care about the protection settings
if (i != 0 || !self.may_edit || !info.protection || mw.config.get('wgNamespaceNumber') != 6) return;
var protection = Array.any( info.protection, function ( e ) {
// Care about the protection settings
return ( e.type == 'edit' ? e : null );
var protection = Array.any(info.protection, function (e) {
} );
return (e.type == 'edit' ? e : null);
self.may_edit =
});
!protection ||
self.may_edit =
( mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( protection.level ) );
!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 ] );
}
}
for (var page in json.query.pages) {
} // end if
get_size (json.query.pages[page]);
} catch ( ex ) {
}
}
} // end if
}
} catch (ex) {
}
}
 
if ( ( !window.XMLHttpRequest && !!window.ActiveXObject ) || !self.haveAjax ) {
// IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that
// prompt by using getScript instead of parseWikitext in this case.
ImageAnnotator.info_callbacks = [];
var template = mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php?action=query&rawcontinue=&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' )
IA.getScript(
template .replace( 'info_callbacks[].callback&titles=&', 'info_callbacks[&titles=' + idxtitles + '].callback&' ),
true // No local caching!
.replace('&titles=&', '&titles=' + titles + '&')
);
, true // No local caching!
// We do bypass the local JavaScript cache of importScriptURI, but on IE, we still may
);
// Weget dothe bypassscript from the local JavaScriptbrowser's cache, ofand importScriptURI,if butthat onhappens, IE, wemay stillexecute maythe
// script (and call the callback) synchronously before the assignment is done. Clean
// get the script from the browser's cache, and if that happens, IE may execute the
// up in that case.
// script (and call the callback) synchronously before the assignment is done. Clean
if (
// up in that case.
if ( ImageAnnotator.info_callbacks && ImageAnnotator.info_callbacks[ idx ] &&
&& ImageAnnotator.info_callbacks[ idx ].done && ImageAnnotator.info_callbacks[ idx ].script
) {
) {
LAPI.DOM.removeNode( ImageAnnotator.info_callbacks[ idx ].script );
ImageAnnotator.info_callbacks[ idx ].script = null;
}
}
},
}
( , (LAPI.Browser.is_ie ? 1950 : 4000 ) - template.length // Some slack for caching parameters
);
} else {
make_calls (
function ( length, titles ) {
LAPI.Ajax.apiGet(
'query'
, { titles : titles,
, prop: 'info|imageinfo',
, inprop : 'protection',
, iiprop : 'size'
}
}
, function ( request, json_result ) {
set_info( (json_result );
check_done( (length );
}
}
, function () { check_done( (length ); }
);
);
}
}
);
} // end if can use Ajax
},
 
setup_ui: function () {
// Complete the UI object we've gotten from config.
 
ImageAnnotator.UI.ready = false;
ImageAnnotator.UI.repo = null;
ImageAnnotator.UI.needs_plea = false;
 
var readyEvent = [];
 
ImageAnnotator.UI.fireReadyEvent = function () {
if ( ImageAnnotator.UI.ready ) { return; } // Already fired, nothing to do.
ImageAnnotator.UI.ready = true;
// Call all registered handlers, and clear the array.
Array.forEach( readyEvent , function ( f, idx ) {
try { f (); } catch ( ex ) {}
readyEvent[ idx ] = null;
} );
});
readyEvent = null;
};
 
ImageAnnotator.UI.addReadyEventHandler = function ( f ) {
if ( ImageAnnotator.UI.ready ) {
f (); // Already fired: call directly
} else {
readyEvent[ readyEvent.length ] = f;
}
}
};
 
ImageAnnotator.UI.setup = function () {
if ( ImageAnnotator.UI.repo ) { return; }
var self = ImageAnnotator.UI;
var node = LAPI.make( 'div', null, { display: 'none' } );
document.body.appendChild( node );
if ( typeof UIElements === 'undefined' ) {
self.basic = true;
self.repo = {};
for ( var item in self.defaults ) {
node.innerHTML = self.defaults[ item ];
self.repo[ item ] = node.firstChild;
LAPI.DOM.removeChildren( node );
}
}
} else {
self.basic = false;
self.repo = UIElements.emptyRepository( self.defaultLanguage );
for ( var item in self.defaults ) {
node.innerHTML = self.defaults[ item ];
UIElements.setEntry( item, self.repo, node.firstChild );
LAPI.DOM.removeChildren( node );
}
}
UIElements.load( 'wpImageAnnotatorTexts', null, null, self.repo );
}
}
LAPI.DOM.removeNode( node );
};
 
ImageAnnotator.UI.get = function ( id, basic, no_plea ) {
var self = ImageAnnotator.UI;
if ( !self.repo ) { self.setup(); }
var result = null;
var add_plea = false;
if ( self.basic ) {
result = self.repo[ id ];
} else {
result = UIElements.getEntry( id, self.repo, mw.config.get( 'wgUserLanguage' ), null );
add_plea = !result;
if ( !result ) { result = UIElements.getEntry( id, self.repo ); }
}
}
self.needs_plea = add_plea;
if ( !result ) { 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 () {
var self = ImageAnnotator.UI;
var translate = self.get( 'wpTranslate', false, true ) || 'translate';
var span = LAPI.make( 'small' );
span.appendChild( document.createTextNode( '\xa0(' ) );
span.appendChild(
LAPI.DOM.makeLink(
mw.config.get( 'wgServer' ) + mw.config.get( 'wgScript' ) + '?title=MediaWiki_talk:ImageAnnotatorTexts' +
+ '&action=edit&section=new&withJS=MediaWiki:ImageAnnotatorTranslator.js' +
+ '&language=' + mw.config.get( 'wgUserLanguage' ),
, translate,
( , (typeof translate === 'string' ? translate : LAPI.DOM.getInnerText( translate ).trim() )
)
)
);
span.appendChild( document.createTextNode( ')' ) );
return span;
};
 
ImageAnnotator.UI.init = function ( html_text_or_json ) {
var text;
if ( typeof html_text_or_json === 'string' ) {
text = html_text_or_json;
} else if (
typeof html_text_or_json !== 'undefined' &&
&& typeof html_text_or_json.parse !== 'undefined' &&
&& typeof html_text_or_json.parse.text !== 'undefined' &&
&& typeof html_text_or_json.parse.text[ '*' ] !== 'undefined'
) {
)
text = html_text_or_json.parse.text[ '*' ];
} else {
else
text = null;
}
 
if ( !text ) {
ImageAnnotator.UI.fireReadyEvent();
return;
}
}
 
var node = LAPI.make( 'div', null, { display: 'none' } );
document.body.appendChild( node );
try {
node.innerHTML = text;
} catch ( ex ) {
LAPI.DOM.removeNode( node );
node = null;
// Swallow. We'll just work with the default UI
}
}
if ( node && !ImageAnnotator.UI.repo ) { ImageAnnotator.UI.setup(); }
ImageAnnotator.UI.fireReadyEvent();
};
 
var ui_page = '{{MediaWiki:ImageAnnotatorTexts' +
( + (mw.config.get( 'wgUserLanguage' ) != mw.config.get( 'wgContentLanguage' ) ? '|lang=' + mw.config.get( 'wgUserLanguage' ) : '' ) +
+ '|live=1}}';
 
function get_ui_no_ajax () {
var url =
{
mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php?action=parse&pst&text=' +
var url =
encodeURIComponent( ui_page ) + '&title=API&prop=text&format=json' +
mw.config.get('wgServer') + mw.config.get('wgScriptPath') + '/api.php?action=parse&pst&text='
'&callback=ImageAnnotator.UI.init&maxage=14400&smaxage=14400';
+ encodeURIComponent(ui_page) + '&title=API&prop=text&format=json'
// Result cached for 4 hours. How to properly handle an error? It appears there's no way to catch
+ '&callback=ImageAnnotator.UI.init&maxage=14400&smaxage=14400'
// that on IE. (On FF, we could use an onerror handler on the script tag, but on FF, we use Ajax
;
// anyway.)
// Result cached for 4 hours. How to properly handle an error? It appears there's no way to catch
IA.getScript( url, true ); // No local caching!
// that on IE. (On FF, we could use an onerror handler on the script tag, but on FF, we use Ajax
}
// anyway.)
IA.getScript(url, true); // No local caching!
}
 
function get_ui () {
IA.haveAjax = ( LAPI.Ajax.getRequest() != null );
{
IA.ajaxQueried = true;
IA.haveAjax = (LAPI.Ajax.getRequest() != null);
IA.ajaxQueried = true;
 
// Works only with Ajax (but then, most of this script doesn't work without).
// Check what this does to load times... If lots of people used this, it might be better to
// have the UI texts included in some footer as we did on Special:Upload. True, everybody
// would get the texts, even people not using this, but the texts are small anyway...
if ( !IA.haveAjax ) {
get_ui_no_ajax (); // Fallback.
return;
}
}
 
LAPI.Ajax.parseWikitext(
ui_page,
, ImageAnnotator.UI.init,
, ImageAnnotator.UI.fireReadyEvent,
, false,
, null,
, " 'API"', // A fixed string to enable caching at all.
, 14400 // 4 hour caching.
);
} // end get_ui
 
if ( !window.XMLHttpRequest && !!window.ActiveXObject ) {
// IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that
// prompt by using getScript instead of parseWikitext in this case. The disadvantage
// is that we don't do anything if this fails for some reason.
get_ui_no_ajax ();
} else {
get_ui ();
}
}
},
 
setup_step_two: function () {
var self = IA;
 
// Throw out any images for which we miss either the thumbnail or the full image size.
// Also throws out thumbnails that are larger than the full image.
self.imgs = Array.select( self.imgs, function ( elem, idx ) {
var result =
elem.thumb.width > 0 && elem.thumb.height > 0 &&
&& typeof elem.full_img !== 'undefined' &&
&& elem.full_img.width > 0 && elem.full_img.height > 0 &&
&& elem.full_img.width >= elem.thumb.width &&
&& elem.full_img.height >= elem.thumb.height;
if ( self.may_edit && idx === 0 && !result ) { self.may_edit = false; }
;
return result;
if (self.may_edit && idx === 0 && !result) self.may_edit = false;
} );
return result;
});
 
if ( self.imgs.length === 0 ) { return; }
 
ImageAnnotator.UI.addReadyEventHandler( IA.complete_setup );
},
 
complete_setup: function () {
// We can be sure to have the UI here because this is called only when the ready event of the
// UI object is fired.
var self = IA;
 
// Check edit permissions
if ( self.may_edit && mw.config.get( 'wgRestrictionEdit' ) ) {
self.may_edit = (
( (mw.config.get( 'wgRestrictionEdit' ).length === 0 || mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( 'sysop' ) ) ||
(
|| ( mw.config.get('wgRestrictionEdit').length === 1 && mw.config.get('wgRestrictionEdit')[0] === 'autoconfirmed'
&& mw.config.get( 'wgUserGroupswgRestrictionEdit' ).length === 1 && mw.config.get( 'wgUserGroups').join(wgRestrictionEdit' ').contains('confirmed')[ //0 confirmed] &=== 'autoconfirmed' &&
mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( 'confirmed' ) // confirmed & autoconfirmed
)
)
);
);
}
}
 
if ( self.may_edit ) {
// Check whether the image is local. Don't allow editing if the file is remote.
var sharedUpload = getElementsByClassName( (document, 'div', 'sharedUploadNotice' );
self.may_edit = ( !sharedUpload || sharedUpload.length === 0 );
}
}
if ( self.may_edit && mw.config.get( 'wgNamespaceNumber' ) != 6 ) {
// Only allow edits if the stored page name matches the current one.
var img_page_name = getElementsByClassName( self.imgs[ 0 ].scope, '*', 'wpImageAnnotatorPageName' );
var img_page_name =
if ( img_page_name && img_page_name.length ) { img_page_name = LAPI.DOM.getInnerText( img_page_name[ 0 ] ); } else { img_page_name = ''; }
getElementsByClassName (self.imgs[0].scope, '*', 'wpImageAnnotatorPageName');
self.may_edit = ( img_page_name.replace( / /g, '_' ) == mw.config.get( 'wgTitle' ).replace( / /g, '_' ) );
if (img_page_name && img_page_name.length)
}
img_page_name = LAPI.DOM.getInnerText(img_page_name[0]);
else
img_page_name = '';
self.may_edit = (img_page_name.replace(/ /g, '_') == mw.config.get('wgTitle').replace(/ /g, '_'));
}
 
if ( self.may_edit && self.ajaxQueried ) { self.may_edit = self.haveAjax; }
 
// Now create viewers for all images
self.viewers = new Array( (self.imgs.length );
for ( var i = 0; i < self.imgs.length; i++ ) {
self.viewers[ i ] = new ImageNotesViewer( (self.imgs[ i ], i === 0 && self.may_edit );
}
}
 
if ( self.may_edit ) {
 
// Respect user override for zoom, if any
self.zoom_threshold = ImageAnnotator_config.zoom_threshold;
if (
if ( typeof window.ImageAnnotator_zoom_threshold !== 'undefined'
typeof window.ImageAnnotator_zoom_threshold !== 'undefined' &&
&& !isNaN (window.ImageAnnotator_zoom_threshold)
&& !isNaN( window.ImageAnnotator_zoom_threshold >=) 0.0&&
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 (
if ( self.viewers[0].full_img.width > 300
&& Math.min( self.viewers[ 0].factors.dx, self.viewers[0].factorsfull_img.dy)width >= 2.0300 &&
Math.min( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy ) >= 2.0
)
) {
if (
if ( self.viewers[0].thumb.width < 400
|| self.viewers[ 0 ].thumb.width /< self.viewers[0].thumb.height400 > 2.0||
|| self.viewers[ 0 ].thumb.heightwidth / self.viewers[ 0 ].thumb.widthheight > 2.0 ||
self.viewers[ 0 ].thumb.height / self.viewers[ 0 ].thumb.width > 2.0
)
) {
self.zoom_threshold = 0; // Force zooming
}
}
}
}
 
self.editor = new ImageAnnotationEditor ();
 
function track( (evt ) {
evt = evt || window.event;
if ( self.is_adding ) { self.update_zoom( evt ); }
if ( !self.is_tracking ) { return LAPI.Evt.kill( evt ); }
var mouse_pos = LAPI.Pos.mousePosition( evt );
if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) { return; }
var origin = LAPI.Pos.position( self.cover );
// Make mouse pos relative to cover
mouse_pos.x = mouse_pos.x - origin.x;
mouse_pos.y = mouse_pos.y - origin.y;
if ( mouse_pos.x >= self.base_x ) {
self.definer.style.width = ''String( + (mouse_pos.x - self.base_x ) + 'px';
self.definer.style.left = '' +String( self.base_x ) + 'px';
} else {
self.definer.style.width = ''String( + (self.base_x - mouse_pos.x ) + 'px';
self.definer.style.left = '' +String( mouse_pos.x ) + 'px';
}
}
if ( mouse_pos.y >= self.base_y ) {
self.definer.style.height = ''String( + (mouse_pos.y - self.base_y ) + 'px';
self.definer.style.top = '' +String( self.base_y ) + 'px';
} else {
self.definer.style.height = ''String( + (self.base_y - mouse_pos.y ) + 'px';
self.definer.style.top = '' +String( mouse_pos.y ) + 'px';
}
}
return LAPI.Evt.kill( evt );
}
}
 
function pause( (evt ) {
LAPI.Evt.remove( document, 'mousemove', track, true );
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( null ); }
self.move_listening = false;
document.captureEvents(null);
}
self.move_listening = false;
}
 
function resume( (evt ) {
// captureEvents is actually deprecated, but I haven't succeeded to make this work with
// addEventListener only.
if ( ( self.is_tracking || self.is_adding ) && !self.move_listening ) {
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( Event.MOUSEMOVE ); }
LAPI.Evt.attach( document, 'mousemove', track, true );
document.captureEvents(Event.MOUSEMOVE);
self.move_listening = true;
LAPI.Evt.attach(document, 'mousemove', track, true);
}
self.move_listening = true;
}
}
}
 
function stop_tracking( (evt ) {
evt = evt || window.event;
// Check that we're within the image. Note: this check can fail only on IE >= 7, on other
// browsers, we attach the handler on self.cover and thus we don't even get events outside
// that range.
var mouse_pos = LAPI.Pos.mousePosition( evt );
if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) { return; }
if ( self.is_tracking ) {
self.is_tracking = false;
self.is_adding = false;
// Done.
pause ();
if ( LAPI.Browser.is_ie ) {
// Trust Microsoft to get everything wrong!
LAPI.Evt.remove( document, 'mouseup', stop_tracking );
} else {
LAPI.Evt.remove( self.cover, 'mouseup', stop_tracking );
}
}
LAPI.Evt.remove( window, 'blur', pause );
LAPI.Evt.remove( window, 'focus', resume );
self.cover.style.cursor = 'auto';
LAPI.DOM.removeNode( self.border );
LAPI.Evt.remove( self.cover, self.mouse_in, self.update_zoom_evt );
LAPI.Evt.remove( self.cover, self.mouse_out, self.hide_zoom_evt );
self.hide_zoom();
self.viewers[ 0 ].hide(); // Hide all existing boxes
if ( !self.definer || self.definer.offsetWidth <= 0 || self.definer.offsetHeight <= 0 ) {
// Nothing: just remove the definer:
if ( self.definer ) { LAPI.DOM.removeNode( self.definer ); }
// Re-attach event handlers
self.viewers[ 0 ].setShowHideEvents( true );
self.hide_cover();
self.viewers[ 0 ].setDefaultMsg();
// And make sure we get the real view again
self.viewers[ 0 ].show();
} else {
// We have a div with some extent: remove event capturing and create a new annotation
var new_note = new ImageAnnotation( (self.definer, self.viewers[ 0 ], -1 );
self.viewers[ 0 ].register( new_note );
self.editor.editNote( new_note );
}
}
self.definer = null;
}
}
if ( evt ) { return LAPI.Evt.kill( evt ); }
return false;
}
}
 
function start_tracking( (evt ) {
if ( !self.is_tracking ) {
self.is_tracking = true;
evt = evt || window.event;
// Set the position, size 1
var mouse_pos = LAPI.Pos.mousePosition( evt );
var origin = LAPI.Pos.position( self.cover );
self.base_x = mouse_pos.x - origin.x;
self.base_y = mouse_pos.y - origin.y;
Object.merge(
{ left: '' +String( self.base_x ) + 'px',
, top: '' +String( self.base_y ) + 'px',
, width: '0px',
, height : '0px',
, display: ''
}
}
, self.definer.style
);
);
// Set mouse handlers
LAPI.Evt.remove( self.cover, 'mousedown', start_tracking );
if ( LAPI.Browser.is_ie ) {
LAPI.Evt.attach( document, 'mouseup', stop_tracking ); // Doesn't work properly on self.cover...
} else {
LAPI.Evt.attach( self.cover, 'mouseup', stop_tracking );
}
}
resume ();
LAPI.Evt.attach( window, 'blur', pause );
LAPI.Evt.attach( window, 'focus', resume );
}
}
if ( evt ) { return LAPI.Evt.kill( evt ); }
return false;
}
}
 
function add_new( (evt ) {
if ( !self.canEdit() ) { return; }
 
self.editor.hide_editor();
Tooltips.close();
var cover = self.get_cover();
cover.style.cursor = 'crosshair';
self.definer = LAPI.make(
'div', null,
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 = '';
(ImageAnnotator.UI.get('wpImageAnnotatorDrawRectMsg', false));
}
self.viewers[0].msg.style.display = '';
}
 
self.button_div = LAPI.make( 'div' );
self.viewers[ 0 ].main_div.appendChild( self.button_div );
self.add_button = LAPI.DOM.makeButton(
'ImageAnnotationAddButton',
LAPI.DOM.makeButton(
ImageAnnotator.UI.get( 'wpImageAnnotatorAddButtonText', true ),
'ImageAnnotationAddButton'
add_new
, ImageAnnotator.UI.get('wpImageAnnotatorAddButtonText', true)
);
, add_new
var add_plea = ImageAnnotator.UI.needs_plea;
);
self.button_div.appendChild( self.add_button );
var add_plea = ImageAnnotator.UI.needs_plea;
self.help_link = self.createHelpLink();
self.button_div.appendChild(self.add_button);
if ( self.help_link =) self.createHelpLink();{
self.button_div.appendChild( document.createTextNode( '\xa0' ) );
if (self.help_link) {
self.button_div.appendChild(document self.createTextNode('\xa0')help_link );
}
self.button_div.appendChild(self.help_link);
if ( add_plea && mw.config.get( 'wgServer' ).contains( '/commons' ) ) { self.button_div.appendChild( ImageAnnotator.UI.get_plea() ); }
}
if (add_plea && mw.config.get('wgServer').contains('/commons'))
self.button_div.appendChild(ImageAnnotator.UI.get_plea());
 
} // end if may_edit
 
// Get the file description pages of thumbnails. Figure out for which viewers we need to do this.
var cache = {};
var get_local = [];
var get_foreign = [];
Array.forEach( self.viewers, function ( viewer, idx ) {
if ( viewer.setup_done || viewer.isLocal && !viewer.has_page ) { return; }
// Handle only images that either are foreign or local and do have a page.
if ( cache[ viewer.realName ] ) {
cache[ viewer.realName ][ cache[ viewer.realName ].length ] = idx;
} else {
cache[ viewer.realName ] = [ idx ];
if ( !viewer.has_page ) {
get_foreign[ get_foreign.length ] = viewer.realName;
} else {
get_local[ get_local.length ] = viewer.realName;
}
}
}
}
} });
 
if ( get_local.length === 0 && get_foreign.length === 0 ) { return; }
 
// Now we have unique page names in the cache and in to_get. Go get the corresponding file
// description pages. We make a series of simultaneous asynchronous calls to avoid hitting
// API limits and to keep the URL length below the limit for the foreign_repo calls.
 
function make_calls( (list, execute_call, url_limit ) {
function composer( list, from, length, url_limit ) {
{
function composercompose( (list, from, length, url_limit ) {
var text = '';
{
var done = 0;
function compose (list, from, length, url_limit)
for ( var i = from; i < from + length; i++ ) {
{
var textnew_text = '';
'<div class="wpImageAnnotatorInlineImageWrapper" style="display:none;">' +
var done = 0;
'<span class="image_annotation_inline_name">' + list[ i ] + '</span>' +
for (var i = from; i < from + length; i++) {
'{{:' + list[ i ] + '}}' + // Leading dot to avoid getting links to the full images if we hit a parser limit
var new_text =
'</div>';
'<div class="wpImageAnnotatorInlineImageWrapper" style="display:none;">'
if ( url_limit ) {
+ '<span class="image_annotation_inline_name">' + list[i] + '</span>'
new_text = encodeURIComponent( new_text );
+ '{{:' + list[i] + '}}' // Leading dot to avoid getting links to the full images if we hit a parser limit
if ( text.length && ( text.length + new_text.length > url_limit ) ) { break; }
+ '</div>'
}
;
text = text + new_text;
if (url_limit) {
done++;
new_text = encodeURIComponent(new_text);
// Additionally, limit the number of image pages to get: these can be large, and the server
if (text.length && (text.length + new_text.length > url_limit)) break;
// 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
text = text + new_text;
// many thumbs to large images on the page.
done++;
if ( done == 5 ) { break; }
// 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
return { text: text, n: done };
// in the result, which would make us load the full images, which is desastrous if there are
}
// many thumbs to large images on the page.
if (done == 5) break;
}
return {text: text, n: done};
}
 
var param = compose( (list, from, length, url_limit );
execute_call( (param.text );
return param.n;
}
}
 
var start = 0, chunk = 0, to_do = list.length;
while ( to_do > 0 ) {
chunk = composer( (list, start, Math.min( 50, to_do ), url_limit );
to_do -= chunk;
start += chunk;
}
}
}
}
 
var divRE = /(<\s*div\b)|(<\/\s*div\s*>)/ig;
var blockStart = '<div class="wpImageAnnotatorInlineImageWrapper"';
var inlineNameEnd = '</span>';
var noteStart = '<div id="image_annotation_note_';
var noteControlRE = /<div\s*class="(wpImageAnnotatorInlinedRules|image_annotation_colors")(\S|\s)*?\/div>/g;
 
// Our parse request returns the full html of the description pages' contents, including any
// license or information or other templates that we don't care about, and which may contain
// additional images we'd rather not load when we add this (X)HTML to the DOM. Therefore, we
// strip out everything but the notes.
function strip_noise( (html ) {
var result = '';
var m;
// First, get rid of HTML comments and scripts
html = html.replace( /<\!--(.|\s)*?--\>/g, '' ).replace( /<script(.|\s)*?\/script>/g, '' );
var i = html.indexOf( blockStart, idx ), idx = 0, l = html.length;
// Now collect pages
while ( idx < l && i >= idx ) {
var j = html.indexOf( inlineNameEnd, i + blockStart.length );
if ( j < i + blockStart.length ) { break; }
result += html.substring( i, j + inlineNameEnd.length );
idx = j + inlineNameEnd.length;
// Now collect all image image notes for that page
var note_begin = 0, next_block = html.indexOf( blockStart, idx );
// Do we have image note control or color templates?
j = idx;
for ( ;; ) {
noteControlRE.lastIndex = j;
m = noteControlRE.exec( html );
if ( !m || next_block >= idx && m.index > next_block ) { break; }
result += m[ 0 ];
j = m.index + m[ 0 ].length;
}
}
// Collect notes
for ( ;; ) {
note_begin = html.indexOf( noteStart, idx );
// Check whether next wrapper comes first
if ( note_begin < idx || ( next_block >= idx && note_begin > next_block ) ) { break; }
// Start parsing nested <div> and </div>, from note_begin on. Just ignore any other tags.
var level = 1, k = note_begin + noteStart.length;
while ( level > 0 && k < l ) {
divRE.lastIndex = k;
m = divRE.exec( html );
if ( !m || m.length < 2 ) {
k = l; // Nothing found at all?
} else {
if ( m[ 1 ] ) {
level++; k = m.index + m[ 1 ].length; // divStart found first
} else if ( m.length > 2 && m[ 2 ] ) {
level--; k = m.index + m[ 2 ].length; // divEnd found first
} else {
k = l; // Huh?
}
}
}
}
} // end loop for nested divs
result += html.substring( note_begin, k );
while ( level-- > 0 ) { result += '</div>'; } // Missing ends.
idx = k;
} // end loop collecting notes
result += '</div>'; // Close the wrapper
i = next_block;
} // end loop collecting pages
return result;
}
}
 
function setup_thumb_viewers( (html_text ) {
var node = LAPI.make( 'div', null, { display: 'none' } );
{
document.body.appendChild( node );
var node = LAPI.make('div', null, {display: 'none'});
try {
document.body.appendChild(node);
node.innerHTML = strip_noise( html_text );
try {
var pages = getElementsByClassName( node, 'div', 'wpImageAnnotatorInlineImageWrapper' );
node.innerHTML = strip_noise (html_text);
for ( var i = 0; pages && i < pages.length; i++ ) {
var pages = getElementsByClassName (node, 'div', 'wpImageAnnotatorInlineImageWrapper');
for ( var inotes = 0;getElementsByClassName( pages &&[ i <], pages'div', self.length;annotation_class i++) {;
if ( !notes || notes.length === 0 ) {
var notes = getElementsByClassName (pages[i], 'div', self.annotation_class);
continue;
if (!notes || notes.length === 0) {
}
continue;
var page = self.getItem( 'inline_name', pages[ i ] );
}
if ( !page ) { continue; }
var page = self.getItem('inline_name', pages[i]);
page = page.replace( / /g, '_' );
if (!page) continue;
var viewers = cache[ page ] || cache[ 'File:' + page =.substring( page.replaceindexOf(/ /g, '_:' ) + 1 ) ];
if ( !viewers || viewers.length === 0 ) {
var viewers = cache[page] || cache['File:' + page.substring(page.indexOf(':') + 1)];
continue;
if (!viewers || viewers.length === 0) {
}
continue;
// Update rules.
}
var rules = getElementsByClassName( pages[ i ], 'div', 'wpImageAnnotatorInlinedRules' );
// Update rules.
var local_rules = {
var rules = getElementsByClassName (pages[i], 'div', 'wpImageAnnotatorInlinedRules');
inline: Object.clone( IA.rules.inline ),
var local_rules = {
inline thumbs: Object.clone( IA.rules.inlinethumbs ),
};
thumbs: Object.clone(IA.rules.thumbs)
if ( rules && rules.length ) {
};
rules = rules[ 0 ];
if (rules && rules.length) {
if (
rules = rules[0];
if ( typeof local_rules.inline.show === 'undefined' &&
&& LAPI.DOM.hasClass( rules, 'wpImageAnnotatorNoInlineDisplay' )
) {
)
local_rules.inline.show = false;
{
}
local_rules.inline.show = false;
if (
}
if ( typeof local_rules.inline.icon === 'undefined' &&
&& LAPI.DOM.hasClass( rules, 'wpImageAnnotatorInlineDisplayIcon' )
) {
)
local_rules.inline.icon = true;
{
}
local_rules.inline.icon = true;
if (
}
if ( typeof local_rules.thumbs.show === 'undefined' &&
&& LAPI.DOM.hasClass( rules, 'wpImageAnnotatorNoThumbs' )
) {
)
local_rules.thumbs.show = false;
{
}
local_rules.thumbs.show = false;
if (
}
if ( typeof local_rules.thumbs.icon === 'undefined' &&
&& LAPI.DOM.hasClass( rules, 'wpImageAnnotatorThumbDisplayIcon' )
) {
)
local_rules.thumbs.icon = true;
{
}
local_rules.thumbs.icon = true;
}
}
// Make sure all are set
}
local_rules.inline.show =
// Make sure all are set
typeof local_rules.inline.show === 'undefined' || local_rules.inline.show =;
typeof local_rules.inlinethumbs.show === 'undefined' || local_rules.inline.show;
typeof local_rules.thumbs.show === 'undefined' || local_rules.thumbs.show =;
local_rules.inline.icon =
typeof local_rules.thumbs.show === 'undefined' || local_rules.thumbs.show;
typeof local_rules.inline.icon !== 'undefined' && local_rules.inline.icon =;
typeof local_rules.inlinethumbs.icon !== 'undefined' && local_rules.inline.icon;
typeof local_rules.thumbs.icon !== 'undefined' && local_rules.thumbs.icon =;
if ( !local_rules.inline.show ) { continue; }
typeof local_rules.thumbs.icon !== 'undefined' && local_rules.thumbs.icon;
// Now use pages[i] as a scope shared by all the viewers using it. Since we clone note
if (!local_rules.inline.show) continue;
// contents for note display, this works. Otherwise, we'd have to copy the notes into
// Now use pages[i] as a scope shared by all the viewers using it. Since we clone note
// each viewer's scope.
// contents for note display, this works. Otherwise, we'd have to copy the notes into
document.body.appendChild( pages[ i ] ); // Move it out of 'node'
// each viewer's scope.
// Set viewers' scopes and finish their setup.
document.body.appendChild(pages[i]); // Move it out of 'node'
// Set Array.forEach( viewers', scopesfunction and( finishv their) setup.{
if ( !self.viewers[ v ].isThumbnail || local_rules.thumbs.show ) {
Array.forEach(viewers, function (v) {
if (! self.viewers[ v ].isThumbnailscope ||= local_rules.thumbs.show)pages[ i {];
self.viewers[ v ].setup( self.viewers[ v ].scopeisThumbnail =&& pages[i];local_rules.thumbs.icon ||
self.viewers[ v].setup( self.viewers[v].isThumbnailisOther && local_rules.thumbsinline.icon );
}
|| self.viewers[v].isOther && local_rules.inline.icon);
} );
}
}
});
} catch ( ex ) {}
LAPI.DOM.removeNode( node );
} catch (ex) {}
}
LAPI.DOM.removeNode(node);
}
 
ImageAnnotator.script_callbacks = [];
 
function make_script_calls( (list, api ) {
var template = api + '?action=parse&pst&text=&prop=text&format=json' +
{
'&maxage=1800&smaxage=1800&uselang=' + mw.config.get( 'wgUserLanguage' ) + // see bugzilla 22764
var template = api + '?action=parse&pst&text=&prop=text&format=json'
'&callback=ImageAnnotator.script_callbacks[].callback';
+ '&maxage=1800&smaxage=1800&uselang=' + mw.config.get('wgUserLanguage') //see bugzilla 22764
if ( template.startsWith( '//' ) ) { template = document.___location.protocol + template; } // Avoid protocol-relative URIs (IE7 bug)
+ '&callback=ImageAnnotator.script_callbacks[].callback';
make_calls(
if (template.startsWith('//')) template = document.___location.protocol + template; // Avoid protocol-relative URIs (IE7 bug)
list,
make_calls (
function ( text ) {
list
var idx = ImageAnnotator.script_callbacks.length;
, function (text) {
ImageAnnotator.script_callbacks[ idx ] = {
var idx = ImageAnnotator.script_callbacks.length;
callback: function ( json ) {
ImageAnnotator.script_callbacks[idx] =
if ( json && json.parse && json.parse.text && json.parse.text[ '*' ] ) {
{ callback: function (json) {
if setup_thumb_viewers(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 ].donescript =) true;{
LAPI.DOM.removeNode( if (ImageAnnotator.script_callbacks[ idx ].script) {);
LAPI.DOM.removeNode( ImageAnnotator.script_callbacks[ idx ].script) = null;
}
ImageAnnotator.script_callbacks[idx].script = null;
},
}
done: false
}
};
,done: false
ImageAnnotator.script_callbacks[ idx ].script =
};
IA.getScript(
ImageAnnotator.script_callbacks[idx].script =
template.replace( 'script_callbacks[].callback', 'script_callbacks[' + idx + '].callback' )
IA.getScript(
template .replace( 'script_callbacks[].callback&text=&', 'script_callbacks[&text=' + idxtext + '].callback&' ),
true // No local caching!
.replace('&text=&', '&text=' + text + '&')
);
, true // No local caching!
if (
);
if ( ImageAnnotator.script_callbacks && ImageAnnotator.script_callbacks[ idx ] &&
&& ImageAnnotator.script_callbacks[ idx ].done && ImageAnnotator.script_callbacks[ idx ].script)
) {
{
LAPI.DOM.removeNode( ImageAnnotator.script_callbacks[ idx ].script );
ImageAnnotator.script_callbacks[ idx ].script = null;
}
}
},
}
( , (LAPI.DOM.is_ie ? 1950 : 4000 ) - template.length // Some slack for caching parameters
);
}
}
 
if ( ( !window.XMLHttpRequest && !!window.ActiveXObject ) || !self.haveAjax ) {
make_script_calls( get_local, mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php' );
} else {
make_calls(
get_local,
, function ( text ) {
LAPI.Ajax.parseWikitext(
text,
text
, function ( html_text ) { if ( html_text ) { setup_thumb_viewers( html_text ); } },
, function () {},
, false,
null,
, null
, 'API', // Fixed string to enable caching at all
, 1800 // 30 minutes caching.
);
);
}
}
);
}
}
 
// Can't use Ajax for foreign repo, might violate single-origin policy (e.g. from wikisource.org
// to wikimedia.org). Attention, here we must care about the URL length! IE has a limit of 2083
// character (2048 in the path part), and servers also may impose limits (on the WMF servers,
// the limit appears to be 8kB).
make_script_calls( (get_foreign, ImageAnnotator_config.sharedRepositoryAPI() );
},
 
show_zoom: function () {
var self = IA;
if (
if ( ( self.viewers[0].factors.dx < self.zoom_threshold
(
&& self.viewers[0].factors.dy < self.zoom_threshold
self.viewers[ 0 ].factors.dx < self.zoom_threshold &&
)
|| Math.max( self.viewers[ 0].factors.dx, self.viewers[0].factors.dy) < 2self.0zoom_threshold
) ||
)
Math.max( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy ) < 2.0
{
) {
// Below zoom threshold, or full image not even twice the size of the preview
// Below zoom threshold, or full image not even twice the size of the preview
return;
return;
}
}
if (!self.zoom) {
if ( !self.zoom =) {
self.zoom = LAPI.make(
'div',
{ , {id : 'image_annotator_zoom' },
{
, { overflow: 'hidden'
overflow: 'hidden',
,width: '200px'
,height width: '200px',
height: '200px',
,position: 'absolute'
position: 'absolute',
,display: 'none'
display: 'none',
,top: '0px'
,left top: '0px',
left: '0px',
,border: '2px solid #666666'
border: '2px solid #666666',
,backgroundColor : 'white'
backgroundColor: 'white',
,zIndex: 2050 // On top of everything
zIndex: 2050 // On top of everything
}
}
);
);
var src = self.viewers[0].img.getAttribute('src', 2);
var src = self.viewers[ 0 ].img.getAttribute( 'src', 2 );
// Adjust zoom_factor
// Adjust zoom_factor
if (self.zoom_factor > self.viewers[0].factors.dx || self.zoom_factor > self.viewers[0].factors.dy)
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 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.
var zoom_src = null;
if ( zoom_width > 0.9 * self.viewers[ 0 ].full_img.width && src.search( /\.svg\.png$/i ) < 0 ) {
// For SVGs, always use a scaled PNG for the zoom.
// If the thumb we'd be loading was within about 80% of the full image size, we may just as
if (zoom_width > 0.9 * self.viewers[0].full_img.width && src.search(/\.svg\.png$/i) < 0) {
// Ifwell the thumb we'd be loading was within about 80% ofget the full image size,instead weof maya justscaled asversion.
self.zoom_factor = Math.min( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy );
// well get the full image instead of a scaled version.
self.zoom_factor zoom_width = Math.min(self.viewers[ 0].factors.dx, self.viewers[0].factorsfull_img.dy)width;
zoom_width zoom_height = self.viewers[ 0 ].full_img.widthheight;
}
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...
// Construct the initial zoomed image. We need to clone; if we create a completely
var zoomed = self.viewers[ 0 ].img.cloneNode( true );
// new DOM node ourselves, it may not work on IE6...
zoomed.width = String( zoom_width );
var zoomed = self.viewers[0].img.cloneNode(true);
zoomed.widthheight = ''String( +zoom_height zoom_width);
Object.merge( { position: 'absolute', top: '0px', left: '0px' }, zoomed.style );
zoomed.height = '' + zoom_height;
self.zoom.firstChild.appendChild( zoomed );
Object.merge({position: 'absolute', top: '0px',left: '0px'}, zoomed.style);
// Crosshair
self.zoom.firstChild.appendChild(zoomed);
self.zoom.firstChild.appendChild(
// Crosshair
LAPI.make(
self.zoom.firstChild.appendChild(
'div', null,
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'
width: '200px',
,height: '1px'
height: '1px',
,borderTop : '1px solid red'
borderTop: '1px solid red',
,position: 'absolute'
position: 'absolute',
,top: '100px'
top: '100px',
,left: '0px'
left: '0px'
}
}
)
)
);
);
document.body.appendChild(self.zoom);
document.body.appendChild( self.zoom );
LAPI.DOM.loadImage(
LAPI.DOM.loadImage(
self.viewers[0].imgName
self.viewers[ 0 ].imgName,
, src
src,
, zoom_width
zoom_width,
, zoom_height
zoom_height,
, ImageAnnotator_config.thumbnailsGeneratedAutomatically()
ImageAnnotator_config.thumbnailsGeneratedAutomatically(),
, function (img) {
function ( img ) {
// Replace the image in self.zoom by self.zoom_loader, making sure we keep the offsets
// Replace the image in self.zoom by self.zoom_loader, making sure we keep the offsets
img.style.position = 'absolute';
img.style.position = 'absolute';
img.style.top = self.zoom.firstChild.firstChild.style.top;
img.style.lefttop = self.zoom.firstChild.firstChild.style.lefttop;
img.style.left = self.zoom.firstChild.firstChild.style.left;
img.style.display = '';
img.style.display = '';
self.zoom.firstChild.replaceChild(img, self.zoom.firstChild.firstChild);
self.zoom.firstChild.replaceChild( img, self.zoom.firstChild.firstChild );
}
}
);
);
}
}
self.zoom.style.display = 'none'; // Will be shown in update
self.zoom.style.display = 'none'; // Will be shown in update
},
},
 
update_zoom: function ( evt ) {
if ( !evt ) { return; } // We need an event to calculate positions!
var self = IA;
if ( !self.zoom ) { return; }
var mouse_pos = LAPI.Pos.mousePosition( evt );
var origin = LAPI.Pos.position( self.cover );
if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) {
IA.hide_zoom();
return;
}
}
var dx = mouse_pos.x - origin.x;
var dy = mouse_pos.y - origin.y;
// dx, dy is the offset within the preview image. Align the zoom image accordingly.
var top = - dy * self.zoom_factor + 100;
var left = - dx * self.zoom_factor + 100;
self.zoom.firstChild.firstChild.style.top = '' +String( top ) + 'px';
self.zoom.firstChild.firstChild.style.left = '' +String( left ) + 'px';
self.zoom.style.top = mouse_pos.y + 10 + 'px'; // Right below the mouse pointer
// Horizontally keep it in view.
var x = ( self.is_rtl ? mouse_pos.x - 10 : mouse_pos.x + 10 );
if ( x < 0 ) { x = 0; }
self.zoom.style.left = x + 'px';
self.zoom.style.display = '';
// Now that we have offsetWidth, correct the position.
if ( self.is_rtl ) {
x = mouse_pos.x - 10 - self.zoom.offsetWidth;
if ( x < 0 ) { x = 0; }
} else {
var off = LAPI.Pos.scrollOffset();
var view = LAPI.Pos.viewport();
if ( x + self.zoom.offsetWidth > off.x + view.x ) { x = off.x + view.x - self.zoom.offsetWidth; }
if ( x < 0 ) { x = 0; }
}
}
self.zoom.style.left = x + 'px';
},
 
hide_zoom: function ( evt ) {
if ( !IA.zoom ) { return; }
if ( evt ) {
var mouse_pos = LAPI.Pos.mousePosition( evt );
if ( LAPI.Pos.isWithin( IA.cover, mouse_pos.x, mouse_pos.y ) ) { return; }
}
}
IA.zoom.style.display = 'none';
},
 
createHelpLink: function () {
var msg = ImageAnnotator.UI.get( 'wpImageAnnotatorHelp', false, true );
if ( !msg || !msg.lastChild ) { return null; }
// Make sure we use the right protocol for all images:
var imgs = msg.getElementsByTagName( 'img' );
var text;
var tgt;
if ( imgs ) {
for ( var i = 0; i < imgs.length; i++ ) {
var srcFixed = imgs[ i ].getAttribute( 'src', 2 ).replace( /^https?\:/, document.___location.protocol );
imgs[ i ].src = srcFixed;
}
}
}
}
if (
msg.childNodes.length == 1 &&
&& msg.firstChild.nodeName.toLowerCase() == 'a' &&
&& !LAPI.DOM.hasClass( msg.firstChild, 'image' )
) {
msg.firstChild.id = 'ImageAnnotationHelpButton';
return msg.firstChild; // Single link
}
}
// Otherwise, it's either a sequence of up to three images, or a span, followed by a
// link.
tgt = msg.lastChild;
if ( tgt.nodeName.toLowerCase() != 'a' ) { tgt = mw.config.get( 'wgServer' ) + mw.config.get( 'wgArticlePath' ).replace( '$1', 'Help:Gadget-ImageAnnotator' ); } else { tgt = tgt.href; }
tgt = mw.config.get('wgServer') + mw.config.get('wgArticlePath').replace('$1', 'Help:Gadget-ImageAnnotator');
else
tgt = tgt.href;
 
function make_handler( (tgt ) {
var target = tgt;
return function ( evt ) {
var e = evt || window.event;
___location.href = target;
if ( e ) { return LAPI.Evt.kill( e ); }
return false;
};
};
}
}
 
imgs = msg.getElementsByTagName( 'img' );
 
if ( !imgs || !imgs.length ) {
// We're supposed to have a spans giving the button text
text = msg.firstChild;
if ( text.nodeName.toLowerCase() === 'span' ) { text = LAPI.DOM.getInnerText( text ); } else { text = 'Help'; }
text = return LAPI.DOM.getInnerTextmakeButton(text);
'ImageAnnotationHelpButton'
else
, text = 'Help';
, make_handler( tgt )
return LAPI.DOM.makeButton(
);
'ImageAnnotationHelpButton'
} else {
, text
return Buttons.makeButton( imgs, 'ImageAnnotationHelpButton', make_handler( tgt ) );
}
);
},
} else {
return Buttons.makeButton(imgs, 'ImageAnnotationHelpButton', make_handler(tgt));
}
},
 
get_cover: function () {
var self = IA;
var shim;
if ( !self.cover ) {
var pos = { position : 'absolute',
, left: '0px',
, top: '0px',
, width: self.viewers[ 0 ].thumb.width + 'px',
, height: self.viewers[ 0 ].thumb.height + 'px'
};
};
self.cover = LAPI.make( 'div', null, pos );
self.border = self.cover.cloneNode( false );
Object.merge(
{ {border: '3px solid green', top: '-3px', left: '-3px' }, self.border.style );
self.cover.style.zIndex = 2000; // Above the tooltips
if ( LAPI.Browser.is_ie ) {
shim = LAPI.make( 'iframe', { frameBorder: 0, tabIndex: -1 }, pos );
shim.style.filter = 'alpha(Opacity=0)'; // Ensure transparency
// Unfortunately, IE6/SP2 has a "security setting" called "Binary and script
// behaviors". If that is disabled, filters don't work, and our iframe would
// appear as a white rectangle. Fix this by first placing the iframe just above
// image (to block that windowed control) and then placing *another div* just
// above that shim having the image as its background image.
var imgZ = self.viewers[ 0 ].img.style.zIndex;
if ( isNaN( (imgZ ) ) { imgZ = 10; } // Arbitrary, positive, > 1, < 500
shim.style.zIndex = imgZ + 1;
self.ieFix = shim;
// And now the bgImage div...
shim = LAPI.make( 'div', null, pos );
Object.merge(
{ top: '0px',
, backgroundImage: 'url(' + self.viewers[ 0 ].img.src + ')',
, zIndex: imgZ + 2
}
}
, shim.style
);
);
self.ieFix2 = shim;
}
}
if ( LAPI.Browser.is_opera ) {
// It appears that events just pass through completely transparent divs on Opera.
// Hence we have to ensure that these events are killed even if our cover doesn't
// handle them.
shim = LAPI.make( 'div', null, pos );
shim.style.zIndex = self.cover.style.zIndex - 1;
LAPI.Evt.attach( shim, 'mousemove',
function ( evt ) { return LAPI.Evt.kill( evt || window.event ); } );
LAPI.Evt.attach( shim, 'mousedown',
function ( evt ) { return LAPI.Evt.kill( evt || window.event ); } );
LAPI.Evt.attach( shim, 'mouseup',
function ( evt ) { return LAPI.Evt.kill( evt || window.event ); } );
shim.style.cursor = 'default';
self.eventFix = shim;
}
}
self.cover_visible = false;
}
}
return self.cover;
},
 
show_cover: function () {
var self = IA;
if ( self.cover && !self.cover_visible ) {
if ( self.ieFix ) {
self.viewers[ 0 ].img_div.appendChild( self.ieFix );
self.viewers[ 0 ].img_div.appendChild( self.ieFix2 );
}
}
if ( self.eventFix ) { self.viewers[ 0 ].img_div.appendChild( self.eventFix ); }
self.viewers[ 0 ].img_div.appendChild( self.cover );
self.cover_visible = true;
}
}
},
 
hide_cover: function () {
var self = IA;
if ( self.cover && self.cover_visible ) {
if ( self.ieFix ) {
LAPI.DOM.removeNode( self.ieFix );
LAPI.DOM.removeNode( self.ieFix2 );
}
}
if ( self.eventFix ) { LAPI.DOM.removeNode( self.eventFix ); }
LAPI.DOM.removeNode( self.cover );
self.cover_visible = false;
}
}
},
 
getRawItem: function ( what, scope ) {
var node = null;
if ( !scope || scope == document ) {
node = LAPI.$( ('image_annotation_' + what );
} else {
node = getElementsByClassName( (scope, '*', 'image_annotation_' + what );
if ( node && node.length ) { node = node[ 0 ]; } else { node = null; }
}
}
return node;
},
 
getItem: function ( what, scope ) {
var node = IA.getRawItem( what, scope );
if ( !node ) { return null; }
return LAPI.DOM.getInnerText( node ).trim();
},
 
getIntItem: function ( what, scope ) {
var x = IA.getItem( what, scope );
if ( x !== null ) { x = parseInt ( x, 10 ); }
return x;
},
 
findNote: function ( text, id ) {
function find( (text, id, delim ) {
var start = delim.start.replace( '$1', id );
var start_match = text.indexOf( start );
if ( start_match < 0 ) { return null; }
var end = delim.end.replace( '$1', id );
var end_match = text.indexOf( end );
if ( end_match < start_match + start.length ) { return null; }
return { start: start_match, end: end_match + end.length };
}
}
 
var result = null;
for ( var i = 0; i < IA.note_delim.length && !result; i++ ) {
result = find( (text, id, IA.note_delim[ i ] );
}
}
return result;
},
 
setWikitext: function ( pagetext ) {
var self = IA;
if ( self.wiki_read ) { return; }
Array.forEach( self.viewers[ 0 ].annotations, function ( note ) {
if ( note.model.id >= 0 ) {
var span = self.findNote( pagetext, note.model.id );
if ( !span ) { return; }
// Now extract the wikitext
var code = pagetext.substring( span.start, span.end );
for ( var i = 0; i < self.note_delim.length; i++ ) {
var start = self.note_delim[ i ].content_start.replace( '$1', note.model.id );
var end = self.note_delim[ i ].content_end.replace( '$1', note.model.id );
var j = code.indexOf( start );
var k = code.indexOf( end );
if ( j >= 0 && k >= 0 && k >= j + start.length ) {
note.model.wiki = code.substring( j + start.length, k ).trim();
return;
}
}
}
}
}
}
} });
self.wiki_read = true;
},
 
setSummary: function ( summary, initial_text, note_text ) {
if ( initial_text.contains( '$1' ) ) {
var max = ( summary.maxlength || 200 ) - initial_text.length;
if ( note_text ) {
initial_text =
initial_text.replace( '$1', ': ' + note_text.replace( '\n', ' ' ).substring( 0, max ) );
} else {
else
initial_text = initial_text.replace( '$1', '' );
}
}
}
summary.value = initial_text;
summary.value = initial_text;
},
},
 
getScript: function ( url, bypass_local_cache, bypass_caches ) {
// Don't use LAPI here, it may not yet be available
if ( bypass_caches ) {
url += ( ( url.indexOf( '?' ) >= 0 ) ? '&' : '?' ) + 'dummyTimestamp=' + ( new Date() ).getTime();
}
}
// Avoid protocol-relative URIs (IE7 bug)
if ( url.length >= 2 && url.substring( 0, 2 ) === '//' ) { url = document.___location.protocol + url; }
if ( bypass_local_cache ) {
var s = document.createElement( 'script' );
s.setAttribute( 'src', url );
s.setAttribute( 'type', 'text/javascript' );
document.getElementsByTagName( 'head' )[ 0 ].appendChild( s );
return s;
} else {
return importScriptURImw.loader.load( (url );
}
}
},
 
canEdit: function () {
var self = IA;
if ( self.may_edit ) {
if ( !self.ajaxQueried ) {
self.haveAjax = ( LAPI.Ajax.getRequest() != null );
self.ajaxQueried = true;
self.may_edit = self.haveAjax;
if ( !self.may_edit && self.button_div ) {
LAPI.DOM.removeChildren( self.button_div );
self.button_div.appendChild( ImageAnnotator.UI.get( 'wpImageAnnotatorCannotEditMsg', false ) );
self.button_div.appendChild
self.viewers[ 0 ].msg.style.display = '';
(ImageAnnotator.UI.get('wpImageAnnotatorCannotEditMsg', false));
self.viewers[ 0 ].msg.style.display = ''cannotEdit();
}
self.viewers[0].cannotEdit();
}
}
}
}
return self.may_edit;
}
}
return self.may_edit;
}
 
}; // end IA
 
// Backwards compatibility
function getElementsByClassName ( scope, tag, className ) {
if ( window.jQuery ) {
return jQuery( scope ).find( ( ( !tag || tag === '*' ) ? '' : tag ) + '.' + className );
} else {
// For non-WMF wikis that might not have jQuery (yet), use the wikibits.js getElementsByClassName
return getElementsByClassName( scope, tag, className );
}
}
 
window.ImageAnnotator = {
install: function ( config ) { IA.install( config ); }
};
 
// Start it. Bypass caches; but allow for 4 hours client-side caching. Small file.
IA.getScript(
mw.config.get( 'wgScript' ) + '?title=MediaWiki:ImageAnnotatorConfig.js&action=raw&ctype=text/javascript',
true // No local caching!
+ '&dummy=' + Math.floor((new Date()).getTime() / (14400 * 1000)) // 4 hours
);
, true // No local caching!
);
 
})() ); // end local scope
 
} // end if (guard against double inclusions)
 
// </source>