@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.
@@ -1,5 +1,21 @@
1
+ import type { DragEventModel } from '../drag-and-drop';
1
2
  import type { TreeNodeAction } from './tree-node-action';
2
- import type { TreeNodeDragConfig } from './tree-node-drag-config.svelte';
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?: string;
14
- imgIcon?: string;
29
+ /** `null` renders no icon. @default folder icon */
30
+ icon?: TreeNodeIcon;
15
31
  count?: number;
16
- hideIcon?: boolean;
17
- onNodeSelected?: (node: TreeNode, opt: {
18
- selectedExplicitly: boolean;
19
- }) => void;
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: string;
44
+ icon: TreeNodeIcon;
32
45
  count?: number;
33
46
  expandOnDoubleClick: boolean;
34
47
  meta?: string | null;
35
48
  href?: string | null;
36
- private _dragConfig?;
37
- private _onNodeSelected?;
38
- private _onNodeHover?;
39
- private _imgIcon;
40
- private _hideIcon;
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
- /** Whether this node has unsaved changes; set via `setDirty` and propagated up to ancestors. */
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 involvedIntoActivePath(): boolean;
60
- get imgIcon(): string;
61
- get hideIcon(): boolean;
62
- get dragConfig(): TreeNodeDragConfig | undefined;
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(mouseOver: boolean): void;
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 _unselectNodeAndChildren;
84
- private _buildChainToCurrentNode;
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
- _dragConfig;
12
- _onNodeSelected;
13
- _onNodeHover;
14
- _imgIcon;
15
- _hideIcon;
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 || (init.imgIcon ? '' : IconDocumentFolder);
32
- this._imgIcon = init.imgIcon || '';
31
+ this.icon = init.icon === undefined ? IconDocumentFolder : init.icon;
33
32
  this.count = init.count;
34
- this._hideIcon = init.hideIcon || false;
35
- this._onNodeSelected = init.onNodeSelected;
36
- this._onNodeHover = init.onNodeHover;
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
- /** Whether this node has unsaved changes; set via `setDirty` and propagated up to ancestors. */
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 involvedIntoActivePath() {
66
- return this._involvedIntoActivePath;
63
+ get drag() {
64
+ return this._drag;
67
65
  }
68
- get imgIcon() {
69
- return this._imgIcon;
66
+ get renamable() {
67
+ return !!this._on.rename;
70
68
  }
71
- get hideIcon() {
72
- return this._hideIcon;
69
+ get renaming() {
70
+ return this._renaming;
73
71
  }
74
- get dragConfig() {
75
- return this._dragConfig;
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
- this._refreshParentRefs(value);
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 = undefined;
149
+ if (child._parent === this) {
150
+ child._parent = undefined;
151
+ }
103
152
  }
104
153
  setDirty(value) {
105
154
  this._dirty = value;
106
- if (this._parent) {
107
- const parent = this._parent;
108
- const children = parent.flatten().filter((f) => f.id !== parent.id);
109
- parent.setDirty(children.some((f) => f._dirty));
110
- }
111
- }
112
- notifyHover(mouseOver) {
113
- const selectionPath = this._buildChainToCurrentNode(this);
114
- for (const item of selectionPath) {
115
- if (item._onNodeHover) {
116
- if (mouseOver) {
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._unselectNodeAndChildren(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._buildChainToCurrentNode(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
- this._unselectNodeAndChildren(r);
198
+ r._clearSelection();
194
199
  }
195
- // process selection path
196
200
  this._selected = true;
197
- if (selectionPath.length > 1) {
198
- for (let i = 1, length = selectionPath.length; i < length; i++) {
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
- _unselectNodeAndChildren(node) {
229
- node._involvedIntoActivePath = false;
230
- node._selected = false;
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
- _buildChainToCurrentNode(node) {
240
- const chain = [];
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
  }
@@ -263,7 +263,7 @@ const handleSeek = (percent) => {
263
263
  <track src="" kind="captions" />
264
264
  </video>
265
265
  {#if !everActivated && poster}
266
- <img class="video__poster" src={poster} alt="" />
266
+ <img class="video__poster" src={poster} alt="" draggable="false" />
267
267
  {/if}
268
268
  <div
269
269
  class="video__overlay"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.65.1",
3
+ "version": "0.67.0",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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
- };
@@ -1,4 +0,0 @@
1
- export const DraggingNodeState = $state({
2
- draggingNode: null,
3
- dragOverNode: null
4
- });