@operato/data-grist 8.0.0-alpha.4 → 8.0.0-alpha.45

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 (51) hide show
  1. package/CHANGELOG.md +135 -0
  2. package/dist/src/data-card/record-card.js +10 -1
  3. package/dist/src/data-card/record-card.js.map +1 -1
  4. package/dist/src/data-grid/data-grid-body.js +15 -6
  5. package/dist/src/data-grid/data-grid-body.js.map +1 -1
  6. package/dist/src/data-grid/data-grid-footer.js +2 -0
  7. package/dist/src/data-grid/data-grid-footer.js.map +1 -1
  8. package/dist/src/data-grid/data-grid-header.d.ts +1 -1
  9. package/dist/src/data-grid/data-grid-header.js +13 -9
  10. package/dist/src/data-grid/data-grid-header.js.map +1 -1
  11. package/dist/src/data-grist.d.ts +16 -1
  12. package/dist/src/data-grist.js +53 -20
  13. package/dist/src/data-grist.js.map +1 -1
  14. package/dist/src/data-list/record-partial.js +10 -1
  15. package/dist/src/data-list/record-partial.js.map +1 -1
  16. package/dist/src/editors/ox-grist-editor.js +3 -3
  17. package/dist/src/editors/ox-grist-editor.js.map +1 -1
  18. package/dist/src/record-view/record-creator.d.ts +5 -0
  19. package/dist/src/record-view/record-creator.js +47 -4
  20. package/dist/src/record-view/record-creator.js.map +1 -1
  21. package/dist/src/record-view/record-view-body.d.ts +6 -1
  22. package/dist/src/record-view/record-view-body.js +43 -5
  23. package/dist/src/record-view/record-view-body.js.map +1 -1
  24. package/dist/src/record-view/record-view.d.ts +7 -1
  25. package/dist/src/record-view/record-view.js +7 -1
  26. package/dist/src/record-view/record-view.js.map +1 -1
  27. package/dist/src/renderers/ox-grist-renderer-boolean.js +1 -1
  28. package/dist/src/renderers/ox-grist-renderer-boolean.js.map +1 -1
  29. package/dist/src/types.d.ts +6 -0
  30. package/dist/src/types.js +7 -0
  31. package/dist/src/types.js.map +1 -1
  32. package/dist/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +18 -18
  34. package/src/data-card/record-card.ts +10 -1
  35. package/src/data-grid/data-grid-body.ts +18 -6
  36. package/src/data-grid/data-grid-footer.ts +2 -0
  37. package/src/data-grid/data-grid-header.ts +13 -9
  38. package/src/data-grist.ts +60 -24
  39. package/src/data-list/record-partial.ts +10 -1
  40. package/src/editors/ox-grist-editor.ts +3 -3
  41. package/src/record-view/record-creator.ts +50 -4
  42. package/src/record-view/record-view-body.ts +49 -5
  43. package/src/record-view/record-view.ts +10 -2
  44. package/src/renderers/ox-grist-renderer-boolean.ts +1 -1
  45. package/src/types.ts +8 -0
  46. package/themes/calendar-theme.css +3 -1
  47. package/translations/en.json +5 -1
  48. package/translations/ja.json +5 -1
  49. package/translations/ko.json +5 -1
  50. package/translations/ms.json +5 -1
  51. 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.4",
3
+ "version": "8.0.0-alpha.45",
4
4
  "description": "User interface for grid (desktop) and list (mobile)",
5
5
  "author": "heartyoh",
6
6
  "main": "dist/index.js",
@@ -62,32 +62,32 @@
62
62
  },
