+Fab menu, littered edit buttons

This commit is contained in:
Simon Martens
2026-01-09 18:34:59 +01:00
parent 80c28eca4e
commit 257bde5563
17 changed files with 981 additions and 377 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -22,6 +22,8 @@
{{ template "_footer" . }}
{{ template "_fab" . }}
{{ block "scripts" . }}
<!-- Default scripts... -->
{{ end }}

View File

@@ -0,0 +1,10 @@
{{- if .request.user -}}
<fab-menu
data-user-name="{{ if .request.user.Name }}{{ .request.user.Name }}{{ end }}"
data-user-email="{{ .request.user.Email }}"
data-user-id="{{ .request.user.Id }}"
data-is-admin-or-editor="{{ if (IsAdminOrEditor .request.user) }}true{{ else }}false{{ end }}"
data-is-admin="{{ if eq .request.user.Role "Admin" }}true{{ else }}false{{ end }}"
data-redirect-path="{{ .request.fullpath }}"
></fab-menu>
{{- end -}}

View File

@@ -1,37 +1,5 @@
{{- $date := Today -}}
<footer id="footer" class="container-normal pb-1.5 text-base text-gray-800 relative" x-data="{ openusermenu: false }">
<!-- INFO: User menu -->
{{- if .request.user -}}
<div class="" x-show="openusermenu">
<div
class="absolute right-0 bottom-10 bg-white border-gray-300 rounded-md shadow mt-2
[&>a]:no-underline [&>a]:text-gray-700 [&>a]:hover:bg-gray-100 [&>a]:hover:text-gray-900
[&>a]:block [&>a]:px-3 [&>a]:py-2 [&>a]:text-sm [&>a]:w-full [&>a]:text-left
[&>a]:whitespace-nowrap [&>a]:transition-all [&>a]:duration-200 [&>a]:border-b
[&>a]:last:border-b-0">
<a :href="'/user/{{ .request.user.Id }}/edit?redirectTo=' + window.location" class="">
<i class="ri-user-3-line"></i>
Profil bearbeiten
</a>
{{ if and .request.user (eq .request.user.Role "Admin") }}
<a href="/user/management/access/User?redirectTo={{ .request.fullpath }}" class="">
<i class="ri-group-3-line"></i>
Nutzer einladen
</a>
<a href="/user/management?redirectTo={{ .request.fullpath }}" class="">
<i class="ri-group-2-line"></i>
Benuzterverwaltung
</a>
{{ end }}
<a href="/logout?redirectTo={{ .request.fullpath }}" class="">
<i class="ri-logout-box-line"></i>
Logout
</a>
</div>
</div>
{{- end -}}
<!-- END: User menu -->
<footer id="footer" class="container-normal pb-1.5 text-base text-gray-800 relative">
<!-- INFO: Actual Footer -->
<div class="mt-12 pt-3 flex flex-row justify-between">
<div>
@@ -52,24 +20,12 @@
<a href="https://github.com/Theodor-Springmann-Stiftung/musenalm">Code</a>
<span>&middot;</span>
{{ if .request.user }}
<button
class="inline-block cursor-pointer hover:shadow-lg hover:bg-gray-100 px-1.5
py-0.5 rounded-xs"
@click="openusermenu = !openusermenu"
x-bind:class="openusermenu ? 'bg-gray-100 shadow-lg' : ''">
<i class="ri-user-3-line"></i>
{{ if .request.user.Name }}
<b>{{ .request.user.Name }}</b>
{{ else }}
<b>{{ .request.user.Email }}</b>
{{ end }}
<i class="ri-arrow-up-s-fill"></i>
</button>
<!--
|
<i class="ri-logout-box-line"></i>
<a href="/logout?redirectTo={{ .request.fullpath }}">Logout</a>
-->
<i class="ri-user-3-line"></i>
{{ if .request.user.Name }}
<b>{{ .request.user.Name }}</b>
{{ else }}
<b>{{ .request.user.Email }}</b>
{{ end }}
{{ else }}
<i class="ri-login-box-line"></i>
<a href="/login?redirectTo={{ .request.fullpath }}">Login</a>

View File

@@ -25,6 +25,8 @@
{{ template "_footer" . }}
{{ template "_fab" . }}
{{ block "scripts" . }}
<!-- Default scripts... -->
{{ end }}

View File

