feat: sidebar compact + menu contextuel (clic droit/long press) + boutons Expand/Collapse All
Sidebar workspace: - Arbre plus compact: padding réduit (4+12*depth au lieu de 8+16), gap 4px, min-height 24px, font 13px, chevron 12px - Boutons d'action (📄📁✏️🗑) remplacés par menu contextuel au clic droit (@contextmenu.prevent) et long-press mobile (500ms) - Menu: New File, New Folder (si dossier), Rename, Delete - Nom de l'item affiché en bas du menu Page workspace: - Boutons ⊞ (Expand All) et ⊟ (Collapse All) dans la toolbar - expandAll(): parcourt l'arbre, set expanded[id]=true, toggle DOM - collapseAll(): vide expanded, masque tous les <ul>
This commit is contained in:
+94
-9
@@ -116,14 +116,18 @@
|
|||||||
@dragover.prevent="dragOver($event, {{ page.db_id }}, {{ 'true' if page.is_folder else 'false' }})"
|
@dragover.prevent="dragOver($event, {{ page.db_id }}, {{ 'true' if page.is_folder else 'false' }})"
|
||||||
@dragleave="dragLeave($event)"
|
@dragleave="dragLeave($event)"
|
||||||
@drop.prevent="drop($event, {{ page.db_id }})"
|
@drop.prevent="drop($event, {{ page.db_id }})"
|
||||||
style="padding-left:{{ 8 + depth * 16 }}px;">
|
style="padding-left:{{ 4 + depth * 12 }}px;"
|
||||||
|
@contextmenu.prevent="showWsContext($event, {{ page.db_id }}, '{{ page.name | e }}', {{ 'true' if page.is_folder else 'false' }})"
|
||||||
|
@touchstart="wsTouchStart($event)"
|
||||||
|
@touchend="wsTouchEnd($event, {{ page.db_id }}, '{{ page.name | e }}', {{ 'true' if page.is_folder else 'false' }})"
|
||||||
|
@touchmove="wsTouchMove($event)">
|
||||||
{% if page.is_folder %}
|
{% if page.is_folder %}
|
||||||
<button class="tree-chevron"
|
<button class="tree-chevron"
|
||||||
@click.stop="toggleTreeFolder({{ page.db_id }})"
|
@click.stop="toggleTreeFolder({{ page.db_id }})"
|
||||||
:class="{ open: expandedFolders[{{ page.db_id }}] }"
|
:class="{ open: expandedFolders[{{ page.db_id }}] }"
|
||||||
title="Toggle folder">▶</button>
|
title="Toggle folder">▶</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span style="width:16px;flex-shrink:0;"></span>
|
<span style="width:12px;flex-shrink:0;"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<span class="page-icon">{{ page.icon }}</span>
|
<span class="page-icon">{{ page.icon }}</span>
|
||||||
{% if page.is_folder %}
|
{% if page.is_folder %}
|
||||||
@@ -134,13 +138,6 @@
|
|||||||
{% else %}
|
{% else %}
|
||||||
<a href="/pages/{{ page.db_id }}" class="page-name" draggable="false">{{ page.name }}</a>
|
<a href="/pages/{{ page.db_id }}" class="page-name" draggable="false">{{ page.name }}</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="sidebar-item-actions">
|
|
||||||
{% if page.is_folder %}
|
|
||||||
<button class="ws-act" @click.stop="newPageInFolder({{ page.db_id }})" title="New file in folder">📄</button>
|
|
||||||
<button class="ws-act" @click.stop="newFolderInFolder({{ page.db_id }})" title="New sub-folder">📁</button>
|
|
||||||
{% endif %}
|
|
||||||
<button class="ws-act danger" @click.stop="deleteWorkspacePage({{ page.db_id }})" title="Delete">×</button>
|
|
||||||
</div>
|
|
||||||
</li>
|
</li>
|
||||||
{% if page.children %}
|
{% if page.children %}
|
||||||
<li x-show="expandedFolders[{{ page.db_id }}]" x-transition style="list-style:none;margin:0;padding:0;">
|
<li x-show="expandedFolders[{{ page.db_id }}]" x-transition style="list-style:none;margin:0;padding:0;">
|
||||||
@@ -446,6 +443,36 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ═══════════ WORKSPACE TREE CONTEXT MENU ═══════════ -->
|
||||||
|
<div class="context-menu" id="ws-context-menu"
|
||||||
|
:class="{ visible: wsCtx.visible }"
|
||||||
|
:style="'left:' + wsCtx.x + 'px; top:' + wsCtx.y + 'px'"
|
||||||
|
@click.away="wsCtx.visible = false">
|
||||||
|
<template x-if="wsCtx.isFolder">
|
||||||
|
<div class="context-menu-section">
|
||||||
|
<div class="context-menu-item" @click="wsCtxAction('new-file')">
|
||||||
|
<span class="menu-icon">📄</span> New File
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" @click="wsCtxAction('new-folder')">
|
||||||
|
<span class="menu-icon">📁</span> New Folder
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="context-menu-section">
|
||||||
|
<div class="context-menu-item" @click="wsCtxAction('rename')">
|
||||||
|
<span class="menu-icon">✏️</span> Rename
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-item" @click="wsCtxAction('delete')">
|
||||||
|
<span class="menu-icon">🗑</span> Delete
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="context-menu-section">
|
||||||
|
<div class="context-menu-item text-dim" style="font-size:11px;cursor:default;">
|
||||||
|
<span x-text="wsCtx.name"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ═══════════ TOOLTIP ═══════════ -->
|
<!-- ═══════════ TOOLTIP ═══════════ -->
|
||||||
<div class="tooltip" id="tooltip" x-ref="tooltip"></div>
|
<div class="tooltip" id="tooltip" x-ref="tooltip"></div>
|
||||||
|
|
||||||
@@ -492,6 +519,64 @@
|
|||||||
contextActions: {},
|
contextActions: {},
|
||||||
showNewPageMenu: false,
|
showNewPageMenu: false,
|
||||||
|
|
||||||
|
// ── Workspace tree context menu ──
|
||||||
|
wsCtx: { visible: false, x: 0, y: 0, id: null, name: '', isFolder: false },
|
||||||
|
wsTouchTimer: null,
|
||||||
|
showWsContext(ev, id, name, isFolder) {
|
||||||
|
this.wsCtx = { visible: true, x: ev.clientX, y: ev.clientY, id: id, name: name, isFolder: isFolder };
|
||||||
|
},
|
||||||
|
hideWsContext() { this.wsCtx.visible = false; },
|
||||||
|
wsTouchStart(ev) {
|
||||||
|
var self = this;
|
||||||
|
this.wsTouchTimer = setTimeout(function() {
|
||||||
|
self.wsTouchTimer = null;
|
||||||
|
var el = ev.target.closest('.ws-tree-item');
|
||||||
|
if (el) {
|
||||||
|
var id = parseInt(el.getAttribute('data-id'));
|
||||||
|
var name = el.getAttribute('data-name') || '';
|
||||||
|
var isFolder = el.getAttribute('data-is-folder') === 'true';
|
||||||
|
var touch = ev.touches[0];
|
||||||
|
self.wsCtx = { visible: true, x: touch.clientX, y: touch.clientY, id: id, name: name, isFolder: isFolder };
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
},
|
||||||
|
wsTouchEnd(ev, id, name, isFolder) {
|
||||||
|
if (this.wsTouchTimer) { clearTimeout(this.wsTouchTimer); this.wsTouchTimer = null; return; }
|
||||||
|
// Already opened by touchstart; prevent Alpine @touchend from also firing the context menu
|
||||||
|
ev.preventDefault();
|
||||||
|
},
|
||||||
|
wsTouchMove(ev) {
|
||||||
|
if (this.wsTouchTimer) { clearTimeout(this.wsTouchTimer); this.wsTouchTimer = null; }
|
||||||
|
},
|
||||||
|
wsCtxAction(action) {
|
||||||
|
var id = this.wsCtx.id;
|
||||||
|
var name = this.wsCtx.name;
|
||||||
|
this.wsCtx.visible = false;
|
||||||
|
if (!id) return;
|
||||||
|
switch(action) {
|
||||||
|
case 'new-file':
|
||||||
|
this.newPageInFolder(id);
|
||||||
|
break;
|
||||||
|
case 'new-folder':
|
||||||
|
this.newFolderInFolder(id);
|
||||||
|
break;
|
||||||
|
case 'rename':
|
||||||
|
var newName = prompt('Rename:', name);
|
||||||
|
if (newName && newName.trim()) {
|
||||||
|
fetch('/api/local-workspace/items/' + id, {
|
||||||
|
method: 'PUT',
|
||||||
|
headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
|
||||||
|
body: JSON.stringify({ name: newName.trim() })
|
||||||
|
}).then(function(r) { if (r.ok) window.location.reload(); });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'delete':
|
||||||
|
if (!confirm('Delete "' + name + '"?')) break;
|
||||||
|
this.deleteWorkspacePage(id);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// ── Favorites tracking ──
|
// ── Favorites tracking ──
|
||||||
favoriteIds: new Set([{% for p in favorite_pages %}'{{ p.id }}'{% if not loop.last %},{% endif %}{% endfor %}]),
|
favoriteIds: new Set([{% for p in favorite_pages %}'{{ p.id }}'{% if not loop.last %},{% endif %}{% endfor %}]),
|
||||||
|
|
||||||
|
|||||||
@@ -158,6 +158,8 @@
|
|||||||
📁 <span x-text="breadcrumb.length > 0 ? breadcrumb[breadcrumb.length-1].name : ''"></span>
|
📁 <span x-text="breadcrumb.length > 0 ? breadcrumb[breadcrumb.length-1].name : ''"></span>
|
||||||
</h1>
|
</h1>
|
||||||
<div class="ws-toolbar">
|
<div class="ws-toolbar">
|
||||||
|
<button class="btn-icon" @click="expandAll()" title="Expand all">⊞</button>
|
||||||
|
<button class="btn-icon" @click="collapseAll()" title="Collapse all">⊟</button>
|
||||||
<button class="btn-icon" @click="openModal('file')" title="New file">📄</button>
|
<button class="btn-icon" @click="openModal('file')" title="New file">📄</button>
|
||||||
<button class="btn-icon" @click="openModal('folder')" title="New folder">📁</button>
|
<button class="btn-icon" @click="openModal('folder')" title="New folder">📁</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -448,6 +450,41 @@ document.addEventListener('alpine:init', function() {
|
|||||||
window.location = '/local-workspace?folder=' + id;
|
window.location = '/local-workspace?folder=' + id;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// ── Expand / Collapse all ──
|
||||||
|
expandAll() {
|
||||||
|
var self = this;
|
||||||
|
this._walkTree(this.tree, function(n) {
|
||||||
|
if (n.is_folder && n.children && n.children.length > 0) {
|
||||||
|
self.expanded[n.id] = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.expanded = Object.assign({}, this.expanded);
|
||||||
|
// Toggle static HTML for renderChildren
|
||||||
|
this._toggleAllUl(this.$el, true);
|
||||||
|
},
|
||||||
|
collapseAll() {
|
||||||
|
this.expanded = {};
|
||||||
|
this._toggleAllUl(this.$el, false);
|
||||||
|
},
|
||||||
|
_walkTree(nodes, fn) {
|
||||||
|
for (var i=0; i<nodes.length; i++) {
|
||||||
|
fn(nodes[i]);
|
||||||
|
if (nodes[i].children) this._walkTree(nodes[i].children, fn);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
_toggleAllUl(el, show) {
|
||||||
|
var uls = el.querySelectorAll('ul.ws-tree');
|
||||||
|
for (var i=0; i<uls.length; i++) {
|
||||||
|
uls[i].style.display = show ? '' : 'none';
|
||||||
|
}
|
||||||
|
// Also toggle chevrons
|
||||||
|
var chevs = el.querySelectorAll('.ws-chevron');
|
||||||
|
for (var j=0; j<chevs.length; j++) {
|
||||||
|
if (show) chevs[j].classList.add('open');
|
||||||
|
else chevs[j].classList.remove('open');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
// ── Search / Filter ──
|
// ── Search / Filter ──
|
||||||
doFilter() {
|
doFilter() {
|
||||||
var q = (this.searchQuery || '').toLowerCase().trim();
|
var q = (this.searchQuery || '').toLowerCase().trim();
|
||||||
|
|||||||
+5
-5
@@ -321,15 +321,15 @@ input, select, textarea { font-family: inherit; color: inherit; }
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: var(--space-sm);
|
gap: 4px;
|
||||||
padding: 3px 6px;
|
padding: 2px 4px;
|
||||||
padding-left: 8px;
|
padding-left: 4px;
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
font-size: 14px;
|
font-size: 13px;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background var(--transition);
|
transition: background var(--transition);
|
||||||
min-height: 28px;
|
min-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-item:hover {
|
.sidebar-item:hover {
|
||||||
|
|||||||
Reference in New Issue
Block a user