Files
musenalm/views/transform/relations-editor.js
2026-01-09 13:04:18 +01:00

459 lines
15 KiB
JavaScript

const ROLE_ADD_TOGGLE = "[data-role='relation-add-toggle']";
const ROLE_ADD_PANEL = "[data-role='relation-add-panel']";
const ROLE_ADD_CLOSE = "[data-role='relation-add-close']";
const ROLE_ADD_APPLY = "[data-role='relation-add-apply']";
const ROLE_ADD_ERROR = "[data-role='relation-add-error']";
const ROLE_ADD_ROW = "[data-role='relation-add-row']";
const ROLE_ADD_SELECT = "[data-role='relation-add-select']";
const ROLE_TYPE_SELECT = "[data-role='relation-type-select']";
const ROLE_UNCERTAIN = "[data-role='relation-uncertain']";
const ROLE_NEW_TEMPLATE = "template[data-role='relation-new-template']";
const ROLE_NEW_DELETE = "[data-role='relation-new-delete']";
const ROLE_REL_ROW = "[data-rel-row]";
const ROLE_REL_STRIKE = "[data-rel-strike]";
export class RelationsEditor extends HTMLElement {
constructor() {
super();
this._pendingItem = null;
this._pendingApply = false;
}
connectedCallback() {
this._prefix = this.getAttribute("data-prefix") || "";
this._linkBase = this.getAttribute("data-link-base") || "";
this._newLabel = this.getAttribute("data-new-label") || "(Neu)";
this._addToggleId = this.getAttribute("data-add-toggle-id") || "";
this._preferredLabel = (this.getAttribute("data-preferred-label") || "").trim();
this._emptyText = this.querySelector(".rel-empty-text");
this._setupAddPanel();
this._setupDeleteToggles();
this._setupPreferredOptionHandling();
}
_getExistingIds() {
const ids = new Set();
// For series: entries_series_series[...]
// For agents: entries_agents_agent[...]
const targetField = this._prefix === "entries_series" ? "series" : "agent";
// Get existing relation target IDs (from server-rendered rows)
this.querySelectorAll(`input[name^="${this._prefix}_${targetField}["]`).forEach((input) => {
const value = input.value.trim();
if (value) {
ids.add(value);
}
});
// Get new relation target IDs (from dynamically added rows, but not from the add panel)
if (this._addRow) {
this._addRow.querySelectorAll(`input[name="${this._prefix}_new_id"]`).forEach((input) => {
const value = input.value.trim();
if (value) {
ids.add(value);
}
});
}
return ids;
}
_updateEmptyTextVisibility() {
if (!this._emptyText) {
return;
}
// Check if there are any existing relations (server-rendered rows)
const targetField = this._prefix === "entries_series" ? "series" : "agent";
const hasExisting = this.querySelectorAll(`input[name^="${this._prefix}_${targetField}["]`).length > 0;
// Check if there are any new relations in the add row
const hasNew = this._addRow && this._addRow.querySelectorAll(`input[name="${this._prefix}_new_id"]`).length > 0;
// Check if add panel is visible
const isPanelVisible = this._addPanel && !this._addPanel.classList.contains("hidden");
// Hide empty text if: panel is visible OR there are any relations (existing or new)
if (isPanelVisible || hasExisting || hasNew) {
this._emptyText.classList.add("hidden");
} else {
this._emptyText.classList.remove("hidden");
}
}
_setupAddPanel() {
this._addToggle = this.querySelector(ROLE_ADD_TOGGLE);
if (this._addToggleId) {
const externalToggle = document.getElementById(this._addToggleId);
if (externalToggle) {
this._addToggle = externalToggle;
}
}
this._addPanel = this.querySelector(ROLE_ADD_PANEL);
this._addClose = this.querySelector(ROLE_ADD_CLOSE);
this._addApply = this.querySelector(ROLE_ADD_APPLY);
this._addError = this.querySelector(ROLE_ADD_ERROR);
this._addRow = this.querySelector(ROLE_ADD_ROW);
this._addSelect = this.querySelector(ROLE_ADD_SELECT);
this._typeSelect = this.querySelector(ROLE_TYPE_SELECT);
this._uncertain = this.querySelector(ROLE_UNCERTAIN);
this._template = this.querySelector(ROLE_NEW_TEMPLATE);
this._addInput = this._addSelect ? this._addSelect.querySelector(".ssr-input") : null;
if (!this._addPanel || !this._addRow || !this._addSelect || !this._typeSelect || !this._uncertain || !this._template) {
return;
}
// Set up filtering for single-select-remote (only for series, not agents)
if (this._addSelect && this._prefix === "entries_series") {
this._addSelect.addEventListener("ssrbeforefetch", () => {
this._addSelect._excludeIds = Array.from(this._getExistingIds());
});
}
if (this._addToggle) {
this._addToggle.addEventListener("click", () => {
this._addPanel.classList.toggle("hidden");
this._updateEmptyTextVisibility();
});
}
if (this._addClose) {
this._addClose.addEventListener("click", () => {
this._addPanel.classList.add("hidden");
this._updateEmptyTextVisibility();
});
}
if (this._addInput) {
this._addInput.addEventListener("keydown", (event) => {
if (event.key === "Enter") {
this._pendingApply = true;
}
});
}
if (this._addApply) {
this._addApply.addEventListener("click", () => {
this._pendingApply = false;
const idInput = this._addPanel.querySelector(`input[name='${this._prefix}_new_id']`);
const hasSelection = idInput && idInput.value.trim().length > 0;
if (!hasSelection) {
if (this._addError) {
this._addError.textContent = this._addError.getAttribute("data-error-empty") || "Bitte Reihe auswählen.";
this._addError.classList.remove("hidden");
}
return;
}
if (!this._pendingItem) {
return;
}
// Check for duplicates (only for series, not agents)
if (this._prefix === "entries_series") {
const existingIds = this._getExistingIds();
if (existingIds.has(this._pendingItem.id)) {
if (this._addError) {
this._addError.textContent = this._addError.getAttribute("data-error-duplicate") || "Diese Verknüpfung existiert bereits.";
this._addError.classList.remove("hidden");
}
return;
}
}
if (this._addError) {
this._addError.classList.add("hidden");
}
this._insertNewRow();
});
}
this._addSelect.addEventListener("ssrchange", (event) => {
this._pendingItem = event.detail?.item || null;
if (this._pendingItem && this._addError) {
this._addError.classList.add("hidden");
}
if (this._pendingApply && this._pendingItem && this._addApply) {
this._pendingApply = false;
this._addApply.click();
}
});
}
_clearAddPanel() {
if (this._addSelect) {
const clearButton = this._addSelect.querySelector(".ssr-clear-button");
if (clearButton) {
clearButton.click();
}
}
if (this._typeSelect) {
this._typeSelect.selectedIndex = 0;
}
if (this._uncertain) {
this._uncertain.checked = false;
}
if (this._addError) {
this._addError.classList.add("hidden");
}
}
_insertNewRow() {
const fragment = this._template.content.cloneNode(true);
const row = fragment.querySelector(ROLE_REL_ROW) || fragment.firstElementChild;
if (!row) {
return;
}
const link = fragment.querySelector("[data-rel-link]");
if (link) {
link.setAttribute("href", `${this._linkBase}${this._pendingItem.id}`);
}
const nameEl = fragment.querySelector("[data-rel-name]");
if (nameEl) {
nameEl.textContent = this._pendingItem.name || "";
}
const detailEl = fragment.querySelector("[data-rel-detail]");
const detailContainer = fragment.querySelector("[data-rel-detail-container]");
const detailText = this._pendingItem.detail || this._pendingItem.bio || "";
if (detailEl && detailText) {
detailEl.textContent = detailText;
} else if (detailContainer) {
detailContainer.remove();
}
const newBadge = fragment.querySelector("[data-rel-new]");
if (newBadge) {
newBadge.textContent = this._newLabel;
}
const typeSelect = fragment.querySelector("[data-rel-input='type']");
if (typeSelect && this._typeSelect) {
typeSelect.innerHTML = this._typeSelect.innerHTML;
typeSelect.value = this._typeSelect.value;
typeSelect.name = `${this._prefix}_new_type`;
typeSelect.addEventListener("change", () => this._updatePreferredOptions());
}
const uncertain = fragment.querySelector("[data-rel-input='uncertain']");
if (uncertain && this._uncertain) {
uncertain.checked = this._uncertain.checked;
uncertain.name = `${this._prefix}_new_uncertain`;
const uncertainId = `${this._prefix}_new_uncertain_row`;
uncertain.id = uncertainId;
const uncertainLabel = fragment.querySelector("[data-rel-uncertain-label]");
if (uncertainLabel) {
uncertainLabel.setAttribute("for", uncertainId);
}
}
const hiddenId = fragment.querySelector("[data-rel-input='id']");
if (hiddenId) {
hiddenId.name = `${this._prefix}_new_id`;
hiddenId.value = this._pendingItem.id;
}
const deleteButton = fragment.querySelector(ROLE_NEW_DELETE);
if (deleteButton) {
deleteButton.addEventListener("click", () => {
this._addRow.innerHTML = "";
this._pendingItem = null;
this._clearAddPanel();
if (this._addPanel) {
this._addPanel.classList.add("hidden");
}
this._updateEmptyTextVisibility();
});
}
this._addRow.innerHTML = "";
this._addRow.appendChild(fragment);
this._pendingItem = null;
this._clearAddPanel();
if (this._addPanel) {
this._addPanel.classList.add("hidden");
}
this._updateEmptyTextVisibility();
this._updatePreferredOptions();
}
_setupDeleteToggles() {
this.querySelectorAll("[data-delete-toggle]").forEach((button) => {
button.addEventListener("click", () => {
const targetId = button.getAttribute("data-delete-toggle");
const checkbox = this.querySelector(`#${CSS.escape(targetId)}`);
if (!checkbox) {
return;
}
checkbox.checked = !checkbox.checked;
const row = button.closest(ROLE_REL_ROW);
if (row) {
row.classList.toggle("bg-red-50", checkbox.checked);
// Disable/enable form controls (but not the delete checkbox itself)
row.querySelectorAll("select, input[type='checkbox']").forEach((control) => {
// Skip the delete checkbox itself
if (control === checkbox) {
return;
}
control.disabled = checkbox.checked;
});
}
const isHovered = button.matches(":hover");
const label = button.querySelector("[data-delete-label]");
if (label) {
let nextLabel;
if (checkbox.checked && isHovered) {
nextLabel = label.getAttribute("data-delete-hover") || "Rückgängig";
} else if (checkbox.checked) {
nextLabel = label.getAttribute("data-delete-active") || "Wird entfernt";
} else {
nextLabel = label.getAttribute("data-delete-default") || "Entfernen";
}
label.textContent = nextLabel;
}
const icon = button.querySelector("i");
if (icon) {
if (checkbox.checked) {
if (isHovered) {
icon.classList.remove("hidden");
icon.classList.add("ri-arrow-go-back-line");
icon.classList.remove("ri-delete-bin-line");
} else {
icon.classList.add("hidden");
icon.classList.remove("ri-delete-bin-line", "ri-arrow-go-back-line");
}
} else {
icon.classList.remove("hidden");
icon.classList.add("ri-delete-bin-line");
icon.classList.remove("ri-arrow-go-back-line");
}
}
this._updatePreferredOptions();
});
button.addEventListener("mouseenter", () => {
const targetId = button.getAttribute("data-delete-toggle");
const checkbox = this.querySelector(`#${CSS.escape(targetId)}`);
if (!checkbox || !checkbox.checked) {
return;
}
const label = button.querySelector("[data-delete-label]");
if (label) {
label.textContent = label.getAttribute("data-delete-hover") || "Rückgängig";
}
const icon = button.querySelector("i");
if (icon) {
icon.classList.remove("hidden");
icon.classList.add("ri-arrow-go-back-line");
icon.classList.remove("ri-delete-bin-line");
}
});
button.addEventListener("mouseleave", () => {
const targetId = button.getAttribute("data-delete-toggle");
const checkbox = this.querySelector(`#${CSS.escape(targetId)}`);
const label = button.querySelector("[data-delete-label]");
if (!label) {
return;
}
if (checkbox && checkbox.checked) {
label.textContent = label.getAttribute("data-delete-active") || "Wird entfernt";
} else {
label.textContent = label.getAttribute("data-delete-default") || "Entfernen";
}
const icon = button.querySelector("i");
if (icon) {
if (checkbox && checkbox.checked) {
icon.classList.add("hidden");
icon.classList.remove("ri-delete-bin-line", "ri-arrow-go-back-line");
} else {
icon.classList.remove("hidden");
icon.classList.add("ri-delete-bin-line");
icon.classList.remove("ri-arrow-go-back-line");
}
}
});
});
}
_setupPreferredOptionHandling() {
if (this._prefix !== "entries_series" || !this._preferredLabel) {
return;
}
this.querySelectorAll(`select[name^="${this._prefix}_type["]`).forEach((select) => {
select.addEventListener("change", () => this._updatePreferredOptions());
});
if (this._typeSelect) {
this._typeSelect.addEventListener("change", () => this._updatePreferredOptions());
}
this._updatePreferredOptions();
}
_updatePreferredOptions() {
if (this._prefix !== "entries_series" || !this._preferredLabel) {
return;
}
const preferredLabel = this._preferredLabel.trim();
const selects = [];
this.querySelectorAll(`select[name^="${this._prefix}_type["]`).forEach((select) => {
selects.push({ select, row: select.closest(ROLE_REL_ROW), isAddPanel: false });
});
if (this._addRow) {
this._addRow.querySelectorAll(`select[name='${this._prefix}_new_type']`).forEach((select) => {
selects.push({ select, row: select.closest(ROLE_REL_ROW), isAddPanel: false });
});
}
if (this._typeSelect) {
selects.push({ select: this._typeSelect, row: this._typeSelect.closest(ROLE_REL_ROW), isAddPanel: true });
}
const hasPreferred = selects.some(({ select, row, isAddPanel }) => {
if (isAddPanel) {
return false;
}
const currentValue = (select?.value || "").trim();
if (!select || currentValue !== preferredLabel) {
return false;
}
if (!row) {
return true;
}
const deleteInput = row.querySelector(`input[name^="${this._prefix}_delete["]`);
return !(deleteInput && deleteInput.checked);
});
selects.forEach(({ select, row, isAddPanel }) => {
if (!select) {
return;
}
const option = Array.from(select.options).find((opt) => opt.value.trim() === preferredLabel);
if (!option) {
return;
}
const deleteInput = row ? row.querySelector(`input[name^="${this._prefix}_delete["]`) : null;
const rowDeleted = Boolean(deleteInput && deleteInput.checked);
const currentValue = (select.value || "").trim();
const keepVisible = !hasPreferred || (currentValue === preferredLabel && !rowDeleted);
if (isAddPanel && hasPreferred && currentValue === preferredLabel) {
const fallback = Array.from(select.options).find((opt) => opt.value.trim() !== preferredLabel);
if (fallback) {
select.value = fallback.value;
}
}
const shouldHide = !keepVisible || (isAddPanel && hasPreferred);
option.hidden = shouldHide;
option.disabled = shouldHide;
option.style.display = shouldHide ? "none" : "";
});
}
}