MediaWiki:Common.js: Difference between revisions
From No Way Out Wiki
No edit summary |
No edit summary |
||
| Line 1: | Line 1: | ||
/* ImageMap Highlighter | /* ImageMap Highlighter | ||
- | - Highlights mapped regions on hover/focus and syncs with legend items | ||
- Builds a compact multi-column legend (via CSS grid) | |||
- | Public domain; based on user:קיפודנחש (hewiki) | ||
Public domain; based on | |||
*/ | */ | ||
(function ($, mw) { | (function ($, mw) { | ||
'use strict'; | 'use strict'; | ||
// ---- | // ---- Global guard (don’t register twice) ---- | ||
if (window.IMH_GRID_LOADED) return; | |||
window.IMH_GRID_LOADED = true; | |||
// --------- | // ---- Config ---- | ||
var ART = 'imageMapHighlighterArtefacts'; // artifact class prefix | |||
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 ---- | |||
function drawMarker(context, areas) { | function drawMarker(context, areas) { | ||
function drawPoly(coords) { | function drawPoly(coords) { | ||
context.moveTo(coords | context.moveTo(coords[0], coords[1]); | ||
for (var i = 2; i < coords.length; i += 2) context.lineTo(coords[i], coords[i+1]); | |||
} | } | ||
for (var i = 0; i < areas.length; i++) { | for (var i = 0; i < areas.length; i++) { | ||
var a = areas[i], coords = a.coords.split(',').map(Number); | var a = areas[i], coords = (a.coords || '').split(',').map(Number); | ||
if (!coords.length) continue; | |||
context.beginPath(); | context.beginPath(); | ||
switch (a.shape) { | switch (a.shape) { | ||
case 'circle': | case 'circle': | ||
context.arc(coords[0], coords[1], coords[2], 0, Math.PI * 2); | context.arc(coords[0], coords[1], coords[2], 0, Math.PI * 2); | ||
| Line 45: | Line 43: | ||
drawPoly(coords); | drawPoly(coords); | ||
break; | break; | ||
case 'rect': | |||
case null: | |||
case '': | |||
default: | |||
// Some MW versions omit shape="rect" | |||
drawPoly([coords[0], coords[1], coords[0], coords[3], coords[2], coords[3], coords[2], coords[1]]); | |||
} | } | ||
context.closePath(); | context.closePath(); | ||
| Line 52: | Line 56: | ||
} | } | ||
// | // ---- Legend hover handler (grid items) ---- | ||
function mouseAction(e) { | function mouseAction(e) { | ||
var $this = $(this), | var $this = $(this), | ||
activate = highlightEvents.indexOf(e.type) >= 0, | activate = highlightEvents.indexOf(e.type) >= 0, | ||
cap = ($this.find('.im-label a').text() || $this.find('.im-label').text() || $this.text()).trim(), | |||
$legend = $this.closest('.im-legend | $legend = $this.closest('.im-legend'), | ||
context = $legend.data('context'), | context = $legend.data('context'), | ||
special = $legend.data(specialAreaMark); | special = $legend.data(specialAreaMark); | ||
$this.toggleClass(liHighlightClass, activate); | $this.toggleClass(liHighlightClass, activate); | ||
if (!context) return; | |||
context.clearRect(0, 0, context.canvas.width, context.canvas.height); | context.clearRect(0, 0, context.canvas.width, context.canvas.height); | ||
$legend.find('.im-row').each(function () { | |||
var $row = $(this); | |||
var $ | var rowCap = ($row.find('.im-label a').text() || $row.find('.im-label').text() || $row.text()).trim(); | ||
var | var param = (activate && rowCap === cap) | ||
var param | ? (special && special.hover) || areaHighLighting | ||
: (special && special.nover && (special.nover[rowCap] || special.nover.default)); | |||
if (param) { | if (param) { | ||
$.extend(context, param); | $.extend(context, param); | ||
drawMarker(context, $ | drawMarker(context, $row.data('areas') || []); | ||
} | } | ||
}); | }); | ||
} | } | ||
// | // ---- Build one imagemap ---- | ||
function handleOneMap() { | function handleOneMap() { | ||
var img = $(this); | var $img = $(this); | ||
if ($img.data('imhProcessed')) return; // once per image | |||
// wait for real layout size | |||
// wait | |||
if (!this.complete || !this.naturalWidth) { | if (!this.complete || !this.naturalWidth) { | ||
img.one('load', handleOneMap); | $img.one('load', handleOneMap); | ||
return; | return; | ||
} | } | ||
| Line 96: | Line 96: | ||
var w = this.width, h = this.height; | var w = this.width, h = this.height; | ||
// Find the <map> | // Find the <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'); // "#name" | ||
if (usemap) { | if (usemap) { | ||
var name = usemap.replace(/^#/, ''); | var name = usemap.replace(/^#/, ''); | ||
| Line 106: | Line 106: | ||
if (m.length) return m.first(); | if (m.length) return m.first(); | ||
} | } | ||
return img.closest(hilightDivMarker).find('map:first'); | return $img.closest(hilightDivMarker).find('map:first'); | ||
})(); | })(); | ||
if (!$map.length || !$map.find('area').length) return; | |||
// Reuse/create wrapper | |||
var $wrap = $img.closest('.' + WRAP_CLASS); | |||
if (!$wrap.length) { | |||
$wrap = $('<div>', { 'class': WRAP_CLASS }) | |||
.css({ position: 'relative', width: w + 'px', height: h + 'px' }); | |||
$img.before($wrap); | |||
$wrap.append($img); | |||
} else { | |||
// Clear any prior artifacts inside wrapper | |||
$wrap.find('canvas.' + ART + ', img.' + ART).remove(); | |||
} | |||
// Remove any old legends we (or other scripts) made after the wrapper | |||
$wrap.nextAll('div.im-legend, ol.' + ART + ', hr.' + ART).remove(); | |||
// Layer sandwich: bg image ( | // Layer sandwich: bg image (z0), canvas (z1), original (z2, transparent) | ||
var dims = { position: 'absolute', width: w + 'px', height: h + 'px', border: 0, top: 0, left: 0 }; | var dims = { position: 'absolute', width: w + 'px', height: h + 'px', border: 0, top: 0, left: 0 }; | ||
var | var $bg = $('<img>', { 'class': ART, src: $img.attr('src') }).css($.extend({ zIndex: 0 }, dims)); | ||
var | var $cv = $('<canvas>', { 'class': ART }).css($.extend({ zIndex: 1 }, dims)).attr({ width: w, height: h }); | ||
var | var ctx = $.extend($cv[0].getContext('2d'), areaHighLighting); | ||
$img.css({ position: 'absolute', zIndex: 2 }).fadeTo(1, 0); | |||
$wrap.append($bg).append($cv); | |||
$img.data('imhProcessed', true); | |||
// Legend wrapper + | // Legend wrapper + grid | ||
var $ | var $legend = $('<div>', { | ||
'class': | 'class': ART + ' im-legend mw-collapsible mw-collapsed' | ||
}).attr({ | }).attr({ 'data-expandtext': expandLegend, 'data-collapsetext': collapseLegend }) | ||
.data(specialAreaMark, $img.closest(hilightDivMarker).data(specialAreaMark)) | |||
.data('context', ctx); | |||
var $ | var $grid = $('<div class="im-grid" role="list"></div>'); | ||
$legend.append($grid); | |||
$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; | ||
var specialLiClasses = $img.closest(hilightDivMarker).data(specialLiClassesMark); | |||
var specialLiClasses = img.closest(hilightDivMarker).data(specialLiClassesMark); | |||
$('area' | $map.find('area').each(function () { | ||
var title = this.title || ''; | var title = this.title || ('Area ' + (idx + 1)); | ||
var href = this.href || '#'; | var href = this.href || '#'; | ||
var key = title; | var key = title; | ||
var $row = rowsByTitle[key]; | |||
if (!$row) { | if (!$row) { | ||
idx += 1; | idx += 1; | ||
$row = $('< | $row = $('<div>', { 'class': ART + ' im-row', 'role': 'listitem', 'tabindex': '0' }) | ||
.append($('< | .append($('<span class="im-num">').text(idx)) | ||
.append($('< | .append($('<span class="im-label">').append($('<a>', { href: href }).text(title))) | ||
.on(liEvents, mouseAction) | .on(liEvents, mouseAction) | ||
.data('areas', []) | .data('areas', []); | ||
if (specialLiClasses) { | |||
var extra = specialLiClasses[title] || specialLiClasses['default']; | |||
if (extra) $row.addClass(extra); | |||
} | |||
rowsByTitle[key] = $row; | rowsByTitle[key] = $row; | ||
$grid.append($row); | |||
} | } | ||
$row.data('areas').push(this); | $row.data('areas').push(this); | ||
$someRow = $row; | $someRow = $row; | ||
| Line 178: | Line 178: | ||
if ($someRow) $someRow.trigger('mouseout'); | if ($someRow) $someRow.trigger('mouseout'); | ||
mw.loader.using('jquery.makeCollapsible').then(function () { | mw.loader.using('jquery.makeCollapsible').then(function () { | ||
$ | $legend.makeCollapsible(); | ||
}); | }); | ||
} | } | ||
// | // ---- Init (only run where needed) ---- | ||
function init($scope) { | function init($scope) { | ||
var $ | var $root = ($scope && $scope.length) ? $scope : $(document); | ||
$ | var $targets = $root.find(hilightDivMarker + ' img'); | ||
if (!$targets.length) return; | |||
$targets.each(handleOneMap); | |||
} | } | ||
// Run on ready and | // Run on ready and on content updates (VE/Ajax) | ||
$(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); | ||
Revision as of 04:50, 25 September 2025
/* ImageMap Highlighter
- Highlights mapped regions on hover/focus and syncs with legend items
- Builds a compact multi-column legend (via CSS grid)
Public domain; based on user:קיפודנחש (hewiki)
*/
(function ($, mw) {
'use strict';
// ---- Global guard (don’t register twice) ----
if (window.IMH_GRID_LOADED) return;
window.IMH_GRID_LOADED = true;
// ---- Config ----
var ART = 'imageMapHighlighterArtefacts'; // artifact class prefix
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 ----
function drawMarker(context, areas) {
function drawPoly(coords) {
context.moveTo(coords[0], coords[1]);
for (var i = 2; i < coords.length; i += 2) context.lineTo(coords[i], coords[i+1]);
}
for (var i = 0; i < areas.length; i++) {
var a = areas[i], coords = (a.coords || '').split(',').map(Number);
if (!coords.length) continue;
context.beginPath();
switch (a.shape) {
case 'circle':
context.arc(coords[0], coords[1], coords[2], 0, Math.PI * 2);
break;
case 'poly':
drawPoly(coords);
break;
case 'rect':
case null:
case '':
default:
// Some MW versions omit shape="rect"
drawPoly([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 (grid items) ----
function mouseAction(e) {
var $this = $(this),
activate = highlightEvents.indexOf(e.type) >= 0,
cap = ($this.find('.im-label a').text() || $this.find('.im-label').text() || $this.text()).trim(),
$legend = $this.closest('.im-legend'),
context = $legend.data('context'),
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 real layout size
if (!this.complete || !this.naturalWidth) {
$img.one('load', handleOneMap);
return;
}
var w = this.width, h = this.height;
// Find 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;
// Reuse/create wrapper
var $wrap = $img.closest('.' + WRAP_CLASS);
if (!$wrap.length) {
$wrap = $('<div>', { 'class': WRAP_CLASS })
.css({ position: 'relative', width: w + 'px', height: h + 'px' });
$img.before($wrap);
$wrap.append($img);
} else {
// Clear any prior artifacts inside wrapper
$wrap.find('canvas.' + ART + ', img.' + ART).remove();
}
// Remove any old legends we (or other scripts) made after the wrapper
$wrap.nextAll('div.im-legend, ol.' + ART + ', hr.' + ART).remove();
// Layer sandwich: bg image (z0), canvas (z1), original (z2, transparent)
var dims = { position: 'absolute', width: w + 'px', height: h + 'px', border: 0, top: 0, left: 0 };
var $bg = $('<img>', { 'class': ART, src: $img.attr('src') }).css($.extend({ zIndex: 0 }, dims));
var $cv = $('<canvas>', { 'class': ART }).css($.extend({ zIndex: 1 }, dims)).attr({ width: w, height: h });
var ctx = $.extend($cv[0].getContext('2d'), areaHighLighting);
$img.css({ position: 'absolute', zIndex: 2 }).fadeTo(1, 0);
$wrap.append($bg).append($cv);
$img.data('imhProcessed', true);
// Legend wrapper + grid
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);
$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 += 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);
}
$row.data('areas').push(this);
$someRow = $row;
$(this).on(mouseEvents, function (e) { $row.trigger(e.type); });
});
if ($someRow) $someRow.trigger('mouseout');
mw.loader.using('jquery.makeCollapsible').then(function () {
$legend.makeCollapsible();
});
}
// ---- Init (only run 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);
