feat: add action buttons and long-press support for vault tree items with mobile-friendly context menus
- Add action buttons (ellipsis icon) to vault, directory, and file tree items - Implement long-press gesture detection for mobile devices with 550ms delay and 10px movement threshold - Show action buttons on hover for desktop and always visible on mobile/touch devices - Position context menus near action buttons to prevent off-screen rendering - Prevent click events from firing after long-press ges
This commit is contained in:
parent
628a664c59
commit
db812a6176
@ -1963,6 +1963,8 @@
|
||||
const isReadonly = false;
|
||||
ContextMenuManager.show(e.clientX, e.clientY, v.name, '', 'vault', isReadonly);
|
||||
});
|
||||
attachTreeItemActionButton(vaultItem, v.name, "", "vault", false);
|
||||
attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: false }));
|
||||
|
||||
container.appendChild(vaultItem);
|
||||
|
||||
@ -2058,6 +2060,8 @@
|
||||
const isReadonly = false;
|
||||
ContextMenuManager.show(e.clientX, e.clientY, v.name, '', 'vault', isReadonly);
|
||||
});
|
||||
attachTreeItemActionButton(vaultItem, v.name, "", "vault", false);
|
||||
attachTreeItemLongPress(vaultItem, () => ({ vault: v.name, path: "", type: "vault", isReadonly: false }));
|
||||
|
||||
container.appendChild(vaultItem);
|
||||
|
||||
@ -2264,6 +2268,8 @@
|
||||
|
||||
if (item.type === "directory") {
|
||||
const dirItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon("chevron-right", 14), icon("folder", 16), document.createTextNode(` ${item.name} `), smallBadge(item.children_count)]);
|
||||
attachTreeItemActionButton(dirItem, vaultName, item.path, "directory", false);
|
||||
attachTreeItemLongPress(dirItem, () => ({ vault: vaultName, path: item.path, type: "directory", isReadonly: false }));
|
||||
fragment.appendChild(dirItem);
|
||||
|
||||
const subContainer = el("div", { class: "tree-children collapsed", id: `dir-${vaultName}-${item.path}` });
|
||||
@ -2296,6 +2302,8 @@
|
||||
const fileIconName = getFileIcon(item.name);
|
||||
const displayName = item.name.match(/\.md$/i) ? item.name.replace(/\.md$/i, "") : item.name;
|
||||
const fileItem = el("div", { class: "tree-item", "data-vault": vaultName, "data-path": item.path }, [icon(fileIconName, 16), document.createTextNode(` ${displayName}`)]);
|
||||
attachTreeItemActionButton(fileItem, vaultName, item.path, "file", false);
|
||||
attachTreeItemLongPress(fileItem, () => ({ vault: vaultName, path: item.path, type: "file", isReadonly: false }));
|
||||
fileItem.addEventListener("click", () => {
|
||||
scrollTreeItemIntoView(fileItem, false);
|
||||
openFile(vaultName, item.path);
|
||||
@ -4727,6 +4735,87 @@
|
||||
return s;
|
||||
}
|
||||
|
||||
function getContextMenuPositionFromElement(target) {
|
||||
const rect = target.getBoundingClientRect();
|
||||
return {
|
||||
x: Math.min(rect.right - 8, window.innerWidth - 16),
|
||||
y: Math.min(rect.top + rect.height / 2, window.innerHeight - 16),
|
||||
};
|
||||
}
|
||||
|
||||
function attachTreeItemActionButton(itemEl, vault, path, type, isReadonly) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = "tree-item-action-btn";
|
||||
button.setAttribute("aria-label", "Afficher le menu d’actions");
|
||||
button.setAttribute("title", "Actions");
|
||||
button.appendChild(icon("ellipsis", 16));
|
||||
button.addEventListener("click", (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
const pos = getContextMenuPositionFromElement(button);
|
||||
ContextMenuManager.show(pos.x, pos.y, vault, path, type, isReadonly);
|
||||
});
|
||||
itemEl.appendChild(button);
|
||||
}
|
||||
|
||||
function attachTreeItemLongPress(itemEl, getMenuData) {
|
||||
let pressTimer = null;
|
||||
let pressHandled = false;
|
||||
let startX = 0;
|
||||
let startY = 0;
|
||||
const longPressDelay = 550;
|
||||
const moveThreshold = 10;
|
||||
|
||||
const clearPressTimer = () => {
|
||||
if (pressTimer) {
|
||||
clearTimeout(pressTimer);
|
||||
pressTimer = null;
|
||||
}
|
||||
};
|
||||
|
||||
itemEl.addEventListener("touchstart", (e) => {
|
||||
if (!e.touches || e.touches.length !== 1) return;
|
||||
pressHandled = false;
|
||||
startX = e.touches[0].clientX;
|
||||
startY = e.touches[0].clientY;
|
||||
clearPressTimer();
|
||||
pressTimer = setTimeout(() => {
|
||||
const data = getMenuData();
|
||||
if (!data) return;
|
||||
pressHandled = true;
|
||||
ContextMenuManager.show(startX, startY, data.vault, data.path, data.type, data.isReadonly);
|
||||
}, longPressDelay);
|
||||
}, { passive: true });
|
||||
|
||||
itemEl.addEventListener("touchmove", (e) => {
|
||||
if (!e.touches || e.touches.length !== 1) return;
|
||||
const dx = Math.abs(e.touches[0].clientX - startX);
|
||||
const dy = Math.abs(e.touches[0].clientY - startY);
|
||||
if (dx > moveThreshold || dy > moveThreshold) {
|
||||
clearPressTimer();
|
||||
}
|
||||
}, { passive: true });
|
||||
|
||||
itemEl.addEventListener("touchend", () => {
|
||||
clearPressTimer();
|
||||
}, { passive: true });
|
||||
|
||||
itemEl.addEventListener("touchcancel", () => {
|
||||
clearPressTimer();
|
||||
}, { passive: true });
|
||||
|
||||
itemEl.addEventListener("click", (e) => {
|
||||
if (pressHandled) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setTimeout(() => {
|
||||
pressHandled = false;
|
||||
}, 0);
|
||||
}
|
||||
}, true);
|
||||
}
|
||||
|
||||
function getVaultIcon(vaultName, size = 16) {
|
||||
const v = allVaults.find((val) => val.name === vaultName);
|
||||
const type = v ? v.type : "VAULT";
|
||||
|
||||
@ -837,6 +837,9 @@ select {
|
||||
background: var(--bg-hover);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
.tree-item > .badge-small {
|
||||
margin-left: auto;
|
||||
}
|
||||
.tree-item.active {
|
||||
background: var(--bg-hover);
|
||||
color: var(--accent);
|
||||
@ -880,6 +883,44 @@ select {
|
||||
color: var(--accent-green);
|
||||
}
|
||||
|
||||
.tree-item-action-btn {
|
||||
margin-left: auto;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
transition:
|
||||
opacity 140ms ease,
|
||||
color 140ms ease,
|
||||
background 140ms ease,
|
||||
border-color 140ms ease;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tree-item-action-btn:hover,
|
||||
.tree-item-action-btn:focus-visible {
|
||||
background: color-mix(in srgb, var(--accent) 10%, transparent);
|
||||
border-color: color-mix(in srgb, var(--accent) 25%, var(--border));
|
||||
color: var(--accent);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.tree-item:hover .tree-item-action-btn,
|
||||
.tree-item:focus-within .tree-item-action-btn,
|
||||
.tree-item.path-selected .tree-item-action-btn,
|
||||
.tree-item.active .tree-item-action-btn {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.tree-children {
|
||||
padding-left: 16px;
|
||||
overflow: hidden;
|
||||
@ -903,6 +944,13 @@ select {
|
||||
padding: 4px 0 10px 12px;
|
||||
}
|
||||
|
||||
@media (hover: none), (pointer: coarse), (max-width: 768px) {
|
||||
.tree-item-action-btn {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-result-item {
|
||||
align-items: flex-start;
|
||||
white-space: normal;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user