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>
This commit is contained in:
parent
42377f0b67
commit
4fd0b50a43
2 changed files with 37 additions and 33 deletions
|
|
@ -133,7 +133,7 @@ async def security_headers(request: Request, call_next):
|
||||||
"default-src 'self'; "
|
"default-src 'self'; "
|
||||||
"script-src 'self' https://cdn.tailwindcss.com https://unpkg.com; "
|
"script-src 'self' https://cdn.tailwindcss.com https://unpkg.com; "
|
||||||
"style-src 'self' https://cdn.tailwindcss.com https://unpkg.com 'unsafe-inline'; "
|
"style-src 'self' https://cdn.tailwindcss.com https://unpkg.com 'unsafe-inline'; "
|
||||||
"img-src 'self' data: https://*.tile.openstreetmap.org https://tile.openstreetmap.org; "
|
"img-src 'self' data: https://*.tile.openstreetmap.org https://tile.openstreetmap.org https://unpkg.com; "
|
||||||
"connect-src 'self'; frame-ancestors 'none';"
|
"connect-src 'self'; frame-ancestors 'none';"
|
||||||
)
|
)
|
||||||
return resp
|
return resp
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
// lazyflat — Leaflet flat map
|
// lazyflat — Leaflet flat map
|
||||||
// A single Leaflet map instance; re-initialised after every HTMX swap of
|
// Initialised LAZILY: we only build the Leaflet instance when the container
|
||||||
// the Wohnungen body. Also flushes size when the view toggle flips from
|
// actually has a rendered size (> 0 height). Building it on a hidden 0×0
|
||||||
// list to map (Leaflet needs invalidateSize on a hidden-then-shown map).
|
// container leaves Leaflet in a state where tiles never load.
|
||||||
|
|
||||||
let mapInstance = null;
|
let mapInstance = null;
|
||||||
const BERLIN_CENTER = [52.52, 13.405];
|
const BERLIN_CENTER = [52.52, 13.405];
|
||||||
const BERLIN_ZOOM = 11;
|
const BERLIN_ZOOM = 11;
|
||||||
|
|
||||||
function initFlatsMap() {
|
function buildMap(el) {
|
||||||
const el = document.getElementById("flats-map");
|
|
||||||
if (!el || typeof L === "undefined") return;
|
|
||||||
if (mapInstance) {
|
if (mapInstance) {
|
||||||
try { mapInstance.remove(); } catch (e) {}
|
try { mapInstance.remove(); } catch (e) {}
|
||||||
mapInstance = null;
|
mapInstance = null;
|
||||||
|
|
@ -25,28 +23,22 @@ function initFlatsMap() {
|
||||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||||
attribution: "© OpenStreetMap",
|
attribution: "© OpenStreetMap",
|
||||||
maxZoom: 18,
|
maxZoom: 18,
|
||||||
|
subdomains: "abc",
|
||||||
}).addTo(mapInstance);
|
}).addTo(mapInstance);
|
||||||
|
|
||||||
let data = [];
|
let data = [];
|
||||||
try {
|
try { data = JSON.parse(el.dataset.flats || "[]"); } catch (e) {}
|
||||||
data = JSON.parse(el.dataset.flats || "[]");
|
|
||||||
} catch (e) {
|
|
||||||
console.warn("flats-map: bad JSON in data-flats", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
const bounds = [];
|
const bounds = [];
|
||||||
data.forEach((f) => {
|
data.forEach((f) => {
|
||||||
if (typeof f.lat !== "number" || typeof f.lng !== "number") return;
|
if (typeof f.lat !== "number" || typeof f.lng !== "number") return;
|
||||||
const m = L.marker([f.lat, f.lng]).addTo(mapInstance);
|
|
||||||
const rent = f.rent ? Math.round(f.rent) + " €" : "";
|
const rent = f.rent ? Math.round(f.rent) + " €" : "";
|
||||||
const rooms = f.rooms ? f.rooms + " Zi" : "";
|
const rooms = f.rooms ? f.rooms + " Zi" : "";
|
||||||
const size = f.size ? Math.round(f.size) + " m²" : "";
|
const size = f.size ? Math.round(f.size) + " m²" : "";
|
||||||
const meta = [rooms, size, rent].filter(Boolean).join(" · ");
|
const meta = [rooms, size, rent].filter(Boolean).join(" · ");
|
||||||
const safeAddr = (f.address || "").replace(/</g, "<");
|
const safeAddr = (f.address || "").replace(/</g, "<");
|
||||||
const safeLink = (f.link || "#").replace(/"/g, """);
|
const safeLink = (f.link || "#").replace(/"/g, """);
|
||||||
m.bindPopup(
|
L.marker([f.lat, f.lng]).addTo(mapInstance).bindPopup(
|
||||||
`<b>${safeAddr}</b>` +
|
`<b>${safeAddr}</b>` + (meta ? `<br>${meta}` : "") +
|
||||||
(meta ? `<br>${meta}` : "") +
|
|
||||||
`<br><a href="${safeLink}" target="_blank" rel="noopener">Zur Anzeige →</a>`,
|
`<br><a href="${safeLink}" target="_blank" rel="noopener">Zur Anzeige →</a>`,
|
||||||
);
|
);
|
||||||
bounds.push([f.lat, f.lng]);
|
bounds.push([f.lat, f.lng]);
|
||||||
|
|
@ -58,10 +50,20 @@ function initFlatsMap() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function flushMapSize() {
|
function ensureMap() {
|
||||||
if (mapInstance) {
|
const el = document.getElementById("flats-map");
|
||||||
setTimeout(() => mapInstance.invalidateSize(), 50);
|
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() {
|
function wireViewToggle() {
|
||||||
|
|
@ -69,33 +71,35 @@ function wireViewToggle() {
|
||||||
if (r.dataset.wired === "1") return;
|
if (r.dataset.wired === "1") return;
|
||||||
r.dataset.wired = "1";
|
r.dataset.wired = "1";
|
||||||
r.addEventListener("change", (e) => {
|
r.addEventListener("change", (e) => {
|
||||||
try {
|
try { localStorage.setItem("lazyflat_view_mode", e.target.value); } catch (err) {}
|
||||||
localStorage.setItem("lazyflat_view_mode", e.target.value);
|
// Wait for CSS :has() to reflow, then build/size the map.
|
||||||
} catch (err) {}
|
requestAnimationFrame(() => requestAnimationFrame(ensureMap));
|
||||||
flushMapSize();
|
// belt & suspenders — re-check a couple more times in case of layout shifts.
|
||||||
|
setTimeout(ensureMap, 120);
|
||||||
|
setTimeout(ensureMap, 400);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function restoreView() {
|
function restoreView() {
|
||||||
let stored = null;
|
let stored = null;
|
||||||
try {
|
try { stored = localStorage.getItem("lazyflat_view_mode"); } catch (e) {}
|
||||||
stored = localStorage.getItem("lazyflat_view_mode");
|
|
||||||
} catch (err) {}
|
|
||||||
if (!stored) return;
|
if (!stored) return;
|
||||||
const el = document.querySelector(
|
const el = document.querySelector(`input[name="view_mode"][value="${stored}"]`);
|
||||||
`input[name="view_mode"][value="${stored}"]`,
|
|
||||||
);
|
|
||||||
if (el && !el.checked) {
|
if (el && !el.checked) {
|
||||||
el.checked = true;
|
el.checked = true;
|
||||||
flushMapSize();
|
// Manually dispatching change would bubble and double-fire; call directly.
|
||||||
|
requestAnimationFrame(() => requestAnimationFrame(ensureMap));
|
||||||
|
setTimeout(ensureMap, 120);
|
||||||
|
setTimeout(ensureMap, 400);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onReady() {
|
function onReady() {
|
||||||
initFlatsMap();
|
|
||||||
wireViewToggle();
|
wireViewToggle();
|
||||||
restoreView();
|
restoreView();
|
||||||
|
ensureMap(); // handles the case where map view is already visible
|
||||||
}
|
}
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", onReady);
|
document.addEventListener("DOMContentLoaded", onReady);
|
||||||
document.body && document.body.addEventListener("htmx:afterSwap", onReady);
|
document.body && document.body.addEventListener("htmx:afterSwap", onReady);
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue