@schukai/monster 4.109.0 → 4.111.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.
@@ -0,0 +1,2361 @@
1
+ /**
2
+ * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
3
+ * Node module: @schukai/monster
4
+ *
5
+ * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
6
+ * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
7
+ *
8
+ * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
9
+ * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
10
+ * For more information about purchasing a commercial license, please contact Volker Schukai.
11
+ *
12
+ * SPDX-License-Identifier: AGPL-3.0
13
+ */
14
+
15
+ import { instanceSymbol } from "../../constants.mjs";
16
+ import { addAttributeToken } from "../../dom/attributes.mjs";
17
+ import {
18
+ ATTRIBUTE_ERRORMESSAGE,
19
+ ATTRIBUTE_ROLE,
20
+ } from "../../dom/constants.mjs";
21
+ import { CustomControl } from "../../dom/customcontrol.mjs";
22
+ import {
23
+ assembleMethodSymbol,
24
+ registerCustomElement,
25
+ } from "../../dom/customelement.mjs";
26
+ import { fireCustomEvent } from "../../dom/events.mjs";
27
+ import { getLocaleOfDocument } from "../../dom/locale.mjs";
28
+ import { Observer } from "../../types/observer.mjs";
29
+ import { isArray, isObject, isString } from "../../types/is.mjs";
30
+ import { ID } from "../../types/id.mjs";
31
+ import { clone } from "../../util/clone.mjs";
32
+
33
+ import { SheetStyleSheet } from "./stylesheet/sheet.mjs";
34
+
35
+ export { Sheet };
36
+
37
+ const gridElementSymbol = Symbol("sheetGrid");
38
+ const gridWrapperSymbol = Symbol("sheetGridWrapper");
39
+ const spacerElementSymbol = Symbol("sheetGridSpacer");
40
+ const addRowButtonSymbol = Symbol("sheetAddRowButton");
41
+ const addColumnButtonSymbol = Symbol("sheetAddColumnButton");
42
+ const lastSnapshotSymbol = Symbol("sheetLastSnapshot");
43
+ const resizeStateSymbol = Symbol("sheetResizeState");
44
+ const skipRenderSymbol = Symbol("sheetSkipRender");
45
+ const lastViewportSymbol = Symbol("sheetLastViewport");
46
+ const scrollFrameSymbol = Symbol("sheetScrollFrame");
47
+ const forceRenderSymbol = Symbol("sheetForceRender");
48
+ const resizeFrameSymbol = Symbol("sheetResizeFrame");
49
+ const selectionSymbol = Symbol("sheetSelection");
50
+ const selectionBoxSymbol = Symbol("sheetSelectionBox");
51
+ const fillHandleSymbol = Symbol("sheetFillHandle");
52
+ const statusSymbol = Symbol("sheetStatus");
53
+ const statusTimeoutSymbol = Symbol("sheetStatusTimeout");
54
+ const contextMenuSymbol = Symbol("sheetContextMenu");
55
+ const menuStateSymbol = Symbol("sheetMenuState");
56
+ const dragFillSymbol = Symbol("sheetDragFill");
57
+ const lastCopySymbol = Symbol("sheetLastCopy");
58
+ const dragSelectSymbol = Symbol("sheetDragSelect");
59
+
60
+ class Sheet extends CustomControl {
61
+ static get [instanceSymbol]() {
62
+ return Symbol.for("@schukai/monster/components/form/sheet@@instance");
63
+ }
64
+
65
+ [assembleMethodSymbol]() {
66
+ super[assembleMethodSymbol]();
67
+ initControlReferences.call(this);
68
+ initEventHandler.call(this);
69
+ initOptionObserver.call(this);
70
+ updateControl.call(this);
71
+ return this;
72
+ }
73
+
74
+ get defaults() {
75
+ return Object.assign({}, super.defaults, {
76
+ templates: { main: getTemplate() },
77
+ labels: getTranslations(),
78
+ classes: {
79
+ button: "monster-button-outline-primary",
80
+ },
81
+ features: {
82
+ addRows: false,
83
+ addColumns: false,
84
+ editable: true,
85
+ resizeRows: true,
86
+ resizeColumns: true,
87
+ virtualize: false,
88
+ },
89
+ virtualization: {
90
+ rowBuffer: 4,
91
+ columnBuffer: 2,
92
+ },
93
+ columns: defaultColumns(3),
94
+ rows: defaultRows(3),
95
+ sizes: {
96
+ columns: {},
97
+ rows: {},
98
+ rowHeaderWidth: 56,
99
+ headerHeight: 32,
100
+ },
101
+ constraints: {
102
+ minColumnWidth: 64,
103
+ maxColumnWidth: 360,
104
+ minRowHeight: 28,
105
+ maxRowHeight: 120,
106
+ },
107
+ value: { cells: {}, formulas: {} },
108
+ disabled: false,
109
+ cell: {
110
+ placeholder: "",
111
+ },
112
+ });
113
+ }
114
+
115
+ static getTag() {
116
+ return "monster-sheet";
117
+ }
118
+
119
+ static getCSSStyleSheet() {
120
+ return [SheetStyleSheet];
121
+ }
122
+
123
+ get value() {
124
+ return this.getOption("value");
125
+ }
126
+
127
+ set value(value) {
128
+ this.setOption("value", value);
129
+ this[forceRenderSymbol] = true;
130
+ setFormValueSafe.call(this);
131
+ updateControl.call(this);
132
+ }
133
+ }
134
+
135
+ function initControlReferences() {
136
+ const root = this.shadowRoot;
137
+ this[gridElementSymbol] = root.querySelector(`[${ATTRIBUTE_ROLE}=grid]`);
138
+ this[gridWrapperSymbol] = root.querySelector(
139
+ `[${ATTRIBUTE_ROLE}=grid-wrapper]`,
140
+ );
141
+ if (this[gridWrapperSymbol]) {
142
+ let spacer = this[gridWrapperSymbol].querySelector(
143
+ `[${ATTRIBUTE_ROLE}=spacer]`,
144
+ );
145
+ if (!spacer) {
146
+ spacer = document.createElement("div");
147
+ spacer.setAttribute(ATTRIBUTE_ROLE, "spacer");
148
+ spacer.setAttribute("part", "spacer");
149
+ this[gridWrapperSymbol].prepend(spacer);
150
+ }
151
+ this[spacerElementSymbol] = spacer;
152
+ }
153
+ this[addRowButtonSymbol] = root.querySelector(`[${ATTRIBUTE_ROLE}=add-row]`);
154
+ this[addColumnButtonSymbol] = root.querySelector(
155
+ `[${ATTRIBUTE_ROLE}=add-column]`,
156
+ );
157
+ this[selectionBoxSymbol] = root.querySelector(
158
+ `[${ATTRIBUTE_ROLE}=selection]`,
159
+ );
160
+ this[fillHandleSymbol] = root.querySelector(
161
+ `[${ATTRIBUTE_ROLE}=fill-handle]`,
162
+ );
163
+ this[statusSymbol] = root.querySelector(`[${ATTRIBUTE_ROLE}=status]`);
164
+ this[contextMenuSymbol] = root.querySelector(
165
+ `[${ATTRIBUTE_ROLE}=context-menu]`,
166
+ );
167
+ }
168
+
169
+ function initEventHandler() {
170
+ this[addRowButtonSymbol].addEventListener("click", () => {
171
+ if (!canAddRows.call(this)) return;
172
+ addRow.call(this);
173
+ });
174
+
175
+ this[addColumnButtonSymbol].addEventListener("click", () => {
176
+ if (!canAddColumns.call(this)) return;
177
+ addColumn.call(this);
178
+ });
179
+
180
+ this[gridElementSymbol].addEventListener("input", (event) => {
181
+ const input = event.target;
182
+ if (!(input instanceof HTMLInputElement)) return;
183
+ const cell = input.closest(`[${ATTRIBUTE_ROLE}=cell]`);
184
+ if (!cell) return;
185
+ const rowId = cell.dataset.rowId;
186
+ const colId = cell.dataset.colId;
187
+ if (!rowId || !colId) return;
188
+ setCellValue.call(this, rowId, colId, input.value);
189
+ });
190
+
191
+ this[gridElementSymbol].addEventListener("focusin", (event) => {
192
+ const input = event.target;
193
+ if (!(input instanceof HTMLInputElement)) return;
194
+ const cell = input.closest(`[${ATTRIBUTE_ROLE}=cell]`);
195
+ if (!cell) return;
196
+ const rowId = cell.dataset.rowId;
197
+ const colId = cell.dataset.colId;
198
+ if (!rowId || !colId) return;
199
+ const data = normalizeValue.call(this);
200
+ const formula = data.formulas?.[rowId]?.[colId];
201
+ if (isString(formula)) {
202
+ input.value = formula;
203
+ }
204
+ const selection = this[selectionSymbol];
205
+ const isMulti =
206
+ selection?.anchor &&
207
+ selection?.focus &&
208
+ (selection.anchor.rowId !== selection.focus.rowId ||
209
+ selection.anchor.colId !== selection.focus.colId);
210
+ if (!event.shiftKey && isMulti) return;
211
+ setSelectionFromCell.call(this, rowId, colId, event.shiftKey);
212
+ });
213
+
214
+ this[gridElementSymbol].addEventListener("focusout", (event) => {
215
+ const input = event.target;
216
+ if (!(input instanceof HTMLInputElement)) return;
217
+ const cell = input.closest(`[${ATTRIBUTE_ROLE}=cell]`);
218
+ if (!cell) return;
219
+ const rowId = cell.dataset.rowId;
220
+ const colId = cell.dataset.colId;
221
+ if (!rowId || !colId) return;
222
+ refreshDisplayValues.call(this);
223
+ });
224
+
225
+ this[gridElementSymbol].addEventListener("pointerdown", (event) => {
226
+ if (event.button !== 0) return;
227
+ const columnHandle = event.target.closest(
228
+ `[${ATTRIBUTE_ROLE}=column-resize]`,
229
+ );
230
+ if (columnHandle) {
231
+ if (!this.getOption("features.resizeColumns", true)) return;
232
+ startColumnResize.call(this, event, columnHandle);
233
+ return;
234
+ }
235
+
236
+ const rowHandle = event.target.closest(`[${ATTRIBUTE_ROLE}=row-resize]`);
237
+ if (rowHandle) {
238
+ if (!this.getOption("features.resizeRows", true)) return;
239
+ startRowResize.call(this, event, rowHandle);
240
+ return;
241
+ }
242
+
243
+ const cell = event.target.closest(`[${ATTRIBUTE_ROLE}=cell]`);
244
+ if (!cell) return;
245
+ const rowId = cell.dataset.rowId;
246
+ const colId = cell.dataset.colId;
247
+ if (!rowId || !colId) return;
248
+ setSelectionFromCell.call(this, rowId, colId, event.shiftKey);
249
+ startSelectionDrag.call(this, event);
250
+ });
251
+
252
+ this[gridElementSymbol].addEventListener("keydown", (event) => {
253
+ if (!(event.ctrlKey || event.metaKey)) return;
254
+ if (event.key === "c" || event.key === "C") {
255
+ event.preventDefault();
256
+ void copySelectionToClipboard.call(this);
257
+ return;
258
+ }
259
+ if (event.key === "x" || event.key === "X") {
260
+ event.preventDefault();
261
+ void cutSelectionToClipboard.call(this);
262
+ return;
263
+ }
264
+ if (event.key === "v" || event.key === "V") {
265
+ event.preventDefault();
266
+ void pasteFromClipboard.call(this);
267
+ }
268
+ });
269
+
270
+ this[gridElementSymbol].addEventListener("contextmenu", (event) => {
271
+ const cell = event.target.closest(`[${ATTRIBUTE_ROLE}=cell]`);
272
+ if (!cell) return;
273
+ event.preventDefault();
274
+ const rowId = cell.dataset.rowId;
275
+ const colId = cell.dataset.colId;
276
+ if (rowId && colId) {
277
+ setSelectionFromCell.call(this, rowId, colId, false);
278
+ }
279
+ showContextMenu.call(this, event.clientX, event.clientY);
280
+ });
281
+
282
+ this.shadowRoot.addEventListener("pointerdown", (event) => {
283
+ const menu = this[contextMenuSymbol];
284
+ if (!menu || menu.hidden) return;
285
+ if (menu.contains(event.target)) return;
286
+ hideContextMenu.call(this);
287
+ });
288
+
289
+ if (this[gridWrapperSymbol]) {
290
+ this[gridWrapperSymbol].addEventListener("scroll", () => {
291
+ if (!this.getOption("features.virtualize", false)) return;
292
+ if (this[scrollFrameSymbol]) return;
293
+ this[scrollFrameSymbol] = requestAnimationFrame(() => {
294
+ this[scrollFrameSymbol] = null;
295
+ updateControl.call(this);
296
+ });
297
+ });
298
+ }
299
+
300
+ if (this[gridWrapperSymbol]) {
301
+ this[gridWrapperSymbol].addEventListener("scroll", () => {
302
+ hideContextMenu.call(this);
303
+ });
304
+ }
305
+
306
+ if (this[fillHandleSymbol]) {
307
+ this[fillHandleSymbol].addEventListener("pointerdown", (event) => {
308
+ startFillDrag.call(this, event);
309
+ });
310
+ }
311
+
312
+ wireContextMenuActions.call(this);
313
+ }
314
+
315
+ function initOptionObserver() {
316
+ this[lastSnapshotSymbol] = "";
317
+ this.attachObserver(
318
+ new Observer(() => {
319
+ const snapshot = JSON.stringify({
320
+ value: this.getOption("value"),
321
+ columns: this.getOption("columns"),
322
+ rows: this.getOption("rows"),
323
+ sizes: this.getOption("sizes"),
324
+ features: this.getOption("features"),
325
+ disabled: this.getOption("disabled"),
326
+ classes: this.getOption("classes"),
327
+ labels: this.getOption("labels"),
328
+ cell: this.getOption("cell"),
329
+ });
330
+ if (this[skipRenderSymbol]) {
331
+ this[lastSnapshotSymbol] = snapshot;
332
+ this[skipRenderSymbol] = false;
333
+ return;
334
+ }
335
+ if (snapshot === this[lastSnapshotSymbol]) return;
336
+ this[lastSnapshotSymbol] = snapshot;
337
+ updateControl.call(this);
338
+ }),
339
+ );
340
+ }
341
+
342
+ function updateControl() {
343
+ const normalized = normalizeValue.call(this);
344
+ const current = this.getOption("value");
345
+ if (!isSameValue(current, normalized)) {
346
+ this.setOption("value", normalized);
347
+ setFormValueSafe.call(this);
348
+ }
349
+
350
+ const virtualize = this.getOption("features.virtualize", false) === true;
351
+ if (virtualize) {
352
+ this.setAttribute("data-virtualized", "");
353
+ if (this[forceRenderSymbol]) {
354
+ this[lastViewportSymbol] = null;
355
+ this[forceRenderSymbol] = false;
356
+ }
357
+ renderVirtual.call(this, normalized);
358
+ } else {
359
+ if (this.hasAttribute("data-virtualized")) {
360
+ this.removeAttribute("data-virtualized");
361
+ }
362
+ if (this[spacerElementSymbol]) {
363
+ this[spacerElementSymbol].style.width = "0px";
364
+ this[spacerElementSymbol].style.height = "0px";
365
+ }
366
+ this[lastViewportSymbol] = null;
367
+ const displayCells = buildDisplayCells.call(this, normalized);
368
+ renderGrid.call(this, normalized, displayCells);
369
+ }
370
+ syncToolbarState.call(this);
371
+ applyDisabledState.call(this);
372
+ updateSelectionDisplay.call(this);
373
+ }
374
+
375
+ function syncToolbarState() {
376
+ const addRow = canAddRows.call(this);
377
+ const addCol = canAddColumns.call(this);
378
+ const labels = this.getOption("labels", {});
379
+ const buttonClass = this.getOption("classes.button", "");
380
+
381
+ this[addRowButtonSymbol].hidden = !addRow;
382
+ this[addColumnButtonSymbol].hidden = !addCol;
383
+ this[addRowButtonSymbol].disabled =
384
+ this.getOption("disabled", false) || !addRow;
385
+ this[addColumnButtonSymbol].disabled =
386
+ this.getOption("disabled", false) || !addCol;
387
+
388
+ this[addRowButtonSymbol].className = buttonClass;
389
+ this[addColumnButtonSymbol].className = buttonClass;
390
+ if (isString(labels.addRow)) {
391
+ this[addRowButtonSymbol].textContent = labels.addRow;
392
+ }
393
+ if (isString(labels.addColumn)) {
394
+ this[addColumnButtonSymbol].textContent = labels.addColumn;
395
+ }
396
+ syncContextMenuLabels.call(this);
397
+ }
398
+
399
+ function applyDisabledState() {
400
+ const disabled = this.getOption("disabled", false);
401
+ const editable = this.getOption("features.editable", true);
402
+ const inputs = this.shadowRoot.querySelectorAll(
403
+ `[${ATTRIBUTE_ROLE}=cell-input]`,
404
+ );
405
+ inputs.forEach((input) => {
406
+ input.disabled = disabled || !editable;
407
+ });
408
+ }
409
+
410
+ function renderGrid(value, displayCells) {
411
+ const grid = this[gridElementSymbol];
412
+ if (!grid) return;
413
+ grid.style.position = "";
414
+ grid.style.left = "";
415
+ grid.style.top = "";
416
+ grid.style.transform = "";
417
+ grid.textContent = "";
418
+
419
+ const fragment = document.createDocumentFragment();
420
+ fragment.appendChild(buildCornerCell.call(this));
421
+
422
+ const lastColumnId =
423
+ value.columns.length > 0
424
+ ? value.columns[value.columns.length - 1].id
425
+ : null;
426
+
427
+ for (const col of value.columns) {
428
+ fragment.appendChild(
429
+ buildColumnHeader.call(this, col, col.id === lastColumnId),
430
+ );
431
+ }
432
+
433
+ for (const row of value.rows) {
434
+ fragment.appendChild(buildRowHeader.call(this, row));
435
+ for (const col of value.columns) {
436
+ fragment.appendChild(
437
+ buildCell.call(this, row, col, displayCells, col.id === lastColumnId),
438
+ );
439
+ }
440
+ }
441
+
442
+ grid.appendChild(fragment);
443
+ applyGridTemplate.call(this, value);
444
+ }
445
+
446
+ function renderVirtual(value) {
447
+ const grid = this[gridElementSymbol];
448
+ const wrapper = this[gridWrapperSymbol];
449
+ const spacer = this[spacerElementSymbol];
450
+ if (!grid || !wrapper || !spacer) return;
451
+
452
+ const sizes = getVirtualSizes.call(this, value);
453
+ const viewportWidth = wrapper.clientWidth;
454
+ const viewportHeight = wrapper.clientHeight;
455
+ if (viewportWidth === 0 || viewportHeight === 0) {
456
+ if (!this[scrollFrameSymbol]) {
457
+ this[scrollFrameSymbol] = requestAnimationFrame(() => {
458
+ this[scrollFrameSymbol] = null;
459
+ updateControl.call(this);
460
+ });
461
+ }
462
+ return;
463
+ }
464
+ const scrollLeft = wrapper.scrollLeft;
465
+ const scrollTop = wrapper.scrollTop;
466
+ const bufferCols = getSizeNumber(
467
+ this.getOption("virtualization.columnBuffer"),
468
+ 2,
469
+ );
470
+ const bufferRows = getSizeNumber(
471
+ this.getOption("virtualization.rowBuffer"),
472
+ 4,
473
+ );
474
+
475
+ const visible = getVisibleRange(
476
+ sizes.columnOffsets,
477
+ sizes.rowOffsets,
478
+ scrollLeft - sizes.rowHeaderWidth,
479
+ scrollTop - sizes.headerHeight,
480
+ viewportWidth,
481
+ viewportHeight,
482
+ );
483
+
484
+ const colStart = Math.max(0, visible.colStart - bufferCols);
485
+ const colEnd = Math.min(
486
+ value.columns.length - 1,
487
+ visible.colEnd + bufferCols,
488
+ );
489
+ const rowStart = Math.max(0, visible.rowStart - bufferRows);
490
+ const rowEnd = Math.min(value.rows.length - 1, visible.rowEnd + bufferRows);
491
+
492
+ const viewportKey = `${colStart}-${colEnd}-${rowStart}-${rowEnd}-${sizes.totalWidth}-${sizes.totalHeight}`;
493
+ if (this[lastViewportSymbol] === viewportKey) return;
494
+ this[lastViewportSymbol] = viewportKey;
495
+
496
+ spacer.style.width = `${sizes.totalWidth}px`;
497
+ spacer.style.height = `${sizes.totalHeight}px`;
498
+
499
+ const offsetX = sizes.rowHeaderWidth + sizes.columnOffsets[colStart];
500
+ const offsetY = sizes.headerHeight + sizes.rowOffsets[rowStart];
501
+
502
+ grid.style.position = "absolute";
503
+ grid.style.left = "0";
504
+ grid.style.top = "0";
505
+ grid.style.transform = `translate(${offsetX}px, ${offsetY}px)`;
506
+
507
+ const visibleColumns = value.columns.slice(colStart, colEnd + 1);
508
+ const visibleRows = value.rows.slice(rowStart, rowEnd + 1);
509
+
510
+ const visibleWidths = visibleColumns.map(
511
+ (col, index) => `${sizes.columnWidths[colStart + index]}px`,
512
+ );
513
+ const visibleHeights = visibleRows.map(
514
+ (row, index) => `${sizes.rowHeights[rowStart + index]}px`,
515
+ );
516
+
517
+ grid.style.gridTemplateColumns = `${sizes.rowHeaderWidth}px ${visibleWidths.join(" ")}`;
518
+ grid.style.gridTemplateRows = `${sizes.headerHeight}px ${visibleHeights.join(" ")}`;
519
+
520
+ grid.textContent = "";
521
+ const fragment = document.createDocumentFragment();
522
+ fragment.appendChild(buildCornerCell.call(this));
523
+
524
+ const lastVisibleColumnId =
525
+ visibleColumns.length > 0
526
+ ? visibleColumns[visibleColumns.length - 1].id
527
+ : null;
528
+
529
+ for (const col of visibleColumns) {
530
+ fragment.appendChild(
531
+ buildColumnHeader.call(this, col, col.id === lastVisibleColumnId),
532
+ );
533
+ }
534
+
535
+ const errors = [];
536
+ const getDisplayValue = (rowId, colId) => {
537
+ const formula = value.formulas?.[rowId]?.[colId];
538
+ if (isString(formula)) {
539
+ const evaluated = evaluateFormula.call(this, value, formula);
540
+ if (Number.isFinite(evaluated)) return evaluated;
541
+ errors.push({ rowId, colId, formula });
542
+ return "#ERR";
543
+ }
544
+ const raw = value.cells?.[rowId]?.[colId];
545
+ return raw === undefined || raw === null ? "" : raw;
546
+ };
547
+
548
+ for (const row of visibleRows) {
549
+ fragment.appendChild(buildRowHeader.call(this, row));
550
+ for (const col of visibleColumns) {
551
+ fragment.appendChild(
552
+ buildCell.call(
553
+ this,
554
+ row,
555
+ col,
556
+ getDisplayValue,
557
+ col.id === lastVisibleColumnId,
558
+ ),
559
+ );
560
+ }
561
+ }
562
+
563
+ if (errors.length > 0) {
564
+ fireCustomEvent(this, "monster-sheet-formula-error", { errors });
565
+ }
566
+
567
+ grid.appendChild(fragment);
568
+ }
569
+
570
+ function buildCornerCell() {
571
+ const labels = this.getOption("labels", {});
572
+ const cell = document.createElement("div");
573
+ cell.setAttribute(ATTRIBUTE_ROLE, "corner");
574
+ cell.setAttribute("part", "corner");
575
+ cell.textContent = isString(labels.corner) ? labels.corner : "";
576
+ return cell;
577
+ }
578
+
579
+ function buildColumnHeader(column, isLastColumn) {
580
+ const cell = document.createElement("div");
581
+ cell.setAttribute(ATTRIBUTE_ROLE, "column-header");
582
+ cell.setAttribute("part", "column-header");
583
+ cell.dataset.colId = column.id;
584
+ if (isLastColumn) {
585
+ cell.dataset.lastColumn = "true";
586
+ }
587
+ cell.textContent = column.label ?? column.id;
588
+
589
+ if (this.getOption("features.resizeColumns", true)) {
590
+ const handle = document.createElement("span");
591
+ handle.setAttribute(ATTRIBUTE_ROLE, "column-resize");
592
+ handle.setAttribute("part", "column-resize");
593
+ handle.dataset.colId = column.id;
594
+ cell.appendChild(handle);
595
+ }
596
+ return cell;
597
+ }
598
+
599
+ function buildRowHeader(row) {
600
+ const cell = document.createElement("div");
601
+ cell.setAttribute(ATTRIBUTE_ROLE, "row-header");
602
+ cell.setAttribute("part", "row-header");
603
+ cell.dataset.rowId = row.id;
604
+ cell.textContent = row.label ?? row.id;
605
+
606
+ if (this.getOption("features.resizeRows", true)) {
607
+ const handle = document.createElement("span");
608
+ handle.setAttribute(ATTRIBUTE_ROLE, "row-resize");
609
+ handle.setAttribute("part", "row-resize");
610
+ handle.dataset.rowId = row.id;
611
+ cell.appendChild(handle);
612
+ }
613
+ return cell;
614
+ }
615
+
616
+ function buildCell(row, column, cells, isLastColumn) {
617
+ const wrapper = document.createElement("div");
618
+ wrapper.setAttribute(ATTRIBUTE_ROLE, "cell");
619
+ wrapper.setAttribute("part", "cell");
620
+ wrapper.dataset.rowId = row.id;
621
+ wrapper.dataset.colId = column.id;
622
+ if (isLastColumn) {
623
+ wrapper.dataset.lastColumn = "true";
624
+ }
625
+
626
+ const input = document.createElement("input");
627
+ input.setAttribute(ATTRIBUTE_ROLE, "cell-input");
628
+ input.setAttribute("part", "cell-input");
629
+ input.type = "text";
630
+ input.autocomplete = "off";
631
+ input.inputMode = "text";
632
+ input.placeholder = this.getOption("cell.placeholder", "");
633
+ const value =
634
+ typeof cells === "function"
635
+ ? cells(row.id, column.id)
636
+ : cells?.[row.id]?.[column.id];
637
+ input.value = value === undefined || value === null ? "" : String(value);
638
+ input.disabled =
639
+ this.getOption("disabled", false) ||
640
+ !this.getOption("features.editable", true);
641
+
642
+ wrapper.appendChild(input);
643
+ return wrapper;
644
+ }
645
+
646
+ function applyGridTemplate(value) {
647
+ const sizes = this.getOption("sizes", {});
648
+ const rowHeaderWidth = getRowHeaderWidthNumber.call(
649
+ this,
650
+ sizes.rowHeaderWidth,
651
+ value.rows,
652
+ );
653
+ const headerHeight = getSizeNumber(sizes.headerHeight, 32);
654
+
655
+ const columnSizes = value.columns.map((col) =>
656
+ getColumnWidth.call(this, col.id),
657
+ );
658
+ const rowSizes = value.rows.map((row) => getRowHeight.call(this, row.id));
659
+
660
+ this[gridElementSymbol].style.gridTemplateColumns =
661
+ `${rowHeaderWidth}px ${columnSizes.join(" ")}`;
662
+ this[gridElementSymbol].style.gridTemplateRows =
663
+ `${headerHeight}px ${rowSizes.join(" ")}`;
664
+ }
665
+
666
+ function getVirtualSizes(value) {
667
+ const sizes = this.getOption("sizes", {});
668
+ const rowHeaderWidth = getRowHeaderWidthNumber.call(
669
+ this,
670
+ sizes.rowHeaderWidth,
671
+ value.rows,
672
+ );
673
+ const headerHeight = getSizeNumber(sizes.headerHeight, 32);
674
+ const columnWidths = value.columns.map((col) =>
675
+ getColumnWidthNumber.call(this, col.id),
676
+ );
677
+ const rowHeights = value.rows.map((row) =>
678
+ getRowHeightNumber.call(this, row.id),
679
+ );
680
+
681
+ const columnOffsets = buildOffsets(columnWidths);
682
+ const rowOffsets = buildOffsets(rowHeights);
683
+
684
+ const totalWidth =
685
+ rowHeaderWidth +
686
+ (columnWidths.length > 0
687
+ ? columnOffsets[columnOffsets.length - 1] +
688
+ columnWidths[columnWidths.length - 1]
689
+ : 0);
690
+ const totalHeight =
691
+ headerHeight +
692
+ (rowHeights.length > 0
693
+ ? rowOffsets[rowOffsets.length - 1] + rowHeights[rowHeights.length - 1]
694
+ : 0);
695
+
696
+ return {
697
+ rowHeaderWidth,
698
+ headerHeight,
699
+ columnWidths,
700
+ rowHeights,
701
+ columnOffsets,
702
+ rowOffsets,
703
+ totalWidth,
704
+ totalHeight,
705
+ };
706
+ }
707
+
708
+ function buildOffsets(values) {
709
+ const offsets = [];
710
+ let current = 0;
711
+ for (const value of values) {
712
+ offsets.push(current);
713
+ current += value;
714
+ }
715
+ return offsets;
716
+ }
717
+
718
+ function getVisibleRange(
719
+ columnOffsets,
720
+ rowOffsets,
721
+ scrollLeft,
722
+ scrollTop,
723
+ viewportWidth,
724
+ viewportHeight,
725
+ ) {
726
+ const colStart = findStartIndex(columnOffsets, Math.max(0, scrollLeft));
727
+ const colEnd = findEndIndex(
728
+ columnOffsets,
729
+ Math.max(0, scrollLeft) + viewportWidth,
730
+ );
731
+ const rowStart = findStartIndex(rowOffsets, Math.max(0, scrollTop));
732
+ const rowEnd = findEndIndex(
733
+ rowOffsets,
734
+ Math.max(0, scrollTop) + viewportHeight,
735
+ );
736
+ return { colStart, colEnd, rowStart, rowEnd };
737
+ }
738
+
739
+ function findStartIndex(offsets, value) {
740
+ let low = 0;
741
+ let high = offsets.length - 1;
742
+ let result = 0;
743
+ while (low <= high) {
744
+ const mid = Math.floor((low + high) / 2);
745
+ if (offsets[mid] <= value) {
746
+ result = mid;
747
+ low = mid + 1;
748
+ } else {
749
+ high = mid - 1;
750
+ }
751
+ }
752
+ return result;
753
+ }
754
+
755
+ function findEndIndex(offsets, value) {
756
+ let low = 0;
757
+ let high = offsets.length - 1;
758
+ let result = offsets.length - 1;
759
+ while (low <= high) {
760
+ const mid = Math.floor((low + high) / 2);
761
+ if (offsets[mid] < value) {
762
+ low = mid + 1;
763
+ } else {
764
+ result = mid;
765
+ high = mid - 1;
766
+ }
767
+ }
768
+ return Math.max(0, result);
769
+ }
770
+
771
+ function normalizeValue() {
772
+ const optionsValue = this.getOption("value");
773
+ const columns = normalizeColumns(
774
+ optionsValue?.columns ?? this.getOption("columns"),
775
+ );
776
+ const rows = normalizeRows(optionsValue?.rows ?? this.getOption("rows"));
777
+ const cells = isObject(optionsValue?.cells) ? clone(optionsValue.cells) : {};
778
+ const formulas = isObject(optionsValue?.formulas)
779
+ ? clone(optionsValue.formulas)
780
+ : {};
781
+
782
+ return { columns, rows, cells, formulas };
783
+ }
784
+
785
+ function normalizeColumns(columns) {
786
+ const list = isArray(columns) ? columns : [];
787
+ return list.map((col, index) => {
788
+ if (isString(col)) {
789
+ return { id: col, label: col };
790
+ }
791
+ if (isObject(col)) {
792
+ const id = isString(col.id) ? col.id : nextColumnLabel(index);
793
+ return { id, label: col.label ?? id };
794
+ }
795
+ const id = nextColumnLabel(index);
796
+ return { id, label: id };
797
+ });
798
+ }
799
+
800
+ function normalizeRows(rows) {
801
+ const list = isArray(rows) ? rows : [];
802
+ return list.map((row, index) => {
803
+ if (isString(row)) {
804
+ return { id: row, label: row };
805
+ }
806
+ if (isObject(row)) {
807
+ const id = isString(row.id) ? row.id : nextRowLabel(index);
808
+ return { id, label: row.label ?? id };
809
+ }
810
+ const id = nextRowLabel(index);
811
+ return { id, label: id };
812
+ });
813
+ }
814
+
815
+ function setCellValue(rowId, colId, value) {
816
+ const data = normalizeValue.call(this);
817
+ if (!data.cells[rowId]) data.cells[rowId] = {};
818
+ if (!data.formulas[rowId]) data.formulas[rowId] = {};
819
+ const next = isString(value) ? value : String(value);
820
+ if (next.trim().startsWith("=")) {
821
+ data.formulas[rowId][colId] = next.trim();
822
+ delete data.cells[rowId][colId];
823
+ } else {
824
+ delete data.formulas[rowId][colId];
825
+ data.cells[rowId][colId] = value;
826
+ }
827
+ this.setOption("value", data);
828
+ this[skipRenderSymbol] = true;
829
+ this[forceRenderSymbol] = true;
830
+ setFormValueSafe.call(this);
831
+ fireCustomEvent(this, "monster-sheet-change", {
832
+ value: data,
833
+ cell: { rowId, colId, value },
834
+ });
835
+ }
836
+
837
+ function addRow() {
838
+ const data = normalizeValue.call(this);
839
+ const newRow = createRow(data.rows.length, data.rows);
840
+ data.rows.push(newRow);
841
+ this.setOption("value", data);
842
+ this[skipRenderSymbol] = true;
843
+ setFormValueSafe.call(this);
844
+ fireCustomEvent(this, "monster-sheet-add-row", { row: newRow, value: data });
845
+ updateControl.call(this);
846
+ }
847
+
848
+ function addColumn() {
849
+ const data = normalizeValue.call(this);
850
+ const newColumn = createColumn(data.columns.length, data.columns);
851
+ data.columns.push(newColumn);
852
+ this.setOption("value", data);
853
+ this[skipRenderSymbol] = true;
854
+ setFormValueSafe.call(this);
855
+ fireCustomEvent(this, "monster-sheet-add-column", {
856
+ column: newColumn,
857
+ value: data,
858
+ });
859
+ updateControl.call(this);
860
+ }
861
+
862
+ function canAddRows() {
863
+ return this.getOption("features.addRows", false) === true;
864
+ }
865
+
866
+ function canAddColumns() {
867
+ return this.getOption("features.addColumns", false) === true;
868
+ }
869
+
870
+ function createColumn(index, columns) {
871
+ const label = nextColumnLabel(index);
872
+ const existing = new Set(columns.map((col) => col.id));
873
+ const id = existing.has(label) ? new ID("column-").toString() : label;
874
+ return { id, label };
875
+ }
876
+
877
+ function createRow(index, rows) {
878
+ const label = nextRowLabel(index);
879
+ const existing = new Set(rows.map((row) => row.id));
880
+ const id = existing.has(label) ? new ID("row-").toString() : label;
881
+ return { id, label };
882
+ }
883
+
884
+ function nextColumnLabel(index) {
885
+ let value = index + 1;
886
+ let label = "";
887
+ while (value > 0) {
888
+ const mod = (value - 1) % 26;
889
+ label = String.fromCharCode(65 + mod) + label;
890
+ value = Math.floor((value - 1) / 26);
891
+ }
892
+ return label;
893
+ }
894
+
895
+ function nextRowLabel(index) {
896
+ return String(index + 1);
897
+ }
898
+
899
+ function defaultColumns(count) {
900
+ return Array.from({ length: count }, (_, i) => {
901
+ const label = nextColumnLabel(i);
902
+ return { id: label, label };
903
+ });
904
+ }
905
+
906
+ function defaultRows(count) {
907
+ return Array.from({ length: count }, (_, i) => {
908
+ const label = nextRowLabel(i);
909
+ return { id: label, label };
910
+ });
911
+ }
912
+
913
+ function getSizeNumber(value, fallback) {
914
+ const n = Number(value);
915
+ return Number.isFinite(n) ? n : fallback;
916
+ }
917
+
918
+ function getColumnWidth(columnId) {
919
+ const sizes = this.getOption("sizes.columns", {});
920
+ const width = getSizeNumber(sizes?.[columnId], 120);
921
+ const min = getSizeNumber(this.getOption("constraints.minColumnWidth"), 64);
922
+ const max = getSizeNumber(this.getOption("constraints.maxColumnWidth"), 360);
923
+ return `${clamp(width, min, max)}px`;
924
+ }
925
+
926
+ function getColumnWidthNumber(columnId) {
927
+ const sizes = this.getOption("sizes.columns", {});
928
+ const width = getSizeNumber(sizes?.[columnId], 120);
929
+ const min = getSizeNumber(this.getOption("constraints.minColumnWidth"), 64);
930
+ const max = getSizeNumber(this.getOption("constraints.maxColumnWidth"), 360);
931
+ return clamp(width, min, max);
932
+ }
933
+
934
+ function getRowHeight(rowId) {
935
+ const sizes = this.getOption("sizes.rows", {});
936
+ const height = getSizeNumber(sizes?.[rowId], 32);
937
+ const min = getSizeNumber(this.getOption("constraints.minRowHeight"), 28);
938
+ const max = getSizeNumber(this.getOption("constraints.maxRowHeight"), 120);
939
+ return `${clamp(height, min, max)}px`;
940
+ }
941
+
942
+ function getRowHeightNumber(rowId) {
943
+ const sizes = this.getOption("sizes.rows", {});
944
+ const height = getSizeNumber(sizes?.[rowId], 32);
945
+ const min = getSizeNumber(this.getOption("constraints.minRowHeight"), 28);
946
+ const max = getSizeNumber(this.getOption("constraints.maxRowHeight"), 120);
947
+ return clamp(height, min, max);
948
+ }
949
+
950
+ function getRowHeaderWidthNumber(value, rows) {
951
+ if (value === "auto" || value === null || value === undefined) {
952
+ const maxLen = getMaxRowLabelLength(rows);
953
+ const base = 16;
954
+ const charWidth = 8;
955
+ return Math.max(56, base + maxLen * charWidth);
956
+ }
957
+ return getSizeNumber(value, 56);
958
+ }
959
+
960
+ function getMaxRowLabelLength(rows) {
961
+ if (!isArray(rows) || rows.length === 0) return 1;
962
+ const last = rows[rows.length - 1];
963
+ const label = isString(last?.label) ? last.label : last?.id;
964
+ const text = label === undefined || label === null ? "" : String(label);
965
+ if (/^\d+$/.test(text)) {
966
+ return String(rows.length).length;
967
+ }
968
+ return text.length;
969
+ }
970
+
971
+ function clamp(value, min, max) {
972
+ return Math.min(Math.max(value, min), max);
973
+ }
974
+
975
+ function startColumnResize(event, handle) {
976
+ const colId = handle.dataset.colId;
977
+ if (!colId) return;
978
+ event.preventDefault();
979
+ event.stopPropagation();
980
+ const grid = this[gridElementSymbol];
981
+ const headerCell = handle.parentElement;
982
+ if (!headerCell) return;
983
+ const startWidth = headerCell.getBoundingClientRect().width;
984
+ this.setAttribute("data-resizing", "");
985
+ this[resizeStateSymbol] = {
986
+ kind: "column",
987
+ id: colId,
988
+ start: event.clientX,
989
+ startSize: startWidth,
990
+ };
991
+ grid.setPointerCapture(event.pointerId);
992
+ grid.addEventListener("pointermove", handleResizeMove);
993
+ grid.addEventListener("pointerup", handleResizeEnd);
994
+ grid.addEventListener("pointercancel", handleResizeEnd);
995
+ }
996
+
997
+ function startRowResize(event, handle) {
998
+ const rowId = handle.dataset.rowId;
999
+ if (!rowId) return;
1000
+ event.preventDefault();
1001
+ event.stopPropagation();
1002
+ const headerCell = handle.parentElement;
1003
+ if (!headerCell) return;
1004
+ const startHeight = headerCell.getBoundingClientRect().height;
1005
+ const grid = this[gridElementSymbol];
1006
+ this.setAttribute("data-resizing", "");
1007
+ this[resizeStateSymbol] = {
1008
+ kind: "row",
1009
+ id: rowId,
1010
+ start: event.clientY,
1011
+ startSize: startHeight,
1012
+ };
1013
+ grid.setPointerCapture(event.pointerId);
1014
+ grid.addEventListener("pointermove", handleResizeMove);
1015
+ grid.addEventListener("pointerup", handleResizeEnd);
1016
+ grid.addEventListener("pointercancel", handleResizeEnd);
1017
+ }
1018
+
1019
+ function handleResizeMove(event) {
1020
+ const grid = event.currentTarget;
1021
+ const sheet = grid.getRootNode().host;
1022
+ if (!sheet || !sheet[resizeStateSymbol]) return;
1023
+ const state = sheet[resizeStateSymbol];
1024
+ if (state.kind === "column") {
1025
+ const delta = event.clientX - state.start;
1026
+ const size = state.startSize + delta;
1027
+ setColumnSize.call(sheet, state.id, size);
1028
+ } else if (state.kind === "row") {
1029
+ const delta = event.clientY - state.start;
1030
+ const size = state.startSize + delta;
1031
+ setRowSize.call(sheet, state.id, size);
1032
+ }
1033
+ }
1034
+
1035
+ function handleResizeEnd(event) {
1036
+ const grid = event.currentTarget;
1037
+ const sheet = grid.getRootNode().host;
1038
+ if (!sheet) return;
1039
+ if (sheet.hasAttribute("data-resizing")) {
1040
+ sheet.removeAttribute("data-resizing");
1041
+ }
1042
+ grid.releasePointerCapture(event.pointerId);
1043
+ grid.removeEventListener("pointermove", handleResizeMove);
1044
+ grid.removeEventListener("pointerup", handleResizeEnd);
1045
+ grid.removeEventListener("pointercancel", handleResizeEnd);
1046
+ sheet[resizeStateSymbol] = null;
1047
+ }
1048
+
1049
+ function setColumnSize(columnId, size) {
1050
+ const min = getSizeNumber(this.getOption("constraints.minColumnWidth"), 64);
1051
+ const max = getSizeNumber(this.getOption("constraints.maxColumnWidth"), 360);
1052
+ const next = clamp(size, min, max);
1053
+ this.setOption(`sizes.columns.${columnId}`, next);
1054
+ this[skipRenderSymbol] = true;
1055
+ if (this.getOption("features.virtualize", false) === true) {
1056
+ scheduleVirtualResizeRender.call(this);
1057
+ } else {
1058
+ applyGridTemplate.call(this, normalizeValue.call(this));
1059
+ }
1060
+ fireCustomEvent(this, "monster-sheet-resize-column", {
1061
+ columnId,
1062
+ width: next,
1063
+ });
1064
+ }
1065
+
1066
+ function setRowSize(rowId, size) {
1067
+ const min = getSizeNumber(this.getOption("constraints.minRowHeight"), 28);
1068
+ const max = getSizeNumber(this.getOption("constraints.maxRowHeight"), 120);
1069
+ const next = clamp(size, min, max);
1070
+ this.setOption(`sizes.rows.${rowId}`, next);
1071
+ this[skipRenderSymbol] = true;
1072
+ if (this.getOption("features.virtualize", false) === true) {
1073
+ scheduleVirtualResizeRender.call(this);
1074
+ } else {
1075
+ applyGridTemplate.call(this, normalizeValue.call(this));
1076
+ }
1077
+ fireCustomEvent(this, "monster-sheet-resize-row", {
1078
+ rowId,
1079
+ height: next,
1080
+ });
1081
+ }
1082
+
1083
+ function setFormValueSafe() {
1084
+ try {
1085
+ this.setFormValue(this.value);
1086
+ } catch (e) {
1087
+ addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
1088
+ }
1089
+ }
1090
+
1091
+ function isSameValue(a, b) {
1092
+ try {
1093
+ return JSON.stringify(a) === JSON.stringify(b);
1094
+ } catch (e) {
1095
+ return false;
1096
+ }
1097
+ }
1098
+
1099
+ function refreshDisplayValues() {
1100
+ const data = normalizeValue.call(this);
1101
+ const virtualize = this.getOption("features.virtualize", false) === true;
1102
+ const displayCells = virtualize ? null : buildDisplayCells.call(this, data);
1103
+ const active = this.shadowRoot?.activeElement;
1104
+ const inputs = this.shadowRoot.querySelectorAll(
1105
+ `[${ATTRIBUTE_ROLE}=cell-input]`,
1106
+ );
1107
+
1108
+ inputs.forEach((input) => {
1109
+ if (!(input instanceof HTMLInputElement)) return;
1110
+ if (input === active) return;
1111
+ const cell = input.closest(`[${ATTRIBUTE_ROLE}=cell]`);
1112
+ if (!cell) return;
1113
+ const rowId = cell.dataset.rowId;
1114
+ const colId = cell.dataset.colId;
1115
+ if (!rowId || !colId) return;
1116
+ const value = virtualize
1117
+ ? getCellDisplayValue.call(this, data, rowId, colId)
1118
+ : displayCells?.[rowId]?.[colId];
1119
+ input.value = value === undefined || value === null ? "" : String(value);
1120
+ });
1121
+ }
1122
+
1123
+ function setSelectionFromCell(rowId, colId, expand) {
1124
+ hideContextMenu.call(this);
1125
+ const selection = this[selectionSymbol] || {};
1126
+ if (expand && selection.anchor) {
1127
+ selection.focus = { rowId, colId };
1128
+ } else {
1129
+ selection.anchor = { rowId, colId };
1130
+ selection.focus = { rowId, colId };
1131
+ }
1132
+ this[selectionSymbol] = selection;
1133
+ updateSelectionDisplay.call(this);
1134
+ }
1135
+
1136
+ function setSelectionFocus(rowId, colId) {
1137
+ const selection = this[selectionSymbol] || {};
1138
+ if (!selection.anchor) {
1139
+ selection.anchor = { rowId, colId };
1140
+ }
1141
+ selection.focus = { rowId, colId };
1142
+ this[selectionSymbol] = selection;
1143
+ updateSelectionDisplay.call(this);
1144
+ }
1145
+
1146
+ function startSelectionDrag(event) {
1147
+ if (event.button !== 0) return;
1148
+ const cell = event.target.closest(`[${ATTRIBUTE_ROLE}=cell]`);
1149
+ if (!cell) return;
1150
+ const state = {
1151
+ active: true,
1152
+ startX: event.clientX,
1153
+ startY: event.clientY,
1154
+ moved: false,
1155
+ };
1156
+ this[dragSelectSymbol] = state;
1157
+ this.setAttribute("data-selecting", "");
1158
+ const move = (moveEvent) => {
1159
+ handleSelectionDragMove.call(this, moveEvent);
1160
+ };
1161
+ const end = () => {
1162
+ handleSelectionDragEnd.call(this);
1163
+ };
1164
+ window.addEventListener("pointermove", move);
1165
+ window.addEventListener("pointerup", end, { once: true });
1166
+ window.addEventListener("pointercancel", end, { once: true });
1167
+ state.cleanup = () => {
1168
+ window.removeEventListener("pointermove", move);
1169
+ };
1170
+ }
1171
+
1172
+ function handleSelectionDragMove(event) {
1173
+ const state = this[dragSelectSymbol];
1174
+ if (!state?.active) return;
1175
+ if (!state.moved) {
1176
+ const dx = event.clientX - state.startX;
1177
+ const dy = event.clientY - state.startY;
1178
+ if (Math.abs(dx) < 3 && Math.abs(dy) < 3) return;
1179
+ state.moved = true;
1180
+ }
1181
+ const cell = getCellFromPoint.call(this, event.clientX, event.clientY);
1182
+ if (!cell) return;
1183
+ const rowId = cell.dataset.rowId;
1184
+ const colId = cell.dataset.colId;
1185
+ if (!rowId || !colId) return;
1186
+ setSelectionFocus.call(this, rowId, colId);
1187
+ }
1188
+
1189
+ function handleSelectionDragEnd() {
1190
+ const state = this[dragSelectSymbol];
1191
+ if (!state) return;
1192
+ if (state.cleanup) state.cleanup();
1193
+ this[dragSelectSymbol] = null;
1194
+ this.removeAttribute("data-selecting");
1195
+ }
1196
+
1197
+ function updateSelectionDisplay() {
1198
+ const grid = this[gridElementSymbol];
1199
+ if (!grid) return;
1200
+ const data = normalizeValue.call(this);
1201
+ const selection = this[selectionSymbol];
1202
+ const range = selection ? getSelectionRange(selection, data) : null;
1203
+ const virtualize = this.getOption("features.virtualize", false) === true;
1204
+
1205
+ const cells = grid.querySelectorAll(`[${ATTRIBUTE_ROLE}=cell]`);
1206
+ let overlay = null;
1207
+ const wrapper = this[gridWrapperSymbol];
1208
+ const wrapperRect = wrapper?.getBoundingClientRect() ?? null;
1209
+ let minLeft = Infinity;
1210
+ let minTop = Infinity;
1211
+ let maxRight = -Infinity;
1212
+ let maxBottom = -Infinity;
1213
+
1214
+ cells.forEach((cell) => {
1215
+ const rowId = cell.dataset.rowId;
1216
+ const colId = cell.dataset.colId;
1217
+ let selected = false;
1218
+ if (range && rowId && colId) {
1219
+ const { rowIndexById, colIndexById } = range;
1220
+ const rowIndex = rowIndexById.get(rowId);
1221
+ const colIndex = colIndexById.get(colId);
1222
+ if (
1223
+ rowIndex !== undefined &&
1224
+ colIndex !== undefined &&
1225
+ rowIndex >= range.rowStart &&
1226
+ rowIndex <= range.rowEnd &&
1227
+ colIndex >= range.colStart &&
1228
+ colIndex <= range.colEnd
1229
+ ) {
1230
+ selected = true;
1231
+ }
1232
+ }
1233
+
1234
+ if (selected) {
1235
+ cell.dataset.selected = "true";
1236
+ if (wrapperRect) {
1237
+ const rect = cell.getBoundingClientRect();
1238
+ minLeft = Math.min(minLeft, rect.left);
1239
+ minTop = Math.min(minTop, rect.top);
1240
+ maxRight = Math.max(maxRight, rect.right);
1241
+ maxBottom = Math.max(maxBottom, rect.bottom);
1242
+ }
1243
+ } else {
1244
+ delete cell.dataset.selected;
1245
+ }
1246
+
1247
+ if (
1248
+ selection?.focus?.rowId === rowId &&
1249
+ selection?.focus?.colId === colId
1250
+ ) {
1251
+ cell.dataset.active = "true";
1252
+ } else {
1253
+ delete cell.dataset.active;
1254
+ }
1255
+ });
1256
+
1257
+ if (Number.isFinite(minLeft) && Number.isFinite(maxRight) && wrapperRect) {
1258
+ overlay = {
1259
+ left: minLeft - wrapperRect.left + wrapper.scrollLeft,
1260
+ top: minTop - wrapperRect.top + wrapper.scrollTop,
1261
+ width: Math.max(0, maxRight - minLeft),
1262
+ height: Math.max(0, maxBottom - minTop),
1263
+ };
1264
+ } else if (range && virtualize) {
1265
+ const sizes = getVirtualSizes.call(this, data);
1266
+ const colStart = range.colStart;
1267
+ const colEnd = range.colEnd;
1268
+ const rowStart = range.rowStart;
1269
+ const rowEnd = range.rowEnd;
1270
+ const left = sizes.rowHeaderWidth + sizes.columnOffsets[colStart];
1271
+ const top = sizes.headerHeight + sizes.rowOffsets[rowStart];
1272
+ const width =
1273
+ sizes.columnOffsets[colEnd] +
1274
+ sizes.columnWidths[colEnd] -
1275
+ sizes.columnOffsets[colStart];
1276
+ const height =
1277
+ sizes.rowOffsets[rowEnd] +
1278
+ sizes.rowHeights[rowEnd] -
1279
+ sizes.rowOffsets[rowStart];
1280
+ overlay = { left, top, width, height };
1281
+ }
1282
+
1283
+ updateSelectionOverlay.call(this, overlay);
1284
+ }
1285
+
1286
+ function updateSelectionOverlay(bounds) {
1287
+ const box = this[selectionBoxSymbol];
1288
+ const wrapper = this[gridWrapperSymbol];
1289
+ const handle = this[fillHandleSymbol];
1290
+ if (!box || !wrapper || !handle) return;
1291
+ if (!bounds) {
1292
+ box.style.display = "none";
1293
+ handle.style.display = "none";
1294
+ return;
1295
+ }
1296
+ box.style.display = "block";
1297
+ box.style.left = `${bounds.left}px`;
1298
+ box.style.top = `${bounds.top}px`;
1299
+ box.style.width = `${bounds.width}px`;
1300
+ box.style.height = `${bounds.height}px`;
1301
+ handle.style.display = "block";
1302
+ handle.style.left = `${bounds.left + bounds.width - 4}px`;
1303
+ handle.style.top = `${bounds.top + bounds.height - 4}px`;
1304
+ }
1305
+
1306
+ function getSelectionRange(selection, data) {
1307
+ if (!selection?.anchor || !selection?.focus) return null;
1308
+ const { rowIndexById, colIndexById } = getIndexMaps(data);
1309
+ const startRow = rowIndexById.get(selection.anchor.rowId);
1310
+ const endRow = rowIndexById.get(selection.focus.rowId);
1311
+ const startCol = colIndexById.get(selection.anchor.colId);
1312
+ const endCol = colIndexById.get(selection.focus.colId);
1313
+ if (
1314
+ startRow === undefined ||
1315
+ endRow === undefined ||
1316
+ startCol === undefined ||
1317
+ endCol === undefined
1318
+ ) {
1319
+ return null;
1320
+ }
1321
+ return {
1322
+ rowStart: Math.min(startRow, endRow),
1323
+ rowEnd: Math.max(startRow, endRow),
1324
+ colStart: Math.min(startCol, endCol),
1325
+ colEnd: Math.max(startCol, endCol),
1326
+ rowIndexById,
1327
+ colIndexById,
1328
+ };
1329
+ }
1330
+
1331
+ function getIndexMaps(data) {
1332
+ const rowIndexById = new Map();
1333
+ const colIndexById = new Map();
1334
+ data.rows.forEach((row, index) => {
1335
+ if (row?.id) rowIndexById.set(row.id, index);
1336
+ });
1337
+ data.columns.forEach((col, index) => {
1338
+ if (col?.id) colIndexById.set(col.id, index);
1339
+ });
1340
+ return { rowIndexById, colIndexById };
1341
+ }
1342
+
1343
+ function getActiveCell() {
1344
+ const active = this.shadowRoot?.activeElement;
1345
+ if (!(active instanceof HTMLInputElement)) return null;
1346
+ const cell = active.closest(`[${ATTRIBUTE_ROLE}=cell]`);
1347
+ if (!cell) return null;
1348
+ const rowId = cell.dataset.rowId;
1349
+ const colId = cell.dataset.colId;
1350
+ if (!rowId || !colId) return null;
1351
+ return { rowId, colId };
1352
+ }
1353
+
1354
+ async function copySelectionToClipboard() {
1355
+ const data = normalizeValue.call(this);
1356
+ const range = getSelectionRange.call(this, this[selectionSymbol], data);
1357
+ const resolvedRange = range ?? getRangeFromActiveCell.call(this, data);
1358
+ if (!resolvedRange) return;
1359
+
1360
+ const text = buildClipboardText(data, resolvedRange);
1361
+ this[lastCopySymbol] = {
1362
+ text,
1363
+ range: {
1364
+ rowStart: resolvedRange.rowStart,
1365
+ rowEnd: resolvedRange.rowEnd,
1366
+ colStart: resolvedRange.colStart,
1367
+ colEnd: resolvedRange.colEnd,
1368
+ },
1369
+ values: buildClipboardMatrix(data, resolvedRange),
1370
+ };
1371
+ const success = await writeClipboardText(text);
1372
+ if (success) {
1373
+ showStatus.call(this, this.getOption("labels.copied") || "Copied");
1374
+ }
1375
+ }
1376
+
1377
+ async function cutSelectionToClipboard() {
1378
+ const data = normalizeValue.call(this);
1379
+ const range = getSelectionRange.call(this, this[selectionSymbol], data);
1380
+ const resolvedRange = range ?? getRangeFromActiveCell.call(this, data);
1381
+ if (!resolvedRange) return;
1382
+
1383
+ const text = buildClipboardText(data, resolvedRange);
1384
+ const success = await writeClipboardText(text);
1385
+ if (!success) return;
1386
+
1387
+ const next = clearRange(data, resolvedRange);
1388
+ this.value = next;
1389
+ showStatus.call(this, this.getOption("labels.cutDone") || "Cut");
1390
+ }
1391
+
1392
+ async function pasteFromClipboard() {
1393
+ const text = await readClipboardText();
1394
+ if (text === null) return;
1395
+ const data = normalizeValue.call(this);
1396
+ const range = getSelectionRange.call(this, this[selectionSymbol], data);
1397
+ const startRange = range ?? getRangeFromActiveCell.call(this, data);
1398
+ if (!startRange) return;
1399
+
1400
+ const { startRow, startCol } = {
1401
+ startRow: startRange.rowStart,
1402
+ startCol: startRange.colStart,
1403
+ };
1404
+ const cached = this[lastCopySymbol];
1405
+ const next =
1406
+ cached && cached.text === text
1407
+ ? applyPasteCached(data, cached, startRow, startCol)
1408
+ : applyPasteText(data, text, startRow, startCol);
1409
+ this.value = next.value;
1410
+ setSelectionByRange.call(this, next.range, next.value);
1411
+ showStatus.call(this, this.getOption("labels.pasted") || "Pasted");
1412
+ }
1413
+
1414
+ function buildClipboardText(data, range) {
1415
+ const rows = [];
1416
+ for (let r = range.rowStart; r <= range.rowEnd; r += 1) {
1417
+ const rowId = data.rows[r]?.id;
1418
+ if (!rowId) continue;
1419
+ const cols = [];
1420
+ for (let c = range.colStart; c <= range.colEnd; c += 1) {
1421
+ const colId = data.columns[c]?.id;
1422
+ if (!colId) continue;
1423
+ const value = getCellRawValue(data, rowId, colId);
1424
+ cols.push(value);
1425
+ }
1426
+ rows.push(cols.join("\t"));
1427
+ }
1428
+ return rows.join("\n");
1429
+ }
1430
+
1431
+ function buildClipboardMatrix(data, range) {
1432
+ const rows = [];
1433
+ for (let r = range.rowStart; r <= range.rowEnd; r += 1) {
1434
+ const rowId = data.rows[r]?.id;
1435
+ if (!rowId) continue;
1436
+ const cols = [];
1437
+ for (let c = range.colStart; c <= range.colEnd; c += 1) {
1438
+ const colId = data.columns[c]?.id;
1439
+ if (!colId) continue;
1440
+ cols.push(getCellRawValue(data, rowId, colId));
1441
+ }
1442
+ rows.push(cols);
1443
+ }
1444
+ return rows;
1445
+ }
1446
+
1447
+ function getCellRawValue(data, rowId, colId) {
1448
+ const formula = data.formulas?.[rowId]?.[colId];
1449
+ if (isString(formula)) return formula;
1450
+ const raw = data.cells?.[rowId]?.[colId];
1451
+ return raw === undefined || raw === null ? "" : String(raw);
1452
+ }
1453
+
1454
+ function clearRange(data, range) {
1455
+ const next = {
1456
+ ...data,
1457
+ cells: data.cells ? { ...data.cells } : {},
1458
+ formulas: data.formulas ? { ...data.formulas } : {},
1459
+ };
1460
+ for (let r = range.rowStart; r <= range.rowEnd; r += 1) {
1461
+ const rowId = data.rows[r]?.id;
1462
+ if (!rowId) continue;
1463
+ for (let c = range.colStart; c <= range.colEnd; c += 1) {
1464
+ const colId = data.columns[c]?.id;
1465
+ if (!colId) continue;
1466
+ if (next.cells[rowId]) delete next.cells[rowId][colId];
1467
+ if (next.formulas[rowId]) delete next.formulas[rowId][colId];
1468
+ }
1469
+ }
1470
+ return next;
1471
+ }
1472
+
1473
+ function getRangeFromActiveCell(data) {
1474
+ const active = getActiveCell.call(this);
1475
+ if (!active) return null;
1476
+ const { rowIndexById, colIndexById } = getIndexMaps(data);
1477
+ const rowIndex = rowIndexById.get(active.rowId);
1478
+ const colIndex = colIndexById.get(active.colId);
1479
+ if (rowIndex === undefined || colIndex === undefined) return null;
1480
+ return {
1481
+ rowStart: rowIndex,
1482
+ rowEnd: rowIndex,
1483
+ colStart: colIndex,
1484
+ colEnd: colIndex,
1485
+ rowIndexById,
1486
+ colIndexById,
1487
+ };
1488
+ }
1489
+
1490
+ function applyPasteText(data, text, startRowIndex, startColIndex) {
1491
+ const rows = normalizeClipboardRows(text);
1492
+ const next = {
1493
+ ...data,
1494
+ cells: data.cells ? { ...data.cells } : {},
1495
+ formulas: data.formulas ? { ...data.formulas } : {},
1496
+ };
1497
+ let rowEnd = startRowIndex;
1498
+ let colEnd = startColIndex;
1499
+
1500
+ rows.forEach((rowText, rowOffset) => {
1501
+ const rowIndex = startRowIndex + rowOffset;
1502
+ if (rowIndex >= data.rows.length) return;
1503
+ const rowId = data.rows[rowIndex]?.id;
1504
+ if (!rowId) return;
1505
+ const cols = rowText.split("\t");
1506
+ cols.forEach((cellText, colOffset) => {
1507
+ const colIndex = startColIndex + colOffset;
1508
+ if (colIndex >= data.columns.length) return;
1509
+ const colId = data.columns[colIndex]?.id;
1510
+ if (!colId) return;
1511
+ setCellData(next, rowId, colId, cellText);
1512
+ rowEnd = Math.max(rowEnd, rowIndex);
1513
+ colEnd = Math.max(colEnd, colIndex);
1514
+ });
1515
+ });
1516
+
1517
+ return {
1518
+ value: next,
1519
+ range: {
1520
+ rowStart: startRowIndex,
1521
+ rowEnd,
1522
+ colStart: startColIndex,
1523
+ colEnd,
1524
+ },
1525
+ };
1526
+ }
1527
+
1528
+ function applyPasteCached(data, cached, startRowIndex, startColIndex) {
1529
+ const next = {
1530
+ ...data,
1531
+ cells: data.cells ? { ...data.cells } : {},
1532
+ formulas: data.formulas ? { ...data.formulas } : {},
1533
+ };
1534
+ const deltaRow = startRowIndex - cached.range.rowStart;
1535
+ const deltaCol = startColIndex - cached.range.colStart;
1536
+ let rowEnd = startRowIndex;
1537
+ let colEnd = startColIndex;
1538
+
1539
+ cached.values.forEach((rowValues, rowOffset) => {
1540
+ const rowIndex = startRowIndex + rowOffset;
1541
+ if (rowIndex >= data.rows.length) return;
1542
+ const rowId = data.rows[rowIndex]?.id;
1543
+ if (!rowId) return;
1544
+ rowValues.forEach((cellValue, colOffset) => {
1545
+ const colIndex = startColIndex + colOffset;
1546
+ if (colIndex >= data.columns.length) return;
1547
+ const colId = data.columns[colIndex]?.id;
1548
+ if (!colId) return;
1549
+ let nextValue = cellValue;
1550
+ if (isFormulaString(cellValue)) {
1551
+ nextValue = adjustFormulaReferences(cellValue, deltaRow, deltaCol);
1552
+ }
1553
+ setCellData(next, rowId, colId, nextValue);
1554
+ rowEnd = Math.max(rowEnd, rowIndex);
1555
+ colEnd = Math.max(colEnd, colIndex);
1556
+ });
1557
+ });
1558
+
1559
+ return {
1560
+ value: next,
1561
+ range: {
1562
+ rowStart: startRowIndex,
1563
+ rowEnd,
1564
+ colStart: startColIndex,
1565
+ colEnd,
1566
+ },
1567
+ };
1568
+ }
1569
+
1570
+ function normalizeClipboardRows(text) {
1571
+ const normalized = String(text).replace(/\r\n/g, "\n").replace(/\r/g, "\n");
1572
+ const rows = normalized.split("\n");
1573
+ while (rows.length > 1 && rows[rows.length - 1] === "") {
1574
+ rows.pop();
1575
+ }
1576
+ return rows;
1577
+ }
1578
+
1579
+ function setCellData(data, rowId, colId, value) {
1580
+ if (!data.cells) data.cells = {};
1581
+ if (!data.formulas) data.formulas = {};
1582
+ if (!data.cells[rowId]) data.cells[rowId] = {};
1583
+ if (!data.formulas[rowId]) data.formulas[rowId] = {};
1584
+ const next = String(value ?? "");
1585
+ if (next.trim().startsWith("=")) {
1586
+ data.formulas[rowId][colId] = next.trim();
1587
+ delete data.cells[rowId][colId];
1588
+ } else {
1589
+ delete data.formulas[rowId][colId];
1590
+ data.cells[rowId][colId] = next;
1591
+ }
1592
+ }
1593
+
1594
+ function isFormulaString(value) {
1595
+ return isString(value) && value.trim().startsWith("=");
1596
+ }
1597
+
1598
+ function adjustFormulaReferences(formula, deltaRow, deltaCol) {
1599
+ const expr = formula.trim();
1600
+ if (!expr.startsWith("=")) return formula;
1601
+ const body = expr.slice(1);
1602
+ const adjusted = body.replace(/([A-Za-z]+)([0-9]+)/g, (match, col, row) => {
1603
+ const colIndex = columnLabelToIndex(col);
1604
+ if (colIndex === null) return match;
1605
+ const rowIndex = Number(row) - 1;
1606
+ if (!Number.isFinite(rowIndex)) return match;
1607
+ const nextCol = colIndex + deltaCol;
1608
+ const nextRow = rowIndex + deltaRow;
1609
+ if (nextCol < 0 || nextRow < 0) return match;
1610
+ return `${indexToColumnLabel(nextCol)}${nextRow + 1}`;
1611
+ });
1612
+ return `=${adjusted}`;
1613
+ }
1614
+
1615
+ function columnLabelToIndex(label) {
1616
+ const text = String(label || "").toUpperCase();
1617
+ if (!/^[A-Z]+$/.test(text)) return null;
1618
+ let index = 0;
1619
+ for (let i = 0; i < text.length; i += 1) {
1620
+ index = index * 26 + (text.charCodeAt(i) - 64);
1621
+ }
1622
+ return index - 1;
1623
+ }
1624
+
1625
+ function indexToColumnLabel(index) {
1626
+ if (!Number.isFinite(index) || index < 0) return "A";
1627
+ return nextColumnLabel(index);
1628
+ }
1629
+
1630
+ function setSelectionByRange(range, data) {
1631
+ if (!range) return;
1632
+ const rows = data.rows;
1633
+ const cols = data.columns;
1634
+ if (
1635
+ range.rowStart < 0 ||
1636
+ range.colStart < 0 ||
1637
+ range.rowEnd >= rows.length ||
1638
+ range.colEnd >= cols.length
1639
+ ) {
1640
+ return;
1641
+ }
1642
+ this[selectionSymbol] = {
1643
+ anchor: {
1644
+ rowId: rows[range.rowStart].id,
1645
+ colId: cols[range.colStart].id,
1646
+ },
1647
+ focus: {
1648
+ rowId: rows[range.rowEnd].id,
1649
+ colId: cols[range.colEnd].id,
1650
+ },
1651
+ };
1652
+ updateSelectionDisplay.call(this);
1653
+ }
1654
+
1655
+ async function writeClipboardText(text) {
1656
+ try {
1657
+ if (navigator.clipboard?.writeText) {
1658
+ await navigator.clipboard.writeText(text);
1659
+ return true;
1660
+ }
1661
+ } catch (e) {
1662
+ return false;
1663
+ }
1664
+ return false;
1665
+ }
1666
+
1667
+ async function readClipboardText() {
1668
+ try {
1669
+ if (navigator.clipboard?.readText) {
1670
+ return await navigator.clipboard.readText();
1671
+ }
1672
+ } catch (e) {
1673
+ return null;
1674
+ }
1675
+ return null;
1676
+ }
1677
+
1678
+ function showStatus(message) {
1679
+ const status = this[statusSymbol];
1680
+ if (!status) return;
1681
+ status.textContent = message;
1682
+ status.dataset.show = "true";
1683
+ if (this[statusTimeoutSymbol]) {
1684
+ clearTimeout(this[statusTimeoutSymbol]);
1685
+ }
1686
+ this[statusTimeoutSymbol] = setTimeout(() => {
1687
+ status.dataset.show = "false";
1688
+ }, 1200);
1689
+ }
1690
+
1691
+ function syncContextMenuLabels() {
1692
+ const menu = this[contextMenuSymbol];
1693
+ if (!menu) return;
1694
+ const labels = this.getOption("labels", {});
1695
+ const copy = menu.querySelector(`[${ATTRIBUTE_ROLE}=menu-copy]`);
1696
+ const paste = menu.querySelector(`[${ATTRIBUTE_ROLE}=menu-paste]`);
1697
+ const cut = menu.querySelector(`[${ATTRIBUTE_ROLE}=menu-cut]`);
1698
+ if (copy && isString(labels.copy)) copy.textContent = labels.copy;
1699
+ if (paste && isString(labels.paste)) paste.textContent = labels.paste;
1700
+ if (cut && isString(labels.cut)) cut.textContent = labels.cut;
1701
+ }
1702
+
1703
+ function wireContextMenuActions() {
1704
+ const menu = this[contextMenuSymbol];
1705
+ if (!menu) return;
1706
+ const copy = menu.querySelector(`[${ATTRIBUTE_ROLE}=menu-copy]`);
1707
+ const paste = menu.querySelector(`[${ATTRIBUTE_ROLE}=menu-paste]`);
1708
+ const cut = menu.querySelector(`[${ATTRIBUTE_ROLE}=menu-cut]`);
1709
+ const labels = this.getOption("labels", {});
1710
+ if (copy) copy.textContent = labels.copy || "Copy";
1711
+ if (paste) paste.textContent = labels.paste || "Paste";
1712
+ if (cut) cut.textContent = labels.cut || "Cut";
1713
+
1714
+ if (copy) {
1715
+ copy.addEventListener("click", async () => {
1716
+ await copySelectionToClipboard.call(this);
1717
+ hideContextMenu.call(this);
1718
+ });
1719
+ }
1720
+ if (paste) {
1721
+ paste.addEventListener("click", async () => {
1722
+ await pasteFromClipboard.call(this);
1723
+ hideContextMenu.call(this);
1724
+ });
1725
+ }
1726
+ if (cut) {
1727
+ cut.addEventListener("click", async () => {
1728
+ await cutSelectionToClipboard.call(this);
1729
+ hideContextMenu.call(this);
1730
+ });
1731
+ }
1732
+ }
1733
+
1734
+ function showContextMenu(clientX, clientY) {
1735
+ const menu = this[contextMenuSymbol];
1736
+ const wrapper = this[gridWrapperSymbol];
1737
+ if (!menu || !wrapper) return;
1738
+ menu.hidden = false;
1739
+ menu.style.display = "block";
1740
+ const wrapperRect = wrapper.getBoundingClientRect();
1741
+ const offsetX = clientX - wrapperRect.left + wrapper.scrollLeft;
1742
+ const offsetY = clientY - wrapperRect.top + wrapper.scrollTop;
1743
+ const menuWidth = menu.offsetWidth || 160;
1744
+ const menuHeight = menu.offsetHeight || 96;
1745
+ const maxLeft = wrapper.scrollLeft + wrapper.clientWidth - menuWidth;
1746
+ const maxTop = wrapper.scrollTop + wrapper.clientHeight - menuHeight;
1747
+ menu.style.left = `${Math.max(wrapper.scrollLeft, Math.min(offsetX, maxLeft))}px`;
1748
+ menu.style.top = `${Math.max(wrapper.scrollTop, Math.min(offsetY, maxTop))}px`;
1749
+ this[menuStateSymbol] = { open: true };
1750
+ }
1751
+
1752
+ function hideContextMenu() {
1753
+ const menu = this[contextMenuSymbol];
1754
+ if (!menu || menu.hidden) return;
1755
+ menu.hidden = true;
1756
+ menu.style.display = "none";
1757
+ this[menuStateSymbol] = { open: false };
1758
+ }
1759
+
1760
+ function startFillDrag(event) {
1761
+ if (event.button !== 0) return;
1762
+ const data = normalizeValue.call(this);
1763
+ const range = getSelectionRange.call(this, this[selectionSymbol], data);
1764
+ if (!range) return;
1765
+ event.preventDefault();
1766
+ event.stopPropagation();
1767
+ if (event.target?.setPointerCapture) {
1768
+ event.target.setPointerCapture(event.pointerId);
1769
+ }
1770
+ const state = {
1771
+ origin: {
1772
+ rowStart: range.rowStart,
1773
+ rowEnd: range.rowEnd,
1774
+ colStart: range.colStart,
1775
+ colEnd: range.colEnd,
1776
+ },
1777
+ current: null,
1778
+ startX: event.clientX,
1779
+ startY: event.clientY,
1780
+ moved: false,
1781
+ };
1782
+ this[dragFillSymbol] = state;
1783
+ this.setAttribute("data-filling", "");
1784
+
1785
+ const move = (moveEvent) => {
1786
+ handleFillMove.call(this, moveEvent);
1787
+ };
1788
+ const end = (endEvent) => {
1789
+ handleFillEnd.call(this, endEvent);
1790
+ };
1791
+ window.addEventListener("pointermove", move);
1792
+ window.addEventListener("pointerup", end, { once: true });
1793
+ window.addEventListener("pointercancel", end, { once: true });
1794
+ state.cleanup = () => {
1795
+ window.removeEventListener("pointermove", move);
1796
+ };
1797
+ }
1798
+
1799
+ function handleFillMove(event) {
1800
+ const state = this[dragFillSymbol];
1801
+ if (!state) return;
1802
+ if (!state.moved) {
1803
+ const dx = event.clientX - state.startX;
1804
+ const dy = event.clientY - state.startY;
1805
+ if (Math.abs(dx) < 3 && Math.abs(dy) < 3) return;
1806
+ state.moved = true;
1807
+ }
1808
+ const data = normalizeValue.call(this);
1809
+ const cell = getCellFromPoint.call(this, event.clientX, event.clientY);
1810
+ if (!cell) return;
1811
+ const rowId = cell.dataset.rowId;
1812
+ const colId = cell.dataset.colId;
1813
+ if (!rowId || !colId) return;
1814
+ const { rowIndexById, colIndexById } = getIndexMaps(data);
1815
+ const rowIndex = rowIndexById.get(rowId);
1816
+ const colIndex = colIndexById.get(colId);
1817
+ if (rowIndex === undefined || colIndex === undefined) return;
1818
+
1819
+ const nextRange = {
1820
+ rowStart: Math.min(state.origin.rowStart, rowIndex),
1821
+ rowEnd: Math.max(state.origin.rowEnd, rowIndex),
1822
+ colStart: Math.min(state.origin.colStart, colIndex),
1823
+ colEnd: Math.max(state.origin.colEnd, colIndex),
1824
+ };
1825
+ state.current = nextRange;
1826
+ setSelectionByRange.call(this, nextRange, data);
1827
+ }
1828
+
1829
+ function handleFillEnd() {
1830
+ const state = this[dragFillSymbol];
1831
+ if (!state) return;
1832
+ if (state.cleanup) state.cleanup();
1833
+ this.removeAttribute("data-filling");
1834
+ const data = normalizeValue.call(this);
1835
+ const target = state.current;
1836
+ const origin = state.origin;
1837
+ this[dragFillSymbol] = null;
1838
+ if (!state.moved || !target) return;
1839
+ const next = applyFillRange(data, origin, target);
1840
+ this.value = next;
1841
+ setSelectionByRange.call(this, target, next);
1842
+ }
1843
+
1844
+ function getCellFromPoint(clientX, clientY) {
1845
+ if (!this.shadowRoot) return null;
1846
+ const elements = document.elementsFromPoint(clientX, clientY);
1847
+ for (const el of elements) {
1848
+ if (!this.shadowRoot.contains(el)) continue;
1849
+ const cell = el.closest(`[${ATTRIBUTE_ROLE}=cell]`);
1850
+ if (cell) return cell;
1851
+ }
1852
+ const virtualize = this.getOption("features.virtualize", false) === true;
1853
+ if (!virtualize) return null;
1854
+ const wrapper = this[gridWrapperSymbol];
1855
+ if (!wrapper) return null;
1856
+ const rect = wrapper.getBoundingClientRect();
1857
+ const data = normalizeValue.call(this);
1858
+ const sizes = getVirtualSizes.call(this, data);
1859
+ const x = clientX - rect.left + wrapper.scrollLeft - sizes.rowHeaderWidth;
1860
+ const y = clientY - rect.top + wrapper.scrollTop - sizes.headerHeight;
1861
+ if (x < 0 || y < 0) return null;
1862
+ const colIndex = findStartIndex(sizes.columnOffsets, x);
1863
+ const rowIndex = findStartIndex(sizes.rowOffsets, y);
1864
+ if (
1865
+ colIndex < 0 ||
1866
+ rowIndex < 0 ||
1867
+ colIndex >= data.columns.length ||
1868
+ rowIndex >= data.rows.length
1869
+ ) {
1870
+ return null;
1871
+ }
1872
+ return {
1873
+ dataset: {
1874
+ rowId: data.rows[rowIndex].id,
1875
+ colId: data.columns[colIndex].id,
1876
+ },
1877
+ };
1878
+ }
1879
+
1880
+ function applyFillRange(data, origin, target) {
1881
+ const next = {
1882
+ ...data,
1883
+ cells: data.cells ? { ...data.cells } : {},
1884
+ formulas: data.formulas ? { ...data.formulas } : {},
1885
+ };
1886
+ const originRows = origin.rowEnd - origin.rowStart + 1;
1887
+ const originCols = origin.colEnd - origin.colStart + 1;
1888
+ for (let r = target.rowStart; r <= target.rowEnd; r += 1) {
1889
+ const rowId = data.rows[r]?.id;
1890
+ if (!rowId) continue;
1891
+ for (let c = target.colStart; c <= target.colEnd; c += 1) {
1892
+ if (
1893
+ r >= origin.rowStart &&
1894
+ r <= origin.rowEnd &&
1895
+ c >= origin.colStart &&
1896
+ c <= origin.colEnd
1897
+ ) {
1898
+ continue;
1899
+ }
1900
+ const colId = data.columns[c]?.id;
1901
+ if (!colId) continue;
1902
+ const sourceRow = origin.rowStart + ((r - origin.rowStart) % originRows);
1903
+ const sourceCol = origin.colStart + ((c - origin.colStart) % originCols);
1904
+ const sourceRowId = data.rows[sourceRow]?.id;
1905
+ const sourceColId = data.columns[sourceCol]?.id;
1906
+ if (!sourceRowId || !sourceColId) continue;
1907
+ let value = getCellRawValue(data, sourceRowId, sourceColId);
1908
+ if (isFormulaString(value)) {
1909
+ const deltaRow = r - sourceRow;
1910
+ const deltaCol = c - sourceCol;
1911
+ value = adjustFormulaReferences(value, deltaRow, deltaCol);
1912
+ }
1913
+ setCellData(next, rowId, colId, value);
1914
+ }
1915
+ }
1916
+ return next;
1917
+ }
1918
+
1919
+ function scheduleVirtualResizeRender() {
1920
+ if (this[resizeFrameSymbol]) return;
1921
+ this[resizeFrameSymbol] = requestAnimationFrame(() => {
1922
+ this[resizeFrameSymbol] = null;
1923
+ this[forceRenderSymbol] = true;
1924
+ updateControl.call(this);
1925
+ });
1926
+ }
1927
+
1928
+ function getCellDisplayValue(data, rowId, colId) {
1929
+ const formula = data.formulas?.[rowId]?.[colId];
1930
+ if (isString(formula)) {
1931
+ const evaluated = evaluateFormula.call(this, data, formula);
1932
+ return Number.isFinite(evaluated) ? evaluated : "#ERR";
1933
+ }
1934
+ const raw = data.cells?.[rowId]?.[colId];
1935
+ return raw === undefined || raw === null ? "" : raw;
1936
+ }
1937
+
1938
+ function buildDisplayCells(data) {
1939
+ const result = clone(data.cells);
1940
+ const errors = [];
1941
+
1942
+ for (const rowId of Object.keys(data.formulas || {})) {
1943
+ for (const colId of Object.keys(data.formulas[rowId] || {})) {
1944
+ const formula = data.formulas[rowId][colId];
1945
+ if (!isString(formula)) continue;
1946
+ const value = evaluateFormula.call(this, data, formula);
1947
+ if (!result[rowId]) result[rowId] = {};
1948
+ if (Number.isFinite(value)) {
1949
+ result[rowId][colId] = value;
1950
+ } else {
1951
+ result[rowId][colId] = "#ERR";
1952
+ errors.push({ rowId, colId, formula });
1953
+ }
1954
+ }
1955
+ }
1956
+
1957
+ if (errors.length > 0) {
1958
+ fireCustomEvent(this, "monster-sheet-formula-error", { errors });
1959
+ }
1960
+
1961
+ return result;
1962
+ }
1963
+
1964
+ function evaluateFormula(data, formula, stack = []) {
1965
+ const expr = formula.trim().startsWith("=")
1966
+ ? formula.trim().slice(1)
1967
+ : formula.trim();
1968
+ if (expr === "") return NaN;
1969
+ try {
1970
+ const tokens = tokenizeFormula(expr);
1971
+ const rpn = toRpn(tokens);
1972
+ const value = evalRpn.call(this, data, rpn, stack);
1973
+ return Number.isFinite(value) ? value : NaN;
1974
+ } catch (e) {
1975
+ return NaN;
1976
+ }
1977
+ }
1978
+
1979
+ function tokenizeFormula(expr) {
1980
+ const tokens = [];
1981
+ const pattern = /\s*([A-Za-z]+[0-9]+|\d+(?:\.\d+)?|[()+\-*/])\s*/g;
1982
+ let match;
1983
+ while ((match = pattern.exec(expr)) !== null) {
1984
+ tokens.push(match[1]);
1985
+ }
1986
+ return tokens;
1987
+ }
1988
+
1989
+ function toRpn(tokens) {
1990
+ const output = [];
1991
+ const ops = [];
1992
+ const precedence = { "+": 1, "-": 1, "*": 2, "/": 2 };
1993
+
1994
+ for (const token of tokens) {
1995
+ if (isNumberToken(token) || isRefToken(token)) {
1996
+ output.push(token);
1997
+ continue;
1998
+ }
1999
+
2000
+ if (token === "(") {
2001
+ ops.push(token);
2002
+ continue;
2003
+ }
2004
+ if (token === ")") {
2005
+ while (ops.length > 0 && ops[ops.length - 1] !== "(") {
2006
+ output.push(ops.pop());
2007
+ }
2008
+ ops.pop();
2009
+ continue;
2010
+ }
2011
+
2012
+ while (
2013
+ ops.length > 0 &&
2014
+ ops[ops.length - 1] !== "(" &&
2015
+ precedence[ops[ops.length - 1]] >= precedence[token]
2016
+ ) {
2017
+ output.push(ops.pop());
2018
+ }
2019
+ ops.push(token);
2020
+ }
2021
+
2022
+ while (ops.length > 0) {
2023
+ output.push(ops.pop());
2024
+ }
2025
+
2026
+ return output;
2027
+ }
2028
+
2029
+ function evalRpn(data, rpn, stack) {
2030
+ const s = [];
2031
+ for (const token of rpn) {
2032
+ if (isNumberToken(token)) {
2033
+ s.push(Number(token));
2034
+ continue;
2035
+ }
2036
+ if (isRefToken(token)) {
2037
+ const { rowId, colId } = parseRefToken(token);
2038
+ const value = getCellNumericValue.call(this, data, rowId, colId, stack);
2039
+ s.push(value);
2040
+ continue;
2041
+ }
2042
+ const b = s.pop();
2043
+ const a = s.pop();
2044
+ if (!Number.isFinite(a) || !Number.isFinite(b)) return NaN;
2045
+ switch (token) {
2046
+ case "+":
2047
+ s.push(a + b);
2048
+ break;
2049
+ case "-":
2050
+ s.push(a - b);
2051
+ break;
2052
+ case "*":
2053
+ s.push(a * b);
2054
+ break;
2055
+ case "/":
2056
+ s.push(b === 0 ? NaN : a / b);
2057
+ break;
2058
+ default:
2059
+ return NaN;
2060
+ }
2061
+ }
2062
+ return s.length === 1 ? s[0] : NaN;
2063
+ }
2064
+
2065
+ function getCellNumericValue(data, rowId, colId, stack) {
2066
+ const key = `${rowId}:${colId}`;
2067
+ if (stack.includes(key)) return NaN;
2068
+
2069
+ const formula = data.formulas?.[rowId]?.[colId];
2070
+ if (isString(formula)) {
2071
+ const nextStack = [...stack, key];
2072
+ return evaluateFormula.call(this, data, formula, nextStack);
2073
+ }
2074
+
2075
+ const raw = data.cells?.[rowId]?.[colId];
2076
+ const number = Number(raw);
2077
+ return Number.isFinite(number) ? number : NaN;
2078
+ }
2079
+
2080
+ function isNumberToken(token) {
2081
+ return /^-?\d+(?:\.\d+)?$/.test(token);
2082
+ }
2083
+
2084
+ function isRefToken(token) {
2085
+ return /^[A-Za-z]+[0-9]+$/.test(token);
2086
+ }
2087
+
2088
+ function parseRefToken(token) {
2089
+ const match = /^([A-Za-z]+)([0-9]+)$/.exec(token);
2090
+ return { colId: match[1].toUpperCase(), rowId: match[2] };
2091
+ }
2092
+
2093
+ function getTranslations() {
2094
+ const locale = getLocaleOfDocument();
2095
+ switch (locale.language) {
2096
+ case "de":
2097
+ return {
2098
+ addRow: "Zeile hinzufügen",
2099
+ addColumn: "Spalte hinzufügen",
2100
+ corner: "",
2101
+ copy: "Kopieren",
2102
+ paste: "Einfügen",
2103
+ cut: "Ausschneiden",
2104
+ copied: "Kopiert",
2105
+ pasted: "Eingefügt",
2106
+ cutDone: "Ausgeschnitten",
2107
+ };
2108
+ case "fr":
2109
+ return {
2110
+ addRow: "Ajouter une ligne",
2111
+ addColumn: "Ajouter une colonne",
2112
+ corner: "",
2113
+ copy: "Copier",
2114
+ paste: "Coller",
2115
+ cut: "Couper",
2116
+ copied: "Copié",
2117
+ pasted: "Collé",
2118
+ cutDone: "Coupé",
2119
+ };
2120
+ case "es":
2121
+ return {
2122
+ addRow: "Agregar fila",
2123
+ addColumn: "Agregar columna",
2124
+ corner: "",
2125
+ copy: "Copiar",
2126
+ paste: "Pegar",
2127
+ cut: "Cortar",
2128
+ copied: "Copiado",
2129
+ pasted: "Pegado",
2130
+ cutDone: "Cortado",
2131
+ };
2132
+ case "zh":
2133
+ return {
2134
+ addRow: "添加行",
2135
+ addColumn: "添加列",
2136
+ corner: "",
2137
+ copy: "复制",
2138
+ paste: "粘贴",
2139
+ cut: "剪切",
2140
+ copied: "已复制",
2141
+ pasted: "已粘贴",
2142
+ cutDone: "已剪切",
2143
+ };
2144
+ case "hi":
2145
+ return {
2146
+ addRow: "पंक्ति जोड़ें",
2147
+ addColumn: "स्तंभ जोड़ें",
2148
+ corner: "",
2149
+ copy: "कॉपी करें",
2150
+ paste: "पेस्ट करें",
2151
+ cut: "कट करें",
2152
+ copied: "कॉपी हो गया",
2153
+ pasted: "पेस्ट हो गया",
2154
+ cutDone: "कट हो गया",
2155
+ };
2156
+ case "bn":
2157
+ return {
2158
+ addRow: "সারি যোগ করুন",
2159
+ addColumn: "কলাম যোগ করুন",
2160
+ corner: "",
2161
+ copy: "কপি",
2162
+ paste: "পেস্ট",
2163
+ cut: "কাট",
2164
+ copied: "কপি করা হয়েছে",
2165
+ pasted: "পেস্ট করা হয়েছে",
2166
+ cutDone: "কাট করা হয়েছে",
2167
+ };
2168
+ case "pt":
2169
+ return {
2170
+ addRow: "Adicionar linha",
2171
+ addColumn: "Adicionar coluna",
2172
+ corner: "",
2173
+ copy: "Copiar",
2174
+ paste: "Colar",
2175
+ cut: "Recortar",
2176
+ copied: "Copiado",
2177
+ pasted: "Colado",
2178
+ cutDone: "Recortado",
2179
+ };
2180
+ case "ru":
2181
+ return {
2182
+ addRow: "Добавить строку",
2183
+ addColumn: "Добавить столбец",
2184
+ corner: "",
2185
+ copy: "Копировать",
2186
+ paste: "Вставить",
2187
+ cut: "Вырезать",
2188
+ copied: "Скопировано",
2189
+ pasted: "Вставлено",
2190
+ cutDone: "Вырезано",
2191
+ };
2192
+ case "ja":
2193
+ return {
2194
+ addRow: "行を追加",
2195
+ addColumn: "列を追加",
2196
+ corner: "",
2197
+ copy: "コピー",
2198
+ paste: "貼り付け",
2199
+ cut: "切り取り",
2200
+ copied: "コピーしました",
2201
+ pasted: "貼り付けました",
2202
+ cutDone: "切り取りました",
2203
+ };
2204
+ case "pa":
2205
+ return {
2206
+ addRow: "ਕਤਾਰ ਸ਼ਾਮਲ ਕਰੋ",
2207
+ addColumn: "ਕਾਲਮ ਸ਼ਾਮਲ ਕਰੋ",
2208
+ corner: "",
2209
+ copy: "ਕਾਪੀ",
2210
+ paste: "ਪੇਸਟ",
2211
+ cut: "ਕੱਟੋ",
2212
+ copied: "ਕਾਪੀ ਹੋਇਆ",
2213
+ pasted: "ਪੇਸਟ ਹੋਇਆ",
2214
+ cutDone: "ਕੱਟਿਆ ਗਿਆ",
2215
+ };
2216
+ case "mr":
2217
+ return {
2218
+ addRow: "ओळ जोडा",
2219
+ addColumn: "स्तंभ जोडा",
2220
+ corner: "",
2221
+ copy: "कॉपी",
2222
+ paste: "पेस्ट",
2223
+ cut: "कट",
2224
+ copied: "कॉपी झाले",
2225
+ pasted: "पेस्ट झाले",
2226
+ cutDone: "कट झाले",
2227
+ };
2228
+ case "it":
2229
+ return {
2230
+ addRow: "Aggiungi riga",
2231
+ addColumn: "Aggiungi colonna",
2232
+ corner: "",
2233
+ copy: "Copia",
2234
+ paste: "Incolla",
2235
+ cut: "Taglia",
2236
+ copied: "Copiato",
2237
+ pasted: "Incollato",
2238
+ cutDone: "Tagliato",
2239
+ };
2240
+ case "nl":
2241
+ return {
2242
+ addRow: "Rij toevoegen",
2243
+ addColumn: "Kolom toevoegen",
2244
+ corner: "",
2245
+ copy: "Kopiëren",
2246
+ paste: "Plakken",
2247
+ cut: "Knippen",
2248
+ copied: "Gekopieerd",
2249
+ pasted: "Geplakt",
2250
+ cutDone: "Geknipt",
2251
+ };
2252
+ case "sv":
2253
+ return {
2254
+ addRow: "Lägg till rad",
2255
+ addColumn: "Lägg till kolumn",
2256
+ corner: "",
2257
+ copy: "Kopiera",
2258
+ paste: "Klistra in",
2259
+ cut: "Klipp ut",
2260
+ copied: "Kopierad",
2261
+ pasted: "Inklistrad",
2262
+ cutDone: "Urklippt",
2263
+ };
2264
+ case "pl":
2265
+ return {
2266
+ addRow: "Dodaj wiersz",
2267
+ addColumn: "Dodaj kolumnę",
2268
+ corner: "",
2269
+ copy: "Kopiuj",
2270
+ paste: "Wklej",
2271
+ cut: "Wytnij",
2272
+ copied: "Skopiowano",
2273
+ pasted: "Wklejono",
2274
+ cutDone: "Wycięto",
2275
+ };
2276
+ case "da":
2277
+ return {
2278
+ addRow: "Tilføj række",
2279
+ addColumn: "Tilføj kolonne",
2280
+ corner: "",
2281
+ copy: "Kopier",
2282
+ paste: "Indsæt",
2283
+ cut: "Klip",
2284
+ copied: "Kopieret",
2285
+ pasted: "Indsat",
2286
+ cutDone: "Klippet",
2287
+ };
2288
+ case "fi":
2289
+ return {
2290
+ addRow: "Lisää rivi",
2291
+ addColumn: "Lisää sarake",
2292
+ corner: "",
2293
+ copy: "Kopioi",
2294
+ paste: "Liitä",
2295
+ cut: "Leikkaa",
2296
+ copied: "Kopioitu",
2297
+ pasted: "Liitetty",
2298
+ cutDone: "Leikattu",
2299
+ };
2300
+ case "no":
2301
+ return {
2302
+ addRow: "Legg til rad",
2303
+ addColumn: "Legg til kolonne",
2304
+ corner: "",
2305
+ copy: "Kopier",
2306
+ paste: "Lim inn",
2307
+ cut: "Klipp ut",
2308
+ copied: "Kopiert",
2309
+ pasted: "Limt inn",
2310
+ cutDone: "Klippet",
2311
+ };
2312
+ case "cs":
2313
+ return {
2314
+ addRow: "Přidat řádek",
2315
+ addColumn: "Přidat sloupec",
2316
+ corner: "",
2317
+ copy: "Kopírovat",
2318
+ paste: "Vložit",
2319
+ cut: "Vyjmout",
2320
+ copied: "Zkopírováno",
2321
+ pasted: "Vloženo",
2322
+ cutDone: "Vyjmuto",
2323
+ };
2324
+ default:
2325
+ return {
2326
+ addRow: "Add row",
2327
+ addColumn: "Add column",
2328
+ corner: "",
2329
+ copy: "Copy",
2330
+ paste: "Paste",
2331
+ cut: "Cut",
2332
+ copied: "Copied",
2333
+ pasted: "Pasted",
2334
+ cutDone: "Cut",
2335
+ };
2336
+ }
2337
+ }
2338
+
2339
+ function getTemplate() {
2340
+ return `
2341
+ <div data-monster-role="control" part="control">
2342
+ <div data-monster-role="toolbar" part="toolbar">
2343
+ <button type="button" data-monster-role="add-row" part="add-row"></button>
2344
+ <button type="button" data-monster-role="add-column" part="add-column"></button>
2345
+ </div>
2346
+ <div data-monster-role="grid-wrapper" part="grid-wrapper">
2347
+ <div data-monster-role="grid" part="grid"></div>
2348
+ <div data-monster-role="selection" part="selection" aria-hidden="true"></div>
2349
+ <span data-monster-role="fill-handle" part="fill-handle" aria-hidden="true"></span>
2350
+ <div data-monster-role="status" part="status" role="status" aria-live="polite"></div>
2351
+ <div data-monster-role="context-menu" part="context-menu" role="menu" hidden>
2352
+ <button type="button" data-monster-role="menu-copy" part="menu-item" role="menuitem"></button>
2353
+ <button type="button" data-monster-role="menu-paste" part="menu-item" role="menuitem"></button>
2354
+ <button type="button" data-monster-role="menu-cut" part="menu-item" role="menuitem"></button>
2355
+ </div>
2356
+ </div>
2357
+ </div>
2358
+ `;
2359
+ }
2360
+
2361
+ registerCustomElement(Sheet);