mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 18:45:31 +00:00
494 lines
16 KiB
JavaScript
494 lines
16 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._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" : "";
|
|
});
|
|
}
|
|
}
|