@gradio/dataframe 0.16.5 → 0.17.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.
Files changed (87) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/Dataframe.stories.svelte +202 -9
  3. package/Index.svelte +7 -13
  4. package/dist/Index.svelte +5 -9
  5. package/dist/Index.svelte.d.ts +9 -2
  6. package/dist/shared/CellMenu.svelte +91 -10
  7. package/dist/shared/CellMenu.svelte.d.ts +6 -0
  8. package/dist/shared/CellMenuButton.svelte +45 -0
  9. package/dist/shared/CellMenuButton.svelte.d.ts +16 -0
  10. package/dist/shared/CellMenuIcons.svelte +79 -0
  11. package/dist/shared/EditableCell.svelte +83 -14
  12. package/dist/shared/EditableCell.svelte.d.ts +12 -3
  13. package/dist/shared/EmptyRowButton.svelte +28 -0
  14. package/dist/shared/EmptyRowButton.svelte.d.ts +16 -0
  15. package/dist/shared/RowNumber.svelte +40 -0
  16. package/dist/shared/RowNumber.svelte.d.ts +17 -0
  17. package/dist/shared/Table.svelte +564 -1121
  18. package/dist/shared/Table.svelte.d.ts +4 -0
  19. package/dist/shared/TableCell.svelte +291 -0
  20. package/dist/shared/TableCell.svelte.d.ts +57 -0
  21. package/dist/shared/TableHeader.svelte +239 -0
  22. package/dist/shared/TableHeader.svelte.d.ts +45 -0
  23. package/dist/shared/Toolbar.svelte +18 -8
  24. package/dist/shared/VirtualTable.svelte +66 -19
  25. package/dist/shared/VirtualTable.svelte.d.ts +4 -0
  26. package/dist/shared/context/keyboard_context.d.ts +37 -0
  27. package/dist/shared/context/keyboard_context.js +12 -0
  28. package/dist/shared/context/selection_context.d.ts +32 -0
  29. package/dist/shared/context/selection_context.js +107 -0
  30. package/dist/shared/context/table_context.d.ts +141 -0
  31. package/dist/shared/context/table_context.js +375 -0
  32. package/dist/shared/icons/Padlock.svelte +24 -0
  33. package/dist/shared/icons/Padlock.svelte.d.ts +23 -0
  34. package/dist/shared/icons/SelectionButtons.svelte +85 -0
  35. package/dist/shared/icons/SelectionButtons.svelte.d.ts +18 -0
  36. package/dist/shared/icons/SortArrowDown.svelte +24 -0
  37. package/dist/shared/icons/SortArrowDown.svelte.d.ts +16 -0
  38. package/dist/shared/icons/SortArrowUp.svelte +24 -0
  39. package/dist/shared/icons/SortArrowUp.svelte.d.ts +16 -0
  40. package/dist/shared/icons/SortButtonDown.svelte +14 -0
  41. package/dist/shared/icons/SortButtonDown.svelte.d.ts +23 -0
  42. package/dist/shared/icons/SortButtonUp.svelte +15 -0
  43. package/dist/shared/icons/SortButtonUp.svelte.d.ts +23 -0
  44. package/dist/shared/icons/SortIcon.svelte +46 -68
  45. package/dist/shared/icons/SortIcon.svelte.d.ts +3 -2
  46. package/dist/shared/selection_utils.d.ts +2 -1
  47. package/dist/shared/selection_utils.js +39 -10
  48. package/dist/shared/utils/data_processing.d.ts +13 -0
  49. package/dist/shared/utils/data_processing.js +45 -0
  50. package/dist/shared/utils/drag_utils.d.ts +15 -0
  51. package/dist/shared/utils/drag_utils.js +57 -0
  52. package/dist/shared/utils/keyboard_utils.d.ts +2 -0
  53. package/dist/shared/utils/keyboard_utils.js +186 -0
  54. package/dist/shared/utils/sort_utils.d.ts +22 -3
  55. package/dist/shared/utils/sort_utils.js +44 -24
  56. package/dist/shared/utils/table_utils.d.ts +6 -5
  57. package/dist/shared/utils/table_utils.js +13 -56
  58. package/package.json +7 -7
  59. package/shared/CellMenu.svelte +90 -10
  60. package/shared/CellMenuButton.svelte +46 -0
  61. package/shared/CellMenuIcons.svelte +79 -0
  62. package/shared/EditableCell.svelte +97 -18
  63. package/shared/EmptyRowButton.svelte +29 -0
  64. package/shared/RowNumber.svelte +41 -0
  65. package/shared/Table.svelte +604 -1235
  66. package/shared/TableCell.svelte +324 -0
  67. package/shared/TableHeader.svelte +256 -0
  68. package/shared/Toolbar.svelte +19 -8
  69. package/shared/VirtualTable.svelte +72 -19
  70. package/shared/context/keyboard_context.ts +65 -0
  71. package/shared/context/selection_context.ts +168 -0
  72. package/shared/context/table_context.ts +625 -0
  73. package/shared/icons/Padlock.svelte +24 -0
  74. package/shared/icons/SelectionButtons.svelte +93 -0
  75. package/shared/icons/SortArrowDown.svelte +25 -0
  76. package/shared/icons/SortArrowUp.svelte +25 -0
  77. package/shared/icons/SortButtonDown.svelte +14 -0
  78. package/shared/icons/SortButtonUp.svelte +15 -0
  79. package/shared/icons/SortIcon.svelte +47 -70
  80. package/shared/selection_utils.ts +39 -13
  81. package/shared/utils/data_processing.ts +72 -0
  82. package/shared/utils/drag_utils.ts +92 -0
  83. package/shared/utils/keyboard_utils.ts +238 -0
  84. package/shared/utils/sort_utils.test.ts +262 -14
  85. package/shared/utils/sort_utils.ts +67 -31
  86. package/shared/utils/table_utils.test.ts +66 -45
  87. package/shared/utils/table_utils.ts +16 -86
