├── public ├── cloudeep.ico └── vuejs-paginate-next.gif ├── src ├── main.js ├── components │ ├── index.js │ └── Paginate.vue └── App.vue ├── .gitignore ├── .github └── workflows │ └── test.yaml ├── index.html ├── vite.config.js ├── LICENSE ├── package.json ├── test └── paginate.test.js └── README.md /public/cloudeep.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudeep/vuejs-paginate-next/HEAD/public/cloudeep.ico -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import App from "./App.vue"; 3 | 4 | createApp(App).mount("#app"); 5 | -------------------------------------------------------------------------------- /public/vuejs-paginate-next.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cloudeep/vuejs-paginate-next/HEAD/public/vuejs-paginate-next.gif -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | // If you want to export the component as VuejsPaginateNext.Paginate 2 | // export { default as Paginate } from "./Paginate.vue"; 3 | 4 | import Paginate from "./Paginate.vue"; 5 | export default Paginate; 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Use Node.js 16 | uses: actions/setup-node@v2 17 | with: 18 | node-version: '16.x' 19 | cache: 'npm' 20 | - run: npm install 21 | - run: npm run test 22 | 23 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import vue from "@vitejs/plugin-vue"; 3 | 4 | import path from "path"; 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | build: { 9 | lib: { 10 | entry: path.resolve(__dirname, "src/components", "index.js"), 11 | name: "VuejsPaginateNext", 12 | target: "es2015", 13 | minify: false, 14 | fileName: (format) => `vuejs-paginate-next.${format}.js`, 15 | }, 16 | rollupOptions: { 17 | external: ["vue"], 18 | output: { 19 | globals: { 20 | vue: "Vue", 21 | }, 22 | }, 23 | }, 24 | }, 25 | test: { 26 | globals: true, 27 | environment: "happy-dom", 28 | }, 29 | plugins: [vue()], 30 | }); 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Cloudeep 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuejs-paginate-next", 3 | "version": "1.0.2", 4 | "description": "A simple Vue3 pagination component.", 5 | "private": false, 6 | "keywords": [ 7 | "vue", 8 | "paginate", 9 | "pagination" 10 | ], 11 | "files": [ 12 | "dist" 13 | ], 14 | "main": "./dist/vuejs-paginate-next.umd.js", 15 | "module": "./dist/vuejs-paginate-next.es.js", 16 | "exports": { 17 | ".": { 18 | "import": "./dist/vuejs-paginate-next.es.js", 19 | "require": "./dist/vuejs-paginate-next.umd.js" 20 | } 21 | }, 22 | "unpkg": "./dist/vuejs-paginate-next.umd.js", 23 | "jsdelivr": "./dist/vuejs-paginate-next.umd.js", 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/cloudeep/vuejs-paginate-next" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/cloudeep/vuejs-paginate-next/issues" 30 | }, 31 | "homepage": "https://github.com/cloudeep/vuejs-paginate-next#readme", 32 | "author": "Neil Perng", 33 | "license": "MIT", 34 | "scripts": { 35 | "dev": "vite", 36 | "build": "vite build", 37 | "test": "vitest", 38 | "preview": "vite preview" 39 | }, 40 | "dependencies": { 41 | "vue": "^3.2.25" 42 | }, 43 | "devDependencies": { 44 | "@vitejs/plugin-vue": "^2.2.0", 45 | "@vue/test-utils": "^2.0.0-rc.17", 46 | "happy-dom": "^2.41.0", 47 | "jsdom": "^19.0.0", 48 | "vite": "^2.8.0", 49 | "vitest": "^0.5.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 153 | 154 | 159 | -------------------------------------------------------------------------------- /test/paginate.test.js: -------------------------------------------------------------------------------- 1 | import Paginate from "../src/components/Paginate.vue"; 2 | 3 | import { describe, it } from "vitest"; 4 | import { mount } from "@vue/test-utils"; 5 | 6 | describe("Paginate", async () => { 7 | function initComponent() { 8 | return mount(Paginate, { 9 | propsData: { 10 | pageCount: 10, 11 | }, 12 | }); 13 | } 14 | 15 | describe("Simple Cases", () => { 16 | it("success", async () => { 17 | const wrapper = initComponent(); 18 | expect(wrapper.find("li:first-child a").text()).toBe("Prev"); 19 | expect(wrapper.find("li:last-child a").text()).toBe("Next"); 20 | expect(wrapper.find(".active a").text()).toBe("1"); 21 | }); 22 | 23 | it("next and prev button event right", async () => { 24 | const wrapper = initComponent(); 25 | const nextButton = wrapper.find("li:last-child a"); 26 | 27 | await nextButton.trigger("click"); 28 | expect(wrapper.find(".active a").text()).toBe("2"); 29 | 30 | const prevButton = wrapper.find("li:first-child a"); 31 | await prevButton.trigger("click"); 32 | expect(wrapper.find(".active a").text()).toBe("1"); 33 | }); 34 | 35 | it("prev button when first page", async () => { 36 | const wrapper = initComponent(); 37 | const prevButton = wrapper.find("li:first-child a"); 38 | await prevButton.trigger("click"); 39 | expect(wrapper.find(".active a").text()).toBe("1"); 40 | }); 41 | 42 | it("click page element", async () => { 43 | const wrapper = initComponent(); 44 | const pageItem = wrapper.find("li:nth-child(3) a"); 45 | await pageItem.trigger("click"); 46 | expect(wrapper.find(".active a").text()).toBe("2"); 47 | }); 48 | 49 | it("set initial page success", async () => { 50 | const wrapper = mount(Paginate, { 51 | propsData: { 52 | pageCount: 10, 53 | initialPage: 2, 54 | }, 55 | }); 56 | expect(wrapper.find(".active a").text()).toBe("2"); 57 | }); 58 | 59 | it("set forcePage success", async () => { 60 | const wrapper = mount(Paginate, { 61 | propsData: { 62 | pageCount: 10, 63 | forcePage: 5, 64 | }, 65 | }); 66 | 67 | const nextButton = wrapper.find("li:last-child a"); 68 | await nextButton.trigger("click"); 69 | expect(wrapper.find(".active a").text()).toBe("5"); 70 | 71 | const prevButton = wrapper.find("li:first-child a"); 72 | await prevButton.trigger("click"); 73 | expect(wrapper.find(".active a").text()).toBe("5"); 74 | }); 75 | 76 | it("set forcePage in initialPage", async () => { 77 | const wrapper = mount(Paginate, { 78 | propsData: { 79 | pageCount: 10, 80 | initialPage: 6, 81 | forcePage: 6, 82 | }, 83 | }); 84 | expect(wrapper.find(".active a").text()).toBe("6"); 85 | const nextButton = wrapper.find("li:last-child a"); 86 | await nextButton.trigger("click"); 87 | expect(wrapper.find(".active a").text()).toBe("6"); 88 | }); 89 | }); 90 | 91 | describe("page range tests", () => { 92 | it("page count not more than range", async () => { 93 | const wrapper = mount(Paginate, { 94 | propsData: { 95 | pageCount: 5, 96 | pageRange: 5, 97 | }, 98 | }); 99 | expect(wrapper.findAll("li a").length).toBe(7); 100 | }); 101 | 102 | it("only has breakView in left", async () => { 103 | const wrapper = mount(Paginate, { 104 | propsData: { 105 | pageCount: 10, 106 | initialPage: 10, 107 | breakViewClass: "break-view", 108 | }, 109 | }); 110 | 111 | expect(wrapper.findAll(`li:nth-child(3).break-view`).length).toBe(1); 112 | expect(wrapper.find(`li:nth-child(3) a`).text()).toBe("…"); 113 | expect(wrapper.find(`li:nth-child(4) a`).text()).toBe("8"); 114 | }); 115 | 116 | it("page range is correct when current page is 1", async () => { 117 | const wrapper = mount(Paginate, { 118 | propsData: { 119 | pageCount: 10, 120 | pageRange: 5, 121 | marginPages: 0, 122 | initialPage: 1, 123 | prevClass: "ignore", 124 | nextClass: "ignore", 125 | disabledClass: "ignore", 126 | }, 127 | }); 128 | expect( 129 | wrapper.findAll("li a").length - wrapper.findAll("li.ignore a").length 130 | ).toBe(5); 131 | }); 132 | 133 | it("page range is correct when current page is middle page", async () => { 134 | const wrapper = mount(Paginate, { 135 | propsData: { 136 | pageCount: 10, 137 | pageRange: 5, 138 | marginPages: 0, 139 | initialPage: 5, 140 | prevClass: "ignore", 141 | nextClass: "ignore", 142 | disabledClass: "ignore", 143 | }, 144 | }); 145 | expect( 146 | wrapper.findAll("li a").length - wrapper.findAll("li.ignore a").length 147 | ).toBe(5); 148 | }); 149 | 150 | it("page range is correct when current page is last page", async () => { 151 | const wrapper = mount(Paginate, { 152 | propsData: { 153 | pageCount: 10, 154 | pageRange: 5, 155 | marginPages: 0, 156 | initialPage: 10, 157 | prevClass: "ignore", 158 | nextClass: "ignore", 159 | disabledClass: "ignore", 160 | }, 161 | }); 162 | expect( 163 | wrapper.findAll("li a").length - wrapper.findAll("li.ignore a").length 164 | ).toBe(5); 165 | }); 166 | }); 167 | 168 | describe("enable first and last button", () => { 169 | it("Show fist and last button", async () => { 170 | const wrapper = mount(Paginate, { 171 | propsData: { 172 | initialPage: 2, 173 | pageCount: 10, 174 | firstLastButton: true, 175 | }, 176 | }); 177 | 178 | const firstButton = wrapper.find("li:first-child a"); 179 | const lastButton = wrapper.find("li:last-child a"); 180 | let activeItem = wrapper.find(".active a"); 181 | expect(firstButton.text()).toBe("First"); 182 | expect(lastButton.text()).toBe("Last"); 183 | expect(activeItem.text()).toBe("2"); 184 | 185 | await firstButton.trigger("click"); 186 | activeItem = wrapper.find(".active a"); 187 | expect(activeItem.text()).toBe("1"); 188 | 189 | await lastButton.trigger("click"); 190 | activeItem = wrapper.find(".active a"); 191 | expect(activeItem.text()).toBe("10"); 192 | }); 193 | 194 | it("Show fist and last button when no li surround", async () => { 195 | const wrapper = mount(Paginate, { 196 | propsData: { 197 | initialPage: 2, 198 | pageCount: 10, 199 | noLiSurround: true, 200 | firstLastButton: true, 201 | }, 202 | }); 203 | 204 | const firstButton = wrapper.find("a:first-child"); 205 | const lastButton = wrapper.find("a:last-child"); 206 | const activeItem = wrapper.find("a.active"); 207 | expect(firstButton.text()).toBe("First"); 208 | expect(lastButton.text()).toBe("Last"); 209 | expect(activeItem.text()).toBe("2"); 210 | }); 211 | }); 212 | 213 | describe("prev and next button hide", () => { 214 | it("hide prev button when there is no previous page", async () => { 215 | const wrapper = mount(Paginate, { 216 | propsData: { 217 | pageCount: 10, 218 | initialPage: 1, 219 | hidePrevNext: true, 220 | }, 221 | }); 222 | const firstButton = wrapper.find("li:first-child a"); 223 | expect(firstButton.text()).toBe("1"); 224 | }); 225 | 226 | it("hide next button when there is no next page", async () => { 227 | const wrapper = mount(Paginate, { 228 | propsData: { 229 | pageCount: 10, 230 | initialPage: 10, 231 | hidePrevNext: true, 232 | }, 233 | }); 234 | 235 | const lastButton = wrapper.find("li:last-child a"); 236 | expect(lastButton.text()).toBe("10"); 237 | }); 238 | 239 | it("hide next and prev button when only one page", async () => { 240 | const wrapper = mount(Paginate, { 241 | propsData: { 242 | pageCount: 1, 243 | hidePrevNext: true, 244 | }, 245 | }); 246 | 247 | const firstButton = wrapper.find("li:first-child a"); 248 | const lastButton = wrapper.find("li:last-child a"); 249 | expect(firstButton.text()).toBe("1"); 250 | expect(lastButton.text()).toBe("1"); 251 | }); 252 | }); 253 | 254 | it("Use custom text", () => { 255 | const wrapper = mount(Paginate, { 256 | propsData: { 257 | pageCount: 10, 258 | prevClass: "prev-item", 259 | nextClass: "next-item", 260 | breakViewClass: "break-view", 261 | prevText: "PREVIOUS TEXT", 262 | nextText: "NEXT TEXT", 263 | breakViewText: "BREAK VIEW TEXT", 264 | }, 265 | }); 266 | 267 | const prevButton = wrapper.find(".prev-item"); 268 | const nextButton = wrapper.find(".next-item"); 269 | const breakView = wrapper.find(".break-view"); 270 | expect(prevButton.text()).toBe("PREVIOUS TEXT"); 271 | expect(nextButton.text()).toBe("NEXT TEXT"); 272 | expect(breakView.text()).toBe("BREAK VIEW TEXT"); 273 | }); 274 | }); 275 | -------------------------------------------------------------------------------- /src/components/Paginate.vue: -------------------------------------------------------------------------------- 1 | 245 | 246 | 400 | 401 | 402 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vuejs-paginate-next 2 | 3 | ![tests](https://github.com/cloudeep/vuejs-paginate-next/actions/workflows/test.yaml/badge.svg) 4 | 5 | npm 6 | 7 | 8 | MIT 9 | 10 | 11 | [![vue](https://badges.aleen42.com/src/vue.svg)](https://badges.aleen42.com/src/vue.svg) 12 | [![javascript](https://badges.aleen42.com/src/javascript.svg)](https://badges.aleen42.com/src/javascript.svg) 13 | [![vitejs](https://badges.aleen42.com/src/vitejs.svg)](https://badges.aleen42.com/src/vitejs.svg) 14 | [![rollup](https://badges.aleen42.com/src/rollup.svg)](https://badges.aleen42.com/src/rollup.svg) 15 | 16 | 17 | 18 | 19 | 20 | 21 | A Vue.js (v3) component to make pagination, based on [vuejs-paginate](https://github.com/lokyoung/vuejs-paginate) from [lokyoung](https://github.com/lokyoung). Thank [bverheec](https://github.com/bverheec) for his Vue.js v3 solution in issue [#128](https://github.com/lokyoung/vuejs-paginate/issues/128). 22 | 23 | Easy to use by providing simple api. And you can customize the style of this component by CSS. 24 | 25 | 26 | 27 | ## Installation 28 | 29 | ### NPM 30 | 31 | Install the npm package. 32 | 33 | ```js 34 | $ npm install vuejs-paginate-next --save 35 | ``` 36 | 37 | Register the component. 38 | 39 | ```js 40 | import Paginate from "vuejs-paginate-next"; 41 | ``` 42 | 43 | --- 44 | 45 | _Note_: For users using original **vuejs-paginate** package, just directly adopt **initial-page** as initial page setting instead of **value**. 46 | 47 | After Vue 3, the `Vue.use(Paginate)` is **deprecated**. You should use `app.createApp({...}).use(Paginate).mount('#app')` instead. 48 | 49 | --- 50 | 51 | ### CDN 52 | 53 | Include the source file. 54 | 55 | ```html 56 | 57 | 58 | 59 | 60 | ``` 61 | 62 | ## Usage 63 | 64 | ### In Vue Template 65 | 66 | **Basic Usage** 67 | 68 | ```html 69 | 76 | 77 | ``` 78 | 79 | _Note_: In vue template, camelCase and kebab-case are both supported. For example, you can either use prop `page-count` or `pageCount`. They are leading to the same result. 80 | 81 | So this is also avaliable 82 | 83 | ```html 84 | 91 | 92 | ``` 93 | 94 | **Example** 95 | 96 | ```html 97 | 110 | 111 | 124 | 125 | 135 | ``` 136 | 137 | ### Value Binding 138 | 139 | Use `v-model` to set the selected page number. You can programmatically modify the current page by using this. 140 | 141 | ```html 142 | 156 | 157 | 166 | ``` 167 | 168 | ### In HTML 169 | 170 | Must use kebab-case for props in pure HTML. 171 | 172 | **Example** 173 | 174 | JavaScript 175 | 176 | ```js 177 | new Vue.createApp({ 178 | components: { 179 | paginate: VuejsPaginateNext, 180 | }, 181 | methods: { 182 | clickCallback: function (pageNum) { 183 | console.log(pageNum); 184 | }, 185 | }, 186 | }).mount("#app"); 187 | ``` 188 | 189 | HTML 190 | 191 | ```html 192 |
193 | 200 | 201 |
202 | ``` 203 | 204 | ## Props 205 | 206 | | Name                                        | Type | Description | 207 | | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 208 | | `page-count` | `Number` | Total count of pages. **required** | 209 | | `page-range` | `Number` | Range of pages which displayed. **default: 3**
_(Note: It is recommended to use an odd number, so that the same number of pages are displayed before and after the active page. If using an even number, there will be one more page number before the active page than after the current page)_ | 210 | | `margin-pages` | `Number` | The number of displayed pages for margins. **default: 1** | 211 | | `prev-text` | `String` | Text for the previous button. You can use HTML here. **default: Prev** | 212 | | `next-text` | `String` | Text for the next button. You can use HTML here. **default: Next** | 213 | | `break-view-text` | `String` | Text for the break view indicator. **default: ...** | 214 | | `initial-page`
| `Number` | The index of initial page which selected. **default: 1** | 215 | | `force-page` | `Number` | The page number of overridden selected page. | 216 | | `click-handler` | `Function` | The method to call when page clicked. Use clicked page number as parameter. | 217 | | `container-class` | `String` | CSS class name for the layout. | 218 | | `page-class` | `String` | CSS class name for tag `li` of each page element. | 219 | | `page-link-class` | `String` | CSS class name for tag `a` of each page element. | 220 | | `prev-class` | `String` | CSS class name for tag `li` of `previous` element. | 221 | | `prev-link-class` | `String` | CSS class name for tag `a` of `previous` element. | 222 | | `next-class` | `String` | CSS class name for tag `li` of `next` element. | 223 | | `next-link-class` | `String` | CSS class name for tag `a` of `next` element. | 224 | | `break-view-class` | `String` | CSS class name for tag `li` of `break view` element. | 225 | | `break-view-link-class` | `String` | CSS class name for tag `a` of `break view` element. | 226 | | `active-class` | `String` | CSS class name for active page element. **default: active** | 227 | | `disabled-class` | `String` | CSS class name for disabled page element. **default: disabled** | 228 | | `no-li-surround` | `Boolean` | Support no `li` tag surround `a` tag. **default: false** | 229 | | `first-last-button` | `Boolean` | Support buttons to turn to the first and last page. **default: false** | 230 | | `first-button-text` | `String` | Text for first button. (Not visible when `first-last-button` is false. You can use HTML here.) **default: 'First'** | 231 | | `last-button-text` | `String` | Text for last button. (Not visible when `first-last-button` is false. You can use HTML here.) **default: 'Last'** | 232 | | `hide-prev-next` | `Boolean` | Hide prev/next button when there is no previous or next page. **default: false** | 233 | 234 | ## Customize inner HTML (experimental) 235 | 236 | You can customize the inner HTML of the previous button, next button, and break view indicator, with the `slot` tag. 237 | 238 | **Slot names** 239 | 240 | | Name | Description | 241 | | ------------------ | -------------------- | 242 | | `prevContent` | Previous button | 243 | | `nextContent` | Next button | 244 | | `breakViewContent` | Break view indicator | 245 | 246 | **Note** 247 | Slot of `prevContent` and `nextContent` are not supported after `v1.9.5`. You can directly set the HTML by `prev-text` and `next-text` props. 248 | 249 | **Example** 250 | 251 | ```html 252 | 259 | Changed previous button 260 | Changed next button 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | ``` 270 | 271 | ## Demo 272 | 273 | You can see the demo for quickly understand how to use this package. 274 | 275 | ```sh 276 | $ git clone https://github.com/cloudeep/vuejs-paginate-next.git 277 | $ cd vuejs-paginate-next 278 | $ npm install 279 | $ npm run dev 280 | ``` 281 | 282 | Check the code from `./index.html` and `./src/App.vue`. 283 | --------------------------------------------------------------------------------