Skip to content

Commit 38e14e0

Browse files
kvapsclaude
andcommitted
feat: multi-select arrows, move and delete
- Shift+arrow extends selection through viselect.select() instead of replacing it, working for hierarchical and spatial fallback paths. - Cmd/Ctrl+arrow now operates on the full currentNodes selection when more than one node is active and they share the same parent. Nodes are sorted by sibling order and moved together via moveNodeBefore/After/In. Bails out silently if the selection spans different parents. - Shift+Backspace now removes every selected node, not only the last. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: Andrei Kvapil <kvapss@gmail.com>
1 parent 575593a commit 38e14e0

1 file changed

Lines changed: 79 additions & 16 deletions

File tree

src/components/MindMap.vue

Lines changed: 79 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -318,21 +318,61 @@ function handleMoveHotkey(e: KeyboardEvent) {
318318
const target = e.target as HTMLElement
319319
if (target.id === 'input-box' || target.closest?.('#input-box')) return
320320
if (host.value?.querySelector('#input-box')) return
321-
const current = mind.currentNode
322-
if (!current) return
323-
const nodeObj = current.nodeObj
324-
if (!nodeObj.parent) return
321+
const nodes = (mind.currentNodes?.length ? mind.currentNodes : mind.currentNode ? [mind.currentNode] : []) as Array<NonNullable<MindElixirInstance['currentNode']>>
322+
if (!nodes.length) return
323+
const firstObj = nodes[0].nodeObj
324+
if (!firstObj.parent) return
325+
if (nodes.length > 1) {
326+
const parentId = firstObj.parent.id
327+
if (!nodes.every((n) => (n.nodeObj.parent as { id: string } | undefined)?.id === parentId)) return
328+
}
325329
326330
e.preventDefault()
327331
e.stopImmediatePropagation()
328332
329-
if (e.key === 'ArrowUp') return mind.moveUpNode(current)
330-
if (e.key === 'ArrowDown') return mind.moveDownNode(current)
333+
const parent = firstObj.parent as NodeObj
334+
const siblings = parent.children ?? []
335+
const sortedNodes = [...nodes].sort(
336+
(a, b) =>
337+
siblings.findIndex((c) => c.id === a.nodeObj.id) -
338+
siblings.findIndex((c) => c.id === b.nodeObj.id)
339+
)
331340
332-
const onLeft = isOnLeftSide(current as unknown as HTMLElement)
341+
if (e.key === 'ArrowUp') {
342+
const firstIdx = siblings.findIndex((c) => c.id === sortedNodes[0].nodeObj.id)
343+
if (firstIdx <= 0) return
344+
const prev = siblings[firstIdx - 1]
345+
const prevEl = MindElixir.E(prev.id)
346+
mind.moveNodeBefore(sortedNodes, prevEl)
347+
return
348+
}
349+
if (e.key === 'ArrowDown') {
350+
const lastIdx = siblings.findIndex(
351+
(c) => c.id === sortedNodes[sortedNodes.length - 1].nodeObj.id
352+
)
353+
if (lastIdx >= siblings.length - 1) return
354+
const next = siblings[lastIdx + 1]
355+
const nextEl = MindElixir.E(next.id)
356+
mind.moveNodeAfter(sortedNodes, nextEl)
357+
return
358+
}
359+
360+
const onLeft = isOnLeftSide(sortedNodes[0] as unknown as HTMLElement)
333361
const goingIn = (e.key === 'ArrowRight' && !onLeft) || (e.key === 'ArrowLeft' && onLeft)
334-
if (goingIn) moveIn(current)
335-
else moveOut(current)
362+
if (goingIn) {
363+
const firstIdx = siblings.findIndex((c) => c.id === sortedNodes[0].nodeObj.id)
364+
const lastIdx = siblings.findIndex(
365+
(c) => c.id === sortedNodes[sortedNodes.length - 1].nodeObj.id
366+
)
367+
let into: { id: string } | undefined
368+
if (firstIdx > 0) into = siblings[firstIdx - 1]
369+
else if (lastIdx < siblings.length - 1) into = siblings[lastIdx + 1]
370+
if (!into) return
371+
mind.moveNodeIn(sortedNodes, MindElixir.E(into.id))
372+
} else {
373+
if (!parent.parent) return
374+
mind.moveNodeAfter(sortedNodes, MindElixir.E(parent.id))
375+
}
336376
}
337377
338378
function findNearestNode(
@@ -431,7 +471,10 @@ function handleTypeToEdit(e: KeyboardEvent) {
431471
e.preventDefault()
432472
e.stopImmediatePropagation()
433473
if (e.shiftKey) {
434-
mind.removeNodes([mind.currentNode])
474+
const nodes = (mind.currentNodes?.length
475+
? mind.currentNodes
476+
: [mind.currentNode]) as Array<NonNullable<MindElixirInstance['currentNode']>>
477+
mind.removeNodes(nodes)
435478
} else {
436479
mind.beginEdit(mind.currentNode)
437480
}
@@ -454,7 +497,7 @@ function handleTypeToEdit(e: KeyboardEvent) {
454497
}
455498
456499
function handleSpatialNav(e: KeyboardEvent) {
457-
if (e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return
500+
if (e.metaKey || e.ctrlKey || e.altKey) return
458501
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) return
459502
if (!mind || !mind.currentNode) return
460503
const target = e.target as HTMLElement
@@ -463,30 +506,50 @@ function handleSpatialNav(e: KeyboardEvent) {
463506
e.preventDefault()
464507
e.stopImmediatePropagation()
465508
509+
const extend = e.shiftKey
466510
const cur = mind.currentNode
467511
const nodeObj = cur.nodeObj
468512
const dir = e.key as 'ArrowUp' | 'ArrowDown' | 'ArrowLeft' | 'ArrowRight'
469513
514+
const select = (id: string) => {
515+
if (!mind) return false
516+
try {
517+
const el = MindElixir.E(id)
518+
if (!el) return false
519+
if (extend) {
520+
mind.selection?.select(el as unknown as Parameters<NonNullable<MindElixirInstance['selection']>['select']>[0])
521+
} else {
522+
mind.selectNode(el)
523+
}
524+
return true
525+
} catch {
526+
return false
527+
}
528+
}
529+
470530
if (dir === 'ArrowDown' || dir === 'ArrowUp') {
471531
const sibling = findSiblingNode(nodeObj, dir === 'ArrowDown' ? 'next' : 'prev')
472-
if (sibling && trySelectById(sibling.id)) return
532+
if (sibling && select(sibling.id)) return
473533
} else {
474534
const onLeft = isOnLeftSide(cur as unknown as HTMLElement)
475535
const goingIn = (dir === 'ArrowRight' && !onLeft) || (dir === 'ArrowLeft' && onLeft)
476536
if (goingIn) {
477537
if (nodeObj.expanded !== false && nodeObj.children?.length) {
478538
const remembered = lastChildMap.get(nodeObj.id)
479-
const target = nodeObj.children.find((c) => c.id === remembered) ?? nodeObj.children[0]
480-
if (trySelectById(target.id)) return
539+
const targetNode = nodeObj.children.find((c) => c.id === remembered) ?? nodeObj.children[0]
540+
if (select(targetNode.id)) return
481541
}
482542
} else {
483543
const parent = nodeObj.parent as NodeObj | undefined
484-
if (parent && parent.parent && trySelectById(parent.id)) return
544+
if (parent && parent.parent && select(parent.id)) return
485545
}
486546
}
487547
488548
const nearest = findNearestNode(cur as unknown as HTMLElement, dir)
489-
if (nearest) mind.selectNode(nearest as Parameters<MindElixirInstance['selectNode']>[0])
549+
if (nearest) {
550+
const id = (nearest as unknown as { nodeObj?: { id: string } }).nodeObj?.id
551+
if (id) select(id)
552+
}
490553
}
491554
492555
onMounted(() => {

0 commit comments

Comments
 (0)