diff --git a/web/app.py b/web/app.py index bcc983a..17d560c 100644 --- a/web/app.py +++ b/web/app.py @@ -133,7 +133,7 @@ async def security_headers(request: Request, call_next): "default-src 'self'; " "script-src 'self' https://cdn.tailwindcss.com https://unpkg.com; " "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';" ) return resp diff --git a/web/static/map.js b/web/static/map.js index d9dc155..9cc6c3e 100644 --- a/web/static/map.js +++ b/web/static/map.js @@ -1,15 +1,13 @@ // lazyflat — Leaflet flat map -// A single Leaflet map instance; re-initialised after every HTMX swap of -// the Wohnungen body. Also flushes size when the view toggle flips from -// list to map (Leaflet needs invalidateSize on a hidden-then-shown 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 initFlatsMap() { - const el = document.getElementById("flats-map"); - if (!el || typeof L === "undefined") return; +function buildMap(el) { if (mapInstance) { try { mapInstance.remove(); } catch (e) {} mapInstance = null; @@ -25,29 +23,23 @@ function initFlatsMap() { 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) { - console.warn("flats-map: bad JSON in data-flats", e); - } - + 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 m = L.marker([f.lat, f.lng]).addTo(mapInstance); 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(/${safeAddr}` + - (meta ? `
${meta}` : "") + - `
Zur Anzeige →`, + L.marker([f.lat, f.lng]).addTo(mapInstance).bindPopup( + `${safeAddr}` + (meta ? `
${meta}` : "") + + `
Zur Anzeige →`, ); bounds.push([f.lat, f.lng]); }); @@ -58,10 +50,20 @@ function initFlatsMap() { } } -function flushMapSize() { - if (mapInstance) { - setTimeout(() => mapInstance.invalidateSize(), 50); +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() { @@ -69,33 +71,35 @@ function wireViewToggle() { 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) {} - flushMapSize(); + 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 (err) {} + try { stored = localStorage.getItem("lazyflat_view_mode"); } catch (e) {} if (!stored) return; - const el = document.querySelector( - `input[name="view_mode"][value="${stored}"]`, - ); + const el = document.querySelector(`input[name="view_mode"][value="${stored}"]`); if (el && !el.checked) { 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() { - initFlatsMap(); wireViewToggle(); restoreView(); + ensureMap(); // handles the case where map view is already visible } + document.addEventListener("DOMContentLoaded", onReady); document.body && document.body.addEventListener("htmx:afterSwap", onReady);