From 5c6a79f7869b0da7a03be265686fc35ea110b0e0 Mon Sep 17 00:00:00 2001 From: Yasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com> Date: Mon, 2 Nov 2026 07:35:17 +0900 Subject: parks: スマホの地図操作と全画面ボタンを修正 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit スマホでは1本指でページをスクロールし、2本指で地図を移動・拡大できるようにした。全画面ボタンはFullscreen APIが使えない端末でもCSSで最大化する。 --- src/components/ParkMap.astro | 124 ++++++++++++++++++++++++++++++++++----- src/components/ParkMapHelp.astro | 2 +- 2 files changed, 111 insertions(+), 15 deletions(-) (limited to 'src/components') diff --git a/src/components/ParkMap.astro b/src/components/ParkMap.astro index ac85c0e..4d5541c 100644 --- a/src/components/ParkMap.astro +++ b/src/components/ParkMap.astro @@ -32,6 +32,22 @@ border: 0; border-radius: 0; } + .park-map.is-maximized { + position: fixed; + inset: 0; + width: 100vw; + height: 100dvh; + margin: 0; + border: 0; + border-radius: 0; + z-index: 10000; + } + :global(body.park-map-maximized) { + overflow: hidden; + } + :global(.park-map.park-map-touch.leaflet-container) { + touch-action: none; + } :global(.park-map-fullscreen button) { width: 30px; height: 30px; @@ -457,7 +473,13 @@ if (el && !el.dataset.initialized) { el.dataset.initialized = "1"; + const isTouch = + L.Browser.mobile || window.matchMedia("(pointer: coarse)").matches; const map = L.map(el, { + // タッチ端末では1本指のドラッグを無効にし、ページのスクロールを妨げない。 + // 地図の移動・拡大は2本指(touchZoom)で行う。 + dragging: !isTouch, + touchZoom: true, scrollWheelZoom: true, zoomSnap: 1, zoomDelta: 1, @@ -466,6 +488,55 @@ markerZoomAnimation: false, }).setView([35.728, 139.477], 13); + // タッチ端末では touch-action: none にして2本指ジェスチャーをLeafletに届け、 + // 1本指の縦ドラッグはページスクロールとして自前で処理する。 + if (isTouch) { + el.classList.add("park-map-touch"); + let tracking = false; + let lastX = 0; + let lastY = 0; + el.addEventListener( + "touchstart", + (e: TouchEvent) => { + if (e.touches.length !== 1) { + tracking = false; + return; + } + const target = e.target as Element | null; + if (target && target.closest(".leaflet-control")) { + tracking = false; + return; + } + tracking = true; + lastX = e.touches[0].clientX; + lastY = e.touches[0].clientY; + }, + { passive: true }, + ); + el.addEventListener( + "touchmove", + (e: TouchEvent) => { + if (!tracking || e.touches.length !== 1) return; + const x = e.touches[0].clientX; + const y = e.touches[0].clientY; + const dx = x - lastX; + const dy = y - lastY; + if (Math.abs(dy) >= Math.abs(dx)) { + e.preventDefault(); + window.scrollBy(0, -dy); + lastX = x; + lastY = y; + } + }, + { passive: false }, + ); + const stopTracking = () => { + tracking = false; + }; + el.addEventListener("touchend", stopTracking); + el.addEventListener("touchcancel", stopTracking); + } + // 帰属表示の「Leaflet」旗アイコンを消す(OSMの表示は残す)。 map.attributionControl.setPrefix(false); @@ -484,29 +555,54 @@ button.setAttribute("aria-label", "全画面表示"); button.innerHTML = "⛶"; L.DomEvent.disableClickPropagation(container); + L.DomEvent.disableScrollPropagation(container); + + const target = map.getContainer(); + const isCssMax = () => target.classList.contains("is-maximized"); + const update = () => { + const active = document.fullscreenElement === target || isCssMax(); + button.innerHTML = active ? "✕" : "⛶"; + button.title = active ? "全画面を終了" : "全画面表示"; + button.setAttribute("aria-label", button.title); + window.setTimeout(() => map.invalidateSize(), 60); + }; + const enterCssMax = () => { + target.classList.add("is-maximized"); + document.body.classList.add("park-map-maximized"); + update(); + }; + const exitCssMax = () => { + target.classList.remove("is-maximized"); + document.body.classList.remove("park-map-maximized"); + update(); + }; + L.DomEvent.on(button, "click", (ev: Event) => { L.DomEvent.stop(ev); - const target = map.getContainer(); - if (!document.fullscreenElement) { - target.requestFullscreen?.().catch(() => {}); + if (document.fullscreenElement === target) { + document.exitFullscreen?.().catch(() => exitCssMax()); + return; + } + if (isCssMax()) { + exitCssMax(); + return; + } + // Fullscreen API が使えない端末(iPhoneなど)はCSSで最大化する。 + if (document.fullscreenEnabled && target.requestFullscreen) { + target.requestFullscreen().catch(() => enterCssMax()); } else { - document.exitFullscreen?.().catch(() => {}); + enterCssMax(); } }); - const update = () => { - const active = document.fullscreenElement === map.getContainer(); - button.innerHTML = active ? "✕" : "⛶"; - button.title = active ? "全画面を終了" : "全画面表示"; - button.setAttribute("aria-label", active ? "全画面を終了" : "全画面表示"); - map.invalidateSize(); - }; + document.addEventListener("fullscreenchange", update); + document.addEventListener("keydown", (ev) => { + if (ev.key === "Escape" && isCssMax()) exitCssMax(); + }); return container; }, }); - if (document.fullscreenEnabled) { - new FullscreenControl({ position: "topright" }).addTo(map); - } + new FullscreenControl({ position: "topright" }).addTo(map); const layersByName = new Map(); const allLayers: any[] = []; diff --git a/src/components/ParkMapHelp.astro b/src/components/ParkMapHelp.astro index 4999389..d693a2d 100644 --- a/src/components/ParkMapHelp.astro +++ b/src/components/ParkMapHelp.astro @@ -20,7 +20,7 @@

- 公園の範囲を赤、市外の近い公園を青、公共施設を紫(市外はオレンジ)で表示しています。地図を拡大すると見やすくなります。PCでは地図上でスクロールするとズームできます。市外の近い公園のうち、範囲が取得できないものは丸で表示しています。トイレ・水飲み場・時計・消火栓・防火水槽のアイコンは拡大すると表示されます。さらに拡大すると、遊具アイコン(上の凡例)が表示されます。OpenStreetMapに公園として登録されている範囲を表示しているため、すべての公園を網羅しているわけではありません。現在地は端末内でのみ利用し、サーバーには送信しません。地図データ © OpenStreetMap