Кнопка «Сохранить» активна только при несохранённых изменениях

Раньше она предлагала сохранить даже пустую вкладку: на свежей установке
конфигов ещё нет, полей на странице ноль, а кнопка зовёт нажать себя.

Теперь на всех четырёх вкладках кнопка включается, только когда есть что
записывать, и показывает счётчик изменённых полей. Возврат значения к
исходному снова её гасит — правки считаются сравнением с состоянием на
момент загрузки, а не фактом ввода.

Для модов изменением считается и переключённая галочка, и переставленный
порядок загрузки, и правка списка карт: состояние сводится к строке и
сравнивается со снимком, снятым при открытии вкладки.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-04 20:36:33 +03:00
co-authored by Claude Opus 5
parent 1e0b0a8d2b
commit ed08df4a60
2 changed files with 77 additions and 4 deletions
+73
View File
@@ -471,6 +471,7 @@ async function loadServerConfig() {
if (!data.exists) {
$("#config-empty").textContent = data.hint;
refreshFieldSaveState(SAVE_TARGETS[0]);
return;
}
serverConfigValues = data.values;
@@ -479,6 +480,7 @@ async function loadServerConfig() {
container.appendChild(buildConfigField(key, data.values[key], CONFIG_HINTS[key]));
});
applyFieldFilter("#config-filter", "#config-fields");
refreshFieldSaveState(SAVE_TARGETS[0]);
}
// buildConfigField выбирает тип поля по текущему значению: булевы значения
@@ -539,12 +541,35 @@ function collectChanged(containerSelector) {
return changed;
}
// Кнопка «Сохранить» включается, только когда есть что сохранять: на пустой
// вкладке (файл ещё не создан игрой) или без правок нажимать её незачем.
const SAVE_TARGETS = [
{ fields: "#config-fields", button: "#btn-config-save" },
{ fields: "#sandbox-fields", button: "#btn-sandbox-save" },
];
function refreshFieldSaveState(target) {
const count = Object.keys(collectChanged(target.fields)).length;
const button = $(target.button);
button.disabled = count === 0;
button.textContent = count ? `Сохранить (${count})` : "Сохранить";
button.title = count ? `Изменено параметров: ${count}` : "Изменений нет";
}
SAVE_TARGETS.forEach((target) => {
// Слушаем на контейнере: поля пересоздаются при каждой загрузке вкладки.
["input", "change"].forEach((event) =>
$(target.fields).addEventListener(event, () => refreshFieldSaveState(target)));
});
function markSaved(containerSelector) {
$$(`${containerSelector} .cfg-field`).forEach((field) => {
const input = field.querySelector("input, select");
if (input) input.dataset.original = input.value;
field.classList.remove("changed");
});
const target = SAVE_TARGETS.find((t) => t.fields === containerSelector);
if (target) refreshFieldSaveState(target);
}
$("#btn-config-save").addEventListener("click", async () => {
@@ -584,6 +609,7 @@ async function loadSandbox() {
if (!data.exists) {
$("#sandbox-empty").textContent = data.hint;
refreshFieldSaveState(SAVE_TARGETS[1]);
return;
}
const container = $("#sandbox-fields");
@@ -591,6 +617,7 @@ async function loadSandbox() {
container.appendChild(buildConfigField(entry.path, entry.value, null));
});
applyFieldFilter("#sandbox-filter", "#sandbox-fields");
refreshFieldSaveState(SAVE_TARGETS[1]);
}
$("#btn-sandbox-save").addEventListener("click", async () => {
@@ -616,6 +643,10 @@ $("#btn-sandbox-save").addEventListener("click", async () => {
// WorkshopItems — что скачать из мастерской, Mods — что включить и в каком
// порядке. Панель показывает их одним списком: строка на мод, галочка =
// присутствие в Mods, порядок строк = порядок в Mods.
// Снимок списков на момент загрузки: по нему видно, есть ли несохранённые
// правки. Пустая строка означает, что конфига ещё нет и сохранять нечего.
let modsSnapshot = "";
let mods = {
workshop: [], // WorkshopItems
enabled: [], // Mods, порядок важен
@@ -630,6 +661,8 @@ async function loadMods() {
$("#mods-body").hidden = !data.exists;
if (!data.exists) {
$("#mods-empty").textContent = data.hint;
modsSnapshot = "";
refreshModsSaveState();
return;
}
@@ -641,6 +674,7 @@ async function loadMods() {
details: mods.details,
};
$("#map-list").value = mods.maps.join("\n");
modsSnapshot = modsState();
renderMods();
lookupWorkshopTitles().catch(() => {
/* без интернета останутся идентификаторы — это не ошибка панели */
@@ -711,6 +745,23 @@ function buildModRows() {
return rows;
}
// modsState сводит списки к строке — так проще всего заметить и переключённую
// галочку, и переставленный порядок загрузки.
function modsState() {
return JSON.stringify({
enabled: mods.enabled,
workshop: mods.workshop,
maps: $("#map-list").value.split("\n").map((line) => line.trim()).filter(Boolean),
});
}
function refreshModsSaveState() {
const button = $("#btn-mods-save");
const changed = modsSnapshot !== "" && modsState() !== modsSnapshot;
button.disabled = !changed;
button.title = changed ? "Есть несохранённые изменения" : "Изменений нет";
}
function renderMods() {
const filter = $("#mod-filter").value.trim().toLowerCase();
const onlyEnabled = $("#mod-only-enabled").checked;
@@ -725,6 +776,8 @@ function renderMods() {
$("#mod-summary").textContent =
`Модов: ${rows.length}, включено: ${mods.enabled.length}, пакетов мастерской: ${mods.workshop.length}`;
refreshModsSaveState();
$("#mod-list").innerHTML = visible
.map((row) => {
const on = mods.enabled.includes(row.modID);
@@ -857,6 +910,7 @@ $("#mod-list").addEventListener("drop", (event) => {
renderMods();
});
$("#map-list").addEventListener("input", refreshModsSaveState);
$("#mod-filter").addEventListener("input", renderMods);
$("#mod-only-enabled").addEventListener("change", renderMods);
@@ -921,6 +975,8 @@ $("#btn-mods-save").addEventListener("click", async () => {
workshop_items: mods.workshop,
map: maps,
});
modsSnapshot = modsState();
refreshModsSaveState();
ok("Список модов сохранён. Перезапустите сервер, чтобы он их подтянул.");
} catch (error) {
fail(error);
@@ -1302,6 +1358,7 @@ async function loadSettings() {
} else {
input.value = config[key] ?? "";
}
input.dataset.original = type === "checkbox" ? String(input.checked) : input.value;
wrapper.appendChild(input);
if (hint) {
@@ -1318,6 +1375,7 @@ async function loadSettings() {
pathNote.textContent = `Файл конфигурации: ${config.config_path}`;
form.appendChild(pathNote);
refreshSettingsSaveState();
await loadUsers();
// Проверка обновления ходит в интернет и может не ответить — она не должна
// мешать показу остальных настроек.
@@ -1326,6 +1384,21 @@ async function loadSettings() {
});
}
function refreshSettingsSaveState() {
const changed = $$("#settings-form input").filter((input) => {
const current = input.type === "checkbox" ? String(input.checked) : input.value;
return current !== input.dataset.original;
}).length;
const button = $("#btn-settings-save");
button.disabled = changed === 0;
button.textContent = changed ? `Сохранить настройки (${changed})` : "Сохранить настройки";
button.title = changed ? `Изменено полей: ${changed}` : "Изменений нет";
}
["input", "change"].forEach((event) =>
$("#settings-form").addEventListener(event, refreshSettingsSaveState));
$("#btn-settings-save").addEventListener("click", async () => {
const payload = {};
SETTINGS_FIELDS.forEach(([key, , type]) => {
+4 -4
View File
@@ -145,7 +145,7 @@
</div>
<div>
<input type="search" id="config-filter" placeholder="Поиск параметра…" class="input-sm">
<button class="btn btn-primary" id="btn-config-save">Сохранить</button>
<button class="btn btn-primary" id="btn-config-save" disabled>Сохранить</button>
</div>
</div>
<p class="muted" id="config-empty" hidden></p>
@@ -161,7 +161,7 @@
</div>
<div>
<input type="search" id="sandbox-filter" placeholder="Поиск параметра…" class="input-sm">
<button class="btn btn-primary" id="btn-sandbox-save">Сохранить</button>
<button class="btn btn-primary" id="btn-sandbox-save" disabled>Сохранить</button>
</div>
</div>
<p class="muted" id="sandbox-empty" hidden></p>
@@ -176,7 +176,7 @@
<p class="muted small">Галочка включает мод, перетаскивание задаёт порядок загрузки.
Сервер сам скачивает моды из мастерской при запуске.</p>
</div>
<button class="btn btn-primary" id="btn-mods-save">Сохранить</button>
<button class="btn btn-primary" id="btn-mods-save" disabled>Сохранить</button>
</div>
<p class="muted" id="mods-empty" hidden></p>
@@ -290,7 +290,7 @@
<h2>Настройки менеджера</h2>
<form id="settings-form" class="fields"></form>
<button class="btn btn-primary" id="btn-settings-save">Сохранить настройки</button>
<button class="btn btn-primary" id="btn-settings-save" disabled>Сохранить настройки</button>
<h2>Пользователи панели</h2>
<ul class="user-list" id="user-list"></ul>