diff --git a/fes.js b/fes.js index 56df3f3..8e84559 100644 --- a/fes.js +++ b/fes.js @@ -1,7 +1,6 @@ /* script.js - スマホ優先で作り込んだマップロジック + Panzoom対応 - CSV: data/floor1.csv / data/floor2.csv を想定 - - CSVヘッダ(日本語 or 英語): 名前,name / 説明,description / 画像,image / X座標,x / Y座標,y */ 'use strict'; @@ -24,7 +23,7 @@ let tooltipEls = []; let panzoomInstance = null; -/* ================= CSV パーサ ================= */ +/* ---------------- CSV パーサなど(前と同様) ---------------- */ function parseCSV(text) { const rows = []; let cur = '', row = [], inQuotes = false; @@ -90,8 +89,6 @@ return []; } } - -/* 画像ロード待ち */ function waitImageLoad(imgEl) { return new Promise(resolve => { if (imgEl.complete && imgEl.naturalWidth !== 0) return resolve(); @@ -100,45 +97,35 @@ }); } -/* ツールチップ位置調整(map 内の座標 -> ビューポートに合わせて補正) */ +/* ---------------- ツールチップの位置調整(viewport基準) ---------------- */ function adjustTooltipPosition(tooltip, desiredLeftPxMap, desiredTopPxMap) { - // mapRect = map 要素のビューポート座標 const mapRect = map.getBoundingClientRect(); const desiredLeftViewport = mapRect.left + desiredLeftPxMap; const desiredTopViewport = mapRect.top + desiredTopPxMap; - // temporarily show (hidden) to measure size tooltip.style.display = 'block'; tooltip.style.visibility = 'hidden'; tooltip.style.transform = 'translate(-50%, -120%)'; - // measure tooltip in viewport coords const tRect = tooltip.getBoundingClientRect(); const margin = 8; - // horizontal shift (viewport) let shiftXViewport = 0; if (tRect.left < margin) shiftXViewport = margin - tRect.left; if (tRect.right > window.innerWidth - margin) shiftXViewport = (window.innerWidth - margin) - tRect.right; - // vertical flip if top would go above viewport margin - let flipped = false; let newTopViewport = desiredTopViewport; if (tRect.top < margin) { - flipped = true; - tooltip.style.transform = 'translate(-50%, 8px)'; // put below - // put new top slightly below pin (pin height ~44) + tooltip.style.transform = 'translate(-50%, 8px)'; newTopViewport = desiredTopViewport + 44 + 10; } else { tooltip.style.transform = 'translate(-50%, -120%)'; } - // convert shiftXViewport to map-relative px (mapRect left offset) - const shiftXMap = shiftXViewport; // because left in px is same units + const shiftXMap = shiftXViewport; const newLeftMap = desiredLeftPxMap + shiftXMap; const newTopMap = newTopViewport - mapRect.top; - // apply tooltip.style.left = newLeftMap + 'px'; tooltip.style.top = newTopMap + 'px'; tooltip.style.visibility = 'visible'; @@ -149,14 +136,12 @@ tooltipEls.forEach(t => { if (t) { t.style.display = 'none'; t.setAttribute('aria-hidden','true'); } }); } -/* ピン配置(data は csvToObjects の配列) */ +/* ---------------- ピン配置 ---------------- */ function placePins(data) { - // remove existing pinEls.forEach(p => p.remove()); tooltipEls.forEach(t => t.remove()); pinEls = []; tooltipEls = []; - // img の表示サイズ取得(縦長で height 基準) const rect = img.getBoundingClientRect(); const width = rect.width; const height = rect.height; @@ -165,7 +150,6 @@ const xPx = d.x * width; const yPx = d.y * height; - // Pin const pin = document.createElement('div'); pin.className = 'pin'; pin.setAttribute('role','button'); @@ -180,7 +164,7 @@ pinImg.alt = ''; pin.appendChild(pinImg); - // stop propagation so Panzoom doesn't begin pan when tapping a pin + // stop pointer propagation to avoid accidental pan start pin.addEventListener('pointerdown', e => e.stopPropagation()); pin.addEventListener('touchstart', e => e.stopPropagation(), {passive:true}); @@ -188,51 +172,27 @@ if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); pin.click(); } }); - // Tooltip (card) + // Tooltip const tooltip = document.createElement('div'); tooltip.className = 'tooltip'; tooltip.dataset.index = String(idx); tooltip.setAttribute('aria-hidden','true'); - const card = document.createElement('div'); - card.className = 'tooltip-card'; + const card = document.createElement('div'); card.className = 'tooltip-card'; + const closeBtn = document.createElement('button'); closeBtn.className = 'close-btn'; closeBtn.type='button'; closeBtn.textContent='✕'; closeBtn.setAttribute('aria-label','閉じる'); + const header = document.createElement('div'); header.className='tooltip-header'; header.textContent = d.name || '(無題)'; + const body = document.createElement('div'); body.className='tooltip-body'; + const thumb = document.createElement('img'); thumb.className='tooltip-thumb'; thumb.src = d.image ? d.image : 'images/placeholder.png'; thumb.alt = d.name || ''; + const p = document.createElement('p'); p.className='desc'; p.textContent = d.description || ''; - const closeBtn = document.createElement('button'); - closeBtn.className = 'close-btn'; - closeBtn.type = 'button'; - closeBtn.textContent = '✕'; - closeBtn.setAttribute('aria-label','閉じる'); - - const header = document.createElement('div'); - header.className = 'tooltip-header'; - header.textContent = d.name || '(無題)'; - - const body = document.createElement('div'); - body.className = 'tooltip-body'; - - const thumb = document.createElement('img'); - thumb.className = 'tooltip-thumb'; - thumb.src = d.image ? d.image : 'images/placeholder.png'; - thumb.alt = d.name || ''; - - const p = document.createElement('p'); - p.className = 'desc'; - p.textContent = d.description || ''; - - body.appendChild(thumb); - body.appendChild(p); - - card.appendChild(closeBtn); - card.appendChild(header); - card.appendChild(body); + body.appendChild(thumb); body.appendChild(p); + card.appendChild(closeBtn); card.appendChild(header); card.appendChild(body); tooltip.appendChild(card); - // initial hidden position (map-relative) tooltip.style.left = xPx + 'px'; tooltip.style.top = yPx + 'px'; tooltip.style.display = 'none'; - // events pin.addEventListener('click', (e) => { e.stopPropagation(); const isVisible = tooltip.style.display === 'block'; @@ -253,11 +213,9 @@ tooltip.setAttribute('aria-hidden','true'); }); - // prevent tooltip pointer events from panning map (stop propagation) tooltip.addEventListener('pointerdown', ev => ev.stopPropagation()); tooltip.addEventListener('touchstart', ev => ev.stopPropagation(), {passive:true}); - // append map.appendChild(pin); map.appendChild(tooltip); pinEls.push(pin); @@ -267,7 +225,7 @@ renderListPanel(data); } -/* 一覧パネルに描画 */ +/* 一覧パネル */ function renderListPanel(data) { listContent.innerHTML = ''; data.forEach((d, idx) => { @@ -280,22 +238,12 @@ thumb.src = d.image ? d.image : 'images/placeholder.png'; thumb.alt = d.name || ''; - const meta = document.createElement('div'); - meta.className = 'shop-meta'; + const meta = document.createElement('div'); meta.className='shop-meta'; + const title = document.createElement('p'); title.className='shop-title'; title.textContent = d.name || `スポット ${idx+1}`; + const desc = document.createElement('p'); desc.className='shop-desc'; desc.textContent = d.description || ''; - const title = document.createElement('p'); - title.className = 'shop-title'; - title.textContent = d.name || `スポット ${idx+1}`; - - const desc = document.createElement('p'); - desc.className = 'shop-desc'; - desc.textContent = d.description || ''; - - meta.appendChild(title); - meta.appendChild(desc); - - item.appendChild(thumb); - item.appendChild(meta); + meta.appendChild(title); meta.appendChild(desc); + item.appendChild(thumb); item.appendChild(meta); item.addEventListener('click', () => { openTooltipByIndex(idx); @@ -305,19 +253,12 @@ listContent.appendChild(item); }); } - -/* 指定 index の tooltip を開く */ function openTooltipByIndex(index) { if (!pinEls[index]) return; - // ensure visible on screen: scroll to map const topY = Math.max(0, map.getBoundingClientRect().top + window.scrollY - 80); window.scrollTo({ top: topY, behavior: 'smooth' }); - - // show tooltip pinEls[index].click(); } - -/* フロア読み込み */ async function loadFloor(floor) { if (currentFloor === floor) return; tabButtons.forEach(btn => { @@ -333,29 +274,23 @@ currentData = data; placePins(data); - // reset zoom so new floor displays consistently + // リセット(左揃えを保つため transform を初期化) if (panzoomInstance && typeof panzoomInstance.reset === 'function') { panzoomInstance.reset(); + // さらに明示的に pan を 0,0 にする(左上を基準に) + try { panzoomInstance.pan(0, 0); } catch (e) { /* ignore if not supported */ } } } - -/* 一覧 open/close */ function openList() { - listPanel.classList.add('open'); - listPanel.setAttribute('aria-hidden','false'); - listToggle.setAttribute('aria-expanded','true'); - setTimeout(() => listContent.focus(), 160); + listPanel.classList.add('open'); listPanel.setAttribute('aria-hidden','false'); listToggle.setAttribute('aria-expanded','true'); setTimeout(()=>listContent.focus(),160); } function closeList() { - listPanel.classList.remove('open'); - listPanel.setAttribute('aria-hidden','true'); - listToggle.setAttribute('aria-expanded','false'); - listToggle.focus(); + listPanel.classList.remove('open'); listPanel.setAttribute('aria-hidden','true'); listToggle.setAttribute('aria-expanded','false'); listToggle.focus(); } /* 初期化 */ async function init() { - // Panzoom を map 要素に適用(map 内の img + pin を一緒に拡大縮小) + // Panzoom を map 要素に適用 panzoomInstance = Panzoom(map, { maxScale: 4, minScale: 1, @@ -363,9 +298,9 @@ contain: 'outside' }); - // wheel は親(map-wrapper)でキャプチャして zoomWithWheel を使う + // wheel は wrapper 側で扱う const wrapper = map.parentElement; - wrapper.addEventListener('wheel', panzoomInstance.zoomWithWheel); + wrapper.addEventListener('wheel', panzoomInstance.zoomWithWheel, {passive:false}); // タブ tabButtons.forEach(btn => { @@ -375,14 +310,13 @@ }); }); - // グローバルタップでツールチップを閉じる + // グローバルタップで吹き出し閉じる document.addEventListener('pointerdown', (ev) => { const target = ev.target; if (target.closest('.pin') || target.closest('.tooltip') || target.closest('.list-panel') || target.closest('.list-toggle')) return; closeAllTooltips(); }); - // 一覧操作 listToggle.addEventListener('click', openList); closeListBtn.addEventListener('click', closeList); @@ -393,12 +327,17 @@ resizeTimer = setTimeout(() => { if (currentData && currentData.length) placePins(currentData); }, 200); }); - // 最初に 1F を読み込み + // 初期ロード(1F) img.src = IMAGE_PATH[1]; await waitImageLoad(img); currentData = await loadCSV(CSV_PATH[1]); placePins(currentData); + + // 最初は左揃えを確実にする(reset + pan(0,0)) + if (panzoomInstance) { + panzoomInstance.reset(); + try { panzoomInstance.pan(0,0); } catch (e) {} + } } -// 起動 document.addEventListener('DOMContentLoaded', init);