@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,93 @@
1
+ <script lang="ts">
2
+ export let position: "column" | "row";
3
+ export let coords: [number, number];
4
+ export let on_click: (() => void) | null = null;
5
+
6
+ $: is_first_position =
7
+ position === "column" ? coords[0] === 0 : coords[1] === 0;
8
+ $: direction =
9
+ position === "column"
10
+ ? is_first_position
11
+ ? "down"
12
+ : "up"
13
+ : is_first_position
14
+ ? "right"
15
+ : "left";
16
+ </script>
17
+
18
+ <button
19
+ class="selection-button selection-button-{position} {is_first_position
20
+ ? `move-${direction}`
21
+ : ''}"
22
+ on:click|stopPropagation={() => on_click && on_click()}
23
+ aria-label={`Select ${position}`}
24
+ >
25
+ <span class={direction}>
26
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
27
+ <path
28
+ d="m16.707 13.293-4-4a1 1 0 0 0-1.414 0l-4 4A1 1 0 0 0 8 15h8a1 1 0 0 0 .707-1.707z"
29
+ data-name={direction}
30
+ />
31
+ </svg>
32
+ </span>
33
+ </button>
34
+
35
+ <style>
36
+ .selection-button {
37
+ position: absolute;
38
+ background: var(--color-accent);
39
+ width: var(--size-3);
40
+ height: var(--size-5);
41
+ color: var(--background-fill-primary);
42
+ }
43
+
44
+ .selection-button-column {
45
+ top: -15px;
46
+ left: 50%;
47
+ transform: translateX(-50%) rotate(90deg);
48
+ border-radius: var(--radius-sm) 0 0 var(--radius-sm);
49
+ }
50
+
51
+ .selection-button-row {
52
+ left: calc(var(--size-2-5) * -1);
53
+ border-radius: var(--radius-sm) 0 0 var(--radius-sm);
54
+ }
55
+
56
+ .move-down {
57
+ bottom: -14px;
58
+ top: auto;
59
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
60
+ }
61
+
62
+ .move-right {
63
+ left: auto;
64
+ right: calc(var(--size-2-5) * -1);
65
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
66
+ }
67
+
68
+ svg {
69
+ fill: currentColor;
70
+ }
71
+
72
+ span {
73
+ display: flex;
74
+ width: 100%;
75
+ height: 100%;
76
+ }
77
+
78
+ .up {
79
+ transform: rotate(-90deg);
80
+ }
81
+
82
+ .down {
83
+ transform: rotate(90deg);
84
+ }
85
+
86
+ .left {
87
+ transform: rotate(-90deg);
88
+ }
89
+
90
+ .right {
91
+ transform: rotate(90deg);
92
+ }
93
+ </style>
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ export let size = 16;
3
+ </script>
4
+
5
+ <svg
6
+ width={size}
7
+ height={size}
8
+ viewBox="0 0 16 16"
9
+ fill="none"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ >
12
+ <path
13
+ d="M4 8L8 12L12 8"
14
+ stroke="currentColor"
15
+ stroke-width="1.5"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ />
19
+ <path
20
+ d="M8 12V4"
21
+ stroke="currentColor"
22
+ stroke-width="1.5"
23
+ stroke-linecap="round"
24
+ />
25
+ </svg>
@@ -0,0 +1,25 @@
1
+ <script lang="ts">
2
+ export let size = 16;
3
+ </script>
4
+
5
+ <svg
6
+ width={size}
7
+ height={size}
8
+ viewBox="0 0 16 16"
9
+ fill="none"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ >
12
+ <path
13
+ d="M4 8L8 4L12 8"
14
+ stroke="currentColor"
15
+ stroke-width="1.5"
16
+ stroke-linecap="round"
17
+ stroke-linejoin="round"
18
+ />
19
+ <path
20
+ d="M8 4V12"
21
+ stroke="currentColor"
22
+ stroke-width="1.5"
23
+ stroke-linecap="round"
24
+ />
25
+ </svg>
@@ -0,0 +1,14 @@
1
+ <svg
2
+ viewBox="0 0 24 24"
3
+ fill="none"
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ focusable="false"
6
+ >
7
+ <path
8
+ d="M7 10l5 5 5-5"
9
+ stroke="currentColor"
10
+ stroke-width="2"
11
+ stroke-linecap="round"
12
+ stroke-linejoin="round"
13
+ />
14
+ </svg>
@@ -0,0 +1,15 @@
1
+ <svg
2
+ viewBox="0 0 24 24"
3
+ fill="none"
4
+ xmlns="http://www.w3.org/2000/svg"
5
+ aria-hidden="true"
6
+ focusable="false"
7
+ >
8
+ <path
9
+ d="M7 14l5-5 5 5"
10
+ stroke="currentColor"
11
+ stroke-width="2"
12
+ stroke-linecap="round"
13
+ stroke-linejoin="round"
14
+ />
15
+ </svg>
@@ -1,61 +1,43 @@
1
1
  <script lang="ts">
