@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,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
|
};
|
|
@@ -1,5 +1,21 @@
|
|
|
1
|
+
import type { DragEventModel } from '../drag-and-drop';
|
|
1
2
|
import type { TreeNodeAction } from './tree-node-action';
|
|
2
|
-
|
|
3
|
+
/** SVG source, a round image, or `null` for no icon. */
|
|
4
|
+
export type TreeNodeIcon = string | {
|
|
5
|
+
image: string;
|
|
6
|
+
} | null;
|
|
7
|
+
type TreeNodeHandlers = {
|
|
8
|
+
select?: (node: TreeNode, opt: {
|
|
9
|
+
selectedExplicitly: boolean;
|
|
10
|
+
}) => void;
|
|
11
|
+
hover?: {
|
|
12
|
+
enter: (node: TreeNode) => void;
|
|
13
|
+
leave: (node: TreeNode) => void;
|
|
14
|
+
};
|
|
15
|
+
drop?: (e: DragEventModel<unknown>) => void;
|
|
16
|
+
/** Enables inline rename (`startRename()`, F2). Resolve `false` to reject the name and keep editing; on `true` the node takes the new name. A throw counts as `false`. */
|
|
17
|
+
rename?: (name: string) => boolean | Promise<boolean>;
|
|
18
|
+
};
|
|
3
19
|
export type TreeNodeInitializer = {
|
|
4
20
|
name?: string;
|
|
5
21
|
id?: string;
|
|
@@ -10,45 +26,42 @@ export type TreeNodeInitializer = {
|
|
|
10
26
|
meta?: string | null;
|
|
11
27
|
/** Destination route for this node. The kit does not navigate; read it in your selection handler (e.g. `goto(node.href)`). */
|
|
12
28
|
href?: string | null;
|
|
13
|
-
icon
|
|
14
|
-
|
|
29
|
+
/** `null` renders no icon. @default folder icon */
|
|
30
|
+
icon?: TreeNodeIcon;
|
|
15
31
|
count?: number;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
onNodeHover?: {
|
|
21
|
-
mouseover: (node: TreeNode) => void;
|
|
22
|
-
mouseleave: (node: TreeNode) => void;
|
|
23
|
-
};
|
|
24
|
-
dragConfig?: TreeNodeDragConfig;
|
|
32
|
+
/** Payload the row puts into the `DragAndDrop` store when dragged. */
|
|
33
|
+
drag?: DragEventModel<unknown>;
|
|
34
|
+
/** Decides per `DragAndDrop` payload whether this node takes the drop (`on.drop`). */
|
|
35
|
+
accepts?: (e: DragEventModel<unknown>) => boolean;
|
|
25
36
|
children?: TreeNode[];
|
|
26
37
|
actions?: TreeNodeAction[];
|
|
38
|
+
/** `select` / `hover` go to the nearest handler up the chain from the target node (self included) — only that one fires. */
|
|
39
|
+
on?: TreeNodeHandlers;
|
|
27
40
|
};
|
|
28
41
|
export declare class TreeNode {
|
|
29
42
|
id: string;
|
|
30
43
|
name: string;
|
|
31
|
-
icon:
|
|
44
|
+
icon: TreeNodeIcon;
|
|
32
45
|
count?: number;
|
|
33
46
|
expandOnDoubleClick: boolean;
|
|
34
47
|
meta?: string | null;
|
|
35
48
|
href?: string | null;
|
|
36
|
-
private
|
|
37
|
-
private
|
|
38
|
-
private
|
|
39
|
-
private
|
|
40
|
-
private
|
|
49
|
+
private _drag?;
|
|
50
|
+
private _accepts?;
|
|
51
|
+
private _on;
|
|
52
|
+
private _renaming;
|
|
53
|
+
private _renamePending;
|
|
41
54
|
private _expanded;
|
|
42
55
|
private _selected;
|
|
43
|
-
private _involvedIntoActivePath;
|
|
44
56
|
private _actions;
|
|
45
57
|
private _nonSelectable;
|
|
46
58
|
private _children;
|
|
47
59
|
private _parent?;
|
|
48
60
|
private _forestRoots?;
|
|
49
61
|
private _dirty;
|
|
62
|
+
private _hoverLeave?;
|
|
50
63
|
constructor(init: TreeNodeInitializer);
|
|
51
|
-
/**
|
|
64
|
+
/** Marked itself via `setDirty`, or has a dirty descendant. */
|
|
52
65
|
get dirty(): boolean;
|
|
53
66
|
get expanded(): boolean;
|
|
54
67
|
get selected(): boolean;
|
|
@@ -56,30 +69,37 @@ export declare class TreeNode {
|
|
|
56
69
|
get actions(): TreeNodeAction[];
|
|
57
70
|
get parent(): TreeNode | undefined;
|
|
58
71
|
get nonSelectable(): boolean;
|
|
59
|
-
get
|
|
60
|
-
get
|
|
61
|
-
get
|
|
62
|
-
|
|
72
|
+
get drag(): DragEventModel<unknown> | undefined;
|
|
73
|
+
get renamable(): boolean;
|
|
74
|
+
get renaming(): boolean;
|
|
75
|
+
/** A committed name is waiting for `on.rename` to answer. */
|
|
76
|
+
get renamePending(): boolean;
|
|
77
|
+
accepts(e: DragEventModel<unknown>): boolean;
|
|
78
|
+
/** Hands the payload to `on.drop`; the caller has already checked `accepts`. */
|
|
79
|
+
receiveDrop(e: DragEventModel<unknown>): void;
|
|
80
|
+
startRename(): void;
|
|
81
|
+
cancelRename(): void;
|
|
82
|
+
/** Resolves `false` when the name was rejected or a commit is already pending — the node stays in rename mode. Empty or unchanged input just leaves rename mode. */
|
|
83
|
+
commitRename(value: string): Promise<boolean>;
|
|
63
84
|
select(options?: {
|
|
64
85
|
explicitly?: boolean;
|
|
65
86
|
}): void;
|
|
66
87
|
/** Register the full set of forest roots so single-select clears across sibling roots, not just this node's own root. Set by the `Tree` component. */
|
|
67
88
|
setForest(getRoots: () => readonly TreeNode[]): void;
|
|
68
89
|
setChildren(value: TreeNode[]): void;
|
|
90
|
+
/** Detaches the child from its current parent first, so a move is a single call. */
|
|
69
91
|
addChild(child: TreeNode): void;
|
|
70
|
-
insertChildAt(index: number, child: TreeNode): void;
|
|
71
92
|
removeChild(child: TreeNode): void;
|
|
72
93
|
setDirty(value: boolean): void;
|
|
73
|
-
notifyHover(
|
|
94
|
+
notifyHover(entered: boolean): void;
|
|
74
95
|
collapse(): void;
|
|
75
96
|
expand(): void;
|
|
76
97
|
unselect(): void;
|
|
77
98
|
flatten(): TreeNode[];
|
|
78
|
-
canMoveToParent(targetParent: TreeNode): boolean;
|
|
79
|
-
moveToFolder(targetFolder: TreeNode): boolean;
|
|
80
|
-
private _refreshParentRefs;
|
|
81
99
|
private _select;
|
|
100
|
+
private _nearestHandler;
|
|
82
101
|
private _firstSelectable;
|
|
83
|
-
private
|
|
84
|
-
private
|
|
102
|
+
private _clearSelection;
|
|
103
|
+
private _chain;
|
|
85
104
|
}
|
|
105
|
+
export {};
|
|
@@ -3,46 +3,44 @@ import IconDocumentFolder from '@fluentui/svg-icons/icons/document_folder_20_reg
|
|
|
3
3
|
export class TreeNode {
|
|
4
4
|
id = randomNanoid(9);
|
|
5
5
|
name = $state('');
|
|
6
|
-
icon = $state(
|
|
6
|
+
icon = $state(null);
|
|
7
7
|
count = $state(undefined);
|
|
8
8
|
expandOnDoubleClick = $state(false);
|
|
9
9
|
meta = $state(undefined);
|
|
10
10
|
href = $state(undefined);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
_drag;
|
|
12
|
+
_accepts;
|
|
13
|
+
_on;
|
|
14
|
+
_renaming = $state(false);
|
|
15
|
+
_renamePending = $state(false);
|
|
16
16
|
_expanded = $state(false);
|
|
17
17
|
_selected = $state(false);
|
|
18
|
-
_involvedIntoActivePath = $state(false);
|
|
19
18
|
_actions = [];
|
|
20
19
|
_nonSelectable;
|
|
21
20
|
_children = $state([]);
|
|
22
21
|
_parent;
|
|
23
22
|
_forestRoots;
|
|
24
23
|
_dirty = $state(false);
|
|
24
|
+
_hoverLeave;
|
|
25
25
|
constructor(init) {
|
|
26
26
|
this.id = init.id || this.id;
|
|
27
27
|
this.name = init.name || '';
|
|
28
28
|
this.href = init.href;
|
|
29
29
|
this.meta = init.meta;
|
|
30
30
|
this._nonSelectable = init.nonSelectable || false;
|
|
31
|
-
this.icon = init.icon
|
|
32
|
-
this._imgIcon = init.imgIcon || '';
|
|
31
|
+
this.icon = init.icon === undefined ? IconDocumentFolder : init.icon;
|
|
33
32
|
this.count = init.count;
|
|
34
|
-
this.
|
|
35
|
-
this.
|
|
36
|
-
this.
|
|
37
|
-
this._dragConfig = init.dragConfig;
|
|
33
|
+
this._drag = init.drag;
|
|
34
|
+
this._accepts = init.accepts;
|
|
35
|
+
this._on = init.on ?? {};
|
|
38
36
|
this.setChildren(init.children || []);
|
|
39
37
|
this._expanded = init.initiallyExpanded || false;
|
|
40
38
|
this.expandOnDoubleClick = init.expandOnDoubleClick || false;
|
|
41
39
|
this._actions = init.actions || [];
|
|
42
40
|
}
|
|
43
|
-
/**
|
|
41
|
+
/** Marked itself via `setDirty`, or has a dirty descendant. */
|
|
44
42
|
get dirty() {
|
|
45
|
-
return this._dirty;
|
|
43
|
+
return this._dirty || this._children.some((c) => c.dirty);
|
|
46
44
|
}
|
|
47
45
|
get expanded() {
|
|
48
46
|
return this._expanded;
|
|
@@ -62,17 +60,66 @@ export class TreeNode {
|
|
|
62
60
|
get nonSelectable() {
|
|
63
61
|
return this._nonSelectable;
|
|
64
62
|
}
|
|
65
|
-
get
|
|
66
|
-
return this.
|
|
63
|
+
get drag() {
|
|
64
|
+
return this._drag;
|
|
67
65
|
}
|
|
68
|
-
get
|
|
69
|
-
return this.
|
|
66
|
+
get renamable() {
|
|
67
|
+
return !!this._on.rename;
|
|
70
68
|
}
|
|
71
|
-
get
|
|
72
|
-
return this.
|
|
69
|
+
get renaming() {
|
|
70
|
+
return this._renaming;
|
|
73
71
|
}
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
/** A committed name is waiting for `on.rename` to answer. */
|
|
73
|
+
get renamePending() {
|
|
74
|
+
return this._renamePending;
|
|
75
|
+
}
|
|
76
|
+
accepts(e) {
|
|
77
|
+
return !!this._on.drop && !!this._accepts?.(e);
|
|
78
|
+
}
|
|
79
|
+
/** Hands the payload to `on.drop`; the caller has already checked `accepts`. */
|
|
80
|
+
receiveDrop(e) {
|
|
81
|
+
this._on.drop?.(e);
|
|
82
|
+
}
|
|
83
|
+
startRename() {
|
|
84
|
+
if (this._on.rename) {
|
|
85
|
+
this._renaming = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
cancelRename() {
|
|
89
|
+
if (!this._renamePending) {
|
|
90
|
+
this._renaming = false;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/** Resolves `false` when the name was rejected or a commit is already pending — the node stays in rename mode. Empty or unchanged input just leaves rename mode. */
|
|
94
|
+
async commitRename(value) {
|
|
95
|
+
if (!this._renaming) {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
if (this._renamePending) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
const name = value.trim();
|
|
102
|
+
const rename = this._on.rename;
|
|
103
|
+
if (!rename || !name || name === this.name) {
|
|
104
|
+
this._renaming = false;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
this._renamePending = true;
|
|
108
|
+
let accepted;
|
|
109
|
+
try {
|
|
110
|
+
accepted = await rename(name);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
accepted = false;
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
this._renamePending = false;
|
|
117
|
+
}
|
|
118
|
+
if (accepted) {
|
|
119
|
+
this.name = name;
|
|
120
|
+
this._renaming = false;
|
|
121
|
+
}
|
|
122
|
+
return accepted;
|
|
76
123
|
}
|
|
77
124
|
select(options) {
|
|
78
125
|
this._select(options?.explicitly ?? false);
|
|
@@ -83,44 +130,39 @@ export class TreeNode {
|
|
|
83
130
|
}
|
|
84
131
|
setChildren(value) {
|
|
85
132
|
this._children = value;
|
|
86
|
-
|
|
133
|
+
for (const child of value) {
|
|
134
|
+
child._parent = this;
|
|
135
|
+
}
|
|
87
136
|
}
|
|
137
|
+
/** Detaches the child from its current parent first, so a move is a single call. */
|
|
88
138
|
addChild(child) {
|
|
139
|
+
child._parent?.removeChild(child);
|
|
89
140
|
child._parent = this;
|
|
90
141
|
this._children.push(child);
|
|
91
142
|
}
|
|
92
|
-
insertChildAt(index, child) {
|
|
93
|
-
child._parent = this;
|
|
94
|
-
this._children.splice(index, 0, child);
|
|
95
|
-
}
|
|
96
143
|
removeChild(child) {
|
|
97
144
|
const i = this._children.indexOf(child);
|
|
98
145
|
if (i < 0) {
|
|
99
146
|
return;
|
|
100
147
|
}
|
|
101
148
|
this._children.splice(i, 1);
|
|
102
|
-
child._parent
|
|
149
|
+
if (child._parent === this) {
|
|
150
|
+
child._parent = undefined;
|
|
151
|
+
}
|
|
103
152
|
}
|
|
104
153
|
setDirty(value) {
|
|
105
154
|
this._dirty = value;
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
item._onNodeHover.mouseover(this);
|
|
118
|
-
}
|
|
119
|
-
else {
|
|
120
|
-
item._onNodeHover.mouseleave(this);
|
|
121
|
-
}
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
155
|
+
}
|
|
156
|
+
notifyHover(entered) {
|
|
157
|
+
if (entered) {
|
|
158
|
+
const hover = this._nearestHandler('hover');
|
|
159
|
+
this._hoverLeave = hover?.leave;
|
|
160
|
+
hover?.enter(this);
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
const leave = this._hoverLeave;
|
|
164
|
+
this._hoverLeave = undefined;
|
|
165
|
+
leave?.(this);
|
|
124
166
|
}
|
|
125
167
|
}
|
|
126
168
|
collapse() {
|
|
@@ -133,7 +175,7 @@ export class TreeNode {
|
|
|
133
175
|
this._expanded = true;
|
|
134
176
|
}
|
|
135
177
|
unselect() {
|
|
136
|
-
this.
|
|
178
|
+
this._clearSelection();
|
|
137
179
|
}
|
|
138
180
|
flatten() {
|
|
139
181
|
const result = [];
|
|
@@ -144,74 +186,25 @@ export class TreeNode {
|
|
|
144
186
|
processNode(this);
|
|
145
187
|
return result;
|
|
146
188
|
}
|
|
147
|
-
canMoveToParent(targetParent) {
|
|
148
|
-
if (!this._dragConfig) {
|
|
149
|
-
return false;
|
|
150
|
-
}
|
|
151
|
-
if (!targetParent._dragConfig) {
|
|
152
|
-
return false;
|
|
153
|
-
}
|
|
154
|
-
const canMoveTo = (targetParent) => {
|
|
155
|
-
let current = targetParent;
|
|
156
|
-
while (current) {
|
|
157
|
-
if (current.id === this.id) {
|
|
158
|
-
return false;
|
|
159
|
-
}
|
|
160
|
-
current = current._parent;
|
|
161
|
-
}
|
|
162
|
-
return true;
|
|
163
|
-
};
|
|
164
|
-
if (!canMoveTo(targetParent)) {
|
|
165
|
-
return false;
|
|
166
|
-
}
|
|
167
|
-
return targetParent._dragConfig.acceptZones.includes(this._dragConfig.dragZone);
|
|
168
|
-
}
|
|
169
|
-
moveToFolder(targetFolder) {
|
|
170
|
-
if (!this._parent || !this._dragConfig || !this.canMoveToParent(targetFolder)) {
|
|
171
|
-
return false;
|
|
172
|
-
}
|
|
173
|
-
this._dragConfig.onNodeMovedToFolder({ movedNode: this, targetFolder });
|
|
174
|
-
return true;
|
|
175
|
-
}
|
|
176
|
-
_refreshParentRefs(children) {
|
|
177
|
-
for (const child of children) {
|
|
178
|
-
child._parent = this;
|
|
179
|
-
if (child._children.length) {
|
|
180
|
-
child._refreshParentRefs(child._children);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
189
|
_select(selectedExplicitly) {
|
|
185
190
|
if (this._nonSelectable) {
|
|
186
191
|
this._firstSelectable()?._select(selectedExplicitly);
|
|
187
192
|
return;
|
|
188
193
|
}
|
|
189
|
-
const selectionPath = this.
|
|
194
|
+
const selectionPath = this._chain();
|
|
190
195
|
const root = selectionPath[selectionPath.length - 1];
|
|
191
196
|
const roots = root._forestRoots?.() ?? [root];
|
|
192
197
|
for (const r of roots) {
|
|
193
|
-
|
|
198
|
+
r._clearSelection();
|
|
194
199
|
}
|
|
195
|
-
// process selection path
|
|
196
200
|
this._selected = true;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const item = selectionPath[i];
|
|
200
|
-
item._expanded = true;
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
let callbackInvoked = false;
|
|
204
|
-
for (let i = 0, length = selectionPath.length; i < length; i++) {
|
|
205
|
-
const item = selectionPath[i];
|
|
206
|
-
item._involvedIntoActivePath = true;
|
|
207
|
-
if (callbackInvoked) {
|
|
208
|
-
continue;
|
|
209
|
-
}
|
|
210
|
-
if (item._onNodeSelected) {
|
|
211
|
-
item._onNodeSelected(this, { selectedExplicitly });
|
|
212
|
-
callbackInvoked = true;
|
|
213
|
-
}
|
|
201
|
+
for (const ancestor of selectionPath.slice(1)) {
|
|
202
|
+
ancestor._expanded = true;
|
|
214
203
|
}
|
|
204
|
+
this._nearestHandler('select')?.(this, { selectedExplicitly });
|
|
205
|
+
}
|
|
206
|
+
_nearestHandler(key) {
|
|
207
|
+
return this._chain().find((n) => n._on[key])?._on[key];
|
|
215
208
|
}
|
|
216
209
|
_firstSelectable() {
|
|
217
210
|
if (!this._nonSelectable) {
|
|
@@ -225,24 +218,11 @@ export class TreeNode {
|
|
|
225
218
|
}
|
|
226
219
|
return null;
|
|
227
220
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
const children = node._children;
|
|
232
|
-
if (!children.length) {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
children.forEach((f) => {
|
|
236
|
-
this._unselectNodeAndChildren(f);
|
|
237
|
-
});
|
|
221
|
+
_clearSelection() {
|
|
222
|
+
this._selected = false;
|
|
223
|
+
this._children.forEach((c) => c._clearSelection());
|
|
238
224
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
let current = node;
|
|
242
|
-
while (current) {
|
|
243
|
-
chain.push(current);
|
|
244
|
-
current = current._parent;
|
|
245
|
-
}
|
|
246
|
-
return chain;
|
|
225
|
+
_chain() {
|
|
226
|
+
return this._parent ? [this, ...this._parent._chain()] : [this];
|
|
247
227
|
}
|
|
248
228
|
}
|
package/package.json
CHANGED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { TreeNode } from './tree-node.svelte';
|
|
2
|
-
export type TreeNodeDragConfig = {
|
|
3
|
-
/** This node's own drag type (e.g. `'folder'`, `'jpeg'`). */
|
|
4
|
-
dragZone: string;
|
|
5
|
-
/** Drag types this node accepts dropped into it (e.g. a folder accepts `['folder', 'jpeg', 'mp3']`; a file accepts `[]`). */
|
|
6
|
-
acceptZones: string[];
|
|
7
|
-
onNodeMovedToFolder: (params: TreeNodeMoveToFolderCallbackParams) => void;
|
|
8
|
-
};
|
|
9
|
-
export type TreeNodeMoveToFolderCallbackParams = {
|
|
10
|
-
movedNode: TreeNode;
|
|
11
|
-
targetFolder: TreeNode;
|
|
12
|
-
};
|
|
13
|
-
export declare const DraggingNodeState: {
|
|
14
|
-
draggingNode: TreeNode | null;
|
|
15
|
-
dragOverNode: TreeNode | null;
|
|
16
|
-
};
|