@gradio/dataframe 0.22.0 → 0.23.0

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 (128) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/Dataframe.test.ts +697 -32
  3. package/Index.svelte +107 -106
  4. package/dist/Index.svelte +107 -106
  5. package/dist/Index.svelte.d.ts +0 -1
  6. package/dist/shared/BooleanCell.svelte +12 -6
  7. package/dist/shared/BooleanCell.svelte.d.ts +4 -18
  8. package/dist/shared/CellMenu.svelte +74 -49
  9. package/dist/shared/CellMenu.svelte.d.ts +5 -19
  10. package/dist/shared/CellMenuButton.svelte +3 -3
  11. package/dist/shared/CellMenuButton.svelte.d.ts +4 -18
  12. package/dist/shared/CellMenuIcons.svelte +1 -1
  13. package/dist/shared/CellMenuIcons.svelte.d.ts +4 -18
  14. package/dist/shared/DataCell.svelte +189 -0
  15. package/dist/shared/DataCell.svelte.d.ts +44 -0
  16. package/dist/shared/EditableCell.svelte +88 -64
  17. package/dist/shared/EditableCell.svelte.d.ts +8 -26
  18. package/dist/shared/EmptyRowButton.svelte +2 -2
  19. package/dist/shared/EmptyRowButton.svelte.d.ts +4 -18
  20. package/dist/shared/FilterMenu.svelte +26 -18
  21. package/dist/shared/FilterMenu.svelte.d.ts +5 -21
  22. package/dist/shared/HeaderCell.svelte +218 -0
  23. package/dist/shared/HeaderCell.svelte.d.ts +30 -0
  24. package/dist/shared/RowNumber.svelte +4 -2
  25. package/dist/shared/RowNumber.svelte.d.ts +4 -18
  26. package/dist/shared/Table.svelte +1037 -949
  27. package/dist/shared/Table.svelte.d.ts +12 -30
  28. package/dist/shared/Toolbar.svelte +32 -22
  29. package/dist/shared/Toolbar.svelte.d.ts +6 -21
  30. package/dist/shared/column_measurement.svelte.d.ts +20 -0
  31. package/dist/shared/column_measurement.svelte.js +79 -0
  32. package/dist/shared/icons/FilterIcon.svelte +14 -6
  33. package/dist/shared/icons/FilterIcon.svelte.d.ts +14 -6
  34. package/dist/shared/icons/Padlock.svelte +6 -2
  35. package/dist/shared/icons/Padlock.svelte.d.ts +4 -25
  36. package/dist/shared/icons/SelectionButtons.svelte +23 -8
  37. package/dist/shared/icons/SelectionButtons.svelte.d.ts +4 -18
  38. package/dist/shared/icons/SortButtonDown.svelte +15 -4
  39. package/dist/shared/icons/SortButtonDown.svelte.d.ts +4 -25
  40. package/dist/shared/icons/SortButtonUp.svelte +15 -5
  41. package/dist/shared/icons/SortButtonUp.svelte.d.ts +4 -25
  42. package/dist/shared/icons/SortIcon.svelte +13 -7
  43. package/dist/shared/icons/SortIcon.svelte.d.ts +7 -21
  44. package/dist/shared/tanstack/index.d.ts +3 -0
  45. package/dist/shared/tanstack/index.js +3 -0
  46. package/dist/shared/tanstack/table.svelte.d.ts +26 -0
  47. package/dist/shared/tanstack/table.svelte.js +102 -0
  48. package/dist/shared/tanstack/virtual.svelte.d.ts +14 -0
  49. package/dist/shared/tanstack/virtual.svelte.js +76 -0
  50. package/dist/shared/types.d.ts +2 -0
  51. package/dist/shared/utils/filter.d.ts +5 -0
  52. package/dist/shared/utils/filter.js +62 -0
  53. package/dist/shared/utils/selection_utils.js +1 -1
  54. package/dist/shared/utils/table_utils.d.ts +1 -14
  55. package/dist/shared/utils/table_utils.js +0 -61
  56. package/dist/standalone/Index.svelte +71 -46
  57. package/dist/standalone/Index.svelte.d.ts +4 -23
  58. package/dist/types.d.ts +32 -0
  59. package/dist/types.js +1 -0
  60. package/package.json +10 -9
  61. package/shared/BooleanCell.svelte +12 -6
  62. package/shared/CellMenu.svelte +74 -49
  63. package/shared/CellMenuButton.svelte +3 -3
  64. package/shared/CellMenuIcons.svelte +1 -1
  65. package/shared/DataCell.svelte +189 -0
  66. package/shared/EditableCell.svelte +88 -64
  67. package/shared/EmptyRowButton.svelte +2 -2
  68. package/shared/FilterMenu.svelte +26 -18
  69. package/shared/HeaderCell.svelte +218 -0
  70. package/shared/RowNumber.svelte +4 -2
  71. package/shared/Table.svelte +1037 -949
  72. package/shared/Toolbar.svelte +32 -22
  73. package/shared/column_measurement.svelte.ts +103 -0
  74. package/shared/icons/FilterIcon.svelte +14 -6
  75. package/shared/icons/Padlock.svelte +6 -2
  76. package/shared/icons/SelectionButtons.svelte +23 -8
  77. package/shared/icons/SortButtonDown.svelte +15 -4
  78. package/shared/icons/SortButtonUp.svelte +15 -5
  79. package/shared/icons/SortIcon.svelte +13 -7
  80. package/shared/tanstack/index.ts +23 -0
  81. package/shared/tanstack/table.svelte.ts +117 -0
  82. package/shared/tanstack/virtual.svelte.ts +105 -0
  83. package/shared/types.ts +3 -0
  84. package/shared/utils/filter.ts +66 -0
  85. package/shared/utils/selection_utils.ts +1 -1
  86. package/shared/utils/table_utils.ts +1 -100
  87. package/standalone/Index.svelte +71 -46
  88. package/test/filter.test.ts +350 -0
  89. package/test/selection_utils.test.ts +416 -0
  90. package/test/table_utils.test.ts +45 -112
  91. package/types.ts +34 -0
  92. package/dist/shared/TableCell.svelte +0 -330
  93. package/dist/shared/TableCell.svelte.d.ts +0 -66
  94. package/dist/shared/TableHeader.svelte +0 -344
  95. package/dist/shared/TableHeader.svelte.d.ts +0 -61
  96. package/dist/shared/VirtualTable.svelte +0 -379
  97. package/dist/shared/VirtualTable.svelte.d.ts +0 -42
  98. package/dist/shared/context/dataframe_context.d.ts +0 -180
  99. package/dist/shared/context/dataframe_context.js +0 -412
  100. package/dist/shared/icons/SortArrowDown.svelte +0 -25
  101. package/dist/shared/icons/SortArrowDown.svelte.d.ts +0 -20
  102. package/dist/shared/icons/SortArrowUp.svelte +0 -25
  103. package/dist/shared/icons/SortArrowUp.svelte.d.ts +0 -20
  104. package/dist/shared/utils/data_processing.d.ts +0 -14
  105. package/dist/shared/utils/data_processing.js +0 -47
  106. package/dist/shared/utils/drag_utils.d.ts +0 -15
  107. package/dist/shared/utils/drag_utils.js +0 -67
  108. package/dist/shared/utils/filter_utils.d.ts +0 -29
  109. package/dist/shared/utils/filter_utils.js +0 -123
  110. package/dist/shared/utils/keyboard_utils.d.ts +0 -3
  111. package/dist/shared/utils/keyboard_utils.js +0 -236
  112. package/dist/shared/utils/menu_utils.d.ts +0 -42
  113. package/dist/shared/utils/menu_utils.js +0 -58
  114. package/dist/shared/utils/sort_utils.d.ts +0 -26
  115. package/dist/shared/utils/sort_utils.js +0 -59
  116. package/shared/TableCell.svelte +0 -330
  117. package/shared/TableHeader.svelte +0 -344
  118. package/shared/VirtualTable.svelte +0 -379
  119. package/shared/context/dataframe_context.ts +0 -701
  120. package/shared/icons/SortArrowDown.svelte +0 -25
  121. package/shared/icons/SortArrowUp.svelte +0 -25
  122. package/shared/utils/data_processing.ts +0 -77
  123. package/shared/utils/drag_utils.ts +0 -105
  124. package/shared/utils/filter_utils.ts +0 -208
  125. package/shared/utils/keyboard_utils.ts +0 -309
  126. package/shared/utils/menu_utils.ts +0 -115
  127. package/shared/utils/sort_utils.ts +0 -91
  128. package/test/sort_utils.test.ts +0 -323
@@ -1,933 +1,839 @@
1
- <script lang="ts" context="module">
2
- import {
3
- create_dataframe_context,
4
- type SortDirection,
5
- type FilterDatatype
6
- } from "./context/dataframe_context";
7
- </script>
8
-
9
1
  <script lang="ts">
10
- import { afterUpdate, createEventDispatcher, tick, onMount } from "svelte";
11
- import { dequal } from "dequal/lite";
2
+ import {
3
+ createSvelteTable,
4
+ createSvelteVirtualizer,
5
+ getCoreRowModel,
6
+ getSortedRowModel,
7
+ getFilteredRowModel,
8
+ type ColumnDef,
9
+ type SortingState,
10
+ type ColumnFiltersState,
11
+ type ColumnPinningState
12
+ } from "./tanstack/index.js";
13
+ import { tick, onMount } from "svelte";
12
14
  import { Upload } from "@gradio/upload";
13
15
 
14
- import EditableCell from "./EditableCell.svelte";
15
- import RowNumber from "./RowNumber.svelte";
16
- import TableHeader from "./TableHeader.svelte";
17
- import TableCell from "./TableCell.svelte";
16
+ import HeaderCell from "./HeaderCell.svelte";
17
+ import DataCell from "./DataCell.svelte";
18
18
  import EmptyRowButton from "./EmptyRowButton.svelte";
19
19
  import type { SelectData } from "@gradio/utils";
20
20
  import type { I18nFormatter } from "js/core/src/gradio_helper";
21
21
  import { type Client } from "@gradio/client";
22
- import VirtualTable from "./VirtualTable.svelte";
23
- import type {
24
- Headers,
25
- DataframeValue,
26
- Datatype,
27
- EditData
28
- } from "./utils/utils";
22
+ import type { Datatype, DataframeValue, EditData } from "./utils/utils";
23
+ import { cast_value_to_type } from "./utils/utils";
29
24
  import CellMenu from "./CellMenu.svelte";
30
25
  import Toolbar from "./Toolbar.svelte";
31
- import type { CellCoordinate, CellValue } from "./types";
26
+ import type {
27
+ CellValue,
28
+ CellCoordinate,
29
+ SortDirection,
30
+ FilterDatatype
31
+ } from "./types";
32
32
  import {
33
+ is_cell_in_selection,
33
34
  is_cell_selected,
34
- should_show_cell_menu,
35
- get_current_indices,
36
- handle_click_outside as handle_click_outside_util,
37
- calculate_selection_positions
35
+ handle_click_outside as handle_click_outside_util
38
36
  } from "./utils/selection_utils";
