Difference between revisions of "MediaWiki:Gadget-ImageAnnotator.js"
From Birocapedia
Jump to navigationJump to search (Created page with '// <source lang="javascript"> /* THIS IS ImageAnnotator VERSION 2 Image annotations. Draw rectangles onto image thumbnail displayed on image description page and associ...') |
|||
Line 1: | Line 1: | ||
// <source lang="javascript"> | // <source lang="javascript"> | ||
− | + | ||
/* | /* | ||
THIS IS ImageAnnotator VERSION 2 | THIS IS ImageAnnotator VERSION 2 | ||
− | + | ||
Image annotations. Draw rectangles onto image thumbnail displayed on image description | Image annotations. Draw rectangles onto image thumbnail displayed on image description | ||
page and associate them with textual descriptions that will be displayed when the mouse | page and associate them with textual descriptions that will be displayed when the mouse | ||
moves over the rectangles. If an image has annotations, display the rectangles. Add a | moves over the rectangles. If an image has annotations, display the rectangles. Add a | ||
button to create new annotations. | button to create new annotations. | ||
− | + | ||
Note: if an image that has annotations is overwritten by a new version, only display the | 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 if the size of the top image matches the stored size exactly. To recover | ||
Line 18: | Line 18: | ||
Choose whichever license of these you like best :-) | Choose whichever license of these you like best :-) | ||
− | + | ||
See http://commons.wikimedia.org/wiki/Help:Gadget-ImageAnnotator for documentation. | See http://commons.wikimedia.org/wiki/Help:Gadget-ImageAnnotator for documentation. | ||
*/ | */ | ||
− | + | ||
// Global: getElementsByClassName, importScript, importScriptURI (wiki.js) | // Global: getElementsByClassName, importScript, importScriptURI (wiki.js) | ||
// Global: wgPageName, wgCurRevisionId, wgUserGroups, wgRestrictionEdit (inline script on the page) | // Global: wgPageName, wgCurRevisionId, wgUserGroups, wgRestrictionEdit (inline script on the page) | ||
// Global: wgAction, wgNamespaceNumber, wgUserLanguage, wgContentLanguage (inline script) | // Global: wgAction, wgNamespaceNumber, wgUserLanguage, wgContentLanguage (inline script) | ||
− | + | ||
if (typeof (ImageAnnotator) == 'undefined') { // Guard against multiple inclusions | if (typeof (ImageAnnotator) == 'undefined') { // Guard against multiple inclusions | ||
− | + | ||
importScript ('MediaWiki:LAPI.js'); | importScript ('MediaWiki:LAPI.js'); | ||
importScript ('MediaWiki:Tooltips.js'); | importScript ('MediaWiki:Tooltips.js'); | ||
importScript ('MediaWiki:TextCleaner.js'); | importScript ('MediaWiki:TextCleaner.js'); | ||
importScript ('MediaWiki:UIElements.js'); | importScript ('MediaWiki:UIElements.js'); | ||
− | + | ||
var ImageAnnotator_DefaultLanguage = wgContentLanguage; | var ImageAnnotator_DefaultLanguage = wgContentLanguage; | ||
− | + | ||
var ImageAnnotation = function () {this.initialize.apply (this, arguments);} | var ImageAnnotation = function () {this.initialize.apply (this, arguments);} | ||
− | + | ||
ImageAnnotation.compare = function (a, b) | ImageAnnotation.compare = function (a, b) | ||
{ | { | ||
Line 43: | Line 43: | ||
return a.model.id - b.model.id; // Just to make sure the order is complete | return a.model.id - b.model.id; // Just to make sure the order is complete | ||
}; | }; | ||
− | + | ||
ImageAnnotation.prototype = | ImageAnnotation.prototype = | ||
{ | { | ||
Line 51: | Line 51: | ||
content : null, // Content of the tooltip | content : null, // Content of the tooltip | ||
viewer : null, // Reference to the viewer this note belongs to | viewer : null, // Reference to the viewer this note belongs to | ||
− | + | ||
initialize : function (node, viewer, id) | initialize : function (node, viewer, id) | ||
{ | { | ||
Line 157: | Line 157: | ||
} | } | ||
}, | }, | ||
− | + | ||
setTooltip : function () | setTooltip : function () | ||
{ | { | ||
Line 201: | Line 201: | ||
); | ); | ||
}, | }, | ||
− | + | ||
display : function (evt) | display : function (evt) | ||
{ | { | ||
Line 253: | Line 253: | ||
return this.content; | return this.content; | ||
}, | }, | ||
− | + | ||
edit : function (evt) | edit : function (evt) | ||
{ | { | ||
Line 260: | Line 260: | ||
return false; | return false; | ||
}, | }, | ||
− | + | ||
remove_event : function (evt) | remove_event : function (evt) | ||
{ | { | ||
Line 266: | Line 266: | ||
return LAPI.Evt.kill (evt); | return LAPI.Evt.kill (evt); | ||
}, | }, | ||
− | + | ||
remove : function () | remove : function () | ||
{ | { | ||
Line 274: | Line 274: | ||
} | } | ||
// Close and remove tooltip only if edit succeeded! Where and how to display error messages? | // Close and remove tooltip only if edit succeeded! Where and how to display error messages? | ||
− | + | ||
// Prompt for a removal reson | // Prompt for a removal reson | ||
var reason = window.prompt (ImageAnnotator.UI.get ('wpImageAnnotatorDeleteReason', true), ""); | var reason = window.prompt (ImageAnnotator.UI.get ('wpImageAnnotatorDeleteReason', true), ""); | ||
Line 281: | Line 281: | ||
// it was hidden because of the alert. If possible, we want the user to see the spinner. | // it was hidden because of the alert. If possible, we want the user to see the spinner. | ||
this.tooltip.show_now (this.tooltip); | this.tooltip.show_now (this.tooltip); | ||
− | + | ||
var self = this; | var self = this; | ||
var spinnerId = 'image_annotation_delete_' + this.model.id; | var spinnerId = 'image_annotation_delete_' + this.model.id; | ||
Line 293: | Line 293: | ||
if (revision_id && revision_id != wgCurRevisionId) | if (revision_id && revision_id != wgCurRevisionId) | ||
throw new Error ('#Page version (revision ID) mismatch: edit conflict.'); | throw new Error ('#Page version (revision ID) mismatch: edit conflict.'); | ||
− | + | ||
var textbox = editForm.wpTextbox1; | var textbox = editForm.wpTextbox1; | ||
if (!textbox) throw new Error ('#Server replied with invalid edit page.'); | if (!textbox) throw new Error ('#Server replied with invalid edit page.'); | ||
Line 301: | Line 301: | ||
// We normally don't care, but here we need this to make sure we don't leave extra line | // 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. | // breaks when we remove the note. | ||
− | + | ||
ImageAnnotator.setWikitext (pagetext); | ImageAnnotator.setWikitext (pagetext); | ||
− | + | ||
var span = ImageAnnotator.findNote (pagetext, self.model.id); | var span = ImageAnnotator.findNote (pagetext, self.model.id); | ||
if (!span) { // Hmmm? Doesn't seem to exist | if (!span) { // Hmmm? Doesn't seem to exist | ||
Line 364: | Line 364: | ||
} | } | ||
); | ); | ||
− | + | ||
return true; | return true; | ||
}, | }, | ||
− | + | ||
destroy : function () | destroy : function () | ||
{ | { | ||
Line 380: | Line 380: | ||
this.viewer = null; | this.viewer = null; | ||
}, | }, | ||
− | + | ||
area : function () | area : function () | ||
{ | { | ||
Line 386: | Line 386: | ||
return (this.model.dimension.w * this.model.dimension.h); | return (this.model.dimension.w * this.model.dimension.h); | ||
} | } | ||
− | + | ||
}; // end ImageAnnotation | }; // end ImageAnnotation | ||
− | + | ||
var ImageAnnotationEditor = function () {this.initialize.apply (this, arguments);}; | var ImageAnnotationEditor = function () {this.initialize.apply (this, arguments);}; | ||
− | + | ||
if (typeof (ImageAnnotationEditor_columns) == 'undefined') | if (typeof (ImageAnnotationEditor_columns) == 'undefined') | ||
var ImageAnnotationEditor_columns = 50; | var ImageAnnotationEditor_columns = 50; | ||
− | + | ||
ImageAnnotationEditor.prototype = | ImageAnnotationEditor.prototype = | ||
{ | { | ||
Line 477: | Line 477: | ||
); | ); | ||
}, | }, | ||
− | + | ||
get_editor : function () | get_editor : function () | ||
{ | { | ||
return this.box; | return this.box; | ||
}, | }, | ||
− | + | ||
editNote : function (note) | editNote : function (note) | ||
{ | { | ||
Line 488: | Line 488: | ||
this.note = note; | this.note = note; | ||
this.viewer = this.note.viewer; | this.viewer = this.note.viewer; | ||
− | + | ||
var cover = ImageAnnotator.get_cover (); | var cover = ImageAnnotator.get_cover (); | ||
cover.style.cursor = 'auto'; | cover.style.cursor = 'auto'; | ||
ImageAnnotator.show_cover (); | ImageAnnotator.show_cover (); | ||
− | + | ||
if (note.tooltip) note.tooltip.hide_now (); | if (note.tooltip) note.tooltip.hide_now (); | ||
− | + | ||
ImageAnnotator.is_editing = true; | ImageAnnotator.is_editing = true; | ||
if (note.content && !ImageAnnotator.wiki_read) { | if (note.content && !ImageAnnotator.wiki_read) { | ||
Line 532: | Line 532: | ||
} | } | ||
}, | }, | ||
− | + | ||
open_editor : function (same_note, cover) | open_editor : function (same_note, cover) | ||
{ | { | ||
Line 564: | Line 564: | ||
this.visible = true; | this.visible = true; | ||
}, | }, | ||
− | + | ||
hide_editor : function (evt) | hide_editor : function (evt) | ||
{ | { | ||
Line 584: | Line 584: | ||
// pressed, it may actually be anywhere.) | // pressed, it may actually be anywhere.) | ||
}, | }, | ||
− | + | ||
save : function (editor) | save : function (editor) | ||
{ | { | ||
Line 672: | Line 672: | ||
// Page was edited since the user loaded it. | // Page was edited since the user loaded it. | ||
throw new Error ('#Page version (revision ID) mismatch: edit conflict.'); | throw new Error ('#Page version (revision ID) mismatch: edit conflict.'); | ||
− | + | ||
// Modify the page | // Modify the page | ||
var textbox = editForm.wpTextbox1; | var textbox = editForm.wpTextbox1; | ||
if (!textbox) throw new Error ('#Server replied with invalid edit page.'); | if (!textbox) throw new Error ('#Server replied with invalid edit page.'); | ||
var pagetext = textbox.value; | var pagetext = textbox.value; | ||
− | + | ||
ImageAnnotator.setWikitext (pagetext); | ImageAnnotator.setWikitext (pagetext); | ||
− | + | ||
var span = null; | var span = null; | ||
if (self.note.content) // Otherwise it's a new note! | if (self.note.content) // Otherwise it's a new note! | ||
Line 830: | Line 830: | ||
); | ); | ||
}, | }, | ||
− | + | ||
onpreview : function (editor) | onpreview : function (editor) | ||
{ | { | ||
if (this.tooltip) this.tooltip.size_change (); | if (this.tooltip) this.tooltip.size_change (); | ||
}, | }, | ||
− | + | ||
cancel : function (editor) | cancel : function (editor) | ||
{ | { | ||
Line 846: | Line 846: | ||
if (editor) this.hide_editor (); | if (editor) this.hide_editor (); | ||
}, | }, | ||
− | + | ||
close_tooltip : function (tooltip, evt) | close_tooltip : function (tooltip, evt) | ||
{ | { | ||
Line 852: | Line 852: | ||
this.cancel (); | this.cancel (); | ||
} | } | ||
− | + | ||
}; | }; | ||
− | + | ||
var ImageNotesViewer = function () {this.initialize.apply (this, arguments); }; | var ImageNotesViewer = function () {this.initialize.apply (this, arguments); }; | ||
− | + | ||
ImageNotesViewer.prototype = | ImageNotesViewer.prototype = | ||
{ | { | ||
Line 874: | Line 874: | ||
,dy : this.full_img.height / this.thumb.height | ,dy : this.full_img.height / this.thumb.height | ||
}; | }; | ||
− | + | ||
if (!this.isThumbnail && !this.isOther) { | if (!this.isThumbnail && !this.isOther) { | ||
this.setup (); | this.setup (); | ||
Line 887: | Line 887: | ||
} | } | ||
}, | }, | ||
− | + | ||
setup : function (onlyIcon) | setup : function (onlyIcon) | ||
{ | { | ||
Line 898: | Line 898: | ||
this.realName = ((name && name.length > 0) ? LAPI.DOM.getInnerText (name[0]) : ""); | this.realName = ((name && name.length > 0) ? LAPI.DOM.getInnerText (name[0]) : ""); | ||
} | } | ||
− | + | ||
var annotations = getElementsByClassName (this.scope, 'div', ImageAnnotator.annotation_class); | var annotations = getElementsByClassName (this.scope, 'div', ImageAnnotator.annotation_class); | ||
− | + | ||
if (!this.may_edit && (!annotations || annotations.length == 0)) | if (!this.may_edit && (!annotations || annotations.length == 0)) | ||
return; // Nothing to do | return; // Nothing to do | ||
− | + | ||
// A div inserted around the image. It ensures that everything we add is positioned properly | // 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. | // over the image, even if the browser window size changes and re-layouts occur. | ||
Line 1,049: | Line 1,049: | ||
if (this.isThumbnail) this.msg.style.fontSize = '90%'; | if (this.isThumbnail) this.msg.style.fontSize = '90%'; | ||
this.main_div.appendChild (this.msg); | this.main_div.appendChild (this.msg); | ||
− | + | ||
// Set overflow parents, if any | // Set overflow parents, if any | ||
− | + | ||
var simple = !!window.getComputedStyle; | var simple = !!window.getComputedStyle; | ||
var checks = (simple ? ['overflow', 'overflow-x', 'overflow-y'] | var checks = (simple ? ['overflow', 'overflow-x', 'overflow-y'] | ||
Line 1,077: | Line 1,077: | ||
ImageAnnotator.may_edit = false; | ImageAnnotator.may_edit = false; | ||
} | } | ||
− | + | ||
this.show_evt = LAPI.Evt.makeListener (this, this.show); | this.show_evt = LAPI.Evt.makeListener (this, this.show); | ||
if (this.overflowParents || LAPI.Browser.is_ie) { | if (this.overflowParents || LAPI.Browser.is_ie) { | ||
Line 1,096: | Line 1,096: | ||
this.setDefaultMsg (); | this.setDefaultMsg (); | ||
}, | }, | ||
− | + | ||
setShowHideEvents : function (set) | setShowHideEvents : function (set) | ||
{ | { | ||
Line 1,111: | Line 1,111: | ||
} | } | ||
}, | }, | ||
− | + | ||
removeMoveListener : function () | removeMoveListener : function () | ||
{ | { | ||
Line 1,122: | Line 1,122: | ||
} | } | ||
}, | }, | ||
− | + | ||
adjustRectangleSize : function (node) | adjustRectangleSize : function (node) | ||
{ | { | ||
Line 1,154: | Line 1,154: | ||
} | } | ||
}, | }, | ||
− | + | ||
toggle : function (dummies) | toggle : function (dummies) | ||
{ | { | ||
Line 1,177: | Line 1,177: | ||
this.visible = !this.visible; | this.visible = !this.visible; | ||
}, | }, | ||
− | + | ||
show : function (evt) | show : function (evt) | ||
{ | { | ||
Line 1,189: | Line 1,189: | ||
} | } | ||
}, | }, | ||
− | + | ||
hide : function (evt) | hide : function (evt) | ||
{ | { | ||
Line 1,214: | Line 1,214: | ||
// Compute the actually visible region by intersecting the rectangle given by img_pos and | // 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. | // this.img.offsetWidth, this.img.offsetTop with the rectangles of all overflow parents. | ||
− | + | ||
function intersect_rectangles (a, b) | function intersect_rectangles (a, b) | ||
{ | { | ||
Line 1,223: | Line 1,223: | ||
}; | }; | ||
} | } | ||
− | + | ||
for (var i = 0; i < this.overflowParents.length && rect.x < rect.r && rect.y < rect.b; i++) { | for (var i = 0; i < this.overflowParents.length && rect.x < rect.r && rect.y < rect.b; i++) { | ||
img_pos = LAPI.Pos.position (this.overflowParents[i]); | img_pos = LAPI.Pos.position (this.overflowParents[i]); | ||
Line 1,260: | Line 1,260: | ||
return true; | return true; | ||
}, | }, | ||
− | + | ||
check_hide : function (evt) | check_hide : function (evt) | ||
{ | { | ||
Line 1,268: | Line 1,268: | ||
return true; | return true; | ||
}, | }, | ||
− | + | ||
register : function (new_note) | register : function (new_note) | ||
{ | { | ||
Line 1,278: | Line 1,278: | ||
} | } | ||
}, | }, | ||
− | + | ||
deregister : function (note) | deregister : function (note) | ||
{ | { | ||
Array.remove (this.annotations, note); | Array.remove (this.annotations, note); | ||
if (note.model.id == this.max_id) this.max_id--; | if (note.model.id == this.max_id) this.max_id--; | ||
+ | if (this.annotations.length == 0) this.setDefaultMsg (); //If we removed the last one, clear the msg | ||
}, | }, | ||
− | + | ||
setDefaultMsg : function () | setDefaultMsg : function () | ||
{ | { | ||
Line 1,311: | Line 1,312: | ||
this.msg.style.display = ""; | this.msg.style.display = ""; | ||
} else { | } else { | ||
− | this.msg.style.display = 'none'; | + | if (this.msg) this.msg.style.display = 'none'; |
} | } | ||
if (ImageAnnotator.button_div && this.may_edit) ImageAnnotator.button_div.style.display = ""; | if (ImageAnnotator.button_div && this.may_edit) ImageAnnotator.button_div.style.display = ""; | ||
} | } | ||
− | + | ||
}; | }; | ||
− | + | ||
// User configurations | // User configurations | ||
if (typeof (ImageAnnotator_zoom_threshold) == 'undefined') | if (typeof (ImageAnnotator_zoom_threshold) == 'undefined') | ||
var ImageAnnotator_zoom_threshold = 8.0; | var ImageAnnotator_zoom_threshold = 8.0; | ||
− | + | ||
var ImageAnnotator = | var ImageAnnotator = | ||
{ | { | ||
Line 1,327: | Line 1,328: | ||
// annotations in the page source, and adds an "Annotate this image" button plus the support | // 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. | // for drawing rectangles onto the image if there is only one image and editing is allowed. | ||
− | + | ||
haveAjax : false, | haveAjax : false, | ||
− | + | ||
button_div : null, | button_div : null, | ||
add_button : null, | add_button : null, | ||
− | + | ||
cover : null, | cover : null, | ||
border : null, | border : null, | ||
definer : null, | definer : null, | ||
− | + | ||
mouse_in : (!!window.ActiveXObject ? 'mouseenter' : 'mouseover'), | mouse_in : (!!window.ActiveXObject ? 'mouseenter' : 'mouseover'), | ||
mouse_out : (!!window.ActiveXObject ? 'mouseleave' : 'mouseout'), | mouse_out : (!!window.ActiveXObject ? 'mouseleave' : 'mouseout'), | ||
− | + | ||
annotation_class : 'image_annotation', | annotation_class : 'image_annotation', | ||
− | + | ||
// Format of notes in Wikitext. Note: there are two formats, an old one and a new one. | // Format of notes in Wikitext. Note: there are two formats, an old one and a new one. | ||
// We only write the newest (last) one, but we can read also the older formats. Order is | // We only write the newest (last) one, but we can read also the older formats. Order is | ||
Line 1,359: | Line 1,360: | ||
} | } | ||
], | ], | ||
− | + | ||
tooltip_styles : // The style for all our tooltips | tooltip_styles : // The style for all our tooltips | ||
{ border : '1px solid #8888aa' | { border : '1px solid #8888aa' | ||
Line 1,367: | Line 1,368: | ||
// Scale up to default text size | // Scale up to default text size | ||
}, | }, | ||
− | + | ||
editor : null, | editor : null, | ||
− | + | ||
wiki_read : false, | wiki_read : false, | ||
is_rtl : false, | is_rtl : false, | ||
− | + | ||
move_listening : false, | move_listening : false, | ||
is_tracking : false, | is_tracking : false, | ||
is_adding : false, | is_adding : false, | ||
is_editing : false, | is_editing : false, | ||
− | + | ||
zoom_threshold : 8.0, | zoom_threshold : 8.0, | ||
zoom_factor : 4.0, | zoom_factor : 4.0, | ||
− | + | ||
install_attempts : 0, | install_attempts : 0, | ||
max_install_attempts : 10, // Maximum 5 seconds | max_install_attempts : 10, // Maximum 5 seconds | ||
− | + | ||
imgs_with_notes : [], | imgs_with_notes : [], | ||
thumbs : [], | thumbs : [], | ||
other_images : [], | other_images : [], | ||
− | + | ||
// Fallback | // Fallback | ||
indication_icon : 'http://upload.wikimedia.org/wikipedia/commons/8/8a/Gtk-dialog-info-14px.png', | indication_icon : 'http://upload.wikimedia.org/wikipedia/commons/8/8a/Gtk-dialog-info-14px.png', | ||
− | + | ||
config : null, | config : null, | ||
− | + | ||
install : function (config) | install : function (config) | ||
{ | { | ||
if (typeof (ImageAnnotator_disable) != 'undefined' && !!ImageAnnotator_disable) return; | if (typeof (ImageAnnotator_disable) != 'undefined' && !!ImageAnnotator_disable) return; | ||
if (!config || ImageAnnotator.config) return; | if (!config || ImageAnnotator.config) return; | ||
− | + | ||
// Double check. | // Double check. | ||
if (!( wgNamespaceNumber >= 0 && config.viewingEnabled () | if (!( wgNamespaceNumber >= 0 && config.viewingEnabled () | ||
Line 1,407: | Line 1,408: | ||
return; | return; | ||
} | } | ||
− | + | ||
var self = ImageAnnotator; | var self = ImageAnnotator; | ||
− | + | ||
self.config = config; | self.config = config; | ||
// Determine whether we have XmlHttp. We try to determine this here to be able to avoid | // Determine whether we have XmlHttp. We try to determine this here to be able to avoid | ||
Line 1,425: | Line 1,426: | ||
self.ajaxQueried = false; | self.ajaxQueried = false; | ||
} | } | ||
− | + | ||
// We'll include self.haveAjax later on once more, to catch the !ajaxQueried case. | // We'll include self.haveAjax later on once more, to catch the !ajaxQueried case. | ||
self.may_edit = self.haveAjax && config.editingEnabled (); | self.may_edit = self.haveAjax && config.editingEnabled (); | ||
− | + | ||
function namespaceCheck (list) | function namespaceCheck (list) | ||
{ | { | ||
Line 1,441: | Line 1,442: | ||
return false; | return false; | ||
} | } | ||
− | + | ||
self.rules = {inline: {}, thumbs: {}, shared : {}}; | self.rules = {inline: {}, thumbs: {}, shared : {}}; | ||
− | + | ||
// Now set the default rules. Undefined means default setting (true for show, false for icon), | // 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. | // but overrideable by per-image rules. If set, it's not overrideable by per-image rules. | ||
Line 1,477: | Line 1,478: | ||
self.rules.thumbs.icon = true; | self.rules.thumbs.icon = true; | ||
} | } | ||
− | + | ||
var do_images = typeof (self.rules.inline.show) == 'undefined' || self.rules.inline.show; | var do_images = typeof (self.rules.inline.show) == 'undefined' || self.rules.inline.show; | ||
− | + | ||
if (do_images) { | if (do_images) { | ||
// Per-article switching off of note display on inline images and thumbnails | // Per-article switching off of note display on inline images and thumbnails | ||
Line 1,516: | Line 1,517: | ||
} | } | ||
} | } | ||
− | + | ||
// Make sure the shared value is set | // Make sure the shared value is set | ||
self.rules.shared.show = | self.rules.shared.show = | ||
typeof (self.rules.shared.show) == 'undefined' || self.rules.shared.show; | typeof (self.rules.shared.show) == 'undefined' || self.rules.shared.show; | ||
− | + | ||
do_images = typeof (self.rules.inline.show) == 'undefined' || self.rules.inline.show; | do_images = typeof (self.rules.inline.show) == 'undefined' || self.rules.inline.show; | ||
var do_thumbs = do_images | var do_thumbs = do_images | ||
&& (typeof (self.rules.thumbs.show) == 'undefined' || self.rules.thumbs.show); | && (typeof (self.rules.thumbs.show) == 'undefined' || self.rules.thumbs.show); | ||
− | + | ||
if (do_images) { | if (do_images) { | ||
var bodyContent = document.getElementById ('bodyContent') // monobook, vector | var bodyContent = document.getElementById ('bodyContent') // monobook, vector | ||
Line 1,592: | Line 1,593: | ||
} | } | ||
}, | }, | ||
− | + | ||
wait_for_required_libraries : function () | wait_for_required_libraries : function () | ||
{ | { | ||
Line 1,606: | Line 1,607: | ||
ImageAnnotator.setup(); | ImageAnnotator.setup(); | ||
}, | }, | ||
− | + | ||
setup: function () | setup: function () | ||
{ | { | ||
var self = ImageAnnotator; | var self = ImageAnnotator; | ||
self.imgs = []; | self.imgs = []; | ||
− | + | ||
function img_check (img, is_other) | function img_check (img, is_other) | ||
{ | { | ||
Line 1,647: | Line 1,648: | ||
return img; | return img; | ||
} | } | ||
− | + | ||
function setup_one (scope) { | function setup_one (scope) { | ||
var file_div = scope; | var file_div = scope; | ||
Line 1,727: | Line 1,728: | ||
}; | }; | ||
} | } | ||
− | + | ||
function setup_images (list) | function setup_images (list) | ||
{ | { | ||
Line 1,737: | Line 1,738: | ||
); | ); | ||
} | } | ||
− | + | ||
if (wgNamespaceNumber == 6) { | if (wgNamespaceNumber == 6) { | ||
setup_images ([document]); | setup_images ([document]); | ||
Line 1,746: | Line 1,747: | ||
self.may_edit = self.may_edit && (self.imgs.length == 1); | self.may_edit = self.may_edit && (self.imgs.length == 1); | ||
} | } | ||
− | + | ||
self.may_edit = self.may_edit && document.URL.search (/[?&]oldid=/) < 0; | self.may_edit = self.may_edit && document.URL.search (/[?&]oldid=/) < 0; | ||
− | + | ||
if (self.haveAjax) { | if (self.haveAjax) { | ||
setup_images (self.thumbs); | setup_images (self.thumbs); | ||
setup_images (self.other_images); | setup_images (self.other_images); | ||
} | } | ||
− | + | ||
if (self.imgs.length == 0) return; | 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 | // 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. | // 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) { | if (self.imgs.length == 1 && self.imgs[0].scope == document && !self.haveAjax) { | ||
// Try to get the full size without Ajax. | // Try to get the full size without Ajax. | ||
Line 1,767: | Line 1,768: | ||
} | } | ||
} | } | ||
− | + | ||
// Get the full sizes of all the images. If more than 50, make several calls. (The API has limits.) | // 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... | // Also avoid using Ajax on IE6... | ||
Line 1,773: | Line 1,774: | ||
var cache = {}; | var cache = {}; | ||
var names = []; | var names = []; | ||
− | + | ||
Array.forEach ( | Array.forEach ( | ||
self.imgs | self.imgs | ||
Line 1,785: | Line 1,786: | ||
} | } | ||
); | ); | ||
− | + | ||
var to_do = names.length; | var to_do = names.length; | ||
var done = 0; | var done = 0; | ||
− | + | ||
function check_done (length) | function check_done (length) | ||
{ | { | ||
Line 1,797: | Line 1,798: | ||
} | } | ||
} | } | ||
− | + | ||
function make_calls (execute_call, url_limit) | function make_calls (execute_call, url_limit) | ||
{ | { | ||
Line 1,815: | Line 1,816: | ||
return {text: text, n: done}; | return {text: text, n: done}; | ||
} | } | ||
− | + | ||
var start = 0, chunk = 0, params; | var start = 0, chunk = 0, params; | ||
while (to_do > 0) { | while (to_do > 0) { | ||
Line 1,824: | Line 1,825: | ||
} | } | ||
} | } | ||
− | + | ||
function set_info (json) | function set_info (json) | ||
{ | { | ||
Line 1,859: | Line 1,860: | ||
} | } | ||
} | } | ||
− | + | ||
if ((!window.XMLHttpRequest && !!window.ActiveXObject) || !self.haveAjax) { | if ((!window.XMLHttpRequest && !!window.ActiveXObject) || !self.haveAjax) { | ||
// IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that | // IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that | ||
Line 1,920: | Line 1,921: | ||
} // end if can use Ajax | } // end if can use Ajax | ||
}, | }, | ||
− | + | ||
setup_ui : function () | setup_ui : function () | ||
{ | { | ||
// Complete the UI object we've gotten from config. | // Complete the UI object we've gotten from config. | ||
− | + | ||
ImageAnnotator.UI.ready = false; | ImageAnnotator.UI.ready = false; | ||
ImageAnnotator.UI.repo = null; | ImageAnnotator.UI.repo = null; | ||
ImageAnnotator.UI.needs_plea = false; | ImageAnnotator.UI.needs_plea = false; | ||
− | + | ||
var readyEvent = []; | var readyEvent = []; | ||
− | + | ||
ImageAnnotator.UI.fireReadyEvent = function () | ImageAnnotator.UI.fireReadyEvent = function () | ||
{ | { | ||
Line 1,945: | Line 1,946: | ||
readyEvent = null; | readyEvent = null; | ||
} | } | ||
− | + | ||
ImageAnnotator.UI.addReadyEventHandler = function (f) | ImageAnnotator.UI.addReadyEventHandler = function (f) | ||
{ | { | ||
Line 1,954: | Line 1,955: | ||
} | } | ||
} | } | ||
− | + | ||
ImageAnnotator.UI.setup = function () | ImageAnnotator.UI.setup = function () | ||
{ | { | ||
Line 1,981: | Line 1,982: | ||
LAPI.DOM.removeNode (node); | LAPI.DOM.removeNode (node); | ||
}; | }; | ||
− | + | ||
ImageAnnotator.UI.get = function (id, basic, no_plea) | ImageAnnotator.UI.get = function (id, basic, no_plea) | ||
{ | { | ||
Line 2,012: | Line 2,013: | ||
return result; | return result; | ||
}; | }; | ||
− | + | ||
ImageAnnotator.UI.get_plea = function () | ImageAnnotator.UI.get_plea = function () | ||
{ | { | ||
Line 2,031: | Line 2,032: | ||
return span; | return span; | ||
}; | }; | ||
− | + | ||
ImageAnnotator.UI.init = function (html_text_or_json) | ImageAnnotator.UI.init = function (html_text_or_json) | ||
{ | { | ||
Line 2,045: | Line 2,046: | ||
else | else | ||
text = null; | text = null; | ||
− | + | ||
if (!text) { | if (!text) { | ||
ImageAnnotator.UI.fireReadyEvent (); | ImageAnnotator.UI.fireReadyEvent (); | ||
return; | return; | ||
} | } | ||
− | + | ||
var node = LAPI.make ('div', null, {display: 'none'}); | var node = LAPI.make ('div', null, {display: 'none'}); | ||
document.body.appendChild (node); | document.body.appendChild (node); | ||
Line 2,063: | Line 2,064: | ||
ImageAnnotator.UI.fireReadyEvent (); | ImageAnnotator.UI.fireReadyEvent (); | ||
}; | }; | ||
− | + | ||
var ui_page = '{{MediaWiki:ImageAnnotatorTexts' | var ui_page = '{{MediaWiki:ImageAnnotatorTexts' | ||
+ (wgUserLanguage != wgContentLanguage ? '|lang=' + wgUserLanguage : "") | + (wgUserLanguage != wgContentLanguage ? '|lang=' + wgUserLanguage : "") | ||
+ '|live=1}}'; | + '|live=1}}'; | ||
− | + | ||
function get_ui_no_ajax () | function get_ui_no_ajax () | ||
{ | { | ||
Line 2,080: | Line 2,081: | ||
ImageAnnotator.getScript (url, true); // No local caching! | ImageAnnotator.getScript (url, true); // No local caching! | ||
} | } | ||
− | + | ||
function get_ui () | function get_ui () | ||
{ | { | ||
ImageAnnotator.haveAjax = (LAPI.Ajax.getRequest () != null); | ImageAnnotator.haveAjax = (LAPI.Ajax.getRequest () != null); | ||
ImageAnnotator.ajaxQueried = true; | ImageAnnotator.ajaxQueried = true; | ||
− | + | ||
// Works only with Ajax (but then, most of this script doesn't work without). | // 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 | // Check what this does to load times... If lots of people used this, it might be better to | ||
Line 2,094: | Line 2,095: | ||
return; | return; | ||
} | } | ||
− | + | ||
LAPI.Ajax.parseWikitext ( | LAPI.Ajax.parseWikitext ( | ||
ui_page | ui_page | ||
Line 2,105: | Line 2,106: | ||
); | ); | ||
} // end get_ui | } // end get_ui | ||
− | + | ||
if (!window.XMLHttpRequest && !!window.ActiveXObject) { | if (!window.XMLHttpRequest && !!window.ActiveXObject) { | ||
// IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that | // IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that | ||
Line 2,115: | Line 2,116: | ||
} | } | ||
}, | }, | ||
− | + | ||
setup_step_two : function () | setup_step_two : function () | ||
{ | { | ||
var self = ImageAnnotator; | var self = ImageAnnotator; | ||
− | + | ||
// Throw out any images for which we miss either the thumbnail or the full image size. | // 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. | // Also throws out thumbnails that are larger than the full image. | ||
Line 2,137: | Line 2,138: | ||
} | } | ||
); | ); | ||
− | + | ||
if (self.imgs.length == 0) return; | if (self.imgs.length == 0) return; | ||
− | + | ||
// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]] | // Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]] | ||
self.is_rtl = | self.is_rtl = | ||
Line 2,147: | Line 2,148: | ||
) | ) | ||
; | ; | ||
− | + | ||
self.UI.addReadyEventHandler (ImageAnnotator.complete_setup); | self.UI.addReadyEventHandler (ImageAnnotator.complete_setup); | ||
}, | }, | ||
− | + | ||
complete_setup : function () | complete_setup : function () | ||
{ | { | ||
Line 2,156: | Line 2,157: | ||
// UI object is fired. | // UI object is fired. | ||
var self = ImageAnnotator; | var self = ImageAnnotator; | ||
− | + | ||
// Check edit permissions | // Check edit permissions | ||
if (self.may_edit) { | if (self.may_edit) { | ||
Line 2,166: | Line 2,167: | ||
); | ); | ||
} | } | ||
− | + | ||
if (self.may_edit) { | if (self.may_edit) { | ||
// Check whether the image is local. Don't allow editing if the file is remote. | // Check whether the image is local. Don't allow editing if the file is remote. | ||
Line 2,182: | Line 2,183: | ||
self.may_edit = (img_page_name.replace (/ /g, '_') == wgTitle.replace (/ /g, '_')); | self.may_edit = (img_page_name.replace (/ /g, '_') == wgTitle.replace (/ /g, '_')); | ||
} | } | ||
− | + | ||
// Now create viewers for all images | // Now create viewers for all images | ||
self.viewers = new Array (self.imgs.length); | self.viewers = new Array (self.imgs.length); | ||
Line 2,188: | Line 2,189: | ||
self.viewers[i] = new ImageNotesViewer (self.imgs[i], i == 0 && self.may_edit); | self.viewers[i] = new ImageNotesViewer (self.imgs[i], i == 0 && self.may_edit); | ||
}; | }; | ||
− | + | ||
if (self.may_edit) { | if (self.may_edit) { | ||
if (!self.ajaxQueried) { | if (!self.ajaxQueried) { | ||
Line 2,196: | Line 2,197: | ||
self.may_edit = self.haveAjax; | self.may_edit = self.haveAjax; | ||
} | } | ||
− | + | ||
if (self.may_edit) { | if (self.may_edit) { | ||
− | + | ||
// Respect user override for zoom, if any | // Respect user override for zoom, if any | ||
if ( !isNaN (ImageAnnotator_zoom_threshold) | if ( !isNaN (ImageAnnotator_zoom_threshold) | ||
Line 2,222: | Line 2,223: | ||
} | } | ||
} | } | ||
− | + | ||
self.editor = new ImageAnnotationEditor (); | self.editor = new ImageAnnotationEditor (); | ||
− | + | ||
function track (evt) { | function track (evt) { | ||
evt = evt || window.event; | evt = evt || window.event; | ||
Line 2,251: | Line 2,252: | ||
return LAPI.Evt.kill (evt); | return LAPI.Evt.kill (evt); | ||
}; | }; | ||
− | + | ||
function pause (evt) | function pause (evt) | ||
{ | { | ||
Line 2,259: | Line 2,260: | ||
self.move_listening = false; | self.move_listening = false; | ||
}; | }; | ||
− | + | ||
function resume (evt) | function resume (evt) | ||
{ | { | ||
Line 2,271: | Line 2,272: | ||
} | } | ||
}; | }; | ||
− | + | ||
function stop_tracking (evt) | function stop_tracking (evt) | ||
{ | { | ||
Line 2,319: | Line 2,320: | ||
return false; | return false; | ||
}; | }; | ||
− | + | ||
function start_tracking (evt) | function start_tracking (evt) | ||
{ | { | ||
Line 2,353: | Line 2,354: | ||
return false; | return false; | ||
}; | }; | ||
− | + | ||
function add_new (evt) | function add_new (evt) | ||
{ | { | ||
Line 2,400: | Line 2,401: | ||
self.viewers[0].msg.style.display = ""; | self.viewers[0].msg.style.display = ""; | ||
}; | }; | ||
− | + | ||
self.button_div = LAPI.make ('div'); | self.button_div = LAPI.make ('div'); | ||
self.viewers[0].main_div.appendChild (self.button_div); | self.viewers[0].main_div.appendChild (self.button_div); | ||
Line 2,418: | Line 2,419: | ||
if (add_plea && wgServer.contains ('/commons')) | if (add_plea && wgServer.contains ('/commons')) | ||
self.button_div.appendChild (self.UI.get_plea ()); | self.button_div.appendChild (self.UI.get_plea ()); | ||
− | + | ||
} // end if may_edit | } // end if may_edit | ||
− | + | ||
// Get the file description pages of thumbnails. Figure out for which viewers we need to do this. | // Get the file description pages of thumbnails. Figure out for which viewers we need to do this. | ||
var cache = {}; | var cache = {}; | ||
Line 2,442: | Line 2,443: | ||
} | } | ||
); | ); | ||
− | + | ||
if (get_local.length == 0 && get_foreign.length == 0) return; | 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 | // 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 | // 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. | // 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 make_calls (list, execute_call, url_limit) | ||
{ | { | ||
Line 2,473: | Line 2,474: | ||
return {text: text, n: done}; | return {text: text, n: done}; | ||
} | } | ||
− | + | ||
var param = compose (list, from, length, url_limit); | var param = compose (list, from, length, url_limit); | ||
execute_call (param.text); | execute_call (param.text); | ||
return param.n; | return param.n; | ||
} | } | ||
− | + | ||
var start = 0, chunk = 0, to_do = list.length; | var start = 0, chunk = 0, to_do = list.length; | ||
while (to_do > 0) { | while (to_do > 0) { | ||
Line 2,486: | Line 2,487: | ||
} | } | ||
} | } | ||
− | + | ||
function setup_thumb_viewers (html_text) | function setup_thumb_viewers (html_text) | ||
{ | { | ||
Line 2,564: | Line 2,565: | ||
LAPI.DOM.removeNode (node); | LAPI.DOM.removeNode (node); | ||
} | } | ||
− | + | ||
self.script_callbacks = []; | self.script_callbacks = []; | ||
− | + | ||
function make_script_calls (list, api) | function make_script_calls (list, api) | ||
{ | { | ||
Line 2,603: | Line 2,604: | ||
); | ); | ||
} | } | ||
− | + | ||
if ((!window.XMLHttpRequest && !!window.ActiveXObject) || !self.haveAjax) { | if ((!window.XMLHttpRequest && !!window.ActiveXObject) || !self.haveAjax) { | ||
make_script_calls (get_local, wgServer + wgScriptPath + '/api.php'); | make_script_calls (get_local, wgServer + wgScriptPath + '/api.php'); | ||
Line 2,622: | Line 2,623: | ||
); | ); | ||
} | } | ||
− | + | ||
// Can't use Ajax for foreign repo, might violate single-origin policy (e.g. from wikisource.org | // 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 | // to wikimedia.org). Attention, here we must care about the URL length! IE has a limit of 2083 | ||
Line 2,629: | Line 2,630: | ||
make_script_calls (get_foreign, self.sharedRepositoryAPI ()); | make_script_calls (get_foreign, self.sharedRepositoryAPI ()); | ||
}, | }, | ||
− | + | ||
show_zoom : function () | show_zoom : function () | ||
{ | { | ||
Line 2,738: | Line 2,739: | ||
self.zoom.style.display = 'none'; // Will be shown in update | self.zoom.style.display = 'none'; // Will be shown in update | ||
}, | }, | ||
− | + | ||
update_zoom : function (evt) | update_zoom : function (evt) | ||
{ | { | ||
Line 2,775: | Line 2,776: | ||
self.zoom.style.left = x + 'px'; | self.zoom.style.left = x + 'px'; | ||
}, | }, | ||
− | + | ||
hide_zoom : function (evt) | hide_zoom : function (evt) | ||
{ | { | ||
Line 2,785: | Line 2,786: | ||
ImageAnnotator.zoom.style.display = 'none'; | ImageAnnotator.zoom.style.display = 'none'; | ||
}, | }, | ||
− | + | ||
createHelpLink : function () | createHelpLink : function () | ||
{ | { | ||
Line 2,804: | Line 2,805: | ||
else | else | ||
tgt = tgt.href; | tgt = tgt.href; | ||
− | + | ||
function make_handler (tgt) { | function make_handler (tgt) { | ||
var target = tgt; | var target = tgt; | ||
Line 2,814: | Line 2,815: | ||
}; | }; | ||
} | } | ||
− | + | ||
var imgs = msg.getElementsByTagName ('img'); | var imgs = msg.getElementsByTagName ('img'); | ||
− | + | ||
if (!imgs || imgs.length == 0) { | if (!imgs || imgs.length == 0) { | ||
// We're supposed to have a spans giving the button text | // We're supposed to have a spans giving the button text | ||
Line 2,833: | Line 2,834: | ||
} | } | ||
}, | }, | ||
− | + | ||
get_cover : function () | get_cover : function () | ||
{ | { | ||
Line 2,891: | Line 2,892: | ||
return self.cover; | return self.cover; | ||
}, | }, | ||
− | + | ||
show_cover : function () | show_cover : function () | ||
{ | { | ||
Line 2,905: | Line 2,906: | ||
} | } | ||
}, | }, | ||
− | + | ||
hide_cover : function () | hide_cover : function () | ||
{ | { | ||
Line 2,919: | Line 2,920: | ||
} | } | ||
}, | }, | ||
− | + | ||
getRawItem : function (what, scope) | getRawItem : function (what, scope) | ||
{ | { | ||
Line 2,931: | Line 2,932: | ||
return node; | return node; | ||
}, | }, | ||
− | + | ||
getItem : function (what, scope) | getItem : function (what, scope) | ||
{ | { | ||
Line 2,938: | Line 2,939: | ||
return LAPI.DOM.getInnerText (node).trim(); | return LAPI.DOM.getInnerText (node).trim(); | ||
}, | }, | ||
− | + | ||
getIntItem : function (what, scope) | getIntItem : function (what, scope) | ||
{ | { | ||
Line 2,945: | Line 2,946: | ||
return x; | return x; | ||
}, | }, | ||
− | + | ||
findNote : function (text, id) | findNote : function (text, id) | ||
{ | { | ||
Line 2,957: | Line 2,958: | ||
return {start: start_match, end: end_match + end.length}; | return {start: start_match, end: end_match + end.length}; | ||
} | } | ||
− | + | ||
var result = null; | var result = null; | ||
for (var i=0; i < ImageAnnotator.note_delim.length && !result; i++) { | for (var i=0; i < ImageAnnotator.note_delim.length && !result; i++) { | ||
Line 2,964: | Line 2,965: | ||
return result; | return result; | ||
}, | }, | ||
− | + | ||
setWikitext : function (pagetext) | setWikitext : function (pagetext) | ||
{ | { | ||
Line 2,991: | Line 2,992: | ||
self.wiki_read = true; | self.wiki_read = true; | ||
}, | }, | ||
− | + | ||
setSummary : function (summary, initial_text, note_text) | setSummary : function (summary, initial_text, note_text) | ||
{ | { | ||
Line 3,004: | Line 3,005: | ||
summary.value = initial_text; | summary.value = initial_text; | ||
}, | }, | ||
− | + | ||
getScript : function (url, bypass_local_cache, bypass_caches) | getScript : function (url, bypass_local_cache, bypass_caches) | ||
{ | { | ||
Line 3,021: | Line 3,022: | ||
} | } | ||
} | } | ||
− | + | ||
}; // end ImageAnnotator | }; // end ImageAnnotator | ||
− | + | ||
if (wgNamespaceNumber != -1 && wgAction && (wgAction == 'view' || wgAction == 'purge')) { | if (wgNamespaceNumber != -1 && wgAction && (wgAction == 'view' || wgAction == 'purge')) { | ||
// Start it. Bypass caches; but allow for 4 hours client-side caching. Small file. | // Start it. Bypass caches; but allow for 4 hours client-side caching. Small file. | ||
Line 3,032: | Line 3,033: | ||
); | ); | ||
} // end if we may run at all | } // end if we may run at all | ||
− | + | ||
} // end if (guard against double inclusions) | } // end if (guard against double inclusions) | ||
− | + | ||
// </source> | // </source> |
Latest revision as of 22:48, 16 December 2009
// <source lang="javascript"> /* THIS IS ImageAnnotator VERSION 2 Image annotations. Draw rectangles onto image thumbnail displayed on image description page and associate them with textual descriptions that will be displayed when the mouse moves over the rectangles. If an image has annotations, display the rectangles. Add a button to create new annotations. Note: if an image that has annotations is overwritten by a new version, only display the annotations if the size of the top image matches the stored size exactly. To recover annotations, one will need to edit the image description page manually, adjusting image sizes and rectangle coordinates, or re-enter annotations. Author: [[User:Lupo]], June-October 2009 License: Quadruple licensed GFDL, GPL, LGPL and Creative Commons Attribution 3.0 (CC-BY-3.0) Choose whichever license of these you like best :-) See http://commons.wikimedia.org/wiki/Help:Gadget-ImageAnnotator for documentation. */ // Global: getElementsByClassName, importScript, importScriptURI (wiki.js) // Global: wgPageName, wgCurRevisionId, wgUserGroups, wgRestrictionEdit (inline script on the page) // Global: wgAction, wgNamespaceNumber, wgUserLanguage, wgContentLanguage (inline script) if (typeof (ImageAnnotator) == 'undefined') { // Guard against multiple inclusions importScript ('MediaWiki:LAPI.js'); importScript ('MediaWiki:Tooltips.js'); importScript ('MediaWiki:TextCleaner.js'); importScript ('MediaWiki:UIElements.js'); var ImageAnnotator_DefaultLanguage = wgContentLanguage; var ImageAnnotation = function () {this.initialize.apply (this, arguments);} ImageAnnotation.compare = function (a, b) { var result = b.area () - a.area (); if (result != 0) return result; return a.model.id - b.model.id; // Just to make sure the order is complete }; ImageAnnotation.prototype = { view : null, // Rectangle to be displayed on image: a div with pos and size model : null, // Internal representation of the annotation tooltip : null, // Tooltip to display the annotation content : null, // Content of the tooltip viewer : null, // Reference to the viewer this note belongs to 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, ImageAnnotator.annotation_class)) { // Extract the info we need var x = ImageAnnotator.getIntItem ('view_x_' + id, viewer.scope); var y = ImageAnnotator.getIntItem ('view_y_' + id, viewer.scope); var w = ImageAnnotator.getIntItem ('view_w_' + id, viewer.scope); var h = ImageAnnotator.getIntItem ('view_h_' + id, viewer.scope); var html = ImageAnnotator.getRawItem ('content_' + id, viewer.scope); if (x === null || y === null || w === null || h === null || html === null) throw new Error ('Invalid note'); if (x < 0 || x >= viewer.full_img.width || y < 0 || y >= viewer.full_img.height) throw new Error ('Invalid note: origin invalid on note ' + id); if ( x + w > viewer.full_img.width + 10 || y + h > viewer.full_img.height + 10) { throw new Error ('Invalid note: size extends beyond image on note ' + id); } // Notes written by early versions may be slightly too large, whence the + 10 above. Fix this. if (x + w > viewer.full_img.width) w = viewer.full_img.width - x; if (y + h > viewer.full_img.height) h = viewer.full_img.height - y; view_w = Math.floor (w / viewer.factors.dx); view_h = Math.floor (h / viewer.factors.dy); view_x = Math.floor (x / viewer.factors.dx); view_y = Math.floor (y / viewer.factors.dy); this.view = LAPI.make ( 'div', null , { position : 'absolute' ,display : 'none' ,lineHeight : '0px' // IE ,fontSize : '0px' // IE ,top : "" + view_y + 'px' ,left : "" + view_x + 'px' ,width : "" + view_w + 'px' ,height : "" + view_h + 'px' } ); // We'll add the view to the DOM once we've loaded all notes this.model = { id : id ,dimension: {x: x, y: y, w: w, h: h} ,wiki : "" ,html : html.cloneNode (true) }; } else { is_new = true; this.view = node; this.model = { id : -1 ,dimension: null ,wiki : "" ,html : null }; view_w = this.view.offsetWidth - 2; // Subtract cumulated border widths view_h = this.view.offsetHeight - 2; view_x = this.view.offsetLeft; view_y = this.view.offsetTop; } // Enforce a minimum size of the view. Center the 6x6px square over the center of the old view. // If we overlap the image boundary, adjustRectangleSize will take care of it later. if (view_w < 6) {view_x = Math.floor (view_x + view_w / 2 - 3); view_w = 6; } if (view_h < 6) {view_y = Math.floor (view_y + view_h / 2 - 3); view_h = 6; } Object.merge ( { left: "" + view_x + 'px', top: "" + view_y + 'px' ,width: "" + view_w + 'px', height: "" + view_h + 'px'} , this.view.style ); this.view.style.zIndex = 500; // Below tooltips try { this.view.style.border = '1px solid ' + this.viewer.outer_border; } catch (ex) { this.view.style.border = '1px solid ' + ImageAnnotator.outer_border; } this.view.appendChild ( LAPI.make ( 'div', null , { lineHeight : '0px' // IE ,fontSize : '0px' // IE ,width : "" + Math.max (view_w - 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. ); try { this.view.firstChild.style.border = '1px solid ' + this.viewer.inner_border; } catch (ex) { this.view.firstChild.style.border = '1px solid ' + ImageAnnotator.inner_border; } if (is_new) viewer.adjustRectangleSize (this.view); // IE somehow passes through event to the view even if covered by our cover, displaying the tooltips // when drawing a new rectangle, which is confusing and produces a selection nightmare. Hence we just // display raw rectangles without any tooltips attached while drawing. Yuck. this.dummy = this.view.cloneNode (true); viewer.img_div.appendChild (this.dummy); if (!is_new) { // New notes get their tooltip only once the editor has saved, otherwise IE may try to // open them if the mouse moves onto the view even though there is the cover above them! this.setTooltip (); } }, setTooltip : function () { if (this.tooltip || !this.view) 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 : (LAPI.DOM.is_ie ? Tooltip.ESCAPE : Tooltip.LEAVE) ,close_button : null ,mode : Tooltip.MOUSE ,mouse_offset : {x: -5, y: -5, dx: (ImageAnnotator.is_rtl ? -1 : 1), dy: 1} ,open_delay : 0 ,hide_delay : 0 ,onclose : (function (tooltip, evt) { if (this.view) { try { this.view.style.border = '1px solid ' + this.viewer.outer_border; } catch (ex) { this.view.style.border = '1px solid ' + ImageAnnotator.outer_border; } } if (this.viewer.tip == tooltip) this.viewer.tip = null; // Hide all boxes if we're outside the image. Relies on hide checking the // coordinates! (Otherwise, we'd always hide...) if (evt) this.viewer.hide (evt); }).bind (this) ,onopen : (function (tooltip) { if (this.view) { try { this.view.style.border = '1px solid ' + this.viewer.active_border; } catch (ex) { this.view.style.border = '1px solid ' + ImageAnnotator.active_border; } } this.viewer.tip = tooltip; }).bind (this) } , ImageAnnotator.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) { if (!ImageAnnotator.ajaxQueried) { // Actually, this should be impossible, but just in case... ImageAnnotator.haveAjax = (LAPI.Ajax.getRequest () != null); ImageAnnotator.ajaxQueried = true; this.viewer.may_edit = ImageAnnotator.haveAjax; ImageAnnotator.may_edit = ImageAnnotator.haveAjax; } } if (this.viewer.may_edit) { this.content.button_section = LAPI.make ( 'div' ,null ,{ fontSize : 'smaller' ,textAlign: (ImageAnnotator.is_rtl ? 'left' : 'right') ,borderTop: ImageAnnotator.tooltip_styles.border } ); this.content.appendChild (this.content.button_section); this.content.button_section.appendChild (LAPI.DOM.makeLink ( '#' , ImageAnnotator.UI.get ('wpImageAnnotatorEdit', true) , null , LAPI.Evt.makeListener (this, this.edit) ) ); this.content.button_section.appendChild (document.createTextNode ('\xa0')); this.content.button_section.appendChild (LAPI.DOM.makeLink ( '#' , ImageAnnotator.UI.get ('wpImageAnnotatorDelete', true) , null , LAPI.Evt.makeListener (this, this.remove_event) ) ); } } return this.content; }, edit : function (evt) { ImageAnnotator.editor.editNote (this); if (evt) return LAPI.Evt.kill (evt); return false; }, remove_event : function (evt) { this.remove (); return LAPI.Evt.kill (evt); }, remove : function () { if (!this.content) { // New note: just destroy it. this.destroy (); return true; } // Close and remove tooltip only if edit succeeded! Where and how to display error messages? // Prompt for a removal reson var reason = window.prompt (ImageAnnotator.UI.get ('wpImageAnnotatorDeleteReason', true), ""); if (!reason || reason.trim().length == 0) 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 ( wgPageName , function (doc, editForm, failureFunc, revision_id) { try { if (revision_id && revision_id != wgCurRevisionId) throw new Error ('#Page version (revision ID) mismatch: edit conflict.'); var textbox = editForm.wpTextbox1; if (!textbox) throw new Error ('#Server replied with invalid edit page.'); var pagetext = textbox.value.replace(/\r\n/g, '\n'); // Normalize different end-of-line handling. Opera and IE may use \r\n, whereas other // browsers just use '\n'. Note that all browsers do the right thing if a '\n' is added. // We normally don't care, but here we need this to make sure we don't leave extra line // breaks when we remove the note. ImageAnnotator.setWikitext (pagetext); var span = ImageAnnotator.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.'); ImageAnnotator.setSummary ( summary , ImageAnnotator.UI.get ('wpImageAnnotatorRemoveSummary', true) || '[[MediaWiki talk:Gadget-ImageAnnotator.js|Removing image note]]$1' , 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 = request.responseText.match (/wgCurRevisionId\s*=\s*(\d+)[;,]/); if (revision_id) revision_id = parseInt (revision_id[1]); if (!revision_id) { failureFunc (request, new Error ('Revision ID not found. Please reload the page.')); return; } wgCurRevisionId = revision_id; // Bump revision id!! LAPI.Ajax.removeSpinner (spinnerId); if (self.tooltip) self.tooltip.size_change (); self.destroy (); } , function (request, ex) { if (edit_page.isFake && (typeof (edit_page.dispose) == 'function')) edit_page.dispose (); failureFunc (request, ex); } ); } , function (request, ex) { // Failure. What now? TODO: Implement some kind of user feedback. LAPI.Ajax.removeSpinner (spinnerId); if (self.tooltip) self.tooltip.size_change (); } ); return true; }, destroy : function () { if (this.view) LAPI.DOM.removeNode (this.view); if (this.dummy) LAPI.DOM.removeNode (this.dummy); if (this.tooltip) this.tooltip.hide_now (); if (this.model && this.model.id > 0 && this.viewer) this.viewer.deregister (this); this.model = null; this.view = null; this.content = null; this.tooltip = null; this.viewer = null; }, area : function () { if (!this.model || !this.model.dimension) return 0; return (this.model.dimension.w * this.model.dimension.h); } }; // end ImageAnnotation var ImageAnnotationEditor = function () {this.initialize.apply (this, arguments);}; if (typeof (ImageAnnotationEditor_columns) == 'undefined') var ImageAnnotationEditor_columns = 50; ImageAnnotationEditor.prototype = { initialize : function () { if ( !ImageAnnotationEditor_columns || isNaN (ImageAnnotationEditor_columns) || ImageAnnotationEditor_columns < 30 || ImageAnnotationEditor_columns > 100) { ImageAnnotationEditor_columns = 50; } this.editor = new LAPI.Edit ( "" , ImageAnnotationEditor_columns, 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.UI.get ('wpImageAnnotatorDelete', true).capitalizeFirst () ,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, '{{$1') ; // Guard against people trying to break notes on purpose if (text.length > 0 && 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, ImageAnnotator.tooltip_styles); temp.appendChild (LAPI.make ('textarea', {cols : ImageAnnotationEditor_columns, 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 ( ImageAnnotator.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) } , ImageAnnotator.tooltip_styles ); this.note = null; this.visible = false; LAPI.Evt.listenTo (this, this.tooltip.popup, ImageAnnotator.mouse_in, function (evt) { Array.forEach ( ImageAnnotator.viewers , (function (viewer) { if (viewer != this.viewer && viewer.visible) viewer.hide (); }).bind(this) ); } ); }, get_editor : function () { return this.box; }, editNote : function (note) { var same_note = (note == this.note); this.note = note; this.viewer = this.note.viewer; var cover = ImageAnnotator.get_cover (); cover.style.cursor = 'auto'; ImageAnnotator.show_cover (); if (note.tooltip) note.tooltip.hide_now (); ImageAnnotator.is_editing = true; if (note.content && !ImageAnnotator.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 : wgPageName ,rvlimit : 1 ,rvstartid : 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 (page in json_result.query.pages) { var p = json_result.query.pages[page]; if (p && p.revisions && p.revisions.length > 0) { var rev = p.revisions[0]; if (rev.revid == wgCurRevisionId && rev["*"] && rev["*"].length > 0) ImageAnnotator.setWikitext (rev["*"]); } break; } } // TODO: What upon a failure? self.open_editor (same_note, cover); } , function (request) { // TODO: What upon a failure? self.open_editor (same_note, cover); } ); } else { this.open_editor (same_note, cover); } }, open_editor : function (same_note, cover) { this.editor.hidePreview (); if (!same_note || this.editor.textarea.readOnly) // Different note, or save error last time this.editor.setText (this.note.model.wiki); this.editor.enable (LAPI.Edit.SAVE + LAPI.Edit.PREVIEW + LAPI.Edit.REVERT + LAPI.Edit.CANCEL); this.editor.textarea.readOnly = false; this.editor.textarea.style.backgroundColor = 'white'; // Set the position relative to the note's view. var view_pos = LAPI.Pos.position (this.note.view); var origin = LAPI.Pos.position (cover); this.tooltip.options.fixed_offset.x = view_pos.x - origin.x + this.tooltip.options.mouse_offset.x; this.tooltip.options.fixed_offset.y = view_pos.y - origin.y + this.tooltip.options.mouse_offset.y; this.tooltip.options.fixed_offset.dx = 1; this.tooltip.options.fixed_offset.dy = 1; this.tooltip.show_tip (null, false); var tpos = LAPI.Pos.position (this.editor.textarea); var ppos = LAPI.Pos.position (this.tooltip.popup); tpos = tpos.x - ppos.x; if (tpos + this.editor.textarea.offsetWidth > this.tooltip.popup.offsetWidth) this.editor.textarea.style.width = (this.tooltip.popup.offsetWidth - 2 * tpos) + 'px'; if (LAPI.Browser.is_ie) { // Fixate textarea width to prevent ugly flicker on each keypress in IE6... this.editor.textarea.style.width = this.editor.textarea.offsetWidth + 'px'; } this.visible = true; }, hide_editor : function (evt) { if (!this.visible) return; this.visible = false; ImageAnnotator.is_editing = false; this.tooltip.hide_now (evt); if (evt && evt.type == 'keydown' && !this.saving) { // ESC pressed on new note before a save attempt this.cancel (); } ImageAnnotator.hide_cover (); this.viewer.setDefaultMsg (); this.viewer.setShowHideEvents (true); this.viewer.show (); // Make sure we get the real views again. // FIXME in Version 2.1: Unfortunately, we don't have a mouse position here, so sometimes we // may show the not rectangles even though the mouse is now outside the image. (It was // somewhere inside the editor in most cases (if an editor button was clicked), but if ESC was // pressed, it may actually be anywhere.) }, save : function (editor) { var data = editor.getText (); if (!data || data.length == 0) { // Empty text if (this.note.remove ()) { this.hide_editor (); this.cancel (); this.note = null; } return; } else if (data == this.note.model.wiki) { this.hide_editor (); // Text unchanged this.cancel (); return; } // Construct what to insert var dim = Object.clone (this.note.model.dimension); if (!dim) { dim = { x : Math.round (this.note.view.offsetLeft * this.viewer.factors.dx) ,y : Math.round (this.note.view.offsetTop * this.viewer.factors.dy) ,w : Math.round (this.note.view.offsetWidth * this.viewer.factors.dx) ,h : Math.round (this.note.view.offsetHeight * this.viewer.factors.dy) }; // Make sure everything is within bounds if (dim.x + dim.w > this.viewer.full_img.width) { if (dim.w > this.note.view.offsetWidth * 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.note.view.offsetHeight * this.viewer.factors.dy) { dim.h--; if (dim.y + dim.h > this.viewer.full_img.height) { if (dim.y > 0) dim.y--; else dim.h = this.viewer.full_img.height; } } else { // Height already was rounded down if (dim.y > 0) dim.y--; } } // If still too large, adjust width and height if (dim.x + dim.w > this.viewer.full_img.width) { if (this.viewer.full_img.width > dim.x) { dim.w = this.viewer.full_img.width - dim.x; } else { dim.x = this.viewer.full_img.width - 1; dim.w = 1; } } if (dim.y + dim.h > this.viewer.full_img.height) { if (this.viewer.full_img.height > dim.y) { dim.h = this.viewer.full_img.height - dim.y; } else { dim.y = this.viewer.full_img.height - 1; dim.h = 1; } } } this.to_insert = '{{ImageNote' + '|id=' + this.note.model.id + '|x=' + dim.x + '|y=' + dim.y + '|w=' + dim.w + '|h=' + dim.h + '|dimx=' + this.viewer.full_img.width + '|dimy=' + this.viewer.full_img.height + '|style=2' + '}}\n' + data + (data.endsWith ('\n') ? "" : '\n') + '{{ImageNoteEnd|id=' + this.note.model.id + '}}'; // Now edit the page var self = this; this.editor.busy (true); this.editor.enable (0); // Disable all buttons this.saving = true; LAPI.Ajax.editPage ( wgPageName , function (doc, editForm, failureFunc, revision_id) { try { if (revision_id && revision_id != wgCurRevisionId) // Page was edited since the user loaded it. throw new Error ('#Page version (revision ID) mismatch: edit conflict.'); // Modify the page var textbox = editForm.wpTextbox1; if (!textbox) throw new Error ('#Server replied with invalid edit page.'); var pagetext = textbox.value; ImageAnnotator.setWikitext (pagetext); var span = null; if (self.note.content) // Otherwise it's a new note! span = ImageAnnotator.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) { ImageAnnotator.setSummary ( summary , ImageAnnotator.UI.get ('wpImageAnnotatorChangeSummary', true) || '[[MediaWiki talk:Gadget-ImageAnnotator.js|Changing image note]]$1' , data ); } else { ImageAnnotator.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 = request.responseText.match (/wgCurRevisionId\s*=\s*(\d+)[;,]/); if (revision_id) revision_id = parseInt (revision_id[1]); if (!revision_id) { if (doc.isFake && (typeof (doc.dispose) == 'function')) doc.dispose (); failureFunc (request, new Error ('#Version inconsistency after saving. Please reload the page.')); return; } wgCurRevisionId = revision_id; // Bump revision id!! self.note.model.html = LAPI.DOM.importNode (document, html, true); if (doc.isFake && (typeof (doc.dispose) == 'function')) doc.dispose (); self.note.model.dimension = dim; // record dimension self.note.model.html.style.display = ""; self.note.model.wiki = data; self.editor.busy (false); if (self.note.content) { LAPI.DOM.removeChildren (self.note.content.main); self.note.content.main.appendChild (self.note.model.html); } else { // New note. self.note.display (); // Actually a misnomer. Just creates 'content'. if (self.viewer.annotations.length > 1) { self.viewer.annotations.sort (ImageAnnotation.compare); var idxOfNote = Array.indexOf (self.viewer.annotations, self.note); if (idxOfNote+1 < self.viewer.annotations.length) LAPI.DOM.insertNode (self.note.view, self.viewer.annotations [idxOfNote+1].view); } } self.to_insert = null; self.saving = false; if (!self.note.tooltip) self.note.setTooltip (); self.hide_editor (); ImageAnnotator.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 > 0 && lk[0].firstChild.nodeName.toLowerCase () == 'a') { lk = lk[0].firstChild; lk.href = wgServer + wgArticlePath.replace ('$1', encodeURI (wgPageName)) + '?action=edit'; } if (ex) { var ex_msg = LAPI.formatException (ex, true); if (ex_msg) { ex_msg.style.borderBottom = '1px solid red'; var tmp = LAPI.make ('div'); tmp.appendChild (ex_msg); tmp.appendChild (error_msg); error_msg = tmp; } } self.editor.setPreview (error_msg); self.editor.showPreview (); self.editor.textarea.readOnly = true; // Force a light gray background, since IE has no visual readonly indication. self.editor.textarea.style.backgroundColor = '#EEEEEE'; self.editor.enable (LAPI.Edit.CANCEL); // Disable all other buttons } ); }, onpreview : function (editor) { if (this.tooltip) this.tooltip.size_change (); }, cancel : function (editor) { if (!this.note) return; if (!this.note.content) { // No content: Cancel and remove this note! this.note.destroy (); this.note = null; } if (editor) this.hide_editor (); }, close_tooltip : function (tooltip, evt) { this.hide_editor (evt); this.cancel (); } }; var ImageNotesViewer = function () {this.initialize.apply (this, arguments); }; ImageNotesViewer.prototype = { initialize : function (descriptor, may_edit) { Object.merge (descriptor, this); this.annotations = []; this.max_id = 0; this.main_div = null; this.msg = null; this.may_edit = may_edit; this.setup_done = false; this.tip = null; this.icon = null; this.factors = { dx : this.full_img.width / this.thumb.width ,dy : this.full_img.height / this.thumb.height }; if (!this.isThumbnail && !this.isOther) { this.setup (); } else { // Normalize the namespace of the realName to 'File' to account for images possibly stored at // a foreign repository (the Commons). Otherwise a later information load might fail because // the link is local and might actually be given as "Bild:Foo.jpg". If that page doesn't exist // locally, we want to ask at the Commons about "File:Foo.jpg". The Commons doesn't understand // the localized namespace names of other wikis, but the canonical namespace name 'File' works // also locally. this.realName = 'File:' + this.realName.substring (this.realName.indexOf (':') + 1); } }, setup : function (onlyIcon) { this.setup_done = true; var name = this.realName; if (this.isThumbnail || this.scope == document || this.may_edit || !ImageAnnotator.haveAjax) this.realName = ""; else { var name = getElementsByClassName (this.scope, '*', 'wpImageAnnotatorFullName'); this.realName = ((name && name.length > 0) ? LAPI.DOM.getInnerText (name[0]) : ""); } var annotations = getElementsByClassName (this.scope, 'div', ImageAnnotator.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 (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 : (ImageAnnotator.is_rtl ? 'right' : 'left') ,styleFloat: (ImageAnnotator.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: (ImageAnnotator.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); } 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 || ImageAnnotator.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' ) { 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 ( ImageAnnotator.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 () {window.location = this.img.parentNode.href;}).bind (this) if (ImageAnnotator.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 = ImageAnnotator.getRawItem ('colors', this.scope); this.outer_border = colors && ImageAnnotator.getItem ('outer', colors) || ImageAnnotator.outer_border; this.inner_border = colors && ImageAnnotator.getItem ('inner', colors) || ImageAnnotator.inner_border; this.active_border = colors && ImageAnnotator.getItem ('active', colors) || ImageAnnotator.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 = ImageAnnotator.getIntItem ('full_width_' + id, this.scope); var h = ImageAnnotator.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 (ImageAnnotator.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); } } this.msg = LAPI.make ('div', null, {display: 'none'}); if (ImageAnnotator.is_rtl) { this.msg.style.direction = 'rtl'; this.msg.className = 'rtl'; } if (this.isThumbnail) this.msg.style.fontSize = '90%'; this.main_div.appendChild (this.msg); // Set overflow parents, if any var simple = !!window.getComputedStyle; var checks = (simple ? ['overflow', 'overflow-x', 'overflow-y'] : ['overflow', 'overflowX', 'overflowY'] ); var curStyle = null; for (var up = this.img.parentNode.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; ImageAnnotator.may_edit = false; } this.show_evt = LAPI.Evt.makeListener (this, this.show); if (this.overflowParents || LAPI.Browser.is_ie) { // If we have overflowParents, also use a mousemove listener to show/hide the whole // view (FF doesn't send mouseout events if the visible border is still within the image, i.e., // if not the whole image is visible). On IE, also use this handler to show/hide the notes // if we're still within the visible area of the image. IE passes through mouse_over events to // the img even if the mouse is within a note's rectangle. Apparently is doesn't handle // transparent divs correctly. As a result, the notes will pop up and disappear only when the // mouse crosses the border, and if one moves the mouse a little fast across the border, we // don't get any event at all. That's no good. this.move_evt = LAPI.Evt.makeListener (this, this.check_hide); } else this.hide_evt = LAPI.Evt.makeListener (this, this.hide); this.move_listening = false; this.setShowHideEvents (true); this.visible = false; this.setDefaultMsg (); }, setShowHideEvents : function (set) { if (this.icon) return; if (set) { LAPI.Evt.attach (this.img, ImageAnnotator.mouse_in, this.show_evt); if (this.hide_evt) LAPI.Evt.attach (this.img, ImageAnnotator.mouse_out, this.hide_evt); } else { LAPI.Evt.remove(this.img, ImageAnnotator.mouse_in, this.show_evt); if (this.hide_evt) { LAPI.Evt.remove (this.img, ImageAnnotator.mouse_out, this.hide_evt); } else if (this.move_listening) this.removeMoveListener (); } }, removeMoveListener : function () { if (this.icon) return; this.move_listening = false; if (this.move_evt) { if (!LAPI.Browser.is_ie && typeof (document.captureEvents) == 'function') document.captureEvents (null); LAPI.Evt.remove (document, 'mousemove', this.move_evt, true); } }, adjustRectangleSize : function (node) { if (this.icon) return; // Make sure the note boxes don't overlap the image boundary; we might get an event // loop otherwise if the mouse was just on that overlapped boundary, resulting in flickering. var view_x = node.offsetLeft; var view_y = node.offsetTop; var view_w = node.offsetWidth; var view_h = node.offsetHeight; if (view_x == 0) view_x = 1; if (view_y == 0) view_y = 1; if (view_x + view_w >= this.thumb.width) { view_w = this.thumb.width - view_x - 1; if (view_w <= 4) { view_w = 4; view_x = this.thumb.width - view_w - 1;} } if (view_y + view_h >= this.thumb.height) { view_h = this.thumb.height - view_y - 1; if (view_h <= 4) { view_h = 4; view_y = this.thumb.height - view_h - 1;} } // Now set position and width and height, subtracting cumulated border widths if ( view_x != node.offsetLeft || view_y != node.offsetTop || view_w != node.offsetWidth || view_h != node.offsetHeight) { node.style.top = "" + 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 : function (dummies) { if (!this.annotations || this.annotations.length == 0 || this.icon) return; if (dummies) { for (var i = 0; i < this.annotations.length; i++) { this.annotations[i].view.style.display = 'none'; if (this.visible && this.annotations[i].tooltip) this.annotations[i].tooltip.hide_now (null); this.annotations[i].dummy.style.display = (this.visible ? 'none' : ""); if (!this.visible) this.adjustRectangleSize (this.annotations[i].dummy); } } else { for (var i = 0; i < this.annotations.length; i++) { this.annotations[i].dummy.style.display = 'none'; this.annotations[i].view.style.display = (this.visible ? 'none' : ""); if (!this.visible) this.adjustRectangleSize (this.annotations[i].view); if (this.visible && this.annotations[i].tooltip) this.annotations[i].tooltip.hide_now (null); } } this.visible = !this.visible; }, show : function (evt) { if (this.visible || this.icon) return; this.toggle (ImageAnnotator.is_adding || ImageAnnotator.is_editing); if (this.move_evt && !this.move_listening) { LAPI.Evt.attach (document, 'mousemove', this.move_evt, true); this.move_listening = true; if (!LAPI.Browser.is_ie && typeof (document.captureEvents) == 'function') document.captureEvents (Event.MOUSEMOVE); } }, hide : function (evt) { if (this.icon) return true; if (!this.visible) { // Huh? if (this.move_listening) this.removeMoveListener (); return true; } if (evt) { var mouse_pos = LAPI.Pos.mousePosition (evt); if (mouse_pos) { if (this.tip) { // Check whether we're within the visible note. if (LAPI.Pos.isWithin (this.tip.popup, mouse_pos.x, mouse_pos.y)) return true; } var is_within = true; var img_pos = LAPI.Pos.position (this.img); var rect = { x: img_pos.x, y: img_pos.y ,r: (img_pos.x + this.img.offsetWidth), b: (img_pos.y + this.img.offsetHeight) }; if (this.overflowParents) { // We're within some elements having overflow:hidden or overflow:auto or overflow:scroll set. // Compute the actually visible region by intersecting the rectangle given by img_pos and // this.img.offsetWidth, this.img.offsetTop with the rectangles of all overflow parents. function intersect_rectangles (a, b) { if (b.x > a.r || b.r < a.x || b.y > a.b || b.b < a.y) return {x:0, y:0, r:0, b:0}; return { x: Math.max (a.x, b.x), y: Math.max (a.y, b.y) ,r: Math.min (a.r, b.r), b: Math.min (a.b, b.b) }; } for (var i = 0; i < this.overflowParents.length && rect.x < rect.r && rect.y < rect.b; i++) { img_pos = LAPI.Pos.position (this.overflowParents[i]); img_pos.r = img_pos.x + this.overflowParents[i].clientWidth; img_pos.b = img_pos.y + this.overflowParents[i].clientHeight; rect = intersect_rectangles (rect, img_pos); } } is_within = !( rect.x >= rect.r || rect.y >= rect.b // Empty rectangle || rect.x >= mouse_pos.x || rect.r <= mouse_pos.x || rect.y >= mouse_pos.y || rect.b <= mouse_pos.y ); if (is_within) { if (LAPI.Browser.is_ie && evt.type == 'mousemove') { var display; // Loop in reverse order to properly display top rectangle's note! for (var 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 (ImageAnnotator.is_adding || ImageAnnotator.is_editing); return true; }, check_hide : function (evt) { if (this.icon) return true; if (this.visible) this.hide (evt); return true; }, register : function (new_note) { this.annotations[this.annotations.length] = new_note; if (new_note.model.id > 0) { if (new_note.model.id > this.max_id) this.max_id = new_note.model.id; } else { new_note.model.id = ++this.max_id; } }, deregister : function (note) { Array.remove (this.annotations, note); if (note.model.id == this.max_id) this.max_id--; if (this.annotations.length == 0) this.setDefaultMsg (); //If we removed the last one, clear the msg }, setDefaultMsg : function () { if (this.annotations && this.annotations.length > 0 && 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 > 0) { var otherPageMsg = ImageAnnotator.UI.get ('wpImageAnnotatorEditNotesMsg', false); if (otherPageMsg) { var lk = otherPageMsg.getElementsByTagName ('a'); if (lk && lk.length > 0) { lk = lk[0]; lk.parentNode.replaceChild ( LAPI.DOM.makeLink ( wgArticlePath.replace ('$1', encodeURI (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 (ImageAnnotator.button_div && this.may_edit) ImageAnnotator.button_div.style.display = ""; } }; // User configurations if (typeof (ImageAnnotator_zoom_threshold) == 'undefined') var ImageAnnotator_zoom_threshold = 8.0; var ImageAnnotator = { // This object is responsible for setting up annotations when a page is loaded. It loads all // annotations in the page source, and adds an "Annotate this image" button plus the support // for drawing rectangles onto the image if there is only one image and editing is allowed. haveAjax : false, button_div : null, add_button : null, cover : null, border : null, definer : null, mouse_in : (!!window.ActiveXObject ? 'mouseenter' : 'mouseover'), mouse_out : (!!window.ActiveXObject ? 'mouseleave' : 'mouseout'), annotation_class : 'image_annotation', // Format of notes in Wikitext. Note: there are two formats, an old one and a new one. // We only write the newest (last) one, but we can read also the older formats. Order is // important, because the old format also used the ImageNote template, but for a different // purpose. note_delim : [ { start : '<div id="image_annotation_note_$1"' ,end : '</div><!-- End of annotation $1-->' ,content_start : '<div id="image_annotation_content_$1">\n' ,content_end : '</div>\n<span id="image_annotation_wikitext_$1"' } ,{ start : '{{ImageNote|id=$1' ,end : '{{ImageNoteEnd|id=$1}}' ,content_start : '}}\n' ,content_end : '{{ImageNoteEnd|id=$1}}' } ], tooltip_styles : // The style for all our tooltips { border : '1px solid #8888aa' , backgroundColor : '#ffffe0' , padding : '0.3em' , fontSize : ((skin && (skin == 'monobook' || skin == 'modern')) ? '127%' : '100%') // Scale up to default text size }, editor : null, wiki_read : false, is_rtl : false, move_listening : false, is_tracking : false, is_adding : false, is_editing : false, zoom_threshold : 8.0, zoom_factor : 4.0, install_attempts : 0, max_install_attempts : 10, // Maximum 5 seconds imgs_with_notes : [], thumbs : [], other_images : [], // Fallback indication_icon : 'http://upload.wikimedia.org/wikipedia/commons/8/8a/Gtk-dialog-info-14px.png', config : null, install : function (config) { if (typeof (ImageAnnotator_disable) != 'undefined' && !!ImageAnnotator_disable) return; if (!config || ImageAnnotator.config) return; // Double check. if (!( wgNamespaceNumber >= 0 && config.viewingEnabled () && wgAction && (wgAction == 'view' || wgAction == 'purge') && document.URL.search (/[?&]diff=/) < 0 ) ) { return; } var self = ImageAnnotator; self.config = config; // Determine whether we have XmlHttp. We try to determine this here to be able to avoid // doing too much work. if ( window.XMLHttpRequest && typeof (LAPI) != 'undefined' && typeof (LAPI.Ajax) != 'undefined' && typeof (LAPI.Ajax.getRequest) != 'undefined' ) { self.haveAjax = (LAPI.Ajax.getRequest () != null); self.ajaxQueried = true; } else { self.haveAjax = true; // A pity. May occur on IE. We'll check again later on. self.ajaxQueried = false; } // We'll include self.haveAjax later on once more, to catch the !ajaxQueried case. self.may_edit = self.haveAjax && config.editingEnabled (); function namespaceCheck (list) { if (!list || Object.prototype.toString.call (list) !== '[object Array]') return false; for (var i = 0; i < list.length; i++) { if (wgNamespaceIds && typeof (list[i]) == 'string' && wgNamespaceIds[list[i].toLowerCase().replace(/ /g, '_')] == wgNamespaceNumber ) return true; } return false; } self.rules = {inline: {}, thumbs: {}, shared : {}}; // Now set the default rules. Undefined means default setting (true for show, false for icon), // but overrideable by per-image rules. If set, it's not overrideable by per-image rules. // if ( !self.haveAjax || !config.generalImagesEnabled () || namespaceCheck (window.ImageAnnotator_no_images || null) ) { self.rules.inline.show = false; self.rules.thumbs.show = false; self.rules.shared.show = false; } else { if ( !self.haveAjax || !config.thumbsEnabled () || namespaceCheck (window.ImageAnnotator_no_thumbs || null) ) { self.rules.thumbs.show = false; self.rules.thumbs.show = false; } if (wgNamespaceNumber == 6) self.rules.shared.show = true; else if ( !config.sharedImagesEnabled () || namespaceCheck (window.ImageAnnotator_no_shared || null) ) { self.rules.shared.show = false; } if (namespaceCheck (window.ImageAnnotator_icon_images || null)) self.rules.inline.icon = true; if (namespaceCheck (window.ImageAnnotator_icon_thumbs || null)) self.rules.thumbs.icon = true; } var do_images = typeof (self.rules.inline.show) == 'undefined' || self.rules.inline.show; if (do_images) { // Per-article switching off of note display on inline images and thumbnails var rules = document.getElementById ('wpImageAnnotatorImageRules'); if (rules) { if (rules.className.indexOf ('wpImageAnnotatorNone') >= 0) { self.rules.inline.show = false; self.rules.thumbs.show = false; self.rules.shared.show = false; } if ( typeof (self.rules.inline.show) == 'undefined' && rules.className.indexOf ('wpImageAnnotatorDisplay') >= 0 ) { self.rules.inline.show = true; } if (rules.className.indexOf ('wpImageAnnotatorNoThumbDisplay') >= 0) { self.rules.thumbs.show = false; } if ( typeof (self.rules.thumbs.show) == 'undefined' && rules.className.indexOf ('wpImageAnnotatorThumbDisplay') >= 0 ) { self.rules.thumbs.show = true; } if (rules.className.indexOf ('wpImageAnnotatorInlineDisplayIcons') >= 0) { self.rules.inline.icon = true; } if (rules.className.indexOf ('wpImageAnnotatorThumbDisplayIcons') >= 0) { self.rules.thumbs.icon = true; } if (rules.className.indexOf ('wpImageAnnotatorOnlyLocal') >= 0) { self.rules.shared.show = false; } } } // Make sure the shared value is set self.rules.shared.show = typeof (self.rules.shared.show) == 'undefined' || self.rules.shared.show; do_images = typeof (self.rules.inline.show) == 'undefined' || self.rules.inline.show; var do_thumbs = do_images && (typeof (self.rules.thumbs.show) == 'undefined' || self.rules.thumbs.show); if (do_images) { var bodyContent = document.getElementById ('bodyContent') // monobook, vector || document.getElementById ('mw_contentholder') // modern || document.getElementById ('article') // old skins ; if (bodyContent) { var all_imgs = bodyContent.getElementsByTagName ('img'); 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 (do_thumbs && (' ' + up.className + ' ').indexOf (' thumbinner ') >= 0) { self.thumbs[self.thumbs.length] = up; continue; } up = up.parentNode; if (!up) 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 (' gallerybox ') < 0) { if ((' ' + up.className + ' ').indexOf (' wpImageAnnotatorEnable ') >= 0) { self.imgs_with_notes[self.imgs_with_notes.length] = up; } else { up = up.parentNode; if (up && (' ' + up.className + ' ').indexOf (' wpImageAnnotatorEnable ') >= 0) { self.imgs_with_notes[self.imgs_with_notes.length] = up; } else { self.other_images[self.other_images.length] = all_imgs[i]; } } } } // end loop } } else { self.imgs_with_notes = getElementsByClassName (document, '*', 'wpImageAnnotatorEnable'); if (do_thumbs) self.thumbs = getElementsByClassName (document, 'div', 'thumbinner'); // No galleries! } if ( wgNamespaceNumber == 6 || (self.imgs_with_notes.length > 0) || (self.thumbs.length > 0) || (self.other_images.length > 0) ) { // Publish parts of config. self.UI = config.UI; self.outer_border = config.outer_border; self.inner_border = config.inner_border; self.active_border = config.active_border; self.new_border = config.new_border; if (self.thumbs.length > 0 || self.other_images.length > 0) self.inlineImageUsesIndicator = function () {var t = self; return config.inlineImageUsesIndicator.apply (t, arguments); }; self.sharedRepositoryAPI = function () {var t = self; return config.sharedRepositoryAPI.apply (t, arguments); }; self.imageIsFromSharedRepository = function () {var t = self; return config.imageIsFromSharedRepository.apply (t, arguments); }; self.wait_for_required_libraries (); } }, wait_for_required_libraries : function () { if (typeof (Tooltip) == 'undefined' || typeof (LAPI) == 'undefined') { if (ImageAnnotator.install_attempts++ < ImageAnnotator.max_install_attempts) { window.setTimeout (ImageAnnotator.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. ImageAnnotator.setup_ui (); ImageAnnotator.setup(); }, setup: function () { var self = ImageAnnotator; self.imgs = []; function img_check (img, is_other) { var w = img.clientWidth; // Don't use offsetWidth, thumbnails may have a boundary... var h = img.clientHeight; // Don't do anything on extremely small previews. We need some minimum extent to be able to place // rectangles after all... if (w < 20 || h < 20) return null; // For non-thumbnail images, the size limit is larger. if (is_other && (w < 60 || h < 60)) return null; // Quit if the image wasn't loaded properly for some reason: if ( w != parseInt (img.getAttribute ('width'), 10) || h != parseInt (img.getAttribute ('height'), 10)) return null; // Exclude system images if (img.src.contains ('/skin')) return null; // Only if within a link if (img.parentNode.nodeName.toLowerCase () != 'a') return null; if (is_other) { // Only if the img-within-link construction is within some element that may contain a div! if (img.parentNode.parentNode.nodeName.toLowerCase () == 'p') { // Special case: a paragraph may contain only inline elements, but we want to be able to handle // files in single paragraphs. Maybe we need to properly split the paragraph and wrap the image // in a div, but for now we assume that all browsers can handle a div within a paragraph in a // meaningful way, even if that is not really allowed. } else if (!/^(object|applet|map|fieldset|noscript|iframe|body|div|li|dd|blockquote|center|ins|del|button|th|td|form)$/i.test (img.parentNode.parentNode.nodeName)) return null; } // Exclude any that are within an image note! var up = img.parentNode.parentNode; while (up != document.body) { if (LAPI.DOM.hasClass (up, ImageAnnotator.annotation_class)) return null; up = up.parentNode; } return img; } 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 > 0 && scope == self.imgs[0]) return null; if (scope == document) { file_div = LAPI.$ ('file'); } else if (!is_thumb && !is_other) { file_div = getElementsByClassName (scope, 'div', 'wpImageAnnotatorFile'); if (!file_div || file_div.length != 1) return null; file_div = file_div[0]; } if (!file_div) return null; var img = null; if (scope == document) { img = LAPI.WP.getPreviewImage (wgTitle); } else if (is_other) { img = scope; } else { img = file_div.getElementsByTagName ('img'); if (!img || img.length == 0) return null; img = img[0]; } if (!img) return null; img = img_check (img, is_other); if (!img) return null; // Conditionally exclude shared images. if ( scope != document && !self.rules.shared.show && self.imageIsFromSharedRepository (img.src) ) return null; var name = null; if (scope == document) { name = wgPageName; } else { name = LAPI.WP.pageFromLink (img.parentNode); if (!name) return null; name = name.replace (/ /g, '_'); if (is_thumb || is_other) { var img_src = img.getAttribute ('src', 2); img_src = decodeURIComponent (img_src.substring (img_src.lastIndexOf ('/') + 1)) .replace (/ /g, '_') .replace (/(\.svg)\.png$/i, '$1') ; var colon = name.indexOf (':'); if (colon <= 0) return null; var img_name = name.substring (colon + 1); if ( img_name != img_src && !(img_src.endsWith (img_name) && /^\d+px-$/.test (img_src.substring (0, img_src.length - img_name.length)) ) ) 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)$/i) < 0) return null; // Only PNG, JPE?G, GIF, SVG if (is_other) { // Insert a file_div, and set the scope to that div file_div = LAPI.make ('div'); scope = file_div; img.parentNode.parentNode.insertBefore (file_div, img.parentNode); file_div.appendChild (img.parentNode); } return { scope : scope ,file_div : file_div ,img : img ,realName : name ,isThumbnail: is_thumb ,isOther : is_other ,thumb : {width: img.clientWidth, height: img.clientHeight} }; } function setup_images (list) { Array.forEach (list, function (elem) { var desc = setup_one (elem); if (desc) self.imgs[self.imgs.length] = desc; } ); } if (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 && document.URL.search (/[?&]oldid=/) < 0; if (self.haveAjax) { setup_images (self.thumbs); setup_images (self.other_images); } 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 (self.info_callbacks) != 'undefined') self.info_callbacks = null; self.setup_step_two (); } } function make_calls (execute_call, url_limit) { function build_titles (from, length, url_limit) { var done = 0; var text = ""; for (var i = from; i < from + length; i++) { var new_text = names[i]; if (url_limit) { new_text = encodeURIComponent (new_text); if (text.length > 0 && (text.length + new_text.length + 1 > url_limit)) break; } text += (text.length > 0 ? '|' : "") + new_text; done++; } return {text: text, n: done}; } var start = 0, chunk = 0, params; while (to_do > 0) { params = build_titles (start, Math.min (50, to_do), url_limit); execute_call (params.n, params.text); to_do -= params.n; start += params.n; } } function set_info (json) { try { if (json && json.query && json.query.pages) { function get_size (info) { if (!info.imageinfo || info.imageinfo.length == 0) return; var title = info.title.replace (/ /g, '_'); var indices = cache[title]; if (!indices) return; Array.forEach ( indices , function (i) { self.imgs[i].full_img = { width : info.imageinfo[0].width ,height: info.imageinfo[0].height}; self.imgs[i].has_page = (typeof (info.missing) == 'undefined'); self.imgs[i].isLocal = !info.imagerepository || info.imagerepository == 'local'; if (i != 0 || !self.may_edit || !info.protection) return; // Care about the protection settings var protection = Array.any (info.protection, function (e) { return (e.type == 'edit' ? e : null); }); self.may_edit = !protection || (wgUserGroups && wgUserGroups.join (' ').contains (protection.level)) ; } ); } for (var page in json.query.pages) { get_size (json.query.pages[page]); } } // end if } catch (ex) { } } 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. self.info_callbacks = []; make_calls ( function (length, titles) { var idx = self.info_callbacks.length; self.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 }; self.info_callbacks[idx].script = ImageAnnotator.getScript ( wgServer + wgScriptPath + '/api.php?action=query&format=json' + '&prop=info|imageinfo&inprop=protection&iiprop=size' + '&titles=' + titles + '&callback=ImageAnnotator.info_callbacks[' + idx + '].callback' , true // No local caching! ); // We do bypass the local JavaScript cache of importScriptURI, but on IE, we still may // get the script from the browser's cache, and if that happens, IE may execute the // script (and call the callback) synchronously before the assignment is done. Clean // up in that case. if ( self.info_callbacks && self.info_callbacks[idx] && self.info_callbacks[idx].done && self.info_callbacks[idx].script) { LAPI.DOM.removeNode (self.info_callbacks[idx].script); self.info_callbacks[idx].script = null; } } , LAPI.Browser.is_ie ? 1900 : 8000 ); } 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, wgUserLanguage, null); add_plea = !result; if (!result) result = UIElements.getEntry (id, self.repo); } self.needs_plea = add_plea; if (!result) return null; // Hmmm... what happened here? We normally have defaults... if (basic) return LAPI.DOM.getInnerText (result).trim (); result = result.cloneNode (true); if (wgServer.contains ('/commons') && add_plea && !no_plea) { // Add a translation plea. if (result.nodeName.toLowerCase () == 'div') { result.appendChild (self.get_plea ()); } else { var span = LAPI.make ('span'); span.appendChild (result); span.appendChild (self.get_plea ()); result = span; } } return result; }; ImageAnnotator.UI.get_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 ( wgServer + wgScript + '?title=MediaWiki_talk:ImageAnnotatorTexts' + '&action=edit§ion=new&withJS=MediaWiki:ImageAnnotatorTranslator.js' + '&language=' + wgUserLanguage , translate , (typeof (translate) == 'string' ? translate : LAPI.DOM.getInnerText (translate).trim ()) ) ); span.appendChild (document.createTextNode (')')); return span; }; ImageAnnotator.UI.init = function (html_text_or_json) { var text; if (typeof (html_text_or_json) == 'string') text = html_text_or_json; else if ( typeof (html_text_or_json) != 'undefined' && typeof (html_text_or_json.parse) != 'undefined' && typeof (html_text_or_json.parse.text) != 'undefined' && typeof (html_text_or_json.parse.text['*']) != 'undefined' ) text = html_text_or_json.parse.text['*']; else text = null; if (!text) { ImageAnnotator.UI.fireReadyEvent (); return; } var node = LAPI.make ('div', null, {display: 'none'}); document.body.appendChild (node); try { node.innerHTML = text; } catch (ex) { LAPI.DOM.removeNode (node); node = null; // Swallow. We'll just work with the default UI } if (node && !ImageAnnotator.UI.repo) ImageAnnotator.UI.setup (); ImageAnnotator.UI.fireReadyEvent (); }; var ui_page = '{{MediaWiki:ImageAnnotatorTexts' + (wgUserLanguage != wgContentLanguage ? '|lang=' + wgUserLanguage : "") + '|live=1}}'; function get_ui_no_ajax () { var url = wgServer + wgScriptPath + '/api.php?action=parse&pst&text=' + encodeURIComponent (ui_page) + '&title=API&prop=text&format=json' + '&callback=ImageAnnotator.UI.init&maxage=14400&smaxage=14400' ; // Result cached for 4 hours. How to properly handle an error? It appears there's no way to catch // that on IE. (On FF, we could use an onerror handler on the script tag, but on FF, we use Ajax // anyway.) ImageAnnotator.getScript (url, true); // No local caching! } function get_ui () { ImageAnnotator.haveAjax = (LAPI.Ajax.getRequest () != null); ImageAnnotator.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 (!ImageAnnotator.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 = ImageAnnotator; // Throw out any images for which we miss either the thumbnail or the full image size. // Also throws out thumbnails that are larger than the full image. self.imgs = Array.select ( self.imgs , function (elem, idx) { var result = elem.thumb.width > 0 && elem.thumb.height > 0 && typeof (elem.full_img) != 'undefined' && elem.full_img.width > 0 && elem.full_img.height > 0 && elem.full_img.width >= elem.thumb.width && elem.full_img.height >= elem.thumb.height ; if (self.may_edit && idx == 0 && !result) self.may_edit = false; return result; } ); if (self.imgs.length == 0) return; // Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]] self.is_rtl = LAPI.DOM.hasClass (document.body, 'rtl') || ( LAPI.DOM.currentStyle // Paranoia: added recently, not everyone might have it && LAPI.DOM.currentStyle (document.body, 'direction') == 'rtl' ) ; self.UI.addReadyEventHandler (ImageAnnotator.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 = ImageAnnotator; // Check edit permissions if (self.may_edit) { self.may_edit = ( (wgRestrictionEdit.length == 0 || wgUserGroups && wgUserGroups.join (' ').contains ('sysop')) || ( wgRestrictionEdit.length == 1 && wgRestrictionEdit[0] == 'autoconfirmed' && wgUserGroups && wgUserGroups.join (' ').contains ('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 && wgNamespaceNumber != 6) { // Only allow edits if the stored page name matches the current one. var img_page_name = getElementsByClassName (self.imgs[0].scope, '*', 'wpImageAnnotatorPageName'); if (img_page_name && img_page_name.length > 0) img_page_name = LAPI.DOM.getInnerText (img_page_name[0]); else img_page_name = ""; self.may_edit = (img_page_name.replace (/ /g, '_') == wgTitle.replace (/ /g, '_')); } // 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) { if (!self.ajaxQueried) { self.haveAjax = (LAPI.Ajax.getRequest () != null); self.ajaxQueried = true; } self.may_edit = self.haveAjax; } if (self.may_edit) { // Respect user override for zoom, if any if ( !isNaN (ImageAnnotator_zoom_threshold) && 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 = ImageAnnotator_zoom_threshold; } // Adapt zoom threshold for small thumbnails or images with a very lopsided width/height ratio, // but only if we *can* zoom at least twice if ( self.viewers[0].full_img.width > 300 && Math.min (self.viewers[0].factors.dx, self.viewers[0].factors.dy) >= 2.0 ) { if ( self.viewers[0].thumb.width < 400 || self.viewers[0].thumb.width / self.viewers[0].thumb.height > 2.0 || self.viewers[0].thumb.height / self.viewers[0].thumb.width > 2.0 ) { self.zoom_threshold = 0; // Force zooming } } self.editor = new ImageAnnotationEditor (); function track (evt) { evt = evt || window.event; if (self.is_adding) self.update_zoom (evt); if (!self.is_tracking) return LAPI.Evt.kill (evt); var mouse_pos = LAPI.Pos.mousePosition (evt); if (!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 = "" + (mouse_pos.x - self.base_x) + 'px'; self.definer.style.left = "" + self.base_x + 'px'; } else { self.definer.style.width = "" + (self.base_x - mouse_pos.x) + 'px'; self.definer.style.left = "" + mouse_pos.x + 'px'; } if (mouse_pos.y >= self.base_y) { self.definer.style.height = "" + (mouse_pos.y - self.base_y) + 'px'; self.definer.style.top = "" + self.base_y + 'px'; } else { self.definer.style.height = "" + (self.base_y - mouse_pos.y) + 'px'; self.definer.style.top = "" + mouse_pos.y + 'px'; } return LAPI.Evt.kill (evt); }; function pause (evt) { LAPI.Evt.remove (document, 'mousemove', track, true); if (!LAPI.Browser.is_ie && typeof (document.captureEvents) == 'function') document.captureEvents (null); self.move_listening = false; }; function resume (evt) { // captureEvents is actually deprecated, but I haven't succeeded to make this work with // addEventListener only. if ((self.is_tracking || self.is_adding) && !self.move_listening) { if (!LAPI.Browser.is_ie && typeof (document.captureEvents) == 'function') document.captureEvents (Event.MOUSEMOVE); LAPI.Evt.attach (document, 'mousemove', track, true); self.move_listening = true; } }; function stop_tracking (evt) { evt = evt || window.event; // Check that we're within the image. Note: this check can fail only on IE >= 7, on other // browsers, we attach the handler on self.cover and thus we don't even get events outside // that range. var mouse_pos = LAPI.Pos.mousePosition (evt); if (!LAPI.Pos.isWithin (self.cover, mouse_pos.x, mouse_pos.y)) return; if (self.is_tracking) { self.is_tracking = false; self.is_adding = false; // Done. pause (); if (LAPI.Browser.is_ie) { //Trust Microsoft to get everything wrong! LAPI.Evt.remove (document, 'mouseup', stop_tracking); } else { LAPI.Evt.remove (self.cover, 'mouseup', stop_tracking); } LAPI.Evt.remove (window, 'blur', pause); LAPI.Evt.remove (window, 'focus', resume); self.cover.style.cursor = 'auto'; LAPI.DOM.removeNode (self.border); LAPI.Evt.remove (self.cover, self.mouse_in, self.update_zoom_evt); LAPI.Evt.remove (self.cover, self.mouse_out, self.hide_zoom_evt); self.hide_zoom (); self.viewers[0].hide (); // Hide all existing boxes if (!self.definer || self.definer.offsetWidth <= 0 || self.definer.offsetHeight <= 0) { // Nothing: just remove the definer: if (self.definer) LAPI.DOM.removeNode (self.definer); // Re-attach event handlers self.viewers[0].setShowHideEvents (true); self.hide_cover (); self.viewers[0].setDefaultMsg (); // And make sure we get the real view again self.viewers[0].show (); } else { // We have a div with some extent: remove event capturing and create a new annotation var new_note = new ImageAnnotation (self.definer, self.viewers[0], -1); self.viewers[0].register (new_note); self.editor.editNote (new_note); } self.definer = null; } if (evt) return LAPI.Evt.kill (evt); return false; }; function start_tracking (evt) { if (!self.is_tracking) { self.is_tracking = true; evt = evt || window.event; // Set the position, size 1 var mouse_pos = LAPI.Pos.mousePosition (evt); var origin = LAPI.Pos.position (self.cover); self.base_x = mouse_pos.x - origin.x; self.base_y = mouse_pos.y - origin.y Object.merge ( { left : "" + self.base_x + 'px' ,top : "" + self.base_y + 'px' ,width : '0px' ,height : '0px' ,display: "" } , self.definer.style ); // Set mouse handlers LAPI.Evt.remove (self.cover, 'mousedown', start_tracking); if (LAPI.Browser.is_ie) { LAPI.Evt.attach (document, 'mouseup', stop_tracking); // Doesn't work properly on self.cover... } else { LAPI.Evt.attach (self.cover, 'mouseup', stop_tracking); } resume (); LAPI.Evt.attach (window, 'blur', pause); LAPI.Evt.attach (window, 'focus', resume); } if (evt) return LAPI.Evt.kill (evt); return false; }; function add_new (evt) { self.editor.hide_editor (); Tooltips.close (); var cover = self.get_cover (); cover.style.cursor = 'crosshair'; self.definer = LAPI.make ( 'div', null ,{ border : '1px solid ' + ImageAnnotator.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 (self.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' , self.UI.get ('wpImageAnnotatorAddButtonText', true) , add_new ); var add_plea = self.UI.needs_plea; self.button_div.appendChild (self.add_button); self.help_link = self.createHelpLink (); if (self.help_link) { self.button_div.appendChild (document.createTextNode ('\xa0')); self.button_div.appendChild (self.help_link); } if (add_plea && wgServer.contains ('/commons')) self.button_div.appendChild (self.UI.get_plea ()); } // end if may_edit // Get the file description pages of thumbnails. Figure out for which viewers we need to do this. var cache = {}; var get_local = []; var get_foreign = []; Array.forEach ( self.viewers , function (viewer, idx) { if (viewer.setup_done || viewer.isLocal && !viewer.has_page) return; // Handle only images that either are foreign or local and do have a page. if (cache[viewer.realName]) { cache[viewer.realName][cache[viewer.realName].length] = idx; } else { cache[viewer.realName] = [idx]; if (!viewer.has_page) { get_foreign[get_foreign.length] = viewer.realName; } else { get_local[get_local.length] = viewer.realName; } } } ); if (get_local.length == 0 && get_foreign.length == 0) return; // Now we have unique page names in the cache and in to_get. Go get the corresponding file // description pages. We make a series of simultaneous asynchronous calls to avoid hitting // API limits and to keep the URL length below the limit for the foreign_repo calls. function make_calls (list, execute_call, url_limit) { function composer (list, from, length, url_limit) { function compose (list, from, length, url_limit) { var text = ""; var done = 0; for (var i = from; i < from + length; i++) { var new_text = '<div class="wpImageAnnotatorInlineImageWrapper" style="display:none;">' + '<span class="image_annotation_inline_name">' + list[i] + '</span>' + '{{' + list[i] + '}}' + '</div>' ; if (url_limit) { new_text = encodeURIComponent (new_text); if (text.length > 0 && (text.length + new_text.length > url_limit)) break; } text = text + new_text; done++; } 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; } } function setup_thumb_viewers (html_text) { var node = LAPI.make ('div', null, {display: 'none'}); document.body.appendChild (node); try { node.innerHTML = html_text; var pages = getElementsByClassName (node, 'div', 'wpImageAnnotatorInlineImageWrapper'); for (var i = 0; pages && i < pages.length; i++) { var notes = getElementsByClassName (pages[i], 'div', self.annotation_class); if (!notes || notes.length == 0) continue; var page = self.getItem ('inline_name', pages[i]); if (!page) continue; page = page.replace (/ /g, '_'); var viewers = cache[page] || cache['File:' + page.substring (page.indexOf (':') + 1)]; if (!viewers || viewers.length == 0) continue; // Update rules. var rules = getElementsByClassName (pages[i], 'div', 'wpImageAnnotatorInlinedRules'); var local_rules = { inline: Object.clone (ImageAnnotator.rules.inline) ,thumbs: Object.clone (ImageAnnotator.rules.thumbs) }; if (rules && rules.length > 0) { rules = rules[0]; if ( typeof (local_rules.inline.show) == 'undefined' && LAPI.DOM.hasClass (rules, 'wpImageAnnotatorNoInlineDisplay') ) { local_rules.inline.show = false; } if ( typeof (local_rules.inline.icon) == 'undefined' && LAPI.DOM.hasClass (rules, 'wpImageAnnotatorInlineDisplayIcon') ) { local_rules.inline.icon = true; } if ( typeof (local_rules.thumbs.show) == 'undefined' && LAPI.DOM.hasClass (rules, 'wpImageAnnotatorNoThumbs') ) { local_rules.thumbs.show = false; } if ( typeof (local_rules.thumbs.icon) == 'undefined' && LAPI.DOM.hasClass (rules, 'wpImageAnnotatorThumbDisplayIcon') ) { local_rules.thumbs.icon = true; } } // Make sure all are set local_rules.inline.show = typeof (local_rules.inline.show) == 'undefined' || local_rules.inline.show; local_rules.thumbs.show = typeof (local_rules.thumbs.show) == 'undefined' || local_rules.thumbs.show; local_rules.inline.icon = typeof (local_rules.inline.icon) != 'undefined' && local_rules.inline.icon; local_rules.thumbs.icon = typeof (local_rules.thumbs.icon) != 'undefined' && local_rules.thumbs.icon; if (!local_rules.inline.show) continue; // Now use pages[i] as a scope shared by all the viewers using it. Since we clone note // contents for note display, this works. Otherwise, we'd have to copy the notes into // each viewer's scope. document.body.appendChild (pages[i]); // Move it out of 'node' // Set viewers' scopes and finish their setup. Array.forEach ( viewers , function (v) { if (!self.viewers[v].isThumbnail || local_rules.thumbs.show) { self.viewers[v].scope = pages[i]; self.viewers[v].setup ( self.viewers[v].isThumbnail && local_rules.thumbs.icon || self.viewers[v].isOther && local_rules.inline.icon); } } ); } } catch (ex) {} LAPI.DOM.removeNode (node); } self.script_callbacks = []; function make_script_calls (list, api) { make_calls ( list , function (text) { var idx = self.script_callbacks.length; self.script_callbacks[idx] = { callback : function (json) { if (json && json.parse && json.parse.text && json.parse.text['*']) { setup_thumb_viewers (json.parse.text['*']); } ImageAnnotator.script_callbacks[idx].done = true; if (ImageAnnotator.script_callbacks[idx].script) { LAPI.DOM.removeNode (ImageAnnotator.script_callbacks[idx].script); ImageAnnotator.script_callbacks[idx].script = null; } } ,done : false }; self.script_callbacks[idx].script = ImageAnnotator.getScript ( api + '?action=parse&pst&text=' + text + '&prop=text&format=json' + '&maxage=1800&smaxage=1800' + '&callback=ImageAnnotator.script_callbacks[' + idx + '].callback' , true // No local caching! ); if ( self.script_callbacks && self.script_callbacks[idx] && self.script_callbacks[idx].done && self.script_callbacks[idx].script) { LAPI.DOM.removeNode (ImageAnnotator.script_callbacks[idx].script); ImageAnnotator.script_callbacks[idx].script = null; } } , LAPI.DOM.is_ie ? 1900 : 8000 ); } if ((!window.XMLHttpRequest && !!window.ActiveXObject) || !self.haveAjax) { make_script_calls (get_local, wgServer + wgScriptPath + '/api.php'); } else { make_calls ( get_local , function (text) { LAPI.Ajax.parseWikitext ( text , function (html_text) {if (html_text) setup_thumb_viewers (html_text);} , function () {} , false , null , 'API' // Fixed string to enable caching at all , 1800 // 30 minutes caching. ); } ); } // 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, self.sharedRepositoryAPI ()); }, show_zoom : function () { var self = ImageAnnotator; if ( ( self.viewers[0].factors.dx < self.zoom_threshold && self.viewers[0].factors.dy < self.zoom_threshold ) || Math.max (self.viewers[0].factors.dx, self.viewers[0].factors.dy) < 2.0 ) { // Below zoom threshold, or full image not even twice the size of the preview return; } if (!self.zoom) { self.zoom = LAPI.make ( 'div' , {id : 'image_annotator_zoom'} , { overflow : 'hidden' ,width : '200px' ,height : '200px' ,position : 'absolute' ,display : 'none' ,top : '0px' ,left : '0px' ,border : '2px solid #666666' ,backgroundColor : 'white' ,zIndex : 2050 // On top of everything } ); var src = self.viewers[0].img.getAttribute ('src', 2); // Adjust zoom_factor if (self.zoom_factor > self.viewers[0].factors.dx || self.zoom_factor > self.viewers[0].factors.dy) self.zoom_factor = Math.min (self.viewers[0].factors.dx, self.viewers[0].factors.dy); self.zoom.appendChild (LAPI.make ('div', null, {position : 'relative'})); // Calculate zoom size and source link var zoom_width = Math.floor (self.viewers[0].thumb.width * self.zoom_factor); var zoom_height = Math.floor (self.viewers[0].thumb.height * self.zoom_factor); var zoom_src = null; // For SVGs, always use a scaled PNG for the zoom. if (zoom_width < 0.9 * self.viewers[0].full_img.width || src.search (/\.svg\.png$/i) >= 0) { var i = src.lastIndexOf ('/'); if (i >= 0) { zoom_src = src.substring (0, i) + src.substring (i).replace (/^\/\d+px-/, '/' + zoom_width + 'px-'); } } else { // If the thumb we'd be loading was within about 80% of the full image size, we may just as // well get the full image instead of a scaled version. self.zoom_factor = Math.min (self.viewers[0].factors.dx, self.viewers[0].factors.dy); zoom_width = self.viewers[0].full_img.width; zoom_height = self.viewers[0].full_img.height; zoom_src = self.viewers[0].img.parentNode.getAttribute ('href', 2); } // Construct the initial zoomed image. We need to clone; if we create a completely // new DOM node ourselves, it may not work on IE6... var zoomed = self.viewers[0].img.cloneNode (true); zoomed.width = "" + zoom_width; zoomed.height = "" + zoom_height; Object.merge ({position: 'absolute', top: '0px',left: '0px'}, zoomed.style); self.zoom.firstChild.appendChild (zoomed); // Crosshair self.zoom.firstChild.appendChild ( LAPI.make ( 'div', null , { width : '1px' ,height : '200px' ,borderLeft : '1px solid red' ,position : 'absolute' ,top : '0px' ,left : '100px' } ) ); self.zoom.firstChild.appendChild ( LAPI.make ( 'div', null , { width : '200px' ,height : '1px' ,borderTop : '1px solid red' ,position : 'absolute' ,top : '100px' ,left : '0px' } ) ); document.body.appendChild (self.zoom); if (zoom_src) { var zoom_loader = LAPI.make ( 'img' , {width : "" + zoom_width, height: "" + zoom_height, src: zoom_src} , {position: 'absolute', top: '0px', left: '0px', display: 'none'} ); LAPI.Evt.attach ( zoom_loader, 'load' , function () { // Replace the image in self.zoom by self.zoom_loader, making sure we keep the offsets zoom_loader.style.top = self.zoom.firstChild.firstChild.style.top; zoom_loader.style.left = self.zoom.firstChild.firstChild.style.left; zoom_loader.style.display = ""; self.zoom.firstChild.replaceChild (zoom_loader, self.zoom.firstChild.firstChild); } ); document.body.appendChild (zoom_loader); // Now the browser goes loading the larger image } } 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 = ImageAnnotator; 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)) { ImageAnnotator.hide_zoom (); return; } var dx = mouse_pos.x - origin.x; var dy = mouse_pos.y - origin.y; // dx, dy is the offset within the preview image. Align the zoom image accordingly. var top = - dy * self.zoom_factor + 100; var left = - dx * self.zoom_factor + 100; self.zoom.firstChild.firstChild.style.top = "" + top + 'px'; self.zoom.firstChild.firstChild.style.left = "" + left + 'px'; self.zoom.style.top = mouse_pos.y + 10 + 'px'; // Right below the mouse pointer // Horizontally keep it in view. var x = (self.is_rtl ? mouse_pos.x - 10 : mouse_pos.x + 10); if (x < 0) x = 0; self.zoom.style.left = x + 'px'; self.zoom.style.display = ""; // Now that we have offsetWidth, correct the position. if (self.is_rtl) { x = mouse_pos.x - 10 - self.zoom.offsetWidth; if (x < 0) x = 0; } else { var off = LAPI.Pos.scrollOffset (); var view = LAPI.Pos.viewport (); if (x + self.zoom.offsetWidth > off.x + view.x) x = off.x + view.x - self.zoom.offsetWidth; if (x < 0) x = 0; } self.zoom.style.left = x + 'px'; }, hide_zoom : function (evt) { if (!ImageAnnotator.zoom) return; if (evt) { var mouse_pos = LAPI.Pos.mousePosition (evt); if (LAPI.Pos.isWithin (ImageAnnotator.cover, mouse_pos.x, mouse_pos.y)) return; } ImageAnnotator.zoom.style.display = 'none'; }, createHelpLink : function () { var msg = ImageAnnotator.UI.get ('wpImageAnnotatorHelp', false, true); if (!msg || !msg.lastChild) return null; 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. var tgt = msg.lastChild; if (tgt.nodeName.toLowerCase () != 'a') tgt = wgServer + 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; window.location = target; if (e) return LAPI.Evt.kill (e); return false; }; } var imgs = msg.getElementsByTagName ('img'); if (!imgs || imgs.length == 0) { // We're supposed to have a spans giving the button text var text = msg.firstChild; if (text.nodeName.toLowerCase () == 'span') text = LAPI.DOM.getInnerText (text); else text = 'Help'; return LAPI.DOM.makeButton ( 'ImageAnnotationHelpButton' , text , make_handler (tgt) ); } else { return Buttons.makeButton (imgs, 'ImageAnnotationHelpButton', make_handler (tgt)); } }, get_cover : function () { var self = ImageAnnotator; 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) { var 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 : '1px' // Fix strange 1px jog on IE6... ,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. var 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 = ImageAnnotator; 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 = ImageAnnotator; 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 > 0) node = node[0]; else node = null; } return node; }, getItem : function (what, scope) { var node = ImageAnnotator.getRawItem (what, scope); if (!node) return null; return LAPI.DOM.getInnerText (node).trim(); }, getIntItem : function (what, scope) { var x = ImageAnnotator.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 < ImageAnnotator.note_delim.length && !result; i++) { result = find (text, id, ImageAnnotator.note_delim[i]); } return result; }, setWikitext : function (pagetext) { var self = ImageAnnotator; 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); return; } } } } ); self.wiki_read = true; }, setSummary : function (summary, initial_text, note_text) { if (initial_text.contains ('$1')) { var max = (summary.maxlength || 200) - initial_text.length; if (note_text) initial_text = initial_text.replace ('$1', ': ' + note_text.replace ('\n', ' ').substring (0, max)); else initial_text = initial_text.replace ('$1', ""); } summary.value = initial_text; }, getScript : function (url, bypass_local_cache, bypass_caches) { // Don't use LAPI here, it may not yet be available if (bypass_caches) { url += ((url.indexOf ('?') >= 0) ? '&' : '?') + 'dummyTimestamp=' + (new Date()).getTime (); } if (bypass_local_cache) { var s = document.createElement ('script'); s.setAttribute ('src', url); s.setAttribute ('type', 'text/javascript'); document.getElementsByTagName ('head')[0].appendChild (s); return s; } else { return importScriptURI (url); } } }; // end ImageAnnotator if (wgNamespaceNumber != -1 && wgAction && (wgAction == 'view' || wgAction == 'purge')) { // Start it. Bypass caches; but allow for 4 hours client-side caching. Small file. ImageAnnotator.getScript ( wgScript + '?title=MediaWiki:ImageAnnotatorConfig.js&action=raw&ctype=text/javascript' + '&dummy=' + Math.floor ((new Date()).getTime () / (14400 * 1000)) // 4 hours , true // No local caching! ); } // end if we may run at all } // end if (guard against double inclusions) // </source>