|
4 | 4 | import SearchBar from './components/SearchBar.svelte' |
5 | 5 | import { itemStore } from './stores/itemStore.js' |
6 | 6 | import { flattenVisibleTree } from './utils/tree.js' |
7 | | - import { focusItem } from './utils/focus.js' |
| 7 | + import { focusItem, flattenVisible } from './utils/focus.js' |
8 | 8 | import { tick } from 'svelte' |
9 | 9 | import { generateId } from './utils/id.js' |
10 | 10 |
|
11 | 11 | const { items, filteredItems, highlightPhrase, selection, zoomedItemId, zoomedItem } = itemStore |
12 | 12 |
|
13 | | - let isSelecting = false |
14 | | - let selectionStartId = null |
| 13 | + let isDragging = false |
| 14 | + let dragStartX = 0 |
| 15 | + let dragStartY = 0 |
| 16 | + let selectionBox = null |
| 17 | + let containerRef |
15 | 18 |
|
16 | 19 | function handleCopyJson() { |
17 | 20 | const data = JSON.stringify($items, null, 2) |
|
48 | 51 | if (event.key === 'Escape') { |
49 | 52 | itemStore.clearSelection() |
50 | 53 | itemStore.clearSearch() |
| 54 | + selectionBox = null |
51 | 55 | } |
52 | 56 | } |
53 | 57 |
|
|
60 | 64 | return null |
61 | 65 | } |
62 | 66 |
|
| 67 | + function rectsIntersect(r1, r2) { |
| 68 | + return !(r2.left > r1.right || |
| 69 | + r2.right < r1.left || |
| 70 | + r2.top > r1.bottom || |
| 71 | + r2.bottom < r1.top) |
| 72 | + } |
| 73 | +
|
| 74 | + function getItemsInBox(box) { |
| 75 | + const itemElements = document.querySelectorAll('.item') |
| 76 | + const selectedIds = [] |
| 77 | + |
| 78 | + for (const el of itemElements) { |
| 79 | + const rect = el.getBoundingClientRect() |
| 80 | + if (rectsIntersect(box, rect)) { |
| 81 | + const id = el.id.replace('item_', '') |
| 82 | + if (id) selectedIds.push(id) |
| 83 | + } |
| 84 | + } |
| 85 | + |
| 86 | + return selectedIds |
| 87 | + } |
| 88 | +
|
63 | 89 | function handleMouseDown(event) { |
64 | 90 | if (event.target.closest('[contenteditable]') || event.target.closest('button')) { |
65 | 91 | return |
66 | 92 | } |
67 | | - |
| 93 | +
|
68 | 94 | const itemId = getItemIdFromElement(event.target) |
| 95 | + |
69 | 96 | if (itemId) { |
70 | | - isSelecting = true |
71 | | - selectionStartId = itemId |
72 | | - |
73 | 97 | if (event.shiftKey || event.metaKey || event.ctrlKey) { |
74 | 98 | itemStore.select(itemId, true) |
75 | 99 | } else { |
76 | 100 | itemStore.clearSelection() |
77 | 101 | itemStore.select(itemId, false) |
78 | 102 | } |
| 103 | + return |
| 104 | + } |
| 105 | +
|
| 106 | + isDragging = true |
| 107 | + dragStartX = event.clientX |
| 108 | + dragStartY = event.clientY |
| 109 | + selectionBox = null |
| 110 | + |
| 111 | + if (!event.shiftKey && !event.metaKey && !event.ctrlKey) { |
| 112 | + itemStore.clearSelection() |
79 | 113 | } |
80 | 114 | } |
81 | 115 |
|
82 | 116 | function handleMouseMove(event) { |
83 | | - if (!isSelecting || !selectionStartId) return |
| 117 | + if (!isDragging) return |
| 118 | +
|
| 119 | + const currentX = event.clientX |
| 120 | + const currentY = event.clientY |
84 | 121 | |
85 | | - const itemId = getItemIdFromElement(event.target) |
86 | | - if (itemId && itemId !== selectionStartId) { |
87 | | - const flatItems = flattenVisibleTree($filteredItems.tree) |
88 | | - const startIdx = flatItems.findIndex(i => i.id === selectionStartId) |
89 | | - const endIdx = flatItems.findIndex(i => i.id === itemId) |
| 122 | + const left = Math.min(dragStartX, currentX) |
| 123 | + const top = Math.min(dragStartY, currentY) |
| 124 | + const right = Math.max(dragStartX, currentX) |
| 125 | + const bottom = Math.max(dragStartY, currentY) |
| 126 | + |
| 127 | + const width = right - left |
| 128 | + const height = bottom - top |
| 129 | + |
| 130 | + if (width > 5 || height > 5) { |
| 131 | + selectionBox = { left, top, right, bottom, width, height } |
90 | 132 | |
91 | | - if (startIdx !== -1 && endIdx !== -1) { |
92 | | - const minIdx = Math.min(startIdx, endIdx) |
93 | | - const maxIdx = Math.max(startIdx, endIdx) |
94 | | - const selectedIds = flatItems.slice(minIdx, maxIdx + 1).map(i => i.id) |
95 | | - itemStore.selectRange(selectedIds) |
96 | | - } |
| 133 | + const selectedIds = getItemsInBox(selectionBox) |
| 134 | + itemStore.selectRange(selectedIds) |
| 135 | + |
| 136 | + document.querySelectorAll('.item').forEach(el => { |
| 137 | + el.classList.remove('drag-selecting') |
| 138 | + }) |
| 139 | + selectedIds.forEach(id => { |
| 140 | + const el = document.querySelector(`#item_${id}`) |
| 141 | + if (el) el.classList.add('drag-selecting') |
| 142 | + }) |
97 | 143 | } |
98 | 144 | } |
99 | 145 |
|
100 | 146 | function handleMouseUp() { |
101 | | - isSelecting = false |
| 147 | + isDragging = false |
| 148 | + selectionBox = null |
| 149 | + document.querySelectorAll('.item.drag-selecting').forEach(el => { |
| 150 | + el.classList.remove('drag-selecting') |
| 151 | + }) |
102 | 152 | } |
103 | 153 |
|
104 | 154 | function handleContainerClick(event) { |
|
115 | 165 | <svelte:window |
116 | 166 | on:keydown={handleGlobalKeyDown} |
117 | 167 | on:mouseup={handleMouseUp} |
| 168 | + on:mousemove={handleMouseMove} |
118 | 169 | /> |
119 | 170 |
|
120 | 171 | <div class="header"> |
|
136 | 187 |
|
137 | 188 | <div |
138 | 189 | class="outer_container" |
| 190 | + bind:this={containerRef} |
139 | 191 | on:mousedown={handleMouseDown} |
140 | | - on:mousemove={handleMouseMove} |
141 | 192 | on:click={handleContainerClick} |
142 | 193 | > |
143 | 194 | <div class="container"> |
|
150 | 201 | </div> |
151 | 202 | </div> |
152 | 203 |
|
| 204 | +{#if selectionBox} |
| 205 | + <div |
| 206 | + class="selection-box" |
| 207 | + style="left: {selectionBox.left}px; top: {selectionBox.top}px; width: {selectionBox.width}px; height: {selectionBox.height}px;" |
| 208 | + ></div> |
| 209 | +{/if} |
| 210 | + |
153 | 211 | <style> |
154 | 212 | :root { |
155 | 213 | --accent: #49baf2; |
|
220 | 278 | color: #666; |
221 | 279 | } |
222 | 280 |
|
| 281 | + .selection-box { |
| 282 | + position: fixed; |
| 283 | + background: rgba(73, 186, 242, 0.1); |
| 284 | + border: 1px solid rgba(73, 186, 242, 0.5); |
| 285 | + pointer-events: none; |
| 286 | + z-index: 1000; |
| 287 | + } |
| 288 | +
|
223 | 289 | :global(::-webkit-scrollbar-thumb) { |
224 | 290 | background-color: rgb(236, 238, 240); |
225 | 291 | border-radius: 4px; |
|
228 | 294 | :global(::-webkit-scrollbar) { |
229 | 295 | width: 8px; |
230 | 296 | } |
| 297 | +
|
| 298 | + :global(.item.drag-selecting) { |
| 299 | + background: rgba(73, 186, 242, 0.15); |
| 300 | + border-radius: 4px; |
| 301 | + } |
231 | 302 | </style> |
0 commit comments