refactoring of cmaps, widgets, etc.

This commit is contained in:
2026-09-02 16:06:26 +02:00
parent 38f255c1a4
commit f0562a06cc
52 changed files with 3626 additions and 2642 deletions
@@ -0,0 +1,158 @@
/**
* Manage the shared people list used by the concept picker and people dialog.
*
* The controller loads and updates people through the wiki API, renders both
* views from the same collection and exposes only the selected person names to
* the concept editor.
*/
export class CmapPeopleDialog {
constructor(dialog, picker, repository, tr) {
this.dialog = dialog;
this.picker = picker;
this.repository = repository;
this.tr = tr;
this.people = [];
this.options = picker.querySelector("#cmap-person-tag-options");
this.managementList = dialog.querySelector("#cmap-people-list");
this.conceptInput = picker.querySelector("#cmap-person-new-name");
this.managementInput = dialog.querySelector("#cmap-people-new-name");
picker.querySelector("#cmap-person-add").addEventListener("click", () => {
this.createPerson(this.conceptInput, true)
.catch((error) => window.alert(error.message));
});
this.conceptInput.addEventListener("keydown", (event) => {
if (event.key !== "Enter") return;
event.preventDefault();
this.createPerson(this.conceptInput, true)
.catch((error) => window.alert(error.message));
});
dialog.querySelector("#cmap-people-add").addEventListener("click", () => {
this.createPerson(this.managementInput, false)
.then(() => this.renderManagement())
.catch((error) => window.alert(error.message));
});
this.managementInput.addEventListener("keydown", (event) => {
if (event.key !== "Enter") return;
event.preventDefault();
this.createPerson(this.managementInput, false)
.then(() => this.renderManagement())
.catch((error) => window.alert(error.message));
});
this.options.addEventListener("change", () => this.renderOptions(this.selectedNames()));
dialog.querySelector("#cmap-people-close").addEventListener("click", () => dialog.close());
}
async load() {
this.people = await this.repository.all();
return this.people;
}
selectedNames() {
return Array.from(this.options.querySelectorAll("input[type='checkbox']:checked"))
.map((input) => input.dataset.personName)
.filter(Boolean);
}
showSelection(selectedNames = []) {
this.renderOptions(selectedNames);
this.picker.open = false;
}
/** Render the picker while retaining inactive or missing selected people. */
renderOptions(selectedNames = []) {
const selected = new Set(selectedNames.map((name) => name.toLocaleLowerCase()));
this.options.replaceChildren();
const visiblePeople = this.people.filter((person) =>
person.active || selected.has(String(person.name).toLocaleLowerCase()));
for (const person of visiblePeople) {
this.options.append(this.personOption(person.name, person.active, selected));
}
const knownNames = new Set(
this.people.map((person) => String(person.name).toLocaleLowerCase()));
for (const selectedName of selectedNames) {
if (!knownNames.has(String(selectedName).toLocaleLowerCase())) {
this.options.append(this.personOption(selectedName, false, selected));
}
}
if (!this.options.childElementCount) {
const empty = document.createElement("span");
empty.className = "muted";
empty.textContent = this.tr("no-active-people", "No active people yet.");
this.options.append(empty);
}
const summary = this.picker.querySelector("summary");
summary.textContent = selected.size ?
this.tr("people-selected", "{count} people selected")
.replace("{count}", String(selected.size)) :
this.tr("select-people", "Select people");
}
personOption(name, active, selected) {
const label = document.createElement("label");
label.className = "cmap-person-tag-option";
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.dataset.personName = name;
checkbox.checked = selected.has(String(name).toLocaleLowerCase());
const text = document.createElement("span");
text.textContent = active ? name : `${name} (${this.tr("inactive", "inactive")})`;
label.append(checkbox, text);
return label;
}
async createPerson(input, selectInConceptDialog) {
const name = input.value.trim();
if (!name) {
input.focus();
return null;
}
const selected = selectInConceptDialog ? this.selectedNames() : [];
const person = await this.repository.create(name);
input.value = "";
await this.load();
if (selectInConceptDialog) this.renderOptions([...selected, person.name]);
return person;
}
renderManagement() {
this.managementList.replaceChildren();
for (const person of this.people) {
const row = document.createElement("div");
row.className = "cmap-person-admin-row";
const name = document.createElement("strong");
name.textContent = person.name;
const activeLabel = document.createElement("label");
const active = document.createElement("input");
active.type = "checkbox";
active.checked = Boolean(person.active);
const activeText = document.createElement("span");
activeText.textContent = this.tr("active", "Active");
activeLabel.append(active, activeText);
const save = document.createElement("button");
save.type = "button";
save.textContent = this.tr("save", "Save");
save.addEventListener("click", async () => {
save.disabled = true;
try {
await this.repository.update(person, active.checked);
await this.load();
this.renderManagement();
} catch (error) {
window.alert(error.message);
save.disabled = false;
}
});
row.append(name, activeLabel, save);
this.managementList.append(row);
}
}
async open() {
await this.load();
this.renderManagement();
this.dialog.showModal();
}
}