Refactor CountryMap and CardFull components for ISO code handling

- Updated CardFull to ensure ISO codes are trimmed and uppercased before passing to CountryMap.
- Removed unused countryNames prop from CountryMap and adjusted highlighting logic to use data-iso attributes for country identification.
- Enhanced GeographyQuiz to prioritize ISO code checks and normalize values for consistency.
- Cleaned up code by removing legacy attribute handling and ensuring robust attribute management during country highlighting.
This commit is contained in:
sHa
2025-08-16 00:01:01 +03:00
parent 3ee3ffeb17
commit fba47c142c
4 changed files with 683 additions and 785 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 221 KiB

After

Width:  |  Height:  |  Size: 223 KiB

View File

@@ -166,8 +166,7 @@
<div class="right-column">
{#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"]]}
countryCodes={[String(logo.meta["ISO code"]).trim().toUpperCase()]}
countryScale={true}
/>
{/if}

View File

@@ -2,7 +2,6 @@
import { afterUpdate, onMount } from "svelte";
import InlineSvg from "./InlineSvg.svelte";
export let countryCodes = [];
export let countryNames = [];
export let forceCenterKey = 0;
export let mapPath = "/data/world.svg";
export let countryScale = false;
@@ -58,174 +57,67 @@
let svgSeen = false; // whether we've already run the initial highlight for the inlined SVG
function highlightCountries() {
// backward-compatible signature: allow passing a force flag
const args = Array.from(arguments);
const forceCenter = args && args[0] === true;
if (!wrapperRef) return;
const svgEl = wrapperRef.querySelector("svg");
if (!svgEl) return;
// Clear previous highlights robustly. Try to restore any saved inline
// attributes; if they are missing (e.g. due to re-mounts) also remove
// the highlight styling if present.
// Simple cleanup: remove any inline fill/stroke/filter and any
// data-geo* helper attributes on all path and g elements. We don't
// keep or restore previous values; just clear styling before
// applying new highlight styles.
try {
const candidateSelectors = 'path, g, polygon, rect, circle, ellipse, use';
const allEls = Array.from(svgEl.querySelectorAll(candidateSelectors));
allEls.forEach((el) => {
const els = Array.from(svgEl.querySelectorAll('path, g'));
els.forEach((el) => {
try {
const prevFill = el.getAttribute('data-geo-prev-fill');
const prevStroke = el.getAttribute('data-geo-prev-stroke');
const prevFilter = el.getAttribute('data-geo-prev-filter');
// Clear basic inline styling
el.removeAttribute('fill');
el.removeAttribute('stroke');
if (el.style) el.style.filter = '';
// If we saved explicit previous values, restore them (empty string means remove attr)
if (prevFill !== null) {
if (prevFill === '') el.removeAttribute('fill');
else el.setAttribute('fill', prevFill);
} else {
// no saved value: if the element currently has the highlight color, remove it
// Remove any attributes that start with 'data-geo'
try {
const curFill = el.getAttribute && el.getAttribute('fill');
if (curFill && curFill.toLowerCase() === '#4f8cff') el.removeAttribute('fill');
} catch (e) {}
const attrs = Array.from(el.attributes || []);
attrs.forEach((a) => {
try {
if (a && typeof a.name === 'string' && a.name.indexOf('data-geo') === 0) {
el.removeAttribute(a.name);
}
if (prevStroke !== null) {
if (prevStroke === '') el.removeAttribute('stroke');
else el.setAttribute('stroke', prevStroke);
} else {
try {
const curStroke = el.getAttribute && el.getAttribute('stroke');
if (curStroke && curStroke.toLowerCase() === '#222') el.removeAttribute('stroke');
} catch (e) {}
}
if (prevFilter !== null) {
if (prevFilter === '') el.style.filter = '';
else el.style.filter = prevFilter;
} else {
try {
const cf = el.style && el.style.filter;
if (cf && cf.indexOf('4f8cff') !== -1) el.style.filter = '';
});
} catch (e) {}
}
// Clean up helper attributes if present
try {
el.removeAttribute('data-geo-prev-fill');
el.removeAttribute('data-geo-prev-stroke');
el.removeAttribute('data-geo-prev-filter');
el.removeAttribute('data-geo-highlight');
} catch (e) {}
} catch (err) {}
});
} catch (err) {}
// Highlight by country code (id)
// Highlight by country ISO using data-iso attribute only (case-insensitive via uppercasing)
let highlighted = [];
countryCodes.forEach((code) => {
const countryPath = svgEl.querySelector(`#${code}`);
try {
if (!code) return;
const iso = String(code).trim().toUpperCase();
// Prefer elements that explicitly declare data-iso
let countryPath = null;
try {
countryPath = svgEl.querySelector(`[data-iso="${iso}"]`);
} catch (err) {
// Fallback: try lowercase attribute selector
try {
countryPath = svgEl.querySelector(`[data-iso='${iso.toLowerCase()}']`);
} catch (err2) {
countryPath = null;
}
}
if (countryPath) {
// Save previous inline attributes so we can restore later
const prevFill = countryPath.getAttribute('fill');
const prevStroke = countryPath.getAttribute('stroke');
const prevFilter = countryPath.style.filter || '';
if (prevFill !== null) countryPath.setAttribute('data-geo-prev-fill', prevFill);
else countryPath.setAttribute('data-geo-prev-fill', '');
if (prevStroke !== null) countryPath.setAttribute('data-geo-prev-stroke', prevStroke);
else countryPath.setAttribute('data-geo-prev-stroke', '');
countryPath.setAttribute('data-geo-prev-filter', prevFilter);
countryPath.setAttribute("fill", "#4f8cff");
countryPath.setAttribute("stroke", "#222");
countryPath.style.filter = "drop-shadow(0 0 4px #4f8cff44)";
countryPath.setAttribute('data-geo-highlight', '1');
highlighted.push(countryPath);
}
});
// Highlight by country name (try many fallbacks: attributes, class names, <title> children)
const names = Array.isArray(countryNames) ? countryNames : countryNames ? [countryNames] : [];
if (names.length > 0) {
// candidate elements to check for name matches
const candidateSelectors = 'path, g, polygon, rect, circle, ellipse, use';
const candidates = Array.from(svgEl.querySelectorAll(candidateSelectors));
names.forEach((nameRaw) => {
if (!nameRaw) return;
const name = String(nameRaw).trim();
if (!name) return;
const nameLower = name.toLowerCase();
const matched = new Set();
// First pass: strict attribute/title/id matches (case-insensitive)
try {
const attrsToCheck = ['name', 'data-name', 'id', 'title', 'inkscape:label'];
candidates.forEach((el) => {
for (const a of attrsToCheck) {
try {
const val = el.getAttribute && el.getAttribute(a);
if (val && String(val).trim().toLowerCase() === nameLower) {
matched.add(el);
return;
}
} catch (err) {}
}
// child <title> element (redundant with 'title' attr check but kept safe)
try {
const titleEl = el.querySelector && el.querySelector('title');
if (titleEl && titleEl.textContent && titleEl.textContent.trim().toLowerCase() === nameLower) {
matched.add(el);
return;
}
} catch (err) {}
});
} catch (err) {}
// If we didn't find any strict matches, fall back to class/slug heuristics
if (matched.size === 0) {
try {
// Try direct selectors only when safe
try {
const byNameAttr = svgEl.querySelectorAll(`[name='${name}']`);
byNameAttr.forEach((el) => matched.add(el));
} catch (err) {}
try {
const byClassDot = svgEl.querySelectorAll(`.${name.replace(/\s+/g, '.')}`);
byClassDot.forEach((el) => matched.add(el));
} catch (err) {}
// class list heuristics: slug (lower, dashes)
candidates.forEach((el) => {
try {
const cls = (el.className && (typeof el.className === 'string' ? el.className : el.className.baseVal)) || '';
if (cls) {
const parts = cls.split(/\s+/).map((c) => c.trim()).filter(Boolean);
const slug = nameLower.replace(/[^a-z0-9]+/g, '-').replace(/^\-+|\-+$/g, '');
if (parts.includes(name) || parts.includes(nameLower) || parts.includes(slug)) matched.add(el);
}
} catch (err) {}
});
} catch (err) {}
}
// Apply highlighting to matched elements
matched.forEach((countryPath) => {
const prevFill = countryPath.getAttribute('fill');
const prevStroke = countryPath.getAttribute('stroke');
const prevFilter = countryPath.style.filter || '';
if (prevFill !== null) countryPath.setAttribute('data-geo-prev-fill', prevFill);
else countryPath.setAttribute('data-geo-prev-fill', '');
if (prevStroke !== null) countryPath.setAttribute('data-geo-prev-stroke', prevStroke);
else countryPath.setAttribute('data-geo-prev-stroke', '');
countryPath.setAttribute('data-geo-prev-filter', prevFilter);
countryPath.setAttribute('fill', '#4f8cff');
countryPath.setAttribute('stroke', '#222');
countryPath.style.filter = 'drop-shadow(0 0 4px #4f8cff44)';
countryPath.setAttribute('data-geo-highlight', '1');
highlighted.push(countryPath);
});
});
}
} catch (err) {}
});
// Smart scale/center if enabled and at least one country is highlighted
if (countryScale && highlighted.length > 0) {
// Compute bounding box of all highlighted paths

View File

@@ -50,10 +50,14 @@
}
function isFlagOnMap(flag) {
if (!mapParsed) return true; // allow through if we couldn't parse map
// Prefer ISO comparison against data-iso values. If map couldn't be parsed, allow.
if (!mapParsed) return true;
try {
const iso = flag?.meta?.["ISO code"] || flag?.meta?.["ISO Code"] || flag?.meta?.["ISO"] || flag?.ISO;
if (iso && availableIso.has(String(iso).trim())) return true;
const isoRaw = flag?.meta?.["ISO code"] || flag?.meta?.["ISO Code"] || flag?.meta?.["ISO"] || flag?.ISO;
const iso = isoRaw ? String(isoRaw).trim().toUpperCase() : '';
if (iso && availableIso.has(iso)) return true;
// Fallbacks to name/slug checks remain, but only used if no ISO matches.
const name = getCountryName(flag);
const n = normalizeNameForLookup(name);
if (n && availableNames.has(n)) return true;
@@ -206,8 +210,13 @@
const all = Array.from(doc.querySelectorAll('*'));
all.forEach((el) => {
try {
// Prefer data-iso attribute for ISO lookup. Normalize to uppercase.
const dataIso = el.getAttribute && (el.getAttribute('data-iso') || el.getAttribute('data-ISO'));
if (dataIso) availableIso.add(String(dataIso).trim().toUpperCase());
// Backwards-compatible: also include id if present (older maps)
const id = el.getAttribute && el.getAttribute('id');
if (id) availableIso.add(String(id).trim());
if (id) availableIso.add(String(id).trim().toUpperCase());
const nameAttr = el.getAttribute && (el.getAttribute('name') || el.getAttribute('data-name') || el.getAttribute('inkscape:label'));
if (nameAttr) availableNames.add(String(nameAttr).trim().toLowerCase());
// title child
@@ -570,18 +579,12 @@
currentQuestion.correct.meta["ISO code"]) ||
currentQuestion.correct?.ISO ||
"",
].filter(Boolean)
]
.filter(Boolean)
.map((c) => (c ? String(c).trim().toUpperCase() : c))
: [];
// Reactive country names array for CountryMap (fallback when no ISO present)
$: countryNames = currentQuestion
? [
currentQuestion.correct?.name ||
(currentQuestion.correct?.meta && currentQuestion.correct.meta.country) ||
currentQuestion.correct?.meta?.country ||
"",
].filter(Boolean)
: [];
</script>
<svelte:head>
@@ -661,7 +664,6 @@
<div class="map-display">
<CountryMap
{countryCodes}
{countryNames}
countryScale={true}
scalePadding={90}
forceCenterKey={questionKey}