@@ -0,0 +1,238 @@
1
+ import { dequal } from "dequal/lite";
2
+ import { handle_delete_key } from "../selection_utils";
3
+ import type { KeyboardContext } from "../context/keyboard_context";
4
+ import { tick } from "svelte";
5
+ import { copy_table_data } from "./table_utils";
6
+
7
+ function handle_header_navigation(
8
+ event: KeyboardEvent,
9
+ ctx: KeyboardContext
10
+ ): boolean {
11
+ if (ctx.selected_header === false || ctx.header_edit !== false) return false;
12
+ switch (event.key) {
13
+ case "ArrowDown":
14
+ ctx.df_actions.set_selected_header(false);
15
+ ctx.df_actions.set_selected([0, ctx.selected_header]);
16
+ ctx.df_actions.set_selected_cells([[0, ctx.selected_header]]);
17
+ return true;
18
+ case "ArrowLeft":
19
+ ctx.df_actions.set_selected_header(
20
+ ctx.selected_header > 0 ? ctx.selected_header - 1 : ctx.selected_header
21
+ );
22
+ return true;
23
+ case "ArrowRight":
24
+ ctx.df_actions.set_selected_header(
25
+ ctx.selected_header < ctx.headers.length - 1
26
+ ? ctx.selected_header + 1
27
+ : ctx.selected_header
28
+ );
29
+ return true;
30
+ case "Escape":
31
+ event.preventDefault();
32
+ ctx.df_actions.set_selected_header(false);
33
+ return true;
34
+ case "Enter":
35
+ event.preventDefault();
36
+ if (ctx.editable) {
37
+ ctx.df_actions.set_header_edit(ctx.selected_header);
38
+ }
39
+ return true;
40
+ }
41
+ return false;
42
+ }
43
+
44
+ function handle_delete_operation(
45
+ event: KeyboardEvent,
46
+ ctx: KeyboardContext
47
+ ): boolean {
48
+ if (!ctx.editable) return false;
49
+ if (event.key !== "Delete" && event.key !== "Backspace") return false;
50
+
51
+ if (ctx.editing) {
52
+ const [row, col] = ctx.editing;
53
+ const input_el = ctx.els[ctx.data[row][col].id].input;
54
+ if (input_el && input_el.selectionStart !== input_el.selectionEnd) {
55
+ return false;
56
+ }
57
+ if (
58
+ event.key === "Delete" &&
59
+ input_el?.selectionStart !== input_el?.value.length
60
+ ) {
61
+ return false;
62
+ }
63
+ if (event.key === "Backspace" && input_el?.selectionStart !== 0) {
64
+ return false;
65
+ }
66
+ }
67
+
68
+ event.preventDefault();
69
+ if (ctx.selected_cells.length > 0) {
70
+ const new_data = handle_delete_key(ctx.data, ctx.selected_cells);
71
+ ctx.dispatch("change", {
72
+ data: new_data.map((row) => row.map((cell) => cell.value)),
73
+ headers: ctx.headers.map((h) => h.value),
74
+ metadata: null
75
+ });
76
+ ctx.dispatch("input");
77
+ }
78
+ return true;
79
+ }
80
+
81
+ function handle_arrow_keys(
82
+ event: KeyboardEvent,
83
+ ctx: KeyboardContext,
84
+ i: number,
85
+ j: number
86
+ ): boolean {
87
+ if (ctx.editing) return false;
88
+ event.preventDefault();
89
+
90
+ const next_coords = ctx.move_cursor(event, [i, j], ctx.data);
91
+ if (next_coords) {
92
+ if (event.shiftKey) {
93
+ ctx.df_actions.set_selected_cells(
94
+ ctx.get_range_selection(
95
+ ctx.selected_cells.length > 0 ? ctx.selected_cells[0] : [i, j],
96
+ next_coords
97
+ )
98
+ );
99
+ ctx.df_actions.set_editing(false);
100
+ } else {
101
+ ctx.df_actions.set_selected_cells([next_coords]);
102
+ ctx.df_actions.set_editing(false);
103
+ }
104
+ ctx.df_actions.set_selected(next_coords);
105
+ } else if (next_coords === false && event.key === "ArrowUp" && i === 0) {
106
+ ctx.df_actions.set_selected_header(j);
107
+ ctx.df_actions.set_selected(false);
108
+ ctx.df_actions.set_selected_cells([]);
109
+ ctx.df_actions.set_editing(false);
110
+ }
111
+ return true;
112
+ }
113
+
114
+ async function handle_enter_key(
115
+ event: KeyboardEvent,
116
+ ctx: KeyboardContext,
117
+ i: number,
118
+ j: number
119
+ ): Promise<boolean> {
120
+ event.preventDefault();
121
+ if (!ctx.editable) return false;
122
+
123
+ if (event.shiftKey) {
124
+ await ctx.add_row(i);
125
+ await tick();
126
+ ctx.df_actions.set_selected([i + 1, j]);
127
+ } else {
128
+ if (dequal(ctx.editing, [i, j])) {
129
+ const cell_id = ctx.data[i][j].id;
130
+ const input_el = ctx.els[cell_id].input;
131
+ if (input_el) {
132
+ const old_value = ctx.data[i][j].value;
133
+ ctx.data[i][j].value = input_el.value;
134
+ if (old_value !== input_el.value) {
135
+ ctx.dispatch("input");
136
+ }
137
+ }
138
+ ctx.df_actions.set_editing(false);
139
+ await tick();
140
+ ctx.df_actions.set_selected([i, j]);
141
+ } else {
142
+ ctx.df_actions.set_editing([i, j]);
143
+ }
144
+ }
145
+ return true;
146
+ }
147
+
148
+ function handle_tab_key(
149
+ event: KeyboardEvent,
150
+ ctx: KeyboardContext,
151
+ i: number,
152
+ j: number
153
+ ): boolean {
154
+ event.preventDefault();
155
+ ctx.df_actions.set_editing(false);
156
+ const next_cell = ctx.get_next_cell_coordinates(
157
+ [i, j],
158
+ ctx.data,
159
+ event.shiftKey
160
+ );
161
+ if (next_cell) {
162
+ ctx.df_actions.set_selected_cells([next_cell]);
163
+ ctx.df_actions.set_selected(next_cell);
164
+ if (ctx.editable) {
165
+ ctx.df_actions.set_editing(next_cell);
166
+ }
167
+ }
168
+ return true;
169
+ }
170
+
171
+ function handle_default_key(
172
+ event: KeyboardEvent,
173
+ ctx: KeyboardContext,
174
+ i: number,
175
+ j: number
176
+ ): boolean {
177
+ if (!ctx.editable) return false;
178
+ if (
179
+ (!ctx.editing || (ctx.editing && dequal(ctx.editing, [i, j]))) &&
180
+ event.key.length === 1
181
+ ) {
182
+ ctx.df_actions.set_editing([i, j]);
183
+ return true;
184
+ }
185
+ return false;
186
+ }
187
+
188
+ async function handle_cell_navigation(
189
+ event: KeyboardEvent,
190
+ ctx: KeyboardContext
191
+ ): Promise<boolean> {
192
+ if (!ctx.selected) return false;
193
+ if (event.key === "c" && (event.metaKey || event.ctrlKey)) {
194
+ event.preventDefault();
195
+ if (ctx.selected_cells.length > 0) {
196
+ await copy_table_data(ctx.data, ctx.selected_cells);
197
+ }
198
+ ctx.set_copy_flash(true);
199
+
200
+ return true;
201
+ }
202
+
203
+ const [i, j] = ctx.selected;
204
+
205
+ switch (event.key) {
206
+ case "ArrowRight":
207
+ case "ArrowLeft":
208
+ case "ArrowDown":
209
+ case "ArrowUp":
210
+ return handle_arrow_keys(event, ctx, i, j);
211
+ case "Escape":
212
+ if (!ctx.editable) return false;
213
+ event.preventDefault();
214
+ ctx.df_actions.set_editing(false);
215
+ tick().then(() => {
216
+ if (ctx.parent_element) {
217
+ ctx.parent_element.focus();
218
+ }
219
+ });
220
+
221
+ return true;
222
+ case "Enter":
223
+ return await handle_enter_key(event, ctx, i, j);
224
+ case "Tab":
225
+ return handle_tab_key(event, ctx, i, j);
226
+ default:
227
+ return handle_default_key(event, ctx, i, j);
228
+ }
229
+ }
230
+
231
+ export async function handle_keydown(
232
+ event: KeyboardEvent,
233
+ context: KeyboardContext
234
+ ): Promise<void> {
235
+ if (handle_header_navigation(event, context)) return;
236
+ if (handle_delete_operation(event, context)) return;
237
+ await handle_cell_navigation(event, context);
238
+ }
@@ -1,24 +1,89 @@
1
1
  import { describe, test, expect } from "vitest";
