@gradio/dataframe 0.17.4 → 0.17.6

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 (38) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/Dataframe.stories.svelte +29 -0
  3. package/dist/shared/CellMenu.svelte.d.ts +1 -1
  4. package/dist/shared/EditableCell.svelte +8 -17
  5. package/dist/shared/EditableCell.svelte.d.ts +5 -3
  6. package/dist/shared/Table.svelte +111 -138
  7. package/dist/shared/TableCell.svelte +4 -6
  8. package/dist/shared/TableCell.svelte.d.ts +5 -1
  9. package/dist/shared/TableHeader.svelte +4 -3
  10. package/dist/shared/TableHeader.svelte.d.ts +1 -2
  11. package/dist/shared/context/dataframe_context.d.ts +147 -0
  12. package/dist/shared/context/dataframe_context.js +335 -0
  13. package/dist/shared/selection_utils.d.ts +1 -2
  14. package/dist/shared/selection_utils.js +0 -13
  15. package/dist/shared/utils/drag_utils.js +1 -0
  16. package/dist/shared/utils/keyboard_utils.d.ts +3 -2
  17. package/dist/shared/utils/keyboard_utils.js +107 -68
  18. package/package.json +6 -6
  19. package/shared/CellMenu.svelte +1 -1
  20. package/shared/EditableCell.svelte +9 -20
  21. package/shared/Table.svelte +147 -165
  22. package/shared/TableCell.svelte +9 -6
  23. package/shared/TableHeader.svelte +5 -8
  24. package/shared/context/dataframe_context.ts +576 -0
  25. package/shared/selection_utils.ts +1 -23
  26. package/shared/utils/drag_utils.ts +1 -0
  27. package/shared/utils/keyboard_utils.ts +142 -80
  28. package/{shared/utils → test}/sort_utils.test.ts +5 -1
  29. package/{shared/utils → test}/table_utils.test.ts +2 -2
  30. package/dist/shared/context/keyboard_context.d.ts +0 -37
  31. package/dist/shared/context/keyboard_context.js +0 -12
  32. package/dist/shared/context/selection_context.d.ts +0 -32
  33. package/dist/shared/context/selection_context.js +0 -107
  34. package/dist/shared/context/table_context.d.ts +0 -141
  35. package/dist/shared/context/table_context.js +0 -375
  36. package/shared/context/keyboard_context.ts +0 -65
  37. package/shared/context/selection_context.ts +0 -168
  38. package/shared/context/table_context.ts +0 -625
