@warkypublic/svelix 0.1.47 → 0.2.2
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/components/Boxer/Boxer.svelte +16 -1
- package/dist/components/Boxer/types.d.ts +2 -0
- package/dist/components/CardGrid/CardGrid.svelte +50 -4
- package/dist/components/CardGrid/CardGrid.svelte.d.ts +4 -0
- package/dist/components/CardGrid/DefaultCard.svelte +7 -2
- package/dist/components/CardGrid/DefaultCard.svelte.d.ts +1 -0
- package/dist/components/CardGrid/cardGrid.d.ts +24 -0
- package/dist/components/CardGrid/cardGrid.js +20 -0
- package/dist/components/Former/FormerDrawer.svelte +10 -22
- package/dist/components/Former/FormerModal.svelte +10 -22
- package/dist/components/Former/formerOverlayHelpers.svelte.d.ts +11 -0
- package/dist/components/Former/formerOverlayHelpers.svelte.js +38 -0
- package/dist/components/Former/index.d.ts +1 -0
- package/dist/components/Former/index.js +1 -0
- package/dist/components/FormerControllers/ButtonCtrl.svelte +11 -16
- package/dist/components/FormerControllers/CtrlFieldShell.svelte +15 -0
- package/dist/components/FormerControllers/CtrlFieldShell.svelte.d.ts +12 -0
- package/dist/components/FormerControllers/DateTimeCtrl/DateTimeCtrl.svelte +6 -13
- package/dist/components/FormerControllers/IconButtonCtrl.svelte +11 -16
- package/dist/components/FormerControllers/NativeSelectCtrl.svelte +7 -13
- package/dist/components/FormerControllers/NumberInputCtrl.svelte +18 -14
- package/dist/components/FormerControllers/PasswordInputCtrl.svelte +7 -13
- package/dist/components/FormerControllers/SwitchCtrl.svelte +4 -3
- package/dist/components/FormerControllers/TextAreaCtrl.svelte +7 -13
- package/dist/components/FormerControllers/TextInputCtrl.svelte +7 -13
- package/dist/components/FormerControllers/ctrlLoading.svelte.d.ts +8 -0
- package/dist/components/FormerControllers/ctrlLoading.svelte.js +43 -0
- package/dist/components/FormerControllers/index.d.ts +2 -0
- package/dist/components/FormerControllers/index.js +2 -0
- package/dist/components/FormerControllers/utils.d.ts +1 -0
- package/dist/components/FormerControllers/utils.js +3 -0
- package/dist/components/Gridler/components/Gridler.svelte +10 -4
- package/dist/components/Gridler/components/GridlerFull.svelte +2 -1
- package/dist/components/Gridler/utils/serverPaging.d.ts +1 -0
- package/dist/components/Gridler/utils/serverPaging.js +7 -0
- package/dist/components/VTree/VTree.svelte +107 -2
- package/dist/components/VTree/VTree.utils.d.ts +23 -0
- package/dist/components/VTree/VTree.utils.js +73 -0
- package/dist/components/VTree/VTreeControlledSelectionDemo.svelte +20 -0
- package/dist/components/VTree/VTreeControlledSelectionDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeCustomizationDemo.svelte +23 -0
- package/dist/components/VTree/VTreeCustomizationDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeInteractionDemo.svelte +29 -0
- package/dist/components/VTree/VTreeInteractionDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeMultiSelectDemo.svelte +27 -0
- package/dist/components/VTree/VTreeMultiSelectDemo.svelte.d.ts +18 -0
- package/dist/components/VTree/VTreeRow.svelte +71 -57
- package/dist/components/VTree/VTreeRow.svelte.d.ts +34 -18
- package/dist/components/VTree/VTreeVirtualViewport.svelte +20 -40
- package/dist/components/VTree/VTreeVirtualViewport.svelte.d.ts +35 -18
- package/dist/components/VTree/types.d.ts +39 -1
- package/llm/COMPONENT_GUIDE.md +25 -0
- package/package.json +45 -45
|
@@ -1,25 +1,32 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
1
|
+
<script lang="ts">import CtrlFieldShell from "./CtrlFieldShell.svelte";
|
|
2
|
+
import { makeFieldId } from "./utils";
|
|
3
|
+
let { disabled, error, label, max, min, name, onchange, placeholder, required, sid, step, tooltip, value = $bindable(undefined) } = $props();
|
|
4
|
+
const id = $derived(makeFieldId(name, sid));
|
|
3
5
|
function handleInput(e) {
|
|
4
6
|
const target = e.target;
|
|
5
|
-
const
|
|
7
|
+
const raw = target.value;
|
|
8
|
+
if (raw === "") {
|
|
9
|
+
value = undefined;
|
|
10
|
+
onchange?.(undefined);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const parsed = Number(raw);
|
|
14
|
+
// Intermediate typed states (e.g. "-", "1e") parse to NaN; ignore until valid.
|
|
15
|
+
if (Number.isNaN(parsed)) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
6
18
|
value = parsed;
|
|
7
19
|
onchange?.(parsed);
|
|
8
20
|
}
|
|
9
|
-
export {};
|
|
10
21
|
</script>
|
|
11
22
|
|
|
12
|
-
<
|
|
13
|
-
{#if label}
|
|
14
|
-
<label class="label text-sm font-medium" for={id}>
|
|
15
|
-
{label}{#if required}<span class="text-error-500">*</span>{/if}
|
|
16
|
-
</label>
|
|
17
|
-
{/if}
|
|
23
|
+
<CtrlFieldShell {error} {id} {label} {required} {tooltip}>
|
|
18
24
|
<input
|
|
19
25
|
{id}
|
|
20
26
|
{disabled}
|
|
21
27
|
{max}
|
|
22
28
|
{min}
|
|
29
|
+
{name}
|
|
23
30
|
{placeholder}
|
|
24
31
|
{required}
|
|
25
32
|
{step}
|
|
@@ -29,7 +36,4 @@ export {};
|
|
|
29
36
|
value={value ?? ''}
|
|
30
37
|
oninput={handleInput}
|
|
31
38
|
/>
|
|
32
|
-
|
|
33
|
-
<p class="text-error-500 text-xs">{error}</p>
|
|
34
|
-
{/if}
|
|
35
|
-
</div>
|
|
39
|
+
</CtrlFieldShell>
|
|
@@ -1,19 +1,16 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
1
|
+
<script lang="ts">import CtrlFieldShell from "./CtrlFieldShell.svelte";
|
|
2
|
+
import { makeFieldId } from "./utils";
|
|
3
|
+
let { disabled, error, label, name, onchange, placeholder, required, sid, tooltip, value = $bindable("") } = $props();
|
|
4
|
+
const id = $derived(makeFieldId(name, sid));
|
|
3
5
|
let showPassword = $state(false);
|
|
4
|
-
export {};
|
|
5
6
|
</script>
|
|
6
7
|
|
|
7
|
-
<
|
|
8
|
-
{#if label}
|
|
9
|
-
<label class="label text-sm font-medium" for={id}>
|
|
10
|
-
{label}{#if required}<span class="text-error-500">*</span>{/if}
|
|
11
|
-
</label>
|
|
12
|
-
{/if}
|
|
8
|
+
<CtrlFieldShell {error} {id} {label} {required} {tooltip}>
|
|
13
9
|
<div class="flex gap-0">
|
|
14
10
|
<input
|
|
15
11
|
{id}
|
|
16
12
|
{disabled}
|
|
13
|
+
{name}
|
|
17
14
|
{placeholder}
|
|
18
15
|
{required}
|
|
19
16
|
bind:value
|
|
@@ -32,7 +29,4 @@ export {};
|
|
|
32
29
|
{showPassword ? '🙈' : '👁'}
|
|
33
30
|
</button>
|
|
34
31
|
</div>
|
|
35
|
-
|
|
36
|
-
<p class="text-error-500 text-xs">{error}</p>
|
|
37
|
-
{/if}
|
|
38
|
-
</div>
|
|
32
|
+
</CtrlFieldShell>
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<script lang="ts">import { makeFieldId } from "./utils";
|
|
2
|
+
let { checked = $bindable(false), disabled, error, label, name, onchange, sid, tooltip } = $props();
|
|
3
|
+
const id = $derived(makeFieldId(name, sid));
|
|
4
4
|
</script>
|
|
5
5
|
|
|
6
6
|
<div class="flex items-center gap-2" title={tooltip}>
|
|
7
7
|
<input
|
|
8
8
|
{id}
|
|
9
9
|
{disabled}
|
|
10
|
+
{name}
|
|
10
11
|
bind:checked
|
|
11
12
|
class="checkbox"
|
|
12
13
|
type="checkbox"
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<script lang="ts">import CtrlFieldShell from "./CtrlFieldShell.svelte";
|
|
2
|
+
import { makeFieldId } from "./utils";
|
|
3
|
+
let { disabled, error, label, name, onchange, placeholder, required, rows = 4, sid, tooltip, value = $bindable("") } = $props();
|
|
4
|
+
const id = $derived(makeFieldId(name, sid));
|
|
4
5
|
</script>
|
|
5
6
|
|
|
6
|
-
<
|
|
7
|
-
{#if label}
|
|
8
|
-
<label class="label text-sm font-medium" for={id}>
|
|
9
|
-
{label}{#if required}<span class="text-error-500">*</span>{/if}
|
|
10
|
-
</label>
|
|
11
|
-
{/if}
|
|
7
|
+
<CtrlFieldShell {error} {id} {label} {required} {tooltip}>
|
|
12
8
|
<textarea
|
|
13
9
|
{id}
|
|
14
10
|
{disabled}
|
|
11
|
+
{name}
|
|
15
12
|
{placeholder}
|
|
16
13
|
{required}
|
|
17
14
|
{rows}
|
|
@@ -20,7 +17,4 @@ export {};
|
|
|
20
17
|
class:textarea-error={!!error}
|
|
21
18
|
oninput={() => onchange?.(value)}
|
|
22
19
|
></textarea>
|
|
23
|
-
|
|
24
|
-
<p class="text-error-500 text-xs">{error}</p>
|
|
25
|
-
{/if}
|
|
26
|
-
</div>
|
|
20
|
+
</CtrlFieldShell>
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
<script lang="ts">import CtrlFieldShell from "./CtrlFieldShell.svelte";
|
|
2
|
+
import { makeFieldId } from "./utils";
|
|
3
|
+
let { disabled, error, label, name, onchange, placeholder, required, sid, tooltip, type = "text", value = $bindable("") } = $props();
|
|
4
|
+
const id = $derived(makeFieldId(name, sid));
|
|
4
5
|
</script>
|
|
5
6
|
|
|
6
|
-
<
|
|
7
|
-
{#if label}
|
|
8
|
-
<label class="label text-sm font-medium" for={id}>
|
|
9
|
-
{label}{#if required}<span class="text-error-500">*</span>{/if}
|
|
10
|
-
</label>
|
|
11
|
-
{/if}
|
|
7
|
+
<CtrlFieldShell {error} {id} {label} {required} {tooltip}>
|
|
12
8
|
<input
|
|
13
9
|
{id}
|
|
14
10
|
{disabled}
|
|
11
|
+
{name}
|
|
15
12
|
{placeholder}
|
|
16
13
|
{required}
|
|
17
14
|
{type}
|
|
@@ -20,7 +17,4 @@ export {};
|
|
|
20
17
|
class:input-error={!!error}
|
|
21
18
|
oninput={() => onchange?.(value)}
|
|
22
19
|
/>
|
|
23
|
-
|
|
24
|
-
<p class="text-error-500 text-xs">{error}</p>
|
|
25
|
-
{/if}
|
|
26
|
-
</div>
|
|
20
|
+
</CtrlFieldShell>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare function createClickLoading(getExternalLoading: () => boolean | undefined): {
|
|
2
|
+
readonly isLoading: boolean;
|
|
3
|
+
run: (action?: () => Promise<void>) => Promise<void>;
|
|
4
|
+
};
|
|
5
|
+
export declare function createClickPulse(durationMs?: number): {
|
|
6
|
+
readonly pressed: boolean;
|
|
7
|
+
trigger: () => void;
|
|
8
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export function createClickLoading(getExternalLoading) {
|
|
2
|
+
let internalLoading = $state(false);
|
|
3
|
+
const isLoading = $derived(internalLoading || (getExternalLoading() ?? false));
|
|
4
|
+
async function run(action) {
|
|
5
|
+
if (!action || isLoading)
|
|
6
|
+
return;
|
|
7
|
+
internalLoading = true;
|
|
8
|
+
try {
|
|
9
|
+
await action();
|
|
10
|
+
}
|
|
11
|
+
finally {
|
|
12
|
+
internalLoading = false;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
get isLoading() {
|
|
17
|
+
return isLoading;
|
|
18
|
+
},
|
|
19
|
+
run,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
// Guarantees a visible "click" pulse even when the action resolves faster than
|
|
23
|
+
// a human can perceive the loading spinner — decoupled from actual duration.
|
|
24
|
+
export function createClickPulse(durationMs = 200) {
|
|
25
|
+
let pressed = $state(false);
|
|
26
|
+
let timeout;
|
|
27
|
+
function trigger() {
|
|
28
|
+
pressed = true;
|
|
29
|
+
clearTimeout(timeout);
|
|
30
|
+
timeout = setTimeout(() => {
|
|
31
|
+
pressed = false;
|
|
32
|
+
}, durationMs);
|
|
33
|
+
}
|
|
34
|
+
$effect(() => {
|
|
35
|
+
return () => clearTimeout(timeout);
|
|
36
|
+
});
|
|
37
|
+
return {
|
|
38
|
+
get pressed() {
|
|
39
|
+
return pressed;
|
|
40
|
+
},
|
|
41
|
+
trigger,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as ButtonCtrl } from './ButtonCtrl.svelte';
|
|
2
|
+
export { default as CtrlFieldShell } from './CtrlFieldShell.svelte';
|
|
2
3
|
export { DateTimeCtrl } from './DateTimeCtrl/index';
|
|
3
4
|
export { default as IconButtonCtrl } from './IconButtonCtrl.svelte';
|
|
4
5
|
export { default as InlineWrapper } from './InlineWrapper.svelte';
|
|
@@ -8,5 +9,6 @@ export { default as PasswordInputCtrl } from './PasswordInputCtrl.svelte';
|
|
|
8
9
|
export { default as SwitchCtrl } from './SwitchCtrl.svelte';
|
|
9
10
|
export { default as TextAreaCtrl } from './TextAreaCtrl.svelte';
|
|
10
11
|
export { default as TextInputCtrl } from './TextInputCtrl.svelte';
|
|
12
|
+
export { makeFieldId } from './utils';
|
|
11
13
|
export type { DateTimeConstraints, DateTimeMode, DateTimeParts } from './DateTimeCtrl/DateTimeCtrl.utils';
|
|
12
14
|
export * from './types';
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { default as ButtonCtrl } from './ButtonCtrl.svelte';
|
|
2
|
+
export { default as CtrlFieldShell } from './CtrlFieldShell.svelte';
|
|
2
3
|
export { DateTimeCtrl } from './DateTimeCtrl/index';
|
|
3
4
|
export { default as IconButtonCtrl } from './IconButtonCtrl.svelte';
|
|
4
5
|
export { default as InlineWrapper } from './InlineWrapper.svelte';
|
|
@@ -8,4 +9,5 @@ export { default as PasswordInputCtrl } from './PasswordInputCtrl.svelte';
|
|
|
8
9
|
export { default as SwitchCtrl } from './SwitchCtrl.svelte';
|
|
9
10
|
export { default as TextAreaCtrl } from './TextAreaCtrl.svelte';
|
|
10
11
|
export { default as TextInputCtrl } from './TextInputCtrl.svelte';
|
|
12
|
+
export { makeFieldId } from './utils';
|
|
11
13
|
export * from './types';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function makeFieldId(name?: string, sid?: string): string;
|
|
@@ -10,12 +10,18 @@ const resolvedRowMarkers = $derived(rowMarkers ?? (settings?.showRowMarkers ? se
|
|
|
10
10
|
// ── Theme ─────────────────────────────────────────────────────────────────────
|
|
11
11
|
let isDarkMode = $state(false);
|
|
12
12
|
let resolvedSkeletonTheme = $state(DEFAULT_THEME);
|
|
13
|
+
// Both isDarkMode and resolvedSkeletonTheme are derived from a local `dark`
|
|
14
|
+
// value rather than read back from isDarkMode itself, so this effect never
|
|
15
|
+
// tracks its own write target — avoids tearing down/recreating the
|
|
16
|
+
// MutationObserver on an incidental extra run.
|
|
13
17
|
$effect(() => {
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
const dark = detectDarkMode();
|
|
19
|
+
isDarkMode = dark;
|
|
20
|
+
resolvedSkeletonTheme = resolveSkeletonGridTheme(dark) ?? (dark ? DEFAULT_THEME_DARK : DEFAULT_THEME);
|
|
16
21
|
const mo = new MutationObserver(() => {
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
const dark = detectDarkMode();
|
|
23
|
+
isDarkMode = dark;
|
|
24
|
+
resolvedSkeletonTheme = resolveSkeletonGridTheme(dark) ?? (dark ? DEFAULT_THEME_DARK : DEFAULT_THEME);
|
|
19
25
|
});
|
|
20
26
|
const opts = {
|
|
21
27
|
attributes: true,
|
|
@@ -8,6 +8,7 @@ import { reorderColumns } from "../utils/columns.js";
|
|
|
8
8
|
import { applyLocalFilter, buildSearchFilters, gridColumnFiltersToFilterOptions } from "../utils/filters.js";
|
|
9
9
|
import { applyLocalSort, sortOrderToSortOptions } from "../utils/sort.js";
|
|
10
10
|
import { makeGetCellContent } from "../utils/cellContent.js";
|
|
11
|
+
import { getServerRenderableRows } from "../utils/serverPaging.js";
|
|
11
12
|
let { columns, data, getCellContent, rows, manageColumns = true, onColumnsChange, searchValue, defaultSearchValue = "", onSearchValueChange, onDataChange, dataSource, dataSourceOptions, pageSize = 200, uniqueID, serverSideSearch = false, searchColumns, onLoadError, total = $bindable(0), loading = $bindable(false), filters, onFilterChange, sortOrder, onSortOrderChange, selectedItems: _selectedItems, onSelectedItemsChange, settings, fixedColumns = 0, rowMarkers, showSearch, height = 500, onColumnMoved, onCellEdited, onCellDblClick, onRowDblClick, onRowClick, onRowContextMenu, onMenuClick, onSelectionChange, onCellEvent, onGridEvent, menuItems, onMenuItemSelect, children, ...rest } = $props();
|
|
12
13
|
// ── Column management ─────────────────────────────────────────────────────
|
|
13
14
|
// eslint-disable-next-line svelte/no-unused-svelte-ignore
|
|
@@ -471,7 +472,7 @@ const resolvedGetCellContent = $derived.by(() => {
|
|
|
471
472
|
});
|
|
472
473
|
// ── Row count ──────────────────────────────────────────────────────────────
|
|
473
474
|
const resolvedRows = $derived.by(() => {
|
|
474
|
-
if (isServerMode) return serverTotal;
|
|
475
|
+
if (isServerMode) return getServerRenderableRows(serverData.length, serverTotal, serverAllLoaded);
|
|
475
476
|
if (rows != null) return rows;
|
|
476
477
|
return filteredDataState.length;
|
|
477
478
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getServerRenderableRows(loadedRows: number, reportedTotal: number, allLoaded: boolean): number;
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
import VTreeSearch from "./VTreeSearch.svelte";
|
|
3
3
|
import VTreeVirtualViewport from "./VTreeVirtualViewport.svelte";
|
|
4
4
|
import VTreeContextMenu from "./VTreeContextMenu.svelte";
|
|
5
|
-
|
|
5
|
+
import { buildNodeIndex, computeMultiSelectClick, findAncestorIds } from "./VTree.utils.js";
|
|
6
|
+
const { data = [], adapter, idField = "id", labelField = "label", descriptionField = "description", childrenField = "children", hasChildrenField = "hasChildren", height = 420, rowHeight = 36, indent = 18, overscan = 8, searchable = true, searchPlaceholder = "Search tree...", searchValue = "", serverSideSearch = false, autoExpandSearchResults = true, loadOnMount = true, emptyMessage = "Nothing here...", loadingMessage = "Loading...", leftSection, rightSection, contextMenuItems, onselect, onclick, ondoubleclick, oncontext, ontoggle, onsearch, onloaderror, selected, onSelectedChange, multiple = false, selectedItems, onSelectedItemsChange, selectionMode = "modifier", autoMeasure = false, iconField, icon, classes, showRowSeparator = true, hoverEffect = true, onsingleclick, singleClickDelay = 250, onlongpress, longPressDelay = 500, longPressMoveTolerance = 10, onrowhover, onrowhoverend } = $props();
|
|
6
7
|
let viewportRef = $state(undefined);
|
|
7
8
|
let focusedIndex = $state(0);
|
|
8
9
|
let selectedId = $state(null);
|
|
@@ -22,6 +23,50 @@ let contextX = $state(0);
|
|
|
22
23
|
let contextY = $state(0);
|
|
23
24
|
let contextRow = $state(null);
|
|
24
25
|
let contextItems = $state([]);
|
|
26
|
+
// Long-press is handled per row and committed here when fired.
|
|
27
|
+
function handleRowLongpress(index, event) {
|
|
28
|
+
const row = flattenedRows[index];
|
|
29
|
+
if (!row) return;
|
|
30
|
+
selectRow(index);
|
|
31
|
+
commitSelection(index);
|
|
32
|
+
onlongpress?.(row.node, treeDataSnapshot, event);
|
|
33
|
+
}
|
|
34
|
+
// Selection model state.
|
|
35
|
+
let internalSelected = $state(null);
|
|
36
|
+
let internalSelectedItems = $state([]);
|
|
37
|
+
let rangeAnchorIndex = $state(null);
|
|
38
|
+
let lastRevealedId = $state(null);
|
|
39
|
+
let lastScrolledId = $state(null);
|
|
40
|
+
let singleClickTimer;
|
|
41
|
+
let clickCount = 0;
|
|
42
|
+
function commitSelection(index, event, forceToggle = false) {
|
|
43
|
+
const row = flattenedRows[index];
|
|
44
|
+
if (!row) return;
|
|
45
|
+
if (multiple) {
|
|
46
|
+
applyMultiSelect(row.node, index, event, forceToggle);
|
|
47
|
+
} else {
|
|
48
|
+
onSelectedChange?.(row.node);
|
|
49
|
+
if (selected === undefined) internalSelected = row.node;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function applyMultiSelect(node, index, event, forceToggle = false) {
|
|
53
|
+
const currentItems = selectedItems ?? internalSelectedItems;
|
|
54
|
+
const orderedRowIds = flattenedRows.map((row) => row.id);
|
|
55
|
+
const clickedId = getNodeId(node);
|
|
56
|
+
const result = computeMultiSelectClick({
|
|
57
|
+
currentIds: currentItems.map((item) => getNodeId(item)),
|
|
58
|
+
clickedId,
|
|
59
|
+
clickedIndex: index,
|
|
60
|
+
anchorIndex: rangeAnchorIndex,
|
|
61
|
+
orderedRowIds,
|
|
62
|
+
ctrlOrMeta: forceToggle || Boolean(event?.ctrlKey || event?.metaKey),
|
|
63
|
+
shift: Boolean(event?.shiftKey)
|
|
64
|
+
});
|
|
65
|
+
rangeAnchorIndex = result.nextAnchorIndex;
|
|
66
|
+
const items = result.ids.map((id) => nodeIndex.byId.get(id)).filter((item) => Boolean(item));
|
|
67
|
+
if (selectedItems === undefined) internalSelectedItems = items;
|
|
68
|
+
onSelectedItemsChange?.(items);
|
|
69
|
+
}
|
|
25
70
|
function getNodeId(node) {
|
|
26
71
|
const value = node?.[idField];
|
|
27
72
|
return String(value ?? "");
|
|
@@ -122,6 +167,7 @@ const displayRoots = $derived.by(() => {
|
|
|
122
167
|
if (adapter && serverSideSearch && searchQuery.trim().length > 0) return searchNodes;
|
|
123
168
|
return rootNodes;
|
|
124
169
|
});
|
|
170
|
+
const nodeIndex = $derived.by(() => buildNodeIndex(displayRoots, getNodeId, getNodeChildren));
|
|
125
171
|
const flattenedRows = $derived.by(() => {
|
|
126
172
|
const rows = [];
|
|
127
173
|
const activeSearch = searchQuery.trim().toLowerCase();
|
|
@@ -178,10 +224,36 @@ const treeDataSnapshot = $derived.by(() => {
|
|
|
178
224
|
});
|
|
179
225
|
return buildTree(displayRoots);
|
|
180
226
|
});
|
|
227
|
+
const selectedNodeId = $derived(selected ? getNodeId(selected) : selected === null ? "" : internalSelected ? getNodeId(internalSelected) : "");
|
|
228
|
+
$effect(() => {
|
|
229
|
+
if (!selectedNodeId || lastRevealedId === selectedNodeId) return;
|
|
230
|
+
const ancestors = findAncestorIds(nodeIndex.parentById, selectedNodeId);
|
|
231
|
+
const next = { ...expandedById };
|
|
232
|
+
let changed = false;
|
|
233
|
+
for (const id of ancestors) {
|
|
234
|
+
if (!next[id]) {
|
|
235
|
+
next[id] = true;
|
|
236
|
+
changed = true;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (changed) expandedById = next;
|
|
240
|
+
lastRevealedId = selectedNodeId;
|
|
241
|
+
lastScrolledId = null;
|
|
242
|
+
});
|
|
243
|
+
$effect(() => {
|
|
244
|
+
if (!selectedNodeId || lastScrolledId === selectedNodeId) return;
|
|
245
|
+
const index = flattenedRows.findIndex((row) => row.id === selectedNodeId);
|
|
246
|
+
if (index < 0) return;
|
|
247
|
+
focusedIndex = index;
|
|
248
|
+
selectedId = selectedNodeId;
|
|
249
|
+
viewportRef?.scrollToIndex(index);
|
|
250
|
+
lastScrolledId = selectedNodeId;
|
|
251
|
+
});
|
|
181
252
|
function selectRow(index) {
|
|
182
253
|
const row = flattenedRows[index];
|
|
183
254
|
if (!row) return;
|
|
184
255
|
focusedIndex = index;
|
|
256
|
+
// Keep existing uncontrolled fallback behavior.
|
|
185
257
|
selectedId = row.id;
|
|
186
258
|
onselect?.(row.node);
|
|
187
259
|
viewportRef?.scrollToIndex(index);
|
|
@@ -247,13 +319,20 @@ function handleTreeKeydown(event) {
|
|
|
247
319
|
}
|
|
248
320
|
return;
|
|
249
321
|
}
|
|
250
|
-
if (event.key === "
|
|
322
|
+
if (event.key === "Enter") {
|
|
251
323
|
event.preventDefault();
|
|
252
324
|
selectRow(focusedIndex);
|
|
325
|
+
// Single-click selection via Enter.
|
|
326
|
+
commitSelection(focusedIndex);
|
|
253
327
|
if (current.hasChildren) {
|
|
254
328
|
toggleNode(current.node);
|
|
255
329
|
}
|
|
256
330
|
}
|
|
331
|
+
// Single-click selection via Space.
|
|
332
|
+
if (event.key === " ") {
|
|
333
|
+
event.preventDefault();
|
|
334
|
+
commitSelection(focusedIndex);
|
|
335
|
+
}
|
|
257
336
|
}
|
|
258
337
|
async function runServerSearch(query) {
|
|
259
338
|
if (!adapter || !serverSideSearch) return;
|
|
@@ -281,10 +360,20 @@ function handleRowClick(index, event) {
|
|
|
281
360
|
if (!row) return;
|
|
282
361
|
selectRow(index);
|
|
283
362
|
onclick?.(row.node, treeDataSnapshot, event);
|
|
363
|
+
// Checkbox mode intentionally leaves row-body clicks out of multi-selection.
|
|
364
|
+
if (!multiple || selectionMode === "modifier") commitSelection(index, event);
|
|
365
|
+
clearTimeout(singleClickTimer);
|
|
366
|
+
clickCount += 1;
|
|
367
|
+
singleClickTimer = setTimeout(() => {
|
|
368
|
+
if (clickCount === 1) onsingleclick?.(row.node, treeDataSnapshot, event);
|
|
369
|
+
clickCount = 0;
|
|
370
|
+
}, singleClickDelay);
|
|
284
371
|
}
|
|
285
372
|
function handleRowDoubleClick(index, event) {
|
|
286
373
|
const row = flattenedRows[index];
|
|
287
374
|
if (!row) return;
|
|
375
|
+
clearTimeout(singleClickTimer);
|
|
376
|
+
clickCount = 0;
|
|
288
377
|
selectRow(index);
|
|
289
378
|
ondoubleclick?.(row.node, treeDataSnapshot, event);
|
|
290
379
|
}
|
|
@@ -358,6 +447,7 @@ onMount(() => {
|
|
|
358
447
|
bind:this={viewportRef}
|
|
359
448
|
rows={flattenedRows}
|
|
360
449
|
{rowHeight}
|
|
450
|
+
{autoMeasure}
|
|
361
451
|
{height}
|
|
362
452
|
{indent}
|
|
363
453
|
{overscan}
|
|
@@ -371,12 +461,27 @@ onMount(() => {
|
|
|
371
461
|
{treeDataSnapshot}
|
|
372
462
|
{leftSection}
|
|
373
463
|
{rightSection}
|
|
464
|
+
{iconField}
|
|
465
|
+
{icon}
|
|
466
|
+
{classes}
|
|
467
|
+
{showRowSeparator}
|
|
468
|
+
{hoverEffect}
|
|
469
|
+
{multiple}
|
|
470
|
+
{selectionMode}
|
|
471
|
+
selectedItemIds={(selectedItems ?? internalSelectedItems).map((item) => getNodeId(item))}
|
|
472
|
+
selectedNodeId={selectedNodeId}
|
|
374
473
|
onselectrow={handleRowClick}
|
|
375
474
|
onrowdoubleclick={handleRowDoubleClick}
|
|
376
475
|
onrowcontext={handleRowContext}
|
|
377
476
|
onviewportcontext={handleViewportContext}
|
|
378
477
|
ontogglerow={toggleNode}
|
|
379
478
|
ontreekeydown={handleTreeKeydown}
|
|
479
|
+
onrowlongpress={handleRowLongpress}
|
|
480
|
+
oncheckboxselect={(index, event) => commitSelection(index, event, true)}
|
|
481
|
+
onrowhover={(index, event) => { const row = flattenedRows[index]; if (row) onrowhover?.(row.node, treeDataSnapshot, index, event); }}
|
|
482
|
+
onrowhoverend={(index, event) => { const row = flattenedRows[index]; if (row) onrowhoverend?.(row.node, treeDataSnapshot, index, event); }}
|
|
483
|
+
{longPressDelay}
|
|
484
|
+
{longPressMoveTolerance}
|
|
380
485
|
/>
|
|
381
486
|
|
|
382
487
|
<VTreeContextMenu
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { VTreeRecord } from './types.js';
|
|
2
|
+
export interface NodeIndex<T extends VTreeRecord = VTreeRecord> {
|
|
3
|
+
byId: Map<string, T>;
|
|
4
|
+
parentById: Map<string, string | null>;
|
|
5
|
+
}
|
|
6
|
+
export declare function mergeClasses(base: string, override?: string): string;
|
|
7
|
+
export declare function buildNodeIndex<T extends VTreeRecord>(roots: T[], getId: (node: T) => string, getChildren: (node: T) => T[]): NodeIndex<T>;
|
|
8
|
+
export declare function findAncestorIds(parentById: Map<string, string | null>, targetId: string): string[];
|
|
9
|
+
export declare function resolveRowHeight<T extends VTreeRecord>(rowHeight: number | ((node: T, level: number, index: number) => number), node: T | undefined, level: number, index: number, fallback?: number): number;
|
|
10
|
+
export interface MultiSelectClickInput {
|
|
11
|
+
currentIds: string[];
|
|
12
|
+
clickedId: string;
|
|
13
|
+
clickedIndex: number;
|
|
14
|
+
anchorIndex: number | null;
|
|
15
|
+
orderedRowIds: string[];
|
|
16
|
+
ctrlOrMeta?: boolean;
|
|
17
|
+
shift?: boolean;
|
|
18
|
+
}
|
|
19
|
+
export declare function computeMultiSelectClick(input: MultiSelectClickInput): {
|
|
20
|
+
ids: string[];
|
|
21
|
+
nextAnchorIndex: number;
|
|
22
|
+
};
|
|
23
|
+
export declare function isImageLikeValue(value: unknown): boolean;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export function mergeClasses(base, override) {
|
|
2
|
+
return [base, override].filter(Boolean).join(' ');
|
|
3
|
+
}
|
|
4
|
+
export function buildNodeIndex(roots, getId, getChildren) {
|
|
5
|
+
const byId = new Map();
|
|
6
|
+
const parentById = new Map();
|
|
7
|
+
const visiting = new Set();
|
|
8
|
+
const walk = (nodes, parentId) => {
|
|
9
|
+
for (const node of nodes) {
|
|
10
|
+
if (!node || visiting.has(node))
|
|
11
|
+
continue;
|
|
12
|
+
const id = getId(node);
|
|
13
|
+
if (!id || byId.has(id))
|
|
14
|
+
continue;
|
|
15
|
+
byId.set(id, node);
|
|
16
|
+
parentById.set(id, parentId);
|
|
17
|
+
visiting.add(node);
|
|
18
|
+
walk(getChildren(node), id);
|
|
19
|
+
visiting.delete(node);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
walk(roots, null);
|
|
23
|
+
return { byId, parentById };
|
|
24
|
+
}
|
|
25
|
+
export function findAncestorIds(parentById, targetId) {
|
|
26
|
+
const visited = new Set();
|
|
27
|
+
const ancestors = [];
|
|
28
|
+
let current = targetId;
|
|
29
|
+
while (current && !visited.has(current)) {
|
|
30
|
+
visited.add(current);
|
|
31
|
+
const parent = parentById.get(current);
|
|
32
|
+
if (!parent)
|
|
33
|
+
break;
|
|
34
|
+
ancestors.push(parent);
|
|
35
|
+
current = parent;
|
|
36
|
+
}
|
|
37
|
+
return ancestors.reverse();
|
|
38
|
+
}
|
|
39
|
+
export function resolveRowHeight(rowHeight, node, level, index, fallback = 36) {
|
|
40
|
+
const value = typeof rowHeight === 'function' ? (node ? rowHeight(node, level, index) : fallback) : rowHeight;
|
|
41
|
+
return Number.isFinite(value) && value > 0 ? value : fallback;
|
|
42
|
+
}
|
|
43
|
+
export function computeMultiSelectClick(input) {
|
|
44
|
+
const current = new Set(input.currentIds);
|
|
45
|
+
let ids;
|
|
46
|
+
const anchor = input.anchorIndex ?? input.clickedIndex;
|
|
47
|
+
if (input.shift) {
|
|
48
|
+
const start = Math.max(0, Math.min(anchor, input.clickedIndex));
|
|
49
|
+
const end = Math.min(input.orderedRowIds.length - 1, Math.max(anchor, input.clickedIndex));
|
|
50
|
+
ids = input.ctrlOrMeta ? new Set(current) : new Set();
|
|
51
|
+
for (const id of input.orderedRowIds.slice(start, end + 1))
|
|
52
|
+
ids.add(id);
|
|
53
|
+
}
|
|
54
|
+
else if (input.ctrlOrMeta) {
|
|
55
|
+
ids = new Set(current);
|
|
56
|
+
if (ids.has(input.clickedId))
|
|
57
|
+
ids.delete(input.clickedId);
|
|
58
|
+
else
|
|
59
|
+
ids.add(input.clickedId);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
ids = new Set([input.clickedId]);
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
ids: input.orderedRowIds.filter((id) => ids.has(id)),
|
|
66
|
+
nextAnchorIndex: input.clickedIndex
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export function isImageLikeValue(value) {
|
|
70
|
+
if (typeof value !== 'string')
|
|
71
|
+
return false;
|
|
72
|
+
return /^(https?:\/\/|data:image\/|\/|\.\/|\.\.\/).+\.(png|jpe?g|gif|svg|webp)(\?.*)?$/i.test(value) || /^data:image\//i.test(value);
|
|
73
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<script lang="ts">import VTree from "./VTree.svelte";
|
|
2
|
+
const data = [{
|
|
3
|
+
id: "root",
|
|
4
|
+
label: "Root",
|
|
5
|
+
children: [{
|
|
6
|
+
id: "deep",
|
|
7
|
+
label: "Deep child",
|
|
8
|
+
children: [{
|
|
9
|
+
id: "leaf",
|
|
10
|
+
label: "Leaf"
|
|
11
|
+
}]
|
|
12
|
+
}]
|
|
13
|
+
}];
|
|
14
|
+
let selected = null;
|
|
15
|
+
</script>
|
|
16
|
+
|
|
17
|
+
<div class="space-y-2">
|
|
18
|
+
<button class="btn preset-filled-primary-500" type="button" onclick={() => selected = data[0].children[0].children[0]}>Select leaf</button>
|
|
19
|
+
<VTree {data} {selected} onSelectedChange={(node) => selected = node} searchable={false} />
|
|
20
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
|
|
2
|
+
new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
|
|
3
|
+
$$bindings?: Bindings;
|
|
4
|
+
} & Exports;
|
|
5
|
+
(internal: unknown, props: {
|
|
6
|
+
$$events?: Events;
|
|
7
|
+
$$slots?: Slots;
|
|
8
|
+
}): Exports & {
|
|
9
|
+
$set?: any;
|
|
10
|
+
$on?: any;
|
|
11
|
+
};
|
|
12
|
+
z_$$bindings?: Bindings;
|
|
13
|
+
}
|
|
14
|
+
declare const VTreeControlledSelectionDemo: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
|
|
15
|
+
[evt: string]: CustomEvent<any>;
|
|
16
|
+
}, {}, {}, string>;
|
|
17
|
+
type VTreeControlledSelectionDemo = InstanceType<typeof VTreeControlledSelectionDemo>;
|
|
18
|
+
export default VTreeControlledSelectionDemo;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">// @ts-nocheck
|
|
2
|
+
import VTree from "./VTree.svelte";
|
|
3
|
+
const data = [{
|
|
4
|
+
id: "folder",
|
|
5
|
+
label: "Folder",
|
|
6
|
+
icon: "📁",
|
|
7
|
+
children: [{
|
|
8
|
+
id: "doc",
|
|
9
|
+
label: "Document",
|
|
10
|
+
icon: "📄"
|
|
11
|
+
}]
|
|
12
|
+
}, {
|
|
13
|
+
id: "settings",
|
|
14
|
+
label: "Settings",
|
|
15
|
+
icon: "⚙️"
|
|
16
|
+
}];
|
|
17
|
+
</script>
|
|
18
|
+
|
|
19
|
+
{#snippet customIcon({ node })}
|
|
20
|
+
<span aria-hidden="true">{node.id === 'folder' ? '🗂️' : '✨'}</span>
|
|
21
|
+
{/snippet}
|
|
22
|
+
|
|
23
|
+
<VTree {data} iconField="icon" icon={customIcon} searchable={false} />
|