63
63
  "dependencies": {
64
64
  "@material/web": "^2.0.0",
65
- "@operato/headroom": "^8.0.0-alpha.0",
66
- "@operato/input": "^8.0.0-alpha.4",
67
- "@operato/p13n": "^8.0.0-alpha.4",
68
- "@operato/popup": "^8.0.0-alpha.4",
69
- "@operato/pull-to-refresh": "^8.0.0-alpha.0",
70
- "@operato/styles": "^8.0.0-alpha.4",
71
- "@operato/time-calculator": "^8.0.0-alpha.0",
72
- "@operato/utils": "^8.0.0-alpha.0",
65
+ "@operato/headroom": "^8.0.0-alpha.37",
66
+ "@operato/input": "^8.0.0-alpha.45",
67
+ "@operato/p13n": "^8.0.0-alpha.41",
68
+ "@operato/popup": "^8.0.0-alpha.41",
69
+ "@operato/pull-to-refresh": "^8.0.0-alpha.37",
70
+ "@operato/styles": "^8.0.0-alpha.37",
71
+ "@operato/time-calculator": "^8.0.0-alpha.37",
72
+ "@operato/utils": "^8.0.0-alpha.37",
73
73
  "i18next": "^23.11.5",
74
74
  "json5": "^2.2.0",
75
75
  "lit": "^3.1.2",
76
76
  "lodash-es": "^4.17.21"
77
77
  },
78
78
  "devDependencies": {
79
- "@custom-elements-manifest/analyzer": "^0.9.2",
79
+ "@custom-elements-manifest/analyzer": "^0.10.0",
80
80
  "@hatiolab/prettier-config": "^1.0.0",
81
81
  "@open-wc/eslint-config": "^12.0.3",
82
- "@open-wc/testing": "^3.1.6",
82
+ "@open-wc/testing": "^4.0.0",
83
83
  "@types/lodash-es": "^4.17.5",
84
- "@typescript-eslint/eslint-plugin": "^7.0.1",
85
- "@typescript-eslint/parser": "^7.0.1",
86
- "@web/dev-server": "^0.3.0",
84
+ "@typescript-eslint/eslint-plugin": "^8.0.0",
85
+ "@typescript-eslint/parser": "^8.0.0",
86
+ "@web/dev-server": "^0.4.0",
87
87
  "@web/dev-server-storybook": "^2.0.1",
88
- "@web/test-runner": "^0.18.0",
89
- "concurrently": "^8.0.1",
90
- "eslint": "^8.39.0",
88
+ "@web/test-runner": "^0.19.0",
89
+ "concurrently": "^9.0.0",
90
+ "eslint": "^9.0.0",
91
91
  "eslint-config-prettier": "^9.1.0",
92
92
  "husky": "^9.0.11",
93
93
  "lint-staged": "^15.2.2",
@@ -108,5 +108,5 @@
108
108
  "prettier --write"
109
109
  ]
110
110
  },
111
- "gitHead": "ac097b448ea96721b3418132e92988afdf764519"
111
+ "gitHead": "4099b112eea6e2c554d1d0a74e7b026f377bb039"
112
112
  }
@@ -25,7 +25,16 @@ const OPTIONS: Intl.DateTimeFormatOptions = {
25
25
  // timeZone: 'America/Los_Angeles'
26
26
  }
27
27
 
