MediaWiki:Common.js: Difference between revisions
From No Way Out Wiki
No edit summary Tag: Manual revert |
No edit summary |
||
| Line 1: | Line 1: | ||
// ImageMap Highlighter - only load on pages that have the highlighter div | // ImageMap Highlighter - only load on pages that have the highlighter div | ||
if ($('.imageMapHighlighter').length > 0) { | if ($('.imageMapHighlighter').length > 0) { | ||
/ | /* ImageMap Highlighter — table legend edition (JS only; no CSS here) | ||
- Renders a numbered table (No., Label) under each imagemap | |||
- Hovering a table row highlights the mapped region (and vice versa) | |||
- Robust map lookup via <img usemap> / siblings | |||
- Safe to run multiple times (per-image guard) | |||
Public domain; based on the original by user:קיפודנחש (hewiki) | |||
*/ | */ | ||
(function ($, mw) { | |||
'use strict'; | |||
// ---------- Config ---------- | |||
// | var myClassName = 'imageMapHighlighterArtefacts', | ||
liHighlightClass = 'liHighlighting', | |||
specialAreaMark = 'area_mark', | |||
specialLiClassesMark = 'list_classes', | |||
areaHighLighting = { fillStyle: 'rgba(0,0,0,0.35)', strokeStyle: 'yellow', lineJoin: 'round', lineWidth: 2 }, | |||
hilightDivMarker = '.imageMapHighlighter', | |||
he = mw && mw.config && mw.config.get('wgUserLanguage') === 'he', | |||
expandLegend = he ? 'הצגת מקרא' : 'Show legend', | |||
collapseLegend = he ? 'הסתרת המקרא' : 'Hide legend', | |||
liEvents = 'mouseover mouseout focus blur', | |||
mouseEvents = 'mouseover mouseout', | |||
highlightEvents = ['mouseover', 'focus']; | |||
// ---------- Drawing ---------- | |||
function drawMarker(context, areas) { | |||
function drawPoly(coords) { | |||
context.moveTo(coords.shift(), coords.shift()); | |||
while (coords.length) context.lineTo(coords.shift(), coords.shift()); | |||
} | |||
for (var i = 0; i < areas.length; i++) { | |||
var a = areas[i], coords = a.coords.split(',').map(Number); | |||
context.beginPath(); | |||
switch (a.shape) { | |||
case 'rect': | |||
case null: | |||
case '': | |||
// MW 1.45+ sometimes omits shape="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; | |||
case 'poly': | |||
drawPoly(coords); | |||
break; | |||
} | |||
context.closePath(); | |||
context.stroke(); | |||
context.fill(); | |||
} | |||
} | |||
// ---------- Legend hover handler (rows or legacy <li>) ---------- | |||
function mouseAction(e) { | |||
var $this = $(this), | |||
activate = highlightEvents.indexOf(e.type) >= 0, | |||
caption = ($this.find('.im-label a').text() || $this.find('.im-label').text() || $this.text()).trim(), | |||
$legend = $this.closest('.im-legend, ol'), | |||
context = $legend.data('context'), | |||
special = $legend.data(specialAreaMark); | |||
$this.toggleClass(liHighlightClass, activate); | |||
context.clearRect(0, 0, context.canvas.width, context.canvas.height); | |||
var $items = $legend.is('ol') ? $legend.children('li') : $legend.find('tbody tr.im-row'); | |||
$items.each(function () { | |||
var $item = $(this); | |||
var licap = ($item.find('.im-label a').text() || $item.find('.im-label').text() || $item.text()).trim(); | |||
var param; | |||
if (activate && licap === caption) { | |||
param = (special && special.hover) || areaHighLighting; | |||
} else { | |||
param = special && special.nover && (special.nover[licap] || special.nover.default); | |||
} | |||
if (param) { | |||
$.extend(context, param); | |||
drawMarker(context, $item.data('areas') || []); | |||
} | |||
}); | |||
} | |||
// ---------- Build one imagemap ---------- | |||
function handleOneMap() { | |||
var img = $(this); | |||
// guard: process each image once | |||
if (img.data('imhProcessed')) return; | |||
// wait until the image has real layout size | |||
if (!this.complete || !this.naturalWidth) { | |||
img.one('load', handleOneMap); | |||
return; | |||
} | |||
var w = this.width, h = this.height; | |||
// Find the <map> reliably | |||
var map = (function () { | |||
var m = img.parent().siblings('map:first'); | |||
if (m.length) return m; | |||
var usemap = img.attr('usemap'); // e.g. "#imagemap-123" | |||
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 || !$('area', map).length) return; // nothing to do | |||
// Layer sandwich: bg image (absolute), canvas (absolute), original img (absolute, transparent) | |||
}); | var dims = { position: 'absolute', width: w + 'px', height: h + 'px', border: 0, top: 0, left: 0 }; | ||
} | var jcanvas = $('<canvas>', { 'class': myClassName }).css(dims).attr({ width: w, height: h }); | ||
var bgimg = $('<img>', { 'class': myClassName, src: img.attr('src') }).css(dims); | |||
var context = $.extend(jcanvas[0].getContext('2d'), areaHighLighting); | |||
var wrapper = $('<div>').css({ position: 'relative', width: w + 'px', height: h + 'px' }); | |||
img.before(wrapper); | |||
wrapper.append(bgimg).append(jcanvas).append(img); | |||
img.fadeTo(1, 0); // mouse events still come from the original image | |||
img.data('imhProcessed', true); | |||
// Legend wrapper + table | |||
var $legendWrap = $('<div>', { | |||
'class': myClassName + ' im-legend mw-collapsible mw-collapsed' | |||
}).attr({ | |||
'data-expandtext': expandLegend, | |||
'data-collapsetext': collapseLegend | |||
}); | |||
var $table = $( | |||
'<table class="' + myClassName + ' im-table" aria-label="Image map legend">' + | |||
'<thead><tr>' + | |||
'<th class="im-num">#</th>' + | |||
'<th class="im-label">Location</th>' + | |||
'</tr></thead>' + | |||
'<tbody></tbody>' + | |||
'</table>' | |||
); | |||
$legendWrap.append($table); | |||
wrapper.after($('<hr>', { 'class': myClassName }).css('clear', 'both')).after($legendWrap); | |||
// Collapse duplicate titles into one row | |||
var rowsByTitle = Object.create(null); | |||
var idx = 0, $someRow; | |||
var specialHighlight = img.closest(hilightDivMarker).data(specialAreaMark); | |||
var specialLiClasses = img.closest(hilightDivMarker).data(specialLiClassesMark); | |||
$('area', map).each(function () { | |||
var title = this.title || ''; | |||
var href = this.href || '#'; | |||
var key = title; // use plain text for matching | |||
var $row = rowsByTitle[key]; | |||
if (!$row) { | |||
idx += 1; | |||
$row = $('<tr>', { 'class': myClassName + ' im-row' }) | |||
.append($('<td class="im-num">').text(idx)) | |||
.append($('<td class="im-label">').append($('<a>', { href: href }).text(title))) | |||
.on(liEvents, mouseAction) | |||
.data('areas', []) | |||
.appendTo($table.find('tbody')); | |||
var addClass = specialLiClasses && (specialLiClasses[title] || specialLiClasses['default']); | |||
if (addClass) $row.addClass(addClass); | |||
rowsByTitle[key] = $row; | |||
} | |||
// bind area ↔ row hover | |||
$row.data('areas').push(this); | |||
$someRow = $row; | |||
$(this).on(mouseEvents, function (e) { $row.trigger(e.type); }); | |||
}); | |||
if ($someRow) $someRow.trigger('mouseout'); | |||
// store drawing context on the wrapper for mouseAction | |||
$legendWrap.data(specialAreaMark, specialHighlight).data('context', context); | |||
mw.loader.using('jquery.makeCollapsible').then(function () { | |||
$legendWrap.makeCollapsible(); | |||
}); | |||
} | |||
// ---------- Init ---------- | |||
function init($scope) { | |||
var $roots = ($scope && $scope.length) ? $scope : $(document); | |||
$roots.find(hilightDivMarker + ' img').each(handleOneMap); | |||
} | |||
// Run on ready and when content changes (VE previews, Ajax loads, etc.) | |||
$(function () { init(); }); | |||
mw.hook('wikipage.content').add(function ($c) { init($c); }); | |||
})(jQuery, mw); | |||
Revision as of 04:37, 25 September 2025
// ImageMap Highlighter - only load on pages that have the highlighter div
if ($('.imageMapHighlighter').length > 0) {
/* ImageMap Highlighter — table legend edition (JS only; no CSS here)
- Renders a numbered table (No., Label) under each imagemap
- Hovering a table row highlights the mapped region (and vice versa)
- Robust map lookup via <img usemap> / siblings
- Safe to run multiple times (per-image guard)
Public domain; based on the original by user:קיפודנחש (hewiki)
*/
(function ($, mw) {
'use strict';
// ---------- Config ----------
var myClassName = 'imageMapHighlighterArtefacts',
liHighlightClass = 'liHighlighting',
specialAreaMark = 'area_mark',
specialLiClassesMark = 'list_classes',
areaHighLighting = { fillStyle: 'rgba(0,0,0,0.35)', strokeStyle: 'yellow', lineJoin: 'round', lineWidth: 2 },
hilightDivMarker = '.imageMapHighlighter',
he = mw && mw.config && mw.config.get('wgUserLanguage') === 'he',
expandLegend = he ? 'הצגת מקרא' : 'Show legend',
collapseLegend = he ? 'הסתרת המקרא' : 'Hide legend',
liEvents = 'mouseover mouseout focus blur',
mouseEvents = 'mouseover mouseout',
highlightEvents = ['mouseover', 'focus'];
// ---------- Drawing ----------
function drawMarker(context, areas) {
function drawPoly(coords) {
context.moveTo(coords.shift(), coords.shift());
while (coords.length) context.lineTo(coords.shift(), coords.shift());
}
for (var i = 0; i < areas.length; i++) {
var a = areas[i], coords = a.coords.split(',').map(Number);
context.beginPath();
switch (a.shape) {
case 'rect':
case null:
case '':
// MW 1.45+ sometimes omits shape="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;
case 'poly':
drawPoly(coords);
break;
}
context.closePath();
context.stroke();
context.fill();
}
}
// ---------- Legend hover handler (rows or legacy <li>) ----------
function mouseAction(e) {
var $this = $(this),
activate = highlightEvents.indexOf(e.type) >= 0,
caption = ($this.find('.im-label a').text() || $this.find('.im-label').text() || $this.text()).trim(),
$legend = $this.closest('.im-legend, ol'),
context = $legend.data('context'),
special = $legend.data(specialAreaMark);
$this.toggleClass(liHighlightClass, activate);
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
var $items = $legend.is('ol') ? $legend.children('li') : $legend.find('tbody tr.im-row');
$items.each(function () {
var $item = $(this);
var licap = ($item.find('.im-label a').text() || $item.find('.im-label').text() || $item.text()).trim();
var param;
if (activate && licap === caption) {
param = (special && special.hover) || areaHighLighting;
} else {
param = special && special.nover && (special.nover[licap] || special.nover.default);
}
if (param) {
$.extend(context, param);
drawMarker(context, $item.data('areas') || []);
}
});
}
// ---------- Build one imagemap ----------
function handleOneMap() {
var img = $(this);
// guard: process each image once
if (img.data('imhProcessed')) return;
// wait until the image has real layout size
if (!this.complete || !this.naturalWidth) {
img.one('load', handleOneMap);
return;
}
var w = this.width, h = this.height;
// Find the <map> reliably
var map = (function () {
var m = img.parent().siblings('map:first');
if (m.length) return m;
var usemap = img.attr('usemap'); // e.g. "#imagemap-123"
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 || !$('area', map).length) return; // nothing to do
// Layer sandwich: bg image (absolute), canvas (absolute), original img (absolute, transparent)
var dims = { position: 'absolute', width: w + 'px', height: h + 'px', border: 0, top: 0, left: 0 };
var jcanvas = $('<canvas>', { 'class': myClassName }).css(dims).attr({ width: w, height: h });
var bgimg = $('<img>', { 'class': myClassName, src: img.attr('src') }).css(dims);
var context = $.extend(jcanvas[0].getContext('2d'), areaHighLighting);
var wrapper = $('<div>').css({ position: 'relative', width: w + 'px', height: h + 'px' });
img.before(wrapper);
wrapper.append(bgimg).append(jcanvas).append(img);
img.fadeTo(1, 0); // mouse events still come from the original image
img.data('imhProcessed', true);
// Legend wrapper + table
var $legendWrap = $('<div>', {
'class': myClassName + ' im-legend mw-collapsible mw-collapsed'
}).attr({
'data-expandtext': expandLegend,
'data-collapsetext': collapseLegend
});
var $table = $(
'<table class="' + myClassName + ' im-table" aria-label="Image map legend">' +
'<thead><tr>' +
'<th class="im-num">#</th>' +
'<th class="im-label">Location</th>' +
'</tr></thead>' +
'<tbody></tbody>' +
'</table>'
);
$legendWrap.append($table);
wrapper.after($('<hr>', { 'class': myClassName }).css('clear', 'both')).after($legendWrap);
// Collapse duplicate titles into one row
var rowsByTitle = Object.create(null);
var idx = 0, $someRow;
var specialHighlight = img.closest(hilightDivMarker).data(specialAreaMark);
var specialLiClasses = img.closest(hilightDivMarker).data(specialLiClassesMark);
$('area', map).each(function () {
var title = this.title || '';
var href = this.href || '#';
var key = title; // use plain text for matching
var $row = rowsByTitle[key];
if (!$row) {
idx += 1;
$row = $('<tr>', { 'class': myClassName + ' im-row' })
.append($('<td class="im-num">').text(idx))
.append($('<td class="im-label">').append($('<a>', { href: href }).text(title)))
.on(liEvents, mouseAction)
.data('areas', [])
.appendTo($table.find('tbody'));
var addClass = specialLiClasses && (specialLiClasses[title] || specialLiClasses['default']);
if (addClass) $row.addClass(addClass);
rowsByTitle[key] = $row;
}
// bind area ↔ row hover
$row.data('areas').push(this);
$someRow = $row;
$(this).on(mouseEvents, function (e) { $row.trigger(e.type); });
});
if ($someRow) $someRow.trigger('mouseout');
// store drawing context on the wrapper for mouseAction
$legendWrap.data(specialAreaMark, specialHighlight).data('context', context);
mw.loader.using('jquery.makeCollapsible').then(function () {
$legendWrap.makeCollapsible();
});
}
// ---------- Init ----------
function init($scope) {
var $roots = ($scope && $scope.length) ? $scope : $(document);
$roots.find(hilightDivMarker + ' img').each(handleOneMap);
}
// Run on ready and when content changes (VE previews, Ajax loads, etc.)
$(function () { init(); });
mw.hook('wikipage.content').add(function ($c) { init($c); });
})(jQuery, mw);
