Skip to content

Commit 791b384

Browse files
Minor fixes
1 parent 46f1b1b commit 791b384

File tree

6 files changed

+328
-40
lines changed

6 files changed

+328
-40
lines changed

src/App.svelte

Lines changed: 91 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
let dragStartY = 0
1717
let selectionBox = null
1818
let containerRef
19+
let justFinishedDragSelection = false
1920
2021
async function handleCopy() {
2122
if ($selection.size > 0) {
@@ -33,6 +34,15 @@
3334
}
3435
}
3536
37+
if ((event.metaKey || event.ctrlKey) && event.key === 'x') {
38+
if ($selection.size > 0) {
39+
event.preventDefault()
40+
itemStore.copySelected().then(() => {
41+
itemStore.deleteSelected()
42+
})
43+
}
44+
}
45+
3646
if ((event.metaKey || event.ctrlKey) && event.key === 'v') {
3747
event.preventDefault()
3848
itemStore.paste($zoomedItemId)
@@ -42,6 +52,14 @@
4252
if ($selection.size > 0 && !event.target.closest('[contenteditable]')) {
4353
event.preventDefault()
4454
itemStore.deleteSelected()
55+
} else if (!event.target.closest('[contenteditable]')) {
56+
event.preventDefault()
57+
const currentRoot = $zoomedItem || $filteredItems.tree
58+
const flat = flattenVisibleTree(currentRoot)
59+
if (flat.length > 0) {
60+
const lastItem = flat[flat.length - 1]
61+
focusItem(lastItem.id)
62+
}
4563
}
4664
}
4765
@@ -58,6 +76,25 @@
5876
selectionBox = null
5977
}
6078
79+
if (event.key === 'Enter' && !event.target.closest('[contenteditable]') && !event.metaKey && !event.ctrlKey && !event.shiftKey) {
80+
event.preventDefault()
81+
const currentRoot = $zoomedItem || $filteredItems.tree
82+
const newId = generateId()
83+
const newItem = { id: newId, text: '', description: '', completed: false, open: true, children: [] }
84+
85+
if (currentRoot.children?.length) {
86+
itemStore.updateItem(currentRoot.id, {
87+
children: [...currentRoot.children, newItem]
88+
})
89+
} else {
90+
itemStore.updateItem(currentRoot.id, {
91+
children: [newItem]
92+
})
93+
}
94+
95+
tick().then(() => focusItem(newId))
96+
}
97+
6198
if ((event.metaKey || event.ctrlKey) && event.key === 'z') {
6299
if (event.shiftKey) {
63100
event.preventDefault()
@@ -100,22 +137,42 @@
100137
return selectedIds
101138
}
102139
140+
let potentialDragStart = null
141+
103142
function handleMouseDown(event) {
104-
if (event.target.closest('[contenteditable]') || event.target.closest('button')) {
143+
if (event.target.closest('button')) {
105144
return
106145
}
107146
108-
isDragging = true
109-
dragStartX = event.clientX
110-
dragStartY = event.clientY
147+
potentialDragStart = { x: event.clientX, y: event.clientY, fromEditor: !!event.target.closest('[contenteditable]') }
148+
isDragging = false
111149
selectionBox = null
112150
}
113151
114152
function handleMouseMove(event) {
115-
if (!isDragging) return
153+
if (!potentialDragStart) return
116154
117155
const currentX = event.clientX
118156
const currentY = event.clientY
157+
const dx = currentX - potentialDragStart.x
158+
const dy = currentY - potentialDragStart.y
159+
const distance = Math.sqrt(dx * dx + dy * dy)
160+
161+
if (!isDragging && distance > 5) {
162+
isDragging = true
163+
dragStartX = potentialDragStart.x
164+
dragStartY = potentialDragStart.y
165+
166+
if (potentialDragStart.fromEditor) {
167+
const activeEl = document.activeElement
168+
if (activeEl && activeEl.closest('[contenteditable]')) {
169+
activeEl.blur()
170+
}
171+
window.getSelection()?.removeAllRanges()
172+
}
173+
}
174+
175+
if (!isDragging) return
119176
120177
const left = Math.min(dragStartX, currentX)
121178
const top = Math.min(dragStartY, currentY)
@@ -125,20 +182,18 @@
125182
const width = right - left
126183
const height = bottom - top
127184
128-
if (width > 5 || height > 5) {
129-
selectionBox = { left, top, right, bottom, width, height }
130-
131-
const selectedIds = getItemsInBox(selectionBox)
132-
itemStore.selectRange(selectedIds)
133-
134-
document.querySelectorAll('.item').forEach(el => {
135-
el.classList.remove('drag-selecting')
136-
})
137-
selectedIds.forEach(id => {
138-
const el = document.querySelector(`#item_${id}`)
139-
if (el) el.classList.add('drag-selecting')
140-
})
141-
}
185+
selectionBox = { left, top, right, bottom, width, height }
186+
187+
const selectedIds = getItemsInBox(selectionBox)
188+
itemStore.selectRange(selectedIds)
189+
190+
document.querySelectorAll('.item').forEach(el => {
191+
el.classList.remove('drag-selecting')
192+
})
193+
selectedIds.forEach(id => {
194+
const el = document.querySelector(`#item_${id}`)
195+
if (el) el.classList.add('drag-selecting')
196+
})
142197
}
143198
144199
function handleMouseUp(event) {
@@ -147,6 +202,18 @@
147202
148203
isDragging = false
149204
selectionBox = null
205+
potentialDragStart = null
206+
207+
if (hadDragBox) {
208+
justFinishedDragSelection = true
209+
210+
if ($selection.size > 0) {
211+
const activeEl = document.activeElement
212+
if (activeEl && activeEl.closest('[contenteditable]')) {
213+
activeEl.blur()
214+
}
215+
}
216+
}
150217
151218
requestAnimationFrame(() => {
152219
document.querySelectorAll('.item.drag-selecting').forEach(el => {
@@ -175,6 +242,11 @@
175242
}
176243
177244
function handleContainerClick(event) {
245+
if (justFinishedDragSelection) {
246+
justFinishedDragSelection = false
247+
return
248+
}
249+
178250
if (event.target.closest('.item') ||
179251
event.target.closest('button') ||
180252
event.target.closest('[contenteditable]') ||

src/components/Item.svelte

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,18 @@
2020
let titleEditorRef
2121
2222
$: hasCheckboxSyntax = /^\[( |x)\]\s/.test(item.text || '')
23-
$: isCheckboxChecked = /^\[x\]\s/.test(item.text || '')
23+
$: isCheckboxChecked = /^\[x\]\s/.test(item.text || '') || item.completed
2424
$: displayText = hasCheckboxSyntax ? item.text.replace(/^\[( |x)\]\s/, '') : item.text
2525
$: hasDescription = !!item.description?.trim()
2626
$: hasChildren = item.children?.length > 0
2727
2828
function handleToggleComplete() {
2929
if (hasCheckboxSyntax) {
30-
const newText = isCheckboxChecked
31-
? item.text.replace(/^\[x\]\s/, '[ ] ')
32-
: item.text.replace(/^\[ \]\s/, '[x] ')
33-
itemStore.updateItem(item.id, { text: newText })
30+
const newChecked = !isCheckboxChecked
31+
const newText = newChecked
32+
? item.text.replace(/^\[ \]\s/, '[x] ')
33+
: item.text.replace(/^\[x\]\s/, '[ ] ')
34+
itemStore.updateItem(item.id, { text: newText, completed: newChecked })
3435
} else {
3536
itemStore.toggleComplete(item.id)
3637
}

src/components/List.svelte

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
1515
const dispatch = createEventDispatcher()
1616
17-
const { zoomedItemId, selection } = itemStore
17+
const { zoomedItemId, selection, selectionAnchor, selectionDirection } = itemStore
1818
1919
let containerElement
2020
let showDescriptionEditor = false
@@ -85,23 +85,57 @@
8585
const items = get(itemStore.items)
8686
const prevItem = getItemAbove(items, id)
8787
88-
if (prevItem) {
88+
if (!prevItem) return
89+
90+
const currentAnchor = get(selectionAnchor)
91+
const currentSelection = get(selection)
92+
const lastDirection = get(selectionDirection)
93+
94+
if (currentSelection.size === 0) {
95+
itemStore.setSelectionAnchor(id)
8996
itemStore.addToSelection(id)
9097
itemStore.addToSelection(prevItem.id)
91-
focusItem(prevItem.id)
98+
itemStore.setSelectionDirection('up')
99+
} else if (lastDirection === 'down' && currentAnchor && id !== currentAnchor) {
100+
itemStore.removeFromSelection(id)
101+
if (currentSelection.size <= 2) {
102+
itemStore.setSelectionDirection(null)
103+
}
104+
} else {
105+
itemStore.addToSelection(prevItem.id)
106+
itemStore.setSelectionDirection('up')
92107
}
108+
109+
focusItem(prevItem.id)
93110
}
94111
95112
function handleShiftSelectDown(event) {
96113
const id = event.detail.id
97114
const items = get(itemStore.items)
98115
const nextItem = getItemBelow(items, id)
99116
100-
if (nextItem) {
117+
if (!nextItem) return
118+
119+
const currentAnchor = get(selectionAnchor)
120+
const currentSelection = get(selection)
121+
const lastDirection = get(selectionDirection)
122+
123+
if (currentSelection.size === 0) {
124+
itemStore.setSelectionAnchor(id)
101125
itemStore.addToSelection(id)
102126
itemStore.addToSelection(nextItem.id)
103-
focusItem(nextItem.id)
127+
itemStore.setSelectionDirection('down')
128+
} else if (lastDirection === 'up' && currentAnchor && id !== currentAnchor) {
129+
itemStore.removeFromSelection(id)
130+
if (currentSelection.size <= 2) {
131+
itemStore.setSelectionDirection(null)
132+
}
133+
} else {
134+
itemStore.addToSelection(nextItem.id)
135+
itemStore.setSelectionDirection('down')
104136
}
137+
138+
focusItem(nextItem.id)
105139
}
106140
107141
function handleZoom(event) {
@@ -111,7 +145,16 @@
111145
function handleDescriptionClick() {
112146
showDescriptionEditor = true
113147
tick().then(() => {
114-
focusDescription(item.id)
148+
requestAnimationFrame(() => {
149+
if (containerElement) {
150+
const el = containerElement.querySelector('.zoomed_description [contenteditable]')
151+
if (el) {
152+
el.focus()
153+
return
154+
}
155+
}
156+
focusDescription(item.id)
157+
})
115158
})
116159
}
117160
@@ -263,7 +306,7 @@
263306
<div class="item-row">
264307
<Item
265308
item={child}
266-
isSelected={isItemSelected(child.id)}
309+
isSelected={$selection.has(child.id)}
267310
{highlightPhrase}
268311
on:delete={handleDelete}
269312
on:forcedelete={handleForceDelete}

src/components/RichEditor.svelte

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,7 @@
284284
border: 1px solid #ddd;
285285
border-radius: 6px;
286286
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
287+
visibility: hidden;
287288
}
288289
289290
.bubble-menu button {
@@ -347,4 +348,59 @@
347348
height: 0;
348349
pointer-events: none;
349350
}
351+
352+
:global(.inline-checkbox) {
353+
display: inline-flex;
354+
align-items: center;
355+
vertical-align: middle;
356+
margin-right: 6px;
357+
}
358+
359+
:global(.checkbox-button) {
360+
all: unset;
361+
display: inline-flex;
362+
align-items: center;
363+
justify-content: center;
364+
cursor: pointer;
365+
padding: 2px;
366+
border-radius: 4px;
367+
color: #999;
368+
transition: all 0.15s ease;
369+
}
370+
371+
:global(.checkbox-button:hover) {
372+
color: #666;
373+
background: rgba(0, 0, 0, 0.05);
374+
}
375+
376+
:global(.checkbox-button.checked) {
377+
color: var(--accent, #49baf2);
378+
}
379+
380+
:global(.checkbox-button.checked:hover) {
381+
color: var(--accent, #49baf2);
382+
background: rgba(73, 186, 242, 0.1);
383+
}
384+
385+
:global(.checkbox-button svg) {
386+
width: 16px;
387+
height: 16px;
388+
transition: transform 0.15s ease;
389+
}
390+
391+
:global(.checkbox-button.checked svg) {
392+
animation: checkPop 0.2s ease;
393+
}
394+
395+
@keyframes checkPop {
396+
0% { transform: scale(0.8); }
397+
50% { transform: scale(1.1); }
398+
100% { transform: scale(1); }
399+
}
400+
401+
:global(.ProseMirror:has(.inline-checkbox .checkbox-button.checked)) {
402+
text-decoration: line-through;
403+
opacity: 0.6;
404+
text-decoration-color: currentColor;
405+
}
350406
</style>

0 commit comments

Comments
 (0)