@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.
@@ -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
@@ -10,6 +10,12 @@
10
10
 
11
11
  ## Version History
12
12
 
13
+ ### v4.7.0
14
+
15
+ - :arrow_up: Remove Babel
16
+ - :bug: Fix paging bugs in TablerPager
17
+ - :white_check_mark: Add tests for TablerPager
18
+
13
19
  ### v4.6.0
14
20
 
15
21
  - :arrow_up: Update VueRouter Peer Dependency
@@ -1,22 +1,14 @@
1
1
  <template>
2
2
  <div class='pagination m-0'>
3
3
  <div>
4
- <template v-if='total <= limit'>
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='end > 5 && current > 3'>
21
+ <template v-if='showLeadingEllipsis'>
30
22
  <span class=''> ... </span>
31
23
  </template>
32
24
 
33
- <template v-if='total / limit > 2'>
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='end > 5 && current < end - spread'>
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 === end - 1 }'
50
- @click='changePage(end - 1)'
51
- v-text='end'
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 { ref, watch } from 'vue'
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 spread = ref(0)
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
- // Array containing middle page number
93
- let middleAr = new Array(spreadValue).fill(1, 0, spreadValue).map((ele, i) => {
94
- return 1 + i;
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 changePage = (page: number) => {
106
- current.value = page;
107
- emit('page', current.value);
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
- // Initialize values
111
- const initialValues = create();
112
- spread.value = initialValues.spread;
113
- middle.value = initialValues.middle;
114
- current.value = initialValues.current;
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
- middle.value = middle.value.map((ele, i) => {
154
- return start + i + 1;
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.6.0",
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": "echo \"Error: no test specified\" && exit 1",
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
- "@babel/eslint-parser": "^7.22.15",
38
+ "@eslint/js": "^10.0.0",
38
39
  "@typescript-eslint/parser": "^8.8.1",
39
- "@vue/cli-plugin-babel": "^5.0.8",
40
- "eslint": "^9.12.0",
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
@@ -22,7 +22,8 @@
22
22
  "include": [
23
23
  "lib.ts",
24
24
  "components/**/*.vue",
25
- "components/**/*.ts"
25
+ "components/**/*.ts",
26
+ "test/**/*.ts"
26
27
  ],
27
28
  "exclude": [
28
29
  "node_modules"
@@ -0,0 +1,10 @@
1
+ import { defineConfig } from 'vitest/config'
2
+ import vue from '@vitejs/plugin-vue'
3
+
4
+ export default defineConfig({
5
+ plugins: [vue()],
6
+ test: {
7
+ environment: 'jsdom',
8
+ include: ['test/**/*.spec.ts'],
9
+ },
10
+ })
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
- }
package/babel.config.json DELETED
@@ -1,5 +0,0 @@
1
- {
2
- "presets": [
3
- "@vue/cli-plugin-babel/preset"
4
- ]
5
- }