2
- import { get_sort_status, sort_data } from "./sort_utils";
2
+ import { get_sort_status, sort_data, SortDirection } from "./sort_utils";
3
3
 
4
4
  describe("sort_utils", () => {
5
5
  describe("get_sort_status", () => {
6
6
  const headers = ["A", "B", "C"];
7
7
 
8
8
  test("returns none when no sort is active", () => {
9
- expect(get_sort_status("A", undefined, undefined, headers)).toBe("none");
9
+ expect(get_sort_status("A", [], headers)).toBe("none");
10
10
  });
11
11
 
12
12
  test("returns ascending when column is sorted ascending", () => {
13
- expect(get_sort_status("A", 0, "asc", headers)).toBe("ascending");
13
+ expect(
14
+ get_sort_status(
15
+ "A",
16
+ [{ col: 0, direction: "asc" as SortDirection }],
17
+ headers
18
+ )
19
+ ).toBe("asc");
14
20
  });
15
21
 
16
22
  test("returns descending when column is sorted descending", () => {
17
- expect(get_sort_status("B", 1, "des", headers)).toBe("descending");
23
+ expect(
24
+ get_sort_status(
25
+ "B",
26
+ [{ col: 1, direction: "desc" as SortDirection }],
27
+ headers
28
+ )
29
+ ).toBe("desc");
18
30
  });
19
31
 
20
32
  test("returns none for non-matching column", () => {
21
- expect(get_sort_status("A", 1, "asc", headers)).toBe("none");
33
+ expect(
34
+ get_sort_status(
35
+ "A",
36
+ [{ col: 1, direction: "asc" as SortDirection }],
37
+ headers
38
+ )
39
+ ).toBe("none");
40
+ });
41
+
42
+ test("handles multiple sort columns", () => {
43
+ const sort_columns = [
44
+ { col: 0, direction: "asc" as SortDirection },
45
+ { col: 1, direction: "desc" as SortDirection }
46
+ ];
47
+ expect(get_sort_status("A", sort_columns, headers)).toBe("asc");
48
+ expect(get_sort_status("B", sort_columns, headers)).toBe("desc");
49
+ expect(get_sort_status("C", sort_columns, headers)).toBe("none");
50
+ });
51
+
52
+ test("handles invalid column indices", () => {
53
+ expect(
54
+ get_sort_status(
55
+ "A",
56
+ [{ col: -1, direction: "asc" as SortDirection }],
57
+ headers
58
+ )
59
+ ).toBe("none");
60
+
61
+ expect(
62
+ get_sort_status(
63
+ "A",
64
+ [{ col: 999, direction: "asc" as SortDirection }],
65
+ headers
66
+ )
67
+ ).toBe("none");
68
+ });
69
+
70
+ test("handles empty headers", () => {
71
+ expect(
72
+ get_sort_status(
73
+ "A",
74
+ [{ col: 0, direction: "asc" as SortDirection }],
75
+ []
76
+ )
77
+ ).toBe("none");
78
+ });
79
+
80
+ test("prioritizes first matching column in sort_columns", () => {
81
+ const sort_columns = [
82
+ { col: 0, direction: "asc" as SortDirection },
83
+ { col: 0, direction: "desc" as SortDirection }
84
+ ];
85
+
86
+ expect(get_sort_status("A", sort_columns, headers)).toBe("asc");
22
87
  });
23
88
  });
24
89
 
@@ -39,33 +104,216 @@ describe("sort_utils", () => {
39
104
  ];
40
105
 
41
106
  test("sorts strings ascending", () => {
42
- const indices = sort_data(data, 0, "asc");
107
+ const indices = sort_data(data, [
108
+ { col: 0, direction: "asc" as SortDirection }
109
+ ]);
43
110
  expect(indices).toEqual([1, 0, 2]); // A, B, C
44
111
  });
45
112
 
46
113
  test("sorts numbers ascending", () => {
47
- const indices = sort_data(data, 1, "asc");
48
- expect(indices).toEqual([1, 0, 2]); // 1, 2, 3
114
+ const indices = sort_data(data, [
115
+ { col: 1, direction: "asc" as SortDirection }
116
+ ]);
117
+ expect(indices).toEqual([1, 0, 2]);
49
118
  });
50
119
 
51
120
  test("sorts strings descending", () => {
52
- const indices = sort_data(data, 0, "des");
53
- expect(indices).toEqual([2, 0, 1]); // C, B, A
121
+ const indices = sort_data(data, [
122
+ { col: 0, direction: "desc" as SortDirection }
123
+ ]);
124
+ expect(indices).toEqual([2, 0, 1]);
54
125
  });
55
126
 
56
- test("returns original order when sort params are invalid", () => {
57
- const indices = sort_data(data, undefined, undefined);
127
+ test("returns original order when sort params are empty", () => {
128
+ const indices = sort_data(data, []);
58
129
  expect(indices).toEqual([0, 1, 2]);
59
130
  });
60
131
 
61
132
  test("handles empty data", () => {
62
- const indices = sort_data([], 0, "asc");
133
+ const indices = sort_data(
134
+ [],
135
+ [{ col: 0, direction: "asc" as SortDirection }]
136
+ );
63
137
  expect(indices).toEqual([]);
64
138
  });
65
139
 
66
140
  test("handles invalid column index", () => {
67
- const indices = sort_data(data, 999, "asc");
141
+ const indices = sort_data(data, [
142
+ { col: 999, direction: "asc" as SortDirection }
143
+ ]);
144
+ expect(indices).toEqual([0, 1, 2]);
145
+ });
146
+
147
+ test("sorts by multiple columns", () => {
148
+ const test_data = [
149
+ [
150
+ { id: "1", value: "A" },
151
+ { id: "2", value: 2 }
152
+ ],
153
+ [
154
+ { id: "3", value: "A" },
155
+ { id: "4", value: 1 }
156
+ ],
157
+ [
158
+ { id: "5", value: "B" },
159
+ { id: "6", value: 3 }
160
+ ]
161
+ ];
162
+
163
+ const indices = sort_data(test_data, [
164
+ { col: 0, direction: "asc" as SortDirection },
165
+ { col: 1, direction: "asc" as SortDirection }
166
+ ]);
167
+
168
+ expect(indices).toEqual([1, 0, 2]);
169
+ });
170
+
171
+ test("respects sort direction for each column", () => {
172
+ const test_data = [
173
+ [
174
+ { id: "1", value: "A" },
175
+ { id: "2", value: 2 }
176
+ ],
177
+ [
178
+ { id: "3", value: "A" },
179
+ { id: "4", value: 1 }
180
+ ],
181
+ [
182
+ { id: "5", value: "B" },
183
+ { id: "6", value: 3 }
184
+ ]
185
+ ];
186
+
187
+ const indices = sort_data(test_data, [
188
+ { col: 0, direction: "asc" as SortDirection },
189
+ { col: 1, direction: "desc" as SortDirection }
190
+ ]);
191
+
192
+ expect(indices).toEqual([0, 1, 2]);
193
+ });
194
+
195
+ test("handles mixed data types in sort columns", () => {
196
+ const mixed_data = [
197
+ [
198
+ { id: "1", value: "A" },
199
+ { id: "2", value: 2 }
200
+ ],
201
+ [
202
+ { id: "3", value: "A" },
203
+ { id: "4", value: 1 }
204
+ ],
205
+ [
206
+ { id: "5", value: "B" },
207
+ { id: "6", value: 2 }
208
+ ]
209
+ ];
210
+
211
+ const indices = sort_data(mixed_data, [
212
+ { col: 0, direction: "asc" as SortDirection },
213
+ { col: 1, direction: "asc" as SortDirection }
214
+ ]);
215
+
216
+ expect(indices).toEqual([1, 0, 2]);
217
+ });
218
+
219
+ test("handles more than two sort columns", () => {
220
+ const complex_data = [
221
+ [
222
+ { id: "1", value: "A" },
223
+ { id: "2", value: 1 },
224
+ { id: "3", value: "X" }
225
+ ],
226
+ [
227
+ { id: "4", value: "A" },
228
+ { id: "5", value: 1 },
229
+ { id: "6", value: "Y" }
230
+ ],
231
+ [
232
+ { id: "7", value: "B" },
233
+ { id: "8", value: 2 },
234
+ { id: "9", value: "Z" }
235
+ ]
236
+ ];
237
+
238
+ const indices = sort_data(complex_data, [
239
+ { col: 0, direction: "asc" as SortDirection },
240
+ { col: 1, direction: "asc" as SortDirection },
241
+ { col: 2, direction: "asc" as SortDirection }
242
+ ]);
243
+
244
+ expect(indices).toEqual([0, 1, 2]);
245
+ });
246
+
247
+ test("ignores invalid sort columns", () => {
248
+ const indices = sort_data(data, [
249
+ { col: -1, direction: "asc" as SortDirection },
250
+ { col: 0, direction: "asc" as SortDirection }
251
+ ]);
252
+
253
+ expect(indices).toEqual([1, 0, 2]);
254
+ });
255
+
256
+ test("maintains original order when all values are equal", () => {
257
+ const equal_data = [
258
+ [
259
+ { id: "1", value: "A" },
260
+ { id: "2", value: 1 }
261
+ ],
262
+ [
263
+ { id: "3", value: "A" },
264
+ { id: "4", value: 1 }
265
+ ],
266
+ [
267
+ { id: "5", value: "A" },
268
+ { id: "6", value: 1 }
269
+ ]
270
+ ];
271
+
272
+ const indices = sort_data(equal_data, [
273
+ { col: 0, direction: "asc" as SortDirection },
274
+ { col: 1, direction: "asc" as SortDirection }
275
+ ]);
276
+
68
277
  expect(indices).toEqual([0, 1, 2]);
69
278
  });
279
+
280
+ test("handles undefined values in data rows", () => {
281
+ const data_with_undefined = [
282
+ [
283
+ { id: "1", value: "A" },
284
+ { id: "2", value: "" }
285
+ ],
286
+ [
287
+ { id: "3", value: "B" },
288
+ { id: "4", value: 2 }
289
+ ]
290
+ ];
291
+
292
+ const indices = sort_data(data_with_undefined, [
293
+ { col: 0, direction: "asc" as SortDirection },
294
+ { col: 1, direction: "asc" as SortDirection }
295
+ ]);
296
+
297
+ expect(indices).toEqual([0, 1]);
298
+ });
299
+
300
+ test("handles missing values in data", () => {
301
+ const data_with_missing = [
302
+ [
303
+ { id: "1", value: "" },
304
+ { id: "2", value: 1 }
305
+ ],
306
+ [
307
+ { id: "3", value: "A" },
308
+ { id: "4", value: 2 }
309
+ ]
310
+ ];
311
+
312
+ const indices = sort_data(data_with_missing, [
313
+ { col: 0, direction: "asc" as SortDirection }
314
+ ]);
315
+
316
+ expect(indices).toEqual([0, 1]);
317
+ });
70
318
  });
71
319
  });
@@ -1,55 +1,91 @@
1
1
  import type { Headers } from "../types";
2
+ import { sort_table_data } from "./table_utils";
2
3
 
3
- export type SortDirection = "asc" | "des";
4
+ export type SortDirection = "asc" | "desc";
4
5
 
5
6
  export function get_sort_status(
6
7
  name: string,
7
- sort_by: number | undefined,
8
- direction: SortDirection | undefined,
8
+ sort_columns: { col: number; direction: SortDirection }[],
9
9
  headers: Headers
10
- ): "none" | "ascending" | "descending" {
11
- if (typeof sort_by !== "number") return "none";
12
- if (sort_by < 0 || sort_by >= headers.length) return "none";
13
- if (headers[sort_by] === name) {
14
- if (direction === "asc") return "ascending";
15
- if (direction === "des") return "descending";
16
- }
17
- return "none";
10
+ ): "none" | "asc" | "desc" {
11
+ if (!sort_columns.length) return "none";
12
+
13
+ const sort_item = sort_columns.find((item) => {
14
+ const col = item.col;
15
+ if (col < 0 || col >= headers.length) return false;
16
+ return headers[col] === name;
17
+ });
18
+
19
+ if (!sort_item) return "none";
20
+ return sort_item.direction;
18
21
  }
19
22
 
20
23
  export function sort_data(
21
24
  data: { id: string; value: string | number }[][],
22
- sort_by: number | undefined,
23
- sort_direction: SortDirection | undefined
25
+ sort_columns: { col: number; direction: SortDirection }[]
24
26
  ): number[] {
25
27
  if (!data || !data.length || !data[0]) {
26
28
  return [];
27
29
  }
28
30
 
29
- if (
30
- typeof sort_by === "number" &&
31
- sort_direction &&
32
- sort_by >= 0 &&
33
- sort_by < data[0].length
34
- ) {
31
+ if (sort_columns.length > 0) {
35
32
  const row_indices = [...Array(data.length)].map((_, i) => i);
36
33
  row_indices.sort((row_a_idx, row_b_idx) => {
37
34
  const row_a = data[row_a_idx];
38
35
  const row_b = data[row_b_idx];
39
- if (
40
- !row_a ||
41
- !row_b ||
42
- sort_by >= row_a.length ||
43
- sort_by >= row_b.length
44
- )
45
- return 0;
46
-
47
- const val_a = row_a[sort_by].value;
48
- const val_b = row_b[sort_by].value;
49
- const comparison = val_a < val_b ? -1 : val_a > val_b ? 1 : 0;
50
- return sort_direction === "asc" ? comparison : -comparison;
36
+
37
+ for (const { col: sort_by, direction } of sort_columns) {
38
+ if (
39
+ !row_a ||
40
+ !row_b ||
41
+ sort_by < 0 ||
42
+ sort_by >= row_a.length ||
43
+ sort_by >= row_b.length ||
44
+ !row_a[sort_by] ||
45
+ !row_b[sort_by]
46
+ ) {
47
+ continue;
48
+ }
49
+
50
+ const val_a = row_a[sort_by].value;
51
+ const val_b = row_b[sort_by].value;
52
+ const comparison = val_a < val_b ? -1 : val_a > val_b ? 1 : 0;
53
+
54
+ if (comparison !== 0) {
55
+ return direction === "asc" ? comparison : -comparison;
56
+ }
57
+ }
58
+
59
+ return 0;
51
60
  });
52
61
  return row_indices;
53
62
  }
54
63
  return [...Array(data.length)].map((_, i) => i);
55
64
  }
65
+
66
+ export function sort_data_and_preserve_selection(
67
+ data: { id: string; value: string | number }[][],
68
+ display_value: string[][] | null,
69
+ styling: string[][] | null,
70
+ sort_columns: { col: number; direction: SortDirection }[],
71
+ selected: [number, number] | false,
72
+ get_current_indices: (
73
+ id: string,
74
+ data: { id: string; value: string | number }[][]
75
+ ) => [number, number]
76
+ ): { data: typeof data; selected: [number, number] | false } {
77
+ let id = null;
78
+ if (selected && selected[0] in data && selected[1] in data[selected[0]]) {
79
+ id = data[selected[0]][selected[1]].id;
80
+ }
81
+
82
+ sort_table_data(data, display_value, styling, sort_columns);
83
+
84
+ let new_selected = selected;
85
+ if (id) {
86
+ const [i, j] = get_current_indices(id, data);
87
+ new_selected = [i, j];
88
+ }
89
+
90
+ return { data, selected: new_selected };
91
+ }