├── .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 | [![Build Status](https://travis-ci.org/stackjie/vue-pull-to.svg?branch=master)](https://travis-ci.org/stackjie/vue-pull-to) 7 | [![Coverage Status](https://coveralls.io/repos/github/stackjie/vue-pull-to/badge.svg?branch=master)](https://coveralls.io/github/stackjie/vue-pull-to?branch=master) 8 | [![GitHub issues](https://img.shields.io/github/issues/stackjie/vue-pull-to.svg)](https://github.com/stackjie/vue-pull-to/issues) 9 | [![GitHub stars](https://img.shields.io/github/stars/stackjie/vue-pull-to.svg)](https://github.com/stackjie/vue-pull-to/stargazers) 10 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/stackjie/vue-pull-to/master/LICENSE) 11 | [![npm](https://img.shields.io/npm/v/vue-pull-to.svg)](https://www.npmjs.com/package/vue-pull-to) 12 | 13 | ## Live Examples 14 | qrcode 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 | 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 | [![Build Status](https://travis-ci.org/stackjie/vue-pull-to.svg?branch=master)](https://travis-ci.org/stackjie/vue-pull-to) 5 | [![Coverage Status](https://coveralls.io/repos/github/stackjie/vue-pull-to/badge.svg?branch=master)](https://coveralls.io/github/stackjie/vue-pull-to?branch=master) 6 | [![GitHub issues](https://img.shields.io/github/issues/stackjie/vue-pull-to.svg)](https://github.com/stackjie/vue-pull-to/issues) 7 | [![GitHub stars](https://img.shields.io/github/stars/stackjie/vue-pull-to.svg)](https://github.com/stackjie/vue-pull-to/stargazers) 8 | [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/stackjie/vue-pull-to/master/LICENSE) 9 | [![npm](https://img.shields.io/npm/v/vue-pull-to.svg)](https://www.npmjs.com/package/vue-pull-to) 10 | 11 | ## 在线示例 12 | qrcode 13 | 14 | [examples](http://www.vuepullto.top) 15 | 16 | ## 安装 17 | ``` sh 18 | npm install vue-pull-to --save 19 | ``` 20 | 21 | ## 快速上手 22 | ``` vue 23 | 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 | 6 | 7 | 28 | 29 | 40 | -------------------------------------------------------------------------------- /examples/components/RouterLink.vue: -------------------------------------------------------------------------------- 1 | 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 | 97 | 98 | 100 | 101 | 112 | -------------------------------------------------------------------------------- /examples/pages/Home.vue: -------------------------------------------------------------------------------- 1 | 48 | 49 | 113 | 114 | 126 | -------------------------------------------------------------------------------- /examples/pages/InfiniteScroll.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 40 | 41 | 69 | -------------------------------------------------------------------------------- /examples/pages/SimplePullToLoadMore.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 52 | 53 | 92 | -------------------------------------------------------------------------------- /examples/pages/SimplePullToRefresh.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 |
8 |
9 |
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 |
23 |
24 |
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 |
38 |
39 |
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 |
53 |
54 |
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 |
70 | 76 |
77 |
78 |
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 |
171 |
172 |
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 |
187 |
188 |
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 |
242 |
246 |
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 |
263 |
266 |
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 | --------------------------------------------------------------------------------