1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
|
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>公園データ編集</title>
<style>
:root { color-scheme: light dark; }
body { font-family: system-ui, -apple-system, "Hiragino Kaku Gothic ProN", sans-serif; margin: 0; }
header { position: sticky; top: 0; background: Canvas; border-bottom: 1px solid #9995; padding: .5rem .75rem; display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; z-index: 10; }
header h1 { font-size: 1rem; margin: 0 .5rem 0 0; }
#q { padding: .3rem .5rem; }
#count { font-size: .85rem; opacity: .7; }
#save { padding: .35rem .9rem; font-weight: bold; }
#status { font-size: .85rem; }
.wrap { overflow: auto; height: calc(100vh - 3.2rem); }
table { border-collapse: collapse; font-size: .8rem; }
th, td { border: 1px solid #9994; padding: 0; }
th { position: sticky; top: 0; background: Canvas; padding: .25rem .4rem; white-space: nowrap; z-index: 5; }
td { padding: 0; }
td input { width: 100%; box-sizing: border-box; border: none; padding: .25rem .35rem; font: inherit; background: transparent; color: inherit; }
td input:focus { outline: 2px solid #3b82f6; }
tr.dirty td { background: #ff9d0033; }
tr[hidden] { display: none; }
td.rownum { padding: 0 .4rem; text-align: right; opacity: .5; }
td.mapcell { text-align: center; }
td.mapcell a { text-decoration: none; padding: 0 .4rem; cursor: pointer; }
</style>
</head>
<body>
<header>
<h1>公園データ編集</h1>
<input id="q" placeholder="絞り込み(公園名・町名・住所)" size="24">
<span id="count"></span>
<button id="save">保存</button>
<button id="revert">再読込</button>
<span id="status"></span>
</header>
<div class="wrap"><table id="tbl"><thead></thead><tbody></tbody></table></div>
<script>
const FIELDS = ["公園名","住所","町名","面積㎡","500㎡以上","種別","南西部指定管理","トイレ","水飲み場","ベンチ・椅子","テーブル","防災用具","ボール遊び","遊具","備考"];
let parks = [];
const tbl = document.getElementById("tbl");
const thead = tbl.querySelector("thead");
const tbody = tbl.querySelector("tbody");
const q = document.getElementById("q");
const count = document.getElementById("count");
const status = document.getElementById("status");
function mapsUrl(name) {
return "https://www.google.com/maps/search/?api=1&query=" + encodeURIComponent("小平市" + (name || ""));
}
async function load() {
const res = await fetch("/api/parks", { cache: "no-store" });
parks = await res.json();
render();
}
function render() {
thead.innerHTML = "<tr>" + ["#", "地図"].concat(FIELDS).map(function (f) { return "<th>" + f + "</th>"; }).join("") + "</tr>";
tbody.innerHTML = "";
parks.forEach(function (p, r) {
const tr = document.createElement("tr");
const th = document.createElement("td");
th.className = "rownum";
th.textContent = String(r + 1);
tr.appendChild(th);
const mapTd = document.createElement("td");
mapTd.className = "mapcell";
const mapA = document.createElement("a");
mapA.textContent = "🗺";
mapA.title = "Googleマップで開く";
mapA.href = mapsUrl(p["公園名"] || "");
mapA.target = "_blank";
mapA.rel = "noopener";
mapA.addEventListener("click", function (ev) {
ev.preventDefault();
const name = tr.querySelector('input[data-c="0"]').value.trim();
if (name) window.open(mapsUrl(name), "_blank", "noopener");
});
mapTd.appendChild(mapA);
tr.appendChild(mapTd);
FIELDS.forEach(function (f, c) {
const td = document.createElement("td");
const inp = document.createElement("input");
inp.value = p[f] != null ? p[f] : "";
inp.dataset.r = r;
inp.dataset.c = c;
inp.addEventListener("input", function () {
tr.classList.add("dirty");
status.textContent = "未保存の変更があります";
});
td.appendChild(inp);
tr.appendChild(td);
});
tr.dataset.search = (p["公園名"] || "") + " " + (p["町名"] || "") + " " + (p["住所"] || "");
tbody.appendChild(tr);
});
filter();
}
function filter() {
const s = q.value.trim();
let shown = 0;
Array.prototype.forEach.call(tbody.children, function (tr) {
const ok = !s || tr.dataset.search.indexOf(s) >= 0;
tr.hidden = !ok;
if (ok) shown++;
});
count.textContent = shown + " / " + parks.length + " 件";
}
function collect() {
Array.prototype.forEach.call(tbody.querySelectorAll("input"), function (inp) {
parks[Number(inp.dataset.r)][FIELDS[Number(inp.dataset.c)]] = inp.value;
});
}
document.getElementById("save").onclick = async function () {
collect();
status.textContent = "保存中…";
try {
const res = await fetch("/api/parks", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(parks),
});
if (res.ok) {
status.textContent = "保存しました(" + parks.length + "件)。git にコミットを忘れずに。";
Array.prototype.forEach.call(tbody.children, function (tr) { tr.classList.remove("dirty"); });
} else {
status.textContent = "保存に失敗: " + (await res.text());
}
} catch (e) {
status.textContent = "保存に失敗: " + e.message;
}
};
document.getElementById("revert").onclick = function () {
if (confirm("保存していない変更は破棄されます。よろしいですか?")) load();
};
q.addEventListener("input", filter);
load();
</script>
</body>
</html>
|