Feat: Improve marker tooltips and Excel parsing
- Add toggle button for marker tooltips (Show All / Show Active) - Update tooltip to use HTML formatting (bold name, secondary phrase) - Improve Excel column parsing using nullish coalescing operator - Add support for '사진' (photo) column in Excel import
This commit is contained in:
146
index.html
146
index.html
@@ -253,6 +253,7 @@ body { font-family: 'Segoe UI', sans-serif; display: flex; height: 100vh; color:
|
|||||||
<div class="stats-bar">
|
<div class="stats-bar">
|
||||||
<span id="stats">0 / 0 placed</span>
|
<span id="stats">0 / 0 placed</span>
|
||||||
<div class="filter-btns">
|
<div class="filter-btns">
|
||||||
|
<button id="toggle-tooltips" class="active" title="말풍선 켜기/끄기">💬 켜짐</button>
|
||||||
<button id="filter-all" class="active">All</button>
|
<button id="filter-all" class="active">All</button>
|
||||||
<button id="filter-unplaced">To Place</button>
|
<button id="filter-unplaced">To Place</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -458,6 +459,37 @@ let flags = [];
|
|||||||
let nextId = 1;
|
let nextId = 1;
|
||||||
let selectedFlagId = null;
|
let selectedFlagId = null;
|
||||||
let filterMode = 'all';
|
let filterMode = 'all';
|
||||||
|
let showAllTooltips = true;
|
||||||
|
let activeTooltipId = null;
|
||||||
|
|
||||||
|
function refreshMarkerTooltip(flag) {
|
||||||
|
if (!flag.marker) return;
|
||||||
|
flag.marker.unbindTooltip();
|
||||||
|
const text = getFlagTooltipHtml(flag);
|
||||||
|
if (text) {
|
||||||
|
const isPermanent = showAllTooltips || activeTooltipId === flag.id;
|
||||||
|
flag.marker.bindTooltip(text, {
|
||||||
|
permanent: isPermanent,
|
||||||
|
direction: 'top',
|
||||||
|
offset: [0, -17],
|
||||||
|
className: 'flag-marker-tooltip'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setActiveTooltip(flagId) {
|
||||||
|
if (showAllTooltips) return;
|
||||||
|
const prevId = activeTooltipId;
|
||||||
|
activeTooltipId = flagId;
|
||||||
|
if (prevId) {
|
||||||
|
const prevFlag = flags.find(f => f.id === prevId);
|
||||||
|
if (prevFlag) refreshMarkerTooltip(prevFlag);
|
||||||
|
}
|
||||||
|
if (flagId) {
|
||||||
|
const flag = flags.find(f => f.id === flagId);
|
||||||
|
if (flag) refreshMarkerTooltip(flag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// ─── API Key UI ──────────────────────────────────────────
|
// ─── API Key UI ──────────────────────────────────────────
|
||||||
@@ -495,9 +527,13 @@ function esc(s) {
|
|||||||
({'&':'&','<':'<','>':'>','"':'"'})[c]);
|
({'&':'&','<':'<','>':'>','"':'"'})[c]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFlagTooltipText(flag) {
|
function getFlagTooltipHtml(flag) {
|
||||||
if (!flag.name) return flag.phrase || '';
|
const n = esc(flag.name || '');
|
||||||
return flag.phrase ? `${flag.name} · ${flag.phrase}` : flag.name;
|
const p = esc(flag.phrase || '');
|
||||||
|
if (!n && !p) return '';
|
||||||
|
if (!n) return `<div style="text-align:center; line-height:1.3;">${p}</div>`;
|
||||||
|
if (!p) return `<div style="text-align:center; line-height:1.3;"><strong>${n}</strong></div>`;
|
||||||
|
return `<div style="text-align:center; line-height:1.3;"><strong>${n}</strong><br><span style="font-size:0.9em;color:#555;">${p}</span></div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
@@ -578,6 +614,7 @@ function renderList() {
|
|||||||
header.addEventListener('click', (e) => {
|
header.addEventListener('click', (e) => {
|
||||||
if (e.target.closest('.actions')) return;
|
if (e.target.closest('.actions')) return;
|
||||||
flag.expanded = !flag.expanded;
|
flag.expanded = !flag.expanded;
|
||||||
|
if (flag.expanded && flag.marker) setActiveTooltip(flag.id);
|
||||||
renderList();
|
renderList();
|
||||||
});
|
});
|
||||||
li.appendChild(header);
|
li.appendChild(header);
|
||||||
@@ -616,7 +653,10 @@ function renderList() {
|
|||||||
|
|
||||||
header.querySelector('.idx')?.addEventListener('click', (e) => {
|
header.querySelector('.idx')?.addEventListener('click', (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (flag.marker) map.flyTo(flag.marker.getLatLng(), 17);
|
if (flag.marker) {
|
||||||
|
setActiveTooltip(flag.id);
|
||||||
|
map.flyTo(flag.marker.getLatLng(), 17);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
header.querySelector('[data-action="edit-name"]')?.addEventListener('click', (e) => {
|
header.querySelector('[data-action="edit-name"]')?.addEventListener('click', (e) => {
|
||||||
e.stopPropagation(); openNameEdit(flag.id);
|
e.stopPropagation(); openNameEdit(flag.id);
|
||||||
@@ -638,6 +678,7 @@ function removeFlag(id) {
|
|||||||
const flag = flags.find(f => f.id === id);
|
const flag = flags.find(f => f.id === id);
|
||||||
if (!flag || !flag.placed) return;
|
if (!flag || !flag.placed) return;
|
||||||
if (flag.marker) map.removeLayer(flag.marker);
|
if (flag.marker) map.removeLayer(flag.marker);
|
||||||
|
if (activeTooltipId === id) setActiveTooltip(null);
|
||||||
flag.placed = false; flag.lat = null; flag.lng = null;
|
flag.placed = false; flag.lat = null; flag.lng = null;
|
||||||
flag.address = null; flag.marker = null;
|
flag.address = null; flag.marker = null;
|
||||||
if (selectedFlagId === id) selectedFlagId = null;
|
if (selectedFlagId === id) selectedFlagId = null;
|
||||||
@@ -674,13 +715,7 @@ document.getElementById('name-edit-apply').addEventListener('click', () => {
|
|||||||
if (nameVal) {
|
if (nameVal) {
|
||||||
flag.name = nameVal;
|
flag.name = nameVal;
|
||||||
flag.phrase = phraseVal;
|
flag.phrase = phraseVal;
|
||||||
if (flag.marker) {
|
if (flag.marker) refreshMarkerTooltip(flag);
|
||||||
flag.marker.unbindTooltip();
|
|
||||||
const tooltipText = esc(getFlagTooltipText(flag));
|
|
||||||
if (tooltipText) {
|
|
||||||
flag.marker.bindTooltip(tooltipText, { permanent: true, direction: 'right', offset: [10, 0], className: 'flag-marker-tooltip' });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
renderList();
|
renderList();
|
||||||
}
|
}
|
||||||
document.getElementById('name-edit-modal').classList.remove('open');
|
document.getElementById('name-edit-modal').classList.remove('open');
|
||||||
@@ -721,38 +756,70 @@ document.getElementById('photo-overlay').addEventListener('click', () => {
|
|||||||
// ─── Map Click ──────────────────────────────────────────
|
// ─── Map Click ──────────────────────────────────────────
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
map.on('click', (e) => {
|
map.on('click', (e) => {
|
||||||
if (!selectedFlagId) { alert('Select a flag from the list first.'); return; }
|
let clearedTooltip = false;
|
||||||
|
if (!showAllTooltips && activeTooltipId) {
|
||||||
|
setActiveTooltip(null);
|
||||||
|
clearedTooltip = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!selectedFlagId) {
|
||||||
|
if (!clearedTooltip && flags.length > 0) alert('Select a flag from the list first.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
const flag = flags.find(f => f.id === selectedFlagId);
|
const flag = flags.find(f => f.id === selectedFlagId);
|
||||||
if (!flag || flag.placed) return;
|
if (!flag || flag.placed) return;
|
||||||
|
|
||||||
const { lat, lng } = e.latlng;
|
const { lat, lng } = e.latlng;
|
||||||
const displayNum = flag.number || flags.indexOf(flag) + 1;
|
const displayNum = flag.number || flags.indexOf(flag) + 1;
|
||||||
const marker = L.marker([lat, lng], { icon: makeNumIcon(displayNum), draggable: true }).addTo(map);
|
const marker = L.marker([lat, lng], { icon: makeNumIcon(displayNum), draggable: true }).addTo(map);
|
||||||
const tooltipText = esc(getFlagTooltipText(flag));
|
|
||||||
if (tooltipText) marker.bindTooltip(tooltipText, { permanent: true, direction: 'right', offset: [10, 0], className: 'flag-marker-tooltip' });
|
|
||||||
marker.on('dragend', () => {
|
marker.on('dragend', () => {
|
||||||
const p = marker.getLatLng(); flag.lat = p.lat; flag.lng = p.lng;
|
const p = marker.getLatLng(); flag.lat = p.lat; flag.lng = p.lng;
|
||||||
scheduleGeocode(flag); renderList();
|
scheduleGeocode(flag); renderList();
|
||||||
});
|
});
|
||||||
marker.on('contextmenu', () => { removeFlag(flag.id); });
|
marker.on('contextmenu', () => { removeFlag(flag.id); });
|
||||||
|
marker.on('click', () => { setActiveTooltip(flag.id); });
|
||||||
|
|
||||||
flag.placed = true; flag.lat = lat; flag.lng = lng; flag.marker = marker;
|
flag.placed = true; flag.lat = lat; flag.lng = lng; flag.marker = marker;
|
||||||
|
refreshMarkerTooltip(flag);
|
||||||
|
|
||||||
selectedFlagId = null;
|
selectedFlagId = null;
|
||||||
renderList();
|
renderList();
|
||||||
scheduleGeocode(flag);
|
scheduleGeocode(flag);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
// ─── Filters ────────────────────────────────────────────
|
// ─── Filters & Tooltip Toggle ───────────────────────────
|
||||||
// ═══════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════
|
||||||
|
document.getElementById('toggle-tooltips').addEventListener('click', (e) => {
|
||||||
|
showAllTooltips = !showAllTooltips;
|
||||||
|
const btn = e.target;
|
||||||
|
if (showAllTooltips) {
|
||||||
|
btn.classList.add('active');
|
||||||
|
btn.textContent = '💬 켜짐';
|
||||||
|
activeTooltipId = null;
|
||||||
|
} else {
|
||||||
|
btn.classList.remove('active');
|
||||||
|
btn.textContent = '💬 꺼짐';
|
||||||
|
}
|
||||||
|
flags.forEach(f => {
|
||||||
|
if (f.placed) refreshMarkerTooltip(f);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
document.getElementById('filter-all').addEventListener('click', () => {
|
document.getElementById('filter-all').addEventListener('click', () => {
|
||||||
filterMode = 'all';
|
filterMode = 'all';
|
||||||
document.querySelectorAll('.filter-btns button').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.filter-btns button').forEach(b => {
|
||||||
|
if (b.id !== 'toggle-tooltips') b.classList.remove('active');
|
||||||
|
});
|
||||||
document.getElementById('filter-all').classList.add('active');
|
document.getElementById('filter-all').classList.add('active');
|
||||||
renderList();
|
renderList();
|
||||||
});
|
});
|
||||||
document.getElementById('filter-unplaced').addEventListener('click', () => {
|
document.getElementById('filter-unplaced').addEventListener('click', () => {
|
||||||
filterMode = 'unplaced';
|
filterMode = 'unplaced';
|
||||||
document.querySelectorAll('.filter-btns button').forEach(b => b.classList.remove('active'));
|
document.querySelectorAll('.filter-btns button').forEach(b => {
|
||||||
|
if (b.id !== 'toggle-tooltips') b.classList.remove('active');
|
||||||
|
});
|
||||||
document.getElementById('filter-unplaced').classList.add('active');
|
document.getElementById('filter-unplaced').classList.add('active');
|
||||||
renderList();
|
renderList();
|
||||||
});
|
});
|
||||||
@@ -764,36 +831,55 @@ document.getElementById('file-input').addEventListener('change', async (e) => {
|
|||||||
const file = e.target.files[0];
|
const file = e.target.files[0];
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
for (const f of flags) { if (f.marker) map.removeLayer(f.marker); }
|
for (const f of flags) { if (f.marker) map.removeLayer(f.marker); }
|
||||||
flags = []; nextId = 1; selectedFlagId = null;
|
flags = []; nextId = 1; selectedFlagId = null; activeTooltipId = null;
|
||||||
|
|
||||||
const data = await file.arrayBuffer();
|
const data = await file.arrayBuffer();
|
||||||
const wb = XLSX.read(data, { type: 'array' });
|
const wb = XLSX.read(data, { type: 'array' });
|
||||||
const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { defval: '' });
|
const rows = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]], { defval: '' });
|
||||||
|
|
||||||
for (const row of rows) {
|
for (const row of rows) {
|
||||||
const name = row.Name || row.name || row.이름 || row['Flag Name'] || Object.values(row)[0] || '';
|
let name = row.Name ?? row.name ?? row.이름 ?? row['Flag Name'];
|
||||||
const number = row.Number || row.number || row.숫자 || row['Flag Number'] || Object.values(row)[1] || '';
|
let number = row.Number ?? row.number ?? row.숫자 ?? row['Flag Number'];
|
||||||
const phrase = row.Phrase || row.phrase || row.문구 || row['Flag Phrase'] || row['Flag Text'] || row.Text || '';
|
let phrase = row.Phrase ?? row.phrase ?? row.문구 ?? row['Flag Phrase'] ?? row['Flag Text'] ?? row.Text;
|
||||||
if (!String(name).trim()) continue;
|
|
||||||
const flag = addFlag(String(name).trim(), String(number).trim(), String(phrase).trim());
|
|
||||||
|
|
||||||
const photo = row.Photo || row.photo || '';
|
if (name === undefined && number === undefined) {
|
||||||
|
const vals = Object.values(row);
|
||||||
|
name = vals[0];
|
||||||
|
number = vals[1];
|
||||||
|
if (phrase === undefined) phrase = vals[2];
|
||||||
|
}
|
||||||
|
|
||||||
|
name = String(name ?? '').trim();
|
||||||
|
number = String(number ?? '').trim();
|
||||||
|
phrase = String(phrase ?? '').trim();
|
||||||
|
|
||||||
|
if (!name) continue;
|
||||||
|
const flag = addFlag(name, number, phrase);
|
||||||
|
|
||||||
|
const photo = row.Photo ?? row.photo ?? row.사진;
|
||||||
if (photo) flag.photoPath = String(photo);
|
if (photo) flag.photoPath = String(photo);
|
||||||
|
|
||||||
let lat = parseFloat(row.Latitude || row.latitude || row.위도);
|
let latRaw = row.Latitude ?? row.latitude ?? row.위도;
|
||||||
let lng = parseFloat(row.Longitude || row.longitude || row.경도);
|
let lngRaw = row.Longitude ?? row.longitude ?? row.경도;
|
||||||
|
let lat = parseFloat(latRaw);
|
||||||
|
let lng = parseFloat(lngRaw);
|
||||||
|
|
||||||
if (!isNaN(lat) && !isNaN(lng)) {
|
if (!isNaN(lat) && !isNaN(lng)) {
|
||||||
const displayNum = flag.number || flags.indexOf(flag) + 1;
|
const displayNum = flag.number || flags.indexOf(flag) + 1;
|
||||||
const marker = L.marker([lat, lng], { icon: makeNumIcon(displayNum), draggable: true }).addTo(map);
|
const marker = L.marker([lat, lng], { icon: makeNumIcon(displayNum), draggable: true }).addTo(map);
|
||||||
const tooltipText = esc(getFlagTooltipText(flag));
|
|
||||||
if (tooltipText) marker.bindTooltip(tooltipText, { permanent: true, direction: 'right', offset: [10, 0], className: 'flag-marker-tooltip' });
|
|
||||||
marker.on('dragend', () => {
|
marker.on('dragend', () => {
|
||||||
const p = marker.getLatLng(); flag.lat = p.lat; flag.lng = p.lng;
|
const p = marker.getLatLng(); flag.lat = p.lat; flag.lng = p.lng;
|
||||||
scheduleGeocode(flag); renderList();
|
scheduleGeocode(flag); renderList();
|
||||||
});
|
});
|
||||||
marker.on('contextmenu', () => { removeFlag(flag.id); });
|
marker.on('contextmenu', () => { removeFlag(flag.id); });
|
||||||
|
marker.on('click', () => { setActiveTooltip(flag.id); });
|
||||||
|
|
||||||
flag.placed = true; flag.lat = lat; flag.lng = lng; flag.marker = marker;
|
flag.placed = true; flag.lat = lat; flag.lng = lng; flag.marker = marker;
|
||||||
if (row.Address || row.address || row.주소) flag.address = String(row.Address || row.address || row.주소);
|
refreshMarkerTooltip(flag);
|
||||||
|
|
||||||
|
let addr = row.Address ?? row.address ?? row.주소;
|
||||||
|
if (addr) flag.address = String(addr);
|
||||||
else scheduleGeocode(flag);
|
else scheduleGeocode(flag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1090,4 +1176,4 @@ document.getElementById('boundary-toggle').addEventListener('click', () => {
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user