carbon-components-svelte 0.98.0 → 0.98.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/package.json +1 -1
- package/src/Checkbox/Checkbox.svelte +17 -3
- package/src/Checkbox/InlineCheckbox.svelte +1 -1
- package/src/CodeSnippet/CodeSnippet.svelte +2 -2
- package/src/ComboBox/ComboBox.svelte +12 -7
- package/src/ContainedList/ContainedList.svelte +1 -1
- package/src/ContentSwitcher/ContentSwitcher.svelte +15 -3
- package/src/ContentSwitcher/Switch.svelte +2 -8
- package/src/ContextMenu/ContextMenuOption.svelte +1 -1
- package/src/DataTable/DataTable.svelte +12 -45
- package/src/DataTable/DataTableTypes.d.ts +2 -2
- package/src/DataTable/TableHeader.svelte +1 -1
- 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/DatePicker/DatePicker.svelte +1 -1
- package/src/DatePicker/DatePickerInput.svelte +1 -1
- package/src/DatePicker/DatePickerSkeleton.svelte +1 -1
- package/src/Dropdown/Dropdown.svelte +1 -1
- package/src/FileUploader/FileUploaderButton.svelte +1 -1
- package/src/FileUploader/FileUploaderDropContainer.svelte +1 -1
- package/src/FileUploader/FileUploaderItem.svelte +1 -1
- package/src/FormLabel/FormLabel.svelte +1 -1
- package/src/ImageLoader/ImageLoader.svelte +2 -2
- package/src/ListBox/ListBoxField.svelte +1 -1
- package/src/ListBox/ListBoxMenu.svelte +1 -1
- package/src/Modal/Modal.svelte +1 -1
- package/src/MultiSelect/MultiSelect.svelte +2 -1
- package/src/Notification/InlineNotification.svelte +1 -1
- package/src/Notification/NotificationQueue.svelte +4 -4
- package/src/Notification/ToastNotification.svelte +1 -1
- package/src/NumberInput/NumberInput.svelte +1 -1
- package/src/OverflowMenu/OverflowMenu.svelte +6 -6
- package/src/OverflowMenu/OverflowMenuItem.svelte +1 -1
- package/src/Pagination/Pagination.svelte +1 -1
- package/src/ProgressBar/ProgressBar.svelte +2 -2
- package/src/ProgressIndicator/ProgressStep.svelte +1 -1
- package/src/RadioButton/RadioButton.svelte +1 -1
- package/src/Search/Search.svelte +1 -1
- package/src/Select/Select.svelte +1 -1
- package/src/Select/SelectItem.svelte +1 -1
- package/src/Slider/Slider.svelte +1 -1
- package/src/StructuredList/StructuredList.svelte +10 -1
- package/src/StructuredList/StructuredListInput.svelte +1 -1
- package/src/Tabs/Tab.svelte +1 -1
- package/src/Tabs/TabContent.svelte +1 -1
- package/src/Tag/Tag.svelte +1 -1
- package/src/TextArea/TextArea.svelte +1 -1
- package/src/TextInput/PasswordInput.svelte +1 -1
- package/src/TextInput/TextInput.svelte +1 -1
- package/src/Tile/ExpandableTile.svelte +1 -1
- package/src/Tile/RadioTile.svelte +1 -1
- package/src/Tile/SelectableTile.svelte +1 -1
- package/src/TimePicker/TimePicker.svelte +1 -1
- package/src/TimePicker/TimePickerSelect.svelte +1 -1
- package/src/Toggle/Toggle.svelte +1 -1
- package/src/Toggle/ToggleSkeleton.svelte +1 -1
- package/src/Tooltip/Tooltip.svelte +12 -5
- package/src/TooltipDefinition/TooltipDefinition.svelte +1 -1
- package/src/TooltipIcon/TooltipIcon.svelte +1 -1
- package/src/TreeView/TreeView.svelte +112 -74
- package/src/TreeView/TreeViewNode.svelte +3 -3
- package/src/UIShell/Header.svelte +2 -2
- package/src/UIShell/HeaderNavItem.svelte +1 -1
- package/types/Checkbox/Checkbox.svelte.d.ts +3 -1
- package/types/Checkbox/InlineCheckbox.svelte.d.ts +1 -1
- package/types/CodeSnippet/CodeSnippet.svelte.d.ts +1 -1
- package/types/ComboBox/ComboBox.svelte.d.ts +6 -3
- package/types/ContainedList/ContainedList.svelte.d.ts +5 -1
- package/types/ContainedList/ContainedListItem.svelte.d.ts +2 -0
- package/types/ContentSwitcher/ContentSwitcher.svelte.d.ts +1 -1
- package/types/ContentSwitcher/Switch.svelte.d.ts +1 -1
- package/types/ContextMenu/ContextMenuOption.svelte.d.ts +3 -1
- package/types/DataTable/DataTable.svelte.d.ts +11 -1
- package/types/DataTable/DataTableTypes.d.ts +2 -2
- package/types/DataTable/TableHeader.svelte.d.ts +1 -1
- package/types/DataTable/ToolbarBatchActions.svelte.d.ts +2 -0
- package/types/DataTable/data-table-utils.d.ts +54 -0
- package/types/DatePicker/DatePicker.svelte.d.ts +1 -1
- package/types/DatePicker/DatePickerInput.svelte.d.ts +3 -1
- package/types/DatePicker/DatePickerSkeleton.svelte.d.ts +1 -1
- package/types/Dropdown/Dropdown.svelte.d.ts +1 -1
- package/types/FileUploader/FileUploaderButton.svelte.d.ts +3 -1
- package/types/FileUploader/FileUploaderDropContainer.svelte.d.ts +3 -1
- package/types/FileUploader/FileUploaderItem.svelte.d.ts +1 -1
- package/types/FormLabel/FormLabel.svelte.d.ts +1 -1
- package/types/ImageLoader/ImageLoader.svelte.d.ts +2 -1
- package/types/ListBox/ListBoxField.svelte.d.ts +1 -1
- package/types/ListBox/ListBoxMenu.svelte.d.ts +1 -1
- package/types/Modal/Modal.svelte.d.ts +5 -1
- package/types/MultiSelect/MultiSelect.svelte.d.ts +3 -1
- package/types/Notification/InlineNotification.svelte.d.ts +7 -1
- package/types/Notification/NotificationQueue.svelte.d.ts +2 -0
- package/types/Notification/ToastNotification.svelte.d.ts +7 -1
- package/types/NumberInput/NumberInput.svelte.d.ts +3 -1
- package/types/OverflowMenu/OverflowMenu.svelte.d.ts +3 -1
- package/types/OverflowMenu/OverflowMenuItem.svelte.d.ts +1 -1
- package/types/Pagination/Pagination.svelte.d.ts +1 -1
- package/types/ProgressBar/ProgressBar.svelte.d.ts +3 -1
- package/types/ProgressIndicator/ProgressStep.svelte.d.ts +1 -1
- package/types/RadioButton/RadioButton.svelte.d.ts +3 -1
- package/types/RadioButtonGroup/RadioButtonGroup.svelte.d.ts +2 -0
- package/types/Search/Search.svelte.d.ts +3 -1
- package/types/Select/Select.svelte.d.ts +3 -1
- package/types/Slider/Slider.svelte.d.ts +3 -1
- package/types/StructuredList/StructuredListInput.svelte.d.ts +1 -1
- package/types/Tabs/Tab.svelte.d.ts +1 -1
- package/types/Tabs/TabContent.svelte.d.ts +1 -1
- package/types/Tabs/Tabs.svelte.d.ts +2 -0
- package/types/Tag/Tag.svelte.d.ts +1 -1
- package/types/TextArea/TextArea.svelte.d.ts +3 -1
- package/types/TextInput/PasswordInput.svelte.d.ts +3 -1
- package/types/TextInput/TextInput.svelte.d.ts +3 -1
- package/types/Tile/ExpandableTile.svelte.d.ts +5 -1
- package/types/Tile/RadioTile.svelte.d.ts +1 -1
- package/types/Tile/SelectableTile.svelte.d.ts +1 -1
- package/types/Tile/SelectableTileGroup.svelte.d.ts +2 -0
- package/types/Tile/TileGroup.svelte.d.ts +2 -0
- package/types/TimePicker/TimePicker.svelte.d.ts +3 -1
- package/types/TimePicker/TimePickerSelect.svelte.d.ts +3 -1
- package/types/Toggle/Toggle.svelte.d.ts +3 -1
- package/types/Toggle/ToggleSkeleton.svelte.d.ts +3 -1
- package/types/Tooltip/Tooltip.svelte.d.ts +2 -2
- package/types/TooltipDefinition/TooltipDefinition.svelte.d.ts +3 -1
- package/types/TooltipIcon/TooltipIcon.svelte.d.ts +1 -1
- package/types/TreeView/TreeView.svelte.d.ts +6 -2
- package/types/TreeView/TreeViewNode.svelte.d.ts +2 -1
- package/types/UIShell/Header.svelte.d.ts +6 -0
- package/types/UIShell/HeaderAction.svelte.d.ts +2 -0
package/package.json
CHANGED
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
export let title = undefined;
|
|
54
54
|
|
|
55
55
|
/** Set an id for the input label */
|
|
56
|
-
export let id =
|
|
56
|
+
export let id = `ccs-${Math.random().toString(36)}`;
|
|
57
57
|
|
|
58
58
|
/** Obtain a reference to the input HTML element */
|
|
59
59
|
export let ref = null;
|
|
@@ -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
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
export let title = undefined;
|
|
13
13
|
|
|
14
14
|
/** Set an id for the input label */
|
|
15
|
-
export let id =
|
|
15
|
+
export let id = `ccs-${Math.random().toString(36)}`;
|
|
16
16
|
|
|
17
17
|
/** Obtain a reference to the input HTML element */
|
|
18
18
|
export let ref = null;
|
|
@@ -103,7 +103,7 @@
|
|
|
103
103
|
export let showMoreLess = true;
|
|
104
104
|
|
|
105
105
|
/** Set an id for the code element */
|
|
106
|
-
export let id =
|
|
106
|
+
export let id = `ccs-${Math.random().toString(36)}`;
|
|
107
107
|
|
|
108
108
|
/** Obtain a reference to the pre HTML element */
|
|
109
109
|
export let ref = null;
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
|
|
128
128
|
$: expandText = expanded ? showLessText : showMoreText;
|
|
129
129
|
$: minHeight = expanded ? 16 * 15 : 48;
|
|
130
|
-
$: maxHeight = expanded ? "none" : 16 * 15
|
|
130
|
+
$: maxHeight = expanded ? "none" : `${16 * 15}px`;
|
|
131
131
|
|
|
132
132
|
// Show more/less only applies to multi-line code snippets
|
|
133
133
|
$: if (type !== "multi") showMoreLess = false;
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
export let translateWithIdSelection = undefined;
|
|
121
121
|
|
|
122
122
|
/** Set an id for the list box component */
|
|
123
|
-
export let id =
|
|
123
|
+
export let id = `ccs-${Math.random().toString(36)}`;
|
|
124
124
|
|
|
125
125
|
/**
|
|
126
126
|
* Specify a name attribute for the input.
|
|
@@ -200,10 +200,9 @@
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
/**
|
|
203
|
-
* Clear the combo box programmatically
|
|
204
|
-
*
|
|
205
|
-
* @
|
|
206
|
-
* @param {boolean} [options.focus=true] - Whether to focus the combo box after clearing
|
|
203
|
+
* Clear the combo box programmatically.
|
|
204
|
+
* By default, focuses the combo box after clearing. Set `options.focus` to `false` to prevent focusing.
|
|
205
|
+
* @type {(options?: { focus?: boolean; }) => Promise<void>}
|
|
207
206
|
* @example
|
|
208
207
|
* ```svelte
|
|
209
208
|
* <ComboBox bind:this={comboBox} items={items} />
|
|
@@ -211,7 +210,7 @@
|
|
|
211
210
|
* <button on:click={() => comboBox.clear({ focus: false })}>Clear (No Focus)</button>
|
|
212
211
|
* ```
|
|
213
212
|
*/
|
|
214
|
-
export function clear(options = {}) {
|
|
213
|
+
export async function clear(options = {}) {
|
|
215
214
|
prevSelectedId = null;
|
|
216
215
|
highlightedIndex = -1;
|
|
217
216
|
highlightedId = undefined;
|
|
@@ -219,6 +218,8 @@
|
|
|
219
218
|
selectedItem = undefined;
|
|
220
219
|
open = false;
|
|
221
220
|
value = "";
|
|
221
|
+
// Ensure binding updates are complete before focusing.
|
|
222
|
+
await tick();
|
|
222
223
|
if (options?.focus !== false) ref?.focus();
|
|
223
224
|
}
|
|
224
225
|
|
|
@@ -262,6 +263,8 @@
|
|
|
262
263
|
|
|
263
264
|
$: if (selectedId !== undefined) {
|
|
264
265
|
if (prevSelectedId !== selectedId) {
|
|
266
|
+
// Only dispatch select event if not initial render (prevSelectedId was not null)
|
|
267
|
+
const isInitialRender = prevSelectedId === null;
|
|
265
268
|
prevSelectedId = selectedId;
|
|
266
269
|
if (filteredItems?.length === 1 && open) {
|
|
267
270
|
selectedId = filteredItems[0].id;
|
|
@@ -271,7 +274,9 @@
|
|
|
271
274
|
} else {
|
|
272
275
|
selectedItem = items.find((item) => item.id === selectedId);
|
|
273
276
|
}
|
|
274
|
-
|
|
277
|
+
if (!isInitialRender) {
|
|
278
|
+
dispatch("select", { selectedId, selectedItem });
|
|
279
|
+
}
|
|
275
280
|
}
|
|
276
281
|
} else {
|
|
277
282
|
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"}
|
|
@@ -18,12 +18,12 @@
|
|
|
18
18
|
export let disabled = false;
|
|
19
19
|
|
|
20
20
|
/** Set an id for the button element */
|
|
21
|
-
export let id =
|
|
21
|
+
export let id = `ccs-${Math.random().toString(36)}`;
|
|
22
22
|
|
|
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
|
});
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
* Specify the id.
|
|
56
56
|
* It's recommended to provide an id as a value to bind to within a selectable/radio menu group.
|
|
57
57
|
*/
|
|
58
|
-
export let id =
|
|
58
|
+
export let id = `ccs-${Math.random().toString(36)}`;
|
|
59
59
|
|
|
60
60
|
/** Obtain a reference to the list item HTML element */
|
|
61
61
|
export let ref = null;
|
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
* When the table is inside a form, this name will
|
|
103
103
|
* be included in the form data on submit.
|
|
104
104
|
*/
|
|
105
|
-
export let inputName =
|
|
105
|
+
export let inputName = `ccs-${Math.random().toString(36)}`;
|
|
106
106
|
|
|
107
107
|
/** Set to `true` to use zebra styles */
|
|
108
108
|
export let zebra = false;
|
|
@@ -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",
|
|
@@ -219,7 +223,7 @@
|
|
|
219
223
|
|
|
220
224
|
// Internal ID prefix for radio buttons, checkboxes, etc.
|
|
221
225
|
// since there may be multiple `DataTable` instances that have overlapping row ids.
|
|
222
|
-
const id =
|
|
226
|
+
const id = `ccs-${Math.random().toString(36)}`;
|
|
223
227
|
|
|
224
228
|
// Store a copy of the original rows for filter restoration.
|
|
225
229
|
$: originalRows = [...rows];
|
|
@@ -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}
|
|
@@ -275,7 +272,7 @@
|
|
|
275
272
|
return searchableKeys.some((key) => {
|
|
276
273
|
const _value = resolvePath(row, key);
|
|
277
274
|
if (typeof _value === "string" || typeof _value === "number") {
|
|
278
|
-
return
|
|
275
|
+
return `${_value}`?.toLowerCase().includes(value);
|
|
279
276
|
}
|
|
280
277
|
return false;
|
|
281
278
|
});
|
|
@@ -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
|
? {
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
export let translateWithId = (id) => defaultTranslations[id];
|
|
32
32
|
|
|
33
33
|
/** Set an id for the top-level element */
|
|
34
|
-
export let id =
|
|
34
|
+
export let id = `ccs-${Math.random().toString(36)}`;
|
|
35
35
|
|
|
36
36
|
import ArrowsVertical from "../icons/ArrowsVertical.svelte";
|
|
37
37
|
import ArrowUp from "../icons/ArrowUp.svelte";
|
|
@@ -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
|
+
}
|