refactor(theme): consolidate map init into MapUtils.initEntryMap
Extract the duplicated MapLibre init orchestration (map construction, marker/popup loop, bounds fit, GPX journey, fullscreen toggle) into one config-driven MapUtils.initEntryMap(opts) in maplibre-utils.js, bundled into map.js. Convert trip.html.twig and both home.html.twig branches to call it; home active gains the flash-highlight + a fullscreen button to match trip, and home highlights' marker click now navigates to the article. Adds a markLatest opt (false for highlights) and exposes the map as window.tripMap/window.homeMap (used by existing Playwright specs). feed-map.html.twig and map.html.twig left on their inline init (deferred). Plan: docs/working/plans/2026-06-27-map-init-consolidation.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BftDn9vu9SonFAY4vxu4uk
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -417,8 +417,138 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Config-driven entry-map initialisation — the single orchestration shared by
|
||||||
|
* the trip page and both home views. Mirrors the trip page's canonical handler:
|
||||||
|
* compact collapsed attribution bottom-left, an on('load') marker loop with a
|
||||||
|
* map-tip hover popup, bounds fit (jumpTo for one entry), GPX journey rendering,
|
||||||
|
* and the fullscreen toggle.
|
||||||
|
*
|
||||||
|
* opts:
|
||||||
|
* container string — map div id ('trip-map', 'home-map')
|
||||||
|
* entries array — parsed map_entries [{lat,lng,slug,title,url,type?,force_connect?}, ...]
|
||||||
|
* cardPrefix string? — e.g. 'entry-'; when set and a card #prefix+slug exists,
|
||||||
|
* click scrolls+flashes that card; otherwise click navigates to entry.url
|
||||||
|
* storyMarkers bool — render createStoryMarker() for entry.type === 'story' (default: dot markers)
|
||||||
|
* markLatest bool — enlarge the final non-story entry's dot (default true; pass false for
|
||||||
|
* unordered sets like home highlights so no marker is singled out)
|
||||||
|
* fullscreen object? — { btnId, colSelector }; wires the fullscreen toggle + fullscreen-aware click
|
||||||
|
* gpx object? — { urls, use, autoconnect, sourcePrefix, journeyId }; forwarded to renderGpxJourney
|
||||||
|
* fit object? — { padding=60, maxZoom=11, singleZoom=10 }
|
||||||
|
*
|
||||||
|
* Returns the maplibregl.Map instance.
|
||||||
|
*/
|
||||||
|
function initEntryMap(opts) {
|
||||||
|
opts = opts || {};
|
||||||
|
var entries = opts.entries || [];
|
||||||
|
var fit = opts.fit || {};
|
||||||
|
var padding = fit.padding != null ? fit.padding : 60;
|
||||||
|
var maxZoom = fit.maxZoom != null ? fit.maxZoom : 11;
|
||||||
|
var singleZoom = fit.singleZoom != null ? fit.singleZoom : 10;
|
||||||
|
var markLatest = opts.markLatest !== false;
|
||||||
|
|
||||||
|
var map = new maplibregl.Map({
|
||||||
|
container: opts.container,
|
||||||
|
style: MAP_STYLE,
|
||||||
|
center: [20, 20],
|
||||||
|
zoom: 2,
|
||||||
|
attributionControl: false
|
||||||
|
});
|
||||||
|
map.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-left');
|
||||||
|
|
||||||
|
map.on('load', function () {
|
||||||
|
if (entries.length === 0) {
|
||||||
|
map.jumpTo({ center: [0, 20], zoom: 2 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bounds = new maplibregl.LngLatBounds();
|
||||||
|
|
||||||
|
entries.forEach(function (entry, i) {
|
||||||
|
var isLatest = markLatest && (entry.type !== 'story') && (i === entries.length - 1);
|
||||||
|
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
||||||
|
bounds.extend(lngLat);
|
||||||
|
|
||||||
|
var el = (opts.storyMarkers && entry.type === 'story')
|
||||||
|
? createStoryMarker()
|
||||||
|
: createDotMarker(isLatest);
|
||||||
|
el.dataset.url = entry.url;
|
||||||
|
|
||||||
|
var popup = new maplibregl.Popup({ offset: 12, closeButton: false, closeOnClick: false, className: 'map-tip-popup' })
|
||||||
|
.setLngLat(lngLat)
|
||||||
|
.setHTML('<span class="map-tip">' + entry.title + '</span>');
|
||||||
|
el.addEventListener('mouseenter', function () { popup.addTo(map); });
|
||||||
|
el.addEventListener('mouseleave', function () { popup.remove(); });
|
||||||
|
el.addEventListener('click', function () {
|
||||||
|
var card = opts.cardPrefix ? document.getElementById(opts.cardPrefix + entry.slug) : null;
|
||||||
|
if (!card) { window.location.href = entry.url; return; }
|
||||||
|
var hashId = opts.cardPrefix + entry.slug;
|
||||||
|
function scrollAndHighlight() {
|
||||||
|
window.location.hash = hashId;
|
||||||
|
setTimeout(function () {
|
||||||
|
card.classList.add('is-highlighted');
|
||||||
|
setTimeout(function () { card.classList.remove('is-highlighted'); }, 700);
|
||||||
|
}, 350);
|
||||||
|
}
|
||||||
|
if (opts.fullscreen) {
|
||||||
|
var col = document.querySelector(opts.fullscreen.colSelector);
|
||||||
|
var isFs = col && col.classList.contains('is-fullscreen');
|
||||||
|
if (isFs) {
|
||||||
|
var fsBtn = document.getElementById(opts.fullscreen.btnId);
|
||||||
|
if (fsBtn) fsBtn.click();
|
||||||
|
setTimeout(scrollAndHighlight, 450);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
scrollAndHighlight();
|
||||||
|
});
|
||||||
|
|
||||||
|
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(map);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (entries.length === 1) {
|
||||||
|
map.jumpTo({ center: [parseFloat(entries[0].lng), parseFloat(entries[0].lat)], zoom: singleZoom });
|
||||||
|
} else {
|
||||||
|
map.fitBounds(bounds, { padding: padding, maxZoom: maxZoom });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts.gpx) {
|
||||||
|
renderGpxJourney(
|
||||||
|
map,
|
||||||
|
opts.gpx.use ? (opts.gpx.urls || []) : [],
|
||||||
|
entries,
|
||||||
|
opts.gpx.sourcePrefix,
|
||||||
|
opts.gpx.journeyId,
|
||||||
|
{ connectMode: opts.gpx.autoconnect }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Collapse attribution <details> which MapLibre may open on load */
|
||||||
|
var attrib = map.getContainer().querySelector('.maplibregl-ctrl-attrib');
|
||||||
|
if (attrib) attrib.removeAttribute('open');
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(function () { map.resize(); }, 100);
|
||||||
|
|
||||||
|
if (opts.fullscreen) {
|
||||||
|
var fsBtn = document.getElementById(opts.fullscreen.btnId);
|
||||||
|
var col = document.querySelector(opts.fullscreen.colSelector);
|
||||||
|
if (fsBtn && col) {
|
||||||
|
fsBtn.addEventListener('click', function () {
|
||||||
|
var isFs = col.classList.toggle('is-fullscreen');
|
||||||
|
fsBtn.setAttribute('aria-label', isFs ? 'Close map' : 'Expand map');
|
||||||
|
document.body.style.overflow = isFs ? 'hidden' : '';
|
||||||
|
setTimeout(function () { map.resize(); }, 50);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
global.MapUtils = {
|
global.MapUtils = {
|
||||||
MAP_STYLE: MAP_STYLE,
|
MAP_STYLE: MAP_STYLE,
|
||||||
|
initEntryMap: initEntryMap,
|
||||||
ACCENT: ACCENT,
|
ACCENT: ACCENT,
|
||||||
haversineKm: haversineKm,
|
haversineKm: haversineKm,
|
||||||
parseGpxFiles: parseGpxFiles,
|
parseGpxFiles: parseGpxFiles,
|
||||||
|
|||||||
@@ -61,7 +61,14 @@
|
|||||||
|
|
||||||
<div class="home-layout">
|
<div class="home-layout">
|
||||||
<div class="home-map-col">
|
<div class="home-map-col">
|
||||||
<div class="home-map" id="home-map"></div>
|
<div class="home-map" id="home-map">
|
||||||
|
<button class="feed-map-fullscreen-btn" id="home-map-fullscreen" aria-label="Expand map">
|
||||||
|
<svg class="feed-map-fs-open" aria-hidden="true" width="14" height="14" viewBox="0 0 14 14" fill="currentColor">
|
||||||
|
<path d="M0 0v4h1.5V1.5H4V0z M14 0H10v1.5h2.5V4H14z M0 14v-4h1.5v2.5H4V14z M14 14H10v-1.5h2.5V10H14z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="feed-map-fs-close" aria-hidden="true">✕</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if all_items|length == 0 %}
|
{% if all_items|length == 0 %}
|
||||||
@@ -91,49 +98,14 @@ var USE_GPX = {{ trip and trip.header.use_gpx is not null ? (trip.header.u
|
|||||||
var AUTOCONNECT = "{{ trip ? (trip.header.autoconnect ?? 'on') : 'on' }}";
|
var AUTOCONNECT = "{{ trip ? (trip.header.autoconnect ?? 'on') : 'on' }}";
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
var homeMap = new maplibregl.Map({
|
window.homeMap = MapUtils.initEntryMap({
|
||||||
container: 'home-map',
|
container: 'home-map',
|
||||||
style: MapUtils.MAP_STYLE,
|
entries: HOME_ENTRIES,
|
||||||
center: [20, 20],
|
cardPrefix: 'entry-',
|
||||||
zoom: 2
|
fullscreen: { btnId: 'home-map-fullscreen', colSelector: '.home-map-col' },
|
||||||
});
|
gpx: { urls: HOME_GPX_URLS, use: USE_GPX, autoconnect: AUTOCONNECT, sourcePrefix: 'home-gpx', journeyId: 'home-journey' },
|
||||||
|
fit: { padding: 60, maxZoom: 11, singleZoom: 10 }
|
||||||
homeMap.on('load', function () {
|
|
||||||
var bounds = new maplibregl.LngLatBounds();
|
|
||||||
var coords = [];
|
|
||||||
|
|
||||||
HOME_ENTRIES.forEach(function (entry, i) {
|
|
||||||
var isLatest = (i === HOME_ENTRIES.length - 1);
|
|
||||||
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
|
||||||
coords.push(lngLat);
|
|
||||||
bounds.extend(lngLat);
|
|
||||||
|
|
||||||
var el = MapUtils.createDotMarker(isLatest);
|
|
||||||
el.dataset.url = entry.url;
|
|
||||||
var popup = new maplibregl.Popup({ offset: 12, closeButton: false, closeOnClick: false, className: 'map-tip-popup' })
|
|
||||||
.setLngLat(lngLat)
|
|
||||||
.setHTML('<span class="map-tip">' + entry.title + '</span>');
|
|
||||||
el.addEventListener('mouseenter', function () { popup.addTo(homeMap); });
|
|
||||||
el.addEventListener('mouseleave', function () { popup.remove(); });
|
|
||||||
el.addEventListener('click', function () {
|
|
||||||
var card = document.getElementById('entry-' + entry.slug);
|
|
||||||
if (!card) return;
|
|
||||||
window.location.hash = 'entry-' + entry.slug;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(homeMap);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (HOME_ENTRIES.length === 1) {
|
|
||||||
homeMap.jumpTo({ center: coords[0], zoom: 10 });
|
|
||||||
} else {
|
|
||||||
homeMap.fitBounds(bounds, { padding: 60, maxZoom: 11 });
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(function () { homeMap.resize(); }, 100);
|
|
||||||
|
|
||||||
MapUtils.renderGpxJourney(homeMap, USE_GPX ? HOME_GPX_URLS : [], HOME_ENTRIES, 'home-gpx', 'home-journey', { connectMode: AUTOCONNECT });
|
|
||||||
});
|
|
||||||
}); // DOMContentLoaded
|
}); // DOMContentLoaded
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -247,44 +219,12 @@ homeMap.on('load', function () {
|
|||||||
var HIGHLIGHTS_ENTRIES = {{ highlights_map_entries|json_encode|raw }};
|
var HIGHLIGHTS_ENTRIES = {{ highlights_map_entries|json_encode|raw }};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
var homeMap = new maplibregl.Map({
|
window.homeMap = MapUtils.initEntryMap({
|
||||||
container: 'home-map',
|
container: 'home-map',
|
||||||
style: MapUtils.MAP_STYLE,
|
entries: HIGHLIGHTS_ENTRIES,
|
||||||
center: [20, 20],
|
markLatest: false,
|
||||||
zoom: 2
|
fit: { padding: 60, maxZoom: 8, singleZoom: 8 }
|
||||||
});
|
|
||||||
|
|
||||||
homeMap.on('load', function () {
|
|
||||||
if (HIGHLIGHTS_ENTRIES.length === 0) return;
|
|
||||||
|
|
||||||
var bounds = new maplibregl.LngLatBounds();
|
|
||||||
|
|
||||||
HIGHLIGHTS_ENTRIES.forEach(function (entry) {
|
|
||||||
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
|
||||||
bounds.extend(lngLat);
|
|
||||||
|
|
||||||
var el = MapUtils.createDotMarker(false);
|
|
||||||
var popup = new maplibregl.Popup({ offset: 12, closeButton: false, closeOnClick: false, className: 'map-tip-popup' })
|
|
||||||
.setLngLat(lngLat)
|
|
||||||
.setHTML('<span class="map-tip">' + entry.title + '</span>');
|
|
||||||
el.addEventListener('mouseenter', function () { popup.addTo(homeMap); });
|
|
||||||
el.addEventListener('mouseleave', function () { popup.remove(); });
|
|
||||||
el.addEventListener('click', function () {
|
|
||||||
var card = document.getElementById('highlight-' + entry.slug);
|
|
||||||
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(homeMap);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (HIGHLIGHTS_ENTRIES.length === 1) {
|
|
||||||
homeMap.jumpTo({ center: [parseFloat(HIGHLIGHTS_ENTRIES[0].lng), parseFloat(HIGHLIGHTS_ENTRIES[0].lat)], zoom: 8 });
|
|
||||||
} else {
|
|
||||||
homeMap.fitBounds(bounds, { padding: 60, maxZoom: 8 });
|
|
||||||
}
|
|
||||||
|
|
||||||
setTimeout(function () { homeMap.resize(); }, 100);
|
|
||||||
});
|
|
||||||
}); // DOMContentLoaded
|
}); // DOMContentLoaded
|
||||||
</script>
|
</script>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -87,89 +87,15 @@ var USE_GPX = {{ page.header.use_gpx ?? true ? 'true' : 'false' }};
|
|||||||
var AUTOCONNECT = "{{ page.header.autoconnect ?? 'on' }}";
|
var AUTOCONNECT = "{{ page.header.autoconnect ?? 'on' }}";
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function() {
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
window.tripMap = MapUtils.initEntryMap({
|
||||||
var tripMap = new maplibregl.Map({
|
|
||||||
container: 'trip-map',
|
container: 'trip-map',
|
||||||
style: MapUtils.MAP_STYLE,
|
entries: TRIP_ENTRIES,
|
||||||
center: [20, 20],
|
cardPrefix: 'entry-',
|
||||||
zoom: 2,
|
storyMarkers: true,
|
||||||
attributionControl: false
|
fullscreen: { btnId: 'trip-map-fullscreen', colSelector: '.home-map-col' },
|
||||||
});
|
gpx: { urls: GPX_URLS, use: USE_GPX, autoconnect: AUTOCONNECT, sourcePrefix: 'gpx', journeyId: 'trip-journey' },
|
||||||
tripMap.addControl(new maplibregl.AttributionControl({ compact: true }), 'bottom-left');
|
fit: { padding: 60, maxZoom: 11, singleZoom: 10 }
|
||||||
|
|
||||||
tripMap.on('load', function () {
|
|
||||||
if (TRIP_ENTRIES.length === 0) {
|
|
||||||
tripMap.jumpTo({ center: [0, 20], zoom: 2 });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── Markers + bounds ──────────────────────────────────────── */
|
|
||||||
var bounds = new maplibregl.LngLatBounds();
|
|
||||||
|
|
||||||
TRIP_ENTRIES.forEach(function (entry, i) {
|
|
||||||
var isLatest = (entry.type !== 'story') && (i === TRIP_ENTRIES.length - 1);
|
|
||||||
var lngLat = [parseFloat(entry.lng), parseFloat(entry.lat)];
|
|
||||||
bounds.extend(lngLat);
|
|
||||||
|
|
||||||
var el = entry.type === 'story' ? MapUtils.createStoryMarker() : MapUtils.createDotMarker(isLatest);
|
|
||||||
el.dataset.url = entry.url;
|
|
||||||
var popup = new maplibregl.Popup({ offset: 12, closeButton: false, closeOnClick: false, className: 'map-tip-popup' })
|
|
||||||
.setLngLat(lngLat)
|
|
||||||
.setHTML('<span class="map-tip">' + entry.title + '</span>');
|
|
||||||
el.addEventListener('mouseenter', function () { popup.addTo(tripMap); });
|
|
||||||
el.addEventListener('mouseleave', function () { popup.remove(); });
|
|
||||||
el.addEventListener('click', function () {
|
|
||||||
var card = document.getElementById('entry-' + entry.slug);
|
|
||||||
if (!card) return;
|
|
||||||
var mapCol = document.querySelector('.home-map-col');
|
|
||||||
var isFs = mapCol && mapCol.classList.contains('is-fullscreen');
|
|
||||||
function scrollAndHighlight() {
|
|
||||||
window.location.hash = 'entry-' + entry.slug;
|
|
||||||
setTimeout(function () {
|
|
||||||
card.classList.add('is-highlighted');
|
|
||||||
setTimeout(function () { card.classList.remove('is-highlighted'); }, 700);
|
|
||||||
}, 350);
|
|
||||||
}
|
|
||||||
if (isFs) {
|
|
||||||
var fsBtn = document.getElementById('trip-map-fullscreen');
|
|
||||||
if (fsBtn) fsBtn.click();
|
|
||||||
setTimeout(scrollAndHighlight, 450);
|
|
||||||
} else {
|
|
||||||
scrollAndHighlight();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
new maplibregl.Marker({ element: el }).setLngLat(lngLat).addTo(tripMap);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* ── Fit bounds ─────────────────────────────────────────────── */
|
|
||||||
if (TRIP_ENTRIES.length === 1) {
|
|
||||||
tripMap.jumpTo({ center: [parseFloat(TRIP_ENTRIES[0].lng), parseFloat(TRIP_ENTRIES[0].lat)], zoom: 10 });
|
|
||||||
} else {
|
|
||||||
tripMap.fitBounds(bounds, { padding: 60, maxZoom: 11 });
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ── GPX tracks + journey segments ─────────────────────────── */
|
|
||||||
MapUtils.renderGpxJourney(tripMap, USE_GPX ? GPX_URLS : [], TRIP_ENTRIES, 'gpx', 'trip-journey', { connectMode: AUTOCONNECT });
|
|
||||||
|
|
||||||
// Collapse attribution <details> which MapLibre may open on load
|
|
||||||
var attrib = tripMap.getContainer().querySelector('.maplibregl-ctrl-attrib');
|
|
||||||
if (attrib) attrib.removeAttribute('open');
|
|
||||||
});
|
|
||||||
setTimeout(function () { tripMap.resize(); }, 100);
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
var fsBtn = document.getElementById('trip-map-fullscreen');
|
|
||||||
var mapCol = document.querySelector('.home-map-col');
|
|
||||||
if (!fsBtn || !mapCol) return;
|
|
||||||
fsBtn.addEventListener('click', function() {
|
|
||||||
var isFs = mapCol.classList.toggle('is-fullscreen');
|
|
||||||
fsBtn.setAttribute('aria-label', isFs ? 'Close map' : 'Expand map');
|
|
||||||
document.body.style.overflow = isFs ? 'hidden' : '';
|
|
||||||
setTimeout(function() { tripMap.resize(); }, 50);
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
|
|
||||||
}); // DOMContentLoaded
|
}); // DOMContentLoaded
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user