mirror of
https://github.com/shadoll/sLogos.git
synced 2025-12-20 03:26:59 +00:00
Enhance CountryMap component with zoom and recenter functionality, and add map controls for improved user interaction
This commit is contained in:
@@ -165,7 +165,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="right-column">
|
<div class="right-column">
|
||||||
{#if logo.tags && logo.tags.some((tagObj) => (tagObj.text || tagObj) === "Country") && logo.meta && logo.meta["ISO code"]}
|
{#if logo.tags && logo.tags.some((tagObj) => (tagObj.text || tagObj) === "Country") && logo.meta && logo.meta["ISO code"]}
|
||||||
<CountryMap countryCodes={[logo.meta["ISO code"]]} countryNames={[logo.meta["country"]]} />
|
<CountryMap
|
||||||
|
countryCodes={[logo.meta["ISO code"]]}
|
||||||
|
countryNames={[logo.meta["country"]]}
|
||||||
|
countryScale={true}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="logo-details fullscreen-details">
|
<div class="logo-details fullscreen-details">
|
||||||
@@ -510,5 +514,4 @@
|
|||||||
.logo-details span {
|
.logo-details span {
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -4,8 +4,40 @@
|
|||||||
export let countryCodes = [];
|
export let countryCodes = [];
|
||||||
export let countryNames = [];
|
export let countryNames = [];
|
||||||
export let mapPath = "/data/world.svg";
|
export let mapPath = "/data/world.svg";
|
||||||
|
export let countryScale = false;
|
||||||
|
export let scalePadding = 30;
|
||||||
let wrapperRef;
|
let wrapperRef;
|
||||||
|
|
||||||
|
// --- Zoom and recenter logic ---
|
||||||
|
let lastViewBox = null;
|
||||||
|
function getSvgEl() {
|
||||||
|
return wrapperRef ? wrapperRef.querySelector("svg") : null;
|
||||||
|
}
|
||||||
|
function zoom(factor) {
|
||||||
|
const svgEl = getSvgEl();
|
||||||
|
if (!svgEl) return;
|
||||||
|
const vb = svgEl.getAttribute("viewBox");
|
||||||
|
if (!vb) return;
|
||||||
|
let [x, y, w, h] = vb.split(" ").map(Number);
|
||||||
|
const cx = x + w / 2;
|
||||||
|
const cy = y + h / 2;
|
||||||
|
w /= factor;
|
||||||
|
h /= factor;
|
||||||
|
x = cx - w / 2;
|
||||||
|
y = cy - h / 2;
|
||||||
|
svgEl.setAttribute("viewBox", `${x} ${y} ${w} ${h}`);
|
||||||
|
lastViewBox = `${x} ${y} ${w} ${h}`;
|
||||||
|
}
|
||||||
|
function zoomIn() {
|
||||||
|
zoom(1.2);
|
||||||
|
}
|
||||||
|
function zoomOut() {
|
||||||
|
zoom(0.8);
|
||||||
|
}
|
||||||
|
function recenter() {
|
||||||
|
highlightCountries();
|
||||||
|
}
|
||||||
|
|
||||||
// Highlight countries after SVG loads, using MutationObserver for reliability
|
// Highlight countries after SVG loads, using MutationObserver for reliability
|
||||||
let observer;
|
let observer;
|
||||||
function highlightCountries() {
|
function highlightCountries() {
|
||||||
@@ -13,15 +45,17 @@
|
|||||||
const svgEl = wrapperRef.querySelector("svg");
|
const svgEl = wrapperRef.querySelector("svg");
|
||||||
if (!svgEl) return;
|
if (!svgEl) return;
|
||||||
// Highlight by country code (id)
|
// Highlight by country code (id)
|
||||||
|
let highlighted = [];
|
||||||
countryCodes.forEach((code) => {
|
countryCodes.forEach((code) => {
|
||||||
const countryPath = svgEl.querySelector(`#${code}`);
|
const countryPath = svgEl.querySelector(`#${code}`);
|
||||||
if (countryPath) {
|
if (countryPath) {
|
||||||
countryPath.setAttribute("fill", "#4f8cff");
|
countryPath.setAttribute("fill", "#4f8cff");
|
||||||
countryPath.setAttribute("stroke", "#222");
|
countryPath.setAttribute("stroke", "#222");
|
||||||
countryPath.style.filter = "drop-shadow(0 0 4px #4f8cff44)";
|
countryPath.style.filter = "drop-shadow(0 0 4px #4f8cff44)";
|
||||||
|
highlighted.push(countryPath);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// Highlight by country name (name attribute)
|
// Highlight by country name (name attribute or class)
|
||||||
const names = Array.isArray(countryNames)
|
const names = Array.isArray(countryNames)
|
||||||
? countryNames
|
? countryNames
|
||||||
: countryNames
|
: countryNames
|
||||||
@@ -37,8 +71,89 @@
|
|||||||
countryPath.setAttribute("fill", "#4f8cff");
|
countryPath.setAttribute("fill", "#4f8cff");
|
||||||
countryPath.setAttribute("stroke", "#222");
|
countryPath.setAttribute("stroke", "#222");
|
||||||
countryPath.style.filter = "drop-shadow(0 0 4px #4f8cff44)";
|
countryPath.style.filter = "drop-shadow(0 0 4px #4f8cff44)";
|
||||||
|
highlighted.push(countryPath);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
// Smart scale/center if enabled and at least one country is highlighted
|
||||||
|
if (countryScale && highlighted.length > 0) {
|
||||||
|
// Compute bounding box of all highlighted paths
|
||||||
|
let minX = Infinity,
|
||||||
|
minY = Infinity,
|
||||||
|
maxX = -Infinity,
|
||||||
|
maxY = -Infinity;
|
||||||
|
highlighted.forEach((path) => {
|
||||||
|
let bbox;
|
||||||
|
try {
|
||||||
|
bbox = path.getBBox();
|
||||||
|
} catch (e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (bbox.x < minX) minX = bbox.x;
|
||||||
|
if (bbox.y < minY) minY = bbox.y;
|
||||||
|
if (bbox.x + bbox.width > maxX) maxX = bbox.x + bbox.width;
|
||||||
|
if (bbox.y + bbox.height > maxY) maxY = bbox.y + bbox.height;
|
||||||
|
});
|
||||||
|
// Center and scale the SVG viewBox to the highlighted country
|
||||||
|
if (
|
||||||
|
isFinite(minX) &&
|
||||||
|
isFinite(minY) &&
|
||||||
|
isFinite(maxX) &&
|
||||||
|
isFinite(maxY)
|
||||||
|
) {
|
||||||
|
minX -= scalePadding;
|
||||||
|
minY -= scalePadding;
|
||||||
|
maxX += scalePadding;
|
||||||
|
maxY += scalePadding;
|
||||||
|
const width = maxX - minX;
|
||||||
|
const height = maxY - minY;
|
||||||
|
svgEl.setAttribute(
|
||||||
|
"viewBox",
|
||||||
|
`${minX} ${minY} ${width} ${height}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// --- Map drag/move by mouse ---
|
||||||
|
let isDragging = false;
|
||||||
|
let dragStart = { x: 0, y: 0 };
|
||||||
|
let viewBoxStart = null;
|
||||||
|
|
||||||
|
function onMouseDown(e) {
|
||||||
|
const svgEl = wrapperRef.querySelector("svg");
|
||||||
|
if (!svgEl) return;
|
||||||
|
isDragging = true;
|
||||||
|
dragStart = { x: e.clientX, y: e.clientY };
|
||||||
|
const vb = svgEl.getAttribute("viewBox");
|
||||||
|
if (vb) {
|
||||||
|
const [x, y, w, h] = vb.split(" ").map(Number);
|
||||||
|
viewBoxStart = { x, y, w, h };
|
||||||
|
}
|
||||||
|
window.addEventListener("mousemove", onMouseMove);
|
||||||
|
window.addEventListener("mouseup", onMouseUp);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseMove(e) {
|
||||||
|
if (!isDragging || !viewBoxStart) return;
|
||||||
|
const svgEl = wrapperRef.querySelector("svg");
|
||||||
|
if (!svgEl) return;
|
||||||
|
const dx = e.clientX - dragStart.x;
|
||||||
|
const dy = e.clientY - dragStart.y;
|
||||||
|
// Scale drag to SVG units
|
||||||
|
const scaleX = viewBoxStart.w / wrapperRef.offsetWidth;
|
||||||
|
const scaleY = viewBoxStart.h / wrapperRef.offsetHeight;
|
||||||
|
const newX = viewBoxStart.x - dx * scaleX;
|
||||||
|
const newY = viewBoxStart.y - dy * scaleY;
|
||||||
|
svgEl.setAttribute(
|
||||||
|
"viewBox",
|
||||||
|
`${newX} ${newY} ${viewBoxStart.w} ${viewBoxStart.h}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMouseUp() {
|
||||||
|
isDragging = false;
|
||||||
|
viewBoxStart = null;
|
||||||
|
window.removeEventListener("mousemove", onMouseMove);
|
||||||
|
window.removeEventListener("mouseup", onMouseUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
function observeSvg() {
|
function observeSvg() {
|
||||||
@@ -65,8 +180,15 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="country-map-section">
|
<div class="country-map-section">
|
||||||
<div class="svg-wrapper" bind:this={wrapperRef}>
|
<div bind:this={wrapperRef} style="width:100%;height:100%;position:relative;">
|
||||||
<InlineSvg path={mapPath} alt="World map" color={undefined} />
|
<InlineSvg path={mapPath} alt="World map" color={undefined} />
|
||||||
|
{#if countryScale}
|
||||||
|
<div class="map-controls-on-map">
|
||||||
|
<button class="zoom-btn-on-map" on:click={zoomIn}>+</button>
|
||||||
|
<button class="zoom-btn-on-map" on:click={recenter}>⦿</button>
|
||||||
|
<button class="zoom-btn-on-map" on:click={zoomOut}>-</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -77,12 +199,35 @@
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
box-shadow: 0 2px 8px 2px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 2px 8px 2px rgba(0, 0, 0, 0.08);
|
||||||
}
|
}
|
||||||
.svg-wrapper {
|
|
||||||
width: 100%;
|
.map-controls-on-map {
|
||||||
height: 180px;
|
position: absolute;
|
||||||
margin: 0 auto;
|
right: 0.5em;
|
||||||
background: var(--color-bg);
|
bottom: 0.5em;
|
||||||
border-radius: 8px;
|
display: flex;
|
||||||
box-shadow: 0 1px 4px 0 rgba(0, 0, 0, 0.04);
|
flex-direction: column;
|
||||||
|
gap: 0.5em;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
.zoom-btn-on-map {
|
||||||
|
background: var(--color-accent, #222);
|
||||||
|
color: var(--color-bg, #4f8cff);
|
||||||
|
border: 1px solid var(--color-border, #222);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
font-size: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
box-shadow: 0 2px 8px 2px rgba(0,0,0,0.08);
|
||||||
|
transition: background 0.2s, color 0.2s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-weight: bold;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.zoom-btn-on-map:hover {
|
||||||
|
background: var(--color-bg, #4f8cff);
|
||||||
|
color: var(--color-text, #fff);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user