├── .eslintignore ├── .prettierrc.js ├── src ├── core │ ├── third-party │ │ ├── scroller │ │ │ ├── README.MD │ │ │ ├── render.js │ │ │ ├── requestAnimationFrame.js │ │ │ ├── listener.js │ │ │ └── animate.js │ │ ├── resize-detector │ │ │ ├── README.MD │ │ │ └── index.js │ │ ├── easingPattern │ │ │ ├── README.MD │ │ │ └── index.js │ │ └── README.MD │ ├── mixins │ │ ├── index.js │ │ ├── mix-panel.js │ │ ├── config.js │ │ ├── api.js │ │ └── core.js │ └── components │ │ └── panel.js ├── mode │ ├── mix │ │ ├── mixins │ │ │ ├── index.js │ │ │ ├── update-mix.js │ │ │ └── api.js │ │ ├── index.js │ │ ├── mix-panel.js │ │ ├── config.js │ │ └── core.js │ ├── native │ │ ├── mixins │ │ │ ├── index.js │ │ │ ├── scrollAnimate.js │ │ │ ├── update-native.js │ │ │ └── api.js │ │ ├── index.js │ │ ├── config.js │ │ ├── native-panel.js │ │ └── core.js │ └── slide │ │ ├── mixins │ │ ├── index.js │ │ └── api.js │ │ ├── index.js │ │ ├── config.js │ │ ├── slide-panel.js │ │ └── core.js ├── shared │ ├── log.js │ ├── index.js │ ├── constants.js │ ├── scroll-map.js │ ├── zoomManager.js │ ├── utils.js │ ├── touchManager.js │ ├── base-config.js │ └── runtime.js ├── entry-slide-mode.js ├── entry-native-mode.js └── entry-mix-mode.js ├── .gitignore ├── test └── unit │ ├── index.js │ ├── specs │ ├── util.spec.js │ ├── mode.spec.js │ ├── event.spec.js │ ├── class-hooks.spec.js │ ├── slot.spec.js │ ├── scroll-panel.spec.js │ ├── bar.spec.js │ └── assert-warn-info.spec.js │ ├── karma.conf.js │ └── util.js ├── dist ├── vuescroll.css ├── vuescroll-native.d.ts └── vuescroll-slide.d.ts ├── types ├── index.d.ts ├── vue.d.ts ├── vuescroll.d.ts ├── Config.d.ts ├── vuescroll-native.d.ts └── vuescroll-slide.d.ts ├── jsconfig.json ├── .babelrc ├── scripts ├── alias.js ├── debug-build.js ├── build.js └── config.js ├── .eslintrc.js ├── LICENSE ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── COMMIT_CONVENTION.md ├── .circleci └── config.yml ├── examples └── base-scroll.html ├── package.json └── README.md /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | coverage 3 | node_modules 4 | examples -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | singleQuote: true, 3 | trailingComma: 'none' 4 | }; 5 | -------------------------------------------------------------------------------- /src/core/third-party/scroller/README.MD: -------------------------------------------------------------------------------- 1 | # Scroller 2 | 3 | [Github Address](https://github.com/pbakaus/scroller) 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vscode 3 | coverage 4 | package-lock.json 5 | debug.log 6 | yarn-error.log 7 | .idea 8 | .npmrc -------------------------------------------------------------------------------- /src/mode/mix/mixins/index.js: -------------------------------------------------------------------------------- 1 | import api from './api'; 2 | import update from './update-mix'; 3 | 4 | export default [api, ...update]; 5 | -------------------------------------------------------------------------------- /src/core/third-party/resize-detector/README.MD: -------------------------------------------------------------------------------- 1 | # resize-detector 2 | 3 | [Github Address](https://github.com/wnr/element-resize-detector) 4 | -------------------------------------------------------------------------------- /src/mode/native/mixins/index.js: -------------------------------------------------------------------------------- 1 | import api from './api'; 2 | import update from './update-native'; 3 | 4 | export default [api, update]; 5 | -------------------------------------------------------------------------------- /src/mode/slide/mixins/index.js: -------------------------------------------------------------------------------- 1 | import api from './api'; 2 | import update from './update-slide'; 3 | 4 | export default [api, update]; 5 | -------------------------------------------------------------------------------- /test/unit/index.js: -------------------------------------------------------------------------------- 1 | // require all test files 2 | const testsContext = require.context('./', true, /\.spec$/); 3 | testsContext.keys().forEach(testsContext); 4 | -------------------------------------------------------------------------------- /dist/vuescroll.css: -------------------------------------------------------------------------------- 1 | /* 2 | Since 4.10.0, you don't need to import css flie any more, 3 | this flie is only used for backward compatible. Maybe deleted 4 | in the future. 5 | */ 6 | -------------------------------------------------------------------------------- /src/mode/mix/mixins/update-mix.js: -------------------------------------------------------------------------------- 1 | import slideMix from 'mode/slide/mixins/update-slide'; 2 | import nativeMix from 'mode/native/mixins/update-native'; 3 | 4 | export default [slideMix, nativeMix]; 5 | -------------------------------------------------------------------------------- /src/shared/log.js: -------------------------------------------------------------------------------- 1 | export const log = { 2 | error: (msg) => { 3 | console.error(`[vuescroll] ${msg}`); 4 | }, 5 | warn: (msg) => { 6 | console.warn(`[vuescroll] ${msg}`); 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import './vue'; 2 | 3 | import Config from './Config'; 4 | 5 | import { vuescroll } from './vuescroll'; 6 | 7 | export default vuescroll; 8 | 9 | export { Config }; 10 | -------------------------------------------------------------------------------- /src/mode/native/index.js: -------------------------------------------------------------------------------- 1 | import { createPanel } from './native-panel'; 2 | import core from './core'; 3 | import { configs } from './config'; 4 | 5 | export { core, createPanel as render, configs as extraConfigs }; 6 | -------------------------------------------------------------------------------- /src/core/third-party/easingPattern/README.MD: -------------------------------------------------------------------------------- 1 | # Smooth scroll easingPattern 2 | 3 | [Github Address](https://github.com/cferdinandi/smooth-scroll/blob/a10c2f39e0a20611a74296c9ee8fb1eb9abf90e2/src/js/smooth-scroll.js#L177) 4 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "src/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/mode/native/config.js: -------------------------------------------------------------------------------- 1 | export const configs = [ 2 | { 3 | vuescroll: { 4 | wheelScrollDuration: 0, 5 | wheelDirectionReverse: false, 6 | checkShiftKey: true, 7 | deltaPercent: 1 8 | } 9 | } 10 | ]; 11 | -------------------------------------------------------------------------------- /types/vue.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Augment the typings of Vue.js 3 | */ 4 | 5 | import Vue from 'vue'; 6 | import Config from './Config'; 7 | 8 | declare module 'vue/types/vue' { 9 | interface Vue { 10 | $vuescrollConfig: Config; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["@babel/preset-env", { "modules": false }], 4 | ], 5 | "plugins": ["@vue/babel-plugin-jsx"] 6 | // "env": { 7 | // "build": { 8 | // "plugins": ["external-helpers"] 9 | // } 10 | // } 11 | } 12 | -------------------------------------------------------------------------------- /src/entry-slide-mode.js: -------------------------------------------------------------------------------- 1 | import _install from 'src/core'; 2 | import { core, render, extraConfigs, extraValidators } from './mode/slide'; 3 | 4 | const Vuescroll = { 5 | ..._install(core, render, extraConfigs, extraValidators) 6 | }; 7 | 8 | export default Vuescroll; 9 | -------------------------------------------------------------------------------- /src/mode/mix/index.js: -------------------------------------------------------------------------------- 1 | import render from './mix-panel'; 2 | import core from './core'; 3 | import { configs, configValidators } from './config'; 4 | 5 | export { 6 | core, 7 | render, 8 | configs as extraConfigs, 9 | configValidators as extraValidators 10 | }; 11 | -------------------------------------------------------------------------------- /src/shared/index.js: -------------------------------------------------------------------------------- 1 | export * from './base-config'; 2 | export * from './constants'; 3 | export * from './log'; 4 | export * from './runtime'; 5 | export * from './scroll-map'; 6 | export * from './touchManager'; 7 | export * from './zoomManager'; 8 | export * from './utils'; 9 | -------------------------------------------------------------------------------- /src/mode/slide/index.js: -------------------------------------------------------------------------------- 1 | import { createPanel } from './slide-panel'; 2 | import core from './core'; 3 | import { configs, configValidator } from './config'; 4 | 5 | export { 6 | core, 7 | createPanel as render, 8 | configs as extraConfigs, 9 | configValidator as extraValidators 10 | }; 11 | -------------------------------------------------------------------------------- /src/core/third-party/README.MD: -------------------------------------------------------------------------------- 1 | #### The following is the third party library used by Vuescroll and its corresponding module. 2 | 3 | | Libarary Name | Module | 4 | | --------------- | ------------- | 5 | | Scroller | core, animate | 6 | | Smooth-scroll | easingPattern | 7 | | resize-detector | injectObject | 8 | -------------------------------------------------------------------------------- /src/entry-native-mode.js: -------------------------------------------------------------------------------- 1 | import { scrollTo } from 'src/mode/native/mixins/api'; 2 | import _install from 'src/core'; 3 | 4 | import { core, render, extraConfigs } from './mode/native'; 5 | 6 | const Vuescroll = { 7 | scrollTo, 8 | ..._install(core, render, extraConfigs) 9 | }; 10 | 11 | export default Vuescroll; 12 | -------------------------------------------------------------------------------- /src/entry-mix-mode.js: -------------------------------------------------------------------------------- 1 | import { scrollTo } from 'src/mode/native/mixins/api'; 2 | import _install from 'src/core'; 3 | import { core, render, extraConfigs, extraValidators } from './mode/mix'; 4 | 5 | const Vuescroll = { 6 | scrollTo, 7 | ..._install(core, render, extraConfigs, extraValidators) 8 | }; 9 | 10 | export default Vuescroll; 11 | -------------------------------------------------------------------------------- /src/shared/constants.js: -------------------------------------------------------------------------------- 1 | // all modes 2 | export const modes = ['slide', 'native']; 3 | // some small changes. 4 | export const smallChangeArray = [ 5 | 'mergedOptions.vuescroll.pullRefresh.tips', 6 | 'mergedOptions.vuescroll.pushLoad.tips', 7 | 'mergedOptions.vuescroll.scroller.disable', 8 | 'mergedOptions.rail', 9 | 'mergedOptions.bar' 10 | ]; 11 | // refresh/load dom ref/key... 12 | export const __REFRESH_DOM_NAME = 'refreshDom'; 13 | export const __LOAD_DOM_NAME = 'loadDom'; 14 | -------------------------------------------------------------------------------- /scripts/alias.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs'); 3 | 4 | const resolve = (p) => path.resolve(__dirname, '../', p); 5 | const alias = { 6 | src: resolve('src'), 7 | test: resolve('./test'), 8 | vue$: 'vue/dist/vue.cjs.js' 9 | }; 10 | 11 | const extend = (alias) => { 12 | const dirs = fs.readdirSync(alias.src); 13 | dirs.forEach((dir) => { 14 | alias[dir] = `${alias.src}/${dir}`; 15 | }); 16 | }; 17 | extend(alias); 18 | 19 | module.exports = alias; 20 | -------------------------------------------------------------------------------- /src/core/mixins/index.js: -------------------------------------------------------------------------------- 1 | import render from './mix-panel'; 2 | import core from './core'; 3 | import { configs, configValidators } from './config'; 4 | 5 | import { _install } from 'mode/shared/util'; 6 | 7 | const component = _install(core, render, configs, configValidators); 8 | 9 | export default function install(Vue, opts = {}) { 10 | Vue.component(opts.name || component.name, component); 11 | Vue.config.globalProperties.$vuescrollConfig = opts.ops || {}; 12 | } 13 | 14 | export { component }; 15 | -------------------------------------------------------------------------------- /src/mode/mix/mix-panel.js: -------------------------------------------------------------------------------- 1 | // begin importing 2 | import { createPanel as createNativePanel } from 'mode/native/native-panel'; 3 | import { createPanel as createSlidePanel } from 'mode/slide/slide-panel'; 4 | /** 5 | * create a scrollPanel 6 | * 7 | * @param {any} size 8 | * @param {any} vm 9 | * @returns 10 | */ 11 | export default function createPanel(vm) { 12 | if (vm.mode == 'native') { 13 | return createNativePanel(vm); 14 | } else if (vm.mode == 'slide') { 15 | return createSlidePanel(vm); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/core/mixins/mix-panel.js: -------------------------------------------------------------------------------- 1 | // begin importing 2 | import { createPanel as createNativePanel } from 'mode/native/native-panel'; 3 | import { createPanel as createSlidePanel } from 'mode/slide/slide-panel'; 4 | /** 5 | * create a scrollPanel 6 | * 7 | * @param {any} size 8 | * @param {any} vm 9 | * @returns 10 | */ 11 | export default function createPanel(vm) { 12 | if (vm.mode == 'native') { 13 | return createNativePanel(vm); 14 | } else if (vm.mode == 'slide') { 15 | return createSlidePanel(vm); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: 'eslint:recommended', 4 | parser: 'babel-eslint', 5 | rules: { 6 | semi: ['error', 'always'], 7 | 'no-undef': 0, 8 | 'no-unused-vars': [ 9 | 'error', 10 | { 11 | argsIgnorePattern: '^h$', 12 | varsIgnorePattern: '^h$' 13 | } 14 | ], 15 | quotes: ['error', 'single'], 16 | excludedFiles: 'dist/*.js'.anchor, 17 | 'no-console': [0], 18 | indent: 0 19 | }, 20 | parserOptions: { 21 | ecmaVersion: 6, 22 | sourceType: 'module', 23 | ecmaFeatures: { 24 | jsx: true, 25 | experimentalObjectRestSpread: true 26 | } 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /scripts/debug-build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const aliases = require('./alias'); 3 | const path = require('path'); 4 | const { build, blue } = require('./build'); 5 | let builds = require('./config').getAllBuilds(); 6 | 7 | const filePath = `${aliases.src}`; 8 | let timeout; 9 | 10 | print('Start building in debug mode....\n'); 11 | 12 | fs.watch(path.resolve('./', filePath), { recursive: true }, function( 13 | event, 14 | filename 15 | ) { 16 | print('Detected ' + event); 17 | if (filename) { 18 | print('Filename provided: ' + filename); 19 | } else { 20 | print('Filename not provided'); 21 | } 22 | 23 | clearTimeout(timeout); 24 | timeout = setTimeout(() => { 25 | build(builds); 26 | timeout = null; 27 | }, 500); 28 | }); 29 | 30 | function print(str) { 31 | console.log(blue(str)); 32 | } 33 | -------------------------------------------------------------------------------- /src/shared/scroll-map.js: -------------------------------------------------------------------------------- 1 | export const scrollMap = { 2 | vertical: { 3 | size: 'height', 4 | opsSize: 'width', 5 | posName: 'top', 6 | opposName: 'bottom', 7 | sidePosName: 'right', 8 | page: 'pageY', 9 | scroll: 'scrollTop', 10 | scrollSize: 'scrollHeight', 11 | offset: 'offsetHeight', 12 | client: 'clientY', 13 | axis: 'Y', 14 | scrollButton: { 15 | start: 'top', 16 | end: 'bottom' 17 | } 18 | }, 19 | horizontal: { 20 | size: 'width', 21 | opsSize: 'height', 22 | posName: 'left', 23 | opposName: 'right', 24 | sidePosName: 'bottom', 25 | page: 'pageX', 26 | scroll: 'scrollLeft', 27 | scrollSize: 'scrollWidth', 28 | offset: 'offsetWidth', 29 | client: 'clientX', 30 | axis: 'X', 31 | scrollButton: { 32 | start: 'left', 33 | end: 'right' 34 | } 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /src/mode/mix/config.js: -------------------------------------------------------------------------------- 1 | import { log, modes } from 'shared'; 2 | import { 3 | configs as slideConfig, 4 | configValidator as slideValidator 5 | } from 'mode/slide/config'; 6 | import { configs as nativeConfig } from 'mode/native/config'; 7 | 8 | const { error } = log; 9 | 10 | const config = { 11 | // vuescroll 12 | vuescroll: { 13 | mode: 'native' 14 | } 15 | }; 16 | /** 17 | * validate the options 18 | * @export 19 | * @param {any} ops 20 | */ 21 | function configValidator(ops) { 22 | let renderError = false; 23 | const { vuescroll } = ops; 24 | 25 | // validate modes 26 | if (!~modes.indexOf(vuescroll.mode)) { 27 | error( 28 | `Unknown mode: ${vuescroll.mode},the vuescroll's option "mode" should be one of the ${modes}` 29 | ); 30 | renderError = true; 31 | } 32 | 33 | return renderError; 34 | } 35 | 36 | export const configs = [config, ...slideConfig, ...nativeConfig]; 37 | export const configValidators = [configValidator, slideValidator]; 38 | -------------------------------------------------------------------------------- /src/mode/mix/mixins/api.js: -------------------------------------------------------------------------------- 1 | import nativeApi from 'mode/native/mixins/api'; 2 | import slideApi from 'mode/slide/mixins/api'; 3 | 4 | export default { 5 | // mix slide and nitive modes apis. 6 | mixins: [slideApi, nativeApi], 7 | methods: { 8 | // private api 9 | internalScrollTo(destX, destY, speed, easing) { 10 | if (this.mode == 'native') { 11 | this.nativeScrollTo(destX, destY, speed, easing); 12 | } 13 | // for non-native we use scroller's scorllTo 14 | else if (this.mode == 'slide') { 15 | this.slideScrollTo(destX, destY, speed, easing); 16 | } 17 | }, 18 | stop() { 19 | this.nativeStop(); 20 | }, 21 | pause() { 22 | this.nativePause(); 23 | }, 24 | continue() { 25 | this.nativeContinue(); 26 | }, 27 | getCurrentviewDom() { 28 | return this.mode == 'slide' 29 | ? this.getCurrentviewDomSlide() 30 | : this.getCurrentviewDomNative(); 31 | } 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/core/mixins/config.js: -------------------------------------------------------------------------------- 1 | import { modes } from 'shared/constants'; 2 | import { error } from 'shared/util'; 3 | import { 4 | config as slideConfig, 5 | configValidator as slideValidator 6 | } from 'mode/slide/config'; 7 | 8 | import { config as nativeConfig } from 'mode/native/config'; 9 | 10 | const config = { 11 | // vuescroll 12 | vuescroll: { 13 | mode: 'native' 14 | } 15 | }; 16 | /** 17 | * validate the options 18 | * @export 19 | * @param {any} ops 20 | */ 21 | function configValidator(ops) { 22 | let renderError = false; 23 | const { vuescroll } = ops; 24 | 25 | // validate modes 26 | if (!~modes.indexOf(vuescroll.mode)) { 27 | error( 28 | `Unknown mode: ${ 29 | vuescroll.mode 30 | },the vuescroll's option "mode" should be one of the ${modes}` 31 | ); 32 | renderError = true; 33 | } 34 | 35 | return renderError; 36 | } 37 | 38 | export const configs = [config, slideConfig, nativeConfig]; 39 | export const configValidators = [configValidator, slideValidator]; 40 | -------------------------------------------------------------------------------- /test/unit/specs/util.spec.js: -------------------------------------------------------------------------------- 1 | import { deepCopy, mergeObject, isIos } from 'src/shared'; 2 | 3 | describe('Util', () => { 4 | it('after deeping copy, b[1].a1 should be 2', () => { 5 | const a = [{ a1: 1 }, { a1: 2 }]; 6 | const b = deepCopy(a); 7 | expect(b[1].a1).toBe(2); 8 | a[1].a1 = 3; 9 | expect(b[1].a1).toBe(2); 10 | }); 11 | 12 | it('deep copy a dom', () => { 13 | const a = document.createElement('div'); 14 | const b = deepCopy(a, b); 15 | expect(b.nodeType).not.toBe(null); 16 | }); 17 | 18 | it('deep merge shallowly,force', () => { 19 | const foo = document.createElement('div'); 20 | const a = [[1], [2], foo]; 21 | const b = [undefined, 2, 'bar']; 22 | const c = mergeObject(a, b, true /* force */, true /* shallow */); 23 | 24 | expect(c[0][0]).toBe(1); 25 | expect(c[1][0]).toBe(2); 26 | expect(c[2].nodeType).not.toBe(null); 27 | }); 28 | 29 | it('isIos should return false', () => { 30 | const IOS = isIos(); 31 | 32 | expect(IOS).toBe(false); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Yi(Yves) Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /src/shared/zoomManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * ZoomManager 3 | * Get the browser zoom ratio 4 | */ 5 | 6 | export class ZoomManager { 7 | constructor() { 8 | this.originPixelRatio = this.getRatio(); 9 | this.lastPixelRatio = this.originPixelRatio; 10 | window.addEventListener('resize', () => { 11 | this.lastPixelRatio = this.getRatio(); 12 | }); 13 | } 14 | getRatio() { 15 | let ratio = 0; 16 | const screen = window.screen; 17 | const ua = navigator.userAgent.toLowerCase(); 18 | 19 | if (window.devicePixelRatio !== undefined) { 20 | ratio = window.devicePixelRatio; 21 | } else if (~ua.indexOf('msie')) { 22 | if (screen.deviceXDPI && screen.logicalXDPI) { 23 | ratio = screen.deviceXDPI / screen.logicalXDPI; 24 | } 25 | } else if ( 26 | window.outerWidth !== undefined && 27 | window.innerWidth !== undefined 28 | ) { 29 | ratio = window.outerWidth / window.innerWidth; 30 | } 31 | 32 | if (ratio) { 33 | ratio = Math.round(ratio * 100); 34 | } 35 | 36 | return ratio; 37 | } 38 | getRatioBetweenPreAndCurrent() { 39 | return this.originPixelRatio / this.lastPixelRatio; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/unit/specs/mode.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | destroyVM, 3 | createVue, 4 | makeTemplate, 5 | startSchedule 6 | } from 'test/unit/util'; 7 | 8 | describe('mode', () => { 9 | let vm; 10 | 11 | afterEach(() => { 12 | destroyVM(vm); 13 | }); 14 | 15 | it('toggle mode', done => { 16 | vm = createVue( 17 | { 18 | template: makeTemplate( 19 | { 20 | w: 200, 21 | h: 200 22 | }, 23 | { 24 | w: 100, 25 | h: 100 26 | } 27 | ), 28 | data: { 29 | ops: { 30 | vuescroll: { 31 | mode: 'native' 32 | } 33 | } 34 | } 35 | }, 36 | true 37 | ); 38 | 39 | let content = vm.$el.querySelector('.__view'); 40 | let panel = vm.$el.querySelector('.__panel'); 41 | expect(content).not.toBe(null); 42 | expect(content.parentNode).toEqual(panel); 43 | vm.ops.vuescroll.mode = 'slide'; 44 | startSchedule().then(() => { 45 | let content = vm.$el.querySelector('.__view'); 46 | let panel = vm.$el.querySelector('.__panel'); 47 | expect(panel).not.toBe(null); 48 | expect(content).toBe(null); 49 | done(); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/core/third-party/scroller/render.js: -------------------------------------------------------------------------------- 1 | import { getPrefix } from 'shared'; 2 | 3 | /* DOM-based rendering (Uses 3D when available, falls back on margin when transform not available) */ 4 | export function render(content, global, suffix, type) { 5 | if (type == 'position') { 6 | return function (left, top) { 7 | content.style.left = -left + 'px'; 8 | content.style.top = -top + 'px'; 9 | }; 10 | } 11 | 12 | var vendorPrefix = getPrefix(global); 13 | 14 | var helperElem = document.createElement('div'); 15 | var undef; 16 | 17 | var perspectiveProperty = vendorPrefix + 'Perspective'; 18 | var transformProperty = 'transform'; //vendorPrefix + 'Transform'; 19 | 20 | if (helperElem.style[perspectiveProperty] !== undef) { 21 | return function (left, top, zoom) { 22 | content.style[transformProperty] = 23 | 'translate3d(' + 24 | -left + 25 | suffix + 26 | ',' + 27 | -top + 28 | suffix + 29 | ',0) scale(' + 30 | zoom + 31 | ')'; 32 | }; 33 | } else if (helperElem.style[transformProperty] !== undef) { 34 | return function (left, top, zoom) { 35 | content.style[transformProperty] = 36 | 'translate(' + 37 | -left + 38 | suffix + 39 | ',' + 40 | -top + 41 | suffix + 42 | ') scale(' + 43 | zoom + 44 | ')'; 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/core/components/panel.js: -------------------------------------------------------------------------------- 1 | // begin importing 2 | import { insertChildrenIntoSlot, getRealParent } from 'shared'; 3 | import { h } from 'vue'; 4 | 5 | export default { 6 | name: 'ScrollPanel', 7 | props: { ops: { type: Object, required: true } }, 8 | methods: { 9 | // trigger scrollPanel options initialScrollX, 10 | // initialScrollY 11 | updateInitialScroll() { 12 | let x = 0; 13 | let y = 0; 14 | const parent = getRealParent(this); 15 | 16 | if (this.ops.initialScrollX) { 17 | x = this.ops.initialScrollX; 18 | } 19 | if (this.ops.initialScrollY) { 20 | y = this.ops.initialScrollY; 21 | } 22 | if (x || y) { 23 | parent.scrollTo({ x, y }); 24 | } 25 | } 26 | }, 27 | mounted() { 28 | setTimeout(() => { 29 | if (!this._isDestroyed) { 30 | this.updateInitialScroll(); 31 | } 32 | }, 0); 33 | }, 34 | render() { 35 | // eslint-disable-line 36 | let data = { 37 | class: ['__panel'], 38 | style: { 39 | position: 'relative', 40 | boxSizing: 'border-box' 41 | } 42 | }; 43 | 44 | const parent = getRealParent(this); 45 | 46 | const _customPanel = parent.$slots['scroll-panel']; 47 | if (_customPanel) { 48 | return insertChildrenIntoSlot(_customPanel, this.$slots.default, data); 49 | } 50 | return
{this.$slots.default()}
; 51 | } 52 | }; 53 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /src/core/third-party/resize-detector/index.js: -------------------------------------------------------------------------------- 1 | // detect content size change 2 | import { eventCenter, isIE } from 'shared'; 3 | export function installResizeDetection(element, callback) { 4 | return injectObject(element, callback); 5 | } 6 | 7 | function injectObject(element, callback) { 8 | if (element.hasResized) { 9 | return; 10 | } 11 | 12 | var OBJECT_STYLE = 13 | 'display: block; position: absolute; top: 0; left: 0; width: 100%; height: 100%; border: none; padding: 0; margin: 0; opacity: 0; z-index: -1000; pointer-events: none;'; 14 | // define a wrap due to ie's zIndex bug 15 | var objWrap = document.createElement('div'); 16 | objWrap.style.cssText = OBJECT_STYLE; 17 | var object = document.createElement('object'); 18 | object.style.cssText = OBJECT_STYLE; 19 | object.type = 'text/html'; 20 | object.tabIndex = -1; 21 | 22 | object.onload = () => { 23 | eventCenter(object.contentDocument.defaultView, 'resize', callback); 24 | }; 25 | // https://github.com/wnr/element-resize-detector/blob/aafe9f7ea11d1eebdab722c7c5b86634e734b9b8/src/detection-strategy/object.js#L159 26 | if (!isIE()) { 27 | object.data = 'about:blank'; 28 | } 29 | objWrap.isResizeElm = true; 30 | objWrap.appendChild(object); 31 | element.appendChild(objWrap); 32 | if (isIE()) { 33 | object.data = 'about:blank'; 34 | } 35 | return function destroy() { 36 | if (object.contentDocument) { 37 | eventCenter( 38 | object.contentDocument.defaultView, 39 | 'resize', 40 | callback, 41 | 'off' 42 | ); 43 | } 44 | element.removeChild(objWrap); 45 | element.hasResized = false; 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | defaults: &defaults 6 | working_directory: ~/project/vuescroll 7 | docker: 8 | - image: circleci/node:10-browsers 9 | version: 2 10 | jobs: 11 | install: 12 | <<: *defaults 13 | steps: 14 | - checkout 15 | # Download and cache dependencies 16 | - restore_cache: 17 | keys: 18 | - v1-dependencies-{{ checksum "package.json" }} 19 | # fallback to using the latest cache if no exact match is found 20 | - v1-dependencies- 21 | - run: sudo npm install 22 | - save_cache: 23 | paths: 24 | - node_modules 25 | key: v1-dependencies-{{ checksum "package.json" }} 26 | - persist_to_workspace: 27 | root: ~/project 28 | paths: 29 | - vuescroll 30 | test-cover: 31 | <<: *defaults 32 | steps: 33 | - attach_workspace: 34 | at: ~/project 35 | # run tests! 36 | - run: npm run test 37 | - run: 38 | name: report coverage stats for non-PRs 39 | command: | 40 | if [[ -z $CI_PULL_REQUEST ]]; then 41 | cat ./test/coverage/lcov.info | ./node_modules/.bin/codecov 42 | fi 43 | lint: 44 | <<: *defaults 45 | steps: 46 | - attach_workspace: 47 | at: ~/project 48 | # run tests! 49 | - run: npm run lint 50 | workflows: 51 | version: 2 52 | install-and-parallel-test: 53 | jobs: 54 | - install 55 | - test-cover: 56 | requires: 57 | - install 58 | - lint: 59 | requires: 60 | - install 61 | -------------------------------------------------------------------------------- /src/core/third-party/scroller/requestAnimationFrame.js: -------------------------------------------------------------------------------- 1 | export function requestAnimationFrame(global) { 2 | // Check for request animation Frame support 3 | var requestFrame = 4 | global.requestAnimationFrame || 5 | global.webkitRequestAnimationFrame || 6 | global.mozRequestAnimationFrame || 7 | global.oRequestAnimationFrame; 8 | var isNative = !!requestFrame; 9 | 10 | if ( 11 | requestFrame && 12 | !/requestAnimationFrame\(\)\s*\{\s*\[native code\]\s*\}/i.test( 13 | requestFrame.toString() 14 | ) 15 | ) { 16 | isNative = false; 17 | } 18 | 19 | if (isNative) { 20 | return function(callback, root) { 21 | requestFrame(callback, root); 22 | }; 23 | } 24 | 25 | var TARGET_FPS = 60; 26 | var requests = {}; 27 | var rafHandle = 1; 28 | var intervalHandle = null; 29 | var lastActive = +new Date(); 30 | 31 | return function(callback) { 32 | var callbackHandle = rafHandle++; 33 | 34 | // Store callback 35 | requests[callbackHandle] = callback; 36 | 37 | // Create timeout at first request 38 | if (intervalHandle === null) { 39 | intervalHandle = setInterval(function() { 40 | var time = +new Date(); 41 | var currentRequests = requests; 42 | 43 | // Reset data structure before executing callbacks 44 | requests = {}; 45 | 46 | for (var key in currentRequests) { 47 | if (currentRequests.hasOwnProperty(key)) { 48 | currentRequests[key](time); 49 | lastActive = time; 50 | } 51 | } 52 | 53 | // Disable the timeout when nothing happens for a certain 54 | // period of time 55 | if (time - lastActive > 2500) { 56 | clearInterval(intervalHandle); 57 | intervalHandle = null; 58 | } 59 | }, 1000 / TARGET_FPS); 60 | } 61 | 62 | return callbackHandle; 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /test/unit/specs/event.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | destroyVM, 3 | createVue, 4 | makeTemplate, 5 | startSchedule 6 | } from 'test/unit/util'; 7 | 8 | let _r = () => {}; 9 | const callResize = () => { 10 | _r(); 11 | }; 12 | 13 | describe('handle-resize', () => { 14 | let vm; 15 | 16 | afterEach(() => { 17 | destroyVM(vm); 18 | }); 19 | 20 | it('toggle mode test resize', (done) => { 21 | vm = createVue( 22 | { 23 | template: makeTemplate( 24 | { 25 | w: 198, 26 | h: 198 27 | }, 28 | { 29 | w: 99, 30 | h: 99 31 | }, 32 | '@handle-resize="handleResize"' 33 | ), 34 | data: { 35 | ops: { 36 | vuescroll: { 37 | mode: 'native' 38 | } 39 | } 40 | }, 41 | methods: { 42 | handleResize() { 43 | callResize(); 44 | } 45 | } 46 | }, 47 | true 48 | ); 49 | let hBar; 50 | let content = vm.$el.querySelector('.__view > div'); 51 | startSchedule() 52 | .then((r) => { 53 | hBar = vm.$el.querySelector('.__bar-is-horizontal'); 54 | expect(hBar).not.toBe(null); 55 | content.style.width = '99px'; 56 | _r = r; 57 | }) 58 | .then((r) => { 59 | hBar = vm.$el.querySelector('.__bar-is-horizontal'); 60 | expect(hBar).toBe(null); 61 | content.style.width = '198px'; 62 | _r = r; 63 | }) 64 | .then(() => { 65 | hBar = vm.$el.querySelector('.__bar-is-horizontal'); 66 | expect(hBar).not.toBe(null); 67 | // test slide mode 68 | vm.ops.vuescroll.mode = 'slide'; 69 | }) 70 | .wait(5) 71 | .then((r) => { 72 | content = vm.$el.querySelector('.__panel > div'); 73 | content.style.width = '99px'; 74 | _r = r; 75 | }) 76 | .then((r) => { 77 | hBar = vm.$el.querySelector('.__bar-is-horizontal'); 78 | expect(hBar).toBe(null); 79 | content.style.width = '198px'; 80 | _r = r; 81 | }) 82 | .then(() => { 83 | hBar = vm.$el.querySelector('.__bar-is-horizontal'); 84 | expect(hBar).not.toBe(null); 85 | done(); 86 | }); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /scripts/build.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const rollup = require('rollup'); 4 | const { uglify } = require('rollup-plugin-uglify'); 5 | 6 | if (!fs.existsSync('dist')) { 7 | fs.mkdirSync('dist'); 8 | } 9 | 10 | let builds = require('./config').getAllBuilds(); 11 | 12 | build(builds); 13 | 14 | function build(builds) { 15 | let built = 0; 16 | const total = builds.length; 17 | const next = () => { 18 | buildEntry(builds[built]) 19 | .then(() => { 20 | built++; 21 | if (built < total) { 22 | next(); 23 | } else { 24 | if (process.env.VS_ENV != 'DEBUG') { 25 | copyOtherFiles(); 26 | } 27 | } 28 | }) 29 | .catch(logError); 30 | }; 31 | 32 | next(); 33 | } 34 | 35 | function buildEntry(config) { 36 | const output = config.output; 37 | const { file } = output; 38 | const isProd = /min\.js$/.test(file); 39 | if (isProd) { 40 | (config.plugins || (config.plugins = [])).push( 41 | uglify({ 42 | output: { 43 | comments: 'some' 44 | } 45 | }) 46 | ); 47 | } 48 | // eslint-disable-next-line 49 | return rollup.rollup(config).then(async (bundle) => { 50 | const ot = await bundle.generate(output); 51 | const fileName = path.basename(output.file); 52 | await report(fileName, ot.output[0].code); 53 | return bundle.write(output); 54 | }); 55 | } 56 | 57 | async function report(fileName, code) { 58 | const size = await getSize(code); 59 | console.log(blue(path.relative(process.cwd(), fileName)) + ' ' + size); 60 | } 61 | 62 | function getSize(code) { 63 | return (code.length / 1024).toFixed(2) + 'kb'; 64 | } 65 | 66 | function logError(e) { 67 | console.log(e); 68 | } 69 | 70 | function blue(str) { 71 | return '\x1b[1m\x1b[34m' + str + '\x1b[39m\x1b[22m'; 72 | } 73 | 74 | const resolve = (dir) => path.resolve(__dirname, '../', dir); 75 | const copyFilesArr = [ 76 | resolve('types/vuescroll-native.d.ts'), 77 | resolve('types/vuescroll-slide.d.ts') 78 | ]; 79 | function copyOtherFiles() { 80 | copyFilesArr.forEach((f) => { 81 | const file = fs.readFileSync(f, 'utf8'); 82 | report(f, file); 83 | fs.writeFileSync(resolve(`dist/${path.basename(f)}`), file, 'utf8'); 84 | }); 85 | } 86 | 87 | module.exports = { 88 | build, 89 | blue 90 | }; 91 | -------------------------------------------------------------------------------- /examples/base-scroll.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Document 7 | 8 | 9 | 34 | 35 | 36 |
37 |
38 |

