├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── eslint.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.en-US.md ├── README.md ├── babel.config.js ├── example ├── .browserslistrc ├── .gitignore ├── README.md ├── babel.config.js ├── package.json ├── public │ ├── favicon.ico │ └── index.html ├── src │ ├── App.vue │ ├── assets │ │ └── reset.scss │ └── main.js ├── vue.config.js └── yarn.lock ├── jest.config.js ├── lib ├── index.js └── index.module.js ├── package.json ├── packages ├── icon │ ├── index.js │ ├── loading.scss │ └── loading.vue ├── index.js ├── locale │ ├── index.js │ └── lang │ │ ├── en-US.js │ │ └── zh-CN.js ├── mixins │ ├── bind-event.js │ ├── timer.js │ └── touch.js ├── style │ └── var.scss ├── utils │ ├── event.js │ ├── getDeepValByKey.js │ ├── scroll.js │ └── throttle.js └── vuejs-loadmore │ ├── index.scss │ └── index.vue ├── rollup.config.js ├── test ├── __snapshots__ │ └── index.spec.js.snap ├── index.spec.js └── utils │ └── event.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | quote_type = single -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lib/ 2 | /example/ 3 | /test/ 4 | jest.config.js 5 | babel.config.js -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | extends: [ 7 | 'plugin:vue/essential', 8 | '@vue/standard' 9 | ], 10 | parserOptions: { 11 | parser: 'babel-eslint' 12 | }, 13 | rules: { 14 | 'no-console': 'warn', 15 | 'no-debugger': 'warn', 16 | 'semi': [2, 'always'] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/workflows/eslint.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm install 30 | - run: npm run lint 31 | - run: npm test 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build lib 2 | lib 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /example 2 | /test -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 staticdeng 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 | -------------------------------------------------------------------------------- /README.en-US.md: -------------------------------------------------------------------------------- 1 | # vuejs-loadmore 2 | [![Npm Version](https://img.shields.io/npm/v/vuejs-loadmore)](https://www.npmjs.com/package/vuejs-loadmore) [![Build Status](https://img.shields.io/github/workflow/status/staticdeng/vuejs-loadmore/Node.js%20CI)](https://github.com/staticdeng/vuejs-loadmore/actions) 3 | 4 | [![NPM](https://nodei.co/npm/vuejs-loadmore.png)](https://nodei.co/npm/vuejs-loadmore/) 5 | 6 | A pull-down refresh and pull-up loadmore scroll component for Vue.js. 7 | 8 | Easy to use by providing simple api. Unlike other component libraries, it uses the browser itself to scroll instead of js, so it has a smaller code size but does not lose the user experience. 9 | 10 | **English** | [中文](./README.md) 11 | 12 | ## Preview 13 | [Online demo](https://staticdeng.github.io/vuejs-loadmore/) 14 | 15 | You can also scan the following QR code to access the demo: 16 | 17 | 18 | 19 | ## Installation 20 | 21 | #### Install the npm package 22 | 23 | ```bash 24 | # npm 25 | npm install vuejs-loadmore --save 26 | ``` 27 | 28 | #### Import 29 | 30 | ```js 31 | import Vue from 'vue'; 32 | import VueLoadmore from 'vuejs-loadmore'; 33 | 34 | Vue.use(VueLoadmore); 35 | ``` 36 | 37 | ## Internationalization support 38 | 39 | Support Chinese zh-CN and English en-US, the default is zh-CN. 40 | 41 | ```js 42 | import VueLoadmore from 'vuejs-loadmore'; 43 | 44 | Vue.use(VueLoadmore, { 45 | lang: 'en-US' 46 | }) 47 | ``` 48 | 49 | You can also use `locale.use()` to specify the language. 50 | 51 | ```js 52 | import VueLoadmore, { locale } from 'vuejs-loadmore'; 53 | 54 | Vue.use(VueLoadmore); 55 | locale.use('en-US'); 56 | ``` 57 | 58 | ## Usage 59 | 60 | ### Basic Usage 61 | 62 | ```html 63 | 67 |
68 |
69 | ``` 70 | The `on-refresh` and `on-loadmore` will be Emitted when pull refresh or scroll to the bottom, you should need to execute the callback function `done()` after processing the data request. 71 | 72 | If you don't need refresh, only not to bind `on-refresh`. 73 | 74 | When the data request is finished, the data of `finished` you can changed to true, then will show `finished-text`. 75 | 76 | ```js 77 | export default { 78 | data() { 79 | return { 80 | list: [], 81 | page: 1, 82 | pageSize: 10, 83 | finished: false 84 | }; 85 | }, 86 | mounted() { 87 | this.fetch(); 88 | }, 89 | methods: { 90 | onRefresh(done) { 91 | this.list = []; 92 | this.page = 1; 93 | this.finished = false; 94 | this.fetch(); 95 | 96 | done(); 97 | }, 98 | 99 | onLoad(done) { 100 | if (this.page <= 10) { 101 | this.fetch(); 102 | } else { 103 | // all data loaded 104 | this.finished = true; 105 | } 106 | done(); 107 | }, 108 | 109 | fetch() { 110 | for (let i = 0; i < this.pageSize; i++) { 111 | this.list.push(this.list.length + 1); 112 | } 113 | this.page++; 114 | } 115 | }, 116 | } 117 | ``` 118 | 119 | ### Load Error Info 120 | 121 | ```html 122 | 126 |
127 |
128 | ``` 129 | 130 | ```js 131 | export default { 132 | data() { 133 | return { 134 | list: [], 135 | finished: false, 136 | error: false, 137 | }; 138 | }, 139 | methods: { 140 | onLoad() { 141 | fetchSomeThing().catch(() => { 142 | this.error = true; 143 | }); 144 | }, 145 | }, 146 | }; 147 | ``` 148 | 149 | ## API 150 | 151 | ### Props 152 | 153 | | Attribute | Description | Type | Default | 154 | | --- | --- | --- | --- | 155 | | on-refresh | Will be Emitted when pull refresh | _function_ | - | 156 | | pulling-text | The Text when pulling in refresh | _string_ | `Pull down to refresh` | 157 | | loosing-text | The Text when loosing in refresh | _string_ | `Loosing to refresh` | 158 | | refresh-text | The Text when loading in refresh | _string_ | `Refreshing` | 159 | | success-text | The Text when loading success in refresh | _string_ | `Refresh success` | 160 | | show-success-text | Whether to show `success-text` | _boolean_ | `true` | 161 | | pull-distance | The distance to trigger the refresh status | _number \| string_ | `50` | 162 | | head-height | The height of the area of the refresh shows | _number \| string_ | `50` | 163 | | animation-duration | Animation duration of the refresh | _number \| string_ | `200` | 164 | | on-loadmore | Will be Emitted when scroll to the bottom | _function_ | - | 165 | | immediate-check | Whether to check loadmore position immediately after mounted | _boolean_ | `true` | 166 | | load-offset | The `on-loadmore` will be Emitted when the distance from the scroll bar to the bottom is less than the `load-offset` | _number \| string_ | `50` | 167 | | finished | Whether the data is loaded | _boolean_ | `false` | 168 | | error | Whether the data is loaded error, the `on-loadmore` will be Emitted only when error text clicked, the `sync` modifier is needed | _boolean_ | `false` | 169 | | loading-text | The Text when loading in loaded | _string_ | `Loading` | 170 | | finished-text | The Text when the data is loaded | _string_ | `No more data` | 171 | | error-text | The Text when error loaded | _string_ | `Request failed, click to reload` | 172 | 173 | ### Methods 174 | 175 | Use ref to get List instance and call instance methods. 176 | 177 | | Name | Description | Attribute | Return value | 178 | | ----- | --------------------- | --------- | ------------ | 179 | | checkScroll | Check scroll position | - | - | 180 | 181 | 182 | ## Example 183 | 184 | You can see the demo for quickly understand how to use this package. 185 | 186 | ```bash 187 | git clone git@github.com:staticdeng/vuejs-loadmore.git 188 | yarn install 189 | yarn example:dev 190 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vuejs-loadmore 2 | [![Npm Version](https://img.shields.io/npm/v/vuejs-loadmore)](https://www.npmjs.com/package/vuejs-loadmore) [![Build Status](https://img.shields.io/github/workflow/status/staticdeng/vuejs-loadmore/Node.js%20CI)](https://github.com/staticdeng/vuejs-loadmore/actions) 3 | 4 | [![NPM](https://nodei.co/npm/vuejs-loadmore.png)](https://nodei.co/npm/vuejs-loadmore/) 5 | 6 | 一个Vue.js 的下拉刷新和上拉加载组件。 7 | 8 | 通过提供简单的api易于使用。与其他组件库不同,它使用浏览器本身而不是js来作滚动容器,因此它的代码量更小,但不损失用户体验。 9 | 10 | **中文** | [English](./README.en-US.md) 11 | 12 | ## 预览 13 | [在线demo](https://staticdeng.github.io/vuejs-loadmore/) 14 | 15 | 也可以扫描以下二维码访问演示: 16 | 17 | 18 | 19 | ## 安装 & 使用 20 | 21 | #### 安装 npm 包 22 | 23 | ```bash 24 | # npm 25 | npm install vuejs-loadmore --save 26 | ``` 27 | 28 | #### 全局导入 29 | 30 | ```js 31 | import Vue from 'vue'; 32 | import VueLoadmore from 'vuejs-loadmore'; 33 | 34 | Vue.use(VueLoadmore); 35 | ``` 36 | 37 | ## 国际化支持 38 | 39 | 支持中文 zh-CN 和英文 en-US, 默认为 zh-CN。 40 | 41 | ```js 42 | import VueLoadmore from 'vuejs-loadmore'; 43 | 44 | Vue.use(VueLoadmore, { 45 | lang: 'en-US' 46 | }) 47 | ``` 48 | 49 | 你也可以使用 `locale.use()` 指定语言。 50 | 51 | ```js 52 | import VueLoadmore, { locale } from 'vuejs-loadmore'; 53 | 54 | Vue.use(VueLoadmore); 55 | locale.use('en-US'); 56 | ``` 57 | 58 | ## 用法 59 | 60 | ### 基础用法 61 | 62 | ```html 63 | 67 |
68 |
69 | ``` 70 | `on-refresh` 和 `on-loadmore` 会在下拉刷新或滚动到底部时触发,需要在处理完数据请求后执行回调函数 `done()`。 71 | 72 | 如果你不需要刷新,只需要不绑定`on-refresh`。 73 | 74 | 当数据请求完成后,你可以将`finished`的数据改为true,这样就会显示`没有更多了`。 75 | 76 | ```js 77 | export default { 78 | data() { 79 | return { 80 | list: [], 81 | page: 1, 82 | pageSize: 10, 83 | finished: false 84 | }; 85 | }, 86 | mounted() { 87 | this.fetch(); 88 | }, 89 | methods: { 90 | onRefresh(done) { 91 | this.list = []; 92 | this.page = 1; 93 | this.finished = false; 94 | this.fetch(); 95 | 96 | done(); 97 | }, 98 | 99 | onLoad(done) { 100 | if (this.page <= 10) { 101 | this.fetch(); 102 | } else { 103 | // all data loaded 104 | this.finished = true; 105 | } 106 | done(); 107 | }, 108 | 109 | fetch() { 110 | for (let i = 0; i < this.pageSize; i++) { 111 | this.list.push(this.list.length + 1); 112 | } 113 | this.page++; 114 | } 115 | }, 116 | } 117 | ``` 118 | 119 | ### 错误提示 120 | 121 | ```html 122 | 126 |
127 |
128 | ``` 129 | 130 | ```js 131 | export default { 132 | data() { 133 | return { 134 | list: [], 135 | finished: false, 136 | error: false, 137 | }; 138 | }, 139 | methods: { 140 | onLoad() { 141 | fetchSomeThing().catch(() => { 142 | this.error = true; 143 | }); 144 | }, 145 | }, 146 | }; 147 | ``` 148 | 149 | ## API 150 | 151 | ### Props 152 | 153 | | Attribute | Description | Type | Default | 154 | | --- | --- | --- | --- | 155 | | on-refresh | 顶部下拉触发 | _function_ | - | 156 | | pulling-text | 下拉显示文本 | _string_ | `下拉刷新` | 157 | | loosing-text | 释放显示文本 | _string_ | `释放刷新` | 158 | | refresh-text | 正在刷新显示文本 | _string_ | `正在刷新` | 159 | | success-text | 刷新完成显示文本 | _string_ | `刷新完成` | 160 | | show-success-text | 是否显示`success-text` | _boolean_ | `true` | 161 | | pull-distance | 触发正在刷新状态的距离 | _number \| string_ | `50` | 162 | | head-height | 正在刷新显示区域的高度 | _number \| string_ | `50` | 163 | | animation-duration | 下拉刷新动画持续时间 | _number \| string_ | `200` | 164 | | on-loadmore | 滚动到底部触发 | _function_ | - | 165 | | immediate-check | 是否立即触发数据加载;默认是,否的话则自己定义触发数据加载时机 | _boolean_ | `true` | 166 | | load-offset | 当滚动条到底部的距离小于 `load-offset` 时,会发出 `on-loadmore` | _number \| string_ | `50` | 167 | | finished | 数据是否加载完毕,改变为true,则会显示`finished-text` | _boolean_ | `false` | 168 | | error | 数据是否加载错误,`on-loadmore`只有在点击错误文本时才会触发,需要`sync`修饰符 | _boolean_ | `false` | 169 | | loading-text | 滚动到底部正在加载显示文本 | _string_ | `正在加载` | 170 | | finished-text | 滚动到底部加载完毕的显示文本 | _string_ | `没有更多了` | 171 | | error-text | 加载错误显示文本 | _string_ | `请求失败,点击重新加载` | 172 | 173 | ### 方法 174 | 175 | 使用 ref 获取 List 实例并调用实例方法。 176 | 177 | | Name | Description | 178 | | ----- | --------------------- | 179 | | checkScroll | 检查当前的滚动位置,若已滚动至底部,则会触发 `on-loadmore` | 180 | 181 | 182 | ## 例子 183 | 184 | 查看demo以快速了解如何使用此包。 185 | 186 | ```bash 187 | git clone git@github.com:staticdeng/vuejs-loadmore.git 188 | yarn install 189 | yarn example:dev 190 | ``` -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env" 5 | ] 6 | ] 7 | } -------------------------------------------------------------------------------- /example/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | not dead 4 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # vuejs-loadmore-example 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Customize configuration 19 | See [Configuration Reference](https://cli.vuejs.org/config/). 20 | -------------------------------------------------------------------------------- /example/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuejs-loadmore", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "build": "vue-cli-service build" 8 | }, 9 | "dependencies": { 10 | "core-js": "^3.6.5", 11 | "vue": "^2.6.11" 12 | }, 13 | "devDependencies": { 14 | "@vue/cli-plugin-babel": "~4.5.0", 15 | "@vue/cli-service": "~4.5.0", 16 | "node-sass": "5.0.0", 17 | "sass-loader": "10.1.1", 18 | "vue-template-compiler": "^2.6.11" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/staticdeng/vuejs-loadmore/cc56adaa1afbae4ae7277596990eefbcf5b7bf94/example/public/favicon.ico -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/src/App.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 94 | 95 | 144 | -------------------------------------------------------------------------------- /example/src/assets/reset.scss: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | div, 4 | span, 5 | applet, 6 | object, 7 | iframe, 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6, 14 | p, 15 | blockquote, 16 | pre, 17 | a, 18 | abbr, 19 | acronym, 20 | address, 21 | big, 22 | cite, 23 | code, 24 | del, 25 | dfn, 26 | em, 27 | img, 28 | ins, 29 | kbd, 30 | q, 31 | s, 32 | samp, 33 | small, 34 | strike, 35 | strong, 36 | sub, 37 | sup, 38 | tt, 39 | var, 40 | b, 41 | u, 42 | i, 43 | center, 44 | dl, 45 | dt, 46 | dd, 47 | ol, 48 | ul, 49 | li, 50 | fieldset, 51 | form, 52 | label, 53 | legend, 54 | table, 55 | caption, 56 | tbody, 57 | tfoot, 58 | thead, 59 | tr, 60 | th, 61 | td, 62 | article, 63 | aside, 64 | canvas, 65 | details, 66 | embed, 67 | figure, 68 | figcaption, 69 | footer, 70 | header, 71 | hgroup, 72 | menu, 73 | nav, 74 | output, 75 | ruby, 76 | section, 77 | summary, 78 | time, 79 | mark, 80 | audio, 81 | video { 82 | margin: 0; 83 | padding: 0; 84 | font: inherit; 85 | font-size: 100%; 86 | vertical-align: baseline; 87 | border: 0; 88 | } 89 | 90 | html { 91 | line-height: 1; 92 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 93 | } 94 | 95 | ol, 96 | ul { 97 | list-style: none; 98 | } 99 | 100 | table { 101 | border-collapse: collapse; 102 | border-spacing: 0; 103 | } 104 | 105 | caption, 106 | th, 107 | td { 108 | font-weight: normal; 109 | vertical-align: middle; 110 | } 111 | 112 | * { 113 | box-sizing: content-box; 114 | } 115 | 116 | body { 117 | color: #323233; 118 | background-color: #f7f8fa; 119 | } 120 | 121 | 122 | button, 123 | input[type='number'], 124 | input[type='text'], 125 | input[type='password'], 126 | input[type='email'], 127 | input[type='search'], 128 | select, 129 | textarea { 130 | margin: 0; 131 | font-family: inherit; 132 | -webkit-appearance: none; 133 | } 134 | -------------------------------------------------------------------------------- /example/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueLoadmore, { locale } from '../../packages/index' 3 | import App from './App.vue' 4 | 5 | Vue.config.productionTip = false 6 | 7 | Vue.use(VueLoadmore, { 8 | lang: 'en-US' 9 | }) 10 | // Vue.use(VueLoadmore); 11 | // locale.use('en-US'); 12 | 13 | new Vue({ 14 | render: h => h(App), 15 | }).$mount('#app') 16 | -------------------------------------------------------------------------------- /example/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: '/vuejs-loadmore', 3 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "testMatch": ["**/test/*.spec.[jt]s?(x)"], // Jest 测试的文件 3 | 'moduleFileExtensions': [ 4 | 'js', 5 | // 告诉 Jest 处理 `*.vue` 文件 6 | 'vue' 7 | ], 8 | 'transform': { 9 | // 用 `vue-jest` 处理 `*.vue` 文件 10 | '.*\\.(vue)$': 'vue-jest', 11 | // 用 `babel-jest` 处理 js 12 | '.*\\.(js)$': 'babel-jest' 13 | } 14 | } -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("vue")):"function"==typeof define&&define.amd?define(["exports","vue"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).loadmore={},t.vue)}(this,(function(t,e){"use strict";function i(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var n=i(e);function o(t,e,i){var n=arguments.length>3&&void 0!==arguments[3]&&arguments[3];t.addEventListener(e,i,!!n&&{capture:!1,passive:n})}function s(t,e,i){t.removeEventListener(e,i)}var r={data:function(){return{direction:""}},methods:{bindTouchEvent:function(t){var e=this.onTouchStart,i=this.onTouchMove,n=this.onTouchEnd;o(t,"touchstart",e),o(t,"touchmove",i),n&&(o(t,"touchend",n),o(t,"touchcancel",n))},touchStart:function(t){this.resetTouchStatus(),this.startX=t.touches[0].clientX,this.startY=t.touches[0].clientY},touchMove:function(t){var e,i,n,o,s=t.touches[0];this.deltaX=s.clientX<0?0:s.clientX-this.startX,this.deltaY=s.clientY-this.startY,this.direction=this.direction||(e=this.deltaX,i=this.deltaY,n=Math.abs(e),o=Math.abs(i),n>o&&n>10?"horizontal":o>n&&o>10?"vertical":"")},resetTouchStatus:function(){this.direction="",this.deltaX=0,this.deltaY=0}}};var a={data:function(){return{timer:null}},methods:{timeout:function(t,e){clearTimeout(this.timer),setTimeout((function(){"function"==typeof t&&t()}),e)}},beforeDestroy:function(){clearTimeout(this.timer)}},l=/scroll|auto/i;function c(t){for(var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:window,i=t;i&&"HTML"!==i.tagName&&"BODY"!==i.tagName&&1===i.nodeType&&i!==e;){var n=window.getComputedStyle(i),o=n.overflowY;if(l.test(o))return i;i=i.parentNode}return e}var u={"zh-CN":{refresh:{pulling:"下拉刷新",loosing:"释放刷新",refresh:"正在刷新",success:"刷新完成"},loadmore:{loading:"正在加载",finished:"没有更多了",error:"请求失败,点击重新加载"}},"en-US":{refresh:{pulling:"Pull down to refresh",loosing:"Loosing to refresh",refresh:"Refreshing",success:"Refresh success"},loadmore:{loading:"Loading",finished:"No more data",error:"Request failed, click to reload"}}},d=n.default.prototype;(0,n.default.util.defineReactive)(d,"lang","zh-CN");var h={t:function(t){return function(t,e){var i=t;return e.split(".").forEach((function(t){var e;i=null!==(e=i[t])&&void 0!==e?e:""})),i}(u[d.lang],t)},use:function(t){u[t]&&(d.lang=t)}};var f=function(t,e,i,n,o,s,r,a,l,c){"boolean"!=typeof r&&(l=a,a=r,r=!1);var u,d="function"==typeof i?i.options:i;if(t&&t.render&&(d.render=t.render,d.staticRenderFns=t.staticRenderFns,d._compiled=!0,o&&(d.functional=!0)),n&&(d._scopeId=n),s?(u=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),e&&e.call(this,l(t)),t&&t._registeredComponents&&t._registeredComponents.add(s)},d._ssrRegister=u):e&&(u=r?function(t){e.call(this,c(t,this.$root.$options.shadowRoot))}:function(t){e.call(this,a(t))}),u)if(d.functional){var h=d.render;d.render=function(t,e){return u.call(e),h(t,e)}}else{var f=d.beforeCreate;d.beforeCreate=f?[].concat(f,u):[u]}return i},v={name:"loading"},p=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"vuejs-loading vuejs-loading-circular"},[i("span",{staticClass:"vuejs-loading-spinner vuejs-loading-spinner-circular"},[i("svg",{staticClass:"vuejs-loading-circular",attrs:{viewBox:"25 25 50 50"}},[i("circle",{attrs:{cx:"50",cy:"50",r:"20",fill:"none"}})])]),t._v(" "),i("span",{staticClass:"vuejs-loading-text"},[t._t("default")],2)])};p._withStripped=!0;var g=f({render:p,staticRenderFns:[]},undefined,v,undefined,false,undefined,!1,void 0,void 0,void 0),m=["pulling","loosing","refresh","success"],y={name:"loadmore",mixins:[r,function(t){function e(){t.call(this,o)}function i(){t.call(this,s)}return{mounted:e,activated:e,deactivated:i,beforeDestroy:i}}((function(t){var e,i,n,o,s,r,a,l;this.scroller||(this.scroller=c(this.$el)),t(this.scroller,"scroll",(e=this.checkSroll,i=200,l=function(){e.apply(r,a),o=n},function(){if(r=this,a=arguments,n=Date.now(),s&&(clearTimeout(s),s=null),o){var t=i-(n-o);t<0?l():s=setTimeout((function(){l()}),t)}else l()}))})),a],components:{Loading:g},props:{onRefresh:Function,pullingText:{type:String},loosingText:{type:String},refreshText:{type:String},successText:{type:String},showSuccessText:{type:Boolean,default:!0},pullDistance:{type:[Number,String],default:50},headHeight:{type:[Number,String],default:50},animationDuration:{type:[Number,String],default:200},onLoadmore:Function,immediateCheck:{type:Boolean,default:!1},loadOffset:{type:[Number,String],default:50},finished:Boolean,error:Boolean,loadingText:{type:String},finishedText:{type:String},errorText:{type:String}},data:function(){return{status:"normal",distance:0,duration:0,scroller:null,loadLoading:!1}},mounted:function(){this.bindTouchEvent(this.$refs.track),this.scroller=c(this.$el),this.immediateCheck&&this.checkSroll()},computed:{touchable:function(){return"refresh"!==this.status&&"success"!==this.status&&this.onRefresh},headStyle:function(){return 50!==this.headHeight?{height:"".concat(this.headHeight,"px")}:{}},genStatus:function(){var t=this.status,e=this["".concat(t,"Text")]||h.t("refresh.".concat(t));return-1!==m.indexOf(t)?e:""}},methods:{t:h.t,checkPullStart:function(t){var e,i;this.ceiling=0===(e=this.scroller,i="scrollTop"in e?e.scrollTop:e.pageYOffset,Math.max(i,0)),this.ceiling&&(this.duration=0,this.touchStart(t))},onTouchStart:function(t){this.touchable&&this.checkPullStart(t)},onTouchMove:function(t){this.touchable&&(this.ceiling||this.checkPullStart(t),this.touchMove(t),this.ceiling&&this.deltaY>=0&&"vertical"===this.direction&&(!function(t){("boolean"!=typeof t.cancelable||t.cancelable)&&t.preventDefault()}(t),this.setStatus(this.ease(this.deltaY))))},onTouchEnd:function(){var t=this;this.deltaY&&this.touchable&&(this.duration=this.animationDuration,"loosing"===this.status?(this.showRefreshTip(),this.$nextTick((function(){t.onRefresh(t.refreshDone)}))):this.setStatus(0))},ease:function(t){var e=+(this.pullDistance||this.headHeight);return t>e&&(t=t<2*e?e+(t-e)/2:1.5*e+(t-2*e)/4),Math.round(t)},setStatus:function(t){var e,i=arguments.length>1&&void 0!==arguments[1]&&arguments[1];e=i?"refresh":0===t?"normal":t<(this.pullDistance||this.headHeight)?"pulling":"loosing",this.distance=t,e!==this.status&&(this.status=e)},refreshDone:function(){var t=this;this.showSuccessText?this.timeout(this.showSuccessTip,500):this.timeout((function(){return t.setStatus(0)}),500)},showRefreshTip:function(){this.setStatus(+this.headHeight,!0)},showSuccessTip:function(){var t=this;this.status="success",this.timeout((function(){return t.setStatus(0)}),1e3)},checkSroll:function(){var t=this;this.$nextTick((function(){if(!t.loadLoading&&t.onLoadmore&&!t.finished&&!t.error){var e,i=t.scroller,n=t.loadOffset,o=(e=i.getBoundingClientRect?i.getBoundingClientRect():{top:0,bottom:i.innerHeight}).bottom-e.top,s=t.$refs.placeholder;if(!o||!s)return!1;var r=s.getBoundingClientRect();Math.abs(r.bottom-e.bottom)<=n&&(t.loadLoading=!0,t.timeout((function(){return t.onLoadmore(t.loadmoreDone)}),500))}}))},clickErrorText:function(){var t=this;this.$emit("update:error",!1),this.loadLoading=!0,this.timeout((function(){return t.onLoadmore(t.loadmoreDone)}),500)},loadmoreDone:function(){this.loadLoading=!1}}},x=y,k=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"vuejs-loadmore-wrap"},[i("div",{ref:"track",staticClass:"vuejs-refresh-track",style:{transform:t.distance?"translate3d(0, "+t.distance+"px, 0)":"",webkitTransform:t.distance?"translate3d(0, "+t.distance+"px, 0)":"",transitionDuration:t.duration+"ms"}},[i("div",{staticClass:"vuejs-refresh-head",style:t.headStyle},["refresh"===t.status?i("div",[i("Loading",[t._v(t._s(t.genStatus))])],1):i("div",{staticClass:"vuejs-refresh-text"},[t._v(t._s(t.genStatus))])]),t._v(" "),t._t("default"),t._v(" "),i("div",{ref:"placeholder",staticClass:"vuejs-loadmore"},[!t.loadLoading||t.finished||t.error?t._e():i("div",{staticClass:"vuejs-loadmore-loading"},[i("Loading",[t._v(t._s(t.loadingText||t.t("loadmore.loading")))])],1),t._v(" "),t.finished?i("div",{staticClass:"vuejs-loadmore-finished-text"},[t._v("\n "+t._s(t.finishedText||t.t("loadmore.finished"))+"\n ")]):t._e(),t._v(" "),t.error?i("div",{staticClass:"vuejs-loadmore-error-text",on:{click:t.clickErrorText}},[t._v("\n "+t._s(t.errorText||t.t("loadmore.error"))+"\n ")]):t._e()])],2)])};k._withStripped=!0;var b=f({render:k,staticRenderFns:[]},undefined,x,undefined,false,undefined,!1,void 0,void 0,void 0);function T(t,e){void 0===e&&(e={});var i=e.insertAt;if(t&&"undefined"!=typeof document){var n=document.head||document.getElementsByTagName("head")[0],o=document.createElement("style");o.type="text/css","top"===i&&n.firstChild?n.insertBefore(o,n.firstChild):n.appendChild(o),o.styleSheet?o.styleSheet.cssText=t:o.appendChild(document.createTextNode(t))}}T(".vuejs-loadmore-wrap{-webkit-user-select:none;user-select:none}.vuejs-refresh-track{height:100%;position:relative;-webkit-transition-property:-webkit-transform;transition-property:-webkit-transform;transition-property:transform;transition-property:transform,-webkit-transform}.vuejs-refresh-head{-webkit-box-pack:center;-webkit-box-align:center;-webkit-align-items:center;align-items:center;color:#646566;display:-webkit-box;display:-webkit-flex;display:flex;font-size:14px;height:50px;-webkit-justify-content:center;justify-content:center;left:0;overflow:hidden;position:absolute;text-align:center;-webkit-transform:translateY(-100%);transform:translateY(-100%);width:100%}.vuejs-loadmore{height:36px}.vuejs-loadmore-error-text,.vuejs-loadmore-finished-text,.vuejs-loadmore-loading{color:#646566;font-size:14px;line-height:36px;text-align:center}");T(".vuejs-loading{color:#323233;font-size:0}.vuejs-loading,.vuejs-loading-spinner{position:relative;vertical-align:middle}.vuejs-loading-spinner{display:inline-block;height:20px;max-height:100%;max-width:100%;width:20px}.vuejs-loading-spinner-circular{-webkit-animation-duration:2s;animation-duration:2s}.vuejs-loading-circular{display:block;height:100%;width:100%}.vuejs-loading-circular circle{stroke:currentColor;stroke-width:3;stroke-linecap:round;-webkit-animation:vuejs-circular 1.5s ease-in-out infinite;animation:vuejs-circular 1.5s ease-in-out infinite}.vuejs-loading-text{color:#646566;display:inline-block;font-size:14px;margin-left:8px;vertical-align:middle}@-webkit-keyframes vuejs-circular{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40}to{stroke-dasharray:90,150;stroke-dashoffset:-120}}@keyframes vuejs-circular{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40}to{stroke-dasharray:90,150;stroke-dashoffset:-120}}");var S={install:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};t.component("vue-loadmore",b),h.use(e.lang)}};t.default=S,t.locale=h,Object.defineProperty(t,"__esModule",{value:!0})})); 2 | -------------------------------------------------------------------------------- /lib/index.module.js: -------------------------------------------------------------------------------- 1 | import t from"vue";function e(t,e,i){var n=arguments.length>3&&void 0!==arguments[3]&&arguments[3];t.addEventListener(e,i,!!n&&{capture:!1,passive:n})}function i(t,e,i){t.removeEventListener(e,i)}var n={data:function(){return{direction:""}},methods:{bindTouchEvent:function(t){var i=this.onTouchStart,n=this.onTouchMove,s=this.onTouchEnd;e(t,"touchstart",i),e(t,"touchmove",n),s&&(e(t,"touchend",s),e(t,"touchcancel",s))},touchStart:function(t){this.resetTouchStatus(),this.startX=t.touches[0].clientX,this.startY=t.touches[0].clientY},touchMove:function(t){var e,i,n,s,o=t.touches[0];this.deltaX=o.clientX<0?0:o.clientX-this.startX,this.deltaY=o.clientY-this.startY,this.direction=this.direction||(e=this.deltaX,i=this.deltaY,n=Math.abs(e),s=Math.abs(i),n>s&&n>10?"horizontal":s>n&&s>10?"vertical":"")},resetTouchStatus:function(){this.direction="",this.deltaX=0,this.deltaY=0}}};var s={data:function(){return{timer:null}},methods:{timeout:function(t,e){clearTimeout(this.timer),setTimeout((function(){"function"==typeof t&&t()}),e)}},beforeDestroy:function(){clearTimeout(this.timer)}},o=/scroll|auto/i;function r(t){for(var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:window,i=t;i&&"HTML"!==i.tagName&&"BODY"!==i.tagName&&1===i.nodeType&&i!==e;){var n=window.getComputedStyle(i),s=n.overflowY;if(o.test(s))return i;i=i.parentNode}return e}var a={"zh-CN":{refresh:{pulling:"下拉刷新",loosing:"释放刷新",refresh:"正在刷新",success:"刷新完成"},loadmore:{loading:"正在加载",finished:"没有更多了",error:"请求失败,点击重新加载"}},"en-US":{refresh:{pulling:"Pull down to refresh",loosing:"Loosing to refresh",refresh:"Refreshing",success:"Refresh success"},loadmore:{loading:"Loading",finished:"No more data",error:"Request failed, click to reload"}}},l=t.prototype;(0,t.util.defineReactive)(l,"lang","zh-CN");var c={t:function(t){return function(t,e){var i=t;return e.split(".").forEach((function(t){var e;i=null!==(e=i[t])&&void 0!==e?e:""})),i}(a[l.lang],t)},use:function(t){a[t]&&(l.lang=t)}};var u=function(t,e,i,n,s,o,r,a,l,c){"boolean"!=typeof r&&(l=a,a=r,r=!1);var u,d="function"==typeof i?i.options:i;if(t&&t.render&&(d.render=t.render,d.staticRenderFns=t.staticRenderFns,d._compiled=!0,s&&(d.functional=!0)),n&&(d._scopeId=n),o?(u=function(t){(t=t||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext)||"undefined"==typeof __VUE_SSR_CONTEXT__||(t=__VUE_SSR_CONTEXT__),e&&e.call(this,l(t)),t&&t._registeredComponents&&t._registeredComponents.add(o)},d._ssrRegister=u):e&&(u=r?function(t){e.call(this,c(t,this.$root.$options.shadowRoot))}:function(t){e.call(this,a(t))}),u)if(d.functional){var h=d.render;d.render=function(t,e){return u.call(e),h(t,e)}}else{var f=d.beforeCreate;d.beforeCreate=f?[].concat(f,u):[u]}return i},d={name:"loading"},h=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"vuejs-loading vuejs-loading-circular"},[i("span",{staticClass:"vuejs-loading-spinner vuejs-loading-spinner-circular"},[i("svg",{staticClass:"vuejs-loading-circular",attrs:{viewBox:"25 25 50 50"}},[i("circle",{attrs:{cx:"50",cy:"50",r:"20",fill:"none"}})])]),t._v(" "),i("span",{staticClass:"vuejs-loading-text"},[t._t("default")],2)])};h._withStripped=!0;var f=u({render:h,staticRenderFns:[]},undefined,d,undefined,false,undefined,!1,void 0,void 0,void 0),v=["pulling","loosing","refresh","success"],p={name:"loadmore",mixins:[n,function(t){function n(){t.call(this,e)}function s(){t.call(this,i)}return{mounted:n,activated:n,deactivated:s,beforeDestroy:s}}((function(t){var e,i,n,s,o,a,l,c;this.scroller||(this.scroller=r(this.$el)),t(this.scroller,"scroll",(e=this.checkSroll,i=200,c=function(){e.apply(a,l),s=n},function(){if(a=this,l=arguments,n=Date.now(),o&&(clearTimeout(o),o=null),s){var t=i-(n-s);t<0?c():o=setTimeout((function(){c()}),t)}else c()}))})),s],components:{Loading:f},props:{onRefresh:Function,pullingText:{type:String},loosingText:{type:String},refreshText:{type:String},successText:{type:String},showSuccessText:{type:Boolean,default:!0},pullDistance:{type:[Number,String],default:50},headHeight:{type:[Number,String],default:50},animationDuration:{type:[Number,String],default:200},onLoadmore:Function,immediateCheck:{type:Boolean,default:!1},loadOffset:{type:[Number,String],default:50},finished:Boolean,error:Boolean,loadingText:{type:String},finishedText:{type:String},errorText:{type:String}},data:function(){return{status:"normal",distance:0,duration:0,scroller:null,loadLoading:!1}},mounted:function(){this.bindTouchEvent(this.$refs.track),this.scroller=r(this.$el),this.immediateCheck&&this.checkSroll()},computed:{touchable:function(){return"refresh"!==this.status&&"success"!==this.status&&this.onRefresh},headStyle:function(){return 50!==this.headHeight?{height:"".concat(this.headHeight,"px")}:{}},genStatus:function(){var t=this.status,e=this["".concat(t,"Text")]||c.t("refresh.".concat(t));return-1!==v.indexOf(t)?e:""}},methods:{t:c.t,checkPullStart:function(t){var e,i;this.ceiling=0===(e=this.scroller,i="scrollTop"in e?e.scrollTop:e.pageYOffset,Math.max(i,0)),this.ceiling&&(this.duration=0,this.touchStart(t))},onTouchStart:function(t){this.touchable&&this.checkPullStart(t)},onTouchMove:function(t){this.touchable&&(this.ceiling||this.checkPullStart(t),this.touchMove(t),this.ceiling&&this.deltaY>=0&&"vertical"===this.direction&&(!function(t){("boolean"!=typeof t.cancelable||t.cancelable)&&t.preventDefault()}(t),this.setStatus(this.ease(this.deltaY))))},onTouchEnd:function(){var t=this;this.deltaY&&this.touchable&&(this.duration=this.animationDuration,"loosing"===this.status?(this.showRefreshTip(),this.$nextTick((function(){t.onRefresh(t.refreshDone)}))):this.setStatus(0))},ease:function(t){var e=+(this.pullDistance||this.headHeight);return t>e&&(t=t<2*e?e+(t-e)/2:1.5*e+(t-2*e)/4),Math.round(t)},setStatus:function(t){var e,i=arguments.length>1&&void 0!==arguments[1]&&arguments[1];e=i?"refresh":0===t?"normal":t<(this.pullDistance||this.headHeight)?"pulling":"loosing",this.distance=t,e!==this.status&&(this.status=e)},refreshDone:function(){var t=this;this.showSuccessText?this.timeout(this.showSuccessTip,500):this.timeout((function(){return t.setStatus(0)}),500)},showRefreshTip:function(){this.setStatus(+this.headHeight,!0)},showSuccessTip:function(){var t=this;this.status="success",this.timeout((function(){return t.setStatus(0)}),1e3)},checkSroll:function(){var t=this;this.$nextTick((function(){if(!t.loadLoading&&t.onLoadmore&&!t.finished&&!t.error){var e,i=t.scroller,n=t.loadOffset,s=(e=i.getBoundingClientRect?i.getBoundingClientRect():{top:0,bottom:i.innerHeight}).bottom-e.top,o=t.$refs.placeholder;if(!s||!o)return!1;var r=o.getBoundingClientRect();Math.abs(r.bottom-e.bottom)<=n&&(t.loadLoading=!0,t.timeout((function(){return t.onLoadmore(t.loadmoreDone)}),500))}}))},clickErrorText:function(){var t=this;this.$emit("update:error",!1),this.loadLoading=!0,this.timeout((function(){return t.onLoadmore(t.loadmoreDone)}),500)},loadmoreDone:function(){this.loadLoading=!1}}},m=function(){var t=this,e=t.$createElement,i=t._self._c||e;return i("div",{staticClass:"vuejs-loadmore-wrap"},[i("div",{ref:"track",staticClass:"vuejs-refresh-track",style:{transform:t.distance?"translate3d(0, "+t.distance+"px, 0)":"",webkitTransform:t.distance?"translate3d(0, "+t.distance+"px, 0)":"",transitionDuration:t.duration+"ms"}},[i("div",{staticClass:"vuejs-refresh-head",style:t.headStyle},["refresh"===t.status?i("div",[i("Loading",[t._v(t._s(t.genStatus))])],1):i("div",{staticClass:"vuejs-refresh-text"},[t._v(t._s(t.genStatus))])]),t._v(" "),t._t("default"),t._v(" "),i("div",{ref:"placeholder",staticClass:"vuejs-loadmore"},[!t.loadLoading||t.finished||t.error?t._e():i("div",{staticClass:"vuejs-loadmore-loading"},[i("Loading",[t._v(t._s(t.loadingText||t.t("loadmore.loading")))])],1),t._v(" "),t.finished?i("div",{staticClass:"vuejs-loadmore-finished-text"},[t._v("\n "+t._s(t.finishedText||t.t("loadmore.finished"))+"\n ")]):t._e(),t._v(" "),t.error?i("div",{staticClass:"vuejs-loadmore-error-text",on:{click:t.clickErrorText}},[t._v("\n "+t._s(t.errorText||t.t("loadmore.error"))+"\n ")]):t._e()])],2)])};m._withStripped=!0;var g=u({render:m,staticRenderFns:[]},undefined,p,undefined,false,undefined,!1,void 0,void 0,void 0);function x(t,e){void 0===e&&(e={});var i=e.insertAt;if(t&&"undefined"!=typeof document){var n=document.head||document.getElementsByTagName("head")[0],s=document.createElement("style");s.type="text/css","top"===i&&n.firstChild?n.insertBefore(s,n.firstChild):n.appendChild(s),s.styleSheet?s.styleSheet.cssText=t:s.appendChild(document.createTextNode(t))}}x(".vuejs-loadmore-wrap{-webkit-user-select:none;user-select:none}.vuejs-refresh-track{height:100%;position:relative;-webkit-transition-property:-webkit-transform;transition-property:-webkit-transform;transition-property:transform;transition-property:transform,-webkit-transform}.vuejs-refresh-head{-webkit-box-pack:center;-webkit-box-align:center;-webkit-align-items:center;align-items:center;color:#646566;display:-webkit-box;display:-webkit-flex;display:flex;font-size:14px;height:50px;-webkit-justify-content:center;justify-content:center;left:0;overflow:hidden;position:absolute;text-align:center;-webkit-transform:translateY(-100%);transform:translateY(-100%);width:100%}.vuejs-loadmore{height:36px}.vuejs-loadmore-error-text,.vuejs-loadmore-finished-text,.vuejs-loadmore-loading{color:#646566;font-size:14px;line-height:36px;text-align:center}");x(".vuejs-loading{color:#323233;font-size:0}.vuejs-loading,.vuejs-loading-spinner{position:relative;vertical-align:middle}.vuejs-loading-spinner{display:inline-block;height:20px;max-height:100%;max-width:100%;width:20px}.vuejs-loading-spinner-circular{-webkit-animation-duration:2s;animation-duration:2s}.vuejs-loading-circular{display:block;height:100%;width:100%}.vuejs-loading-circular circle{stroke:currentColor;stroke-width:3;stroke-linecap:round;-webkit-animation:vuejs-circular 1.5s ease-in-out infinite;animation:vuejs-circular 1.5s ease-in-out infinite}.vuejs-loading-text{color:#646566;display:inline-block;font-size:14px;margin-left:8px;vertical-align:middle}@-webkit-keyframes vuejs-circular{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40}to{stroke-dasharray:90,150;stroke-dashoffset:-120}}@keyframes vuejs-circular{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40}to{stroke-dasharray:90,150;stroke-dashoffset:-120}}");var k={install:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};t.component("vue-loadmore",g),c.use(e.lang)}};export{k as default,c as locale}; 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuejs-loadmore", 3 | "version": "1.0.8", 4 | "description": "A pull-down refresh and pull-up loadmore scroll component for Vue.js", 5 | "entry": "packages/index.js", 6 | "main": "lib/index.js", 7 | "module": "lib/index.module.js", 8 | "scripts": { 9 | "build": "yarn clean && rollup -c", 10 | "test": "jest", 11 | "lint": "eslint ./packages --ext .vue,.js,.ts", 12 | "lint-fix": "eslint --fix ./packages --ext .vue,.js,.ts", 13 | "clean": "rimraf ./lib", 14 | "example:dev": "cd example && yarn install && yarn serve", 15 | "example:build": "cd example && yarn install && yarn build" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git@github.com:staticdeng/vuejs-loadmore.git" 20 | }, 21 | "keywords": [ 22 | "vue-loadmore", 23 | "vue-refresh", 24 | "vue-srcoll", 25 | "loadmore", 26 | "refresh", 27 | "srcoll" 28 | ], 29 | "author": "staticdeng", 30 | "license": "MIT", 31 | "devDependencies": { 32 | "@babel/core": "^7.16.0", 33 | "@babel/preset-env": "^7.16.4", 34 | "@rollup/plugin-node-resolve": "^13.0.6", 35 | "@vue/eslint-config-standard": "^5.1.2", 36 | "@vue/test-utils": "1.0.0-beta.29", 37 | "autoprefixer": "^10.3.3", 38 | "babel-core": "^7.0.0-bridge.0", 39 | "babel-eslint": "^10.1.0", 40 | "babel-jest": "^26.0.1", 41 | "eslint": "^6.7.2", 42 | "eslint-plugin-import": "^2.20.2", 43 | "eslint-plugin-node": "^11.1.0", 44 | "eslint-plugin-promise": "^4.2.1", 45 | "eslint-plugin-standard": "^4.0.0", 46 | "eslint-plugin-vue": "^6.2.2", 47 | "jest": "^25.5.4", 48 | "postcss": "^8.4.4", 49 | "rimraf": "^3.0.2", 50 | "rollup": "^2.60.2", 51 | "rollup-plugin-babel": "^4.4.0", 52 | "rollup-plugin-commonjs": "^10.1.0", 53 | "rollup-plugin-postcss": "^4.0.2", 54 | "rollup-plugin-terser": "^7.0.2", 55 | "rollup-plugin-vue": "^5.1.9", 56 | "vue": "^2.6.14", 57 | "vue-jest": "4.0.0-rc.0", 58 | "vue-template-compiler": "^2.6.14" 59 | }, 60 | "browserslist": [ 61 | "Android >= 4.0", 62 | "iOS >= 8" 63 | ], 64 | "bugs": { 65 | "url": "https://github.com/staticdeng/vuejs-loadmore/issues" 66 | }, 67 | "homepage": "https://github.com/staticdeng/vuejs-loadmore#readme" 68 | } 69 | -------------------------------------------------------------------------------- /packages/icon/index.js: -------------------------------------------------------------------------------- 1 | import loading from './loading.vue'; 2 | 3 | export default loading; 4 | -------------------------------------------------------------------------------- /packages/icon/loading.scss: -------------------------------------------------------------------------------- 1 | @import '../style/var'; 2 | .vuejs-loading { 3 | position: relative; 4 | color: $loading-spinner-color; 5 | font-size: 0; 6 | vertical-align: middle; 7 | 8 | &-spinner { 9 | position: relative; 10 | display: inline-block; 11 | width: $loading-spinner-size; 12 | // compatible for 1.x, users may set width or height in root element 13 | max-width: 100%; 14 | height: $loading-spinner-size; 15 | max-height: 100%; 16 | vertical-align: middle; 17 | 18 | &-circular { 19 | animation-duration: 2s; 20 | } 21 | } 22 | 23 | &-circular { 24 | display: block; 25 | width: 100%; 26 | height: 100%; 27 | 28 | circle { 29 | animation: vuejs-circular 1.5s ease-in-out infinite; 30 | stroke: currentColor; 31 | stroke-width: 3; 32 | stroke-linecap: round; 33 | } 34 | } 35 | 36 | &-text { 37 | display: inline-block; 38 | margin-left: $padding-xs; 39 | color: $loading-text-color; 40 | font-size: $loading-text-font-size; 41 | vertical-align: middle; 42 | } 43 | } 44 | @keyframes vuejs-circular { 45 | 0% { 46 | stroke-dasharray: 1, 200; 47 | stroke-dashoffset: 0; 48 | } 49 | 50 | 50% { 51 | stroke-dasharray: 90, 150; 52 | stroke-dashoffset: -40; 53 | } 54 | 55 | 100% { 56 | stroke-dasharray: 90, 150; 57 | stroke-dashoffset: -120; 58 | } 59 | } -------------------------------------------------------------------------------- /packages/icon/loading.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 19 | -------------------------------------------------------------------------------- /packages/index.js: -------------------------------------------------------------------------------- 1 | import VueLoadmore from './vuejs-loadmore/index'; 2 | import './vuejs-loadmore/index.scss'; 3 | import './icon/loading.scss'; 4 | import locale from './locale/index'; 5 | 6 | export default { 7 | install (Vue, options = {}) { 8 | Vue.component('vue-loadmore', VueLoadmore); 9 | 10 | locale.use(options.lang); 11 | } 12 | }; 13 | 14 | export { 15 | locale 16 | }; 17 | -------------------------------------------------------------------------------- /packages/locale/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import { getDeepVal } from '../utils/getDeepValByKey'; 3 | import zhCN from './lang/zh-CN'; 4 | import enUS from './lang/en-US'; 5 | 6 | const langLibrary = { 7 | 'zh-CN': zhCN, 8 | 'en-US': enUS 9 | }; 10 | 11 | const proto = Vue.prototype; 12 | const { defineReactive } = Vue.util; 13 | // 将proto.lang定义成响应式数据 14 | defineReactive(proto, 'lang', 'zh-CN'); 15 | 16 | const getLangLibrary = () => langLibrary[proto.lang]; 17 | 18 | export default { 19 | // 获取当前语言库的值 20 | t (path) { 21 | const library = getLangLibrary(); 22 | return getDeepVal(library, path); 23 | }, 24 | 25 | // 使用某个语言库(zhCN/enUS) 26 | use (lang) { 27 | if (langLibrary[lang]) { 28 | proto.lang = lang; 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /packages/locale/lang/en-US.js: -------------------------------------------------------------------------------- 1 | export default { 2 | refresh: { 3 | pulling: 'Pull down to refresh', 4 | loosing: 'Loosing to refresh', 5 | refresh: 'Refreshing', 6 | success: 'Refresh success' 7 | }, 8 | loadmore: { 9 | loading: 'Loading', 10 | finished: 'No more data', 11 | error: 'Request failed, click to reload' 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /packages/locale/lang/zh-CN.js: -------------------------------------------------------------------------------- 1 | export default { 2 | refresh: { 3 | pulling: '下拉刷新', 4 | loosing: '释放刷新', 5 | refresh: '正在刷新', 6 | success: '刷新完成' 7 | }, 8 | loadmore: { 9 | loading: '正在加载', 10 | finished: '没有更多了', 11 | error: '请求失败,点击重新加载' 12 | } 13 | }; 14 | -------------------------------------------------------------------------------- /packages/mixins/bind-event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * event事件绑定和取消 3 | */ 4 | 5 | import { on, off } from '../utils/event'; 6 | 7 | export function BindEventMixin (eventFn) { 8 | function bind () { 9 | eventFn.call(this, on); 10 | } 11 | 12 | function unbind () { 13 | eventFn.call(this, off); 14 | } 15 | return { 16 | mounted: bind, 17 | activated: bind, 18 | deactivated: unbind, 19 | beforeDestroy: unbind 20 | }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/mixins/timer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * timeout mixin 3 | */ 4 | 5 | export const TimeoutMixin = { 6 | data () { 7 | return { 8 | timer: null 9 | }; 10 | }, 11 | methods: { 12 | timeout (fn, time) { 13 | clearTimeout(this.timer); 14 | setTimeout(() => { 15 | typeof fn === 'function' && fn(); 16 | }, time); 17 | } 18 | }, 19 | beforeDestroy () { 20 | clearTimeout(this.timer); 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /packages/mixins/touch.js: -------------------------------------------------------------------------------- 1 | /** 2 | * touch相关 3 | */ 4 | 5 | import { on } from '../utils/event'; 6 | 7 | function getDirection (deltaX, deltaY) { 8 | const MIN_DISTANCE = 10; 9 | const x = Math.abs(deltaX); 10 | const y = Math.abs(deltaY); 11 | if (x > y && x > MIN_DISTANCE) { 12 | return 'horizontal'; 13 | } 14 | if (y > x && y > MIN_DISTANCE) { 15 | return 'vertical'; 16 | } 17 | return ''; 18 | } 19 | 20 | export const TouchMixin = { 21 | data () { 22 | return { direction: '' }; 23 | }, 24 | methods: { 25 | // 绑定touch事件 26 | bindTouchEvent (el) { 27 | const { onTouchStart, onTouchMove, onTouchEnd } = this; 28 | 29 | on(el, 'touchstart', onTouchStart); 30 | on(el, 'touchmove', onTouchMove); 31 | 32 | if (onTouchEnd) { 33 | on(el, 'touchend', onTouchEnd); 34 | on(el, 'touchcancel', onTouchEnd); 35 | } 36 | }, 37 | // touchStart 38 | touchStart (event) { 39 | this.resetTouchStatus(); 40 | this.startX = event.touches[0].clientX; 41 | this.startY = event.touches[0].clientY; 42 | }, 43 | // touchmove 44 | touchMove (event) { 45 | const touch = event.touches[0]; 46 | // Fix: Safari back will set clientX to negative number 47 | this.deltaX = touch.clientX < 0 ? 0 : touch.clientX - this.startX; 48 | this.deltaY = touch.clientY - this.startY; 49 | this.direction = this.direction || getDirection(this.deltaX, this.deltaY); 50 | }, 51 | // reset touch 52 | resetTouchStatus () { 53 | this.direction = ''; 54 | this.deltaX = 0; 55 | this.deltaY = 0; 56 | } 57 | } 58 | }; 59 | -------------------------------------------------------------------------------- /packages/style/var.scss: -------------------------------------------------------------------------------- 1 | $gray-1: #646566; 2 | $gray-2: #323233; 3 | $font-size-xs: 10px; 4 | $font-size-sm: 12px; 5 | $font-size-md: 14px; 6 | $font-size-lg: 20px; 7 | // refresh 8 | $refresh-head-height: 50px; 9 | $refresh-head-font-size: $font-size-md; 10 | $refresh-head-text-color: $gray-1; 11 | // loadmore 12 | $loadmore-text-color: $gray-1; 13 | $loadmore-text-font-size: $font-size-md; 14 | $loadmore-text-line-height: 36px; 15 | // icon 16 | $padding-base: 4px; 17 | $loading-spinner-color: $gray-2; 18 | $loading-spinner-size: $font-size-lg; 19 | $loading-spinner-animation-duration: 0.9s; 20 | $padding-xs: $padding-base * 2; 21 | $loading-text-color: $gray-1; 22 | $loading-text-font-size: $font-size-md; 23 | 24 | -------------------------------------------------------------------------------- /packages/utils/event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * dom事件 3 | */ 4 | export function on (target, event, handler, passive = false) { 5 | target.addEventListener( 6 | event, 7 | handler, 8 | passive ? { capture: false, passive } : false 9 | ); 10 | } 11 | 12 | export function off (target, event, handler) { 13 | target.removeEventListener(event, handler); 14 | } 15 | 16 | export function preventDefault (event) { 17 | if (typeof event.cancelable !== 'boolean' || event.cancelable) { 18 | event.preventDefault(); 19 | } 20 | }; 21 | -------------------------------------------------------------------------------- /packages/utils/getDeepValByKey.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 遍历对象,获取某个深遍历节点的值 3 | */ 4 | 5 | export function getDeepVal (object, path) { 6 | let result = object; 7 | const keys = path.split('.'); 8 | 9 | keys.forEach((key) => { 10 | result = result[key] ?? ''; 11 | }); 12 | 13 | return result; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/utils/scroll.js: -------------------------------------------------------------------------------- 1 | // get nearest scroll element 2 | const overflowScrollReg = /scroll|auto/i; 3 | 4 | // 遍历父级返回最近一个可滚动的父级元素(overflow-y: scroll和auto的元素),找不到则为window(不能为html,body) 5 | export function getScroller (el, root = window) { 6 | let node = el; 7 | 8 | while ( 9 | node && 10 | node.tagName !== 'HTML' && 11 | node.tagName !== 'BODY' && 12 | node.nodeType === 1 && 13 | node !== root 14 | ) { 15 | const { overflowY } = window.getComputedStyle(node); 16 | if (overflowScrollReg.test(overflowY)) { 17 | return node; 18 | } 19 | node = node.parentNode; 20 | } 21 | 22 | return root; 23 | } 24 | 25 | export function getScrollTop (el) { 26 | const top = 'scrollTop' in el ? el.scrollTop : el.pageYOffset; 27 | 28 | // iOS scroll bounce cause minus scrollTop 29 | return Math.max(top, 0); 30 | } 31 | -------------------------------------------------------------------------------- /packages/utils/throttle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 节流函数 3 | */ 4 | 5 | export function throttle (handle, wait) { 6 | var now, previous, timer, context, args; 7 | 8 | var execute = function () { 9 | handle.apply(context, args); 10 | previous = now; 11 | }; 12 | 13 | return function () { 14 | context = this; 15 | args = arguments; 16 | 17 | now = Date.now(); 18 | 19 | if (timer) { 20 | clearTimeout(timer); 21 | timer = null; 22 | } 23 | 24 | if (previous) { 25 | var diff = wait - (now - previous); 26 | if (diff < 0) { 27 | // 第一次触发可以立即响应 28 | execute(); 29 | } else { 30 | // 结束触发后也能有响应 31 | timer = setTimeout(() => { 32 | execute(); 33 | }, diff); 34 | } 35 | } else { 36 | execute(); 37 | } 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/vuejs-loadmore/index.scss: -------------------------------------------------------------------------------- 1 | @import '../style/var'; 2 | .vuejs-loadmore-wrap { 3 | user-select: none; 4 | } 5 | 6 | .vuejs-refresh { 7 | &-track { 8 | position: relative; 9 | height: 100%; 10 | transition-property: transform; 11 | } 12 | 13 | &-head { 14 | position: absolute; 15 | left: 0; 16 | width: 100%; 17 | height: $refresh-head-height; 18 | overflow: hidden; 19 | color: $refresh-head-text-color; 20 | font-size: $refresh-head-font-size; 21 | text-align: center; 22 | transform: translateY(-100%); 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | } 27 | } 28 | 29 | .vuejs-loadmore { 30 | height: $loadmore-text-line-height; 31 | &-loading, 32 | &-finished-text, 33 | &-error-text { 34 | color: $loadmore-text-color; 35 | font-size: $loadmore-text-font-size; 36 | line-height: $loadmore-text-line-height; 37 | text-align: center; 38 | } 39 | } -------------------------------------------------------------------------------- /packages/vuejs-loadmore/index.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 314 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import vue from 'rollup-plugin-vue'; 3 | import resolve from '@rollup/plugin-node-resolve'; 4 | import postcss from 'rollup-plugin-postcss'; 5 | import babel from 'rollup-plugin-babel'; 6 | import commonjs from 'rollup-plugin-commonjs'; 7 | import { terser } from 'rollup-plugin-terser'; 8 | 9 | const pkg = require(path.resolve(__dirname, 'package.json')); 10 | // 公共插件配置 11 | const getPlugins = () => { 12 | return [ 13 | resolve({ 14 | extensions: ['.vue', '.js'] 15 | }), 16 | vue({ 17 | include: /\.vue$/, 18 | normalizer: '~vue-runtime-helpers/dist/normalize-component.js' 19 | }), 20 | commonjs(), 21 | postcss({ 22 | plugins: [require('autoprefixer')], 23 | // 把 css 放到和js同一目录 24 | // extract: true, 25 | // Minimize CSS, boolean or options for cssnano. 26 | minimize: true, 27 | // Enable sourceMap. 28 | sourceMap: false, 29 | // This plugin will process files ending with these extensions and the extensions supported by custom loaders. 30 | extensions: ['.sass', '.scss', '.css'] 31 | }), 32 | babel({ 33 | exclude: 'node_modules/**', 34 | extensions: ['.js', '.vue'] 35 | }), 36 | terser() 37 | ]; 38 | }; 39 | 40 | export default { 41 | input: path.resolve(__dirname, pkg.entry), 42 | output: [ 43 | { 44 | name: 'loadmore', 45 | file: path.resolve(__dirname, pkg.main), 46 | format: 'umd', 47 | sourcemap: false, 48 | globals: { 49 | vue: 'vue' 50 | } 51 | }, 52 | { 53 | name: 'loadmore', 54 | file: path.join(__dirname, pkg.module), 55 | format: 'es', 56 | sourcemap: false, 57 | globals: { 58 | vue: 'vue' 59 | } 60 | } 61 | ], 62 | plugins: getPlugins(), 63 | external: ['vue'] 64 | }; 65 | -------------------------------------------------------------------------------- /test/__snapshots__/index.spec.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`pull refresh 1`] = ` 4 |
7 |
11 |
14 |
17 | 下拉刷新 18 |
19 |
20 | 21 |
24 | 25 | 26 | 27 | 28 | 29 |
30 |
31 |
32 | `; 33 | 34 | exports[`pull refresh 2`] = ` 35 |
38 |
42 |
45 |
48 | 释放刷新 49 |
50 |
51 | 52 |
55 | 56 | 57 | 58 | 59 | 60 |
61 |
62 |
63 | `; 64 | 65 | exports[`pull refresh 3`] = ` 66 |
69 |
73 |
76 |
77 |
80 | 83 | 87 | 93 | 94 | 95 | 96 | 99 | 正在刷新 100 | 101 |
102 |
103 |
104 | 105 |
108 | 109 | 110 | 111 | 112 | 113 |
114 |
115 |
116 | `; 117 | -------------------------------------------------------------------------------- /test/index.spec.js: -------------------------------------------------------------------------------- 1 | import VueLoadmore from '../packages/vuejs-loadmore/index'; 2 | import { mount } from '@vue/test-utils'; 3 | import { trigger } from './utils/event'; 4 | 5 | // 下拉测试 6 | test('pull refresh', () => { 7 | const wrapper = mount(VueLoadmore, { 8 | propsData: { 9 | onRefresh: (done) => { done() }, 10 | }, 11 | }); 12 | const track = wrapper.find('.vuejs-refresh-track'); 13 | 14 | // pulling 15 | trigger(track, 'touchstart', 0, 0); 16 | trigger(track, 'touchmove', 0, 20); 17 | expect(wrapper.vm.$el).toMatchSnapshot(); 18 | 19 | 20 | // loosing 21 | trigger(track, 'touchmove', 0, 75); 22 | trigger(track, 'touchmove', 0, 100); 23 | expect(wrapper.vm.$el).toMatchSnapshot(); 24 | 25 | // loading 26 | trigger(track, 'touchend', 0, 100); 27 | expect(wrapper.vm.$el).toMatchSnapshot(); 28 | 29 | }); -------------------------------------------------------------------------------- /test/utils/event.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 触发dom事件/自定义事件 3 | * @param {*} wrapper 4 | * @param {*} eventName 5 | * @param {*} x 6 | * @param {*} y 7 | */ 8 | export function trigger(wrapper, eventName, x = 0, y = 0) { 9 | const el = 'element' in wrapper ? wrapper.element : wrapper; 10 | const touchList = [{ 11 | target: el, 12 | clientX: x, 13 | clientY: y 14 | }]; 15 | 16 | // 自定义事件 17 | const event = customEvent(eventName); 18 | 19 | // 扩展 20 | Object.assign(event, { 21 | clientX: x, 22 | clientY: 100, 23 | touches: touchList, 24 | targetTouches: touchList, 25 | changedTouches: touchList, 26 | }); 27 | 28 | // 触发自定义事件 29 | el.dispatchEvent(event); 30 | } 31 | 32 | /** 33 | * 自定义事件 34 | * @param {*} eventName 35 | */ 36 | function customEvent(eventName) { 37 | var event; 38 | if (window.CustomEvent) { 39 | // 新版自定义事件 40 | event = new window.CustomEvent(eventName, { 41 | canBubble: true, 42 | cancelable: true 43 | }); 44 | } else { 45 | // 已被废弃的方法,做兼容 46 | event = document.createEvent('CustomEvent'); 47 | event.initCustomEvent(eventName, true, true); 48 | } 49 | 50 | return event; 51 | } --------------------------------------------------------------------------------