This commit is contained in:
587
www/maps.js
Normal file
587
www/maps.js
Normal file
@@ -0,0 +1,587 @@
|
||||
(() => {
|
||||
const PAGE_SIZE = 10;
|
||||
|
||||
const ICONS = {
|
||||
map: `<svg class="mapsMirMapIcon" width="18" height="18" viewBox="0 0 18 18" aria-hidden="true"><rect x="2" y="2" width="14" height="14" rx="1" fill="none" stroke="currentColor" stroke-width="1.2"/><path d="M2 6h14M6 2v14M12 2v14" stroke="currentColor" stroke-width=".8" opacity=".5"/></svg>`,
|
||||
edit: `<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M9.5 2.5l2 2L5 11H3v-2L9.5 2.5z" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>`,
|
||||
view: `<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M1 7s2.5-4 6-4 6 4 6 4-2.5 4-6 4-6-4-6-4z" fill="none" stroke="currentColor" stroke-width="1.2"/><circle cx="7" cy="7" r="1.8" fill="none" stroke="currentColor" stroke-width="1.2"/></svg>`,
|
||||
delete: `<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M3 4h8M5 4V2.5h4V4M5.5 6v4M8.5 6v4M4.5 4l.5 7.5h4L9.5 4" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
||||
active: `<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true"><path d="M2 5l2.2 2.2L8 3.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
||||
};
|
||||
|
||||
const el = (id) => document.getElementById(id);
|
||||
const t = (key, vars) => window.I18n?.t(key, vars) ?? key;
|
||||
|
||||
const listViewEl = el("mapsListView");
|
||||
const createViewEl = el("mapsCreateView");
|
||||
const editorViewEl = el("mapEditorView");
|
||||
const listEl = el("mapsList");
|
||||
const listEmptyEl = el("mapsListEmpty");
|
||||
const tableEl = el("mapsTable");
|
||||
const activeHintEl = el("mapsActiveHint");
|
||||
const filterInputEl = el("mapsFilterInput");
|
||||
const filterCountEl = el("mapsFilterCount");
|
||||
const pageLabelEl = el("mapsPageLabel");
|
||||
const sitesDialogEl = el("mapsSitesDialog");
|
||||
const sitesListEl = el("mapsSitesList");
|
||||
const siteFormDialogEl = el("mapsSiteFormDialog");
|
||||
const createSiteSelectEl = el("mapsCreateSite");
|
||||
|
||||
const SITE_ICONS = {
|
||||
chevron: `<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M4 3l3 4-3 4M8 3l3 4-3 4" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
|
||||
edit: `<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M9.5 2.5l2 2L5 11H3v-2L9.5 2.5z" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>`,
|
||||
delete: `<svg width="14" height="14" viewBox="0 0 14 14" aria-hidden="true"><path d="M4 4l6 6M10 4l-6 6" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>`,
|
||||
};
|
||||
|
||||
const store = {
|
||||
maps: [],
|
||||
sites: [],
|
||||
activeMapId: null,
|
||||
filter: "",
|
||||
page: 1,
|
||||
editingSiteId: null,
|
||||
sitesDialogSelectedId: null,
|
||||
sitesDialogSnapshotId: null,
|
||||
};
|
||||
|
||||
function canWrite() {
|
||||
return window.AuthApp?.canWrite?.("maps") ?? true;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str)
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/"/g, """);
|
||||
}
|
||||
|
||||
async function api(path, opts = {}) {
|
||||
const res = await fetch(path, { credentials: "include", ...opts });
|
||||
if (!res.ok) {
|
||||
let msg = res.statusText;
|
||||
try {
|
||||
const err = await res.json();
|
||||
if (err.error) msg = err.error;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
throw new Error(msg);
|
||||
}
|
||||
if (res.status === 204) return null;
|
||||
return res.json();
|
||||
}
|
||||
|
||||
function findMap(id) {
|
||||
return store.maps.find((m) => m.id === id) || null;
|
||||
}
|
||||
|
||||
function findSite(id) {
|
||||
return store.sites.find((s) => s.id === id) || null;
|
||||
}
|
||||
|
||||
function siteName(siteId) {
|
||||
return findSite(siteId)?.name || siteId || "—";
|
||||
}
|
||||
|
||||
function mapImageUrl(map) {
|
||||
if (!map?.id || !map.image_file) return null;
|
||||
return `/api/maps/${encodeURIComponent(map.id)}/image?t=${encodeURIComponent(map.updated_at || "")}`;
|
||||
}
|
||||
|
||||
function filteredMaps() {
|
||||
const q = store.filter.trim().toLowerCase();
|
||||
let items = [...store.maps].sort((a, b) => {
|
||||
const sa = siteName(a.site_id).localeCompare(siteName(b.site_id));
|
||||
if (sa !== 0) return sa;
|
||||
return (a.name || "").localeCompare(b.name || "");
|
||||
});
|
||||
if (q) {
|
||||
items = items.filter((m) => {
|
||||
const name = (m.name || "").toLowerCase();
|
||||
const site = siteName(m.site_id).toLowerCase();
|
||||
return name.includes(q) || site.includes(q);
|
||||
});
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
function pageCount(total) {
|
||||
return Math.max(1, Math.ceil(total / PAGE_SIZE));
|
||||
}
|
||||
|
||||
function pagedMaps(items) {
|
||||
const totalPages = pageCount(items.length);
|
||||
if (store.page > totalPages) store.page = totalPages;
|
||||
if (store.page < 1) store.page = 1;
|
||||
const start = (store.page - 1) * PAGE_SIZE;
|
||||
return items.slice(start, start + PAGE_SIZE);
|
||||
}
|
||||
|
||||
function updatePagerUi(totalItems) {
|
||||
const totalPages = pageCount(totalItems);
|
||||
if (filterCountEl) {
|
||||
filterCountEl.textContent = t("maps.itemsFound", { n: totalItems });
|
||||
}
|
||||
if (pageLabelEl) {
|
||||
pageLabelEl.textContent = t("maps.pageOf", { page: store.page, total: totalPages });
|
||||
}
|
||||
const atStart = store.page <= 1;
|
||||
const atEnd = store.page >= totalPages;
|
||||
el("mapsPageFirst")?.toggleAttribute("disabled", atStart);
|
||||
el("mapsPagePrev")?.toggleAttribute("disabled", atStart);
|
||||
el("mapsPageNext")?.toggleAttribute("disabled", atEnd);
|
||||
el("mapsPageLast")?.toggleAttribute("disabled", atEnd);
|
||||
}
|
||||
|
||||
async function loadSites() {
|
||||
const data = await api("/api/sites");
|
||||
store.sites = Array.isArray(data.sites) ? data.sites : [];
|
||||
}
|
||||
|
||||
async function loadMaps() {
|
||||
const data = await api("/api/maps");
|
||||
store.maps = Array.isArray(data.maps) ? data.maps : [];
|
||||
}
|
||||
|
||||
async function loadActiveMap() {
|
||||
try {
|
||||
const status = await api("/api/robot/status");
|
||||
store.activeMapId = status.active_map_id || null;
|
||||
} catch {
|
||||
store.activeMapId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function renderActiveHint() {
|
||||
if (!activeHintEl) return;
|
||||
const active = findMap(store.activeMapId);
|
||||
if (active) {
|
||||
activeHintEl.hidden = false;
|
||||
activeHintEl.textContent = t("maps.activeHint", { name: active.name });
|
||||
} else {
|
||||
activeHintEl.hidden = true;
|
||||
activeHintEl.textContent = "";
|
||||
}
|
||||
}
|
||||
|
||||
function renderSiteSelect(selectedId) {
|
||||
if (!createSiteSelectEl) return;
|
||||
createSiteSelectEl.innerHTML = "";
|
||||
store.sites.forEach((site) => {
|
||||
const opt = document.createElement("option");
|
||||
opt.value = site.id;
|
||||
opt.textContent = site.name || site.id;
|
||||
createSiteSelectEl.appendChild(opt);
|
||||
});
|
||||
if (selectedId) createSiteSelectEl.value = selectedId;
|
||||
else if (store.sites[0]) createSiteSelectEl.value = store.sites[0].id;
|
||||
}
|
||||
|
||||
function renderList() {
|
||||
if (!listEl) return;
|
||||
const items = filteredMaps();
|
||||
const pageItems = pagedMaps(items);
|
||||
updatePagerUi(items.length);
|
||||
|
||||
listEl.innerHTML = "";
|
||||
const showEmpty = items.length === 0;
|
||||
if (tableEl) tableEl.hidden = showEmpty;
|
||||
if (listEmptyEl) {
|
||||
listEmptyEl.hidden = !showEmpty;
|
||||
listEmptyEl.textContent = store.filter.trim() ? t("maps.emptyFilter") : t("maps.empty");
|
||||
}
|
||||
|
||||
let lastSiteId = null;
|
||||
pageItems.forEach((map) => {
|
||||
const siteId = map.site_id || "";
|
||||
if (siteId !== lastSiteId) {
|
||||
lastSiteId = siteId;
|
||||
const siteTr = document.createElement("tr");
|
||||
siteTr.className = "mapsMirSiteRow";
|
||||
siteTr.innerHTML = `<td colspan="3">${escapeHtml(siteName(siteId))}</td>`;
|
||||
listEl.appendChild(siteTr);
|
||||
}
|
||||
|
||||
const isActive = map.id === store.activeMapId;
|
||||
const tr = document.createElement("tr");
|
||||
tr.className = "mapsMirRow";
|
||||
tr.dataset.mapId = map.id;
|
||||
|
||||
const activeBadge = isActive
|
||||
? `<span class="mapsActiveBadge">${ICONS.active}<span>${escapeHtml(t("maps.activeBadge"))}</span></span>`
|
||||
: "";
|
||||
|
||||
const actions = canWrite()
|
||||
? `<div class="mapsMirRowActions">
|
||||
<button type="button" class="mapsMirIconBtn" data-edit="${map.id}" data-i18n-title="common.edit" title="${escapeHtml(t("common.edit"))}">${ICONS.edit}</button>
|
||||
<button type="button" class="mapsMirIconBtn" data-view="${map.id}" data-i18n-title="maps.view" title="${escapeHtml(t("maps.view"))}">${ICONS.view}</button>
|
||||
<button type="button" class="mapsMirIconBtn mapsMirIconBtn--danger" data-delete="${map.id}" data-i18n-title="common.delete" title="${escapeHtml(t("common.delete"))}">${ICONS.delete}</button>
|
||||
</div>`
|
||||
: `<div class="mapsMirRowActions">
|
||||
<button type="button" class="mapsMirIconBtn" data-view="${map.id}" data-i18n-title="maps.view" title="${escapeHtml(t("maps.view"))}">${ICONS.view}</button>
|
||||
</div>`;
|
||||
|
||||
tr.innerHTML = `
|
||||
<td class="mapsMirCellName">
|
||||
<div class="mapsMirNameCell">
|
||||
${ICONS.map}
|
||||
<button type="button" class="mapsMirNameLink" data-open="${map.id}">${escapeHtml(map.name || map.id)}</button>
|
||||
${activeBadge}
|
||||
</div>
|
||||
</td>
|
||||
<td class="mapsMirCellCreatedBy">${escapeHtml(map.created_by || "—")}</td>
|
||||
<td class="mapsMirCellActions">${actions}</td>`;
|
||||
|
||||
tr.querySelector("[data-open]")?.addEventListener("click", () => openEditor(map.id));
|
||||
tr.querySelector("[data-edit]")?.addEventListener("click", () => openEditor(map.id));
|
||||
tr.querySelector("[data-view]")?.addEventListener("click", () => openEditor(map.id, { readOnly: !canWrite() }));
|
||||
tr.querySelector("[data-delete]")?.addEventListener("click", () => {
|
||||
void deleteMapFromList(map.id);
|
||||
});
|
||||
tr.addEventListener("dblclick", () => openEditor(map.id));
|
||||
listEl.appendChild(tr);
|
||||
});
|
||||
renderActiveHint();
|
||||
}
|
||||
|
||||
function hideAllViews() {
|
||||
[listViewEl, createViewEl, editorViewEl].forEach((view) => {
|
||||
if (!view) return;
|
||||
view.hidden = true;
|
||||
view.setAttribute("aria-hidden", "true");
|
||||
});
|
||||
}
|
||||
|
||||
function showList() {
|
||||
hideAllViews();
|
||||
if (listViewEl) {
|
||||
listViewEl.hidden = false;
|
||||
listViewEl.removeAttribute("aria-hidden");
|
||||
}
|
||||
window.MapEditorApp?.close?.();
|
||||
}
|
||||
|
||||
function showCreate() {
|
||||
if (!canWrite()) return;
|
||||
hideAllViews();
|
||||
renderSiteSelect();
|
||||
const nameEl = el("mapsCreateName");
|
||||
if (nameEl) nameEl.value = "";
|
||||
if (createViewEl) {
|
||||
createViewEl.hidden = false;
|
||||
createViewEl.removeAttribute("aria-hidden");
|
||||
}
|
||||
nameEl?.focus();
|
||||
}
|
||||
|
||||
function openEditor(mapId, opts = {}) {
|
||||
const map = findMap(mapId);
|
||||
if (!map) return;
|
||||
hideAllViews();
|
||||
if (editorViewEl) {
|
||||
editorViewEl.hidden = false;
|
||||
editorViewEl.removeAttribute("aria-hidden");
|
||||
}
|
||||
window.MapEditorApp?.open?.(mapId, {
|
||||
readOnly: opts.readOnly,
|
||||
onMapUpdated: (updated) => {
|
||||
const idx = store.maps.findIndex((m) => m.id === updated.id);
|
||||
if (idx >= 0) store.maps[idx] = updated;
|
||||
else store.maps.push(updated);
|
||||
},
|
||||
onMapDeleted: (id) => {
|
||||
store.maps = store.maps.filter((m) => m.id !== id);
|
||||
if (store.activeMapId === id) store.activeMapId = null;
|
||||
showList();
|
||||
renderList();
|
||||
},
|
||||
onActivated: (id) => {
|
||||
store.activeMapId = id;
|
||||
renderList();
|
||||
},
|
||||
onClose: () => {
|
||||
showList();
|
||||
renderList();
|
||||
},
|
||||
getSiteName: siteName,
|
||||
getActiveMapId: () => store.activeMapId,
|
||||
canWrite: canWrite(),
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteMapFromList(mapId) {
|
||||
const map = findMap(mapId);
|
||||
if (!map) return;
|
||||
if (!confirm(t("maps.deleteConfirm", { name: map.name }))) return;
|
||||
await api(`/api/maps/${encodeURIComponent(map.id)}`, { method: "DELETE" });
|
||||
store.maps = store.maps.filter((m) => m.id !== map.id);
|
||||
if (store.activeMapId === map.id) store.activeMapId = null;
|
||||
renderList();
|
||||
}
|
||||
|
||||
async function activateMap(mapId) {
|
||||
const map = findMap(mapId);
|
||||
if (!map) return;
|
||||
if (!map.image_file) {
|
||||
alert(t("maps.error.noImage"));
|
||||
return;
|
||||
}
|
||||
await api("/api/robot/active_map", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ map_id: mapId }),
|
||||
});
|
||||
store.activeMapId = mapId;
|
||||
renderList();
|
||||
}
|
||||
|
||||
function openCreatePage() {
|
||||
showCreate();
|
||||
}
|
||||
|
||||
function renderSitesDialogList() {
|
||||
if (!sitesListEl) return;
|
||||
sitesListEl.innerHTML = "";
|
||||
if (store.sites.length === 0) {
|
||||
const empty = document.createElement("li");
|
||||
empty.className = "mapsMirSitesEmpty";
|
||||
empty.textContent = t("maps.sitesDialog.empty");
|
||||
sitesListEl.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
store.sites.forEach((site) => {
|
||||
const li = document.createElement("li");
|
||||
li.className = "mapsMirSitesItem";
|
||||
if (site.id === store.sitesDialogSelectedId) li.classList.add("is-selected");
|
||||
li.dataset.siteId = site.id;
|
||||
li.innerHTML = `
|
||||
<button type="button" class="mapsMirSitesItemMain" data-select-site="${site.id}">
|
||||
<span class="mapsMirSitesChevron">${SITE_ICONS.chevron}</span>
|
||||
<span class="mapsMirSitesItemName">${escapeHtml(site.name || site.id)}</span>
|
||||
</button>
|
||||
<div class="mapsMirSitesItemActions">
|
||||
<button type="button" class="mapsMirSitesIconBtn" data-edit-site="${site.id}" data-i18n-title="common.edit" title="${escapeHtml(t("common.edit"))}">${SITE_ICONS.edit}</button>
|
||||
<button type="button" class="mapsMirSitesIconBtn mapsMirSitesIconBtn--danger" data-delete-site="${site.id}" data-i18n-title="common.delete" title="${escapeHtml(t("common.delete"))}">${SITE_ICONS.delete}</button>
|
||||
</div>`;
|
||||
li.querySelector("[data-select-site]")?.addEventListener("click", () => {
|
||||
store.sitesDialogSelectedId = site.id;
|
||||
renderSitesDialogList();
|
||||
});
|
||||
li.querySelector("[data-edit-site]")?.addEventListener("click", (evt) => {
|
||||
evt.stopPropagation();
|
||||
openSiteFormDialog(site.id);
|
||||
});
|
||||
li.querySelector("[data-delete-site]")?.addEventListener("click", (evt) => {
|
||||
evt.stopPropagation();
|
||||
void deleteSite(site.id);
|
||||
});
|
||||
sitesListEl.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
async function openSitesDialog() {
|
||||
await loadSites();
|
||||
store.sitesDialogSnapshotId = createSiteSelectEl?.value || store.sites[0]?.id || null;
|
||||
store.sitesDialogSelectedId = store.sitesDialogSnapshotId;
|
||||
renderSitesDialogList();
|
||||
sitesDialogEl?.showModal();
|
||||
}
|
||||
|
||||
function closeSitesDialog(apply) {
|
||||
if (apply && store.sitesDialogSelectedId) {
|
||||
renderSiteSelect(store.sitesDialogSelectedId);
|
||||
}
|
||||
sitesDialogEl?.close();
|
||||
}
|
||||
|
||||
function openSiteFormDialog(siteId) {
|
||||
store.editingSiteId = siteId || null;
|
||||
const site = siteId ? findSite(siteId) : null;
|
||||
const titleEl = el("mapsSiteFormTitle");
|
||||
const nameEl = el("mapsSiteName");
|
||||
if (titleEl) {
|
||||
titleEl.textContent = site ? t("maps.siteForm.edit") : t("maps.siteForm.create");
|
||||
}
|
||||
if (nameEl) nameEl.value = site?.name || "";
|
||||
siteFormDialogEl?.showModal();
|
||||
nameEl?.focus();
|
||||
}
|
||||
|
||||
async function saveSite(evt) {
|
||||
evt.preventDefault();
|
||||
const name = el("mapsSiteName")?.value.trim();
|
||||
if (!name) return;
|
||||
if (store.editingSiteId) {
|
||||
const updated = await api(`/api/sites/${encodeURIComponent(store.editingSiteId)}`, {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
const idx = store.sites.findIndex((s) => s.id === store.editingSiteId);
|
||||
if (idx >= 0) store.sites[idx] = updated;
|
||||
} else {
|
||||
const created = await api("/api/sites", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
store.sites.push(created);
|
||||
store.sitesDialogSelectedId = created.id;
|
||||
}
|
||||
siteFormDialogEl?.close();
|
||||
renderSitesDialogList();
|
||||
renderList();
|
||||
}
|
||||
|
||||
async function deleteSite(siteId) {
|
||||
const site = findSite(siteId);
|
||||
if (!site) return;
|
||||
if (!confirm(t("maps.sitesDialog.deleteConfirm", { name: site.name }))) return;
|
||||
try {
|
||||
await api(`/api/sites/${encodeURIComponent(siteId)}`, { method: "DELETE" });
|
||||
} catch (e) {
|
||||
alert(e.message);
|
||||
return;
|
||||
}
|
||||
store.sites = store.sites.filter((s) => s.id !== siteId);
|
||||
if (store.sitesDialogSelectedId === siteId) {
|
||||
store.sitesDialogSelectedId = store.sites[0]?.id || null;
|
||||
}
|
||||
renderSitesDialogList();
|
||||
renderList();
|
||||
}
|
||||
|
||||
async function createMap(evt) {
|
||||
evt.preventDefault();
|
||||
const name = el("mapsCreateName")?.value.trim();
|
||||
const site_id = createSiteSelectEl?.value;
|
||||
if (!name || !site_id) return;
|
||||
const user = window.AuthApp?.getUser?.();
|
||||
const created = await api("/api/maps", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
site_id,
|
||||
created_by: user?.display_name || user?.username || "",
|
||||
resolution: 0.05,
|
||||
origin_x: 0,
|
||||
origin_y: 0,
|
||||
origin_yaw: 0,
|
||||
}),
|
||||
});
|
||||
store.maps.push(created);
|
||||
store.filter = "";
|
||||
if (filterInputEl) filterInputEl.value = "";
|
||||
store.page = 1;
|
||||
renderList();
|
||||
openEditor(created.id);
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
store.filter = "";
|
||||
store.page = 1;
|
||||
if (filterInputEl) filterInputEl.value = "";
|
||||
renderList();
|
||||
}
|
||||
|
||||
function goToPage(page) {
|
||||
const total = pageCount(filteredMaps().length);
|
||||
store.page = Math.min(Math.max(1, page), total);
|
||||
renderList();
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
el("mapsCreateOpenBtn")?.addEventListener("click", openCreatePage);
|
||||
el("mapsCreateGoBackBtn")?.addEventListener("click", showList);
|
||||
el("mapsCreateCancelBtn")?.addEventListener("click", showList);
|
||||
el("mapsCreateHelpBtn")?.addEventListener("click", () => alert(t("maps.createPage.helpText")));
|
||||
el("mapsImportSiteBtn")?.addEventListener("click", () => alert(t("maps.importComingSoon")));
|
||||
el("mapsClearFiltersBtn")?.addEventListener("click", clearFilters);
|
||||
el("mapsHelpBtn")?.addEventListener("click", () => alert(t("maps.helpText")));
|
||||
|
||||
filterInputEl?.addEventListener("input", () => {
|
||||
store.filter = filterInputEl.value;
|
||||
store.page = 1;
|
||||
renderList();
|
||||
});
|
||||
|
||||
el("mapsPageFirst")?.addEventListener("click", () => goToPage(1));
|
||||
el("mapsPagePrev")?.addEventListener("click", () => goToPage(store.page - 1));
|
||||
el("mapsPageNext")?.addEventListener("click", () => goToPage(store.page + 1));
|
||||
el("mapsPageLast")?.addEventListener("click", () => goToPage(pageCount(filteredMaps().length)));
|
||||
|
||||
el("mapsCreateForm")?.addEventListener("submit", (evt) => {
|
||||
createMap(evt).catch((e) => alert(e.message));
|
||||
});
|
||||
el("mapsSiteForm")?.addEventListener("submit", (evt) => {
|
||||
saveSite(evt).catch((e) => alert(e.message));
|
||||
});
|
||||
el("mapsCreateSiteBtn")?.addEventListener("click", () => {
|
||||
openSitesDialog().catch((e) => alert(e.message));
|
||||
});
|
||||
el("mapsSitesCreateBtn")?.addEventListener("click", () => openSiteFormDialog(null));
|
||||
el("mapsSitesOkBtn")?.addEventListener("click", () => closeSitesDialog(true));
|
||||
el("mapsSitesCancelBtn")?.addEventListener("click", () => closeSitesDialog(false));
|
||||
sitesDialogEl?.addEventListener("cancel", (evt) => {
|
||||
evt.preventDefault();
|
||||
closeSitesDialog(false);
|
||||
});
|
||||
|
||||
document.querySelectorAll("[data-close-dialog]").forEach((btn) => {
|
||||
btn.addEventListener("click", () => {
|
||||
const id = btn.getAttribute("data-close-dialog");
|
||||
el(id)?.close();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function applyReadOnly() {
|
||||
document.body.classList.toggle("auth-readonly-maps-page", !canWrite());
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await Promise.all([loadSites(), loadMaps(), loadActiveMap()]);
|
||||
renderList();
|
||||
}
|
||||
|
||||
async function init() {
|
||||
applyReadOnly();
|
||||
showList();
|
||||
bindEvents();
|
||||
try {
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
if (listEmptyEl) {
|
||||
listEmptyEl.hidden = false;
|
||||
listEmptyEl.textContent = t("common.error", { msg: e.message });
|
||||
}
|
||||
if (tableEl) tableEl.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
window.MapsApp = {
|
||||
init,
|
||||
refresh,
|
||||
onPageShow() {
|
||||
applyReadOnly();
|
||||
showList();
|
||||
refresh().catch(() => {});
|
||||
},
|
||||
getMaps: () => [...store.maps],
|
||||
getMapById: findMap,
|
||||
activateMap,
|
||||
};
|
||||
|
||||
function boot() {
|
||||
init();
|
||||
}
|
||||
|
||||
if (window.AuthApp?.isReady()) boot();
|
||||
else window.addEventListener("lm:auth-ready", boot, { once: true });
|
||||
window.addEventListener("lm:locale-change", () => {
|
||||
renderList();
|
||||
renderActiveHint();
|
||||
});
|
||||
})();
|
||||
Reference in New Issue
Block a user