@operato/data-grist 10.0.0-beta.7 → 10.1.0
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/CHANGELOG.md +288 -0
- package/dist/src/data-card/data-card-field.js +3 -0
- package/dist/src/data-card/data-card-field.js.map +1 -1
- package/dist/src/data-card/data-card-gutter.js +11 -1
- package/dist/src/data-card/data-card-gutter.js.map +1 -1
- package/dist/src/data-card/record-card.js +7 -3
- package/dist/src/data-card/record-card.js.map +1 -1
- package/dist/src/data-grid/data-grid-body-style.js +1 -1
- package/dist/src/data-grid/data-grid-body-style.js.map +1 -1
- package/dist/src/data-grid/data-grid-body.d.ts +17 -1
- package/dist/src/data-grid/data-grid-body.js +407 -56
- package/dist/src/data-grid/data-grid-body.js.map +1 -1
- package/dist/src/data-grid/event-handlers/data-grid-body-click-handler.js +14 -14
- package/dist/src/data-grid/event-handlers/data-grid-body-click-handler.js.map +1 -1
- package/dist/src/data-grid/event-handlers/data-grid-body-keydown-handler.js +39 -5
- package/dist/src/data-grid/event-handlers/data-grid-body-keydown-handler.js.map +1 -1
- package/dist/src/data-grist.d.ts +2 -3
- package/dist/src/data-grist.js +31 -1
- package/dist/src/data-grist.js.map +1 -1
- package/dist/src/editors/ox-grist-editor-date.js +8 -1
- package/dist/src/editors/ox-grist-editor-date.js.map +1 -1
- package/dist/src/editors/ox-grist-editor-datetime.js +8 -1
- package/dist/src/editors/ox-grist-editor-datetime.js.map +1 -1
- package/dist/src/editors/ox-grist-editor-text-select.d.ts +18 -0
- package/dist/src/editors/ox-grist-editor-text-select.js +184 -0
- package/dist/src/editors/ox-grist-editor-text-select.js.map +1 -0
- package/dist/src/editors/ox-grist-editor-time.js +7 -1
- package/dist/src/editors/ox-grist-editor-time.js.map +1 -1
- package/dist/src/editors/registry.js +3 -1
- package/dist/src/editors/registry.js.map +1 -1
- package/dist/src/filters/filter-select.js +2 -1
- package/dist/src/filters/filter-select.js.map +1 -1
- package/dist/src/filters/filters-form.d.ts +2 -0
- package/dist/src/filters/filters-form.js +71 -0
- package/dist/src/filters/filters-form.js.map +1 -1
- package/dist/src/renderers/ox-grist-renderer-text-select.d.ts +2 -0
- package/dist/src/renderers/ox-grist-renderer-text-select.js +6 -0
- package/dist/src/renderers/ox-grist-renderer-text-select.js.map +1 -0
- package/dist/src/renderers/registry.js +3 -1
- package/dist/src/renderers/registry.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +11 -10
- package/themes/grist-theme.css +1 -1
- package/translations/ko.json +2 -1
|
@@ -33,6 +33,40 @@ function calcScrollPos(parent, child) {
|
|
|
33
33
|
top: ct < pt ? scrollTop - (pt - ct) : cb > pb ? scrollTop - (pb - cb) - scrollbarHeight : undefined
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
/* select 타입 붙여넣기: display 또는 value 텍스트와 매칭되는 옵션의 value를 반환 */
|
|
37
|
+
function resolveSelectPasteValue(text, column, record) {
|
|
38
|
+
var _a, _b;
|
|
39
|
+
const rowOptionField = record[((_a = column.record) === null || _a === void 0 ? void 0 : _a.rowOptionField) || ''];
|
|
40
|
+
let { options = [] } = rowOptionField ? rowOptionField : column.record || {};
|
|
41
|
+
/* options가 함수인 경우 호출하여 배열을 가져옴 (arrow function이므로 this는 정의 시점에 바인딩됨) */
|
|
42
|
+
if (typeof options === 'function') {
|
|
43
|
+
try {
|
|
44
|
+
options = options();
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(options)) {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const trimmed = String(text).trim();
|
|
54
|
+
if (!trimmed)
|
|
55
|
+
return undefined;
|
|
56
|
+
for (const opt of options) {
|
|
57
|
+
if (typeof opt === 'string') {
|
|
58
|
+
if (opt.trim() === trimmed)
|
|
59
|
+
return opt;
|
|
60
|
+
}
|
|
61
|
+
else if (opt && typeof opt === 'object') {
|
|
62
|
+
const display = String(opt.display || opt.name || '').trim();
|
|
63
|
+
const val = String((_b = opt.value) !== null && _b !== void 0 ? _b : '').trim();
|
|
64
|
+
if (display === trimmed || val === trimmed)
|
|
65
|
+
return opt.value;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
36
70
|
const ZERO_FOCUS = {
|
|
37
71
|
row: 0,
|
|
38
72
|
column: 0
|
|
@@ -56,11 +90,18 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
56
90
|
this.from = -1;
|
|
57
91
|
this.to = -1;
|
|
58
92
|
this.fixedLefts = [];
|
|
93
|
+
this._imeComposing = false;
|
|
94
|
+
this._imeEditing = false;
|
|
95
|
+
this._lastPointerTouch = false;
|
|
59
96
|
// 검색시 스크롤 맨 위로
|
|
60
97
|
this._onFetchParamsChange = () => {
|
|
61
98
|
this.scrollTop = 0;
|
|
62
99
|
};
|
|
63
100
|
}
|
|
101
|
+
/* 편집 가능한 셀이 하나라도 있는지 — 없으면(읽기 전용 그리드) IME 입력 버퍼로 포커스를 넘기지 않는다 */
|
|
102
|
+
get _hasEditableColumn() {
|
|
103
|
+
return this.columns.some(column => { var _a; return !column.hidden && !column.gutterName && !!((_a = column.record) === null || _a === void 0 ? void 0 : _a.editable); });
|
|
104
|
+
}
|
|
64
105
|
resetEdit() {
|
|
65
106
|
this.editTarget = null;
|
|
66
107
|
}
|
|
@@ -157,6 +198,7 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
157
198
|
`
|
|
158
199
|
: nothing}
|
|
159
200
|
${start && end && start !== end ? html ` <div select-block></div> ` : html ``}
|
|
201
|
+
<input id="ime-buffer" type="text" />
|
|
160
202
|
<slot></slot>
|
|
161
203
|
`;
|
|
162
204
|
}
|
|
@@ -165,12 +207,43 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
165
207
|
// this.addEventListener('scroll', this.handleOnScroll.bind(this))
|
|
166
208
|
/* focus() 를 받을 수 있도록 함. */
|
|
167
209
|
this.setAttribute('tabindex', '-1');
|
|
210
|
+
/* 한글 IME composition을 위한 숨겨진 input 버퍼 설정 */
|
|
211
|
+
const imeBuffer = this.renderRoot.querySelector('#ime-buffer');
|
|
212
|
+
this._imeBuffer = imeBuffer;
|
|
213
|
+
imeBuffer.addEventListener('compositionstart', () => {
|
|
214
|
+
this._imeComposing = true;
|
|
215
|
+
this._imeEditing = true;
|
|
216
|
+
imeBuffer.classList.add('active');
|
|
217
|
+
this._positionIMEBuffer();
|
|
218
|
+
});
|
|
219
|
+
imeBuffer.addEventListener('compositionend', () => {
|
|
220
|
+
this._imeComposing = false;
|
|
221
|
+
});
|
|
222
|
+
imeBuffer.addEventListener('input', () => {
|
|
223
|
+
if (!this._imeComposing && !this._imeEditing && imeBuffer.value) {
|
|
224
|
+
/* 영어/숫자: 실제 값이 입력된 경우에만 활성화 */
|
|
225
|
+
this._imeEditing = true;
|
|
226
|
+
imeBuffer.classList.add('active');
|
|
227
|
+
this._positionIMEBuffer();
|
|
228
|
+
}
|
|
229
|
+
});
|
|
168
230
|
/*
|
|
169
231
|
* focusout 으로 property를 변경시키는 경우, focusout에 의해 update가 발생하는 경우에는,
|
|
170
232
|
* 그리드 내부의 컴포넌트가 갱신되는 현상을 초래하게 된다.
|
|
171
233
|
* 따라서, focusout 핸들러에서 update를 유발하는 코드는 강력하게 금지시킨다.
|
|
172
234
|
*/
|
|
173
235
|
this.addEventListener('focusout', e => {
|
|
236
|
+
var _a;
|
|
237
|
+
/* IME buffer로 포커스가 이동하는 경우 keydown listener 유지 */
|
|
238
|
+
const related = e.relatedTarget;
|
|
239
|
+
if (related && ((_a = this.shadowRoot) === null || _a === void 0 ? void 0 : _a.contains(related))) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
/* 그리드 외부로 포커스 이동 시 IME buffer 커밋 */
|
|
243
|
+
if (this._imeEditing) {
|
|
244
|
+
const { row = 0, column = 0 } = this.focused || {};
|
|
245
|
+
this._commitIMEBuffer(row, column);
|
|
246
|
+
}
|
|
174
247
|
if (this._focusedListener) {
|
|
175
248
|
this.removeEventListener('keydown', this._focusedListener);
|
|
176
249
|
delete this._focusedListener;
|
|
@@ -205,8 +278,21 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
205
278
|
}));
|
|
206
279
|
});
|
|
207
280
|
this.renderRoot.addEventListener('pointerdown', (e) => {
|
|
281
|
+
const pe = e;
|
|
282
|
+
const isTouch = pe.pointerType === 'touch';
|
|
283
|
+
/* 터치 이벤트 여부 기록 — 태블릿에서 input focus로 가상 키보드가 뜨는 것을 방지 */
|
|
284
|
+
this._lastPointerTouch = isTouch;
|
|
285
|
+
/* 클릭 시 IME buffer가 활성 상태면 먼저 커밋 */
|
|
286
|
+
if (this._imeEditing) {
|
|
287
|
+
const { row = 0, column = 0 } = this.focused || {};
|
|
288
|
+
this._commitIMEBuffer(row, column);
|
|
289
|
+
}
|
|
208
290
|
this.setSelectBlock();
|
|
209
|
-
if (
|
|
291
|
+
if (isTouch) {
|
|
292
|
+
/* 터치: click 이벤트에서 처리 (스크롤 시에는 click이 발생하지 않음) */
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
if ('buttons' in e && pe.buttons !== 1) {
|
|
210
296
|
return;
|
|
211
297
|
}
|
|
212
298
|
e.preventDefault();
|
|
@@ -245,8 +331,10 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
245
331
|
}
|
|
246
332
|
});
|
|
247
333
|
this.renderRoot.addEventListener('pointerup', (event) => {
|
|
248
|
-
event.
|
|
249
|
-
|
|
334
|
+
if (event.pointerType !== 'touch') {
|
|
335
|
+
event.preventDefault();
|
|
336
|
+
event.stopPropagation();
|
|
337
|
+
}
|
|
250
338
|
this._draggable = false;
|
|
251
339
|
});
|
|
252
340
|
this.renderRoot.addEventListener('click', dataGridBodyClickHandler.bind(this));
|
|
@@ -254,8 +342,15 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
254
342
|
this.renderRoot.addEventListener('contextmenu', dataGridBodyContextMenuHandler.bind(this));
|
|
255
343
|
this.addEventListener('focus-change', dataGridBodyFocusChangeHandler.bind(this));
|
|
256
344
|
requestAnimationFrame(() => {
|
|
257
|
-
|
|
258
|
-
this.
|
|
345
|
+
// 1. 공백 찌꺼기가 들어갈 수 있으므로 .trim()으로 깔끔하게 처리
|
|
346
|
+
const primaryColor = getComputedStyle(this).getPropertyValue('--md-sys-color-primary').trim();
|
|
347
|
+
// 2. 가독성을 위해 svg 문자열 조립 분리
|
|
348
|
+
const svgString = `<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><rect fill='${primaryColor}' fill-opacity='0.2' x='0' y='0' width='100%' height='100%'/></svg>`;
|
|
349
|
+
// 3. Hex 코드의 '#' 기호 및 '< >' 괄호들이 파서를 깨뜨리는 것을 완벽 방지하기 위해 전체 인코딩
|
|
350
|
+
const encodedSvg = encodeURIComponent(svgString);
|
|
351
|
+
this.style.setProperty('--focused-background-image',
|
|
352
|
+
// 4. 비표준인 ;utf8 태그를 떼어내고, 인코딩된 SVG 데이터를 주입
|
|
353
|
+
`url("data:image/svg+xml,${encodedSvg}")`);
|
|
259
354
|
});
|
|
260
355
|
this.addEventListener('show-record-view', (e) => this.popupRecordView(e.detail));
|
|
261
356
|
this.addEventListener('scroll', function () {
|
|
@@ -298,6 +393,8 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
298
393
|
if (this.editTarget && this.editTarget.row == row && this.editTarget.column == column) {
|
|
299
394
|
return;
|
|
300
395
|
}
|
|
396
|
+
/* 에디터 편집 전환 시 IME buffer 정리 */
|
|
397
|
+
this._resetIMEBuffer();
|
|
301
398
|
this.editTarget = {
|
|
302
399
|
row,
|
|
303
400
|
column,
|
|
@@ -333,6 +430,11 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
333
430
|
if (left !== undefined) {
|
|
334
431
|
this.scrollLeft = left;
|
|
335
432
|
}
|
|
433
|
+
/* IME buffer 위치 업데이트 */
|
|
434
|
+
this._positionIMEBuffer();
|
|
435
|
+
if (!this.editTarget && this._imeBuffer && !this._lastPointerTouch && this._hasEditableColumn) {
|
|
436
|
+
this._imeBuffer.focus();
|
|
437
|
+
}
|
|
336
438
|
}
|
|
337
439
|
// 페이징 바뀌면 스크롤 맨 위로
|
|
338
440
|
if (changes.has('data')) {
|
|
@@ -348,7 +450,18 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
348
450
|
}
|
|
349
451
|
}
|
|
350
452
|
focus() {
|
|
351
|
-
|
|
453
|
+
if (this.editTarget) {
|
|
454
|
+
/* 에디터 편집 모드에서는 에디터가 자체적으로 포커스를 관리하므로 간섭하지 않는다 */
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
if (this._imeBuffer && !this._lastPointerTouch && this._hasEditableColumn) {
|
|
458
|
+
/* 비편집 모드에서는 IME buffer로 포커스 리다이렉트 (터치 시/읽기 전용 그리드에서는 제외) */
|
|
459
|
+
this._positionIMEBuffer();
|
|
460
|
+
this._imeBuffer.focus();
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
super.focus();
|
|
464
|
+
}
|
|
352
465
|
if (this.focused === ZERO_FOCUS) {
|
|
353
466
|
let { records } = this.data;
|
|
354
467
|
let row = records.findIndex(record => record['__selected__']);
|
|
@@ -371,6 +484,7 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
371
484
|
delete this._recordViewRow;
|
|
372
485
|
});
|
|
373
486
|
}
|
|
487
|
+
/* 선택된 셀 블록의 값을 HTML(table)과 plain text(TSV) 두 가지 포맷으로 반환 */
|
|
374
488
|
getSelectedBlockValues() {
|
|
375
489
|
var { start, end } = this._selectBlock || {};
|
|
376
490
|
if (!(start && end)) {
|
|
@@ -384,34 +498,117 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
384
498
|
const endColumnIndex = start.columnIndex < end.columnIndex ? end.columnIndex : start.columnIndex;
|
|
385
499
|
const columnArray = new Array(endColumnIndex - startColumnIndex + 1).fill(startColumnIndex);
|
|
386
500
|
const columns = this.columns.filter(column => !column.hidden);
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
501
|
+
/*
|
|
502
|
+
* 클립보드에 두 가지 포맷(text/html, text/plain)을 동시에 저장한다.
|
|
503
|
+
* 붙여넣기하는 앱이 자신이 지원하는 포맷을 골라 읽는다.
|
|
504
|
+
* 생성되는 HTML:
|
|
505
|
+
* <td type=object data-value="{"id":...}">탈수 절임배추</td>
|
|
506
|
+
* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^
|
|
507
|
+
* 그리드만 읽는 숨겨진 JSON Excel/메모장이 보는 텍스트
|
|
508
|
+
*/
|
|
509
|
+
const rows = new Array(endRowIndex - startRowIndex + 1).fill(startRowIndex).map((start, index) => {
|
|
510
|
+
const rowIndex = start + index;
|
|
511
|
+
const record = this.data.records[rowIndex];
|
|
512
|
+
const cells = columnArray.map((start, index) => {
|
|
513
|
+
var _a, _b, _c, _d;
|
|
514
|
+
const columnIndex = start + index;
|
|
515
|
+
const column = columns[columnIndex];
|
|
516
|
+
const value = record === null || record === void 0 ? void 0 : record[column.name];
|
|
517
|
+
const type = typeof value;
|
|
518
|
+
/* select 타입: value → display 텍스트 미리 계산 (htmlText/plainText 공용) */
|
|
519
|
+
let selectDisplay = value;
|
|
520
|
+
if (column.type === 'select' && value !== undefined && value !== null) {
|
|
521
|
+
const src = (_b = record[((_a = column.record) === null || _a === void 0 ? void 0 : _a.rowOptionField) || '']) !== null && _b !== void 0 ? _b : column.record;
|
|
522
|
+
const options = Array.isArray(src === null || src === void 0 ? void 0 : src.options) ? src.options : [];
|
|
523
|
+
const opt = options.find((o) => { var _a; return (typeof o === 'string' ? o : String((_a = o === null || o === void 0 ? void 0 : o.value) !== null && _a !== void 0 ? _a : '')).trim() === String(value).trim(); });
|
|
524
|
+
selectDisplay = opt ? (typeof opt === 'string' ? opt : opt.display || opt.name || value) : value;
|
|
525
|
+
}
|
|
526
|
+
/* htmlText: object는 JSON 문자열로 보존(data-value) → 그리드 붙여넣기 시 복원됨. select는 display 텍스트 */
|
|
527
|
+
const htmlText = value === undefined || value === null ? '' : type == 'object' ? JSON.stringify(value) : column.type === 'select' ? selectDisplay : value;
|
|
528
|
+
/* plainText: ① null/undefined → renderer ② object → name → renderer ③ select → display ④ 기타 → 값 그대로 */
|
|
529
|
+
let plainText = '';
|
|
530
|
+
if (value === undefined || value === null) {
|
|
531
|
+
/* 값이 없지만 renderer가 record의 다른 경로로 텍스트를 표시할 수 있음 */
|
|
532
|
+
if ((_c = column.record) === null || _c === void 0 ? void 0 : _c.renderer) {
|
|
533
|
+
try {
|
|
534
|
+
const r = column.record.renderer(value, column, record, rowIndex, null);
|
|
535
|
+
if (typeof r === 'string')
|
|
536
|
+
plainText = r;
|
|
537
|
+
}
|
|
538
|
+
catch (e) { }
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
else if (type == 'object') {
|
|
542
|
+
/* object: name 필드가 문자열인 경우 우선, 없으면 renderer */
|
|
543
|
+
const rawName = value === null || value === void 0 ? void 0 : value.name;
|
|
544
|
+
plainText = typeof rawName === 'string' ? rawName : '';
|
|
545
|
+
if (!plainText && ((_d = column.record) === null || _d === void 0 ? void 0 : _d.renderer)) {
|
|
546
|
+
try {
|
|
547
|
+
const r = column.record.renderer(value, column, record, rowIndex, null);
|
|
548
|
+
if (typeof r === 'string')
|
|
549
|
+
plainText = r;
|
|
550
|
+
}
|
|
551
|
+
catch (e) { }
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
else if (column.type === 'select') {
|
|
555
|
+
plainText = selectDisplay;
|
|
556
|
+
}
|
|
557
|
+
else {
|
|
558
|
+
plainText = value;
|
|
559
|
+
}
|
|
560
|
+
/* 안전장치: plainText가 object이면 빈 문자열로 대체하여 [object Object] 방지 */
|
|
561
|
+
if (plainText !== null && plainText !== undefined && typeof plainText === 'object') {
|
|
562
|
+
plainText = '';
|
|
563
|
+
}
|
|
564
|
+
return { type, htmlText, plainText };
|
|
565
|
+
});
|
|
566
|
+
return cells;
|
|
567
|
+
});
|
|
568
|
+
const escapeHtml = (unsafe) => {
|
|
569
|
+
if (unsafe == null)
|
|
570
|
+
return '';
|
|
571
|
+
return String(unsafe).replace(/[&<>"']/g, function (m) {
|
|
572
|
+
switch (m) {
|
|
573
|
+
case '&':
|
|
574
|
+
return '&';
|
|
575
|
+
case '<':
|
|
576
|
+
return '<';
|
|
577
|
+
case '>':
|
|
578
|
+
return '>';
|
|
579
|
+
case '"':
|
|
580
|
+
return '"';
|
|
581
|
+
case "'":
|
|
582
|
+
return ''';
|
|
583
|
+
default:
|
|
584
|
+
return m;
|
|
585
|
+
}
|
|
586
|
+
});
|
|
587
|
+
};
|
|
588
|
+
const html = '<table>' +
|
|
589
|
+
rows
|
|
590
|
+
.map(cells => '<tr>' +
|
|
591
|
+
cells
|
|
592
|
+
.map(c => c.type == 'object' && c.htmlText
|
|
593
|
+
? `<td type=${c.type} data-value="${escapeHtml(c.htmlText)}">${escapeHtml(c.plainText)}</td>`
|
|
594
|
+
: `<td type=${c.type}>${escapeHtml(c.htmlText)}</td>`)
|
|
595
|
+
.join('') +
|
|
596
|
+
'</tr>')
|
|
405
597
|
.join('') +
|
|
406
|
-
'</table>'
|
|
598
|
+
'</table>';
|
|
599
|
+
/* 태그 없는 TSV 포맷 (object는 name만) */
|
|
600
|
+
const text = rows.map(cells => cells.map(c => String(c.plainText)).join('\t')).join('\n');
|
|
601
|
+
return { html, text };
|
|
407
602
|
}
|
|
408
603
|
}
|
|
409
604
|
async copy() {
|
|
410
605
|
const copied = this.getSelectedBlockValues();
|
|
606
|
+
if (!copied)
|
|
607
|
+
return;
|
|
411
608
|
await navigator.clipboard.write([
|
|
412
609
|
new ClipboardItem({
|
|
413
|
-
'text/html': new Blob([copied], { type: 'text/html' }),
|
|
414
|
-
'text/plain': new Blob([copied], { type: 'text/plain' })
|
|
610
|
+
'text/html': new Blob([copied.html], { type: 'text/html' }),
|
|
611
|
+
'text/plain': new Blob([copied.text], { type: 'text/plain' })
|
|
415
612
|
})
|
|
416
613
|
]);
|
|
417
614
|
const selectBlock = this.selectBlock || this.focusedField;
|
|
@@ -487,10 +684,14 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
487
684
|
}
|
|
488
685
|
const cells = record.querySelectorAll('td');
|
|
489
686
|
cells.forEach((item, columnIndex) => {
|
|
687
|
+
var _a;
|
|
490
688
|
const targetColumn = columns[column + columnIndex];
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
689
|
+
if (!targetColumn || targetColumn.gutterName)
|
|
690
|
+
return;
|
|
691
|
+
/* data-value 속성이 있으면 우선 사용 (그리드에서 복사한 object 데이터 복원용) */
|
|
692
|
+
let value = (item.dataset.value || item.textContent);
|
|
693
|
+
const sourceType = item.getAttribute('type') || 'string';
|
|
694
|
+
let type = targetColumn.type || sourceType;
|
|
494
695
|
type = type.includes('object') ? 'object' : type; // 오브젝트 타입 예외처리
|
|
495
696
|
let { editable } = targetColumn.record;
|
|
496
697
|
if (typeof editable === 'function') {
|
|
@@ -506,13 +707,17 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
506
707
|
? null
|
|
507
708
|
: value;
|
|
508
709
|
}
|
|
509
|
-
catch (err) {
|
|
710
|
+
catch (err) {
|
|
711
|
+
value = null;
|
|
712
|
+
}
|
|
510
713
|
break;
|
|
511
714
|
case 'parameters':
|
|
512
715
|
try {
|
|
513
716
|
value = JSON.parse(value || 'null');
|
|
514
717
|
}
|
|
515
|
-
catch (err) {
|
|
718
|
+
catch (err) {
|
|
719
|
+
value = null;
|
|
720
|
+
}
|
|
516
721
|
break;
|
|
517
722
|
case 'boolean':
|
|
518
723
|
case 'checkbox':
|
|
@@ -526,19 +731,41 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
526
731
|
break;
|
|
527
732
|
case 'string':
|
|
528
733
|
try {
|
|
529
|
-
|
|
734
|
+
const parsed = JSON.parse(value);
|
|
735
|
+
/* 오브젝트를 문자열 셀에 붙여넣을 때 셀의 표시 텍스트(textContent) 우선 사용 */
|
|
736
|
+
if (typeof parsed === 'object' && parsed !== null) {
|
|
737
|
+
value = ((_a = item.textContent) === null || _a === void 0 ? void 0 : _a.trim()) || parsed.name || '';
|
|
738
|
+
}
|
|
739
|
+
else {
|
|
740
|
+
value = String(parsed).trim();
|
|
741
|
+
}
|
|
530
742
|
}
|
|
531
743
|
catch (err) { }
|
|
532
744
|
break;
|
|
745
|
+
case 'select':
|
|
746
|
+
/* select 타입: 붙여넣은 텍스트를 옵션의 display/value와 매칭하여 value로 변환 */
|
|
747
|
+
value = resolveSelectPasteValue(String(value), targetColumn, targetRecord);
|
|
748
|
+
break;
|
|
533
749
|
default:
|
|
534
|
-
|
|
535
|
-
|
|
750
|
+
/* 원본이 string 으로 복사된 값(color-map 등 JSON 문자열 저장 타입)은
|
|
751
|
+
파싱하지 않고 문자열 그대로 유지 */
|
|
752
|
+
if (sourceType === 'string' && !item.dataset.value) {
|
|
753
|
+
value = value === null || value === void 0 ? void 0 : value.trim();
|
|
754
|
+
}
|
|
755
|
+
else {
|
|
756
|
+
try {
|
|
757
|
+
value = JSON.parse(value);
|
|
758
|
+
}
|
|
759
|
+
catch (err) {
|
|
760
|
+
if (type === 'json' || type.includes('object'))
|
|
761
|
+
value = null;
|
|
762
|
+
}
|
|
536
763
|
}
|
|
537
|
-
catch (err) { }
|
|
538
764
|
}
|
|
539
765
|
}
|
|
540
766
|
else {
|
|
541
|
-
|
|
767
|
+
/* 기존 레코드(id 있음)는 명시적으로 비우기 위해 null, 신규 레코드는 default value 적용을 위해 undefined */
|
|
768
|
+
value = targetRecord.id ? null : undefined;
|
|
542
769
|
}
|
|
543
770
|
if (targetColumn && !targetColumn.gutterName && editable) {
|
|
544
771
|
this.dispatchEvent(new CustomEvent('field-change', {
|
|
@@ -557,31 +784,54 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
557
784
|
});
|
|
558
785
|
return;
|
|
559
786
|
}
|
|
560
|
-
else if (
|
|
561
|
-
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
787
|
+
else if (type === 'text/plain') {
|
|
788
|
+
/* TSV 포맷 파싱: 줄바꿈으로 행 분리, 탭으로 열 분리 (엑셀 등 외부 소스 붙여넣기 지원) */
|
|
789
|
+
const tsvRows = content.split('\n');
|
|
790
|
+
tsvRows.forEach((line, rowOffset) => {
|
|
791
|
+
const targetRowIndex = row + rowOffset;
|
|
792
|
+
var targetRecord = records[targetRowIndex] || { __dirty__: '+' };
|
|
793
|
+
if (targetRowIndex >= records.length) {
|
|
794
|
+
records.push(targetRecord);
|
|
795
|
+
}
|
|
796
|
+
const tsvCells = line.split('\t');
|
|
797
|
+
tsvCells.forEach((cellValue, colOffset) => {
|
|
798
|
+
const targetColumn = columns[column + colOffset];
|
|
799
|
+
if (!targetColumn || targetColumn.gutterName)
|
|
800
|
+
return;
|
|
801
|
+
let { editable } = targetColumn.record;
|
|
802
|
+
if (typeof editable === 'function') {
|
|
803
|
+
editable = editable.call(this, cellValue, targetColumn, targetRecord, targetRowIndex, this);
|
|
577
804
|
}
|
|
578
|
-
|
|
579
|
-
|
|
805
|
+
if (editable) {
|
|
806
|
+
/* select 타입: 붙여넣은 텍스트를 옵션의 display/value와 매칭하여 value로 변환 */
|
|
807
|
+
/* 빈값: 기존 레코드(id 있음)는 명시적으로 비우기 위해 null, 신규 레코드는 default value 적용을 위해 undefined */
|
|
808
|
+
let resolvedValue = cellValue || (targetRecord.id ? null : undefined);
|
|
809
|
+
if (targetColumn.type === 'select' && cellValue) {
|
|
810
|
+
resolvedValue = resolveSelectPasteValue(cellValue, targetColumn, targetRecord);
|
|
811
|
+
}
|
|
812
|
+
this.dispatchEvent(new CustomEvent('field-change', {
|
|
813
|
+
bubbles: true,
|
|
814
|
+
composed: true,
|
|
815
|
+
detail: {
|
|
816
|
+
before: targetRecord[targetColumn.name],
|
|
817
|
+
after: resolvedValue,
|
|
818
|
+
column: targetColumn,
|
|
819
|
+
record: targetRecord,
|
|
820
|
+
row: targetRowIndex
|
|
821
|
+
}
|
|
822
|
+
}));
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
});
|
|
580
826
|
}
|
|
581
827
|
}
|
|
582
828
|
catch (e) {
|
|
583
829
|
console.log('e : ', e);
|
|
584
830
|
}
|
|
831
|
+
finally {
|
|
832
|
+
/* paste 완료 후 포커스를 그리드로 복원 */
|
|
833
|
+
this.focus();
|
|
834
|
+
}
|
|
585
835
|
}
|
|
586
836
|
setSelectBlock(start, end) {
|
|
587
837
|
var _a;
|
|
@@ -620,6 +870,75 @@ let DataGridBody = class DataGridBody extends LitElement {
|
|
|
620
870
|
return record;
|
|
621
871
|
}, {});
|
|
622
872
|
}
|
|
873
|
+
/** IME buffer를 포커스된 셀 위에 위치시킨다 */
|
|
874
|
+
_positionIMEBuffer() {
|
|
875
|
+
var _a;
|
|
876
|
+
const focusedEl = (_a = this.renderRoot) === null || _a === void 0 ? void 0 : _a.querySelector('[focused]');
|
|
877
|
+
if (focusedEl && this._imeBuffer) {
|
|
878
|
+
this._imeBuffer.style.left = focusedEl.offsetLeft + 'px';
|
|
879
|
+
this._imeBuffer.style.top = focusedEl.offsetTop + 'px';
|
|
880
|
+
this._imeBuffer.style.width = focusedEl.offsetWidth + 'px';
|
|
881
|
+
this._imeBuffer.style.height = focusedEl.offsetHeight + 'px';
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
/** IME buffer 값을 field-change 이벤트로 커밋한다 */
|
|
885
|
+
_commitIMEBuffer(row, column) {
|
|
886
|
+
var _a;
|
|
887
|
+
const text = (_a = this._imeBuffer) === null || _a === void 0 ? void 0 : _a.value;
|
|
888
|
+
this._resetIMEBuffer();
|
|
889
|
+
if (!text)
|
|
890
|
+
return;
|
|
891
|
+
const columns = this.columns.filter(c => !c.hidden);
|
|
892
|
+
const col = columns[column];
|
|
893
|
+
if (!col || col.gutterName)
|
|
894
|
+
return;
|
|
895
|
+
const record = this.data.records[row] || { __dirty__: '+' };
|
|
896
|
+
let { editable } = col.record;
|
|
897
|
+
if (typeof editable === 'function') {
|
|
898
|
+
editable = editable.call(this, record[col.name], col, record, row, this);
|
|
899
|
+
}
|
|
900
|
+
if (!editable)
|
|
901
|
+
return;
|
|
902
|
+
/* 타입에 따른 값 변환 */
|
|
903
|
+
let value = text;
|
|
904
|
+
switch (col.type) {
|
|
905
|
+
case 'number':
|
|
906
|
+
case 'float':
|
|
907
|
+
case 'integer':
|
|
908
|
+
case 'progress':
|
|
909
|
+
value = parseToNumberOrNull(text);
|
|
910
|
+
if (value === null)
|
|
911
|
+
return;
|
|
912
|
+
break;
|
|
913
|
+
case 'boolean':
|
|
914
|
+
case 'checkbox':
|
|
915
|
+
return; /* boolean은 키보드 텍스트 입력으로 편집 불가 */
|
|
916
|
+
}
|
|
917
|
+
this.dispatchEvent(new CustomEvent('field-change', {
|
|
918
|
+
bubbles: true,
|
|
919
|
+
composed: true,
|
|
920
|
+
detail: {
|
|
921
|
+
before: record[col.name],
|
|
922
|
+
after: value,
|
|
923
|
+
column: col,
|
|
924
|
+
record,
|
|
925
|
+
row
|
|
926
|
+
}
|
|
927
|
+
}));
|
|
928
|
+
}
|
|
929
|
+
/** IME buffer 취소 (값 버리기) */
|
|
930
|
+
_discardIMEBuffer() {
|
|
931
|
+
this._resetIMEBuffer();
|
|
932
|
+
}
|
|
933
|
+
/** IME buffer 초기화 */
|
|
934
|
+
_resetIMEBuffer() {
|
|
935
|
+
if (this._imeBuffer) {
|
|
936
|
+
this._imeBuffer.value = '';
|
|
937
|
+
this._imeBuffer.classList.remove('active');
|
|
938
|
+
}
|
|
939
|
+
this._imeEditing = false;
|
|
940
|
+
this._imeComposing = false;
|
|
941
|
+
}
|
|
623
942
|
};
|
|
624
943
|
DataGridBody.styles = [
|
|
625
944
|
dataGridBodyStyle,
|
|
@@ -673,6 +992,38 @@ DataGridBody.styles = [
|
|
|
673
992
|
box-shadow: none !important;
|
|
674
993
|
}
|
|
675
994
|
}
|
|
995
|
+
|
|
996
|
+
@media (pointer: coarse) {
|
|
997
|
+
/* 터치 디바이스에서는 길게 눌러 텍스트 선택 허용 */
|
|
998
|
+
:host {
|
|
999
|
+
user-select: auto;
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
|
|
1003
|
+
/* 한글 IME composition을 위한 숨겨진 input 버퍼 — 기존 에디터 셀 스타일과 일치 */
|
|
1004
|
+
#ime-buffer {
|
|
1005
|
+
position: absolute;
|
|
1006
|
+
opacity: 0;
|
|
1007
|
+
z-index: 10;
|
|
1008
|
+
background-color: var(--grid-record-editing-background-color, var(--grid-record-background-color, #fff));
|
|
1009
|
+
color: var(--md-sys-color-on-background, inherit);
|
|
1010
|
+
font: inherit;
|
|
1011
|
+
font-size: var(--grid-record-wide-fontsize);
|
|
1012
|
+
border: 1px solid transparent;
|
|
1013
|
+
border-width: 1px 0;
|
|
1014
|
+
border-top: var(--grid-record-editing-border, none);
|
|
1015
|
+
border-bottom: var(--grid-record-editing-border, none);
|
|
1016
|
+
padding: var(--grid-record-padding);
|
|
1017
|
+
box-sizing: border-box;
|
|
1018
|
+
outline: none;
|
|
1019
|
+
overflow: hidden;
|
|
1020
|
+
pointer-events: none;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
#ime-buffer.active {
|
|
1024
|
+
opacity: 1;
|
|
1025
|
+
pointer-events: auto;
|
|
1026
|
+
}
|
|
676
1027
|
`
|
|
677
1028
|
];
|
|
678
1029
|
__decorate([
|