@operato/data-grist 8.2.20 → 8.2.23

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 (35) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/src/data-grid/data-grid-body-style.js +1 -1
  3. package/dist/src/data-grid/data-grid-body-style.js.map +1 -1
  4. package/dist/src/data-grid/data-grid-body.d.ts +16 -1
  5. package/dist/src/data-grid/data-grid-body.js +292 -49
  6. package/dist/src/data-grid/data-grid-body.js.map +1 -1
  7. package/dist/src/data-grid/event-handlers/data-grid-body-click-handler.js +14 -14
  8. package/dist/src/data-grid/event-handlers/data-grid-body-click-handler.js.map +1 -1
  9. package/dist/src/data-grid/event-handlers/data-grid-body-keydown-handler.js +39 -5
  10. package/dist/src/data-grid/event-handlers/data-grid-body-keydown-handler.js.map +1 -1
  11. package/dist/src/editors/ox-grist-editor.js +3 -0
  12. package/dist/src/editors/ox-grist-editor.js.map +1 -1
  13. package/dist/tsconfig.tsbuildinfo +1 -1
  14. package/package.json +2 -2
  15. package/dist/src/editors/ox-grist-editor-json5.d.ts +0 -8
  16. package/dist/src/editors/ox-grist-editor-json5.js +0 -66
  17. package/dist/src/editors/ox-grist-editor-json5.js.map +0 -1
  18. package/dist/src/editors/ox-grist-editor-secret.d.ts +0 -5
  19. package/dist/src/editors/ox-grist-editor-secret.js +0 -23
  20. package/dist/src/editors/ox-grist-editor-secret.js.map +0 -1
  21. package/dist/src/editors/ox-grist-editor-timezone.d.ts +0 -6
  22. package/dist/src/editors/ox-grist-editor-timezone.js +0 -41
  23. package/dist/src/editors/ox-grist-editor-timezone.js.map +0 -1
  24. package/dist/src/editors/ox-popup-code-input.d.ts +0 -14
  25. package/dist/src/editors/ox-popup-code-input.js +0 -78
  26. package/dist/src/editors/ox-popup-code-input.js.map +0 -1
  27. package/dist/src/renderers/ox-grist-renderer-secret.d.ts +0 -2
  28. package/dist/src/renderers/ox-grist-renderer-secret.js +0 -77
  29. package/dist/src/renderers/ox-grist-renderer-secret.js.map +0 -1
  30. package/dist/stories/append-position.stories.d.ts +0 -8
  31. package/dist/stories/append-position.stories.js +0 -183
  32. package/dist/stories/append-position.stories.js.map +0 -1
  33. package/dist/stories/secret.stories.d.ts +0 -37
  34. package/dist/stories/secret.stories.js +0 -219
  35. package/dist/stories/secret.stories.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -3,6 +3,33 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ ### [8.2.23](https://github.com/hatiolab/operato/compare/v8.2.22...v8.2.23) (2026-04-14)
