@operato/data-grist 1.7.3 → 1.8.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/src/data-grid/data-grid-body-style.js +11 -11
  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 +1 -0
  5. package/dist/src/data-grid/data-grid-body.js +11 -0
  6. package/dist/src/data-grid/data-grid-body.js.map +1 -1
  7. package/dist/src/data-grid/data-grid-field-header.d.ts +24 -0
  8. package/dist/src/data-grid/data-grid-field-header.js +381 -0
  9. package/dist/src/data-grid/data-grid-field-header.js.map +1 -0
  10. package/dist/src/data-grid/data-grid-field.d.ts +1 -0
  11. package/dist/src/data-grid/data-grid-field.js +9 -0
  12. package/dist/src/data-grid/data-grid-field.js.map +1 -1
  13. package/dist/src/data-grid/data-grid-header.d.ts +2 -1
  14. package/dist/src/data-grid/data-grid-header.js +16 -2
  15. package/dist/src/data-grid/data-grid-header.js.map +1 -1
  16. package/dist/src/data-grid/data-grid.d.ts +1 -1
  17. package/dist/src/data-grid/data-grid.js +7 -3
  18. package/dist/src/data-grid/data-grid.js.map +1 -1
  19. package/dist/src/types.d.ts +1 -0
  20. package/dist/src/types.js.map +1 -1
  21. package/dist/stories/fixed-column.stories.d.ts +26 -0
  22. package/dist/stories/fixed-column.stories.js +444 -0
  23. package/dist/stories/fixed-column.stories.js.map +1 -0
  24. package/dist/tsconfig.tsbuildinfo +1 -1
  25. package/package.json +3 -3
  26. package/src/data-grid/data-grid-body-style.ts +11 -11
  27. package/src/data-grid/data-grid-body.ts +8 -0
  28. package/src/data-grid/data-grid-field.ts +7 -0
  29. package/src/data-grid/data-grid-header.ts +18 -5
  30. package/src/data-grid/data-grid.ts +5 -5
  31. package/src/types.ts +1 -0
  32. package/stories/fixed-column.stories.ts +476 -0
  33. package/themes/grist-theme.css +4 -4
@@ -1,7 +1,7 @@
1
1
  import '@operato/popup/ox-popup.js'
2
2
  import '@material/mwc-icon'
3
3
 
4
- import { css, html, LitElement } from 'lit'
4
+ import { css, html, LitElement, PropertyValues } from 'lit'
5
5
  import { customElement, property } from 'lit/decorators.js'
6
6
  import throttle from 'lodash-es/throttle'
7
7
 
