MediaWiki:Gadget-ImageAnnotator.js: Difference between revisions
Content deleted Content added
Update to latest version from Commons |
Clean up |
||
Line 2:
/*
Image annotations. Draw rectangles onto image thumbnail displayed on image description
Line 8:
moves over the rectangles. If an image has annotations, display the rectangles. Add a
button to create new annotations.
Note: if an image that has annotations is overwritten by a new version, only display the
annotations if the size of the top image matches the stored size exactly. To recover
annotations, one will need to edit the image description page manually, adjusting image
sizes and rectangle coordinates, or re-enter annotations.
Author: [[User:Lupo]], June 2009 - March 2010
License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0)
Choose whichever license of these you like best :-)
Line 26:
// Global: wgAction, wgNamespaceNumber, wgUserLanguage, wgContentLanguage, stylepath (inline script)
// Global: wgNamespaceIds (inline script)
/*jshint eqnull:true, laxbreak:true, laxcomma:true */
if (typeof
importScript
importScript
importScript
importScript
(function () { // Local scope
Line 38 ⟶ 39:
var ImageAnnotator_config = null;
var ImageAnnotation = function () { this.initialize.apply
ImageAnnotation.compare = function (a, b)
{
var result = b.area
if (result !== 0) return result;
return a.model.id - b.model.id; // Just to make sure the order is complete
};
Line 54 ⟶ 55:
content : null, // Content of the tooltip
viewer : null, // Reference to the viewer this note belongs to
initialize : function (node, viewer, id)
{
Line 60 ⟶ 61:
var view_w = 0, view_h = 0, view_x = 0, view_y = 0;
this.viewer = viewer;
if (LAPI.DOM.hasClass
// Extract the info we need
var x = IA.getIntItem
var y = IA.getIntItem
var w = IA.getIntItem
var h = IA.getIntItem
var html = IA.getRawItem
if (x === null || y === null || w === null || h === null || html === null)
throw new Error ('Invalid note');
Line 79 ⟶ 80:
if (x + w > viewer.full_img.width) w = viewer.full_img.width - x;
if (y + h > viewer.full_img.height) h = viewer.full_img.height - y;
view_w = Math.floor
view_h = Math.floor
view_x = Math.floor
view_y = Math.floor
this.view =
LAPI.make
'div', null
, { position : 'absolute'
Line 90 ⟶ 91:
,lineHeight : '0px' // IE
,fontSize : '0px' // IE
,top :
,left :
,width :
,height :
}
);
Line 100 ⟶ 101:
{ id : id
,dimension: {x: x, y: y, w: w, h: h}
,wiki :
,html : html.cloneNode
};
} else {
is_new = true;
this.view = node;
this.model =
{ id : -1
,dimension: null
,wiki :
,html : null
};
Line 119 ⟶ 120:
// Enforce a minimum size of the view. Center the 6x6px square over the center of the old view.
// If we overlap the image boundary, adjustRectangleSize will take care of it later.
if (view_w < 6) {view_x = Math.floor
if (view_h < 6) {view_y = Math.floor
Object.merge
{ left:
,width:
, this.view.style
);
this.view.style.zIndex = 500; // Below tooltips
try {
Line 131 ⟶ 132:
} catch (ex) {
this.view.style.border = '1px solid ' + IA.outer_border;
}
this.view.appendChild
LAPI.make
'div', null
, { lineHeight : '0px' // IE
,fontSize : '0px' // IE
,width :
,height :
}
)
Line 148 ⟶ 149:
this.view.firstChild.style.border = '1px solid ' + IA.inner_border;
}
if (is_new) viewer.adjustRectangleSize
// IE somehow passes through event to the view even if covered by our cover, displaying the tooltips
// when drawing a new rectangle, which is confusing and produces a selection nightmare. Hence we just
// display raw rectangles without any tooltips attached while drawing. Yuck.
this.dummy = this.view.cloneNode
viewer.img_div.appendChild
if (!is_new) {
// New notes get their tooltip only once the editor has saved, otherwise IE may try to
// open them if the mouse moves onto the view even though there is the cover above them!
this.setTooltip
}
},
setTooltip : function ()
{
Line 169 ⟶ 170:
this.tooltip = new Tooltip
( this.view.firstChild
, this.display.bind
, { activate : (LAPI.DOM.is_ie ? Tooltip.NONE : Tooltip.HOVER)
,deactivate : (LAPI.DOM.is_ie ? Tooltip.ESCAPE : Tooltip.LEAVE)
Line 188 ⟶ 189:
// Hide all boxes if we're outside the image. Relies on hide checking the
// coordinates! (Otherwise, we'd always hide...)
if (evt) this.viewer.hide
}).bind
,onopen : (function (tooltip) {
if (this.view) {
Line 199 ⟶ 200:
}
this.viewer.tip = tooltip;
}).bind
}
, IA.tooltip_styles
Line 208 ⟶ 209:
{
if (!this.content) {
this.content = LAPI.make
var main = LAPI.make
this.content.appendChild
this.content.main = main;
if (this.model.html) main.appendChild
// Make sure that the popup encompasses all floats
this.content.appendChild
if (this.viewer.may_edit) {
this.content.button_section =
LAPI.make(
,null
,{ fontSize : 'smaller'
Line 225 ⟶ 226:
}
);
this.content.appendChild
this.content.button_section.appendChild(LAPI.DOM.makeLink(
, null
, LAPI.Evt.makeListener
)
);
if (ImageAnnotator_config.mayDelete
this.content.button_section.appendChild
this.content.button_section.appendChild
(LAPI.DOM.makeLink
( '#'
, ImageAnnotator.UI.get
, null
, LAPI.Evt.makeListener
)
);
Line 249:
return this.content;
},
edit : function (evt)
{
if (IA.canEdit
if (evt) return LAPI.Evt.kill
return false;
},
Line 259:
remove_event : function (evt)
{
if (IA.canEdit
return LAPI.Evt.kill
},
Line 266:
{
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 =
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
}
var self = this;
var spinnerId = 'image_annotation_delete_' + this.model.id;
LAPI.Ajax.injectSpinner
if (this.tooltip) this.tooltip.size_change
LAPI.Ajax.editPage
wgPageName
, function (doc, editForm, failureFunc, revision_id)
Line 308:
// breaks when we remove the note.
IA.setWikitext
var span = IA.findNote
if (!span) { // Hmmm? Doesn't seem to exist
LAPI.Ajax.removeSpinner
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
if (span.end < pagetext.length) char_after = pagetext.charCodeAt
if ( String.fromCharCode
&& String.fromCharCode
span.start = span.start - 1;
pagetext = pagetext.substring
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
|| '[[MediaWiki talk:Gadget-ImageAnnotator.js|Removing image note]]$1'
, (reason.length
);
} catch (ex) {
Line 340:
}
var edit_page = doc;
LAPI.Ajax.submitEdit
editForm
, function (request) {
if (edit_page.isFake && (typeof
edit_page.dispose
var revision_id = LAPI.WP.revisionFromHtml
if (!revision_id) {
failureFunc (request, new Error ('Revision ID not found. Please reload the page.'));
Line 351:
}
wgCurRevisionId = revision_id; // Bump revision id!!
LAPI.Ajax.removeSpinner
if (self.tooltip) self.tooltip.size_change
self.destroy
}
, function (request, ex) {
if (edit_page.isFake && (typeof
edit_page.dispose
failureFunc (request, ex);
}
Line 364:
, function (request, ex) {
// Failure. What now? TODO: Implement some kind of user feedback.
LAPI.Ajax.removeSpinner
if (self.tooltip) self.tooltip.size_change
}
);
Line 374:
destroy : function ()
{
if (this.view) LAPI.DOM.removeNode
if (this.dummy) LAPI.DOM.removeNode
if (this.tooltip) this.tooltip.hide_now
if (this.model && this.model.id > 0 && this.viewer) this.viewer.deregister
this.model = null;
this.view = null;
Line 394:
{
if (this.content && this.content.button_section) {
LAPI.DOM.removeNode
this.content.button_section = null;
if (this.tooltip) this.tooltip.size_change
}
}
Line 402:
}; // end ImageAnnotation
var ImageAnnotationEditor = function () {this.initialize.apply
ImageAnnotationEditor.prototype =
Line 412:
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
, { box : ImageAnnotator.UI.get
,preview : ImageAnnotator.UI.get
,save : ImageAnnotator.UI.get
,revert : ImageAnnotator.UI.get
,cancel : ImageAnnotator.UI.get
,nullsave : ImageAnnotator_config.mayDelete()
? ImageAnnotator.UI.get
: null
,post : ImageAnnotator.UI.get
}
, {
onsave : this.save.bind
,onpreview : this.onpreview.bind
,oncancel : this.cancel.bind
,ongettext : function (text) {
if (text == null) return
text = text.trim
.replace
;
// Guard against people trying to break notes on purpose
if (text.length
text = TextCleaner.sanitizeWikiText
return text;
}
}
);
this.box = LAPI.make
this.box.appendChild
// 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
temp.appendChild
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
// 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
, { activate : Tooltip.NONE // We'll always show it explicitly
,deactivate : Tooltip.ESCAPE
Line 476 ⟶ 483:
,open_delay : 0
,hide_delay : 0
,onclose : this.close_tooltip.bind
}
, IA.tooltip_styles
Line 482 ⟶ 489:
this.note = null;
this.visible = false;
LAPI.Evt.listenTo
function (evt) {
Array.forEach(IA.viewers, (function (viewer) {
if (viewer != this.viewer && viewer.visible) viewer.hide();
}
);
},
get_editor : function () {
return this.box;
},
Line 505 ⟶ 508:
this.viewer = this.note.viewer;
var cover = IA.get_cover
cover.style.cursor = 'auto';
IA.show_cover
if (note.tooltip) note.tooltip.hide_now
IA.is_editing = true;
Line 515 ⟶ 518:
// Existing note, and we don't have the wikitext yet: go get it
var self = this;
LAPI.Ajax.apiGet
'query'
, { prop : 'revisions'
Line 528 ⟶ 531:
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 == wgCurRevisionId && rev["*"] && rev["*"].length
IA.setWikitext
}
break;
Line 537 ⟶ 540:
}
// TODO: What upon a failure?
self.open_editor
}
, function (request) {
// TODO: What upon a failure?
self.open_editor
}
);
} else {
this.open_editor
}
},
Line 551 ⟶ 554:
open_editor : function (same_note, cover)
{
this.editor.hidePreview
if (!same_note || this.editor.textarea.readOnly)
// Different note, or save error last time
this.editor.setText
this.editor.enable
(LAPI.Edit.SAVE + LAPI.Edit.PREVIEW + LAPI.Edit.REVERT + LAPI.Edit.CANCEL);
Line 560 ⟶ 563:
this.editor.textarea.style.backgroundColor = 'white';
// Set the position relative to the note's view.
var view_pos = LAPI.Pos.position
var origin = LAPI.Pos.position
this.tooltip.options.fixed_offset.x =
view_pos.x - origin.x + this.tooltip.options.mouse_offset.x;
Line 570 ⟶ 573:
// 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
this.viewer.hide
this.viewer.toggle
// Now show the editor
this.tooltip.show_tip
var tpos = LAPI.Pos.position
var ppos = LAPI.Pos.position
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';
}
Line 593 ⟶ 596:
this.visible = false;
IA.is_editing = false;
this.tooltip.hide_now
if (evt && evt.type == 'keydown' && !this.saving) {
// ESC pressed on new note before a save attempt
this.cancel
}
IA.hide_cover
this.viewer.setDefaultMsg
this.viewer.setShowHideEvents
this.viewer.hide
this.viewer.show
// FIXME in Version 2.1: Unfortunately, we don't have a mouse position here, so sometimes we
// may show the note rectangles even though the mouse is now outside the image. (It was
Line 611 ⟶ 614:
save : function (editor)
{
var data = editor.getText
if (!data || !data.length
// Empty text if (this.note.remove
this.hide_editor
this.cancel
this.note = null;
} else {
this.hide_editor();
this.cancel
}
return;
} else if (data == this.note.model.wiki) {
this.
this.cancel();
return;
}
// Construct what to insert
var dim = Object.clone
if (!dim) {
dim = {
x : Math.round // Make sure everything is within bounds
if (dim.x + dim.w > this.viewer.full_img.width) {
Line 684 ⟶ 690:
+ '|style=2'
+ '}}\n'
+ data + (data.endsWith
+ '{{ImageNoteEnd|id=' + this.note.model.id + '}}';
// Now edit the page
var self = this;
this.editor.busy
this.editor.enable
this.saving = true;
LAPI.Ajax.editPage
wgPageName
, function (doc, editForm, failureFunc, revision_id)
Line 698 ⟶ 704:
if (revision_id && revision_id != wgCurRevisionId)
// Page was edited since the user loaded it.
throw new Error
// Modify the page
var textbox = editForm.wpTextbox1;
if (!textbox) throw new Error
var pagetext = textbox.value;
IA.setWikitext
var span = null;
if (self.note.content) // Otherwise it's a new note!
span = IA.findNote
if (span) { // Replace
pagetext =
pagetext.substring
+ self.to_insert
+ pagetext.substring
;
} else { // If not found, append
// Try to append right after existing notes
var lastNote = pagetext.lastIndexOf
if (lastNote >= 0) {
var endLastNote = pagetext.substring
if (endLastNote < 0) {
endLastNote = pagetext.substring
if (endLastNote < 0) lastNote = -1; else lastNote += endLastNote;
} else
Line 729 ⟶ 735:
if (lastNote >= 0) {
pagetext =
pagetext.substring
+ '\n' + self.to_insert
+ pagetext.substring
;
} else
pagetext = pagetext.trimRight
}
textbox.value = pagetext;
Line 742 ⟶ 748:
// If [[MediaWiki:Copyrightwarning]] is invalid XHTML, we may not have wpSummary!
if (self.note.content != null) {
IA.setSummary
summary
, ImageAnnotator.UI.get
|| '[[MediaWiki talk:Gadget-ImageAnnotator.js|Changing image note]]$1'
, data
);
} else {
IA.setSummary
summary
, ImageAnnotator.UI.get
|| '[[MediaWiki talk:Gadget-ImageAnnotator.js|Adding image note]]$1'
, data
Line 761 ⟶ 767:
}
var edit_page = doc;
LAPI.Ajax.submitEdit
editForm
, function (request) {
// After a successful submit.
if (edit_page.isFake && (typeof
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
if (!doc) return;
var html = LAPI.$ (id, doc);
if (!html) {
if (doc.isFake && (typeof
failureFunc
(request, new Error ('#Note not found after saving. Please reload the page.'));
return;
}
var revision_id = LAPI.WP.revisionFromHtml
if (!revision_id) {
if (doc.isFake && (typeof
failureFunc
(request, new Error ('#Version inconsistency after saving. Please reload the page.'));
Line 787 ⟶ 793:
}
wgCurRevisionId = revision_id; // Bump revision id!!
self.note.model.html = LAPI.DOM.importNode
if (doc.isFake && (typeof
self.note.model.dimension = dim; // record dimension
self.note.model.html.style.display =
self.note.model.wiki = data;
self.editor.busy
if (self.note.content) {
LAPI.DOM.removeChildren
self.note.content.main.appendChild
} else {
// New note.
self.note.display
if (self.viewer.annotations.length > 1) {
self.viewer.annotations.sort
var idxOfNote = Array.indexOf
if (idxOfNote+1 < self.viewer.annotations.length)
LAPI.DOM.insertNode
Line 809 ⟶ 815:
self.to_insert = null;
self.saving = false;
if (!self.note.tooltip) self.note.setTooltip
self.hide_editor
IA.is_editing = false;
self.editor.setText
}
, function (request, ex) {
if (edit_page.isFake && (typeof
edit_page.dispose
failureFunc (request, ex);
}
Line 823 ⟶ 829:
, function (request, ex)
{
self.editor.busy
self.saving = false;
// TODO: How and where to display error if user closed editor through ESC (or through
Line 829 ⟶ 835:
if (!self.visible) return;
// Change the tooltip to show the error.
self.editor.setText
// Error message. Use preview field for this.
var error_msg = ImageAnnotator.UI.get
var lk = getElementsByClassName
if (lk && lk.length
lk = lk[0].firstChild;
lk.href = wgServer + wgArticlePath.replace
}
if (ex) {
var ex_msg = LAPI.formatException
if (ex_msg) {
ex_msg.style.borderBottom = '1px solid red';
var tmp = LAPI.make
tmp.appendChild
tmp.appendChild
error_msg = tmp;
}
}
self.editor.setPreview
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
}
);
Line 859 ⟶ 865:
onpreview : function (editor)
{
if (this.tooltip) this.tooltip.size_change
},
Line 867 ⟶ 873:
if (!this.note.content) {
// No content: Cancel and remove this note!
this.note.destroy
this.note = null;
}
if (editor) this.hide_editor
},
close_tooltip : function (tooltip, evt)
{
this.hide_editor
this.cancel
}
};
var ImageNotesViewer = function () {this.initialize.apply
ImageNotesViewer.prototype =
Line 887 ⟶ 893:
initialize : function (descriptor, may_edit)
{
Object.merge
this.annotations = [];
this.max_id = 0;
Line 902 ⟶ 908:
if (!this.isThumbnail && !this.isOther) {
this.setup
} else {
// Normalize the namespace of the realName to 'File' to account for images possibly stored at
Line 910 ⟶ 916:
// the localized namespace names of other wikis, but the canonical namespace name 'File' works
// also locally.
this.realName = 'File:' + this.realName.substring
}
},
setup : function (onlyIcon)
{
Line 920 ⟶ 926:
if (this.isThumbnail || this.scope == document || this.may_edit || !IA.haveAjax) {
this.imgName = this.realName;
this.realName =
} else {
this.realName = ((name && name.length
this.imgName = this.realName;
}
var annotations = getElementsByClassName (this.scope, 'div', IA.annotation_class);
if (!this.may_edit && (!annotations || annotations.length === 0))
return; // Nothing to do
// 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
if (!this.isThumbnail && !this.isOther && !isEnabledImage) {
this.img_div =
LAPI.make
var floater =
LAPI.make
'div', null
, { cssFloat : (IA.is_rtl ? 'right' : 'left')
,styleFloat: (IA.is_rtl ? 'right' : 'left') // For IE...
,width :
,position : 'relative' // Fixes IE layout bugs...
}
);
floater.appendChild
this.img.parentNode.parentNode.insertBefore
this.img_div.appendChild
// And now a clear:left to make the rest appear below the image, as usual.
var breaker = LAPI.make
LAPI.DOM.insertAfter
// Remove spurious br tag.
if (breaker.nextSibling && breaker.nextSibling.nodeName.toLowerCase
LAPI.DOM.removeNode
} else if (this.isOther || isEnabledImage) {
this.img_div =
LAPI.make
this.img.parentNode.parentNode.insertBefore
this.img_div.appendChild
// Insert one more to have a file_div, so that we can align the message text correctly
this.file_div = LAPI.make
this.img_div.parentNode.insertBefore
this.file_div.appendChild
} else { // Thumbnail
this.img_div =
LAPI.make
'div'
, {className: 'thumbimage'}
, {position: 'relative', width:
);
this.img.parentNode.parentNode.insertBefore
this.img.style.border = 'none';
this.img_div.appendChild
}
if ( (this.isThumbnail || this.isOther) && !this.may_edit
Line 987 ⟶ 993:
// 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
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
&& this.icon.firstChild.nodeName.toLowerCase
)
{
// Make sure we use the right protocol:
var srcFixed = this.icon.firstChild.getAttribute
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
this.icon =
LAPI.DOM.makeImage
IA.indication_icon
, 14, 14
, ImageAnnotator.UI.get
);
}
Object.merge
{position: 'absolute', zIndex: 1000, top: '0px', cursor: 'pointer'}
, this.icon.style
);
this.icon.onclick = (function () {
if (IA.is_rtl)
this.icon.style.right = '0px';
else
this.icon.style.left = '0px';
this.img_div.appendChild
// And done. We just show the icon, no fancy event handling needed.
return;
}
// Set colors
var colors = IA.getRawItem
this.outer_border =
colors && IA.getItem
this.inner_border =
colors && IA.getItem
this.active_border =
colors && IA.getItem
if (annotations) {
for (var i = 0; i < annotations.length; i++) {
var id = annotations[i].id;
if (id && /^image_annotation_note_(\d+)$/.test
id = parseInt (id.substring
} else
id = null;
if (id) {
if (id > this.max_id) this.max_id = id;
var w = IA.getIntItem
var h = IA.getIntItem
if ( w == this.full_img.width && h == this.full_img.height
&& !Array.exists
)
{
try {
this.register
} catch (ex) {
// Swallow.
Line 1,053 ⟶ 1,059:
}
}
if (this.annotations.length > 1) this.annotations.sort
// Add the rectangles of existing notes to the DOM now that they are sorted.
Array.forEach
if (this.isThumbnail) {
this.main_div = getElementsByClassName (this.file_div, 'div', 'thumbcaption');
Line 1,064 ⟶ 1,070:
}
if (!this.main_div) {
this.main_div = LAPI.make
if (IA.is_rtl) {
this.main_div.style.direction = 'rtl';
Line 1,073 ⟶ 1,079:
}
if (!this.isThumbnail && !this.isOther && !isEnabledImage) {
LAPI.DOM.insertAfter
} else {
LAPI.DOM.insertAfter
}
}
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
if (IA.is_rtl) {
this.msg.style.direction = 'rtl';
Line 1,091 ⟶ 1,097:
}
if (this.isThumbnail) this.msg.style.fontSize = '90%';
this.main_div.appendChild
}
// Set overflow parents, if any
var simple = !!window.getComputedStyle;
var checks = (simple ? ['overflow', 'overflow-x', 'overflow-y']
Line 1,102 ⟶ 1,108:
var curStyle = null;
for (var up = this.img.parentNode.parentNode; up != document.body; up = up.parentNode) {
curStyle = (simple ? window.getComputedStyle
// "up.style" is actually incorrect, but a best-effort fallback.
var overflow =
Array.any(checks, function (t) {
var o
});
if (overflow) {
if (!this.overflowParents)
Line 1,122 ⟶ 1,128:
}
this.show_evt = LAPI.Evt.makeListener
if (this.overflowParents || LAPI.Browser.is_ie) {
// If we have overflowParents, also use a mousemove listener to show/hide the whole
Line 1,132 ⟶ 1,138:
// 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
} else
this.hide_evt = LAPI.Evt.makeListener
this.move_listening = false;
this.setShowHideEvents
this.visible = false;
this.setDefaultMsg
},
Line 1,145 ⟶ 1,151:
if (!this.may_edit) return;
this.may_edit = false;
Array.forEach
},
Line 1,152 ⟶ 1,158:
if (this.icon) return;
if (set) {
LAPI.Evt.attach
if (this.hide_evt) LAPI.Evt.attach
} else {
LAPI.Evt.remove(this.img, IA.mouse_in, this.show_evt);
if (this.hide_evt) {
LAPI.Evt.remove
} else if (this.move_listening)
this.removeMoveListener
}
},
Line 1,168 ⟶ 1,174:
this.move_listening = false;
if (this.move_evt) {
if (!LAPI.Browser.is_ie && typeof
document.captureEvents
LAPI.Evt.remove
}
},
Line 1,183 ⟶ 1,189:
var view_w = node.offsetWidth;
var view_h = node.offsetHeight;
if (view_x === 0) view_x = 1;
if (view_y === 0) view_y = 1;
if (view_x + view_w >= this.thumb.width) {
view_w = this.thumb.width - view_x - 1;
Line 1,197 ⟶ 1,203:
|| view_w != node.offsetWidth || view_h != node.offsetHeight)
{
node.style.top =
node.style.left =
node.style.width =
node.style.height =
node.firstChild.style.width =
node.firstChild.style.height =
}
},
Line 1,208 ⟶ 1,214:
toggle : function (dummies)
{
var i;
if (!this.annotations || this.annotations.length === 0 || this.icon) return;
if (dummies) {
for (
this.annotations[i].view.style.display = 'none';
if (this.visible && this.annotations[i].tooltip)
this.annotations[i].tooltip.hide_now
this.annotations[i].dummy.style.display = (this.visible ? 'none' :
if (!this.visible) this.adjustRectangleSize
}
} else {
for (
this.annotations[i].dummy.style.display = 'none';
this.annotations[i].view.style.display = (this.visible ? 'none' :
if (!this.visible) this.adjustRectangleSize
if (this.visible && this.annotations[i].tooltip)
this.annotations[i].tooltip.hide_now
}
}
this.visible = !this.visible;
},
show : function (evt)
{
if (this.visible || this.icon) return;
this.toggle
if (this.move_evt && !this.move_listening) {
LAPI.Evt.attach
this.move_listening = true;
if (!LAPI.Browser.is_ie && typeof
document.captureEvents
}
},
hide : function (evt)
{
Line 1,246 ⟶ 1,253:
if (!this.visible) {
// Huh?
if (this.move_listening) this.removeMoveListener
return true;
}
if (evt) {
var mouse_pos = LAPI.Pos.mousePosition
if (mouse_pos) {
if (this.tip) {
// Check whether we're within the visible note.
if (LAPI.Pos.isWithin
}
var is_within = true;
var img_pos = LAPI.Pos.position
var rect = {
x: img_pos.x, y: img_pos.y, b: (img_pos.y + this.img.offsetHeight) };
var if (this.overflowParents) {
// We're within some elements having overflow:hidden or overflow:auto or overflow:scroll set.
// Compute the actually visible region by intersecting the rectangle given by img_pos and
// this.img.offsetWidth, this.img.offsetTop with the rectangles of all overflow parents.
function intersect_rectangles (a, b) {
if (b.x > a.r || b.r < a.x || b.y > a.b || b.b < a.y)
return { x:0, y:0, r:0, b:0 };
return {
x: Math.max(a.x,
y: Math.max(a.y, b.y),
r: Math.min(a.r, b.r),
b: Math.min(a.b, b.b)
};
}
for (
img_pos = LAPI.Pos.position
img_pos.r = img_pos.x + this.overflowParents[i].clientWidth;
img_pos.b = img_pos.y + this.overflowParents[i].clientHeight;
Line 1,282 ⟶ 1,296:
}
}
is_within = !( rect.x >= rect.r || rect.y >= rect.b // Empty rectangle
|| rect.x >= mouse_pos.x || rect.r <= mouse_pos.x
|| rect.y >= mouse_pos.y || rect.b <= mouse_pos.y
if (is_within) {
if (LAPI.Browser.is_ie && evt.type === 'mousemove') {
var display;
// Loop in reverse order to properly display top rectangle's note!
for (
display = this.annotations[i].view.style.display;
if ( display !== 'none' && display != null
&& LAPI.Pos.isWithin
)
{
if (!this.annotations[i].tooltip.visible) this.annotations[i].tooltip.show
return true;
}
}
if (this.tip) this.tip.hide_now
}
return true;
Line 1,307 ⟶ 1,322:
}
// Not within the image, or forced hiding (no event)
if (this.move_listening) this.removeMoveListener
this.toggle
return true;
},
check_hide : function (evt)
{
if (this.icon) return true;
if (this.visible)
this.hide
return true;
},
Line 1,329 ⟶ 1,344:
}
},
deregister : function (note)
{
Array.remove
if (note.model.id == this.max_id) this.max_id--;
if (this.annotations.length === 0) this.setDefaultMsg
},
setDefaultMsg : function ()
{
if (this.annotations && this.annotations.length
LAPI.DOM.removeChildren
this.msg.appendChild
(ImageAnnotator.UI.get
if (this.realName && typeof
var otherPageMsg = ImageAnnotator.UI.get
if (otherPageMsg) {
var lk = otherPageMsg.getElementsByTagName
if (lk && lk.length
lk = lk[0];
lk.parentNode.replaceChild
LAPI.DOM.makeLink
wgArticlePath.replace
, this.realName
, this.realName
Line 1,357 ⟶ 1,372:
, lk
);
this.msg.appendChild
}
}
}
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 =
Line 1,375 ⟶ 1,390:
// annotations in the page source, and adds an "Annotate this image" button plus the support
// for drawing rectangles onto the image if there is only one image and editing is allowed.
haveAjax : false,
button_div : null,
add_button : null,
cover : null,
border : null,
definer : null,
mouse_in : (
mouse_out : (
annotation_class : 'image_annotation',
Line 1,417 ⟶ 1,432:
editor : null,
wiki_read : false,
is_rtl : false,
Line 1,441 ⟶ 1,456:
install : function (config)
{
if (typeof
if (!config || ImageAnnotator_config != null) return;
// Double check.
if (!config.viewingEnabled
var self = IA;
Line 1,453 ⟶ 1,468:
// doing too much work.
if ( window.XMLHttpRequest
&& typeof
&& typeof
&& typeof
)
{
self.haveAjax = (LAPI.Ajax.getRequest
self.ajaxQueried = true;
} else {
Line 1,466 ⟶ 1,481:
// We'll include self.haveAjax later on once more, to catch the !ajaxQueried case.
self.may_edit = wgNamespaceNumber >= 0 && wgArticleId > 0 && self.haveAjax && config.editingEnabled
function namespaceCheck (list)
{
if (!list || Object.prototype.toString.call
for (var i = 0; i < list.length; i++) {
if (wgNamespaceIds
&& typeof
&& (list[i] === '*'
|| wgNamespaceIds[list[i].toLowerCase().replace(/ /g, '_')] === wgNamespaceNumber
)
)
Line 1,483 ⟶ 1,498:
}
self.rules = { inline: {}, thumbs: {}, shared : {} };
// Now set the default rules. Undefined means default setting (true for show, false for icon),
// but overrideable by per-image rules. If set, it's not overrideable by per-image rules.
//
if ( !self.haveAjax
|| !config.generalImagesEnabled
|| namespaceCheck (window.ImageAnnotator_no_images || null)
)
Line 1,498 ⟶ 1,513:
} else {
if ( !self.haveAjax
|| !config.thumbsEnabled
|| namespaceCheck
)
{
Line 1,506 ⟶ 1,521:
if (wgNamespaceNumber == 6)
self.rules.shared.show = true;
else if ( !config.sharedImagesEnabled
|| namespaceCheck
)
{
self.rules.shared.show = false;
}
if (namespaceCheck
self.rules.inline.icon = true;
if (namespaceCheck
self.rules.thumbs.icon = true;
}
// User rule for displaying captions on images in articles
self.hideCaptions = namespaceCheck
var do_images = typeof self.rules.inline.show === 'undefined' || self.rules.inline.show;
if (do_images) {
// Per-article switching off of note display on inline images and thumbnails
var rules = document.getElementById
if (rules) {
if (rules.className.indexOf
self.rules.inline.show = false;
self.rules.thumbs.show = false;
self.rules.shared.show = false;
}
if ( typeof
&& rules.className.indexOf
)
{
self.rules.inline.show = true;
}
if (rules.className.indexOf
self.rules.thumbs.show = false;
}
if ( typeof
&& rules.className.indexOf
)
{
self.rules.thumbs.show = true;
}
if (rules.className.indexOf
self.rules.inline.icon = true;
}
if (rules.className.indexOf
self.rules.thumbs.icon = true;
}
if (rules.className.indexOf
{
self.rules.shared.show = false;
Line 1,562 ⟶ 1,577:
// Make sure the shared value is set
self.rules.shared.show =
typeof
do_images = typeof
var do_thumbs = typeof
if (do_images) {
var bodyContent = document.getElementById
|| document.getElementById
|| document.getElementById
;
if (bodyContent) {
var all_imgs = bodyContent.getElementsByTagName
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
up = up.parentNode;
if ((' ' + up.className + ' ').indexOf
if ((' ' + up.className + ' ').indexOf
if (do_thumbs) self.thumbs[self.thumbs.length] = up;
continue;
Line 1,586 ⟶ 1,601:
up = up.parentNode;
if (!up) continue;
if ((' ' + up.className + ' ').indexOf
if ((' ' + up.className + ' ').indexOf
self.imgs_with_notes[self.imgs_with_notes.length] = up;
continue;
Line 1,594 ⟶ 1,609:
if (!up) continue;
// Other images not in galleries
if ((' ' + up.className + ' ').indexOf
if ((' ' + up.className + ' ').indexOf
if ((' ' + up.className + ' ').indexOf
self.imgs_with_notes[self.imgs_with_notes.length] = up;
continue;
}
up = up.parentNode;
if (!up) continue;
if ((' ' + up.className + ' ').indexOf
if ((' ' + up.className + ' ').indexOf
if ((' ' + up.className + ' ').indexOf
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
up = up.parentNode;
if (up) is_other = (' ' + up.className + ' ').indexOf
}
if (is_other) self.other_images[self.other_images.length] = all_imgs[i];
Line 1,623 ⟶ 1,638:
}
if ( wgNamespaceNumber == 6
|| (self.imgs_with_notes.length
|| (self.thumbs.length
|| (self.other_images.length
)
{
Line 1,634 ⟶ 1,649:
self.active_border = config.active_border;
self.new_border = config.new_border;
self.wait_for_required_libraries
}
},
Line 1,640 ⟶ 1,655:
wait_for_required_libraries : function ()
{
if (typeof
if (IA.install_attempts++ < IA.max_install_attempts) {
}
return;
Line 1,648 ⟶ 1,663:
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();
},
Line 1,656 ⟶ 1,671:
var self = IA;
self.imgs = [];
// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
self.is_rtl =
LAPI.DOM.hasClass
|| ( LAPI.DOM.currentStyle // Paranoia: added recently, not everyone might have it
&& LAPI.DOM.currentStyle
)
;
var stylepath =
// Use this to temporarily display an image off-screen to get its dimensions
var testImgDiv =
LAPI.make
{ display: 'none', position: 'absolute', width: '300px'
, overflow: 'hidden', overflowX: 'hidden', left: '-10000px'
}
);
document.body.insertBefore
function img_check (img, is_other)
{
var srcW = parseInt (img.getAttribute
var srcH = parseInt (img.getAttribute
// Don't do anything on extremely small previews. We need some minimum extent to be able to place
// rectangles after all...
Line 1,691 ⟶ 1,706:
if ((!w || !h) && img.style.display != 'none') {
var copied = img.cloneNode(true);
copied.style.display =
testImgDiv.appendChild
testImgDiv.style.display =
w = copied.clientWidth;
h = copied.clientHeight;
testImgDiv.style.display = 'none';
LAPI.DOM.removeNode
}
// Quit if the image wasn't loaded properly for some reason:
if (w != srcW || h != srcH) return null;
// Exclude system images
if (img.src.contains
// Only if within a link
if (img.parentNode.nodeName.toLowerCase
if (is_other) {
// Only if the img-within-link construction is within some element that may contain a div!
Line 1,712 ⟶ 1,727:
// in a div, but for now we assume that all browsers can handle a div within a paragraph or
// a span in a meaningful way, even if that is not really allowed.
} else if (!/^(object|applet|map|fieldset|noscript|iframe|body|div|li|dd|blockquote|center|ins|del|button|th|td|form)$/i.test
return null;
}
Line 1,718 ⟶ 1,733:
var up = img.parentNode.parentNode;
while (up != document.body) {
if (LAPI.DOM.hasClass
up = up.parentNode;
}
return {width: w, height: h};
}
function setup_one (scope) {
var file_div = scope;
Line 1,729 ⟶ 1,744:
scope != document
&& scope.nodeName.toLowerCase() == 'div'
&& LAPI.DOM.hasClass
;
var is_other = scope.nodeName.toLowerCase() == 'img';
if (is_other && self.imgs.length
if (scope == document) {
file_div = LAPI.$
} else if (!is_thumb && !is_other) {
file_div = getElementsByClassName
if (!file_div || file_div.length != 1) return null;
file_div = file_div[0];
Line 1,743 ⟶ 1,758:
var img = null;
if (scope == document) {
img = LAPI.WP.getPreviewImage
// TIFFs may be multi-paged: allow only for single-page TIFFs
if ( document.pageselector ) img = null;
Line 1,749 ⟶ 1,764:
img = scope;
} else {
img = file_div.getElementsByTagName
if (!img || img.length === 0) return null;
img = img[0];
}
Line 1,759 ⟶ 1,774:
if ( scope != document
&& !self.rules.shared.show
&& ImageAnnotator_config.imageIsFromSharedRepository
)
return null;
Line 1,766 ⟶ 1,781:
name = wgPageName;
} else {
name = LAPI.WP.pageFromLink
if (!name) return null;
name = name.replace
if (is_thumb || is_other) {
var img_src = decodeURIComponent
// img_src should have a component "/name" in there somewhere
var colon = name.indexOf
if (colon <= 0) return null;
var img_name = name.substring
if (img_src.search(new RegExp('/' + img_name.escapeRE() + '(/.*)?$')) < 0)
return null;
Line 1,781 ⟶ 1,796:
}
}
if (name.search
// Finally check for wpImageAnnotatorControl
var icon_only = false;
Line 1,789 ⟶ 1,804:
// Three levels is sufficient: thumbinner-thumb-control, or floatnone-center-control, or direct
for (var i = 0; ++i <= 3 && up; up = up.parentNode) {
if (LAPI.DOM.hasClass
if (LAPI.DOM.hasClass
if (LAPI.DOM.hasClass
if (LAPI.DOM.hasClass
break;
}
}
}
return { scope : scope
,file_div : file_div
Line 1,811 ⟶ 1,826:
function setup_images (list)
{
Array.forEach
function (elem) {
var desc = setup_one (elem);
Line 1,820 ⟶ 1,835:
if (wgNamespaceNumber == 6) {
setup_images ([document]);
self.may_edit = self.may_edit && (self.imgs.length == 1);
setup_images (self.imgs_with_notes);
Line 1,828 ⟶ 1,843:
}
self.may_edit = self.may_edit &&
if (self.haveAjax) {
setup_images (self.thumbs);
Line 1,836 ⟶ 1,851:
// Remove the test div
LAPI.DOM.removeNode
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.
Line 1,845 ⟶ 1,860:
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;
}
Line 1,854 ⟶ 1,869:
// 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].length] = idx;
}
names[names.length] =
}
});
var to_do = names.length;
Line 1,877 ⟶ 1,889:
done += length;
if (done >= names.length) {
if (typeof
self.setup_step_two
}
}
function make_calls (execute_call, url_limit)
{
Line 1,887 ⟶ 1,899:
{
var done = 0;
var text =
for (var i = from; i < from + length; i++) {
var new_text = names[i];
if (url_limit) {
new_text = encodeURIComponent
if (text.length
}
text += (text.length
done++;
}
Line 1,902 ⟶ 1,914:
var start = 0, chunk = 0, params;
while (to_do > 0) {
params = build_titles (start, Math.min
execute_call (params.n, params.text);
to_do -= params.n;
start += params.n;
}
}
Line 1,914 ⟶ 1,926:
if (json && json.query && json.query.pages) {
function get_size (info) {
if (!info.imageinfo || info.imageinfo.length === 0) return;
var title = info.title.replace
var indices = cache[title];
if (!indices) return;
Array.forEach
indices
, function (i) {
self.imgs[i].full_img = { width : info.imageinfo[0].width
,height: info.imageinfo[0].height};
self.imgs[i].has_page = (typeof
self.imgs[i].isLocal = !info.imagerepository || info.imagerepository == 'local';
if (i != 0 || !self.may_edit || !info.protection || wgNamespaceNumber != 6) return;
// Care about the protection settings
var protection = Array.any(info.protection, function (e) {
self.may_edit =
!protection
|| (wgUserGroups && wgUserGroups.join
;
}
Line 1,960 ⟶ 1,973:
ImageAnnotator.info_callbacks[idx].done = true;
if (ImageAnnotator.info_callbacks[idx].script) {
LAPI.DOM.removeNode
ImageAnnotator.info_callbacks[idx].script = null;
}
Line 1,968 ⟶ 1,981:
};
ImageAnnotator.info_callbacks[idx].script =
IA.getScript
template.replace
.replace
, true // No local caching!
);
Line 1,980 ⟶ 1,993:
&& ImageAnnotator.info_callbacks[idx].done && ImageAnnotator.info_callbacks[idx].script)
{
LAPI.DOM.removeNode
ImageAnnotator.info_callbacks[idx].script = null;
}
}
, (LAPI.Browser.is_ie ? 1950 : 4000) - template.length // Some slack for caching parameters
Line 1,989 ⟶ 2,002:
make_calls (
function (length, titles) {
LAPI.Ajax.apiGet
'query'
, { titles : titles
Line 2,012 ⟶ 2,025:
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
readyEvent = null;
};
ImageAnnotator.UI.addReadyEventHandler = function (f) {
if (ImageAnnotator.UI.ready) {
f (); // Already fired: call directly
Line 2,039 ⟶ 2,047:
readyEvent[readyEvent.length] = f;
}
};
ImageAnnotator.UI.setup = function ()
Line 2,045 ⟶ 2,053:
if (ImageAnnotator.UI.repo) return;
var self = ImageAnnotator.UI;
var node = LAPI.make
document.body.appendChild
if (typeof
self.basic = true;
self.repo = {};
Line 2,053 ⟶ 2,061:
node.innerHTML = self.defaults[item];
self.repo[item] = node.firstChild;
LAPI.DOM.removeChildren
}
} else {
self.basic = false;
self.repo = UIElements.emptyRepository
for (var item in self.defaults) {
node.innerHTML = self.defaults[item];
UIElements.setEntry
LAPI.DOM.removeChildren
}
UIElements.load
}
LAPI.DOM.removeNode
};
Line 2,071 ⟶ 2,079:
{
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
add_plea = !result;
if (!result) result = UIElements.getEntry
}
self.needs_plea = add_plea;
if (!result) return null; // Hmmm... what happened here? We normally have defaults...
if (basic) return LAPI.DOM.getInnerText
result = result.cloneNode
if (wgServer.contains
// Add a translation plea.
if (result.nodeName.toLowerCase
result.appendChild
} else {
var span = LAPI.make
span.appendChild
span.appendChild
result = span;
}
Line 2,102 ⟶ 2,110:
{
var self = ImageAnnotator.UI;
var translate = self.get
var span = LAPI.make
span.appendChild
span.appendChild
LAPI.DOM.makeLink
wgServer + wgScript + '?title=MediaWiki_talk:ImageAnnotatorTexts'
+ '&action=edit§ion=new&withJS=MediaWiki:ImageAnnotatorTranslator.js'
+ '&language=' + wgUserLanguage
, translate
, (typeof
)
);
span.appendChild
return span;
};
ImageAnnotator.UI.init = function (html_text_or_json)
{
var text;
if (typeof
text = html_text_or_json;
else if ( typeof
&& typeof
&& typeof
&& typeof
)
text = html_text_or_json.parse.text['*'];
Line 2,133 ⟶ 2,141:
if (!text) {
ImageAnnotator.UI.fireReadyEvent
return;
}
var node = LAPI.make
document.body.appendChild
try {
node.innerHTML = text;
} catch (ex) {
LAPI.DOM.removeNode
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'
+ (wgUserLanguage != wgContentLanguage ? '|lang=' + wgUserLanguage :
+ '|live=1}}';
Line 2,158 ⟶ 2,166:
var url =
wgServer + wgScriptPath + '/api.php?action=parse&pst&text='
+ encodeURIComponent
+ '&callback=ImageAnnotator.UI.init&maxage=14400&smaxage=14400'
;
Line 2,164 ⟶ 2,172:
// that on IE. (On FF, we could use an onerror handler on the script tag, but on FF, we use Ajax
// anyway.)
IA.getScript
}
function get_ui ()
{
IA.haveAjax = (LAPI.Ajax.getRequest
IA.ajaxQueried = true;
Line 2,181 ⟶ 2,189:
}
LAPI.Ajax.parseWikitext
ui_page
, ImageAnnotator.UI.init
Line 2,208 ⟶ 2,216:
// 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) {
elem.thumb.width > 0 && elem.thumb.height > 0
&& elem.full_img.width > 0 && elem.full_img.height > 0
;
if (self.may_edit && idx === 0 &&
return result;
if (self.imgs.length ===
ImageAnnotator.UI.addReadyEventHandler(IA.complete_setup);
},
Line 2,234 ⟶ 2,238:
// UI object is fired.
var self = IA;
// Check edit permissions
if (self.may_edit && typeof
self.may_edit =
( (wgRestrictionEdit.length === 0 || wgUserGroups && wgUserGroups.join
|| ( wgRestrictionEdit.length === 1 && wgRestrictionEdit[0] === 'autoconfirmed'
&& wgUserGroups && wgUserGroups.join
)
);
Line 2,248 ⟶ 2,252:
// 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 && wgNamespaceNumber != 6) {
Line 2,254 ⟶ 2,258:
var img_page_name =
getElementsByClassName (self.imgs[0].scope, '*', 'wpImageAnnotatorPageName');
if (img_page_name && img_page_name.length
img_page_name = LAPI.DOM.getInnerText
else
img_page_name =
self.may_edit = (img_page_name.replace
}
if (self.may_edit && self.ajaxQueried) self.may_edit = self.haveAjax;
Line 2,266 ⟶ 2,270:
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) {
Line 2,273 ⟶ 2,277:
// Respect user override for zoom, if any
self.zoom_threshold = ImageAnnotator_config.zoom_threshold;
if ( typeof
&& !isNaN (window.ImageAnnotator_zoom_threshold)
&& window.ImageAnnotator_zoom_threshold >= 0.0
Line 2,285 ⟶ 2,289:
// but only if we *can* zoom at least twice
if ( self.viewers[0].full_img.width > 300
&& Math.min
)
{
if ( self.viewers[0].thumb.width < 400
|| self.viewers[0].thumb.width / self.viewers[0].thumb.height > 2.0
|| self.viewers[0].thumb.height / self.viewers[0].thumb.width > 2.0
)
{
Line 2,298 ⟶ 2,302:
self.editor = new ImageAnnotationEditor ();
function track (evt) {
evt = evt || window.event;
if (self.is_adding) self.update_zoom
if (!self.is_tracking) return LAPI.Evt.kill
var mouse_pos = LAPI.Pos.mousePosition
if (!LAPI.Pos.isWithin
var origin = LAPI.Pos.position
// 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 =
self.definer.style.left =
} else {
self.definer.style.width =
self.definer.style.left =
}
if (mouse_pos.y >= self.base_y) {
self.definer.style.height =
self.definer.style.top =
} else {
self.definer.style.height =
self.definer.style.top =
}
return LAPI.Evt.kill
}
function pause (evt) {
LAPI.Evt.remove(document, 'mousemove', track, true);
if (!LAPI.
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
LAPI.Evt.attach
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
if (!LAPI.Pos.isWithin
if (self.is_tracking) {
self.is_tracking = false;
Line 2,361 ⟶ 2,362:
if (LAPI.Browser.is_ie) {
//Trust Microsoft to get everything wrong!
LAPI.Evt.remove
} else {
LAPI.Evt.remove
}
LAPI.Evt.remove
LAPI.Evt.remove
self.cover.style.cursor = 'auto';
LAPI.DOM.removeNode
LAPI.Evt.remove
LAPI.Evt.remove
self.hide_zoom
self.viewers[0].hide
if (!self.definer || self.definer.offsetWidth <= 0 || self.definer.offsetHeight <= 0) {
// Nothing: just remove the definer:
if (self.definer) LAPI.DOM.removeNode
// Re-attach event handlers
self.viewers[0].setShowHideEvents
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
self.editor.editNote
}
self.definer = null;
}
if (evt) return LAPI.Evt.kill
return false;
}
function start_tracking (evt)
{
Line 2,400 ⟶ 2,401:
evt = evt || window.event;
// Set the position, size 1
var mouse_pos = LAPI.Pos.mousePosition
var origin = LAPI.Pos.position
self.base_x = mouse_pos.x - origin.x;
self.base_y = mouse_pos.y - origin.y;
Object.merge
{ left :
,top :
,width : '0px'
,height : '0px'
,display:
}
, self.definer.style
);
// Set mouse handlers
LAPI.Evt.remove
if (LAPI.Browser.is_ie) {
LAPI.Evt.attach
} else {
LAPI.Evt.attach
}
resume ();
LAPI.Evt.attach
LAPI.Evt.attach
}
if (evt) return LAPI.Evt.kill
return false;
}
function add_new (evt) {
if (!self.canEdit()) return;
self.editor.hide_editor
Tooltips.close
var cover = self.get_cover
cover.style.cursor = 'crosshair';
self.definer =
LAPI.make
'div', null
,{ border : '1px solid ' + IA.new_border
Line 2,452:
}
);
self.viewers[0].img_div.appendChild
// 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.show_cover
self.is_tracking = false;
self.is_adding = true;
LAPI.Evt.attach
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
self.viewers[0].hide
self.viewers[0].toggle
self.update_zoom_evt = LAPI.Evt.makeListener
self.hide_zoom_evt = LAPI.Evt.makeListener
self.show_zoom
LAPI.Evt.attach
LAPI.Evt.attach
LAPI.DOM.removeChildren
self.viewers[0].msg.appendChild
(ImageAnnotator.UI.get
self.viewers[0].msg.style.display =
}
self.button_div = LAPI.make
self.viewers[0].main_div.appendChild
self.add_button =
LAPI.DOM.makeButton
'ImageAnnotationAddButton'
, ImageAnnotator.UI.get
, add_new
);
var add_plea = ImageAnnotator.UI.needs_plea;
self.button_div.appendChild
self.help_link = self.createHelpLink
if (self.help_link) {
self.button_div.appendChild
self.button_div.appendChild
}
if (add_plea && wgServer.contains
self.button_div.appendChild
} // end if may_edit
Line 2,501:
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.
cache[viewer.realName][cache[viewer.realName].length] = idx;
}
if
}
}
});
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)
{
Line 2,531 ⟶ 2,528:
function compose (list, from, length, url_limit)
{
var text =
var done = 0;
for (var i = from; i < from + length; i++) {
Line 2,541 ⟶ 2,538:
;
if (url_limit) {
new_text = encodeURIComponent
if (text.length
}
text = text + new_text;
Line 2,562 ⟶ 2,559:
var start = 0, chunk = 0, to_do = list.length;
while (to_do > 0) {
chunk = composer (list, start, Math.min
to_do -= chunk;
start += chunk;
Line 2,579 ⟶ 2,576:
// 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,
var i = html.indexOf
// Now collect pages
while (idx < l && i >= idx) {
var j = html.indexOf
if (j < i+blockStart.length) break;
result += html.substring
idx = j+inlineNameEnd.length;
// Now collect all image image notes for that page
var note_begin = 0, next_block = html.indexOf
// Do we have image note control or color templates?
j = idx;
for (;;) {
noteControlRE.lastIndex = j;
if (!m || next_block >= idx && m.index > next_block) break;
result += m[0];
Line 2,602 ⟶ 2,600:
// Collect notes
for (;;) {
note_begin = html.indexOf
// Check whether next wrapper comes first
if (note_begin < idx || (next_block >= idx && note_begin > next_block)) break;
Line 2,609 ⟶ 2,607:
while (level > 0 && k < l) {
divRE.lastIndex = k;
if (!m || m.length < 2) {
k = l; // Nothing found at all?
Line 2,622 ⟶ 2,620:
}
} // end loop for nested divs
result += html.substring
while (level-- > 0) result += '</div>'; // Missing ends.
idx = k;
Line 2,634 ⟶ 2,632:
function setup_thumb_viewers (html_text)
{
var node = LAPI.make
document.body.appendChild
try {
node.innerHTML = strip_noise (html_text);
Line 2,642 ⟶ 2,640:
var notes = getElementsByClassName (pages[i], 'div', self.annotation_class);
if (!notes || notes.length == 0) continue;
var page = self.getItem
if (!page) continue;
page = page.replace
var viewers = cache[page] || cache['File:' + page.substring
if (!viewers || viewers.length == 0) continue;
// Update rules.
var rules = getElementsByClassName (pages[i], 'div', 'wpImageAnnotatorInlinedRules');
var local_rules =
{ inline: Object.clone
,thumbs: Object.clone
};
if (rules && rules.length
rules = rules[0];
if ( typeof
&& LAPI.DOM.hasClass
)
{
local_rules.inline.show = false;
}
if ( typeof
&& LAPI.DOM.hasClass
)
{
local_rules.inline.icon = true;
}
if ( typeof
&& LAPI.DOM.hasClass
)
{
local_rules.thumbs.show = false;
}
if ( typeof
&& LAPI.DOM.hasClass
)
{
Line 2,682 ⟶ 2,680:
// Make sure all are set
local_rules.inline.show =
typeof
local_rules.thumbs.show =
typeof
local_rules.inline.icon =
typeof
local_rules.thumbs.icon =
typeof
if (!local_rules.inline.show) continue;
// Now use pages[i] as a scope shared by all the viewers using it. Since we clone note
// contents for note display, this works. Otherwise, we'd have to copy the notes into
// each viewer's scope.
document.body.appendChild
// Set viewers' scopes and finish their setup.
Array.forEach(viewers, function (v) {
if
self.viewers[v].setup(
|| self.viewers[v].
}
});
}
} catch (ex) {}
LAPI.DOM.removeNode
}
Line 2,729 ⟶ 2,724:
ImageAnnotator.script_callbacks[idx].done = true;
if (ImageAnnotator.script_callbacks[idx].script) {
LAPI.DOM.removeNode
ImageAnnotator.script_callbacks[idx].script = null;
}
Line 2,736 ⟶ 2,731:
};
ImageAnnotator.script_callbacks[idx].script =
IA.getScript
template.replace
.replace
, true // No local caching!
);
Line 2,744 ⟶ 2,739:
&& ImageAnnotator.script_callbacks[idx].done && ImageAnnotator.script_callbacks[idx].script)
{
LAPI.DOM.removeNode
ImageAnnotator.script_callbacks[idx].script = null;
}
Line 2,751 ⟶ 2,746:
);
}
if ((!window.XMLHttpRequest && !!window.ActiveXObject) || !self.haveAjax) {
make_script_calls
} else {
make_calls
get_local
, function (text) {
LAPI.Ajax.parseWikitext
text
, function (html_text) {if (html_text) setup_thumb_viewers
, function () {}
, false
Line 2,775 ⟶ 2,770:
// 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 ()
{
Line 2,784 ⟶ 2,779:
&& self.viewers[0].factors.dy < self.zoom_threshold
)
|| Math.max
)
{
Line 2,792 ⟶ 2,787:
if (!self.zoom) {
self.zoom =
LAPI.make
'div'
, {id : 'image_annotator_zoom'}
Line 2,807 ⟶ 2,802:
}
);
var src = self.viewers[0].img.getAttribute
// Adjust zoom_factor
if (self.zoom_factor > self.viewers[0].factors.dx || self.zoom_factor > self.viewers[0].factors.dy)
self.zoom_factor = Math.min
self.zoom.appendChild
// Calculate zoom size and source link
var zoom_width = Math.floor
var zoom_height = Math.floor
var zoom_src = null;
// For SVGs, always use a scaled PNG for the zoom.
if (zoom_width > 0.9 * self.viewers[0].full_img.width && src.search
// If the thumb we'd be loading was within about 80% of the full image size, we may just as
// well get the full image instead of a scaled version.
self.zoom_factor = Math.min
zoom_width = self.viewers[0].full_img.width;
zoom_height = self.viewers[0].full_img.height;
Line 2,826 ⟶ 2,821:
// Construct the initial zoomed image. We need to clone; if we create a completely
// new DOM node ourselves, it may not work on IE6...
var zoomed = self.viewers[0].img.cloneNode
zoomed.width =
zoomed.height =
Object.merge
self.zoom.firstChild.appendChild
// Crosshair
self.zoom.firstChild.appendChild
LAPI.make
'div', null
, { width : '1px'
Line 2,844 ⟶ 2,839:
)
);
self.zoom.firstChild.appendChild
LAPI.make
'div', null
, { width : '200px'
Line 2,856 ⟶ 2,851:
)
);
document.body.appendChild
LAPI.DOM.loadImage
self.viewers[0].imgName
, src
, zoom_width
, zoom_height
, ImageAnnotator_config.thumbnailsGeneratedAutomatically
, function (img) {
// Replace the image in self.zoom by self.zoom_loader, making sure we keep the offsets
Line 2,868 ⟶ 2,863:
img.style.top = self.zoom.firstChild.firstChild.style.top;
img.style.left = self.zoom.firstChild.firstChild.style.left;
img.style.display =
self.zoom.firstChild.replaceChild
}
);
Line 2,881 ⟶ 2,876:
var self = IA;
if (!self.zoom) return;
var mouse_pos = LAPI.Pos.mousePosition
var origin = LAPI.Pos.position
if (!LAPI.Pos.isWithin
IA.hide_zoom
return;
}
Line 2,892 ⟶ 2,887:
var top = - dy * self.zoom_factor + 100;
var left = - dx * self.zoom_factor + 100;
self.zoom.firstChild.firstChild.style.top =
self.zoom.firstChild.firstChild.style.left =
self.zoom.style.top = mouse_pos.y + 10 + 'px'; // Right below the mouse pointer
// Horizontally keep it in view.
Line 2,899 ⟶ 2,894:
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) {
Line 2,905 ⟶ 2,900:
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;
Line 2,917 ⟶ 2,912:
if (!IA.zoom) return;
if (evt) {
var mouse_pos = LAPI.Pos.mousePosition
if (LAPI.Pos.isWithin
}
IA.zoom.style.display = 'none';
Line 2,925 ⟶ 2,920:
createHelpLink : function ()
{
var msg = ImageAnnotator.UI.get
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
imgs[i].src = srcFixed;
}
}
if ( msg.childNodes.length == 1
&& msg.firstChild.nodeName.toLowerCase
&& !LAPI.DOM.hasClass
) {
msg.firstChild.id = 'ImageAnnotationHelpButton';
Line 2,944 ⟶ 2,941:
// Otherwise, it's either a sequence of up to three images, or a span, followed by a
// link.
if (tgt.nodeName.toLowerCase
tgt = wgServer + wgArticlePath.replace
else
tgt = tgt.href;
function make_handler (tgt) {
var target = tgt;
return function (evt) {
var e = evt || window.event;
if (e) return LAPI.Evt.kill
return false;
};
}
if (!imgs || !imgs.length
// We're supposed to have a spans giving the button text
if (text.nodeName.toLowerCase
text = LAPI.DOM.getInnerText
else
text = 'Help';
return LAPI.DOM.makeButton
'ImageAnnotationHelpButton'
, text
, make_handler
);
} else {
return Buttons.makeButton
}
},
Line 2,982 ⟶ 2,979:
{
var self = IA;
var shim;
if (!self.cover) {
var pos = { position : 'absolute'
Line 2,989 ⟶ 2,987:
,height : self.viewers[0].thumb.height + 'px'
};
self.cover = LAPI.make
self.border = self.cover.cloneNode
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.style.filter = 'alpha(Opacity=0)'; // Ensure transparency
// Unfortunately, IE6/SP2 has a "security setting" called "Binary and script
Line 3,007 ⟶ 3,005:
self.ieFix = shim;
// And now the bgImage div...
shim = LAPI.make
Object.merge
{ top : '0px'
,backgroundImage: 'url(' + self.viewers[0].img.src + ')'
Line 3,021 ⟶ 3,019:
// Hence we have to ensure that these events are killed even if our cover doesn't
// handle them.
shim.style.zIndex = self.cover.style.zIndex - 1;
LAPI.Evt.attach
function (evt) {return LAPI.Evt.kill
LAPI.Evt.attach
function (evt) {return LAPI.Evt.kill
LAPI.Evt.attach
function (evt) {return LAPI.Evt.kill
shim.style.cursor = 'default';
self.eventFix = shim;
Line 3,036 ⟶ 3,034:
return self.cover;
},
show_cover : function ()
{
Line 3,042 ⟶ 3,040:
if (self.cover && !self.cover_visible) {
if (self.ieFix) {
self.viewers[0].img_div.appendChild
self.viewers[0].img_div.appendChild
}
if (self.eventFix) self.viewers[0].img_div.appendChild
self.viewers[0].img_div.appendChild
self.cover_visible = true;
}
Line 3,056 ⟶ 3,054:
if (self.cover && self.cover_visible) {
if (self.ieFix) {
LAPI.DOM.removeNode
LAPI.DOM.removeNode
}
if (self.eventFix) LAPI.DOM.removeNode
LAPI.DOM.removeNode
self.cover_visible = false;
}
Line 3,072 ⟶ 3,070:
} else {
node = getElementsByClassName (scope, '*', 'image_annotation_' + what);
if (node && node.length
}
return node;
Line 3,079 ⟶ 3,077:
getItem : function (what, scope)
{
var node = IA.getRawItem
if (!node) return null;
return LAPI.DOM.getInnerText
},
getIntItem : function (what, scope)
{
var x = IA.getItem
if (x !== null) x = parseInt (x, 10);
return x;
Line 3,094 ⟶ 3,092:
{
function find (text, id, delim) {
var start = delim.start.replace
var start_match = text.indexOf
if (start_match < 0) return null;
var end = delim.end.replace
var end_match = text.indexOf
if (end_match < start_match + start.length) return null;
return {start: start_match, end: end_match + end.length};
}
Line 3,114 ⟶ 3,112:
var self = IA;
if (self.wiki_read) return;
Array.forEach
if
// Now
var code = pagetext.substring(span.start, span.end);
for
var end
if (j
return;
}
}
}
});
self.wiki_read = true;
},
Line 3,139 ⟶ 3,135:
setSummary : function (summary, initial_text, note_text)
{
if (initial_text.contains
var max = (summary.maxlength || 200) - initial_text.length;
if (note_text)
initial_text =
initial_text.replace
else
initial_text = initial_text.replace
}
summary.value = initial_text;
Line 3,154 ⟶ 3,150:
// Don't use LAPI here, it may not yet be available
if (bypass_caches) {
url += ((url.indexOf
}
// 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
s.setAttribute
s.setAttribute
document.getElementsByTagName
return s;
} else {
Line 3,174 ⟶ 3,170:
if (self.may_edit) {
if (!self.ajaxQueried) {
self.haveAjax = (LAPI.Ajax.getRequest
self.ajaxQueried = true;
self.may_edit = self.haveAjax;
if (!self.may_edit && self.button_div) {
LAPI.DOM.removeChildren
self.button_div.appendChild
(ImageAnnotator.UI.get
self.viewers[0].msg.style.display =
self.viewers[0].cannotEdit
}
}
Line 3,194 ⟶ 3,190:
function getElementsByClassName (scope, tag, className) {
if (window.jQuery) {
return
} else {
// For non-WMF wikis that might not have jQuery (yet),
return
}
}
window.ImageAnnotator = {
install: function (config) { IA.install
};
// Start it. Bypass caches; but allow for 4 hours client-side caching. Small file.
IA.getScript
wgScript + '?title=MediaWiki:ImageAnnotatorConfig.js&action=raw&ctype=text/javascript'
+ '&dummy=' + Math.floor
, true // No local caching!
);
|