Skip to content

Commit e73a6aa

Browse files
feat: Implement item referencing and global search, refactor selection management into itemStore.
1 parent c94dd0a commit e73a6aa

File tree

11 files changed

+437
-75
lines changed

11 files changed

+437
-75
lines changed

src/App.svelte

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,40 @@
104104
itemStore.undo()
105105
}
106106
}
107+
108+
if (event.shiftKey && (event.key === 'ArrowUp' || event.key === 'ArrowDown')) {
109+
if (!event.target.closest('[contenteditable]')) {
110+
event.preventDefault()
111+
const currentRoot = $zoomedItem || $filteredItems.tree
112+
const flat = flattenVisibleTree(currentRoot)
113+
if (flat.length === 0) return
114+
115+
if ($selection.size === 0) {
116+
const targetItem = event.key === 'ArrowUp' ? flat[flat.length - 1] : flat[0]
117+
itemStore.selectRange([targetItem.id])
118+
itemStore.setSelectionHead(targetItem.id)
119+
itemStore.setSelectionAnchor(targetItem.id)
120+
} else {
121+
const direction = event.key === 'ArrowUp' ? 'up' : 'down'
122+
itemStore.extendSelection(direction)
123+
}
124+
}
125+
}
126+
127+
if ((event.key === 'ArrowUp' || event.key === 'ArrowDown') && !event.shiftKey) {
128+
if (!event.target.closest('[contenteditable]')) {
129+
event.preventDefault()
130+
const currentRoot = $zoomedItem || $filteredItems.tree
131+
const flat = flattenVisibleTree(currentRoot)
132+
if (flat.length === 0) return
133+
134+
if (event.key === 'ArrowUp') {
135+
focusItem(flat[flat.length - 1].id)
136+
} else {
137+
focusItem(flat[0].id)
138+
}
139+
}
140+
}
107141
}
108142
109143
function getItemIdFromElement(element) {
@@ -340,6 +374,16 @@
340374
animation: shrinkOut var(--transition-fast) both;
341375
}
342376
377+
@keyframes highlightPulse {
378+
0%, 100% { background: transparent; }
379+
30% { background: rgba(139, 92, 246, 0.25); }
380+
}
381+
382+
:global(.highlight-pulse) {
383+
animation: highlightPulse 1.5s ease-out;
384+
border-radius: 4px;
385+
}
386+
343387
:global(body) {
344388
margin: 0;
345389
padding: 0;

src/components/AutocompleteMenu.svelte

Lines changed: 31 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,19 @@
1111
let selectedIndex = 0
1212
let menuElement
1313
14-
$: filteredItems = items.filter(item =>
15-
item.toLowerCase().includes(query.toLowerCase())
16-
)
14+
function getItemText(item) {
15+
return typeof item === 'object' ? item.text : item
16+
}
17+
18+
function truncateText(text, maxLen = 50) {
19+
if (!text) return ''
20+
return text.length > maxLen ? text.slice(0, maxLen) + '' : text
21+
}
22+
23+
$: filteredItems = items.filter(item => {
24+
const text = getItemText(item)
25+
return text?.toLowerCase().includes(query.toLowerCase())
26+
})
1727
1828
$: if (filteredItems.length > 0 && selectedIndex >= filteredItems.length) {
1929
selectedIndex = filteredItems.length - 1
@@ -70,7 +80,7 @@
7080
on:click={() => selectItem(item)}
7181
on:mouseenter={() => selectedIndex = index}
7282
>
73-
<span class="item-text">{item}</span>
83+
{truncateText(getItemText(item))}
7484
</button>
7585
{/each}
7686
</div>
@@ -81,35 +91,34 @@
8191
position: fixed;
8292
z-index: 1000;
8393
background: white;
84-
border: 1px solid rgba(0, 0, 0, 0.08);
85-
border-radius: 8px;
86-
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
87-
min-width: 160px;
88-
max-width: 280px;
89-
max-height: 200px;
94+
border: 1px solid rgba(0, 0, 0, 0.1);
95+
border-radius: 6px;
96+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
97+
min-width: 140px;
98+
max-width: 260px;
99+
max-height: 180px;
90100
overflow-y: auto;
91-
padding: 4px;
101+
padding: 3px;
92102
}
93103
94104
.autocomplete-item {
95105
all: unset;
96-
display: flex;
97-
align-items: center;
106+
display: block;
98107
width: 100%;
99-
padding: 8px 12px;
108+
padding: 6px 10px;
100109
cursor: pointer;
101-
border-radius: 6px;
102-
font-size: 14px;
103-
color: #333;
110+
border-radius: 4px;
111+
font-size: 13px;
112+
color: #555;
104113
box-sizing: border-box;
114+
white-space: nowrap;
115+
overflow: hidden;
116+
text-overflow: ellipsis;
105117
}
106118
107119
.autocomplete-item:hover,
108120
.autocomplete-item.selected {
109-
background: rgba(73, 186, 242, 0.1);
110-
}
111-
112-
.item-text {
113-
color: var(--accent, #49baf2);
121+
background: rgba(0, 0, 0, 0.05);
122+
color: #333;
114123
}
115124
</style>

src/components/BreadCrumb.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
3636
function handleHome() {
3737
itemStore.zoom(null)
38+
itemStore.clearSearch()
3839
}
3940
4041
function handleCrumbClick(item) {

src/components/Item.svelte

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,10 @@
101101
itemStore.setSearch(event.detail.hashtag)
102102
}
103103
104+
function handleItemRefClick(event) {
105+
itemStore.navigateToItem(event.detail.id)
106+
}
107+
104108
function handleTextChange(event) {
105109
const newVal = hasCheckboxSyntax
106110
? (isCheckboxChecked ? '[x] ' : '[ ] ') + event.detail.value
@@ -233,6 +237,7 @@
233237
value={displayText}
234238
showPlaceholder={false}
235239
{highlightPhrase}
240+
itemId={item.id}
236241
on:change={handleTextChange}
237242
on:delete={handleDelete}
238243
on:forcedelete={handleForceDelete}
@@ -246,6 +251,7 @@
246251
on:togglecomplete={handleToggleComplete}
247252
on:description={handleShowDescription}
248253
on:hashtagclick={handleHashtagClick}
254+
on:itemrefclick={handleItemRefClick}
249255
/>
250256
</div>
251257
</div>
@@ -263,6 +269,7 @@
263269
on:exitdescription={handleExitDescription}
264270
on:togglecomplete={handleToggleComplete}
265271
on:hashtagclick={handleHashtagClick}
272+
on:itemrefclick={handleItemRefClick}
266273
/>
267274
</div>
268275
{/if}

src/components/List.svelte

Lines changed: 22 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@
77
import { focusItem, focusDescription, getItemAbove, getItemBelow, flattenVisible } from '../utils/focus.js'
88
import { get } from 'svelte/store'
99
10+
function handleHashtagClick(event) {
11+
itemStore.setSearch(event.detail.hashtag)
12+
}
13+
14+
function handleItemRefClick(event) {
15+
itemStore.navigateToItem(event.detail.id)
16+
}
17+
1018
export let item
1119
export let isTop = false
1220
export let outermost = false
@@ -81,61 +89,21 @@
8189
}
8290
8391
function handleShiftSelectUp(event) {
84-
const id = event.detail.id
85-
const items = get(itemStore.items)
86-
const prevItem = getItemAbove(items, id)
92+
itemStore.extendSelection('up')
8793
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)
96-
itemStore.addToSelection(id)
97-
itemStore.addToSelection(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')
94+
const active = document.activeElement
95+
if (active?.closest('[contenteditable]')) {
96+
active.blur()
10797
}
108-
109-
focusItem(prevItem.id)
11098
}
11199
112100
function handleShiftSelectDown(event) {
113-
const id = event.detail.id
114-
const items = get(itemStore.items)
115-
const nextItem = getItemBelow(items, id)
101+
itemStore.extendSelection('down')
116102
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)
125-
itemStore.addToSelection(id)
126-
itemStore.addToSelection(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')
103+
const active = document.activeElement
104+
if (active?.closest('[contenteditable]')) {
105+
active.blur()
136106
}
137-
138-
focusItem(nextItem.id)
139107
}
140108
141109
function handleZoom(event) {
@@ -262,9 +230,12 @@
262230
bind:value={item.text}
263231
{highlightPhrase}
264232
showPlaceholder={false}
233+
itemId={item.id}
265234
on:selectdown={handleTitleSelectDown}
266235
on:newbullet={handleTitleNewBullet}
267236
on:change={() => itemStore.updateItem(item.id, { text: item.text })}
237+
on:hashtagclick={handleHashtagClick}
238+
on:itemrefclick={handleItemRefClick}
268239
/>
269240
</h1>
270241
@@ -293,9 +264,12 @@
293264
bind:value={item.text}
294265
{highlightPhrase}
295266
showPlaceholder={false}
267+
itemId={item.id}
296268
on:selectdown={handleTitleSelectDown}
297269
on:newbullet={handleTitleNewBullet}
298270
on:change={() => itemStore.updateItem(item.id, { text: item.text })}
271+
on:hashtagclick={handleHashtagClick}
272+
on:itemrefclick={handleItemRefClick}
299273
/>
300274
</h2>
301275
{/if}

0 commit comments

Comments
 (0)