@@ -214,7 +214,8 @@ type AlmanachResult struct {
<div class="inputwrapper {{ if eq $model.result.Entry.ResponsibilityStmt "" }}hidden{{ end }}" data-dm-target="publication">
<div class="flex flex-row justify-between">
<label for="responsibility_statement" class="inputlabel menu-label">Autorangabe</label>
<label for="responsibility_statement" class="inputlabel
menu-label">Herausgeberangabe</label>
<div class="pr-2">
<button class="dm-close-button font-bold input-label">
<i class="ri-close-line"></i>

View File

@@ -4,6 +4,7 @@
{{ $showidseries := index . 3 }}
{{ $marka := index . 4 }}
{{ $markr := index . 5 }}
{{ $request := index . 6 }}
{{ $bds := index $relations $r.Id }}
@@ -27,10 +28,16 @@
</div>
{{ end }}
{{ end }}
<div class="font-sans py-0.5 text-sm">
<div class="font-sans py-0.5 text-sm flex flex-row gap-2">
<a href="/reihe/{{ $r.MusenalmID }}" class="no-underline rounded bg-stone-100 px-1.5">
<i class="ri-links-line"></i> Link
</a>
{{- if (IsAdminOrEditor $request.user) -}}
<a href="/reihe/{{ $r.MusenalmID }}/edit" class="no-underline rounded bg-stone-100 px-1.5">
<i class="ri-edit-line"></i>
Edit
</a>
{{- end -}}
</div>
</div>
<div class="grow lg:px-0 ml-3 lg:ml-8">
@@ -43,6 +50,6 @@
<span class="{{ if $marka }}reihen-text{{ end }}">{{ Safe $r.Annotation }}</span>
{{ end }}
</div>
{{ template "_reiherelations" (Arr $r $bds $entries false) }}
{{ template "_reiherelations" (Arr $r $bds $entries false $request) }}
</div>
</div>

View File

@@ -2,6 +2,7 @@
{{ $rels := index . 1 }}
{{ $entries := index . 2 }}
{{ $shownos := index . 3 }}
{{ $request := index . 4 }}
{{- if $rels -}}
<div class="reiherelations flex flex-col text-base font-sans w-full pt-1 -ml-3">
@@ -58,7 +59,13 @@
<div class="whitespace-nowrap align-top">
Alm
{{ $bd.MusenalmID }}
{{- if (IsAdminOrEditor $request.user) -}}
<a href="/almanach/{{ $bd.MusenalmID }}/edit" class="no-underline rounded bg-stone-100 px-1.5">
<i class="ri-edit-line"></i>
</a>
{{- end -}}
</div>
</div>
{{- end -}}

View File

@@ -30,6 +30,7 @@
</div>
<div class="container-normal font-serif mt-12">
<div class="flex inline-flex flex-inline gap-x-3"
{{ if $model.result.Agent.CorporateBody }}
<div class="font-sans">
<i class="ri-team-line"></i>
@@ -42,11 +43,13 @@
</div>
{{ end }}
{{- if (IsAdminOrEditor $model.request.user) -}}
<div class="font-sans mt-1">
<div class="font-sans">
&middot;&nbsp;
<i class="ri-edit-line"></i>
<a href="/person/{{ $model.result.Agent.Id }}/edit">Bearbeiten</a>
</div>
{{- end -}}
</div>
<h1 class="text-3xl font-bold">{{ $model.result.Agent.Name }}</h1>
{{- if $model.result.Agent.Pseudonyms -}}
<p class="italic">

View File

@@ -96,6 +96,14 @@
<div class="w-64 ml-4 shrink-0 {{ if $model.FTS -}}search-result{{- end -}}">
{{ $agent.References }}
</div>
{{- if (IsAdminOrEditor $model.request.user) -}}
<div class="font-sans">
<a href="/person/{{ $agent.Id}}/edit" class="no-underline hover:text-slate-950">
<i class="ri-edit-line"></i>
</a>
</div>
{{- end -}}
</div>
{{ end }}

View File

@@ -21,12 +21,4 @@
</span>
<span x-show="search"> Suche &middot; Alle Personen &amp; Körperschaften </span>
</h1>
{{- if (IsAdminOrEditor $model.request.user) -}}
<div class="ml-24 -mt-4">
<a href="/personen/new/" class="inline-flex items-center gap-2 text-sm font-bold text-gray-700 hover:text-slate-950 no-underline">
<i class="ri-add-line"></i>
<span>Neu</span>
</a>
</div>
{{- end -}}
</div>

View File

@@ -17,7 +17,8 @@
</div>
<div class="container-normal flex flex-col font-serif mt-12">
<div class="font-sans">
<div class="flex inline-flex flex-inline gap-x-3">
<div class="font-sans">
{{/* <svg
class="w-[0.9rem] h-[0.9rem] relative bottom-[0.04rem] inline-block"
width="65px"
@@ -42,13 +43,15 @@
*/}}
<i class="ri-book-shelf-fill"></i>
Reihe
</div>
</div>
{{- if (IsAdminOrEditor $model.request.user) -}}
<div class="font-sans mt-1">
<div class="font-sans">
&middot;&nbsp;
<i class="ri-edit-line"></i>
<a href="/reihe/{{ $r.MusenalmID }}/edit">Bearbeiten</a>
</div>
{{- end -}}
</div>
<div class="grow-0">
<div>
<span class="font-bold text-3xl mr-2">{{ $r.Title }}</span>
@@ -65,7 +68,8 @@
</div>
{{ end }}
<div class="max-w-[64rem] [&_*]:!text-lg mt-6">
{{ template "_reiherelations" (Arr $r $model.relations $model.entries true) }}
{{ template "_reiherelations" (Arr $r $model.relations $model.entries true
$model.request) }}
</div>
</div>
</div>

View File

@@ -47,14 +47,6 @@
<!-- INFO: 2. Header -->
<div id="pageheading" class="headingcontainer">
<h1 class="heading">Bände nach Reihentiteln</h1>
{{- if (IsAdminOrEditor $model.request.user) -}}
<div class="mt-2">
<a href="/reihen/new/" class="inline-flex items-center gap-2 text-sm font-bold text-gray-700 hover:text-slate-950 no-underline">
<i class="ri-add-line"></i>
<span>Neu</span>
</a>
</div>
{{- end -}}
{{ template "notifier" $model }}
{{ if not (or .search .hidden) }}
@@ -83,7 +75,8 @@
{{ if and .search $model.result.IDSeries }}
<div class="mb-1 max-w-[60rem] hyphens-auto">
{{ range $id, $r := $model.result.IDSeries }}
{{ template "_reihe" (Arr $r $model.result.Entries $model.result.EntriesSeries true false false) }}
{{ template "_reihe" (Arr $r $model.result.Entries $model.result.EntriesSeries
true false false $model.request) }}
{{ end }}
</div>
{{ end }}
@@ -92,7 +85,7 @@
<div class="mb-1 max-w-[60rem] hyphens-auto">
{{ range $id, $r := $model.result.Series }}
{{ template "_reihe" (Arr $r $model.result.Entries $model.result.EntriesSeries false false
false)
false $model.request)
}}
{{ end }}
</div>
@@ -122,7 +115,8 @@
</div>
<div class="mb-1 max-w-[60rem] hyphens-auto">
{{ range $id, $r := $model.result.AltSeries }}
{{ template "_reihe" (Arr $r $model.result.Entries $model.result.EntriesSeries false true true) }}
{{ template "_reihe" (Arr $r $model.result.Entries $model.result.EntriesSeries
false true true $model.request) }}
{{ end }}
</div>
{{ end }}

