Skip to content

Commit 17c0a59

Browse files
refactor(svelte): update components and utilities
Updated App.svelte, Item.svelte, ItemMenu.svelte, List.svelte, RichEditor.svelte; updated extensions/KeyboardExtension.js and utils/clipboard.js, utils/focus.js; added actions/contextMenu.js and src/composables/. Consolidating refactor work on Svelte components, editor, and utility functions.
1 parent 6f2aba1 commit 17c0a59

File tree

10 files changed

+625
-75
lines changed

10 files changed

+625
-75
lines changed

src/App.svelte

Lines changed: 92 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
import SearchBar from './components/SearchBar.svelte'
55
import { itemStore } from './stores/itemStore.js'
66
import { flattenVisibleTree } from './utils/tree.js'
7-
import { focusItem } from './utils/focus.js'
7+
import { focusItem, flattenVisible } from './utils/focus.js'
88
import { tick } from 'svelte'
99
import { generateId } from './utils/id.js'
1010
1111
const { items, filteredItems, highlightPhrase, selection, zoomedItemId, zoomedItem } = itemStore
1212
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
1518
1619
function handleCopyJson() {
1720
const data = JSON.stringify($items, null, 2)
@@ -48,6 +51,7 @@
4851
if (event.key === 'Escape') {
4952
itemStore.clearSelection()
5053
itemStore.clearSearch()
54+
selectionBox = null
5155
}
5256
}
5357
@@ -60,45 +64,91 @@
6064
return null
6165
}
6266
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+
6389
function handleMouseDown(event) {
6490
if (event.target.closest('[contenteditable]') || event.target.closest('button')) {
6591
return
6692
}
67-
93+
6894
const itemId = getItemIdFromElement(event.target)
95+
6996
if (itemId) {
70-
isSelecting = true
71-
selectionStartId = itemId
72-
7397
if (event.shiftKey || event.metaKey || event.ctrlKey) {
7498
itemStore.select(itemId, true)
7599
} else {
76100
itemStore.clearSelection()
77101
itemStore.select(itemId, false)
78102
}
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()
79113
}
80114
}
81115
82116
function handleMouseMove(event) {
83-
if (!isSelecting || !selectionStartId) return
117+
if (!isDragging) return
118+
119+
const currentX = event.clientX
120+
const currentY = event.clientY
84121
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 }
90132
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+
})
97143
}
98144
}
99145
100146
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+
})
102152
}
103153
104154
function handleContainerClick(event) {
@@ -115,6 +165,7 @@
115165
<svelte:window
116166
on:keydown={handleGlobalKeyDown}
117167
on:mouseup={handleMouseUp}
168+
on:mousemove={handleMouseMove}
118169
/>
119170

120171
<div class="header">
@@ -136,8 +187,8 @@
136187

137188
<div
138189
class="outer_container"
190+
bind:this={containerRef}
139191
on:mousedown={handleMouseDown}
140-
on:mousemove={handleMouseMove}
141192
on:click={handleContainerClick}
142193
>
143194
<div class="container">
@@ -150,6 +201,13 @@
150201
</div>
151202
</div>
152203

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+
153211
<style>
154212
:root {
155213
--accent: #49baf2;
@@ -220,6 +278,14 @@
220278
color: #666;
221279
}
222280
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+
223289
:global(::-webkit-scrollbar-thumb) {
224290
background-color: rgb(236, 238, 240);
225291
border-radius: 4px;
@@ -228,4 +294,9 @@
228294
:global(::-webkit-scrollbar) {
229295
width: 8px;
230296
}
297+
298+
:global(.item.drag-selecting) {
299+
background: rgba(73, 186, 242, 0.15);
300+
border-radius: 4px;
301+
}
231302
</style>

src/actions/contextMenu.js

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
export function contextMenu(node, options = {}) {
2+
let showCallback = options.onShow
3+
let hideCallback = options.onHide
4+
let triggerElement = options.triggerElement
5+
6+
function handleContextMenu(event) {
7+
event.preventDefault()
8+
event.stopPropagation()
9+
10+
const rect = node.getBoundingClientRect()
11+
const bulletElement = node.querySelector('.bullet-line')
12+
const descriptionEditor = node.querySelector('.description-editor')
13+
14+
let anchorY = rect.top
15+
if (bulletElement) {
16+
const bulletRect = bulletElement.getBoundingClientRect()
17+
anchorY = bulletRect.bottom
18+
}
19+
if (descriptionEditor) {
20+
const descRect = descriptionEditor.getBoundingClientRect()
21+
anchorY = Math.max(anchorY, descRect.bottom)
22+
}
23+
24+
const bulletEl = node.querySelector(':scope > .top > .content-area > .bullet-line > .bullet')
25+
let anchorX = rect.left
26+
if (bulletEl) {
27+
const bulletRect = bulletEl.getBoundingClientRect()
28+
anchorX = bulletRect.left
29+
}
30+
31+
if (showCallback) {
32+
showCallback({
33+
x: anchorX,
34+
y: anchorY + 4
35+
})
36+
}
37+
}
38+
39+
function handleTriggerClick(event) {
40+
event.preventDefault()
41+
event.stopPropagation()
42+
43+
const rect = node.getBoundingClientRect()
44+
const bulletElement = node.querySelector('.bullet-line')
45+
const descriptionEditor = node.querySelector('.description-editor')
46+
47+
let anchorY = rect.top
48+
if (bulletElement) {
49+
const bulletRect = bulletElement.getBoundingClientRect()
50+
anchorY = bulletRect.bottom
51+
}
52+
if (descriptionEditor) {
53+
const descRect = descriptionEditor.getBoundingClientRect()
54+
anchorY = Math.max(anchorY, descRect.bottom)
55+
}
56+
57+
const bulletEl = node.querySelector(':scope > .top > .content-area > .bullet-line > .bullet')
58+
let anchorX = rect.left
59+
if (bulletEl) {
60+
const bulletRect = bulletEl.getBoundingClientRect()
61+
anchorX = bulletRect.left
62+
}
63+
64+
if (showCallback) {
65+
showCallback({
66+
x: anchorX,
67+
y: anchorY + 4
68+
})
69+
}
70+
}
71+
72+
node.addEventListener('contextmenu', handleContextMenu)
73+
74+
if (triggerElement) {
75+
triggerElement.addEventListener('click', handleTriggerClick)
76+
}
77+
78+
return {
79+
update(newOptions) {
80+
if (triggerElement) {
81+
triggerElement.removeEventListener('click', handleTriggerClick)
82+
}
83+
84+
showCallback = newOptions.onShow
85+
hideCallback = newOptions.onHide
86+
triggerElement = newOptions.triggerElement
87+
88+
if (triggerElement) {
89+
triggerElement.addEventListener('click', handleTriggerClick)
90+
}
91+
},
92+
destroy() {
93+
node.removeEventListener('contextmenu', handleContextMenu)
94+
if (triggerElement) {
95+
triggerElement.removeEventListener('click', handleTriggerClick)
96+
}
97+
}
98+
}
99+
}
100+
101+
export function createContextMenuState() {
102+
let isOpen = false
103+
let position = { x: 0, y: 0 }
104+
let closeCallback = null
105+
106+
return {
107+
get isOpen() { return isOpen },
108+
get position() { return position },
109+
110+
show(pos) {
111+
position = pos
112+
isOpen = true
113+
},
114+
115+
hide() {
116+
isOpen = false
117+
},
118+
119+
toggle(pos) {
120+
if (isOpen) {
121+
isOpen = false
122+
} else {
123+
position = pos
124+
isOpen = true
125+
}
126+
},
127+
128+
setCloseCallback(cb) {
129+
closeCallback = cb
130+
}
131+
}
132+
}

0 commit comments

Comments
 (0)