MediaWiki:Common.js: Difference between revisions

No edit summary
No edit summary
 
(One intermediate revision by the same user not shown)
Line 183: Line 183:
   ------------------------------------------------------------
   ------------------------------------------------------------
   Paste this whole block at the bottom of  MediaWiki:Common.js
   Paste this whole block at the bottom of  MediaWiki:Common.js
   Supports clickable pins, text labels, and a per-marker zoom
   Supports clickable pins, text labels, per-marker zoom ranges,
   range so things appear / disappear as the reader zooms.
   and category layers with an on/off legend.
   ============================================================ */
   ============================================================ */
( function () {
( function () {
Line 229: Line 229:
} catch ( e2 ) { /* fall through */ }
} catch ( e2 ) { /* fall through */ }
}
}
return [];
return { pins: [], categories: {} };
}
}


function normalize( data ) {
function normalize( data ) {
if ( Array.isArray( data ) ) {
if ( Array.isArray( data ) ) {
return {
return { pins: data, categories: {} };
pins: data,
}
categories: {}
if ( data && Array.isArray( data.pins ) ) {
};
return { pins: data.pins, categories: data.categories || {} };
}
}
if ( data && Array.isArray( data.pins ) ) {
return { pins: [], categories: {} };
return {
pins: data.pins,
categories: data.categories || {}
};
}
}
return {
pins: [],
categories: {}
};
}


function fetchMarkers( src ) {
function fetchMarkers( src ) {
if ( !src ) {
if ( !src ) {
return Promise.resolve( [] );
return Promise.resolve( { pins: [], categories: {} } );
}
}
if ( /^https?:\/\//.test( src ) ) {
if ( /^https?:\/\//.test( src ) ) {
Line 274: Line 265:
'<circle cx="12" cy="11.2" r="4.2" fill="#fff" fill-opacity=".92"/></svg>';
'<circle cx="12" cy="11.2" r="4.2" fill="#fff" fill-opacity=".92"/></svg>';


function buildPin( p, categories ) {
// A marker's colour: its own "color" wins, else its category's colour.
var category = categories && p.category ? categories[p.category] : null;
function markerColor( p, categories ) {
var color = p.color || ( category && category.color );
var cat = ( categories && p.category ) ? categories[ p.category ] : null;
return p.color || ( cat && cat.color );
}


var pin = document.createElement( p.page ? 'a' : 'span' );
function buildPin( p, categories ) {
var color = markerColor( p, categories );
var pin = document.createElement( p.page ? 'a' : 'span' );
pin.className = 'imap-pin';
pin.className = 'imap-pin';
if ( p.page ) {
if ( p.page ) {
pin.href = articleUrl( p.page );
pin.href = articleUrl( p.page );
}
}
if ( color ) {
if ( color ) {
pin.style.setProperty( '--pin-color', color );
pin.style.setProperty( '--pin-color', color );
}
}
pin.setAttribute( 'aria-label', p.title || p.page || 'map location' );
pin.setAttribute( 'aria-label', p.title || p.page || 'map location' );
pin.innerHTML = PIN_SVG;
pin.innerHTML = PIN_SVG;
Line 297: Line 292:
}
}


function buildLabel( p ) {
function buildLabel( p, categories ) {
var color = markerColor( p, categories );
var el = document.createElement( p.page ? 'a' : 'span' );
var el = document.createElement( p.page ? 'a' : 'span' );
el.className = 'imap-label';
el.className = 'imap-label';
Line 304: Line 300:
el.href = articleUrl( p.page );
el.href = articleUrl( p.page );
}
}
if ( p.color ) {
if ( color ) {
el.style.color = p.color;
el.style.color = color;
}
}
if ( p.size ) {
if ( p.size ) {
Line 313: Line 309:
}
}


// Add every marker; return a list we can show / hide by zoom.
// Add every marker; return a list we can show / hide by zoom + category.
function addMarkers( viewer, markers, categories ) {
function addMarkers( viewer, markers, categories ) {
var ti = viewer.world.getItemAt( 0 );
var ti = viewer.world.getItemAt( 0 );
var tracked = [];
var tracked = [];
Line 324: Line 320:
return;
return;
}
}
var built = ( p.kind === 'label' ) ? buildLabel( p ) : buildPin( p, categories );
var built = ( p.kind === 'label' ) ? buildLabel( p, categories ) : buildPin( p, categories );
viewer.addOverlay( {
viewer.addOverlay( {
element: built.element,
element: built.element,
Line 332: Line 328:
tracked.push( {
tracked.push( {
el: built.element,
el: built.element,
cat: p.category || null,
min: typeof p.minZoom === 'number' ? p.minZoom : 0,
min: typeof p.minZoom === 'number' ? p.minZoom : 0,
max: typeof p.maxZoom === 'number' ? p.maxZoom : Infinity,
max: typeof p.maxZoom === 'number' ? p.maxZoom : Infinity,
Line 346: Line 343:
}
}


function makeVisibilityUpdater( viewer, tracked ) {
// 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 () {
return function () {
if ( !tracked.length ) {
if ( !tracked.length ) {
Line 353: Line 352:
var z = relativeZoom( viewer );
var z = relativeZoom( viewer );
tracked.forEach( function ( t ) {
tracked.forEach( function ( t ) {
var visible = z >= t.min && z <= t.max;
var catOn = !t.cat || enabled[ t.cat ] !== false;
var visible = catOn && z >= t.min && z <= t.max;
if ( visible !== t.shown ) {
if ( visible !== t.shown ) {
t.shown = visible;
t.shown = visible;
Line 360: Line 360:
} );
} );
};
};
}
// 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 );
}
}


Line 432: Line 540:
enableCoordHelper( viewer, el );
enableCoordHelper( viewer, el );
}
}
fetchMarkers( markersSrc ).then( function ( markerData ) {
fetchMarkers( markersSrc ).then( function ( markerData ) {
var tracked = addMarkers( viewer, markerData.pins, markerData.categories );
var pins = markerData.pins || [];
var update = makeVisibilityUpdater( viewer, tracked );
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
update(); // set the correct state for the starting zoom
viewer.addHandler( 'animation', update );
viewer.addHandler( 'animation', update );