🔥 vuescroll demo - base scroll

39 |

40 | 41 | 👉 Go to online demo 43 |

44 |

📝 Go to docs

45 |

46 | ⭐ Star it on GitHub! 49 |

50 |
51 |
52 | 53 |
{{i}}
54 |
55 |
56 |
57 | 58 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /src/core/mixins/api.js: -------------------------------------------------------------------------------- 1 | import { log, isChildInParent, getNumericValue } from 'shared'; 2 | const { warn } = log; 3 | export default { 4 | mounted() { 5 | vsInstances[this.$.uid] = this; 6 | }, 7 | beforeUnmount() { 8 | /* istanbul ignore next */ 9 | delete vsInstances[this.$.uid]; 10 | }, 11 | methods: { 12 | // public api 13 | scrollTo({ x, y }, speed, easing) { 14 | // istanbul ignore if 15 | if (speed === true || typeof speed == 'undefined') { 16 | speed = this.mergedOptions.scrollPanel.speed; 17 | } 18 | this.internalScrollTo(x, y, speed, easing); 19 | }, 20 | scrollBy({ dx = 0, dy = 0 }, speed, easing) { 21 | let { scrollLeft = 0, scrollTop = 0 } = this.getPosition(); 22 | if (dx) { 23 | scrollLeft += getNumericValue( 24 | dx, 25 | this.scrollPanelElm.scrollWidth - this.$el.clientWidth 26 | ); 27 | } 28 | if (dy) { 29 | scrollTop += getNumericValue( 30 | dy, 31 | this.scrollPanelElm.scrollHeight - this.$el.clientHeight 32 | ); 33 | } 34 | this.internalScrollTo(scrollLeft, scrollTop, speed, easing); 35 | }, 36 | scrollIntoView(elm, animate = true) { 37 | const parentElm = this.$el; 38 | 39 | if (typeof elm === 'string') { 40 | elm = parentElm.querySelector(elm); 41 | } 42 | 43 | if (!isChildInParent(elm, parentElm)) { 44 | warn( 45 | 'The element or selector you passed is not the element of Vuescroll, please pass the element that is in Vuescroll to scrollIntoView API. ' 46 | ); 47 | return; 48 | } 49 | 50 | // parent elm left, top 51 | const { left, top } = this.$el.getBoundingClientRect(); 52 | // child elm left, top 53 | const { left: childLeft, top: childTop } = elm.getBoundingClientRect(); 54 | 55 | const diffX = left - childLeft; 56 | const diffY = top - childTop; 57 | 58 | this.scrollBy( 59 | { 60 | dx: -diffX, 61 | dy: -diffY 62 | }, 63 | animate 64 | ); 65 | }, 66 | refresh() { 67 | this.refreshInternalStatus(); 68 | // refresh again to keep status is correct 69 | this.$nextTick(this.refreshInternalStatus); 70 | } 71 | } 72 | }; 73 | 74 | /** Public Api */ 75 | 76 | /** 77 | * Refresh all 78 | */ 79 | const vsInstances = {}; 80 | export function refreshAll() { 81 | for (let vs in vsInstances) { 82 | vsInstances[vs].refresh(); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/core/third-party/easingPattern/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Compatible to scroller's animation function 3 | */ 4 | export function createEasingFunction(easing, easingPattern) { 5 | return function(time) { 6 | return easingPattern(easing, time); 7 | }; 8 | } 9 | 10 | /** 11 | * Calculate the easing pattern 12 | * @link https://github.com/cferdinandi/smooth-scroll/blob/master/src/js/smooth-scroll.js 13 | * modified by wangyi7099 14 | * @param {String} type Easing pattern 15 | * @param {Number} time Time animation should take to complete 16 | * @returns {Number} 17 | */ 18 | export function easingPattern(easing, time) { 19 | let pattern = null; 20 | /* istanbul ignore next */ 21 | { 22 | // Default Easing Patterns 23 | if (easing === 'easeInQuad') pattern = time * time; // accelerating from zero velocity 24 | if (easing === 'easeOutQuad') pattern = time * (2 - time); // decelerating to zero velocity 25 | if (easing === 'easeInOutQuad') 26 | pattern = time < 0.5 ? 2 * time * time : -1 + (4 - 2 * time) * time; // acceleration until halfway, then deceleration 27 | if (easing === 'easeInCubic') pattern = time * time * time; // accelerating from zero velocity 28 | if (easing === 'easeOutCubic') pattern = --time * time * time + 1; // decelerating to zero velocity 29 | if (easing === 'easeInOutCubic') 30 | pattern = 31 | time < 0.5 32 | ? 4 * time * time * time 33 | : (time - 1) * (2 * time - 2) * (2 * time - 2) + 1; // acceleration until halfway, then deceleration 34 | if (easing === 'easeInQuart') pattern = time * time * time * time; // accelerating from zero velocity 35 | if (easing === 'easeOutQuart') pattern = 1 - --time * time * time * time; // decelerating to zero velocity 36 | if (easing === 'easeInOutQuart') 37 | pattern = 38 | time < 0.5 39 | ? 8 * time * time * time * time 40 | : 1 - 8 * --time * time * time * time; // acceleration until halfway, then deceleration 41 | if (easing === 'easeInQuint') pattern = time * time * time * time * time; // accelerating from zero velocity 42 | if (easing === 'easeOutQuint') 43 | pattern = 1 + --time * time * time * time * time; // decelerating to zero velocity 44 | if (easing === 'easeInOutQuint') 45 | pattern = 46 | time < 0.5 47 | ? 16 * time * time * time * time * time 48 | : 1 + 16 * --time * time * time * time * time; // acceleration until halfway, then deceleration 49 | } 50 | return pattern || time; // no easing, no acceleration 51 | } 52 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # English Version 2 | 3 | Contributing code from two aspects: 4 | 5 | ### Code level 6 | 7 | Vuescroll is extremely easy to expand.You only have to do 2 steps 8 | 9 | 1. To modify / add the corresponding features at the corresponding modules in the [global-config.js](https://github.com/YvesCoding/blob/dev/src/shared/global-config.js) file, for example, I want to add a feature that can configure the color of the scrolling panel, the default is red, as follows: 10 | ![](https://github.com/wangyi7099/pictureCdn/blob/master/allPic/vuescroll/s1.jpg?raw=true) 11 | 2. Find the corresponding module file and modify it in the corresponding code of the module, as follows: 12 | ![](https://github.com/wangyi7099/pictureCdn/blob/master/allPic/vuescroll/s2.jpg?raw=true) 13 |
14 | ![](https://github.com/wangyi7099/pictureCdn/blob/master/allPic/vuescroll/s3.jpg?raw=true) 15 | 16 | ### Git level 17 | 18 | 1. Fork this repo. 19 | 2. Clone the repo you have just forked. 20 | 21 | ```base 22 | git clone git@github.com:/vuescroll.git 23 | ``` 24 | 25 | 3. Modify the code in your local and push the code to your remote repo(Commit messages should follow the [commit message convention](./COMMIT_CONVENTION.md) so that changelogs can be automatically generated). 26 | 4. Click `New pull request` in vuescroll repo as follows: 27 |
28 | 5. When I agree, your code will merge into the `dev` branch! 29 | 30 | # 中文版本 31 | 32 | 从两方面贡献代码: 33 | 34 | ### 代码层面 35 | 36 | Vuescroll 是极其容易扩展的,你基本只需要做 2 步即可。 37 | 38 | 1. 在 [global-config.js](https://github.com/YvesCoding/blob/dev/src/shared/global-config.js) 文件中对应的模块处修改/增加对应的特性,比如,我想增加一个可以配置滚动面板颜色的特性,默认是红色,如下图: 39 |
40 | ![](https://github.com/wangyi7099/pictureCdn/blob/master/allPic/vuescroll/s1.jpg?raw=true) 41 | 2. 找到对应的模块文件, 并在模块的对应的代码处修改即可,如下图: 42 |
43 | ![](https://github.com/wangyi7099/pictureCdn/blob/master/allPic/vuescroll/s2.jpg?raw=true) 44 |
45 | ![](https://github.com/wangyi7099/pictureCdn/blob/master/allPic/vuescroll/s3.jpg?raw=true) 46 | 47 | ### Git 层面 48 | 49 | 1. 把这个项目 fork 下来。 50 | 2. 把你的 fork 的项目克隆下来 51 | 52 | ```base 53 | git clone git@github.com:/vuescroll.git 54 | ``` 55 | 56 | 3. 在你的本地修改代码然后 push 到你的远程仓库(提交消息应该遵循[[commit message convention](./COMMIT_CONVENTION.md) 以便能自动生成changelog) 57 | 4. 在 vuescroll 项目地址点击`New pull request`,如下图所示:
58 | 5. 等我点击同意, 你的代码就会被 merge 到`dev`分支了! 59 | -------------------------------------------------------------------------------- /src/mode/slide/config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The slide mode config 3 | */ 4 | import { log } from 'shared'; 5 | const { error } = log; 6 | export const configs = [ 7 | { 8 | // vuescroll 9 | vuescroll: { 10 | // position or transform 11 | renderMethod: 'transform', 12 | // pullRefresh or pushLoad is only for the slide mode... 13 | pullRefresh: { 14 | enable: false, 15 | tips: { 16 | deactive: 'Pull to Refresh', 17 | active: 'Release to Refresh', 18 | start: 'Refreshing...', 19 | beforeDeactive: 'Refresh Successfully!' 20 | } 21 | }, 22 | pushLoad: { 23 | enable: false, 24 | tips: { 25 | deactive: 'Push to Load', 26 | active: 'Release to Load', 27 | start: 'Loading...', 28 | beforeDeactive: 'Load Successfully!' 29 | }, 30 | auto: false, 31 | autoLoadDistance: 0 32 | }, 33 | paging: false, 34 | zooming: true, 35 | snapping: { 36 | enable: false, 37 | width: 100, 38 | height: 100 39 | }, 40 | /* some scroller options */ 41 | scroller: { 42 | /** Enable bouncing (content can be slowly moved outside and jumps back after releasing) */ 43 | bouncing: { 44 | top: 100, 45 | bottom: 100, 46 | left: 100, 47 | right: 100 48 | }, 49 | /** Minimum zoom level */ 50 | minZoom: 0.5, 51 | /** Maximum zoom level */ 52 | maxZoom: 3, 53 | /** Multiply or decrease scrolling speed **/ 54 | speedMultiplier: 1, 55 | /** This configures the amount of change applied to deceleration when reaching boundaries **/ 56 | penetrationDeceleration: 0.03, 57 | /** This configures the amount of change applied to acceleration when reaching boundaries **/ 58 | penetrationAcceleration: 0.08, 59 | /** Whether call e.preventDefault event when sliding the content or not */ 60 | preventDefault: false, 61 | /** Whether call preventDefault when (mouse/touch)move*/ 62 | preventDefaultOnMove: true, 63 | disable: false 64 | } 65 | } 66 | } 67 | ]; 68 | /** 69 | * validate the options 70 | * @export 71 | * @param {any} ops 72 | */ 73 | export function configValidator(ops) { 74 | let renderError = false; 75 | const { vuescroll } = ops; 76 | 77 | // validate pushLoad, pullReresh, snapping 78 | if ( 79 | vuescroll.paging == vuescroll.snapping.enable && 80 | vuescroll.paging && 81 | (vuescroll.pullRefresh || vuescroll.pushLoad) 82 | ) { 83 | error( 84 | 'paging, snapping, (pullRefresh with pushLoad) can only one of them to be true.' 85 | ); 86 | } 87 | 88 | return renderError; 89 | } 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuescroll", 3 | "version": "5.1.1", 4 | "author": { 5 | "name": "Yves Wang" 6 | }, 7 | "description": "A powerful, customizable, multi-mode scrollbar plugin based on Vue.js", 8 | "scripts": { 9 | "build": "cross-env BABEL_ENV=build node scripts/build.js", 10 | "debug": "cross-env BABEL_ENV=build VS_ENV=DEBUG node scripts/debug-build.js", 11 | "test": "npm run test:cover", 12 | "lint": "eslint --fix src scripts test", 13 | "test:cover": "karma start test/unit/karma.conf.js", 14 | "debug-test": "karma start test/unit/karma.conf.js --no-single-run", 15 | "report-coverage": "codecov", 16 | "gen-changelog": "conventional-changelog -p angular -i CHANGELOG.md -s" 17 | }, 18 | "license": "MIT", 19 | "homepage": "https://github.com/YvesCoding/vuescroll#readme", 20 | "keywords": [ 21 | "vuescroll", 22 | "scrollbar", 23 | "vuescrollbar", 24 | "vue" 25 | ], 26 | "bugs": { 27 | "url": "https://github.com/YvesCoding/vuescroll/issues" 28 | }, 29 | "peerDependencies": { 30 | "vue": "^3.0.0" 31 | }, 32 | "typings": "types/index.d.ts", 33 | "files": [ 34 | "src", 35 | "dist/*.js", 36 | "dist/*.css", 37 | "types/*.d.ts" 38 | ], 39 | "deprecated": false, 40 | "jsdelivr": "./dist/vuescroll.js", 41 | "main": "./dist/vuescroll.js", 42 | "unpkg": "./dist/vuescroll.js", 43 | "module": "./dist/vuescroll-esm.js", 44 | "repository": { 45 | "type": "git", 46 | "url": "git+https://github.com/YvesCoding/vuescroll.git" 47 | }, 48 | "devDependencies": { 49 | "@babel/core": "^7.12.16", 50 | "@babel/eslint-parser": "^7.12.16", 51 | "@babel/plugin-syntax-jsx": "^7.12.13", 52 | "@babel/plugin-transform-spread": "^7.12.13", 53 | "@babel/preset-env": "^7.12.16", 54 | "@rollup/plugin-alias": "^3.1.2", 55 | "@rollup/plugin-babel": "^5.2.3", 56 | "@rollup/plugin-commonjs": "^17.1.0", 57 | "@rollup/plugin-node-resolve": "^11.1.1", 58 | "@rollup/plugin-replace": "^2.3.4", 59 | "@vue/babel-helper-vue-jsx-merge-props": "^1.2.1", 60 | "@vue/babel-plugin-jsx": "^1.1.1", 61 | "babel-eslint": "^10.1.0", 62 | "babel-loader": "^8.2.3", 63 | "babel-plugin-istanbul": "^4.1.5", 64 | "babel-preset-es2015-rollup": "^3.0.0", 65 | "codecov": "^3.0.0", 66 | "cross-env": "^5.2.0", 67 | "css-loader": "^0.28.11", 68 | "eslint": "^4.13.1", 69 | "husky": "^2.3.0", 70 | "istanbul": "^0.4.5", 71 | "jasmine-core": "^2.9.1", 72 | "karma": "^4.4.1", 73 | "karma-chrome-launcher": "^2.2.0", 74 | "karma-coverage": "^1.1.1", 75 | "karma-jasmine": "^1.1.1", 76 | "karma-phantomjs-launcher": "^1.0.4", 77 | "karma-sourcemap-loader": "^0.3.7", 78 | "karma-webpack": "^2.0.9", 79 | "rollup": "2.38.5", 80 | "rollup-plugin-uglify": "^6.0.4", 81 | "vue": "^3.0.0", 82 | "webpack": "^4.0.0" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/unit/specs/class-hooks.spec.js: -------------------------------------------------------------------------------- 1 | import { 2 | destroyVM, 3 | createVue, 4 | makeTemplate, 5 | trigger, 6 | startSchedule 7 | } from 'test/unit/util'; 8 | 9 | const hasClass = (el, name) => { 10 | return el.classList.contains(name); 11 | }; 12 | 13 | /** 14 | * we won't test mode and zooming here, 15 | * instead, we test it in 16 | * *-mode.spec.js and api/index.spec.js 17 | */ 18 | describe('vuescroll', () => { 19 | let vm; 20 | 21 | afterEach(() => { 22 | destroyVM(vm); 23 | }); 24 | 25 | it('class hook: hasVBar, hasHBar', done => { 26 | vm = createVue( 27 | { 28 | template: makeTemplate( 29 | { 30 | w: 200, 31 | h: 200 32 | }, 33 | { 34 | w: 100, 35 | h: 100 36 | } 37 | ), 38 | data: { 39 | ops: { 40 | vuescroll: { 41 | sizeStrategy: 'number' 42 | } 43 | } 44 | } 45 | }, 46 | true 47 | ); 48 | const vs = vm.$refs['vs'].$el; 49 | const vmel = vm.$el; 50 | 51 | startSchedule() 52 | .then(() => { 53 | expect(hasClass(vs, 'hasVBar')).toBe(true); 54 | expect(hasClass(vs, 'hasHBar')).toBe(true); 55 | 56 | vmel.style.width = '200px'; 57 | vmel.style.height = '200px'; 58 | vm.$refs['vs'].refresh(); 59 | }) 60 | .wait(100) 61 | .then(() => { 62 | expect(hasClass(vs, 'hasVBar')).toBe(false); 63 | expect(hasClass(vs, 'hasHBar')).toBe(false); 64 | 65 | done(); 66 | }); 67 | }); 68 | 69 | it('class hook: vBarVisible, hBarVisible', done => { 70 | vm = createVue( 71 | { 72 | template: makeTemplate( 73 | { 74 | w: 200, 75 | h: 200 76 | }, 77 | { 78 | w: 100, 79 | h: 100 80 | } 81 | ), 82 | data: { 83 | ops: { 84 | bar: { 85 | onlyShowBarOnScroll: false 86 | } 87 | } 88 | } 89 | }, 90 | true 91 | ); 92 | const vs = vm.$refs['vs'].$el; 93 | 94 | startSchedule(1000) 95 | .then(() => { 96 | expect(hasClass(vs, 'vBarVisible')).toBe(false); 97 | expect(hasClass(vs, 'hBarVisible')).toBe(false); 98 | 99 | trigger(vs, 'mouseenter'); 100 | }) 101 | .wait(1) 102 | .then(() => { 103 | expect(hasClass(vs, 'vBarVisible')).toBe(true); 104 | expect(hasClass(vs, 'hBarVisible')).toBe(true); 105 | 106 | trigger(vs, 'mouseleave'); 107 | }) 108 | .wait(1) 109 | .then(() => { 110 | expect(hasClass(vs, 'vBarVisible')).toBe(false); 111 | expect(hasClass(vs, 'hBarVisible')).toBe(false); 112 | 113 | done(); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /test/unit/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration 2 | // Generated on Tue Jan 30 2018 19:35:39 GMT+0800 (中国标准时间) 3 | 4 | var alias = require('../../scripts/alias'); 5 | var webpack = { 6 | mode: 'development', 7 | resolve: { 8 | alias: alias 9 | }, 10 | module: { 11 | rules: [ 12 | { 13 | test: /\.js$/, 14 | loader: 'babel-loader', 15 | exclude: /node_modules/, 16 | options: { 17 | plugins: [ 18 | [ 19 | 'istanbul', 20 | { 21 | exclude: ['src/core/third-party/**/*.js', 'test/**/*.js'] 22 | } 23 | ] 24 | ] 25 | } 26 | } 27 | ] 28 | }, 29 | devtool: '#inline-source-map' 30 | }; 31 | module.exports = function (config) { 32 | config.set({ 33 | webpack: webpack, 34 | plugins: [ 35 | 'karma-jasmine', 36 | 'jasmine-core', 37 | 'karma-webpack', 38 | 'karma-coverage', 39 | 'karma-sourcemap-loader', 40 | //'karma-phantomjs-launcher' 41 | 'karma-chrome-launcher' 42 | ], 43 | // base path that will be used to resolve all patterns (eg. files, exclude) 44 | basePath: '', 45 | 46 | // frameworks to use 47 | // available frameworks: https://npmjs.org/browse/keyword/karma-adapter 48 | frameworks: ['jasmine'], 49 | 50 | // list of files / patterns to load in the browser 51 | files: ['./index.js'], 52 | 53 | // list of files / patterns to exclude 54 | exclude: [], 55 | 56 | // preprocess matching files before serving them to the browser 57 | // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor 58 | preprocessors: { 59 | './index.js': ['webpack', 'sourcemap'] 60 | }, 61 | 62 | // test results reporter to use 63 | // possible values: 'dots', 'progress' 64 | // available reporters: https://npmjs.org/browse/keyword/karma-reporter 65 | reporters: ['progress', 'coverage'], 66 | 67 | coverageReporter: { 68 | reporters: [ 69 | { type: 'lcov', dir: '../coverage', subdir: '../coverage' }, 70 | { type: 'text-summary', dir: '../coverage', subdir: '../coverage' } 71 | ] 72 | }, 73 | 74 | // enable / disable colors in the output (reporters and logs) 75 | colors: true, 76 | 77 | // level of logging 78 | // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG 79 | logLevel: config.LOG_INFO, 80 | 81 | // enable / disable watching file and executing tests whenever any file changes 82 | autoWatch: false, 83 | 84 | // start these browsers 85 | // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher 86 | browsers: ['Chrome'], 87 | 88 | // Continuous Integration mode 89 | // if true, Karma captures browsers, runs the tests and exits 90 | singleRun: true, 91 | 92 | // Concurrency level 93 | // how many browser should be started simultaneous 94 | concurrency: Infinity 95 | }); 96 | }; 97 | -------------------------------------------------------------------------------- /src/mode/native/mixins/scrollAnimate.js: -------------------------------------------------------------------------------- 1 | import { requestAnimationFrame } from 'core/third-party/scroller/requestAnimationFrame'; 2 | 3 | function noop() { 4 | return true; 5 | } 6 | 7 | /* istanbul ignore next */ 8 | const now = 9 | Date.now || 10 | function() { 11 | return new Date().getTime(); 12 | }; 13 | 14 | export default class ScrollControl { 15 | constructor() { 16 | this.init(); 17 | 18 | this.isRunning = false; 19 | } 20 | 21 | pause() { 22 | /* istanbul ignore if */ 23 | if (!this.isRunning) return; 24 | 25 | this.isPaused = true; 26 | } 27 | 28 | stop() { 29 | this.isStopped = true; 30 | } 31 | 32 | continue() { 33 | /* istanbul ignore if */ 34 | if (!this.isPaused) return; 35 | 36 | this.isPaused = false; 37 | this.ts = now() - this.percent * this.spd; 38 | this.execScroll(); 39 | } 40 | 41 | startScroll( 42 | st, 43 | ed, 44 | spd, 45 | stepCb = noop, 46 | completeCb = noop, 47 | vertifyCb = noop, 48 | easingMethod = noop 49 | ) { 50 | const df = ed - st; 51 | const dir = df > 0 ? -1 : 1; 52 | const nt = now(); 53 | 54 | if (!this.isRunning) { 55 | this.init(); 56 | } 57 | 58 | if (dir != this.dir || nt - this.ts > 200) { 59 | this.ts = nt; 60 | 61 | this.dir = dir; 62 | this.st = st; 63 | this.ed = ed; 64 | this.df = df; 65 | } /* istanbul ignore next */ else { 66 | this.df += df; 67 | } 68 | 69 | this.spd = spd; 70 | 71 | this.completeCb = completeCb; 72 | this.vertifyCb = vertifyCb; 73 | this.stepCb = stepCb; 74 | this.easingMethod = easingMethod; 75 | 76 | if (!this.isRunning) this.execScroll(); 77 | } 78 | 79 | execScroll() { 80 | if (!this.df) return; 81 | 82 | let percent = this.percent || 0; 83 | this.percent = 0; 84 | this.isRunning = true; 85 | 86 | const loop = () => { 87 | /* istanbul ignore if */ 88 | if (!this.isRunning || !this.vertifyCb(percent) || this.isStopped) { 89 | this.isRunning = false; 90 | return; 91 | } 92 | 93 | percent = (now() - this.ts) / this.spd; 94 | 95 | if (this.isPaused) { 96 | this.percent = percent; 97 | this.isRunning = false; 98 | return; 99 | } 100 | 101 | if (percent < 1) { 102 | const value = this.st + this.df * this.easingMethod(percent); 103 | this.stepCb(value); 104 | this.ref(loop); 105 | } else { 106 | // trigger complete 107 | this.stepCb(this.st + this.df); 108 | this.completeCb(); 109 | 110 | this.isRunning = false; 111 | } 112 | }; 113 | 114 | this.ref(loop); 115 | } 116 | 117 | init() { 118 | this.st = 0; 119 | this.ed = 0; 120 | this.df = 0; 121 | this.spd = 0; 122 | this.ts = 0; 123 | this.dir = 0; 124 | this.ref = requestAnimationFrame(window); 125 | 126 | this.isPaused = false; 127 | this.isStopped = false; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/mode/slide/mixins/api.js: -------------------------------------------------------------------------------- 1 | import { log, getNumericValue, getCurrentViewportDom } from 'shared'; 2 | const { warn } = log; 3 | 4 | export default { 5 | emits: [ 6 | 'refresh-activate', 7 | 'refresh-before-deactivate', 8 | 'refresh-before-deactivate-end', 9 | 'refresh-deactivate', 10 | 11 | 'load-activate', 12 | 'load-before-deactivate', 13 | 'load-before-deactivate-end', 14 | 'load-deactivate' 15 | ], 16 | methods: { 17 | slideScrollTo(x, y, speed, easing) { 18 | const { scrollLeft, scrollTop } = this.getPosition(); 19 | 20 | x = getNumericValue(x || scrollLeft, this.scroller.__maxScrollLeft); 21 | y = getNumericValue(y || scrollTop, this.scroller.__maxScrollTop); 22 | 23 | this.scroller.scrollTo(x, y, speed > 0, undefined, false, speed, easing); 24 | }, 25 | zoomBy(factor, animate, originLeft, originTop, callback) { 26 | if (!this.scroller) { 27 | warn('zoomBy and zoomTo are only for slide mode!'); 28 | return; 29 | } 30 | this.scroller.zoomBy(factor, animate, originLeft, originTop, callback); 31 | }, 32 | zoomTo(level, animate = false, originLeft, originTop, callback) { 33 | if (!this.scroller) { 34 | warn('zoomBy and zoomTo are only for slide mode!'); 35 | return; 36 | } 37 | this.scroller.zoomTo(level, animate, originLeft, originTop, callback); 38 | }, 39 | getCurrentPage() { 40 | if (!this.scroller || !this.mergedOptions.vuescroll.paging) { 41 | warn( 42 | 'getCurrentPage and goToPage are only for slide mode and paging is enble!' 43 | ); 44 | return; 45 | } 46 | return this.scroller.getCurrentPage(); 47 | }, 48 | goToPage(dest, animate = false) { 49 | if (!this.scroller || !this.mergedOptions.vuescroll.paging) { 50 | warn( 51 | 'getCurrentPage and goToPage are only for slide mode and paging is enble!' 52 | ); 53 | return; 54 | } 55 | this.scroller.goToPage(dest, animate); 56 | }, 57 | triggerRefreshOrLoad(type) { 58 | if (!this.scroller) { 59 | warn('You can only use triggerRefreshOrLoad in slide mode!'); 60 | return; 61 | } 62 | 63 | const isRefresh = this.mergedOptions.vuescroll.pullRefresh.enable; 64 | const isLoad = this.mergedOptions.vuescroll.pushLoad.enable; 65 | 66 | if (type == 'refresh' && !isRefresh) { 67 | warn('refresh must be enabled!'); 68 | return; 69 | } else if (type == 'load' && !isLoad) { 70 | // eslint-disable-next-line 71 | warn("load must be enabled and content's height > container's height!"); 72 | return; 73 | } else if (type !== 'refresh' && type !== 'load') { 74 | warn('param must be one of load and refresh!'); 75 | return; 76 | } 77 | 78 | /* istanbul ignore if */ 79 | if (this.vuescroll.state[`${type}Stage`] == 'start') { 80 | return; 81 | } 82 | 83 | this.scroller.triggerRefreshOrLoad(type); 84 | return true; 85 | }, 86 | getCurrentviewDomSlide() { 87 | const parent = this.scrollPanelElm; 88 | const domFragment = getCurrentViewportDom(parent, this.$el); 89 | return domFragment; 90 | } 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /.github/COMMIT_CONVENTION.md: -------------------------------------------------------------------------------- 1 | ## Git Commit Message Convention 2 | 3 | > This is adapted from [Angular's commit convention](https://github.com/conventional-changelog/conventional-changelog/tree/master/packages/conventional-changelog-angular). 4 | 5 | #### TL;DR: 6 | 7 | Messages must be matched by the following regex: 8 | 9 | ``` js 10 | /^(revert: )?(feat|fix|polish|docs|style|refactor|perf|test|workflow|ci|chore|types)(\(.+\))?: .{1,50}/ 11 | ``` 12 | 13 | #### Examples 14 | 15 | Appears under "Features" header, `compiler` subheader: 16 | 17 | ``` 18 | feat(compiler): add 'comments' option 19 | ``` 20 | 21 | Appears under "Bug Fixes" header, `v-model` subheader, with a link to issue #28: 22 | 23 | ``` 24 | fix(v-model): handle events on blur 25 | 26 | close #28 27 | ``` 28 | 29 | Appears under "Performance Improvements" header, and under "Breaking Changes" with the breaking change explanation: 30 | 31 | ``` 32 | perf(core): improve vdom diffing by removing 'foo' option 33 | 34 | BREAKING CHANGE: The 'foo' option has been removed. 35 | ``` 36 | 37 | The following commit and commit `667ecc1` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header. 38 | 39 | ``` 40 | revert: feat(compiler): add 'comments' option 41 | 42 | This reverts commit 667ecc1654a317a13331b17617d973392f415f02. 43 | ``` 44 | 45 | ### Full Message Format 46 | 47 | A commit message consists of a **header**, **body** and **footer**. The header has a **type**, **scope** and **subject**: 48 | 49 | ``` 50 | (): 51 | 52 | 53 | 54 |