@inglorious/ui 9.2.0 → 9.4.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/package.json +2 -2
- package/src/controls/input/template.js +2 -1
- package/src/data-display/data-grid/base.css +13 -2
- package/src/data-display/data-grid/data-grid.stories.js +39 -0
- package/src/data-display/data-grid/filters/range.js +0 -2
- package/src/data-display/data-grid/handlers.js +56 -2
- package/src/data-display/data-grid/helpers.js +54 -3
- package/src/data-display/data-grid/index.js +7 -0
- package/src/data-display/data-grid/style.css +1 -0
- package/src/data-display/data-grid/template.js +178 -19
- package/src/data-display/data-grid/theme.css +17 -0
- package/src/examples/dashboard/primitive-section.js +68 -68
- package/types/controls/input.d.ts +2 -0
- package/types/data-display/data-grid.d.ts +61 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/ui",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.4.0",
|
|
4
4
|
"description": "A design system for Inglorious Web with render-function components and customizable themes",
|
|
5
5
|
"author": "Matteo Antony Mistretta <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -315,7 +315,7 @@
|
|
|
315
315
|
"@iconify-icons/fa7-regular": "^2.0.0",
|
|
316
316
|
"@iconify-icons/fa7-solid": "^2.0.0",
|
|
317
317
|
"@iconify-icons/material-symbols": "^2.0.6",
|
|
318
|
-
"@inglorious/charts": "12.1
|
|
318
|
+
"@inglorious/charts": "12.2.1",
|
|
319
319
|
"@inglorious/eslint-config": "1.2.0",
|
|
320
320
|
"@inglorious/store": "12.0.0",
|
|
321
321
|
"@inglorious/vite-plugin-jsx": "2.0.5",
|
|
@@ -93,6 +93,7 @@ export const Input = {
|
|
|
93
93
|
type, // eslint-disable-line no-unused-vars
|
|
94
94
|
name = "",
|
|
95
95
|
label,
|
|
96
|
+
ariaLabel,
|
|
96
97
|
hint, // eslint-disable-line no-unused-vars
|
|
97
98
|
messageId,
|
|
98
99
|
inputType = "text",
|
|
@@ -129,7 +130,7 @@ export const Input = {
|
|
|
129
130
|
id=${controlId || nothing}
|
|
130
131
|
name=${name}
|
|
131
132
|
type=${inputType}
|
|
132
|
-
aria-label=${fallbackLabel(controlId, label)}
|
|
133
|
+
aria-label=${ariaLabel || fallbackLabel(controlId, label)}
|
|
133
134
|
aria-describedby=${messageId || nothing}
|
|
134
135
|
aria-invalid=${error ? "true" : "false"}
|
|
135
136
|
.value=${value}
|
|
@@ -35,9 +35,16 @@
|
|
|
35
35
|
gap: var(--iw-space-2);
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
38
|
+
/* The two ends of a range share the column; native date and number
|
|
39
|
+
fields would otherwise keep a width of their own and spill out. */
|
|
40
|
+
.iw-data-grid-filter-range > .iw-input {
|
|
40
41
|
flex: 1 1 0;
|
|
42
|
+
min-width: 0;
|
|
43
|
+
|
|
44
|
+
.iw-input-frame,
|
|
45
|
+
.iw-input-control {
|
|
46
|
+
min-width: 0;
|
|
47
|
+
}
|
|
41
48
|
}
|
|
42
49
|
|
|
43
50
|
.iw-data-grid-row {
|
|
@@ -48,6 +55,10 @@
|
|
|
48
55
|
display: flex;
|
|
49
56
|
}
|
|
50
57
|
|
|
58
|
+
.iw-data-grid-edits-actions {
|
|
59
|
+
display: flex;
|
|
60
|
+
}
|
|
61
|
+
|
|
51
62
|
.iw-data-grid-column-resizer {
|
|
52
63
|
position: absolute;
|
|
53
64
|
inset: 0 0 0 auto;
|
|
@@ -74,6 +74,45 @@ export const Default = {
|
|
|
74
74
|
},
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** Editable columns: change cells, then reset or save the edits in the footer. */
|
|
78
|
+
export const Editable = {
|
|
79
|
+
args: {
|
|
80
|
+
...Default.args,
|
|
81
|
+
id: "dataGridEditable",
|
|
82
|
+
search: null,
|
|
83
|
+
selection: [],
|
|
84
|
+
columns: [
|
|
85
|
+
{ id: "name", title: "Name", isSortable: true, isEditable: true },
|
|
86
|
+
{ id: "age", title: "Age", type: "number", isEditable: true },
|
|
87
|
+
{
|
|
88
|
+
id: "active",
|
|
89
|
+
title: "Active",
|
|
90
|
+
type: "boolean",
|
|
91
|
+
width: 100,
|
|
92
|
+
isEditable: true,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: "role",
|
|
96
|
+
title: "Role",
|
|
97
|
+
width: 140,
|
|
98
|
+
isEditable: true,
|
|
99
|
+
options: [
|
|
100
|
+
{ value: "Admin", label: "Admin" },
|
|
101
|
+
{ value: "User", label: "User" },
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
parameters: {
|
|
107
|
+
docs: {
|
|
108
|
+
description: {
|
|
109
|
+
story:
|
|
110
|
+
"Columns with `isEditable` put an editor in each cell, by type, or a drop-down with `options`. Edits stay apart from the rows, counted in the footer, until saved.",
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
}
|
|
115
|
+
|
|
77
116
|
export const Paginated = {
|
|
78
117
|
args: {
|
|
79
118
|
...Default.args,
|
|
@@ -19,7 +19,6 @@ export const RangeFilter = {
|
|
|
19
19
|
placeholder: column.filter.placeholder ?? "≥",
|
|
20
20
|
size: "sm",
|
|
21
21
|
isFullWidth: true,
|
|
22
|
-
className: "iw-data-grid-filter-range-input",
|
|
23
22
|
onChange: (value) => {
|
|
24
23
|
const formattedValue = value ? format(value, column.type) : null
|
|
25
24
|
entity.onFilterChange?.({
|
|
@@ -34,7 +33,6 @@ export const RangeFilter = {
|
|
|
34
33
|
placeholder: column.filter.placeholder ?? "≤",
|
|
35
34
|
size: "sm",
|
|
36
35
|
isFullWidth: true,
|
|
37
|
-
className: "iw-data-grid-filter-range-input",
|
|
38
36
|
onChange: (value) => {
|
|
39
37
|
const formattedValue = value ? format(value, column.type) : null
|
|
40
38
|
entity.onFilterChange?.({
|
|
@@ -3,7 +3,12 @@
|
|
|
3
3
|
* @typedef {import('../../../types/data-display/data-grid').Column} Column
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
getEditedRows,
|
|
8
|
+
getRowKeyValue,
|
|
9
|
+
getRows,
|
|
10
|
+
getTotalRows,
|
|
11
|
+
} from "./helpers.js"
|
|
7
12
|
|
|
8
13
|
const MIN_COLUMN_WIDTH = 72
|
|
9
14
|
const VIRTUAL_START = 0
|
|
@@ -285,6 +290,43 @@ export function selectionClear(entity) {
|
|
|
285
290
|
entity.selection.length = 0
|
|
286
291
|
}
|
|
287
292
|
|
|
293
|
+
/**
|
|
294
|
+
* Records the user's edit of a cell, kept apart from the rows until saved. An
|
|
295
|
+
* edit back to the row's own value is no edit at all.
|
|
296
|
+
* @param {DataGridEntity} entity
|
|
297
|
+
* @param {{ rowId: string|number, columnId: string, value: any }} payload
|
|
298
|
+
*/
|
|
299
|
+
export function cellChange(entity, { rowId, columnId, value }) {
|
|
300
|
+
const row = entity.rows.find((item) => getRowKeyValue(entity, item) === rowId)
|
|
301
|
+
if (!row) return
|
|
302
|
+
|
|
303
|
+
entity.edits ??= {}
|
|
304
|
+
const edits = (entity.edits[rowId] ??= {})
|
|
305
|
+
if (isSameValue(row[columnId], value)) {
|
|
306
|
+
delete edits[columnId]
|
|
307
|
+
} else {
|
|
308
|
+
edits[columnId] = value
|
|
309
|
+
}
|
|
310
|
+
if (!Object.keys(edits).length) delete entity.edits[rowId]
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Drops every unsaved edit.
|
|
315
|
+
* @param {DataGridEntity} entity
|
|
316
|
+
*/
|
|
317
|
+
export function editsReset(entity) {
|
|
318
|
+
entity.edits = {}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Writes the unsaved edits into the rows.
|
|
323
|
+
* @param {DataGridEntity} entity
|
|
324
|
+
*/
|
|
325
|
+
export function editsSave(entity) {
|
|
326
|
+
entity.rows = getEditedRows(entity)
|
|
327
|
+
entity.edits = {}
|
|
328
|
+
}
|
|
329
|
+
|
|
288
330
|
/**
|
|
289
331
|
* Handles the scroll event to update the virtualized visible range.
|
|
290
332
|
* @param {DataGridEntity} entity
|
|
@@ -394,6 +436,7 @@ function initDataGrid(entity) {
|
|
|
394
436
|
}
|
|
395
437
|
entity.selection ??= []
|
|
396
438
|
entity.selectionAnchor ??= null
|
|
439
|
+
entity.edits ??= {}
|
|
397
440
|
entity.isMultiSelect ??= true
|
|
398
441
|
entity.isVirtualized ??= false
|
|
399
442
|
|
|
@@ -457,7 +500,7 @@ function getDefaultColumnFilter(type) {
|
|
|
457
500
|
*/
|
|
458
501
|
function getDefaultColumnWidth(filterType) {
|
|
459
502
|
if (filterType === "number") return 70
|
|
460
|
-
if (filterType === "range") return
|
|
503
|
+
if (filterType === "range") return 160
|
|
461
504
|
if (filterType === "select") return 70
|
|
462
505
|
if (filterType === "date") return 120
|
|
463
506
|
if (filterType === "time") return 120
|
|
@@ -521,3 +564,14 @@ function assertRowKeys(entity) {
|
|
|
521
564
|
seen.add(rowKey)
|
|
522
565
|
})
|
|
523
566
|
}
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* Whether two cell values are the same, dates by their time.
|
|
570
|
+
* @param {any} a
|
|
571
|
+
* @param {any} b
|
|
572
|
+
* @returns {boolean}
|
|
573
|
+
*/
|
|
574
|
+
function isSameValue(a, b) {
|
|
575
|
+
if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime()
|
|
576
|
+
return Object.is(a, b)
|
|
577
|
+
}
|
|
@@ -8,6 +8,57 @@ export function getRowKeyValue(entity, row) {
|
|
|
8
8
|
return row?.[entity.rowId]
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* A cell's value as shown: the user's unsaved edit if there is one, else the
|
|
13
|
+
* row's own.
|
|
14
|
+
* @param {TableEntity} entity
|
|
15
|
+
* @param {any} row
|
|
16
|
+
* @param {string} columnId
|
|
17
|
+
* @returns {any}
|
|
18
|
+
*/
|
|
19
|
+
export function getCellValue(entity, row, columnId) {
|
|
20
|
+
const edits = entity.edits?.[getRowKeyValue(entity, row)]
|
|
21
|
+
return edits && Object.hasOwn(edits, columnId)
|
|
22
|
+
? edits[columnId]
|
|
23
|
+
: row?.[columnId]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Whether a cell has an unsaved edit.
|
|
28
|
+
* @param {TableEntity} entity
|
|
29
|
+
* @param {any} row
|
|
30
|
+
* @param {string} columnId
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function isCellEdited(entity, row, columnId) {
|
|
34
|
+
const edits = entity.edits?.[getRowKeyValue(entity, row)]
|
|
35
|
+
return Boolean(edits && Object.hasOwn(edits, columnId))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* How many cells have unsaved edits.
|
|
40
|
+
* @param {TableEntity} entity
|
|
41
|
+
* @returns {number}
|
|
42
|
+
*/
|
|
43
|
+
export function getEditCount(entity) {
|
|
44
|
+
return Object.values(entity.edits ?? {}).reduce(
|
|
45
|
+
(count, edits) => count + Object.keys(edits).length,
|
|
46
|
+
0,
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Every row with its unsaved edits applied, in the rows' own order.
|
|
52
|
+
* @param {TableEntity} entity
|
|
53
|
+
* @returns {any[]}
|
|
54
|
+
*/
|
|
55
|
+
export function getEditedRows(entity) {
|
|
56
|
+
return entity.rows.map((row) => {
|
|
57
|
+
const edits = entity.edits?.[getRowKeyValue(entity, row)]
|
|
58
|
+
return edits ? { ...row, ...edits } : row
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
11
62
|
/**
|
|
12
63
|
* Gets the processed rows (filtered, searched, sorted, paginated).
|
|
13
64
|
* @param {TableEntity} entity
|
|
@@ -69,7 +120,7 @@ export function getPaginationInfo(entity) {
|
|
|
69
120
|
* @returns {"asc"|"desc"|null}
|
|
70
121
|
*/
|
|
71
122
|
export function getSortDirection(entity, columnId) {
|
|
72
|
-
const sort = entity.sorts
|
|
123
|
+
const sort = entity.sorts?.find((s) => s.column === columnId)
|
|
73
124
|
return sort?.direction || null
|
|
74
125
|
}
|
|
75
126
|
|
|
@@ -136,7 +187,7 @@ export function isSomeSelected(entity) {
|
|
|
136
187
|
* @returns {any[]}
|
|
137
188
|
*/
|
|
138
189
|
function applyFilters(entity, rows) {
|
|
139
|
-
if (!Object.keys(entity.filters).length) {
|
|
190
|
+
if (!Object.keys(entity.filters ?? {}).length) {
|
|
140
191
|
return rows
|
|
141
192
|
}
|
|
142
193
|
|
|
@@ -201,7 +252,7 @@ function applySearch(entity, rows) {
|
|
|
201
252
|
* @returns {any[]}
|
|
202
253
|
*/
|
|
203
254
|
function applySorts(entity, rows) {
|
|
204
|
-
if (!entity.sorts
|
|
255
|
+
if (!entity.sorts?.length) {
|
|
205
256
|
return rows
|
|
206
257
|
}
|
|
207
258
|
|
|
@@ -19,6 +19,9 @@ export const DataGrid = {
|
|
|
19
19
|
onPagePrev: () => api.notify(`#${id}:pagePrev`),
|
|
20
20
|
onPageSizeChange: (pageSize) =>
|
|
21
21
|
api.notify(`#${id}:pageSizeChange`, pageSize),
|
|
22
|
+
onCellChange: (payload) => api.notify(`#${id}:cellChange`, payload),
|
|
23
|
+
onEditsReset: () => api.notify(`#${id}:editsReset`),
|
|
24
|
+
onEditsSave: () => api.notify(`#${id}:editsSave`),
|
|
22
25
|
onVirtualMount: (containerEl) =>
|
|
23
26
|
api.notify(`#${id}:virtualMount`, containerEl),
|
|
24
27
|
onVirtualScroll: (containerEl) =>
|
|
@@ -30,6 +33,10 @@ export const DataGrid = {
|
|
|
30
33
|
}
|
|
31
34
|
|
|
32
35
|
export const {
|
|
36
|
+
getCellValue,
|
|
37
|
+
getEditCount,
|
|
38
|
+
getEditedRows,
|
|
39
|
+
isCellEdited,
|
|
33
40
|
getPaginationInfo,
|
|
34
41
|
getRows,
|
|
35
42
|
isSomeSelected,
|
|
@@ -10,16 +10,21 @@ import { classMap } from "@inglorious/web/directives/class-map"
|
|
|
10
10
|
import { repeat } from "@inglorious/web/directives/repeat"
|
|
11
11
|
import { when } from "@inglorious/web/directives/when"
|
|
12
12
|
|
|
13
|
+
import { Button } from "../../controls/button/index.js"
|
|
14
|
+
import { Checkbox } from "../../controls/checkbox/index.js"
|
|
13
15
|
import { Input } from "../../controls/input/index.js"
|
|
14
16
|
import { Select } from "../../controls/select/index.js"
|
|
15
17
|
import { Pagination } from "../../navigation/pagination/index.js"
|
|
16
18
|
import { VirtualList } from "../virtual-list/index.js"
|
|
17
19
|
import { Filters } from "./filters/index.js"
|
|
18
20
|
import {
|
|
21
|
+
getCellValue,
|
|
22
|
+
getEditCount,
|
|
19
23
|
getPaginationInfo,
|
|
20
24
|
getRowKeyValue,
|
|
21
25
|
getRows,
|
|
22
26
|
getSortDirection,
|
|
27
|
+
isCellEdited,
|
|
23
28
|
} from "./helpers.js"
|
|
24
29
|
|
|
25
30
|
const DIVISOR = 2
|
|
@@ -27,6 +32,14 @@ const DIVISOR = 2
|
|
|
27
32
|
const PRETTY_PAGE = 1
|
|
28
33
|
const FIRST_PAGE = 0
|
|
29
34
|
const LAST_PAGE = 1
|
|
35
|
+
const ONE_EDIT = 1
|
|
36
|
+
const WEB_URL = /^https?:\/\//i
|
|
37
|
+
/** Fields edit dates as `YYYY-MM-DD`. */
|
|
38
|
+
const DATE_LENGTH = 10
|
|
39
|
+
const DATE_PART_LENGTH = 2
|
|
40
|
+
/** Date's months count from zero, people's from one. */
|
|
41
|
+
const MONTH_OFFSET = 1
|
|
42
|
+
const INPUT_TYPES = { number: "number", date: "date", url: "url" }
|
|
30
43
|
|
|
31
44
|
export const DataGrid = {
|
|
32
45
|
/**
|
|
@@ -67,7 +80,7 @@ export const DataGrid = {
|
|
|
67
80
|
*/
|
|
68
81
|
renderHeader(props) {
|
|
69
82
|
return html`<div class="iw-data-grid-header">
|
|
70
|
-
${props.search
|
|
83
|
+
${when(props.search, () => this.renderToolbar?.(props))}
|
|
71
84
|
<div class="iw-data-grid-header-row">
|
|
72
85
|
${repeat(
|
|
73
86
|
props.columns,
|
|
@@ -108,7 +121,10 @@ export const DataGrid = {
|
|
|
108
121
|
@click=${() => column.isSortable && props.onSortChange?.(column.id)}
|
|
109
122
|
class="iw-data-grid-header-title"
|
|
110
123
|
>
|
|
111
|
-
${column.title}
|
|
124
|
+
${column.title}
|
|
125
|
+
${when(column.isSortable, () =>
|
|
126
|
+
getSortIcon(getSortDirection(props, column.id)),
|
|
127
|
+
)}
|
|
112
128
|
</div>
|
|
113
129
|
|
|
114
130
|
${when(column.isFilterable, () => Filters.render(props, column))}
|
|
@@ -225,7 +241,11 @@ export const DataGrid = {
|
|
|
225
241
|
props.columns,
|
|
226
242
|
(column) => column.id,
|
|
227
243
|
(column, index) =>
|
|
228
|
-
this.renderCell?.(props, {
|
|
244
|
+
this.renderCell?.(props, {
|
|
245
|
+
cell: getCellValue(props, row, column.id),
|
|
246
|
+
index,
|
|
247
|
+
row,
|
|
248
|
+
}),
|
|
229
249
|
)}
|
|
230
250
|
</div>`
|
|
231
251
|
},
|
|
@@ -235,25 +255,86 @@ export const DataGrid = {
|
|
|
235
255
|
* Override to customize per-column cell structure.
|
|
236
256
|
* @param {DataGridProps} props The data grid props.
|
|
237
257
|
* @param {object} payload The payload.
|
|
238
|
-
* @param {any} payload.cell The cell data.
|
|
258
|
+
* @param {any} payload.cell The cell data, with any unsaved edit.
|
|
239
259
|
* @param {number} payload.index The column index.
|
|
260
|
+
* @param {Row} payload.row The row the cell is in.
|
|
240
261
|
* @returns {TemplateResult} The rendered cell.
|
|
241
262
|
*/
|
|
242
|
-
renderCell(props, { cell, index }) {
|
|
263
|
+
renderCell(props, { cell, index, row }) {
|
|
243
264
|
const column = props.columns[index]
|
|
265
|
+
const isEditable = Boolean(column.isEditable && row)
|
|
244
266
|
|
|
267
|
+
// Typing in a cell's editor is not picking its row.
|
|
245
268
|
return html`<div
|
|
246
269
|
class="iw-data-grid-cell ${classMap({
|
|
247
270
|
"iw-data-grid-cell-number": column.type === "number",
|
|
248
271
|
"iw-data-grid-cell-date": column.type === "date",
|
|
249
272
|
"iw-data-grid-cell-boolean": column.type === "boolean",
|
|
273
|
+
"iw-data-grid-cell-editable": isEditable,
|
|
274
|
+
"iw-data-grid-cell-edited":
|
|
275
|
+
isEditable && isCellEdited(props, row, column.id),
|
|
250
276
|
})}"
|
|
251
277
|
style=${getColumnStyle(column)}
|
|
278
|
+
@click=${isEditable ? (event) => event.stopPropagation() : undefined}
|
|
252
279
|
>
|
|
253
|
-
${
|
|
280
|
+
${when(
|
|
281
|
+
isEditable,
|
|
282
|
+
() => this.renderEditor?.(props, { value: cell, column, row, index }),
|
|
283
|
+
() => this.renderValue?.(props, { value: cell, column, index }),
|
|
284
|
+
)}
|
|
254
285
|
</div>`
|
|
255
286
|
},
|
|
256
287
|
|
|
288
|
+
/**
|
|
289
|
+
* Renders the field that edits a cell of an `isEditable` column: a
|
|
290
|
+
* drop-down when the column has `options`, a checkbox for booleans, a date
|
|
291
|
+
* or number field for those types, and a text field otherwise.
|
|
292
|
+
* Override to edit some columns differently.
|
|
293
|
+
* @param {DataGridProps} props The data grid props.
|
|
294
|
+
* @param {object} payload The payload.
|
|
295
|
+
* @param {any} payload.value The cell's value, with any unsaved edit.
|
|
296
|
+
* @param {Column} payload.column The column definition.
|
|
297
|
+
* @param {Row} payload.row The row the cell is in.
|
|
298
|
+
* @returns {TemplateResult} The rendered editor.
|
|
299
|
+
*/
|
|
300
|
+
renderEditor(props, { value, column, row }) {
|
|
301
|
+
const change = (next) =>
|
|
302
|
+
props.onCellChange?.({
|
|
303
|
+
rowId: getRowKeyValue(props, row),
|
|
304
|
+
columnId: column.id,
|
|
305
|
+
value: next,
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
if (column.options) {
|
|
309
|
+
return Select.render({
|
|
310
|
+
size: "sm",
|
|
311
|
+
ariaLabel: column.title,
|
|
312
|
+
value: value ?? "",
|
|
313
|
+
options: column.options,
|
|
314
|
+
isFullWidth: true,
|
|
315
|
+
onChange: change,
|
|
316
|
+
})
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (column.type === "boolean") {
|
|
320
|
+
return Checkbox.render({
|
|
321
|
+
"aria-label": column.title,
|
|
322
|
+
isChecked: Boolean(value),
|
|
323
|
+
onChange: change,
|
|
324
|
+
})
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const saved = row[column.id]
|
|
328
|
+
return Input.render({
|
|
329
|
+
size: "sm",
|
|
330
|
+
ariaLabel: column.title,
|
|
331
|
+
inputType: INPUT_TYPES[column.type] ?? "text",
|
|
332
|
+
value: column.type === "date" ? toDateField(value) : (value ?? ""),
|
|
333
|
+
isFullWidth: true,
|
|
334
|
+
onChange: (text) => change(fromField(text, column.type, saved)),
|
|
335
|
+
})
|
|
336
|
+
},
|
|
337
|
+
|
|
257
338
|
/**
|
|
258
339
|
* Renders the raw cell value for a column.
|
|
259
340
|
* Override to format values or inject custom content.
|
|
@@ -262,8 +343,16 @@ export const DataGrid = {
|
|
|
262
343
|
* @param {any} payload.value The value to render.
|
|
263
344
|
* @returns {any} The value to be rendered.
|
|
264
345
|
*/
|
|
265
|
-
renderValue(_props, { value }) {
|
|
266
|
-
|
|
346
|
+
renderValue(_props, { value, column }) {
|
|
347
|
+
// A link only for a web address: a `javascript:` one must not run.
|
|
348
|
+
return when(
|
|
349
|
+
column.type === "url" && WEB_URL.test(String(value ?? "")),
|
|
350
|
+
() =>
|
|
351
|
+
html`<a href=${value} target="_blank" rel="noopener noreferrer"
|
|
352
|
+
>${value}</a
|
|
353
|
+
>`,
|
|
354
|
+
() => value,
|
|
355
|
+
)
|
|
267
356
|
},
|
|
268
357
|
|
|
269
358
|
/**
|
|
@@ -274,22 +363,57 @@ export const DataGrid = {
|
|
|
274
363
|
*/
|
|
275
364
|
renderFooter(props) {
|
|
276
365
|
const pagination = getPaginationInfo(props)
|
|
366
|
+
const editCount = getEditCount(props)
|
|
277
367
|
|
|
278
|
-
if (!pagination) {
|
|
368
|
+
if (!pagination && !editCount) {
|
|
279
369
|
return null
|
|
280
370
|
}
|
|
281
371
|
|
|
282
372
|
return html`<div class="iw-data-grid-footer">
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
373
|
+
${when(editCount, () => this.renderEdits?.(props, editCount))}
|
|
374
|
+
${when(
|
|
375
|
+
pagination,
|
|
376
|
+
() =>
|
|
377
|
+
html`<div class="iw-data-grid-footer-row">
|
|
378
|
+
<div>
|
|
379
|
+
${pagination.start + PRETTY_PAGE} to ${pagination.end} of
|
|
380
|
+
${pagination.totalRows} entries
|
|
381
|
+
</div>
|
|
382
|
+
|
|
383
|
+
${this.renderPagination?.(props, pagination)}
|
|
384
|
+
${when(pagination.pageSizes, () =>
|
|
385
|
+
this.renderPageSize?.(props, pagination),
|
|
386
|
+
)}
|
|
387
|
+
</div>`,
|
|
388
|
+
)}
|
|
389
|
+
</div>`
|
|
390
|
+
},
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Renders the bar that counts the unsaved edits, with buttons to reset or
|
|
394
|
+
* save them. Shown only while there are edits.
|
|
395
|
+
* @param {DataGridProps} props The data grid props.
|
|
396
|
+
* @param {number} editCount How many cells have unsaved edits.
|
|
397
|
+
* @returns {TemplateResult} The rendered bar.
|
|
398
|
+
*/
|
|
399
|
+
renderEdits(props, editCount) {
|
|
400
|
+
return html`<div class="iw-data-grid-footer-row iw-data-grid-edits">
|
|
401
|
+
<div role="status">
|
|
402
|
+
${editCount} ${editCount === ONE_EDIT ? "change" : "changes"}
|
|
403
|
+
</div>
|
|
404
|
+
<div class="iw-data-grid-edits-actions">
|
|
405
|
+
${Button.render({
|
|
406
|
+
children: "Reset",
|
|
407
|
+
variant: "ghost",
|
|
408
|
+
size: "sm",
|
|
409
|
+
onClick: () => props.onEditsReset?.(),
|
|
410
|
+
})}
|
|
411
|
+
${Button.render({
|
|
412
|
+
children: props.saveLabel ?? "Save changes",
|
|
413
|
+
color: "primary",
|
|
414
|
+
size: "sm",
|
|
415
|
+
onClick: () => props.onEditsSave?.(),
|
|
416
|
+
})}
|
|
293
417
|
</div>
|
|
294
418
|
</div>`
|
|
295
419
|
},
|
|
@@ -471,3 +595,38 @@ function getVirtualizationState(props, totalItems) {
|
|
|
471
595
|
visibleRange: { start, end },
|
|
472
596
|
}
|
|
473
597
|
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* A date, in whatever form a row keeps it, as a date field shows it.
|
|
601
|
+
* @param {Date|number|string|null|undefined} value
|
|
602
|
+
* @returns {string}
|
|
603
|
+
*/
|
|
604
|
+
function toDateField(value) {
|
|
605
|
+
if (value == null || value === "") return ""
|
|
606
|
+
if (typeof value === "string") return value.slice(0, DATE_LENGTH)
|
|
607
|
+
const date = new Date(value)
|
|
608
|
+
if (Number.isNaN(date.getTime())) return ""
|
|
609
|
+
const pad = (part) => String(part).padStart(DATE_PART_LENGTH, "0")
|
|
610
|
+
return `${date.getFullYear()}-${pad(date.getMonth() + MONTH_OFFSET)}-${pad(date.getDate())}`
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* What a field holds, back in the form the row kept the value in: a number,
|
|
615
|
+
* or a date as a `Date`, a timestamp or a string, as it was. A date that had
|
|
616
|
+
* a time of day keeps it.
|
|
617
|
+
* @param {string} text
|
|
618
|
+
* @param {string} type The column's type.
|
|
619
|
+
* @param {any} saved The row's own value.
|
|
620
|
+
* @returns {any}
|
|
621
|
+
*/
|
|
622
|
+
function fromField(text, type, saved) {
|
|
623
|
+
if (type === "number") return text === "" ? null : Number(text)
|
|
624
|
+
if (type !== "date") return text
|
|
625
|
+
if (text === "") return null
|
|
626
|
+
if (!(saved instanceof Date) && typeof saved !== "number") return text
|
|
627
|
+
|
|
628
|
+
const [year, month, day] = text.split("-").map(Number)
|
|
629
|
+
const date = new Date(saved)
|
|
630
|
+
date.setFullYear(year, month - MONTH_OFFSET, day)
|
|
631
|
+
return saved instanceof Date ? date : date.getTime()
|
|
632
|
+
}
|
|
@@ -145,4 +145,21 @@
|
|
|
145
145
|
);
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
|
+
|
|
149
|
+
/* An editor fills its cell, which keeps less padding around it. */
|
|
150
|
+
.iw-data-grid-cell-editable {
|
|
151
|
+
padding: var(--iw-space-2);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.iw-data-grid-cell-edited {
|
|
155
|
+
background: color-mix(in srgb, var(--iw-color-warning) 12%, transparent);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
.iw-data-grid-edits {
|
|
159
|
+
gap: var(--iw-space-3);
|
|
160
|
+
|
|
161
|
+
& + .iw-data-grid-footer-row {
|
|
162
|
+
margin-block-start: var(--iw-space-3);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
148
165
|
}
|
|
@@ -41,20 +41,20 @@ export const PrimitiveSection = {
|
|
|
41
41
|
variant: "h4",
|
|
42
42
|
children: showEmpty ? "Choose a Primitive" : name,
|
|
43
43
|
}),
|
|
44
|
-
category
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
summary
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
44
|
+
when(category, () =>
|
|
45
|
+
Typography.render({
|
|
46
|
+
variant: "body2",
|
|
47
|
+
color: "secondary",
|
|
48
|
+
children: `Category: ${category}`,
|
|
49
|
+
}),
|
|
50
|
+
),
|
|
51
|
+
when(summary, () =>
|
|
52
|
+
Typography.render({
|
|
53
|
+
variant: "body1",
|
|
54
|
+
color: "secondary",
|
|
55
|
+
children: summary,
|
|
56
|
+
}),
|
|
57
|
+
),
|
|
58
58
|
],
|
|
59
59
|
}),
|
|
60
60
|
Card.render({
|
|
@@ -99,61 +99,61 @@ export const PrimitiveSection = {
|
|
|
99
99
|
`,
|
|
100
100
|
}),
|
|
101
101
|
}),
|
|
102
|
-
showEmpty
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
],
|
|
155
|
-
}),
|
|
102
|
+
when(!showEmpty, () =>
|
|
103
|
+
Card.render({
|
|
104
|
+
padding: "xl",
|
|
105
|
+
className: "iw-primitive-example",
|
|
106
|
+
children: Flex.render({
|
|
107
|
+
direction: "column",
|
|
108
|
+
gap: "md",
|
|
109
|
+
children: [
|
|
110
|
+
Typography.render({
|
|
111
|
+
variant: "h6",
|
|
112
|
+
children: "Example",
|
|
113
|
+
}),
|
|
114
|
+
Grid.render({
|
|
115
|
+
minColumnWidth: "18rem",
|
|
116
|
+
gap: "lg",
|
|
117
|
+
children: [
|
|
118
|
+
Flex.render({
|
|
119
|
+
direction: "column",
|
|
120
|
+
gap: "sm",
|
|
121
|
+
children: [
|
|
122
|
+
Typography.render({
|
|
123
|
+
variant: "body2",
|
|
124
|
+
color: "secondary",
|
|
125
|
+
children: "Code",
|
|
126
|
+
}),
|
|
127
|
+
html`<pre class="iw-primitive-code"><code>${
|
|
128
|
+
exampleCode ?? "// Example coming soon."
|
|
129
|
+
}</code></pre>`,
|
|
130
|
+
],
|
|
131
|
+
}),
|
|
132
|
+
Flex.render({
|
|
133
|
+
direction: "column",
|
|
134
|
+
gap: "sm",
|
|
135
|
+
children: [
|
|
136
|
+
Typography.render({
|
|
137
|
+
variant: "body2",
|
|
138
|
+
color: "secondary",
|
|
139
|
+
children: "Result",
|
|
140
|
+
}),
|
|
141
|
+
html`<div class="iw-primitive-preview">
|
|
142
|
+
${
|
|
143
|
+
examplePreview ??
|
|
144
|
+
html`<div class="iw-primitive-preview-empty">
|
|
145
|
+
Preview coming soon.
|
|
146
|
+
</div>`
|
|
147
|
+
}
|
|
148
|
+
</div>`,
|
|
149
|
+
],
|
|
150
|
+
}),
|
|
151
|
+
],
|
|
152
|
+
}),
|
|
153
|
+
],
|
|
156
154
|
}),
|
|
155
|
+
}),
|
|
156
|
+
),
|
|
157
157
|
],
|
|
158
158
|
}),
|
|
159
159
|
})
|
|
@@ -26,7 +26,11 @@ export interface DataGridVirtualization {
|
|
|
26
26
|
export interface Column {
|
|
27
27
|
id: string
|
|
28
28
|
title: string
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* `"url"` cells show web addresses as links. A date column's editor gives
|
|
31
|
+
* back dates in the form the row keeps them: `Date`, timestamp or string.
|
|
32
|
+
*/
|
|
33
|
+
type?: "string" | "number" | "boolean" | "date" | "url"
|
|
30
34
|
isSortable?: boolean
|
|
31
35
|
isFilterable?: boolean
|
|
32
36
|
filter?: {
|
|
@@ -42,6 +46,13 @@ export interface Column {
|
|
|
42
46
|
* - number / CSS length string: fixed width
|
|
43
47
|
*/
|
|
44
48
|
width?: number | string
|
|
49
|
+
/**
|
|
50
|
+
* Lets the user edit the column's cells: a text, number or date field by
|
|
51
|
+
* its `type`, a checkbox for booleans, or a drop-down with `options`.
|
|
52
|
+
*/
|
|
53
|
+
isEditable?: boolean
|
|
54
|
+
/** The values an editable cell can take, picked from a drop-down. */
|
|
55
|
+
options?: SelectOption[]
|
|
45
56
|
formatter?: string | ((value: any) => any)
|
|
46
57
|
sortFn?: (a: Row, b: Row) => number
|
|
47
58
|
filterFn?: (row: Row, filterValue: any) => boolean
|
|
@@ -52,7 +63,7 @@ export interface Column {
|
|
|
52
63
|
export interface DataGridProps<T extends Row = Row> {
|
|
53
64
|
id?: string
|
|
54
65
|
type: string
|
|
55
|
-
|
|
66
|
+
rows: T[]
|
|
56
67
|
columns: Column[]
|
|
57
68
|
sorts: { column: string; direction: "asc" | "desc" }[]
|
|
58
69
|
filters: Record<string, any>
|
|
@@ -65,6 +76,20 @@ export interface DataGridProps<T extends Row = Row> {
|
|
|
65
76
|
pagination?: { page: number; pageSize: number; pageSizes: number[] } | null
|
|
66
77
|
rowId?: string
|
|
67
78
|
isMultiSelect: boolean
|
|
79
|
+
/**
|
|
80
|
+
* The user's unsaved edits, by row key and column id, shown over the rows
|
|
81
|
+
* until saved. Sorting, filtering and search go by the saved values.
|
|
82
|
+
*/
|
|
83
|
+
edits?: Record<string | number, Record<string, any>>
|
|
84
|
+
/** The label of the button that saves the edits. Defaults to "Save changes". */
|
|
85
|
+
saveLabel?: string
|
|
86
|
+
onCellChange?: (payload: {
|
|
87
|
+
rowId: string | number
|
|
88
|
+
columnId: string
|
|
89
|
+
value: any
|
|
90
|
+
}) => void
|
|
91
|
+
onEditsReset?: () => void
|
|
92
|
+
onEditsSave?: () => void
|
|
68
93
|
onSortChange?: (columnId: string) => void
|
|
69
94
|
onFilterChange?: (payload: { columnId: string; value: any }) => void
|
|
70
95
|
onSearchChange?: (value: string) => void
|
|
@@ -122,6 +147,15 @@ export type DataGridType = {
|
|
|
122
147
|
rowsToggleAll(entity: DataGridProps): void
|
|
123
148
|
rowsSelectAll(entity: DataGridProps): void
|
|
124
149
|
selectionClear(entity: DataGridProps): void
|
|
150
|
+
/** Records an edit of a cell; an edit back to the saved value drops it. */
|
|
151
|
+
cellChange(
|
|
152
|
+
entity: DataGridProps,
|
|
153
|
+
payload: { rowId: string | number; columnId: string; value: any },
|
|
154
|
+
): void
|
|
155
|
+
/** Drops every unsaved edit. */
|
|
156
|
+
editsReset(entity: DataGridProps): void
|
|
157
|
+
/** Writes the unsaved edits into the rows. */
|
|
158
|
+
editsSave(entity: DataGridProps): void
|
|
125
159
|
virtualMount(entity: DataGridProps, containerEl: HTMLElement): void
|
|
126
160
|
virtualScroll(entity: DataGridProps, containerEl: HTMLElement): void
|
|
127
161
|
render(entity: DataGridProps, api: Api): TemplateResult
|
|
@@ -141,9 +175,15 @@ export type DataGridType = {
|
|
|
141
175
|
): TemplateResult
|
|
142
176
|
renderCell(
|
|
143
177
|
entity: DataGridProps,
|
|
144
|
-
payload: { cell: any; index: number },
|
|
178
|
+
payload: { cell: any; index: number; row?: Row },
|
|
145
179
|
api: Api,
|
|
146
180
|
): TemplateResult
|
|
181
|
+
renderEditor(
|
|
182
|
+
entity: DataGridProps,
|
|
183
|
+
payload: { value: any; column: Column; row: Row; index: number },
|
|
184
|
+
api: Api,
|
|
185
|
+
): TemplateResult
|
|
186
|
+
renderEdits(entity: DataGridProps, editCount: number): TemplateResult
|
|
147
187
|
renderValue(
|
|
148
188
|
entity: DataGridProps,
|
|
149
189
|
payload: { value: any; column: Column; index: number },
|
|
@@ -181,3 +221,21 @@ export declare function getSortDirection(
|
|
|
181
221
|
): "asc" | "desc" | null
|
|
182
222
|
export declare function isAllSelected(entity: DataGridProps): boolean
|
|
183
223
|
export declare function isSomeSelected(entity: DataGridProps): boolean
|
|
224
|
+
/** A cell's value as shown: its unsaved edit, or the row's own. */
|
|
225
|
+
export declare function getCellValue(
|
|
226
|
+
entity: DataGridProps,
|
|
227
|
+
row: Row,
|
|
228
|
+
columnId: string,
|
|
229
|
+
): any
|
|
230
|
+
/** Whether a cell has an unsaved edit. */
|
|
231
|
+
export declare function isCellEdited(
|
|
232
|
+
entity: DataGridProps,
|
|
233
|
+
row: Row,
|
|
234
|
+
columnId: string,
|
|
235
|
+
): boolean
|
|
236
|
+
/** How many cells have unsaved edits. */
|
|
237
|
+
export declare function getEditCount(entity: DataGridProps): number
|
|
238
|
+
/** Every row with its unsaved edits applied. */
|
|
239
|
+
export declare function getEditedRows<T extends Row = Row>(
|
|
240
|
+
entity: DataGridProps<T>,
|
|
241
|
+
): T[]
|