@inglorious/ui 9.1.1 → 9.3.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/checkbox/checkbox.stories.js +16 -0
- package/src/controls/checkbox/style.css +24 -0
- package/src/controls/checkbox/template.js +15 -2
- package/src/controls/input/template.js +2 -1
- package/src/controls/radio-group/radio-group.stories.js +19 -0
- package/src/controls/radio-group/style.css +30 -0
- package/src/controls/radio-group/template.js +15 -2
- package/src/data-display/chip/chip.stories.js +13 -0
- package/src/data-display/chip/style.css +22 -0
- package/src/data-display/chip/template.js +25 -2
- package/src/data-display/data-grid/base.css +4 -0
- package/src/data-display/data-grid/data-grid.stories.js +39 -0
- package/src/data-display/data-grid/handlers.js +44 -1
- 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 +127 -17
- package/src/data-display/data-grid/theme.css +17 -0
- package/src/examples/dashboard/primitive-section.js +68 -68
- package/types/controls/checkbox.d.ts +4 -0
- package/types/controls/input.d.ts +2 -0
- package/types/controls/radio-group.d.ts +4 -0
- package/types/data-display/chip.d.ts +7 -0
- package/types/data-display/data-grid.d.ts +56 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/ui",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.3.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.
|
|
318
|
+
"@inglorious/charts": "12.2.0",
|
|
319
319
|
"@inglorious/eslint-config": "1.2.0",
|
|
320
320
|
"@inglorious/store": "12.0.0",
|
|
321
321
|
"@inglorious/vite-plugin-jsx": "2.0.5",
|
|
@@ -12,6 +12,12 @@ export default {
|
|
|
12
12
|
description: "A line of detail under the label.",
|
|
13
13
|
},
|
|
14
14
|
name: { control: "text", description: "Native HTML name attribute." },
|
|
15
|
+
variant: {
|
|
16
|
+
control: "select",
|
|
17
|
+
options: ["default", "card"],
|
|
18
|
+
description:
|
|
19
|
+
"`card` draws the checkbox as a card, outlined while checked.",
|
|
20
|
+
},
|
|
15
21
|
isChecked: { control: "boolean", description: "Current checked state." },
|
|
16
22
|
isDisabled: { control: "boolean", description: "Disables interaction." },
|
|
17
23
|
isRequired: { control: "boolean", description: "Marks input as required." },
|
|
@@ -60,3 +66,13 @@ WithDescription.args = {
|
|
|
60
66
|
name: "summary",
|
|
61
67
|
description: "An email every Monday with last week's totals.",
|
|
62
68
|
}
|
|
69
|
+
|
|
70
|
+
export const Card = {}
|
|
71
|
+
Card.args = {
|
|
72
|
+
...Default.args,
|
|
73
|
+
label: "Breakfast",
|
|
74
|
+
name: "breakfast",
|
|
75
|
+
description: "Served from 7 to 10, €12",
|
|
76
|
+
variant: "card",
|
|
77
|
+
isChecked: true,
|
|
78
|
+
}
|
|
@@ -38,6 +38,30 @@
|
|
|
38
38
|
font-size: var(--iw-font-size-lg);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
.iw-checkbox-media {
|
|
42
|
+
display: block;
|
|
43
|
+
margin-block-end: var(--iw-space-2);
|
|
44
|
+
|
|
45
|
+
img {
|
|
46
|
+
display: block;
|
|
47
|
+
max-inline-size: 100%;
|
|
48
|
+
border-radius: var(--iw-radius-sm);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
&.iw-checkbox-card {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: flex-start;
|
|
55
|
+
padding: var(--iw-space-3);
|
|
56
|
+
border: 1px solid var(--iw-color-border);
|
|
57
|
+
border-radius: var(--iw-radius-md);
|
|
58
|
+
|
|
59
|
+
&:has(:checked) {
|
|
60
|
+
border-color: var(--iw-checkbox-accent);
|
|
61
|
+
background: color-mix(in srgb, var(--iw-checkbox-accent) 8%, transparent);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
41
65
|
&.iw-checkbox-disabled {
|
|
42
66
|
opacity: 0.6;
|
|
43
67
|
cursor: not-allowed;
|
|
@@ -23,6 +23,8 @@ export function render(props) {
|
|
|
23
23
|
name = "",
|
|
24
24
|
label = "",
|
|
25
25
|
description,
|
|
26
|
+
media,
|
|
27
|
+
variant = "default",
|
|
26
28
|
isChecked = false,
|
|
27
29
|
isDisabled = false,
|
|
28
30
|
isRequired = false,
|
|
@@ -39,6 +41,7 @@ export function render(props) {
|
|
|
39
41
|
const classes = {
|
|
40
42
|
"iw-checkbox": true,
|
|
41
43
|
"iw-checkbox-disabled": isDisabled,
|
|
44
|
+
"iw-checkbox-card": variant === "card",
|
|
42
45
|
[`iw-checkbox-${color}`]: color !== "primary",
|
|
43
46
|
[`iw-checkbox-${size}`]: size !== "md",
|
|
44
47
|
}
|
|
@@ -57,11 +60,21 @@ export function render(props) {
|
|
|
57
60
|
${ref((element) => applyElementProps(element, rest))}
|
|
58
61
|
/>
|
|
59
62
|
${when(
|
|
60
|
-
description,
|
|
63
|
+
description || media,
|
|
61
64
|
() =>
|
|
62
65
|
html`<span class="iw-checkbox-text">
|
|
66
|
+
${when(
|
|
67
|
+
media,
|
|
68
|
+
() => html`<span class="iw-checkbox-media">${media}</span>`,
|
|
69
|
+
)}
|
|
63
70
|
<span class="iw-checkbox-label">${label}</span>
|
|
64
|
-
|
|
71
|
+
${when(
|
|
72
|
+
description,
|
|
73
|
+
() =>
|
|
74
|
+
html`<span class="iw-checkbox-description"
|
|
75
|
+
>${description}</span
|
|
76
|
+
>`,
|
|
77
|
+
)}
|
|
65
78
|
</span>`,
|
|
66
79
|
() => html`<span class="iw-checkbox-label">${label}</span>`,
|
|
67
80
|
)}
|
|
@@ -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}
|
|
@@ -14,6 +14,11 @@ export default {
|
|
|
14
14
|
options: ["column", "row"],
|
|
15
15
|
description: "Layout direction for options.",
|
|
16
16
|
},
|
|
17
|
+
variant: {
|
|
18
|
+
control: "select",
|
|
19
|
+
options: ["default", "card"],
|
|
20
|
+
description: "`card` draws each option as a card in a grid.",
|
|
21
|
+
},
|
|
17
22
|
color: {
|
|
18
23
|
control: "select",
|
|
19
24
|
options: ["primary", "secondary", "success", "warning", "error", "info"],
|
|
@@ -72,3 +77,17 @@ WithDescriptions.args = {
|
|
|
72
77
|
{ label: "Express", value: "express", description: "Next day, €9" },
|
|
73
78
|
],
|
|
74
79
|
}
|
|
80
|
+
|
|
81
|
+
export const Cards = {}
|
|
82
|
+
Cards.args = {
|
|
83
|
+
...Default.args,
|
|
84
|
+
label: "Room",
|
|
85
|
+
name: "room",
|
|
86
|
+
value: "double",
|
|
87
|
+
variant: "card",
|
|
88
|
+
options: [
|
|
89
|
+
{ label: "Single", value: "single", description: "One bed, €80" },
|
|
90
|
+
{ label: "Double", value: "double", description: "Two beds, €120" },
|
|
91
|
+
{ label: "Suite", value: "suite", description: "With a view, €200" },
|
|
92
|
+
],
|
|
93
|
+
}
|
|
@@ -70,4 +70,34 @@
|
|
|
70
70
|
&.iw-radio-group-info {
|
|
71
71
|
--iw-radio-accent: var(--iw-color-info);
|
|
72
72
|
}
|
|
73
|
+
|
|
74
|
+
/* After the directions, which it overrides with a grid. */
|
|
75
|
+
&.iw-radio-group-card {
|
|
76
|
+
display: grid;
|
|
77
|
+
grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr));
|
|
78
|
+
gap: var(--iw-space-3);
|
|
79
|
+
|
|
80
|
+
.iw-radio-option {
|
|
81
|
+
align-items: flex-start;
|
|
82
|
+
padding: var(--iw-space-3);
|
|
83
|
+
border: 1px solid var(--iw-color-border);
|
|
84
|
+
border-radius: var(--iw-radius-md);
|
|
85
|
+
|
|
86
|
+
&:has(:checked) {
|
|
87
|
+
border-color: var(--iw-radio-accent);
|
|
88
|
+
background: color-mix(in srgb, var(--iw-radio-accent) 8%, transparent);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.iw-radio-media {
|
|
94
|
+
display: block;
|
|
95
|
+
margin-block-end: var(--iw-space-2);
|
|
96
|
+
|
|
97
|
+
img {
|
|
98
|
+
display: block;
|
|
99
|
+
max-inline-size: 100%;
|
|
100
|
+
border-radius: var(--iw-radius-sm);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
73
103
|
}
|
|
@@ -25,6 +25,7 @@ export function render(props) {
|
|
|
25
25
|
value,
|
|
26
26
|
options = [],
|
|
27
27
|
direction = "column",
|
|
28
|
+
variant = "default",
|
|
28
29
|
color = "primary",
|
|
29
30
|
isDisabled = false,
|
|
30
31
|
onChange,
|
|
@@ -38,6 +39,7 @@ export function render(props) {
|
|
|
38
39
|
const rootClasses = {
|
|
39
40
|
"iw-radio-group": true,
|
|
40
41
|
[`iw-radio-group-${direction}`]: true,
|
|
42
|
+
"iw-radio-group-card": variant === "card",
|
|
41
43
|
[`iw-radio-group-${color}`]: color !== "primary",
|
|
42
44
|
"iw-radio-group-disabled": isDisabled,
|
|
43
45
|
}
|
|
@@ -56,6 +58,7 @@ export function render(props) {
|
|
|
56
58
|
const {
|
|
57
59
|
label: optionLabel,
|
|
58
60
|
description: optionDescription,
|
|
61
|
+
media,
|
|
59
62
|
value: optionValue,
|
|
60
63
|
isDisabled: optionDisabled = false,
|
|
61
64
|
...optionRest
|
|
@@ -75,11 +78,21 @@ export function render(props) {
|
|
|
75
78
|
${ref((element) => applyElementProps(element, optionRest))}
|
|
76
79
|
/>
|
|
77
80
|
${when(
|
|
78
|
-
optionDescription,
|
|
81
|
+
optionDescription || media,
|
|
79
82
|
() =>
|
|
80
83
|
html`<span class="iw-radio-text">
|
|
84
|
+
${when(
|
|
85
|
+
media,
|
|
86
|
+
() => html`<span class="iw-radio-media">${media}</span>`,
|
|
87
|
+
)}
|
|
81
88
|
<span>${optionLabel}</span>
|
|
82
|
-
|
|
89
|
+
${when(
|
|
90
|
+
optionDescription,
|
|
91
|
+
() =>
|
|
92
|
+
html`<span class="iw-radio-description"
|
|
93
|
+
>${optionDescription}</span
|
|
94
|
+
>`,
|
|
95
|
+
)}
|
|
83
96
|
</span>`,
|
|
84
97
|
() => html`<span>${optionLabel}</span>`,
|
|
85
98
|
)}
|
|
@@ -10,6 +10,10 @@ export default {
|
|
|
10
10
|
render: createRender(Chip),
|
|
11
11
|
argTypes: {
|
|
12
12
|
children: { control: "text", description: "Chip label content." },
|
|
13
|
+
isSelected: {
|
|
14
|
+
control: "boolean",
|
|
15
|
+
description: "Makes the chip a toggle button, pressed while true.",
|
|
16
|
+
},
|
|
13
17
|
icon: {
|
|
14
18
|
control: false,
|
|
15
19
|
description: "Shown before the label, such as an `Icon.render(...)`.",
|
|
@@ -72,3 +76,12 @@ export const WithIcon = {
|
|
|
72
76
|
color: "success",
|
|
73
77
|
},
|
|
74
78
|
}
|
|
79
|
+
|
|
80
|
+
export const Selectable = {
|
|
81
|
+
args: {
|
|
82
|
+
...Default.args,
|
|
83
|
+
children: "Vegan",
|
|
84
|
+
isRemovable: false,
|
|
85
|
+
isSelected: true,
|
|
86
|
+
},
|
|
87
|
+
}
|
|
@@ -64,6 +64,28 @@
|
|
|
64
64
|
border-radius: var(--iw-radius-none);
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
&.iw-chip-selectable {
|
|
68
|
+
border: 1px solid var(--iw-color-border);
|
|
69
|
+
font: inherit;
|
|
70
|
+
font-size: var(--iw-font-size-sm);
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
|
|
73
|
+
&:focus-visible {
|
|
74
|
+
outline: 2px solid var(--iw-color-primary);
|
|
75
|
+
outline-offset: 2px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
&:disabled {
|
|
79
|
+
opacity: 0.6;
|
|
80
|
+
cursor: not-allowed;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
&.iw-chip-selected {
|
|
85
|
+
border-color: var(--iw-color-primary);
|
|
86
|
+
background: color-mix(in srgb, var(--iw-color-primary) 20%, transparent);
|
|
87
|
+
}
|
|
88
|
+
|
|
67
89
|
&.iw-chip-primary {
|
|
68
90
|
background: color-mix(in srgb, var(--iw-color-primary) 20%, transparent);
|
|
69
91
|
}
|
|
@@ -21,6 +21,8 @@ export function render(props) {
|
|
|
21
21
|
color = "default",
|
|
22
22
|
size = "md",
|
|
23
23
|
shape = "pill",
|
|
24
|
+
isSelected,
|
|
25
|
+
isDisabled = false,
|
|
24
26
|
onClick,
|
|
25
27
|
} = props
|
|
26
28
|
|
|
@@ -31,9 +33,30 @@ export function render(props) {
|
|
|
31
33
|
[`iw-chip-shape-${shape}`]: shape !== "pill",
|
|
32
34
|
}
|
|
33
35
|
|
|
36
|
+
const content = html`${when(
|
|
37
|
+
icon,
|
|
38
|
+
() => html`<span class="iw-chip-icon" aria-hidden="true">${icon}</span>`,
|
|
39
|
+
)} <span class="iw-chip-label">${children}</span>`
|
|
40
|
+
|
|
41
|
+
// A chip that can be picked is a toggle button, pressed while selected.
|
|
42
|
+
if (isSelected !== undefined) {
|
|
43
|
+
return html`<button
|
|
44
|
+
type="button"
|
|
45
|
+
class=${classMap({
|
|
46
|
+
...classes,
|
|
47
|
+
"iw-chip-selectable": true,
|
|
48
|
+
"iw-chip-selected": isSelected,
|
|
49
|
+
})}
|
|
50
|
+
aria-pressed=${String(isSelected)}
|
|
51
|
+
?disabled=${isDisabled}
|
|
52
|
+
@click=${onClick}
|
|
53
|
+
>
|
|
54
|
+
${content}
|
|
55
|
+
</button>`
|
|
56
|
+
}
|
|
57
|
+
|
|
34
58
|
return html`<span class=${classMap(classes)}>
|
|
35
|
-
${
|
|
36
|
-
<span class="iw-chip-label">${children}</span>
|
|
59
|
+
${content}
|
|
37
60
|
${when(
|
|
38
61
|
isRemovable,
|
|
39
62
|
() =>
|
|
@@ -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,
|
|
@@ -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 (Object.is(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
|
|
|
@@ -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,7 @@ 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
|
|
30
36
|
|
|
31
37
|
export const DataGrid = {
|
|
32
38
|
/**
|
|
@@ -67,7 +73,7 @@ export const DataGrid = {
|
|
|
67
73
|
*/
|
|
68
74
|
renderHeader(props) {
|
|
69
75
|
return html`<div class="iw-data-grid-header">
|
|
70
|
-
${props.search
|
|
76
|
+
${when(props.search, () => this.renderToolbar?.(props))}
|
|
71
77
|
<div class="iw-data-grid-header-row">
|
|
72
78
|
${repeat(
|
|
73
79
|
props.columns,
|
|
@@ -108,7 +114,10 @@ export const DataGrid = {
|
|
|
108
114
|
@click=${() => column.isSortable && props.onSortChange?.(column.id)}
|
|
109
115
|
class="iw-data-grid-header-title"
|
|
110
116
|
>
|
|
111
|
-
${column.title}
|
|
117
|
+
${column.title}
|
|
118
|
+
${when(column.isSortable, () =>
|
|
119
|
+
getSortIcon(getSortDirection(props, column.id)),
|
|
120
|
+
)}
|
|
112
121
|
</div>
|
|
113
122
|
|
|
114
123
|
${when(column.isFilterable, () => Filters.render(props, column))}
|
|
@@ -225,7 +234,11 @@ export const DataGrid = {
|
|
|
225
234
|
props.columns,
|
|
226
235
|
(column) => column.id,
|
|
227
236
|
(column, index) =>
|
|
228
|
-
this.renderCell?.(props, {
|
|
237
|
+
this.renderCell?.(props, {
|
|
238
|
+
cell: getCellValue(props, row, column.id),
|
|
239
|
+
index,
|
|
240
|
+
row,
|
|
241
|
+
}),
|
|
229
242
|
)}
|
|
230
243
|
</div>`
|
|
231
244
|
},
|
|
@@ -235,25 +248,87 @@ export const DataGrid = {
|
|
|
235
248
|
* Override to customize per-column cell structure.
|
|
236
249
|
* @param {DataGridProps} props The data grid props.
|
|
237
250
|
* @param {object} payload The payload.
|
|
238
|
-
* @param {any} payload.cell The cell data.
|
|
251
|
+
* @param {any} payload.cell The cell data, with any unsaved edit.
|
|
239
252
|
* @param {number} payload.index The column index.
|
|
253
|
+
* @param {Row} payload.row The row the cell is in.
|
|
240
254
|
* @returns {TemplateResult} The rendered cell.
|
|
241
255
|
*/
|
|
242
|
-
renderCell(props, { cell, index }) {
|
|
256
|
+
renderCell(props, { cell, index, row }) {
|
|
243
257
|
const column = props.columns[index]
|
|
258
|
+
const isEditable = Boolean(column.isEditable && row)
|
|
244
259
|
|
|
260
|
+
// Typing in a cell's editor is not picking its row.
|
|
245
261
|
return html`<div
|
|
246
262
|
class="iw-data-grid-cell ${classMap({
|
|
247
263
|
"iw-data-grid-cell-number": column.type === "number",
|
|
248
264
|
"iw-data-grid-cell-date": column.type === "date",
|
|
249
265
|
"iw-data-grid-cell-boolean": column.type === "boolean",
|
|
266
|
+
"iw-data-grid-cell-editable": isEditable,
|
|
267
|
+
"iw-data-grid-cell-edited":
|
|
268
|
+
isEditable && isCellEdited(props, row, column.id),
|
|
250
269
|
})}"
|
|
251
270
|
style=${getColumnStyle(column)}
|
|
271
|
+
@click=${isEditable ? (event) => event.stopPropagation() : undefined}
|
|
252
272
|
>
|
|
253
|
-
${
|
|
273
|
+
${when(
|
|
274
|
+
isEditable,
|
|
275
|
+
() => this.renderEditor?.(props, { value: cell, column, row, index }),
|
|
276
|
+
() => this.renderValue?.(props, { value: cell, column, index }),
|
|
277
|
+
)}
|
|
254
278
|
</div>`
|
|
255
279
|
},
|
|
256
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Renders the field that edits a cell of an `isEditable` column: a
|
|
283
|
+
* drop-down when the column has `options`, a checkbox for booleans, a date
|
|
284
|
+
* or number field for those types, and a text field otherwise.
|
|
285
|
+
* Override to edit some columns differently.
|
|
286
|
+
* @param {DataGridProps} props The data grid props.
|
|
287
|
+
* @param {object} payload The payload.
|
|
288
|
+
* @param {any} payload.value The cell's value, with any unsaved edit.
|
|
289
|
+
* @param {Column} payload.column The column definition.
|
|
290
|
+
* @param {Row} payload.row The row the cell is in.
|
|
291
|
+
* @returns {TemplateResult} The rendered editor.
|
|
292
|
+
*/
|
|
293
|
+
renderEditor(props, { value, column, row }) {
|
|
294
|
+
const change = (next) =>
|
|
295
|
+
props.onCellChange?.({
|
|
296
|
+
rowId: getRowKeyValue(props, row),
|
|
297
|
+
columnId: column.id,
|
|
298
|
+
value: next,
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
if (column.options) {
|
|
302
|
+
return Select.render({
|
|
303
|
+
size: "sm",
|
|
304
|
+
ariaLabel: column.title,
|
|
305
|
+
value: value ?? "",
|
|
306
|
+
options: column.options,
|
|
307
|
+
isFullWidth: true,
|
|
308
|
+
onChange: change,
|
|
309
|
+
})
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (column.type === "boolean") {
|
|
313
|
+
return Checkbox.render({
|
|
314
|
+
"aria-label": column.title,
|
|
315
|
+
isChecked: Boolean(value),
|
|
316
|
+
onChange: change,
|
|
317
|
+
})
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const isNumber = column.type === "number"
|
|
321
|
+
return Input.render({
|
|
322
|
+
size: "sm",
|
|
323
|
+
ariaLabel: column.title,
|
|
324
|
+
inputType: isNumber ? "number" : column.type === "date" ? "date" : "text",
|
|
325
|
+
value: value == null ? "" : String(value),
|
|
326
|
+
isFullWidth: true,
|
|
327
|
+
onChange: (text) =>
|
|
328
|
+
change(isNumber ? (text === "" ? null : Number(text)) : text),
|
|
329
|
+
})
|
|
330
|
+
},
|
|
331
|
+
|
|
257
332
|
/**
|
|
258
333
|
* Renders the raw cell value for a column.
|
|
259
334
|
* Override to format values or inject custom content.
|
|
@@ -274,22 +349,57 @@ export const DataGrid = {
|
|
|
274
349
|
*/
|
|
275
350
|
renderFooter(props) {
|
|
276
351
|
const pagination = getPaginationInfo(props)
|
|
352
|
+
const editCount = getEditCount(props)
|
|
277
353
|
|
|
278
|
-
if (!pagination) {
|
|
354
|
+
if (!pagination && !editCount) {
|
|
279
355
|
return null
|
|
280
356
|
}
|
|
281
357
|
|
|
282
358
|
return html`<div class="iw-data-grid-footer">
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
359
|
+
${when(editCount, () => this.renderEdits?.(props, editCount))}
|
|
360
|
+
${when(
|
|
361
|
+
pagination,
|
|
362
|
+
() =>
|
|
363
|
+
html`<div class="iw-data-grid-footer-row">
|
|
364
|
+
<div>
|
|
365
|
+
${pagination.start + PRETTY_PAGE} to ${pagination.end} of
|
|
366
|
+
${pagination.totalRows} entries
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
${this.renderPagination?.(props, pagination)}
|
|
370
|
+
${when(pagination.pageSizes, () =>
|
|
371
|
+
this.renderPageSize?.(props, pagination),
|
|
372
|
+
)}
|
|
373
|
+
</div>`,
|
|
374
|
+
)}
|
|
375
|
+
</div>`
|
|
376
|
+
},
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Renders the bar that counts the unsaved edits, with buttons to reset or
|
|
380
|
+
* save them. Shown only while there are edits.
|
|
381
|
+
* @param {DataGridProps} props The data grid props.
|
|
382
|
+
* @param {number} editCount How many cells have unsaved edits.
|
|
383
|
+
* @returns {TemplateResult} The rendered bar.
|
|
384
|
+
*/
|
|
385
|
+
renderEdits(props, editCount) {
|
|
386
|
+
return html`<div class="iw-data-grid-footer-row iw-data-grid-edits">
|
|
387
|
+
<div role="status">
|
|
388
|
+
${editCount} ${editCount === ONE_EDIT ? "change" : "changes"}
|
|
389
|
+
</div>
|
|
390
|
+
<div class="iw-data-grid-edits-actions">
|
|
391
|
+
${Button.render({
|
|
392
|
+
children: "Reset",
|
|
393
|
+
variant: "ghost",
|
|
394
|
+
size: "sm",
|
|
395
|
+
onClick: () => props.onEditsReset?.(),
|
|
396
|
+
})}
|
|
397
|
+
${Button.render({
|
|
398
|
+
children: props.saveLabel ?? "Save changes",
|
|
399
|
+
color: "primary",
|
|
400
|
+
size: "sm",
|
|
401
|
+
onClick: () => props.onEditsSave?.(),
|
|
402
|
+
})}
|
|
293
403
|
</div>
|
|
294
404
|
</div>`
|
|
295
405
|
},
|
|
@@ -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
|
})
|
|
@@ -6,6 +6,10 @@ export interface CheckboxProps {
|
|
|
6
6
|
label?: string
|
|
7
7
|
/** A line of detail under the label. */
|
|
8
8
|
description?: string
|
|
9
|
+
/** Shown above the label, such as an icon or an image. */
|
|
10
|
+
media?: TemplateResult
|
|
11
|
+
/** `"card"` draws the checkbox as a card, outlined while checked. */
|
|
12
|
+
variant?: "default" | "card"
|
|
9
13
|
isChecked?: boolean
|
|
10
14
|
isDisabled?: boolean
|
|
11
15
|
isRequired?: boolean
|
|
@@ -4,6 +4,8 @@ export interface RadioOption {
|
|
|
4
4
|
label: string
|
|
5
5
|
/** A line of detail under the label. */
|
|
6
6
|
description?: string
|
|
7
|
+
/** Shown above the label, such as an icon or an image. */
|
|
8
|
+
media?: TemplateResult
|
|
7
9
|
value: string
|
|
8
10
|
isDisabled?: boolean
|
|
9
11
|
[key: string]: unknown
|
|
@@ -16,6 +18,8 @@ export interface RadioGroupProps {
|
|
|
16
18
|
value?: string
|
|
17
19
|
options?: RadioOption[]
|
|
18
20
|
direction?: "column" | "row"
|
|
21
|
+
/** `"card"` draws each option as a card in a grid, outlined while checked. */
|
|
22
|
+
variant?: "default" | "card"
|
|
19
23
|
color?: "primary" | "secondary" | "success" | "warning" | "error" | "info"
|
|
20
24
|
isDisabled?: boolean
|
|
21
25
|
onChange?: (value: string) => void
|
|
@@ -16,6 +16,13 @@ export interface ChipProps {
|
|
|
16
16
|
| "info"
|
|
17
17
|
size?: "sm" | "md" | "lg"
|
|
18
18
|
shape?: "pill" | "rounded" | "square"
|
|
19
|
+
/**
|
|
20
|
+
* Makes the chip a toggle button, pressed while true, for picking options:
|
|
21
|
+
* `onClick` toggles it. Left out, the chip is a plain label.
|
|
22
|
+
*/
|
|
23
|
+
isSelected?: boolean
|
|
24
|
+
/** Only for a chip with `isSelected`. */
|
|
25
|
+
isDisabled?: boolean
|
|
19
26
|
onClick?: () => void
|
|
20
27
|
[key: string]: unknown
|
|
21
28
|
}
|
|
@@ -42,6 +42,13 @@ export interface Column {
|
|
|
42
42
|
* - number / CSS length string: fixed width
|
|
43
43
|
*/
|
|
44
44
|
width?: number | string
|
|
45
|
+
/**
|
|
46
|
+
* Lets the user edit the column's cells: a text, number or date field by
|
|
47
|
+
* its `type`, a checkbox for booleans, or a drop-down with `options`.
|
|
48
|
+
*/
|
|
49
|
+
isEditable?: boolean
|
|
50
|
+
/** The values an editable cell can take, picked from a drop-down. */
|
|
51
|
+
options?: SelectOption[]
|
|
45
52
|
formatter?: string | ((value: any) => any)
|
|
46
53
|
sortFn?: (a: Row, b: Row) => number
|
|
47
54
|
filterFn?: (row: Row, filterValue: any) => boolean
|
|
@@ -52,7 +59,7 @@ export interface Column {
|
|
|
52
59
|
export interface DataGridProps<T extends Row = Row> {
|
|
53
60
|
id?: string
|
|
54
61
|
type: string
|
|
55
|
-
|
|
62
|
+
rows: T[]
|
|
56
63
|
columns: Column[]
|
|
57
64
|
sorts: { column: string; direction: "asc" | "desc" }[]
|
|
58
65
|
filters: Record<string, any>
|
|
@@ -65,6 +72,20 @@ export interface DataGridProps<T extends Row = Row> {
|
|
|
65
72
|
pagination?: { page: number; pageSize: number; pageSizes: number[] } | null
|
|
66
73
|
rowId?: string
|
|
67
74
|
isMultiSelect: boolean
|
|
75
|
+
/**
|
|
76
|
+
* The user's unsaved edits, by row key and column id, shown over the rows
|
|
77
|
+
* until saved. Sorting, filtering and search go by the saved values.
|
|
78
|
+
*/
|
|
79
|
+
edits?: Record<string | number, Record<string, any>>
|
|
80
|
+
/** The label of the button that saves the edits. Defaults to "Save changes". */
|
|
81
|
+
saveLabel?: string
|
|
82
|
+
onCellChange?: (payload: {
|
|
83
|
+
rowId: string | number
|
|
84
|
+
columnId: string
|
|
85
|
+
value: any
|
|
86
|
+
}) => void
|
|
87
|
+
onEditsReset?: () => void
|
|
88
|
+
onEditsSave?: () => void
|
|
68
89
|
onSortChange?: (columnId: string) => void
|
|
69
90
|
onFilterChange?: (payload: { columnId: string; value: any }) => void
|
|
70
91
|
onSearchChange?: (value: string) => void
|
|
@@ -122,6 +143,15 @@ export type DataGridType = {
|
|
|
122
143
|
rowsToggleAll(entity: DataGridProps): void
|
|
123
144
|
rowsSelectAll(entity: DataGridProps): void
|
|
124
145
|
selectionClear(entity: DataGridProps): void
|
|
146
|
+
/** Records an edit of a cell; an edit back to the saved value drops it. */
|
|
147
|
+
cellChange(
|
|
148
|
+
entity: DataGridProps,
|
|
149
|
+
payload: { rowId: string | number; columnId: string; value: any },
|
|
150
|
+
): void
|
|
151
|
+
/** Drops every unsaved edit. */
|
|
152
|
+
editsReset(entity: DataGridProps): void
|
|
153
|
+
/** Writes the unsaved edits into the rows. */
|
|
154
|
+
editsSave(entity: DataGridProps): void
|
|
125
155
|
virtualMount(entity: DataGridProps, containerEl: HTMLElement): void
|
|
126
156
|
virtualScroll(entity: DataGridProps, containerEl: HTMLElement): void
|
|
127
157
|
render(entity: DataGridProps, api: Api): TemplateResult
|
|
@@ -141,9 +171,15 @@ export type DataGridType = {
|
|
|
141
171
|
): TemplateResult
|
|
142
172
|
renderCell(
|
|
143
173
|
entity: DataGridProps,
|
|
144
|
-
payload: { cell: any; index: number },
|
|
174
|
+
payload: { cell: any; index: number; row?: Row },
|
|
145
175
|
api: Api,
|
|
146
176
|
): TemplateResult
|
|
177
|
+
renderEditor(
|
|
178
|
+
entity: DataGridProps,
|
|
179
|
+
payload: { value: any; column: Column; row: Row; index: number },
|
|
180
|
+
api: Api,
|
|
181
|
+
): TemplateResult
|
|
182
|
+
renderEdits(entity: DataGridProps, editCount: number): TemplateResult
|
|
147
183
|
renderValue(
|
|
148
184
|
entity: DataGridProps,
|
|
149
185
|
payload: { value: any; column: Column; index: number },
|
|
@@ -181,3 +217,21 @@ export declare function getSortDirection(
|
|
|
181
217
|
): "asc" | "desc" | null
|
|
182
218
|
export declare function isAllSelected(entity: DataGridProps): boolean
|
|
183
219
|
export declare function isSomeSelected(entity: DataGridProps): boolean
|
|
220
|
+
/** A cell's value as shown: its unsaved edit, or the row's own. */
|
|
221
|
+
export declare function getCellValue(
|
|
222
|
+
entity: DataGridProps,
|
|
223
|
+
row: Row,
|
|
224
|
+
columnId: string,
|
|
225
|
+
): any
|
|
226
|
+
/** Whether a cell has an unsaved edit. */
|
|
227
|
+
export declare function isCellEdited(
|
|
228
|
+
entity: DataGridProps,
|
|
229
|
+
row: Row,
|
|
230
|
+
columnId: string,
|
|
231
|
+
): boolean
|
|
232
|
+
/** How many cells have unsaved edits. */
|
|
233
|
+
export declare function getEditCount(entity: DataGridProps): number
|
|
234
|
+
/** Every row with its unsaved edits applied. */
|
|
235
|
+
export declare function getEditedRows<T extends Row = Row>(
|
|
236
|
+
entity: DataGridProps<T>,
|
|
237
|
+
): T[]
|