= 0 && mw.config.get( 'wgArticleId' ) > 0 && self.haveAjax && config.editingEnabled();
function namespaceCheck( list ) {
if ( !list || Object.prototype.toString.call( list ) !== '[object Array]' ) { return false; }
var namespaceIds = mw.config.get( 'wgNamespaceIds' );
if ( !namespaceIds ) { return false; }
var namespaceNumber = mw.config.get( 'wgNamespaceNumber' );
for ( var i = 0; i < list.length; i++ ) {
if (
typeof list[ i ] === 'string' &&
(
list[ i ] === '*' ||
namespaceIds[ list[ i ].toLowerCase().replace( / /g, '_' ) ] === namespaceNumber
)
) {
return true;
}
}
return false;
}
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;
}
if ( mw.config.get( 'wgNamespaceNumber' ) == 6 ) {
self.rules.shared.show = true;
} else if ( !config.sharedImagesEnabled() ||
namespaceCheck( window.ImageAnnotator_no_shared || null )
) {
self.rules.shared.show = false;
}
if ( namespaceCheck( window.ImageAnnotator_icon_images || null ) ) { self.rules.inline.icon = true; }
if ( namespaceCheck( window.ImageAnnotator_icon_thumbs || null ) ) { self.rules.thumbs.icon = true; }
}
// User rule for displaying captions on images in articles
self.hideCaptions = namespaceCheck( window.ImageAnnotator_hide_captions || null );
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;
var do_thumbs = 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' );
// This prevents traversing a page with more than 400 images
// There are extreme cases like [[Emoji]] that high number of images can cause
// huge lag specially on Chrome
if ( all_imgs.length > 400 ) {
// purging the array, simply a hack to avoid more indention
all_imgs = [];
}
for ( var i = 0; i < all_imgs.length; i++ ) {
// Exclude all that are in img_with_notes or in thumbs. Also exclude all in galleries.
var up = all_imgs[ i ].parentNode;
if ( up.nodeName.toLowerCase() !== 'a' ) { continue; }
up = up.parentNode;
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' thumbinner ' ) >= 0 ) {
if ( do_thumbs ) { self.thumbs[ self.thumbs.length ] = up; }
continue;
}
up = up.parentNode;
if ( !up ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorEnable ' ) >= 0 ) {
self.imgs_with_notes[ self.imgs_with_notes.length ] = up;
continue;
}
up = up.parentNode;
if ( !up ) { continue; }
// Other images not in galleries
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' gallerybox ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorEnable ' ) >= 0 ) {
self.imgs_with_notes[ self.imgs_with_notes.length ] = up;
continue;
}
up = up.parentNode;
if ( !up ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorOff ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' gallerybox ' ) >= 0 ) { continue; }
if ( ( ' ' + up.className + ' ' ).indexOf( ' wpImageAnnotatorEnable ' ) >= 0 ) {
self.imgs_with_notes[ self.imgs_with_notes.length ] = up;
} else {
// Guard against other scripts adding aribtrary numbers of divs (dshuf for instance!)
var is_other = true;
while ( up && up.nodeName.toLowerCase() == 'div' && is_other ) {
up = up.parentNode;
if ( up ) { is_other = ( ' ' + up.className + ' ' ).indexOf( ' gallerybox ' ) < 0; }
}
if ( is_other ) { self.other_images[ self.other_images.length ] = all_imgs[ i ]; }
}
} // end loop
}
} else {
self.imgs_with_notes = getElementsByClassName( document, '*', 'wpImageAnnotatorEnable' );
if ( do_thumbs ) { self.thumbs = getElementsByClassName( document, 'div', 'thumbinner' ); } // No galleries!
}
if (
mw.config.get( 'wgNamespaceNumber' ) == 6 ||
( self.imgs_with_notes.length ) ||
( self.thumbs.length ) ||
( self.other_images.length )
) {
// Publish parts of config.
ImageAnnotator.UI = config.UI;
self.outer_border = config.outer_border;
self.inner_border = config.inner_border;
self.active_border = config.active_border;
self.new_border = config.new_border;
self.wait_for_required_libraries();
}
},
wait_for_required_libraries: function () {
if ( typeof Tooltip === 'undefined' || typeof LAPI === 'undefined' || typeof LAPI.Ajax === 'undefined' || typeof LAPI.DOM === 'undefined' ) {
if ( IA.install_attempts++ < IA.max_install_attempts ) {
setTimeout( IA.wait_for_required_libraries, 500 ); // 0.5 sec.
}
return;
}
if ( LAPI.Browser && LAPI.Browser.is_opera && !LAPI.Browser.is_opera_ge_9 ) { return; } // Opera 8 has severe problems
// Get the UI. We're likely to need it if we made it to here.
IA.setup_ui();
IA.setup();
},
setup: function () {
var self = IA;
self.imgs = [];
// Catch both native RTL and "faked" RTL through [[MediaWiki:Rtl.js]]
self.is_rtl =
LAPI.DOM.hasClass( document.body, 'rtl' ) ||
(
LAPI.DOM.currentStyle && // Paranoia: added recently, not everyone might have it
LAPI.DOM.currentStyle( document.body, 'direction' ) == 'rtl'
);
var stylepath = mw.config.get( 'stylepath' ) || '/skin';
// Use this to temporarily display an image off-screen to get its dimensions
var testImgDiv = LAPI.make(
'div', null,
{
display: 'none', position: 'absolute', width: '300px',
overflow: 'hidden', overflowX: 'hidden', left: '-10000px'
}
);
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 );
// Don't do anything on extremely small previews. We need some minimum extent to be able to place
// rectangles after all...
if ( !srcW || !srcH || srcW < 20 || srcH < 20 ) { return null; }
// For non-thumbnail images, the size limit is larger.
if ( is_other && ( srcW < 60 || srcH < 60 ) ) { return null; }
var w = img.clientWidth; // Don't use offsetWidth, thumbnails may have a boundary...
var h = img.clientHeight;
// If the image is currently hidden, its clientWidth and clientHeight are not set. Try
// harder to get the true width and height:
if ( ( !w || !h ) && img.style.display != 'none' ) {
var copied = img.cloneNode( true );
copied.style.display = '';
testImgDiv.appendChild( copied );
testImgDiv.style.display = '';
w = copied.clientWidth;
h = copied.clientHeight;
testImgDiv.style.display = 'none';
LAPI.DOM.removeNode( copied );
}
// Quit if the image wasn't loaded properly for some reason:
if ( w != srcW || h != srcH ) { return null; }
// Exclude system images
if ( img.src.contains( stylepath ) ) { return null; }
// Only if within a link
if ( img && img.parentNode && img.parentNode.nodeName.toLowerCase() != 'a' ) { return null; }
if ( is_other ) {
// Only if the img-within-link construction is within some element that may contain a div!
if ( /^(p|span)$/i.test( img.parentNode && img.parentNode.parentNode.nodeName ) ) {
// Special case: a paragraph may contain only inline elements, but we want to be able to handle
// files in single paragraphs. Maybe we need to properly split the paragraph and wrap the image
// in a div, but for now we assume that all browsers can handle a div within a paragraph or
// a span in a meaningful way, even if that is not really allowed.
} else if ( !/^(object|applet|map|fieldset|noscript|iframe|body|div|li|dd|blockquote|center|ins|del|button|th|td|form)$/i.test( img.parentNode && img.parentNode.parentNode.nodeName ) ) { return null; }
}
// Exclude any that are within an image note!
var up = img.parentNode && img.parentNode.parentNode;
while ( up && up != document.body ) {
if ( LAPI.DOM.hasClass( up, IA.annotation_class ) ) { return null; }
up = up.parentNode;
}
return { width: w, height: h };
}
function setup_one( scope ) {
var file_div = scope;
var is_thumb =
scope != document &&
scope.nodeName.toLowerCase() == 'div' &&
LAPI.DOM.hasClass( scope, 'thumbinner' );
var is_other = scope.nodeName.toLowerCase() == 'img';
if ( is_other && self.imgs.length && scope == self.imgs[ 0 ] ) { return null; }
if ( scope == document ) {
file_div = LAPI.$( 'file' );
} else if ( !is_thumb && !is_other ) {
file_div = getElementsByClassName( scope, 'div', 'wpImageAnnotatorFile' );
if ( !file_div || file_div.length != 1 ) { return null; }
file_div = file_div[ 0 ];
}
if ( !file_div ) { return null; }
var img = null;
if ( scope == document ) {
img = LAPI.WP.getPreviewImage( mw.config.get( 'wgTitle' ) );
// TIFFs may be multi-paged: allow only for single-page TIFFs
if ( document.pageselector ) { img = null; }
} else if ( is_other ) {
img = scope;
} else {
img = file_div.getElementsByTagName( 'img' );
if ( !img || img.length === 0 ) { return null; }
img = img[ 0 ];
}
if ( !img ) { return null; }
var dim = img_check( img, is_other );
if ( !dim ) { return null; }
// Conditionally exclude shared images.
if (
scope != document &&
!self.rules.shared.show &&
ImageAnnotator_config.imageIsFromSharedRepository( img.src )
) { return null; }
var name = null;
if ( scope == document ) {
name = mw.config.get( 'wgPageName' );
} else {
name = LAPI.WP.pageFromLink( img.parentNode );
if ( !name ) { return null; }
name = name.replace( / /g, '_' );
if ( is_thumb || is_other ) {
var img_src = decodeURIComponent( img.getAttribute( 'src', 2 ) ).replace( / /g, '_' );
// img_src should have a component "/name" in there somewhere
var colon = name.indexOf( ':' );
if ( colon <= 0 ) { return null; }
var img_name = name.substring( colon + 1 );
if ( img_src.search( new RegExp( '/' + img_name.escapeRE() + '(/.*)?$' ) ) < 0 ) { return null; }
// If the link is not going to file namespace, we won't find the full size later on and
// thus we won't do anything with it.
}
}
if ( name.search( /\.(jpe?g|png|gif|svg|tiff?|webp)$/i ) < 0 ) { return null; } // Only PNG, JPE?G, GIF, SVG, TIFF?, and WebP
// Finally check for wpImageAnnotatorControl
var icon_only = false;
var no_caption = false;
if ( is_thumb || is_other ) {
var up = img.parentNode && img.parentNode.parentNode;
// Three levels is sufficient: thumbinner-thumb-control, or floatnone-center-control, or direct
for ( var i = 0; ++i <= 3 && up; up = up.parentNode ) {
if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorControl' ) ) {
if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorOff' ) ) { return null; }
if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorIconOnly' ) ) { icon_only = true; }
if ( LAPI.DOM.hasClass( up, 'wpImageAnnotatorCaptionOff' ) ) { no_caption = true; }
break;
}
}
}
return { scope: scope,
file_div: file_div,
img: img,
realName: name,
isThumbnail: is_thumb,
isOther: is_other,
thumb: { width: dim.width, height: dim.height },
iconOnly: icon_only,
noCaption: no_caption
};
}
function setup_images( list ) {
Array.forEach( list,
function ( elem ) {
var desc = setup_one( elem );
if ( desc ) { self.imgs[ self.imgs.length ] = desc; }
}
);
}
if ( mw.config.get( 'wgNamespaceNumber' ) == 6 ) {
setup_images( [ document ] );
self.may_edit = self.may_edit && ( self.imgs.length == 1 );
setup_images( self.imgs_with_notes );
} else {
setup_images( self.imgs_with_notes );
self.may_edit = self.may_edit && ( self.imgs.length == 1 );
}
self.may_edit = self.may_edit && location.href.search( /[?&]oldid=/ ) < 0;
if ( self.haveAjax ) {
setup_images( self.thumbs );
setup_images( self.other_images );
}
// Remove the test div
LAPI.DOM.removeNode( testImgDiv );
if ( self.imgs.length === 0 ) { return; }
// We get the UI texts in parallel, but wait for them at the beginning of complete_setup, where we
// need them. This has in particular a benefit if we do have to query for the file sizes below.
if ( self.imgs.length == 1 && self.imgs[ 0 ].scope == document && !self.haveAjax ) {
// Try to get the full size without Ajax.
self.imgs[ 0 ].full_img = LAPI.WP.fullImageSizeFromPage();
if ( self.imgs[ 0 ].full_img.width > 0 && self.imgs[ 0 ].full_img.height > 0 ) {
self.setup_step_two();
return;
}
}
// Get the full sizes of all the images. If more than 50, make several calls. (The API has limits.)
// Also avoid using Ajax on IE6...
var cache = {};
var names = [];
Array.forEach( self.imgs, function ( img, idx ) {
if ( cache[ img.realName ] ) {
cache[ img.realName ][ cache[ img.realName ].length ] = idx;
} else {
cache[ img.realName ] = [ idx ];
names[ names.length ] = img.realName;
}
} );
var to_do = names.length;
var done = 0;
function check_done( length ) {
done += length;
if ( done >= names.length ) {
if ( typeof ImageAnnotator.info_callbacks !== 'undefined' ) { ImageAnnotator.info_callbacks = null; }
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 && ( text.length + new_text.length + 1 > url_limit ) ) { break; }
}
text += ( text.length ? '|' : '' ) + 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 || mw.config.get( 'wgNamespaceNumber' ) != 6 ) { return; }
// Care about the protection settings
var protection = Array.any( info.protection, function ( e ) {
return ( e.type == 'edit' ? e : null );
} );
self.may_edit =
!protection ||
( mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( protection.level ) );
}
);
}
for ( var page in json.query.pages ) {
get_size( json.query.pages[ page ] );
}
} // end if
} catch ( ex ) {
}
}
if ( ( !window.XMLHttpRequest && !!window.ActiveXObject ) || !self.haveAjax ) {
// IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that
// prompt by using getScript instead of parseWikitext in this case.
ImageAnnotator.info_callbacks = [];
var template = mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php?action=query&format=json' +
'&prop=info|imageinfo&inprop=protection&iiprop=size' +
'&titles=&callback=ImageAnnotator.info_callbacks[].callback';
if ( template.startsWith( '//' ) ) { template = document.location.protocol + template; } // Avoid protocol-relative URIs (IE7 bug)
make_calls(
function ( length, titles ) {
var idx = ImageAnnotator.info_callbacks.length;
ImageAnnotator.info_callbacks[ idx ] = {
callback: function ( json ) {
set_info( json );
ImageAnnotator.info_callbacks[ idx ].done = true;
if ( ImageAnnotator.info_callbacks[ idx ].script ) {
LAPI.DOM.removeNode( ImageAnnotator.info_callbacks[ idx ].script );
ImageAnnotator.info_callbacks[ idx ].script = null;
}
check_done( length );
},
done: false
};
ImageAnnotator.info_callbacks[ idx ].script = IA.getScript(
template.replace( 'info_callbacks[].callback', 'info_callbacks[' + idx + '].callback' )
.replace( '&titles=&', '&titles=' + titles + '&' ),
true // No local caching!
);
// We do bypass the local JavaScript cache of importScriptURI, but on IE, we still may
// get the script from the browser's cache, and if that happens, IE may execute the
// script (and call the callback) synchronously before the assignment is done. Clean
// up in that case.
if (
ImageAnnotator.info_callbacks && ImageAnnotator.info_callbacks[ idx ] &&
ImageAnnotator.info_callbacks[ idx ].done && ImageAnnotator.info_callbacks[ idx ].script
) {
LAPI.DOM.removeNode( ImageAnnotator.info_callbacks[ idx ].script );
ImageAnnotator.info_callbacks[ idx ].script = null;
}
},
( LAPI.Browser.is_ie ? 1950 : 4000 ) - template.length // Some slack for caching parameters
);
} else {
make_calls(
function ( length, titles ) {
LAPI.Ajax.apiGet(
'query'
, { titles: titles,
prop: 'info|imageinfo',
inprop: 'protection',
iiprop: 'size'
}
, function ( request, json_result ) {
set_info( json_result );
check_done( length );
}
, function () { check_done( length ); }
);
}
);
} // end if can use Ajax
},
setup_ui: function () {
// Complete the UI object we've gotten from config.
ImageAnnotator.UI.ready = false;
ImageAnnotator.UI.repo = null;
ImageAnnotator.UI.needs_plea = false;
var readyEvent = [];
ImageAnnotator.UI.fireReadyEvent = function () {
if ( ImageAnnotator.UI.ready ) { return; } // Already fired, nothing to do.
ImageAnnotator.UI.ready = true;
// Call all registered handlers, and clear the array.
Array.forEach( readyEvent, function ( f, idx ) {
try { f(); } catch ( ex ) {}
readyEvent[ idx ] = null;
} );
readyEvent = null;
};
ImageAnnotator.UI.addReadyEventHandler = function ( f ) {
if ( ImageAnnotator.UI.ready ) {
f(); // Already fired: call directly
} else {
readyEvent[ readyEvent.length ] = f;
}
};
ImageAnnotator.UI.setup = function () {
if ( ImageAnnotator.UI.repo ) { return; }
var self = ImageAnnotator.UI;
var node = LAPI.make( 'div', null, { display: 'none' } );
document.body.appendChild( node );
if ( typeof UIElements === 'undefined' ) {
self.basic = true;
self.repo = {};
for ( var item in self.defaults ) {
node.innerHTML = self.defaults[ item ];
self.repo[ item ] = node.firstChild;
LAPI.DOM.removeChildren( node );
}
} else {
self.basic = false;
self.repo = UIElements.emptyRepository( self.defaultLanguage );
for ( var item in self.defaults ) {
node.innerHTML = self.defaults[ item ];
UIElements.setEntry( item, self.repo, node.firstChild );
LAPI.DOM.removeChildren( node );
}
UIElements.load( 'wpImageAnnotatorTexts', null, null, self.repo );
}
LAPI.DOM.removeNode( node );
};
ImageAnnotator.UI.get = function ( id, basic, no_plea ) {
var self = ImageAnnotator.UI;
if ( !self.repo ) { self.setup(); }
var result = null;
var add_plea = false;
if ( self.basic ) {
result = self.repo[ id ];
} else {
result = UIElements.getEntry( id, self.repo, mw.config.get( 'wgUserLanguage' ), null );
add_plea = !result;
if ( !result ) { result = UIElements.getEntry( id, self.repo ); }
}
self.needs_plea = add_plea;
if ( !result ) { return null; } // Hmmm... what happened here? We normally have defaults...
if ( basic ) { return LAPI.DOM.getInnerText( result ).trim(); }
result = result.cloneNode( true );
if ( mw.config.get( 'wgServer' ).contains( '/commons' ) && add_plea && !no_plea ) {
// Add a translation plea.
if ( result.nodeName.toLowerCase() == 'div' ) {
result.appendChild( self.get_plea() );
} else {
var span = LAPI.make( 'span' );
span.appendChild( result );
span.appendChild( self.get_plea() );
result = span;
}
}
return result;
};
ImageAnnotator.UI.get_plea = function () {
var self = ImageAnnotator.UI;
var translate = self.get( 'wpTranslate', false, true ) || 'translate';
var span = LAPI.make( 'small' );
span.appendChild( document.createTextNode( '\xa0(' ) );
span.appendChild(
LAPI.DOM.makeLink(
mw.config.get( 'wgServer' ) + mw.config.get( 'wgScript' ) + '?title=MediaWiki_talk:ImageAnnotatorTexts' +
'&action=edit§ion=new&withJS=MediaWiki:ImageAnnotatorTranslator.js' +
'&language=' + mw.config.get( 'wgUserLanguage' ),
translate,
( typeof translate === 'string' ? translate : LAPI.DOM.getInnerText( translate ).trim() )
)
);
span.appendChild( document.createTextNode( ')' ) );
return span;
};
ImageAnnotator.UI.init = function ( html_text_or_json ) {
var text;
if ( typeof html_text_or_json === 'string' ) {
text = html_text_or_json;
} else if (
typeof html_text_or_json !== 'undefined' &&
typeof html_text_or_json.parse !== 'undefined' &&
typeof html_text_or_json.parse.text !== 'undefined' &&
typeof html_text_or_json.parse.text[ '*' ] !== 'undefined'
) {
text = html_text_or_json.parse.text[ '*' ];
} else {
text = null;
}
if ( !text ) {
ImageAnnotator.UI.fireReadyEvent();
return;
}
var node = LAPI.make( 'div', null, { display: 'none' } );
document.body.appendChild( node );
try {
node.innerHTML = text;
} catch ( ex ) {
LAPI.DOM.removeNode( node );
node = null;
// Swallow. We'll just work with the default UI
}
if ( node && !ImageAnnotator.UI.repo ) { ImageAnnotator.UI.setup(); }
ImageAnnotator.UI.fireReadyEvent();
};
var ui_page = '{{MediaWiki:ImageAnnotatorTexts' +
( mw.config.get( 'wgUserLanguage' ) != mw.config.get( 'wgContentLanguage' ) ? '|lang=' + mw.config.get( 'wgUserLanguage' ) : '' ) +
'|live=1}}';
function get_ui_no_ajax() {
var url =
mw.config.get( 'wgServer' ) + mw.config.get( 'wgScriptPath' ) + '/api.php?action=parse&pst&text=' +
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.)
IA.getScript( url, true ); // No local caching!
}
function get_ui() {
IA.haveAjax = ( LAPI.Ajax.getRequest() != null );
IA.ajaxQueried = true;
// Works only with Ajax (but then, most of this script doesn't work without).
// Check what this does to load times... If lots of people used this, it might be better to
// have the UI texts included in some footer as we did on Special:Upload. True, everybody
// would get the texts, even people not using this, but the texts are small anyway...
if ( !IA.haveAjax || !LAPI.Ajax.parseWikitext ) {
get_ui_no_ajax(); // Fallback.
return;
}
LAPI.Ajax.parseWikitext(
ui_page,
ImageAnnotator.UI.init,
ImageAnnotator.UI.fireReadyEvent,
false,
null,
'API', // A fixed string to enable caching at all.
14400 // 4 hour caching.
);
} // end get_ui
if ( !window.XMLHttpRequest && !!window.ActiveXObject ) {
// IE has a stupid security setting asking whether ActiveX should be allowed. We avoid that
// prompt by using getScript instead of parseWikitext in this case. The disadvantage
// is that we don't do anything if this fails for some reason.
get_ui_no_ajax();
} else {
get_ui();
}
},
setup_step_two: function () {
var self = IA;
// Throw out any images for which we miss either the thumbnail or the full image size.
// Also throws out thumbnails that are larger than the full image.
self.imgs = Array.select( self.imgs, function ( elem, idx ) {
var result =
elem.thumb.width > 0 && elem.thumb.height > 0 &&
typeof elem.full_img !== 'undefined' &&
elem.full_img.width > 0 && elem.full_img.height > 0 &&
elem.full_img.width >= elem.thumb.width &&
elem.full_img.height >= elem.thumb.height;
if ( self.may_edit && idx === 0 && !result ) { self.may_edit = false; }
return result;
} );
if ( self.imgs.length === 0 ) { return; }
ImageAnnotator.UI.addReadyEventHandler( IA.complete_setup );
},
complete_setup: function () {
// We can be sure to have the UI here because this is called only when the ready event of the
// UI object is fired.
var self = IA;
// Check edit permissions
if ( self.may_edit && mw.config.get( 'wgRestrictionEdit' ) ) {
self.may_edit = (
( mw.config.get( 'wgRestrictionEdit' ).length === 0 || mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( 'sysop' ) ) ||
(
mw.config.get( 'wgRestrictionEdit' ).length === 1 && mw.config.get( 'wgRestrictionEdit' )[ 0 ] === 'autoconfirmed' &&
mw.config.get( 'wgUserGroups' ) && mw.config.get( 'wgUserGroups' ).join( ' ' ).contains( 'confirmed' ) // confirmed & autoconfirmed
)
);
}
if ( self.may_edit ) {
// Check whether the image is local. Don't allow editing if the file is remote.
var sharedUpload = getElementsByClassName( document, 'div', 'sharedUploadNotice' );
self.may_edit = ( !sharedUpload || sharedUpload.length === 0 );
}
if ( self.may_edit && mw.config.get( 'wgNamespaceNumber' ) != 6 ) {
// Only allow edits if the stored page name matches the current one.
var img_page_name = getElementsByClassName( self.imgs[ 0 ].scope, '*', 'wpImageAnnotatorPageName' );
if ( img_page_name && img_page_name.length ) { img_page_name = LAPI.DOM.getInnerText( img_page_name[ 0 ] ); } else { img_page_name = ''; }
self.may_edit = ( img_page_name.replace( / /g, '_' ) == mw.config.get( 'wgTitle' ).replace( / /g, '_' ) );
}
if ( self.may_edit && self.ajaxQueried ) { self.may_edit = self.haveAjax; }
// Now create viewers for all images
self.viewers = new Array( self.imgs.length );
for ( var i = 0; i < self.imgs.length; i++ ) {
self.viewers[ i ] = new ImageNotesViewer( self.imgs[ i ], i === 0 && self.may_edit );
}
if ( self.may_edit ) {
// Respect user override for zoom, if any
self.zoom_threshold = ImageAnnotator_config.zoom_threshold;
if (
typeof window.ImageAnnotator_zoom_threshold !== 'undefined' &&
!isNaN( window.ImageAnnotator_zoom_threshold ) &&
window.ImageAnnotator_zoom_threshold >= 0.0
) {
// If somebody sets it to a nonsensical high value, that's his or her problem: there won't be any
// zooming.
self.zoom_threshold = window.ImageAnnotator_zoom_threshold;
}
// Adapt zoom threshold for small thumbnails or images with a very lopsided width/height ratio,
// but only if we *can* zoom at least twice
if (
self.viewers[ 0 ].full_img.width > 300 &&
Math.min( self.viewers[ 0 ].factors.dx, self.viewers[ 0 ].factors.dy ) >= 2.0
) {
if (
self.viewers[ 0 ].thumb.width < 400 ||
self.viewers[ 0 ].thumb.width / self.viewers[ 0 ].thumb.height > 2.0 ||
self.viewers[ 0 ].thumb.height / self.viewers[ 0 ].thumb.width > 2.0
) {
self.zoom_threshold = 0; // Force zooming
}
}
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(!mouse_pos) return;
if ( !LAPI.Pos.isWithin( self.cover, mouse_pos.x, mouse_pos.y ) ) { return; }
var origin = LAPI.Pos.position( self.cover );
// Make mouse pos relative to cover
mouse_pos.x = mouse_pos.x - origin.x;
mouse_pos.y = mouse_pos.y - origin.y;
if ( mouse_pos.x >= self.base_x ) {
self.definer.style.width = String( mouse_pos.x - self.base_x ) + 'px';
self.definer.style.left = String( self.base_x ) + 'px';
} else {
self.definer.style.width = String( self.base_x - mouse_pos.x ) + 'px';
self.definer.style.left = String( mouse_pos.x ) + 'px';
}
if ( mouse_pos.y >= self.base_y ) {
self.definer.style.height = String( mouse_pos.y - self.base_y ) + 'px';
self.definer.style.top = String( self.base_y ) + 'px';
} else {
self.definer.style.height = String( self.base_y - mouse_pos.y ) + 'px';
self.definer.style.top = String( mouse_pos.y ) + 'px';
}
return LAPI.Evt.kill( evt );
}
function pause( evt ) {
LAPI.Evt.remove( document, 'mousemove', track, true );
if ( !LAPI.Browser.is_ie && typeof document.captureEvents === 'function' ) { document.captureEvents( null ); }
self.move_listening = false;
}
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: String( self.base_x ) + 'px',
top: String( self.base_y ) + 'px',
width: '0px',
height: '0px',
display: ''
}
, self.definer.style
);
// Set mouse handlers
LAPI.Evt.remove( self.cover, 'mousedown', start_tracking );
if ( LAPI.Browser.is_ie ) {
LAPI.Evt.attach( document, 'mouseup', stop_tracking ); // Doesn't work properly on self.cover...
} else {
LAPI.Evt.attach( self.cover, 'mouseup', stop_tracking );
}
resume();
LAPI.Evt.attach( window, 'blur', pause );
LAPI.Evt.attach( window, 'focus', resume );
}
if ( evt ) { return LAPI.Evt.kill( evt ); }
return false;
}
function add_new( evt ) {
if ( !self.canEdit() ) { return; }
self.editor.hide_editor();
Tooltips.close();
var cover = self.get_cover();
cover.style.cursor = 'crosshair';
self.definer = LAPI.make(
'div', null,
{
border: '1px solid ' + IA.new_border,
display: 'none',
position: 'absolute',
top: '0px',
left: '0px',
width: '0px',
height: '0px',
padding: '0',
lineHeight: '0px', // IE needs this, even though there are no lines within
fontSize: '0px', // IE
zIndex: cover.style.zIndex - 2 // Below the mouse capture div
}
);
self.viewers[ 0 ].img_div.appendChild( self.definer );
// Enter mouse-tracking mode to define extent of view. Mouse cursor is outside of image,
// hence none of our tooltips are visible.
self.viewers[ 0 ].img_div.appendChild( self.border );
self.show_cover();
self.is_tracking = false;
self.is_adding = true;
LAPI.Evt.attach( cover, 'mousedown', start_tracking );
resume();
self.button_div.style.display = 'none';
// Remove the event listeners on the image: IE sometimes invokes them even when the image is covered
self.viewers[ 0 ].setShowHideEvents( false );
self.viewers[ 0 ].hide(); // Make sure notes are hidden
self.viewers[ 0 ].toggle( true ); // Show all note rectangles (but only the dummies)
self.update_zoom_evt = LAPI.Evt.makeListener( self, self.update_zoom );
self.hide_zoom_evt = LAPI.Evt.makeListener( self, self.hide_zoom );
self.show_zoom();
LAPI.Evt.attach( cover, self.mouse_in, self.update_zoom_evt );
LAPI.Evt.attach( cover, self.mouse_out, self.hide_zoom_evt );
LAPI.DOM.removeChildren( self.viewers[ 0 ].msg );
self.viewers[ 0 ].msg.appendChild( ImageAnnotator.UI.get( 'wpImageAnnotatorDrawRectMsg', false ) );
self.viewers[ 0 ].msg.style.display = '';
}
self.button_div = LAPI.make( 'div' );
self.viewers[ 0 ].main_div.appendChild( self.button_div );
self.add_button = LAPI.DOM.makeButton(
'ImageAnnotationAddButton',
ImageAnnotator.UI.get( 'wpImageAnnotatorAddButtonText', true ),
add_new
);
var add_plea = ImageAnnotator.UI.needs_plea;
self.button_div.appendChild( self.add_button );
self.help_link = self.createHelpLink();
if ( self.help_link ) {
self.button_div.appendChild( document.createTextNode( '\xa0' ) );
self.button_div.appendChild( self.help_link );
}
if ( add_plea && mw.config.get( 'wgServer' ).contains( '/commons' ) ) { self.button_div.appendChild( ImageAnnotator.UI.get_plea() ); }
} // end if may_edit
// Get the file description pages of thumbnails. Figure out for which viewers we need to do this.
var cache = {};
var get_local = [];
var get_foreign = [];
Array.forEach( self.viewers, function ( viewer, idx ) {
if ( viewer.setup_done || viewer.isLocal && !viewer.has_page ) { return; }
// Handle only images that either are foreign or local and do have a page.
if ( cache[ viewer.realName ] ) {
cache[ viewer.realName ][ cache[ viewer.realName ].length ] = idx;
} else {
cache[ viewer.realName ] = [ idx ];
if ( !viewer.has_page ) {
get_foreign[ get_foreign.length ] = viewer.realName;
} else {
get_local[ get_local.length ] = viewer.realName;
}
}
} );
if ( get_local.length === 0 && get_foreign.length === 0 ) { return; }
// Now we have unique page names in the cache and in to_get. Go get the corresponding file
// description pages. We make a series of simultaneous asynchronous calls to avoid hitting
// API limits and to keep the URL length below the limit for the foreign_repo calls.
function make_calls( list, execute_call, url_limit ) {
function composer( list, from, length, url_limit ) {
function compose( list, from, length, url_limit ) {
var text = '';
var done = 0;
for ( var i = from; i < from + length; i++ ) {
var new_text =
'' +
'' + list[ i ] + '' +
'{{:' + list[ i ] + '}}' + // Leading dot to avoid getting links to the full images if we hit a parser limit
'
';
if ( url_limit ) {
new_text = encodeURIComponent( new_text );
if ( text.length && ( text.length + new_text.length > url_limit ) ) { break; }
}
text = text + new_text;
done++;
// Additionally, limit the number of image pages to get: these can be large, and the server
// may refuse to actually do the transclusions but may in that case include the full images
// in the result, which would make us load the full images, which is desastrous if there are
// many thumbs to large images on the page.
if ( done == 5 ) { break; }
}
return { text: text, n: done };
}
var param = compose( list, from, length, url_limit );
execute_call( param.text );
return param.n;
}
var start = 0, chunk = 0, to_do = list.length;
while ( to_do > 0 ) {
chunk = composer( list, start, Math.min( 50, to_do ), url_limit );
to_do -= chunk;
start += chunk;
}
}
var divRE = /(<\s*div\b)|(<\/\s*div\s*>)/ig;
var blockStart = '/g;
// Our parse request returns the full html of the description pages' contents, including any
// license or information or other templates that we don't care about, and which may contain
// additional images we'd rather not load when we add this (X)HTML to the DOM. Therefore, we
// strip out everything but the notes.
function strip_noise( html ) {
var result = '';
var m;
// First, get rid of HTML comments and scripts
html = html.replace( //g, '' ).replace( /
'; } // Missing ends.
idx = k;
} // end loop collecting notes
result += '