fix: multiselect range includes anchor note and prevents text selection (#127)

- selectRange() now includes the currently open note as the anchor
- Added setAnchor() to track which note is the selection start
- Added select-none to NoteList to prevent browser text selection on Shift+click

Co-authored-by: Test <test@test.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Luca Rossi
2026-02-27 16:53:18 +01:00
committed by GitHub
parent a990b24861
commit 9de3cecaa2
2 changed files with 13 additions and 7 deletions

View File

@@ -196,7 +196,7 @@ function countExpiredTrash(entries: VaultEntry[]): number {
// --- Click routing ---
type MultiSelectActions = { toggle: (path: string) => void; selectRange: (path: string) => void; clear: () => void }
type MultiSelectActions = { toggle: (path: string) => void; selectRange: (path: string) => void; clear: () => void; setAnchor: (path: string) => void }
function routeNoteClick(
entry: VaultEntry, e: React.MouseEvent,
@@ -205,7 +205,7 @@ function routeNoteClick(
) {
if (e.shiftKey) { multiSelect.selectRange(entry.path) }
else if (e.metaKey || e.ctrlKey) { multiSelect.toggle(entry.path) }
else { multiSelect.clear(); onReplaceActiveTab(entry) }
else { multiSelect.clear(); multiSelect.setAnchor(entry.path); onReplaceActiveTab(entry) }
}
// --- Pure helpers extracted from NoteListInner to reduce cyclomatic complexity ---
@@ -307,7 +307,7 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF
const listDirection = listConfig.direction
const { isEntityView, isTrashView, isChangesView, typeDocument, searched, searchedGroups, expiredTrashCount } = useNoteListData({ entries, selection, allContent, query, listSort, listDirection, modifiedPathSet })
const multiSelect = useMultiSelect(searched)
const multiSelect = useMultiSelect(searched, selectedNote?.path ?? null)
// Clear multi-select when sidebar selection changes
useEffect(() => { multiSelect.clear() }, [selection]) // eslint-disable-line react-hooks/exhaustive-deps -- clear on selection change only
@@ -353,7 +353,7 @@ function NoteListInner({ entries, selection, selectedNote, allContent, modifiedF
), [selectedNote?.path, handleClickNote, typeEntryMap, resolvedGetNoteStatus, multiSelect.selectedPaths])
return (
<div className="flex flex-col overflow-hidden border-r border-border bg-card text-foreground" style={{ height: '100%' }}>
<div className="flex flex-col select-none overflow-hidden border-r border-border bg-card text-foreground" style={{ height: '100%' }}>
<div className="flex h-[52px] shrink-0 items-center justify-between border-b border-border px-4" onMouseDown={onDragMouseDown} style={{ cursor: 'default' }}>
<h3 className="m-0 min-w-0 flex-1 truncate text-[14px] font-semibold">{resolveHeaderTitle(selection, typeDocument)}</h3>
<div className="flex items-center gap-3" style={{ WebkitAppRegion: 'no-drag' } as React.CSSProperties}>

View File

@@ -7,10 +7,11 @@ export interface MultiSelectState {
toggle: (path: string) => void
selectRange: (toPath: string) => void
clear: () => void
setAnchor: (path: string) => void
selectAll: () => void
}
export function useMultiSelect(visibleEntries: VaultEntry[]): MultiSelectState {
export function useMultiSelect(visibleEntries: VaultEntry[], activePath: string | null = null): MultiSelectState {
const [selectedPaths, setSelectedPaths] = useState<Set<string>>(new Set())
const lastClickedRef = useRef<string | null>(null)
@@ -25,7 +26,7 @@ export function useMultiSelect(visibleEntries: VaultEntry[]): MultiSelectState {
}, [])
const selectRange = useCallback((toPath: string) => {
const fromPath = lastClickedRef.current
const fromPath = lastClickedRef.current ?? activePath
if (!fromPath) {
toggle(toPath)
return
@@ -45,13 +46,17 @@ export function useMultiSelect(visibleEntries: VaultEntry[]): MultiSelectState {
return next
})
lastClickedRef.current = toPath
}, [visibleEntries, toggle])
}, [visibleEntries, activePath, toggle])
const clear = useCallback(() => {
setSelectedPaths(new Set())
lastClickedRef.current = null
}, [])
const setAnchor = useCallback((path: string) => {
lastClickedRef.current = path
}, [])
const selectAll = useCallback(() => {
setSelectedPaths(new Set(visibleEntries.map((e) => e.path)))
}, [visibleEntries])
@@ -62,6 +67,7 @@ export function useMultiSelect(visibleEntries: VaultEntry[]): MultiSelectState {
toggle,
selectRange,
clear,
setAnchor,
selectAll,
}
}