/** * 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(); } }