@shiguri/solid-grid 0.0.1 → 0.1.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.
package/dist/index.js CHANGED
@@ -1,11 +1,14 @@
1
- import { className, createComponent, delegateEvents, effect, insert, setAttribute, style, template, use } from "solid-js/web";
1
+ import { className, createComponent, delegateEvents, effect, insert, memo, setAttribute, style, template, use } from "solid-js/web";
2
2
  import { For, Index, batch, createEffect, createMemo, createSignal, onCleanup, onMount } from "solid-js";
3
3
 
4
- //#region src/index.tsx
4
+ //#region src/gridsheet.tsx
5
5
  var _tmpl$ = /* @__PURE__ */ template(`<table data-slot=gridsheet tabindex=-1><thead data-slot=gridsheet-header><tr data-slot=gridsheet-row><th data-slot=gridsheet-corner></th></tr></thead><tbody data-slot=gridsheet-body>`), _tmpl$2 = /* @__PURE__ */ template(`<tr data-slot=gridsheet-row>`), _tmpl$3 = /* @__PURE__ */ template(`<th data-slot=gridsheet-rowheader>`), _tmpl$4 = /* @__PURE__ */ template(`<th data-slot=gridsheet-colheader>`), _tmpl$5 = /* @__PURE__ */ template(`<td data-slot=gridsheet-cell tabindex=-1>`);
6
+ /** Zero-based cell position. */
7
+ /** Inclusive range of cells. */
6
8
  function isPositionInRange(pos, range) {
7
9
  return pos.row >= range.min.row && pos.row <= range.max.row && pos.col >= range.min.col && pos.col <= range.max.col;
8
10
  }
