참고: 설정을 저장한 후에 바뀐 점을 확인하기 위해서는 브라우저의 캐시를 새로 고쳐야 합니다.
- 파이어폭스 / 사파리: Shift 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5 또는 Ctrl-R을 입력 (Mac에서는 ⌘-R)
- 구글 크롬: Ctrl-Shift-R키를 입력 (Mac에서는 ⌘-Shift-R)
- 인터넷 익스플로러 / 엣지: Ctrl 키를 누르면서 새로 고침을 클릭하거나, Ctrl-F5를 입력.
- 오페라: Ctrl-F5를 입력.
/* =========================================
COASTLINE: BLACK ICE - Nations Panel
시대 문서: 시대 전환 / 역사 연도 / 대륙 탭 제어
========================================= */
(function () {
'use strict';
var ENTRY_CONSUME_FIX_BUILD = '20260708-boot-skip-preview-and-manager-save-001';
var NATIONS_PANEL_BUILD = '20260710-interaction-budget-bg-parity-001';
var DEFAULT_ERAS = [
'1950', '1960', '1970', '1980', '1990', '2000', '2010', '2020',
'2030', '2040', '2050', '2060', '2070', '2080', '2090', '2100'
];
var nationPanelLastDirectInteractionAt = 0;
function markNationPanelDirectInteraction() {
nationPanelLastDirectInteractionAt = Date.now();
}
function hasRecentNationPanelDirectInteraction(windowMs) {
return Date.now() - Number(nationPanelLastDirectInteractionAt || 0) < Math.max(120, Number(windowMs || 900));
}
function isNationsGlobeBusyForPanelWork() {
var globe = document.querySelector('.clbi-nations-globe-window[data-nations-globe], [data-nations-globe="real-world"]');
var instance;
if (!globe) return false;
if (globe.classList && globe.classList.contains('is-dragging')) return true;
if (globe.getAttribute && globe.getAttribute('data-clbi-globe-bg-busy') === '1') return true;
instance = globe.CLBI_NationsGlobeInstance || null;
return !!(instance && typeof instance.isHalftoneBackgroundBusy === 'function' && instance.isHalftoneBackgroundBusy());
}
function toArray(list) {
return Array.prototype.slice.call(list || []);
}
function unique(values) {
var seen = {};
var out = [];
values.forEach(function (value) {
value = String(value || '').trim();
if (!value || seen[value]) return;
seen[value] = true;
out.push(value);
});
return out;
}
function parseEraList(stack) {
var raw = stack ? stack.getAttribute('data-nations-era-years') : '';
var values = raw ? raw.split(/[\s,|]+/) : [];
values = unique(values.filter(function (value) {
return /^\d{4}$/.test(value);
}));
return values.length ? values : DEFAULT_ERAS.slice();
}
function getEraIndex(eras, era) {
var index = eras.indexOf(String(era || ''));
return index >= 0 ? index : 0;
}
function getWrappedEra(eras, index) {
if (!eras.length) return '';
index = ((index % eras.length) + eras.length) % eras.length;
return eras[index];
}
function setHidden(element, hidden) {
var active = !hidden;
if (!element) return;
if (element.classList.contains('is-active') === active &&
element.hasAttribute('hidden') === !!hidden &&
element.getAttribute('aria-hidden') === (hidden ? 'true' : 'false')) return;
if (hidden) {
element.setAttribute('hidden', 'hidden');
element.setAttribute('aria-hidden', 'true');
element.classList.remove('is-active');
} else {
element.removeAttribute('hidden');
element.setAttribute('aria-hidden', 'false');
element.classList.add('is-active');
}
}
function setActiveButton(buttons, activeButton) {
buttons.forEach(function (button) {
var active = button === activeButton;
if (button.classList.contains('is-active') !== active) {
button.classList.toggle('is-active', active);
}
if (button.getAttribute('aria-selected') !== (active ? 'true' : 'false')) {
button.setAttribute('aria-selected', active ? 'true' : 'false');
}
if (button.getAttribute('tabindex') !== (active ? '0' : '-1')) {
button.setAttribute('tabindex', active ? '0' : '-1');
}
});
}
function activatePanelsByAttribute(panels, attrName, value) {
panels.forEach(function (panel) {
setHidden(panel, panel.getAttribute(attrName) !== value);
});
}
function activatePersistentPanelsByAttribute(panels, attrName, value) {
panels.forEach(function (panel) {
var active = panel.getAttribute(attrName) === value;
var aria = active ? 'false' : 'true';
if (panel.hasAttribute('hidden')) panel.removeAttribute('hidden');
if (panel.classList.contains('is-active') !== active) {
panel.classList.toggle('is-active', active);
}
if (panel.getAttribute('aria-hidden') !== aria) {
panel.setAttribute('aria-hidden', aria);
}
if (panel.getAttribute('data-clbi-panel-warm') !== '1') {
panel.setAttribute('data-clbi-panel-warm', '1');
}
});
}
function getIdleScheduler() {
return window.requestIdleCallback || function (callback) {
return window.setTimeout(function () {
callback({
didTimeout: false,
timeRemaining: function () { return 12; }
});
}, 96);
};
}
function getIdleCanceller() {
return window.cancelIdleCallback || window.clearTimeout;
}
function scheduleIdle(callback, options) {
return getIdleScheduler()(callback, options || { timeout: 650 });
}
function cancelIdle(handle) {
if (handle == null) return;
getIdleCanceller()(handle);
}
function replaceYearTemplate(template, era) {
return String(template || '').replace(/\{year\}/g, String(era || ''));
}
function readTemplate(root, attrName, fallback) {
if (!root) return fallback;
return root.getAttribute(attrName) || fallback;
}
function setAttributeFromTemplate(root, attrName, templateName, fallback, era) {
var template = readTemplate(root, templateName, fallback);
if (!template) {
root.removeAttribute(attrName);
return;
}
root.setAttribute(attrName, replaceYearTemplate(template, era));
}
function resetGlobeDom(globe) {
var instance;
if (!globe) return;
instance = globe.CLBI_NationsGlobeInstance || null;
if (instance && typeof instance.dispose === 'function') {
try { instance.dispose(); } catch (err) {}
}
globe.CLBI_NationsGlobeInstance = null;
globe.removeAttribute('data-nations-globe-ready');
globe.classList.remove('is-ready', 'has-error', 'is-dragging');
globe.innerHTML = '';
}
function reloadGlobe(globe) {
if (!globe) return;
if (window.CLBI_NationsGlobe && typeof window.CLBI_NationsGlobe.reload === 'function') {
window.CLBI_NationsGlobe.reload(globe);
return;
}
resetGlobeDom(globe);
if (window.CLBI_NationsGlobe && typeof window.CLBI_NationsGlobe.init === 'function') {
window.CLBI_NationsGlobe.init(globe.parentNode || document);
return;
}
window.setTimeout(function () {
if (window.CLBI_NationsGlobe && typeof window.CLBI_NationsGlobe.init === 'function') {
window.CLBI_NationsGlobe.init(globe.parentNode || document);
}
}, 0);
}
function applyGlobeEra(globe, era, reload) {
if (!globe || !era) return;
globe.setAttribute('data-era-active', era);
globe.setAttribute('data-era', replaceYearTemplate(
readTemplate(globe, 'data-era-readout-template', '{year}'),
era
));
setAttributeFromTemplate(
globe,
'data-geojson-url',
'data-era-border-url-template',
'/index.php/Special:Redirect/file/{year}_Border_Lines.geojson',
era
);
setAttributeFromTemplate(
globe,
'data-polygon-overlay-url',
'data-era-polygon-overlay-url-template',
'/index.php/Special:Redirect/file/{year}_Polygon_Overlay.webp',
era
);
setAttributeFromTemplate(
globe,
'data-polygon-hit-url',
'data-era-polygon-hit-url-template',
'/index.php/Special:Redirect/file/{year}_Polygon_Hit.geojson',
era
);
setAttributeFromTemplate(
globe,
'data-nation-list-url',
'data-era-nation-list-url-template',
'/index.php?title=MediaWiki:{year}_Nation_List.json&action=raw&ctype=application/json',
era
);
setAttributeFromTemplate(
globe,
'data-nation-link-map-url',
'data-era-nation-link-map-url-template',
'/index.php?title=MediaWiki:{year}_Nation_Link_Map.json&action=raw&ctype=application/json',
era
);
setAttributeFromTemplate(
globe,
'data-lakes-overlay-url',
'data-era-lakes-overlay-url-template',
'/index.php/Special:Redirect/file/{year}_Lakes_Overlay.webp',
era
);
if (reload) {
reloadGlobe(globe);
}
}
var NATION_CONTINENT_FALLBACK = [
{ key: 'america', label: '아메리카', regions: ['북아메리카', '남아메리카'] },
{ key: 'europe', label: '유럽', regions: ['북유럽', '서유럽', '중앙유럽', '동유럽', '동남유럽', '남유럽', '남서유럽'] },
{ key: 'africa', label: '아프리카', regions: ['북아프리카', '서아프리카', '중앙아프리카', '동아프리카', '남아프리카'] },
{ key: 'asia', label: '아시아', regions: ['서아시아', '중앙아시아', '북아시아', '남아시아', '동아시아', '동남아시아'] },
{ key: 'oceania', label: '오세아니아', regions: ['오세아니아'] },
{ key: 'antarctica', label: '남극', regions: ['남극'] }
];
var nationListCache = {};
var nationLinkMapCache = {};
var historyEventsCache = {};
var nationRuntimeCacheBust = 'revision-ledger';
var nationResolvedFileUrlCache = {};
var PanelPerf = (function () {
var startedAt = Date.now();
var sequence = 1;
var active = {};
var entries = [];
function cloneMeta(meta) {
var copy = {};
if (!meta || typeof meta !== 'object') return copy;
Object.keys(meta).forEach(function (key) {
var value = meta[key];
if (value == null || typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
copy[key] = value;
} else if (Array.isArray(value)) {
copy[key] = value.length <= 12 ? value.slice() : value.slice(0, 12).concat(['...+' + (value.length - 12)]);
} else {
try { copy[key] = JSON.parse(JSON.stringify(value)); }
catch (err) { copy[key] = String(value); }
}
});
return copy;
}
function mergeMeta(a, b) {
var out = cloneMeta(a);
b = cloneMeta(b);
Object.keys(b).forEach(function (key) { out[key] = b[key]; });
return out;
}
function start(name, meta) {
var id = sequence++;
active[id] = {
id: id,
name: String(name || 'step'),
startedAt: Date.now(),
offset: Date.now() - startedAt,
meta: cloneMeta(meta)
};
return id;
}
function end(id, meta) {
var item = active[id];
if (!item) return null;
delete active[id];
item.ms = Date.now() - item.startedAt;
item.meta = mergeMeta(item.meta, meta || { ok: true });
entries.push({
offset: item.offset,
ms: item.ms,
name: item.name,
meta: item.meta
});
return item;
}
function mark(name, meta) {
entries.push({
offset: Date.now() - startedAt,
ms: 0,
name: String(name || 'mark'),
meta: cloneMeta(meta)
});
}
function measure(name, meta, callback) {
var id = start(name, meta);
var value;
try {
value = callback();
} catch (err) {
end(id, { ok: false, error: err && err.message ? err.message : String(err) });
throw err;
}
if (value && typeof value.then === 'function') {
return value.then(function (result) {
end(id, { ok: true });
return result;
}, function (err) {
end(id, { ok: false, error: err && err.message ? err.message : String(err) });
throw err;
});
}
end(id, { ok: true });
return value;
}
function summary() {
return {
build: NATIONS_PANEL_BUILD,
startedAt: startedAt,
totalMs: Date.now() - startedAt,
entries: entries.slice().sort(function (a, b) {
if (a.offset !== b.offset) return a.offset - b.offset;
return b.ms - a.ms;
}),
active: Object.keys(active).map(function (id) {
var item = active[id];
return {
offset: item.offset,
ms: Date.now() - item.startedAt,
name: item.name,
meta: cloneMeta(item.meta)
};
})
};
}
function table() {
var data = summary().entries.map(function (entry) {
return { name: entry.name, ms: entry.ms, offset: entry.offset, meta: entry.meta };
});
if (window.console && console.table) console.table(data);
return data;
}
return {
build: NATIONS_PANEL_BUILD,
start: start,
end: end,
mark: mark,
measure: measure,
summary: summary,
table: table
};
}());
window.NationsPanelPerf = PanelPerf;
function interactionPerfMeasure(name, meta, fn) {
var perf = window.InteractionPerf;
if (perf && typeof perf.measureSync === 'function') {
return perf.measureSync(name, meta || {}, fn);
}
return fn();
}
function interactionPerfMark(name, meta) {
var perf = window.InteractionPerf;
if (perf && typeof perf.mark === 'function') {
perf.mark(name, meta || {});
}
}
function countNationListItems(data) {
var continents = data && Array.isArray(data.continents) ? data.continents : [];
var regions = 0;
var items = 0;
continents.forEach(function (continent) {
(continent.regions || []).forEach(function (region) {
regions += 1;
items += Array.isArray(region.items) ? region.items.length : 0;
});
});
return { continents: continents.length, regions: regions, items: items };
}
function extractRawTitleFromUrl(url) {
var text = String(url || '').trim();
var match;
if (!text) return '';
match = text.match(/[?&]title=([^&#]+)/i);
if (match) {
try { return decodeURIComponent(match[1].replace(/\+/g, ' ')).replace(/_/g, ' ').trim(); }
catch (err) { return match[1].replace(/_/g, ' ').trim(); }
}
return '';
}
function addNationCacheBust(url, ref) {
url = String(url || '').trim();
ref = String(ref || extractRawTitleFromUrl(url) || '').trim();
if (!url) return '';
if (/[?&](?:_clbiBust|_|_entryRev)=/.test(url)) return url;
if (ref && window.RevisionManifest && typeof window.RevisionManifest.addRevisionParam === 'function') {
return window.RevisionManifest.addRevisionParam(url, ref);
}
return url;
}
function fetchNationJson(url, fallbackRef) {
var ref = String(extractRawTitleFromUrl(url) || fallbackRef || '').trim();
var requestUrl;
if (ref && window.EntryStore && typeof window.EntryStore.fetchJsonRef === 'function') {
return PanelPerf.measure('fetch nation json via EntryStore', { ref: ref }, function () {
return window.EntryStore.fetchJsonRef(ref, { noStore: false });
});
}
requestUrl = addNationCacheBust(url, ref);
return PanelPerf.measure('fetch nation json via fetch', { ref: ref, url: requestUrl }, function () {
return fetch(requestUrl, { credentials: 'same-origin', cache: 'force-cache' })
.then(function (res) {
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.text();
})
.then(function (text) {
var trimmed = String(text || '').trim();
if (!trimmed) throw new Error('empty JSON response: ' + requestUrl);
return JSON.parse(trimmed);
});
});
}
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
function encodeWikiTitle(title) {
return String(title || '')
.trim()
.replace(/ /g, '_')
.split('/')
.map(function (part) { return encodeURIComponent(part); })
.join('/');
}
function normalizeNationFile(value) {
var text = String(value || '').trim();
var titleMatch;
var redirectMatch;
var previous;
var i;
var j;
if (!text) return '';
text = text
.replace(/^\[\[:?\s*/i, '')
.replace(/\]\]$/i, '')
.trim();
for (i = 0; i < 8; i += 1) {
previous = text;
for (j = 0; j < 3; j += 1) {
try {
if (/%[0-9a-f]{2}/i.test(text)) {
text = decodeURIComponent(text);
}
} catch (err) {
break;
}
}
text = text.replace(/\\/g, '/').trim();
titleMatch = text.match(/[?&]title=([^&#]+)/i);
if (titleMatch) {
text = titleMatch[1];
continue;
}
text = text
.replace(/[?#].*$/g, '')
.replace(/^https?:\/\/[^/]+/i, '')
.replace(/^\/+/, '')
.replace(/^index\.php\/?/i, '')
.replace(/^wiki\/?/i, '')
.trim();
for (j = 0; j < 6; j += 1) {
var stripped = text
.replace(/^\s*파일\s*:/i, '')
.replace(/^\s*File\s*:/i, '')
.replace(/^\s*이미지\s*:/i, '')
.replace(/^\s*Image\s*:/i, '')
.replace(/^:+/, '')
.trim();
if (stripped === text) break;
text = stripped;
}
redirectMatch = text.match(/^(?:(?:Special|특수)\s*[:/]\s*)+(?:Redirect|넘겨주기)\s*\/\s*file\s*\/(.+)$/i) ||
text.match(/^(?:Redirect|넘겨주기)\s*\/\s*file\s*\/(.+)$/i) ||
text.match(/(?:^|\/)(?:(?:Special|특수)\s*[:/]\s*)+(?:Redirect|넘겨주기)\s*\/\s*file\s*\/(.+)$/i);
if (redirectMatch) {
text = redirectMatch[1].trim();
continue;
}
if (text === previous) break;
}
text = text.replace(/^\/+/, '').trim();
return text;
}
function normalizeNationFileKey(file) {
return normalizeNationFile(file).replace(/_/g, ' ').trim().toLowerCase();
}
function getWikiApiUrl(params) {
var query = [];
Object.keys(params || {}).forEach(function (key) {
if (params[key] == null) return;
query.push(encodeURIComponent(key) + '=' + encodeURIComponent(String(params[key])));
});
return (window.mw && mw.util && mw.util.wikiScript ? mw.util.wikiScript('api') : '/api.php') + '?' + query.join('&');
}
function getWikiApiEndpoint() {
return (window.mw && mw.util && mw.util.wikiScript ? mw.util.wikiScript('api') : '/api.php');
}
function fetchWikiApi(params) {
var body;
try {
body = new URLSearchParams();
Object.keys(params || {}).forEach(function (key) {
if (params[key] == null) return;
body.append(key, String(params[key]));
});
return fetch(getWikiApiEndpoint(), {
method: 'POST',
credentials: 'same-origin',
cache: 'no-store',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
},
body: body.toString()
}).then(function (res) {
if (!res.ok) throw new Error('wiki api failed: ' + res.status);
return res.json();
});
} catch (err) {
return fetch(getWikiApiUrl(params), { credentials: 'same-origin', cache: 'no-store' }).then(function (res) {
if (!res.ok) throw new Error('wiki api failed: ' + res.status);
return res.json();
});
}
}
function buildNationFileUrl(file) {
var clean = normalizeNationFile(file);
var key = normalizeNationFileKey(clean);
var resolved;
if (!clean || !key) return '';
/*
Render nation-list flags through MediaWiki's own file thumbnail pipeline.
This mirrors [[파일:Name.svg|16px]] far more closely than using the raw
SVG URL or Special:Redirect/file path directly. If MediaWiki cannot
resolve the file title, return an empty URL and hide the flag slot.
*/
resolved = nationResolvedFileUrlCache[key];
/* Entry full-load prewarms the exact thumbnail URL. Adding a Date-based cache
buster here would create a new URL and make the preloaded flag useless.
When EntryCache has a same-revision blob URL, render that blob URL so a
second tab consumes the persistent cache instead of relying on a fresh <img>
request path. */
if (resolved && window.EntryStore && typeof window.EntryStore.getImageDisplayUrl === 'function') {
return window.EntryStore.getImageDisplayUrl(resolved) || resolved;
}
return resolved || '';
}
function collectNationFlagFileNames(data, options) {
var files = [];
var continentKeys = options && options.continentKeys;
function addFlag(flag) {
var file = normalizeNationFile(flag && (flag.file || flag.flag_file || flag.flag || flag.flag_title || flag));
if (file) files.push(file);
}
function scanItems(items) {
(items || []).forEach(function (item) {
if (!item || typeof item !== 'object') return;
if (Array.isArray(item.flags)) item.flags.forEach(addFlag);
if (Array.isArray(item.sybs)) item.sybs.forEach(addFlag);
if (Array.isArray(item.symbols)) item.symbols.forEach(addFlag);
addFlag(item.flag_file || item.flag || item.flag_title || '');
addFlag(item.syb_file || item.syb || item.symbol_file || item.symbol || '');
});
}
(data && data.continents || []).forEach(function (continent) {
var key = String((continent && continent.key) || '').trim();
if (continentKeys && key && !continentKeys[key]) return;
if (continentKeys && !key) return;
(continent.regions || []).forEach(function (region) {
scanItems(region.items || []);
});
});
return unique(files);
}
function getInitialContinentKey(panel, data) {
var activeTab = panel && panel.querySelector ? panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent], .clbi-nations-tabpanel-tab[aria-selected="true"][data-continent]') : null;
var activeKey = activeTab ? String(activeTab.getAttribute('data-continent') || '').trim() : '';
var continents = data && Array.isArray(data.continents) ? data.continents : [];
if (!activeKey && continents.length) activeKey = String(continents[0].key || '').trim();
return activeKey;
}
function collectInitialNationFlagFileNames(data, panel) {
var activeKey = getInitialContinentKey(panel, data);
var map = {};
if (!activeKey) return collectNationFlagFileNames(data);
map[activeKey] = true;
return collectNationFlagFileNames(data, { continentKeys: map });
}
function subtractNationFlagFiles(allFiles, initialFiles) {
var initial = {};
(initialFiles || []).forEach(function (file) {
var key = normalizeNationFileKey(file);
if (key) initial[key] = true;
});
return unique((allFiles || []).filter(function (file) {
var key = normalizeNationFileKey(file);
return key && !initial[key];
}));
}
function resolveNationFlagFileUrls(files) {
var cleanFiles = unique((files || []).map(normalizeNationFile).filter(Boolean));
cleanFiles.forEach(function (file) {
var key = normalizeNationFileKey(file);
var readyUrl = window.EntryStore && typeof window.EntryStore.getFlagUrl === 'function' ? window.EntryStore.getFlagUrl(file) : '';
if (key && readyUrl && nationResolvedFileUrlCache[key] === undefined) nationResolvedFileUrlCache[key] = readyUrl;
});
var pending = cleanFiles.filter(function (file) {
var key = normalizeNationFileKey(file);
return key && nationResolvedFileUrlCache[key] === undefined;
});
var chunks = [];
while (pending.length) chunks.push(pending.splice(0, 25));
if (!chunks.length) {
PanelPerf.mark('resolve nation flag URLs', { count: cleanFiles.length, pending: 0, cached: cleanFiles.length });
return Promise.resolve(nationResolvedFileUrlCache);
}
return PanelPerf.measure('resolve nation flag URLs', { count: cleanFiles.length, pending: pending.length, chunks: chunks.length }, function () {
return Promise.all(chunks.map(function (chunk) {
var titleToKey = {};
var titles = [];
chunk.forEach(function (file) {
var key = normalizeNationFileKey(file);
var canonical = 'File:' + file;
var local = '파일:' + file;
if (!key) return;
titleToKey[normalizeNationFileKey(canonical)] = key;
titleToKey[normalizeNationFileKey(local)] = key;
titles.push(canonical);
titles.push(local);
});
return fetchWikiApi({
action: 'query',
format: 'json',
formatversion: '2',
redirects: '1',
prop: 'imageinfo',
iiprop: 'url|sha1|timestamp|size',
iiurlwidth: '16',
titles: titles.join('|')
}).then(function (json) {
var pages = (json && json.query && json.query.pages) || [];
var seen = {};
pages.forEach(function (page) {
var file = normalizeNationFile(page && page.title);
var pageKey = normalizeNationFileKey(file);
var originalKey = titleToKey[pageKey] || pageKey;
var imageinfo = page && page.imageinfo && page.imageinfo[0];
if (!originalKey) return;
seen[originalKey] = true;
if (imageinfo && (imageinfo.thumburl || imageinfo.url)) {
var resolvedUrl = imageinfo.thumburl || imageinfo.url;
var rev = imageinfo.sha1 || imageinfo.timestamp || imageinfo.size || '';
var sep;
if (rev && !/[?&]_entryFileRev=/.test(resolvedUrl)) {
sep = resolvedUrl.indexOf('?') === -1 ? '?' : '&';
resolvedUrl += sep + '_entryFileRev=' + encodeURIComponent(String(rev));
}
nationResolvedFileUrlCache[originalKey] = resolvedUrl;
if (pageKey) nationResolvedFileUrlCache[pageKey] = resolvedUrl;
if (window.EntryStore && typeof window.EntryStore.setFlagUrl === 'function') window.EntryStore.setFlagUrl(file, resolvedUrl);
} else if (nationResolvedFileUrlCache[originalKey] === undefined) {
nationResolvedFileUrlCache[originalKey] = '';
}
});
chunk.forEach(function (file) {
var key = normalizeNationFileKey(file);
if (key && !seen[key] && nationResolvedFileUrlCache[key] === undefined) {
nationResolvedFileUrlCache[key] = '';
}
});
}).catch(function () {
chunk.forEach(function (file) {
var key = normalizeNationFileKey(file);
if (key && nationResolvedFileUrlCache[key] === undefined) nationResolvedFileUrlCache[key] = '';
});
});
})).then(function () {
return nationResolvedFileUrlCache;
});
});
}
function isLegacyNationIndexFileUrl(src) {
src = String(src || '');
if (!src) return false;
if (!/\.svg(?:[?#]|$)/i.test(src)) return false;
if (/\/images\//i.test(src)) return false;
return /(?:Special|%ED%8A%B9%EC%88%98|특수).*?(?:Redirect|%EB%84%98%EA%B2%A8%EC%A3%BC%EA%B8%B0|넘겨주기).*?file/i.test(src) || /\/index\.php\//i.test(src);
}
function purgeLegacyNationIndexFileImages(scope) {
var root = scope || document;
toArray(root.querySelectorAll ? root.querySelectorAll('.clbi-nations-tabpanel-body img, .clbi-nations-tabpanel img') : []).forEach(function (img) {
var src = img.currentSrc || img.getAttribute('src') || '';
if (!isLegacyNationIndexFileUrl(src)) return;
img.removeAttribute('src');
img.setAttribute('data-clbi-legacy-src-removed', src);
});
}
function getNationFlagOffsetY(flag) {
var value;
if (!flag || typeof flag !== 'object') return 0;
value = flag.offsetY;
if (value == null) value = flag.offset_y;
if (value == null) value = flag.flagOffsetY;
if (value == null && flag.decorator === 'top') value = -1;
value = parseFloat(value);
if (!isFinite(value)) value = 0;
return Math.max(-3, Math.min(3, Math.round(value)));
}
function buildNationPageUrl(page) {
page = String(page || '').trim();
return page ? '/index.php/' + encodeWikiTitle(page) : '';
}
function getPanelEra(panel) {
var eraPanel = panel && panel.closest ? panel.closest('.clbi-nations-era-content[data-era-content]') : null;
var stack = panel && panel.closest ? panel.closest('.clbi-nations-panel-stack') : null;
return (eraPanel && eraPanel.getAttribute('data-era-content')) ||
(panel && panel.getAttribute('data-era')) ||
(stack && (stack.getAttribute('data-nations-current-era') || stack.getAttribute('data-era'))) ||
'1950';
}
function getNationListUrl(panel, era) {
var stack = panel && panel.closest ? panel.closest('.clbi-nations-panel-stack') : null;
var template = (panel && panel.getAttribute('data-nation-list-url-template')) ||
(stack && stack.getAttribute('data-nation-list-url-template')) ||
'/index.php?title=MediaWiki:{year}_Nation_List.json&action=raw&ctype=application/json';
return (panel && panel.getAttribute('data-nation-list-url')) || replaceYearTemplate(template, era);
}
function getNationLinkMapUrl(panel, era) {
var stack = panel && panel.closest ? panel.closest('.clbi-nations-panel-stack') : null;
var template = (panel && panel.getAttribute('data-nation-link-map-url-template')) ||
(stack && stack.getAttribute('data-nation-link-map-url-template')) ||
'/index.php?title=MediaWiki:{year}_Nation_Link_Map.json&action=raw&ctype=application/json';
return (panel && panel.getAttribute('data-nation-link-map-url')) || replaceYearTemplate(template, era);
}
function getHistoryEventsUrl(panel, era) {
var stack = panel && panel.closest ? panel.closest('.clbi-nations-panel-stack') : null;
var template = (panel && panel.getAttribute('data-history-events-url-template')) ||
(stack && stack.getAttribute('data-history-events-url-template')) ||
'/index.php?title=MediaWiki:{year}_History_Events.json&action=raw&ctype=application/json';
return (panel && panel.getAttribute('data-history-events-url')) || replaceYearTemplate(template, era);
}
function makeDefaultHistoryYears(era) {
var start = parseInt(era || '1950', 10);
var years = [];
var i;
if (!isFinite(start)) start = 1950;
for (i = 0; i < 10; i += 1) {
years.push({ year: String(start + i), label: String(start + i), items: [] });
}
return years;
}
function normalizeHistoryEventItem(item) {
item = item && typeof item === 'object' ? item : {};
return {
period: String(item.period || item.date || item.range || item.year || '').trim(),
title: String(item.title || item.event || item.name || item.label || '').trim(),
page: String(item.page || item.wiki_title || item.title_page || '').trim(),
external_url: String(item.external_url || item.url || '').trim(),
major: item.major === true || item.type === 'major',
note: String(item.note || '').trim(),
enabled: item.enabled !== false
};
}
function normalizeHistoryEvents(payload, era) {
var sourceYears = [];
var normalized;
if (payload && Array.isArray(payload.years)) {
sourceYears = payload.years;
} else if (payload && payload.events && typeof payload.events === 'object') {
Object.keys(payload.events).forEach(function (year) {
sourceYears.push({ year: year, label: year, items: payload.events[year] });
});
}
normalized = sourceYears.map(function (row) {
var year;
var items;
if (typeof row === 'string' || typeof row === 'number') {
row = { year: String(row), items: [] };
}
row = row && typeof row === 'object' ? row : {};
year = String(row.year || row.key || row.label || '').trim();
items = Array.isArray(row.items) ? row.items : (Array.isArray(row.events) ? row.events : []);
return {
year: year,
label: String(row.label || year).trim() || year,
items: items.map(normalizeHistoryEventItem).filter(function (item) {
return item.enabled !== false;
})
};
}).filter(function (row) {
return !!row.year;
});
if (!normalized.length) normalized = makeDefaultHistoryYears(era);
return {
version: payload && payload.version ? payload.version : 1,
era: payload && (payload.era || payload.year) ? String(payload.era || payload.year) : String(era || '1950'),
description: payload && payload.description ? String(payload.description) : '',
years: normalized
};
}
function fetchHistoryEvents(panel, era) {
var url = getHistoryEventsUrl(panel, era);
var cacheKey = era + '|' + url;
var cached = getEntryStoreJson(url) || getEntryStoreJson('MediaWiki:' + era + '_History_Events.json');
if (historyEventsCache[cacheKey]) return historyEventsCache[cacheKey];
if (cached) {
historyEventsCache[cacheKey] = Promise.resolve(normalizeHistoryEvents(cached, era));
return historyEventsCache[cacheKey];
}
historyEventsCache[cacheKey] = fetchNationJson(url, 'MediaWiki:' + era + '_History_Events.json')
.then(function (payload) {
return normalizeHistoryEvents(payload, era);
});
return historyEventsCache[cacheKey];
}
function buildHistoryPageUrl(page) {
page = String(page || '').trim();
return page ? '/index.php/' + encodeWikiTitle(page) : '';
}
function renderInlineWikiLinks(text) {
var source = String(text || '');
var out = '';
var lastIndex = 0;
var match;
var pattern = /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g;
pattern.lastIndex = 0;
while ((match = pattern.exec(source))) {
var rawPage = String(match[1] || '').trim();
var rawLabel = String(match[2] || rawPage).trim();
out += escapeHtml(source.slice(lastIndex, match.index));
if (rawPage) {
out += '<a href="' + escapeHtml(buildHistoryPageUrl(rawPage)) + '">' + escapeHtml(rawLabel) + '</a>';
} else {
out += escapeHtml(match[0]);
}
lastIndex = pattern.lastIndex;
}
out += escapeHtml(source.slice(lastIndex));
return out;
}
function hasInlineWikiLink(text) {
return /\[\[[^\]]+\]\]/.test(String(text || ''));
}
function renderHistoryEventTitle(item) {
var title = item.title || item.page || item.external_url || '미지정';
var href;
if (hasInlineWikiLink(title)) return renderInlineWikiLinks(title);
href = item.external_url || buildHistoryPageUrl(item.page || '');
return href ? '<a href="' + escapeHtml(href) + '">' + escapeHtml(title) + '</a>' : escapeHtml(title);
}
function renderHistoryEventItem(item) {
var classes = 'clbi-nations-history-item' + (item.major ? ' clbi-nations-history-item-major' : '');
var nameHtml = renderHistoryEventTitle(item);
return '<div class="' + classes + '">' +
'<span class="clbi-nations-history-period">' + escapeHtml(item.period || '') + '</span>' +
'<span class="clbi-nations-history-event-name">' + nameHtml + '</span>' +
'</div>';
}
function renderHistoryEmpty() {
return '<div class="clbi-nations-history-empty"><span class="clbi-nations-history-empty-ko">작업 중...</span><span class="clbi-nations-history-empty-en">Working in Progress...</span></div>';
}
function renderHistoryPanel(panel, data) {
var body = panel && panel.querySelector ? panel.querySelector('.clbi-nations-history-body') : null;
var years = data && Array.isArray(data.years) ? data.years : [];
var activeTab = panel.querySelector('.clbi-nations-history-year-button.is-active[data-year], .clbi-nations-history-year-button[aria-selected="true"][data-year]');
var activeYear = activeTab ? activeTab.getAttribute('data-year') : '';
var pages;
var tabs;
if (!body || !years.length) return;
if (!activeYear || !years.some(function (row) { return row.year === activeYear; })) activeYear = years[0].year;
pages = years.map(function (row) {
var active = row.year === activeYear;
var items = (row.items || []).filter(function (item) { return item && item.enabled !== false; });
return '<div class="clbi-nations-history-list clbi-nations-history-page' + (active ? ' is-active' : '') + '" data-year-panel="' + escapeHtml(row.year) + '"' + (active ? '' : ' hidden="hidden"') + '>' +
(items.length ? items.map(renderHistoryEventItem).join('') : renderHistoryEmpty()) +
'</div>';
}).join('');
tabs = years.map(function (row) {
var active = row.year === activeYear;
return '<span class="clbi-nations-history-year-button' + (active ? ' is-active' : '') + '" role="tab" tabindex="' + (active ? '0' : '-1') + '" aria-selected="' + (active ? 'true' : 'false') + '" data-year="' + escapeHtml(row.year) + '">' + escapeHtml(row.label || row.year) + '</span>';
}).join('');
body.innerHTML = '<div class="clbi-nations-history-pages">' + pages + '</div>' +
'<div class="clbi-nations-history-year-well"><div class="clbi-nations-history-year-list" role="tablist" aria-label="역사적 사건 연도 선택">' + tabs + '</div></div>';
panel.removeAttribute('data-nations-history-ready');
panel.classList.add('clbi-nations-history-json-ready');
panel.setAttribute('data-history-events-year-loaded', String(data.era || getPanelEra(panel)));
initHistoryYearPanel(panel);
scheduleHistoryItemSlides(panel);
}
function initHistoryEventsPanel(panel) {
var era;
var promise;
if (!panel || isNationListPanelHidden(panel)) return Promise.resolve(null);
if (panel.getAttribute('data-history-events-year-loaded') === getPanelEra(panel)) return Promise.resolve(panel);
if (panel.getAttribute('data-history-events-loading') === '1' && panel.HistoryEventsLoadPromise) return panel.HistoryEventsLoadPromise;
era = getPanelEra(panel);
panel.setAttribute('data-history-events-loading', '1');
promise = fetchHistoryEvents(panel, era).then(function (data) {
renderHistoryPanel(panel, data);
return panel;
}).catch(function () {
panel.classList.add('clbi-nations-history-json-fallback');
return panel;
}).finally(function () {
panel.removeAttribute('data-history-events-loading');
panel.HistoryEventsLoadPromise = null;
});
panel.HistoryEventsLoadPromise = promise;
return promise;
}
function getEntryStoreJson(url) {
if (!window.EntryStore || typeof window.EntryStore.getJsonSync !== 'function') return null;
return window.EntryStore.getJsonSync(url);
}
function normalizeNationTag(value) {
value = String(value == null ? '' : value).trim().toUpperCase();
return /^[A-Z0-9]{3}$/.test(value) ? value : '';
}
function normalizeLookupKey(value) {
return String(value == null ? '' : value)
.trim()
.replace(/_/g, ' ')
.replace(/\s+/g, ' ')
.toLowerCase();
}
function getNationItemTag(item) {
if (!item || typeof item !== 'object') return '';
return normalizeNationTag(item.tag || item.code || item.map_tag || item.mapTag || '');
}
function collectNationItemFlags(item) {
var flags = [];
if (!item || typeof item !== 'object') return flags;
if (Array.isArray(item.flags)) {
item.flags.forEach(function (flag) {
if (!flag) return;
if (typeof flag === 'string') {
flag = { file: flag };
}
if (typeof flag !== 'object') return;
if (normalizeNationFile(flag.file || flag.flag_file || flag.flag || '')) {
flags.push(flag);
}
});
}
if (!flags.length && (item.flag_file || item.flag || item.flag_title)) {
flags.push({
file: item.flag_file || item.flag || item.flag_title,
width: item.flag_width || '16px',
border: item.border !== false,
offsetMode: item.offsetMode || item.offset_mode || 'auto',
offsetY: item.offsetY || item.offset_y || 0
});
}
return flags;
}
function collectNationItemSybs(item) {
var sybs = [];
if (!item || typeof item !== 'object') return sybs;
function addSymbol(symbol) {
if (!symbol) return;
if (typeof symbol === 'string') symbol = { file: symbol };
if (typeof symbol !== 'object') return;
if (normalizeNationFile(symbol.file || symbol.syb_file || symbol.syb || symbol.symbol_file || symbol.symbol || '')) {
sybs.push(symbol);
}
}
if (Array.isArray(item.sybs)) item.sybs.forEach(addSymbol);
if (Array.isArray(item.symbols)) item.symbols.forEach(addSymbol);
if (!sybs.length && (item.syb_file || item.syb || item.symbol_file || item.symbol)) {
sybs.push({
file: item.syb_file || item.syb || item.symbol_file || item.symbol,
width: item.syb_width || item.symbol_width || '16px',
border: item.syb_border === true || item.symbol_border === true
});
}
return sybs;
}
function collectNationItemSymbols(item) {
var symbols = [];
var legacySeen = {};
function add(symbol, dedupeLegacy) {
var file = normalizeNationSymbolFile(symbol);
var key = normalizeNationFileKey(file);
var out;
if (!file) return;
if (dedupeLegacy && key) {
if (legacySeen[key]) return;
legacySeen[key] = true;
}
if (typeof symbol === 'string') {
out = { file: file };
} else {
out = Object.assign({}, symbol || {});
out.file = file;
}
if (out.border == null && out.symbol_border != null) out.border = out.symbol_border !== false;
if (out.border == null && out.syb_border != null) out.border = out.syb_border === true;
if (out.border == null) out.border = !isNationSybFile(file);
symbols.push(out);
}
if (!item || typeof item !== 'object') return symbols;
if (Array.isArray(item.symbols)) {
item.symbols.slice(0, 3).forEach(function (symbol) {
add(symbol, false);
});
if (symbols.length) return symbols.slice(0, 3);
}
collectNationItemFlags(item).forEach(function (symbol) { add(symbol, true); });
collectNationItemSybs(item).forEach(function (symbol) { add(symbol, true); });
if (!symbols.length) {
add(item.flag_file || item.flag || item.flag_title || '', true);
add(item.syb_file || item.syb || item.symbol_file || item.symbol || '', true);
}
return symbols.slice(0, 3);
}
function normalizeNationLinkMap(payload) {
var source = payload && payload.items ? payload.items : {};
var index = { byTag: {}, byPage: {}, byLabel: {} };
Object.keys(source || {}).forEach(function (key) {
var entry = source[key];
var tag;
var pageKey;
var labelKey;
if (!entry || typeof entry !== 'object') return;
tag = normalizeNationTag(entry.tag || key || entry.page || '');
pageKey = normalizeLookupKey(entry.wiki_title || entry.page_title || entry.title || '');
labelKey = normalizeLookupKey(entry.name_ko || entry.label || entry.page_name || '');
if (tag && !index.byTag[tag]) index.byTag[tag] = entry;
if (pageKey && !index.byPage[pageKey]) index.byPage[pageKey] = entry;
if (labelKey && !index.byLabel[labelKey]) index.byLabel[labelKey] = entry;
});
return index;
}
function fetchNationLinkMap(panel, era) {
var url = getNationLinkMapUrl(panel, era);
var cacheKey = era + '|' + url;
var cached = getEntryStoreJson(url) || getEntryStoreJson('MediaWiki:' + era + '_Nation_Link_Map.json');
if (nationLinkMapCache[cacheKey]) {
PanelPerf.mark('fetch nation link map cache promise hit', { era: era });
return nationLinkMapCache[cacheKey];
}
if (cached) {
PanelPerf.mark('fetch nation link map EntryStore hit', { era: era });
nationLinkMapCache[cacheKey] = Promise.resolve(PanelPerf.measure('normalize nation link map', { era: era, source: 'EntryStore' }, function () {
return normalizeNationLinkMap(cached);
}));
return nationLinkMapCache[cacheKey];
}
nationLinkMapCache[cacheKey] = PanelPerf.measure('fetch nation link map remote', { era: era }, function () {
return fetchNationJson(url, 'MediaWiki:' + era + '_Nation_Link_Map.json')
.then(function (payload) {
return PanelPerf.measure('normalize nation link map', { era: era, source: 'remote' }, function () {
return normalizeNationLinkMap(payload);
});
})
.catch(function () {
return normalizeNationLinkMap(null);
});
});
return nationLinkMapCache[cacheKey];
}
function findNationLinkEntryForItem(item, index) {
var tag;
var pageKey;
var labelKey;
if (!item || !index) return null;
tag = getNationItemTag(item);
if (tag && index.byTag && index.byTag[tag]) return index.byTag[tag];
pageKey = normalizeLookupKey(item.page || item.wiki_title || '');
if (pageKey && index.byPage && index.byPage[pageKey]) return index.byPage[pageKey];
labelKey = normalizeLookupKey(item.label || item.name || item.name_ko || '');
if (labelKey && index.byLabel && index.byLabel[labelKey]) return index.byLabel[labelKey];
return null;
}
function applyNationLinkMapFlagFallback(data, linkMapIndex) {
if (!data || !Array.isArray(data.continents)) return data;
data.continents.forEach(function (continent) {
(continent.regions || []).forEach(function (region) {
(region.items || []).forEach(function (item) {
var entry;
var file;
if (!item || collectNationItemFlags(item).length) return;
entry = findNationLinkEntryForItem(item, linkMapIndex);
file = entry ? normalizeNationFile(entry.flag_file || entry.flag || entry.flag_title || '') : '';
if (!file) return;
item.flags = [{
file: file,
width: '16px',
border: true,
offsetMode: 'auto',
offsetY: 0,
source: 'Nation_Link_Map'
}];
});
});
});
return data;
}
function getNationListFallback() {
return {
version: 1,
year: 1950,
continents: NATION_CONTINENT_FALLBACK.map(function (continent) {
return {
key: continent.key,
label: continent.label,
regions: continent.regions.map(function (region) {
return { key: region, label: region, items: [] };
})
};
})
};
}
function normalizeNationList(payload, era) {
var fallback = getNationListFallback();
var sourceContinents = payload && Array.isArray(payload.continents) ? payload.continents : [];
var sourceByKey = {};
sourceContinents.forEach(function (continent) {
if (continent && continent.key) sourceByKey[continent.key] = continent;
});
return {
version: payload && payload.version ? payload.version : 1,
year: payload && payload.year ? payload.year : parseInt(era || '1950', 10),
description: payload && payload.description ? payload.description : '',
continents: fallback.continents.map(function (base) {
var source = sourceByKey[base.key] || {};
var regionsByLabel = {};
if (Array.isArray(source.regions)) {
source.regions.forEach(function (region) {
if (!region) return;
if (region.key) regionsByLabel[region.key] = region;
if (region.label) regionsByLabel[region.label] = region;
});
}
return {
key: base.key,
label: source.label || base.label,
regions: base.regions.map(function (baseRegion) {
var region = regionsByLabel[baseRegion.key] || regionsByLabel[baseRegion.label] || baseRegion;
return {
key: region.key || baseRegion.key,
label: region.label || baseRegion.label,
items: Array.isArray(region.items) ? region.items.filter(function (item) {
return item && item.enabled !== false;
}) : []
};
})
};
})
};
}
function fetchNationList(panel, era) {
var url = getNationListUrl(panel, era);
var cacheKey = era + '|' + url;
var cached = getEntryStoreJson(url) || getEntryStoreJson('MediaWiki:' + era + '_Nation_List.json');
if (nationListCache[cacheKey]) {
PanelPerf.mark('fetch nation list cache promise hit', { era: era });
return nationListCache[cacheKey];
}
if (cached) {
PanelPerf.mark('fetch nation list EntryStore hit', { era: era });
nationListCache[cacheKey] = Promise.resolve(PanelPerf.measure('normalize nation list', { era: era, source: 'EntryStore' }, function () {
return normalizeNationList(cached, era);
}));
return nationListCache[cacheKey];
}
nationListCache[cacheKey] = PanelPerf.measure('fetch nation list remote', { era: era }, function () {
return fetchNationJson(url, 'MediaWiki:' + era + '_Nation_List.json')
.then(function (payload) {
return PanelPerf.measure('normalize nation list', { era: era, source: 'remote' }, function () {
return normalizeNationList(payload, era);
});
});
});
return nationListCache[cacheKey];
}
function normalizeNationFlagMaxWidth(value) {
var raw = String(value || '').trim();
var match = raw.match(/^(\d+(?:\.\d+)?)\s*(px)?$/i);
var n = match ? parseFloat(match[1]) : 16;
if (!isFinite(n)) n = 16;
if (n < 8) n = 8;
if (n > 32) n = 32;
return Math.round(n * 100) / 100 + 'px';
}
function isNationSybFile(file) {
return /^Syb-/i.test(normalizeNationFile(file).replace(/_/g, ' ').trim());
}
function getNationSymbolBorder(symbol, file) {
if (!symbol || typeof symbol !== 'object') return !isNationSybFile(file);
if (symbol.border != null) return symbol.border !== false;
if (symbol.symbol_border != null) return symbol.symbol_border !== false;
if (symbol.syb_border != null) return symbol.syb_border === true;
return !isNationSybFile(file);
}
function normalizeNationSymbolFile(symbol) {
if (typeof symbol === 'string') return normalizeNationFile(symbol);
if (!symbol || typeof symbol !== 'object') return '';
return normalizeNationFile(symbol.file || symbol.flag_file || symbol.flag || symbol.flag_title || symbol.syb_file || symbol.syb || symbol.symbol_file || symbol.symbol || '');
}
function renderNationFlag(flag) {
var file;
var url;
var width;
var classes;
var offsetY;
if (!flag) return '';
file = normalizeNationFile(flag.file || flag.flag_file || flag.flag || '');
if (!file) return '';
url = buildNationFileUrl(file);
width = normalizeNationFlagMaxWidth(flag.width || flag.flag_width || '24px');
offsetY = getNationFlagOffsetY(flag);
classes = 'clbi-nations-list-flag' + (flag.border !== false ? ' has-border' : '');
if (!url) {
return '<span class="clbi-nations-list-flag-slot is-missing" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:' + offsetY + 'px;" data-flag-file="' + escapeHtml(file) + '" title="국기 파일 URL 미확인: ' + escapeHtml(file) + '"></span>';
}
return '<span class="clbi-nations-list-flag-slot" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:' + offsetY + 'px;" data-flag-file="' + escapeHtml(file) + '">' +
'<img class="' + classes + '" src="' + escapeHtml(url) + '" alt="" loading="eager" decoding="sync">' +
'</span>';
}
function renderNationSyb(syb) {
var file;
var url;
var width;
var classes;
if (!syb) return '';
file = normalizeNationFile(syb.file || syb.syb_file || syb.syb || syb.symbol_file || syb.symbol || '');
if (!file) return '';
url = buildNationFileUrl(file);
width = normalizeNationFlagMaxWidth(syb.width || syb.syb_width || syb.symbol_width || '16px');
classes = 'clbi-nations-list-flag clbi-nations-list-syb' + (syb.border === true ? ' has-border' : '');
if (!url) {
return '<span class="clbi-nations-list-flag-slot clbi-nations-list-syb-slot is-missing" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:0px;" data-flag-file="' + escapeHtml(file) + '" title="SYB 파일 URL 미확인: ' + escapeHtml(file) + '"></span>';
}
return '<span class="clbi-nations-list-flag-slot clbi-nations-list-syb-slot" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:0px;" data-flag-file="' + escapeHtml(file) + '">' +
'<img class="' + classes + '" src="' + escapeHtml(url) + '" alt="" loading="eager" decoding="sync">' +
'</span>';
}
function renderNationSymbol(symbol) {
var file;
var url;
var width;
var classes;
var offsetY;
var isSyb;
var hasBorder;
if (!symbol) return '';
file = normalizeNationSymbolFile(symbol);
if (!file) return '';
isSyb = isNationSybFile(file);
url = buildNationFileUrl(file);
width = normalizeNationFlagMaxWidth(symbol.width || symbol.flag_width || symbol.syb_width || symbol.symbol_width || (isSyb ? '16px' : '24px'));
offsetY = getNationFlagOffsetY(symbol);
hasBorder = getNationSymbolBorder(symbol, file);
classes = 'clbi-nations-list-flag clbi-nations-list-symbol' + (isSyb ? ' clbi-nations-list-syb' : '') + (hasBorder ? ' has-border' : '');
if (!url) {
return '<span class="clbi-nations-list-flag-slot clbi-nations-list-symbol-slot' + (isSyb ? ' clbi-nations-list-syb-slot' : '') + ' is-missing" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:' + offsetY + 'px;" data-flag-file="' + escapeHtml(file) + '" title="상징물 파일 URL 미확인: ' + escapeHtml(file) + '"></span>';
}
return '<span class="clbi-nations-list-flag-slot clbi-nations-list-symbol-slot' + (isSyb ? ' clbi-nations-list-syb-slot' : '') + '" style="--flag-max-w:' + escapeHtml(width) + ';--flag-y:' + offsetY + 'px;" data-flag-file="' + escapeHtml(file) + '">' +
'<img class="' + classes + '" src="' + escapeHtml(url) + '" alt="" loading="eager" decoding="sync">' +
'</span>';
}
function bindNationFlagFallbacks(scope) {
var root = scope || document;
toArray(root.querySelectorAll('.clbi-nations-list-flag-slot .clbi-nations-list-flag')).forEach(function (img) {
if (img.getAttribute('data-flag-missing-handler') === '1') return;
img.setAttribute('data-flag-missing-handler', '1');
img.addEventListener('error', function () {
var slot = img.closest ? img.closest('.clbi-nations-list-flag-slot') : null;
if (slot) {
slot.classList.add('is-missing');
slot.setAttribute('title', '국기 파일 로드 실패: ' + (slot.getAttribute('data-flag-file') || ''));
}
img.removeAttribute('src');
});
});
}
function getResolvedFlagUrl(file) {
var key = normalizeNationFileKey(file);
return key ? (nationResolvedFileUrlCache[key] || '') : '';
}
function collectResolvedNationFlagUrls(data) {
var files = collectNationFlagFileNames(data);
var seen = {};
var urls = [];
files.forEach(function (file) {
var url = getResolvedFlagUrl(file);
if (!url || seen[url]) return;
seen[url] = true;
urls.push(url);
});
return urls;
}
function collectResolvedNationFlagUrlsForFiles(files) {
var seen = {};
var urls = [];
(files || []).forEach(function (file) {
var url = getResolvedFlagUrl(file);
if (!url || seen[url]) return;
seen[url] = true;
urls.push(url);
});
return urls;
}
function warmNationFlagUrlsToCompletion(urls, owner, options) {
var queue = unique((urls || []).filter(Boolean));
var index = 0;
var active = 0;
var done = 0;
var ok = 0;
var fail = 0;
var concurrency = Math.max(1, Math.min(32, Number(options && options.concurrency) || 18));
var storeKey = options && options.storeKey ? String(options.storeKey) : 'CLBI_NationsFlagWarmImages';
var meta = options && options.meta ? options.meta : {};
var startId;
if (!queue.length) return Promise.resolve({ total: 0, ok: 0, fail: 0 });
if (owner) owner[storeKey] = [];
startId = PanelPerf.start('background nation flag image load', Object.assign({}, meta, { count: queue.length, concurrency: concurrency }));
return new Promise(function (resolve) {
function settle(success, img) {
active -= 1;
done += 1;
if (success) ok += 1;
else fail += 1;
if (owner && owner[storeKey]) {
var idx = owner[storeKey].indexOf(img);
if (idx >= 0) owner[storeKey].splice(idx, 1);
}
pump();
}
function startOne(url) {
var img = new Image();
var finished = false;
active += 1;
img.decoding = 'sync';
img.loading = 'eager';
try { img.fetchPriority = 'high'; } catch (ignorePriority) {}
if (owner && owner[storeKey]) owner[storeKey].push(img);
function finish(success) {
if (finished) return;
finished = true;
settle(success, img);
}
img.onload = function () {
if (img.decode) {
img.decode().then(function () { finish(true); }).catch(function () { finish(true); });
} else {
finish(true);
}
};
img.onerror = function () { finish(false); };
img.src = url;
if (img.complete) {
scheduleIdle(function () { finish(img.naturalWidth > 0); }, { timeout: 250 });
}
}
function pump() {
while (active < concurrency && index < queue.length) {
startOne(queue[index++]);
}
if (done >= queue.length) {
PanelPerf.end(startId, Object.assign({}, meta, { count: queue.length, ok: ok, fail: fail }));
resolve({ total: queue.length, ok: ok, fail: fail });
}
}
scheduleIdle(pump, { timeout: 300 });
});
}
function prewarmImageUrls(urls, limit) {
var index = 0;
var max = Math.max(0, limit || 64);
urls = (urls || []).filter(Boolean).slice(0, max);
if (!urls.length) return;
function step(deadline) {
var started = 0;
while (index < urls.length && started < 6 && (!deadline || deadline.timeRemaining() > 4 || deadline.didTimeout)) {
var img = new Image();
var url = urls[index++];
started += 1;
img.decoding = 'async';
img.loading = 'eager';
img.src = url;
if (img.decode) {
img.decode().catch(function () {});
}
}
if (index < urls.length) scheduleIdle(step, { timeout: 850 });
}
scheduleIdle(step, { timeout: 850 });
}
function prewarmRenderedNationFlags(scope) {
var urls = [];
var seen = {};
var perf = window.InteractionPerf;
var perfId = perf && typeof perf.start === 'function' ? perf.start('panel collect rendered flag prewarm urls', {
scope: scope === document ? 'document' : 'panel'
}) : null;
toArray((scope || document).querySelectorAll ? (scope || document).querySelectorAll('.clbi-nations-list-flag[src]') : []).forEach(function (img) {
var url = img.currentSrc || img.getAttribute('src') || '';
if (!url || seen[url]) return;
seen[url] = true;
urls.push(url);
});
if (perf && typeof perf.end === 'function' && perfId) {
perf.end(perfId, { urls: urls.length });
}
prewarmImageUrls(urls, 80);
}
function hydrateResolvedFlagSlots(scope) {
var root = scope || document;
var hydrated = 0;
var scanned = 0;
var perf = window.InteractionPerf;
var perfId = perf && typeof perf.start === 'function' ? perf.start('panel hydrate resolved flag slots', {
scope: root === document ? 'document' : 'panel'
}) : null;
try {
toArray(root.querySelectorAll ? root.querySelectorAll('.clbi-nations-list-flag-slot[data-flag-file]') : []).forEach(function (slot) {
scanned += 1;
var file = normalizeNationFile(slot.getAttribute('data-flag-file') || '');
var url = buildNationFileUrl(file);
var img;
var isSyb;
var isSymbol;
var classes;
if (!file || !url) return;
if (slot.querySelector && slot.querySelector('.clbi-nations-list-flag[src]')) return;
isSyb = slot.classList.contains('clbi-nations-list-syb-slot') || isNationSybFile(file);
isSymbol = slot.classList.contains('clbi-nations-list-symbol-slot');
classes = 'clbi-nations-list-flag' +
(isSymbol ? ' clbi-nations-list-symbol' : '') +
(isSyb ? ' clbi-nations-list-syb' : '') +
(!isSyb ? ' has-border' : '');
slot.classList.remove('is-missing');
slot.removeAttribute('title');
slot.innerHTML = '';
img = document.createElement('img');
img.className = classes;
img.alt = '';
img.loading = 'eager';
img.decoding = 'sync';
try { img.fetchPriority = 'high'; } catch (ignorePriority) {}
img.src = url;
slot.appendChild(img);
hydrated += 1;
});
if (hydrated) bindNationFlagFallbacks(root);
return hydrated;
} finally {
if (perf && typeof perf.end === 'function' && perfId) {
perf.end(perfId, { scanned: scanned, hydrated: hydrated });
}
}
}
function scheduleResolvedFlagHydrationSweeps(panel, options) {
var delays = options && options.delays ? options.delays : [0, 16, 80, 180, 360, 720, 1200, 2000];
var era = options && options.era ? String(options.era) : getPanelEra(panel);
if (!panel || !panel.querySelector) return;
if (!panel.CLBI_NationsFlagHydrationSweepTimers) panel.CLBI_NationsFlagHydrationSweepTimers = [];
panel.CLBI_NationsFlagHydrationSweepTimers.forEach(function (timer) {
try { window.clearTimeout(timer); } catch (ignoreClear) {}
});
panel.CLBI_NationsFlagHydrationSweepTimers = [];
delays.forEach(function (delay, index) {
var timer = window.setTimeout(function () {
var hydrated;
if (!document.body.contains(panel)) return;
hydrated = hydrateResolvedFlagSlots(panel);
if (hydrated) {
prewarmRenderedNationFlags(panel);
PanelPerf.mark('remaining nation flags hydration sweep', {
era: era,
pass: index,
delay: delay,
hydrated: hydrated
});
}
}, Math.max(0, delay));
panel.CLBI_NationsFlagHydrationSweepTimers.push(timer);
});
}
function bindRemainingFlagHydrationWatcher(panel) {
if (!panel || panel.CLBI_NationsRemainingFlagHydrationWatcherBound) return;
panel.CLBI_NationsRemainingFlagHydrationWatcherBound = true;
if (typeof MutationObserver === 'function') {
panel.CLBI_NationsRemainingFlagObserver = new MutationObserver(function (mutations) {
var shouldHydrate = false;
mutations.forEach(function (mutation) {
if (shouldHydrate) return;
toArray(mutation.addedNodes || []).forEach(function (node) {
if (shouldHydrate || !node || node.nodeType !== 1) return;
if ((node.matches && node.matches('.clbi-nations-list-flag-slot[data-flag-file]')) ||
(node.querySelector && node.querySelector('.clbi-nations-list-flag-slot[data-flag-file]'))) {
shouldHydrate = true;
}
});
});
if (shouldHydrate && panel.CLBI_NationsRemainingFlagsResolved) {
scheduleResolvedFlagHydrationSweeps(panel, {
era: getPanelEra(panel),
delays: [0, 60, 180, 420]
});
}
});
try {
panel.CLBI_NationsRemainingFlagObserver.observe(panel, { childList: true, subtree: true });
} catch (ignoreObserve) {}
}
panel.addEventListener('click', function (event) {
var target = event && event.target;
if (!panel.CLBI_NationsRemainingFlagsResolved) return;
if (target && target.closest && target.closest('.clbi-nations-tabpanel-tab, [data-continent], .clbi-nations-era-button')) {
scheduleResolvedFlagHydrationSweeps(panel, {
era: getPanelEra(panel),
delays: [0, 40, 120, 260]
});
}
}, true);
}
function scheduleAllNationFlagBackgroundWarm(panel, data) {
var urls;
var era;
if (!panel || !data) return;
if (panel.CLBI_NationsAllFlagBackgroundWarmStarted) return;
urls = collectResolvedNationFlagUrls(data);
if (!urls.length) return;
panel.CLBI_NationsAllFlagBackgroundWarmStarted = true;
era = String(data && (data.year || getPanelEra(panel)) || '');
window.setTimeout(function () {
if (!document.body.contains(panel)) return;
warmNationFlagUrlsToCompletion(urls, panel, {
storeKey: 'CLBI_NationsAllContinentFlagWarmImages',
concurrency: 24,
meta: { era: era, phase: 'all-continent-prebuilt-flags' }
}).then(function (result) {
PanelPerf.mark('all continent flag background load complete', {
era: era,
total: result.total,
ok: result.ok,
fail: result.fail
});
});
}, 32);
}
function scheduleRemainingNationFlagResolution(panel, data, initialFiles) {
var allFiles = collectNationFlagFileNames(data);
var remainingFiles = subtractNationFlagFiles(allFiles, initialFiles);
var handle;
if (!panel || !remainingFiles.length) return;
panel.CLBI_NationsRemainingFlagFiles = remainingFiles.slice();
panel.CLBI_NationsRemainingFlagsResolved = false;
bindRemainingFlagHydrationWatcher(panel);
handle = panel.CLBI_NationsRemainingFlagResolveHandle;
if (handle != null) {
try { window.clearTimeout(handle); } catch (ignoreClear) {}
try { cancelIdle(handle); } catch (ignoreIdleClear) {}
}
/*
The first visible continent is part of the entry-ready contract. The
rest of the flags must not wait until the user opens another tab:
start resolving them right after the first paint, inject eager images
into every already-rendered slot, and keep explicit Image() objects
alive until load/decode completes so a fast tab switch still sees
warm flags instead of blank placeholders.
*/
panel.CLBI_NationsRemainingFlagResolveHandle = window.setTimeout(function () {
panel.CLBI_NationsRemainingFlagResolveHandle = null;
PanelPerf.measure('resolve remaining nation flag URLs', {
era: String(data && (data.year || getPanelEra(panel)) || ''),
count: remainingFiles.length,
mode: 'post-visible-aggressive'
}, function () {
return resolveNationFlagFileUrls(remainingFiles).then(function () {
var hydrated;
var urls = collectResolvedNationFlagUrlsForFiles(remainingFiles);
var era = String(data && (data.year || getPanelEra(panel)) || '');
panel.CLBI_NationsRemainingFlagsResolved = true;
hydrated = hydrateResolvedFlagSlots(panel);
scheduleResolvedFlagHydrationSweeps(panel, {
era: era,
delays: [0, 16, 80, 180, 360, 720, 1200, 2000, 3200]
});
PanelPerf.mark('hydrate remaining nation flags', {
era: era,
files: remainingFiles.length,
hydrated: hydrated,
urls: urls.length,
mode: 'post-visible-aggressive'
});
prewarmRenderedNationFlags(panel);
return warmNationFlagUrlsToCompletion(urls, panel, {
storeKey: 'CLBI_NationsRemainingFlagWarmImages',
concurrency: 24,
meta: { era: era, phase: 'remaining-flags-aggressive' }
}).then(function (result) {
var hydratedAgain = hydrateResolvedFlagSlots(panel);
scheduleResolvedFlagHydrationSweeps(panel, {
era: era,
delays: [0, 100, 300, 700]
});
PanelPerf.mark('remaining nation flag background load complete', {
era: era,
total: result.total,
ok: result.ok,
fail: result.fail,
hydratedAgain: hydratedAgain
});
});
});
});
}, 32);
}
function waitForRenderedFlagImages(scope, options) {
var root = scope || document;
var imageRoot = root;
var images;
var timeoutMs = options && options.timeoutMs ? Number(options && options.timeoutMs) : 15000;
var started = Date.now();
if (options && options.activeOnly && root && root.querySelector) {
imageRoot = root.querySelector('.clbi-nations-tabpanel-continent.is-active[data-continent-panel]') || root;
}
images = toArray(imageRoot.querySelectorAll ? imageRoot.querySelectorAll('.clbi-nations-list-flag[src]') : []);
function waitOne(img) {
var src = img.currentSrc || img.getAttribute('src') || '';
if (!src) return Promise.resolve(false);
if (img.complete && img.naturalWidth > 0) {
if (img.decode) return img.decode().then(function () { return true; }).catch(function () { return true; });
return Promise.resolve(true);
}
return new Promise(function (resolve) {
var done = false;
var timer;
function finish(ok) {
if (done) return;
done = true;
if (timer) window.clearTimeout(timer);
resolve(!!ok);
}
timer = window.setTimeout(function () { finish(false); }, Math.max(500, timeoutMs - (Date.now() - started)));
img.addEventListener('load', function () {
if (img.decode) img.decode().then(function () { finish(true); }).catch(function () { finish(true); });
else finish(true);
}, { once: true });
img.addEventListener('error', function () { finish(false); }, { once: true });
});
}
if (!images.length) return Promise.resolve({ total: 0, ok: 0, fail: 0 });
return Promise.all(images.map(waitOne)).then(function (results) {
var ok = results.filter(Boolean).length;
return { total: images.length, ok: ok, fail: images.length - ok };
});
}
function renderNationItem(item, index, items) {
var symbols = collectNationItemSymbols(item);
var symbolHtml = symbols.map(renderNationSymbol).join('');
var label = item.label || item.name || item.name_ko || item.page || item.external_url || '미지정';
var href = item.external_url || buildNationPageUrl(item.page || item.wiki_title || '');
var tag = String(item.tag || item.code || item.page_tag || '').trim();
var page = String(item.page || item.wiki_title || '').trim();
var linkHtml = href ? '<a class="clbi-nations-list-link" href="' + escapeHtml(href) + '">' + escapeHtml(label) + '</a>' : '<span class="clbi-nations-list-label">' + escapeHtml(label) + '</span>';
var divider = index < items.length - 1 ? '<span class="clbi-nations-list-divider" aria-hidden="true" draggable="false">·</span>' : '';
return '<span class="clbi-nations-list-item" data-nation-tag="' + escapeHtml(tag) + '" data-nation-page="' + escapeHtml(page) + '" data-nation-label="' + escapeHtml(label) + '">' + symbolHtml + linkHtml + '</span>' + divider;
}
function renderNationEmpty() {
return '<span class="clbi-nations-era-wip-message"><span class="clbi-nations-era-wip-message-ko">작업 중...</span><span class="clbi-nations-era-wip-message-en">Working in Progress...</span></span>';
}
function getElementOuterWidth(element) {
var rect;
var style;
var width;
if (!element || !element.getBoundingClientRect) return 0;
rect = element.getBoundingClientRect();
style = window.getComputedStyle ? window.getComputedStyle(element) : null;
width = rect && isFinite(rect.width) ? rect.width : (element.offsetWidth || 0);
if (style) {
width += parseFloat(style.marginLeft) || 0;
width += parseFloat(style.marginRight) || 0;
}
return width;
}
function getElementContentWidth(element) {
var style;
var width;
if (!element) return 0;
style = window.getComputedStyle ? window.getComputedStyle(element) : null;
width = element.clientWidth || 0;
if (style) {
width -= parseFloat(style.paddingLeft) || 0;
width -= parseFloat(style.paddingRight) || 0;
}
return Math.max(0, Math.floor(width));
}
function isNationListElement(node, className) {
return !!(node && node.nodeType === 1 && node.classList && node.classList.contains(className));
}
function collectNationListSnapGroups(well) {
var nodes = toArray(well ? well.childNodes : []);
var groups = [];
var index = 0;
while (index < nodes.length) {
var node = nodes[index];
var group;
var nextIndex;
var nextNode;
if (!isNationListElement(node, 'clbi-nations-list-item')) {
index += 1;
continue;
}
group = [node];
nextIndex = index + 1;
while (nextIndex < nodes.length && nodes[nextIndex].nodeType === 3 && !String(nodes[nextIndex].nodeValue || '').trim()) {
nextIndex += 1;
}
nextNode = nodes[nextIndex];
if (isNationListElement(nextNode, 'clbi-nations-list-divider')) {
group.push(nextNode);
index = nextIndex + 1;
} else {
index += 1;
}
groups.push(group);
}
return groups;
}
function measureNationListGroup(group) {
return (group || []).reduce(function (sum, node) {
return sum + getElementOuterWidth(node);
}, 0);
}
function positionNationListSnapLines(well) {
var contentWidth = getElementContentWidth(well);
if (!well || !contentWidth) return;
toArray(well.querySelectorAll('.nations-list-line')).forEach(function (line) {
var inner = line.querySelector('.nations-list-line-inner');
var rect = inner && inner.getBoundingClientRect ? inner.getBoundingClientRect() : null;
var width = rect && isFinite(rect.width) ? rect.width : (inner ? inner.offsetWidth : 0);
var left = Math.max(0, Math.round((contentWidth - width) / 2));
/*
* Keep visual centering, but snap the row start to the integer
* pixel grid. The stylesheet intentionally zeroes row padding
* with !important, so a plain inline paddingLeft is ignored.
* Use setProperty(..., 'important') or the row falls back to
* left alignment even though the centering math is correct.
*/
line.style.setProperty('padding-left', left + 'px', 'important');
});
}
function snapNationListWellRows(well) {
var contentWidth;
var groups;
var lines = [];
var current = [];
var currentWidth = 0;
var container;
if (!well || !well.querySelector || !well.querySelector('.clbi-nations-list-item')) return;
contentWidth = getElementContentWidth(well);
if (!contentWidth) return;
if (!well.NationListRowSnapSourceHtml || !well.classList.contains('is-row-snapped')) {
well.NationListRowSnapSourceHtml = well.innerHTML;
} else {
well.innerHTML = well.NationListRowSnapSourceHtml;
bindNationFlagFallbacks(well);
}
well.classList.remove('is-row-snapped');
groups = collectNationListSnapGroups(well);
if (!groups.length) return;
groups.forEach(function (group) {
var width = Math.ceil(measureNationListGroup(group));
if (current.length && currentWidth + width > contentWidth) {
lines.push(current);
current = [];
currentWidth = 0;
}
current.push(group);
currentWidth += width;
});
if (current.length) lines.push(current);
container = document.createElement('div');
container.className = 'nations-list-lines';
lines.forEach(function (lineGroups) {
var line = document.createElement('span');
var inner = document.createElement('span');
line.className = 'nations-list-line';
inner.className = 'nations-list-line-inner';
lineGroups.forEach(function (group, groupIndex) {
var isLastGroupInLine = groupIndex === lineGroups.length - 1;
group.forEach(function (node) {
if (isLastGroupInLine && isNationListElement(node, 'clbi-nations-list-divider')) {
return;
}
inner.appendChild(node);
});
});
line.appendChild(inner);
container.appendChild(line);
});
well.innerHTML = '';
well.appendChild(container);
well.classList.add('is-row-snapped');
positionNationListSnapLines(well);
}
function snapNationListRows(scope) {
var root = scope || document;
if (!root || !root.querySelectorAll) return;
if (root !== document && isNationListPanelHidden(root)) return;
toArray(root.querySelectorAll('.clbi-nations-tabpanel-well')).forEach(snapNationListWellRows);
}
function shouldDelayNationListRowSnap(root, reason) {
if (reason === 'render') return false;
if (hasRecentNationPanelDirectInteraction(900)) return true;
if (isNationsGlobeBusyForPanelWork()) return true;
return false;
}
function scheduleNationListRowSnap(scope, options) {
var root = scope || document;
var force = !!(options && options.force);
var reason = options && options.reason ? String(options.reason) : '';
var delay;
if (!root) return;
/*
* Row snap scheduler — 20260710.
*
* The row snap pass rewrites well contents and forces layout. It must
* run for the initial rendered panel and real viewport/font changes, but
* it must not compete with continent clicks or globe manipulation. Keep
* the same generated DOM/classes, collapse duplicates, and postpone late
* maintenance until the user has stopped interacting for a short window.
*/
if (root !== document && !document.body.contains(root)) return;
if (root !== document && isNationListPanelHidden(root)) return;
if (!force && root.NationListRowSnapSettled) return;
if (root.NationListRowSnapRaf || root.NationListRowSnapTimer) return;
if (force && root.NationListRowSnapSettled && reason !== 'resize' && reason !== 'font') return;
if (shouldDelayNationListRowSnap(root, reason)) {
delay = reason === 'font' ? 720 : 360;
root.NationListRowSnapTimer = window.setTimeout(function () {
root.NationListRowSnapTimer = null;
scheduleNationListRowSnap(root, { force: force, reason: reason || 'deferred' });
}, delay);
return;
}
root.NationListRowSnapRaf = window.requestAnimationFrame(function () {
root.NationListRowSnapRaf = null;
if (root !== document && (!document.body.contains(root) || isNationListPanelHidden(root))) return;
interactionPerfMeasure('panel row snap frame', { pass: 1, scope: root === document ? 'document' : 'panel', reason: reason || 'auto' }, function () {
snapNationListRows(root);
});
window.requestAnimationFrame(function () {
if (root !== document && (!document.body.contains(root) || isNationListPanelHidden(root))) return;
interactionPerfMeasure('panel row snap frame', { pass: 2, scope: root === document ? 'document' : 'panel', reason: reason || 'auto' }, function () {
snapNationListRows(root);
});
root.NationListRowSnapSettled = true;
});
});
if (document.fonts && document.fonts.ready && !root.NationListFontSnapBound) {
root.NationListFontSnapBound = true;
document.fonts.ready.then(function () {
if (!root || (root !== document && !document.body.contains(root))) return;
if (root.NationListFontSnapSettled) return;
root.NationListFontSnapSettled = true;
root.NationListRowSnapSettled = false;
scheduleNationListRowSnap(root, { force: true, reason: 'font' });
}).catch(function () {});
}
}
function bindNationListRowSnapResize() {
if (window.NationListRowSnapResizeBound) return;
window.NationListRowSnapResizeBound = true;
window.addEventListener('resize', function () {
if (window.NationListRowSnapResizeRaf) {
window.cancelAnimationFrame(window.NationListRowSnapResizeRaf);
}
window.NationListRowSnapResizeRaf = window.requestAnimationFrame(function () {
var panel;
window.NationListRowSnapResizeRaf = null;
panel = findActiveContinentPanel();
if (!panel) return;
panel.NationListRowSnapSettled = false;
scheduleNationListRowSnap(panel, { force: true, reason: 'resize' });
});
});
}
function ensureHistoryScrollText(target) {
var direct;
var wrap;
if (!target || !target.querySelector) return null;
direct = target.querySelector(':scope > .clbi-nations-history-scroll-text');
if (direct) return direct;
if (target.children.length === 1 && target.firstElementChild && target.firstElementChild.tagName && target.firstElementChild.tagName.toLowerCase() === 'a') {
target.firstElementChild.classList.add('clbi-nations-history-scroll-text');
return target.firstElementChild;
}
wrap = document.createElement('span');
wrap.className = 'clbi-nations-history-scroll-text';
while (target.firstChild) wrap.appendChild(target.firstChild);
target.appendChild(wrap);
return wrap;
}
function updateHistorySlideTarget(target) {
var text;
var wrapW;
var textW;
var scrollDist;
var duration;
var scrollKey;
if (!target || !target.getBoundingClientRect) return;
if (target.offsetParent === null && !target.getClientRects().length) return;
text = ensureHistoryScrollText(target);
if (!text) return;
target.classList.add('is-history-scroll-viewport');
wrapW = Math.floor(target.clientWidth || 0);
textW = Math.ceil(text.scrollWidth || 0);
if (!wrapW || !textW || textW <= wrapW + 1) {
target.classList.remove('is-history-scrolling');
text.style.animation = '';
text.style.animationDelay = '';
text.style.removeProperty('--scroll-dist');
text.removeAttribute('data-history-scroll-key');
return;
}
scrollDist = '-' + Math.max(0, textW - wrapW + 8) + 'px';
duration = Math.max(4.5, textW / 40);
scrollKey = scrollDist + '|' + duration;
target.classList.add('is-history-scrolling');
if (text.getAttribute('data-history-scroll-key') === scrollKey) return;
text.setAttribute('data-history-scroll-key', scrollKey);
text.style.setProperty('--scroll-dist', scrollDist);
text.style.animation = 'clbi-scroll ' + duration + 's linear infinite';
text.style.animationDelay = '0.4s';
}
function updateHistoryItemSlides(scope) {
var root = scope || document;
if (!root || !root.querySelectorAll) return;
toArray(root.querySelectorAll('.clbi-nations-history-period, .clbi-nations-history-event-name')).forEach(updateHistorySlideTarget);
}
function scheduleHistoryItemSlides(scope) {
var root = scope || document;
if (!root) return;
if (root.HistoryItemSlideRaf) {
window.cancelAnimationFrame(root.HistoryItemSlideRaf);
root.HistoryItemSlideRaf = null;
}
root.HistoryItemSlideRaf = window.requestAnimationFrame(function () {
root.HistoryItemSlideRaf = null;
if (root !== document && !document.body.contains(root)) return;
updateHistoryItemSlides(root);
window.requestAnimationFrame(function () {
if (root !== document && !document.body.contains(root)) return;
updateHistoryItemSlides(root);
});
});
if (document.fonts && document.fonts.ready && !root.HistoryItemSlideFontBound) {
root.HistoryItemSlideFontBound = true;
document.fonts.ready.then(function () {
scheduleHistoryItemSlides(root);
}).catch(function () {});
}
}
function bindHistoryItemSlideResize() {
if (window.HistoryItemSlideResizeBound) return;
window.HistoryItemSlideResizeBound = true;
window.addEventListener('resize', function () {
if (window.HistoryItemSlideResizeRaf) {
window.cancelAnimationFrame(window.HistoryItemSlideResizeRaf);
}
window.HistoryItemSlideResizeRaf = window.requestAnimationFrame(function () {
window.HistoryItemSlideResizeRaf = null;
scheduleHistoryItemSlides(document);
});
});
}
function playNationsPanelClickSound() {
try {
if (window && typeof window.playStaticSound === 'function') {
window.playStaticSound();
} else if (typeof playStaticSound === 'function') {
playStaticSound();
}
} catch (err) {}
}
function getNationItemFromEventTarget(target, panel) {
var item = target && target.closest ? target.closest('.clbi-nations-list-item[data-nation-tag]') : null;
if (!item || !panel || !panel.contains(item)) return null;
return item;
}
function getGlobeInstanceForNationItem(item) {
var stack = item && item.closest ? item.closest('.clbi-nations-panel-stack') : null;
var globe = stack && stack.querySelector ? stack.querySelector('.clbi-nations-globe-window[data-nations-globe], .clbi-nations-globe-window') : null;
var instance = globe && globe.CLBI_NationsGlobeInstance ? globe.CLBI_NationsGlobeInstance : null;
if (instance && typeof instance.focusNationByPage === 'function') return instance;
if (window.CLBI_NationsGlobe && typeof window.CLBI_NationsGlobe.init === 'function' && stack) {
window.CLBI_NationsGlobe.init(stack);
instance = globe && globe.CLBI_NationsGlobeInstance ? globe.CLBI_NationsGlobeInstance : null;
}
return instance && typeof instance.focusNationByPage === 'function' ? instance : null;
}
function clearNationPanelHoverState(panel, except) {
if (!panel || !panel.querySelectorAll) return;
toArray(panel.querySelectorAll('.clbi-nations-list-item.is-panel-hovered')).forEach(function (item) {
if (item !== except) item.classList.remove('is-panel-hovered');
});
}
function cancelNationPanelPendingHoverFocus(panel) {
if (!panel) return;
panel.CLBI_NationHoverFocusToken = (panel.CLBI_NationHoverFocusToken || 0) + 1;
if (panel.CLBI_NationHoverFocusTimer) {
window.clearTimeout(panel.CLBI_NationHoverFocusTimer);
panel.CLBI_NationHoverFocusTimer = null;
}
if (panel.CLBI_NationHoverFocusRaf) {
window.cancelAnimationFrame(panel.CLBI_NationHoverFocusRaf);
panel.CLBI_NationHoverFocusRaf = null;
}
panel.CLBI_NationHoverFocusPendingTag = '';
}
function clearNationPanelHoverFocus(panel) {
cancelNationPanelPendingHoverFocus(panel);
clearNationPanelHoverState(panel, null);
}
function requestNationItemGlobeFocus(panel, item, immediate) {
var tag;
var delay;
var token;
if (!panel || !item) return;
tag = String(item.getAttribute('data-nation-tag') || '').trim();
if (!tag) return;
cancelNationPanelPendingHoverFocus(panel);
clearNationPanelHoverState(panel, item);
item.classList.add('is-panel-hovered');
delay = immediate ? 0 : 75;
token = panel.CLBI_NationHoverFocusToken || 0;
panel.CLBI_NationHoverFocusPendingTag = tag;
panel.CLBI_NationHoverFocusTimer = window.setTimeout(function () {
panel.CLBI_NationHoverFocusTimer = null;
if (panel.CLBI_NationHoverFocusToken !== token) return;
panel.CLBI_NationHoverFocusRaf = window.requestAnimationFrame(function () {
var instance;
panel.CLBI_NationHoverFocusRaf = null;
if (panel.CLBI_NationHoverFocusToken !== token) return;
if (!document.body.contains(item) || !item.classList.contains('is-panel-hovered')) return;
if (panel.CLBI_NationHoverFocusPendingTag !== tag) return;
instance = getGlobeInstanceForNationItem(item);
if (instance && typeof instance.focusNationByPage === 'function') {
instance.focusNationByPage(tag, {
source: 'nation-panel',
label: item.getAttribute('data-nation-label') || '',
duration: immediate ? 360 : null
});
}
});
}, delay);
}
function markNationContinentSwitching(panel) {
var detail;
if (!panel) return;
panel.CLBI_NationContinentSwitchingUntil = Date.now() + 240;
panel.classList.add('is-continent-switching');
/*
* Continent switch relief signal — 20260708.
*
* The visible tab/body swap is tiny, but the globe render loop, pointer
* hit-test, and decorative maintenance can compete for the same frame.
* Broadcast a short, explicit relief window so NationsGlobe can skip
* non-essential resize/raycast work while the panel is repainting. This
* does not remove or reload globe assets; it only avoids doing expensive
* housekeeping in the exact input frame where the user expects the tab to
* respond instantly.
*/
detail = { panel: panel, until: panel.CLBI_NationContinentSwitchingUntil, duration: 240 };
try {
window.dispatchEvent(new CustomEvent('clbi:nations-continent-switching', { detail: detail }));
} catch (err) {}
window.setTimeout(function () {
if (!panel || Date.now() < (panel.CLBI_NationContinentSwitchingUntil || 0)) return;
panel.classList.remove('is-continent-switching');
}, 260);
}
function isNationContinentSwitching(panel) {
return !!(panel && Date.now() < (panel.CLBI_NationContinentSwitchingUntil || 0));
}
var lastContinentPointerX = null;
var lastContinentPointerY = null;
var continentHoverGuardReady = false;
function rememberContinentPointer(event) {
if (!event) return;
lastContinentPointerX = event.clientX;
lastContinentPointerY = event.clientY;
}
function getContinentTabUnderPointer(panel) {
var element;
var tab = null;
var hovered;
if (!panel || !panel.querySelectorAll) return null;
/*
* Keyboard/shortcut continent switches do not move the physical mouse.
* Browsers therefore keep the old CSS :hover target alive even after JS
* changes the active tab. We treat the tab under the last pointer
* coordinate as a stale hover candidate and suppress only that visual
* state until the pointer actually moves away or onto another tab.
*/
if (lastContinentPointerX !== null && lastContinentPointerY !== null && typeof document.elementFromPoint === 'function') {
element = document.elementFromPoint(lastContinentPointerX, lastContinentPointerY);
tab = element && element.closest ? element.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;
if (tab && panel.contains(tab)) return tab;
}
try {
hovered = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]:hover'));
if (hovered.length) return hovered[hovered.length - 1];
} catch (err) {}
return null;
}
function clearContinentKeyboardStaleHoverMarkers(panel) {
if (!panel || !panel.querySelectorAll) return;
toArray(panel.querySelectorAll('.clbi-nations-tabpanel-tab.is-keyboard-stale-hover')).forEach(function (tab) {
tab.classList.remove('is-keyboard-stale-hover');
tab.removeAttribute('data-clbi-keyboard-stale-hover');
});
}
function clearContinentKeyboardHoverSuppress(panel) {
if (!panel) return;
panel.classList.remove('is-keyboard-switching');
panel.removeAttribute('data-clbi-hover-suppress-continent');
panel.removeAttribute('data-clbi-hover-suppress-source');
clearContinentKeyboardStaleHoverMarkers(panel);
}
function syncContinentKeyboardHoverSuppress(panel, activeTab, force) {
var staleTab;
var focusedTab;
var staleContinent;
var active = activeTab;
if (!panel) return;
if (!active && panel.querySelector) {
active = panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent], .clbi-nations-tabpanel-tab[aria-selected="true"][data-continent]');
}
staleTab = getContinentTabUnderPointer(panel);
if (staleTab && active && staleTab === active) {
clearContinentKeyboardHoverSuppress(panel);
return;
}
if (staleTab && (staleTab.classList.contains('is-active') || staleTab.getAttribute('aria-selected') === 'true')) {
clearContinentKeyboardHoverSuppress(panel);
return;
}
focusedTab = document.activeElement && document.activeElement.closest ?
document.activeElement.closest('.clbi-nations-tabpanel-tab[data-continent]') : null;
if (focusedTab && panel.contains(focusedTab) && active && focusedTab !== active) {
/*
* A mouse click can leave keyboard focus on the old tab. If a later
* Ctrl+Q/Ctrl+E switch moves the active state elsewhere, that stale
* :focus rule looks exactly like a stuck hover. Move focus to the
* newly active tab only when focus is already inside this continent
* tab strip; normal document focus outside the strip is not touched.
*/
try { active.focus({ preventScroll: true }); } catch (err) { try { active.focus(); } catch (ignore) {} }
if (!staleTab) staleTab = focusedTab;
}
if (!staleTab) {
if (!force) clearContinentKeyboardHoverSuppress(panel);
return;
}
staleContinent = staleTab.getAttribute('data-continent') || '';
/*
* Stale hover/focus marker — 20260708.
*
* The panel-level is-keyboard-switching class is still useful, but it is
* not specific enough after warm-cache/SAP prepaint fixes because click
* and keyboard paths can now share the same live delegate. Mark the
* exact tab that remained under the physical mouse or retained focus.
* CSS can then neutralize only that stale visual surface while the real
* active tab stays highlighted. Pointer movement, pointerleave, click,
* and blur all clear this marker through clearContinentKeyboardHoverSuppress().
*/
clearContinentKeyboardStaleHoverMarkers(panel);
staleTab.classList.add('is-keyboard-stale-hover');
staleTab.setAttribute('data-clbi-keyboard-stale-hover', '1');
panel.classList.add('is-keyboard-switching');
panel.setAttribute('data-clbi-hover-suppress-source', 'keyboard-continent-switch');
if (staleContinent) panel.setAttribute('data-clbi-hover-suppress-continent', staleContinent);
else panel.removeAttribute('data-clbi-hover-suppress-continent');
}
function validateContinentKeyboardHoverSuppressions() {
toArray(document.querySelectorAll('.clbi-nations-tabpanel.is-keyboard-switching')).forEach(function (panel) {
var suppressed = panel.getAttribute('data-clbi-hover-suppress-continent') || '';
var hovered = getContinentTabUnderPointer(panel);
var hoveredContinent = hovered ? (hovered.getAttribute('data-continent') || '') : '';
var hoveredActive = hovered && (hovered.classList.contains('is-active') || hovered.getAttribute('aria-selected') === 'true');
if (!hovered || hoveredActive || (suppressed && hoveredContinent !== suppressed)) {
clearContinentKeyboardHoverSuppress(panel);
} else if (hovered && suppressed && hoveredContinent === suppressed) {
clearContinentKeyboardStaleHoverMarkers(panel);
hovered.classList.add('is-keyboard-stale-hover');
hovered.setAttribute('data-clbi-keyboard-stale-hover', '1');
}
});
}
function ensureContinentKeyboardHoverGuard() {
if (continentHoverGuardReady) return;
continentHoverGuardReady = true;
document.addEventListener('pointermove', function (event) {
rememberContinentPointer(event);
validateContinentKeyboardHoverSuppressions();
}, true);
document.addEventListener('pointerleave', function () {
lastContinentPointerX = null;
lastContinentPointerY = null;
validateContinentKeyboardHoverSuppressions();
}, true);
window.addEventListener('blur', function () {
lastContinentPointerX = null;
lastContinentPointerY = null;
validateContinentKeyboardHoverSuppressions();
});
}
function clearNationItemPanelHoverOnly(panel, item) {
var tag;
if (!panel || !item) return;
tag = String(item.getAttribute('data-nation-tag') || '').trim();
item.classList.remove('is-panel-hovered');
if (panel.CLBI_NationHoverFocusPendingTag === tag) cancelNationPanelPendingHoverFocus(panel);
}
function clearNationItemGlobeFocus(panel, item) {
var tag;
var instance;
var timer;
if (!panel || !item) return;
if (isNationContinentSwitching(panel)) {
clearNationItemPanelHoverOnly(panel, item);
return;
}
tag = String(item.getAttribute('data-nation-tag') || '').trim();
item.classList.remove('is-panel-hovered');
timer = panel.CLBI_NationHoverFocusTimer || null;
if (panel.CLBI_NationHoverFocusPendingTag === tag ||
(!panel.CLBI_NationHoverFocusPendingTag && (timer || panel.CLBI_NationHoverFocusRaf))) {
cancelNationPanelPendingHoverFocus(panel);
}
if (!tag) return;
instance = getGlobeInstanceForNationItem(item);
if (instance && typeof instance.clearPanelNationFocus === 'function') {
instance.clearPanelNationFocus(tag);
}
}
function bindNationListHoverFocus(panel) {
if (!panel || panel.CLBI_NationPanelHoverFocusBound) return;
panel.CLBI_NationPanelHoverFocusBound = true;
panel.setAttribute('data-nation-panel-hover-focus-ready', '1');
panel.addEventListener('mouseover', function (event) {
var item = getNationItemFromEventTarget(event.target, panel);
if (!item) return;
if (event.relatedTarget && item.contains(event.relatedTarget)) return;
requestNationItemGlobeFocus(panel, item, false);
});
panel.addEventListener('mouseout', function (event) {
var item = getNationItemFromEventTarget(event.target, panel);
if (!item) return;
if (event.relatedTarget && item.contains(event.relatedTarget)) return;
clearNationItemGlobeFocus(panel, item);
});
panel.addEventListener('focusin', function (event) {
var item = getNationItemFromEventTarget(event.target, panel);
if (item) requestNationItemGlobeFocus(panel, item, true);
});
panel.addEventListener('focusout', function (event) {
var item = getNationItemFromEventTarget(event.target, panel);
if (item) clearNationItemGlobeFocus(panel, item);
});
}
function buildNationTabSkeletonHtml(extraClass) {
var tabs = NATION_CONTINENT_FALLBACK.map(function (continent, index) {
var active = index === 0;
return '<span class="clbi-nations-tabpanel-tab' + (active ? ' is-active' : '') + '" role="tab" tabindex="' + (active ? '0' : '-1') + '" aria-selected="' + (active ? 'true' : 'false') + '" data-continent="' + escapeHtml(continent.key) + '">' + escapeHtml(continent.label) + '</span>';
}).join('');
var bodies = NATION_CONTINENT_FALLBACK.map(function (continent, index) {
var rows = continent.regions.map(function (region) {
return '<div class="clbi-nations-tabpanel-row">' +
'<div class="clbi-nations-tabpanel-region">' + escapeHtml(region) + '</div>' +
'<div class="clbi-nations-tabpanel-well">' + renderNationEmpty() + '</div>' +
'</div>';
}).join('');
return '<div class="clbi-nations-tabpanel-continent' + (index === 0 ? ' is-active' : '') + '" data-continent-panel="' + escapeHtml(continent.key) + '" aria-hidden="' + (index === 0 ? 'false' : 'true') + '">' + rows + '</div>';
}).join('');
return '<div class="clbi-nations-tabpanel' + (extraClass ? ' ' + extraClass : '') + '" data-clbi-nations-simple data-nation-list-source="1">' +
'<div class="clbi-nations-tabpanel-tabs" role="tablist" aria-label="대륙 선택">' +
tabs +
'<span class="clbi-nations-tabpanel-title" aria-hidden="true">당대 존재 국가</span>' +
'</div>' +
'<div class="clbi-nations-tabpanel-body">' + bodies + '</div>' +
'</div>';
}
function buildHistoryPanelSkeletonHtml(era) {
var start = parseInt(era || '1950', 10);
var years = [];
if (!isFinite(start)) start = 1950;
for (var year = start; year < start + 10; year += 1) {
years.push(String(year));
}
var pages = years.map(function (year, index) {
return '<div class="clbi-nations-history-list clbi-nations-history-page' + (index === 0 ? ' is-active' : '') + '" data-year-panel="' + escapeHtml(year) + '"' + (index === 0 ? '' : ' hidden="hidden"') + '>' +
'<div class="clbi-nations-history-empty"><span class="clbi-nations-history-empty-ko">작업 중...</span><span class="clbi-nations-history-empty-en">Working in Progress...</span></div>' +
'</div>';
}).join('');
var tabs = years.map(function (year, index) {
return '<span class="clbi-nations-history-year-button' + (index === 0 ? ' is-active' : '') + '" role="tab" tabindex="' + (index === 0 ? '0' : '-1') + '" aria-selected="' + (index === 0 ? 'true' : 'false') + '" data-year="' + escapeHtml(year) + '">' + escapeHtml(year) + '</span>';
}).join('');
return '<div class="clbi-nations-forces-panel clbi-nations-module-panel clbi-nations-history-panel">' +
'<div class="clbi-nations-module-title">역사적 사건</div>' +
'<div class="clbi-nations-module-body clbi-nations-history-body">' +
'<div class="clbi-nations-history-pages">' + pages + '</div>' +
'<div class="clbi-nations-history-year-well"><div class="clbi-nations-history-year-list" data-decade-only="1" role="tablist" aria-label="역사적 사건 연도 선택">' + tabs + '</div></div>' +
'</div>' +
'</div>';
}
function buildEraContentSkeletonHtml(era) {
return '<div class="clbi-nations-era-content clbi-nations-era-wip" data-era-content="' + escapeHtml(era) + '" hidden="hidden">' +
buildHistoryPanelSkeletonHtml(era) +
buildNationTabSkeletonHtml('clbi-nations-era-wip-tabpanel') +
'</div>';
}
function ensureEraContentPanels(stack, eras) {
var insertAfter;
if (!stack || !eras || !eras.length) return;
insertAfter = toArray(stack.querySelectorAll('.clbi-nations-era-content[data-era-content]')).pop() || stack.querySelector('.clbi-nations-map-row');
eras.forEach(function (era) {
var selector = '.clbi-nations-era-content[data-era-content="' + String(era).replace(/"/g, '\\"') + '"]';
var wrapper;
var panel;
if (stack.querySelector(selector)) return;
wrapper = document.createElement('div');
wrapper.innerHTML = buildEraContentSkeletonHtml(era);
panel = wrapper.firstElementChild;
if (!panel) return;
if (insertAfter && insertAfter.parentNode) {
insertAfter.parentNode.insertBefore(panel, insertAfter.nextSibling);
} else {
stack.appendChild(panel);
}
insertAfter = panel;
});
}
function renderNationContinent(continent, activeKey) {
var rows = (continent.regions || []).map(function (region) {
var items = Array.isArray(region.items) ? region.items : [];
var content = items.length ? items.map(function (item, index) {
return renderNationItem(item, index, items);
}).join(' ') : renderNationEmpty();
return '' +
'<div class="clbi-nations-tabpanel-row">' +
'<div class="clbi-nations-tabpanel-region">' + escapeHtml(region.label || region.key || '') + '</div>' +
'<div class="clbi-nations-tabpanel-well">' + content + '</div>' +
'</div>';
}).join('');
return '<div class="clbi-nations-tabpanel-continent' + (continent.key === activeKey ? ' is-active' : '') + '" data-continent-panel="' + escapeHtml(continent.key) + '" aria-hidden="' + (continent.key === activeKey ? 'false' : 'true') + '">' + rows + '</div>';
}
function renderNationListPanel(panel, data) {
var body = panel.querySelector('.clbi-nations-tabpanel-body');
var activeTab = panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent], .clbi-nations-tabpanel-tab[aria-selected="true"][data-continent]');
var activeKey = activeTab ? activeTab.getAttribute('data-continent') : '';
var continents = data && Array.isArray(data.continents) ? data.continents : [];
var stats = countNationListItems(data);
if (!body || !continents.length) return;
if (!activeKey) activeKey = continents[0].key;
return PanelPerf.measure('render nation list panel', {
era: String(data.year || getPanelEra(panel) || ''),
continents: stats.continents,
regions: stats.regions,
items: stats.items
}, function () {
var html;
PanelPerf.measure('render nation list panel: html build+insert', {
era: String(data.year || getPanelEra(panel) || ''),
continents: stats.continents,
items: stats.items
}, function () {
html = continents.map(function (continent) {
return renderNationContinent(continent, activeKey);
}).join('');
body.innerHTML = html;
});
PanelPerf.measure('render nation list panel: bind flag fallbacks', {}, function () {
bindNationFlagFallbacks(body);
});
PanelPerf.measure('render nation list panel: bind hover focus', {}, function () {
bindNationListHoverFocus(panel);
});
PanelPerf.measure('render nation list panel: prepare continent panels', {}, function () {
preparePersistentContinentPanels(panel);
});
panel.classList.add('clbi-nations-list-json-ready');
panel.setAttribute('data-nation-list-year-loaded', String(data.year || getPanelEra(panel)));
PanelPerf.measure('render nation list panel: schedule flag prewarm', {}, function () {
prewarmRenderedNationFlags(body);
});
PanelPerf.measure('render nation list panel: row snap schedule', {}, function () {
panel.NationListRowSnapSettled = false;
bindNationListRowSnapResize();
scheduleNationListRowSnap(panel, { force: true, reason: 'render' });
});
});
}
function seedResolvedFlagUrlsFromEntryStore(data) {
var files;
if (!window.EntryStore || typeof window.EntryStore.getFlagUrl !== 'function') return;
files = collectNationFlagFileNames(data);
files.forEach(function (file) {
var key = normalizeNationFileKey(file);
var url = window.EntryStore.getFlagUrl(file);
if (key && url) nationResolvedFileUrlCache[key] = url;
});
}
function getPreparedNationDataForPanel(panel, era) {
var listUrl;
var linkUrl;
var listPayload;
var linkPayload;
var linkIndex;
var data;
if (!window.EntryStore || typeof window.EntryStore.getJsonSync !== 'function') return null;
era = String(era || getPanelEra(panel) || '1950');
listUrl = getNationListUrl(panel, era);
linkUrl = getNationLinkMapUrl(panel, era);
listPayload = getEntryStoreJson(listUrl) || getEntryStoreJson('MediaWiki:' + era + '_Nation_List.json');
if (!listPayload) return null;
linkPayload = getEntryStoreJson(linkUrl) || getEntryStoreJson('MediaWiki:' + era + '_Nation_Link_Map.json');
linkIndex = normalizeNationLinkMap(linkPayload);
data = applyNationLinkMapFlagFallback(normalizeNationList(listPayload, era), linkIndex);
seedResolvedFlagUrlsFromEntryStore(data);
return data;
}
function prepareContentForEntry(root, options) {
return PanelPerf.measure('prepare content for entry', {}, function () {
var scope = root && root.querySelectorAll ? root : document;
var panels = toArray(scope.querySelectorAll('.clbi-nations-tabpanel[data-nation-list-source="1"]'));
var rendered = 0;
var skipped = 0;
panels.forEach(function (panel) {
var era = getPanelEra(panel);
var data = getPreparedNationDataForPanel(panel, era);
if (!data) {
skipped += 1;
PanelPerf.mark('prepare content panel skipped', { era: era, reason: 'no prepared data' });
return;
}
renderNationListPanel(panel, data);
rendered += 1;
});
PanelPerf.mark('prepare content for entry result', { panels: panels.length, rendered: rendered, skipped: skipped });
return { panels: panels.length, rendered: rendered, skipped: skipped };
});
}
function isNationListPanelHidden(panel) {
var node = panel;
while (node && node !== document && node.nodeType === 1) {
if (node.hasAttribute('hidden') || node.getAttribute('aria-hidden') === 'true') return true;
node = node.parentElement;
}
return false;
}
function initNationListPanel(panel) {
var era;
var promise;
var loadId;
if (!panel || panel.getAttribute('data-nation-list-source') !== '1') return Promise.resolve(null);
if (isNationListPanelHidden(panel)) {
PanelPerf.mark('init nation list panel skipped', { reason: 'hidden' });
return Promise.resolve(null);
}
if (panel.getAttribute('data-nation-list-year-loaded') === getPanelEra(panel)) {
PanelPerf.mark('init nation list panel skipped', { era: getPanelEra(panel), reason: 'already loaded' });
return Promise.resolve(panel);
}
if (panel.getAttribute('data-nation-list-loading') === '1' && panel.CLBI_NationListLoadPromise) {
PanelPerf.mark('init nation list panel existing promise', { era: getPanelEra(panel) });
return panel.CLBI_NationListLoadPromise;
}
era = getPanelEra(panel);
loadId = PanelPerf.start('init nation list panel', { era: era });
purgeLegacyNationIndexFileImages(panel);
panel.setAttribute('data-nation-list-loading', '1');
promise = Promise.all([
fetchNationList(panel, era),
fetchNationLinkMap(panel, era)
]).then(function (results) {
var data = PanelPerf.measure('apply nation link map flag fallback', { era: era }, function () {
return applyNationLinkMapFlagFallback(results[0], results[1]);
});
var initialFiles = collectInitialNationFlagFileNames(data, panel);
var allFiles = collectNationFlagFileNames(data);
PanelPerf.mark('initial nation flag URL set', {
era: era,
count: initialFiles.length,
activeContinent: getInitialContinentKey(panel, data)
});
PanelPerf.mark('all continent nation flag URL set', {
era: era,
count: allFiles.length,
mode: 'pre-render-all-tabs'
});
return resolveNationFlagFileUrls(allFiles).then(function () {
panel.CLBI_NationsAllFlagUrlsResolved = true;
renderNationListPanel(panel, data);
panel.CLBI_NationsAllContinentPanelsPrebuilt = true;
scheduleAllNationFlagBackgroundWarm(panel, data);
return panel;
});
}).then(function (result) {
PanelPerf.end(loadId, { ok: true });
return result;
}).catch(function () {
panel.classList.add('clbi-nations-list-json-fallback');
PanelPerf.end(loadId, { ok: false, fallback: true });
return panel;
}).finally(function () {
panel.removeAttribute('data-nation-list-loading');
panel.CLBI_NationListLoadPromise = null;
});
panel.CLBI_NationListLoadPromise = promise;
return promise;
}
function waitUntilNationsPanel(predicate, timeoutMs, intervalMs) {
var started = Date.now();
timeoutMs = Math.max(500, timeoutMs || 12000);
intervalMs = Math.max(20, intervalMs || 80);
return new Promise(function (resolve) {
function tick() {
var result = false;
try { result = predicate(); } catch (err) { result = false; }
if (result) return resolve({ ok: true, value: result });
if (Date.now() - started >= timeoutMs) return resolve({ ok: false, timeout: true });
setTimeout(tick, intervalMs);
}
tick();
});
}
function findEraNationPanels(era) {
var selector;
era = String(era || '').trim();
if (!era) return [];
selector = '.clbi-nations-era-content[data-era-content="' + era.replace(/"/g, '\\"') + '"] .clbi-nations-tabpanel[data-nation-list-source="1"]';
return toArray(document.querySelectorAll(selector));
}
function whenEraReady(era, options) {
var panels;
var timeoutMs = options && options.timeoutMs ? options.timeoutMs : 15000;
era = String(era || '1950');
panels = findEraNationPanels(era);
if (!panels.length) {
panels = toArray(document.querySelectorAll('.clbi-nations-tabpanel[data-nation-list-source="1"]')).filter(function (panel) {
return getPanelEra(panel) === era;
});
}
if (!panels.length) {
PanelPerf.mark('when era ready skipped', { era: era, reason: 'no panels' });
return Promise.resolve({ era: era, skipped: true });
}
return PanelPerf.measure('when era ready', { era: era, panels: panels.length, timeoutMs: timeoutMs }, function () {
return PanelPerf.measure('when era ready: init panels', { era: era, panels: panels.length }, function () {
return Promise.all(panels.map(function (panel) {
return initNationListPanel(panel);
}));
}).then(function () {
return PanelPerf.measure('when era ready: wait json-ready marker', { era: era, panels: panels.length }, function () {
return waitUntilNationsPanel(function () {
return panels.every(function (panel) {
return panel && panel.classList.contains('clbi-nations-list-json-ready') && panel.getAttribute('data-nation-list-year-loaded') === era;
});
}, timeoutMs, 80);
});
}).then(function (result) {
return PanelPerf.measure('when era ready: wait rendered flags', { era: era, panels: panels.length }, function () {
return Promise.all(panels.map(function (panel) {
return waitForRenderedFlagImages(panel, { timeoutMs: timeoutMs, activeOnly: true }).catch(function () {
return { total: 0, ok: 0, fail: 0 };
});
}));
}).then(function (flagResults) {
var total = 0;
var ok = 0;
var fail = 0;
flagResults.forEach(function (item) {
total += item && item.total ? item.total : 0;
ok += item && item.ok ? item.ok : 0;
fail += item && item.fail ? item.fail : 0;
});
return {
era: era,
ok: !!(result && result.ok),
panels: panels.length,
timeout: !!(result && result.timeout),
flags: { total: total, ok: ok, fail: fail }
};
});
});
});
}
function preparePersistentContinentPanels(panel) {
var activePanel;
if (!panel || !panel.querySelectorAll) return;
panel.classList.add('clbi-nations-continent-cache');
toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]')).forEach(function (body) {
if (body.hasAttribute('hidden')) body.removeAttribute('hidden');
body.setAttribute('data-clbi-panel-warm', '1');
if (!body.hasAttribute('aria-hidden')) {
body.setAttribute('aria-hidden', body.classList.contains('is-active') ? 'false' : 'true');
}
});
activePanel = panel.querySelector('.clbi-nations-tabpanel-continent.is-active[data-continent-panel]') ||
panel.querySelector('.clbi-nations-tabpanel-continent[data-continent-panel]');
if (activePanel) {
activatePersistentPanelsByAttribute(
toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]')),
'data-continent-panel',
activePanel.getAttribute('data-continent-panel')
);
}
stabilizeContinentBodyHeight(panel, true);
}
function syncDecorationRuntimeNow() {
var runtime = window.Decorations || window.CLBI_DECORATIONS || null;
try {
if (runtime && typeof runtime.updateVisibility === 'function') runtime.updateVisibility(document);
else if (runtime && typeof runtime.sync === 'function') runtime.sync();
else if (runtime && typeof runtime.renderPrepared === 'function') runtime.renderPrepared();
else if (runtime && typeof runtime.render === 'function' && runtime.lastRegistry) runtime.render(runtime.lastRegistry);
} catch (err) {}
}
function scheduleDecorationVisibilitySync(panel) {
if (!panel) return;
/*
* Continent switch fast path — 20260708.
*
* Changing a continent tab must feel like a local state flip, not a
* mini page load. Decoration visibility is not required to decide the
* active tab/body, and running the decoration runtime in the same input
* frame competes with the globe animation and panel repaint. Keep the
* visible switch synchronous and move decoration maintenance to the next
* animation frame. This preserves the prepared-canvas behavior while
* avoiding the old "tab click pauses the globe" feeling.
*/
if (panel.CLBI_NationDecorationSyncRaf) {
window.cancelAnimationFrame(panel.CLBI_NationDecorationSyncRaf);
panel.CLBI_NationDecorationSyncRaf = null;
}
panel.CLBI_NationDecorationSyncRaf = window.requestAnimationFrame(function () {
panel.CLBI_NationDecorationSyncRaf = null;
if (!document.body.contains(panel)) return;
syncDecorationRuntimeNow();
});
}
function scheduleHoverFocusMaintenance(panel) {
if (!panel) return;
/*
* Continent switch fast path — 20260708.
*
* Pending hover-to-globe focus timers are cancelled immediately so an old
* country cannot request a new zoom after the user changes continents.
* The visual cleanup scan is deferred one frame: it is housekeeping, and
* doing it in the click/shortcut frame used to make the globe stutter.
*/
cancelNationPanelPendingHoverFocus(panel);
if (panel.CLBI_NationHoverCleanupRaf) {
window.cancelAnimationFrame(panel.CLBI_NationHoverCleanupRaf);
panel.CLBI_NationHoverCleanupRaf = null;
}
panel.CLBI_NationHoverCleanupRaf = window.requestAnimationFrame(function () {
panel.CLBI_NationHoverCleanupRaf = null;
if (!document.body.contains(panel)) return;
clearNationPanelHoverState(panel, null);
});
}
function stabilizeContinentBodyHeight(panel, force) {
var body;
var bodies;
var max = 0;
if (!panel || !panel.querySelectorAll) return;
if (!force && panel.CLBI_NationContinentHeightStable) return;
body = panel.querySelector('.clbi-nations-tabpanel-body');
bodies = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]'));
if (!body || !bodies.length) return;
/*
* Height stabilization — 20260708.
*
* The six continent bodies are already kept warm in DOM. If the stack
* height follows each active body, every continent switch can still cause
* a layout jump and steal a frame from the WebGL globe. Measure the warm
* bodies once and pin the body well to the tallest continent. The number
* of bodies is tiny, so this one-time measurement is cheaper than paying
* repeated layout work on every switch.
*/
bodies.forEach(function (item) {
var h;
if (!item) return;
h = Math.max(item.scrollHeight || 0, item.offsetHeight || 0);
if (h > max) max = h;
});
if (max > 0) {
body.style.setProperty('--clbi-nations-continent-body-min-h', Math.ceil(max) + 'px');
body.setAttribute('data-clbi-continent-height-stable', '1');
panel.CLBI_NationContinentHeightStable = true;
}
}
function getContinentPanelParts(panel) {
if (!panel || !panel.querySelectorAll) return { tabs: [], bodies: [] };
/*
* Mouse-click continent regression guard — 20260708.
*
* The nations panel is prebuilt once while detached, then consumed again
* after SPA insertion. Older fixes correctly avoided serialized
* attributes as binding guards, but the cached tab/body node arrays could
* still point at nodes that were replaced or detached during warm entry
* hydration. Keyboard shortcuts often survived because a later fallback
* queried the live DOM, while mouse clicks could be swallowed by the
* owner API returning true after updating stale cached nodes.
*
* Keep this function live-DOM based. The tab count is tiny, so a fresh
* query is cheaper and safer than carrying stale references across
* detached preparation, innerHTML insertion, and manifest warm-cache
* hydration. This preserves the old solved behavior: mouse clicks,
* keyboard shortcuts, and fallback activation all operate on the same
* currently visible DOM nodes.
*/
panel.CLBI_NationContinentTabs = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
panel.CLBI_NationContinentBodies = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]'));
return {
tabs: panel.CLBI_NationContinentTabs || [],
bodies: panel.CLBI_NationContinentBodies || []
};
}
function findActiveContinentPanel() {
var panels = toArray(document.querySelectorAll('.clbi-nations-tabpanel'));
var visible = panels.filter(function (panel) {
return !isNationListPanelHidden(panel) && panel.querySelector('.clbi-nations-tabpanel-tab[data-continent]');
});
return visible.find(function (panel) { return panel.getAttribute('data-nation-list-source') === '1'; }) || visible[0] || null;
}
function activateContinentOnPanel(panel, continent, options) {
var parts;
var tabs;
var bodies;
var activeTab = null;
var currentTab = null;
var source = options && options.source ? String(options.source) : '';
var keyboardSwitch = source === 'shortcut' || source === 'keyboard' || /keyboard|shortcut/i.test(source);
if (!panel || !continent) return false;
if (source && source !== 'init') markNationPanelDirectInteraction();
parts = interactionPerfMeasure('panel switch query live tabs', { continent: continent, source: source }, function () {
return getContinentPanelParts(panel);
});
tabs = parts.tabs;
bodies = parts.bodies;
if (!tabs.length || !bodies.length) return false;
tabs.forEach(function (tab) {
if (tab.getAttribute('data-continent') === continent) activeTab = tab;
if (tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true') currentTab = tab;
});
if (!activeTab && tabs.length) {
activeTab = tabs[0];
continent = activeTab.getAttribute('data-continent');
}
if (!activeTab) return false;
panel.CLBI_NationsPanelOwned = true;
if (currentTab === activeTab && panel.getAttribute('data-active-continent') === continent) {
if (keyboardSwitch) syncContinentKeyboardHoverSuppress(panel, activeTab, true);
else clearContinentKeyboardHoverSuppress(panel);
scheduleNationListRowSnap(panel);
scheduleDecorationVisibilitySync(panel);
return true;
}
/*
* Flag continuity on continent switches — 20260710.
*
* The remaining flag resolver runs after the first visible continent. When a
* user switches tabs after those URLs are ready, the target continent must not
* become visible first and then receive its <img> nodes a frame later; that
* reads as every tab loading its flags from zero. Hydrate the target hidden
* body before flipping the active class, then keep the old post-switch pass
* only as a fallback for unresolved/late slots.
*/
if (!panel.CLBI_NationsAllFlagUrlsResolved) {
interactionPerfMeasure('panel switch prehydrate target flags', { continent: continent, source: source }, function () {
bodies.forEach(function (body) {
if (!body || body.getAttribute('data-continent-panel') !== continent) return;
hydrateResolvedFlagSlots(body);
prewarmRenderedNationFlags(body);
});
});
}
markNationContinentSwitching(panel);
interactionPerfMeasure('panel switch flip active classes', { continent: continent, source: source, tabs: tabs.length, bodies: bodies.length }, function () {
setActiveButton(tabs, activeTab);
activatePersistentPanelsByAttribute(bodies, 'data-continent-panel', continent);
});
if (!panel.CLBI_NationsAllFlagUrlsResolved && panel.CLBI_NationsRemainingFlagsResolved) {
scheduleResolvedFlagHydrationSweeps(panel, {
era: getPanelEra(panel),
delays: [80, 220]
});
}
panel.setAttribute('data-active-continent', continent);
if (!panel.CLBI_NationsAllContinentPanelsPrebuilt) {
scheduleNationListRowSnap(panel);
}
if (!panel.CLBI_NationContinentHeightStable) {
/* Height measurement can force layout. Do it outside the click frame. */
window.requestAnimationFrame(function () {
if (document.body.contains(panel)) stabilizeContinentBodyHeight(panel, false);
});
}
if (keyboardSwitch) syncContinentKeyboardHoverSuppress(panel, activeTab, true);
else clearContinentKeyboardHoverSuppress(panel);
/*
* The actual continent switch ends here. Everything below is maintenance
* and is intentionally deferred so mouse clicks and shortcuts only pay
* for the tiny tab/body class flip in the input frame.
*/
scheduleHoverFocusMaintenance(panel);
scheduleDecorationVisibilitySync(panel);
interactionPerfMark('panel continent switch complete', { continent: continent, source: source });
return true;
}
function activateContinent(continent, options) {
var panel = options && options.panel ? options.panel : findActiveContinentPanel();
return activateContinentOnPanel(panel, continent, options || {});
}
function moveContinent(direction, options) {
var panel = options && options.panel ? options.panel : findActiveContinentPanel();
var parts;
var tabs;
var activeIndex;
var nextIndex;
direction = direction < 0 ? -1 : 1;
if (!panel) return false;
parts = getContinentPanelParts(panel);
tabs = parts.tabs;
if (!tabs.length) return false;
activeIndex = tabs.findIndex(function (tab) {
return tab.classList.contains('is-active') || tab.getAttribute('aria-selected') === 'true';
});
if (activeIndex < 0) activeIndex = 0;
nextIndex = (activeIndex + direction + tabs.length) % tabs.length;
return activateContinentOnPanel(panel, tabs[nextIndex].getAttribute('data-continent'), options || {});
}
function initContinentPanel(panel) {
var tabs;
var bodies;
if (!panel) return;
if (isNationListPanelHidden(panel)) return;
bindNationListHoverFocus(panel);
ensureContinentKeyboardHoverGuard();
/*
* Detached SPA preparation serializes attributes but not event listeners.
* A panel can therefore arrive with data-nations-tabpanel-ready="1" while
* its click handlers were lost during innerHTML insertion. Use a DOM
* property as the real binding guard and keep the attribute only as a
* diagnostic marker. This fixes mouse continent tabs after warm prepaint.
*/
if (panel.CLBI_NationsTabPanelBound) return;
panel.CLBI_NationsTabPanelBound = true;
panel.setAttribute('data-nations-tabpanel-ready', '1');
panel.addEventListener('pointerdown', function () {
clearContinentKeyboardHoverSuppress(panel);
}, true);
panel.addEventListener('pointerleave', function () {
clearContinentKeyboardHoverSuppress(panel);
}, true);
preparePersistentContinentPanels(panel);
panel.CLBI_NationsPanelOwned = true;
panel.CLBI_NationContinentTabs = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-tab[data-continent]'));
panel.CLBI_NationContinentBodies = toArray(panel.querySelectorAll('.clbi-nations-tabpanel-continent[data-continent-panel]'));
stabilizeContinentBodyHeight(panel, true);
tabs = panel.CLBI_NationContinentTabs;
bodies = panel.CLBI_NationContinentBodies;
function activate(continent, options) {
return activateContinentOnPanel(panel, continent, options || {});
}
tabs.forEach(function (tab) {
tab.setAttribute('role', 'tab');
tab.setAttribute('tabindex', tab.classList.contains('is-active') ? '0' : '-1');
tab.addEventListener('click', function (event) {
if (event) {
event.preventDefault();
if (typeof event.stopPropagation === 'function') event.stopPropagation();
if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
}
interactionPerfMeasure('panel tab click handler', { continent: tab.getAttribute('data-continent') || '' }, function () {
playNationsPanelClickSound();
activate(tab.getAttribute('data-continent'), { source: 'click' });
});
});
tab.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
interactionPerfMeasure('panel tab keyboard handler', { continent: tab.getAttribute('data-continent') || '', key: event.key || '' }, function () {
playNationsPanelClickSound();
activate(tab.getAttribute('data-continent'), { source: 'keyboard' });
});
}
});
});
activate((panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent]') || tabs[0] || {}).getAttribute && (panel.querySelector('.clbi-nations-tabpanel-tab.is-active[data-continent]') || tabs[0]).getAttribute('data-continent'), { source: 'init' });
}
function initHistoryYearPanel(panel) {
var tabs;
var pages;
if (!panel || panel.getAttribute('data-nations-history-ready') === '1') return;
panel.setAttribute('data-nations-history-ready', '1');
tabs = toArray(panel.querySelectorAll('.clbi-nations-history-year-button[data-year]'));
pages = toArray(panel.querySelectorAll('.clbi-nations-history-page[data-year-panel]'));
function activate(year) {
var activeTab = null;
tabs.forEach(function (tab) {
if (tab.getAttribute('data-year') === year) activeTab = tab;
});
if (!activeTab && tabs.length) {
activeTab = tabs[0];
year = activeTab.getAttribute('data-year');
}
setActiveButton(tabs, activeTab);
activatePanelsByAttribute(pages, 'data-year-panel', year);
scheduleHistoryItemSlides(panel);
}
tabs.forEach(function (tab) {
tab.setAttribute('role', 'tab');
tab.setAttribute('tabindex', tab.classList.contains('is-active') ? '0' : '-1');
tab.addEventListener('click', function () {
playNationsPanelClickSound();
activate(tab.getAttribute('data-year'));
});
tab.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
playNationsPanelClickSound();
activate(tab.getAttribute('data-year'));
}
});
});
activate((panel.querySelector('.clbi-nations-history-year-button.is-active[data-year]') || tabs[0] || {}).getAttribute && (panel.querySelector('.clbi-nations-history-year-button.is-active[data-year]') || tabs[0]).getAttribute('data-year'));
}
function prewarmNationEra(stack, era) {
var eraPanel;
var panel;
var selector;
if (!stack || !era) return;
selector = '.clbi-nations-era-content[data-era-content="' + String(era).replace(/"/g, '\"') + '"]';
eraPanel = stack.querySelector(selector);
panel = eraPanel && eraPanel.querySelector ? eraPanel.querySelector('.clbi-nations-tabpanel[data-nation-list-source="1"]') : null;
if (!panel || panel.getAttribute('data-nation-prewarm-loading') === '1') return;
if (panel.getAttribute('data-nation-prewarm-year-loaded') === String(era)) return;
panel.setAttribute('data-nation-prewarm-loading', '1');
Promise.all([
fetchNationList(panel, era),
fetchNationLinkMap(panel, era)
]).then(function () {
panel.setAttribute('data-nation-prewarm-year-loaded', String(era));
}).catch(function () {
}).finally(function () {
panel.removeAttribute('data-nation-prewarm-loading');
});
}
function scheduleEraNeighborsPrewarm(stack, eras, era) {
var index;
var prevEra;
var nextEra;
var oldHandle;
if (!stack || !eras || !eras.length || !era) return;
oldHandle = stack.CLBI_NationsEraPrewarmHandle;
if (oldHandle != null) cancelIdle(oldHandle);
index = getEraIndex(eras, era);
prevEra = index > 0 ? eras[index - 1] : '';
nextEra = index < eras.length - 1 ? eras[index + 1] : '';
stack.CLBI_NationsEraPrewarmHandle = scheduleIdle(function () {
stack.CLBI_NationsEraPrewarmHandle = null;
prewarmNationEra(stack, nextEra);
prewarmNationEra(stack, prevEra);
}, { timeout: 900 });
}
function initEraStack(stack) {
var eras;
var titleNode;
var titlePlates;
var contentPanels;
var leftButton;
var rightButton;
var globe;
var currentEra;
if (!stack || stack.getAttribute('data-nations-era-controller-ready') === '1') return;
stack.setAttribute('data-nations-era-controller-ready', '1');
eras = parseEraList(stack);
ensureEraContentPanels(stack, eras);
titleNode = stack.querySelector('[data-nations-era-title]');
titlePlates = toArray(stack.querySelectorAll('.clbi-nations-era-title-plate[data-era]'));
contentPanels = toArray(stack.querySelectorAll('.clbi-nations-era-content[data-era-content]'));
leftButton = stack.querySelector('[data-nations-era-move="prev"], .clbi-nations-era-side-button-left');
rightButton = stack.querySelector('[data-nations-era-move="next"], .clbi-nations-era-side-button-right');
globe = stack.querySelector('.clbi-nations-globe-window');
currentEra = stack.getAttribute('data-nations-current-era') || stack.getAttribute('data-era') || eras[0];
function normalizeEra(era) {
var index = getEraIndex(eras, String(era || eras[0]));
return eras[index];
}
function setEraShell(era, preview) {
var index;
var prevEra;
var nextEra;
era = normalizeEra(era);
index = getEraIndex(eras, era);
prevEra = getWrappedEra(eras, index - 1);
nextEra = getWrappedEra(eras, index + 1);
stack.setAttribute('data-era', era);
if (preview) {
stack.setAttribute('data-nations-preview-era', era);
stack.classList.add('is-era-previewing');
} else {
stack.removeAttribute('data-nations-preview-era');
stack.classList.remove('is-era-previewing');
}
if (titleNode) {
titleNode.textContent = era + '년';
titleNode.setAttribute('data-era', era);
}
titlePlates.forEach(function (plate) {
setHidden(plate, plate.getAttribute('data-era') !== era);
});
if (leftButton) {
leftButton.setAttribute('data-target-era', prevEra);
leftButton.setAttribute('title', prevEra + '년으로 이동');
leftButton.setAttribute('aria-label', prevEra + '년으로 이동');
}
if (rightButton) {
rightButton.setAttribute('data-target-era', nextEra);
rightButton.setAttribute('title', nextEra + '년으로 이동');
rightButton.setAttribute('aria-label', nextEra + '년으로 이동');
}
return era;
}
function clearEraCommitTimer() {
if (stack.CLBI_NationsEraCommitTimer) {
window.clearTimeout(stack.CLBI_NationsEraCommitTimer);
stack.CLBI_NationsEraCommitTimer = null;
}
}
function setEra(era, options) {
var reload;
options = options || {};
era = setEraShell(era, false);
reload = !!options.reloadGlobe;
clearEraCommitTimer();
currentEra = era;
stack.setAttribute('data-nations-current-era', era);
contentPanels.forEach(function (panel) {
var inactive = panel.getAttribute('data-era-content') !== era;
setHidden(panel, inactive);
if (!inactive) {
toArray(panel.querySelectorAll('.clbi-nations-tabpanel')).forEach(initContinentPanel);
toArray(panel.querySelectorAll('.clbi-nations-tabpanel')).forEach(initNationListPanel);
toArray(panel.querySelectorAll('.clbi-nations-history-panel')).forEach(initHistoryEventsPanel);
toArray(panel.querySelectorAll('.clbi-nations-history-panel')).forEach(initHistoryYearPanel);
}
});
applyGlobeEra(globe, era, reload);
scheduleEraNeighborsPrewarm(stack, eras, era);
}
function commitEraPreview() {
var target = stack.getAttribute('data-nations-preview-era') || '';
clearEraCommitTimer();
if (!target) return false;
setEra(target, { reloadGlobe: true });
return true;
}
function scheduleEraCommit() {
clearEraCommitTimer();
stack.CLBI_NationsEraCommitTimer = window.setTimeout(function () {
stack.CLBI_NationsEraCommitTimer = null;
commitEraPreview();
}, 135);
}
function previewMove(direction) {
var active = stack.getAttribute('data-nations-preview-era') || stack.getAttribute('data-nations-current-era') || currentEra || eras[0];
var index = getEraIndex(eras, active);
var target = getWrappedEra(eras, index + direction);
if (!target) return false;
playNationsPanelClickSound();
setEraShell(target, true);
scheduleEraCommit();
return true;
}
function move(direction) {
var active = stack.getAttribute('data-nations-current-era') || currentEra || eras[0];
var index = getEraIndex(eras, active);
var target = getWrappedEra(eras, index + direction);
if (!target) return false;
playNationsPanelClickSound();
setEra(target, { reloadGlobe: true });
return true;
}
if (leftButton) {
leftButton.addEventListener('click', function (event) {
event.preventDefault();
move(-1);
});
}
if (rightButton) {
rightButton.addEventListener('click', function (event) {
event.preventDefault();
move(1);
});
}
stack.CLBI_NationsEraController = {
eras: eras.slice(),
setEra: function (era) { setEra(era, { reloadGlobe: true }); },
previewMove: previewMove,
commitPreview: commitEraPreview,
move: move
};
setEra(currentEra, { reloadGlobe: false });
}
function init(root) {
var scope = root && root.querySelectorAll ? root : document;
purgeLegacyNationIndexFileImages(scope);
toArray(scope.querySelectorAll('.clbi-nations-panel-stack')).forEach(initEraStack);
toArray(scope.querySelectorAll('.clbi-nations-tabpanel')).forEach(initContinentPanel);
toArray(scope.querySelectorAll('.clbi-nations-history-panel')).forEach(initHistoryEventsPanel);
toArray(scope.querySelectorAll('.clbi-nations-history-panel')).forEach(initHistoryYearPanel);
toArray(scope.querySelectorAll('.clbi-nations-tabpanel')).forEach(initNationListPanel);
scheduleHistoryItemSlides(scope);
bindHistoryItemSlideResize();
}
function findEraStack() {
return document.querySelector('.clbi-nations-panel-stack');
}
function moveEra(direction) {
var stack = findEraStack();
if (!stack || !stack.CLBI_NationsEraController) return false;
return stack.CLBI_NationsEraController.move(direction);
}
function previewMoveEra(direction) {
var stack = findEraStack();
if (!stack || !stack.CLBI_NationsEraController || typeof stack.CLBI_NationsEraController.previewMove !== 'function') return false;
return stack.CLBI_NationsEraController.previewMove(direction);
}
function commitEraPreview() {
var stack = findEraStack();
if (!stack || !stack.CLBI_NationsEraController || typeof stack.CLBI_NationsEraController.commitPreview !== 'function') return false;
return stack.CLBI_NationsEraController.commitPreview();
}
function setEra(era) {
var stack = findEraStack();
if (!stack || !stack.CLBI_NationsEraController) return false;
stack.CLBI_NationsEraController.setEra(era);
return true;
}
function refresh(root) {
var scope = root && root.querySelectorAll ? root : document;
nationRuntimeCacheBust = 'revision-ledger-refresh';
nationListCache = {};
nationLinkMapCache = {};
historyEventsCache = {};
toArray(scope.querySelectorAll('.clbi-nations-tabpanel[data-nation-list-source="1"]')).forEach(function (panel) {
panel.removeAttribute('data-nation-list-loading');
panel.removeAttribute('data-nation-list-year-loaded');
panel.classList.remove('clbi-nations-list-json-ready', 'clbi-nations-list-json-fallback');
});
toArray(scope.querySelectorAll('.clbi-nations-history-panel')).forEach(function (panel) {
panel.removeAttribute('data-history-events-loading');
panel.removeAttribute('data-history-events-year-loaded');
panel.removeAttribute('data-nations-history-ready');
panel.classList.remove('clbi-nations-history-json-ready', 'clbi-nations-history-json-fallback');
});
init(scope);
}
function diagnostics() {
var flags = toArray(document.querySelectorAll('.clbi-nations-list-flag-slot')).map(function (slot) {
var img = slot.querySelector('img');
return {
file: slot.getAttribute('data-flag-file') || '',
src: img ? img.getAttribute('src') : '',
missing: slot.classList.contains('is-missing')
};
});
var legacyImages = toArray(document.images).filter(function (img) {
return isLegacyNationIndexFileUrl(img.currentSrc || img.getAttribute('src') || '');
}).map(function (img) {
return img.currentSrc || img.getAttribute('src') || '';
});
var perfLegacy = (window.performance && performance.getEntriesByType ? performance.getEntriesByType('resource') : []).filter(function (entry) {
return isLegacyNationIndexFileUrl(entry && entry.name);
}).map(function (entry) { return entry.name; });
return {
build: NATIONS_PANEL_BUILD,
resolvedFileCount: Object.keys(nationResolvedFileUrlCache || {}).length,
unresolvedFiles: Object.keys(nationResolvedFileUrlCache || {}).filter(function (key) {
return nationResolvedFileUrlCache[key] === '';
}),
renderedFlagCount: flags.length,
ownedContinentPanels: toArray(document.querySelectorAll('.clbi-nations-tabpanel')).filter(function (panel) { return !!panel.CLBI_NationsPanelOwned; }).length,
warmContinentBodies: toArray(document.querySelectorAll('.clbi-nations-tabpanel-continent[data-clbi-panel-warm="1"]')).length,
missingRenderedFlags: flags.filter(function (flag) { return flag.missing || !flag.src; }),
mediaWikiThumbRenderedFlags: flags.filter(function (flag) { return /\/images\/thumb\//i.test(flag.src || ''); }).length,
legacyImageCount: legacyImages.length,
legacyImages: legacyImages.slice(0, 40),
performanceLegacyRequests: perfLegacy.slice(-40),
sampleFlags: flags.slice(0, 20),
keyboardHoverSuppressedPanels: toArray(document.querySelectorAll('.clbi-nations-tabpanel.is-keyboard-switching')).length,
keyboardStaleHoverTabs: toArray(document.querySelectorAll('.clbi-nations-tabpanel-tab.is-keyboard-stale-hover')).length
};
}
window.NationsPanel = {
build: NATIONS_PANEL_BUILD,
init: init,
refresh: refresh,
moveEra: moveEra,
previewMoveEra: previewMoveEra,
commitEraPreview: commitEraPreview,
setEra: setEra,
activateContinent: activateContinent,
moveContinent: moveContinent,
ownsContinentTabs: true,
whenEraReady: whenEraReady,
cancelPanelHoverFocus: clearNationPanelHoverFocus,
diagnostics: diagnostics,
prepareContentForEntry: prepareContentForEntry,
perf: PanelPerf
};
window.CLBI_NATIONS_PANEL = window.NationsPanel;
if (window.mw && mw.hook) {
mw.hook('wikipage.content').add(function ($content) {
init($content && $content[0] ? $content[0] : document);
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function () { init(document); });
} else {
init(document);
}
}());