@kikiloaw/simple-table 1.0.0 β 1.0.2
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 +369 -17
- package/package.json +2 -2
- package/src/SimpleTable.vue +109 -49
package/README.md
CHANGED
|
@@ -212,6 +212,14 @@ const columns = [
|
|
|
212
212
|
label: 'Email'
|
|
213
213
|
},
|
|
214
214
|
|
|
215
|
+
// Auto-numbering (row numbers instead of data)
|
|
216
|
+
{
|
|
217
|
+
key: 'id',
|
|
218
|
+
label: '#',
|
|
219
|
+
autonumber: true,
|
|
220
|
+
width: '80px'
|
|
221
|
+
},
|
|
222
|
+
|
|
215
223
|
// Sticky actions column (always visible)
|
|
216
224
|
{
|
|
217
225
|
key: 'actions',
|
|
@@ -233,6 +241,50 @@ Use custom sort keys when:
|
|
|
233
241
|
|
|
234
242
|
## π¨ Features
|
|
235
243
|
|
|
244
|
+
### Auto-Numbering
|
|
245
|
+
|
|
246
|
+
**Display sequential row numbers instead of actual data:**
|
|
247
|
+
|
|
248
|
+
```vue
|
|
249
|
+
const columns = [
|
|
250
|
+
{
|
|
251
|
+
key: 'id',
|
|
252
|
+
label: '#',
|
|
253
|
+
autonumber: true,
|
|
254
|
+
width: '80px'
|
|
255
|
+
},
|
|
256
|
+
// ... other columns
|
|
257
|
+
]
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Features:**
|
|
261
|
+
- β
Displays 1, 2, 3, 4... for each data row
|
|
262
|
+
- β
Skips group headers (only counts data rows)
|
|
263
|
+
- β
Pagination-aware: Page 2 shows 11, 12, 13... (with 10 per page)
|
|
264
|
+
- β
Overrides actual column data
|
|
265
|
+
- β
Works with both server-side and client-side modes
|
|
266
|
+
|
|
267
|
+
**Example:**
|
|
268
|
+
```
|
|
269
|
+
ββββββ¬βββββββββββββββββββ¬ββββββββββ
|
|
270
|
+
β # β Name β Status β
|
|
271
|
+
ββββββΌβββββββββββββββββββΌββββββββββ€
|
|
272
|
+
β ACTIVE USERS β β Header (not counted)
|
|
273
|
+
β 1 β John Doe β Active β
|
|
274
|
+
β 2 β Jane Smith β Active β
|
|
275
|
+
ββββββ΄βββββββββββββββββββ΄ββββββββββ€
|
|
276
|
+
β INACTIVE USERS β β Header (not counted)
|
|
277
|
+
β 3 β Bob Johnson β Inactiveβ
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
**Perfect for:**
|
|
281
|
+
- Sequential numbering regardless of actual IDs
|
|
282
|
+
- User-friendly row references
|
|
283
|
+
- Tables with group headers
|
|
284
|
+
- Paginated lists with continuous numbering
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
236
288
|
### Custom Sort Keys
|
|
237
289
|
|
|
238
290
|
**Problem:** You want to display `department.name` but sort by `department_id`.
|
|
@@ -263,19 +315,22 @@ const columns = [
|
|
|
263
315
|
|
|
264
316
|
**Use Case:** Add filters like status, department, date range, etc.
|
|
265
317
|
|
|
318
|
+
**Performance:** Query parameters are sent with every request but **do NOT auto-refetch**. This prevents unnecessary API calls when you have multiple filters. Call `refresh()` manually when ready.
|
|
319
|
+
|
|
266
320
|
```vue
|
|
267
321
|
<script setup>
|
|
268
322
|
import { ref } from 'vue'
|
|
269
323
|
|
|
324
|
+
const tableRef = ref()
|
|
270
325
|
const filters = ref({
|
|
271
326
|
status: 'active',
|
|
272
327
|
department_id: 5,
|
|
273
328
|
year: 2025
|
|
274
329
|
})
|
|
275
330
|
|
|
276
|
-
function
|
|
277
|
-
filters
|
|
278
|
-
|
|
331
|
+
function applyFilters() {
|
|
332
|
+
// After setting all filters, manually refresh
|
|
333
|
+
tableRef.value?.refresh()
|
|
279
334
|
}
|
|
280
335
|
</script>
|
|
281
336
|
|
|
@@ -291,10 +346,13 @@ function updateStatus(newStatus) {
|
|
|
291
346
|
<option :value="1">IT</option>
|
|
292
347
|
<option :value="5">HR</option>
|
|
293
348
|
</select>
|
|
349
|
+
|
|
350
|
+
<button @click="applyFilters" class="btn">Apply Filters</button>
|
|
294
351
|
</div>
|
|
295
352
|
|
|
296
353
|
<!-- Table with filters -->
|
|
297
354
|
<SimpleTable
|
|
355
|
+
ref="tableRef"
|
|
298
356
|
fetch-url="/api/users"
|
|
299
357
|
:columns="columns"
|
|
300
358
|
:query-params="filters"
|
|
@@ -302,6 +360,8 @@ function updateStatus(newStatus) {
|
|
|
302
360
|
</template>
|
|
303
361
|
```
|
|
304
362
|
|
|
363
|
+
**Important:** Query parameters are **NOT automatically watched**. This prevents multiple API calls when you have many filters. Call `tableRef.value?.refresh()` manually when you want to refetch.
|
|
364
|
+
|
|
305
365
|
**API Request:**
|
|
306
366
|
```
|
|
307
367
|
GET /api/users?page=1&per_page=10&status=active&department_id=5&year=2025
|
|
@@ -379,24 +439,316 @@ function handleCreate() {
|
|
|
379
439
|
|
|
380
440
|
### Custom Actions and Slots
|
|
381
441
|
|
|
382
|
-
**Add custom buttons to the toolbar:**
|
|
442
|
+
**Add custom buttons to the toolbar with access to table data:**
|
|
383
443
|
|
|
384
444
|
```vue
|
|
385
|
-
<
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
445
|
+
<script setup>
|
|
446
|
+
import { Download, Printer, Trash } from 'lucide-vue-next'
|
|
447
|
+
|
|
448
|
+
const handleExport = (type, rows) => {
|
|
449
|
+
console.log(`Exporting ${rows.length} rows as ${type}`)
|
|
450
|
+
|
|
451
|
+
if (type === 'csv') {
|
|
452
|
+
const csv = rows.map(row =>
|
|
453
|
+
`${row.id},${row.name},${row.email}`
|
|
454
|
+
).join('\n')
|
|
455
|
+
|
|
456
|
+
downloadCSV(csv, 'export.csv')
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const handleBulkDelete = (rows) => {
|
|
461
|
+
const ids = rows.map(r => r.id)
|
|
462
|
+
if (confirm(`Delete ${ids.length} items?`)) {
|
|
463
|
+
axios.delete('/api/bulk-delete', { data: { ids } })
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const downloadCSV = (content, filename) => {
|
|
468
|
+
const blob = new Blob([content], { type: 'text/csv' })
|
|
469
|
+
const url = URL.createObjectURL(blob)
|
|
470
|
+
const a = document.createElement('a')
|
|
471
|
+
a.href = url
|
|
472
|
+
a.download = filename
|
|
473
|
+
a.click()
|
|
474
|
+
}
|
|
475
|
+
</script>
|
|
476
|
+
|
|
477
|
+
<template>
|
|
478
|
+
<SimpleTable :columns="columns" fetch-url="/api/users">
|
|
479
|
+
<template #actions="{ rows, columns }">
|
|
480
|
+
<Button @click="handleExport('csv', rows)">
|
|
481
|
+
<Download class="mr-2 h-4 w-4" />
|
|
482
|
+
Export ({{ rows.length }})
|
|
483
|
+
</Button>
|
|
484
|
+
|
|
485
|
+
<Button @click="handleExport('excel', rows)">
|
|
486
|
+
<Download class="mr-2 h-4 w-4" />
|
|
487
|
+
Excel
|
|
488
|
+
</Button>
|
|
489
|
+
|
|
490
|
+
<Button @click="handleBulkDelete(rows)" variant="destructive">
|
|
491
|
+
<Trash class="mr-2 h-4 w-4" />
|
|
492
|
+
Bulk Delete
|
|
493
|
+
</Button>
|
|
494
|
+
</template>
|
|
495
|
+
</SimpleTable>
|
|
496
|
+
</template>
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
**Slot Props:**
|
|
500
|
+
- **`rows`**: Currently visible table data (array of objects)
|
|
501
|
+
- **`columns`**: Column definitions (array of column config)
|
|
502
|
+
|
|
503
|
+
**Use Cases:**
|
|
504
|
+
- β
Custom export buttons (CSV, Excel, PDF)
|
|
505
|
+
- β
Bulk actions (delete, update, approve)
|
|
506
|
+
- β
Print functionality
|
|
507
|
+
- β
Custom filters or search
|
|
508
|
+
- β
Integration with third-party libraries
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
### Group Headers
|
|
514
|
+
|
|
515
|
+
**Organize your table data with full-width group headers:**
|
|
516
|
+
|
|
517
|
+
```vue
|
|
518
|
+
<script setup>
|
|
519
|
+
const addGroupHeaders = (rows) => {
|
|
520
|
+
// Sort by category first
|
|
521
|
+
const sorted = [...rows].sort((a, b) =>
|
|
522
|
+
(a.category || '').localeCompare(b.category || '')
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
const result = []
|
|
526
|
+
let currentCategory = null
|
|
527
|
+
|
|
528
|
+
sorted.forEach(row => {
|
|
529
|
+
const category = row.category || 'Uncategorized'
|
|
530
|
+
|
|
531
|
+
// When category changes, add a header row
|
|
532
|
+
if (category !== currentCategory) {
|
|
533
|
+
result.push({
|
|
534
|
+
_isGroupHeader: true, // Special flag
|
|
535
|
+
_groupTitle: category, // Header text
|
|
536
|
+
// Add empty values for all columns
|
|
537
|
+
...Object.fromEntries(
|
|
538
|
+
Object.keys(row).map(key => [key, ''])
|
|
539
|
+
)
|
|
540
|
+
})
|
|
541
|
+
currentCategory = category
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
result.push(row)
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
return result
|
|
548
|
+
}
|
|
549
|
+
</script>
|
|
550
|
+
|
|
551
|
+
<template>
|
|
552
|
+
<SimpleTable
|
|
553
|
+
:columns="columns"
|
|
554
|
+
:before-render="addGroupHeaders"
|
|
555
|
+
odd-row-color="bg-gray-50"
|
|
556
|
+
even-row-color="bg-white"
|
|
557
|
+
fetch-url="/api/data"
|
|
558
|
+
/>
|
|
559
|
+
</template>
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
**Result:**
|
|
563
|
+
```
|
|
564
|
+
ββββββββββββββββββββββββββββββββββββββββββ
|
|
565
|
+
β CATEGORY A β β Full-width header
|
|
566
|
+
βββββββ¬βββββββββββ¬βββββββββββ¬βββββββββββββ€
|
|
567
|
+
β 1 β Item 1 β $100 β Active β
|
|
568
|
+
β 2 β Item 2 β $200 β Active β
|
|
569
|
+
βββββββ΄βββββββββββ΄βββββββββββ΄βββββββββββββ€
|
|
570
|
+
β CATEGORY B β β Full-width header
|
|
571
|
+
βββββββ¬βββββββββββ¬βββββββββββ¬βββββββββββββ€
|
|
572
|
+
β 3 β Item 3 β $150 β Inactive β
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
**How It Works:**
|
|
576
|
+
1. Use `beforeRender` callback to transform data
|
|
577
|
+
2. Insert rows with `_isGroupHeader: true` flag
|
|
578
|
+
3. Component renders these as full-width cells with `colspan`
|
|
579
|
+
4. Striping continues correctly across all rows
|
|
580
|
+
|
|
581
|
+
**Grouping Examples:**
|
|
582
|
+
|
|
583
|
+
```vue
|
|
584
|
+
// Group by first letter
|
|
585
|
+
const groupByLetter = (rows) => {
|
|
586
|
+
const sorted = [...rows].sort((a, b) =>
|
|
587
|
+
a.name.localeCompare(b.name)
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
const result = []
|
|
591
|
+
let currentLetter = null
|
|
592
|
+
|
|
593
|
+
sorted.forEach(row => {
|
|
594
|
+
const letter = row.name.charAt(0).toUpperCase()
|
|
595
|
+
|
|
596
|
+
if (letter !== currentLetter) {
|
|
597
|
+
result.push({
|
|
598
|
+
_isGroupHeader: true,
|
|
599
|
+
_groupTitle: letter,
|
|
600
|
+
})
|
|
601
|
+
currentLetter = letter
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
result.push(row)
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
return result
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// Group by date range
|
|
611
|
+
const groupByDate = (rows) => {
|
|
612
|
+
const sorted = [...rows].sort((a, b) =>
|
|
613
|
+
new Date(b.created_at) - new Date(a.created_at)
|
|
614
|
+
)
|
|
615
|
+
|
|
616
|
+
const result = []
|
|
617
|
+
let currentMonth = null
|
|
618
|
+
|
|
619
|
+
sorted.forEach(row => {
|
|
620
|
+
const month = new Date(row.created_at).toLocaleDateString('en', {
|
|
621
|
+
year: 'numeric',
|
|
622
|
+
month: 'long'
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
if (month !== currentMonth) {
|
|
626
|
+
result.push({
|
|
627
|
+
_isGroupHeader: true,
|
|
628
|
+
_groupTitle: month,
|
|
629
|
+
})
|
|
630
|
+
currentMonth = month
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
result.push(row)
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
return result
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Group by status
|
|
640
|
+
const groupByStatus = (rows) => {
|
|
641
|
+
const active = rows.filter(r => r.is_active)
|
|
642
|
+
const inactive = rows.filter(r => !r.is_active)
|
|
643
|
+
|
|
644
|
+
return [
|
|
645
|
+
{ _isGroupHeader: true, _groupTitle: 'Active Items' },
|
|
646
|
+
...active,
|
|
647
|
+
{ _isGroupHeader: true, _groupTitle: 'Inactive Items' },
|
|
648
|
+
...inactive
|
|
649
|
+
]
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
**Styling Group Headers:**
|
|
654
|
+
|
|
655
|
+
The component applies these classes to group header rows:
|
|
656
|
+
- `border-b border-gray-200` - Bottom border
|
|
657
|
+
- Font styling via the inner div
|
|
658
|
+
- Row striping colors (odd-row-color / even-row-color)
|
|
659
|
+
|
|
660
|
+
You can customize by overriding row colors:
|
|
661
|
+
```vue
|
|
662
|
+
<SimpleTable
|
|
663
|
+
:before-render="addGroupHeaders"
|
|
664
|
+
odd-row-color="bg-blue-50"
|
|
665
|
+
even-row-color="bg-white"
|
|
666
|
+
/>
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
### Data Transformation (beforeRender)
|
|
672
|
+
|
|
673
|
+
**Transform data before it's rendered in the table:**
|
|
674
|
+
|
|
675
|
+
```vue
|
|
676
|
+
<script setup>
|
|
677
|
+
import dayjs from 'dayjs'
|
|
678
|
+
|
|
679
|
+
const transformData = (rows) => {
|
|
680
|
+
return rows.map(row => ({
|
|
681
|
+
...row,
|
|
682
|
+
// Add computed properties
|
|
683
|
+
full_name: `${row.first_name} ${row.last_name}`,
|
|
684
|
+
|
|
685
|
+
// Format dates
|
|
686
|
+
created_at_formatted: dayjs(row.created_at).format('MMM D, YYYY'),
|
|
687
|
+
|
|
688
|
+
// Add status badge classes
|
|
689
|
+
status_class: row.status === 'active' ? 'text-green-600' : 'text-red-600',
|
|
690
|
+
|
|
691
|
+
// Transform arrays
|
|
692
|
+
tags_joined: row.tags?.join(', ') || 'No tags',
|
|
693
|
+
|
|
694
|
+
// Add custom logic
|
|
695
|
+
is_urgent: row.priority > 8,
|
|
696
|
+
days_since_created: dayjs().diff(dayjs(row.created_at), 'days')
|
|
697
|
+
}))
|
|
698
|
+
}
|
|
699
|
+
</script>
|
|
700
|
+
|
|
701
|
+
<template>
|
|
702
|
+
<SimpleTable
|
|
703
|
+
:columns="columns"
|
|
704
|
+
fetch-url="/api/users"
|
|
705
|
+
:before-render="transformData"
|
|
706
|
+
/>
|
|
707
|
+
</template>
|
|
708
|
+
```
|
|
709
|
+
|
|
710
|
+
**When to Use:**
|
|
711
|
+
- β
Format dates, numbers, or currencies
|
|
712
|
+
- β
Combine multiple fields into one
|
|
713
|
+
- β
Add computed properties
|
|
714
|
+
- β
Transform nested objects to flat properties
|
|
715
|
+
- β
Add CSS classes based on data
|
|
716
|
+
- β
Filter unwanted rows (return modified array)
|
|
717
|
+
|
|
718
|
+
**Example - Adding Full Names:**
|
|
719
|
+
```vue
|
|
720
|
+
<script setup>
|
|
721
|
+
const columns = [
|
|
722
|
+
{ key: 'full_name', label: 'Name' }, // Not in API response
|
|
723
|
+
{ key: 'email', label: 'Email' },
|
|
724
|
+
{ key: 'created_at_formatted', label: 'Joined' }
|
|
725
|
+
]
|
|
726
|
+
|
|
727
|
+
const beforeRender = (rows) => {
|
|
728
|
+
return rows.map(row => ({
|
|
729
|
+
...row,
|
|
730
|
+
full_name: `${row.first_name} ${row.last_name}`,
|
|
731
|
+
created_at_formatted: new Date(row.created_at).toLocaleDateString()
|
|
732
|
+
}))
|
|
733
|
+
}
|
|
734
|
+
</script>
|
|
735
|
+
|
|
736
|
+
<template>
|
|
737
|
+
<SimpleTable
|
|
738
|
+
:columns="columns"
|
|
739
|
+
:before-render="beforeRender"
|
|
740
|
+
fetch-url="/api/users"
|
|
741
|
+
/>
|
|
742
|
+
</template>
|
|
395
743
|
```
|
|
396
744
|
|
|
397
|
-
**
|
|
398
|
-
|
|
399
|
-
|
|
745
|
+
**Execution Order:**
|
|
746
|
+
1. Data fetched from API
|
|
747
|
+
2. Filtering & Searching (client-side only)
|
|
748
|
+
3. Sorting (client-side only)
|
|
749
|
+
4. Pagination (client-side only)
|
|
750
|
+
5. **`beforeRender` called** β Your transformation here
|
|
751
|
+
6. Rows rendered in table
|
|
400
752
|
|
|
401
753
|
---
|
|
402
754
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kikiloaw/simple-table",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "A lightweight, dependency-light DataTable component for Vue 3 with Tailwind CSS",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"module": "src/index.js",
|
|
@@ -29,4 +29,4 @@
|
|
|
29
29
|
"type": "git",
|
|
30
30
|
"url": "git+https://github.com/kikiloaw/simple-table.git"
|
|
31
31
|
}
|
|
32
|
-
}
|
|
32
|
+
}
|
package/src/SimpleTable.vue
CHANGED
|
@@ -26,6 +26,7 @@ interface Props {
|
|
|
26
26
|
class?: string
|
|
27
27
|
fixed?: boolean // 'left' or 'right' could be added later, assuming 'right' for actions usually
|
|
28
28
|
width?: string
|
|
29
|
+
autonumber?: boolean // If true, display auto-incremented row numbers instead of data
|
|
29
30
|
}[]
|
|
30
31
|
mode?: 'auto' | 'server' | 'client'
|
|
31
32
|
protocol?: 'laravel' | 'datatables' // API request/response format
|
|
@@ -34,6 +35,9 @@ interface Props {
|
|
|
34
35
|
pageSizes?: any[] // number[] or { label: string, value: number }[]
|
|
35
36
|
fetchUrl?: string
|
|
36
37
|
|
|
38
|
+
// Callbacks
|
|
39
|
+
beforeRender?: (rows: any[]) => any[] // Transform data before rendering
|
|
40
|
+
|
|
37
41
|
// Cache Props
|
|
38
42
|
enableCache?: boolean // If true, cache responses by page/search/sort to avoid redundant requests
|
|
39
43
|
|
|
@@ -48,6 +52,7 @@ interface Props {
|
|
|
48
52
|
|
|
49
53
|
const props = withDefaults(defineProps<Props>(), {
|
|
50
54
|
data: () => [],
|
|
55
|
+
columns: () => [],
|
|
51
56
|
mode: 'auto',
|
|
52
57
|
protocol: 'laravel',
|
|
53
58
|
searchable: true,
|
|
@@ -164,14 +169,6 @@ watch(() => props.data, (newVal) => {
|
|
|
164
169
|
internalData.value = newVal
|
|
165
170
|
}, { deep: true })
|
|
166
171
|
|
|
167
|
-
// Watch queryParams and refetch when they change
|
|
168
|
-
watch(() => props.queryParams, () => {
|
|
169
|
-
if (isServerSide.value) {
|
|
170
|
-
currentPage.value = 1 // Reset to first page when filters change
|
|
171
|
-
fetchData()
|
|
172
|
-
}
|
|
173
|
-
}, { deep: true })
|
|
174
|
-
|
|
175
172
|
// -- Computed: Mode Detection --
|
|
176
173
|
const isServerSide = computed(() => {
|
|
177
174
|
if (props.mode === 'server') return true
|
|
@@ -196,6 +193,7 @@ const serverMeta = computed(() => {
|
|
|
196
193
|
return {
|
|
197
194
|
current_page: meta.current_page ?? 1,
|
|
198
195
|
last_page: meta.last_page ?? 1,
|
|
196
|
+
per_page: meta.per_page ?? currentPerPage.value,
|
|
199
197
|
from: meta.from ?? 0,
|
|
200
198
|
to: meta.to ?? 0,
|
|
201
199
|
total: meta.total ?? 0,
|
|
@@ -205,39 +203,52 @@ const serverMeta = computed(() => {
|
|
|
205
203
|
|
|
206
204
|
// -- Computed: Data Normalization --
|
|
207
205
|
const tableData = computed(() => {
|
|
206
|
+
let result: any[] = []
|
|
207
|
+
|
|
208
208
|
if (isServerSide.value) {
|
|
209
209
|
const d = internalData.value as any
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
let items = [...(internalData.value as any[])]
|
|
210
|
+
result = d.data || []
|
|
211
|
+
} else {
|
|
212
|
+
// Client Side Processing
|
|
213
|
+
let items = [...(internalData.value as any[])]
|
|
215
214
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
215
|
+
// 1. Filter
|
|
216
|
+
if (searchQuery.value) {
|
|
217
|
+
const lowerQuery = searchQuery.value.toLowerCase()
|
|
218
|
+
items = items.filter((item) =>
|
|
219
|
+
Object.values(item).some((val) =>
|
|
220
|
+
String(val).toLowerCase().includes(lowerQuery)
|
|
221
|
+
)
|
|
222
222
|
)
|
|
223
|
-
|
|
224
|
-
}
|
|
223
|
+
}
|
|
225
224
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
225
|
+
// 2. Sort
|
|
226
|
+
if (sortColumn.value) {
|
|
227
|
+
items.sort((a, b) => {
|
|
228
|
+
const valA = a[sortColumn.value]
|
|
229
|
+
const valB = b[sortColumn.value]
|
|
230
|
+
if (valA === valB) return 0
|
|
231
|
+
const comparison = valA > valB ? 1 : -1
|
|
232
|
+
return sortDirection.value === 'asc' ? comparison : -comparison
|
|
233
|
+
})
|
|
234
|
+
}
|
|
236
235
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
236
|
+
// 3. Paginate
|
|
237
|
+
const start = (currentPage.value - 1) * currentPerPage.value
|
|
238
|
+
const end = start + currentPerPage.value
|
|
239
|
+
result = items.slice(start, end)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Apply beforeRender callback if provided
|
|
243
|
+
if (props.beforeRender && typeof props.beforeRender === 'function') {
|
|
244
|
+
const transformed = props.beforeRender(result)
|
|
245
|
+
// Ensure callback returns an array
|
|
246
|
+
if (Array.isArray(transformed)) {
|
|
247
|
+
result = transformed
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return result
|
|
241
252
|
})
|
|
242
253
|
|
|
243
254
|
const totalPages = computed(() => {
|
|
@@ -539,6 +550,38 @@ function handlePageSizeChange(size: any) {
|
|
|
539
550
|
fetchData()
|
|
540
551
|
}
|
|
541
552
|
|
|
553
|
+
// Get row class with simple alternating stripes (all rows)
|
|
554
|
+
function getRowClass(row: any, idx: number) {
|
|
555
|
+
// Alternate: index 0 = white, index 1 = gray, index 2 = white, etc.
|
|
556
|
+
const isOdd = idx % 2 === 0 // Changed: even index = odd color (white)
|
|
557
|
+
return [
|
|
558
|
+
{ [props.oddRowColor]: isOdd, [props.evenRowColor]: !isOdd },
|
|
559
|
+
row._isGroupHeader ? '' : props.hoverColor // No hover on headers
|
|
560
|
+
]
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Get row number for auto-numbering (excluding group headers)
|
|
564
|
+
function getRowNumber(idx: number): number {
|
|
565
|
+
// Count only data rows before this index
|
|
566
|
+
let dataRowCount = 0
|
|
567
|
+
for (let i = 0; i <= idx; i++) {
|
|
568
|
+
if (!tableData.value[i]?._isGroupHeader) {
|
|
569
|
+
dataRowCount++
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// Add offset for pagination
|
|
574
|
+
if (isServerSide.value) {
|
|
575
|
+
const currentPage = serverMeta.value?.current_page || 1
|
|
576
|
+
const perPage = serverMeta.value?.per_page || currentPerPage.value
|
|
577
|
+
const offset = (currentPage - 1) * perPage
|
|
578
|
+
return offset + dataRowCount
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
// Client-side: just return the count
|
|
582
|
+
return dataRowCount
|
|
583
|
+
}
|
|
584
|
+
|
|
542
585
|
function handlePageChange(page: number) {
|
|
543
586
|
if (page < 1 || page > totalPages.value) return
|
|
544
587
|
|
|
@@ -713,21 +756,38 @@ function getCellStyle(col: any) {
|
|
|
713
756
|
v-for="(row, idx) in tableData"
|
|
714
757
|
:key="idx"
|
|
715
758
|
class="group"
|
|
716
|
-
:class="
|
|
759
|
+
:class="getRowClass(row, idx)"
|
|
717
760
|
>
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
:
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
761
|
+
<!-- Group Header Row: Single cell spanning all columns -->
|
|
762
|
+
<template v-if="row._isGroupHeader">
|
|
763
|
+
<TableCell :colspan="columns.length" class="border-b border-gray-200">
|
|
764
|
+
<div class="px-2 py-2 font-semibold text-gray-700 text-sm uppercase tracking-wide">
|
|
765
|
+
{{ row._groupTitle }}
|
|
766
|
+
</div>
|
|
767
|
+
</TableCell>
|
|
768
|
+
</template>
|
|
769
|
+
|
|
770
|
+
<!-- Regular Data Row: Individual cells -->
|
|
771
|
+
<template v-else>
|
|
772
|
+
<TableCell
|
|
773
|
+
v-for="(col, cIdx) in columns"
|
|
774
|
+
:key="col.key"
|
|
775
|
+
:class="getCellClass(col, cIdx, columns.length, idx)"
|
|
776
|
+
:style="getCellStyle(col)"
|
|
777
|
+
>
|
|
778
|
+
<!-- Auto-numbering or custom cell rendering -->
|
|
779
|
+
<div>
|
|
780
|
+
<template v-if="col.autonumber">
|
|
781
|
+
{{ getRowNumber(idx) }}
|
|
782
|
+
</template>
|
|
783
|
+
<template v-else>
|
|
784
|
+
<slot :name="`cell-${col.key}`" :row="row">
|
|
785
|
+
{{ row[col.key] }}
|
|
786
|
+
</slot>
|
|
787
|
+
</template>
|
|
788
|
+
</div>
|
|
789
|
+
</TableCell>
|
|
790
|
+
</template>
|
|
731
791
|
</TableRow>
|
|
732
792
|
</template>
|
|
733
793
|
<TableRow v-else>
|