MediaWiki:Common.js
From No Way Out Wiki
Note: After publishing, you may have to bypass your browser's cache to see the changes.
- Firefox / Safari: Hold Shift while clicking Reload, or press either Ctrl-F5 or Ctrl-R (⌘-R on a Mac)
- Google Chrome: Press Ctrl-Shift-R (⌘-Shift-R on a Mac)
- Edge: Hold Ctrl while clicking Refresh, or press Ctrl-F5.
/* ImageMap Highlighter
- Highlights mapped regions on hover/focus and syncs with legend items
- Builds a compact multi-column legend (grid is styled in CSS)
Public domain; based on user:קיפודנחש (hewiki)
*/
(function ($, mw) {
'use strict';
// guard against double-loading
if (window.IMH_GRID_LOADED) return;
window.IMH_GRID_LOADED = true;
// ---- Config ----
var ART = 'imageMapHighlighterArtefacts'; // artifact class
var WRAP_CLASS = 'im-wrap';
var liHighlightClass = 'liHighlighting';
var specialAreaMark = 'area_mark';
var specialLiClassesMark = 'list_classes';
var hilightDivMarker = '.imageMapHighlighter';
var areaHighLighting = { fillStyle: 'rgba(0,0,0,0.35)', strokeStyle: 'yellow', lineJoin: 'round', lineWidth: 2 };
var he = mw && mw.config && mw.config.get('wgUserLanguage') === 'he';
var expandLegend = he ? 'הצגת מקרא' : 'Show legend';
var collapseLegend = he ? 'הסתרת המקרא' : 'Hide legend';
var liEvents = 'mouseover mouseout focus blur';
var mouseEvents = 'mouseover mouseout';
var highlightEvents = ['mouseover', 'focus'];
// ---- Drawing (with scaling) ----
function drawMarker(context, areas) {
var sx = context._imhSx || 1; // natural→rendered scale X
var sy = context._imhSy || 1; // natural→rendered scale Y
function drawPolyScaled(coords) {
// coords: [x1,y1, x2,y2, ...] in natural image coords
context.moveTo(coords[0] * sx, coords[1] * sy);
for (var i = 2; i < coords.length; i += 2) {
context.lineTo(coords[i] * sx, coords[i + 1] * sy);
}
}
for (var i = 0; i < areas.length; i++) {
var a = areas[i];
var coords = (a.coords || '').split(',').map(Number);
if (!coords.length) continue;
context.beginPath();
switch (a.shape) {
case 'circle': {
var x = coords[0] * sx;
var y = coords[1] * sy;
// use geometric-mean scale for radius if x/y scales differ
var r = coords[2] * Math.sqrt((sx * sx + sy * sy) / 2);
context.arc(x, y, r, 0, Math.PI * 2);
break;
}
case 'poly':
drawPolyScaled(coords);
break;
case 'rect':
case null:
case '':
default:
// Some MW versions omit shape="rect"
drawPolyScaled([coords[0], coords[1], coords[0], coords[3], coords[2], coords[3], coords[2], coords[1]]);
}
context.closePath();
context.stroke();
context.fill();
}
}
// ---- Legend hover handler ----
function mouseAction(e) {
var $this = $(this);
var activate = highlightEvents.indexOf(e.type) >= 0;
var cap = ($this.find('.im-label a').text() || $this.find('.im-label').text() || $this.text()).trim();
var $legend = $this.closest('.im-legend');
var context = $legend.data('context');
var special = $legend.data(specialAreaMark);
$this.toggleClass(liHighlightClass, activate);
if (!context) return;
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
$legend.find('.im-row').each(function () {
var $row = $(this);
var rowCap = ($row.find('.im-label a').text() || $row.find('.im-label').text() || $row.text()).trim();
var param = (activate && rowCap === cap)
? (special && special.hover) || areaHighLighting
: (special && special.nover && (special.nover[rowCap] || special.nover.default));
if (param) {
$.extend(context, param);
drawMarker(context, $row.data('areas') || []);
}
});
}
// ---- Build one imagemap ----
function handleOneMap() {
var $img = $(this);
if ($img.data('imhProcessed')) return; // once per image
// wait for intrinsic dimensions
var el = $img[0];
if (!el.complete || !el.naturalWidth) {
$img.one('load', handleOneMap);
return;
}
// Locate the <map>
var $map = (function () {
var m = $img.parent().siblings('map:first');
if (m.length) return m;
var usemap = $img.attr('usemap'); // "#name"
if (usemap) {
var name = usemap.replace(/^#/, '');
m = $('map[name="' + name + '"]');
if (m.length) return m.first();
}
return $img.closest(hilightDivMarker).find('map:first');
})();
if (!$map.length || !$map.find('area').length) return;
// Natural vs rendered sizes
var natW = el.naturalWidth;
var natH = el.naturalHeight;
var rect = el.getBoundingClientRect(); // rendered CSS size (fractional ok)
var cssW = rect.width;
var cssH = rect.height;
// Scale factors from coords → rendered pixels
var sx = cssW / natW;
var sy = cssH / natH;
// Hi-DPI canvas
var dpr = window.devicePixelRatio || 1;
// Create/clean wrapper
var $wrap = $img.closest('.' + WRAP_CLASS);
if (!$wrap.length) {
$wrap = $('<div>', { 'class': WRAP_CLASS })
.css({ position: 'relative', width: cssW + 'px', height: cssH + 'px' });
$img.before($wrap);
$wrap.append($img);
} else {
$wrap.css({ width: cssW + 'px', height: cssH + 'px' });
$wrap.find('canvas.' + ART + ', img.' + ART).remove();
}
// Remove previous legend/toggle artifacts if re-run
$wrap.nextAll('div.im-legend, ol.' + ART + ', hr.' + ART + ', .mw-collapsible-toggle.im-toggle').remove();
// Layer sandwich: bg (z0), canvas (z1), original (z2, transparent but keeps events)
var layerCSS = { position: 'absolute', top: 0, left: 0, width: cssW + 'px', height: cssH + 'px', border: 0 };
var $bg = $('<img>', { 'class': ART, src: $img.attr('src') }).css($.extend({ zIndex: 0, display: 'block' }, layerCSS));
var $cv = $('<canvas>', { 'class': ART })
.css($.extend({ zIndex: 1 }, layerCSS))
.attr({ width: Math.round(cssW * dpr), height: Math.round(cssH * dpr) });
var ctx = $cv[0].getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0); // draw in CSS px
$.extend(ctx, areaHighLighting);
ctx._imhSx = sx;
ctx._imhSy = sy;
// Put original image on top but transparent so it still handles mouse events
$img.css($.extend({ zIndex: 2, opacity: 0, display: 'block' }, layerCSS));
$wrap.append($bg).append($cv);
$img.data('imhProcessed', true);
// ---- Legend ----
var $legend = $('<div>', {
'class': ART + ' im-legend mw-collapsible mw-collapsed'
}).attr({ 'data-expandtext': expandLegend, 'data-collapsetext': collapseLegend })
.data(specialAreaMark, $img.closest(hilightDivMarker).data(specialAreaMark))
.data('context', ctx);
var $grid = $('<div class="im-grid" role="list"></div>');
$legend.append($grid);
// Place legend below image
$wrap.after($('<hr>', { 'class': ART }).css('clear', 'both')).after($legend);
var rowsByTitle = Object.create(null);
var idx = 0;
var $someRow;
var specialLiClasses = $img.closest(hilightDivMarker).data(specialLiClassesMark);
$map.find('area').each(function () {
var title = this.title || ('Area ' + (idx + 1));
var href = this.href || '#';
var key = title;
var $row = rowsByTitle[key];
if (!$row) {
idx += 1;
$row = $('<div>', { 'class': ART + ' im-row', 'role': 'listitem', 'tabindex': '0' })
.append($('<span class="im-num">').text(idx))
.append($('<span class="im-label">').append($('<a>', { href: href }).text(title)))
.on(liEvents, mouseAction)
.data('areas', []);
if (specialLiClasses) {
var extra = specialLiClasses[title] || specialLiClasses['default'];
if (extra) $row.addClass(extra);
}
rowsByTitle[key] = $row;
$grid.append($row);
}
// link area <-> legend row
$row.data('areas').push(this);
$someRow = $row;
$(this).on(mouseEvents, function (e) { $row.trigger(e.type); });
});
if ($someRow) $someRow.trigger('mouseout');
// Activate collapsible and move toggle inside the bordered legend box
mw.loader.using('jquery.makeCollapsible').then(function () {
$legend.makeCollapsible();
// MediaWiki adds the toggle as a sibling; move it inside for nicer layout
var $toggle = $legend.prev('.mw-collapsible-toggle');
if (!$toggle.length) $toggle = $legend.next('.mw-collapsible-toggle');
if ($toggle.length) {
$toggle.addClass('im-toggle');
$legend.prepend($toggle);
}
});
}
// ---- Init (only where needed) ----
function init($scope) {
var $root = ($scope && $scope.length) ? $scope : $(document);
var $targets = $root.find(hilightDivMarker + ' img');
if (!$targets.length) return;
$targets.each(handleOneMap);
}
// Run on ready and on content updates (VE/Ajax)
$(function () { init(); });
mw.hook('wikipage.content').add(function ($c) { init($c); });
})(jQuery, mw);