2
2
  import { createEventDispatcher } from "svelte";
3
3
  import type { I18nFormatter } from "@gradio/utils";
4
-
5
- type SortDirection = "asc" | "des";
4
+ import SortButtonUp from "./SortButtonUp.svelte";
5
+ import SortButtonDown from "./SortButtonDown.svelte";
6
+ import { IconButton } from "@gradio/atoms";
7
+ type SortDirection = "asc" | "desc";
6
8
  export let direction: SortDirection | null = null;
9
+ export let priority: number | null = null;
7
10
  export let i18n: I18nFormatter;
8
11
 
9
12
  const dispatch = createEventDispatcher<{ sort: SortDirection }>();
10
13
  </script>
11
14
 
12
15
  <div class="sort-icons" role="group" aria-label={i18n("dataframe.sort_column")}>
13
- <button
14
- class="sort-button up"
15
- class:active={direction === "asc"}
16
- on:click={() => dispatch("sort", "asc")}
17
- aria-label={i18n("dataframe.sort_ascending")}
18
- aria-pressed={direction === "asc"}
19
- >
20
- <svg
21
- viewBox="0 0 24 24"
22
- fill="none"
23
- xmlns="http://www.w3.org/2000/svg"
24
- aria-hidden="true"
25
- focusable="false"
26
- >
27
- <path
28
- d="M7 14l5-5 5 5"
29
- stroke="currentColor"
30
- stroke-width="2"
31
- stroke-linecap="round"
32
- stroke-linejoin="round"
33
- />
34
- </svg>
35
- </button>
36
- <button
37
- class="sort-button down"
38
- class:active={direction === "des"}
39
- on:click={() => dispatch("sort", "des")}
40
- aria-label={i18n("dataframe.sort_descending")}
41
- aria-pressed={direction === "des"}
42
- >
43
- <svg
44
- viewBox="0 0 24 24"
45
- fill="none"
46
- xmlns="http://www.w3.org/2000/svg"
47
- aria-hidden="true"
48
- focusable="false"
16
+ {#if (direction === "asc" || direction === "desc") && priority !== null}
17
+ <span aria-label={`Sort priority: ${priority}`} class="priority"
18
+ >{priority}</span
49
19
  >
50
- <path
51
- d="M7 10l5 5 5-5"
52
- stroke="currentColor"
53
- stroke-width="2"
54
- stroke-linecap="round"
55
- stroke-linejoin="round"
56
- />
57
- </svg>
58
- </button>
20
+ {/if}
21
+ <IconButton
22
+ size="x-small"
23
+ label={i18n("dataframe.sort_ascending")}
24
+ Icon={SortButtonUp}
25
+ highlight={direction === "asc"}
26
+ on:click={(event) => {
27
+ event.stopPropagation();
28
+ dispatch("sort", "asc");
29
+ }}
30
+ ></IconButton>
31
+ <IconButton
32
+ size="x-small"
33
+ label={i18n("dataframe.sort_descending")}
34
+ Icon={SortButtonDown}
35
+ highlight={direction === "desc"}
36
+ on:click={(event) => {
37
+ event.stopPropagation();
38
+ dispatch("sort", "desc");
39
+ }}
40
+ ></IconButton>
59
41
  </div>
60
42
 
61
43
  <style>
@@ -63,33 +45,28 @@
63
45
  display: flex;
64
46
  flex-direction: column;
65
47
  gap: 0;
66
- margin-right: var(--spacing-md);
48
+ margin-right: var(--spacing-sm);
67
49
  }