View File

@@ -68,14 +68,6 @@
<div id="searchcontrol" class="container-normal">
{{- template "_heading" $model.parameters -}}
{{- if (IsAdminOrEditor $model.request.user) -}}
<div class="mt-2">
<a href="/almanach-new/" class="inline-flex items-center gap-2 text-sm font-bold text-gray-700 hover:text-slate-950 no-underline">
<i class="ri-add-line"></i>
<span>Neu</span>
</a>
</div>
{{- end -}}
<div id="searchform" class="border-l border-zinc-300 px-8 py-10 relative">
{{- if not $model.parameters.Extended -}}
{{- template "_musenalmidbox" Arr $model.parameters.AlmString "baende" -}}

397
views/transform/fab-menu.js Normal file
View File

@@ -0,0 +1,397 @@
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
? `
<button data-action="delete-reihe" data-id="${reiheId}" class="w-full flex items-center px-4 py-2 hover:bg-gray-100 transition-colors text-sm text-left">
<i class="ri-delete-bin-line text-base text-red-600 mr-2.5"></i>
<span class="text-red-600">Löschen</span>
</button>
`
: "";
halfOpenContent = `
<div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider">
Reihe
</div>
<a href="/reihe/${reiheId}/edit" class="flex items-center px-4 py-2 hover:bg-gray-100 transition-colors no-underline text-sm">
<i class="ri-edit-line text-base text-gray-700 mr-2.5"></i>
<span class="text-gray-900">Bearbeiten</span>
</a>
${deleteButton}
`;
} else if (hasPerson) {
halfOpenContent = `
<div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider">
Person
</div>
<a href="/person/${personId}/edit" class="flex items-center px-4 py-2 hover:bg-gray-100 transition-colors no-underline text-sm">
<i class="ri-edit-line text-base text-gray-700 mr-2.5"></i>
<span class="text-gray-900">Bearbeiten</span>
</a>
`;
} else if (hasEntry) {
const deleteButton = hasCsrf
? `
<button data-action="delete-almanach" data-id="${entryId}" class="w-full flex items-center px-4 py-2 hover:bg-gray-100 transition-colors text-sm text-left">
<i class="ri-delete-bin-line text-base text-red-600 mr-2.5"></i>
<span class="text-red-600">Löschen</span>
</button>
`
: "";
halfOpenContent = `
<div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider">
Almanach
</div>
<a href="/almanach/${entryId}/edit" class="flex items-center px-4 py-2 hover:bg-gray-100 transition-colors no-underline text-sm">
<i class="ri-edit-line text-base text-gray-700 mr-2.5"></i>
<span class="text-gray-900">Bearbeiten</span>
</a>
${deleteButton}
`;
}
// Build full menu content
const createSection = isAdminOrEditor
? `
<div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider">
Erstellen
</div>
<a href="/almanach-new/" class="flex items-center px-4 py-2 hover:bg-gray-100 transition-colors no-underline text-sm">
<i class="ri-book-line text-base text-gray-700 mr-2.5"></i>
<span class="text-gray-900">Neuer Band</span>
</a>
<a href="/reihen/new/" class="flex items-center px-4 py-2 hover:bg-gray-100 transition-colors no-underline text-sm">
<i class="ri-stack-line text-base text-gray-700 mr-2.5"></i>
<span class="text-gray-900">Neue Reihe</span>
</a>
<a href="/orte/new/" class="flex items-center px-4 py-2 hover:bg-gray-100 transition-colors no-underline text-sm">
<i class="ri-map-pin-line text-base text-gray-700 mr-2.5"></i>
<span class="text-gray-900">Neuer Ort</span>
</a>
<a href="/personen/new/" class="flex items-center px-4 py-2 hover:bg-gray-100 transition-colors no-underline text-sm">
<i class="ri-group-line text-base text-gray-700 mr-2.5"></i>
<span class="text-gray-900">Neue Person</span>
</a>
<div class="border-t border-gray-200 my-1"></div>
`
: "";
const adminSection = isAdmin
? `
<div class="px-3 py-1.5 text-xs font-semibold text-gray-500 uppercase tracking-wider">
Administration
</div>
<a href="/user/management/access/User?redirectTo=${redirectPath}" class="flex items-center px-4 py-2 hover:bg-gray-100 transition-colors no-underline text-sm">
<i class="ri-group-3-line text-base text-gray-700 mr-2.5"></i>
<span class="text-gray-900">Nutzer einladen</span>
</a>
<a href="/user/management?redirectTo=${redirectPath}" class="flex items-center px-4 py-2 hover:bg-gray-100 transition-colors no-underline text-sm">
<i class="ri-group-2-line text-base text-gray-700 mr-2.5"></i>
<span class="text-gray-900">Benutzerverwaltung</span>
</a>
<div class="border-t border-gray-200 my-1"></div>
`
: "";
// Build delete dialogs (only if CSRF token is available)
let deleteDialogs = "";
if (hasReihe && hasCsrf) {
deleteDialogs += `
<div class="fab-delete-dialog hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" data-dialog="reihe">
<div class="bg-white rounded-lg p-6 max-w-md mx-4">
<h3 class="text-lg font-semibold mb-4">Reihe löschen?</h3>
<p class="text-gray-700 mb-6">Möchten Sie diese Reihe wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.</p>
<div class="flex gap-3 justify-end">
<button data-action="cancel-delete" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded transition-colors">
Abbrechen
</button>
<form method="POST" action="/reihe/${reiheId}/edit/delete">
<input type="hidden" name="csrf_token" value="${csrfToken}">
<input type="hidden" name="last_edited" value="${reiheUpdated}">
<button type="submit" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded transition-colors">
Löschen
</button>
</form>
</div>
</div>
</div>
`;
}
if (hasEntry && hasCsrf) {
deleteDialogs += `
<div class="fab-delete-dialog hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" data-dialog="almanach">
<div class="bg-white rounded-lg p-6 max-w-md mx-4">
<h3 class="text-lg font-semibold mb-4">Almanach-Eintrag löschen?</h3>
<p class="text-gray-700 mb-6">Möchten Sie diesen Eintrag wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.</p>
<div class="flex gap-3 justify-end">
<button data-action="cancel-delete" class="px-4 py-2 bg-gray-200 hover:bg-gray-300 rounded transition-colors">
Abbrechen
</button>
<form method="POST" action="/almanach/${entryId}/edit/delete">
<input type="hidden" name="csrf_token" value="${csrfToken}">
<input type="hidden" name="last_edited" value="${entryUpdated}">
<button type="submit" class="px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded transition-colors">
Löschen
</button>
</form>
</div>
</div>
</div>
`;
}
// Build the unified menu content
const contextualSection = halfOpenContent ? halfOpenContent : "";
const contextualDivider = halfOpenContent ? '<div class="border-t border-gray-200"></div>' : "";
// Insert HTML into light DOM
this.innerHTML = `
<div class="fixed bottom-12 left-8 z-50">
<!-- Unified Menu Container -->
<div class="fab-menu hidden absolute bottom-16 left-0 w-64 bg-white rounded border border-gray-300 shadow transition-all duration-200 ease-out">
<!-- Contextual actions (always at top when present) -->
${contextualSection}
${contextualDivider}
<!-- Rest of menu (hidden in half state, shown in full state) -->
<div class="fab-full-content overflow-hidden transition-all duration-300 ease-in-out" style="max-height: 0; opacity: 0;">
${createSection}
${adminSection}
<div class="px-4 py-2">
<div class="font-semibold text-gray-900 text-sm">${userName}</div>
<div class="text-xs text-gray-600 truncate">${userEmail}</div>
</div>
<a href="/user/${userId}/edit?redirectTo=${encodeURIComponent(window.location.href)}" class="flex items-center px-4 py-2 hover:bg-gray-100 transition-colors no-underline text-sm">
<i class="ri-user-3-line text-base text-gray-700 mr-2.5"></i>
<span class="text-gray-900">Profil bearbeiten</span>
</a>
<a href="/logout?redirectTo=${redirectPath}" class="flex items-center px-4 py-2 hover:bg-gray-100 transition-colors no-underline text-sm">
<i class="ri-logout-box-line text-base text-gray-700 mr-2.5 mb-1"></i>
<span class="text-gray-900">Logout</span>
</a>
</div>
</div>
<!-- FAB Button -->
<button class="fab-button w-12 h-12 bg-slate-700 hover:bg-slate-800 text-white rounded border-2 border-slate-600 shadow-sm transition-all duration-200 flex items-center justify-center" aria-label="Menü">
<i class="fab-icon text-2xl transition-all duration-200 ri-menu-line"></i>
</button>
${deleteDialogs}
</div>
`;
// 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");
}
}
}

