@streamscloud/kit 0.65.1 → 0.67.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/actions/horizontal-wheel-scroll.d.ts +6 -2
- package/dist/core/actions/horizontal-wheel-scroll.js +3 -1
- package/dist/ui/breadcrumb/breadcrumb-localization.d.ts +1 -0
- package/dist/ui/breadcrumb/breadcrumb-localization.js +7 -0
- package/dist/ui/breadcrumb/cmp.breadcrumb.svelte +151 -13
- package/dist/ui/breadcrumb/cmp.breadcrumb.svelte.d.ts +7 -1
- package/dist/ui/grid-card/cmp.grid-card-media.svelte +2 -2
- package/dist/ui/image/cmp.image.svelte +10 -2
- package/dist/ui/image/cmp.image.svelte.d.ts +2 -0
- package/dist/ui/tree/cmp.tree-node-view.svelte +182 -80
- package/dist/ui/tree/cmp.tree-node-view.svelte.d.ts +4 -1
- package/dist/ui/tree/cmp.tree.svelte +8 -7
- package/dist/ui/tree/cmp.tree.svelte.d.ts +6 -6
- package/dist/ui/tree/index.d.ts +1 -2
- package/dist/ui/tree/tree-drop-target.svelte.d.ts +13 -0
- package/dist/ui/tree/tree-drop-target.svelte.js +30 -0
- package/dist/ui/tree/tree-keyboard.svelte.d.ts +1 -0
- package/dist/ui/tree/tree-keyboard.svelte.js +7 -0
- package/dist/ui/tree/tree-localization.d.ts +3 -0
- package/dist/ui/tree/tree-localization.js +12 -0
- package/dist/ui/tree/tree-node.svelte.d.ts +51 -31
- package/dist/ui/tree/tree-node.svelte.js +103 -123
- package/dist/ui/video/cmp.video.svelte +1 -1
- package/package.json +1 -1
- package/dist/ui/tree/tree-node-drag-config.svelte.d.ts +0 -16
- package/dist/ui/tree/tree-node-drag-config.svelte.js +0 -4
|
@@ -1,69 +1,146 @@
|
|
|
1
1
|
<script lang="ts">import { transitionDuration } from '../../core/transitions';
|
|
2
|
+
import { DragAndDrop } from '../drag-and-drop';
|
|
2
3
|
import { Icon } from '../icon';
|
|
3
4
|
import { IconText } from '../icon-text';
|
|
4
5
|
import { ImageRound } from '../image';
|
|
6
|
+
import { Input } from '../input';
|
|
5
7
|
import { Popover, PopoverItem } from '../popover';
|
|
6
8
|
import Self from './cmp.tree-node-view.svelte';
|
|
7
|
-
import {
|
|
9
|
+
import { TreeDropTarget } from './tree-drop-target.svelte';
|
|
10
|
+
import { TreeLocalization } from './tree-localization';
|
|
8
11
|
import {} from './tree-node.svelte';
|
|
9
|
-
import
|
|
12
|
+
import IconCheckmark from '@fluentui/svg-icons/icons/checkmark_20_regular.svg?raw';
|
|
10
13
|
import IconChevronRight from '@fluentui/svg-icons/icons/chevron_right_20_regular.svg?raw';
|
|
11
14
|
import IconMoreVertical from '@fluentui/svg-icons/icons/more_vertical_20_regular.svg?raw';
|
|
15
|
+
import { untrack } from 'svelte';
|
|
12
16
|
import { slide } from 'svelte/transition';
|
|
13
|
-
let { node, keyboard, hideExpandIcon = false, level = 0
|
|
17
|
+
let { node, keyboard, checked, posInSet, setSize, hideExpandIcon = false, level = 0 } = $props();
|
|
18
|
+
const localization = new TreeLocalization();
|
|
14
19
|
let actionsOpened = $state(false);
|
|
20
|
+
let renameInput = $state();
|
|
21
|
+
let renameDraft = $state('');
|
|
22
|
+
let hoveredNode = null;
|
|
23
|
+
let leafElement = $state();
|
|
15
24
|
let dbClickTimeout = undefined;
|
|
16
|
-
|
|
17
|
-
const children = $derived(node.children || []);
|
|
18
|
-
const hideChildExpandIcon = $derived(children.every((f) => !f.children.length));
|
|
25
|
+
const children = $derived(node.children);
|
|
19
26
|
const visibleActions = $derived(node.actions.filter((a) => !a.visible || a.visible(node)));
|
|
20
|
-
const
|
|
21
|
-
const
|
|
27
|
+
const actionsShown = $derived(!node.renaming && visibleActions.length > 0);
|
|
28
|
+
const isChecked = $derived(checked?.has(node.id) ?? false);
|
|
29
|
+
const acceptsCurrentDrag = $derived(!!DragAndDrop.event && node.accepts(DragAndDrop.event));
|
|
30
|
+
const dragOver = $derived(acceptsCurrentDrag && TreeDropTarget.node === node);
|
|
31
|
+
$effect(() => {
|
|
32
|
+
if (!dragOver || node.expanded || !children.length) {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const timeout = window.setTimeout(() => node.expand(), 500);
|
|
36
|
+
return () => clearTimeout(timeout);
|
|
37
|
+
});
|
|
38
|
+
$effect(() => {
|
|
39
|
+
return () => {
|
|
40
|
+
handleMouseLeave();
|
|
41
|
+
clearTimeout(dbClickTimeout);
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
$effect(() => {
|
|
45
|
+
if (node.renaming) {
|
|
46
|
+
renameDraft = untrack(() => node.name);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
$effect(() => {
|
|
50
|
+
const input = renameInput;
|
|
51
|
+
if (!node.renaming || !input) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Deferred: the actions Popover restores focus on close, which would blur the input and end a rename started from an action.
|
|
55
|
+
const frame = requestAnimationFrame(() => {
|
|
56
|
+
input.focus();
|
|
57
|
+
input.select();
|
|
58
|
+
});
|
|
59
|
+
return () => cancelAnimationFrame(frame);
|
|
60
|
+
});
|
|
22
61
|
const handleDragStart = (e) => {
|
|
23
|
-
|
|
62
|
+
if (!node.drag) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
e.stopPropagation();
|
|
66
|
+
DragAndDrop.event = node.drag;
|
|
24
67
|
if (e.dataTransfer) {
|
|
25
68
|
e.dataTransfer.effectAllowed = 'move';
|
|
26
|
-
e.dataTransfer.setData('
|
|
69
|
+
e.dataTransfer.setData('application/x-sc-tree-node', node.id);
|
|
27
70
|
}
|
|
28
71
|
};
|
|
29
72
|
const handleDragEnd = () => {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
73
|
+
DragAndDrop.event = null;
|
|
74
|
+
};
|
|
75
|
+
const handleDragEnter = () => {
|
|
76
|
+
if (acceptsCurrentDrag && leafElement) {
|
|
77
|
+
TreeDropTarget.claim(node, leafElement);
|
|
78
|
+
}
|
|
33
79
|
};
|
|
34
80
|
const handleDragOver = (e) => {
|
|
35
|
-
if (!
|
|
81
|
+
if (!acceptsCurrentDrag) {
|
|
36
82
|
return;
|
|
37
83
|
}
|
|
38
84
|
e.preventDefault();
|
|
85
|
+
e.stopPropagation();
|
|
39
86
|
if (e.dataTransfer) {
|
|
40
87
|
e.dataTransfer.dropEffect = 'move';
|
|
41
88
|
}
|
|
42
|
-
|
|
43
|
-
DraggingNodeState.dragOverNode = node;
|
|
44
|
-
clearTimeout(dragExpandTimeout);
|
|
45
|
-
if (!node.expanded && children.length) {
|
|
46
|
-
dragExpandTimeout = window.setTimeout(() => node.expand(), 500);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
const handleDragLeave = (e) => {
|
|
51
|
-
const rect = e.currentTarget.getBoundingClientRect();
|
|
52
|
-
const isLeavingElement = e.clientX < rect.left || e.clientX >= rect.right || e.clientY < rect.top || e.clientY >= rect.bottom;
|
|
53
|
-
if (isLeavingElement && DraggingNodeState.dragOverNode?.id === node.id) {
|
|
54
|
-
clearTimeout(dragExpandTimeout);
|
|
55
|
-
DraggingNodeState.dragOverNode = null;
|
|
56
|
-
}
|
|
89
|
+
handleDragEnter();
|
|
57
90
|
};
|
|
58
91
|
const handleDrop = (e) => {
|
|
59
|
-
|
|
92
|
+
const event = DragAndDrop.event;
|
|
93
|
+
if (!event || !acceptsCurrentDrag) {
|
|
60
94
|
return;
|
|
61
95
|
}
|
|
62
96
|
e.preventDefault();
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
97
|
+
e.stopPropagation();
|
|
98
|
+
DragAndDrop.event = null;
|
|
99
|
+
node.receiveDrop(event);
|
|
100
|
+
};
|
|
101
|
+
const handleAnchorKeydown = (e) => {
|
|
102
|
+
if (e.key === 'F2' && node.renaming) {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
renameInput?.focus();
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
keyboard.handleKeydown(node, e);
|
|
108
|
+
};
|
|
109
|
+
const handleRenameKeydown = async (e) => {
|
|
110
|
+
if (e.key === 'Escape') {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
node.cancelRename();
|
|
113
|
+
if (!node.renaming) {
|
|
114
|
+
await keyboard.focusAfterTick(node);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else if (e.key === 'Enter' && !e.isComposing && !node.renamePending) {
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
const accepted = await node.commitRename(renameDraft);
|
|
120
|
+
const focusStayedWithRow = document.activeElement === document.body || !!leafElement?.contains(document.activeElement);
|
|
121
|
+
if (!focusStayedWithRow) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (accepted) {
|
|
125
|
+
await keyboard.focusAfterTick(node);
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
renameInput?.focus();
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
const handleRenameBlur = () => {
|
|
133
|
+
if (!node.renamePending) {
|
|
134
|
+
void node.commitRename(renameDraft);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const handleMouseEnter = () => {
|
|
138
|
+
hoveredNode = node;
|
|
139
|
+
node.notifyHover(true);
|
|
140
|
+
};
|
|
141
|
+
const handleMouseLeave = () => {
|
|
142
|
+
hoveredNode?.notifyHover(false);
|
|
143
|
+
hoveredNode = null;
|
|
67
144
|
};
|
|
68
145
|
const handleSelectNode = (e) => {
|
|
69
146
|
e.stopPropagation();
|
|
@@ -99,45 +176,33 @@ const handleActionsOpened = () => {
|
|
|
99
176
|
const handleActionsClosed = () => {
|
|
100
177
|
actionsOpened = false;
|
|
101
178
|
};
|
|
102
|
-
const handleMouseOver = () => {
|
|
103
|
-
node.notifyHover(true);
|
|
104
|
-
};
|
|
105
|
-
const handleMouseLeave = () => {
|
|
106
|
-
node.notifyHover(false);
|
|
107
|
-
};
|
|
108
179
|
</script>
|
|
109
180
|
|
|
110
|
-
<div
|
|
111
|
-
class="tree-node"
|
|
112
|
-
style={`--_tree-node--level: ${level}`}
|
|
113
|
-
role="treeitem"
|
|
114
|
-
aria-level={level + 1}
|
|
115
|
-
aria-selected={node.selected}
|
|
116
|
-
aria-expanded={children.length ? node.expanded : undefined}>
|
|
117
|
-
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
|
181
|
+
<div class="tree-node" style={`--_tree-node--level: ${level}`} role="none">
|
|
118
182
|
<div
|
|
183
|
+
bind:this={leafElement}
|
|
119
184
|
class="tree-node__leaf"
|
|
120
185
|
class:tree-node__leaf--active={node.selected}
|
|
121
|
-
class:tree-node__leaf--draggable={node.
|
|
186
|
+
class:tree-node__leaf--draggable={node.drag}
|
|
122
187
|
class:tree-node__leaf--expandable={children.length}
|
|
123
|
-
class:tree-node__leaf--drag-over={
|
|
124
|
-
draggable={!!node.
|
|
188
|
+
class:tree-node__leaf--drag-over={dragOver}
|
|
189
|
+
draggable={!!node.drag && !node.renaming}
|
|
125
190
|
ondragstart={handleDragStart}
|
|
126
191
|
ondragend={handleDragEnd}
|
|
192
|
+
ondragenter={handleDragEnter}
|
|
127
193
|
ondragover={handleDragOver}
|
|
128
|
-
ondragleave={handleDragLeave}
|
|
129
194
|
ondrop={handleDrop}
|
|
130
|
-
|
|
195
|
+
onmouseenter={handleMouseEnter}
|
|
131
196
|
onmouseleave={handleMouseLeave}
|
|
132
197
|
role="none">
|
|
133
|
-
{#if !hideExpandIcon
|
|
198
|
+
{#if !hideExpandIcon}
|
|
134
199
|
<button
|
|
135
200
|
type="button"
|
|
136
201
|
class="tree-node__expand-button"
|
|
137
202
|
class:tree-node__expand-button--hidden={!children.length}
|
|
138
|
-
class:tree-node__expand-button--active={node.selected || node.involvedIntoActivePath}
|
|
139
203
|
tabindex="-1"
|
|
140
204
|
aria-hidden="true"
|
|
205
|
+
onmousedown={(e) => e.preventDefault()}
|
|
141
206
|
onclick={handleExpandNode}>
|
|
142
207
|
<span class="tree-node__expand-icon" class:tree-node__expand-icon--expanded={node.expanded}>
|
|
143
208
|
<Icon src={IconChevronRight} />
|
|
@@ -147,37 +212,60 @@ const handleMouseLeave = () => {
|
|
|
147
212
|
|
|
148
213
|
<div
|
|
149
214
|
class="tree-node__anchor"
|
|
215
|
+
class:tree-node__anchor--renaming={node.renaming}
|
|
150
216
|
id={keyboard.anchorId(node)}
|
|
151
|
-
role="
|
|
217
|
+
role="treeitem"
|
|
218
|
+
aria-level={level + 1}
|
|
219
|
+
aria-posinset={posInSet}
|
|
220
|
+
aria-setsize={setSize}
|
|
221
|
+
aria-selected={node.selected}
|
|
222
|
+
aria-checked={checked ? isChecked : undefined}
|
|
223
|
+
aria-expanded={children.length ? node.expanded : undefined}
|
|
152
224
|
tabindex={keyboard.currentId === node.id ? 0 : -1}
|
|
153
225
|
onclick={handleSelectNode}
|
|
154
226
|
ondblclick={handleExpandOnDoubleClick}
|
|
155
227
|
onfocusin={() => keyboard.setFocused(node.id)}
|
|
156
|
-
onkeydown={
|
|
157
|
-
{#if
|
|
158
|
-
<div class="tree-node__icon"
|
|
159
|
-
{#if node.icon}
|
|
228
|
+
onkeydown={handleAnchorKeydown}>
|
|
229
|
+
{#if node.icon}
|
|
230
|
+
<div class="tree-node__icon">
|
|
231
|
+
{#if typeof node.icon === 'string'}
|
|
160
232
|
<Icon src={node.icon} />
|
|
161
|
-
{
|
|
162
|
-
|
|
163
|
-
<ImageRound src={node.imgIcon} alt="" />
|
|
233
|
+
{:else}
|
|
234
|
+
<ImageRound src={node.icon.image} alt="" />
|
|
164
235
|
{/if}
|
|
165
236
|
</div>
|
|
166
237
|
{/if}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
238
|
+
{#if node.renaming}
|
|
239
|
+
<div
|
|
240
|
+
class="tree-node__rename"
|
|
241
|
+
onclick={(e) => e.stopPropagation()}
|
|
242
|
+
ondblclick={(e) => e.stopPropagation()}
|
|
243
|
+
onkeydown={(e) => e.stopPropagation()}
|
|
244
|
+
role="none">
|
|
245
|
+
<Input
|
|
246
|
+
bind:this={renameInput}
|
|
247
|
+
value={renameDraft}
|
|
248
|
+
readonly={node.renamePending}
|
|
249
|
+
aria-label={localization.nodeName}
|
|
250
|
+
on={{ input: (v) => (renameDraft = v), keydown: handleRenameKeydown, blur: handleRenameBlur }} />
|
|
251
|
+
</div>
|
|
252
|
+
{:else}
|
|
253
|
+
<div class="tree-node__text" class:tree-node__text--active={node.selected}>
|
|
254
|
+
{node.name}
|
|
255
|
+
{#if node.dirty}*{/if}
|
|
256
|
+
</div>
|
|
257
|
+
{#if node.count}
|
|
258
|
+
<div class="tree-node__count">{node.count}</div>
|
|
259
|
+
{/if}
|
|
173
260
|
{/if}
|
|
174
261
|
</div>
|
|
175
262
|
|
|
176
|
-
{#if
|
|
177
|
-
<div class="
|
|
178
|
-
<Icon src={
|
|
263
|
+
{#if isChecked && !node.renaming}
|
|
264
|
+
<div class="tree-node__check" aria-hidden="true">
|
|
265
|
+
<Icon src={IconCheckmark} color="accent" />
|
|
179
266
|
</div>
|
|
180
|
-
{
|
|
267
|
+
{/if}
|
|
268
|
+
{#if actionsShown}
|
|
181
269
|
<div class="node-actions" class:node-actions--opened={actionsOpened}>
|
|
182
270
|
<Popover position="bottom-end" fixedPosition on={{ opened: handleActionsOpened, closed: handleActionsClosed }}>
|
|
183
271
|
{#snippet trigger()}
|
|
@@ -196,9 +284,9 @@ const handleMouseLeave = () => {
|
|
|
196
284
|
{/if}
|
|
197
285
|
</div>
|
|
198
286
|
{#if node.expanded && children.length}
|
|
199
|
-
<div transition:slide={{ duration: transitionDuration.base }} class="tree-node__children" role="
|
|
200
|
-
{#each children as child (child.id)}
|
|
201
|
-
<Self node={child} keyboard={keyboard}
|
|
287
|
+
<div transition:slide={{ duration: transitionDuration.base }} class="tree-node__children" role="none">
|
|
288
|
+
{#each children as child, i (child.id)}
|
|
289
|
+
<Self node={child} keyboard={keyboard} checked={checked} posInSet={i + 1} setSize={children.length} level={level + 1} />
|
|
202
290
|
{/each}
|
|
203
291
|
</div>
|
|
204
292
|
{/if}
|
|
@@ -253,6 +341,7 @@ const handleMouseLeave = () => {
|
|
|
253
341
|
.tree-node__leaf--drag-over {
|
|
254
342
|
background: var(--sc-kit--color--bg--menu-active);
|
|
255
343
|
outline: 1px solid var(--sc-kit--color--accent);
|
|
344
|
+
outline-offset: -1px;
|
|
256
345
|
}
|
|
257
346
|
.tree-node__children {
|
|
258
347
|
width: 100%;
|
|
@@ -287,6 +376,9 @@ const handleMouseLeave = () => {
|
|
|
287
376
|
padding: var(--_tree-node--padding-block) 0;
|
|
288
377
|
outline: none;
|
|
289
378
|
}
|
|
379
|
+
.tree-node__anchor--renaming {
|
|
380
|
+
padding-block: 0;
|
|
381
|
+
}
|
|
290
382
|
.tree-node__icon {
|
|
291
383
|
--sc-kit--icon--size: var(--_tree-node--icon-size);
|
|
292
384
|
display: inline-flex;
|
|
@@ -297,6 +389,12 @@ const handleMouseLeave = () => {
|
|
|
297
389
|
height: var(--_tree-node--icon-size);
|
|
298
390
|
color: var(--sc-kit--color--text--muted);
|
|
299
391
|
}
|
|
392
|
+
.tree-node__check {
|
|
393
|
+
display: flex;
|
|
394
|
+
flex-shrink: 0;
|
|
395
|
+
align-items: center;
|
|
396
|
+
margin-left: var(--sc-kit--space--1);
|
|
397
|
+
}
|
|
300
398
|
.tree-node__text {
|
|
301
399
|
flex: 1;
|
|
302
400
|
min-width: 0;
|
|
@@ -309,6 +407,12 @@ const handleMouseLeave = () => {
|
|
|
309
407
|
.tree-node__text--active {
|
|
310
408
|
font-weight: var(--sc-kit--font-weight--semibold);
|
|
311
409
|
}
|
|
410
|
+
.tree-node__rename {
|
|
411
|
+
--sc-kit--input--font-size: var(--_tree-node--font-size);
|
|
412
|
+
flex: 1;
|
|
413
|
+
min-width: 0;
|
|
414
|
+
user-select: text;
|
|
415
|
+
}
|
|
312
416
|
.tree-node__count {
|
|
313
417
|
flex-shrink: 0;
|
|
314
418
|
font-size: var(--sc-kit--font-size--xs);
|
|
@@ -316,12 +420,10 @@ const handleMouseLeave = () => {
|
|
|
316
420
|
}
|
|
317
421
|
|
|
318
422
|
.node-actions {
|
|
319
|
-
position: absolute;
|
|
320
|
-
top: 0;
|
|
321
|
-
bottom: 0;
|
|
322
|
-
right: var(--sc-kit--space--2);
|
|
323
423
|
display: flex;
|
|
424
|
+
flex-shrink: 0;
|
|
324
425
|
align-items: center;
|
|
426
|
+
margin-left: var(--sc-kit--space--1);
|
|
325
427
|
opacity: var(--_tree-node--action-opacity);
|
|
326
428
|
transition: opacity var(--sc-kit--duration--base) var(--sc-kit--ease--default);
|
|
327
429
|
}
|
|
@@ -3,9 +3,12 @@ import { type TreeNode } from './tree-node.svelte';
|
|
|
3
3
|
type Props = {
|
|
4
4
|
node: TreeNode;
|
|
5
5
|
keyboard: TreeKeyboard;
|
|
6
|
+
checked: ReadonlySet<string> | null;
|
|
7
|
+
posInSet: number;
|
|
8
|
+
setSize: number;
|
|
9
|
+
/** Drops the chevron slot; set by the tree on roots when no root is expandable. */
|
|
6
10
|
hideExpandIcon?: boolean;
|
|
7
11
|
level?: number;
|
|
8
|
-
showSelectedCheckmark?: boolean;
|
|
9
12
|
};
|
|
10
13
|
declare const Cmp: import("svelte").Component<Props, {}, "">;
|
|
11
14
|
type Cmp = ReturnType<typeof Cmp>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<script lang="ts">import NodeView from './cmp.tree-node-view.svelte';
|
|
2
2
|
import { createTreeKeyboard } from './tree-keyboard.svelte';
|
|
3
|
-
let { nodes,
|
|
3
|
+
let { nodes, checked, header } = $props();
|
|
4
4
|
const nodesComputed = $derived(nodes ? (Array.isArray(nodes) ? nodes : [nodes]) : []);
|
|
5
5
|
const hideExpandIcon = $derived(nodesComputed.every((f) => !f.children.length));
|
|
6
|
+
const checkedSet = $derived(checked ? new Set(checked) : null);
|
|
6
7
|
$effect(() => {
|
|
7
8
|
for (const root of nodesComputed) {
|
|
8
9
|
root.setForest(() => nodesComputed);
|
|
@@ -14,8 +15,8 @@ const keyboard = createTreeKeyboard(() => nodesComputed);
|
|
|
14
15
|
<div class="tree">
|
|
15
16
|
{@render header?.()}
|
|
16
17
|
<div class="tree__nodes" role="tree">
|
|
17
|
-
{#each nodesComputed as node (node.id)}
|
|
18
|
-
<NodeView node={node} keyboard={keyboard}
|
|
18
|
+
{#each nodesComputed as node, i (node.id)}
|
|
19
|
+
<NodeView node={node} keyboard={keyboard} checked={checkedSet} posInSet={i + 1} setSize={nodesComputed.length} hideExpandIcon={hideExpandIcon} />
|
|
19
20
|
{/each}
|
|
20
21
|
</div>
|
|
21
22
|
</div>
|
|
@@ -23,13 +24,13 @@ const keyboard = createTreeKeyboard(() => nodesComputed);
|
|
|
23
24
|
<!--
|
|
24
25
|
@component
|
|
25
26
|
Tree — hierarchical, collapsible node list driven by `TreeNode` model instances. Each `TreeNode`
|
|
26
|
-
holds its own expanded / selected state, children, optional icon / count / actions,
|
|
27
|
-
|
|
28
|
-
root(s) as `nodes
|
|
27
|
+
holds its own expanded / selected state, children, optional icon / count / actions, optional drag /
|
|
28
|
+
drop over the shared `DragAndDrop` store, and optional inline rename. Build the model with
|
|
29
|
+
`new TreeNode({ … })` and pass the root(s) as `nodes`; `checked` marks any set of nodes with a checkmark.
|
|
29
30
|
|
|
30
31
|
Click selects a node — selection is the single source of truth. Keyboard: the current row is the tab
|
|
31
32
|
stop (native focus + `:focus-visible`), ↑/↓ move between visible rows, →/← expand-or-enter / collapse-or-leave,
|
|
32
|
-
Home/End jump to first/last, Enter/Space select.
|
|
33
|
+
Home/End jump to first/last, Enter/Space select, F2 renames. In rename mode Enter / blur commit, Esc cancels.
|
|
33
34
|
|
|
34
35
|
### CSS Custom Properties
|
|
35
36
|
| Property | Description | Default |
|
|
@@ -3,20 +3,20 @@ import type { Snippet } from 'svelte';
|
|
|
3
3
|
type Props = {
|
|
4
4
|
/** Root node, or array of root nodes. */
|
|
5
5
|
nodes: TreeNode | readonly TreeNode[];
|
|
6
|
-
/**
|
|
7
|
-
|
|
6
|
+
/** Ids of nodes that show a checkmark. Display only — the tree never changes it. */
|
|
7
|
+
checked?: readonly string[];
|
|
8
8
|
/** Optional content rendered above the nodes (e.g. a header). */
|
|
9
9
|
header?: Snippet;
|
|
10
10
|
};
|
|
11
11
|
/**
|
|
12
12
|
* Tree — hierarchical, collapsible node list driven by `TreeNode` model instances. Each `TreeNode`
|
|
13
|
-
* holds its own expanded / selected state, children, optional icon / count / actions,
|
|
14
|
-
*
|
|
15
|
-
* root(s) as `nodes
|
|
13
|
+
* holds its own expanded / selected state, children, optional icon / count / actions, optional drag /
|
|
14
|
+
* drop over the shared `DragAndDrop` store, and optional inline rename. Build the model with
|
|
15
|
+
* `new TreeNode({ … })` and pass the root(s) as `nodes`; `checked` marks any set of nodes with a checkmark.
|
|
16
16
|
*
|
|
17
17
|
* Click selects a node — selection is the single source of truth. Keyboard: the current row is the tab
|
|
18
18
|
* stop (native focus + `:focus-visible`), ↑/↓ move between visible rows, →/← expand-or-enter / collapse-or-leave,
|
|
19
|
-
* Home/End jump to first/last, Enter/Space select.
|
|
19
|
+
* Home/End jump to first/last, Enter/Space select, F2 renames. In rename mode Enter / blur commit, Esc cancels.
|
|
20
20
|
*
|
|
21
21
|
* ### CSS Custom Properties
|
|
22
22
|
* | Property | Description | Default |
|
package/dist/ui/tree/index.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export { default as Tree } from './cmp.tree.svelte';
|
|
2
|
-
export { TreeNode, type TreeNodeInitializer } from './tree-node.svelte';
|
|
2
|
+
export { TreeNode, type TreeNodeIcon, type TreeNodeInitializer } from './tree-node.svelte';
|
|
3
3
|
export type { TreeNodeAction } from './tree-node-action';
|
|
4
|
-
export type { TreeNodeDragConfig, TreeNodeMoveToFolderCallbackParams } from './tree-node-drag-config.svelte';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { TreeNode } from './tree-node.svelte';
|
|
2
|
+
declare class TreeDropTargetContainer {
|
|
3
|
+
node: TreeNode | null;
|
|
4
|
+
private _element;
|
|
5
|
+
private _listening;
|
|
6
|
+
claim(node: TreeNode, element: HTMLElement): void;
|
|
7
|
+
private _release;
|
|
8
|
+
private _releaseOutside;
|
|
9
|
+
private _listen;
|
|
10
|
+
}
|
|
11
|
+
/** The single row under the pointer that accepts the current drag, shared by every row of every tree. */
|
|
12
|
+
export declare const TreeDropTarget: TreeDropTargetContainer;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class TreeDropTargetContainer {
|
|
2
|
+
node = $state.raw(null);
|
|
3
|
+
_element = null;
|
|
4
|
+
_listening = false;
|
|
5
|
+
claim(node, element) {
|
|
6
|
+
this.node = node;
|
|
7
|
+
this._element = element;
|
|
8
|
+
this._listen();
|
|
9
|
+
}
|
|
10
|
+
_release = () => {
|
|
11
|
+
this.node = null;
|
|
12
|
+
this._element = null;
|
|
13
|
+
};
|
|
14
|
+
_releaseOutside = (e) => {
|
|
15
|
+
if (!(e.target instanceof Node) || !this._element?.contains(e.target)) {
|
|
16
|
+
this._release();
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
_listen() {
|
|
20
|
+
if (this._listening) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
this._listening = true;
|
|
24
|
+
window.addEventListener('dragenter', this._releaseOutside, true);
|
|
25
|
+
window.addEventListener('drop', this._release, true);
|
|
26
|
+
window.addEventListener('dragend', this._release, true);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/** The single row under the pointer that accepts the current drag, shared by every row of every tree. */
|
|
30
|
+
export const TreeDropTarget = new TreeDropTargetContainer();
|
|
@@ -4,6 +4,7 @@ export declare const createTreeKeyboard: (getRoots: () => readonly TreeNode[]) =
|
|
|
4
4
|
anchorId: (node: TreeNode) => string;
|
|
5
5
|
readonly currentId: string | null;
|
|
6
6
|
setFocused: (id: string) => void;
|
|
7
|
+
focusAfterTick: (node: TreeNode) => Promise<void>;
|
|
7
8
|
handleKeydown: (node: TreeNode, e: KeyboardEvent) => void;
|
|
8
9
|
};
|
|
9
10
|
export type TreeKeyboard = ReturnType<typeof createTreeKeyboard>;
|
|
@@ -52,6 +52,7 @@ export const createTreeKeyboard = (getRoots) => {
|
|
|
52
52
|
setFocused: (id) => {
|
|
53
53
|
focusedId = id;
|
|
54
54
|
},
|
|
55
|
+
focusAfterTick,
|
|
55
56
|
handleKeydown: (node, e) => {
|
|
56
57
|
switch (e.key) {
|
|
57
58
|
case 'ArrowDown':
|
|
@@ -101,6 +102,12 @@ export const createTreeKeyboard = (getRoots) => {
|
|
|
101
102
|
e.preventDefault();
|
|
102
103
|
node.select({ explicitly: true });
|
|
103
104
|
break;
|
|
105
|
+
case 'F2':
|
|
106
|
+
if (node.renamable) {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
node.startRename();
|
|
109
|
+
}
|
|
110
|
+
break;
|
|
104
111
|
}
|
|
105
112
|
}
|
|
106
113
|
};
|