@tak-ps/vue-tabler 4.6.0 → 4.7.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/.github/workflows/test.yml +39 -0
- package/CHANGELOG.md +6 -0
- package/components/Pager.vue +62 -95
- package/package.json +11 -5
- package/test/Pager.spec.ts +109 -0
- package/tsconfig.json +2 -1
- package/vitest.config.mjs +10 -0
- package/.eslintrc +0 -17
- package/babel.config.json +0 -5
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
|
|
3
|
+
permissions:
|
|
4
|
+
contents: read
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
pull_request:
|
|
8
|
+
push:
|
|
9
|
+
branches:
|
|
10
|
+
- main
|
|
11
|
+
- master
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
node-version: [22, 24]
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v6
|
|
23
|
+
|
|
24
|
+
- uses: actions/setup-node@v6
|
|
25
|
+
with:
|
|
26
|
+
node-version: ${{ matrix.node-version }}
|
|
27
|
+
cache: npm
|
|
28
|
+
|
|
29
|
+
- name: Install dependencies
|
|
30
|
+
run: npm ci
|
|
31
|
+
|
|
32
|
+
- name: Type check
|
|
33
|
+
run: npm run check
|
|
34
|
+
|
|
35
|
+
- name: Lint
|
|
36
|
+
run: npm run lint
|
|
37
|
+
|
|
38
|
+
- name: Test
|
|
39
|
+
run: npm test
|
package/CHANGELOG.md
CHANGED
package/components/Pager.vue
CHANGED
|
@@ -1,22 +1,14 @@
|
|
|
1
1
|
<template>
|
|
2
2
|
<div class='pagination m-0'>
|
|
3
3
|
<div>
|
|
4
|
-
<template v-if='
|
|
5
|
-
<button
|
|
6
|
-
class='btn mx-1'
|
|
7
|
-
@click='changePage(0)'
|
|
8
|
-
>
|
|
9
|
-
<IconHome
|
|
10
|
-
:size='32'
|
|
11
|
-
stroke='1'
|
|
12
|
-
class='icon'
|
|
13
|
-
/>Home
|
|
14
|
-
</button>
|
|
15
|
-
</template>
|
|
4
|
+
<template v-if='pageCount <= 1' />
|
|
16
5
|
<template v-else>
|
|
17
6
|
<button
|
|
7
|
+
type='button'
|
|
18
8
|
class='btn mx-1'
|
|
19
9
|
:class='{ "btn-primary": current === 0 }'
|
|
10
|
+
:disabled='current === 0'
|
|
11
|
+
aria-label='Go to first page'
|
|
20
12
|
@click='changePage(0)'
|
|
21
13
|
>
|
|
22
14
|
<IconHome
|
|
@@ -26,29 +18,35 @@
|
|
|
26
18
|
/>Home
|
|
27
19
|
</button>
|
|
28
20
|
|
|
29
|
-
<template v-if='
|
|
21
|
+
<template v-if='showLeadingEllipsis'>
|
|
30
22
|
<span class=''> ... </span>
|
|
31
23
|
</template>
|
|
32
24
|
|
|
33
|
-
<template v-if='
|
|
25
|
+
<template v-if='middle.length'>
|
|
34
26
|
<button
|
|
35
27
|
v-for='i in middle'
|
|
36
28
|
:key='i'
|
|
29
|
+
type='button'
|
|
37
30
|
class='btn mx-1'
|
|
38
31
|
:class='{ "btn-primary": current === i }'
|
|
32
|
+
:disabled='current === i'
|
|
33
|
+
:aria-current='current === i ? "page" : undefined'
|
|
39
34
|
@click='changePage(i)'
|
|
40
35
|
v-text='i + 1'
|
|
41
36
|
/>
|
|
42
37
|
</template>
|
|
43
38
|
|
|
44
|
-
<template v-if='
|
|
39
|
+
<template v-if='showTrailingEllipsis'>
|
|
45
40
|
<span class=''> ... </span>
|
|
46
41
|
</template>
|
|
47
42
|
<button
|
|
43
|
+
type='button'
|
|
48
44
|
class='btn mx-1'
|
|
49
|
-
:class='{ "btn-primary": current ===
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
:class='{ "btn-primary": current === pageCount - 1 }'
|
|
46
|
+
:disabled='current === pageCount - 1'
|
|
47
|
+
:aria-current='current === pageCount - 1 ? "page" : undefined'
|
|
48
|
+
@click='changePage(pageCount - 1)'
|
|
49
|
+
v-text='pageCount'
|
|
52
50
|
/>
|
|
53
51
|
</template>
|
|
54
52
|
</div>
|
|
@@ -56,7 +54,7 @@
|
|
|
56
54
|
</template>
|
|
57
55
|
|
|
58
56
|
<script setup lang="ts">
|
|
59
|
-
import {
|
|
57
|
+
import { computed } from 'vue'
|
|
60
58
|
import {
|
|
61
59
|
IconHome
|
|
62
60
|
} from '@tabler/icons-vue';
|
|
@@ -73,85 +71,54 @@ const emit = defineEmits<{
|
|
|
73
71
|
(e: 'page', page: number): void
|
|
74
72
|
}>()
|
|
75
73
|
|
|
76
|
-
const
|
|
77
|
-
const middle = ref<number[]>([])
|
|
78
|
-
const current = ref(0)
|
|
79
|
-
const end = ref(0)
|
|
80
|
-
|
|
81
|
-
const create = () => {
|
|
82
|
-
const endValue = Math.ceil(props.total / props.limit);
|
|
83
|
-
let spreadValue; //Number of pages in between home button and last page
|
|
84
|
-
if (endValue <= 2) {
|
|
85
|
-
spreadValue = 0;
|
|
86
|
-
} else if (endValue >= 7) {
|
|
87
|
-
spreadValue = 5;
|
|
88
|
-
} else {
|
|
89
|
-
spreadValue = endValue - 2;
|
|
90
|
-
}
|
|
74
|
+
const MAX_MIDDLE_PAGES = 5;
|
|
91
75
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
spread: spreadValue,
|
|
99
|
-
middle: middleAr,
|
|
100
|
-
current: props.page || 0,
|
|
101
|
-
end: endValue
|
|
102
|
-
};
|
|
103
|
-
}
|
|
76
|
+
const pageCount = computed(() => {
|
|
77
|
+
if (props.limit <= 0) return 0;
|
|
78
|
+
return Math.ceil(props.total / props.limit);
|
|
79
|
+
});
|
|
104
80
|
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
81
|
+
const current = computed(() => {
|
|
82
|
+
if (pageCount.value <= 1) return 0;
|
|
83
|
+
|
|
84
|
+
const maxPage = pageCount.value - 1;
|
|
85
|
+
const page = Number.isFinite(props.page) ? props.page : 0;
|
|
86
|
+
|
|
87
|
+
return Math.min(Math.max(page, 0), maxPage);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
const middleWindow = computed(() => {
|
|
91
|
+
if (pageCount.value <= 2) return 0;
|
|
92
|
+
return Math.min(MAX_MIDDLE_PAGES, pageCount.value - 2);
|
|
93
|
+
});
|
|
109
94
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
end.value = initialValues.end;
|
|
116
|
-
|
|
117
|
-
// Watch for page changes
|
|
118
|
-
watch(() => props.page, () => {
|
|
119
|
-
current.value = props.page;
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
// Watch for total changes
|
|
123
|
-
watch(() => props.total, () => {
|
|
124
|
-
const set = create();
|
|
125
|
-
spread.value = set.spread;
|
|
126
|
-
middle.value = set.middle;
|
|
127
|
-
current.value = set.current;
|
|
128
|
-
end.value = set.end;
|
|
129
|
-
})
|
|
130
|
-
|
|
131
|
-
// Watch for limit changes
|
|
132
|
-
watch(() => props.limit, () => {
|
|
133
|
-
const set = create();
|
|
134
|
-
spread.value = set.spread;
|
|
135
|
-
middle.value = set.middle;
|
|
136
|
-
current.value = set.current;
|
|
137
|
-
end.value = set.end;
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
// Watch for current changes
|
|
141
|
-
watch(current, () => {
|
|
142
|
-
if (end.value < 5) return; // All buttons are shown already
|
|
143
|
-
|
|
144
|
-
let start: number;
|
|
145
|
-
if (current.value <= 3) {
|
|
146
|
-
start = 0;
|
|
147
|
-
} else if (current.value >= end.value - 4) {
|
|
148
|
-
start = end.value - spread.value - 2;
|
|
149
|
-
} else {
|
|
150
|
-
start = current.value - 3;
|
|
95
|
+
const middle = computed(() => {
|
|
96
|
+
if (!middleWindow.value) return [];
|
|
97
|
+
|
|
98
|
+
if (pageCount.value <= MAX_MIDDLE_PAGES + 2) {
|
|
99
|
+
return Array.from({ length: middleWindow.value }, (_, index) => index + 1);
|
|
151
100
|
}
|
|
152
101
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
102
|
+
let start = Math.max(1, current.value - 3);
|
|
103
|
+
let end = Math.min(pageCount.value - 2, current.value + 1);
|
|
104
|
+
|
|
105
|
+
if (start === 1) {
|
|
106
|
+
end = Math.min(pageCount.value - 2, start + middleWindow.value - 1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return Array.from({ length: Math.max(0, end - start + 1) }, (_, index) => start + index);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const showLeadingEllipsis = computed(() => {
|
|
113
|
+
return middle.value.length > 0 && middle.value[0] > 1;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const showTrailingEllipsis = computed(() => {
|
|
117
|
+
return middle.value.length > 0 && middle.value[middle.value.length - 1] < pageCount.value - 2;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const changePage = (page: number) => {
|
|
121
|
+
if (page === current.value) return;
|
|
122
|
+
emit('page', page);
|
|
123
|
+
}
|
|
157
124
|
</script>
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tak-ps/vue-tabler",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "4.
|
|
4
|
+
"version": "4.7.0",
|
|
5
5
|
"lib": "lib.ts",
|
|
6
6
|
"main": "lib.ts",
|
|
7
7
|
"module": "lib.ts",
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
},
|
|
11
11
|
"description": "Tabler UI components for Vue3",
|
|
12
12
|
"scripts": {
|
|
13
|
-
"test": "
|
|
13
|
+
"test": "vitest run",
|
|
14
|
+
"test:watch": "vitest",
|
|
14
15
|
"check": "vue-tsc --noEmit",
|
|
15
16
|
"lint": "eslint lib.ts components/"
|
|
16
17
|
},
|
|
@@ -34,12 +35,17 @@
|
|
|
34
35
|
"vue-router": "^5.0.0"
|
|
35
36
|
},
|
|
36
37
|
"devDependencies": {
|
|
37
|
-
"@
|
|
38
|
+
"@eslint/js": "^10.0.0",
|
|
38
39
|
"@typescript-eslint/parser": "^8.8.1",
|
|
39
|
-
"@
|
|
40
|
-
"
|
|
40
|
+
"@vitejs/plugin-vue": "^6.0.5",
|
|
41
|
+
"@vue/test-utils": "^2.4.6",
|
|
42
|
+
"eslint": "^10.0.0",
|
|
41
43
|
"eslint-plugin-vue": "^10.0.0",
|
|
42
44
|
"globals": "^17.0.0",
|
|
45
|
+
"jsdom": "^29.0.0",
|
|
46
|
+
"typescript": "^5.9.3",
|
|
47
|
+
"vite": "^8.0.0",
|
|
48
|
+
"vitest": "^4.1.0",
|
|
43
49
|
"vue-tsc": "^3.2.1"
|
|
44
50
|
}
|
|
45
51
|
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { mount } from '@vue/test-utils'
|
|
3
|
+
import Pager from '../components/Pager.vue'
|
|
4
|
+
|
|
5
|
+
describe('TablerPager', () => {
|
|
6
|
+
it('renders no pager controls when there is only one page', () => {
|
|
7
|
+
const wrapper = mount(Pager, {
|
|
8
|
+
props: {
|
|
9
|
+
total: 10,
|
|
10
|
+
page: 0,
|
|
11
|
+
limit: 10,
|
|
12
|
+
},
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
expect(wrapper.findAll('button')).toHaveLength(0)
|
|
16
|
+
expect(wrapper.findAll('span')).toHaveLength(0)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('shows all pages without ellipses when the page count is small', () => {
|
|
20
|
+
const wrapper = mount(Pager, {
|
|
21
|
+
props: {
|
|
22
|
+
total: 60,
|
|
23
|
+
page: 2,
|
|
24
|
+
limit: 10,
|
|
25
|
+
},
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const buttons = wrapper.findAll('button').map((button) => button.text().replace(/\s+/g, ' ').trim())
|
|
29
|
+
|
|
30
|
+
expect(buttons).toEqual(['Home', '2', '3', '4', '5', '6'])
|
|
31
|
+
expect(wrapper.findAll('span')).toHaveLength(0)
|
|
32
|
+
expect(wrapper.find('button.btn-primary').text().trim()).toBe('3')
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('shows the trailing page window when the last page is selected', () => {
|
|
36
|
+
const wrapper = mount(Pager, {
|
|
37
|
+
props: {
|
|
38
|
+
total: 3320,
|
|
39
|
+
page: 331,
|
|
40
|
+
limit: 10,
|
|
41
|
+
},
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const buttons = wrapper.findAll('button').map((button) => button.text().replace(/\s+/g, ' ').trim())
|
|
45
|
+
const ellipses = wrapper.findAll('span').map((span) => span.text().trim())
|
|
46
|
+
|
|
47
|
+
expect(buttons).toEqual(['Home', '329', '330', '331', '332'])
|
|
48
|
+
expect(ellipses).toEqual(['...'])
|
|
49
|
+
expect(wrapper.findAll('button.btn-primary')).toHaveLength(1)
|
|
50
|
+
expect(wrapper.find('button.btn-primary').text().trim()).toBe('332')
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('shows a centered window with ellipses on both sides for mid-range pages', () => {
|
|
54
|
+
const wrapper = mount(Pager, {
|
|
55
|
+
props: {
|
|
56
|
+
total: 3320,
|
|
57
|
+
page: 166,
|
|
58
|
+
limit: 10,
|
|
59
|
+
},
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
const buttons = wrapper.findAll('button').map((button) => button.text().replace(/\s+/g, ' ').trim())
|
|
63
|
+
const ellipses = wrapper.findAll('span').map((span) => span.text().trim())
|
|
64
|
+
|
|
65
|
+
expect(buttons).toEqual(['Home', '164', '165', '166', '167', '168', '332'])
|
|
66
|
+
expect(ellipses).toEqual(['...', '...'])
|
|
67
|
+
expect(wrapper.findAll('button.btn-primary')).toHaveLength(1)
|
|
68
|
+
expect(wrapper.find('button.btn-primary').text().trim()).toBe('167')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('clamps page values below zero to the first page', () => {
|
|
72
|
+
const wrapper = mount(Pager, {
|
|
73
|
+
props: {
|
|
74
|
+
total: 3320,
|
|
75
|
+
page: -5,
|
|
76
|
+
limit: 10,
|
|
77
|
+
},
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
expect(wrapper.find('button.btn-primary').text().trim()).toBe('Home')
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
it('clamps page values above the max to the last page', () => {
|
|
84
|
+
const wrapper = mount(Pager, {
|
|
85
|
+
props: {
|
|
86
|
+
total: 3320,
|
|
87
|
+
page: 999,
|
|
88
|
+
limit: 10,
|
|
89
|
+
},
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
expect(wrapper.find('button.btn-primary').text().trim()).toBe('332')
|
|
93
|
+
expect(wrapper.findAll('span').map((span) => span.text().trim())).toEqual(['...'])
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('emits the selected page when a non-active page is clicked', async () => {
|
|
97
|
+
const wrapper = mount(Pager, {
|
|
98
|
+
props: {
|
|
99
|
+
total: 3320,
|
|
100
|
+
page: 331,
|
|
101
|
+
limit: 10,
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
await wrapper.get('button:not(.btn-primary)').trigger('click')
|
|
106
|
+
|
|
107
|
+
expect(wrapper.emitted('page')).toEqual([[0]])
|
|
108
|
+
})
|
|
109
|
+
})
|
package/tsconfig.json
CHANGED
package/.eslintrc
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"root": true,
|
|
3
|
-
"globals": {},
|
|
4
|
-
"env": {
|
|
5
|
-
"node": true
|
|
6
|
-
},
|
|
7
|
-
"extends": [
|
|
8
|
-
"plugin:vue/essential",
|
|
9
|
-
"eslint:recommended"
|
|
10
|
-
],
|
|
11
|
-
"parserOptions": {
|
|
12
|
-
"parser": "@babel/eslint-parser"
|
|
13
|
-
},
|
|
14
|
-
"rules": {
|
|
15
|
-
"vue/multi-word-component-names": 1
|
|
16
|
-
}
|
|
17
|
-
}
|