├── .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 | [](https://www.npmjs.com/package/vuejs-loadmore) [](https://github.com/staticdeng/vuejs-loadmore/actions)
3 |
4 | [](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 | [](https://www.npmjs.com/package/vuejs-loadmore) [](https://github.com/staticdeng/vuejs-loadmore/actions)
3 |
4 | [](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 |
2 |
3 |
4 |
{{ language === 'zh-CN' ? '下拉刷新上拉加载' : 'pull up and pull down' }}
5 |
{{ language === 'zh-CN' ? 'English' : 'Chinese' }}
6 |
7 |
8 |
15 |
16 | - {{ language === 'zh-CN' ? '测试数据' : 'This is data' }} {{ index + 1 }}
17 |
18 |
19 |
20 |
21 |
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 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
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 |
2 |
3 |
12 |
13 |
14 |
15 | {{ genStatus }}
16 |
17 |
{{ genStatus }}
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {{ loadingText || t(`loadmore.loading`) }}
27 |
28 |
29 |
30 | {{ finishedText || t(`loadmore.finished`) }}
31 |
32 |
33 |
34 | {{ errorText || t(`loadmore.error`) }}
35 |
36 |
37 |
38 |
39 |
40 |
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 |
20 |
21 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | `;
33 |
34 | exports[`pull refresh 2`] = `
35 |
38 |
42 |
51 |
52 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | `;
64 |
65 | exports[`pull refresh 3`] = `
66 |
69 |
73 |
76 |
77 |
80 |
83 |
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 | }
--------------------------------------------------------------------------------