diff --git a/public/modules/dynamic/editors/cultures-editor.js b/public/modules/dynamic/editors/cultures-editor.js index 5c2b9b5b4..3ceeec7c4 100644 --- a/public/modules/dynamic/editors/cultures-editor.js +++ b/public/modules/dynamic/editors/cultures-editor.js @@ -4,6 +4,18 @@ let culturesManualHistory = []; const cultureTypes = ["Generic", "River", "Lake", "Naval", "Nomadic", "Hunting", "Highland"]; +const culturesPage = {page: 1}; +const CULTURES_SORT_ACCESSORS = { + name: c => c.name, + type: c => c.type || "", + base: c => c.base, + cells: c => c.cells, + expansionism: c => c.expansionism || 0, + area: c => c.area, + population: c => c.rural * populationRate + c.urban * populationRate * urbanization, + emblems: c => c.shield +}; + export function open() { closeDialogs("#culturesEditor, .stable"); if (!layerIsOn("toggleCultures")) toggleCultures(); @@ -12,6 +24,7 @@ export function open() { if (layerIsOn("toggleReligions")) toggleReligions(); if (layerIsOn("toggleProvinces")) toggleProvinces(); + culturesPage.page = 1; refreshCulturesEditor(); $("#culturesEditor").dialog({ @@ -77,6 +90,10 @@ function insertEditorHtml() { function addListeners() { applySortingByHeader("culturesHeader"); + bindEditorSortReset(ensureEl("culturesHeader"), () => { + culturesPage.page = 1; + culturesEditorAddLines(); + }); ensureEl("culturesEditorRefresh").on("click", refreshCulturesEditor); ensureEl("culturesEditStyle").on("click", () => editStyle("cults")); @@ -120,15 +137,25 @@ function culturesCollectStatistics() { function culturesEditorAddLines() { const unit = getAreaUnit(); - let lines = ""; - let totalArea = 0; - let totalPopulation = 0; const emblemShapeGroup = ensureEl("emblemShape").selectedOptions[0]?.parentNode?.label; const selectShape = emblemShapeGroup === "Diversiform"; - for (const c of pack.cultures) { - if (c.removed) continue; + const allCultures = pack.cultures.filter(c => !c.removed); + sortDataByActiveHeader(ensureEl("culturesHeader"), allCultures, CULTURES_SORT_ACCESSORS); + + // footer totals over the full set + let totalArea = 0; + let totalPopulation = 0; + for (const c of allCultures) { + totalArea += getArea(c.area); + totalPopulation += rn(c.rural * populationRate + c.urban * populationRate * urbanization); + } + + const pageInfo = getEditorPage(allCultures, culturesPage); + let lines = ""; + + for (const c of pageInfo.items) { const area = getArea(c.area); const rural = c.rural * populationRate; const urban = c.urban * populationRate * urbanization; @@ -136,8 +163,6 @@ function culturesEditorAddLines() { const populationTip = `Total population: ${si(population)}. Rural population: ${si(rural)}. Urban population: ${si( urban )}. Click to edit`; - totalArea += area; - totalPopulation += population; if (!c.i) { // Uncultured (neutral) line @@ -231,6 +256,11 @@ function culturesEditorAddLines() { ensureEl("culturesFooterArea").dataset.area = totalArea; ensureEl("culturesFooterPopulation").dataset.population = totalPopulation; + renderEditorPagination(ensureEl("culturesFooter"), pageInfo, page => { + culturesPage.page = page; + culturesEditorAddLines(); + }); + // add listeners $body.querySelectorAll(":scope > div").forEach($line => { $line.on("mouseenter", cultureHighlightOn); @@ -258,7 +288,6 @@ function culturesEditorAddLines() { $body.dataset.type = "absolute"; togglePercentageMode(); } - applySorting($culturesHeader); $("#culturesEditor").dialog({ width: fitContent() }); } @@ -578,12 +607,12 @@ function drawCultureCenters() { .attr("cy", d => pack.cells.p[d.center][1]) .on("mouseenter", d => { tip(tooltip, true); - $body.querySelector(`div[data-id='${d.i}']`).classList.add("selected"); + $body.querySelector(`div[data-id='${d.i}']`)?.classList.add("selected"); cultureHighlightOn(event); }) .on("mouseleave", d => { tip("", true); - $body.querySelector(`div[data-id='${d.i}']`).classList.remove("selected"); + $body.querySelector(`div[data-id='${d.i}']`)?.classList.remove("selected"); cultureHighlightOff(event); }) .call(d3.drag().on("start", cultureCenterDrag)); @@ -682,6 +711,8 @@ function recalculateCultures(force) { function enterCultureManualAssignent() { if (!layerIsOn("toggleCultures")) toggleCultures(); customization = 4; + culturesPage.page = 1; + culturesEditorAddLines(); cults.append("g").attr("id", "temp"); document.querySelectorAll("#culturesBottom > *").forEach(el => (el.style.display = "none")); ensureEl("culturesManuallyButtons").style.display = "inline-block"; @@ -718,8 +749,10 @@ function selectCultureOnMapClick() { const assigned = cults.select("#temp").select("polygon[data-cell='" + i + "']"); const culture = assigned.size() ? +assigned.attr("data-culture") : pack.cells.culture[i]; - $body.querySelector("div.selected").classList.remove("selected"); - $body.querySelector("div[data-id='" + culture + "']").classList.add("selected"); + const $row = $body.querySelector("div[data-id='" + culture + "']"); + if (!$row) return; // clicked culture's row is on another page; ignore to avoid a crash + $body.querySelector("div.selected")?.classList.remove("selected"); + $row.classList.add("selected"); } function dragCultureBrush() { @@ -794,7 +827,7 @@ function exitCulturesManualAssignment(close) { ensureEl("culturesManuallyButtons").style.display = "none"; culturesEditor.querySelectorAll(".hide").forEach(el => el.classList.remove("hidden")); - culturesFooter.style.display = "block"; + culturesFooter.style.display = "flex"; $body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all")); if (!close) $("#culturesEditor").dialog({ position: { my: "right top", at: "right-10 top+10", of: "svg" } }); @@ -858,15 +891,16 @@ function addCulture() { function downloadCulturesCsv() { const unit = getAreaUnit("2"); const headers = `Id,Name,Color,Cells,Expansionism,Type,Area ${unit},Population,Namesbase,Emblems Shape,Origins`; - const lines = Array.from($body.querySelectorAll(":scope > div")); - const data = lines.map($line => { - const { id, name, color, cells, expansionism, type, area, population, emblems, base } = $line.dataset; - const namesbase = nameBases[+base].name; - const { origins } = pack.cultures[+id]; - const originList = origins.filter(origin => origin).map(origin => pack.cultures[origin].name); - const originText = '"' + originList.join(", ") + '"'; - return [id, name, color, cells, expansionism, type, area, population, namesbase, emblems, originText].join(","); - }); + const data = pack.cultures + .filter(c => !c.removed) + .map(c => { + const area = getArea(c.area); + const population = rn(c.rural * populationRate + c.urban * populationRate * urbanization); + const namesbase = nameBases[c.base].name; + const originList = (c.origins || []).filter(origin => origin).map(origin => pack.cultures[origin].name); + const originText = '"' + originList.join(", ") + '"'; + return [c.i, c.name, c.color || "", c.cells, c.i ? (c.expansionism ?? "") : "", c.type || "", area, population, namesbase, c.shield, originText].join(","); + }); const csvData = [headers].concat(data).join("\n"); const name = getFileName("Cultures") + ".csv"; diff --git a/public/modules/dynamic/editors/religions-editor.js b/public/modules/dynamic/editors/religions-editor.js index b3d151635..543dc6ec2 100644 --- a/public/modules/dynamic/editors/religions-editor.js +++ b/public/modules/dynamic/editors/religions-editor.js @@ -1,6 +1,18 @@ const $body = insertEditorHtml(); addListeners(); +const religionsPage = {page: 1}; +const RELIGIONS_SORT_ACCESSORS = { + name: r => r.name, + type: r => r.type || "", + form: r => r.form || "", + deity: r => r.deity || "", + area: r => r.area, + population: r => r.rural * populationRate + r.urban * populationRate * urbanization, + expansion: r => r.expansion || "", + expansionism: r => r.expansionism || 0 +}; + export function open() { closeDialogs("#religionsEditor, .stable"); if (!layerIsOn("toggleReligions")) toggleReligions(); @@ -9,6 +21,7 @@ export function open() { if (layerIsOn("toggleCultures")) toggleCultures(); if (layerIsOn("toggleProvinces")) toggleProvinces(); + religionsPage.page = 1; refreshReligionsEditor(); drawReligionCenters(); @@ -92,6 +105,10 @@ function insertEditorHtml() { function addListeners() { applySortingByHeader("religionsHeader"); + bindEditorSortReset(ensureEl("religionsHeader"), () => { + religionsPage.page = 1; + religionsEditorAddLines(); + }); ensureEl("religionsEditorRefresh").on("click", refreshReligionsEditor); ensureEl("religionsEditStyle").on("click", () => editStyle("relig")); @@ -129,17 +146,27 @@ function religionsCollectStatistics() { } } -// add line for each religion +// add line for each religion (current page only; sort + totals span all religions) function religionsEditorAddLines() { const unit = " " + getAreaUnit(); - let lines = ""; + + const allReligions = pack.religions.filter( + r => !r.removed && !(r.i && !r.cells && $body.dataset.extinct !== "show") + ); + sortDataByActiveHeader(ensureEl("religionsHeader"), allReligions, RELIGIONS_SORT_ACCESSORS); + + // footer totals over the full (visible) set let totalArea = 0; let totalPopulation = 0; + for (const r of allReligions) { + totalArea += getArea(r.area); + totalPopulation += rn(r.rural * populationRate + r.urban * populationRate * urbanization); + } - for (const r of pack.religions) { - if (r.removed) continue; - if (r.i && !r.cells && $body.dataset.extinct !== "show") continue; // hide extinct religions + const pageInfo = getEditorPage(allReligions, religionsPage); + let lines = ""; + for (const r of pageInfo.items) { const area = getArea(r.area); const rural = r.rural * populationRate; const urban = r.urban * populationRate * urbanization; @@ -147,8 +174,6 @@ function religionsEditorAddLines() { const populationTip = `Believers: ${si(population)}; Rural areas: ${si(rural)}; Urban areas: ${si( urban )}. Click to change`; - totalArea += area; - totalPopulation += population; if (!r.i) { // No religion (neutral) line @@ -233,6 +258,11 @@ function religionsEditorAddLines() { ensureEl("religionsFooterArea").dataset.area = totalArea; ensureEl("religionsFooterPopulation").dataset.population = totalPopulation; + renderEditorPagination(ensureEl("religionsFooter"), pageInfo, page => { + religionsPage.page = page; + religionsEditorAddLines(); + }); + // add listeners $body.querySelectorAll(":scope > div").forEach($line => { $line.on("mouseenter", religionHighlightOn); @@ -258,7 +288,6 @@ function religionsEditorAddLines() { togglePercentageMode(); } - applySorting(religionsHeader); $("#religionsEditor").dialog({ width: fitContent() }); } @@ -640,6 +669,7 @@ async function showHierarchy() { } function toggleExtinct() { + religionsPage.page = 1; $body.dataset.extinct = $body.dataset.extinct !== "show" ? "show" : "hide"; religionsEditorAddLines(); drawReligionCenters(); @@ -648,6 +678,8 @@ function toggleExtinct() { function enterReligionsManualAssignent() { if (!layerIsOn("toggleReligions")) toggleReligions(); customization = 7; + religionsPage.page = 1; + religionsEditorAddLines(); relig.append("g").attr("id", "temp"); document.querySelectorAll("#religionsBottom > *").forEach(el => (el.style.display = "none")); ensureEl("religionsManuallyButtons").style.display = "inline-block"; @@ -683,8 +715,10 @@ function selectReligionOnMapClick() { const assigned = relig.select("#temp").select("polygon[data-cell='" + i + "']"); const religion = assigned.size() ? +assigned.attr("data-religion") : pack.cells.religion[i]; - $body.querySelector("div.selected").classList.remove("selected"); - $body.querySelector("div[data-id='" + religion + "']").classList.add("selected"); + const $row = $body.querySelector("div[data-id='" + religion + "']"); + if (!$row) return; // clicked religion's row is on another page; ignore to avoid a crash + $body.querySelector("div.selected")?.classList.remove("selected"); + $row.classList.add("selected"); } function dragReligionBrush() { @@ -760,7 +794,7 @@ function exitReligionsManualAssignment(close) { ensureEl("religionsEditor") .querySelectorAll(".hide") .forEach(el => el.classList.remove("hidden")); - ensureEl("religionsFooter").style.display = "block"; + ensureEl("religionsFooter").style.display = "flex"; $body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all")); if (!close) $("#religionsEditor").dialog({ position: { my: "right top", at: "right-10 top+10", of: "svg" } }); @@ -809,15 +843,28 @@ function addReligion() { function downloadReligionsCsv() { const unit = getAreaUnit("2"); const headers = `Id,Name,Color,Type,Form,Supreme Deity,Area ${unit},Believers,Origins,Potential,Expansionism`; - const lines = Array.from($body.querySelectorAll(":scope > div")); - const data = lines.map($line => { - const { id, name, color, type, form, deity, area, population, expansion, expansionism } = $line.dataset; - const deityText = '"' + deity + '"'; - const { origins } = pack.religions[+id]; - const originList = (origins || []).filter(origin => origin).map(origin => pack.religions[origin].name); - const originText = '"' + originList.join(", ") + '"'; - return [id, name, color, type, form, deityText, area, population, originText, expansion, expansionism].join(","); - }); + const data = pack.religions + .filter(r => !r.removed && !(r.i && !r.cells && $body.dataset.extinct !== "show")) + .map(r => { + const area = getArea(r.area); + const population = rn(r.rural * populationRate + r.urban * populationRate * urbanization); + const deityText = '"' + (r.deity || "") + '"'; + const originList = (r.origins || []).filter(origin => origin).map(origin => pack.religions[origin].name); + const originText = '"' + originList.join(", ") + '"'; + return [ + r.i, + r.name, + r.color || "", + r.type || "", + r.form || "", + deityText, + area, + population, + originText, + r.expansion || "", + r.i ? (r.expansionism ?? "") : "" + ].join(","); + }); const csvData = [headers].concat(data).join("\n"); const name = getFileName("Religions") + ".csv"; diff --git a/public/modules/dynamic/editors/states-editor.js b/public/modules/dynamic/editors/states-editor.js index 1433aac92..2548ebb1b 100644 --- a/public/modules/dynamic/editors/states-editor.js +++ b/public/modules/dynamic/editors/states-editor.js @@ -1,6 +1,19 @@ const $body = insertEditorHtml(); addListeners(); let statesManualHistory = []; +const statesPage = {page: 1}; +const STATES_SORT_ACCESSORS = { + name: s => s.name, + form: s => s.formName, + capital: s => (s.i ? pack.burgs[s.capital].name : ""), + culture: s => (s.i ? pack.cultures[s.culture].name : ""), + burgs: s => s.burgs, + area: s => s.area, + population: s => s.rural * populationRate + s.urban * populationRate * urbanization, + type: s => s.type || "", + expansionism: s => s.expansionism || 0, + cells: s => s.cells +}; export function open() { closeDialogs("#statesEditor, .stable"); @@ -10,6 +23,7 @@ export function open() { if (layerIsOn("toggleBiomes")) toggleBiomes(); if (layerIsOn("toggleReligions")) toggleReligions(); + statesPage.page = 1; refreshStatesEditor(); $("#statesEditor").dialog({ @@ -96,6 +110,10 @@ function insertEditorHtml() { function addListeners() { applySortingByHeader("statesHeader"); + bindEditorSortReset(ensureEl("statesHeader"), () => { + statesPage.page = 1; + statesEditorAddLines(); + }); ensureEl("statesEditorRefresh").on("click", refreshStatesEditor); ensureEl("statesEditStyle").on("click", () => editStyle("regions")); @@ -156,17 +174,28 @@ function refreshStatesEditor() { statesEditorAddLines(); } -// add line for each state +// add line for each state (current page only; sort + totals span all states) function statesEditorAddLines() { const unit = getAreaUnit(); const hidden = ensureEl("statesRegenerateButtons").style.display === "block" ? "" : "hidden"; // toggle regenerate columns - let lines = ""; + + const allStates = pack.states.filter(s => !s.removed); + sortDataByActiveHeader(ensureEl("statesHeader"), allStates, STATES_SORT_ACCESSORS); + + // footer totals over the full set let totalArea = 0; let totalPopulation = 0; let totalBurgs = 0; + for (const s of allStates) { + totalArea += getArea(s.area); + totalPopulation += rn(s.rural * populationRate + s.urban * populationRate * urbanization); + totalBurgs += s.burgs; + } - for (const s of pack.states) { - if (s.removed) continue; + const pageInfo = getEditorPage(allStates, statesPage); + let lines = ""; + + for (const s of pageInfo.items) { const area = getArea(s.area); const rural = s.rural * populationRate; const urban = s.urban * populationRate * urbanization; @@ -174,9 +203,6 @@ function statesEditorAddLines() { const populationTip = `Total population: ${si(population)}; Rural population: ${si(rural)}; Urban population: ${si( urban )}. Click to change`; - totalArea += area; - totalPopulation += population; - totalBurgs += s.burgs; const focused = defs.select("#fog #focusState" + s.i).size(); if (!s.i) { @@ -283,6 +309,11 @@ function statesEditorAddLines() { ensureEl("statesFooterPopulation").innerHTML = si(totalPopulation); ensureEl("statesFooterPopulation").dataset.population = totalPopulation; + renderEditorPagination(ensureEl("statesFooter"), pageInfo, page => { + statesPage.page = page; + statesEditorAddLines(); + }); + // add listeners $body.querySelectorAll(":scope > div").forEach($line => { $line.on("mouseenter", stateHighlightOn); @@ -294,7 +325,6 @@ function statesEditorAddLines() { $body.dataset.type = "absolute"; togglePercentageMode(); } - applySorting(statesHeader); $("#statesEditor").dialog({ width: fitContent() }); } @@ -873,7 +903,8 @@ function randomizeStatesExpansion() { if (!s.i || s.removed) return; const expansionism = rn(Math.random() * 4 + 1, 1); s.expansionism = expansionism; - $body.querySelector("div.states[data-id='" + s.i + "'] > input.statePower").value = expansionism; + const $power = $body.querySelector("div.states[data-id='" + s.i + "'] > input.statePower"); + if ($power) $power.value = expansionism; }); recalculateStates(true, true); } @@ -892,6 +923,8 @@ function exitRegenerationMenu() { function enterStatesManualAssignent() { if (!layerIsOn("toggleStates")) toggleStates(); customization = 2; + statesPage.page = 1; + statesEditorAddLines(); statesBody.append("g").attr("id", "temp"); document.querySelectorAll("#statesBottom > button").forEach(el => (el.style.display = "none")); ensureEl("statesManuallyButtons").style.display = "inline-block"; @@ -930,8 +963,10 @@ function selectStateOnMapClick() { const assigned = statesBody.select("#temp").select("polygon[data-cell='" + i + "']"); const state = assigned.size() ? +assigned.attr("data-state") : pack.cells.state[i]; - $body.querySelector("div.selected").classList.remove("selected"); - $body.querySelector("div[data-id='" + state + "']").classList.add("selected"); + const $row = $body.querySelector("div[data-id='" + state + "']"); + if (!$row) return; // clicked state's row is on another page; ignore to avoid a crash + $body.querySelector("div.selected")?.classList.remove("selected"); + $row.classList.add("selected"); } function dragStateBrush() { @@ -1171,7 +1206,7 @@ function exitStatesManualAssignment(close) { ensureEl("statesEditor") .querySelectorAll(".hide:not(.show)") .forEach(el => el.classList.remove("hidden")); - statesFooter.style.display = "block"; + statesFooter.style.display = "flex"; $body.querySelectorAll("div > input, select, span, svg").forEach(e => (e.style.pointerEvents = "all")); if (!close) $("#statesEditor").dialog({ position: { my: "right top", at: "right-10 top+10", of: "svg", collision: "fit" } }); @@ -1491,31 +1526,33 @@ function openStateMergeDialog() { function downloadStatesCsv() { const unit = getAreaUnit("2"); const headers = `Id,State,Full Name,Form,Color,Capital,Culture,Type,Expansionism,Cells,Burgs,Area ${unit},Total Population,Rural Population,Urban Population`; - const lines = Array.from($body.querySelectorAll(":scope > div")); - const data = lines.map($line => { - const { id, name, form, color, capital, culture, type, expansionism, cells, burgs, area, population } = - $line.dataset; - const { fullName = "", rural, urban } = pack.states[+id]; - const ruralPopulation = Math.round(rural * populationRate); - const urbanPopulation = Math.round(urban * populationRate * urbanization); - return [ - id, - name, - fullName, - form, - color, - capital, - culture, - type, - expansionism, - cells, - burgs, - area, - population, - ruralPopulation, - urbanPopulation - ].join(","); - }); + const data = pack.states + .filter(s => !s.removed) + .map(s => { + const area = getArea(s.area); + const ruralPopulation = Math.round(s.rural * populationRate); + const urbanPopulation = Math.round(s.urban * populationRate * urbanization); + const population = ruralPopulation + urbanPopulation; + const capital = s.i ? pack.burgs[s.capital].name : ""; + const culture = s.i ? pack.cultures[s.culture].name : ""; + return [ + s.i, + s.name, + s.fullName || "", + s.formName || "", + s.color || "", + capital, + culture, + s.type || "", + s.expansionism ?? "", + s.cells, + s.burgs, + area, + population, + ruralPopulation, + urbanPopulation + ].join(","); + }); const csvData = [headers].concat(data).join("\n"); const name = getFileName("States") + ".csv"; diff --git a/public/modules/ui/burgs-overview.js b/public/modules/ui/burgs-overview.js index ce922e22b..e78dd4ea4 100644 --- a/public/modules/ui/burgs-overview.js +++ b/public/modules/ui/burgs-overview.js @@ -1,4 +1,19 @@ "use strict"; + +const burgsPage = {page: 1}; +const BURGS_SORT_ACCESSORS = { + name: b => b.name || "", + province: b => { + const p = pack.cells.province[b.cell]; + return p ? pack.provinces[p]?.name || "" : ""; + }, + state: b => pack.states[b.state]?.name || "", + culture: b => pack.cultures[b.culture]?.name || "", + group: b => b.group || "", + population: b => b.population * populationRate * urbanization, + features: b => (b.capital && b.port ? "a-capital-port" : b.capital ? "c-capital" : b.port ? "p-port" : "z-burg") +}; + function overviewBurgs(settings = {stateId: null, cultureId: null}) { if (customization) return; closeDialogs("#burgsOverview, .stable"); @@ -8,6 +23,7 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { const body = ensureEl("burgsBody"); updateFilter(); updateLockAllIcon(); + burgsPage.page = 1; burgsOverviewAddLines(); $("#burgsOverview").dialog(); @@ -26,9 +42,9 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { ensureEl("burgsOverviewRefresh").addEventListener("click", refreshBurgsEditor); ensureEl("burgsGroupsEditorButton").addEventListener("click", editBurgGroups); ensureEl("burgsChart").addEventListener("click", showBurgsChart); - ensureEl("burgsFilterState").addEventListener("change", burgsOverviewAddLines); - ensureEl("burgsFilterCulture").addEventListener("change", burgsOverviewAddLines); - ensureEl("burgsSearch").addEventListener("input", burgsOverviewAddLines); + ensureEl("burgsFilterState").addEventListener("change", resetAndRender); + ensureEl("burgsFilterCulture").addEventListener("change", resetAndRender); + ensureEl("burgsSearch").addEventListener("input", resetAndRender); ensureEl("regenerateBurgNames").addEventListener("click", regenerateNames); ensureEl("addNewBurg").addEventListener("click", enterAddBurgMode); ensureEl("burgsExport").addEventListener("click", downloadBurgsData); @@ -39,8 +55,17 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { ensureEl("burgsLockAll").addEventListener("click", toggleLockAll); ensureEl("burgsRemoveAll").addEventListener("click", triggerAllBurgsRemove); + // re-render on column sort so sorting applies across all pages, not just the visible one + bindEditorSortReset(ensureEl("burgsHeader"), resetAndRender); + + function resetAndRender() { + burgsPage.page = 1; + burgsOverviewAddLines(); + } + function refreshBurgsEditor() { updateFilter(); + burgsPage.page = 1; burgsOverviewAddLines(); } @@ -62,14 +87,13 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { culturesSorted.forEach(c => cultureFilter.options.add(new Option(c.name, c.i, false, c.i == selectedCulture))); } - // add line for each burg - function burgsOverviewAddLines() { + // collect the burgs matching the current search + state/culture filters (all pages) + function getFilteredBurgs() { const searchText = ensureEl("burgsSearch").value.toLowerCase().trim(); const selectedStateId = +ensureEl("burgsFilterState").value; const selectedCultureId = +ensureEl("burgsFilterCulture").value; - const validBurgs = pack.burgs.filter(b => b.i && !b.removed); - let filtered = validBurgs; + let filtered = pack.burgs.filter(b => b.i && !b.removed); if (searchText) { // filter by search text @@ -90,14 +114,25 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { } if (selectedStateId !== -1) filtered = filtered.filter(b => b.state === selectedStateId); // filtered by state if (selectedCultureId !== -1) filtered = filtered.filter(b => b.culture === selectedCultureId); // filtered by culture + return filtered; + } + + // add line for each burg (current page only; sort, totals and footer span the full filtered set) + function burgsOverviewAddLines() { + const validCount = pack.burgs.filter(b => b.i && !b.removed).length; + const filtered = getFilteredBurgs(); + sortDataByActiveHeader(ensureEl("burgsHeader"), filtered, BURGS_SORT_ACCESSORS); + + let totalPopulation = 0; + for (const b of filtered) totalPopulation += b.population * populationRate * urbanization; + + const pageInfo = getEditorPage(filtered, burgsPage); body.innerHTML = ""; let lines = ""; - let totalPopulation = 0; - for (const b of filtered) { + for (const b of pageInfo.items) { const population = b.population * populationRate * urbanization; - totalPopulation += population; const features = b.capital && b.port ? "a-capital-port" : b.capital ? "c-capital" : b.port ? "p-port" : "z-burg"; const state = pack.states[b.state].name; const prov = pack.cells.province[b.cell]; @@ -140,10 +175,15 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { if (!filtered.length) body.innerHTML = /* html */ `
No burgs found
`; body.insertAdjacentHTML("beforeend", lines); - // update footer - burgsFooterBurgs.innerHTML = `${filtered.length} of ${validBurgs.length}`; + // update footer (counts/averages span the full filtered set, not just the page) + burgsFooterBurgs.innerHTML = `${filtered.length} of ${validCount}`; burgsFooterPopulation.innerHTML = filtered.length ? si(totalPopulation / filtered.length) : 0; + renderEditorPagination(ensureEl("burgsFooter"), pageInfo, page => { + burgsPage.page = page; + burgsOverviewAddLines(); + }); + // add listeners body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseenter", ev => burgHighlightOn(ev))); body.querySelectorAll("div.states").forEach(el => el.addEventListener("mouseleave", ev => burgHighlightOff(ev))); @@ -151,8 +191,6 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { body.querySelectorAll("div > span.locks").forEach(el => el.addEventListener("click", toggleBurgLockStatus)); body.querySelectorAll("div > span.icon-pencil").forEach(el => el.addEventListener("click", openBurgEditor)); body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.addEventListener("click", triggerBurgRemove)); - - applySorting(burgsHeader); } function getCultureOptions(culture) { @@ -220,17 +258,14 @@ function overviewBurgs(settings = {stateId: null, cultureId: null}) { } function regenerateNames() { - body.querySelectorAll(":scope > div").forEach(function (el) { - const burg = +el.dataset.id; - if (pack.burgs[burg].lock) return; - - const culture = pack.burgs[burg].culture; - const name = Names.getCulture(culture); - - el.querySelector(".burgName").value = name; - pack.burgs[burg].name = el.dataset.name = name; - burgLabels.select("[data-id='" + burg + "']").text(name); - }); + // regenerate across the full filtered set (all pages), not just the visible page + for (const b of getFilteredBurgs()) { + if (b.lock) continue; + const name = Names.getCulture(b.culture); + b.name = name; + burgLabels.select("[data-id='" + b.i + "']").text(name); + } + burgsOverviewAddLines(); } function enterAddBurgMode() { diff --git a/public/modules/ui/editors.js b/public/modules/ui/editors.js index eabf03dba..bdaf0c016 100644 --- a/public/modules/ui/editors.js +++ b/public/modules/ui/editors.js @@ -129,6 +129,91 @@ function applySorting(headers) { .forEach(line => list.appendChild(line)); } +// ---- Shared editor pagination (used by states, cultures, religions, rivers, routes) ---- +const EDITOR_PAGE_SIZE = 200; + +// Read the active sort column from a header element, or null if none is active. +// Mirrors how applySorting reads the icon-sort-* class set by sortLines. +function getActiveSort(headers) { + const header = headers.querySelector("div[class*='icon-sort']"); + if (!header) return null; + return { + sortby: header.dataset.sortby, + name: header.classList.contains("alphabetically"), + desc: header.className.includes("-down") ? -1 : 1 + }; +} + +// Sort `data` IN PLACE to match the header's active column. +// `accessors` maps a data-sortby key to a value getter, e.g. {name: s => s.name}. +function sortDataByActiveHeader(headers, data, accessors) { + const sort = getActiveSort(headers); + if (!sort) return data; + const get = accessors[sort.sortby]; + if (!get) return data; + return data.sort((a, b) => { + const av = get(a); + const bv = get(b); + if (sort.name) { + const as = String(av); + const bs = String(bv); + return (as > bs ? 1 : as < bs ? -1 : 0) * sort.desc; + } + return (av - bv) * sort.desc; + }); +} + +// Clamp the page and return the slice for the current page. +// `pageRef` is a mutable {page} holder so the clamped value persists across calls. +function getEditorPage(data, pageRef, size = EDITOR_PAGE_SIZE) { + const total = data.length; + const totalPages = Math.max(1, Math.ceil(total / size)); + pageRef.page = Math.min(Math.max(1, pageRef.page || 1), totalPages); + const start = (pageRef.page - 1) * size; + return {items: data.slice(start, start + size), page: pageRef.page, totalPages, total}; +} + +// Inject/refresh pagination controls inside `footerEl`. Calls onGoto(page) on navigation. +// Hidden when there is a single page. Rebuilding innerHTML drops stale listeners. +function renderEditorPagination(footerEl, info, onGoto) { + let nav = footerEl.querySelector(":scope > .editorPagination"); + if (!nav) { + // The editor dialogs are sized with fit-content (max-content of their children). If the footer + // were allowed to grow with the pager it would become the widest child for narrow tables + // (e.g. Rivers) and stretch the whole dialog, leaving an empty gutter beside the rows. + // width:0 + min-width:100% makes the footer fill the table-driven width WITHOUT contributing + // to it; flex-wrap lets the pager drop to its own line instead of widening the dialog. + footerEl.style.display = "flex"; + footerEl.style.flexWrap = "wrap"; + footerEl.style.alignItems = "center"; + footerEl.style.width = "0"; + footerEl.style.minWidth = "100%"; + nav = document.createElement("div"); + nav.className = "editorPagination"; + nav.style.cssText = "margin-left: auto; display: inline-flex; gap: 0.3em; align-items: center;"; + footerEl.appendChild(nav); + } + if (info.totalPages <= 1) { + nav.style.display = "none"; + nav.innerHTML = ""; + return; + } + nav.style.display = "inline-flex"; + nav.innerHTML = /* html */ ` + + Page  of ${info.totalPages} + `; + nav.querySelector(".editorPagePrev").on("click", () => onGoto(info.page - 1)); + nav.querySelector(".editorPageNext").on("click", () => onGoto(info.page + 1)); + nav.querySelector(".editorPageInput").on("change", e => onGoto(+e.target.value)); +} + +// Bind sort-header clicks to reset to page 1 and re-render. +// Register AFTER sortLines is bound so the icon-sort-* class is toggled first. +function bindEditorSortReset(headerEl, onSort) { + headerEl.querySelectorAll(".sortable").forEach(el => el.on("click", () => onSort())); +} + // draw legend box function drawLegend(name, data) { legend.selectAll("*").remove(); // fully redraw every time diff --git a/public/modules/ui/rivers-overview.js b/public/modules/ui/rivers-overview.js index 73e1a3288..1c76a4e81 100644 --- a/public/modules/ui/rivers-overview.js +++ b/public/modules/ui/rivers-overview.js @@ -1,11 +1,21 @@ "use strict"; +const riversPage = {page: 1}; +const RIVERS_SORT_ACCESSORS = { + name: r => r.name || "", + type: r => r.type || "", + discharge: r => r.discharge, + length: r => r.length, + width: r => r.width +}; + function overviewRivers() { if (customization) return; closeDialogs("#riversOverview, .stable"); if (!layerIsOn("toggleRivers")) toggleRivers(); const body = ensureEl("riversBody"); + riversPage.page = 1; riversOverviewAddLines(); $("#riversOverview").dialog(); @@ -26,7 +36,14 @@ function overviewRivers() { ensureEl("riversBasinHighlight").on("click", toggleBasinsHightlight); ensureEl("riversExport").on("click", downloadRiversData); ensureEl("riversRemoveAll").on("click", triggerAllRiversRemove); - ensureEl("riversSearch").on("input", riversOverviewAddLines); + ensureEl("riversSearch").on("input", () => { + riversPage.page = 1; + riversOverviewAddLines(); + }); + bindEditorSortReset(ensureEl("riversHeader"), () => { + riversPage.page = 1; + riversOverviewAddLines(); + }); // add line for each river function riversOverviewAddLines() { @@ -37,7 +54,7 @@ function overviewRivers() { // Precompute a lookup map from river id to river for efficient basin lookup const riversById = new Map(pack.rivers.map(river => [river.i, river])); - let filteredRivers = pack.rivers; + let filteredRivers = pack.rivers.slice(); // copy so cross-page sort never mutates pack.rivers order const searchText = ensureEl("riversSearch").value.toLowerCase().trim(); if (searchText) { filteredRivers = filteredRivers.filter(r => { @@ -49,7 +66,13 @@ function overviewRivers() { }); } - for (const r of filteredRivers) { + sortDataByActiveHeader(ensureEl("riversHeader"), filteredRivers, { + ...RIVERS_SORT_ACCESSORS, + basin: r => riversById.get(r.basin)?.name || "" + }); + const pageInfo = getEditorPage(filteredRivers, riversPage); + + for (const r of pageInfo.items) { const discharge = r.discharge + " m³/s"; const length = rn(r.length * distanceScale) + " " + unit; const width = rn(r.width * distanceScale, 3) + " " + unit; @@ -94,7 +117,10 @@ function overviewRivers() { body.querySelectorAll("div > span.icon-pencil").forEach(el => el.on("click", openRiverEditor)); body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", triggerRiverRemove)); - applySorting(riversHeader); + renderEditorPagination(ensureEl("riversFooter"), pageInfo, page => { + riversPage.page = page; + riversOverviewAddLines(); + }); } function riverHighlightOn(event) { @@ -154,12 +180,22 @@ function overviewRivers() { function downloadRiversData() { let data = "Id,River,Type,Discharge,Length,Width,Basin\n"; // headers - body.querySelectorAll(":scope > div").forEach(function (el) { - const d = el.dataset; - const discharge = d.discharge + " m³/s"; - const length = rn(d.length * distanceScale) + " " + distanceUnitInput.value; - const width = rn(d.width * distanceScale, 3) + " " + distanceUnitInput.value; - data += [d.id, d.name, d.type, discharge, length, width, d.basin].join(",") + "\n"; + const riversById = new Map(pack.rivers.map(river => [river.i, river])); + const searchText = ensureEl("riversSearch").value.toLowerCase().trim(); + const exported = pack.rivers.filter(r => { + if (!searchText) return true; + const name = (r.name || "").toLowerCase(); + const type = (r.type || "").toLowerCase(); + const basinName = (riversById.get(r.basin)?.name || "").toLowerCase(); + return name.includes(searchText) || type.includes(searchText) || basinName.includes(searchText); + }); + + exported.forEach(function (r) { + const discharge = r.discharge + " m³/s"; + const length = rn(r.length * distanceScale) + " " + distanceUnitInput.value; + const width = rn(r.width * distanceScale, 3) + " " + distanceUnitInput.value; + const basin = riversById.get(r.basin)?.name || ""; + data += [r.i, r.name, r.type, discharge, length, width, basin].join(",") + "\n"; }); const name = getFileName("Rivers") + ".csv"; diff --git a/public/modules/ui/routes-overview.js b/public/modules/ui/routes-overview.js index 808693664..649efe7a3 100644 --- a/public/modules/ui/routes-overview.js +++ b/public/modules/ui/routes-overview.js @@ -1,11 +1,19 @@ "use strict"; +const routesPage = {page: 1}; +const ROUTES_SORT_ACCESSORS = { + name: route => route.name || "", + group: route => route.group || "", + length: route => route.length +}; + function overviewRoutes() { if (customization) return; closeDialogs("#routesOverview, .stable"); if (!layerIsOn("toggleRoutes")) toggleRoutes(); const body = ensureEl("routesBody"); + routesPage.page = 1; routesOverviewAddLines(); $("#routesOverview").dialog(); @@ -25,14 +33,29 @@ function overviewRoutes() { ensureEl("routesExport").on("click", downloadRoutesData); ensureEl("routesLockAll").on("click", toggleLockAll); ensureEl("routesRemoveAll").on("click", triggerAllRoutesRemove); - ensureEl("routesSearch").on("input", routesOverviewAddLines); + ensureEl("routesSearch").on("input", () => { + routesPage.page = 1; + routesOverviewAddLines(); + }); + bindEditorSortReset(ensureEl("routesHeader"), () => { + routesPage.page = 1; + routesOverviewAddLines(); + }); // add line for each route function routesOverviewAddLines() { body.innerHTML = ""; let lines = ""; - let filteredRoutes = pack.routes; + let filteredRoutes = pack.routes.slice(); // copy so cross-page sort never mutates pack.routes order + + // route name/length are computed lazily; populate them for the whole set so search, + // sort, footer averages and CSV export are consistent across all pages, not just the visible one + for (const route of filteredRoutes) { + if (!route.points || route.points.length < 2) continue; + route.name = route.name || Routes.generateName(route); + route.length = route.length || Routes.getLength(route.i); + } const searchText = ensureEl("routesSearch").value.toLowerCase().trim(); if (searchText) { @@ -43,10 +66,11 @@ function overviewRoutes() { }); } - for (const route of filteredRoutes) { + sortDataByActiveHeader(ensureEl("routesHeader"), filteredRoutes, ROUTES_SORT_ACCESSORS); + const pageInfo = getEditorPage(filteredRoutes, routesPage); + + for (const route of pageInfo.items) { if (!route.points || route.points.length < 2) continue; - route.name = route.name || Routes.generateName(route); - route.length = route.length || Routes.getLength(route.i); const length = rn(route.length * distanceScale) + " " + distanceUnitInput.value; lines += /* html */ `
span.locks").forEach(el => el.on("click", toggleLockStatus)); body.querySelectorAll("div > span.icon-trash-empty").forEach(el => el.on("click", triggerRouteRemove)); - applySorting(routesHeader); + renderEditorPagination(ensureEl("routesFooter"), pageInfo, page => { + routesPage.page = page; + routesOverviewAddLines(); + }); } function routeHighlightOn(event) { @@ -113,10 +140,20 @@ function overviewRoutes() { function downloadRoutesData() { let data = "Id,Route,Group,Length\n"; // headers - body.querySelectorAll(":scope > div").forEach(function (el) { - const d = el.dataset; - const length = rn(d.length * distanceScale) + " " + distanceUnitInput.value; - data += [d.id, d.name, d.group, length].join(",") + "\n"; + const searchText = ensureEl("routesSearch").value.toLowerCase().trim(); + const exported = pack.routes.filter(route => { + if (!route.points || route.points.length < 2) return false; // skip degenerate routes (never rendered) + if (!searchText) return true; + const name = (route.name || "").toLowerCase(); + const group = (route.group || "").toLowerCase(); + return name.includes(searchText) || group.includes(searchText); + }); + + exported.forEach(function (route) { + route.name = route.name || Routes.generateName(route); + route.length = route.length || Routes.getLength(route.i); + const length = rn(route.length * distanceScale) + " " + distanceUnitInput.value; + data += [route.i, route.name, route.group, length].join(",") + "\n"; }); const name = getFileName("Routes") + ".csv";