68
50
 
69
- .sort-button {
51
+ .sort-icons :global(button) {
52
+ margin-bottom: var(--spacing-xs);
53
+ border: 1px solid var(--border-color-primary);
54
+ background: unset;
55
+ }
56
+
57
+ .priority {
70
58
  display: flex;
71
59
  align-items: center;
72
60
  justify-content: center;
73
- padding: 0;
74
- background: none;
75
- border: none;
76
- cursor: pointer;
77
- opacity: 0.5;
78
- transition: opacity 150ms;
79
- }
80
-
81
- .sort-button:hover {
82
- opacity: 0.8;
83
- }
84
-
85
- .sort-button.active {
86
- opacity: 1;
87
- color: var(--color-accent);
88
- }
89
-
90
- svg {
91
- width: var(--size-3);
92
- height: var(--size-3);
93
- display: block;
61
+ position: absolute;
62
+ font-size: var(--size-2);
63
+ left: 19px;
64
+ z-index: var(--layer-3);
65
+ top: var(--spacing-xs);
66
+ background-color: var(--button-secondary-background-fill);
67
+ color: var(--body-text-color);
68
+ border-radius: var(--radius-sm);
69
+ width: var(--size-2-5);
70
+ height: var(--size-2-5);
94
71
  }
95
72
  </style>
@@ -2,6 +2,14 @@ import type { CellCoordinate, EditingState } from "./types";
2
2
 
3
3
  export type CellData = { id: string; value: string | number };
4
4
 
5
+ export function is_cell_in_selection(
6
+ coords: [number, number],
7
+ selected_cells: [number, number][]
8
+ ): boolean {
9
+ const [row, col] = coords;
10
+ return selected_cells.some(([r, c]) => r === row && c === col);
11
+ }
12
+
5
13
  export function is_cell_selected(
6
14
  cell: CellCoordinate,
7
15
  selected_cells: CellCoordinate[]
@@ -29,8 +37,14 @@ export function get_range_selection(
29
37
  const max_col = Math.max(start_col, end_col);
30
38
 
31
39
  const cells: CellCoordinate[] = [];
40
+ // Add the start cell as the "anchor" cell so that when
41
+ // we press shift+arrow keys, the selection will always
42
+ // include the anchor cell.
43
+ cells.push(start);
44
+
32
45
  for (let i = min_row; i <= max_row; i++) {
33
46
  for (let j = min_col; j <= max_col; j++) {
47
+ if (i === start_row && j === start_col) continue;
34
48
  cells.push([i, j]);
35
49
  }
36
50
  }
@@ -137,10 +151,11 @@ export function get_next_cell_coordinates(
137
151
  }
138
152
 
139
153
  export function move_cursor(
140
- key: "ArrowRight" | "ArrowLeft" | "ArrowDown" | "ArrowUp",
154
+ event: KeyboardEvent,
141
155
  current_coords: CellCoordinate,
142
156
  data: CellData[][]
143
157
  ): CellCoordinate | false {
158
+ const key = event.key as "ArrowRight" | "ArrowLeft" | "ArrowDown" | "ArrowUp";
144
159
  const dir = {
145
160
  ArrowRight: [0, 1],
146
161
  ArrowLeft: [0, -1],
@@ -148,8 +163,27 @@ export function move_cursor(
148
163
  ArrowUp: [-1, 0]
149
164
  }[key];
150
165
 
151
- const i = current_coords[0] + dir[0];
152
- const j = current_coords[1] + dir[1];
166
+ let i, j;
167
+ if (event.metaKey || event.ctrlKey) {
168
+ if (key === "ArrowRight") {
169
+ i = current_coords[0];
170
+ j = data[0].length - 1;
171
+ } else if (key === "ArrowLeft") {
172
+ i = current_coords[0];
173
+ j = 0;
174
+ } else if (key === "ArrowDown") {
175
+ i = data.length - 1;
176
+ j = current_coords[1];
177
+ } else if (key === "ArrowUp") {
178
+ i = 0;
179
+ j = current_coords[1];
180
+ } else {
181
+ return false;
182
+ }
183
+ } else {
184
+ i = current_coords[0] + dir[0];
185
+ j = current_coords[1] + dir[1];
186
+ }
153
187
 
154
188
  if (i < 0 && j <= 0) {
155
189
  return false;
@@ -206,25 +240,17 @@ export function calculate_selection_positions(
206
240
  return { col_pos: "0px", row_pos: undefined };
207
241
  }
208
242
 
209
- let offset = 0;
210
- for (let i = 0; i < col; i++) {
211
- offset += parseFloat(
212
- getComputedStyle(parent).getPropertyValue(`--cell-width-${i}`)
213
- );
214
- }
215
-
216
243
  const cell_id = data[row][col].id;
217
244
  const cell_el = els[cell_id]?.cell;
218
245
 
219
246
  if (!cell_el) {
220
- // if we cant get the row position, just return the column position which is static
221
247
  return { col_pos: "0px", row_pos: undefined };
222
248
  }
223
249
 
224
250
  const cell_rect = cell_el.getBoundingClientRect();
225
251
  const table_rect = table.getBoundingClientRect();
226
252
  const col_pos = `${cell_rect.left - table_rect.left + cell_rect.width / 2}px`;
227
- const relative_top = cell_rect.top - table_rect.top;
228
- const row_pos = `${relative_top + cell_rect.height / 2}px`;
253
+ const row_pos = `${cell_rect.top - table_rect.top + cell_rect.height / 2}px`;
254
+
229
255
  return { col_pos, row_pos };
230
256
  }
@@ -0,0 +1,72 @@
1
+ import type { Headers, HeadersWithIDs } from "../utils";
2
+
3
+ export function make_headers(
4
+ _head: Headers,
5
+ col_count: [number, "fixed" | "dynamic"],
6
+ els: Record<
7
+ string,
8
+ { cell: null | HTMLTableCellElement; input: null | HTMLInputElement }
9
+ >,
10
+ make_id: () => string
11
+ ): HeadersWithIDs {
12
+ let _h = _head || [];
13
+ if (col_count[1] === "fixed" && _h.length < col_count[0]) {
14
+ const fill = Array(col_count[0] - _h.length)
15
+ .fill("")
16
+ .map((_, i) => `${i + _h.length}`);
17
+ _h = _h.concat(fill);
18
+ }
19
+
20
+ if (!_h || _h.length === 0) {
21
+ return Array(col_count[0])
22
+ .fill(0)
23
+ .map((_, i) => {
24
+ const _id = make_id();
25
+ els[_id] = { cell: null, input: null };
26
+ return { id: _id, value: JSON.stringify(i + 1) };
27
+ });
28
+ }
29
+
30
+ return _h.map((h, i) => {
31
+ const _id = make_id();
32
+ els[_id] = { cell: null, input: null };
33
+ return { id: _id, value: h ?? "" };
34
+ });
35
+ }
36
+
37
+ export function process_data(
38
+ values: (string | number)[][],
39
+ els: Record<
40
+ string,
41
+ { cell: null | HTMLTableCellElement; input: null | HTMLInputElement }
42
+ >,
43
+ data_binding: Record<string, any>,
44
+ make_id: () => string,
45
+ display_value: string[][] | null = null
46
+ ): { id: string; value: string | number; display_value?: string }[][] {
47
+ if (!values || values.length === 0) {
48
+ return [];
49
+ }
50
+
51
+ const result = values.map((row, i) => {
52
+ return row.map((value, j) => {
53
+ const _id = make_id();
54
+ els[_id] = { cell: null, input: null };
55
+ data_binding[_id] = value;
56
+
57
+ let display = display_value?.[i]?.[j];
58
+
59
+ if (display === undefined) {
60
+ display = String(value);
61
+ }
62
+
63
+ return {
64
+ id: _id,
65
+ value,
66
+ display_value: display
67
+ };
68
+ });
69
+ });
70
+
71
+ return result;
72
+ }
@@ -0,0 +1,92 @@
1
+ import type { CellCoordinate } from "../types";
2
+ import { get_range_selection } from "../selection_utils";
3
+
4
+ export type DragState = {
5
+ is_dragging: boolean;
6
+ drag_start: CellCoordinate | null;
7
+ mouse_down_pos: { x: number; y: number } | null;
8
+ };
9
+
10
+ export type DragHandlers = {
11
+ handle_mouse_down: (event: MouseEvent, row: number, col: number) => void;
12
+ handle_mouse_move: (event: MouseEvent) => void;
13
+ handle_mouse_up: (event: MouseEvent) => void;
14
+ };
15
+
16
+ export function create_drag_handlers(
17
+ state: DragState,
18
+ set_is_dragging: (value: boolean) => void,
19
+ set_selected_cells: (cells: CellCoordinate[]) => void,
20
+ set_selected: (cell: CellCoordinate | false) => void,
21
+ handle_cell_click: (event: MouseEvent, row: number, col: number) => void,
22
+ show_row_numbers: boolean,
23
+ parent_element?: HTMLElement
24
+ ): DragHandlers {
25
+ const start_drag = (event: MouseEvent, row: number, col: number): void => {
26
+ if (
27
+ event.target instanceof HTMLAnchorElement ||
28
+ (show_row_numbers && col === -1)
29
+ )
30
+ return;
31
+
32
+ event.preventDefault();
33
+ event.stopPropagation();
34
+
35
+ state.mouse_down_pos = { x: event.clientX, y: event.clientY };
36
+ state.drag_start = [row, col];
37
+
38
+ if (!event.shiftKey && !event.metaKey && !event.ctrlKey) {
39
+ set_selected_cells([[row, col]]);
40
+ set_selected([row, col]);
41
+ }
42
+ };
43
+
44
+ const update_selection = (event: MouseEvent): void => {
45
+ const cell = (event.target as HTMLElement).closest("td");
46
+ if (!cell) return;
47
+
48
+ const row = parseInt(cell.getAttribute("data-row") || "0");
49
+ const col = parseInt(cell.getAttribute("data-col") || "0");
50
+
51
+ if (isNaN(row) || isNaN(col)) return;
52
+
53
+ const selection_range = get_range_selection(state.drag_start!, [row, col]);
54
+ set_selected_cells(selection_range);
55
+ set_selected([row, col]);
56
+ };
57
+
58
+ const end_drag = (event: MouseEvent): void => {
59
+ if (!state.is_dragging && state.drag_start) {
60
+ handle_cell_click(event, state.drag_start[0], state.drag_start[1]);
61
+ } else if (state.is_dragging && parent_element) {
62
+ parent_element.focus();
63
+ }
64
+
65
+ state.is_dragging = false;
66
+ set_is_dragging(false);
67
+ state.drag_start = null;
68
+ state.mouse_down_pos = null;
69
+ };
70
+
71
+ return {
72
+ handle_mouse_down: start_drag,
73
+
74
+ handle_mouse_move(event: MouseEvent): void {
75
+ if (!state.drag_start || !state.mouse_down_pos) return;
76
+
77
+ const dx = Math.abs(event.clientX - state.mouse_down_pos.x);
78
+ const dy = Math.abs(event.clientY - state.mouse_down_pos.y);
79
+
80
+ if (!state.is_dragging && (dx > 3 || dy > 3)) {
81
+ state.is_dragging = true;
82
+ set_is_dragging(true);
83
+ }
84
+
85
+ if (state.is_dragging) {
86
+ update_selection(event);
87
+ }
88
+ },
89
+
90
+ handle_mouse_up: end_drag
91
+ };
92
+ }