@kikiloaw/simple-table 1.0.6 → 1.1.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/README.md
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
[](https://vuejs.org/)
|
|
7
7
|
[](https://www.typescriptlang.org/)
|
|
8
8
|
|
|
9
|
+
> **🚀 Explore the Demo**: Check out the [SimpleTable Demo Repository](https://github.com/kikiloaw/simple-table-demo.git) for a complete documentation site with live server-side examples, seeding, and source code.
|
|
10
|
+
|
|
9
11
|
---
|
|
10
12
|
|
|
11
13
|
## ✨ Why SimpleTable?
|
|
@@ -476,7 +478,8 @@ SimpleTable supports three data modes:
|
|
|
476
478
|
sortable: true, // Optional: Enable sorting
|
|
477
479
|
width: '200px', // Optional: Fixed column width
|
|
478
480
|
fixed: true, // Optional: Sticky column (left for first, right for last, others left). **Requires `width` to be set.**
|
|
479
|
-
|
|
481
|
+
align: 'right', // Optional: Text alignment ('left', 'center', 'right'). Default: 'left'
|
|
482
|
+
class: 'text-red-500' // Optional: Additional CSS classes
|
|
480
483
|
}
|
|
481
484
|
```
|
|
482
485
|
|
|
@@ -1400,6 +1403,28 @@ table.value?.refresh()
|
|
|
1400
1403
|
| `@update:sort` | `{ column, direction }` | Emitted when sort changes |
|
|
1401
1404
|
| `@page-change` | `number` | Emitted when page changes |
|
|
1402
1405
|
| `@export` | `{ format, data }` | Emitted when export is triggered |
|
|
1406
|
+
| `@fetched` | `Object` (Response) | Emitted when data is successfully fetched from API. Contains raw response. |
|
|
1407
|
+
|
|
1408
|
+
---
|
|
1409
|
+
|
|
1410
|
+
## 🔧 Exposed Methods
|
|
1411
|
+
|
|
1412
|
+
Access these methods via template ref:
|
|
1413
|
+
|
|
1414
|
+
```vue
|
|
1415
|
+
<script setup>
|
|
1416
|
+
const tableRef = ref()
|
|
1417
|
+
|
|
1418
|
+
// ...
|
|
1419
|
+
tableRef.value?.refresh()
|
|
1420
|
+
</script>
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
| Method | Parameters | Description |
|
|
1424
|
+
|--------|------------|-------------|
|
|
1425
|
+
| `refresh()` | None | Resets page to 1 and refetches data |
|
|
1426
|
+
| `fetchData(params)` | `params: Object` | Fetch data with specific custom parameters |
|
|
1427
|
+
| `clearCache()` | None | Clears the client-side response cache |
|
|
1403
1428
|
|
|
1404
1429
|
---
|
|
1405
1430
|
|
package/package.json
CHANGED
package/src/SimpleTable.vue
CHANGED
|
@@ -63,10 +63,12 @@ const props = withDefaults(defineProps<Props>(), {
|
|
|
63
63
|
pageSizes: () => [10, 20, 30, 50, 100],
|
|
64
64
|
rowHeight: 38,
|
|
65
65
|
oddRowColor: 'bg-white',
|
|
66
|
-
evenRowColor: 'bg-
|
|
67
|
-
hoverColor: 'hover:bg-
|
|
66
|
+
evenRowColor: 'bg-gray-50',
|
|
67
|
+
hoverColor: 'hover:bg-gray-100'
|
|
68
68
|
})
|
|
69
69
|
|
|
70
|
+
|
|
71
|
+
|
|
70
72
|
// ...
|
|
71
73
|
|
|
72
74
|
// -- Computed: Page Sizes Normalization --
|
|
@@ -163,7 +165,7 @@ const densityConfig = computed(() => {
|
|
|
163
165
|
</div>
|
|
164
166
|
*/
|
|
165
167
|
|
|
166
|
-
const emit = defineEmits(['update:search', 'update:sort', 'page-change'])
|
|
168
|
+
const emit = defineEmits(['update:search', 'update:sort', 'page-change', 'fetched'])
|
|
167
169
|
|
|
168
170
|
// -- State --
|
|
169
171
|
const searchQuery = ref('')
|
|
@@ -464,6 +466,7 @@ async function fetchData(params: any = {}) {
|
|
|
464
466
|
}
|
|
465
467
|
|
|
466
468
|
internalData.value = data
|
|
469
|
+
emit('fetched', data)
|
|
467
470
|
|
|
468
471
|
// Store in cache if enabled
|
|
469
472
|
if (props.enableCache) {
|
|
@@ -583,6 +586,13 @@ function handlePageSizeChange(size: any) {
|
|
|
583
586
|
fetchData()
|
|
584
587
|
}
|
|
585
588
|
|
|
589
|
+
// Helper for header content alignment (justify for flex)
|
|
590
|
+
function getHeaderJustifyClass(col: any) {
|
|
591
|
+
if (col.align === 'center') return 'justify-center'
|
|
592
|
+
if (col.align === 'right') return 'justify-end'
|
|
593
|
+
return 'justify-start'
|
|
594
|
+
}
|
|
595
|
+
|
|
586
596
|
// Get row class with simple alternating stripes (all rows)
|
|
587
597
|
function getRowClass(row: any, idx: number) {
|
|
588
598
|
// Alternate: index 0 = white, index 1 = gray, index 2 = white, etc.
|
|
@@ -640,71 +650,152 @@ onMounted(() => {
|
|
|
640
650
|
|
|
641
651
|
defineExpose({
|
|
642
652
|
refresh,
|
|
643
|
-
fetchData,
|
|
644
|
-
clearCache
|
|
653
|
+
fetchData,
|
|
654
|
+
clearCache
|
|
645
655
|
})
|
|
646
656
|
|
|
657
|
+
|
|
658
|
+
// -- Helper Functions --
|
|
659
|
+
|
|
660
|
+
function isColFixed(col: any) {
|
|
661
|
+
return !!col.fixed
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
// Calculate left offset dynamically
|
|
665
|
+
function getStickyLeftOffset(index: number) {
|
|
666
|
+
let offset = 0
|
|
667
|
+
for (let i = 0; i < index; i++) {
|
|
668
|
+
const col = props.columns[i]
|
|
669
|
+
// Only consider left-fixed columns
|
|
670
|
+
const isRightFixed = (col.fixed && i === props.columns.length - 1)
|
|
671
|
+
|
|
672
|
+
if (col.fixed && !isRightFixed) {
|
|
673
|
+
let w = 100 // Default width
|
|
674
|
+
if (col.width) {
|
|
675
|
+
if (typeof col.width === 'string') {
|
|
676
|
+
// Extract number from "80px", "100", etc.
|
|
677
|
+
const match = col.width.match(/^(\d+(\.\d+)?)/)
|
|
678
|
+
if (match) w = parseFloat(match[1])
|
|
679
|
+
} else if (typeof col.width === 'number') {
|
|
680
|
+
w = col.width
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
offset += w
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
return offset
|
|
687
|
+
}
|
|
688
|
+
|
|
647
689
|
// -- Helper Styles --
|
|
648
690
|
function getCellClass(col: any, index: number, totalCols: number, rowIndex: number = -1) {
|
|
649
|
-
let classes =
|
|
691
|
+
let classes = ''
|
|
650
692
|
|
|
651
693
|
// Base classes
|
|
652
|
-
|
|
694
|
+
|
|
695
|
+
// Add text alignment (default: left)
|
|
696
|
+
const alignClass = col.align === 'center' ? ' text-center' : col.align === 'right' ? ' text-right' : ' text-left'
|
|
697
|
+
classes += alignClass
|
|
653
698
|
|
|
654
699
|
if (col.fixed) {
|
|
655
700
|
// Sticky logic
|
|
656
|
-
let stickyClass = ' whitespace-nowrap'
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
701
|
+
let stickyClass = ' whitespace-nowrap'
|
|
702
|
+
|
|
703
|
+
// Check if this is the LAST left-fixed column
|
|
704
|
+
// It is the last left-fixed if:
|
|
705
|
+
// 1. It is NOT the last column (which is right-fixed)
|
|
706
|
+
// 2. The NEXT column is NOT fixed OR is the last column (right-fixed)
|
|
707
|
+
const isRightFixed = index === totalCols - 1
|
|
708
|
+
|
|
709
|
+
if (isRightFixed) {
|
|
710
|
+
// Last Column -> Right Sticky (shadow on LEFT side)
|
|
711
|
+
stickyClass = ' sticky right-0 z-50 fixed-column-boundary-left'
|
|
660
712
|
} else {
|
|
661
|
-
//
|
|
662
|
-
|
|
713
|
+
// Left Sticky - must have high z-index to stay on top of scrolling content
|
|
714
|
+
// Apply consistent shadow and border to all left-fixed columns (shadow on RIGHT side)
|
|
715
|
+
stickyClass = ' sticky z-50'
|
|
663
716
|
}
|
|
664
717
|
|
|
665
718
|
// Determine background
|
|
666
|
-
// Sticky cells need opaque bg.
|
|
667
|
-
let bgClass = '
|
|
719
|
+
// Sticky cells need opaque bg.
|
|
720
|
+
let bgClass = ''
|
|
668
721
|
|
|
669
722
|
if (rowIndex !== -1) {
|
|
670
723
|
// Body Row
|
|
671
724
|
const isOdd = rowIndex % 2 === 0
|
|
672
|
-
|
|
725
|
+
// Use custom class if provided, otherwise use row colors
|
|
726
|
+
if (col.class) {
|
|
727
|
+
bgClass = col.class
|
|
728
|
+
} else {
|
|
729
|
+
bgClass = isOdd ? (props.oddRowColor || 'bg-white') : (props.evenRowColor || 'bg-gray-50')
|
|
730
|
+
}
|
|
673
731
|
|
|
674
732
|
// Should also match hover
|
|
675
|
-
// If the row has a hover class (like hover:bg-muted), the sticky cell needs group-hover:bg-muted to match.
|
|
676
|
-
// We assume hoverColor is passed as 'hover:bg-...'
|
|
677
|
-
// We try to convert 'hover:bg-...' to 'group-hover:bg-...'
|
|
678
733
|
if (props.hoverColor) {
|
|
679
734
|
const hoverParts = props.hoverColor.split(':')
|
|
680
735
|
if (hoverParts.length > 1) {
|
|
681
|
-
// e.g. ['hover', 'bg-blue-100'] -> 'group-hover:bg-blue-100'
|
|
682
736
|
bgClass += ` group-hover:${hoverParts[1]}`
|
|
683
|
-
// Also handle things like 'hover:bg-muted/50'
|
|
684
737
|
if (hoverParts.length > 2) {
|
|
685
738
|
bgClass = bgClass + ':' + hoverParts.slice(2).join(':')
|
|
686
739
|
}
|
|
687
740
|
}
|
|
688
741
|
}
|
|
689
742
|
} else {
|
|
690
|
-
// Header Row
|
|
691
|
-
bgClass = 'bg-
|
|
743
|
+
// Header Row - use custom class if provided, otherwise default to white
|
|
744
|
+
bgClass = col.class || 'bg-white' // Must be opaque
|
|
692
745
|
}
|
|
693
746
|
|
|
694
|
-
|
|
747
|
+
// Check if this is the last left-fixed column (boundary)
|
|
748
|
+
const nextCol = props.columns[index + 1]
|
|
749
|
+
const isLastLeftFixed = nextCol && !nextCol.fixed
|
|
750
|
+
|
|
751
|
+
if (isLastLeftFixed) {
|
|
752
|
+
classes += ' fixed-column-boundary-right !pr-6'
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
classes += stickyClass + ' ' + bgClass + ' !bg-opacity-100'
|
|
756
|
+
} else {
|
|
757
|
+
// Non-fixed column - just add custom class if provided
|
|
758
|
+
if (col.class) {
|
|
759
|
+
classes += ' ' + col.class
|
|
760
|
+
}
|
|
695
761
|
}
|
|
696
762
|
return classes
|
|
697
763
|
}
|
|
698
764
|
|
|
699
|
-
function getCellStyle(col: any) {
|
|
765
|
+
function getCellStyle(col: any, index: number, totalCols: number) {
|
|
766
|
+
const style: any = {}
|
|
767
|
+
|
|
700
768
|
if (col.width) {
|
|
701
|
-
|
|
769
|
+
style.width = col.width
|
|
770
|
+
style.minWidth = col.width
|
|
771
|
+
style.maxWidth = col.width
|
|
772
|
+
} else if (col.fixed) {
|
|
773
|
+
style.width = '100px' // Default fixed width if not processing
|
|
774
|
+
style.minWidth = '100px'
|
|
702
775
|
}
|
|
703
|
-
|
|
776
|
+
|
|
704
777
|
if (col.fixed) {
|
|
705
|
-
|
|
778
|
+
// Handle Left vs Right
|
|
779
|
+
if (index !== totalCols - 1) {
|
|
780
|
+
// Left Sticky: Calculate directly
|
|
781
|
+
const left = getStickyLeftOffset(index)
|
|
782
|
+
style.left = `${left}px`
|
|
783
|
+
|
|
784
|
+
// Only add separator to the LAST left-fixed column (the boundary)
|
|
785
|
+
const nextCol = props.columns[index + 1]
|
|
786
|
+
const isLastLeftFixed = nextCol && !nextCol.fixed
|
|
787
|
+
|
|
788
|
+
if (isLastLeftFixed) {
|
|
789
|
+
// Add position relative so the ::after pseudo-element works
|
|
790
|
+
style.position = 'sticky' // Already sticky, but make it explicit
|
|
791
|
+
}
|
|
792
|
+
} else {
|
|
793
|
+
// Right sticky - will use CSS class for border
|
|
794
|
+
}
|
|
795
|
+
// Right sticky is handled by CSS class right-0
|
|
706
796
|
}
|
|
707
|
-
|
|
797
|
+
|
|
798
|
+
return style
|
|
708
799
|
}
|
|
709
800
|
|
|
710
801
|
</script>
|
|
@@ -753,33 +844,35 @@ function getCellStyle(col: any) {
|
|
|
753
844
|
</div>
|
|
754
845
|
|
|
755
846
|
<!-- Table -->
|
|
756
|
-
<div class="border bg-background
|
|
757
|
-
|
|
758
|
-
|
|
847
|
+
<div class="border bg-background relative">
|
|
848
|
+
<div class="overflow-x-auto">
|
|
849
|
+
<!-- We add min-w-full to Table to ensure it stretches -->
|
|
850
|
+
<Table class="min-w-full table-auto">
|
|
759
851
|
<TableHeader>
|
|
760
852
|
<TableRow :style="{ height: densityConfig.cellHeight }">
|
|
761
853
|
<TableHead
|
|
762
854
|
v-for="(col, idx) in columns"
|
|
763
855
|
:key="col.key"
|
|
764
856
|
:class="getCellClass(col, idx, columns.length)"
|
|
765
|
-
:style="getCellStyle(col)"
|
|
857
|
+
:style="getCellStyle(col, idx, columns.length)"
|
|
766
858
|
:height="densityConfig.headerHeight"
|
|
767
859
|
:padding="densityConfig.headerPadding"
|
|
768
860
|
>
|
|
769
861
|
<div
|
|
770
862
|
v-if="col.sortable"
|
|
771
|
-
class="flex items-center space-x-2 cursor-pointer select-none hover:text-foreground"
|
|
863
|
+
class="flex items-center space-x-2 cursor-pointer select-none hover:text-foreground w-full"
|
|
864
|
+
:class="getHeaderJustifyClass(col)"
|
|
772
865
|
@click="handleSort(col)"
|
|
773
866
|
>
|
|
774
867
|
<div>{{ col.label }}</div>
|
|
775
868
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4 opacity-50 flex-shrink-0"><path d="m7 15 5 5 5-5"/><path d="m7 9 5-5 5 5"/></svg>
|
|
776
869
|
</div>
|
|
777
|
-
<div v-else>{{ col.label }}</div>
|
|
870
|
+
<div v-else class="w-full flex" :class="getHeaderJustifyClass(col)">{{ col.label }}</div>
|
|
778
871
|
</TableHead>
|
|
779
872
|
</TableRow>
|
|
780
873
|
</TableHeader>
|
|
781
874
|
<TableBody>
|
|
782
|
-
<template v-if="isLoading">
|
|
875
|
+
<template v-if="isLoading && tableData.length === 0">
|
|
783
876
|
<TableRow>
|
|
784
877
|
<TableCell :colspan="columns.length" class="h-24 text-center">
|
|
785
878
|
<div class="flex items-center justify-center">
|
|
@@ -799,7 +892,12 @@ function getCellStyle(col: any) {
|
|
|
799
892
|
<!-- Group Header Row: Single cell spanning all columns -->
|
|
800
893
|
<template v-if="row._isGroupHeader">
|
|
801
894
|
<TableCell :colspan="columns.length" class="border-b border-gray-200">
|
|
802
|
-
<div :class="[
|
|
895
|
+
<div :class="[
|
|
896
|
+
'px-2',
|
|
897
|
+
densityConfig.groupHeaderPadding,
|
|
898
|
+
'font-semibold text-sm uppercase tracking-wide',
|
|
899
|
+
row._groupTitleClass || 'text-gray-700'
|
|
900
|
+
]">
|
|
803
901
|
{{ row._groupTitle }}
|
|
804
902
|
</div>
|
|
805
903
|
</TableCell>
|
|
@@ -811,7 +909,7 @@ function getCellStyle(col: any) {
|
|
|
811
909
|
v-for="(col, cIdx) in columns"
|
|
812
910
|
:key="col.key"
|
|
813
911
|
:class="getCellClass(col, cIdx, columns.length, idx)"
|
|
814
|
-
:style="getCellStyle(col)"
|
|
912
|
+
:style="getCellStyle(col, cIdx, columns.length)"
|
|
815
913
|
:padding="densityConfig.cellPadding"
|
|
816
914
|
:height="densityConfig.cellHeight"
|
|
817
915
|
>
|
|
@@ -837,6 +935,12 @@ function getCellStyle(col: any) {
|
|
|
837
935
|
</TableRow>
|
|
838
936
|
</TableBody>
|
|
839
937
|
</Table>
|
|
938
|
+
</div>
|
|
939
|
+
|
|
940
|
+
<!-- Loading Overlay -->
|
|
941
|
+
<div v-if="isLoading && tableData.length > 0" class="absolute inset-0 bg-background/50 flex items-center justify-center z-[60]">
|
|
942
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-8 w-8 animate-spin text-primary"><path d="M21 12a9 9 0 1 1-6.219-8.56"/></svg>
|
|
943
|
+
</div>
|
|
840
944
|
</div>
|
|
841
945
|
|
|
842
946
|
<!-- Pagination -->
|
|
@@ -893,3 +997,28 @@ function getCellStyle(col: any) {
|
|
|
893
997
|
</div>
|
|
894
998
|
</div>
|
|
895
999
|
</template>
|
|
1000
|
+
|
|
1001
|
+
<style scoped>
|
|
1002
|
+
/* Fixed column boundary separator (DataTables approach) */
|
|
1003
|
+
.fixed-column-boundary-right::after {
|
|
1004
|
+
content: "";
|
|
1005
|
+
position: absolute;
|
|
1006
|
+
top: 0;
|
|
1007
|
+
right: 0;
|
|
1008
|
+
bottom: 0;
|
|
1009
|
+
width: 10px;
|
|
1010
|
+
box-shadow: rgba(0, 0, 0, 0.2) 6px 0px 4px -4px inset;
|
|
1011
|
+
pointer-events: none;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
.fixed-column-boundary-left::before {
|
|
1015
|
+
content: "";
|
|
1016
|
+
position: absolute;
|
|
1017
|
+
top: 0;
|
|
1018
|
+
left: -10px;
|
|
1019
|
+
bottom: 0;
|
|
1020
|
+
width: 10px;
|
|
1021
|
+
box-shadow: rgba(0, 0, 0, 0.2) -6px 0px 4px -4px inset;
|
|
1022
|
+
pointer-events: none;
|
|
1023
|
+
}
|
|
1024
|
+
</style>
|
|
@@ -16,7 +16,7 @@ const delegatedProps = computed(() => {
|
|
|
16
16
|
<template>
|
|
17
17
|
<div class="relative w-full overflow-auto">
|
|
18
18
|
<table
|
|
19
|
-
:class="cn('w-full caption-bottom text-sm border-separate border-spacing-0', props.class)"
|
|
19
|
+
:class="cn('w-full caption-bottom text-sm !border-separate border-spacing-0', props.class)"
|
|
20
20
|
v-bind="delegatedProps"
|
|
21
21
|
>
|
|
22
22
|
<slot />
|
|
@@ -7,6 +7,7 @@ const props = defineProps<{
|
|
|
7
7
|
style?: any
|
|
8
8
|
padding?: string
|
|
9
9
|
height?: string
|
|
10
|
+
colspan?: number | string
|
|
10
11
|
}>()
|
|
11
12
|
|
|
12
13
|
const delegatedProps = computed(() => {
|
|
@@ -25,7 +26,7 @@ const cellStyle = computed(() => {
|
|
|
25
26
|
|
|
26
27
|
<template>
|
|
27
28
|
<td
|
|
28
|
-
:class="cn(props.padding || '
|
|
29
|
+
:class="cn(props.padding || 'px-2.5 py-2', 'border-b border-stone-300 align-middle [&:has([role=checkbox])]:pr-0', props.class)"
|
|
29
30
|
:style="cellStyle"
|
|
30
31
|
v-bind="delegatedProps"
|
|
31
32
|
>
|
|
@@ -20,8 +20,8 @@ const delegatedProps = computed(() => {
|
|
|
20
20
|
:class="
|
|
21
21
|
cn(
|
|
22
22
|
props.height || 'h-[38px]',
|
|
23
|
-
props.padding || 'px-2',
|
|
24
|
-
'border-b border-stone-300
|
|
23
|
+
props.padding || 'px-2.5',
|
|
24
|
+
'border-b border-stone-300 align-middle font-bold text-muted-foreground [&:has([role=checkbox])]:pr-0',
|
|
25
25
|
props.class,
|
|
26
26
|
)
|
|
27
27
|
"
|