@@ -32,7 +32,7 @@ export class DataGridHeader extends LitElement {
32
32
 
33
33
  white-space: nowrap;
34
34
  overflow: hidden;
35
- background-color: var(--grid-header-background-color);
35
+ background-color: var(--grist-background-color);
36
36
  border-top: var(--grid-header-top-border);
37
37
  border-bottom: var(--grid-header-bottom-border);
38
38
  padding: var(--grid-header-padding);
@@ -115,6 +115,12 @@ export class DataGridHeader extends LitElement {
115
115
  margin-bottom: 5px;
116
116
  }
117
117
 
118
+ [fixed] {
119
+ position: sticky;
120
+ background-color: #fff;
121
+ z-index: 1;
122
+ }
123
+
118
124
  @media print {
119
125
  :host {
120
126
  grid-template-columns: var(--grid-template-print-columns);
@@ -125,10 +131,8 @@ export class DataGridHeader extends LitElement {
125
131
 
126
132
  @property({ type: Array }) columns: ColumnConfig[] = ZERO_COLUMNS
127
133
  @property({ type: Object }) data: GristData = ZERO_DATA
128
-
129
134
  @property({ type: Array }) sorters: SortersConfig = []
130
135
  @property({ type: Array }) filters: FilterValue[] = []
131
-
132
136
  @property({ type: Boolean, attribute: 'filtering-feature' }) filteringFeature: boolean = false
133
137
 
134
138
  private _lastAccVal?: number
@@ -179,7 +183,7 @@ export class DataGridHeader extends LitElement {
179
183
  ${columns.map((column, idx) =>
180
184
  !column.hidden
181
185
  ? html`
182
- <div ?gutter=${column.type == 'gutter'} column>
186
+ <div ?gutter=${column.type == 'gutter'} ?fixed=${column.fixed} column>
183
187
  <span
184
188
  for-title
185
189
  style="text-align:${column.record.align || 'left'};${column.header?.style || ''}"
@@ -224,6 +228,15 @@ export class DataGridHeader extends LitElement {
224
228
  `
225
229
  }
226
230
 
231
+ updated(changes: PropertyValues) {
232
+ if (changes.has('columns')) {
233
+ const fixeds = Array.from(this.renderRoot.querySelectorAll('div[fixed]')) as HTMLElement[]
234
+ fixeds.forEach((header: HTMLElement) => {
235
+ header.style.left = header.offsetLeft + 'px'
236
+ })
237
+ }
238
+ }
239
+
227
240
  _renderHeader(column: ColumnConfig) {
228
241
  var { renderer } = column.header || {}
229
242
  var title = renderer.call(this, column)
@@ -58,7 +58,7 @@ export class DataGrid extends DataManipulator {
58
58
  @query('ox-grid-header', true) header!: DataGridHeader
59
59
  @query('ox-grid-footer', true) footer!: LitElement
60
60
 
61
- private _widths?: string
61
+ private widths: string[] = []
62
62
 
63
63
  firstUpdated() {
64
64
  /* header and body scroll synchronization */
@@ -194,7 +194,7 @@ export class DataGrid extends DataManipulator {
194
194
  /*
195
195
  * 컬럼 모델 마지막에 'auto' cell을 추가하여, 자투리 영역을 꽉 채워서 표시한다.
196
196
  */
197
- this._widths = columns
197
+ const widths = columns
198
198
  .filter(column => !column.hidden)
199
199
  .map(column => {
200
200
  switch (typeof column.width) {
@@ -211,8 +211,8 @@ export class DataGrid extends DataManipulator {
211
211
  .concat(['auto'])
212
212
  .join(' ')
213
213
 
214
- this.style.setProperty('--grid-template-columns', this._widths)
215
- this.style.setProperty('--grid-template-print-columns', this._widths.replace(/px/gi, 'fr'))
214
+ this.style.setProperty('--grid-template-columns', widths)
215
+ this.style.setProperty('--grid-template-print-columns', widths.replace(/px/gi, 'fr'))
216
216
  }
217
217
 
218
218
  render() {
@@ -235,7 +235,7 @@ export class DataGrid extends DataManipulator {
235
235
  }}
236
236
  ></ox-grid-header>
237
237
 
238
- <ox-grid-body .config=${this.config} .columns=${columns} .data=${data} .focused=${this.focused}>
238
+ <ox-grid-body .config=${this.config} .columns=${columns} .data=${data} .focused=${this.focused!}>
239
239
  ${this.empty ? html` <ox-empty-note title="NO RECORDS"></ox-empty-note> ` : html``}
240
240
  </ox-grid-body>
241
241
  ${paginatable ? html` <ox-grid-footer .data=${data} .pagination=${this.pagination}></ox-grid-footer> ` : html``}
package/src/types.ts CHANGED
@@ -125,6 +125,7 @@ export type ColumnConfig = {
125
125
  type: string
126
126
  name: string
127
127
  gutterName?: string
128
+ fixed?: boolean
128
129
  header: HeaderConfig
129
130
  record: RecordConfig
130
131
  handlers: GristEventHandlerSet
@@ -0,0 +1,476 @@
1
+ import '../src/index.js'
2
+ import '../src/filters/filters-form.js'
3
+ import '../src/sorters/sorters-control.js'
4
+ import '../src/record-view/record-creator.js'
5
+ import '@operato/popup/ox-popup-list.js'
6
+ import '@material/mwc-icon'
7
+
8
+ import { html, TemplateResult } from 'lit'
9
+
10
+ import {
11
+ ColumnConfig,
12
+ FetchHandler,
13
+ GristClassifier,
14
+ GristEventHandlerSet,
15
+ GristRecord,
16
+ ValidationCallback
17
+ } from '../src/types.js'
18
+
19
+ const fetchHandler: FetchHandler = async ({ page, limit }) => {
20
+ var total = 120993
21
+ var start = (page! - 1) * limit!
22
+
23
+ await new Promise(resolve => setTimeout(resolve, 500))
24
+
25
+ return {
26
+ total,
27
+ records: Array(limit! * page! > total ? total % limit! : limit)
28
+ .fill('')
29
+ .map((item, idx) => {
30
+ return {
31
+ id: String(idx),
32
+ name: idx % 2 ? `shnam-${start + idx + 1}` : `heartyoh-${start + idx + 1}`,
33
+ description: idx % 2 ? `hatiolabmanager${start + idx + 1}1234567890` : `hatiosea manager-${start + idx + 1}`,
34
+ email: idx % 2 ? `shnam-${start + idx + 1}@gmail.com` : `heartyoh-${start + idx + 1}@gmail.com`,
35
+ active: Math.round(Math.random() * 2) % 2 ? true : false,
36
+ barcode: idx % 2 ? `1234567890${start + idx + 1}` : `0987654321${start + idx + 1}`,
37
+ company:
38
+ idx % 2
39
+ ? {
40
+ id: '2',
41
+ name: 'HatioLAB',
42
+ description: `경기도 성남시-${start + idx + 1}`
43
+ }
44
+ : {
45
+ id: '3',
46
+ name: 'HatioSEA',
47
+ description: `말레이시아 세티아알람-${start + idx + 1}`
48
+ },
49
+ thumbnail:
50
+ idx % 4 === 0
51
+ ? '' /* no source */
52
+ : idx % 4 === 1
53
+ ? `http://www.hatiolab.com/assets/img/operato-biz3.png`
54
+ : idx % 4 === 2
55
+ ? `http://www.hatiolab.com/assets/img/thingsboard-30.png`
56
+ : `http://www.hatiolab.com/wrong-url.png` /* wrong source */,
57
+ role: ['admin', 'worker', 'tester'][idx % 3],
58
+ color: idx % 2 ? `#87f018` : `#180f87`,
59
+ rate: Math.round(Math.random() * 100),
60
+ dynamicType: ['text', 'email', 'checkbox', 'color', 'progress', 'barcode'][idx % 5],
61
+ dynamicValue: ['abcdefghijkl', 'heartyoh@hatiolab.com', 'true', 'orange', '50', '1234567890'][idx % 5],
62
+ homepage:
63
+ idx % 2 ? `http://hatiolab.com/${start + idx + 1}` : `http://deadpool.hatiolab.com/${start + idx + 1}`,
64
+ json5: {
65
+ abc: 'abc',
66
+ value: 123
67
+ },
68
+ createdAt: Date.now(),
69
+ updatedAt: Date.now()
70
+ }
71
+ })
72
+ }
73
+ }
74
+
75
+ const config = {
76
+ list: {
77
+ thumbnail: 'thumbnail',
78
+ fields: ['name', 'description'],
79
+ details: ['role', 'email']
80
+ },
81
+ columns: [
82
+ {
83
+ type: 'gutter',
84
+ gutterName: 'dirty',
85
+ fixed: true
86
+ },
87
+ {
88
+ type: 'gutter',
89
+ gutterName: 'sequence',
90
+ fixed: true
91
+ },
92
+ {
93
+ type: 'gutter',
94
+ gutterName: 'row-selector',
95
+ multiple: true,
96
+ fixed: true
97
+ },
98
+ {
99
+ type: 'gutter',
100
+ gutterName: 'button',
101
+ icon: 'edit',
102
+ title: 'edit',
103
+ handlers: {
104
+ click: function () {
105
+ console.log('clicked')
106
+ }
107
+ },
108
+ fixed: true
109
+ },
110
+ {
111
+ type: 'gutter',
112
+ gutterName: 'button',
113
+ icon: 'add',
114
+ title: 'add',
115
+ handlers: {
116
+ click: 'record-copy'
117
+ }
118
+ },
119
+ {
120
+ type: 'gutter',
121
+ gutterName: 'button',
122
+ icon: 'arrow_downward',
123
+ title: 'download',
124
+ handlers: {
125
+ click: 'move-down'
126
+ }
127
+ },
128
+ {
129
+ type: 'string',
130
+ name: 'id',
131
+ hidden: true
132
+ },
133
+ {
134
+ type: 'link',
135
+ name: 'name',
136
+ label: true,
137
+ header: 'name',
138
+ record: {
139
+ editable: true,
140
+ options: {
141
+ // href: 'http://hatiolab.com',
142
+ href: function (column: ColumnConfig, record: GristRecord, rowIndex: number) {
143
+ return record['homepage']
144
+ },
145
+ target: '_blank'
146
+ }
147
+ },
148
+ filter: 'search',
149
+ sortable: true,
150
+ width: 120
151
+ },
152
+ {
153
+ type: 'string',
154
+ name: 'description',
155
+ header: 'description',
156
+ filter: 'search',
157
+ record: {
158
+ editable: true,
159
+ align: 'left'
160
+ },
161
+ width: 200,
162
+ handlers: {
163
+ click: (columns, data, column, record, rowIndex, target) => {
164
+ alert(`${column!.name} ${record![column!.name]}, row : ${rowIndex}`)
165
+ }
166
+ } as GristEventHandlerSet
167
+ },
168
+ {
169
+ type: 'email',
170
+ name: 'email',
171
+ label: true,
172
+ header: 'email',
173
+ record: {
174
+ editable: true
175
+ },
176
+ filter: 'search',
177
+ sortable: true,
178
+ width: 130,
179
+ validation: function (after, before, record, column) {
180
+ if (after.indexOf('@') == -1) {
181
+ document.dispatchEvent(
182
+ new CustomEvent('notify', {
183
+ detail: {
184
+ type: 'error',
185
+ message: `invalid value - ${after}`
186
+ }
187
+ })
188
+ )
189
+ return false
190
+ }
191
+ return true
192
+ } as ValidationCallback
193
+ },
194
+ {
195
+ type: 'boolean',
196
+ name: 'active',
197
+ header: 'active',
198
+ record: {
199
+ editable: true
200
+ },
201
+ filter: true,
202
+ handlers: {
203
+ dblclick: () => {
204
+ const grist = document.querySelector('ox-grist') as any
205
+ console.log(grist!.dirtyRecords)
206
+ }
207
+ },
208
+ sortable: true,
209
+ width: 60
210
+ },
211
+ {
212
+ type: 'select',
213
+ name: 'role',
214
+ label: true,
215
+ header: 'role',
216
+ record: {
217
+ options: ['', 'admin', 'worker', 'tester'],
218
+ editable: true
219
+ },
220
+ filter: true,
221
+ sortable: true,
222
+ width: 120
223
+ },
224
+ {
225
+ type: 'color',
226
+ name: 'color',
227
+ header: 'color',
228
+ record: {
229
+ editable: true
230
+ },
231
+ sortable: true,
232
+ width: 50
233
+ },
234
+ {
235
+ type: 'float',
236
+ name: 'rate',
237
+ header: 'rate',
238
+ record: {
239
+ align: 'right',
240
+ editable: true,
241
+ defaultValue: 10000.1
242
+ },
243
+ filter: 'between',
244
+ sortable: true,
245
+ width: 50
246
+ },
247
+ {
248
+ type: 'json5',
249
+ name: 'json5',
250
+ header: 'JSON5',
251
+ width: 200
252
+ },
253
+ {
254
+ type: 'image',
255
+ name: 'thumbnail',
256
+ header: 'thumbnail',
257
+ record: {
258
+ editable: true
259
+ }
260
+ },
261
+ {
262
+ type: 'datetime',
263
+ name: 'updatedAt',
264
+ header: 'updated at',
265
+ record: {
266
+ editable: true,
267
+ defaultValue: {
268
+ name: 'now'
269
+ }
270
+ },
271
+ filter: 'between',
272
+ sortable: true,
273
+ width: 180
274
+ },
275
+ {
276
+ type: 'datetime',
277
+ name: 'createdAt',
278
+ header: 'created at',
279
+ record: {
280
+ editable: false
281
+ },
282
+ sortable: true,
283
+ width: 180
284
+ }
285
+ ],
286
+ rows: {
287
+ selectable: {
288
+ multiple: true
289
+ },
290
+ handlers: {
291
+ focus: 'select-row-toggle'
292
+ },
293
+ classifier: function (record, rowIndex) {
294
+ const rate = record['rate']
295
+ const emphasized =
296
+ rate < 10 ? ['black', 'white'] : rate < 25 ? ['yellow', 'blue'] : rate < 40 ? ['cyan', 'red'] : undefined
297
+ return {
298
+ emphasized
299
+ }
300
+ } as GristClassifier
301
+ },
302
+ sorters: [
303
+ {
304
+ name: 'name',
305
+ desc: true
306
+ },
307
+ {
308
+ name: 'email'
309
+ }
310
+ ],
311
+ pagination: {
312
+ pages: [20, 30, 50, 100, 200]
313
+ }
314
+ }
315
+
316
+ export default {
317
+ title: 'fixed column',
318
+ component: 'ox-grist',
319
+ argTypes: {
320
+ config: { control: 'object' }
321
+ }
322
+ }
323
+
324
+ interface Story<T> {
325
+ (args: T): TemplateResult
326
+ args?: Partial<T>
327
+ argTypes?: Record<string, unknown>
328
+ }
329
+
330
+ interface ArgTypes {
331
+ config: object
332
+ }
333
+
334
+ const Template: Story<ArgTypes> = ({ config }: ArgTypes) => html` <link
335
+ href="https://fonts.googleapis.com/css?family=Material+Icons&display=block"
336
+ rel="stylesheet"
337
+ />
338
+ <link href="/themes/app-theme.css" rel="stylesheet" />
339
+ <link href="/themes/oops-theme.css" rel="stylesheet" />
340
+ <link href="/themes/grist-theme.css" rel="stylesheet" />
341
+
342
+ <style>
343
+ [slot='headroom'] {
344
+ display: flex;
345
+ flex-direction: row;
346
+ align-items: center;
347
+ padding: var(--padding-default) var(--padding-wide);
348
+ background-color: var(--theme-white-color);
349
+ box-shadow: var(--box-shadow);
350
+
351
+ --mdc-icon-size: 24px;
352
+ }
353
+ #sorters mwc-icon,
354
+ #modes mwc-icon {
355
+ --mdc-icon-size: 18px;
356
+ }
357
+ #sorters {
358
+ margin-left: auto;
359
+ margin-right: var(--margin-default);
360
+ padding-left: var(--padding-narrow);
361
+ border-bottom: var(--border-dark-color);
362
+ position: relative;
363
+ color: var(--secondary-color);
364
+ font-size: var(--fontsize-default);
365
+ user-select: none;
366
+ }
367
+
368
+ #sorters > * {
369
+ padding: var(--padding-narrow);
370
+ vertical-align: middle;
371
+ }
372
+
373
+ #modes > * {
374
+ padding: var(--padding-narrow);
375
+ opacity: 0.5;
376
+ color: var(--primary-text-color);
377
+ cursor: pointer;
378
+ }
379
+
380
+ #modes > mwc-icon[active] {
381
+ border-radius: 9px;
382
+ background-color: rgba(var(--primary-color-rgb), 0.05);
383
+ opacity: 1;
384
+ color: var(--secondary-text-color);
385
+ cursor: default;
386
+ }
387
+
388
+ #modes > mwc-icon:hover {
389
+ opacity: 1;
390
+ color: var(--secondary-text-color);
391
+ }
392
+
393
+ #add {
394
+ width: 50px;
395
+ text-align: right;
396
+ }
397
+
398
+ #add button {
399
+ background-color: var(--primary-color);
400
+ border: 0;
401
+ border-radius: 50%;
402
+ padding: 5px;
403
+ width: 36px;
404
+ height: 36px;
405
+ cursor: pointer;
406
+ }
407
+
408
+ #add button:hover {
409
+ background-color: var(--focus-background-color);
410
+ box-shadow: var(--box-shadow);
411
+ }
412
+
413
+ #add button mwc-icon {
414
+ font-size: 2em;
415
+ color: var(--theme-white-color);
416
+ }
417
+
418
+ #filters {
419
+ display: flex;
420
+ justify-content: center;
421
+ align-items: center;
422
+ }
423
+
424
+ #filters * {
425
+ margin-right: var(--margin-default);
426
+ }
427
+
428
+ @media only screen and (max-width: 460px) {
429
+ #filters {
430
+ flex-direction: column;
431
+ }
432
+
433
+ #modes {
434
+ display: none;
435
+ }
436
+ }
437
+ </style>
438
+
439
+ <ox-grist
440
+ mode="GRID"
441
+ .config=${config}
442
+ .fetchHandler=${fetchHandler}
443
+ @filters-change=${(e: Event) => console.log('filters', (e.target as any).filters)}
444
+ >
445
+ <div slot="headroom">
446
+ <div id="filters">
447
+ <ox-filters-form autofocus></ox-filters-form>
448
+ </div>
449
+
450
+ <div id="sorters">
451
+ Sort
452
+ <mwc-icon
453
+ @click=${(e: Event) => {
454
+ const target = e.currentTarget as HTMLElement
455
+ ;(target.closest('#sorters')!.querySelector('#sorter-control') as any).open({
456
+ right: 0,
457
+ top: target.offsetTop + target.offsetHeight
458
+ })
459
+ }}
460
+ >expand_more</mwc-icon
461
+ >
462
+ <ox-popup id="sorter-control">
463
+ <ox-sorters-control> </ox-sorters-control>
464
+ </ox-popup>
465
+ </div>
466
+
467
+ <ox-record-creator id="add" light-popup>
468
+ <button><mwc-icon>add</mwc-icon></button>
469
+ </ox-record-creator>
470
+ </div>
471
+ </ox-grist>`
472
+
473
+ export const Regular = Template.bind({})
474
+ Regular.args = {
475
+ config
476
+ }
@@ -1,8 +1,8 @@
1
1
  body {
2
2
  --grid-container-border-color: 1px solid rgba(0, 0, 0, 0.09);
3
3
  --grid-container-border-width: 1px 0;
4
- --grid-wrap-container-border:0px solid transparent;
5
- --grid-wrap-container-border-width:0;
4
+ --grid-wrap-container-border: 0px solid transparent;
5
+ --grid-wrap-container-border-width: 0;
6
6
 
7
7
  --grist-background-color: var(--main-section-background-color);
8
8
  --grist-title-margin: 0 0 0 10px;
@@ -38,10 +38,10 @@ body {
38
38
  --grid-header-filter-title-font: normal 12px var(--theme-font);
39
39
  --grid-header-filter-title-icon-color: var(--primary-color);
40
40
 
41
- --grid-gutter-padding:var(--padding-default) 0;
41
+ --grid-gutter-padding: var(--padding-default) 0;
42
42
 
43
43
  --grid-record-background-color: var(--theme-white-color);
44
- --grid-record-odd-background-color: rgba(255, 255, 255, 0.4);
44
+ --grid-record-odd-background-color: #f9f7f5;
45
45
  --grid-record-padding: 0 0 0 var(--padding-default);
46
46
  --grid-record-color: var(--secondary-color);
47
47
  --grid-record-color-hover: var(--primary-color);