From 85ea26b57a0f882037708679aaaa9f4ccb017fee Mon Sep 17 00:00:00 2001 From: Yuri Karamian Date: Thu, 25 Jun 2026 13:29:24 +0200 Subject: [PATCH] Notebook reorder: use pointer events via a drag handle so manual sort works on Windows and Android --- src/lib/components/Sidebar.svelte | 104 ++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 5 deletions(-) diff --git a/src/lib/components/Sidebar.svelte b/src/lib/components/Sidebar.svelte index 22019e5..e89ddbf 100644 --- a/src/lib/components/Sidebar.svelte +++ b/src/lib/components/Sidebar.svelte @@ -53,6 +53,7 @@ let dropTargetPath = $state(null); let dropPosition = $state<'above' | 'into' | 'below' | null>(null); let draggedNotebookPath = $state(null); + let nbPointer: { id: number; src: string } | null = null; // Apply manual ordering recursively when in 'manual' sort mode. // In 'alphabetical' mode the Rust scanner already sorts, so we pass through unchanged. @@ -360,9 +361,13 @@ async function handleNotebookDrop(e: DragEvent, destPath: string) { e.preventDefault(); - dropTargetPath = null; const srcPath = e.dataTransfer?.getData('text/plain'); - if (!srcPath) return; + if (srcPath) await nestNotebook(srcPath, destPath); + } + + async function nestNotebook(srcPath: string, destPath: string) { + dropTargetPath = null; + dropPosition = null; draggedNotebookPath = null; // Don't move onto self or descendant if (destPath === srcPath || destPath.startsWith(srcPath + '/')) return; @@ -395,6 +400,61 @@ } } + function nbHandleDown(e: PointerEvent, nb: NotebookEntry) { + if (e.pointerType === 'mouse' && e.button !== 0) return; + e.stopPropagation(); + e.preventDefault(); + nbPointer = { id: e.pointerId, src: nb.path }; + draggedNotebookPath = nb.path; + try { (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); } catch {} + } + + function nbPointerMove(e: PointerEvent) { + if (!nbPointer || e.pointerId !== nbPointer.id) return; + e.preventDefault(); + const el = document.elementFromPoint(e.clientX, e.clientY) as HTMLElement | null; + if (el?.closest('[data-nb-root]')) { dropTargetPath = '__root__'; dropPosition = null; return; } + const row = el?.closest('[data-nb-path]') as HTMLElement | null; + const path = row?.getAttribute('data-nb-path'); + if (!path) { dropTargetPath = null; dropPosition = null; return; } + const src = nbPointer.src; + if (path === src || path.startsWith(src + '/')) { dropTargetPath = null; dropPosition = null; return; } + const rect = row!.getBoundingClientRect(); + const relY = (e.clientY - rect.top) / rect.height; + const pos: 'above' | 'into' | 'below' = relY < 0.3 ? 'above' : relY > 0.7 ? 'below' : 'into'; + if (pos === 'into' && src.substring(0, src.lastIndexOf('/')) === path) { dropTargetPath = null; dropPosition = null; return; } + dropTargetPath = path; + dropPosition = pos; + } + + function nbPointerUp(e: PointerEvent) { + if (!nbPointer || e.pointerId !== nbPointer.id) return; + const src = nbPointer.src; + const target = dropTargetPath; + const pos = dropPosition; + nbPointer = null; + draggedNotebookPath = null; + dropTargetPath = null; + dropPosition = null; + if (!target) return; + if (target === '__root__') { + const vaultRoot = $appConfig?.active_vault; + if (vaultRoot) nestNotebook(src, vaultRoot); + } else if (pos === 'above' || pos === 'below') { + handleNotebookReorder(src, target, pos); + } else if (pos === 'into') { + nestNotebook(src, target); + } + } + + function nbPointerCancel(e: PointerEvent) { + if (!nbPointer || e.pointerId !== nbPointer.id) return; + nbPointer = null; + draggedNotebookPath = null; + dropTargetPath = null; + dropPosition = null; + } + function findSiblingsOfPath(tree: NotebookEntry[], parentPath: string, vaultRoot: string): NotebookEntry[] { if (parentPath === vaultRoot) return tree; for (const nb of tree) { @@ -680,6 +740,7 @@
{ if (draggedNotebookPath) { e.preventDefault(); @@ -903,7 +964,7 @@ class:drop-above={dropTargetPath === nb.path && dropPosition === 'above'} class:drop-below={dropTargetPath === nb.path && dropPosition === 'below'} style="padding-left: {8 + depth * 16}px" - draggable="true" + data-nb-path={nb.path} onclick={() => selectNotebook(nb)} onkeydown={(e) => { if (e.key === 'F2') { @@ -912,8 +973,6 @@ } }} oncontextmenu={(e) => onContextMenu(e, nb)} - ondragstart={(e) => { draggedNotebookPath = nb.path; e.dataTransfer!.setData('text/plain', nb.path); e.dataTransfer!.effectAllowed = 'move'; }} - ondragend={() => { draggedNotebookPath = null; dropPosition = null; }} ondragover={(e) => { e.preventDefault(); if (draggedNotebookPath) { @@ -978,6 +1037,20 @@ {/if} {nb.name} {nb.note_count} + {#if $notebookSortMode === 'manual'} + + nbHandleDown(e, nb)} + onpointermove={nbPointerMove} + onpointerup={nbPointerUp} + onpointercancel={nbPointerCancel} + onclick={(e) => e.stopPropagation()} + > + + + {/if} {/if} {#if hasChildren && !isCollapsed} @@ -1307,6 +1380,27 @@ margin-left: 2px; } + .nb-drag-handle { + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + padding: 2px; + margin-left: 2px; + color: var(--text-tertiary); + opacity: 0.45; + cursor: grab; + touch-action: none; + } + + .nb-drag-handle:hover { + opacity: 1; + } + + .nb-drag-handle:active { + cursor: grabbing; + } + .tag-list { padding: 0 4px; }