@iyulab/data-components 0.1.0 → 0.1.2

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,855 @@
1
+ import { html } from 'lit';
2
+ import { property, state } from 'lit/decorators.js';
3
+ import { ref } from 'lit/directives/ref.js';
4
+ import { UElement } from '@iyulab/components/dist/components/UElement.js';
5
+ import { styles } from './USimpleSheet.styles.js';
6
+
7
+ var __defProp = Object.defineProperty;
8
+ var __decorateClass = (decorators, target, key, kind) => {
9
+ var result = void 0 ;
10
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
11
+ if (decorator = decorators[i])
12
+ result = (decorator(target, key, result) ) || result;
13
+ if (result) __defProp(target, key, result);
14
+ return result;
15
+ };
16
+ function normalizeRange(sel) {
17
+ return {
18
+ minRow: Math.min(sel.anchor.row, sel.focus.row),
19
+ maxRow: Math.max(sel.anchor.row, sel.focus.row),
20
+ minCol: Math.min(sel.anchor.col, sel.focus.col),
21
+ maxCol: Math.max(sel.anchor.col, sel.focus.col)
22
+ };
23
+ }
24
+ const _USimpleSheet = class _USimpleSheet extends UElement {
25
+ constructor() {
26
+ super(...arguments);
27
+ this.data = [];
28
+ this.rows = 20;
29
+ this.cols = 10;
30
+ this.readonly = false;
31
+ this._sel = null;
32
+ this._editing = null;
33
+ this._editVal = "";
34
+ this._replaceOnEdit = false;
35
+ // @state() 없음 — 직접 requestUpdate() 호출
36
+ this._data = [];
37
+ this._colWidths = [];
38
+ this._isMouseSelecting = false;
39
+ this._containerEl = null;
40
+ // 열 너비 조절
41
+ this._resizing = null;
42
+ // Undo/Redo 히스토리
43
+ this._history = [];
44
+ this._historyIndex = -1;
45
+ // ──────────────────────────────────────────
46
+ // ref 콜백
47
+ // ──────────────────────────────────────────
48
+ this._refContainer = (el) => {
49
+ this._containerEl = el ?? null;
50
+ };
51
+ // ──────────────────────────────────────────
52
+ // 컨테이너 이벤트
53
+ // ──────────────────────────────────────────
54
+ this._onContainerFocus = () => {
55
+ if (!this._sel) {
56
+ this._select(0, 0);
57
+ }
58
+ };
59
+ this._onContainerKeyDown = (e) => {
60
+ if (this._editing) return;
61
+ const anchor = this._sel?.anchor ?? { row: 0, col: 0 };
62
+ if (e.ctrlKey || e.metaKey) {
63
+ if (e.key === "z" || e.key === "Z") {
64
+ e.preventDefault();
65
+ if (!this.readonly) {
66
+ if (e.shiftKey) this._redo();
67
+ else this._undo();
68
+ }
69
+ return;
70
+ }
71
+ if (e.key === "y" || e.key === "Y") {
72
+ e.preventDefault();
73
+ if (!this.readonly) this._redo();
74
+ return;
75
+ }
76
+ if (e.key === "a" || e.key === "A") {
77
+ e.preventDefault();
78
+ this._sel = {
79
+ anchor: { row: 0, col: 0 },
80
+ focus: { row: this._rowCount - 1, col: this._colCount - 1 }
81
+ };
82
+ this.requestUpdate();
83
+ return;
84
+ }
85
+ if (e.key === "d" || e.key === "D") {
86
+ e.preventDefault();
87
+ if (!this.readonly && this._sel) this._fillDown();
88
+ return;
89
+ }
90
+ if (e.key === "r" || e.key === "R") {
91
+ e.preventDefault();
92
+ if (!this.readonly && this._sel) this._fillRight();
93
+ return;
94
+ }
95
+ }
96
+ if (e.key === "Delete" || e.key === "Backspace") {
97
+ e.preventDefault();
98
+ if (!this.readonly) this._clearSelection();
99
+ return;
100
+ }
101
+ if (e.key === "F2") {
102
+ e.preventDefault();
103
+ if (!this.readonly && !this._isColReadonly(anchor.col)) {
104
+ this._startEdit(anchor.row, anchor.col);
105
+ }
106
+ return;
107
+ }
108
+ if (e.key === "Enter") {
109
+ e.preventDefault();
110
+ if (e.shiftKey) {
111
+ this._select(Math.max(0, anchor.row - 1), anchor.col);
112
+ } else {
113
+ this._select(Math.min(this._rowCount - 1, anchor.row + 1), anchor.col);
114
+ }
115
+ return;
116
+ }
117
+ if (e.key === "Tab") {
118
+ e.preventDefault();
119
+ if (e.shiftKey) {
120
+ if (anchor.col === 0 && anchor.row > 0) {
121
+ this._select(anchor.row - 1, this._colCount - 1);
122
+ } else {
123
+ this._select(anchor.row, Math.max(0, anchor.col - 1));
124
+ }
125
+ } else {
126
+ if (anchor.col === this._colCount - 1) {
127
+ if (anchor.row < this._rowCount - 1) {
128
+ this._select(anchor.row + 1, 0);
129
+ }
130
+ } else {
131
+ this._select(anchor.row, anchor.col + 1);
132
+ }
133
+ }
134
+ return;
135
+ }
136
+ const arrowMoves = {
137
+ ArrowUp: [-1, 0],
138
+ ArrowDown: [1, 0],
139
+ ArrowLeft: [0, -1],
140
+ ArrowRight: [0, 1]
141
+ };
142
+ if (arrowMoves[e.key]) {
143
+ e.preventDefault();
144
+ const [dr, dc] = arrowMoves[e.key];
145
+ if (e.shiftKey && this._sel) {
146
+ const curFocus = this._sel.focus;
147
+ const nextFocus = {
148
+ row: Math.max(0, Math.min(this._rowCount - 1, curFocus.row + dr)),
149
+ col: Math.max(0, Math.min(this._colCount - 1, curFocus.col + dc))
150
+ };
151
+ this._sel = { anchor: this._sel.anchor, focus: nextFocus };
152
+ this.requestUpdate();
153
+ } else {
154
+ const newRow = Math.max(0, Math.min(this._rowCount - 1, anchor.row + dr));
155
+ const newCol = Math.max(0, Math.min(this._colCount - 1, anchor.col + dc));
156
+ this._select(newRow, newCol);
157
+ }
158
+ return;
159
+ }
160
+ if (e.key === "Home") {
161
+ e.preventDefault();
162
+ if (e.ctrlKey) this._select(0, 0);
163
+ else this._select(anchor.row, 0);
164
+ return;
165
+ }
166
+ if (e.key === "End") {
167
+ e.preventDefault();
168
+ if (e.ctrlKey) this._select(this._rowCount - 1, this._colCount - 1);
169
+ else this._select(anchor.row, this._colCount - 1);
170
+ return;
171
+ }
172
+ if (e.key === "PageUp") {
173
+ e.preventDefault();
174
+ this._select(Math.max(0, anchor.row - 10), anchor.col);
175
+ return;
176
+ }
177
+ if (e.key === "PageDown") {
178
+ e.preventDefault();
179
+ this._select(Math.min(this._rowCount - 1, anchor.row + 10), anchor.col);
180
+ return;
181
+ }
182
+ if (!this.readonly && !this._isColReadonly(anchor.col) && e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
183
+ this._startEdit(anchor.row, anchor.col, e.key);
184
+ }
185
+ };
186
+ // ──────────────────────────────────────────
187
+ // 입력 이벤트
188
+ // ──────────────────────────────────────────
189
+ this._onInputChange = (e) => {
190
+ this._editVal = e.target.value;
191
+ };
192
+ this._onInputKeyDown = (e) => {
193
+ e.stopPropagation();
194
+ if (e.key === "Escape") {
195
+ e.preventDefault();
196
+ this._cancelEdit();
197
+ return;
198
+ }
199
+ if (e.key === "Enter") {
200
+ e.preventDefault();
201
+ const pos = this._editing;
202
+ this._commitEdit();
203
+ if (e.ctrlKey) {
204
+ this._select(pos.row, pos.col);
205
+ } else if (e.shiftKey) {
206
+ this._select(Math.max(0, pos.row - 1), pos.col);
207
+ } else {
208
+ this._select(Math.min(this._rowCount - 1, pos.row + 1), pos.col);
209
+ }
210
+ return;
211
+ }
212
+ if (e.key === "Tab") {
213
+ e.preventDefault();
214
+ const pos = this._editing;
215
+ this._commitEdit();
216
+ if (e.shiftKey) {
217
+ if (pos.col === 0 && pos.row > 0) {
218
+ this._select(pos.row - 1, this._colCount - 1);
219
+ } else {
220
+ this._select(pos.row, Math.max(0, pos.col - 1));
221
+ }
222
+ } else {
223
+ if (pos.col === this._colCount - 1) {
224
+ if (pos.row < this._rowCount - 1) {
225
+ this._select(pos.row + 1, 0);
226
+ } else {
227
+ this._select(pos.row, pos.col);
228
+ }
229
+ } else {
230
+ this._select(pos.row, pos.col + 1);
231
+ }
232
+ }
233
+ return;
234
+ }
235
+ if (e.key === "ArrowUp") {
236
+ e.preventDefault();
237
+ const pos = this._editing;
238
+ this._commitEdit();
239
+ this._select(Math.max(0, pos.row - 1), pos.col);
240
+ return;
241
+ }
242
+ if (e.key === "ArrowDown") {
243
+ e.preventDefault();
244
+ const pos = this._editing;
245
+ this._commitEdit();
246
+ this._select(Math.min(this._rowCount - 1, pos.row + 1), pos.col);
247
+ return;
248
+ }
249
+ };
250
+ this._onInputBlur = () => {
251
+ if (this._editing) {
252
+ this._commitEdit();
253
+ }
254
+ };
255
+ // ──────────────────────────────────────────
256
+ // 마우스 이벤트
257
+ // ──────────────────────────────────────────
258
+ this._onTableMouseDown = (e) => {
259
+ const cell = this._getCellFromEvent(e);
260
+ if (!cell) return;
261
+ e.preventDefault();
262
+ if (this._editing) {
263
+ this._commitEdit();
264
+ }
265
+ const { row, col } = cell;
266
+ if (e.shiftKey && this._sel) {
267
+ this._sel = { anchor: this._sel.anchor, focus: { row, col } };
268
+ this.requestUpdate();
269
+ } else {
270
+ this._select(row, col);
271
+ }
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 = { anchor: this._sel.anchor, focus: cell };
280
+ this.requestUpdate();
281
+ };
282
+ this._onTableDblClick = (e) => {
283
+ const cell = this._getCellFromEvent(e);
284
+ if (!cell || this.readonly || this._isColReadonly(cell.col)) return;
285
+ this._startEdit(cell.row, cell.col);
286
+ };
287
+ this._onDocMouseUp = () => {
288
+ this._isMouseSelecting = false;
289
+ };
290
+ /** 열 너비 조절 시작 */
291
+ this._onResizeStart = (e, col) => {
292
+ e.preventDefault();
293
+ e.stopPropagation();
294
+ this._resizing = {
295
+ col,
296
+ startX: e.clientX,
297
+ startWidth: this._colWidths[col] ?? _USimpleSheet.DEFAULT_COL_WIDTH,
298
+ current: this._colWidths[col] ?? _USimpleSheet.DEFAULT_COL_WIDTH
299
+ };
300
+ document.addEventListener("mousemove", this._onResizeMove);
301
+ document.addEventListener("mouseup", this._onResizeEnd);
302
+ };
303
+ this._onResizeMove = (e) => {
304
+ if (!this._resizing) return;
305
+ const dx = e.clientX - this._resizing.startX;
306
+ const newWidth = Math.max(_USimpleSheet.MIN_COL_WIDTH, this._resizing.startWidth + dx);
307
+ this._resizing.current = newWidth;
308
+ const th = this.renderRoot.querySelector(
309
+ `th.col-header:nth-child(${this._resizing.col + 2})`
310
+ );
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
+ /** 코너 클릭 → 전체 선택 */
326
+ this._onSelectAll = (e) => {
327
+ e.preventDefault();
328
+ this._sel = {
329
+ anchor: { row: 0, col: 0 },
330
+ focus: { row: this._rowCount - 1, col: this._colCount - 1 }
331
+ };
332
+ this.requestUpdate();
333
+ this._containerEl?.focus();
334
+ };
335
+ /** 열 헤더 클릭 → 전체 열 선택 */
336
+ this._onColHeaderMouseDown = (e, col) => {
337
+ e.preventDefault();
338
+ if (this._editing) this._commitEdit();
339
+ if (e.shiftKey && this._sel) {
340
+ this._sel = {
341
+ anchor: { row: 0, col: this._sel.anchor.col },
342
+ focus: { row: this._rowCount - 1, col }
343
+ };
344
+ } else {
345
+ this._sel = {
346
+ anchor: { row: 0, col },
347
+ focus: { row: this._rowCount - 1, col }
348
+ };
349
+ }
350
+ this.requestUpdate();
351
+ this._containerEl?.focus();
352
+ };
353
+ /** 행 헤더 클릭 → 전체 행 선택 */
354
+ this._onRowHeaderMouseDown = (e, row) => {
355
+ e.preventDefault();
356
+ if (this._editing) this._commitEdit();
357
+ if (e.shiftKey && this._sel) {
358
+ this._sel = {
359
+ anchor: { row: this._sel.anchor.row, col: 0 },
360
+ focus: { row, col: this._colCount - 1 }
361
+ };
362
+ } else {
363
+ this._sel = {
364
+ anchor: { row, col: 0 },
365
+ focus: { row, col: this._colCount - 1 }
366
+ };
367
+ }
368
+ this.requestUpdate();
369
+ this._containerEl?.focus();
370
+ };
371
+ // ──────────────────────────────────────────
372
+ // 클립보드 (copy/paste)
373
+ // ──────────────────────────────────────────
374
+ this._onCopy = (e) => {
375
+ if (!this._sel || this._editing) return;
376
+ e.preventDefault();
377
+ const tsv = this._selectionToTSV();
378
+ e.clipboardData?.setData("text/plain", tsv);
379
+ };
380
+ this._onPaste = (e) => {
381
+ if (this.readonly || !this._sel) return;
382
+ e.preventDefault();
383
+ const text = e.clipboardData?.getData("text/plain") ?? "";
384
+ this._pasteFromText(text);
385
+ };
386
+ }
387
+ static {
388
+ this.styles = [super.styles, styles];
389
+ }
390
+ static {
391
+ this.MIN_COL_WIDTH = 30;
392
+ }
393
+ static {
394
+ this.DEFAULT_COL_WIDTH = 80;
395
+ }
396
+ static {
397
+ this.HISTORY_LIMIT = 100;
398
+ }
399
+ connectedCallback() {
400
+ super.connectedCallback();
401
+ this._syncData();
402
+ document.addEventListener("mouseup", this._onDocMouseUp);
403
+ }
404
+ disconnectedCallback() {
405
+ super.disconnectedCallback();
406
+ document.removeEventListener("mouseup", this._onDocMouseUp);
407
+ document.removeEventListener("mousemove", this._onResizeMove);
408
+ document.removeEventListener("mouseup", this._onResizeEnd);
409
+ }
410
+ willUpdate(changed) {
411
+ const dataRelated = ["data", "rows", "cols", "columns"].some((k) => changed.has(k));
412
+ if (dataRelated) {
413
+ this._syncDataFromProps();
414
+ }
415
+ }
416
+ // ──────────────────────────────────────────
417
+ // 데이터 동기화
418
+ // ──────────────────────────────────────────
419
+ _syncDataFromProps() {
420
+ const rowCount = Math.max(this.rows, this.data?.length ?? 0);
421
+ const colCount = Math.max(
422
+ this.columns?.length ?? this.cols,
423
+ ...this.data?.map((r) => r?.length ?? 0) ?? [0]
424
+ );
425
+ const newData = [];
426
+ for (let r = 0; r < rowCount; r++) {
427
+ const row = [];
428
+ for (let c = 0; c < colCount; c++) {
429
+ row.push(this.data?.[r]?.[c] ?? "");
430
+ }
431
+ newData.push(row);
432
+ }
433
+ this._data = newData;
434
+ this._history = [this._data.map((r) => [...r])];
435
+ this._historyIndex = 0;
436
+ const prevWidths = this._colWidths;
437
+ this._colWidths = Array.from(
438
+ { length: colCount },
439
+ (_, c) => this.columns?.[c]?.width ?? prevWidths[c] ?? _USimpleSheet.DEFAULT_COL_WIDTH
440
+ );
441
+ }
442
+ _syncData() {
443
+ this._syncDataFromProps();
444
+ }
445
+ get _rowCount() {
446
+ return this._data.length;
447
+ }
448
+ get _colCount() {
449
+ return this._data[0]?.length ?? (this.columns?.length ?? this.cols);
450
+ }
451
+ // ──────────────────────────────────────────
452
+ // Undo/Redo
453
+ // ──────────────────────────────────────────
454
+ _pushHistory() {
455
+ this._history.splice(this._historyIndex + 1);
456
+ this._history.push(this._data.map((r) => [...r]));
457
+ if (this._history.length > _USimpleSheet.HISTORY_LIMIT) {
458
+ this._history.shift();
459
+ } else {
460
+ this._historyIndex++;
461
+ }
462
+ }
463
+ _undo() {
464
+ if (this._historyIndex <= 0) return;
465
+ this._historyIndex--;
466
+ this._data = this._history[this._historyIndex].map((r) => [...r]);
467
+ this._emitChange();
468
+ this.requestUpdate();
469
+ }
470
+ _redo() {
471
+ if (this._historyIndex >= this._history.length - 1) return;
472
+ this._historyIndex++;
473
+ this._data = this._history[this._historyIndex].map((r) => [...r]);
474
+ this._emitChange();
475
+ this.requestUpdate();
476
+ }
477
+ // ──────────────────────────────────────────
478
+ // 헤더 유틸리티
479
+ // ──────────────────────────────────────────
480
+ /** 열 인덱스를 A, B, ..., Z, AA, AB, ... 형태로 변환 */
481
+ _colLabel(c) {
482
+ if (this.columns?.[c]?.label) return this.columns[c].label;
483
+ let label = "";
484
+ let n = c;
485
+ do {
486
+ label = String.fromCharCode(65 + n % 26) + label;
487
+ n = Math.floor(n / 26) - 1;
488
+ } while (n >= 0);
489
+ return label;
490
+ }
491
+ _colWidthStyle(c) {
492
+ const w = this._colWidths[c] ?? _USimpleSheet.DEFAULT_COL_WIDTH;
493
+ return `width:${w}px;min-width:${w}px;`;
494
+ }
495
+ // ──────────────────────────────────────────
496
+ // 선택 유틸리티
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
+ // ──────────────────────────────────────────
517
+ // 렌더링
518
+ // ──────────────────────────────────────────
519
+ render() {
520
+ return html`
521
+ <div
522
+ class="sheet-container${this._resizing ? " is-resizing" : ""}"
523
+ tabindex="0"
524
+ ${ref(this._refContainer)}
525
+ @keydown=${this._onContainerKeyDown}
526
+ @focus=${this._onContainerFocus}
527
+ @copy=${this._onCopy}
528
+ @paste=${this._onPaste}
529
+ >
530
+ <div class="sheet-scroll">
531
+ <table
532
+ class="sheet-table"
533
+ @mousedown=${this._onTableMouseDown}
534
+ @mouseover=${this._onTableMouseOver}
535
+ @dblclick=${this._onTableDblClick}
536
+ >
537
+ <thead>
538
+ <tr>
539
+ <th class="corner" @click=${this._onSelectAll}></th>
540
+ ${Array.from({ length: this._colCount }, (_, c) => html`
541
+ <th
542
+ class="col-header ${this._isColSelected(c) ? "col-selected" : ""}"
543
+ style=${this._colWidthStyle(c)}
544
+ @mousedown=${(e) => this._onColHeaderMouseDown(e, c)}
545
+ >
546
+ ${this._colLabel(c)}
547
+ <span
548
+ class="resize-handle"
549
+ @mousedown=${(e) => this._onResizeStart(e, c)}
550
+ ></span>
551
+ </th>
552
+ `)}
553
+ </tr>
554
+ </thead>
555
+ <tbody>
556
+ ${Array.from({ length: this._rowCount }, (_, r) => html`
557
+ <tr>
558
+ <td
559
+ class="row-num ${this._isRowSelected(r) ? "row-selected" : ""}"
560
+ @mousedown=${(e) => this._onRowHeaderMouseDown(e, r)}
561
+ >${r + 1}</td>
562
+ ${Array.from({ length: this._colCount }, (_2, c) => this._renderCell(r, c))}
563
+ </tr>
564
+ `)}
565
+ </tbody>
566
+ </table>
567
+ </div>
568
+ </div>
569
+ `;
570
+ }
571
+ _renderCell(r, c) {
572
+ const isEditing = this._editing?.row === r && this._editing?.col === c;
573
+ const isSelected = this._inSel(r, c);
574
+ const isAnchor = this._isAnchor(r, c);
575
+ const value = this._data[r]?.[c] ?? "";
576
+ const classes = [
577
+ "cell",
578
+ isSelected ? "selected" : "",
579
+ isAnchor ? "anchor" : "",
580
+ isEditing ? "editing" : ""
581
+ ].filter(Boolean).join(" ");
582
+ return html`
583
+ <td
584
+ class=${classes}
585
+ data-row=${r}
586
+ data-col=${c}
587
+ >
588
+ ${isEditing ? html`
589
+ <input
590
+ class="cell-input"
591
+ .value=${this._editVal}
592
+ @input=${this._onInputChange}
593
+ @keydown=${this._onInputKeyDown}
594
+ @blur=${this._onInputBlur}
595
+ />
596
+ ` : value}
597
+ </td>
598
+ `;
599
+ }
600
+ _selectionToTSV() {
601
+ if (!this._sel) return "";
602
+ const { minRow, maxRow, minCol, maxCol } = normalizeRange(this._sel);
603
+ const rows = [];
604
+ for (let r = minRow; r <= maxRow; r++) {
605
+ const cells = [];
606
+ for (let c = minCol; c <= maxCol; c++) {
607
+ cells.push(this._data[r]?.[c] ?? "");
608
+ }
609
+ rows.push(cells.join(" "));
610
+ }
611
+ return rows.join("\n");
612
+ }
613
+ _pasteFromText(text) {
614
+ if (!this._sel) return;
615
+ const { anchor } = this._sel;
616
+ const pasteRows = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\n$/, "").split("\n").map((row) => row.split(" "));
617
+ const newData = this._data.map((r) => [...r]);
618
+ const neededRows = anchor.row + pasteRows.length;
619
+ const rawNeededCols = anchor.col + Math.max(...pasteRows.map((r) => r.length));
620
+ const neededCols = this.columns?.length ? Math.min(rawNeededCols, this.columns.length) : rawNeededCols;
621
+ while (newData.length < neededRows) {
622
+ newData.push(Array(newData[0]?.length ?? this._colCount).fill(""));
623
+ }
624
+ for (let r = 0; r < newData.length; r++) {
625
+ while (newData[r].length < neededCols) {
626
+ newData[r].push("");
627
+ }
628
+ }
629
+ for (let ri = 0; ri < pasteRows.length; ri++) {
630
+ for (let ci = 0; ci < pasteRows[ri].length; ci++) {
631
+ const r = anchor.row + ri;
632
+ const c = anchor.col + ci;
633
+ if (r < newData.length && c < (newData[r]?.length ?? 0)) {
634
+ newData[r][c] = pasteRows[ri][ci];
635
+ }
636
+ }
637
+ }
638
+ this._data = newData;
639
+ this._pushHistory();
640
+ this._emitChange();
641
+ this.requestUpdate();
642
+ }
643
+ // ──────────────────────────────────────────
644
+ // Fill 기능
645
+ // ──────────────────────────────────────────
646
+ /** Ctrl+D: 선택 영역 첫 행 값을 아래로 채우기 */
647
+ _fillDown() {
648
+ if (!this._sel) return;
649
+ const { minRow, maxRow, minCol, maxCol } = normalizeRange(this._sel);
650
+ if (minRow === maxRow) return;
651
+ const newData = this._data.map((r) => [...r]);
652
+ for (let c = minCol; c <= maxCol; c++) {
653
+ const fillVal = newData[minRow]?.[c] ?? "";
654
+ for (let r = minRow + 1; r <= maxRow; r++) {
655
+ if (newData[r] && c < newData[r].length && !this._isColReadonly(c)) {
656
+ newData[r][c] = fillVal;
657
+ }
658
+ }
659
+ }
660
+ this._data = newData;
661
+ this._pushHistory();
662
+ this._emitChange();
663
+ this.requestUpdate();
664
+ }
665
+ /** Ctrl+R: 선택 영역 첫 열 값을 오른쪽으로 채우기 */
666
+ _fillRight() {
667
+ if (!this._sel) return;
668
+ const { minRow, maxRow, minCol, maxCol } = normalizeRange(this._sel);
669
+ if (minCol === maxCol) return;
670
+ const newData = this._data.map((r) => [...r]);
671
+ for (let r = minRow; r <= maxRow; r++) {
672
+ const fillVal = newData[r]?.[minCol] ?? "";
673
+ for (let c = minCol + 1; c <= maxCol; c++) {
674
+ if (newData[r] && c < newData[r].length && !this._isColReadonly(c)) {
675
+ newData[r][c] = fillVal;
676
+ }
677
+ }
678
+ }
679
+ this._data = newData;
680
+ this._pushHistory();
681
+ this._emitChange();
682
+ this.requestUpdate();
683
+ }
684
+ // ──────────────────────────────────────────
685
+ // 셀 편집
686
+ // ──────────────────────────────────────────
687
+ /**
688
+ * @param typedChar - 타이핑으로 시작할 경우 첫 글자. 미제공시 기존 값 유지.
689
+ */
690
+ _startEdit(row, col, typedChar) {
691
+ this._replaceOnEdit = typedChar !== void 0;
692
+ this._editVal = typedChar !== void 0 ? typedChar : this._data[row]?.[col] ?? "";
693
+ this._editing = { row, col };
694
+ this._select(row, col);
695
+ this.updateComplete.then(() => {
696
+ const input = this.renderRoot.querySelector(".cell-input");
697
+ if (input) {
698
+ input.focus();
699
+ if (this._replaceOnEdit) {
700
+ const len = input.value.length;
701
+ input.setSelectionRange(len, len);
702
+ } else {
703
+ input.select();
704
+ }
705
+ }
706
+ });
707
+ }
708
+ _commitEdit() {
709
+ if (!this._editing) return;
710
+ const { row, col } = this._editing;
711
+ const prevVal = this._data[row]?.[col] ?? "";
712
+ const newData = this._data.map((r) => [...r]);
713
+ newData[row][col] = this._editVal;
714
+ this._data = newData;
715
+ this._editing = null;
716
+ if (this._editVal !== prevVal) {
717
+ this._pushHistory();
718
+ this._emitChange();
719
+ }
720
+ this.requestUpdate();
721
+ this._containerEl?.focus();
722
+ }
723
+ _cancelEdit() {
724
+ this._editing = null;
725
+ this.requestUpdate();
726
+ this._containerEl?.focus();
727
+ }
728
+ _clearSelection() {
729
+ if (!this._sel) return;
730
+ const { minRow, maxRow, minCol, maxCol } = normalizeRange(this._sel);
731
+ const newData = this._data.map((r) => [...r]);
732
+ for (let r = minRow; r <= maxRow; r++) {
733
+ for (let c = minCol; c <= maxCol; c++) {
734
+ if (!this._isColReadonly(c)) newData[r][c] = "";
735
+ }
736
+ }
737
+ this._data = newData;
738
+ this._pushHistory();
739
+ this._emitChange();
740
+ this.requestUpdate();
741
+ }
742
+ // ──────────────────────────────────────────
743
+ // 유틸리티
744
+ // ──────────────────────────────────────────
745
+ _select(row, col) {
746
+ this._sel = { anchor: { row, col }, focus: { row, col } };
747
+ this.requestUpdate();
748
+ this._scrollCellIntoView(row, col);
749
+ }
750
+ _scrollCellIntoView(row, col) {
751
+ this.updateComplete.then(() => {
752
+ const td = this.renderRoot.querySelector(
753
+ `td[data-row="${row}"][data-col="${col}"]`
754
+ );
755
+ td?.scrollIntoView({ block: "nearest", inline: "nearest" });
756
+ });
757
+ }
758
+ _getCellFromEvent(e) {
759
+ const target = e.target.closest("td[data-row]");
760
+ if (!target) return null;
761
+ const row = parseInt(target.dataset.row ?? "-1");
762
+ const col = parseInt(target.dataset.col ?? "-1");
763
+ if (row < 0 || col < 0) return null;
764
+ return { row, col };
765
+ }
766
+ _isColReadonly(col) {
767
+ return this.columns?.[col]?.readonly ?? false;
768
+ }
769
+ _emitChange() {
770
+ this.emit("change", { data: this._data });
771
+ }
772
+ // ──────────────────────────────────────────
773
+ // 공개 API
774
+ // ──────────────────────────────────────────
775
+ /** 현재 데이터를 2D 배열로 반환 */
776
+ getData() {
777
+ return this._data.map((r) => [...r]);
778
+ }
779
+ /**
780
+ * 정의된 columns의 key를 기준으로 객체 배열로 반환.
781
+ * columns 미설정시 A, B, C... 키를 사용.
782
+ */
783
+ getDataAsObjects() {
784
+ if (!this.columns?.length) {
785
+ return this._data.map(
786
+ (row) => Object.fromEntries(row.map((v, i) => [this._colLabel(i), v]))
787
+ );
788
+ }
789
+ return this._data.map(
790
+ (row) => Object.fromEntries(
791
+ this.columns.map((col, i) => [col.key ?? this._colLabel(i), row[i] ?? ""])
792
+ )
793
+ );
794
+ }
795
+ /** 데이터를 직접 설정하고 재렌더링 */
796
+ setData(data) {
797
+ this.data = data;
798
+ this._syncData();
799
+ this.requestUpdate();
800
+ }
801
+ /** 특정 셀 값 설정 */
802
+ setCell(row, col, value) {
803
+ if (row >= 0 && row < this._data.length && col >= 0 && col < this._colCount) {
804
+ const newData = this._data.map((r) => [...r]);
805
+ newData[row][col] = value;
806
+ this._data = newData;
807
+ this._pushHistory();
808
+ this._emitChange();
809
+ this.requestUpdate();
810
+ }
811
+ }
812
+ /** 현재 선택 영역 반환 */
813
+ getSelection() {
814
+ if (!this._sel) return null;
815
+ return normalizeRange(this._sel);
816
+ }
817
+ /** Undo 가능 여부 */
818
+ get canUndo() {
819
+ return this._historyIndex > 0;
820
+ }
821
+ /** Redo 가능 여부 */
822
+ get canRedo() {
823
+ return this._historyIndex < this._history.length - 1;
824
+ }
825
+ };
826
+ __decorateClass([
827
+ property({ type: Array })
828
+ ], _USimpleSheet.prototype, "data");
829
+ __decorateClass([
830
+ property({ type: Array })
831
+ ], _USimpleSheet.prototype, "columns");
832
+ __decorateClass([
833
+ property({ type: Number })
834
+ ], _USimpleSheet.prototype, "rows");
835
+ __decorateClass([
836
+ property({ type: Number })
837
+ ], _USimpleSheet.prototype, "cols");
838
+ __decorateClass([
839
+ property({ type: Boolean })
840
+ ], _USimpleSheet.prototype, "readonly");
841
+ __decorateClass([
842
+ state()
843
+ ], _USimpleSheet.prototype, "_sel");
844
+ __decorateClass([
845
+ state()
846
+ ], _USimpleSheet.prototype, "_editing");
847
+ __decorateClass([
848
+ state()
849
+ ], _USimpleSheet.prototype, "_editVal");
850
+ __decorateClass([
851
+ state()
852
+ ], _USimpleSheet.prototype, "_replaceOnEdit");
853
+ let USimpleSheet = _USimpleSheet;
854
+
855
+ export { USimpleSheet };