@iyulab/data-components 0.1.8 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,935 @@
1
- import { USimpleSheet } from './USimpleSheet.component.js';
2
-
3
- USimpleSheet.define("u-simple-sheet");
4
-
1
+ import { __decorateMetadata } from "../../_virtual/_@oxc-project_runtime@0.122.0/helpers/decorateMetadata.js";
2
+ import { __decorate } from "../../_virtual/_@oxc-project_runtime@0.122.0/helpers/decorate.js";
3
+ import { styles } from "./USimpleSheet.styles.js";
4
+ import { html } from "lit";
5
+ import { customElement, property, state } from "lit/decorators.js";
6
+ import { UElement } from "@iyulab/components/dist/components/UElement.js";
7
+ import { ref } from "lit/directives/ref.js";
8
+ //#region src/components/simple-sheet/USimpleSheet.ts
9
+ var _USimpleSheet;
10
+ function normalizeRange(sel) {
11
+ return {
12
+ minRow: Math.min(sel.anchor.row, sel.focus.row),
13
+ maxRow: Math.max(sel.anchor.row, sel.focus.row),
14
+ minCol: Math.min(sel.anchor.col, sel.focus.col),
15
+ maxCol: Math.max(sel.anchor.col, sel.focus.col)
16
+ };
17
+ }
18
+ var USimpleSheet = class USimpleSheet extends UElement {
19
+ static {
20
+ _USimpleSheet = this;
21
+ }
22
+ constructor(..._args) {
23
+ super(..._args);
24
+ this.data = [];
25
+ this.rows = 20;
26
+ this.cols = 10;
27
+ this.readonly = false;
28
+ this._sel = null;
29
+ this._editing = null;
30
+ this._editVal = "";
31
+ this._replaceOnEdit = false;
32
+ this._dropdownItems = [];
33
+ this._dropdownIndex = -1;
34
+ this._isDropdownClick = false;
35
+ this._data = [];
36
+ this._colWidths = [];
37
+ this._isMouseSelecting = false;
38
+ this._containerEl = null;
39
+ this._resizing = null;
40
+ this._history = [];
41
+ this._historyIndex = -1;
42
+ this._refContainer = (el) => {
43
+ this._containerEl = el ?? null;
44
+ };
45
+ this._onContainerFocus = () => {
46
+ if (!this._sel) this._select(0, 0);
47
+ };
48
+ this._onContainerKeyDown = (e) => {
49
+ if (this._editing) return;
50
+ const anchor = this._sel?.anchor ?? {
51
+ row: 0,
52
+ col: 0
53
+ };
54
+ if (e.ctrlKey || e.metaKey) {
55
+ if (e.key === "z" || e.key === "Z") {
56
+ e.preventDefault();
57
+ if (!this.readonly) if (e.shiftKey) this._redo();
58
+ else this._undo();
59
+ return;
60
+ }
61
+ if (e.key === "y" || e.key === "Y") {
62
+ e.preventDefault();
63
+ if (!this.readonly) this._redo();
64
+ return;
65
+ }
66
+ if (e.key === "a" || e.key === "A") {
67
+ e.preventDefault();
68
+ this._sel = {
69
+ anchor: {
70
+ row: 0,
71
+ col: 0
72
+ },
73
+ focus: {
74
+ row: this._rowCount - 1,
75
+ col: this._colCount - 1
76
+ }
77
+ };
78
+ this.requestUpdate();
79
+ return;
80
+ }
81
+ if (e.key === "d" || e.key === "D") {
82
+ e.preventDefault();
83
+ if (!this.readonly && this._sel) this._fillDown();
84
+ return;
85
+ }
86
+ if (e.key === "r" || e.key === "R") {
87
+ e.preventDefault();
88
+ if (!this.readonly && this._sel) this._fillRight();
89
+ return;
90
+ }
91
+ }
92
+ if (e.key === "Delete" || e.key === "Backspace") {
93
+ e.preventDefault();
94
+ if (!this.readonly) this._clearSelection();
95
+ return;
96
+ }
97
+ if (e.key === "F2") {
98
+ e.preventDefault();
99
+ if (!this.readonly && !this._isColReadonly(anchor.col)) this._startEdit(anchor.row, anchor.col);
100
+ return;
101
+ }
102
+ if (e.key === "Enter") {
103
+ e.preventDefault();
104
+ if (e.shiftKey) this._select(Math.max(0, anchor.row - 1), anchor.col);
105
+ else this._select(Math.min(this._rowCount - 1, anchor.row + 1), anchor.col);
106
+ return;
107
+ }
108
+ if (e.key === "Tab") {
109
+ e.preventDefault();
110
+ if (e.shiftKey) if (anchor.col === 0 && anchor.row > 0) this._select(anchor.row - 1, this._colCount - 1);
111
+ else this._select(anchor.row, Math.max(0, anchor.col - 1));
112
+ else if (anchor.col === this._colCount - 1) {
113
+ if (anchor.row < this._rowCount - 1) this._select(anchor.row + 1, 0);
114
+ } else this._select(anchor.row, anchor.col + 1);
115
+ return;
116
+ }
117
+ const arrowMoves = {
118
+ ArrowUp: [-1, 0],
119
+ ArrowDown: [1, 0],
120
+ ArrowLeft: [0, -1],
121
+ ArrowRight: [0, 1]
122
+ };
123
+ if (arrowMoves[e.key]) {
124
+ e.preventDefault();
125
+ const [dr, dc] = arrowMoves[e.key];
126
+ if (e.shiftKey && this._sel) {
127
+ const curFocus = this._sel.focus;
128
+ const nextFocus = {
129
+ row: Math.max(0, Math.min(this._rowCount - 1, curFocus.row + dr)),
130
+ col: Math.max(0, Math.min(this._colCount - 1, curFocus.col + dc))
131
+ };
132
+ this._sel = {
133
+ anchor: this._sel.anchor,
134
+ focus: nextFocus
135
+ };
136
+ this.requestUpdate();
137
+ } else {
138
+ const newRow = Math.max(0, Math.min(this._rowCount - 1, anchor.row + dr));
139
+ const newCol = Math.max(0, Math.min(this._colCount - 1, anchor.col + dc));
140
+ this._select(newRow, newCol);
141
+ }
142
+ return;
143
+ }
144
+ if (e.key === "Home") {
145
+ e.preventDefault();
146
+ if (e.ctrlKey) this._select(0, 0);
147
+ else this._select(anchor.row, 0);
148
+ return;
149
+ }
150
+ if (e.key === "End") {
151
+ e.preventDefault();
152
+ if (e.ctrlKey) this._select(this._rowCount - 1, this._colCount - 1);
153
+ else this._select(anchor.row, this._colCount - 1);
154
+ return;
155
+ }
156
+ if (e.key === "PageUp") {
157
+ e.preventDefault();
158
+ this._select(Math.max(0, anchor.row - 10), anchor.col);
159
+ return;
160
+ }
161
+ if (e.key === "PageDown") {
162
+ e.preventDefault();
163
+ this._select(Math.min(this._rowCount - 1, anchor.row + 10), anchor.col);
164
+ return;
165
+ }
166
+ if (!this.readonly && !this._isColReadonly(anchor.col) && e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
167
+ e.preventDefault();
168
+ this._startEdit(anchor.row, anchor.col, e.key);
169
+ }
170
+ };
171
+ this._onInputChange = (e) => {
172
+ this._editVal = e.target.value;
173
+ if (this._editing) {
174
+ const options = this._getColOptions(this._editing.row, this._editing.col);
175
+ if (options) {
176
+ const filtered = this._filterOptions(options, this._editVal);
177
+ this._dropdownItems = filtered;
178
+ this._dropdownIndex = filtered.length > 0 ? 0 : -1;
179
+ }
180
+ }
181
+ };
182
+ this._onInputKeyDown = (e) => {
183
+ e.stopPropagation();
184
+ const hasDropdown = this._dropdownItems.length > 0;
185
+ if (e.key === "Escape") {
186
+ e.preventDefault();
187
+ this._cancelEdit();
188
+ return;
189
+ }
190
+ if (hasDropdown && (e.key === "ArrowDown" || e.key === "ArrowUp")) {
191
+ e.preventDefault();
192
+ const len = this._dropdownItems.length;
193
+ if (e.key === "ArrowDown") this._dropdownIndex = (this._dropdownIndex + 1) % len;
194
+ else this._dropdownIndex = (this._dropdownIndex - 1 + len) % len;
195
+ this.updateComplete.then(() => {
196
+ const dropdown = this.renderRoot.querySelector(".cell-dropdown");
197
+ const highlighted = this.renderRoot.querySelector(".dropdown-item.highlighted");
198
+ if (dropdown && highlighted) {
199
+ const dRect = dropdown.getBoundingClientRect();
200
+ const hRect = highlighted.getBoundingClientRect();
201
+ if (hRect.bottom > dRect.bottom) dropdown.scrollTop += hRect.bottom - dRect.bottom;
202
+ else if (hRect.top < dRect.top) dropdown.scrollTop -= dRect.top - hRect.top;
203
+ }
204
+ });
205
+ return;
206
+ }
207
+ if (hasDropdown && this._dropdownIndex >= 0 && (e.key === "Enter" || e.key === "Tab")) this._editVal = this._dropdownItems[this._dropdownIndex];
208
+ if (e.key === "Enter") {
209
+ e.preventDefault();
210
+ const pos = this._editing;
211
+ this._commitEdit();
212
+ if (e.ctrlKey) this._select(pos.row, pos.col);
213
+ else if (e.shiftKey) this._select(Math.max(0, pos.row - 1), pos.col);
214
+ else this._select(Math.min(this._rowCount - 1, pos.row + 1), pos.col);
215
+ return;
216
+ }
217
+ if (e.key === "Tab") {
218
+ e.preventDefault();
219
+ const pos = this._editing;
220
+ this._commitEdit();
221
+ if (e.shiftKey) if (pos.col === 0 && pos.row > 0) this._select(pos.row - 1, this._colCount - 1);
222
+ else this._select(pos.row, Math.max(0, pos.col - 1));
223
+ else if (pos.col === this._colCount - 1) if (pos.row < this._rowCount - 1) this._select(pos.row + 1, 0);
224
+ else this._select(pos.row, pos.col);
225
+ else this._select(pos.row, pos.col + 1);
226
+ return;
227
+ }
228
+ if (e.key === "ArrowUp") {
229
+ e.preventDefault();
230
+ const pos = this._editing;
231
+ this._commitEdit();
232
+ this._select(Math.max(0, pos.row - 1), pos.col);
233
+ return;
234
+ }
235
+ if (e.key === "ArrowDown") {
236
+ e.preventDefault();
237
+ const pos = this._editing;
238
+ this._commitEdit();
239
+ this._select(Math.min(this._rowCount - 1, pos.row + 1), pos.col);
240
+ return;
241
+ }
242
+ };
243
+ this._onInputBlur = () => {
244
+ if (this._isDropdownClick) {
245
+ this._isDropdownClick = false;
246
+ return;
247
+ }
248
+ if (this._editing) this._commitEdit();
249
+ };
250
+ this._onDropdownItemMouseDown = (e, value) => {
251
+ e.preventDefault();
252
+ this._isDropdownClick = true;
253
+ this._editVal = value;
254
+ this._commitEdit();
255
+ };
256
+ this._onTableMouseDown = (e) => {
257
+ const cell = this._getCellFromEvent(e);
258
+ if (!cell) return;
259
+ e.preventDefault();
260
+ if (this._editing) this._commitEdit();
261
+ const { row, col } = cell;
262
+ if (e.shiftKey && this._sel) {
263
+ this._sel = {
264
+ anchor: this._sel.anchor,
265
+ focus: {
266
+ row,
267
+ col
268
+ }
269
+ };
270
+ this.requestUpdate();
271
+ } else this._select(row, col);
272
+ this._isMouseSelecting = true;
273
+ this._containerEl?.focus();
274
+ };
275
+ this._onTableMouseOver = (e) => {
276
+ if (!this._isMouseSelecting || !this._sel) return;
277
+ const cell = this._getCellFromEvent(e);
278
+ if (!cell) return;
279
+ this._sel = {
280
+ anchor: this._sel.anchor,
281
+ focus: cell
282
+ };
283
+ this.requestUpdate();
284
+ };
285
+ this._onTableDblClick = (e) => {
286
+ const cell = this._getCellFromEvent(e);
287
+ if (!cell || this.readonly || this._isColReadonly(cell.col)) return;
288
+ this._startEdit(cell.row, cell.col);
289
+ };
290
+ this._onDocMouseUp = () => {
291
+ this._isMouseSelecting = false;
292
+ };
293
+ this._onResizeStart = (e, col) => {
294
+ e.preventDefault();
295
+ e.stopPropagation();
296
+ this._resizing = {
297
+ col,
298
+ startX: e.clientX,
299
+ startWidth: this._colWidths[col] ?? _USimpleSheet.DEFAULT_COL_WIDTH,
300
+ current: this._colWidths[col] ?? _USimpleSheet.DEFAULT_COL_WIDTH
301
+ };
302
+ document.addEventListener("mousemove", this._onResizeMove);
303
+ document.addEventListener("mouseup", this._onResizeEnd);
304
+ };
305
+ this._onResizeMove = (e) => {
306
+ if (!this._resizing) return;
307
+ const dx = e.clientX - this._resizing.startX;
308
+ const newWidth = Math.max(_USimpleSheet.MIN_COL_WIDTH, this._resizing.startWidth + dx);
309
+ this._resizing.current = newWidth;
310
+ const th = this.renderRoot.querySelector(`th.col-header:nth-child(${this._resizing.col + 2})`);
311
+ if (th) {
312
+ th.style.width = `${newWidth}px`;
313
+ th.style.minWidth = `${newWidth}px`;
314
+ }
315
+ };
316
+ this._onResizeEnd = () => {
317
+ if (this._resizing) {
318
+ this._colWidths[this._resizing.col] = this._resizing.current;
319
+ this._resizing = null;
320
+ this.requestUpdate();
321
+ }
322
+ document.removeEventListener("mousemove", this._onResizeMove);
323
+ document.removeEventListener("mouseup", this._onResizeEnd);
324
+ };
325
+ this._onSelectAll = (e) => {
326
+ e.preventDefault();
327
+ this._sel = {
328
+ anchor: {
329
+ row: 0,
330
+ col: 0
331
+ },
332
+ focus: {
333
+ row: this._rowCount - 1,
334
+ col: this._colCount - 1
335
+ }
336
+ };
337
+ this.requestUpdate();
338
+ this._containerEl?.focus();
339
+ };
340
+ this._onColHeaderMouseDown = (e, col) => {
341
+ e.preventDefault();
342
+ if (this._editing) this._commitEdit();
343
+ if (e.shiftKey && this._sel) this._sel = {
344
+ anchor: {
345
+ row: 0,
346
+ col: this._sel.anchor.col
347
+ },
348
+ focus: {
349
+ row: this._rowCount - 1,
350
+ col
351
+ }
352
+ };
353
+ else this._sel = {
354
+ anchor: {
355
+ row: 0,
356
+ col
357
+ },
358
+ focus: {
359
+ row: this._rowCount - 1,
360
+ col
361
+ }
362
+ };
363
+ this.requestUpdate();
364
+ this._containerEl?.focus();
365
+ };
366
+ this._onRowHeaderMouseDown = (e, row) => {
367
+ e.preventDefault();
368
+ if (this._editing) this._commitEdit();
369
+ if (e.shiftKey && this._sel) this._sel = {
370
+ anchor: {
371
+ row: this._sel.anchor.row,
372
+ col: 0
373
+ },
374
+ focus: {
375
+ row,
376
+ col: this._colCount - 1
377
+ }
378
+ };
379
+ else this._sel = {
380
+ anchor: {
381
+ row,
382
+ col: 0
383
+ },
384
+ focus: {
385
+ row,
386
+ col: this._colCount - 1
387
+ }
388
+ };
389
+ this.requestUpdate();
390
+ this._containerEl?.focus();
391
+ };
392
+ this._onCopy = (e) => {
393
+ if (!this._sel || this._editing) return;
394
+ e.preventDefault();
395
+ const tsv = this._selectionToTSV();
396
+ e.clipboardData?.setData("text/plain", tsv);
397
+ };
398
+ this._onPaste = (e) => {
399
+ if (this.readonly || !this._sel) return;
400
+ e.preventDefault();
401
+ const text = e.clipboardData?.getData("text/plain") ?? "";
402
+ this._pasteFromText(text);
403
+ };
404
+ }
405
+ static {
406
+ this.styles = [super.styles, styles];
407
+ }
408
+ static {
409
+ this.MIN_COL_WIDTH = 30;
410
+ }
411
+ static {
412
+ this.DEFAULT_COL_WIDTH = 80;
413
+ }
414
+ static {
415
+ this.HISTORY_LIMIT = 100;
416
+ }
417
+ connectedCallback() {
418
+ super.connectedCallback();
419
+ this._syncData();
420
+ document.addEventListener("mouseup", this._onDocMouseUp);
421
+ }
422
+ disconnectedCallback() {
423
+ super.disconnectedCallback();
424
+ document.removeEventListener("mouseup", this._onDocMouseUp);
425
+ document.removeEventListener("mousemove", this._onResizeMove);
426
+ document.removeEventListener("mouseup", this._onResizeEnd);
427
+ }
428
+ willUpdate(changed) {
429
+ if ([
430
+ "data",
431
+ "rows",
432
+ "cols",
433
+ "columns"
434
+ ].some((k) => changed.has(k))) this._syncDataFromProps();
435
+ }
436
+ _syncDataFromProps() {
437
+ const rowCount = Math.max(this.rows, this.data?.length ?? 0);
438
+ const colCount = Math.max(this.columns?.length ?? this.cols, ...this.data?.map((r) => r?.length ?? 0) ?? [0]);
439
+ const newData = [];
440
+ for (let r = 0; r < rowCount; r++) {
441
+ const row = [];
442
+ for (let c = 0; c < colCount; c++) row.push(this.data?.[r]?.[c] ?? "");
443
+ newData.push(row);
444
+ }
445
+ this._data = newData;
446
+ this._recompute();
447
+ this._history = [this._data.map((r) => [...r])];
448
+ this._historyIndex = 0;
449
+ const prevWidths = this._colWidths;
450
+ this._colWidths = Array.from({ length: colCount }, (_, c) => this.columns?.[c]?.width ?? prevWidths[c] ?? _USimpleSheet.DEFAULT_COL_WIDTH);
451
+ }
452
+ _syncData() {
453
+ this._syncDataFromProps();
454
+ }
455
+ get _rowCount() {
456
+ return this._data.length;
457
+ }
458
+ get _colCount() {
459
+ return this._data[0]?.length ?? this.columns?.length ?? this.cols;
460
+ }
461
+ _pushHistory() {
462
+ this._history.splice(this._historyIndex + 1);
463
+ this._history.push(this._data.map((r) => [...r]));
464
+ if (this._history.length > _USimpleSheet.HISTORY_LIMIT) this._history.shift();
465
+ else this._historyIndex++;
466
+ }
467
+ _undo() {
468
+ if (this._historyIndex <= 0) return;
469
+ this._historyIndex--;
470
+ this._data = this._history[this._historyIndex].map((r) => [...r]);
471
+ this._recompute();
472
+ this._emitChange();
473
+ this.requestUpdate();
474
+ }
475
+ _redo() {
476
+ if (this._historyIndex >= this._history.length - 1) return;
477
+ this._historyIndex++;
478
+ this._data = this._history[this._historyIndex].map((r) => [...r]);
479
+ this._recompute();
480
+ this._emitChange();
481
+ this.requestUpdate();
482
+ }
483
+ /** 열 인덱스를 A, B, ..., Z, AA, AB, ... 형태로 변환 */
484
+ _colLabel(c) {
485
+ if (this.columns?.[c]?.label) return this.columns[c].label;
486
+ let label = "";
487
+ let n = c;
488
+ do {
489
+ label = String.fromCharCode(65 + n % 26) + label;
490
+ n = Math.floor(n / 26) - 1;
491
+ } while (n >= 0);
492
+ return label;
493
+ }
494
+ _colWidthStyle(c) {
495
+ const w = this._colWidths[c] ?? _USimpleSheet.DEFAULT_COL_WIDTH;
496
+ return `width:${w}px;min-width:${w}px;`;
497
+ }
498
+ _inSel(r, c) {
499
+ if (!this._sel) return false;
500
+ const { minRow, maxRow, minCol, maxCol } = normalizeRange(this._sel);
501
+ return r >= minRow && r <= maxRow && c >= minCol && c <= maxCol;
502
+ }
503
+ _isAnchor(r, c) {
504
+ return this._sel?.anchor.row === r && this._sel?.anchor.col === c;
505
+ }
506
+ _isColSelected(c) {
507
+ if (!this._sel) return false;
508
+ const { minCol, maxCol } = normalizeRange(this._sel);
509
+ return c >= minCol && c <= maxCol;
510
+ }
511
+ _isRowSelected(r) {
512
+ if (!this._sel) return false;
513
+ const { minRow, maxRow } = normalizeRange(this._sel);
514
+ return r >= minRow && r <= maxRow;
515
+ }
516
+ render() {
517
+ return html`
518
+ <div
519
+ class="sheet-container${this._resizing ? " is-resizing" : ""}"
520
+ tabindex="0"
521
+ ${ref(this._refContainer)}
522
+ @keydown=${this._onContainerKeyDown}
523
+ @focus=${this._onContainerFocus}
524
+ @copy=${this._onCopy}
525
+ @paste=${this._onPaste}
526
+ >
527
+ <div class="sheet-scroll">
528
+ <table
529
+ class="sheet-table"
530
+ @mousedown=${this._onTableMouseDown}
531
+ @mouseover=${this._onTableMouseOver}
532
+ @dblclick=${this._onTableDblClick}
533
+ >
534
+ <thead>
535
+ <tr>
536
+ <th class="corner" @click=${this._onSelectAll}></th>
537
+ ${Array.from({ length: this._colCount }, (_, c) => html`
538
+ <th
539
+ class="col-header ${this._isColSelected(c) ? "col-selected" : ""}"
540
+ style=${this._colWidthStyle(c)}
541
+ @mousedown=${(e) => this._onColHeaderMouseDown(e, c)}
542
+ >
543
+ ${this._colLabel(c)}
544
+ <span
545
+ class="resize-handle"
546
+ @mousedown=${(e) => this._onResizeStart(e, c)}
547
+ ></span>
548
+ </th>
549
+ `)}
550
+ </tr>
551
+ </thead>
552
+ <tbody>
553
+ ${Array.from({ length: this._rowCount }, (_, r) => html`
554
+ <tr>
555
+ <td
556
+ class="row-num ${this._isRowSelected(r) ? "row-selected" : ""}"
557
+ @mousedown=${(e) => this._onRowHeaderMouseDown(e, r)}
558
+ >${r + 1}</td>
559
+ ${Array.from({ length: this._colCount }, (_, c) => this._renderCell(r, c))}
560
+ </tr>
561
+ `)}
562
+ </tbody>
563
+ </table>
564
+ </div>
565
+ </div>
566
+ `;
567
+ }
568
+ _renderCell(r, c) {
569
+ const isEditing = this._editing?.row === r && this._editing?.col === c;
570
+ const isSelected = this._inSel(r, c);
571
+ const isAnchor = this._isAnchor(r, c);
572
+ const value = this._data[r]?.[c] ?? "";
573
+ const isColReadonly = this._isColReadonly(c);
574
+ const isComputed = this._isColComputed(c);
575
+ const isNumeric = this.columns?.[c]?.format && typeof this.columns[c].format !== "function" || this._isNumeric(value);
576
+ const classes = [
577
+ "cell",
578
+ isSelected ? "selected" : "",
579
+ isAnchor ? "anchor" : "",
580
+ isEditing ? "editing" : "",
581
+ isColReadonly ? "cell-readonly" : "",
582
+ isComputed ? "cell-computed" : "",
583
+ isNumeric ? "cell-numeric" : ""
584
+ ].filter(Boolean).join(" ");
585
+ const hasOptions = isEditing && this._getColOptions(r, c) !== null;
586
+ const showDropdown = isEditing && this._dropdownItems.length > 0;
587
+ const isStrict = this.columns?.[c]?.strict ?? false;
588
+ const noMatch = isEditing && hasOptions && this._dropdownItems.length === 0 && this._editVal !== "";
589
+ return html`
590
+ <td
591
+ class=${classes}
592
+ data-row=${r}
593
+ data-col=${c}
594
+ >
595
+ ${isEditing ? html`
596
+ <input
597
+ class="cell-input"
598
+ .value=${this._editVal}
599
+ @input=${this._onInputChange}
600
+ @keydown=${this._onInputKeyDown}
601
+ @blur=${this._onInputBlur}
602
+ />
603
+ ${showDropdown ? html`
604
+ <div class="cell-dropdown">
605
+ ${this._dropdownItems.map((item, i) => html`
606
+ <div
607
+ class="dropdown-item ${i === this._dropdownIndex ? "highlighted" : ""}"
608
+ @mousedown=${(e) => this._onDropdownItemMouseDown(e, item)}
609
+ >${item}</div>
610
+ `)}
611
+ </div>
612
+ ` : noMatch && isStrict ? html`
613
+ <div class="cell-dropdown">
614
+ <div class="dropdown-empty">일치하는 항목 없음</div>
615
+ </div>
616
+ ` : ""}
617
+ ` : this._formatValue(value, c, r)}
618
+ </td>
619
+ `;
620
+ }
621
+ _selectionToTSV() {
622
+ if (!this._sel) return "";
623
+ const { minRow, maxRow, minCol, maxCol } = normalizeRange(this._sel);
624
+ const rows = [];
625
+ for (let r = minRow; r <= maxRow; r++) {
626
+ const cells = [];
627
+ for (let c = minCol; c <= maxCol; c++) cells.push(this._data[r]?.[c] ?? "");
628
+ rows.push(cells.join(" "));
629
+ }
630
+ return rows.join("\n");
631
+ }
632
+ _pasteFromText(text) {
633
+ if (!this._sel) return;
634
+ const { anchor } = this._sel;
635
+ const pasteRows = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n$/, "").split("\n").map((row) => row.split(" "));
636
+ const newData = this._data.map((r) => [...r]);
637
+ const neededRows = anchor.row + pasteRows.length;
638
+ const rawNeededCols = anchor.col + Math.max(...pasteRows.map((r) => r.length));
639
+ const neededCols = this.columns?.length ? Math.min(rawNeededCols, this.columns.length) : rawNeededCols;
640
+ while (newData.length < neededRows) newData.push(Array(newData[0]?.length ?? this._colCount).fill(""));
641
+ for (let r = 0; r < newData.length; r++) while (newData[r].length < neededCols) newData[r].push("");
642
+ for (let ri = 0; ri < pasteRows.length; ri++) for (let ci = 0; ci < pasteRows[ri].length; ci++) {
643
+ const r = anchor.row + ri;
644
+ const c = anchor.col + ci;
645
+ if (r < newData.length && c < (newData[r]?.length ?? 0) && !this._isColComputed(c)) newData[r][c] = pasteRows[ri][ci];
646
+ }
647
+ this._data = newData;
648
+ this._recompute();
649
+ this._pushHistory();
650
+ this._emitChange();
651
+ this.requestUpdate();
652
+ }
653
+ /** Ctrl+D: 선택 영역 첫 행 값을 아래로 채우기 */
654
+ _fillDown() {
655
+ if (!this._sel) return;
656
+ const { minRow, maxRow, minCol, maxCol } = normalizeRange(this._sel);
657
+ if (minRow === maxRow) return;
658
+ const newData = this._data.map((r) => [...r]);
659
+ for (let c = minCol; c <= maxCol; c++) {
660
+ const fillVal = newData[minRow]?.[c] ?? "";
661
+ for (let r = minRow + 1; r <= maxRow; r++) if (newData[r] && c < newData[r].length && !this._isColReadonly(c)) newData[r][c] = fillVal;
662
+ }
663
+ this._data = newData;
664
+ this._recompute();
665
+ this._pushHistory();
666
+ this._emitChange();
667
+ this.requestUpdate();
668
+ }
669
+ /** Ctrl+R: 선택 영역 첫 열 값을 오른쪽으로 채우기 */
670
+ _fillRight() {
671
+ if (!this._sel) return;
672
+ const { minRow, maxRow, minCol, maxCol } = normalizeRange(this._sel);
673
+ if (minCol === maxCol) return;
674
+ const newData = this._data.map((r) => [...r]);
675
+ for (let r = minRow; r <= maxRow; r++) {
676
+ const fillVal = newData[r]?.[minCol] ?? "";
677
+ for (let c = minCol + 1; c <= maxCol; c++) if (newData[r] && c < newData[r].length && !this._isColReadonly(c)) newData[r][c] = fillVal;
678
+ }
679
+ this._data = newData;
680
+ this._recompute();
681
+ this._pushHistory();
682
+ this._emitChange();
683
+ this.requestUpdate();
684
+ }
685
+ /**
686
+ * @param typedChar - 타이핑으로 시작할 경우 첫 글자. 미제공시 기존 값 유지.
687
+ */
688
+ _startEdit(row, col, typedChar) {
689
+ this._replaceOnEdit = typedChar !== void 0;
690
+ this._editVal = typedChar !== void 0 ? typedChar : this._data[row]?.[col] ?? "";
691
+ this._editing = {
692
+ row,
693
+ col
694
+ };
695
+ this._select(row, col);
696
+ const options = this._getColOptions(row, col);
697
+ if (options) {
698
+ const filtered = this._filterOptions(options, this._editVal);
699
+ this._dropdownItems = filtered;
700
+ this._dropdownIndex = filtered.length > 0 ? 0 : -1;
701
+ } else {
702
+ this._dropdownItems = [];
703
+ this._dropdownIndex = -1;
704
+ }
705
+ this.updateComplete.then(() => {
706
+ const input = this.renderRoot.querySelector(".cell-input");
707
+ if (input) {
708
+ input.focus();
709
+ if (this._replaceOnEdit) {
710
+ const len = input.value.length;
711
+ input.setSelectionRange(len, len);
712
+ } else input.select();
713
+ }
714
+ });
715
+ }
716
+ _commitEdit() {
717
+ if (!this._editing) return;
718
+ const { row, col } = this._editing;
719
+ const colDef = this.columns?.[col];
720
+ if (colDef?.strict && colDef?.options && this._editVal !== "") {
721
+ if (!(this._getColOptions(row, col) ?? []).includes(this._editVal)) {
722
+ this._editing = null;
723
+ this._dropdownItems = [];
724
+ this._dropdownIndex = -1;
725
+ this._isDropdownClick = false;
726
+ this.requestUpdate();
727
+ this._containerEl?.focus();
728
+ return;
729
+ }
730
+ }
731
+ const prevVal = this._data[row]?.[col] ?? "";
732
+ const newData = this._data.map((r) => [...r]);
733
+ newData[row][col] = this._editVal;
734
+ this._data = newData;
735
+ this._editing = null;
736
+ this._dropdownItems = [];
737
+ this._dropdownIndex = -1;
738
+ if (this._editVal !== prevVal) {
739
+ this._recompute();
740
+ this._pushHistory();
741
+ this._emitChange();
742
+ }
743
+ this.requestUpdate();
744
+ this._containerEl?.focus();
745
+ }
746
+ _cancelEdit() {
747
+ this._editing = null;
748
+ this._dropdownItems = [];
749
+ this._dropdownIndex = -1;
750
+ this.requestUpdate();
751
+ this._containerEl?.focus();
752
+ }
753
+ _clearSelection() {
754
+ if (!this._sel) return;
755
+ const { minRow, maxRow, minCol, maxCol } = normalizeRange(this._sel);
756
+ const newData = this._data.map((r) => [...r]);
757
+ for (let r = minRow; r <= maxRow; r++) for (let c = minCol; c <= maxCol; c++) if (!this._isColReadonly(c)) newData[r][c] = "";
758
+ this._data = newData;
759
+ this._recompute();
760
+ this._pushHistory();
761
+ this._emitChange();
762
+ this.requestUpdate();
763
+ }
764
+ _select(row, col) {
765
+ this._sel = {
766
+ anchor: {
767
+ row,
768
+ col
769
+ },
770
+ focus: {
771
+ row,
772
+ col
773
+ }
774
+ };
775
+ this.requestUpdate();
776
+ this._scrollCellIntoView(row, col);
777
+ }
778
+ _scrollCellIntoView(row, col) {
779
+ this.updateComplete.then(() => {
780
+ this.renderRoot.querySelector(`td[data-row="${row}"][data-col="${col}"]`)?.scrollIntoView({
781
+ block: "nearest",
782
+ inline: "nearest"
783
+ });
784
+ });
785
+ }
786
+ _getCellFromEvent(e) {
787
+ const target = e.target.closest("td[data-row]");
788
+ if (!target) return null;
789
+ const row = parseInt(target.dataset.row ?? "-1");
790
+ const col = parseInt(target.dataset.col ?? "-1");
791
+ if (row < 0 || col < 0) return null;
792
+ return {
793
+ row,
794
+ col
795
+ };
796
+ }
797
+ _isColReadonly(col) {
798
+ return (this.columns?.[col]?.readonly ?? false) || this._isColComputed(col);
799
+ }
800
+ _isColComputed(col) {
801
+ return typeof this.columns?.[col]?.compute === "function";
802
+ }
803
+ _isNumeric(value) {
804
+ if (!value || !value.trim()) return false;
805
+ return !isNaN(Number(value.replace(/,/g, "")));
806
+ }
807
+ /** 열의 format 설정에 따라 표시값을 반환. format 미설정 시 원본 반환. */
808
+ _formatValue(value, col, row) {
809
+ const fmt = this.columns?.[col]?.format;
810
+ if (!fmt || !value) return value;
811
+ try {
812
+ if (typeof fmt === "function") return fmt(value, row);
813
+ const num = Number(value.replace(/,/g, ""));
814
+ if (isNaN(num)) return value;
815
+ return new Intl.NumberFormat("ko-KR", fmt).format(num);
816
+ } catch {
817
+ return value;
818
+ }
819
+ }
820
+ /** compute 열을 재계산 (열 순서 좌→우, 행 순서 위→아래) */
821
+ _recompute() {
822
+ if (!this.columns?.some((c) => c.compute)) return;
823
+ for (let c = 0; c < this._colCount; c++) {
824
+ const fn = this.columns?.[c]?.compute;
825
+ if (!fn) continue;
826
+ for (let r = 0; r < this._rowCount; r++) try {
827
+ this._data[r][c] = fn(r, this._data);
828
+ } catch {
829
+ this._data[r][c] = "";
830
+ }
831
+ }
832
+ }
833
+ _getColOptions(row, col) {
834
+ const colDef = this.columns?.[col];
835
+ if (!colDef?.options) return null;
836
+ return typeof colDef.options === "function" ? colDef.options(row, col) : colDef.options;
837
+ }
838
+ _filterOptions(options, query) {
839
+ if (!query) return options;
840
+ const lower = query.toLowerCase();
841
+ return options.filter((o) => o.toLowerCase().includes(lower));
842
+ }
843
+ _emitChange() {
844
+ this.fire("change", { detail: { data: this._data } });
845
+ }
846
+ /** 현재 데이터를 2D 배열로 반환 */
847
+ getData() {
848
+ return this._data.map((r) => [...r]);
849
+ }
850
+ /**
851
+ * 정의된 columns의 key를 기준으로 객체 배열로 반환.
852
+ * columns 미설정시 A, B, C... 키를 사용.
853
+ */
854
+ getDataAsObjects() {
855
+ if (!this.columns?.length) return this._data.map((row) => Object.fromEntries(row.map((v, i) => [this._colLabel(i), v])));
856
+ return this._data.map((row) => Object.fromEntries(this.columns.map((col, i) => [col.key ?? this._colLabel(i), row[i] ?? ""])));
857
+ }
858
+ /** 데이터를 직접 설정하고 재렌더링 */
859
+ setData(data) {
860
+ this.data = data;
861
+ this._syncData();
862
+ this.requestUpdate();
863
+ }
864
+ /** 특정 셀 값 설정 */
865
+ setCell(row, col, value) {
866
+ if (row >= 0 && row < this._data.length && col >= 0 && col < this._colCount) {
867
+ const newData = this._data.map((r) => [...r]);
868
+ newData[row][col] = value;
869
+ this._data = newData;
870
+ this._recompute();
871
+ this._pushHistory();
872
+ this._emitChange();
873
+ this.requestUpdate();
874
+ }
875
+ }
876
+ /** 현재 선택 영역 반환 */
877
+ getSelection() {
878
+ if (!this._sel) return null;
879
+ return normalizeRange(this._sel);
880
+ }
881
+ /** 선택 영역을 프로그래밍 방식으로 설정 */
882
+ setSelection(range) {
883
+ const minRow = Math.max(0, Math.min(range.minRow, this._rowCount - 1));
884
+ const maxRow = Math.max(0, Math.min(range.maxRow, this._rowCount - 1));
885
+ const minCol = Math.max(0, Math.min(range.minCol, this._colCount - 1));
886
+ const maxCol = Math.max(0, Math.min(range.maxCol, this._colCount - 1));
887
+ this._sel = {
888
+ anchor: {
889
+ row: minRow,
890
+ col: minCol
891
+ },
892
+ focus: {
893
+ row: maxRow,
894
+ col: maxCol
895
+ }
896
+ };
897
+ this.requestUpdate();
898
+ }
899
+ /** 전체 셀 선택 */
900
+ selectAll() {
901
+ this._sel = {
902
+ anchor: {
903
+ row: 0,
904
+ col: 0
905
+ },
906
+ focus: {
907
+ row: this._rowCount - 1,
908
+ col: this._colCount - 1
909
+ }
910
+ };
911
+ this.requestUpdate();
912
+ }
913
+ /** Undo 가능 여부 */
914
+ get canUndo() {
915
+ return this._historyIndex > 0;
916
+ }
917
+ /** Redo 가능 여부 */
918
+ get canRedo() {
919
+ return this._historyIndex < this._history.length - 1;
920
+ }
921
+ };
922
+ __decorate([property({ type: Array }), __decorateMetadata("design:type", Array)], USimpleSheet.prototype, "data", void 0);
923
+ __decorate([property({ type: Array }), __decorateMetadata("design:type", Array)], USimpleSheet.prototype, "columns", void 0);
924
+ __decorate([property({ type: Number }), __decorateMetadata("design:type", Object)], USimpleSheet.prototype, "rows", void 0);
925
+ __decorate([property({ type: Number }), __decorateMetadata("design:type", Object)], USimpleSheet.prototype, "cols", void 0);
926
+ __decorate([property({ type: Boolean }), __decorateMetadata("design:type", Object)], USimpleSheet.prototype, "readonly", void 0);
927
+ __decorate([state(), __decorateMetadata("design:type", Object)], USimpleSheet.prototype, "_sel", void 0);
928
+ __decorate([state(), __decorateMetadata("design:type", Object)], USimpleSheet.prototype, "_editing", void 0);
929
+ __decorate([state(), __decorateMetadata("design:type", Object)], USimpleSheet.prototype, "_editVal", void 0);
930
+ __decorate([state(), __decorateMetadata("design:type", Object)], USimpleSheet.prototype, "_replaceOnEdit", void 0);
931
+ __decorate([state(), __decorateMetadata("design:type", Array)], USimpleSheet.prototype, "_dropdownItems", void 0);
932
+ __decorate([state(), __decorateMetadata("design:type", Object)], USimpleSheet.prototype, "_dropdownIndex", void 0);
933
+ USimpleSheet = _USimpleSheet = __decorate([customElement("u-simple-sheet")], USimpleSheet);
934
+ //#endregion
5
935
  export { USimpleSheet };