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._setupNewRowDeletes(); 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", () => { const wasHidden = this._addPanel.classList.contains("hidden"); this._addPanel.classList.toggle("hidden"); this._updateEmptyTextVisibility(); // Auto-focus the search input when opening the panel if (wasHidden && this._addInput) { // Use setTimeout to ensure the panel is visible before focusing setTimeout(() => { this._addInput.focus(); }, 0); } }); } 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`; uncertain.value = this._pendingItem.id; 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", () => { row.remove(); this._pendingItem = null; this._clearAddPanel(); if (this._addPanel) { this._addPanel.classList.add("hidden"); } this._updateEmptyTextVisibility(); }); } 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"); } } }); }); } _setupNewRowDeletes() { if (!this._addRow) { return; } this._addRow.querySelectorAll(ROLE_NEW_DELETE).forEach((button) => { if (button.dataset.relationNewBound === "true") { return; } button.dataset.relationNewBound = "true"; button.addEventListener("click", () => { const row = button.closest(ROLE_REL_ROW); if (row) { row.remove(); } this._pendingItem = null; this._clearAddPanel(); if (this._addPanel) { this._addPanel.classList.add("hidden"); } this._updateEmptyTextVisibility(); this._updatePreferredOptions(); }); }); } _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" : ""; }); } }