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' }})"
|
||||
@dragleave="dragLeave($event)"
|
||||
@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 %}
|
||||
<button class="tree-chevron"
|
||||
@click.stop="toggleTreeFolder({{ page.db_id }})"
|
||||
:class="{ open: expandedFolders[{{ page.db_id }}] }"
|
||||
title="Toggle folder">▶</button>
|
||||
{% else %}
|
||||
<span style="width:16px;flex-shrink:0;"></span>
|
||||
<span style="width:12px;flex-shrink:0;"></span>
|
||||
{% endif %}
|
||||
<span class="page-icon">{{ page.icon }}</span>
|
||||
{% if page.is_folder %}
|
||||
@@ -134,13 +138,6 @@
|
||||
{% else %}
|
||||
<a href="/pages/{{ page.db_id }}" class="page-name" draggable="false">{{ page.name }}</a>
|
||||
{% 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>
|
||||
{% if page.children %}
|
||||
<li x-show="expandedFolders[{{ page.db_id }}]" x-transition style="list-style:none;margin:0;padding:0;">
|
||||
@@ -446,6 +443,36 @@
|
||||
</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 ═══════════ -->
|
||||
<div class="tooltip" id="tooltip" x-ref="tooltip"></div>
|
||||
|
||||
@@ -492,6 +519,64 @@
|
||||
contextActions: {},
|
||||
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 ──
|
||||
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>
|
||||
</h1>
|
||||
<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('folder')" title="New folder">📁</button>
|
||||
</div>
|
||||
@@ -448,6 +450,41 @@ document.addEventListener('alpine:init', function() {
|
||||
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 ──
|
||||
doFilter() {
|
||||
var q = (this.searchQuery || '').toLowerCase().trim();
|
||||
|
||||
+5
-5
@@ -321,15 +321,15 @@ input, select, textarea { font-family: inherit; color: inherit; }
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
padding: 3px 6px;
|
||||
padding-left: 8px;
|
||||
gap: 4px;
|
||||
padding: 2px 4px;
|
||||
padding-left: 4px;
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
color: var(--text-primary);
|
||||
cursor: pointer;
|
||||
transition: background var(--transition);
|
||||
min-height: 28px;
|
||||
min-height: 24px;
|
||||
}
|
||||
|
||||
.sidebar-item:hover {
|
||||
|
||||
Reference in New Issue
Block a user