@operato/data-grist 8.0.0-alpha.3 → 8.0.0-alpha.30
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 +103 -0
- package/dist/src/data-card/record-card.js +10 -1
- package/dist/src/data-card/record-card.js.map +1 -1
- package/dist/src/data-grid/data-grid-body.js +15 -6
- package/dist/src/data-grid/data-grid-body.js.map +1 -1
- package/dist/src/data-grid/data-grid-footer.js +2 -0
- package/dist/src/data-grid/data-grid-footer.js.map +1 -1
- package/dist/src/data-grid/data-grid-header.d.ts +1 -1
- package/dist/src/data-grid/data-grid-header.js +13 -9
- package/dist/src/data-grid/data-grid-header.js.map +1 -1
- package/dist/src/data-grist.d.ts +16 -1
- package/dist/src/data-grist.js +53 -20
- package/dist/src/data-grist.js.map +1 -1
- package/dist/src/data-list/record-partial.js +10 -1
- package/dist/src/data-list/record-partial.js.map +1 -1
- package/dist/src/editors/ox-grist-editor-file.js +2 -1
- package/dist/src/editors/ox-grist-editor-file.js.map +1 -1
- package/dist/src/editors/ox-grist-editor.js +3 -3
- package/dist/src/editors/ox-grist-editor.js.map +1 -1
- package/dist/src/record-view/record-creator.d.ts +5 -0
- package/dist/src/record-view/record-creator.js +47 -4
- package/dist/src/record-view/record-creator.js.map +1 -1
- package/dist/src/record-view/record-view-body.d.ts +6 -1
- package/dist/src/record-view/record-view-body.js +43 -5
- package/dist/src/record-view/record-view-body.js.map +1 -1
- package/dist/src/record-view/record-view.d.ts +7 -1
- package/dist/src/record-view/record-view.js +7 -1
- package/dist/src/record-view/record-view.js.map +1 -1
- package/dist/src/renderers/ox-grist-renderer-boolean.js +1 -1
- package/dist/src/renderers/ox-grist-renderer-boolean.js.map +1 -1
- package/dist/src/renderers/ox-grist-renderer-file.js +17 -5
- package/dist/src/renderers/ox-grist-renderer-file.js.map +1 -1
- package/dist/src/types.d.ts +6 -0
- package/dist/src/types.js +7 -0
- package/dist/src/types.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +8 -8
- package/src/data-card/record-card.ts +10 -1
- package/src/data-grid/data-grid-body.ts +18 -6
- package/src/data-grid/data-grid-footer.ts +2 -0
- package/src/data-grid/data-grid-header.ts +13 -9
- package/src/data-grist.ts +60 -24
- package/src/data-list/record-partial.ts +10 -1
- package/src/editors/ox-grist-editor-file.ts +3 -1
- package/src/editors/ox-grist-editor.ts +3 -3
- package/src/record-view/record-creator.ts +50 -4
- package/src/record-view/record-view-body.ts +49 -5
- package/src/record-view/record-view.ts +10 -2
- package/src/renderers/ox-grist-renderer-boolean.ts +1 -1
- package/src/renderers/ox-grist-renderer-file.ts +20 -6
- package/src/types.ts +8 -0
- package/themes/calendar-theme.css +3 -1
- package/translations/en.json +5 -1
- package/translations/ja.json +5 -1
- package/translations/ko.json +5 -1
- package/translations/ms.json +5 -1
- package/translations/zh.json +5 -1
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "@operato/data-grist",
|
3
|
-
"version": "8.0.0-alpha.
|
3
|
+
"version": "8.0.0-alpha.30",
|
4
4
|
"description": "User interface for grid (desktop) and list (mobile)",
|
5
5
|
"author": "heartyoh",
|
6
6
|
"main": "dist/index.js",
|
@@ -63,13 +63,13 @@
|
|
63
63
|
"dependencies": {
|
64
64
|
"@material/web": "^2.0.0",
|
65
65
|
"@operato/headroom": "^8.0.0-alpha.0",
|
66
|
-
"@operato/input": "^8.0.0-alpha.
|
67
|
-
"@operato/p13n": "^8.0.0-alpha.
|
68
|
-
"@operato/popup": "^8.0.0-alpha.
|
66
|
+
"@operato/input": "^8.0.0-alpha.30",
|
67
|
+
"@operato/p13n": "^8.0.0-alpha.29",
|
68
|
+
"@operato/popup": "^8.0.0-alpha.29",
|
69
69
|
"@operato/pull-to-refresh": "^8.0.0-alpha.0",
|
70
|
-
"@operato/styles": "^8.0.0-alpha.
|
71
|
-
"@operato/time-calculator": "^8.0.0-alpha.
|
72
|
-
"@operato/utils": "^8.0.0-alpha.
|
70
|
+
"@operato/styles": "^8.0.0-alpha.27",
|
71
|
+
"@operato/time-calculator": "^8.0.0-alpha.6",
|
72
|
+
"@operato/utils": "^8.0.0-alpha.29",
|
73
73
|
"i18next": "^23.11.5",
|
74
74
|
"json5": "^2.2.0",
|
75
75
|
"lit": "^3.1.2",
|
@@ -108,5 +108,5 @@
|
|
108
108
|
"prettier --write"
|
109
109
|
]
|
110
110
|
},
|
111
|
-
"gitHead": "
|
111
|
+
"gitHead": "7024870c9c802e70e81e58e894c4ab4854ab5563"
|
112
112
|
}
|
@@ -25,7 +25,16 @@ const OPTIONS: Intl.DateTimeFormatOptions = {
|
|
25
25
|
// timeZone: 'America/Los_Angeles'
|
26
26
|
}
|
27
27
|
|
28
|
-
|
28
|
+
function getSafeFormatter(locale: string, options: Intl.DateTimeFormatOptions) {
|
29
|
+
try {
|
30
|
+
const safeLocale = locale || 'en-US'
|
31
|
+
return new Intl.DateTimeFormat(safeLocale, options)
|
32
|
+
} catch (e) {
|
33
|
+
return new Intl.DateTimeFormat('en-US', options)
|
34
|
+
}
|
35
|
+
}
|
36
|
+
|
37
|
+
const formatter = getSafeFormatter(navigator.language, OPTIONS)
|
29
38
|
|
30
39
|
@customElement('ox-record-card')
|
31
40
|
export class RecordCard extends LitElement {
|
@@ -61,6 +61,9 @@ export class DataGridBody extends LitElement {
|
|
61
61
|
css`
|
62
62
|
:host {
|
63
63
|
font-variation-settings: 'FILL' 1;
|
64
|
+
|
65
|
+
overscroll-behavior: none;
|
66
|
+
user-select: none;
|
64
67
|
}
|
65
68
|
|
66
69
|
[select-block] {
|
@@ -72,6 +75,7 @@ export class DataGridBody extends LitElement {
|
|
72
75
|
border: var(--grid-record-focused-cell-border);
|
73
76
|
background-image: var(--focused-background-image);
|
74
77
|
pointer-events: none;
|
78
|
+
|
75
79
|
z-index: 5;
|
76
80
|
}
|
77
81
|
|
@@ -289,14 +293,16 @@ export class DataGridBody extends LitElement {
|
|
289
293
|
)
|
290
294
|
})
|
291
295
|
|
292
|
-
this.renderRoot.addEventListener('
|
293
|
-
const e = event as MouseEvent
|
296
|
+
this.renderRoot.addEventListener('pointerdown', (e: Event) => {
|
294
297
|
this.setSelectBlock()
|
295
298
|
|
296
|
-
if (e.buttons !== 1) {
|
299
|
+
if ('buttons' in e && e.buttons !== 1) {
|
297
300
|
return
|
298
301
|
}
|
299
302
|
|
303
|
+
e.preventDefault()
|
304
|
+
e.stopPropagation()
|
305
|
+
|
300
306
|
this._draggable = true
|
301
307
|
|
302
308
|
var target = (e.target as Element).closest('ox-grid-field') as DataGridField
|
@@ -318,12 +324,15 @@ export class DataGridBody extends LitElement {
|
|
318
324
|
}
|
319
325
|
})
|
320
326
|
|
321
|
-
this.renderRoot.addEventListener('
|
327
|
+
this.renderRoot.addEventListener('pointermove', (event: Event) => {
|
322
328
|
const e = event as MouseEvent
|
323
|
-
if (e.buttons !== 1 || !this._draggable) {
|
329
|
+
if (('buttons' in e && e.buttons !== 1) || !this._draggable) {
|
324
330
|
return
|
325
331
|
}
|
326
332
|
|
333
|
+
e.preventDefault()
|
334
|
+
e.stopPropagation()
|
335
|
+
|
327
336
|
const field = e.target as DataGridField
|
328
337
|
if (!this._selectBlock) {
|
329
338
|
this.setSelectBlock(this.focusedField || field, this.focusedField || field)
|
@@ -340,7 +349,10 @@ export class DataGridBody extends LitElement {
|
|
340
349
|
}
|
341
350
|
})
|
342
351
|
|
343
|
-
this.renderRoot.addEventListener('
|
352
|
+
this.renderRoot.addEventListener('pointerup', (event: Event) => {
|
353
|
+
event.preventDefault()
|
354
|
+
event.stopPropagation()
|
355
|
+
|
344
356
|
this._draggable = false
|
345
357
|
})
|
346
358
|
|
@@ -28,6 +28,8 @@ export class DataGridHeader extends LitElement {
|
|
28
28
|
overflow: hidden;
|
29
29
|
|
30
30
|
font-variation-settings: 'FILL' 1;
|
31
|
+
|
32
|
+
user-select: none;
|
31
33
|
}
|
32
34
|
|
33
35
|
:host([raised]) [fixed] {
|
@@ -266,7 +268,9 @@ export class DataGridHeader extends LitElement {
|
|
266
268
|
: nothing}
|
267
269
|
${column.resizable !== false
|
268
270
|
? html`
|
269
|
-
<span splitter draggable="false" @
|
271
|
+
<span splitter draggable="false" @pointerdown=${(e: PointerEvent) => this._pointerdown(e, index)}
|
272
|
+
> </span
|
273
|
+
>
|
270
274
|
`
|
271
275
|
: nothing}
|
272
276
|
</div>
|
@@ -543,11 +547,11 @@ export class DataGridHeader extends LitElement {
|
|
543
547
|
this._throttledNotifier(idx, width)
|
544
548
|
}
|
545
549
|
|
546
|
-
|
550
|
+
_pointerdown(e: MouseEvent, idx: number) {
|
547
551
|
e.stopPropagation()
|
548
552
|
e.preventDefault()
|
549
553
|
|
550
|
-
var
|
554
|
+
var pointermoveHandler = ((e: MouseEvent) => {
|
551
555
|
e.stopPropagation()
|
552
556
|
e.preventDefault()
|
553
557
|
let column = this.columns[idx]
|
@@ -561,14 +565,14 @@ export class DataGridHeader extends LitElement {
|
|
561
565
|
this._notifyWidthChange(idx, width)
|
562
566
|
}).bind(this)
|
563
567
|
|
564
|
-
var
|
565
|
-
document.removeEventListener('
|
566
|
-
document.removeEventListener('
|
568
|
+
var pointerupHandler = ((e: MouseEvent) => {
|
569
|
+
document.removeEventListener('pointermove', pointermoveHandler)
|
570
|
+
document.removeEventListener('pointerup', pointerupHandler)
|
567
571
|
|
568
|
-
|
572
|
+
pointermoveHandler(e)
|
569
573
|
}).bind(this)
|
570
574
|
|
571
|
-
document.addEventListener('
|
572
|
-
document.addEventListener('
|
575
|
+
document.addEventListener('pointermove', pointermoveHandler)
|
576
|
+
document.addEventListener('pointerup', pointerupHandler)
|
573
577
|
}
|
574
578
|
}
|
package/src/data-grist.ts
CHANGED
@@ -31,7 +31,8 @@ import {
|
|
31
31
|
GristSelectFunction,
|
32
32
|
PaginationConfig,
|
33
33
|
PersonalGristPreference,
|
34
|
-
SortersConfig
|
34
|
+
SortersConfig,
|
35
|
+
ValidationReason
|
35
36
|
} from './types'
|
36
37
|
import { convertListParamToSearchString, convertSearchStringToListParam } from './utils'
|
37
38
|
|
@@ -478,28 +479,28 @@ export class DataGrist extends LitElement implements DataConsumer {
|
|
478
479
|
</ox-grid>
|
479
480
|
`
|
480
481
|
: this.mode == 'CARD'
|
481
|
-
|
482
|
-
|
483
|
-
|
484
|
-
|
485
|
-
|
486
|
-
|
487
|
-
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
482
|
+
? html`
|
483
|
+
<ox-card
|
484
|
+
id="grist"
|
485
|
+
.config=${this.compiledConfig}
|
486
|
+
.data=${this._data}
|
487
|
+
.sorters=${this.sorters || []}
|
488
|
+
.filters=${this.filters || []}
|
489
|
+
?empty=${empty}
|
490
|
+
>
|
491
|
+
</ox-card>
|
492
|
+
`
|
493
|
+
: html`
|
494
|
+
<ox-list
|
495
|
+
id="grist"
|
496
|
+
.config=${this.compiledConfig}
|
497
|
+
.data=${this._data}
|
498
|
+
.sorters=${this.sorters || []}
|
499
|
+
.filters=${this.filters || []}
|
500
|
+
?empty=${empty}
|
501
|
+
>
|
502
|
+
</ox-list>
|
503
|
+
`}
|
503
504
|
</div>
|
504
505
|
|
505
506
|
<div id="spinner" ?show=${this._showSpinner}></div>
|
@@ -1190,8 +1191,43 @@ export class DataGrist extends LitElement implements DataConsumer {
|
|
1190
1191
|
})
|
1191
1192
|
)
|
1192
1193
|
}
|
1193
|
-
|
1194
|
+
/**
|
1195
|
+
* Returns the current pagination limit.
|
1196
|
+
* @returns {number} The current pagination limit value
|
1197
|
+
*/
|
1194
1198
|
getCurrentLimit() {
|
1195
1199
|
return this.dataProvider?.limit || ZERO_PAGINATION.limit
|
1196
1200
|
}
|
1201
|
+
|
1202
|
+
/**
|
1203
|
+
* Checks the validity of dirty records.
|
1204
|
+
* @returns {Array<{record: GristRecord, invalidFields: Array<{field: string, reason: ValidationReason}>}>} List of invalid records and their corresponding invalid fields
|
1205
|
+
*/
|
1206
|
+
checkDirtyRecordsValidity(): { record: GristRecord; invalidFields: { field: string; reason: ValidationReason }[] }[] {
|
1207
|
+
const records = this.dirtyRecords
|
1208
|
+
const validationResults = []
|
1209
|
+
|
1210
|
+
for (const record of records) {
|
1211
|
+
const invalidFields = []
|
1212
|
+
|
1213
|
+
for (const column of this.compiledConfig.columns) {
|
1214
|
+
if (column.record?.mandatory && (record[column.name] === undefined || record[column.name] === null)) {
|
1215
|
+
invalidFields.push({
|
1216
|
+
field: column.name,
|
1217
|
+
reason: ValidationReason.MANDATORY
|
1218
|
+
})
|
1219
|
+
}
|
1220
|
+
// Additional validation rules can be implemented here.
|
1221
|
+
}
|
1222
|
+
|
1223
|
+
if (invalidFields.length > 0) {
|
1224
|
+
validationResults.push({
|
1225
|
+
record,
|
1226
|
+
invalidFields
|
1227
|
+
})
|
1228
|
+
}
|
1229
|
+
}
|
1230
|
+
|
1231
|
+
return validationResults
|
1232
|
+
}
|
1197
1233
|
}
|
@@ -26,7 +26,16 @@ const OPTIONS: Intl.DateTimeFormatOptions = {
|
|
26
26
|
// timeZone: 'America/Los_Angeles'
|
27
27
|
}
|
28
28
|
|
29
|
-
|
29
|
+
function getSafeFormatter(locale: string, options: Intl.DateTimeFormatOptions) {
|
30
|
+
try {
|
31
|
+
const safeLocale = locale || 'en-US'
|
32
|
+
return new Intl.DateTimeFormat(safeLocale, options)
|
33
|
+
} catch (e) {
|
34
|
+
return new Intl.DateTimeFormat('en-US', options)
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
const formatter = getSafeFormatter(navigator.language, OPTIONS)
|
30
39
|
|
31
40
|
@customElement('ox-record-partial')
|
32
41
|
export class RecordPartial extends LitElement {
|
@@ -21,6 +21,8 @@ export class OxGristEditorFile extends OxGristEditor {
|
|
21
21
|
|
22
22
|
get editorTemplate() {
|
23
23
|
var value = typeof this.value === 'string' ? null : this.value
|
24
|
-
|
24
|
+
var { multiple = false } = this.column.record || {}
|
25
|
+
|
26
|
+
return html` <input style="opacity: 1" type="file" ?multiple=${multiple} /> `
|
25
27
|
}
|
26
28
|
}
|
@@ -107,9 +107,9 @@ export class OxGristEditor extends LitElement {
|
|
107
107
|
this.addEventListener('keydown', this._onkeydown.bind(this))
|
108
108
|
|
109
109
|
/* editor mode 인 경우의 마우스 움직임이, grist-body의 이벤트 처리에 의해서 에디터를 리셋시킬 수 있으므로, 이벤트 전파를 막는다. */
|
110
|
-
this.addEventListener('
|
111
|
-
this.addEventListener('
|
112
|
-
this.addEventListener('
|
110
|
+
this.addEventListener('pointerdown', (e: Event) => e.stopPropagation())
|
111
|
+
this.addEventListener('pointermove', (e: Event) => e.stopPropagation())
|
112
|
+
this.addEventListener('pointerup', (e: Event) => e.stopPropagation())
|
113
113
|
|
114
114
|
const { name = '' } = this.column
|
115
115
|
const { align, defaultValue } = this.column.record
|
@@ -7,7 +7,7 @@ import { customElement, property, state } from 'lit/decorators.js'
|
|
7
7
|
import { OxPopup } from '@operato/popup'
|
8
8
|
|
9
9
|
import { DataGrist } from '../data-grist'
|
10
|
-
import { ColumnConfig, GristRecord } from '../types'
|
10
|
+
import { ColumnConfig, GristRecord, ValidationReason } from '../types'
|
11
11
|
import { RecordView } from './record-view'
|
12
12
|
|
13
13
|
@customElement('ox-record-creator')
|
@@ -43,6 +43,23 @@ export class RecordCreator extends LitElement {
|
|
43
43
|
return html`<slot></slot>`
|
44
44
|
}
|
45
45
|
|
46
|
+
validateRecord(record: GristRecord): { field: string; reason: ValidationReason }[] {
|
47
|
+
const columns = this.grist!.compiledConfig.columns;
|
48
|
+
const invalidFields: { field: string; reason: ValidationReason }[] = [];
|
49
|
+
|
50
|
+
columns.forEach(column => {
|
51
|
+
if (column.record?.mandatory && (record[column.name] === undefined || record[column.name] === null || record[column.name] === '')) {
|
52
|
+
invalidFields.push({
|
53
|
+
field: column.name,
|
54
|
+
reason: ValidationReason.MANDATORY
|
55
|
+
});
|
56
|
+
}
|
57
|
+
// 여기에 추가적인 유효성 검사 규칙을 구현할 수 있습니다.
|
58
|
+
});
|
59
|
+
|
60
|
+
return invalidFields
|
61
|
+
}
|
62
|
+
|
46
63
|
lightPopupRecordView() {
|
47
64
|
const config = this.grist!.compiledConfig
|
48
65
|
var title = 'create'
|
@@ -87,10 +104,21 @@ export class RecordCreator extends LitElement {
|
|
87
104
|
@cancel=${(e: Event) => {
|
88
105
|
popup.close()
|
89
106
|
}}
|
90
|
-
@ok=${(e: Event) => {
|
91
|
-
|
107
|
+
@ok=${async (e: Event) => {
|
108
|
+
const view = e.currentTarget as RecordView
|
92
109
|
|
93
|
-
|
110
|
+
// 레코드 밸리데이션 체크
|
111
|
+
const invalidFields = await this.validateRecord(view.record);
|
112
|
+
if (invalidFields.length > 0) {
|
113
|
+
// const firstInvalidField = invalidFields[0];
|
114
|
+
// if (firstInvalidField) {
|
115
|
+
// view.setFocus(firstInvalidField.field)
|
116
|
+
// }
|
117
|
+
view.setFocusOnInvalid(invalidFields);
|
118
|
+
return false;
|
119
|
+
}
|
120
|
+
|
121
|
+
popup.close()
|
94
122
|
|
95
123
|
this.dispatchEvent(
|
96
124
|
new CustomEvent('ok', {
|
@@ -142,8 +170,26 @@ export class RecordCreator extends LitElement {
|
|
142
170
|
|
143
171
|
recordView.addEventListener('ok', async (e: Event) => {
|
144
172
|
const view = e.currentTarget as RecordView
|
173
|
+
|
174
|
+
// 레코드 밸리데이션 체크
|
175
|
+
const invalidFields = await this.validateRecord(view.record);
|
176
|
+
if (invalidFields.length > 0) {
|
177
|
+
const firstInvalidField = invalidFields[0];
|
178
|
+
if (firstInvalidField) {
|
179
|
+
const fieldElement = view.renderRoot?.querySelector(`[name="${firstInvalidField}"]`);
|
180
|
+
if (fieldElement) {
|
181
|
+
(fieldElement as HTMLElement).focus();
|
182
|
+
}
|
183
|
+
}
|
184
|
+
return false;
|
185
|
+
}
|
186
|
+
|
145
187
|
if (await this.callback?.(view.record)) {
|
146
188
|
popup.close()
|
189
|
+
} else {
|
190
|
+
// 밸리데이션 실패 시 처리
|
191
|
+
console.error('레코드 밸리데이션 실패');
|
192
|
+
// 여기에 사용자에게 오류 메시지를 표시하는 로직을 추가할 수 있습니다.
|
147
193
|
}
|
148
194
|
})
|
149
195
|
|
@@ -5,8 +5,9 @@ import { css, html, LitElement } from 'lit'
|
|
5
5
|
import { customElement, property } from 'lit/decorators.js'
|
6
6
|
|
7
7
|
import { ZERO_RECORD } from '../configure/zero-config'
|
8
|
-
import { ColumnConfig, GristRecord } from '../types'
|
8
|
+
import { ColumnConfig, GristRecord, ValidationReason } from '../types'
|
9
9
|
import { recordViewBodyClickHandler } from './event-handlers/record-view-body-click-handler'
|
10
|
+
import i18next from 'i18next'
|
10
11
|
|
11
12
|
@customElement('ox-record-view-body')
|
12
13
|
export class RecordViewBody extends LitElement {
|
@@ -23,7 +24,7 @@ export class RecordViewBody extends LitElement {
|
|
23
24
|
}
|
24
25
|
|
25
26
|
div[content] {
|
26
|
-
align-self:
|
27
|
+
align-self: center;
|
27
28
|
|
28
29
|
display: grid;
|
29
30
|
grid-template-columns: 2fr 3fr 2fr 3fr;
|
@@ -69,7 +70,6 @@ export class RecordViewBody extends LitElement {
|
|
69
70
|
}
|
70
71
|
|
71
72
|
ox-grid-field {
|
72
|
-
justify-content: var();
|
73
73
|
background-color: var(--record-view-grid-field-background-color, var(--md-sys-color-surface-container-lowest));
|
74
74
|
border: var(--record-view-grid-field-border);
|
75
75
|
border-radius: var(--md-sys-shape-corner-small);
|
@@ -96,6 +96,20 @@ export class RecordViewBody extends LitElement {
|
|
96
96
|
font-weight: bold;
|
97
97
|
}
|
98
98
|
|
99
|
+
.highlight-invalid {
|
100
|
+
position: relative;
|
101
|
+
padding: var(--spacing-tiny) var(--spacing-small);
|
102
|
+
}
|
103
|
+
|
104
|
+
.highlight-invalid::after {
|
105
|
+
content: attr(data-reason); /* 콘텐츠를 동적으로 변경하기 위해 data-reason 속성을 사용 */
|
106
|
+
color: red;
|
107
|
+
font-size: 12px;
|
108
|
+
position: absolute;
|
109
|
+
left: 0;
|
110
|
+
bottom: -8px; /* 라벨 아래쪽에 메시지를 표시 */
|
111
|
+
}
|
112
|
+
|
99
113
|
@media only screen and (max-width: 1000px) {
|
100
114
|
div[content] {
|
101
115
|
grid-template-columns: 2fr 3fr;
|
@@ -152,6 +166,35 @@ export class RecordViewBody extends LitElement {
|
|
152
166
|
this.removeEventListener('keydown', this._onKeyDown)
|
153
167
|
}
|
154
168
|
|
169
|
+
setFocus(fieldElement: HTMLElement) {
|
170
|
+
fieldElement?.dispatchEvent(new CustomEvent('click', { bubbles: true, composed: true }))
|
171
|
+
}
|
172
|
+
|
173
|
+
setFocusOnInvalid(invalidFields: { field: string; reason: ValidationReason }[]) {
|
174
|
+
const allLabels = this.renderRoot.querySelectorAll('label')
|
175
|
+
allLabels.forEach((label: HTMLLabelElement) => {
|
176
|
+
label.classList.remove('highlight-invalid')
|
177
|
+
label.removeAttribute('data-reason')
|
178
|
+
})
|
179
|
+
|
180
|
+
// 유효성 검사를 통과하지 못한 필드에 대해 처리
|
181
|
+
invalidFields.forEach(({ field, reason }, index) => {
|
182
|
+
const labelElement = this.renderRoot.querySelector(`[data-name="${field}"]`) as HTMLLabelElement
|
183
|
+
const fieldElement = this.renderRoot.querySelector(`[data-name="${field}"] + ox-grid-field`) as HTMLInputElement
|
184
|
+
|
185
|
+
// 동적으로 data-reason 속성을 설정하여 메시지를 변경
|
186
|
+
if (labelElement) {
|
187
|
+
labelElement.classList.add('highlight-invalid')
|
188
|
+
labelElement.setAttribute('data-reason', '(' + i18next.t(`text.validation-reason.${reason}`) + ')')
|
189
|
+
}
|
190
|
+
|
191
|
+
// 첫 번째 필드에 포커스 설정
|
192
|
+
if (index === 0 && fieldElement) {
|
193
|
+
this.setFocus(fieldElement)
|
194
|
+
}
|
195
|
+
})
|
196
|
+
}
|
197
|
+
|
155
198
|
_onKeyDown(event: KeyboardEvent) {
|
156
199
|
if (event.key === 'Tab') {
|
157
200
|
const fields = Array.from(this.renderRoot.querySelectorAll('ox-grid-field[tabstop]'))
|
@@ -165,7 +208,8 @@ export class RecordViewBody extends LitElement {
|
|
165
208
|
}
|
166
209
|
|
167
210
|
event.preventDefault()
|
168
|
-
fields[nextIndex]
|
211
|
+
const nextField = fields[nextIndex] as HTMLInputElement
|
212
|
+
nextField && this.setFocus(nextField)
|
169
213
|
}
|
170
214
|
}
|
171
215
|
|
@@ -185,7 +229,7 @@ export class RecordViewBody extends LitElement {
|
|
185
229
|
let dirtyFields = record['__dirtyfields__'] || {}
|
186
230
|
|
187
231
|
return html`
|
188
|
-
<label ?editable=${editable} ?wide=${wide}>
|
232
|
+
<label ?editable=${editable} ?wide=${wide} data-name=${column.name}>
|
189
233
|
<span>${mandatory ? '*' : ''}${this._renderLabel(column)}</span>
|
190
234
|
<md-icon>edit</md-icon>
|
191
235
|
</label>
|
@@ -4,12 +4,14 @@ import '@operato/input/ox-input-file.js'
|
|
4
4
|
import '../data-grid/data-grid-field'
|
5
5
|
|
6
6
|
import { css, html, LitElement } from 'lit'
|
7
|
-
import { customElement, property } from 'lit/decorators.js'
|
7
|
+
import { customElement, property, query } from 'lit/decorators.js'
|
8
8
|
|
9
9
|
import { ScrollbarStyles } from '@operato/styles'
|
10
10
|
|
11
11
|
import { ZERO_RECORD } from '../configure/zero-config'
|
12
|
-
import { ColumnConfig, GristRecord } from '../types'
|
12
|
+
import { ColumnConfig, GristRecord, ValidationReason } from '../types'
|
13
|
+
|
14
|
+
import { RecordViewBody } from './record-view-body'
|
13
15
|
|
14
16
|
@customElement('ox-record-view')
|
15
17
|
export class RecordView extends LitElement {
|
@@ -66,6 +68,8 @@ export class RecordView extends LitElement {
|
|
66
68
|
@property({ type: Object }) record: GristRecord = ZERO_RECORD
|
67
69
|
@property({ type: Number }) rowIndex: number = -1
|
68
70
|
|
71
|
+
@query('ox-record-view-body') body!: RecordViewBody
|
72
|
+
|
69
73
|
render() {
|
70
74
|
return html`
|
71
75
|
<ox-record-view-body .columns=${this.columns} .record=${this.record} .rowIndex=${this.rowIndex}>
|
@@ -82,6 +86,10 @@ export class RecordView extends LitElement {
|
|
82
86
|
this.setAttribute('tabindex', '0')
|
83
87
|
}
|
84
88
|
|
89
|
+
setFocusOnInvalid(invalidFields: { field: string; reason: ValidationReason }[]) {
|
90
|
+
this.body.setFocusOnInvalid(invalidFields)
|
91
|
+
}
|
92
|
+
|
85
93
|
onReset() {
|
86
94
|
this.focus()
|
87
95
|
|
@@ -12,7 +12,7 @@ export const OxGristRendererBoolean: FieldRenderer = (value, column, record, row
|
|
12
12
|
type="checkbox"
|
13
13
|
.checked=${!!value && !!String(value).match(/true/i)}
|
14
14
|
?disabled=${!editable}
|
15
|
-
@
|
15
|
+
@pointerdown=${(e: Event) => {
|
16
16
|
/* edit mode로 전환되지 않도록 차단함. 체크박스인풋은 렌더러 모드에서도 처리가능하므로. */
|
17
17
|
e.stopPropagation()
|
18
18
|
}}
|
@@ -1,17 +1,31 @@
|
|
1
|
-
import { html } from 'lit'
|
1
|
+
import { html, nothing } from 'lit'
|
2
2
|
|
3
3
|
import { FieldRenderer } from '../types'
|
4
4
|
|
5
|
+
function download(file: { mimetype: string; name: string; fullpath: string }) {
|
6
|
+
const element = document.createElement('a')
|
7
|
+
element.setAttribute('href', file.fullpath)
|
8
|
+
element.setAttribute('download', file.name!)
|
9
|
+
document.body.appendChild(element)
|
10
|
+
element.click()
|
11
|
+
}
|
12
|
+
|
5
13
|
export const OxGristRendererFile: FieldRenderer = (value, column, record, rowIndex, field) => {
|
6
14
|
let text: string
|
7
15
|
|
8
16
|
if (!value) {
|
9
|
-
|
17
|
+
return html`<span> </span>`
|
10
18
|
} else if (typeof value === 'string') {
|
11
|
-
|
19
|
+
return html`<span>${value}</span>`
|
12
20
|
} else {
|
13
|
-
|
21
|
+
const files = value as { mimetype: string; name: string; fullpath: string }[]
|
22
|
+
return html`<span
|
23
|
+
>${files.map(
|
24
|
+
(file, idx) => html`
|
25
|
+
<a @click=${() => download(file)} file><md-icon>description</md-icon>${file.name}</a>
|
26
|
+
${files.length - 1 == idx ? html`</br>` : nothing}
|
27
|
+
`
|
28
|
+
)}</span
|
29
|
+
>`
|
14
30
|
}
|
15
|
-
|
16
|
-
return html`<span>${text}</span>`
|
17
31
|
}
|
package/src/types.ts
CHANGED
@@ -9,7 +9,8 @@ body {
|
|
9
9
|
/* monthly layout */
|
10
10
|
--calendar-monthly-ol-margin: var(--margin-default) 0;
|
11
11
|
--calendar-monthly-ol-top-border: 2px solid var(--md-sys-color-secondary);
|
12
|
-
--calendar-current-
|
12
|
+
--calendar-current-month-background-color: var(--md-sys-color-surface-variant);
|
13
|
+
--calendar-current-month-color: var(--md-sys-color-on-surface);
|
13
14
|
--calendar-monthly-label-align: left;
|
14
15
|
--calendar-monthly-label-padding: var(--padding-narrow) 0;
|
15
16
|
--calendar-monthly-label-color: var(--md-sys-color-secondary);
|
@@ -35,6 +36,7 @@ body {
|
|
35
36
|
--calendar-weekly-ol-margin: var(--margin-default) 0;
|
36
37
|
--calendar-weekly-ol-top-border: 2px solid var(--md-sys-color-secondary);
|
37
38
|
--calendar-current-week-background-color: var(--md-sys-color-surface-variant);
|
39
|
+
--calendar-current-week-color: var(--md-sys-color-on-surface);
|
38
40
|
--calendar-weekly-label-align: center;
|
39
41
|
--calendar-weekly-label-padding: var(--padding-narrow) 0;
|
40
42
|
--calendar-weekly-label-color: var(--md-sys-color-secondary);
|
package/translations/en.json
CHANGED
@@ -6,5 +6,9 @@
|
|
6
6
|
"label.accumulator_avg": "avg",
|
7
7
|
"label.accumulator_count": "cnt",
|
8
8
|
"label.accumulator_max": "min",
|
9
|
-
"label.accumulator_min": "max"
|
9
|
+
"label.accumulator_min": "max",
|
10
|
+
"text.validation-reason.MANDATORY": "mandatory",
|
11
|
+
"text.validation-reason.FORMAT": "invalid format",
|
12
|
+
"text.validation-reason.RANGE": "out of range",
|
13
|
+
"text.validation-reason.UNIQUE": "duplicate value"
|
10
14
|
}
|
package/translations/ja.json
CHANGED
@@ -5,5 +5,9 @@
|
|
5
5
|
"label.accumulator_avg": "平均",
|
6
6
|
"label.accumulator_count": "カウント",
|
7
7
|
"label.accumulator_max": "最小",
|
8
|
-
"label.accumulator_min": "最大"
|
8
|
+
"label.accumulator_min": "最大",
|
9
|
+
"text.validation-reason.MANDATORY": "必須",
|
10
|
+
"text.validation-reason.FORMAT": "形式が正しくありません",
|
11
|
+
"text.validation-reason.RANGE": "範囲外",
|
12
|
+
"text.validation-reason.UNIQUE": "重複"
|
9
13
|
}
|