@inglorious/web 2.0.0 → 2.1.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/README.md +71 -11
- package/package.json +5 -3
- package/src/index.js +3 -0
- package/src/list.js +40 -32
- package/src/mount.js +2 -3
- package/src/table/base.css +34 -0
- package/src/table/filters/date.js +42 -0
- package/src/table/filters/number.js +22 -0
- package/src/table/filters/range.js +42 -0
- package/src/table/filters/select.js +34 -0
- package/src/table/filters/text.js +22 -0
- package/src/table/filters.js +24 -0
- package/src/table/logic.js +401 -0
- package/src/table/rendering.js +239 -0
- package/src/table/theme.css +83 -0
- package/src/table.js +7 -0
package/README.md
CHANGED
|
@@ -164,6 +164,77 @@ mount(store, renderApp, document.getElementById("root"))
|
|
|
164
164
|
|
|
165
165
|
The router automatically intercepts clicks on local `<a>` tags and handles browser back/forward events, keeping your UI in sync with the URL.
|
|
166
166
|
|
|
167
|
+
### 3. Programmatic Navigation
|
|
168
|
+
|
|
169
|
+
To navigate from your JavaScript code, dispatch a `navigate` event.
|
|
170
|
+
|
|
171
|
+
```javascript
|
|
172
|
+
api.notify("navigate", "/users/456")
|
|
173
|
+
|
|
174
|
+
// Or navigate back in history
|
|
175
|
+
api.notify("navigate", -1)
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Table
|
|
181
|
+
|
|
182
|
+
`@inglorious/web` includes a `table` type for displaying data in a tabular format. It's designed to be flexible and customizable.
|
|
183
|
+
|
|
184
|
+
### 1. Add the `table` type
|
|
185
|
+
|
|
186
|
+
To use it, import the `table` type and its CSS, then create an entity for your table. You must define the `data` to be displayed and can optionally provide `columns` definitions.
|
|
187
|
+
|
|
188
|
+
```javascript
|
|
189
|
+
// In your entity definition file
|
|
190
|
+
import { table } from "@inglorious/web"
|
|
191
|
+
|
|
192
|
+
// Import base styles and a theme. You can create your own theme.
|
|
193
|
+
import "@inglorious/web/table/base.css"
|
|
194
|
+
import "@inglorious/web/table/theme.css"
|
|
195
|
+
|
|
196
|
+
export default {
|
|
197
|
+
...table,
|
|
198
|
+
data: [
|
|
199
|
+
{ id: 1, name: "Product A", price: 100 },
|
|
200
|
+
{ id: 2, name: "Product B", price: 150 },
|
|
201
|
+
],
|
|
202
|
+
columns: [
|
|
203
|
+
{ id: "id", label: "ID" },
|
|
204
|
+
{ id: "name", label: "Product Name" },
|
|
205
|
+
{ id: "price", label: "Price" },
|
|
206
|
+
],
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### 2. Custom Rendering
|
|
211
|
+
|
|
212
|
+
You can customize how data is rendered in the table cells by overriding the `renderValue` method. This is useful for formatting values or displaying custom content.
|
|
213
|
+
|
|
214
|
+
The example below from `examples/apps/web-table/src/product-table/product-table.js` shows how to format values based on a `formatter` property in the column definition.
|
|
215
|
+
|
|
216
|
+
```javascript
|
|
217
|
+
import { table } from "@inglorious/web"
|
|
218
|
+
import { format } from "date-fns"
|
|
219
|
+
|
|
220
|
+
const formatters = {
|
|
221
|
+
isAvailable: (val) => (val ? "✔️" : "❌"),
|
|
222
|
+
createdAt: (val) => format(val, "dd/MM/yyyy HH:mm"),
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const productTable = {
|
|
226
|
+
...table,
|
|
227
|
+
|
|
228
|
+
renderValue(value, column) {
|
|
229
|
+
return formatters[column.formatter]?.(value) ?? value
|
|
230
|
+
},
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 3. Theming
|
|
235
|
+
|
|
236
|
+
The table comes with a base stylesheet (`@inglorious/web/table/base.css`) and a default theme (`@inglorious/web/table/theme.css`). You can create your own theme by creating a new CSS file and styling the table elements to match your application's design.
|
|
237
|
+
|
|
167
238
|
---
|
|
168
239
|
|
|
169
240
|
## Forms
|
|
@@ -315,17 +386,6 @@ const store = createStore({ types, entities })
|
|
|
315
386
|
|
|
316
387
|
See `src/list.js` in the package for the implementation details and the `examples/apps/web-list` demo for a complete working example. In the demo the `productList` type extends the `list` type and provides `renderItem(item, index)` to render each visible item — see `examples/apps/web-list/src/product-list/product-list.js`.
|
|
317
388
|
|
|
318
|
-
### 3. Programmatic Navigation
|
|
319
|
-
|
|
320
|
-
To navigate from your JavaScript code, dispatch a `navigate` event.
|
|
321
|
-
|
|
322
|
-
```javascript
|
|
323
|
-
api.notify("navigate", "/users/456")
|
|
324
|
-
|
|
325
|
-
// Or navigate back in history
|
|
326
|
-
api.notify("navigate", -1)
|
|
327
|
-
```
|
|
328
|
-
|
|
329
389
|
## API Reference
|
|
330
390
|
|
|
331
391
|
**`mount(store, renderFn, element)`**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inglorious/web",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "A new web framework that leverages the power of the Inglorious Store combined with the performance and simplicity of lit-html.",
|
|
5
5
|
"author": "IceOnFire <antony.mistretta@gmail.com> (https://ingloriouscoderz.it)",
|
|
6
6
|
"license": "MIT",
|
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
".": {
|
|
26
26
|
"types": "./types/index.d.ts",
|
|
27
27
|
"import": "./src/index.js"
|
|
28
|
-
}
|
|
28
|
+
},
|
|
29
|
+
"./table/base.css": "./src/table/base.css",
|
|
30
|
+
"./table/theme.css": "./src/table/theme.css"
|
|
29
31
|
},
|
|
30
32
|
"files": [
|
|
31
33
|
"src",
|
|
@@ -36,7 +38,7 @@
|
|
|
36
38
|
},
|
|
37
39
|
"dependencies": {
|
|
38
40
|
"lit-html": "^3.3.1",
|
|
39
|
-
"@inglorious/store": "7.
|
|
41
|
+
"@inglorious/store": "7.1.0",
|
|
40
42
|
"@inglorious/utils": "3.7.0"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|
package/src/index.js
CHANGED
|
@@ -2,9 +2,12 @@ export { form, getFieldError, getFieldValue, isFieldTouched } from "./form.js"
|
|
|
2
2
|
export { list } from "./list.js"
|
|
3
3
|
export { mount } from "./mount.js"
|
|
4
4
|
export { router } from "./router.js"
|
|
5
|
+
export { table } from "./table.js"
|
|
5
6
|
export { createStore } from "@inglorious/store"
|
|
6
7
|
export { createDevtools } from "@inglorious/store/client/devtools.js"
|
|
7
8
|
export { createSelector } from "@inglorious/store/select.js"
|
|
8
9
|
export { html, render, svg } from "lit-html"
|
|
10
|
+
export { choose } from "lit-html/directives/choose.js"
|
|
9
11
|
export { classMap } from "lit-html/directives/class-map.js"
|
|
12
|
+
export { ref } from "lit-html/directives/ref.js"
|
|
10
13
|
export { repeat } from "lit-html/directives/repeat.js"
|
package/src/list.js
CHANGED
|
@@ -2,6 +2,7 @@ import { html } from "lit-html"
|
|
|
2
2
|
import { ref } from "lit-html/directives/ref.js"
|
|
3
3
|
|
|
4
4
|
const LIST_START = 0
|
|
5
|
+
const PRETTY_INDEX = 1
|
|
5
6
|
|
|
6
7
|
export const list = {
|
|
7
8
|
init(entity) {
|
|
@@ -12,11 +13,45 @@ export const list = {
|
|
|
12
13
|
resetList(entity)
|
|
13
14
|
},
|
|
14
15
|
|
|
16
|
+
scroll(entity, containerEl) {
|
|
17
|
+
const scrollTop = containerEl.scrollTop
|
|
18
|
+
const { items, bufferSize, itemHeight, estimatedHeight, viewportHeight } =
|
|
19
|
+
entity
|
|
20
|
+
const height = itemHeight || estimatedHeight
|
|
21
|
+
|
|
22
|
+
const start = Math.max(
|
|
23
|
+
LIST_START,
|
|
24
|
+
Math.floor(scrollTop / height) - bufferSize,
|
|
25
|
+
)
|
|
26
|
+
const visibleCount = Math.ceil(viewportHeight / height)
|
|
27
|
+
const end = Math.min(start + visibleCount + bufferSize, items.length)
|
|
28
|
+
|
|
29
|
+
if (
|
|
30
|
+
entity.visibleRange.start === start &&
|
|
31
|
+
entity.visibleRange.end === end
|
|
32
|
+
) {
|
|
33
|
+
return
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
entity.scrollTop = scrollTop
|
|
37
|
+
entity.visibleRange = { start, end }
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
mount(entity, containerEl) {
|
|
41
|
+
const firstItem = containerEl.querySelector("[data-index]")
|
|
42
|
+
if (!firstItem) return
|
|
43
|
+
|
|
44
|
+
entity.itemHeight = firstItem.offsetHeight
|
|
45
|
+
entity.visibleRange = {
|
|
46
|
+
start: 0,
|
|
47
|
+
end: Math.ceil(entity.viewportHeight / entity.itemHeight),
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
|
|
15
51
|
render(entity, api) {
|
|
16
52
|
const { items, visibleRange, viewportHeight, itemHeight, estimatedHeight } =
|
|
17
53
|
entity
|
|
18
|
-
const
|
|
19
|
-
const type = types[entity.type]
|
|
54
|
+
const type = api.getType(entity.type)
|
|
20
55
|
|
|
21
56
|
if (!items) {
|
|
22
57
|
console.warn(`list entity ${entity.id} needs 'items'`)
|
|
@@ -39,7 +74,7 @@ export const list = {
|
|
|
39
74
|
${ref((el) => {
|
|
40
75
|
if (el && !itemHeight) {
|
|
41
76
|
queueMicrotask(() => {
|
|
42
|
-
api.notify(`#${entity.id}:
|
|
77
|
+
api.notify(`#${entity.id}:mount`, el)
|
|
43
78
|
})
|
|
44
79
|
}
|
|
45
80
|
})}
|
|
@@ -63,35 +98,8 @@ export const list = {
|
|
|
63
98
|
`
|
|
64
99
|
},
|
|
65
100
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
const { items, bufferSize, itemHeight, estimatedHeight, viewportHeight } =
|
|
69
|
-
entity
|
|
70
|
-
const height = itemHeight || estimatedHeight
|
|
71
|
-
|
|
72
|
-
const start = Math.max(
|
|
73
|
-
LIST_START,
|
|
74
|
-
Math.floor(scrollTop / height) - bufferSize,
|
|
75
|
-
)
|
|
76
|
-
const visibleCount = Math.ceil(viewportHeight / height)
|
|
77
|
-
const end = Math.min(start + visibleCount + bufferSize, items.length)
|
|
78
|
-
|
|
79
|
-
if (
|
|
80
|
-
entity.visibleRange.start === start &&
|
|
81
|
-
entity.visibleRange.end === end
|
|
82
|
-
) {
|
|
83
|
-
return
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
entity.scrollTop = scrollTop
|
|
87
|
-
entity.visibleRange = { start, end }
|
|
88
|
-
},
|
|
89
|
-
|
|
90
|
-
measureHeight(entity, containerEl) {
|
|
91
|
-
const firstItem = containerEl.querySelector("[data-index]")
|
|
92
|
-
if (!firstItem) return
|
|
93
|
-
|
|
94
|
-
entity.itemHeight = firstItem.offsetHeight
|
|
101
|
+
renderItem(item, index) {
|
|
102
|
+
return html`<div>${index + PRETTY_INDEX}. ${JSON.stringify(item)}</div>`
|
|
95
103
|
},
|
|
96
104
|
}
|
|
97
105
|
|
package/src/mount.js
CHANGED
|
@@ -14,7 +14,6 @@ export function mount(store, renderFn, element) {
|
|
|
14
14
|
/** @param {string} id */
|
|
15
15
|
render(id, options = {}) {
|
|
16
16
|
const entity = api.getEntity(id)
|
|
17
|
-
const types = api.getTypes()
|
|
18
17
|
|
|
19
18
|
if (!entity) {
|
|
20
19
|
const { allowType } = options
|
|
@@ -23,7 +22,7 @@ export function mount(store, renderFn, element) {
|
|
|
23
22
|
}
|
|
24
23
|
|
|
25
24
|
// No entity with this ID, try static type
|
|
26
|
-
const type =
|
|
25
|
+
const type = api.getType(id)
|
|
27
26
|
if (!type?.render) {
|
|
28
27
|
console.warn(`No entity or type found: ${id}`)
|
|
29
28
|
return html`<div>Not found: ${id}</div>`
|
|
@@ -32,7 +31,7 @@ export function mount(store, renderFn, element) {
|
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
// Entity exists, render it
|
|
35
|
-
const type =
|
|
34
|
+
const type = api.getType(entity.type)
|
|
36
35
|
if (!type?.render) {
|
|
37
36
|
console.warn(`No render function for type: ${entity.type}`)
|
|
38
37
|
return html`<div>No renderer for ${entity.type}</div>`
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
.iw-table {
|
|
2
|
+
user-select: none;
|
|
3
|
+
|
|
4
|
+
.iw-table-header {
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.iw-table-header-row {
|
|
10
|
+
display: flex;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.iw-table-header-column {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.iw-table-filter-input,
|
|
19
|
+
.iw-table-filter-select {
|
|
20
|
+
width: 100%;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.iw-table-searchbar {
|
|
24
|
+
width: 100%;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.iw-table-row {
|
|
28
|
+
display: flex;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
.iw-table-footer-row {
|
|
32
|
+
display: flex;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { html } from "@inglorious/web"
|
|
2
|
+
|
|
3
|
+
const INPUT_TYPE = {
|
|
4
|
+
date: "date",
|
|
5
|
+
time: "time",
|
|
6
|
+
datetime: "datetime-local",
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const dateFilter = {
|
|
10
|
+
render(entity, column, api) {
|
|
11
|
+
const filter = entity.filters[column.id] ?? {}
|
|
12
|
+
|
|
13
|
+
return html`<input
|
|
14
|
+
name=${`${column.id}Min`}
|
|
15
|
+
type=${INPUT_TYPE[column.filter.type]}
|
|
16
|
+
value=${entity.filters[column.id]}
|
|
17
|
+
@input=${(event) => {
|
|
18
|
+
const value = event.target.value
|
|
19
|
+
const formattedValue = value ? new Date(value).getTime() : null
|
|
20
|
+
|
|
21
|
+
api.notify(`#${entity.id}:filterChange`, {
|
|
22
|
+
columnId: column.id,
|
|
23
|
+
value: { ...filter, min: formattedValue },
|
|
24
|
+
})
|
|
25
|
+
}}
|
|
26
|
+
/>
|
|
27
|
+
<input
|
|
28
|
+
name=${`${column.id}Max`}
|
|
29
|
+
type=${INPUT_TYPE[column.filter.type]}
|
|
30
|
+
value=${entity.filters[column.id]}
|
|
31
|
+
@input=${(event) => {
|
|
32
|
+
const value = event.target.value
|
|
33
|
+
const formattedValue = value ? new Date(value).getTime() : null
|
|
34
|
+
|
|
35
|
+
api.notify(`#${entity.id}:filterChange`, {
|
|
36
|
+
columnId: column.id,
|
|
37
|
+
value: { ...filter, max: formattedValue },
|
|
38
|
+
})
|
|
39
|
+
}}
|
|
40
|
+
/>`
|
|
41
|
+
},
|
|
42
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { html } from "@inglorious/web"
|
|
2
|
+
|
|
3
|
+
export const numberFilter = {
|
|
4
|
+
render(entity, column, api) {
|
|
5
|
+
return html`<input
|
|
6
|
+
name=${column.id}
|
|
7
|
+
type="number"
|
|
8
|
+
placeholder=${column.filter.placeholder ?? "="}
|
|
9
|
+
value=${entity.filters[column.id]}
|
|
10
|
+
@input=${(event) => {
|
|
11
|
+
const value = event.target.value
|
|
12
|
+
const formattedValue = value ? Number(value) : null
|
|
13
|
+
|
|
14
|
+
api.notify(`#${entity.id}:filterChange`, {
|
|
15
|
+
columnId: column.id,
|
|
16
|
+
value: formattedValue,
|
|
17
|
+
})
|
|
18
|
+
}}
|
|
19
|
+
class="iw-table-cell-number"
|
|
20
|
+
/>`
|
|
21
|
+
},
|
|
22
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { html } from "@inglorious/web"
|
|
2
|
+
|
|
3
|
+
export const rangeFilter = {
|
|
4
|
+
render(entity, column, api) {
|
|
5
|
+
const filter = entity.filters[column.id] ?? {}
|
|
6
|
+
|
|
7
|
+
return html`<div class="row">
|
|
8
|
+
<input
|
|
9
|
+
name=${`${column.id}Min`}
|
|
10
|
+
type="number"
|
|
11
|
+
placeholder=${column.filter.placeholder ?? "≥"}
|
|
12
|
+
value=${filter.min}
|
|
13
|
+
@input=${(event) => {
|
|
14
|
+
const value = event.target.value
|
|
15
|
+
const formattedValue = value ? Number(value) : null
|
|
16
|
+
|
|
17
|
+
api.notify(`#${entity.id}:filterChange`, {
|
|
18
|
+
columnId: column.id,
|
|
19
|
+
value: { ...filter, min: formattedValue },
|
|
20
|
+
})
|
|
21
|
+
}}
|
|
22
|
+
class="iw-table-cell-number"
|
|
23
|
+
/>
|
|
24
|
+
<input
|
|
25
|
+
name=${`${column.id}Max`}
|
|
26
|
+
type="number"
|
|
27
|
+
placeholder=${column.filter.placeholder ?? "≤"}
|
|
28
|
+
value=${filter.max}
|
|
29
|
+
@input=${(event) => {
|
|
30
|
+
const value = event.target.value
|
|
31
|
+
const formattedValue = value ? Number(value) : null
|
|
32
|
+
|
|
33
|
+
api.notify(`#${entity.id}:filterChange`, {
|
|
34
|
+
columnId: column.id,
|
|
35
|
+
value: { ...filter, max: formattedValue },
|
|
36
|
+
})
|
|
37
|
+
}}
|
|
38
|
+
class="iw-table-cell-number"
|
|
39
|
+
/>
|
|
40
|
+
</div>`
|
|
41
|
+
},
|
|
42
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { html } from "@inglorious/web"
|
|
2
|
+
|
|
3
|
+
export const selectFilter = {
|
|
4
|
+
render(entity, column, api) {
|
|
5
|
+
return html`<select
|
|
6
|
+
name=${column.id}
|
|
7
|
+
?multiple=${column.filter.isMultiple}
|
|
8
|
+
autocomplete="off"
|
|
9
|
+
value=${entity.filters[column.id]}
|
|
10
|
+
@change=${(event) => {
|
|
11
|
+
const value = event.target.value
|
|
12
|
+
const formattedValue = value ? format(value, column.type) : null
|
|
13
|
+
|
|
14
|
+
api.notify(`#${entity.id}:filterChange`, {
|
|
15
|
+
columnId: column.id,
|
|
16
|
+
value: formattedValue,
|
|
17
|
+
})
|
|
18
|
+
}}
|
|
19
|
+
>
|
|
20
|
+
${column.filter.options.map(
|
|
21
|
+
(option) => html`<option value=${option}>${option}</option>`,
|
|
22
|
+
)}
|
|
23
|
+
</select>`
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function format(value, type) {
|
|
28
|
+
if (type === "number") return Number(value)
|
|
29
|
+
if (type === "boolean")
|
|
30
|
+
return value === "true" ? true : value === "false" ? false : null
|
|
31
|
+
if (["date", "time", "datetime"].includes(type))
|
|
32
|
+
return new Date(value).getTime()
|
|
33
|
+
return value
|
|
34
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { html } from "@inglorious/web"
|
|
2
|
+
|
|
3
|
+
export const textFilter = {
|
|
4
|
+
render(entity, column, api) {
|
|
5
|
+
return html`<input
|
|
6
|
+
name=${column.id}
|
|
7
|
+
type="text"
|
|
8
|
+
placeholder=${column.filter.placeholder ?? "Contains..."}
|
|
9
|
+
autocomplete="off"
|
|
10
|
+
value=${entity.filters[column.id]}
|
|
11
|
+
@input=${(event) => {
|
|
12
|
+
const value = event.target.value
|
|
13
|
+
const formattedValue = value || null
|
|
14
|
+
|
|
15
|
+
api.notify(`#${entity.id}:filterChange`, {
|
|
16
|
+
columnId: column.id,
|
|
17
|
+
value: formattedValue,
|
|
18
|
+
})
|
|
19
|
+
}}
|
|
20
|
+
/>`
|
|
21
|
+
},
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { choose, html } from "@inglorious/web"
|
|
2
|
+
|
|
3
|
+
import { dateFilter } from "./filters/date"
|
|
4
|
+
import { numberFilter } from "./filters/number"
|
|
5
|
+
import { rangeFilter } from "./filters/range"
|
|
6
|
+
import { selectFilter } from "./filters/select"
|
|
7
|
+
import { textFilter } from "./filters/text"
|
|
8
|
+
|
|
9
|
+
export const filters = {
|
|
10
|
+
render(entity, column, api) {
|
|
11
|
+
return html`${choose(
|
|
12
|
+
column.filter.type,
|
|
13
|
+
[
|
|
14
|
+
["number", () => numberFilter.render(entity, column, api)],
|
|
15
|
+
["range", () => rangeFilter.render(entity, column, api)],
|
|
16
|
+
["select", () => selectFilter.render(entity, column, api)],
|
|
17
|
+
["date", () => dateFilter.render(entity, column, api)],
|
|
18
|
+
["time", () => dateFilter.render(entity, column, api)],
|
|
19
|
+
["datetime", () => dateFilter.render(entity, column, api)],
|
|
20
|
+
],
|
|
21
|
+
() => textFilter.render(entity, column, api),
|
|
22
|
+
)}`
|
|
23
|
+
},
|
|
24
|
+
}
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
/* eslint-disable no-magic-numbers */
|
|
2
|
+
|
|
3
|
+
export const logic = {
|
|
4
|
+
init(entity) {
|
|
5
|
+
initTable(entity)
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
create(entity) {
|
|
9
|
+
initTable(entity)
|
|
10
|
+
},
|
|
11
|
+
|
|
12
|
+
sortChange(entity, columnId) {
|
|
13
|
+
const column = entity.columns.find((c) => c.id === columnId)
|
|
14
|
+
if (!column?.isSortable) return
|
|
15
|
+
|
|
16
|
+
const existingIndex = entity.sorts.findIndex((s) => s.column === columnId)
|
|
17
|
+
|
|
18
|
+
if (existingIndex !== -1) {
|
|
19
|
+
// Toggle direction
|
|
20
|
+
const existing = entity.sorts[existingIndex]
|
|
21
|
+
if (existing.direction === "asc") {
|
|
22
|
+
existing.direction = "desc"
|
|
23
|
+
} else {
|
|
24
|
+
// Remove from sort if going from desc back to nothing
|
|
25
|
+
entity.sorts.splice(existingIndex, 1)
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
// Add new sort
|
|
29
|
+
entity.sorts.push({ column: columnId, direction: "asc" })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (entity.pagination) {
|
|
33
|
+
entity.pagination.page = 0
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
sortsClear(entity) {
|
|
38
|
+
entity.sorts = []
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
filterChange(entity, { columnId, value }) {
|
|
42
|
+
if (value == null || value === "") {
|
|
43
|
+
delete entity.filters[columnId]
|
|
44
|
+
} else {
|
|
45
|
+
entity.filters[columnId] = value
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Reset to first page when filtering
|
|
49
|
+
if (entity.pagination) {
|
|
50
|
+
entity.pagination.page = 0
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
filtersClear(entity) {
|
|
55
|
+
entity.filters = {}
|
|
56
|
+
if (entity.pagination) {
|
|
57
|
+
entity.pagination.page = 0
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
searchChange(entity, search) {
|
|
62
|
+
entity.search.value = search
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
pageChange(entity, page) {
|
|
66
|
+
if (!entity.pagination) return
|
|
67
|
+
|
|
68
|
+
const totalPages = Math.ceil(
|
|
69
|
+
getTotalRows(entity) / entity.pagination.pageSize,
|
|
70
|
+
)
|
|
71
|
+
entity.pagination.page = Math.max(0, Math.min(page, totalPages - 1))
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
pageNext(entity) {
|
|
75
|
+
if (!entity.pagination) return
|
|
76
|
+
const totalPages = Math.ceil(
|
|
77
|
+
getTotalRows(entity) / entity.pagination.pageSize,
|
|
78
|
+
)
|
|
79
|
+
entity.pagination.page = Math.min(
|
|
80
|
+
entity.pagination.page + 1,
|
|
81
|
+
totalPages - 1,
|
|
82
|
+
)
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
pagePrev(entity) {
|
|
86
|
+
if (!entity.pagination) return
|
|
87
|
+
entity.pagination.page = Math.max(entity.pagination.page - 1, 0)
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
pageSizeChange(entity, pageSize) {
|
|
91
|
+
if (!entity.pagination) return
|
|
92
|
+
|
|
93
|
+
entity.pagination.pageSize = pageSize
|
|
94
|
+
entity.pagination.page = 0
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
rowSelect(entity, rowId) {
|
|
98
|
+
if (!entity.isMultiSelect) {
|
|
99
|
+
entity.selection = []
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!entity.selection.includes(rowId)) {
|
|
103
|
+
entity.selection.push(rowId)
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
rowDeselect(entity, rowId) {
|
|
108
|
+
const index = entity.selection.indexOf(rowId)
|
|
109
|
+
if (index !== -1) {
|
|
110
|
+
entity.selection.splice(index, 1)
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
rowToggle(entity, rowId) {
|
|
115
|
+
const index = entity.selection.indexOf(rowId)
|
|
116
|
+
|
|
117
|
+
if (index === -1) {
|
|
118
|
+
if (!entity.isMultiSelect) {
|
|
119
|
+
entity.selection = [rowId] // Replace entirely
|
|
120
|
+
} else {
|
|
121
|
+
entity.selection.push(rowId)
|
|
122
|
+
}
|
|
123
|
+
} else {
|
|
124
|
+
entity.selection.splice(index, 1)
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
rowsToggleAll(entity) {
|
|
129
|
+
const rows = getRows(entity)
|
|
130
|
+
const allSelected = rows.every((row) => entity.selection.includes(row.id))
|
|
131
|
+
|
|
132
|
+
if (allSelected) {
|
|
133
|
+
// Deselect all visible
|
|
134
|
+
rows.forEach((row) => {
|
|
135
|
+
const index = entity.selection.indexOf(row.id)
|
|
136
|
+
if (index !== -1) entity.selection.splice(index, 1)
|
|
137
|
+
})
|
|
138
|
+
} else {
|
|
139
|
+
// Select all visible
|
|
140
|
+
rows.forEach((row) => {
|
|
141
|
+
if (!entity.selection.includes(row.id)) {
|
|
142
|
+
entity.selection.push(row.id)
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
rowsSelectAll(entity) {
|
|
149
|
+
const rows = getRows(entity)
|
|
150
|
+
rows.forEach((row) => {
|
|
151
|
+
if (!entity.selection.includes(row.id)) {
|
|
152
|
+
entity.selection.push(row.id)
|
|
153
|
+
}
|
|
154
|
+
})
|
|
155
|
+
},
|
|
156
|
+
|
|
157
|
+
selectionClear(entity) {
|
|
158
|
+
entity.selection.length = 0
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Helper functions outside the type (like form helpers)
|
|
163
|
+
|
|
164
|
+
export function getRows(entity) {
|
|
165
|
+
let rows = entity.data
|
|
166
|
+
rows = applyFilters(entity, rows)
|
|
167
|
+
rows = applySearch(entity, rows)
|
|
168
|
+
rows = applySorts(entity, rows)
|
|
169
|
+
rows = applyPagination(entity, rows)
|
|
170
|
+
|
|
171
|
+
return rows
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export function getTotalRows(entity) {
|
|
175
|
+
let rows = entity.data
|
|
176
|
+
rows = applyFilters(entity, rows)
|
|
177
|
+
rows = applySearch(entity, rows)
|
|
178
|
+
return rows.length
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function getPaginationInfo(entity) {
|
|
182
|
+
if (!entity.pagination) return null
|
|
183
|
+
|
|
184
|
+
const totalRows = getTotalRows(entity)
|
|
185
|
+
const { page, pageSize } = entity.pagination
|
|
186
|
+
const totalPages = Math.ceil(totalRows / pageSize)
|
|
187
|
+
const start = page * pageSize
|
|
188
|
+
const end = Math.min((page + 1) * pageSize, totalRows)
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
page,
|
|
192
|
+
pageSize,
|
|
193
|
+
totalPages,
|
|
194
|
+
totalRows,
|
|
195
|
+
start,
|
|
196
|
+
end,
|
|
197
|
+
hasNextPage: page < totalPages - 1,
|
|
198
|
+
hasPrevPage: page > 0,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function getSortDirection(entity, columnId) {
|
|
203
|
+
const sort = entity.sorts.find((s) => s.column === columnId)
|
|
204
|
+
return sort?.direction || null
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function getSortIndex(entity, columnId) {
|
|
208
|
+
return entity.sorts.findIndex((s) => s.column === columnId)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function getFilter(entity, columnId) {
|
|
212
|
+
return entity.filters[columnId]
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function isRowSelected(entity, rowId) {
|
|
216
|
+
return entity.selection.includes(rowId)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export function isAllSelected(entity) {
|
|
220
|
+
const rows = getRows(entity)
|
|
221
|
+
return rows.length && rows.every((row) => entity.selection.includes(row.id))
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function isSomeSelected(entity) {
|
|
225
|
+
const rows = getRows(entity)
|
|
226
|
+
const selectedCount = rows.filter((row) =>
|
|
227
|
+
entity.selection.includes(row.id),
|
|
228
|
+
).length
|
|
229
|
+
return selectedCount && selectedCount < rows.length
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function initTable(entity) {
|
|
233
|
+
entity.data ??= []
|
|
234
|
+
|
|
235
|
+
// Auto-generate columns from first data item if not provided
|
|
236
|
+
if (!entity.columns && entity.data.length) {
|
|
237
|
+
const [firstRow] = entity.data
|
|
238
|
+
|
|
239
|
+
entity.columns = Object.keys(firstRow).map((key) => {
|
|
240
|
+
const value = firstRow[key]
|
|
241
|
+
const type = getDefaultColumnType(value)
|
|
242
|
+
const filter = getDefaultColumnFilter(type)
|
|
243
|
+
|
|
244
|
+
return {
|
|
245
|
+
id: key,
|
|
246
|
+
title: capitalize(key),
|
|
247
|
+
type,
|
|
248
|
+
isSortable: false,
|
|
249
|
+
isFilterable: false,
|
|
250
|
+
filter,
|
|
251
|
+
width: getDefaultColumnWidth(filter.type),
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
} else {
|
|
255
|
+
entity.columns ??= []
|
|
256
|
+
entity.columns.forEach((column) => {
|
|
257
|
+
column.title ??= capitalize(column.id)
|
|
258
|
+
column.type ??= getDefaultColumnType()
|
|
259
|
+
column.filter ??= getDefaultColumnFilter(column.type)
|
|
260
|
+
column.width ??= getDefaultColumnWidth(column.filter.type)
|
|
261
|
+
})
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// State
|
|
265
|
+
entity.sorts ??= []
|
|
266
|
+
entity.filters ??= {}
|
|
267
|
+
entity.search ??= null
|
|
268
|
+
if (entity.search) {
|
|
269
|
+
entity.search.value ??= ""
|
|
270
|
+
}
|
|
271
|
+
entity.selection ??= []
|
|
272
|
+
|
|
273
|
+
entity.pagination ??= null
|
|
274
|
+
if (entity.pagination) {
|
|
275
|
+
entity.pagination.page ??= 0
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function getDefaultColumnType(value) {
|
|
280
|
+
if (typeof value === "number") return "number"
|
|
281
|
+
if (typeof value === "boolean") return "boolean"
|
|
282
|
+
if (value instanceof Date) return "date"
|
|
283
|
+
return "string"
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function getDefaultColumnFilter(type) {
|
|
287
|
+
if (type === "number") return { type: "range" }
|
|
288
|
+
if (type === "boolean")
|
|
289
|
+
return { type: "select", options: [null, true, false] }
|
|
290
|
+
if (type === "date") return { type: "date" }
|
|
291
|
+
return { type: "text" }
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function getDefaultColumnWidth(filterType) {
|
|
295
|
+
if (filterType === "number") return 100
|
|
296
|
+
if (filterType === "range") return 200
|
|
297
|
+
if (filterType === "select") return 70
|
|
298
|
+
if (filterType === "date") return 120
|
|
299
|
+
if (filterType === "time") return 120
|
|
300
|
+
if (filterType === "datetime") return 170
|
|
301
|
+
return 200
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function applyFilters(entity, rows) {
|
|
305
|
+
if (!Object.keys(entity.filters).length) {
|
|
306
|
+
return rows
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return rows.filter((row) => {
|
|
310
|
+
return Object.entries(entity.filters).every(([columnId, filterValue]) => {
|
|
311
|
+
const column = entity.columns.find((c) => c.id === columnId)
|
|
312
|
+
if (!column) return true
|
|
313
|
+
|
|
314
|
+
// Custom filter function
|
|
315
|
+
if (column.filterFn) {
|
|
316
|
+
return column.filterFn(row, filterValue)
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// Default filters by type
|
|
320
|
+
const value = row[columnId]
|
|
321
|
+
|
|
322
|
+
if (["range", "date", "time", "datetime"].includes(column.filter.type)) {
|
|
323
|
+
const { min, max } = filterValue
|
|
324
|
+
if (min != null && value < min) return false
|
|
325
|
+
if (max != null && value > max) return false
|
|
326
|
+
return true
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (["number", "boolean", "select"].includes(column.filter.type)) {
|
|
330
|
+
return value === filterValue
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// String filtering (case-insensitive contains)
|
|
334
|
+
return String(value)
|
|
335
|
+
.toLowerCase()
|
|
336
|
+
.includes(String(filterValue).toLowerCase())
|
|
337
|
+
})
|
|
338
|
+
})
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function applySearch(entity, rows) {
|
|
342
|
+
if (!entity.search?.value) {
|
|
343
|
+
return rows
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const searchLower = entity.search.value.toLowerCase()
|
|
347
|
+
|
|
348
|
+
return rows.filter((row) =>
|
|
349
|
+
entity.columns.some((column) => {
|
|
350
|
+
const value = row[column.id]
|
|
351
|
+
const formattedValue = column.format?.(value) ?? String(value)
|
|
352
|
+
return formattedValue.toLowerCase().includes(searchLower)
|
|
353
|
+
}),
|
|
354
|
+
)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function applySorts(entity, rows) {
|
|
358
|
+
if (!entity.sorts.length) {
|
|
359
|
+
return rows
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
return [...rows].sort((a, b) => {
|
|
363
|
+
for (const { column: columnId, direction } of entity.sorts) {
|
|
364
|
+
const column = entity.columns.find((c) => c.id === columnId)
|
|
365
|
+
let aVal = a[columnId]
|
|
366
|
+
let bVal = b[columnId]
|
|
367
|
+
|
|
368
|
+
// Custom sort function
|
|
369
|
+
if (column?.sortFn) {
|
|
370
|
+
const result =
|
|
371
|
+
direction === "asc" ? column.sortFn(a, b) : column.sortFn(b, a)
|
|
372
|
+
if (result !== 0) return result
|
|
373
|
+
continue
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Default sorting
|
|
377
|
+
if (aVal === bVal) continue
|
|
378
|
+
if (aVal == null) return 1
|
|
379
|
+
if (bVal == null) return -1
|
|
380
|
+
|
|
381
|
+
const comparison = aVal < bVal ? -1 : 1
|
|
382
|
+
return direction === "asc" ? comparison : -comparison
|
|
383
|
+
}
|
|
384
|
+
return 0
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function applyPagination(entity, rows) {
|
|
389
|
+
if (!entity.pagination) {
|
|
390
|
+
return rows
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const { page, pageSize } = entity.pagination
|
|
394
|
+
const start = page * pageSize
|
|
395
|
+
return rows.slice(start, start + pageSize)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function capitalize(str) {
|
|
399
|
+
const [firstChar, ...rest] = str
|
|
400
|
+
return [firstChar.toUpperCase(), ...rest].join("")
|
|
401
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import { html } from "lit-html"
|
|
2
|
+
import { ref } from "lit-html/directives/ref.js"
|
|
3
|
+
|
|
4
|
+
import { filters } from "./filters"
|
|
5
|
+
import { getPaginationInfo, getRows, getSortDirection } from "./logic"
|
|
6
|
+
|
|
7
|
+
const DIVISOR = 2
|
|
8
|
+
const FIRST_PAGE = 0
|
|
9
|
+
const LAST_PAGE = 1
|
|
10
|
+
const PRETTY_PAGE = 1
|
|
11
|
+
const PERCENTAGE_TO_FLEX = 0.01
|
|
12
|
+
|
|
13
|
+
export const rendering = {
|
|
14
|
+
mount(entity, containerEl) {
|
|
15
|
+
const columns = containerEl.querySelectorAll(":scope > *")
|
|
16
|
+
;[...columns].forEach((column, index) => {
|
|
17
|
+
entity.columns[index].width = column.offsetWidth
|
|
18
|
+
})
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
render(entity, api) {
|
|
22
|
+
const type = api.getType(entity.type)
|
|
23
|
+
|
|
24
|
+
return html`<div class="iw-table">
|
|
25
|
+
${type.renderHeader(entity, api)} ${type.renderBody(entity, api)}
|
|
26
|
+
${type.renderFooter(entity, api)}
|
|
27
|
+
</div> `
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
renderHeader(entity, api) {
|
|
31
|
+
const type = api.getType(entity.type)
|
|
32
|
+
|
|
33
|
+
return html`<div class="iw-table-header">
|
|
34
|
+
<div
|
|
35
|
+
class="iw-table-header-row"
|
|
36
|
+
${ref((el) => {
|
|
37
|
+
if (
|
|
38
|
+
el &&
|
|
39
|
+
entity.columns.some(({ width }) => typeof width === "string")
|
|
40
|
+
) {
|
|
41
|
+
queueMicrotask(() => {
|
|
42
|
+
api.notify(`#${entity.id}:mount`, el)
|
|
43
|
+
})
|
|
44
|
+
}
|
|
45
|
+
})}
|
|
46
|
+
>
|
|
47
|
+
${entity.columns.map((column) =>
|
|
48
|
+
type.renderHeaderColumn(entity, column, api),
|
|
49
|
+
)}
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
${entity.search && type.renderSearchbar(entity, api)}
|
|
53
|
+
</div>`
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
renderHeaderColumn(entity, column, api) {
|
|
57
|
+
return html`<div
|
|
58
|
+
class="iw-table-header-column"
|
|
59
|
+
style=${getColumnStyle(column)}
|
|
60
|
+
>
|
|
61
|
+
<div
|
|
62
|
+
@click=${() =>
|
|
63
|
+
column.isSortable &&
|
|
64
|
+
api.notify(`#${entity.id}:sortChange`, column.id)}
|
|
65
|
+
class="iw-table-header-title"
|
|
66
|
+
>
|
|
67
|
+
${column.title} ${getSortIcon(getSortDirection(entity, column.id))}
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
${column.isFilterable && filters.render(entity, column, api)}
|
|
71
|
+
</div>`
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
renderSearchbar(entity, api) {
|
|
75
|
+
return html`<input
|
|
76
|
+
name="search"
|
|
77
|
+
type="text"
|
|
78
|
+
placeholder=${entity.search.placeholder ?? "Fuzzy search..."}
|
|
79
|
+
value=${entity.search.value}
|
|
80
|
+
@input=${(event) =>
|
|
81
|
+
api.notify(`#${entity.id}:searchChange`, event.target.value)}
|
|
82
|
+
class="iw-table-searchbar"
|
|
83
|
+
/>`
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
renderBody(entity, api) {
|
|
87
|
+
const type = api.getType(entity.type)
|
|
88
|
+
|
|
89
|
+
return html`<div class="iw-table-body">
|
|
90
|
+
${getRows(entity).map((row, index) =>
|
|
91
|
+
type.renderRow(entity, row, index, api),
|
|
92
|
+
)}
|
|
93
|
+
</div>`
|
|
94
|
+
},
|
|
95
|
+
|
|
96
|
+
renderRow(entity, row, index, api) {
|
|
97
|
+
const type = api.getType(entity.type)
|
|
98
|
+
const rowId = row[entity.rowId ?? "id"]
|
|
99
|
+
|
|
100
|
+
return html`<div
|
|
101
|
+
@click=${() => api.notify(`#${entity.id}:rowToggle`, rowId)}
|
|
102
|
+
class="iw-table-row ${index % DIVISOR
|
|
103
|
+
? "iw-table-row-even"
|
|
104
|
+
: "iw-table-row-odd"} ${entity.selection.includes(rowId)
|
|
105
|
+
? "iw-table-row-selected"
|
|
106
|
+
: ""}"
|
|
107
|
+
>
|
|
108
|
+
${Object.values(row).map((value, index) =>
|
|
109
|
+
type.renderCell(entity, value, index, api),
|
|
110
|
+
)}
|
|
111
|
+
</div>`
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
renderCell(entity, cell, index, api) {
|
|
115
|
+
const type = api.getType(entity.type)
|
|
116
|
+
const column = entity.columns[index]
|
|
117
|
+
|
|
118
|
+
return html`<div
|
|
119
|
+
class=${`iw-table-cell ${column.type === "number" ? "iw-table-cell-number" : ""} ${column.type === "date" ? "iw-table-cell-date" : ""} ${column.type === "boolean" ? "iw-table-cell-boolean" : ""}`}
|
|
120
|
+
style=${getColumnStyle(column)}
|
|
121
|
+
>
|
|
122
|
+
${type.renderValue(cell, column, api)}
|
|
123
|
+
</div>`
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
renderValue(value) {
|
|
127
|
+
return value
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
renderFooter(entity, api) {
|
|
131
|
+
const type = api.getType(entity.type)
|
|
132
|
+
const pagination = getPaginationInfo(entity)
|
|
133
|
+
|
|
134
|
+
return html`<div class="iw-table-footer">
|
|
135
|
+
<div class="iw-table-footer-row">
|
|
136
|
+
<div>
|
|
137
|
+
${pagination.start + PRETTY_PAGE} to ${pagination.end} of ${pagination.totalRows}
|
|
138
|
+
entries
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
${type.renderPagination(entity, pagination, api)}
|
|
142
|
+
|
|
143
|
+
<div class="iw-table-footer-row">
|
|
144
|
+
<div>Page size:</div>
|
|
145
|
+
<select
|
|
146
|
+
name="pageSize"
|
|
147
|
+
@change=${(event) =>
|
|
148
|
+
api.notify(
|
|
149
|
+
`#${entity.id}:pageSizeChange`,
|
|
150
|
+
Number(event.target.value),
|
|
151
|
+
)}
|
|
152
|
+
>
|
|
153
|
+
<option>10</option>
|
|
154
|
+
<option>20</option>
|
|
155
|
+
<option>30</option>
|
|
156
|
+
</select>
|
|
157
|
+
</div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
</div>`
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
renderPagination(entity, pagination, api) {
|
|
164
|
+
return html`<div class="iw-table-row">
|
|
165
|
+
<button
|
|
166
|
+
?disabled=${!pagination.hasPrevPage}
|
|
167
|
+
@click=${() => api.notify(`#${entity.id}:pageChange`, FIRST_PAGE)}
|
|
168
|
+
class="iw-table-pagination-button"
|
|
169
|
+
>
|
|
170
|
+
|❮
|
|
171
|
+
</button>
|
|
172
|
+
<button
|
|
173
|
+
?disabled=${!pagination.hasPrevPage}
|
|
174
|
+
@click=${() => api.notify(`#${entity.id}:pagePrev`)}
|
|
175
|
+
class="iw-table-pagination-button"
|
|
176
|
+
>
|
|
177
|
+
❮
|
|
178
|
+
</button>
|
|
179
|
+
<input
|
|
180
|
+
name="page"
|
|
181
|
+
type="number"
|
|
182
|
+
min="1"
|
|
183
|
+
max=${pagination.totalPages}
|
|
184
|
+
value=${pagination.page + PRETTY_PAGE}
|
|
185
|
+
class=${`iw-table-page-input`}
|
|
186
|
+
@input=${(event) =>
|
|
187
|
+
api.notify(
|
|
188
|
+
`#${entity.id}:pageChange`,
|
|
189
|
+
Number(event.target.value) - PRETTY_PAGE,
|
|
190
|
+
)}
|
|
191
|
+
/>
|
|
192
|
+
/
|
|
193
|
+
<span>${pagination.totalPages}</span>
|
|
194
|
+
<button
|
|
195
|
+
?disabled="${!pagination.hasNextPage}"
|
|
196
|
+
@click=${() => api.notify(`#${entity.id}:pageNext`)}
|
|
197
|
+
class="iw-table-pagination-button"
|
|
198
|
+
>
|
|
199
|
+
❯
|
|
200
|
+
</button>
|
|
201
|
+
<button
|
|
202
|
+
?disabled=${!pagination.hasNextPage}
|
|
203
|
+
@click=${() =>
|
|
204
|
+
api.notify(
|
|
205
|
+
`#${entity.id}:pageChange`,
|
|
206
|
+
pagination.totalPages - LAST_PAGE,
|
|
207
|
+
)}
|
|
208
|
+
class="iw-table-pagination-button"
|
|
209
|
+
>
|
|
210
|
+
❯|
|
|
211
|
+
</button>
|
|
212
|
+
</div>`
|
|
213
|
+
},
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function getColumnStyle(column) {
|
|
217
|
+
if (typeof column.width === "string") {
|
|
218
|
+
if (column.width?.endsWith("%")) {
|
|
219
|
+
// eslint-disable-next-line no-magic-numbers
|
|
220
|
+
const percentage = Number(column.width.slice(0, -1))
|
|
221
|
+
return `flex: ${percentage * PERCENTAGE_TO_FLEX}`
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return `width: ${column.width}`
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return `width: ${column.width}px`
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function getSortIcon(direction) {
|
|
231
|
+
switch (direction) {
|
|
232
|
+
case "asc":
|
|
233
|
+
return "▲"
|
|
234
|
+
case "desc":
|
|
235
|
+
return "▼"
|
|
236
|
+
default:
|
|
237
|
+
return "▲▼"
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
.iw-table {
|
|
2
|
+
.iw-table-header {
|
|
3
|
+
border-bottom: 1px solid grey;
|
|
4
|
+
padding: 1em 0;
|
|
5
|
+
row-gap: 1em;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.iw-table-header-row {
|
|
9
|
+
align-items: flex-start;
|
|
10
|
+
column-gap: 1em;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.iw-table-header-column {
|
|
14
|
+
row-gap: 0.5em;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.iw-table-header-title {
|
|
18
|
+
font-weight: bold;
|
|
19
|
+
white-space: nowrap;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.iw-table-body {
|
|
23
|
+
max-height: 35em;
|
|
24
|
+
overflow: auto;
|
|
25
|
+
border-bottom: 1px solid grey;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.iw-table-row {
|
|
29
|
+
align-items: center;
|
|
30
|
+
column-gap: 1em;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.iw-table-row-even {
|
|
34
|
+
background-color: aliceblue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.iw-table-row-selected {
|
|
38
|
+
border-left: 1px solid cornflowerblue;
|
|
39
|
+
border-right: 1px solid cornflowerblue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
:not(.iw-table-row-selected) + .iw-table-row-selected {
|
|
43
|
+
border-top: 1px solid cornflowerblue;
|
|
44
|
+
margin-top: -1px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.iw-table-row-selected + :not(.iw-table-row-selected) {
|
|
48
|
+
border-top: 1px solid cornflowerblue;
|
|
49
|
+
margin-top: -1px;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.iw-table-cell {
|
|
53
|
+
padding: 1em;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.iw-table-cell-number,
|
|
57
|
+
.iw-table-cell-date {
|
|
58
|
+
text-align: right;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.iw-table-cell-boolean {
|
|
62
|
+
text-align: center;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.iw-table-footer {
|
|
66
|
+
padding: 1em 0;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.iw-table-footer-row {
|
|
70
|
+
justify-content: space-between;
|
|
71
|
+
align-items: center;
|
|
72
|
+
column-gap: 1em;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.iw-table-page-input {
|
|
76
|
+
min-width: 4em;
|
|
77
|
+
text-align: right;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.iw-table-pagination-button {
|
|
81
|
+
white-space: nowrap;
|
|
82
|
+
}
|
|
83
|
+
}
|