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); } 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 = ""; let hasContent = false, contentId = "", contentEntryId = ""; let hasPage = false, pageKey = ""; // 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; } } // Content detail page: /beitrag/{id} const contentMatch = path.match(/^\/beitrag\/([^\/]+)\/?$/); if (contentMatch) { hasContent = true; contentId = contentMatch[1]; const entryLink = document.querySelector('#breadcrumbs a[href^="/almanach/"]'); if (entryLink) { const match = entryLink.getAttribute("href")?.match(/^\/almanach\/([^\/#]+)/); if (match) { contentEntryId = match[1]; } } } // Page views use page editor keys via meta tag or URL mapping const pageKeyMeta = document.querySelector('meta[name="page-key"]'); if (pageKeyMeta && pageKeyMeta.content) { hasPage = true; pageKey = pageKeyMeta.content; } else { const textPageMatch = path.match(/^\/redaktion\/([^\/]+)\/?$/); const textPageKey = textPageMatch ? textPageMatch[1] : ""; const knownPageKeys = new Set([ "kontakt", "danksagungen", "literatur", "einleitung", "benutzerhinweise", "lesekabinett", ]); if (textPageKey && knownPageKeys.has(textPageKey)) { hasPage = true; pageKey = textPageKey; } else if (path === "/" || path === "/index/") { hasPage = true; pageKey = "index"; } else if (path === "/reihen" || path === "/reihen/") { hasPage = true; pageKey = "reihen"; } } // 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 || hasContent || hasPage; // Build half-open menu content let halfOpenContent = ""; if (isAdminOrEditor && hasReihe) { halfOpenContent = `
Reihe
Bearbeiten `; } else if (isAdminOrEditor && hasPerson) { halfOpenContent = `
Person
Bearbeiten `; } else if (isAdminOrEditor && hasEntry) { halfOpenContent = `
Almanach
Bearbeiten Beiträge bearbeiten Neuer Beitrag `; } else if (isAdminOrEditor && hasContent && contentEntryId) { halfOpenContent = `
Beitrag
Bearbeiten Beiträge bearbeiten Neuer Beitrag `; } else if (isAdminOrEditor && hasPage) { halfOpenContent = `
Seite
Seite bearbeiten `; } // Build full menu content const createSection = isAdminOrEditor ? `
Erstellen
Neuer Band
Neue Reihe
Neuer Ort
Neue Person
` : ""; const listenSection = isAdminOrEditor ? `
Listen
Reihen
Orte
Personen
Abkürzungen
Seiten
` : ""; const adminSection = isAdmin ? `
Administration
Nutzer einladen
Benutzerverwaltung
` : ""; // Build the unified menu content const contextualSection = halfOpenContent ? halfOpenContent : ""; const contextualDivider = halfOpenContent ? '
' : ""; // Insert HTML into light DOM this.innerHTML = `
`; // 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); } 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"); } } 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"); } } }