View File

@@ -19,8 +19,10 @@ import { SingleSelectRemote } from "./single-select-remote.js";
import { AlmanachEditPage } from "./almanach-edit.js";
import { RelationsEditor } from "./relations-editor.js";
import { EditPage } from "./edit-page.js";
import { FabMenu } from "./fab-menu.js";
const FILTER_LIST_ELEMENT = "filter-list";
const FAB_MENU_ELEMENT = "fab-menu";
const SCROLL_BUTTON_ELEMENT = "scroll-button";
const TOOLTIP_ELEMENT = "tool-tip";
const ABBREV_TOOLTIPS_ELEMENT = "abbrev-tooltips";
@@ -57,6 +59,7 @@ customElements.define(ITEMS_EDITOR_ELEMENT, ItemsEditor);
customElements.define(ALMANACH_EDIT_PAGE_ELEMENT, AlmanachEditPage);
customElements.define(RELATIONS_EDITOR_ELEMENT, RelationsEditor);
customElements.define(EDIT_PAGE_ELEMENT, EditPage);
customElements.define(FAB_MENU_ELEMENT, FabMenu);
function PathPlusQuery() {
const path = window.location.pathname;
@@ -426,4 +429,5 @@ export {
AlmanachEditPage,
RelationsEditor,
EditPage,
FabMenu,
};