carbon-components-svelte 0.98.0 → 0.98.1
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/package.json +1 -1
- package/src/Checkbox/Checkbox.svelte +16 -2
- package/src/ComboBox/ComboBox.svelte +9 -3
- package/src/ContentSwitcher/ContentSwitcher.svelte +15 -3
- package/src/ContentSwitcher/Switch.svelte +1 -7
- package/src/DataTable/DataTable.svelte +9 -42
- package/src/DataTable/DataTableTypes.d.ts +2 -2
- package/src/DataTable/ToolbarSearch.svelte +2 -1
- package/src/DataTable/data-table-utils.d.ts +54 -0
- package/src/DataTable/data-table-utils.js +239 -0
- package/src/StructuredList/StructuredList.svelte +10 -1
- package/src/Tooltip/Tooltip.svelte +8 -1
- package/src/TreeView/TreeView.svelte +108 -65
- package/types/ComboBox/ComboBox.svelte.d.ts +1 -1
- package/types/ContentSwitcher/ContentSwitcher.svelte.d.ts +1 -1
- package/types/DataTable/DataTableTypes.d.ts +2 -2
- package/types/DataTable/data-table-utils.d.ts +54 -0
package/package.json
CHANGED
|
@@ -65,7 +65,17 @@
|
|
|
65
65
|
|
|
66
66
|
$: useGroup = Array.isArray(group);
|
|
67
67
|
$: if (useGroup) checked = group.includes(value);
|
|
68
|
-
|
|
68
|
+
|
|
69
|
+
// Track previous checked value to avoid duplicate dispatches in Svelte 5
|
|
70
|
+
// The reactive statement will only dispatch when checked changes externally (e.g., via bind:checked)
|
|
71
|
+
let previousChecked = checked;
|
|
72
|
+
$: {
|
|
73
|
+
const hasChanged = previousChecked !== checked;
|
|
74
|
+
if (hasChanged) {
|
|
75
|
+
previousChecked = checked;
|
|
76
|
+
dispatch("check", checked);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
69
79
|
|
|
70
80
|
let refLabel = null;
|
|
71
81
|
|
|
@@ -121,7 +131,11 @@
|
|
|
121
131
|
? group.filter((_value) => _value !== value)
|
|
122
132
|
: [...group, value];
|
|
123
133
|
} else {
|
|
124
|
-
|
|
134
|
+
const newChecked = !checked;
|
|
135
|
+
previousChecked = newChecked;
|
|
136
|
+
checked = newChecked;
|
|
137
|
+
// Dispatch directly for user-initiated changes to avoid duplicate events in Svelte 5
|
|
138
|
+
dispatch("check", newChecked);
|
|
125
139
|
}
|
|
126
140
|
}}
|
|
127
141
|
on:change
|
|
@@ -201,7 +201,7 @@
|
|
|
201
201
|
|
|
202
202
|
/**
|
|
203
203
|
* Clear the combo box programmatically
|
|
204
|
-
* @type {(options?: { focus?: boolean; }) => void}
|
|
204
|
+
* @type {(options?: { focus?: boolean; }) => Promise<void>}
|
|
205
205
|
* @param {object} [options] - Configuration options for clearing
|
|
206
206
|
* @param {boolean} [options.focus=true] - Whether to focus the combo box after clearing
|
|
207
207
|
* @example
|
|
@@ -211,7 +211,7 @@
|
|
|
211
211
|
* <button on:click={() => comboBox.clear({ focus: false })}>Clear (No Focus)</button>
|
|
212
212
|
* ```
|
|
213
213
|
*/
|
|
214
|
-
export function clear(options = {}) {
|
|
214
|
+
export async function clear(options = {}) {
|
|
215
215
|
prevSelectedId = null;
|
|
216
216
|
highlightedIndex = -1;
|
|
217
217
|
highlightedId = undefined;
|
|
@@ -219,6 +219,8 @@
|
|
|
219
219
|
selectedItem = undefined;
|
|
220
220
|
open = false;
|
|
221
221
|
value = "";
|
|
222
|
+
// Ensure binding updates are complete before focusing.
|
|
223
|
+
await tick();
|
|
222
224
|
if (options?.focus !== false) ref?.focus();
|
|
223
225
|
}
|
|
224
226
|
|
|
@@ -262,6 +264,8 @@
|
|
|
262
264
|
|
|
263
265
|
$: if (selectedId !== undefined) {
|
|
264
266
|
if (prevSelectedId !== selectedId) {
|
|
267
|
+
// Only dispatch select event if not initial render (prevSelectedId was not null)
|
|
268
|
+
const isInitialRender = prevSelectedId === null;
|
|
265
269
|
prevSelectedId = selectedId;
|
|
266
270
|
if (filteredItems?.length === 1 && open) {
|
|
267
271
|
selectedId = filteredItems[0].id;
|
|
@@ -271,7 +275,9 @@
|
|
|
271
275
|
} else {
|
|
272
276
|
selectedItem = items.find((item) => item.id === selectedId);
|
|
273
277
|
}
|
|
274
|
-
|
|
278
|
+
if (!isInitialRender) {
|
|
279
|
+
dispatch("select", { selectedId, selectedItem });
|
|
280
|
+
}
|
|
275
281
|
}
|
|
276
282
|
} else {
|
|
277
283
|
prevSelectedId = selectedId;
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export let size = undefined;
|
|
14
14
|
|
|
15
|
-
import { afterUpdate, createEventDispatcher, setContext } from "svelte";
|
|
15
|
+
import { afterUpdate, createEventDispatcher, setContext, tick } from "svelte";
|
|
16
16
|
import { writable } from "svelte/store";
|
|
17
17
|
|
|
18
18
|
const dispatch = createEventDispatcher();
|
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
*/
|
|
22
22
|
const currentId = writable(null);
|
|
23
23
|
|
|
24
|
+
/** @type {HTMLDivElement | null} */
|
|
25
|
+
let refContainer = null;
|
|
26
|
+
|
|
24
27
|
$: currentIndex = -1;
|
|
25
28
|
$: switches = [];
|
|
26
29
|
$: if (switches[currentIndex]) {
|
|
@@ -47,9 +50,9 @@
|
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
/**
|
|
50
|
-
* @type {(direction: number) => void}
|
|
53
|
+
* @type {(direction: number) => Promise<void>}
|
|
51
54
|
*/
|
|
52
|
-
const change = (direction) => {
|
|
55
|
+
const change = async (direction) => {
|
|
53
56
|
let index = currentIndex + direction;
|
|
54
57
|
|
|
55
58
|
if (index < 0) {
|
|
@@ -59,6 +62,14 @@
|
|
|
59
62
|
}
|
|
60
63
|
|
|
61
64
|
selectedIndex = index;
|
|
65
|
+
|
|
66
|
+
await tick();
|
|
67
|
+
const tabs = refContainer?.querySelectorAll("[role='tab']");
|
|
68
|
+
const tab = tabs?.[index];
|
|
69
|
+
|
|
70
|
+
if (tab instanceof HTMLElement) {
|
|
71
|
+
tab.focus();
|
|
72
|
+
}
|
|
62
73
|
};
|
|
63
74
|
|
|
64
75
|
setContext("ContentSwitcher", {
|
|
@@ -78,6 +89,7 @@
|
|
|
78
89
|
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
|
79
90
|
<!-- svelte-ignore a11y-interactive-supports-focus -->
|
|
80
91
|
<div
|
|
92
|
+
bind:this={refContainer}
|
|
81
93
|
role="tablist"
|
|
82
94
|
class:bx--content-switcher={true}
|
|
83
95
|
class:bx--content-switcher--sm={size === "sm"}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
/** Obtain a reference to the button HTML element */
|
|
24
24
|
export let ref = null;
|
|
25
25
|
|
|
26
|
-
import {
|
|
26
|
+
import { getContext, onMount } from "svelte";
|
|
27
27
|
|
|
28
28
|
const ctx = getContext("ContentSwitcher");
|
|
29
29
|
|
|
@@ -33,12 +33,6 @@
|
|
|
33
33
|
selected = currentId === id;
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
afterUpdate(() => {
|
|
37
|
-
if (selected) {
|
|
38
|
-
ref.focus();
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
36
|
onMount(() => {
|
|
43
37
|
return () => unsubscribe();
|
|
44
38
|
});
|
|
@@ -192,6 +192,12 @@
|
|
|
192
192
|
import InlineCheckbox from "../Checkbox/InlineCheckbox.svelte";
|
|
193
193
|
import ChevronRight from "../icons/ChevronRight.svelte";
|
|
194
194
|
import RadioButton from "../RadioButton/RadioButton.svelte";
|
|
195
|
+
import {
|
|
196
|
+
compareValues,
|
|
197
|
+
formatHeaderWidth,
|
|
198
|
+
getDisplayedRows,
|
|
199
|
+
resolvePath,
|
|
200
|
+
} from "./data-table-utils.js";
|
|
195
201
|
import Table from "./Table.svelte";
|
|
196
202
|
import TableBody from "./TableBody.svelte";
|
|
197
203
|
import TableCell from "./TableCell.svelte";
|
|
@@ -200,8 +206,6 @@
|
|
|
200
206
|
import TableHeader from "./TableHeader.svelte";
|
|
201
207
|
import TableRow from "./TableRow.svelte";
|
|
202
208
|
|
|
203
|
-
const PATH_SPLIT_REGEX = /[.[\]'"]/;
|
|
204
|
-
|
|
205
209
|
const sortDirectionMap = {
|
|
206
210
|
none: "ascending",
|
|
207
211
|
ascending: "descending",
|
|
@@ -228,13 +232,6 @@
|
|
|
228
232
|
a[c.key] = c.key;
|
|
229
233
|
return a;
|
|
230
234
|
}, {});
|
|
231
|
-
const resolvePath = (object, path) => {
|
|
232
|
-
if (path in object) return object[path];
|
|
233
|
-
return path
|
|
234
|
-
.split(PATH_SPLIT_REGEX)
|
|
235
|
-
.filter((p) => p)
|
|
236
|
-
.reduce((o, p) => (o && typeof o === "object" ? o[p] : o), object);
|
|
237
|
-
};
|
|
238
235
|
|
|
239
236
|
/**
|
|
240
237
|
* @type {() => void}
|
|
@@ -342,48 +339,18 @@
|
|
|
342
339
|
sortedRows = $tableRows;
|
|
343
340
|
} else {
|
|
344
341
|
sortedRows = [...$tableRows].sort((a, b) => {
|
|
345
|
-
const itemA =
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
const itemB = ascending
|
|
349
|
-
? resolvePath(b, sortKey)
|
|
350
|
-
: resolvePath(a, sortKey);
|
|
351
|
-
|
|
352
|
-
if (sortingHeader?.sort) return sortingHeader.sort(itemA, itemB);
|
|
353
|
-
|
|
354
|
-
if (typeof itemA === "number" && typeof itemB === "number")
|
|
355
|
-
return itemA - itemB;
|
|
356
|
-
|
|
357
|
-
if ([itemA, itemB].every((item) => !item && item !== 0)) return 0;
|
|
358
|
-
if (!itemA && itemA !== 0) return ascending ? 1 : -1;
|
|
359
|
-
if (!itemB && itemB !== 0) return ascending ? -1 : 1;
|
|
360
|
-
|
|
361
|
-
return itemA
|
|
362
|
-
.toString()
|
|
363
|
-
.localeCompare(itemB.toString(), "en", { numeric: true });
|
|
342
|
+
const itemA = resolvePath(a, sortKey);
|
|
343
|
+
const itemB = resolvePath(b, sortKey);
|
|
344
|
+
return compareValues(itemA, itemB, ascending, sortingHeader?.sort);
|
|
364
345
|
});
|
|
365
346
|
}
|
|
366
347
|
}
|
|
367
|
-
const getDisplayedRows = (rows, page, pageSize) =>
|
|
368
|
-
page && pageSize
|
|
369
|
-
? rows.slice((page - 1) * pageSize, page * pageSize)
|
|
370
|
-
: rows;
|
|
371
348
|
$: displayedRows = getDisplayedRows($tableRows, page, pageSize);
|
|
372
349
|
$: displayedSortedRows = getDisplayedRows(sortedRows, page, pageSize);
|
|
373
350
|
|
|
374
351
|
$: hasCustomHeaderWidth = headers.some(
|
|
375
352
|
(header) => header.width || header.minWidth,
|
|
376
353
|
);
|
|
377
|
-
|
|
378
|
-
/** @type {(header: DataTableHeader) => undefined | string} */
|
|
379
|
-
const formatHeaderWidth = (header) => {
|
|
380
|
-
const styles = [
|
|
381
|
-
header.width && `width: ${header.width}`,
|
|
382
|
-
header.minWidth && `min-width: ${header.minWidth}`,
|
|
383
|
-
].filter(Boolean);
|
|
384
|
-
if (styles.length === 0) return undefined;
|
|
385
|
-
return styles.join(";");
|
|
386
|
-
};
|
|
387
354
|
</script>
|
|
388
355
|
|
|
389
356
|
<TableContainer {useStaticWidth} {...$$restProps}>
|
|
@@ -6,8 +6,8 @@ type Join<K, P> = K extends string | number
|
|
|
6
6
|
: never
|
|
7
7
|
: never;
|
|
8
8
|
|
|
9
|
-
// For performance, the maximum traversal depth is
|
|
10
|
-
export type PropertyPath<T, D extends number =
|
|
9
|
+
// For performance, the maximum traversal depth is 3.
|
|
10
|
+
export type PropertyPath<T, D extends number = 3> = [D] extends [never]
|
|
11
11
|
? never
|
|
12
12
|
: T extends object
|
|
13
13
|
? {
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
|
|
49
49
|
import { afterUpdate, getContext, onMount, tick } from "svelte";
|
|
50
50
|
import Search from "../Search/Search.svelte";
|
|
51
|
+
import { rowsEqual } from "./data-table-utils.js";
|
|
51
52
|
|
|
52
53
|
const ctx = getContext("DataTable") ?? {};
|
|
53
54
|
|
|
@@ -58,7 +59,7 @@
|
|
|
58
59
|
unsubscribe = ctx?.tableRows.subscribe((tableRows) => {
|
|
59
60
|
// Only update if the rows have actually changed.
|
|
60
61
|
// This approach works in both Svelte 4 and Svelte 5.
|
|
61
|
-
if (
|
|
62
|
+
if (!rowsEqual(tableRows, rows)) {
|
|
62
63
|
rows = tableRows;
|
|
63
64
|
}
|
|
64
65
|
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight deep equality check optimized for DataTable rows.
|
|
3
|
+
* Compares arrays of row objects by first checking IDs (fast path),
|
|
4
|
+
* then falling back to deep object comparison to handle nested structures.
|
|
5
|
+
*/
|
|
6
|
+
export function rowsEqual<Row extends Record<string, any>>(
|
|
7
|
+
a: ReadonlyArray<Row> | null,
|
|
8
|
+
b: ReadonlyArray<Row> | null,
|
|
9
|
+
): boolean;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolves a nested property path in an object.
|
|
13
|
+
* Supports both direct property access and nested paths like "contact.company".
|
|
14
|
+
*/
|
|
15
|
+
export function resolvePath<T extends Record<string, unknown>>(
|
|
16
|
+
object: T,
|
|
17
|
+
path: string,
|
|
18
|
+
): unknown;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Paginates an array of rows based on page number and page size.
|
|
22
|
+
*/
|
|
23
|
+
export function getDisplayedRows<Row extends Record<string, unknown>>(
|
|
24
|
+
rows: ReadonlyArray<Row>,
|
|
25
|
+
page: number,
|
|
26
|
+
pageSize: number,
|
|
27
|
+
): ReadonlyArray<Row>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Formats header width styles for table headers.
|
|
31
|
+
* Combines width and minWidth into a CSS style string.
|
|
32
|
+
*/
|
|
33
|
+
export function formatHeaderWidth<
|
|
34
|
+
Header extends {
|
|
35
|
+
width?: string | null | number;
|
|
36
|
+
minWidth?: string | null | number;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
} = {
|
|
39
|
+
width?: string | null | number;
|
|
40
|
+
minWidth?: string | null | number;
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
},
|
|
43
|
+
>(header: Header): string | undefined;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compares two values for sorting in a data table.
|
|
47
|
+
* Handles numbers, strings, null/undefined values, and custom sort functions.
|
|
48
|
+
*/
|
|
49
|
+
export function compareValues<T = unknown>(
|
|
50
|
+
itemA: T,
|
|
51
|
+
itemB: T,
|
|
52
|
+
ascending: boolean,
|
|
53
|
+
customSort?: ((a: T, b: T) => number) | false | undefined,
|
|
54
|
+
): number;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// @ts-check
|
|
2
|
+
/**
|
|
3
|
+
* Deep equality check for values (nested objects and arrays).
|
|
4
|
+
* @param {*} a - First value to compare
|
|
5
|
+
* @param {*} b - Second value to compare
|
|
6
|
+
* @param {WeakMap<*, Set<*>>} [stack] - WeakMap used to track circular references
|
|
7
|
+
* @returns {boolean} True if values are deeply equal, false otherwise
|
|
8
|
+
*/
|
|
9
|
+
function deepEqual(a, b, stack = new WeakMap()) {
|
|
10
|
+
// Fast path: reference equality.
|
|
11
|
+
if (a === b) return true;
|
|
12
|
+
|
|
13
|
+
// Handle null/undefined.
|
|
14
|
+
if (a == null || b == null) return a === b;
|
|
15
|
+
|
|
16
|
+
// Handle NaN: NaN is the only value where NaN !== NaN is true in JavaScript
|
|
17
|
+
// Without this check, two NaN values would incorrectly be considered unequal.
|
|
18
|
+
if (Number.isNaN(a) && Number.isNaN(b)) return true;
|
|
19
|
+
if (Number.isNaN(a) || Number.isNaN(b)) return false;
|
|
20
|
+
|
|
21
|
+
if (typeof a !== typeof b) return false;
|
|
22
|
+
|
|
23
|
+
if (a instanceof Date && b instanceof Date) {
|
|
24
|
+
return a.getTime() === b.getTime();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (a instanceof RegExp && b instanceof RegExp) {
|
|
28
|
+
return a.source === b.source && a.flags === b.flags;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (typeof a === "function" && typeof b === "function") {
|
|
32
|
+
return a === b;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
36
|
+
if (a.length !== b.length) return false;
|
|
37
|
+
for (let i = 0; i < a.length; i++) {
|
|
38
|
+
// Pass the stack to handle nested arrays and prevent infinite recursion.
|
|
39
|
+
if (!deepEqual(a[i], b[i], stack)) return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
45
|
+
const aVisited = stack.get(a);
|
|
46
|
+
|
|
47
|
+
if (aVisited?.has(b)) {
|
|
48
|
+
// Circular reference: if we've already seen this (a, b) pair, they're equal.
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// WeakMap entries are auto-removed when keys are garbage collected.
|
|
53
|
+
if (aVisited) {
|
|
54
|
+
aVisited.add(b);
|
|
55
|
+
} else {
|
|
56
|
+
stack.set(a, new Set([b]));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Compare string keys: objects must have the same enumerable properties.
|
|
60
|
+
const keysA = Object.keys(a);
|
|
61
|
+
const keysB = Object.keys(b);
|
|
62
|
+
if (keysA.length !== keysB.length) {
|
|
63
|
+
stack.get(a)?.delete(b);
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
for (const key of keysA) {
|
|
68
|
+
// Pass the stack to handle nested objects and prevent infinite recursion.
|
|
69
|
+
if (!(key in b) || !deepEqual(a[key], b[key], stack)) {
|
|
70
|
+
stack.get(a)?.delete(b);
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const symKeysA = Object.getOwnPropertySymbols(a);
|
|
76
|
+
const symKeysB = Object.getOwnPropertySymbols(b);
|
|
77
|
+
if (symKeysA.length !== symKeysB.length) {
|
|
78
|
+
stack.get(a)?.delete(b);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Recursively compare Symbol property values.
|
|
83
|
+
for (const key of symKeysA) {
|
|
84
|
+
if (!symKeysB.includes(key) || !deepEqual(a[key], b[key], stack)) {
|
|
85
|
+
stack.get(a)?.delete(b);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// All checks passed: remove (a, b) from tracking before returning
|
|
91
|
+
// Cleanup is for correctness (not GC): allows same objects to be
|
|
92
|
+
// compared again without false circular reference detection.
|
|
93
|
+
stack.get(a)?.delete(b);
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Finally, use strict equality for primitives.
|
|
98
|
+
return a === b;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Lightweight deep equality check optimized for DataTable rows.
|
|
103
|
+
* Compares arrays of row objects by first checking IDs (fast path),
|
|
104
|
+
* then falling back to deep object comparison to handle nested structures.
|
|
105
|
+
* @template {Record<string, any>} Row - Row type with at least an optional `id` property
|
|
106
|
+
* @param {ReadonlyArray<Row> | null} a - First array of rows to compare
|
|
107
|
+
* @param {ReadonlyArray<Row> | null} b - Second array of rows to compare
|
|
108
|
+
* @returns {boolean} True if row arrays are deeply equal, false otherwise
|
|
109
|
+
*/
|
|
110
|
+
export function rowsEqual(a, b) {
|
|
111
|
+
if (a === b) return true;
|
|
112
|
+
if (a === null || b === null) return false;
|
|
113
|
+
if (!Array.isArray(a) || !Array.isArray(b)) return false;
|
|
114
|
+
|
|
115
|
+
if (a.length !== b.length) return false;
|
|
116
|
+
|
|
117
|
+
// Fast path: compare by row IDs first, assuming rows have stable IDs.
|
|
118
|
+
for (let i = 0; i < a.length; i++) {
|
|
119
|
+
if (a[i]?.id !== b[i]?.id) return false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// If IDs match, do deep comparison of row objects
|
|
123
|
+
// This catches cases where row data changed but ID stayed the same,
|
|
124
|
+
// including changes in nested objects (e.g., "contact.company")
|
|
125
|
+
for (let i = 0; i < a.length; i++) {
|
|
126
|
+
const rowA = a[i];
|
|
127
|
+
const rowB = b[i];
|
|
128
|
+
|
|
129
|
+
// Fast path: same reference
|
|
130
|
+
if (rowA === rowB) continue;
|
|
131
|
+
|
|
132
|
+
// Deep comparison to handle nested objects and arrays
|
|
133
|
+
if (!deepEqual(rowA, rowB)) return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const PATH_SPLIT_REGEX = /[.[\]'"]/;
|
|
140
|
+
const MAX_PATH_CACHE_SIZE = 1000;
|
|
141
|
+
const pathCache = new Map();
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Resolves a nested property path in an object.
|
|
145
|
+
* Supports both direct property access and nested paths like "contact.company".
|
|
146
|
+
* @template {Record<string, unknown>} T
|
|
147
|
+
* @param {T} object - The object to resolve the path from
|
|
148
|
+
* @param {string} path - The property path (e.g., "name" or "contact.company")
|
|
149
|
+
* @returns {unknown} The resolved value, or undefined if the path doesn't exist
|
|
150
|
+
*/
|
|
151
|
+
export function resolvePath(object, path) {
|
|
152
|
+
if (path in object) return object[path];
|
|
153
|
+
|
|
154
|
+
let segments = pathCache.get(path);
|
|
155
|
+
if (!segments) {
|
|
156
|
+
segments = path.split(PATH_SPLIT_REGEX).filter((p) => p);
|
|
157
|
+
if (segments.length > 1) {
|
|
158
|
+
if (pathCache.size >= MAX_PATH_CACHE_SIZE) {
|
|
159
|
+
const firstKey = pathCache.keys().next().value;
|
|
160
|
+
pathCache.delete(firstKey);
|
|
161
|
+
}
|
|
162
|
+
pathCache.set(path, segments);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return segments.reduce(
|
|
167
|
+
(o, p) => (o && typeof o === "object" ? o[p] : o),
|
|
168
|
+
object,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Paginates an array of rows based on page number and page size.
|
|
174
|
+
* @template {Record<string, unknown>} Row
|
|
175
|
+
* @param {ReadonlyArray<Row>} rows - The rows to paginate
|
|
176
|
+
* @param {number} page - The current page number (1-indexed)
|
|
177
|
+
* @param {number} pageSize - The number of items per page
|
|
178
|
+
* @returns {ReadonlyArray<Row>} The paginated rows, or all rows if pagination is disabled
|
|
179
|
+
*/
|
|
180
|
+
export function getDisplayedRows(rows, page, pageSize) {
|
|
181
|
+
if (page && pageSize) {
|
|
182
|
+
return rows.slice((page - 1) * pageSize, page * pageSize);
|
|
183
|
+
}
|
|
184
|
+
return rows;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Formats header width styles for table headers.
|
|
189
|
+
* Combines width and minWidth into a CSS style string.
|
|
190
|
+
* @template {object} Header
|
|
191
|
+
* @param {Header & { width?: string | null | number; minWidth?: string | null | number; [key: string]: unknown }} header - The header object
|
|
192
|
+
* @returns {string | undefined} The formatted style string, or undefined if no width styles
|
|
193
|
+
*/
|
|
194
|
+
export function formatHeaderWidth(header) {
|
|
195
|
+
const styles = [
|
|
196
|
+
header.width && `width: ${header.width}`,
|
|
197
|
+
header.minWidth && `min-width: ${header.minWidth}`,
|
|
198
|
+
].filter(Boolean);
|
|
199
|
+
if (styles.length === 0) return undefined;
|
|
200
|
+
return styles.join(";");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Compares two values for sorting in a data table.
|
|
205
|
+
* Handles numbers, strings, null/undefined values, and custom sort functions.
|
|
206
|
+
* @template T
|
|
207
|
+
* @param {T} itemA - First value to compare
|
|
208
|
+
* @param {T} itemB - Second value to compare
|
|
209
|
+
* @param {boolean} ascending - Whether to sort in ascending order
|
|
210
|
+
* @param {((a: T, b: T) => number) | false | undefined} customSort - Optional custom sort function
|
|
211
|
+
* @returns {number} Negative if a < b (ascending) or a > b (descending), positive if a > b (ascending) or a < b (descending), 0 if equal
|
|
212
|
+
*/
|
|
213
|
+
export function compareValues(itemA, itemB, ascending, customSort) {
|
|
214
|
+
if (customSort) return customSort(itemA, itemB);
|
|
215
|
+
|
|
216
|
+
let result;
|
|
217
|
+
|
|
218
|
+
// Fast path: numeric comparison
|
|
219
|
+
if (typeof itemA === "number" && typeof itemB === "number") {
|
|
220
|
+
result = itemA - itemB;
|
|
221
|
+
} else {
|
|
222
|
+
// Handle null/undefined values
|
|
223
|
+
if ([itemA, itemB].every((item) => !item && item !== 0)) {
|
|
224
|
+
result = 0;
|
|
225
|
+
} else if (!itemA && itemA !== 0) {
|
|
226
|
+
result = 1;
|
|
227
|
+
} else if (!itemB && itemB !== 0) {
|
|
228
|
+
result = -1;
|
|
229
|
+
} else {
|
|
230
|
+
// String comparison with locale-aware numeric sorting
|
|
231
|
+
result = String(itemA).localeCompare(String(itemB), "en", {
|
|
232
|
+
numeric: true,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Reverse result for descending order
|
|
238
|
+
return ascending ? result : -result;
|
|
239
|
+
}
|
|
@@ -27,6 +27,9 @@
|
|
|
27
27
|
*/
|
|
28
28
|
const selectedValue = writable(selected);
|
|
29
29
|
|
|
30
|
+
let prevSelectedValue = undefined;
|
|
31
|
+
let isInitialRender = true;
|
|
32
|
+
|
|
30
33
|
/**
|
|
31
34
|
* @type {(value: string) => void}
|
|
32
35
|
*/
|
|
@@ -40,7 +43,13 @@
|
|
|
40
43
|
});
|
|
41
44
|
|
|
42
45
|
$: selected = $selectedValue;
|
|
43
|
-
$:
|
|
46
|
+
$: {
|
|
47
|
+
if (!isInitialRender && prevSelectedValue !== $selectedValue) {
|
|
48
|
+
dispatch("change", $selectedValue);
|
|
49
|
+
}
|
|
50
|
+
prevSelectedValue = $selectedValue;
|
|
51
|
+
isInitialRender = false;
|
|
52
|
+
}
|
|
44
53
|
</script>
|
|
45
54
|
|
|
46
55
|
<!-- svelte-ignore a11y-mouse-events-have-key-events -->
|
|
@@ -78,6 +78,8 @@
|
|
|
78
78
|
*/
|
|
79
79
|
const tooltipOpen = writable(open);
|
|
80
80
|
|
|
81
|
+
let prevOpen = undefined;
|
|
82
|
+
|
|
81
83
|
setContext("Tooltip", { tooltipOpen });
|
|
82
84
|
|
|
83
85
|
function onKeydown(e) {
|
|
@@ -175,7 +177,12 @@
|
|
|
175
177
|
});
|
|
176
178
|
|
|
177
179
|
$: tooltipOpen.set(open);
|
|
178
|
-
$:
|
|
180
|
+
$: {
|
|
181
|
+
if (prevOpen !== undefined) {
|
|
182
|
+
dispatch(open ? "open" : "close");
|
|
183
|
+
}
|
|
184
|
+
prevOpen = open;
|
|
185
|
+
}
|
|
179
186
|
$: buttonProps = {
|
|
180
187
|
role: "button",
|
|
181
188
|
"aria-haspopup": "true",
|
|
@@ -25,6 +25,38 @@
|
|
|
25
25
|
|
|
26
26
|
return null;
|
|
27
27
|
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Creates a TreeWalker instance for keyboard navigation.
|
|
31
|
+
* @param {HTMLElement} root - The root element to traverse
|
|
32
|
+
* @returns {TreeWalker} A TreeWalker configured to navigate tree nodes
|
|
33
|
+
*/
|
|
34
|
+
function createTreeWalkerInstance(root) {
|
|
35
|
+
return document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, {
|
|
36
|
+
acceptNode: (node) => {
|
|
37
|
+
if (node.classList.contains("bx--tree-node--disabled"))
|
|
38
|
+
return NodeFilter.FILTER_REJECT;
|
|
39
|
+
if (node.matches("li.bx--tree-node")) return NodeFilter.FILTER_ACCEPT;
|
|
40
|
+
return NodeFilter.FILTER_SKIP;
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Recursively flattens a tree of nodes into a single array
|
|
47
|
+
* @template {object} Node
|
|
48
|
+
* @param {ReadonlyArray<Node & { nodes?: Node[] }>} nodes
|
|
49
|
+
* @returns {Array<Node>}
|
|
50
|
+
*/
|
|
51
|
+
function traverse(nodes) {
|
|
52
|
+
return nodes.reduce((acc, node) => {
|
|
53
|
+
acc.push(node);
|
|
54
|
+
if (Array.isArray(node.nodes) && node.nodes.length > 0) {
|
|
55
|
+
acc.push(...traverse(node.nodes));
|
|
56
|
+
}
|
|
57
|
+
return acc;
|
|
58
|
+
}, []);
|
|
59
|
+
}
|
|
28
60
|
</script>
|
|
29
61
|
|
|
30
62
|
<script>
|
|
@@ -98,7 +130,9 @@
|
|
|
98
130
|
* ```
|
|
99
131
|
*/
|
|
100
132
|
export function expandAll() {
|
|
101
|
-
|
|
133
|
+
expandedIdsSet = new Set(nodeIds);
|
|
134
|
+
expandedIds = Array.from(expandedIdsSet);
|
|
135
|
+
lastExpandedIdsRef = expandedIds;
|
|
102
136
|
}
|
|
103
137
|
|
|
104
138
|
/**
|
|
@@ -111,7 +145,9 @@
|
|
|
111
145
|
* ```
|
|
112
146
|
*/
|
|
113
147
|
export function collapseAll() {
|
|
148
|
+
expandedIdsSet.clear();
|
|
114
149
|
expandedIds = [];
|
|
150
|
+
lastExpandedIdsRef = expandedIds;
|
|
115
151
|
}
|
|
116
152
|
|
|
117
153
|
/**
|
|
@@ -128,13 +164,16 @@
|
|
|
128
164
|
* ```
|
|
129
165
|
*/
|
|
130
166
|
export function expandNodes(filterNode = (node) => false) {
|
|
131
|
-
|
|
167
|
+
const nodesToExpand = flattenedNodes
|
|
132
168
|
.filter(
|
|
133
169
|
(node) =>
|
|
134
170
|
filterNode(node) ||
|
|
135
171
|
node.nodes?.some((child) => filterNode(child) && child.nodes),
|
|
136
172
|
)
|
|
137
173
|
.map((node) => node.id);
|
|
174
|
+
nodesToExpand.forEach((id) => expandedIdsSet.add(id));
|
|
175
|
+
expandedIds = Array.from(expandedIdsSet);
|
|
176
|
+
lastExpandedIdsRef = expandedIds;
|
|
138
177
|
}
|
|
139
178
|
|
|
140
179
|
/**
|
|
@@ -151,9 +190,13 @@
|
|
|
151
190
|
* ```
|
|
152
191
|
*/
|
|
153
192
|
export function collapseNodes(filterNode = (node) => true) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
193
|
+
flattenedNodes.forEach((node) => {
|
|
194
|
+
if (expandedIdsSet.has(node.id) && filterNode(node)) {
|
|
195
|
+
expandedIdsSet.delete(node.id);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
expandedIds = Array.from(expandedIdsSet);
|
|
199
|
+
lastExpandedIdsRef = expandedIds;
|
|
157
200
|
}
|
|
158
201
|
|
|
159
202
|
/**
|
|
@@ -181,18 +224,21 @@
|
|
|
181
224
|
const { expand = true, select = true, focus = true } = options;
|
|
182
225
|
|
|
183
226
|
for (const child of nodes) {
|
|
184
|
-
const
|
|
227
|
+
const path = findNodeById(child, id);
|
|
185
228
|
|
|
186
|
-
if (
|
|
187
|
-
const ids =
|
|
188
|
-
const
|
|
229
|
+
if (path) {
|
|
230
|
+
const ids = path.map((node) => node.id);
|
|
231
|
+
const lastId = ids[ids.length - 1];
|
|
189
232
|
|
|
190
233
|
if (expand) {
|
|
191
|
-
|
|
234
|
+
const ancestorIds = ids.slice(0, -1);
|
|
235
|
+
for (const ancestorId of ancestorIds) {
|
|
236
|
+
expandedIdsSet.add(ancestorId);
|
|
237
|
+
}
|
|
238
|
+
expandedIds = Array.from(expandedIdsSet);
|
|
239
|
+
lastExpandedIdsRef = expandedIds;
|
|
192
240
|
}
|
|
193
241
|
|
|
194
|
-
const lastId = ids[ids.length - 1];
|
|
195
|
-
|
|
196
242
|
if (select) {
|
|
197
243
|
activeId = lastId;
|
|
198
244
|
selectedIds = [lastId];
|
|
@@ -215,57 +261,58 @@
|
|
|
215
261
|
|
|
216
262
|
const dispatch = createEventDispatcher();
|
|
217
263
|
const labelId = `label-${Math.random().toString(36)}`;
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
*/
|
|
264
|
+
|
|
265
|
+
/** @type {import("svelte/store").Writable<TreeNodeId>} */
|
|
221
266
|
const activeNodeId = writable(activeId);
|
|
222
|
-
/**
|
|
223
|
-
* @type {import("svelte/store").Writable<ReadonlyArray<TreeNodeId>>}
|
|
224
|
-
*/
|
|
267
|
+
/** @type {import("svelte/store").Writable<ReadonlyArray<TreeNodeId>>} */
|
|
225
268
|
const selectedNodeIds = writable(selectedIds);
|
|
226
|
-
/**
|
|
227
|
-
* @type {import("svelte/store").Writable<ReadonlyArray<TreeNodeId>>}
|
|
228
|
-
*/
|
|
269
|
+
/** @type {import("svelte/store").Writable<ReadonlyArray<TreeNodeId>>} */
|
|
229
270
|
const expandedNodeIds = writable(expandedIds);
|
|
230
271
|
|
|
272
|
+
/** @type {HTMLElement | null} */
|
|
231
273
|
let ref = null;
|
|
274
|
+
/** @type {TreeWalker | null} */
|
|
232
275
|
let treeWalker = null;
|
|
233
276
|
|
|
234
|
-
/**
|
|
235
|
-
|
|
236
|
-
|
|
277
|
+
/** @type {ReadonlyArray<Node> | null} */
|
|
278
|
+
let cachedNodes = null;
|
|
279
|
+
/** @type {Array<Node> | null} */
|
|
280
|
+
let cachedFlattenedNodes = null;
|
|
281
|
+
/** @type {Array<TreeNodeId> | null} */
|
|
282
|
+
let cachedNodeIds = null;
|
|
283
|
+
|
|
284
|
+
/** @type {Set<TreeNodeId>} */
|
|
285
|
+
let expandedIdsSet = new Set(expandedIds);
|
|
286
|
+
/** @type {ReadonlyArray<TreeNodeId>} */
|
|
287
|
+
let lastExpandedIdsRef = expandedIds;
|
|
288
|
+
|
|
289
|
+
/** @type {(node: TreeNode) => void} */
|
|
237
290
|
const clickNode = (node) => {
|
|
238
291
|
activeId = node.id;
|
|
239
292
|
selectedIds = [node.id];
|
|
240
293
|
dispatch("select", node);
|
|
241
294
|
};
|
|
242
295
|
|
|
243
|
-
/**
|
|
244
|
-
* @type {(node: TreeNode) => void}
|
|
245
|
-
*/
|
|
296
|
+
/** @type {(node: TreeNode) => void} */
|
|
246
297
|
const selectNode = (node) => {
|
|
247
298
|
selectedIds = [node.id];
|
|
248
299
|
};
|
|
249
300
|
|
|
250
|
-
/**
|
|
251
|
-
* @type {(node: TreeNode, expanded: boolean) => void}
|
|
252
|
-
*/
|
|
301
|
+
/** @type {(node: TreeNode, expanded: boolean) => void} */
|
|
253
302
|
const expandNode = (node, expanded) => {
|
|
254
303
|
if (expanded) {
|
|
255
|
-
|
|
304
|
+
expandedIdsSet.add(node.id);
|
|
256
305
|
} else {
|
|
257
|
-
|
|
306
|
+
expandedIdsSet.delete(node.id);
|
|
258
307
|
}
|
|
308
|
+
expandedIds = Array.from(expandedIdsSet);
|
|
309
|
+
lastExpandedIdsRef = expandedIds;
|
|
259
310
|
};
|
|
260
311
|
|
|
261
|
-
/**
|
|
262
|
-
* @type {(node: TreeNode) => void}
|
|
263
|
-
*/
|
|
312
|
+
/** @type {(node: TreeNode) => void} */
|
|
264
313
|
const focusNode = (node) => dispatch("focus", node);
|
|
265
314
|
|
|
266
|
-
/**
|
|
267
|
-
* @type {(node: TreeNode) => void}
|
|
268
|
-
*/
|
|
315
|
+
/** @type {(node: TreeNode) => void} */
|
|
269
316
|
const toggleNode = (node) => dispatch("toggle", node);
|
|
270
317
|
|
|
271
318
|
setContext("TreeView", {
|
|
@@ -302,37 +349,33 @@
|
|
|
302
349
|
if (firstFocusableNode != null) {
|
|
303
350
|
firstFocusableNode.tabIndex = "0";
|
|
304
351
|
}
|
|
352
|
+
|
|
353
|
+
if (ref && !treeWalker) {
|
|
354
|
+
treeWalker = createTreeWalkerInstance(ref);
|
|
355
|
+
}
|
|
305
356
|
});
|
|
306
357
|
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
*/
|
|
312
|
-
function traverse(nodes) {
|
|
313
|
-
return nodes.reduce((acc, node) => {
|
|
314
|
-
acc.push(node);
|
|
315
|
-
if (Array.isArray(node.nodes) && node.nodes.length > 0) {
|
|
316
|
-
acc.push(...traverse(node.nodes));
|
|
317
|
-
}
|
|
318
|
-
return acc;
|
|
319
|
-
}, []);
|
|
358
|
+
$: if (nodes !== cachedNodes) {
|
|
359
|
+
cachedNodes = nodes;
|
|
360
|
+
cachedFlattenedNodes = traverse(nodes);
|
|
361
|
+
cachedNodeIds = cachedFlattenedNodes.map((node) => node.id);
|
|
320
362
|
}
|
|
321
363
|
|
|
322
|
-
$: flattenedNodes =
|
|
323
|
-
$: nodeIds =
|
|
324
|
-
|
|
325
|
-
$:
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
364
|
+
$: flattenedNodes = cachedFlattenedNodes ?? [];
|
|
365
|
+
$: nodeIds = cachedNodeIds ?? [];
|
|
366
|
+
|
|
367
|
+
$: {
|
|
368
|
+
if (expandedIds !== lastExpandedIdsRef) {
|
|
369
|
+
expandedIdsSet = new Set(expandedIds);
|
|
370
|
+
lastExpandedIdsRef = expandedIds;
|
|
371
|
+
}
|
|
372
|
+
activeNodeId.set(activeId);
|
|
373
|
+
selectedNodeIds.set(selectedIds);
|
|
374
|
+
expandedNodeIds.set(expandedIds);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
$: if (ref && (nodes !== cachedNodes || !treeWalker)) {
|
|
378
|
+
treeWalker = createTreeWalkerInstance(ref);
|
|
336
379
|
}
|
|
337
380
|
</script>
|
|
338
381
|
|
|
@@ -5,7 +5,7 @@ export type ContentSwitcherContext = {
|
|
|
5
5
|
currentId: import("svelte/store").Writable<string | null>;
|
|
6
6
|
add: (data: { id: string; text: string; selected: boolean }) => void;
|
|
7
7
|
update: (id: string) => void;
|
|
8
|
-
change: (direction: number) => void
|
|
8
|
+
change: (direction: number) => Promise<void>;
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
type $RestProps = SvelteHTMLElements["div"];
|
|
@@ -6,8 +6,8 @@ type Join<K, P> = K extends string | number
|
|
|
6
6
|
: never
|
|
7
7
|
: never;
|
|
8
8
|
|
|
9
|
-
// For performance, the maximum traversal depth is
|
|
10
|
-
export type PropertyPath<T, D extends number =
|
|
9
|
+
// For performance, the maximum traversal depth is 3.
|
|
10
|
+
export type PropertyPath<T, D extends number = 3> = [D] extends [never]
|
|
11
11
|
? never
|
|
12
12
|
: T extends object
|
|
13
13
|
? {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight deep equality check optimized for DataTable rows.
|
|
3
|
+
* Compares arrays of row objects by first checking IDs (fast path),
|
|
4
|
+
* then falling back to deep object comparison to handle nested structures.
|
|
5
|
+
*/
|
|
6
|
+
export function rowsEqual<Row extends Record<string, any>>(
|
|
7
|
+
a: ReadonlyArray<Row> | null,
|
|
8
|
+
b: ReadonlyArray<Row> | null,
|
|
9
|
+
): boolean;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolves a nested property path in an object.
|
|
13
|
+
* Supports both direct property access and nested paths like "contact.company".
|
|
14
|
+
*/
|
|
15
|
+
export function resolvePath<T extends Record<string, unknown>>(
|
|
16
|
+
object: T,
|
|
17
|
+
path: string,
|
|
18
|
+
): unknown;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Paginates an array of rows based on page number and page size.
|
|
22
|
+
*/
|
|
23
|
+
export function getDisplayedRows<Row extends Record<string, unknown>>(
|
|
24
|
+
rows: ReadonlyArray<Row>,
|
|
25
|
+
page: number,
|
|
26
|
+
pageSize: number,
|
|
27
|
+
): ReadonlyArray<Row>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Formats header width styles for table headers.
|
|
31
|
+
* Combines width and minWidth into a CSS style string.
|
|
32
|
+
*/
|
|
33
|
+
export function formatHeaderWidth<
|
|
34
|
+
Header extends {
|
|
35
|
+
width?: string | null | number;
|
|
36
|
+
minWidth?: string | null | number;
|
|
37
|
+
[key: string]: unknown;
|
|
38
|
+
} = {
|
|
39
|
+
width?: string | null | number;
|
|
40
|
+
minWidth?: string | null | number;
|
|
41
|
+
[key: string]: unknown;
|
|
42
|
+
},
|
|
43
|
+
>(header: Header): string | undefined;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Compares two values for sorting in a data table.
|
|
47
|
+
* Handles numbers, strings, null/undefined values, and custom sort functions.
|
|
48
|
+
*/
|
|
49
|
+
export function compareValues<T = unknown>(
|
|
50
|
+
itemA: T,
|
|
51
|
+
itemB: T,
|
|
52
|
+
ascending: boolean,
|
|
53
|
+
customSort?: ((a: T, b: T) => number) | false | undefined,
|
|
54
|
+
): number;
|