aboutsummaryrefslogtreecommitdiffhomepage
path: root/scripts/park-editor.html
diff options
context:
space:
mode:
authorYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-27 15:21:44 +0900
committerYasutake Yohei <61961825+yasutakeyohei@users.noreply.github.com>2026-10-27 15:21:44 +0900
commitcb24e2fd1fab1924084f2765bb870f01570544a2 (patch)
tree0f00433e7203ef3c231be4f56ac8bc3faf9afa09 /scripts/park-editor.html
parent32d73749135dbe217fa94a96fe19bc966b3b8de6 (diff)
park-editor: 文字化けを保存時に警告し、場所を表示
保存を中止せず、該当セルを赤くハイライトして、保存前に公園名・項目・値の一覧を確認できるようにした。サーバー側も保存を継続し、警告を返す。
Diffstat (limited to 'scripts/park-editor.html')
-rw-r--r--scripts/park-editor.html50
1 files changed, 49 insertions, 1 deletions
diff --git a/scripts/park-editor.html b/scripts/park-editor.html
index 32e91c4..e320e5e 100644
--- a/scripts/park-editor.html
+++ b/scripts/park-editor.html
@@ -26,6 +26,8 @@
td input:not([type="checkbox"]) { width: 100%; box-sizing: border-box; border: none; padding: .25rem .35rem; font: inherit; background: transparent; color: inherit; }
td input:not([type="checkbox"]):focus { outline: 2px solid #3b82f6; }
tr.dirty td { background: #ff9d0033; }
+ td input.corrupt { background: #ffebee; box-shadow: inset 0 0 0 2px #e53935; }
+ #status.has-warning { color: #c62828; font-weight: 700; }
tbody tr.is-focused td { background: #3b82f622 !important; }
tbody tr.is-focused td.rownum { color: #1d4ed8; font-weight: 700; opacity: 1; }
tbody tr.is-focused td:first-child { box-shadow: inset 3px 0 0 #2563eb; }
@@ -448,6 +450,7 @@ function render() {
inp.dataset.c = c;
inp.addEventListener("input", () => {
tr.classList.add("dirty");
+ inp.classList.toggle("corrupt", inp.value.includes("\uFFFD"));
statusEl.textContent = "未保存の変更があります";
});
td.appendChild(inp);
@@ -458,6 +461,7 @@ function render() {
tbody.appendChild(tr);
});
filter();
+ markCorrupt();
updateCheckedCount();
}
@@ -477,6 +481,27 @@ function filter() {
count.textContent = shown + " / " + rows.length + " 件";
}
+function markCorrupt() {
+ tbody.querySelectorAll("input[data-r][data-c]").forEach((inp) => {
+ inp.classList.toggle("corrupt", inp.value.includes("\uFFFD"));
+ });
+}
+
+function corruptInputs() {
+ return Array.from(tbody.querySelectorAll("input[data-r][data-c]")).filter((inp) =>
+ inp.value.includes("\uFFFD"),
+ );
+}
+
+function corruptLabel(inp) {
+ const row = rows[Number(inp.dataset.r)];
+ const props = isFC ? row.properties || {} : row;
+ const name =
+ props["公園名"] || props["name"] || props["名称"] || `行${Number(inp.dataset.r) + 1}`;
+ const label = fields[Number(inp.dataset.c)].label;
+ return `${name} / ${label}: ${inp.value}`;
+}
+
function collect() {
const toyValues = new Map();
Array.prototype.forEach.call(tbody.querySelectorAll("input[data-r][data-c]"), (inp) => {
@@ -510,8 +535,21 @@ function collect() {
}
document.getElementById("save").onclick = async function () {
+ const bad = corruptInputs();
+ if (bad.length) {
+ const list = bad.slice(0, 20).map((inp) => `・${corruptLabel(inp)}`).join("\n");
+ const more = bad.length > 20 ? `\n…ほか ${bad.length - 20} 件` : "";
+ if (!confirm(`文字化け(U+FFFD)が ${bad.length} 件あります。このまま保存しますか?\n\n${list}${more}`)) {
+ statusEl.textContent = `文字化けが ${bad.length} 件あります(赤い枠のセル)。保存を中止しました。`;
+ statusEl.classList.add("has-warning");
+ bad[0].scrollIntoView({ block: "center", behavior: "smooth" });
+ bad[0].focus();
+ return;
+ }
+ }
collect();
statusEl.textContent = "保存中…";
+ statusEl.classList.remove("has-warning");
try {
const res = await fetch("/api/data/" + currentId, {
method: "POST",
@@ -520,13 +558,23 @@ document.getElementById("save").onclick = async function () {
});
if (res.ok) {
const result = await res.json();
- statusEl.textContent = `保存しました(${DATASETS[currentId].label} ${result.count}件)。git にコミットを忘れずに。`;
tbody.querySelectorAll("tr.dirty").forEach((tr) => tr.classList.remove("dirty"));
+ markCorrupt();
+ if (Array.isArray(result.warnings) && result.warnings.length) {
+ const list = result.warnings.slice(0, 10).map((w) => `・${w.where}`).join(" / ");
+ statusEl.textContent = `保存しました(${DATASETS[currentId].label} ${result.count}件)。文字化け ${result.warnings.length} 件: ${list}`;
+ statusEl.classList.add("has-warning");
+ } else {
+ statusEl.textContent = `保存しました(${DATASETS[currentId].label} ${result.count}件)。git にコミットを忘れずに。`;
+ statusEl.classList.remove("has-warning");
+ }
} else {
statusEl.textContent = "保存に失敗: " + (await res.text());
+ statusEl.classList.add("has-warning");
}
} catch (e) {
statusEl.textContent = "保存に失敗: " + e.message;
+ statusEl.classList.add("has-warning");
}
};