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)
written by user:קיפודנחש on hewiki.
  - Renders a numbered table (No., Label) under each imagemap
released as public domain. you may copy, modify and redistribute any way you want.
   - Hovering a table row highlights the mapped region (and vice versa)
no guaranty or waranty of any kind, explicit or implied.
  - Robust map lookup via <img usemap> / siblings
  - Safe to run multiple times (per-image guard)
  Public domain; based on the original by user:קיפודנחש (hewiki)
*/
*/
$(document).ready(function() {
(function ($, mw) {
  'use strict';


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


function drawMarker(context, areas) { // mthis is where the magic is done.
  // ---------- 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);


function drawPoly(coords) {
    $this.toggleClass(liHighlightClass, activate);
context.moveTo(coords.shift(), coords.shift());
    context.clearRect(0, 0, context.canvas.width, context.canvas.height);
while (coords.length)
context.lineTo(coords.shift(), coords.shift());
}


for (var i in areas) {
    var $items = $legend.is('ol') ? $legend.children('li') : $legend.find('tbody tr.im-row');
var coords = areas[i].coords.split(',');
    $items.each(function () {
context.beginPath();
      var $item = $(this);
switch (areas[i].shape) {
      var licap = ($item.find('.im-label a').text() || $item.find('.im-label').text() || $item.text()).trim();
case 'rect':
      var param;
case null:
      if (activate && licap === caption) {
case '': // imagemap extension stopped setting shape="rect" for rectangles in mw version 1.45.0-wmf.7
        param = (special && special.hover) || areaHighLighting;
drawPoly([coords[0], coords[1], coords[0], coords[3], coords[2], coords[3], coords[2], coords[1]]);
      } else {
break;
        param = special && special.nover && (special.nover[licap] || special.nover.default);
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;
      if (param) {
}
        $.extend(context, param);
context.closePath();
        drawMarker(context, $item.data('areas') || []);
context.stroke();
      }
context.fill();
    });
}
  }
}


function mouseAction(e, fromMap) {
  // ---------- Build one imagemap ----------
var $this = $(this),
  function handleOneMap() {
activate = $.inArray(e.type, highlightEvents) >= 0,
    var img = $(this);
caption = $this.text(),
ol = $this.parent(),
context = ol.data('context'),
special = ol.data(specialAreaMark);
$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  || areaHighLighting;
} else {
param = special && special.nover && (special.nover[licap] || special.nover.default);
}
if (param) {
$.extend(context, param);
drawMarker(context, $li.data('areas'));
}
});
}


function handleOneMap() {
    // guard: process each image once
var img = $(this),
    if (img.data('imhProcessed')) return;
w = this.width,
h = this.height,
map = img.parent().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);
if (!($('area', map).length))
return; //not an imagemap, or map with 0 areas.


var jcanvas = $('<canvas>', {'class': myClassName})
    // wait until the image has real layout size
.css(dims)
    if (!this.complete || !this.naturalWidth) {
.attr({width: w, height: h});
      img.one('load', handleOneMap);
var bgimg = $('<img>', {'class': myClassName, src: img.attr('src')})
      return;
.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
var ol = $('<ol>', {'class': myClassName})
.css({clear: 'both', margin: 0, listStyle: 'none', maxWidth: w + 'px', float: 'left', position: 'relative'})
.attr({'data-expandtext' : expandLegend, 'data-collapsetext': collapseLegend})
.data(specialAreaMark, specialHighlight)
.data('context', context);


// ol below image, hr below ol. original caption pushed below hr.
    var w = this.width, h = this.height;
div.after($('<hr>', {'class': myClassName}).css('clear', 'both'))
.after(ol);
var lis = {}; //collapse areas with same caption to one list item
var someli; // select arbitrary one
$('area', map).each(function() {
var html = mw.html.escape(this.title);
var li = lis[html]; // title already met? use the same li
if (!li) { //no? create a new one.
var href = this.href;
lis[html] = li = $('<li>', {'class': myClassName})
.append($('<a>', {href: href}).html(html)) 
.on(liEvents, mouseAction)
.data('areas', [])
.addClass(specialLiClasses && (specialLiClasses[html] || specialLiClasses['default']))
.appendTo(ol);
}
li.data('areas').push(this); //add the area to the li
someli = li; // whichever - we just want one...
$(this).on(mouseEvents, function(e) {
li.trigger(e.type);
});
});
if (someli) someli.trigger('mouseout');
mw.loader.using('jquery.makeCollapsible').then( function () {
ol.addClass('mw-collapsed')
.makeCollapsible();
});
}


function init() {
    // Find the <map> reliably
mw.util.addCSS('li.' + myClassName + '{white-space:nowrap;border:solid 1px transparent;border-radius:6px;}\n' + //css for li element
    var map = (function () {
'li.' +  myClassName + '.' + liHighlightClass + '{background-color:yellow;border-color:green;}\n' + //css for highlighted li element.
      var m = img.parent().siblings('map:first');
'.rtl li.' + myClassName + '{float: right; margin-left: 3em;}\n' +
      if (m.length) return m;
'.ltr li.' + myClassName + '{float: left; margin-right: 3em;}');
      var usemap = img.attr('usemap'); // e.g. "#imagemap-123"
$(hilightDivMarker+ ' img').each(handleOneMap);
      if (usemap) {
}
        var name = usemap.replace(/^#/, '');
        m = $('map[name="' + name + '"]');
        if (m.length) return m.first();
      }
      return img.closest(hilightDivMarker).find('map:first');
    })();


//has at least one "imagehighlight" div, and canvas-capable browser:
    if (!map.length || !$('area', map).length) return; // nothing to do
if ( $(hilightDivMarker).length && $('<canvas>')[0].getContext )
 
mw.loader.using( ['jquery.makeCollapsible', 'mediawiki.util'] ).done( init );
    // 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);