28
- const formatter = new Intl.DateTimeFormat(navigator.language, OPTIONS)
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('mousedown', (event: Event) => {
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('mousemove', (event: Event) => {
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('mouseup', (event: Event) => {
352
+ this.renderRoot.addEventListener('pointerup', (event: Event) => {
353
+ event.preventDefault()
354
+ event.stopPropagation()
355
+
344
356
  this._draggable = false
345
357
  })
346
358
 
@@ -21,6 +21,8 @@ export class DataGridFooter extends LitElement {
21
21
  background-color: var(--grid-footer-background-color);
22
22
  font-size: var(--grid-footer-font-size);
23
23
  align-items: center;
24
+
25
+ user-select: none;
24
26
  }
25
27
 
26
28
  :host * {
@@ -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" @mousedown=${(e: MouseEvent) => this._mousedown(e, index)}>&nbsp;</span>
271
+ <span splitter draggable="false" @pointerdown=${(e: PointerEvent) => this._pointerdown(e, index)}
272
+ >&nbsp;</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
- _mousedown(e: MouseEvent, idx: number) {
550
+ _pointerdown(e: MouseEvent, idx: number) {
547
551
  e.stopPropagation()
548
552
  e.preventDefault()
549
553
 
550
- var mousemoveHandler = ((e: MouseEvent) => {
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 mouseupHandler = ((e: MouseEvent) => {
565
- document.removeEventListener('mousemove', mousemoveHandler)
566
- document.removeEventListener('mouseup', mouseupHandler)
568
+ var pointerupHandler = ((e: MouseEvent) => {
569
+ document.removeEventListener('pointermove', pointermoveHandler)
570
+ document.removeEventListener('pointerup', pointerupHandler)
567
571
 
568
- mousemoveHandler(e)
572
+ pointermoveHandler(e)
569
573
  }).bind(this)
570
574
 
571
- document.addEventListener('mousemove', mousemoveHandler)
572
- document.addEventListener('mouseup', mouseupHandler)
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
- ? html`
482
- <ox-card
483
- id="grist"
484
- .config=${this.compiledConfig}
485
- .data=${this._data}
486
- .sorters=${this.sorters || []}
487
- .filters=${this.filters || []}
488
- ?empty=${empty}
489
- >
490
- </ox-card>
491
- `
492
- : html`
493
- <ox-list
494
- id="grist"
495
- .config=${this.compiledConfig}
496
- .data=${this._data}
497
- .sorters=${this.sorters || []}
498
- .filters=${this.filters || []}
499
- ?empty=${empty}
500
- >
501
- </ox-list>
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
- const formatter = new Intl.DateTimeFormat(navigator.language, OPTIONS)
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 {
@@ -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('mousedown', (e: Event) => e.stopPropagation())
111
- this.addEventListener('mousemove', (e: Event) => e.stopPropagation())
112
- this.addEventListener('mouseup', (e: Event) => e.stopPropagation())
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
- popup.close()
107
+ @ok=${async (e: Event) => {
108
+ const view = e.currentTarget as RecordView
92
109
 
93
- const view = e.currentTarget as RecordView
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: auto;
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]?.dispatchEvent(new CustomEvent('click', { bubbles: true, composed: true }))
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
- @mousedown=${(e: Event) => {
15
+ @pointerdown=${(e: Event) => {
16
16
  /* edit mode로 전환되지 않도록 차단함. 체크박스인풋은 렌더러 모드에서도 처리가능하므로. */
17
17
  e.stopPropagation()
18
18
  }}
package/src/types.ts CHANGED
@@ -803,3 +803,11 @@ export type FilterPreference = {
803
803
  hidden?: boolean
804
804
  value?: any
805
805
  }
806
+
807
+ export enum ValidationReason {
808
+ MANDATORY = 'MANDATORY',
809
+ FORMAT = 'FORMAT',
810
+ RANGE = 'RANGE',
811
+ UNIQUE = 'UNIQUE'
812
+ }
813
+
@@ -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-monty-background-color: var(--md-sys-color-surface-variant);
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);
@@ -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
  }
@@ -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
  }
@@ -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
  }
@@ -5,5 +5,9 @@
5
5
  "label.accumulator_avg": "Purata",
6
6
  "label.accumulator_count": "Kiraan",
7
7
  "label.accumulator_max": "Minimum",
8
- "label.accumulator_min": "Maksimum"
8
+ "label.accumulator_min": "Maksimum",
9
+ "text.validation-reason.MANDATORY": "wajib",
10
+ "text.validation-reason.FORMAT": "format tidak sah",
11
+ "text.validation-reason.RANGE": "diluar jangka",
12
+ "text.validation-reason.UNIQUE": "nilai duplikat"
9
13
  }