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