├── .babelrc
├── .coveralls.yml
├── .editorconfig
├── .eslintignore
├── .eslintrc.js
├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── README.zh-CN.md
├── build
├── base.config.js
├── examples.dev.config.js
├── examples.prod.config.js
├── index.js
└── prod.config.js
├── dist
├── vue-pull-to.js
├── vue-pull-to.min.js
└── vue-pull-to.min.js.map
├── examples
├── App.vue
├── assets
│ └── icon
│ │ ├── iconfont.css
│ │ ├── iconfont.eot
│ │ ├── iconfont.js
│ │ ├── iconfont.svg
│ │ ├── iconfont.ttf
│ │ └── iconfont.woff
├── components
│ ├── AppHeader.vue
│ ├── RouterLink.vue
│ └── RouterView.vue
├── index.html
├── main.js
├── pages
│ ├── BounceScroll.vue
│ ├── Home.vue
│ ├── InfiniteScroll.vue
│ ├── SimplePullToLoadMore.vue
│ └── SimplePullToRefresh.vue
└── routes.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── src
├── config.js
├── index.js
├── utils.js
└── vue-pull-to.vue
└── test
└── unit
├── .eslintrc
├── index.js
├── karma.conf.js
├── specs
├── dom.spec.js
├── event.spec.js
└── feature.spec.js
└── utils.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["@babel/env"],
3 | "plugins": ["@babel/transform-runtime"],
4 | "env": {
5 | "test": {
6 | "presets": ["@babel/env"],
7 | "plugins": ["istanbul"]
8 | }
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/.coveralls.yml:
--------------------------------------------------------------------------------
1 | service_name: travis-ci
2 | repo_token: CG7bluJ3IFzSIXUR4BKUFOGpwX5fQrGgG
3 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | dist/
2 | node_modules/
3 | examples/assets
4 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // http://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 | parserOptions: {
6 | parser: 'babel-eslint',
7 | sourceType: 'module'
8 | },
9 | env: {
10 | browser: true,
11 | },
12 | extends: [
13 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
14 | 'standard',
15 |
16 | // required to lint *.vue files
17 | 'plugin:vue/essential'
18 | ],
19 | plugins: [
20 | 'vue'
21 | ],
22 | // add your custom rules here
23 | 'rules': {
24 | // allow paren-less arrow functions
25 | 'arrow-parens': 0,
26 | // allow async-await
27 | 'generator-star-spacing': 0,
28 | "semi": ["error", "always"],
29 | 'space-before-function-paren': 0,
30 | 'no-useless-return': 0,
31 | 'indent': 0
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | npm-debug.log*
4 | yarn-debug.log*
5 | yarn-error.log*
6 | test/unit/coverage
7 | examples/dist
8 |
9 | # Editor directories and files
10 | .idea
11 | *.suo
12 | *.ntvs*
13 | *.njsproj
14 | *.sln
15 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 8.9.3
4 | install:
5 | |
6 | npm install -g npm@latest
7 | npm --version
8 | npm install --registry http://registry.npmjs.org
9 | script:
10 | - npm run test
11 | after_script:
12 | - npm run coveralls
13 | after_success:
14 | - cat ./test/unit/coverage/lcov.info | ./node_modules/.bin/coveralls
15 |
16 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017-present, stackjie
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
13 | all 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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Vue-Pull-To
2 | A pull-down refresh and pull-up load more and infinite scroll component for Vue.js.
3 |
4 | [zh-CN中文文档](https://github.com/stackjie/vue-pull-to/tree/master/README.zh-CN.md)
5 |
6 | [](https://travis-ci.org/stackjie/vue-pull-to)
7 | [](https://coveralls.io/github/stackjie/vue-pull-to?branch=master)
8 | [](https://github.com/stackjie/vue-pull-to/issues)
9 | [](https://github.com/stackjie/vue-pull-to/stargazers)
10 | [](https://github.com/stackjie/vue-pull-to/master/LICENSE)
11 | [](https://www.npmjs.com/package/vue-pull-to)
12 |
13 | ## Live Examples
14 |
15 |
16 | [examples](http://www.vuepullto.top)
17 |
18 | ## Installation
19 | ``` sh
20 | npm install vue-pull-to --save
21 | ```
22 |
23 | ## Usage
24 | ``` vue
25 |
26 |
33 |
34 |
35 |
60 | ```
61 |
62 | The component will occupy 100% height of the parent element by default. props top-load-method and bottom-load-method will default to a loaded parameter, which is a function that changes the state of the component's load, and must be called once loaded. The component will always be loaded, if `loaded('done')` The internal state of the component will become a successful state of loading, `loaded('fail')` for the failure.
63 |
64 | [More usage examples](https://github.com/stackjie/vue-pull-to/tree/master/examples)
65 |
66 | ## API Docs
67 |
68 | ### props
69 | | Attribute | Description | type | Default |
70 | | --- | --- | --- | --- |
71 | | distance-index | Slip the threshold (the greater the value the slower the sliding) | Number | 2 |
72 | | top-block-height | The height of the block element area outside the top of the scroll container | Number | 50 |
73 | | bottom-block-height | The height of the block element area outside the scrolling container | Number | 50 |
74 | | wrapper-height | The height of the scrolling container | String | '100%' |
75 | | top-load-method | Top drop-down method | Function | |
76 | | bottom-load-method | Bottom pull-up method | Function | |
77 | | is-throttle-top-pull | Whether the disable of the `top-pull` throttle event is triggered to ensure performance if the real-time trigger is set to false | Boolean | true |
78 | | is-throttle-bottom-pull | Whether the disable of the `bottom-pull` throttle event is triggered to ensure performance if the real-time trigger is set to false | Boolean | true |
79 | | is-throttle-scroll | Whether the disable of the `scroll` throttle event is triggered to ensure performance if the real-time trigger is set to false | Boolean | true |
80 | | is-touch-sensitive | Whether to handle touch events | Boolean | true |
81 | | is-scroll-sensitive | Whether to handle scroll events | Boolean | true |
82 | | is-top-bounce | Whether to enable the pull-down bounce effect | Boolean | true |
83 | | is-bottom-bounce | Whether to enable the pull-up bounce effect | Boolean | true |
84 | | is-bottom-keep-scroll | Whether to make the scroll container stay in place after completing the pull-down method | Boolean | false |
85 | | top-config | Configuration for the topmost part of the scroll container | Object | default config |
86 | | bottom-config | Configuration for the bottommost part of the scroll container | Object | default config |
87 |
88 | `topConfig` and `bottomConfig` Configurable options and default configuration item values
89 | ``` javascript
90 | const TOP_DEFAULT_CONFIG = {
91 | pullText: '下拉刷新', // The text is displayed when you pull down
92 | triggerText: '释放更新', // The text that appears when the trigger distance is pulled down
93 | loadingText: '加载中...', // The text in the load
94 | doneText: '加载完成', // Load the finished text
95 | failText: '加载失败', // Load failed text
96 | loadedStayTime: 400, // Time to stay after loading ms
97 | stayDistance: 50, // Trigger the distance after the refresh
98 | triggerDistance: 70 // Pull down the trigger to trigger the distance
99 | }
100 |
101 | const BOTTOM_DEFAULT_CONFIG = {
102 | pullText: '上拉加载',
103 | triggerText: '释放更新',
104 | loadingText: '加载中...',
105 | doneText: '加载完成',
106 | failText: '加载失败',
107 | loadedStayTime: 400,
108 | stayDistance: 50,
109 | triggerDistance: 70
110 | }
111 | ```
112 | ### slots
113 | | Name | Description | scope |
114 | | --- | --- | --- |
115 | | default | The default slot scrolls the contents of the container |
116 | | top-block | Scroll the contents of the top of the container outer (support the scope slot need to use `template` tag with scope `attribute`) | `state`:Current state、`state-text`:State corresponding to the text |
117 | | bottom-block | Scroll the contents of the bottom of the container outer (support the scope slot need to use `template` tag with scope `attribute`) | `state`:Current state、`state-text`:State corresponding to the text |
118 |
119 | ### events
120 | | name | Description |
121 | | --- | --- |
122 | | top-state-change | When the top state has changed, the first parameter is the current state |
123 | | bottom-state-change | When the bottom state has changed, the first parameter is the current state |
124 | | top-pull | Pull down the trigger, the first parameter for the current pull of the distance value, the default will be throttle, config `isThrottle` to real-time trigger |
125 | | bottom-pull | Pull up the trigger, the first parameter for the current pull of the distance value, the default will be throttle, config `isThrottle` to real-time trigger |
126 | | infinite-scroll | Triggered when the scroll container scrolls to the end |
127 | | scroll | When scrolling, the event callback function, the first parameter, is the native `event` object |
128 |
--------------------------------------------------------------------------------
/README.zh-CN.md:
--------------------------------------------------------------------------------
1 | # Vue-Pull-To
2 | 一个集成了下拉刷新、上拉加载、无限滚动加载的Vue组件。
3 |
4 | [](https://travis-ci.org/stackjie/vue-pull-to)
5 | [](https://coveralls.io/github/stackjie/vue-pull-to?branch=master)
6 | [](https://github.com/stackjie/vue-pull-to/issues)
7 | [](https://github.com/stackjie/vue-pull-to/stargazers)
8 | [](https://github.com/stackjie/vue-pull-to/master/LICENSE)
9 | [](https://www.npmjs.com/package/vue-pull-to)
10 |
11 | ## 在线示例
12 |
13 |
14 | [examples](http://www.vuepullto.top)
15 |
16 | ## 安装
17 | ``` sh
18 | npm install vue-pull-to --save
19 | ```
20 |
21 | ## 快速上手
22 | ``` vue
23 |
24 |
31 |
32 |
33 |
58 | ```
59 | 组件会默认占据父元素的百分之百高度。props `top-load-method`和`bottom-load-method`会默认传进一个`loaded`参数,该参数是一个改变组件加载状态的函数,必须调用一次`loaded`不然组件就会一直处于加载状态,如果执行`loaded('done')`组件内部状态就会变成成功加载的状态,`loaded('fail')`为失败。
60 |
61 | [更多使用示例请参考Examples的代码](https://github.com/stackjie/vue-pull-to/tree/master/examples)
62 |
63 | ## API文档
64 |
65 | ### props
66 | | 属性 | 说明 | 类型 | 默认值 |
67 | | --- | --- | --- | --- |
68 | | distance-index | 滑动的阀值(值越大滑动的速度越慢) | Number | 2 |
69 | | top-block-height | 顶部在滚动容器外的块级元素区域高度 | Number | 50 |
70 | | bottom-block-height | 底部在滚动容器外的块级元素区域高度 | Number | 50 |
71 | | wrapper-height | 滚动容器的高度 | String | '100%' |
72 | | top-load-method | 顶部下拉时执行的方法 | Function | |
73 | | bottom-load-method | 底部上拉时执行的方法 | Function | |
74 | | is-throttle-top-pull | 是否截流`top-pull`事件的触发以保证性能,如果需要实时触发设为false | Boolean | true |
75 | | is-throttle-bottom-pull | 是否截流`bottom-pull`事件的触发以保证性能,如果需要实时触发设为false | Boolean | true |
76 | | is-throttle-scroll | 是否截流`scroll`事件的触发以保证性能,如果需要实时触发设为false | Boolean | true |
77 | | is-touch-sensitive | 是否处理触摸事件 | Boolean | true |
78 | | is-scroll-sensitive | 是否处理滚动事件 | Boolean | true |
79 | | is-top-bounce | 是否启用下拉回弹效果 | Boolean | true |
80 | | is-bottom-bounce | 是否启用上拉回弹效果 | Boolean | true |
81 | | is-bottom-keep-scroll | 是否在完成底部上拉时执行的方法后使滚动容器保持在原位 | Boolean | true |
82 | | top-config | 滚动容器顶部信息的一些配置 | Object | 默认配置 |
83 | | bottom-config | 滚动容器底部信息的一些配置 | Object | 默认配置 |
84 |
85 | `topConfig`和`bottomConfig`可配置的选项和默认配置项的值
86 | ``` javascript
87 | const TOP_DEFAULT_CONFIG = {
88 | pullText: '下拉刷新', // 下拉时显示的文字
89 | triggerText: '释放更新', // 下拉到触发距离时显示的文字
90 | loadingText: '加载中...', // 加载中的文字
91 | doneText: '加载完成', // 加载完成的文字
92 | failText: '加载失败', // 加载失败的文字
93 | loadedStayTime: 400, // 加载完后停留的时间ms
94 | stayDistance: 50, // 触发刷新后停留的距离
95 | triggerDistance: 70 // 下拉刷新触发的距离
96 | }
97 |
98 | const BOTTOM_DEFAULT_CONFIG = {
99 | pullText: '上拉加载',
100 | triggerText: '释放更新',
101 | loadingText: '加载中...',
102 | doneText: '加载完成',
103 | failText: '加载失败',
104 | loadedStayTime: 400,
105 | stayDistance: 50,
106 | triggerDistance: 70
107 | }
108 | ```
109 | ### slots
110 | | 名称 | 说明 | scope |
111 | | --- | --- | --- |
112 | | default | 默认slot滚动容器的内容 |
113 | | top-block | 滚动容器外顶部的内容(支持作用域slot需用`template`标签加上`scope`属性)| `state`:当前的状态、`state-text`:状态对应的文本 |
114 | | bottom-block | 滚动容器外底部的内容(支持作用域slot需用`template`标签加上`scope`属性)| `state`:当前的状态、`state-text`:状态对应的文本 |
115 |
116 | ### events
117 | | 事件名 | 说明 |
118 | | --- | --- |
119 | | top-state-change | 顶部状态发生了改变时触发,第一个参数为当前的状态 |
120 | | bottom-state-change | 底部状态发生了改变时触发,第一个参数为当前的状态 |
121 | | top-pull | 下拉时触发,第一个参数为当前拉动的距离值,默认会被截流,可配置props `isThrottle`来实时触发 |
122 | | bottom-pull | 上拉时触发,第一个参数为当前拉动的距离值,默认会被截流,可配置props `isThrottle`来实时触发 |
123 | | infinite-scroll | 当滚动容器滚动到底部时触发 |
124 | | scroll | 滚动时触发,事件回调函数第一个参数为原生的`event`对象 |
125 |
--------------------------------------------------------------------------------
/build/base.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var webpack = require('webpack');
4 | var VueLoaderPlugin = require('vue-loader/lib/plugin');
5 | var resolve = require('./');
6 |
7 | module.exports = {
8 | resolve: {
9 | extensions: ['.js', '.vue', '.json'],
10 | alias: {
11 | 'vue$': 'vue/dist/vue.esm.js',
12 | '@': resolve('src'),
13 | 'examples': resolve('examples')
14 | }
15 | },
16 | plugins: [
17 | new VueLoaderPlugin()
18 | ],
19 | module: {
20 | rules: [
21 | {
22 | test: /\.(js|vue)$/,
23 | use: [
24 | {
25 | loader: 'eslint-loader',
26 | options: {
27 | formatter: require('eslint-friendly-formatter')
28 | }
29 | }
30 | ],
31 | enforce: 'pre',
32 | include: [resolve('src'), resolve('examples'), resolve('test')],
33 | },
34 | {
35 | test: /\.vue$/,
36 | use: 'vue-loader',
37 | include: [resolve('src'), resolve('examples'), resolve('test')],
38 | },
39 | {
40 | test: /\.js$/,
41 | use: 'babel-loader',
42 | include: [resolve('src'), resolve('examples'), resolve('test')],
43 | },
44 | {
45 | test: /\.css|.less$/,
46 | use: [
47 | 'vue-style-loader',
48 | 'css-loader',
49 | 'postcss-loader',
50 | 'less-loader'
51 | ],
52 | include: [resolve('src'), resolve('examples')],
53 | }
54 | ]
55 | }
56 | };
57 |
--------------------------------------------------------------------------------
/build/examples.dev.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var merge = require('webpack-merge');
4 | var baseConfig = require('./base.config');
5 | var HtmlWebpackPlugin = require('html-webpack-plugin');
6 | var resolve = require('./');
7 |
8 | module.exports = merge(baseConfig, {
9 | mode: 'development',
10 | entry: resolve('examples/main.js'),
11 | output: {
12 | filename: 'main.js'
13 | },
14 | devServer: {
15 | contentBase: '/assets/',
16 | hot: true,
17 | disableHostCheck: true,
18 | historyApiFallback: true,
19 | stats: {
20 | colors: true
21 | }
22 | },
23 | plugins: [
24 | new HtmlWebpackPlugin({
25 | filename: 'index.html',
26 | template: './examples/index.html',
27 | inject: true
28 | })
29 | ]
30 | });
31 |
--------------------------------------------------------------------------------
/build/examples.prod.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var merge = require('webpack-merge');
4 | var baseConfig = require('./base.config');
5 | var webpack = require('webpack');
6 | var HtmlWebpackPlugin = require('html-webpack-plugin');
7 | var UglifyJsPlugin = require('uglifyjs-webpack-plugin');
8 | var resolve = require('./');
9 |
10 | module.exports = merge(baseConfig, {
11 | mode: 'production',
12 | entry: resolve('examples/main.js'),
13 | output: {
14 | filename: '[name]-[chunkhash].js',
15 | path: resolve('examples/dist')
16 | },
17 | plugins: [
18 | new HtmlWebpackPlugin({
19 | filename: 'index.html',
20 | template: './examples/index.html',
21 | minify: {
22 | removeComments: true,
23 | collapseWhitespace: true,
24 | removeAttributeQuotes: true
25 | }
26 | })
27 | ],
28 | optimization: {
29 | minimize: true,
30 | minimizer: [
31 | new UglifyJsPlugin({
32 | uglifyOptions: {
33 | warnings: false,
34 | compress: {
35 | drop_console: true,
36 | drop_debugger: true
37 | }
38 | }
39 | })
40 | ]
41 | }
42 | });
43 |
--------------------------------------------------------------------------------
/build/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 | var path = require('path');
3 |
4 | module.exports = path.join.bind(path, __dirname, '..');
5 |
--------------------------------------------------------------------------------
/build/prod.config.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | var merge = require('webpack-merge');
4 | var baseConfig = require('./base.config');
5 | var webpack = require('webpack');
6 | var CleanWebpackPlugin = require('clean-webpack-plugin');
7 | var UglifyJsPlugin = require('uglifyjs-webpack-plugin');
8 | var resolve = require('./');
9 |
10 | var ENTRY = './src/index.js';
11 | module.exports = merge(baseConfig, {
12 | mode: 'production',
13 | entry: {
14 | 'vue-pull-to': ENTRY,
15 | 'vue-pull-to.min': ENTRY
16 | },
17 | plugins: [
18 | new CleanWebpackPlugin(),
19 | new webpack.SourceMapDevToolPlugin({
20 | filename: '[name].js.map',
21 | append: '\n//# sourceMappingURL=[url]\n',
22 | include: /\.min\.js$/,
23 | }),
24 | ],
25 | output: {
26 | library: 'VuePullTo',
27 | libraryTarget: 'umd',
28 | filename: '[name].js',
29 | path: resolve('dist')
30 | },
31 | optimization: {
32 | minimize: true,
33 | minimizer: [
34 | new UglifyJsPlugin({
35 | sourceMap: true,
36 | include: /\.min\.js$/
37 | })
38 | ]
39 | }
40 | });
41 |
--------------------------------------------------------------------------------
/dist/vue-pull-to.js:
--------------------------------------------------------------------------------
1 | (function webpackUniversalModuleDefinition(root, factory) {
2 | if(typeof exports === 'object' && typeof module === 'object')
3 | module.exports = factory();
4 | else if(typeof define === 'function' && define.amd)
5 | define([], factory);
6 | else if(typeof exports === 'object')
7 | exports["VuePullTo"] = factory();
8 | else
9 | root["VuePullTo"] = factory();
10 | })(window, function() {
11 | return /******/ (function(modules) { // webpackBootstrap
12 | /******/ // The module cache
13 | /******/ var installedModules = {};
14 | /******/
15 | /******/ // The require function
16 | /******/ function __webpack_require__(moduleId) {
17 | /******/
18 | /******/ // Check if module is in cache
19 | /******/ if(installedModules[moduleId]) {
20 | /******/ return installedModules[moduleId].exports;
21 | /******/ }
22 | /******/ // Create a new module (and put it into the cache)
23 | /******/ var module = installedModules[moduleId] = {
24 | /******/ i: moduleId,
25 | /******/ l: false,
26 | /******/ exports: {}
27 | /******/ };
28 | /******/
29 | /******/ // Execute the module function
30 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
31 | /******/
32 | /******/ // Flag the module as loaded
33 | /******/ module.l = true;
34 | /******/
35 | /******/ // Return the exports of the module
36 | /******/ return module.exports;
37 | /******/ }
38 | /******/
39 | /******/
40 | /******/ // expose the modules object (__webpack_modules__)
41 | /******/ __webpack_require__.m = modules;
42 | /******/
43 | /******/ // expose the module cache
44 | /******/ __webpack_require__.c = installedModules;
45 | /******/
46 | /******/ // define getter function for harmony exports
47 | /******/ __webpack_require__.d = function(exports, name, getter) {
48 | /******/ if(!__webpack_require__.o(exports, name)) {
49 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
50 | /******/ }
51 | /******/ };
52 | /******/
53 | /******/ // define __esModule on exports
54 | /******/ __webpack_require__.r = function(exports) {
55 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
56 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
57 | /******/ }
58 | /******/ Object.defineProperty(exports, '__esModule', { value: true });
59 | /******/ };
60 | /******/
61 | /******/ // create a fake namespace object
62 | /******/ // mode & 1: value is a module id, require it
63 | /******/ // mode & 2: merge all properties of value into the ns
64 | /******/ // mode & 4: return value when already ns object
65 | /******/ // mode & 8|1: behave like require
66 | /******/ __webpack_require__.t = function(value, mode) {
67 | /******/ if(mode & 1) value = __webpack_require__(value);
68 | /******/ if(mode & 8) return value;
69 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
70 | /******/ var ns = Object.create(null);
71 | /******/ __webpack_require__.r(ns);
72 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
73 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
74 | /******/ return ns;
75 | /******/ };
76 | /******/
77 | /******/ // getDefaultExport function for compatibility with non-harmony modules
78 | /******/ __webpack_require__.n = function(module) {
79 | /******/ var getter = module && module.__esModule ?
80 | /******/ function getDefault() { return module['default']; } :
81 | /******/ function getModuleExports() { return module; };
82 | /******/ __webpack_require__.d(getter, 'a', getter);
83 | /******/ return getter;
84 | /******/ };
85 | /******/
86 | /******/ // Object.prototype.hasOwnProperty.call
87 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
88 | /******/
89 | /******/ // __webpack_public_path__
90 | /******/ __webpack_require__.p = "";
91 | /******/
92 | /******/
93 | /******/ // Load entry module and return exports
94 | /******/ return __webpack_require__(__webpack_require__.s = 9);
95 | /******/ })
96 | /************************************************************************/
97 | /******/ ([
98 | /* 0 */
99 | /***/ (function(module, exports, __webpack_require__) {
100 |
101 | // style-loader: Adds some css to the DOM by adding a
16 |
17 |
84 |
85 |
103 |
--------------------------------------------------------------------------------
/examples/assets/icon/iconfont.css:
--------------------------------------------------------------------------------
1 |
2 | @font-face {font-family: "iconfont";
3 | src: url('iconfont.eot?t=1499004025419'); /* IE9*/
4 | src: url('iconfont.eot?t=1499004025419#iefix') format('embedded-opentype'), /* IE6-IE8 */
5 | url('iconfont.woff?t=1499004025419') format('woff'), /* chrome, firefox */
6 | url('iconfont.ttf?t=1499004025419') format('truetype'), /* chrome, firefox, opera, Safari, Android, iOS 4.2+*/
7 | url('iconfont.svg?t=1499004025419#iconfont') format('svg'); /* iOS 4.1- */
8 | }
9 |
10 | .iconfont {
11 | font-family:"iconfont" !important;
12 | font-size:16px;
13 | font-style:normal;
14 | -webkit-font-smoothing: antialiased;
15 | -moz-osx-font-smoothing: grayscale;
16 | }
17 |
18 | .icon-finish:before { content: "\e7de"; }
19 |
20 | .icon-loading:before { content: "\e60d"; }
21 |
22 | .icon-arrow-bottom:before { content: "\e600"; }
23 |
24 | .icon-arrow-right:before { content: "\e768"; }
25 |
26 | .icon-face-20:before { content: "\e77b"; }
27 |
28 | .icon-face-19:before { content: "\e77c"; }
29 |
30 | .icon-face-18:before { content: "\e77d"; }
31 |
32 | .icon-face-17:before { content: "\e77e"; }
33 |
34 | .icon-face-16:before { content: "\e77f"; }
35 |
36 | .icon-face-15:before { content: "\e780"; }
37 |
38 | .icon-face-14:before { content: "\e781"; }
39 |
40 | .icon-face-13:before { content: "\e782"; }
41 |
42 | .icon-face-12:before { content: "\e783"; }
43 |
44 | .icon-face-11:before { content: "\e784"; }
45 |
46 | .icon-face-10:before { content: "\e785"; }
47 |
48 | .icon-face-9:before { content: "\e786"; }
49 |
50 | .icon-face-8:before { content: "\e787"; }
51 |
52 | .icon-face-7:before { content: "\e788"; }
53 |
54 | .icon-face-6:before { content: "\e789"; }
55 |
56 | .icon-face-5:before { content: "\e78a"; }
57 |
58 | .icon-face-4:before { content: "\e78b"; }
59 |
60 | .icon-face-3:before { content: "\e78c"; }
61 |
62 | .icon-face-2:before { content: "\e78d"; }
63 |
64 | .icon-face-1:before { content: "\e78f"; }
65 |
66 |
--------------------------------------------------------------------------------
/examples/assets/icon/iconfont.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackjie/vue-pull-to/54a1711efe7c7890ae1215ecb6c84de614493a0c/examples/assets/icon/iconfont.eot
--------------------------------------------------------------------------------
/examples/assets/icon/iconfont.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Created by FontForge 20120731 at Sun Jul 2 22:00:25 2017
6 | By admin
7 |
8 |
9 |
10 |
24 |
26 |
28 |
30 |
32 |
34 |
38 |
43 |
49 |
52 |
54 |
63 |
72 |
80 |
88 |
97 |
107 |
117 |
127 |
136 |
145 |
154 |
163 |
173 |
182 |
191 |
201 |
210 |
218 |
226 |
234 |
235 |
236 |
--------------------------------------------------------------------------------
/examples/assets/icon/iconfont.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackjie/vue-pull-to/54a1711efe7c7890ae1215ecb6c84de614493a0c/examples/assets/icon/iconfont.ttf
--------------------------------------------------------------------------------
/examples/assets/icon/iconfont.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stackjie/vue-pull-to/54a1711efe7c7890ae1215ecb6c84de614493a0c/examples/assets/icon/iconfont.woff
--------------------------------------------------------------------------------
/examples/components/AppHeader.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
28 |
29 |
40 |
--------------------------------------------------------------------------------
/examples/components/RouterLink.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
9 |
32 |
--------------------------------------------------------------------------------
/examples/components/RouterView.vue:
--------------------------------------------------------------------------------
1 |
16 |
--------------------------------------------------------------------------------
/examples/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | vue-pull-to showcases
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/examples/main.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 | import App from './App';
3 | require('./assets/icon/iconfont');
4 |
5 | new Vue(App).$mount('#app');
6 |
--------------------------------------------------------------------------------
/examples/pages/BounceScroll.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | (=゚ω゚)ノ
6 |
7 |
8 | (/TДT)/
9 |
10 |
11 | π__π
12 |
13 |
14 | (´・ω・`)
15 |
16 |
17 | (☆゚∀゚)
18 |
19 |
20 | (╭ ̄3 ̄)╭♡
21 |
22 |
23 | (=゚ω゚)ノ
24 |
25 |
26 | (/TДT)/
27 |
28 |
29 | π__π
30 |
31 |
32 | (´・ω・`)
33 |
34 |
35 | (☆゚∀゚)
36 |
37 |
38 | (╭ ̄3 ̄)╭♡
39 |
40 |
41 | (=゚ω゚)ノ
42 |
43 |
44 | (/TДT)/
45 |
46 |
47 | π__π
48 |
49 |
50 | (´・ω・`)
51 |
52 |
53 | (☆゚∀゚)
54 |
55 |
56 | (╭ ̄3 ̄)╭♡
57 |
58 |
59 | (=゚ω゚)ノ
60 |
61 |
62 | (/TДT)/
63 |
64 |
65 | π__π
66 |
67 |
68 | (´・ω・`)
69 |
70 |
71 | (☆゚∀゚)
72 |
73 |
74 | (╭ ̄3 ̄)╭♡
75 |
76 |
77 | (=゚ω゚)ノ
78 |
79 |
80 | (/TДT)/
81 |
82 |
83 | π__π
84 |
85 |
86 | (´・ω・`)
87 |
88 |
89 | (☆゚∀゚)
90 |
91 |
92 | (╭ ̄3 ̄)╭♡
93 |
94 |
95 |
96 |
97 |
98 |
100 |
101 |
112 |
--------------------------------------------------------------------------------
/examples/pages/Home.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Vue-Pull-To
5 |
for Vue.js 2.0
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | Bounce scroll
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | Simple pull to refresh
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | Simple pull to load more
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | Infinite scroll
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
113 |
114 |
126 |
--------------------------------------------------------------------------------
/examples/pages/InfiniteScroll.vue:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 | {{ item }}
7 |
8 |
9 |
10 |
12 |
13 |
14 | 加载中...
15 |
16 |
17 |
18 |
19 |
40 |
41 |
69 |
--------------------------------------------------------------------------------
/examples/pages/SimplePullToLoadMore.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 | {{ item }}
8 |
9 |
10 |
11 |
12 |
13 |
19 |
20 |
21 | {{ props.stateText }}
22 |
23 |
24 |
25 |
26 |
27 |
52 |
53 |
92 |
--------------------------------------------------------------------------------
/examples/pages/SimplePullToRefresh.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
8 |
14 |
15 |
16 | {{ props.stateText }}
17 |
18 |
19 |
20 |
21 | {{ item }}
22 |
23 |
24 |
25 |
26 |
27 |
52 |
53 |
92 |
--------------------------------------------------------------------------------
/examples/routes.js:
--------------------------------------------------------------------------------
1 | import Home from './pages/Home';
2 | import BounceScroll from './pages/BounceScroll';
3 | import SimplePullToLoadMore from './pages/SimplePullToLoadMore';
4 | import SimplePullToRefresh from './pages/SimplePullToRefresh';
5 | import InfiniteScroll from './pages/InfiniteScroll';
6 |
7 | export default {
8 | '/': Home,
9 | '/bounce-scroll': BounceScroll,
10 | '/simple-pullto-loadmore': SimplePullToLoadMore,
11 | '/simple-pullto-refresh': SimplePullToRefresh,
12 | '/infinite-scroll': InfiniteScroll
13 | };
14 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-pull-to",
3 | "version": "0.1.8",
4 | "description": "A pull-down refresh and pull-up load more and infinite scroll component of the vue.js",
5 | "main": "dist/vue-pull-to.js",
6 | "files": [
7 | "dist",
8 | "src"
9 | ],
10 | "scripts": {
11 | "dev": "webpack-dev-server --config ./build/examples.dev.config.js",
12 | "build": "webpack --config ./build/prod.config.js",
13 | "build-examples": "webpack --config ./build/examples.prod.config.js",
14 | "unit": "cross-env NODE_ENV=test karma start test/unit/karma.conf.js --single-run",
15 | "test": "npm run unit"
16 | },
17 | "repository": {
18 | "type": "git",
19 | "url": "git+https://github.com/stackjie/vue-pull-to.git"
20 | },
21 | "author": "stackjie",
22 | "license": "MIT",
23 | "bugs": {
24 | "url": "https://github.com/stackjie/vue-pull-to/issues"
25 | },
26 | "homepage": "https://github.com/stackjie/vue-pull-to#readme",
27 | "browserslist": [
28 | "iOS >= 7",
29 | "Android >= 4.1"
30 | ],
31 | "peerDependencies": {
32 | "vue": "^2.2.6"
33 | },
34 | "devDependencies": {
35 | "@babel/core": "^7.4.5",
36 | "@babel/plugin-transform-runtime": "^7.4.4",
37 | "@babel/preset-env": "^7.4.5",
38 | "@babel/register": "^7.4.4",
39 | "@babel/runtime": "^7.4.5",
40 | "autoprefixer": "^9.5.1",
41 | "babel-eslint": "^10.0.1",
42 | "babel-loader": "^8.0.6",
43 | "babel-plugin-istanbul": "^5.1.4",
44 | "chai": "^4.2.0",
45 | "clean-webpack-plugin": "^2.0.2",
46 | "coveralls": "^3.0.3",
47 | "cross-env": "^5.2.0",
48 | "css-loader": "^2.1.1",
49 | "eslint": "^5.16.0",
50 | "eslint-config-standard": "^12.0.0",
51 | "eslint-friendly-formatter": "^4.0.1",
52 | "eslint-loader": "^2.1.2",
53 | "eslint-plugin-html": "^5.0.5",
54 | "eslint-plugin-import": "^2.17.2",
55 | "eslint-plugin-node": "^9.1.0",
56 | "eslint-plugin-promise": "^4.1.1",
57 | "eslint-plugin-standard": "^4.0.0",
58 | "eslint-plugin-vue": "^5.2.2",
59 | "gulp": "^4.0.2",
60 | "gulp-rename": "^1.4.0",
61 | "gulp-sourcemaps": "^2.6.5",
62 | "gulp-uglify": "^3.0.2",
63 | "html-webpack-plugin": "^3.2.0",
64 | "karma": "^4.1.0",
65 | "karma-chrome-launcher": "^2.2.0",
66 | "karma-coverage": "^1.1.2",
67 | "karma-mocha": "^1.3.0",
68 | "karma-sinon-chai": "^2.0.2",
69 | "karma-sourcemap-loader": "^0.3.7",
70 | "karma-spec-reporter": "0.0.32",
71 | "karma-webpack": "^4.0.0-rc.6",
72 | "less": "^3.9.0",
73 | "less-loader": "^5.0.0",
74 | "mocha": "^6.1.4",
75 | "postcss-loader": "^3.0.0",
76 | "puppeteer": "^1.17.0",
77 | "sinon": "^7.3.2",
78 | "sinon-chai": "^3.3.0",
79 | "standard": "^12.0.1",
80 | "uglifyjs-webpack-plugin": "^2.1.3",
81 | "vue": "^2.2.6",
82 | "vue-loader": "^15.7.0",
83 | "vue-style-loader": "^4.1.2",
84 | "vue-template-compiler": "^2.6.10",
85 | "webpack": "^4.32.2",
86 | "webpack-cli": "^3.3.2",
87 | "webpack-dev-server": "^3.4.1",
88 | "webpack-merge": "^4.2.1"
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: [
3 | require('autoprefixer')()
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/src/config.js:
--------------------------------------------------------------------------------
1 | const TOP_DEFAULT_CONFIG = {
2 | pullText: '下拉刷新',
3 | triggerText: '释放更新',
4 | loadingText: '加载中...',
5 | doneText: '加载完成',
6 | failText: '加载失败',
7 | loadedStayTime: 400,
8 | stayDistance: 50,
9 | triggerDistance: 70
10 | };
11 |
12 | const BOTTOM_DEFAULT_CONFIG = {
13 | pullText: '上拉加载',
14 | triggerText: '释放更新',
15 | loadingText: '加载中...',
16 | doneText: '加载完成',
17 | failText: '加载失败',
18 | loadedStayTime: 400,
19 | stayDistance: 50,
20 | triggerDistance: 70
21 | };
22 |
23 | export { TOP_DEFAULT_CONFIG, BOTTOM_DEFAULT_CONFIG };
24 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import PullTo from './vue-pull-to.vue';
2 |
3 | export default PullTo;
4 |
--------------------------------------------------------------------------------
/src/utils.js:
--------------------------------------------------------------------------------
1 | // http://www.alloyteam.com/2012/11/javascript-throttle/
2 |
3 | export function throttle (fn, delay, mustRunDelay = 0) {
4 | if (delay == null) return fn;
5 | /* istanbul ignore next */
6 | const timestampProvider =
7 | typeof performance === 'object' ? performance : Date;
8 | let timer = null;
9 | let tStart;
10 | return function () {
11 | const tCurr = timestampProvider.now();
12 | if (timer != null) clearTimeout(timer);
13 | if (!tStart) {
14 | tStart = tCurr;
15 | }
16 | if (mustRunDelay !== 0 && tCurr - tStart >= mustRunDelay) {
17 | fn.apply(this, arguments);
18 | tStart = tCurr;
19 | } else {
20 | const context = this;
21 | const args = [...arguments];
22 | timer = setTimeout(function () {
23 | timer = null;
24 | return fn.apply(context, args);
25 | }, delay);
26 | }
27 | };
28 | }
29 |
30 | export const PASSIVE_OPTS = (function () {
31 | let value = false;
32 | try {
33 | window.addEventListener('test', noop, {
34 | get passive() {
35 | value = true;
36 | return true;
37 | }
38 | });
39 | window.removeEventListener('test', noop);
40 | } catch (e) {
41 | /* istanbul ignore next */
42 | value = false;
43 | }
44 | return value && { passive: true };
45 |
46 | /* istanbul ignore next */
47 | function noop() {}
48 | })();
49 |
50 | export function create(prototype, properties) {
51 | const obj = Object.create(prototype);
52 | Object.assign(obj, properties);
53 | return obj;
54 | }
55 |
--------------------------------------------------------------------------------
/src/vue-pull-to.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
14 | {{ topText }}
15 |
16 |
17 |
22 |
26 |
31 | {{ bottomText }}
32 |
33 |
34 |
35 |
36 |
37 |
398 |
399 |
441 |
--------------------------------------------------------------------------------
/test/unit/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "mocha": true
4 | },
5 | "globals": {
6 | "expect": true,
7 | "sinon": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/test/unit/index.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | Vue.config.productionTip = false;
4 |
5 | // require all test files (files that ends with .spec.js)
6 | const testsContext = require.context('./specs', true, /\.spec$/);
7 | testsContext.keys().forEach(testsContext);
8 |
9 | // require all src files except main.js for coverage.
10 | // you can also change this to match only the subset of files that
11 | // you want coverage for.
12 | const srcContext = require.context('../../src', true, /^\.\/(?!main\.js$).+\.(js|vue)$/i);
13 | srcContext.keys().forEach(srcContext);
14 |
--------------------------------------------------------------------------------
/test/unit/karma.conf.js:
--------------------------------------------------------------------------------
1 | // This is a karma config file. For more details see
2 | // http://karma-runner.github.io/0.13/config/configuration-file.html
3 | // we are also using it with karma-webpack
4 | // https://github.com/webpack/karma-webpack
5 |
6 | process.env.CHROME_BIN = require('puppeteer').executablePath();
7 |
8 | var webpackConfig = require('../../build/base.config');
9 | webpackConfig.mode = 'development';
10 | webpackConfig.devtool = '#inline-source-map';
11 |
12 | module.exports = function (config) {
13 | config.set({
14 | // to run in additional browsers:
15 | // 1. install corresponding karma launcher
16 | // http://karma-runner.github.io/0.13/config/browsers.html
17 | // 2. add it to the `browsers` array below.
18 | browsers: ['ChromeHeadless'],
19 | frameworks: ['mocha', 'sinon-chai'],
20 | reporters: ['spec', 'coverage'],
21 | files: ['./index.js'],
22 | preprocessors: {
23 | './index.js': ['webpack', 'sourcemap']
24 | },
25 | webpack: webpackConfig,
26 | webpackMiddleware: {
27 | noInfo: true
28 | },
29 | coverageReporter: {
30 | dir: './coverage',
31 | reporters: [
32 | { type: 'lcov', subdir: '.' },
33 | { type: 'text-summary' }
34 | ]
35 | }
36 | })
37 | };
38 |
--------------------------------------------------------------------------------
/test/unit/specs/dom.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { createTest } from '../utils';
3 | import PullTo from '../../../src';
4 |
5 | describe('dom', () => {
6 | it('create', () => createTest(PullTo, true, (vm) => {
7 | expect(vm.$el.classList.contains('vue-pull-to-wrapper')).to.be.ok;
8 | expect(vm.$refs['scroll-container'].classList.contains('scroll-container')).to.be.true;
9 | }));
10 |
11 | it('create action block', () => createTest(PullTo, {
12 | topLoadMethod() {},
13 | bottomLoadMethod() {}
14 | }, true, (vm) => {
15 | expect(vm.$refs['action-block-top'].classList.contains('action-block')).to.be.true;
16 | expect(vm.$refs['action-block-bottom'].classList.contains('action-block')).to.be.true;
17 | }));
18 |
19 | it('set prop BlockHeight', () => createTest(PullTo, {
20 | topLoadMethod() {},
21 | bottomLoadMethod() {},
22 | topBlockHeight: 60,
23 | bottomBlockHeight: 60
24 | }, true, (vm) => {
25 | expect(vm.$refs['action-block-top'].style).to.be.a('CSSStyleDeclaration')
26 | .but.not.an('array').that.includes({ height: '60px', marginTop: '-60px' });
27 | expect(vm.$refs['action-block-bottom'].style).to.be.a('CSSStyleDeclaration')
28 | .but.not.an('array').that.includes({ height: '60px', marginTop: '-60px' });
29 | }));
30 |
31 | it('set wrapperHeight', () => createTest(PullTo, {
32 | wrapperHeight: '80%'
33 | }, true, (vm) => {
34 | expect(vm.$el.style).to.be.a('CSSStyleDeclaration')
35 | .but.not.an('array').that.includes({ height: '80%' });
36 | }));
37 | });
38 |
--------------------------------------------------------------------------------
/test/unit/specs/event.spec.js:
--------------------------------------------------------------------------------
1 | import { createTest, waitFor, waitForSeq, touch } from '../utils';
2 | import PullTo from '../../../src';
3 |
4 | describe('event', () => {
5 | it('top pull', done => createTest({
6 | template: `
7 |
10 | `,
11 | components: { PullTo }
12 | }, true, done, ({ $refs: { pt } }, done) => {
13 | pt.$on('top-pull', waitFor(350, done, true));
14 |
15 | const elem = pt.$refs['scroll-container'];
16 | touch(elem, 'touchstart', 0);
17 | touch(elem, 'touchmove', 10);
18 | }));
19 |
20 | it('bottom pull', done => createTest({
21 | template: `
22 |
25 | `,
26 | components: { PullTo }
27 | }, true, done, ({ $refs: { pt } }, done) => {
28 | pt.$on('bottom-pull', waitFor(350, done, true));
29 |
30 | const elem = pt.$refs['scroll-container'];
31 | touch(elem, 'touchstart', 0);
32 | touch(elem, 'touchmove', -30);
33 | }));
34 |
35 | it('top pull negative', done => createTest({
36 | template: `
37 |
40 | `,
41 | components: { PullTo }
42 | }, true, done, ({ $refs: { pt } }, done) => {
43 | pt.$on('top-pull', waitFor(350, done, false, true));
44 |
45 | const elem = pt.$refs['scroll-container'];
46 | touch(elem, 'touchstart', 0);
47 | touch(elem, 'touchmove', 10);
48 | }));
49 |
50 | it('bottom pull negative', done => createTest({
51 | template: `
52 |
55 | `,
56 | components: { PullTo }
57 | }, true, done, ({ $refs: { pt } }, done) => {
58 | pt.$on('bottom-pull', waitFor(350, done, false, true));
59 |
60 | const elem = pt.$refs['scroll-container'];
61 | touch(elem, 'touchstart', 0);
62 | touch(elem, 'touchmove', -30);
63 | }));
64 |
65 | function testStateChange(which, isAsync, isBottomKeepScroll, loadedState, done) {
66 | let actionLoaded;
67 | return createTest({
68 | template: `
69 |
79 | `,
80 | components: { PullTo },
81 | computed: {
82 | keepScroll: () => isBottomKeepScroll
83 | },
84 | methods: {
85 | load(sf) {
86 | if (isAsync) {
87 | actionLoaded = sf;
88 | } else {
89 | sf(loadedState);
90 | }
91 | }
92 | }
93 | }, true, done, ({ $refs: { pt } }, done) => {
94 | const goal = waitForSeq(1e3, done, [
95 | 'pull',
96 | 'trigger',
97 | 'loading',
98 | `loaded-${loadedState === undefined ? 'done' : loadedState}`,
99 | ''
100 | ]);
101 | expect(pt.state).to.be.equal('');
102 | expect(pt.direction).to.be.equal(0);
103 | pt.$on(which < 0 ? 'bottom-state-change' : 'top-state-change', (s) => {
104 | try {
105 | expect(pt.distance * which).to.not.be.below(0);
106 | expect(pt.direction).to.be.equal({
107 | '-1': 'up', 0: 0, 1: 'down'
108 | }[Math.sign(pt.distance)]);
109 | expect(pt.state).to.be.equal(s);
110 | } catch (e) {
111 | goal(null, e);
112 | return;
113 | }
114 | return goal(s);
115 | });
116 | pt.$on(which < 0 ? 'top-state-change' : 'bottom-state-change',
117 | () => goal(null, new Error(`unexpected ${which < 0 ? 'top' : 'bottom'} state`)));
118 | expect(pt.state).to.be.equal('');
119 |
120 | const elem = pt.$refs['scroll-container'];
121 | const trans = [
122 | [ 'touchstart', 0 ],
123 | [ 'touchend' ],
124 | [ 'touchstart', 0 ],
125 | [ 'touchmove', which ],
126 | [ 'touchmove', which * 32767 ],
127 | [ 'touchend' ]
128 | ];
129 | if (isAsync) {
130 | trans.push(
131 | [ 'touchstart', 0 ],
132 | [ 'touchmove', which * -60 ],
133 | [ 'touchmove', which * 60 ],
134 | [ 'touchmove', which * -32767 ],
135 | [ 'touchmove', which * 32767 ],
136 | [ 'touchmove', which * 50 ],
137 | [ 'touchmove', which * 50, 1000 ],
138 | [ 'touchmove', which * 50, -1000 ],
139 | [ 'touchmove', 0 ],
140 | [ 'touchend' ]);
141 | }
142 | let i = 0;
143 | (function next() {
144 | if (!(i < trans.length)) {
145 | if (actionLoaded) actionLoaded(loadedState);
146 | return;
147 | }
148 | touch(elem, ...trans[i++]);
149 | pt.$nextTick(next);
150 | })();
151 | });
152 | }
153 |
154 | [+1, -1].forEach((which) => {
155 | [false, true].forEach((isAsync) => {
156 | [false, true].forEach((isBottomKeepScroll) => {
157 | ['fail', 'done'].forEach((loadedState) => {
158 | const words = [`loadstate=${loadedState}`];
159 | if (isAsync) words.push('async');
160 | if (isBottomKeepScroll) words.push('keep-scroll');
161 | it(`${which < 0 ? 'bottom' : 'top'} state change (${words.join(', ')})`,
162 | done => testStateChange(which, isAsync, isBottomKeepScroll, loadedState, done));
163 | });
164 | });
165 | });
166 | });
167 |
168 | it('infinite scroll', done => createTest({
169 | template: `
170 |
173 | `,
174 | components: { PullTo }
175 | }, true, done, ({ $refs: { pt } }, done) => {
176 | pt.$on('infinite-scroll', waitFor(420, done, () => {}));
177 |
178 | const elem = pt.$refs['scroll-container'];
179 | elem.dispatchEvent(new Event('scroll', {
180 | bubbles: true, cancelable: true
181 | }));
182 | }));
183 |
184 | it('scroll', done => createTest({
185 | template: `
186 |
189 | `,
190 | components: { PullTo }
191 | }, true, done, ({ $refs: { pt } }, done) => {
192 | pt.$on('scroll', waitFor(200, done, true));
193 |
194 | const elem = pt.$refs['scroll-container'];
195 | elem.dispatchEvent(new Event('scroll', {
196 | bubbles: true, cancelable: true
197 | }));
198 | }));
199 |
200 | let id = 0;
201 | [false, true].forEach((isSensitive) => {
202 | function toggleTester(vm, done, events, wc, fn, endfn) {
203 | const myid = id++;
204 | const { $refs: { pt } } = vm;
205 | const elem = pt.$refs['scroll-container'];
206 | let waitCount = 0;
207 | let phase = 0;
208 | let flag = isSensitive;
209 | const goal = waitFor(200, done, function (x) {
210 | if (x != null) throw x;
211 | }, function () {
212 | if (phase < 1 || waitCount > 0) throw new Error('timeout');
213 | });
214 | function onTouch() {
215 | if (waitCount <= 0) {
216 | goal(new Error('unexpected touch event'));
217 | return;
218 | }
219 | if (--waitCount <= 0) {
220 | (phase === 1 ? goal : cont)();
221 | }
222 | }
223 | function cont() {
224 | phase = 1;
225 | if (flag && endfn) endfn(elem);
226 | vm.isSensitive = flag = !flag;
227 | vm.$nextTick(() => {
228 | waitCount = flag ? wc : 0;
229 | fn(elem);
230 | });
231 | }
232 | events.forEach(n => pt.$on(n, onTouch));
233 |
234 | waitCount = flag ? wc : 0;
235 | fn(elem);
236 | if (!flag && phase === 0) cont();
237 | }
238 |
239 | it(`touch sensitivity (initial=${isSensitive})`, done => createTest({
240 | template: `
241 |
247 | `,
248 | data() {
249 | return { isThrottle: false, isSensitive };
250 | },
251 | components: { PullTo }
252 | }, true, done, (vm, done) => toggleTester(
253 | vm, done, ['top-pull', 'bottom-pull'], 2, (elem) => {
254 | touch(elem, 'touchstart', 0);
255 | touch(elem, 'touchmove', -1e4);
256 | touch(elem, 'touchmove', 1e4);
257 | }, elem => touch(elem, 'touchend'))
258 | ));
259 |
260 | it(`scroll sensitivity (initial=${isSensitive})`, done => createTest({
261 | template: `
262 |
267 | `,
268 | data() {
269 | return { isSensitive };
270 | },
271 | components: { PullTo }
272 | }, true, done, (vm, done) => toggleTester(
273 | vm, done, ['scroll'], 1, (elem) => {
274 | elem.dispatchEvent(new Event('scroll', {
275 | bubbles: true, cancelable: true
276 | }));
277 | })
278 | ));
279 | });
280 | });
281 |
--------------------------------------------------------------------------------
/test/unit/specs/feature.spec.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import { throttle } from '../../../src/utils';
3 |
4 | describe('throttle', () => {
5 | it('should pass through its arguments', () => {
6 | const obj = { a: 1 };
7 | let run = false;
8 | try {
9 | throttle(function () {
10 | if (run == null) return; // do not interrupt other tests
11 | expect(this).to.equal(obj);
12 | expect(arguments.length).to.equal(42);
13 | run = true;
14 | }, 0, -Infinity).apply(obj, Array(42));
15 | expect(run).to.be.true;
16 | } finally {
17 | run = null;
18 | }
19 | });
20 |
21 | it('should be identity when delay == null', () => {
22 | function noop() {}
23 | expect(throttle(noop)).to.be.equal(noop);
24 | expect(throttle(noop, null)).to.be.equal(noop);
25 | });
26 |
27 | it('should correctly handle delay', (done) => {
28 | let state = 'pre';
29 | let to;
30 | const fn = throttle(function () {
31 | if (state == null) return;
32 | try {
33 | expect(state).to.equal('post');
34 | state = 'fired';
35 | } catch (e) {
36 | if (to != null) {
37 | clearTimeout(to);
38 | to = null;
39 | }
40 | done(e);
41 | }
42 | }, 0);
43 | for (let i = 0; i < 64; i++) fn();
44 | to = setTimeout(function () {
45 | to = null;
46 | try {
47 | expect(state).to.equal('fired');
48 | } catch (e) {
49 | done(e);
50 | return;
51 | }
52 | done();
53 | }, 100);
54 | state = 'post';
55 | });
56 |
57 | it('should correctly handle must-run delay', (done) => {
58 | let state = 'pre';
59 | let to;
60 | const fn = throttle(function () {
61 | switch (state) {
62 | case null: return;
63 | case 'post': state = '1-pre'; break;
64 | case '1-pre': state = '1-post'; break;
65 | default:
66 | try {
67 | expect.fail("unexpected state: " + state);
68 | } catch (e) {
69 | if (to != null) {
70 | clearTimeout(to);
71 | to = null;
72 | }
73 | done(e);
74 | break;
75 | }
76 | done();
77 | }
78 | }, 0, 3e-16);
79 | fn();
80 | to = setTimeout(function () {
81 | to = null;
82 | try {
83 | expect(state).to.be.equal('1-pre');
84 | fn();
85 | expect(state).to.be.equal('1-post');
86 | } catch (e) {
87 | done(e);
88 | return;
89 | }
90 | done();
91 | }, 100);
92 | state = 'post';
93 | });
94 | });
95 |
--------------------------------------------------------------------------------
/test/unit/utils.js:
--------------------------------------------------------------------------------
1 | import Vue from 'vue';
2 |
3 | // https://github.com/ElemeFE/element/blob/dev/test/unit/util.js
4 | let id = 0;
5 |
6 | function createElm() {
7 | const elm = document.createElement('div');
8 | elm.id = `app${++id}`;
9 | document.body.appendChild(elm);
10 | return elm;
11 | };
12 |
13 | /**
14 | * 回收 vm
15 | * @param {Object} vm
16 | */
17 | function destroyVM(vm) {
18 | const el = vm.$el;
19 | if (el != null) {
20 | const p = el.parentNode;
21 | if (p != null) p.removeChild(el);
22 | }
23 | }
24 |
25 | /**
26 | * 创建一个测试组件实例
27 | * @link http://vuejs.org/guide/unit-testing.html#Writing-Testable-Components
28 | * @param {Object} Compo - 组件对象,可直接传 template
29 | * @param {Object} propsData - props 数据
30 | * @param {Boolean=false} shallMount - 是否添加到 DOM 上
31 | * @param {Function} done - 完成后调用的函数
32 | * @param {Function} closure - 回调函数
33 | * @return {Object?} vm - 如果提供 closure,则返回 undefined
34 | */
35 | export function createTest(
36 | Compo, propsData = {}, shallMount = false, done, closure) {
37 | if (typeof closure === 'undefined' && typeof shallMount === 'function') {
38 | if (typeof done === 'function') {
39 | closure = done;
40 | done = shallMount;
41 | } else {
42 | closure = shallMount;
43 | }
44 | shallMount = false;
45 | }
46 | if (typeof propsData === 'boolean') {
47 | shallMount = propsData;
48 | propsData = {};
49 | }
50 | let vm = new (Vue.extend(Compo))({ propsData })
51 | .$mount(shallMount ? createElm() : undefined);
52 | if (closure == null) return vm;
53 | const fin = function (e) {
54 | const d = vm;
55 | if (d == null) return;
56 | vm = null;
57 | try {
58 | destroyVM(d);
59 | } catch (e2) {
60 | if (e == null && e2 != null) e = e2;
61 | }
62 | vm = null;
63 | if (typeof done === 'function') {
64 | return done(e);
65 | } else if (e != null) {
66 | throw e;
67 | }
68 | };
69 | let callFin = closure.length < 2;
70 | try {
71 | if (!callFin) {
72 | closure(vm, fin);
73 | } else {
74 | closure(vm);
75 | }
76 | } catch (e) {
77 | fin(e);
78 | callFin = false;
79 | }
80 | if (callFin) fin();
81 | return undefined;
82 | }
83 |
84 | export function waitFor(timeout, done, onCallback, onTimeout) {
85 | if (typeof onCallback !== 'function') {
86 | onCallback = onCallback
87 | ? function (r) { void expect(r).to.be.exist; }
88 | : function (r) { expect.fail('unexpected callback: ' + r); };
89 | }
90 | if (typeof onTimeout !== 'function') {
91 | onTimeout = onTimeout
92 | ? function () {}
93 | : function () { expect.fail('timeout'); };
94 | }
95 | let to = setTimeout(() => {
96 | try {
97 | to = null;
98 | onTimeout();
99 | } catch (e) {
100 | return done(e);
101 | }
102 | return done();
103 | }, timeout);
104 | return function () {
105 | if (to == null) return;
106 | try {
107 | clearTimeout(to);
108 | to = null;
109 | onCallback.apply(this, arguments);
110 | } catch (e) {
111 | return done(e);
112 | }
113 | return done();
114 | };
115 | }
116 |
117 | export function waitForSeq(timeout, done, seq, onTimeout) {
118 | const { length } = seq;
119 | if (!(length >= 1)) {
120 | done();
121 | return;
122 | }
123 | function onCallback(e) {
124 | if (e != null) throw e;
125 | }
126 | const goal = waitFor(timeout, done, onCallback, onTimeout);
127 | let i = 0;
128 | return function (state, error) {
129 | if (seq == null) return;
130 | if (arguments.length >= 2) {
131 | seq = null;
132 | goal(error);
133 | return;
134 | }
135 | try {
136 | expect(state).to.be.equal(seq[i]);
137 | } catch (e) {
138 | seq = null;
139 | goal(e);
140 | return;
141 | }
142 | if (++i >= length) {
143 | seq = null;
144 | goal();
145 | }
146 | };
147 | }
148 |
149 | export function touch(elem, name, clientY, clientX) {
150 | elem.dispatchEvent(new TouchEvent(name, {
151 | bubbles: true,
152 | cancelable: true,
153 | touches: [new Touch({ identifier: 0, target: elem, clientY, clientX })]
154 | }));
155 | }
156 |
--------------------------------------------------------------------------------