39
- import {
40
- copy_table_data,
41
- get_max,
42
- handle_file_upload
43
- } from "./utils/table_utils";
44
- import { make_headers, process_data } from "./utils/data_processing";
45
- import { handle_keydown, handle_cell_blur } from "./utils/keyboard_utils";
46
- import {
47
- create_drag_handlers,
48
- type DragState,
49
- type DragHandlers
50
- } from "./utils/drag_utils";
51
- import { sort_data_and_preserve_selection } from "./utils/sort_utils";
52
- import { filter_data_and_preserve_selection } from "./utils/filter_utils";
53
-
54
- export let datatype: Datatype | Datatype[];
55
- export let label: string | null = null;
56
- export let show_label = true;
57
- export let headers: Headers = [];
58
- export let values: CellValue[][] = [];
59
- export let col_count: [number, "fixed" | "dynamic"];
60
- export let row_count: [number, "fixed" | "dynamic"];
61
- export let latex_delimiters: {
62
- left: string;
63
- right: string;
64
- display: boolean;
65
- }[];
66
- export let components: Record<string, any> = {};
67
-
68
- export let editable = true;
69
- export let wrap = false;
70
- export let root: string;
71
- export let i18n: I18nFormatter;
72
-
73
- export let max_height = 500;
74
- export let line_breaks = true;
75
- export let column_widths: string[] = [];
76
- export let show_row_numbers = false;
77
- export let upload: Client["upload"];
78
- export let stream_handler: Client["stream"];
79
- export let buttons: string[] | null = null;
80
- export let value_is_output = false;
81
- export let max_chars: number | undefined = undefined;
82
- export let show_search: "none" | "search" | "filter" = "none";
83
- export let pinned_columns = 0;
84
- export let static_columns: (string | number)[] = [];
85
- export let fullscreen = false;
86
-
87
- const df_ctx = create_dataframe_context({
88
- show_fullscreen_button:
89
- buttons === null ? true : buttons.includes("fullscreen"),
90
- show_copy_button: buttons === null ? true : buttons.includes("copy"),
91
- show_search,
92
- show_row_numbers,
93
- editable,
94
- pinned_columns,
95
- show_label,
96
- line_breaks,
97
- wrap,
98
- max_height,
99
- column_widths,
100
- max_chars,
101
- static_columns
102
- });
103
-
104
- const { state: df_state, actions: df_actions } = df_ctx;
105
-
106
- $: selected_cells = $df_state.ui_state.selected_cells;
107
- $: selected = $df_state.ui_state.selected;
108
- $: editing = $df_state.ui_state.editing;
109
- $: header_edit = $df_state.ui_state.header_edit;
110
- $: selected_header = $df_state.ui_state.selected_header;
111
- $: active_cell_menu = $df_state.ui_state.active_cell_menu;
112
- $: active_header_menu = $df_state.ui_state.active_header_menu;
113
- $: copy_flash = $df_state.ui_state.copy_flash;
114
-
115
- $: actual_pinned_columns =
116
- pinned_columns && data?.[0]?.length
117
- ? Math.min(pinned_columns, data[0].length)
118
- : 0;
119
-
120
- onMount(() => {
121
- df_ctx.parent_element = parent;
122
- df_ctx.get_data_at = get_data_at;
123
- df_ctx.get_column = get_column;
124
- df_ctx.get_row = get_row;
125
- df_ctx.dispatch = dispatch;
126
- init_drag_handlers();
127
-
128
- const observer = new IntersectionObserver((entries) => {
129
- entries.forEach((entry) => {
130
- if (entry.isIntersecting && !is_visible) {
131
- width_calculated = false;
132
- }
133
- is_visible = entry.isIntersecting;
37
+ import { copy_table_data, handle_file_upload } from "./utils/table_utils";
38
+ import { gradio_filter_fn } from "./utils/filter";
39
+ import { create_column_measurement } from "./column_measurement.svelte.js";
40
+
41
+ let {
42
+ datatype,
43
+ label = null,
44
+ show_label = true,
45
+ headers = $bindable([]),
46
+ values = $bindable([]),
47
+ col_count,
48
+ row_count,
49
+ latex_delimiters,
50
+ components = {},
51
+ editable = true,
52
+ wrap = false,
53
+ root,
54
+ i18n,
55
+ max_height = 500,
56
+ line_breaks = true,
57
+ column_widths = [],
58
+ show_row_numbers = false,
59
+ upload,
60
+ stream_handler,
61
+ buttons = null,
62
+ value_is_output = $bindable(false),
63
+ max_chars = undefined,
64
+ show_search = "none",
65
+ pinned_columns = 0,
66
+ static_columns = [],
67
+ fullscreen = false,
68
+ display_value = null,
69
+ styling = null,
70
+ onchange,
71
+ oninput,
72
+ onselect,
73
+ onedit,
74
+ onsearch,
75
+ onfullscreen
76
+ }: {
77
+ datatype: Datatype | Datatype[];
78
+ label?: string | null;
79
+ show_label?: boolean;
80
+ headers?: (string | null)[];
81
+ values?: CellValue[][];
82
+ col_count: [number, "fixed" | "dynamic"];
83
+ row_count: [number, "fixed" | "dynamic"];
84
+ latex_delimiters: { left: string; right: string; display: boolean }[];
85
+ components?: Record<string, any>;
86
+ editable?: boolean;
87
+ wrap?: boolean;
88
+ root: string;
89
+ i18n: I18nFormatter;
90
+ max_height?: number;
91
+ line_breaks?: boolean;
92
+ column_widths?: string[];
93
+ show_row_numbers?: boolean;
94
+ upload: Client["upload"];
95
+ stream_handler: Client["stream"];
96
+ buttons?: string[] | null;
97
+ value_is_output?: boolean;
98
+ max_chars?: number | undefined;
99
+ show_search?: "none" | "search" | "filter";
100
+ pinned_columns?: number;
101
+ static_columns?: (string | number)[];
102
+ fullscreen?: boolean;
103
+ display_value?: string[][] | null;
104
+ styling?: string[][] | null;
105
+ onchange?: (detail: DataframeValue) => void;
106
+ oninput?: () => void;
107
+ onselect?: (detail: SelectData) => void;
108
+ onedit?: (detail: EditData) => void;
109
+ onsearch?: (detail: string | null) => void;
110
+ onfullscreen?: () => void;
111
+ } = $props();
112
+
113
+ type GradioRow = Record<string, CellValue> & { _index: number };
114
+
115
+ // convert values[][] into tanstack row objects
116
+ let row_data: GradioRow[] = $derived(
117
+ (values ?? []).map((row, i) => {
118
+ const obj: GradioRow = { _index: i };
119
+ (row ?? []).forEach((val, j) => {
120
+ const dtype = Array.isArray(datatype) ? datatype[j] : datatype;
121
+ obj[`col_${j}`] = cast_value_to_type(val, dtype);
134
122
  });
135
- });
136
- observer.observe(parent);
137
- document.addEventListener("click", handle_click_outside);
138
- window.addEventListener("resize", handle_resize);
123
+ return obj;
124
+ })
125
+ );
126
+
127
+ let resolved_headers = $derived.by(() => {
128
+ let h = headers ?? [];
129
+ if (col_count[1] === "fixed" && h.length < col_count[0]) {
130
+ h = [
131
+ ...h,
132
+ ...Array(col_count[0] - h.length)
133
+ .fill(null)
134
+ .map((_, i) => `${i + h.length}`)
135
+ ];
136
+ }
137
+ if (!h.length) {
138
+ h = Array(col_count[0])
139
+ .fill(null)
140
+ .map((_, i) => `${i}`);
141
+ }
142
+ return h.map((v) => v ?? "");
143
+ });
139
144
 
140
- const global_mouse_up = (event: MouseEvent): void => {
141
- if (is_dragging || drag_start) {
142
- handle_mouse_up(event);
145
+ let column_defs: ColumnDef<GradioRow, CellValue>[] = $derived(
146
+ resolved_headers.map((header_value, j) => ({
147
+ id: `col_${j}`,
148
+ accessorKey: `col_${j}`,
149
+ header: header_value,
150
+ size: column_widths[j] ? parseInt(column_widths[j]) || 150 : 150,
151
+ minSize: 45,
152
+ filterFn: gradio_filter_fn,
153
+ meta: {
154
+ colIndex: j,
155
+ datatype: Array.isArray(datatype) ? datatype[j] : datatype,
156
+ isStatic:
157
+ static_columns.includes(j) || static_columns.includes(header_value),
158
+ isPinned: j < pinned_columns
143
159
  }
144
- };
145
- document.addEventListener("mouseup", global_mouse_up);
160
+ }))
161
+ );
162
+
163
+ let sorting: SortingState = $state([]);
164
+ let column_filters: ColumnFiltersState = $state([]);
165
+ let global_filter: string = $state("");
166
+ let column_pinning: ColumnPinningState = $derived({
167
+ left: column_defs.filter((_, i) => i < pinned_columns).map((c) => c.id!)
168
+ });
146
169
 
147
- return () => {
148
- observer.disconnect();
149
- document.removeEventListener("click", handle_click_outside);
150
- window.removeEventListener("resize", handle_resize);
151
- document.removeEventListener("mouseup", global_mouse_up);
152
- };
170
+ const table = createSvelteTable<GradioRow>({
171
+ get data() {
172
+ return row_data;
173
+ },
174
+ get columns() {
175
+ return column_defs;
176
+ },
177
+ state: {
178
+ get sorting() {
179
+ return sorting;
180
+ },
181
+ get columnFilters() {
182
+ return column_filters;
183
+ },
184
+ get globalFilter() {
185
+ return global_filter;
186
+ },
187
+ get columnPinning() {
188
+ return column_pinning;
189
+ }
190
+ },
191
+ onSortingChange: (updater) => {
192
+ sorting = typeof updater === "function" ? updater(sorting) : updater;
193
+ },
194
+ onColumnFiltersChange: (updater) => {
195
+ column_filters =
196
+ typeof updater === "function" ? updater(column_filters) : updater;
197
+ },
198
+ onGlobalFilterChange: (updater) => {
199
+ global_filter =
200
+ typeof updater === "function" ? updater(global_filter) : updater;
201
+ },
202
+ getCoreRowModel: getCoreRowModel(),
203
+ getSortedRowModel: getSortedRowModel(),
204
+ getFilteredRowModel: getFilteredRowModel(),
205
+ globalFilterFn: "includesString",
206
+ enableSorting: true,
207
+ enableMultiSort: true,
208
+ maxMultiSortColCount: 3
153
209
  });
154
210
 
155
- $: {
156
- if (data || _headers || els) {
157
- df_ctx.data = data;
158
- df_ctx.headers = _headers;
159
- df_ctx.els = els;
160
- df_ctx.display_value = display_value;
161
- df_ctx.styling = styling;
211
+ let rows = $derived(table.getRowModel().rows);
212
+ let header_groups = $derived(table.getHeaderGroups());
213
+
214
+ let scroll_container: HTMLDivElement;
215
+
216
+ const virtualizer = createSvelteVirtualizer<
217
+ HTMLDivElement,
218
+ HTMLTableRowElement
219
+ >({
220
+ get count() {
221
+ return rows.length;
222
+ },
223
+ getScrollElement: () => scroll_container,
224
+ estimateSize: () => 35,
225
+ overscan: 10,
226
+ measureElement: (el, _entry, instance) => {
227
+ const h = el.getBoundingClientRect().height;
228
+ if (h > 0) return h;
229
+
230
+ const idx = el.getAttribute("data-index");
231
+ if (idx != null) {
232
+ const cached = (instance as any).itemSizeCache?.get(Number(idx));
233
+ if (typeof cached === "number") return cached;
234
+ }
235
+ return 35;
162
236
  }
163
- }
164
-
165
- const dispatch = createEventDispatcher<{
166
- change: DataframeValue;
167
- input: undefined;
168
- select: SelectData;
169
- search: string | null;
170
- edit: EditData;
171
- }>();
172
-
173
- let els: Record<
174
- string,
175
- { cell: null | HTMLTableCellElement; input: null | HTMLTextAreaElement }
176
- > = {};
177
- let data_binding: Record<string, (typeof data)[0][0]> = {};
178
- let _headers = make_headers(headers, col_count, els, make_id);
179
- let old_headers: string[] = headers;
180
- let data: { id: string; value: CellValue; display_value?: string }[][] = [[]];
181
- let old_val: undefined | CellValue[][] = undefined;
182
- let search_results: {
183
- id: string;
184
- value: CellValue;
185
- display_value?: string;
186
- styling?: string;
187
- }[][] = [[]];
188
- let dragging = false;
189
- let color_accent_copied: string;
190
- let filtered_to_original_map: number[] = [];
191
-
192
- onMount(() => {
193
- const color = getComputedStyle(document.documentElement)
194
- .getPropertyValue("--color-accent")
195
- .trim();
196
- color_accent_copied = color + "40"; // 80 is 50% opacity in hex
197
- document.documentElement.style.setProperty(
198
- "--color-accent-copied",
199
- color_accent_copied
200
- );
201
237
  });
202
238
 
203
- const get_data_at = (row: number, col: number): CellValue =>
204
- data?.[row]?.[col]?.value;
239
+ let virtual_items = $derived(virtualizer.virtualItems());
240
+ let total_size = $derived(virtualizer.totalSize());
241
+
242
+ let selected_cells: CellCoordinate[] = $state([]);
243
+ let selected: CellCoordinate | false = $state(false);
244
+ let editing: CellCoordinate | false = $state(false);
245
+ let header_edit: number | false = $state(false);
246
+ let selected_header: number | false = $state(false);
247
+ let active_cell_menu: {
248
+ row: number;
249
+ col: number;
250
+ x: number;
251
+ y: number;
252
+ } | null = $state(null);
253
+ let active_header_menu: { col: number; x: number; y: number } | null =
254
+ $state(null);
255
+ let copy_flash = $state(false);
256
+ let is_dragging = $state(false);
257
+ let show_scroll_button = $state(false);
258
+ let dragging = $state(false); // file drag
205
259
 
206
- const get_column = (col: number): CellValue[] =>
207
- data?.map((row) => row[col]?.value) ?? [];
260
+ let parent: HTMLDivElement;
208
261
 
209
- const get_row = (row: number): CellValue[] =>
210
- data?.[row]?.map((cell) => cell.value) ?? [];
262
+ function get_dtype(col: number): Datatype {
263
+ return Array.isArray(datatype) ? (datatype[col] ?? "str") : datatype;
264
+ }
211
265
 
212
- $: {
213
- if (!dequal(headers, old_headers)) {
214
- _headers = make_headers(headers, col_count, els, make_id);
215
- old_headers = JSON.parse(JSON.stringify(headers));
216
- }
266
+ function get_display_value(row: number, col: number): string {
267
+ if (display_value?.[row]?.[col] !== undefined)
268
+ return display_value[row][col];
269
+ return String(values?.[row]?.[col] ?? "");
217
270
  }
218
271
 
219
- function make_id(): string {
220
- return Math.random().toString(36).substring(2, 15);
272
+ function get_styling(row: number, col: number): string {
273
+ return styling?.[row]?.[col] ?? "";
221
274
  }
222
275
 
223
- export let display_value: string[][] | null = null;
224
- export let styling: string[][] | null = null;
276
+ function push_change(
277
+ new_values?: CellValue[][],
278
+ new_headers?: (string | null)[]
279
+ ): void {
280
+ onchange?.({
281
+ data: new_values ?? values,
282
+ headers: (new_headers ?? resolved_headers) as string[],
283
+ metadata: null
284
+ });
285
+ if (!value_is_output) oninput?.();
286
+ value_is_output = false;
287
+ }
225
288
 
226
- $: if (!dequal(values, old_val)) {
227
- if (parent) {
228
- // only clear column widths when the data structure changes
229
- const is_reset =
230
- values.length === 0 || (values.length === 1 && values[0].length === 0);
231
- const is_different_structure =
232
- old_val !== undefined &&
233
- (values.length !== old_val.length ||
234
- (values[0] && old_val[0] && values[0].length !== old_val[0].length));
289
+ function handle_cell_click(
290
+ event: MouseEvent,
291
+ row: number,
292
+ col: number
293
+ ): void {
294
+ event.preventDefault();
295
+ event.stopPropagation();
235
296
 
236
- if (is_reset || is_different_structure) {
237
- for (let i = 0; i < 50; i++) {
238
- parent.style.removeProperty(`--cell-width-${i}`);
297
+ const coord: CellCoordinate = [row, col];
298
+ if (event.shiftKey && selected) {
299
+ // range select
300
+ const [r1, c1] = selected;
301
+ const [r2, c2] = coord;
302
+ const new_cells: CellCoordinate[] = [];
303
+ for (let r = Math.min(r1, r2); r <= Math.max(r1, r2); r++) {
304
+ for (let c = Math.min(c1, c2); c <= Math.max(c1, c2); c++) {
305
+ new_cells.push([r, c]);
239
306
  }
240
- last_width_data_length = 0;
241
- last_width_column_count = 0;
242
- width_calculated = false;
243
307
  }
308
+ selected_cells = new_cells;
309
+ } else if (event.metaKey || event.ctrlKey) {
310
+ // toggle select
311
+ const exists = selected_cells.some(([r, c]) => r === row && c === col);
312
+ selected_cells = exists
313
+ ? selected_cells.filter(([r, c]) => !(r === row && c === col))
314
+ : [...selected_cells, coord];
315
+ } else {
316
+ selected_cells = [coord];
244
317
  }
245
318
 
246
- // only reset sort state when values are changed
247
- const is_reset =
248
- values.length === 0 || (values.length === 1 && values[0].length === 0);
249
- const is_different_structure =
250
- old_val !== undefined &&
251
- (values.length !== old_val.length ||
252
- (values[0] && old_val[0] && values[0].length !== old_val[0].length));
253
-
254
- data = process_data(
255
- values as CellValue[][],
256
- els,
257
- data_binding,
258
- make_id,
259
- display_value,
260
- datatype
261
- );
262
- old_val = JSON.parse(JSON.stringify(values)) as CellValue[][];
319
+ selected = coord;
320
+ header_edit = false;
321
+ selected_header = false;
322
+ active_cell_menu = null;
323
+ active_header_menu = null;
263
324
 
264
- if (is_reset || is_different_structure) {
265
- df_actions.reset_sort_state();
266
- } else if ($df_state.sort_state.sort_columns.length > 0) {
267
- sort_data(data, display_value, styling);
268
- } else {
269
- df_actions.handle_sort(-1, "asc");
270
- df_actions.reset_sort_state();
271
- }
325
+ // click selects, does NOT enter edit mode (double-click or typing does)
326
+ editing = false;
272
327
 
273
- if ($df_state.filter_state.filter_columns.length > 0) {
274
- filter_data(data, display_value, styling);
275
- } else {
276
- df_actions.reset_filter_state();
277
- }
328
+ onselect?.({
329
+ index: coord,
330
+ value: values?.[row]?.[col],
331
+ row_value: values?.[row] ?? [],
332
+ col_value: values?.map((r) => r[col]) ?? []
333
+ } as any);
278
334
 
279
- if ($df_state.current_search_query) {
280
- df_actions.handle_search(null);
281
- }
335
+ tick().then(() => parent?.focus());
336
+ }
282
337
 
283
- if (parent && cells.length > 0 && (is_reset || is_different_structure)) {
284
- width_calculated = false;
338
+ function handle_cell_dblclick(
339
+ event: MouseEvent,
340
+ row: number,
341
+ col: number
342
+ ): void {
343
+ event.preventDefault();
344
+ event.stopPropagation();
345
+ if (!editable) return;
346
+ const col_is_static =
347
+ static_columns.includes(col) ||
348
+ static_columns.includes(resolved_headers[col]);
349
+ if (!col_is_static) {
350
+ editing = [row, col];
285
351
  }
286
352
  }
287
353
 
288
- $: if ($df_state.current_search_query !== undefined) {
289
- const cell_map = new Map();
290
- filtered_to_original_map = [];
291
-
292
- data.forEach((row, row_idx) => {
293
- if (
294
- row.some((cell) =>
295
- String(cell?.value)
296
- .toLowerCase()
297
- .includes($df_state.current_search_query?.toLowerCase() || "")
298
- )
299
- ) {
300
- filtered_to_original_map.push(row_idx);
301
- }
302
- row.forEach((cell, col_idx) => {
303
- cell_map.set(cell.id, {
304
- value: cell.value,
305
- display_value:
306
- cell.display_value !== undefined
307
- ? cell.display_value
308
- : String(cell.value),
309
- styling: styling?.[row_idx]?.[col_idx] || ""
310
- });
354
+ function handle_blur(detail: {
355
+ blur_event: FocusEvent;
356
+ coords: [number, number];
357
+ }): void {
358
+ const { coords } = detail;
359
+ const input_el = detail.blur_event.target as HTMLTextAreaElement;
360
+ if (!input_el || input_el.value === undefined) return;
361
+
362
+ const [row, col] = coords;
363
+ const old_value = values?.[row]?.[col];
364
+ const new_value = input_el.value;
365
+
366
+ if (String(old_value) !== String(new_value)) {
367
+ const new_values = values.map((r) => [...r]);
368
+ new_values[row][col] = new_value;
369
+ values = new_values;
370
+
371
+ onedit?.({
372
+ index: [row, col],
373
+ value: new_value,
374
+ previous_value: String(old_value ?? "")
311
375
  });
312
- });
313
-
314
- const filtered = df_actions.filter_data(data);
315
-
316
- search_results = filtered.map((row) =>
317
- row.map((cell) => {
318
- const original = cell_map.get(cell.id);
319
- return {
320
- ...cell,
321
- display_value:
322
- original?.display_value !== undefined
323
- ? original.display_value
324
- : String(cell.value),
325
- styling: original?.styling || ""
326
- };
327
- })
328
- );
329
- } else {
330
- filtered_to_original_map = [];
331
- }
332
-
333
- let previous_headers = _headers.map((h) => h.value);
334
- let previous_data = data.map((row) => row.map((cell) => cell.value));
335
-
336
- $: {
337
- if (data || _headers) {
338
- df_actions.trigger_change(
339
- data,
340
- _headers,
341
- previous_data,
342
- previous_headers,
343
- value_is_output,
344
- dispatch
345
- );
346
- previous_data = data.map((row) => row.map((cell) => cell.value));
347
- previous_headers = _headers.map((h) => h.value);
376
+ push_change(new_values);
348
377
  }
349
378
  }
350
379
 
351
- function handle_sort(col: number, direction: SortDirection): void {
352
- df_actions.handle_sort(col, direction);
353
- sort_data(data, display_value, styling);
380
+ function handle_header_click(event: MouseEvent, col: number): void {
381
+ if (event.target instanceof HTMLAnchorElement) return;
382
+ event.preventDefault();
383
+ event.stopPropagation();
384
+ if (!editable) return;
385
+
386
+ editing = false;
387
+ selected = false;
388
+ selected_cells = [];
389
+ active_cell_menu = null;
390
+ active_header_menu = null;
391
+ selected_header = col;
392
+ header_edit = editable ? col : false;
393
+ parent?.focus();
354
394
  }
355
395
 
356
- function clear_sort(): void {
357
- df_actions.reset_sort_state();
358
- sort_data(data, display_value, styling);
396
+ function end_header_edit(key: string): void {
397
+ if (["Escape", "Enter", "Tab"].includes(key)) {
398
+ header_edit = false;
399
+ parent?.focus();
400
+ }
359
401
  }
360
402
 
361
- $: {
362
- if ($df_state.filter_state.filter_columns.length > 0) {
363
- filter_data(data, display_value, styling);
403
+ function toggle_header_menu(event: MouseEvent, col: number): void {
404
+ event.stopPropagation();
405
+ if (active_header_menu?.col === col) {
406
+ active_header_menu = null;
407
+ } else {
408
+ const th = (event.target as HTMLElement).closest("th");
409
+ if (th) {
410
+ const rect = th.getBoundingClientRect();
411
+ active_header_menu = { col, x: rect.right, y: rect.bottom };
412
+ }
364
413
  }
414
+ }
365
415
 
366
- if ($df_state.sort_state.sort_columns.length > 0) {
367
- sort_data(data, display_value, styling);
368
- df_actions.update_row_order(data);
416
+ function handle_sort(col: number, direction: SortDirection): void {
417
+ const col_id = `col_${col}`;
418
+ const desc = direction === "desc";
419
+ // if already sorted this way, remove it
420
+ const existing = sorting.findIndex((s) => s.id === col_id);
421
+ if (existing >= 0 && sorting[existing].desc === desc) {
422
+ sorting = sorting.filter((s) => s.id !== col_id);
423
+ } else {
424
+ sorting = [
425
+ ...sorting.filter((s) => s.id !== col_id),
426
+ { id: col_id, desc }
427
+ ].slice(-3);
369
428
  }
370
429
  }
371
430
 
431
+ function clear_sort(): void {
432
+ sorting = [];
433
+ }
434
+
372
435
  function handle_filter(
373
436
  col: number,
374
- datatype: FilterDatatype,
437
+ dtype: FilterDatatype,
375
438
  filter: string,
376
- value: string
439
+ fvalue: string
377
440
  ): void {
378
- df_actions.handle_filter(col, datatype, filter, value);
379
- filter_data(data, display_value, styling);
441
+ const col_id = `col_${col}`;
442
+ const existing = column_filters.findIndex((f) => f.id === col_id);
443
+ if (existing >= 0) {
444
+ column_filters = column_filters.filter((f) => f.id !== col_id);
445
+ } else {
446
+ column_filters = [
447
+ ...column_filters,
448
+ { id: col_id, value: { dtype, filter, value: fvalue } }
449
+ ];
450
+ }
380
451
  }
381
452
 
382
453
  function clear_filter(): void {
383
- df_actions.reset_filter_state();
384
- filter_data(data, display_value, styling);
454
+ column_filters = [];
385
455
  }
386
456
 
387
- async function edit_header(i: number, _select = false): Promise<void> {
388
- if (!editable || header_edit === i || col_count[1] !== "dynamic") return;
389
- df_actions.set_header_edit(i);
457
+ function handle_search(query: string | null): void {
458
+ global_filter = query ?? "";
459
+ onsearch?.(query);
390
460
  }
391
461
 
392
- function handle_header_click(event: MouseEvent, col: number): void {
393
- if (event.target instanceof HTMLAnchorElement) {
394
- return;
462
+ function add_row(index?: number): void {
463
+ if (row_count[1] !== "dynamic") return;
464
+ const col_len = values[0]?.length || resolved_headers.length || 1;
465
+ const new_row: CellValue[] = Array(col_len).fill("");
466
+ const new_values = [...values];
467
+ if (index !== undefined) {
468
+ new_values.splice(index, 0, new_row);
469
+ } else {
470
+ new_values.push(new_row);
395
471
  }
396
- event.preventDefault();
397
- event.stopPropagation();
398
- if (!editable) return;
399
- df_actions.set_editing(false);
400
- df_actions.handle_header_click(col, editable);
401
- parent.focus();
472
+ values = new_values;
473
+ push_change(new_values);
474
+ selected = [index ?? new_values.length - 1, 0];
475
+ parent?.focus();
402
476
  }
403
477
 
404
- function end_header_edit(event: CustomEvent<KeyboardEvent>): void {
405
- if (!editable) return;
406
- df_actions.end_header_edit(event.detail.key);
407
- parent.focus();
408
- }
409
-
410
- async function add_row(index?: number): Promise<void> {
411
- parent.focus();
412
-
413
- if (row_count[1] !== "dynamic") return;
414
-
415
- const new_row = Array(data[0]?.length || headers.length)
416
- .fill(0)
417
- .map((_, i) => {
418
- const _id = make_id();
419
- els[_id] = { cell: null, input: null };
420
- return { id: _id, value: "" };
421
- });
422
-
423
- if (data.length === 0) {
424
- data = [new_row];
425
- } else if (index !== undefined && index >= 0 && index <= data.length) {
426
- data.splice(index, 0, new_row);
427
- } else {
428
- data.push(new_row);
478
+ function add_col(index?: number): void {
479
+ if (col_count[1] !== "dynamic") return;
480
+ const new_headers = [
481
+ ...(headers ?? []),
482
+ `Header ${(headers?.length ?? 0) + 1}`
483
+ ];
484
+ const new_values = values.map((row) => [...row, ""]);
485
+ if (index !== undefined) {
486
+ new_headers.splice(index, 0, new_headers.pop()!);
487
+ new_values.forEach((row) => row.splice(index, 0, row.pop()!));
429
488
  }
489
+ values = new_values;
490
+ headers = new_headers;
491
+ push_change(new_values, new_headers);
492
+ parent?.focus();
493
+ }
430
494
 
431
- selected = [index !== undefined ? index : data.length - 1, 0];
495
+ function delete_row_at(index: number): void {
496
+ if (values.length <= 1) return;
497
+ values = [...values.slice(0, index), ...values.slice(index + 1)];
498
+ push_change(values);
499
+ active_cell_menu = null;
500
+ active_header_menu = null;
432
501
  }
433
502
 
434
- async function add_col(index?: number): Promise<void> {
435
- parent.focus();
503
+ function delete_col_at(index: number): void {
436
504
  if (col_count[1] !== "dynamic") return;
505
+ if ((values[0]?.length ?? 0) <= 1) return;
506
+ values = values.map((row) => [
507
+ ...row.slice(0, index),
508
+ ...row.slice(index + 1)
509
+ ]);
510
+ headers = [
511
+ ...(headers ?? []).slice(0, index),
512
+ ...(headers ?? []).slice(index + 1)
513
+ ];
514
+ push_change(values, headers as string[]);
515
+ active_cell_menu = null;
516
+ active_header_menu = null;
517
+ selected = false;
518
+ selected_cells = [];
519
+ editing = false;
520
+ }
437
521
 
438
- const result = df_actions.add_col(data, headers, make_id, index);
439
-
440
- result.data.forEach((row) => {
441
- row.forEach((cell) => {
442
- if (!els[cell.id]) {
443
- els[cell.id] = { cell: null, input: null };
444
- }
445
- });
446
- });
522
+ function add_row_at(index: number, position: "above" | "below"): void {
523
+ add_row(position === "above" ? index : index + 1);
524
+ active_cell_menu = null;
525
+ }
447
526
 
448
- data = result.data;
449
- headers = result.headers;
527
+ function add_col_at(index: number, position: "left" | "right"): void {
528
+ add_col(position === "left" ? index : index + 1);
529
+ active_cell_menu = null;
530
+ }
450
531
 
451
- await tick();
532
+ // function handle_select_all(col: number, checked: boolean): void {
533
+ // values = values.map((row) => {
534
+ // const new_row = [...row];
535
+ // new_row[col] = checked;
536
+ // return new_row;
537
+ // });
538
+ // push_change(values);
539
+ // }
452
540
 
453
- requestAnimationFrame(() => {
454
- edit_header(index !== undefined ? index : data[0].length - 1, true);
455
- const new_w = parent.querySelectorAll("tbody")[1].offsetWidth;
456
- parent.querySelectorAll("table")[1].scrollTo({ left: new_w });
541
+ function commit_filter(): void {
542
+ if (!global_filter || show_search !== "filter") return;
543
+ // get the filtered rows from tanstack and push as new values
544
+ const filtered_values = rows.map((row) => {
545
+ const original_idx = row.original._index;
546
+ return values[original_idx];
457
547
  });
548
+ values = filtered_values;
549
+ global_filter = "";
550
+ push_change(filtered_values);
551
+ }
552
+
553
+ async function handle_copy(): Promise<void> {
554
+ const data_for_copy = values.map((row) =>
555
+ row.map((val, j) => ({ id: `${j}`, value: val }))
556
+ );
557
+ const cells_to_copy = selected_cells.length > 0 ? selected_cells : null;
558
+ await copy_table_data(data_for_copy, cells_to_copy);
559
+ copy_flash = true;
560
+ setTimeout(() => (copy_flash = false), 800);
458
561
  }
459
562
 
460
563
  function handle_click_outside(event: Event): void {
461
564
  if (handle_click_outside_util(event, parent)) {
462
- df_actions.clear_ui_state();
565
+ selected_cells = [];
566
+ selected = false;
567
+ editing = false;
463
568
  header_edit = false;
464
569
  selected_header = false;
570
+ active_cell_menu = null;
571
+ active_header_menu = null;
465
572
  }
466
573
  }
467
574
 
468
- $: max = get_max(data);
469
-
470
- let width_calc_timeout: ReturnType<typeof setTimeout>;
471
- $: if (cells[0] && cells[0]?.clientWidth) {
472
- clearTimeout(width_calc_timeout);
473
- width_calc_timeout = setTimeout(() => set_cell_widths(), 100);
474
- }
475
-
476
- let width_calculated = false;
477
- $: if (cells[0] && !width_calculated) {
478
- set_cell_widths();
479
- width_calculated = true;
480
- }
481
- let cells: HTMLTableCellElement[] = [];
482
- let parent: HTMLDivElement;
483
- let table: HTMLTableElement;
484
- let last_width_data_length = 0;
485
- let last_width_column_count = 0;
486
-
487
- function set_cell_widths(): void {
488
- const column_count = data[0]?.length || 0;
489
- if ($df_state.filter_state.filter_columns.length > 0) {
490
- return;
491
- }
492
- if (
493
- last_width_data_length === data.length &&
494
- last_width_column_count === column_count &&
495
- $df_state.sort_state.sort_columns.length > 0
496
- ) {
497
- return;
498
- }
575
+ function handle_keydown(e: KeyboardEvent): void {
576
+ if (!selected && selected_header === false) return;
499
577
 
500
- if (!parent) {
501
- return;
502
- }
578
+ const num_rows = rows.length;
579
+ const num_cols = resolved_headers.length;
503
580
 
504
- last_width_data_length = data.length;
505
- last_width_column_count = column_count;
581
+ if (selected) {
582
+ const [row, col] = selected;
506
583
 
507
- const widths = cells.map((el) => el?.clientWidth || 0);
508
- if (widths.length === 0) return;
509
-
510
- if (show_row_numbers) {
511
- parent.style.setProperty(`--cell-width-row-number`, `${widths[0]}px`);
512
- }
513
-
514
- for (let i = 0; i < 50; i++) {
515
- if (!column_widths[i]) {
516
- parent.style.removeProperty(`--cell-width-${i}`);
517
- } else if (column_widths[i].endsWith("%")) {
518
- const percentage = parseFloat(column_widths[i]);
519
- const pixel_width = Math.floor((percentage / 100) * parent.clientWidth);
520
- parent.style.setProperty(`--cell-width-${i}`, `${pixel_width}px`);
521
- } else {
522
- parent.style.setProperty(`--cell-width-${i}`, column_widths[i]);
584
+ switch (e.key) {
585
+ case "ArrowUp":
586
+ e.preventDefault();
587
+ if (row > 0) {
588
+ selected = [row - 1, col];
589
+ selected_cells = [selected];
590
+ editing = false;
591
+ virtualizer.instance.scrollToIndex(row - 1, { align: "auto" });
592
+ }
593
+ break;
594
+ case "ArrowDown":
595
+ e.preventDefault();
596
+ if (row < num_rows - 1) {
597
+ selected = [row + 1, col];
598
+ selected_cells = [selected];
599
+ editing = false;
600
+ virtualizer.instance.scrollToIndex(row + 1, { align: "auto" });
601
+ }
602
+ break;
603
+ case "ArrowLeft":
604
+ e.preventDefault();
605
+ if (col > 0) {
606
+ selected = [row, col - 1];
607
+ selected_cells = [selected];
608
+ editing = false;
609
+ }
610
+ break;
611
+ case "ArrowRight":
612
+ e.preventDefault();
613
+ if (col < num_cols - 1) {
614
+ selected = [row, col + 1];
615
+ selected_cells = [selected];
616
+ editing = false;
617
+ }
618
+ break;
619
+ case "Tab": {
620
+ e.preventDefault();
621
+ const was_editing = !!editing;
622
+ if (e.shiftKey) {
623
+ if (col > 0) selected = [row, col - 1];
624
+ else if (row > 0) selected = [row - 1, num_cols - 1];
625
+ } else {
626
+ if (col < num_cols - 1) selected = [row, col + 1];
627
+ else if (row < num_rows - 1) selected = [row + 1, 0];
628
+ }
629
+ selected_cells = [selected];
630
+ if (was_editing) {
631
+ const tab_col = (selected as CellCoordinate)[1];
632
+ const tab_static =
633
+ static_columns.includes(tab_col) ||
634
+ static_columns.includes(resolved_headers[tab_col]);
635
+ editing = editable && !tab_static ? selected : false;
636
+ } else {
637
+ editing = false;
638
+ }
639
+ if (!editing) tick().then(() => parent?.focus());
640
+ break;
641
+ }
642
+ case "Enter":
643
+ if (editing && e.shiftKey) {
644
+ // shift+enter inserts newline in textarea — don't intercept
645
+ return;
646
+ }
647
+ e.preventDefault();
648
+ if (editing) {
649
+ editing = false;
650
+ if (row < num_rows - 1) {
651
+ selected = [row + 1, col];
652
+ selected_cells = [selected];
653
+ }
654
+ tick().then(() => parent?.focus());
655
+ } else if (editable) {
656
+ const enter_static =
657
+ static_columns.includes(col) ||
658
+ static_columns.includes(resolved_headers[col]);
659
+ if (!enter_static) {
660
+ editing = [row, col];
661
+ }
662
+ }
663
+ break;
664
+ case "Escape":
665
+ editing = false;
666
+ tick().then(() => parent?.focus());
667
+ break;
668
+ case "Delete":
669
+ case "Backspace":
670
+ if (!editing && editable) {
671
+ e.preventDefault();
672
+ const new_values = values.map((r) => [...r]);
673
+ selected_cells.forEach(([r, c]) => {
674
+ if (!static_columns.includes(c)) {
675
+ new_values[r][c] = "";
676
+ }
677
+ });
678
+ values = new_values;
679
+ push_change(new_values);
680
+ }
681
+ break;
682
+ default:
683
+ // start editing on printable character
684
+ if (
685
+ editable &&
686
+ !editing &&
687
+ e.key.length === 1 &&
688
+ !e.ctrlKey &&
689
+ !e.metaKey &&
690
+ !static_columns.includes(col)
691
+ ) {
692
+ editing = [row, col];
693
+ }
694
+ break;
523
695
  }
524
- }
525
696
 
526
- widths.forEach((width, i) => {
527
- if (!column_widths[i]) {
528
- const calculated_width = `${Math.max(width, 45)}px`;
529
- parent.style.setProperty(`--cell-width-${i}`, calculated_width);
697
+ if ((e.ctrlKey || e.metaKey) && e.key === "c") {
698
+ handle_copy();
530
699
  }
531
- });
532
- }
533
-
534
- function get_cell_width(index: number): string {
535
- return `var(--cell-width-${index})`;
536
- }
537
-
538
- let table_height: number =
539
- values.slice(0, (max_height / values.length) * 37).length * 37 + 37;
540
- let scrollbar_width = 0;
541
-
542
- function sort_data(
543
- _data: typeof data,
544
- _display_value: string[][] | null,
545
- _styling: string[][] | null
546
- ): void {
547
- const result = sort_data_and_preserve_selection(
548
- _data,
549
- _display_value,
550
- _styling,
551
- $df_state.sort_state.sort_columns,
552
- selected,
553
- get_current_indices
554
- );
555
-
556
- data = result.data;
557
- selected = result.selected;
558
- }
559
-
560
- function filter_data(
561
- _data: typeof data,
562
- _display_value: string[][] | null,
563
- _styling: string[][] | null
564
- ): void {
565
- const result = filter_data_and_preserve_selection(
566
- _data,
567
- _display_value,
568
- _styling,
569
- $df_state.filter_state.filter_columns,
570
- selected,
571
- get_current_indices,
572
- $df_state.filter_state.initial_data?.data,
573
- $df_state.filter_state.initial_data?.display_value,
574
- $df_state.filter_state.initial_data?.styling
575
- );
576
- data = result.data;
577
- selected = result.selected;
578
- }
579
-
580
- $: selected_index = !!selected && selected[0];
581
-
582
- let is_visible = false;
583
-
584
- const set_copy_flash = (value: boolean): void => {
585
- df_actions.set_copy_flash(value);
586
- if (value) {
587
- setTimeout(() => df_actions.set_copy_flash(false), 800);
588
700
  }
589
- };
590
-
591
- let previous_selected_cells: [number, number][] = [];
701
+ }
592
702
 
593
- $: {
594
- if (copy_flash && !dequal(selected_cells, previous_selected_cells)) {
595
- set_copy_flash(false);
703
+ function handle_scroll(): void {
704
+ if (scroll_container) {
705
+ show_scroll_button = scroll_container.scrollTop > 100;
596
706
  }
597
- previous_selected_cells = selected_cells;
598
707
  }
599
708
 
600
- function handle_blur(
601
- event: CustomEvent<{
602
- blur_event: FocusEvent;
603
- coords: [number, number];
604
- }>
605
- ): void {
606
- const { blur_event, coords } = event.detail;
607
- handle_cell_blur(blur_event, df_ctx, coords);
709
+ function scroll_to_top(): void {
710
+ scroll_container?.scrollTo({ top: 0 });
608
711
  }
609
712
 
610
- function toggle_header_menu(event: MouseEvent, col: number): void {
713
+ function toggle_cell_menu(event: MouseEvent, row: number, col: number): void {
611
714
  event.stopPropagation();
612
- if (active_header_menu && active_header_menu.col === col) {
613
- df_actions.set_active_header_menu(null);
715
+ if (active_cell_menu?.row === row && active_cell_menu.col === col) {
716
+ active_cell_menu = null;
614
717
  } else {
615
- const header = (event.target as HTMLElement).closest("th");
616
- if (header) {
617
- const rect = header.getBoundingClientRect();
618
- df_actions.set_active_header_menu({
619
- col,
620
- x: rect.right,
621
- y: rect.bottom
622
- });
718
+ const cell = (event.target as HTMLElement).closest(".body-cell, td");
719
+ if (cell) {
720
+ const rect = cell.getBoundingClientRect();
721
+ active_cell_menu = { row, col, x: rect.right, y: rect.bottom };
623
722
  }
624
723
  }
625
724
  }
626
725
 
627
- afterUpdate(() => {
628
- value_is_output = false;
629
- });
630
-
631
- function delete_col_at(index: number): void {
632
- if (col_count[1] !== "dynamic") return;
633
- if (data[0].length <= 1) return;
634
-
635
- const result = df_actions.delete_col_at(data, headers, index);
636
- data = result.data;
637
- headers = result.headers;
638
- _headers = make_headers(headers, col_count, els, make_id);
639
- df_actions.set_active_cell_menu(null);
640
- df_actions.set_active_header_menu(null);
641
- df_actions.set_selected(false);
642
- df_actions.set_selected_cells([]);
643
- df_actions.set_editing(false);
644
- }
645
-
646
- function delete_row_at(index: number): void {
647
- data = df_actions.delete_row_at(data, index);
648
- df_actions.set_active_cell_menu(null);
649
- df_actions.set_active_header_menu(null);
650
- }
651
-
652
- let selected_cell_coords: CellCoordinate;
653
- $: if (selected !== false) selected_cell_coords = selected;
654
-
655
- $: if (selected !== false) {
656
- const positions = calculate_selection_positions(
657
- selected,
658
- data,
659
- els,
660
- parent,
661
- table
662
- );
663
- document.documentElement.style.setProperty(
664
- "--selected-col-pos",
665
- positions.col_pos
666
- );
667
- document.documentElement.style.setProperty(
668
- "--selected-row-pos",
669
- positions.row_pos || "0px"
670
- );
671
- }
672
-
673
- function commit_filter(): void {
674
- if ($df_state.current_search_query && show_search === "filter") {
675
- const filtered_data: CellValue[][] = [];
676
- const filtered_display_values: string[][] = [];
677
- const filtered_styling: string[][] = [];
678
-
679
- search_results.forEach((row) => {
680
- const data_row: CellValue[] = [];
681
- const display_row: string[] = [];
682
- const styling_row: string[] = [];
683
-
684
- row.forEach((cell) => {
685
- data_row.push(cell.value);
686
- display_row.push(
687
- cell.display_value !== undefined
688
- ? cell.display_value
689
- : String(cell.value)
690
- );
691
- styling_row.push(cell.styling || "");
692
- });
693
-
694
- filtered_data.push(data_row);
695
- filtered_display_values.push(display_row);
696
- filtered_styling.push(styling_row);
697
- });
698
-
699
- const change_payload = {
700
- data: filtered_data,
701
- headers: _headers.map((h) => h.value),
702
- metadata: {
703
- display_value: filtered_display_values,
704
- styling: filtered_styling
705
- }
706
- };
707
-
708
- dispatch("change", change_payload);
709
-
710
- if (!value_is_output) {
711
- dispatch("input");
726
+ function on_file_upload(file_data: any): void {
727
+ handle_file_upload(
728
+ typeof file_data === "string" ? file_data : (file_data?.data ?? ""),
729
+ (head) => {
730
+ headers = head.map((h: any) => h ?? "");
731
+ return (headers as string[]).map((h: string, i: number) => ({
732
+ id: `h_${i}`,
733
+ value: h
734
+ }));
735
+ },
736
+ (vals) => {
737
+ values = vals;
738
+ push_change(vals, headers as string[]);
712
739
  }
713
-
714
- df_actions.handle_search(null);
715
- }
716
- }
717
-
718
- let viewport: HTMLTableElement;
719
- let show_scroll_button = false;
720
-
721
- function scroll_to_top(): void {
722
- viewport.scrollTo({
723
- top: 0
724
- });
725
- }
726
-
727
- function handle_resize(): void {
728
- df_actions.set_active_cell_menu(null);
729
- df_actions.set_active_header_menu(null);
730
- selected_cells = [];
731
- selected = false;
732
- editing = false;
733
- width_calculated = false;
734
- set_cell_widths();
735
- }
736
-
737
- function add_row_at(index: number, position: "above" | "below"): void {
738
- const row_index = position === "above" ? index : index + 1;
739
- add_row(row_index);
740
- active_cell_menu = null;
741
- active_header_menu = null;
742
- }
743
-
744
- function add_col_at(index: number, position: "left" | "right"): void {
745
- const col_index = position === "left" ? index : index + 1;
746
- add_col(col_index);
747
- active_cell_menu = null;
748
- active_header_menu = null;
740
+ );
749
741
  }
750
742
 
751
- export function reset_sort_state(): void {
752
- df_actions.reset_sort_state();
753
- }
743
+ onMount(() => {
744
+ document.addEventListener("click", handle_click_outside);
745
+ return () => document.removeEventListener("click", handle_click_outside);
746
+ });
754
747
 
755
- function handle_select_all(col: number, checked: boolean): void {
756
- data = data.map((row) => {
757
- const new_row = [...row];
758
- if (new_row[col]) {
759
- new_row[col] = {
760
- ...new_row[col],
761
- value: checked
762
- };
763
- }
764
- return new_row;
748
+ function measure_row(node: HTMLElement) {
749
+ tick().then(() => {
750
+ console.log("measuring");
751
+ virtualizer.instance.measureElement(node as any);
765
752
  });
766
- }
767
-
768
- let is_dragging = false;
769
- let drag_start: [number, number] | null = null;
770
- let mouse_down_pos: { x: number; y: number } | null = null;
771
-
772
- const drag_state: DragState = {
773
- is_dragging,
774
- drag_start,
775
- mouse_down_pos
776
- };
777
753
 
778
- $: {
779
- is_dragging = drag_state.is_dragging;
780
- drag_start = drag_state.drag_start;
781
- mouse_down_pos = drag_state.mouse_down_pos;
782
- }
783
-
784
- let drag_handlers: DragHandlers;
785
-
786
- function init_drag_handlers(): void {
787
- drag_handlers = create_drag_handlers(
788
- drag_state,
789
- (value) => (is_dragging = value),
790
- (cells) => df_actions.set_selected_cells(cells),
791
- (cell) => df_actions.set_selected(cell),
792
- (event, row, col) => df_actions.handle_cell_click(event, row, col),
793
- show_row_numbers,
794
- parent
795
- );
754
+ return {
755
+ destroy() {}
756
+ };
796
757
  }
797
758
 
798
- $: if (parent) init_drag_handlers();
799
-
800
- $: handle_mouse_down = drag_handlers?.handle_mouse_down || (() => {});
801
- $: handle_mouse_move = drag_handlers?.handle_mouse_move || (() => {});
802
- $: handle_mouse_up = drag_handlers?.handle_mouse_up || (() => {});
759
+ let header_row_el: HTMLTableRowElement;
760
+ let header_table_el: HTMLTableElement;
761
+
762
+ const measurement = create_column_measurement({
763
+ header_row_el: () => header_row_el,
764
+ header_table_el: () => header_table_el,
765
+ resolved_headers: () => resolved_headers,
766
+ row_data: () => row_data,
767
+ show_row_numbers: () => show_row_numbers,
768
+ column_widths: () => column_widths,
769
+ on_resize: undefined
770
+ });
803
771
 
804
- function get_cell_display_value(row: number, col: number): string {
805
- const is_search_active = $df_state.current_search_query !== undefined;
772
+ let disable_scroll = $derived(
773
+ active_cell_menu !== null || active_header_menu !== null
774
+ );
775
+ let selected_index = $derived(selected !== false ? selected[0] : false);
806
776
 
807
- if (is_search_active && search_results?.[row]?.[col]) {
808
- return search_results[row][col].display_value !== undefined
809
- ? search_results[row][col].display_value
810
- : String(search_results[row][col].value);
777
+ $effect(() => {
778
+ if (typeof selected_index === "number") {
779
+ virtualizer.instance.scrollToIndex(selected_index, { align: "auto" });
811
780
  }
781
+ });
812
782
 
813
- if (data?.[row]?.[col]) {
814
- return data[row][col].display_value !== undefined
815
- ? data[row][col].display_value
816
- : String(data[row][col].value);
817
- }
783
+ function get_sort_info(col: number): {
784
+ direction: SortDirection | null;
785
+ priority: number | null;
786
+ } {
787
+ const col_id = `col_${col}`;
788
+ const idx = sorting.findIndex((s) => s.id === col_id);
789
+ if (idx === -1) return { direction: null, priority: null };
790
+ return { direction: sorting[idx].desc ? "desc" : "asc", priority: idx + 1 };
791
+ }
818
792
 
819
- return "";
793
+ function get_filter_active(col: number): boolean {
794
+ return column_filters.some((f) => f.id === `col_${col}`);
820
795
  }
821
796
  </script>
822
797
 
823
- <svelte:window on:resize={() => set_cell_widths()} />
798
+ <svelte:window
799
+ onresize={() => {
800
+ active_cell_menu = null;
801
+ active_header_menu = null;
802
+ }}
803
+ />
824
804
 
825
- <div class="table-container">
805
+ <div class="table-container" class:fullscreen>
826
806
  {#if (label && label.length !== 0 && show_label) || (buttons === null ? true : buttons.includes("fullscreen")) || (buttons === null ? true : buttons.includes("copy")) || show_search !== "none"}
827
807
  <div class="header-row">
828
808
  {#if label && label.length !== 0 && show_label}
829
- <div class="label">
830
- <p>{label}</p>
831
- </div>
809
+ <div class="label"><p>{label}</p></div>
832
810
  {/if}
833
811
  <Toolbar
834
812
  show_fullscreen_button={buttons === null
835
813
  ? true
836
814
  : buttons.includes("fullscreen")}
837
815
  {fullscreen}
838
- on_copy={async () => await copy_table_data(data, null)}
816
+ on_copy={handle_copy}
839
817
  show_copy_button={buttons === null ? true : buttons.includes("copy")}
840
818
  {show_search}
841
- on:search={(e) => df_actions.handle_search(e.detail)}
842
- on:fullscreen
819
+ onsearch={(query) => handle_search(query)}
820
+ {onfullscreen}
843
821
  on_commit_filter={commit_filter}
844
- current_search_query={$df_state.current_search_query}
822
+ current_search_query={global_filter || null}
845
823
  />
846
824
  </div>
847
825
  {/if}
826
+
848
827
  <div
849
828
  bind:this={parent}
850
829
  class="table-wrap"
851
830
  class:dragging={is_dragging}
852
831
  class:no-wrap={!wrap}
853
832
  class:menu-open={active_cell_menu || active_header_menu}
854
- on:keydown={(e) => handle_keydown(e, df_ctx)}
855
- on:mousemove={handle_mouse_move}
856
- on:mouseup={handle_mouse_up}
857
- on:mouseleave={handle_mouse_up}
833
+ onkeydown={handle_keydown}
858
834
  role="grid"
859
835
  tabindex="0"
860
836
  >
861
- <table bind:this={table} aria-hidden="true">
862
- {#if label && label.length !== 0}
863
- <caption class="sr-only">{label}</caption>
864
- {/if}
865
- <thead>
866
- <tr>
867
- {#if show_row_numbers}
868
- <RowNumber is_header={true} />
869
- {/if}
870
- {#each _headers as { value, id }, i (id)}
871
- <TableHeader
872
- bind:value={_headers[i].value}
873
- {i}
874
- {actual_pinned_columns}
875
- {header_edit}
876
- {selected_header}
877
- {headers}
878
- {get_cell_width}
879
- {handle_header_click}
880
- {toggle_header_menu}
881
- {end_header_edit}
882
- sort_columns={$df_state.sort_state.sort_columns}
883
- filter_columns={$df_state.filter_state.filter_columns}
884
- {latex_delimiters}
885
- {line_breaks}
886
- {max_chars}
887
- {editable}
888
- is_static={static_columns.includes(i)}
889
- {i18n}
890
- bind:el={els[id].input}
891
- {col_count}
892
- datatype={Array.isArray(datatype) ? datatype[i] : datatype}
893
- {data}
894
- on_select_all={handle_select_all}
895
- />
896
- {/each}
897
- </tr>
898
- </thead>
899
- <tbody>
900
- <tr>
901
- {#if show_row_numbers}
902
- <RowNumber index={0} />
903
- {/if}
904
- {#each max as { value, id }, j (id)}
905
- <td tabindex="-1" bind:this={cells[j]}>
906
- <div class="cell-wrap">
907
- <EditableCell
908
- {value}
909
- {latex_delimiters}
910
- {line_breaks}
911
- datatype={Array.isArray(datatype) ? datatype[j] : datatype}
912
- edit={false}
913
- el={null}
914
- {editable}
915
- {i18n}
916
- show_selection_buttons={selected_cells.length === 1 &&
917
- selected_cells[0][0] === 0 &&
918
- selected_cells[0][1] === j}
919
- coords={selected_cell_coords}
920
- on_select_column={df_actions.handle_select_column}
921
- on_select_row={df_actions.handle_select_row}
922
- {is_dragging}
923
- on:blur={handle_blur}
924
- />
925
- </div>
926
- </td>
927
- {/each}
928
- </tr>
929
- </tbody>
930
- </table>
931
837
  <Upload
932
838
  {upload}
933
839
  {stream_handler}
@@ -936,125 +842,197 @@
936
842
  boundedheight={false}
937
843
  disable_click={true}
938
844
  {root}
939
- on:load={({ detail }) =>
940
- handle_file_upload(
941
- detail.data,
942
- (head) => {
943
- _headers = make_headers(
944
- head.map((h) => h ?? ""),
945
- col_count,
946
- els,
947
- make_id
948
- );
949
- return _headers;
950
- },
951
- (vals) => {
952
- values = vals;
953
- }
954
- )}
845
+ onload={on_file_upload}
955
846
  bind:dragging
956
847
  aria_label={i18n("dataframe.drop_to_upload")}
957
848
  >
958
- <div class="table-wrap">
959
- <VirtualTable
960
- bind:items={search_results}
961
- {max_height}
962
- bind:actual_height={table_height}
963
- bind:table_scrollbar_width={scrollbar_width}
964
- selected={selected_index}
965
- disable_scroll={active_cell_menu !== null ||
966
- active_header_menu !== null}
967
- bind:viewport
968
- bind:show_scroll_button
969
- {label}
970
- on:scroll_top={(_) => {}}
971
- >
972
- <tr slot="thead">
973
- {#if show_row_numbers}
974
- <RowNumber is_header={true} />
849
+ <div
850
+ class="virtual-table-viewport"
851
+ class:disable-scroll={disable_scroll}
852
+ bind:this={scroll_container}
853
+ onscroll={handle_scroll}
854
+ style="max-height: {max_height}px;"
855
+ role="grid"
856
+ >
857
+ {#if label && label.length !== 0}
858
+ <span class="sr-only">{label}</span>
859
+ {/if}
860
+ <!-- header row: uses table layout to auto-size columns by content -->
861
+ <table class="header-table" bind:this={header_table_el}>
862
+ <thead>
863
+ <tr bind:this={header_row_el}>
864
+ {#if show_row_numbers}
865
+ <th class="row-number-header">&nbsp;</th>
866
+ {/if}
867
+ {#each header_groups as headerGroup (headerGroup.id)}
868
+ {#each headerGroup.headers as header (header.id)}
869
+ {@const col_idx =
870
+ (header.column.columnDef.meta as any)?.colIndex ?? 0}
871
+ <HeaderCell
872
+ value={String(header.column.columnDef.header ?? "")}
873
+ {col_idx}
874
+ is_editing={header_edit === col_idx}
875
+ is_selected={selected_header === col_idx}
876
+ is_static={!!(header.column.columnDef.meta as any)
877
+ ?.isStatic}
878
+ sort_direction={get_sort_info(col_idx).direction}
879
+ sort_priority={get_sort_info(col_idx).priority}
880
+ multi_sort={sorting.length > 1}
881
+ is_filtered={get_filter_active(col_idx)}
882
+ show_menu_button={col_count[1] === "dynamic"}
883
+ is_first_column={col_idx === 0 && !show_row_numbers}
884
+ {latex_delimiters}
885
+ {line_breaks}
886
+ {editable}
887
+ {max_chars}
888
+ {i18n}
889
+ onclick={handle_header_click}
890
+ on_menu_click={toggle_header_menu}
891
+ on_end_edit={end_header_edit}
892
+ />
893
+ {/each}
894
+ {/each}
895
+ </tr>
896
+ </thead>
897
+ <!-- hidden sizing row: lets table-layout:auto consider body content widths too -->
898
+ <tbody class="sizing-body" aria-hidden="true">
899
+ {#if rows.length > 0}
900
+ {@const sizing_row = rows.reduce((widest, row) => {
901
+ const cells = row.getVisibleCells();
902
+ cells.forEach((cell, i) => {
903
+ const val = String(cell.getValue() ?? "");
904
+ if (!widest[i] || val.length > widest[i].length) {
905
+ widest[i] = val;
906
+ }
907
+ });
908
+ return widest;
909
+ }, [] as string[])}
910
+ <tr>
911
+ {#if show_row_numbers}
912
+ <td class="row-number-cell">{rows.length}</td>
913
+ {/if}
914
+ {#each sizing_row as val, ci}
915
+ {@const dtype = get_dtype(ci)}
916
+ <td
917
+ ><div class="cell-wrap">
918
+ {#if dtype === "html" || dtype === "markdown"}{@html val}{:else}{val}{/if}
919
+ </div></td
920
+ >
921
+ {/each}
922
+ </tr>
975
923
  {/if}
976
- {#each _headers as { value, id }, i (id)}
977
- <TableHeader
978
- bind:value={_headers[i].value}
979
- {i}
980
- {actual_pinned_columns}
981
- {header_edit}
982
- {selected_header}
983
- {headers}
984
- {get_cell_width}
985
- {handle_header_click}
986
- {toggle_header_menu}
987
- {end_header_edit}
988
- sort_columns={$df_state.sort_state.sort_columns}
989
- filter_columns={$df_state.filter_state.filter_columns}
990
- {latex_delimiters}
991
- {line_breaks}
992
- {max_chars}
993
- {editable}
994
- is_static={static_columns.includes(i)}
995
- {i18n}
996
- bind:el={els[id].input}
997
- {col_count}
998
- datatype={Array.isArray(datatype) ? datatype[i] : datatype}
999
- {data}
1000
- on_select_all={handle_select_all}
1001
- />
1002
- {/each}
1003
- </tr>
1004
- <tr slot="tbody" let:item let:index class:row-odd={index % 2 === 0}>
1005
- {#if show_row_numbers}
1006
- <RowNumber {index} />
924
+ </tbody>
925
+ </table>
926
+
927
+ <!-- table body: absolutely positioned rows (standard tanstack virtual pattern) -->
928
+ <div
929
+ class="virtual-body"
930
+ style="height: {total_size}px; position: relative; flex-shrink: 0; width: {measurement.total_header_width
931
+ ? `${measurement.total_header_width}px`
932
+ : '100%'};"
933
+ >
934
+ {#each virtual_items as virtual_row (virtual_row.key)}
935
+ {@const row = rows[virtual_row.index]}
936
+ {@const row_idx = row?.original._index ?? virtual_row.index}
937
+ {#if row}
938
+ <div
939
+ class="virtual-row"
940
+ class:row-odd={virtual_row.index % 2 !== 0}
941
+ data-index={virtual_row.index}
942
+ style="position: absolute; top: 0; left: 0; width: 100%; transform: translateY({virtual_row.start}px);{selected_cells.some(
943
+ ([r]) => r === row_idx
944
+ )
945
+ ? ' z-index: 3;'
946
+ : ''}"
947
+ use:measure_row={row}
948
+ >
949
+ {#if show_row_numbers}
950
+ <div
951
+ class="row-number-cell"
952
+ data-row={row_idx}
953
+ data-col="row-number"
954
+ style="flex: 0 0 {measurement.row_num_width}px; width: {measurement.row_num_width}px;"
955
+ >
956
+ {row_idx + 1}
957
+ </div>
958
+ {/if}
959
+ {#each row.getVisibleCells() as cell, ci (cell.id)}
960
+ {@const col_idx =
961
+ (cell.column.columnDef.meta as any)?.colIndex ?? 0}
962
+ {@const is_sel = is_cell_in_selection(
963
+ [row_idx, col_idx],
964
+ selected_cells
965
+ )}
966
+ <DataCell
967
+ value={cell.getValue() as CellValue}
968
+ display_value={get_display_value(row_idx, col_idx)}
969
+ datatype={get_dtype(col_idx)}
970
+ {row_idx}
971
+ {col_idx}
972
+ col_style={measurement.get_col_style(ci)}
973
+ cell_style={get_styling(row_idx, col_idx)}
974
+ selection_classes={is_cell_selected(
975
+ [row_idx, col_idx],
976
+ selected_cells
977
+ )}
978
+ is_editing={!!(
979
+ editing &&
980
+ editing[0] === row_idx &&
981
+ editing[1] === col_idx
982
+ )}
983
+ is_flash={copy_flash && is_sel}
984
+ is_static={!!(cell.column.columnDef.meta as any)?.isStatic}
985
+ show_menu_button={editable &&
986
+ selected_cells.length === 1 &&
987
+ selected_cells[0][0] === row_idx &&
988
+ selected_cells[0][1] === col_idx}
989
+ show_selection_buttons={selected_cells.length === 1 &&
990
+ selected_cells[0][0] === row_idx &&
991
+ selected_cells[0][1] === col_idx}
992
+ is_first_column={ci === 0 && !show_row_numbers}
993
+ {latex_delimiters}
994
+ {line_breaks}
995
+ {editable}
996
+ {max_chars}
997
+ {i18n}
998
+ {components}
999
+ {is_dragging}
1000
+ wrap_text={wrap}
1001
+ onmousedown={(e) => handle_cell_click(e, row_idx, col_idx)}
1002
+ ondblclick={(e) =>
1003
+ handle_cell_dblclick(e, row_idx, col_idx)}
1004
+ oncontextmenu={(e) => {
1005
+ e.preventDefault();
1006
+ toggle_cell_menu(e, row_idx, col_idx);
1007
+ }}
1008
+ onblur={handle_blur}
1009
+ on_menu_click={(e) => toggle_cell_menu(e, row_idx, col_idx)}
1010
+ on_select_column={(c) => {
1011
+ selected_cells = rows.map(
1012
+ (_, r) => [r, c] as CellCoordinate
1013
+ );
1014
+ selected = selected_cells[0];
1015
+ }}
1016
+ on_select_row={(r) => {
1017
+ selected_cells = resolved_headers.map(
1018
+ (_, c) => [r, c] as CellCoordinate
1019
+ );
1020
+ selected = selected_cells[0];
1021
+ }}
1022
+ />
1023
+ {/each}
1024
+ </div>
1007
1025
  {/if}
1008
- {#each item as { value, id }, j (id)}
1009
- <TableCell
1010
- bind:value={search_results[index][j].value}
1011
- display_value={get_cell_display_value(index, j)}
1012
- index={$df_state.current_search_query !== undefined &&
1013
- filtered_to_original_map[index] !== undefined
1014
- ? filtered_to_original_map[index]
1015
- : index}
1016
- {j}
1017
- {actual_pinned_columns}
1018
- {get_cell_width}
1019
- handle_cell_click={(e, r, c) => handle_mouse_down(e, r, c)}
1020
- {handle_blur}
1021
- toggle_cell_menu={df_actions.toggle_cell_menu}
1022
- {is_cell_selected}
1023
- {should_show_cell_menu}
1024
- {selected_cells}
1025
- {copy_flash}
1026
- {active_cell_menu}
1027
- styling={search_results[index][j].styling}
1028
- {latex_delimiters}
1029
- {line_breaks}
1030
- datatype={Array.isArray(datatype) ? datatype[j] : datatype}
1031
- {editing}
1032
- {max_chars}
1033
- {editable}
1034
- is_static={static_columns.includes(j)}
1035
- {i18n}
1036
- {components}
1037
- handle_select_column={df_actions.handle_select_column}
1038
- handle_select_row={df_actions.handle_select_row}
1039
- bind:el={els[id]}
1040
- {is_dragging}
1041
- {wrap}
1042
- />
1043
- {/each}
1044
- </tr>
1045
- </VirtualTable>
1026
+ {/each}
1027
+ </div>
1046
1028
  </div>
1047
1029
  </Upload>
1030
+
1048
1031
  {#if show_scroll_button}
1049
- <button class="scroll-top-button" on:click={scroll_to_top}>
1050
- &uarr;
1051
- </button>
1032
+ <button class="scroll-top-button" onclick={scroll_to_top}>&uarr;</button>
1052
1033
  {/if}
1053
1034
  </div>
1054
1035
  </div>
1055
- {#if data.length === 0 && editable && row_count[1] === "dynamic"}
1056
- <EmptyRowButton on_click={() => add_row()} />
1057
- {/if}
1058
1036
 
1059
1037
  {#if active_cell_menu || active_header_menu}
1060
1038
  <CellMenu
@@ -1079,66 +1057,96 @@
1079
1057
  on_delete_col={() =>
1080
1058
  delete_col_at(active_cell_menu?.col ?? active_header_menu?.col ?? -1)}
1081
1059
  {editable}
1082
- can_delete_rows={!active_header_menu && data.length > 1 && editable}
1083
- can_delete_cols={data.length > 0 && data[0]?.length > 1 && editable}
1060
+ can_delete_rows={!active_header_menu && values.length > 1 && editable}
1061
+ can_delete_cols={values.length > 0 &&
1062
+ (values[0]?.length ?? 0) > 1 &&
1063
+ editable}
1084
1064
  {i18n}
1085
1065
  on_sort={active_header_menu
1086
1066
  ? (direction) => {
1087
- if (active_header_menu) {
1088
- handle_sort(active_header_menu.col, direction);
1089
- df_actions.set_active_header_menu(null);
1090
- }
1067
+ handle_sort(active_header_menu!.col, direction);
1068
+ active_header_menu = null;
1091
1069
  }
1092
1070
  : undefined}
1093
1071
  on_clear_sort={active_header_menu
1094
1072
  ? () => {
1095
1073
  clear_sort();
1096
- df_actions.set_active_header_menu(null);
1074
+ active_header_menu = null;
1097
1075
  }
1098
1076
  : undefined}
1099
1077
  sort_direction={active_header_menu
1100
- ? ($df_state.sort_state.sort_columns.find(
1101
- (item) => item.col === (active_header_menu?.col ?? -1)
1102
- )?.direction ?? null)
1078
+ ? get_sort_info(active_header_menu.col).direction
1103
1079
  : null}
1104
1080
  sort_priority={active_header_menu
1105
- ? $df_state.sort_state.sort_columns.findIndex(
1106
- (item) => item.col === (active_header_menu?.col ?? -1)
1107
- ) + 1 || null
1081
+ ? get_sort_info(active_header_menu.col).priority
1108
1082
  : null}
1109
1083
  on_filter={active_header_menu
1110
- ? (datatype, filter, value) => {
1111
- if (active_header_menu) {
1112
- handle_filter(active_header_menu.col, datatype, filter, value);
1113
- df_actions.set_active_header_menu(null);
1114
- }
1084
+ ? (dtype, filter, fvalue) => {
1085
+ handle_filter(active_header_menu!.col, dtype, filter, fvalue);
1086
+ active_header_menu = null;
1115
1087
  }
1116
1088
  : undefined}
1117
1089
  on_clear_filter={active_header_menu
1118
1090
  ? () => {
1119
1091
  clear_filter();
1120
- df_actions.set_active_header_menu(null);
1092
+ active_header_menu = null;
1121
1093
  }
1122
1094
  : undefined}
1123
1095
  filter_active={active_header_menu
1124
- ? $df_state.filter_state.filter_columns.some(
1125
- (c) => c.col === (active_header_menu?.col ?? -1)
1126
- )
1096
+ ? get_filter_active(active_header_menu.col)
1127
1097
  : null}
1128
1098
  />
1129
1099
  {/if}
1130
1100
 
1101
+ {#if values.length === 0 && editable && row_count[1] === "dynamic"}
1102
+ <EmptyRowButton on_click={() => add_row()} />
1103
+ {/if}
1104
+
1131
1105
  <style>
1132
1106
  .table-container {
1133
1107
  display: flex;
1134
1108
  flex-direction: column;
1135
1109
  gap: var(--size-2);
1136
1110
  position: relative;
1111
+ max-width: 100%;
1112
+ overflow: hidden;
1113
+ }
1114
+
1115
+ .table-container.fullscreen {
1116
+ padding: var(--size-4);
1117
+ height: 100%;
1118
+ box-sizing: border-box;
1119
+ }
1120
+
1121
+ .table-container.fullscreen .table-wrap {
1122
+ flex: 1 1 auto;
1123
+ min-height: 0;
1124
+ display: flex;
1125
+ flex-direction: column;
1126
+ }
1127
+
1128
+ .table-container.fullscreen .table-wrap > :global(*) {
1129
+ flex: 1 1 auto;
1130
+ min-height: 0;
1131
+ display: flex;
1132
+ flex-direction: column;
1133
+ }
1134
+
1135
+ .table-container.fullscreen .virtual-table-viewport {
1136
+ max-height: none !important;
1137
+ flex: 1 1 auto;
1138
+ min-height: 0;
1137
1139
  }
1138
1140
 
1139
1141
  .table-wrap {
1140
1142
  position: relative;
1141
1143
  transition: 150ms;
1144
+ width: 100%;
1145
+ }
1146
+
1147
+ /* Constrain Upload component wrapper */
1148
+ .table-wrap > :global(*) {
1149
+ max-width: 100%;
1142
1150
  }
1143
1151
 
1144
1152
  .table-wrap.menu-open {
@@ -1165,60 +1173,147 @@
1165
1173
  overflow: hidden;
1166
1174
  }
1167
1175
 
1168
- table {
1169
- position: absolute;
1170
- opacity: 0;
1171
- z-index: -1;
1172
- transition: 150ms;
1173
- width: var(--size-full);
1174
- table-layout: auto;
1176
+ /* Virtual scroll container */
1177
+ .virtual-table-viewport {
1178
+ display: flex;
1179
+ flex-direction: column;
1180
+ overflow: auto;
1181
+ position: relative;
1182
+ -webkit-overflow-scrolling: touch;
1183
+ min-width: 0;
1184
+ max-width: 100%;
1185
+ scrollbar-width: thin;
1186
+ scrollbar-color: rgba(128, 128, 128, 0.5) transparent;
1187
+ }
1188
+
1189
+ .virtual-table-viewport::-webkit-scrollbar {
1190
+ width: 4px;
1191
+ height: 4px;
1192
+ }
1193
+
1194
+ .virtual-table-viewport::-webkit-scrollbar-thumb {
1195
+ background-color: rgba(128, 128, 128, 0.5);
1196
+ border-radius: 4px;
1197
+ }
1198
+
1199
+ .virtual-table-viewport:hover {
1200
+ scrollbar-color: rgba(160, 160, 160, 0.7) transparent;
1201
+ }
1202
+
1203
+ .virtual-table-viewport.disable-scroll {
1204
+ overflow: hidden !important;
1205
+ }
1206
+
1207
+ /* Header table: auto-sizes columns by content, sticky */
1208
+ .header-table {
1209
+ width: 100%;
1175
1210
  color: var(--body-text-color);
1176
1211
  font-size: var(--input-text-size);
1177
1212
  line-height: var(--line-md);
1178
1213
  font-family: var(--font-mono);
1179
1214
  border-spacing: 0;
1180
1215
  border-collapse: separate;
1181
- }
1182
-
1183
- thead {
1216
+ table-layout: auto;
1184
1217
  position: sticky;
1185
1218
  top: 0;
1186
- z-index: 5;
1187
- box-shadow: var(--shadow-drop);
1219
+ z-index: 7;
1220
+ flex-shrink: 0;
1188
1221
  }
1189
1222
 
1190
- thead :global(th.pinned-column) {
1191
- position: sticky;
1192
- z-index: 6;
1193
- background: var(--table-even-background-fill) !important;
1223
+ /* Hidden sizing row — visibility:collapse hides the row but keeps column width contribution */
1224
+ .sizing-body tr {
1225
+ visibility: collapse;
1194
1226
  }
1195
1227
 
1196
- .dragging {
1197
- border-color: var(--color-accent);
1228
+ .sizing-body td {
1229
+ padding: var(--size-2);
1230
+ border: none;
1231
+ white-space: nowrap;
1198
1232
  }
1199
1233
 
1200
- .no-wrap {
1201
- white-space: nowrap;
1234
+ /* Virtual body */
1235
+ .virtual-body {
1236
+ box-sizing: border-box;
1202
1237
  }
1203
1238
 
1204
- div:not(.no-wrap) td {
1205
- overflow-wrap: anywhere;
1239
+ .virtual-row {
1240
+ display: flex;
1241
+ align-items: stretch;
1242
+ background: var(--table-odd-background-fill);
1243
+
1244
+ text-align: left;
1245
+ font-size: var(--input-text-size);
1246
+ line-height: var(--line-md);
1247
+ font-family: var(--font-mono);
1248
+ color: var(--body-text-color);
1249
+ min-height: var(--size-9);
1206
1250
  }
1207
1251
 
1208
- div.no-wrap td {
1209
- overflow-x: hidden;
1252
+ .virtual-row:last-child {
1253
+ border-bottom: none;
1210
1254
  }
1211
1255
 
1212
- tr {
1256
+ .virtual-row.row-odd {
1213
1257
  background: var(--table-even-background-fill);
1214
1258
  }
1215
1259
 
1216
- tr.row-odd {
1217
- background: var(--table-odd-background-fill);
1260
+ .virtual-row:hover {
1261
+ background: var(--table-row-focus);
1218
1262
  }
1219
1263
 
1220
- tr:hover {
1221
- background: var(--table-row-focus);
1264
+ /* Cell content wrapper (used in sizing-body) */
1265
+ .cell-wrap {
1266
+ display: flex;
1267
+ align-items: center;
1268
+ justify-content: flex-start;
1269
+ outline: none;
1270
+ min-height: var(--size-9);
1271
+ position: relative;
1272
+ height: 100%;
1273
+ padding: var(--size-2);
1274
+ box-sizing: border-box;
1275
+ gap: var(--size-1);
1276
+ overflow: visible;
1277
+ min-width: 0;
1278
+ }
1279
+
1280
+ /* Row number cells */
1281
+ .row-number-header {
1282
+ text-align: center;
1283
+ padding: var(--size-1);
1284
+ min-width: var(--size-12);
1285
+ width: var(--size-12);
1286
+ font-weight: var(--weight-semibold);
1287
+ border-right: 1px solid var(--border-color-primary);
1288
+ background: var(--table-even-background-fill) !important;
1289
+ }
1290
+
1291
+ .row-number-cell {
1292
+ text-align: center;
1293
+ padding: var(--size-1);
1294
+ min-width: var(--size-12);
1295
+ width: var(--size-12);
1296
+ flex-shrink: 0;
1297
+ overflow: hidden;
1298
+ text-overflow: ellipsis;
1299
+ white-space: nowrap;
1300
+ font-weight: var(--weight-semibold);
1301
+ border-right: 1px solid var(--border-color-primary);
1302
+ display: flex;
1303
+ align-items: center;
1304
+ justify-content: center;
1305
+ }
1306
+
1307
+ .no-wrap {
1308
+ white-space: nowrap;
1309
+ }
1310
+
1311
+ div:not(.no-wrap) :global(td) {
1312
+ overflow-wrap: anywhere;
1313
+ }
1314
+
1315
+ div.no-wrap :global(td) {
1316
+ overflow-x: hidden;
1222
1317
  }
1223
1318
 
1224
1319
  .header-row {
@@ -1242,8 +1337,6 @@
1242
1337
  color: var(--block-label-text-color);
1243
1338
  font-size: var(--block-label-text-size);
1244
1339
  line-height: var(--line-sm);
1245
- position: relative;
1246
- z-index: 4;
1247
1340
  }
1248
1341
 
1249
1342
  .scroll-top-button {
@@ -1268,9 +1361,4 @@
1268
1361
  .scroll-top-button:hover {
1269
1362
  opacity: 1;
1270
1363
  }
1271
-
1272
- tr {
1273
- border-bottom: 1px solid var(--border-color-primary);
1274
- text-align: left;
1275
- }
1276
1364
  </style>