MediaWiki:Common.js: Difference between revisions
From No Way Out Wiki
No edit summary |
No edit summary |
||
| (3 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
/* ImageMap Highlighter | /* ImageMap Highlighter (content-box scaled + offset-aware) | ||
- Highlights mapped regions | - Highlights mapped regions and syncs with legend | ||
- | - Handles devicePixelRatio, float wrappers, borders/padding on <img> | ||
Public domain; based on user:קיפודנחש (hewiki) | Public domain; based on user:קיפודנחש (hewiki) | ||
*/ | */ | ||
| Line 11: | Line 11: | ||
// ---- Config ---- | // ---- Config ---- | ||
var ART = 'imageMapHighlighterArtefacts'; | var ART = 'imageMapHighlighterArtefacts'; | ||
var WRAP_CLASS = 'im-wrap'; | var WRAP_CLASS = 'im-wrap'; | ||
var liHighlightClass = 'liHighlighting'; | var liHighlightClass = 'liHighlighting'; | ||
| Line 25: | Line 25: | ||
var highlightEvents = ['mouseover', 'focus']; | var highlightEvents = ['mouseover', 'focus']; | ||
// ---- Drawing ---- | // ---- Drawing (scaled to content box, with offset) ---- | ||
function drawMarker(context, areas) { | function drawMarker(context, areas) { | ||
function | var sx = context._imhSx || 1; | ||
context.moveTo(coords[0], coords[1]); | var sy = context._imhSy || 1; | ||
for (var i = 2; i < coords.length; i += 2) context.lineTo(coords[i], coords[i+1]); | var ox = context._imhOffX || 0; // left padding+border of <img> | ||
var oy = context._imhOffY || 0; // top padding+border of <img> | |||
function drawPolyScaled(coords) { | |||
context.moveTo(coords[0] * sx + ox, coords[1] * sy + oy); | |||
for (var i = 2; i < coords.length; i += 2) { | |||
context.lineTo(coords[i] * sx + ox, coords[i + 1] * sy + oy); | |||
} | |||
} | } | ||
for (var i = 0; i < areas.length; i++) { | for (var i = 0; i < areas.length; i++) { | ||
var a = areas[i] | var a = areas[i]; | ||
var coords = (a.coords || '').split(',').map(Number); | |||
if (!coords.length) continue; | if (!coords.length) continue; | ||
context.beginPath(); | context.beginPath(); | ||
switch (a.shape) { | switch (a.shape) { | ||
case 'circle': | case 'circle': { | ||
var cx = coords[0] * sx + ox; | |||
var cy = coords[1] * sy + oy; | |||
var r = coords[2]; | |||
if (context.ellipse) { | |||
context.ellipse(cx, cy, r * sx, r * sy, 0, 0, Math.PI * 2); | |||
} else { | |||
var rMean = r * Math.sqrt((sx * sx + sy * sy) / 2); | |||
context.arc(cx, cy, rMean, 0, Math.PI * 2); | |||
} | |||
break; | break; | ||
} | |||
case 'poly': | case 'poly': | ||
drawPolyScaled(coords); | |||
break; | break; | ||
case 'rect': | case 'rect': | ||
| Line 46: | Line 65: | ||
case '': | case '': | ||
default: | default: | ||
drawPolyScaled([coords[0], coords[1], coords[0], coords[3], coords[2], coords[3], coords[2], coords[1]]); | |||
} | } | ||
context.closePath(); | context.closePath(); | ||
| Line 55: | Line 73: | ||
} | } | ||
// ---- Legend hover handler | // ---- Legend hover handler ---- | ||
function mouseAction(e) { | function mouseAction(e) { | ||
var $this = $(this) | 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); | $this.toggleClass(liHighlightClass, activate); | ||
if (!context) return; | if (!context) return; | ||
context.clearRect(0, 0, context.canvas.width, context.canvas.height); | context.clearRect(0, 0, context.canvas.width, context.canvas.height); | ||
| Line 74: | Line 93: | ||
? (special && special.hover) || areaHighLighting | ? (special && special.hover) || areaHighLighting | ||
: (special && special.nover && (special.nover[rowCap] || special.nover.default)); | : (special && special.nover && (special.nover[rowCap] || special.nover.default)); | ||
if (param) { | if (param) { | ||
$.extend(context, param); | $.extend(context, param); | ||
| Line 81: | Line 99: | ||
}); | }); | ||
} | } | ||
// Utility to parse pixel strings safely | |||
function px(v) { var n = parseFloat(v); return isNaN(n) ? 0 : n; } | |||
// ---- Build one imagemap ---- | // ---- Build one imagemap ---- | ||
function handleOneMap() { | function handleOneMap() { | ||
var $img = $(this); | var $img = $(this); | ||
if ($img.data('imhProcessed')) return; | if ($img.data('imhProcessed')) return; | ||
var el = $img[0]; | |||
if (!el.complete || !el.naturalWidth) { $img.one('load', handleOneMap); return; } | |||
// Find <map> | |||
var $map = (function () { | var $map = (function () { | ||
var m = $img.parent().siblings('map:first'); | var m = $img.parent().siblings('map:first'); | ||
if (m.length) return m; | if (m.length) return m; | ||
var usemap = $img.attr('usemap'); | var usemap = $img.attr('usemap'); | ||
if (usemap) { | if (usemap) { | ||
var name = usemap.replace(/^#/, ''); | var name = usemap.replace(/^#/, ''); | ||
| Line 108: | Line 125: | ||
if (!$map.length || !$map.find('area').length) return; | if (!$map.length || !$map.find('area').length) return; | ||
// Natural & rendered sizes | |||
var natW = el.naturalWidth, natH = el.naturalHeight; | |||
var rect = el.getBoundingClientRect(); | |||
var cssW = rect.width, cssH = rect.height; | |||
// Compute content-box size & offset (exclude img borders/padding) | |||
var cs = window.getComputedStyle(el); | |||
var offX = px(cs.borderLeftWidth) + px(cs.paddingLeft); | |||
var offY = px(cs.borderTopWidth) + px(cs.paddingTop); | |||
var innerW = cssW - offX - (px(cs.borderRightWidth) + px(cs.paddingRight)); | |||
var innerH = cssH - offY - (px(cs.borderBottomWidth) + px(cs.paddingBottom)); | |||
if (innerW <= 0 || innerH <= 0) { innerW = cssW; innerH = cssH; offX = 0; offY = 0; } | |||
// Scale from imagemap coords (natural) → rendered content pixels | |||
var sx = innerW / natW; | |||
var sy = innerH / natH; | |||
// Prepare wrapper (inherit float & margins so layout doesn’t shift) | |||
var $wrap = $img.closest('.' + WRAP_CLASS); | var $wrap = $img.closest('.' + WRAP_CLASS); | ||
if (!$wrap.length) { | if (!$wrap.length) { | ||
$wrap = $('<div>', { 'class': WRAP_CLASS }) | $wrap = $('<div>', { 'class': WRAP_CLASS }).css({ position: 'relative' }); | ||
// copy float & margins from img | |||
var fl = (cs.float || cs.cssFloat || 'none'); | |||
if (fl && fl !== 'none') $wrap.css('float', fl); | |||
['marginTop','marginRight','marginBottom','marginLeft'].forEach(function (p) { $wrap.css(p, cs[p]); }); | |||
$img.before($wrap); | $img.before($wrap); | ||
$wrap.append($img); | $wrap.append($img); | ||
$img.css({ margin: 0, display: 'block' }); // remove baseline gap | |||
$ | |||
} | } | ||
$wrap. | // Size wrapper to the img’s border box | ||
$wrap.css({ width: cssW + 'px', height: cssH + 'px' }); | |||
// | // Clean prior artifacts | ||
$wrap.find('canvas.' + ART + ', img.' + ART).remove(); | |||
$wrap.nextAll('div.im-legend, ol.' + ART + ', hr.' + ART + ', .mw-collapsible-toggle.im-toggle').remove(); | |||
$img.css({ | // Canvas (DPR-aware) + inert bg image | ||
var dpr = window.devicePixelRatio || 1; | |||
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 pixels | |||
$.extend(ctx, areaHighLighting); | |||
ctx._imhSx = sx; ctx._imhSy = sy; // scale factors to content | |||
ctx._imhOffX = offX; ctx._imhOffY = offY; // left/top offsets of content | |||
// Original image stays on top (transparent) to keep mouse events | |||
$img.css($.extend({ zIndex: 2, opacity: 0, display: 'block' }, layerCSS)); | |||
$wrap.append($bg).append($cv); | $wrap.append($bg).append($cv); | ||
$img.data('imhProcessed', true); | $img.data('imhProcessed', true); | ||
// ---- Legend (grid; styled via site CSS) ---- | |||
var $legend = $('<div>', { | var $legend = $('<div>', { | ||
'class': ART + ' im-legend mw-collapsible mw-collapsed' | 'class': ART + ' im-legend mw-collapsible mw-collapsed' | ||
| Line 140: | Line 190: | ||
var $grid = $('<div class="im-grid" role="list"></div>'); | var $grid = $('<div class="im-grid" role="list"></div>'); | ||
$legend.append($grid); | $legend.append($grid); | ||
// Insert below image | |||
$wrap.after($('<hr>', { 'class': ART }).css('clear', 'both')).after($legend); | $wrap.after($('<hr>', { 'class': ART }).css('clear', 'both')).after($legend); | ||
// Build rows (collapse duplicate titles) | |||
var rowsByTitle = Object.create(null); | var rowsByTitle = Object.create(null); | ||
var idx = 0, $someRow; | var idx = 0, $someRow; | ||
| Line 153: | Line 206: | ||
if (!$row) { | if (!$row) { | ||
idx + | idx++; | ||
$row = $('<div>', { 'class': ART + ' im-row', 'role': 'listitem', 'tabindex': '0' }) | $row = $('<div>', { 'class': ART + ' im-row', 'role': 'listitem', 'tabindex': '0' }) | ||
.append($('<span class="im-num">').text(idx)) | .append($('<span class="im-num">').text(idx)) | ||
| Line 174: | Line 227: | ||
if ($someRow) $someRow.trigger('mouseout'); | if ($someRow) $someRow.trigger('mouseout'); | ||
mw.loader.using('jquery.makeCollapsible').then(function () { | // Collapsible + move toggle inside the legend box | ||
mw.loader.using('jquery.makeCollapsible').then(function () { | |||
$legend.makeCollapsible(); | |||
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 | // ---- Init ---- | ||
function init($scope) { | function init($scope) { | ||
var $root = ($scope && $scope.length) ? $scope : $(document); | var $root = ($scope && $scope.length) ? $scope : $(document); | ||
| Line 194: | Line 244: | ||
} | } | ||
$(function () { init(); }); | $(function () { init(); }); | ||
mw.hook('wikipage.content').add(function ($c) { init($c); }); | mw.hook('wikipage.content').add(function ($c) { init($c); }); | ||
})(jQuery, mw); | })(jQuery, mw); | ||
Latest revision as of 12:19, 29 September 2025
/* ImageMap Highlighter (content-box scaled + offset-aware)
- Highlights mapped regions and syncs with legend
- Handles devicePixelRatio, float wrappers, borders/padding on <img>
Public domain; based on user:קיפודנחש (hewiki)
*/
(function ($, mw) {
'use strict';
if (window.IMH_GRID_LOADED) return;
window.IMH_GRID_LOADED = true;
// ---- Config ----
var ART = 'imageMapHighlighterArtefacts';
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 (scaled to content box, with offset) ----
function drawMarker(context, areas) {
var sx = context._imhSx || 1;
var sy = context._imhSy || 1;
var ox = context._imhOffX || 0; // left padding+border of <img>
var oy = context._imhOffY || 0; // top padding+border of <img>
function drawPolyScaled(coords) {
context.moveTo(coords[0] * sx + ox, coords[1] * sy + oy);
for (var i = 2; i < coords.length; i += 2) {
context.lineTo(coords[i] * sx + ox, coords[i + 1] * sy + oy);
}
}
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 cx = coords[0] * sx + ox;
var cy = coords[1] * sy + oy;
var r = coords[2];
if (context.ellipse) {
context.ellipse(cx, cy, r * sx, r * sy, 0, 0, Math.PI * 2);
} else {
var rMean = r * Math.sqrt((sx * sx + sy * sy) / 2);
context.arc(cx, cy, rMean, 0, Math.PI * 2);
}
break;
}
case 'poly':
drawPolyScaled(coords);
break;
case 'rect':
case null:
case '':
default:
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') || []);
}
});
}
// Utility to parse pixel strings safely
function px(v) { var n = parseFloat(v); return isNaN(n) ? 0 : n; }
// ---- Build one imagemap ----
function handleOneMap() {
var $img = $(this);
if ($img.data('imhProcessed')) return;
var el = $img[0];
if (!el.complete || !el.naturalWidth) { $img.one('load', handleOneMap); return; }
// Find <map>
var $map = (function () {
var m = $img.parent().siblings('map:first');
if (m.length) return m;
var usemap = $img.attr('usemap');
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 & rendered sizes
var natW = el.naturalWidth, natH = el.naturalHeight;
var rect = el.getBoundingClientRect();
var cssW = rect.width, cssH = rect.height;
// Compute content-box size & offset (exclude img borders/padding)
var cs = window.getComputedStyle(el);
var offX = px(cs.borderLeftWidth) + px(cs.paddingLeft);
var offY = px(cs.borderTopWidth) + px(cs.paddingTop);
var innerW = cssW - offX - (px(cs.borderRightWidth) + px(cs.paddingRight));
var innerH = cssH - offY - (px(cs.borderBottomWidth) + px(cs.paddingBottom));
if (innerW <= 0 || innerH <= 0) { innerW = cssW; innerH = cssH; offX = 0; offY = 0; }
// Scale from imagemap coords (natural) → rendered content pixels
var sx = innerW / natW;
var sy = innerH / natH;
// Prepare wrapper (inherit float & margins so layout doesn’t shift)
var $wrap = $img.closest('.' + WRAP_CLASS);
if (!$wrap.length) {
$wrap = $('<div>', { 'class': WRAP_CLASS }).css({ position: 'relative' });
// copy float & margins from img
var fl = (cs.float || cs.cssFloat || 'none');
if (fl && fl !== 'none') $wrap.css('float', fl);
['marginTop','marginRight','marginBottom','marginLeft'].forEach(function (p) { $wrap.css(p, cs[p]); });
$img.before($wrap);
$wrap.append($img);
$img.css({ margin: 0, display: 'block' }); // remove baseline gap
}
// Size wrapper to the img’s border box
$wrap.css({ width: cssW + 'px', height: cssH + 'px' });
// Clean prior artifacts
$wrap.find('canvas.' + ART + ', img.' + ART).remove();
$wrap.nextAll('div.im-legend, ol.' + ART + ', hr.' + ART + ', .mw-collapsible-toggle.im-toggle').remove();
// Canvas (DPR-aware) + inert bg image
var dpr = window.devicePixelRatio || 1;
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 pixels
$.extend(ctx, areaHighLighting);
ctx._imhSx = sx; ctx._imhSy = sy; // scale factors to content
ctx._imhOffX = offX; ctx._imhOffY = offY; // left/top offsets of content
// Original image stays on top (transparent) to keep mouse events
$img.css($.extend({ zIndex: 2, opacity: 0, display: 'block' }, layerCSS));
$wrap.append($bg).append($cv);
$img.data('imhProcessed', true);
// ---- Legend (grid; styled via site CSS) ----
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);
// Insert below image
$wrap.after($('<hr>', { 'class': ART }).css('clear', 'both')).after($legend);
// Build rows (collapse duplicate titles)
var rowsByTitle = Object.create(null);
var idx = 0, $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++;
$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);
}
$row.data('areas').push(this);
$someRow = $row;
$(this).on(mouseEvents, function (e) { $row.trigger(e.type); });
});
if ($someRow) $someRow.trigger('mouseout');
// Collapsible + move toggle inside the legend box
mw.loader.using('jquery.makeCollapsible').then(function () {
$legend.makeCollapsible();
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 ----
function init($scope) {
var $root = ($scope && $scope.length) ? $scope : $(document);
var $targets = $root.find(hilightDivMarker + ' img');
if (!$targets.length) return;
$targets.each(handleOneMap);
}
$(function () { init(); });
mw.hook('wikipage.content').add(function ($c) { init($c); });
})(jQuery, mw);
