@inglorious/web 2.0.1 → 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 +4 -2
- package/src/index.js +3 -1
- package/src/list.js +6 -2
- 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 +4 -369
|
@@ -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
|
+
}
|