@inglorious/web 4.4.0 → 4.5.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.
@@ -1,573 +0,0 @@
1
- /* eslint-disable no-magic-numbers */
2
-
3
- /**
4
- * @typedef {import('../../types/table').TableEntity} TableEntity
5
- * @typedef {import('../../types/table').TableColumn} TableColumn
6
- */
7
-
8
- export const logic = {
9
- /**
10
- * Resets the table entity with default state.
11
- * @param {TableEntity} entity
12
- */
13
- create(entity) {
14
- initTable(entity)
15
- },
16
-
17
- /**
18
- * Toggles sorting for a column.
19
- * @param {TableEntity} entity
20
- * @param {string} columnId
21
- */
22
- sortChange(entity, columnId) {
23
- const column = entity.columns.find((c) => c.id === columnId)
24
- if (!column?.isSortable) return
25
-
26
- const existingIndex = entity.sorts.findIndex((s) => s.column === columnId)
27
-
28
- if (existingIndex !== -1) {
29
- // Toggle direction
30
- const existing = entity.sorts[existingIndex]
31
- if (existing.direction === "asc") {
32
- existing.direction = "desc"
33
- } else {
34
- // Remove from sort if going from desc back to nothing
35
- entity.sorts.splice(existingIndex, 1)
36
- }
37
- } else {
38
- // Add new sort
39
- entity.sorts.push({ column: columnId, direction: "asc" })
40
- }
41
-
42
- if (entity.pagination) {
43
- entity.pagination.page = 0
44
- }
45
- },
46
-
47
- /**
48
- * Clears all sorts.
49
- * @param {TableEntity} entity
50
- */
51
- sortsClear(entity) {
52
- entity.sorts = []
53
- },
54
-
55
- /**
56
- * Updates a filter value for a column.
57
- * @param {TableEntity} entity
58
- * @param {{ columnId: string, value: any }} payload
59
- */
60
- filterChange(entity, { columnId, value }) {
61
- if (value == null || value === "") {
62
- delete entity.filters[columnId]
63
- } else {
64
- entity.filters[columnId] = value
65
- }
66
-
67
- // Reset to first page when filtering
68
- if (entity.pagination) {
69
- entity.pagination.page = 0
70
- }
71
- },
72
-
73
- /**
74
- * Clears all filters.
75
- * @param {TableEntity} entity
76
- */
77
- filtersClear(entity) {
78
- entity.filters = {}
79
- if (entity.pagination) {
80
- entity.pagination.page = 0
81
- }
82
- },
83
-
84
- /**
85
- * Updates the search term.
86
- * @param {TableEntity} entity
87
- * @param {string} search
88
- */
89
- searchChange(entity, search) {
90
- entity.search.value = search
91
- },
92
-
93
- /**
94
- * Changes the current page.
95
- * @param {TableEntity} entity
96
- * @param {number} page
97
- */
98
- pageChange(entity, page) {
99
- if (!entity.pagination) return
100
-
101
- const totalPages = Math.ceil(
102
- getTotalRows(entity) / entity.pagination.pageSize,
103
- )
104
- entity.pagination.page = Math.max(0, Math.min(page, totalPages - 1))
105
- },
106
-
107
- /**
108
- * Moves to the next page.
109
- * @param {TableEntity} entity
110
- */
111
- pageNext(entity) {
112
- if (!entity.pagination) return
113
- const totalPages = Math.ceil(
114
- getTotalRows(entity) / entity.pagination.pageSize,
115
- )
116
- entity.pagination.page = Math.min(
117
- entity.pagination.page + 1,
118
- totalPages - 1,
119
- )
120
- },
121
-
122
- /**
123
- * Moves to the previous page.
124
- * @param {TableEntity} entity
125
- */
126
- pagePrev(entity) {
127
- if (!entity.pagination) return
128
- entity.pagination.page = Math.max(entity.pagination.page - 1, 0)
129
- },
130
-
131
- /**
132
- * Changes the page size.
133
- * @param {TableEntity} entity
134
- * @param {number} pageSize
135
- */
136
- pageSizeChange(entity, pageSize) {
137
- if (!entity.pagination) return
138
-
139
- entity.pagination.pageSize = pageSize
140
- entity.pagination.page = 0
141
- },
142
-
143
- /**
144
- * Selects a row.
145
- * @param {TableEntity} entity
146
- * @param {string|number} rowId
147
- */
148
- rowSelect(entity, rowId) {
149
- if (!entity.isMultiSelect) {
150
- entity.selection = []
151
- }
152
-
153
- if (!entity.selection.includes(rowId)) {
154
- entity.selection.push(rowId)
155
- }
156
- },
157
-
158
- /**
159
- * Deselects a row.
160
- * @param {TableEntity} entity
161
- * @param {string|number} rowId
162
- */
163
- rowDeselect(entity, rowId) {
164
- const index = entity.selection.indexOf(rowId)
165
- if (index !== -1) {
166
- entity.selection.splice(index, 1)
167
- }
168
- },
169
-
170
- /**
171
- * Toggles row selection.
172
- * @param {TableEntity} entity
173
- * @param {string|number} rowId
174
- */
175
- rowToggle(entity, rowId) {
176
- const index = entity.selection.indexOf(rowId)
177
-
178
- if (index === -1) {
179
- if (!entity.isMultiSelect) {
180
- entity.selection = [rowId] // Replace entirely
181
- } else {
182
- entity.selection.push(rowId)
183
- }
184
- } else {
185
- entity.selection.splice(index, 1)
186
- }
187
- },
188
-
189
- /**
190
- * Toggles selection of all currently visible rows.
191
- * @param {TableEntity} entity
192
- */
193
- rowsToggleAll(entity) {
194
- const rows = getRows(entity)
195
- const allSelected = rows.every((row) => entity.selection.includes(row.id))
196
-
197
- if (allSelected) {
198
- // Deselect all visible
199
- rows.forEach((row) => {
200
- const index = entity.selection.indexOf(row.id)
201
- if (index !== -1) entity.selection.splice(index, 1)
202
- })
203
- } else {
204
- // Select all visible
205
- rows.forEach((row) => {
206
- if (!entity.selection.includes(row.id)) {
207
- entity.selection.push(row.id)
208
- }
209
- })
210
- }
211
- },
212
-
213
- /**
214
- * Selects all currently visible rows.
215
- * @param {TableEntity} entity
216
- */
217
- rowsSelectAll(entity) {
218
- const rows = getRows(entity)
219
- rows.forEach((row) => {
220
- if (!entity.selection.includes(row.id)) {
221
- entity.selection.push(row.id)
222
- }
223
- })
224
- },
225
-
226
- /**
227
- * Clears all row selections.
228
- * @param {TableEntity} entity
229
- */
230
- selectionClear(entity) {
231
- entity.selection.length = 0
232
- },
233
- }
234
-
235
- // Helper functions
236
-
237
- /**
238
- * Gets the processed rows (filtered, searched, sorted, paginated).
239
- * @param {TableEntity} entity
240
- * @returns {any[]}
241
- */
242
- export function getRows(entity) {
243
- let rows = entity.data
244
- rows = applyFilters(entity, rows)
245
- rows = applySearch(entity, rows)
246
- rows = applySorts(entity, rows)
247
- rows = applyPagination(entity, rows)
248
-
249
- return rows
250
- }
251
-
252
- /**
253
- * Gets the total number of rows after filtering and searching.
254
- * @param {TableEntity} entity
255
- * @returns {number}
256
- */
257
- export function getTotalRows(entity) {
258
- let rows = entity.data
259
- rows = applyFilters(entity, rows)
260
- rows = applySearch(entity, rows)
261
- return rows.length
262
- }
263
-
264
- /**
265
- * Gets pagination information.
266
- * @param {TableEntity} entity
267
- * @returns {object|null}
268
- */
269
- export function getPaginationInfo(entity) {
270
- if (!entity.pagination) return null
271
-
272
- const totalRows = getTotalRows(entity)
273
- const { page, pageSize } = entity.pagination
274
- const totalPages = Math.ceil(totalRows / pageSize)
275
- const start = page * pageSize
276
- const end = Math.min((page + 1) * pageSize, totalRows)
277
-
278
- return {
279
- page,
280
- pageSize,
281
- totalPages,
282
- totalRows,
283
- start,
284
- end,
285
- hasNextPage: page < totalPages - 1,
286
- hasPrevPage: page > 0,
287
- }
288
- }
289
-
290
- /**
291
- * Gets the sort direction for a column.
292
- * @param {TableEntity} entity
293
- * @param {string} columnId
294
- * @returns {"asc"|"desc"|null}
295
- */
296
- export function getSortDirection(entity, columnId) {
297
- const sort = entity.sorts.find((s) => s.column === columnId)
298
- return sort?.direction || null
299
- }
300
-
301
- /**
302
- * Gets the sort index (priority) for a column.
303
- * @param {TableEntity} entity
304
- * @param {string} columnId
305
- * @returns {number}
306
- */
307
- export function getSortIndex(entity, columnId) {
308
- return entity.sorts.findIndex((s) => s.column === columnId)
309
- }
310
-
311
- /**
312
- * Gets the filter value for a column.
313
- * @param {TableEntity} entity
314
- * @param {string} columnId
315
- * @returns {any}
316
- */
317
- export function getFilter(entity, columnId) {
318
- return entity.filters[columnId]
319
- }
320
-
321
- /**
322
- * Checks if a row is selected.
323
- * @param {TableEntity} entity
324
- * @param {string|number} rowId
325
- * @returns {boolean}
326
- */
327
- export function isRowSelected(entity, rowId) {
328
- return entity.selection.includes(rowId)
329
- }
330
-
331
- /**
332
- * Checks if all visible rows are selected.
333
- * @param {TableEntity} entity
334
- * @returns {boolean}
335
- */
336
- export function isAllSelected(entity) {
337
- const rows = getRows(entity)
338
- return rows.length && rows.every((row) => entity.selection.includes(row.id))
339
- }
340
-
341
- /**
342
- * Checks if some (but not all) visible rows are selected.
343
- * @param {TableEntity} entity
344
- * @returns {boolean}
345
- */
346
- export function isSomeSelected(entity) {
347
- const rows = getRows(entity)
348
- const selectedCount = rows.filter((row) =>
349
- entity.selection.includes(row.id),
350
- ).length
351
- return selectedCount && selectedCount < rows.length
352
- }
353
-
354
- // Private helper functions
355
-
356
- /**
357
- * Initializes the table state.
358
- * @param {TableEntity} entity
359
- */
360
- function initTable(entity) {
361
- entity.data ??= []
362
-
363
- // Auto-generate columns from first data item if not provided
364
- if (!entity.columns && entity.data.length) {
365
- const [firstRow] = entity.data
366
-
367
- entity.columns = Object.keys(firstRow).map((key) => {
368
- const value = firstRow[key]
369
- const type = getDefaultColumnType(value)
370
- const filter = getDefaultColumnFilter(type)
371
-
372
- return {
373
- id: key,
374
- title: capitalize(key),
375
- type,
376
- isSortable: false,
377
- isFilterable: false,
378
- filter,
379
- width: getDefaultColumnWidth(filter.type),
380
- }
381
- })
382
- } else {
383
- entity.columns ??= []
384
- entity.columns.forEach((column) => {
385
- column.title ??= capitalize(column.id)
386
- column.type ??= getDefaultColumnType()
387
- column.filter ??= getDefaultColumnFilter(column.type)
388
- column.width ??= getDefaultColumnWidth(column.filter.type)
389
- })
390
- }
391
-
392
- // State
393
- entity.sorts ??= []
394
- entity.filters ??= {}
395
- entity.search ??= null
396
- if (entity.search) {
397
- entity.search.value ??= ""
398
- }
399
- entity.selection ??= []
400
-
401
- entity.pagination ??= null
402
- if (entity.pagination) {
403
- entity.pagination.page ??= 0
404
- }
405
- }
406
-
407
- /**
408
- * Infers the column type from a value.
409
- * @param {any} value
410
- * @returns {string}
411
- */
412
- function getDefaultColumnType(value) {
413
- if (typeof value === "number") return "number"
414
- if (typeof value === "boolean") return "boolean"
415
- if (value instanceof Date) return "date"
416
- return "string"
417
- }
418
-
419
- /**
420
- * Gets the default filter configuration for a column type.
421
- * @param {string} type
422
- * @returns {object}
423
- */
424
- function getDefaultColumnFilter(type) {
425
- if (type === "number") return { type: "range" }
426
- if (type === "boolean")
427
- return { type: "select", options: [null, true, false] }
428
- if (type === "date") return { type: "date" }
429
- return { type: "text" }
430
- }
431
-
432
- /**
433
- * Gets the default column width based on filter type.
434
- * @param {string} filterType
435
- * @returns {number}
436
- */
437
- function getDefaultColumnWidth(filterType) {
438
- if (filterType === "number") return 70
439
- if (filterType === "range") return 100
440
- if (filterType === "select") return 70
441
- if (filterType === "date") return 120
442
- if (filterType === "time") return 120
443
- if (filterType === "datetime") return 170
444
- return 200
445
- }
446
-
447
- /**
448
- * Applies filters to the data.
449
- * @param {TableEntity} entity
450
- * @param {any[]} rows
451
- * @returns {any[]}
452
- */
453
- function applyFilters(entity, rows) {
454
- if (!Object.keys(entity.filters).length) {
455
- return rows
456
- }
457
-
458
- return rows.filter((row) => {
459
- return Object.entries(entity.filters).every(([columnId, filterValue]) => {
460
- const column = entity.columns.find((c) => c.id === columnId)
461
- if (!column) return true
462
-
463
- // Custom filter function
464
- if (column.filterFn) {
465
- return column.filterFn(row, filterValue)
466
- }
467
-
468
- // Default filters by type
469
- const value = row[columnId]
470
-
471
- if (["range", "date", "time", "datetime"].includes(column.filter.type)) {
472
- const { min, max } = filterValue
473
- if (min != null && value < min) return false
474
- if (max != null && value > max) return false
475
- return true
476
- }
477
-
478
- if (["number", "boolean", "select"].includes(column.filter.type)) {
479
- return value === filterValue
480
- }
481
-
482
- // String filtering (case-insensitive contains)
483
- return String(value)
484
- .toLowerCase()
485
- .includes(String(filterValue).toLowerCase())
486
- })
487
- })
488
- }
489
-
490
- /**
491
- * Applies search to the data.
492
- * @param {TableEntity} entity
493
- * @param {any[]} rows
494
- * @returns {any[]}
495
- */
496
- function applySearch(entity, rows) {
497
- if (!entity.search?.value) {
498
- return rows
499
- }
500
-
501
- const searchLower = entity.search.value.toLowerCase()
502
-
503
- return rows.filter((row) =>
504
- entity.columns.some((column) => {
505
- const value = row[column.id]
506
- const formattedValue = column.format?.(value) ?? String(value)
507
- return formattedValue.toLowerCase().includes(searchLower)
508
- }),
509
- )
510
- }
511
-
512
- /**
513
- * Applies sorting to the data.
514
- * @param {TableEntity} entity
515
- * @param {any[]} rows
516
- * @returns {any[]}
517
- */
518
- function applySorts(entity, rows) {
519
- if (!entity.sorts.length) {
520
- return rows
521
- }
522
-
523
- return [...rows].sort((a, b) => {
524
- for (const { column: columnId, direction } of entity.sorts) {
525
- const column = entity.columns.find((c) => c.id === columnId)
526
- let aVal = a[columnId]
527
- let bVal = b[columnId]
528
-
529
- // Custom sort function
530
- if (column?.sortFn) {
531
- const result =
532
- direction === "asc" ? column.sortFn(a, b) : column.sortFn(b, a)
533
- if (result !== 0) return result
534
- continue
535
- }
536
-
537
- // Default sorting
538
- if (aVal === bVal) continue
539
- if (aVal == null) return 1
540
- if (bVal == null) return -1
541
-
542
- const comparison = aVal < bVal ? -1 : 1
543
- return direction === "asc" ? comparison : -comparison
544
- }
545
- return 0
546
- })
547
- }
548
-
549
- /**
550
- * Applies pagination to the data.
551
- * @param {TableEntity} entity
552
- * @param {any[]} rows
553
- * @returns {any[]}
554
- */
555
- function applyPagination(entity, rows) {
556
- if (!entity.pagination) {
557
- return rows
558
- }
559
-
560
- const { page, pageSize } = entity.pagination
561
- const start = page * pageSize
562
- return rows.slice(start, start + pageSize)
563
- }
564
-
565
- /**
566
- * Capitalizes the first letter of a string.
567
- * @param {string} str
568
- * @returns {string}
569
- */
570
- function capitalize(str) {
571
- const [firstChar, ...rest] = str
572
- return [firstChar.toUpperCase(), ...rest].join("")
573
- }