@iyulab/data-components 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
데이터 표시 및 입력을 위한 웹 컴포넌트 라이브러리.
|
|
4
4
|
|
|
5
|
+
**[Live Demo](https://iyulab.github.io/node-data-components/)**
|
|
6
|
+
|
|
5
7
|
- **USimpleSheet** — 엑셀 호환 스프레드시트 입력 컴포넌트 (Lit)
|
|
6
8
|
- **UDataView** — Grid / List / Table 뷰 전환 컴포넌트 (Lit)
|
|
7
9
|
- **UDataGrid** — OData 기반 서버 사이드 데이터 그리드 (React + DevExtreme)
|
|
@@ -9,6 +9,10 @@ export interface SheetColumn {
|
|
|
9
9
|
width?: number;
|
|
10
10
|
/** 읽기 전용 열 */
|
|
11
11
|
readonly?: boolean;
|
|
12
|
+
/** 선택 가능한 옵션 목록 (정적 배열 또는 동적 콜백) */
|
|
13
|
+
options?: string[] | ((row: number, col: number) => string[]);
|
|
14
|
+
/** true이면 목록에 있는 값만 입력 가능 (기본: false = 자유 입력 허용) */
|
|
15
|
+
strict?: boolean;
|
|
12
16
|
}
|
|
13
17
|
/**
|
|
14
18
|
* USimpleSheet - 심플 스프레드시트 컴포넌트
|
|
@@ -44,6 +48,9 @@ export declare class USimpleSheet extends UElement {
|
|
|
44
48
|
private _editing;
|
|
45
49
|
private _editVal;
|
|
46
50
|
private _replaceOnEdit;
|
|
51
|
+
private _dropdownItems;
|
|
52
|
+
private _dropdownIndex;
|
|
53
|
+
private _isDropdownClick;
|
|
47
54
|
private _data;
|
|
48
55
|
private _colWidths;
|
|
49
56
|
private _isMouseSelecting;
|
|
@@ -79,6 +86,7 @@ export declare class USimpleSheet extends UElement {
|
|
|
79
86
|
private _onInputChange;
|
|
80
87
|
private _onInputKeyDown;
|
|
81
88
|
private _onInputBlur;
|
|
89
|
+
private _onDropdownItemMouseDown;
|
|
82
90
|
private _onTableMouseDown;
|
|
83
91
|
private _onTableMouseOver;
|
|
84
92
|
private _onTableDblClick;
|
|
@@ -112,6 +120,8 @@ export declare class USimpleSheet extends UElement {
|
|
|
112
120
|
private _scrollCellIntoView;
|
|
113
121
|
private _getCellFromEvent;
|
|
114
122
|
private _isColReadonly;
|
|
123
|
+
private _getColOptions;
|
|
124
|
+
private _filterOptions;
|
|
115
125
|
private _emitChange;
|
|
116
126
|
/** 현재 데이터를 2D 배열로 반환 */
|
|
117
127
|
getData(): string[][];
|
|
@@ -32,6 +32,9 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
32
32
|
this._editing = null;
|
|
33
33
|
this._editVal = "";
|
|
34
34
|
this._replaceOnEdit = false;
|
|
35
|
+
this._dropdownItems = [];
|
|
36
|
+
this._dropdownIndex = -1;
|
|
37
|
+
this._isDropdownClick = false;
|
|
35
38
|
// @state() 없음 — 직접 requestUpdate() 호출
|
|
36
39
|
this._data = [];
|
|
37
40
|
this._colWidths = [];
|
|
@@ -180,6 +183,7 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
180
183
|
return;
|
|
181
184
|
}
|
|
182
185
|
if (!this.readonly && !this._isColReadonly(anchor.col) && e.key.length === 1 && !e.ctrlKey && !e.metaKey) {
|
|
186
|
+
e.preventDefault();
|
|
183
187
|
this._startEdit(anchor.row, anchor.col, e.key);
|
|
184
188
|
}
|
|
185
189
|
};
|
|
@@ -188,14 +192,46 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
188
192
|
// ──────────────────────────────────────────
|
|
189
193
|
this._onInputChange = (e) => {
|
|
190
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
|
+
}
|
|
191
203
|
};
|
|
192
204
|
this._onInputKeyDown = (e) => {
|
|
193
205
|
e.stopPropagation();
|
|
206
|
+
const hasDropdown = this._dropdownItems.length > 0;
|
|
194
207
|
if (e.key === "Escape") {
|
|
195
208
|
e.preventDefault();
|
|
196
209
|
this._cancelEdit();
|
|
197
210
|
return;
|
|
198
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
|
+
}
|
|
199
235
|
if (e.key === "Enter") {
|
|
200
236
|
e.preventDefault();
|
|
201
237
|
const pos = this._editing;
|
|
@@ -248,10 +284,20 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
248
284
|
}
|
|
249
285
|
};
|
|
250
286
|
this._onInputBlur = () => {
|
|
287
|
+
if (this._isDropdownClick) {
|
|
288
|
+
this._isDropdownClick = false;
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
251
291
|
if (this._editing) {
|
|
252
292
|
this._commitEdit();
|
|
253
293
|
}
|
|
254
294
|
};
|
|
295
|
+
this._onDropdownItemMouseDown = (e, value) => {
|
|
296
|
+
e.preventDefault();
|
|
297
|
+
this._isDropdownClick = true;
|
|
298
|
+
this._editVal = value;
|
|
299
|
+
this._commitEdit();
|
|
300
|
+
};
|
|
255
301
|
// ──────────────────────────────────────────
|
|
256
302
|
// 마우스 이벤트
|
|
257
303
|
// ──────────────────────────────────────────
|
|
@@ -579,6 +625,10 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
579
625
|
isAnchor ? "anchor" : "",
|
|
580
626
|
isEditing ? "editing" : ""
|
|
581
627
|
].filter(Boolean).join(" ");
|
|
628
|
+
const hasOptions = isEditing && this._getColOptions(r, c) !== null;
|
|
629
|
+
const showDropdown = isEditing && this._dropdownItems.length > 0;
|
|
630
|
+
const isStrict = this.columns?.[c]?.strict ?? false;
|
|
631
|
+
const noMatch = isEditing && hasOptions && this._dropdownItems.length === 0 && this._editVal !== "";
|
|
582
632
|
return html`
|
|
583
633
|
<td
|
|
584
634
|
class=${classes}
|
|
@@ -593,6 +643,20 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
593
643
|
@keydown=${this._onInputKeyDown}
|
|
594
644
|
@blur=${this._onInputBlur}
|
|
595
645
|
/>
|
|
646
|
+
${showDropdown ? html`
|
|
647
|
+
<div class="cell-dropdown">
|
|
648
|
+
${this._dropdownItems.map((item, i) => html`
|
|
649
|
+
<div
|
|
650
|
+
class="dropdown-item ${i === this._dropdownIndex ? "highlighted" : ""}"
|
|
651
|
+
@mousedown=${(e) => this._onDropdownItemMouseDown(e, item)}
|
|
652
|
+
>${item}</div>
|
|
653
|
+
`)}
|
|
654
|
+
</div>
|
|
655
|
+
` : noMatch && isStrict ? html`
|
|
656
|
+
<div class="cell-dropdown">
|
|
657
|
+
<div class="dropdown-empty">일치하는 항목 없음</div>
|
|
658
|
+
</div>
|
|
659
|
+
` : ""}
|
|
596
660
|
` : value}
|
|
597
661
|
</td>
|
|
598
662
|
`;
|
|
@@ -692,6 +756,15 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
692
756
|
this._editVal = typedChar !== void 0 ? typedChar : this._data[row]?.[col] ?? "";
|
|
693
757
|
this._editing = { row, col };
|
|
694
758
|
this._select(row, col);
|
|
759
|
+
const options = this._getColOptions(row, col);
|
|
760
|
+
if (options) {
|
|
761
|
+
const filtered = this._filterOptions(options, this._editVal);
|
|
762
|
+
this._dropdownItems = filtered;
|
|
763
|
+
this._dropdownIndex = filtered.length > 0 ? 0 : -1;
|
|
764
|
+
} else {
|
|
765
|
+
this._dropdownItems = [];
|
|
766
|
+
this._dropdownIndex = -1;
|
|
767
|
+
}
|
|
695
768
|
this.updateComplete.then(() => {
|
|
696
769
|
const input = this.renderRoot.querySelector(".cell-input");
|
|
697
770
|
if (input) {
|
|
@@ -708,11 +781,26 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
708
781
|
_commitEdit() {
|
|
709
782
|
if (!this._editing) return;
|
|
710
783
|
const { row, col } = this._editing;
|
|
784
|
+
const colDef = this.columns?.[col];
|
|
785
|
+
if (colDef?.strict && colDef?.options && this._editVal !== "") {
|
|
786
|
+
const allOptions = this._getColOptions(row, col) ?? [];
|
|
787
|
+
if (!allOptions.includes(this._editVal)) {
|
|
788
|
+
this._editing = null;
|
|
789
|
+
this._dropdownItems = [];
|
|
790
|
+
this._dropdownIndex = -1;
|
|
791
|
+
this._isDropdownClick = false;
|
|
792
|
+
this.requestUpdate();
|
|
793
|
+
this._containerEl?.focus();
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
711
797
|
const prevVal = this._data[row]?.[col] ?? "";
|
|
712
798
|
const newData = this._data.map((r) => [...r]);
|
|
713
799
|
newData[row][col] = this._editVal;
|
|
714
800
|
this._data = newData;
|
|
715
801
|
this._editing = null;
|
|
802
|
+
this._dropdownItems = [];
|
|
803
|
+
this._dropdownIndex = -1;
|
|
716
804
|
if (this._editVal !== prevVal) {
|
|
717
805
|
this._pushHistory();
|
|
718
806
|
this._emitChange();
|
|
@@ -722,6 +810,8 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
722
810
|
}
|
|
723
811
|
_cancelEdit() {
|
|
724
812
|
this._editing = null;
|
|
813
|
+
this._dropdownItems = [];
|
|
814
|
+
this._dropdownIndex = -1;
|
|
725
815
|
this.requestUpdate();
|
|
726
816
|
this._containerEl?.focus();
|
|
727
817
|
}
|
|
@@ -766,6 +856,16 @@ const _USimpleSheet = class _USimpleSheet extends UElement {
|
|
|
766
856
|
_isColReadonly(col) {
|
|
767
857
|
return this.columns?.[col]?.readonly ?? false;
|
|
768
858
|
}
|
|
859
|
+
_getColOptions(row, col) {
|
|
860
|
+
const colDef = this.columns?.[col];
|
|
861
|
+
if (!colDef?.options) return null;
|
|
862
|
+
return typeof colDef.options === "function" ? colDef.options(row, col) : colDef.options;
|
|
863
|
+
}
|
|
864
|
+
_filterOptions(options, query) {
|
|
865
|
+
if (!query) return options;
|
|
866
|
+
const lower = query.toLowerCase();
|
|
867
|
+
return options.filter((o) => o.toLowerCase().includes(lower));
|
|
868
|
+
}
|
|
769
869
|
_emitChange() {
|
|
770
870
|
this.emit("change", { data: this._data });
|
|
771
871
|
}
|
|
@@ -850,6 +950,12 @@ __decorateClass([
|
|
|
850
950
|
__decorateClass([
|
|
851
951
|
state()
|
|
852
952
|
], _USimpleSheet.prototype, "_replaceOnEdit");
|
|
953
|
+
__decorateClass([
|
|
954
|
+
state()
|
|
955
|
+
], _USimpleSheet.prototype, "_dropdownItems");
|
|
956
|
+
__decorateClass([
|
|
957
|
+
state()
|
|
958
|
+
], _USimpleSheet.prototype, "_dropdownIndex");
|
|
853
959
|
let USimpleSheet = _USimpleSheet;
|
|
854
960
|
|
|
855
961
|
export { USimpleSheet };
|
|
@@ -197,6 +197,49 @@ const styles = css`
|
|
|
197
197
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
/* Dropdown overlay */
|
|
201
|
+
.cell-dropdown {
|
|
202
|
+
position: absolute;
|
|
203
|
+
top: 100%;
|
|
204
|
+
left: -1px;
|
|
205
|
+
width: calc(100% + 2px);
|
|
206
|
+
min-width: 120px;
|
|
207
|
+
max-height: 200px;
|
|
208
|
+
overflow-y: auto;
|
|
209
|
+
background: var(--u-bg-color, #fff);
|
|
210
|
+
border: 1px solid var(--u-border-color, #e2e8f0);
|
|
211
|
+
border-top: none;
|
|
212
|
+
border-radius: 0 0 4px 4px;
|
|
213
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
214
|
+
z-index: 20;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.dropdown-item {
|
|
218
|
+
padding: 4px 8px;
|
|
219
|
+
font-size: 13px;
|
|
220
|
+
color: var(--u-txt-color, #0f172a);
|
|
221
|
+
cursor: pointer;
|
|
222
|
+
white-space: nowrap;
|
|
223
|
+
overflow: hidden;
|
|
224
|
+
text-overflow: ellipsis;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.dropdown-item:hover {
|
|
228
|
+
background: var(--u-neutral-100, #f1f5f9);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.dropdown-item.highlighted {
|
|
232
|
+
background: var(--u-blue-50, #eff6ff);
|
|
233
|
+
color: var(--u-blue-800, #1e40af);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.dropdown-empty {
|
|
237
|
+
padding: 6px 8px;
|
|
238
|
+
font-size: 12px;
|
|
239
|
+
color: var(--u-txt-color-weak, #64748b);
|
|
240
|
+
font-style: italic;
|
|
241
|
+
}
|
|
242
|
+
|
|
200
243
|
/* Readonly mode */
|
|
201
244
|
:host([readonly]) .cell {
|
|
202
245
|
cursor: default;
|
|
@@ -273,6 +316,29 @@ const styles = css`
|
|
|
273
316
|
outline-color: var(--u-blue-500, #6ba3e3);
|
|
274
317
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.5);
|
|
275
318
|
}
|
|
319
|
+
|
|
320
|
+
:host-context([theme="dark"]) .cell-dropdown {
|
|
321
|
+
background: var(--u-bg-color, #121212);
|
|
322
|
+
border-color: var(--u-border-color, #3D3D3D);
|
|
323
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
:host-context([theme="dark"]) .dropdown-item {
|
|
327
|
+
color: var(--u-txt-color, #D4D4D4);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
:host-context([theme="dark"]) .dropdown-item:hover {
|
|
331
|
+
background: var(--u-neutral-100, #1E1E1E);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
:host-context([theme="dark"]) .dropdown-item.highlighted {
|
|
335
|
+
background: var(--u-blue-100, #1e3a5f);
|
|
336
|
+
color: var(--u-blue-800, #c2deff);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
:host-context([theme="dark"]) .dropdown-empty {
|
|
340
|
+
color: var(--u-txt-color-weak, #8A8A8A);
|
|
341
|
+
}
|
|
276
342
|
`;
|
|
277
343
|
|
|
278
344
|
export { styles };
|