@@ -0,0 +1,576 @@
1
+ import { getContext, setContext } from "svelte";
2
+ import { dequal } from "dequal";
3
+ import { writable, get } from "svelte/store";
4
+ import { sort_table_data } from "../utils/table_utils";
5
+ import { tick } from "svelte";
6
+ import {
7
+ handle_selection,
8
+ get_next_cell_coordinates,
9
+ get_range_selection,
10
+ move_cursor
11
+ } from "../selection_utils";
12
+
13
+ export const DATAFRAME_KEY = Symbol("dataframe");
14
+
15
+ export type SortDirection = "asc" | "desc";
16
+ export type CellCoordinate = [number, number];
17
+
18
+ interface DataFrameState {
19
+ config: {
20
+ show_fullscreen_button: boolean;
21
+ show_copy_button: boolean;
22
+ show_search: "none" | "search" | "filter";
23
+ show_row_numbers: boolean;
24
+ editable: boolean;
25
+ pinned_columns: number;
26
+ show_label: boolean;
27
+ line_breaks: boolean;
28
+ wrap: boolean;
29
+ max_height: number;
30
+ column_widths: string[];
31
+ max_chars?: number;
32
+ };
33
+ current_search_query: string | null;
34
+ sort_state: {
35
+ sort_columns: { col: number; direction: SortDirection }[];
36
+ row_order: number[];
37
+ };
38
+ ui_state: {
39
+ active_cell_menu: { row: number; col: number; x: number; y: number } | null;
40
+ active_header_menu: { col: number; x: number; y: number } | null;
41
+ selected_cells: CellCoordinate[];
42
+ selected: CellCoordinate | false;
43
+ editing: CellCoordinate | false;
44
+ header_edit: number | false;
45
+ selected_header: number | false;
46
+ active_button: {
47
+ type: "header" | "cell";
48
+ row?: number;
49
+ col: number;
50
+ } | null;
51
+ copy_flash: boolean;
52
+ };
53
+ }
54
+
55
+ interface DataFrameActions {
56
+ handle_search: (query: string | null) => void;
57
+ handle_sort: (col: number, direction: SortDirection) => void;
58
+ get_sort_status: (name: string, headers: string[]) => "none" | "asc" | "desc";
59
+ sort_data: (
60
+ data: any[][],
61
+ display_value: string[][] | null,
62
+ styling: string[][] | null
63
+ ) => void;
64
+ update_row_order: (data: any[][]) => void;
65
+ filter_data: (data: any[][]) => any[][];
66
+ add_row: (data: any[][], make_id: () => string, index?: number) => any[][];
67
+ add_col: (
68
+ data: any[][],
69
+ headers: string[],
70
+ make_id: () => string,
71
+ index?: number
72
+ ) => { data: any[][]; headers: string[] };
73
+ add_row_at: (
74
+ data: any[][],
75
+ index: number,
76
+ position: "above" | "below",
77
+ make_id: () => string
78
+ ) => any[][];
79
+ add_col_at: (
80
+ data: any[][],
81
+ headers: string[],
82
+ index: number,
83
+ position: "left" | "right",
84
+ make_id: () => string
85
+ ) => { data: any[][]; headers: string[] };
86
+ delete_row: (data: any[][], index: number) => any[][];
87
+ delete_col: (
88
+ data: any[][],
89
+ headers: string[],
90
+ index: number
91
+ ) => { data: any[][]; headers: string[] };
92
+ delete_row_at: (data: any[][], index: number) => any[][];
93
+ delete_col_at: (
94
+ data: any[][],
95
+ headers: string[],
96
+ index: number
97
+ ) => { data: any[][]; headers: string[] };
98
+ trigger_change: (
99
+ data: any[][],
100
+ headers: any[],
101
+ previous_data: string[][],
102
+ previous_headers: string[],
103
+ value_is_output: boolean,
104
+ dispatch: (e: "change" | "input", detail?: any) => void
105
+ ) => Promise<void>;
106
+ reset_sort_state: () => void;
107
+ set_active_cell_menu: (
108
+ menu: { row: number; col: number; x: number; y: number } | null
109
+ ) => void;
110
+ set_active_header_menu: (
111
+ menu: { col: number; x: number; y: number } | null
112
+ ) => void;
113
+ set_selected_cells: (cells: CellCoordinate[]) => void;
114
+ set_selected: (selected: CellCoordinate | false) => void;
115
+ set_editing: (editing: CellCoordinate | false) => void;
116
+ clear_ui_state: () => void;
117
+ set_header_edit: (header_index: number | false) => void;
118
+ set_selected_header: (header_index: number | false) => void;
119
+ handle_header_click: (col: number, editable: boolean) => void;
120
+ end_header_edit: (key: string) => void;
121
+ get_selected_cells: () => CellCoordinate[];
122
+ get_active_cell_menu: () => {
123
+ row: number;
124
+ col: number;
125
+ x: number;
126
+ y: number;
127
+ } | null;
128
+ get_active_button: () => {
129
+ type: "header" | "cell";
130
+ row?: number;
131
+ col: number;
132
+ } | null;
133
+ set_active_button: (
134
+ button: { type: "header" | "cell"; row?: number; col: number } | null
135
+ ) => void;
136
+ set_copy_flash: (value: boolean) => void;
137
+ handle_cell_click: (event: MouseEvent, row: number, col: number) => void;
138
+ toggle_cell_menu: (event: MouseEvent, row: number, col: number) => void;
139
+ toggle_cell_button: (row: number, col: number) => void;
140
+ handle_select_column: (col: number) => void;
141
+ handle_select_row: (row: number) => void;
142
+ get_next_cell_coordinates: typeof get_next_cell_coordinates;
143
+ get_range_selection: typeof get_range_selection;
144
+ move_cursor: typeof move_cursor;
145
+ }
146
+
147
+ export interface DataFrameContext {
148
+ state: ReturnType<typeof writable<DataFrameState>>;
149
+ actions: DataFrameActions;
150
+ data?: any[][];
151
+ headers?: { id: string; value: string }[];
152
+ els?: Record<
153
+ string,
154
+ { cell: HTMLTableCellElement | null; input: HTMLInputElement | null }
155
+ >;
156
+ parent_element?: HTMLElement;
157
+ get_data_at?: (row: number, col: number) => string | number;
158
+ dispatch?: (e: "change" | "select" | "search", detail?: any) => void;
159
+ }
160
+
161
+ function create_actions(
162
+ state: ReturnType<typeof writable<DataFrameState>>,
163
+ context: DataFrameContext
164
+ ): DataFrameActions {
165
+ const update_state = (
166
+ updater: (s: DataFrameState) => Partial<DataFrameState>
167
+ ): void => state.update((s) => ({ ...s, ...updater(s) }));
168
+
169
+ const add_row = (
170
+ data: any[][],
171
+ make_id: () => string,
172
+ index?: number
173
+ ): any[][] => {
174
+ const new_row = data[0]?.length
175
+ ? Array(data[0].length)
176
+ .fill(null)
177
+ .map(() => ({ value: "", id: make_id() }))
178
+ : [{ value: "", id: make_id() }];
179
+ const new_data = [...data];
180
+ index !== undefined
181
+ ? new_data.splice(index, 0, new_row)
182
+ : new_data.push(new_row);
183
+ return new_data;
184
+ };
185
+
186
+ const add_col = (
187
+ data: any[][],
188
+ headers: string[],
189
+ make_id: () => string,
190
+ index?: number
191
+ ): { data: any[][]; headers: string[] } => {
192
+ const new_headers = context.headers
193
+ ? [...headers.map((h) => context.headers![headers.indexOf(h)].value)]
194
+ : [...headers, `Header ${headers.length + 1}`];
195
+ const new_data = data.map((row) => [...row, { value: "", id: make_id() }]);
196
+ if (index !== undefined) {
197
+ new_headers.splice(index, 0, new_headers.pop()!);
198
+ new_data.forEach((row) => row.splice(index, 0, row.pop()!));
199
+ }
200
+ return { data: new_data, headers: new_headers };
201
+ };
202
+
203
+ return {
204
+ handle_search: (query) =>
205
+ update_state((s) => ({ current_search_query: query })),
206
+ handle_sort: (col, direction) =>
207
+ update_state((s) => {
208
+ const sort_cols = s.sort_state.sort_columns.filter(
209
+ (c) => c.col !== col
210
+ );
211
+ if (
212
+ !s.sort_state.sort_columns.some(
213
+ (c) => c.col === col && c.direction === direction
214
+ )
215
+ ) {
216
+ sort_cols.push({ col, direction });
217
+ }
218
+ return {
219
+ sort_state: { ...s.sort_state, sort_columns: sort_cols.slice(-3) }
220
+ };
221
+ }),
222
+ get_sort_status: (name, headers) => {
223
+ const s = get(state);
224
+ const sort_item = s.sort_state.sort_columns.find(
225
+ (item) => headers[item.col] === name
226
+ );
227
+ return sort_item ? sort_item.direction : "none";
228
+ },
229
+ sort_data: (data, display_value, styling) => {
230
+ const {
231
+ sort_state: { sort_columns }
232
+ } = get(state);
233
+ if (sort_columns.length)
234
+ sort_table_data(data, display_value, styling, sort_columns);
235
+ },
236
+ update_row_order: (data) =>
237
+ update_state((s) => ({
238
+ sort_state: {
239
+ ...s.sort_state,
240
+ row_order:
241
+ s.sort_state.sort_columns.length && data[0]
242
+ ? [...Array(data.length)]
243
+ .map((_, i) => i)
244
+ .sort((a, b) => {
245
+ for (const { col, direction } of s.sort_state
246
+ .sort_columns) {
247
+ const comp =
248
+ (data[a]?.[col]?.value ?? "") <
249
+ (data[b]?.[col]?.value ?? "")
250
+ ? -1
251
+ : 1;
252
+ if (comp) return direction === "asc" ? comp : -comp;
253
+ }
254
+ return 0;
255
+ })
256
+ : [...Array(data.length)].map((_, i) => i)
257
+ }
258
+ })),
259
+ filter_data: (data) => {
260
+ const query = get(state).current_search_query?.toLowerCase();
261
+ return query
262
+ ? data.filter((row) =>
263
+ row.some((cell) =>
264
+ String(cell?.value).toLowerCase().includes(query)
265
+ )
266
+ )
267
+ : data;
268
+ },
269
+ add_row,
270
+ add_col,
271
+ add_row_at: (data, index, position, make_id) =>
272
+ add_row(data, make_id, position === "above" ? index : index + 1),
273
+ add_col_at: (data, headers, index, position, make_id) =>
274
+ add_col(data, headers, make_id, position === "left" ? index : index + 1),
275
+ delete_row: (data, index) =>
276
+ data.length > 1 ? data.filter((_, i) => i !== index) : data,
277
+ delete_col: (data, headers, index) =>
278
+ headers.length > 1
279
+ ? {
280
+ data: data.map((row) => row.filter((_, i) => i !== index)),
281
+ headers: headers.filter((_, i) => i !== index)
282
+ }
283
+ : { data, headers },
284
+ delete_row_at: (data, index) =>
285
+ data.length > 1
286
+ ? [...data.slice(0, index), ...data.slice(index + 1)]
287
+ : data,
288
+ delete_col_at: (data, headers, index) =>
289
+ headers.length > 1
290
+ ? {
291
+ data: data.map((row) => [
292
+ ...row.slice(0, index),
293
+ ...row.slice(index + 1)
294
+ ]),
295
+ headers: [...headers.slice(0, index), ...headers.slice(index + 1)]
296
+ }
297
+ : { data, headers },
298
+ trigger_change: async (
299
+ data,
300
+ headers,
301
+ previous_data,
302
+ previous_headers,
303
+ value_is_output,
304
+ dispatch
305
+ ) => {
306
+ const s = get(state);
307
+ if (s.current_search_query) return;
308
+
309
+ const current_headers = headers.map((h) => h.value);
310
+ const current_data = data.map((row) =>
311
+ row.map((cell) => String(cell.value))
312
+ );
313
+
314
+ if (
315
+ !dequal(current_data, previous_data) ||
316
+ !dequal(current_headers, previous_headers)
317
+ ) {
318
+ if (!dequal(current_headers, previous_headers)) {
319
+ update_state((s) => ({
320
+ sort_state: { sort_columns: [], row_order: [] }
321
+ }));
322
+ }
323
+ dispatch("change", {
324
+ data: data.map((row) => row.map((cell) => cell.value)),
325
+ headers: current_headers,
326
+ metadata: null
327
+ });
328
+ if (!value_is_output) dispatch("input");
329
+ }
330
+ },
331
+ reset_sort_state: () =>
332
+ update_state((s) => ({
333
+ sort_state: { sort_columns: [], row_order: [] }
334
+ })),
335
+ set_active_cell_menu: (menu) =>
336
+ update_state((s) => ({
337
+ ui_state: { ...s.ui_state, active_cell_menu: menu }
338
+ })),
339
+ set_active_header_menu: (menu) =>
340
+ update_state((s) => ({
341
+ ui_state: { ...s.ui_state, active_header_menu: menu }
342
+ })),
343
+ set_selected_cells: (cells) =>
344
+ update_state((s) => ({
345
+ ui_state: { ...s.ui_state, selected_cells: cells }
346
+ })),
347
+ set_selected: (selected) =>
348
+ update_state((s) => ({ ui_state: { ...s.ui_state, selected } })),
349
+ set_editing: (editing) =>
350
+ update_state((s) => ({ ui_state: { ...s.ui_state, editing } })),
351
+ clear_ui_state: () =>
352
+ update_state((s) => ({
353
+ ui_state: {
354
+ active_cell_menu: null,
355
+ active_header_menu: null,
356
+ selected_cells: [],
357
+ selected: false,
358
+ editing: false,
359
+ header_edit: false,
360
+ selected_header: false,
361
+ active_button: null,
362
+ copy_flash: false
363
+ }
364
+ })),
365
+ set_header_edit: (header_index) =>
366
+ update_state((s) => ({
367
+ ui_state: {
368
+ ...s.ui_state,
369
+ selected_cells: [],
370
+ selected_header: header_index,
371
+ header_edit: header_index
372
+ }
373
+ })),
374
+ set_selected_header: (header_index) =>
375
+ update_state((s) => ({
376
+ ui_state: {
377
+ ...s.ui_state,
378
+ selected_header: header_index,
379
+ selected: false,
380
+ selected_cells: []
381
+ }
382
+ })),
383
+ handle_header_click: (col, editable) =>
384
+ update_state((s) => ({
385
+ ui_state: {
386
+ ...s.ui_state,
387
+ active_cell_menu: null,
388
+ active_header_menu: null,
389
+ selected: false,
390
+ selected_cells: [],
391
+ selected_header: col,
392
+ header_edit: editable ? col : false
393
+ }
394
+ })),
395
+ end_header_edit: (key) => {
396
+ if (["Escape", "Enter", "Tab"].includes(key)) {
397
+ update_state((s) => ({
398
+ ui_state: { ...s.ui_state, selected: false, header_edit: false }
399
+ }));
400
+ }
401
+ },
402
+ get_selected_cells: () => get(state).ui_state.selected_cells,
403
+ get_active_cell_menu: () => get(state).ui_state.active_cell_menu,
404
+ get_active_button: () => get(state).ui_state.active_button,
405
+ set_active_button: (button) =>
406
+ update_state((s) => ({
407
+ ui_state: { ...s.ui_state, active_button: button }
408
+ })),
409
+ set_copy_flash: (value) =>
410
+ update_state((s) => ({ ui_state: { ...s.ui_state, copy_flash: value } })),
411
+ handle_cell_click: (event, row, col) => {
412
+ event.preventDefault();
413
+ event.stopPropagation();
414
+
415
+ const s = get(state);
416
+ if (s.config.show_row_numbers && col === -1) return;
417
+
418
+ let actual_row = row;
419
+ if (s.current_search_query && context.data) {
420
+ const filtered_indices: number[] = [];
421
+ context.data.forEach((dataRow, idx) => {
422
+ if (
423
+ dataRow.some((cell) =>
424
+ String(cell?.value)
425
+ .toLowerCase()
426
+ .includes(s.current_search_query?.toLowerCase() || "")
427
+ )
428
+ ) {
429
+ filtered_indices.push(idx);
430
+ }
431
+ });
432
+ actual_row = filtered_indices[row] ?? row;
433
+ }
434
+
435
+ const cells = handle_selection(
436
+ [actual_row, col],
437
+ s.ui_state.selected_cells,
438
+ event
439
+ );
440
+ update_state((s) => ({
441
+ ui_state: {
442
+ ...s.ui_state,
443
+ active_cell_menu: null,
444
+ active_header_menu: null,
445
+ selected_header: false,
446
+ header_edit: false,
447
+ selected_cells: cells,
448
+ selected: cells[0]
449
+ }
450
+ }));
451
+
452
+ if (s.config.editable && cells.length === 1) {
453
+ update_state((s) => ({
454
+ ui_state: { ...s.ui_state, editing: [actual_row, col] }
455
+ }));
456
+ tick().then(() =>
457
+ context.els![context.data![actual_row][col].id]?.input?.focus()
458
+ );
459
+ } else {
460
+ // ensure parent has focus for keyboard navigation
461
+ tick().then(() => {
462
+ if (context.parent_element) {
463
+ context.parent_element.focus();
464
+ }
465
+ });
466
+ }
467
+
468
+ context.dispatch?.("select", {
469
+ index: [actual_row, col],
470
+ value: context.get_data_at!(actual_row, col)
471
+ });
472
+ },
473
+ toggle_cell_menu: (event, row, col) => {
474
+ event.stopPropagation();
475
+ const current_menu = get(state).ui_state.active_cell_menu;
476
+ if (current_menu?.row === row && current_menu.col === col) {
477
+ update_state((s) => ({
478
+ ui_state: { ...s.ui_state, active_cell_menu: null }
479
+ }));
480
+ } else {
481
+ const cell = (event.target as HTMLElement).closest("td");
482
+ if (cell) {
483
+ const rect = cell.getBoundingClientRect();
484
+ update_state((s) => ({
485
+ ui_state: {
486
+ ...s.ui_state,
487
+ active_cell_menu: { row, col, x: rect.right, y: rect.bottom }
488
+ }
489
+ }));
490
+ }
491
+ }
492
+ },
493
+ toggle_cell_button: (row, col) => {
494
+ const current_button = get(state).ui_state.active_button;
495
+ const new_button =
496
+ current_button?.type === "cell" &&
497
+ current_button.row === row &&
498
+ current_button.col === col
499
+ ? null
500
+ : { type: "cell" as const, row, col };
501
+ update_state((s) => ({
502
+ ui_state: { ...s.ui_state, active_button: new_button }
503
+ }));
504
+ },
505
+ handle_select_column: (col) => {
506
+ if (!context.data) return;
507
+ const cells = context.data.map((_, row) => [row, col] as CellCoordinate);
508
+ update_state((s) => ({
509
+ ui_state: {
510
+ ...s.ui_state,
511
+ selected_cells: cells,
512
+ selected: cells[0],
513
+ editing: false
514
+ }
515
+ }));
516
+ setTimeout(() => context.parent_element?.focus(), 0);
517
+ },
518
+ handle_select_row: (row) => {
519
+ if (!context.data || !context.data[0]) return;
520
+ const cells = context.data[0].map(
521
+ (_, col) => [row, col] as CellCoordinate
522
+ );
523
+ update_state((s) => ({
524
+ ui_state: {
525
+ ...s.ui_state,
526
+ selected_cells: cells,
527
+ selected: cells[0],
528
+ editing: false
529
+ }
530
+ }));
531
+ setTimeout(() => context.parent_element?.focus(), 0);
532
+ },
533
+ get_next_cell_coordinates,
534
+ get_range_selection,
535
+ move_cursor
536
+ };
537
+ }
538
+
539
+ export function create_dataframe_context(
540
+ config: DataFrameState["config"]
541
+ ): DataFrameContext {
542
+ const state = writable<DataFrameState>({
543
+ config,
544
+ current_search_query: null,
545
+ sort_state: { sort_columns: [], row_order: [] },
546
+ ui_state: {
547
+ active_cell_menu: null,
548
+ active_header_menu: null,
549
+ selected_cells: [],
550
+ selected: false,
551
+ editing: false,
552
+ header_edit: false,
553
+ selected_header: false,
554
+ active_button: null,
555
+ copy_flash: false
556
+ }
557
+ });
558
+
559
+ const context: DataFrameContext = { state, actions: null as any };
560
+ context.actions = create_actions(state, context);
561
+
562
+ const instance_id = Symbol(
563
+ `dataframe_${Math.random().toString(36).substring(2)}`
564
+ );
565
+ setContext(instance_id, context);
566
+ setContext(DATAFRAME_KEY, { instance_id, context });
567
+
568
+ return context;
569
+ }
570
+
571
+ export function get_dataframe_context(): DataFrameContext {
572
+ const ctx = getContext<{ instance_id: symbol; context: DataFrameContext }>(
573
+ DATAFRAME_KEY
574
+ );
575
+ return ctx?.context ?? getContext<DataFrameContext>(DATAFRAME_KEY);
576
+ }
@@ -1,4 +1,4 @@
1
- import type { CellCoordinate, EditingState } from "./types";
1
+ import type { CellCoordinate } from "./types";
2
2
 
