From fd126a8a7f6a1d2d9c2e8a557411dc7ccf57034d Mon Sep 17 00:00:00 2001 From: barrulus Date: Tue, 9 Jun 2026 22:05:51 +0100 Subject: [PATCH] fix(states-editor): null-safe DOM ops so state merge survives missing COA/army elements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The merge dialog crashed on maps whose states have no rendered COA/army DOM nodes (e.g. custom emblems that failed to render). ensureEl() returns null + logs when an element is missing, so ensureEl(id).remove()/.appendChild() threw: - stateCOA removal (TypeError 'remove' of null) aborted the whole merge loop mid-iteration, leaving a half-merged pack — the user-visible breakage. - hover highlight read .attr('d') on an empty selection (getAttribute of null). - 'armyN not found' / 'provinceCOAN not found' console spam. Switch these to document.getElementById(...)?.remove()/optional-chaining, matching the safe-removal convention used elsewhere. No behaviour change on maps where the elements exist. Bump states-editor cache-bust token to 1.122.14. None of these lines were part of the merge-to-provinces feature; they are pre-existing base-merge bugs surfaced by this map. --- public/modules/dynamic/editors/states-editor.js | 11 ++++++----- public/modules/ui/editors.js | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/public/modules/dynamic/editors/states-editor.js b/public/modules/dynamic/editors/states-editor.js index 57cd5ecc7..dbd645c55 100644 --- a/public/modules/dynamic/editors/states-editor.js +++ b/public/modules/dynamic/editors/states-editor.js @@ -1391,7 +1391,8 @@ function openStateMergeDialog() { if (!layerIsOn("toggleStates")) return; const state = +event.currentTarget.dataset.id; if (!state) return; - const d = regions.select("#state" + state).attr("d"); + const stateNode = regions.select("#state" + state).node(); + const d = stateNode && stateNode.getAttribute("d"); if (!d) return; stateHighlightOff(); @@ -1467,7 +1468,7 @@ function openStateMergeDialog() { function mergeStates(statesToMerge, rulingStateId, mergeToProvinces = false) { const rulingState = pack.states[rulingStateId]; - const rulingStateArmy = ensureEl("army" + rulingStateId); + const rulingStateArmy = document.getElementById("army" + rulingStateId); // demote a merged state into a single province of the ruling state, collapsing // any of its existing internal provinces into that one new province @@ -1479,7 +1480,7 @@ function openStateMergeDialog() { pack.provinces.forEach(province => { if (!province.i || province.removed || province.state !== stateId) return; const coaId = "provinceCOA" + province.i; - if (ensureEl(coaId)) ensureEl(coaId).remove(); + document.getElementById(coaId)?.remove(); emblems.select(`#provinceEmblems > use[data-i='${province.i}']`).remove(); pack.provinces[province.i] = {i: province.i, removed: true}; }); @@ -1516,7 +1517,7 @@ function openStateMergeDialog() { labels.select("#stateLabel" + stateId).remove(); defs.select("#textPath_stateLabel" + stateId).remove(); - ensureEl("stateCOA" + stateId).remove(); + document.getElementById("stateCOA" + stateId)?.remove(); emblems.select(`#stateEmblems > use[data-i='${stateId}']`).remove(); // add merged state regiments to the ruling state @@ -1534,7 +1535,7 @@ function openStateMergeDialog() { element.id = newId; element.dataset.state = rulingStateId; element.dataset.id = newIndex; - rulingStateArmy.appendChild(element); + rulingStateArmy?.appendChild(element); } }); diff --git a/public/modules/ui/editors.js b/public/modules/ui/editors.js index 5748f02f4..ba3593cc0 100644 --- a/public/modules/ui/editors.js +++ b/public/modules/ui/editors.js @@ -1079,7 +1079,7 @@ function refreshAllEditors() { // dynamically loaded editors async function editStates() { if (customization) return; - const Editor = await import("../dynamic/editors/states-editor.js?v=1.122.13"); + const Editor = await import("../dynamic/editors/states-editor.js?v=1.122.14"); Editor.open(); }