11
+ /** Normalize two positions into an inclusive range. */
9
12
  function normalizeRange(pos1, pos2) {
10
13
  return {
11
14
  min: {
@@ -18,296 +21,144 @@ function normalizeRange(pos1, pos2) {
18
21
  }
19
22
  };
20
23
  }
21
- const DEFAULT_SELECTION_MODE = "cell";
22
- function Gridsheet(props) {
23
- const numRows = createMemo(() => props.data.length);
24
- const numCols = createMemo(() => props.data[0] ? props.data[0].length : 0);
25
- const [innerActiveCell, setInnerActiveCell] = createSignal(null);
26
- const activeCell = createMemo(() => props.activeCell === void 0 ? innerActiveCell() : props.activeCell);
27
- const setActiveCell = (pos) => {
28
- setInnerActiveCell(pos);
29
- if (props.onActiveCellChange) props.onActiveCellChange(pos);
30
- };
31
- const isCellActive = (pos) => {
32
- const ac = activeCell();
33
- return ac !== null && ac.row === pos.row && ac.col === pos.col;
24
+ /** Render-time context for a single cell. */
25
+ /** Events emitted from Gridsheet to plugins/handlers. */
26
+ /** Return true to stop further handling. */
27
+ /** Props for the Gridsheet component. */
28
+ function createControllable(propsValue, onChange, initial) {
29
+ const [inner, setInner] = createSignal(initial);
30
+ const value = createMemo(() => propsValue() === void 0 ? inner() : propsValue());
31
+ const set = (v) => {
32
+ setInner(() => v);
33
+ onChange?.(v);
34
34
  };
35
- const [innerSelection, setInnerSelection] = createSignal(null);
36
- const selection = createMemo(() => props.selection === void 0 ? innerSelection() : props.selection);
35
+ return [value, set];
36
+ }
37
+ /** Patch for a single cell update. */
38
+ /** API exposed to plugins and external handlers. */
39
+ function createGridApi(props) {
40
+ const numRows = createMemo(() => props.data.length);
41
+ const numCols = createMemo(() => props.data[0]?.length ?? 0);
42
+ const [activeCell, setActiveCell] = createControllable(() => props.activeCell, props.onActiveCellChange, null);
43
+ const [selection, setSelectionRaw] = createControllable(() => props.selection, props.onSelectionChange, null);
37
44
  const setSelection = (range) => {
38
- const normalizedRange = range ? normalizeRange(range.min, range.max) : null;
39
- setInnerSelection(normalizedRange);
40
- if (props.onSelectionChange) props.onSelectionChange(normalizedRange);
41
- };
42
- const isCellSelected = (pos) => {
43
- const sel = selection();
44
- return sel !== null && isPositionInRange(pos, sel);
45
- };
46
- const isRowHeaderSelected = (rowIndex) => {
47
- const sel = selection();
48
- return sel !== null && rowIndex >= sel.min.row && rowIndex <= sel.max.row;
49
- };
50
- const isColHeaderSelected = (colIndex) => {
51
- const sel = selection();
52
- return sel !== null && colIndex >= sel.min.col && colIndex <= sel.max.col;
53
- };
54
- const [innerIsEditing, setInnerIsEditing] = createSignal(false);
55
- const isEditing = createMemo(() => props.isEditing === void 0 ? innerIsEditing() : props.isEditing);
56
- const setIsEditing = (editing) => {
57
- setInnerIsEditing(editing);
58
- if (props.onIsEditingChange) props.onIsEditingChange(editing);
45
+ setSelectionRaw(range ? normalizeRange(range.min, range.max) : null);
59
46
  };
60
- const isCellEditing = (pos) => {
61
- return isEditing() && isCellActive(pos);
47
+ const [isEditing, setIsEditing] = createControllable(() => props.isEditing, props.onIsEditingChange, false);
48
+ const updateCells = (patches) => {
49
+ props.onCellsChange?.(patches);
62
50
  };
63
- const beginCellEdit = (pos) => {
51
+ const beginEdit = (pos) => {
64
52
  batch(() => {
65
53
  setIsEditing(true);
66
54
  setActiveCell(pos);
67
55
  setSelection(normalizeRange(pos, pos));
68
56
  });
69
57
  };
70
- const commitCellEdit = (pos, value) => {
71
- const nextData = props.data.map((row) => row.slice());
72
- const targetRow = nextData[pos.row];
73
- if (targetRow && pos.col >= 0 && pos.col < targetRow.length) {
74
- targetRow[pos.col] = value;
75
- if (props.onDataChange) props.onDataChange(nextData);
76
- }
77
- setIsEditing(false);
78
- };
79
- const cancelCellEdit = () => {
58
+ const cancelEdit = () => setIsEditing(false);
59
+ const commitEdit = (pos, value) => {
60
+ updateCells([{
61
+ pos,
62
+ value
63
+ }]);
80
64
  setIsEditing(false);
81
65
  };
82
- const [isMouseDown, setIsMouseDown] = createSignal(false);
83
- const [selectionMode, setSelectionMode] = createSignal(DEFAULT_SELECTION_MODE);
84
- const [selectionAnchor, setSelectionAnchor] = createSignal(null);
85
- let previousBodyUserSelect = null;
86
- const disableTextSelectionDuringDrag = () => {
87
- if (typeof document === "undefined" || previousBodyUserSelect !== null) return;
88
- const body = document.body;
89
- if (!body) return;
90
- previousBodyUserSelect = body.style.userSelect;
91
- body.style.userSelect = "none";
92
- };
93
- const restoreTextSelectionAfterDrag = () => {
94
- if (typeof document === "undefined" || previousBodyUserSelect === null) return;
95
- const body = document.body;
96
- if (body) body.style.userSelect = previousBodyUserSelect;
97
- previousBodyUserSelect = null;
98
- };
99
- const handleMouseUp = () => {
100
- setIsMouseDown(false);
101
- setSelectionMode(DEFAULT_SELECTION_MODE);
102
- setSelectionAnchor(null);
103
- restoreTextSelectionAfterDrag();
104
- };
105
- onMount(() => {
106
- window.addEventListener("mouseup", handleMouseUp);
107
- });
108
- onCleanup(() => {
109
- window.removeEventListener("mouseup", handleMouseUp);
110
- restoreTextSelectionAfterDrag();
111
- });
112
- const handleMouseDownOnCell = (pos, e) => {
113
- if (e.button !== 0) return;
114
- if (isCellEditing(pos)) return;
115
- if (e.detail === 2) {
116
- e.preventDefault();
117
- beginCellEdit(pos);
118
- return;
119
- }
120
- e.preventDefault();
121
- batch(() => {
122
- disableTextSelectionDuringDrag();
123
- setIsMouseDown(true);
124
- setSelectionMode("cell");
125
- setSelectionAnchor(pos);
126
- setIsEditing(false);
127
- setSelection(normalizeRange(pos, pos));
128
- setActiveCell(pos);
129
- });
130
- };
131
- const handleMouseOverOnCell = (pos, e) => {
132
- if ((e.buttons & 1) === 0) return;
133
- batch(() => {
134
- if (!isMouseDown() || selectionMode() !== "cell") return;
135
- const start = selectionAnchor();
136
- if (start) setSelection(normalizeRange(start, pos));
137
- else console.warn("selectionAnchor is null during mouse drag selection");
138
- });
66
+ return {
67
+ numRows,
68
+ numCols,
69
+ activeCell,
70
+ selection,
71
+ isEditing,
72
+ setActiveCell,
73
+ setSelection,
74
+ beginEdit,
75
+ cancelEdit,
76
+ commitEdit,
77
+ updateCells
139
78
  };
140
- const handleMouseDownOnRowHeader = (rowIndex) => {
141
- if (numCols() === 0) return;
142
- batch(() => {
143
- disableTextSelectionDuringDrag();
144
- setIsMouseDown(true);
145
- setSelectionMode("row");
146
- const start = {
147
- row: rowIndex,
148
- col: 0
149
- };
150
- const end = {
151
- row: rowIndex,
152
- col: numCols() - 1
153
- };
154
- setSelectionAnchor(start);
155
- setIsEditing(false);
156
- setSelection(normalizeRange(start, end));
157
- setActiveCell(start);
158
- });
79
+ }
80
+ function Gridsheet(props) {
81
+ const api = createGridApi(props);
82
+ const emit = (ev) => {
83
+ props.onEvent?.(ev, api);
159
84
  };
160
- const handleMouseOverOnRowHeader = (rowIndex) => {
161
- batch(() => {
162
- if (!isMouseDown() || selectionMode() !== "row") return;
163
- const start = selectionAnchor();
164
- if (start) setSelection(normalizeRange(start, {
165
- row: rowIndex,
166
- col: numCols() - 1
167
- }));
168
- else console.warn("selectionAnchor is null during mouse drag selection");
169
- });
85
+ const isCellActive = (pos) => {
86
+ const ac = api.activeCell();
87
+ return ac !== null && ac.row === pos.row && ac.col === pos.col;
170
88
  };
171
- const handleMouseDownOnColHeader = (colIndex) => {
172
- if (numRows() === 0) return;
173
- batch(() => {
174
- disableTextSelectionDuringDrag();
175
- setIsMouseDown(true);
176
- setSelectionMode("col");
177
- const start = {
178
- row: 0,
179
- col: colIndex
180
- };
181
- const end = {
182
- row: numRows() - 1,
183
- col: colIndex
184
- };
185
- setSelectionAnchor(start);
186
- setIsEditing(false);
187
- setSelection(normalizeRange(start, end));
188
- setActiveCell(start);
189
- });
89
+ const isCellSelected = (pos) => {
90
+ const sel = api.selection();
91
+ return sel !== null && isPositionInRange(pos, sel);
190
92
  };
191
- const handleMouseOverOnColHeader = (colIndex) => {
192
- batch(() => {
193
- if (!isMouseDown() || selectionMode() !== "col") return;
194
- const start = selectionAnchor();
195
- if (start) setSelection(normalizeRange(start, {
196
- row: numRows() - 1,
197
- col: colIndex
198
- }));
199
- else console.warn("selectionAnchor is null during mouse drag selection");
200
- });
93
+ const isRowHeaderSelected = (rowIndex) => {
94
+ const sel = api.selection();
95
+ return sel !== null && rowIndex >= sel.min.row && rowIndex <= sel.max.row;
201
96
  };
202
- const handleClickOnCorner = () => {
203
- if (numCols() === 0 || numRows() === 0) return;
204
- batch(() => {
205
- setIsEditing(false);
206
- setSelection(normalizeRange({
207
- row: 0,
208
- col: 0
209
- }, {
210
- row: numRows() - 1,
211
- col: numCols() - 1
212
- }));
213
- setActiveCell({
214
- row: 0,
215
- col: 0
216
- });
217
- });
97
+ const isColHeaderSelected = (colIndex) => {
98
+ const sel = api.selection();
99
+ return sel !== null && colIndex >= sel.min.col && colIndex <= sel.max.col;
218
100
  };
219
- const navigate = (deltaRow, deltaCol, isSelection) => {
220
- const ac = activeCell();
101
+ const renderRowHeader = props.renderRowHeader ?? defaultRenderRowHeader;
102
+ const renderColHeader = props.renderColHeader ?? defaultRenderColHeader;
103
+ const isCellEditing = (pos) => api.isEditing() && isCellActive(pos);
104
+ const cellRefs = /* @__PURE__ */ new Map();
105
+ const getCellKey = (pos) => `${pos.row}:${pos.col}`;
106
+ createEffect(() => {
107
+ if (api.isEditing()) return;
108
+ const ac = api.activeCell();
221
109
  if (!ac) return;
222
- if (isSelection) {
223
- let anchor = selectionAnchor();
224
- if (!anchor) {
225
- anchor = ac;
226
- setSelectionAnchor(anchor);
227
- }
228
- const sel = selection();
229
- let headRow = anchor.row;
230
- let headCol = anchor.col;
231
- if (sel) {
232
- headRow = sel.min.row === anchor.row ? sel.max.row : sel.min.row;
233
- headCol = sel.min.col === anchor.col ? sel.max.col : sel.min.col;
234
- }
235
- const nextHeadRow = Math.max(0, Math.min(numRows() - 1, headRow + deltaRow));
236
- const nextHeadCol = Math.max(0, Math.min(numCols() - 1, headCol + deltaCol));
237
- setSelection(normalizeRange(anchor, {
238
- row: nextHeadRow,
239
- col: nextHeadCol
240
- }));
241
- } else {
242
- const nextPos = {
243
- row: Math.max(0, Math.min(numRows() - 1, ac.row + deltaRow)),
244
- col: Math.max(0, Math.min(numCols() - 1, ac.col + deltaCol))
245
- };
246
- setActiveCell(nextPos);
247
- setSelection(normalizeRange(nextPos, nextPos));
248
- setSelectionAnchor(null);
249
- }
250
- };
110
+ cellRefs.get(getCellKey(ac))?.focus();
111
+ });
251
112
  const handleKeyDown = (e) => {
252
113
  if (e.isComposing) return;
253
- if (isEditing()) return;
254
- const ac = activeCell();
255
- if (!ac) return;
256
- switch (e.key) {
257
- case "ArrowUp":
258
- e.preventDefault();
259
- navigate(-1, 0, e.shiftKey);
260
- break;
261
- case "ArrowDown":
262
- e.preventDefault();
263
- navigate(1, 0, e.shiftKey);
264
- break;
265
- case "ArrowLeft":
266
- e.preventDefault();
267
- navigate(0, -1, e.shiftKey);
268
- break;
269
- case "ArrowRight":
270
- e.preventDefault();
271
- navigate(0, 1, e.shiftKey);
272
- break;
273
- case "Tab":
274
- e.preventDefault();
275
- navigate(0, e.shiftKey ? -1 : 1, false);
276
- break;
277
- case "Enter":
278
- e.preventDefault();
279
- beginCellEdit(ac);
280
- break;
281
- default: break;
282
- }
114
+ if (api.isEditing()) return;
115
+ emit({
116
+ type: "key:down",
117
+ e
118
+ });
283
119
  };
284
- const cellRefs = /* @__PURE__ */ new Map();
285
- const getCellKey = (pos) => `${pos.row}:${pos.col}`;
286
- createEffect(() => {
287
- if (isEditing()) return;
288
- const ac = activeCell();
289
- if (ac) cellRefs.get(getCellKey(ac))?.focus();
120
+ const onUp = (e) => emit({
121
+ type: "pointer:up",
122
+ e
123
+ });
124
+ onMount(() => {
125
+ window.addEventListener("pointerup", onUp);
126
+ });
127
+ onCleanup(() => {
128
+ window.removeEventListener("pointerup", onUp);
290
129
  });
291
130
  return (() => {
292
131
  var _el$ = _tmpl$(), _el$2 = _el$.firstChild, _el$3 = _el$2.firstChild, _el$4 = _el$3.firstChild, _el$5 = _el$2.nextSibling;
293
132
  var _ref$ = props.ref;
294
133
  typeof _ref$ === "function" ? use(_ref$, _el$) : props.ref = _el$;
295
134
  _el$.$$keydown = handleKeyDown;
296
- _el$4.$$click = handleClickOnCorner;
135
+ _el$4.$$click = (e) => emit({
136
+ type: "corner:click",
137
+ e
138
+ });
297
139
  insert(_el$3, createComponent(Index, {
298
140
  get each() {
299
- return Array.from({ length: numCols() });
141
+ return Array.from({ length: api.numCols() });
300
142
  },
301
143
  children: (_, colIndex) => createComponent(ColHeader, {
302
144
  index: colIndex,
303
145
  get isSelected() {
304
146
  return isColHeaderSelected(colIndex);
305
147
  },
306
- onMouseDown: handleMouseDownOnColHeader,
307
- onMouseOver: handleMouseOverOnColHeader,
148
+ onMouseDown: (col, e) => emit({
149
+ type: "colheader:pointerdown",
150
+ col,
151
+ e
152
+ }),
153
+ onMouseOver: (col, e) => emit({
154
+ type: "colheader:pointerover",
155
+ col,
156
+ e
157
+ }),
308
158
  get ["class"]() {
309
159
  return props.classes?.colHeader;
310
- }
160
+ },
161
+ renderHeader: renderColHeader
311
162
  })
312
163
  }), null);
313
164
  insert(_el$5, createComponent(For, {
@@ -323,11 +174,20 @@ function Gridsheet(props) {
323
174
  get isSelected() {
324
175
  return isRowHeaderSelected(rowIndex());
325
176
  },
326
- onMouseDown: handleMouseDownOnRowHeader,
327
- onMouseOver: handleMouseOverOnRowHeader,
177
+ onMouseDown: (r, e) => emit({
178
+ type: "rowheader:pointerdown",
179
+ row: r,
180
+ e
181
+ }),
182
+ onMouseOver: (r, e) => emit({
183
+ type: "rowheader:pointerover",
184
+ row: r,
185
+ e
186
+ }),
328
187
  get ["class"]() {
329
188
  return props.classes?.rowHeader;
330
- }
189
+ },
190
+ renderHeader: renderRowHeader
331
191
  }), null);
332
192
  insert(_el$6, createComponent(For, {
333
193
  each: row,
@@ -357,20 +217,31 @@ function Gridsheet(props) {
357
217
  col: colIndex()
358
218
  });
359
219
  },
360
- beginEdit: beginCellEdit,
361
- commitEdit: commitCellEdit,
362
- cancelEditing: cancelCellEdit,
363
- setActiveCell,
364
- setSelection,
220
+ beginEdit: (pos) => api.beginEdit(pos),
221
+ commitEdit: (pos, value) => api.commitEdit(pos, value),
222
+ cancelEditing: () => api.cancelEdit(),
365
223
  get renderCell() {
366
224
  return props.renderCell;
367
225
  },
368
- onMouseDown: handleMouseDownOnCell,
369
- onMouseOver: handleMouseOverOnCell,
370
- registerCellRef: (rowPos, colPos, el) => {
226
+ onMouseDown: (pos, e) => emit({
227
+ type: "cell:pointerdown",
228
+ pos,
229
+ e
230
+ }),
231
+ onMouseOver: (pos, e) => emit({
232
+ type: "cell:pointerover",
233
+ pos,
234
+ e
235
+ }),
236
+ onDoubleClick: (pos, e) => emit({
237
+ type: "cell:dblclick",
238
+ pos,
239
+ e
240
+ }),
241
+ registerCellRef: (r, c, el) => {
371
242
  cellRefs.set(getCellKey({
372
- row: rowPos,
373
- col: colPos
243
+ row: r,
244
+ col: c
374
245
  }), el);
375
246
  },
376
247
  get ["class"]() {
@@ -403,12 +274,18 @@ function Gridsheet(props) {
403
274
  function getRowLabel(rowIndex) {
404
275
  return `${rowIndex + 1}`;
405
276
  }
277
+ function defaultRenderRowHeader(ctx) {
278
+ return memo(() => getRowLabel(ctx.index));
279
+ }
406
280
  function RowHeader(props) {
407
281
  return (() => {
408
282
  var _el$7 = _tmpl$3();
409
- _el$7.$$mouseover = () => props.onMouseOver(props.index);
410
- _el$7.$$mousedown = () => props.onMouseDown(props.index);
411
- insert(_el$7, () => getRowLabel(props.index));
283
+ _el$7.$$mouseover = (e) => props.onMouseOver(props.index, e);
284
+ _el$7.$$mousedown = (e) => props.onMouseDown(props.index, e);
285
+ insert(_el$7, () => props.renderHeader({
286
+ index: props.index,
287
+ isSelected: props.isSelected
288
+ }));
412
289
  effect((_p$) => {
413
290
  var _v$6 = typeof props.class === "function" ? props.class({
414
291
  rowIndex: props.index,
@@ -434,12 +311,18 @@ function getColLabel(colIndex) {
434
311
  }
435
312
  return label;
436
313
  }
314
+ function defaultRenderColHeader(ctx) {
315
+ return memo(() => getColLabel(ctx.index));
316
+ }
437
317
  function ColHeader(props) {
438
318
  return (() => {
439
319
  var _el$8 = _tmpl$4();
440
- _el$8.$$mouseover = () => props.onMouseOver(props.index);
441
- _el$8.$$mousedown = () => props.onMouseDown(props.index);
442
- insert(_el$8, () => getColLabel(props.index));
320
+ _el$8.$$mouseover = (e) => props.onMouseOver(props.index, e);
321
+ _el$8.$$mousedown = (e) => props.onMouseDown(props.index, e);
322
+ insert(_el$8, () => props.renderHeader({
323
+ index: props.index,
324
+ isSelected: props.isSelected
325
+ }));
443
326
  effect((_p$) => {
444
327
  var _v$8 = typeof props.class === "function" ? props.class({
445
328
  colIndex: props.index,
@@ -461,30 +344,11 @@ function Cell(props) {
461
344
  row: props.row,
462
345
  col: props.col
463
346
  });
464
- const commitEdit = (value) => {
465
- props.commitEdit({
466
- row: props.row,
467
- col: props.col
468
- }, value);
469
- };
347
+ const commitEdit = (value) => props.commitEdit({
348
+ row: props.row,
349
+ col: props.col
350
+ }, value);
470
351
  const cancelEditing = () => props.cancelEditing();
471
- const handleMouseDown = (e) => {
472
- props.onMouseDown({
473
- row: props.row,
474
- col: props.col
475
- }, e);
476
- };
477
- const handleMouseOver = (e) => {
478
- props.onMouseOver({
479
- row: props.row,
480
- col: props.col
481
- }, e);
482
- };
483
- const handleDoubleClick = (e) => {
484
- if (e.button !== 0) return;
485
- e.preventDefault();
486
- if (!props.isEditing) beginEdit();
487
- };
488
352
  const className$1 = createMemo(() => {
489
353
  if (typeof props.class === "function") return props.class({
490
354
  row: props.row,
@@ -498,13 +362,22 @@ function Cell(props) {
498
362
  commitEdit,
499
363
  cancelEditing
500
364
  });
501
- else return props.class;
365
+ return props.class;
502
366
  });
503
367
  return (() => {
504
368
  var _el$9 = _tmpl$5();
505
- _el$9.$$dblclick = handleDoubleClick;
506
- _el$9.$$mouseover = handleMouseOver;
507
- _el$9.$$mousedown = handleMouseDown;
369
+ _el$9.$$dblclick = (e) => props.onDoubleClick({
370
+ row: props.row,
371
+ col: props.col
372
+ }, e);
373
+ _el$9.$$mouseover = (e) => props.onMouseOver({
374
+ row: props.row,
375
+ col: props.col
376
+ }, e);
377
+ _el$9.$$mousedown = (e) => props.onMouseDown({
378
+ row: props.row,
379
+ col: props.col
380
+ }, e);
508
381
  use((el) => {
509
382
  cellRef = el;
510
383
  props.registerCellRef(props.row, props.col, el);
@@ -550,5 +423,504 @@ delegateEvents([
550
423
  ]);
551
424
 
552
425
  //#endregion
553
- export { Gridsheet };
426
+ //#region src/plugin.ts
427
+ /** Compose multiple plugins into a single event handler. */
428
+ function createPluginHost(plugins) {
429
+ const onEvent = (ev, api) => {
430
+ for (const p of plugins) if (p.onEvent?.(ev, api) === true) return true;
431
+ return false;
432
+ };
433
+ return { onEvent };
434
+ }
435
+
436
+ //#endregion
437
+ //#region src/plugins/utils.ts
438
+ function buildClearPatches(range, getEmptyValue) {
439
+ const patches = [];
440
+ for (let r = range.min.row; r <= range.max.row; r++) for (let c = range.min.col; c <= range.max.col; c++) {
441
+ const pos = {
442
+ row: r,
443
+ col: c
444
+ };
445
+ patches.push({
446
+ pos,
447
+ value: getEmptyValue(pos)
448
+ });
449
+ }
450
+ return patches;
451
+ }
452
+
453
+ //#endregion
454
+ //#region src/plugins/clipboard-memory.ts
455
+ function extractSelection(data, range) {
456
+ const result = [];
457
+ for (let r = range.min.row; r <= range.max.row; r++) {
458
+ const row = [];
459
+ for (let c = range.min.col; c <= range.max.col; c++) {
460
+ const rowData = data[r];
461
+ if (rowData && c < rowData.length) {
462
+ const value = rowData[c];
463
+ if (value !== void 0) row.push(value);
464
+ }
465
+ }
466
+ result.push(row);
467
+ }
468
+ return result;
469
+ }
470
+ function buildPastePatches(data, start, rows, cols) {
471
+ const patches = [];
472
+ for (let r = 0; r < data.length; r++) {
473
+ const row = data[r];
474
+ if (!row) continue;
475
+ for (let c = 0; c < row.length; c++) {
476
+ const targetRow = start.row + r;
477
+ const targetCol = start.col + c;
478
+ if (targetRow < 0 || targetCol < 0) continue;
479
+ if (targetRow >= rows || targetCol >= cols) continue;
480
+ const value = row[c];
481
+ if (value === void 0) continue;
482
+ patches.push({
483
+ pos: {
484
+ row: targetRow,
485
+ col: targetCol
486
+ },
487
+ value
488
+ });
489
+ }
490
+ }
491
+ return patches;
492
+ }
493
+ /** In-memory clipboard with optional cut clearing. */
494
+ function clipboardMemoryPlugin(options) {
495
+ let clipboard = null;
496
+ const copyKeys = options.copyKeys ?? ["c"];
497
+ const cutKeys = options.cutKeys ?? ["x"];
498
+ const pasteKeys = options.pasteKeys ?? ["v"];
499
+ const getEmptyValue = options.getEmptyValue ?? (options.emptyValue !== void 0 ? () => options.emptyValue : null);
500
+ const setClipboard = (next) => {
501
+ clipboard = next;
502
+ options.onClipboardChange?.(next);
503
+ };
504
+ return {
505
+ name: "clipboard-memory",
506
+ onEvent(ev, api) {
507
+ if (ev.type !== "key:down") return;
508
+ const e = ev.e;
509
+ if (e.isComposing || api.isEditing()) return;
510
+ const key = e.key.toLowerCase();
511
+ const isCopy = (e.ctrlKey || e.metaKey) && copyKeys.includes(key);
512
+ const isCut = (e.ctrlKey || e.metaKey) && cutKeys.includes(key);
513
+ const isPaste = (e.ctrlKey || e.metaKey) && pasteKeys.includes(key);
514
+ if (!isCopy && !isCut && !isPaste) return;
515
+ const sel = api.selection();
516
+ const ac = api.activeCell();
517
+ if (isCopy || isCut) {
518
+ if (!sel) return true;
519
+ e.preventDefault();
520
+ (async () => {
521
+ const copiedData = extractSelection(options.getData(), sel);
522
+ if ((isCopy ? await options.onCopy?.(copiedData, sel) : await options.onCut?.(copiedData, sel)) === false) return;
523
+ setClipboard({
524
+ data: copiedData,
525
+ range: sel
526
+ });
527
+ if (isCut && getEmptyValue) {
528
+ const patches = buildClearPatches(sel, getEmptyValue);
529
+ if (patches.length > 0) api.updateCells(patches);
530
+ }
531
+ })();
532
+ return true;
533
+ }
534
+ if (isPaste) {
535
+ if (!ac || !clipboard) return true;
536
+ e.preventDefault();
537
+ (async () => {
538
+ const result = await options.onPaste?.(clipboard.data, ac);
539
+ if (result === false) return;
540
+ const patches = result ?? buildPastePatches(clipboard.data, ac, api.numRows(), api.numCols());
541
+ if (patches.length > 0) api.updateCells(patches);
542
+ })();
543
+ return true;
544
+ }
545
+ }
546
+ };
547
+ }
548
+
549
+ //#endregion
550
+ //#region src/plugins/clipboard-text.ts
551
+ const DEFAULT_COPY_KEYS = ["c"];
552
+ const DEFAULT_CUT_KEYS = ["x"];
553
+ const DEFAULT_PASTE_KEYS = ["v"];
554
+ function defaultReadText() {
555
+ if (typeof navigator === "undefined" || !navigator.clipboard) return Promise.resolve("");
556
+ return navigator.clipboard.readText();
557
+ }
558
+ function defaultWriteText(text) {
559
+ if (typeof navigator === "undefined" || !navigator.clipboard) return Promise.resolve();
560
+ return navigator.clipboard.writeText(text);
561
+ }
562
+ function serializeTsv(data, formatCell) {
563
+ return data.map((row) => row.map(formatCell).join(" ")).join("\n");
564
+ }
565
+ function parseTsv(text, target, parseCell) {
566
+ const rows = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
567
+ const patches = [];
568
+ for (let r = 0; r < rows.length; r++) {
569
+ const cols = rows[r]?.split(" ") ?? [];
570
+ for (let c = 0; c < cols.length; c++) patches.push({
571
+ pos: {
572
+ row: target.row + r,
573
+ col: target.col + c
574
+ },
575
+ value: parseCell(cols[c] ?? "")
576
+ });
577
+ }
578
+ return patches;
579
+ }
580
+ /** System clipboard integration with TSV defaults. */
581
+ function clipboardTextPlugin(options) {
582
+ const copyKeys = options.copyKeys ?? DEFAULT_COPY_KEYS;
583
+ const cutKeys = options.cutKeys ?? DEFAULT_CUT_KEYS;
584
+ const pasteKeys = options.pasteKeys ?? DEFAULT_PASTE_KEYS;
585
+ const parseCell = options.parseCell ?? ((raw) => raw);
586
+ const formatCell = options.formatCell ?? ((value) => String(value));
587
+ const toText = options.toText ?? ((data) => serializeTsv(data, formatCell));
588
+ const fromText = options.fromText ?? ((text, target) => parseTsv(text, target, parseCell));
589
+ const readText = options.readText ?? defaultReadText;
590
+ const writeText = options.writeText ?? defaultWriteText;
591
+ const getEmptyValue = options.getEmptyValue ?? (options.emptyValue !== void 0 ? () => options.emptyValue : null);
592
+ return {
593
+ name: "clipboard-text",
594
+ onEvent(ev, api) {
595
+ if (ev.type !== "key:down") return;
596
+ const e = ev.e;
597
+ if (e.isComposing || api.isEditing()) return;
598
+ const key = e.key.toLowerCase();
599
+ const isCopy = (e.ctrlKey || e.metaKey) && copyKeys.includes(key);
600
+ const isCut = (e.ctrlKey || e.metaKey) && cutKeys.includes(key);
601
+ const isPaste = (e.ctrlKey || e.metaKey) && pasteKeys.includes(key);
602
+ if (!isCopy && !isCut && !isPaste) return;
603
+ if (isCopy || isCut) {
604
+ const sel = api.selection();
605
+ if (!sel) return true;
606
+ e.preventDefault();
607
+ (async () => {
608
+ await writeText(toText(options.getData().slice(sel.min.row, sel.max.row + 1).map((row) => row.slice(sel.min.col, sel.max.col + 1))));
609
+ if (isCut && getEmptyValue) {
610
+ const patches = buildClearPatches(sel, getEmptyValue);
611
+ if (patches.length > 0) api.updateCells(patches);
612
+ }
613
+ })();
614
+ return true;
615
+ }
616
+ if (isPaste) {
617
+ const ac = api.activeCell();
618
+ if (!ac) return true;
619
+ e.preventDefault();
620
+ (async () => {
621
+ const text = await readText();
622
+ if (!text) return;
623
+ const patches = fromText(text, ac);
624
+ if (patches === false) return;
625
+ if (patches.length > 0) api.updateCells(patches);
626
+ })();
627
+ return true;
628
+ }
629
+ }
630
+ };
631
+ }
632
+
633
+ //#endregion
634
+ //#region src/plugins/delete.ts
635
+ /** Clears selected range with provided empty values. */
636
+ function deletePlugin(options) {
637
+ const keys = options.keys ?? ["Delete", "Backspace"];
638
+ const getEmptyValue = options.getEmptyValue ?? (options.emptyValue !== void 0 ? () => options.emptyValue : null);
639
+ return {
640
+ name: "delete",
641
+ onEvent(ev, api) {
642
+ if (ev.type !== "key:down") return;
643
+ const e = ev.e;
644
+ if (e.isComposing || api.isEditing()) return;
645
+ if (!keys.includes(e.key)) return;
646
+ const range = api.selection();
647
+ if (!range) {
648
+ e.preventDefault();
649
+ return true;
650
+ }
651
+ const result = options.onDelete?.(range);
652
+ if (result === false) {
653
+ e.preventDefault();
654
+ return true;
655
+ }
656
+ if (Array.isArray(result)) {
657
+ api.updateCells(result);
658
+ e.preventDefault();
659
+ return true;
660
+ }
661
+ if (getEmptyValue) {
662
+ const patches = buildClearPatches(range, getEmptyValue);
663
+ if (patches.length > 0) api.updateCells(patches);
664
+ e.preventDefault();
665
+ return true;
666
+ }
667
+ return true;
668
+ }
669
+ };
670
+ }
671
+
672
+ //#endregion
673
+ //#region src/plugins/editing.ts
674
+ /** Starts editing via double click or configured keys. */
675
+ function editingPlugin(options = {}) {
676
+ const triggerKeys = options.triggerKeys ?? ["Enter"];
677
+ return {
678
+ name: "editing",
679
+ onEvent(ev, api) {
680
+ if (ev.type === "cell:dblclick") {
681
+ if (api.isEditing()) return true;
682
+ api.beginEdit(ev.pos);
683
+ return true;
684
+ }
685
+ if (ev.type === "key:down") {
686
+ const e = ev.e;
687
+ if (e.isComposing || api.isEditing()) return;
688
+ if (!triggerKeys.includes(e.key)) return;
689
+ const ac = api.activeCell();
690
+ if (!ac) return true;
691
+ e.preventDefault();
692
+ api.beginEdit(ac);
693
+ return true;
694
+ }
695
+ }
696
+ };
697
+ }
698
+
699
+ //#endregion
700
+ //#region src/utils.ts
701
+ function clamp(n, min, max) {
702
+ return Math.max(min, Math.min(max, n));
703
+ }
704
+
705
+ //#endregion
706
+ //#region src/plugins/selection.ts
707
+ function moveBy(pos, dr, dc, rows, cols) {
708
+ return {
709
+ row: clamp(pos.row + dr, 0, rows - 1),
710
+ col: clamp(pos.col + dc, 0, cols - 1)
711
+ };
712
+ }
713
+ /** Selection, drag, and arrow-key navigation. */
714
+ function selectionPlugin() {
715
+ let anchor = null;
716
+ let head = null;
717
+ let dragging = false;
718
+ let headerMode = null;
719
+ let previousBodyUserSelect = null;
720
+ const disableTextSelectionDuringDrag = () => {
721
+ if (typeof document === "undefined" || previousBodyUserSelect !== null) return;
722
+ const body = document.body;
723
+ if (!body) return;
724
+ previousBodyUserSelect = body.style.userSelect;
725
+ body.style.userSelect = "none";
726
+ };
727
+ const restoreTextSelectionAfterDrag = () => {
728
+ if (typeof document === "undefined" || previousBodyUserSelect === null) return;
729
+ const body = document.body;
730
+ if (body) body.style.userSelect = previousBodyUserSelect;
731
+ previousBodyUserSelect = null;
732
+ };
733
+ const setSingle = (pos, api) => {
734
+ anchor = pos;
735
+ head = pos;
736
+ batch(() => {
737
+ api.setActiveCell(pos);
738
+ api.setSelection(normalizeRange(pos, pos));
739
+ });
740
+ };
741
+ const setRange = (rangeAnchor, rangeHead, api) => {
742
+ anchor = rangeAnchor;
743
+ head = rangeHead;
744
+ api.setSelection(normalizeRange(rangeAnchor, rangeHead));
745
+ };
746
+ return {
747
+ name: "selection",
748
+ onEvent(ev, api) {
749
+ switch (ev.type) {
750
+ case "cell:pointerdown":
751
+ if (api.isEditing()) return true;
752
+ if (ev.e.button !== 0) return;
753
+ ev.e.preventDefault();
754
+ dragging = true;
755
+ headerMode = null;
756
+ disableTextSelectionDuringDrag();
757
+ setSingle(ev.pos, api);
758
+ return true;
759
+ case "cell:pointerover":
760
+ if (!dragging) return;
761
+ if ((ev.e.buttons & 1) === 0) return;
762
+ if (!anchor) anchor = ev.pos;
763
+ setRange(anchor, ev.pos, api);
764
+ return true;
765
+ case "pointer:up":
766
+ if (dragging) dragging = false;
767
+ headerMode = null;
768
+ restoreTextSelectionAfterDrag();
769
+ return;
770
+ case "corner:click": {
771
+ const rows = api.numRows();
772
+ const cols = api.numCols();
773
+ if (rows <= 0 || cols <= 0) return true;
774
+ const min = {
775
+ row: 0,
776
+ col: 0
777
+ };
778
+ const max = {
779
+ row: rows - 1,
780
+ col: cols - 1
781
+ };
782
+ dragging = false;
783
+ anchor = min;
784
+ head = max;
785
+ batch(() => {
786
+ api.setActiveCell(min);
787
+ api.setSelection(normalizeRange(min, max));
788
+ });
789
+ return true;
790
+ }
791
+ case "rowheader:pointerdown": {
792
+ const cols = api.numCols();
793
+ if (cols <= 0) return true;
794
+ const min = {
795
+ row: ev.row,
796
+ col: 0
797
+ };
798
+ const max = {
799
+ row: ev.row,
800
+ col: cols - 1
801
+ };
802
+ dragging = false;
803
+ headerMode = "row";
804
+ disableTextSelectionDuringDrag();
805
+ anchor = min;
806
+ head = max;
807
+ batch(() => {
808
+ api.setActiveCell(min);
809
+ api.setSelection(normalizeRange(min, max));
810
+ });
811
+ return true;
812
+ }
813
+ case "rowheader:pointerover": {
814
+ if (headerMode !== "row") return;
815
+ if ((ev.e.buttons & 1) === 0) return;
816
+ const cols = api.numCols();
817
+ if (cols <= 0) return true;
818
+ const startRow = anchor?.row ?? ev.row;
819
+ const min = {
820
+ row: Math.min(startRow, ev.row),
821
+ col: 0
822
+ };
823
+ const max = {
824
+ row: Math.max(startRow, ev.row),
825
+ col: cols - 1
826
+ };
827
+ head = max;
828
+ api.setSelection(normalizeRange(min, max));
829
+ return true;
830
+ }
831
+ case "colheader:pointerdown": {
832
+ const rows = api.numRows();
833
+ if (rows <= 0) return true;
834
+ const min = {
835
+ row: 0,
836
+ col: ev.col
837
+ };
838
+ const max = {
839
+ row: rows - 1,
840
+ col: ev.col
841
+ };
842
+ dragging = false;
843
+ headerMode = "col";
844
+ disableTextSelectionDuringDrag();
845
+ anchor = min;
846
+ head = max;
847
+ batch(() => {
848
+ api.setActiveCell(min);
849
+ api.setSelection(normalizeRange(min, max));
850
+ });
851
+ return true;
852
+ }
853
+ case "colheader:pointerover": {
854
+ if (headerMode !== "col") return;
855
+ if ((ev.e.buttons & 1) === 0) return;
856
+ const rows = api.numRows();
857
+ if (rows <= 0) return true;
858
+ const startCol = anchor?.col ?? ev.col;
859
+ const min = {
860
+ row: 0,
861
+ col: Math.min(startCol, ev.col)
862
+ };
863
+ const max = {
864
+ row: rows - 1,
865
+ col: Math.max(startCol, ev.col)
866
+ };
867
+ head = max;
868
+ api.setSelection(normalizeRange(min, max));
869
+ return true;
870
+ }
871
+ case "key:down": {
872
+ const e = ev.e;
873
+ if (e.isComposing) return;
874
+ if (api.isEditing()) return;
875
+ const ac = api.activeCell();
876
+ if (!ac) return;
877
+ const key = e.key;
878
+ const isArrow = key === "ArrowUp" || key === "ArrowDown" || key === "ArrowLeft" || key === "ArrowRight";
879
+ const isTab = key === "Tab";
880
+ if (!isArrow && !isTab) return;
881
+ e.preventDefault();
882
+ const rows = api.numRows();
883
+ const cols = api.numCols();
884
+ if (rows <= 0 || cols <= 0) return true;
885
+ if (isTab) {
886
+ let next = ac;
887
+ if (e.shiftKey) {
888
+ if (ac.col > 0) next = {
889
+ row: ac.row,
890
+ col: ac.col - 1
891
+ };
892
+ else if (ac.row > 0) next = {
893
+ row: ac.row - 1,
894
+ col: cols - 1
895
+ };
896
+ } else if (ac.col < cols - 1) next = {
897
+ row: ac.row,
898
+ col: ac.col + 1
899
+ };
900
+ else if (ac.row < rows - 1) next = {
901
+ row: ac.row + 1,
902
+ col: 0
903
+ };
904
+ dragging = false;
905
+ setSingle(next, api);
906
+ return true;
907
+ }
908
+ const dr = key === "ArrowUp" ? -1 : key === "ArrowDown" ? 1 : 0;
909
+ const dc = key === "ArrowLeft" ? -1 : key === "ArrowRight" ? 1 : 0;
910
+ if (e.shiftKey) setRange(anchor ?? ac, moveBy(head ?? ac, dr, dc, rows, cols), api);
911
+ else {
912
+ const next = moveBy(ac, dr, dc, rows, cols);
913
+ dragging = false;
914
+ setSingle(next, api);
915
+ }
916
+ return true;
917
+ }
918
+ default: return;
919
+ }
920
+ }
921
+ };
922
+ }
923
+
924
+ //#endregion
925
+ export { Gridsheet, clipboardMemoryPlugin, clipboardTextPlugin, createPluginHost, deletePlugin, editingPlugin, normalizeRange, selectionPlugin };
554
926
  //# sourceMappingURL=index.js.map