export class FabMenu extends HTMLElement { constructor() { super(); this.state = null; // Will be set in connectedCallback: 'closed', 'half', 'full' this.handleClick = this.handleClick.bind(this); this.handleClickAway = this.handleClickAway.bind(this); this.handleDeleteClick = this.handleDeleteClick.bind(this); } connectedCallback() { // Get data attributes passed from template const userName = this.getAttribute("data-user-name") || "Benutzer"; const userEmail = this.getAttribute("data-user-email") || ""; const userId = this.getAttribute("data-user-id") || ""; const isAdminOrEditor = this.getAttribute("data-is-admin-or-editor") === "true"; const isAdmin = this.getAttribute("data-is-admin") === "true"; const redirectPath = this.getAttribute("data-redirect-path") || ""; // Detect context from URL const path = window.location.pathname; let hasReihe = false, reiheId = "", reiheUpdated = ""; let hasPerson = false, personId = ""; let hasEntry = false, entryId = "", entryUpdated = ""; // Reihe detail page: /reihe/{id} (but not /reihe/new or /reihe/{id}/edit) const reiheMatch = path.match(/^\/reihe\/([^\/]+)\/?$/); if (reiheMatch && reiheMatch[1] !== "new") { hasReihe = true; reiheId = reiheMatch[1]; // Try to get updated timestamp from page const updatedMeta = document.querySelector('meta[name="entity-updated"]'); if (updatedMeta) { reiheUpdated = updatedMeta.content; } } // Person detail page: /person/{id} (but not /person/new or /person/{id}/edit) const personMatch = path.match(/^\/person\/([^\/]+)\/?$/); if (personMatch && personMatch[1] !== "new") { hasPerson = true; personId = personMatch[1]; } // Almanach detail page: /almanach/{id} (but not /almanach-new or /almanach/{id}/edit) const almanachMatch = path.match(/^\/almanach\/([^\/]+)\/?$/); if (almanachMatch && almanachMatch[1] !== "new") { hasEntry = true; entryId = almanachMatch[1]; // Try to get updated timestamp from page const updatedMeta = document.querySelector('meta[name="entity-updated"]'); if (updatedMeta) { entryUpdated = updatedMeta.content; } } // Try to find CSRF token from page forms let csrfToken = ""; const csrfInput = document.querySelector('input[name="csrf_token"]'); if (csrfInput) { csrfToken = csrfInput.value; } const hasCsrf = csrfToken !== ""; this.hasContext = hasReihe || hasPerson || hasEntry; // Build half-open menu content let halfOpenContent = ""; if (hasReihe) { const deleteButton = hasCsrf ? ` ` : ""; halfOpenContent = `
Reihe
Bearbeiten ${deleteButton} `; } else if (hasPerson) { halfOpenContent = `
Person
Bearbeiten `; } else if (hasEntry) { const deleteButton = hasCsrf ? ` ` : ""; halfOpenContent = `
Almanach
Bearbeiten ${deleteButton} `; } // Build full menu content const createSection = isAdminOrEditor ? `
Erstellen
Neuer Band
Neue Reihe
Neuer Ort
Neue Person
` : ""; const listenSection = isAdminOrEditor ? `
Listen
Reihen
Orte
Personen
` : ""; const adminSection = isAdmin ? `
Administration
Nutzer einladen
Benutzerverwaltung
` : ""; // Build delete dialogs (only if CSRF token is available) let deleteDialogs = ""; if (hasReihe && hasCsrf) { deleteDialogs += ` `; } if (hasEntry && hasCsrf) { deleteDialogs += ` `; } // Build the unified menu content const contextualSection = halfOpenContent ? halfOpenContent : ""; const contextualDivider = halfOpenContent ? '
' : ""; // Insert HTML into light DOM this.innerHTML = `
${deleteDialogs}
`; // Get elements this._button = this.querySelector(".fab-button"); this._icon = this.querySelector(".fab-icon"); this._menu = this.querySelector(".fab-menu"); this._fullContent = this.querySelector(".fab-full-content"); // Initialize state: if we have context, start half-open, otherwise closed this.state = this.hasContext ? "half" : "closed"; this.setState(this.state); // Add event listeners this._button.addEventListener("click", this.handleClick); document.addEventListener("click", this.handleClickAway); // Delete button handlers const deleteButtons = this.querySelectorAll('[data-action^="delete-"]'); deleteButtons.forEach((btn) => { btn.addEventListener("click", this.handleDeleteClick); }); // Cancel delete handlers const cancelButtons = this.querySelectorAll('[data-action="cancel-delete"]'); cancelButtons.forEach((btn) => { btn.addEventListener("click", () => { this.closeAllDialogs(); }); }); } disconnectedCallback() { this._button.removeEventListener("click", this.handleClick); document.removeEventListener("click", this.handleClickAway); } handleClick(e) { e.stopPropagation(); this.nextState(); } handleClickAway(e) { if (!this.contains(e.target)) { this.setState("closed"); } } handleDeleteClick(e) { const action = e.currentTarget.getAttribute("data-action"); const entityType = action.replace("delete-", ""); const dialog = this.querySelector(`[data-dialog="${entityType}"]`); if (dialog) { dialog.classList.remove("hidden"); } } closeAllDialogs() { const dialogs = this.querySelectorAll(".fab-delete-dialog"); dialogs.forEach((dialog) => dialog.classList.add("hidden")); } nextState() { if (this.state === "closed") { this.setState(this.hasContext ? "half" : "full"); } else if (this.state === "half") { this.setState("full"); } else { this.setState("closed"); } } setState(newState) { this.state = newState; // Update menu visibility with animations if (newState === "closed") { // Fade out and slide down this._menu.style.opacity = "0"; this._menu.style.transform = "translateY(8px)"; // Collapse full content this._fullContent.style.maxHeight = "0"; this._fullContent.style.opacity = "0"; // After animation, hide completely setTimeout(() => { if (this.state === "closed") { this._menu.classList.add("hidden"); } }, 200); // Button state: closed - menu icon this._icon.classList.remove("ri-arrow-up-s-line", "ri-close-line"); this._icon.classList.add("ri-menu-line"); this._button.style.backgroundColor = ""; this._button.style.borderColor = ""; this._button.classList.remove("shadow-md"); this._button.classList.add("shadow-sm"); } else if (newState === "half") { // Show menu, fade in and slide up this._menu.classList.remove("hidden"); // Force reflow for animation this._menu.offsetHeight; this._menu.style.opacity = "1"; this._menu.style.transform = "translateY(0)"; // Keep full content collapsed this._fullContent.style.maxHeight = "0"; this._fullContent.style.opacity = "0"; // Button state: half-open - arrow up icon (can expand more) this._icon.classList.remove("ri-menu-line", "ri-close-line"); this._icon.classList.add("ri-arrow-up-s-line"); this._button.style.backgroundColor = "rgb(51 65 85)"; // slate-700 this._button.style.borderColor = "rgb(71 85 105)"; // slate-600 this._button.classList.remove("shadow-sm"); this._button.classList.add("shadow-md"); } else if (newState === "full") { // Menu visible, ensure it's shown this._menu.classList.remove("hidden"); this._menu.style.opacity = "1"; this._menu.style.transform = "translateY(0)"; // Expand full content with animation // Calculate the natural height this._fullContent.style.maxHeight = "none"; const height = this._fullContent.scrollHeight; this._fullContent.style.maxHeight = "0"; // Force reflow this._fullContent.offsetHeight; // Animate to full height this._fullContent.style.maxHeight = height + "px"; this._fullContent.style.opacity = "1"; // Button state: full-open - close icon this._icon.classList.remove("ri-menu-line", "ri-arrow-up-s-line"); this._icon.classList.add("ri-close-line"); this._button.style.backgroundColor = "rgb(30 41 59)"; // slate-800 this._button.style.borderColor = "rgb(51 65 85)"; // slate-700 this._button.classList.remove("shadow-sm"); this._button.classList.add("shadow-md"); } } }