3
3
  export type CellData = { id: string; value: string | number };
4
4
 
@@ -88,28 +88,6 @@ export function handle_delete_key(
88
88
  return new_data;
89
89
  }
90
90
 
91
- export function handle_editing_state(
92
- current: CellCoordinate,
93
- editing: EditingState,
94
- selected_cells: CellCoordinate[],
95
- editable: boolean
96
- ): EditingState {
97
- const [row, col] = current;
98
- if (!editable) return false;
99
-
100
- if (editing && editing[0] === row && editing[1] === col) return editing;
101
-
102
- if (
103
- selected_cells.length === 1 &&
104
- selected_cells[0][0] === row &&
105
- selected_cells[0][1] === col
106
- ) {
107
- return [row, col];
108
- }
109
-
110
- return false;
111
- }
112
-
113
91
  export function should_show_cell_menu(
114
92
  cell: CellCoordinate,
115
93
  selected_cells: CellCoordinate[],
@@ -38,6 +38,7 @@ export function create_drag_handlers(
38
38
  if (!event.shiftKey && !event.metaKey && !event.ctrlKey) {
39
39
  set_selected_cells([[row, col]]);
40
40
  set_selected([row, col]);
41
+ handle_cell_click(event, row, col);
41
42
  }
42
43
  };
43
44