MediaWiki:Imagemap-Highlight.js: Difference between revisions

From ArdaCraft Wiki
Jump to navigation Jump to search
(Created page with " $(document).ready(function() { var //add this class to all elements created by the script. the reason is that we call the script again on //window resize, and use the class to remove all the "artefacts" we created in the previous run. myClassName = 'imageMapHighlighterArtefacts' , liHighlightClass = 'liHighlighting' , specialAreaMark = 'area_mark' , specialLiClassesMark = 'list_classes' // "2d context" attributes used for highlighting. , areaHighLighting...")
 
No edit summary
 
Line 1: Line 1:
function imagehighlight(startcollapsed) {


$(document).ready(function() {
var
//add this class to all elements created by the script. the reason is that we call the script again on
//window resize, and use the class to remove all the "artefacts" we created in the previous run.
myClassName = 'imageMapHighlighterArtefacts',
liHighlightClass = 'liHighlighting',
specialAreaMark = 'area_mark',
specialLiClassesMark = 'list_classes',
specialAreaMarkFile = 'area_mark_file',
// "2d context" attributes used for highlighting.
areaHighLighting = {
fillStyle: 'rgba(0,0,0,0.35)',
strokeStyle: 'yellow',
lineJoin: 'round',
lineWidth: 2
},
//every imagemap that wants highlighting, should reside in a div of this 'class':
hilightDivMarker = '.imageMapHighlighter',
// specifically for wikis - redlinks tooltip adds this message
ru = mw && mw.config && mw.config.get('wgUserLanguage') == 'ru',
expandLegend = ru ? 'показать ссылки текстом' : 'show links as text',
collapseLegend = ru ? 'скрыть ссылки текстом' : 'hide links as text',
files = [],
curimage,
localplace,
imagehighlighted = false;


    var
//add this class to all elements created by the script. the reason is that we call the script again on
//window resize, and use the class to remove all the "artefacts" we created in the previous run.
myClassName = 'imageMapHighlighterArtefacts'
, liHighlightClass = 'liHighlighting'
, specialAreaMark = 'area_mark'
, specialLiClassesMark = 'list_classes'
// "2d context" attributes used for highlighting.
, areaHighLighting = {fillStyle: 'rgba(0,0,0,0.35)', strokeStyle: 'yellow', lineJoin: 'round', lineWidth: 2}
//every imagemap that wants highlighting, should reside in a div of this 'class':
, hilightDivMarker = '.imageMapHighlighter'
// specifically for wikis - redlinks tooltip adds this message
, he = mw && mw.config && mw.config.get('wgUserLanguage') == 'he'
, expandLegend = he ? 'הצגת מקרא' : 'ּShow Legend'
, collapseLegend = he ? 'הסתרת המקרא' : 'Hide Legend'
;


 
function drawMarker(context, areas, fromMap) { // this is where the magic is done.
function drawMarker(context, areas) { // mthis is where the magic is done.


function drawPoly(coords) {
function drawPoly(coords) {
Line 28: Line 36:
}
}


function getminmax(array, divheight, scrolltop) {
if (array.length < 2)
return [Infinity, -Infinity, false];
var prev = getminmax(array.slice(2), divheight, scrolltop),
curpos = array[1] - scrolltop;
return [Math.min(array[1], prev[0]), Math.max(array[1], prev[1]),
prev[2] || ((curpos >= 15) && (curpos <= divheight - 15))
];
}
var ycoord, scroll, scrolltop, box, globally, locally,
minmax = {},
imagediv = curimage.parent(),
divheight = imagediv.height();
if ((scroll = !fromMap && imagediv[0].scrollHeight > imagediv[0].clientHeight))
scrolltop = imagediv.scrollTop();
for (var i in areas) {
for (var i in areas) {
var coords = areas[i].coords.split(',');
var coords = areas[i].coords.split(',');
if (scroll && (box = getminmax(coords, divheight, scrolltop)))
minmax[areas[i].title == localplace ? 'locally' : 'globally'] = box;
context.beginPath();
context.beginPath();
switch (areas[i].shape) {
switch (areas[i].shape) {
case 'rect': drawPoly([coords[0], coords[1], coords[0], coords[3], coords[2], coords[3], coords[2], coords[1]]); break;
case 'rect':
case 'circle': context.arc(coords[0],coords[1],coords[2],0,Math.PI*2); break;//x,y,r,startAngle,endAngle
drawPoly([coords[0], coords[1], coords[0], coords[3], coords[2], coords[
case 'poly': drawPoly(coords); break;
3], coords[2], coords[1]]);
break;
case 'circle':
context.arc(coords[0], coords[1], coords[2], 0, Math.PI * 2);
break; //x,y,r,startAngle,endAngle
case 'poly':
drawPoly(coords);
break;
}
}
context.closePath();
context.closePath();
context.stroke();
context.stroke();
context.fill();
context.fill();
}
if (scroll) {
if ((box = minmax.globally) && !box[2] && ((ycoord = Math.floor((box[0] + box[1]) /
2)) < scrolltop + 15 || ycoord > scrolltop + divheight - 15))
globally = ycoord - Math.floor(divheight / 2);
else if (!box && (locally = minmax.locally) && !locally[2] && ((ycoord = Math.floor((locally[0] + locally[1]) /
2)) < scrolltop + 15 || ycoord > scrolltop + divheight - 15))
locally = ycoord - Math.floor(divheight / 2);
if (globally || locally || box) {
return [globally, locally, !!box, imagediv];
}
}
}
}
}
Line 48: Line 92:
ol = $this.parent(),
ol = $this.parent(),
context = ol.data('context'),
context = ol.data('context'),
special = ol.data(specialAreaMark);
special = ol.data(specialAreaMark),
specialFile = ol.data(specialAreaMarkFile); //read JSON file addition
 
if (specialFile) {
if (files[specialFile]) {
$.extend(special, files[specialFile]);
always(activate, caption, context, ol, special, $this, fromMap);
} else {
new mw.Api().get({
action: 'expandtemplates',
text: '{{' + specialFile + '}}',
prop: 'wikitext',
format: 'json',
formatversion: 2
})
.done(function(data) {
files[specialFile] = JSON.parse(data.expandtemplates.wikitext);
$.extend(special, files[specialFile]);
})
.always(function() {
always(activate, caption, context, ol, special, $this, fromMap);
});
}
} else
always(activate, caption, context, ol, special, $this, fromMap);
}
 
function always(activate, caption, context, ol, special, $this, fromMap) {
var localstep, globalstep, box, imagediv, globally, locally,
globalcond = false;
 
$this.toggleClass(liHighlightClass, activate); // mark/unmark the list item.  
$this.toggleClass(liHighlightClass, activate); // mark/unmark the list item.  
 
context.clearRect(0, 0, context.canvas.width, context.canvas.height); // prepare for a new day.
context.clearRect(0, 0, context.canvas.width, context.canvas.height); // prepare for a new day.
 
ol.find('li').each(function() {
ol.find('li')
var $li = $(this);
.each(function() {
var licap = $li.text();
var $li = $(this);
var param;
var licap = $li.text();
if (activate && licap === caption) { // highlight!!!
var param;
param = special && special.hover || areaHighLighting;
if (activate && licap === caption) { // highlight!!!
} else {
param = special && (special.hover && special.hover[licap] ||
param = special && special.nover && (special.nover[licap] || special.nover.default);
getblocks(special, licap)) || areaHighLighting;
}
} else {
if (param) {
param = special && special.nover && (special.nover[licap] || special.nover
$.extend(context, param);
.default);
drawMarker(context, $li.data('areas'));
}
}
if (param) {
});
$.extend(context, param);
if ((box = drawMarker(context, $li.data('areas'), fromMap))) {
[box, localstep, globalstep, imagediv] = box;
globalcond = globalcond || globalstep;
globally = box || globally;
locally = (!globalcond || undefined) && localstep || locally;
}
}
});
if (!fromMap && globalcond || locally)
imagediv.stop(true);
if (!fromMap && (box = globally || locally) && typeof box === 'number')
imagediv.delay(300).animate({
scrollTop: box
}, {
easing: 'swing'
});
}
}


function getblocks(special, licap) {
if (special.hoverblocks) {
if (special.hoverblocks[licap])
return special.hoverblocks[licap].value;
for (var key in special.hoverblocks)
if (special.hoverblocks[key] && special.hoverblocks[key].list && special.hoverblocks[key].list.indexOf(licap) >=0 )
return special.hoverblocks[key].value;
}
if (special.hover)
return special.hover.default;
}
function handleOneMap() {
function handleOneMap() {
var img = $(this),  
var img = $(this),
w = img.width(),  
w = img.width(),
h = img.height(),
h = img.height(),
map = img.parent().siblings('map:first'),
infoIcon = img.next(),
dims = {position: 'absolute', width: w + 'px', height: h + 'px', border: 0, top:0, left:0},
parent = img.parent(),
specialHighlight = img.closest(hilightDivMarker).data(specialAreaMark),
map = img.siblings('map:first'),
specialLiClasses = img.closest(hilightDivMarker).data(specialLiClassesMark);
dims = {
position: 'absolute',
width: w + 'px',
if (!($('area', map).length))
height: h + 'px',
return; //not an imagemap, or map with 0 areas.
border: 0,
top: 0,
left: 0
},
specialHighlight = img.closest(hilightDivMarker)
.data(specialAreaMark),
specialLiClasses = img.closest(hilightDivMarker)
.data(specialLiClassesMark),
specialHover = img.closest(hilightDivMarker)
.data(specialAreaMarkFile);
curimage = img;
 
if (!('area', map)
.length)
return; //not an imagemap. inside "each" anonymous function, 'return' means "continue".


var jcanvas = $('<canvas>', {'class': myClassName})
var jcanvas = $('<canvas>', {
'class': myClassName
})
.css(dims)
.css(dims)
.attr({width: w, height: h});
.attr({
var bgimg = $('<img>', {'class': myClassName, src: img.attr('src')})
width: w,
.css(dims);//completely inert image. this is what we see.
height: h
});
var bgimg = $('<img>', {
'class': myClassName,
src: img.attr('src'),
srcset: img.attr('srcset')
})
.css(dims); //completely inert image. this is what we see.
var context = $.extend(jcanvas[0].getContext("2d"), areaHighLighting);
var context = $.extend(jcanvas[0].getContext("2d"), areaHighLighting);
 
// this is where the magic is done: prepare a sandwich of the inert bgimg at the bottom,
// this is where the magic is done: prepare a sandwich of the inert bgimg at the bottom,
// the canvas above it, and the original image on top,
// the canvas above it, and the original image on top,
// so canvas won't steal the mouse events.
// so canvas won't steal the mouse events.
// pack them all TIGHTLY in a newly minted "relative" div, so when page chnage
// pack them all TIGHTLY in a newly minted "relative" div, so when page chnage
// (other scripts adding elements, window resize etc.), canvas and imagese remain aligned.
// (other scripts adding elements, window resize etc.), canvas and imagese remain aligned.
var div = $('<div>').css({position: 'relative', width: w + 'px', height: h + 'px'});
var div = $('<div>')
img.before(div); // put the div just above the image, and ...
.css({
div.append(bgimg) // place the background image in the div
position: 'relative',
.append(jcanvas)// and the canvas. both are "absolute", so they don't occupy space in the div
width: w + 'px',
.append(img); // now yank the original image from the window and place it on the div.
height: h + 'px'
img.fadeTo(1, 0); // make the image transparent - we see canvas and bgimg through it.  
});
// the original, now transparent image is creating our mouse events
img.before(div); // put the div just above the image, and ...
div.append(bgimg) // place the background image in the div
var ol = $('<ol>', {'class': myClassName})
.append(jcanvas) // and the canvas. both are "absolute", so they don't occupy space in the div
.css({clear: 'both', margin: 0, listStyle: 'none', maxWidth: w + 'px', float: 'left', position: 'relative'})
.append(img); // now yank the original image from the window and place it on the div.
.attr({'data-expandtext' : expandLegend, 'data-collapsetext': collapseLegend})
img.fadeTo(1, 0); // make the image transparent - we see canvas and bgimg through it.  
// the original, now transparent image is creating our mouse events
 
infoIcon.css({
position: 'relative'
}); // set position to info icon
var ol = $('<ol>', {
'class': myClassName
})
.css({
clear: 'both',
margin: 0,
listStyle: 'none',
maxWidth: w + 'px',
position: 'relative'
})
.data(specialAreaMark, specialHighlight)
.data(specialAreaMark, specialHighlight)
.data(specialAreaMarkFile, specialHover)
.data('context', context);
.data('context', context);
var oldiv = $('<div>')
.html(ol)
.css({
clear: 'both',
margin: 0,
listStyle: 'none',
maxWidth: w + 'px',
position: 'relative'
})
.attr({
'data-expandtext': expandLegend,
'data-collapsetext': collapseLegend
});


// ol below image, hr below ol. original caption pushed below hr.
// ol below image parent, hr below ol. original caption pushed below hr.
div.after($('<hr>', {'class': myClassName}).css('clear', 'both'))
var $hr = $('<hr>', { 'class': myClassName }).css('clear', 'both');
.after(ol);
parent.after($hr).after(oldiv);
var lis = {}; //collapse areas with same caption to one list item
$hr.clone().insertBefore($(oldiv));
var lis = {}; //collapse areas with same caption to one list item
var someli; // select arbitrary one
var someli; // select arbitrary one
$('area', map).each(function() {
$('area', map)
var html = mw.html.escape(this.title);
.each(function() {
var li = lis[html]; // title already met? use the same li
var text = this.title;
if (!li) { //no? create a new one.
var li = lis[text]; // title already met? use the same li
var href = this.href;
if (!li) { //no? create a new one.
lis[html] = li = $('<li>', {'class': myClassName})
var href = this.href;
.append($('<a>', {href: href}).html(html)
lis[text] = li = $('<li>', {
.on('mouseover mouseout', mouseAction)
'class': myClassName
.data('areas', [])
})
.addClass(specialLiClasses && (specialLiClasses[html] || specialLiClasses['default']))
.append($('<a>', {
.appendTo(ol);
href: href,
}
text: text
li.data('areas').push(this); //add the area to the li
}))
someli = li; // whichever - we just want one...
.on('mouseover mouseout', mouseAction)
$(this).on('mouseover mouseout',  
.data('areas', [])
function(e) {
.addClass(specialLiClasses && (specialLiClasses[text] ||
li.trigger(e.type, true);
specialLiClasses['default']))
});
.appendTo(ol);
});
if (specialLiClasses && specialLiClasses[text + ' super'])
if (someli) someli.trigger('mouseout');
localplace = li.find('a')
mw.loader.using('jquery.makeCollapsible').then( function () {
.addClass(specialLiClasses[text + ' super']).text();
ol.addClass('mw-collapsed')
}
.makeCollapsible();
li.data('areas')
});
.push(this); //add the area to the li
someli = li; // whichever - we just want one...
$(this)
.on('mouseover mouseout', function(e) {
li.trigger(e, true);
});
});
if (specialLiClasses && specialLiClasses.order) {
specialLiClasses.order.forEach(function(elem) {
var what = $(elem.what);
if (elem.dir === 'before') {
what.each(function(inner){$(what[inner])
.insertBefore($(what[inner]).parent().find(elem.where))});
} else {
what.each(function(inner){$(what[inner])
.insertAfter($(what[inner]).parent().find(elem.where))});
}
});
}
if (specialLiClasses && specialLiClasses.todefault) {
specialLiClasses.todefault.forEach(function(elem) {
$(elem).addClass(specialLiClasses.default);
});
}
if (someli) {
someli.trigger('mouseout');
}
if (startcollapsed)
oldiv.addClass('mw-collapsed')
.makeCollapsible();
else
oldiv.makeCollapsible();
ol.attr('style', ol.attr('style')
.replace('none', 'disc'));
}
}


function init() {
function init() {
mw.util.addCSS('li.' + myClassName + '{white-space:nowrap;border:solid 1px transparent;border-radius:6px;}\n' + //css for li element
mw.util.addCSS('li.' + myClassName +
'li.' +  myClassName + '.' + liHighlightClass + '{background-color:yellow;border-color:green;}\n' + //css for highlighted li element.
'{white-space:nowrap; font-size:88.36%;}\n' + //css for li element
'.rtl li.' + myClassName + '{float: right; margin-left: 3em;}\n' +
'li.' + liHighlightClass + '{background-color:yellow;}\n' + //css for highlighted li element.
'.ltr li.' + myClassName + '{float: left; margin-right: 3em;}');
'.rtl li.' + myClassName + '{float: right; margin-left: 3px;}\n' +
$(hilightDivMarker+ ' img').each(handleOneMap);
'.ltr li.' + myClassName + '{float: left; margin-right: 3px;}\n' +
hilightDivMarker + ' .mw-collapsible-toggle {float: none}');
if (imagehighlighted)
$('.popupclass img')
.each(handleOneMap);
else
$(hilightDivMarker + ' img')
.each(handleOneMap);
imagehighlighted = true;
}
}


//has at least one "imagehighlight" div, and canvas-capable browser:
//has at least one "imagehighlight" div, and canvas-capable browser:
if ( $(hilightDivMarker).length && $('<canvas>')[0].getContext )
if ( $(hilightDivMarker).length && $('<canvas>')[0].getContext ) {
mw.loader.using( ['jquery.makeCollapsible', 'mediawiki.util'] ).done( init );
mw.loader.using(['jquery.makeCollapsible', 'mediawiki.util', 'mediawiki.api'])
});
.done(init);
}
}
 
