MediaWiki:Common.js: Difference between revisions
No edit summary |
No edit summary |
||
| (26 intermediate revisions by the same user not shown) | |||
| Line 1: | Line 1: | ||
// ------------------------- | |||
// Infobox cleanup | |||
// ------------------------- | |||
$(document).ready(function() { | $(document).ready(function() { | ||
$('.infoboxTable th').each(function() { | $('.infoboxTable th').each(function() { | ||
if ($(this).text().trim() === "Image") { | if ($(this).text().trim() === "Image") { | ||
$(this).text(""); | $(this).text(""); | ||
} | } | ||
}); | }); | ||
}); | }); | ||
// ------------------------- | |||
// Remove unwanted Introduction headers | |||
// ------------------------- | |||
$(document).ready(function() { | $(document).ready(function() { | ||
$('h2').each(function() { | $('h2').each(function() { | ||
if ($(this).text().trim() === "Introduction") { | if ($(this).text().trim() === "Introduction") { | ||
$(this).remove(); | $(this).remove(); | ||
} | } | ||
}); | }); | ||
}); | }); | ||
// ------------------------- | |||
// Remove first empty paragraph | |||
// ------------------------- | |||
$(document).ready(function() { | |||
$('p').first().each(function() { | |||
if ($(this).html().trim() === "<br>") { | |||
$(this).remove(); | |||
} | |||
}); | |||
}); | |||
mw.loader.load('https://cdn.jsdelivr.net/gh/openlayers/openlayers.github.io@main/dist/en/v7.0.0/legacy/ol.js'); | |||
mw.loader.load('/maps/elemara/elemara-map.js'); | |||
mw.loader.using([], function () { | |||
$(function () { | |||
var container = document.getElementById("pixel-area-calculator"); | |||
if (!container) { | |||
return; | |||
} | |||
container.innerHTML = | |||
'<div class="pixel-area-tool">' + | |||
'<h3>Pixel to Square Miles Calculator</h3>' + | |||
'<label>Width in pixels:' + | |||
'<input id="pixel-width" type="number" min="0" step="any" placeholder="Width">' + | |||
'</label>' + | |||
'<label>Height in pixels:' + | |||
'<input id="pixel-height" type="number" min="0" step="any" placeholder="Height">' + | |||
'</label>' + | |||
'<button id="pixel-area-button" type="button">Calculate</button>' + | |||
'<div id="pixel-area-result"></div>' + | |||
'</div>'; | |||
document.getElementById("pixel-area-button").addEventListener("click", function () { | |||
var widthPixels = parseFloat(document.getElementById("pixel-width").value); | |||
var heightPixels = parseFloat(document.getElementById("pixel-height").value); | |||
var result = document.getElementById("pixel-area-result"); | |||
if (isNaN(widthPixels) || isNaN(heightPixels)) { | |||
result.textContent = "Please enter both pixel values."; | |||
return; | |||
} | |||
var widthMiles = (widthPixels / 1000) * 75; | |||
var heightMiles = (heightPixels / 1000) * 75; | |||
var squareMiles = widthMiles * heightMiles; | |||
result.innerHTML = | |||
"<strong>" + widthMiles.toLocaleString(undefined, { maximumFractionDigits: 0 }) + " miles</strong>" + | |||
" × " + | |||
"<strong>" + heightMiles.toLocaleString(undefined, { maximumFractionDigits: 0 }) + " miles</strong>" + | |||
" = " + | |||
"<strong>" + squareMiles.toLocaleString(undefined, { maximumFractionDigits: 0 }) + " mi²</strong>"; | |||
}); | |||
}); | |||
}); | |||
(function () { | |||
function stripHeaderLines(text) { | |||
return text | |||
// Remove standalone short lines that look like section headers | |||
.replace(/(?:^|\n)\s*[A-Z][A-Za-z0-9'’\- ,()]{0,50}\s*(?=\n)/g, '\n') | |||
// Clean up excess blank lines | |||
.replace(/\n{2,}/g, '\n') | |||
.trim(); | |||
} | |||
function cleanAdarisPopups() { | |||
document.querySelectorAll( | |||
'.mwe-popups-extract, .mwe-popups-extract p, .mwe-popups .extract, .mwe-popups .mwe-popups-extract' | |||
).forEach(function (el) { | |||
if (!el || el.dataset.adarisCleaned === '1') { | |||
return; | |||
} | |||
el.textContent = stripHeaderLines(el.textContent); | |||
el.dataset.adarisCleaned = '1'; | |||
}); | |||
} | |||
const observer = new MutationObserver(function () { | |||
cleanAdarisPopups(); | |||
setTimeout(cleanAdarisPopups, 100); | |||
setTimeout(cleanAdarisPopups, 300); | |||
}); | |||
observer.observe(document.body, { | |||
childList: true, | |||
subtree: true, | |||
characterData: true | |||
}); | |||
cleanAdarisPopups(); | |||
})(); | |||
mw.loader.using('mediawiki.util', function () { | |||
$(function () { | |||
var input = document.getElementById('pixelToMileInput'); | |||
var output = document.getElementById('pixelToMileOutput'); | |||
if (!input || !output) { | |||
return; | |||
} | |||
input.addEventListener('input', function () { | |||
var pixels = parseFloat(input.value); | |||
if (isNaN(pixels)) { | |||
output.textContent = '0 mi'; | |||
return; | |||
} | |||
var miles = (pixels / 1000) * 75; | |||
output.textContent = Math.round(miles) + ' mi'; | |||
}); | |||
}); | |||
}); | |||
$(function () { | |||
var container = document.getElementById('pixel-to-mile-calculator'); | |||
if (!container) { | |||
return; | |||
} | |||
container.className = 'adaris-tool-box'; | |||
container.innerHTML = | |||
'<label for="pixelToMileInput"><strong>Pixels:</strong></label><br>' + | |||
'<input id="pixelToMileInput" type="number" placeholder="Enter pixels">' + | |||
'<p><strong>Distance:</strong> <span id="pixelToMileOutput">0 mi</span></p>'; | |||
var input = document.getElementById('pixelToMileInput'); | |||
var output = document.getElementById('pixelToMileOutput'); | |||
input.addEventListener('input', function () { | |||
var pixels = parseFloat(input.value); | |||
if (isNaN(pixels)) { | |||
output.textContent = '0 mi'; | |||
return; | |||
} | |||
var miles = (pixels / 1000) * 75; | |||
output.textContent = Math.round(miles) + ' mi'; | |||
}); | |||
}); | |||
mw.loader.load('/index.php?title=MediaWiki:Familytree.js&action=raw&ctype=text/javascript'); | |||
/* ============================================================ | |||
Interactive deep-zoom map for MediaWiki (OpenSeadragon) | |||
------------------------------------------------------------ | |||
Paste this whole block at the bottom of MediaWiki:Common.js | |||
Supports clickable pins, text labels, per-marker zoom ranges, | |||
and category layers with an on/off legend. | |||
============================================================ */ | |||
( function () { | |||
'use strict'; | |||
// Same major version is used for the script and its button images. | |||
var OSD_BASE = 'https://cdn.jsdelivr.net/npm/openseadragon@6/build/openseadragon/'; | |||
function truthy( v ) { | |||
return v === 'true' || v === '1' || v === 'yes'; | |||
} | |||
function loadOSD() { | |||
if ( window.OpenSeadragon ) { | |||
return Promise.resolve(); | |||
} | |||
return new Promise( function ( resolve, reject ) { | |||
var s = document.createElement( 'script' ); | |||
s.src = OSD_BASE + 'openseadragon.min.js'; | |||
s.onload = resolve; | |||
s.onerror = function () { | |||
reject( new Error( 'InteractiveMap: failed to load OpenSeadragon' ) ); | |||
}; | |||
document.head.appendChild( s ); | |||
} ); | |||
} | |||
// Build a wiki article URL without depending on extra RL modules. | |||
function articleUrl( page ) { | |||
var path = mw.config.get( 'wgArticlePath' ); // e.g. "/wiki/$1" | |||
return path.replace( '$1', encodeURIComponent( String( page ).replace( / /g, '_' ) ) ); | |||
} | |||
// Forgiving JSON parse: tolerates a page wrapped in <pre> or | |||
// <syntaxhighlight> by grabbing the first {...} / [...] block. | |||
function parseMarkers( txt ) { | |||
try { | |||
return normalize( JSON.parse( txt ) ); | |||
} catch ( e ) { /* fall through */ } | |||
var m = txt.match( /[\[{][\s\S]*[\]}]/ ); | |||
if ( m ) { | |||
try { | |||
return normalize( JSON.parse( m[ 0 ] ) ); | |||
} catch ( e2 ) { /* fall through */ } | |||
} | |||
return { pins: [], categories: {} }; | |||
} | |||
function normalize( data ) { | |||
if ( Array.isArray( data ) ) { | |||
return { pins: data, categories: {} }; | |||
} | |||
if ( data && Array.isArray( data.pins ) ) { | |||
return { pins: data.pins, categories: data.categories || {} }; | |||
} | |||
return { pins: [], categories: {} }; | |||
} | |||
function fetchMarkers( src ) { | |||
if ( !src ) { | |||
return Promise.resolve( { pins: [], categories: {} } ); | |||
} | |||
if ( /^https?:\/\//.test( src ) ) { | |||
return fetch( src ).then( function ( r ) { | |||
return r.text(); | |||
} ).then( parseMarkers ); | |||
} | |||
var scriptPath = mw.config.get( 'wgScriptPath' ) || ''; | |||
var url = scriptPath + '/api.php?action=parse&prop=wikitext&formatversion=2&format=json&page=' + | |||
encodeURIComponent( src ); | |||
return fetch( url ).then( function ( r ) { | |||
return r.json(); | |||
} ).then( function ( d ) { | |||
return parseMarkers( ( d && d.parse && d.parse.wikitext ) || '[]' ); | |||
} ); | |||
} | |||
var PIN_SVG = '<svg viewBox="0 0 24 34" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">' + | |||
'<path d="M12 0C5.7 0 .8 4.9.8 11.2.8 19.7 12 34 12 34s11.2-14.3 11.2-22.8C23.2 4.9 18.3 0 12 0z" fill="currentColor"/>' + | |||
'<circle cx="12" cy="11.2" r="4.2" fill="#fff" fill-opacity=".92"/></svg>'; | |||
// A marker's colour: its own "color" wins, else its category's colour. | |||
function markerColor( p, categories ) { | |||
var cat = ( categories && p.category ) ? categories[ p.category ] : null; | |||
return p.color || ( cat && cat.color ); | |||
} | |||
function buildPin( p, categories ) { | |||
var color = markerColor( p, categories ); | |||
var pin = document.createElement( p.page ? 'a' : 'span' ); | |||
pin.className = 'imap-pin'; | |||
if ( p.page ) { | |||
pin.href = articleUrl( p.page ); | |||
} | |||
if ( color ) { | |||
pin.style.setProperty( '--pin-color', color ); | |||
} | |||
pin.setAttribute( 'aria-label', p.title || p.page || 'map location' ); | |||
pin.innerHTML = PIN_SVG; | |||
if ( p.title ) { | |||
var hover = document.createElement( 'span' ); | |||
hover.className = 'imap-pin-label'; | |||
hover.textContent = p.title; | |||
pin.appendChild( hover ); | |||
} | |||
return { element: pin, placement: OpenSeadragon.Placement.BOTTOM }; | |||
} | |||
function buildLabel( p, categories ) { | |||
var color = markerColor( p, categories ); | |||
var el = document.createElement( p.page ? 'a' : 'span' ); | |||
el.className = 'imap-label'; | |||
el.textContent = p.title || ''; | |||
if ( p.page ) { | |||
el.href = articleUrl( p.page ); | |||
} | |||
if ( color ) { | |||
el.style.color = color; | |||
} | |||
if ( p.size ) { | |||
el.style.fontSize = p.size + 'px'; | |||
} | |||
return { element: el, placement: OpenSeadragon.Placement.CENTER }; | |||
} | |||
// Add every marker; return a list we can show / hide by zoom + category. | |||
function addMarkers( viewer, markers, categories ) { | |||
var ti = viewer.world.getItemAt( 0 ); | |||
var tracked = []; | |||
if ( !ti ) { | |||
return tracked; | |||
} | |||
markers.forEach( function ( p ) { | |||
if ( typeof p.x !== 'number' || typeof p.y !== 'number' ) { | |||
return; | |||
} | |||
var built = ( p.kind === 'label' ) ? buildLabel( p, categories ) : buildPin( p, categories ); | |||
viewer.addOverlay( { | |||
element: built.element, | |||
location: ti.imageToViewportCoordinates( new OpenSeadragon.Point( p.x, p.y ) ), | |||
placement: built.placement | |||
} ); | |||
tracked.push( { | |||
el: built.element, | |||
cat: p.category || null, | |||
min: typeof p.minZoom === 'number' ? p.minZoom : 0, | |||
max: typeof p.maxZoom === 'number' ? p.maxZoom : Infinity, | |||
shown: true | |||
} ); | |||
} ); | |||
return tracked; | |||
} | |||
// Relative zoom: 1 = whole map fits the frame, higher = more zoomed in. | |||
// Independent of screen size, so thresholds mean the same everywhere. | |||
function relativeZoom( viewer ) { | |||
return viewer.viewport.getZoom( true ) / viewer.viewport.getHomeZoom(); | |||
} | |||
// A marker is visible when its category is enabled AND the current zoom | |||
// is inside its range. `enabled` is shared with the legend checkboxes. | |||
function makeVisibilityUpdater( viewer, tracked, enabled ) { | |||
return function () { | |||
if ( !tracked.length ) { | |||
return; | |||
} | |||
var z = relativeZoom( viewer ); | |||
tracked.forEach( function ( t ) { | |||
var catOn = !t.cat || enabled[ t.cat ] !== false; | |||
var visible = catOn && z >= t.min && z <= t.max; | |||
if ( visible !== t.shown ) { | |||
t.shown = visible; | |||
t.el.classList.toggle( 'imap-hidden', !visible ); // CSS fades it | |||
} | |||
} ); | |||
}; | |||
} | |||
// Build the on/off legend from the categories actually used by pins. | |||
// Populates `enabled` with each category's starting state and wires the | |||
// checkboxes to re-run `onChange`. | |||
function buildLegend( el, pins, categories, enabled, onChange ) { | |||
var used = {}; | |||
pins.forEach( function ( p ) { | |||
if ( p.category ) { | |||
used[ p.category ] = true; | |||
} | |||
} ); | |||
// Config order first, then any used categories not in the config. | |||
var keys = []; | |||
Object.keys( categories ).forEach( function ( k ) { | |||
if ( used[ k ] ) { | |||
keys.push( k ); | |||
} | |||
} ); | |||
Object.keys( used ).forEach( function ( k ) { | |||
if ( keys.indexOf( k ) === -1 ) { | |||
keys.push( k ); | |||
} | |||
} ); | |||
// Starting state: on, unless the category sets "default": false. | |||
keys.forEach( function ( k ) { | |||
var cfg = categories[ k ] || {}; | |||
enabled[ k ] = ( cfg.default !== false ); | |||
} ); | |||
if ( !keys.length ) { | |||
return; | |||
} | |||
var panel = document.createElement( 'div' ); | |||
panel.className = 'imap-legend'; | |||
var title = document.createElement( 'div' ); | |||
title.className = 'imap-legend-title'; | |||
title.textContent = 'Layers'; | |||
panel.appendChild( title ); | |||
var boxes = []; // child checkboxes, parallel to `keys` | |||
// Master "All" checkbox: checked when every layer is on, unchecked | |||
// when none are, and a dash (indeterminate) when it's a mix. | |||
var master = document.createElement( 'input' ); | |||
master.type = 'checkbox'; | |||
function syncMaster() { | |||
var on = 0; | |||
boxes.forEach( function ( b ) { | |||
if ( b.checked ) { | |||
on++; | |||
} | |||
} ); | |||
master.checked = ( boxes.length > 0 && on === boxes.length ); | |||
master.indeterminate = ( on > 0 && on < boxes.length ); | |||
} | |||
master.addEventListener( 'change', function () { | |||
boxes.forEach( function ( b, i ) { | |||
b.checked = master.checked; | |||
enabled[ keys[ i ] ] = master.checked; | |||
} ); | |||
master.indeterminate = false; | |||
onChange(); | |||
} ); | |||
var masterRow = document.createElement( 'label' ); | |||
masterRow.className = 'imap-legend-all'; | |||
var masterText = document.createElement( 'span' ); | |||
masterText.textContent = 'All'; | |||
masterRow.appendChild( master ); | |||
masterRow.appendChild( masterText ); | |||
panel.appendChild( masterRow ); | |||
keys.forEach( function ( k ) { | |||
var cfg = categories[ k ] || {}; | |||
var row = document.createElement( 'label' ); | |||
var cb = document.createElement( 'input' ); | |||
cb.type = 'checkbox'; | |||
cb.checked = enabled[ k ]; | |||
cb.addEventListener( 'change', function () { | |||
enabled[ k ] = cb.checked; | |||
syncMaster(); | |||
onChange(); | |||
} ); | |||
boxes.push( cb ); | |||
var swatch = document.createElement( 'span' ); | |||
swatch.className = 'imap-legend-swatch'; | |||
swatch.style.background = ( cfg.color || '#2f6fed' ); | |||
var text = document.createElement( 'span' ); | |||
text.textContent = cfg.label || k; | |||
row.appendChild( cb ); | |||
row.appendChild( swatch ); | |||
row.appendChild( text ); | |||
panel.appendChild( row ); | |||
} ); | |||
syncMaster(); // reflect the starting state (handles "default": false) | |||
el.appendChild( panel ); | |||
} | |||
// Optional helper: shows the live zoom level and copies the pixel | |||
// coordinates of a clicked point. Enable with | coordhelper = 1 | |||
function enableCoordHelper( viewer, el ) { | |||
var box = document.createElement( 'div' ); | |||
box.className = 'imap-coords'; | |||
var zoomLine = document.createElement( 'div' ); | |||
var ptLine = document.createElement( 'div' ); | |||
ptLine.textContent = 'Click the map to copy a pin\u2019s coordinates'; | |||
box.appendChild( zoomLine ); | |||
box.appendChild( ptLine ); | |||
el.appendChild( box ); | |||
function showZoom() { | |||
zoomLine.textContent = 'zoom \u00d7' + relativeZoom( viewer ).toFixed( 2 ); | |||
} | |||
showZoom(); | |||
viewer.addHandler( 'animation', showZoom ); | |||
viewer.addHandler( 'zoom', showZoom ); | |||
viewer.addHandler( 'resize', showZoom ); | |||
viewer.addHandler( 'canvas-click', function ( e ) { | |||
if ( !e.quick ) { | |||
return; // ignore the click that ends a drag | |||
} | |||
var img = viewer.world.getItemAt( 0 ) | |||
.viewportToImageCoordinates( viewer.viewport.pointFromPixel( e.position ) ); | |||
var snippet = '{ "x": ' + Math.round( img.x ) + | |||
', "y": ' + Math.round( img.y ) + ', "title": "", "page": "" }'; | |||
ptLine.textContent = snippet; | |||
if ( navigator.clipboard ) { | |||
navigator.clipboard.writeText( snippet ).catch( function () {} ); | |||
} | |||
} ); | |||
} | |||
function initMap( el ) { | |||
var dzi = el.getAttribute( 'data-dzi' ); | |||
if ( !dzi ) { | |||
return; | |||
} | |||
var markersSrc = el.getAttribute( 'data-markers' ); | |||
var height = el.getAttribute( 'data-height' ); | |||
if ( height ) { | |||
el.style.height = /^\d+$/.test( height ) ? height + 'px' : height; | |||
} | |||
var opts = { | |||
element: el, | |||
tileSources: dzi, | |||
prefixUrl: OSD_BASE + 'images/', | |||
showNavigator: true, | |||
navigatorPosition: 'BOTTOM_RIGHT', | |||
showRotationControl: false, | |||
gestureSettingsMouse: { clickToZoom: false }, // single click is for pins | |||
maxZoomPixelRatio: 2, | |||
visibilityRatio: 1, | |||
constrainDuringPan: true, | |||
animationTime: 0.6, | |||
springStiffness: 7 | |||
}; | |||
if ( truthy( el.getAttribute( 'data-cors' ) ) ) { | |||
opts.crossOriginPolicy = 'Anonymous'; | |||
} | |||
var viewer = OpenSeadragon( opts ); | |||
viewer.addHandler( 'open', function () { | |||
if ( truthy( el.getAttribute( 'data-coord-helper' ) ) ) { | |||
enableCoordHelper( viewer, el ); | |||
} | |||
fetchMarkers( markersSrc ).then( function ( markerData ) { | |||
var pins = markerData.pins || []; | |||
var categories = markerData.categories || {}; | |||
var enabled = {}; | |||
var tracked = addMarkers( viewer, pins, categories ); | |||
var update = makeVisibilityUpdater( viewer, tracked, enabled ); | |||
buildLegend( el, pins, categories, enabled, update ); // sets defaults in `enabled` | |||
update(); // set the correct state for the starting zoom | |||
viewer.addHandler( 'animation', update ); | |||
viewer.addHandler( 'animation-finish', update ); | |||
viewer.addHandler( 'zoom', update ); | |||
viewer.addHandler( 'resize', update ); | |||
} ).catch( function ( err ) { | |||
if ( window.console ) { | |||
console.warn( 'InteractiveMap: could not load markers \u2014', err ); | |||
} | |||
} ); | |||
} ); | |||
} | |||
// wikipage.content fires on first load and again after live previews. | |||
mw.hook( 'wikipage.content' ).add( function ( $content ) { | |||
var root = ( $content && $content[ 0 ] ) ? $content[ 0 ] : document; | |||
var nodes = root.querySelectorAll( '.interactive-map:not([data-imap-ready])' ); | |||
if ( !nodes.length ) { | |||
return; | |||
} | |||
loadOSD().then( function () { | |||
nodes.forEach( function ( el ) { | |||
el.setAttribute( 'data-imap-ready', '1' ); | |||
initMap( el ); | |||
} ); | |||
} ).catch( function ( err ) { | |||
if ( window.console ) { | |||
console.error( err ); | |||
} | |||
} ); | |||
} ); | |||
}() ); | |||