mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2026-02-04 10:35:30 +00:00
+Fab menu, littered edit buttons
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -22,6 +22,8 @@
|
||||
|
||||
{{ template "_footer" . }}
|
||||
|
||||
{{ template "_fab" . }}
|
||||
|
||||
{{ block "scripts" . }}
|
||||
<!-- Default scripts... -->
|
||||
{{ end }}
|
||||
|
||||
10
views/layouts/components/_fab.gohtml
Normal file
10
views/layouts/components/_fab.gohtml
Normal 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 -}}
|
||||
@@ -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>·</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>
|
||||
|
||||
@@ -25,6 +25,8 @@
|
||||
|
||||
{{ template "_footer" . }}
|
||||
|
||||
{{ template "_fab" . }}
|
||||
|
||||
{{ block "scripts" . }}
|
||||
<!-- Default scripts... -->
|
||||
{{ end }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 -}}
|
||||
|
||||
|
||||
@@ -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">
|
||||
·
|
||||
<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">
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
|
||||
@@ -21,12 +21,4 @@
|
||||
</span>
|
||||
<span x-show="search"> Suche · Alle Personen & 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>
|
||||
|
||||
@@ -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">
|
||||
·
|
||||
<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>
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
397
views/transform/fab-menu.js
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user