159 lines
5.8 KiB
JavaScript
159 lines
5.8 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|
|
}
|