feat: breadcrumb hover Notion-style + nav_workspace_id sur toutes les pages
- _header.html: remplace click par hover (mouseenter/mouseleave)
avec timer 200ms anti-flicker, cascade sous-menus au hover
- dashboard.py: ajoute nav_workspace_id à trash, library, workspace,
gitea-workspace, local-workspace, workspaces, settings, pages/{id}
- La section du popover affiche 'Workspaces' pour Home, 'Private' pour le reste
- Les clics dans les popovers ferment le menu + naviguent
This commit is contained in:
+57
-21
@@ -48,7 +48,8 @@
|
||||
|
||||
<script type="application/json" id="fd-breadcrumb-data">{{ {"workspace_id": nav_workspace_id|default(0), "crumbs": ns.items}|tojson }}</script>
|
||||
|
||||
<div class="topbar-left header-breadcrumb" x-data="fdBreadcrumb()" x-init="init()" @click.outside="closeAll()">
|
||||
<div class="topbar-left header-breadcrumb" x-data="fdBreadcrumb()" x-init="init()"
|
||||
@mouseleave="dragCloseTimer()">
|
||||
{% if page_icon %}
|
||||
<span class="header-page-icon">{{ page_icon }}</span>
|
||||
{% endif %}
|
||||
@@ -57,11 +58,15 @@
|
||||
<span class="crumb-cell">
|
||||
{# ── Collapsed "…" segment ── #}
|
||||
<template x-if="crumb.ellipsis">
|
||||
<span class="crumb-anchor">
|
||||
<button type="button" class="breadcrumb-link crumb-ellipsis" @click.stop="toggleEllipsis()">…</button>
|
||||
<div class="fd-nav-menu" x-show="ellipsisOpen" x-cloak x-transition.opacity>
|
||||
<span class="crumb-anchor"
|
||||
@mouseenter="cancelCloseTimer(); ellipsisOpen = true"
|
||||
@mouseleave="dragCloseTimer()">
|
||||
<button type="button" class="breadcrumb-link crumb-ellipsis">…</button>
|
||||
<div class="fd-nav-menu" x-show="ellipsisOpen" x-cloak x-transition.opacity
|
||||
@mouseenter="cancelCloseTimer()"
|
||||
@mouseleave="dragCloseTimer()">
|
||||
<template x-for="h in crumb.hidden" :key="h.id">
|
||||
<div class="fd-nav-item" @click.stop="go(h.url)">
|
||||
<div class="fd-nav-item" @click.stop="go(h.url); closeAll()">
|
||||
<span class="fd-nav-ico" x-text="h.icon || '📄'"></span>
|
||||
<span class="fd-nav-label" x-text="h.label"></span>
|
||||
</div>
|
||||
@@ -72,23 +77,29 @@
|
||||
|
||||
{# ── Plain link crumb (Home / non-menu with url) ── #}
|
||||
<template x-if="!crumb.ellipsis && !crumb.menu && crumb.url">
|
||||
<a class="breadcrumb-link" :href="crumb.url" x-text="crumb.label"></a>
|
||||
<a class="breadcrumb-link" :href="crumb.url" x-text="crumb.label"
|
||||
@mouseenter="closeAll()"></a>
|
||||
</template>
|
||||
|
||||
{# ── Plain current crumb (no menu, no url) ── #}
|
||||
<template x-if="!crumb.ellipsis && !crumb.menu && !crumb.url">
|
||||
<span class="breadcrumb-current" x-text="crumb.label"></span>
|
||||
<span class="breadcrumb-current" x-text="crumb.label"
|
||||
@mouseenter="closeAll()"></span>
|
||||
</template>
|
||||
|
||||
{# ── Interactive crumb with navigation dropdown ── #}
|
||||
{# ── Interactive crumb with navigation dropdown (Notion-style hover) ── #}
|
||||
<template x-if="!crumb.ellipsis && crumb.menu">
|
||||
<span class="crumb-anchor">
|
||||
<span class="crumb-anchor"
|
||||
@mouseenter="cancelCloseTimer(); openMenu(crumb.origIndex)"
|
||||
@mouseleave="dragCloseTimer()">
|
||||
<button type="button" class="breadcrumb-link"
|
||||
:class="{ 'breadcrumb-current': crumb.origIndex === crumbs.length - 1 }"
|
||||
@click.stop="openMenu(crumb.origIndex)"
|
||||
x-text="crumb.label"></button>
|
||||
<div class="fd-nav-menu" x-show="openIdx === crumb.origIndex" x-cloak x-transition.opacity>
|
||||
<div class="fd-nav-section">Private</div>
|
||||
<div class="fd-nav-menu"
|
||||
x-show="openIdx === crumb.origIndex" x-cloak x-transition.opacity
|
||||
@mouseenter="cancelCloseTimer()"
|
||||
@mouseleave="dragCloseTimer()">
|
||||
<div class="fd-nav-section" x-text="crumb.home ? 'Workspaces' : 'Private'"></div>
|
||||
<template x-if="loading">
|
||||
<div class="fd-nav-empty">Loading…</div>
|
||||
</template>
|
||||
@@ -98,13 +109,15 @@
|
||||
<template x-for="item in activeItems" :key="item.id">
|
||||
<div class="fd-nav-item"
|
||||
@mouseenter="openSub(item)"
|
||||
@click.stop="go(item.url)">
|
||||
@click.stop="go(item.url); closeAll()">
|
||||
<span class="fd-nav-ico" x-text="item.icon || '📄'"></span>
|
||||
<span class="fd-nav-label" x-text="item.name"></span>
|
||||
<span class="fd-nav-arrow" x-show="item.has_children">›</span>
|
||||
<div class="fd-nav-menu fd-nav-submenu" x-show="subOpenId === item.id" x-cloak>
|
||||
<div class="fd-nav-menu fd-nav-submenu" x-show="subOpenId === item.id" x-cloak
|
||||
@mouseenter="cancelCloseTimer()"
|
||||
@mouseleave="dragCloseTimer()">
|
||||
<template x-for="s in subItems" :key="s.id">
|
||||
<div class="fd-nav-item" @click.stop="go(s.url)">
|
||||
<div class="fd-nav-item" @click.stop="go(s.url); closeAll()">
|
||||
<span class="fd-nav-ico" x-text="s.icon || '📄'"></span>
|
||||
<span class="fd-nav-label" x-text="s.name"></span>
|
||||
<span class="fd-nav-arrow" x-show="s.has_children">›</span>
|
||||
@@ -142,6 +155,8 @@ document.addEventListener('alpine:init', function () {
|
||||
activeItems: [],
|
||||
subOpenId: null,
|
||||
subItems: [],
|
||||
closeTimer: null,
|
||||
|
||||
init: function () {
|
||||
var el = document.getElementById('fd-breadcrumb-data');
|
||||
if (el) {
|
||||
@@ -152,6 +167,7 @@ document.addEventListener('alpine:init', function () {
|
||||
} catch (e) { this.crumbs = []; }
|
||||
}
|
||||
},
|
||||
|
||||
get display() {
|
||||
var c = this.crumbs;
|
||||
if (c.length <= 4) return c;
|
||||
@@ -160,11 +176,13 @@ document.addEventListener('alpine:init', function () {
|
||||
var tail = [c[c.length - 2], c[c.length - 1]];
|
||||
return head.concat([{ ellipsis: true, hidden: hidden }], tail);
|
||||
},
|
||||
|
||||
parentIdFor: function (origIndex) {
|
||||
if (origIndex <= 0) return null;
|
||||
var prev = this.crumbs[origIndex - 1];
|
||||
return prev && prev.id ? prev.id : null;
|
||||
},
|
||||
|
||||
fetchMenu: function (parentId) {
|
||||
var key = parentId == null ? 'root' : String(parentId);
|
||||
var self = this;
|
||||
@@ -177,10 +195,13 @@ document.addEventListener('alpine:init', function () {
|
||||
.then(function (d) { self.cache[key] = d.items || []; return self.cache[key]; })
|
||||
.catch(function () { return []; });
|
||||
},
|
||||
|
||||
openMenu: function (origIndex) {
|
||||
if (this.openIdx === origIndex) return; // already open
|
||||
var self = this;
|
||||
if (this.openIdx === origIndex) { this.closeAll(); return; }
|
||||
this.closeAll();
|
||||
// Close sub first
|
||||
this.subOpenId = null;
|
||||
this.subItems = [];
|
||||
this.openIdx = origIndex;
|
||||
this.loading = true;
|
||||
this.fetchMenu(this.parentIdFor(origIndex)).then(function (items) {
|
||||
@@ -188,6 +209,7 @@ document.addEventListener('alpine:init', function () {
|
||||
self.loading = false;
|
||||
});
|
||||
},
|
||||
|
||||
openSub: function (item) {
|
||||
var self = this;
|
||||
if (!item.has_children) { this.subOpenId = null; this.subItems = []; return; }
|
||||
@@ -198,18 +220,32 @@ document.addEventListener('alpine:init', function () {
|
||||
if (self.subOpenId === item.id) { self.subItems = items; }
|
||||
});
|
||||
},
|
||||
toggleEllipsis: function () {
|
||||
var wasOpen = this.ellipsisOpen;
|
||||
this.closeAll();
|
||||
this.ellipsisOpen = !wasOpen;
|
||||
|
||||
// ── Hover close timer ──
|
||||
dragCloseTimer: function () {
|
||||
var self = this;
|
||||
this.cancelCloseTimer();
|
||||
this.closeTimer = setTimeout(function () {
|
||||
self.closeAll();
|
||||
}, 200);
|
||||
},
|
||||
|
||||
cancelCloseTimer: function () {
|
||||
if (this.closeTimer) {
|
||||
clearTimeout(this.closeTimer);
|
||||
this.closeTimer = null;
|
||||
}
|
||||
},
|
||||
|
||||
closeAll: function () {
|
||||
this.cancelCloseTimer();
|
||||
this.openIdx = null;
|
||||
this.ellipsisOpen = false;
|
||||
this.subOpenId = null;
|
||||
this.subItems = [];
|
||||
this.activeItems = [];
|
||||
},
|
||||
|
||||
go: function (url) { if (url) window.location.href = url; }
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user