function imagehighlightexpand() {
imagehighlight(false);
}
 
$(imagehighlight);
 
$('body').on('refresh-imagehighlight-nolinks', imagehighlight);
$('body').on('refresh-imagehighlight-links', imagehighlightexpand);

Latest revision as of 05:36, 18 April 2023

function imagehighlight(startcollapsed) {

	var
		//add this class to all elements created by the script. the reason is that we call the script again on
		//window resize, and use the class to remove all the "artefacts" we created in the previous run.
		myClassName = 'imageMapHighlighterArtefacts',
		liHighlightClass = 'liHighlighting',
		specialAreaMark = 'area_mark',
		specialLiClassesMark = 'list_classes',
		specialAreaMarkFile = 'area_mark_file',
		// "2d context" attributes used for highlighting.
		areaHighLighting = {
			fillStyle: 'rgba(0,0,0,0.35)',
			strokeStyle: 'yellow',
			lineJoin: 'round',
			lineWidth: 2
		},
		//every imagemap that wants highlighting, should reside in a div of this 'class':
		hilightDivMarker = '.imageMapHighlighter',
		// specifically for wikis - redlinks tooltip adds this message
		ru = mw && mw.config && mw.config.get('wgUserLanguage') == 'ru',
		expandLegend = ru ? 'показать ссылки текстом' : 'show links as text',
		collapseLegend = ru ? 'скрыть ссылки текстом' : 'hide links as text',
		files = [],
		curimage,
		localplace,
		imagehighlighted = false;


	function drawMarker(context, areas, fromMap) { // this is where the magic is done.

		function drawPoly(coords) {
			context.moveTo(coords.shift(), coords.shift());
			while (coords.length)
				context.lineTo(coords.shift(), coords.shift());
		}

		function getminmax(array, divheight, scrolltop) {
			if (array.length < 2)
				return [Infinity, -Infinity, false];
			var prev = getminmax(array.slice(2), divheight, scrolltop),
				curpos = array[1] - scrolltop;
			return [Math.min(array[1], prev[0]), Math.max(array[1], prev[1]),
				prev[2] || ((curpos >= 15) && (curpos <= divheight - 15))
			];
		}

		var ycoord, scroll, scrolltop, box, globally, locally,
			minmax = {},
			imagediv = curimage.parent(),
			divheight = imagediv.height();
		if ((scroll = !fromMap && imagediv[0].scrollHeight > imagediv[0].clientHeight))
			scrolltop = imagediv.scrollTop();
		for (var i in areas) {
			var coords = areas[i].coords.split(',');
			if (scroll && (box = getminmax(coords, divheight, scrolltop)))
				minmax[areas[i].title == localplace ? 'locally' : 'globally'] = box;
			context.beginPath();
			switch (areas[i].shape) {
				case 'rect':
					drawPoly([coords[0], coords[1], coords[0], coords[3], coords[2], coords[
						3], coords[2], coords[1]]);
					break;
				case 'circle':
					context.arc(coords[0], coords[1], coords[2], 0, Math.PI * 2);
					break; //x,y,r,startAngle,endAngle
				case 'poly':
					drawPoly(coords);
					break;
			}
			context.closePath();
			context.stroke();
			context.fill();
		}
		if (scroll) {
			if ((box = minmax.globally) && !box[2] && ((ycoord = Math.floor((box[0] + box[1]) /
					2)) < scrolltop + 15 || ycoord > scrolltop + divheight - 15))
				globally = ycoord - Math.floor(divheight / 2);
			else if (!box && (locally = minmax.locally) && !locally[2] && ((ycoord = Math.floor((locally[0] + locally[1]) /
					2)) < scrolltop + 15 || ycoord > scrolltop + divheight - 15))
				locally = ycoord - Math.floor(divheight / 2);
			if (globally || locally || box) {
				return [globally, locally, !!box, imagediv];
			}
		}
	}

	function mouseAction(e, fromMap) {
		var $this = $(this),
			activate = e.type == 'mouseover',
			caption = $this.text(),
			ol = $this.parent(),
			context = ol.data('context'),
			special = ol.data(specialAreaMark),
			specialFile = ol.data(specialAreaMarkFile); //read JSON file addition

		if (specialFile) {
			if (files[specialFile]) {
				$.extend(special, files[specialFile]);
				always(activate, caption, context, ol, special, $this, fromMap);
			} else {
				new mw.Api().get({
						action: 'expandtemplates',
						text: '{{' + specialFile + '}}',
						prop: 'wikitext',
						format: 'json',
						formatversion: 2
					})
					.done(function(data) {
						files[specialFile] = JSON.parse(data.expandtemplates.wikitext);
						$.extend(special, files[specialFile]);
					})
					.always(function() {
						always(activate, caption, context, ol, special, $this, fromMap);
					});
			}
		} else
			always(activate, caption, context, ol, special, $this, fromMap);
	}

	function always(activate, caption, context, ol, special, $this, fromMap) {
		var localstep, globalstep, box, imagediv, globally, locally,
			globalcond = false;

		$this.toggleClass(liHighlightClass, activate); // mark/unmark the list item. 

		context.clearRect(0, 0, context.canvas.width, context.canvas.height); // prepare for a new day.

		ol.find('li')
			.each(function() {
				var $li = $(this);
				var licap = $li.text();
				var param;
				if (activate && licap === caption) { // highlight!!!
					param = special && (special.hover && special.hover[licap] ||
						getblocks(special, licap)) || areaHighLighting;
				} else {
					param = special && special.nover && (special.nover[licap] || special.nover
						.default);
				}
				if (param) {
					$.extend(context, param);
					if ((box = drawMarker(context, $li.data('areas'), fromMap))) {
						[box, localstep, globalstep, imagediv] = box;
						globalcond = globalcond || globalstep;
						globally = box || globally;
						locally = (!globalcond || undefined) && localstep || locally;
					}
				}
			});
		if (!fromMap && globalcond || locally)
			imagediv.stop(true);
		if (!fromMap && (box = globally || locally) && typeof box === 'number')
			imagediv.delay(300).animate({
				scrollTop: box
			}, {
				easing: 'swing'
			});
	}

	function getblocks(special, licap) {
		if (special.hoverblocks) {
			if (special.hoverblocks[licap])
				return special.hoverblocks[licap].value;
			for (var key in special.hoverblocks)
				if (special.hoverblocks[key] && special.hoverblocks[key].list && special.hoverblocks[key].list.indexOf(licap) >=0 )
					return special.hoverblocks[key].value;
		}
		if (special.hover)
			return special.hover.default;
	}
	
	function handleOneMap() {
		var img = $(this),
			w = img.width(),
			h = img.height(),
			infoIcon = img.next(),
			parent = img.parent(),
			map = img.siblings('map:first'),
			dims = {
				position: 'absolute',
				width: w + 'px',
				height: h + 'px',
				border: 0,
				top: 0,
				left: 0
			},
			specialHighlight = img.closest(hilightDivMarker)
			.data(specialAreaMark),
			specialLiClasses = img.closest(hilightDivMarker)
			.data(specialLiClassesMark),
			specialHover = img.closest(hilightDivMarker)
			.data(specialAreaMarkFile);
		curimage = img;

		if (!('area', map)
			.length)
			return; //not an imagemap. inside "each" anonymous function, 'return' means "continue".

		var jcanvas = $('<canvas>', {
				'class': myClassName
			})
			.css(dims)
			.attr({
				width: w,
				height: h
			});
		var bgimg = $('<img>', {
				'class': myClassName,
				src: img.attr('src'),
				srcset: img.attr('srcset')
			})
			.css(dims); //completely inert image. this is what we see.
		var context = $.extend(jcanvas[0].getContext("2d"), areaHighLighting);

		// this is where the magic is done: prepare a sandwich of the inert bgimg at the bottom,
		// the canvas above it, and the original image on top,
		// so canvas won't steal the mouse events.
		// pack them all TIGHTLY in a newly minted "relative" div, so when page chnage
		// (other scripts adding elements, window resize etc.), canvas and imagese remain aligned.
		var div = $('<div>')
			.css({
				position: 'relative',
				width: w + 'px',
				height: h + 'px'
			});
		img.before(div); // put the div just above the image, and ...
		div.append(bgimg) // place the background image in the div
			.append(jcanvas) // and the canvas. both are "absolute", so they don't occupy space in the div
			.append(img); // now yank the original image from the window and place it on the div.
		img.fadeTo(1, 0); // make the image transparent - we see canvas and bgimg through it. 
		// the original, now transparent image is creating our mouse events

		infoIcon.css({
			position: 'relative'
		}); // set position to info icon
		var ol = $('<ol>', {
				'class': myClassName
			})
			.css({
				clear: 'both',
				margin: 0,
				listStyle: 'none',
				maxWidth: w + 'px',
				position: 'relative'
			})
			.data(specialAreaMark, specialHighlight)
			.data(specialAreaMarkFile, specialHover)
			.data('context', context);
		var oldiv = $('<div>')
			.html(ol)
			.css({
				clear: 'both',
				margin: 0,
				listStyle: 'none',
				maxWidth: w + 'px',
				position: 'relative'
			})
			.attr({
				'data-expandtext': expandLegend,
				'data-collapsetext': collapseLegend
			});

		// ol below image parent, hr below ol. original caption pushed below hr.
		var $hr = $('<hr>', { 'class': myClassName }).css('clear', 'both');
		parent.after($hr).after(oldiv);
		$hr.clone().insertBefore($(oldiv));
		
		var lis = {}; //collapse areas with same caption to one list item
		var someli; // select arbitrary one
		$('area', map)
			.each(function() {
				var text = this.title;
				var li = lis[text]; // title already met? use the same li
				if (!li) { //no? create a new one.
					var href = this.href;
					lis[text] = li = $('<li>', {
							'class': myClassName
						})
						.append($('<a>', {
							href: href,
							text: text
						}))
						.on('mouseover mouseout', mouseAction)
						.data('areas', [])
						.addClass(specialLiClasses && (specialLiClasses[text] ||
							specialLiClasses['default']))
						.appendTo(ol);
					if (specialLiClasses && specialLiClasses[text + ' super'])
						localplace = li.find('a')
							.addClass(specialLiClasses[text + ' super']).text();
				}
				li.data('areas')
					.push(this); //add the area to the li
				someli = li; // whichever - we just want one...
				$(this)
					.on('mouseover mouseout', function(e) {
						li.trigger(e, true);
					});
			});
		if (specialLiClasses && specialLiClasses.order) {
			specialLiClasses.order.forEach(function(elem) {
				var what = $(elem.what);
				if (elem.dir === 'before') {
					what.each(function(inner){$(what[inner])
						.insertBefore($(what[inner]).parent().find(elem.where))});
				} else {
					what.each(function(inner){$(what[inner])
						.insertAfter($(what[inner]).parent().find(elem.where))});
				}
			});
		}
		if (specialLiClasses && specialLiClasses.todefault) {
			specialLiClasses.todefault.forEach(function(elem) {
				$(elem).addClass(specialLiClasses.default);
			});
		}
		if (someli) {
			someli.trigger('mouseout');
		}
		if (startcollapsed)
			oldiv.addClass('mw-collapsed')
			.makeCollapsible();
		else
			oldiv.makeCollapsible();
		ol.attr('style', ol.attr('style')
			.replace('none', 'disc'));
	}

	function init() {
		mw.util.addCSS('li.' + myClassName +
			'{white-space:nowrap; font-size:88.36%;}\n' + //css for li element
			'li.' + liHighlightClass + '{background-color:yellow;}\n' + //css for highlighted li element.
			'.rtl li.' + myClassName + '{float: right; margin-left: 3px;}\n' +
			'.ltr li.' + myClassName + '{float: left; margin-right: 3px;}\n' +
			hilightDivMarker + ' .mw-collapsible-toggle {float: none}');
		if (imagehighlighted)
			$('.popupclass img')
			.each(handleOneMap);
		else
			$(hilightDivMarker + ' img')
			.each(handleOneMap);
		imagehighlighted = true;
	}

	//has at least one "imagehighlight" div, and canvas-capable browser:
	if ( $(hilightDivMarker).length && $('<canvas>')[0].getContext ) {
		mw.loader.using(['jquery.makeCollapsible', 'mediawiki.util', 'mediawiki.api'])
			.done(init);
	}
}

function imagehighlightexpand() {
	imagehighlight(false);
}

$(imagehighlight);

$('body').on('refresh-imagehighlight-nolinks', imagehighlight);
$('body').on('refresh-imagehighlight-links', imagehighlightexpand);