lazyflat/web/static/map.js
Moritz 4fd0b50a43 fix: lazy-init Leaflet map so tiles actually load
The map was being initialised on DOMContentLoaded even when its container
was display:none (0×0). Leaflet sets up internal bounds from container size
at init; on a zero-sized container no tiles are ever requested. Even
invalidateSize afterwards didn't recover reliably.

* map.js now only builds the Leaflet instance once the container has real
  dimensions (clientHeight >= 10). Triggered when the view toggle flips to
  Karte (rAF x2 + safety timers), and via restoreView on page load if the
  user's last choice was Karte.
* CSP img-src now includes https://unpkg.com for Leaflet marker icons.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 12:14:54 +02:00

105 lines
3.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// lazyflat — Leaflet flat map
// Initialised LAZILY: we only build the Leaflet instance when the container
// actually has a rendered size (> 0 height). Building it on a hidden 0×0
// container leaves Leaflet in a state where tiles never load.
let mapInstance = null;
const BERLIN_CENTER = [52.52, 13.405];
const BERLIN_ZOOM = 11;
function buildMap(el) {
if (mapInstance) {
try { mapInstance.remove(); } catch (e) {}
mapInstance = null;
}
mapInstance = L.map(el, {
zoomControl: false,
scrollWheelZoom: false,
doubleClickZoom: false,
boxZoom: false,
touchZoom: false,
keyboard: false,
}).setView(BERLIN_CENTER, BERLIN_ZOOM);
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: "© OpenStreetMap",
maxZoom: 18,
subdomains: "abc",
}).addTo(mapInstance);
let data = [];
try { data = JSON.parse(el.dataset.flats || "[]"); } catch (e) {}
const bounds = [];
data.forEach((f) => {
if (typeof f.lat !== "number" || typeof f.lng !== "number") return;
const rent = f.rent ? Math.round(f.rent) + " €" : "";
const rooms = f.rooms ? f.rooms + " Zi" : "";
const size = f.size ? Math.round(f.size) + " m²" : "";
const meta = [rooms, size, rent].filter(Boolean).join(" · ");
const safeAddr = (f.address || "").replace(/</g, "&lt;");
const safeLink = (f.link || "#").replace(/"/g, "&quot;");
L.marker([f.lat, f.lng]).addTo(mapInstance).bindPopup(
`<b>${safeAddr}</b>` + (meta ? `<br>${meta}` : "") +
`<br><a href="${safeLink}" target="_blank" rel="noopener">Zur Anzeige →</a>`,
);
bounds.push([f.lat, f.lng]);
});
if (bounds.length === 1) {
mapInstance.setView(bounds[0], 14);
} else if (bounds.length > 1) {
mapInstance.fitBounds(bounds, { padding: [30, 30] });
}
}
function ensureMap() {
const el = document.getElementById("flats-map");
if (!el || typeof L === "undefined") return;
// Container not actually visible yet → bail, we'll retry when the view toggles.
if (el.clientHeight < 10) return;
// Existing instance bound to THIS element → just recheck size.
if (mapInstance && mapInstance._container === el) {
try { mapInstance.invalidateSize(); } catch (e) {}
return;
}
buildMap(el);
}
function wireViewToggle() {
document.querySelectorAll('input[name="view_mode"]').forEach((r) => {
if (r.dataset.wired === "1") return;
r.dataset.wired = "1";
r.addEventListener("change", (e) => {
try { localStorage.setItem("lazyflat_view_mode", e.target.value); } catch (err) {}
// Wait for CSS :has() to reflow, then build/size the map.
requestAnimationFrame(() => requestAnimationFrame(ensureMap));
// belt & suspenders — re-check a couple more times in case of layout shifts.
setTimeout(ensureMap, 120);
setTimeout(ensureMap, 400);
});
});
}
function restoreView() {
let stored = null;
try { stored = localStorage.getItem("lazyflat_view_mode"); } catch (e) {}
if (!stored) return;
const el = document.querySelector(`input[name="view_mode"][value="${stored}"]`);
if (el && !el.checked) {
el.checked = true;
// Manually dispatching change would bubble and double-fire; call directly.
requestAnimationFrame(() => requestAnimationFrame(ensureMap));
setTimeout(ensureMap, 120);
setTimeout(ensureMap, 400);
}
}
function onReady() {
wireViewToggle();
restoreView();
ensureMap(); // handles the case where map view is already visible
}
document.addEventListener("DOMContentLoaded", onReady);
document.body && document.body.addEventListener("htmx:afterSwap", onReady);