@djangocfg/ui-tools 2.1.413 → 2.1.416
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/file-icon/index.d.cts +1 -1
- package/dist/file-icon/index.d.ts +1 -1
- package/dist/slots-ClRpIzoh.d.cts +88 -0
- package/dist/slots-ClRpIzoh.d.ts +88 -0
- package/dist/tree/index.cjs +1994 -276
- package/dist/tree/index.cjs.map +1 -1
- package/dist/tree/index.d.cts +717 -72
- package/dist/tree/index.d.ts +717 -72
- package/dist/tree/index.mjs +1984 -279
- package/dist/tree/index.mjs.map +1 -1
- package/package.json +10 -6
- package/src/tools/chat/README.md +111 -1
- package/src/tools/chat/composer/Composer.tsx +138 -17
- package/src/tools/chat/composer/ComposerRichTextarea.tsx +25 -0
- package/src/tools/chat/composer/index.ts +22 -0
- package/src/tools/chat/composer/slash/README.md +187 -0
- package/src/tools/chat/composer/slash/SlashHighlightTextarea.tsx +144 -0
- package/src/tools/chat/composer/slash/SlashMenu.tsx +142 -0
- package/src/tools/chat/composer/slash/SlashToken.tsx +57 -0
- package/src/tools/chat/composer/slash/index.ts +44 -0
- package/src/tools/chat/composer/slash/labels.ts +19 -0
- package/src/tools/chat/composer/slash/state.ts +168 -0
- package/src/tools/chat/composer/slash/types.ts +64 -0
- package/src/tools/chat/composer/slash/useSlashCommands.ts +204 -0
- package/src/tools/chat/composer/types.ts +8 -0
- package/src/tools/chat/shell/SuggestedPrompts.tsx +194 -0
- package/src/tools/chat/shell/index.ts +6 -0
- package/src/tools/data/Listbox/lazy.tsx +1 -1
- package/src/tools/data/Masonry/lazy.tsx +1 -1
- package/src/tools/data/Timeline/lazy.tsx +1 -1
- package/src/tools/data/Tree/FinderTree.tsx +42 -0
- package/src/tools/data/Tree/README.md +337 -208
- package/src/tools/data/Tree/TreeDndProvider.tsx +137 -0
- package/src/tools/data/Tree/TreeRoot.tsx +170 -55
- package/src/tools/data/Tree/__tests__/dnd.test.ts +160 -0
- package/src/tools/data/Tree/__tests__/keyboard.test.ts +137 -0
- package/src/tools/data/Tree/__tests__/renameUtils.test.ts +52 -0
- package/src/tools/data/Tree/__tests__/selection.test.ts +227 -0
- package/src/tools/data/Tree/components/TreeDropIndicator.tsx +65 -0
- package/src/tools/data/Tree/components/TreeEmptyArea.tsx +160 -0
- package/src/tools/data/Tree/components/TreeRenameInput.tsx +114 -0
- package/src/tools/data/Tree/components/TreeRow.tsx +92 -8
- package/src/tools/data/Tree/components/index.ts +6 -0
- package/src/tools/data/Tree/context/TreeContext.tsx +204 -363
- package/src/tools/data/Tree/context/TreeContextValue.ts +139 -0
- package/src/tools/data/Tree/context/async-children/collect-ids.ts +27 -0
- package/src/tools/data/Tree/context/async-children/index.ts +8 -0
- package/src/tools/data/Tree/context/async-children/use-async-children.ts +157 -0
- package/src/tools/data/Tree/context/clipboard/index.ts +4 -0
- package/src/tools/data/Tree/context/clipboard/use-clipboard.ts +115 -0
- package/src/tools/data/Tree/context/dnd/index.ts +8 -0
- package/src/tools/data/Tree/context/dnd/use-dnd.ts +194 -0
- package/src/tools/data/Tree/context/expansion/index.ts +4 -0
- package/src/tools/data/Tree/context/expansion/use-expansion.ts +55 -0
- package/src/tools/data/Tree/context/hooks.ts +68 -1
- package/src/tools/data/Tree/context/index.ts +3 -0
- package/src/tools/data/Tree/context/menu/builtin-actions.ts +357 -0
- package/src/tools/data/Tree/context/menu/index.ts +10 -0
- package/src/tools/data/Tree/context/menu/use-resolved-menu.ts +127 -0
- package/src/tools/data/Tree/context/persist/index.ts +4 -0
- package/src/tools/data/Tree/context/persist/use-persist-sync.ts +74 -0
- package/src/tools/data/Tree/context/rename/index.ts +4 -0
- package/src/tools/data/Tree/context/rename/use-rename.ts +113 -0
- package/src/tools/data/Tree/context/selection/index.ts +4 -0
- package/src/tools/data/Tree/context/selection/use-selection.ts +146 -0
- package/src/tools/data/Tree/context/state/index.ts +6 -0
- package/src/tools/data/Tree/context/state/initial.ts +41 -0
- package/src/tools/data/Tree/context/state/reducer.ts +76 -0
- package/src/tools/data/Tree/context/state/types.ts +46 -0
- package/src/tools/data/Tree/data/clipboard.ts +33 -0
- package/src/tools/data/Tree/data/dnd.ts +123 -0
- package/src/tools/data/Tree/data/finderShortcuts.ts +67 -0
- package/src/tools/data/Tree/data/index.ts +19 -0
- package/src/tools/data/Tree/data/renameUtils.ts +51 -0
- package/src/tools/data/Tree/data/selection.ts +157 -0
- package/src/tools/data/Tree/hooks/finder-hotkeys/build-ctx.ts +48 -0
- package/src/tools/data/Tree/hooks/finder-hotkeys/index.ts +8 -0
- package/src/tools/data/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts +166 -0
- package/src/tools/data/Tree/hooks/index.ts +23 -4
- package/src/tools/data/Tree/hooks/keyboard/activation.ts +27 -0
- package/src/tools/data/Tree/hooks/keyboard/arrow-nav.ts +26 -0
- package/src/tools/data/Tree/hooks/keyboard/expand-collapse.ts +54 -0
- package/src/tools/data/Tree/hooks/keyboard/index.ts +10 -0
- package/src/tools/data/Tree/hooks/keyboard/types.ts +39 -0
- package/src/tools/data/Tree/hooks/keyboard/use-tree-keyboard.ts +196 -0
- package/src/tools/data/Tree/hooks/type-ahead/index.ts +5 -0
- package/src/tools/data/Tree/hooks/type-ahead/match-prefix.ts +42 -0
- package/src/tools/data/Tree/hooks/{useTreeTypeAhead.ts → type-ahead/use-tree-type-ahead.ts} +8 -19
- package/src/tools/data/Tree/index.tsx +25 -2
- package/src/tools/data/Tree/types/activation.ts +30 -0
- package/src/tools/data/Tree/types/adapter.ts +70 -0
- package/src/tools/data/Tree/types/index.ts +27 -0
- package/src/tools/data/Tree/types/labels.ts +97 -0
- package/src/tools/data/Tree/types/loader.ts +9 -0
- package/src/tools/data/Tree/types/node.ts +38 -0
- package/src/tools/data/Tree/types/root-props.ts +142 -0
- package/src/tools/data/Tree/types/selection.ts +3 -0
- package/src/tools/data/Tree/types/slots.ts +64 -0
- package/src/tools/dev/OpenapiViewer/components/DocsLayout/EndpointDoc/Responses/ResponseBody.tsx +1 -1
- package/src/tools/dev/OpenapiViewer/components/shared/ResponsePanel/PrettyView.tsx +1 -1
- package/src/tools/forms/MarkdownEditor/MarkdownEditor.tsx +85 -0
- package/src/tools/forms/MarkdownEditor/index.ts +1 -0
- package/src/tools/forms/MarkdownEditor/lazy.tsx +6 -0
- package/src/tools/forms/MarkdownEditor/slash/SlashCommandNode.ts +162 -0
- package/src/tools/forms/MarkdownEditor/slash/index.ts +4 -0
- package/src/tools/forms/MarkdownEditor/slash/syncSlashNode.ts +97 -0
- package/src/tools/forms/MarkdownEditor/slash/types.ts +13 -0
- package/src/tools/forms/MarkdownEditor/styles.css +18 -0
- package/src/tools/index.ts +2 -2
- package/dist/types-j2vhn4Kv.d.cts +0 -241
- package/dist/types-j2vhn4Kv.d.ts +0 -241
- package/src/tools/data/Tree/hooks/useTreeKeyboard.ts +0 -171
- package/src/tools/data/Tree/types.ts +0 -217
package/dist/tree/index.mjs
CHANGED
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
import { __name } from '../chunk-PAWJFY3S.mjs';
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import { createContext, memo,
|
|
3
|
+
import { createContext, memo, useRef, useCallback, useMemo, useEffect, useState, useReducer, Fragment as Fragment$1 } from 'react';
|
|
4
4
|
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
5
|
import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuSeparator, ContextMenuItem, ContextMenuShortcut } from '@djangocfg/ui-core/components';
|
|
6
|
-
import {
|
|
7
|
-
import { ChevronDown, ChevronRight, FolderOpen, Folder, File, Loader2, Search, X, AlertCircle } from 'lucide-react';
|
|
6
|
+
import { getDialog } from '@djangocfg/ui-core/lib/dialog-service';
|
|
7
|
+
import { CornerUpLeft, Pencil, Copy, Scissors, Trash2, FilePlus, FolderPlus, ChevronDown, ChevronRight, FolderOpen, Folder, File, Loader2, Search, X, AlertCircle } from 'lucide-react';
|
|
8
|
+
import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
|
|
9
|
+
import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, useDraggable, useDroppable } from '@dnd-kit/core';
|
|
8
10
|
import { useHotkey } from '@djangocfg/ui-core/hooks';
|
|
9
11
|
|
|
10
|
-
// src/tools/data/Tree/types.ts
|
|
11
|
-
var DEFAULT_TREE_LABELS = {
|
|
12
|
-
loading: "Loading\u2026",
|
|
13
|
-
empty: "Nothing to show",
|
|
14
|
-
error: "Failed to load",
|
|
15
|
-
searchPlaceholder: "Search\u2026",
|
|
16
|
-
searchMatches: /* @__PURE__ */ __name((n) => `${n} match${n === 1 ? "" : "es"}`, "searchMatches"),
|
|
17
|
-
ariaLabel: "Tree"
|
|
18
|
-
};
|
|
19
|
-
|
|
20
12
|
// src/tools/data/Tree/data/childCache.ts
|
|
21
13
|
var createChildCache = /* @__PURE__ */ __name(() => /* @__PURE__ */ new Map(), "createChildCache");
|
|
22
14
|
var resolveChildren = /* @__PURE__ */ __name((cache, node) => {
|
|
@@ -205,7 +197,44 @@ function rowStateClasses(a) {
|
|
|
205
197
|
].join(" ");
|
|
206
198
|
}
|
|
207
199
|
__name(rowStateClasses, "rowStateClasses");
|
|
208
|
-
|
|
200
|
+
|
|
201
|
+
// src/tools/data/Tree/types/labels.ts
|
|
202
|
+
var DEFAULT_TREE_LABELS = {
|
|
203
|
+
loading: "Loading\u2026",
|
|
204
|
+
empty: "Nothing to show",
|
|
205
|
+
error: "Failed to load",
|
|
206
|
+
searchPlaceholder: "Search\u2026",
|
|
207
|
+
searchMatches: /* @__PURE__ */ __name((n) => `${n} match${n === 1 ? "" : "es"}`, "searchMatches"),
|
|
208
|
+
ariaLabel: "Tree",
|
|
209
|
+
actionOpen: "Open",
|
|
210
|
+
actionRename: "Rename",
|
|
211
|
+
actionDuplicate: "Duplicate",
|
|
212
|
+
actionCut: "Cut",
|
|
213
|
+
actionCopy: "Copy",
|
|
214
|
+
actionPaste: "Paste",
|
|
215
|
+
actionDelete: "Delete",
|
|
216
|
+
actionNewFile: "New file",
|
|
217
|
+
actionNewFolder: "New folder",
|
|
218
|
+
confirmDeleteTitle: /* @__PURE__ */ __name((n) => n === 1 ? "Delete item?" : `Delete ${n} items?`, "confirmDeleteTitle"),
|
|
219
|
+
confirmDeleteMessage: /* @__PURE__ */ __name((names) => names.length === 1 ? `"${names[0]}" will be removed. This action cannot be undone.` : `${names.length} items will be removed. This action cannot be undone.`, "confirmDeleteMessage"),
|
|
220
|
+
confirmDeleteOk: "Delete",
|
|
221
|
+
confirmDeleteCancel: "Cancel",
|
|
222
|
+
newFileTitle: "New file",
|
|
223
|
+
newFileMessage: "File name",
|
|
224
|
+
newFilePlaceholder: "untitled.txt",
|
|
225
|
+
newFileDefault: "untitled.txt",
|
|
226
|
+
newFolderTitle: "New folder",
|
|
227
|
+
newFolderMessage: "Folder name",
|
|
228
|
+
newFolderPlaceholder: "untitled folder",
|
|
229
|
+
newFolderDefault: "untitled folder",
|
|
230
|
+
renameTitle: "Rename",
|
|
231
|
+
renameMessage: "New name",
|
|
232
|
+
invalidNameEmpty: "Name cannot be empty",
|
|
233
|
+
duplicateSuffix: /* @__PURE__ */ __name((name) => `${name} copy`, "duplicateSuffix")
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
// src/tools/data/Tree/context/state/reducer.ts
|
|
237
|
+
function reducer(state, action) {
|
|
209
238
|
switch (action.type) {
|
|
210
239
|
case "expand": {
|
|
211
240
|
if (state.expanded.has(action.id)) return state;
|
|
@@ -230,100 +259,92 @@ var reducer = /* @__PURE__ */ __name((state, action) => {
|
|
|
230
259
|
case "select": {
|
|
231
260
|
if (action.mode === "none") return state;
|
|
232
261
|
if (action.mode === "single") {
|
|
233
|
-
return {
|
|
262
|
+
return {
|
|
263
|
+
...state,
|
|
264
|
+
selected: /* @__PURE__ */ new Set([action.id]),
|
|
265
|
+
anchor: action.id,
|
|
266
|
+
focused: action.id
|
|
267
|
+
};
|
|
234
268
|
}
|
|
235
269
|
const next = new Set(state.selected);
|
|
236
270
|
if (next.has(action.id)) next.delete(action.id);
|
|
237
271
|
else next.add(action.id);
|
|
238
|
-
return { ...state, selected: next, focused: action.id };
|
|
272
|
+
return { ...state, selected: next, anchor: action.id, focused: action.id };
|
|
239
273
|
}
|
|
240
274
|
case "select-many":
|
|
241
275
|
return { ...state, selected: new Set(action.ids) };
|
|
242
276
|
case "clear-selection":
|
|
243
|
-
return { ...state, selected: /* @__PURE__ */ new Set() };
|
|
277
|
+
return { ...state, selected: /* @__PURE__ */ new Set(), anchor: null };
|
|
278
|
+
case "selection-replace":
|
|
279
|
+
return {
|
|
280
|
+
...state,
|
|
281
|
+
selected: new Set(action.selected),
|
|
282
|
+
anchor: action.anchor,
|
|
283
|
+
focused: action.focused
|
|
284
|
+
};
|
|
285
|
+
case "set-anchor":
|
|
286
|
+
return { ...state, anchor: action.id };
|
|
244
287
|
case "focus":
|
|
245
288
|
return { ...state, focused: action.id };
|
|
246
289
|
case "set-query":
|
|
247
290
|
return { ...state, query: action.q };
|
|
291
|
+
case "start-rename":
|
|
292
|
+
return { ...state, renaming: action.id };
|
|
293
|
+
case "stop-rename":
|
|
294
|
+
return state.renaming === null ? state : { ...state, renaming: null };
|
|
295
|
+
case "clipboard-set":
|
|
296
|
+
return { ...state, clipboard: action.payload };
|
|
248
297
|
case "cache-tick":
|
|
249
298
|
return { ...state, cacheTick: state.cacheTick + 1 };
|
|
250
299
|
default:
|
|
251
300
|
return state;
|
|
252
301
|
}
|
|
253
|
-
}, "reducer");
|
|
254
|
-
var TreeContext = createContext(null);
|
|
255
|
-
function useTreeContext() {
|
|
256
|
-
const ctx = React.useContext(TreeContext);
|
|
257
|
-
if (!ctx) {
|
|
258
|
-
throw new Error("useTreeContext must be used inside <TreeProvider>");
|
|
259
|
-
}
|
|
260
|
-
return ctx;
|
|
261
302
|
}
|
|
262
|
-
__name(
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
303
|
+
__name(reducer, "reducer");
|
|
304
|
+
|
|
305
|
+
// src/tools/data/Tree/context/state/initial.ts
|
|
306
|
+
function createInitialState(input) {
|
|
307
|
+
const { persisted, initialExpandedIds, initialSelectedIds, persistSelection } = input;
|
|
308
|
+
const initialSelected = new Set(
|
|
309
|
+
(persistSelection ? persisted?.selectedItems : void 0) ?? initialSelectedIds ?? []
|
|
310
|
+
);
|
|
311
|
+
const initialAnchor = initialSelected.size > 0 ? initialSelected.values().next().value : null;
|
|
312
|
+
return {
|
|
313
|
+
expanded: new Set(persisted?.expandedItems ?? initialExpandedIds ?? []),
|
|
314
|
+
selected: initialSelected,
|
|
315
|
+
anchor: initialAnchor,
|
|
316
|
+
focused: null,
|
|
317
|
+
query: "",
|
|
318
|
+
renaming: null,
|
|
319
|
+
clipboard: null,
|
|
320
|
+
cacheTick: 0
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
__name(createInitialState, "createInitialState");
|
|
324
|
+
|
|
325
|
+
// src/tools/data/Tree/context/async-children/collect-ids.ts
|
|
326
|
+
function collectAllFolderIds(roots, cache, out) {
|
|
269
327
|
for (const node of roots) {
|
|
270
328
|
if (Array.isArray(node.children)) {
|
|
271
329
|
out.push(node.id);
|
|
272
|
-
|
|
330
|
+
collectAllFolderIds(node.children, cache, out);
|
|
273
331
|
} else if (node.isFolder) {
|
|
274
332
|
out.push(node.id);
|
|
275
333
|
const entry = cache.get(node.id);
|
|
276
|
-
if (entry?.children)
|
|
334
|
+
if (entry?.children) collectAllFolderIds(entry.children, cache, out);
|
|
277
335
|
}
|
|
278
336
|
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
appearance,
|
|
291
|
-
onSelectionChange,
|
|
292
|
-
onExpansionChange,
|
|
293
|
-
onActivate,
|
|
294
|
-
filterNode,
|
|
295
|
-
enableSearch = false,
|
|
296
|
-
showIndentGuides = false,
|
|
297
|
-
renderIcon,
|
|
298
|
-
renderLabel,
|
|
299
|
-
renderActions,
|
|
300
|
-
renderContextMenu,
|
|
301
|
-
labels: labelsOverride,
|
|
302
|
-
persistKey,
|
|
303
|
-
persistSelection = false,
|
|
304
|
-
children
|
|
305
|
-
} = props;
|
|
306
|
-
const labels = useMemo(
|
|
307
|
-
() => ({ ...DEFAULT_TREE_LABELS, ...labelsOverride }),
|
|
308
|
-
[labelsOverride]
|
|
309
|
-
);
|
|
310
|
-
const resolvedAppearance = useMemo(
|
|
311
|
-
() => resolveAppearance(appearance, indent),
|
|
312
|
-
[appearance, indent]
|
|
313
|
-
);
|
|
314
|
-
const persisted = useMemo(
|
|
315
|
-
() => persistKey ? loadTreeState(persistKey) : null,
|
|
316
|
-
[persistKey]
|
|
317
|
-
);
|
|
318
|
-
const [state, dispatch] = useReducer(reducer, void 0, () => ({
|
|
319
|
-
expanded: new Set(persisted?.expandedItems ?? initialExpandedIds ?? []),
|
|
320
|
-
selected: new Set(
|
|
321
|
-
(persistSelection ? persisted?.selectedItems : void 0) ?? initialSelectedIds ?? []
|
|
322
|
-
),
|
|
323
|
-
focused: null,
|
|
324
|
-
query: "",
|
|
325
|
-
cacheTick: 0
|
|
326
|
-
}));
|
|
337
|
+
}
|
|
338
|
+
__name(collectAllFolderIds, "collectAllFolderIds");
|
|
339
|
+
|
|
340
|
+
// src/tools/data/Tree/context/async-children/use-async-children.ts
|
|
341
|
+
function useAsyncChildren({
|
|
342
|
+
data,
|
|
343
|
+
loadChildren,
|
|
344
|
+
expanded,
|
|
345
|
+
cacheTick,
|
|
346
|
+
bumpCacheTick
|
|
347
|
+
}) {
|
|
327
348
|
const cacheRef = useRef(createChildCache());
|
|
328
349
|
const inflightRef = useRef(/* @__PURE__ */ new Map());
|
|
329
350
|
const fetchChildren = useCallback(
|
|
@@ -335,11 +356,11 @@ function TreeProvider(props) {
|
|
|
335
356
|
const inflight = inflightRef.current.get(node.id);
|
|
336
357
|
if (inflight) return inflight;
|
|
337
358
|
cacheRef.current.set(node.id, { status: "loading", children: [] });
|
|
338
|
-
|
|
359
|
+
bumpCacheTick();
|
|
339
360
|
const promise = (async () => {
|
|
340
361
|
try {
|
|
341
|
-
const
|
|
342
|
-
cacheRef.current.set(node.id, { status: "loaded", children
|
|
362
|
+
const children = await loadChildren(node);
|
|
363
|
+
cacheRef.current.set(node.id, { status: "loaded", children });
|
|
343
364
|
} catch (err) {
|
|
344
365
|
cacheRef.current.set(node.id, {
|
|
345
366
|
status: "error",
|
|
@@ -348,13 +369,13 @@ function TreeProvider(props) {
|
|
|
348
369
|
});
|
|
349
370
|
} finally {
|
|
350
371
|
inflightRef.current.delete(node.id);
|
|
351
|
-
|
|
372
|
+
bumpCacheTick();
|
|
352
373
|
}
|
|
353
374
|
})();
|
|
354
375
|
inflightRef.current.set(node.id, promise);
|
|
355
376
|
return promise;
|
|
356
377
|
},
|
|
357
|
-
[loadChildren]
|
|
378
|
+
[loadChildren, bumpCacheTick]
|
|
358
379
|
);
|
|
359
380
|
const nodeById = useMemo(() => {
|
|
360
381
|
const map = /* @__PURE__ */ new Map();
|
|
@@ -370,141 +391,1056 @@ function TreeProvider(props) {
|
|
|
370
391
|
}, "walk");
|
|
371
392
|
walk(data);
|
|
372
393
|
return map;
|
|
373
|
-
}, [data,
|
|
394
|
+
}, [data, cacheTick]);
|
|
374
395
|
useEffect(() => {
|
|
375
396
|
if (!loadChildren) return;
|
|
376
|
-
for (const id of
|
|
397
|
+
for (const id of expanded) {
|
|
377
398
|
const node = nodeById.get(id);
|
|
378
399
|
if (!node) continue;
|
|
379
400
|
void fetchChildren(node);
|
|
380
401
|
}
|
|
381
|
-
}, [loadChildren,
|
|
382
|
-
const
|
|
383
|
-
() =>
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
402
|
+
}, [loadChildren, expanded, cacheTick, nodeById, fetchChildren]);
|
|
403
|
+
const refresh = useCallback(
|
|
404
|
+
async (id) => {
|
|
405
|
+
const node = nodeById.get(id);
|
|
406
|
+
if (!node || !loadChildren) return;
|
|
407
|
+
cacheRef.current.delete(id);
|
|
408
|
+
bumpCacheTick();
|
|
409
|
+
await fetchChildren(node);
|
|
410
|
+
},
|
|
411
|
+
[nodeById, loadChildren, fetchChildren, bumpCacheTick]
|
|
390
412
|
);
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
413
|
+
const refreshAll = useCallback(async () => {
|
|
414
|
+
cacheRef.current.clear();
|
|
415
|
+
bumpCacheTick();
|
|
416
|
+
if (!loadChildren) return;
|
|
417
|
+
await Promise.all(
|
|
418
|
+
[...expanded].map((id) => {
|
|
419
|
+
const node = nodeById.get(id);
|
|
420
|
+
return node ? fetchChildren(node) : void 0;
|
|
421
|
+
})
|
|
422
|
+
);
|
|
423
|
+
}, [loadChildren, expanded, nodeById, fetchChildren, bumpCacheTick]);
|
|
424
|
+
const collectFolderIds = useCallback(() => {
|
|
425
|
+
const ids = [];
|
|
426
|
+
collectAllFolderIds(data, cacheRef.current, ids);
|
|
427
|
+
return ids;
|
|
428
|
+
}, [data]);
|
|
429
|
+
return {
|
|
430
|
+
cache: cacheRef.current,
|
|
431
|
+
nodeById,
|
|
432
|
+
fetchChildren,
|
|
433
|
+
refresh,
|
|
434
|
+
refreshAll,
|
|
435
|
+
collectFolderIds
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
__name(useAsyncChildren, "useAsyncChildren");
|
|
439
|
+
function useExpansion({
|
|
440
|
+
dispatch,
|
|
441
|
+
collectFolderIds
|
|
442
|
+
}) {
|
|
443
|
+
const expand = useCallback(
|
|
444
|
+
(id) => dispatch({ type: "expand", id }),
|
|
445
|
+
[dispatch]
|
|
446
|
+
);
|
|
447
|
+
const collapse = useCallback(
|
|
448
|
+
(id) => dispatch({ type: "collapse", id }),
|
|
449
|
+
[dispatch]
|
|
450
|
+
);
|
|
451
|
+
const toggle = useCallback(
|
|
452
|
+
(id) => dispatch({ type: "toggle", id }),
|
|
453
|
+
[dispatch]
|
|
454
|
+
);
|
|
455
|
+
const expandAll = useCallback(() => {
|
|
456
|
+
dispatch({ type: "set-expanded", ids: collectFolderIds() });
|
|
457
|
+
}, [dispatch, collectFolderIds]);
|
|
458
|
+
const collapseAll = useCallback(
|
|
459
|
+
() => dispatch({ type: "set-expanded", ids: [] }),
|
|
460
|
+
[dispatch]
|
|
461
|
+
);
|
|
462
|
+
return { expand, collapse, toggle, expandAll, collapseAll };
|
|
463
|
+
}
|
|
464
|
+
__name(useExpansion, "useExpansion");
|
|
465
|
+
|
|
466
|
+
// src/tools/data/Tree/data/selection.ts
|
|
467
|
+
function indexOf(rows, id) {
|
|
468
|
+
if (id == null) return -1;
|
|
469
|
+
for (let i = 0; i < rows.length; i++) {
|
|
470
|
+
if (rows[i].node.id === id) return i;
|
|
471
|
+
}
|
|
472
|
+
return -1;
|
|
473
|
+
}
|
|
474
|
+
__name(indexOf, "indexOf");
|
|
475
|
+
function computeRange(rows, fromId, toId) {
|
|
476
|
+
if (fromId == null || toId == null) return [];
|
|
477
|
+
const a = indexOf(rows, fromId);
|
|
478
|
+
const b = indexOf(rows, toId);
|
|
479
|
+
if (a < 0 || b < 0) return [];
|
|
480
|
+
const [lo, hi] = a <= b ? [a, b] : [b, a];
|
|
481
|
+
const out = [];
|
|
482
|
+
for (let i = lo; i <= hi; i++) out.push(rows[i].node.id);
|
|
483
|
+
return out;
|
|
484
|
+
}
|
|
485
|
+
__name(computeRange, "computeRange");
|
|
486
|
+
function selectionFromClick(state, rows, id, mods, multi) {
|
|
487
|
+
if (!multi) {
|
|
488
|
+
return { selected: /* @__PURE__ */ new Set([id]), anchor: id, focused: id };
|
|
489
|
+
}
|
|
490
|
+
if (mods.shift && mods.meta) {
|
|
491
|
+
const anchor = state.anchor ?? state.focused ?? id;
|
|
492
|
+
const range = computeRange(rows, anchor, id);
|
|
493
|
+
const next = new Set(state.selected);
|
|
494
|
+
for (const r of range) next.add(r);
|
|
495
|
+
return { selected: next, anchor: state.anchor ?? id, focused: id };
|
|
496
|
+
}
|
|
497
|
+
if (mods.shift) {
|
|
498
|
+
const anchor = state.anchor ?? state.focused ?? id;
|
|
499
|
+
const range = computeRange(rows, anchor, id);
|
|
500
|
+
return {
|
|
501
|
+
selected: new Set(range.length > 0 ? range : [id]),
|
|
502
|
+
anchor: state.anchor ?? anchor,
|
|
503
|
+
focused: id
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
if (mods.meta) {
|
|
507
|
+
const next = new Set(state.selected);
|
|
508
|
+
if (next.has(id)) next.delete(id);
|
|
509
|
+
else next.add(id);
|
|
510
|
+
return { selected: next, anchor: id, focused: id };
|
|
511
|
+
}
|
|
512
|
+
return { selected: /* @__PURE__ */ new Set([id]), anchor: id, focused: id };
|
|
513
|
+
}
|
|
514
|
+
__name(selectionFromClick, "selectionFromClick");
|
|
515
|
+
function selectionFromMove(state, rows, nextFocusedId, extend, multi) {
|
|
516
|
+
if (!multi || !extend) {
|
|
517
|
+
return { selected: /* @__PURE__ */ new Set([nextFocusedId]), anchor: nextFocusedId, focused: nextFocusedId };
|
|
518
|
+
}
|
|
519
|
+
const anchor = state.anchor ?? state.focused ?? nextFocusedId;
|
|
520
|
+
const range = computeRange(rows, anchor, nextFocusedId);
|
|
521
|
+
return {
|
|
522
|
+
selected: new Set(range.length > 0 ? range : [nextFocusedId]),
|
|
523
|
+
anchor,
|
|
524
|
+
focused: nextFocusedId
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
__name(selectionFromMove, "selectionFromMove");
|
|
528
|
+
function selectionSelectAll(rows, focused) {
|
|
529
|
+
if (rows.length === 0) {
|
|
530
|
+
return { selected: /* @__PURE__ */ new Set(), anchor: null, focused };
|
|
531
|
+
}
|
|
532
|
+
const ids = rows.map((r) => r.node.id);
|
|
533
|
+
return {
|
|
534
|
+
selected: new Set(ids),
|
|
535
|
+
anchor: ids[0],
|
|
536
|
+
focused: focused ?? ids[0]
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
__name(selectionSelectAll, "selectionSelectAll");
|
|
540
|
+
|
|
541
|
+
// src/tools/data/Tree/context/selection/use-selection.ts
|
|
542
|
+
function useSelection({
|
|
543
|
+
dispatch,
|
|
544
|
+
selectionMode,
|
|
545
|
+
flatRows,
|
|
546
|
+
selected,
|
|
547
|
+
anchor,
|
|
548
|
+
focused
|
|
549
|
+
}) {
|
|
550
|
+
const flatRowsRef = useRef(flatRows);
|
|
551
|
+
flatRowsRef.current = flatRows;
|
|
552
|
+
const selectionRef = useRef({ selected, anchor, focused });
|
|
553
|
+
selectionRef.current = { selected, anchor, focused };
|
|
554
|
+
const select = useCallback(
|
|
555
|
+
(id) => dispatch({ type: "select", id, mode: selectionMode }),
|
|
556
|
+
[dispatch, selectionMode]
|
|
557
|
+
);
|
|
558
|
+
const setSelectedIds = useCallback(
|
|
559
|
+
(ids) => dispatch({ type: "select-many", ids }),
|
|
560
|
+
[dispatch]
|
|
561
|
+
);
|
|
562
|
+
const clearSelection = useCallback(
|
|
563
|
+
() => dispatch({ type: "clear-selection" }),
|
|
564
|
+
[dispatch]
|
|
565
|
+
);
|
|
566
|
+
const setFocus = useCallback(
|
|
567
|
+
(id) => dispatch({ type: "focus", id }),
|
|
568
|
+
[dispatch]
|
|
569
|
+
);
|
|
570
|
+
const clickSelect = useCallback(
|
|
571
|
+
(id, mods) => {
|
|
572
|
+
if (selectionMode === "none") return;
|
|
573
|
+
if (selectionMode === "single") {
|
|
574
|
+
dispatch({ type: "select", id, mode: "single" });
|
|
575
|
+
return;
|
|
576
|
+
}
|
|
577
|
+
const next = selectionFromClick(
|
|
578
|
+
selectionRef.current,
|
|
579
|
+
flatRowsRef.current,
|
|
580
|
+
id,
|
|
581
|
+
mods,
|
|
582
|
+
true
|
|
583
|
+
);
|
|
584
|
+
dispatch({
|
|
585
|
+
type: "selection-replace",
|
|
586
|
+
selected: [...next.selected],
|
|
587
|
+
anchor: next.anchor,
|
|
588
|
+
focused: next.focused
|
|
589
|
+
});
|
|
590
|
+
},
|
|
591
|
+
[dispatch, selectionMode]
|
|
592
|
+
);
|
|
593
|
+
const moveSelect = useCallback(
|
|
594
|
+
(id, opts) => {
|
|
595
|
+
if (selectionMode !== "multiple" || !opts.extend) {
|
|
596
|
+
dispatch({ type: "focus", id });
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
const next = selectionFromMove(
|
|
600
|
+
selectionRef.current,
|
|
601
|
+
flatRowsRef.current,
|
|
602
|
+
id,
|
|
603
|
+
true,
|
|
604
|
+
true
|
|
605
|
+
);
|
|
606
|
+
dispatch({
|
|
607
|
+
type: "selection-replace",
|
|
608
|
+
selected: [...next.selected],
|
|
609
|
+
anchor: next.anchor,
|
|
610
|
+
focused: next.focused
|
|
611
|
+
});
|
|
612
|
+
},
|
|
613
|
+
[dispatch, selectionMode]
|
|
614
|
+
);
|
|
615
|
+
const selectAll = useCallback(() => {
|
|
616
|
+
if (selectionMode !== "multiple") return;
|
|
617
|
+
const next = selectionSelectAll(
|
|
618
|
+
flatRowsRef.current,
|
|
619
|
+
selectionRef.current.focused
|
|
620
|
+
);
|
|
621
|
+
dispatch({
|
|
622
|
+
type: "selection-replace",
|
|
623
|
+
selected: [...next.selected],
|
|
624
|
+
anchor: next.anchor,
|
|
625
|
+
focused: next.focused
|
|
626
|
+
});
|
|
627
|
+
}, [dispatch, selectionMode]);
|
|
628
|
+
return {
|
|
629
|
+
select,
|
|
630
|
+
setSelectedIds,
|
|
631
|
+
clearSelection,
|
|
632
|
+
clickSelect,
|
|
633
|
+
moveSelect,
|
|
634
|
+
selectAll,
|
|
635
|
+
setFocus
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
__name(useSelection, "useSelection");
|
|
639
|
+
function useRename({
|
|
640
|
+
dispatch,
|
|
641
|
+
adapter,
|
|
642
|
+
enableInlineRename,
|
|
643
|
+
nodeById,
|
|
644
|
+
getItemName,
|
|
645
|
+
labels
|
|
646
|
+
}) {
|
|
647
|
+
const enabled = enableInlineRename && !!adapter?.rename;
|
|
648
|
+
const startRename = useCallback(
|
|
649
|
+
(id) => {
|
|
650
|
+
if (!enableInlineRename) return;
|
|
651
|
+
if (!adapter?.rename) {
|
|
652
|
+
if (process.env.NODE_ENV !== "production") {
|
|
653
|
+
console.warn(
|
|
654
|
+
"[Tree] startRename called but adapter.rename is not defined."
|
|
655
|
+
);
|
|
656
|
+
}
|
|
657
|
+
return;
|
|
658
|
+
}
|
|
659
|
+
dispatch({ type: "start-rename", id });
|
|
660
|
+
},
|
|
661
|
+
[dispatch, enableInlineRename, adapter]
|
|
662
|
+
);
|
|
663
|
+
const cancelRename = useCallback(
|
|
664
|
+
() => dispatch({ type: "stop-rename" }),
|
|
665
|
+
[dispatch]
|
|
666
|
+
);
|
|
667
|
+
const commitRename = useCallback(
|
|
668
|
+
async (id, nextName) => {
|
|
669
|
+
if (!adapter?.rename) {
|
|
670
|
+
dispatch({ type: "stop-rename" });
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
const node = nodeById.get(id);
|
|
674
|
+
if (!node) {
|
|
675
|
+
dispatch({ type: "stop-rename" });
|
|
676
|
+
return false;
|
|
677
|
+
}
|
|
678
|
+
const trimmed = nextName.trim();
|
|
679
|
+
if (trimmed === getItemName(node)) {
|
|
680
|
+
dispatch({ type: "stop-rename" });
|
|
681
|
+
return true;
|
|
398
682
|
}
|
|
683
|
+
let err = null;
|
|
684
|
+
if (trimmed === "") err = labels.invalidNameEmpty;
|
|
685
|
+
else err = adapter.validateName?.(trimmed, { node }) ?? null;
|
|
686
|
+
if (err) {
|
|
687
|
+
const dialog = getDialog();
|
|
688
|
+
await dialog?.alert({ title: labels.error, message: err });
|
|
689
|
+
return false;
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
await adapter.rename(node, trimmed);
|
|
693
|
+
} catch (e) {
|
|
694
|
+
const dialog = getDialog();
|
|
695
|
+
await dialog?.alert({
|
|
696
|
+
title: labels.error,
|
|
697
|
+
message: e instanceof Error ? e.message : String(e)
|
|
698
|
+
});
|
|
699
|
+
return false;
|
|
700
|
+
}
|
|
701
|
+
dispatch({ type: "stop-rename" });
|
|
702
|
+
return true;
|
|
703
|
+
},
|
|
704
|
+
[dispatch, adapter, nodeById, getItemName, labels]
|
|
705
|
+
);
|
|
706
|
+
return { enabled, startRename, cancelRename, commitRename };
|
|
707
|
+
}
|
|
708
|
+
__name(useRename, "useRename");
|
|
709
|
+
function useClipboard({
|
|
710
|
+
dispatch,
|
|
711
|
+
clipboard,
|
|
712
|
+
adapter,
|
|
713
|
+
nodeById,
|
|
714
|
+
labels
|
|
715
|
+
}) {
|
|
716
|
+
const clipboardRef = useRef(clipboard);
|
|
717
|
+
clipboardRef.current = clipboard;
|
|
718
|
+
const cutToClipboard = useCallback(
|
|
719
|
+
(ids) => {
|
|
720
|
+
if (ids.length === 0) return;
|
|
721
|
+
dispatch({ type: "clipboard-set", payload: { kind: "cut", ids } });
|
|
722
|
+
},
|
|
723
|
+
[dispatch]
|
|
724
|
+
);
|
|
725
|
+
const copyToClipboard = useCallback(
|
|
726
|
+
(ids) => {
|
|
727
|
+
if (ids.length === 0) return;
|
|
728
|
+
dispatch({ type: "clipboard-set", payload: { kind: "copy", ids } });
|
|
729
|
+
},
|
|
730
|
+
[dispatch]
|
|
731
|
+
);
|
|
732
|
+
const clearClipboard = useCallback(
|
|
733
|
+
() => dispatch({ type: "clipboard-set", payload: null }),
|
|
734
|
+
[dispatch]
|
|
735
|
+
);
|
|
736
|
+
const pasteFromClipboard = useCallback(
|
|
737
|
+
async (target, position = "inside") => {
|
|
738
|
+
const cb = clipboardRef.current;
|
|
739
|
+
if (!cb || cb.ids.length === 0) return;
|
|
740
|
+
if (!adapter) return;
|
|
741
|
+
const nodes = cb.ids.map((id) => nodeById.get(id)).filter((n) => !!n);
|
|
742
|
+
if (nodes.length === 0) {
|
|
743
|
+
dispatch({ type: "clipboard-set", payload: null });
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
try {
|
|
747
|
+
if (cb.kind === "cut") {
|
|
748
|
+
if (!adapter.move) return;
|
|
749
|
+
await adapter.move(nodes, target, position);
|
|
750
|
+
dispatch({ type: "clipboard-set", payload: null });
|
|
751
|
+
} else {
|
|
752
|
+
if (!adapter.copy) return;
|
|
753
|
+
await adapter.copy(nodes, target, position);
|
|
754
|
+
}
|
|
755
|
+
} catch (e) {
|
|
756
|
+
const dialog = getDialog();
|
|
757
|
+
await dialog?.alert({
|
|
758
|
+
title: labels.error,
|
|
759
|
+
message: e instanceof Error ? e.message : String(e)
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
[dispatch, adapter, nodeById, labels]
|
|
764
|
+
);
|
|
765
|
+
return {
|
|
766
|
+
cutToClipboard,
|
|
767
|
+
copyToClipboard,
|
|
768
|
+
pasteFromClipboard,
|
|
769
|
+
clearClipboard
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
__name(useClipboard, "useClipboard");
|
|
773
|
+
async function confirmDelete(ctx) {
|
|
774
|
+
const dialog = getDialog();
|
|
775
|
+
if (!dialog) return false;
|
|
776
|
+
const { selectedNodes, labels, getName } = ctx;
|
|
777
|
+
return dialog.confirm({
|
|
778
|
+
title: labels.confirmDeleteTitle(selectedNodes.length),
|
|
779
|
+
message: labels.confirmDeleteMessage(selectedNodes.map(getName)),
|
|
780
|
+
confirmText: labels.confirmDeleteOk,
|
|
781
|
+
cancelText: labels.confirmDeleteCancel,
|
|
782
|
+
variant: "destructive"
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
__name(confirmDelete, "confirmDelete");
|
|
786
|
+
async function promptName(ctx, spec) {
|
|
787
|
+
const dialog = getDialog();
|
|
788
|
+
if (!dialog) return null;
|
|
789
|
+
return dialog.prompt({
|
|
790
|
+
title: spec.title,
|
|
791
|
+
message: spec.message,
|
|
792
|
+
placeholder: spec.placeholder,
|
|
793
|
+
defaultValue: spec.defaultValue
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
__name(promptName, "promptName");
|
|
797
|
+
async function alertError(ctx, message) {
|
|
798
|
+
const dialog = getDialog();
|
|
799
|
+
if (!dialog) return;
|
|
800
|
+
await dialog.alert({ message, title: ctx.labels.error });
|
|
801
|
+
}
|
|
802
|
+
__name(alertError, "alertError");
|
|
803
|
+
function validateName(ctx, name, validateCtx) {
|
|
804
|
+
if (name.trim() === "") return ctx.labels.invalidNameEmpty;
|
|
805
|
+
return ctx.adapter.validateName?.(name, validateCtx) ?? null;
|
|
806
|
+
}
|
|
807
|
+
__name(validateName, "validateName");
|
|
808
|
+
var BUILTIN_ACTIONS = [
|
|
809
|
+
{
|
|
810
|
+
id: "open",
|
|
811
|
+
label: /* @__PURE__ */ __name((ctx) => ctx.labels.actionOpen, "label"),
|
|
812
|
+
icon: CornerUpLeft,
|
|
813
|
+
available: /* @__PURE__ */ __name(() => false, "available"),
|
|
814
|
+
// wired by Tree on activate; not in menu by default
|
|
815
|
+
run: /* @__PURE__ */ __name(() => {
|
|
816
|
+
}, "run")
|
|
817
|
+
},
|
|
818
|
+
{
|
|
819
|
+
id: "rename",
|
|
820
|
+
label: /* @__PURE__ */ __name((ctx) => ctx.labels.actionRename, "label"),
|
|
821
|
+
icon: Pencil,
|
|
822
|
+
shortcut: "F2",
|
|
823
|
+
available: /* @__PURE__ */ __name((ctx) => !!ctx.adapter.rename && ctx.selectedNodes.length === 1 && !ctx.selectedNodes[0].disabled, "available"),
|
|
824
|
+
run: /* @__PURE__ */ __name(async (ctx) => {
|
|
825
|
+
const node = ctx.selectedNodes[0];
|
|
826
|
+
if (ctx.startInlineRename) {
|
|
827
|
+
ctx.startInlineRename(node.id);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
const name = await promptName(ctx, {
|
|
831
|
+
title: ctx.labels.renameTitle,
|
|
832
|
+
message: ctx.labels.renameMessage,
|
|
833
|
+
placeholder: ctx.getName(node),
|
|
834
|
+
defaultValue: ctx.getName(node)
|
|
835
|
+
});
|
|
836
|
+
if (name === null) return;
|
|
837
|
+
const err = validateName(ctx, name, { node });
|
|
838
|
+
if (err) return alertError(ctx, err);
|
|
839
|
+
await ctx.adapter.rename(node, name);
|
|
840
|
+
}, "run")
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
id: "duplicate",
|
|
844
|
+
label: /* @__PURE__ */ __name((ctx) => ctx.labels.actionDuplicate, "label"),
|
|
845
|
+
icon: Copy,
|
|
846
|
+
shortcut: "\u2318D",
|
|
847
|
+
available: /* @__PURE__ */ __name((ctx) => !!ctx.adapter.duplicate && ctx.selectedNodes.length > 0, "available"),
|
|
848
|
+
run: /* @__PURE__ */ __name((ctx) => ctx.adapter.duplicate(ctx.selectedNodes), "run")
|
|
849
|
+
},
|
|
850
|
+
{
|
|
851
|
+
id: "cut",
|
|
852
|
+
label: /* @__PURE__ */ __name((ctx) => ctx.labels.actionCut, "label"),
|
|
853
|
+
icon: Scissors,
|
|
854
|
+
shortcut: "\u2318X",
|
|
855
|
+
// Only meaningful when the adapter supports `move` (paste-after-cut)
|
|
856
|
+
// AND Tree provided a clipboard binding.
|
|
857
|
+
available: /* @__PURE__ */ __name((ctx) => !!ctx.adapter.move && !!ctx.clipboard && ctx.selectedNodes.length > 0, "available"),
|
|
858
|
+
run: /* @__PURE__ */ __name((ctx) => {
|
|
859
|
+
ctx.clipboard?.cut(ctx.selectedNodes.map((n) => n.id));
|
|
860
|
+
}, "run")
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
id: "copy",
|
|
864
|
+
label: /* @__PURE__ */ __name((ctx) => ctx.labels.actionCopy, "label"),
|
|
865
|
+
icon: Copy,
|
|
866
|
+
shortcut: "\u2318C",
|
|
867
|
+
available: /* @__PURE__ */ __name((ctx) => !!ctx.adapter.copy && !!ctx.clipboard && ctx.selectedNodes.length > 0, "available"),
|
|
868
|
+
run: /* @__PURE__ */ __name((ctx) => {
|
|
869
|
+
ctx.clipboard?.copy(ctx.selectedNodes.map((n) => n.id));
|
|
870
|
+
}, "run")
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
id: "paste",
|
|
874
|
+
label: /* @__PURE__ */ __name((ctx) => ctx.labels.actionPaste, "label"),
|
|
875
|
+
icon: CornerUpLeft,
|
|
876
|
+
shortcut: "\u2318V",
|
|
877
|
+
available: /* @__PURE__ */ __name((ctx) => !!ctx.clipboard?.hasItems, "available"),
|
|
878
|
+
run: /* @__PURE__ */ __name(async (ctx) => {
|
|
879
|
+
await ctx.clipboard?.paste();
|
|
880
|
+
}, "run")
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
id: "delete",
|
|
884
|
+
label: /* @__PURE__ */ __name((ctx) => ctx.labels.actionDelete, "label"),
|
|
885
|
+
icon: Trash2,
|
|
886
|
+
shortcut: "\u2318\u232B",
|
|
887
|
+
destructive: true,
|
|
888
|
+
available: /* @__PURE__ */ __name((ctx) => !!ctx.adapter.remove && ctx.selectedNodes.length > 0, "available"),
|
|
889
|
+
run: /* @__PURE__ */ __name(async (ctx) => {
|
|
890
|
+
const ok = await confirmDelete(ctx);
|
|
891
|
+
if (!ok) return;
|
|
892
|
+
await ctx.adapter.remove(ctx.selectedNodes);
|
|
893
|
+
}, "run")
|
|
894
|
+
},
|
|
895
|
+
{
|
|
896
|
+
id: "new-file",
|
|
897
|
+
label: /* @__PURE__ */ __name((ctx) => ctx.labels.actionNewFile, "label"),
|
|
898
|
+
icon: FilePlus,
|
|
899
|
+
available: /* @__PURE__ */ __name((ctx) => !!ctx.adapter.createFile, "available"),
|
|
900
|
+
run: /* @__PURE__ */ __name(async (ctx) => {
|
|
901
|
+
const parent = resolveParentForCreate(ctx);
|
|
902
|
+
const name = await promptName(ctx, {
|
|
903
|
+
title: ctx.labels.newFileTitle,
|
|
904
|
+
message: ctx.labels.newFileMessage,
|
|
905
|
+
placeholder: ctx.labels.newFilePlaceholder,
|
|
906
|
+
defaultValue: ctx.labels.newFileDefault
|
|
907
|
+
});
|
|
908
|
+
if (name === null) return;
|
|
909
|
+
const err = validateName(ctx, name, { parent });
|
|
910
|
+
if (err) return alertError(ctx, err);
|
|
911
|
+
await ctx.adapter.createFile(parent, name);
|
|
912
|
+
}, "run")
|
|
913
|
+
},
|
|
914
|
+
{
|
|
915
|
+
id: "new-folder",
|
|
916
|
+
label: /* @__PURE__ */ __name((ctx) => ctx.labels.actionNewFolder, "label"),
|
|
917
|
+
icon: FolderPlus,
|
|
918
|
+
shortcut: "\u2318\u21E7N",
|
|
919
|
+
available: /* @__PURE__ */ __name((ctx) => !!ctx.adapter.createFolder, "available"),
|
|
920
|
+
run: /* @__PURE__ */ __name(async (ctx) => {
|
|
921
|
+
const parent = resolveParentForCreate(ctx);
|
|
922
|
+
const name = await promptName(ctx, {
|
|
923
|
+
title: ctx.labels.newFolderTitle,
|
|
924
|
+
message: ctx.labels.newFolderMessage,
|
|
925
|
+
placeholder: ctx.labels.newFolderPlaceholder,
|
|
926
|
+
defaultValue: ctx.labels.newFolderDefault
|
|
927
|
+
});
|
|
928
|
+
if (name === null) return;
|
|
929
|
+
const err = validateName(ctx, name, { parent });
|
|
930
|
+
if (err) return alertError(ctx, err);
|
|
931
|
+
await ctx.adapter.createFolder(parent, name);
|
|
932
|
+
}, "run")
|
|
933
|
+
}
|
|
934
|
+
];
|
|
935
|
+
var DEFAULT_BUILTIN_MENU_ORDER = [
|
|
936
|
+
"rename",
|
|
937
|
+
"duplicate",
|
|
938
|
+
"separator",
|
|
939
|
+
"cut",
|
|
940
|
+
"copy",
|
|
941
|
+
"paste",
|
|
942
|
+
"separator",
|
|
943
|
+
"new-file",
|
|
944
|
+
"new-folder",
|
|
945
|
+
"separator",
|
|
946
|
+
"delete"
|
|
947
|
+
];
|
|
948
|
+
function resolveParentForCreate(ctx) {
|
|
949
|
+
const { targetNode } = ctx;
|
|
950
|
+
if (!targetNode) return null;
|
|
951
|
+
const isFolder = Array.isArray(targetNode.children) || !!targetNode.isFolder;
|
|
952
|
+
return isFolder ? targetNode : null;
|
|
953
|
+
}
|
|
954
|
+
__name(resolveParentForCreate, "resolveParentForCreate");
|
|
955
|
+
function buildDefaultMenuItems(ctx, order = DEFAULT_BUILTIN_MENU_ORDER) {
|
|
956
|
+
const items = [];
|
|
957
|
+
let pendingSeparator = false;
|
|
958
|
+
for (const entry of order) {
|
|
959
|
+
if (entry === "separator") {
|
|
960
|
+
pendingSeparator = items.length > 0;
|
|
961
|
+
continue;
|
|
399
962
|
}
|
|
400
|
-
|
|
401
|
-
|
|
963
|
+
const desc = BUILTIN_ACTIONS.find((a) => a.id === entry);
|
|
964
|
+
if (!desc) continue;
|
|
965
|
+
if (!desc.available(ctx)) continue;
|
|
966
|
+
if (pendingSeparator) {
|
|
967
|
+
items.push("separator");
|
|
968
|
+
pendingSeparator = false;
|
|
969
|
+
}
|
|
970
|
+
items.push({
|
|
971
|
+
id: desc.id,
|
|
972
|
+
label: desc.label(ctx),
|
|
973
|
+
icon: desc.icon,
|
|
974
|
+
shortcut: desc.shortcut,
|
|
975
|
+
destructive: desc.destructive,
|
|
976
|
+
onSelect: /* @__PURE__ */ __name(() => void desc.run(ctx), "onSelect")
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
return items.length > 0 ? items : null;
|
|
980
|
+
}
|
|
981
|
+
__name(buildDefaultMenuItems, "buildDefaultMenuItems");
|
|
982
|
+
async function runBuiltinAction(id, ctx) {
|
|
983
|
+
const desc = BUILTIN_ACTIONS.find((a) => a.id === id);
|
|
984
|
+
if (!desc) return false;
|
|
985
|
+
if (!desc.available(ctx)) return false;
|
|
986
|
+
await desc.run(ctx);
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
__name(runBuiltinAction, "runBuiltinAction");
|
|
990
|
+
function useResolvedMenu(opts) {
|
|
991
|
+
const {
|
|
992
|
+
adapter,
|
|
993
|
+
contextMenuActions,
|
|
994
|
+
defaultMenuItems,
|
|
995
|
+
labels,
|
|
996
|
+
selected,
|
|
997
|
+
clipboard,
|
|
998
|
+
nodeById,
|
|
999
|
+
getItemName,
|
|
1000
|
+
enableInlineRename,
|
|
1001
|
+
startRename,
|
|
1002
|
+
cutToClipboard,
|
|
1003
|
+
copyToClipboard,
|
|
1004
|
+
pasteFromClipboard
|
|
1005
|
+
} = opts;
|
|
1006
|
+
return useMemo(() => {
|
|
1007
|
+
if (!adapter && !contextMenuActions) return void 0;
|
|
1008
|
+
return (rowProps) => {
|
|
1009
|
+
const selectedIds = selected.has(rowProps.node.id) ? [...selected] : [rowProps.node.id];
|
|
1010
|
+
const selectedNodes = selectedIds.map((id) => nodeById.get(id)).filter((n) => !!n);
|
|
1011
|
+
const builtin = adapter ? buildDefaultMenuItems(
|
|
1012
|
+
{
|
|
1013
|
+
adapter,
|
|
1014
|
+
labels,
|
|
1015
|
+
selectedNodes,
|
|
1016
|
+
targetNode: rowProps.node,
|
|
1017
|
+
getName: getItemName,
|
|
1018
|
+
startInlineRename: enableInlineRename && adapter.rename ? startRename : void 0,
|
|
1019
|
+
clipboard: {
|
|
1020
|
+
hasItems: !!clipboard && clipboard.ids.length > 0,
|
|
1021
|
+
cut: cutToClipboard,
|
|
1022
|
+
copy: copyToClipboard,
|
|
1023
|
+
paste: /* @__PURE__ */ __name(() => pasteFromClipboard(rowProps.node, "inside"), "paste")
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
defaultMenuItems ? defaultMenuItems : void 0
|
|
1027
|
+
) : null;
|
|
1028
|
+
const user = contextMenuActions?.({ ...rowProps, selectedNodes }) ?? null;
|
|
1029
|
+
if (!builtin && !user) return null;
|
|
1030
|
+
if (!user) return builtin;
|
|
1031
|
+
if (!builtin) return user;
|
|
1032
|
+
return [...builtin, "separator", ...user];
|
|
1033
|
+
};
|
|
1034
|
+
}, [
|
|
1035
|
+
adapter,
|
|
1036
|
+
contextMenuActions,
|
|
1037
|
+
defaultMenuItems,
|
|
1038
|
+
labels,
|
|
1039
|
+
selected,
|
|
1040
|
+
clipboard,
|
|
1041
|
+
nodeById,
|
|
1042
|
+
getItemName,
|
|
1043
|
+
enableInlineRename,
|
|
1044
|
+
startRename,
|
|
1045
|
+
cutToClipboard,
|
|
1046
|
+
copyToClipboard,
|
|
1047
|
+
pasteFromClipboard
|
|
1048
|
+
]);
|
|
1049
|
+
}
|
|
1050
|
+
__name(useResolvedMenu, "useResolvedMenu");
|
|
1051
|
+
|
|
1052
|
+
// src/tools/data/Tree/data/dnd.ts
|
|
1053
|
+
function resolveDropZone(input) {
|
|
1054
|
+
const { pointerY, rowRect, isFolder } = input;
|
|
1055
|
+
const offset = pointerY - rowRect.top;
|
|
1056
|
+
const ratio = rowRect.height > 0 ? offset / rowRect.height : 0.5;
|
|
1057
|
+
if (isFolder) {
|
|
1058
|
+
if (ratio < 0.33) return "before";
|
|
1059
|
+
if (ratio > 0.66) return "after";
|
|
1060
|
+
return "inside";
|
|
1061
|
+
}
|
|
1062
|
+
return ratio < 0.5 ? "before" : "after";
|
|
1063
|
+
}
|
|
1064
|
+
__name(resolveDropZone, "resolveDropZone");
|
|
1065
|
+
function defaultCanDrop(input) {
|
|
1066
|
+
const { source, target, position } = input;
|
|
1067
|
+
if (source.length === 0) return false;
|
|
1068
|
+
if (!target) return true;
|
|
1069
|
+
if (position === "inside") {
|
|
1070
|
+
const isFolder = Array.isArray(target.children) || !!target.isFolder;
|
|
1071
|
+
if (!isFolder) return false;
|
|
1072
|
+
}
|
|
1073
|
+
for (const node of source) {
|
|
1074
|
+
if (node.id === target.id) return false;
|
|
1075
|
+
if (isDescendant(node, target.id)) return false;
|
|
1076
|
+
}
|
|
1077
|
+
return true;
|
|
1078
|
+
}
|
|
1079
|
+
__name(defaultCanDrop, "defaultCanDrop");
|
|
1080
|
+
function isDescendant(root, id) {
|
|
1081
|
+
if (!Array.isArray(root.children)) return false;
|
|
1082
|
+
for (const child of root.children) {
|
|
1083
|
+
if (child.id === id) return true;
|
|
1084
|
+
if (isDescendant(child, id)) return true;
|
|
1085
|
+
}
|
|
1086
|
+
return false;
|
|
1087
|
+
}
|
|
1088
|
+
__name(isDescendant, "isDescendant");
|
|
1089
|
+
var TREE_DND_MIME = "application/x-djangocfg-tree";
|
|
1090
|
+
var TREE_ROOT_DROP_ID = "__tree_root_drop__";
|
|
1091
|
+
|
|
1092
|
+
// src/tools/data/Tree/context/dnd/use-dnd.ts
|
|
1093
|
+
function useDnd({
|
|
1094
|
+
enabled,
|
|
1095
|
+
adapter,
|
|
1096
|
+
nodeById,
|
|
1097
|
+
selected,
|
|
1098
|
+
labels,
|
|
1099
|
+
canDrop
|
|
1100
|
+
}) {
|
|
1101
|
+
const active = enabled && !!adapter?.move;
|
|
1102
|
+
const [draggingIds, setDraggingIds] = useState(
|
|
1103
|
+
() => /* @__PURE__ */ new Set()
|
|
1104
|
+
);
|
|
1105
|
+
const [dropTarget, setDropTarget] = useState(null);
|
|
1106
|
+
const beginDrag = useCallback(
|
|
1107
|
+
(rowId) => {
|
|
1108
|
+
if (!active) return;
|
|
1109
|
+
const ids = selected.has(rowId) ? new Set(selected) : /* @__PURE__ */ new Set([rowId]);
|
|
1110
|
+
setDraggingIds(ids);
|
|
1111
|
+
},
|
|
1112
|
+
[active, selected]
|
|
1113
|
+
);
|
|
1114
|
+
const cancelDrag = useCallback(() => {
|
|
1115
|
+
setDraggingIds(/* @__PURE__ */ new Set());
|
|
1116
|
+
setDropTarget(null);
|
|
1117
|
+
}, []);
|
|
1118
|
+
const resolveSourceNodes = useCallback(() => {
|
|
1119
|
+
const out = [];
|
|
1120
|
+
for (const id of draggingIds) {
|
|
1121
|
+
const node = nodeById.get(id);
|
|
1122
|
+
if (node) out.push(node);
|
|
1123
|
+
}
|
|
1124
|
+
return out;
|
|
1125
|
+
}, [draggingIds, nodeById]);
|
|
1126
|
+
const isAllowedDrop = useCallback(
|
|
1127
|
+
(target, position) => {
|
|
1128
|
+
if (!active) return false;
|
|
1129
|
+
if (draggingIds.size === 0) return false;
|
|
1130
|
+
const source = resolveSourceNodes();
|
|
1131
|
+
if (!defaultCanDrop({
|
|
1132
|
+
source,
|
|
1133
|
+
target,
|
|
1134
|
+
position})) {
|
|
1135
|
+
return false;
|
|
1136
|
+
}
|
|
1137
|
+
return canDrop?.({ source, target, position }) ?? true;
|
|
1138
|
+
},
|
|
1139
|
+
[active, draggingIds, resolveSourceNodes, nodeById, canDrop]
|
|
1140
|
+
);
|
|
1141
|
+
const commitDrop = useCallback(async () => {
|
|
1142
|
+
if (!active || !adapter?.move) {
|
|
1143
|
+
cancelDrag();
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
const t = dropTarget;
|
|
1147
|
+
if (!t) {
|
|
1148
|
+
cancelDrag();
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const targetNode = t.id ? nodeById.get(t.id) ?? null : null;
|
|
1152
|
+
const source = resolveSourceNodes();
|
|
1153
|
+
if (source.length === 0) {
|
|
1154
|
+
cancelDrag();
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
if (!isAllowedDrop(targetNode, t.position)) {
|
|
1158
|
+
cancelDrag();
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
try {
|
|
1162
|
+
await adapter.move(source, targetNode, t.position);
|
|
1163
|
+
} catch (e) {
|
|
1164
|
+
const dialog = getDialog();
|
|
1165
|
+
await dialog?.alert({
|
|
1166
|
+
title: labels.error,
|
|
1167
|
+
message: e instanceof Error ? e.message : String(e)
|
|
1168
|
+
});
|
|
1169
|
+
} finally {
|
|
1170
|
+
cancelDrag();
|
|
1171
|
+
}
|
|
1172
|
+
}, [
|
|
1173
|
+
active,
|
|
1174
|
+
adapter,
|
|
1175
|
+
cancelDrag,
|
|
1176
|
+
dropTarget,
|
|
1177
|
+
isAllowedDrop,
|
|
1178
|
+
labels,
|
|
1179
|
+
nodeById,
|
|
1180
|
+
resolveSourceNodes
|
|
1181
|
+
]);
|
|
1182
|
+
return useMemo(
|
|
1183
|
+
() => ({
|
|
1184
|
+
active,
|
|
1185
|
+
draggingIds,
|
|
1186
|
+
dropTarget,
|
|
1187
|
+
beginDrag,
|
|
1188
|
+
setDropTarget,
|
|
1189
|
+
commitDrop,
|
|
1190
|
+
cancelDrag,
|
|
1191
|
+
isAllowedDrop
|
|
1192
|
+
}),
|
|
1193
|
+
[
|
|
1194
|
+
active,
|
|
1195
|
+
draggingIds,
|
|
1196
|
+
dropTarget,
|
|
1197
|
+
beginDrag,
|
|
1198
|
+
commitDrop,
|
|
1199
|
+
cancelDrag,
|
|
1200
|
+
isAllowedDrop
|
|
1201
|
+
]
|
|
1202
|
+
);
|
|
1203
|
+
}
|
|
1204
|
+
__name(useDnd, "useDnd");
|
|
1205
|
+
function usePersistSync({
|
|
1206
|
+
expanded,
|
|
1207
|
+
selected,
|
|
1208
|
+
persistKey,
|
|
1209
|
+
persistSelection,
|
|
1210
|
+
onSelectionChange,
|
|
1211
|
+
onExpansionChange
|
|
1212
|
+
}) {
|
|
402
1213
|
const onSelectionChangeRef = useRef(onSelectionChange);
|
|
403
1214
|
const onExpansionChangeRef = useRef(onExpansionChange);
|
|
404
|
-
const onActivateRef = useRef(onActivate);
|
|
405
1215
|
onSelectionChangeRef.current = onSelectionChange;
|
|
406
1216
|
onExpansionChangeRef.current = onExpansionChange;
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
const lastExpandedArrRef = useRef([...state.expanded]);
|
|
1217
|
+
const lastSelectedArrRef = useRef([...selected]);
|
|
1218
|
+
const lastExpandedArrRef = useRef([...expanded]);
|
|
410
1219
|
useEffect(() => {
|
|
411
|
-
const arr = [...
|
|
412
|
-
if (!setEqualsArr(
|
|
1220
|
+
const arr = [...expanded];
|
|
1221
|
+
if (!setEqualsArr(expanded, lastExpandedArrRef.current)) {
|
|
413
1222
|
lastExpandedArrRef.current = arr;
|
|
414
1223
|
onExpansionChangeRef.current?.(arr);
|
|
415
1224
|
if (persistKey) {
|
|
416
1225
|
saveTreeState(persistKey, {
|
|
417
1226
|
expandedItems: arr,
|
|
418
|
-
selectedItems: persistSelection ? [...
|
|
1227
|
+
selectedItems: persistSelection ? [...selected] : []
|
|
419
1228
|
});
|
|
420
1229
|
}
|
|
421
1230
|
}
|
|
422
|
-
}, [
|
|
1231
|
+
}, [expanded, persistKey, persistSelection, selected]);
|
|
423
1232
|
useEffect(() => {
|
|
424
|
-
const arr = [...
|
|
425
|
-
if (!setEqualsArr(
|
|
1233
|
+
const arr = [...selected];
|
|
1234
|
+
if (!setEqualsArr(selected, lastSelectedArrRef.current)) {
|
|
426
1235
|
lastSelectedArrRef.current = arr;
|
|
427
1236
|
onSelectionChangeRef.current?.(arr);
|
|
428
1237
|
if (persistKey && persistSelection) {
|
|
429
1238
|
saveTreeState(persistKey, {
|
|
430
|
-
expandedItems: [...
|
|
1239
|
+
expandedItems: [...expanded],
|
|
431
1240
|
selectedItems: arr
|
|
432
1241
|
});
|
|
433
1242
|
}
|
|
434
1243
|
}
|
|
435
|
-
}, [
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
)
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
1244
|
+
}, [selected, persistKey, persistSelection, expanded]);
|
|
1245
|
+
}
|
|
1246
|
+
__name(usePersistSync, "usePersistSync");
|
|
1247
|
+
function setEqualsArr(set, arr) {
|
|
1248
|
+
if (set.size !== arr.length) return false;
|
|
1249
|
+
for (const id of arr) if (!set.has(id)) return false;
|
|
1250
|
+
return true;
|
|
1251
|
+
}
|
|
1252
|
+
__name(setEqualsArr, "setEqualsArr");
|
|
1253
|
+
var TreeContext = createContext(null);
|
|
1254
|
+
function useTreeContext() {
|
|
1255
|
+
const ctx = React.useContext(TreeContext);
|
|
1256
|
+
if (!ctx) {
|
|
1257
|
+
throw new Error("useTreeContext must be used inside <TreeProvider>");
|
|
1258
|
+
}
|
|
1259
|
+
return ctx;
|
|
1260
|
+
}
|
|
1261
|
+
__name(useTreeContext, "useTreeContext");
|
|
1262
|
+
function TreeProvider(props) {
|
|
1263
|
+
const {
|
|
1264
|
+
data,
|
|
1265
|
+
getItemName,
|
|
1266
|
+
loadChildren,
|
|
1267
|
+
selectionMode = "single",
|
|
1268
|
+
activationMode = "single-click",
|
|
1269
|
+
initialExpandedIds,
|
|
1270
|
+
initialSelectedIds,
|
|
1271
|
+
indent,
|
|
1272
|
+
appearance,
|
|
1273
|
+
onSelectionChange,
|
|
1274
|
+
onExpansionChange,
|
|
1275
|
+
onActivate,
|
|
1276
|
+
filterNode,
|
|
1277
|
+
enableSearch = false,
|
|
1278
|
+
showIndentGuides = false,
|
|
1279
|
+
renderIcon,
|
|
1280
|
+
renderLabel,
|
|
1281
|
+
renderActions,
|
|
1282
|
+
renderContextMenu,
|
|
1283
|
+
contextMenuActions,
|
|
1284
|
+
labels: labelsOverride,
|
|
1285
|
+
persistKey,
|
|
1286
|
+
persistSelection = false,
|
|
1287
|
+
adapter,
|
|
1288
|
+
defaultMenuItems,
|
|
1289
|
+
enableInlineRename = false,
|
|
1290
|
+
enableDnD = false,
|
|
1291
|
+
canDrop,
|
|
1292
|
+
children
|
|
1293
|
+
} = props;
|
|
1294
|
+
const labels = useMemo(
|
|
1295
|
+
() => ({ ...DEFAULT_TREE_LABELS, ...labelsOverride }),
|
|
1296
|
+
[labelsOverride]
|
|
455
1297
|
);
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
[]
|
|
1298
|
+
const resolvedAppearance = useMemo(
|
|
1299
|
+
() => resolveAppearance(appearance, indent),
|
|
1300
|
+
[appearance, indent]
|
|
460
1301
|
);
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
const node = nodeById.get(id);
|
|
465
|
-
if (!node || !loadChildren) return;
|
|
466
|
-
cacheRef.current.delete(id);
|
|
467
|
-
dispatch({ type: "cache-tick" });
|
|
468
|
-
await fetchChildren(node);
|
|
469
|
-
},
|
|
470
|
-
[nodeById, loadChildren, fetchChildren]
|
|
1302
|
+
const persisted = useMemo(
|
|
1303
|
+
() => persistKey ? loadTreeState(persistKey) : null,
|
|
1304
|
+
[persistKey]
|
|
471
1305
|
);
|
|
472
|
-
const
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
1306
|
+
const [state, dispatch] = useReducer(
|
|
1307
|
+
reducer,
|
|
1308
|
+
void 0,
|
|
1309
|
+
() => createInitialState({
|
|
1310
|
+
persisted,
|
|
1311
|
+
initialExpandedIds,
|
|
1312
|
+
initialSelectedIds,
|
|
1313
|
+
persistSelection
|
|
1314
|
+
})
|
|
1315
|
+
);
|
|
1316
|
+
const bumpCacheTick = useCallback(() => dispatch({ type: "cache-tick" }), []);
|
|
1317
|
+
const {
|
|
1318
|
+
nodeById,
|
|
1319
|
+
refresh,
|
|
1320
|
+
refreshAll,
|
|
1321
|
+
collectFolderIds,
|
|
1322
|
+
cache
|
|
1323
|
+
} = useAsyncChildren({
|
|
1324
|
+
data,
|
|
1325
|
+
loadChildren,
|
|
1326
|
+
expanded: state.expanded,
|
|
1327
|
+
cacheTick: state.cacheTick,
|
|
1328
|
+
bumpCacheTick
|
|
1329
|
+
});
|
|
1330
|
+
const flatRows = useMemo(
|
|
1331
|
+
() => flattenTree({
|
|
1332
|
+
roots: data,
|
|
1333
|
+
expandedIds: state.expanded,
|
|
1334
|
+
cache,
|
|
1335
|
+
filterNode
|
|
1336
|
+
}),
|
|
1337
|
+
[data, state.expanded, state.cacheTick, cache, filterNode]
|
|
1338
|
+
);
|
|
1339
|
+
const matchingIds = useMemo(() => {
|
|
1340
|
+
const set = /* @__PURE__ */ new Set();
|
|
1341
|
+
if (!enableSearch || state.query.trim() === "") return set;
|
|
1342
|
+
const q = state.query.trim().toLowerCase();
|
|
1343
|
+
for (const row of flatRows) {
|
|
1344
|
+
if (getItemName(row.node).toLowerCase().includes(q)) {
|
|
1345
|
+
set.add(row.node.id);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return set;
|
|
1349
|
+
}, [enableSearch, state.query, flatRows, getItemName]);
|
|
1350
|
+
const expansion = useExpansion({ dispatch, collectFolderIds });
|
|
1351
|
+
const selection = useSelection({
|
|
1352
|
+
dispatch,
|
|
1353
|
+
selectionMode,
|
|
1354
|
+
flatRows,
|
|
1355
|
+
selected: state.selected,
|
|
1356
|
+
anchor: state.anchor,
|
|
1357
|
+
focused: state.focused
|
|
1358
|
+
});
|
|
1359
|
+
const rename = useRename({
|
|
1360
|
+
dispatch,
|
|
1361
|
+
adapter,
|
|
1362
|
+
enableInlineRename,
|
|
1363
|
+
nodeById,
|
|
1364
|
+
getItemName,
|
|
1365
|
+
labels
|
|
1366
|
+
});
|
|
1367
|
+
const clipboard = useClipboard({
|
|
1368
|
+
dispatch,
|
|
1369
|
+
clipboard: state.clipboard,
|
|
1370
|
+
adapter,
|
|
1371
|
+
nodeById,
|
|
1372
|
+
labels
|
|
1373
|
+
});
|
|
1374
|
+
const dnd = useDnd({
|
|
1375
|
+
enabled: enableDnD,
|
|
1376
|
+
adapter,
|
|
1377
|
+
nodeById,
|
|
1378
|
+
selected: state.selected,
|
|
1379
|
+
labels,
|
|
1380
|
+
canDrop
|
|
1381
|
+
});
|
|
1382
|
+
const onActivateRef = useRef(onActivate);
|
|
1383
|
+
onActivateRef.current = onActivate;
|
|
483
1384
|
const activate = useCallback(
|
|
484
1385
|
(node, opts = { preview: false }) => onActivateRef.current?.(node, opts),
|
|
485
1386
|
[]
|
|
486
1387
|
);
|
|
1388
|
+
const setQuery = useCallback(
|
|
1389
|
+
(q) => dispatch({ type: "set-query", q }),
|
|
1390
|
+
[]
|
|
1391
|
+
);
|
|
1392
|
+
usePersistSync({
|
|
1393
|
+
expanded: state.expanded,
|
|
1394
|
+
selected: state.selected,
|
|
1395
|
+
persistKey,
|
|
1396
|
+
persistSelection,
|
|
1397
|
+
onSelectionChange,
|
|
1398
|
+
onExpansionChange
|
|
1399
|
+
});
|
|
1400
|
+
const resolvedContextMenuActions = useResolvedMenu({
|
|
1401
|
+
adapter,
|
|
1402
|
+
contextMenuActions,
|
|
1403
|
+
defaultMenuItems,
|
|
1404
|
+
labels,
|
|
1405
|
+
selected: state.selected,
|
|
1406
|
+
clipboard: state.clipboard,
|
|
1407
|
+
nodeById,
|
|
1408
|
+
getItemName,
|
|
1409
|
+
enableInlineRename,
|
|
1410
|
+
startRename: rename.startRename,
|
|
1411
|
+
cutToClipboard: clipboard.cutToClipboard,
|
|
1412
|
+
copyToClipboard: clipboard.copyToClipboard,
|
|
1413
|
+
pasteFromClipboard: clipboard.pasteFromClipboard
|
|
1414
|
+
});
|
|
487
1415
|
const value = useMemo(
|
|
488
1416
|
() => ({
|
|
1417
|
+
// state
|
|
489
1418
|
expanded: state.expanded,
|
|
490
1419
|
selected: state.selected,
|
|
1420
|
+
anchor: state.anchor,
|
|
491
1421
|
focused: state.focused,
|
|
492
1422
|
query: state.query,
|
|
1423
|
+
renamingId: state.renaming,
|
|
1424
|
+
inlineRenameEnabled: rename.enabled,
|
|
1425
|
+
clipboard: state.clipboard,
|
|
493
1426
|
flatRows,
|
|
494
1427
|
matchingIds,
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
collapseAll,
|
|
500
|
-
select,
|
|
501
|
-
setSelectedIds,
|
|
502
|
-
clearSelection,
|
|
503
|
-
setFocus,
|
|
1428
|
+
// expansion
|
|
1429
|
+
...expansion,
|
|
1430
|
+
// selection (note: `select` is from selection hook; expansion exports no `select`)
|
|
1431
|
+
...selection,
|
|
504
1432
|
setQuery,
|
|
1433
|
+
// clipboard
|
|
1434
|
+
...clipboard,
|
|
1435
|
+
// rename
|
|
1436
|
+
startRename: rename.startRename,
|
|
1437
|
+
cancelRename: rename.cancelRename,
|
|
1438
|
+
commitRename: rename.commitRename,
|
|
1439
|
+
// async
|
|
505
1440
|
refresh,
|
|
506
1441
|
refreshAll,
|
|
507
1442
|
activate,
|
|
1443
|
+
// config
|
|
508
1444
|
labels,
|
|
509
1445
|
appearance: resolvedAppearance,
|
|
510
1446
|
indent: resolvedAppearance.indent,
|
|
@@ -513,28 +1449,34 @@ function TreeProvider(props) {
|
|
|
513
1449
|
enableSearch,
|
|
514
1450
|
showIndentGuides,
|
|
515
1451
|
getItemName,
|
|
1452
|
+
// slots
|
|
516
1453
|
renderIcon,
|
|
517
1454
|
renderLabel,
|
|
518
1455
|
renderActions,
|
|
519
|
-
renderContextMenu
|
|
1456
|
+
renderContextMenu,
|
|
1457
|
+
adapter,
|
|
1458
|
+
resolvedContextMenuActions,
|
|
1459
|
+
getNodeById: /* @__PURE__ */ __name((id) => nodeById.get(id), "getNodeById"),
|
|
1460
|
+
dnd
|
|
520
1461
|
}),
|
|
521
1462
|
[
|
|
522
1463
|
state.expanded,
|
|
523
1464
|
state.selected,
|
|
1465
|
+
state.anchor,
|
|
524
1466
|
state.focused,
|
|
525
1467
|
state.query,
|
|
1468
|
+
state.renaming,
|
|
1469
|
+
state.clipboard,
|
|
1470
|
+
rename.enabled,
|
|
1471
|
+
rename.startRename,
|
|
1472
|
+
rename.cancelRename,
|
|
1473
|
+
rename.commitRename,
|
|
526
1474
|
flatRows,
|
|
527
1475
|
matchingIds,
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
toggle,
|
|
531
|
-
expandAll,
|
|
532
|
-
collapseAll,
|
|
533
|
-
select,
|
|
534
|
-
setSelectedIds,
|
|
535
|
-
clearSelection,
|
|
536
|
-
setFocus,
|
|
1476
|
+
expansion,
|
|
1477
|
+
selection,
|
|
537
1478
|
setQuery,
|
|
1479
|
+
clipboard,
|
|
538
1480
|
refresh,
|
|
539
1481
|
refreshAll,
|
|
540
1482
|
activate,
|
|
@@ -548,12 +1490,98 @@ function TreeProvider(props) {
|
|
|
548
1490
|
renderIcon,
|
|
549
1491
|
renderLabel,
|
|
550
1492
|
renderActions,
|
|
551
|
-
renderContextMenu
|
|
1493
|
+
renderContextMenu,
|
|
1494
|
+
adapter,
|
|
1495
|
+
resolvedContextMenuActions,
|
|
1496
|
+
nodeById,
|
|
1497
|
+
dnd
|
|
552
1498
|
]
|
|
553
1499
|
);
|
|
554
1500
|
return /* @__PURE__ */ jsx(TreeContext.Provider, { value, children });
|
|
555
1501
|
}
|
|
556
1502
|
__name(TreeProvider, "TreeProvider");
|
|
1503
|
+
function TreeDndProvider({ children }) {
|
|
1504
|
+
const ctx = useTreeContext();
|
|
1505
|
+
const { dnd } = ctx;
|
|
1506
|
+
const sensors = useSensors(
|
|
1507
|
+
useSensor(PointerSensor, { activationConstraint: { distance: 4 } }),
|
|
1508
|
+
useSensor(KeyboardSensor)
|
|
1509
|
+
);
|
|
1510
|
+
const cursorYRef = useRef(0);
|
|
1511
|
+
const handleDragStart = useCallback(
|
|
1512
|
+
(e) => {
|
|
1513
|
+
dnd.beginDrag(e.active.id);
|
|
1514
|
+
},
|
|
1515
|
+
[dnd]
|
|
1516
|
+
);
|
|
1517
|
+
const handleDragMove = useCallback(
|
|
1518
|
+
(e) => {
|
|
1519
|
+
const overId = e.over?.id;
|
|
1520
|
+
if (overId === TREE_ROOT_DROP_ID) {
|
|
1521
|
+
const current2 = dnd.dropTarget;
|
|
1522
|
+
if (current2?.id !== null || current2?.position !== "inside") {
|
|
1523
|
+
dnd.setDropTarget({ id: null, position: "inside" });
|
|
1524
|
+
}
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
if (typeof overId !== "string") {
|
|
1528
|
+
if (dnd.dropTarget !== null) dnd.setDropTarget(null);
|
|
1529
|
+
return;
|
|
1530
|
+
}
|
|
1531
|
+
const rect = e.over?.rect;
|
|
1532
|
+
if (!rect) return;
|
|
1533
|
+
const el = document.querySelector(
|
|
1534
|
+
`[data-tree-row][data-id="${CSS.escape(overId)}"]`
|
|
1535
|
+
);
|
|
1536
|
+
const isFolder = el?.dataset.folder === "true";
|
|
1537
|
+
const position = resolveDropZone({
|
|
1538
|
+
pointerY: cursorYRef.current,
|
|
1539
|
+
rowRect: { top: rect.top, bottom: rect.top + rect.height, height: rect.height },
|
|
1540
|
+
isFolder
|
|
1541
|
+
});
|
|
1542
|
+
const current = dnd.dropTarget;
|
|
1543
|
+
if (current?.id !== overId || current.position !== position) {
|
|
1544
|
+
dnd.setDropTarget({ id: overId, position });
|
|
1545
|
+
}
|
|
1546
|
+
},
|
|
1547
|
+
[dnd]
|
|
1548
|
+
);
|
|
1549
|
+
const handleDragEnd = useCallback(
|
|
1550
|
+
async (_e) => {
|
|
1551
|
+
await dnd.commitDrop();
|
|
1552
|
+
},
|
|
1553
|
+
[dnd]
|
|
1554
|
+
);
|
|
1555
|
+
const handleDragCancel = useCallback(() => {
|
|
1556
|
+
dnd.cancelDrag();
|
|
1557
|
+
}, [dnd]);
|
|
1558
|
+
const handlePointerMove = useCallback((e) => {
|
|
1559
|
+
cursorYRef.current = e.clientY;
|
|
1560
|
+
}, []);
|
|
1561
|
+
if (!dnd.active) {
|
|
1562
|
+
return /* @__PURE__ */ jsx(Fragment, { children });
|
|
1563
|
+
}
|
|
1564
|
+
return /* @__PURE__ */ jsx(
|
|
1565
|
+
DndContext,
|
|
1566
|
+
{
|
|
1567
|
+
sensors,
|
|
1568
|
+
onDragStart: handleDragStart,
|
|
1569
|
+
onDragMove: handleDragMove,
|
|
1570
|
+
onDragEnd: handleDragEnd,
|
|
1571
|
+
onDragCancel: handleDragCancel,
|
|
1572
|
+
children: /* @__PURE__ */ jsx(
|
|
1573
|
+
"div",
|
|
1574
|
+
{
|
|
1575
|
+
onPointerMove: handlePointerMove,
|
|
1576
|
+
className: "contents",
|
|
1577
|
+
"data-tree-dnd-surface": "",
|
|
1578
|
+
children
|
|
1579
|
+
}
|
|
1580
|
+
)
|
|
1581
|
+
}
|
|
1582
|
+
);
|
|
1583
|
+
}
|
|
1584
|
+
__name(TreeDndProvider, "TreeDndProvider");
|
|
557
1585
|
function TreeChevronRaw({ isExpanded, isFolder, className }) {
|
|
558
1586
|
const { appearance } = useTreeContext();
|
|
559
1587
|
const size = { width: "var(--tree-icon-size)", height: "var(--tree-icon-size)" };
|
|
@@ -583,6 +1611,48 @@ function TreeChevronRaw({ isExpanded, isFolder, className }) {
|
|
|
583
1611
|
}
|
|
584
1612
|
__name(TreeChevronRaw, "TreeChevronRaw");
|
|
585
1613
|
var TreeChevron = memo(TreeChevronRaw);
|
|
1614
|
+
function TreeDropIndicator({
|
|
1615
|
+
position,
|
|
1616
|
+
indent,
|
|
1617
|
+
invalid = false
|
|
1618
|
+
}) {
|
|
1619
|
+
if (position === "inside") {
|
|
1620
|
+
return /* @__PURE__ */ jsx(
|
|
1621
|
+
"span",
|
|
1622
|
+
{
|
|
1623
|
+
"aria-hidden": true,
|
|
1624
|
+
"data-tree-drop": "inside",
|
|
1625
|
+
className: cn(
|
|
1626
|
+
"pointer-events-none absolute inset-0 rounded-sm ring-1",
|
|
1627
|
+
invalid ? "bg-destructive/10 ring-destructive/40" : "bg-primary/10 ring-primary/40"
|
|
1628
|
+
)
|
|
1629
|
+
}
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
const isBefore = position === "before";
|
|
1633
|
+
return /* @__PURE__ */ jsx(
|
|
1634
|
+
"span",
|
|
1635
|
+
{
|
|
1636
|
+
"aria-hidden": true,
|
|
1637
|
+
"data-tree-drop": position,
|
|
1638
|
+
style: { paddingLeft: indent },
|
|
1639
|
+
className: cn(
|
|
1640
|
+
"pointer-events-none absolute right-0 left-0 h-px",
|
|
1641
|
+
isBefore ? "top-0" : "bottom-0"
|
|
1642
|
+
),
|
|
1643
|
+
children: /* @__PURE__ */ jsx(
|
|
1644
|
+
"span",
|
|
1645
|
+
{
|
|
1646
|
+
className: cn(
|
|
1647
|
+
"block h-0.5 rounded-full",
|
|
1648
|
+
invalid ? "bg-destructive/70" : "bg-primary"
|
|
1649
|
+
)
|
|
1650
|
+
}
|
|
1651
|
+
)
|
|
1652
|
+
}
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
__name(TreeDropIndicator, "TreeDropIndicator");
|
|
586
1656
|
function TreeIconRaw({ isFolder, isExpanded, className }) {
|
|
587
1657
|
const { appearance } = useTreeContext();
|
|
588
1658
|
const Icon = isFolder ? isExpanded ? FolderOpen : Folder : File;
|
|
@@ -639,6 +1709,96 @@ function TreeLabelRaw({ children, isMatchingSearch, className }) {
|
|
|
639
1709
|
}
|
|
640
1710
|
__name(TreeLabelRaw, "TreeLabelRaw");
|
|
641
1711
|
var TreeLabel = memo(TreeLabelRaw);
|
|
1712
|
+
|
|
1713
|
+
// src/tools/data/Tree/data/renameUtils.ts
|
|
1714
|
+
function splitFileName(name) {
|
|
1715
|
+
if (name.length === 0) return { base: "", ext: "" };
|
|
1716
|
+
if (name.startsWith(".")) {
|
|
1717
|
+
const rest = name.slice(1);
|
|
1718
|
+
const dot2 = rest.lastIndexOf(".");
|
|
1719
|
+
if (dot2 < 0) return { base: name, ext: "" };
|
|
1720
|
+
return { base: "." + rest.slice(0, dot2), ext: rest.slice(dot2) };
|
|
1721
|
+
}
|
|
1722
|
+
const dot = name.lastIndexOf(".");
|
|
1723
|
+
if (dot <= 0) return { base: name, ext: "" };
|
|
1724
|
+
return { base: name.slice(0, dot), ext: name.slice(dot) };
|
|
1725
|
+
}
|
|
1726
|
+
__name(splitFileName, "splitFileName");
|
|
1727
|
+
function autoSelectRange(name, isFolder) {
|
|
1728
|
+
if (isFolder) return [0, name.length];
|
|
1729
|
+
const { base } = splitFileName(name);
|
|
1730
|
+
return [0, base.length];
|
|
1731
|
+
}
|
|
1732
|
+
__name(autoSelectRange, "autoSelectRange");
|
|
1733
|
+
function TreeRenameInput({
|
|
1734
|
+
initialValue,
|
|
1735
|
+
isFolder,
|
|
1736
|
+
onCommit,
|
|
1737
|
+
onCancel,
|
|
1738
|
+
className
|
|
1739
|
+
}) {
|
|
1740
|
+
const [value, setValue] = useState(initialValue);
|
|
1741
|
+
const inputRef = useRef(null);
|
|
1742
|
+
const settledRef = useRef(false);
|
|
1743
|
+
useEffect(() => {
|
|
1744
|
+
const el = inputRef.current;
|
|
1745
|
+
if (!el) return;
|
|
1746
|
+
el.focus();
|
|
1747
|
+
const [start, end] = autoSelectRange(initialValue, isFolder);
|
|
1748
|
+
requestAnimationFrame(() => {
|
|
1749
|
+
try {
|
|
1750
|
+
el.setSelectionRange(start, end);
|
|
1751
|
+
} catch {
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
}, []);
|
|
1755
|
+
const commit = /* @__PURE__ */ __name(() => {
|
|
1756
|
+
if (settledRef.current) return;
|
|
1757
|
+
settledRef.current = true;
|
|
1758
|
+
void onCommit(value);
|
|
1759
|
+
}, "commit");
|
|
1760
|
+
const cancel = /* @__PURE__ */ __name(() => {
|
|
1761
|
+
if (settledRef.current) return;
|
|
1762
|
+
settledRef.current = true;
|
|
1763
|
+
onCancel();
|
|
1764
|
+
}, "cancel");
|
|
1765
|
+
return /* @__PURE__ */ jsx(
|
|
1766
|
+
"input",
|
|
1767
|
+
{
|
|
1768
|
+
ref: inputRef,
|
|
1769
|
+
type: "text",
|
|
1770
|
+
value,
|
|
1771
|
+
onKeyDown: (e) => {
|
|
1772
|
+
e.stopPropagation();
|
|
1773
|
+
if (e.key === "Enter") {
|
|
1774
|
+
e.preventDefault();
|
|
1775
|
+
commit();
|
|
1776
|
+
} else if (e.key === "Escape") {
|
|
1777
|
+
e.preventDefault();
|
|
1778
|
+
cancel();
|
|
1779
|
+
}
|
|
1780
|
+
},
|
|
1781
|
+
onChange: (e) => setValue(e.target.value),
|
|
1782
|
+
onBlur: commit,
|
|
1783
|
+
onClick: (e) => e.stopPropagation(),
|
|
1784
|
+
onDoubleClick: (e) => e.stopPropagation(),
|
|
1785
|
+
onMouseDown: (e) => e.stopPropagation(),
|
|
1786
|
+
onContextMenu: (e) => e.stopPropagation(),
|
|
1787
|
+
className: cn(
|
|
1788
|
+
"min-w-0 flex-1 rounded-sm border border-primary/50 bg-background",
|
|
1789
|
+
"px-1 py-0 text-foreground outline-none",
|
|
1790
|
+
"focus:ring-1 focus:ring-primary/40",
|
|
1791
|
+
className
|
|
1792
|
+
),
|
|
1793
|
+
style: {
|
|
1794
|
+
// Match the row's font metrics so the input doesn't visibly jolt.
|
|
1795
|
+
fontSize: "var(--tree-font-size)",
|
|
1796
|
+
height: "calc(var(--tree-row-height) - 4px)"
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
__name(TreeRenameInput, "TreeRenameInput");
|
|
642
1802
|
function TreeRowRaw({ row, className }) {
|
|
643
1803
|
const ctx = useTreeContext();
|
|
644
1804
|
const {
|
|
@@ -650,6 +1810,7 @@ function TreeRowRaw({ row, className }) {
|
|
|
650
1810
|
matchingIds,
|
|
651
1811
|
select,
|
|
652
1812
|
setSelectedIds,
|
|
1813
|
+
clickSelect,
|
|
653
1814
|
toggle,
|
|
654
1815
|
setFocus,
|
|
655
1816
|
activate,
|
|
@@ -657,13 +1818,19 @@ function TreeRowRaw({ row, className }) {
|
|
|
657
1818
|
renderIcon,
|
|
658
1819
|
renderLabel,
|
|
659
1820
|
renderActions,
|
|
660
|
-
renderContextMenu
|
|
1821
|
+
renderContextMenu,
|
|
1822
|
+
renamingId,
|
|
1823
|
+
commitRename,
|
|
1824
|
+
cancelRename,
|
|
1825
|
+
clipboard,
|
|
1826
|
+
dnd
|
|
661
1827
|
} = ctx;
|
|
662
1828
|
const { node, level, isFolder, isExpanded, isLoading, posInSet, setSize } = row;
|
|
663
1829
|
const isSelected = selected.has(node.id);
|
|
664
1830
|
const isFocused = focused === node.id;
|
|
665
1831
|
const isMatchingSearch = matchingIds.has(node.id);
|
|
666
1832
|
const isMultiSelect = ctx.selectionMode === "multiple";
|
|
1833
|
+
const isCut = clipboard?.kind === "cut" && clipboard.ids.includes(node.id);
|
|
667
1834
|
const slot = {
|
|
668
1835
|
node,
|
|
669
1836
|
level,
|
|
@@ -674,30 +1841,57 @@ function TreeRowRaw({ row, className }) {
|
|
|
674
1841
|
isLoading,
|
|
675
1842
|
isMatchingSearch
|
|
676
1843
|
};
|
|
1844
|
+
const isRenaming = renamingId === node.id;
|
|
1845
|
+
const isDragging = dnd.draggingIds.has(node.id);
|
|
1846
|
+
const dropTarget = dnd.dropTarget;
|
|
1847
|
+
const isDropTarget = dropTarget?.id === node.id;
|
|
1848
|
+
const dropPosition = isDropTarget ? dropTarget.position : null;
|
|
1849
|
+
const dndDisabled = !dnd.active || isRenaming || node.disabled;
|
|
1850
|
+
const draggable = useDraggable({ id: node.id, disabled: dndDisabled });
|
|
1851
|
+
const droppable = useDroppable({ id: node.id, disabled: dndDisabled });
|
|
1852
|
+
const setRowEl = useCallback(
|
|
1853
|
+
(el) => {
|
|
1854
|
+
draggable.setNodeRef(el);
|
|
1855
|
+
droppable.setNodeRef(el);
|
|
1856
|
+
},
|
|
1857
|
+
[draggable, droppable]
|
|
1858
|
+
);
|
|
1859
|
+
const isAllowedDrop = dropPosition && !isDragging ? dnd.isAllowedDrop(node, dropPosition) : true;
|
|
677
1860
|
const handleClick = /* @__PURE__ */ __name((e) => {
|
|
678
|
-
if (node.disabled) return;
|
|
1861
|
+
if (node.disabled || isRenaming) return;
|
|
679
1862
|
setFocus(node.id);
|
|
680
|
-
if (isMultiSelect
|
|
681
|
-
|
|
1863
|
+
if (isMultiSelect) {
|
|
1864
|
+
clickSelect(node.id, { shift: e.shiftKey, meta: e.metaKey || e.ctrlKey });
|
|
682
1865
|
} else {
|
|
683
1866
|
select(node.id);
|
|
684
1867
|
}
|
|
685
1868
|
if (isFolder) {
|
|
1869
|
+
if (e.shiftKey || e.metaKey || e.ctrlKey) return;
|
|
686
1870
|
toggle(node.id);
|
|
687
1871
|
} else if (activationMode === "single-click") {
|
|
1872
|
+
if (e.shiftKey || e.metaKey || e.ctrlKey) return;
|
|
688
1873
|
activate(node, { preview: false });
|
|
689
1874
|
} else if (activationMode === "single-click-preview") {
|
|
1875
|
+
if (e.shiftKey || e.metaKey || e.ctrlKey) return;
|
|
690
1876
|
activate(node, { preview: true });
|
|
691
1877
|
}
|
|
692
1878
|
}, "handleClick");
|
|
693
1879
|
const handleDoubleClick = /* @__PURE__ */ __name(() => {
|
|
694
|
-
if (node.disabled) return;
|
|
1880
|
+
if (node.disabled || isRenaming) return;
|
|
695
1881
|
if (isFolder) return;
|
|
696
1882
|
activate(node, { preview: false });
|
|
697
1883
|
}, "handleDoubleClick");
|
|
1884
|
+
const handleContextMenu = /* @__PURE__ */ __name(() => {
|
|
1885
|
+
if (node.disabled || isRenaming) return;
|
|
1886
|
+
setFocus(node.id);
|
|
1887
|
+
if (!isSelected) {
|
|
1888
|
+
setSelectedIds([node.id]);
|
|
1889
|
+
}
|
|
1890
|
+
}, "handleContextMenu");
|
|
698
1891
|
const trigger = /* @__PURE__ */ jsxs(
|
|
699
1892
|
"div",
|
|
700
1893
|
{
|
|
1894
|
+
ref: dnd.active ? setRowEl : void 0,
|
|
701
1895
|
id: treeRowDomId(node.id),
|
|
702
1896
|
role: "treeitem",
|
|
703
1897
|
"aria-level": level + 1,
|
|
@@ -710,6 +1904,8 @@ function TreeRowRaw({ row, className }) {
|
|
|
710
1904
|
"data-id": node.id,
|
|
711
1905
|
"data-activation-mode": activationMode,
|
|
712
1906
|
"data-selected": isSelected ? "true" : void 0,
|
|
1907
|
+
"data-clipboard": isCut ? "cut" : void 0,
|
|
1908
|
+
"data-dragging": isDragging ? "true" : void 0,
|
|
713
1909
|
"data-focused": isFocused && !isSelected ? "true" : void 0,
|
|
714
1910
|
"data-folder": isFolder || void 0,
|
|
715
1911
|
"data-expanded": isExpanded || void 0,
|
|
@@ -719,8 +1915,11 @@ function TreeRowRaw({ row, className }) {
|
|
|
719
1915
|
height: "var(--tree-row-height)",
|
|
720
1916
|
gap: "var(--tree-gap)"
|
|
721
1917
|
},
|
|
1918
|
+
...dnd.active ? draggable.listeners : {},
|
|
1919
|
+
...dnd.active ? draggable.attributes : {},
|
|
722
1920
|
onClick: handleClick,
|
|
723
1921
|
onDoubleClick: handleDoubleClick,
|
|
1922
|
+
onContextMenu: handleContextMenu,
|
|
724
1923
|
onFocus: () => setFocus(node.id),
|
|
725
1924
|
className: cn(
|
|
726
1925
|
"group/row relative flex w-full select-none items-center pr-2 text-left",
|
|
@@ -730,6 +1929,8 @@ function TreeRowRaw({ row, className }) {
|
|
|
730
1929
|
rowStateClasses(appearance),
|
|
731
1930
|
"focus-visible:ring-1 focus-visible:ring-ring/50",
|
|
732
1931
|
isMatchingSearch && "ring-1 ring-primary/30",
|
|
1932
|
+
isCut && "opacity-60",
|
|
1933
|
+
isDragging && "opacity-40",
|
|
733
1934
|
node.disabled && "opacity-50",
|
|
734
1935
|
className
|
|
735
1936
|
),
|
|
@@ -744,6 +1945,14 @@ function TreeRowRaw({ row, className }) {
|
|
|
744
1945
|
)
|
|
745
1946
|
}
|
|
746
1947
|
) : null,
|
|
1948
|
+
dropPosition && !isDragging ? /* @__PURE__ */ jsx(
|
|
1949
|
+
TreeDropIndicator,
|
|
1950
|
+
{
|
|
1951
|
+
position: dropPosition,
|
|
1952
|
+
indent: 6 + level * appearance.indent,
|
|
1953
|
+
invalid: !isAllowedDrop
|
|
1954
|
+
}
|
|
1955
|
+
) : null,
|
|
747
1956
|
showIndentGuides && level > 0 ? /* @__PURE__ */ jsx(TreeIndentGuides, { level, indent: appearance.indent }) : null,
|
|
748
1957
|
/* @__PURE__ */ jsx(TreeChevron, { isExpanded, isFolder }),
|
|
749
1958
|
isLoading ? /* @__PURE__ */ jsx(
|
|
@@ -760,7 +1969,15 @@ function TreeRowRaw({ row, className }) {
|
|
|
760
1969
|
{
|
|
761
1970
|
className: "flex min-w-0 flex-1 items-center",
|
|
762
1971
|
style: { gap: "var(--tree-gap)" },
|
|
763
|
-
children:
|
|
1972
|
+
children: renamingId === node.id ? /* @__PURE__ */ jsx(
|
|
1973
|
+
TreeRenameInput,
|
|
1974
|
+
{
|
|
1975
|
+
initialValue: getItemName(node),
|
|
1976
|
+
isFolder,
|
|
1977
|
+
onCommit: (next) => commitRename(node.id, next),
|
|
1978
|
+
onCancel: cancelRename
|
|
1979
|
+
}
|
|
1980
|
+
) : renderLabel ? renderLabel(slot) : /* @__PURE__ */ jsx(TreeLabel, { isMatchingSearch, children: getItemName(node) })
|
|
764
1981
|
}
|
|
765
1982
|
),
|
|
766
1983
|
renderActions ? /* @__PURE__ */ jsx(
|
|
@@ -832,6 +2049,105 @@ function TreeContent({
|
|
|
832
2049
|
);
|
|
833
2050
|
}
|
|
834
2051
|
__name(TreeContent, "TreeContent");
|
|
2052
|
+
function TreeEmptyArea({ className }) {
|
|
2053
|
+
const ctx = useTreeContext();
|
|
2054
|
+
const {
|
|
2055
|
+
adapter,
|
|
2056
|
+
labels,
|
|
2057
|
+
getItemName,
|
|
2058
|
+
clipboard,
|
|
2059
|
+
cutToClipboard,
|
|
2060
|
+
copyToClipboard,
|
|
2061
|
+
pasteFromClipboard,
|
|
2062
|
+
startRename,
|
|
2063
|
+
inlineRenameEnabled,
|
|
2064
|
+
dnd
|
|
2065
|
+
} = ctx;
|
|
2066
|
+
const { setNodeRef: setDroppableRef, isOver } = useDroppable({
|
|
2067
|
+
id: TREE_ROOT_DROP_ID,
|
|
2068
|
+
disabled: !dnd.active
|
|
2069
|
+
});
|
|
2070
|
+
const items = useMemo(() => {
|
|
2071
|
+
if (!adapter) return null;
|
|
2072
|
+
const builtinCtx = {
|
|
2073
|
+
adapter,
|
|
2074
|
+
labels,
|
|
2075
|
+
selectedNodes: [],
|
|
2076
|
+
targetNode: null,
|
|
2077
|
+
getName: getItemName,
|
|
2078
|
+
startInlineRename: inlineRenameEnabled ? startRename : void 0,
|
|
2079
|
+
clipboard: {
|
|
2080
|
+
hasItems: !!clipboard && clipboard.ids.length > 0,
|
|
2081
|
+
cut: cutToClipboard,
|
|
2082
|
+
copy: copyToClipboard,
|
|
2083
|
+
paste: /* @__PURE__ */ __name(() => pasteFromClipboard(null, "inside"), "paste")
|
|
2084
|
+
}
|
|
2085
|
+
};
|
|
2086
|
+
return buildDefaultMenuItems(builtinCtx);
|
|
2087
|
+
}, [
|
|
2088
|
+
adapter,
|
|
2089
|
+
labels,
|
|
2090
|
+
getItemName,
|
|
2091
|
+
inlineRenameEnabled,
|
|
2092
|
+
startRename,
|
|
2093
|
+
clipboard,
|
|
2094
|
+
cutToClipboard,
|
|
2095
|
+
copyToClipboard,
|
|
2096
|
+
pasteFromClipboard
|
|
2097
|
+
]);
|
|
2098
|
+
const surface = /* @__PURE__ */ jsx(
|
|
2099
|
+
"div",
|
|
2100
|
+
{
|
|
2101
|
+
ref: dnd.active ? setDroppableRef : void 0,
|
|
2102
|
+
"data-tree-empty-area": "",
|
|
2103
|
+
"data-drop-active": dnd.active && isOver ? "true" : void 0,
|
|
2104
|
+
className: cn(
|
|
2105
|
+
// Soaks up the remaining vertical space inside the scroll
|
|
2106
|
+
// container so right-click on whitespace lands here, not on
|
|
2107
|
+
// the scroll viewport.
|
|
2108
|
+
"min-h-[2.5rem] flex-1",
|
|
2109
|
+
dnd.active && isOver && "bg-primary/5 rounded-sm ring-1 ring-primary/30",
|
|
2110
|
+
className
|
|
2111
|
+
)
|
|
2112
|
+
}
|
|
2113
|
+
);
|
|
2114
|
+
if (!items || items.length === 0) {
|
|
2115
|
+
return surface;
|
|
2116
|
+
}
|
|
2117
|
+
return /* @__PURE__ */ jsxs(ContextMenu, { children: [
|
|
2118
|
+
/* @__PURE__ */ jsx(ContextMenuTrigger, { asChild: true, children: surface }),
|
|
2119
|
+
/* @__PURE__ */ jsx(ContextMenuContent, { children: items.map((item, idx) => {
|
|
2120
|
+
if (item === "separator") {
|
|
2121
|
+
return /* @__PURE__ */ jsx(ContextMenuSeparator, {}, `sep-${idx}`);
|
|
2122
|
+
}
|
|
2123
|
+
const Icon = item.icon;
|
|
2124
|
+
return /* @__PURE__ */ jsxs(
|
|
2125
|
+
ContextMenuItem,
|
|
2126
|
+
{
|
|
2127
|
+
disabled: item.disabled,
|
|
2128
|
+
variant: item.destructive ? "destructive" : void 0,
|
|
2129
|
+
onSelect: () => item.onSelect({
|
|
2130
|
+
node: void 0,
|
|
2131
|
+
level: 0,
|
|
2132
|
+
isSelected: false,
|
|
2133
|
+
isExpanded: false,
|
|
2134
|
+
isFocused: false,
|
|
2135
|
+
isFolder: false,
|
|
2136
|
+
isLoading: false,
|
|
2137
|
+
isMatchingSearch: false
|
|
2138
|
+
}),
|
|
2139
|
+
children: [
|
|
2140
|
+
Icon ? /* @__PURE__ */ jsx(Icon, {}) : null,
|
|
2141
|
+
item.label,
|
|
2142
|
+
item.shortcut ? /* @__PURE__ */ jsx(ContextMenuShortcut, { children: item.shortcut }) : null
|
|
2143
|
+
]
|
|
2144
|
+
},
|
|
2145
|
+
item.id
|
|
2146
|
+
);
|
|
2147
|
+
}) })
|
|
2148
|
+
] });
|
|
2149
|
+
}
|
|
2150
|
+
__name(TreeEmptyArea, "TreeEmptyArea");
|
|
835
2151
|
function useTreeLabels() {
|
|
836
2152
|
return useTreeContext().labels;
|
|
837
2153
|
}
|
|
@@ -850,12 +2166,26 @@ function useTreeSelection() {
|
|
|
850
2166
|
return useMemo(
|
|
851
2167
|
() => ({
|
|
852
2168
|
selectedIds,
|
|
2169
|
+
anchor: ctx.anchor,
|
|
853
2170
|
select: ctx.select,
|
|
854
2171
|
setSelectedIds: ctx.setSelectedIds,
|
|
855
2172
|
clear: ctx.clearSelection,
|
|
2173
|
+
clickSelect: ctx.clickSelect,
|
|
2174
|
+
moveSelect: ctx.moveSelect,
|
|
2175
|
+
selectAll: ctx.selectAll,
|
|
856
2176
|
isSelected
|
|
857
2177
|
}),
|
|
858
|
-
[
|
|
2178
|
+
[
|
|
2179
|
+
selectedIds,
|
|
2180
|
+
ctx.anchor,
|
|
2181
|
+
ctx.select,
|
|
2182
|
+
ctx.setSelectedIds,
|
|
2183
|
+
ctx.clearSelection,
|
|
2184
|
+
ctx.clickSelect,
|
|
2185
|
+
ctx.moveSelect,
|
|
2186
|
+
ctx.selectAll,
|
|
2187
|
+
isSelected
|
|
2188
|
+
]
|
|
859
2189
|
);
|
|
860
2190
|
}
|
|
861
2191
|
__name(useTreeSelection, "useTreeSelection");
|
|
@@ -910,6 +2240,59 @@ function useTreeSearch() {
|
|
|
910
2240
|
);
|
|
911
2241
|
}
|
|
912
2242
|
__name(useTreeSearch, "useTreeSearch");
|
|
2243
|
+
function useTreeDnd() {
|
|
2244
|
+
const ctx = useTreeContext();
|
|
2245
|
+
return ctx.dnd;
|
|
2246
|
+
}
|
|
2247
|
+
__name(useTreeDnd, "useTreeDnd");
|
|
2248
|
+
function useTreeClipboard() {
|
|
2249
|
+
const ctx = useTreeContext();
|
|
2250
|
+
const isCut = useCallback(
|
|
2251
|
+
(id) => ctx.clipboard?.kind === "cut" && ctx.clipboard.ids.includes(id),
|
|
2252
|
+
[ctx.clipboard]
|
|
2253
|
+
);
|
|
2254
|
+
return useMemo(
|
|
2255
|
+
() => ({
|
|
2256
|
+
clipboard: ctx.clipboard,
|
|
2257
|
+
isCut,
|
|
2258
|
+
cut: ctx.cutToClipboard,
|
|
2259
|
+
copy: ctx.copyToClipboard,
|
|
2260
|
+
paste: ctx.pasteFromClipboard,
|
|
2261
|
+
clear: ctx.clearClipboard
|
|
2262
|
+
}),
|
|
2263
|
+
[
|
|
2264
|
+
ctx.clipboard,
|
|
2265
|
+
isCut,
|
|
2266
|
+
ctx.cutToClipboard,
|
|
2267
|
+
ctx.copyToClipboard,
|
|
2268
|
+
ctx.pasteFromClipboard,
|
|
2269
|
+
ctx.clearClipboard
|
|
2270
|
+
]
|
|
2271
|
+
);
|
|
2272
|
+
}
|
|
2273
|
+
__name(useTreeClipboard, "useTreeClipboard");
|
|
2274
|
+
function useTreeRename() {
|
|
2275
|
+
const ctx = useTreeContext();
|
|
2276
|
+
return useMemo(
|
|
2277
|
+
() => ({
|
|
2278
|
+
/** True when the host allowed inline rename AND the adapter exposes `rename`. */
|
|
2279
|
+
enabled: ctx.inlineRenameEnabled,
|
|
2280
|
+
/** Currently renaming id, or `null`. */
|
|
2281
|
+
renamingId: ctx.renamingId,
|
|
2282
|
+
startRename: ctx.startRename,
|
|
2283
|
+
cancelRename: ctx.cancelRename,
|
|
2284
|
+
commitRename: ctx.commitRename
|
|
2285
|
+
}),
|
|
2286
|
+
[
|
|
2287
|
+
ctx.inlineRenameEnabled,
|
|
2288
|
+
ctx.renamingId,
|
|
2289
|
+
ctx.startRename,
|
|
2290
|
+
ctx.cancelRename,
|
|
2291
|
+
ctx.commitRename
|
|
2292
|
+
]
|
|
2293
|
+
);
|
|
2294
|
+
}
|
|
2295
|
+
__name(useTreeRename, "useTreeRename");
|
|
913
2296
|
function useTreeActions() {
|
|
914
2297
|
const ctx = useTreeContext();
|
|
915
2298
|
return useMemo(
|
|
@@ -974,16 +2357,78 @@ function TreeSearchInput({ className, showMatches = true }) {
|
|
|
974
2357
|
);
|
|
975
2358
|
}
|
|
976
2359
|
__name(TreeSearchInput, "TreeSearchInput");
|
|
2360
|
+
|
|
2361
|
+
// src/tools/data/Tree/hooks/keyboard/arrow-nav.ts
|
|
2362
|
+
function nextRowId(rows, idx) {
|
|
2363
|
+
if (rows.length === 0) return null;
|
|
2364
|
+
const next = rows[Math.min(idx + 1, rows.length - 1)] ?? rows[0];
|
|
2365
|
+
return next.node.id;
|
|
2366
|
+
}
|
|
2367
|
+
__name(nextRowId, "nextRowId");
|
|
2368
|
+
function prevRowId(rows, idx) {
|
|
2369
|
+
if (rows.length === 0) return null;
|
|
2370
|
+
const prev = rows[Math.max(idx - 1, 0)] ?? rows[0];
|
|
2371
|
+
return prev.node.id;
|
|
2372
|
+
}
|
|
2373
|
+
__name(prevRowId, "prevRowId");
|
|
2374
|
+
function edgeRowId(rows, edge) {
|
|
2375
|
+
if (rows.length === 0) return null;
|
|
2376
|
+
return edge === "first" ? rows[0].node.id : rows[rows.length - 1].node.id;
|
|
2377
|
+
}
|
|
2378
|
+
__name(edgeRowId, "edgeRowId");
|
|
2379
|
+
|
|
2380
|
+
// src/tools/data/Tree/hooks/keyboard/expand-collapse.ts
|
|
2381
|
+
function resolveRightArrow(current, rows, idx) {
|
|
2382
|
+
if (!current) return { kind: "noop" };
|
|
2383
|
+
if (current.isFolder && !current.isExpanded) {
|
|
2384
|
+
return { kind: "expand", id: current.node.id };
|
|
2385
|
+
}
|
|
2386
|
+
if (current.isFolder && current.isExpanded) {
|
|
2387
|
+
const next = rows[idx + 1];
|
|
2388
|
+
return next ? { kind: "focus", id: next.node.id } : { kind: "noop" };
|
|
2389
|
+
}
|
|
2390
|
+
return { kind: "noop" };
|
|
2391
|
+
}
|
|
2392
|
+
__name(resolveRightArrow, "resolveRightArrow");
|
|
2393
|
+
function resolveLeftArrow(current) {
|
|
2394
|
+
if (!current) return { kind: "noop" };
|
|
2395
|
+
if (current.isFolder && current.isExpanded) {
|
|
2396
|
+
return { kind: "collapse", id: current.node.id };
|
|
2397
|
+
}
|
|
2398
|
+
if (current.parentId) {
|
|
2399
|
+
return { kind: "focus", id: current.parentId };
|
|
2400
|
+
}
|
|
2401
|
+
return { kind: "noop" };
|
|
2402
|
+
}
|
|
2403
|
+
__name(resolveLeftArrow, "resolveLeftArrow");
|
|
2404
|
+
|
|
2405
|
+
// src/tools/data/Tree/hooks/keyboard/activation.ts
|
|
2406
|
+
function resolveActivate(current) {
|
|
2407
|
+
if (!current) return { kind: "noop" };
|
|
2408
|
+
if (current.isFolder) {
|
|
2409
|
+
return {
|
|
2410
|
+
kind: "toggle-folder",
|
|
2411
|
+
id: current.node.id,
|
|
2412
|
+
willExpand: !current.isExpanded
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
return { kind: "activate-leaf", id: current.node.id };
|
|
2416
|
+
}
|
|
2417
|
+
__name(resolveActivate, "resolveActivate");
|
|
2418
|
+
|
|
2419
|
+
// src/tools/data/Tree/hooks/keyboard/use-tree-keyboard.ts
|
|
977
2420
|
function useTreeKeyboard({
|
|
978
2421
|
rows,
|
|
979
2422
|
focusedId,
|
|
980
2423
|
enabled = true,
|
|
2424
|
+
multiSelect = false,
|
|
981
2425
|
onFocus,
|
|
982
2426
|
onSelect,
|
|
983
2427
|
onActivate,
|
|
984
2428
|
onExpand,
|
|
985
2429
|
onCollapse,
|
|
986
|
-
onClearSelection
|
|
2430
|
+
onClearSelection,
|
|
2431
|
+
onSelectAll
|
|
987
2432
|
}) {
|
|
988
2433
|
const rowsRef = useRef(rows);
|
|
989
2434
|
const focusedIdRef = useRef(focusedId);
|
|
@@ -996,53 +2441,65 @@ function useTreeKeyboard({
|
|
|
996
2441
|
return { rows: r, idx, current: idx >= 0 ? r[idx] : null };
|
|
997
2442
|
}, "getCurrent");
|
|
998
2443
|
const refDown = useHotkey(
|
|
999
|
-
"down",
|
|
1000
|
-
() => {
|
|
2444
|
+
["down", "shift+down"],
|
|
2445
|
+
(e) => {
|
|
1001
2446
|
const { rows: r, idx } = getCurrent();
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
onFocus(next.node.id);
|
|
2447
|
+
const id = nextRowId(r, idx);
|
|
2448
|
+
if (id) onFocus(id, { extend: multiSelect && e.shiftKey });
|
|
1005
2449
|
},
|
|
1006
|
-
{ enabled, preventDefault: true, description: "Next row" }
|
|
2450
|
+
{ enabled, preventDefault: true, description: "Next row (Shift extends)" }
|
|
1007
2451
|
);
|
|
1008
2452
|
const refUp = useHotkey(
|
|
1009
|
-
"up",
|
|
1010
|
-
() => {
|
|
2453
|
+
["up", "shift+up"],
|
|
2454
|
+
(e) => {
|
|
1011
2455
|
const { rows: r, idx } = getCurrent();
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
onFocus(prev.node.id);
|
|
2456
|
+
const id = prevRowId(r, idx);
|
|
2457
|
+
if (id) onFocus(id, { extend: multiSelect && e.shiftKey });
|
|
1015
2458
|
},
|
|
1016
|
-
{ enabled, preventDefault: true, description: "Previous row" }
|
|
2459
|
+
{ enabled, preventDefault: true, description: "Previous row (Shift extends)" }
|
|
1017
2460
|
);
|
|
1018
2461
|
const refHome = useHotkey(
|
|
1019
|
-
"home",
|
|
1020
|
-
() => {
|
|
1021
|
-
const
|
|
1022
|
-
if (
|
|
1023
|
-
onFocus(r[0].node.id);
|
|
2462
|
+
["home", "shift+home"],
|
|
2463
|
+
(e) => {
|
|
2464
|
+
const id = edgeRowId(rowsRef.current, "first");
|
|
2465
|
+
if (id) onFocus(id, { extend: multiSelect && e.shiftKey });
|
|
1024
2466
|
},
|
|
1025
|
-
{ enabled, preventDefault: true, description: "First row" }
|
|
2467
|
+
{ enabled, preventDefault: true, description: "First row (Shift extends)" }
|
|
1026
2468
|
);
|
|
1027
2469
|
const refEnd = useHotkey(
|
|
1028
|
-
"end",
|
|
2470
|
+
["end", "shift+end"],
|
|
2471
|
+
(e) => {
|
|
2472
|
+
const id = edgeRowId(rowsRef.current, "last");
|
|
2473
|
+
if (id) onFocus(id, { extend: multiSelect && e.shiftKey });
|
|
2474
|
+
},
|
|
2475
|
+
{ enabled, preventDefault: true, description: "Last row (Shift extends)" }
|
|
2476
|
+
);
|
|
2477
|
+
const refSelectAll = useHotkey(
|
|
2478
|
+
"mod+a",
|
|
1029
2479
|
() => {
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
onFocus(r[r.length - 1].node.id);
|
|
2480
|
+
if (!multiSelect) return;
|
|
2481
|
+
onSelectAll?.();
|
|
1033
2482
|
},
|
|
1034
|
-
{
|
|
2483
|
+
{
|
|
2484
|
+
enabled: enabled && multiSelect,
|
|
2485
|
+
preventDefault: true,
|
|
2486
|
+
description: "Select all visible rows"
|
|
2487
|
+
}
|
|
1035
2488
|
);
|
|
1036
2489
|
const refRight = useHotkey(
|
|
1037
2490
|
"right",
|
|
1038
2491
|
() => {
|
|
1039
2492
|
const { rows: r, idx, current } = getCurrent();
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
2493
|
+
const out = resolveRightArrow(current, r, idx);
|
|
2494
|
+
switch (out.kind) {
|
|
2495
|
+
case "expand":
|
|
2496
|
+
onExpand(out.id);
|
|
2497
|
+
return;
|
|
2498
|
+
case "focus":
|
|
2499
|
+
onFocus(out.id, { extend: false });
|
|
2500
|
+
return;
|
|
2501
|
+
case "noop":
|
|
2502
|
+
return;
|
|
1046
2503
|
}
|
|
1047
2504
|
},
|
|
1048
2505
|
{ enabled, preventDefault: true, description: "Expand / first child" }
|
|
@@ -1051,11 +2508,16 @@ function useTreeKeyboard({
|
|
|
1051
2508
|
"left",
|
|
1052
2509
|
() => {
|
|
1053
2510
|
const { current } = getCurrent();
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
2511
|
+
const out = resolveLeftArrow(current);
|
|
2512
|
+
switch (out.kind) {
|
|
2513
|
+
case "collapse":
|
|
2514
|
+
onCollapse(out.id);
|
|
2515
|
+
return;
|
|
2516
|
+
case "focus":
|
|
2517
|
+
onFocus(out.id, { extend: false });
|
|
2518
|
+
return;
|
|
2519
|
+
case "noop":
|
|
2520
|
+
return;
|
|
1059
2521
|
}
|
|
1060
2522
|
},
|
|
1061
2523
|
{ enabled, preventDefault: true, description: "Collapse / parent" }
|
|
@@ -1064,13 +2526,14 @@ function useTreeKeyboard({
|
|
|
1064
2526
|
["enter", "space"],
|
|
1065
2527
|
() => {
|
|
1066
2528
|
const { current } = getCurrent();
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
2529
|
+
const out = resolveActivate(current);
|
|
2530
|
+
if (out.kind === "noop") return;
|
|
2531
|
+
onSelect(out.kind === "activate-leaf" ? out.id : out.id);
|
|
2532
|
+
if (out.kind === "toggle-folder") {
|
|
2533
|
+
if (out.willExpand) onExpand(out.id);
|
|
2534
|
+
else onCollapse(out.id);
|
|
1072
2535
|
} else {
|
|
1073
|
-
onActivate(
|
|
2536
|
+
onActivate(out.id);
|
|
1074
2537
|
}
|
|
1075
2538
|
},
|
|
1076
2539
|
{ enabled, preventDefault: true, description: "Activate / toggle" }
|
|
@@ -1090,12 +2553,44 @@ function useTreeKeyboard({
|
|
|
1090
2553
|
refLeft(instance);
|
|
1091
2554
|
refActivate(instance);
|
|
1092
2555
|
refEscape(instance);
|
|
2556
|
+
refSelectAll(instance);
|
|
1093
2557
|
},
|
|
1094
|
-
[
|
|
2558
|
+
[
|
|
2559
|
+
refDown,
|
|
2560
|
+
refUp,
|
|
2561
|
+
refHome,
|
|
2562
|
+
refEnd,
|
|
2563
|
+
refRight,
|
|
2564
|
+
refLeft,
|
|
2565
|
+
refActivate,
|
|
2566
|
+
refEscape,
|
|
2567
|
+
refSelectAll
|
|
2568
|
+
]
|
|
1095
2569
|
);
|
|
1096
2570
|
return { ref };
|
|
1097
2571
|
}
|
|
1098
2572
|
__name(useTreeKeyboard, "useTreeKeyboard");
|
|
2573
|
+
|
|
2574
|
+
// src/tools/data/Tree/hooks/type-ahead/match-prefix.ts
|
|
2575
|
+
function findRowByPrefix(rows, getName, prefix) {
|
|
2576
|
+
if (prefix.length === 0) return void 0;
|
|
2577
|
+
return rows.find((row) => getName(row.node).toLowerCase().startsWith(prefix));
|
|
2578
|
+
}
|
|
2579
|
+
__name(findRowByPrefix, "findRowByPrefix");
|
|
2580
|
+
function isResetKey(key) {
|
|
2581
|
+
return key === "Escape" || key === "Enter" || key === "Tab" || key.startsWith("Arrow") || key === "Home" || key === "End" || key === "PageUp" || key === "PageDown";
|
|
2582
|
+
}
|
|
2583
|
+
__name(isResetKey, "isResetKey");
|
|
2584
|
+
function isTypingTarget(target) {
|
|
2585
|
+
const el = target;
|
|
2586
|
+
if (!el) return false;
|
|
2587
|
+
const tag = el.tagName;
|
|
2588
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") return true;
|
|
2589
|
+
return el.isContentEditable === true;
|
|
2590
|
+
}
|
|
2591
|
+
__name(isTypingTarget, "isTypingTarget");
|
|
2592
|
+
|
|
2593
|
+
// src/tools/data/Tree/hooks/type-ahead/use-tree-type-ahead.ts
|
|
1099
2594
|
var FLUSH_MS = 600;
|
|
1100
2595
|
function useTreeTypeAhead({
|
|
1101
2596
|
rows,
|
|
@@ -1124,11 +2619,9 @@ function useTreeTypeAhead({
|
|
|
1124
2619
|
}
|
|
1125
2620
|
}, "reset");
|
|
1126
2621
|
const handler = /* @__PURE__ */ __name((e) => {
|
|
1127
|
-
|
|
1128
|
-
if (tag === "INPUT" || tag === "TEXTAREA") return;
|
|
1129
|
-
if (e.target?.isContentEditable) return;
|
|
2622
|
+
if (isTypingTarget(e.target)) return;
|
|
1130
2623
|
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
1131
|
-
if (
|
|
2624
|
+
if (isResetKey(e.key)) {
|
|
1132
2625
|
reset();
|
|
1133
2626
|
return;
|
|
1134
2627
|
}
|
|
@@ -1136,9 +2629,10 @@ function useTreeTypeAhead({
|
|
|
1136
2629
|
bufferRef.current += e.key.toLowerCase();
|
|
1137
2630
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
1138
2631
|
timerRef.current = setTimeout(reset, FLUSH_MS);
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1141
|
-
|
|
2632
|
+
const hit = findRowByPrefix(
|
|
2633
|
+
rowsRef.current,
|
|
2634
|
+
getNameRef.current,
|
|
2635
|
+
bufferRef.current
|
|
1142
2636
|
);
|
|
1143
2637
|
if (hit) {
|
|
1144
2638
|
e.preventDefault();
|
|
@@ -1153,36 +2647,166 @@ function useTreeTypeAhead({
|
|
|
1153
2647
|
}, [containerRef, enabled]);
|
|
1154
2648
|
}
|
|
1155
2649
|
__name(useTreeTypeAhead, "useTreeTypeAhead");
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
Icon ? /* @__PURE__ */ jsx(Icon, {}) : null,
|
|
1175
|
-
item.label,
|
|
1176
|
-
item.shortcut ? /* @__PURE__ */ jsx(ContextMenuShortcut, { children: item.shortcut }) : null
|
|
1177
|
-
]
|
|
1178
|
-
},
|
|
1179
|
-
item.id
|
|
1180
|
-
);
|
|
1181
|
-
}) })
|
|
1182
|
-
] });
|
|
2650
|
+
|
|
2651
|
+
// src/tools/data/Tree/hooks/finder-hotkeys/build-ctx.ts
|
|
2652
|
+
function buildBuiltinCtx(input) {
|
|
2653
|
+
if (!input.adapter) return null;
|
|
2654
|
+
const selectedNodes = [];
|
|
2655
|
+
for (const id of input.selected) {
|
|
2656
|
+
const n = input.getNodeById(id);
|
|
2657
|
+
if (n) selectedNodes.push(n);
|
|
2658
|
+
}
|
|
2659
|
+
const targetNode = input.focused ? input.getNodeById(input.focused) ?? null : null;
|
|
2660
|
+
return {
|
|
2661
|
+
adapter: input.adapter,
|
|
2662
|
+
labels: input.labels,
|
|
2663
|
+
selectedNodes,
|
|
2664
|
+
targetNode,
|
|
2665
|
+
getName: input.getItemName,
|
|
2666
|
+
startInlineRename: input.startInlineRename,
|
|
2667
|
+
clipboard: input.clipboard
|
|
1183
2668
|
};
|
|
1184
2669
|
}
|
|
1185
|
-
__name(
|
|
2670
|
+
__name(buildBuiltinCtx, "buildBuiltinCtx");
|
|
2671
|
+
|
|
2672
|
+
// src/tools/data/Tree/hooks/finder-hotkeys/use-tree-finder-hotkeys.ts
|
|
2673
|
+
function useTreeFinderHotkeys(opts) {
|
|
2674
|
+
const optsRef = useRef(opts);
|
|
2675
|
+
optsRef.current = opts;
|
|
2676
|
+
const run = useCallback(async (action) => {
|
|
2677
|
+
const o = optsRef.current;
|
|
2678
|
+
if (o.paused) return;
|
|
2679
|
+
const ctx = buildBuiltinCtx({
|
|
2680
|
+
adapter: o.adapter,
|
|
2681
|
+
labels: o.labels,
|
|
2682
|
+
selected: o.selected,
|
|
2683
|
+
focused: o.focused,
|
|
2684
|
+
getNodeById: o.getNodeById,
|
|
2685
|
+
getItemName: o.getItemName,
|
|
2686
|
+
startInlineRename: o.startInlineRename,
|
|
2687
|
+
clipboard: o.clipboard
|
|
2688
|
+
});
|
|
2689
|
+
if (!ctx) return;
|
|
2690
|
+
await runBuiltinAction(action, ctx);
|
|
2691
|
+
}, []);
|
|
2692
|
+
const refDelete = useHotkey(
|
|
2693
|
+
["mod+backspace", "delete"],
|
|
2694
|
+
() => void run("delete"),
|
|
2695
|
+
{
|
|
2696
|
+
enabled: opts.enabled,
|
|
2697
|
+
preventDefault: true,
|
|
2698
|
+
description: "Delete selected items",
|
|
2699
|
+
scope: "tree"
|
|
2700
|
+
}
|
|
2701
|
+
);
|
|
2702
|
+
const refRename = useHotkey("f2", () => void run("rename"), {
|
|
2703
|
+
enabled: opts.enabled,
|
|
2704
|
+
preventDefault: true,
|
|
2705
|
+
description: "Rename selected item",
|
|
2706
|
+
scope: "tree"
|
|
2707
|
+
});
|
|
2708
|
+
const refDuplicate = useHotkey("mod+d", () => void run("duplicate"), {
|
|
2709
|
+
enabled: opts.enabled,
|
|
2710
|
+
preventDefault: true,
|
|
2711
|
+
description: "Duplicate selected items",
|
|
2712
|
+
scope: "tree"
|
|
2713
|
+
});
|
|
2714
|
+
const refNewFolder = useHotkey("mod+shift+n", () => void run("new-folder"), {
|
|
2715
|
+
enabled: opts.enabled,
|
|
2716
|
+
preventDefault: true,
|
|
2717
|
+
description: "New folder",
|
|
2718
|
+
scope: "tree"
|
|
2719
|
+
});
|
|
2720
|
+
const refNewFile = useHotkey("mod+n", () => void run("new-file"), {
|
|
2721
|
+
enabled: opts.enabled,
|
|
2722
|
+
preventDefault: true,
|
|
2723
|
+
description: "New file",
|
|
2724
|
+
scope: "tree"
|
|
2725
|
+
});
|
|
2726
|
+
const refCut = useHotkey("mod+x", () => void run("cut"), {
|
|
2727
|
+
enabled: opts.enabled,
|
|
2728
|
+
preventDefault: true,
|
|
2729
|
+
description: "Cut",
|
|
2730
|
+
scope: "tree"
|
|
2731
|
+
});
|
|
2732
|
+
const refCopy = useHotkey("mod+c", () => void run("copy"), {
|
|
2733
|
+
enabled: opts.enabled,
|
|
2734
|
+
preventDefault: true,
|
|
2735
|
+
description: "Copy",
|
|
2736
|
+
scope: "tree"
|
|
2737
|
+
});
|
|
2738
|
+
const refPaste = useHotkey("mod+v", () => void run("paste"), {
|
|
2739
|
+
enabled: opts.enabled,
|
|
2740
|
+
preventDefault: true,
|
|
2741
|
+
description: "Paste",
|
|
2742
|
+
scope: "tree"
|
|
2743
|
+
});
|
|
2744
|
+
const ref = useCallback(
|
|
2745
|
+
(instance) => {
|
|
2746
|
+
refDelete(instance);
|
|
2747
|
+
refRename(instance);
|
|
2748
|
+
refDuplicate(instance);
|
|
2749
|
+
refNewFolder(instance);
|
|
2750
|
+
refNewFile(instance);
|
|
2751
|
+
refCut(instance);
|
|
2752
|
+
refCopy(instance);
|
|
2753
|
+
refPaste(instance);
|
|
2754
|
+
},
|
|
2755
|
+
[
|
|
2756
|
+
refDelete,
|
|
2757
|
+
refRename,
|
|
2758
|
+
refDuplicate,
|
|
2759
|
+
refNewFolder,
|
|
2760
|
+
refNewFile,
|
|
2761
|
+
refCut,
|
|
2762
|
+
refCopy,
|
|
2763
|
+
refPaste
|
|
2764
|
+
]
|
|
2765
|
+
);
|
|
2766
|
+
return { ref };
|
|
2767
|
+
}
|
|
2768
|
+
__name(useTreeFinderHotkeys, "useTreeFinderHotkeys");
|
|
2769
|
+
function renderItemsAsContextMenu(rowProps, items, trigger) {
|
|
2770
|
+
return /* @__PURE__ */ jsxs(ContextMenu, { children: [
|
|
2771
|
+
/* @__PURE__ */ jsx(ContextMenuTrigger, { asChild: true, children: trigger }),
|
|
2772
|
+
/* @__PURE__ */ jsx(ContextMenuContent, { children: items.map((item, idx) => {
|
|
2773
|
+
if (item === "separator") {
|
|
2774
|
+
return /* @__PURE__ */ jsx(ContextMenuSeparator, {}, `sep-${idx}`);
|
|
2775
|
+
}
|
|
2776
|
+
const Icon = item.icon;
|
|
2777
|
+
return /* @__PURE__ */ jsxs(
|
|
2778
|
+
ContextMenuItem,
|
|
2779
|
+
{
|
|
2780
|
+
disabled: item.disabled,
|
|
2781
|
+
variant: item.destructive ? "destructive" : void 0,
|
|
2782
|
+
onSelect: () => item.onSelect(rowProps),
|
|
2783
|
+
children: [
|
|
2784
|
+
Icon ? /* @__PURE__ */ jsx(Icon, {}) : null,
|
|
2785
|
+
item.label,
|
|
2786
|
+
item.shortcut ? /* @__PURE__ */ jsx(ContextMenuShortcut, { children: item.shortcut }) : null
|
|
2787
|
+
]
|
|
2788
|
+
},
|
|
2789
|
+
item.id
|
|
2790
|
+
);
|
|
2791
|
+
}) })
|
|
2792
|
+
] });
|
|
2793
|
+
}
|
|
2794
|
+
__name(renderItemsAsContextMenu, "renderItemsAsContextMenu");
|
|
2795
|
+
function tidyMenuItems(items) {
|
|
2796
|
+
const out = [];
|
|
2797
|
+
for (const it of items) {
|
|
2798
|
+
if (it === "separator") {
|
|
2799
|
+
if (out.length === 0) continue;
|
|
2800
|
+
if (out[out.length - 1] === "separator") continue;
|
|
2801
|
+
out.push(it);
|
|
2802
|
+
} else {
|
|
2803
|
+
out.push(it);
|
|
2804
|
+
}
|
|
2805
|
+
}
|
|
2806
|
+
while (out.length > 0 && out[out.length - 1] === "separator") out.pop();
|
|
2807
|
+
return out;
|
|
2808
|
+
}
|
|
2809
|
+
__name(tidyMenuItems, "tidyMenuItems");
|
|
1186
2810
|
function TreeRoot(props) {
|
|
1187
2811
|
const {
|
|
1188
2812
|
data,
|
|
@@ -1201,6 +2825,10 @@ function TreeRoot(props) {
|
|
|
1201
2825
|
enableSearch = false,
|
|
1202
2826
|
enableTypeAhead = true,
|
|
1203
2827
|
showIndentGuides = false,
|
|
2828
|
+
enableInlineRename = false,
|
|
2829
|
+
enableFinderHotkeys = false,
|
|
2830
|
+
enableDnD = false,
|
|
2831
|
+
canDrop,
|
|
1204
2832
|
renderRow,
|
|
1205
2833
|
renderIcon,
|
|
1206
2834
|
renderLabel,
|
|
@@ -1210,14 +2838,11 @@ function TreeRoot(props) {
|
|
|
1210
2838
|
labels,
|
|
1211
2839
|
persistKey,
|
|
1212
2840
|
persistSelection = false,
|
|
2841
|
+
adapter,
|
|
2842
|
+
defaultMenuItems,
|
|
1213
2843
|
className,
|
|
1214
2844
|
style
|
|
1215
2845
|
} = props;
|
|
1216
|
-
const resolvedRenderContextMenu = useMemo(() => {
|
|
1217
|
-
if (renderContextMenu) return renderContextMenu;
|
|
1218
|
-
if (contextMenuActions) return actionsToRenderContextMenu(contextMenuActions);
|
|
1219
|
-
return void 0;
|
|
1220
|
-
}, [renderContextMenu, contextMenuActions]);
|
|
1221
2846
|
return /* @__PURE__ */ jsx(
|
|
1222
2847
|
TreeProvider,
|
|
1223
2848
|
{
|
|
@@ -1239,7 +2864,13 @@ function TreeRoot(props) {
|
|
|
1239
2864
|
renderIcon,
|
|
1240
2865
|
renderLabel,
|
|
1241
2866
|
renderActions,
|
|
1242
|
-
renderContextMenu
|
|
2867
|
+
renderContextMenu,
|
|
2868
|
+
contextMenuActions,
|
|
2869
|
+
adapter,
|
|
2870
|
+
defaultMenuItems,
|
|
2871
|
+
enableInlineRename,
|
|
2872
|
+
enableDnD,
|
|
2873
|
+
canDrop,
|
|
1243
2874
|
labels,
|
|
1244
2875
|
persistKey,
|
|
1245
2876
|
persistSelection,
|
|
@@ -1250,6 +2881,7 @@ function TreeRoot(props) {
|
|
|
1250
2881
|
style,
|
|
1251
2882
|
enableSearch,
|
|
1252
2883
|
enableTypeAhead,
|
|
2884
|
+
enableFinderHotkeys,
|
|
1253
2885
|
renderRow
|
|
1254
2886
|
}
|
|
1255
2887
|
)
|
|
@@ -1262,14 +2894,27 @@ function TreeRootShell({
|
|
|
1262
2894
|
style,
|
|
1263
2895
|
enableSearch,
|
|
1264
2896
|
enableTypeAhead,
|
|
2897
|
+
enableFinderHotkeys,
|
|
1265
2898
|
renderRow
|
|
1266
2899
|
}) {
|
|
1267
2900
|
const containerRef = useRef(null);
|
|
1268
2901
|
const ctx = useTreeContext();
|
|
2902
|
+
const isMulti = ctx.selectionMode === "multiple";
|
|
1269
2903
|
const { ref: keyboardRef } = useTreeKeyboard({
|
|
1270
2904
|
rows: ctx.flatRows,
|
|
1271
2905
|
focusedId: ctx.focused,
|
|
1272
|
-
|
|
2906
|
+
multiSelect: isMulti,
|
|
2907
|
+
// Pause container hotkeys while inline rename is active so the
|
|
2908
|
+
// user can type freely (TreeRenameInput stops bubbling already, but
|
|
2909
|
+
// gating here is the cleaner second line of defence).
|
|
2910
|
+
enabled: ctx.renamingId === null,
|
|
2911
|
+
onFocus: /* @__PURE__ */ __name((id, { extend }) => {
|
|
2912
|
+
if (extend && isMulti) {
|
|
2913
|
+
ctx.moveSelect(id, { extend: true });
|
|
2914
|
+
} else {
|
|
2915
|
+
ctx.setFocus(id);
|
|
2916
|
+
}
|
|
2917
|
+
}, "onFocus"),
|
|
1273
2918
|
onSelect: ctx.select,
|
|
1274
2919
|
onActivate: /* @__PURE__ */ __name((id) => {
|
|
1275
2920
|
const row = ctx.flatRows.find((r) => r.node.id === id);
|
|
@@ -1277,14 +2922,37 @@ function TreeRootShell({
|
|
|
1277
2922
|
}, "onActivate"),
|
|
1278
2923
|
onExpand: ctx.expand,
|
|
1279
2924
|
onCollapse: ctx.collapse,
|
|
1280
|
-
onClearSelection: ctx.clearSelection
|
|
2925
|
+
onClearSelection: ctx.clearSelection,
|
|
2926
|
+
onSelectAll: ctx.selectAll
|
|
2927
|
+
});
|
|
2928
|
+
const { ref: finderHotkeysRef } = useTreeFinderHotkeys({
|
|
2929
|
+
enabled: enableFinderHotkeys,
|
|
2930
|
+
paused: ctx.renamingId !== null,
|
|
2931
|
+
adapter: ctx.adapter,
|
|
2932
|
+
labels: ctx.labels,
|
|
2933
|
+
selected: ctx.selected,
|
|
2934
|
+
focused: ctx.focused,
|
|
2935
|
+
getNodeById: ctx.getNodeById,
|
|
2936
|
+
getItemName: ctx.getItemName,
|
|
2937
|
+
startInlineRename: ctx.inlineRenameEnabled ? ctx.startRename : void 0,
|
|
2938
|
+
clipboard: {
|
|
2939
|
+
hasItems: !!ctx.clipboard && ctx.clipboard.ids.length > 0,
|
|
2940
|
+
cut: ctx.cutToClipboard,
|
|
2941
|
+
copy: ctx.copyToClipboard,
|
|
2942
|
+
// Hotkey paste targets the currently focused row (or null = root).
|
|
2943
|
+
paste: /* @__PURE__ */ __name(() => {
|
|
2944
|
+
const target = ctx.focused ? ctx.getNodeById(ctx.focused) ?? null : null;
|
|
2945
|
+
return ctx.pasteFromClipboard(target, "inside");
|
|
2946
|
+
}, "paste")
|
|
2947
|
+
}
|
|
1281
2948
|
});
|
|
1282
2949
|
const setContainerRef = useCallback(
|
|
1283
2950
|
(instance) => {
|
|
1284
2951
|
containerRef.current = instance;
|
|
1285
2952
|
keyboardRef(instance);
|
|
2953
|
+
finderHotkeysRef(instance);
|
|
1286
2954
|
},
|
|
1287
|
-
[keyboardRef]
|
|
2955
|
+
[keyboardRef, finderHotkeysRef]
|
|
1288
2956
|
);
|
|
1289
2957
|
const focusedId = ctx.focused;
|
|
1290
2958
|
useEffect(() => {
|
|
@@ -1307,7 +2975,22 @@ function TreeRootShell({
|
|
|
1307
2975
|
onMatch: onTypeAheadMatch,
|
|
1308
2976
|
enabled: enableTypeAhead
|
|
1309
2977
|
});
|
|
1310
|
-
|
|
2978
|
+
const finalRenderContextMenu = useMemo(() => {
|
|
2979
|
+
if (ctx.renderContextMenu) return ctx.renderContextMenu;
|
|
2980
|
+
const resolve = ctx.resolvedContextMenuActions;
|
|
2981
|
+
if (!resolve) return void 0;
|
|
2982
|
+
return (rowProps, trigger) => {
|
|
2983
|
+
const items = resolve(rowProps);
|
|
2984
|
+
const cleaned = items ? tidyMenuItems(items) : null;
|
|
2985
|
+
if (!cleaned || cleaned.length === 0) return trigger;
|
|
2986
|
+
return renderItemsAsContextMenu(rowProps, cleaned, trigger);
|
|
2987
|
+
};
|
|
2988
|
+
}, [ctx.renderContextMenu, ctx.resolvedContextMenuActions]);
|
|
2989
|
+
const childCtx = useMemo(
|
|
2990
|
+
() => ({ ...ctx, renderContextMenu: finalRenderContextMenu }),
|
|
2991
|
+
[ctx, finalRenderContextMenu]
|
|
2992
|
+
);
|
|
2993
|
+
const treeBody = /* @__PURE__ */ jsxs(
|
|
1311
2994
|
"div",
|
|
1312
2995
|
{
|
|
1313
2996
|
ref: setContainerRef,
|
|
@@ -1325,13 +3008,35 @@ function TreeRootShell({
|
|
|
1325
3008
|
"data-tree-root": "",
|
|
1326
3009
|
children: [
|
|
1327
3010
|
enableSearch ? /* @__PURE__ */ jsx(TreeSearchInput, { className: "mx-2 mt-2" }) : null,
|
|
1328
|
-
/* @__PURE__ */
|
|
3011
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col overflow-auto px-1", children: [
|
|
3012
|
+
/* @__PURE__ */ jsx(TreeContent, { role: "group", children: renderRow }),
|
|
3013
|
+
/* @__PURE__ */ jsx(TreeEmptyArea, {})
|
|
3014
|
+
] })
|
|
1329
3015
|
]
|
|
1330
3016
|
}
|
|
1331
3017
|
);
|
|
3018
|
+
const body = finalRenderContextMenu === ctx.renderContextMenu ? treeBody : /* @__PURE__ */ jsx(TreeContext.Provider, { value: childCtx, children: treeBody });
|
|
3019
|
+
return /* @__PURE__ */ jsx(TreeDndProvider, { children: body });
|
|
1332
3020
|
}
|
|
1333
3021
|
__name(TreeRootShell, "TreeRootShell");
|
|
1334
3022
|
var TreeRoot_default = TreeRoot;
|
|
3023
|
+
function FinderTree(props) {
|
|
3024
|
+
return /* @__PURE__ */ jsx(
|
|
3025
|
+
TreeRoot,
|
|
3026
|
+
{
|
|
3027
|
+
selectionMode: "multiple",
|
|
3028
|
+
activationMode: "double-click",
|
|
3029
|
+
enableInlineRename: true,
|
|
3030
|
+
enableFinderHotkeys: true,
|
|
3031
|
+
enableDnD: true,
|
|
3032
|
+
enableTypeAhead: true,
|
|
3033
|
+
showIndentGuides: true,
|
|
3034
|
+
appearance: { density: "cozy" },
|
|
3035
|
+
...props
|
|
3036
|
+
}
|
|
3037
|
+
);
|
|
3038
|
+
}
|
|
3039
|
+
__name(FinderTree, "FinderTree");
|
|
1335
3040
|
function TreeSkeleton({ rows = 6, className }) {
|
|
1336
3041
|
return /* @__PURE__ */ jsx("div", { className: cn("flex flex-col gap-1 p-2", className), "aria-hidden": true, children: Array.from({ length: rows }).map((_, i) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", style: { paddingLeft: i % 3 * 16 }, children: [
|
|
1337
3042
|
/* @__PURE__ */ jsx("span", { className: "size-4 shrink-0 animate-pulse rounded bg-muted" }),
|
|
@@ -1383,6 +3088,6 @@ function createDemoTree({
|
|
|
1383
3088
|
}
|
|
1384
3089
|
__name(createDemoTree, "createDemoTree");
|
|
1385
3090
|
|
|
1386
|
-
export { DEFAULT_TREE_APPEARANCE, DEFAULT_TREE_LABELS, TreeRoot as Tree, TreeChevron, TreeContent, TreeEmpty, TreeError, TreeIcon, TreeIndentGuides, TreeLabel, TreeProvider, TreeRoot, TreeRow, TreeSearchInput, TreeSkeleton, appearanceToStyle, clearTreeState, createChildCache, createDemoTree, TreeRoot_default as default, flattenTree, loadTreeState, resolveAppearance, resolveChildren, saveTreeState, useTreeActions, useTreeContext, useTreeExpansion, useTreeFocus, useTreeKeyboard, useTreeLabels, useTreeRows, useTreeSearch, useTreeSelection, useTreeTypeAhead };
|
|
3091
|
+
export { DEFAULT_TREE_APPEARANCE, DEFAULT_TREE_LABELS, FinderTree, TREE_DND_MIME, TreeRoot as Tree, TreeChevron, TreeContent, TreeDropIndicator, TreeEmpty, TreeEmptyArea, TreeError, TreeIcon, TreeIndentGuides, TreeLabel, TreeProvider, TreeRenameInput, TreeRoot, TreeRow, TreeSearchInput, TreeSkeleton, appearanceToStyle, autoSelectRange, clearTreeState, createChildCache, createDemoTree, TreeRoot_default as default, defaultCanDrop, flattenTree, loadTreeState, resolveAppearance, resolveChildren, resolveDropZone, saveTreeState, splitFileName, useTreeActions, useTreeClipboard, useTreeContext, useTreeDnd, useTreeExpansion, useTreeFinderHotkeys, useTreeFocus, useTreeKeyboard, useTreeLabels, useTreeRename, useTreeRows, useTreeSearch, useTreeSelection, useTreeTypeAhead };
|
|
1387
3092
|
//# sourceMappingURL=index.mjs.map
|
|
1388
3093
|
//# sourceMappingURL=index.mjs.map
|