MediaWiki:Gadget-ImageAnnotator.js: Difference between revisions
Content deleted Content added
It's really time to start using jQuery, and other modern mw.* libraries. If we want to maintain back-compat with non-WMF wikis for ever, lets just freeze this version in time. |
lint, and phase out legacy getElementsByClassName |
||
Line 2:
/*
ImageAnnotator
Image annotations. Draw rectangles onto image thumbnail displayed on image description
Line 27 ⟶ 22:
*/
// Global: importScript, importScriptURI (
// Global: wgPageName, wgCurRevisionId, wgUserGroups, wgRestrictionEdit (inline script on the page)
// Global:
// Global: wgNamespaceIds (inline script)
/*jshint eqnull:true, laxbreak:true, laxcomma:true */
Line 41 ⟶ 36:
(function () { // Local scope
var conf = mw.config.get([
'skin',
'stylepath'
]);
var ImageAnnotator_config = null;
Line 61:
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;
Line 78 ⟶ 77:
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
}
// Notes written by early versions may be slightly too large, whence the + 10 above. Fix this.
Line 90 ⟶ 89:
view_y = Math.floor(y / viewer.factors.dy);
this.view =
LAPI.make( 'div', null, {
//
fontSize: '0px',
// We'll add the view to the DOM once we've loaded all notes
this.model = {
} else {
is_new = true;
this.view = node;
this.model = {
view_w = this.view.offsetWidth - 2; // Subtract cumulated border widths
view_h = this.view.offsetHeight - 2;
Line 128 ⟶ 127:
if (view_h < 6) {view_y = Math.floor(view_y + view_h / 2 - 3); view_h = 6; }
Object.merge(
{
top: '' + view_y + 'px', height: '' + view_h + 'px' },
this.view.style );
this.view.style.zIndex = 500; // Below tooltips
Line 167 ⟶ 170:
},
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
Line 211 ⟶ 213:
},
display : function (evt) {
if (!this.content) {
this.content = LAPI.make('div');
Line 255 ⟶ 256:
},
edit : function (evt) {
if (IA.canEdit()) IA.editor.editNote(this);
if (evt) return LAPI.Evt.kill(evt);
Line 262:
},
remove_event : function (evt) {
if (IA.canEdit()) this.remove();
return LAPI.Evt.kill(evt);
},
remove : function () {
if (!this.content) { // New note: just destroy it.
this.destroy();
Line 299 ⟶ 297:
LAPI.Ajax.editPage(
wgPageName
, function (doc, editForm, failureFunc, revision_id) {
try {
if (revision_id && revision_id != wgCurRevisionId)
Line 377 ⟶ 374:
},
destroy : function () {
if (this.view) LAPI.DOM.removeNode(this.view);
if (this.dummy) LAPI.DOM.removeNode(this.dummy);
Line 390 ⟶ 386:
},
area : function () {
if (!this.model || !this.model.dimension) return 0;
return (this.model.dimension.w * this.model.dimension.h);
},
cannotEdit : function () {
if (this.content && this.content.button_section) {
LAPI.DOM.removeNode(this.content.button_section);
Line 411 ⟶ 405:
ImageAnnotationEditor.prototype =
{
initialize : function () {
var editor_width = 50;
// Respect potential user-defined width setting
Line 507 ⟶ 500:
},
editNote : function (note) {
var same_note = (note == this.note);
this.note = note;
Line 557 ⟶ 549:
},
open_editor : function (same_note, cover) {
this.editor.hidePreview();
if (!same_note || this.editor.textarea.readOnly)
Line 596 ⟶ 587:
},
hide_editor : function (evt) {
if (!this.visible) return;
this.visible = false;
Line 617 ⟶ 607:
},
save : function (editor) {
var data = editor.getText();
if (!data || !data.length) {
Line 704 ⟶ 693:
LAPI.Ajax.editPage(
wgPageName
, function (doc, editForm, failureFunc, revision_id) {
try {
if (revision_id && revision_id != wgCurRevisionId)
Line 832 ⟶ 820:
);
}
, function (request, ex) {
self.editor.busy(false);
self.saving = false;
Line 843 ⟶ 830:
// Error message. Use preview field for this.
var error_msg = ImageAnnotator.UI.get('wpImageAnnotatorSaveError', false);
var lk = error_msg.getElementsByClassName(
if (lk && lk.length && lk[0].firstChild.nodeName.toLowerCase() === 'a') {
lk = lk[0].firstChild;
Line 868 ⟶ 855:
},
onpreview : function (editor) {
if (this.tooltip) this.tooltip.size_change();
},
cancel : function (editor) {
if (!this.note) return;
if (!this.note.content) {
Line 884 ⟶ 869:
},
close_tooltip : function (tooltip, evt) {
this.hide_editor(evt);
this.cancel();
Line 896 ⟶ 880:
ImageNotesViewer.prototype =
{
initialize : function (descriptor, may_edit) {
Object.merge(descriptor, this);
this.annotations = [];
Line 907 ⟶ 890:
this.tip = null;
this.icon = null;
this.factors = {
if (!this.isThumbnail && !this.isOther) {
Line 925 ⟶ 908:
},
setup : function (onlyIcon) {
this.setup_done = true;
var name = this.realName;
var $fullname;
if (this.isThumbnail || this.scope == document || this.may_edit || !IA.haveAjax) {
this.imgName = this.realName;
this.realName = '';
} else {
this.realName =
this.imgName = this.realName;
}
var annotations =
if (!this.may_edit && (!annotations || annotations.length === 0))
Line 993 ⟶ 976:
(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
Line 1,004 ⟶ 986:
&& this.icon.nodeName.toLowerCase() == 'a'
&& this.icon.firstChild.nodeName.toLowerCase() == 'img'
)
// Make sure we use the right protocol:
var srcFixed = this.icon.firstChild.getAttribute('src', 2).replace(/^https?\:/, document.___location.protocol);
Line 1,020 ⟶ 1,001:
}
Object.merge(
{
);
this.icon.onclick = (function () { ___location.href = this.img.parentNode.href; }).bind(this);
Line 1,053 ⟶ 1,034:
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));
Line 1,068 ⟶ 1,048:
Array.forEach(this.annotations, (function (note) {this.img_div.appendChild(note.view);}).bind(this));
if (this.isThumbnail) {
this.main_div =
if (!this.main_div || this.main_div.length === 0)
this.main_div = null;
else
Line 1,094 ⟶ 1,074:
&& ImageAnnotator_config.displayCaptionInArticles
(name, this.isLocal, this.thumb, this.full_img, annotations.length, this.isThumbnail)
)
this.msg = LAPI.make('div', null, { display: 'none' });
if (IA.is_rtl) {
this.msg.style.direction = 'rtl';
Line 1,152 ⟶ 1,131:
},
cannotEdit : function () {
if (!this.may_edit) return;
this.may_edit = false;
Line 1,159 ⟶ 1,137:
},
setShowHideEvents : function (set) {
if (this.icon) return;
if (set) {
Line 1,174 ⟶ 1,151:
},
removeMoveListener : function () {
if (this.icon) return;
this.move_listening = false;
Line 1,185 ⟶ 1,161:
},
adjustRectangleSize : function (node) {
if (this.icon) return;
// Make sure the note boxes don't overlap the image boundary; we might get an event
Line 1,206 ⟶ 1,181:
// 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';
Line 1,217 ⟶ 1,192:
},
toggle : function (dummies) {
var i;
if (!this.annotations || this.annotations.length === 0 || this.icon) return;
Line 1,241 ⟶ 1,215:
},
show : function (evt) {
if (this.visible || this.icon) return;
this.toggle(IA.is_adding || IA.is_editing);
Line 1,253 ⟶ 1,226:
},
hide : function (evt) {
if (this.icon) return true;
if (!this.visible) {
Line 1,314 ⟶ 1,286:
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;
Line 1,332 ⟶ 1,303:
},
check_hide : function (evt) {
if (this.icon) return true;
if (this.visible)
Line 1,340 ⟶ 1,310:
},
register : function (new_note) {
this.annotations[this.annotations.length] = new_note;
if (new_note.model.id > 0) {
Line 1,350 ⟶ 1,319:
},
deregister : function (note) {
Array.remove(this.annotations, note);
if (note.model.id == this.max_id) this.max_id--;
Line 1,357 ⟶ 1,325:
},
setDefaultMsg : function () {
if (this.annotations && this.annotations.length && this.msg) {
LAPI.DOM.removeChildren(this.msg);
Line 1,390 ⟶ 1,357:
};
var IA = {
// This object is responsible for setting up annotations when a page is loaded. It loads all
// annotations in the page source, and adds an "Annotate this image" button plus the support
Line 1,428 ⟶ 1,394:
],
tooltip_styles: {
,
, padding: '0.3em'
, fontSize: (conf.skin === 'monobook' ? '127%' : '100%')
// Scale up to default text size
},
editor
wiki_read
is_rtl
move_listening
is_tracking
is_adding
is_editing
zoom_threshold
zoom_factor
install_attempts
max_install_attempts
imgs_with_notes
thumbs
other_images
// Fallback
indication_icon
install
if (typeof ImageAnnotator_disable !== 'undefined' && !!ImageAnnotator_disable) return;
if (!config || ImageAnnotator_config != null) return;
Line 1,476 ⟶ 1,442:
&& typeof LAPI.Ajax !== 'undefined'
&& typeof LAPI.Ajax.getRequest !== 'undefined'
)
self.haveAjax = (LAPI.Ajax.getRequest() != null);
self.ajaxQueried = true;
Line 1,488 ⟶ 1,453:
self.may_edit = wgNamespaceNumber >= 0 && wgArticleId > 0 && 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++) {
Line 1,510 ⟶ 1,474:
if ( !self.haveAjax
|| !config.generalImagesEnabled()
|| namespaceCheck
)
self.rules.inline.show = false;
self.rules.thumbs.show = false;
Line 1,520 ⟶ 1,483:
|| !config.thumbsEnabled()
|| namespaceCheck(window.ImageAnnotator_no_thumbs || null)
)
self.rules.thumbs.show = false;
}
Line 1,528 ⟶ 1,490:
else if ( !config.sharedImagesEnabled()
|| namespaceCheck(window.ImageAnnotator_no_shared || null)
)
self.rules.shared.show = false;
}
Line 1,554 ⟶ 1,515:
if ( typeof self.rules.inline.show === 'undefined'
&& rules.className.indexOf('wpImageAnnotatorDisplay') >= 0
)
self.rules.inline.show = true;
}
Line 1,563 ⟶ 1,523:
if ( typeof self.rules.thumbs.show === 'undefined'
&& rules.className.indexOf('wpImageAnnotatorThumbDisplay') >= 0
)
self.rules.thumbs.show = true;
}
Line 1,573 ⟶ 1,532:
self.rules.thumbs.icon = true;
}
if (rules.className.indexOf('wpImageAnnotatorOnlyLocal') >= 0) {
self.rules.shared.show = false;
}
Line 1,638 ⟶ 1,596:
}
} else {
self.imgs_with_notes = document.getElementsByClassName
if (do_thumbs)
self.thumbs = document.getElementsByClassName('thumbinner');
}
if ( wgNamespaceNumber == 6
Line 1,646 ⟶ 1,605:
|| (self.thumbs.length)
|| (self.other_images.length)
)
// Publish parts of config.
ImageAnnotator.UI = config.UI;
Line 1,658 ⟶ 1,616:
},
wait_for_required_libraries : function () {
if (typeof Tooltip == 'undefined' || typeof LAPI == 'undefined') {
if (IA.install_attempts++ < IA.max_install_attempts) {
Line 1,672 ⟶ 1,629:
},
setup: function () {
var self = IA;
self.imgs = [];
Line 1,684 ⟶ 1,640:
)
;
// Use this to temporarily display an image off-screen to get its dimensions
var testImgDiv =
LAPI.make('div', null, {
document.body.insertBefore(testImgDiv, document.body.firstChild);
function img_check (img, is_other) {
var srcW = parseInt (img.getAttribute('width', 2), 10);
var srcH = parseInt (img.getAttribute('height', 2), 10);
Line 1,722 ⟶ 1,674:
if (w != srcW || h != srcH) return null;
// Exclude system images
if (img.src.contains(conf.stylepath)) return null;
// Only if within a link
if (img.parentNode.nodeName.toLowerCase() != 'a') return null;
Line 1,756 ⟶ 1,708:
file_div = LAPI.$('file');
} else if (!is_thumb && !is_other) {
file_div = scope.getElementsByClassName(
if (!file_div || file_div.length != 1) return null;
file_div = file_div[0];
Line 1,829 ⟶ 1,781:
}
function setup_images (list) {
Array.forEach(list,
function (elem) {
Line 1,890 ⟶ 1,841:
var done = 0;
function check_done (length) {
done += length;
if (done >= names.length) {
Line 1,899 ⟶ 1,849:
}
function make_calls (execute_call, url_limit) {
function build_titles (from, length, url_limit) {
var done = 0;
var text = '';
Line 1,926 ⟶ 1,874:
}
function set_info (json) {
try {
if (json && json.query && json.query.pages) {
Line 1,973 ⟶ 1,920:
function (length, titles) {
var idx = ImageAnnotator.info_callbacks.length;
ImageAnnotator.info_callbacks[idx] = {
},
ImageAnnotator.info_callbacks[idx].script =
IA.getScript(
Line 1,996 ⟶ 1,943:
// up in that case.
if ( ImageAnnotator.info_callbacks && ImageAnnotator.info_callbacks[idx]
&& ImageAnnotator.info_callbacks[idx].done && ImageAnnotator.info_callbacks[idx].script
) {
LAPI.DOM.removeNode(ImageAnnotator.info_callbacks[idx].script);
ImageAnnotator.info_callbacks[idx].script = null;
Line 2,025 ⟶ 1,972:
},
setup_ui : function () {
// Complete the UI object we've gotten from config.
Line 2,054 ⟶ 2,000:
};
ImageAnnotator.UI.setup = function () {
if (ImageAnnotator.UI.repo) return;
var self = ImageAnnotator.UI;
var node = LAPI.make('div', null, { display: 'none' });
var item;
document.body.appendChild(node);
if (typeof UIElements === 'undefined') {
self.basic = true;
self.repo = {};
for (
node.innerHTML = self.defaults[item];
self.repo[item] = node.firstChild;
Line 2,071 ⟶ 2,017:
self.basic = false;
self.repo = UIElements.emptyRepository(self.defaultLanguage);
for (
node.innerHTML = self.defaults[item];
UIElements.setEntry(item, self.repo, node.firstChild);
Line 2,081 ⟶ 2,027:
};
ImageAnnotator.UI.get = function (id, basic, no_plea) {
var self = ImageAnnotator.UI;
if (!self.repo) self.setup();
Line 2,112 ⟶ 2,057:
};
ImageAnnotator.UI.get_plea = function () {
var self = ImageAnnotator.UI;
var translate = self.get('wpTranslate', false, true) || 'translate';
Line 2,131 ⟶ 2,075:
};
ImageAnnotator.UI.init = function (html_text_or_json) {
var text;
if (typeof html_text_or_json === 'string')
Line 2,150 ⟶ 2,093:
}
var node = LAPI.make('div', null, { display: 'none' });
document.body.appendChild(node);
try {
Line 2,167 ⟶ 2,110:
+ '|live=1}}';
function get_ui_no_ajax () {
var url =
wgServer + wgScriptPath + '/api.php?format=json&action=parse&pst&text='
+ encodeURIComponent(ui_page) + '&title=API&prop=text
+ '&callback=ImageAnnotator.UI.init&maxage=14400&smaxage=14400'
;
Line 2,180 ⟶ 2,122:
}
function get_ui () {
IA.haveAjax = (LAPI.Ajax.getRequest() != null);
IA.ajaxQueried = true;
Line 2,215 ⟶ 2,156:
},
setup_step_two : function () {
var self = IA;
Line 2,238 ⟶ 2,178:
},
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.
Line 2,256 ⟶ 2,195:
if (self.may_edit) {
// Check whether the image is local. Don't allow editing if the file is remote.
var sharedUpload = document.getElementsByClassName
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 = self.imgs[0].scope.getElementsByClassName('wpImageAnnotatorPageName');
if (img_page_name && img_page_name.length)
img_page_name = LAPI.DOM.getInnerText(img_page_name[0]);
Line 2,285 ⟶ 2,223:
&& !isNaN (window.ImageAnnotator_zoom_threshold)
&& window.ImageAnnotator_zoom_threshold >= 0.0
)
// If somebody sets it to a nonsensical high value, that's his or her problem: there won't be any
// zooming.
Line 2,295 ⟶ 2,232:
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
}
Line 2,400 ⟶ 2,335:
}
function start_tracking (evt) {
if (!self.is_tracking) {
self.is_tracking = true;
Line 2,527 ⟶ 2,461:
// 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
var text = '';
var done = 0;
Line 2,635 ⟶ 2,566:
}
function setup_thumb_viewers (html_text) {
var node = LAPI.make('div', null, {display: 'none'});
document.body.appendChild(node);
try {
node.innerHTML = strip_noise (html_text);
var pages = node.getElementsByClassName
for (var i = 0; pages && i < pages.length; i++) {
var notes =
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 =
var local_rules = {
if (rules && rules.length) {
rules = rules[0];
if ( typeof local_rules.inline.show === 'undefined'
&& LAPI.DOM.hasClass(rules, 'wpImageAnnotatorNoInlineDisplay')
)
local_rules.inline.show = false;
}
if ( typeof local_rules.inline.icon === 'undefined'
&& LAPI.DOM.hasClass(rules, 'wpImageAnnotatorInlineDisplayIcon')
)
local_rules.inline.icon = true;
}
if ( typeof local_rules.thumbs.show === 'undefined'
&& LAPI.DOM.hasClass(rules, 'wpImageAnnotatorNoThumbs')
)
local_rules.thumbs.show = false;
}
if ( typeof local_rules.thumbs.icon === 'undefined'
&& LAPI.DOM.hasClass(rules, 'wpImageAnnotatorThumbDisplayIcon')
)
local_rules.thumbs.icon = true;
}
Line 2,712 ⟶ 2,638:
ImageAnnotator.script_callbacks = [];
function make_script_calls (list, api) {
var template = api + '?action=parse&pst&text=&prop=text&format=json'
+ '&maxage=1800&smaxage=1800&uselang=' + wgUserLanguage //see bugzilla 22764
Line 2,722 ⟶ 2,647:
, function (text) {
var idx = ImageAnnotator.script_callbacks.length;
ImageAnnotator.script_callbacks[idx] = {
}
}, done: false
}; ImageAnnotator.script_callbacks[idx].script =
IA.getScript(
Line 2,742 ⟶ 2,667:
);
if ( ImageAnnotator.script_callbacks && ImageAnnotator.script_callbacks[idx]
&& ImageAnnotator.script_callbacks[idx].done && ImageAnnotator.script_callbacks[idx].script
) {
LAPI.DOM.removeNode(ImageAnnotator.script_callbacks[idx].script);
ImageAnnotator.script_callbacks[idx].script = null;
Line 2,778 ⟶ 2,703:
},
show_zoom : function () {
var self = IA;
if ( ( self.viewers[0].factors.dx < self.zoom_threshold
Line 2,785 ⟶ 2,709:
)
|| 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;
Line 2,876 ⟶ 2,799:
},
update_zoom : function (evt) {
if (!evt) return; // We need an event to calculate positions!
var self = IA;
Line 2,913 ⟶ 2,835:
},
hide_zoom : function (evt) {
if (!IA.zoom) return;
if (evt) {
Line 2,923 ⟶ 2,844:
},
createHelpLink : function () {
var msg = ImageAnnotator.UI.get('wpImageAnnotatorHelp', false, true);
if (!msg || !msg.lastChild) return null;
Line 2,981 ⟶ 2,901:
},
get_cover : function () {
var self = IA;
var shim;
Line 3,040 ⟶ 2,959:
},
show_cover : function () {
var self = IA;
if (self.cover && !self.cover_visible) {
Line 3,054 ⟶ 2,972:
},
hide_cover : function () {
var self = IA;
if (self.cover && self.cover_visible) {
Line 3,068 ⟶ 2,985:
},
getRawItem : function (what, scope) {
var node = null;
if (!scope || scope == document) {
node = LAPI.$ ('image_annotation_' + what);
} else {
node = scope.getElementsByClassName
if (node && node.length) node = node[0]; else node = null;
}
Line 3,080 ⟶ 2,996:
},
getItem : function (what, scope) {
var node = IA.getRawItem(what, scope);
if (!node) return null;
Line 3,087 ⟶ 3,002:
},
getIntItem : function (what, scope) {
var x = IA.getItem(what, scope);
if (x !== null) x = parseInt (x, 10);
Line 3,094 ⟶ 3,008:
},
findNote : function (text, id) {
function find (text, id, delim) {
var start = delim.start.replace('$1', id);
Line 3,113 ⟶ 3,026:
},
setWikitext : function (pagetext) {
var self = IA;
if (self.wiki_read) return;
Line 3,138 ⟶ 3,050:
},
setSummary : function (summary, initial_text, note_text) {
if (initial_text.contains('$1')) {
var max = (summary.maxlength || 200) - initial_text.length;
Line 3,151 ⟶ 3,062:
},
getScript : function (url, bypass_local_cache, bypass_caches) {
// Don't use LAPI here, it may not yet be available
if (bypass_caches) {
Line 3,170 ⟶ 3,080:
},
canEdit : function () {
var self = IA;
if (self.may_edit) {
Line 3,191 ⟶ 3,100:
}; // end IA
window.ImageAnnotator = {
install: function (config) {
IA.install(config); } };
// Start it. Bypass caches; but allow for 4 hours client-side caching. Small file.
IA.getScript(
// Cache 4
+ '&dummy=' + Math.floor((new Date()).getTime() / (14400 * 1000)) true
);
|