7
+
8
+
9
+ ### :bug: Bug Fix
10
+
11
+ * 모바일 포커스 시 바로 선택이 아니라 드래그 인지 판단 ([3c4b89c](https://github.com/hatiolab/operato/commit/3c4b89c301200349ab779f3bc74cf8169b807264))
12
+ * 테블릿에서 롱 클릭시 복사 나오도록 수정 ([ad300b7](https://github.com/hatiolab/operato/commit/ad300b7c9bdc79bf5d892211e9cb84f273662def))
13
+ * 포커스시 textarea 클릭되는 현상 수정 ([fa54e2c](https://github.com/hatiolab/operato/commit/fa54e2cbdbf4fff6488b9610f18839f91708ce40))
14
+
15
+
16
+
17
+ ### [8.2.22](https://github.com/hatiolab/operato/compare/v8.2.21...v8.2.22) (2026-04-10)
18
+
19
+
20
+ ### :bug: Bug Fix
21
+
22
+ * 그리드 공백 2개일때 1개로 인식 현상 수정 ([8a4c2cd](https://github.com/hatiolab/operato/commit/8a4c2cd18113b32be2826217b1ff247b4da9c9b4))
23
+ * 그리드 복붙 renderer 케이스 추가 ([61dfd01](https://github.com/hatiolab/operato/commit/61dfd013abb7ac11b68ff8936aa1d2cff614f438))
24
+ * 그리드 복붙시 태그 제거 ([4a8fdc1](https://github.com/hatiolab/operato/commit/4a8fdc1c4198a7bb61afc77a8cafde3adfba453d))
25
+ * 그리드 복붙시 태그 제거 ([a2c572c](https://github.com/hatiolab/operato/commit/a2c572c6f056744f1599bc61967364e273b77236))
26
+ * 그리드 셀 ime-buffer로 한글 입력시 composing 유지 ([7fbcc88](https://github.com/hatiolab/operato/commit/7fbcc880ca80ad65860f5ef36fc54348999e85c3))
27
+ * 그리드 셀 ime-buffer로 한글 입력시 composing 유지 ([690464f](https://github.com/hatiolab/operato/commit/690464f0ae8dddbf94612c573a6a6be8dc51fcea))
28
+ * 복사후 그리드 포커스 빠짐 현상 수정 ([e020535](https://github.com/hatiolab/operato/commit/e020535cf370f659549f81521b06a0c9ffa8523e))
29
+ * 체리픽 보정 ([5bbf3da](https://github.com/hatiolab/operato/commit/5bbf3da644f222212c70e451b84d03dd4e98e73f))
30
+
31
+
32
+
6
33
  ### [8.2.20](https://github.com/hatiolab/operato/compare/v8.2.19...v8.2.20) (2026-02-09)
7
34
 
8
35
 
@@ -53,7 +53,7 @@ export const dataGridBodyStyle = css `
53
53
  color: var(--grid-record-emphasized-color) !important;
54
54
  }
55
55
 
56
- [editing] {
56
+ ox-grid-field[editing] {
57
57
  background-color: var(--grid-record-editing-background-color);
58
58
  }
59
59
 
@@ -1 +1 @@
1
- {"version":3,"file":"data-grid-body-style.js","sourceRoot":"","sources":["../../../src/data-grid/data-grid-body-style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgGnC,CAAA","sourcesContent":["import { css } from 'lit'\n\nexport const dataGridBodyStyle = css`\n :host {\n display: grid;\n grid-template-columns: var(--grid-template-columns);\n grid-auto-rows: var(--grid-record-height, min-content);\n\n overflow: auto;\n outline: none;\n color: var(--grid-record-color);\n position: relative;\n border-bottom: var(--grid-body-bottom-border);\n }\n\n ox-grid-field[odd] {\n background-color: var(--grid-record-odd-background-color);\n }\n\n ox-grid-field[disabled] {\n background-color: var(--grid-record-disabled-background-color, var(--grid-record-background-color));\n color: var(--grid-record-disabled-color, var(--grid-record-color));\n opacity: var(--grid-record-disabled-opacity, 1);\n }\n ox-grid-field[odd][disabled] {\n background-color: var(\n --grid-odd-record-disabled-background-color,\n var(--grid-record-disabled-background-color, var(--grid-record-odd-background-color))\n );\n color: var(--grid-odd-record-disabled-color, var(--grid-record-disabled-color, var(--grid-record-color)));\n opacity: var(--grid-odd-record-disabled-opacity, var(--grid-record-disabled-opacity, 1));\n }\n\n ox-grid-field[selected-row] {\n background-color: var(--grid-record-selected-background-color);\n color: var(--grid-record-selected-color);\n }\n\n ox-grid-field[focused-row] {\n box-shadow: var(--grid-record-focused-box-shadow);\n font-weight: bold;\n color: var(--grid-record-focused-color);\n background-image: var(--focused-background-image);\n background-blend-mode: darken;\n }\n\n ox-grid-field[focused] {\n border: var(--grid-record-focused-cell-border);\n }\n\n ox-grid-field[emphasized-row],\n ox-grid-field[emphasized-row][focused] {\n background-color: var(--grid-record-emphasized-background-color) !important;\n color: var(--grid-record-emphasized-color) !important;\n }\n\n [editing] {\n background-color: var(--grid-record-editing-background-color);\n }\n\n @media print {\n :host {\n grid-template-columns: var(--grid-template-print-columns);\n }\n ox-grid-field[focused] {\n border: none;\n }\n\n ox-grid-field[selected-row] {\n background-color: transparent;\n }\n\n ox-grid-field[emphasized-row] {\n background-color: transparent;\n color: initial;\n }\n\n ox-grid-field[focused-row] {\n background-color: transparent;\n color: initial;\n }\n\n ox-grid-field[editing] {\n background-color: transparent;\n }\n }\n\n @media (prefers-color-scheme: dark) {\n ox-grid-field[focused-row] {\n background-blend-mode: lighten;\n }\n }\n\n @media (prefers-color-scheme: light) {\n ox-grid-field[focused-row] {\n background-blend-mode: darken;\n }\n }\n`\n"]}
1
+ {"version":3,"file":"data-grid-body-style.js","sourceRoot":"","sources":["../../../src/data-grid/data-grid-body-style.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,KAAK,CAAA;AAEzB,MAAM,CAAC,MAAM,iBAAiB,GAAG,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgGnC,CAAA","sourcesContent":["import { css } from 'lit'\n\nexport const dataGridBodyStyle = css`\n :host {\n display: grid;\n grid-template-columns: var(--grid-template-columns);\n grid-auto-rows: var(--grid-record-height, min-content);\n\n overflow: auto;\n outline: none;\n color: var(--grid-record-color);\n position: relative;\n border-bottom: var(--grid-body-bottom-border);\n }\n\n ox-grid-field[odd] {\n background-color: var(--grid-record-odd-background-color);\n }\n\n ox-grid-field[disabled] {\n background-color: var(--grid-record-disabled-background-color, var(--grid-record-background-color));\n color: var(--grid-record-disabled-color, var(--grid-record-color));\n opacity: var(--grid-record-disabled-opacity, 1);\n }\n ox-grid-field[odd][disabled] {\n background-color: var(\n --grid-odd-record-disabled-background-color,\n var(--grid-record-disabled-background-color, var(--grid-record-odd-background-color))\n );\n color: var(--grid-odd-record-disabled-color, var(--grid-record-disabled-color, var(--grid-record-color)));\n opacity: var(--grid-odd-record-disabled-opacity, var(--grid-record-disabled-opacity, 1));\n }\n\n ox-grid-field[selected-row] {\n background-color: var(--grid-record-selected-background-color);\n color: var(--grid-record-selected-color);\n }\n\n ox-grid-field[focused-row] {\n box-shadow: var(--grid-record-focused-box-shadow);\n font-weight: bold;\n color: var(--grid-record-focused-color);\n background-image: var(--focused-background-image);\n background-blend-mode: darken;\n }\n\n ox-grid-field[focused] {\n border: var(--grid-record-focused-cell-border);\n }\n\n ox-grid-field[emphasized-row],\n ox-grid-field[emphasized-row][focused] {\n background-color: var(--grid-record-emphasized-background-color) !important;\n color: var(--grid-record-emphasized-color) !important;\n }\n\n ox-grid-field[editing] {\n background-color: var(--grid-record-editing-background-color);\n }\n\n @media print {\n :host {\n grid-template-columns: var(--grid-template-print-columns);\n }\n ox-grid-field[focused] {\n border: none;\n }\n\n ox-grid-field[selected-row] {\n background-color: transparent;\n }\n\n ox-grid-field[emphasized-row] {\n background-color: transparent;\n color: initial;\n }\n\n ox-grid-field[focused-row] {\n background-color: transparent;\n color: initial;\n }\n\n ox-grid-field[editing] {\n background-color: transparent;\n }\n }\n\n @media (prefers-color-scheme: dark) {\n ox-grid-field[focused-row] {\n background-blend-mode: lighten;\n }\n }\n\n @media (prefers-color-scheme: light) {\n ox-grid-field[focused-row] {\n background-blend-mode: darken;\n }\n }\n`\n"]}
@@ -32,6 +32,10 @@ export declare class DataGridBody extends LitElement {
32
32
  private _recordViewRow?;
33
33
  private _draggable?;
34
34
  private _gridElement?;
35
+ private _imeBuffer?;
36
+ _imeComposing: boolean;
37
+ _imeEditing: boolean;
38
+ _lastPointerTouch: boolean;
35
39
  resetEdit(): void;
36
40
  handleOnScroll(e: WheelEvent): void;
37
41
  render(): import("lit-html").TemplateResult<1>;
@@ -48,9 +52,20 @@ export declare class DataGridBody extends LitElement {
48
52
  row: number;
49
53
  record: GristRecord;
50
54
  }): void;
51
- getSelectedBlockValues(): Array<Array<any>> | any | undefined;
55
+ getSelectedBlockValues(): {
56
+ html: string;
57
+ text: string;
58
+ } | undefined;
52
59
  copy(): Promise<void>;
53
60
  paste(): Promise<void>;
54
61
  setSelectBlock(start?: DataGridField, end?: DataGridField): void;
55
62
  buildAccumulatorRecord(): GristRecord;
63
+ /** IME buffer를 포커스된 셀 위에 위치시킨다 */
64
+ private _positionIMEBuffer;
65
+ /** IME buffer 값을 field-change 이벤트로 커밋한다 */
66
+ _commitIMEBuffer(row: number, column: number): void;
67
+ /** IME buffer 취소 (값 버리기) */
68
+ _discardIMEBuffer(): void;
69
+ /** IME buffer 초기화 */
70
+ private _resetIMEBuffer;
56
71
  }
@@ -55,6 +55,9 @@ let DataGridBody = class DataGridBody extends LitElement {
55
55
  this.from = -1;
56
56
  this.to = -1;
57
57
  this.fixedLefts = [];
58
+ this._imeComposing = false;
59
+ this._imeEditing = false;
60
+ this._lastPointerTouch = false;
58
61
  // 검색시 스크롤 맨 위로
59
62
  this._onFetchParamsChange = () => {
60
63
  this.scrollTop = 0;
@@ -156,6 +159,7 @@ let DataGridBody = class DataGridBody extends LitElement {
156
159
  `
157
160
  : nothing}
158
161
  ${start && end && start !== end ? html ` <div select-block></div> ` : html ``}
162
+ <input id="ime-buffer" type="text" />
159
163
  <slot></slot>
160
164
  `;
161
165
  }
@@ -164,12 +168,43 @@ let DataGridBody = class DataGridBody extends LitElement {
164
168
  // this.addEventListener('scroll', this.handleOnScroll.bind(this))
165
169
  /* focus() 를 받을 수 있도록 함. */
166
170
  this.setAttribute('tabindex', '-1');
171
+ /* 한글 IME composition을 위한 숨겨진 input 버퍼 설정 */
172
+ const imeBuffer = this.renderRoot.querySelector('#ime-buffer');
173
+ this._imeBuffer = imeBuffer;
174
+ imeBuffer.addEventListener('compositionstart', () => {
175
+ this._imeComposing = true;
176
+ this._imeEditing = true;
177
+ imeBuffer.classList.add('active');
178
+ this._positionIMEBuffer();
179
+ });
180
+ imeBuffer.addEventListener('compositionend', () => {
181
+ this._imeComposing = false;
182
+ });
183
+ imeBuffer.addEventListener('input', () => {
184
+ if (!this._imeComposing && !this._imeEditing && imeBuffer.value) {
185
+ /* 영어/숫자: 실제 값이 입력된 경우에만 활성화 */
186
+ this._imeEditing = true;
187
+ imeBuffer.classList.add('active');
188
+ this._positionIMEBuffer();
189
+ }
190
+ });
167
191
  /*
168
192
  * focusout 으로 property를 변경시키는 경우, focusout에 의해 update가 발생하는 경우에는,
169
193
  * 그리드 내부의 컴포넌트가 갱신되는 현상을 초래하게 된다.
170
194
  * 따라서, focusout 핸들러에서 update를 유발하는 코드는 강력하게 금지시킨다.
171
195
  */
172
196
  this.addEventListener('focusout', e => {
197
+ var _a;
198
+ /* IME buffer로 포커스가 이동하는 경우 keydown listener 유지 */
199
+ const related = e.relatedTarget;
200
+ if (related && ((_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.contains(related))) {
201
+ return;
202
+ }
203
+ /* 그리드 외부로 포커스 이동 시 IME buffer 커밋 */
204
+ if (this._imeEditing) {
205
+ const { row = 0, column = 0 } = this.focused || {};
206
+ this._commitIMEBuffer(row, column);
207
+ }
173
208
  if (this._focusedListener) {
174
209
  this.removeEventListener('keydown', this._focusedListener);
175
210
  delete this._focusedListener;
@@ -203,12 +238,26 @@ let DataGridBody = class DataGridBody extends LitElement {
203
238
  }
204
239
  }));
205
240
  });
206
- this.renderRoot.addEventListener('mousedown', (event) => {
207
- const e = event;
241
+ this.renderRoot.addEventListener('pointerdown', (e) => {
242
+ const pe = e;
243
+ const isTouch = pe.pointerType === 'touch';
244
+ /* 터치 이벤트 여부 기록 — 태블릿에서 input focus로 가상 키보드가 뜨는 것을 방지 */
245
+ this._lastPointerTouch = isTouch;
246
+ /* 클릭 시 IME buffer가 활성 상태면 먼저 커밋 */
247
+ if (this._imeEditing) {
248
+ const { row = 0, column = 0 } = this.focused || {};
249
+ this._commitIMEBuffer(row, column);
250
+ }
208
251
  this.setSelectBlock();
209
- if (e.buttons !== 1) {
252
+ if (isTouch) {
253
+ /* 터치: click 이벤트에서 처리 (스크롤 시에는 click이 발생하지 않음) */
210
254
  return;
211
255
  }
256
+ if ('buttons' in e && pe.buttons !== 1) {
257
+ return;
258
+ }
259
+ e.preventDefault();
260
+ e.stopPropagation();
212
261
  this._draggable = true;
213
262
  var target = e.target.closest('ox-grid-field');
214
263
  var { rowIndex, columnIndex } = target || {};
@@ -224,11 +273,13 @@ let DataGridBody = class DataGridBody extends LitElement {
224
273
  this.startEditTarget(rowIndex, columnIndex);
225
274
  }
226
275
  });
227
- this.renderRoot.addEventListener('mousemove', (event) => {
276
+ this.renderRoot.addEventListener('pointermove', (event) => {
228
277
  const e = event;
229
- if (e.buttons !== 1 || !this._draggable) {
278
+ if (('buttons' in e && e.buttons !== 1) || !this._draggable) {
230
279
  return;
231
280
  }
281
+ e.preventDefault();
282
+ e.stopPropagation();
232
283
  const field = e.target;
233
284
  if (!this._selectBlock) {
234
285
  this.setSelectBlock(this.focusedField || field, this.focusedField || field);
@@ -240,7 +291,11 @@ let DataGridBody = class DataGridBody extends LitElement {
240
291
  this.setSelectBlock(start, end);
241
292
  }
242
293
  });
243
- this.renderRoot.addEventListener('mouseup', (event) => {
294
+ this.renderRoot.addEventListener('pointerup', (event) => {
295
+ if (event.pointerType !== 'touch') {
296
+ event.preventDefault();
297
+ event.stopPropagation();
298
+ }
244
299
  this._draggable = false;
245
300
  });
246
301
  this.renderRoot.addEventListener('click', dataGridBodyClickHandler.bind(this));
@@ -292,6 +347,8 @@ let DataGridBody = class DataGridBody extends LitElement {
292
347
  if (this.editTarget && this.editTarget.row == row && this.editTarget.column == column) {
293
348
  return;
294
349
  }
350
+ /* 에디터 편집 전환 시 IME buffer 정리 */
351
+ this._resetIMEBuffer();
295
352
  this.editTarget = {
296
353
  row,
297
354
  column,
@@ -327,6 +384,11 @@ let DataGridBody = class DataGridBody extends LitElement {
327
384
  if (left !== undefined) {
328
385
  this.scrollLeft = left;
329
386
  }
387
+ /* IME buffer 위치 업데이트 */
388
+ this._positionIMEBuffer();
389
+ if (!this.editTarget && this._imeBuffer && !this._lastPointerTouch) {
390
+ this._imeBuffer.focus();
391
+ }
330
392
  }
331
393
  // 페이징 바뀌면 스크롤 맨 위로
332
394
  if (changes.has('data')) {
@@ -342,7 +404,18 @@ let DataGridBody = class DataGridBody extends LitElement {
342
404
  }
343
405
  }
344
406
  focus() {
345
- super.focus();
407
+ if (this.editTarget) {
408
+ /* 에디터 편집 모드에서는 에디터가 자체적으로 포커스를 관리하므로 간섭하지 않는다 */
409
+ return;
410
+ }
411
+ if (this._imeBuffer && !this._lastPointerTouch) {
412
+ /* 비편집 모드에서는 IME buffer로 포커스 리다이렉트 (터치 시에는 가상 키보드 방지를 위해 제외) */
413
+ this._positionIMEBuffer();
414
+ this._imeBuffer.focus();
415
+ }
416
+ else {
417
+ super.focus();
418
+ }
346
419
  if (this.focused === ZERO_FOCUS) {
347
420
  let { records } = this.data;
348
421
  let row = records.findIndex(record => record['__selected__']);
@@ -365,6 +438,7 @@ let DataGridBody = class DataGridBody extends LitElement {
365
438
  delete this._recordViewRow;
366
439
  });
367
440
  }
441
+ /* 선택된 셀 블록의 값을 HTML(table)과 plain text(TSV) 두 가지 포맷으로 반환 */
368
442
  getSelectedBlockValues() {
369
443
  var { start, end } = this._selectBlock || {};
370
444
  if (!(start && end)) {
@@ -378,34 +452,81 @@ let DataGridBody = class DataGridBody extends LitElement {
378
452
  const endColumnIndex = start.columnIndex < end.columnIndex ? end.columnIndex : start.columnIndex;
379
453
  const columnArray = new Array(endColumnIndex - startColumnIndex + 1).fill(startColumnIndex);
380
454
  const columns = this.columns.filter(column => !column.hidden);
381
- return ('<table>' +
382
- new Array(endRowIndex - startRowIndex + 1)
383
- .fill(startRowIndex)
384
- .map((start, index) => {
385
- const rowIndex = start + index;
386
- const record = this.data.records[rowIndex];
387
- const tds = columnArray
388
- .map((start, index) => {
389
- const columnIndex = start + index;
390
- const column = columns[columnIndex];
391
- const value = record === null || record === void 0 ? void 0 : record[column.name];
392
- const type = typeof value;
393
- const text = value === undefined || value === null ? '' : type == 'object' ? JSON.stringify(value) : value;
394
- return `<td type=${type}>${text}</td>`;
395
- })
396
- .join('');
397
- return `<tr>${tds}</tr>`;
398
- })
455
+ /*
456
+ * 클립보드에 가지 포맷(text/html, text/plain)을 동시에 저장한다.
457
+ * 붙여넣기하는 앱이 자신이 지원하는 포맷을 골라 읽는다.
458
+ * 생성되는 HTML:
459
+ * <td type=object data-value="{&quot;id&quot;:...}">탈수 절임배추</td>
460
+ * ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^
461
+ * 그리드만 읽는 숨겨진 JSON Excel/메모장이 보는 텍스트
462
+ */
463
+ const rows = new Array(endRowIndex - startRowIndex + 1).fill(startRowIndex).map((start, index) => {
464
+ const rowIndex = start + index;
465
+ const record = this.data.records[rowIndex];
466
+ const cells = columnArray.map((start, index) => {
467
+ var _a, _b;
468
+ const columnIndex = start + index;
469
+ const column = columns[columnIndex];
470
+ const value = record === null || record === void 0 ? void 0 : record[column.name];
471
+ const type = typeof value;
472
+ /* htmlText: object는 JSON 문자열로 보존 → data-value 속성에 저장되어 그리드 붙여넣기 시 복원됨 */
473
+ const htmlText = value === undefined || value === null ? '' : type == 'object' ? JSON.stringify(value) : value;
474
+ /* plainText: ① null/undefined → renderer ② object → name → renderer ③ 기타 → 값 그대로 */
475
+ let plainText = '';
476
+ if (value === undefined || value === null) {
477
+ /* 값이 없지만 renderer가 record의 다른 경로로 텍스트를 표시할 수 있음 */
478
+ if ((_a = column.record) === null || _a === void 0 ? void 0 : _a.renderer) {
479
+ try {
480
+ const r = column.record.renderer(value, column, record, rowIndex, null);
481
+ if (typeof r === 'string')
482
+ plainText = r;
483
+ }
484
+ catch (e) { }
485
+ }
486
+ }
487
+ else if (type == 'object') {
488
+ /* object: name 필드 우선, 없으면 renderer */
489
+ plainText = (value === null || value === void 0 ? void 0 : value.name) || '';
490
+ if (!plainText && ((_b = column.record) === null || _b === void 0 ? void 0 : _b.renderer)) {
491
+ try {
492
+ const r = column.record.renderer(value, column, record, rowIndex, null);
493
+ if (typeof r === 'string')
494
+ plainText = r;
495
+ }
496
+ catch (e) { }
497
+ }
498
+ }
499
+ else {
500
+ plainText = value;
501
+ }
502
+ return { type, htmlText, plainText };
503
+ });
504
+ return cells;
505
+ });
506
+ const html = '<table>' +
507
+ rows
508
+ .map(cells => '<tr>' +
509
+ cells
510
+ .map(c => c.type == 'object' && c.htmlText
511
+ ? `<td type=${c.type} data-value="${String(c.htmlText).replace(/"/g, '&quot;')}">${c.plainText}</td>`
512
+ : `<td type=${c.type}>${c.htmlText}</td>`)
513
+ .join('') +
514
+ '</tr>')
399
515
  .join('') +
400
- '</table>');
516
+ '</table>';
517
+ /* 태그 없는 TSV 포맷 (object는 name만) */
518
+ const text = rows.map(cells => cells.map(c => String(c.plainText)).join('\t')).join('\n');
519
+ return { html, text };
401
520
  }
402
521
  }
403
522
  async copy() {
404
523
  const copied = this.getSelectedBlockValues();
524
+ if (!copied)
525
+ return;
405
526
  await navigator.clipboard.write([
406
527
  new ClipboardItem({
407
- 'text/html': new Blob([copied], { type: 'text/html' }),
408
- 'text/plain': new Blob([copied], { type: 'text/plain' })
528
+ 'text/html': new Blob([copied.html], { type: 'text/html' }),
529
+ 'text/plain': new Blob([copied.text], { type: 'text/plain' })
409
530
  })
410
531
  ]);
411
532
  const selectBlock = this.selectBlock || this.focusedField;
@@ -482,8 +603,8 @@ let DataGridBody = class DataGridBody extends LitElement {
482
603
  const cells = record.querySelectorAll('td');
483
604
  cells.forEach((item, columnIndex) => {
484
605
  const targetColumn = columns[column + columnIndex];
485
- let value = item.textContent;
486
- // let value = item.textContent?.trim() as any
606
+ /* data-value 속성이 있으면 우선 사용 (그리드에서 복사한 object 데이터 복원용) */
607
+ let value = (item.dataset.value || item.textContent);
487
608
  let type = targetColumn.type || item.getAttribute('type') || 'string';
488
609
  type = type.includes('object') ? 'object' : type; // 오브젝트 타입 예외처리
489
610
  let { editable } = targetColumn.record;
@@ -551,31 +672,48 @@ let DataGridBody = class DataGridBody extends LitElement {
551
672
  });
552
673
  return;
553
674
  }
554
- else if (!selection && type === 'text/plain') {
555
- const targetRecord = records[row] || { __dirty__: '+' };
556
- const targetColumn = columns[column];
557
- let { editable } = targetColumn.record;
558
- if (typeof editable === 'function') {
559
- editable = editable.call(this, content, targetColumn, targetRecord, row, this);
560
- }
561
- if (targetColumn && !targetColumn.gutterName && editable) {
562
- this.dispatchEvent(new CustomEvent('field-change', {
563
- bubbles: true,
564
- composed: true,
565
- detail: {
566
- before: targetRecord[targetColumn.name],
567
- after: content,
568
- column: targetColumn,
569
- record: targetRecord,
570
- row: row
675
+ else if (type === 'text/plain') {
676
+ /* TSV 포맷 파싱: 줄바꿈으로 분리, 탭으로 열 분리 (엑셀 등 외부 소스 붙여넣기 지원) */
677
+ const tsvRows = content.split('\n');
678
+ tsvRows.forEach((line, rowOffset) => {
679
+ const targetRowIndex = row + rowOffset;
680
+ var targetRecord = records[targetRowIndex] || { __dirty__: '+' };
681
+ if (targetRowIndex >= records.length) {
682
+ records.push(targetRecord);
683
+ }
684
+ const tsvCells = line.split('\t');
685
+ tsvCells.forEach((cellValue, colOffset) => {
686
+ const targetColumn = columns[column + colOffset];
687
+ if (!targetColumn || targetColumn.gutterName)
688
+ return;
689
+ let { editable } = targetColumn.record;
690
+ if (typeof editable === 'function') {
691
+ editable = editable.call(this, cellValue, targetColumn, targetRecord, targetRowIndex, this);
571
692
  }
572
- }));
573
- }
693
+ if (editable) {
694
+ this.dispatchEvent(new CustomEvent('field-change', {
695
+ bubbles: true,
696
+ composed: true,
697
+ detail: {
698
+ before: targetRecord[targetColumn.name],
699
+ after: cellValue || undefined,
700
+ column: targetColumn,
701
+ record: targetRecord,
702
+ row: targetRowIndex
703
+ }
704
+ }));
705
+ }
706
+ });
707
+ });
574
708
  }
575
709
  }
576
710
  catch (e) {
577
711
  console.log('e : ', e);
578
712
  }
713
+ finally {
714
+ /* paste 완료 후 포커스를 그리드로 복원 */
715
+ this.focus();
716
+ }
579
717
  }
580
718
  setSelectBlock(start, end) {
581
719
  var _a;
@@ -614,12 +752,84 @@ let DataGridBody = class DataGridBody extends LitElement {
614
752
  return record;
615
753
  }, {});
616
754
  }
755
+ /** IME buffer를 포커스된 셀 위에 위치시킨다 */
756
+ _positionIMEBuffer() {
757
+ var _a;
758
+ const focusedEl = (_a = this.renderRoot) === null || _a === void 0 ? void 0 : _a.querySelector('[focused]');
759
+ if (focusedEl && this._imeBuffer) {
760
+ this._imeBuffer.style.left = focusedEl.offsetLeft + 'px';
761
+ this._imeBuffer.style.top = focusedEl.offsetTop + 'px';
762
+ this._imeBuffer.style.width = focusedEl.offsetWidth + 'px';
763
+ this._imeBuffer.style.height = focusedEl.offsetHeight + 'px';
764
+ }
765
+ }
766
+ /** IME buffer 값을 field-change 이벤트로 커밋한다 */
767
+ _commitIMEBuffer(row, column) {
768
+ var _a;
769
+ const text = (_a = this._imeBuffer) === null || _a === void 0 ? void 0 : _a.value;
770
+ this._resetIMEBuffer();
771
+ if (!text)
772
+ return;
773
+ const columns = this.columns.filter(c => !c.hidden);
774
+ const col = columns[column];
775
+ if (!col || col.gutterName)
776
+ return;
777
+ const record = this.data.records[row] || { __dirty__: '+' };
778
+ let { editable } = col.record;
779
+ if (typeof editable === 'function') {
780
+ editable = editable.call(this, record[col.name], col, record, row, this);
781
+ }
782
+ if (!editable)
783
+ return;
784
+ /* 타입에 따른 값 변환 */
785
+ let value = text;
786
+ switch (col.type) {
787
+ case 'number':
788
+ case 'float':
789
+ case 'integer':
790
+ case 'progress':
791
+ value = parseToNumberOrNull(text);
792
+ if (value === null)
793
+ return;
794
+ break;
795
+ case 'boolean':
796
+ case 'checkbox':
797
+ return; /* boolean은 키보드 텍스트 입력으로 편집 불가 */
798
+ }
799
+ this.dispatchEvent(new CustomEvent('field-change', {
800
+ bubbles: true,
801
+ composed: true,
802
+ detail: {
803
+ before: record[col.name],
804
+ after: value,
805
+ column: col,
806
+ record,
807
+ row
808
+ }
809
+ }));
810
+ }
811
+ /** IME buffer 취소 (값 버리기) */
812
+ _discardIMEBuffer() {
813
+ this._resetIMEBuffer();
814
+ }
815
+ /** IME buffer 초기화 */
816
+ _resetIMEBuffer() {
817
+ if (this._imeBuffer) {
818
+ this._imeBuffer.value = '';
819
+ this._imeBuffer.classList.remove('active');
820
+ }
821
+ this._imeEditing = false;
822
+ this._imeComposing = false;
823
+ }
617
824
  };
618
825
  DataGridBody.styles = [
619
826
  dataGridBodyStyle,
620
827
  css `
621
828
  :host {
622
829
  font-variation-settings: 'FILL' 1;
830
+
831
+ overscroll-behavior: none;
832
+ user-select: none;
623
833
  }
624
834
 
625
835
  [select-block] {
@@ -631,6 +841,7 @@ DataGridBody.styles = [
631
841
  border: var(--grid-record-focused-cell-border);
632
842
  background-image: var(--focused-background-image);
633
843
  pointer-events: none;
844
+
634
845
  z-index: 5;
635
846
  }
636
847
 
@@ -663,6 +874,38 @@ DataGridBody.styles = [
663
874
  box-shadow: none !important;
664
875
  }
665
876
  }
877
+
878
+ @media (pointer: coarse) {
879
+ /* 터치 디바이스에서는 길게 눌러 텍스트 선택 허용 */
880
+ :host {
881
+ user-select: auto;
882
+ }
883
+ }
884
+
885
+ /* 한글 IME composition을 위한 숨겨진 input 버퍼 — 기존 에디터 셀 스타일과 일치 */
886
+ #ime-buffer {
887
+ position: absolute;
888
+ opacity: 0;
889
+ z-index: 10;
890
+ background-color: var(--grid-record-editing-background-color, var(--grid-record-background-color, #fff));
891
+ color: var(--md-sys-color-on-background, inherit);
892
+ font: inherit;
893
+ font-size: var(--grid-record-wide-fontsize);
894
+ border: 1px solid transparent;
895
+ border-width: 1px 0;
896
+ border-top: var(--grid-record-editing-border, none);
897
+ border-bottom: var(--grid-record-editing-border, none);
898
+ padding: var(--grid-record-padding);
899
+ box-sizing: border-box;
900
+ outline: none;
901
+ overflow: hidden;
902
+ pointer-events: none;
903
+ }
904
+
905
+ #ime-buffer.active {
906
+ opacity: 1;
907
+ pointer-events: auto;
908
+ }
666
909
  `
667
910
  ];
668
911
  __decorate([