@streamscloud/kit 0.65.1 → 0.66.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/tree/cmp.tree-node-view.svelte +181 -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/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,5 +1,9 @@
|
|
|
1
1
|
import type { Action } from 'svelte/action';
|
|
2
|
-
|
|
2
|
+
type HorizontalWheelScrollParams = {
|
|
3
3
|
isolateScroll: boolean;
|
|
4
|
+
/** `false` lets the wheel through to the page once the element can't scroll further that way. @default true */
|
|
5
|
+
isolateAtEdges?: boolean;
|
|
4
6
|
speed?: number;
|
|
5
|
-
}
|
|
7
|
+
};
|
|
8
|
+
export declare const horizontalWheelScroll: Action<HTMLElement, HorizontalWheelScrollParams | undefined>;
|
|
9
|
+
export {};
|
|
@@ -5,8 +5,10 @@ export const horizontalWheelScroll = (node, param = { isolateScroll: true, speed
|
|
|
5
5
|
return;
|
|
6
6
|
}
|
|
7
7
|
const delta = Math.abs(event.deltaX) > Math.abs(event.deltaY) ? event.deltaX : event.deltaY;
|
|
8
|
+
const maxScrollLeft = node.scrollWidth - node.clientWidth;
|
|
9
|
+
const atEdge = (delta < 0 && node.scrollLeft <= 0) || (delta > 0 && node.scrollLeft >= maxScrollLeft - 1);
|
|
8
10
|
node.scrollBy({ left: delta * (param.speed ?? 1) });
|
|
9
|
-
if (param.isolateScroll) {
|
|
11
|
+
if (param.isolateScroll && (param.isolateAtEdges !== false || !atEdge)) {
|
|
10
12
|
event.preventDefault();
|
|
11
13
|
event.stopPropagation();
|
|
12
14
|
}
|
|
@@ -3,10 +3,17 @@ const loc = {
|
|
|
3
3
|
navLabel: {
|
|
4
4
|
en: 'Breadcrumb',
|
|
5
5
|
no: 'Brødsmuler'
|
|
6
|
+
},
|
|
7
|
+
hiddenLevels: {
|
|
8
|
+
en: 'Show hidden levels',
|
|
9
|
+
no: 'Vis skjulte nivåer'
|
|
6
10
|
}
|
|
7
11
|
};
|
|
8
12
|
export class BreadcrumbLocalization {
|
|
9
13
|
get navLabel() {
|
|
10
14
|
return loc.navLabel[AppLocale.current];
|
|
11
15
|
}
|
|
16
|
+
get hiddenLevels() {
|
|
17
|
+
return loc.hiddenLevels[AppLocale.current];
|
|
18
|
+
}
|
|
12
19
|
}
|
|
@@ -1,19 +1,95 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
2
|
-
|
|
1
|
+
<script lang="ts">import { horizontalWheelScroll } from '../../core/actions';
|
|
2
|
+
import { Popover } from '../popover';
|
|
3
|
+
import { BreadcrumbLocalization } from './breadcrumb-localization';
|
|
4
|
+
import { tick } from 'svelte';
|
|
5
|
+
let { items, separator = '/', underline = 'hover', overflow = 'wrap', maxItems } = $props();
|
|
3
6
|
const localization = new BreadcrumbLocalization();
|
|
7
|
+
let canScrollBack = $state(false);
|
|
8
|
+
let canScrollForward = $state(false);
|
|
9
|
+
let menuOpened = $state(false);
|
|
10
|
+
const tailLength = $derived(maxItems === undefined ? 0 : Math.max(2, maxItems) - 1);
|
|
11
|
+
const collapsed = $derived(maxItems !== undefined && items.length > tailLength + 1);
|
|
12
|
+
const hiddenItems = $derived(collapsed ? items.slice(1, items.length - tailLength) : []);
|
|
13
|
+
const shownItems = $derived(collapsed ? [items[0], ...items.slice(items.length - tailLength)] : items);
|
|
14
|
+
const scrolls = $derived(overflow === 'scroll');
|
|
15
|
+
// The "…" Popover renders inside the track; a mask would hide it.
|
|
16
|
+
const masked = $derived(scrolls && !menuOpened);
|
|
17
|
+
const updateScrollState = (node) => {
|
|
18
|
+
canScrollBack = node.scrollLeft > 0;
|
|
19
|
+
canScrollForward = node.scrollLeft < node.scrollWidth - node.clientWidth - 1;
|
|
20
|
+
};
|
|
21
|
+
const scrollTrack = (node, _items) => {
|
|
22
|
+
const scrollToCurrent = () => {
|
|
23
|
+
if (scrolls) {
|
|
24
|
+
node.scrollLeft = node.scrollWidth;
|
|
25
|
+
}
|
|
26
|
+
updateScrollState(node);
|
|
27
|
+
};
|
|
28
|
+
// Children too: the track's own box is pinned at max-width, so a web-font swap only resizes its content.
|
|
29
|
+
const observer = new ResizeObserver(scrollToCurrent);
|
|
30
|
+
const observeTrackAndCrumbs = () => {
|
|
31
|
+
observer.disconnect();
|
|
32
|
+
observer.observe(node);
|
|
33
|
+
for (const child of node.children) {
|
|
34
|
+
observer.observe(child);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
observeTrackAndCrumbs();
|
|
38
|
+
return {
|
|
39
|
+
update: async () => {
|
|
40
|
+
await tick();
|
|
41
|
+
observeTrackAndCrumbs();
|
|
42
|
+
scrollToCurrent();
|
|
43
|
+
},
|
|
44
|
+
destroy: () => observer.disconnect()
|
|
45
|
+
};
|
|
46
|
+
};
|
|
4
47
|
</script>
|
|
5
48
|
|
|
6
|
-
|
|
7
|
-
{#
|
|
49
|
+
{#snippet crumb(item: BreadcrumbItem, current: boolean, menuItem: boolean)}
|
|
50
|
+
{#if current}
|
|
51
|
+
<span class="breadcrumb__current" aria-current="page">{item.label}</span>
|
|
52
|
+
{:else if item.href}
|
|
53
|
+
<a class="breadcrumb__link" class:breadcrumb__link--menu={menuItem} href={item.href} onclick={(e) => item.on?.click?.(e)}>{item.label}</a>
|
|
54
|
+
{:else if item.on?.click}
|
|
55
|
+
<button type="button" class="breadcrumb__link" class:breadcrumb__link--menu={menuItem} onclick={(e) => item.on?.click?.(e)}>{item.label}</button>
|
|
56
|
+
{:else}
|
|
57
|
+
<span class="breadcrumb__current" class:breadcrumb__current--menu={menuItem}>{item.label}</span>
|
|
58
|
+
{/if}
|
|
59
|
+
{/snippet}
|
|
60
|
+
|
|
61
|
+
<div
|
|
62
|
+
class="breadcrumb breadcrumb--underline-{underline}"
|
|
63
|
+
class:breadcrumb--scroll={scrolls}
|
|
64
|
+
class:breadcrumb--mask-back={masked && canScrollBack && !canScrollForward}
|
|
65
|
+
class:breadcrumb--mask-forward={masked && canScrollForward && !canScrollBack}
|
|
66
|
+
class:breadcrumb--mask-both={masked && canScrollBack && canScrollForward}
|
|
67
|
+
role="navigation"
|
|
68
|
+
aria-label={localization.navLabel}
|
|
69
|
+
use:scrollTrack={shownItems}
|
|
70
|
+
use:horizontalWheelScroll={{ isolateScroll: scrolls, isolateAtEdges: false }}
|
|
71
|
+
onscroll={(e) => updateScrollState(e.currentTarget)}>
|
|
72
|
+
{#each shownItems as item, i (`${i}:${item.label}`)}
|
|
8
73
|
{#if i > 0}<span class="breadcrumb__separator" aria-hidden="true">{separator}</span>{/if}
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
74
|
+
{@render crumb(item, i === shownItems.length - 1, false)}
|
|
75
|
+
{#if i === 0 && collapsed}
|
|
76
|
+
<span class="breadcrumb__separator" aria-hidden="true">{separator}</span>
|
|
77
|
+
<span class="breadcrumb__more" onwheel={(e) => e.stopPropagation()}>
|
|
78
|
+
<Popover
|
|
79
|
+
position="bottom-start"
|
|
80
|
+
fixedPosition
|
|
81
|
+
aria-label={localization.hiddenLevels}
|
|
82
|
+
on={{ opened: () => (menuOpened = true), closed: () => (menuOpened = false) }}>
|
|
83
|
+
{#snippet trigger()}
|
|
84
|
+
<span class="breadcrumb__more-trigger">…</span>
|
|
85
|
+
{/snippet}
|
|
86
|
+
<div class="breadcrumb__menu">
|
|
87
|
+
{#each hiddenItems as hidden, j (`${j}:${hidden.label}`)}
|
|
88
|
+
{@render crumb(hidden, false, true)}
|
|
89
|
+
{/each}
|
|
90
|
+
</div>
|
|
91
|
+
</Popover>
|
|
92
|
+
</span>
|
|
17
93
|
{/if}
|
|
18
94
|
{/each}
|
|
19
95
|
</div>
|
|
@@ -27,7 +103,9 @@ A horizontal trail of crumbs ending at the current page. Each non-last item rend
|
|
|
27
103
|
- plain text `<span>` when neither is set.
|
|
28
104
|
|
|
29
105
|
The last item is always plain text with `aria-current="page"`. `underline` decides whether the crumb
|
|
30
|
-
links show their underline on hover only (default), always, or never.
|
|
106
|
+
links show their underline on hover only (default), always, or never. `overflow="scroll"` keeps the
|
|
107
|
+
trail on one line that scrolls sideways; `maxItems` folds the middle into a "…" crumb that lists the
|
|
108
|
+
hidden levels.
|
|
31
109
|
|
|
32
110
|
### CSS Custom Properties
|
|
33
111
|
| Property | Description | Default |
|
|
@@ -63,7 +141,34 @@ links show their underline on hover only (default), always, or never.
|
|
|
63
141
|
.breadcrumb--underline-never {
|
|
64
142
|
--_breadcrumb--link-text-decoration-hover: none;
|
|
65
143
|
}
|
|
144
|
+
.breadcrumb--scroll {
|
|
145
|
+
--_breadcrumb--mask-size: 1.5rem;
|
|
146
|
+
flex-wrap: nowrap;
|
|
147
|
+
max-width: 100%;
|
|
148
|
+
padding-block: 0.25rem;
|
|
149
|
+
margin-block: -0.25rem;
|
|
150
|
+
white-space: nowrap;
|
|
151
|
+
overflow-x: auto;
|
|
152
|
+
scrollbar-width: none;
|
|
153
|
+
}
|
|
154
|
+
.breadcrumb--scroll::-webkit-scrollbar {
|
|
155
|
+
display: none;
|
|
156
|
+
}
|
|
157
|
+
.breadcrumb--mask-back {
|
|
158
|
+
mask-image: linear-gradient(to right, transparent 0%, black var(--_breadcrumb--mask-size), black 100%);
|
|
159
|
+
}
|
|
160
|
+
.breadcrumb--mask-forward {
|
|
161
|
+
mask-image: linear-gradient(to right, black 0%, black calc(100% - var(--_breadcrumb--mask-size)), transparent 100%);
|
|
162
|
+
}
|
|
163
|
+
.breadcrumb--mask-both {
|
|
164
|
+
mask-image: linear-gradient(to right, transparent 0%, black var(--_breadcrumb--mask-size), black calc(100% - var(--_breadcrumb--mask-size)), transparent 100%);
|
|
165
|
+
}
|
|
66
166
|
.breadcrumb__link {
|
|
167
|
+
padding: 0;
|
|
168
|
+
font: inherit;
|
|
169
|
+
background: none;
|
|
170
|
+
border: none;
|
|
171
|
+
cursor: pointer;
|
|
67
172
|
color: var(--_breadcrumb--link-color);
|
|
68
173
|
text-decoration: var(--_breadcrumb--link-text-decoration);
|
|
69
174
|
transition: color var(--sc-kit--duration--base) var(--sc-kit--ease--default);
|
|
@@ -77,11 +182,44 @@ links show their underline on hover only (default), always, or never.
|
|
|
77
182
|
outline-offset: 2px;
|
|
78
183
|
border-radius: var(--sc-kit--radius--sm);
|
|
79
184
|
}
|
|
185
|
+
.breadcrumb__link--menu {
|
|
186
|
+
display: block;
|
|
187
|
+
width: 100%;
|
|
188
|
+
padding: var(--sc-kit--space--1) var(--sc-kit--space--3);
|
|
189
|
+
text-align: start;
|
|
190
|
+
border-radius: var(--sc-kit--radius--sm);
|
|
191
|
+
white-space: nowrap;
|
|
192
|
+
}
|
|
193
|
+
.breadcrumb__link--menu:hover {
|
|
194
|
+
background: var(--sc-kit--color--bg--hover);
|
|
195
|
+
}
|
|
80
196
|
.breadcrumb__current {
|
|
81
197
|
color: var(--_breadcrumb--current-color);
|
|
82
198
|
font-weight: var(--sc-kit--font-weight--medium);
|
|
83
199
|
}
|
|
200
|
+
.breadcrumb__current--menu {
|
|
201
|
+
display: block;
|
|
202
|
+
padding: var(--sc-kit--space--1) var(--sc-kit--space--3);
|
|
203
|
+
font-weight: var(--sc-kit--font-weight--regular);
|
|
204
|
+
white-space: nowrap;
|
|
205
|
+
}
|
|
84
206
|
.breadcrumb__separator {
|
|
85
207
|
color: var(--_breadcrumb--color);
|
|
86
208
|
user-select: none;
|
|
209
|
+
}
|
|
210
|
+
.breadcrumb__more {
|
|
211
|
+
display: inline-flex;
|
|
212
|
+
}
|
|
213
|
+
.breadcrumb__more-trigger {
|
|
214
|
+
color: var(--_breadcrumb--link-color);
|
|
215
|
+
transition: color var(--sc-kit--duration--base) var(--sc-kit--ease--default);
|
|
216
|
+
}
|
|
217
|
+
.breadcrumb__more-trigger:hover {
|
|
218
|
+
color: var(--_breadcrumb--link-color-hover);
|
|
219
|
+
}
|
|
220
|
+
.breadcrumb__menu {
|
|
221
|
+
display: flex;
|
|
222
|
+
flex-direction: column;
|
|
223
|
+
padding: var(--sc-kit--space--1);
|
|
224
|
+
font-size: var(--_breadcrumb--font-size);
|
|
87
225
|
}</style>
|
|
@@ -5,6 +5,10 @@ type Props = {
|
|
|
5
5
|
separator?: string;
|
|
6
6
|
/** When the crumb links show their underline: only on hover, permanently, or not at all. @default 'hover' */
|
|
7
7
|
underline?: 'hover' | 'always' | 'never';
|
|
8
|
+
/** `'wrap'` breaks the trail onto new lines; `'scroll'` keeps one line that scrolls sideways (wheel included) and starts scrolled to the current crumb. @default 'wrap' */
|
|
9
|
+
overflow?: 'wrap' | 'scroll';
|
|
10
|
+
/** Collapse the middle of a longer trail: keep the first crumb and the last `maxItems - 1`, the hidden ones open from a "…" crumb. Values below 2 count as 2. */
|
|
11
|
+
maxItems?: number;
|
|
8
12
|
};
|
|
9
13
|
/**
|
|
10
14
|
* A horizontal trail of crumbs ending at the current page. Each non-last item renders as:
|
|
@@ -14,7 +18,9 @@ type Props = {
|
|
|
14
18
|
* - plain text `<span>` when neither is set.
|
|
15
19
|
*
|
|
16
20
|
* The last item is always plain text with `aria-current="page"`. `underline` decides whether the crumb
|
|
17
|
-
* links show their underline on hover only (default), always, or never.
|
|
21
|
+
* links show their underline on hover only (default), always, or never. `overflow="scroll"` keeps the
|
|
22
|
+
* trail on one line that scrolls sideways; `maxItems` folds the middle into a "…" crumb that lists the
|
|
23
|
+
* hidden levels.
|
|
18
24
|
*
|
|
19
25
|
* ### CSS Custom Properties
|
|
20
26
|
* | Property | Description | Default |
|
|
@@ -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}
|
|
@@ -287,6 +375,9 @@ const handleMouseLeave = () => {
|
|
|
287
375
|
padding: var(--_tree-node--padding-block) 0;
|
|
288
376
|
outline: none;
|
|
289
377
|
}
|
|
378
|
+
.tree-node__anchor--renaming {
|
|
379
|
+
padding-block: 0;
|
|
380
|
+
}
|
|
290
381
|
.tree-node__icon {
|
|
291
382
|
--sc-kit--icon--size: var(--_tree-node--icon-size);
|
|
292
383
|
display: inline-flex;
|
|
@@ -297,6 +388,12 @@ const handleMouseLeave = () => {
|
|
|
297
388
|
height: var(--_tree-node--icon-size);
|
|
298
389
|
color: var(--sc-kit--color--text--muted);
|
|
299
390
|
}
|
|
391
|
+
.tree-node__check {
|
|
392
|
+
display: flex;
|
|
393
|
+
flex-shrink: 0;
|
|
394
|
+
align-items: center;
|
|
395
|
+
margin-left: var(--sc-kit--space--1);
|
|
396
|
+
}
|
|
300
397
|
.tree-node__text {
|
|
301
398
|
flex: 1;
|
|
302
399
|
min-width: 0;
|
|
@@ -309,6 +406,12 @@ const handleMouseLeave = () => {
|
|
|
309
406
|
.tree-node__text--active {
|
|
310
407
|
font-weight: var(--sc-kit--font-weight--semibold);
|
|
311
408
|
}
|
|
409
|
+
.tree-node__rename {
|
|
410
|
+
--sc-kit--input--font-size: var(--_tree-node--font-size);
|
|
411
|
+
flex: 1;
|
|
412
|
+
min-width: 0;
|
|
413
|
+
user-select: text;
|
|
414
|
+
}
|
|
312
415
|
.tree-node__count {
|
|
313
416
|
flex-shrink: 0;
|
|
314
417
|
font-size: var(--sc-kit--font-size--xs);
|
|
@@ -316,12 +419,10 @@ const handleMouseLeave = () => {
|
|
|
316
419
|
}
|
|
317
420
|
|
|
318
421
|
.node-actions {
|
|
319
|
-
position: absolute;
|
|
320
|
-
top: 0;
|
|
321
|
-
bottom: 0;
|
|
322
|
-
right: var(--sc-kit--space--2);
|
|
323
422
|
display: flex;
|
|
423
|
+
flex-shrink: 0;
|
|
324
424
|
align-items: center;
|
|
425
|
+
margin-left: var(--sc-kit--space--1);
|
|
325
426
|
opacity: var(--_tree-node--action-opacity);
|
|
326
427
|
transition: opacity var(--sc-kit--duration--base) var(--sc-kit--ease--default);
|
|
327
428
|
}
|
|
@@ -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>;
|