@inglorious/web 2.0.0 → 2.0.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inglorious/web",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
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",
@@ -36,7 +36,7 @@
36
36
  },
37
37
  "dependencies": {
38
38
  "lit-html": "^3.3.1",
39
- "@inglorious/store": "7.0.0",
39
+ "@inglorious/store": "7.1.0",
40
40
  "@inglorious/utils": "3.7.0"
41
41
  },
42
42
  "devDependencies": {
package/src/index.js CHANGED
@@ -2,6 +2,7 @@ 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 { getPaginationInfo, getRows, getSortDirection, 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"
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,41 @@ 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
+ measureHeight(entity, containerEl) {
41
+ const firstItem = containerEl.querySelector("[data-index]")
42
+ if (!firstItem) return
43
+
44
+ entity.itemHeight = firstItem.offsetHeight
45
+ },
46
+
15
47
  render(entity, api) {
16
48
  const { items, visibleRange, viewportHeight, itemHeight, estimatedHeight } =
17
49
  entity
18
- const types = api.getTypes()
19
- const type = types[entity.type]
50
+ const type = api.getType(entity.type)
20
51
 
21
52
  if (!items) {
22
53
  console.warn(`list entity ${entity.id} needs 'items'`)
@@ -63,35 +94,8 @@ export const list = {
63
94
  `
64
95
  },
65
96
 
66
- scroll(entity, containerEl) {
67
- const scrollTop = containerEl.scrollTop
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
97
+ renderItem(item, index) {
98
+ return html`<div>${index + PRETTY_INDEX}. ${JSON.stringify(item)}</div>`
95
99
  },
96
100
  }
97
101
 
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 = types[id]
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 = types[entity.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>`
package/src/table.js ADDED
@@ -0,0 +1,372 @@
1
+ /* eslint-disable no-magic-numbers */
2
+
3
+ export const table = {
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
+ filter(entity, { columnId, value }) {
42
+ if (value === null || value === undefined || 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
+ pageChange(entity, page) {
62
+ if (!entity.pagination) return
63
+
64
+ const totalPages = Math.ceil(
65
+ getTotalRows(entity) / entity.pagination.pageSize,
66
+ )
67
+ entity.pagination.page = Math.max(0, Math.min(page, totalPages - 1))
68
+ },
69
+
70
+ pageNext(entity) {
71
+ if (!entity.pagination) return
72
+ const totalPages = Math.ceil(
73
+ getTotalRows(entity) / entity.pagination.pageSize,
74
+ )
75
+ entity.pagination.page = Math.min(
76
+ entity.pagination.page + 1,
77
+ totalPages - 1,
78
+ )
79
+ },
80
+
81
+ pagePrev(entity) {
82
+ if (!entity.pagination) return
83
+ entity.pagination.page = Math.max(entity.pagination.page - 1, 0)
84
+ },
85
+
86
+ pageSizeChange(entity, pageSize) {
87
+ if (!entity.pagination) return
88
+
89
+ entity.pagination.pageSize = pageSize
90
+ entity.pagination.page = 0
91
+ },
92
+
93
+ rowSelect(entity, rowId) {
94
+ if (!entity.selection.includes(rowId)) {
95
+ entity.selection.push(rowId)
96
+ }
97
+ },
98
+
99
+ rowDeselect(entity, rowId) {
100
+ const index = entity.selection.indexOf(rowId)
101
+ if (index !== -1) {
102
+ entity.selection.splice(index, 1)
103
+ }
104
+ },
105
+
106
+ rowToggle(entity, rowId) {
107
+ const index = entity.selection.indexOf(rowId)
108
+ if (index === -1) {
109
+ entity.selection.push(rowId)
110
+ } else {
111
+ entity.selection.splice(index, 1)
112
+ }
113
+ },
114
+
115
+ rowsToggleAll(entity) {
116
+ const rows = getRows(entity)
117
+ const allSelected = rows.every((row) => entity.selection.includes(row.id))
118
+
119
+ if (allSelected) {
120
+ // Deselect all visible
121
+ rows.forEach((row) => {
122
+ const index = entity.selection.indexOf(row.id)
123
+ if (index !== -1) entity.selection.splice(index, 1)
124
+ })
125
+ } else {
126
+ // Select all visible
127
+ rows.forEach((row) => {
128
+ if (!entity.selection.includes(row.id)) {
129
+ entity.selection.push(row.id)
130
+ }
131
+ })
132
+ }
133
+ },
134
+
135
+ rowsSelectAll(entity) {
136
+ const rows = getRows(entity)
137
+ rows.forEach((row) => {
138
+ if (!entity.selection.includes(row.id)) {
139
+ entity.selection.push(row.id)
140
+ }
141
+ })
142
+ },
143
+
144
+ selectionClear(entity) {
145
+ entity.selection.length = 0
146
+ },
147
+ }
148
+
149
+ // Helper functions outside the type (like form helpers)
150
+
151
+ export function getRows(entity) {
152
+ let rows = entity.data || []
153
+
154
+ // Filtering
155
+ if (Object.keys(entity.filters).length > 0) {
156
+ rows = rows.filter((row) => {
157
+ return Object.entries(entity.filters).every(([columnId, filterValue]) => {
158
+ const column = entity.columns.find((c) => c.id === columnId)
159
+ if (!column) return true
160
+
161
+ // Custom filter function
162
+ if (column.filterFn) {
163
+ return column.filterFn(row, filterValue)
164
+ }
165
+
166
+ // Default filters by type
167
+ const value = row[columnId]
168
+
169
+ if (column.type === "number") {
170
+ if (typeof filterValue === "object") {
171
+ const { min, max } = filterValue
172
+ if (min !== undefined && value < min) return false
173
+ if (max !== undefined && value > max) return false
174
+ return true
175
+ }
176
+ return value === filterValue
177
+ }
178
+
179
+ if (column.type === "boolean") {
180
+ return value === filterValue
181
+ }
182
+
183
+ // String filtering (case-insensitive contains)
184
+ return String(value)
185
+ .toLowerCase()
186
+ .includes(String(filterValue).toLowerCase())
187
+ })
188
+ })
189
+ }
190
+
191
+ // Sorting
192
+ if (entity.sorts.length) {
193
+ rows = [...rows].sort((a, b) => {
194
+ for (const { column: columnId, direction } of entity.sorts) {
195
+ const column = entity.columns.find((c) => c.id === columnId)
196
+ let aVal = a[columnId]
197
+ let bVal = b[columnId]
198
+
199
+ // Custom sort function
200
+ if (column?.sortFn) {
201
+ const result =
202
+ direction === "asc" ? column.sortFn(a, b) : column.sortFn(b, a)
203
+ if (result !== 0) return result
204
+ continue
205
+ }
206
+
207
+ // Default sorting
208
+ if (aVal === bVal) continue
209
+ if (aVal == null) return 1
210
+ if (bVal == null) return -1
211
+
212
+ const comparison = aVal < bVal ? -1 : 1
213
+ return direction === "asc" ? comparison : -comparison
214
+ }
215
+ return 0
216
+ })
217
+ }
218
+
219
+ // Pagination
220
+ if (entity.pagination) {
221
+ const { page, pageSize } = entity.pagination
222
+ const start = page * pageSize
223
+ rows = rows.slice(start, start + pageSize)
224
+ }
225
+
226
+ return rows
227
+ }
228
+
229
+ export function getTotalRows(entity) {
230
+ if (!entity.data) return 0
231
+
232
+ // If no filters, return total data length
233
+ if (Object.keys(entity.filters).length === 0) {
234
+ return entity.data.length
235
+ }
236
+
237
+ // Count filtered rows
238
+ return entity.data.filter((row) => {
239
+ return Object.entries(entity.filters).every(([columnId, filterValue]) => {
240
+ const column = entity.columns.find((c) => c.id === columnId)
241
+ if (!column) return true
242
+
243
+ // Custom filter function
244
+ if (column.filterFn) {
245
+ return column.filterFn(row, filterValue)
246
+ }
247
+
248
+ // Default filters by type
249
+ const value = row[columnId]
250
+
251
+ if (column.type === "number") {
252
+ if (typeof filterValue === "object") {
253
+ const { min, max } = filterValue
254
+ if (min !== undefined && value < min) return false
255
+ if (max !== undefined && value > max) return false
256
+ return true
257
+ }
258
+ return value === filterValue
259
+ }
260
+
261
+ if (column.type === "boolean") {
262
+ return value === filterValue
263
+ }
264
+
265
+ // String filtering (case-insensitive contains)
266
+ return String(value)
267
+ .toLowerCase()
268
+ .includes(String(filterValue).toLowerCase())
269
+ })
270
+ }).length
271
+ }
272
+
273
+ export function getPaginationInfo(entity) {
274
+ if (!entity.pagination) return null
275
+
276
+ const totalRows = getTotalRows(entity)
277
+ const { page, pageSize } = entity.pagination
278
+ const totalPages = Math.ceil(totalRows / pageSize)
279
+ const start = page * pageSize
280
+ const end = Math.min((page + 1) * pageSize, totalRows)
281
+
282
+ return {
283
+ page,
284
+ pageSize,
285
+ totalPages,
286
+ totalRows,
287
+ start,
288
+ end,
289
+ hasNextPage: page < totalPages - 1,
290
+ hasPrevPage: page > 0,
291
+ }
292
+ }
293
+
294
+ export function getSortDirection(entity, columnId) {
295
+ const sort = entity.sorts.find((s) => s.column === columnId)
296
+ return sort?.direction || null
297
+ }
298
+
299
+ export function getSortIndex(entity, columnId) {
300
+ return entity.sorts.findIndex((s) => s.column === columnId)
301
+ }
302
+
303
+ export function getFilter(entity, columnId) {
304
+ return entity.filters[columnId]
305
+ }
306
+
307
+ export function isRowSelected(entity, rowId) {
308
+ return entity.selection.includes(rowId)
309
+ }
310
+
311
+ export function isAllSelected(entity) {
312
+ const rows = getRows(entity)
313
+ return rows.length && rows.every((row) => entity.selection.includes(row.id))
314
+ }
315
+
316
+ export function isSomeSelected(entity) {
317
+ const rows = getRows(entity)
318
+ const selectedCount = rows.filter((row) =>
319
+ entity.selection.includes(row.id),
320
+ ).length
321
+ return selectedCount && selectedCount < rows.length
322
+ }
323
+
324
+ function initTable(entity) {
325
+ // Auto-generate columns from first data item if not provided
326
+ if (!entity.columns && entity.data?.length) {
327
+ const [firstRow] = entity.data
328
+
329
+ entity.columns = Object.keys(firstRow).map((key) => {
330
+ const value = firstRow[key]
331
+ const type = getDefaultColumnType(value)
332
+ const [firstChar, ...rest] = key
333
+
334
+ return {
335
+ id: key,
336
+ title: [firstChar.toUpperCase(), ...rest].join(""),
337
+ isSortable: true,
338
+ isFilterable: type !== "boolean",
339
+ type,
340
+ width: getDefaultColumnWidth(type),
341
+ }
342
+ })
343
+ } else {
344
+ entity.columns ??= []
345
+ }
346
+
347
+ // State
348
+ entity.sorts ??= []
349
+ entity.filters ??= {}
350
+ entity.selection ??= []
351
+
352
+ // Pagination (null = disabled)
353
+ entity.pagination ??= null
354
+
355
+ if (entity.pagination) {
356
+ entity.pagination.page ??= 0
357
+ }
358
+ }
359
+
360
+ function getDefaultColumnType(value) {
361
+ if (typeof value === "number") return "number"
362
+ if (typeof value === "boolean") return "boolean"
363
+ if (value instanceof Date) return "date"
364
+ return "string"
365
+ }
366
+
367
+ function getDefaultColumnWidth(type) {
368
+ if (type === "number") return 100
369
+ if (type === "boolean") return 80
370
+ if (type === "date") return 120
371
+ return 200
372
+ }