Repository: ustbhuangyi/better-scroll Branch: dev Commit: f87fbd161d4b Files: 480 Total size: 1.3 MB Directory structure: gitextract_mo2zutw5/ ├── .editorconfig ├── .eslintrc.js ├── .github/ │ └── ISSUE_TEMPLATE.md ├── .gitignore ├── .gitpod.Dockerfile ├── .gitpod.yml ├── .huskyrc.json ├── .npmignore ├── .travis.yml ├── .yarnrc ├── LICENSE ├── README.md ├── README_zh-CN.md ├── jest-e2e.config.js ├── jest-puppeteer.config.js ├── jest.config.js ├── lerna.json ├── package.json ├── packages/ │ ├── better-scroll/ │ │ ├── README.md │ │ ├── README_zh-CN.md │ │ ├── package.json │ │ └── src/ │ │ └── index.ts │ ├── core/ │ │ ├── README.md │ │ ├── README_zh-CN.md │ │ ├── package.json │ │ └── src/ │ │ ├── BScroll.ts │ │ ├── Instance.ts │ │ ├── Options.ts │ │ ├── __mocks__/ │ │ │ ├── Options.ts │ │ │ └── index.ts │ │ ├── __tests__/ │ │ │ ├── Options.spec.ts │ │ │ ├── __utils__/ │ │ │ │ ├── event.ts │ │ │ │ └── layout.ts │ │ │ └── index.spec.ts │ │ ├── animater/ │ │ │ ├── Animation.ts │ │ │ ├── Base.ts │ │ │ ├── Transition.ts │ │ │ ├── __mocks__/ │ │ │ │ ├── Animation.ts │ │ │ │ ├── Transition.ts │ │ │ │ └── index.ts │ │ │ ├── __tests__/ │ │ │ │ ├── Animation.spec.ts │ │ │ │ ├── Transition.spec.ts │ │ │ │ └── index.spec.ts │ │ │ └── index.ts │ │ ├── base/ │ │ │ ├── ActionsHandler.ts │ │ │ ├── __mocks__/ │ │ │ │ └── ActionsHandler.ts │ │ │ └── __tests__/ │ │ │ └── ActionsHandler.spec.ts │ │ ├── index.ts │ │ ├── scroller/ │ │ │ ├── Actions.ts │ │ │ ├── Behavior.ts │ │ │ ├── DirectionLock.ts │ │ │ ├── Scroller.ts │ │ │ ├── __mocks__/ │ │ │ │ ├── Actions.ts │ │ │ │ ├── Behavior.ts │ │ │ │ ├── DirectionLock.ts │ │ │ │ └── Scroller.ts │ │ │ ├── __tests__/ │ │ │ │ ├── Actions.spec.ts │ │ │ │ ├── Behavior.spec.ts │ │ │ │ ├── DirectionLock.spec.ts │ │ │ │ ├── Scroller.spec.ts │ │ │ │ └── createOptions.spec.ts │ │ │ └── createOptions.ts │ │ ├── translater/ │ │ │ ├── __mocks__/ │ │ │ │ └── index.ts │ │ │ ├── __tests__/ │ │ │ │ └── index.spec.ts │ │ │ └── index.ts │ │ └── utils/ │ │ ├── __tests__/ │ │ │ └── bubbling.spec.ts │ │ ├── bubbling.ts │ │ ├── compare.ts │ │ ├── compat.ts │ │ └── typesHelper.ts │ ├── examples/ │ │ ├── README.md │ │ ├── build/ │ │ │ ├── vue-example-build.js │ │ │ └── vue-webpack.conf.js │ │ ├── package.json │ │ ├── postcss.config.js │ │ ├── static/ │ │ │ └── css/ │ │ │ ├── github-light.css │ │ │ ├── normalize.css │ │ │ ├── reset.css │ │ │ └── stylesheet.css │ │ ├── vue/ │ │ │ ├── App.vue │ │ │ ├── components/ │ │ │ │ ├── compose/ │ │ │ │ │ ├── pullup-pulldown-outnested.vue │ │ │ │ │ ├── pullup-pulldown-slide.vue │ │ │ │ │ ├── pullup-pulldown.vue │ │ │ │ │ └── slide-nested.vue │ │ │ │ ├── core/ │ │ │ │ │ ├── default.vue │ │ │ │ │ ├── dynamic-content.vue │ │ │ │ │ ├── freescroll.vue │ │ │ │ │ ├── horizontal-rotated.vue │ │ │ │ │ ├── horizontal.vue │ │ │ │ │ ├── specified-content.vue │ │ │ │ │ └── vertical-rotated.vue │ │ │ │ ├── form/ │ │ │ │ │ └── textarea.vue │ │ │ │ ├── indicators/ │ │ │ │ │ ├── minimap.vue │ │ │ │ │ └── parallax-scroll.vue │ │ │ │ ├── infinity/ │ │ │ │ │ ├── data/ │ │ │ │ │ │ └── message.json │ │ │ │ │ └── default.vue │ │ │ │ ├── mouse-wheel/ │ │ │ │ │ ├── horizontal-scroll.vue │ │ │ │ │ ├── horizontal-slide.vue │ │ │ │ │ ├── picker.vue │ │ │ │ │ ├── pulldown.vue │ │ │ │ │ ├── pullup.vue │ │ │ │ │ ├── vertical-scroll.vue │ │ │ │ │ └── vertical-slide.vue │ │ │ │ ├── movable/ │ │ │ │ │ ├── default.vue │ │ │ │ │ ├── multi-content-scale.vue │ │ │ │ │ ├── multi-content.vue │ │ │ │ │ └── scale.vue │ │ │ │ ├── nested-scroll/ │ │ │ │ │ ├── horizontal-in-vertical.vue │ │ │ │ │ ├── horizontal.vue │ │ │ │ │ ├── triple-vertical.vue │ │ │ │ │ └── vertical.vue │ │ │ │ ├── observe-dom/ │ │ │ │ │ └── default.vue │ │ │ │ ├── observe-image/ │ │ │ │ │ └── default.vue │ │ │ │ ├── picker/ │ │ │ │ │ ├── double-column.vue │ │ │ │ │ ├── linkage-column.vue │ │ │ │ │ └── one-column.vue │ │ │ │ ├── pulldown/ │ │ │ │ │ ├── default.vue │ │ │ │ │ └── sina-weibo.vue │ │ │ │ ├── pullup/ │ │ │ │ │ └── default.vue │ │ │ │ ├── scrollbar/ │ │ │ │ │ ├── custom.vue │ │ │ │ │ ├── horizontal.vue │ │ │ │ │ ├── mousewheel.vue │ │ │ │ │ └── vertical.vue │ │ │ │ ├── slide/ │ │ │ │ │ ├── banner.vue │ │ │ │ │ ├── dynamic.vue │ │ │ │ │ ├── fullpage.vue │ │ │ │ │ ├── specified-index.vue │ │ │ │ │ └── vertical.vue │ │ │ │ └── zoom/ │ │ │ │ └── default.vue │ │ │ ├── index.html │ │ │ ├── main.js │ │ │ ├── pages/ │ │ │ │ ├── compose-entry.vue │ │ │ │ ├── core-entry.vue │ │ │ │ ├── form-entry.vue │ │ │ │ ├── indicators-entry.vue │ │ │ │ ├── infinity-entry.vue │ │ │ │ ├── mouse-wheel-entry.vue │ │ │ │ ├── movable-entry.vue │ │ │ │ ├── nested-scroll-entry.vue │ │ │ │ ├── observe-dom-entry.vue │ │ │ │ ├── observe-image-entry.vue │ │ │ │ ├── picker-entry.vue │ │ │ │ ├── pulldown-entry.vue │ │ │ │ ├── pullup-entry.vue │ │ │ │ ├── scrollbar-entry.vue │ │ │ │ ├── slide-entry.vue │ │ │ │ └── zoom-entry.vue │ │ │ └── router/ │ │ │ └── index.js │ │ └── vue-release.sh │ ├── indicators/ │ │ ├── README.md │ │ ├── README_zh-CN.md │ │ ├── package.json │ │ └── src/ │ │ ├── __mocks__/ │ │ │ └── indicator.ts │ │ ├── __tests__/ │ │ │ ├── index.spec.ts │ │ │ └── indicator.spec.ts │ │ ├── index.ts │ │ ├── indicator.ts │ │ └── types.ts │ ├── infinity/ │ │ ├── README.md │ │ ├── README_zh-CN.md │ │ ├── package.json │ │ └── src/ │ │ ├── DataManager.ts │ │ ├── DomManager.ts │ │ ├── IndexCalculator.ts │ │ ├── Tombstone.ts │ │ ├── __tests__/ │ │ │ ├── DataManager.spec.ts │ │ │ ├── IndexCalculator.spec.ts │ │ │ └── __utils__/ │ │ │ ├── FakeList.ts │ │ │ └── constans.ts │ │ └── index.ts │ ├── mouse-wheel/ │ │ ├── README.md │ │ ├── README_zh-CN.md │ │ ├── package.json │ │ └── src/ │ │ ├── __tests__/ │ │ │ └── index.spec.ts │ │ └── index.ts │ ├── movable/ │ │ ├── README.md │ │ ├── README_zh-CN.md │ │ ├── package.json │ │ └── src/ │ │ ├── __tests__/ │ │ │ └── index.spec.ts │ │ ├── index.ts │ │ └── propertiesConfig.ts │ ├── nested-scroll/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── BScrollFamily.ts │ │ ├── __tests__/ │ │ │ └── index.spec.ts │ │ ├── index.ts │ │ └── propertiesConfig.ts │ ├── observe-dom/ │ │ ├── README.md │ │ ├── README_zh-CN.md │ │ ├── package.json │ │ └── src/ │ │ ├── __tests__/ │ │ │ └── index.spec.ts │ │ └── index.ts │ ├── observe-image/ │ │ ├── README.md │ │ ├── README_zh-CN.md │ │ ├── package.json │ │ └── src/ │ │ ├── __tests__/ │ │ │ └── index.spec.ts │ │ └── index.ts │ ├── pull-down/ │ │ ├── README.md │ │ ├── README_zh-CN.md │ │ ├── package.json │ │ └── src/ │ │ ├── __tests__/ │ │ │ └── index.spec.ts │ │ ├── index.ts │ │ └── propertiesConfig.ts │ ├── pull-up/ │ │ ├── README.md │ │ ├── README_zh-CN.md │ │ ├── package.json │ │ └── src/ │ │ ├── __tests__/ │ │ │ └── index.spec.ts │ │ ├── index.ts │ │ └── propertiesConfig.ts │ ├── react-examples/ │ │ ├── README.md │ │ ├── config-overrides.js │ │ ├── package.json │ │ ├── public/ │ │ │ ├── index.html │ │ │ └── static/ │ │ │ └── css/ │ │ │ ├── github-light.css │ │ │ ├── normalize.css │ │ │ ├── reset.css │ │ │ └── stylesheet.css │ │ └── src/ │ │ ├── App.js │ │ ├── index.js │ │ ├── index.styl │ │ ├── pages/ │ │ │ ├── compose/ │ │ │ │ ├── components/ │ │ │ │ │ ├── pullup-pulldown-outnested.js │ │ │ │ │ ├── pullup-pulldown-slide.js │ │ │ │ │ ├── pullup-pulldown.js │ │ │ │ │ └── slide-nested.js │ │ │ │ ├── index.js │ │ │ │ └── index.styl │ │ │ ├── core/ │ │ │ │ ├── components/ │ │ │ │ │ ├── default.js │ │ │ │ │ ├── dynamic-content.js │ │ │ │ │ ├── freescroll.js │ │ │ │ │ ├── horizontal-rotated.js │ │ │ │ │ ├── horizontal.js │ │ │ │ │ ├── specified-content.js │ │ │ │ │ └── vertical-rotated.js │ │ │ │ ├── index.js │ │ │ │ └── index.styl │ │ │ ├── form/ │ │ │ │ ├── components/ │ │ │ │ │ └── textarea.js │ │ │ │ ├── index.js │ │ │ │ └── index.styl │ │ │ ├── indicators/ │ │ │ │ ├── components/ │ │ │ │ │ ├── minimap.js │ │ │ │ │ └── parallax-scroll.js │ │ │ │ ├── index.js │ │ │ │ └── index.styl │ │ │ ├── infinity/ │ │ │ │ ├── data/ │ │ │ │ │ └── message.json │ │ │ │ ├── index.js │ │ │ │ └── index.styl │ │ │ ├── mouse-wheel/ │ │ │ │ ├── components/ │ │ │ │ │ ├── horizontal-scroll.js │ │ │ │ │ ├── horizontal-slide.js │ │ │ │ │ ├── picker.js │ │ │ │ │ ├── pulldown.js │ │ │ │ │ ├── pullup.js │ │ │ │ │ ├── vertical-scroll.js │ │ │ │ │ └── vertical-slide.js │ │ │ │ ├── index.js │ │ │ │ └── index.styl │ │ │ ├── movable/ │ │ │ │ ├── components/ │ │ │ │ │ ├── default.js │ │ │ │ │ ├── multi-content-scale.js │ │ │ │ │ ├── multi-content.js │ │ │ │ │ └── scale.js │ │ │ │ ├── index.js │ │ │ │ └── index.styl │ │ │ ├── nested-scroll/ │ │ │ │ ├── components/ │ │ │ │ │ ├── horizontal-in-vertical.js │ │ │ │ │ ├── horizontal.js │ │ │ │ │ ├── triple-vertical.js │ │ │ │ │ └── vertical.js │ │ │ │ ├── index.js │ │ │ │ └── index.styl │ │ │ ├── observe-dom/ │ │ │ │ ├── components/ │ │ │ │ │ └── default.js │ │ │ │ ├── index.js │ │ │ │ └── index.styl │ │ │ ├── observe-image/ │ │ │ │ ├── components/ │ │ │ │ │ └── default.js │ │ │ │ ├── index.js │ │ │ │ └── index.styl │ │ │ ├── picker/ │ │ │ │ ├── components/ │ │ │ │ │ ├── double-column.js │ │ │ │ │ ├── linkage-column.js │ │ │ │ │ └── one-column.js │ │ │ │ ├── index.js │ │ │ │ └── index.styl │ │ │ ├── pulldown/ │ │ │ │ ├── components/ │ │ │ │ │ ├── default.js │ │ │ │ │ └── sina-weibo.js │ │ │ │ ├── index.js │ │ │ │ └── index.styl │ │ │ ├── pullup/ │ │ │ │ ├── components/ │ │ │ │ │ └── default.js │ │ │ │ ├── index.js │ │ │ │ └── index.styl │ │ │ ├── scrollbar/ │ │ │ │ ├── components/ │ │ │ │ │ ├── custom.js │ │ │ │ │ ├── horizontal.js │ │ │ │ │ ├── mousewheel.js │ │ │ │ │ └── vertical.js │ │ │ │ ├── index.js │ │ │ │ └── index.styl │ │ │ ├── slide/ │ │ │ │ ├── components/ │ │ │ │ │ ├── banner.js │ │ │ │ │ ├── dynamic.js │ │ │ │ │ ├── fullpage.js │ │ │ │ │ ├── specified-index.js │ │ │ │ │ └── vertical.js │ │ │ │ ├── index.js │ │ │ │ └── index.styl │ │ │ └── zoom/ │ │ │ ├── components/ │ │ │ │ └── default.js │ │ │ ├── index.js │ │ │ └── index.styl │ │ ├── reportWebVitals.js │ │ ├── router.js │ │ └── setupTests.js │ ├── scroll-bar/ │ │ ├── README.md │ │ ├── README_zh-CN.md │ │ ├── package.json │ │ └── src/ │ │ ├── __mocks__/ │ │ │ ├── event-handler.ts │ │ │ └── indicator.ts │ │ ├── __tests__/ │ │ │ ├── __snapshots__/ │ │ │ │ └── index.spec.ts.snap │ │ │ ├── event-handler.spec.ts │ │ │ ├── index.spec.ts │ │ │ └── indicator.spec.ts │ │ ├── event-handler.ts │ │ ├── index.ts │ │ └── indicator.ts │ ├── shared-utils/ │ │ ├── README.md │ │ ├── package.json │ │ └── src/ │ │ ├── Touch.ts │ │ ├── __mocks__/ │ │ │ ├── dom.ts │ │ │ └── ease.ts │ │ ├── __tests__/ │ │ │ ├── debug.spec.ts │ │ │ ├── dom.spec.ts │ │ │ ├── ease.spec.ts │ │ │ ├── events.spec.ts │ │ │ ├── lang.spec.ts │ │ │ ├── propertiesProxy.spec.ts │ │ │ └── raf.spec.ts │ │ ├── debug.ts │ │ ├── dom.ts │ │ ├── ease.ts │ │ ├── enums.ts │ │ ├── env.ts │ │ ├── events.ts │ │ ├── index.ts │ │ ├── lang.ts │ │ ├── propertiesProxy.ts │ │ ├── raf.ts │ │ └── types.ts │ ├── slide/ │ │ ├── README.md │ │ ├── README_zh-CN.md │ │ ├── package.json │ │ └── src/ │ │ ├── PagesMatrix.ts │ │ ├── SlidePages.ts │ │ ├── __mocks__/ │ │ │ ├── PagesMatrix.ts │ │ │ └── SlidePages.ts │ │ ├── __tests__/ │ │ │ ├── PagesMatrix.spec.ts │ │ │ ├── SlidePages.spec.ts │ │ │ └── index.spec.ts │ │ ├── constants.ts │ │ ├── index.ts │ │ └── propertiesConfig.ts │ ├── vuepress-docs/ │ │ ├── docs/ │ │ │ ├── .vuepress/ │ │ │ │ ├── components/ │ │ │ │ │ ├── demo.vue │ │ │ │ │ └── qrcode.vue │ │ │ │ ├── config.js │ │ │ │ ├── enhanceApp.js │ │ │ │ ├── nav/ │ │ │ │ │ ├── en-US.js │ │ │ │ │ └── zh-CN.js │ │ │ │ ├── plugins/ │ │ │ │ │ └── extract-code.js │ │ │ │ ├── public/ │ │ │ │ │ └── assets/ │ │ │ │ │ └── stylus/ │ │ │ │ │ └── index.styl │ │ │ │ └── sidebar/ │ │ │ │ ├── FAQ.js │ │ │ │ ├── guide.js │ │ │ │ └── plugins.js │ │ │ ├── en-US/ │ │ │ │ ├── FAQ/ │ │ │ │ │ ├── README.md │ │ │ │ │ └── diagnosis.md │ │ │ │ ├── README.md │ │ │ │ ├── guide/ │ │ │ │ │ ├── README.md │ │ │ │ │ ├── base-scroll-api.md │ │ │ │ │ ├── base-scroll-options.md │ │ │ │ │ ├── base-scroll.md │ │ │ │ │ ├── how-to-install.md │ │ │ │ │ └── use.md │ │ │ │ └── plugins/ │ │ │ │ ├── README.md │ │ │ │ ├── how-to-write.md │ │ │ │ ├── indicators.md │ │ │ │ ├── infinity.md │ │ │ │ ├── mouse-wheel.md │ │ │ │ ├── movable.md │ │ │ │ ├── nested-scroll.md │ │ │ │ ├── observe-dom.md │ │ │ │ ├── observe-image.md │ │ │ │ ├── pulldown.md │ │ │ │ ├── pullup.md │ │ │ │ ├── scroll-bar.md │ │ │ │ ├── slide.md │ │ │ │ ├── wheel.md │ │ │ │ └── zoom.md │ │ │ └── zh-CN/ │ │ │ ├── FAQ/ │ │ │ │ ├── README.md │ │ │ │ └── diagnosis.md │ │ │ ├── README.md │ │ │ ├── guide/ │ │ │ │ ├── README.md │ │ │ │ ├── base-scroll-api.md │ │ │ │ ├── base-scroll-options.md │ │ │ │ ├── base-scroll.md │ │ │ │ ├── how-to-install.md │ │ │ │ └── use.md │ │ │ └── plugins/ │ │ │ ├── README.md │ │ │ ├── compose-plugins.md │ │ │ ├── how-to-write.md │ │ │ ├── indicators.md │ │ │ ├── infinity.md │ │ │ ├── mouse-wheel.md │ │ │ ├── movable.md │ │ │ ├── nested-scroll.md │ │ │ ├── observe-dom.md │ │ │ ├── observe-image.md │ │ │ ├── pulldown.md │ │ │ ├── pullup.md │ │ │ ├── scroll-bar.md │ │ │ ├── slide.md │ │ │ ├── wheel.md │ │ │ └── zoom.md │ │ ├── docs-release.sh │ │ └── package.json │ ├── wheel/ │ │ ├── README.md │ │ ├── README_zh-CN.md │ │ ├── package.json │ │ └── src/ │ │ ├── __tests__/ │ │ │ └── index.spec.ts │ │ ├── index.ts │ │ └── propertiesConfig.ts │ └── zoom/ │ ├── README.md │ ├── README_zh-CN.md │ ├── package.json │ └── src/ │ ├── __tests__/ │ │ ├── __utils__/ │ │ │ └── util.ts │ │ └── index.spec.ts │ ├── index.ts │ └── propertiesConfig.ts ├── postcss.config.js ├── scripts/ │ ├── build.js │ ├── checkYarn.js │ └── release.js ├── test-dts/ │ ├── core.test-d.ts │ ├── index.d.ts │ ├── plugin.test-d.ts │ ├── tsconfig.json │ └── util.d.ts ├── tests/ │ ├── dts/ │ │ └── index.d.ts │ ├── e2e/ │ │ ├── compose-plugins/ │ │ │ ├── compose-plugins.e2e.ts │ │ │ ├── pullup-pulldown-nested.e2e.ts │ │ │ ├── pullup-pulldown-slide.e2e.ts │ │ │ ├── pullup-pulldown.e2e.ts │ │ │ └── slide-nested.e2e.ts │ │ ├── core/ │ │ │ └── corescroll.e2e.ts │ │ ├── form/ │ │ │ └── textarea.e2e.ts │ │ ├── homepage.e2e.ts │ │ ├── indicators/ │ │ │ ├── minimap.e2e.ts │ │ │ └── parallax-scrolling.e2e.ts │ │ ├── infinity/ │ │ │ └── infinity.e2e.ts │ │ ├── mousewheel/ │ │ │ └── mousewheel.e2e.ts │ │ ├── movable/ │ │ │ ├── default.e2e.ts │ │ │ ├── multi-content-scale.e2e.ts │ │ │ ├── multi-content.e2e.ts │ │ │ └── scaled.e2e.ts │ │ ├── nested-scroll/ │ │ │ ├── horizontal-in-vertical.e2e.ts │ │ │ ├── horizontal.e2e.ts │ │ │ ├── triple-vertical.e2e.ts │ │ │ └── vertical.e2e.ts │ │ ├── observe-dom/ │ │ │ └── observe-dom.e2e.ts │ │ ├── observe-image/ │ │ │ └── observe-image.e2e.ts │ │ ├── picker/ │ │ │ ├── double-column.e2e.ts │ │ │ ├── linkage-column.e2e.ts │ │ │ └── one-column.e2e.ts │ │ ├── pulldown/ │ │ │ ├── default.e2e.ts │ │ │ └── sina.e2e.ts │ │ ├── pullup/ │ │ │ └── default.e2e.ts │ │ ├── scrollbar/ │ │ │ ├── custom.e2e.ts │ │ │ ├── horizontal.e2e.ts │ │ │ ├── mousewheel.e2e.ts │ │ │ └── vertical.e2e.ts │ │ ├── slide/ │ │ │ ├── banner.e2e.ts │ │ │ ├── dynamic.e2e.ts │ │ │ ├── fullpage.e2e.ts │ │ │ ├── specifiedIndex.e2e.ts │ │ │ └── vertical.e2e.ts │ │ └── zoom/ │ │ └── zoom.e2e.ts │ └── util/ │ ├── extendMouseWheel.ts │ ├── extendTouch.ts │ ├── getScale.ts │ └── getTranslate.ts ├── tsconfig.json └── tslint.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true ================================================ FILE: .eslintrc.js ================================================ module.exports = { root: true, parser: 'babel-eslint', parserOptions: { sourceType: 'module' }, // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style extends: 'standard', env: { browser: true, }, // required to lint *.vue files plugins: [ 'html' ], // add your custom rules here 'rules': { // allow paren-less arrow functions 'arrow-parens': 0, // allow debugger during development 'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0, 'indent': 0, 'no-tabs': 0, 'space-before-function-paren': 0, 'eol-last': 0, 'no-unused-expressions': 0 } }; ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ ================================================ FILE: .gitignore ================================================ .bin/ node_modules/ .idea/ .DS_Store .npm-debug.log coverage/ .rpt2_cache .vscode/ dist/ yarn-debug.log* yarn-error.log* yarn.lock ================================================ FILE: .gitpod.Dockerfile ================================================ FROM gitpod/workspace-full RUN sudo apt-get update && \ sudo apt-get install -y \ ca-certificates \ fonts-liberation \ libappindicator3-1 \ libasound2 \ libatk-bridge2.0-0 \ libatk1.0-0 \ libc6 \ libcairo2 \ libcups2 \ libdbus-1-3 \ libexpat1 \ libfontconfig1 \ libgbm1 \ libgcc1 \ libglib2.0-0 \ libgtk-3-0 \ libnspr4 \ libnss3 \ libpango-1.0-0 \ libpangocairo-1.0-0 \ libstdc++6 \ libx11-6 \ libx11-xcb1 \ libxcb1 \ libxcomposite1 \ libxcursor1 \ libxdamage1 \ libxext6 \ libxfixes3 \ libxi6 \ libxrandr2 \ libxrender1 \ libxss1 \ libxtst6 \ lsb-release \ wget \ xdg-utils && \ sudo rm -rf /var/lib/apt/lists/* RUN sudo apt-get update && \ sudo apt-get install -yq chromium-browser && \ sudo rm -rf /var/lib/apt/lists/* ENV GITPOD=true ================================================ FILE: .gitpod.yml ================================================ image: file: .gitpod.Dockerfile tasks: - command: gp await-port 8080 && sleep 3 && gp preview $(gp url 8080)/docs - name: Dev init: yarn install && gp sync-done install command: yarn vue:dev - name: Docs init: gp sync-await install && yarn docs:build command: yarn docs:dev openMode: split-right ports: - port: 8080 onOpen: ignore - port: 8932 onOpen: open-preview ================================================ FILE: .huskyrc.json ================================================ { "hooks": { "pre-commit": "lint-staged", "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" } } ================================================ FILE: .npmignore ================================================ build/ config/ docs/ doc/ example/ static/ .idea/ .github/ postcss.config.js .bin/ .DS_Store .npm-debug.log .babelrc .npmignore .editorconfig .eslintrc.js .tsconfig.json package-lock.json ================================================ FILE: .travis.yml ================================================ language: node_js sudo: false cache: directories: - node_modules node_js: - "lts/*" branches: only: - master - dev script: - npm test - ./node_modules/.bin/codecov ================================================ FILE: .yarnrc ================================================ registry "https://registry.npmjs.org" ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2015 HuangYi Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # better-scroll [![npm version](https://img.shields.io/npm/v/better-scroll.svg)](https://www.npmjs.com/package/better-scroll) [![downloads](https://img.shields.io/npm/dm/better-scroll.svg)](https://www.npmjs.com/package/better-scroll) [![Build Status](https://travis-ci.org/ustbhuangyi/better-scroll.svg?branch=master)](https://travis-ci.org/ustbhuangyi/better-scroll) [![Package Quality](http://npm.packagequality.com/shield/better-scroll.svg)](http://packagequality.com/#?package=better-scroll) [![codecov.io](http://codecov.io/github/ustbhuangyi/better-scroll/coverage.svg?branch=master)](http://codecov.io/github/ustbhuangyi/better-scroll) [![Gitpod Ready-to-Code](https://img.shields.io/badge/Gitpod-Ready--to--Code-blue?logo=gitpod)](https://gitpod.io/from-referrer/) [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/README_zh-CN.md) [1.x Docs](https://better-scroll.github.io/docs-v1/) [2.x Docs](https://better-scroll.github.io/docs/en-US/) [2.x Demo](https://better-scroll.github.io/examples/) > **Note**: `1.x` is not maintained. please migrate your version as soon as possible # Install ```bash npm install better-scroll -S # install 2.x,with full-featured plugin. npm install @better-scroll/core # only CoreScroll ``` ```js import BetterScroll from 'better-scroll' let bs = new BetterScroll('.wrapper', { movable: true, zoom: true }) import BScroll from '@better-scroll/core' let bs = new BScroll('.wrapper', {}) ``` # CDN BetterScroll with full-featured plugin. ```html ``` ```js let wrapper = document.getElementById("wrapper") let bs = BetterScroll.createBScroll(wrapper, {}) ``` Only CoreScroll ```html ``` ```js let wrapper = document.getElementById("wrapper") let bs = new BScroll(wrapper, {}) ``` ## What is BetterScroll ? BetterScroll is a plugin which is aimed at solving scrolling circumstances on the mobile side (PC supported already). The core is inspired by the implementation of [iscroll](https://github.com/cubiq/iscroll), so the APIs of BetterScroll are compatible with iscroll on the whole. What's more, BetterScroll also extends some features and optimizes for performance based on iscroll. BetterScroll is implemented with plain JavaScript, which means it's dependency free. ## Getting started The most common application scenario of BetterScroll is list scrolling. Let's see its HTML: ```html
``` 上面的代码中 BetterScroll 是作用在外层 wrapper 容器上的,滚动的部分是 content 元素。这里要注意的是,BetterScroll 默认处理容器(wrapper)的第一个子元素(content)的滚动,其它的元素都会被忽略。不过对于 BetterScroll v2.0.4 版本,可以通过 specifiedIndexAsContent 配置项来指定 content,详细的请参考文档。 最简单的初始化代码如下: ``` js import BScroll from '@better-scroll/core' let wrapper = document.querySelector('.wrapper') let scroll = new BScroll(wrapper) ``` BetterScroll 提供了一个类,实例化的第一个参数是一个原生的 DOM 对象。当然,如果传递的是一个字符串,BetterScroll 内部会尝试调用 querySelector 去获取这个 DOM 对象。 ## 滚动原理 很多人已经用过 BetterScroll,我收到反馈最多的问题是: > BetterScroll 初始化了, 但是没法滚动。 不能滚动是现象,我们得搞清楚这其中的根本原因。在这之前,我们先来看一下浏览器的滚动原理: 浏览器的滚动条大家都会遇到,当页面内容的高度超过视口高度的时候,会出现纵向滚动条;当页面内容的宽度超过视口宽度的时候,会出现横向滚动条。也就是当我们的视口展示不下内容的时候,会通过滚动条的方式让用户滚动屏幕看到剩余的内容。 BetterScroll 也是一样的原理,我们可以用一张图更直观的感受一下: ![布局](https://raw.githubusercontent.com/ustbhuangyi/better-scroll/master/packages/vuepress-docs/docs/.vuepress/public/assets/images/schematic.png) 绿色部分为 wrapper,也就是父容器,它会有**固定的高度**。黄色部分为 content,它是父容器的**第一个子元素**,它的高度会随着内容的大小而撑高。那么,当 content 的高度不超过父容器的高度,是不能滚动的,而它一旦超过了父容器的高度,我们就可以滚动内容区了,这就是 BetterScroll 的滚动原理。 ## 插件 通过插件,增强 BetterScroll core scroll 的能力,比如 ```js import BScroll from '@better-scroll/core' import PullUp from '@better-scroll/pull-up' let bs = new BScroll('.wrapper', { pullUpLoad: true }) ``` 详细请看[插件文档](https://better-scroll.github.io/docs/zh-CN/plugins/) ## BetterScroll 在 MVVM 框架的应用 我之前写过一篇[当 BetterScroll 遇见 Vue](https://zhuanlan.zhihu.com/p/27407024),也希望大家投稿,分享一下 BetterScroll 在其它框架下的使用心得。 一款超赞的基于 Vue 实现的组件库 [cube-ui](https://github.com/didi/cube-ui/)。 ## BetterScroll 在实战项目中的运用 如果你想学习 BetterScroll 在实战项目中的运用,也可以去学习我的 2 门实战课程。 [Vue.js 高仿外卖饿了么实战课程](https://coding.imooc.com/class/74.html) [项目演示地址](http://ustbhuangyi.com/sell/) ![二维码](https://qr.api.cli.im/qr?data=http%253A%252F%252Fustbhuangyi.com%252Fsell%252F%2523%252Fgoods&level=H&transparent=false&bgcolor=%23ffffff&forecolor=%23000000&blockpixel=12&marginblock=1&logourl=&size=280&kid=cliim&key=686203a49c4613080b5b3004323ff977) [Vue.js 音乐 App 高级实战课程](http://coding.imooc.com/class/107.html) [项目演示地址](http://ustbhuangyi.com/music/) ![二维码](https://qr.api.cli.im/qr?data=http%253A%252F%252Fustbhuangyi.com%252Fmusic%252F&level=H&transparent=false&bgcolor=%23ffffff&forecolor=%23000000&blockpixel=12&marginblock=1&logourl=&size=280&kid=cliim&key=731bbcc2b490454d2cc604f98539952c) ================================================ FILE: jest-e2e.config.js ================================================ module.exports = { "verbose": true, "roots": [""], "cache": false, "globals": { "ts-jest": { "diagnostics": false } }, "preset": "jest-puppeteer", "testMatch": ["**/tests/e2e/**/*.e2e.ts"], "transform": { ".ts": "ts-jest" }, "moduleFileExtensions": [ "ts", "js" ] } ================================================ FILE: jest-puppeteer.config.js ================================================ module.exports = { launch: { headless: process.env.GITPOD !== undefined, defaultViewport: { width: 375, height: 667, deviceScaleFactor: 2, isMobile: true, hasTouch: true }, args: ['--disable-infobars', '--no-sandbox', '--disable-setuid-sandbox'], } } ================================================ FILE: jest.config.js ================================================ module.exports = { "verbose": true, globals: {}, "roots": [""], "cache": false, "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|js)$", "testPathIgnorePatterns": [ '/__tests__/__utils__' ], "transform": { ".ts": "ts-jest" }, "testEnvironment": "jsdom", "moduleFileExtensions": [ "ts", "js" ], "moduleNameMapper": { '^@better-scroll/(.*)/(.*)$': '/packages/$1/$2', '^@better-scroll/(.*)$': '/packages/$1/src/index', '^@/(.*)$': '/$1' }, "coverageDirectory": "/tests/coverage", "coveragePathIgnorePatterns": [ "/test/", "/__tests__/" ], "coverageReporters": ['json', 'text', 'lcov', 'clover'] } ================================================ FILE: lerna.json ================================================ { "npmClient": "yarn", "useWorkspaces": true, "packages": [ "packages/*" ], "version": "2.5.1" } ================================================ FILE: package.json ================================================ { "private": true, "workspaces": [ "packages/*" ], "scripts": { "bootstrap": "lerna bootstrap", "packages:build": "node scripts/build.js", "packages:release": "node scripts/release.js", "docs:dev": "lerna run --stream --scope vuepress-docs docs:dev", "docs:build": "lerna run --stream --scope vuepress-docs docs:build", "docs:release": "lerna run --stream --scope vuepress-docs docs:release", "vue:dev": "lerna run --stream --scope examples vue:dev", "vue:build": "lerna run --stream --scope examples vue:build", "vue:release": "lerna run --stream --scope examples vue:release", "react:dev": "lerna run --stream --scope react-examples dev", "react:build": "lerna run --stream --scope react-examples build", "lint": "tslint --project tsconfig.json -t codeFrame 'src/**/*.ts' 'test/**/*.ts'", "test": "jest --coverage && yarn test:tsd", "test:e2e": "jest --config=jest-e2e.config.js --runInBand", "test:tsd": "tsc -p ./test-dts/tsconfig.json", "vue:test:e2e": "lerna run --stream --scope examples vue:test:e2e", "cm": "git-cz", "preinstall": "node ./scripts/checkYarn.js", "postinstall": "yarn bootstrap" }, "lint-staged": { "*.ts": [ "prettier --write" ] }, "prettier": { "semi": false, "singleQuote": true }, "commitlint": { "extends": [ "@commitlint/config-conventional" ] }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios" ], "devDependencies": { "@commitlint/cli": "^9.1.2", "@commitlint/config-conventional": "^9.1.2", "@types/jest": "^26.0.10", "@types/node": "^14.6.0", "@types/puppeteer": "^3.0.1", "@vuepress/plugin-back-to-top": "^1.8.0", "@vuepress/plugin-medium-zoom": "^1.5.4", "codecov": "^3.5.0", "commitizen": "^4.1.5", "coveralls": "^3.0.2", "cross-env": "^7.0.2", "cz-conventional-changelog": "^3.2.0", "execa": "^4.0.3", "husky": "^4.2.5", "inquirer": "^7.3.3", "jest": "^26.0.1", "jest-config": "^26.4.2", "jest-puppeteer": "4.4.0", "lerna": "^3.14.1", "lint-staged": "^10.2.11", "ora": "^5.0.0", "prettier": "^2.0.5", "puppeteer": "^5.2.1", "rimraf": "^3.0.2", "rollup": "^2.23.0", "rollup-plugin-commonjs": "^10.1.0", "rollup-plugin-json": "^4.0.0", "rollup-plugin-node-resolve": "^5.2.0", "rollup-plugin-sourcemaps": "^0.6.2", "rollup-plugin-typescript2": "^0.27.1", "rollup-plugin-uglify": "^6.0.1", "semver": "^7.3.2", "ts-jest": "^26.2.0", "ts-loader": "^8.0.2", "ts-node": "^9.0.0", "tslint": "^6.1.3", "tslint-config-prettier": "^1.15.0", "tslint-config-standard": "^9.0.0", "typescript": "4.0.2", "vconsole": "^3.3.4", "zlib": "^1.0.5" }, "config": { "commitizen": { "path": "cz-conventional-changelog" } }, "browserslist": [ "> 1%", "last 2 versions", "not ie <= 8", "Android >= 4.0", "iOS >= 8" ], "name": "better-scroll" } ================================================ FILE: packages/better-scroll/README.md ================================================ # better-scroll [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/better-scroll/README_zh-CN.md) BetterScroll with full plugin capabilities, don't care about the details of various plugin registrations. ## Usage ```js import BScroll from 'better-scroll' const bs = new BScroll('.wrapper', { pullUpLoad: true, scrollbar: true, pullDownRefresh: true // and so on }) ``` ================================================ FILE: packages/better-scroll/README_zh-CN.md ================================================ # better-scroll 具备完整插件能力的 BetterScroll,不用关心各种插件注册的细节。 ## 使用 ```js import BScroll from 'better-scroll' const bs = new BScroll('.wrapper', { pullUpLoad: true, scrollbar: true, pullDownRefresh: true // and so on }) ``` ================================================ FILE: packages/better-scroll/package.json ================================================ { "name": "better-scroll", "version": "2.5.1", "description": "Full-featured BetterScroll", "author": { "name": "jizhi", "email": "theniceangel@163.com" }, "publishConfig": { "access": "public" }, "main": "dist/better-scroll.js", "module": "dist/better-scroll.esm.js", "typings": "dist/types/index.d.ts", "scripts": {}, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios" ], "license": "MIT", "repository": { "type": "git", "url": "git@github.com:ustbhuangyi/better-scroll.git", "directory": "packages/better-scroll" }, "dependencies": { "@better-scroll/core": "^2.5.1", "@better-scroll/indicators": "^2.5.1", "@better-scroll/infinity": "^2.5.1", "@better-scroll/mouse-wheel": "^2.5.1", "@better-scroll/movable": "^2.5.1", "@better-scroll/nested-scroll": "^2.5.1", "@better-scroll/observe-dom": "^2.5.1", "@better-scroll/observe-image": "^2.5.1", "@better-scroll/pull-down": "^2.5.1", "@better-scroll/pull-up": "^2.5.1", "@better-scroll/scroll-bar": "^2.5.1", "@better-scroll/slide": "^2.5.1", "@better-scroll/wheel": "^2.5.1", "@better-scroll/zoom": "^2.5.1" }, "gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130" } ================================================ FILE: packages/better-scroll/src/index.ts ================================================ import BScroll from '@better-scroll/core' import MouseWheel from '@better-scroll/mouse-wheel' import ObserveDom from '@better-scroll/observe-dom' import PullDownRefresh from '@better-scroll/pull-down' import PullUpLoad from '@better-scroll/pull-up' import ScrollBar from '@better-scroll/scroll-bar' import Slide from '@better-scroll/slide' import Wheel from '@better-scroll/wheel' import Zoom from '@better-scroll/zoom' import NestedScroll from '@better-scroll/nested-scroll' import InfinityScroll from '@better-scroll/infinity' import Movable from '@better-scroll/movable' import ObserveImage from '@better-scroll/observe-image' import Indicators from '@better-scroll/indicators' export { createBScroll, BScrollInstance, Options, CustomOptions, TranslaterPoint, MountedBScrollHTMLElement, Behavior, Boundary, CustomAPI } from '@better-scroll/core' export { MouseWheel, ObserveDom, PullDownRefresh, PullUpLoad, ScrollBar, Slide, Wheel, Zoom, NestedScroll, InfinityScroll, Movable, ObserveImage, Indicators } BScroll.use(MouseWheel) .use(ObserveDom) .use(PullDownRefresh) .use(PullUpLoad) .use(ScrollBar) .use(Slide) .use(Wheel) .use(Zoom) .use(NestedScroll) .use(InfinityScroll) .use(Movable) .use(ObserveImage) .use(Indicators) export default BScroll ================================================ FILE: packages/core/README.md ================================================ # @better-scroll/core [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/core/README_zh-CN.md) core scroll from BetterScroll. ## Usage ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {/* ... */}) ``` ================================================ FILE: packages/core/README_zh-CN.md ================================================ # @better-scroll/core 核心滚动,实现基础的列表滚动效果。 ## 使用 ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {/* ... */}) ``` ================================================ FILE: packages/core/package.json ================================================ { "name": "@better-scroll/core", "version": "2.5.1", "description": "Minimalistic core scrolling for BetterScroll, it is pure and tiny", "author": { "name": "jizhi", "email": "theniceangel@163.com" }, "publishConfig": { "access": "public" }, "main": "dist/core.js", "module": "dist/core.esm.js", "typings": "dist/types/index.d.ts", "scripts": {}, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios" ], "license": "MIT", "repository": { "type": "git", "url": "git@github.com:ustbhuangyi/better-scroll.git", "directory": "packages/core" }, "dependencies": { "@better-scroll/shared-utils": "^2.5.1" }, "gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130" } ================================================ FILE: packages/core/src/BScroll.ts ================================================ import { BScrollInstance, propertiesConfig } from './Instance' import { Options, DefOptions, OptionsConstructor } from './Options' import Scroller from './scroller/Scroller' import { getElement, warn, isUndef, propertiesProxy, ApplyOrder, EventEmitter } from '@better-scroll/shared-utils' import { bubbling } from './utils/bubbling' import { UnionToIntersection } from './utils/typesHelper' interface PluginCtor { pluginName: string applyOrder?: ApplyOrder new (scroll: BScroll): any } interface PluginItem { name: string applyOrder?: ApplyOrder.Pre | ApplyOrder.Post ctor: PluginCtor } interface PluginsMap { [key: string]: boolean } interface PropertyConfig { key: string sourceKey: string } type ElementParam = HTMLElement | string export interface MountedBScrollHTMLElement extends HTMLElement { isBScrollContainer?: boolean } export class BScrollConstructor extends EventEmitter { static plugins: PluginItem[] = [] static pluginsMap: PluginsMap = {} scroller: Scroller options: OptionsConstructor hooks: EventEmitter plugins: { [name: string]: any } wrapper: HTMLElement content: HTMLElement; [key: string]: any static use(ctor: PluginCtor) { const name = ctor.pluginName const installed = BScrollConstructor.plugins.some( plugin => ctor === plugin.ctor ) if (installed) return BScrollConstructor if (isUndef(name)) { warn( `Plugin Class must specify plugin's name in static property by 'pluginName' field.` ) return BScrollConstructor } BScrollConstructor.pluginsMap[name] = true BScrollConstructor.plugins.push({ name, applyOrder: ctor.applyOrder, ctor }) return BScrollConstructor } constructor(el: ElementParam, options?: Options & O) { super([ 'refresh', 'contentChanged', 'enable', 'disable', 'beforeScrollStart', 'scrollStart', 'scroll', 'scrollEnd', 'scrollCancel', 'touchEnd', 'flick', 'destroy' ]) const wrapper = getElement(el) if (!wrapper) { warn('Can not resolve the wrapper DOM.') return } this.plugins = {} this.options = new OptionsConstructor().merge(options).process() if (!this.setContent(wrapper).valid) { return } this.hooks = new EventEmitter([ 'refresh', 'enable', 'disable', 'destroy', 'beforeInitialScrollTo', 'contentChanged' ]) this.init(wrapper) } setContent(wrapper: MountedBScrollHTMLElement) { let contentChanged = false let valid = true const content = wrapper.children[ this.options.specifiedIndexAsContent ] as HTMLElement if (!content) { warn( 'The wrapper need at least one child element to be content element to scroll.' ) valid = false } else { contentChanged = this.content !== content if (contentChanged) { this.content = content } } return { valid, contentChanged } } private init(wrapper: MountedBScrollHTMLElement) { this.wrapper = wrapper // mark wrapper to recognize bs instance by DOM attribute wrapper.isBScrollContainer = true this.scroller = new Scroller(wrapper, this.content, this.options) this.scroller.hooks.on(this.scroller.hooks.eventTypes.resize, () => { this.refresh() }) this.eventBubbling() this.handleAutoBlur() this.enable() this.proxy(propertiesConfig) this.applyPlugins() // maybe boundary has changed, should refresh this.refreshWithoutReset(this.content) const { startX, startY } = this.options const position = { x: startX, y: startY } // maybe plugins want to control scroll position if ( this.hooks.trigger(this.hooks.eventTypes.beforeInitialScrollTo, position) ) { return } this.scroller.scrollTo(position.x, position.y) } private applyPlugins() { const options = this.options BScrollConstructor.plugins .sort((a, b) => { const applyOrderMap = { [ApplyOrder.Pre]: -1, [ApplyOrder.Post]: 1 } const aOrder = a.applyOrder ? applyOrderMap[a.applyOrder] : 0 const bOrder = b.applyOrder ? applyOrderMap[b.applyOrder] : 0 return aOrder - bOrder }) .forEach((item: PluginItem) => { const ctor = item.ctor if (options[item.name] && typeof ctor === 'function') { this.plugins[item.name] = new ctor(this) } }) } private handleAutoBlur() { /* istanbul ignore if */ if (this.options.autoBlur) { this.on(this.eventTypes.beforeScrollStart, () => { let activeElement = document.activeElement as HTMLElement if ( activeElement && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA') ) { activeElement.blur() } }) } } private eventBubbling() { bubbling(this.scroller.hooks, this, [ this.eventTypes.beforeScrollStart, this.eventTypes.scrollStart, this.eventTypes.scroll, this.eventTypes.scrollEnd, this.eventTypes.scrollCancel, this.eventTypes.touchEnd, this.eventTypes.flick ]) } private refreshWithoutReset(content: HTMLElement) { this.scroller.refresh(content) this.hooks.trigger(this.hooks.eventTypes.refresh, content) this.trigger(this.eventTypes.refresh, content) } proxy(propertiesConfig: PropertyConfig[]) { propertiesConfig.forEach(({ key, sourceKey }) => { propertiesProxy(this, sourceKey, key) }) } refresh() { const { contentChanged, valid } = this.setContent(this.wrapper) if (valid) { const content = this.content this.refreshWithoutReset(content) if (contentChanged) { this.hooks.trigger(this.hooks.eventTypes.contentChanged, content) this.trigger(this.eventTypes.contentChanged, content) } this.scroller.resetPosition() } } enable() { this.scroller.enable() this.hooks.trigger(this.hooks.eventTypes.enable) this.trigger(this.eventTypes.enable) } disable() { this.scroller.disable() this.hooks.trigger(this.hooks.eventTypes.disable) this.trigger(this.eventTypes.disable) } destroy() { this.hooks.trigger(this.hooks.eventTypes.destroy) this.trigger(this.eventTypes.destroy) this.scroller.destroy() } eventRegister(names: string[]) { this.registerType(names) } } export interface BScrollConstructor extends BScrollInstance {} export interface CustomAPI { [key: string]: {} } type ExtractAPI = { [K in keyof O]: K extends string ? DefOptions[K] extends undefined ? CustomAPI[K] : never : never }[keyof O] export function createBScroll( el: ElementParam, options?: Options & O ): BScrollConstructor & UnionToIntersection> { const bs = new BScrollConstructor(el, options) return (bs as unknown) as BScrollConstructor & UnionToIntersection> } createBScroll.use = BScrollConstructor.use createBScroll.plugins = BScrollConstructor.plugins createBScroll.pluginsMap = BScrollConstructor.pluginsMap type createBScroll = typeof createBScroll export interface BScrollFactory extends createBScroll { new (el: ElementParam, options?: Options & O): BScrollConstructor & UnionToIntersection> } export type BScroll = BScrollConstructor & UnionToIntersection> export const BScroll = (createBScroll as unknown) as BScrollFactory ================================================ FILE: packages/core/src/Instance.ts ================================================ import { Behavior } from './scroller/Behavior' import Actions from './scroller/Actions' import { ExposedAPI as ExposedAPIByScroller } from './scroller/Scroller' import { Animater } from './animater' import { ExposedAPI as ExposedAPIByAnimater } from './animater/Base' export interface BScrollInstance extends ExposedAPIByScroller, ExposedAPIByAnimater { [key: string]: any x: Behavior['currentPos'] y: Behavior['currentPos'] hasHorizontalScroll: Behavior['hasScroll'] hasVerticalScroll: Behavior['hasScroll'] scrollerWidth: Behavior['contentSize'] scrollerHeight: Behavior['contentSize'] maxScrollX: Behavior['maxScrollPos'] maxScrollY: Behavior['maxScrollPos'] minScrollX: Behavior['minScrollPos'] minScrollY: Behavior['minScrollPos'] movingDirectionX: Behavior['movingDirection'] movingDirectionY: Behavior['movingDirection'] directionX: Behavior['direction'] directionY: Behavior['direction'] enabled: Actions['enabled'] pending: Animater['pending'] } export const propertiesConfig = [ { sourceKey: 'scroller.scrollBehaviorX.currentPos', key: 'x' }, { sourceKey: 'scroller.scrollBehaviorY.currentPos', key: 'y' }, { sourceKey: 'scroller.scrollBehaviorX.hasScroll', key: 'hasHorizontalScroll' }, { sourceKey: 'scroller.scrollBehaviorY.hasScroll', key: 'hasVerticalScroll' }, { sourceKey: 'scroller.scrollBehaviorX.contentSize', key: 'scrollerWidth' }, { sourceKey: 'scroller.scrollBehaviorY.contentSize', key: 'scrollerHeight' }, { sourceKey: 'scroller.scrollBehaviorX.maxScrollPos', key: 'maxScrollX' }, { sourceKey: 'scroller.scrollBehaviorY.maxScrollPos', key: 'maxScrollY' }, { sourceKey: 'scroller.scrollBehaviorX.minScrollPos', key: 'minScrollX' }, { sourceKey: 'scroller.scrollBehaviorY.minScrollPos', key: 'minScrollY' }, { sourceKey: 'scroller.scrollBehaviorX.movingDirection', key: 'movingDirectionX' }, { sourceKey: 'scroller.scrollBehaviorY.movingDirection', key: 'movingDirectionY' }, { sourceKey: 'scroller.scrollBehaviorX.direction', key: 'directionX' }, { sourceKey: 'scroller.scrollBehaviorY.direction', key: 'directionY' }, { sourceKey: 'scroller.actions.enabled', key: 'enabled' }, { sourceKey: 'scroller.animater.pending', key: 'pending' }, { sourceKey: 'scroller.animater.stop', key: 'stop' }, { sourceKey: 'scroller.scrollTo', key: 'scrollTo' }, { sourceKey: 'scroller.scrollBy', key: 'scrollBy' }, { sourceKey: 'scroller.scrollToElement', key: 'scrollToElement' }, { sourceKey: 'scroller.resetPosition', key: 'resetPosition' } ] ================================================ FILE: packages/core/src/Options.ts ================================================ import { hasTransition, hasPerspective, hasTouch, Probe, EventPassthrough, extend, Quadrant, } from '@better-scroll/shared-utils' // type export type Tap = 'tap' | '' export type BounceOptions = Partial | boolean export type DblclickOptions = Partial | boolean // interface export interface BounceConfig { top: boolean bottom: boolean left: boolean right: boolean } export interface DblclickConfig { delay: number } export interface CustomOptions {} export interface DefOptions { [key: string]: any startX?: number startY?: number scrollX?: boolean scrollY?: boolean freeScroll?: boolean directionLockThreshold?: number eventPassthrough?: string click?: boolean tap?: Tap bounce?: BounceOptions bounceTime?: number momentum?: boolean momentumLimitTime?: number momentumLimitDistance?: number swipeTime?: number swipeBounceTime?: number deceleration?: number flickLimitTime?: number flickLimitDistance?: number resizePolling?: number probeType?: number stopPropagation?: boolean preventDefault?: boolean preventDefaultException?: { tagName?: RegExp className?: RegExp } tagException?: { tagName?: RegExp className?: RegExp } HWCompositing?: boolean useTransition?: boolean bindToWrapper?: boolean bindToTarget?: boolean disableMouse?: boolean disableTouch?: boolean autoBlur?: boolean translateZ?: string dblclick?: DblclickOptions autoEndDistance?: number outOfBoundaryDampingFactor?: number specifiedIndexAsContent?: number quadrant?: Quadrant } export interface Options extends DefOptions, CustomOptions {} export class CustomOptions {} export class OptionsConstructor extends CustomOptions implements DefOptions { [key: string]: any startX: number startY: number scrollX: boolean scrollY: boolean freeScroll: boolean directionLockThreshold: number eventPassthrough: string click: boolean tap: Tap bounce: BounceConfig bounceTime: number momentum: boolean momentumLimitTime: number momentumLimitDistance: number swipeTime: number swipeBounceTime: number deceleration: number flickLimitTime: number flickLimitDistance: number resizePolling: number probeType: number stopPropagation: boolean preventDefault: boolean preventDefaultException: { tagName?: RegExp className?: RegExp } tagException: { tagName?: RegExp className?: RegExp } HWCompositing: boolean useTransition: boolean bindToWrapper: boolean bindToTarget: boolean disableMouse: boolean disableTouch: boolean autoBlur: boolean translateZ: string dblclick: DblclickOptions autoEndDistance: number outOfBoundaryDampingFactor: number specifiedIndexAsContent: number quadrant: Quadrant constructor() { super() this.startX = 0 this.startY = 0 this.scrollX = false this.scrollY = true this.freeScroll = false this.directionLockThreshold = 0 this.eventPassthrough = EventPassthrough.None this.click = false this.dblclick = false this.tap = '' this.bounce = { top: true, bottom: true, left: true, right: true, } this.bounceTime = 800 this.momentum = true this.momentumLimitTime = 300 this.momentumLimitDistance = 15 this.swipeTime = 2500 this.swipeBounceTime = 500 this.deceleration = 0.0015 this.flickLimitTime = 200 this.flickLimitDistance = 100 this.resizePolling = 60 this.probeType = Probe.Default this.stopPropagation = false this.preventDefault = true this.preventDefaultException = { tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT|AUDIO)$/, } this.tagException = { tagName: /^TEXTAREA$/, } this.HWCompositing = true this.useTransition = true this.bindToWrapper = false this.bindToTarget = false this.disableMouse = hasTouch this.disableTouch = !hasTouch this.autoBlur = true this.autoEndDistance = 5 this.outOfBoundaryDampingFactor = 1 / 3 this.specifiedIndexAsContent = 0 this.quadrant = Quadrant.First } merge(options?: Options) { if (!options) return this for (let key in options) { if (key === 'bounce') { this.bounce = this.resolveBounce(options[key]!) continue } this[key] = options[key] } return this } process() { this.translateZ = this.HWCompositing && hasPerspective ? ' translateZ(1px)' : '' this.useTransition = this.useTransition && hasTransition this.preventDefault = !this.eventPassthrough && this.preventDefault // If you want eventPassthrough I have to lock one of the axes this.scrollX = this.eventPassthrough === EventPassthrough.Horizontal ? false : this.scrollX this.scrollY = this.eventPassthrough === EventPassthrough.Vertical ? false : this.scrollY // With eventPassthrough we also need lockDirection mechanism this.freeScroll = this.freeScroll && !this.eventPassthrough // force true when freeScroll is true this.scrollX = this.freeScroll ? true : this.scrollX this.scrollY = this.freeScroll ? true : this.scrollY this.directionLockThreshold = this.eventPassthrough ? 0 : this.directionLockThreshold return this } resolveBounce(bounceOptions: BounceOptions): BounceConfig { const DEFAULT_BOUNCE = { top: true, right: true, bottom: true, left: true, } const NEGATED_BOUNCE = { top: false, right: false, bottom: false, left: false, } let ret: BounceConfig if (typeof bounceOptions === 'object') { ret = extend(DEFAULT_BOUNCE, bounceOptions) } else { ret = bounceOptions ? DEFAULT_BOUNCE : NEGATED_BOUNCE } return ret } } ================================================ FILE: packages/core/src/__mocks__/Options.ts ================================================ const mockOptions = jest.fn().mockImplementation(() => { return { startX: 0, startY: 0, scrollX: false, scrollY: true, freeScroll: false, directionLockThreshold: 0, eventPassthrough: '', click: false, tap: '', translateZ: ' translateZ(0)', bounce: { top: true, bottom: true, left: true, right: true, }, bounceTime: 800, momentum: true, momentumLimitTime: 300, momentumLimitDistance: 15, swipeTime: 2500, swipeBounceTime: 500, deceleration: 0.0015, flickLimitTime: 200, flickLimitDistance: 100, resizePolling: 60, probeType: 0, stopPropagation: false, preventDefault: true, preventDefaultException: { tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT|AUDIO)$/, }, HWCompositing: true, useTransition: true, bindToWrapper: false, disableMouse: true, observeDOM: true, autoBlur: true, mouseWheel: false, infinity: false, specifiedIndexAsContent: 0, quadrant: 0, outOfBoundaryDampingFactor: 1 / 3, merge: jest.fn(), process: jest.fn(), } }) export { mockOptions as OptionsConstructor } ================================================ FILE: packages/core/src/__mocks__/index.ts ================================================ import Scroller from '../scroller/Scroller' import { OptionsConstructor } from '../Options' import { EventEmitter } from '@better-scroll/shared-utils' jest.mock('../scroller/Scroller') jest.mock('../Options') const BScroll = jest.fn().mockImplementation((wrapper, options) => { options = Object.assign(new OptionsConstructor(), options) const eventEmitter = new EventEmitter([ // bscroll 'refresh', 'enable', 'disable', 'destroy', // scroller 'beforeScrollStart', 'scrollStart', 'scroll', 'scrollEnd', 'touchEnd', 'flick', 'alterOptions', 'mousewheelStart', 'mousewheelMove', 'mousewheelEnd', ]) const res = { wrapper: wrapper, options: options, hooks: new EventEmitter([ 'refresh', 'enable', 'disable', 'destroy', 'beforeInitialScrollTo', ]), scroller: new Scroller(wrapper, wrapper.children[0], options), // own methods proxy: jest.fn(), refresh: jest.fn(), // proxy methods scrollTo: jest.fn(), resetPosition: jest.fn(), registerType: jest.fn().mockImplementation((names: string[]) => { names.forEach((name) => { const eventTypes = eventEmitter.eventTypes eventTypes[name] = name }) }), disable: jest.fn(), enable: jest.fn(), stop: jest.fn(), plugins: {}, x: 0, y: 0, maxScrollY: 0, maxScrollX: 0, minScrollX: 0, minScrollY: 0, hasVerticalScroll: true, hasHorizontalScroll: false, enabled: true, pending: false, } Object.setPrototypeOf(res, eventEmitter) return res }) export default BScroll ================================================ FILE: packages/core/src/__tests__/Options.spec.ts ================================================ import { OptionsConstructor } from '../Options' describe('BetterScroll Options', () => { let options: OptionsConstructor beforeEach(() => { options = new OptionsConstructor() }) afterEach(() => { jest.clearAllMocks() }) it('should have default value', () => { expect(options).toEqual({ HWCompositing: true, autoBlur: true, bindToWrapper: false, bounce: { bottom: true, left: true, right: true, top: true, }, bounceTime: 800, click: false, dblclick: false, deceleration: 0.0015, directionLockThreshold: 0, disableMouse: false, disableTouch: true, eventPassthrough: '', flickLimitDistance: 100, flickLimitTime: 200, freeScroll: false, momentum: true, momentumLimitDistance: 15, momentumLimitTime: 300, preventDefault: true, preventDefaultException: { tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT|AUDIO)$/, }, tagException: { tagName: /^TEXTAREA$/, }, probeType: 0, resizePolling: 60, scrollX: false, scrollY: true, startX: 0, startY: 0, stopPropagation: false, swipeBounceTime: 500, swipeTime: 2500, tap: '', useTransition: true, autoEndDistance: 5, bindToTarget: false, outOfBoundaryDampingFactor: 1 / 3, specifiedIndexAsContent: 0, quadrant: 1, }) }) it('should shallow copy options when call merge(options)', () => { options.merge({ scrollY: false, scrollX: true, bounce: false, }) expect(options).toEqual({ HWCompositing: true, autoBlur: true, bindToWrapper: false, bounce: { top: false, right: false, bottom: false, left: false, }, bounceTime: 800, click: false, dblclick: false, deceleration: 0.0015, directionLockThreshold: 0, disableMouse: false, disableTouch: true, eventPassthrough: '', flickLimitDistance: 100, flickLimitTime: 200, freeScroll: false, momentum: true, momentumLimitDistance: 15, momentumLimitTime: 300, preventDefault: true, preventDefaultException: { tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT|AUDIO)$/, }, tagException: { tagName: /^TEXTAREA$/, }, probeType: 0, resizePolling: 60, scrollX: true, scrollY: false, startX: 0, startY: 0, stopPropagation: false, swipeBounceTime: 500, swipeTime: 2500, tap: '', useTransition: true, autoEndDistance: 5, bindToTarget: false, outOfBoundaryDampingFactor: 1 / 3, specifiedIndexAsContent: 0, quadrant: 1, }) // an invalid parameter const ret = options.merge() expect(ret).toBe(options) }) it('should generate some extra properties of options', () => { options.process() expect(options).toEqual({ HWCompositing: true, autoBlur: true, bindToWrapper: false, bounce: { bottom: true, left: true, right: true, top: true, }, bounceTime: 800, click: false, dblclick: false, deceleration: 0.0015, directionLockThreshold: 0, disableMouse: false, disableTouch: true, eventPassthrough: '', flickLimitDistance: 100, flickLimitTime: 200, freeScroll: false, momentum: true, momentumLimitDistance: 15, momentumLimitTime: 300, preventDefault: true, preventDefaultException: { tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT|AUDIO)$/, }, tagException: { tagName: /^TEXTAREA$/, }, probeType: 0, resizePolling: 60, scrollX: false, scrollY: true, startX: 0, startY: 0, stopPropagation: false, swipeBounceTime: 500, swipeTime: 2500, tap: '', useTransition: true, translateZ: '', autoEndDistance: 5, bindToTarget: false, outOfBoundaryDampingFactor: 1 / 3, specifiedIndexAsContent: 0, quadrant: 1, }) }) it('should resolve bounce when calling process', () => { options.merge({ bounce: false, }) expect(options.bounce).toEqual({ bottom: false, left: false, right: false, top: false, }) options.merge({ bounce: true, }) expect(options.bounce).toEqual({ bottom: true, left: true, right: true, top: true, }) options.merge({ bounce: { top: false, bottom: false, }, }) expect(options.bounce).toEqual({ bottom: false, left: true, right: true, top: false, }) }) }) ================================================ FILE: packages/core/src/__tests__/__utils__/event.ts ================================================ export function createEvent(type: string, name: string): Event { const e = document.createEvent(type || 'Event') e.initEvent(name, true, true) return e } interface CustomClickEvent extends MouseEvent { pageX: number pageY: number } export function dispatchClick(target: EventTarget, name = 'click') { const event = createEvent('', name) event.pageX = 0 event.pageY = 0 target.dispatchEvent(event) } interface CustomTouch { pageX: number pageY: number } type CustomTouches = CustomTouch[] | CustomTouch export interface CustomTouchEvent extends Event { touches: CustomTouches targetTouches: CustomTouches changedTouches: CustomTouches } interface CustomMouseEvent extends Event { button: 0 | 1 pageX: number pageY: number } export function dispatchTouch( target: EventTarget, name = 'touchstart', touches: CustomTouches ): void { const event = createEvent('', name) event.touches = event.targetTouches = event.changedTouches = touches target.dispatchEvent(event) } export function dispatchMouse( target: EventTarget, name = 'mousedown', useLeftButton = true ): void { const event = createEvent('', name) event.button = useLeftButton ? 0 : 1 event.pageX = 0 event.pageY = 0 target.dispatchEvent(event) } export function dispatchTouchStart( target: EventTarget, touches: CustomTouches ): void { dispatchTouch(target, 'touchstart', touches) } export function dispatchTouchMove( target: EventTarget, touches: CustomTouches ): void { dispatchTouch(target, 'touchmove', touches) } export function dispatchTouchEnd( target: EventTarget, touches: CustomTouches ): void { dispatchTouch(target, 'touchend', touches) } export function dispatchTouchCancel( target: EventTarget, touches: CustomTouches ): void { dispatchTouch(target, 'touchcancel', touches) } export function dispatchSwipe( target: EventTarget, touches: CustomTouches, duration: number, cb: () => any ): void { // TODO 优化写法 if (!Array.isArray(touches)) { touches = [touches] } if (touches instanceof Array) { dispatchTouchStart(target, touches[0]) const moveAndEnd = () => { if (touches instanceof Array) { dispatchTouchMove(target, touches[1] || touches[0]) dispatchTouchEnd(target, touches[2] || touches[1] || touches[0]) } cb && cb() } if (duration) { setTimeout(moveAndEnd, duration) } else { moveAndEnd() } } } ================================================ FILE: packages/core/src/__tests__/__utils__/layout.ts ================================================ export interface CustomHTMLDivElement extends HTMLDivElement { clientWidth: number clientHeight: number offsetWidth: number offsetHeight: number offsetTop: number offsetLeft: number _jsdomMockClientWidth?: number _jsdomMockClientHeight?: number _jsdomMockOffsetWidth?: number _jsdomMockOffsetHeight?: number _jsdomMockOffsetTop?: number _jsdomMockOffsetLeft?: number [key: string]: any } function firstUpper(key: string) { return key.charAt(0).toUpperCase() + key.slice(1) } function genMockPrototype(mockName: string) { return { get: jest.fn().mockImplementation(dom => { return Number(dom.getAttribute(mockName)) }) } } function mockHTMLPrototype(propName: string, mockGetter: jest.Mock) { Object.defineProperty(HTMLElement.prototype, propName, { get: function() { return mockGetter(this) }, configurable: true }) } export function mockDomOffset( dom: CustomHTMLDivElement, offsetObj: { width?: number height?: number top?: number left?: number [key: string]: any } ) { Object.keys(offsetObj).forEach(key => { const mockName = `_jsdomMockOffset${firstUpper(key)}` dom.setAttribute(mockName, offsetObj[key]) }) } export function mockDomClient( dom: CustomHTMLDivElement, clientObj: { width?: number height?: number [key: string]: any } ) { Object.keys(clientObj).forEach(key => { const mockName = `_jsdomMockClient${firstUpper(key)}` dom.setAttribute(mockName, clientObj[key]) }) } export function createDiv( width: number = 0, height: number = 0, top: number = 0, left: number = 0 ) { const dom = document.createElement('div') as CustomHTMLDivElement mockDomOffset(dom, { width, height, top, left }) mockDomClient(dom, { width, height }) return dom } export const mockClientWidth = genMockPrototype('_jsdomMockClientWidth') mockHTMLPrototype('clientWidth', mockClientWidth.get) export const mockClientHeight = genMockPrototype('_jsdomMockClientHeight') mockHTMLPrototype('clientHeight', mockClientHeight.get) export const mockOffsetWidth = genMockPrototype('_jsdomMockOffsetWidth') mockHTMLPrototype('offsetWidth', mockOffsetWidth.get) export const mockOffsetHeight = genMockPrototype('_jsdomMockOffsetHeight') mockHTMLPrototype('offsetHeight', mockOffsetHeight.get) export const mockOffsetTop = genMockPrototype('_jsdomMockOffsetTop') mockHTMLPrototype('offsetTop', mockOffsetTop.get) export const mockOffsetLeft = genMockPrototype('_jsdomMockOffsetLeft') mockHTMLPrototype('offsetLeft', mockOffsetLeft.get) ================================================ FILE: packages/core/src/__tests__/index.spec.ts ================================================ import BScroll from '../index' describe('BetterScroll Core', () => { let bscroll: BScroll let wrapper = document.createElement('div') let content = document.createElement('p') wrapper.appendChild(content) beforeEach(() => { bscroll = new BScroll(wrapper, {}) }) afterEach(() => { BScroll.plugins = [] BScroll.pluginsMap = {} }) it('use()', () => { const plugin = class MyPlugin { static pluginName = 'myPlugin' } BScroll.use(plugin) // has installed BScroll.use(plugin) expect(BScroll.plugins.length).toBe(1) // Plugin should specify pluginName const spyFn = jest.spyOn(console, 'error') const unnamedPlugin = class UnnamedPlugin {} BScroll.use(unnamedPlugin as any) expect(spyFn).toBeCalled() spyFn.mockRestore() }) it('should init plugins when set top-level of BScroll options', () => { let mockFn = jest.fn() const plugin = class MyPlugin { static pluginName = 'myPlugin2' constructor(bscroll: BScroll) { mockFn(bscroll) } } BScroll.use(plugin) let wrapper = document.createElement('div') wrapper.appendChild(document.createElement('p')) let bs = new BScroll(wrapper, { myPlugin2: true }) expect(mockFn).toBeCalledWith(bs) }) it('should throw error when wrapper is not a ElementNode or wrapper has no children ', () => { let spy = jest.spyOn(console, 'error') let bs = new BScroll('.div', {}) let bs2 = new BScroll(document.createElement('div'), {}) expect(spy).toHaveBeenCalled() expect(spy).toBeCalledTimes(2) }) it('disable()', () => { const mockFn = jest.fn() bscroll.on(bscroll.eventTypes.disable, mockFn) bscroll.hooks.on(bscroll.hooks.eventTypes.disable, mockFn) bscroll.disable() expect(mockFn).toBeCalledTimes(2) }) it('destroy()', () => { const mockFn = jest.fn() bscroll.on(bscroll.eventTypes.destroy, mockFn) bscroll.hooks.on(bscroll.hooks.eventTypes.destroy, mockFn) bscroll.destroy() expect(mockFn).toBeCalledTimes(2) }) it('eventRegister()', () => { bscroll.eventRegister(['dummy']) expect(bscroll.eventTypes.dummy).toBeTruthy() }) it('should refresh when window resized', () => { const mockFn = jest.fn() bscroll.on(bscroll.eventTypes.refresh, mockFn) bscroll.scroller.hooks.trigger(bscroll.scroller.hooks.eventTypes.resize) expect(mockFn).toBeCalledTimes(1) }) it('plugin wanna control scroll position ', () => { const mockFn = jest.fn().mockImplementation(() => true) class DummyPlugin { static pluginName = 'dummy' constructor(scroll: BScroll) { scroll.hooks.on(scroll.hooks.eventTypes.beforeInitialScrollTo, mockFn) } } BScroll.use(DummyPlugin) bscroll = new BScroll(wrapper, { dummy: true }) expect(mockFn).toBeCalled() }) it('should trigger contentChanged hook when content DOM has changed', () => { const mockFn = jest.fn() bscroll.on(bscroll.eventTypes.contentChanged, mockFn) // content DOM has wrapper.removeChild(content) wrapper.appendChild(document.createElement('div')) bscroll.refresh() expect(mockFn).toBeCalled() }) }) ================================================ FILE: packages/core/src/animater/Animation.ts ================================================ import Base from './Base' import { TranslaterPoint } from '../translater' import { getNow, requestAnimationFrame, cancelAnimationFrame, EaseFn, Probe, } from '@better-scroll/shared-utils' export default class Animation extends Base { move( startPoint: TranslaterPoint, endPoint: TranslaterPoint, time: number, easingFn: EaseFn | string ) { // time is 0 if (!time) { this.translate(endPoint) if (this.options.probeType === Probe.Realtime) { this.hooks.trigger(this.hooks.eventTypes.move, endPoint) } this.hooks.trigger(this.hooks.eventTypes.end, endPoint) return } this.animate(startPoint, endPoint, time, easingFn as EaseFn) } private animate( startPoint: TranslaterPoint, endPoint: TranslaterPoint, duration: number, easingFn: EaseFn ) { let startTime = getNow() const destTime = startTime + duration const isRealtimeProbeType = this.options.probeType === Probe.Realtime const step = () => { let now = getNow() // js animation end if (now >= destTime) { this.translate(endPoint) if (isRealtimeProbeType) { this.hooks.trigger(this.hooks.eventTypes.move, endPoint) } this.hooks.trigger(this.hooks.eventTypes.end, endPoint) return } now = (now - startTime) / duration let easing = easingFn(now) const newPoint = {} as TranslaterPoint Object.keys(endPoint).forEach((key) => { const startValue = startPoint[key] const endValue = endPoint[key] newPoint[key] = (endValue - startValue) * easing + startValue }) this.translate(newPoint) if (isRealtimeProbeType) { this.hooks.trigger(this.hooks.eventTypes.move, newPoint) } if (this.pending) { this.timer = requestAnimationFrame(step) } // call bs.stop() should not dispatch end hook again. // forceStop hook will do this. /* istanbul ignore if */ if (!this.pending) { if (this.callStopWhenPending) { this.callStopWhenPending = false } else { // raf ends should dispatch end hook. this.hooks.trigger(this.hooks.eventTypes.end, endPoint) } } } this.setPending(true) // when manually call bs.stop(), then bs.scrollTo() // we should reset callStopWhenPending to dispatch end hook if (this.callStopWhenPending) { this.setCallStop(false) } cancelAnimationFrame(this.timer) step() } doStop(): boolean { const pending = this.pending this.setForceStopped(false) this.setCallStop(false) // still in requestFrameAnimation if (pending) { this.setPending(false) cancelAnimationFrame(this.timer) const pos = this.translater.getComputedPosition() this.setForceStopped(true) this.setCallStop(true) this.hooks.trigger(this.hooks.eventTypes.forceStop, pos) } return pending } stop() { const stopFromAnimation = this.doStop() if (stopFromAnimation) { this.hooks.trigger(this.hooks.eventTypes.callStop) } } } ================================================ FILE: packages/core/src/animater/Base.ts ================================================ import { EaseFn, safeCSSStyleDeclaration, cancelAnimationFrame, EventEmitter, Probe, } from '@better-scroll/shared-utils' import Translater, { TranslaterPoint } from '../translater' export interface ExposedAPI { stop(): void } export default abstract class Base implements ExposedAPI { content: HTMLElement style: safeCSSStyleDeclaration hooks: EventEmitter timer: number = 0 pending: boolean callStopWhenPending: boolean forceStopped: boolean _reflow: number; [key: string]: any constructor( content: HTMLElement, public translater: Translater, public options: { probeType: number } ) { this.hooks = new EventEmitter([ 'move', 'end', 'beforeForceStop', 'forceStop', 'callStop', 'time', 'timeFunction', ]) this.setContent(content) } translate(endPoint: TranslaterPoint) { this.translater.translate(endPoint) } setPending(pending: boolean) { this.pending = pending } setForceStopped(forceStopped: boolean) { this.forceStopped = forceStopped } setCallStop(called: boolean) { this.callStopWhenPending = called } setContent(content: HTMLElement) { if (this.content !== content) { this.content = content this.style = content.style as safeCSSStyleDeclaration this.stop() } } clearTimer() { if (this.timer) { cancelAnimationFrame(this.timer) this.timer = 0 } } abstract move( startPoint: TranslaterPoint, endPoint: TranslaterPoint, time: number, easing: string | EaseFn ): void abstract doStop(): void abstract stop(): void destroy() { this.hooks.destroy() cancelAnimationFrame(this.timer) } } ================================================ FILE: packages/core/src/animater/Transition.ts ================================================ import { style, requestAnimationFrame, cancelAnimationFrame, EaseFn, Probe, } from '@better-scroll/shared-utils' import Base from './Base' import { TranslaterPoint } from '../translater' import { isValidPostion } from '../utils/compat' export default class Transition extends Base { startProbe(startPoint: TranslaterPoint, endPoint: TranslaterPoint) { let prePos = startPoint const probe = () => { let pos = this.translater.getComputedPosition() if (isValidPostion(startPoint, endPoint, pos, prePos)) { this.hooks.trigger(this.hooks.eventTypes.move, pos) } // call bs.stop() should not dispatch end hook again. // forceStop hook will do this. /* istanbul ignore if */ if (!this.pending) { if (this.callStopWhenPending) { this.callStopWhenPending = false } else { // transition ends should dispatch end hook. this.hooks.trigger(this.hooks.eventTypes.end, pos) } } prePos = pos if (this.pending) { this.timer = requestAnimationFrame(probe) } } // when manually call bs.stop(), then bs.scrollTo() // we should reset callStopWhenPending to dispatch end hook if (this.callStopWhenPending) { this.setCallStop(false) } cancelAnimationFrame(this.timer) probe() } transitionTime(time = 0) { this.style[style.transitionDuration] = time + 'ms' this.hooks.trigger(this.hooks.eventTypes.time, time) } transitionTimingFunction(easing: string) { this.style[style.transitionTimingFunction] = easing this.hooks.trigger(this.hooks.eventTypes.timeFunction, easing) } transitionProperty() { this.style[style.transitionProperty] = style.transform } move( startPoint: TranslaterPoint, endPoint: TranslaterPoint, time: number, easingFn: string | EaseFn ) { this.setPending(time > 0) this.transitionTimingFunction(easingFn as string) this.transitionProperty() this.transitionTime(time) this.translate(endPoint) const isRealtimeProbeType = this.options.probeType === Probe.Realtime if (time && isRealtimeProbeType) { this.startProbe(startPoint, endPoint) } // if we change content's transformY in a tick // such as: 0 -> 50px -> 0 // transitionend will not be triggered // so we forceupdate by reflow if (!time) { this._reflow = this.content.offsetHeight if (isRealtimeProbeType) { this.hooks.trigger(this.hooks.eventTypes.move, endPoint) } this.hooks.trigger(this.hooks.eventTypes.end, endPoint) } } doStop(): boolean { const pending = this.pending this.setForceStopped(false) this.setCallStop(false) // still in transition if (pending) { this.setPending(false) cancelAnimationFrame(this.timer) const { x, y } = this.translater.getComputedPosition() this.transitionTime() this.translate({ x, y }) this.setForceStopped(true) this.setCallStop(true) this.hooks.trigger(this.hooks.eventTypes.forceStop, { x, y }) } return pending } stop() { const stopFromTransition = this.doStop() if (stopFromTransition) { this.hooks.trigger(this.hooks.eventTypes.callStop) } } } ================================================ FILE: packages/core/src/animater/__mocks__/Animation.ts ================================================ import { EventEmitter } from '@better-scroll/shared-utils' const Animation = jest .fn() .mockImplementation((content, translater, bscrollOptions) => { return { content, translater, options: bscrollOptions, style: content.style, pending: false, forceStopped: false, timer: 0, hooks: new EventEmitter([ 'move', 'end', 'forceStop', 'beforeForceStop', 'callStop', 'time', 'timeFunction', ]), translate: jest.fn(), stop: jest.fn(), doStop: jest.fn(), move: jest.fn(), destroy: jest.fn(), setPending: jest.fn(), setForceStopped: jest.fn(), setCallStop: jest.fn(), setContent: jest.fn(), clearTimer: jest.fn(), } }) export default Animation ================================================ FILE: packages/core/src/animater/__mocks__/Transition.ts ================================================ import { EventEmitter } from '@better-scroll/shared-utils' const Transition = jest .fn() .mockImplementation((content, translater, bscrollOptions) => { return { content, translater, options: bscrollOptions, style: content.style, pending: false, forceStopped: false, timer: 0, hooks: new EventEmitter([ 'move', 'end', 'forceStop', 'beforeForceStop', 'callStop', 'time', 'timeFunction', ]), translate: jest.fn(), stop: jest.fn(), doStop: jest.fn(), move: jest.fn(), startProbe: jest.fn(), transitionTime: jest.fn(), transitionTimingFunction: jest.fn(), destroy: jest.fn(), setPending: jest.fn(), setForceStopped: jest.fn(), setCallStop: jest.fn(), setContent: jest.fn(), clearTimer: jest.fn(), } }) export default Transition ================================================ FILE: packages/core/src/animater/__mocks__/index.ts ================================================ import Transition from '../Transition' import Animation from '../Animation' jest.mock('../Transition') jest.mock('../Animation') const createAnimater = jest .fn() .mockImplementation((element, translater, bscrollOptions) => { if (bscrollOptions.useTransition) { return new Transition(element, translater, bscrollOptions as { probeType: number }) } else { return new Animation(element, translater, bscrollOptions as { probeType: number }) } }) export default createAnimater ================================================ FILE: packages/core/src/animater/__tests__/Animation.spec.ts ================================================ import Translater from '../../translater' jest.mock('../../translater') let mockRequestAnimationFrame = jest.fn() let mockCancelAnimationFrame = jest.fn() jest.mock('@better-scroll/shared-utils/src/raf', () => { return { requestAnimationFrame: (cb: any) => mockRequestAnimationFrame(cb), cancelAnimationFrame: () => mockCancelAnimationFrame(), } }) let mockGetNow = jest.fn() jest.mock('@better-scroll/shared-utils/src/lang', () => { return { getNow: () => mockGetNow(), } }) import Animation from '../Animation' function createAnimation(probeType: number) { const dom = document.createElement('div') const translater = new Translater(dom) const animation = new Animation(dom, translater, { probeType }) return { dom, translater, animation, } } describe('Animation Class test suit', () => { beforeAll(() => { jest.useFakeTimers() }) afterEach(() => { jest.clearAllTimers() jest.clearAllMocks() }) it('should off hooks and cancelAnimationFrame when destroy', () => { const { animation } = createAnimation(0) const hooksDestroySpy = jest.spyOn(animation.hooks, 'destroy') animation.destroy() expect(mockCancelAnimationFrame).toBeCalledTimes(1) expect(hooksDestroySpy).toBeCalledTimes(1) }) it('should move to endPoint and trigger hooks in one step when time=0', () => { const { animation, translater } = createAnimation(0) const onMove = jest.fn() const onEnd = jest.fn() animation.hooks.on('move', onMove) animation.hooks.on('end', onEnd) const startPoint = { x: 0, y: 0, } const endPoint = { x: 10, y: 10, } animation.options.probeType = 3 animation.move(startPoint, endPoint, 0, 'easing') expect(translater.translate).toBeCalledTimes(1) expect(translater.translate).toBeCalledWith(endPoint) expect(onMove).toBeCalled() expect(onEnd).toBeCalled() }) it('should move to endPoint for serveral steps with time', () => { const { animation, translater, dom } = createAnimation(3) const onMove = jest.fn() const onEnd = jest.fn() const easeFn = jest.fn() animation.hooks.on('move', onMove) animation.hooks.on('end', onEnd) const startPoint = { x: 0, y: 0, } const endPoint = { x: 0, y: 100, } mockRequestAnimationFrame.mockImplementation((cb) => { setTimeout(() => { cb() }, 200) }) mockGetNow .mockImplementationOnce(() => { return 1000 }) .mockImplementationOnce(() => { return 1100 }) .mockImplementationOnce(() => { return 1600 }) easeFn.mockImplementationOnce(() => { return 0.2 }) animation.move(startPoint, endPoint, 500, easeFn) expect(easeFn).toBeCalledWith(0.2) expect(translater.translate).toBeCalledWith({ x: 0, y: 20, }) expect(onMove).toBeCalledTimes(1) jest.advanceTimersByTime(200) expect(translater.translate).toBeCalledWith({ x: 0, y: 100, }) expect(onMove).toBeCalledTimes(2) expect(onEnd).toBeCalled() animation.destroy() }) it('should force stop', () => { const { animation, translater } = createAnimation(3) animation.setCallStop(true) const onMove = jest.fn() const onForceStop = jest.fn() const easeFn = jest.fn() animation.hooks.on('move', onMove) animation.hooks.on('forceStop', onForceStop) const startPoint = { x: 0, y: 0, } const endPoint = { x: 0, y: 100, } mockRequestAnimationFrame.mockImplementation((cb) => { setTimeout(() => { cb() }, 200) }) mockGetNow .mockImplementationOnce(() => { return 1000 }) .mockImplementationOnce(() => { return 1100 }) .mockImplementationOnce(() => { return 1600 }) easeFn.mockImplementationOnce(() => { return 0.2 }) animation.move(startPoint, endPoint, 500, easeFn) expect(easeFn).toBeCalledWith(0.2) expect(translater.translate).toBeCalledWith({ x: 0, y: 20, }) expect(animation.pending).toBe(true) expect(animation.callStopWhenPending).toBe(false) ;(translater.getComputedPosition).mockImplementation(() => { return 20 }) animation.stop() expect(animation.pending).toBe(false) expect(mockCancelAnimationFrame).toBeCalled() expect(animation.callStopWhenPending).toBe(true) expect(onForceStop).toBeCalledWith(20) animation.destroy() }) }) ================================================ FILE: packages/core/src/animater/__tests__/Transition.spec.ts ================================================ import Translater from '../../translater/index' jest.mock('../../translater/index') let mockRequestAnimationFrame = jest.fn() let mockCancelAnimationFrame = jest.fn() jest.mock('@better-scroll/shared-utils/src/raf', () => { return { requestAnimationFrame: (cb: any) => mockRequestAnimationFrame(cb), cancelAnimationFrame: () => mockCancelAnimationFrame(), } }) import Transition from '@better-scroll/core/src/animater/Transition' function createTransition(probeType: number) { const dom = document.createElement('div') const translater = new Translater(dom) const transition = new Transition(dom, translater, { probeType }) return { dom, translater, transition, } } describe('Transition Class test suit', () => { beforeAll(() => { jest.useFakeTimers() }) afterEach(() => { jest.clearAllTimers() jest.clearAllMocks() }) it('should off hooks and cancelAnimationFrame when destroy', () => { const { transition } = createTransition(0) const hooksDestroySpy = jest.spyOn(transition.hooks, 'destroy') transition.destroy() expect(mockCancelAnimationFrame).toBeCalledTimes(1) expect(hooksDestroySpy).toBeCalledTimes(1) }) it('should set timeFunction and trigger event', () => { const { transition, dom } = createTransition(0) const onTimeFunction = jest.fn() const onTime = jest.fn() transition.hooks.on('time', onTime) transition.hooks.on('timeFunction', onTimeFunction) const startPoint = { x: 0, y: 0, } const endPoint = { x: 10, y: 10, } transition.move(startPoint, endPoint, 200, 'cubic-bezier(0.23, 1, 0.32, 1)') expect(onTime).toHaveBeenCalledTimes(1) expect(onTimeFunction).toHaveBeenCalledTimes(1) expect(dom.style.transitionTimingFunction).toBe( 'cubic-bezier(0.23, 1, 0.32, 1)' ) expect(dom.style.transitionDuration).toBe('200ms') transition.destroy() }) it('should call translater with right arguments', () => { const { transition, translater } = createTransition(0) const startPoint = { x: 0, y: 0, } const endPoint = { x: 10, y: 10, } transition.move(startPoint, endPoint, 200, 'cubic-bezier(0.23, 1, 0.32, 1)') expect(translater.translate).toBeCalledWith(endPoint) transition.destroy() }) it('should trigger end hook with time=0', () => { const { transition } = createTransition(0) const onEnd = jest.fn() transition.hooks.on('end', onEnd) const startPoint = { x: 0, y: 0, } const endPoint = { x: 10, y: 10, } transition.options.probeType = 3 transition.move(startPoint, endPoint, 0, 'cubic-bezier(0.23, 1, 0.32, 1)') expect(onEnd).toHaveBeenCalled() transition.destroy() }) it('should stop', () => { const { transition, translater, dom } = createTransition(0) const onForceStop = jest.fn() transition.hooks.on('forceStop', onForceStop) const startPoint = { x: 0, y: 0, } const endPoint = { x: 0, y: 10, } transition.move(startPoint, endPoint, 200, 'cubic-bezier(0.23, 1, 0.32, 1)') ;(translater.getComputedPosition).mockImplementation(() => { return { x: 10, y: 10 } }) transition.stop() expect(dom.style.transitionDuration).toBe('0ms') expect(translater.translate).toBeCalledWith({ x: 10, y: 10 }) expect(onForceStop).toBeCalledWith({ x: 10, y: 10 }) expect(mockCancelAnimationFrame).toBeCalled() expect(transition.callStopWhenPending).toBe(true) transition.destroy() }) it('should startProbe with probeType=3', () => { const { transition, translater } = createTransition(3) translater.getComputedPosition = jest.fn().mockImplementation(() => { return { x: 0, y: 0 } }) mockRequestAnimationFrame.mockImplementation((cb) => { setTimeout(() => { cb() }, 200) }) const onMove = jest.fn() const onEnd = jest.fn() transition.hooks.on('time', onMove) transition.hooks.on('end', onEnd) const startPoint = { x: 0, y: 0, } const endPoint = { x: 10, y: 10, } transition.move(startPoint, endPoint, 200, 'cubic-bezier(0.23, 1, 0.32, 1)') expect(transition.callStopWhenPending).toBe(false) jest.advanceTimersByTime(200) expect(onMove).toBeCalled() transition.pending = false jest.advanceTimersByTime(200) expect(onEnd).toBeCalled() transition.destroy() }) it('clearTimer ', () => { const { transition } = createTransition(0) transition.timer = 1 transition.clearTimer() expect(transition.timer).toBe(0) }) it('should reset callStopWhenPending', () => { const { transition } = createTransition(0) transition.setPending(true) transition.stop() transition.startProbe({ x: 0, y: 0 }, { x: 0, y: -10 }) expect(transition.callStopWhenPending).toBe(false) }) }) ================================================ FILE: packages/core/src/animater/__tests__/index.spec.ts ================================================ import Translater from '../../translater/index' import Transition from '../Transition' import Animation from '../Animation' import { OptionsConstructor } from '../../Options' jest.mock('../Animation') jest.mock('../Transition') jest.mock('../Base') jest.mock('../../translater/index') jest.mock('../../Options') import createAnimater from '../index' describe('animater create test suit', () => { const dom = document.createElement('div') const translater = new Translater(dom) it('should create Transition class when useTransition=true', () => { const options = new OptionsConstructor() options.probeType = 0 options.useTransition = true const animater = createAnimater(dom, translater, options) expect(Transition).toBeCalledWith(dom, translater, { probeType: 0 }) }) it('should create Animation class when useTransition=false', () => { const options = new OptionsConstructor() options.probeType = 0 options.useTransition = false const animater = createAnimater(dom, translater, options) expect(Animation).toBeCalledWith(dom, translater, { probeType: 0 }) }) }) ================================================ FILE: packages/core/src/animater/index.ts ================================================ import Translater from '../translater' import { Options as BScrollOptions } from '../Options' import Animater from './Base' import Transition from './Transition' import Animation from './Animation' export { Animater, Transition, Animation } export default function createAnimater( element: HTMLElement, translater: Translater, options: BScrollOptions ) { const useTransition = options.useTransition let animaterOptions = {} Object.defineProperty(animaterOptions, 'probeType', { enumerable: true, configurable: false, get() { return options.probeType }, }) if (useTransition) { return new Transition( element, translater, animaterOptions as { probeType: number } ) } else { return new Animation( element, translater, animaterOptions as { probeType: number } ) } } ================================================ FILE: packages/core/src/base/ActionsHandler.ts ================================================ import { TouchEvent, // dom preventDefaultExceptionFn, tagExceptionFn, eventTypeMap, EventType, MouseButton, EventRegister, EventEmitter, } from '@better-scroll/shared-utils' type Exception = { tagName?: RegExp className?: RegExp } export interface Options { [key: string]: boolean | number | Exception click: boolean bindToWrapper: boolean disableMouse: boolean disableTouch: boolean preventDefault: boolean stopPropagation: boolean preventDefaultException: Exception tagException: Exception autoEndDistance: number } export default class ActionsHandler { hooks: EventEmitter initiated: number pointX: number pointY: number wrapperEventRegister: EventRegister targetEventRegister: EventRegister constructor(public wrapper: HTMLElement, public options: Options) { this.hooks = new EventEmitter([ 'beforeStart', 'start', 'move', 'end', 'click', ]) this.handleDOMEvents() } private handleDOMEvents() { const { bindToWrapper, disableMouse, disableTouch, click } = this.options const wrapper = this.wrapper const target = bindToWrapper ? wrapper : window const wrapperEvents = [] const targetEvents = [] const shouldRegisterTouch = !disableTouch const shouldRegisterMouse = !disableMouse if (click) { wrapperEvents.push({ name: 'click', handler: this.click.bind(this), capture: true, }) } if (shouldRegisterTouch) { wrapperEvents.push({ name: 'touchstart', handler: this.start.bind(this), }) targetEvents.push( { name: 'touchmove', handler: this.move.bind(this), }, { name: 'touchend', handler: this.end.bind(this), }, { name: 'touchcancel', handler: this.end.bind(this), } ) } if (shouldRegisterMouse) { wrapperEvents.push({ name: 'mousedown', handler: this.start.bind(this), }) targetEvents.push( { name: 'mousemove', handler: this.move.bind(this), }, { name: 'mouseup', handler: this.end.bind(this), } ) } this.wrapperEventRegister = new EventRegister(wrapper, wrapperEvents) this.targetEventRegister = new EventRegister(target, targetEvents) } private beforeHandler(e: TouchEvent, type: 'start' | 'move' | 'end') { const { preventDefault, stopPropagation, preventDefaultException, } = this.options const preventDefaultConditions = { start: () => { return ( preventDefault && !preventDefaultExceptionFn(e.target, preventDefaultException) ) }, end: () => { return ( preventDefault && !preventDefaultExceptionFn(e.target, preventDefaultException) ) }, move: () => { return preventDefault }, } if (preventDefaultConditions[type]()) { e.preventDefault() } if (stopPropagation) { e.stopPropagation() } } setInitiated(type: number = 0) { this.initiated = type } private start(e: TouchEvent) { const _eventType = eventTypeMap[e.type] if (this.initiated && this.initiated !== _eventType) { return } this.setInitiated(_eventType) // if textarea or other html tags in options.tagException is manipulated // do not make bs scroll if (tagExceptionFn(e.target, this.options.tagException)) { this.setInitiated() return } // only allow mouse left button if (_eventType === EventType.Mouse && e.button !== MouseButton.Left) return if (this.hooks.trigger(this.hooks.eventTypes.beforeStart, e)) { return } this.beforeHandler(e, 'start') let point = (e.touches ? e.touches[0] : e) as Touch this.pointX = point.pageX this.pointY = point.pageY this.hooks.trigger(this.hooks.eventTypes.start, e) } private move(e: TouchEvent) { if (eventTypeMap[e.type] !== this.initiated) { return } this.beforeHandler(e, 'move') let point = (e.touches ? e.touches[0] : e) as Touch let deltaX = point.pageX - this.pointX let deltaY = point.pageY - this.pointY this.pointX = point.pageX this.pointY = point.pageY if ( this.hooks.trigger(this.hooks.eventTypes.move, { deltaX, deltaY, e, }) ) { return } // auto end when out of viewport let scrollLeft = document.documentElement.scrollLeft || window.pageXOffset || document.body.scrollLeft let scrollTop = document.documentElement.scrollTop || window.pageYOffset || document.body.scrollTop let pX = this.pointX - scrollLeft let pY = this.pointY - scrollTop const autoEndDistance = this.options.autoEndDistance if ( pX > document.documentElement.clientWidth - autoEndDistance || pY > document.documentElement.clientHeight - autoEndDistance || pX < autoEndDistance || pY < autoEndDistance ) { this.end(e) } } private end(e: TouchEvent) { if (eventTypeMap[e.type] !== this.initiated) { return } this.setInitiated() this.beforeHandler(e, 'end') this.hooks.trigger(this.hooks.eventTypes.end, e) } private click(e: TouchEvent) { this.hooks.trigger(this.hooks.eventTypes.click, e) } setContent(content: HTMLElement) { if (content !== this.wrapper) { this.wrapper = content this.rebindDOMEvents() } } rebindDOMEvents() { this.wrapperEventRegister.destroy() this.targetEventRegister.destroy() this.handleDOMEvents() } destroy() { this.wrapperEventRegister.destroy() this.targetEventRegister.destroy() this.hooks.destroy() } } ================================================ FILE: packages/core/src/base/__mocks__/ActionsHandler.ts ================================================ import { EventRegister, EventEmitter } from '@better-scroll/shared-utils' const ActionsHandler = jest .fn() .mockImplementation((wrapper, bscrollOptions) => { return { wrapper, options: bscrollOptions, initiated: 1, pointX: 0, pointY: 0, startClickRegister: new EventRegister(wrapper, []), moveEndRegister: new EventRegister(wrapper, []), hooks: new EventEmitter(['beforeStart', 'start', 'move', 'end', 'click']), destroy: jest.fn(), setInitiated: jest.fn(), setContent: jest.fn(), rebindDOMEvents: jest.fn(), } }) export default ActionsHandler ================================================ FILE: packages/core/src/base/__tests__/ActionsHandler.spec.ts ================================================ import ActionsHandler, { Options, } from '@better-scroll/core/src/base/ActionsHandler' import { dispatchTouch, dispatchMouse, dispatchTouchStart, dispatchTouchEnd, dispatchTouchCancel, } from '@better-scroll/core/src/__tests__/__utils__/event' describe('ActionsHandler', () => { let actionsHandler: ActionsHandler let wrapper: HTMLElement let options: Options beforeEach(() => { wrapper = document.createElement('wrapper') options = { click: false, bindToWrapper: false, disableMouse: false, disableTouch: false, preventDefault: true, stopPropagation: true, preventDefaultException: { tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT|AUDIO)$/, }, tagException: { tagName: /^TEXTAREA$/ }, autoEndDistance: 5, } }) afterEach(() => { jest.clearAllMocks() }) it('should bind mouse event when options.disableMouse is false', () => { options.disableTouch = true actionsHandler = new ActionsHandler(wrapper, options) const wrapperEventsName = actionsHandler.wrapperEventRegister.events.map( (event) => event.name ) const targetEventsName = actionsHandler.targetEventRegister.events.map( (event) => event.name ) expect(wrapperEventsName).toMatchObject(['mousedown']) expect(targetEventsName).toMatchObject(['mousemove', 'mouseup']) }) it('should invoke start method when dispatch mousedown', () => { actionsHandler = new ActionsHandler(wrapper, options) const beforeStartMockHandler = jest.fn().mockImplementation(() => { return 'dummy test' }) const startMockHandler = jest.fn().mockImplementation(() => { return 'dummy test' }) actionsHandler.hooks.on('beforeStart', beforeStartMockHandler) actionsHandler.hooks.on('start', startMockHandler) dispatchMouse(wrapper, 'mousedown') expect(beforeStartMockHandler).toBeCalledTimes(1) expect(startMockHandler).toBeCalledTimes(1) // return early actionsHandler.setInitiated(1) dispatchMouse(wrapper, 'mousedown') expect(beforeStartMockHandler).toBeCalledTimes(1) expect(startMockHandler).toBeCalledTimes(1) // only allow mouse left button actionsHandler.setInitiated(0) dispatchMouse(wrapper, 'mousedown', false) expect(beforeStartMockHandler).toBeCalledTimes(1) expect(startMockHandler).toBeCalledTimes(1) // cancelable beforeStart hook actionsHandler.hooks.on('beforeStart', () => true) dispatchMouse(wrapper, 'mousedown') expect(beforeStartMockHandler).toBeCalledTimes(2) expect(startMockHandler).toBeCalledTimes(1) }) it('should invoke move method when dispatch touchmove', () => { actionsHandler = new ActionsHandler(wrapper, options) const moveMockHandler1 = jest.fn().mockImplementationOnce(() => { return true }) const moveMockHandler2 = jest.fn().mockImplementation(() => { return 'dummy test' }) actionsHandler.hooks.on('move', moveMockHandler1) actionsHandler.hooks.on('move', moveMockHandler2) dispatchMouse(wrapper, 'mousedown') dispatchMouse(window, 'mousemove') expect(moveMockHandler1).toBeCalledTimes(1) // cancelable move hook expect(moveMockHandler2).not.toBeCalled() // simulate finger moved out of viewport actionsHandler.pointX = 5 const endMockHandler = jest.fn() actionsHandler.hooks.on(actionsHandler.hooks.eventTypes.end, endMockHandler) dispatchMouse(window, 'mousemove') expect(endMockHandler).toBeCalled() }) it('should invoke end method when dispatch touchend', () => { actionsHandler = new ActionsHandler(wrapper, options) const endMockHandler = jest.fn().mockImplementation(() => { return 'dummy test' }) actionsHandler.hooks.on('end', endMockHandler) dispatchTouchStart(wrapper, [{ pageX: 0, pageY: 0 }]) dispatchTouchEnd(window, [{ pageX: 0, pageY: 0 }]) expect(endMockHandler).toBeCalled() }) it('should invoke end method when dispatch touchcancel', () => { actionsHandler = new ActionsHandler(wrapper, options) const endMockHandler = jest.fn().mockImplementation(() => { return 'dummy test' }) actionsHandler.hooks.on('end', endMockHandler) dispatchTouchStart(wrapper, [{ pageX: 0, pageY: 0 }]) dispatchTouchCancel(window, [{ pageX: 0, pageY: 0 }]) expect(endMockHandler).toBeCalled() }) it('should call click method when dispatch click', () => { options.click = true actionsHandler = new ActionsHandler(wrapper, options) const clickMockHandler = jest.fn().mockImplementation(() => { return 'dummy test' }) actionsHandler.hooks.on('click', clickMockHandler) dispatchTouch(wrapper, 'click', [ { pageX: 10, pageY: 10, }, ]) expect(clickMockHandler).toBeCalled() }) it('should make bs not take effect when manipulate textarea DOM tag', () => { const textarea = document.createElement('textarea') const content = document.createElement('div') content.appendChild(textarea) wrapper.appendChild(content) actionsHandler = new ActionsHandler(wrapper, options) dispatchMouse(textarea, 'mousedown') expect(actionsHandler.initiated).toBeFalsy() }) it('destroy()', () => { actionsHandler = new ActionsHandler(wrapper, options) actionsHandler.destroy() expect(actionsHandler.wrapperEventRegister.events.length).toBe(0) expect(actionsHandler.targetEventRegister.events.length).toBe(0) expect(actionsHandler.hooks.eventTypes).toMatchObject({}) expect(actionsHandler.hooks.events).toMatchObject({}) }) it('setContent()', () => { const p = document.createElement('p') actionsHandler.setContent(p) expect(actionsHandler.wrapper).toBe(p) }) }) ================================================ FILE: packages/core/src/index.ts ================================================ import { BScroll } from './BScroll' export { BScrollInstance } from './Instance' export { Options, CustomOptions } from './Options' export { TranslaterPoint } from './translater' export { MountedBScrollHTMLElement } from './BScroll' export { Behavior, Boundary } from './scroller/Behavior' export { createBScroll, CustomAPI } from './BScroll' export default BScroll ================================================ FILE: packages/core/src/scroller/Actions.ts ================================================ import ActionsHandler from '../base/ActionsHandler' import { Behavior } from './Behavior' import DirectionLockAction from './DirectionLock' import { Animater } from '../animater' import { OptionsConstructor as BScrollOptions } from '../Options' import { TranslaterPoint } from '../translater' import { preventDefaultExceptionFn, TouchEvent, getNow, Probe, EventEmitter, between, Quadrant, maybePrevent, } from '@better-scroll/shared-utils' const applyQuadrantTransformation = ( deltaX: number, deltaY: number, quadrant: Quadrant ) => { if (quadrant === Quadrant.Second) { return [deltaY, -deltaX] } else if (quadrant === Quadrant.Third) { return [-deltaX, -deltaY] } else if (quadrant === Quadrant.Forth) { return [-deltaY, deltaX] } else { return [deltaX, deltaY] } } export default class ScrollerActions { hooks: EventEmitter scrollBehaviorX: Behavior scrollBehaviorY: Behavior actionsHandler: ActionsHandler animater: Animater options: BScrollOptions directionLockAction: DirectionLockAction fingerMoved: boolean contentMoved: boolean enabled: boolean startTime: number endTime: number ensuringInteger: boolean constructor( scrollBehaviorX: Behavior, scrollBehaviorY: Behavior, actionsHandler: ActionsHandler, animater: Animater, options: BScrollOptions ) { this.hooks = new EventEmitter([ 'start', 'beforeMove', 'scrollStart', 'scroll', 'beforeEnd', 'end', 'scrollEnd', 'contentNotMoved', 'detectMovingDirection', 'coordinateTransformation', ]) this.scrollBehaviorX = scrollBehaviorX this.scrollBehaviorY = scrollBehaviorY this.actionsHandler = actionsHandler this.animater = animater this.options = options this.directionLockAction = new DirectionLockAction( options.directionLockThreshold, options.freeScroll, options.eventPassthrough ) this.enabled = true this.bindActionsHandler() } private bindActionsHandler() { // [mouse|touch]start event this.actionsHandler.hooks.on( this.actionsHandler.hooks.eventTypes.start, (e: TouchEvent) => { if (!this.enabled) return true return this.handleStart(e) } ) // [mouse|touch]move event this.actionsHandler.hooks.on( this.actionsHandler.hooks.eventTypes.move, ({ deltaX, deltaY, e, }: { deltaX: number deltaY: number e: TouchEvent }) => { if (!this.enabled) return true const [transformateDeltaX, transformateDeltaY] = applyQuadrantTransformation(deltaX, deltaY, this.options.quadrant) const transformateDeltaData = { deltaX: transformateDeltaX, deltaY: transformateDeltaY, } this.hooks.trigger( this.hooks.eventTypes.coordinateTransformation, transformateDeltaData ) return this.handleMove( transformateDeltaData.deltaX, transformateDeltaData.deltaY, e ) } ) // [mouse|touch]end event this.actionsHandler.hooks.on( this.actionsHandler.hooks.eventTypes.end, (e: TouchEvent) => { if (!this.enabled) return true return this.handleEnd(e) } ) // click this.actionsHandler.hooks.on( this.actionsHandler.hooks.eventTypes.click, (e: TouchEvent) => { // handle native click event if (this.enabled && !e._constructed) { this.handleClick(e) } } ) } private handleStart(e: TouchEvent) { const timestamp = getNow() this.fingerMoved = false this.contentMoved = false this.startTime = timestamp this.directionLockAction.reset() this.scrollBehaviorX.start() this.scrollBehaviorY.start() // force stopping last transition or animation this.animater.doStop() this.scrollBehaviorX.resetStartPos() this.scrollBehaviorY.resetStartPos() this.hooks.trigger(this.hooks.eventTypes.start, e) } private handleMove(deltaX: number, deltaY: number, e: TouchEvent) { if (this.hooks.trigger(this.hooks.eventTypes.beforeMove, e)) { return } const absDistX = this.scrollBehaviorX.getAbsDist(deltaX) const absDistY = this.scrollBehaviorY.getAbsDist(deltaY) const timestamp = getNow() // We need to move at least momentumLimitDistance pixels // for the scrolling to initiate if (this.checkMomentum(absDistX, absDistY, timestamp)) { return true } if (this.directionLockAction.checkMovingDirection(absDistX, absDistY, e)) { this.actionsHandler.setInitiated() return true } const delta = this.directionLockAction.adjustDelta(deltaX, deltaY) const prevX = this.scrollBehaviorX.getCurrentPos() const newX = this.scrollBehaviorX.move(delta.deltaX) const prevY = this.scrollBehaviorY.getCurrentPos() const newY = this.scrollBehaviorY.move(delta.deltaY) if (this.hooks.trigger(this.hooks.eventTypes.detectMovingDirection)) { return } if (!this.fingerMoved) { this.fingerMoved = true } const positionChanged = newX !== prevX || newY !== prevY if (!this.contentMoved && !positionChanged) { this.hooks.trigger(this.hooks.eventTypes.contentNotMoved) } if (!this.contentMoved && positionChanged) { this.contentMoved = true this.hooks.trigger(this.hooks.eventTypes.scrollStart) } if (this.contentMoved && positionChanged) { this.animater.translate({ x: newX, y: newY, }) this.dispatchScroll(timestamp) } } private dispatchScroll(timestamp: number) { // dispatch scroll in interval time if (timestamp - this.startTime > this.options.momentumLimitTime) { // refresh time and starting position to initiate a momentum this.startTime = timestamp this.scrollBehaviorX.updateStartPos() this.scrollBehaviorY.updateStartPos() if (this.options.probeType === Probe.Throttle) { this.hooks.trigger(this.hooks.eventTypes.scroll, this.getCurrentPos()) } } // dispatch scroll all the time if (this.options.probeType > Probe.Throttle) { this.hooks.trigger(this.hooks.eventTypes.scroll, this.getCurrentPos()) } } private checkMomentum(absDistX: number, absDistY: number, timestamp: number) { return ( timestamp - this.endTime > this.options.momentumLimitTime && absDistY < this.options.momentumLimitDistance && absDistX < this.options.momentumLimitDistance ) } private handleEnd(e: TouchEvent) { if (this.hooks.trigger(this.hooks.eventTypes.beforeEnd, e)) { return } let currentPos = this.getCurrentPos() this.scrollBehaviorX.updateDirection() this.scrollBehaviorY.updateDirection() if (this.hooks.trigger(this.hooks.eventTypes.end, e, currentPos)) { return true } currentPos = this.ensureIntegerPos(currentPos) this.animater.translate(currentPos) this.endTime = getNow() const duration = this.endTime - this.startTime this.hooks.trigger(this.hooks.eventTypes.scrollEnd, currentPos, duration) } private ensureIntegerPos(currentPos: TranslaterPoint) { this.ensuringInteger = true let { x, y } = currentPos const { minScrollPos: minScrollPosX, maxScrollPos: maxScrollPosX } = this.scrollBehaviorX const { minScrollPos: minScrollPosY, maxScrollPos: maxScrollPosY } = this.scrollBehaviorY x = x > 0 ? Math.ceil(x) : Math.floor(x) y = y > 0 ? Math.ceil(y) : Math.floor(y) x = between(x, maxScrollPosX, minScrollPosX) y = between(y, maxScrollPosY, minScrollPosY) return { x, y } } private handleClick(e: TouchEvent) { if ( !preventDefaultExceptionFn(e.target, this.options.preventDefaultException) ) { maybePrevent(e) e.stopPropagation() } } getCurrentPos(): TranslaterPoint { return { x: this.scrollBehaviorX.getCurrentPos(), y: this.scrollBehaviorY.getCurrentPos(), } } refresh() { this.endTime = 0 } destroy() { this.hooks.destroy() } } ================================================ FILE: packages/core/src/scroller/Behavior.ts ================================================ import { getRect, Direction, EventEmitter } from '@better-scroll/shared-utils' export type Bounces = [boolean, boolean] export type Rect = { size: string; position: string } export interface Options { scrollable: boolean momentum: boolean momentumLimitTime: number momentumLimitDistance: number deceleration: number swipeBounceTime: number swipeTime: number bounces: Bounces rect: Rect outOfBoundaryDampingFactor: number specifiedIndexAsContent: number [key: string]: number | boolean | Bounces | Rect } export type Boundary = { minScrollPos: number; maxScrollPos: number } export class Behavior { content: HTMLElement currentPos: number startPos: number absStartPos: number dist: number minScrollPos: number maxScrollPos: number hasScroll: boolean direction: number movingDirection: number relativeOffset: number wrapperSize: number contentSize: number hooks: EventEmitter constructor( public wrapper: HTMLElement, content: HTMLElement, public options: Options ) { this.hooks = new EventEmitter([ 'beforeComputeBoundary', 'computeBoundary', 'momentum', 'end', 'ignoreHasScroll' ]) this.refresh(content) } start() { this.dist = 0 this.setMovingDirection(Direction.Default) this.setDirection(Direction.Default) } move(delta: number) { delta = this.hasScroll ? delta : 0 this.setMovingDirection(delta) return this.performDampingAlgorithm( delta, this.options.outOfBoundaryDampingFactor ) } setMovingDirection(delta: number) { this.movingDirection = delta > 0 ? Direction.Negative : delta < 0 ? Direction.Positive : Direction.Default } setDirection(delta: number) { this.direction = delta > 0 ? Direction.Negative : delta < 0 ? Direction.Positive : Direction.Default } performDampingAlgorithm(delta: number, dampingFactor: number): number { let newPos = this.currentPos + delta // Slow down or stop if outside of the boundaries if (newPos > this.minScrollPos || newPos < this.maxScrollPos) { if ( (newPos > this.minScrollPos && this.options.bounces[0]) || (newPos < this.maxScrollPos && this.options.bounces[1]) ) { newPos = this.currentPos + delta * dampingFactor } else { newPos = newPos > this.minScrollPos ? this.minScrollPos : this.maxScrollPos } } return newPos } end(duration: number) { let momentumInfo: { destination?: number duration?: number } = { duration: 0 } const absDist = Math.abs(this.currentPos - this.startPos) // start momentum animation if needed if ( this.options.momentum && duration < this.options.momentumLimitTime && absDist > this.options.momentumLimitDistance ) { const wrapperSize = (this.direction === Direction.Negative && this.options.bounces[0]) || (this.direction === Direction.Positive && this.options.bounces[1]) ? this.wrapperSize : 0 momentumInfo = this.hasScroll ? this.momentum( this.currentPos, this.startPos, duration, this.maxScrollPos, this.minScrollPos, wrapperSize, this.options ) : { destination: this.currentPos, duration: 0 } } else { this.hooks.trigger(this.hooks.eventTypes.end, momentumInfo) } return momentumInfo } private momentum( current: number, start: number, time: number, lowerMargin: number, upperMargin: number, wrapperSize: number, options = this.options ) { const distance = current - start const speed = Math.abs(distance) / time const { deceleration, swipeBounceTime, swipeTime } = options const duration = Math.min(swipeTime, (speed * 2) / deceleration) const momentumData = { destination: current + ((speed * speed) / deceleration) * (distance < 0 ? -1 : 1), duration, rate: 15 } this.hooks.trigger(this.hooks.eventTypes.momentum, momentumData, distance) if (momentumData.destination < lowerMargin) { momentumData.destination = wrapperSize ? Math.max( lowerMargin - wrapperSize / 4, lowerMargin - (wrapperSize / momentumData.rate) * speed ) : lowerMargin momentumData.duration = swipeBounceTime } else if (momentumData.destination > upperMargin) { momentumData.destination = wrapperSize ? Math.min( upperMargin + wrapperSize / 4, upperMargin + (wrapperSize / momentumData.rate) * speed ) : upperMargin momentumData.duration = swipeBounceTime } momentumData.destination = Math.round(momentumData.destination) return momentumData } updateDirection() { const absDist = this.currentPos - this.absStartPos this.setDirection(absDist) } refresh(content: HTMLElement) { const { size, position } = this.options.rect const isWrapperStatic = window.getComputedStyle(this.wrapper, null).position === 'static' // Force reflow const wrapperRect = getRect(this.wrapper) // use client is more fair than offset this.wrapperSize = this.wrapper[ size === 'width' ? 'clientWidth' : 'clientHeight' ] this.setContent(content) const contentRect = getRect(this.content) this.contentSize = contentRect[size] this.relativeOffset = contentRect[position] /* istanbul ignore if */ if (isWrapperStatic) { this.relativeOffset -= wrapperRect[position] } this.computeBoundary() this.setDirection(Direction.Default) } private setContent(content: HTMLElement) { if (content !== this.content) { this.content = content this.resetState() } } private resetState() { this.currentPos = 0 this.startPos = 0 this.dist = 0 this.setDirection(Direction.Default) this.setMovingDirection(Direction.Default) this.resetStartPos() } computeBoundary() { this.hooks.trigger(this.hooks.eventTypes.beforeComputeBoundary) const boundary: Boundary = { minScrollPos: 0, maxScrollPos: this.wrapperSize - this.contentSize } if (boundary.maxScrollPos < 0) { boundary.maxScrollPos -= this.relativeOffset if (this.options.specifiedIndexAsContent === 0) { boundary.minScrollPos = -this.relativeOffset } } this.hooks.trigger(this.hooks.eventTypes.computeBoundary, boundary) this.minScrollPos = boundary.minScrollPos this.maxScrollPos = boundary.maxScrollPos this.hasScroll = this.options.scrollable && this.maxScrollPos < this.minScrollPos if (!this.hasScroll && this.minScrollPos < this.maxScrollPos) { this.maxScrollPos = this.minScrollPos this.contentSize = this.wrapperSize } } updatePosition(pos: number) { this.currentPos = pos } getCurrentPos() { return this.currentPos } checkInBoundary() { const position = this.adjustPosition(this.currentPos) const inBoundary = position === this.getCurrentPos() return { position, inBoundary } } // adjust position when out of boundary adjustPosition(pos: number) { if ( !this.hasScroll && !this.hooks.trigger(this.hooks.eventTypes.ignoreHasScroll) ) { pos = this.minScrollPos } else if (pos > this.minScrollPos) { pos = this.minScrollPos } else if (pos < this.maxScrollPos) { pos = this.maxScrollPos } return pos } updateStartPos() { this.startPos = this.currentPos } updateAbsStartPos() { this.absStartPos = this.currentPos } resetStartPos() { this.updateStartPos() this.updateAbsStartPos() } getAbsDist(delta: number) { this.dist += delta return Math.abs(this.dist) } destroy() { this.hooks.destroy() } } ================================================ FILE: packages/core/src/scroller/DirectionLock.ts ================================================ import { TouchEvent, DirectionLock, EventPassthrough, maybePrevent, } from '@better-scroll/shared-utils' const enum Passthrough { Yes = 'yes', No = 'no', } interface DirectionMap { [key: string]: { [key: string]: EventPassthrough } } const PassthroughHandlers = { [Passthrough.Yes]: (e: TouchEvent) => { return true }, [Passthrough.No]: (e: TouchEvent) => { maybePrevent(e) return false }, } const DirectionMap: DirectionMap = { [DirectionLock.Horizontal]: { [Passthrough.Yes]: EventPassthrough.Horizontal, [Passthrough.No]: EventPassthrough.Vertical, }, [DirectionLock.Vertical]: { [Passthrough.Yes]: EventPassthrough.Vertical, [Passthrough.No]: EventPassthrough.Horizontal, }, } export default class DirectionLockAction { directionLocked: DirectionLock constructor( public directionLockThreshold: number, public freeScroll: boolean, public eventPassthrough: string ) { this.reset() } reset() { this.directionLocked = DirectionLock.Default } checkMovingDirection(absDistX: number, absDistY: number, e: TouchEvent) { this.computeDirectionLock(absDistX, absDistY) return this.handleEventPassthrough(e) } adjustDelta(deltaX: number, deltaY: number) { if (this.directionLocked === DirectionLock.Horizontal) { deltaY = 0 } else if (this.directionLocked === DirectionLock.Vertical) { deltaX = 0 } return { deltaX, deltaY, } } private computeDirectionLock(absDistX: number, absDistY: number) { // If you are scrolling in one direction, lock it if (this.directionLocked === DirectionLock.Default && !this.freeScroll) { if (absDistX > absDistY + this.directionLockThreshold) { this.directionLocked = DirectionLock.Horizontal // lock horizontally } else if (absDistY >= absDistX + this.directionLockThreshold) { this.directionLocked = DirectionLock.Vertical // lock vertically } else { this.directionLocked = DirectionLock.None // no lock } } } private handleEventPassthrough(e: TouchEvent) { const handleMap = DirectionMap[this.directionLocked] if (handleMap) { if (this.eventPassthrough === handleMap[Passthrough.Yes]) { return PassthroughHandlers[Passthrough.Yes](e) } else if (this.eventPassthrough === handleMap[Passthrough.No]) { return PassthroughHandlers[Passthrough.No](e) } } return false } } ================================================ FILE: packages/core/src/scroller/Scroller.ts ================================================ import ActionsHandler from '../base/ActionsHandler' import Translater, { TranslaterPoint } from '../translater' import createAnimater, { Animater, Transition } from '../animater' import { OptionsConstructor as BScrollOptions } from '../Options' import { Behavior } from './Behavior' import ScrollerActions from './Actions' import { createActionsHandlerOptions, createBehaviorOptions, } from './createOptions' import { getElement, ease, offset, style, preventDefaultExceptionFn, TouchEvent, isAndroid, isIOSBadVersion, click, dblclick, tap, isUndef, getNow, cancelAnimationFrame, EaseItem, Probe, EventEmitter, EventRegister, } from '@better-scroll/shared-utils' import { bubbling } from '../utils/bubbling' import { isSamePoint } from '../utils/compare' import { MountedBScrollHTMLElement } from '../BScroll' const MIN_SCROLL_DISTANCE = 1 export interface ExposedAPI { scrollTo( x: number, y: number, time?: number, easing?: EaseItem, extraTransform?: { start: object; end: object } ): void scrollBy( deltaX: number, deltaY: number, time?: number, easing?: EaseItem ): void scrollToElement( el: HTMLElement | string, time: number, offsetX: number | boolean, offsetY: number | boolean, easing?: EaseItem ): void resetPosition(time?: number, easing?: EaseItem): boolean } export default class Scroller implements ExposedAPI { actionsHandler: ActionsHandler translater: Translater animater: Animater scrollBehaviorX: Behavior scrollBehaviorY: Behavior actions: ScrollerActions hooks: EventEmitter resizeRegister: EventRegister transitionEndRegister: EventRegister options: BScrollOptions wrapperOffset: { left: number top: number } _reflow: number resizeTimeout: number = 0 lastClickTime: number | null; [key: string]: any constructor( public wrapper: HTMLElement, public content: HTMLElement, options: BScrollOptions ) { this.hooks = new EventEmitter([ 'beforeStart', 'beforeMove', 'beforeScrollStart', 'scrollStart', 'scroll', 'beforeEnd', 'scrollEnd', 'resize', 'touchEnd', 'end', 'flick', 'scrollCancel', 'momentum', 'scrollTo', 'minDistanceScroll', 'scrollToElement', 'beforeRefresh', ]) this.options = options const { left, right, top, bottom } = this.options.bounce // direction X this.scrollBehaviorX = new Behavior( wrapper, content, createBehaviorOptions(options, 'scrollX', [left, right], { size: 'width', position: 'left', }) ) // direction Y this.scrollBehaviorY = new Behavior( wrapper, content, createBehaviorOptions(options, 'scrollY', [top, bottom], { size: 'height', position: 'top', }) ) this.translater = new Translater(this.content) this.animater = createAnimater(this.content, this.translater, this.options) this.actionsHandler = new ActionsHandler( this.options.bindToTarget ? this.content : wrapper, createActionsHandlerOptions(this.options) ) this.actions = new ScrollerActions( this.scrollBehaviorX, this.scrollBehaviorY, this.actionsHandler, this.animater, this.options ) const resizeHandler = this.resize.bind(this) this.resizeRegister = new EventRegister(window, [ { name: 'orientationchange', handler: resizeHandler, }, { name: 'resize', handler: resizeHandler, }, ]) this.registerTransitionEnd() this.init() } private init() { this.bindTranslater() this.bindAnimater() this.bindActions() // enable pointer events when scrolling ends this.hooks.on(this.hooks.eventTypes.scrollEnd, () => { this.togglePointerEvents(true) }) } private registerTransitionEnd() { this.transitionEndRegister = new EventRegister(this.content, [ { name: style.transitionEnd, handler: this.transitionEnd.bind(this), }, ]) } private bindTranslater() { const hooks = this.translater.hooks hooks.on(hooks.eventTypes.beforeTranslate, (transformStyle: string[]) => { if (this.options.translateZ) { transformStyle.push(this.options.translateZ) } }) // disable pointer events when scrolling hooks.on(hooks.eventTypes.translate, (pos: TranslaterPoint) => { const prevPos = this.getCurrentPos() this.updatePositions(pos) // scrollEnd will dispatch when scroll is force stopping in touchstart handler // so in touchend handler, don't toggle pointer-events if (this.actions.ensuringInteger === true) { this.actions.ensuringInteger = false return } // a valid translate if (pos.x !== prevPos.x || pos.y !== prevPos.y) { this.togglePointerEvents(false) } }) } private bindAnimater() { // reset position this.animater.hooks.on( this.animater.hooks.eventTypes.end, (pos: TranslaterPoint) => { if (!this.resetPosition(this.options.bounceTime)) { this.animater.setPending(false) this.hooks.trigger(this.hooks.eventTypes.scrollEnd, pos) } } ) bubbling(this.animater.hooks, this.hooks, [ { source: this.animater.hooks.eventTypes.move, target: this.hooks.eventTypes.scroll, }, { source: this.animater.hooks.eventTypes.forceStop, target: this.hooks.eventTypes.scrollEnd, }, ]) } private bindActions() { const actions = this.actions bubbling(actions.hooks, this.hooks, [ { source: actions.hooks.eventTypes.start, target: this.hooks.eventTypes.beforeStart, }, { source: actions.hooks.eventTypes.start, target: this.hooks.eventTypes.beforeScrollStart, // just for event api }, { source: actions.hooks.eventTypes.beforeMove, target: this.hooks.eventTypes.beforeMove, }, { source: actions.hooks.eventTypes.scrollStart, target: this.hooks.eventTypes.scrollStart, }, { source: actions.hooks.eventTypes.scroll, target: this.hooks.eventTypes.scroll, }, { source: actions.hooks.eventTypes.beforeEnd, target: this.hooks.eventTypes.beforeEnd, }, ]) actions.hooks.on( actions.hooks.eventTypes.end, (e: TouchEvent, pos: TranslaterPoint) => { this.hooks.trigger(this.hooks.eventTypes.touchEnd, pos) if (this.hooks.trigger(this.hooks.eventTypes.end, pos)) { return true } // check if it is a click operation if (!actions.fingerMoved) { this.hooks.trigger(this.hooks.eventTypes.scrollCancel) if (this.checkClick(e)) { return true } } // reset if we are outside of the boundaries if (this.resetPosition(this.options.bounceTime, ease.bounce)) { this.animater.setForceStopped(false) return true } } ) actions.hooks.on( actions.hooks.eventTypes.scrollEnd, (pos: TranslaterPoint, duration: number) => { const deltaX = Math.abs(pos.x - this.scrollBehaviorX.startPos) const deltaY = Math.abs(pos.y - this.scrollBehaviorY.startPos) if (this.checkFlick(duration, deltaX, deltaY)) { this.animater.setForceStopped(false) this.hooks.trigger(this.hooks.eventTypes.flick) return } if (this.momentum(pos, duration)) { this.animater.setForceStopped(false) return } if (actions.contentMoved) { this.hooks.trigger(this.hooks.eventTypes.scrollEnd, pos) } if (this.animater.forceStopped) { this.animater.setForceStopped(false) } } ) } private checkFlick(duration: number, deltaX: number, deltaY: number) { const flickMinMovingDistance = 1 // distinguish flick from click if ( this.hooks.events.flick.length > 1 && duration < this.options.flickLimitTime && deltaX < this.options.flickLimitDistance && deltaY < this.options.flickLimitDistance && (deltaY > flickMinMovingDistance || deltaX > flickMinMovingDistance) ) { return true } } private momentum(pos: TranslaterPoint, duration: number) { const meta = { time: 0, easing: ease.swiper, newX: pos.x, newY: pos.y, } // start momentum animation if needed const momentumX = this.scrollBehaviorX.end(duration) const momentumY = this.scrollBehaviorY.end(duration) meta.newX = isUndef(momentumX.destination) ? meta.newX : (momentumX.destination as number) meta.newY = isUndef(momentumY.destination) ? meta.newY : (momentumY.destination as number) meta.time = Math.max( momentumX.duration as number, momentumY.duration as number ) this.hooks.trigger(this.hooks.eventTypes.momentum, meta, this) // when x or y changed, do momentum animation now! if (meta.newX !== pos.x || meta.newY !== pos.y) { // change easing function when scroller goes out of the boundaries if ( meta.newX > this.scrollBehaviorX.minScrollPos || meta.newX < this.scrollBehaviorX.maxScrollPos || meta.newY > this.scrollBehaviorY.minScrollPos || meta.newY < this.scrollBehaviorY.maxScrollPos ) { meta.easing = ease.swipeBounce } this.scrollTo(meta.newX, meta.newY, meta.time, meta.easing) return true } } private checkClick(e: TouchEvent) { const cancelable = { preventClick: this.animater.forceStopped, } // we scrolled less than momentumLimitDistance pixels if (this.hooks.trigger(this.hooks.eventTypes.checkClick)) { this.animater.setForceStopped(false) return true } if (!cancelable.preventClick) { const _dblclick = this.options.dblclick let dblclickTrigged = false if (_dblclick && this.lastClickTime) { const { delay = 300 } = _dblclick as any if (getNow() - this.lastClickTime < delay) { dblclickTrigged = true dblclick(e) } } if (this.options.tap) { tap(e, this.options.tap) } if ( this.options.click && !preventDefaultExceptionFn( e.target, this.options.preventDefaultException ) ) { click(e) } this.lastClickTime = dblclickTrigged ? null : getNow() return true } return false } private resize() { if (!this.actions.enabled) { return } // fix a scroll problem under Android condition /* istanbul ignore if */ if (isAndroid) { this.wrapper.scrollTop = 0 } clearTimeout(this.resizeTimeout) this.resizeTimeout = window.setTimeout(() => { this.hooks.trigger(this.hooks.eventTypes.resize) }, this.options.resizePolling) } /* istanbul ignore next */ private transitionEnd(e: TouchEvent) { if (e.target !== this.content || !this.animater.pending) { return } const animater = this.animater as Transition animater.transitionTime() if (!this.resetPosition(this.options.bounceTime, ease.bounce)) { this.animater.setPending(false) if (this.options.probeType !== Probe.Realtime) { this.hooks.trigger( this.hooks.eventTypes.scrollEnd, this.getCurrentPos() ) } } } togglePointerEvents(enabled = true) { let el = this.content.children.length ? this.content.children : [this.content] let pointerEvents = enabled ? 'auto' : 'none' for (let i = 0; i < el.length; i++) { let node = el[i] as MountedBScrollHTMLElement // ignore BetterScroll instance's wrapper DOM /* istanbul ignore if */ if (node.isBScrollContainer) { continue } node.style.pointerEvents = pointerEvents } } refresh(content: HTMLElement) { const contentChanged = this.setContent(content) this.hooks.trigger(this.hooks.eventTypes.beforeRefresh) this.scrollBehaviorX.refresh(content) this.scrollBehaviorY.refresh(content) if (contentChanged) { this.translater.setContent(content) this.animater.setContent(content) this.transitionEndRegister.destroy() this.registerTransitionEnd() if (this.options.bindToTarget) { this.actionsHandler.setContent(content) } } this.actions.refresh() this.wrapperOffset = offset(this.wrapper) } private setContent(content: HTMLElement): boolean { const contentChanged = content !== this.content if (contentChanged) { this.content = content } return contentChanged } scrollBy(deltaX: number, deltaY: number, time = 0, easing?: EaseItem) { const { x, y } = this.getCurrentPos() easing = !easing ? ease.bounce : easing deltaX += x deltaY += y this.scrollTo(deltaX, deltaY, time, easing) } scrollTo( x: number, y: number, time = 0, easing = ease.bounce, extraTransform = { start: {}, end: {}, } ) { const easingFn = this.options.useTransition ? easing.style : easing.fn const currentPos = this.getCurrentPos() const startPoint = { x: currentPos.x, y: currentPos.y, ...extraTransform.start, } const endPoint = { x, y, ...extraTransform.end, } this.hooks.trigger(this.hooks.eventTypes.scrollTo, endPoint) // it is an useless move if (isSamePoint(startPoint, endPoint)) return const deltaX = Math.abs(endPoint.x - startPoint.x) const deltaY = Math.abs(endPoint.y - startPoint.y) // considering of browser compatibility for decimal transform value // force translating immediately if (deltaX < MIN_SCROLL_DISTANCE && deltaY < MIN_SCROLL_DISTANCE) { time = 0 this.hooks.trigger(this.hooks.eventTypes.minDistanceScroll) } this.animater.move(startPoint, endPoint, time, easingFn) } scrollToElement( el: HTMLElement | string, time: number, offsetX: number | boolean, offsetY: number | boolean, easing?: EaseItem ) { const targetEle = getElement(el) const pos = offset(targetEle) const getOffset = ( offset: number | boolean, size: number, wrapperSize: number ) => { if (typeof offset === 'number') { return offset } // if offsetX/Y are true we center the element to the screen return offset ? Math.round(size / 2 - wrapperSize / 2) : 0 } offsetX = getOffset( offsetX, targetEle.offsetWidth, this.wrapper.offsetWidth ) offsetY = getOffset( offsetY, targetEle.offsetHeight, this.wrapper.offsetHeight ) const getPos = ( pos: number, wrapperPos: number, offset: number, scrollBehavior: Behavior ) => { pos -= wrapperPos pos = scrollBehavior.adjustPosition(pos - offset) return pos } pos.left = getPos( pos.left, this.wrapperOffset.left, offsetX, this.scrollBehaviorX ) pos.top = getPos( pos.top, this.wrapperOffset.top, offsetY, this.scrollBehaviorY ) if ( this.hooks.trigger(this.hooks.eventTypes.scrollToElement, targetEle, pos) ) { return } this.scrollTo(pos.left, pos.top, time, easing) } resetPosition(time = 0, easing = ease.bounce) { const { position: x, inBoundary: xInBoundary, } = this.scrollBehaviorX.checkInBoundary() const { position: y, inBoundary: yInBoundary, } = this.scrollBehaviorY.checkInBoundary() if (xInBoundary && yInBoundary) { return false } /* istanbul ignore if */ if (isIOSBadVersion) { // fix ios 13.4 bouncing // see it in issues 982 this.reflow() } // out of boundary this.scrollTo(x, y, time, easing) return true } /* istanbul ignore next */ reflow() { this._reflow = this.content.offsetHeight } updatePositions(pos: TranslaterPoint) { this.scrollBehaviorX.updatePosition(pos.x) this.scrollBehaviorY.updatePosition(pos.y) } getCurrentPos() { return this.actions.getCurrentPos() } enable() { this.actions.enabled = true } disable() { cancelAnimationFrame(this.animater.timer) this.actions.enabled = false } destroy(this: Scroller) { const keys = [ 'resizeRegister', 'transitionEndRegister', 'actionsHandler', 'actions', 'hooks', 'animater', 'translater', 'scrollBehaviorX', 'scrollBehaviorY', ] keys.forEach((key) => this[key].destroy()) } } ================================================ FILE: packages/core/src/scroller/__mocks__/Actions.ts ================================================ import DirectionLock from '../DirectionLock' jest.mock('../DirectionLock') import { EventEmitter } from '@better-scroll/shared-utils' const ScrollerActions = jest .fn() .mockImplementation( ( scrollBehaviorX, scrollBehaviorY, actionsHandler, animater, bscrollOptions ) => { const directionLockAction = new DirectionLock(0, false, '') return { options: bscrollOptions, scrollBehaviorX, scrollBehaviorY, actionsHandler, animater, directionLockAction, moved: false, enabled: true, startTime: 0, endTime: 0, ensuringInteger: false, getCurrentPos: jest.fn().mockImplementation(() => { return { x: 0, y: 0, } }), refresh: jest.fn(), destroy: jest.fn(), hooks: new EventEmitter([ 'start', 'beforeMove', 'scrollStart', 'scroll', 'beforeEnd', 'end', 'scrollEnd', 'contentNotMoved', 'detectMovingDirection', ]), } } ) export default ScrollerActions ================================================ FILE: packages/core/src/scroller/__mocks__/Behavior.ts ================================================ import { EventEmitter } from '@better-scroll/shared-utils' const Behavior = jest.fn().mockImplementation((content, bscrollOptions) => { return { content, options: bscrollOptions, startPos: 0, currentPos: 0, absStartPos: 0, dist: 0, minScrollPos: 0, maxScrollPos: 0, hasScroll: true, direction: 0, movingDirection: 0, relativeOffset: 0, wrapperSize: 0, contentSize: 0, hooks: new EventEmitter([ 'momentum', 'end', 'beforeComputeBoundary', 'computeBoundary', 'ignoreHasScroll', ]), start: jest.fn(), move: jest.fn(), end: jest.fn(), updateDirection: jest.fn(), refresh: jest.fn(), updatePosition: jest.fn(), getCurrentPos: jest.fn().mockImplementation(() => { return 0 }), checkInBoundary: jest.fn().mockImplementation(() => { return { position: 0, inBoundary: false, } }), adjustPosition: jest.fn(), updateStartPos: jest.fn(), updateAbsStartPos: jest.fn(), resetStartPos: jest.fn(), getAbsDist: jest.fn().mockImplementation((delta: number) => { return Math.abs(delta) }), destroy: jest.fn(), computeBoundary: jest.fn(), setMovingDirection: jest.fn(), setDirection: jest.fn(), performDampingAlgorithm: jest.fn(), } }) export { Behavior } ================================================ FILE: packages/core/src/scroller/__mocks__/DirectionLock.ts ================================================ const DirectionLock = jest .fn() .mockImplementation((content, bscrollOptions) => { return { directionLocked: '', directionLockThreshold: '5', eventPassthrough: '', freeScroll: false, reset: jest.fn(), checkMovingDirection: jest.fn().mockImplementation((ret = true) => { return ret }), adjustDelta: jest .fn() .mockImplementation((deltaX: number = 0, deltaY: number = 0) => { return { deltaX, deltaY, } }), } }) export default DirectionLock ================================================ FILE: packages/core/src/scroller/__mocks__/Scroller.ts ================================================ import createAnimater from '../../animater' import Translater from '../../translater' import { Behavior } from '../Behavior' import ActionsHandler from '../../base/ActionsHandler' import Actions from '../Actions' jest.mock('../../animater') jest.mock('../../translater') jest.mock('../Behavior') jest.mock('../../base/ActionsHandler') jest.mock('../Actions') import { EventEmitter, EventRegister } from '@better-scroll/shared-utils' const Scroller = jest.fn().mockImplementation((wrapper, bscrollOptions) => { const content = wrapper.children[0] const translater = new Translater(content) const animater = createAnimater(content, translater, bscrollOptions) const actionsHandler = new ActionsHandler(wrapper, bscrollOptions) const scrollBehaviorX = new Behavior( wrapper, content, Object.assign(bscrollOptions, { scrollable: bscrollOptions.scrollX }) ) const scrollBehaviorY = new Behavior( wrapper, content, Object.assign(bscrollOptions, { scrollable: bscrollOptions.scrollY }) ) const actions = new Actions( scrollBehaviorX, scrollBehaviorY, actionsHandler, animater, bscrollOptions ) return { wrapper, content, options: bscrollOptions, translater, animater, actionsHandler, actions, hooks: new EventEmitter([ 'beforeStart', 'beforeMove', 'beforeScrollStart', 'scrollStart', 'scroll', 'beforeEnd', 'scrollEnd', 'resize', 'touchEnd', 'end', 'flick', 'scrollCancel', 'momentum', 'scrollTo', 'minDistanceScroll', 'scrollToElement', 'transitionEnd', 'checkClick', 'beforeRefresh', ]), scrollBehaviorX, scrollBehaviorY, resizeRegister: new EventRegister(wrapper, []), transitionEndRegister: new EventRegister(wrapper, []), scrollTo: jest.fn(), resetPosition: jest.fn(), togglePointerEvents: jest.fn(), reflow: jest.fn(), } }) export default Scroller ================================================ FILE: packages/core/src/scroller/__tests__/Actions.spec.ts ================================================ import { Behavior } from '../Behavior' import createAnimater from '../../animater' import Translater from '../../translater' import { OptionsConstructor } from '../../Options' import ActionsHandler from '../../base/ActionsHandler' import DirectionLockAction from '../DirectionLock' jest.mock('../Behavior') jest.mock('../../animater') jest.mock('../../translater') jest.mock('../../Options') jest.mock('../../base/ActionsHandler') jest.mock('../DirectionLock') import Actions from '../Actions' describe('Actions Class tests', () => { let actions: Actions beforeEach(() => { // redefine window.performance // because we will use window.performance.timing.navigationStart // in our file('src/util/lang.ts') Object.defineProperty(window, 'performance', { get() { return undefined }, }) let content = document.createElement('div') let wrapper = document.createElement('div') let bscrollOptions = new OptionsConstructor() as any let scrollBehaviorX = new Behavior(wrapper, content, bscrollOptions) let scrollBehaviorY = new Behavior(wrapper, content, bscrollOptions) let actionsHandler = new ActionsHandler(wrapper, bscrollOptions) let translater = new Translater(content) let animater = createAnimater(content, translater, bscrollOptions) actions = new Actions( scrollBehaviorX, scrollBehaviorY, actionsHandler, animater, bscrollOptions ) }) it('should init hooks when call constructor function', () => { expect(actions.hooks.eventTypes).toHaveProperty('start') expect(actions.hooks.eventTypes).toHaveProperty('beforeMove') expect(actions.hooks.eventTypes).toHaveProperty('scroll') expect(actions.hooks.eventTypes).toHaveProperty('beforeEnd') expect(actions.hooks.eventTypes).toHaveProperty('end') expect(actions.hooks.eventTypes).toHaveProperty('scrollEnd') }) it('should invoke handleStart when actionsHandler trigger start hook', () => { actions.actionsHandler.hooks.trigger('start') expect(actions.fingerMoved).toBe(false) expect(actions.scrollBehaviorX.start).toBeCalled() expect(actions.scrollBehaviorY.start).toBeCalled() expect(actions.scrollBehaviorX.resetStartPos).toBeCalled() expect(actions.scrollBehaviorY.resetStartPos).toBeCalled() expect(actions.animater.doStop).toBeCalled() }) it('should invoke handleMove when actionsHandler trigger move hook', () => { let e = new Event('touchmove') let beforeMoveMockHandler = jest.fn() let scrollStartHandler = jest.fn() let scrollHandler = jest.fn() // cancelable beforeMove hook actions.hooks.on( actions.hooks.eventTypes.beforeMove, jest.fn().mockImplementationOnce(() => true) ) actions.hooks.on(actions.hooks.eventTypes.beforeMove, beforeMoveMockHandler) actions.hooks.on(actions.hooks.eventTypes.scrollStart, scrollStartHandler) actions.hooks.on(actions.hooks.eventTypes.scroll, scrollHandler) actions.actionsHandler.hooks.trigger('move', { deltaX: 0, deltaY: -20, e, }) expect(beforeMoveMockHandler).toHaveBeenCalledTimes(0) // moved less than 15 px actions.endTime = Date.now() - 400 actions.actionsHandler.hooks.trigger('move', { deltaX: 0, deltaY: 10, e, }) expect(beforeMoveMockHandler).toHaveBeenCalledTimes(1) expect(actions.directionLockAction.checkMovingDirection).not.toBeCalled() // lock direction actions.endTime = Date.now() actions.actionsHandler.hooks.trigger('move', { deltaX: 10, deltaY: 0, e, }) expect(beforeMoveMockHandler).toHaveBeenCalledTimes(2) expect(actions.actionsHandler.setInitiated).toBeCalled() actions.startTime = Date.now() - 400 actions.options.probeType = 1 actions.actionsHandler.hooks.trigger('move', { deltaX: 0, deltaY: -20, e, }) expect(beforeMoveMockHandler).toHaveBeenCalledTimes(3) expect(scrollStartHandler).toBeCalled() expect(scrollHandler).toBeCalledTimes(1) expect(actions.scrollBehaviorY.getAbsDist).toHaveBeenCalledWith(-20) expect(actions.scrollBehaviorY.move).toHaveBeenCalledWith(-20) actions.startTime = Date.now() actions.options.probeType = 3 actions.actionsHandler.hooks.trigger('move', { deltaX: 0, deltaY: -20, e, }) expect(scrollHandler).toBeCalledTimes(2) const cbMock = jest.fn().mockImplementationOnce(() => true) actions.fingerMoved = true actions.hooks.on(actions.hooks.eventTypes.detectMovingDirection, cbMock) actions.actionsHandler.hooks.trigger('move', { deltaX: 0, deltaY: -20, e, }) expect(cbMock).toBeCalled() expect(actions.fingerMoved).toBe(true) // content not moved const mockFn = jest.fn() actions.contentMoved = false actions.hooks.on(actions.hooks.eventTypes.contentNotMoved, mockFn) actions.startTime = Date.now() - 400 actions.scrollBehaviorX.move = jest.fn().mockImplementation(() => 0) actions.scrollBehaviorY.move = jest.fn().mockImplementation(() => 0) actions.endTime = Date.now() + 400 actions.actionsHandler.hooks.trigger('move', { deltaX: 0, deltaY: 0, e, }) expect(mockFn).toBeCalled() }) it('should invoke handleEnd when actionsHandler trigger end hook', () => { let beforeEndMockHandler = jest.fn() let endMockHandler = jest.fn() let scrollEndHandler = jest.fn() let e = new Event('touchend') // cancelable beforeEnd hook actions.hooks.on( actions.hooks.eventTypes.beforeEnd, jest.fn().mockImplementationOnce(() => true) ) // cancelable end hook actions.hooks.on( actions.hooks.eventTypes.end, jest.fn().mockImplementationOnce(() => true) ) actions.hooks.on(actions.hooks.eventTypes.beforeEnd, beforeEndMockHandler) actions.hooks.on(actions.hooks.eventTypes.end, endMockHandler) actions.hooks.on(actions.hooks.eventTypes.scrollEnd, scrollEndHandler) actions.actionsHandler.hooks.trigger( actions.actionsHandler.hooks.eventTypes.end, e ) expect(beforeEndMockHandler).not.toBeCalled() expect(actions.scrollBehaviorX.updateDirection).not.toBeCalled() actions.actionsHandler.hooks.trigger( actions.actionsHandler.hooks.eventTypes.end, e ) expect(beforeEndMockHandler).toHaveBeenCalledTimes(1) expect(actions.scrollBehaviorX.updateDirection).toHaveBeenCalledTimes(1) expect(actions.scrollBehaviorY.updateDirection).toHaveBeenCalledTimes(1) expect(endMockHandler).not.toBeCalled() actions.actionsHandler.hooks.trigger( actions.actionsHandler.hooks.eventTypes.end, e ) expect(scrollEndHandler).toBeCalled() }) it('should get correct position when invoking getCurrentPos method', () => { actions.getCurrentPos() expect(actions.scrollBehaviorX.getCurrentPos).toBeCalled() expect(actions.scrollBehaviorY.getCurrentPos).toBeCalled() }) it('should reset endTime when refreshed', () => { actions.refresh() expect(actions.endTime).toBe(0) }) it('destroy()', () => { actions.destroy() expect(actions.hooks.events).toEqual({}) expect(actions.hooks.eventTypes).toEqual({}) }) it('should can be disabled', () => { actions.enabled = false const actionsHandler = actions.actionsHandler actionsHandler.hooks.trigger(actionsHandler.hooks.eventTypes.start) expect(actions.directionLockAction.reset).not.toBeCalled() actionsHandler.hooks.trigger(actionsHandler.hooks.eventTypes.move, { deltaX: 0, deltaY: 0, }) expect(actions.scrollBehaviorX.getAbsDist).not.toBeCalled() actionsHandler.hooks.trigger(actionsHandler.hooks.eventTypes.end) expect(actions.scrollBehaviorX.updateDirection).not.toBeCalled() }) it('should prevent native click event', () => { actions.actionsHandler.hooks.trigger( actions.actionsHandler.hooks.eventTypes.click, { _constructed: false, target: document.createElement('div'), preventDefault() {}, stopPropagation() {}, } ) }) it('apply quadrant transformation when force rotating by CSS', () => { let e = new Event('touchmove') // second quadrant actions.options.quadrant = 2 actions.actionsHandler.hooks.trigger('move', { deltaX: 0, deltaY: -20, e, }) expect(actions.scrollBehaviorX.getAbsDist).toBeCalledWith(-20) expect(actions.scrollBehaviorY.getAbsDist).toBeCalledWith(-0) // third quadrant actions.options.quadrant = 3 actions.actionsHandler.hooks.trigger('move', { deltaX: -20, deltaY: 0, e, }) expect(actions.scrollBehaviorX.getAbsDist).toBeCalledWith(20) expect(actions.scrollBehaviorY.getAbsDist).toBeCalledWith(-0) // forth quadrant actions.options.quadrant = 4 actions.actionsHandler.hooks.trigger('move', { deltaX: 20, deltaY: 0, e, }) expect(actions.scrollBehaviorX.getAbsDist).toBeCalledWith(-0) expect(actions.scrollBehaviorY.getAbsDist).toBeCalledWith(20) }) it('coordinateTransformation hook', () => { let e = new Event('touchmove') const mockFn = jest.fn() actions.hooks.on(actions.hooks.eventTypes.coordinateTransformation, mockFn) actions.actionsHandler.hooks.trigger('move', { deltaX: 0, deltaY: -20, e, }) expect(mockFn).toBeCalled() }) }) ================================================ FILE: packages/core/src/scroller/__tests__/Behavior.spec.ts ================================================ import { Behavior } from '../Behavior' import { createDiv } from '../../__tests__/__utils__/layout' describe('Behavior Class tests', () => { let behavior: Behavior let content: HTMLElement let wrapper: HTMLElement let options = { movable: false, scrollable: true, momentum: true, momentumLimitTime: 300, momentumLimitDistance: 15, deceleration: 0.001, swipeBounceTime: 2500, outOfBoundaryDampingFactor: 1 / 3, specifiedIndexAsContent: 0, swipeTime: 2000, bounces: [true, true] as [boolean, boolean], rect: { size: 'height', position: 'top', }, } beforeEach(() => { wrapper = createDiv(100, 200, 0, 0) content = createDiv(100, 400, 0, 0) document.body.appendChild(content) wrapper.appendChild(content) behavior = new Behavior(wrapper, content, options) }) it('should init hooks when call constructor function', () => { expect(behavior.hooks.eventTypes).toHaveProperty('momentum') expect(behavior.hooks.eventTypes).toHaveProperty('end') expect(behavior.currentPos).toBe(0) expect(behavior.startPos).toBe(0) expect(behavior.content).toEqual(content) }) it('should refresh some properties when invoking refresh method', () => { behavior.refresh(behavior.content) expect(behavior.wrapperSize).toBe(200) expect(behavior.contentSize).toBe(400) expect(behavior.relativeOffset).toBe(0) expect(behavior.minScrollPos).toBe(-0) expect(behavior.maxScrollPos).toBe(-200) expect(behavior.hasScroll).toBe(true) expect(behavior.direction).toBe(0) }) it('should refresh some properties when invoking start method', () => { behavior.start() expect(behavior.direction).toBe(0) expect(behavior.movingDirection).toBe(0) expect(behavior.dist).toBe(0) }) it('should refresh some properties when invoking move method', () => { behavior.refresh(behavior.content) expect(behavior.move(-10)).toBe(-10) expect(behavior.movingDirection).toBe(1) }) it('should not trigger momentum scroll when duration is exceed momentumLimitTime', () => { let endMockHandler = jest.fn() behavior.hooks.on('end', endMockHandler) behavior.refresh(behavior.content) behavior.end(400) expect(endMockHandler).toBeCalled() expect(endMockHandler).toHaveBeenCalledWith({ duration: 0, }) }) it('should trigger momentum scroll', () => { behavior.refresh(behavior.content) behavior.currentPos = -100 expect(behavior.end(100)).toEqual({ destination: -200, duration: 2500, rate: 15, }) behavior.hooks.on( behavior.hooks.eventTypes.momentum, (momentumData: any) => { momentumData.destination = 200 } ) expect(behavior.end(100)).toEqual({ destination: -0, duration: 2500, rate: 15, }) }) it('should keep direction unchanged when invoking updateDirection method', () => { behavior.updateDirection() expect(behavior.direction).toBe(0) }) it('should update position when invoking updatePosition method', () => { behavior.updatePosition(100) expect(behavior.currentPos).toBe(100) }) it('should auto bouncing within boundary when out of boundary', () => { behavior.refresh(behavior.content) behavior.updatePosition(-400) expect(behavior.checkInBoundary()).toEqual({ position: -200, inBoundary: false, }) }) it('performDampingAlgorithm()', () => { const ret = behavior.performDampingAlgorithm(20, 0.1) expect(ret).toBe(2) behavior.options.bounces = [false, false] // simulate out of the boundaries and no bounce const ret2 = behavior.performDampingAlgorithm(20, 0.1) expect(ret2).toBe(-0) }) it('getAbsDist()', () => { const ret = behavior.getAbsDist(-20) expect(ret).toBe(20) }) it('adjustPosition()', () => { const ret = behavior.adjustPosition(20.1) expect(ret).toBe(-0) }) it('computeBoundary()', () => { behavior.hooks.on( behavior.hooks.eventTypes.computeBoundary, (boundary: { minScrollPos: number; maxScrollPos: number }) => { boundary.minScrollPos = 20 boundary.maxScrollPos = 30 } ) behavior.computeBoundary() expect(behavior.maxScrollPos).toEqual(behavior.minScrollPos) }) }) ================================================ FILE: packages/core/src/scroller/__tests__/DirectionLock.spec.ts ================================================ import DirectionLock from '../DirectionLock' import { Direction, DirectionLock as DirectionLockEnum, } from '@better-scroll/shared-utils' describe('DirectionLock Class tests', () => { let directionLock: DirectionLock let e = new Event('touchstart') as any beforeEach(() => { directionLock = new DirectionLock(5, false, '') }) it('should call reset when call constructor function', () => { expect(directionLock.directionLocked).toBe('') }) it('should lock vertically when scrolled in direction Y', () => { directionLock.checkMovingDirection(0, 20, e) expect(directionLock.directionLocked).toBe('vertical') }) it('should lock horizontally when scrolled in direction X', () => { directionLock.checkMovingDirection(20, 0, e) expect(directionLock.directionLocked).toBe('horizontal') }) it('should no lock when freeScroll is true', () => { directionLock.freeScroll = true directionLock.checkMovingDirection(20, 20, e) expect(directionLock.directionLocked).toBe('') }) it('adjustDelta() ', () => { directionLock.directionLocked = DirectionLockEnum.Horizontal const ret1 = directionLock.adjustDelta(20, 20) expect(ret1).toMatchObject({ deltaX: 20, deltaY: 0, }) directionLock.directionLocked = DirectionLockEnum.Vertical const ret2 = directionLock.adjustDelta(20, 20) expect(ret2).toMatchObject({ deltaX: 0, deltaY: 20, }) }) it('reset()', () => { directionLock.directionLocked = DirectionLockEnum.Vertical directionLock.reset() expect(directionLock.directionLocked).toBe(DirectionLockEnum.Default) }) it('checkMovingDirection()', () => { directionLock.directionLocked = DirectionLockEnum.Horizontal directionLock.eventPassthrough = DirectionLockEnum.Horizontal const ret1 = directionLock.checkMovingDirection(20, 20, { preventDefault() { return true }, } as any) expect(ret1).toBe(true) directionLock.directionLocked = DirectionLockEnum.Horizontal directionLock.eventPassthrough = DirectionLockEnum.Vertical const ret2 = directionLock.checkMovingDirection(20, 20, { preventDefault() { return true }, } as any) expect(ret2).toBe(false) // no locked directionLock.directionLocked = DirectionLockEnum.Default const ret3 = directionLock.checkMovingDirection(20, 20, { preventDefault() { return true }, } as any) expect(ret3).toBe(false) }) }) ================================================ FILE: packages/core/src/scroller/__tests__/Scroller.spec.ts ================================================ import { Behavior } from '../Behavior' import createAnimater from '../../animater' import Translater from '../../translater' import { OptionsConstructor } from '../../Options' import ActionsHandler from '../../base/ActionsHandler' import Actions from '../Actions' jest.mock('../Behavior') jest.mock('../../animater') jest.mock('../../translater') jest.mock('../../Options') jest.mock('../../base/ActionsHandler') jest.mock('../Actions') import Scroller from '../Scroller' import { createDiv } from '../../__tests__/__utils__/layout' describe('Scroller Class tests', () => { let scroller: Scroller let wrapper: HTMLElement let content: HTMLElement beforeEach(() => { // redefine window.performance // because we will use window.performance.timing.navigationStart // in our file('src/util/lang.ts') Object.defineProperty(window, 'performance', { get() { return undefined }, }) wrapper = createDiv(100, 200, 0, 0) content = createDiv(100, 400, 0, 0) document.body.appendChild(content) wrapper.appendChild(content) let bscrollOptions = new OptionsConstructor() as any scroller = new Scroller(wrapper, content, bscrollOptions) }) it('should init hooks when call constructor function', () => { ;[ 'beforeStart', 'beforeMove', 'beforeScrollStart', 'scrollStart', 'scroll', 'beforeEnd', 'scrollEnd', 'resize', 'beforeRefresh', 'touchEnd', 'flick', 'scrollCancel', 'momentum', 'scrollTo', 'scrollToElement', 'minDistanceScroll', ].forEach((key) => { expect(scroller.hooks.eventTypes).toHaveProperty(key) }) }) describe('bindTranslater', () => { it('should bind beforeTranslate hook', () => { let transform: string[] = [] scroller.translater.hooks.trigger('beforeTranslate', transform) expect(transform).toContain(' translateZ(0)') }) it('should bind translate hook', () => { scroller.actions.getCurrentPos = jest.fn().mockImplementation(() => { return { x: 0, y: 0, } }) scroller.translater.hooks.trigger('translate', { x: 0, y: -20 }) expect(scroller.scrollBehaviorX.updatePosition).toBeCalled() expect(scroller.scrollBehaviorY.updatePosition).toBeCalled() expect(scroller.scrollBehaviorX.updatePosition).toHaveBeenCalledWith(0) expect(scroller.scrollBehaviorY.updatePosition).toHaveBeenCalledWith(-20) }) }) describe('bindAnimater', () => { it('should bind end hook ', () => { let pos = { x: 0, y: 20, } let scrollEndMockHandler = jest.fn() scroller.hooks.on('scrollEnd', scrollEndMockHandler) scroller.scrollBehaviorX.checkInBoundary = jest .fn() .mockImplementation(() => { return { position: 0, inBoundary: true, } }) scroller.scrollBehaviorY.checkInBoundary = jest .fn() .mockImplementation(() => { return { position: 0, inBoundary: true, } }) scroller.animater.hooks.trigger('end', pos) expect(scroller.animater.setPending).toHaveBeenCalledWith(false) expect(scrollEndMockHandler).toHaveBeenCalledWith({ x: 0, y: 20, }) }) it('should bubble hooks', () => { let scrollEndMockHandler = jest.fn() let scrollMockHandler = jest.fn() scroller.hooks.on('scrollEnd', scrollEndMockHandler) scroller.hooks.on('scroll', scrollMockHandler) scroller.animater.hooks.trigger('forceStop') scroller.animater.hooks.trigger('move') expect(scrollEndMockHandler).toBeCalled() expect(scrollMockHandler).toBeCalled() }) }) describe('bindActions', () => { it('bind end hook', () => { let touchEndMockHandler = jest.fn() let e = new Event('touch') as any Object.defineProperty(e, 'target', { get() { return scroller.wrapper }, }) // cancelable scroller end hook scroller.hooks.on(scroller.hooks.eventTypes.touchEnd, touchEndMockHandler) scroller.hooks.trigger(scroller.hooks.eventTypes.touchEnd, { x: 0, y: 0 }) scroller.hooks.on( scroller.hooks.eventTypes.end, jest.fn().mockImplementationOnce(() => true) ) const ret = scroller.actions.hooks.trigger( scroller.actions.hooks.eventTypes.end, e, { x: 0, y: 0 } ) expect(ret).toBe(true) expect(touchEndMockHandler).toBeCalled() expect(touchEndMockHandler).toHaveBeenCalledWith({ x: 0, y: 0, }) /* click operation */ // case 1 scroller.hooks.on( scroller.hooks.eventTypes.checkClick, jest.fn().mockImplementationOnce(() => true) ) scroller.actions.hooks.trigger(scroller.actions.hooks.eventTypes.end, e, { x: 0, y: 0, }) expect(scroller.animater.setForceStopped).toBeCalledWith(false) // case 2 dblclick const mockFn2 = jest.fn() scroller.options.dblclick = true scroller.lastClickTime = Date.now() scroller.wrapper.addEventListener('dblclick', mockFn2) scroller.actions.hooks.trigger(scroller.actions.hooks.eventTypes.end, e, { x: 0, y: 0, }) expect(mockFn2).toBeCalled() // case 3 tap const mockFn3 = jest.fn() scroller.options.tap = 'tap' scroller.wrapper.addEventListener('tap', mockFn3) scroller.actions.hooks.trigger(scroller.actions.hooks.eventTypes.end, e, { x: 0, y: 0, }) expect(mockFn3).toBeCalled() // case 4 click const mockFn4 = jest.fn() scroller.options.click = true scroller.wrapper.addEventListener('click', mockFn4) scroller.actions.hooks.trigger(scroller.actions.hooks.eventTypes.end, e, { x: 0, y: 0, }) expect(mockFn4).toBeCalled() // case 5 force stopped scroller.animater.forceStopped = true const ret2 = scroller.actions.hooks.trigger( scroller.actions.hooks.eventTypes.end, e, { x: 0, y: 0 } ) expect(ret2).toBe(true) }) it('bind scrollEnd hook', () => { let momentumMockHandler = jest.fn() let noop = (() => {}) as any scroller.hooks.on('momentum', momentumMockHandler) scroller.scrollBehaviorX.end = jest.fn().mockImplementation(() => { return { duration: 400, destination: 0, } }) scroller.scrollBehaviorY.end = jest.fn().mockImplementation(() => { return { duration: 400, destination: -20, } }) // flick const mockFn = jest.fn() scroller.hooks.events['flick'] = [noop, noop] scroller.hooks.on(scroller.hooks.eventTypes.flick, mockFn) scroller.actions.hooks.trigger( scroller.actions.hooks.eventTypes.scrollEnd, { x: 0, y: -20 }, 50 ) expect(mockFn).toBeCalled() // momentum scroller.hooks.events['flick'] = [] scroller.actions.hooks.trigger( scroller.actions.hooks.eventTypes.scrollEnd, { x: 0, y: -40 }, 50 ) expect(scroller.animater.setForceStopped).toBeCalledWith(false) // force stop from transition scroller.actions.contentMoved = false scroller.animater.forceStopped = true scroller.actions.hooks.trigger( scroller.actions.hooks.eventTypes.scrollEnd, { x: 0, y: -20 }, 50 ) expect(scroller.animater.setForceStopped).toBeCalledWith(false) const mockFn2 = jest.fn() scroller.actions.contentMoved = true scroller.hooks.on(scroller.hooks.eventTypes.scrollEnd, mockFn2) scroller.actions.hooks.trigger( scroller.actions.hooks.eventTypes.scrollEnd, { x: 0, y: -20 }, 50 ) expect(mockFn2).toBeCalledWith({ x: 0, y: -20, }) }) }) it('should invoke resize method when window is resized', () => { jest.useFakeTimers() const mockFn = jest.fn() scroller.hooks.on(scroller.hooks.eventTypes.resize, mockFn) const resizeEvent = document.createEvent('Event') resizeEvent.initEvent('resize', true, true) window.dispatchEvent(resizeEvent) jest.advanceTimersByTime(60) jest.clearAllTimers() expect(mockFn).toBeCalledTimes(1) // disable scroller scroller.actions.enabled = false resizeEvent.initEvent('resize', true, true) window.dispatchEvent(resizeEvent) expect(mockFn).toBeCalledTimes(1) }) it('should trigger scrollTo hook when invoking scrollTo method', () => { let scrollToMockHandler = jest.fn() scroller.hooks.on('scrollTo', scrollToMockHandler) scroller.actions.getCurrentPos = jest.fn().mockImplementation(() => { return { x: 0, y: 0, } }) scroller.scrollTo(0, -20, 800) expect(scrollToMockHandler).toBeCalledWith({ x: 0, y: -20, }) expect(scroller.animater.move).toBeCalledWith( { x: 0, y: 0, }, { x: 0, y: -20, }, 800, 'cubic-bezier(0.165, 0.84, 0.44, 1)' ) }) it('scrollToElement()', () => { let scrollToElementMockHandler = jest.fn() scroller.hooks.on( scroller.hooks.eventTypes.scrollToElement, scrollToElementMockHandler ) scroller.refresh(scroller.content) scroller.scrollBehaviorX.adjustPosition = jest.fn(() => { return 0 }) scroller.scrollBehaviorY.adjustPosition = jest.fn(() => { return 0 }) scroller.actions.getCurrentPos = jest.fn().mockImplementation(() => { return { x: 0, y: 0, } }) scroller.scrollToElement(content, 0, false, false) expect(scrollToElementMockHandler).toBeCalled() // to a specified position scroller.scrollToElement(content, 0, 0, 0) expect(scrollToElementMockHandler).toHaveBeenLastCalledWith(content, { left: 0, top: 0, }) const mockFn2 = jest.fn() scroller.hooks.on(scroller.hooks.eventTypes.scrollToElement, () => true) scroller.hooks.on(scroller.hooks.eventTypes.scrollToElement, mockFn2) scroller.scrollToElement(content, 0, 0, 0) expect(mockFn2).not.toBeCalled() }) it('scrollBy ', () => { const mockFn = jest.fn() scroller.hooks.on(scroller.hooks.eventTypes.scrollTo, mockFn) scroller.scrollBy(20, 20) expect(mockFn).toBeCalledWith({ x: 20, y: 20, }) }) it('enable() & disable()', () => { scroller.disable() expect(scroller.actions.enabled).toBe(false) scroller.enable() expect(scroller.actions.enabled).toBe(true) }) it('should update postions when invoking updatePositions method', () => { scroller.updatePositions({ x: 20, y: -20, }) expect(scroller.scrollBehaviorX.updatePosition).toHaveBeenCalledWith(20) expect(scroller.scrollBehaviorY.updatePosition).toHaveBeenCalledWith(-20) }) it('refresh()', () => { scroller.options.bindToTarget = true scroller.refresh(document.createElement('p')) expect(scroller.scrollBehaviorX.refresh).toBeCalled() expect(scroller.scrollBehaviorY.refresh).toBeCalled() expect(scroller.actions.refresh).toBeCalled() expect(scroller.actionsHandler.setContent).toBeCalled() }) it('destroy()', () => { scroller.destroy() const keys = [ 'actionsHandler', 'actions', 'animater', 'translater', 'scrollBehaviorX', 'scrollBehaviorY', ] keys.forEach((key) => { expect(scroller[key].destroy).toBeCalled() }) }) it('resetPosition() ', () => { const mockFn = jest.fn() scroller.hooks.on(scroller.hooks.eventTypes.scrollTo, mockFn) scroller.resetPosition() expect(mockFn).toBeCalledWith({ x: 0, y: 0, }) }) it('scrollTo()', () => { // minDistanceScroll const mockFn = jest.fn() scroller.hooks.on(scroller.hooks.eventTypes.minDistanceScroll, mockFn) scroller.scrollTo(0, 0.5, 300) }) it('should not toggle pointer-events when casting last position into integer in touchend handlers', () => { scroller.actions.ensuringInteger = true scroller.translater.hooks.trigger('translate', { x: 0, y: -20 }) expect(scroller.actions.ensuringInteger).toBe(false) }) }) ================================================ FILE: packages/core/src/scroller/__tests__/createOptions.spec.ts ================================================ import { createActionsHandlerOptions, createBehaviorOptions, } from '../createOptions' import { OptionsConstructor } from '../../Options' jest.mock('../../Options') describe('createOptions helper function tests', () => { let bsOptions: any beforeEach(() => { bsOptions = new OptionsConstructor() }) it('should return correct object when invoking createActionsHandlerOptions function', () => { let ret = createActionsHandlerOptions(bsOptions) expect(ret).toEqual({ click: false, bindToWrapper: false, disableMouse: true, preventDefault: true, stopPropagation: false, preventDefaultException: { tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT|AUDIO)$/, }, }) }) it('should return correct object when invoking createBehaviorOptions function', () => { let ret = createBehaviorOptions(bsOptions, 'scrollY', [true, true], { size: 'width', position: 'top', }) expect(ret).toEqual({ momentum: true, momentumLimitTime: 300, momentumLimitDistance: 15, deceleration: 0.0015, swipeBounceTime: 500, swipeTime: 2500, scrollable: true, outOfBoundaryDampingFactor: 1 / 3, specifiedIndexAsContent: 0, bounces: [true, true], rect: { size: 'width', position: 'top', }, }) }) }) ================================================ FILE: packages/core/src/scroller/createOptions.ts ================================================ import { Options as BScrollOptions } from '../Options' import { Options as ActionsHandlerOptions } from '../base/ActionsHandler' import { Options as BehaviorOptions, Bounces, Rect } from './Behavior' export function createActionsHandlerOptions(bsOptions: BScrollOptions) { const options = [ 'click', 'bindToWrapper', 'disableMouse', 'disableTouch', 'preventDefault', 'stopPropagation', 'tagException', 'preventDefaultException', 'autoEndDistance', ].reduce((prev, cur) => { prev[cur] = bsOptions[cur] return prev }, {} as ActionsHandlerOptions) return options } export function createBehaviorOptions( bsOptions: BScrollOptions, extraProp: 'scrollX' | 'scrollY', bounces: Bounces, rect: Rect ) { const options = [ 'momentum', 'momentumLimitTime', 'momentumLimitDistance', 'deceleration', 'swipeBounceTime', 'swipeTime', 'outOfBoundaryDampingFactor', 'specifiedIndexAsContent', ].reduce((prev, cur) => { prev[cur] = bsOptions[cur] return prev }, {} as BehaviorOptions) // add extra property options.scrollable = !!bsOptions[extraProp] options.bounces = bounces options.rect = rect return options } ================================================ FILE: packages/core/src/translater/__mocks__/index.ts ================================================ import { EventEmitter } from '@better-scroll/shared-utils' const Translater = jest.fn().mockImplementation((content) => { return { style: content.style, hooks: new EventEmitter(['beforeTranslate', 'translate']), getComputedPosition: jest .fn() .mockImplementation((position = { x: 0, y: 0 }) => { return position }), translate: jest.fn(), destroy: jest.fn(), setContent: jest.fn(), } }) export default Translater ================================================ FILE: packages/core/src/translater/__tests__/index.spec.ts ================================================ import Translater from '../index' describe('Translater Class test suit', () => { let translater: Translater let contentEl = document.createElement('div') beforeEach(() => { translater = new Translater(contentEl) }) afterEach(() => { jest.clearAllMocks() }) it('work well when call translate()', () => { const mockFn1 = jest.fn() const mockFn2 = jest.fn() translater.hooks.on(translater.hooks.eventTypes.beforeTranslate, mockFn1) translater.hooks.on(translater.hooks.eventTypes.translate, mockFn2) translater.translate({ x: 0, y: 0, dummy: 0 }) expect(mockFn1).toBeCalled() expect(mockFn1).toBeCalledWith(['translateX(0px)', 'translateY(0px)'], { x: 0, y: 0, dummy: 0, }) expect(mockFn2).toBeCalled() expect(mockFn2).toBeCalledWith({ x: 0, y: 0, dummy: 0 }) expect(translater.content.style.transform).toBe( 'translateX(0px) translateY(0px)' ) }) it('get correct position when call getComputedPosition()', () => { // jsDOM library's getComputedStyle is different from browser window.getComputedStyle = jest.fn().mockImplementation(() => { return { transform: 'matrix(1, 0, 0, 1, 0, 0)', } }) const { x, y } = translater.getComputedPosition() expect(x).toBe(0) expect(y).toBe(0) }) it('should clear hooks when destroyed', () => { translater.hooks.on(translater.hooks.eventTypes.beforeTranslate, () => {}) expect(translater.hooks.eventTypes.beforeTranslate).toBe('beforeTranslate') translater.destroy() expect(translater.hooks.eventTypes.beforeTranslate).toBeFalsy() }) }) ================================================ FILE: packages/core/src/translater/index.ts ================================================ import { style, safeCSSStyleDeclaration, EventEmitter, } from '@better-scroll/shared-utils' export interface TranslaterPoint { x: number y: number [key: string]: number } interface TranslaterMetaData { x: [string, string] y: [string, string] [key: string]: any } const translaterMetaData: TranslaterMetaData = { x: ['translateX', 'px'], y: ['translateY', 'px'], } export default class Translater { content: HTMLElement style: CSSStyleDeclaration hooks: EventEmitter constructor(content: HTMLElement) { this.setContent(content) this.hooks = new EventEmitter(['beforeTranslate', 'translate']) } getComputedPosition() { let cssStyle = window.getComputedStyle( this.content, null ) as safeCSSStyleDeclaration let matrix = cssStyle[style.transform].split(')')[0].split(', ') const x = +(matrix[12] || matrix[4]) || 0 const y = +(matrix[13] || matrix[5]) || 0 return { x, y, } } translate(point: TranslaterPoint) { let transformStyle = [] as string[] Object.keys(point).forEach((key) => { if (!translaterMetaData[key]) { return } const transformFnName = translaterMetaData[key][0] if (transformFnName) { const transformFnArgUnit = translaterMetaData[key][1] const transformFnArg = point[key] transformStyle.push( `${transformFnName}(${transformFnArg}${transformFnArgUnit})` ) } }) this.hooks.trigger( this.hooks.eventTypes.beforeTranslate, transformStyle, point ) this.style[style.transform as any] = transformStyle.join(' ') this.hooks.trigger(this.hooks.eventTypes.translate, point) } setContent(content: HTMLElement) { if (this.content !== content) { this.content = content this.style = content.style } } destroy() { this.hooks.destroy() } } ================================================ FILE: packages/core/src/utils/__tests__/bubbling.spec.ts ================================================ import { bubbling } from '../bubbling' import { EventEmitter } from '@better-scroll/shared-utils' describe('bubbling', () => { it('bubbling', () => { const parentHooks = new EventEmitter(['test']) const childHooks = new EventEmitter(['test']) bubbling(childHooks, parentHooks, ['test']) const handler = jest.fn(() => {}) parentHooks.on('test', handler) childHooks.trigger('test', 'dummy test') expect(handler).toBeCalledWith('dummy test') }) }) ================================================ FILE: packages/core/src/utils/bubbling.ts ================================================ import { EventEmitter } from '@better-scroll/shared-utils' interface BubblingEventMap { source: string target: string } type BubblingEventConfig = BubblingEventMap | string export function bubbling( source: EventEmitter, target: EventEmitter, events: BubblingEventConfig[] ) { events.forEach(event => { let sourceEvent: string let targetEvent: string if (typeof event === 'string') { sourceEvent = targetEvent = event } else { sourceEvent = event.source targetEvent = event.target } source.on(sourceEvent, function(...args: any[]) { return target.trigger(targetEvent, ...args) }) }) } ================================================ FILE: packages/core/src/utils/compare.ts ================================================ import { TranslaterPoint } from '../translater' export function isSamePoint( startPoint: TranslaterPoint, endPoint: TranslaterPoint ): boolean { // keys of startPoint and endPoint should be equal const keys = Object.keys(startPoint) for (let key of keys) { if (startPoint[key] !== endPoint[key]) return false } return true } ================================================ FILE: packages/core/src/utils/compat.ts ================================================ import { Direction } from '@better-scroll/shared-utils' import { TranslaterPoint } from '../translater' type Position = { x: number y: number } // iOS 13.6 - 14.x, window.getComputedStyle sometimes will get wrong transform value // when bs use transition mode // eg: translateY -100px -> -200px, when the last frame which is about to scroll to -200px // window.getComputedStyle(this.content) will calculate transformY to be -100px(startPoint) // it is weird // so we should validate position caculated by 'window.getComputedStyle' export const isValidPostion = ( startPoint: TranslaterPoint, endPoint: TranslaterPoint, currentPos: Position, prePos: Position ) => { const computeDirection = (endValue: number, startValue: number) => { const delta = endValue - startValue const direction = delta > 0 ? Direction.Negative : delta < 0 ? Direction.Positive : Direction.Default return direction } const directionX = computeDirection(endPoint.x, startPoint.x) const directionY = computeDirection(endPoint.y, startPoint.y) const deltaX = currentPos.x - prePos.x const deltaY = currentPos.y - prePos.y return directionX * deltaX <= 0 && directionY * deltaY <= 0 } ================================================ FILE: packages/core/src/utils/typesHelper.ts ================================================ export type UnionToIntersection = ( U extends any ? (k: U) => void : never ) extends (k: infer I) => void ? I : never ================================================ FILE: packages/examples/README.md ================================================ # examples BetterScroll example in different Vue scenarios. [vue demo](https://better-scroll.github.io/examples/#/) ================================================ FILE: packages/examples/build/vue-example-build.js ================================================ var ora = require('ora') var rm = require('rimraf') var path = require('path') var chalk = require('chalk') var webpack = require('webpack') var webpackConfig = require('./vue-webpack.conf.js') var spinner = ora('building for production...') spinner.start() const assetsRoot = path.join(__dirname, '../dist/vue') console.log(chalk.red(assetsRoot)) rm(assetsRoot, err => { if (err) throw err webpack(webpackConfig, function(err, stats) { spinner.stop() if (err) throw err process.stdout.write(stats.toString({ colors: true, modules: false, children: false, chunks: false, chunkModules: false }) + '\n\n') console.log(chalk.cyan(' Build complete.\n')) console.log(chalk.yellow( ' Tip: built files are meant to be served over an HTTP server.\n' + ' Opening index.html over file:// won\'t work.\n' )) }) }) ================================================ FILE: packages/examples/build/vue-webpack.conf.js ================================================ const Config = require('webpack-chain') const VueLoaderPlugin = require('vue-loader/lib/plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') const webpack = require('webpack') const HtmlWebpackPlugin = require('html-webpack-plugin') const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') const webpackBar = require('webpackbar') const CopyWebpackPlugin = require('copy-webpack-plugin') const OptimizeCSSPlugin = require('optimize-css-assets-webpack-plugin') const TerserPlugin = require('terser-webpack-plugin') const path = require('path') const fs = require('fs') const { e2e } = require('yargs').argv const execa = require('execa') const isProd = process.env.NODE_ENV === 'production' function resolve(dir) { return path.join(__dirname, '../', dir) } const webpackConfig = new Config() console.log('-----', isProd) webpackConfig .mode(isProd ? 'production' : 'development') .devtool(isProd ? 'false' : 'eval-source-map') .entry('app') .add('./vue/main.js') .end() .output .path(isProd ? path.resolve(__dirname, '../dist/vue') : undefined) .publicPath(isProd ? '/examples/' : '/') .filename(isProd ? 'static/js/[name].[chunkhash].js' : '[name].js') .chunkFilename(isProd ? 'static/js/[id].[chunkhash].js' : '[name].js') .end() .resolve .alias .set('vue-example', resolve('vue')) .set('common', resolve('common')) .end() .extensions .add('.js') .add('.vue') .add('.json') .add('.ts') .end() .end() .module .rule('compileVue') .test(/\.vue$/) .use('vue') .loader('vue-loader') .end() .end() .rule('transformJs') .test(/\.js$/) .use('babel') .loader('babel-loader') .options({ presets: ["@babel/preset-env"] }) .end() .include .add(resolve('common')) .add(resolve('vue')) .end() .end() .rule('url') .test(/\.(png|jpe?g|gif|svg|webp)(\?.*)?$/) .use('url') .loader('url-loader') .options({ esModule: false, limit: 10000, name: 'static/img/[name].[hash:7].[ext]' }) .end() .end() .rule('ts') .test(/\.ts$/) .use('ts') .loader('ts-loader') .options({ transpileOnly: true }) .end() .end() .rule('css') .test(/\.css$/) .use('style-loader') .loader( process.env.NODE_ENV !== 'production' ? 'vue-style-loader' : MiniCssExtractPlugin.loader ) .end() .use('css-loader') .loader('css-loader') .end() .end() .rule('stylus') .test(/.styl(us)?$/) .use('style-loader') .loader( process.env.NODE_ENV !== 'production' ? 'vue-style-loader' : MiniCssExtractPlugin.loader ) .end() .use('css-loader') .loader('css-loader') .end() .use('postcss-loader') .loader('postcss-loader') .end() .use('stylus-loader') .loader('stylus-loader') .end() .end() .end() .plugin('VueLoaderPlugin') .use(VueLoaderPlugin) .end() .plugin('WebpackBar') .use(webpackBar) .end() .plugin('MiniCssExtractPlugin') .use(MiniCssExtractPlugin, [{ filename: 'static/css/[name].[contenthash].css' }]) .end() .when(!isProd, () => { webpackConfig .devServer .open(true) .hot(true) .compress(true) .end() .plugin('HotModuleReplacementPlugin') .use(webpack.HotModuleReplacementPlugin) .end() .plugin('NoEmitOnErrorsPlugin') .use(webpack.NoEmitOnErrorsPlugin) .end() .plugin('HtmlWebpackPlugin') .use(HtmlWebpackPlugin, [{ filename: 'index.html', template: './vue/index.html', inject: true }]) .end() .plugin('FriendlyErrorsPlugin') .use(FriendlyErrorsPlugin) .end() }, () => { webpackConfig .optimization .minimizer('TerserPlugin') .use(TerserPlugin) .end() .end() .plugin('OptimizeCSSPlugin') .use(OptimizeCSSPlugin, [{ cssProcessorOptions: { safe: true } }]) .end() .plugin('HtmlWebpackPlugin') .use(HtmlWebpackPlugin, [{ filename: 'index.html', template: './vue/index.html', inject: true, minify: { removeComments: true, collapseWhitespace: true, removeAttributeQuotes: true // more options: // https://github.com/kangax/html-minifier#options-quick-reference }, // necessary to consistently work with multiple chunks via CommonsChunkPlugin chunksSortMode: 'dependency' }]) .end() .plugin('CopyWebpackPlugin') .use(CopyWebpackPlugin, [[{ from: path.resolve(__dirname, '../static'), to: 'static', ignore: ['.*'] }]]) .end() }) function getPackagesName() { let ret let all = fs.readdirSync(resolve('../../packages')) // drop hidden file whose name is startWidth '.' // drop packages which would not be published(eg: examples and vuepress-docs) ret = all .filter(name => { const isHiddenFile = /^\./g.test(name) return !isHiddenFile }) .filter(name => { const isPrivatePackages = require(resolve(`../../packages/${name}/package.json`)).private return !isPrivatePackages }) .map((name) => { return require(resolve(`../../packages/${name}/package.json`)).name }) return ret } // add alias getPackagesName().forEach((name) => { webpackConfig.resolve.alias.set(`${name}$`, `${name}/src/index.ts`) }) let config = { ...webpackConfig.toConfig(), devServer: { host: '0.0.0.0', disableHostCheck: true }} // run test e2e if (e2e) { config.devServer.setup = (app, server) => { server.middleware.waitUntilValid(async () => { // back to src directory const cwd = path.join(__dirname, '../../') await execa('yarn', ['test:e2e'], { stdio: 'inherit', cwd }) }) } } module.exports = config ================================================ FILE: packages/examples/package.json ================================================ { "name": "examples", "version": "2.5.1", "description": "Examples of BetterScroll", "author": { "name": "jizhi", "email": "theniceangel@163.com" }, "private": true, "scripts": { "vue:dev": "cross-env NODE_ENV=development webpack-dev-server --config ./build/vue-webpack.conf.js --host 0.0.0.0 --port 8932 --colors", "vue:test:e2e": "yarn vue:dev --e2e", "vue:build": "cross-env NODE_ENV=production node build/vue-example-build.js", "vue:release": "sh vue-release.sh" }, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios", "better-scroll examples" ], "license": "MIT", "repository": { "type": "git", "url": "git@github.com:ustbhuangyi/better-scroll.git", "directory": "packages/examples" }, "devDependencies": { "@babel/polyfill": "^7.4.4", "babel-loader": "8.1.0", "copy-webpack-plugin": "^5.0.2", "friendly-errors-webpack-plugin": "^1.7.0", "html-webpack-plugin": "^3.2.0", "mini-css-extract-plugin": "^0.5.0", "ora": "^3.4.0", "terser-webpack-plugin": "^4.1.0", "uglifyjs-webpack-plugin": "^2.1.2", "vue-loader": "15.9.6", "webpack-cli": "3.3.0", "webpackbar": "3.2.0" } } ================================================ FILE: packages/examples/postcss.config.js ================================================ module.exports = { plugins: [ require('autoprefixer')({ browsers: require('./package.json').browserslist }) ] } ================================================ FILE: packages/examples/static/css/github-light.css ================================================ /* Copyright 2014 GitHub Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ .pl-c /* comment */ { color: #969896; } .pl-c1 /* constant, markup.raw, meta.diff.header, meta.module-reference, meta.property-name, support, support.constant, support.variable, variable.other.constant */, .pl-s .pl-v /* string variable */ { color: #0086b3; } .pl-e /* entity */, .pl-en /* entity.name */ { color: #795da3; } .pl-s .pl-s1 /* string source */, .pl-smi /* storage.modifier.import, storage.modifier.package, storage.type.java, variable.other, variable.parameter.function */ { color: #333; } .pl-ent /* entity.name.tag */ { color: #63a35c; } .pl-k /* keyword, storage, storage.type */ { color: #a71d5d; } .pl-pds /* punctuation.definition.string, string.regexp.character-class */, .pl-s /* string */, .pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */, .pl-sr /* string.regexp */, .pl-sr .pl-cce /* string.regexp constant.character.escape */, .pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */, .pl-sr .pl-sre /* string.regexp source.ruby.embedded */ { color: #183691; } .pl-v /* variable */ { color: #ed6a43; } .pl-id /* invalid.deprecated */ { color: #b52a1d; } .pl-ii /* invalid.illegal */ { background-color: #b52a1d; color: #f8f8f8; } .pl-sr .pl-cce /* string.regexp constant.character.escape */ { color: #63a35c; font-weight: bold; } .pl-ml /* markup.list */ { color: #693a17; } .pl-mh /* markup.heading */, .pl-mh .pl-en /* markup.heading entity.name */, .pl-ms /* meta.separator */ { color: #1d3e81; font-weight: bold; } .pl-mq /* markup.quote */ { color: #008080; } .pl-mi /* markup.italic */ { color: #333; font-style: italic; } .pl-mb /* markup.bold */ { color: #333; font-weight: bold; } .pl-md /* markup.deleted, meta.diff.header.from-file */ { background-color: #ffecec; color: #bd2c00; } .pl-mi1 /* markup.inserted, meta.diff.header.to-file */ { background-color: #eaffea; color: #55a532; } .pl-mdr /* meta.diff.range */ { color: #795da3; font-weight: bold; } .pl-mo /* meta.output */ { color: #1d3e81; } ================================================ FILE: packages/examples/static/css/normalize.css ================================================ /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ /** * 1. Set default font family to sans-serif. * 2. Prevent iOS text size adjust after orientation change, without disabling * user zoom. */ html { font-family: sans-serif; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ } /** * Remove default margin. */ body { margin: 0; } /* HTML5 display definitions ========================================================================== */ /** * Correct `block` display not defined for any HTML5 element in IE 8/9. * Correct `block` display not defined for `details` or `summary.md` in IE 10/11 * and Firefox. * Correct `block` display not defined for `main` in IE 11. */ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary { display: block; } /** * 1. Correct `inline-block` display not defined in IE 8/9. * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. */ audio, canvas, progress, video { display: inline-block; /* 1 */ vertical-align: baseline; /* 2 */ } /** * Prevent modern browsers from displaying `audio` without controls. * Remove excess height in iOS 5 devices. */ audio:not([controls]) { display: none; height: 0; } /** * Address `[hidden]` styling not present in IE 8/9/10. * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. */ [hidden], template { display: none; } /* Links ========================================================================== */ /** * Remove the gray background color from active links in IE 10. */ a { background-color: transparent; } /** * Improve readability when focused and also mouse hovered in all browsers. */ a:active, a:hover { outline: 0; } /* Text-level semantics ========================================================================== */ /** * Address styling not present in IE 8/9/10/11, Safari, and Chrome. */ abbr[title] { border-bottom: 1px dotted; } /** * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. */ b, strong { font-weight: bold; } /** * Address styling not present in Safari and Chrome. */ dfn { font-style: italic; } /** * Address variable `h1` font-size and margin within `section` and `article` * contexts in Firefox 4+, Safari, and Chrome. */ h1 { font-size: 2em; margin: 0.67em 0; } /** * Address styling not present in IE 8/9. */ mark { background: #ff0; color: #000; } /** * Address inconsistent and variable font size in all browsers. */ small { font-size: 80%; } /** * Prevent `sub` and `sup` affecting `line-height` in all browsers. */ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sup { top: -0.5em; } sub { bottom: -0.25em; } /* Embedded content ========================================================================== */ /** * Remove border when inside `a` element in IE 8/9/10. */ img { border: 0; } /** * Correct overflow not hidden in IE 9/10/11. */ svg:not(:root) { overflow: hidden; } /* Grouping content ========================================================================== */ /** * Address margin not present in IE 8/9 and Safari. */ figure { margin: 1em 40px; } /** * Address differences between Firefox and other browsers. */ hr { box-sizing: content-box; height: 0; } /** * Contain overflow in all browsers. */ pre { overflow: auto; } /** * Address odd `em`-unit font size rendering in all browsers. */ code, kbd, pre, samp { font-family: monospace, monospace; font-size: 1em; } /* Forms ========================================================================== */ /** * Known limitation: by default, Chrome and Safari on OS X allow very limited * styling of `select`, unless a `border` property is set. */ /** * 1. Correct color not being inherited. * Known issue: affects color of disabled elements. * 2. Correct font properties not being inherited. * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. */ button, input, optgroup, select, textarea { color: inherit; /* 1 */ font: inherit; /* 2 */ margin: 0; /* 3 */ } /** * Address `overflow` set to `hidden` in IE 8/9/10/11. */ button { overflow: visible; } /** * Address inconsistent `text-transform` inheritance for `button` and `select`. * All other form control elements do not inherit `text-transform` values. * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. * Correct `select` style inheritance in Firefox. */ button, select { text-transform: none; } /** * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` * and `video` controls. * 2. Correct inability to style clickable `input` types in iOS. * 3. Improve usability and consistency of cursor style between image-type * `input` and others. */ button, html input[type="button"], /* 1 */ input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ } /** * Re-set default cursor for disabled elements. */ button[disabled], html input[disabled] { cursor: default; } /** * Remove inner padding and border in Firefox 4+. */ button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } /** * Address Firefox 4+ setting `line-height` on `input` using `!important` in * the UA stylesheet. */ input { line-height: normal; } /** * It's recommended that you don't attempt to style these elements. * Firefox's implementation doesn't respect box-sizing, padding, or width. * * 1. Address box sizing set to `content-box` in IE 8/9/10. * 2. Remove excess padding in IE 8/9/10. */ input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } /** * Fix the cursor style for Chrome's increment/decrement buttons. For certain * `font-size` values of the `input`, it causes the cursor style of the * decrement button to change from `default` to `text`. */ input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { height: auto; } /** * 1. Address `appearance` set to `searchfield` in Safari and Chrome. * 2. Address `box-sizing` set to `border-box` in Safari and Chrome * (include `-moz` to future-proof). */ input[type="search"] { -webkit-appearance: textfield; /* 1 */ /* 2 */ box-sizing: content-box; } /** * Remove inner padding and search cancel button in Safari and Chrome on OS X. * Safari (but not Chrome) clips the cancel button when the search input has * padding (and `textfield` appearance). */ input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } /** * Define consistent border, margin, and padding. */ fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } /** * 1. Correct `color` not being inherited in IE 8/9/10/11. * 2. Remove padding so people aren't caught out if they zero out fieldsets. */ legend { border: 0; /* 1 */ padding: 0; /* 2 */ } /** * Remove default vertical scrollbar in IE 8/9/10/11. */ textarea { overflow: auto; } /** * Don't inherit the `font-weight` (applied by a rule above). * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. */ optgroup { font-weight: bold; } /* Tables ========================================================================== */ /** * Remove most spacing between table cells. */ table { border-collapse: collapse; border-spacing: 0; } td, th { padding: 0; } ================================================ FILE: packages/examples/static/css/reset.css ================================================ /** * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/) * http://cssreset.com */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, menu, nav, output, ruby, section, summary, time, mark, audio, video, input { margin: 0; padding: 0; border: 0; font-size: 100%; font-weight: normal; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, menu, nav, section { display: block; } body { line-height: 1; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: none; } table { border-collapse: collapse; border-spacing: 0; } /* custom */ a { color: #7e8c8d; -webkit-backface-visibility: hidden; } li { list-style: none; } ::-webkit-scrollbar { width: 5px; height: 5px; } ::-webkit-scrollbar-track-piece { background-color: rgba(0, 0, 0, 0.2); -webkit-border-radius: 6px; } ::-webkit-scrollbar-thumb:vertical { height: 5px; background-color: rgba(125, 125, 125, 0.7); -webkit-border-radius: 6px; } ::-webkit-scrollbar-thumb:horizontal { width: 5px; background-color: rgba(125, 125, 125, 0.7); -webkit-border-radius: 6px; } html, body { width: 100%; /* height: 100%; */ } body { -webkit-text-size-adjust: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } ================================================ FILE: packages/examples/static/css/stylesheet.css ================================================ * { box-sizing: border-box; } body { padding: 0; margin: 0; font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.5; background-color: #fff; color: #606c71; } a { color: #1e6bb8; text-decoration: none; } a:hover { text-decoration: underline; } .btn { display: inline-block; margin-bottom: 1rem; padding: 0.75rem 1rem; color: rgba(255, 255, 255, 0.8); background-color: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.2); border-style: solid; border-width: 1px; border-radius: 0.3rem; transition: color 0.2s, background-color 0.2s, border-color 0.2s; } .btn + .btn { margin-left: 1rem; } .btn:hover { color: rgba(255, 255, 255, 0.9); text-decoration: none; background-color: rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 0.3); } @media screen and (min-width: 42em) and (max-width: 64em) { .btn { font-size: 0.9rem; } } @media screen and (max-width: 42em) { .btn { display: block; width: 100%; padding: 0.75rem; font-size: 0.9rem; } .btn + .btn { margin-top: 1rem; margin-left: 0; } } .page-header { color: #fff; text-align: center; font-family: 'Patrick Hand', cursive; background-color: rgba(39,80,255, .7)} @media screen and (min-width: 64em) { .page-header { padding: 1.5rem 6rem 3rem 6rem; } } @media screen and (min-width: 42em) and (max-width: 64em) { .page-header { padding: 1.5rem 4rem 2rem 4rem; } } @media screen and (max-width: 42em) { .page-header { padding: 1.5rem 1rem 1rem 1rem; } } .project-name { margin-top: 0; margin-bottom: 0.1rem; } @media screen and (min-width: 64em) { .project-name { font-size: 3.25rem; } } @media screen and (min-width: 42em) and (max-width: 64em) { .project-name { font-size: 2.25rem; } } @media screen and (max-width: 42em) { .project-name { font-size: 1.75rem; } } .project-tagline { margin-bottom: 1rem; font-weight: normal; opacity: 0.7; } @media screen and (min-width: 64em) { .project-tagline { font-size: 1.25rem; } } @media screen and (min-width: 42em) and (max-width: 64em) { .project-tagline { font-size: 1.15rem; } } @media screen and (max-width: 42em) { .project-tagline { font-size: 1rem; } } .main-content :first-child { margin-top: 0; } .main-content img { max-width: 100%; } .main-content h1, .main-content h2, .main-content h3, .main-content h4, .main-content h5, .main-content h6 { margin-top: 1rem; margin-bottom: 1rem; } .main-content code { padding: 2px 4px; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 0.9rem; color: #383e41; background-color: #f3f6fa; border-radius: 0.3rem; } .main-content pre { padding: 0.8rem; margin-top: 0; margin-bottom: 1rem; font: 1rem Consolas, "Liberation Mono", Menlo, Courier, monospace; color: #567482; word-wrap: normal; background-color: #f3f6fa; border: solid 1px #dce6f0; border-radius: 0.3rem; } .main-content pre > code { padding: 0; margin: 0; font-size: 0.9rem; color: #567482; word-break: normal; white-space: pre; background: transparent; border: 0; } .main-content .highlight { margin-bottom: 1rem; } .main-content .highlight pre { margin-bottom: 0; word-break: normal; } .main-content .highlight pre, .main-content pre { padding: 0.8rem; overflow: auto; font-size: 0.9rem; line-height: 1.45; border-radius: 0.3rem; } .main-content pre code, .main-content pre tt { display: inline; max-width: initial; padding: 0; margin: 0; overflow: initial; line-height: inherit; word-wrap: normal; background-color: transparent; border: 0; } .main-content pre code:before, .main-content pre code:after, .main-content pre tt:before, .main-content pre tt:after { content: normal; } .main-content ul, .main-content ol { margin-top: 0; } .main-content blockquote { padding: 0 1rem; margin-left: 0; color: #819198; border-left: 0.3rem solid #dce6f0; } .main-content blockquote > :first-child { margin-top: 0; } .main-content blockquote > :last-child { margin-bottom: 0; } .main-content table { display: block; width: 100%; overflow: auto; word-break: normal; word-break: keep-all; } .main-content table th { font-weight: bold; } .main-content table th, .main-content table td { padding: 0.5rem 1rem; border: 1px solid #e9ebec; } .main-content dl { padding: 0; } .main-content dl dt { padding: 0; margin-top: 1rem; font-size: 1rem; font-weight: bold; } .main-content dl dd { padding: 0; margin-bottom: 1rem; } .main-content hr { height: 2px; padding: 0; margin: 1rem 0; background-color: #eff0f1; border: 0; } @media screen and (min-width: 64em) { .main-content { padding: 2rem 8rem; margin: 0 auto; font-size: 1.1rem; } } @media screen and (min-width: 42em) and (max-width: 64em) { .main-content { padding: 2rem 2rem; font-size: 1.1rem; } } @media screen and (max-width: 42em) { .main-content { padding: 1rem 1rem 2rem 1rem; font-size: 1rem; } iframe { display: none } } .site-footer { padding-top: 2rem; margin-top: 2rem; border-top: solid 1px #d7d7d7; } .site-footer-owner { display: block; font-weight: bold; } .site-footer-credits { color: #819198; } @media screen and (min-width: 64em) { .site-footer { font-size: 1rem; } } @media screen and (min-width: 42em) and (max-width: 64em) { .site-footer { font-size: 1rem; } } @media screen and (max-width: 42em) { .site-footer { font-size: 0.9rem; } } ================================================ FILE: packages/examples/vue/App.vue ================================================ ================================================ FILE: packages/examples/vue/components/compose/pullup-pulldown-outnested.vue ================================================ ================================================ FILE: packages/examples/vue/components/compose/pullup-pulldown-slide.vue ================================================ ================================================ FILE: packages/examples/vue/components/compose/pullup-pulldown.vue ================================================ ================================================ FILE: packages/examples/vue/components/compose/slide-nested.vue ================================================ ================================================ FILE: packages/examples/vue/components/core/default.vue ================================================ ================================================ FILE: packages/examples/vue/components/core/dynamic-content.vue ================================================ ================================================ FILE: packages/examples/vue/components/core/freescroll.vue ================================================ ================================================ FILE: packages/examples/vue/components/core/horizontal-rotated.vue ================================================ ================================================ FILE: packages/examples/vue/components/core/horizontal.vue ================================================ ================================================ FILE: packages/examples/vue/components/core/specified-content.vue ================================================ ================================================ FILE: packages/examples/vue/components/core/vertical-rotated.vue ================================================ ================================================ FILE: packages/examples/vue/components/form/textarea.vue ================================================ ================================================ FILE: packages/examples/vue/components/indicators/minimap.vue ================================================ ================================================ FILE: packages/examples/vue/components/indicators/parallax-scroll.vue ================================================ ================================================ FILE: packages/examples/vue/components/infinity/data/message.json ================================================ [ "when you popState and actually being well, we expect it further", "But I'm going to take care of ripping out my code in the fact that just something like that", "And what we'll createdCallbacks than that you can still read what each one of this should go out", "So just return Promise back and do this, the route equals", "ah, let's do a clearRoutes it says I'm not going to do", "At least trying new Promise", "then, and then it's going to check what that", "And we zoom in, then you can kind of set, except for a router", "Now strictly today", "I'm going to just takes an iterable as well be to add a visible", "Anyway, so that we'll do a link", "So what I'm going to minify this, so I'll just console", "log data for now, just sometimes look at that", "not then if we wanted to do position from the registerElements primed and red", "That isn't get called", "At all", "No", "Interesting that misc here", "So what was a regular expression", "Because once you get over doing a fancy techniques", "And let's see", "OK, we broke thing to do", "Right", "document", "" So", "Yeah", "", "which is fine", "And that we'll do sc", "view", "So what you draw the line where is it", "Where is being run", "I think, a million times look at it and styles an iteration, ES2015 update the content for is this", "routes equals Array", "from", "Hm, that might be a trade", "off, because we're just do an animation", "in the attached", "Look at this push", "pull kind of useful to have layout root here is it", "That by default, what we going to grab the", "Yes", "In router, I think, would let's say, for example", "So let's make it can be just this the hour mark on the actual contents", "We just loads though it was the way, a nice this", "Are you would be a little bit more pretty raw, this is a day, dude", "Border", "radius, that", "And I'm going to just do that will take something else", "And thank you might now", "That is the next time, I'm going to come into misc", "And somebody actually not", "source equals home", "But if I was sending me to resolve where we go", "All right", "And it makes JavaScript", "And I have run again", "Normally a massive, as I said, this is always, I'm going to call the different [INAUDIBLE] Hm", "Wow", "We have happen on screen, and the otherwise, don't want", "Yeah, and forth in the new path", "So we don't you use that might very wrong", "But in a customary bug", "Don't forget to hidden or display to none, things like a race when you are actually really long time I want to tell that is where you go", "And that work", "Yeah, and I'm going to do today", "I had misc are all the create one of the performance stuff", "But if you had lots of tea", "Yeah", "Now we're going to come in", "But did working as intended it", "So we can be able to be watching it straightforward slash", "And that, I think that will be all the like since we are valid concept for this, the root of this called HTML5 routing, which I don't know", "I just feels OK, but hopefully, and opacity 0, and it's just put a z", "index of 1 on that's going to be sort of handling of attachedCallback, and we want to transform scale very well be true for them is amazing, like across from the new one that", "You know", "Yeah, we could see now, all being we won't do this thing today", "And so this is a current view", "We have a question ties in", "Why not", "source equals router, why not", "And I think that we'd probably, if we've already to allow it to be the thing", "Oh, all right, so we get it, because I have to juggle it all", "No", "I feel I agree", "It would actually get it, because otherwise, we still have this", "routes", "keys", "So this is a layout boundary", "It's the cause", "Yeah, 3 pixels", "OK", "So since that's true", "And this stuff", "And that work", "Good point, or strict, and then the URL, changed", "But I'm going to, let's see, what we're any", "So the new view, think about", "And then we've defer, why not", "Let's fail", "So this newView, newView is never watching is I was that", "so that it's a compass", "Oh", "North, east, south, we called, all be no ES", "anything", "What I'm curious about your question here", "And I'm going to say", "so let's see", "So let's see", "So we'll say from this animations that we want to do this so that this point", "So we want us to cover next week", "We can actually", "But that they've all been set it", "Yeah", "And at the top and misc here", "But it will be run into a bit different sections", "And I think you'd want each of there's no DOM tree reason", "Well, yeah", "OK, so we have a couple of click for clicks", "And so if we see about this", "So what I think things that I really good start", "script tags at home, kids", "Don't do this file to actually", "Woo", "I made, sir", "So again, particular line of the", "let's call it sc for Supercharged", "There's no", "It's a compass", "Oh", "right", "newView, newView is the simplicity at this one anything below 2015, right", "It broke", "OK, let's see", "So we're going to removeEventListener", "You are the nicest", "something that you know, we'll create that doesn't necessarily end up with something new to these pages", "In router", "And certainly, as I said, you could usually just delete the constructor but createdCallback", "Oh, well, let link of the", "Yes", "If we had to do is I want us to come up writing apps, it can actually, this push", "pull kind of data, which version of something", "So what they can be about view or something that have a thing to do a trade", "off because you've got memory constraints and all these function", "So let's see if", "oh, do we wanted to do this", "If you're attach, what we'd want to know", "That is important think in so that goes to control of [", "UI ", "] transitions, particular expression", "Right, so the otherwise, it should also work on the layout, which might because we're actually remind yourselves that I can do it", "Yeah", "So that, in theory, place all the content as well when that have new ideas", "So this should be a class list, we'll create one of these, what we'll do is I want to do", "All right, bottom, left", "Do you have definitely", "So when the mindset off chaining [INAUDIBLE] out of the same index HTML elements", "Views", "Yeah", "So I'm going to createRoutes, wee, clearRoutes equals static", "Let's do this, status is generally work", "So that's why I was building the nicest", "I'll tell you what we want to come into the panels", "On all of ES2015 updates on the path name", "Because it's an iterate what they see", "I'm going to do", "We'll do that", "And hopefully, you're here in slash about view but we're going to be whichever view was the new view is that", "so that the event that isn't get called, all subscribing to do today", "And then we're just delete the JavaScript language", "Yeah, and we need to extends HTMLElement", "And we app where we actually uncanny valid concept for the out animation", "duration", "count in one tends HTML, I think, would then we've defer, why not", "Let's see what's good on here", "So if you say layout, for example", "Yes, so one of its scope", "What we want to do, I supposed to find out", "The defer mean to your Custom Elements JavaScript says we don't have", "We don't want to say this, so one that you click back to then dot the even though it", "So there a createdCallback, so we never being us", "That doesn't it", "Right", "All right", "That should", "Oh no, Array", "from", "Hm, it shouldNotMakeMoreOutPromises", "And then let's do that is purely for simplicity at this", "I don't takes too longer and I will say this", "routes", "because it matches the current ones will now needs to be run against that going to say const view back", "And then what the createRoute", "That's what I think", "So we have to transitions, particular if branch of this, you're giving us way too much better", "So since the layout, OK", "I think we'll create objects anymore", "You let us know what I'm going to do is I'm going to do is let's just find out", "createdCallbacks", "So if view", "I could do if we don't want to make a nav", "So I'm going to do that", "Super", "route", "So for this, right now, all the like shouldNotMakeMoreOutPromise", "resolve", "Same for the power of Promise, right", "Because why not", "Let's give it or not", "The defer also means that the state by selecting the view", "No", "Interesting", "So the brand", "new thing", "So let's see, so we do that", "All being well, we end with an actually hoping I will be remove this", "Are you this", "So we want to do that, actually just kind of amazing", "You know", "Yeah", "", "which is the current view was the new one that's a layout", "I don't you ask the question ties in it is when it's like a progressive to deal with, with contain strict", "now here", "And I'm going to us", "So onChanged", "Yeah", "Because of the this", "is", "the", "active", "view", "And we are building the routes equals this", "But when the view first time we create that isn't it", "Right", "Yeah, that is amazing", "And I think, a more bugs", "Yeah, I want it to updating to do that I have new view, and some Promise, we can actually can do here", "This is Paul", "Hi", "This time I write bugs, don't like this is actual lifecycle called ES6", "ES2016 was doing that's why I wanted to say", "currentView will be fast because", "You know what, in the back to the current view", "And then we'll say return", "One of the panels", "OK", "Come of that stuff out", "Should that the evaluation from 100", "no, should add that kind of got allowing that back out, right", "newView, newView, what we're kind of got these views that you, very wrong", "But if you about using there", "Because the nav has disappear ago, it was the keyword for all the regular expression and execution of a router", "Now you know, over that, in there", "Let's do that there we already got ourselves some of the way to go", "And it matches the new one for that", "Yeah", "And certain time gaps, think it's an animating to put a route for some reason", "view", "Figure out things simplicity at this point", "So what we're being a little bit of a pickle over right now we've deep", "linked that could want it to be that", "So let's just feels very interactions back in so this", "newView", "Yeah", "And apparent, what we'd want each one of all the debugger standard one", "So this way, it should add the visible", "And we're pretty raw, there will be find out notionally, the code, it's fine, it's fail", "So the question", "Yeah, so we could see now them to makes Jav" ] ================================================ FILE: packages/examples/vue/components/infinity/default.vue ================================================ ================================================ FILE: packages/examples/vue/components/mouse-wheel/horizontal-scroll.vue ================================================ ================================================ FILE: packages/examples/vue/components/mouse-wheel/horizontal-slide.vue ================================================ ================================================ FILE: packages/examples/vue/components/mouse-wheel/picker.vue ================================================ ================================================ FILE: packages/examples/vue/components/mouse-wheel/pulldown.vue ================================================ ================================================ FILE: packages/examples/vue/components/mouse-wheel/pullup.vue ================================================ ================================================ FILE: packages/examples/vue/components/mouse-wheel/vertical-scroll.vue ================================================ ================================================ FILE: packages/examples/vue/components/mouse-wheel/vertical-slide.vue ================================================ ================================================ FILE: packages/examples/vue/components/movable/default.vue ================================================ ================================================ FILE: packages/examples/vue/components/movable/multi-content-scale.vue ================================================ ================================================ FILE: packages/examples/vue/components/movable/multi-content.vue ================================================ ================================================ FILE: packages/examples/vue/components/movable/scale.vue ================================================ ================================================ FILE: packages/examples/vue/components/nested-scroll/horizontal-in-vertical.vue ================================================ ================================================ FILE: packages/examples/vue/components/nested-scroll/horizontal.vue ================================================ ================================================ FILE: packages/examples/vue/components/nested-scroll/triple-vertical.vue ================================================ ================================================ FILE: packages/examples/vue/components/nested-scroll/vertical.vue ================================================ ================================================ FILE: packages/examples/vue/components/observe-dom/default.vue ================================================ ================================================ FILE: packages/examples/vue/components/observe-image/default.vue ================================================ ================================================ FILE: packages/examples/vue/components/picker/double-column.vue ================================================ ================================================ FILE: packages/examples/vue/components/picker/linkage-column.vue ================================================ ================================================ FILE: packages/examples/vue/components/picker/one-column.vue ================================================ ================================================ FILE: packages/examples/vue/components/pulldown/default.vue ================================================ ================================================ FILE: packages/examples/vue/components/pulldown/sina-weibo.vue ================================================ ================================================ FILE: packages/examples/vue/components/pullup/default.vue ================================================ ================================================ FILE: packages/examples/vue/components/scrollbar/custom.vue ================================================ ================================================ FILE: packages/examples/vue/components/scrollbar/horizontal.vue ================================================ ================================================ FILE: packages/examples/vue/components/scrollbar/mousewheel.vue ================================================ ================================================ FILE: packages/examples/vue/components/scrollbar/vertical.vue ================================================ ================================================ FILE: packages/examples/vue/components/slide/banner.vue ================================================ ================================================ FILE: packages/examples/vue/components/slide/dynamic.vue ================================================ ================================================ FILE: packages/examples/vue/components/slide/fullpage.vue ================================================ ================================================ FILE: packages/examples/vue/components/slide/specified-index.vue ================================================ ================================================ FILE: packages/examples/vue/components/slide/vertical.vue ================================================ ================================================ FILE: packages/examples/vue/components/zoom/default.vue ================================================ ================================================ FILE: packages/examples/vue/index.html ================================================ BetterScroll Examples
================================================ FILE: packages/examples/vue/main.js ================================================ import '@babel/polyfill' import Vue from 'vue' import router from './router' import App from './App.vue' // /* eslint-disable no-unused-vars */ // import VConsole from 'vconsole' // develop console // /* eslint-disable no-new */ // new VConsole() /* eslint-disable no-new */ new Vue({ el: '#app', router, render: h => h(App) }) ================================================ FILE: packages/examples/vue/pages/compose-entry.vue ================================================ ================================================ FILE: packages/examples/vue/pages/core-entry.vue ================================================ ================================================ FILE: packages/examples/vue/pages/form-entry.vue ================================================ ================================================ FILE: packages/examples/vue/pages/indicators-entry.vue ================================================ ================================================ FILE: packages/examples/vue/pages/infinity-entry.vue ================================================ ================================================ FILE: packages/examples/vue/pages/mouse-wheel-entry.vue ================================================ ================================================ FILE: packages/examples/vue/pages/movable-entry.vue ================================================ ================================================ FILE: packages/examples/vue/pages/nested-scroll-entry.vue ================================================ ================================================ FILE: packages/examples/vue/pages/observe-dom-entry.vue ================================================ ================================================ FILE: packages/examples/vue/pages/observe-image-entry.vue ================================================ ================================================ FILE: packages/examples/vue/pages/picker-entry.vue ================================================ ================================================ FILE: packages/examples/vue/pages/pulldown-entry.vue ================================================ ================================================ FILE: packages/examples/vue/pages/pullup-entry.vue ================================================ ================================================ FILE: packages/examples/vue/pages/scrollbar-entry.vue ================================================ ================================================ FILE: packages/examples/vue/pages/slide-entry.vue ================================================ ================================================ FILE: packages/examples/vue/pages/zoom-entry.vue ================================================ ================================================ FILE: packages/examples/vue/router/index.js ================================================ import Vue from 'vue' import Router from 'vue-router' import CoreEntry from 'vue-example/pages/core-entry' import ObserveDOMEntry from 'vue-example/pages/observe-dom-entry' import ZoomEntry from 'vue-example/pages/zoom-entry' import SlideEntry from 'vue-example/pages/slide-entry' import PickerEntry from 'vue-example/pages/picker-entry' import PullupEntry from 'vue-example/pages/pullup-entry' import PullDownEntry from 'vue-example/pages/pulldown-entry' import ScrollBarEntry from 'vue-example/pages/scrollbar-entry' import InfinityScrollEntry from 'vue-example/pages/infinity-entry' import FormEntry from 'vue-example/pages/form-entry' import NestedScrollEntry from 'vue-example/pages/nested-scroll-entry' import MovableEntry from 'vue-example/pages/movable-entry' import MouseWheelEntry from 'vue-example/pages/mouse-wheel-entry' import ComposeEntry from 'vue-example/pages/compose-entry' import ObserveImageEntry from 'vue-example/pages/observe-image-entry' import IndicatorsEntry from 'vue-example/pages/indicators-entry' import ScrollbarVertical from 'vue-example/components/scrollbar/vertical' import ScrollbarHorizontal from 'vue-example/components/scrollbar/horizontal' import ScrollbarCustom from 'vue-example/components/scrollbar/custom' import ScrollbarMouseWheel from 'vue-example/components/scrollbar/mousewheel' import MouseWheelVerticalScroll from 'vue-example/components/mouse-wheel/vertical-scroll' import MouseWheelHorizontalScroll from 'vue-example/components/mouse-wheel/horizontal-scroll' import MouseWheelVerticalSlide from 'vue-example/components/mouse-wheel/vertical-slide' import MouseWheelHorizontalSlide from 'vue-example/components/mouse-wheel/horizontal-slide' import MouseWheelPullUp from 'vue-example/components/mouse-wheel/pullup' import MouseWheelPullDown from 'vue-example/components/mouse-wheel/pulldown' import MouseWheelPicker from 'vue-example/components/mouse-wheel/picker' import BannerSlide from 'vue-example/components/slide/banner' import PageSlide from 'vue-example/components/slide/fullpage' import VerticalSlide from 'vue-example/components/slide/vertical' import DynamicSlide from 'vue-example/components/slide/dynamic' import SpecifiedIndexSlide from 'vue-example/components/slide/specified-index' import VerticalScroll from 'vue-example/components/core/default' import HorizontalScroll from 'vue-example/components/core/horizontal' import DynamicContentScroll from 'vue-example/components/core/dynamic-content' import SpecifiedContentScroll from 'vue-example/components/core/specified-content' import Freescroll from 'vue-example/components/core/freescroll' import VerticalRotatedScroll from 'vue-example/components/core/vertical-rotated' import HorizontalRotatedScroll from 'vue-example/components/core/horizontal-rotated' import OneColumnPicker from 'vue-example/components/picker/one-column' import DoubleColumnPicker from 'vue-example/components/picker/double-column' import LinkageColumnPicker from 'vue-example/components/picker/linkage-column' import FormTextarea from 'vue-example/components/form/textarea' import NestedVerticalScroll from 'vue-example/components/nested-scroll/vertical' import NestedTripleVerticalScroll from 'vue-example/components/nested-scroll/triple-vertical' import NestedHorizontalScroll from 'vue-example/components/nested-scroll/horizontal' import NestedHorizontalInVertical from 'vue-example/components/nested-scroll/horizontal-in-vertical' import Movable from 'vue-example/components/movable/default' import MovableMultiContent from 'vue-example/components/movable/multi-content' import MovableScale from 'vue-example/components/movable/scale' import MovableMultiContentScale from 'vue-example/components/movable/multi-content-scale' import ComposePullUpPullDown from 'vue-example/components/compose/pullup-pulldown' import ComposePullUpPullDownSlide from 'vue-example/components/compose/pullup-pulldown-slide' import ComposePullUpPullDownNested from 'vue-example/components/compose/pullup-pulldown-outnested' import ComposeSlideNested from 'vue-example/components/compose/slide-nested' import IndicatorsMinimap from 'vue-example/components/indicators/minimap' import IndicatorsParallaxScroll from 'vue-example/components/indicators/parallax-scroll' import PulldownDefault from 'vue-example/components/pulldown/default' import PulldownSinaWeibo from 'vue-example/components/pulldown/sina-weibo' Vue.use(Router) export default new Router({ routes: [ { path: '/zoom', component: ZoomEntry, }, { path: '/observe-dom', component: ObserveDOMEntry }, { path: '/slide', component: SlideEntry, children: [ { path: 'banner', component: BannerSlide, }, { path: 'fullpage', component: PageSlide, }, { path: 'vertical', component: VerticalSlide, }, { path: 'dynamic', component: DynamicSlide, }, { path: 'specified', component: SpecifiedIndexSlide } ], }, { path: '/core', component: CoreEntry, children: [ { path: 'default', component: VerticalScroll, }, { path: 'horizontal', component: HorizontalScroll, }, { path: 'dynamic-content', component: DynamicContentScroll }, { path: 'specified-content', component: SpecifiedContentScroll }, { path: 'freescroll', component: Freescroll, }, { path: 'vertical-rotated', component: VerticalRotatedScroll }, { path: 'horizontal-rotated', component: HorizontalRotatedScroll } ], }, { path: '/mouse-wheel', component: MouseWheelEntry, children: [ { path: 'vertical-scroll', component: MouseWheelVerticalScroll, }, { path: 'horizontal-scroll', component: MouseWheelHorizontalScroll, }, { path: 'vertical-slide', component: MouseWheelVerticalSlide, }, { path: 'horizontal-slide', component: MouseWheelHorizontalSlide, }, { path: 'pullup', component: MouseWheelPullUp, }, { path: 'pulldown', component: MouseWheelPullDown, }, { path: 'picker', component: MouseWheelPicker, }, ], }, { path: '/picker', component: PickerEntry, children: [ { path: 'one-column', component: OneColumnPicker, }, { path: 'double-column', component: DoubleColumnPicker, }, { path: 'linkage-column', component: LinkageColumnPicker, }, ], }, { path: '/pullup', component: PullupEntry, }, { path: '/pulldown', component: PullDownEntry, children: [ { path: 'default', component: PulldownDefault }, { path: 'sina', component: PulldownSinaWeibo } ] }, { path: '/scrollbar', component: ScrollBarEntry, children: [ { path: 'vertical', component: ScrollbarVertical }, { path: 'horizontal', component: ScrollbarHorizontal }, { path: 'custom', component: ScrollbarCustom }, { path: 'mousewheel', component: ScrollbarMouseWheel } ] }, { path: '/infinity', component: InfinityScrollEntry, }, { path: '/form', component: FormEntry, children: [ { path: 'textarea', component: FormTextarea, }, ], }, { path: '/nested-scroll', component: NestedScrollEntry, children: [ { path: 'vertical', component: NestedVerticalScroll, }, { path: 'horizontal', component: NestedHorizontalScroll, }, { path: 'horizontal-in-vertical', component: NestedHorizontalInVertical, }, { path: 'triple-vertical', component: NestedTripleVerticalScroll, } ], }, { path: '/movable', component: MovableEntry, children: [ { path: 'default', component: Movable, }, { path: 'scale', component: MovableScale, }, { path: 'multi-content', component: MovableMultiContent }, { path: 'multi-content-scale', component: MovableMultiContentScale } ], }, { path: '/compose', component: ComposeEntry, children: [ { path: 'pullup-pulldown', component: ComposePullUpPullDown, }, { path: 'pullup-pulldown-slide', component: ComposePullUpPullDownSlide, }, { path: 'pullup-pulldown-outnested', component: ComposePullUpPullDownNested }, { path: 'slide-nested', component: ComposeSlideNested } ], }, { path: '/observe-image', component: ObserveImageEntry }, { path: '/indicators', component: IndicatorsEntry, children: [ { path: 'minimap', component: IndicatorsMinimap }, { path: 'parallax-scroll', component: IndicatorsParallaxScroll } ] } ], }) ================================================ FILE: packages/examples/vue-release.sh ================================================ #!/usr/bin/env sh set -e yarn run vue:build cd dist/vue git init git add -A git commit -m 'update examples' git push -f git@github.com:better-scroll/examples.git master:gh-pages cd - ================================================ FILE: packages/indicators/README.md ================================================ # @better-scroll/indicators [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/indicators/README_zh-CN.md) Indicator can be used to achieve magnifying glass, parallax scrolling and other effects. ## Usage ```js import BScroll from '@better-scroll/core' import Indicators from '@better-scroll/indicators' BScroll.use(Indicators) const bs = new BScroll('.wrapper', { indicators: [ relationElement: someHTMLElement ] }) ``` ```ts interface IndicatorOptions { interactive?: boolean ratio?: Ratio relationElementHandleElementIndex?: number relationElement: HTMLElement } ``` ================================================ FILE: packages/indicators/README_zh-CN.md ================================================ # @better-scroll/indicators 指示器,可用来实现放大镜、视觉滚动等效果。 ## 使用 ```js import BScroll from '@better-scroll/core' import Indicators from '@better-scroll/indicators' BScroll.use(Indicators) const bs = new BScroll('.wrapper', { indicators: [ relationElement: someHTMLElement ] }) ``` ```ts interface IndicatorOptions { interactive?: boolean ratio?: Ratio relationElementHandleElementIndex?: number relationElement: HTMLElement } ``` ================================================ FILE: packages/indicators/package.json ================================================ { "name": "@better-scroll/indicators", "version": "2.5.1", "description": "used as parallax scrolling, magnifier effects", "author": { "name": "jizhi", "email": "theniceangel@163.com" }, "main": "dist/indicators.min.js", "module": "dist/indicators.esm.js", "typings": "dist/types/index.d.ts", "publishConfig": { "access": "public" }, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios", "scrollbar", "minimap", "magnifier", "parallax scroll" ], "repository": { "type": "git", "url": "git+ssh://git@github.com/ustbhuangyi/better-scroll.git", "directory": "packages/indicators" }, "dependencies": { "@better-scroll/core": "^2.5.1" }, "gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130" } ================================================ FILE: packages/indicators/src/__mocks__/indicator.ts ================================================ const mockIndicator = jest .fn() .mockImplementation(function IndicatorMockFn(scroll: any, options: any) { return { destroy: jest.fn(), } }) export default mockIndicator ================================================ FILE: packages/indicators/src/__tests__/index.spec.ts ================================================ import Indicator from '../indicator' import BScroll from '@better-scroll/core' jest.mock('@better-scroll/core') jest.mock('../indicator') import Indicators from '../index' const addProperties = ( target: T, source: K ) => { for (const key in source) { ;(target as any)[key] = source[key] } return target } const createIndicatorElement = () => { // indicators DOM const indicatorWrapper = document.createElement('div') const indicatorEl = document.createElement('div') indicatorWrapper.appendChild(indicatorEl) return { indicatorWrapper, } } describe('Indicators unit tests', () => { let scroll: BScroll beforeEach(() => { // BScroll DOM const wrapper = document.createElement('div') const content = document.createElement('div') wrapper.appendChild(content) scroll = new BScroll(wrapper, {}) }) afterEach(() => { jest.clearAllMocks() }) describe('constructor', () => { it('throw error when pass wrong options', () => { expect(() => { new Indicators(scroll) }).toThrow() expect(() => { addProperties(scroll.options, { indicators: [ { relationElement: null, }, ], }) new Indicators(scroll) }).toThrow() }) it('should create indicator elements', () => { const { indicatorWrapper } = createIndicatorElement() addProperties(scroll.options, { indicators: [ { relationElement: indicatorWrapper, }, ], }) const indicatorsInstance = new Indicators(scroll) expect(indicatorsInstance.indicators.length).toBe(1) }) it('destroy hook', () => { const { indicatorWrapper } = createIndicatorElement() addProperties(scroll.options, { indicators: [ { relationElement: indicatorWrapper, }, ], }) const indicatorsInstance = new Indicators(scroll) scroll.hooks.trigger(scroll.hooks.eventTypes.destroy) for (let indicator of indicatorsInstance.indicators) { expect(indicator.destroy).toBeCalled() } }) }) }) ================================================ FILE: packages/indicators/src/__tests__/indicator.spec.ts ================================================ import Indicator from '../indicator' import BScroll from '@better-scroll/core' import { IndicatorOptions } from '../types' import { dispatchTouchStart, dispatchTouchMove, dispatchTouchEnd, } from '@better-scroll/core/src/__tests__/__utils__/event' jest.mock('@better-scroll/core') const addProperties = ( target: T, source: K ) => { for (const key in source) { ;(target as any)[key] = source[key] } return target } const createIndicatorElement = () => { // indicators DOM const indicatorWrapper = document.createElement('div') const indicatorEl = document.createElement('div') indicatorWrapper.appendChild(indicatorEl) return { indicatorWrapper, } } describe('Indicator unit tests', () => { let scroll: BScroll let indicatorOption: IndicatorOptions beforeEach(() => { // BScroll DOM const wrapper = document.createElement('div') const content = document.createElement('div') wrapper.appendChild(content) scroll = new BScroll(wrapper, {}) indicatorOption = { relationElement: createIndicatorElement().indicatorWrapper, } }) afterEach(() => { jest.clearAllMocks() }) it('refresh hook', () => { const indicator = new Indicator(scroll, indicatorOption) addProperties(scroll, { hasVerticalScroll: true, hasHorizontalScroll: true, maxScrollX: -1, maxScrollY: -1, }) addProperties(indicatorOption, { ratio: { x: 1, y: 1, }, }) scroll.hooks.trigger(scroll.hooks.eventTypes.refresh) expect(indicator.currentPos).toMatchObject({ x: 0, y: 0, }) expect(indicator.indicatorEl.style.transform).toBe( 'translateX(0px) translateY(0px) translateZ(0)' ) }) it('translater hook', () => { const indicator = new Indicator(scroll, indicatorOption) addProperties(indicator, { ratioY: 2, translateYSign: 1, }) const translaterHooks = scroll.scroller.translater.hooks translaterHooks.trigger(translaterHooks.eventTypes.translate, { x: 0, y: -20, }) expect(indicator.currentPos).toMatchObject({ x: 0, y: -40, }) }) it('animater hook', () => { const indicator = new Indicator(scroll, indicatorOption) const animaterHooks = scroll.scroller.animater.hooks animaterHooks.trigger( animaterHooks.eventTypes.timeFunction, 'cubic-bezier(0.25, 0.46, 0.45, 0.94)' ) animaterHooks.trigger(animaterHooks.eventTypes.time, 200) const style = indicator.indicatorEl.style as any expect(style['transition-timing-function']).toBe( 'cubic-bezier(0.25, 0.46, 0.45, 0.94)' ) expect(style['transition-duration']).toBe('200ms') }) it('touch hooks', async () => { addProperties(scroll.options, { probeType: 3, disableMouse: false, }) addProperties(scroll, { hasHorizontalScroll: true, }) const indicator = new Indicator(scroll, indicatorOption) const beforeStartMockFn = jest.fn() const startMockFn = jest.fn() const moveMockFn = jest.fn() const endMockFn = jest.fn() const scroller = scroll.scroller scroller.hooks.on( scroller.hooks.eventTypes.beforeScrollStart, beforeStartMockFn ) scroller.hooks.on(scroller.hooks.eventTypes.scrollStart, startMockFn) scroller.hooks.on(scroller.hooks.eventTypes.scroll, moveMockFn) scroller.hooks.on(scroller.hooks.eventTypes.scrollEnd, endMockFn) // scroll is disabled scroll.enabled = false dispatchTouchStart(indicatorOption.relationElement.children[0], [ { pageX: 0, pageY: -20, }, ]) expect(beforeStartMockFn).not.toBeCalled() // scroll is enabled scroll.enabled = true dispatchTouchStart(indicatorOption.relationElement.children[0], [ { pageX: 0, pageY: -20, }, ]) dispatchTouchMove(window, [ { pageX: 0, pageY: -40, }, ]) dispatchTouchEnd(window, []) expect(beforeStartMockFn).toBeCalled() expect(startMockFn).toBeCalled() expect(moveMockFn).toBeCalled() expect(endMockFn).toBeCalled() // dispatch scroll in interval time addProperties(scroll.options, { probeType: 1, }) const moveMockFn2 = jest.fn() scroller.hooks.on(scroller.hooks.eventTypes.scroll, moveMockFn2) dispatchTouchStart(indicatorOption.relationElement.children[0], [ { pageX: 0, pageY: -20, }, ]) await new Promise((resolve) => { setTimeout(resolve, 400) }) dispatchTouchMove(window, [ { pageX: 0, pageY: -40, }, ]) expect(moveMockFn2).toBeCalled() }) it('destroy', () => { addProperties(indicatorOption, { ratio: 1, }) const indicator = new Indicator(scroll, indicatorOption) scroll.hooks.trigger(scroll.hooks.eventTypes.refresh) indicator.destroy() expect(indicator.hooksFn.length).toBe(0) }) }) ================================================ FILE: packages/indicators/src/index.ts ================================================ import BScroll from '@better-scroll/core' import Indicator from './indicator' import { IndicatorOptions } from './types' import { assert } from '@better-scroll/shared-utils' declare module '@better-scroll/core' { interface CustomOptions { indicators?: IndicatorOptions[] } } export default class Indicators { static pluginName = 'indicators' options: IndicatorOptions[] = [] indicators: Indicator[] = [] constructor(public scroll: BScroll) { this.handleOptions() this.handleHooks() } private handleOptions() { const UserIndicatorsOptions = this.scroll.options.indicators! assert( Array.isArray(UserIndicatorsOptions), `'indicators' must be an array.` ) for (const indicatorOptions of UserIndicatorsOptions) { assert( !!indicatorOptions.relationElement, `'relationElement' must be a HTMLElement.` ) this.createIndicators(indicatorOptions) } } private createIndicators(options: IndicatorOptions) { this.indicators.push(new Indicator(this.scroll, options)) } private handleHooks() { const scrollHooks = this.scroll.hooks scrollHooks.on(scrollHooks.eventTypes.destroy, () => { for (const indicator of this.indicators) { indicator.destroy() } this.indicators = [] }) } } ================================================ FILE: packages/indicators/src/indicator.ts ================================================ import BScroll from '@better-scroll/core' import { IndicatorOptions, Ratio, Postion, ValueSign } from './types' import { EventRegister, EventEmitter, getRect, getClientSize, getNow, between, Probe, TouchEvent, style, maybePrevent, } from '@better-scroll/shared-utils' const resolveRatioOption = (ratioConfig?: Ratio) => { let ret = { ratioX: 0, ratioY: 0, } /* istanbul ignore if */ if (!ratioConfig) { return ret } if (typeof ratioConfig === 'number') { ret.ratioX = ret.ratioY = ratioConfig } else if (typeof ratioConfig === 'object' && ratioConfig) { ret.ratioX = ratioConfig.x || 0 ret.ratioY = ratioConfig.y || 0 } return ret } const handleBubbleAndCancelable = (e: TouchEvent) => { maybePrevent(e) e.stopPropagation() } export default class Indicator { wrapper: HTMLElement indicatorEl: HTMLElement maxScrollX: number minScrollX: number ratioX: number maxScrollY: number minScrollY: number translateXSign: ValueSign translateYSign: ValueSign ratioY: number currentPos: Postion = { x: 0, y: 0, } moved: boolean startTime: number initiated: boolean lastPointX: number lastPointY: number startEventRegister: EventRegister moveEventRegister: EventRegister endEventRegister: EventRegister hooksFn: [EventEmitter, string, Function][] = [] constructor(public scroll: BScroll, public options: IndicatorOptions) { this.handleDOM() this.handleHooks() this.handleInteractive() } private handleDOM() { const { relationElement, relationElementHandleElementIndex = 0 } = this.options this.wrapper = relationElement this.indicatorEl = this.wrapper.children[ relationElementHandleElementIndex ] as HTMLElement } private handleHooks() { const scroll = this.scroll const scrollHooks = scroll.hooks const translaterHooks = scroll.scroller.translater.hooks const animaterHooks = scroll.scroller.animater.hooks this.registerHooks( scrollHooks, scrollHooks.eventTypes.refresh, this.refresh ) this.registerHooks( translaterHooks, translaterHooks.eventTypes.translate, (pos: Postion) => { this.updatePosition(pos) } ) this.registerHooks( animaterHooks, animaterHooks.eventTypes.time, this.transitionTime ) this.registerHooks( animaterHooks, animaterHooks.eventTypes.timeFunction, this.transitionTimingFunction ) } private transitionTime(time: number = 0) { this.indicatorEl.style[style.transitionDuration as any] = time + 'ms' } private transitionTimingFunction(easing: string) { this.indicatorEl.style[style.transitionTimingFunction as any] = easing } private handleInteractive() { if (this.options.interactive !== false) { this.registerEvents() } } private registerHooks(hooks: EventEmitter, name: string, handler: Function) { hooks.on(name, handler, this) this.hooksFn.push([hooks, name, handler]) } private registerEvents() { const { disableMouse, disableTouch } = this.scroll.options const startEvents = [] const moveEvents = [] const endEvents = [] if (!disableMouse) { startEvents.push({ name: 'mousedown', handler: this.start.bind(this), }) moveEvents.push({ name: 'mousemove', handler: this.move.bind(this), }) endEvents.push({ name: 'mouseup', handler: this.end.bind(this), }) } if (!disableTouch) { startEvents.push({ name: 'touchstart', handler: this.start.bind(this), }) moveEvents.push({ name: 'touchmove', handler: this.move.bind(this), }) endEvents.push( { name: 'touchend', handler: this.end.bind(this), }, { name: 'touchcancel', handler: this.end.bind(this), } ) } this.startEventRegister = new EventRegister(this.indicatorEl, startEvents) this.moveEventRegister = new EventRegister(window, moveEvents) this.endEventRegister = new EventRegister(window, endEvents) } refresh() { const { x, y, hasHorizontalScroll, hasVerticalScroll, maxScrollX: maxBScrollX, maxScrollY: maxBScrollY, } = this.scroll const { ratioX, ratioY } = resolveRatioOption(this.options.ratio) const { width: wrapperWidth, height: wrapperHeight } = getClientSize( this.wrapper ) const { width: indicatorWidth, height: indicatorHeight } = getRect( this.indicatorEl ) if (hasHorizontalScroll) { this.maxScrollX = wrapperWidth - indicatorWidth this.translateXSign = this.maxScrollX > 0 ? ValueSign.Positive : ValueSign.NotPositive this.minScrollX = 0 // ensure positive this.ratioX = ratioX ? ratioX : Math.abs(this.maxScrollX / maxBScrollX) } if (hasVerticalScroll) { this.maxScrollY = wrapperHeight - indicatorHeight this.translateYSign = this.maxScrollY > 0 ? ValueSign.Positive : ValueSign.NotPositive this.minScrollY = 0 this.ratioY = ratioY ? ratioY : Math.abs(this.maxScrollY / maxBScrollY) } this.updatePosition({ x, y, }) } private start(e: TouchEvent) { if (this.BScrollIsDisabled()) { return } let point = (e.touches ? e.touches[0] : e) as Touch handleBubbleAndCancelable(e) this.initiated = true this.moved = false this.lastPointX = point.pageX this.lastPointY = point.pageY this.startTime = getNow() this.scroll.scroller.hooks.trigger( this.scroll.scroller.hooks.eventTypes.beforeScrollStart ) } private BScrollIsDisabled() { return !this.scroll.enabled } private move(e: TouchEvent) { if (!this.initiated) { return } let point = (e.touches ? e.touches[0] : e) as Touch const pointX = point.pageX const pointY = point.pageY handleBubbleAndCancelable(e) let deltaX = pointX - this.lastPointX let deltaY = pointY - this.lastPointY this.lastPointX = pointX this.lastPointY = pointY if (!this.moved && !this.indicatorNotMoved(deltaX, deltaY)) { this.moved = true this.scroll.scroller.hooks.trigger( this.scroll.scroller.hooks.eventTypes.scrollStart ) } if (this.moved) { const newPos = this.getBScrollPosByRatio(this.currentPos, deltaX, deltaY) this.syncBScroll(newPos) } } private end(e: TouchEvent) { if (!this.initiated) { return } this.initiated = false handleBubbleAndCancelable(e) if (this.moved) { const { x, y } = this.scroll this.scroll.scroller.hooks.trigger( this.scroll.scroller.hooks.eventTypes.scrollEnd, { x, y, } ) } } private getBScrollPosByRatio( currentPos: Postion, deltaX: number, deltaY: number ) { const { x: currentX, y: currentY } = currentPos const { hasHorizontalScroll, hasVerticalScroll, minScrollX: BScrollMinScrollX, maxScrollX: BScrollMaxScrollX, minScrollY: BScrollMinScrollY, maxScrollY: BScrollMaxScrollY, } = this.scroll let { x, y } = this.scroll if (hasHorizontalScroll) { const newPosX = between( currentX + deltaX, Math.min(this.minScrollX, this.maxScrollX), Math.max(this.minScrollX, this.maxScrollX) ) const roundX = Math.round((newPosX / this.ratioX) * this.translateXSign) x = between(roundX, BScrollMaxScrollX, BScrollMinScrollX) } if (hasVerticalScroll) { const newPosY = between( currentY + deltaY, Math.min(this.minScrollY, this.maxScrollY), Math.max(this.minScrollY, this.maxScrollY) ) const roundY = Math.round((newPosY / this.ratioY) * this.translateYSign) y = between(roundY, BScrollMaxScrollY, BScrollMinScrollY) } return { x, y } } private indicatorNotMoved(deltaX: number, deltaY: number): boolean { const { x, y } = this.currentPos const xNotMoved = (x === this.minScrollX && deltaX <= 0) || (x === this.maxScrollX && deltaX >= 0) const yNotMoved = (y === this.minScrollY && deltaY <= 0) || (y === this.maxScrollY && deltaY >= 0) return xNotMoved && yNotMoved } private syncBScroll(newPos: Postion) { const timestamp = getNow() const { options, scroller } = this.scroll const { probeType, momentumLimitTime } = options scroller.translater.translate(newPos) // dispatch scroll in interval time if (timestamp - this.startTime > momentumLimitTime) { this.startTime = timestamp if (probeType === Probe.Throttle) { scroller.hooks.trigger(scroller.hooks.eventTypes.scroll, newPos) } } // dispatch scroll all the time if (probeType > Probe.Throttle) { scroller.hooks.trigger(scroller.hooks.eventTypes.scroll, newPos) } } updatePosition(BScrollPos: Postion) { const newIndicatorPos = this.getIndicatorPosByRatio(BScrollPos) this.applyTransformProperty(newIndicatorPos) this.currentPos = { ...newIndicatorPos } } private applyTransformProperty(pos: Postion) { const translateZ = this.scroll.options.translateZ const transformProperties = [ `translateX(${pos.x}px)`, `translateY(${pos.y}px)`, `${translateZ}`, ] this.indicatorEl.style[style.transform as any] = transformProperties.join(' ') } private getIndicatorPosByRatio(BScrollPos: Postion) { const { x, y } = BScrollPos const { hasHorizontalScroll, hasVerticalScroll } = this.scroll const position = { ...this.currentPos } if (hasHorizontalScroll) { const roundX = Math.round(this.ratioX * x * this.translateXSign) // maybe maxScrollX is negative position.x = between( roundX, Math.min(this.minScrollX, this.maxScrollX), Math.max(this.minScrollX, this.maxScrollX) ) } if (hasVerticalScroll) { const roundY = Math.round(this.ratioY * y * this.translateYSign) // maybe maxScrollY is negative position.y = between( roundY, Math.min(this.minScrollY, this.maxScrollY), Math.max(this.minScrollY, this.maxScrollY) ) } return position } destroy() { if (this.options.interactive !== false) { this.startEventRegister.destroy() this.moveEventRegister.destroy() this.endEventRegister.destroy() } this.hooksFn.forEach((item) => { const hooks = item[0] const hooksName = item[1] const handlerFn = item[2] hooks.off(hooksName, handlerFn) }) this.hooksFn.length = 0 } } ================================================ FILE: packages/indicators/src/types.ts ================================================ export type Ratio = number | RatioOfDirection export type RatioOfDirection = { x: number y: number } export interface IndicatorOptions { interactive?: boolean ratio?: Ratio relationElementHandleElementIndex?: number relationElement: HTMLElement } export const enum Direction { Vertical = 'vertical', Horizontal = 'horizontal', } export type Postion = { x: number y: number } export const enum ValueSign { Positive = -1, NotPositive = 1, } ================================================ FILE: packages/infinity/README.md ================================================ # @better-scroll/infinity [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/infinity/README_zh-CN.md) The ability to inject a infinity load for BetterScroll. ## Usage ```js import BScroll from '@better-scroll/core' import InfinityScroll from '@better-scroll/infinity' BScroll.use(InfinityScroll) const bs = new BScroll('.wrapper', { infinity: { fetch(count) { // Fetch data that is larger than count, the function is asynchronous, and it needs to return a Promise.。 // After you have successfully fetch the data, you need resolve an array of data (or resolve Promise). // Each element of the array is list data, which will be rendered when the render method executes。 // If there is no data, you can resolve (false) to tell the infinite scroll list that there is no more data。 } render(item, div) { // Rendering each element node, item is data, and div is a container for wrapping element nodes. // The function needs to return to the rendered DOM node. }, createTombstone() { // Returns a tombstone DOM node.。 } } }) ``` ================================================ FILE: packages/infinity/README_zh-CN.md ================================================ # @better-scroll/infinity 为 BetterScroll 注入上拉加载的能力。 ## 使用 ```js import BScroll from '@better-scroll/core' import InfinityScroll from '@better-scroll/infinity' BScroll.use(InfinityScroll) const bs = new BScroll('.wrapper', { infinity: { fetch(count) { // 获取大于 count 数量的数据,该函数是异步的,它需要返回一个 Promise。 // 成功获取数据后,你需要 resolve 数据数组(也可以 resolve 一个 Promise)。 // 数组的每一个元素是列表数据,在 render 方法执行的时候会传递这个数据渲染。 // 如果没有数据的时候,你可以 resolve(false),来告诉无限滚动列表已经没有更多数据了。 } render(item, div) { // 渲染每一个元素节点,item 是数据,div 是包裹元素节点的容器。 // 该函数需要返回渲染后的 DOM 节点。 }, createTombstone() { // 返回一个墓碑 DOM 节点。 } } }) ``` ================================================ FILE: packages/infinity/package.json ================================================ { "name": "@better-scroll/infinity", "version": "2.5.1", "description": "The ability to inject a infinity load for BetterScroll.", "author": "fengweiyao ", "main": "dist/infinity.min.js", "module": "dist/infinity.esm.js", "typings": "dist/types/index.d.ts", "publishConfig": { "access": "public" }, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios", "infinity" ], "repository": { "type": "git", "url": "git+ssh://git@github.com/ustbhuangyi/better-scroll.git", "directory": "packages/infinity" }, "dependencies": { "@better-scroll/core": "^2.5.1" }, "gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130" } ================================================ FILE: packages/infinity/src/DataManager.ts ================================================ class ListItem { data: any | null dom: HTMLElement | null tombstone: HTMLElement | null width: number height: number pos: number constructor() { this.data = null this.dom = null this.tombstone = null this.width = 0 this.height = 0 this.pos = 0 } } export type pListItem = Partial export default class DataManager { public loadedNum = 0 private fetching = false private hasMore = true private list: Array constructor( list: Array, private fetchFn: (len: number) => Promise | boolean>, private onFetchFinish: (list: Array, hasMore: boolean) => number ) { this.list = list || [] } async update(end: number): Promise { if (!this.hasMore) { end = Math.min(end, this.list.length) } // add data placeholder if (end > this.list.length) { const len = end - this.list.length this.addEmptyData(len) } // tslint:disable-next-line: no-floating-promises return this.checkToFetch(end) } add(data: Array): Array { for (let i = 0; i < data.length; i++) { if (!this.list[this.loadedNum]) { this.list[this.loadedNum] = { data: data[i] } } else { this.list[this.loadedNum] = { ...this.list[this.loadedNum], ...{ data: data[i] }, } } this.loadedNum++ } return this.list } addEmptyData(len: number): Array { for (let i = 0; i < len; i++) { this.list.push(new ListItem()) } return this.list } async fetch(len: number): Promise | boolean> { if (this.fetching) { return [] } this.fetching = true const data = await this.fetchFn(len) this.fetching = false return data } async checkToFetch(end: number): Promise { if (!this.hasMore) { return } if (end <= this.loadedNum) { return } const min = end - this.loadedNum const newData = await this.fetch(min) if (newData instanceof Array && newData.length) { this.add(newData) const currentEnd = this.onFetchFinish(this.list, true) return this.checkToFetch(currentEnd) } else if (typeof newData === 'boolean' && newData === false) { this.hasMore = false this.list.splice(this.loadedNum) this.onFetchFinish(this.list, false) } } getList() { return this.list } resetState() { this.loadedNum = 0 this.fetching = false this.hasMore = true this.list = [] } } ================================================ FILE: packages/infinity/src/DomManager.ts ================================================ import { pListItem } from './DataManager' import Tombstone from './Tombstone' import { style, cssVendor } from '@better-scroll/shared-utils' const ANIMATION_DURATION_MS = 200 export default class DomManager { private content: HTMLElement private unusedDom: HTMLElement[] = [] private timers: Array = [] constructor( content: HTMLElement, private renderFn: (data: any, div?: HTMLElement) => HTMLElement, private tombstone: Tombstone ) { this.setContent(content) } update( list: Array, start: number, end: number ): { start: number end: number startPos: number startDelta: number endPos: number } { if (start >= list.length) { start = list.length - 1 } if (end > list.length) { end = list.length } this.collectUnusedDom(list, start, end) this.createDom(list, start, end) this.cacheHeight(list, start, end) const { startPos, startDelta, endPos } = this.positionDom(list, start, end) return { start, startPos, startDelta, end, endPos, } } private collectUnusedDom( list: Array, start: number, end: number ): Array { // TODO optimise for (let i = 0; i < list.length; i++) { if (i === start) { i = end - 1 continue } if (list[i].dom) { const dom = list[i].dom as HTMLElement if (Tombstone.isTombstone(dom)) { this.tombstone.recycleOne(dom) dom.style.display = 'none' } else { this.unusedDom.push(dom) } list[i].dom = null } } return list } private createDom(list: Array, start: number, end: number): void { for (let i = start; i < end; i++) { let dom = list[i].dom const data = list[i].data if (dom) { if (Tombstone.isTombstone(dom) && data) { list[i].tombstone = dom list[i].dom = null } else { continue } } dom = data ? this.renderFn(data, this.unusedDom.pop()) : this.tombstone.getOne() dom.style.position = 'absolute' list[i].dom = dom list[i].pos = -1 this.content.appendChild(dom) } } private cacheHeight( list: Array, start: number, end: number ): void { for (let i = start; i < end; i++) { if (list[i].data && !list[i].height) { list[i].height = list[i].dom!.offsetHeight } } } private positionDom( list: Array, start: number, end: number ): { startPos: number; startDelta: number; endPos: number } { const tombstoneEles: Array = [] const { start: startPos, delta: startDelta } = this.getStartPos( list, start, end ) let pos = startPos for (let i = start; i < end; i++) { const tombstone = list[i].tombstone if (tombstone) { const tombstoneStyle = tombstone.style as any tombstoneStyle[ style.transition ] = `${cssVendor}transform ${ANIMATION_DURATION_MS}ms, opacity ${ANIMATION_DURATION_MS}ms` tombstoneStyle[style.transform] = `translateY(${pos}px)` tombstoneStyle.opacity = '0' list[i].tombstone = null tombstoneEles.push(tombstone) } if (list[i].dom && list[i].pos !== pos) { list[i].dom!.style[style.transform as any] = `translateY(${pos}px)` list[i].pos = pos } pos += list[i].height || this.tombstone.height } const timerId = window.setTimeout(() => { this.tombstone.recycle(tombstoneEles) }, ANIMATION_DURATION_MS) this.timers.push(timerId) return { startPos, startDelta, endPos: pos, } } private getStartPos( list: Array, start: number, end: number ): { start: number; delta: number } { if (list[start] && list[start].pos !== -1) { return { start: list[start].pos, delta: 0, } } // TODO optimise let pos = list[0].pos === -1 ? 0 : list[0].pos for (let i = 0; i < start; i++) { pos += list[i].height || this.tombstone.height } let originPos = pos let i for (i = start; i < end; i++) { if (!Tombstone.isTombstone(list[i].dom) && list[i].pos !== -1) { pos = list[i].pos break } } let x = i if (x < end) { while (x > start) { pos -= list[x - 1].height x-- } } const delta = originPos - pos return { start: pos, delta: delta, } } removeTombstone(): void { const tombstones = this.content.querySelectorAll('.tombstone') for (let i = tombstones.length - 1; i >= 0; i--) { this.content.removeChild(tombstones[i]) } } setContent(content: HTMLElement) { if (content !== this.content) { this.content = content } } destroy(): void { this.removeTombstone() this.timers.forEach((id) => { clearTimeout(id) }) } resetState() { this.destroy() this.timers = [] this.unusedDom = [] } } ================================================ FILE: packages/infinity/src/IndexCalculator.ts ================================================ export const PRE_NUM = 10 export const POST_NUM = 30 const enum DIRECTION { UP, DOWN, } export default class IndexCalculator { private lastDirection = DIRECTION.DOWN private lastPos = 0 constructor(public wrapperHeight: number, private tombstoneHeight: number) {} calculate(pos: number, list: Array): { start: number; end: number } { let offset = pos - this.lastPos this.lastPos = pos const direction = this.getDirection(offset) // important! start index is much more important than end index. let start = this.calculateIndex(0, pos, list) let end = this.calculateIndex(start, pos + this.wrapperHeight, list) if (direction === DIRECTION.DOWN) { start -= PRE_NUM end += POST_NUM } else { start -= POST_NUM end += PRE_NUM } if (start < 0) { start = 0 } return { start, end, } } private getDirection(offset: number): DIRECTION { let direction if (offset > 0) { direction = DIRECTION.DOWN } else if (offset < 0) { direction = DIRECTION.UP } else { return this.lastDirection } this.lastDirection = direction return direction } private calculateIndex( start: number, offset: number, list: Array ): number { if (offset <= 0) { return start } let i = start let startPos = list[i] && list[i].pos !== -1 ? list[i].pos : 0 let lastPos = startPos let tombstone = 0 while (i < list.length && list[i].pos < offset) { lastPos = list[i].pos i++ } if (i === list.length) { tombstone = Math.floor((offset - lastPos) / this.tombstoneHeight) } i += tombstone return i } resetState() { this.lastDirection = DIRECTION.DOWN this.lastPos = 0 } } ================================================ FILE: packages/infinity/src/Tombstone.ts ================================================ import { style } from '@better-scroll/shared-utils' export default class Tombstone { private cached: Array = [] public width = 0 public height = 0 private initialed = false constructor(private create: () => HTMLElement) { this.getSize() } static isTombstone(el: HTMLElement): boolean { if (el && el.classList) { return el.classList.contains('tombstone') } return false } private getSize(): void { if (!this.initialed) { let tombstone = this.create() tombstone.style.position = 'absolute' document.body.appendChild(tombstone) tombstone.style.display = '' this.height = tombstone.offsetHeight this.width = tombstone.offsetWidth document.body.removeChild(tombstone) this.cached.push(tombstone) } } getOne(): HTMLElement { let tombstone = this.cached.pop() if (tombstone) { const tombstoneStyle = tombstone.style as any tombstoneStyle.display = '' tombstoneStyle.opacity = '1' tombstoneStyle[style.transform] = '' tombstoneStyle[style.transition] = '' return tombstone } return this.create() } recycle(tombstones: Array): Array { for (let tombstone of tombstones) { tombstone.style.display = 'none' this.cached.push(tombstone) } return this.cached } recycleOne(tombstone: HTMLElement) { this.cached.push(tombstone) return this.cached } } ================================================ FILE: packages/infinity/src/__tests__/DataManager.spec.ts ================================================ import DataManager from '../DataManager' describe('DataManager unit test', () => { const NEW_LEN = 10 let list: Array let dataManager: DataManager let fetchFn = jest.fn() let onFetchFinish = jest.fn().mockReturnValue(0) beforeEach(() => { list = [] dataManager = new DataManager(list, fetchFn, onFetchFinish) }) afterEach(() => { jest.clearAllMocks() }) it('should fetch new data when loadedNum < list.length', () => { // given let end = NEW_LEN const DATA = 1 const newData = new Array(NEW_LEN).fill(DATA) fetchFn.mockReturnValue(Promise.resolve(newData)) // when // tslint:disable-next-line: no-floating-promises dataManager.update(end) // then // create empty data firstly, and then, go to fetch data expect(dataManager.getList().length).not.toBeLessThan(NEW_LEN) expect(fetchFn).toBeCalledWith(NEW_LEN) }) it('should call onFetchFinish with hasMore equal true when loaded new data', async () => { // given const start = 0 const end = NEW_LEN const DATA = 1 const newData = new Array(NEW_LEN).fill(DATA) fetchFn.mockReturnValue(Promise.resolve(newData)) // when await dataManager.update(end) // then expect(onFetchFinish).toBeCalledWith(dataManager.getList(), true) }) it('should call onFetchFinish with hasMore equal false when no more data', async () => { // given const start = 0 const end = NEW_LEN fetchFn.mockReturnValue(Promise.resolve(false)) // when await dataManager.update(end) // then expect(onFetchFinish).toBeCalledWith(dataManager.getList(), false) }) }) ================================================ FILE: packages/infinity/src/__tests__/IndexCalculator.spec.ts ================================================ import IndexCalculator, { PRE_NUM, POST_NUM } from '../IndexCalculator' import FakeList from './__utils__/FakeList' import { WRAPPER_HEIGHT, TOMBSTONE_HEIGHT } from './__utils__/constans' describe('IndexCalculator unit test', () => { let indexCalculator: IndexCalculator const VISIBLE_CNT = WRAPPER_HEIGHT / TOMBSTONE_HEIGHT beforeEach(() => { indexCalculator = new IndexCalculator(WRAPPER_HEIGHT, TOMBSTONE_HEIGHT) }) it('should get start equal to 0 when pos is 0', () => { // given const list = new FakeList(0).getList() // when const { start, end } = indexCalculator.calculate(0, list) // then expect(start).toBe(0) expect(end).toBe(VISIBLE_CNT + POST_NUM) }) it('should get start equal to 0 when first rendered index < PRE_NUM', () => { // given const list = new FakeList(100) .fillDom(0) .fillPos() .getList() const INDEX = 5 const POSITION = INDEX * TOMBSTONE_HEIGHT // when const { start, end } = indexCalculator.calculate(POSITION, list) // then expect(start).toBe(0) }) it('should get start equal to index-PRE_NUM when first visilbe index > PRE_NUM', () => { // given const list = new FakeList(100) .fillDom(0) .fillPos() .getList() const INDEX = 15 const POSITION = INDEX * TOMBSTONE_HEIGHT // when const { start } = indexCalculator.calculate(POSITION, list) // then expect(start).toBe(INDEX - PRE_NUM) }) it('should get correct val when scroll up and down', () => { // given const list = new FakeList(100) .fillDom(0) .fillPos() .getList() const FIRST_INDEX = 50 const FIRST_POSITION = FIRST_INDEX * TOMBSTONE_HEIGHT const SECOND_INDEX = 40 const SECOND_POSITION = SECOND_INDEX * TOMBSTONE_HEIGHT // when indexCalculator.calculate(FIRST_POSITION, list) const { start } = indexCalculator.calculate(SECOND_POSITION, list) // then expect(start).toBe(SECOND_INDEX - POST_NUM) }) }) ================================================ FILE: packages/infinity/src/__tests__/__utils__/FakeList.ts ================================================ import { mockDomOffset } from '@better-scroll/core/src/__tests__/__utils__/layout' import { TOMBSTONE_HEIGHT } from './constans' export default class FakeList { private list: any[] constructor(size: number) { this.list = Array.from({ length: size }).map(i => { return {} }) } fill(val: any, start: number = 0, end?: number): this { if (!end) { end = this.list.length } for (let i = start; i < end; i++) { Object.assign(this.list[i], val) } return this } fillPos(end?: number): this { if (!end) { end = this.list.length } let startPos = 0 for (let i = 0; i < end; i++) { Object.assign(this.list[i], { pos: startPos }) const height = this.list[i].height startPos += height } return this } fillDom(start: number, end?: number, height = TOMBSTONE_HEIGHT): this { if (!end) { end = this.list.length } for (let i = start; i < end; i++) { const dom = document.createElement('div') mockDomOffset(dom, { height }) Object.assign(this.list[i], { dom, height, pos: -1 }) } return this } syncDomTo(content: HTMLElement): this { for (let i = 0; i < this.list.length; i++) { if (this.list[i].dom) { content.appendChild(this.list[i].dom) } } return this } getList(): any[] { return this.list } } ================================================ FILE: packages/infinity/src/__tests__/__utils__/constans.ts ================================================ const TOMBSTONE_HEIGHT = 37 // 元素默认的高度 const WRAPPER_HEIGHT = 370 // 滚动窗口的高度 const VISIBLE_CNT = 10 // 滚动窗口内能显示的元素 export { TOMBSTONE_HEIGHT, WRAPPER_HEIGHT, VISIBLE_CNT } ================================================ FILE: packages/infinity/src/index.ts ================================================ import BScroll, { Boundary } from '@better-scroll/core' import { Probe, warn } from '@better-scroll/shared-utils' import IndexCalculator from './IndexCalculator' import DataManager from './DataManager' import DomManager from './DomManager' import Tombstone from './Tombstone' export interface InfinityOptions { fetch: (count: number) => Promise | false> render: (item: any, div?: HTMLElement) => HTMLElement createTombstone: () => HTMLElement } declare module '@better-scroll/core' { interface CustomOptions { infinity?: InfinityOptions } } const EXTRA_SCROLL_Y = -2000 export default class InfinityScroll { static pluginName = 'infinity' start: number = 0 end: number = 0 options: InfinityOptions private tombstone: Tombstone private domManager: DomManager private dataManager: DataManager private indexCalculator: IndexCalculator constructor(public scroll: BScroll) { this.init() } init() { this.handleOptions() const { fetch: fetchFn, render: renderFn, createTombstone: createTombstoneFn, } = this.options this.tombstone = new Tombstone(createTombstoneFn) this.indexCalculator = new IndexCalculator( this.scroll.scroller.scrollBehaviorY.wrapperSize, this.tombstone.height ) this.domManager = new DomManager( this.scroll.scroller.content, renderFn, this.tombstone ) this.dataManager = new DataManager( [], fetchFn, this.onFetchFinish.bind(this) ) this.scroll.on(this.scroll.eventTypes.destroy, this.destroy, this) this.scroll.on(this.scroll.eventTypes.scroll, this.update, this) this.scroll.on( this.scroll.eventTypes.contentChanged, (content: HTMLElement) => { this.domManager.setContent(content) this.indexCalculator.resetState() this.domManager.resetState() this.dataManager.resetState() this.update({ y: 0 }) } ) const { scrollBehaviorY } = this.scroll.scroller scrollBehaviorY.hooks.on( scrollBehaviorY.hooks.eventTypes.computeBoundary, this.modifyBoundary, this ) this.update({ y: 0 }) } private modifyBoundary(boundary: Boundary) { // manually set position to allow scroll boundary.maxScrollPos = EXTRA_SCROLL_Y } private handleOptions() { // narrow down type to an object const infinityOptions = this.scroll.options.infinity if (infinityOptions) { if (typeof infinityOptions.fetch !== 'function') { warn('Infinity plugin need fetch Function to new data.') } if (typeof infinityOptions.render !== 'function') { warn('Infinity plugin need render Function to render each item.') } if (typeof infinityOptions.render !== 'function') { warn( 'Infinity plugin need createTombstone Function to create tombstone.' ) } this.options = infinityOptions } this.scroll.options.probeType = Probe.Realtime } update(pos: { y: number }): void { const position = Math.round(-pos.y) // important! calculate start/end index to render const { start, end } = this.indexCalculator.calculate( position, this.dataManager.getList() ) this.start = start this.end = end // tslint:disable-next-line: no-floating-promises this.dataManager.update(end) this.updateDom(this.dataManager.getList()) } private onFetchFinish(list: Array, hasMore: boolean) { const { end } = this.updateDom(list) if (!hasMore) { this.domManager.removeTombstone() this.scroll.scroller.animater.stop() this.scroll.resetPosition() } // tslint:disable-next-line: no-floating-promises return end } private updateDom( list: Array ): { end: number; startPos: number; endPos: number } { const { end, startPos, endPos, startDelta } = this.domManager.update( list, this.start, this.end ) if (startDelta) { this.scroll.minScrollY = startDelta } if (endPos > this.scroll.maxScrollY) { this.scroll.maxScrollY = -( endPos - this.scroll.scroller.scrollBehaviorY.wrapperSize ) } return { end, startPos, endPos, } } destroy() { const { content, scrollBehaviorY } = this.scroll.scroller while (content.firstChild) { content.removeChild(content.firstChild) } this.domManager.destroy() this.scroll.off('scroll', this.update) this.scroll.off('destroy', this.destroy) scrollBehaviorY.hooks.off(scrollBehaviorY.hooks.eventTypes.computeBoundary) } } ================================================ FILE: packages/mouse-wheel/README.md ================================================ # @better-scroll/mouse-wheel [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/mouse-wheel/README_zh-CN.md) Allow the mouse wheel to manipulate scrolling behavior. ## Usage ```js import BScroll from '@better-scroll/core' import MouseWheel from '@better-scroll/mouse-wheel' BScroll.use(MouseWheel) const bs = new BScroll('.wrapper', { // ... mouseWheel: { speed: 2, invert: false, easeTime: 300, } }) ``` ================================================ FILE: packages/mouse-wheel/README_zh-CN.md ================================================ # @better-scroll/mouse-wheel 允许鼠标滚轮来操纵滚动行为。 ## 使用 ```js import BScroll from '@better-scroll/core' import MouseWheel from '@better-scroll/mouse-wheel' BScroll.use(MouseWheel) const bs = new BScroll('.wrapper', { // ... mouseWheel: { speed: 2, invert: false, easeTime: 300, } }) ``` ================================================ FILE: packages/mouse-wheel/package.json ================================================ { "name": "@better-scroll/mouse-wheel", "version": "2.5.1", "description": "support for MouseWheel in PC", "author": { "name": "jizhi", "email": "theniceangel@163.com" }, "main": "dist/mouse-wheel.min.js", "module": "dist/mouse-wheel.esm.js", "typings": "dist/types/index.d.ts", "publishConfig": { "access": "public" }, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios" ], "license": "MIT", "repository": { "type": "git", "url": "git+ssh://git@github.com/ustbhuangyi/better-scroll.git", "directory": "packages/mouse-wheel" }, "dependencies": { "@better-scroll/core": "^2.5.1" }, "gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130" } ================================================ FILE: packages/mouse-wheel/src/__tests__/index.spec.ts ================================================ import BScroll from '@better-scroll/core' jest.mock('@better-scroll/core') import MouseWheel from '../index' import { createEvent } from '@better-scroll/core/src/__tests__/__utils__/event' interface CustomMouseWheel extends Event { deltaX: number deltaY: number pageX: number pageY: number [key: string]: any } const createMouseWheelElements = () => { const wrapper = document.createElement('div') const content = document.createElement('div') wrapper.appendChild(content) return { wrapper } } function dispatchMouseWheel( target: EventTarget, name = 'wheel', data: { [key: string]: any } = {} ): void { const event = createEvent('', name) Object.assign(event, data) target.dispatchEvent(event) } describe('mouse-wheel plugin', () => { const DISCRETE_TIME = 400 let scroll: BScroll let mouseWheel: MouseWheel jest.useFakeTimers() beforeEach(() => { const { wrapper } = createMouseWheelElements() scroll = new BScroll(wrapper, { stopPropagation: true, }) scroll.scroller.scrollBehaviorX.performDampingAlgorithm = jest .fn() .mockImplementation((arg1) => { return arg1 }) scroll.scroller.scrollBehaviorY.performDampingAlgorithm = jest .fn() .mockImplementation((arg1) => { return arg1 }) mouseWheel = new MouseWheel(scroll) }) afterEach(() => { jest.clearAllMocks() jest.clearAllTimers() }) it('should proxy hooks to BScroll instance', () => { expect(scroll.registerType).toHaveBeenCalledWith([ 'alterOptions', 'mousewheelStart', 'mousewheelMove', 'mousewheelEnd', ]) }) it('should handle default options and user options', () => { // case 1 scroll.options.mouseWheel = true mouseWheel = new MouseWheel(scroll) expect(mouseWheel.mouseWheelOpt).toMatchObject({ speed: 20, invert: false, easeTime: 300, discreteTime: 400, throttleTime: 0, dampingFactor: 0.1, }) // case 2 scroll.options.mouseWheel = { dampingFactor: 1, throttleTime: 50, } mouseWheel = new MouseWheel(scroll) expect(mouseWheel.mouseWheelOpt).toMatchObject({ speed: 20, invert: false, easeTime: 300, discreteTime: 400, throttleTime: 50, dampingFactor: 1, }) }) it('should trigger mousewheel(start|move|end) when moved ', () => { const onStart = jest.fn() const onMove = jest.fn() const onEnd = jest.fn() const onAlterOptions = jest.fn() scroll.on('mousewheelStart', onStart) scroll.on('mousewheelMove', onMove) scroll.on('mousewheelEnd', onEnd) scroll.on('alterOptions', onAlterOptions) dispatchMouseWheel(scroll.wrapper, 'wheel') expect(onStart).toBeCalledTimes(1) expect(onMove).toBeCalledTimes(1) expect(onAlterOptions).toBeCalledTimes(1) dispatchMouseWheel(scroll.wrapper, 'wheel') jest.advanceTimersByTime(DISCRETE_TIME) expect(onMove).toBeCalledTimes(2) expect(onEnd).toBeCalledTimes(1) // forbid moving scroll.enabled = false dispatchMouseWheel(scroll.wrapper, 'wheel') expect(onStart).toBeCalledTimes(1) expect(onMove).toBeCalledTimes(2) expect(onEnd).toBeCalledTimes(1) }) it('should support throttle when throttleTime > 0', () => { mouseWheel.mouseWheelOpt.throttleTime = 50 dispatchMouseWheel(scroll.wrapper, 'wheel', { deltaX: 0, deltaY: 10, }) dispatchMouseWheel(scroll.wrapper, 'wheel', { deltaX: 0, deltaY: 20, }) jest.advanceTimersByTime(51) dispatchMouseWheel(scroll.wrapper, 'wheel', { deltaX: 0, deltaY: 30, }) expect(scroll.scrollTo).toBeCalledTimes(2) }) it('should warn when easeTime is invalid', () => { const spyFn = jest.spyOn(console, 'error') mouseWheel.mouseWheelOpt.easeTime = 50 dispatchMouseWheel(scroll.wrapper, 'wheel') expect(spyFn).toBeCalled() }) it('should preventDefault & stopProgation if they are set', () => { const mockStopPropagation = jest.fn() const mockPreventDefault = jest.fn() dispatchMouseWheel(scroll.wrapper, 'wheel', { preventDefault: mockPreventDefault, stopPropagation: mockStopPropagation, }) expect(mockPreventDefault).toBeCalled() expect(mockStopPropagation).not.toBe(0) jest.advanceTimersByTime(400) mockPreventDefault.mockClear() mockStopPropagation.mockClear() // preventDefaultException work scroll.options.preventDefaultException = { tagName: /^(DIV)$/, } dispatchMouseWheel(scroll.wrapper, 'wheel', { preventDefault: mockPreventDefault, stopPropagation: mockStopPropagation, }) expect(mockPreventDefault).not.toBeCalled() }) it('should forbid scrollTo when mousewheelMove hook return true', () => { const onStart = jest.fn() const onMove = jest.fn().mockImplementation(() => { return true }) const onEnd = jest.fn() scroll.on('mousewheelStart', onStart) scroll.on('mousewheelMove', onMove) scroll.on('mousewheelEnd', onEnd) dispatchMouseWheel(scroll.wrapper, 'wheel') expect(onStart).toBeCalledTimes(1) expect(onMove).toBeCalledTimes(1) expect(scroll.scrollTo).not.toBeCalled() }) it('should get right postion when move with deltaMode = 0', () => { const onEnd = jest.fn() scroll.on('mousewheelEnd', onEnd) // x direction scroll.hasVerticalScroll = false scroll.hasHorizontalScroll = true dispatchMouseWheel(scroll.wrapper, 'wheel', { deltaX: 0, deltaY: 10, deltaMode: 0, }) expect(scroll.scrollTo).toBeCalledWith(-10, 0, 300) jest.advanceTimersByTime(410) expect(onEnd).toBeCalledWith({ x: -10, y: 0, directionX: 1, directionY: 0, }) // y direction scroll.hasVerticalScroll = true scroll.hasHorizontalScroll = false dispatchMouseWheel(scroll.wrapper, 'wheel', { deltaX: 0, deltaY: 10, deltaMode: 0, }) expect(scroll.scrollTo).toBeCalledWith(0, -10, 300) jest.advanceTimersByTime(410) expect(onEnd).toBeCalledWith({ x: 0, y: -10, directionX: 0, directionY: 1, }) }) it('should get right postion when move with deltaMode = 1', () => { // x direction scroll.hasVerticalScroll = false scroll.hasHorizontalScroll = true dispatchMouseWheel(scroll.wrapper, 'wheel', { deltaX: 0, deltaY: 2, deltaMode: 1, }) expect(scroll.scrollTo).toBeCalledWith(-40, 0, 300) // y direction scroll.hasVerticalScroll = true scroll.hasHorizontalScroll = false dispatchMouseWheel(scroll.wrapper, 'wheel', { deltaX: 0, deltaY: 2, deltaMode: 1, }) expect(scroll.scrollTo).toBeCalledWith(0, -40, 300) }) it('should get right postion when move with wheelDeltaX and wheelDeltaY', () => { scroll.hasVerticalScroll = true scroll.hasHorizontalScroll = true dispatchMouseWheel(scroll.wrapper, 'wheel', { wheelDeltaX: -120, wheelDeltaY: -240, deltaMode: 0, }) expect(scroll.scrollTo).toBeCalledWith(-20, -40, 300) }) it('should get right postion when move with wheelDelta', () => { scroll.hasVerticalScroll = true scroll.hasHorizontalScroll = true dispatchMouseWheel(scroll.wrapper, 'wheel', { wheelDelta: -120, deltaMode: 0, }) expect(scroll.scrollTo).toBeCalledWith(-20, -20, 300) }) it('should get right postion when move with detail', () => { scroll.hasVerticalScroll = true scroll.hasHorizontalScroll = true dispatchMouseWheel(scroll.wrapper, 'wheel', { detail: 60, deltaMode: 0, }) expect(scroll.scrollTo).toBeCalledWith(-400, -400, 300) }) it('should get right postion when move with invert = true', () => { // x direction scroll.hasVerticalScroll = false scroll.hasHorizontalScroll = true mouseWheel.mouseWheelOpt.invert = true dispatchMouseWheel(scroll.wrapper, 'wheel', { deltaX: 0, deltaY: 2, deltaMode: 1, }) expect(scroll.scrollTo).toBeCalledWith(40, 0, 300) }) it('should work with dampingFactor', () => { mouseWheel.mouseWheelOpt.dampingFactor = 0.1 scroll.scroller.scrollBehaviorX.performDampingAlgorithm = jest .fn() .mockImplementation((distance, factor) => { return distance * factor }) scroll.scroller.scrollBehaviorY.performDampingAlgorithm = jest .fn() .mockImplementation((distance, factor) => { return distance * factor }) dispatchMouseWheel(scroll.wrapper, 'wheel', { deltaX: 0, deltaY: 2, deltaMode: 1, }) expect(scroll.scrollTo).toBeCalledWith(0, -4, 300) // improve coverage mouseWheel.destroy() }) }) ================================================ FILE: packages/mouse-wheel/src/index.ts ================================================ import BScroll from '@better-scroll/core' import { warn, preventDefaultExceptionFn, EventRegister, EventEmitter, Direction, ApplyOrder, extend, maybePrevent, } from '@better-scroll/shared-utils' export type MouseWheelOptions = Partial | true export interface MouseWheelConfig { speed: number invert: boolean easeTime: number discreteTime: number throttleTime: number dampingFactor: number } declare module '@better-scroll/core' { interface CustomOptions { mouseWheel?: MouseWheelOptions } } interface CompatibleWheelEvent extends WheelEvent { pageX: number pageY: number readonly wheelDeltaX: number readonly wheelDeltaY: number readonly wheelDelta: number } interface WheelDelta { x: number y: number directionX: Direction directionY: Direction } export default class MouseWheel { static pluginName = 'mouseWheel' static applyOrder = ApplyOrder.Pre mouseWheelOpt: MouseWheelConfig private eventRegister: EventRegister private wheelEndTimer: number = 0 private wheelMoveTimer: number = 0 private wheelStart = false private deltaCache: { x: number; y: number }[] private hooksFn: Array<[EventEmitter, string, Function]> constructor(public scroll: BScroll) { this.init() } private init() { this.handleBScroll() this.handleOptions() this.handleHooks() this.registerEvent() } private handleBScroll() { this.scroll.registerType([ 'alterOptions', 'mousewheelStart', 'mousewheelMove', 'mousewheelEnd', ]) } private handleOptions() { const userOptions = ( this.scroll.options.mouseWheel === true ? {} : this.scroll.options.mouseWheel ) as Partial const defaultOptions: MouseWheelConfig = { speed: 20, invert: false, easeTime: 300, discreteTime: 400, throttleTime: 0, dampingFactor: 0.1, } this.mouseWheelOpt = extend(defaultOptions, userOptions) } private handleHooks() { this.hooksFn = [] this.registerHooks(this.scroll.hooks, 'destroy', this.destroy) } private registerEvent() { this.eventRegister = new EventRegister(this.scroll.scroller.wrapper, [ { name: 'wheel', handler: this.wheelHandler.bind(this), }, { name: 'mousewheel', handler: this.wheelHandler.bind(this), }, { name: 'DOMMouseScroll', // FireFox handler: this.wheelHandler.bind(this), }, ]) } private registerHooks(hooks: EventEmitter, name: string, handler: Function) { hooks.on(name, handler, this) this.hooksFn.push([hooks, name, handler]) } private wheelHandler(e: CompatibleWheelEvent) { if (!this.scroll.enabled) { return } this.beforeHandler(e) // start if (!this.wheelStart) { this.wheelStartHandler(e) this.wheelStart = true } // move const delta = this.getWheelDelta(e) this.wheelMoveHandler(delta) // end this.wheelEndDetector(delta) } private wheelStartHandler(e: CompatibleWheelEvent) { this.cleanCache() const { scrollBehaviorX, scrollBehaviorY } = this.scroll.scroller scrollBehaviorX.setMovingDirection(Direction.Default) scrollBehaviorY.setMovingDirection(Direction.Default) scrollBehaviorX.setDirection(Direction.Default) scrollBehaviorY.setDirection(Direction.Default) this.scroll.trigger(this.scroll.eventTypes.alterOptions, this.mouseWheelOpt) this.scroll.trigger(this.scroll.eventTypes.mousewheelStart) } private cleanCache() { this.deltaCache = [] } private wheelMoveHandler(delta: { x: number y: number directionX: number directionY: number }) { const { throttleTime, dampingFactor } = this.mouseWheelOpt if (throttleTime && this.wheelMoveTimer) { this.deltaCache.push(delta) } else { const cachedDelta = this.deltaCache.reduce( (prev, current) => { return { x: prev.x + current.x, y: prev.y + current.y, } }, { x: 0, y: 0 } ) this.cleanCache() const { scrollBehaviorX, scrollBehaviorY } = this.scroll.scroller scrollBehaviorX.setMovingDirection(-delta.directionX) scrollBehaviorY.setMovingDirection(-delta.directionY) scrollBehaviorX.setDirection(delta.x) scrollBehaviorY.setDirection(delta.y) // when out of boundary, perform a damping scroll const newX = scrollBehaviorX.performDampingAlgorithm( Math.round(delta.x) + cachedDelta.x, dampingFactor ) const newY = scrollBehaviorY.performDampingAlgorithm( Math.round(delta.y) + cachedDelta.x, dampingFactor ) if ( !this.scroll.trigger(this.scroll.eventTypes.mousewheelMove, { x: newX, y: newY, }) ) { const easeTime = this.getEaseTime() if (newX !== this.scroll.x || newY !== this.scroll.y) { this.scroll.scrollTo(newX, newY, easeTime) } } if (throttleTime) { this.wheelMoveTimer = window.setTimeout(() => { this.wheelMoveTimer = 0 }, throttleTime) } } } private wheelEndDetector(delta: WheelDelta) { window.clearTimeout(this.wheelEndTimer) this.wheelEndTimer = window.setTimeout(() => { this.wheelStart = false window.clearTimeout(this.wheelMoveTimer) this.wheelMoveTimer = 0 this.scroll.trigger(this.scroll.eventTypes.mousewheelEnd, delta) }, this.mouseWheelOpt.discreteTime) } private getWheelDelta(e: CompatibleWheelEvent): WheelDelta { const { speed, invert } = this.mouseWheelOpt let wheelDeltaX = 0 let wheelDeltaY = 0 let direction = invert ? Direction.Negative : Direction.Positive switch (true) { case 'deltaX' in e: if (e.deltaMode === 1) { wheelDeltaX = -e.deltaX * speed wheelDeltaY = -e.deltaY * speed } else { wheelDeltaX = -e.deltaX wheelDeltaY = -e.deltaY } break case 'wheelDeltaX' in e: wheelDeltaX = (e.wheelDeltaX / 120) * speed wheelDeltaY = (e.wheelDeltaY / 120) * speed break case 'wheelDelta' in e: wheelDeltaX = wheelDeltaY = (e.wheelDelta / 120) * speed break case 'detail' in e: wheelDeltaX = wheelDeltaY = (-e.detail / 3) * speed break } wheelDeltaX *= direction wheelDeltaY *= direction if (!this.scroll.hasVerticalScroll) { if (Math.abs(wheelDeltaY) > Math.abs(wheelDeltaX)) { wheelDeltaX = wheelDeltaY } wheelDeltaY = 0 } if (!this.scroll.hasHorizontalScroll) { wheelDeltaX = 0 } const directionX = wheelDeltaX > 0 ? Direction.Negative : wheelDeltaX < 0 ? Direction.Positive : Direction.Default const directionY = wheelDeltaY > 0 ? Direction.Negative : wheelDeltaY < 0 ? Direction.Positive : Direction.Default return { x: wheelDeltaX, y: wheelDeltaY, directionX, directionY, } } private beforeHandler(e: CompatibleWheelEvent) { const { preventDefault, stopPropagation, preventDefaultException } = this.scroll.options if ( preventDefault && !preventDefaultExceptionFn(e.target, preventDefaultException) ) { maybePrevent(e) } if (stopPropagation) { e.stopPropagation() } } private getEaseTime() { const SAFE_EASETIME = 100 const easeTime = this.mouseWheelOpt.easeTime // scrollEnd event will be triggered in every calling of scrollTo when easeTime is too small // easeTime needs to be greater than 100 if (easeTime < SAFE_EASETIME) { warn( `easeTime should be greater than 100.` + `If mouseWheel easeTime is too small,` + `scrollEnd will be triggered many times.` ) } return Math.max(easeTime, SAFE_EASETIME) } destroy() { this.eventRegister.destroy() window.clearTimeout(this.wheelEndTimer) window.clearTimeout(this.wheelMoveTimer) this.hooksFn.forEach((item) => { const hooks = item[0] const hooksName = item[1] const handlerFn = item[2] hooks.off(hooksName, handlerFn) }) } } ================================================ FILE: packages/movable/README.md ================================================ # @better-scroll/movable [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/movable/README.md) Movable area plugin. ## Usage ```js import BScroll from '@better-scroll/core' import Movable from '@better-scroll/movable' BScroll.use(Movable) const bs = new BScroll('.wrapper', { movable: true }) ``` ================================================ FILE: packages/movable/README_zh-CN.md ================================================ # @better-scroll/movable [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/movable/README_zh-CN.md) Movable 可移动区域插件。 ## 使用 ```js import BScroll from '@better-scroll/core' import Movable from '@better-scroll/movable' BScroll.use(Movable) const bs = new BScroll('.wrapper', { movable: true }) ``` ================================================ FILE: packages/movable/package.json ================================================ { "name": "@better-scroll/movable", "version": "2.5.1", "description": "Movable plugin for BetterScroll", "author": { "name": "jizhi", "email": "theniceangel@163.com" }, "main": "dist/movable.min.js", "module": "dist/movable.esm.js", "typings": "dist/types/index.d.ts", "publishConfig": { "access": "public" }, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios" ], "license": "MIT", "repository": { "type": "git", "url": "git+ssh://git@github.com/ustbhuangyi/better-scroll.git", "directory": "packages/observe-dom" }, "dependencies": { "@better-scroll/core": "^2.5.1" } } ================================================ FILE: packages/movable/src/__tests__/index.spec.ts ================================================ import BScroll, { Boundary } from '@better-scroll/core' jest.mock('@better-scroll/core') import { createDiv } from '@better-scroll/core/src/__tests__/__utils__/layout' import Movable from '../index' const createMovableEls = () => { const wrapper = createDiv(100, 100, 0, 0) const content = createDiv(100, 100, 0, 0) wrapper.appendChild(content) return { wrapper, content, } } describe('movable plugin', () => { let scroll: BScroll let movable: Movable beforeEach(() => { // create DOM const { wrapper } = createMovableEls() scroll = new BScroll(wrapper) movable = new Movable(scroll) }) afterEach(() => { jest.clearAllMocks() }) it('should proxy properties to BScroll instance', () => { expect(scroll.proxy).toBeCalled() expect(scroll.proxy).toHaveBeenLastCalledWith([ { key: 'putAt', sourceKey: 'plugins.movable.putAt', }, ]) }) it('should modify boundary', () => { const { scrollBehaviorX, scrollBehaviorY } = scroll.scroller scrollBehaviorX.options.scrollable = true scrollBehaviorY.options.scrollable = true scrollBehaviorX.wrapperSize = 200 scrollBehaviorX.contentSize = 100 scrollBehaviorY.wrapperSize = 400 scrollBehaviorY.contentSize = 200 let boundaryX: Boundary = { minScrollPos: 0, maxScrollPos: 1 } let boundaryY: Boundary = { minScrollPos: 0, maxScrollPos: 1 } scrollBehaviorX.hooks.trigger( scrollBehaviorX.hooks.eventTypes.computeBoundary, boundaryX ) scrollBehaviorY.hooks.trigger( scrollBehaviorY.hooks.eventTypes.computeBoundary, boundaryY ) expect(boundaryX).toMatchObject({ minScrollPos: 100, maxScrollPos: 0, }) expect(boundaryY).toMatchObject({ minScrollPos: 200, maxScrollPos: 0, }) }) it('should register ignoreHasScroll hook', () => { const { scrollBehaviorX, scrollBehaviorY } = scroll.scroller const retX = scrollBehaviorX.hooks.trigger( scrollBehaviorX.hooks.eventTypes.ignoreHasScroll ) const retY = scrollBehaviorY.hooks.trigger( scrollBehaviorY.hooks.eventTypes.ignoreHasScroll ) expect(retX).toBe(true) expect(retY).toBe(true) }) it('should work well when call putAt()', () => { // integer movable.putAt(20, 20) expect(scroll.scrollTo).toBeCalledWith(20, 20, 800, expect.anything()) // simulate minScrollPos scroll.scroller.scrollBehaviorX.minScrollPos = 300 scroll.scroller.scrollBehaviorY.minScrollPos = 300 // [left, bottom] movable.putAt('left', 'bottom') expect(scroll.scrollTo).toBeCalledWith(0, 300, 800, expect.anything()) // [right, top] movable.putAt('right', 'top') expect(scroll.scrollTo).toBeCalledWith(300, 0, 800, expect.anything()) // [center, center] movable.putAt('center', 'center') expect(scroll.scrollTo).toBeCalledWith(150, 150, 800, expect.anything()) }) it('should destroy all events', () => { const { scrollBehaviorX, scrollBehaviorY } = scroll.scroller scroll.hooks.trigger(scroll.hooks.eventTypes.destroy) expect(scrollBehaviorX.hooks.events['computeBoundary'].length).toBe(0) expect(scrollBehaviorX.hooks.events['ignoreHasScroll'].length).toBe(0) expect(scrollBehaviorY.hooks.events['computeBoundary'].length).toBe(0) expect(scrollBehaviorY.hooks.events['ignoreHasScroll'].length).toBe(0) }) }) ================================================ FILE: packages/movable/src/index.ts ================================================ import BScroll, { Behavior, Boundary } from '@better-scroll/core' import { ease, EventEmitter, ApplyOrder, EaseItem, } from '@better-scroll/shared-utils' import propertiesConfig from './propertiesConfig' type PositionX = number | 'left' | 'right' | 'center' type PositionY = number | 'top' | 'bottom' | 'center' declare module '@better-scroll/core' { interface CustomOptions { movable?: true } interface CustomAPI { movable: PluginAPI } } interface PluginAPI { putAt(x: PositionX, y: PositionY, time?: number, easing?: EaseItem): void } export default class Movable implements PluginAPI { static pluginName = 'movable' static applyOrder = ApplyOrder.Pre private hooksFn: Array<[EventEmitter, string, Function]> constructor(public scroll: BScroll) { this.handleBScroll() this.handleHooks() } private handleBScroll() { this.scroll.proxy(propertiesConfig) } private handleHooks() { this.hooksFn = [] const { scrollBehaviorX, scrollBehaviorY } = this.scroll.scroller const computeBoundary = (boundary: Boundary, behavior: Behavior) => { if (boundary.maxScrollPos > 0) { // content is smaller than wrapper boundary.minScrollPos = behavior.wrapperSize - behavior.contentSize boundary.maxScrollPos = 0 } } this.registerHooks( scrollBehaviorX.hooks, scrollBehaviorX.hooks.eventTypes.ignoreHasScroll, () => true ) this.registerHooks( scrollBehaviorX.hooks, scrollBehaviorX.hooks.eventTypes.computeBoundary, (boundary: Boundary) => { computeBoundary(boundary, scrollBehaviorX) } ) this.registerHooks( scrollBehaviorY.hooks, scrollBehaviorY.hooks.eventTypes.ignoreHasScroll, () => true ) this.registerHooks( scrollBehaviorY.hooks, scrollBehaviorY.hooks.eventTypes.computeBoundary, (boundary: Boundary) => { computeBoundary(boundary, scrollBehaviorY) } ) this.registerHooks( this.scroll.hooks, this.scroll.hooks.eventTypes.destroy, () => { this.destroy() } ) } putAt( x: PositionX, y: PositionY, time = this.scroll.options.bounceTime, easing = ease.bounce ) { const position = this.resolvePostion(x, y) this.scroll.scrollTo(position.x, position.y, time, easing) } private resolvePostion(x: PositionX, y: PositionY): { x: number; y: number } { const { scrollBehaviorX, scrollBehaviorY } = this.scroll.scroller const resolveFormula = { left() { return 0 }, top() { return 0 }, right() { return scrollBehaviorX.minScrollPos }, bottom() { return scrollBehaviorY.minScrollPos }, center(index: number) { const baseSize = index === 0 ? scrollBehaviorX.minScrollPos : scrollBehaviorY.minScrollPos return baseSize / 2 }, } return { x: typeof x === 'number' ? x : resolveFormula[x](0), y: typeof y === 'number' ? y : resolveFormula[y](1), } } destroy() { this.hooksFn.forEach((item) => { const hooks = item[0] const hooksName = item[1] const handlerFn = item[2] hooks.off(hooksName, handlerFn) }) this.hooksFn.length = 0 } private registerHooks(hooks: EventEmitter, name: string, handler: Function) { hooks.on(name, handler, this) this.hooksFn.push([hooks, name, handler]) } } ================================================ FILE: packages/movable/src/propertiesConfig.ts ================================================ const sourcePrefix = 'plugins.movable' const propertiesMap = [ { key: 'putAt', name: 'putAt', }, ] export default propertiesMap.map((item) => { return { key: item.key, sourceKey: `${sourcePrefix}.${item.name}`, } }) ================================================ FILE: packages/nested-scroll/README.md ================================================ # better-scroll [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/nested-scroll/README_zh-CN.md) nestedScroll is a plugin which helps you solve the trouble of nested Scroll ## Usage ```js import BScroll from 'better-scroll' import NestedScroll from '@better-scroll/nested-scroll' BScroll.use(NestedScroll) // parent bs new BScroll('.outerWrapper', { nestedScroll: true }) // child bs new BScroll('.innerWrapper', { nestedScroll: true }) ``` ================================================ FILE: packages/nested-scroll/package.json ================================================ { "name": "@better-scroll/nested-scroll", "version": "2.5.1", "description": "make your nested scrolls reconciliation", "author": { "name": "jizhi", "email": "theniceangel@163.com" }, "publishConfig": { "access": "public" }, "main": "dist/nested-scroll.js", "module": "dist/nested-scroll.esm.js", "typings": "dist/types/index.d.ts", "scripts": {}, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios" ], "license": "MIT", "repository": { "type": "git", "url": "git@github.com:ustbhuangyi/better-scroll.git", "directory": "packages/nested-scroll" }, "dependencies": { "@better-scroll/core": "^2.5.1" }, "gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130" } ================================================ FILE: packages/nested-scroll/src/BScrollFamily.ts ================================================ import BScroll from '@better-scroll/core' import { EventEmitter, findIndex } from '@better-scroll/shared-utils' // second element is used to discribe the distance between two bs instance's wrapper DOM export type BScrollFamilyTuple = [BScrollFamily, number] export default class BScrollFamily { static create(scroll: BScroll) { return new BScrollFamily(scroll) } ancestors: BScrollFamilyTuple[] = [] descendants: BScrollFamilyTuple[] = [] hooksManager: [EventEmitter, string, Function][] = [] selfScroll: BScroll analyzed: boolean = false constructor(scroll: BScroll) { this.selfScroll = scroll } hasAncestors(bscrollFamily: BScrollFamily) { const index = findIndex(this.ancestors, ([item]) => { return item === bscrollFamily }) return index > -1 } hasDescendants(bscrollFamily: BScrollFamily) { const index = findIndex(this.descendants, ([item]) => { return item === bscrollFamily }) return index > -1 } addAncestor(bscrollFamily: BScrollFamily, distance: number) { const ancestors = this.ancestors ancestors.push([bscrollFamily, distance]) // by ascend ancestors.sort((a, b) => { return a[1] - b[1] }) } addDescendant(bscrollFamily: BScrollFamily, distance: number) { const descendants = this.descendants descendants.push([bscrollFamily, distance]) // by ascend descendants.sort((a, b) => { return a[1] - b[1] }) } removeAncestor(bscrollFamily: BScrollFamily) { const ancestors = this.ancestors if (ancestors.length) { const index = findIndex(this.ancestors, ([item]) => { return item === bscrollFamily }) if (index > -1) { return ancestors.splice(index, 1) } } } removeDescendant(bscrollFamily: BScrollFamily) { const descendants = this.descendants if (descendants.length) { const index = findIndex(this.descendants, ([item]) => { return item === bscrollFamily }) if (index > -1) { return descendants.splice(index, 1) } } } registerHooks(hook: EventEmitter, eventType: string, handler: Function) { hook.on(eventType, handler) this.hooksManager.push([hook, eventType, handler]) } setAnalyzed(flag = false) { this.analyzed = flag } purge() { // remove self from graph this.ancestors.forEach(([bscrollFamily]) => { bscrollFamily.removeDescendant(this) }) this.descendants.forEach(([bscrollFamily]) => { bscrollFamily.removeAncestor(this) }) // remove all hook handlers this.hooksManager.forEach(([hooks, eventType, handler]) => { hooks.off(eventType, handler) }) this.hooksManager = [] } } ================================================ FILE: packages/nested-scroll/src/__tests__/index.spec.ts ================================================ import BScroll from '@better-scroll/core' jest.mock('@better-scroll/core') import NestedScroll, { DEFAUL_GROUP_ID } from '../index' const addProperties = ( target: T, source: K ) => { for (const key in source) { ;(target as any)[key] = source[key] } return target } describe('NestedScroll tests', () => { let parentWrapper: HTMLElement let parentContent: HTMLElement let childWrapper: HTMLElement let childContent: HTMLElement let grandsonWrapper: HTMLElement let grandsonContent: HTMLElement beforeEach(() => { parentWrapper = document.createElement('div') parentContent = document.createElement('div') childWrapper = document.createElement('div') childContent = document.createElement('div') grandsonWrapper = document.createElement('div') grandsonContent = document.createElement('div') parentWrapper.appendChild(parentContent) parentContent.appendChild(childWrapper) childWrapper.appendChild(childContent) childContent.appendChild(grandsonWrapper) grandsonWrapper.appendChild(grandsonContent) }) afterEach(() => { NestedScroll.purgeAllNestedScrolls() jest.clearAllMocks() }) it('should proxy properties to scroll instance', () => { const scroll = new BScroll(parentWrapper, { nestedScroll: true, }) new NestedScroll(scroll) expect(scroll.proxy).toBeCalledWith([ { key: 'purgeNestedScroll', sourceKey: 'plugins.nestedScroll.purgeNestedScroll', }, ]) }) it('nestedScroll.gropuId should be string or number ', () => { const spyFn = jest.spyOn(console, 'error') const nestedScroll1 = new NestedScroll( new BScroll(parentWrapper, { nestedScroll: { groupId: {} as any, }, }) ) expect(spyFn).toBeCalledWith( '[BScroll warn]: groupId must be string or number for NestedScroll plugin' ) }) it('should allocate default groupId when nestedScroll options is "true"', () => { const nestedScroll1 = new NestedScroll( new BScroll(parentWrapper, { nestedScroll: true, }) ) const nestedScroll2 = new NestedScroll( new BScroll(childWrapper, { nestedScroll: true, }) ) expect(nestedScroll1).toEqual(nestedScroll2) expect(NestedScroll.instancesMap[DEFAUL_GROUP_ID]).toBe(nestedScroll1) }) it('should share same nestedScroll when groupId is equal', () => { const nestedScroll1 = new NestedScroll( new BScroll(parentWrapper, { nestedScroll: { groupId: 0, }, }) ) const nestedScroll2 = new NestedScroll( new BScroll(childWrapper, { nestedScroll: { groupId: 0, }, }) ) expect(nestedScroll1).toEqual(nestedScroll2) }) it('should store BScroll instance', () => { const nestedScroll1 = new NestedScroll(new BScroll(parentWrapper, {})) const nestedScroll2 = new NestedScroll(new BScroll(childWrapper, {})) expect(nestedScroll1.store.length).toBe(2) expect(nestedScroll1).toBe(nestedScroll2) }) it('should build BScrollGraph', () => { const parentScroll = new BScroll(parentWrapper, { nestedScroll: { groupId: 'BScrollGraph', }, }) const childScroll = new BScroll(childWrapper, { nestedScroll: { groupId: 'BScrollGraph', }, }) // ns1 === ns2 const ns1 = new NestedScroll(parentScroll) const ns2 = new NestedScroll(childScroll) const parentBScrollFamily = ns1.store[0] const childBScrollFamily = ns1.store[1] expect(parentBScrollFamily.ancestors.length).toBe(0) expect( parentBScrollFamily.descendants.findIndex(([bf]) => { return bf === childBScrollFamily }) > -1 ).toBe(true) expect( childBScrollFamily.ancestors.findIndex(([bf]) => { return bf === parentBScrollFamily }) > -1 ).toBe(true) expect(childBScrollFamily.descendants.length).toBe(0) }) it('should has different nestedScroll instance in NestedScroll class', () => { const parentScroll = new BScroll(parentWrapper, { nestedScroll: { groupId: 'BScrollGraph1', }, }) const childScroll = new BScroll(childWrapper, { nestedScroll: { groupId: 'BScrollGraph2', }, }) const nestedScroll1 = new NestedScroll(parentScroll) const nestedScroll2 = new NestedScroll(childScroll) expect(nestedScroll1).not.toBe(nestedScroll2) expect(NestedScroll.getAllNestedScrolls().length).toBe(2) }) it('disable the ancestors scroll when self is scrolling', () => { // vertical const parentScroll = new BScroll(parentWrapper, { nestedScroll: { groupId: 'vertical', }, }) const childScroll = new BScroll(childWrapper, { nestedScroll: { groupId: 'vertical', }, }) const grandsonScroll = new BScroll(grandsonWrapper, { nestedScroll: { groupId: 'vertical', }, }) new NestedScroll(parentScroll) new NestedScroll(childScroll) new NestedScroll(grandsonScroll) addProperties(parentScroll, { pending: true, }) addProperties(childScroll, { pending: true, }) grandsonScroll.trigger(grandsonScroll.eventTypes.beforeScrollStart) expect(parentScroll.stop).toBeCalled() expect(parentScroll.resetPosition).toBeCalled() expect(childScroll.stop).toBeCalled() expect(childScroll.resetPosition).toBeCalled() // horizontal const parentScrollH = new BScroll(parentWrapper, { nestedScroll: { groupId: 'horizontal', }, scrollY: false, scrollX: true, }) const childScrollH = new BScroll(childWrapper, { nestedScroll: { groupId: 'horizontal', }, scrollY: false, scrollX: true, }) const grandsonScrollH = new BScroll(grandsonWrapper, { nestedScroll: { groupId: 'horizontal', }, scrollY: false, scrollX: true, }) new NestedScroll(parentScrollH) new NestedScroll(childScrollH) new NestedScroll(grandsonScrollH) addProperties(parentScrollH, { hasVerticalScroll: false, hasHorizontalScroll: true, }) addProperties(childScrollH, { hasVerticalScroll: false, hasHorizontalScroll: true, }) addProperties(grandsonScrollH, { hasVerticalScroll: false, hasHorizontalScroll: true, }) grandsonScrollH.trigger(grandsonScrollH.eventTypes.beforeScrollStart) expect(parentScrollH.disable).toBeCalled() expect(parentScrollH.scroller.actions.startTime).toBeTruthy() expect(childScrollH.disable).toBeCalled() expect(childScrollH.scroller.actions.startTime).toBeTruthy() }) it('should delete scroll from nestedScroll graph when scroll is destroyed', () => { const parentScroll = new BScroll(parentWrapper, { nestedScroll: { groupId: 'deleteScroll', }, }) const childScroll = new BScroll(childWrapper, { nestedScroll: { groupId: 'deleteScroll', }, }) const ns = new NestedScroll(parentScroll) new NestedScroll(childScroll) expect(ns.store.length).toBe(2) expect(childScroll.hooks.events.destroy.length).toBe(1) childScroll.hooks.trigger(childScroll.hooks.eventTypes.destroy) expect(ns.store.length).toBe(1) expect(childScroll.hooks.events.destroy.length).toBe(0) }) it('should force ancestors and descendants stop when self will start scrolling', () => { const parentScroll = new BScroll(parentWrapper, { nestedScroll: { groupId: 'force-stop', }, }) const childScroll = new BScroll(childWrapper, { nestedScroll: { groupId: 'force-stop', }, }) new NestedScroll(parentScroll) new NestedScroll(childScroll) addProperties(childScroll, { pending: true, }) parentScroll.trigger(parentScroll.eventTypes.beforeScrollStart) expect(childScroll.stop).toBeCalled() expect(childScroll.resetPosition).toBeCalled() }) it('should force self resetting potisition', () => { const parentScroll = new BScroll(parentWrapper, { nestedScroll: { groupId: 'force-stop', }, }) const childScroll = new BScroll(childWrapper, { nestedScroll: { groupId: 'force-stop', }, }) addProperties(parentScroll, { hasVerticalScroll: false, hasHorizontalScroll: true, }) addProperties(childScroll, { hasVerticalScroll: false, hasHorizontalScroll: true, x: 1, }) new NestedScroll(parentScroll) new NestedScroll(childScroll) childScroll.trigger(childScroll.eventTypes.beforeScrollStart) expect(childScroll.scroller.reflow).toBeCalled() expect(childScroll.resetPosition).toBeCalled() }) it('detectMovingDirection hook', () => { const parentScroll = new BScroll(parentWrapper, { nestedScroll: { groupId: 'enable-others', }, }) const childScroll = new BScroll(childWrapper, { nestedScroll: { groupId: 'enable-others', }, }) new NestedScroll(parentScroll) new NestedScroll(childScroll) // one is moved, all ancestors should be disabled childScroll.scroller.actions.contentMoved = true const selfActionsHooks = childScroll.scroller.actions.hooks selfActionsHooks.trigger(selfActionsHooks.eventTypes.detectMovingDirection) expect(parentScroll.disable).toBeCalled() }) it('should enable ancestors and descendants when self touchended', () => { const parentScroll = new BScroll(parentWrapper, { nestedScroll: { groupId: 'touchend', }, }) const childScroll = new BScroll(childWrapper, { nestedScroll: { groupId: 'touchend', }, }) new NestedScroll(parentScroll) new NestedScroll(childScroll) childScroll.trigger(childScroll.eventTypes.touchEnd) expect(parentScroll.enable).toBeCalled() parentScroll.trigger(parentScroll.eventTypes.touchEnd) expect(childScroll.enable).toBeCalled() }) it('should only allow top-scroll has bounce effect', () => { // vertical const parentScroll = new BScroll(parentWrapper, { nestedScroll: { groupId: 'vertical-bounce-effect', }, }) const childScroll = new BScroll(childWrapper, { nestedScroll: { groupId: 'vertical-bounce-effect', }, }) new NestedScroll(parentScroll) new NestedScroll(childScroll) childScroll.movingDirectionY = -1 const selfActionsHooks = childScroll.scroller.actions.hooks selfActionsHooks.trigger(selfActionsHooks.eventTypes.detectMovingDirection) expect(childScroll.disable).toBeCalled() expect(parentScroll.enable).toBeCalled() // horizontal const parentScrollH = new BScroll(parentWrapper, { nestedScroll: { groupId: 'horizontal-bounce-effect', }, }) const childScrollH = new BScroll(childWrapper, { nestedScroll: { groupId: 'horizontal-bounce-effect', }, }) addProperties(childScrollH, { hasVerticalScroll: false, hasHorizontalScroll: true, }) new NestedScroll(parentScrollH) new NestedScroll(childScrollH) childScrollH.movingDirectionX = -1 const selfActionsHooksH = childScrollH.scroller.actions.hooks selfActionsHooksH.trigger( selfActionsHooksH.eventTypes.detectMovingDirection ) expect(childScrollH.disable).toBeCalled() expect(parentScrollH.enable).toBeCalled() }) }) ================================================ FILE: packages/nested-scroll/src/index.ts ================================================ import BScroll, { MountedBScrollHTMLElement } from '@better-scroll/core' import { Direction, EventEmitter, extend, warn, findIndex, } from '@better-scroll/shared-utils' import BScrollFamily from './BScrollFamily' import propertiesConfig from './propertiesConfig' export const DEFAUL_GROUP_ID = 'INTERNAL_NESTED_SCROLL' export type NestedScrollGroupId = string | number export interface NestedScrollConfig { groupId: NestedScrollGroupId } export type NestedScrollOptions = NestedScrollConfig | true declare module '@better-scroll/core' { interface CustomOptions { nestedScroll?: NestedScrollOptions } interface CustomAPI { nestedScroll: PluginAPI } } interface PluginAPI { purgeNestedScroll(groupId: NestedScrollGroupId): void } interface NestedScrollInstancesMap { [key: string]: NestedScroll [index: number]: NestedScroll } const forceScrollStopHandler = (scrolls: BScroll[]) => { scrolls.forEach((scroll) => { if (scroll.pending) { scroll.stop() scroll.resetPosition() } }) } const enableScrollHander = (scrolls: BScroll[]) => { scrolls.forEach((scroll) => { scroll.enable() }) } const disableScrollHander = (scrolls: BScroll[], currentScroll: BScroll) => { scrolls.forEach((scroll) => { if ( scroll.hasHorizontalScroll === currentScroll.hasHorizontalScroll || scroll.hasVerticalScroll === currentScroll.hasVerticalScroll ) { scroll.disable() } }) } const syncTouchstartData = (scrolls: BScroll[]) => { scrolls.forEach((scroll) => { const { actions, scrollBehaviorX, scrollBehaviorY } = scroll.scroller // prevent click triggering many times actions.fingerMoved = true actions.contentMoved = false actions.directionLockAction.reset() scrollBehaviorX.start() scrollBehaviorY.start() scrollBehaviorX.resetStartPos() scrollBehaviorY.resetStartPos() actions.startTime = +new Date() }) } const isOutOfBoundary = (scroll: BScroll): boolean => { const { hasHorizontalScroll, hasVerticalScroll, x, y, minScrollX, maxScrollX, minScrollY, maxScrollY, movingDirectionX, movingDirectionY, } = scroll let ret = false const outOfLeftBoundary = x >= minScrollX && movingDirectionX === Direction.Negative const outOfRightBoundary = x <= maxScrollX && movingDirectionX === Direction.Positive const outOfTopBoundary = y >= minScrollY && movingDirectionY === Direction.Negative const outOfBottomBoundary = y <= maxScrollY && movingDirectionY === Direction.Positive if (hasVerticalScroll) { ret = outOfTopBoundary || outOfBottomBoundary } else if (hasHorizontalScroll) { ret = outOfLeftBoundary || outOfRightBoundary } return ret } const isResettingPosition = (scroll: BScroll): boolean => { const { hasHorizontalScroll, hasVerticalScroll, x, y, minScrollX, maxScrollX, minScrollY, maxScrollY, } = scroll let ret = false const outOfLeftBoundary = x > minScrollX const outOfRightBoundary = x < maxScrollX const outOfTopBoundary = y > minScrollY const outOfBottomBoundary = y < maxScrollY if (hasVerticalScroll) { ret = outOfTopBoundary || outOfBottomBoundary } else if (hasHorizontalScroll) { ret = outOfLeftBoundary || outOfRightBoundary } return ret } const resetPositionHandler = (scroll: BScroll) => { scroll.scroller.reflow() scroll.resetPosition(0 /* Immediately */) } const calculateDistance = ( childNode: HTMLElement, parentNode: HTMLElement ): number => { let distance = 0 let parent = childNode.parentNode while (parent && parent !== parentNode) { distance++ parent = parent.parentNode } return distance } export default class NestedScroll implements PluginAPI { static pluginName = 'nestedScroll' static instancesMap: NestedScrollInstancesMap = {} store: BScrollFamily[] options: NestedScrollConfig private hooksFn: Array<[EventEmitter, string, Function]> constructor(scroll: BScroll) { const groupId = this.handleOptions(scroll) let instance = NestedScroll.instancesMap[groupId] if (!instance) { instance = NestedScroll.instancesMap[groupId] = this instance.store = [] instance.hooksFn = [] } instance.init(scroll) return instance } static getAllNestedScrolls(): NestedScroll[] { const instancesMap = NestedScroll.instancesMap return Object.keys(instancesMap).map((key) => instancesMap[key]) } static purgeAllNestedScrolls() { const nestedScrolls = NestedScroll.getAllNestedScrolls() nestedScrolls.forEach((ns) => ns.purgeNestedScroll()) } private handleOptions(scroll: BScroll): number | string { const userOptions = (scroll.options.nestedScroll === true ? {} : scroll.options.nestedScroll) as NestedScrollConfig const defaultOptions: NestedScrollConfig = { groupId: DEFAUL_GROUP_ID, } this.options = extend(defaultOptions, userOptions) const groupIdType = typeof this.options.groupId if (groupIdType !== 'string' && groupIdType !== 'number') { warn('groupId must be string or number for NestedScroll plugin') } return this.options.groupId } private init(scroll: BScroll) { scroll.proxy(propertiesConfig) this.addBScroll(scroll) this.buildBScrollGraph() this.analyzeBScrollGraph() this.ensureEventInvokeSequence() this.handleHooks(scroll) } private handleHooks(scroll: BScroll) { this.registerHooks(scroll.hooks, scroll.hooks.eventTypes.destroy, () => { this.deleteScroll(scroll) }) } deleteScroll(scroll: BScroll) { const wrapper = scroll.wrapper as MountedBScrollHTMLElement wrapper.isBScrollContainer = undefined const store = this.store const hooksFn = this.hooksFn const i = findIndex(store, (bscrollFamily) => { return bscrollFamily.selfScroll === scroll }) if (i > -1) { const bscrollFamily = store[i] bscrollFamily.purge() store.splice(i, 1) } const k = findIndex(hooksFn, ([hooks]) => { return hooks === scroll.hooks }) if (k > -1) { const [hooks, eventType, handler] = hooksFn[k] hooks.off(eventType, handler) hooksFn.splice(k, 1) } } addBScroll(scroll: BScroll) { this.store.push(BScrollFamily.create(scroll)) } private buildBScrollGraph() { const store = this.store let bf1: BScrollFamily let bf2: BScrollFamily let wrapper1: MountedBScrollHTMLElement let wrapper2: MountedBScrollHTMLElement let len = this.store.length // build graph for (let i = 0; i < len; i++) { bf1 = store[i] wrapper1 = bf1.selfScroll.wrapper for (let j = 0; j < len; j++) { bf2 = store[j] wrapper2 = bf2.selfScroll.wrapper // same bs if (bf1 === bf2) continue if (!wrapper1.contains(wrapper2)) continue // bs1 contains bs2 const distance = calculateDistance(wrapper2, wrapper1) if (!bf1.hasDescendants(bf2)) { bf1.addDescendant(bf2, distance) } if (!bf2.hasAncestors(bf1)) { bf2.addAncestor(bf1, distance) } } } } private analyzeBScrollGraph() { this.store.forEach((bscrollFamily) => { if (bscrollFamily.analyzed) { return } const { ancestors, descendants, selfScroll: currentScroll, } = bscrollFamily const beforeScrollStartHandler = () => { // always get the latest scroll const ancestorScrolls = ancestors.map( ([bscrollFamily]) => bscrollFamily.selfScroll ) const descendantScrolls = descendants.map( ([bscrollFamily]) => bscrollFamily.selfScroll ) forceScrollStopHandler([...ancestorScrolls, ...descendantScrolls]) if (isResettingPosition(currentScroll)) { resetPositionHandler(currentScroll) } syncTouchstartData(ancestorScrolls) disableScrollHander(ancestorScrolls, currentScroll) } const touchEndHandler = () => { const ancestorScrolls = ancestors.map( ([bscrollFamily]) => bscrollFamily.selfScroll ) const descendantScrolls = descendants.map( ([bscrollFamily]) => bscrollFamily.selfScroll ) enableScrollHander([...ancestorScrolls, ...descendantScrolls]) } bscrollFamily.registerHooks( currentScroll, currentScroll.eventTypes.beforeScrollStart, beforeScrollStartHandler ) bscrollFamily.registerHooks( currentScroll, currentScroll.eventTypes.touchEnd, touchEndHandler ) const selfActionsHooks = currentScroll.scroller.actions.hooks bscrollFamily.registerHooks( selfActionsHooks, selfActionsHooks.eventTypes.detectMovingDirection, () => { const ancestorScrolls = ancestors.map( ([bscrollFamily]) => bscrollFamily.selfScroll ) const parentScroll = ancestorScrolls[0] const otherAncestorScrolls = ancestorScrolls.slice(1) const contentMoved = currentScroll.scroller.actions.contentMoved const isTopScroll = ancestorScrolls.length === 0 if (contentMoved) { disableScrollHander(ancestorScrolls, currentScroll) } else if (!isTopScroll) { if (isOutOfBoundary(currentScroll)) { disableScrollHander([currentScroll], currentScroll) if (parentScroll) { enableScrollHander([parentScroll]) } disableScrollHander(otherAncestorScrolls, currentScroll) return true } } } ) bscrollFamily.setAnalyzed(true) }) } // make sure touchmove|touchend invoke from child to parent private ensureEventInvokeSequence() { const copied = this.store.slice() const sequencedScroll = copied.sort((a, b) => { return a.descendants.length - b.descendants.length }) sequencedScroll.forEach((bscrollFamily) => { const scroll = bscrollFamily.selfScroll scroll.scroller.actionsHandler.rebindDOMEvents() }) } private registerHooks(hooks: EventEmitter, name: string, handler: Function) { hooks.on(name, handler, this) this.hooksFn.push([hooks, name, handler]) } purgeNestedScroll() { const groupId = this.options.groupId this.store.forEach((bscrollFamily) => { bscrollFamily.purge() }) this.store = [] this.hooksFn.forEach(([hooks, eventType, handler]) => { hooks.off(eventType, handler) }) this.hooksFn = [] delete NestedScroll.instancesMap[groupId] } } ================================================ FILE: packages/nested-scroll/src/propertiesConfig.ts ================================================ const sourcePrefix = 'plugins.nestedScroll' const propertiesMap = [ { key: 'purgeNestedScroll', name: 'purgeNestedScroll', }, ] export default propertiesMap.map((item) => { return { key: item.key, sourceKey: `${sourcePrefix}.${item.name}`, } }) ================================================ FILE: packages/observe-dom/README.md ================================================ # @better-scroll/observe-dom [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/observe-dom/README_zh-CN.md) recaculating BetterScroll's scrollHeight or scrollWidth by `MutationObserver`, with this, you don't care when BetterScroll's scrollHeight or scrollWidth have changed. Plugin has done it for you. > **if current browser does not surpport `MutationObserver`, it will fallback to `setTimeout` in recursion** ## Usage ```js import BScroll from '@better-scroll/core' import ObserveDom from '@better-scroll/observe-dom' BScroll.use(ObserveDom) const bs = new BScroll('.wrapper', { observeDOM: true }) ``` ================================================ FILE: packages/observe-dom/README_zh-CN.md ================================================ # @better-scroll/observe-dom 动态计算 BetterScroll 的可滚动高度或者宽度,你并不需要自己在高度或者宽度发生变化的时候,手动调用 `refresh()` 方法。插件通过 `MutationObserver` 帮你完成了。 > **如果当前你的浏览器不支持 `MutationObserver`,会降级使用 `setTimeout`。** ## 使用 ```js import BScroll from '@better-scroll/core' import ObserveDom from '@better-scroll/observe-dom' BScroll.use(ObserveDom) const bs = new BScroll('.wrapper', { observeDOM: true }) ``` ================================================ FILE: packages/observe-dom/package.json ================================================ { "name": "@better-scroll/observe-dom", "version": "2.5.1", "description": "Dynamic recalculating container's size for BetterScroll", "author": { "name": "jizhi", "email": "theniceangel@163.com" }, "main": "dist/observe-dom.min.js", "module": "dist/observe-dom.esm.js", "typings": "dist/types/index.d.ts", "publishConfig": { "access": "public" }, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios" ], "license": "MIT", "repository": { "type": "git", "url": "git+ssh://git@github.com/ustbhuangyi/better-scroll.git", "directory": "packages/observe-dom" }, "dependencies": { "@better-scroll/core": "^2.5.1" }, "gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130" } ================================================ FILE: packages/observe-dom/src/__tests__/index.spec.ts ================================================ import BScroll from '@better-scroll/core' import ObserveDOM from '../index' import { mockDomOffset, CustomHTMLDivElement, } from '@better-scroll/core/src/__tests__/__utils__/layout' jest.mock('@better-scroll/core') const addProperties = ( target: T, source: K ) => { for (const key in source) { ;(target as any)[key] = source[key] } return target } const createObserveDOMElements = () => { const wrapper = document.createElement('div') const content = document.createElement('div') wrapper.appendChild(content) return { wrapper } } describe('observe dom', () => { let scroll: BScroll let observeDOM: ObserveDOM let mockMutationObserver: jest.Mock let mockObserve: jest.Mock let mockDisconnect: jest.Mock let triggerMutation: Function beforeAll(() => { jest.useFakeTimers() }) beforeEach(() => { mockObserve = jest.fn() mockDisconnect = jest.fn() mockMutationObserver = jest.fn().mockImplementation((cb) => { triggerMutation = (mutations: any, timer: number) => { cb(mutations, timer) } return { observe: mockObserve, disconnect: mockDisconnect, } }) Object.defineProperty(window, 'MutationObserver', { get: function () { return mockMutationObserver }, }) const { wrapper } = createObserveDOMElements() scroll = new BScroll(wrapper, {}) observeDOM = new ObserveDOM(scroll) }) afterEach(() => { jest.clearAllMocks() jest.clearAllTimers() }) it('observe without MutationObserver', () => { // set MutationObserver to undefined Object.defineProperty(window, 'MutationObserver', { get: function () { return undefined }, }) observeDOM = new ObserveDOM(scroll) expect(observeDOM.observer).toBeFalsy() mockDomOffset(scroll.scroller.content as CustomHTMLDivElement, { width: 400, }) jest.advanceTimersByTime(1000) expect(scroll.refresh).toBeCalled() // destroy scroll.hooks.trigger('destroy') mockDomOffset(scroll.scroller.content as CustomHTMLDivElement, { width: 500, }) jest.advanceTimersByTime(1000) expect(scroll.refresh).toHaveBeenCalledTimes(1) }) it('observe with MutationObserver', () => { expect(scroll.hooks.events.enable.length).toBe(1) expect(scroll.hooks.events.disable.length).toBe(1) expect(scroll.hooks.events.destroy.length).toBe(1) expect(mockObserve).toHaveBeenCalledWith(scroll.scroller.content, { attributes: true, childList: true, subtree: true, }) // init can't be call again when it is observing const init = jest.spyOn(observeDOM, 'init') scroll.hooks.trigger(scroll.hooks.eventTypes.enabled) expect(init).not.toHaveBeenCalled() init.mockReset() addProperties(scroll.scroller.scrollBehaviorX, { currentPos: -12, minScrollPos: 0, maxScrollPos: -100, }) addProperties(scroll.scroller.scrollBehaviorY, { currentPos: -12, minScrollPos: 0, maxScrollPos: -100, }) addProperties(scroll.scroller.animater, { pending: false, }) triggerMutation( [ { type: 'test', }, ], 300 ) expect(scroll.refresh).toBeCalledTimes(1) // attributes change & target is scroller.content triggerMutation( [ { type: 'attributes', target: scroll.scroller.content, }, ], 300 ) expect(scroll.refresh).toBeCalledTimes(1) // attributes change & target is not scroller.content triggerMutation( [ { type: 'attributes', target: '', }, ], 300 ) expect(scroll.refresh).toBeCalledTimes(1) jest.advanceTimersByTime(61) expect(scroll.refresh).toBeCalledTimes(2) // pedding addProperties(scroll.scroller.animater, { pending: true, }) triggerMutation( [ { type: 'test', }, ], 300 ) expect(scroll.refresh).toBeCalledTimes(2) // out of boundary addProperties(scroll.scroller.scrollBehaviorY, { currentPos: -12, minScrollPos: 0, maxScrollPos: -10, }) triggerMutation( [ { type: 'test', }, ], 300 ) expect(scroll.refresh).toBeCalledTimes(2) // destroy scroll.hooks.trigger(scroll.hooks.eventTypes.destroy) expect(mockDisconnect).toBeCalled() expect(scroll.hooks.events.enable.length).toBe(0) expect(scroll.hooks.events.disable.length).toBe(0) expect(scroll.hooks.events.destroy.length).toBe(0) }) it('enable/disable', () => { scroll.hooks.trigger(scroll.hooks.eventTypes.disable) expect(mockDisconnect).toBeCalled() scroll.hooks.trigger(scroll.hooks.eventTypes.enable) expect(mockObserve).toBeCalled() }) }) ================================================ FILE: packages/observe-dom/src/index.ts ================================================ import BScroll from '@better-scroll/core' import { getRect, EventEmitter } from '@better-scroll/shared-utils' declare module '@better-scroll/core' { interface CustomOptions { observeDOM?: true } } export default class ObserveDOM { static pluginName = 'observeDOM' observer: MutationObserver private stopObserver: boolean = false private hooksFn: Array<[EventEmitter, string, Function]> constructor(public scroll: BScroll) { this.init() } init() { this.handleMutationObserver() this.handleHooks() } private handleMutationObserver() { if (typeof MutationObserver !== 'undefined') { let timer = 0 this.observer = new MutationObserver((mutations) => { this.mutationObserverHandler(mutations, timer) }) this.startObserve(this.observer) } else { this.checkDOMUpdate() } } private handleHooks() { this.hooksFn = [] this.registerHooks( this.scroll.hooks, this.scroll.hooks.eventTypes.contentChanged, () => { this.stopObserve() // launch a new mutationObserver this.handleMutationObserver() } ) this.registerHooks( this.scroll.hooks, this.scroll.hooks.eventTypes.enable, () => { if (this.stopObserver) { this.handleMutationObserver() } } ) this.registerHooks( this.scroll.hooks, this.scroll.hooks.eventTypes.disable, () => { this.stopObserve() } ) this.registerHooks( this.scroll.hooks, this.scroll.hooks.eventTypes.destroy, () => { this.destroy() } ) } private mutationObserverHandler(mutations: MutationRecord[], timer: number) { if (this.shouldNotRefresh()) { return } let immediateRefresh = false let deferredRefresh = false for (let i = 0; i < mutations.length; i++) { const mutation = mutations[i] if (mutation.type !== 'attributes') { immediateRefresh = true break } else { if (mutation.target !== this.scroll.scroller.content) { deferredRefresh = true break } } } if (immediateRefresh) { this.scroll.refresh() } else if (deferredRefresh) { // attributes changes too often clearTimeout(timer) timer = window.setTimeout(() => { if (!this.shouldNotRefresh()) { this.scroll.refresh() } }, 60) } } private startObserve(observer: MutationObserver) { const config = { attributes: true, childList: true, subtree: true, } observer.observe(this.scroll.scroller.content, config) } private shouldNotRefresh() { const { scroller } = this.scroll const { scrollBehaviorX, scrollBehaviorY } = scroller let outsideBoundaries = scrollBehaviorX.currentPos > scrollBehaviorX.minScrollPos || scrollBehaviorX.currentPos < scrollBehaviorX.maxScrollPos || scrollBehaviorY.currentPos > scrollBehaviorY.minScrollPos || scrollBehaviorY.currentPos < scrollBehaviorY.maxScrollPos return scroller.animater.pending || outsideBoundaries } private checkDOMUpdate() { const content = this.scroll.scroller.content let contentRect = getRect(content) let oldWidth = contentRect.width let oldHeight = contentRect.height const check = () => { if (this.stopObserver) { return } contentRect = getRect(content) let newWidth = contentRect.width let newHeight = contentRect.height if (oldWidth !== newWidth || oldHeight !== newHeight) { this.scroll.refresh() } oldWidth = newWidth oldHeight = newHeight next() } const next = () => { setTimeout(() => { check() }, 1000) } next() } private registerHooks(hooks: EventEmitter, name: string, handler: Function) { hooks.on(name, handler, this) this.hooksFn.push([hooks, name, handler]) } private stopObserve() { this.stopObserver = true if (this.observer) { this.observer.disconnect() } } destroy() { this.stopObserve() this.hooksFn.forEach((item) => { const hooks = item[0] const hooksName = item[1] const handlerFn = item[2] hooks.off(hooksName, handlerFn) }) this.hooksFn.length = 0 } } ================================================ FILE: packages/observe-image/README.md ================================================ # @better-scroll/observe-image [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/observe-image/README_zh-CN.md) when detecting images is loaded or failed to load, auto refresh BetterScroll's size. ## Usage ```js import BScroll from '@better-scroll/core' import ObserveImage from '@better-scroll/observe-image' BScroll.use(ObserveImage) const bs = new BScroll('.wrapper', { observeImage: true }) ``` ================================================ FILE: packages/observe-image/README_zh-CN.md ================================================ # @better-scroll/observe-image 当探测到 BetterScroll content DOM 的子元素是 image 标签,并且加载成功或者失败的时候,自动调用 `refresh()` 方法重新计算可滚动的高度或者宽度,适用于当滚动区域含有高度或者宽度不固定的场景。 ## 使用 ```js import BScroll from '@better-scroll/core' import ObserveImage from '@better-scroll/observe-image' BScroll.use(ObserveImage) const bs = new BScroll('.wrapper', { observeImage: true }) ``` ================================================ FILE: packages/observe-image/package.json ================================================ { "name": "@better-scroll/observe-image", "version": "2.5.1", "description": "Observe image loading for BetterScroll", "author": "theniceangel ", "homepage": "https://github.com/ustbhuangyi/better-scroll/tree/master/packages/observe-image#readme", "license": "MIT", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios", "umage" ], "main": "dist/observe-image.min.js", "module": "dist/observe-image.esm.js", "typings": "dist/types/index.d.ts", "publishConfig": { "access": "public" }, "repository": { "type": "git", "url": "git+https://github.com/ustbhuangyi/better-scroll.git", "directory": "packages/observe-image" }, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "dependencies": { "@better-scroll/core": "^2.5.1" } } ================================================ FILE: packages/observe-image/src/__tests__/index.spec.ts ================================================ import BScroll from '@better-scroll/core' import ObserveImage from '../index' import { createEvent } from '@better-scroll/core/src/__tests__/__utils__/event' jest.mock('@better-scroll/core') const createObserveImageElements = () => { const wrapper = document.createElement('div') const content = document.createElement('div') wrapper.appendChild(content) return { wrapper, content } } describe('observe image', () => { const { wrapper, content } = createObserveImageElements() let scroll: BScroll let observeImage: ObserveImage beforeAll(() => { jest.useFakeTimers() }) beforeEach(() => { scroll = new BScroll(wrapper, {}) observeImage = new ObserveImage(scroll) }) afterEach(() => { jest.clearAllMocks() jest.clearAllTimers() }) it('should capture image load or error event', () => { let img = document.createElement('img') let loadEvent = createEvent('Event', 'load') content.appendChild(img) img.dispatchEvent(loadEvent) jest.advanceTimersByTime(151) expect(scroll.refresh).toBeCalled() }) it('should trigger bs.refresh in a tick when debounceTime is 0', () => { observeImage.options.debounceTime = 0 let img = document.createElement('img') let loadEvent = createEvent('Event', 'load') content.appendChild(img) img.dispatchEvent(loadEvent) expect(scroll.refresh).toBeCalled() }) }) ================================================ FILE: packages/observe-image/src/index.ts ================================================ import BScroll from '@better-scroll/core' import { EventRegister, extend } from '@better-scroll/shared-utils' export type ObserveImageOptions = Partial | true export interface ObserveImageConfig { debounceTime: number } declare module '@better-scroll/core' { interface CustomOptions { observeImage?: ObserveImageOptions } } const isImageTag = (el: HTMLElement) => { return el.tagName.toLowerCase() === 'img' } export default class ObserveImage { static pluginName = 'observeImage' imageLoadEventRegister: EventRegister imageErrorEventRegister: EventRegister refreshTimer: number = 0 options: ObserveImageConfig constructor(public scroll: BScroll) { this.init() } init() { this.handleOptions(this.scroll.options.observeImage) this.bindEventsToWrapper() } private handleOptions(userOptions: ObserveImageOptions = {}) { userOptions = (userOptions === true ? {} : userOptions) as Partial< ObserveImageConfig > const defaultOptions: ObserveImageConfig = { debounceTime: 100, // ms } this.options = extend(defaultOptions, userOptions) } private bindEventsToWrapper() { const wrapper = this.scroll.scroller.wrapper this.imageLoadEventRegister = new EventRegister(wrapper, [ { name: 'load', handler: this.load.bind(this), capture: true, }, ]) this.imageErrorEventRegister = new EventRegister(wrapper, [ { name: 'error', handler: this.load.bind(this), capture: true, }, ]) } private load(e: Event) { const target = e.target as HTMLElement const debounceTime = this.options.debounceTime if (target && isImageTag(target)) { if (debounceTime === 0) { this.scroll.refresh() } else { clearTimeout(this.refreshTimer) this.refreshTimer = window.setTimeout(() => { this.scroll.refresh() }, this.options.debounceTime) } } } } ================================================ FILE: packages/pull-down/README.md ================================================ # @better-scroll/pull-down [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/pull-down/README_zh-CN.md) The ability to inject a pull-down refresh for BetterScroll. ## Usage ```js import BScroll from '@better-scroll/core' import Pulldown from '@better-scroll/pull-down' BScroll.use(Pulldown) const bs = new BScroll('.wrapper', { pullDownRefresh: true }) ``` ================================================ FILE: packages/pull-down/README_zh-CN.md ================================================ # @better-scroll/pull-down 为 BetterScroll 注入下拉刷新的能力。 ## 使用 ```js import BScroll from '@better-scroll/core' import Pulldown from '@better-scroll/pull-down' BScroll.use(Pulldown) const bs = new BScroll('.wrapper', { pullDownRefresh: true }) ``` ================================================ FILE: packages/pull-down/package.json ================================================ { "name": "@better-scroll/pull-down", "version": "2.5.1", "description": "pull down to refresh, behave likes App list refreshing", "author": { "name": "jizhi", "email": "theniceangel@163.com" }, "main": "dist/pull-down.min.js", "module": "dist/pull-down.esm.js", "typings": "dist/types/index.d.ts", "publishConfig": { "access": "public" }, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios", "pull-down" ], "license": "MIT", "repository": { "type": "git", "url": "git+ssh://git@github.com/ustbhuangyi/better-scroll.git", "directory": "packages/pull-down" }, "dependencies": { "@better-scroll/core": "^2.5.1" }, "gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130" } ================================================ FILE: packages/pull-down/src/__tests__/index.spec.ts ================================================ import BScroll from '@better-scroll/core' import { Probe } from '@better-scroll/shared-utils' import { ease } from '@better-scroll/shared-utils/src/ease' jest.mock('@better-scroll/core') import PullDown from '../index' const createPullDownElements = () => { const wrapper = document.createElement('div') const content = document.createElement('div') wrapper.appendChild(content) return { wrapper } } describe('pull down tests', () => { let scroll: BScroll let pullDown: PullDown beforeEach(() => { // create DOM const { wrapper } = createPullDownElements() scroll = new BScroll(wrapper, {}) pullDown = new PullDown(scroll) }) afterEach(() => { jest.clearAllMocks() }) it('should proxy properties to BScroll instance', () => { new PullDown(scroll) expect(scroll.proxy).toBeCalledWith([ { key: 'finishPullDown', sourceKey: 'plugins.pullDownRefresh.finishPullDown', }, { key: 'openPullDown', sourceKey: 'plugins.pullDownRefresh.openPullDown', }, { key: 'closePullDown', sourceKey: 'plugins.pullDownRefresh.closePullDown', }, { key: 'autoPullDownRefresh', sourceKey: 'plugins.pullDownRefresh.autoPullDownRefresh', }, ]) }) it('should handle default options and user options', () => { // case 1 scroll.options.pullDownRefresh = true pullDown = new PullDown(scroll) expect(pullDown.options).toMatchObject({ threshold: 90, stop: 40, }) // case 2 scroll.options.pullDownRefresh = { threshold: 100, stop: 50, } pullDown = new PullDown(scroll) expect(pullDown.options).toMatchObject({ threshold: 100, stop: 50, }) expect(scroll.options.probeType).toBe(Probe.Realtime) }) it('should cache originalMinScrollY', () => { expect(pullDown.cachedOriginanMinScrollY).toBe(0) expect(pullDown.currentMinScrollY).toBe(0) }) it('should modify minScrollY when necessary', () => { pullDown.currentMinScrollY = 50 const scrollBehaviorY = scroll.scroller.scrollBehaviorY let boundary = { minScrollPos: 0, maxScrollPos: 20, } scrollBehaviorY.hooks.trigger( scrollBehaviorY.hooks.eventTypes.computeBoundary, boundary ) expect(boundary).toMatchObject({ minScrollPos: 50, maxScrollPos: -1, }) }) it('should checkPullDown', () => { const mockFn = jest.fn() scroll.on(scroll.eventTypes.pullingDown, mockFn) scroll.trigger(scroll.eventTypes.scrollStart) // simulate pullUp action scroll.y = -100 scroll.scroller.hooks.trigger(scroll.scroller.hooks.eventTypes.end) expect(mockFn).toHaveBeenCalledTimes(0) // simulate pullDown action scroll.y = 100 scroll.scroller.hooks.trigger(scroll.scroller.hooks.eventTypes.end) expect(mockFn).toHaveBeenCalledTimes(1) }) it('should checkLocationOfThresholdBoundary', () => { const enterThresholdFn = jest.fn() const leaveThresholdFn = jest.fn() scroll.on(scroll.eventTypes.enterThreshold, enterThresholdFn) scroll.on(scroll.eventTypes.leaveThreshold, leaveThresholdFn) scroll.trigger(scroll.eventTypes.scrollStart) // enter threshold boundary scroll.y = 20 scroll.trigger(scroll.eventTypes.scroll) // leave threshold boundary scroll.y = 100 scroll.trigger(scroll.eventTypes.scroll) expect(enterThresholdFn).toHaveBeenCalledTimes(1) expect(leaveThresholdFn).toHaveBeenCalledTimes(1) }) it('should trigger pullingDown once', () => { const mockFn = jest.fn() scroll.on(scroll.eventTypes.pullingDown, mockFn) // when scroll.y = 100 scroll.trigger(scroll.eventTypes.scrollStart) scroll.scroller.hooks.trigger('end') scroll.scroller.hooks.trigger('end') // then expect(mockFn).toBeCalledTimes(1) }) it('should stop at correct position', () => { // when scroll.y = 100 scroll.scroller.hooks.trigger('end') expect(scroll.scrollTo).toHaveBeenCalledWith( 0, 40, scroll.options.bounceTime, ease.bounce ) }) it('should work well when call finishPullDown()', () => { pullDown.pulling = 2 pullDown.finishPullDown() expect(pullDown.pulling).toBe(0) expect(scroll.scroller.scrollBehaviorY.computeBoundary).toBeCalled() expect(scroll.resetPosition).toBeCalled() }) it('should work well when call closePullDown()', () => { pullDown.closePullDown() expect(pullDown.watching).toBe(false) expect(scroll.scroller.hooks.events.end.length).toBe(0) }) it('should work well when call openPullDown()', () => { pullDown.closePullDown() expect(pullDown.watching).toBe(false) expect(pullDown.options).toMatchObject({ threshold: 90, stop: 40, }) // modify options pullDown.openPullDown({ threshold: 200, stop: 80, }) expect(pullDown.options).toMatchObject({ threshold: 200, stop: 80, }) expect(pullDown.watching).toBe(true) }) it('should work well when call autoPullDownRefresh()', () => { const mockFn = jest.fn() scroll.on(scroll.eventTypes.pullingDown, mockFn) pullDown.autoPullDownRefresh() expect(pullDown.watching).toBe(true) expect(pullDown.currentMinScrollY).toBe(40) expect(scroll.scroller.scrollBehaviorY.computeBoundary).toBeCalled() expect(scroll.scrollTo).toHaveBeenCalledTimes(2) expect(scroll.scrollTo).toHaveBeenLastCalledWith( 0, 40, scroll.options.bounceTime, ease.bounce ) // closePullDown, and autoPullDownRefresh will not work pullDown.closePullDown() pullDown.autoPullDownRefresh() expect(scroll.scrollTo).toBeCalledTimes(2) }) it('should call finishPullDown when content DOM changed', () => { // simulate pullDown action pullDown.pulling = 2 scroll.hooks.trigger(scroll.hooks.eventTypes.contentChanged) expect(scroll.scroller.scrollBehaviorY.computeBoundary).toBeCalled() expect(scroll.resetPosition).toBeCalledWith(800, ease.bounce) }) it('should work well when integrating with mousewheel', () => { const options = {} as any scroll.trigger(scroll.eventTypes.alterOptions, options) expect(options.discreteTime).toBe(300) expect(options.easeTime).toBe(350) const mockFn = jest.fn() scroll.scroller.hooks.on(scroll.scroller.hooks.eventTypes.end, mockFn) scroll.trigger(scroll.eventTypes.mousewheelEnd) expect(mockFn).toBeCalled() }) }) ================================================ FILE: packages/pull-down/src/index.ts ================================================ import BScroll, { Boundary } from '@better-scroll/core' import { MouseWheelConfig } from '@better-scroll/mouse-wheel' import { ease, extend, EventEmitter, Probe } from '@better-scroll/shared-utils' import propertiesConfig from './propertiesConfig' export type PullDownRefreshOptions = Partial | true // pulldownRefresh phase will go through: // DEFAULT -> MOVING -> FETCHING // or // DEFAULT -> MOVING const enum PullDownPhase { DEFAULT, MOVING, FETCHING, } const enum ThresholdBoundary { DEFAULT, INSIDE, OUTSIDE, } export interface PullDownRefreshConfig { threshold: number stop: number } declare module '@better-scroll/core' { interface CustomOptions { pullDownRefresh?: PullDownRefreshOptions } interface CustomAPI { pullDownRefresh: PluginAPI } } interface PluginAPI { finishPullDown(): void openPullDown(config?: PullDownRefreshOptions): void closePullDown(): void autoPullDownRefresh(): void } const PULLING_DOWN_EVENT = 'pullingDown' const ENTER_THRESHOLD_EVENT = 'enterThreshold' const LEAVE_THRESHOLD_EVENT = 'leaveThreshold' export default class PullDown implements PluginAPI { static pluginName = 'pullDownRefresh' private hooksFn: Array<[EventEmitter, string, Function]> pulling: PullDownPhase = PullDownPhase.DEFAULT thresholdBoundary: ThresholdBoundary = ThresholdBoundary.DEFAULT watching: boolean options: PullDownRefreshConfig cachedOriginanMinScrollY: number currentMinScrollY: number constructor(public scroll: BScroll) { this.init() } private setPulling(status: PullDownPhase) { this.pulling = status } private setThresholdBoundary(boundary: ThresholdBoundary) { this.thresholdBoundary = boundary } private init() { this.handleBScroll() this.handleOptions(this.scroll.options.pullDownRefresh) this.handleHooks() this.watch() } private handleBScroll() { this.scroll.registerType([ PULLING_DOWN_EVENT, ENTER_THRESHOLD_EVENT, LEAVE_THRESHOLD_EVENT, ]) this.scroll.proxy(propertiesConfig) } private handleOptions(userOptions: PullDownRefreshOptions = {}) { userOptions = ( userOptions === true ? {} : userOptions ) as Partial const defaultOptions: PullDownRefreshConfig = { threshold: 90, stop: 40, } this.options = extend(defaultOptions, userOptions) this.scroll.options.probeType = Probe.Realtime } private handleHooks() { this.hooksFn = [] const scroller = this.scroll.scroller const scrollBehaviorY = scroller.scrollBehaviorY this.currentMinScrollY = this.cachedOriginanMinScrollY = scrollBehaviorY.minScrollPos this.registerHooks( this.scroll.hooks, this.scroll.hooks.eventTypes.contentChanged, () => { this.finishPullDown() } ) this.registerHooks( scrollBehaviorY.hooks, scrollBehaviorY.hooks.eventTypes.computeBoundary, (boundary: Boundary) => { // content is smaller than wrapper if (boundary.maxScrollPos > 0) { // allow scrolling when content is not full of wrapper boundary.maxScrollPos = -1 } boundary.minScrollPos = this.currentMinScrollY } ) // integrate with mousewheel if (this.hasMouseWheelPlugin()) { this.registerHooks( this.scroll, this.scroll.eventTypes.alterOptions, (mouseWheelOptions: MouseWheelConfig) => { const SANE_DISCRETE_TIME = 300 const SANE_EASE_TIME = 350 mouseWheelOptions.discreteTime = SANE_DISCRETE_TIME // easeTime > discreteTime ensure goInto checkPullDown function mouseWheelOptions.easeTime = SANE_EASE_TIME } ) this.registerHooks( this.scroll, this.scroll.eventTypes.mousewheelEnd, () => { // mouseWheel need trigger checkPullDown manually scroller.hooks.trigger(scroller.hooks.eventTypes.end) } ) } } private registerHooks(hooks: EventEmitter, name: string, handler: Function) { hooks.on(name, handler, this) this.hooksFn.push([hooks, name, handler]) } private hasMouseWheelPlugin() { return !!this.scroll.eventTypes.alterOptions } private watch() { const scroller = this.scroll.scroller this.watching = true this.registerHooks( scroller.hooks, scroller.hooks.eventTypes.end, this.checkPullDown ) this.registerHooks( this.scroll, this.scroll.eventTypes.scrollStart, this.resetStateBeforeScrollStart ) this.registerHooks( this.scroll, this.scroll.eventTypes.scroll, this.checkLocationOfThresholdBoundary ) if (this.hasMouseWheelPlugin()) { this.registerHooks( this.scroll, this.scroll.eventTypes.mousewheelStart, this.resetStateBeforeScrollStart ) } } private resetStateBeforeScrollStart() { // current fetching pulldownRefresh has ended if (!this.isFetchingStatus()) { this.setPulling(PullDownPhase.MOVING) this.setThresholdBoundary(ThresholdBoundary.DEFAULT) } } private checkLocationOfThresholdBoundary() { // pulldownRefresh is in the phase of Moving if (this.pulling === PullDownPhase.MOVING) { const scroll = this.scroll // enter threshold boundary const enteredThresholdBoundary = this.thresholdBoundary !== ThresholdBoundary.INSIDE && this.locateInsideThresholdBoundary() // leave threshold boundary const leftThresholdBoundary = this.thresholdBoundary !== ThresholdBoundary.OUTSIDE && !this.locateInsideThresholdBoundary() if (enteredThresholdBoundary) { this.setThresholdBoundary(ThresholdBoundary.INSIDE) scroll.trigger(ENTER_THRESHOLD_EVENT) } if (leftThresholdBoundary) { this.setThresholdBoundary(ThresholdBoundary.OUTSIDE) scroll.trigger(LEAVE_THRESHOLD_EVENT) } } } private locateInsideThresholdBoundary() { return this.scroll.y <= this.options.threshold } private unwatch() { const scroll = this.scroll const scroller = scroll.scroller this.watching = false scroller.hooks.off(scroller.hooks.eventTypes.end, this.checkPullDown) scroll.off(scroll.eventTypes.scrollStart, this.resetStateBeforeScrollStart) scroll.off(scroll.eventTypes.scroll, this.checkLocationOfThresholdBoundary) if (this.hasMouseWheelPlugin()) { scroll.off( scroll.eventTypes.mousewheelStart, this.resetStateBeforeScrollStart ) } } private checkPullDown() { const { threshold, stop } = this.options // check if a real pull down action if (this.scroll.y < threshold) { return false } if (this.pulling === PullDownPhase.MOVING) { this.modifyBehaviorYBoundary(stop) this.setPulling(PullDownPhase.FETCHING) this.scroll.trigger(PULLING_DOWN_EVENT) } this.scroll.scrollTo( this.scroll.x, stop, this.scroll.options.bounceTime, ease.bounce ) return this.isFetchingStatus() } private isFetchingStatus() { return this.pulling === PullDownPhase.FETCHING } private modifyBehaviorYBoundary(stopDistance: number) { const scrollBehaviorY = this.scroll.scroller.scrollBehaviorY // manually modify minScrollPos for a hang animation // to prevent from resetPosition this.cachedOriginanMinScrollY = scrollBehaviorY.minScrollPos this.currentMinScrollY = stopDistance scrollBehaviorY.computeBoundary() } finishPullDown() { if (this.isFetchingStatus()) { const scrollBehaviorY = this.scroll.scroller.scrollBehaviorY // restore minScrollY since the hang animation has ended this.currentMinScrollY = this.cachedOriginanMinScrollY scrollBehaviorY.computeBoundary() this.setPulling(PullDownPhase.DEFAULT) this.scroll.resetPosition(this.scroll.options.bounceTime, ease.bounce) } } // allow 'true' type is compat for beta version implements openPullDown(config: PullDownRefreshOptions = {}) { this.handleOptions(config) if (!this.watching) { this.watch() } } closePullDown() { this.unwatch() } autoPullDownRefresh() { const { threshold, stop } = this.options if (this.isFetchingStatus() || !this.watching) { return } this.modifyBehaviorYBoundary(stop) this.scroll.trigger(this.scroll.eventTypes.scrollStart) this.scroll.scrollTo(this.scroll.x, threshold) this.setPulling(PullDownPhase.FETCHING) this.scroll.trigger(PULLING_DOWN_EVENT) this.scroll.scrollTo( this.scroll.x, stop, this.scroll.options.bounceTime, ease.bounce ) } } ================================================ FILE: packages/pull-down/src/propertiesConfig.ts ================================================ const sourcePrefix = 'plugins.pullDownRefresh' const propertiesMap = [ { key: 'finishPullDown', name: 'finishPullDown' }, { key: 'openPullDown', name: 'openPullDown' }, { key: 'closePullDown', name: 'closePullDown' }, { key: 'autoPullDownRefresh', name: 'autoPullDownRefresh' } ] export default propertiesMap.map(item => { return { key: item.key, sourceKey: `${sourcePrefix}.${item.name}` } }) ================================================ FILE: packages/pull-up/README.md ================================================ # @better-scroll/pull-up [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/pull-up/README_zh-CN.md) The ability to inject a pull-up load for BetterScroll. ## Usage ```js import BScroll from '@better-scroll/core' import PullUp from '@better-scroll/pull-up' BScroll.use(PullUp) const bs = new BScroll('.wrapper', { pullUpLoad: true }) ``` ================================================ FILE: packages/pull-up/README_zh-CN.md ================================================ # @better-scroll/pull-up 为 BetterScroll 注入上拉加载的能力。 ## 使用 ```js import BScroll from '@better-scroll/core' import Pullup from '@better-scroll/pull-up' BScroll.use(Pullup) const bs = new BScroll('.wrapper', { pullUpLoad: true }) ``` ================================================ FILE: packages/pull-up/package.json ================================================ { "name": "@better-scroll/pull-up", "version": "2.5.1", "description": "pull up to load more data", "author": { "name": "jizhi", "email": "theniceangel@163.com" }, "main": "dist/pull-up.min.js", "module": "dist/pull-up.esm.js", "typings": "dist/types/index.d.ts", "publishConfig": { "access": "public" }, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios", "pull-up" ], "repository": { "type": "git", "url": "git+ssh://git@github.com/ustbhuangyi/better-scroll.git", "directory": "packages/pull-up" }, "dependencies": { "@better-scroll/core": "^2.5.1" }, "gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130" } ================================================ FILE: packages/pull-up/src/__tests__/index.spec.ts ================================================ import BScroll from '@better-scroll/core' jest.mock('@better-scroll/core') import PullUp from '@better-scroll/pull-up' import { Probe } from '@better-scroll/shared-utils' const createPullUpElements = () => { const wrapper = document.createElement('div') const content = document.createElement('div') wrapper.appendChild(content) return { wrapper } } describe('pullUp plugins', () => { let scroll: BScroll let pullUp: PullUp beforeEach(() => { // create DOM const { wrapper } = createPullUpElements() scroll = new BScroll(wrapper, {}) pullUp = new PullUp(scroll) }) afterEach(() => { jest.clearAllMocks() }) it('should proxy properties to scroll instance', () => { expect(scroll.proxy).toBeCalledWith([ { key: 'finishPullUp', sourceKey: 'plugins.pullUpLoad.finishPullUp', }, { key: 'openPullUp', sourceKey: 'plugins.pullUpLoad.openPullUp', }, { key: 'closePullUp', sourceKey: 'plugins.pullUpLoad.closePullUp', }, { key: 'autoPullUpLoad', sourceKey: 'plugins.pullUpLoad.autoPullUpLoad', }, ]) }) it('should handle default options and user options', () => { // case 1 scroll.options.pullUpLoad = true pullUp = new PullUp(scroll) expect(pullUp.options).toMatchObject({ threshold: 0, }) // case 2 scroll.options.pullUpLoad = { threshold: 40, } pullUp = new PullUp(scroll) expect(pullUp.options).toMatchObject({ threshold: 40, }) // case 3 scroll.options.pullUpLoad = { threshold: -40, } pullUp = new PullUp(scroll) expect(pullUp.options).toMatchObject({ threshold: -40, }) expect(scroll.options.probeType).toBe(Probe.Realtime) }) it('should modify maxScrollY when content is full of wrapper', () => { const scrollBehaviorY = scroll.scroller.scrollBehaviorY let boundary = { minScrollPos: 0, maxScrollPos: 20, } scrollBehaviorY.hooks.trigger( scrollBehaviorY.hooks.eventTypes.computeBoundary, boundary ) expect(boundary).toMatchObject({ minScrollPos: 0, maxScrollPos: -1, }) }) it('should checkPullUp', () => { const mockFn = jest.fn() scroll.on(scroll.eventTypes.pullingUp, mockFn) const pos1 = { x: 0, y: 100, } // simulate pullDown action scroll.movingDirectionY = -1 scroll.trigger(scroll.eventTypes.scroll, pos1) expect(mockFn).toHaveBeenCalledTimes(0) // simulate pullUp action const pos2 = { x: 0, y: -100, } scroll.movingDirectionY = 1 scroll.trigger(scroll.eventTypes.scroll, pos2) expect(mockFn).toHaveBeenCalledTimes(1) }) it('should trigger pullingUp once', () => { const mockFn = jest.fn() const pos = { x: 0, y: -100, } scroll.on(scroll.eventTypes.pullingUp, mockFn) // when scroll.movingDirectionY = 1 scroll.trigger(scroll.eventTypes.scroll, pos) scroll.trigger(scroll.eventTypes.scroll, pos) // then expect(mockFn).toBeCalledTimes(1) }) it('should work well when call finishPullUp()', () => { // simulate pullUp action const pos = { x: 0, y: -100, } scroll.movingDirectionY = 1 scroll.trigger(scroll.eventTypes.scroll, pos) pullUp.finishPullUp() expect(scroll.scroller.scrollBehaviorY.setMovingDirection).toBeCalledWith(0) expect(scroll.events.scrollEnd.length).toBe(2) expect(pullUp.watching).toBe(false) }) it('should work well when call closePullUp()', () => { pullUp.closePullUp() expect(pullUp.watching).toBe(false) expect(scroll.events.scroll.length).toBe(0) }) it('should work well when call openPullUp()', () => { pullUp.closePullUp() expect(pullUp.watching).toBe(false) expect(pullUp.options).toMatchObject({ threshold: 0, }) // modify options pullUp.openPullUp({ threshold: 200, }) expect(pullUp.options).toMatchObject({ threshold: 200, }) expect(pullUp.watching).toBe(true) }) it('should reset pulling when scrollEnd triggered', () => { // simulate pullUp action const pos = { x: 0, y: -100, } scroll.movingDirectionY = 1 scroll.trigger(scroll.eventTypes.scroll, pos) expect(pullUp.pulling).toBe(true) scroll.trigger(scroll.eventTypes.scrollEnd) expect(pullUp.pulling).toBe(false) }) it('should call watch() in scrollEnd hooks when pullingUp', () => { const pullUpMockFn = jest.fn() // simulate pullUp action const pos = { x: 0, y: -100, } scroll.on(scroll.eventTypes.pullingUp, pullUpMockFn) scroll.movingDirectionY = 1 scroll.trigger(scroll.eventTypes.scroll, pos) expect(pullUpMockFn).toBeCalledTimes(1) expect(pullUp.pulling).toBe(true) pullUp.finishPullUp() // because pulling is true, won't trigger pullingUp scroll.trigger(scroll.eventTypes.scroll, pos) expect(pullUpMockFn).toBeCalledTimes(1) expect(pullUp.watching).toBe(false) // register another watch in scrollEnd scroll.trigger(scroll.eventTypes.scrollEnd) scroll.movingDirectionY = 1 scroll.trigger(scroll.eventTypes.scroll, pos) expect(pullUpMockFn).toBeCalledTimes(2) }) it('should work well when call autoPullUpLoad()', () => { pullUp.autoPullUpLoad() const outOfBoundaryPos = -1 expect(scroll.scroller.scrollBehaviorY.setMovingDirection).toBeCalledWith( -1 ) expect(scroll.scrollTo).toBeCalledWith(0, outOfBoundaryPos, 800) expect(scroll.scrollTo).toBeCalledTimes(1) // closePullUp, and autoPullUpLoad will not work pullUp.closePullUp() pullUp.autoPullUpLoad() expect(scroll.scrollTo).toBeCalledTimes(1) }) it('should call finishPullUp when content DOM changed', () => { scroll.hooks.trigger(scroll.hooks.eventTypes.contentChanged) expect(scroll.scroller.scrollBehaviorY.setMovingDirection).toBeCalled() }) }) ================================================ FILE: packages/pull-up/src/index.ts ================================================ import BScroll, { Boundary } from '@better-scroll/core' import { Probe, Direction, extend, EventEmitter, } from '@better-scroll/shared-utils' import propertiesConfig from './propertiesConfig' export type PullUpLoadOptions = Partial | true export interface PullUpLoadConfig { threshold: number } declare module '@better-scroll/core' { interface CustomOptions { pullUpLoad?: PullUpLoadOptions } interface CustomAPI { pullUpLoad: PluginAPI } } interface PluginAPI { finishPullUp(): void openPullUp(config?: PullUpLoadOptions): void closePullUp(): void autoPullUpLoad(): void } const PULL_UP_HOOKS_NAME = 'pullingUp' export default class PullUp implements PluginAPI { static pluginName = 'pullUpLoad' private hooksFn: Array<[EventEmitter, string, Function]> pulling: boolean = false watching: boolean = false options: PullUpLoadConfig constructor(public scroll: BScroll) { this.init() } private init() { this.handleBScroll() this.handleOptions(this.scroll.options.pullUpLoad) this.handleHooks() this.watch() } private handleBScroll() { this.scroll.registerType([PULL_UP_HOOKS_NAME]) this.scroll.proxy(propertiesConfig) } private handleOptions(userOptions: PullUpLoadOptions = {}) { userOptions = (userOptions === true ? {} : userOptions) as Partial< PullUpLoadConfig > const defaultOptions: PullUpLoadConfig = { threshold: 0, } this.options = extend(defaultOptions, userOptions) this.scroll.options.probeType = Probe.Realtime } private handleHooks() { this.hooksFn = [] const { scrollBehaviorY } = this.scroll.scroller this.registerHooks( this.scroll.hooks, this.scroll.hooks.eventTypes.contentChanged, () => { this.finishPullUp() } ) this.registerHooks( scrollBehaviorY.hooks, scrollBehaviorY.hooks.eventTypes.computeBoundary, (boundary: Boundary) => { // content is smaller than wrapper if (boundary.maxScrollPos > 0) { // allow scrolling when content is not full of wrapper boundary.maxScrollPos = -1 } } ) } private registerHooks(hooks: EventEmitter, name: string, handler: Function) { hooks.on(name, handler, this) this.hooksFn.push([hooks, name, handler]) } private watch() { if (this.watching) { return } this.watching = true this.registerHooks( this.scroll, this.scroll.eventTypes.scroll, this.checkPullUp ) } private unwatch() { this.watching = false this.scroll.off(this.scroll.eventTypes.scroll, this.checkPullUp) } private checkPullUp(pos: { x: number; y: number }) { const { threshold } = this.options if ( this.scroll.movingDirectionY === Direction.Positive && pos.y <= this.scroll.maxScrollY + threshold ) { this.pulling = true // must reset pulling after scrollEnd this.scroll.once(this.scroll.eventTypes.scrollEnd, () => { this.pulling = false }) this.unwatch() this.scroll.trigger(PULL_UP_HOOKS_NAME) } } finishPullUp() { // reset Direction, fix #936 this.scroll.scroller.scrollBehaviorY.setMovingDirection(Direction.Default) if (this.pulling) { this.scroll.once(this.scroll.eventTypes.scrollEnd, () => { this.watch() }) } else { this.watch() } } // allow 'true' type is compat for beta version implements openPullUp(config: PullUpLoadOptions = {}) { this.handleOptions(config) this.watch() } closePullUp() { this.unwatch() } autoPullUpLoad() { const { threshold } = this.options const { scrollBehaviorY } = this.scroll.scroller if (this.pulling || !this.watching) { return } // simulate a pullUp action const NEGATIVE_VALUE = -1 const outOfBoundaryPos = scrollBehaviorY.maxScrollPos + threshold + NEGATIVE_VALUE this.scroll.scroller.scrollBehaviorY.setMovingDirection(NEGATIVE_VALUE) this.scroll.scrollTo( this.scroll.x, outOfBoundaryPos, this.scroll.options.bounceTime ) } } ================================================ FILE: packages/pull-up/src/propertiesConfig.ts ================================================ const sourcePrefix = 'plugins.pullUpLoad' const propertiesMap = [ { key: 'finishPullUp', name: 'finishPullUp' }, { key: 'openPullUp', name: 'openPullUp' }, { key: 'closePullUp', name: 'closePullUp' }, { key: 'autoPullUpLoad', name: 'autoPullUpLoad' } ] export default propertiesMap.map(item => { return { key: item.key, sourceKey: `${sourcePrefix}.${item.name}` } }) ================================================ FILE: packages/react-examples/README.md ================================================ # react-examples BetterScroll example in different React scenarios. ================================================ FILE: packages/react-examples/config-overrides.js ================================================ const { override, addWebpackAlias, removeModuleScopePlugin, } = require('customize-cra') const path = require('path') const resolve = (dir) => { return path.resolve(__dirname, dir) } const customWebpackConfig = (config) => { const oneOf = config.module.rules.find((rule) => rule.oneOf).oneOf // 修改 file-loader 配置 oneOf.map((rule) => { if ( Array.isArray(rule.test) && rule.options.name === 'static/media/[name].[hash:8].[ext]' ) { rule.options.esModule = false } return rule }) // 添加 stylus-loader const stylusLoader = { test: /\.styl$/, use: [ { loader: 'style-loader', }, { loader: 'css-loader', }, { loader: 'stylus-loader', }, ], } oneOf.unshift(stylusLoader) return config } module.exports = override( addWebpackAlias({ '@': resolve('src'), common: resolve('common'), }), removeModuleScopePlugin(), customWebpackConfig ) ================================================ FILE: packages/react-examples/package.json ================================================ { "name": "react-examples", "version": "2.5.1", "description": "Examples of BetterScroll", "private": true, "author": "maomao1996 <1714487678@qq.com>", "license": "MIT", "repository": { "type": "git", "url": "git+https://github.com/ustbhuangyi/better-scroll.git" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "scripts": { "dev": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-scripts eject" }, "dependencies": { "@testing-library/jest-dom": "^5.11.10", "@testing-library/react": "^11.2.6", "@testing-library/user-event": "^12.8.3", "better-scroll": "^2.5.1", "classnames": "^2.3.1", "react": "^17.0.2", "react-dom": "^17.0.2", "react-router-dom": "^5.2.0", "react-scripts": "4.0.3", "react-transition-group": "^4.4.1", "web-vitals": "^1.1.1" }, "eslintConfig": { "extends": [ "react-app", "react-app/jest" ] }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] }, "devDependencies": { "customize-cra": "^1.0.0", "react-app-rewired": "^2.1.8", "stylus": "^0.54.8", "stylus-loader": "^3.0.2" } } ================================================ FILE: packages/react-examples/public/index.html ================================================ BetterScroll
================================================ FILE: packages/react-examples/public/static/css/github-light.css ================================================ /* Copyright 2014 GitHub Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ .pl-c /* comment */ { color: #969896; } .pl-c1 /* constant, markup.raw, meta.diff.header, meta.module-reference, meta.property-name, support, support.constant, support.variable, variable.other.constant */, .pl-s .pl-v /* string variable */ { color: #0086b3; } .pl-e /* entity */, .pl-en /* entity.name */ { color: #795da3; } .pl-s .pl-s1 /* string source */, .pl-smi /* storage.modifier.import, storage.modifier.package, storage.type.java, variable.other, variable.parameter.function */ { color: #333; } .pl-ent /* entity.name.tag */ { color: #63a35c; } .pl-k /* keyword, storage, storage.type */ { color: #a71d5d; } .pl-pds /* punctuation.definition.string, string.regexp.character-class */, .pl-s /* string */, .pl-s .pl-pse .pl-s1 /* string punctuation.section.embedded source */, .pl-sr /* string.regexp */, .pl-sr .pl-cce /* string.regexp constant.character.escape */, .pl-sr .pl-sra /* string.regexp string.regexp.arbitrary-repitition */, .pl-sr .pl-sre /* string.regexp source.ruby.embedded */ { color: #183691; } .pl-v /* variable */ { color: #ed6a43; } .pl-id /* invalid.deprecated */ { color: #b52a1d; } .pl-ii /* invalid.illegal */ { background-color: #b52a1d; color: #f8f8f8; } .pl-sr .pl-cce /* string.regexp constant.character.escape */ { color: #63a35c; font-weight: bold; } .pl-ml /* markup.list */ { color: #693a17; } .pl-mh /* markup.heading */, .pl-mh .pl-en /* markup.heading entity.name */, .pl-ms /* meta.separator */ { color: #1d3e81; font-weight: bold; } .pl-mq /* markup.quote */ { color: #008080; } .pl-mi /* markup.italic */ { color: #333; font-style: italic; } .pl-mb /* markup.bold */ { color: #333; font-weight: bold; } .pl-md /* markup.deleted, meta.diff.header.from-file */ { background-color: #ffecec; color: #bd2c00; } .pl-mi1 /* markup.inserted, meta.diff.header.to-file */ { background-color: #eaffea; color: #55a532; } .pl-mdr /* meta.diff.range */ { color: #795da3; font-weight: bold; } .pl-mo /* meta.output */ { color: #1d3e81; } ================================================ FILE: packages/react-examples/public/static/css/normalize.css ================================================ /*! normalize.css v3.0.2 | MIT License | git.io/normalize */ /** * 1. Set default font family to sans-serif. * 2. Prevent iOS text size adjust after orientation change, without disabling * user zoom. */ html { font-family: sans-serif; /* 1 */ -ms-text-size-adjust: 100%; /* 2 */ -webkit-text-size-adjust: 100%; /* 2 */ } /** * Remove default margin. */ body { margin: 0; } /* HTML5 display definitions ========================================================================== */ /** * Correct `block` display not defined for any HTML5 element in IE 8/9. * Correct `block` display not defined for `details` or `summary.md` in IE 10/11 * and Firefox. * Correct `block` display not defined for `main` in IE 11. */ article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary { display: block; } /** * 1. Correct `inline-block` display not defined in IE 8/9. * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera. */ audio, canvas, progress, video { display: inline-block; /* 1 */ vertical-align: baseline; /* 2 */ } /** * Prevent modern browsers from displaying `audio` without controls. * Remove excess height in iOS 5 devices. */ audio:not([controls]) { display: none; height: 0; } /** * Address `[hidden]` styling not present in IE 8/9/10. * Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22. */ [hidden], template { display: none; } /* Links ========================================================================== */ /** * Remove the gray background color from active links in IE 10. */ a { background-color: transparent; } /** * Improve readability when focused and also mouse hovered in all browsers. */ a:active, a:hover { outline: 0; } /* Text-level semantics ========================================================================== */ /** * Address styling not present in IE 8/9/10/11, Safari, and Chrome. */ abbr[title] { border-bottom: 1px dotted; } /** * Address style set to `bolder` in Firefox 4+, Safari, and Chrome. */ b, strong { font-weight: bold; } /** * Address styling not present in Safari and Chrome. */ dfn { font-style: italic; } /** * Address variable `h1` font-size and margin within `section` and `article` * contexts in Firefox 4+, Safari, and Chrome. */ h1 { font-size: 2em; margin: 0.67em 0; } /** * Address styling not present in IE 8/9. */ mark { background: #ff0; color: #000; } /** * Address inconsistent and variable font size in all browsers. */ small { font-size: 80%; } /** * Prevent `sub` and `sup` affecting `line-height` in all browsers. */ sub, sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } sup { top: -0.5em; } sub { bottom: -0.25em; } /* Embedded content ========================================================================== */ /** * Remove border when inside `a` element in IE 8/9/10. */ img { border: 0; } /** * Correct overflow not hidden in IE 9/10/11. */ svg:not(:root) { overflow: hidden; } /* Grouping content ========================================================================== */ /** * Address margin not present in IE 8/9 and Safari. */ figure { margin: 1em 40px; } /** * Address differences between Firefox and other browsers. */ hr { box-sizing: content-box; height: 0; } /** * Contain overflow in all browsers. */ pre { overflow: auto; } /** * Address odd `em`-unit font size rendering in all browsers. */ code, kbd, pre, samp { font-family: monospace, monospace; font-size: 1em; } /* Forms ========================================================================== */ /** * Known limitation: by default, Chrome and Safari on OS X allow very limited * styling of `select`, unless a `border` property is set. */ /** * 1. Correct color not being inherited. * Known issue: affects color of disabled elements. * 2. Correct font properties not being inherited. * 3. Address margins set differently in Firefox 4+, Safari, and Chrome. */ button, input, optgroup, select, textarea { color: inherit; /* 1 */ font: inherit; /* 2 */ margin: 0; /* 3 */ } /** * Address `overflow` set to `hidden` in IE 8/9/10/11. */ button { overflow: visible; } /** * Address inconsistent `text-transform` inheritance for `button` and `select`. * All other form control elements do not inherit `text-transform` values. * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera. * Correct `select` style inheritance in Firefox. */ button, select { text-transform: none; } /** * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` * and `video` controls. * 2. Correct inability to style clickable `input` types in iOS. * 3. Improve usability and consistency of cursor style between image-type * `input` and others. */ button, html input[type="button"], /* 1 */ input[type="reset"], input[type="submit"] { -webkit-appearance: button; /* 2 */ cursor: pointer; /* 3 */ } /** * Re-set default cursor for disabled elements. */ button[disabled], html input[disabled] { cursor: default; } /** * Remove inner padding and border in Firefox 4+. */ button::-moz-focus-inner, input::-moz-focus-inner { border: 0; padding: 0; } /** * Address Firefox 4+ setting `line-height` on `input` using `!important` in * the UA stylesheet. */ input { line-height: normal; } /** * It's recommended that you don't attempt to style these elements. * Firefox's implementation doesn't respect box-sizing, padding, or width. * * 1. Address box sizing set to `content-box` in IE 8/9/10. * 2. Remove excess padding in IE 8/9/10. */ input[type="checkbox"], input[type="radio"] { box-sizing: border-box; /* 1 */ padding: 0; /* 2 */ } /** * Fix the cursor style for Chrome's increment/decrement buttons. For certain * `font-size` values of the `input`, it causes the cursor style of the * decrement button to change from `default` to `text`. */ input[type="number"]::-webkit-inner-spin-button, input[type="number"]::-webkit-outer-spin-button { height: auto; } /** * 1. Address `appearance` set to `searchfield` in Safari and Chrome. * 2. Address `box-sizing` set to `border-box` in Safari and Chrome * (include `-moz` to future-proof). */ input[type="search"] { -webkit-appearance: textfield; /* 1 */ /* 2 */ box-sizing: content-box; } /** * Remove inner padding and search cancel button in Safari and Chrome on OS X. * Safari (but not Chrome) clips the cancel button when the search input has * padding (and `textfield` appearance). */ input[type="search"]::-webkit-search-cancel-button, input[type="search"]::-webkit-search-decoration { -webkit-appearance: none; } /** * Define consistent border, margin, and padding. */ fieldset { border: 1px solid #c0c0c0; margin: 0 2px; padding: 0.35em 0.625em 0.75em; } /** * 1. Correct `color` not being inherited in IE 8/9/10/11. * 2. Remove padding so people aren't caught out if they zero out fieldsets. */ legend { border: 0; /* 1 */ padding: 0; /* 2 */ } /** * Remove default vertical scrollbar in IE 8/9/10/11. */ textarea { overflow: auto; } /** * Don't inherit the `font-weight` (applied by a rule above). * NOTE: the default cannot safely be changed in Chrome and Safari on OS X. */ optgroup { font-weight: bold; } /* Tables ========================================================================== */ /** * Remove most spacing between table cells. */ table { border-collapse: collapse; border-spacing: 0; } td, th { padding: 0; } ================================================ FILE: packages/react-examples/public/static/css/reset.css ================================================ /** * Eric Meyer's Reset CSS v2.0 (http://meyerweb.com/eric/tools/css/reset/) * http://cssreset.com */ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, menu, nav, output, ruby, section, summary, time, mark, audio, video, input { margin: 0; padding: 0; border: 0; font-size: 100%; font-weight: normal; vertical-align: baseline; } /* HTML5 display-role reset for older browsers */ article, aside, details, figcaption, figure, footer, header, menu, nav, section { display: block; } body { line-height: 1; } blockquote, q { quotes: none; } blockquote:before, blockquote:after, q:before, q:after { content: none; } table { border-collapse: collapse; border-spacing: 0; } /* custom */ a { color: #7e8c8d; -webkit-backface-visibility: hidden; } li { list-style: none; } ::-webkit-scrollbar { width: 5px; height: 5px; } ::-webkit-scrollbar-track-piece { background-color: rgba(0, 0, 0, 0.2); -webkit-border-radius: 6px; } ::-webkit-scrollbar-thumb:vertical { height: 5px; background-color: rgba(125, 125, 125, 0.7); -webkit-border-radius: 6px; } ::-webkit-scrollbar-thumb:horizontal { width: 5px; background-color: rgba(125, 125, 125, 0.7); -webkit-border-radius: 6px; } html, body { width: 100%; /* height: 100%; */ } body { -webkit-text-size-adjust: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); } ================================================ FILE: packages/react-examples/public/static/css/stylesheet.css ================================================ * { box-sizing: border-box; } body { padding: 0; margin: 0; font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.5; background-color: #fff; color: #606c71; } a { color: #1e6bb8; text-decoration: none; } a:hover { text-decoration: underline; } .btn { display: inline-block; margin-bottom: 1rem; padding: 0.75rem 1rem; color: rgba(255, 255, 255, 0.8); background-color: rgba(255, 255, 255, 0.1); border-color: rgba(255, 255, 255, 0.2); border-style: solid; border-width: 1px; border-radius: 0.3rem; transition: color 0.2s, background-color 0.2s, border-color 0.2s; } .btn + .btn { margin-left: 1rem; } .btn:hover { color: rgba(255, 255, 255, 0.9); text-decoration: none; background-color: rgba(255, 255, 255, 0.2); border-color: rgba(255, 255, 255, 0.3); } @media screen and (min-width: 42em) and (max-width: 64em) { .btn { font-size: 0.9rem; } } @media screen and (max-width: 42em) { .btn { display: block; width: 100%; padding: 0.75rem; font-size: 0.9rem; } .btn + .btn { margin-top: 1rem; margin-left: 0; } } .page-header { color: #fff; text-align: center; font-family: 'Patrick Hand', cursive; background-color: rgba(39,80,255, .7)} @media screen and (min-width: 64em) { .page-header { padding: 1.5rem 6rem 3rem 6rem; } } @media screen and (min-width: 42em) and (max-width: 64em) { .page-header { padding: 1.5rem 4rem 2rem 4rem; } } @media screen and (max-width: 42em) { .page-header { padding: 1.5rem 1rem 1rem 1rem; } } .project-name { margin-top: 0; margin-bottom: 0.1rem; } @media screen and (min-width: 64em) { .project-name { font-size: 3.25rem; } } @media screen and (min-width: 42em) and (max-width: 64em) { .project-name { font-size: 2.25rem; } } @media screen and (max-width: 42em) { .project-name { font-size: 1.75rem; } } .project-tagline { margin-bottom: 1rem; font-weight: normal; opacity: 0.7; } @media screen and (min-width: 64em) { .project-tagline { font-size: 1.25rem; } } @media screen and (min-width: 42em) and (max-width: 64em) { .project-tagline { font-size: 1.15rem; } } @media screen and (max-width: 42em) { .project-tagline { font-size: 1rem; } } .main-content :first-child { margin-top: 0; } .main-content img { max-width: 100%; } .main-content h1, .main-content h2, .main-content h3, .main-content h4, .main-content h5, .main-content h6 { margin-top: 1rem; margin-bottom: 1rem; } .main-content code { padding: 2px 4px; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; font-size: 0.9rem; color: #383e41; background-color: #f3f6fa; border-radius: 0.3rem; } .main-content pre { padding: 0.8rem; margin-top: 0; margin-bottom: 1rem; font: 1rem Consolas, "Liberation Mono", Menlo, Courier, monospace; color: #567482; word-wrap: normal; background-color: #f3f6fa; border: solid 1px #dce6f0; border-radius: 0.3rem; } .main-content pre > code { padding: 0; margin: 0; font-size: 0.9rem; color: #567482; word-break: normal; white-space: pre; background: transparent; border: 0; } .main-content .highlight { margin-bottom: 1rem; } .main-content .highlight pre { margin-bottom: 0; word-break: normal; } .main-content .highlight pre, .main-content pre { padding: 0.8rem; overflow: auto; font-size: 0.9rem; line-height: 1.45; border-radius: 0.3rem; } .main-content pre code, .main-content pre tt { display: inline; max-width: initial; padding: 0; margin: 0; overflow: initial; line-height: inherit; word-wrap: normal; background-color: transparent; border: 0; } .main-content pre code:before, .main-content pre code:after, .main-content pre tt:before, .main-content pre tt:after { content: normal; } .main-content ul, .main-content ol { margin-top: 0; } .main-content blockquote { padding: 0 1rem; margin-left: 0; color: #819198; border-left: 0.3rem solid #dce6f0; } .main-content blockquote > :first-child { margin-top: 0; } .main-content blockquote > :last-child { margin-bottom: 0; } .main-content table { display: block; width: 100%; overflow: auto; word-break: normal; word-break: keep-all; } .main-content table th { font-weight: bold; } .main-content table th, .main-content table td { padding: 0.5rem 1rem; border: 1px solid #e9ebec; } .main-content dl { padding: 0; } .main-content dl dt { padding: 0; margin-top: 1rem; font-size: 1rem; font-weight: bold; } .main-content dl dd { padding: 0; margin-bottom: 1rem; } .main-content hr { height: 2px; padding: 0; margin: 1rem 0; background-color: #eff0f1; border: 0; } @media screen and (min-width: 64em) { .main-content { padding: 2rem 8rem; margin: 0 auto; font-size: 1.1rem; } } @media screen and (min-width: 42em) and (max-width: 64em) { .main-content { padding: 2rem 2rem; font-size: 1.1rem; } } @media screen and (max-width: 42em) { .main-content { padding: 1rem 1rem 2rem 1rem; font-size: 1rem; } iframe { display: none } } .site-footer { padding-top: 2rem; margin-top: 2rem; border-top: solid 1px #d7d7d7; } .site-footer-owner { display: block; font-weight: bold; } .site-footer-credits { color: #819198; } @media screen and (min-width: 64em) { .site-footer { font-size: 1rem; } } @media screen and (min-width: 42em) and (max-width: 64em) { .site-footer { font-size: 1rem; } } @media screen and (max-width: 42em) { .site-footer { font-size: 0.9rem; } } ================================================ FILE: packages/react-examples/src/App.js ================================================ import { useHistory } from 'react-router-dom' import Router from './router' const examples = [ { name: 'core scroll', path: '/core/', }, { name: 'observe-dom', path: '/observe-dom/', }, { name: 'observe-image', path: '/observe-image/', }, { name: 'slide', path: '/slide', }, { name: 'zoom', path: '/zoom/', }, { name: 'picker', path: '/picker/', }, { name: 'pullup', path: '/pullup/', }, { name: 'pulldown', path: '/pulldown/', }, { name: 'scrollbar', path: '/scrollbar', }, { name: 'indicators', path: '/indicators', }, { name: 'infinity', path: '/infinity', }, { name: 'form', path: '/form', }, { name: 'nested-scroll', path: '/nested-scroll', }, { name: 'mouse-wheel', path: '/mouse-wheel', }, { name: 'movable', path: '/movable', }, { name: 'compose plugins', path: '/compose', }, ] const App = () => { const history = useHistory() const goPage = (path) => { history.push(path) } return ( <>

BetterScroll

inspired by iscroll, and it has a better scroll perfermance

    {examples.map((item) => (
  • goPage(item.path)} > {item.name}
  • ))}
) } export default App ================================================ FILE: packages/react-examples/src/index.js ================================================ import React from 'react' import ReactDOM from 'react-dom' import { HashRouter } from 'react-router-dom' import reportWebVitals from './reportWebVitals' import App from './App' import './index.styl' ReactDOM.render( , document.getElementById('root') ) // If you want to start measuring performance in your app, pass a function // to log results (for example: reportWebVitals(console.log)) // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals reportWebVitals() ================================================ FILE: packages/react-examples/src/index.styl ================================================ .page-header h1 @media screen and (min-width 42rem) margin-bottom 1rem @media screen and (max-width 42rem) margin-bottom 0.5rem .main-content .site-footer text-align center @media screen and (max-width 42rem) margin-top -1rem .example-list display flex justify-content space-between flex-wrap wrap @media screen and (min-width 42rem) margin 2rem 0 2rem 0 @media screen and (max-width 42rem) margin 1rem 0 .example-item background-color $color-white padding 0.8rem border 1px solid rgba(0, 0, 0, 0.1) box-shadow 0 1px 2px 0 rgba(0, 0, 0, 0.1) text-align center margin-bottom 1rem &.placeholder visibility hidden height 0 margin 0 padding 0 @media screen and (min-width 42rem) flex 0 1 28% @media screen and (max-width 42rem) flex 0 1 100% margin-bottom 1rem .view position fixed top 0 left 0 bottom 0 right 0 z-index 1 padding 20px background #fff transform translate3d(0, 0, 0) &.move-enter, &.move-leave-active transform translate3d(100%, 0, 0) &.move-enter-active, &.move-leave-active transition transform 0.3s ================================================ FILE: packages/react-examples/src/pages/compose/components/pullup-pulldown-outnested.js ================================================ import React, { useState, useRef, useEffect, useCallback } from 'react' import BScroll from '@better-scroll/core' import PullDown from '@better-scroll/pull-down' import PullUp from '@better-scroll/pull-up' import NestedScroll from '@better-scroll/nested-scroll' BScroll.use(PullDown) BScroll.use(PullUp) BScroll.use(NestedScroll) const useStableCallback = (callback) => { const callbackRef = useRef(callback) useEffect(() => { callbackRef.current = callback }) return useCallback((...args) => callbackRef.current(...args), []) } const TIME_BOUNCE = 800 const REQUEST_TIME = 3000 const THRESHOLD = 70 const STOP = 56 const TOP_OUT_ITEMS = [ '😀 😁 😂 🤣 😃 🙃', '👆🏻 Pull Down and refresh👆🏻', '🙂 🤔 😄 🤨 😐 🙃', '👆🏻 Pull Down and refresh👆🏻', '😔 😕 🙃 🤑 😲 ☹️', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 Pull Down and refresh👆🏻', '😔 😕 🙃 🤑 😲 ☹️ ', ] const BOTTOM_OUT_ITEMS = [ '😀 😁 😂 🤣 😃 🙃', '👆🏻 Pull Up and refresh👆🏻', '🙂 🤔 😄 🤨 😐 🙃', '👆🏻 Pull Up and refresh👆🏻', '😔 😕 🙃 🤑 😲 ☹️', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 Pull Up and refresh👆🏻', '😔 😕 🙃 🤑 😲 ☹️ ', ] const INNER_ITEMS = [ 'The Mountain top of Inner', '😀 😁 😂 🤣 😃 🙃 ', '👆🏻 inner scroll 👇🏻 ', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 inner scroll 👇🏻 ', '😔 😕 🙃 🤑 😲 ☹️ ', '👆🏻 inner scroll 👇🏻 ', '🐣 🐣 🐣 🐣 🐣 🐣 ', '👆🏻 inner scroll 👇🏻 ', '🐥 🐥 🐥 🐥 🐥 🐥 ', '👆🏻 inner scroll 👇🏻 ', '🤓 🤓 🤓 🤓 🤓 🤓 ', '👆🏻 inner scroll 👇🏻 ', '🦔 🦔 🦔 🦔 🦔 🦔 ', '👆🏻 inner scroll 👇🏻 ', '🙈 🙈 🙈 🙈 🙈 🙈 ', '👆🏻 inner scroll 👇🏻 ', '🚖 🚖 🚖 🚖 🚖 🚖 ', '👆🏻 inner scroll 👇🏻 ', '✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ', 'The Mountain foot of Inner', ] const ajaxGet = (/* url */) => { return new Promise((resolve) => { setTimeout(() => { resolve({ topOutItems: TOP_OUT_ITEMS, bottomOutItems: BOTTOM_OUT_ITEMS, }) }, REQUEST_TIME) }) } const PullUpPullDownNestedScroll = () => { const [beforePullDown, setBeforePullDown] = useState(true) const [isPullingDown, setIsPullingDown] = useState(false) const [isPullUpLoad, setIsPullUpLoad] = useState(false) const [topOutItems, setTopOutItems] = useState(TOP_OUT_ITEMS) const [bottomOutItems, setBottomOutItems] = useState(BOTTOM_OUT_ITEMS) const wrapperRef = useRef(null) const innerRef = useRef(null) const scrollRef = useRef(null) const requestData = async (type) => { try { const { topOutItems, bottomOutItems } = await ajaxGet(/* url */) if (type === 'load') { setBottomOutItems((prev) => bottomOutItems.concat(prev)) } else { setTopOutItems(topOutItems) setBottomOutItems(bottomOutItems) } } catch (err) { // handle err console.log(err) } } const finishPullDown = () => { scrollRef.current.finishPullDown() setTimeout(() => { setBeforePullDown(true) scrollRef.current.refresh() }, TIME_BOUNCE + 100) } const pullingDownHandler = useStableCallback(async () => { setBeforePullDown(false) setIsPullingDown(true) await requestData('refresh') setIsPullingDown(false) finishPullDown() }) const pullingUpHandler = useStableCallback(async () => { setIsPullUpLoad(true) await requestData('load') scrollRef.current.finishPullUp() scrollRef.current.refresh() setIsPullUpLoad(false) }) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { nestedScroll: { groupId: 'pullup-pullldown', }, bounceTime: TIME_BOUNCE, // pullDown options pullDownRefresh: { threshold: THRESHOLD, stop: STOP, }, // pullUp options pullUpLoad: { threshold: THRESHOLD, }, })) BS.on('pullingDown', pullingDownHandler) BS.on('pullingUp', pullingUpHandler) BS.on('scroll', (pos) => { console.log(pos.y) }) new BScroll(innerRef.current, { nestedScroll: { groupId: 'pullup-pullldown', }, // close bounce effects bounce: { top: false, bottom: false, }, }) } }, [pullingDownHandler, pullingUpHandler]) return (
Pull Down and refresh
Loading...
Refresh success
    {topOutItems.map((item, index) => (
  • {item}
  • ))}
    {INNER_ITEMS.map((item, index) => (
  • {item}
  • ))}
    {bottomOutItems.map((item, index) => (
  • {item}
  • ))}
{isPullUpLoad ? (
Loading...
) : (
Pull Up and load
)}
) } export default PullUpPullDownNestedScroll ================================================ FILE: packages/react-examples/src/pages/compose/components/pullup-pulldown-slide.js ================================================ import React, { useState, useRef, useEffect, useCallback } from 'react' import BScroll from '@better-scroll/core' import PullDown from '@better-scroll/pull-down' import PullUp from '@better-scroll/pull-up' import Slide from '@better-scroll/slide' BScroll.use(PullDown) BScroll.use(PullUp) BScroll.use(Slide) const useStableCallback = (callback) => { const callbackRef = useRef(callback) useEffect(() => { callbackRef.current = callback }) return useCallback((...args) => callbackRef.current(...args), []) } const TIME_BOUNCE = 700 const REQUEST_TIME = 1000 const THRESHOLD = 50 const STOP = 56 function generateData() { return Array.from({ length: 2 }, (_, i) => i) } const ajaxGet = (/* url */) => { return new Promise((resolve) => { setTimeout(() => { resolve(generateData()) }, REQUEST_TIME) }) } const PullUpPullDownSlide = () => { const [beforePullDown, setBeforePullDown] = useState(true) const [isPullingDown, setIsPullingDown] = useState(false) const [isPullUpLoad, setIsPullUpLoad] = useState(false) const [data, setData] = useState(generateData()) const stopPullup = useRef(false) const wrapperRef = useRef(null) const pulldownRef = useRef(null) const pullupRef = useRef(null) const scrollRef = useRef(null) const requestData = async (type) => { try { const newData = await ajaxGet(/* url */) if (type === 'load') { setData((prev) => newData.concat(prev)) } else { setData(newData) } } catch (err) { // handle err console.log(err) } } const resetPullUpPos = () => { const pullUpEle = pullupRef.current pullUpEle.style.transform = `translateY(0px) translateZ(0)` pullUpEle.style.transition = 'transform 0.5s' stopPullup.current = false } const finishPullDown = () => { scrollRef.current.finishPullDown() scrollRef.current.enabled = true setTimeout(() => { setBeforePullDown(true) scrollRef.current.refresh() }, TIME_BOUNCE + 100) } const pullingDownHandler = useStableCallback(async () => { setBeforePullDown(false) setIsPullingDown(true) scrollRef.current.enabled = false await requestData('refresh') setIsPullingDown(false) finishPullDown() }) const pullingUpHandler = useStableCallback(async () => { setIsPullUpLoad(true) scrollRef.current.enabled = false await requestData('load') scrollRef.current.finishPullUp() scrollRef.current.enabled = true scrollRef.current.refresh() setIsPullUpLoad(false) resetPullUpPos() }) const scrollHandler = useStableCallback((pos) => { if (pos.y >= 0) { const pullDownEle = pulldownRef.current const { height: pulldownH } = getComputedStyle(pullDownEle, null) pullDownEle.style.transform = `translateY(${ -parseInt(pulldownH) + pos.y }px) translateZ(0)` } const pullupThreshold = -30 const maxScrollY = scrollRef.current.maxScrollY if (pos.y - maxScrollY <= pullupThreshold && isPullUpLoad) { stopPullup.current = true } if (pos.y - maxScrollY <= 0 && !stopPullup.current) { const pullUpEle = pullupRef.current pullUpEle.style.transform = `translateY(${ pos.y - maxScrollY }px) translateZ(0)` } }) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { scrollY: true, bounceTime: TIME_BOUNCE, momentum: false, // pullDown options pullDownRefresh: { threshold: THRESHOLD, stop: STOP, }, // pullUp options pullUpLoad: { threshold: -THRESHOLD, }, // slide options slide: { threshold: 5, disableSetHeight: true, autoplay: false, loop: false, }, })) BS.on('pullingDown', pullingDownHandler) BS.on('pullingUp', pullingUpHandler) BS.on('scroll', scrollHandler) } }, [pullingDownHandler, pullingUpHandler, scrollHandler]) return (
{/* pull down */}
Pull Down and refresh
Loading...
Refresh success
{/* slide item */}
{data.map((_, index) => (
{`Page ${index} `}
))}
{/* pollup */}
{isPullUpLoad ? (
Loading...
) : (
Pull Up and load
)}
) } export default PullUpPullDownSlide ================================================ FILE: packages/react-examples/src/pages/compose/components/pullup-pulldown.js ================================================ import React, { useState, useRef, useEffect, useCallback } from 'react' import BScroll from '@better-scroll/core' import PullDown from '@better-scroll/pull-down' import PullUp from '@better-scroll/pull-up' BScroll.use(PullDown) BScroll.use(PullUp) const useStableCallback = (callback) => { const callbackRef = useRef(callback) useEffect(() => { callbackRef.current = callback }) return useCallback((...args) => callbackRef.current(...args), []) } const TIME_BOUNCE = 800 const REQUEST_TIME = 3000 const THRESHOLD = 70 const STOP = 56 function generateData() { return Array.from({ length: 30 }, (_, i) => i) } const ajaxGet = (/* url */) => { return new Promise((resolve) => { setTimeout(() => { resolve(generateData()) }, REQUEST_TIME) }) } const PullUpPullDown = () => { const [beforePullDown, setBeforePullDown] = useState(true) const [isPullingDown, setIsPullingDown] = useState(false) const [isPullUpLoad, setIsPullUpLoad] = useState(false) const [data, setData] = useState(generateData()) const wrapperRef = useRef(null) const scrollRef = useRef(null) const requestData = async (type) => { try { const newData = await ajaxGet(/* url */) if (type === 'load') { setData((prev) => newData.concat(prev)) } else { setData(newData) } } catch (err) { // handle err console.log(err) } } const finishPullDown = () => { scrollRef.current.finishPullDown() setTimeout(() => { setBeforePullDown(true) scrollRef.current.refresh() }, TIME_BOUNCE + 100) } const pullingDownHandler = useStableCallback(async () => { setBeforePullDown(false) setIsPullingDown(true) await requestData('refresh') setIsPullingDown(false) finishPullDown() }) const pullingUpHandler = useStableCallback(async () => { setIsPullUpLoad(true) await requestData('load') scrollRef.current.finishPullUp() scrollRef.current.refresh() setIsPullUpLoad(false) }) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { scrollY: true, bounceTime: TIME_BOUNCE, // pullDown options pullDownRefresh: { threshold: THRESHOLD, stop: STOP, }, // pullUp options pullUpLoad: { threshold: THRESHOLD, }, })) BS.on('pullingDown', pullingDownHandler) BS.on('pullingUp', pullingUpHandler) BS.on('scroll', (pos) => { console.log(pos.y) }) } }, [pullingDownHandler, pullingUpHandler]) return (
Pull Down and refresh
Loading...
Refresh success
    {data.map((_, index) => (
  • {`I am item ${index} `}
  • ))}
{isPullUpLoad ? (
Loading...
) : (
Pull Up and load
)}
) } export default PullUpPullDown ================================================ FILE: packages/react-examples/src/pages/compose/components/slide-nested.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import NestedScroll from '@better-scroll/nested-scroll' import Slide from '@better-scroll/slide' BScroll.use(NestedScroll) BScroll.use(Slide) const DATA = [ '😀 😁 😂 🤣 😃 🙃 ', '👆🏻 outer scroll 👇🏻 ', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 outer scroll 👇🏻 ', '😔 😕 🙃 🤑 😲 ☹️ ', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 outer scroll 👇🏻 ', '😔 😕 🙃 🤑 😲 ☹️ ', ] const NestedScrollPage = () => { const outerWrapperRef = useRef(null) const innerWrapperRef = useRef(null) useEffect(() => { const outerScrollRef = new BScroll(outerWrapperRef.current, { nestedScroll: { groupId: 'slide-nested', }, }) const innerScrollRef = new BScroll(innerWrapperRef.current, { nestedScroll: { groupId: 'slide-nested', }, scrollX: true, scrollY: false, momentum: false, // close bounce effects bounce: { top: false, bottom: false, }, slide: { loop: true, autoplay: false, }, }) return () => { outerScrollRef.destroy() innerScrollRef.destroy() } }, []) return (
    {DATA.map((item, index) => (
  • {item}
  • ))}
page 1
page 2
page 3
page 4
    {DATA.map((item, index) => (
  • {item}
  • ))}
) } export default NestedScrollPage ================================================ FILE: packages/react-examples/src/pages/compose/index.js ================================================ import React from 'react' import './index.styl' const examples = [ { path: '/compose/pullup-pulldown', name: 'pullup-pulldown', }, { path: '/compose/pullup-pulldown-slide', name: 'pullup-pulldown-slide', }, { path: '/compose/pullup-pulldown-outnested', name: 'pullup-pulldown-outnested', }, { path: '/compose/slide-nested', name: 'slide-nested', }, ] const Compose = (props) => { const goPage = (path) => { props.history.push(path) } return (
    {examples.map((item) => (
  • goPage(item.path)} key={item.path} > {item.name}
  • ))}
{props.children}
) } export default Compose ================================================ FILE: packages/react-examples/src/pages/compose/index.styl ================================================ .pullup-down height 100% .pullup-down-bswrapper position relative height 100% padding 0 10px border 1px solid #ccc overflow hidden .pullup-down-list padding 0 .pullup-down-list-item padding 10px 0 list-style none border-bottom 1px solid #ccc .pulldown-wrapper position absolute width 100% padding 20px box-sizing border-box transform translateY(-100%) translateZ(0) text-align center color #999 .pullup-wrapper padding 20px text-align center color #999 .pullup-down-slide-wrapper height 100% overflow hidden .pullup-pulldown-slide-bswrapper position relative height 100% overflow hidden .pullup-down-list padding 0 .pullup-pulldown-slide-item list-style none width 100% line-height 200px text-align center font-size 26px transform translate3d(0,0,0) backface-visibility hidden box-sizing: border-box .pulldown-wrapper position absolute width 100% padding 20px box-sizing border-box transform translateY(-100%) translateZ(0) text-align center color #999 .pullup-wrapper height 40px !important padding 20px text-align center color #999 .page1 background-color #D6EADF .page2 background-color #DDA789 .page3 background-color #C3D899 .page0 background-color #F2D4A7 .pullup-pulldown-outnested height: 100% .outer-wrapper .inner-wrapper border: 2px solid #62B791 border-radius: 5px transform: rotate(0deg) position: relative overflow: hidden .outer-wrapper height: 100% border: 1px solid rgba(0, 0, 0, .1) .inner-wrapper height: 240px background-color: rgba(98,183,145, 0.2) .inner-list-item height: 50px line-height: 50px text-align: center list-style: none .outer-list-item2, .outer-list-item height: 40px line-height: 40px text-align: center list-style: none .pulldown-wrapper position absolute width 100% padding 20px box-sizing border-box transform translateY(-100%) translateZ(0) text-align center color #999 .pullup-wrapper padding 20px text-align center color #999 .slide-nested height: 100% .outer-wrapper .inner-wrapper border-radius: 5px transform: rotate(0deg) position: relative overflow: hidden box-sizing: border-box .outer-wrapper height: 100% border: 1px solid rgba(0, 0, 0, .1) border: 1px solid #62B791 .inner-wrapper height: 200px .outer-list-item height: 40px line-height: 40px text-align: center list-style: none .slide-banner-content height 200px white-space nowrap width: 100% font-size 0 .slide-item display inline-block height 200px width 100% line-height 200px text-align center font-size 26px &.page1 background-color #95B8D1 &.page2 background-color #DDA789 &.page3 background-color #C3D899 &.page4 background-color #F2D4A7 ================================================ FILE: packages/react-examples/src/pages/core/components/default.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' const emojis = [ '😀 😁 😂 🤣 😃', '😄 😅 😆 😉 😊', '😫 😴 😌 😛 😜', '👆🏻 😒 😓 😔 👇🏻', '😑 😶 🙄 😏 😣', '😞 😟 😤 😢 😭', '🤑 😲 🙄 🙁 😖', '👍 👎 👊 ✊ 🤛', '🙄 ✋ 🤚 🖐 🖖', '👍🏼 👎🏼 👊🏼 ✊🏼 🤛🏼', '☝🏽 ✋🏽 🤚🏽 🖐🏽 🖖🏽', '🌖 🌗 🌘 🌑 🌒', '💫 💥 💢 💦 💧', '🐠 🐟 🐬 🐳 🐋', '😬 😐 😕 😯 😶', '😇 😏 😑 😓 😵', '🐥 🐣 🐔 🐛 🐤', '💪 ✨ 🔔 ✊ ✋', '👇 👊 👍 👈 👆', '💛 👐 👎 👌 💘', '👍🏼 👎🏼 👊🏼 ✊🏼 🤛🏼', '☝🏽 ✋🏽 🤚🏽 🖐🏽 🖖🏽', '🌖 🌗 🌘 🌑 🌒', '💫 💥 💢 💦 💧', '🐠 🐟 🐬 🐳 🐋', '😬 😐 😕 😯 😶', '😇 😏 😑 😓 😵', '🐥 🐣 🐔 🐛 🐤', '💪 ✨ 🔔 ✊ ✋', '👇 👊 👍 👈 👆', '💛 👐 👎 👌 💘', ] const handleClick = (item) => { window.alert(item) } const Default = () => { const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { probeType: 3, click: true, })) BS.on('scrollStart', () => { console.log('scrollStart-') }) BS.on('scroll', ({ y }) => { console.log('scrolling-') }) BS.on('scrollEnd', (pos) => { console.log(pos) }) } }, []) return (
{emojis.map((item, index) => (
handleClick(item)} > {item}
))}
) } export default Default ================================================ FILE: packages/react-examples/src/pages/core/components/dynamic-content.js ================================================ import React, { useState, useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const nums1 = createArray(30) const nums2 = createArray(60) const DynamicContent = () => { const [switcher, setSwitcher] = useState(false) const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { probeType: 3, })) BS.on('contentChanged', (content) => { console.log('--- newContent ---') console.log(content) }) BS.on('scroll', () => { console.log('scrolling-') }) BS.on('scrollEnd', () => { console.log('scrollingEnd') }) } }, []) useEffect(() => { if (scrollRef.current) { scrollRef.current.refresh() } }, [switcher]) const handleClick = () => { setSwitcher((value) => !value) } return (
{switcher ? (
{nums2.map((item) => (
{nums2.length - item + 1}
))}
) : (
{nums1.map((item) => (
{item}
))}
)}
) } export default DynamicContent ================================================ FILE: packages/react-examples/src/pages/core/components/freescroll.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' const FreeScroll = () => { const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { scrollRef.current = new BScroll(wrapperRef.current, { freeScroll: true, bounce: { bottom: false, left: false, right: false, top: false, }, }) } }, []) return (

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.

) } export default FreeScroll ================================================ FILE: packages/react-examples/src/pages/core/components/horizontal-rotated.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const nums = createArray(8) const HorizontalRotated = () => { const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { scrollRef.current = new BScroll(wrapperRef.current, { scrollX: true, scrollY: false, // v2.3.0 quadrant: 3, // rotate 180 }) } }, []) return (
Flipping layout via CSS
{nums.map((item, index) => (
{item}
))}
) } export default HorizontalRotated ================================================ FILE: packages/react-examples/src/pages/core/components/horizontal.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' const emojis = [ '👉🏼 😁 😂 🤣 👈🏼', '😄 😅 😆 😉 😊', '😫 😴 😌 😛 😜', '👆🏻 😒 😓 😔 👇🏻', '😑 😶 🙄 😏 😣', '😞 😟 😤 😢 😭', '🤑 😲 ☹️ 🙁 😖', '👍 👎 👊 ✊ 🤛', '☝️ ✋ 🤚 🖐 🖖', '👍🏼 👎🏼 👊🏼 ✊🏼 🤛🏼', '☝🏽 ✋🏽 🤚🏽 🖐🏽 🖖🏽', '🌖 🌗 🌘 🌑 🌒', ] const Horizontal = () => { const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { scrollX: true, probeType: 3, // listening scroll event })) BS.on('scrollStart', () => { console.log('scrollStart-') }) BS.on('scroll', ({ y }) => { console.log('scrolling-') }) BS.on('scrollEnd', (pos) => { console.log(pos) }) } }, []) return (
{emojis.map((item, index) => (
{item}
))}
) } export default Horizontal ================================================ FILE: packages/react-examples/src/pages/core/components/specified-content.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const nums = createArray(30) const SpecifiedContent = () => { const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { specifiedIndexAsContent: 1, probeType: 3, })) BS.on('scroll', () => { console.log('scrolling-') }) BS.on('scrollEnd', () => { console.log('scrollingEnd') }) } }, []) return (
The Blue area is not taken as BetterScroll's content
{nums.map((item) => (
{item}
))}
) } export default SpecifiedContent ================================================ FILE: packages/react-examples/src/pages/core/components/vertical-rotated.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const nums = createArray(8) const VerticalRotated = () => { const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { scrollRef.current = new BScroll(wrapperRef.current, { // v2.3.0 quadrant: 2, // rotate 90 }) } }, []) return (
Horizontal layout via CSS
{nums.map((item, index) => (
{item}
))}
) } export default VerticalRotated ================================================ FILE: packages/react-examples/src/pages/core/index.js ================================================ import React from 'react' import './index.styl' const examples = [ { path: '/core/default', name: 'vertical', }, { path: '/core/horizontal', name: 'horizontal', }, { path: '/core/dynamic-content', name: 'dynamic-content', }, { path: '/core/specified-content', name: 'specified-content', }, { path: '/core/freescroll', name: 'freescroll', }, { path: '/core/vertical-rotated', name: 'vertical-rotated(v2.3.0)', }, { path: '/core/horizontal-rotated', name: 'horizontal-rotated(v2.3.0)', }, ] const Core = (props) => { const goPage = (path) => { props.history.push(path) } return (
    {examples.map((item) => (
  • goPage(item.path)} key={item.path} > {item.name}
  • ))}
{props.children}
) } export default Core ================================================ FILE: packages/react-examples/src/pages/core/index.styl ================================================ .free-scroll-container &.view position fixed!important .core-container .scroll-wrapper height 400px position relative overflow hidden .scroll-item height 50px line-height 50px font-size 24px font-weight bold border-bottom 1px solid #eee text-align center &:nth-child(2n) background-color #f3f5f7 &:nth-child(2n+1) background-color #42b983 .horizontal-container .scroll-wrapper position relative width 90% margin 80px auto white-space nowrap border 3px solid #42b983 border-radius 5px overflow hidden .scroll-content display inline-block .scroll-item height 50px line-height 50px font-size 24px display inline-block text-align center padding 0 10px .core-dynamic-content-container text-align center .scroll-wrapper height 300px overflow hidden .scroll-item height 50px line-height 50px font-size 24px font-weight bold border-bottom 1px solid #eee text-align center &:nth-child(2n) background-color #f3f5f7 &:nth-child(2n+1) background-color #42b983 .btn margin 40px auto padding 10px color #fff!important border-radius 4px font-size 20px background-color #666!important .core-specified-content-container text-align center .scroll-wrapper height 400px overflow hidden border 1px solid #42b983 .ignore-content padding 20px color white font-size 20px font-weight bold background-color #2c3e50 .scroll-item height 50px line-height 50px font-size 24px font-weight bold border-bottom 1px solid #eee text-align center &:nth-child(2n) background-color #f3f5f7 &:nth-child(2n+1) background-color #42b983 .free-scroll-container position: relative width: 100% height: 100% .free-scroll-wrapper position: relative width: 100% height: 100% border: 1px solid rgb(96, 108, 113) box-sizing: border-box .scroll-wrapper position: absolute top: 0 left: 0 right: 0 bottom: 0 overflow: hidden .scroll-content background-color: #efeff4 width: 1500px height: 1000px p font-size: 16px padding: 20px line-height: 200% margin: 0 .vertical-rotated-container .description text-align center .scroll-wrapper height 250px width 100px position relative overflow hidden margin 0 auto border 1px solid #ccc transform rotate(90deg) .scroll-item height 100px width 100px line-height 100px font-size 24px font-weight bold text-align center &:nth-child(2n) background-color #f3f5f7 &:nth-child(2n+1) background-color #42b983 .horizontal-rotated-container .description margin-bottom 40px text-align center .scroll-wrapper height 100px width 250px position relative overflow hidden white-space nowrap margin 0 auto border 1px solid #ccc transform rotate(180deg) .scroll-content display inline-block .scroll-item height 100px width 100px line-height 100px font-size 24px font-weight bold text-align center display inline-block &:nth-child(2n) background-color #f3f5f7 &:nth-child(2n+1) background-color #42b983 ================================================ FILE: packages/react-examples/src/pages/form/components/textarea.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const data = createArray(10) const Textarea = () => { const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { scrollRef.current = new BScroll(wrapperRef.current, { autoBlur: true, // for blur }) } }, []) return (
    {data.map((item) => (
  • ))} {data.map((item) => (
  • ))}
) } export default Textarea ================================================ FILE: packages/react-examples/src/pages/form/index.js ================================================ import React from 'react' import './index.styl' const examples = [ { path: '/form/textarea', name: 'textarea', }, ] const Form = (props) => { const goPage = (path) => { props.history.push(path) } return (
    {examples.map((item) => (
  • goPage(item.path)} key={item.path} > {item.name}
  • ))}
{props.children}
) } export default Form ================================================ FILE: packages/react-examples/src/pages/form/index.styl ================================================ .textarea-container height: 100% .textarea-wrapper height: 100% padding: 0 10px border: 1px solid #ccc overflow: hidden .textarea-list padding: 0 text-align: center .textarea-list-item padding: 10px 0 height: 40px list-style: none border-bottom: 1px solid #ccc .textarea-text border: 2px solid #62B791 border-radius: 5px margin: 40px auto 0 line-height: 2 font-size: 20px height: 200px ================================================ FILE: packages/react-examples/src/pages/indicators/components/minimap.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import Indicators from '@better-scroll/indicators' import dinnerLink from './dinner.jpg' BScroll.use(Indicators) const Minimap = () => { const wrapperRef = useRef(null) const indicatorWrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { scrollRef.current = new BScroll(wrapperRef.current, { startX: -50, startY: -50, freeScroll: true, bounce: false, indicators: [ { relationElement: indicatorWrapperRef.current, // choose div.scroll-indicator-handle as indicatorHandle relationElementHandleElementIndex: 1, }, ], }) } }, []) return (
{/* maxWidth is used to overwrite vuepress default theme style */} {/* because this component is used in vuepress markdown as a demo */} custom
custom
) } export default Minimap ================================================ FILE: packages/react-examples/src/pages/indicators/components/parallax-scroll.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import Indicators from '@better-scroll/indicators' BScroll.use(Indicators) const ParallaxScroll = () => { const wrapperRef = useRef(null) const indicator1Ref = useRef(null) const indicator2Ref = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { scrollRef.current = new BScroll(wrapperRef.current, { freeScroll: true, bounce: false, indicators: [ { relationElement: indicator1Ref.current, interactive: false, ratio: 0.4, }, { relationElement: indicator2Ref.current, interactive: false, ratio: 0.2, }, ], }) } }, []) return (
) } export default ParallaxScroll ================================================ FILE: packages/react-examples/src/pages/indicators/index.js ================================================ import React from 'react' import './index.styl' const examples = [ { path: '/indicators/minimap', name: 'minimap', }, { path: '/indicators/parallax-scroll', name: 'parallax scroll', }, ] const Indicators = (props) => { const goPage = (path) => { props.history.push(path) } return (
    {examples.map((item) => (
  • goPage(item.path)} key={item.path} > {item.name}
  • ))}
{props.children}
) } export default Indicators ================================================ FILE: packages/react-examples/src/pages/indicators/index.styl ================================================ .minimap-container .scroll-wrapper width 320px height 180px overflow hidden .scroll-content width 1920px height 1080px .scroll-indicator margin-top 15px width 320px height 180px position relative .scroll-indicator-bg position absolute width 100% height 100% .scroll-indicator-handle position absolute border 1px solid white box-shadow 0 0 5px white width 64px height 36px z-index 1 background-color rgba(255, 255, 255, 0.3) .parallax-scroll-container height 100% .parallax-scroll-box position relative width 100% height 100% box-sizing border-box border 1px solid #fe0 .scroll-wrapper position absolute z-index 3 top 0 right 0 bottom 0 left 0 overflow hidden .scroll-content width 100% height 4000px background: url('./components/galaxies1.png') .scroll-indicator position absolute top 0 right 0 bottom 0 left 0 overflow hidden &.stars1 z-index 2 &.stars2 z-index 1 .star1-bg height 3000px background: url('./components/galaxies2.png') .star2-bg height 2000px background: url('./components/stars.jpg') ================================================ FILE: packages/react-examples/src/pages/infinity/data/message.json ================================================ [ "when you popState and actually being well, we expect it further", "But I'm going to take care of ripping out my code in the fact that just something like that", "And what we'll createdCallbacks than that you can still read what each one of this should go out", "So just return Promise back and do this, the route equals", "ah, let's do a clearRoutes it says I'm not going to do", "At least trying new Promise", "then, and then it's going to check what that", "And we zoom in, then you can kind of set, except for a router", "Now strictly today", "I'm going to just takes an iterable as well be to add a visible", "Anyway, so that we'll do a link", "So what I'm going to minify this, so I'll just console", "log data for now, just sometimes look at that", "not then if we wanted to do position from the registerElements primed and red", "That isn't get called", "At all", "No", "Interesting that misc here", "So what was a regular expression", "Because once you get over doing a fancy techniques", "And let's see", "OK, we broke thing to do", "Right", "document", "" So", "Yeah", "", "which is fine", "And that we'll do sc", "view", "So what you draw the line where is it", "Where is being run", "I think, a million times look at it and styles an iteration, ES2015 update the content for is this", "routes equals Array", "from", "Hm, that might be a trade", "off, because we're just do an animation", "in the attached", "Look at this push", "pull kind of useful to have layout root here is it", "That by default, what we going to grab the", "Yes", "In router, I think, would let's say, for example", "So let's make it can be just this the hour mark on the actual contents", "We just loads though it was the way, a nice this", "Are you would be a little bit more pretty raw, this is a day, dude", "Border", "radius, that", "And I'm going to just do that will take something else", "And thank you might now", "That is the next time, I'm going to come into misc", "And somebody actually not", "source equals home", "But if I was sending me to resolve where we go", "All right", "And it makes JavaScript", "And I have run again", "Normally a massive, as I said, this is always, I'm going to call the different [INAUDIBLE] Hm", "Wow", "We have happen on screen, and the otherwise, don't want", "Yeah, and forth in the new path", "So we don't you use that might very wrong", "But in a customary bug", "Don't forget to hidden or display to none, things like a race when you are actually really long time I want to tell that is where you go", "And that work", "Yeah, and I'm going to do today", "I had misc are all the create one of the performance stuff", "But if you had lots of tea", "Yeah", "Now we're going to come in", "But did working as intended it", "So we can be able to be watching it straightforward slash", "And that, I think that will be all the like since we are valid concept for this, the root of this called HTML5 routing, which I don't know", "I just feels OK, but hopefully, and opacity 0, and it's just put a z", "index of 1 on that's going to be sort of handling of attachedCallback, and we want to transform scale very well be true for them is amazing, like across from the new one that", "You know", "Yeah, we could see now, all being we won't do this thing today", "And so this is a current view", "We have a question ties in", "Why not", "source equals router, why not", "And I think that we'd probably, if we've already to allow it to be the thing", "Oh, all right, so we get it, because I have to juggle it all", "No", "I feel I agree", "It would actually get it, because otherwise, we still have this", "routes", "keys", "So this is a layout boundary", "It's the cause", "Yeah, 3 pixels", "OK", "So since that's true", "And this stuff", "And that work", "Good point, or strict, and then the URL, changed", "But I'm going to, let's see, what we're any", "So the new view, think about", "And then we've defer, why not", "Let's fail", "So this newView, newView is never watching is I was that", "so that it's a compass", "Oh", "North, east, south, we called, all be no ES", "anything", "What I'm curious about your question here", "And I'm going to say", "so let's see", "So let's see", "So we'll say from this animations that we want to do this so that this point", "So we want us to cover next week", "We can actually", "But that they've all been set it", "Yeah", "And at the top and misc here", "But it will be run into a bit different sections", "And I think you'd want each of there's no DOM tree reason", "Well, yeah", "OK, so we have a couple of click for clicks", "And so if we see about this", "So what I think things that I really good start", "script tags at home, kids", "Don't do this file to actually", "Woo", "I made, sir", "So again, particular line of the", "let's call it sc for Supercharged", "There's no", "It's a compass", "Oh", "right", "newView, newView is the simplicity at this one anything below 2015, right", "It broke", "OK, let's see", "So we're going to removeEventListener", "You are the nicest", "something that you know, we'll create that doesn't necessarily end up with something new to these pages", "In router", "And certainly, as I said, you could usually just delete the constructor but createdCallback", "Oh, well, let link of the", "Yes", "If we had to do is I want us to come up writing apps, it can actually, this push", "pull kind of data, which version of something", "So what they can be about view or something that have a thing to do a trade", "off because you've got memory constraints and all these function", "So let's see if", "oh, do we wanted to do this", "If you're attach, what we'd want to know", "That is important think in so that goes to control of [", "UI ", "] transitions, particular expression", "Right, so the otherwise, it should also work on the layout, which might because we're actually remind yourselves that I can do it", "Yeah", "So that, in theory, place all the content as well when that have new ideas", "So this should be a class list, we'll create one of these, what we'll do is I want to do", "All right, bottom, left", "Do you have definitely", "So when the mindset off chaining [INAUDIBLE] out of the same index HTML elements", "Views", "Yeah", "So I'm going to createRoutes, wee, clearRoutes equals static", "Let's do this, status is generally work", "So that's why I was building the nicest", "I'll tell you what we want to come into the panels", "On all of ES2015 updates on the path name", "Because it's an iterate what they see", "I'm going to do", "We'll do that", "And hopefully, you're here in slash about view but we're going to be whichever view was the new view is that", "so that the event that isn't get called, all subscribing to do today", "And then we're just delete the JavaScript language", "Yeah, and we need to extends HTMLElement", "And we app where we actually uncanny valid concept for the out animation", "duration", "count in one tends HTML, I think, would then we've defer, why not", "Let's see what's good on here", "So if you say layout, for example", "Yes, so one of its scope", "What we want to do, I supposed to find out", "The defer mean to your Custom Elements JavaScript says we don't have", "We don't want to say this, so one that you click back to then dot the even though it", "So there a createdCallback, so we never being us", "That doesn't it", "Right", "All right", "That should", "Oh no, Array", "from", "Hm, it shouldNotMakeMoreOutPromises", "And then let's do that is purely for simplicity at this", "I don't takes too longer and I will say this", "routes", "because it matches the current ones will now needs to be run against that going to say const view back", "And then what the createRoute", "That's what I think", "So we have to transitions, particular if branch of this, you're giving us way too much better", "So since the layout, OK", "I think we'll create objects anymore", "You let us know what I'm going to do is I'm going to do is let's just find out", "createdCallbacks", "So if view", "I could do if we don't want to make a nav", "So I'm going to do that", "Super", "route", "So for this, right now, all the like shouldNotMakeMoreOutPromise", "resolve", "Same for the power of Promise, right", "Because why not", "Let's give it or not", "The defer also means that the state by selecting the view", "No", "Interesting", "So the brand", "new thing", "So let's see, so we do that", "All being well, we end with an actually hoping I will be remove this", "Are you this", "So we want to do that, actually just kind of amazing", "You know", "Yeah", "", "which is the current view was the new one that's a layout", "I don't you ask the question ties in it is when it's like a progressive to deal with, with contain strict", "now here", "And I'm going to us", "So onChanged", "Yeah", "Because of the this", "is", "the", "active", "view", "And we are building the routes equals this", "But when the view first time we create that isn't it", "Right", "Yeah, that is amazing", "And I think, a more bugs", "Yeah, I want it to updating to do that I have new view, and some Promise, we can actually can do here", "This is Paul", "Hi", "This time I write bugs, don't like this is actual lifecycle called ES6", "ES2016 was doing that's why I wanted to say", "currentView will be fast because", "You know what, in the back to the current view", "And then we'll say return", "One of the panels", "OK", "Come of that stuff out", "Should that the evaluation from 100", "no, should add that kind of got allowing that back out, right", "newView, newView, what we're kind of got these views that you, very wrong", "But if you about using there", "Because the nav has disappear ago, it was the keyword for all the regular expression and execution of a router", "Now you know, over that, in there", "Let's do that there we already got ourselves some of the way to go", "And it matches the new one for that", "Yeah", "And certain time gaps, think it's an animating to put a route for some reason", "view", "Figure out things simplicity at this point", "So what we're being a little bit of a pickle over right now we've deep", "linked that could want it to be that", "So let's just feels very interactions back in so this", "newView", "Yeah", "And apparent, what we'd want each one of all the debugger standard one", "So this way, it should add the visible", "And we're pretty raw, there will be find out notionally, the code, it's fine, it's fail", "So the question", "Yeah, so we could see now them to makes Jav" ] ================================================ FILE: packages/react-examples/src/pages/infinity/index.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import InfinityScroll from '@better-scroll/infinity' import message from './data/message.json' import './index.styl' BScroll.use(InfinityScroll) const NUM_AVATARS = 4 const NUM_IMAGES = 77 const INIT_TIME = new Date().getTime() function getItem(id) { function pickRandom(a) { return a[Math.floor(Math.random() * a.length)] } return new Promise((resolve) => { const item = { id: id, avatar: Math.floor(Math.random() * NUM_AVATARS), self: Math.random() < 0.1, image: Math.random() < 1.0 / 20 ? Math.floor(Math.random() * NUM_IMAGES) : '', time: new Date( Math.floor(INIT_TIME + id * 20 * 1000 + Math.random() * 20 * 1000) ), message: pickRandom(message), } if (item.image === '') { resolve(item) } else { let image = new Image() image.src = require(`./image/image${item.image}.jpg`) image.addEventListener('load', function () { item.image = image resolve(item) }) image.addEventListener('error', function () { item.image = '' resolve(item) }) } }) } let nextItem = 0 let pageNum = 0 const InfinityPage = () => { const messageRef = useRef(null) const tombstoneRef = useRef(null) const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { infinity: { render: (item, div) => { div = div || messageRef.current.cloneNode(true) div.dataset.id = item.id div.querySelector( '.infinity-avatar' ).src = require(`./image/avatar${item.avatar}.jpg`) div.querySelector('.infinity-bubble p').textContent = item.id + ' ' + item.message div.querySelector( '.infinity-bubble .infinity-posted-date' ).textContent = item.time.toString() let img = div.querySelector('.infinity-bubble img') if (item.image !== '') { img.style.display = '' img.src = item.image.src img.width = item.image.width img.height = item.image.height } else { img.src = '' img.style.display = 'none' } if (item.self) { div.classList.add('infinity-from-me') } else { div.classList.remove('infinity-from-me') } return div }, createTombstone: () => tombstoneRef.current.cloneNode(true), fetch: (count) => { count = Math.max(30, count) return new Promise((resolve, reject) => { // Assume 50 ms per item. setTimeout(() => { if (++pageNum > 20) { resolve(false) } else { console.log('pageNum', pageNum) let items = [] for (let i = 0; i < Math.abs(count); i++) { items[i] = getItem(nextItem++) } resolve(Promise.all(items)) } }, 500) }) }, }, })) BS.on('scroll', () => { console.log('is scrolling') }) BS.on('scrollEnd', () => { console.log('scrollEnd') }) } }, []) return (
    • ) } export default InfinityPage ================================================ FILE: packages/react-examples/src/pages/infinity/index.styl ================================================ .infinity height: 100% .template display: none .infinity-timeline position: relative height: 100% padding: 0 10px border: 1px solid #ccc overflow: hidden will-change: transform background-color: #efeff5 .infinity-timeline > ul position: relative -webkit-backface-visibility: hidden -webkit-transform-style: flat .infinity-item display: flex left: 0 padding: 10px 0 width: 100% contain: layout will-change: transform list-style: none .infinity-avatar border-radius: 500px margin-left: 20px margin-right: 6px min-width: 48px .infinity-item p margin: 0 word-wrap: break-word font-size: 13px .infinity-item.tombstone p width: 100% height: 0.5em background-color: #ccc margin: 0.5em 0 .infinity-bubble img max-width: 100% height: auto .infinity-bubble padding: 7px 10px color: #333 background: #fff /*box-shadow: 0 3px 2px rgba(0, 0, 0, 0.1)*/ position: relative max-width: 420px min-width: 80px margin: 0 5px .infinity-bubble::before content: '' border-style: solid border-width: 0 10px 10px 0 border-color: transparent #fff transparent transparent position: absolute top: 0 left: -10px .infinity-meta font-size: 0.8rem color: #999 margin-top: 3px .infinity-from-me justify-content: flex-end .infinity-from-me .infinity-avatar order: 1 margin-left: 6px margin-right: 20px .infinity-from-me .infinity-bubble background: #F9D7FF .infinity-from-me .infinity-bubble::before left: 100% border-width: 10px 10px 0 0 /*border-color: #F9D7FF transparent transparent transparent*/ .infinity-state display: none .infinity-invisible display: none ================================================ FILE: packages/react-examples/src/pages/mouse-wheel/components/horizontal-scroll.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import MouseWheel from '@better-scroll/mouse-wheel' BScroll.use(MouseWheel) const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const data = createArray(100) const HorizontalScroll = () => { const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { scrollRef.current = new BScroll(wrapperRef.current, { scrollX: true, scrollY: false, mouseWheel: true, }) }, []) return (
      {data.map((item, index) => (
      {item}
      ))}
      ) } export default HorizontalScroll ================================================ FILE: packages/react-examples/src/pages/mouse-wheel/components/horizontal-slide.js ================================================ import React, { useState, useRef, useEffect } from 'react' import classNames from 'classnames' import BScroll from '@better-scroll/core' import Slide from '@better-scroll/slide' import MouseWheel from '@better-scroll/mouse-wheel' BScroll.use(Slide) BScroll.use(MouseWheel) const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const nums = createArray(4) const HorizontalSlide = () => { const [currentPageIndex, setCurrentPageIndex] = useState(0) const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { scrollX: true, scrollY: false, slide: { loop: true, threshold: 100, }, useTransition: false, momentum: false, bounce: false, stopPropagation: true, mouseWheel: { speed: 2, invert: false, easeTime: 300, }, })) BS.on('scrollEnd', () => { setCurrentPageIndex(BS.getCurrentPage().pageY) }) } return () => { scrollRef.current?.destroy() } }, []) return (
      {nums.map((num) => (
      page {num}
      ))}
      {nums.map((num) => ( ))}
      ) } export default HorizontalSlide ================================================ FILE: packages/react-examples/src/pages/mouse-wheel/components/picker.js ================================================ import React, { useState, useRef, useEffect } from 'react' import classNames from 'classnames' import { CSSTransition } from 'react-transition-group' import BScroll from '@better-scroll/core' import Wheel from '@better-scroll/wheel' import MouseWheel from '@better-scroll/mouse-wheel' BScroll.use(Wheel) BScroll.use(MouseWheel) const DATA = [ { text: 'Venomancer', value: 1, disabled: 'wheel-disabled-item', }, { text: 'Nerubian Weaver', value: 2, }, { text: 'Spectre', value: 3, }, { text: 'Juggernaut', value: 4, }, { text: 'Karl', value: 5, }, { text: 'Zeus', value: 6, }, { text: 'Witch Doctor', value: 7, }, { text: 'Lich', value: 8, }, { text: 'Oracle', value: 9, }, { text: 'Earthshaker', value: 10, }, ] const stopPropagation = (e) => { e.stopPropagation() } const preventDefault = (e) => { e.preventDefault() } const OneColumn = () => { const [visible, setVisible] = useState(false) const [selectedIndex, setSelectedIndex] = useState(2) const [selectedText, setSelectedText] = useState('open') const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (visible) { if (!scrollRef.current) { const wrapper = wrapperRef.current.children[0] const BS = (scrollRef.current = new BScroll(wrapper, { wheel: { wheelWrapperClass: 'wheel-scroll', wheelItemClass: 'wheel-item', wheelDisabledItemClass: 'wheel-disabled-item', selectedIndex, }, useTransition: false, probeType: 3, })) BS.on('scrollEnd', () => { console.log('BS.getSelectedIndex()', BS.getSelectedIndex()) }) } else { scrollRef.current.refresh() } } }, [visible, selectedIndex]) const handleShow = () => { if (visible) { return } setVisible(true) } const handleHide = () => { setVisible(false) } const handleConfirm = () => { scrollRef.current.stop() handleHide() const currentSelectedIndex = scrollRef.current.getSelectedIndex() setSelectedIndex(currentSelectedIndex) setSelectedText(`${DATA[currentSelectedIndex].text}`) } const handleCancel = () => { scrollRef.current.restorePosition() handleHide() } return (
      {selectedText}
      { node.style.display = 'block' }} onExited={(node) => { node.style.display = '' }} >
      Cancel Confirm

      Title

        {DATA.map((item, index) => (
      • {item.text}
      • ))}
      ) } export default OneColumn ================================================ FILE: packages/react-examples/src/pages/mouse-wheel/components/pulldown.js ================================================ import React, { useState, useRef, useEffect, useCallback } from 'react' import BScroll from '@better-scroll/core' import PullDown from '@better-scroll/pull-down' import MouseWheel from '@better-scroll/mouse-wheel' BScroll.use(PullDown) BScroll.use(MouseWheel) const useStableCallback = (callback) => { const callbackRef = useRef(callback) useEffect(() => { callbackRef.current = callback }) return useCallback((...args) => callbackRef.current(...args), []) } const ajaxGet = (/* url */) => { return new Promise((resolve) => { setTimeout(() => { const dataList = generateData() resolve(dataList) }, 1000) }) } const TIME_BOUNCE = 800 let STEP = 0 function generateData() { const BASE = 30 const begin = BASE * STEP const end = BASE * (STEP + 1) let ret = [] for (let i = end; i > begin; i--) { ret.push(i) } return ret } const Pulldown = () => { const [beforePullDown, setBeforePullDown] = useState(true) const [isPullingDown, setIsPullingDown] = useState(false) const [data, setData] = useState(generateData()) const wrapperRef = useRef(null) const scrollRef = useRef(null) const requestData = async () => { try { const newData = await ajaxGet(/* url */) setData((prev) => newData.concat(prev)) } catch (err) { // handle err console.log(err) } } const finishPullDown = () => { scrollRef.current.finishPullDown() setTimeout(() => { setBeforePullDown(true) scrollRef.current.refresh() }, TIME_BOUNCE + 100) } const pullingDownHandler = useStableCallback(async () => { setBeforePullDown(false) setIsPullingDown(true) STEP += 1 await requestData() setIsPullingDown(false) finishPullDown() }) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { scrollY: true, bounceTime: TIME_BOUNCE, pullDownRefresh: { threshold: 70, stop: 56, }, mouseWheel: true, })) BS.on('pullingDown', pullingDownHandler) BS.on('scroll', (pos) => { console.log(pos.y) }) BS.on('scrollEnd', () => { console.log('scrollEnd') }) } }, [pullingDownHandler]) return (
      Pull Down and refresh
      Loading...
      Refresh success
        {data.map((item, index) => (
      • {`I am item ${item} `}
      • ))}
      ) } export default Pulldown ================================================ FILE: packages/react-examples/src/pages/mouse-wheel/components/pullup.js ================================================ import React, { useState, useRef, useEffect, useCallback } from 'react' import BScroll from '@better-scroll/core' import Pullup from '@better-scroll/pull-up' import MouseWheel from '@better-scroll/mouse-wheel' BScroll.use(Pullup) BScroll.use(MouseWheel) const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const useStableCallback = (callback) => { const callbackRef = useRef(callback) useEffect(() => { callbackRef.current = callback }) return useCallback((...args) => callbackRef.current(...args), []) } const ajaxGet = (/* url */) => { return new Promise((resolve) => { setTimeout(() => { resolve(20) }, 1000) }) } const PullupPage = () => { const [isPullUpLoad, setIsPullUpLoad] = useState(false) const [num, setNum] = useState(30) const wrapperRef = useRef(null) const scrollRef = useRef(null) const requestData = async () => { try { const newData = await ajaxGet(/* url */) setNum((n) => n + newData) } catch (err) { // handle err console.log(err) } } const pullingUpHandler = useStableCallback(async () => { setIsPullUpLoad(true) await requestData() scrollRef.current.finishPullUp() scrollRef.current.refresh() setIsPullUpLoad(false) }) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { probeType: 3, pullUpLoad: true, mouseWheel: true, })) BS.on('pullingUp', pullingUpHandler) } }, [pullingUpHandler]) return (
        {createArray(num).map((item, index) => (
      • {item % 5 === 0 ? 'scroll up 👆🏻' : `I am item ${item} `}
      • ))}
      {isPullUpLoad ? (
      Loading...
      ) : (
      Pull up and load more
      )}
      ) } export default PullupPage ================================================ FILE: packages/react-examples/src/pages/mouse-wheel/components/vertical-scroll.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import MouseWheel from '@better-scroll/mouse-wheel' BScroll.use(MouseWheel) const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const data = createArray(100) const VerticalScroll = () => { const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { scrollRef.current = new BScroll(wrapperRef.current, { mouseWheel: true, }) }, []) return (
      {data.map((item, index) => (
      {item}
      ))}
      ) } export default VerticalScroll ================================================ FILE: packages/react-examples/src/pages/mouse-wheel/components/vertical-slide.js ================================================ import React, { useState, useRef, useEffect } from 'react' import classNames from 'classnames' import BScroll from '@better-scroll/core' import Slide from '@better-scroll/slide' import MouseWheel from '@better-scroll/mouse-wheel' BScroll.use(Slide) BScroll.use(MouseWheel) const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const nums = createArray(4) const VerticalSlide = () => { const [currentPageIndex, setCurrentPageIndex] = useState(0) const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { scrollX: false, scrollY: true, slide: { loop: true, threshold: 100, }, mouseWheel: true, momentum: false, bounce: false, stopPropagation: true, })) BS.on('scrollEnd', () => { setCurrentPageIndex(BS.getCurrentPage().pageY) }) } return () => { scrollRef.current?.destroy() } }, []) return (
      {nums.map((num) => (
      page {num}
      ))}
      {nums.map((num) => ( ))}
      ) } export default VerticalSlide ================================================ FILE: packages/react-examples/src/pages/mouse-wheel/index.js ================================================ import React from 'react' import './index.styl' const examples = [ { path: '/mouse-wheel/vertical-scroll', name: 'vertical scroll', }, { path: '/mouse-wheel/horizontal-scroll', name: 'horizontal scroll', }, { path: '/mouse-wheel/vertical-slide', name: 'vertical slide', }, { path: '/mouse-wheel/horizontal-slide', name: 'horizontal slide', }, { path: '/mouse-wheel/pullup', name: 'pull up load', }, { path: '/mouse-wheel/pulldown', name: 'pull down refresh', }, { path: '/mouse-wheel/picker', name: 'picker', }, ] const MouseWheel = (props) => { const goPage = (path) => { props.history.push(path) } return (
        {examples.map((item) => (
      • goPage(item.path)} key={item.path} > {item.name}
      • ))}
      {props.children}
      ) } export default MouseWheel ================================================ FILE: packages/react-examples/src/pages/mouse-wheel/index.styl ================================================ .mouse-wheel-vertical-scroll .mouse-wheel-wrapper height 400px overflow hidden .mouse-wheel-item height 50px line-height 50px font-size 20px font-weight bold text-align center &:nth-child(2n) background-color #C3D899 &:nth-child(2n+1) background-color #F2D4A7 .mouse-wheel-horizontal-scroll .mouse-wheel-wrapper width 90% margin 80px auto white-space nowrap border 3px solid #42b983 border-radius 5px overflow hidden .mouse-wheel-content display inline-block .mouse-wheel-item height 50px line-height 50px font-size 24px display inline-block text-align center padding 0 20px &:nth-child(2n) background-color #C3D899 &:nth-child(2n+1) background-color #F2D4A7 .mouse-wheel-slide-vertical height 100% &.view padding 0 height 100% .slide-container position relative height 100% font-size 0 .slide-wrapper height 100% overflow hidden .slide-page display inline-block width 100% line-height 200px text-align center font-size 26px transform translate3d(0,0,0) backface-visibility hidden &.page1 background-color #D6EADF &.page2 background-color #DDA789 &.page3 background-color #C3D899 &.page4 background-color #F2D4A7 .dots-wrapper position absolute right 4px top 50% transform translateY(-50%) .dot display block margin 4px 0 width 8px height 8px border-radius 50% background #eee &.active height 20px border-radius 5px .mouse-wheel-horizontal-slide .slide-container position relative .slide-wrapper min-height 1px overflow hidden .slide-content height 200px white-space nowrap font-size 0 .slide-page display inline-block height 200px width 100% line-height 200px text-align center font-size 26px &.page1 background-color #95B8D1 &.page2 background-color #DDA789 &.page3 background-color #C3D899 &.page4 background-color #F2D4A7 .dots-wrapper position absolute bottom 4px left 50% transform translateX(-50%) .dot display inline-block margin 0 4px width 8px height 8px border-radius 50% background #eee &.active width 20px border-radius 5px .mouse-wheel-pullup height: 100% .pullup-wrapper height: 100% padding: 0 10px border: 1px solid #ccc overflow: hidden .pullup-list padding: 0 .pullup-list-item padding: 10px 0 list-style: none border-bottom: 1px solid #ccc .pullup-tips padding: 20px text-align: center color: #999 .mouse-wheel-pulldown height: 100% .pulldown-wrapper position: relative height: 100% padding: 0 10px border: 1px solid #ccc overflow: hidden .pulldown-list padding: 0 .pulldown-list-item padding: 10px 0 list-style: none border-bottom: 1px solid #ccc .pulldown-tips position: absolute width: 100% padding: 20px box-sizing: border-box transform: translateY(-100%) translateZ(0) text-align: center color: #999 /* reset */ ul list-style none padding 0 .border-bottom-1px, .border-top-1px position: relative &:before, &:after content: "" display: block position: absolute transform-origin: 0 0 .border-bottom-1px &:after border-bottom: 1px solid #ebebeb left: 0 bottom: 0 width: 100% transform-origin: 0 bottom .border-top-1px &:before border-top: 1px solid #ebebeb left: 0 top: 0 width: 100% transform-origin: 0 top @media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) .border-top-1px &:before width: 200% transform: scale(.5) translateZ(0) .border-bottom-1px &:after width: 200% transform: scale(.5) translateZ(0) @media (-webkit-min-device-pixel-ratio: 3), (min-device-pixel-ratio: 3) .border-top-1px &:before width: 300% transform: scale(.333) translateZ(0) .border-bottom-1px &:after width: 300% transform: scale(.333) translateZ(0) .container-btn margin: 2rem background-color white padding: 0.8rem border: 1px solid rgba(0, 0, 0, .1) box-shadow: 0 1px 2px 0 rgba(0,0,0,0.1) text-align: center .picker display: none position: fixed left: 0 top: 0 z-index: 100 width: 100% height: 100% overflow: hidden text-align: center font-size: 14px background-color: rgba(37, 38, 45, .4) &.picker-fade-enter opacity: 0 &.picker-fade-enter-active opacity: 1 transition: all .3s ease-in-out &.picker-fade-exit-active opacity: 0; transition: all .3s ease-in-out .picker-panel position: absolute z-index: 600 bottom: 0 width: 100% height: 273px background: white &.picker-move-enter transform: translate3d(0, 273px, 0) &.picker-move-enter-active transform: translate3d(0, 0, 0) transition: all .3s ease-in-out &.picker-move-exit-active transform: translate3d(0, 273px, 0) transition: all .3s ease-in-out .picker-choose position: relative height: 60px color: #999 .picker-title margin: 0 line-height: 60px font-weight: normal text-align: center font-size: 18px color: #333 .confirm, .cancel position: absolute top: 6px padding: 16px font-size: 14px .confirm right: 0 color: #007bff &:active color: #5aaaff .cancel left: 0 &:active color: #c2c2c2 .picker-content position: relative top: 20px .mask-top, .mask-bottom z-index: 10 width: 100% height: 68px pointer-events: none transform: translateZ(0) .mask-top position: absolute top: 0 background: linear-gradient(to top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.8)) .mask-bottom position: absolute bottom: 1px background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.8)) .wheel-wrapper display: flex padding: 0 16px .wheel flex: 1 width: 1% height: 173px overflow: hidden font-size: 18px .wheel-scroll padding: 0 margin-top: 68px line-height: 36px list-style: none .wheel-item list-style: none height: 36px overflow: hidden white-space: nowrap color: #333 &.wheel-disabled-item opacity: .2; .picker-footer height: 20px ================================================ FILE: packages/react-examples/src/pages/movable/components/default.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import Movable from '@better-scroll/movable' BScroll.use(Movable) const emojis = [ '😀 😁 😂 🤣 😃', '😄 😅 😆 😉 😊', '😫 😴 😌 😛 😜', '👆🏻 😒 😓 😔 👇🏻', ] const Default = () => { const wrapperRef = useRef(null) useEffect(() => { const BS = new BScroll(wrapperRef.current, { bindToTarget: true, scrollX: true, scrollY: true, freeScroll: true, movable: true, startX: 20, startY: 20, }) return () => { BS.destroy() } }, []) return (
      {emojis.map((item, index) => (
      {item}
      ))}
      ) } export default Default ================================================ FILE: packages/react-examples/src/pages/movable/components/multi-content-scale.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import Movable from '@better-scroll/movable' import Zoom from '@better-scroll/zoom' import SwordsmanLink from './ftstr.png' import WitchLink from './qos_crop.png' BScroll.use(Movable) BScroll.use(Zoom) const MultiContentScale = () => { const wrapperRef = useRef(null) const scrollRef2 = useRef(null) useEffect(() => { const BS1 = new BScroll(wrapperRef.current, { bindToTarget: true, scrollX: true, scrollY: true, freeScroll: true, movable: true, zoom: { start: 1.2, min: 0.5, max: 3, }, }) BS1.putAt('center', 'center', 0) const BS2 = (scrollRef2.current = new BScroll(wrapperRef.current, { // use wrapper.children[1] as content specifiedIndexAsContent: 1, bindToTarget: true, scrollX: true, scrollY: true, freeScroll: true, movable: true, startY: 150, zoom: { start: 1, min: 0.5, max: 3, }, })) return () => { BS1.destroy() BS2.destroy() } }, []) const handleClick = () => { scrollRef2.current.putAt('right', 'bottom', 500) } return (
      Cold Oasis
      ftstr
      Warm Oasis
      qos_crop
      ) } export default MultiContentScale ================================================ FILE: packages/react-examples/src/pages/movable/components/multi-content.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import Movable from '@better-scroll/movable' import picture1 from './oasis_one.png' import picture2 from './oasis_two.png' BScroll.use(Movable) const MultiContent = () => { const wrapperRef = useRef(null) const scrollRef2 = useRef(null) useEffect(() => { const BS1 = new BScroll(wrapperRef.current, { bindToTarget: true, scrollX: true, scrollY: true, freeScroll: true, movable: true, startX: 10, startY: 10, }) const BS2 = (scrollRef2.current = new BScroll(wrapperRef.current, { // use wrapper.children[1] as content specifiedIndexAsContent: 1, bindToTarget: true, scrollX: true, scrollY: true, freeScroll: true, movable: true, startX: 0, startY: 170, })) return () => { BS1.destroy() BS2.destroy() } }, []) const handleClick = () => { scrollRef2.current.putAt('center', 'center') } return (
      Cold Oasis
      Cold Oasis
      Warm Oasis
      Warm Oasis
      ) } export default MultiContent ================================================ FILE: packages/react-examples/src/pages/movable/components/scale.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import Movable from '@better-scroll/movable' import Zoom from '@better-scroll/zoom' BScroll.use(Movable) BScroll.use(Zoom) const emojis = [ '😀 😁 😂 🤣 😃', '😄 😅 😆 😉 😊', '😫 😴 😌 😛 😜', '👆🏻 😒 😓 😔 👇🏻', ] const Scale = () => { const wrapperRef = useRef(null) useEffect(() => { const BS = new BScroll(wrapperRef.current, { bindToTarget: true, scrollX: true, scrollY: true, freeScroll: true, movable: true, zoom: { start: 1, min: 0.5, max: 3, }, }) return () => { BS.destroy() } }, []) return (
      {emojis.map((item, index) => (
      {item}
      ))}
      ) } export default Scale ================================================ FILE: packages/react-examples/src/pages/movable/index.js ================================================ import React from 'react' import './index.styl' const examples = [ { path: '/movable/default', name: 'default', }, { path: '/movable/scale', name: 'scale', }, { path: '/movable/multi-content', name: 'multi-content', }, { path: '/movable/multi-content-scale', name: 'multi-content-scale', }, ] const Movable = (props) => { const goPage = (path) => { props.history.push(path) } return (
        {examples.map((item) => (
      • goPage(item.path)} key={item.path} > {item.name}
      • ))}
      {props.children}
      ) } export default Movable ================================================ FILE: packages/react-examples/src/pages/movable/index.styl ================================================ .movable-container .scroll-wrapper height 400px overflow hidden box-shadow 0 0 3px rgba(0, 0, 0, .3) .scroll-content width 220px .scroll-item height 50px line-height 50px font-size 24px font-weight bold border-bottom 1px solid #eee text-align center &:nth-child(2n) background-color #f3f5f7 &:nth-child(2n+1) background-color #42b983 .movable-multi-content-container .scroll-wrapper height 400px overflow hidden position relative box-shadow 0 0 3px rgba(0, 0, 0, .3) .scroll-content position absolute top 0 left 0 width 200px figure margin 0 figcaption font-weight bold margin-bottom 5px text-align center color #ea4c89 .picture width 200px height 150px border-radius 10px .scroll-item height 50px line-height 50px font-size 24px font-weight bold border-bottom 1px solid #eee text-align center &:nth-child(2n) background-color #f3f5f7 &:nth-child(2n+1) background-color #42b983 .btn margin 40px auto padding 10px color #fff border-radius 4px font-size 20px background-color #666 ================================================ FILE: packages/react-examples/src/pages/nested-scroll/components/horizontal-in-vertical.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import NestedScroll from '@better-scroll/nested-scroll' import Slide from '@better-scroll/slide' BScroll.use(NestedScroll) BScroll.use(Slide) const outerOpenData = [ '😀 😁 😂 🤣 😃 🙃 ', '👆🏻 vertical scroll 👇🏻 ', '🙂 🤔 😄 🤨 😐 🙃 ', ] const outerCloseData = [ '😀 😁 😂 🤣 😃 🙃 ', '👆🏻 vertical scroll 👇🏻 ', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 vertical scroll 👇🏻 ', '😔 😕 🙃 🤣 😲 🙃 🤣', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 vertical scroll 👇🏻 ', '😔 😕 🙃 🤣 😲 🙃 🤣', '👆🏻 vertical scroll 👇🏻 ', '😔 😕 🙃 🤣 😲 🙃 🤣', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 vertical scroll 👇🏻 ', '😔 😕 🙃 🤣 😲 🙃 🤣', '👆🏻 vertical scroll 👇🏻 ', '😔 😕 🙃 🤣 😲 🙃 🤣', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 vertical scroll 👇🏻 ', '😔 😕 🙃 🤣 😲 🙃 🤣', ] const handleOuterClick = () => { alert('clicked outer item') } const handleInnerClick = () => { alert('clicked inner item') } const HorizontalInVertical = () => { const outerWrapperRef = useRef(null) const outerScrollRef = useRef(null) const innerWrapperRef = useRef(null) const innerScrollRef = useRef(null) useEffect(() => { if (!outerScrollRef.current) { outerScrollRef.current = new BScroll(outerWrapperRef.current, { nestedScroll: { groupId: 'mixed-nested-scroll', }, click: true, }) } if (!innerScrollRef.current) { innerScrollRef.current = new BScroll(innerWrapperRef.current, { nestedScroll: { groupId: 'mixed-nested-scroll', }, scrollX: true, scrollY: false, slide: { loop: false, autoplay: false, threshold: 100, }, momentum: false, bounce: false, click: true, }) } return () => { outerScrollRef.current?.destroy() innerScrollRef.current?.destroy() } }, []) return (
      {outerOpenData.map((item, index) => (
    • {item}
    • ))}
      horizontal scroll 1
      horizontal scroll 2
      horizontal scroll 3
      horizontal scroll 4
      {outerCloseData.map((item, index) => (
      {item}
      ))}
      ) } export default HorizontalInVertical ================================================ FILE: packages/react-examples/src/pages/nested-scroll/components/horizontal.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import NestedScroll from '@better-scroll/nested-scroll' BScroll.use(NestedScroll) const outerOpenData = ['👈🏻 outer 👉🏻 ', '🙂 🤔 😄 🤨 😐 🙃 '] const outerCloseData = ['😔 😕 🙃 🤑 😲 😲 ', '👈🏻 outer 👉🏻 '] const innerData = [ '👈🏻 inner 👉🏻 ', '🙂 🤔 😄 🤨 😐 🙃 ', '👈🏻 inner 👉🏻 ', '😔 😕 🙃 🤑 😲 ☹️ ', '👈🏻 inner 👉🏻 ', '🐣 🐣 🐣 🐣 🐣 🐣 ', '👈🏻 inner 👉🏻 ', '🐥 🐥 🐥 🐥 🐥 🐥 ', ] const handleOuterClick = () => { alert('clicked outer item') } const handleInnerClick = () => { alert('clicked inner item') } const Horizontal = () => { const outerWrapperRef = useRef(null) const outerScrollRef = useRef(null) const innerWrapperRef = useRef(null) const innerScrollRef = useRef(null) useEffect(() => { if (!outerScrollRef.current) { outerScrollRef.current = new BScroll(outerWrapperRef.current, { nestedScroll: { groupId: 'horizontal-nested-scroll', // groupId is a string or number }, scrollX: true, scrollY: false, click: true, }) } if (!innerScrollRef.current) { innerScrollRef.current = new BScroll(innerWrapperRef.current, { // please keep the same groupId as above // outerScroll and innerScroll will be controlled by the same nestedScroll instance nestedScroll: { groupId: 'horizontal-nested-scroll', }, scrollX: true, scrollY: false, click: true, }) } return () => { outerScrollRef.current?.destroy() innerScrollRef.current?.destroy() } }, []) return (
        {outerOpenData.map((item, index) => (
      • {item}
      • ))}
        • {innerData.map((item, index) => (
        • {item}
        • ))}
      • {outerCloseData.map((item, index) => (
      • {item}
      • ))}
      ) } export default Horizontal ================================================ FILE: packages/react-examples/src/pages/nested-scroll/components/triple-vertical.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import NestedScroll from '@better-scroll/nested-scroll' BScroll.use(NestedScroll) const outerOpenData = [ '----Outer Start----', '👆🏻 outer scroll 👇🏻 ', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 outer scroll 👇🏻 ', ] const outerCloseData = [ '👆🏻 outer scroll 👇🏻 ', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 outer scroll 👇🏻 ', '😔 😕 🙃 🤑 😲 😲 ', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 outer scroll 👇🏻 ', '----Outer End----', ] const middleOpenData = [ '----Middle Start----', '👆🏻 middle scroll 👇🏻 ', '🐣 🐣 🐣 🐣 🐣 🐣 ', ] const middleCloseData = [ '👆🏻 middle scroll 👇🏻 ', '🤓 🤓 🤓 🤓 🤓 🤓 ', '👆🏻 middle scroll 👇🏻 ', '🦔 🦔 🦔 🦔 🦔 🦔 ', '👆🏻 middle scroll 👇🏻 ', '🙈 🙈 🙈 🙈 🙈 🙈 ', '👆🏻 middle scroll 👇🏻 ', '🚖 🚖 🚖 🚖 🚖 🚖 ', '👆🏻 middle scroll 👇🏻 ', '✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ', '----Middle End----', ] const innerData = [ '------Inner Start-----', '😀 😁 😂 🤣 😃 🙃 ', '👆🏻 inner scroll 👇🏻 ', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 inner scroll 👇🏻 ', '😔 😕 🙃 🤑 😲 😐 🙃 ', '👆🏻 inner scroll 👇🏻 ', '🐣 🐣 🐣 🐣 🐣 🐣 ', '👆🏻 inner scroll 👇🏻 ', '🐥 🐥 🐥 🐥 🐥 🐥 ', '👆🏻 inner scroll 👇🏻 ', '🤓 🤓 🤓 🤓 🤓 🤓 ', '👆🏻 inner scroll 👇🏻 ', '🦔 🦔 🦔 🦔 🦔 🦔 ', '👆🏻 inner scroll 👇🏻 ', '🙈 🙈 🙈 🙈 🙈 🙈 ', '👆🏻 inner scroll 👇🏻 ', '🚖 🚖 🚖 🚖 🚖 🚖 ', '👆🏻 inner scroll 👇🏻 ', '✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ', '-----Inner End-----', ] const handleOuterClick = () => { alert('clicked outer item') } const handleMiddleClick = () => { alert('clicked middle item') } const handleInnerClick = () => { alert('clicked inner item') } const TripleVertical = () => { const outerWrapperRef = useRef(null) const middleWrapperRef = useRef(null) const innerWrapperRef = useRef(null) useEffect(() => { const outerScroll = new BScroll(outerWrapperRef.current, { nestedScroll: { groupId: 'triple-nested-scroll', // groupId is a string or number }, click: true, }) const middleScroll = new BScroll(middleWrapperRef.current, { nestedScroll: { groupId: 'triple-nested-scroll', // groupId is a string or number }, probeType: 2, click: true, }) middleScroll.on('scroll', () => { console.log('middleScroll scroll') }) const innerScroll = new BScroll(innerWrapperRef.current, { // please keep the same groupId as above // all scrolls will be controlled by the same nestedScroll instance nestedScroll: { groupId: 'triple-nested-scroll', }, probeType: 2, click: true, }) innerScroll.on('scroll', () => { console.log('innerScroll scroll') }) innerScroll.on('scrollEnd', () => { console.log('innerScroll scrollEnd') }) return () => { outerScroll.destroy() middleScroll.destroy() innerScroll.destroy() } }, []) return (
        {outerOpenData.map((item, index) => (
      • {item}
      • ))}
        {middleOpenData.map((item, index) => (
      • {item}
      • ))}
        {innerData.map((item, index) => (
      • {item}
      • ))}
        {middleCloseData.map((item, index) => (
      • {item}
      • ))}
        {outerCloseData.map((item, index) => (
      • {item}
      • ))}
      ) } export default TripleVertical ================================================ FILE: packages/react-examples/src/pages/nested-scroll/components/vertical.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import NestedScroll from '@better-scroll/nested-scroll' BScroll.use(NestedScroll) const outerOpenData = [ '----Outer Start----', '👆🏻 outer scroll 👇🏻 ', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 outer scroll 👇🏻 ', ] const outerCloseData = [ '👆🏻 outer scroll 👇🏻 ', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 outer scroll 👇🏻 ', '😔 😕 🙃 🤑 😲 😲 ', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 outer scroll 👇🏻 ', '👆🏻 outer scroll 👇🏻 ', '😔 😕 🙃 🤑 😲 😲 ', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 outer scroll 👇🏻 ', '👆🏻 outer scroll 👇🏻 ', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 outer scroll 👇🏻 ', '----Outer End----', ] const innerData = [ '------Inner Start-----', '😀 😁 😂 🤣 😃 🙃 ', '👆🏻 inner scroll 👇🏻 ', '🙂 🤔 😄 🤨 😐 🙃 ', '👆🏻 inner scroll 👇🏻 ', '😔 😕 🙃 🤑 😲 😐 🙃 ', '👆🏻 inner scroll 👇🏻 ', '🐣 🐣 🐣 🐣 🐣 🐣 ', '👆🏻 inner scroll 👇🏻 ', '🐥 🐥 🐥 🐥 🐥 🐥 ', '👆🏻 inner scroll 👇🏻 ', '🤓 🤓 🤓 🤓 🤓 🤓 ', '👆🏻 inner scroll 👇🏻 ', '🦔 🦔 🦔 🦔 🦔 🦔 ', '👆🏻 inner scroll 👇🏻 ', '🙈 🙈 🙈 🙈 🙈 🙈 ', '👆🏻 inner scroll 👇🏻 ', '🚖 🚖 🚖 🚖 🚖 🚖 ', '👆🏻 inner scroll 👇🏻 ', '✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ✌🏻 ', '-----Inner End-----', ] const handleOuterClick = () => { alert('clicked outer item') } const handleInnerClick = () => { alert('clicked inner item') } const Vertical = () => { const outerWrapperRef = useRef(null) const outerScrollRef = useRef(null) const innerWrapperRef = useRef(null) const innerScrollRef = useRef(null) useEffect(() => { if (!outerScrollRef.current) { outerScrollRef.current = new BScroll(outerWrapperRef.current, { nestedScroll: { groupId: 'vertical-nested-scroll', // groupId is a string or number }, click: true, }) } if (!innerScrollRef.current) { innerScrollRef.current = new BScroll(innerWrapperRef.current, { // please keep the same groupId as above // outerScroll and innerScroll will be controlled by the same nestedScroll instance nestedScroll: { groupId: 'vertical-nested-scroll', }, click: true, }) } return () => { outerScrollRef.current?.destroy() innerScrollRef.current?.destroy() } }, []) return (
        {outerOpenData.map((item, index) => (
      • {item}
      • ))}
          {innerData.map((item, index) => (
        • {item}
        • ))}
        {outerCloseData.map((item, index) => (
      • {item}
      • ))}
      ) } export default Vertical ================================================ FILE: packages/react-examples/src/pages/nested-scroll/index.js ================================================ import React from 'react' import './index.styl' const examples = [ { path: '/nested-scroll/vertical', name: 'vertical', }, { path: '/nested-scroll/horizontal', name: 'horizontal', }, { path: '/nested-scroll/horizontal-in-vertical', name: 'horizontal-in-vertical', }, { path: '/nested-scroll/triple-vertical', name: 'triple-vertical', }, ] const NestedScroll = (props) => { const goPage = (path) => { props.history.push(path) } return (
        {examples.map((item) => (
      • goPage(item.path)} key={item.path} > {item.name}
      • ))}
      {props.children}
      ) } export default NestedScroll ================================================ FILE: packages/react-examples/src/pages/nested-scroll/index.styl ================================================ .vertical .outer-wrapper .inner-wrapper border: 2px solid #62B791 border-radius: 5px transform: rotate(0deg) position: relative overflow: hidden .outer-wrapper height: 100% border: 1px solid rgba(0, 0, 0, .1) .inner-wrapper height: 240px background-color rgba(98,183,145, 0.2) .inner-list-item height: 50px line-height: 50px text-align: center list-style: none .outer-list-item height: 40px line-height: 40px text-align: center list-style: none .horizontal .outer-wrapper border: 1px solid rgba(0, 0, 0, 0.1) border-radius: 5px transform: rotate(0deg) margin-top: 50px position: relative overflow: hidden .outer-content display: inline-block vertical-align: top white-space: nowrap .inner-wrapper border: 2px solid #62B791 border-radius: 5px transform: rotate(0deg) position: relative width: 200px overflow: hidden .inner-content display: inline-block vertical-align: top .list-item display: inline-block line-height: 60px .inner-list-item vertical-align: top // important background-color: rgba(98,183,145, 0.2) .horizontal-in-vertical-container height: 100% .vertical-wrapper height: 100% border: 1px solid rgba(0, 0, 0, .1) position: relative overflow: hidden .vertical-item line-height: 40px text-align: center .slide-banner-content height: 120px white-space: nowrap font-size: 0 .slide-item display: inline-block height: 120px width: 100% line-height: 120px text-align: center font-size: 26px &.page1 background-color: #95B8D1 &.page2 background-color: #DDA789 &.page3 background-color: #C3D899 &.page4 background-color: #F2D4A7 .triple-vertical .outer-wrapper .middle-wrapper .inner-wrapper border: 2px solid #62B791 border-radius: 5px transform: rotate(0deg) position: relative overflow: hidden .outer-wrapper height: 100% border: 1px solid rgba(0, 0, 0, .1) .middle-wrapper height: 480px background-color rgba(240,65,85, 0.2) border: 2px solid #f04155 .inner-wrapper height: 240px background-color rgb(98,183,145) .middle-list-item .inner-list-item height: 50px line-height: 50px text-align: center list-style: none color: white .middle-list-item color: #894e06 .outer-list-item height: 40px line-height: 40px text-align: center list-style: none ================================================ FILE: packages/react-examples/src/pages/observe-dom/components/default.js ================================================ import React, { useState, useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import ObserveDOM from '@better-scroll/observe-dom' BScroll.use(ObserveDOM) const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const Default = () => { const [num, setNum] = useState(10) const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { scrollRef.current = new BScroll(wrapperRef.current, { observeDOM: true, scrollX: true, scrollY: false, }) } }, []) const handleClick = () => { setNum((n) => n + 2) } return (
      {createArray(num).map((item, index) => (
      handleClick(item)} > {item}
      ))}
      ) } export default Default ================================================ FILE: packages/react-examples/src/pages/observe-dom/index.js ================================================ import React from 'react' import './index.styl' const ObserveDom = (props) => <>{props.children} export default ObserveDom ================================================ FILE: packages/react-examples/src/pages/observe-dom/index.styl ================================================ .observe-dom-container text-align center .scroll-wrapper width 90% margin 80px auto white-space nowrap border 3px solid #42b983 border-radius 5px overflow hidden .scroll-content display inline-block .scroll-item height 50px line-height 50px font-size 24px display inline-block text-align center padding 0 20px &:nth-child(2n) background-color #C3D899 &:nth-child(2n+1) background-color #F2D4A7 .btn margin 40px auto padding 10px color #fff!important border-radius 4px font-size 20px background-color #666!important ================================================ FILE: packages/react-examples/src/pages/observe-image/components/default.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import ObserveImage from '@better-scroll/observe-image' BScroll.use(ObserveImage) const images = [ 'https://dpubstatic.udache.com/static/dpubimg/dEswI1MVy6/zoo.png', 'https://dpubstatic.udache.com/static/dpubimg/BYb_wPak21/home.png', 'https://dpubstatic.udache.com/static/dpubimg/B6q1pWB0sB/cabin.png', 'https://dpubstatic.udache.com/static/dpubimg/76n1ilzf4R/stone.png', ] const Default = () => { const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { scrollRef.current = new BScroll(wrapperRef.current, { observeImage: true, }) } }, []) return (
      {images.map((item, index) => ( ))}
      ) } export default Default ================================================ FILE: packages/react-examples/src/pages/observe-image/index.js ================================================ import React from 'react' import './index.styl' const ObserveImage = (props) => <>{props.children} export default ObserveImage ================================================ FILE: packages/react-examples/src/pages/observe-image/index.styl ================================================ .observe-image-container text-align center .scroll-wrapper position relative width 300px height 300px margin 20px auto border 3px solid #42b983 border-radius 5px overflow hidden .scroll-content font-size 0 ================================================ FILE: packages/react-examples/src/pages/picker/components/double-column.js ================================================ import React, { useState, useRef, useEffect } from 'react' import { CSSTransition } from 'react-transition-group' import BScroll from '@better-scroll/core' import Wheel from '@better-scroll/wheel' BScroll.use(Wheel) const DATA1 = [ { text: 'Venomancer', value: 1, }, { text: 'Nerubian Weaver', value: 2, }, { text: 'Spectre', value: 3, }, { text: 'Juggernaut', value: 4, }, { text: 'Karl', value: 5, }, { text: 'Zeus', value: 6, }, { text: 'Witch Doctor', value: 7, }, { text: 'Lich', value: 8, }, { text: 'Oracle', value: 9, }, { text: 'Earthshaker', value: 10, }, ] const DATA2 = [ { text: 'Durable', value: 'a', }, { text: 'Pusher', value: 'b', }, { text: 'Carry', value: 'c', }, { text: 'Nuker', value: 'd', }, { text: 'Support', value: 'e', }, { text: 'Jungle', value: 'f', }, { text: 'Escape', value: 'g', }, { text: 'Initiator', value: 'h', }, ] const stopPropagation = (e) => { e.stopPropagation() } const preventDefault = (e) => { e.preventDefault() } const pickerData = [DATA1, DATA2] const DoubleColumn = () => { const [visible, setVisible] = useState(false) const [selectedIndexPair, setSelectedIndexPair] = useState([0, 0]) const [selectedText, setSelectedText] = useState('open') const wrapperRef = useRef(null) const scrollRef = useRef([]) useEffect(() => { if (visible) { const createWheel = (wheelWrapper, i) => { if (scrollRef.current[i]) { scrollRef.current[i].refresh() } else { const BS = (scrollRef.current[i] = new BScroll( wheelWrapper.children[i], { wheel: { wheelWrapperClass: 'wheel-scroll', wheelItemClass: 'wheel-item', selectedIndex: selectedIndexPair[i], }, useTransition: false, probeType: 3, } )) // < v2.1.0 BS.on('scrollEnd', () => { console.log('BS.getSelectedIndex()', BS.getSelectedIndex()) }) } } const wrapper = wrapperRef.current for (let i = 0; i < pickerData.length; i++) { createWheel(wrapper, i) } } }, [visible, selectedIndexPair]) const handleShow = () => { if (visible) { return } setVisible(true) } const handleHide = () => { setVisible(false) } const handleConfirm = () => { scrollRef.current.forEach((wheel) => { /* * if bs is scrolling, force it stop at the nearest wheel-item * or you can use 'restorePosition' method as the below */ // wheel.stop() /* * if bs is scrolling, restore it to the start position * it is same with iOS picker and web Select element implementation * supported at v2.1.0 */ wheel.restorePosition() }) handleHide() const currentSelectedIndexPair = scrollRef.current.map((wheel) => wheel.getSelectedIndex() ) setSelectedIndexPair(currentSelectedIndexPair) setSelectedText( pickerData .map((data, i) => { const index = currentSelectedIndexPair[i] return `${data[index].text}-${index}` }) .join('__') ) } const handleCancel = () => { /* * if bs is scrolling, restore it to the start position * it is same with iOS picker and web Select element implementation * supported at v2.1.0 */ scrollRef.current.forEach((wheel) => { wheel.restorePosition() }) handleHide() } return (
      {selectedText}
      { node.style.display = 'block' }} onExited={(node) => { node.style.display = '' }} >
      Cancel Confirm

      Title

      {pickerData.map((data, index) => (
        {data.map((item, index) => (
      • {item.text}
      • ))}
      ))}
      ) } export default DoubleColumn ================================================ FILE: packages/react-examples/src/pages/picker/components/linkage-column.js ================================================ import React, { useState, useRef, useEffect, useCallback } from 'react' import { CSSTransition } from 'react-transition-group' import BScroll from '@better-scroll/core' import Wheel from '@better-scroll/wheel' BScroll.use(Wheel) const DATA = [ { text: '北京市', value: '110000', children: [ { text: '北京市', value: '110100', }, ], }, { text: '天津市', value: '120000', children: [ { text: '天津市', value: '120000', }, ], }, { text: '河北省', value: '130000', children: [ { text: '石家庄市', value: '130100', }, { text: '唐山市', value: '130200', }, { text: '秦皇岛市', value: '130300', }, { text: '邯郸市', value: '130400', }, { text: '邢台市', value: '130500', }, { text: '保定市', value: '130600', }, { text: '张家口市', value: '130700', }, { text: '承德市', value: '130800', }, ], }, { text: '山西省', value: '140000', children: [ { text: '太原市', value: '140100', }, { text: '大同市', value: '140200', }, { text: '阳泉市', value: '140300', }, { text: '长治市', value: '140400', }, { text: '晋城市', value: '140500', }, { text: '朔州市', value: '140600', }, { text: '晋中市', value: '140700', }, ], }, ] const stopPropagation = (e) => { e.stopPropagation() } const preventDefault = (e) => { e.preventDefault() } const useStableCallback = (callback) => { const callbackRef = useRef(callback) useEffect(() => { callbackRef.current = callback }) return useCallback((...args) => callbackRef.current(...args), []) } const LinkageColumn = () => { const [visible, setVisible] = useState(false) const [pickerData, setPickerData] = useState(() => []) const [selectedIndexPair, setSelectedIndexPair] = useState([0, 0]) const [selectedText, setSelectedText] = useState('open') const wrapperRef = useRef(null) const scrollRef = useRef([]) const loadPickerData = useStableCallback((newIndexPair, oldIndexPair) => { let provinces let cities // first instantiated if (!oldIndexPair) { provinces = DATA.map(({ value, text }) => ({ value, text })) cities = DATA[newIndexPair[0]].children setPickerData([provinces, cities]) } else { // provinces'index changed, refresh cities data if (newIndexPair[0] !== oldIndexPair[0]) { cities = DATA[newIndexPair[0]].children.slice() setPickerData((prev) => { const next = [...prev] next.splice(1, 1, cities) return next }) // Since cities data changed // refresh better-scroll to recaculate scrollHeight scrollRef.current[1].refresh() } } }) const createWheels = useStableCallback(() => { const createWheel = (wheelWrapper, i) => { if (scrollRef.current[i]) { scrollRef.current[i].refresh() } else { const BS = (scrollRef.current[i] = new BScroll( wheelWrapper.children[i], { wheel: { wheelWrapperClass: 'wheel-scroll', wheelItemClass: 'wheel-item', selectedIndex: selectedIndexPair[i], }, useTransition: false, probeType: 3, } )) // when any of wheels'scrolling ended , refresh data let prevSelectedIndexPair = selectedIndexPair BS.on('scrollEnd', () => { const currentSelectedIndexPair = scrollRef.current.map((wheel) => wheel.getSelectedIndex() ) loadPickerData(currentSelectedIndexPair, prevSelectedIndexPair) prevSelectedIndexPair = currentSelectedIndexPair }) } } const wrapper = wrapperRef.current for (let i = 0; i < pickerData.length; i++) { createWheel(wrapper, i) } }) useEffect(() => { loadPickerData(selectedIndexPair) }, [loadPickerData, selectedIndexPair]) useEffect(() => { if (visible) { createWheels() } }, [visible, createWheels]) const handleShow = () => { if (visible) { return } setVisible(true) } const handleHide = () => { setVisible(false) } const handleConfirm = () => { scrollRef.current.forEach((wheel) => { /* * if bs is scrolling, force it stop at the nearest wheel-item * or you can use 'restorePosition' method as the below */ // wheel.stop() /* * if bs is scrolling, restore it to the start position * it is same with iOS picker and web Select element implementation * supported at v2.1.0 */ wheel.restorePosition() }) handleHide() const currentSelectedIndexPair = scrollRef.current.map((wheel) => wheel.getSelectedIndex() ) setSelectedIndexPair(currentSelectedIndexPair) setSelectedText( pickerData .map((data, i) => { const index = currentSelectedIndexPair[i] return `${data[index].text}-${index}` }) .join('__') ) } const handleCancel = () => { /* * if bs is scrolling, restore it to the start position * it is same with iOS picker and web Select element implementation * supported at v2.1.0 */ scrollRef.current.forEach((wheel) => { wheel.restorePosition() }) handleHide() } return (
      {selectedText}
      { node.style.display = 'block' }} onExited={(node) => { node.style.display = '' }} >
      Cancel Confirm

      Title

      {pickerData.map((data, index) => (
        {data.map((item) => (
      • {item.text}
      • ))}
      ))}
      ) } export default LinkageColumn ================================================ FILE: packages/react-examples/src/pages/picker/components/one-column.js ================================================ import React, { useState, useRef, useEffect } from 'react' import classNames from 'classnames' import { CSSTransition } from 'react-transition-group' import BScroll from '@better-scroll/core' import Wheel from '@better-scroll/wheel' BScroll.use(Wheel) const DATA = [ { text: 'Venomancer', value: 1, disabled: 'wheel-disabled-item', }, { text: 'Nerubian Weaver', value: 2, }, { text: 'Spectre', value: 3, }, { text: 'Juggernaut', value: 4, }, { text: 'Karl', value: 5, }, { text: 'Zeus', value: 6, }, { text: 'Witch Doctor', value: 7, }, { text: 'Lich', value: 8, }, { text: 'Oracle', value: 9, }, { text: 'Earthshaker', value: 10, }, ] const stopPropagation = (e) => { e.stopPropagation() } const preventDefault = (e) => { e.preventDefault() } const OneColumn = () => { const [visible, setVisible] = useState(false) const [selectedIndex, setSelectedIndex] = useState(2) const [selectedText, setSelectedText] = useState('open') const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (visible) { if (!scrollRef.current) { const wrapper = wrapperRef.current.children[0] const BS = (scrollRef.current = new BScroll(wrapper, { wheel: { wheelWrapperClass: 'wheel-scroll', wheelItemClass: 'wheel-item', wheelDisabledItemClass: 'wheel-disabled-item', selectedIndex, }, useTransition: false, probeType: 3, })) // < v2.1.0 BS.on('scrollEnd', () => { console.log('BS.getSelectedIndex()', BS.getSelectedIndex()) }) // v2.1.0, only when selectedIndex changed BS.on('wheelIndexChanged', (index) => { console.log(index) }) } else { scrollRef.current.refresh() } } }, [visible, selectedIndex]) const handleShow = () => { if (visible) { return } setVisible(true) } const handleHide = () => { setVisible(false) } const handleConfirm = () => { /* * if bs is scrolling, force it stop at the nearest wheel-item * or you can use 'restorePosition' method as the below */ scrollRef.current.stop() handleHide() const currentSelectedIndex = scrollRef.current.getSelectedIndex() setSelectedIndex(currentSelectedIndex) setSelectedText( `${DATA[currentSelectedIndex].text}-${currentSelectedIndex}` ) } const handleCancel = () => { /* * if bs is scrolling, restore it to the start position * it is same with iOS picker and web Select element implementation * supported at v2.1.0 */ scrollRef.current.restorePosition() handleHide() } return (
      {selectedText}
      { node.style.display = 'block' }} onExited={(node) => { node.style.display = '' }} >
      Cancel Confirm

      Title

        {DATA.map((item, index) => (
      • {item.text}
      • ))}
      ) } export default OneColumn ================================================ FILE: packages/react-examples/src/pages/picker/index.js ================================================ import React from 'react' import './index.styl' const examples = [ { path: '/picker/one-column', name: 'One Column Picker', }, { path: '/picker/double-column', name: 'Double Column Picker', }, { path: '/picker/linkage-column', name: 'Linkage Column Picker', }, ] const Picker = (props) => { const goPage = (path) => { props.history.push(path) } return (
        {examples.map((item) => (
      • goPage(item.path)} key={item.path} > {item.name}
      • ))}
      {props.children}
      ) } export default Picker ================================================ FILE: packages/react-examples/src/pages/picker/index.styl ================================================ /* reset */ ul list-style none padding 0 .border-bottom-1px, .border-top-1px position: relative &:before, &:after content: "" display: block position: absolute transform-origin: 0 0 .border-bottom-1px &:after border-bottom: 1px solid #ebebeb left: 0 bottom: 0 width: 100% transform-origin: 0 bottom .border-top-1px &:before border-top: 1px solid #ebebeb left: 0 top: 0 width: 100% transform-origin: 0 top @media (-webkit-min-device-pixel-ratio: 2), (min-device-pixel-ratio: 2) .border-top-1px &:before width: 200% transform: scale(.5) translateZ(0) .border-bottom-1px &:after width: 200% transform: scale(.5) translateZ(0) @media (-webkit-min-device-pixel-ratio: 3), (min-device-pixel-ratio: 3) .border-top-1px &:before width: 300% transform: scale(.333) translateZ(0) .border-bottom-1px &:after width: 300% transform: scale(.333) translateZ(0) .container-btn margin: 2rem background-color white padding: 0.8rem border: 1px solid rgba(0, 0, 0, .1) box-shadow: 0 1px 2px 0 rgba(0,0,0,0.1) text-align: center .picker display: none position: fixed left: 0 top: 0 z-index: 100 width: 100% height: 100% overflow: hidden text-align: center font-size: 14px background-color: rgba(37, 38, 45, .4) &.picker-fade-enter opacity: 0 &.picker-fade-enter-active opacity: 1 transition: all .3s ease-in-out &.picker-fade-exit-active opacity: 0; transition: all .3s ease-in-out .picker-panel position: absolute z-index: 600 bottom: 0 width: 100% height: 273px background: white &.picker-move-enter transform: translate3d(0, 273px, 0) &.picker-move-enter-active transform: translate3d(0, 0, 0) transition: all .3s ease-in-out &.picker-move-exit-active transform: translate3d(0, 273px, 0) transition: all .3s ease-in-out .picker-choose position: relative height: 60px color: #999 .picker-title margin: 0 line-height: 60px font-weight: normal text-align: center font-size: 18px color: #333 .confirm, .cancel position: absolute top: 6px padding: 16px font-size: 14px .confirm right: 0 color: #007bff &:active color: #5aaaff .cancel left: 0 &:active color: #c2c2c2 .picker-content position: relative top: 20px .mask-top, .mask-bottom z-index: 10 width: 100% height: 68px pointer-events: none transform: translateZ(0) .mask-top position: absolute top: 0 background: linear-gradient(to top, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.8)) .mask-bottom position: absolute bottom: 1px background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.8)) .wheel-wrapper display: flex padding: 0 16px .wheel flex: 1 width: 1% height: 173px overflow: hidden font-size: 18px .wheel-scroll padding: 0 margin-top: 68px line-height: 36px list-style: none .wheel-item list-style: none height: 36px overflow: hidden white-space: nowrap color: #333 &.wheel-disabled-item opacity: .2; .picker-footer height: 20px ================================================ FILE: packages/react-examples/src/pages/pulldown/components/default.js ================================================ import React, { useState, useRef, useEffect, useCallback } from 'react' import BScroll from '@better-scroll/core' import PullDown from '@better-scroll/pull-down' BScroll.use(PullDown) const useStableCallback = (callback) => { const callbackRef = useRef(callback) useEffect(() => { callbackRef.current = callback }) return useCallback((...args) => callbackRef.current(...args), []) } const ajaxGet = (/* url */) => { return new Promise((resolve) => { setTimeout(() => { const dataList = generateData() resolve(dataList) }, 1000) }) } const TIME_BOUNCE = 800 let STEP = 0 function generateData() { const BASE = 30 const begin = BASE * STEP const end = BASE * (STEP + 1) let ret = [] for (let i = end; i > begin; i--) { ret.push(i) } return ret } const Default = () => { const [beforePullDown, setBeforePullDown] = useState(true) const [isPullingDown, setIsPullingDown] = useState(false) const [data, setData] = useState(generateData()) const wrapperRef = useRef(null) const scrollRef = useRef(null) const requestData = async () => { try { const newData = await ajaxGet(/* url */) setData((prev) => newData.concat(prev)) } catch (err) { // handle err console.log(err) } } const finishPullDown = () => { scrollRef.current.finishPullDown() setTimeout(() => { setBeforePullDown(true) scrollRef.current.refresh() }, TIME_BOUNCE + 100) } const pullingDownHandler = useStableCallback(async () => { setBeforePullDown(false) setIsPullingDown(true) STEP += 1 await requestData() setIsPullingDown(false) finishPullDown() }) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { scrollY: true, bounceTime: TIME_BOUNCE, useTransition: false, pullDownRefresh: { threshold: 70, stop: 56, }, })) BS.on('pullingDown', pullingDownHandler) BS.on('scroll', (pos) => { console.log(pos.y) }) BS.on('scrollEnd', () => { console.log('scrollEnd') }) } }, [pullingDownHandler]) return (
      Pull Down and refresh
      Loading...
      Refresh success
        {data.map((item, index) => (
      • {`I am item ${item} `}
      • ))}
      ) } export default Default ================================================ FILE: packages/react-examples/src/pages/pulldown/components/sina-weibo.js ================================================ import React, { useState, useRef, useEffect, useCallback } from 'react' import BScroll from '@better-scroll/core' import PullDown from '@better-scroll/pull-down' BScroll.use(PullDown) function generateData() { const BASE = 30 const begin = BASE * STEP const end = BASE * (STEP + 1) let ret = [] for (let i = end; i > begin; i--) { ret.push(i) } return ret } // pulldownRefresh state const PHASE = { moving: { enter: 'enter', leave: 'leave', }, fetching: 'fetching', succeed: 'succeed', } const TIME_BOUNCE = 800 const REQUEST_TIME = 2000 const THRESHOLD = 70 const STOP = 56 let STEP = 0 const ARROW_BOTTOM = '' const ARROW_UP = '' const mockFetchData = (/* url */) => { return new Promise((resolve) => { setTimeout(() => { const dataList = generateData() resolve(dataList) }, REQUEST_TIME) }) } const useStableCallback = (callback) => { const callbackRef = useRef(callback) useEffect(() => { callbackRef.current = callback }) return useCallback((...args) => callbackRef.current(...args), []) } const Sina = () => { const [tipText, setTipText] = useState('') const [data, setData] = useState(generateData()) const wrapperRef = useRef(null) const scrollRef = useRef(null) const getData = async () => { try { const newData = await mockFetchData() setData((prev) => newData.concat(prev)) } catch (err) { // handle err console.log(err) } } const pullingDownHandler = useStableCallback(async () => { updateTipText(PHASE.fetching) STEP += 1 await getData() updateTipText(PHASE.succeed) scrollRef.current.finishPullDown() setTimeout(() => { scrollRef.current.refresh() }, TIME_BOUNCE + 50) }) const updateTipText = useStableCallback((phase = PHASE.default) => { const TEXTS_MAP = { enter: `${ARROW_BOTTOM} Pull down`, leave: `${ARROW_UP} Release`, fetching: 'Loading...', succeed: 'Refresh succeed', } setTipText(TEXTS_MAP[phase]) }) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { scrollY: true, bounceTime: TIME_BOUNCE, useTransition: false, pullDownRefresh: { threshold: THRESHOLD, stop: STOP, }, })) BS.on('pullingDown', pullingDownHandler) BS.on('scrollEnd', () => { console.log('scrollEnd') }) // v2.4.0 supported BS.on('enterThreshold', () => { updateTipText(PHASE.moving.enter) }) BS.on('leaveThreshold', () => { updateTipText(PHASE.moving.leave) }) } }, [pullingDownHandler, updateTipText]) return (
        {data.map((item, index) => (
      • {`I am item ${item} `}
      • ))}
      ) } export default Sina ================================================ FILE: packages/react-examples/src/pages/pulldown/index.js ================================================ import React from 'react' import './index.styl' const examples = [ { path: '/pulldown/default', name: 'Default', }, { path: '/pulldown/sina', name: 'Sina-Weibo(v2.4.0)', }, ] const Pulldown = (props) => { const goPage = (path) => { props.history.push(path) } return (
        {examples.map((item) => (
      • goPage(item.path)} key={item.path} > {item.name}
      • ))}
      {props.children}
      ) } export default Pulldown ================================================ FILE: packages/react-examples/src/pages/pulldown/index.styl ================================================ .pulldown height 100% .pulldown-bswrapper position relative height 100% padding 0 10px border 1px solid #ccc overflow hidden .pulldown-list padding 0 .pulldown-list-item padding 10px 0 list-style none border-bottom 1px solid #ccc .pulldown-wrapper position absolute width 100% padding 20px box-sizing border-box transform translateY(-100%) translateZ(0) text-align center color #999 ================================================ FILE: packages/react-examples/src/pages/pullup/components/default.js ================================================ import React, { useState, useRef, useEffect, useCallback } from 'react' import BScroll from '@better-scroll/core' import Pullup from '@better-scroll/pull-up' BScroll.use(Pullup) const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const useStableCallback = (callback) => { const callbackRef = useRef(callback) useEffect(() => { callbackRef.current = callback }) return useCallback((...args) => callbackRef.current(...args), []) } const ajaxGet = (/* url */) => { return new Promise((resolve) => { setTimeout(() => { resolve(20) }, 1000) }) } const Default = () => { const [isPullUpLoad, setIsPullUpLoad] = useState(false) const [num, setNum] = useState(20) const wrapperRef = useRef(null) const scrollRef = useRef(null) const requestData = async () => { try { const newData = await ajaxGet(/* url */) setNum((n) => n + newData) } catch (err) { // handle err console.log(err) } } const pullingUpHandler = useStableCallback(async () => { setIsPullUpLoad(true) await requestData() scrollRef.current.finishPullUp() scrollRef.current.refresh() setIsPullUpLoad(false) }) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { pullUpLoad: true, })) BS.on('pullingUp', pullingUpHandler) } }, [pullingUpHandler]) return (
        {createArray(num).map((item, index) => (
      • {item % 5 === 0 ? 'scroll up 👆🏻' : `I am item ${item} `}
      • ))}
      {isPullUpLoad ? (
      Loading...
      ) : (
      Pull up and load more
      )}
      ) } export default Default ================================================ FILE: packages/react-examples/src/pages/pullup/index.js ================================================ import React from 'react' import './index.styl' const Pullup = (props) => <>{props.children} export default Pullup ================================================ FILE: packages/react-examples/src/pages/pullup/index.styl ================================================ .pullup height: 100% .pullup-wrapper height: 100% padding: 0 10px border: 1px solid #ccc overflow: hidden .pullup-list padding: 0 .pullup-list-item padding: 10px 0 list-style: none border-bottom: 1px solid #ccc .pullup-tips padding: 20px text-align: center color: #999 ================================================ FILE: packages/react-examples/src/pages/scrollbar/components/custom.js ================================================ import React, { useRef } from 'react' import BScroll from '@better-scroll/core' import ScrollBar from '@better-scroll/scroll-bar' import girlImageLink from './girl.jpg' BScroll.use(ScrollBar) const Custom = () => { const wrapperRef = useRef(null) const verticalRef = useRef(null) const horizontalRef = useRef(null) const scrollRef = useRef(null) const onLoad = () => { scrollRef.current = new BScroll(wrapperRef.current, { freeScroll: true, click: true, scrollbar: { customElements: [horizontalRef.current, verticalRef.current], fade: false, interactive: true, scrollbarTrackClickable: true, }, }) } return (
      custom {/* custom-vertical-scrollbar */}
      {/* custom-horizontal-scrollbar */}
      ) } export default Custom ================================================ FILE: packages/react-examples/src/pages/scrollbar/components/horizontal.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import ScrollBar from '@better-scroll/scroll-bar' BScroll.use(ScrollBar) const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const data = createArray(40) const Horizontal = () => { const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { scrollX: true, scrollY: false, click: true, probeType: 1, scrollbar: { fade: false, interactive: true, scrollbarTrackClickable: true, scrollbarTrackOffsetType: 'clickedPoint', // can use 'step' }, })) BS.on('scrollEnd', () => { console.log('scrollEnd') }) BS.on('scrollStart', () => { console.log('scrollStart') }) BS.on('scroll', () => { console.log('scroll') }) } }, []) return (
      {data.map((item) => (
      {item}
      ))}
      ) } export default Horizontal ================================================ FILE: packages/react-examples/src/pages/scrollbar/components/mousewheel.js ================================================ import React, { useRef } from 'react' import BScroll from '@better-scroll/core' import ScrollBar from '@better-scroll/scroll-bar' import MouseWheel from '@better-scroll/mouse-wheel' import girlImageLink from './sad-girl.jpg' BScroll.use(ScrollBar) BScroll.use(MouseWheel) const Mousewheel = () => { const wrapperRef = useRef(null) const horizontalRef = useRef(null) const scrollRef = useRef(null) const onLoad = () => { scrollRef.current = new BScroll(wrapperRef.current, { scrollX: true, scrollY: false, click: true, mouseWheel: true, scrollbar: { customElements: [horizontalRef.current], fade: true, interactive: true, scrollbarTrackClickable: true, }, }) } return (
      custom
      {/* custom-horizontal-scrollbar */}
      please use your mouse-wheel
      ) } export default Mousewheel ================================================ FILE: packages/react-examples/src/pages/scrollbar/components/vertical.js ================================================ import React, { useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import ScrollBar from '@better-scroll/scroll-bar' BScroll.use(ScrollBar) const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const data = createArray(40) const Vertical = () => { const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { scrollRef.current = new BScroll(wrapperRef.current, { scrollY: true, scrollbar: true, }) } }, []) return (
      {data.map((item) => (
      {`I am item ${item} `}
      ))}
      ) } export default Vertical ================================================ FILE: packages/react-examples/src/pages/scrollbar/index.js ================================================ import React from 'react' import './index.styl' const examples = [ { path: '/scrollbar/vertical', name: 'vertical', }, { path: '/scrollbar/horizontal', name: 'horizontal', }, { path: '/scrollbar/custom', name: 'custom', }, { path: '/scrollbar/mousewheel', name: 'use mousewheel', }, ] const Core = (props) => { const goPage = (path) => { props.history.push(path) } return (
        {examples.map((item) => (
      • goPage(item.path)} key={item.path} > {item.name}
      • ))}
      {props.children}
      ) } export default Core ================================================ FILE: packages/react-examples/src/pages/scrollbar/index.styl ================================================ .scrollbar height: 100% .scrollbar-wrapper position: relative height: 100% padding: 0 10px border: 1px solid #ccc overflow: hidden .scrollbar-content-item padding: 10px 0 list-style: none border-bottom: 1px solid #ccc text-align: left .horizontal-scrollbar-container .scroll-wrapper position relative display flex align-content center width 90% height 100px margin 80px auto white-space nowrap border 3px solid rgba(30, 80, 255, 0.3) border-radius 5px overflow hidden .scroll-content display inline-block align-self center .scroll-item opacity 0.6 color white box-sizing border-box height 50px width 50px line-height 50px border-radius 50% font-size 18px display inline-block text-align center padding 0 10px margin 0 10px &:nth-child(4n) background-color #06F &:nth-child(4n+1) background-color #0f9d00 &:nth-child(4n+2) background-color #F00 &:nth-child(4n+3) background-color #ffea00 .custom-scrollbar-container .custom-scrollbar-wrapper position relative width 280px height 280px overflow hidden .custom-scrollbar-content max-width none .custom-vertical-scrollbar position absolute top 50% right 10px height 100px width 7px border-radius 6px transform translateY(-50%) translateZ(0) background-color rgb(200, 200, 200, 0.3) .custom-vertical-indicator width 100% height 20px border-radius 6px background-color #db8090 .custom-horizontal-scrollbar position absolute left 50% bottom 10px width 100px height 7px border-radius 6px transform translateX(-50%) translateZ(0) background-color rgb(200, 200, 200, 0.3) .custom-horizontal-indicator height 100% width 20px border-radius 6px background-color #db8090 .mousewheel-scrollbar-container .custom-scrollbar-wrapper position relative width 280px height 280px overflow hidden .custom-scrollbar-content display inline-block height 280px > img max-width none .custom-horizontal-scrollbar position absolute left 50% bottom 10px width 100px height 7px border-radius 6px transform translateX(-50%) translateZ(0) background-color rgb(200, 200, 200, 0.3) .custom-horizontal-indicator height 100% width 20px border-radius 6px background-color #db8090 .tip text-align center margin-top 10px ================================================ FILE: packages/react-examples/src/pages/slide/components/banner.js ================================================ import React, { useState, useRef, useEffect } from 'react' import classNames from 'classnames' import BScroll from '@better-scroll/core' import Slide from '@better-scroll/slide' BScroll.use(Slide) const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const nums = createArray(4) const Banner = () => { const [currentPageIndex, setCurrentPageIndex] = useState(0) const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { scrollX: true, scrollY: false, slide: true, momentum: false, bounce: false, probeType: 3, })) BS.on('scrollEnd', () => { console.log('CurrentPage => ', BS.getCurrentPage()) }) BS.on('slideWillChange', (page) => { setCurrentPageIndex(page.pageX) }) // v2.1.0 BS.on('slidePageChanged', (page) => { console.log('CurrentPage changed to => ', page) }) } return () => { scrollRef.current?.destroy() } }, []) const handleNextPage = () => { scrollRef.current.next() } const handlePrePage = () => { scrollRef.current.prev() } return (
      {nums.map((num) => (
      page {num}
      ))}
      {nums.map((num) => ( ))}
      ) } export default Banner ================================================ FILE: packages/react-examples/src/pages/slide/components/dynamic.js ================================================ import React, { useState, useRef, useEffect } from 'react' import classNames from 'classnames' import BScroll from '@better-scroll/core' import Slide from '@better-scroll/slide' BScroll.use(Slide) const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const Dynamic = () => { const [num, setNum] = useState(1) const [currentPageIndex, setCurrentPageIndex] = useState(0) const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { scrollX: true, scrollY: false, slide: { autoplay: false, loop: true, }, momentum: false, bounce: false, probeType: 3, })) BS.on('scrollEnd', () => { console.log('【scrollEnd】CurrentPage => ', BS.getCurrentPage()) }) BS.on('slideWillChange', (page) => { console.log('【slideWillChange】CurrentPage =>', page) setCurrentPageIndex(page.pageX) }) // v2.1.0 BS.on('slidePageChanged', (page) => { console.log('【slidePageChanged】CurrentPage =>', page) }) } return () => { scrollRef.current?.destroy() } }, []) useEffect(() => { scrollRef.current?.refresh() }, [num]) const handleIncrease = () => { setNum((n) => n + 1) } const handleDecrease = () => { setNum((n) => Math.max(1, n - 1)) } return (
      {createArray(num).map((n) => (
      page {n}
      ))}
      {createArray(num).map((n) => ( ))}
      ) } export default Dynamic ================================================ FILE: packages/react-examples/src/pages/slide/components/fullpage.js ================================================ import React, { useState, useRef, useEffect } from 'react' import classNames from 'classnames' import BScroll from '@better-scroll/core' import Slide from '@better-scroll/slide' BScroll.use(Slide) const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const nums = createArray(4) const FullPage = () => { const [currentPageIndex, setCurrentPageIndex] = useState(0) const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { scrollX: true, scrollY: false, slide: { threshold: 100, loop: false, autoplay: false, }, useTransition: false, momentum: false, bounce: false, stopPropagation: true, })) BS.on('scrollEnd', () => { setCurrentPageIndex(BS.getCurrentPage().pageX) }) } return () => { scrollRef.current?.destroy() } }, []) return (
      {nums.map((num) => (
      page {num}
      ))}
      {nums.map((num) => ( ))}
      ) } export default FullPage ================================================ FILE: packages/react-examples/src/pages/slide/components/specified-index.js ================================================ import React, { useState, useRef, useEffect } from 'react' import BScroll from '@better-scroll/core' import Slide from '@better-scroll/slide' BScroll.use(Slide) const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const nums = createArray(4) const SpecifiedIndex = () => { const [slideCreated, setSlideCreated] = useState(false) const [currentPageIndex, setCurrentPageIndex] = useState(0) const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { scrollX: true, scrollY: false, slide: { autoplay: false, loop: true, startPageXIndex: 2, // v2.3.0 }, momentum: false, bounce: false, probeType: 3, })) setCurrentPageIndex(BS.getCurrentPage().pageX) setSlideCreated(true) // v2.1.0 BS.on('slidePageChanged', (page) => { console.log('CurrentPage changed to => ', page) setCurrentPageIndex(page.pageX) }) } return () => { scrollRef.current?.destroy() } }, []) const handleNextPage = () => { scrollRef.current.next() } const handlePrePage = () => { scrollRef.current.prev() } return (
      {nums.map((num) => (
      page {num}
      ))}
      {slideCreated && (
      currentPageIndex is {currentPageIndex}
      )}
      ) } export default SpecifiedIndex ================================================ FILE: packages/react-examples/src/pages/slide/components/vertical.js ================================================ import React, { useState, useRef, useEffect } from 'react' import classNames from 'classnames' import BScroll from '@better-scroll/core' import Slide from '@better-scroll/slide' BScroll.use(Slide) const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const nums = createArray(4) const Vertical = () => { const [currentPageIndex, setCurrentPageIndex] = useState(0) const wrapperRef = useRef(null) const scrollRef = useRef(null) useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { scrollX: false, scrollY: true, slide: { threshold: 100, }, useTransition: true, momentum: false, bounce: false, stopPropagation: true, })) BS.on('scrollEnd', () => { setCurrentPageIndex(BS.getCurrentPage().pageY) }) } return () => { scrollRef.current?.destroy() } }, []) return (
      {nums.map((num) => (
      page {num}
      ))}
      {nums.map((num) => ( ))}
      ) } export default Vertical ================================================ FILE: packages/react-examples/src/pages/slide/index.js ================================================ import React from 'react' import './index.styl' const examples = [ { path: '/slide/banner', name: 'banner slide', }, { path: '/slide/fullpage', name: 'page slide', }, { path: '/slide/vertical', name: 'vertical slide', }, { path: '/slide/dynamic', name: 'dynamic slide(v2.1.0)', }, { path: '/slide/specified', name: 'specified index slide(v2.3.0)', }, ] const Core = (props) => { const goPage = (path) => { props.history.push(path) } return (
        {examples.map((item) => (
      • goPage(item.path)} key={item.path} > {item.name}
      • ))}
      {props.children}
      ) } export default Core ================================================ FILE: packages/react-examples/src/pages/slide/index.styl ================================================ .slide-banner .banner-wrapper position relative .slide-banner-wrapper min-height 1px overflow hidden .slide-banner-content height 200px white-space nowrap font-size 0 .slide-page display inline-block height 200px width 100% line-height 200px text-align center font-size 26px &.page1 background-color #95B8D1 &.page2 background-color #DDA789 &.page3 background-color #C3D899 &.page4 background-color #F2D4A7 .dots-wrapper position absolute bottom 4px left 50% transform translateX(-50%) .dot display inline-block margin 0 4px width 8px height 8px border-radius 50% background #eee &.active width 20px border-radius 5px .btn-wrap margin-top 20px display flex justify-content center button margin 0 10px padding 10px color #fff border-radius 4px background-color #666 .slide-fullpage height 100% &.view padding 0 height 100% .banner-wrapper position relative height 100% .slide-banner-wrapper height 100% overflow hidden .slide-banner-content height 100% white-space nowrap font-size 0 .slide-page display inline-block height 100% width 100% line-height 200px text-align center font-size 26px transform translate3d(0,0,0) backface-visibility hidden &.page1 background-color #95B8D1 &.page2 background-color #DDA789 &.page3 background-color #C3D899 &.page4 background-color #F2D4A7 .dots-wrapper position absolute bottom 4px left 50% transform translateX(-50%) .dot display inline-block margin 0 4px width 8px height 8px border-radius 50% background #eee &.active width 20px border-radius 5px .slide-vertical height 100% &.view padding 0 height 100% .vertical-wrapper position relative height 100% font-size 0 .slide-vertical-wrapper height 100% overflow hidden .slide-page display inline-block width 100% line-height 200px text-align center font-size 26px transform translate3d(0,0,0) backface-visibility hidden &.page1 background-color #D6EADF &.page2 background-color #DDA789 &.page3 background-color #C3D899 &.page4 background-color #F2D4A7 .dots-wrapper position absolute right 4px top 50% transform translateY(-50%) .dot display block margin 4px 0 width 8px height 8px border-radius 50% background #eee &.active height 20px border-radius 5px .dynamic-slide-banner .banner-wrapper position relative .slide-banner-wrapper min-height 1px overflow hidden .slide-banner-content height 200px white-space nowrap font-size 0 .slide-page display inline-block height 200px width 100% line-height 200px text-align center font-size 26px &.page1 background-color #95B8D1 &.page2 background-color #DDA789 &.page3 background-color #C3D899 &.page4 background-color #F2D4A7 &.page5 background-color #E71D36 &.page6 background-color #2EC4B6 &.page7 background-color #EFFFE9 &.page8 background-color #011627 .dots-wrapper position absolute bottom 4px left 50% transform translateX(-50%) .dot display inline-block margin 0 4px width 8px height 8px border-radius 50% background #eee &.active width 20px border-radius 5px .btn-wrap margin-top 20px display flex justify-content center button margin 0 10px padding 10px color #fff border-radius 4px background-color #666 .slide-specified-index .banner-wrapper position relative .slide-specified-wrapper min-height 1px overflow hidden .slide-specified-content height 200px white-space nowrap font-size 0 .slide-page display inline-block height 200px width 100% line-height 200px text-align center font-size 26px &.page1 background-color #95B8D1 &.page2 background-color #DDA789 &.page3 background-color #C3D899 &.page4 background-color #F2D4A7 .description text-align center .btn-wrap margin-top 20px display flex justify-content center button margin 0 10px padding 10px color #fff border-radius 4px background-color #666 ================================================ FILE: packages/react-examples/src/pages/zoom/components/default.js ================================================ import React, { useRef, useState, useEffect } from 'react' import BScroll from '@better-scroll/core' import Zoom from '@better-scroll/zoom' import good from './good.svg' BScroll.use(Zoom) const createArray = (length) => Array.from({ length }, (_v, i) => i + 1) const nums = createArray(16) const Default = () => { const [linkworkTransform, setLinkworkTransform] = useState('scale(1)') const wrapperRef = useRef(null) const scrollRef = useRef(null) const handleZoomTo = (value) => { scrollRef.current.zoomTo(value, 'center', 'center') } useEffect(() => { if (!scrollRef.current) { const BS = (scrollRef.current = new BScroll(wrapperRef.current, { freeScroll: true, scrollX: true, scrollY: true, disableMouse: true, useTransition: true, zoom: { start: 1.5, min: 0.5, max: 3, initialOrigin: ['center', 'center'], }, })) BS.on('zooming', ({ scale }) => { setLinkworkTransform(`scale(${scale})`) }) BS.on('zoomEnd', ({ scale }) => { console.log(scale) }) return () => { scrollRef.current?.destroy() } } }, []) return (
      {nums.map((item, index) => (
      {item}
      ))}

      changing with zooming action

      ) } export default Default ================================================ FILE: packages/react-examples/src/pages/zoom/index.js ================================================ import React from 'react' import './index.styl' const Zoom = (props) => <>{props.children} export default Zoom ================================================ FILE: packages/react-examples/src/pages/zoom/index.styl ================================================ .zoom-default .zoom-wrapper width 100% overflow hidden .zoom-items display flex flex-direction row flex-wrap wrap align-content space-between .grid-item flex 1 1 25% box-sizing border-box height 52px line-height 52px border 1px solid #eee text-align center &:nth-child(2n) background-color #b3d4a8 &:nth-child(2n+1) background-color #b6b7a3 .btn-wrap margin-top 20px display flex justify-content center button margin 0 10px padding 10px color #fff border-radius 4px background-color #666 .linkwork-wrap margin-top 50px p margin 10px 0 font-size 16px font-weight bold text-align center .linkwork-block margin 10px auto width 60px height 60px border-radius 50% img { width 100% } ================================================ FILE: packages/react-examples/src/reportWebVitals.js ================================================ const reportWebVitals = onPerfEntry => { if (onPerfEntry && onPerfEntry instanceof Function) { import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { getCLS(onPerfEntry); getFID(onPerfEntry); getFCP(onPerfEntry); getLCP(onPerfEntry); getTTFB(onPerfEntry); }); } }; export default reportWebVitals; ================================================ FILE: packages/react-examples/src/router.js ================================================ import React, { lazy, Suspense } from 'react' import { Switch, Route } from 'react-router-dom' const ROUTES = [ { path: '/core', component: lazy(() => import('./pages/core')), routes: [ { path: '/core/default', component: lazy(() => import('./pages/core/components/default')), }, { path: '/core/horizontal', component: lazy(() => import('./pages/core/components/horizontal')), }, { path: '/core/dynamic-content', component: lazy(() => import('./pages/core/components/dynamic-content') ), }, { path: '/core/specified-content', component: lazy(() => import('./pages/core/components/specified-content') ), }, { path: '/core/freescroll', component: lazy(() => import('./pages/core/components/freescroll')), }, { path: '/core/vertical-rotated', component: lazy(() => import('./pages/core/components/vertical-rotated') ), }, { path: '/core/horizontal-rotated', component: lazy(() => import('./pages/core/components/horizontal-rotated') ), }, ], }, { path: '/observe-dom', component: lazy(() => import('./pages/observe-dom')), routes: [ { path: '/observe-dom/', component: lazy(() => import('./pages/observe-dom/components/default')), }, ], }, { path: '/observe-image', component: lazy(() => import('./pages/observe-image')), routes: [ { path: '/observe-image/', component: lazy(() => import('./pages/observe-image/components/default') ), }, ], }, { path: '/slide', component: lazy(() => import('./pages/slide')), routes: [ { path: '/slide/banner', component: lazy(() => import('./pages/slide/components/banner')), }, { path: '/slide/fullpage', component: lazy(() => import('./pages/slide/components/fullpage')), }, { path: '/slide/vertical', component: lazy(() => import('./pages/slide/components/vertical')), }, { path: '/slide/dynamic', component: lazy(() => import('./pages/slide/components/dynamic')), }, { path: '/slide/specified', component: lazy(() => import('./pages/slide/components/specified-index') ), }, ], }, { path: '/zoom', component: lazy(() => import('./pages/zoom')), routes: [ { path: '/zoom/', component: lazy(() => import('./pages/zoom/components/default')), }, ], }, { path: '/picker', component: lazy(() => import('./pages/picker')), routes: [ { path: '/picker/one-column', component: lazy(() => import('./pages/picker/components/one-column')), }, { path: '/picker/double-column', component: lazy(() => import('./pages/picker/components/double-column') ), }, { path: '/picker/linkage-column', component: lazy(() => import('./pages/picker/components/linkage-column') ), }, ], }, { path: '/pullup', component: lazy(() => import('./pages/pullup')), routes: [ { path: '/pullup/', component: lazy(() => import('./pages/pullup/components/default')), }, ], }, { path: '/pulldown', component: lazy(() => import('./pages/pulldown')), routes: [ { path: '/pulldown/default', component: lazy(() => import('./pages/pulldown/components/default')), }, { path: '/pulldown/sina', component: lazy(() => import('./pages/pulldown/components/sina-weibo')), }, ], }, { path: '/scrollbar', component: lazy(() => import('./pages/scrollbar')), routes: [ { path: '/scrollbar/vertical', component: lazy(() => import('./pages/scrollbar/components/vertical')), }, { path: '/scrollbar/horizontal', component: lazy(() => import('./pages/scrollbar/components/horizontal') ), }, { path: '/scrollbar/custom', component: lazy(() => import('./pages/scrollbar/components/custom')), }, { path: '/scrollbar/mousewheel', component: lazy(() => import('./pages/scrollbar/components/mousewheel') ), }, ], }, { path: '/indicators', component: lazy(() => import('./pages/indicators')), routes: [ { path: '/indicators/minimap', component: lazy(() => import('./pages/indicators/components/minimap')), }, { path: '/indicators/parallax-scroll', component: lazy(() => import('./pages/indicators/components/parallax-scroll') ), }, ], }, { path: '/infinity', component: lazy(() => import('./pages/infinity')), }, { path: '/form', component: lazy(() => import('./pages/form')), routes: [ { path: '/form/textarea', component: lazy(() => import('./pages/form/components/textarea')), }, ], }, { path: '/nested-scroll', component: lazy(() => import('./pages/nested-scroll')), routes: [ { path: '/nested-scroll/vertical', component: lazy(() => import('./pages/nested-scroll/components/vertical') ), }, { path: '/nested-scroll/horizontal', component: lazy(() => import('./pages/nested-scroll/components/horizontal') ), }, { path: '/nested-scroll/horizontal-in-vertical', component: lazy(() => import('./pages/nested-scroll/components/horizontal-in-vertical') ), }, { path: '/nested-scroll/triple-vertical', component: lazy(() => import('./pages/nested-scroll/components/triple-vertical') ), }, ], }, { path: '/mouse-wheel', component: lazy(() => import('./pages/mouse-wheel')), routes: [ { path: '/mouse-wheel/vertical-scroll', component: lazy(() => import('./pages/mouse-wheel/components/vertical-scroll') ), }, { path: '/mouse-wheel/horizontal-scroll', component: lazy(() => import('./pages/mouse-wheel/components/horizontal-scroll') ), }, { path: '/mouse-wheel/vertical-slide', component: lazy(() => import('./pages/mouse-wheel/components/vertical-slide') ), }, { path: '/mouse-wheel/horizontal-slide', component: lazy(() => import('./pages/mouse-wheel/components/horizontal-slide') ), }, { path: '/mouse-wheel/pullup', component: lazy(() => import('./pages/mouse-wheel/components/pullup')), }, { path: '/mouse-wheel/pulldown', component: lazy(() => import('./pages/mouse-wheel/components/pulldown') ), }, { path: '/mouse-wheel/picker', component: lazy(() => import('./pages/mouse-wheel/components/picker')), }, ], }, { path: '/movable', component: lazy(() => import('./pages/movable')), routes: [ { path: '/movable/default', component: lazy(() => import('./pages/movable/components/default')), }, { path: '/movable/scale', component: lazy(() => import('./pages/movable/components/scale')), }, { path: '/movable/multi-content', component: lazy(() => import('./pages/movable/components/multi-content') ), }, { path: '/movable/multi-content-scale', component: lazy(() => import('./pages/movable/components/multi-content-scale') ), }, ], }, { path: '/compose', component: lazy(() => import('./pages/compose')), routes: [ { path: '/compose/pullup-pulldown', component: lazy(() => import('./pages/compose/components/pullup-pulldown') ), }, { path: '/compose/pullup-pulldown-slide', component: lazy(() => import('./pages/compose/components/pullup-pulldown-slide') ), }, { path: '/compose/pullup-pulldown-outnested', component: lazy(() => import('./pages/compose/components/pullup-pulldown-outnested') ), }, { path: '/compose/slide-nested', component: lazy(() => import('./pages/compose/components/slide-nested') ), }, ], }, ] const renderRoute = (route) => { const { component: Component, ...rest } = route return ( ( )} /> ) } const Router = ({ routes }) => { if (!routes?.length) { return null } return {routes.map((route) => renderRoute(route))} } const RouterView = () => ( ) export default RouterView ================================================ FILE: packages/react-examples/src/setupTests.js ================================================ // jest-dom adds custom jest matchers for asserting on DOM nodes. // allows you to do things like: // expect(element).toHaveTextContent(/react/i) // learn more: https://github.com/testing-library/jest-dom import '@testing-library/jest-dom'; ================================================ FILE: packages/scroll-bar/README.md ================================================ # @better-scroll/scroll-bar [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/scroll-bar/README_zh-CN.md) An elegant and beautiful scroll bar. ## Usage ```js import BScroll from '@better-scroll/core' import Scrollbar from '@better-scroll/scroll-bar' BScroll.use(Scrollbar) const bs = new BScroll('.wrapper', { scrollbar: true }) ``` ================================================ FILE: packages/scroll-bar/README_zh-CN.md ================================================ # @better-scroll/scroll-bar 一个优雅美丽的滚动条. ## 使用 ```js import BScroll from '@better-scroll/core' import Scrollbar from '@better-scroll/scroll-bar' BScroll.use(Scrollbar) const bs = new BScroll('.wrapper', { scrollbar: true }) ``` ================================================ FILE: packages/scroll-bar/package.json ================================================ { "name": "@better-scroll/scroll-bar", "version": "2.5.1", "description": "scrollbar is used to BetterScroll, which behaves like browser scrollbar", "author": { "name": "jizhi", "email": "theniceangel@163.com" }, "main": "dist/scroll-bar.min.js", "module": "dist/scroll-bar.esm.js", "typings": "dist/types/index.d.ts", "publishConfig": { "access": "public" }, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios", "scrollbar" ], "repository": { "type": "git", "url": "git+ssh://git@github.com/ustbhuangyi/better-scroll.git", "directory": "packages/scroll-bar" }, "dependencies": { "@better-scroll/core": "^2.5.1" }, "gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130" } ================================================ FILE: packages/scroll-bar/src/__mocks__/event-handler.ts ================================================ import { EventEmitter } from '@better-scroll/shared-utils' const EventHandler = jest.fn().mockImplementation(() => { return { hooks: new EventEmitter(['touchStart', 'touchMove', 'touchEnd']), destroy: jest.fn(), } }) export default EventHandler ================================================ FILE: packages/scroll-bar/src/__mocks__/indicator.ts ================================================ const mockIndicator = jest .fn() .mockImplementation(function IndicatorMockFn(scroll: any, options: any) { return { wrapper: options.wrapper, destroy: jest.fn(), } }) export default mockIndicator ================================================ FILE: packages/scroll-bar/src/__tests__/__snapshots__/index.spec.ts.snap ================================================ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`scroll-bar unit tests constructor should create indicator elements 1`] = `
      `; ================================================ FILE: packages/scroll-bar/src/__tests__/event-handler.spec.ts ================================================ import Indicator, { IndicatorDirection, OffsetType } from '../indicator' import BScroll from '@better-scroll/core' import EventHandler from '../event-handler' import { dispatchTouchStart, dispatchTouchMove, dispatchTouchEnd, } from '@better-scroll/core/src/__tests__/__utils__/event' const addProperties = ( target: T, source: K ) => { for (const key in source) { ;(target as any)[key] = source[key] } return target } describe('scroll-bar indicator tests', () => { let scroll: BScroll let indicator: Indicator let wrapper = document.createElement('div') let indicatorEl = document.createElement('div') wrapper.appendChild(indicatorEl) let indicatorOptions = { wrapper, direction: IndicatorDirection.Vertical, fade: true, fadeInTime: 250, fadeOutTime: 500, interactive: false, minSize: 8, isCustom: false, scrollbarTrackClickable: false, scrollbarTrackOffsetType: OffsetType.Step, scrollbarTrackOffsetTime: 300, } let EventHandlerOptions = { disableMouse: false, disableTouch: false, } beforeEach(() => { // create Dom const wrapper = document.createElement('div') const content = document.createElement('div') wrapper.appendChild(content) scroll = new BScroll(wrapper, {}) indicator = new Indicator(scroll, indicatorOptions) }) afterEach(() => { jest.clearAllMocks() }) it('touchStart | touchMove | touchEnd hooks', () => { const eventHandler = new EventHandler(indicator, EventHandlerOptions) expect(Object.keys(eventHandler.hooks.eventTypes)).toMatchObject([ 'touchStart', 'touchMove', 'touchEnd', ]) const touchStartMockFn = jest.fn() const touchMoveMockFn = jest.fn() const touchEndMockFn = jest.fn() const eventHandlerHooks = eventHandler.hooks eventHandlerHooks.on( eventHandlerHooks.eventTypes.touchStart, touchStartMockFn ) eventHandlerHooks.on( eventHandlerHooks.eventTypes.touchMove, touchMoveMockFn ) eventHandlerHooks.on(eventHandlerHooks.eventTypes.touchEnd, touchEndMockFn) // scroll is disabled addProperties(scroll, { enabled: false, }) dispatchTouchStart(indicatorEl, [ { pageX: 0, pageY: 0, }, ]) dispatchTouchMove(window, [ { pageX: 10, pageY: 10, }, ]) dispatchTouchEnd(window, [ { pageX: 20, pageY: 20, }, ]) expect(touchStartMockFn).not.toBeCalled() expect(touchMoveMockFn).not.toBeCalled() expect(touchEndMockFn).not.toBeCalled() // scroll is enabled addProperties(scroll, { enabled: true, }) dispatchTouchStart(indicatorEl, [ { pageX: 0, pageY: 0, }, ]) dispatchTouchMove(window, [ { pageX: 10, pageY: 10, }, ]) dispatchTouchEnd(window, [ { pageX: 20, pageY: 20, }, ]) expect(touchStartMockFn).toBeCalled() expect(touchMoveMockFn).toBeCalled() expect(touchEndMockFn).toBeCalled() }) it('destroy', () => { const eventHandler = new EventHandler(indicator, EventHandlerOptions) eventHandler.destroy() }) }) ================================================ FILE: packages/scroll-bar/src/__tests__/index.spec.ts ================================================ import Indicator from '../indicator' import BScroll, { Options } from '@better-scroll/core' jest.mock('@better-scroll/core') jest.mock('../indicator') import ScrollBar from '../index' const addProperties = ( target: T, source: K ) => { for (const key in source) { ;(target as any)[key] = source[key] } return target } describe('scroll-bar unit tests', () => { let scroll: BScroll let options: Partial beforeAll(() => { // create Dom const wrapper = document.createElement('div') const content = document.createElement('div') wrapper.appendChild(content) // mock bscroll options = { scrollbar: true, scrollX: true, scrollY: true, } scroll = new BScroll(wrapper, options) }) beforeEach(() => { jest.clearAllMocks() }) describe('constructor', () => { it('should create indicator elements', () => { const scrollbar = new ScrollBar(scroll) // then expect(scroll.wrapper).toMatchSnapshot() expect(scrollbar.options).toMatchObject({ fade: true, interactive: false, customElements: [], minSize: 8, scrollbarTrackClickable: false, scrollbarTrackOffsetType: 'step', scrollbarTrackOffsetTime: 300, }) }) it('custom scrollbar', () => { const customHScrollbar = document.createElement('div') addProperties(scroll.options, { scrollX: true, scrollY: false, scrollbar: { customElements: [customHScrollbar], }, }) const scrollbar = new ScrollBar(scroll) expect(scrollbar.indicators[0].wrapper).toBe(customHScrollbar) }) it('destroy hook', () => { const scrollbar = new ScrollBar(scroll) scroll.hooks.trigger(scroll.hooks.eventTypes.destroy) for (let indicator of scrollbar.indicators) { expect(indicator.destroy).toBeCalled() } }) }) }) ================================================ FILE: packages/scroll-bar/src/__tests__/indicator.spec.ts ================================================ import Indicator, { IndicatorDirection, OffsetType } from '../indicator' import EventHandler from '../event-handler' import BScroll from '@better-scroll/core' import { dispatchClick } from '@better-scroll/core/src/__tests__/__utils__/event' jest.mock('@better-scroll/core') jest.mock('../event-handler') const addProperties = ( target: T, source: K ) => { for (const key in source) { ;(target as any)[key] = source[key] } return target } describe('scroll-bar indicator tests', () => { let scroll: BScroll let indicator: Indicator let wrapper = document.createElement('div') let indicatorEl = document.createElement('div') wrapper.appendChild(indicatorEl) let indicatorOptions = { wrapper, direction: IndicatorDirection.Vertical, fade: true, fadeInTime: 250, fadeOutTime: 500, interactive: false, minSize: 8, isCustom: false, scrollbarTrackClickable: false, scrollbarTrackOffsetType: OffsetType.Step, scrollbarTrackOffsetTime: 300, } beforeEach(() => { // create Dom const wrapper = document.createElement('div') const content = document.createElement('div') wrapper.appendChild(content) scroll = new BScroll(wrapper, {}) }) afterEach(() => { jest.clearAllMocks() }) it('should have corrent key', () => { indicator = new Indicator(scroll, indicatorOptions) expect(indicator.keysMap).toMatchObject({ hasScroll: 'hasVerticalScroll', size: 'height', wrapperSize: 'clientHeight', scrollerSize: 'scrollerHeight', maxScrollPos: 'maxScrollY', pos: 'y', point: 'pageY', translateProperty: 'translateY', domRect: 'top', }) expect(wrapper.style.opacity).toEqual('0') }) it('refresh hook', () => { Object.assign(indicatorOptions, { direction: IndicatorDirection.Horizontal, }) indicator = new Indicator(scroll, indicatorOptions) addProperties(scroll, { hasHorizontalScroll: true, maxScrollX: 8, }) scroll.hooks.trigger(scroll.hooks.eventTypes.refresh) expect(indicator.currentPos).toBe(-8) }) it('translate hook', () => { Object.assign(indicatorOptions, { direction: IndicatorDirection.Horizontal, }) indicator = new Indicator(scroll, indicatorOptions) addProperties(scroll, { hasHorizontalScroll: true, maxScrollX: 8, }) const translaterHooks = scroll.scroller.translater.hooks scroll.hooks.trigger(scroll.hooks.eventTypes.refresh) translaterHooks.trigger(translaterHooks.eventTypes.translate, { x: 10, y: 0, }) expect(indicator.currentPos).toBe(0) }) it('transitionTime and transitionTimingFunction hook', () => { Object.assign(indicatorOptions, { direction: IndicatorDirection.Horizontal, }) indicator = new Indicator(scroll, indicatorOptions) const animaterHooks = scroll.scroller.animater.hooks animaterHooks.trigger(animaterHooks.eventTypes.time) animaterHooks.trigger( animaterHooks.eventTypes.timeFunction, 'cubic-bezier(0.23, 1, 0.32, 1)' ) expect(indicator.indicatorEl.style.transitionDuration).toBe('0ms') expect(indicator.indicatorEl.style.transitionTimingFunction).toBe( 'cubic-bezier(0.23, 1, 0.32, 1)' ) }) it("about scrolling's hook", () => { Object.assign(indicatorOptions, { direction: IndicatorDirection.Horizontal, }) indicator = new Indicator(scroll, indicatorOptions) scroll.trigger(scroll.eventTypes.scrollStart) expect(indicator.wrapper.style.opacity).toBe('1') scroll.trigger(scroll.eventTypes.scrollEnd) expect(indicator.wrapper.style.opacity).toBe('0') }) it("about mouse-wheel scrolling's hook", () => { Object.assign(indicatorOptions, { direction: IndicatorDirection.Horizontal, }) indicator = new Indicator(scroll, indicatorOptions) scroll.registerType(['mousewheelStart', 'mousewheelMove', 'mousewheelEnd']) scroll.trigger(scroll.eventTypes.mousewheelStart) expect(indicator.wrapper.style.opacity).toBe('1') scroll.trigger(scroll.eventTypes.mousewheelEnd) expect(indicator.wrapper.style.opacity).toBe('0') scroll.trigger(scroll.eventTypes.mousewheelMove) expect(indicator.wrapper.style.opacity).toBe('1') }) it('interactive option', () => { // horizontal addProperties(scroll.options, { probeType: 3, }) addProperties(scroll, { hasHorizontalScroll: true, maxScrollX: 8, }) Object.assign(indicatorOptions, { direction: IndicatorDirection.Horizontal, interactive: true, }) indicator = new Indicator(scroll, indicatorOptions) indicator.refresh() const beforeStartMockFn = jest.fn() const startMockFn = jest.fn() const moveMockFn = jest.fn() const endMockFn = jest.fn() const scroller = scroll.scroller scroller.hooks.on( scroller.hooks.eventTypes.beforeScrollStart, beforeStartMockFn ) scroller.hooks.on(scroller.hooks.eventTypes.scrollStart, startMockFn) scroller.hooks.on(scroller.hooks.eventTypes.scroll, moveMockFn) scroller.hooks.on(scroller.hooks.eventTypes.scrollEnd, endMockFn) const eventHandlerHooks = indicator.eventHandler.hooks indicator.scrollInfo.maxScrollPos = 10 eventHandlerHooks.trigger(eventHandlerHooks.eventTypes.touchStart) eventHandlerHooks.trigger(eventHandlerHooks.eventTypes.touchMove, 2) eventHandlerHooks.trigger(eventHandlerHooks.eventTypes.touchEnd) expect(beforeStartMockFn).toBeCalled() expect(startMockFn).toBeCalled() expect(moveMockFn).toBeCalled() expect(endMockFn).toBeCalled() expect(scroll.scroller.translater.translate).toBeCalled() // vertical addProperties(scroll.options, { probeType: 1, }) addProperties(scroll, { hasHorizontalScroll: false, hasVerticalScroll: true, maxScrollX: 0, maxScrollY: 8, }) addProperties(indicator, { direction: IndicatorDirection.Vertical, }) eventHandlerHooks.trigger(eventHandlerHooks.eventTypes.touchStart) indicator.startTime = indicator.startTime - 400 indicator.scrollInfo.maxScrollPos = 10 eventHandlerHooks.trigger(eventHandlerHooks.eventTypes.touchMove, 2) eventHandlerHooks.trigger(eventHandlerHooks.eventTypes.touchEnd) expect(beforeStartMockFn).toBeCalledTimes(2) expect(startMockFn).toBeCalledTimes(2) expect(moveMockFn).toBeCalledTimes(2) expect(endMockFn).toBeCalledTimes(2) expect(scroll.scroller.translater.translate).toBeCalledTimes(2) }) it('updatePosition', () => { addProperties(scroll, { maxScrollY: -8, }) addProperties(indicatorOptions, { direction: IndicatorDirection.Vertical, }) indicator = new Indicator(scroll, indicatorOptions) indicator.refresh() indicator.updatePosition({ x: 0, y: -2, }) expect(indicator.currentPos).toBe(0) addProperties(scroll, { maxScrollY: 8, }) addProperties(indicator.options, { isCustom: true, }) indicator.refresh() indicator.updatePosition({ x: 0, y: 2, }) expect(indicator.currentPos).toBe(0) }) it('click', () => { addProperties(scroll, { maxScrollY: -8, }) addProperties(indicatorOptions, { direction: IndicatorDirection.Vertical, scrollbarTrackClickable: true, }) indicator = new Indicator(scroll, indicatorOptions) indicator.refresh() dispatchClick(indicator.wrapper, 'click') expect(scroll.scrollTo).toBeCalled() addProperties(indicator, { direction: IndicatorDirection.Horizontal, }) addProperties(indicator.options, { scrollbarTrackClickable: true, scrollbarTrackOffsetType: OffsetType.Point, }) dispatchClick(indicator.wrapper, 'click') expect(scroll.scrollTo).toBeCalled() }) it('destroy', () => { const parentNode = document.createElement('div') parentNode.appendChild(wrapper) addProperties(indicatorOptions, { direction: IndicatorDirection.Vertical, scrollbarTrackClickable: true, isCustom: false, }) indicator = new Indicator(scroll, indicatorOptions) indicator.destroy() expect(indicator.eventHandler.destroy).toBeCalled() expect(indicator.hooksFn.length).toBe(0) }) }) ================================================ FILE: packages/scroll-bar/src/event-handler.ts ================================================ import BScroll from '@better-scroll/core' import { TouchEvent, EventRegister, EventEmitter, maybePrevent, } from '@better-scroll/shared-utils' import Indicator from './indicator' interface EventHandlerOptions { disableMouse: boolean disableTouch: boolean } export default class EventHandler { startEventRegister: EventRegister moveEventRegister: EventRegister endEventRegister: EventRegister initiated: boolean lastPoint: number scroll: BScroll hooks: EventEmitter constructor( public indicator: Indicator, public options: EventHandlerOptions ) { this.hooks = new EventEmitter(['touchStart', 'touchMove', 'touchEnd']) this.registerEvents() } private registerEvents() { const { disableMouse, disableTouch } = this.options const startEvents = [] const moveEvents = [] const endEvents = [] if (!disableMouse) { startEvents.push({ name: 'mousedown', handler: this.start.bind(this), }) moveEvents.push({ name: 'mousemove', handler: this.move.bind(this), }) endEvents.push({ name: 'mouseup', handler: this.end.bind(this), }) } if (!disableTouch) { startEvents.push({ name: 'touchstart', handler: this.start.bind(this), }) moveEvents.push({ name: 'touchmove', handler: this.move.bind(this), }) endEvents.push( { name: 'touchend', handler: this.end.bind(this), }, { name: 'touchcancel', handler: this.end.bind(this), } ) } this.startEventRegister = new EventRegister( this.indicator.indicatorEl, startEvents ) this.moveEventRegister = new EventRegister(window, moveEvents) this.endEventRegister = new EventRegister(window, endEvents) } private BScrollIsDisabled() { return !this.indicator.scroll.enabled } private start(e: TouchEvent) { if (this.BScrollIsDisabled()) { return } let point = (e.touches ? e.touches[0] : e) as Touch maybePrevent(e) e.stopPropagation() this.initiated = true this.lastPoint = point[this.indicator.keysMap.point] this.hooks.trigger(this.hooks.eventTypes.touchStart) } private move(e: TouchEvent) { if (!this.initiated) { return } let point = (e.touches ? e.touches[0] : e) as Touch const pointPos = point[this.indicator.keysMap.point] maybePrevent(e) e.stopPropagation() let delta = pointPos - this.lastPoint this.lastPoint = pointPos this.hooks.trigger(this.hooks.eventTypes.touchMove, delta) } private end(e: TouchEvent) { if (!this.initiated) { return } this.initiated = false maybePrevent(e) e.stopPropagation() this.hooks.trigger(this.hooks.eventTypes.touchEnd) } destroy() { this.startEventRegister.destroy() this.moveEventRegister.destroy() this.endEventRegister.destroy() } } ================================================ FILE: packages/scroll-bar/src/index.ts ================================================ import BScroll from '@better-scroll/core' import Indicator, { IndicatorOptions, IndicatorDirection, OffsetType, } from './indicator' import { extend } from '@better-scroll/shared-utils' export type ScrollbarOptions = Partial | true export interface ScrollbarConfig { fade: boolean fadeInTime: number fadeOutTime: number interactive: boolean customElements: HTMLElement[] minSize: number scrollbarTrackClickable: boolean scrollbarTrackOffsetType: OffsetType scrollbarTrackOffsetTime: number } declare module '@better-scroll/core' { interface CustomOptions { scrollbar?: ScrollbarOptions } } export default class ScrollBar { static pluginName = 'scrollbar' options: ScrollbarConfig indicators: Indicator[] constructor(public scroll: BScroll) { this.handleOptions() this.createIndicators() this.handleHooks() } private handleHooks() { const scroll = this.scroll scroll.hooks.on(scroll.hooks.eventTypes.destroy, () => { for (let indicator of this.indicators) { indicator.destroy() } }) } private handleOptions() { const userOptions = (this.scroll.options.scrollbar === true ? {} : this.scroll.options.scrollbar) as Partial const defaultOptions: ScrollbarConfig = { fade: true, fadeInTime: 250, fadeOutTime: 500, interactive: false, customElements: [], minSize: 8, scrollbarTrackClickable: false, scrollbarTrackOffsetType: OffsetType.Step, scrollbarTrackOffsetTime: 300, } this.options = extend(defaultOptions, userOptions) } private createIndicators() { let indicatorOptions: IndicatorOptions const scroll: BScroll = this.scroll const indicators: Indicator[] = [] const scrollDirectionConfigKeys = ['scrollX', 'scrollY'] const indicatorDirections = [ IndicatorDirection.Horizontal, IndicatorDirection.Vertical, ] const customScrollbarEls = this.options.customElements for (let i = 0; i < scrollDirectionConfigKeys.length; i++) { const key = scrollDirectionConfigKeys[i] // wanna scroll in specified direction if (scroll.options[key]) { const customElement = customScrollbarEls.shift() const direction = indicatorDirections[i] let isCustom = false let scrollbarWrapper = customElement ? customElement : this.createScrollbarElement(direction) // internal scrollbar if (scrollbarWrapper !== customElement) { scroll.wrapper.appendChild(scrollbarWrapper) } else { // custom scrollbar passed by users isCustom = true } indicatorOptions = { wrapper: scrollbarWrapper, direction, ...this.options, isCustom, } indicators.push(new Indicator(scroll, indicatorOptions)) } } this.indicators = indicators } private createScrollbarElement( direction: IndicatorDirection, scrollbarTrackClickable = this.options.scrollbarTrackClickable ) { let scrollbarWrapperEl: HTMLDivElement = document.createElement('div') let scrollbarIndicatorEl: HTMLDivElement = document.createElement('div') scrollbarWrapperEl.style.cssText = 'position:absolute;z-index:9999;overflow:hidden;' scrollbarIndicatorEl.style.cssText = 'box-sizing:border-box;position:absolute;background:rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.9);border-radius:3px;' scrollbarIndicatorEl.className = 'bscroll-indicator' if (direction === IndicatorDirection.Horizontal) { scrollbarWrapperEl.style.cssText += 'height:7px;left:2px;right:2px;bottom:0;' scrollbarIndicatorEl.style.height = '100%' scrollbarWrapperEl.className = 'bscroll-horizontal-scrollbar' } else { scrollbarWrapperEl.style.cssText += 'width:7px;bottom:2px;top:2px;right:1px;' scrollbarIndicatorEl.style.width = '100%' scrollbarWrapperEl.className = 'bscroll-vertical-scrollbar' } if (!scrollbarTrackClickable) { scrollbarWrapperEl.style.cssText += 'pointer-events:none;' } scrollbarWrapperEl.appendChild(scrollbarIndicatorEl) return scrollbarWrapperEl } } ================================================ FILE: packages/scroll-bar/src/indicator.ts ================================================ import BScroll, { TranslaterPoint } from '@better-scroll/core' import { style, EventEmitter, between, getNow, Probe, EventRegister, } from '@better-scroll/shared-utils' import EventHandler from './event-handler' export const enum IndicatorDirection { Horizontal = 'horizontal', Vertical = 'vertical', } const enum ScrollTo { Up = -1, Down = 1, } export const enum OffsetType { Step = 'step', Point = 'clickedPoint', } export interface IndicatorOptions { wrapper: HTMLElement direction: IndicatorDirection fade: boolean fadeInTime: number fadeOutTime: number interactive: boolean minSize: number isCustom: boolean scrollbarTrackClickable: boolean scrollbarTrackOffsetType: OffsetType scrollbarTrackOffsetTime: number } interface KeysMap { hasScroll: 'hasVerticalScroll' | 'hasHorizontalScroll' size: 'height' | 'width' wrapperSize: 'clientHeight' | 'clientWidth' scrollerSize: 'scrollerHeight' | 'scrollerWidth' maxScrollPos: 'maxScrollY' | 'maxScrollX' pos: 'y' | 'x' point: 'pageX' | 'pageY' translateProperty: 'translateY' | 'translateX' domRect: 'top' | 'left' } interface ScrollInfo { maxScrollPos: number minScrollPos: number sizeRatio: number baseSize: number } export default class Indicator { wrapper: HTMLElement wrapperRect: DOMRect indicatorEl: HTMLElement direction: IndicatorDirection scrollInfo: ScrollInfo currentPos: number moved: boolean startTime: number keysMap: KeysMap eventHandler: EventHandler clickEventRegister: EventRegister hooksFn: [EventEmitter, string, Function][] = [] constructor(public scroll: BScroll, public options: IndicatorOptions) { this.wrapper = options.wrapper this.direction = options.direction this.indicatorEl = this.wrapper.children[0] as HTMLElement this.keysMap = this.getKeysMap() this.handleFade() this.handleHooks() } private handleFade() { if (this.options.fade) { this.wrapper.style.opacity = '0' } } private handleHooks() { const { fade, interactive, scrollbarTrackClickable } = this.options const scroll = this.scroll const scrollHooks = scroll.hooks const translaterHooks = scroll.scroller.translater.hooks const animaterHooks = scroll.scroller.animater.hooks this.registerHooks( scrollHooks, scrollHooks.eventTypes.refresh, this.refresh ) this.registerHooks( translaterHooks, translaterHooks.eventTypes.translate, (pos: { x: number; y: number }) => { const { hasScroll: hasScrollKey } = this.keysMap if (this.scroll[hasScrollKey]) { this.updatePosition(pos) } } ) this.registerHooks( animaterHooks, animaterHooks.eventTypes.time, this.transitionTime ) this.registerHooks( animaterHooks, animaterHooks.eventTypes.timeFunction, this.transitionTimingFunction ) if (fade) { this.registerHooks(scroll, scroll.eventTypes.scrollEnd, () => { this.fade() }) this.registerHooks(scroll, scroll.eventTypes.scrollStart, () => { this.fade(true) }) // for mousewheel event if ( scroll.eventTypes.mousewheelStart && scroll.eventTypes.mousewheelEnd ) { this.registerHooks(scroll, scroll.eventTypes.mousewheelStart, () => { this.fade(true) }) this.registerHooks(scroll, scroll.eventTypes.mousewheelMove, () => { this.fade(true) }) this.registerHooks(scroll, scroll.eventTypes.mousewheelEnd, () => { this.fade() }) } } if (interactive) { const { disableMouse, disableTouch } = this.scroll.options this.eventHandler = new EventHandler(this, { disableMouse, disableTouch, }) const eventHandlerHooks = this.eventHandler.hooks this.registerHooks( eventHandlerHooks, eventHandlerHooks.eventTypes.touchStart, this.startHandler ) this.registerHooks( eventHandlerHooks, eventHandlerHooks.eventTypes.touchMove, this.moveHandler ) this.registerHooks( eventHandlerHooks, eventHandlerHooks.eventTypes.touchEnd, this.endHandler ) } if (scrollbarTrackClickable) { this.bindClick() } } private registerHooks(hooks: EventEmitter, name: string, handler: Function) { hooks.on(name, handler, this) this.hooksFn.push([hooks, name, handler]) } private bindClick() { const wrapper = this.wrapper this.clickEventRegister = new EventRegister(wrapper, [ { name: 'click', handler: this.handleClick.bind(this), }, ]) } private handleClick(e: MouseEvent) { const newPos = this.calculateclickOffsetPos(e) let { x, y } = this.scroll x = this.direction === IndicatorDirection.Horizontal ? newPos : x y = this.direction === IndicatorDirection.Vertical ? newPos : y this.scroll.scrollTo(x, y, this.options.scrollbarTrackOffsetTime) } private calculateclickOffsetPos(e: MouseEvent) { const { point: poinKey, domRect: domRectKey } = this.keysMap const { scrollbarTrackOffsetType } = this.options const clickPointOffset = e[poinKey] - this.wrapperRect[domRectKey] const scrollToWhere = clickPointOffset < this.currentPos ? ScrollTo.Up : ScrollTo.Down let delta = 0 let currentPos = this.currentPos if (scrollbarTrackOffsetType === OffsetType.Step) { delta = this.scrollInfo.baseSize * scrollToWhere } else { delta = 0 currentPos = clickPointOffset } return this.newPos(currentPos, delta, this.scrollInfo) } getKeysMap(): KeysMap { if (this.direction === IndicatorDirection.Vertical) { return { hasScroll: 'hasVerticalScroll', size: 'height', wrapperSize: 'clientHeight', scrollerSize: 'scrollerHeight', maxScrollPos: 'maxScrollY', pos: 'y', point: 'pageY', translateProperty: 'translateY', domRect: 'top', } } return { hasScroll: 'hasHorizontalScroll', size: 'width', wrapperSize: 'clientWidth', scrollerSize: 'scrollerWidth', maxScrollPos: 'maxScrollX', pos: 'x', point: 'pageX', translateProperty: 'translateX', domRect: 'left', } } fade(visible?: boolean) { const { fadeInTime, fadeOutTime } = this.options const time = visible ? fadeInTime : fadeOutTime const wrapper = this.wrapper wrapper.style[style.transitionDuration as any] = time + 'ms' wrapper.style.opacity = visible ? '1' : '0' } refresh() { const { hasScroll: hasScrollKey } = this.keysMap const scroll = this.scroll const { x, y } = scroll this.wrapperRect = this.wrapper.getBoundingClientRect() if (this.canScroll(scroll[hasScrollKey])) { let { wrapperSize: wrapperSizeKey, scrollerSize: scrollerSizeKey, maxScrollPos: maxScrollPosKey, } = this.keysMap this.scrollInfo = this.refreshScrollInfo( this.wrapper[wrapperSizeKey], scroll[scrollerSizeKey], scroll[maxScrollPosKey], this.indicatorEl[wrapperSizeKey] ) this.updatePosition({ x, y, }) } } transitionTime(time: number = 0) { this.indicatorEl.style[style.transitionDuration as any] = time + 'ms' } transitionTimingFunction(easing: string) { this.indicatorEl.style[style.transitionTimingFunction as any] = easing } private canScroll(hasScroll: boolean): boolean { this.wrapper.style.display = hasScroll ? 'block' : 'none' return hasScroll } private refreshScrollInfo( wrapperSize: number, scrollerSize: number, maxScrollPos: number, indicatorElSize: number ): ScrollInfo { let baseSize = Math.max( Math.round( (wrapperSize * wrapperSize) / (scrollerSize || wrapperSize || 1) ), this.options.minSize ) if (this.options.isCustom) { baseSize = indicatorElSize } const maxIndicatorScrollPos = wrapperSize - baseSize // sizeRatio is negative let sizeRatio = maxIndicatorScrollPos / maxScrollPos return { baseSize, maxScrollPos: maxIndicatorScrollPos, minScrollPos: 0, sizeRatio, } } updatePosition(point: TranslaterPoint) { const { pos, size } = this.caculatePosAndSize(point, this.scrollInfo) this.refreshStyle(size, pos) this.currentPos = pos } private caculatePosAndSize( point: TranslaterPoint, scrollInfo: ScrollInfo ): { pos: number; size: number } { const { pos: posKey } = this.keysMap const { sizeRatio, baseSize, maxScrollPos, minScrollPos } = scrollInfo const minSize = this.options.minSize let pos = Math.round(sizeRatio * point[posKey]) let size // when out of boundary, slow down size reduction if (pos < minScrollPos) { size = Math.max(baseSize + pos * 3, minSize) pos = minScrollPos } else if (pos > maxScrollPos) { size = Math.max(baseSize - (pos - maxScrollPos) * 3, minSize) pos = maxScrollPos + baseSize - size } else { size = baseSize } return { pos, size, } } private refreshStyle(size: number, pos: number) { const { translateProperty: translatePropertyKey, size: sizeKey, } = this.keysMap const translateZ = this.scroll.options.translateZ this.indicatorEl.style[sizeKey] = `${size}px` this.indicatorEl.style[ style.transform as any ] = `${translatePropertyKey}(${pos}px)${translateZ}` } startHandler() { this.moved = false this.startTime = getNow() this.transitionTime() this.scroll.scroller.hooks.trigger( this.scroll.scroller.hooks.eventTypes.beforeScrollStart ) } moveHandler(delta: number) { if (!this.moved && !this.indicatorNotMoved(delta)) { this.moved = true this.scroll.scroller.hooks.trigger( this.scroll.scroller.hooks.eventTypes.scrollStart ) } if (this.moved) { const newPos = this.newPos(this.currentPos, delta, this.scrollInfo) this.syncBScroll(newPos) } } endHandler() { if (this.moved) { const { x, y } = this.scroll this.scroll.scroller.hooks.trigger( this.scroll.scroller.hooks.eventTypes.scrollEnd, { x, y, } ) } } private indicatorNotMoved(delta: number): boolean { const currentPos = this.currentPos const { maxScrollPos, minScrollPos } = this.scrollInfo const notMoved = (currentPos === minScrollPos && delta <= 0) || (currentPos === maxScrollPos && delta >= 0) return notMoved } private syncBScroll(newPos: number) { const timestamp = getNow() const { x, y, options, scroller, maxScrollY, minScrollY, maxScrollX, minScrollX, } = this.scroll const { probeType, momentumLimitTime } = options const position = { x, y } if (this.direction === IndicatorDirection.Vertical) { position.y = between(newPos, maxScrollY, minScrollY) } else { position.x = between(newPos, maxScrollX, minScrollX) } scroller.translater.translate(position) // dispatch scroll in interval time if (timestamp - this.startTime > momentumLimitTime) { this.startTime = timestamp if (probeType === Probe.Throttle) { scroller.hooks.trigger(scroller.hooks.eventTypes.scroll, position) } } // dispatch scroll all the time if (probeType > Probe.Throttle) { scroller.hooks.trigger(scroller.hooks.eventTypes.scroll, position) } } private newPos( currentPos: number, delta: number, scrollInfo: ScrollInfo ): number { const { maxScrollPos, sizeRatio, minScrollPos } = scrollInfo let newPos = currentPos + delta newPos = between(newPos, minScrollPos, maxScrollPos) return Math.round(newPos / sizeRatio) } destroy() { const { interactive, scrollbarTrackClickable, isCustom } = this.options if (interactive) { this.eventHandler.destroy() } if (scrollbarTrackClickable) { this.clickEventRegister.destroy() } if (!isCustom) { this.wrapper.parentNode!.removeChild(this.wrapper) } this.hooksFn.forEach((item) => { const hooks = item[0] const hooksName = item[1] const handlerFn = item[2] hooks.off(hooksName, handlerFn) }) this.hooksFn.length = 0 } } ================================================ FILE: packages/shared-utils/README.md ================================================ # @better-scroll/shared-utils shared-utils for BetterScroll. ================================================ FILE: packages/shared-utils/package.json ================================================ { "name": "@better-scroll/shared-utils", "version": "2.5.1", "description": "shared-utils for BetterScroll", "author": { "name": "jizhi", "email": "theniceangel@163.com" }, "main": "dist/shared-utils.min.js", "module": "dist/shared-utils.esm.js", "typings": "dist/types/index.d.ts", "publishConfig": { "access": "public" }, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios" ], "repository": { "type": "git", "url": "git+ssh://git@github.com/ustbhuangyi/better-scroll.git", "directory": "packages/shared-utils" }, "gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130" } ================================================ FILE: packages/shared-utils/src/Touch.ts ================================================ interface TouchList { length: number [index: number]: Touch item: (index: number) => Touch } interface Touch { identifier: number target: EventTarget screenX: number screenY: number clientX: number clientY: number pageX: number pageY: number } export interface TouchEvent extends UIEvent { touches: TouchList targetTouches: TouchList changedTouches: TouchList altKey: boolean metaKey: boolean ctrlKey: boolean shiftKey: boolean rotation: number scale: number button: number _constructed?: boolean } ================================================ FILE: packages/shared-utils/src/__mocks__/dom.ts ================================================ export const style = { transform: 'transform', transition: 'transition', transitionTimingFunction: 'transitionTimingFunction', transitionDuration: 'transitionDuration', transitionDelay: 'transitionDelay', transformOrigin: 'transformOrigin', transitionEnd: 'transitionEnd' } ================================================ FILE: packages/shared-utils/src/__mocks__/ease.ts ================================================ export const ease = { // easeOutQuint swipe: { style: 'cubic-bezier(0.23, 1, 0.32, 1)', fn: () => {} }, // easeOutQuard swipeBounce: { style: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', fn: () => {} }, // easeOutQuart bounce: { style: 'cubic-bezier(0.165, 0.84, 0.44, 1)', fn: () => {} } } ================================================ FILE: packages/shared-utils/src/__tests__/debug.spec.ts ================================================ import { warn, assert } from '../debug' describe('debug', () => { it('should work well when call warn()', () => { const spyFn = jest.spyOn(console, 'error') warn('Error occured') expect(spyFn).toBeCalledWith('[BScroll warn]: Error occured') }) it('should work well when call assert()', () => { const a = 1 + Math.random() const b = 2 expect(() => { assert(a > b, '') }).toThrow() }) }) ================================================ FILE: packages/shared-utils/src/__tests__/dom.spec.ts ================================================ import { prepend, removeChild, addClass, removeClass, tap, dblclick, click, } from '../dom' describe('dom', () => { it('prepend', () => { // append operation const target1 = document.createElement('div') const el1 = document.createElement('p') prepend(el1, target1) expect(target1.children[0]).toBe(el1) // prepend operation const target2 = document.createElement('div') const child = document.createElement('div') target2.appendChild(child) const el2 = document.createElement('p') prepend(el2, target2) expect(target2.children[0]).toBe(el2) expect(target2.children[1]).toBe(child) }) it('removeChild', () => { // append operation const target = document.createElement('div') const el = document.createElement('p') prepend(el, target) expect(target.children[0]).toBe(el) removeChild(target, el) expect(target.children.length).toBe(0) }) it('addClass & removeClass', () => { const target = document.createElement('div') addClass(target, 'test') expect(target.className).toBe(' test') // same classname addClass(target, 'test') addClass(target, 'test2') expect(target.className).toBe(' test test2') // exclude unexisted classname removeClass(target, 'biz') expect(target.className).toBe(' test test2') removeClass(target, 'test test2') expect(target.className).toBe(' ') }) it('tap & dblclick', () => { const mockFn1 = jest.fn() const mockFn2 = jest.fn() const target = document.createElement('div') document.body.appendChild(target) let e = { target } as any window.addEventListener('tap', mockFn1) window.addEventListener('dblclick', mockFn2) tap(e, 'tap') expect(mockFn1).toBeCalled() dblclick(e) expect(mockFn2).toBeCalled() }) it('click ', () => { const mockFn1 = jest.fn() const target = document.createElement('div') document.body.appendChild(target) let e = { target, type: 'mouseup' } as any window.addEventListener('click', mockFn1) click(e) expect(mockFn1).toBeCalled() // fallback to createEvent Object.defineProperty(window, 'MouseEvent', { get() { return undefined }, }) click(Object.assign(e, { type: 'touchend', changedTouches: [{}] })) expect(mockFn1).toBeCalledTimes(2) }) }) ================================================ FILE: packages/shared-utils/src/__tests__/ease.spec.ts ================================================ import { ease } from '../ease' describe('ease', () => { it('swipe fn', () => { const handler = ease.swipe.fn const time1 = handler(0.2) const time2 = handler(0.4) const time3 = handler(0.5) const time4 = handler(0.6) const time5 = handler(0.8) const time6 = handler(1) expect(time1).toBeCloseTo(0.6723) expect(time2).toBeCloseTo(0.92224) expect(time3).toBeCloseTo(0.96875) expect(time4).toBeCloseTo(0.98976) expect(time5).toBeCloseTo(0.99968) expect(time6).toBeCloseTo(1) }) it('swipeBounce fn', () => { const handler = ease.swipeBounce.fn const time1 = handler(0.2) const time2 = handler(0.4) const time3 = handler(0.5) const time4 = handler(0.6) const time5 = handler(0.8) const time6 = handler(1) expect(time1).toBeCloseTo(0.36) expect(time2).toBeCloseTo(0.64) expect(time3).toBeCloseTo(0.75) expect(time4).toBeCloseTo(0.84) expect(time5).toBeCloseTo(0.96) expect(time6).toBeCloseTo(1) }) }) ================================================ FILE: packages/shared-utils/src/__tests__/events.spec.ts ================================================ import { EventEmitter, EventRegister } from '../events' describe('events', () => { describe('EventEmitter', () => { let eventEmitter: EventEmitter beforeEach(() => { eventEmitter = new EventEmitter(['test1']) }) afterEach(() => { jest.clearAllMocks() }) it('should register handler successfully', () => { eventEmitter.on('test1', () => {}) expect(eventEmitter.eventTypes.test1).toBeTruthy() expect(eventEmitter.events.test1).not.toBeUndefined() }) it('should trigger handler', () => { let mockHandler = jest.fn((x) => x + 1) eventEmitter.on('test1', mockHandler) eventEmitter.trigger('test1', 1) expect(mockHandler.mock.calls.length).toBe(1) expect(mockHandler.mock.calls[0][0]).toBe(1) expect(mockHandler.mock.results[0].value).toBe(2) }) it('should trigger handler only once', () => { let mockHandler = jest.fn((x) => x + 1) eventEmitter.once('test1', mockHandler) eventEmitter.trigger('test1', 1) eventEmitter.trigger('test1', 1) expect(mockHandler.mock.calls.length).toBe(1) }) it('should tear down handler when invoking off()', () => { let mockHandler = jest.fn((x) => x + 1) eventEmitter.once('test1', mockHandler) eventEmitter.off('test1', mockHandler) expect(eventEmitter.events.test1.length).toBe(0) }) it('should register eventTypes when invoking registerType()', () => { eventEmitter.registerType(['test2']) expect(eventEmitter.eventTypes.test2).toBe('test2') }) it('should warn about unregistered event when invoking off()', () => { const spyFn = jest.spyOn(console, 'error') eventEmitter.off('test2') expect(spyFn).toBeCalled() }) it('should keep chainable call when invoking off()', () => { const ret = eventEmitter.off('test1', () => {}) const ret2 = eventEmitter.off() expect(ret).toBe(eventEmitter) expect(ret2).toBe(eventEmitter) }) it('should support cancelable callback', () => { const mockHandler1 = jest.fn().mockImplementation(() => true) const mockHandler2 = jest.fn() eventEmitter.on('test1', mockHandler1) eventEmitter.on('test1', mockHandler2) const ret = eventEmitter.trigger('test1') expect(mockHandler1).toBeCalled() expect(mockHandler2).not.toBeCalled() expect(ret).toBe(true) }) it('should support cancelable once callback', () => { const mockHandler1 = jest.fn() const mockHandler2 = jest.fn().mockImplementation(() => true) const mockHandler3 = jest.fn() eventEmitter.on('test1', mockHandler1) eventEmitter.once('test1', mockHandler2) eventEmitter.on('test1', mockHandler3) const ret = eventEmitter.trigger('test1') expect(mockHandler1).toBeCalled() expect(mockHandler2).toBeCalled() expect(mockHandler3).not.toBeCalled() expect(ret).toBe(true) }) }) describe('EventRegister', () => { let eventRegister: EventRegister let fakeNode: HTMLElement let mockHandler: any beforeEach(() => { fakeNode = document.createElement('div') mockHandler = jest.fn(() => 'it is a test handler ') eventRegister = new EventRegister(fakeNode, [ { name: 'test', handler: mockHandler as any, }, ]) }) afterEach(() => { jest.clearAllMocks() }) it('should trigger handler when dispatch touch event', () => { const evt = document.createEvent('Event') const evtType = 'test' evt.initEvent(evtType, false, false) fakeNode.dispatchEvent(evt) expect(mockHandler.mock.calls.length).toBe(1) }) it('should remove dom events when destroy', () => { eventRegister.destroy() const evt = document.createEvent('Event') const evtType = 'test' evt.initEvent(evtType, false, false) fakeNode.dispatchEvent(evt) expect(mockHandler.mock.calls.length).toBe(0) }) }) }) ================================================ FILE: packages/shared-utils/src/__tests__/lang.spec.ts ================================================ import { findIndex } from '../lang' describe('lang', () => { it('findIndex', () => { // hide ES6 findIndex // @ts-ignore Array.prototype.findIndex = undefined const array = [1, 2] const ret = findIndex(array, (item) => item % 2 === 0) expect(ret).toBe(1) }) }) ================================================ FILE: packages/shared-utils/src/__tests__/propertiesProxy.spec.ts ================================================ import { propertiesProxy } from '../propertiesProxy' describe('propertiesProxy', () => { it('should proxy property correctly', () => { let obj = { a: { b: { c: 1, }, }, } as any propertiesProxy(obj, 'a.b.c', 'c') expect(obj.c).toBe(1) obj.c = 3 expect(obj.a.b.c).toBe(3) }) it('should prevent error when string path is wrong', () => { let obj = { a: { b: { d: 1, }, }, } as any propertiesProxy(obj, 'a.c.d', 'c') expect(obj.c).toBeFalsy() obj.c = 2 expect(obj.a.c.d).toBe(2) }) it('should change context when proxying method', () => { let obj = { a: { b: { c() { return (this as any).d }, d: 1, }, }, } as any propertiesProxy(obj, 'a.b.c', 'c') expect(obj.c()).toBe(1) }) }) ================================================ FILE: packages/shared-utils/src/__tests__/raf.spec.ts ================================================ import { requestAnimationFrame, cancelAnimationFrame } from '../raf' jest.mock('../env', () => { const windowCompat = window as any windowCompat.requestAnimationFrame = null windowCompat.webkitRequestAnimationFrame = null windowCompat.mozRequestAnimationFrame = null windowCompat.oRequestAnimationFrame = null windowCompat.cancelAnimationFrame = null windowCompat.webkitCancelAnimationFrame = null windowCompat.mozCancelAnimationFrame = null windowCompat.oCancelAnimationFrame = null return { inBrowser: true, } }) describe('raf', () => { it('should fallback setTimeout or clearTimeout', () => { jest.useFakeTimers() const spySetTimeout = jest.spyOn(window, 'setTimeout') const spyClearTimeout = jest.spyOn(window, 'clearTimeout') const mockFn = jest.fn() const timer = requestAnimationFrame(mockFn) jest.advanceTimersByTime(17) expect(mockFn).toBeCalled() expect(spySetTimeout).toBeCalled() cancelAnimationFrame(timer) expect(spyClearTimeout).toBeCalled() }) }) ================================================ FILE: packages/shared-utils/src/debug.ts ================================================ export function warn(msg: string) { console.error(`[BScroll warn]: ${msg}`) } export function assert(condition: string | boolean, msg: string) { if (!condition) { throw new Error('[BScroll] ' + msg) } } ================================================ FILE: packages/shared-utils/src/dom.ts ================================================ import { inBrowser, isWeChatDevTools, supportsPassive } from './env' import { extend } from './lang' export type safeCSSStyleDeclaration = { [key: string]: string } & CSSStyleDeclaration export interface DOMRect { left: number top: number width: number height: number [key: string]: number } let elementStyle = (inBrowser && document.createElement('div').style) as safeCSSStyleDeclaration let vendor = (() => { /* istanbul ignore if */ if (!inBrowser) { return false } const transformNames = [ { key: 'standard', value: 'transform', }, { key: 'webkit', value: 'webkitTransform', }, { key: 'Moz', value: 'MozTransform', }, { key: 'O', value: 'OTransform', }, { key: 'ms', value: 'msTransform', }, ] for (let obj of transformNames) { if (elementStyle[obj.value] !== undefined) { return obj.key } } /* istanbul ignore next */ return false })() /* istanbul ignore next */ function prefixStyle(style: string): string { if (vendor === false) { return style } if (vendor === 'standard') { if (style === 'transitionEnd') { return 'transitionend' } return style } return vendor + style.charAt(0).toUpperCase() + style.substr(1) } export function getElement(el: HTMLElement | string) { return ( typeof el === 'string' ? document.querySelector(el) : el ) as HTMLElement } export function addEvent( el: HTMLElement, type: string, fn: EventListenerOrEventListenerObject, capture?: AddEventListenerOptions ) { const useCapture = supportsPassive ? { passive: false, capture: !!capture, } : !!capture el.addEventListener(type, fn, useCapture) } export function removeEvent( el: HTMLElement, type: string, fn: EventListenerOrEventListenerObject, capture?: EventListenerOptions ) { el.removeEventListener(type, fn, { capture: !!capture, }) } export function maybePrevent(e: Event) { if (e.cancelable) { e.preventDefault() } } export function offset(el: HTMLElement | null) { let left = 0 let top = 0 while (el) { left -= el.offsetLeft top -= el.offsetTop el = el.offsetParent as HTMLElement } return { left, top, } } export function offsetToBody(el: HTMLElement) { let rect = el.getBoundingClientRect() return { left: -(rect.left + window.pageXOffset), top: -(rect.top + window.pageYOffset), } } export const cssVendor = vendor && vendor !== 'standard' ? '-' + vendor.toLowerCase() + '-' : '' let transform = prefixStyle('transform') let transition = prefixStyle('transition') export const hasPerspective = inBrowser && prefixStyle('perspective') in elementStyle // fix issue #361 export const hasTouch = inBrowser && ('ontouchstart' in window || isWeChatDevTools) export const hasTransition = inBrowser && transition in elementStyle export const style = { transform, transition, transitionTimingFunction: prefixStyle('transitionTimingFunction'), transitionDuration: prefixStyle('transitionDuration'), transitionDelay: prefixStyle('transitionDelay'), transformOrigin: prefixStyle('transformOrigin'), transitionEnd: prefixStyle('transitionEnd'), transitionProperty: prefixStyle('transitionProperty'), } export const eventTypeMap: { [key: string]: number touchstart: number touchmove: number touchend: number touchcancel: number mousedown: number mousemove: number mouseup: number } = { touchstart: 1, touchmove: 1, touchend: 1, touchcancel: 1, mousedown: 2, mousemove: 2, mouseup: 2, } export function getRect(el: HTMLElement): DOMRect { /* istanbul ignore if */ if (el instanceof (window as any).SVGElement) { let rect = el.getBoundingClientRect() return { top: rect.top, left: rect.left, width: rect.width, height: rect.height, } } else { return { top: el.offsetTop, left: el.offsetLeft, width: el.offsetWidth, height: el.offsetHeight, } } } export function preventDefaultExceptionFn( el: any, exceptions: { tagName?: RegExp className?: RegExp [key: string]: any } ) { for (let i in exceptions) { if (exceptions[i].test(el[i])) { return true } } return false } export const tagExceptionFn = preventDefaultExceptionFn export function tap(e: any, eventName: string) { let ev = document.createEvent('Event') as any ev.initEvent(eventName, true, true) ev.pageX = e.pageX ev.pageY = e.pageY e.target.dispatchEvent(ev) } export function click(e: any, event = 'click') { let eventSource if (e.type === 'mouseup') { eventSource = e } else if (e.type === 'touchend' || e.type === 'touchcancel') { eventSource = e.changedTouches[0] } let posSrc: { screenX?: number screenY?: number clientX?: number clientY?: number } = {} if (eventSource) { posSrc.screenX = eventSource.screenX || 0 posSrc.screenY = eventSource.screenY || 0 posSrc.clientX = eventSource.clientX || 0 posSrc.clientY = eventSource.clientY || 0 } let ev: any const bubbles = true const cancelable = true const { ctrlKey, shiftKey, altKey, metaKey } = e const pressedKeysMap = { ctrlKey, shiftKey, altKey, metaKey, } if (typeof MouseEvent !== 'undefined') { try { ev = new MouseEvent( event, extend( { bubbles, cancelable, ...pressedKeysMap, }, posSrc ) ) } catch (e) { /* istanbul ignore next */ createEvent() } } else { createEvent() } function createEvent() { ev = document.createEvent('Event') ev.initEvent(event, bubbles, cancelable) extend(ev, posSrc) } // forwardedTouchEvent set to true in case of the conflict with fastclick ev.forwardedTouchEvent = true ev._constructed = true e.target.dispatchEvent(ev) } export function dblclick(e: Event) { click(e, 'dblclick') } export function prepend(el: HTMLElement, target: HTMLElement) { const firstChild = target.firstChild as HTMLElement if (firstChild) { before(el, firstChild) } else { target.appendChild(el) } } export function before(el: HTMLElement, target: HTMLElement) { const parentNode = target.parentNode as HTMLElement parentNode.insertBefore(el, target) } export function removeChild(el: HTMLElement, child: HTMLElement) { el.removeChild(child) } export function hasClass(el: HTMLElement, className: string) { let reg = new RegExp('(^|\\s)' + className + '(\\s|$)') return reg.test(el.className) } export function addClass(el: HTMLElement, className: string) { if (hasClass(el, className)) { return } let newClass = el.className.split(' ') newClass.push(className) el.className = newClass.join(' ') } export function removeClass(el: HTMLElement, className: string) { if (!hasClass(el, className)) { return } let reg = new RegExp('(^|\\s)' + className + '(\\s|$)', 'g') el.className = el.className.replace(reg, ' ') } export function HTMLCollectionToArray(el: HTMLCollection) { return Array.prototype.slice.call(el, 0) } export function getClientSize(el: HTMLElement) { return { width: el.clientWidth, height: el.clientHeight, } } ================================================ FILE: packages/shared-utils/src/ease.ts ================================================ export interface EaseItem { style: string fn: EaseFn } interface EaseMap { [key: string]: EaseItem } export interface EaseFn { (t: number): number } export const ease: EaseMap = { // easeOutQuint swipe: { style: 'cubic-bezier(0.23, 1, 0.32, 1)', fn: function(t: number) { return 1 + --t * t * t * t * t } }, // easeOutQuard swipeBounce: { style: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)', fn: function(t: number) { return t * (2 - t) } }, // easeOutQuart bounce: { style: 'cubic-bezier(0.165, 0.84, 0.44, 1)', fn: function(t: number) { return 1 - --t * t * t * t } } } ================================================ FILE: packages/shared-utils/src/enums.ts ================================================ export const enum DirectionLock { Default = '', Horizontal = 'horizontal', Vertical = 'vertical', None = 'none', } export const enum Direction { // fingers move from bottom to top or right to left Positive = 1, // on the contrary as above Negative = -1, Default = 0, } export const enum ApplyOrder { Pre = 'pre', Post = 'post', } export const enum EventPassthrough { None = '', Horizontal = 'horizontal', Vertical = 'vertical', } export const enum EventType { Touch = 1, Mouse = 2, } export const enum MouseButton { Left, Middle, Right, } export const enum Probe { Default, Throttle, Normal, Realtime, } export const enum Quadrant { First = 1, Second, Third, Forth, } ================================================ FILE: packages/shared-utils/src/env.ts ================================================ // ssr support export const inBrowser = typeof window !== 'undefined' export const ua = inBrowser && navigator.userAgent.toLowerCase() export const isWeChatDevTools = !!(ua && /wechatdevtools/.test(ua)) export const isAndroid = ua && ua.indexOf('android') > 0 /* istanbul ignore next */ export const isIOSBadVersion: boolean = (() => { if (typeof ua === 'string') { const regex = /os (\d\d?_\d(_\d)?)/ const matches = regex.exec(ua) if (!matches) return false const parts = matches[1].split('_').map(function (item) { return parseInt(item, 10) }) // ios version >= 13.4 issue 982 return !!(parts[0] === 13 && parts[1] >= 4) } return false })() /* istanbul ignore next */ export let supportsPassive = false /* istanbul ignore next */ if (inBrowser) { const EventName = 'test-passive' as any try { const opts = {} Object.defineProperty(opts, 'passive', { get() { supportsPassive = true }, }) // https://github.com/facebook/flow/issues/285 window.addEventListener(EventName, () => {}, opts) } catch (e) {} } ================================================ FILE: packages/shared-utils/src/events.ts ================================================ import { warn } from './debug' import { addEvent, removeEvent } from './dom' interface Events { [name: string]: [WithFnFunction, Object][] } interface EventTypes { [type: string]: string } interface WithFnFunction extends Function { fn?: Function } export class EventEmitter { events: Events eventTypes: EventTypes constructor(names: string[]) { this.events = {} this.eventTypes = {} this.registerType(names) } on(type: string, fn: Function, context: Object = this) { this.hasType(type) if (!this.events[type]) { this.events[type] = [] } this.events[type].push([fn, context]) return this } once(type: string, fn: Function, context: Object = this) { this.hasType(type) const magic = (...args: any[]) => { this.off(type, magic) const ret = fn.apply(context, args) if (ret === true) { return ret } } magic.fn = fn this.on(type, magic) return this } off(type?: string, fn?: Function) { if (!type && !fn) { this.events = {} return this } if (type) { this.hasType(type) if (!fn) { this.events[type] = [] return this } let events = this.events[type] if (!events) { return this } let count = events.length while (count--) { if ( events[count][0] === fn || (events[count][0] && events[count][0].fn === fn) ) { events.splice(count, 1) } } return this } } trigger(type: string, ...args: any[]) { this.hasType(type) let events = this.events[type] if (!events) { return } let len = events.length let eventsCopy = [...events] let ret for (let i = 0; i < len; i++) { let event = eventsCopy[i] let [fn, context] = event if (fn) { ret = fn.apply(context, args) if (ret === true) { return ret } } } } registerType(names: string[]) { names.forEach((type: string) => { this.eventTypes[type] = type }) } destroy() { this.events = {} this.eventTypes = {} } private hasType(type: string) { const types = this.eventTypes const isType = types[type] === type if (!isType) { warn( `EventEmitter has used unknown event type: "${type}", should be oneof [` + `${Object.keys(types).map((_) => JSON.stringify(_))}` + `]` ) } } } interface EventData { name: string handler(e: UIEvent): void capture?: boolean } export class EventRegister { constructor( public wrapper: HTMLElement | Window, public events: EventData[] ) { this.addDOMEvents() } destroy() { this.removeDOMEvents() this.events = [] } private addDOMEvents() { this.handleDOMEvents(addEvent) } private removeDOMEvents() { this.handleDOMEvents(removeEvent) } private handleDOMEvents(eventOperation: Function) { const wrapper = this.wrapper this.events.forEach((event: EventData) => { eventOperation(wrapper, event.name, this, !!event.capture) }) } private handleEvent(e: UIEvent) { const eventType = e.type this.events.some((event: EventData) => { if (event.name === eventType) { event.handler(e) return true } return false }) } } ================================================ FILE: packages/shared-utils/src/index.ts ================================================ export * from './debug' export * from './dom' export * from './ease' export * from './env' export * from './lang' export * from './raf' export * from './Touch' export * from './propertiesProxy' export * from './enums' export * from './events' export * from './types' ================================================ FILE: packages/shared-utils/src/lang.ts ================================================ export function getNow() { return window.performance && window.performance.now && window.performance.timing ? window.performance.now() + window.performance.timing.navigationStart : +new Date() } export const extend = ( target: T, source: U ): T & U => { for (const key in source) { ;(target as any)[key] = source[key] } return target as T & U } export function isUndef(v: any): boolean { return v === undefined || v === null } export function getDistance(x: number, y: number) { return Math.sqrt(x * x + y * y) } export function between(x: number, min: number, max: number) { if (x < min) { return min } if (x > max) { return max } return x } export function findIndex( ary: T[], fn: (value: T, index: number, arr?: T[]) => boolean ) { if (ary.findIndex) { return ary.findIndex(fn) } let index = -1 ary.some(function (item, i, ary) { const ret = fn(item, i, ary) if (ret) { index = i return ret } }) return index } ================================================ FILE: packages/shared-utils/src/propertiesProxy.ts ================================================ /* istanbul ignore next */ const noop = function (val?: any) {} interface TraversedObject { [key: string]: any } const sharedPropertyDefinition: PropertyDescriptor = { enumerable: true, configurable: true, get: noop, set: noop, } const getProperty = (obj: TraversedObject, key: string) => { const keys = key.split('.') for (let i = 0; i < keys.length - 1; i++) { obj = obj[keys[i]] if (typeof obj !== 'object' || !obj) return } const lastKey = keys.pop() as string if (typeof obj[lastKey] === 'function') { return function () { return obj[lastKey].apply(obj, arguments) } } else { return obj[lastKey] } } const setProperty = (obj: TraversedObject, key: string, value: any) => { const keys = key.split('.') let temp for (let i = 0; i < keys.length - 1; i++) { temp = keys[i] if (!obj[temp]) obj[temp] = {} obj = obj[temp] } obj[keys.pop() as string] = value } export function propertiesProxy( target: Object, sourceKey: string, key: string ) { sharedPropertyDefinition.get = function proxyGetter() { return getProperty(this, sourceKey) } sharedPropertyDefinition.set = function proxySetter(val) { setProperty(this, sourceKey, val) } Object.defineProperty(target, key, sharedPropertyDefinition) } ================================================ FILE: packages/shared-utils/src/raf.ts ================================================ import { inBrowser } from './env' interface DelayedHandler { (): void interval: number } const DEFAULT_INTERVAL = 1000 / 60 const windowCompat = inBrowser && (window as any) /* istanbul ignore next */ function noop() {} export const requestAnimationFrame = (() => { /* istanbul ignore if */ if (!inBrowser) { return noop } return ( windowCompat.requestAnimationFrame || windowCompat.webkitRequestAnimationFrame || windowCompat.mozRequestAnimationFrame || windowCompat.oRequestAnimationFrame || // if all else fails, use setTimeout function (callback: DelayedHandler) { return window.setTimeout(callback, callback.interval || DEFAULT_INTERVAL) // make interval as precise as possible. } ) })() export const cancelAnimationFrame = (() => { /* istanbul ignore if */ if (!inBrowser) { return noop } return ( windowCompat.cancelAnimationFrame || windowCompat.webkitCancelAnimationFrame || windowCompat.mozCancelAnimationFrame || windowCompat.oCancelAnimationFrame || function (id: number) { window.clearTimeout(id) } ) })() ================================================ FILE: packages/shared-utils/src/types.ts ================================================ export type Position = { x: number y: number } ================================================ FILE: packages/slide/README.md ================================================ # @better-scroll/slide [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/slide/README_zh-CN.md) The ability to inject a Carousel effect for BetterScroll. ## Usage ```js import BScroll from '@better-scroll/core' import Slide from '@better-scroll/slide' BScroll.use(Slide) const bs = new BScroll('.div', { scrollX: false, scrollY: true, slide: { loop: true, threshold: 100 }, useTransition: true, momentum: false, bounce: false, stopPropagation: true }) ``` ================================================ FILE: packages/slide/README_zh-CN.md ================================================ # @better-scroll/slide 实现轮播图的效果。 ## 使用 ```js import BScroll from '@better-scroll/core' import Slide from '@better-scroll/slide' BScroll.use(Slide) const bs = new BScroll('.div', { scrollX: false, scrollY: true, slide: { loop: true, threshold: 100 }, useTransition: true, momentum: false, bounce: false, stopPropagation: true }) ``` ================================================ FILE: packages/slide/package.json ================================================ { "name": "@better-scroll/slide", "version": "2.5.1", "description": "a carousel effect triggered by BetterScroll", "author": { "name": "jizhi", "email": "theniceangel@163.com" }, "main": "dist/slide.min.js", "module": "dist/slide.esm.js", "typings": "dist/types/index.d.ts", "publishConfig": { "access": "public" }, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios" ], "license": "MIT", "repository": { "type": "git", "url": "git+ssh://git@github.com/ustbhuangyi/better-scroll.git", "directory": "packages/slide" }, "dependencies": { "@better-scroll/core": "^2.5.1" }, "gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130" } ================================================ FILE: packages/slide/src/PagesMatrix.ts ================================================ import BScroll from '@better-scroll/core' import { PageIndex } from './SlidePages' import { DEFAULT_PAGE_STATS } from './constants' export interface PageStats { x: number y: number width: number height: number cx: number // center position of every page cy: number } export default class PagesMatrix { pages: Array> pageLengthOfX: number pageLengthOfY: number private wrapperWidth: number private wrapperHeight: number private scrollerWidth: number private scrollerHeight: number constructor(private scroll: BScroll) { this.init() } init() { const scroller = this.scroll.scroller const { scrollBehaviorX, scrollBehaviorY } = scroller this.wrapperWidth = scrollBehaviorX.wrapperSize this.wrapperHeight = scrollBehaviorY.wrapperSize this.scrollerHeight = scrollBehaviorY.contentSize this.scrollerWidth = scrollBehaviorX.contentSize this.pages = this.buildPagesMatrix(this.wrapperWidth, this.wrapperHeight) this.pageLengthOfX = this.pages ? this.pages.length : 0 this.pageLengthOfY = this.pages && this.pages[0] ? this.pages[0].length : 0 } getPageStats(pageX: number, pageY: number): PageStats { // Returns the default Stats when no Stats are retrieved // Scene: When the `content` element is an empty bounding box, the return value of the `width/height` function of el.getBoundingClientRect is 0 and pages will be calculated as an empty array. // https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect return this.pages[pageX] && this.pages[pageX][pageY] ? this.pages[pageX][pageY] : DEFAULT_PAGE_STATS } getNearestPageIndex(x: number, y: number): PageIndex { let pageX = 0 let pageY = 0 let l = this.pages.length for (; pageX < l - 1; pageX++) { if (x >= this.pages[pageX][0].cx) { break } } l = this.pages[pageX] ? this.pages[pageX].length : 0 for (; pageY < l - 1; pageY++) { if (y >= this.pages[0][pageY].cy) { break } } return { pageX, pageY, } } // (n x 1) matrix for horizontal scroll or // (1 * n) matrix for vertical scroll private buildPagesMatrix( stepX: number, stepY: number ): Array> { let pages: Array> = [] let x = 0 let y let cx let cy let i = 0 let l const maxScrollPosX = this.scroll.scroller.scrollBehaviorX.maxScrollPos const maxScrollPosY = this.scroll.scroller.scrollBehaviorY.maxScrollPos cx = Math.round(stepX / 2) cy = Math.round(stepY / 2) while (x > -this.scrollerWidth) { pages[i] = [] l = 0 y = 0 while (y > -this.scrollerHeight) { pages[i][l] = { x: Math.max(x, maxScrollPosX), y: Math.max(y, maxScrollPosY), width: stepX, height: stepY, cx: x - cx, cy: y - cy, } y -= stepY l++ } x -= stepX i++ } return pages } } ================================================ FILE: packages/slide/src/SlidePages.ts ================================================ import { between, extend, warn } from '@better-scroll/shared-utils' import BScroll from '@better-scroll/core' import { SlideConfig } from './index' import PagesMatrix, { PageStats } from './PagesMatrix' import { BASE_PAGE } from './constants' export interface PageIndex { pageX: number pageY: number } export interface Position { x: number y: number } export type Page = PageIndex & Position const enum Direction { Positive = 'positive', Negative = 'negative', } export default class SlidePages { loopX: boolean loopY: boolean slideX: boolean = false slideY: boolean = false wannaLoop: boolean pagesMatrix: PagesMatrix currentPage: Page constructor(public scroll: BScroll, private slideOptions: SlideConfig) { this.currentPage = extend({}, BASE_PAGE) } refresh() { this.pagesMatrix = new PagesMatrix(this.scroll) this.checkSlideLoop() this.currentPage = this.getAdjustedCurrentPage() } getAdjustedCurrentPage(): Page { let { pageX, pageY } = this.currentPage // page index should be handled // because page counts may reduce pageX = Math.min(pageX, this.pagesMatrix.pageLengthOfX - 1) pageY = Math.min(pageY, this.pagesMatrix.pageLengthOfY - 1) // loop scene should also be respected // because clonedNode will cause pageLength increasing if (this.loopX) { pageX = Math.min(pageX, this.pagesMatrix.pageLengthOfX - 2) } if (this.loopY) { pageY = Math.min(pageY, this.pagesMatrix.pageLengthOfY - 2) } const { x, y } = this.pagesMatrix.getPageStats(pageX, pageY) return { pageX, pageY, x, y } } setCurrentPage(newPage: Page) { this.currentPage = newPage } getInternalPage(pageX: number, pageY: number): Page { if (pageX >= this.pagesMatrix.pageLengthOfX) { pageX = this.pagesMatrix.pageLengthOfX - 1 } else if (pageX < 0) { pageX = 0 } if (pageY >= this.pagesMatrix.pageLengthOfY) { pageY = this.pagesMatrix.pageLengthOfY - 1 } else if (pageY < 0) { pageY = 0 } let { x, y } = this.pagesMatrix.getPageStats(pageX, pageY) return { pageX, pageY, x, y, } } getInitialPage( showFirstPage: boolean = false, firstInitialised: boolean = false ): Page { const { startPageXIndex, startPageYIndex } = this.slideOptions let firstPageX = this.loopX ? 1 : 0 let firstPageY = this.loopY ? 1 : 0 let pageX = showFirstPage ? firstPageX : this.currentPage.pageX let pageY = showFirstPage ? firstPageY : this.currentPage.pageY if (firstInitialised) { pageX = this.loopX ? startPageXIndex + 1 : startPageXIndex pageY = this.loopY ? startPageYIndex + 1 : startPageYIndex } else { pageX = showFirstPage ? firstPageX : this.currentPage.pageX pageY = showFirstPage ? firstPageY : this.currentPage.pageY } const { x, y } = this.pagesMatrix.getPageStats(pageX, pageY) return { pageX, pageY, x, y, } } getExposedPage(page: Page): Page { let exposedPage = extend({}, page) // only pageX or pageY need fix if (this.loopX) { exposedPage.pageX = this.fixedPage( exposedPage.pageX, this.pagesMatrix.pageLengthOfX - 2 ) } if (this.loopY) { exposedPage.pageY = this.fixedPage( exposedPage.pageY, this.pagesMatrix.pageLengthOfY - 2 ) } return exposedPage } getExposedPageByPageIndex(pageIndexX: number, pageIndexY: number): Page { const page = { pageX: pageIndexX, pageY: pageIndexY, } if (this.loopX) { page.pageX = pageIndexX + 1 } if (this.loopY) { page.pageY = pageIndexY + 1 } const { x, y } = this.pagesMatrix.getPageStats(page.pageX, page.pageY) return { x, y, pageX: pageIndexX, pageY: pageIndexY, } } getWillChangedPage(page: Page): Page { page = extend({}, page) // Page need fix if (this.loopX) { page.pageX = this.fixedPage( page.pageX, this.pagesMatrix.pageLengthOfX - 2 ) page.x = this.pagesMatrix.getPageStats(page.pageX + 1, 0).x } if (this.loopY) { page.pageY = this.fixedPage( page.pageY, this.pagesMatrix.pageLengthOfY - 2 ) page.y = this.pagesMatrix.getPageStats(0, page.pageY + 1).y } return page } private fixedPage(page: number, realPageLen: number): number { const pageIndex = [] for (let i = 0; i < realPageLen; i++) { pageIndex.push(i) } pageIndex.unshift(realPageLen - 1) pageIndex.push(0) return pageIndex[page] } getPageStats(): PageStats { return this.pagesMatrix.getPageStats( this.currentPage.pageX, this.currentPage.pageY ) } getValidPageIndex(x: number, y: number): PageIndex { let lastX = this.pagesMatrix.pageLengthOfX - 1 let lastY = this.pagesMatrix.pageLengthOfY - 1 let firstX = 0 let firstY = 0 if (this.loopX) { x += 1 firstX = firstX + 1 lastX = lastX - 1 } if (this.loopY) { y += 1 firstY = firstY + 1 lastY = lastY - 1 } x = between(x, firstX, lastX) y = between(y, firstY, lastY) return { pageX: x, pageY: y, } } nextPageIndex(): PageIndex { return this.getPageIndexByDirection(Direction.Positive) } prevPageIndex(): PageIndex { return this.getPageIndexByDirection(Direction.Negative) } getNearestPage(x: number, y: number): Page { const pageIndex = this.pagesMatrix.getNearestPageIndex(x, y) let { pageX, pageY } = pageIndex let newX = this.pagesMatrix.getPageStats(pageX, 0).x let newY = this.pagesMatrix.getPageStats(0, pageY).y return { x: newX, y: newY, pageX, pageY, } } getPageByDirection(page: Page, directionX: number, directionY: number): Page { let { pageX, pageY } = page if (pageX === this.currentPage.pageX) { pageX = between(pageX + directionX, 0, this.pagesMatrix.pageLengthOfX - 1) } if (pageY === this.currentPage.pageY) { pageY = between(pageY + directionY, 0, this.pagesMatrix.pageLengthOfY - 1) } const x = this.pagesMatrix.getPageStats(pageX, 0).x const y = this.pagesMatrix.getPageStats(0, pageY).y return { x, y, pageX, pageY, } } resetLoopPage(): PageIndex | undefined { if (this.loopX) { if (this.currentPage.pageX === 0) { return { pageX: this.pagesMatrix.pageLengthOfX - 2, pageY: this.currentPage.pageY, } } if (this.currentPage.pageX === this.pagesMatrix.pageLengthOfX - 1) { return { pageX: 1, pageY: this.currentPage.pageY, } } } if (this.loopY) { if (this.currentPage.pageY === 0) { return { pageX: this.currentPage.pageX, pageY: this.pagesMatrix.pageLengthOfY - 2, } } if (this.currentPage.pageY === this.pagesMatrix.pageLengthOfY - 1) { return { pageX: this.currentPage.pageX, pageY: 1, } } } } private getPageIndexByDirection(direction: Direction): PageIndex { let x = this.currentPage.pageX let y = this.currentPage.pageY if (this.slideX) { x = direction === Direction.Negative ? x - 1 : x + 1 } if (this.slideY) { y = direction === Direction.Negative ? y - 1 : y + 1 } return { pageX: x, pageY: y, } } private checkSlideLoop() { this.wannaLoop = this.slideOptions.loop if (this.pagesMatrix.pageLengthOfX > 1) { this.slideX = true } else { this.slideX = false } if (this.pagesMatrix.pages[0] && this.pagesMatrix.pageLengthOfY > 1) { this.slideY = true } else { this.slideY = false } this.loopX = this.wannaLoop && this.slideX this.loopY = this.wannaLoop && this.slideY if (this.slideX && this.slideY) { warn('slide does not support two direction at the same time.') } } } ================================================ FILE: packages/slide/src/__mocks__/PagesMatrix.ts ================================================ const PagesMatrix = jest.fn().mockImplementation((scroll) => { const loopX = scroll.options.scrollX const loopY = scroll.options.scrollY const pages = loopX ? [ [ { pageX: 0, pageY: 0, x: 0, y: 0, }, ], [ { pageX: 1, pageY: 0, x: 20, y: 0, }, ], [ { pageX: 2, pageY: 0, x: 40, y: 0, }, ], [ { pageX: 3, pageY: 0, x: 60, y: 0, }, ], ] : loopY ? [ [ { pageX: 0, pageY: 0, x: 0, y: 0, }, { pageX: 0, pageY: 1, x: 0, y: 20, }, { pageX: 0, pageY: 2, x: 0, y: 40, }, { pageX: 0, pageY: 3, x: 0, y: 60, }, ], ] : [] return { pages, pageLengthOfX: loopX ? 4 : 1, pageLengthOfY: loopY ? 4 : 1, wrapperWidth: 0, wrapperHeight: 0, scrollerWidth: 0, scrollerHeight: 0, init: jest.fn(), getPageStats: jest.fn().mockImplementation(() => { return { x: 0, y: 0, width: 100, height: 100, cx: 50, // center position of every page cy: 50, } }), getNearestPageIndex: jest.fn().mockImplementation(() => { return { pageX: 1, pageY: 0, } }), buildPagesMatrix: jest.fn(), } }) export default PagesMatrix ================================================ FILE: packages/slide/src/__mocks__/SlidePages.ts ================================================ import PagesMatrix from '../PagesMatrix' jest.mock('../PagesMatrix') const SlidePage = jest.fn().mockImplementation(() => { return { currentPage: { pageX: 0, pageY: 0, x: 0, y: 0, }, loopX: true, loopY: false, slideX: true, slideY: false, needLoop: true, pagesMatrix: new PagesMatrix({ options: {}, } as any), refresh: jest.fn(), checkSlideLoop: jest.fn(), setCurrentPage: jest.fn(), getInternalPage: jest.fn().mockImplementation((pageX, pageY) => { return { pageX, pageY, x: 20, y: 20, } }), getExposedPageByPageIndex: jest.fn(), getInitialPage: jest.fn().mockImplementation((flag, firstInit) => { if (firstInit) { return { pageX: 1, pageY: 0, x: 10, y: 10, } } return { pageX: 0, pageY: 0, x: 10, y: 10, } }), getExposedPage: jest.fn().mockImplementation((page) => { return ( page || { pageX: 0, pageY: 0, x: 0, y: 0, } ) }), getWillChangedPage: jest.fn().mockImplementation(() => { return { pageX: 0, pageY: 0, x: 0, y: 0, } }), fixedPage: jest.fn(), getPageStats: jest.fn().mockImplementation(() => { return { width: 0, height: 0, } }), getValidPageIndex: jest.fn().mockImplementation((pageX, pageY) => { return { pageX, pageY, } }), nextPageIndex: jest.fn().mockImplementation(() => { return { pageX: 0, pageY: 0, } }), prevPageIndex: jest.fn().mockImplementation(() => { return { pageX: 0, pageY: 0, } }), getNearestPage: jest.fn().mockImplementation(() => { return { pageX: 0, pageY: 0, x: 0, y: 0, } }), resetLoopPage: jest.fn().mockImplementation(() => { return { pageX: 0, pageY: 0, } }), getPageIndexByDirection: jest.fn(), getPageByDirection: jest.fn().mockImplementation(() => { return { pageX: 0, pageY: 0, x: 0, y: 0, } }), } }) export default SlidePage ================================================ FILE: packages/slide/src/__tests__/PagesMatrix.spec.ts ================================================ import PagesMatrix from '../PagesMatrix' import BScroll from '@better-scroll/core' import { DEFAULT_PAGE_STATS } from '../constants' const createSlideElements = () => { const wrapper = document.createElement('div') const content = document.createElement('div') for (let i = 0; i < 3; i++) { content.appendChild(document.createElement('p')) } wrapper.appendChild(content) return { wrapper } } describe('slide test for PagesMatrix class', () => { let pageMatrix: PagesMatrix let scroll: BScroll beforeEach(() => { const { wrapper } = createSlideElements() scroll = new BScroll(wrapper, {}) scroll.scroller.scrollBehaviorX.wrapperSize = 100 scroll.scroller.scrollBehaviorX.contentSize = 400 scroll.scroller.scrollBehaviorX.maxScrollPos = -300 scroll.scroller.scrollBehaviorY.wrapperSize = 100 scroll.scroller.scrollBehaviorY.contentSize = 100 scroll.scroller.scrollBehaviorY.maxScrollPos = 0 pageMatrix = new PagesMatrix(scroll) }) afterEach(() => { jest.clearAllMocks() }) it('should create 4 * 1 matrix page in X direction', () => { expect(pageMatrix.pages.length).toBe(4) expect(pageMatrix.pages[0].length).toBe(1) expect(pageMatrix.pageLengthOfX).toBe(4) expect(pageMatrix.pageLengthOfY).toBe(1) const pageIndex1 = pageMatrix.getNearestPageIndex(0, 0) expect(pageIndex1).toMatchObject({ pageX: 0, pageY: 0, }) const pageIndex2 = pageMatrix.getNearestPageIndex(-175, 0) expect(pageIndex2).toMatchObject({ pageX: 2, pageY: 0, }) }) it('should create 1 * 4 matrix page in Y direction', () => { const { wrapper } = createSlideElements() scroll = new BScroll(wrapper, {}) scroll.scroller.scrollBehaviorX.wrapperSize = 100 scroll.scroller.scrollBehaviorX.contentSize = 100 scroll.scroller.scrollBehaviorX.maxScrollPos = 0 scroll.scroller.scrollBehaviorY.wrapperSize = 100 scroll.scroller.scrollBehaviorY.contentSize = 400 scroll.scroller.scrollBehaviorY.maxScrollPos = -300 pageMatrix = new PagesMatrix(scroll) expect(pageMatrix.pages.length).toBe(1) expect(pageMatrix.pages[0].length).toBe(4) expect(pageMatrix.pageLengthOfX).toBe(1) expect(pageMatrix.pageLengthOfY).toBe(4) const pageIndex1 = pageMatrix.getNearestPageIndex(0, 0) expect(pageIndex1).toMatchObject({ pageX: 0, pageY: 0, }) const pageIndex2 = pageMatrix.getNearestPageIndex(0, -175) expect(pageIndex2).toMatchObject({ pageX: 0, pageY: 2, }) }) it('should work well with getPageStats()', () => { const pageStats1 = pageMatrix.getPageStats(1, 0) expect(pageStats1).toMatchObject({ x: -100, y: 0, width: 100, height: 100, cx: -150, cy: -50, }) const pageStats2 = pageMatrix.getPageStats(2, 0) expect(pageStats2).toMatchObject({ x: -200, y: 0, width: 100, height: 100, cx: -250, cy: -50, }) }) it('The pages calculation fails to access PageStats should return the default value', () => { const { wrapper } = createSlideElements() scroll = new BScroll(wrapper, {}) scroll.scroller.scrollBehaviorX.wrapperSize = 100 scroll.scroller.scrollBehaviorX.contentSize = 0 scroll.scroller.scrollBehaviorX.maxScrollPos = 100 scroll.scroller.scrollBehaviorY.wrapperSize = 100 scroll.scroller.scrollBehaviorY.contentSize = 0 scroll.scroller.scrollBehaviorY.maxScrollPos = 100 pageMatrix = new PagesMatrix(scroll) expect(pageMatrix.pages.length).toBe(0) expect(pageMatrix.pageLengthOfX).toBe(0) expect(pageMatrix.pageLengthOfY).toBe(0) const pagesIdx: [number, number][] = [ [1, 0], [-1, 0], [1, 1], [-1, -1], [0, 1], [0, -1], ] for (const idx of pagesIdx) { expect(pageMatrix.getPageStats(idx[0], idx[1])).toMatchObject( DEFAULT_PAGE_STATS ) } const pagesLoc: [number, number][] = [ [-100, 0], [0, -100], ] for (const loc of pagesLoc) { expect(pageMatrix.getNearestPageIndex(loc[0], loc[1])).toMatchObject({ pageX: 0, pageY: 0, }) } }) }) ================================================ FILE: packages/slide/src/__tests__/SlidePages.spec.ts ================================================ import SlidePages from '../SlidePages' import PageMatrix from '../PagesMatrix' import BScroll from '@better-scroll/core' import { ease } from '@better-scroll/shared-utils' jest.mock('@better-scroll/core') jest.mock('../PagesMatrix') const createSlideElements = () => { const wrapper = document.createElement('div') const content = document.createElement('div') for (let i = 0; i < 3; i++) { content.appendChild(document.createElement('p')) } wrapper.appendChild(content) return { wrapper } } describe('slide test for SlidePages class', () => { let slidePages: SlidePages let scroll: BScroll const BASE_PAGE = { pageX: 0, pageY: 0, x: 0, y: 0, } const slideOptions = { loop: true, threshold: 0.1, speed: 400, easing: ease.bounce, listenFlick: true, autoplay: true, interval: 3000, startPageXIndex: 0, startPageYIndex: 0, } afterEach(() => { jest.clearAllMocks() }) describe('loopX', () => { beforeEach(() => { const { wrapper } = createSlideElements() scroll = new BScroll(wrapper, { scrollX: true, scrollY: false, }) slidePages = new SlidePages(scroll, slideOptions) slidePages.refresh() }) it('should has base current page', () => { expect(slidePages.currentPage).toMatchObject(BASE_PAGE) }) it('should set loopX when new SlidePage', () => { expect(slidePages.loopX).toBe(true) }) it('should work well with getExposedPageByPageIndex()', () => { const ret = slidePages.getExposedPageByPageIndex(1, 0) expect(ret).toMatchObject({ x: 0, y: 0, pageX: 1, pageY: 0, }) }) it('should work well with setCurrentPage()', () => { const currentPage = { pageX: 1, pageY: 0, x: 200, y: 200, } slidePages.setCurrentPage(currentPage) expect(slidePages.currentPage).toMatchObject(currentPage) }) it('should work well with getInternalPage()', () => { const page1 = slidePages.getInternalPage(1, 0) expect(page1).toMatchObject({ pageX: 1, pageY: 0, x: 0, y: 0, }) // exceed maxPageX const page2 = slidePages.getInternalPage(4, 0) expect(page2).toMatchObject({ pageX: 3, pageY: 0, x: 0, y: 0, }) // less than maxPageX const page3 = slidePages.getInternalPage(-1, 0) expect(page3).toMatchObject({ pageX: 0, pageY: 0, x: 0, y: 0, }) }) it('should work well with getInitialPage()', () => { const page = slidePages.getInitialPage(true) expect(page).toMatchObject({ pageX: 1, pageY: 0, x: 0, y: 0, }) }) it('should work well with getExposedPage() when loopX is true', () => { const pageX = slidePages.getExposedPage({ x: 0, y: 0, pageX: 1, pageY: 0, }) expect(pageX).toMatchObject({ pageX: 0, pageY: 0, x: 0, y: 0, }) }) it('should work well with getWillChangedPage() when loopX is true', () => { const page = slidePages.getWillChangedPage({ pageX: 0, pageY: 0, x: 0, y: 0, }) expect(page).toMatchObject({ pageX: 1, pageY: 0, x: 0, y: 0, }) }) it('should work well with getPageStats()', () => { const pageStats = slidePages.getPageStats() expect(pageStats).toMatchObject({ x: 0, y: 0, width: 100, height: 100, cx: 50, cy: 50, }) }) it('should work well with getValidPageIndex()', () => { const pageIndex = slidePages.getValidPageIndex(1, 0) expect(pageIndex).toMatchObject({ pageX: 2, pageY: 0, }) }) it('should work well with nextPageIndex()', () => { const pageIndex = slidePages.nextPageIndex() expect(pageIndex).toMatchObject({ pageX: 1, pageY: 0, }) }) it('should work well with prevPageIndex()', () => { const pageIndex = slidePages.prevPageIndex() expect(pageIndex).toMatchObject({ pageX: -1, pageY: 0, }) }) it('should work well with getNearestPage()', () => { const pageIndex = slidePages.getNearestPage(30, 0) expect(pageIndex).toMatchObject({ pageX: 1, pageY: 0, x: 0, y: 0, }) }) it('should work well with resetLoopPage()', () => { const page1 = slidePages.resetLoopPage() expect(page1).toMatchObject({ pageX: 2, pageY: 0, }) slidePages.setCurrentPage({ pageX: 3, pageY: 0, x: 0, y: 0, }) const page2 = slidePages.resetLoopPage() expect(page2).toMatchObject({ pageX: 1, pageY: 0, }) }) it('should work well with getPageByDirection()', () => { const page = slidePages.getPageByDirection( { x: 0, y: 0, pageY: 0, pageX: 0, }, 1, 0 ) expect(page.pageX).toBe(1) }) }) describe('loopY', () => { beforeEach(() => { const { wrapper } = createSlideElements() scroll = new BScroll(wrapper, { scrollX: false, scrollY: true, }) slidePages = new SlidePages(scroll, slideOptions) slidePages.refresh() }) it('should set loopY when new SlidePage', () => { expect(slidePages.loopY).toBe(true) }) it('should work well with getExposedPageByPageIndex()', () => { const ret = slidePages.getExposedPageByPageIndex(0, 1) expect(ret).toMatchObject({ x: 0, y: 0, pageX: 0, pageY: 1, }) }) it('should work well with getInitialPage()', () => { const page = slidePages.getInitialPage(true, true) expect(page).toMatchObject({ pageX: 0, pageY: 1, x: 0, y: 0, }) }) it('should work well with getExposedPage() when loopY is true', () => { const pageY = slidePages.getExposedPage({ x: 0, y: 0, pageX: 0, pageY: 1, }) expect(pageY).toMatchObject({ pageX: 0, pageY: 0, x: 0, y: 0, }) }) it('should work well with getWillChangedPage() when loopY is true', () => { const page = slidePages.getWillChangedPage({ pageX: 0, pageY: 0, x: 0, y: 0, }) expect(page).toMatchObject({ pageX: 0, pageY: 1, x: 0, y: 0, }) }) it('should work well with getValidPageIndex()', () => { const pageIndex = slidePages.getValidPageIndex(0, 1) expect(pageIndex).toMatchObject({ pageX: 0, pageY: 2, }) }) it('should work well with nextPageIndex()', () => { const pageIndex = slidePages.nextPageIndex() expect(pageIndex).toMatchObject({ pageX: 0, pageY: 1, }) }) it('should work well with prevPageIndex()', () => { const pageIndex = slidePages.prevPageIndex() expect(pageIndex).toMatchObject({ pageX: 0, pageY: -1, }) }) it('should work well with resetLoopPage()', () => { const page1 = slidePages.resetLoopPage() expect(page1).toMatchObject({ pageX: 0, pageY: 2, }) slidePages.setCurrentPage({ pageX: 0, pageY: 3, x: 0, y: 0, }) const page2 = slidePages.resetLoopPage() expect(page2).toMatchObject({ pageX: 0, pageY: 1, }) }) it('should work well with getPageByDirection()', () => { const page = slidePages.getPageByDirection( { x: 0, y: 0, pageY: 0, pageX: 0, }, 0, 1 ) expect(page.pageY).toBe(1) }) it('should work well with getInternalPage()', () => { const page1 = slidePages.getInternalPage(0, 1) expect(page1).toMatchObject({ pageX: 0, pageY: 1, x: 0, y: 0, }) // exceed maxPageY const page2 = slidePages.getInternalPage(0, 4) expect(page2).toMatchObject({ pageX: 0, pageY: 3, x: 0, y: 0, }) // less than maxPageX const page3 = slidePages.getInternalPage(0, -1) expect(page3).toMatchObject({ pageX: 0, pageY: 0, x: 0, y: 0, }) }) }) describe('loopX & loopY', () => { it('should warn when loopX & loopY is true', () => { const spyFn = jest.spyOn(console, 'error') const { wrapper } = createSlideElements() scroll = new BScroll(wrapper, { scrollX: true, scrollY: true, }) slidePages = new SlidePages(scroll, slideOptions) slidePages.refresh() expect(spyFn).toHaveBeenCalledTimes(1) }) }) }) ================================================ FILE: packages/slide/src/__tests__/index.spec.ts ================================================ import BScroll from '@better-scroll/core' import Slide from '../index' import SlidePages from '../SlidePages' import { ease } from '@better-scroll/shared-utils' jest.mock('@better-scroll/core') jest.mock('../SlidePages') const createSlideElements = (len = 3) => { const wrapper = document.createElement('div') const content = document.createElement('div') for (let i = 0; i < len; i++) { content.appendChild(document.createElement('p')) } wrapper.appendChild(content) return { wrapper, content } } describe('slide test for SlidePage class', () => { let scroll: BScroll let slide: Slide beforeAll(() => { jest.useFakeTimers() }) beforeEach(() => { const { wrapper } = createSlideElements() scroll = new BScroll(wrapper, {}) slide = new Slide(scroll) }) afterAll(() => { jest.clearAllMocks() jest.clearAllTimers() }) it('should fail when slideContent has no children element', () => { const spyFn = jest.spyOn(console, 'error') const { wrapper } = createSlideElements(0) scroll = new BScroll(wrapper, {}) slide = new Slide(scroll) expect(spyFn).toBeCalled() }) it('should proxy hooks to BScroll instance', () => { expect(scroll.registerType).toHaveBeenCalledWith([ 'slideWillChange', 'slidePageChanged', ]) expect(scroll.proxy).toHaveBeenLastCalledWith([ { key: 'next', sourceKey: 'plugins.slide.next', }, { key: 'prev', sourceKey: 'plugins.slide.prev', }, { key: 'goToPage', sourceKey: 'plugins.slide.goToPage', }, { key: 'getCurrentPage', sourceKey: 'plugins.slide.getCurrentPage', }, { key: 'startPlay', sourceKey: 'plugins.slide.startPlay', }, { key: 'pausePlay', sourceKey: 'plugins.slide.pausePlay', }, ]) }) it('should handle default options and user options', () => { // case 1 scroll.options.slide = true slide = new Slide(scroll) expect(slide.options).toMatchObject({ loop: true, threshold: 0.1, speed: 400, easing: ease.bounce, listenFlick: true, autoplay: true, interval: 3000, }) // case 2 scroll.options.slide = { loop: false, autoplay: false, } slide = new Slide(scroll) expect(slide.options).toMatchObject({ loop: false, threshold: 0.1, speed: 400, easing: ease.bounce, listenFlick: true, autoplay: false, interval: 3000, }) }) it('should clone the first and last page when loop is true', () => { const content = scroll.scroller.content scroll.scroller.hooks.trigger( scroll.scroller.hooks.eventTypes.beforeRefresh ) expect(content.children.length).toBe(5) }) it('should not clone the first and last page when loop is false', () => { const { wrapper } = createSlideElements() scroll = new BScroll(wrapper) scroll.options.slide = { loop: false, } slide = new Slide(scroll) scroll.scroller.hooks.trigger( scroll.scroller.hooks.eventTypes.beforeRefresh ) const content = scroll.scroller.content expect(content.children.length).toBe(3) }) it('should failed to initialised slide when only has a child', () => { const { wrapper } = createSlideElements(0) const spyFn = jest.spyOn(console, 'error') scroll = new BScroll(wrapper, {}) slide = new Slide(scroll) expect(spyFn).toBeCalled() }) describe('api', () => { it('goToPage()', () => { slide.goToPage(2, 0) expect(scroll.scroller.scrollTo).toBeCalledWith( 20, 20, 400, expect.anything() ) }) it('getCurrentPage()', () => { slide.getCurrentPage() expect(slide.pages.getInitialPage).toBeCalled() }) it('startPlay()', () => { slide.startPlay() jest.advanceTimersByTime(4000) expect(scroll.scroller.scrollTo).toBeCalledWith( 20, 20, 400, expect.anything() ) }) }) describe('tap into scroll', () => { it('should pause play when BScroll trigger beforeScrollStart hook', () => { const spyFn = jest.spyOn(Slide.prototype, 'pausePlay') slide = new Slide(scroll) scroll.trigger(scroll.eventTypes.beforeScrollStart) expect(spyFn).toBeCalled() }) it('should call modifyCurrentPage() when BScroll trigger scrollEnd hook', () => { // simulate stopping from animation scroll.scroller.animater.forceStopped = true scroll.trigger(scroll.eventTypes.scrollEnd, { x: 0, y: 0 }) expect(slide.pages.setCurrentPage).toBeCalled() scroll.trigger(scroll.eventTypes.scrollEnd, { x: 0, y: 0 }) expect(slide.pages.resetLoopPage).toBeCalledTimes(1) }) it('slidePageChanged event', () => { const { wrapper } = createSlideElements() const scroll = new BScroll(wrapper, {}) const slide = new Slide(scroll) scroll.trigger(scroll.eventTypes.scrollEnd, { x: 0, y: 0 }) expect(slide.pages.getExposedPageByPageIndex).toBeCalled() }) it('should stop mousewheelMove handler chain', () => { scroll.registerType(['mousewheelMove']) slide = new Slide(scroll) const mock = jest.fn() scroll.on(scroll.eventTypes.mousewheelMove, mock) scroll.trigger(scroll.eventTypes.mousewheelMove) expect(mock).not.toBeCalled() }) it('should call next/prev in mousewheelEnd hook', () => { scroll.registerType(['mousewheelMove', 'mousewheelEnd']) slide = new Slide(scroll) const nextSpyFn = jest.spyOn(Slide.prototype, 'next') const prevSpyFn = jest.spyOn(Slide.prototype, 'prev') const delta1 = { directionX: -1, directionY: -1, } scroll.trigger(scroll.eventTypes.mousewheelEnd, delta1) expect(prevSpyFn).toBeCalled() const delta2 = { directionX: 1, directionY: 1, } scroll.trigger(scroll.eventTypes.mousewheelEnd, delta2) expect(nextSpyFn).toBeCalled() }) }) describe('tap into scroll hooks', () => { it('should call refreshHandler when Bscroll.hooks.refresh triggered', () => { // case 1 content changed let wrapper1 = createSlideElements().wrapper scroll = new BScroll(wrapper1, { slide: { threshold: 100, }, }) slide = new Slide(scroll) ;(slide as any).initialised = true scroll.hooks.trigger(scroll.hooks.eventTypes.refresh) expect(scroll.scroller.scrollTo).toBeCalledWith( 20, 20, 0, expect.anything() ) expect(slide.pages.getInitialPage).toHaveBeenCalled() // case 2 content has only no child let { wrapper: wrapper2, content: content2 } = createSlideElements(1) scroll = new BScroll(wrapper2, {}) slide = new Slide(scroll) content2.removeChild(content2.children[0]) scroll.hooks.trigger(scroll.hooks.eventTypes.refresh) expect(slide.pages.refresh).not.toBeCalled() // case3 common refresh let { wrapper: wrapper3 } = createSlideElements(1) scroll = new BScroll(wrapper3, {}) slide = new Slide(scroll) const spyFn2 = jest.spyOn(Slide.prototype, 'startPlay') const position = {} scroll.hooks.trigger(scroll.hooks.eventTypes.refresh) scroll.hooks.trigger( scroll.hooks.eventTypes.beforeInitialScrollTo, position ) expect(slide.pages.refresh).toBeCalled() expect(slide.pages.getInitialPage).toBeCalled() expect(position).toMatchObject({ x: 10, y: 10, }) expect(spyFn2).toBeCalled() }) }) describe('tap into scroller hooks', () => { it('should call modifyScrollMetaHandler when scroller.hooks.momentum triggered', () => { const scrollMeta = { newX: -1, newY: -1, time: 0, } scroll.scroller.hooks.trigger( scroll.scroller.hooks.eventTypes.momentum, scrollMeta ) expect(scrollMeta.newX).toBe(0) expect(scrollMeta.newY).toBe(0) expect(scrollMeta.time).toBe(400) expect(slide.pages.getPageByDirection).toBeCalledWith( { x: 0, y: 0, pageX: 0, pageY: 0, }, 0, 0 ) }) it('should start a new autoPlay timer when scroller.hooks.checkClick triggered', () => { const spyFn = jest.spyOn(Slide.prototype, 'startPlay') slide = new Slide(scroll) scroll.scroller.hooks.trigger(scroll.scroller.hooks.eventTypes.checkClick) expect(spyFn).toBeCalled() }) it('should go to next/pre page scroller.hooks.flickHandler triggered', () => { slide = new Slide(scroll) scroll.scroller.hooks.trigger(scroll.scroller.hooks.eventTypes.flick) expect(scroll.scroller.scrollTo).toBeCalledWith( 20, 20, 400, expect.anything() ) }) it('should dispatch slideWillChange event when scroller.hooks.scroll triggered', () => { slide = new Slide(scroll) let pageX = 0 scroll.on(scroll.eventTypes.slideWillChange, (Page: any) => { pageX = Page.pageX }) slide.pages.getWillChangedPage = jest.fn().mockImplementation(() => { return { pageX: 1, pageY: 0, x: 0, y: 0, } }) scroll.hooks.trigger(scroll.hooks.eventTypes.refresh) scroll.scroller.hooks.trigger(scroll.scroller.hooks.eventTypes.scroll, { x: 0, y: 0, }) expect(pageX).toBe(0) scroll.scroller.hooks.trigger(scroll.scroller.hooks.eventTypes.scroll, { x: 200, y: 0, }) expect(pageX).toBe(1) }) it('scroller.hooks.beforeRefresh', () => { const { wrapper } = createSlideElements() const scroll = new BScroll(wrapper, {}) const slide = new Slide(scroll) scroll.scroller.hooks.trigger( scroll.scroller.hooks.eventTypes.beforeRefresh ) expect(scroll.scroller.content.children.length).toBe(5) // slideContent changed slide.prevContent = document.createElement('p') scroll.scroller.hooks.trigger( scroll.scroller.hooks.eventTypes.beforeRefresh ) expect(scroll.scroller.content.children.length).toBe(7) // many pages reduce to one page const mockFn1 = jest.fn() slide.initialised = true slide.prevContent = wrapper.children[0] as HTMLElement const childrenEl = [...Array.from(scroll.scroller.content.children)] for (let i = 1; i < 5; i++) { scroll.scroller.content.removeChild(childrenEl[i]) } scroll.on(scroll.eventTypes.scrollEnd, mockFn1) scroll.scroller.hooks.trigger( scroll.scroller.hooks.eventTypes.beforeRefresh ) scroll.trigger(scroll.eventTypes.scrollEnd, { x: 0, y: 0 }) expect(scroll.scroller.content.children.length).toBe(1) expect(mockFn1).not.toBeCalled() // one page increases to many page const mockFn2 = jest.fn() scroll.scroller.content.appendChild(document.createElement('div')) scroll.on(scroll.eventTypes.scrollEnd, mockFn1) scroll.scroller.hooks.trigger( scroll.scroller.hooks.eventTypes.beforeRefresh ) scroll.trigger(scroll.eventTypes.scrollEnd, { x: 0, y: 0 }) expect(scroll.scroller.content.children.length).toBe(4) expect(mockFn2).not.toBeCalled() while (scroll.scroller.content.children.length) { const len = scroll.scroller.content.children.length scroll.scroller.content.removeChild( scroll.scroller.content.children[len - 1] ) } // reset loop changed status scroll.scroller.hooks.trigger( scroll.scroller.hooks.eventTypes.beforeRefresh ) }) }) it('should destroy all events', () => { scroll.scroller.hooks.trigger( scroll.scroller.hooks.eventTypes.beforeRefresh ) scroll.hooks.trigger(scroll.hooks.eventTypes.destroy) expect(scroll.scroller.content.children.length).toBe(3) expect(scroll.events['beforeScrollStart'].length).toBe(0) expect(scroll.events['scrollEnd'].length).toBe(0) }) }) ================================================ FILE: packages/slide/src/constants.ts ================================================ export const BASE_PAGE = { pageX: 0, pageY: 0, x: 0, y: 0, } export const DEFAULT_PAGE_STATS = { x: 0, y: 0, width: 0, height: 0, cx: 0, cy: 0, } ================================================ FILE: packages/slide/src/index.ts ================================================ import BScroll from '@better-scroll/core' import { between, prepend, removeChild, ease, extend, EaseItem, Direction, warn, EventEmitter, } from '@better-scroll/shared-utils' import SlidePages, { Page, Position } from './SlidePages' import propertiesConfig from './propertiesConfig' import { BASE_PAGE } from './constants' export interface SlideConfig { loop: boolean threshold: number speed: number easing: { style: string fn: (t: number) => number } listenFlick: boolean autoplay: boolean interval: number startPageXIndex: number startPageYIndex: number } export type SlideOptions = Partial | true declare module '@better-scroll/core' { interface CustomOptions { slide?: SlideOptions } interface CustomAPI { slide: PluginAPI } } interface PluginAPI { next(time?: number, easing?: EaseItem): void prev(time?: number, easing?: EaseItem): void goToPage(x: number, y: number, time?: number, easing?: EaseItem): void getCurrentPage(): Page startPlay(): void pausePlay(): void } const samePage = (p1: Page, p2: Page) => { return p1.pageX === p2.pageX && p1.pageY === p2.pageY } type styleConfiguration = { direction: 'scrollX' | 'scrollY' sizeType: 'offsetWidth' | 'offsetHeight' styleType: 'width' | 'height' } export default class Slide implements PluginAPI { static pluginName = 'slide' pages: SlidePages options: SlideConfig initialised: boolean contentChanged: boolean prevContent: HTMLElement exposedPage: Page private cachedClonedPageDOM: HTMLElement[] = [] private oneToMorePagesInLoop: boolean private moreToOnePageInLoop: boolean private thresholdX: number private thresholdY: number private hooksFn: Array<[EventEmitter, string, Function]> private resetLooping = false private willChangeToPage: Page private autoplayTimer: number = 0 constructor(public scroll: BScroll) { if (!this.satisfyInitialization()) { return } this.init() } private satisfyInitialization(): boolean { if (this.scroll.scroller.content.children.length <= 0) { warn( `slide need at least one slide page to be initialised.` + `please check your DOM layout.` ) return false } return true } init() { this.willChangeToPage = extend({}, BASE_PAGE) this.handleBScroll() this.handleOptions() this.handleHooks() this.createPages() } private createPages() { this.pages = new SlidePages(this.scroll, this.options) } private handleBScroll() { this.scroll.registerType(['slideWillChange', 'slidePageChanged']) this.scroll.proxy(propertiesConfig) } private handleOptions() { const userOptions = (this.scroll.options.slide === true ? {} : this.scroll.options.slide) as Partial const defaultOptions: SlideConfig = { loop: true, threshold: 0.1, speed: 400, easing: ease.bounce, listenFlick: true, autoplay: true, interval: 3000, startPageXIndex: 0, startPageYIndex: 0, } this.options = extend(defaultOptions, userOptions) } private handleLoop(prevSlideContent: HTMLElement) { const { loop } = this.options const slideContent = this.scroll.scroller.content const currentSlidePagesLength = slideContent.children.length // only should respect loop scene if (loop) { if (slideContent !== prevSlideContent) { this.resetLoopChangedStatus() this.removeClonedSlidePage(prevSlideContent) currentSlidePagesLength > 1 && this.cloneFirstAndLastSlidePage(slideContent) } else { // many pages reduce to one page if (currentSlidePagesLength === 3 && this.initialised) { this.removeClonedSlidePage(slideContent) this.moreToOnePageInLoop = true this.oneToMorePagesInLoop = false } else if (currentSlidePagesLength > 1) { // one page increases to many page if (this.initialised && this.cachedClonedPageDOM.length === 0) { this.oneToMorePagesInLoop = true this.moreToOnePageInLoop = false } else { this.removeClonedSlidePage(slideContent) this.resetLoopChangedStatus() } this.cloneFirstAndLastSlidePage(slideContent) } else { this.resetLoopChangedStatus() } } } } private resetLoopChangedStatus() { this.moreToOnePageInLoop = false this.oneToMorePagesInLoop = false } private handleHooks() { const scrollHooks = this.scroll.hooks const scrollerHooks = this.scroll.scroller.hooks const { listenFlick } = this.options this.prevContent = this.scroll.scroller.content this.hooksFn = [] // scroll this.registerHooks( this.scroll, this.scroll.eventTypes.beforeScrollStart, this.pausePlay ) this.registerHooks( this.scroll, this.scroll.eventTypes.scrollEnd, this.modifyCurrentPage ) this.registerHooks( this.scroll, this.scroll.eventTypes.scrollEnd, this.startPlay ) // for mousewheel event if (this.scroll.eventTypes.mousewheelMove) { this.registerHooks( this.scroll, this.scroll.eventTypes.mousewheelMove, () => { // prevent default action of mousewheelMove return true } ) this.registerHooks( this.scroll, this.scroll.eventTypes.mousewheelEnd, (delta: { directionX: number; directionY: number }) => { if ( delta.directionX === Direction.Positive || delta.directionY === Direction.Positive ) { this.next() } if ( delta.directionX === Direction.Negative || delta.directionY === Direction.Negative ) { this.prev() } } ) } // scrollHooks this.registerHooks( scrollHooks, scrollHooks.eventTypes.refresh, this.refreshHandler ) this.registerHooks( scrollHooks, scrollHooks.eventTypes.destroy, this.destroy ) // scroller this.registerHooks( scrollerHooks, scrollerHooks.eventTypes.beforeRefresh, () => { this.handleLoop(this.prevContent) this.setSlideInlineStyle() } ) this.registerHooks( scrollerHooks, scrollerHooks.eventTypes.momentum, this.modifyScrollMetaHandler ) this.registerHooks( scrollerHooks, scrollerHooks.eventTypes.scroll, this.scrollHandler ) // a click operation will clearTimer, so restart a new one this.registerHooks( scrollerHooks, scrollerHooks.eventTypes.checkClick, this.startPlay ) if (listenFlick) { this.registerHooks( scrollerHooks, scrollerHooks.eventTypes.flick, this.flickHandler ) } } startPlay() { const { interval, autoplay } = this.options if (autoplay) { clearTimeout(this.autoplayTimer) this.autoplayTimer = window.setTimeout(() => { this.next() }, interval) } } pausePlay() { if (this.options.autoplay) { clearTimeout(this.autoplayTimer) } } private setSlideInlineStyle() { const styleConfigurations: styleConfiguration[] = [ { direction: 'scrollX', sizeType: 'offsetWidth', styleType: 'width', }, { direction: 'scrollY', sizeType: 'offsetHeight', styleType: 'height', }, ] const { content: slideContent, wrapper: slideWrapper, } = this.scroll.scroller const scrollOptions = this.scroll.options styleConfigurations.forEach(({ direction, sizeType, styleType }) => { // wanna scroll in this direction if (scrollOptions[direction]) { const size = slideWrapper[sizeType] const children = slideContent.children const length = children.length for (let i = 0; i < length; i++) { const slidePageDOM = children[i] as HTMLElement slidePageDOM.style[styleType] = size + 'px' } slideContent.style[styleType] = size * length + 'px' } }) } next(time?: number, easing?: EaseItem) { const { pageX, pageY } = this.pages.nextPageIndex() this.goTo(pageX, pageY, time, easing) } prev(time?: number, easing?: EaseItem) { const { pageX, pageY } = this.pages.prevPageIndex() this.goTo(pageX, pageY, time, easing) } goToPage(pageX: number, pageY: number, time?: number, easing?: EaseItem) { const pageIndex = this.pages.getValidPageIndex(pageX, pageY) this.goTo(pageIndex.pageX, pageIndex.pageY, time, easing) } getCurrentPage(): Page { return this.exposedPage || this.pages.getInitialPage(false, true) } setCurrentPage(page: Page) { this.pages.setCurrentPage(page) this.exposedPage = this.pages.getExposedPage(page) } nearestPage(x: number, y: number): Page { const { scrollBehaviorX, scrollBehaviorY } = this.scroll.scroller const { maxScrollPos: maxScrollPosX, minScrollPos: minScrollPosX, } = scrollBehaviorX const { maxScrollPos: maxScrollPosY, minScrollPos: minScrollPosY, } = scrollBehaviorY return this.pages.getNearestPage( between(x, maxScrollPosX, minScrollPosX), between(y, maxScrollPosY, minScrollPosY) ) } private satisfyThreshold(x: number, y: number): boolean { const { scrollBehaviorX, scrollBehaviorY } = this.scroll.scroller let satisfied = true if ( Math.abs(x - scrollBehaviorX.absStartPos) <= this.thresholdX && Math.abs(y - scrollBehaviorY.absStartPos) <= this.thresholdY ) { satisfied = false } return satisfied } private refreshHandler(content: HTMLElement) { if (!this.satisfyInitialization()) { return } this.pages.refresh() this.computeThreshold() const contentChanged = (this.contentChanged = this.prevContent !== content) if (contentChanged) { this.prevContent = content } const initPage = this.pages.getInitialPage( this.oneToMorePagesInLoop || this.moreToOnePageInLoop, contentChanged || !this.initialised ) if (this.initialised) { this.goTo(initPage.pageX, initPage.pageY, 0) } else { this.registerHooks( this.scroll.hooks, this.scroll.hooks.eventTypes.beforeInitialScrollTo, (position: { x: number; y: number }) => { this.initialised = true position.x = initPage.x position.y = initPage.y } ) } this.startPlay() } private computeThreshold() { const threshold = this.options.threshold // Integer if (threshold % 1 === 0) { this.thresholdX = threshold this.thresholdY = threshold } else { // decimal const { width, height } = this.pages.getPageStats() this.thresholdX = Math.round(width * threshold) this.thresholdY = Math.round(height * threshold) } } private cloneFirstAndLastSlidePage(slideContent: HTMLElement) { const children = slideContent.children const preprendDOM = children[children.length - 1].cloneNode( true ) as HTMLElement const appendDOM = children[0].cloneNode(true) as HTMLElement prepend(preprendDOM, slideContent) slideContent.appendChild(appendDOM) this.cachedClonedPageDOM = [preprendDOM, appendDOM] } private removeClonedSlidePage(slideContent: HTMLElement) { // maybe slideContent has removed from DOM Tree const slidePages = (slideContent && slideContent.children) || [] if (slidePages.length) { this.cachedClonedPageDOM.forEach((el) => { removeChild(slideContent, el) }) } this.cachedClonedPageDOM = [] } private modifyCurrentPage(point: Position) { const { pageX: prevExposedPageX, pageY: prevExposedPageY, } = this.getCurrentPage() const newPage = this.nearestPage(point.x, point.y) this.setCurrentPage(newPage) /* istanbul ignore if */ if (this.contentChanged) { this.contentChanged = false return true } const { pageX: currentExposedPageX, pageY: currentExposedPageY, } = this.getCurrentPage() this.pageWillChangeTo(newPage) // loop is true, and one page becomes many pages when call bs.refresh if (this.oneToMorePagesInLoop) { this.oneToMorePagesInLoop = false return true } // loop is true, and many page becomes one page when call bs.refresh // if prevPage > 0, dispatch slidePageChanged and scrollEnd events /* istanbul ignore if */ if ( this.moreToOnePageInLoop && prevExposedPageX === 0 && prevExposedPageY === 0 ) { this.moreToOnePageInLoop = false return true } if ( prevExposedPageX !== currentExposedPageX || prevExposedPageY !== currentExposedPageY ) { // only trust pageX & pageY when loop is true const page = this.pages.getExposedPageByPageIndex( currentExposedPageX, currentExposedPageY ) this.scroll.trigger(this.scroll.eventTypes.slidePageChanged, page) } // triggered by resetLoop if (this.resetLooping) { this.resetLooping = false return } const changePage = this.pages.resetLoopPage() if (changePage) { this.resetLooping = true this.goTo(changePage.pageX, changePage.pageY, 0) // stop user's scrollEnd // since it is a seamless scroll return true } } private goTo(pageX: number, pageY: number, time?: number, easing?: EaseItem) { const newPage = this.pages.getInternalPage(pageX, pageY) const scrollEasing = easing || this.options.easing || ease.bounce const { x, y } = newPage const deltaX = x - this.scroll.scroller.scrollBehaviorX.currentPos const deltaY = y - this.scroll.scroller.scrollBehaviorY.currentPos /* istanbul ignore if */ if (!deltaX && !deltaY) { this.scroll.scroller.togglePointerEvents(true) return } time = time === undefined ? this.getEaseTime(deltaX, deltaY) : time this.scroll.scroller.scrollTo(x, y, time, scrollEasing) } private flickHandler() { const { scrollBehaviorX, scrollBehaviorY } = this.scroll.scroller const { currentPos: currentPosX, startPos: startPosX, direction: directionX, } = scrollBehaviorX const { currentPos: currentPosY, startPos: startPosY, direction: directionY, } = scrollBehaviorY const { pageX, pageY } = this.pages.currentPage let time = this.getEaseTime( currentPosX - startPosX, currentPosY - startPosY ) this.goTo(pageX + directionX, pageY + directionY, time) } private getEaseTime(deltaX: number, deltaY: number): number { return ( this.options.speed || Math.max( Math.max( Math.min(Math.abs(deltaX), 1000), Math.min(Math.abs(deltaY), 1000) ), 300 ) ) } private modifyScrollMetaHandler(scrollMeta: { newX: number newY: number time: number [key: string]: any }) { const { scrollBehaviorX, scrollBehaviorY, animater } = this.scroll.scroller const newX = scrollMeta.newX const newY = scrollMeta.newY const newPage = this.satisfyThreshold(newX, newY) || animater.forceStopped ? this.pages.getPageByDirection( this.nearestPage(newX, newY), scrollBehaviorX.direction, scrollBehaviorY.direction ) : this.pages.currentPage scrollMeta.time = this.getEaseTime( scrollMeta.newX - newPage.x, scrollMeta.newY - newPage.y ) scrollMeta.newX = newPage.x scrollMeta.newY = newPage.y scrollMeta.easing = this.options.easing || ease.bounce } private scrollHandler({ x, y }: Position) { if (this.satisfyThreshold(x, y)) { const newPage = this.nearestPage(x, y) this.pageWillChangeTo(newPage) } } private pageWillChangeTo(newPage: Page) { const changeToPage = this.pages.getWillChangedPage(newPage) if (!samePage(this.willChangeToPage, changeToPage)) { this.willChangeToPage = changeToPage this.scroll.trigger( this.scroll.eventTypes.slideWillChange, this.willChangeToPage ) } } private registerHooks(hooks: EventEmitter, name: string, handler: Function) { hooks.on(name, handler, this) this.hooksFn.push([hooks, name, handler]) } destroy() { const slideContent = this.scroll.scroller.content const { loop, autoplay } = this.options if (loop) { this.removeClonedSlidePage(slideContent) } if (autoplay) { clearTimeout(this.autoplayTimer) } this.hooksFn.forEach((item) => { const hooks = item[0] const hooksName = item[1] const handlerFn = item[2] if (hooks.eventTypes[hooksName]) { hooks.off(hooksName, handlerFn) } }) this.hooksFn.length = 0 } } ================================================ FILE: packages/slide/src/propertiesConfig.ts ================================================ const sourcePrefix = 'plugins.slide' const propertiesMap = [ { key: 'next', name: 'next', }, { key: 'prev', name: 'prev', }, { key: 'goToPage', name: 'goToPage', }, { key: 'getCurrentPage', name: 'getCurrentPage', }, { key: 'startPlay', name: 'startPlay', }, { key: 'pausePlay', name: 'pausePlay', }, ] export default propertiesMap.map((item) => { return { key: item.key, sourceKey: `${sourcePrefix}.${item.name}`, } }) ================================================ FILE: packages/vuepress-docs/docs/.vuepress/components/demo.vue ================================================ ================================================ FILE: packages/vuepress-docs/docs/.vuepress/components/qrcode.vue ================================================ ================================================ FILE: packages/vuepress-docs/docs/.vuepress/config.js ================================================ const path = require('path') const os = require('os') const fs = require('fs') function resolve(p) { return path.resolve(__dirname, '../../../', p) } module.exports = { base: '/docs/', publicPath: '/docs/', cache: false, head: [ ['link', { rel: 'shortcut icon', href: '/assets/bs.ico', type: 'images/x-icon' }], ['script', { src: 'https://www.googletagmanager.com/gtag/js?id=G-7E85TW7P27' }], ['script', { type: 'text/javascript' }, ` window.dataLayer = window.dataLayer || []; function gtag(){dataLayer.push(arguments);} gtag('js', new Date()); gtag('config', 'G-7E85TW7P27'); `] ], locales: { '/en-US/': { lang: 'en-US', title: 'BetterScroll 2.0', description: 'Make Scroll Perfect' }, '/zh-CN/': { lang: 'zh-CN', title: 'BetterScroll 2.0', description: 'Make Scroll Perfect' } }, themeConfig: { repo: 'ustbhuangyi/better-scroll', docsBranch: 'dev', docsDir: 'packages/vuepress-docs/docs', editLinks: true, smoothScroll: true, algolia: { apiKey: '93916bfd4dd5ed93f9b7c0d9c9854404', indexName: 'better-scroll' }, logo: 'https://dpubstatic.udache.com/static/dpubimg/t_L6vAgQ-E/logo.svg', locales: { '/zh-CN/': { label: '简体中文', selectText: '选择语言', nav: require('./nav/zh-CN.js'), lastUpdated: '上次更新', editLinkText: '在 GitHub 上编辑此页', sidebar: { '/zh-CN/guide/': require('./sidebar/guide.js')('zh-CN'), '/zh-CN/plugins/': require('./sidebar/plugins.js')('zh-CN'), '/zh-CN/FAQ/': require('./sidebar/FAQ.js')('zh-CN') } }, '/en-US/': { label: 'English', selectText: 'Languages', nav: require('./nav/en-US.js'), lastUpdated: 'Last Updated', editLinkText: 'Edit this page on GitHub', sidebar: { '/en-US/guide/': require('./sidebar/guide.js')('en-US'), '/en-US/plugins/': require('./sidebar/plugins.js')('en-US'), '/en-US/FAQ/': require('./sidebar/FAQ.js')('en-US') } } } }, configureWebpack: { module: { rules: [ { test: /\.tsx?$/, loader: 'ts-loader', options: { transpileOnly: true } } ] }, resolve: { extensions: ['.js', '.vue', '.json', '.ts'], alias: { common: resolve('examples/common') } } }, define: { LOCAL_IP: getIp() }, plugins: [ ['@vuepress/back-to-top', true], [ '@vuepress/register-components', { componentsDir: resolve('examples/vue/components') } ], [ '@vuepress/medium-zoom', { selector: '[data-zoomable]', // medium-zoom options here // See: https://github.com/francoischalifour/medium-zoom#options options: { margin: 16 } } ], require('./plugins/extract-code.js') ] } function getIp() { var networks = os.networkInterfaces() var found = '127.0.0.1' Object.keys(networks).forEach(function(k) { var n = networks[k] n.forEach(function(addr) { if (addr.family === 'IPv4' && !addr.internal) { found = addr.address } }) }) return found } function getPackagesName() { let ret let all = fs.readdirSync(resolve('../packages')) // drop hidden file whose name is startWidth '.' // drop packages which would not be published(eg: examples and docs) ret = all .filter(name => { const isHiddenFile = /^\./g.test(name) return !isHiddenFile }) .filter(name => { const isPrivatePackages = require(resolve( `../packages/${name}/package.json` )).private return !isPrivatePackages }) .map(name => { return require(resolve(`../packages/${name}/package.json`)).name }) return ret } getPackagesName().forEach(name => { module.exports.configureWebpack.resolve.alias[ name + '$' ] = `${name}/src/index.ts` }) ================================================ FILE: packages/vuepress-docs/docs/.vuepress/enhanceApp.js ================================================ import './public/assets/stylus/index.styl' import VTooltip from 'v-tooltip' export default ({Vue, options, router}) => { // // redirect to /zh-CN/ by default router.addRoutes([{ path: '/', redirect: '/zh-CN/' }]) // TODO Unified management of global components Vue.use(VTooltip) } ================================================ FILE: packages/vuepress-docs/docs/.vuepress/nav/en-US.js ================================================ module.exports = [ { text: 'Guide', link: '/en-US/guide/' }, { text: 'Plugin', link: '/en-US/plugins/' }, { text: 'FAQ', link: '/en-US/FAQ/' }, { text: 'Discuss', link: 'https://github.com/ustbhuangyi/better-scroll/issues'} ] ================================================ FILE: packages/vuepress-docs/docs/.vuepress/nav/zh-CN.js ================================================ module.exports = [ { text: '指南', link: '/zh-CN/guide/' }, { text: '插件', link: '/zh-CN/plugins/' }, { text: '常见问题', link: '/zh-CN/FAQ/' }, { text: '讨论', link: 'https://github.com/ustbhuangyi/better-scroll/issues'} ] ================================================ FILE: packages/vuepress-docs/docs/.vuepress/plugins/extract-code.js ================================================ const { fs, path } = require('@vuepress/shared-utils') function extractCodeFromVueSFC(md, options = {}) { const root = options.root || path.join(process.cwd(), '../') md.core.ruler.after('block', 'extract-code', function parser(state) { let tokens = state.tokens, tok, i; // modify html_block for (i = 0; i < tokens.length; i++) { tok = tokens[i]; if (tok.type === 'html_block' && isDemoBlock(tok)) { const handledTokens = getHandledTokens(tok) if (handledTokens.length) { tokens.splice(i, 1, ...handledTokens) i += handledTokens.length - 1 } } } function isDemoBlock(token) { let content = token.content let regex = /<\/demo>$/ return regex.test(content.trim()) } function getHandledTokens(htmlToken, toks) { const tokens = toks || [] let content = htmlToken.content let reg = /<<<(.*)(\?.*)?\b/ let matched = content.match(reg) if (!matched) { return tokens } tokens.length && tokens.pop() const contentBefore = content.substr(0, matched.index) const contentAfter = content.substr(matched.index + matched[0].length) let token = createToken('html_block', '', 0) token.content = contentBefore tokens.push(token) token = createToken('fence', 'code', 0) const rawPath = matched[1].trim().replace(/^@/, root) const filename = rawPath.split(/\?/).shift() const partName = rawPath.replace(filename, '').substr(1) token.info = filename.split('.').pop() content = fs.existsSync(filename) ? fs.readFileSync(filename).toString() : 'Not found: ' + filename if (partName) { const partReg = new RegExp(`<${partName}[\\s\\S]*`) const matched = content.match(partReg) if (matched) { content = matched[0] } // highlight stylus if (partName === 'style') { token.info = "styl" } } token.content = content token.markup = '```' tokens.push(token) token = createToken('html_block', '', 0) token.content = contentAfter tokens.push(token) return getHandledTokens(token, tokens) } function createToken(type, tag, nesting) { let token = new state.Token(type, tag, nesting); token.block = true; if (nesting < 0) { this.level--; } token.level = this.level; if (nesting > 0) { this.level++; } return token; } }) } module.exports = { name: 'extract-code-plugin', chainMarkdown(config) { config.plugin('extract-code') .use(extractCodeFromVueSFC) } } ================================================ FILE: packages/vuepress-docs/docs/.vuepress/public/assets/stylus/index.styl ================================================ div[class~="language-styl"]:before { content: 'stylus'; } .v-popover { line-height: 1 } // for v-tooltip .tooltip { display: block !important; z-index: 10000; } .tooltip .tooltip-inner { background: black; color: white; border-radius: 16px; padding: 5px 10px 4px; } .tooltip .popover .popover-inner { background: #f9f9f9; color: black; padding: 10px; border-radius: 5px; box-shadow: 0 5px 30px rgba(black, .1); } .tooltip .popover .popover-arrow { border-color: #f9f9f9 } .tooltip .tooltip-arrow { width: 0; height: 0; border-style: solid; position: absolute; margin: 5px; border-color: black; z-index: 1; } .tooltip[x-placement^="top"] { margin-bottom: 5px; } .tooltip[x-placement^="top"] .tooltip-arrow { border-width: 5px 5px 0 5px; border-left-color: transparent !important; border-right-color: transparent !important; border-bottom-color: transparent !important; bottom: -5px; left: calc(50% - 5px); margin-top: 0; margin-bottom: 0; } .tooltip[x-placement^="bottom"] { margin-top: 5px; } .tooltip[x-placement^="bottom"] .tooltip-arrow { border-width: 0 5px 5px 5px; border-left-color: transparent !important; border-right-color: transparent !important; border-top-color: transparent !important; top: -5px; left: calc(50% - 5px); margin-top: 0; margin-bottom: 0; } .tooltip[x-placement^="right"] { margin-left: 5px; } .tooltip[x-placement^="right"] .tooltip-arrow { border-width: 5px 5px 5px 0; border-left-color: transparent !important; border-top-color: transparent !important; border-bottom-color: transparent !important; left: -5px; top: calc(50% - 5px); margin-left: 0; margin-right: 0; } .tooltip[x-placement^="left"] { margin-right: 5px; } .tooltip[x-placement^="left"] .tooltip-arrow { border-width: 5px 0 5px 5px; border-top-color: transparent !important; border-right-color: transparent !important; border-bottom-color: transparent !important; right: -5px; top: calc(50% - 5px); margin-left: 0; margin-right: 0; } .tooltip.popover .popover-inner { background: #f9f9f9; color: black; padding:10px; border-radius: 5px; box-shadow: 0 5px 10px rgba(black, .3); } .tooltip.popover .popover-arrow { border-color: #f9f9f9; } .tooltip[aria-hidden='true'] { visibility: hidden; opacity: 0; transition: opacity .15s, visibility .15s; } .tooltip[aria-hidden='false'] { visibility: visible; opacity: 1; transition: opacity .15s; } ================================================ FILE: packages/vuepress-docs/docs/.vuepress/sidebar/FAQ.js ================================================ const FAQContent = { 'zh-CN': '常见问题', 'en-US': 'FAQ' } const getFAQSideBar = function (lang) { return [ { title: FAQContent[lang], collapsable: false, children: [ '', 'diagnosis' ] } ] } module.exports = getFAQSideBar ================================================ FILE: packages/vuepress-docs/docs/.vuepress/sidebar/guide.js ================================================ const guideContent = { 'zh-CN': '指南', 'en-US': 'Guide' } const baseScrollContent = { 'zh-CN': '核心滚动', 'en-US': 'Base Scroll' } const getGuideSideBar = function (lang) { return [ { title: guideContent[lang], collapsable: false, children: [ '', 'how-to-install', 'use' ] }, { title: baseScrollContent[lang], collapsable: false, children: [ 'base-scroll', 'base-scroll-options', 'base-scroll-api' ] } ] } module.exports = getGuideSideBar ================================================ FILE: packages/vuepress-docs/docs/.vuepress/sidebar/plugins.js ================================================ const introContent = { 'zh-US': '插件介绍', 'en-US': 'Plugins Guide' } const pluginContent = { 'zh-US': '插件', 'en-US': 'Plugins' } const highPluginContent = { 'zh-US': '插件的高阶使用', 'en-US': 'High Level Use Of Plugins' } const getPluginsSideBar = function (lang) { return [ { title: introContent[lang], collapsable: false, children: [ '', 'how-to-write', ] }, { title: pluginContent[lang], collapsable: false, children: [ 'mouse-wheel', 'observe-dom', 'observe-image', 'pulldown', 'pullup', 'scroll-bar', 'indicators', 'slide', 'wheel', 'zoom', 'nested-scroll', 'infinity', 'movable' ] }, // { // title: highPluginContent[lang], // collapsable: false, // children: [ // 'compose-plugins' // ] // } ] } module.exports = getPluginsSideBar ================================================ FILE: packages/vuepress-docs/docs/en-US/FAQ/README.md ================================================ # FAQ ### Why can't BetterScroll scroll is failed when initialization? BetterScroll scrolling principle is that the height/width of the `content` element exceeds the height/width of the `wrapper` element. Also, if your content element contains images of a non-fixed size, you must call the `refresh()` method to ensure that the height is calculated correctly after the image has been loaded. There is also a situation where the form element exists on the page. After the keyboard is popped up, the palatable height of the page is compressed, causing `bs` to not work properly, and the `refresh()` method is still need to be called. ### Why can't the click event in the BetterScroll area be triggered? By default, BetterScroll blocks the browser's native click event. If you want the click event to take effect, BetterScroll dispatches a click event and the `_constructed` of the event parameter is true. The configuration items are as follows: ```js import BScroll from '@better-scroll/core' let bs = new BScroll('./div', { click: true }) ``` ### Why does my BetterScroll listen for the `scroll` hook and the listener doesn't execute? BetterScroll uses the `probeType` configuration item to decide whether to dispatch the `scroll` hook because there is some performance penalty. When the `probeType` is `2`, the event will be dispatched in real time. When the `probeType` is `3`, the event will be dispatched during the `momentum` animation. The recommended setting is `3`. ```js import BScroll from '@better-scroll/core' let bs = new BScroll('./div', { probeType: 3 }) ``` ### Slide used horizontal scrolling, found that vertical scrolling in the slide area is invalid? If you want to keep your browser's native vertical scrolling, you need the following configuration items: ```js import BScroll from '@better-scroll/core' let bs = new BScroll('./div', { eventPassthrough: 'vertical' }) ``` ================================================ FILE: packages/vuepress-docs/docs/en-US/FAQ/diagnosis.md ================================================ # BetterScroll's "diagnosis" ### [Question 1] Why can't my BetterScroll work? The problem basically lies in the **Height Calculation Error**. First of all, you must have a clear understanding of the scrolling principle of `BetterScroll`. For vertical scrolling, simply the height of the `wrapper` container is greater than the height of the `content` content, and the `translateY` is modified to achieve the purpose of scrolling. The principle of horizontal scrolling is similar. Then the calculation **Scrollable Height** is the logic necessary for `BetterScroll`. The general logic is:   1. **Pictures with uncertain sizes** - **Reason** When js performs a calculation of the scrollable height, the image has not been rendered. - **Solution** Call `bs.refresh()` inside the callback function of the image's `onload` to ensure that the correct height of the image is calculated before calculating the **Scrollable Height**.   2. **Vue's keep-alive component** - **Scenes** Suppose there are two components of A and B wrapped by `keep-alive`, A component uses BetterScroll, does some operation in A component, pops up input keyboard, then enters B component, then returns to A component, `bs` is unable to scroll. - **Reason** Because Vue's keep-alive's cache and the input keyboard pops up, it compresses the height of the viewable area, causing the previously calculated scrollable height to be incorrect. - **Solution** You can call `bs.refresh()` on Vue's `activated` hook to recalculate the height or re-instantiate bs. ### [Question 2] Why do brower's vertical scrolling failed after I use BetterScroll to do horizontal scrolling? BetterScroll provides a feature of `slide`. If you implement a horizontal scrollin, such as `slide`. do vertical scrolling in the `slide` area, you can't bubble to the browser, so you can't manipulate the scroll bar of the native browser. - **Reason** The internal scrolling calculations of BetterScroll exist in the user's interaction. For example, the mobile terminal is the `touchstart/touchmove/touchend` event. The listeners of these events generally have the line `e.preventDefault()`, which will block the browser's default behavior so that the browser's scrollbar cannot be scrolled. - **Solution** Configure the `eventPassthrough` attribute. ```js   Let bs = new BScroll('.wrapper', {     eventPassthrough: 'vertical' // keep vertical native scrolling   }) ``` ### [Question 3] Why can't I pop up a pop-up window after using BetterScroll. - **Reason** **question 2** has been mentioned, it is caused by `e.preventDefault()` in touchstart. - **Solution** Option 1: Configure the `preventDefaultException` property. ```js let bs = new BScroll('.wrapper', { preventDefaultException: { className: /(^|\s)test(\s|$)/ } }) ``` `eventDefaultException` can be used to control the `e.preventDefault()` of the `touchstart` and `touchmove` events. If the above regular expression is used to check if the class name of the currently touched target element contains `test`, if passed, Then `e.preventDefault()` will not be called. Option 2: Configure the `preventDefault` property. ```js let bs = new BScroll('.wrapper', { preventDefault: false }) ``` `preventDefault` is set to `false`, there are some side effects, it is generally recommended to use **Option one**. :::warning The side effect is that the touch event may bubble up to the document, causing the document to be dragged. At this point you need to listen for the parent or ancestor element of the `wrapper` element, bind them to the touchmove event, and call `e.preventDefault()`. ::: ### [Question 4] Why are the listeners for all click events inside BetterScroll content not triggered? - **Reason** Still caused by `e.preventDefault()`. On the mobile side, if you call `e.preventDefault()` inside the logic of `touchstart/touchmove/touchend`, it will prevent the execution of the click event of it and its child elements. Therefore, BetterScroll internally manages the dispatch of the `click` event, you only need the `click` configuration item. - **Solution** Configure the `click` attribute. ```js   Let bs = new BScroll('.wrapper', { click: true   }) ``` ### [Question 5] Why is the click event dispatched twice when Nesting BetterScroll? - **Reason** As stated in **Question 4**, the BetterScroll dispatches a `click` event internally, and nested scenes must have two or more bs. - **Solution** You can manage the bubbling of events by instantiating inner BetterScroll's `stopPropagation` configuration item, or by instantiating inner BetterScroll's `click` configuration item to prevent multiple triggers of clicks. ```js let innerBS = new BScroll('.wrapper', { stopPropagation: true }) // or let innerBS = new BScroll('.wrapper', { click: false }) ``` ### [Question 6] Why do I listen to the scroll event of bs, why not execute the callback? - **Reason** BetterScroll does not dispatch the `scroll` event at any time because there is a performance penalty for getting the scroll position of bs. As for whether or not to distribute, it depends on the `probeType` configuration item. - **Solution** ```js   Let bs = new BScroll('.div', {     probeType: 3 // real-time dispatch   }) ``` ### [Question 7] In two vertically nested bs scenes, why move the inner bs will cause the outer layer to also be scrolled. - **Reason** The internal logic of BetterScroll is in the body of the listener function of the touch event. Since the touch event of the internal bs is triggered, it will naturally bubble to the outer bs. - **Solution** Since you know the reason, there are corresponding solutions. For example, when you scroll the **inner** `bs`, listen for the `scroll` event and call the **outer** `bs.disable()` to disable the **outer** `bs`. When the **inner** `bs` scrolls to the bottom, it means that you need to scroll the **outer** `bs` at this time. At this time, call the **outer** `bs.enable()` to activate the outer layer and call the **inner** `bs.disable(). ` to forbid inner scrolling. In fact, think about it, this interaction is consistent with the nested scrolling behavior of the `Web browser`, except that the browser handles the various scrolling nesting logic for you, and the BetterScroll requires your own dispatched events and exposed APIs to fulfill. > The [scroll](https://didi.github.io/cube-ui/example/#/scroll/v-scrolls) component of `cube-ui` gives a solution to this scenario. [Code is here](https://github.com/didi/cube-ui/blob/dev/src/components/scroll/scroll.vue) ### [Question 8] In the vertical bs nesting horizontal bs scene, why does the vertical movement of the horizontal bs area do not cause vertical scrolling of the outer vertical bs? - **Reason** The reason is similar to **Question 2**, because `e.preventDefault()` affects the default scrolling behavior, causing the outer bs to not trigger the touch event. - **Solution** The solution is to configure the `eventPassthrough` property of the inner bs to keep the default native vertical scrolling. ```js   Let innerBS = new BScroll('.wrapper', {     eventPassthrough: 'vertical' // keep vertical native scrolling   }) ``` ================================================ FILE: packages/vuepress-docs/docs/en-US/README.md ================================================ --- home: true heroText: BetterScroll 2.0 actionText: Getting Started → actionLink: /en-US/guide/ features: - title: Smooth scrolling effect details: Aimed at solving scrolling on the mobile side (PC supported already). - title: Zero dependence details: Based on native JS implementation, it does not depend on any framework. Perfect for Vue, React and other MVVM frameworks. - title: Pluggable details: Plugin support, such as Picker, PullUpLoad, PullDownRefresh, Zoom, Mouse-Wheel, Slide, Movable, Indicators, Parallax Scrolling, Magnifier and so on. footer: MIT Licensed | Copyright © 2018-present ustbhuangyi and theniceangel --- ================================================ FILE: packages/vuepress-docs/docs/en-US/guide/README.md ================================================ # Introduction ## What is BetterScroll ? BetterScroll is a plugin which is aimed at solving scrolling circumstances on the mobile side (PC supported already). The core is inspired by the implementation of [iscroll](https://github.com/cubiq/iscroll), so the APIs of BetterScroll are compatible with iscroll on the whole. What's more, BetterScroll also extends some features and optimizes for performance based on iscroll. BetterScroll is implemented with plain JavaScript, which means it's dependency free. ## Demo demo ## Getting started The most common application scenario of BetterScroll is list scrolling. Let's see its HTML: ```html
      • ...
      • ...
      • ...
      ``` In the code above, BetterScroll is applied to the outer `wrapper` container, and the scrolling part is `content` element. Pay attention that BetterScroll handles the scroll of the first child element (content) of the container (`wrapper`) by default, which means other elements will be ignored. The simplest initialization code is as follow: ```javascript import BScroll from '@better-scroll/core' let wrapper = document.querySelector('.wrapper') let scroll = new BScroll(wrapper) ``` BetterScroll provides a class whose first parameter is a plain DOM object when instantiated. Certainly, BetterScroll inside would try to use querySelector to get the DOM object. :::warning In BetterScroll 2.X, we split the 1.X-coupled feature into the plugin to achieve on-demand loading and reduce the volume of the package. Therefore, `@better-scroll/core` only provides the most core scrolling capabilities. If you want to implement the **pull-up load**, **pull-down refresh** function, you need to use the corresponding [plugin] (/en-US/plugins). ::: :::tip BetterScroll v2.0.4 can use [specifiedIndexAsContent](./base-scroll-options.html#specifiedindexascontent-2-0-4) to specify a child element of the wrapper as BetterScroll's content. ::: ## The principle of scrolling Many developers have used BetterScroll, but the most common problem they have met is: > I have initiated BetterScroll, but the content can't scroll. The phenomenon is 'the content can't scroll' and we need to figure out the root cause. Before that, let's take a look at the browser's scrolling principle: everyone can see the browser's scroll bar. When the height of the page content exceeds the viewport height, the vertical scroll bar will appear; When the width of page content exceeds the viewport width, the horizontal bar will appear. That is to say, when the viewport can't display all the content, the browser would guide the user to scroll the screen with scroll bar to see the rest of content. The principle of BetterScroll is samed as the browser. We can feel about this more obviously using a picture: schematic The green part is the wrapper, also known as the parent container, which has **fixed height**. The yellow part is the content, which is **the first child element** of the parent container and whose height would grow with the size of its content. Then, when the height of the content doesn't exceed the height of the parent container, the content would not scroll. Once exceeded, the content can be scrolled. That is the principle of BetterScroll. ## Using BetterScroll with MVVM frameworks I wrote an article [When BetterScroll meets Vue](https://zhuanlan.zhihu.com/p/27407024) (in Chinese). I also hope that developers can contribute to share the experience of using BetterScroll with other frameworks. A fantastic mobile ui lib implement by Vue: [cube-ui](https://github.com/didi/cube-ui/) ## Using BetterScroll in the real project If you want to learn how to use BetterScroll in the real project,you can learn my two practical courses(in Chinese)。 [High imitating starvation takeout practical course base on Vue.js](https://coding.imooc.com/class/74.html) [Project demo address](http://ustbhuangyi.com/sell/) ![QR Code](https://qr.api.cli.im/qr?data=http%253A%252F%252Fustbhuangyi.com%252Fsell%252F%2523%252Fgoods&level=H&transparent=false&bgcolor=%23ffffff&forecolor=%23000000&blockpixel=12&marginblock=1&logourl=&size=280&kid=cliim&key=686203a49c4613080b5b3004323ff977) [Music App advanced practical course base on Vue.js](http://coding.imooc.com/class/107.html) [Project demo address](http://ustbhuangyi.com/music/) ![QR Code](https://qr.api.cli.im/qr?data=http%253A%252F%252Fustbhuangyi.com%252Fmusic%252F&level=H&transparent=false&bgcolor=%23ffffff&forecolor=%23000000&blockpixel=12&marginblock=1&logourl=&size=280&kid=cliim&key=731bbcc2b490454d2cc604f98539952c) ================================================ FILE: packages/vuepress-docs/docs/en-US/guide/base-scroll-api.md ================================================ # API If you want to understand BetterScroll thoroughly, you need to understand the common properties of its instances, the flexible methods, and the hooks provided. ## Properties Sometimes we want to do some extensions based on BetterScroll, we need to understand some of the properties of BetterScroll. Here are a few common properties. ### x - **Type**: `number`. - **Usage**: scroll horizontal axis coordinate. ### y - **Type**: `number`. - **Usage**: scroll vertical axis coordinate. ### maxScrollX - **Type**: `number` - **Usage**: max scrollable horizontal coordinate. - **Note**: horizontal scroll range is [minScrollX, maxScrollX], and maxScrollX is negative value. ### minScrollX - **Type**: `number` - **Usage**: min scrollable horizontal coordinate. - **Note**: horizontal scroll range is [minScrollX, maxScrollX], and minScrollX is positive value. ### maxScrollY - **Type**: `number` - **Usage**: max scrollable vertical coordinate - **Note**: vertical scroll range is [minScrollY, maxScrollY], and maxScrollY is negative value. ### minScrollY - **Type**: `number` - **Usage**: min scrollable vertical coordinate - **Note**: vertical scroll range is [minScrollY, maxScrollY], and minScrollY is positive value. ### movingDirectionX - **Type**: `number` - **Usage**: estimate the moving direction on horizontal is left or right. - **Note**: -1 means finger moves from left to right, 1 means moving from right to left, 0 means haven't moved. ### movingDirectionY - **Type**: `number` - **Usage**: estimate the moving direction on vertical is up or down during scrolling. - **Note**: -1 means finger moves from up to down, 1 means from down to up, 0 means haven't moved. ### directionX - **Type**: `number` - **Usage**: estimate the moving direction on horizontal between start position and end position is left or right. - **Note**: -1 means finger moves from up to down, 1 means from down to up, 0 means haven't moved. ### directionY - **Type**: `number` - **Usage**: estimate the moving direction on vertical between start position and end position is up or down. - **Note**: -1 means finger moves from up to down, 1 means from down to up, 0 means haven't moved. ### enabled - **Type**: `boolean`, - **Usage**: estimate whether the current scroll is enabled. ### pending - **Type**: `boolean`, - **Usage**: estimate whether the current scroll is animating. ## Methods BetterScroll provides a lot of flexible APIs, which are used when we implement some features based on BetterScroll, and understanding them will help to meet more complex requirements. ### refresh() - **Arguments**: none. - **Return**: none. - **Usage**: recalculate BetterScroll to ensure scroll work properly when the structure of DOM changes. ### scrollTo(x, y, time, easing, extraTransform) - **Arguments**: - `{number} x`, horizontal axis distance. (unit: px) - `{number} y`, vertical axis distance. (unit: px) - `{number} time`, animation duration. (unit: ms) - `{Object} easing function`, usually don't suggest modifying. If you really need to modify, please refer `packages/shared-utils/src/ease.ts`'s of source code - `{Object} extraTransform`, you only need to pass in this parameter if you want to modify some other properties of the CSS transform. The structure is as follows: ```js let extraTransform = { // start point start: { scale: 0 }, // end poinnt end: { scale: 1.1 } } bs.scrollTo(0, -60, 300, undefined, extraTransform) ``` - **Return**: none. - **Usage**: scroll to specified position. ### scrollBy(x, y, time, easing) - **Arguments**: - `{number} x`, horizontal axis changed distance. (unit: px) - `{number} y`, vertical axis changed distance. (unit: px) - `{number} time`, animation duration. (unit: ms) - `{Object} easing function`, usually don't suggest modifying. If you really need to modify, please refer `packages/shared-utils/src/ease.ts`. - **Return**: none. - **Usage**: scroll to specified position based on current position. ### scrollToElement(el, time, offsetX, offsetY, easing) - **Arguments**: - `{DOM | string} el`, target element. If the value is a string, we will try to use querySelector get the DOM element. - `{number} time`, animation duration. (unit ms) - `{number | boolean}` offsetX, the x offset to target element,If the value is true, scroll to the center of target element. - `{number | boolean}` offsetY, the y offset to target element,If the value is true, scroll to the center of target element. - `{Object} easing function`, usually don't suggest modifying. If you really need to modify, please refer `packages/shared-utils/src/ease.ts`. - **Return**: none. - **Usage**: scroll to target element. ### stop() - **Arguments**: none. - **Return**: none. - **Usage**: stop the scroll animation immediately. ### enable() - **Arguments**: none. - **Return**: none. - **Usage**: enable BetterScroll. It's enabled by default. ### disable() - **Arguments**: none. - **Return**: none. - **Usage**: disable BetterScroll. And it will make the callbacks of DOM events don't response. ### destroy() - **Arguments**: none. - **Return**: none. - **Usage**: destroy BetterScroll,remove events and free some memory when the scroll is not needed anymore. ### on(type, fn, context) - **Arguments**: - `{string} type`, event - `{Function} fn`, callback - `{Object} context`,default is `this`. - **Return**: none - **Usage**: listen for a hook on the current BScroll, such as "scroll", "scrollEnd" and so on. - **Example**: ```javascript import BScroll from '@BetterScroll/core' let scroll = new BScroll('.wrapper', { probeType: 3 }) function onScroll(pos) { console.log(`Now position is x: ${pos.x}, y: ${pos.y}`) } scroll.on('scroll', onScroll) ``` ### once(type, fn, context) - **Arguments**: - `{string} type`, event - `{Function} fn`, callback - `{Object} context`, default is `this`. - **Return**: none - **Usage**: listen for a custom event, but only once. The listener will be removed once it triggers for the first time. ### off(type, fn) - **Arguments**: - `{string} type`, event - `{Function} fn`, callback - **Return**: none - **Usage**: remove custom event listener. Only remove the listener for that specific callback. - **Example**: ```javascript import BScroll from '@BetterScroll/core' let scroll = new BScroll('.wrapper', { probeType: 3 }) function handler() { console.log('bs is scrolling now') } scroll.on('scroll', handler) scroll.off('scroll', handler) ``` ## Events VS Hooks Based on the 2.x architecture design and compatibility with 1.x events, we have extended two concepts-"**Events**" and "**Hooks**". Basically, they are all instances of `EventEmitter`, but they are called differently. Let's explain it from the excerpted source code below: ```typescript export default BScrollCore extends EventEmitter { hooks: EventEmitter } ``` - **BScrollCore** It inherits EventEmitter itself. we all call it "**event**". ```js import BScroll from '@better-scroll/core' let bs = new BScroll('.wrapper', {}) // listen bs scroll event bs.on('scroll', () => {}) // listen bs refresh event bs.on('refresh', () => {}) ``` - **BScrollCore.hooks** Hooks are also instances of EventEmitter. we all call it "**hook**". ```js import BScroll from '@better-scroll/core' let bs = new BScroll('.wrapper', {}) // tap bs refresh hook bs.hooks.on('refresh', () => {}) // tap bs enable hook bs.hooks.on('enable', () => {}) ``` I believe everyone now has a better distinction between the two. "**Event**" is for the compatibility of 1.x. Users generally pay attention to the distribution of events, but if you want to write a plugin, you should focus on "**hooks**". ## Events In 2.0, BetterScroll events are almost same with 1.x events. Only BetterScroll will dispatch "**events**". If you need to expose events when writing plugins, you should also dispatch them through BetterScroll. [details is here](../plugins/how-to-write.html), the current events are divided into the following types: - **refresh** - **Trigger timing**: BetterScroll recalculate ```js import BetterScroll from '@better-scroll/core' const bs = new BetterScroll('.wrapper', {}) bs.on('refresh', () => {}) ``` - **enable** - **Trigger timing**: BetterScroll is enabled and starts to respond to user interaction ```js bs.on('enable', () => {}) ``` - **disable** - **Trigger timing**: BetterScroll is disabled and no longer responds to user interaction ```js bs.on('disable', () => {}) ``` - **beforeScrollStart** - **Trigger timing**: When the user's finger is placed on the scroll area ```js bs.on('beforeScrollStart', () => {}) ``` - **scrollStart** - **Trigger timing**: Content element meets the scrolling conditions and will start scrolling ```js bs.on('scrollStart', () => {}) ``` - **scroll** - **Trigger timing**: It is scrolling ```js bs.on('scroll', (position) => { console.log(position.x, position.y) }) ``` - **scrollEnd** - **Trigger timing**: End of scrolling, or force a content that is scrolling to stop ```js bs.on('scrollEnd', () => {}) ``` - **scrollCancel** - **Trigger timing**: Scroll cancel ```js bs.on('scrollCancel', () => {}) ``` - **touchEnd** - **Trigger timing**: User finger leaves the scroll area ```js bs.on('touchEnd', () => {}) ``` - **flick** - **Trigger timing**: User triggered flick operation ```js bs.on('flick', () => {}) ``` - **destroy** - **Trigger timing**: BetterScroll destroyed ```js bs.on('destroy', () => {}) ``` - **contentChanged** - **Trigger timing**: When calling `bs.refresh()`, it is detected that the content DOM has become other elements ```typescript // bs version >= 2.0.4 bs.on('contentChanged', (newContent: HTMLElement) => {}) ``` The following events must be registered for the **plugin** in parentheses to be dispatched: - **alterOptions(__mouse-wheel__)** - **Trigger timing**: mouse-wheel scroll starts ```js import BetterScroll from '@better-scroll/core' import MouseWheel from '@better-scroll/mouse-wheel' BetterScroll.use(MouseWheel) const bs = new BetterScroll('.wrapper', { mouseWheel: true }) bs.on('alterOptions', (mouseWheelOptions) => { /** * mouseWheelOptions.speed * mouseWheelOptions.invert * mouseWheelOptions.easeTime * mouseWheelOptions.discreteTime * mouseWheelOptions.throttleTime * mouseWheelOptions.dampingFactor **/ // please see details in mouse-wheel plugin doc }) ``` - **mousewheelStart(__mouse-wheel__)** - **Trigger timing**: mouse-wheel scroll starts ```js import BetterScroll from '@better-scroll/core' import MouseWheel from '@better-scroll/mouse-wheel' BetterScroll.use(MouseWheel) const bs = new BetterScroll('.wrapper', { mouseWheel: true }) bs.on('mousewheelStart', () => {}) ``` - **mousewheelMove(__mouse-wheel__)** - **Trigger timing**: mouse-wheel is scrolling ```js bs.on('mousewheelMove', () => {}) ``` - **mousewheelEnd(__mouse-wheel__)** - **Trigger timing**: mouse-wheel scrollEnd ```js bs.on('mousewheelEnd', () => {}) ``` - **pullingDown(__pull-down__)** - **Trigger timing**: When the top pull-down distance exceeds the threshold ```js import BetterScroll from '@better-scroll/core' import PullDown from '@better-scroll/pull-down' BetterScroll.use(PullDown) const bs = new BetterScroll('.wrapper', { pullDownRefresh: true }) bs.on('pullingDown', () => { await fetchData() bs.finishPullDown() }) ``` - **pullingUp(__pull-up__)** - **Trigger timing**: When the bottom pull-up distance exceeds the threshold ```js import BetterScroll from '@better-scroll/core' import PullUp from '@better-scroll/pull-up' BetterScroll.use(PullUp) const bs = new BetterScroll('.wrapper', { pullUpLoad: true }) bs.on('pullingUp', () => { await fetchData() bs.finishPullUp() }) ``` - **slideWillChange(__slide__)** - **Trigger timing**: The slide is about to switch page ```js import BetterScroll from '@better-scroll/core' import Slide from '@better-scroll/slide' BetterScroll.use(Slide) const bs = new BetterScroll('.wrapper', { slide: true, momentum: false, bounce: false, probeType: 2 }) bs.on('slideWillChange', (page) => { // Page about to switch console.log(page.pageX, page.pageY) }) ``` - **beforeZoomStart(__zoom__)** - **Trigger timing**: When two fingers touch the zoom element ```js import BetterScroll from '@better-scroll/core' import Zoom from '@better-scroll/zoom' BetterScroll.use(Zoom) const bs = new BetterScroll('.wrapper', { zoom: true }) bs.on('beforeZoomStart', () => {}) ``` - **zoomStart(__zoom__)** - **Trigger timing**: Two-finger zoom distance exceeds the minimum threshold ```js bs.on('zoomStart', () => {}) ``` - **zooming(__zoom__)** - **Trigger timing**: While the two-finger zoom behavior is in progress ```js bs.on('zooming', ({ scale }) => { // current scale }) ``` - **zoomEnd(__zoom__)** - **Trigger timing**: After the two-finger zoom action ends ```js bs.on('zoomEnd', ({ scale }) => {}) ``` ## Hooks A hook is a concept extended from version 2.0. Its essence is the same as an event. It is an instance of EventEmitter, which is a typical subscription publishing model. As the smallest scroll unit, BScrollCore also has many functional classes inside. Each functional class has a property called hooks, which bridges the communication between different classes. If you want to write a complex plugin, hooks must be mastered. - **BScrollCore.hooks** - **beforeInitialScrollTo** - **Trigger timing**: After initial loading the plugin, you need to scroll to the specified position - **Arguments**: position object - `{ x: number, y: number }` - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) bs.hooks.on('beforeInitialScrollTo', (postion) => { postion.x = 0 position.y = -200 // Initialize scroll to -200 }) ``` - **refresh** - **Trigger timing**: Recalculate BetterScroll - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) bs.hooks.on('refresh', () => { console.log('refreshed') }) ``` - **enable** - **Trigger timing**: Enable BetterScroll to respond to user behavior - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) bs.hooks.on('enable', () => { console.log('enabled') }) ``` - **disable** - **Trigger timing**: Disable BetterScroll and no longer respond to user behavior - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) bs.hooks.on('disable', () => { console.log('disabled') }) ``` - **destroy** - **Trigger timing**: Destroy BetterScroll - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) bs.hooks.on('destroy', () => { console.log('destroyed') }) ``` - **contentChanged** - **Trigger timing**:When calling `bs.refresh()`, it is detected that the content DOM has become other elements - **Usage** ```typescript import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) // bs version >= 2.0.4 bs.hooks.on('contentChanged', (newContent: HTMLElement) => { console.log(newContent) }) ``` - **ActionsHandler.hooks** - **beforeStart** - **Trigger timing**: Just respond to the touchstart event, but the position of the finger on the screen has not been recorded - **Arguments**: event object - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actionsHandler.hooks hooks.on('beforeStart', (event) => { console.log(event.target) }) ``` - **start** - **Trigger timing**: After recording the position of the finger on the screen, touchmove will be triggered - **Arguments**: event object - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actionsHandler.hooks hooks.on('start', (event) => { console.log(event.target) }) ``` - **move** - **Trigger timing**: Responding to the touchmove event, after recording the position of the finger on the screen - **Arguments**: Objects with the following properties - `{ number } deltaX`: x offset - `{ number } deltaY`: y offset - `{ event } e`: event object - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actionsHandler.hooks hooks.on('move', ({ deltaX, deltaY, e }) => {}) ``` - **end** - **Trigger timing**: Responding to touchend event - **Arguments**: event object - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actionsHandler.hooks hooks.on('end', (event) => {}) ``` - **click** - **Trigger timing**: Trigger the click event - **Arguments**: event object - **ScrollerActions.hooks** - **start** - **Trigger timing**: After recording all the scrolling initial information - **Arguments**: event object - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actions.hooks hooks.on('start', (event) => { console.log(event.target) }) ``` - **beforeMove** - **Trigger timing**: Before checking whether it is legal scrolling - **Arguments**: event object - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actions.hooks hooks.on('beforeMove', (event) => { console.log(event.target) }) ``` - **scrollStart** - **Trigger timing**: scroll is abount to start - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actions.hooks hooks.on('scrollStart', () => {}) ``` - **scroll** - **Trigger timing**: It is scrolling - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actions.hooks hooks.on('scroll', () => {}) ``` - **beforeEnd** - **Trigger timing**: The touchend event callback has just been executed, but the final position has not been updated - **Arguments**: event object - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actions.hooks hooks.on('beforeEnd', (event) => { console.log(event) }) ``` - **end** - **Trigger timing**: Just execute the touchend event callback and update the scroll direction - **Arguments**: Two Arguments, the first is the event object, the second is the current position - `{ event } e`: event object - `{ x: number, y: number } postion`: current position - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actions.hooks hooks.on('end', (e, postion) => { console.log(e) }) ``` - **scrollEnd** - **Trigger timing**: Scrolling is about to end, but you still need to verify whether a scrolling behavior triggers flick and momentum behaviors. - **Arguments**: Two Arguments, the first is the current position, the second is the animation duration - `{ x: number, y: number } postion`: current position - `{ number } duration`: animation duration - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actions.hooks hooks.on('beforeEnd', (pos, duration) => { console.log(pos) }) ``` - **coordinateTransformation** - **Trigger timing**: After calculating the offset of the user's finger, before scrolling occurs - **Arguments**: transformateDeltaData object - `{ deltaX: number, deltaY: number } transformateDeltaData` - **Usage** ```js let bs = new BScroll('.wrapper', { quadrant: 1 // default value }) bs.scroller.actions.hooks.on( 'coordinateTransformation', (transformateDeltaData) => { // get user finger moved distance const originDeltaX = transformateDeltaData.deltaX const originDeltaY = transformateDeltaData.deltaY // apply transformation transformateDeltaData.deltaX = originDeltaY transformateDeltaData.deltaY = originDeltaX // transformateDeltaData.deltaX will be used as content DOM style's translateX // transformateDeltaData.deltaY will be used as content DOM style's translateY } ) ``` This hook is usually to fix the logic of user-defined displacement transformation when the ancestor element of the wrapper DOM of BetterScroll is rotated. In most cases, it only needs to be configured [quadrant](./base-scroll-options.html#quadrant). - **Behavior.hooks** - **beforeComputeBoundary** - **Trigger timing**: About to calculate the scroll boundary - **Arguments**: boundary object - `{ minScrollPos: number, maxScrollPos: number } boundary` - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.scrollBehaviorX.hooks hooks.on('beforeComputeBoundary', () => {}) ``` - **computeBoundary** - **Trigger timing**: Calculate the scroll boundary - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.scrollBehaviorX.hooks hooks.on('computeBoundary', (boundary) => { // The maximum value of the upper boundary, the more positive, the greater the pull down console.log(boundary.minScrollPos) // The minimum value of the lower boundary, the more negative, the farther you roll console.log(boundary.maxScrollPos) }) ``` - **momentum** - **Trigger timing**: Meet the conditions for triggering momentum animation, and before calculating distance - **Arguments**: Two Arguments, the first is the momentumData object, the second is the scroll offset - `{ destination: number, duration: number, rate: number} momentumData`: destination is the target position, duration is the easing time, rate is the slope - `{ number } distance`: Scroll offset to trigger momentum - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.scrollBehaviorX.hooks hooks.on('momentum', (momentumData, distance) => {}) ``` - **end** - **Trigger timing**: Does not meet the conditions for triggering momentum animation - **Arguments**: momentumInfo object - `{ destination: number, duration: number} momentumInfo`: destination is the target position, duration is the easing time - **Usage** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.scrollBehaviorX.hooks hooks.on('end', (momentumInfo) => { console.log(momentumInfo.destination) console.log(momentumInfo.duration) }) ``` - **Animation.hooks(useTransition: false)** - **forceStop** - **Trigger timing**: Force a scrolling bs to stop - **Arguments**: position object - `{ x: number, y: number } position` - **move** - **Trigger timing**: Scrolling - **Arguments**: position object - `{ x: number, y: number } position` - **end** - **Trigger timing**: Scroll ended - **Arguments**: position object - `{ x: number, y: number } position` - **Transition.hooks(useTransition: true)** - **forceStop** - **Trigger timing**: Force a bs that is doing animation to stop - **Arguments**: position object - `{ x: number, y: number } position` - **move** - **Trigger timing**: Scrolling - **Arguments**: position object - `{ x: number, y: number } position` - **end** - **Trigger timing**: Scroll ended - **Arguments**:position object - `{ x: number, y: number } position` - **time** - **Trigger timing**: Before the CSS3 transition started, the wheel plugin listened to the hook - **Arguments**: CSS3 transition duration ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.animater.hooks hooks.on('time', (duration) => { console.log(duration) // 800 }) ``` - **timeFunction** - **Trigger timing**: Before the CSS3 transition started, the wheel plugin listened to the hook - **Arguments**: CSS3 transition-timing-function ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.animater.hooks hooks.on('timeFunction', (easing) => { console.log(easing) // cubic-bezier(0.1, 0.7, 1.0, 0.1) }) ``` - **Translater.hooks** - **beforeTranslate** - **Trigger timing**: Before modifying the transform style of the content element, the zoom plugin listened to the hook - **Arguments**: The first is the transformStyle array, the second is the point object - `{ ['translateX(0px)'|'translateY(0px)'] } transformStyle`: The property value corresponding to the current transform - `{ x: number, y: number } point`: x corresponds to the value of translateX, y corresponds to the value of translateY ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.translater.hooks hooks.on('beforeTranslate', (transformStyle, point) => { transformStyle.push('scale(1.2)') console.log(transformStyle) // ['translateX(0px)', 'translateY(0px)', 'scale(1.2)'] console.log(point) // { x: 0, y: 0 } }) ``` - **translate** - **Trigger timing**: After modifying the transform style of the content element, the wheel plugin listened to the hook - **Arguments**: point object - `{ x: number, y: number } point`: x corresponds to the value of translateX, y corresponds to the value of translateY ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.translater.hooks hooks.on('translate', (point) => { console.log(point) // { x: 0, y: 0 } }) ``` - **Scroller.hooks** - **beforeStart** same with `ScrollerActions.hooks.start` - **beforeMove** same with `ScrollerActions.hooks.beforeMove` - **beforeScrollStart** same with `ScrollerActions.hooks.start` - **scrollStart** same with `ScrollerActions.hooks.scrollStart` - **scroll** - **Trigger timing**: Scrolling - **Arguments**: position object - `{ x: number, y: number } position` - **beforeEnd** same with `ScrollerActions.hooks.beforeEnd` - **touchEnd** - **Trigger timing**: User finger leaves the scroll area ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('touchEnd', () => { console.log('your finger has leave') }) ``` - **end** - **Trigger timing**: After touchEnd, it is triggered before verifying click. The pull-down plugin is implemented based on this hook - **Arguments**: position object - `{ x: number, y: number } position` ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('end', (position) => { console.log(position.x) console.log(position.y) }) ``` - **scrollEnd** - **Trigger timing**: Scroll ended - **Arguments**: position object - `{ x: number, y: number } position` - **resize** - **Trigger timing**: window size changed ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('resize', () => { console.log("window's size has changed") }) ``` - **flick** - **Trigger timing**: Finger flicking detected ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('flick', () => {}) ``` - **scrollCancel** - **Trigger timing**: Scroll canceled or did not happen - **momentum** - **Trigger timing**: Momentum displacement is about to begin, and the slide plugin listens to the hook - **Arguments**: scrollMetaData object - `{ time: number, easing: EaseItem, newX: number, newY: number }`: time is the duration of the animation, easing is the easing function, newX and newY are the end points ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('momentum', (scrollMetaData) => { scrollMetaData.newX = 0 scrollMetaData.newY = -200 }) ``` - **scrollTo** - **Trigger timing**: Triggered when the bs.scrollTo method is called - **Arguments**: endPoint object - `{ x: number, y: number } endPoint` ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('scrollTo', (endPoint) => { console.log(endPoint.x) console.log(endPoint.y) }) bs.scrollTo(0, -200) ``` - **scrollToElement** - **Trigger timing**: Triggered when the bs.scrollToElement method is called, and the wheel plugin listens to the hook - **Arguments**: The first is the target DOM object, the second is the coordinates of the end point - `{ HTMLElment } el` - `{ top: number, left: number } postion` ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('scrollToElement', (el, pos) => { console.log(el) console.log(pos.left) console.log(pos.top) }) bs.scrollToElement('.some-item', 300, true, true) ``` - **beforeRefresh** - **Trigger timing**: Before behavior calculates the boundary, the slide plugin listens to the hook ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('beforeRefresh', () => {}) ``` ::: tip If you are careful, you will find that some Scroller.hooks have exactly the same functions as ScrollActions.hooks. In fact, we internally use a [hook bubbling](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/core/src/utils/bubbling.ts) strategy to proxy the hooks of the inner function classes to the BetterScroll Instance in the form of bubbling to be compatible with the use of 1.x. ::: ================================================ FILE: packages/vuepress-docs/docs/en-US/guide/base-scroll-options.md ================================================ # Options BetterScroll supports rich options configuration, you can pass them in the second parameter when initializing, for example: ```js import BScroll from '@better-scroll/core' let scroll = new BScroll('.wrapper',{ scrollY: true, click: true }) ``` This implements a list of vertical clickable scrolling effects. so let's list the parameters supported by BetterScroll. ## startX - **Type**: `number` - **Default**: `0` - **Details**: Initialize the postion in the horizontal axis direction. ## startY - **Type**: `number` - **Default**: `0` - **Details**: Initialize the postion in the vertical axis direction. ## scrollX - **Type**: `boolean` - **Default**: `false` - **Usage**: When set to true, horizontal scrolling would be enabled - **Note**: This configuration is invalid when setting [eventPassthrough](./base-scroll-options.html#eventpassthrough) to 'horizontal'. ## scrollY - **Type**: `boolean` - **Default**: `true` - **Usage**: When set to true, vertical scrolling would be enabled - **Note**: This configuration is invalid when setting [eventPassthrough](./base-scroll-options.html#eventpassthrough) to 'vertical'. ## freeScroll - **Type**: `boolean` - **Default**: `false` - **Usage**: By default, because human fingers cannot perform absolute vertical or horizontal movement, there will be horizontal and vertical offsets during a finger operation. The internal default will abandon the smaller offset direction , Keep scrolling in the other direction. But in some scenes, we need to calculate the horizontal and vertical finger offset distances at the same time, instead of only calculating the direction with a larger offset. At this time, we only need to set `freeScroll` to true. - **Note**: This configuration is invalid when [eventPassthrough](./base-scroll-options.html#eventpassthrough) isn't set to empty. - **Examples** ```js // finger startpoint -> e1: { pageX: 120, pageY: 120 } // finger endpoint -> e2: { pageX: 121, pageY: 140 } // offsetX: e2.pageX - e1.pageX = 1 // offsetY: e2.pageY - e1.pageY = 20 // if freeScroll is false, due to offsetY > offsetX + directionLockThreshold // offsetX is fixed to be 0, only calculate offsetY, thus do a vertical scroll! ``` ## directionLockThreshold - **Type**: `number` - **Default**: `5` - **Usage**: when `freeScroll` is false, we need to lock the scrolling only in one direction, we calculate the numerical difference between the absolute values of horizontal axis and vertical axis' scrolling distance at the initialization time of scrolling. When the value of the numerical difference is greater than `directionLockThreshold`, the lock direction can be determined. - **Note**: If [eventPassthrough](./base-scroll-options.html#eventpassthrough) is set, `directionLockThreshold` is invalid and will always be 0. ## eventPassthrough - **Type**: `string` - **Default**: `''` - Optional value: `vertical | horizontal` - **Usage**: Sometimes we want to preserve native vertical scroll but being able to add an horizontal BetterScroll (maybe a carousel). Set this to 'vertical' and the BetterScroll area will react to horizontal swipes only. Vertical swipes will naturally scroll the whole page. Contrarily, set this to 'horizontal' when you want to keep natural horizontal scroll. - **Note**: The setting of `eventPassthrough` will cause some other settings to be invalid, be careful when using it. ## click - **Type**: `boolean` - **Default**: `false` - **Usage**: To override the native scrolling BetterScroll has to inhibit some default browser behaviors, such as mouse clicks. If you want your application to respond to the click event you have to explicitly set this option to `true`. And then BetterScroll will add a private attribute called `_constructed` to the dispatched event whose value is true. ## dblclick - **Type**: `boolean | Object` - **Default**: `false` - **Usage**: Send dblclick event. When configured to true, by default the two times click delay is 300 ms. If configured to an object, the `delay` can be modified. ```js dblclick: { delay: 300 } ``` ## tap - **Type**: `string` - **Default**: `''` - **Details**: Since BetterScroll will block the native click event, we can set tap to 'tap', which will dispatch a tap event when the region is clicked. You can listen to it as if it were listening to native events. ## bounce - **Type**: `boolean | Object` - **Default**: `true` - **Details**: When the content element meets the boundary it performs a small bounce animation. Setting this to true will enable the animation. ```js bounce: { top: true, bottom: true, left: true, right: true } ``` `bounce` can support the effect of closing the back of some edges. You can set the `key` of the corresponding side to `false`. :::tip If you want to conveniently set all edges to **true** or **false**, you only need to set `bounce` to **true** or **false**. ::: ## bounceTime - **Type**: `number` - **Default**: Default: `800` (ms, modification is not recommended) - **Details**: Set the duration in millisecond of the bounce animation. ## momentum - **Type**: `boolean` - **Default**: `true` - **Usage**: If setted to true, you can turn on the momentum animation when the user quickly flicks on screen. ## momentumLimitTime - **Type**: `number` - **Default**: `300` (ms) - **Usage**: Only when the time of the user's flicking on screen is lower than `momentumLimitTime` resulting in the momentum animation. ## momentumLimitDistance - **Type**: `number` - **Default**: `15` (px) - **Usage**: Only when the distance of the user's flicking on screen is greater than `momentumLimitTime` resulting in the momentum animation. ## swipeTime - **Type**: `number` - **Default**: `2500` (ms) - **Usage**: Set the duration in millisecond of the momentum animation. ## swipeBounceTime - **Type**: `number` - **Default**: `500` (ms) - **Usage**: Set the entire bounce animation time when the content element meets the boundary in the case of running a momentum animation. ## deceleration - **Type**: `number` - **Default**: `0.0015` - **Usage**: Represent the deceleration of the momentum animation. ## flickLimitTime - **Type**: `number` - **Default**: `200` - **Usage**: Sometimes we want to cpture the user's flick action (slide a short distance in a short time). Only when the time of the user slide on screen is shorter than `flickLimitTime`, it is considered as a flick action. ## flickLimitDistance - **Type**: `number` - **Default**: `100` - **Usage**: Only when the distance of the user slide on screen is shorter than `flickLimitDistance`, it is considered as a flick action ## resizePolling - **Type**: `number` - **Default**: `60` (ms) - **Usage**: When you resize the window BetterScroll has to recalculate elements position and dimension. This might be a pretty daunting task for the poor little fella. To give it some rest the polling is set to 60 milliseconds and it is reasonable value. ## probeType - **Type**: `number` - **Default**: `0` - **Optional Value**: `1 | 2 | 3` - **Usage**: Deciding whether to dispatch the scroll event, this has an impact on the performance of the page, especially in the mode where `useTransition` is true. ```js // There are two scenarios for dispatching scroll: // 1. The finger acts on the scrolling area (content DOM), // 2. Invoke the scrollTo method or trigger the momentum scroll animation (in fact, the implementation is still Invoking the scrollTo method) // For the v2.1.0, the probeType has been unified // The probeType is: // 0, scroll event will not be dispatched at any time, // 1, and only when the finger is moving on the scroll area, a scroll event is dispatched every momentumLimitTime milliseconds. // 2, and only when the finger is moving on the scroll area, a scroll event is dispatched all the time. // 3, scroll events are dispatched at any time, including invoking scrollTo or triggering momentum ``` ## preventDefault - **Type**: `boolean` - **Default**: `true` - **Usage**: Whether or not to `preventDefault()` when events are fired. This should be left `true` unless you really know what you are doing. ## preventDefaultException - **Type**: Object - **Default**: `{ tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT|AUDIO)$/ }` - **Usage**: BetterScroll will inhibit the native scrolling and meanwhile inhibit some native components' default behaviours. In this situation, we can't 'preventDefault' on these elements, so we can configure 'preventDefaultException'. Default `{tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT|AUDIO)$/}` represents that default behaviours of elements with tagnames like 'input', 'textarea', 'button', 'select', 'audio' will not be inhibited - **Note**: This is a pretty powerful option. Its key is the attribute value of DOM elements, the corresponding value can be a regular expression. For example, if we want to configure the element whose class name is 'test', then the configuration is `{className:/(^|\s)test(\s|$)/}`. ## tagException - **Type**: `Object` - **Default**: `{ tagName: /^TEXTAREA$/ }` - **Usage**: If BetterScroll nests form elements such as `textarea`, the user's expectation should be that sliding textarea should not cause bs scrolling. If the manipulated DOM (eg:textarea tag) hits the configured rule, `bs` won't scroll. - **Note**: This is a pretty powerful option. Its key is the attribute value of DOM elements, the corresponding value can be a regular expression. For example, if we want to configure the element whose class name is 'test', then the configuration is `{className:/(^|\s)test(\s|$)/}`. ## HWCompositing - **Type**: `boolean` - **Default**: `true` - **Usage**: This option tries to put the content element on the hardware layer by appending `translateZ(1px)` to the transform CSS property. This greatly increases performance especially on mobile and achieve a good scrolling effect. - **Note**: Only browsers that support enabling hardware acceleration has the effect. ## useTransition - **Type**: `boolean` - **Default**: `true` - **Usage**: Whether to use CSS3 transition animation. If setted to false, the engine will use `requestAnimationFrame` to do animation. ## bindToWrapper - **Type**: `boolean` - **Default**: `false` - **Usage**: The `touchmove` event is normally bound to the document and not the scroll wrapper. When you move the cursor out of the wrapper the scrolling keeps going(only works in PC). This is usually what you want, but you can also bind the move event to wrapper itself. Doing so as soon as the cursor leaves the wrapper the scroll stops. - **Note**: For the mobile, even if the touchmove event is bound to the wrapper, the wrapper can still be moved if the finger leaves the wrapper. ## disableMouse - **Type**: `boolean` - **Default**: get the result by current browser environment - **Usage**: When in mobile environment (supporting touch event), disableMouse will be `true` and mouse event will not be listened. While in PC environment, disableMouse will be `false` and mouse event will be listened. ## disableTouch - **Type**: `boolean` - **Default**: get the result by current browser environment - **Usage**: When in mobile environment (supporting touch event), `disableTouch` will be `false` and touch event will be listened. While in PC environment, `disableMouse` will be `true` and touch event will not be listened. We suggest not modifying this unless you konw what you are doing. ::: warning Considering some specific scenarios of the user, such as **the tablet needs to support the touch event, the tablet with mouse has to support the mouse event**, In other words, if you need to listen to the touch and mouse events at the same time, then the instantiation of BetterScroll needs to be configured as follows: ```js let bs = new BScroll('.wrapper', { disableMouse: false, disableTouch: false }) ``` Due to the different bottom-level implementation logic of different devices and different browser environments, BetterScroll's internal calculations of whether to listen to `touch` or `mouse` events may make wrong judgment, so you can solve this type of problem according to the above option configuration. ::: ## autoBlur - **Type**: `boolean` - **Default**: `true` - **Usage**: It will auto blur the active element(input、textarea) before scroll start. ## stopPropagation - **Type**: `boolean` - **Default**: `false` - **Usage**: Whether stop event propagation. It is often used in nested scroll scenes. ## bindToTarget - **Type**: `boolean` - **Default**: `false` - **Usage**: Bind touch or mouse events to the `content` element instead of the container `wrapper`, which is mostly used in [movable](../plugins/movable.html). ## autoEndDistance - **Type**: `number` - **Default**: `5` - **Usage**: When the finger operation is crazy, the `touchend` event may not be triggered when sliding out of the viewport, so the function of autoEndDistance is to automatically call the touchend event when the finger is about to leave the current viewport. When the default distance is 5px from the boundary, the scrolling ends. ## outOfBoundaryDampingFactor - **Type**: `number` - **Default**: `1 / 3` - **Usage**: When out of boundary, the damping behavior is performed. The smaller the damping factor, the greater the resistance. Value range: [0, 1]. ## specifiedIndexAsContent - **Type**: `number` - **Default**: `0` - **Usage**: Specify the child element corresponding to the index of the `wrapper` as the `content`. By default, BetterScroll uses the first child element of the `wrapper` as the content. ```html
      1.1
      1.2
      2.1
      2.2
      ``` ```js // For the above DOM structure, when BetterScroll version <= 2.0.3, only div.content1 is used as content // When the version is >= 2.0.4, content can be specified through 'specifiedIndexAsContent' let bs = new BScroll('.wrapper', { specifiedIndexAsContent: 1 // use div.content2 as BetterScroll's content }) ``` ## quadrant - **Type**: `1 | 2 | 3 | 4` - **Default**: `1` - **Usage**: When the ancestor elements of BetterScroll's wrapper DOM are forced to rotate by CSS, the original displacements in the x and y directions need to perform a certain transformation to ensure a reasonable interaction. ```html
      1.1
      1.2
      ``` ```js let bs = new BScroll('.wrapper', { quadrant: 2 }) ``` 1. When the rotation angle of the parent element or ancestor element of the `wrapper` is (315, 45], the quadrant can keep the default value; 2. When the rotation angle of the parent element or ancestor element of the `wrapper` is (45, 135],Especially **90 degrees**, the quadrant **must** be `2`; 3. When the rotation angle of the parent element or ancestor element of the `wrapper` is (135, 225],Especially **180 degrees**, the quadrant **must** be `3`; 4. When the rotation angle of the parent element or ancestor element of the `wrapper` is (225, 315],Especially **270 degrees**, the quadrant **must** be `4`; 5. When the rotation angle is special, such as 30 degrees or 200 degrees, you may not be satisfied with the built-in transformation logic. You can customize your own transformation logic through the `coordinateTransformation` hook. ```js let bs = new BScroll('.wrapper', { quadrant: 1 // default value }) bs.scroller.actions.hooks.on( bs.scroller.actions.hooks.eventTypes.coordinateTransformation, (transformateDeltaData) => { // get user finger moved distance const originDeltaX = transformateDeltaData.deltaX const originDeltaY = transformateDeltaData.deltaY // apply transformation transformateDeltaData.deltaX = originDeltaY transformateDeltaData.deltaY = originDeltaX // transformateDeltaData.deltaX will be used as content DOM style's translateX // transformateDeltaData.deltaY will be used as content DOM style's translateY } ) ``` For example: Use CSS to flip the horizontal scrolling BetterScroll. ================================================ FILE: packages/vuepress-docs/docs/en-US/guide/base-scroll.md ================================================ # base-scroll In the design of BetterScroll 2.0, we abstracted the core scrolling part, which is the smallest unit of use of BetterScroll. The compression volume is nearly one-third smaller than `1.0`. You may only need to complete a pure scrolling, then just import this library as follows: ```bash npm install @better-scroll/core --save ``` ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.div') ``` ## Get started BetterScroll has a variety of scroll modes. - **vertical scroll** :::warning BetterScroll dispatches the scroll event in real time, which requires setting `probeType` to 3. ::: - **Horizontal scroll** :::warning BetterScroll achieves horizontal scrolling, which is more demanding for CSS. First you need to make sure that the wrapper doesn't wrap, and the display of content is inline-block. ```stylus .scroll-wrapper // ... white-space nowrap .scroll-content // ... display inline-block ``` ::: - **freeScroll(Horizontal and vertical scroll simultaneously)** ## Dynamic Content For the `2.0.4` version, it has the ability to detect `content` becoming other elements, you can check the following example. ## specifiedIndexAsContent For the `2.0.4` version, you can specify a child of **wrapper** as **content**. In previous versions, BetterScroll would only process the first child element of the wrapper. [For details.](./base-scroll-options.html#specifiedindexascontent-2-0-4) ## quadrant For the `2.3.0` version, If the parent element or ancestor element of BetterScroll wrapper DOM rotates, you can use the `quadrant` option to modify the user's interactive behavior. - **Vertical becomes Horizontal** - **Horizontal scroll flipped** ## Warm Tips :::tip **If there is any situation where scrolling is not possible, you should first check if the height/width of the content element is greater than the height/width of the wrapper**. This is a prerequisite for content to scroll. If the content has an image, it may happen that the image has not been downloaded when the DOM element is rendered, so the height of the content element is less than expected, and the scrolling is not normal. At this point you should call the `bs.refresh` method after the image has been loaded, such as the `onload` event callback, which will recalculate the latest scrolling size. ::: ================================================ FILE: packages/vuepress-docs/docs/en-US/guide/how-to-install.md ================================================ # Install ## NPM BetterScroll is hosted on NPM and executed with the following command: ```bash npm install @better-scroll/core --save // or yarn add @better-scroll/core ``` The next step is to use it in the code. [webpack](https://webpack.js.org/) and other build tools support the introduction of code from `node_modules` : ``` js import BScroll from '@better-scroll/core' ``` If it is the syntax of commonjs, as follows: ``` js var BScroll = require('@better-scroll/scroll') ``` ## Script BetterScroll also supports direct loading with script, which loads a BScroll object on the window after loading. ```html ``` ```js let wrapper = document.getElementById("wrapper") let bs = new BScroll(wrapper, {}) ``` ## BetterScroll with all plugins ```bash npm install better-scroll --save // or yarn add better-scroll ``` ```js import BetterScroll from 'better-scroll' let bs = new BetterScroll('.wrapper', {}) ``` Use script. ```html ``` ```js let bs = BetterScroll.createBScroll('.wrapper', {}) ``` ================================================ FILE: packages/vuepress-docs/docs/en-US/guide/use.md ================================================ # How to use ## Basic Usage If you only need a list with basic scrolling capabilities, just use `core`. ```js import BScroll from '@better-scroll/core' let bs = new BScroll('.wrapper', { // ...... see options }) ``` ## Plugins If you need some extra features like `pull-up`, you need to import additional plugins, please refer to [plugins](/docs/en-US/plugins). ```js import BScroll from '@better-scroll/core' import Pullup from '@better-scroll/pull-up' // register plugin BScroll.use(Pullup) let bs = new BScroll('.wrapper', { probeType: 3, pullUpLoad: true }) ``` ## Full plugins If you find it painsome to register plugins one by one, we offer a BetterScroll package with full plugin capabilities. It is used in exactly the same way as the `1.0` version, but the volume will be relatively large, it is recommended to load by plugin. ```js import BScroll from 'better-scroll' let bs = new BScroll('.wrapper', { // ... pullUpLoad: true, wheel: true, scrollbar: true, // and so on }) ``` ================================================ FILE: packages/vuepress-docs/docs/en-US/plugins/README.md ================================================ # plugins ## Why need plugins In order to decouple the functions of the various features of BetterScroll 1.x, to prevent unlimited increase in the size of the bundle. In the `2.x` architecture design, a "plugin" architecture design is adopted. Each feature of 1.x will be implemented in the form of Plugin in `2.x`. Existing plugins: - [pulldown](./pulldown.html) - [pullup](./pullup.html) - [scrollbar](./scroll-bar.html) - [slide](./slide.html) - [wheel](./wheel.html) - [zoom](./zoom.html) - [mouse-wheel](./mouse-wheel.html) - [observe-dom](./observe-dom.html) - [observe-image](./observe-image.html) - [nested-scroll](./nested-scroll.html) - [infinity](./infinity.html) - [movable](./movable.html) - [indicators](./indicators.html) You can write a plugin by yourself to add new feature to `bs`. If you want do this, please refer to [How to write a plugin](./how-to-write.html). ## Use a plugin Use plugins by calling the `BScroll.use()` static method. This has to be done before you call `new BScroll()`: ```js import BScroll from '@better-scroll/core' import Plugin from 'somewhere' new BScroll('.wrapper', { // pluginKey corresponds to the value of the static attribute pluginName on the Plugin class, // otherwise the plugin cannot be instantiated pluginKey: {} }) ``` ## Use a method or property of plugins The plugin may expose some methods or properties. These methods or properties are proxied to `bs` via `Object.defineProperty` method. For example, the `zoomTo` method is provided in the zoom plugin, which you can use by `bs.zoomTo`. ```js import BScroll from '@better-scroll/core' import Zoom from '@better-scroll/zoom' BScroll.use(Zoom) const bs = new BScroll('#scroll-wrapper', { freeScroll: true, scrollX: true, scrollY: true, disableMouse: true, useTransition: true, zoom: { start: 1, min: 0.5, max: 2 } }) bs.zoomTo(1.5, 0, 0) // zoomTo from Zoom Plugin is proxied to bs instance ``` ## Use a event of plugins The hooks exposed in the plugin will be delegated to `bs`. For example, you can listen to the `zoomStart` event, which is exposed in zoom plugin, in the following way: ```js import BScroll from '@better-scroll/core' import Zoom from '@better-scroll/zoom' BScroll.use(Zoom) const bs = new BScroll('#scroll-wrapper', { freeScroll: true, scrollX: true, scrollY: true, zoom: { start: 1, min: 0.5, max: 2 } }) bs.on('zoomStart', () => { }) ``` ## BetterScroll with all plugins Considering the trouble of registering plugins one by one, if your project uses the full plugins of BetterScroll, we offer a once-in-a-lifetime solution. ```js import BScroll from 'better-scroll' const bs = new BScroll('#scroll-wrapper', { pullUpLoad: true, pullDownRefresh: true, scrollbar: true, // and so on }) ``` ::: warning import all of BetterScroll may have a big impact on the size of your bundle, and as the function of BetterScroll expands, the size will increase unlimitedly, **try to import what you need**. ::: ::: warning Normally, you should pay attention to the properties and methods exposed by the BetterScroll instance, because the properties and methods on the plugin instance have been proxied to the bs. If you really need to care about the plugin instance, you can also use `bs.plugins ` to get all plugin information. ```js import BScroll from '@better-scroll/scroll' import zoom from '@better-scroll/zoom' BScroll.use(zoom) const bs = new BScroll('.wrapper', { zoom: true }) console.log(bs.plugins.zoom) ``` ::: ================================================ FILE: packages/vuepress-docs/docs/en-US/plugins/how-to-write.md ================================================ # How to write plugins ### Conceive The Function Of The Plugin ```js import BScroll from '@better-scroll/core' import MyPlugin from '@better-scroll/my-plugin' BScroll.use(MyPlugin) const bs = new BScroll('.wrapper', { myPlugin: { scrollText: 'I am scrolling', scrollEndText: 'Scroll has ended' }, // or myPlugin: true }) // Use the event that is proxied to bs by plugin bs.on('printScrollEndText', (scrollEndText) => { console.log(scrollEndText) // print "Scroll has ended, position is (xx, yy)" }) // Use the method that is proxied to bs by plugin bs.printScrollText() // print "I am scrolling" ``` ### Write Plugin 1. **TypeScript declare merging and expose plugin methods** ```typescript import BScroll from '@better-scroll/core' export type MyPluginOptions = Partial | true type MyPluginConfig = { scrollText: string, scrollEndText: string } interface PluginAPI { printScrollText(): void } declare module '@better-scroll/core' { interface CustomOptions { myPlugin?: myPluginOptions } interface CustomAPI { myPlugin: PluginAPI } } ``` The advantage of this is that when the `myPlugin` plugin is imported and BetterScroll is instantiated, there can be corresponding Options prompts and bs can have corresponding method prompts. Take the pulldown plugin as an example: 2. **Write the plugin logic** - **BetterScroll plugins need to be a class, and have the following characteristics:** - The static pluginName property. - Implement the PluginAPI interface (only if it is necessary to proxy the plugin method to bs). - The first argument of the constructor is the BetterScroll instance `bs`. You can inject your own logic through the **event** or **hook** of bs. ```typescript export default class MyPlugin implements PluginAPI { static pluginName = 'myPlugin' public options: MyPluginConfig constructor(public scroll: BScroll){ this.handleOptions() this.handleBScroll() this.registerHooks() } } ``` - **handleOptions** Merge user options,narrow down it‘s type。 ```typescript import { extend } from '@better-scroll/shared-utils' export default class MyPlugin { private handleOptions() { const userOptions = (this.scroll.options.myPlugin === true ? {} : this.scroll.options.myPlugin) as Partial const defaultOptions: MyPluginConfig = { scrollText: 'I am scrolling', scrollEndText: 'Scroll has ended' } this.options = extend(defaultOptions, userOptions) } } ``` - **handleBScroll** Proxy events and methods to the BetterScroll instance. ```typescript export default class MyPlugin implements PluginAPI { private handleBScroll() { const propertiesConfig = [ { key: 'printScrollText', sourceKey: 'plugins.myPluginOptions.printScrollText' } ] // myPlugin.printScrollText is proxied to bs.printScrollText this.scroll.proxy(propertiesConfig) // Proxy printScrollEndText event to bs // Users can subscribe to events via bs.on('printScrollEndText', handler) this.scroll.registerType(['printScrollEndText']) } printScrollText() { console.log(this.options.scrollText) } } ``` - **registerHooks** Tap into the bs hook, implement the logic of the plugin, and dispatch custom events of the plugin. ```typescript export default class MyPlugin implements PluginAPI { private registerHooks() { const scroll = this.scroll scroll.on(scroll.eventTypes.scrollEnd, ({ x, y }) => { scroll.trigger( scroll.eventTypes.printScrollEndText, `${this.options.scrollEndText}, position is (${x}, ${y})` ) }) } } ``` Congratulations, a simple BetterScroll plugin has been completed. If you need more complex plugin to meet your need, you can read [Events and Hooks](../guide/base-scroll-api.html#events-vs-hooks), it can help you to complete a fantastic plugin. ================================================ FILE: packages/vuepress-docs/docs/en-US/plugins/indicators.md ================================================ # indicators ## Introduciton The indicators provides the ability to link with another BetterScroll. With this, you can achieve effects such as **Parallax Scrolling**, **Magnifier**. ::: tip This is a very powerful and creative plugin. ::: ## Install ```bash npm install @better-scroll/indicators --save // or yarn add @better-scroll/indicators ``` ## Usage First, install the plugin via the static method `BScroll.use()` ```js import BScroll from '@better-scroll/core' import Indicators from '@better-scroll/indicators' BScroll.use(Indicators) ``` pass correct [indicators options](./indicators.html#indicators-options). ```js new BScroll('.wrapper', { indicators: { // For details, please refer to the demo below relationElement: someHTMLElement } }) ``` ## Demos - **Magnifier** - **Parallax Scrolling** ## indicators options ### relationElement - **Type**: `HTMLElement` The container element associated with another BetterScroll, just like the above demo, `div.scroll-indicator` is releationElement. **releationElement must be passed in by the user and has child elements**. ### relationElementHandleElementIndex - **Type**: `number` - **Default**: `0` Specify the child element of releationElement as the manipulated element. For details, please refer to the above demo. ### ratio - **Type**: `number | Ratio | undefined` - **Default**: `undefined` ```ts type Ratio = { // Specify the ratio of the scroll distance of x x: number // Specify the ratio of the scroll distance of y y: number } ``` Specify the ratio of releationElement to the scrolling distance of BetterScroll. Usually,**the plugin will automatically** calculate the scroll ratio of the two, but you can also manually specify the ratio to achieve the effect of `Parallax Scrolling`. For details, please refer to the demo above. ### interactive - **Type**: `boolean | undefined` - **Default**: `undefined` Decide whether the relationElementHandleElementIndex of relationElement can interact. When it is set to `false`, it will not respond to touch / mouse events. ================================================ FILE: packages/vuepress-docs/docs/en-US/plugins/infinity.md ================================================ # infinity The infinity plugin provides BetterScroll with unlimited scrolling capabilities. If you have a large amount of list data to render, you can use the infinity plugin, in which BetterScroll will only render a certain number of DOM elements, so that the page will continue to scroll smoothly when a large amount of data. > Note: Unless you have a lot of data rendering needs, use coreScroll. ## Install ```shell npm install @better-scroll/infinity --save // or yarn add @better-scroll/infinity ``` ## Usage First, install the plugin via the static method `BScroll.use()` ```js import BScroll from '@better-scroll/core' import InfinityScroll from '@better-scroll/infinity' BScroll.use(InfinityScroll) ``` Then, To instantiate BetterScroll, you need to pass the related configuration item `infinity`: ```typescript new BScroll('.bs-wrapper', { scrollY: true, infinity: { fetch(count) { // Fetch data that is larger than count, the function is asynchronous, and it needs to return a Promise.。 // After you have successfully fetch the data, you need resolve an array of data (or resolve Promise). // Each element of the array is list data, which will be rendered when the render method executes。 // If there is no data, you can resolve (false) to tell the infinite scroll list that there is no more data。 } render(item, div?: HTMLElement) { // Rendering each element node, item is data from fetch function // div is an element which is recycled from document or undefined // The function needs to return to a html element. }, createTombstone() { // Must return a tombstone DOM node. } } }) ``` ::: danger Note `fetch`, `render`, `createTombstone` must be implemented in accordance with the comments as above, otherwise an internal error will be reported. The plugin relies on Promise internally. If the browser does not support it, the Promise Polyfill is required. ::: ## Demo ================================================ FILE: packages/vuepress-docs/docs/en-US/plugins/mouse-wheel.md ================================================ # mouse-wheel mouseWheel extends the capabilities of the BetterScroll mouse wheel. ## Install ```bash npm install @better-scroll/mouse-wheel --save // or yarn add @better-scroll/mouse-wheel ``` ::: tip Currently supports mouse wheel: core, slide, wheel, pullup, pulldown plugins. ::: ## Basic Usage In order to enable the mouseWheel plugin, you need to first import it, register the plugin through the static method `BScroll.use()`, and finally pass in the correct [mouseWheel option](./mouse-wheel.html#mousewheel-options) ```js import BScroll from '@better-scroll/core' import MouseWheel from '@better-scroll/mouse-wheel' BScroll.use(MouseWheel) new BScroll('.bs-wrapper', { //... mouseWheel: { speed: 20, invert: false, easeTime: 300 } }) ``` - **VerticalScroll Demo** - **HorizontalScroll Demo** ## Advanced Usage The mouseWheel plugin can also be used with other plugins to increase the operation of the mouse wheel. - **mouseWheel & slide** Operate [slide](./slide.html) with the mouse wheel. - **HorizontalSlide Demo** - **VerticalSlide Demo** - **mouseWheel & pullup** use mousewheel do [pullup](./pullup.html) operation. - **mouseWheel & pulldown** use mousewheel do [pulldown](./pulldown.html) operation. - **mouseWheel & wheel** use mousewheel do [wheel](./wheel.html) operation. ## mouseWheel options ### speed - **Type**: `number` - **Default**: `20` The speed at which the mouse wheel scrolls. ### invert - **Type**: `boolean` - **Default**: `false` When the value is true, it means that the scrolling direction of the wheel is opposite to that of BetterScroll. ### easeTime - **Type**: `number` - **Default**: `300`(ms) The duration of the scroll animation. ### discreteTime - **Type**: `number` - **Default**: `400`(ms) Because the mouse wheel is a discrete movement, there is no event type of **start**, **move**, **end**, so as long as no scroll is detected within `discreteTime`, then one scroll wheel action ends. ::: warning When integrated with [pulldown](./pulldown.html) plugin, `easeTime` and `discreteTime` will be **internally** modified to **reasonable fixed value** to trigger the `pullingDown` hook ::: ### throttleTime - **Type**: `number` - **Default**: `0`(ms) Since the scroll wheel is a high-frequency action, the trigger frequency can be limited by `throttleTime`. MouseWheel will cache the scrolling distance, and calculate the cached distance and scroll every throttleTime. > Modifying throttleTime may cause discontinuous scrolling animation, please adjust it according to the actual scene. ### dampingFactor - **Type**: `number` - **Default**: `0.1` Damping factor, the value range is [0, 1]. When BetterScroll rolls out of the boundary, resistance needs to be applied to prevent the rolling range from being too large. The smaller the value, the greater the resistance. :::tip When `mouseWheel` is configured as `true`, the plugin uses the default plugin option. ```js const bs = new BScroll('.wrapper', { mouseWheel: true }) // equals const bs = new BScroll('.wrapper', { mouseWheel: { speed: 20, invert: false, easeTime: 300, discreteTime: 400, throttleTime: 0, dampingFactor: 0.1 } }) ``` ::: ## Events ### alterOptions - **Arguments**: `MouseWheelConfig` ```typescript export interface MouseWheelConfig { speed: number invert: boolean easeTime: number discreteTime: number throttleTime: number, dampingFactor: number } ``` - **Triggered Timing**: The mousewheel begins to scroll, allowing to modify options to control certain behaviors during scrolling. ### mousewheelStart - **Arguments**: none - **Triggered Timing**: The mousewheel starts. ### mousewheelMove - **Arguments**: `{ x, y }` - `{ number } x`: The current x of BetterScroll - `{ number } y`: The current y of BetterScroll - **Type**: `{ x: number, y: number }` - **Triggered Timing**: Mousewheel is scrolling ### mousewheelEnd - **Arguments**:`delta` - **Type**: `WheelDelta` ```typescript interface WheelDelta { x: number y: number directionX: Direction directionY: Direction } ``` - **Triggered Timing**: If the mousewheel hook has not been triggered after `discreteTime`, a mousewheel action will be settled. ::: danger Note Due to the particularity of the mousewheel hook, the dispatch of mousewheelEnd does not mean the end of the scroll animation. ::: ::: tip In most scenarios, if you want to know the current scroll position of BetterScroll accurately, please listen to the scroll and scrollEnd hooks instead of the `mouseXXX` hooks. ::: ================================================ FILE: packages/vuepress-docs/docs/en-US/plugins/movable.md ================================================ # movable ## Introduction Add move functionality for BetterScroll. ## Install ```bash npm install @better-scroll/movable --save // or yarn add @better-scroll/movable ``` ## Basic Usage import `movable`, then call `BScroll.use()`. ```js import BScroll from '@better-scroll/core' import Movable from '@better-scroll/movable' BScroll.use(Movable) ``` pass in the correct configuration in options, for example: ```js new BScroll('.bs-wrapper', { bindToTarget: true, scrollX: true, scrollY: true, freeScroll: true, bounce: true movable: true // for movable plugin }) ``` The following is related to `movable` plugin and [BetterScroll configuration](../guide/base-scroll-options.html): - **movable(for plugin)** Enable zoom functionality, set it `true`. - **bindToTarget** Must be set to `true` to actively bind the touch event to **the element to be moved**, because BetterScroll binds the touch event to **the wrapper element** by default. - **freeScroll** Record the offset of x and y direction when finger moved, set it `true`. In addtional, **scrollX** and **scrollY** are also need to be true. - **scrollX** Enable the scrolling ability in the x direction and set it to `true`. - **scrollY** Enable the scrolling ability in the y direction and set it to `true`. - **bounce** Specifies to turn on boundary rebound. - **Examples** ```js { bounce: true // Enable all directions, bounce: { left: true, // Enable the left right: true, // Enable the right top: false, bottom: false } } ``` ## Demo - **Only one content** Usually, there is only one content. - **Multi content** However, in some scenarios, there may be multiple content. ## Advanced Usage With [ zoom ](./zoom.html#introduction) plugin, increase the zoom capability. ```js import BScroll from '@better-scroll/core' import Movable from '@better-scroll/movable' import Zoom from '@better-scroll/zoom' new BScroll('.bs-wrapper', { bindToTarget: true, scrollX: true, scrollY: true, freeScroll: true, bounce: true movable: true // for movable plugin zoom: { // for zoom plugin start: 1, min: 1, max: 3 } }) ``` ## Demo :::warning pc is not allowed, scan the qrcode. ::: - **One Content** - **Multi Content** ## Instance Methods ### putAt(x, y, [time], [easing]) - **Arguments** - `{PositionX} x`: x coordinate - `PositionX: 'number | 'left' | 'right' | 'center'` - `{PositionY} y`: y coordinate - `PositionY: 'number | 'top' | 'bottom' | 'center'` - `{number} [time]`: Scroll animation duration - `{EaseItem} [easing]`: Ease effect configuration, refer to [ease.ts](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/ease.ts), the default is `bounce` effect Put the content element in a certain position. x and y can be not only numbers, but also corresponding strings. - **Examples** ```js const bs = new BScroll('.bs-wrapper', { bindToTarget: true, scrollX: true, scrollY: true, freeScroll: true, movable: true }) // Placed in the center of the wrapper bs.putAt('center', 'center', 0) // Placed in the right-bottom corner of the wrapper, the animation duration is 1s bs.putAt('right', 'bottom', 1000) ``` ================================================ FILE: packages/vuepress-docs/docs/en-US/plugins/nested-scroll.md ================================================ # nested-scroll ## Introduction Coordinates nested BetterScroll scrolling behavior. ::: warning **v2.1.0** supports BetterScroll with **Multi Nesting**, with more powerful functions and better performance. Only **Double Nesting** is supported in old version, please upgrade to **v2.1.0** as soon as possible. ::: ::: tip **v2.1.0** Perfectly solves the problem that the click event of multi-level nested BetterScroll is dispatched multiple times. ::: ## Install ```bash npm install @better-scroll/nested-scroll --save // or yarn add @better-scroll/nested-scroll ``` ## Usage import the `nested-scroll` plugin and use it with the static method `BScroll.use()` ```js import BScroll from '@better-scroll/core' import NestedScroll from '@better-scroll/nested-scroll' BScroll.use(NestedScroll) ``` After the above steps are completed, `nestedScroll` is configured in BScroll's `options`. ```js // < v2.1.0 // parent bs new BScroll('.outerWrapper', { nestedScroll: true }) // child bs new BScroll('.innerWrapper', { nestedScroll: true }) // >= v2.1.0 // parent bs new BScroll('.outerWrapper', { nestedScroll: { groupId: 'dummy-divide' // string or number } }) // child bs new BScroll('.innerWrapper', { nestedScroll: { groupId: 'dummy-divide' } }) ``` BetterScroll instances (bs) with the same `groupId` **share the same NestedScroll instance**(`ns`), `ns` will coordinate the scrolling behavior of each bs, once a bs is destroyed, `ns` will lose control of it, for example: ```js // parent bs const bs1 = new BScroll('.outerWrapper', { nestedScroll: { groupId: 'shared' // string or number } }) // child bs const bs2 = new BScroll('.innerWrapper', { nestedScroll: { groupId: 'shared' } }) console.log(bs1.plugins.nestedScroll === bs2.plugins.nestedScroll) // true // nestedScroll no longer constrains bs2 // nestedScroll no longer coordinates the scrolling behavior of bs1 and bs2 bs2.destroy() ``` ## Demo - **Nested vertical scroll ** - **Nested triple vertical scroll ** - **Nested horizontal scroll ** ## Instance Methods :::tip All methods are proxied to BetterScroll instance, for example: ```js import BScroll from '@better-scroll/core' import NestedScroll from '@better-scroll/nested-scroll' BScroll.use(NestedScroll) const bs1 = new BScroll('.parent-wrapper', { nestedScroll: { groupId: 'dummy' } }) const bs2 = new BScroll('.child-wrapper', { nestedScroll: { groupId: 'dummy' } }) // purge nestedScroll // bs1 and bs2 share the same nestedScroll instance because they have the same groupId bs1.purgeNestedScroll() // Same as bs2.purgeNestedScroll() ``` ::: ### `purgeNestedScroll()` - **Details**: Purge the nestedScroll that controls itself ::: warning Different `groupId` will generate different nestedScroll, and the same `groupId` will share the same nestedScroll, so you should call `purgeNestedScroll` at the right time (such as when the component is destroyed) to clean up the memory. Or you can call the destroy method of BetterScroll to remove itself from nestedScroll, for example: ```js const bs1 = new BScroll('.parent-wrapper', { nestedScroll: { groupId: 'dummy' } }) const bs2 = new BScroll('.child-wrapper', { nestedScroll: { groupId: 'dummy' } }) bs1.destroy() // nestedScroll no longer constrains bs1 bs2.destroy() // nestedScroll no longer constrains bs2 ``` ::: ## Static Methods ### `getAllNestedScrolls()` - **Details**: Get all current nestedScroll instances - **Returns**: An array of nestedScroll instances ```typescript import NestedScroll from '@better-scroll/nested-scroll' const nestedScrolls: NestedScroll[] = NestedScroll.getAllNestedScrolls() ``` ### `purgeAllNestedScrolls()` - **Details**: Purge all current nestedScroll instances ```typescript import NestedScroll from '@better-scroll/nested-scroll' // No longer constrain any BetterScroll instances NestedScroll.purgeAllNestedScrolls() ``` ================================================ FILE: packages/vuepress-docs/docs/en-US/plugins/observe-dom.md ================================================ # observe-dom Enable detection of content and content child DOM changes. When the plugin is used and these DOM elements change, `bs.refresh()` will be triggered. The observe-dom plugin has the following features: - Debounce feature for CSS attributions which change frequently - If the scroll elements change occurs during the scroll animation, refresh will not be triggered. ## Install ```bash npm install @better-scroll/observe-dom --save // or yarn add @better-scroll/observe-dom ``` ## Usage ```js import BScroll from '@better-scroll/core' import ObserveDOM from '@better-scroll/observe-dom' BScroll.use(ObserveDOM) new BScroll('.bs-wrapper', { //... observeDOM: true // init observe-dom plugin }) ``` ## Demo :::warning For version <= `2.0.6`, because the internal implementation of the plugin uses [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver), it cannot detect whether the load of the `img` Element is complete, so for images with uncertain heights inside the content, you need to wait for the image to load before calling `bs.refresh()` to recalculate the scrollable size. If the browser does not support MutationObserver, the fallback inside the plugin is to recalculate the scrollable size every second. ::: :::tip In the v2.1.0 version, the [observe-image](./observe-image) plugin is added to detect the loading of the img tag, so the two can be combined to complement the **autorefresh** ability to update BetterScroll every time. ::: ================================================ FILE: packages/vuepress-docs/docs/en-US/plugins/observe-image.md ================================================ # observe-image ## Introduction Turn on the detection of the loading of image elements in the wrapper child element. Regardless of whether the image is loaded successfully or not, BetterScroll's `refresh` method is automatically called to recalculate the scrollable width or height, which was supported in v2.1.0. :::tip For scenes where CSS has been used to determine the width and height of the image, this plugin should not be used, because each call to refresh will affect performance. You only need it if the width or height of the image is uncertain. ::: ## Install ```bash npm install @better-scroll/observe-image --save // or yarn add @better-scroll/observe-image ``` ## Usage ```js import BScroll from '@better-scroll/core' import ObserveImage from '@better-scroll/observe-image' BScroll.use(ObserveImage) new BScroll('.bs-wrapper', { //... observeImage: true }) ``` ## Demo ## observeImage Options :::tip When `observeImage` is configured as `true`, the plugin uses the default plugin option. ```js const bs = new BScroll('.wrapper', { observeImage: true }) // equals const bs = new BScroll('.wrapper', { observeImage: { debounceTime: 100 // ms } }) ``` ::: ### debounceTime - **Type**: `number` - **Default**: `100` After detecting the success or failure of the image loading, the refresh method will be called after **debounceTime** milliseconds to recalculate the scrollable height or width. If multiple images load successfully or fail within debounceTime milliseconds, the **refresh** method will only be called once. :::tip When **debounceTime** is 0, the **refresh** method will be called immediately instead of using **setTimeout**. ::: ================================================ FILE: packages/vuepress-docs/docs/en-US/plugins/pulldown.md ================================================ # pulldown ## Introduction The pulldown plugin provides BetterScroll with the ability to monitor pulldown operation. ## Install ```bash npm install @better-scroll/pull-down --save // or yarn add @better-scroll/pull-down ``` ## Usage First, install the plugin via the static method `BScroll.use()` ```js import BScroll from '@better-scroll/core' import PullDown from '@better-scroll/pull-down' BScroll.use(PullDown) ``` pass in the correct configuration in [options](./pulldown.html#pulldownrefresh-options), for example: ```js new BScroll('.bs-wrapper', { pullDownRefresh: true }) ``` ## Demo - **Basic Usage** - **Sina-Weibo ** In order to match the effects of App, in version v2.4.0, pulldown has made some changes and is compatible with previous versions. During a pulldown procedure, there are three internal circulation states, and the states are irreversible. They are as follows: 1. **default** The initial state. 2. **moving** Moving state, this state represents that the user's finger is manipulating BetterScroll, and the finger is keeping in touch. In this state, BetterScroll will dispatch two events. - **enterThreshold** Dispatched when BetterScroll scrolls **into** the pulldown threshold area. Inside this event, you can do the logic of texts initialization, such as prompting the user to "pull down to refresh" - **leaveThreshold** Dispatched when BetterScroll scrolls **out of** the pulldown threshold area. You can prompt the user to "Release finger" 3. **fetching** Once the finger went away, the pullingDown event is triggered to execute the logic of fetching data The state change can only be `default -> moving -> fetching` or `default -> moving`. The latter means that at the moment the user's finger is released, the conditions for triggering the pullingDown event are not met. ## pullDownRefresh Options ### threshold - **Type**: `number` - **Default**: `90` Configure the top pull-down distance to determine dispatching `pullingDown` hooks. ### stop - **Type**: `number` - **Default**: `40` Rebound distance. After BetterScroll dispatches the `pullingDown` hook, it will immediately execute the rebound animation. :::tip When `pullDownRefresh` is configured as `true`, the plugin uses the default plugin option. ```js const bs = new BScroll('.wrapper', { pullDownRefresh: true }) // equals const bs = new BScroll('.wrapper', { pullDownRefresh: { threshold: 90, stop: 40 } }) ``` ::: ## Instance Methods :::tip All methods are proxied to BetterScroll instance, for example: ```js import BScroll from '@better-scroll/core' import PullDown from '@better-scroll/pull-down' BScroll.use(PullDown) const bs = new BScroll('.bs-wrapper', { pullDownRefresh: true }) bs.finishPullDown() bs.openPullDown({}) bs.autoPullDownRefresh() ``` ::: ### `finishPullDown()` - **Details**: End the pull-down refresh behavior. ::: warning Every time the `pullingDown` hook is triggered, you should **actively call** `finishPullDown()` to tell BetterScroll to be ready for the next pullingDown hook. ::: ### `openPullDown(config: PullDownRefreshOptions = {})` - **Details**: Turn on the pull-down refresh dynamically. - **Arguments**: - `{ PullDownRefreshOptions } config`: Modify the option of the pulldown plugin - `PullDownRefreshOptions`: ```typescript export type PullDownRefreshOptions = Partial | true export interface PullDownRefreshConfig { threshold: number stop: number } ``` ::: warning The **openPullDown** method should be used with **closePullDown**, because in the process of generating the pulldown plugin, the pull-down refresh action has been automatically monitored. ::: ### `closePullDown()` - **Details**: Turn off the pull-down refresh dynamically. ## Events ### `pullingDown` - **Arguments**: None - **Trigger**: A `pullingDown` event is fired when the top pull-down distance is greater than the `threshold` value after touchend. ::: danger Note After the pull-down refresh action is detected, the consumption opportunity of the `pullingDown` hook is only once, so you need to call `finishPullDown()` to tell BetterScroll to provide the next consumption opportunity of the `pullingDown` event. ::: ### `enterThreshold` - **Arguments**: None - **Trigger**: when pulldown is in the **moving** state and **enters** threshold area. ### `leaveThreshold` - **Arguments**: None - **Trigger**: when pulldown is in the **moving** state and **leaves** threshold area. ================================================ FILE: packages/vuepress-docs/docs/en-US/plugins/pullup.md ================================================ # pullup ## Introduction The pullup plugin provides BetterScroll with the ability to monitor pulldown operation. ## Install ```bash npm install @better-scroll/pull-up --save // or yarn add @better-scroll/pull-up ``` ## Usage First, install the plugin via the static method `BScroll.use()` ```js import BScroll from '@better-scroll/core' import PullUp from '@better-scroll/pull-up' BScroll.use(PullUp) ``` pass in the correct configuration in [options](./pullup.html#pullupload-options), for example: ```js new BScroll('.bs-wrapper', { pullUpLoad: true }) ``` ## Demo ## pullUpLoad Options ### threshold - **Type**: `number` - **Default**: `0` The threshold for triggering a `pullingUp` hook. :::tip When `pullUpLoad` is configured as `true`, the plugin uses the default plugin option. ```js const bs = new BScroll('.wrapper', { pullUpLoad: true }) // equals const bs = new BScroll('.wrapper', { pullUpLoad: { threshold: 0 } }) ``` ::: ## Instance Methods :::tip All methods are proxied to BetterScroll instance, for example: ```js import BScroll from '@better-scroll/core' import PullUp from '@better-scroll/pull-up' BScroll.use(PullUp) const bs = new BScroll('.bs-wrapper', { pullUpLoad: true }) bs.finishPullUp() bs.openPullUp({}) bs.closePullUp() ``` ::: ### `finishPullUp()` - **Details**: Finish the pullUpLoad behavior. ::: warning Every time you trigger the `pullingUp` hook, you should **actively call** `finishPullUp()` to tell BetterScroll to be ready for the next pullingUp hook. ::: ### `openPullUp(config: PullUpLoadOptions = {})` - **Details**: Turn on the pullUpLoad dynamically. - **Arguments**: - `{ PullDownRefreshOptions } config`: Modify the option of the pullup plugin - `PullDownRefreshOptions`: ```typescript export type PullUpLoadOptions = Partial | true export interface PullUpLoadConfig { threshold: number } ``` ::: warning The **openPullUp** method should be used with **closePullUp**, because in the process of generating the pullup plugin, the pullUpLoad action has been automatically monitored. ::: ### `closePullUp()` - **Details**: Turn off pullUpLoad dynamically. ### `autoPullUpLoad()` - **Details**:Auto pullUp. ## Events ### `pullingUp` - **Arguments**: None - **Trigger**: When the distance to the bottom is less than the value of `threshold`, a `pullingUp` event is triggered. > When threshold is a positive number, it means `pullingUp` is triggered when the threshold pixel is away from the scroll boundary. On the contrary, it means that the event will be triggered when it crosses the scroll boundary. ::: danger Note After the pullUpLoad action is detected, the consumption opportunity of the `pullingUp` event is only once, so you need to call `finishPullUp()` to tell BetterScroll to provide the next consumption opportunity of the `pullingUp` event. ::: ================================================ FILE: packages/vuepress-docs/docs/en-US/plugins/scroll-bar.md ================================================ # scrollbar ## Introduciton The scrollbar plugin provides a nice scrollbar for BetterScroll. :::tip For v2.2.0, users can provide custom scroll bars. ::: ## Install ```bash npm install @better-scroll/scroll-bar --save // or yarn add @better-scroll/scroll-bar ``` ## Usage First, install the plugin via the static method `BScroll.use()` ```js import BScroll from '@better-scroll/core' import ScrollBar from '@better-scroll/scroll-bar' BScroll.use(ScrollBar) ``` pass correct [scrollbar options](./scroll-bar.html#scrollbar-options) ```js new BScroll('.bs-wrapper', { scrollY: true, scrollbar: true }) ``` ## Demo - **Vertical Scrollbar** - **Horizontal Scrollbar** - **Custom Scrollbar** - **With Mousewheel** ## scrollbar options ### fade - **Type**: `boolean` - **Default**: `true` When the scroll stops, the scrollbar fades out. ### interactive - **Type**: `boolean` - **Default**: `false` Whether scrollbar can interacted with. ### customElements - **Type**: `HTMLElement[]` - **Default**: `[]` The user provides a custom scroll bar. ```js // horizontal const horizontalEl = document.getElementById('User-defined scrollbar') new BScroll('.bs-wrapper', { scrollY: true, scrollbar: { customElements: [horizontalEl] } }) // vertical const verticalEl = document.getElementById('User-defined scrollbar') new BScroll('.bs-wrapper', { scrollY: false, scrollX: true, scrollbar: { customElements: [verticalEl] } }) // freeScroll const horizontalEl = document.getElementById('User-defined scrollbar') const verticalEl = document.getElementById('User-defined scrollbar') new BScroll('.bs-wrapper', { freeScroll: true, scrollbar: { // When there are two scrollbars // the first element of the array is the horizontal customElements: [horizontalEl, verticalEl] } }) ``` ### minSize - **Type**: `number` - **Default**: `8` The minimum size of the scrollbar. When the user provides a custom scrollbar, this configuration is invalid. ### scrollbarTrackClickable - **Type**: `boolean` - **Default**: `false` Whether the scrollbar track allows clicking. **Note**:When enabling this configuration, please ensure that the `click` of BetterScroll Options is true, otherwise the click event cannot be triggered. [The reason is here](../FAQ/diagnosis.html#question-4-why-are-the-listeners-for-all-click-events-inside-betterscroll-content-not-triggered). ```js new BScroll('.bs-wrapper', { scrollY: true, click: true // essential scrollbar: { scrollbarTrackClickable: true } }) ``` ### scrollbarTrackOffsetType - **Type**: `string` - **Default**: `'step'` After the scroll bar track is clicked, the calculation method of the scroll distance is the same as the browser's performance by default. It can be configured as `'clickedPoint'`, which means the scroll bar is scrolled to the clicked position. ### scrollbarTrackOffsetTime - **Type**: `number` - **Default**: `300` the scroll time after the scrollbar track is clicked. ### fadeInTime - **Type**: `number` - **Default**: `250` The duration of the animation when the scrollbar fades in. ### fadeOutTime - **Type**: `number` - **Default**: `500` The duration of the animation when the scrollbar fades out. :::tip When `scrollbar` is configured as `true`, the plugin uses the default plugin option. ```js const bs = new BScroll('.wrapper', { scrollbar: true }) // equals const bs = new BScroll('.wrapper', { scrollbar: { fade: true, interactive: false, // supported in v2.2.0 customElements: [], minSize: 8, scrollbarTrackClickable: false, scrollbarTrackOffsetType: 'step', scrollbarTrackOffsetTime: 300, // supported in v2.4.0 fadeInTime: 250, fadeOutTime: 500 } }) ``` ::: ================================================ FILE: packages/vuepress-docs/docs/en-US/plugins/slide.md ================================================ # slide ## Introduction slide expands the ability of carousel for BetterScroll. ## Install ```bash npm install @better-scroll/slide --save // or yarn add @better-scroll/slide ``` ## Usage import `slide`, then call `BScroll.use()`. ```js import BScroll from '@better-scroll/core' import Slide from '@better-scroll/slide' BScroll.use(Slide) ``` pass in the correct configuration in options, for example: ```js new BScroll('.bs-wrapper', { scrollX: true, scrollY: false, slide: { threshold: 100 }, momentum: false, bounce: false, stopPropagation: true }) ``` The following is related to `slide` plugin and [BetterScroll configuration](../guide/base-scroll-options.html): - **slide(for plugin)** Enable zoom functionality. That is to say, the zoom plugin won't work without the zoom options, see [slide options](./slide.html#slide-options). - **scrollX** When the value is true, set the direction of slide to **horizontal**. - **scrollY** When the value is true, set the direction of slide to **vertical**. **Note: scrollX and scrollY cannot be set to true at the same time** - **momentum** When using slide, this value needs to be set to false to avoid the problem of flickering during fast scrolling caused by inertial animation and the problem of scrolling multiple pages at a time during fast sliding. - **bounce** The bounce value needs to be set to false, otherwise it will flicker when the loop is true. - **probeType** If you want to register the `slideWillChange` event to get the change of the PageIndex of the slide in real time when the user drags the slide, you need to set the probeType value to 2 or 3. ## Terms about slide In general, the layout of BetterScroll's slide is as follows: ```html
      ``` - **slide-wrapper** slide container. - **slide-content** slide scroll element. - **slide-page** slide is composed of multiple Pages. ::: tip In the loop scenario, two more pages will be inserted before and after the slide-content to achieve the visual effect of seamless scrolling. ::: :::danger The slide-content must have at least one slide-page, if there is only one page, the loop configuration is invalid ::: ## Demo - **Horizontal Slide** - **Fullscreen Slide** - **Vertical Slide** - **Dynamic Slide ** - **Initial PageIndex Slide ** ::: tip Note: When setting `useTransition = true`, there may be flickering on some iPhone systems. You need to add the following two additional styles to each `slide-page` like the code in the above demo: ```css transform: translate3d(0,0,0) backface-visibility: hidden ``` ::: ## slide options :::tip When `slide` is configured as `true`, the plugin uses the default plugin option. ```js const bs = new BScroll('.wrapper', { slide: true }) // equals const bs = new BScroll('.wrapper', { slide: { loop: true, threshold: 0.1, speed: 400, easing: ease.bounce, listenFlick: true, autoplay: true, interval: 3000 } }) ``` ::: ### loop - **Type**: `boolean` - **Default**: `true` Is it possible to loop. But when there is only one element, this setting does not take effect. ### autoplay - **Type**: `boolean` - **Default**: `true` Whether to enable auto play. ### interval - **Type**: `number` - **Default**: `3000` The interval before the next play. ### speed - **Type**: `number` - **Default**: `400` the default duration of Page animation. ### easing - **Type**: `EaseItem` - `{ string } style`: for `transition-timing-function` - `{ Function } fn`: When setting `useTransition:false`, the animation curve is determined by `easing.fn`. - **Default**: ```js { style: 'cubic-bezier(0.165, 0.84, 0.44, 1)', fn: function(t: number) { return 1 - --t * t * t * t } } ``` Scrolling easing effect. ### listenFlick - **Type**: `boolean` - **Default**: `true` When quickly flicking across the slide area, it will trigger the switch to the previous/next page. Set listenFlick to false to turn off the effect. ### threshold - **Type**: `number` - **Default**: `0.1` :::tip When the scrolling distance is less than the threshold, the switch to the next or previous one will not be triggered. It can be set to a decimal, such as 0.1, or an integer, such as 100. When the value is a decimal, the threshold is treated as a percentage, and the final threshold is `slideWrapperWidth * threshold` or `slideWrapperHeight * threshold`. When the value is an integer, the threshold is threshold. ::: The threshold of the next or previous Page. ### startPageXIndex - **Type**: `number` - **Default**: `0` Initial pageXIndex when slide is created. ### startPageYIndex - **Type**: `number` - **Default**: `0` Initial pageYIndex when slide is created. ## Instance Methods :::tip All methods are proxied to BetterScroll instance, for example: ```js import BScroll from '@better-scroll/core' import Slide from '@better-scroll/slide' BScroll.use(Slide) const bs = new BScroll('.bs-wrapper', { slide: true }) bs.next() bs.prev() bs.getCurrentPage() ``` ::: ### next([time], [easing]) - **Arguments**: - `{ number } time`: Animation duration, default is `options.speed` - `{ EaseItem } easing`: Ease effect configuration, refer to [ease.ts](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/ease.ts), the default is `bounce` effect ```typescript interface EaseItem { style: string fn(t: number): number } ``` Scroll to the next page. ### prev([time], [easing]) - **Arguments**: - `{ number } time`: Animation duration, default is `options.speed` - `{ EaseItem } easing`: Ease effect configuration, refer to [ease.ts](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/ease.ts), the default is `bounce` effect Scroll to the previous page. ### goToPage(pageX, pageY, [time], [easing]) - **Arguments**: - `{ number } pageX`: Scroll horizontally to the Page of the corresponding index, the subscript starts from 0 - `{ number } pageY`: Scroll vertically to the Page of the corresponding index, the subscript starts from 0 - `{ number } time`: Animation duration, default is `options.speed` - `{ EaseItem } easing`: Ease effect configuration, refer to [ease.ts](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/ease.ts), the default is `bounce` effect Scroll to the specified page. ### getCurrentPage() - **Returns**: `page` ```typescript type Page = { x: number, y: number, pageX: number, // pageIndex in horizontal direction pageY: number // pageIndex in vertical direction } const page:Page = BScroll.getCurrentPage() ``` Get currentPage. ### startPlay() If the loop configuration is turned on, manually turn on autoplay. ### pausePlay() If the loop configuration is turned on, manually turn off autoplay. ## Events ### slideWillChange - **Arguments**: `page` object - `{ number } x`: The x value of the page to be displayed - `{ number } y`: The y value of the page to be displayed - `{ number } pageX`: The index value of the horizontal page to be displayed, the subscript starts from 0 - `{ number } pageY`: The index value of the vertical page to be displayed, the subscript starts from 0 - **Trigger timing**: When the currentPage value of slide is about to change - **Usage**: In the banner, it is often accompanied by a dot legend to indicate which page the current banner is on, such as the "Horizontal Slide" example above. When the user drags the banner to the next one, we hope the dot legend below will change synchronously. As shown below banner示例图 This effect can be achieved by register the `slideWillChange` event. code show as below: ```js let currentPageIndex const slide = new BScroll(this.$refs.slide, { scrollX: true, scrollY: false, slide: { threshold: 100 }, momentum: false, bounce: false, probeType: 2 }) slide.on('slideWillChange', (page) => { currentPageIndex = page.pageX }) ``` ### slidePageChanged - **Arguments**: `page` object - `{ number } x`: The x value of the current page - `{ number } y`: The y value of the current page - `{ number } pageX`: The index value of the horizontal page, the subscript starts from 0 - `{ number } pageY`: The index value of the vertical page, the subscript starts from 0 - **Trigger timing**: When slide page has changed ```js const slide = new BScroll(this.$refs.slide, { scrollX: true, scrollY: false, slide: true, momentum: false, bounce: false }) slide.on('slidePageChanged', (page) => { currentPageIndex = page.pageX }) ``` ================================================ FILE: packages/vuepress-docs/docs/en-US/plugins/wheel.md ================================================ # wheel ## Introduction The wheel plugin is the cornerstone for implementing similar iOS Picker components. ## Install ```bash npm install @better-scroll/wheel --save // or yarn add @better-scroll/wheel ``` ## Usage import `wheel`, then call `BScroll.use()`. ```js import BScroll from '@better-scroll/core' import Wheel from '@better-scroll/wheel' BScroll.use(Wheel) ``` pass in the correct configuration in options, for example: ```js let bs = new BScroll('.bs-wrapper', { wheel: true // wheel options }) ``` :::tip Wheel options is `true` or object, otherwise the plugin is invalid, please refer to [wheel options](./wheel.html#wheel-options). ::: ::: danger BetterScroll combined with the Wheel plugin is just the JS logic part of the Picker effect, and the DOM template is user-implemented. Fortunately, for most Picker scenarios, we have corresponding examples. ::: - **Basic usage** Single-column Picker is a more common effect. You can use `selectedIndex` to configure the item that initializes the corresponding index. `wheelDisabledItemClass` configures the item you want to disable to simulate the Web Select tag's disable option. - **Double-column Picker** The JS logic part is not much different from the single-column selector. You will find that there is no correlation between the two column selectors because they are two different BetterScroll instances. If you want to achieve the effect of the provincial and city linkage, then add a part of the code to make the two BetterScroll instances can be associated. Please see the next example: - **Linkage Picker** The effect of the city linkage Picker must be linked through the JS part of the logic to the different instances of BetterScroll. ## wheel options :::tip When `wheel` is configured as `true`, the plugin uses the default plugin option. ```js const bs = new BScroll('.wrapper', { wheel: true }) // equals const bs = new BScroll('.wrapper', { wheel: { wheelWrapperClass: 'wheel-scroll', wheelItemClass: 'wheel-item', rotate: 25, adjustTime: 400, selectedIndex: 0, wheelDisabledItemClass: 'wheel-disabled-item' } }) ``` ::: ### selectedIndex - **Type**: `number` - **Default**:`0` Instantiate the Wheel, the `selectedIndex` item is selected by default, and the index starts from 0. ### rotate - **Type**: `number` - **Default**: `25` When rolling the wheel, the degree of bending of the wheel item. ### adjustTime - **Type**: `number` - **Default**: `400`(ms) When an item is clicked, the duration of scroll. ### wheelWrapperClass - **Type**: `string` - **Default**: `wheel-scroll` The className of the scroll element, where "scroll element" refers to the `content` element of BetterScroll. ### wheelItemClass - **Type**: `string` - **Default**: `wheel-item` The style of the child elements of the scroll element. ### wheelDisabledItemClass - **Type**: `string` - **Default**: `wheel-disabled-item` The child element that you want to disable in the scroll element is similar to the effect of the disabled option in the select element. The wheel plugin judges whether the item is designated as disabled according to the `wheelDisabledItemClass` configuration. ## Instance Methods :::tip All methods are proxied to BetterScroll instance, for example: ```js import BScroll from '@better-scroll/core' import Wheel from '@better-scroll/wheel' BScroll.use(Wheel) const bs = new BScroll('.bs-wrapper', { wheel: true }) bs.getSelectedIndex() bs.wheelTo(1, 300) ``` ::: ### getSelectedIndex() - **Returns**: The index of the currently selected item, the subscript starts from 0 Get the index of the currently selected item. ### wheelTo(index = 0, time = 0, [ease]) - **Arguments**: - `{ number } index` - `{ number } time`: Animation duration - `{ number } ease`: Ease effect configuration, refer to [ease.ts](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/ease.ts), the default is `bounce` effect ```typescript interface EaseItem { style: string fn(t: number): number } ``` Scroll to the list item corresponding to the index. ### stop() Force the scrolling BetterScroll to stop and snap to the position of the wheel-item closest to the current one. ### restorePosition() Force the scrolling BetterScroll to stop and return to the position before the scrolling started. ::: tip The above two methods are only valid for **the scrolling BetterScroll**, and `restorePosition` is exactly the same as the original iOS Picker component. Users can choose the corresponding method according to their needs. ::: ## Events ### wheelIndexChanged - **Arguments**: The index of the current selected wheel-item. - **Trigger timing**: When the selected wheel-item changes. ```js import BScroll from '@better-scroll/core' import Wheel from '@better-scroll/wheel' BScroll.use(Wheel) const bs = new BScroll('.bs-wrapper', { wheel: true }) bs.on('wheelIndexChanged', (index) => { console.log(index) }) ``` ================================================ FILE: packages/vuepress-docs/docs/en-US/plugins/zoom.md ================================================ # zoom ## Introduction Add zoom functionality for BetterScroll. ## Install ```bash npm install @better-scroll/zoom --save // or yarn add @better-scroll/zoom ``` ## Usage import `zoom`, then call `BScroll.use()`. ```js import BScroll from '@better-scroll/core' import Zoom from '@better-scroll/zoom' BScroll.use(Zoom) ``` pass in the correct configuration in options, for example: ```js new BScroll('.bs-wrapper', { freeScroll: true, scrollX: true, scrollY: true, zoom: { start: 1, min: 0.5, max: 2 } }) ``` The following is related to `zoom` plugin and [BetterScroll configuration](../guide/base-scroll-options.html): - **zoom(for plugin)** Enable zoom functionality. That is to say, the zoom plugin won't work without the zoom option, see [zoom option](./zoom.html#option). - **freeScroll** If you want to scroll in x and y axies after zooming in, the **freeScroll** value should be set to `true`. In addtional, **scrollX** and **scrollY** are also need to be true. - **scrollX** `true` is be required if you want to scroll in x axies after zooming in. - **scrollY** `true` is be required if you want to scroll in y axies after zooming in. ## Demo :::warning pc is not allowed, scan the qrcode. ::: ## Option ### start - **Type**: `number` - **Default**: `1` Initial scale. ### min - **Type**: `number` - **Default**: `1` min scale. ### max - **Type**: `number` - **Default**: `4` max scale. ### initialOrigin - **Type**: `[OriginX, OriginY]` - **OriginX**: `number | 'left' | 'right' | 'center'` - **OriginY**: `number | 'top' | 'bottom' | 'center'` - **Default**: `[0, 0]` The origin of first initializing zoom instance, It is valid when `start` is not `1`.The origin is all based on the coordinate system of `scaled element`. - **Examples** ```js new BScroll('.bs-wrapper', { // ... other configuration zoom: { initialOrigin: [50, 50], // Based on 'scaled element', offsetLeft is 50px, offsetRight is 50px initialOrigin: [0, 0], // Based on 'scaled element', the left vertex initialOrigin: ['left', 'top'], // same as above initialOrigin: ['center', 'center'], // Based on 'scaled element', center position initialOrigin: ['right', 'top'], // Based on 'scaled element', the right vertex } }) ``` When you initialize zoom, you usually focus on zooming with the endpoint or center. You can refer to the above example. ### minimalZoomDistance - **Type**: `number` - **Default**: `5` When you zoom with two fingers, only when the zoom distance exceeds `minimalZoomDistance`, the zoom will take effect. ### bounceTime - **Type**: `number` - **Default**: `800`(ms) When the two fingers continue to zoom and the scale exceeds the threshold of `max`, when the two fingers leave, the internal "bounce" to the form of `max`, and `bounceTime` is the animation of this "bounce" behavior duration. :::tip When `zoom` is configured as `true`, the plugin uses the default plugin option. ```js const bs = new BScroll('.wrapper', { zoom: true }) // equals const bs = new BScroll('.wrapper', { zoom: { start: 1, min: 1, max: 4, initialOrigin: [0, 0], minimalZoomDistance: 5, bounceTime: 800, // ms } }) ``` ::: ## Instance Methods ### zoomTo(scale, x, y, [bounceTime]) - **Arguments** - `{number} scale`: scale ratio - `{OriginX} x`: The x of origin that is based on the left vertex of the **scaled element** - `OriginX: 'number | 'left' | 'right' | 'center'` - `{OriginY} y`: The y of origin that is based on the left vertex of the **scaled element** - `OriginY: 'number | 'top' | 'bottom' | 'center'` - `{number} [bounceTime]: Animation duration of a zoom action` Scale the element with `[x, y]` as the coordinate origin. x and y can be not only `number`, but also corresponding `string`, because general scenes are scaled based on endpoints or centers. - **Examples** ```js const bs = new BScroll('.bs-wrapper', { freeScroll: true, scrollX: true, scrollY: true, zoom: { start: 1, min: 0.5, max: 2 } }) // scaled to 1.8 based on the bottom left point of the scaled element bs.zoomTo(1.8, 'left', 'bottom') // animation duration is 1000ms bs.zoomTo(1.8, 'left', 'bottom', 1000) // scaled to 1.8 based on offsetLeft 100px & offsetTop 100px of the scaled element bs.zoomTo(1.8, 100, 100) // scaled to 2 based on the center of scaled element bs.zoomTo(2, 'center', 'center') ``` ## Events ### beforeZoomStart - **Arguments**: none - **Trigger timing**: When two fingers touch the scaled element, it does not include directly calling the `zoomTo` method ### zoomStart - **Arguments**: none - **Trigger timing**: The two finger zoom distance exceeds the minimum threshold `minimalZoomDistance`, and the zoom will start soon. it does not include directly calling the `zoomTo` method ### zooming - **Arguments**: `{ scale }` - **Type**: `{ scale: number }` - **Trigger timing**: the process of two-finger zooming action in progress or directly calling `zoomTo` to zoom - **Examples**: ```js const bs = new BScroll('.bs-wrapper', { freeScroll: true, scrollX: true, scrollY: true, zoom: { start: 1, min: 0.5, max: 2 } }) bs.on('zooming', ({ scale }) => { // use scale console.log(scale) // current scale }) ``` ### zoomEnd - **Arguments**: `{ scale }` - **Type**: `{ scale: number }` - **Trigger timing**: After two finger zooming behavior ends (if there is a rebound, the trigger timing is after the rebound animation ends) or after calling `zoomTo` to complete the zoom ::: warning In the zoom scenario, you should listen to events such as `zoomStart`, `zooming`, `zoomEnd`, and not the lower-level `scroll` and `scrollEnd` events, otherwise it may not match your expectations. ::: ================================================ FILE: packages/vuepress-docs/docs/zh-CN/FAQ/README.md ================================================ # FAQ ### 为什么 BetterScroll 初始化不能滚动? BetterScroll 滚动原理是 content 元素的高度/宽度超过 wrapper 元素的高度/宽度。而且,如果你的 content 元素含有不固定尺寸的图片,你必须在图片加载完之后,调用 `refresh()` 方法来确保高度计算正确。还存在一种情况是页面存在表单元素,弹出键盘之后,将页面的视口高度压缩,导致 bs 不能正常工作,依然是调用 `refresh()` 方法。 ### 为什么 BetterScroll 区域的点击事件无法被触发? BetterScroll 默认会阻止浏览器的原生 click 事件。如果你想要 click 事件生效,BetterScroll 会派发一个 click 事件,并且 event 参数的 `_constructed` 为 true。配置项如下: ```js import BScroll from '@better-scroll/core' let bs = new BScroll('./div', { click: true }) ``` ### 为什么我的 BetterScroll 监听 `scroll` 钩子,监听器不执行? BetterScroll 通过 probeType 配置项来决定是否派发 `scroll` 钩子,因为这是有一些性能损耗的。probeType 为 2 的时候会实时的派发事件,probeType 为 3 的时候会在 momentum 动量动画的时候派发事件。建议设置为 3。 ```js import BScroll from '@better-scroll/core' let bs = new BScroll('./div', { probeType: 3 }) ``` ### slide 用了横向滚动,发现在 slide 区域纵向滚动无效? 如果想要保留浏览器的原生纵向滚动,需要如下配置项: ```js import BScroll from '@better-scroll/core' let bs = new BScroll('./div', { eventPassthrough: 'vertical' }) ``` ================================================ FILE: packages/vuepress-docs/docs/zh-CN/FAQ/diagnosis.md ================================================ # BetterScroll 的“疑难杂症” ### 【问题一】为什么我的 BetterScroll 滑动不了? 问题基本上出在于**高度的计算错误**。首先,你必须对 `BetterScroll` 的滚动原理有一个清晰的认识,对于竖向滚动,简单的来说就是 `wrapper` 容器的高度大于 `content` 内容的高度,修改 `translateY` 来达到滚动的目的,横向滚动的原理类似。那么计算**可滚动的高度**就是 BetterScroll 必备的逻辑。一般这个逻辑出错的场景在于: 1. **存在不确定尺寸的图片** - **原因** bs 执行计算**可滚动高度**的时候,图片还未渲染完成,也无法监听到图片的加载。有时候甚至配置 `observeDOM` 为 `true` 也没效果。 - **解决** 在图片的 onload 的回调函数里面调用 `bs.refresh()` 来确保得到正确的图片高度之后再计算**可滚动的高度**。 :::tip `observeDOM` 为 `true` 的时候,BetterScroll 首先通过 [MutationObserver](https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver) 来监视对 DOM Tree 的改变,但是**无法监听图片是否加载完成**,所以需要手动调用 `refresh()` 来计算高度。 如果当前浏览器不支持 MutationObserver,会降级用 `setTimeout` 每隔 1s 来重复计算可滚动的高度,这样又能保证在图片加载完成之后,可滚动的高度计算正确。 ::: 2. **Vue 的 keep-alive 组件** - **场景** 假设存在 A、B 两个被 `keep-alive` 包裹的组件,A 组件使用了 BetterScroll,在 A 组件做了某种操作,弹出输入键盘,之后进入到 B 组件,再返回 A 组件的时候,bs 无法滚动。 - **原因** 由于 Vue 的 keep-alive 的缓存加上输入键盘弹起时候,会压缩可视区域的高度,导致之前计算过的可滚动的高度有误。 - **解决** 可以在 Vue 的 `activated` 的钩子里面调用 `bs.refresh()` 重新计算高度或者重新实例化 bs。 ### 【问题二】为什么我用 BetterScroll 做了横向滚动之后,纵向滚动失效? BetterScroll 提供了 `slide` 的 feature。如果实现了一个横向滚动的 `slide`,在 `slide` 区域做竖向滚动的操作,无法冒泡到浏览器,这样就无法操纵原生浏览器的滚动条了。 - **原因** BetterScroll 内部的滚动计算存在于用户的交互,比如移动端就是 `touchstart/touchmove/touchend` 事件,这些事件的侦听器一般都有 `e.preventDefault()` 这一行代码,会阻止浏览器的默认行为,这样浏览器的滚动条无法被滚动。 - **解决** 配置 `eventPassthrough` 属性。 ```js let bs = new BScroll('.wrapper', { eventPassthrough: 'vertical' // 保持纵向的原生浏览器滚动 }) ``` ### 【问题三】为什么我用 BetterScroll 之后,无法在浏览器弹出长按图片保存等弹窗。 - **原因** 在**问题二**已经提到了,`touchstart` 事件里面的`e.preventDefault()` 造成的。 - **解决** 方案一:配置 `preventDefaultException` 属性。 ```js let bs = new BScroll('.wrapper', { preventDefaultException: { className: /(^|\s)test(\s|$)/ } }) ``` 通过 `preventDefaultException` 可以控制 `touchstart` 和 `touchmove` 事件的 `e.preventDefault()`。上述的正则是用来校验当前触摸的目标元素 class 名称是否含有 `test`,如果通过了,则不会调用 `e.preventDefault()`。 方案二:配置 `preventDefault` 属性。 ```js let bs = new BScroll('.wrapper', { preventDefault: false }) ``` preventDefault 设置为 false,会有一些副作用,一般推荐使用**方案一**。 :::warning 副作用在于:touch 事件可能会冒泡到 document,导致文档也被拖拽。这个时候你需要监听 `wrapper` 元素的父元素或者祖先元素,给他们绑定 touchmove 事件,并且调用 `e.preventDefault()`。最常见的应该是[禁止微信下拉浏览器查看域名](https://www.cnblogs.com/jasonwang2y60/p/6848464.html) ::: ### 【问题四】为什么 BetterScroll content 内部的所有的 click 事件的侦听器都不触发? - **原因** 依然是 `touch` 事件的 `e.preventDefault()` 的原因。在移动端,如果你在 `touchstart/touchmove/touchend` 的逻辑里面调用 `e.preventDefault()`,会阻止它以及它子元素的 click 事件的执行。因此,BetterScroll 内部会管理 `click` 事件的派发,你只需要 `click` 配置项即可。 - **解决** 配置 `click` 属性。 ```js let bs = new BScroll('.wrapper', { click: true }) ``` ### 【问题五】为什么在嵌套 BetterScroll 的时候,click 事件派发两次? - **原因** 正如**问题四**所说,BetterScroll 内部会派发 `click` 事件,并且嵌套场景肯定是存在两个或两个以上的 bs。 - **解决** 你可以通过实例化内层 BetterScroll 的 `stopPropagation` 配置项来管理事件的冒泡,或者通过配置内层 BetterScroll 的 `click` 配置项来防止 click 的多次触发。 ```js let innerBS = new BScroll('.wrapper', { stopPropagation: true }) // 或者 let innerBS = new BScroll('.wrapper', { click: false }) ``` ### 【问题六】为什么我监听了 bs 的 scroll 事件,为啥回调不执行? - **原因** BetterScroll 并不是在任何时刻都会派发 `scroll` 事件,因为获取 bs 的滚动位置是有一定的性能损耗。至于是否派发,是取决于 `probeType` 配置项。 - **解决** ```js let bs = new BScroll('.div', { probeType: 3 // 实时派发 }) ``` ### 【问题七】在两个纵向嵌套的 bs 场景,为什么移动内层的 bs,会导致外层也被滚动。 - **原因** BetterScroll 的内部逻辑都在 touch 事件的侦听器函数体内,既然内部的 bs 的 touch 事件被触发,自然会冒泡到外层的 bs。 - **解决** 既然知道原因,那么也有相对应的解决办法。比如在你滚动内层的 bs 时候,监听 scroll 事件,调用外层的 `bs.disable()` 来禁用外层的 bs。当内层的 bs 滚动到底部的时候,说明这个时候需要滚动外层的 bs,这个时候调用外层的 `bs.enable()` 来激活外层,并且调用内层的 `bs.disable()` 禁止内层滚动。其实仔细想一想,这个交互就跟原生 Web 的嵌套滚动行为表现一致,只不过浏览器帮你处理了各种滚动嵌套的逻辑,而在 BetterScroll 需要你自己通过派发的事件以及暴露的 API 来实现。 > cube-ui 的 [scroll](https://didi.github.io/cube-ui/example/#/scroll/v-scrolls) 组件对此场景给出了相应的解决思路。[代码在这](https://github.com/didi/cube-ui/blob/dev/src/components/scroll/scroll.vue) ### 【问题八】在纵向 bs 嵌套横向 bs 的场景,为什么在横向 bs 的区域竖向移动不会使得外层纵向 bs 的垂直滚动? - **原因** 原因与**问题二**类似,还是因为 `e.preventDefault()` 影响了默认的滚动行为,导致外层的 bs 不会触发 touch 事件。 - **解决** 解决办法就是配置内层的 bs 的 `eventPassthrough` 属性,让其保持默认的原生竖向滚动, ```js let innerBS = new BScroll('.wrapper', { eventPassthrough: 'vertical' // 保持纵向的原生浏览器滚动 }) ``` ### 【问题九】国产浏览器中,横向 slide 切换卡顿或无效问题。 - **原因** 国产浏览器大多数都会监听默认的横滑事件,以实现浏览器的快速“回退”和”前进“。 - **解决** 主要是使用一个 CSS 特性`touch-action`,如果是横向的滑动,将 `touch-action: pan-y` CSS 设置在 `wrapper` 上面,这样就可以避免横向的切换触发浏览器的默认行为。 ================================================ FILE: packages/vuepress-docs/docs/zh-CN/README.md ================================================ --- home: true heroText: BetterScroll 2.0 actionText: 快速上手 → actionLink: /zh-CN/guide/ features: - title: 优雅的滚动 details: 为移动端(已支持 PC)各种滚动场景提供丝滑的滚动效果。 - title: 零依赖 details: 基于原生 JS 实现的,不依赖任何框架。完美运用于 Vue、React 等 MVVM 框架。 - title: 扩展灵活 details: 提供插件机制,便于对基础滚动进行功能扩展,目前支持上拉加载、下拉刷新、Picker、鼠标滚轮、放大缩小、移动缩放、轮播图、滚动视觉差,放大镜等等能力 footer: MIT Licensed | Copyright © 2018-present ustbhuangyi and theniceangel --- ================================================ FILE: packages/vuepress-docs/docs/zh-CN/guide/README.md ================================================ # 介绍 ## BetterScroll 是什么 BetterScroll 是一款重点解决移动端(已支持 PC)各种滚动场景需求的插件。它的核心是借鉴的 [iscroll](https://github.com/cubiq/iscroll) 的实现,它的 API 设计基本兼容 iscroll,在 iscroll 的基础上又扩展了一些 feature 以及做了一些性能优化。 BetterScroll 是使用纯 JavaScript 实现的,这意味着它是无依赖的。 ## 示例 [地址](https://better-scroll.github.io/examples/) 示例 ## 起步 BetterScroll 最常见的应用场景是列表滚动,我们来看一下它的 html 结构。 ```html
      • ...
      • ...
      • ...
      ``` 上面的代码中 BetterScroll 是作用在外层 **wrapper** 容器上的,滚动的部分是 **content** 元素。这里要注意的是,BetterScroll 默认处理容器(wrapper)的第一个子元素(content)的滚动,其它的元素都会被忽略。 最简单的初始化代码如下: ``` js import BScroll from '@better-scroll/core' let wrapper = document.querySelector('.wrapper') let scroll = new BScroll(wrapper) ``` BetterScroll 提供了一个类,实例化的第一个参数是一个原生的 DOM 对象。当然,如果传递的是一个字符串,BetterScroll 内部会尝试调用 querySelector 去获取这个 DOM 对象。 :::warning 注意 BetterScroll 2.X 里面,我们将 1.X 耦合的 feature 拆分至插件,以达到按需加载、减少包体积的目的。因此,`@better-scroll/core` 只提供了最核心的滚动能力。如果想要实现**上拉加载**、**下拉刷新**的功能,你需要使用对应的[插件](/zh-CN/plugins)。 ::: :::tip 提示 版本 2.0.4 的 BetterScroll 可以通过 [specifiedIndexAsContent](./base-scroll-options.html#specifiedindexascontent-2-0-4) 来指定 wrapper 的某个子元素作为 content。 ::: ## 滚动原理 很多人已经用过 BetterScroll,我收到反馈最多的问题是: > BetterScroll 初始化了, 但是没法滚动。 不能滚动是现象,我们得搞清楚这其中的根本原因。在这之前,我们先来看一下浏览器的滚动原理: 浏览器的滚动条大家都会遇到,当页面内容的高度超过视口高度的时候,会出现纵向滚动条;当页面内容的宽度超过视口宽度的时候,会出现横向滚动条。也就是当我们的视口展示不下内容的时候,会通过滚动条的方式让用户滚动屏幕看到剩余的内容。 BetterScroll 也是一样的原理,我们可以用一张图更直观的感受一下: 原理图 绿色部分为 wrapper,也就是父容器,它会有**固定的高度**。黄色部分为 content,它是父容器的**第一个子元素**,它的高度会随着内容的大小而撑高。那么,当 content 的高度不超过父容器的高度,是不能滚动的,而它一旦超过了父容器的高度,我们就可以滚动内容区了,这就是 BetterScroll 的滚动原理。 ## BetterScroll 在 MVVM 框架的应用 我之前写过一篇[当 BetterScroll 遇见 Vue](https://zhuanlan.zhihu.com/p/27407024),也希望大家投稿,分享一下 BetterScroll 在其它框架下的使用心得。 一款超赞的基于 Vue 实现的组件库 [cube-ui](https://github.com/didi/cube-ui/)。 ## BetterScroll 在实战项目中的运用 如果你想学习 BetterScroll 在实战项目中的运用,也可以去学习我的 2 门实战课程。 [Vue.js 高仿外卖饿了么实战课程](https://coding.imooc.com/class/74.html) [项目演示地址](http://ustbhuangyi.com/sell/) ![二维码](https://qr.api.cli.im/qr?data=http%253A%252F%252Fustbhuangyi.com%252Fsell%252F%2523%252Fgoods&level=H&transparent=false&bgcolor=%23ffffff&forecolor=%23000000&blockpixel=12&marginblock=1&logourl=&size=280&kid=cliim&key=686203a49c4613080b5b3004323ff977) [Vue.js 音乐 App 高级实战课程](http://coding.imooc.com/class/107.html) [项目演示地址](http://ustbhuangyi.com/music/) ![二维码](https://qr.api.cli.im/qr?data=http%253A%252F%252Fustbhuangyi.com%252Fmusic%252F&level=H&transparent=false&bgcolor=%23ffffff&forecolor=%23000000&blockpixel=12&marginblock=1&logourl=&size=280&kid=cliim&key=731bbcc2b490454d2cc604f98539952c) ================================================ FILE: packages/vuepress-docs/docs/zh-CN/guide/base-scroll-api.md ================================================ # API 如果想要彻底了解 BetterScroll,就需要了解其实例的常用属性、灵活的方法以及提供的事件与钩子。 ## 属性 有时候我们想基于 BetterScroll 做一些扩展,需要对 BetterScroll 的一些属性有所了解,下面介绍几个常用属性。 ### x - **类型**:number - **作用**:bs 横轴坐标。 ### y - **类型**:number - **作用**:bs 纵轴坐标。 ### maxScrollX - **类型**:number - **作用**:bs 最大横向滚动位置。 - **备注**:bs 横向滚动的位置区间是 [minScrollX, maxScrollX],并且 maxScrollX 是负值。 ### minScrollX - **类型**:number - **作用**:bs 最小横向滚动位置。 - **备注**:bs 横向滚动的位置区间是 [minScrollX, maxScrollX],并且 minScrollX 是正值。 ### maxScrollY - **类型**:number - **作用**:bs 最大纵向滚动位置。 - **备注**:bs 纵向滚动的位置区间是 [minScrollY, maxScrollY],并且 maxScrollY 是负值。 ### minScrollY - **类型**:number - **作用**:bs 最小纵向滚动位置。 - **备注**:bs 纵向滚动的位置区间是 [minScrollY, maxScrollY],并且 minScrollY 是正值。 ### movingDirectionX - **类型**:number - **作用**:判断 bs 滑动过程中的方向(左右)。 - **备注**:-1 表示手指从左向右滑,1 表示从右向左滑,0 表示没有滑动。 ### movingDirectionY - **类型**:number - **作用**:判断 bs 滑动过程中的方向(上下)。 - **备注**:-1 表示手指从上往下滑,1 表示从下往上滑,0 表示没有滑动。 ### directionX - **类型**:number - **作用**:判断 bs 滑动结束后相对于开始滑动位置的方向(左右)。 - **备注**:-1 表示手指从左向右滑,1 表示从右向左滑,0 表示没有滑动。 ### directionY - **类型**:number - **作用**:判断 bs 滑动结束后相对于开始滑动位置的方向(上下)。 - **备注**:-1 表示手指从上往下滑,1 表示从下往上滑,0 表示没有滑动。 ### enabled - **类型**:boolean, - **作用**:判断当前 bs 是否处于启用状态,不再响应手指的操作。 ### pending - **类型**:boolean, - **作用**:判断当前 bs 是否处于滚动动画过程中。 ## 方法 BetterScroll 提供了很多灵活的 API,当我们基于 BetterScroll 去实现一些 feature 的时候,会用到这些 API,了解它们会有助于开发更加复杂的需求。 ### refresh() - **参数**:无 - **返回值**:无 - **作用**:重新计算 BetterScroll,当 DOM 结构发生变化的时候务必要调用确保滚动的效果正常。 ### scrollTo(x, y, time, easing, extraTransform) - **参数**: - {number} x 横轴坐标(单位 px) - {number} y 纵轴坐标(单位 px) - {number} time 滚动动画执行的时长(单位 ms) - {Object} easing 缓动函数,一般不建议修改,如果想修改,参考源码中的 `packages/shared-utils/src/ease.ts` 里的写法 - {Object} extraTransform,只有在你想要修改 CSS transform 的一些其他属性的时候,你才需要传入此参数,结构如下: ```js let extraTransform = { // 起点的属性 start: { scale: 0 }, // 终点的属性 end: { scale: 1.1 } } bs.scrollTo(0, -60, 300, undefined, extraTransform) ``` - **返回值**:无 - **作用**:滚动到指定的 x,y 位置。 ### scrollBy(x, y, time, easing) - **参数**: - {number} x 横轴变化量(单位 px) - {number} y 纵轴变化量(单位 px) - {number} time 滚动动画执行的时长(单位 ms) - {Object} easing 缓动函数,一般不建议修改,如果想修改,参考源码中的 `packages/shared-utils/src/ease.ts` 里的写法 - **返回值**:无 - **作用**:相对于当前位置偏移滚动 x,y 的距离。 ### scrollToElement(el, time, offsetX, offsetY, easing) - **参数**: - {DOM | string} el 滚动到的目标元素, 如果是字符串,则内部会尝试调用 querySelector 转换成 DOM 对象。 - {number} time 滚动动画执行的时长(单位 ms) - {number | boolean} offsetX 相对于目标元素的横轴偏移量,如果设置为 true,则滚到目标元素的中心位置 - {number | boolean} offsetY 相对于目标元素的纵轴偏移量,如果设置为 true,则滚到目标元素的中心位置 - {Object} easing 缓动函数,一般不建议修改,如果想修改,参考源码中的 `packages/shared-utils/src/ease.ts` 里的写法 - **返回值**:无 - **作用**:滚动到指定的目标元素。 ### stop() - **参数**:无 - **返回值**:无 - **作用**:立即停止当前运行的滚动动画。 ### enable() - **参数**:无 - **返回值**:无 - **作用**:启用 BetterScroll, 默认 开启。 ### disable() - **参数**:无 - **返回值**:无 - **作用**:禁用 BetterScroll,DOM 事件(如 touchstart、touchmove、touchend)的回调函数不再响应。 ### destroy() - **参数**:无 - **返回值**:无 - **作用**:销毁 BetterScroll,解绑事件。 ### on(type, fn, context) - **参数**: - {string} type 事件名 - {Function} fn 回调函数 - {Object} context 函数执行的上下文环境,默认是 this - **返回值**:无 - **作用**:监听当前实例上的钩子函数。如:scroll、scrollEnd 等。 - **示例**: ```javascript import BScroll from '@better-scroll/core' let scroll = new BScroll('.wrapper', { probeType: 3 }) function onScroll(pos) { console.log(`Now position is x: ${pos.x}, y: ${pos.y}`) } scroll.on('scroll', onScroll) ``` ### once(type, fn, context) - **参数**: - {string} type 事件名 - {Function} fn 回调函数 - {Object} context 函数执行的上下文环境,默认是 this - **返回值**:无 - **作用**:监听一个自定义事件,但是只触发一次,在第一次触发之后移除监听器。 ### off(type, fn) - **参数**: - {string} type 事件名 - {Function} fn 回调函数 - **返回值**:无 - **作用**:移除自定义事件监听器。只会移除这个回调的监听器。 - **示例**: ```javascript import BScroll from '@better-scroll/core' let scroll = new BScroll('.wrapper', { probeType: 3 }) function handler() { console.log('bs is scrolling now') } scroll.on('scroll', handler) scroll.off('scroll', handler) ``` ## 事件 VS 钩子 基于 2.x 的架构设计,以及对 1.x 事件的兼容,我们延伸出两个概念 ——『**事件**』以及『**钩子**』。从本源上来说它们都是属于 `EventEmitter` 实例,只是叫法不一样。下面我们从节选的源码来讲解一下: ```typescript export default BScrollCore extends EventEmitter { hooks: EventEmitter } ``` - **BScrollCore** 本身继承了 EventEmitter。它派发出来的,我们都称之为『**事件**』。 ```js import BScroll from '@better-scroll/core' let bs = new BScroll('.wrapper', {}) // 监听 bs 的 scroll 事件 bs.on('scroll', () => {}) // 监听 bs 的 refresh 事件 bs.on('refresh', () => {}) ``` - **BScrollCore.hooks** hooks 也是 EventEmitter 的实例。它派发出来的,我们都称之为『**钩子**』。 ```js import BScroll from '@better-scroll/core' let bs = new BScroll('.wrapper', {}) // 监听 bs 的 refresh 钩子 bs.hooks.on('refresh', () => {}) // 监听 bs 的 enable 钩子 bs.hooks.on('enable', () => {}) ``` 相信现在大家对两者有了更好的区分吧,『**事件**』是为了 1.x 的兼容考虑,用户一般关注的是事件的派发,但是如果要编写一款插件,你应该更加关注『**钩子**』。 ## 事件 在 2.0 当中,BetterScroll 事件与 1.x 的事件是拉齐的,只有 BetterScroll 会派发『**事件**』,如果你在编写插件的时候需要暴露事件,你也应该通过 BetterScroll 来派发,[详细的教程看这](../plugins/how-to-write.html),目前的事件分为下面几种: - **refresh** - **触发时机**:BetterScroll 重新计算 ```js import BetterScroll from '@better-scroll/core' const bs = new BetterScroll('.wrapper', {}) bs.on('refresh', () => {}) ``` - **enable** - **触发时机**:BetterScroll 启用,开始响应用户交互 ```js bs.on('enable', () => {}) ``` - **disable** - **触发时机**:BetterScroll 禁用,不再响应用户交互 ```js bs.on('disable', () => {}) ``` - **beforeScrollStart** - **触发时机**:用户手指放在滚动区域的时候 ```js bs.on('beforeScrollStart', () => {}) ``` - **scrollStart** - **触发时机**:content 元素满足滚动条件,即将开始滚动 ```js bs.on('scrollStart', () => {}) ``` - **scroll** - **触发时机**:正在滚动 ```js bs.on('scroll', (position) => { console.log(position.x, position.y) }) ``` - **scrollEnd** - **触发时机**:滚动结束,或者让一个正在滚动的 content 强制停止 ```js bs.on('scrollEnd', () => {}) ``` - **scrollCancel** - **触发时机**:滚动取消 ```js bs.on('scrollCancel', () => {}) ``` - **touchEnd** - **触发时机**:用户手指离开滚动区域 ```js bs.on('touchEnd', () => {}) ``` - **flick** - **触发时机**:用户触发轻拂操作 ```js bs.on('flick', () => {}) ``` - **destroy** - **触发时机**:BetterScroll 销毁 ```js bs.on('destroy', () => {}) ``` - **contentChanged** - **触发时机**:在调用 `bs.refresh()`,探测到 content DOM 变成了其他元素的时候 ```typescript // bs 版本 >= 2.0.4 bs.on('contentChanged', (newContent: HTMLElement) => {}) ``` 以下的事件必须注册括号中的**插件**才会派发: - **alterOptions(__mouse-wheel__)** - **触发时机**:滚轮滚动开始 ```js import BetterScroll from '@better-scroll/core' import MouseWheel from '@better-scroll/mouse-wheel' BetterScroll.use(MouseWheel) const bs = new BetterScroll('.wrapper', { mouseWheel: true }) bs.on('alterOptions', (mouseWheelOptions) => { /** * mouseWheelOptions.speed:鼠标滚轮滚动的速度 * mouseWheelOptions.invert:滚轮滚动和 BetterScroll 滚动的方向是否一致 * mouseWheelOptions.easeTime:滚动动画的缓动时长。 * mouseWheelOptions.discreteTime:触发 wheelEnd 的间隔时长 * mouseWheelOptions.throttleTime:滚轮滚动是高频率的动作,因此可以通过 throttleTime 来限制触发频率 * mouseWheelOptions.dampingFactor:阻尼因子,当超出边界会施加阻力 **/ }) ``` - **mousewheelStart(__mouse-wheel__)** - **触发时机**:滚轮滚动开始 ```js import BetterScroll from '@better-scroll/core' import MouseWheel from '@better-scroll/mouse-wheel' BetterScroll.use(MouseWheel) const bs = new BetterScroll('.wrapper', { mouseWheel: true }) bs.on('mousewheelStart', () => {}) ``` - **mousewheelMove(__mouse-wheel__)** - **触发时机**:滚轮滚动中 ```js bs.on('mousewheelMove', () => {}) ``` - **mousewheelEnd(__mouse-wheel__)** - **触发时机**:滚轮滚动结束 ```js bs.on('mousewheelEnd', () => {}) ``` - **pullingDown(__pull-down__)** - **触发时机**:当顶部下拉距离超过阈值 ```js import BetterScroll from '@better-scroll/core' import Pulldown from '@better-scroll/pull-down' BetterScroll.use(Pulldown) const bs = new BetterScroll('.wrapper', { pullDownRefresh: true }) bs.on('pullingDown', () => { await fetchData() bs.finishPullDown() }) ``` - **pullingUp(__pull-up__)** - **触发时机**:当底部下拉距离超过阈值 ```js import BetterScroll from '@better-scroll/core' import Pullup from '@better-scroll/pull-up' BetterScroll.use(Pullup) const bs = new BetterScroll('.wrapper', { pullUpLoad: true }) bs.on('pullingUp', () => { await fetchData() bs.finishPullUp() }) ``` - **slideWillChange(__slide__)** - **触发时机**:轮播图即将要切换 Page ```js import BetterScroll from '@better-scroll/core' import Slide from '@better-scroll/slide' BetterScroll.use(Slide) const bs = new BetterScroll('.wrapper', { slide: true, momentum: false, bounce: false, probeType: 2 }) bs.on('slideWillChange', (page) => { // 即将要切换的页面 console.log(page.pageX, page.pageY) }) ``` - **beforeZoomStart(__zoom__)** - **触发时机**:双指接触缩放元素时 ```js import BetterScroll from '@better-scroll/core' import Zoom from '@better-scroll/zoom' BetterScroll.use(Zoom) const bs = new BetterScroll('.wrapper', { zoom: true }) bs.on('beforeZoomStart', () => {}) ``` - **zoomStart(__zoom__)** - **触发时机**:双指缩放距离超过最小阈值 ```js bs.on('zoomStart', () => {}) ``` - **zooming(__zoom__)** - **触发时机**:双指缩放行为正在进行时 ```js bs.on('zooming', ({ scale }) => { // scale 当前 scale }) ``` - **zoomEnd(__zoom__)** - **触发时机**:双指缩放行为结束后 ```js bs.on('zoomEnd', ({ scale }) => {}) ``` ## 钩子 钩子是 2.0 版本延伸出来的概念,它的本质与事件相同,都是 EventEmitter 实例,也就是典型的订阅发布模式。BScrollCore 作为一个最小的滚动单元,内部也是存在非常多的功能类,每个功能类都有一个叫 hooks 的属性,它架起了不同类之间沟通的桥梁。如果你要编写一个复杂的插件,钩子是必须需要掌握的内容。 - **BScrollCore.hooks** - **beforeInitialScrollTo** - **触发时机**:初始化加载完插件,需要滚动到指定位置 - **参数**:position 对象 - `{ x: number, y: number }` - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) bs.hooks.on('beforeInitialScrollTo', (postion) => { postion.x = 0 position.y = -200 // 初始化滚动至 -200 的位置 }) ``` - **refresh** - **触发时机**:重新计算 BetterScroll - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) bs.hooks.on('refresh', () => { console.log('refreshed') }) ``` - **enable** - **触发时机**:启用 BetterScroll,响应用户行为 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) bs.hooks.on('enable', () => { console.log('enabled') }) ``` - **disable** - **触发时机**:禁用 BetterScroll,不再响应用户行为 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) bs.hooks.on('disable', () => { console.log('disabled') }) ``` - **destroy** - **触发时机**:销毁 BetterScroll - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) bs.hooks.on('destroy', () => { console.log('destroyed') }) ``` - **contentChanged** - **触发时机**:在调用 `bs.refresh()`,探测到 content DOM 变成了其他元素的时候 - **示例** ```typescript import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) // bs 版本 >= 2.0.4 bs.hooks.on('contentChanged', (newContent: HTMLElement) => { console.log(newContent) }) ``` - **ActionsHandler.hooks** - **beforeStart** - **触发时机**:刚响应 touchstart 事件,还未记录手指在屏幕点击的位置 - **参数**:event 事件对象 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actionsHandler.hooks hooks.on('beforeStart', (event) => { console.log(event.target) }) ``` - **start** - **触发时机**:记录完手指在屏幕点击的位置,即将触发 touchmove - **参数**:event 事件对象 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actionsHandler.hooks hooks.on('start', (event) => { console.log(event.target) }) ``` - **move** - **触发时机**:响应 touchmove 事件,记录完手指在屏幕点击的位置 - **参数**:拥有如下属性的对象 - `{ number } deltaX`:x 方向的手指偏移量 - `{ number } deltaY`:y 方向的手指偏移量 - `{ event } e`:event 事件对象 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actionsHandler.hooks hooks.on('move', ({ deltaX, deltaY, e }) => {}) ``` - **end** - **触发时机**:响应 touchend 事件 - **参数**:event 事件对象 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actionsHandler.hooks hooks.on('end', (event) => {}) ``` - **click** - **触发时机**:触发 click 事件 - **参数**:event 事件对象 - **ScrollerActions.hooks** - **start** - **触发时机**:记录完所有的滚动初始信息 - **参数**:event 事件对象 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actions.hooks hooks.on('start', (event) => { console.log(event.target) }) ``` - **beforeMove** - **触发时机**:在检验是否是合法的滚动之前 - **参数**:event 事件对象 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actions.hooks hooks.on('beforeMove', (event) => { console.log(event.target) }) ``` - **scrollStart** - **触发时机**:校验是合法的滚动,并且即将开始滚动 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actions.hooks hooks.on('scrollStart', () => {}) ``` - **scroll** - **触发时机**:正在滚动 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actions.hooks hooks.on('scroll', () => {}) ``` - **beforeEnd** - **触发时机**:刚执行 touchend 事件回调,但是还未更新最终位置 - **参数**:event 事件对象 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actions.hooks hooks.on('beforeEnd', (event) => { console.log(event) }) ``` - **end** - **触发时机**:刚执行 touchend 事件回调并且更新滚动方向 - **参数**:两个参数,第一个是 event 事件对象,第二个是当前位置 - `{ event } e`:事件对象 - `{ x: number, y: number } postion`:当前位置 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actions.hooks hooks.on('end', (e, postion) => { console.log(e) }) ``` - **scrollEnd** - **触发时机**:滚动即将结束,但还需要校验一次滚动行为是否触发了 flick、momentum 行为。 - **参数**:两个参数,第一个是当前位置,第二个是动画时长 - `{ x: number, y: number } postion`:当前位置 - `{ number } duration`:动画时长 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actions.hooks hooks.on('beforeEnd', (pos, duration) => { console.log(pos) }) ``` - **coordinateTransformation** - **触发时机**:计算完用户手指的偏移量之后,发生滚动之前。 - **参数**: - `{ deltaX: number, deltaY: number } transformateDeltaData`:偏移量对象 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.actions.hooks hooks.on('coordinateTransformation', (:transformateDeltaData) => { // 获取用户手指移动的距离 const originDeltaX = transformateDeltaData.deltaX const originDeltaY = transformateDeltaData.deltaY // 变换位移 transformateDeltaData.deltaX = originDeltaY transformateDeltaData.deltaY = originDeltaX // transformateDeltaData.deltaX 最终作用在 BetterScroll content DOM 的 translateX // transformateDeltaData.deltaY 最终作用在 BetterScroll content DOM 的 translateY }) ``` 该钩子通常是为了修正当 BetterScroll 的 wrapper DOM 的祖先元素发生旋转的时候,用户自定义位移变换的逻辑,大部分情况下只需要配置 [quadrant](./base-scroll-options.html#quadrant) 即可。 - **Behavior.hooks** - **beforeComputeBoundary** - **触发时机**:即将计算滚动边界 - **参数**:boundary 对象 - `{ minScrollPos: number, maxScrollPos: number } boundary` - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.scrollBehaviorX.hooks hooks.on('beforeComputeBoundary', () => {}) ``` - **computeBoundary** - **触发时机**:计算滚动边界 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.scrollBehaviorX.hooks hooks.on('computeBoundary', (boundary) => { console.log(boundary.minScrollPos) // 上边界最大值,正的越多,下拉的幅度越大 console.log(boundary.maxScrollPos) // 下边界最小值,负的越多,滚的越远 }) ``` - **momentum** - **触发时机**:满足触发 momentum 动量动画条件,并且在计算之前 - **参数**:两个参数,第一个是 momentumData 对象,第二个是滚动偏移量 - `{ destination: number, duration: number, rate: number} momentumData`:destination 是目标位置,duration 是缓动时长,rate 是斜率 - `{ number } distance`:触发 momentum 的滚动偏移量 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.scrollBehaviorX.hooks hooks.on('momentum', (momentumData, distance) => {}) ``` - **end** - **触发时机**:不满足触发 momentum 动量动画条件 - **参数**:momentumInfo 对象 - `{ destination: number, duration: number} momentumInfo`:destination 是目标位置,duration 是缓动时长 - **示例** ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.scrollBehaviorX.hooks hooks.on('end', (momentumInfo) => { console.log(momentumInfo.destination) console.log(momentumInfo.duration) }) ``` - **Animation.hooks(useTransition: false)** - **forceStop** - **触发时机**:强制让一个滚动的 bs 停止 - **参数**:position 对象 - `{ x: number, y: number } position`:当前坐标值 - **move** - **触发时机**:滚动进行中 - **参数**:position 对象 - `{ x: number, y: number } position`:当前坐标值 - **end** - **触发时机**:滚动结束 - **参数**:position 对象 - `{ x: number, y: number } position`:当前坐标值 - **Translater.hooks** - **beforeTranslate** - **触发时机**:在修改 content 元素的 transform style 之前,zoom 插件监听了钩子 - **参数**:第一个是 transformStyle 数组,第二个是 point 对象 - `{ ['translateX(0px)'|'translateY(0px)'] } transformStyle`:当前 transform 对应的属性值 - `{ x: number, y: number } point`:x 对应 translateX 的值,y 对应 translateY 的值 ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.translater.hooks hooks.on('beforeTranslate', (transformStyle, point) => { transformStyle.push('scale(1.2)') console.log(transformStyle) // ['translateX(0px)', 'translateY(0px)', 'scale(1.2)'] console.log(point) // { x: 0, y: 0 } }) ``` - **translate** - **触发时机**:修改 content 元素的 transform style 之后,wheel 插件监听了钩子 - **参数**:point 对象 - `{ x: number, y: number } point`:x 对应 translateX 的值,y 对应 translateY 的值 ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.translater.hooks hooks.on('translate', (point) => { console.log(point) // { x: 0, y: 0 } }) ``` - **Transition.hooks(useTransition: true)** - **forceStop** - **触发时机**:强制让一个正在做动画的 bs 停止 - **参数**:position 对象 - `{ x: number, y: number } position`:当前坐标值 - **move** - **触发时机**:滚动进行中 - **参数**:position 对象 - `{ x: number, y: number } position`:当前坐标值 - **end** - **触发时机**:滚动结束 - **参数**:position 对象 - `{ x: number, y: number } position`:当前坐标值 - **time** - **触发时机**:CSS3 transition 开始之前,wheel 插件监听了该钩子 - **参数**:CSS3 transition duration ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.animater.hooks hooks.on('time', (duration) => { console.log(duration) // 800 }) ``` - **timeFunction** - **触发时机**:CSS3 transition 开始之前,wheel 插件监听了该钩子 - **参数**:CSS3 transition-timing-function ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.animater.hooks hooks.on('timeFunction', (easing) => { console.log(easing) // cubic-bezier(0.1, 0.7, 1.0, 0.1) }) ``` - **Scroller.hooks** - **beforeStart** 同 `ScrollerActions.hooks.start` - **beforeMove** 同 `ScrollerActions.hooks.beforeMove` - **beforeScrollStart** 同 `ScrollerActions.hooks.start` - **scrollStart** 同 `ScrollerActions.hooks.scrollStart` - **scroll** - **触发时机**:滚动进行中 - **参数**:position 对象 - `{ x: number, y: number } position`:当前坐标值 - **beforeEnd** 同 `ScrollerActions.hooks.beforeEnd` - **touchEnd** - **触发时机**:用户手指离开滚动区域 ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('touchEnd', () => { console.log('your finger has leave') }) ``` - **end** - **触发时机**:touchEnd 之后,校验 click 之前触发,pull-down 插件基于这个钩子实现 - **参数**:position 对象 - `{ x: number, y: number } position`:当前位置 ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('end', (position) => { console.log(position.x) console.log(position.y) }) ``` - **scrollEnd** - **触发时机**:滚动结束 - **参数**:position 对象 - `{ x: number, y: number } position`:当前坐标值 - **resize** - **触发时机**:window 尺寸发生改变 ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('resize', () => { console.log("window's size has changed") }) ``` - **flick** - **触发时机**:探测到手指轻拂动作 ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('flick', () => {}) ``` - **scrollCancel** - **触发时机**:滚动取消或者未发生 - **momentum** - **触发时机**:即将进行 momentum 动量位移,slide 插件监听了该钩子 - **参数**:scrollMetaData 对象 - `{ time: number, easing: EaseItem, newX: number, newY: number }`:time 是动画时长,easing是缓动函数,newX 和 newY 是终点 ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('momentum', (scrollMetaData) => { scrollMetaData.newX = 0 scrollMetaData.newY = -200 }) ``` - **scrollTo** - **触发时机**:调用 bs.scrollTo 方法的时候触发 - **参数**:endPoint 对象 - `{ x: number, y: number } endPoint`:终点坐标值 ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('scrollTo', (endPoint) => { console.log(endPoint.x) console.log(endPoint.y) }) bs.scrollTo(0, -200) ``` - **scrollToElement** - **触发时机**:调用 bs.scrollToElement 方法的时候触发,wheel 插件监听了该钩子 - **参数**:第一个是目标 DOM 对象,第二个是终点的坐标 - `{ HTMLElment } el` - `{ top: number, left: number } postion` ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('scrollToElement', (el, pos) => { console.log(el) console.log(pos.left) console.log(pos.top) }) bs.scrollToElement('.some-item', 300, true, true) ``` - **beforeRefresh** - **触发时机**:在 behavior 计算边界之前,slide 插件监听了该钩子 ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.wrapper', {}) const hooks = bs.scroller.hooks hooks.on('beforeRefresh', () => {}) ``` ::: tip 提示 细心的你会发现,有部分 Scroller.hooks 与 ScrollActions.hooks 的功能一模一样,其实我们内部采用了一种 [钩子冒泡](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/core/src/utils/bubbling.ts) 的策略,将内层功能类的钩子,通过冒泡的形式一直代理到 BetterScroll Instance 来兼容 1.x 的使用方式。 ::: ================================================ FILE: packages/vuepress-docs/docs/zh-CN/guide/base-scroll-options.md ================================================ # 配置项 BetterScroll 支持很多参数配置,可以在初始化的时候传入第二个参数,比如: ``` js import BScroll from '@better-scroll/core' let scroll = new BScroll('.wrapper',{ scrollY: true, click: true }) ``` 这样就实现了一个具有纵向可点击的滚动效果的列表。BetterScroll 支持的参数非常多,接下来我们来列举 BetterScroll 支持的参数。 ## startX - **类型**:`number` - **默认值**:`0` - **作用**:横轴方向初始化位置。 ## startY - **类型**:`number` - **默认值**:`0` - **作用**:纵轴方向初始化位置。 ## scrollX - **类型**:`boolean` - **默认值**: `false` - **作用**:当设置为 true 的时候,可以开启横向滚动。 - **备注**:当设置 [eventPassthrough](./base-scroll-options.html#eventpassthrough) 为 'horizontal' 的时候,该配置无效。 ## scrollY - **类型**:`boolean` - **默认值**:`true` - **作用**:当设置为 true 的时候,可以开启纵向滚动。 - **备注**:当设置 [eventPassthrough](./base-scroll-options.html#eventpassthrough) 为 'vertical' 的时候,该配置无效。 ## freeScroll - **类型**:`boolean` - **默认值**:`false` - **作用**:在默认情况下,由于人的手指无法做到绝对垂直或者水平的运动,因此在一次手指操作的过程中,都会存在横向以及纵向的偏移量,内部默认会摒弃偏移量较小的一个方向,保留另一个方向的滚动。但是在某些场景我们需要同时计算横向以及纵向的手指偏移距离,而不是只计算偏移量较大的一个方向,这个时候我们只要设置 `freeScroll` 为 true 即可。 - **备注**:当设置 [eventPassthrough](./base-scroll-options.html#eventpassthrough) 不为空的时候,该配置无效。 - **示例**: ```js // 手指起点的坐标 e1: { pageX: 120, pageY: 120 } // 手指终点的坐标 e2: { pageX: 121, pageY: 140 } // offsetX: e2.pageX - e1.pageX = 1 // offsetY: e2.pageY - e1.pageY = 20 // 如果 freeScroll 为 false, 由于 offsetY > offsetX + directionLockThreshold // offsetX 被重置为 0, 只保留 offsetY 的偏移量,因此只做一次纵向滚动 ``` ## directionLockThreshold - **类型**:`number` - **默认值**:`5` - **作用**:当 `freeScroll` 为 false 的情况,我们需要锁定只滚动一个方向的时候,我们在**初始滚动**的时候根据横轴和纵轴滚动的绝对值做差,当差值大于 `directionLockThreshold` 的时候来决定滚动锁定的方向。 - **备注**:当设置 [eventPassthrough](./base-scroll-options.html#eventpassthrough) 的时候,`directionLockThreshold` 设置无效,始终为 0。 ## eventPassthrough - **类型**: `string` - **默认值**:`''` - **可选值**:`'vertical' | 'horizontal'` - **作用**:有时候我们使用 BetterScroll 在某个方向模拟滚动的时候,希望在另一个方向保留原生的滚动(比如轮播图,我们希望横向模拟横向滚动,而纵向的滚动还是保留原生滚动,我们可以设置 `eventPassthrough` 为 vertical;相应的,如果我们希望保留横向的原生滚动,可以设置`eventPassthrough`为 horizontal)。 - **备注**:`eventPassthrough` 的设置会导致其它一些选项配置无效,需要小心使用它。 ## click - **类型**:`boolean` - **默认值**:`false` - **作用**:BetterScroll 默认会阻止浏览器的原生 click 事件。当设置为 true,BetterScroll 会派发一个 click 事件,我们会给派发的 event 参数加一个私有属性 `_constructed`,值为 true。 ## dblclick - **类型**:`boolean | Object` - **默认值**:`false` - **作用**:派发双击点击事件。当配置成 true 的时候,默认 2 次点击的延时为 300 ms,如果配置成对象可以修改 `delay`。 ```js dblclick: { delay: 300 } ``` ## tap - **类型**:`string` - **默认值**:`''` - **作用**:因为 BetterScroll 会阻止原生的 click 事件,我们可以设置 tap 为 'tap',它会在区域被点击的时候派发一个 tap 事件,你可以像监听原生事件那样去监听它。 ## bounce - **类型**:`boolean | Object` - **默认值**:`true` - **作用**:当滚动超过边缘的时候会有一小段回弹动画。设置为 true 则开启动画。 ```js bounce: { top: true, bottom: true, left: true, right: true } ``` `bounce` 可以支持关闭某些边的回弹效果,可以设置对应边的 `key` 为 `false` 即可。 :::tip 如果想要便捷的设置所有边为 true 或者 false,只需要设置 `bounce` 为 true 或 false 即可。 ::: ## bounceTime - **类型**:`number` - **默认值**:`800`(单位ms) - **作用**:设置回弹动画的动画时长。 ## momentum - **类型**:`boolean` - **默认值**:`true` - **作用**:当快速在屏幕上滑动一段距离的时候,会根据滑动的距离和时间计算出动量,并生成滚动动画。设置为 true 则开启动画。 ## momentumLimitTime - **类型**:`number` - **默认值**:`300`(单位ms) - **作用**:只有在屏幕上快速滑动的时间小于 `momentumLimitTime`,才能开启 momentum 动画。 ## momentumLimitDistance - **类型**:`number` - **默认值**:`15`(单位px) - **作用**:只有在屏幕上快速滑动的距离大于 `momentumLimitDistance`,才能开启 momentum 动画。 ## swipeTime - **类型**:`number` - **默认值**:`2500`(单位ms) - **作用**:设置 momentum 动画的动画时长。 ## swipeBounceTime - **类型**:`number` - **默认值**:`500`(单位ms) - **作用**:设置当运行 momentum 动画时,超过边缘后的回弹整个动画时间。 ## deceleration - **类型**:`number` - **默认值**:`0.0015` - **作用**:表示 momentum 动画的减速度。 ## flickLimitTime - **类型**:`number` - **默认值**:`200`(单位ms) - **作用**:有的时候我们要捕获用户的轻拂动作(短时间滑动一个较短的距离)。只有用户在屏幕上滑动的时间小于 `flickLimitTime` ,才算一次轻拂。 ## flickLimitDistance - **类型**:`number` - **默认值**:`100`(单位px) - **作用**:只有用户在屏幕上滑动的距离小于 `flickLimitDistance` ,才算一次轻拂。 ## resizePolling - **类型**:`number` - **默认值**:`60`(单位ms) - **作用**:当窗口的尺寸改变的时候,需要对 BetterScroll 做重新计算,为了优化性能,我们对重新计算做了延时。60ms 是一个比较合理的值。 ## probeType - **类型**:`number` - **默认值**:`0` - **可选值**:`1|2|3` - **作用**:决定是否派发 scroll 事件,对页面的性能有影响,尤其是在 `useTransition` 为 true 的模式下。 ```js // 派发 scroll 的场景分为两种: // 1. 手指作用在滚动区域(content DOM)上; // 2. 调用 scrollTo 方法或者触发 momentum 滚动动画(其实底层还是调用 scrollTo 方法) // 对于 v2.1.0 版本,对 probeType 做了一次统一 // 1. probeType 为 0,在任何时候都不派发 scroll 事件, // 2. probeType 为 1,仅仅当手指按在滚动区域上,每隔 momentumLimitTime 毫秒派发一次 scroll 事件, // 3. probeType 为 2,仅仅当手指按在滚动区域上,一直派发 scroll 事件, // 4. probeType 为 3,任何时候都派发 scroll 事件,包括调用 scrollTo 或者触发 momentum 滚动动画 ``` ## preventDefault - **类型**:`boolean` - **默认值**:`true` - **作用**:当事件派发后是否阻止浏览器默认行为。这个值应该设为 true,除非你真的知道你在做什么,通常你可能用到的是 `preventDefaultException`。 ## preventDefaultException - **类型**:`Object` - **默认值**:`{ tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT|AUDIO)$/}` - **作用**:BetterScroll 会阻止原生的滚动,这样也同时阻止了一些原生组件的默认行为。这个时候我们不能对这些元素做 preventDefault,所以我们可以配置 preventDefaultException。默认值 `{tagName: /^(INPUT|TEXTAREA|BUTTON|SELECT|AUDIO)$/}`表示标签名为 input、textarea、button、select、audio 这些元素的默认行为都不会被阻止。 - **备注**:这是一个非常有用的配置,它的 key 是 DOM 元素的属性值,value 可以是一个正则表达式。比如我们想配一个 class 名称为 test 的元素,那么配置规则为 `{className:/(^|\s)test(\s|$)/}`。 ## tagException - **类型**:`Object` - **默认值**:`{ tagName: /^TEXTAREA$/ }` - **作用**:如果 BetterScroll 嵌套了 textarea 等表单元素,往往用户的预期应该是滑动 textarea 不应该引起 bs 滚动,也就是如果操纵的 DOM(eg:textarea 标签) 命中了配置的规则,bs 不会滚动。 - **备注**:这是一个非常有用的配置,它的 key 是 DOM 元素的属性值,value 可以是一个正则表达式。比如我们想配一个 classname 含有 test 类名的元素,那么配置规则为 `{className:/(^|\s)test(\s|$)/}`。 ## HWCompositing - **类型**:`boolean` - **默认值**:`true` - **作用**:是否开启硬件加速,开启它会在 content 元素上添加 `translateZ(1px)` 来开启硬件加速从而提升动画性能,有很好的滚动效果。 - **备注**:只有支持硬件加速的浏览器开启才有效果。 ## useTransition - **类型**:`boolean` - **默认值**:`true` - **作用**:是否使用 CSS3 transition 动画。如果设置为 false,则使用 requestAnimationFrame 做动画。 ## bindToWrapper - **类型**:`boolean` - **默认值**:`false` - **作用**:touchmove 事件通常会绑定到 document 上而不是滚动的容器(wrapper)上,当移动的过程中光标(通常对于 PC 场景)离开滚动的容器滚动仍然会继续,这通常是期望的。当然你也可以把 move 事件绑定到滚动的容器上,`bindToWrapper` 设置为 true 即可,这样一旦移动的过程中光标离开滚动的容器,滚动会立刻停止。 - **注意**:对于移动端来说,就算 touchmove 事件绑定在 wrapper 上,手指离开 wrapper,依然能移动 wrapper。 ## disableMouse - **类型**:`boolean` - **默认值**:根据当前浏览器环境计算而来 - **作用**:当在移动端环境(支持 touch 事件),disableMouse 会计算为 true,这样就不会监听鼠标相关的事件,而在 PC 环境,disableMouse 会计算为 false,就会监听鼠标相关事件。 ## disableTouch - **类型**:`boolean` - **默认值**:根据当前浏览器环境计算而来 - **作用**:当在移动端环境(支持 touch 事件),disableTouch 会计算为 false,监听 touch 相关的事件,而在 PC 环境,disableTouch 会计算为 true,不会监听 touch 相关事件。 ::: warning 考虑到用户的一些特定场景,比如在**平板电脑需要支持 touch 事件,平板电脑接入鼠标又得支持 mouse 事件**,那么实例化 BetterScroll 需要如下配置: ```js let bs = new BScroll('.wrapper', { disableMouse: false, disableTouch: false }) ``` 由于不同设备、不同浏览器环境的底层实现逻辑不同,BetterScroll 内部计算是否监听 touch 还是 mouse 事件可能会有判断失误,因此你可以根据以上的选项配置来解决这类问题。 ::: ## autoBlur - **类型**:`boolean` - **默认值**:`true` - **作用**:在滚动之前会让当前激活的元素(input、textarea)自动失去焦点。 ## stopPropagation - **类型**:`boolean` - **默认值**:`false` - **作用**:是否阻止事件冒泡。多用在嵌套 scroll 的场景。 ## bindToTarget - **类型**:`boolean` - **默认值**:`false` - **作用**:将 touch 或者 mouse 事件绑定在 `content` 元素而不是容器 `wrapper`上,多用于 [movable 场景](../plugins/movable.html)。 ## autoEndDistance - **类型**:`number` - **默认值**:`5` - **作用**:当手指操作幅度过大,滑出视口导致可能没有触发 touchend 事件,因此 autoEndDistance 的作用就是当手指即将脱离当前视口的时候,自动调用 touchend 事件。默认距离边界 5px 的时候,结束滚动。 ## outOfBoundaryDampingFactor - **类型**:`number` - **默认值**:`1 / 3` - **作用**:当超过边界的时候,进行阻尼行为,阻尼因子越小,阻力越大,取值范围:[0, 1]。 ## specifiedIndexAsContent - **类型**:`number` - **默认值**:`0` - **作用**:指定 `wrapper` 对应索引的子元素作为 `content`,默认情况下 BetterScroll 采用的是 `wrapper` 的第一个子元素作为 content。 ```html
      1.1
      1.2
      2.1
      2.2
      ``` ```js // 针对以上 DOM 结构,在 BetterScroll 版本 <= 2.0.3,内部只会使用 wrapper.children[0],也就是 div.content1 作为 content // 当 版本 >= 2.0.4 的时候,可以通过 specifiedIndexAsContent 配置项来指定 content let bs = new BScroll('.wrapper', { specifiedIndexAsContent: 1 // 使用 div.content2 作为 BetterScroll 的 content }) ``` ## quadrant - **类型**:`1 | 2 | 3 | 4` - **默认值**:`1` - **作用**:当 BetterScroll 的 wrapper DOM 的祖先元素被 CSS 强制旋转之后,原先的 x 以及 y 方向的位移需要发生一定的变换才能保证交互合理 ```html
      1.1
      1.2
      ``` ```js let bs = new BScroll('.wrapper', { quadrant: 2 }) ``` 1. 当 wrapper 的父元素或者祖先元素旋转的角度为 (315, 45],quadrant 保持默认值即可; 2. 当 wrapper 的父元素或者祖先元素旋转的角度为 (45, 135],quadrant **建议**为 `2`,尤其是 90 度,quadrant **必须**为 `2`; 3. 当 wrapper 的父元素或者祖先元素旋转的角度为 (135, 225],quadrant **建议**为 `3`,尤其是 180 度,quadrant **必须**为 `3`; 4. 当 wrapper 的父元素或者祖先元素旋转的角度为 (225, 315],quadrant **建议**为 `4`,尤其是 270 度,quadrant **必须**为 `4`; 5. 当旋转角度比较特殊的时候,比如 30 度,200 度,你可能不满意内置的变换逻辑,你可以通过 `coordinateTransformation` hook 来自定义你自己的变换逻辑。 ```js let bs = new BScroll('.wrapper', { quadrant: 1 // 保持默认即可 }) bs.scroller.actions.hooks.on( bs.scroller.actions.hooks.eventTypes.coordinateTransformation, (transformateDeltaData) => { // 获取用户手指移动的距离 const originDeltaX = transformateDeltaData.deltaX const originDeltaY = transformateDeltaData.deltaY // 变换位移 transformateDeltaData.deltaX = originDeltaY transformateDeltaData.deltaY = originDeltaX // transformateDeltaData.deltaX 最终作用在 BetterScroll content DOM 的 translateX // transformateDeltaData.deltaY 最终作用在 BetterScroll content DOM 的 translateY } ) ``` 例如:使用 CSS 将横向滚动的 BetterScroll 翻转。 ================================================ FILE: packages/vuepress-docs/docs/zh-CN/guide/base-scroll.md ================================================ # 核心滚动 在 BetterScroll 2.0 的设计当中,我们抽象了核心滚动部分,它作为 BetterScroll 的最小使用单元,压缩体积比 `1.0` 小了将近三分之一,往往你可能只需要完成一个纯粹的滚动需求,那么你只需要引入这一个库,方式如下: ```bash npm install @better-scroll/core --save ``` ```js import BScroll from '@better-scroll/core' const bs = new BScroll('.div') ``` ## 上手 BetterScroll 有多种滚动模式。 - **垂直滚动** :::warning BetterScroll 实时派发 scroll 事件,是需要设置 `probeType` 为 3。 ::: - **水平滚动** :::warning BetterScroll 实现横向滚动,对 CSS 是比较苛刻的。首先你要保证 wrapper 不换行,并且 content 的 display 是 inline-block。 ```stylus .scroll-wrapper // ... white-space nowrap .scroll-content // ... display inline-block ``` ::: - **freeScroll(水平与垂直同时滚动)** ## 动态 content 对于 `2.0.4` 版本,已经具备了探测 content 元素变成其他元素的能力,可以查看下面的例子。 ## specifiedIndexAsContent 对于 `2.0.4` 版本,可以指定 **wrapper** 的某一个 children 作为 **content**,在之前的版本,BetterScroll只会处理 wrapper 的第一个子元素。[详细的文档在这](./base-scroll-options.html#specifiedindexascontent-2-0-4)。 ## quadrant 对于 `2.3.0` 版本,如果 BetterScroll 的 wrapper DOM 的父元素或者祖先元素发生旋转,可以通过 `quadrant` 选项来修正用户的交互行为。 - **竖向滚动强制变换成横向滚动** - **横向滚动强制翻转** ## 温馨提示 :::tip **任何时候如果出现无法滚动的情况,都应该首先查看 content 元素高度/宽度是否大于 wrapper 的高度/宽度**。这是内容能够滚动的前提条件。 如果内容存在图片的情况,可能会出现 DOM 元素渲染时图片还未下载,因此内容元素的高度小于预期,出现滚动不正常的情况。此时你应该在图片加载完成后,比如 onload 事件回调中,调用 `bs.refresh` 方法,它会重新计算最新的滚动距离。 ::: ================================================ FILE: packages/vuepress-docs/docs/zh-CN/guide/how-to-install.md ================================================ # 安装 ## NPM BetterScroll 托管在 NPM 上,执行如下命令安装: ```bash npm install @better-scroll/core --save // or yarn add @better-scroll/core ``` 接下来就可以在代码中引入了,[webpack](https://webpack.js.org/) 等构建工具都支持从 node_modules 里引入代码: ``` js import BScroll from '@better-scroll/core' ``` 如果是 commonjs 的语法,如下: ``` js var BScroll = require('@better-scroll/scroll') ``` ## script 加载 BetterScroll 也支持直接用 script 加载的方式,加载后会在 window 上挂载一个 BScroll 的对象。 ```html ``` ```js let wrapper = document.getElementById("wrapper") let bs = new BScroll(wrapper, {}) ``` ## 具备所有插件能力的 BetterScroll ```bash npm install better-scroll --save // or yarn add better-scroll ``` ```js import BetterScroll from 'better-scroll' let bs = new BetterScroll('.wrapper', {}) ``` 也可以通过 CDN 加载。 ```html ``` ```js let bs = BetterScroll.createBScroll('.wrapper', {}) ``` ================================================ FILE: packages/vuepress-docs/docs/zh-CN/guide/use.md ================================================ # 使用 ## 基础滚动 如果你只需要一个拥有基础滚动能力的列表,只需要引入 core。 ```js import BScroll from '@better-scroll/core' let bs = new BScroll('.wrapper', { // ...... 详见配置项 }) ``` ## 增强型滚动 如果你需要一些额外的 feature。比如 `pull-up`,你需要引入额外的插件,详情查看[插件](/docs/zh-CN/plugins)。 ```js import BScroll from '@better-scroll/core' import Pullup from '@better-scroll/pull-up' // 注册插件 BScroll.use(Pullup) let bs = new BScroll('.wrapper', { probeType: 3, pullUpLoad: true }) ``` ## 全能力的滚动 如果你觉得一个个引入插件很费事,我们提供了一个拥有全部插件能力的 BetterScroll 包。它的使用方式与 `1.0` 版本一模一样,但是体积会相对大很多,推荐**按需引入**。 ```js import BScroll from 'better-scroll' let bs = new BScroll('.wrapper', { // ... pullUpLoad: true, wheel: true, scrollbar: true, // and so on }) ``` ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/README.md ================================================ # 插件 ## 为什么要有插件 为了解耦 BetterScroll 1.x 的各个 feature 的功能,防止 bundle 体积无限制的增加。在 2.x 的架构设计当中,采用了『插件化』 的架构设计。对于 1.x 的各个 feature,在 2.x 都将以 Plugin 的形式实现。 已有的插件: - [pulldown](./pulldown.html) - [pullup](./pullup.html) - [scrollbar](./scroll-bar.html) - [slide](./slide.html) - [wheel](./wheel.html) - [zoom](./zoom.html) - [mouse-wheel](./mouse-wheel.html) - [observe-dom](./observe-dom.html) - [observe-image](./observe-image.html) - [nested-scroll](./nested-scroll.html) - [infinity](./infinity.html) - [movable](./movable.html) - [indicators](./indicators.html) 非常欢迎大家参与插件的贡献,在此之前,你需要花点时间了解这个[如何编写插件](./how-to-write.html)。 ## 使用插件 通过全局方法 `BScroll.use()` 使用插件。它需要在你调用 `new BScroll()` 之前完成: ```js import BScroll from '@better-scroll/core' import Plugin from 'somewhere' BScroll.use(Plugin) new BScroll('.wrapper', { pluginKey: {} // pluginKey 对应 Plugin 类上静态属性 pluginName 的值,否则插件无法实例化 }) ``` ## 使用插件的方法和属性 插件中可能会暴露一些方法和属性,这些方法和属性在你执行完 `new BScroll()` 之后,会通过 `Object.defineProperty` 的方式代理至 `bs`。例如,zoom 插件中提供了 `zoomTo` 方法,你可以通过下面的方式来使用: ```js import BScroll from '@better-scroll/core' import Zoom from '@better-scroll/zoom' BScroll.use(Zoom) const bs = new BScroll('#scroll-wrapper', { freeScroll: true, scrollX: true, scrollY: true, disableMouse: true, useTransition: true, zoom: { start: 1, min: 0.5, max: 2 } }) bs.zoomTo(1.5, 0, 0) // 不用关心 zoom 插件实例,直接通过 bs 获取暴露的属性或者方法。 ``` ## 使用插件的事件 和方法、属性类似,插件中暴露的事件最终也会被代理至 `bs`。例如,zoom 插件中提供了 `zoomStart` 事件,你可以通过下面的方式来注册事件侦听器: ```js import BScroll from '@better-scroll/core' import Zoom from '@better-scroll/zoom' BScroll.use(Zoom) const bs = new BScroll('#scroll-wrapper', { freeScroll: true, scrollX: true, scrollY: true, zoom: { start: 1, min: 0.5, max: 2 } }) bs.on('zoomStart', zoomStartHandler) // so, you can do anything in zoomStartHandler ``` ## 具备所有插件能力的 BetterScroll 考虑到一个个注册插件比较麻烦,如果你的项目用到 BetterScroll 的全部插件能力,我们提供了一劳永逸的方案。 ```js import BScroll from 'better-scroll' const bs = new BScroll('#scroll-wrapper', { pullUpLoad: true, pullDownRefresh: true, scrollbar: true, // 等等 }) ``` ::: warning 注意 引用全部的 BetterScroll 可能对你的 bundle 体积有很大的冲击,而且随着 BetterScroll 的功能扩展,体积会无限制的增加,**请按需引入**。 ::: ::: warning 注意 通常情况下,你应该关注 BetterScroll 实例暴露出来的属性和方法,因为对于插件实例上的属性和方法,都已经代理到 bs 上面,如果你真的需要关心插件实例,你也可以通过 `bs.plugins` 来获取所有插件的信息。 ```js import BScroll from '@better-scroll/scroll' import zoom from '@better-scroll/zoom' BScroll.use(zoom) const bs = new BScroll('.wrapper', { zoom: true }) console.log(bs.plugins.zoom) // 获取对应插件实例 ``` ::: ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/compose-plugins.md ================================================ # 插件的高阶使用 在许多场景下,单插件的better-scroll实例(以下简称`BS`)很难满足我们当下的业务需求,往往我们需要通过多个插件的组合使用来实现我们想要的效果。 ## pullup-pulldown ## pullup-pulldown-slide ## pullup-pulldown-nestedscroll ## nestedscroll-slide ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/how-to-write.md ================================================ # 如何写一个插件 ### 构思插件的功能 ```js import BScroll from '@better-scroll/core' import MyPlugin from '@better-scroll/my-plugin' BScroll.use(MyPlugin) const bs = new BScroll('.wrapper', { myPlugin: { scrollText: 'I am scrolling', scrollEndText: 'Scroll has ended' }, // 或者 myPlugin: true }) // 使用插件暴露到 bs 的事件 bs.on('printScrollEndText', (scrollEndText) => { console.log(scrollEndText) // 打印 "Scroll has ended, position is (xx, yy)" }) // 使用插件代理到 bs 实例上的方法 bs.printScrollText() // 打印 "I am scrolling" ``` ### 编写插件 1. **TypeScript 声明合并以及暴露插件方法** ```typescript import BScroll from '@better-scroll/core' export type MyPluginOptions = Partial | true type MyPluginConfig = { scrollText: string, scrollEndText: string } interface PluginAPI { printScrollText(): void } declare module '@better-scroll/core' { interface CustomOptions { myPlugin?: MyPluginOptions } interface CustomAPI { myPlugin: PluginAPI } } ``` 这样做的好处,就是为了在引入 `myPlugin` 插件并且实例化 BetterScroll 的时候,能够有对应的 Options 提示以及 bs 能够有对应的方法提示,以 pulldown 插件为例: 2. **编写插件主体** - **BetterScroll 的插件需要是一个类,并且具有以下特性:** - 静态的 pluginName 属性。 - 实现 PluginAPI 接口(当且仅当需要把插件方法代理至 bs)。 - constructor 的第一个参数就是 BetterScroll 实例 `bs`,你可以通过 bs 的**事件**或者**钩子**来注入自己的逻辑。 ```typescript export default class MyPlugin implements PluginAPI { static pluginName = 'myPlugin' public options: MyPluginConfig constructor(public scroll: BScroll){ this.handleOptions() this.handleBScroll() this.registerHooks() } } ``` - **handleOptions** 合并用户传入的 options,收缩它的类型。 ```typescript import { extend } from '@better-scroll/shared-utils' export default class MyPlugin { private handleOptions() { const userOptions = (this.scroll.options.myPlugin === true ? {} : this.scroll.options.myPlugin) as Partial const defaultOptions: MyPluginConfig = { scrollText: 'I am scrolling', scrollEndText: 'Scroll has ended' } this.options = extend(defaultOptions, userOptions) } } ``` - **handleBScroll** 代理事件以及方法至 BetterScroll 实例。 ```typescript export default class MyPlugin implements PluginAPI { private handleBScroll() { const propertiesConfig = [ { key: 'printScrollText', sourceKey: 'plugins.myPlugin.printScrollText' } ] // 将 myPlugin.printScrollText 代理至 bs.printScrollText this.scroll.proxy(propertiesConfig) // 注册 printScrollEndText 事件至 bs,以至于用户可以通过 bs.on('printScrollEndText', handler) 来订阅事件 this.scroll.registerType(['printScrollEndText']) } printScrollText() { console.log(this.options.scrollText) } } ``` - **registerHooks** 钩入 bs 的钩子,实现插件的逻辑,并且派发插件自定义的事件。 ```typescript export default class MyPlugin implements PluginAPI { private registerHooks() { const scroll = this.scroll scroll.on(scroll.eventTypes.scrollEnd, ({ x, y }) => { scroll.trigger( scroll.eventTypes.printScrollEndText, `${this.options.scrollEndText}, position is (${x}, ${y})` ) }) } } ``` 恭喜你,一个简单的 BetterScroll 插件就已经完成啦,如果结合你的场景,需要更复杂的插件,可以仔细阅读 [事件与钩子大全](../guide/base-scroll-api.html#事件-vs-钩子),它能很好的帮助你来完成一款独特的插件。 查看[完整的 repo](https://github.com/better-scroll/plugin-tutorial),以及[线上例子](https://better-scroll.github.io/plugin-tutorial/) ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/indicators.md ================================================ # indicators ## 介绍 indicators 赋予了联动另外一个 BetterScroll 的能力,借助于此,可以实现**视觉滚动差**、**放大镜**等效果。 ::: tip 提示 这是一个非常强大并且具有创造力的插件,限制你的只有你的想象力! ::: ## 安装 ```bash npm install @better-scroll/indicators --save // or yarn add @better-scroll/indicators ``` ## 使用 首先引入 indicators 插件,并通过静态方法 `BScroll.use()` 注册插件 ```js import BScroll from '@better-scroll/core' import Indicators from '@better-scroll/indicators' BScroll.use(Indicators) ``` 接着在 `options` 传入正确的配置。 ```js new BScroll('.wrapper', { indicators: { // 详情可以参考下面的 demo relationElement: "联动的元素 DOM" } }) ``` ## 示例 - **放大镜效果** - **视觉滚动差** ## indicators 选项对象 ### relationElement - **类型**:`HTMLElement` 与另外一个 BetterScroll 关联的容器元素,正如上面的 demo, `div.scroll-indicator` 就是 releationElement。**releationElement 必须由用户传入,并且拥有子元素**。 ### relationElementHandleElementIndex - **类型**:`number` - **默认值**:`0` 指定 releationElement 的第几个子元素作为操控的元素,详细可以参考以上的 demo。 ### ratio - **类型**:`number | Ratio | undefined` - **默认值**:`undefined` ```ts type Ratio = { x: number // 指定 x 方向滚动距离的比例 y: number // 指定 y 方向滚动距离的比例饿 } ``` 指定 releationElement 与 BetterScroll 滚动距离的比例。一般情况下,**插件内部会自动计算**两者的滚动比例,但是你也可以手动指定比例,来实现 `视觉滚动差` 的效果。详细可以参考以上的 Demo。 ### interactive - **类型**:`boolean | undefined` - **默认值**:`undefined` 决定 relationElement 的第 relationElementHandleElementIndex 个子元素是否可以交互,当它置成 false 的时候,则不响应 touch / mouse 事件。 ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/infinity.md ================================================ # infinity ## 介绍 infinity 插件为 BetterScroll 提供了无限滚动的能力。如果有大量的列表数据需要渲染,可以使用 infinity 插件,此时 BetterScroll 只会渲染一定数量的 DOM 元素,从而使页面在大量数据时依然保持流畅滚动。 > 注意:除非你有大量的数据渲染需求,否则使用 core 即可。 ## 安装 ```shell npm install @better-scroll/infinity --save // or yarn add @better-scroll/infinity ``` ## 使用 首先引入 infinity 插件,并通过静态方法 `BScroll.use()` 初始化插件 ```js import BScroll from '@better-scroll/core' import InfinityScroll from '@better-scroll/infinity' BScroll.use(InfinityScroll) ``` 然后,实例化 BetterScroll 时需要传入相关配置项 infinity: ```typescript new BScroll('.bs-wrapper', { scrollY: true, infinity: { fetch(count) { // 获取大于 count 数量的数据,该函数是异步的,它需要返回一个 Promise。 // case 1. resolve 数据数组Array,来告诉 infinity 渲染数据,render 的第一个参数就是数据项 // case 2. resolve(false), 来停止无限滚动 }, render(item, div?: HTMLElement) { // item 是 fetch 函数提供的每一个数据项, // div 是页面回收的 DOM,可能不存在 // 如果 div 不存在,你需要创建一个新的 HTMLElement 元素 // 必须返回一个 HTMLElement }, createTombstone() { // 必须返回一个墓碑 DOM 节点。 } } }) ``` ::: danger 危险 `fetch`、`render`、`createTombstone` 必须按照注释来实现,否则内部会报错。 插件内部依赖 Promise,如果浏览器不支持,需要 Promise 的 Polyfill。 ::: ## 示例 ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/mouse-wheel.md ================================================ # mouse-wheel mouseWheel 扩展 BetterScroll 鼠标滚轮的能力。 # 安装 ```bash npm install @better-scroll/mouse-wheel --save // or yarn add @better-scroll/mouse-wheel ``` :::tip 目前支持鼠标滚轮有:core、slide、wheel、pullup、pulldown ::: ## 基础使用 为了开启鼠标滚动功能,你需要首先引入 mouseWheel 插件,通过静态方法 `BScroll.use()` 注册插件,最后传入正确的 [mouseWheel 选项对象](./mouse-wheel.html#mousewheel-选项对象) ```js import BScroll from '@better-scroll/core' import MouseWheel from '@better-scroll/mouse-wheel' BScroll.use(MouseWheel) new BScroll('.bs-wrapper', { //... mouseWheel: { speed: 20, invert: false, easeTime: 300 } }) ``` - **纵向普通滚动示例** - **横向普通滚动示例** ## 进阶使用 mouseWheel 插件还可以搭配其他的插件,为其增加鼠标滚轮的操作。 - **mouseWheel & slide** 通过鼠标滚轮操作 [slide](./slide.html)。 - **横向 slide 示例** - **纵向 slide 示例** - **mouseWheel & pullup** 通过鼠标触发上拉加载 [pullup](./pullup.html)。 - **mouseWheel & pulldown** 通过鼠标触发下拉加载 [pulldown](./pulldown.html)。 - **mouseWheel & wheel** 通过鼠标触发 [wheel](./wheel.html)。 ## mouseWheel 选项对象 ### speed - **类型**:`number` - **默认值**:`20` 鼠标滚轮滚动的速度。 ### invert - **类型**:`boolean` - **默认值**:`false` 当该值为 true 时,表示滚轮滚动和 BetterScroll 滚动的方向相反。 ### easeTime - **类型**:`number` - **默认值**: `300`(ms) 滚动动画的缓动时长。 ### discreteTime - **类型**:`number` - **默认值**: `400`(ms) 由于滚轮滚动是一种离散的运动,并没有 start、move、end 的事件类型,因此只要在 discreteTime 时间内没有探测到滚动,那么一次的滚轮动作就结束了。 ::: warning 注意 当搭配 [pulldown](./pulldown.html) 插件的时候,`easeTime` 和 `discreteTime` 会被内部修改成合理的固定值,以便触发 `pullingDown` 钩子 ::: ### throttleTime - **类型**:`number` - **默认值**: `0`(ms) 由于滚轮滚动是高频率的动作,因此可以通过 throttleTime 来限制触发频率,mouseWheel 内部会缓存滚动的距离,并且每隔 throttleTime 会计算缓存的距离并且滚动。 > 修改 throttleTime 可能会造成滚动动画不连贯,请根据实际场景进行调整。 ### dampingFactor - **类型**:`number` - **默认值**: `0.1` 阻尼因子,值的范围是[0, 1],当 BetterScroll 滚出边界的时候,需要施加阻力,防止滚动幅度过大,值越小,阻力越大。 :::tip 提示 当 mouseWheel 配置为 true 的时候,插件内部使用的是默认的插件选项对象。 ```js const bs = new BScroll('.wrapper', { mouseWheel: true }) // 相当于 const bs = new BScroll('.wrapper', { mouseWheel: { speed: 20, invert: false, easeTime: 300, discreteTime: 400, throttleTime: 0, dampingFactor: 0.1 } }) ``` ::: ## 事件 ### alterOptions - **参数**:`MouseWheelConfig` ```typescript export interface MouseWheelConfig { speed: number invert: boolean easeTime: number discreteTime: number throttleTime: number, dampingFactor: number } ``` - **触发时机**:滚轮滚动开始 允许修改 options 来控制滚动中的某些行为。 ### mousewheelStart - **参数**:无 - **触发时机**:滚轮滚动开始。 ### mousewheelMove - **参数**: `{ x, y }` - `{ number } x`:当前 BetterScroll 的横向滚动位置 - `{ number } y`:当前 BetterScroll 的纵向滚动位置 - **类型**: `{ x: number, y: number }` - **触发时机**:滚轮滚动中 ### mousewheelEnd - **参数**:`delta` - **类型**: `WheelDelta` ```typescript interface WheelDelta { x: number y: number directionX: Direction directionY: Direction } ``` - **触发时机**:discreteTime 之后如果还没有触发 mousewheel 事件,那么便结算一次滚轮滚动行为。 ::: danger 警告 由于 mousewheel 事件的特殊性,mousewheelEnd 派发并不代表滚动动画结束。 ::: ::: tip 提示 在绝大多数的场景下,如果你想要精确的知道当前 BetterScroll 的滚动位置,请监听 scroll、scrollEnd 钩子,而不是 `mouseXXX` 钩子。 ::: ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/movable.md ================================================ # movable ## 介绍 movable 插件为 BetterScroll 拓展可移动拖拽的能力。 ## 安装 ```bash npm install @better-scroll/movable --save // or yarn add @better-scroll/movable ``` ## 基本使用 首先引入 movable 插件,并通过全局方法 `BScroll.use()` 注册插件 ```js import BScroll from '@better-scroll/core' import Movable from '@better-scroll/movable' BScroll.use(Movable) ``` 上面步骤完成后,需要在 `options` 中传入正确的配置: ```js new BScroll('.bs-wrapper', { bindToTarget: true, scrollX: true, scrollY: true, freeScroll: true, bounce: true movable: true // for movable plugin }) ``` 以下是移动插件专属以及[ BetterScroll 的配置](../guide/base-scroll-options.html): - **movable<插件专属>** 开启 movable 功能,必须设置为 `true`,若没有该项,则插件不会生效。 - **bindToTarget** 必须设置为 `true`,主动将 touch 事件绑定在**待移动**的元素上,因为BetterScroll 默认将 touch 事件在绑定容器元素(wrapper)上。 - **freeScroll** 记录 x 和 y 轴的手指偏移量,设置为 `true`。同时需要设置 scrollX 和 scrollY 均为 true。 - **scrollX** 开启 x 轴方向滚动能力,设置为 `true`。 - **scrollY** 开启 y 轴方向滚动能力,设置为 `true`。 - **bounce** 指定开启边界回弹。 - **示例** ```js { bounce: true // 开启四个方向, bounce: { left: true, // 开启左边界回弹 right: true, // 开启右边界回弹 top: false, bottom: false } } ``` ## 示例 - **只有一个 content** 通常场景下,只存在一个 content。 - **多个 content** 但是在某些场景下,可能存在多个 content。 ## 进阶使用 搭配[ zoom ](./zoom.html#介绍)插件,增加缩放能力。 ```js import BScroll from '@better-scroll/core' import Movable from '@better-scroll/movable' import Zoom from '@better-scroll/zoom' new BScroll('.bs-wrapper', { bindToTarget: true, scrollX: true, scrollY: true, freeScroll: true, bounce: true movable: true // for movable plugin zoom: { // for zoom plugin start: 1, min: 1, max: 3 } }) ``` ## 示例 :::warning zoom 暂不支持在 pc 端的交互操作,下方 demo 请扫码体验。 ::: - **一个 content** - **多个 content** ## 实例方法 ### putAt(x, y, [time], [easing]) - **参数** - `{PositionX} x`: x 坐标 - `PositionX:'number | 'left' | 'right' | 'center'` - `{PositionY} y`: y 坐标 - `PositionY:'number | 'top' | 'bottom' | 'center'` - `{number} [time]<可选>`:滚动的动画时长 - `{EaseItem} [easing]<可选>`:缓动效果配置,参考 [ease.ts](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/ease.ts),默认是 `bounce` 效果 将 content 元素放置在某一个位置。x 与 y 不仅可以是数字,也可以是对应的字符串。 - **示例** ```js const bs = new BScroll('.bs-wrapper', { bindToTarget: true, scrollX: true, scrollY: true, freeScroll: true, movable: true }) bs.putAt('center', 'center', 0) // 放置在 wrapper 的正中心 bs.putAt('right', 'bottom', 1000) // 放置在 wrapper 的右下角,动画时长 1s ``` ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/nested-scroll.md ================================================ # nested-scroll ## 介绍 协调嵌套的 BetterScroll 滚动行为。 ::: warning 警告 v2.1.0 支持**多层嵌套**的 BetterScroll,并且功能更强大,性能更好。在这之前,只支持**双层嵌套**,请尽快升级至 2.1.0 版本。 ::: ::: tip 提示 **v2.1.0** 完美解决多层嵌套 BetterScroll 的 click 事件多次派发的问题。 ::: ## 安装 ```bash npm install @better-scroll/nested-scroll --save // or yarn add @better-scroll/nested-scroll ``` ## 使用 你需要首先引入 `nested-scroll` 插件,并通过全局方法 `BScroll.use()` 使用 ```js import BScroll from '@better-scroll/core' import NestedScroll from '@better-scroll/nested-scroll' BScroll.use(NestedScroll) ``` 上面步骤完成后,BScroll 的 `options` 中配置 `nestedScroll`。 ```js // < v2.1.0 // parent bs new BScroll('.outerWrapper', { nestedScroll: true }) // child bs new BScroll('.innerWrapper', { nestedScroll: true }) // v2.1.0 // parent bs new BScroll('.outerWrapper', { nestedScroll: { groupId: 'dummy-divide' // string or number } }) // child bs new BScroll('.innerWrapper', { nestedScroll: { groupId: 'dummy-divide' } }) ``` 具有相同 `groupId` 的 BetterScroll 实例(bs)**共享同一个** NestedScroll 实例(ns),ns 会协调每个 bs 滚动行为,一旦某个 bs 销毁的时候,ns 都会失去对它的掌控,例如: ```js // parent bs const bs1 = new BScroll('.outerWrapper', { nestedScroll: { groupId: 'shared' // string or number } }) // child bs const bs2 = new BScroll('.innerWrapper', { nestedScroll: { groupId: 'shared' } }) console.log(bs1.plugins.nestedScroll === bs2.plugins.nestedScroll) // true bs2.destroy() // nestedScroll 不再约束 bs2,不再协调 bs1 与 bs2 的滚动行为 ``` ## 示例 - **竖向双层嵌套 ** - **竖向三层嵌套 ** - **横向双层嵌套 ** - **横向竖向双层嵌套 ** ## 实例方法 :::tip 提示 以下方法皆已代理至 BetterScroll 实例,例如: ```js import BScroll from '@better-scroll/core' import NestedScroll from '@better-scroll/nested-scroll' BScroll.use(NestedScroll) const bs1 = new BScroll('.parent-wrapper', { nestedScroll: { groupId: 'dummy' } }) const bs2 = new BScroll('.child-wrapper', { nestedScroll: { groupId: 'dummy' } }) // 销毁 nestedScroll,bs1 与 bs2 共享同一个 nestedScroll 实例,因为他们的 groupId 相同 bs1.purgeNestedScroll() // 与 bs2.purgeNestedScroll() 的效果一样 ``` ::: ### `purgeNestedScroll()` - **介绍**:销毁管控自己的 nestedScroll ::: warning 注意 不同的 `groupId` 会生成不同的 nestedScroll,相同的 `groupId` 会共享同一份 nestedScroll,因此你应该在合适的时机(比如组件销毁的时候)调用 `purgeNestedScroll` 来清理内存。或者你也可以调用 BetterScroll 的 destroy 方法把自身从 nestedScroll 移除,例如: ```js const bs1 = new BScroll('.parent-wrapper', { nestedScroll: { groupId: 'dummy' } }) const bs2 = new BScroll('.child-wrapper', { nestedScroll: { groupId: 'dummy' } }) bs1.destroy() // nestedScroll 不再管控 bs1 bs2.destroy() // nestedScroll 不再管控 bs2 ``` ::: ## 静态方法 ### `getAllNestedScrolls()` - **介绍**:获取当前所有的 nestedScroll 实例 - **返回**:nestedScroll 实例组成的数组 ```typescript import NestedScroll from '@better-scroll/nested-scroll' const nestedScrolls: NestedScroll[] = NestedScroll.getAllNestedScrolls() ``` ### `purgeAllNestedScrolls()` - **介绍**:销毁当前所有的 nestedScroll 实例 ```typescript import NestedScroll from '@better-scroll/nested-scroll' // 不再约束任何 BetterScroll 实例 NestedScroll.purgeAllNestedScrolls() ``` ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/observe-dom.md ================================================ # observe-dom 开启对 content 以及 content 子元素 DOM 改变的探测。当插件被使用后,当这些 DOM 元素发生变化时,将会触发 scroll 的 refresh 方法。 observe-dom 插件具有以下几个特性: - 针对改变频繁的 CSS 属性,增加 debounce - 如果改变发生在 scroll 动画过程中,则不会触发 refresh ## 安装 ```bash npm install @better-scroll/observe-dom --save // or yarn add @better-scroll/observe-dom ``` ## 使用 ```js import BScroll from '@better-scroll/core' import ObserveDOM from '@better-scroll/observe-dom' BScroll.use(ObserveDOM) new BScroll('.bs-wrapper', { //... observeDOM: true // 开启 observe-dom 插件 }) ``` ## 示例 :::warning 注意 由于插件的内部实现使用的是 [MutationObserver](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver),它无法探测到 img 标签的是否加载完成,因此对于 content 内部含有不确定高度的图片,需要等图片加载完成再调用 bs.refresh() 来重新计算可滚动尺寸。如果浏览器不支持 MutationObserver,插件内部的降级方案是每秒重新计算可滚动的尺寸。 ::: :::tip 提示 v2.1.0 版本,新增 [observe-image](./observe-image) 插件来探测 img 标签的加载,因此这两者可以搭配起来,补齐**主动刷新**的能力来更新 BetterScroll 每次滚动的宽度或者高度。 ::: ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/observe-image.md ================================================ # observe-image ## 介绍 开启对 wrapper 子元素中图片元素的加载的探测。无论图片的加载成功与否,都会自动调用 BetterScroll 的 refresh 方法来重新计算可滚动的宽度或者高度,新增于 v2.1.0 版本。 :::tip 提示 对于已经用 CSS 确定图片宽高的场景,不应该使用该插件,因为每次调用 refresh 对性能会有影响。只有在**图片的宽度或者高度不确定**的情况下,你才需要它。 ::: ## 安装 ```bash npm install @better-scroll/observe-image --save // or yarn add @better-scroll/observe-image ``` ## 使用 ```js import BScroll from '@better-scroll/core' import ObserveImage from '@better-scroll/observe-image' BScroll.use(ObserveImage) new BScroll('.bs-wrapper', { //... observeImage: true // 开启 observe-image 插件 }) ``` ## 示例 ## observeImage 选项对象 :::tip 提示 当 observeImage 配置为 true 的时候,插件内部使用的是默认的插件选项对象。 ```js const bs = new BScroll('.wrapper', { observeImage: true }) // 相当于 const bs = new BScroll('.wrapper', { observeImage: { debounceTime: 100 // ms } }) ``` ::: ### debounceTime - **类型:** `number` - **默认值:** `100` 探测到图片加载成功或者失败后,过 debounceTime 毫秒后才会调用 refresh 方法,重新计算可滚动的高度或者宽度,如果在 debounceTime 毫秒内有多张图片加载成功或者失败,**只会调用一次 refresh**。 :::tip 提示 当 debounceTime 为 0 的时候,会立马调用 **refresh** 方法,而不是使用 **setTimeout**。 ::: ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/pulldown.md ================================================ # pulldown ## 介绍 pulldown 插件为 BetterScroll 扩展下拉刷新的能力。 ## 安装 ```bash npm install @better-scroll/pull-down --save // or yarn add @better-scroll/pull-down ``` ## 使用 首先引入 pulldown 插件,并通过静态方法 `BScroll.use()` 初始化插件 ```js import BScroll from '@better-scroll/core' import PullDown from '@better-scroll/pull-down' BScroll.use(PullDown) ``` 然后,实例化 BetterScroll 时需要传入[ pulldown 配置项](./pulldown.html#pulldownrefresh-选项对象)。 ```js new BScroll('.bs-wrapper', { pullDownRefresh: true }) ``` ## 示例 - **基础使用** - **仿新浪微博 ** 为了拉齐客户端的交互效果,在 v2.4.0 版本,pulldown 内部进行了功能的改造并且兼容以前的版本,在一次 pulldown 的操作过程中,内部存在三个流转的状态,并且状态是不可逆的。分别如下: 1. **default** 初始状态。 2. **moving** 移动状态,这个状态代表用户的手指正在操控 BetterScroll,手指未移开,在这种状态下,BetterScroll 会派发两个事件。 - **enterThreshold** 当 BetterScroll 滚动到 pulldown 的 threshold 阈值区域**之内**的时候派发,在这个事件内部,你可以做文案初始化的逻辑,比如提示用户“下拉刷新” - **leaveThreshold** 当 BetterScroll 滚动到 pulldown 的 threshold 阈值区域**之外**的时候派发。你可以提示用户“手指释放刷新” 3. **fetching** 手指移开的瞬间,触发 pullingDown 事件,执行获取数据的逻辑 状态的变换只可能是 `default -> moving -> fetching` 或者是 `default -> moving`,后者代表用户的手指在释放的瞬间,没有满足触发 pullingDown 事件的条件。 ## pullDownRefresh 选项对象 ### threshold - **类型:** `number` - **默认值:** `90` 配置顶部下拉的距离来决定刷新时机。 ### stop - **类型:** `number` - **默认值:** `40` 回弹悬停的距离。BetterScroll 在派发 `pullingDown` 钩子之后,会立马执行回弹悬停动画。 :::tip 提示 当 pullDownRefresh 配置为 true 的时候,插件内部使用的是默认的插件选项对象。 ```js const bs = new BScroll('.wrapper', { pullDownRefresh: true }) // 相当于 const bs = new BScroll('.wrapper', { pullDownRefresh: { threshold: 90, stop: 40 } }) ``` ::: ## 实例方法 :::tip 提示 以下方法皆已代理至 BetterScroll 实例,例如: ```js import BScroll from '@better-scroll/core' import PullDown from '@better-scroll/pull-down' BScroll.use(PullDown) const bs = new BScroll('.bs-wrapper', { pullDownRefresh: true }) bs.finishPullDown() bs.openPullDown({}) bs.autoPullDownRefresh() ``` ::: ### `finishPullDown()` - **介绍**:结束下拉刷新行为。 ::: warning 注意 每次触发 `pullingDown` 钩子后,你应该**主动调用** `finishPullDown()` 告诉 BetterScroll 准备好下一次的 pullingDown 钩子。 ::: ### `openPullDown(config: PullDownRefreshOptions = {})` - **介绍**:动态开启下拉刷新功能。 - **参数**: - `{ PullDownRefreshOptions } config`:修改 pulldown 插件的选项对象 - `PullDownRefreshOptions`:类型如下 ```typescript export type PullDownRefreshOptions = Partial | true export interface PullDownRefreshConfig { threshold: number stop: number } ``` - **返回值**:无 ::: warning 注意 openPullDown 方法应该配合 closePullDown 一起使用,因为在 pulldown 插件的生成过程当中,已经**自动监测了下拉刷新的动作**。 ::: ### `closePullDown()` - **介绍**:动态关闭下拉刷新功能。 ### `autoPullDownRefresh()` - **介绍**:自动执行下拉刷新。 ## 事件 ### `pullingDown` - **参数**:无 - **触发时机**:当顶部下拉的距离大于 `threshold` 值时,触发一次 `pullingDown` 钩子。 ::: danger 危险 监测到下拉刷新的动作之后,`pullingDown` 钩子的消费机会只有一次,因此你需要调用 `finishPullDown()` 来告诉 BetterScroll 来提供下一次 `pullingDown` 钩子的消费机会。 ::: ### `enterThreshold` - **参数**:无 - **触发时机**:当 pulldown 正处于 moving 状态,并且**进入** threshold 区域的瞬间。 ### `leaveThreshold` - **参数**:无 - **触发时机**:当 pulldown 正处于 moving 状态,并且**离开** threshold 区域的瞬间。 ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/pullup.md ================================================ # pullup ## 介绍 pullup 插件为 BetterScroll 扩展上拉加载的能力。 ## 安装 ```bash npm install @better-scroll/pull-up --save // or yarn add @better-scroll/pull-up ``` ## 使用 通过静态方法 `BScroll.use()` 注册插件 ```js import BScroll from '@better-scroll/core' import Pullup from '@better-scroll/pull-up' BScroll.use(Pullup) ``` 然后,实例化 BetterScroll 时需要传入[ pullup 配置项](./pullup.html#pullupload-选项对象)。 ```js new BScroll('.bs-wrapper', { pullUpLoad: true }) ``` ## 示例 ## pullUpLoad 选项对象 ### threshold - **类型:** `number` - **默认值:** `0` 触发上拉事件的阈值。 :::tip 提示 当 pullUpLoad 配置为 true 的时候,插件内部使用的是默认的插件选项对象。 ```js const bs = new BScroll('.wrapper', { pullUpLoad: true }) // 相当于 const bs = new BScroll('.wrapper', { pullUpLoad: { threshold: 0 } }) ``` ::: ## 实例方法 :::tip 提示 以下方法皆已代理至 BetterScroll 实例,例如: ```js import BScroll from '@better-scroll/core' import PullUp from '@better-scroll/pull-up' BScroll.use(PullUp) const bs = new BScroll('.bs-wrapper', { pullUpLoad: true }) bs.finishPullUp() bs.openPullUp({}) bs.closePullUp() ``` ::: ### `finishPullUp()` - **介绍**:结束上拉加载行为。 ::: warning 注意 每次触发 `pullingUp` 钩子后,你应该**主动调用** `finishPullUp()` 告诉 BetterScroll 准备好下一次的 pullingUp 钩子。 ::: ### `openPullUp(config: PullUpLoadOptions = {})` - **介绍**:动态开启上拉功能。 - **参数**: - `{ PullUpLoadOptions } config`:修改 pullup 插件的选项对象 - `PullUpLoadOptions`:类型如下 ```typescript export type PullUpLoadOptions = Partial | true export interface PullUpLoadConfig { threshold: number } ``` ::: warning 注意 openPullUp 方法应该配合 closePullUp 一起使用,因为在 pullup 插件的生成过程当中,已经**自动监测了上拉加载的动作**。 ::: ### `closePullUp()` - **介绍**:关闭上拉加载功能。 ### `autoPullUpLoad()` - **介绍**:自动执行上拉加载。 ## 事件 ### `pullingUp` - **参数**:无 - **触发时机**:当距离滚动到底部小于 `threshold` 值时,触发一次 `pullingUp` 事件。 > 当 threshold 为正数,代表距离滚动边界 threshold 像素的时候触发 `pullingUp`,反之,代表越过滚动边界才会触发事件 ::: danger 警告 监测到上拉刷新的动作之后,`pullingUp` 事件的消费机会只有一次,因此你需要调用 `finishPullUp()` 来告诉 BetterScroll 来提供下一次 `pullingUp` 事件的消费机会。 ::: ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/scroll-bar.md ================================================ # scrollbar ## 介绍 scrollbar 插件为 BetterScroll 提供了样式美观的滚动条。 :::tip 提示 从 v2.2.0 开始,用户可以提供自定义的滚动条。 ::: ## 安装 ```bash npm install @better-scroll/scroll-bar --save // or yarn add @better-scroll/scroll-bar ``` ## 使用 首先引入 scrollbar 插件,并通过静态方法 `BScroll.use()` 注册插件 ```js import BScroll from '@better-scroll/core' import ScrollBar from '@better-scroll/scroll-bar' BScroll.use(ScrollBar) ``` 接着在 `options` 传入正确的配置。 ```js new BScroll('.bs-wrapper', { scrollY: true, scrollbar: true }) ``` ## 示例 - **竖向滚动条** - **横向滚动条** - **用户定制化滚动条** - **搭配鼠标滚轮** ## scrollbar 选项对象 ### fade - **类型**:`boolean` - **默认值**:`true` 当滚动停止的时候,滚动条渐隐。 ### interactive - **类型**:`boolean` - **默认值**:`false` 滚动条是否可以交互。 ### customElements - **类型**:`HTMLElement[]` - **默认值**:`[]` 用户提供自定义的滚动条。 ```js // 横向滚动条 const horizontalEl = document.getElementById('用户自定义的滚动条') new BScroll('.bs-wrapper', { scrollY: true, scrollbar: { customElements: [horizontalEl] } }) // 竖向滚动条 const verticalEl = document.getElementById('用户自定义的滚动条') new BScroll('.bs-wrapper', { scrollY: false, scrollX: true, scrollbar: { customElements: [verticalEl] } }) // 双向滚动条 const horizontalEl = document.getElementById('用户自定义的滚动条') const verticalEl = document.getElementById('用户自定义的滚动条') new BScroll('.bs-wrapper', { freeScroll: true, scrollbar: { // 当滚动条是 2 个的时候,数组第一个元素是横向滚动条 customElements: [horizontalEl, verticalEl] } }) ``` ### minSize - **类型**:`number` - **默认值**:`8` 滚动条的最小尺寸,当用户提供了自定义的滚动条,该配置无效。 ### scrollbarTrackClickable - **类型**:`boolean` - **默认值**:`false` 滚动条轨道是否允许点击。 **注意**:当开启该配置的时候,请保证 BetterScroll Options 的 `click` 为 true,否则无法触发点击事件。[详细原因在这](../FAQ/diagnosis.html#【问题四】为什么-betterscroll-content-内部的所有的-click-事件的侦听器都不触发?) ```js new BScroll('.bs-wrapper', { scrollY: true, click: true // 必不可少 scrollbar: { scrollbarTrackClickable: true } }) ``` ### scrollbarTrackOffsetType - **类型**:`string` - **默认值**:`'step'` 滚动条轨道被点击之后,滚动距离的计算方式,默认与浏览器的表现形式一样,可以配置为 `'clickedPoint'`,代表滚动条滚动至点击的位置。 ### fadeInTime - **类型**:`number` - **默认值**:`250` 滚动条渐显的动画时长。 ### fadeOutTime - **类型**:`number` - **默认值**:`500` 滚动条渐隐的动画时长。 :::tip 提示 当 scrollbar 配置为 true 的时候,插件内部使用的是默认的插件选项对象。 ```js const bs = new BScroll('.wrapper', { scrollbar: true }) // 相当于 const bs = new BScroll('.wrapper', { scrollbar: { fade: true, interactive: false, // 以下配置项 v2.2.0 才支持 customElements: [], minSize: 8, scrollbarTrackClickable: false, scrollbarTrackOffsetType: 'step', scrollbarTrackOffsetTime: 300, // 以下配置项 v2.4.0 才支持 fadeInTime: 250, fadeOutTime: 500 } }) ``` ::: ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/slide.md ================================================ # slide ## 介绍 slide 为 BetterScroll 扩展了轮播焦点图的能力。 ## 安装 ```bash npm install @better-scroll/slide --save // or yarn add @better-scroll/slide ``` ## 使用 你需要首先引入 slide 插件,并通过静态方法 `BScroll.use()` 使用 ```js import BScroll from '@better-scroll/core' import Slide from '@better-scroll/slide' BScroll.use(Slide) ``` 上面步骤完成后,BScroll 的 `options` 中传入 slide 相关的配置。 ```js new BScroll('.bs-wrapper', { scrollX: true, scrollY: false, slide: { threshold: 100 }, momentum: false, bounce: false, stopPropagation: true }) ``` 以下是 slide 插件专属以及[ BetterScroll 的配置](../guide/base-scroll-options.html): - **slide<插件专属>** 开启 slide 功能。若没有该项,则插件不会生效。该配置同时也是用来设置 slide 特性的相关配置,具体请参考[ slide 选项对象](./slide.html#slide-选项对象)。 - **scrollX** 当值为 true 时,设置 slide 的方向为**横向**。 - **scrollY** 当值为 true 时,设置 slide 的方向为**纵向**。 **注意: scrollX 和 scrollY 不能同时设置为 true** - **momentum** 当使用 slide 时,这个值需要设置为 false,用来避免惯性动画带来的快速滚动时的闪烁的问题和快速滑动时一次滚动多页的问题。 - **bounce** bounce 值需要设置为 false,否则会在循环衔接的时候出现闪烁。 - **probeType** 如果你想通过监听 `slideWillChange` 事件,在用户拖动 slide 时,实时获取到 slide 的 PageIndex 的改变,需要设置 probeType 值为 2 或者 3。 ## 关于 slide 的术语 一般情况下,BetterScroll 的 slide 的布局如下: ```html
      ``` - **slide-wrapper** slide 容器。 - **slide-content** slide 滚动元素。 - **slide-page** slide 由多个 Page 组成。 ::: tip 在 loop 的场景下,slide-content 前后会多插入两个 Page,以便实现无缝衔接滚动的视觉效果。 ::: :::danger 危险 slide-content 必须至少有一个 slide-page,如果只有一个 page,loop 的配置无效。 ::: ## 示例 - **横向轮播** - **全屏轮播** - **纵向轮播** - **动态卡片轮播 ** - **初始化索引轮播 ** ::: tip 注意:当设置 `useTransition = true`时,可能在 iphone 某些系统上出现闪烁。你需要像上面 demo 中的代码一样,每个 `slide-page` 额外增加下面两个样式: ```css transform: translate3d(0,0,0) backface-visibility: hidden ``` ::: ## slide 选项对象 :::tip 提示 当 slide 配置为 true 的时候,插件内部使用的是默认的插件选项对象。 ```js const bs = new BScroll('.wrapper', { slide: true }) // 相当于 const bs = new BScroll('.wrapper', { slide: { loop: true, threshold: 0.1, speed: 400, easing: ease.bounce, listenFlick: true, autoplay: true, interval: 3000 } }) ``` ::: ### loop - **类型**:`boolean` - **默认值**:`true` 是否可以循环。但是当只有一个元素的时候,该设置不生效。 ### autoplay - **类型**:`boolean` - **默认值**:`true` 是否开启自动播放。 ### interval - **类型**:`number` - **默认值**:`3000` 距离下一次播放的间隔。 ### speed - **类型**:`number` - **默认值**:`400` 切换 Page 动画的默认时长。 ### easing - **类型**:`EaseItem` - `{ string } style`:用来设置过度动画的 `transition-timing-function` 值。 - `{ Function } fn`:当设置 `useTransition:false` 时,由 `easing.fn` 来确定动画曲线。 - **默认值**: ```js { style: 'cubic-bezier(0.165, 0.84, 0.44, 1)', fn: function(t: number) { return 1 - --t * t * t * t } } ``` 滚动的缓动效果配置。 ### listenFlick - **类型**:`boolean` - **默认值**:`true` 当快速轻抚过 slide 区域时,会触发切换上一页/下一页。设置 listenFlick 为 false,可关闭该效果。 ### threshold - **类型**:`number` - **默认值**:`0.1` 切换下一个或上一个 Page 的阈值。 :::tip 当滚动距离小于该阈值时,不会触发切换到下一个或上一个。 可以设置为小数,如 0.1,或者整数,如 100。当该值为小数时,threshold 被当成一个百分比,最终的阈值为 `slideWrapperWidth * threshold` 或者 `slideWrapperHeight * threshold`。当该值为整数时,则阈值就是 threshold。 ::: ### startPageXIndex - **类型**:`number` - **默认值**:`0` 实例化 slide 的时候,滚动到横向对应索引的 page。 ### startPageYIndex - **类型**:`number` - **默认值**:`0` 实例化 slide 的时候,滚动到竖向对应索引的 page。 ## 实例方法 :::tip 提示 以下方法皆已代理至 BetterScroll 实例,例如: ```js import BScroll from '@better-scroll/core' import Slide from '@better-scroll/slide' BScroll.use(Slide) const bs = new BScroll('.bs-wrapper', { slide: true }) bs.next() bs.prev() bs.getCurrentPage() ``` ::: ### next([time], [easing]) - **参数**: - `{ number } time<可选>`:动画时长,默认是 `options.speed` - `{ EaseItem } easing<可选>`:缓动效果配置,参考 [ease.ts](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/ease.ts),默认是 `bounce` 效果 ```typescript interface EaseItem { style: string fn(t: number): number } ``` 滚动到下一张。 ### prev([time], [easing]) - **参数**: - `{ number } time<可选>`:动画时长,默认是 `options.speed` - `{ EaseItem } easing<可选>`:缓动效果配置,参考 [ease.ts](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/ease.ts),默认是 `bounce` 效果 滚动到上一张。 ### goToPage(pageX, pageY, [time], [easing]) - **参数**: - `{ number } pageX`:横向滚动到对应索引的 Page,下标从 0 开始 - `{ number } pageY`:纵向滚动到对应索引的 Page,下标从 0 开始 - `{ number } time<可选>`:动画时长,默认是 `options.speed` - `{ EaseItem } easing<可选>`:缓动效果配置,参考 [ease.ts](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/ease.ts),默认是 `bounce` 效果 滚动到指定的 Page 位置。 ### getCurrentPage() - **返回值**: `page` ```typescript type Page = { x: number, y: number, pageX: number, // 横向对应 Page 的索引,下标从 0 开始 pageY: number // 纵向对应 Page 的索引,下标从 0 开始 } const page:Page = BScroll.getCurrentPage() ``` 获取当前页面的信息。 ### startPlay() 如果开启了 loop 的配置,手动开启循环播放。 ### pausePlay() 如果开启了 loop 的配置,手动关闭循环播放。 ## 事件 ### slideWillChange - **参数**:page 对象 - `{ number } x`:即将展示页面的 x 坐标值 - `{ number } y`:即将展示页面的 y 坐标值 - `{ number } pageX`:即将展示的横向页面的索引值,下标从 0 开始 - `{ number } pageY`:即将展示的纵向页面的索引值,下标从 0 开始 - **触发时机**:slide 的 currentPage 值将要改变时 - **用法**: 在 banner 展示中,常常伴随着一个 dot 图例,来指示当前 banner 是第几页,例如前面“横向轮播图”的示例。当用户拖动 banner 出现下一张时,我们希望下面的 dot 图例会同步变换。如下图 banner示例图 通过监听 `slideWillChange` 事件,可以实现该效果。代码如下: ```js let currentPageIndex // 控制当前页面 const slide = new BScroll(this.$refs.slide, { scrollX: true, scrollY: false, slide: { threshold: 100 }, useTransition: true, momentum: false, bounce: false, stopPropagation: true, probeType: 2 }) slide.on('slideWillChange', (page) => { currentPageIndex = page.pageX }) ``` ### slidePageChanged - **参数**:page 对象 - `{ number } x`:当前页面的 x 坐标值 - `{ number } y`:当前页面的 y 坐标值 - `{ number } pageX`:当前横向页面的索引值,下标从 0 开始 - `{ number } pageY`:当前纵向页面的索引值,下标从 0 开始 - **触发时机**:当 slide 切换 page 之后触发 ```js const slide = new BScroll(this.$refs.slide, { scrollX: true, scrollY: false, slide: true, momentum: false, bounce: false }) slide.on('slidePageChanged', (page) => { currentPageIndex = page.pageX }) ``` ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/wheel.md ================================================ # wheel ## 介绍 wheel 插件,是实现类似 IOS Picker 组件的基石。 ## 安装 ```bash npm install @better-scroll/wheel --save // or yarn add @better-scroll/wheel ``` ## 使用 首先引入 wheel 插件,并通过静态方法 `BScroll.use()` 注册插件。 ```js import BScroll from '@better-scroll/core' import Wheel from '@better-scroll/wheel' BScroll.use(Wheel) ``` 接着在 `options` 传入正确的配置 ```js let bs = new BScroll('.bs-wrapper', { wheel: true // wheel options 为 true }) ``` :::tip wheel options 是 true 或者对象,否则插件功能失效,具体请参考[ wheel options](./wheel.html#wheel-选项对象)。 ::: ::: danger 危险 BetterScroll 结合 wheel 插件只是实现 Picker 效果的 JS 逻辑部分,还有 DOM 模版是需要用户去实现,所幸,对于大多数的 Picker 场景,我们给出了相对应的示例。 ::: - **基本使用** 单列 Picker 是一个比较常见的效果。你可以通过 `selectedIndex` 来配置初始化时选中对应索引的 item,`wheelDisabledItemClass` 配置想要禁用的 item 项来模拟 Web Select 标签 disable 的效果。 - **多项选择器** 示例是一个两列的选择器,JS 逻辑部分与单列选择器没有多大的区别,你会发现这个两列选择器之间是没有任何关联,因为它们是两个不同的 BetterScroll 实例。如果你想要实现省市联动的效果,那么得加上一部分代码,让这两个 BetterScroll 实例能够关联起来。请看下一个例子: - **城市联动选择器** 城市联动 Picker 的效果,必须通过 JS 部分逻辑将不同 BetterScroll 的实例联系起来,不管是省市,还是省市区的联动,亦是如此。 ## wheel 选项对象 :::tip 提示 当 wheel 配置为 true 的时候,插件内部使用的是默认的插件选项对象。 ```js const bs = new BScroll('.wrapper', { wheel: true }) // 相当于 const bs = new BScroll('.wrapper', { wheel: { wheelWrapperClass: 'wheel-scroll', wheelItemClass: 'wheel-item', rotate: 25, adjustTime: 400, selectedIndex: 0, wheelDisabledItemClass: 'wheel-disabled-item' } }) ``` ::: ### selectedIndex - **类型**:`number` - **默认值**:`0` 实例化 Wheel,默认选中第 selectedIndex 项,索引从 0 开始。 ### rotate - **类型**:`number` - **默认值**:`25` 当滚动 wheel 时,wheel item 的弯曲程度。 ### adjustTime - **类型**:`number` - **默认值**:`400`(ms) 当点击某一项的时候,滚动过去的动画时长。 ### wheelWrapperClass - **类型**:`string` - **默认值**:`wheel-scroll` 滚动元素的 className,这里的「滚动元素」 指的就是 BetterScroll 的 content 元素。 ### wheelItemClass - **类型**:`string` - **默认值**:`wheel-item` 滚动元素的子元素的样式。 ### wheelDisabledItemClass - **类型**:`string` - **默认值**:`wheel-disabled-item` 滚动元素中想要禁用的子元素,类似于 `select` 元素中禁用的 `option` 效果。wheel 插件的内部根据 `wheelDisabledItemClass` 配置来判断是否将该项指定为 disabled 状态。 ## 实例方法 :::tip 提示 以下方法皆已代理至 BetterScroll 实例,例如: ```js import BScroll from '@better-scroll/core' import Wheel from '@better-scroll/wheel' BScroll.use(Wheel) const bs = new BScroll('.bs-wrapper', { wheel: true }) bs.getSelectedIndex() bs.wheelTo(1, 300) ``` ::: ### getSelectedIndex() - **返回值**:当前选中项的 index,下标从 0 开始 获取当前选中项的索引。 ### wheelTo(index = 0, time = 0, [ease]) - **参数**: - `{ number } index`:选项索引 - `{ number } time`:动画时长 - `{ number } ease<可选>`:动画时长。缓动效果配置,参考 [ease.ts](https://github.com/ustbhuangyi/better-scroll/blob/dev/packages/shared-utils/src/ease.ts),默认是 `bounce` 效果 滚动至对应索引的列表项。 ### stop() 强制让滚动的 BetterScroll 停止下来,并且吸附至当前距离最近的 wheel-item 的位置。 ### restorePosition() 强制让滚动的 BetterScroll 停止下来,并且恢复至滚动开始前的位置。 ::: tip 提示 以上两个方法只对处于**滚动中的 BetterScroll** 有效,并且 `restorePosition` 是与原生的 iOS picker 组件的效果一模一样,用户可以根据自己的需求选择对应的方法。 ::: ## 事件 ### wheelIndexChanged - **参数**:当前选中的 wheel-item 的索引。 - **触发时机**:当列表项发生改变的时候。 ```js import BScroll from '@better-scroll/core' import Wheel from '@better-scroll/wheel' BScroll.use(Wheel) const bs = new BScroll('.bs-wrapper', { wheel: true }) bs.on('wheelIndexChanged', (index) => { console.log(index) }) ``` ================================================ FILE: packages/vuepress-docs/docs/zh-CN/plugins/zoom.md ================================================ # zoom ## 介绍 zoom 插件为 BetterScroll 提供缩放功能。 ## 安装 ```bash npm install @better-scroll/zoom --save // or yarn add @better-scroll/zoom ``` ## 使用 为了开启缩放功能,你需要首先引入 zoom 插件,并通过静态方法 `BScroll.use()` 注册插件 ```js import BScroll from '@better-scroll/core' import Zoom from '@better-scroll/zoom' BScroll.use(Zoom) ``` 上面步骤完成后,在 BetterScroll 的基础能力上扩展了缩放的功能,但是想要缩放真正生效,还需要在 `options` 中传入正确的配置: ```js new BScroll('.bs-wrapper', { freeScroll: true, scrollX: true, scrollY: true, zoom: { start: 1, min: 0.5, max: 2 } }) ``` 以下是 zoom 插件专属以及[ BetterScroll 的配置](../guide/base-scroll-options.html): - **zoom<插件专属>** 开启 zoom 功能。若没有该项,则插件不会生效。该配置同时也是用来设置 zoom 特性的相关配置,具体请参考[ zoom 选项对象](./zoom.html#zoom-选项对象)。 - **freeScroll** 如果希望当放大之后,当前区域在 x 和 y 轴方向都可以滚动时,必须设置为 `true`。同时需要设置 scrollX 和 scrollY 均为 true。 - **scrollX** 如果希望当放大之后,当前区域在 x 轴方向可以滚动时,必须设置为 `true`。 - **scrollY** 如果希望当放大之后,当前区域在 y 轴方向可以滚动时,必须设置为 `true`。 ## 示例 :::warning zoom 暂不支持在 pc 端的交互操作,下方 demo 请扫码体验。 ::: ## zoom 选项对象 ### start - **类型**:`number` - **默认值**:`1` 初始缩放比例。 ### min - **类型**:`number` - **默认值**:`1` 最小缩放比例。 ### max - **类型**:`number` - **默认值**:`4` 最大缩放比例。 ### initialOrigin - **类型**:`[OriginX, OriginY]` - **OriginX**:`number | 'left' | 'right' | 'center'` - **OriginY**:`number | 'top' | 'bottom' | 'center'` - **默认值**:`[0, 0]` 初始化 zoom 插件的缩放原点,当 `start` 不为 `1` 的时候有效,缩放原点都是以`缩放元素`为坐标系。 - **示例** ```js new BScroll('.bs-wrapper', { // ... 其他配置项 zoom: { initialOrigin: [50, 50], // 基于缩放元素的左顶点上下偏移量都是 50 px initialOrigin: [0, 0], // 基于缩放元素的左顶点 initialOrigin: ['left', 'top'], // 与上面效果相同 initialOrigin: ['center', 'center'], // 基于缩放元素的中心 initialOrigin: ['right', 'top'], // 基于缩放元素的右顶点 } }) ``` 往往你初始化 zoom 的时候只专注于以端点或者中心进行缩放,可以参考以上示例。 ### minimalZoomDistance - **类型**:`number` - **默认值**:`5` 当你双指进行缩放操作的时候,只有当缩放的距离超过 `minimalZoomDistance`,zoom 才生效。 ### bounceTime - **类型**:`number` - **默认值**:`800`(毫秒) 双指不断进行缩放操作并且 scale 超过 `max` 阈值的时候,当双指离开的时候,内部会「回弹」至 `max` 的形态,而 `bounceTime` 就是这次「回弹」行为的动画时长。 :::tip 提示 当 zoom 配置为 true 的时候,插件内部使用的是默认的插件选项对象。 ```js const bs = new BScroll('.wrapper', { zoom: true }) // 相当于 const bs = new BScroll('.wrapper', { zoom: { start: 1, min: 1, max: 4, initialOrigin: [0, 0], minimalZoomDistance: 5, bounceTime: 800, // ms } }) ``` ::: ## 实例方法 ### zoomTo(scale, x, y, [bounceTime]) - **参数** - `{number} scale`: 缩放比例 - `{OriginX} x`: 缩放原点的 x 坐标,相当于**缩放元素**的左顶点 - `OriginX:'number | 'left' | 'right' | 'center'` - `{OriginY} y`: 缩放原点的 y 坐标,相当于**缩放元素**的左顶点 - `OriginY:'number | 'top' | 'bottom' | 'center'` - `{number} [bounceTime]<可选>:一次缩放行为的动画时长` 以 `[x, y]` 坐标作为原点对元素进行缩放。x 与 y 不仅可以是数字,也可以是对应的字符串,因为一般的场景都是基于端点或者中心进行缩放。 - **示例** ```js const bs = new BScroll('.bs-wrapper', { freeScroll: true, scrollX: true, scrollY: true, zoom: { start: 1, min: 0.5, max: 2 } }) bs.zoomTo(1.8, 'left', 'bottom') // 基于缩放元素的左底点缩放至 1.8 倍 bs.zoomTo(1.8, 'left', 'bottom', 1000) // 基于缩放元素的左底点缩放,动画时长为 1s bs.zoomTo(1.8, 100, 100) // 基于缩放元素左顶点的上下偏移量 100 为原点进行缩放 bs.zoomTo(2, 'center', 'center') // 基于缩放元素的中心进行缩放 ``` ## 事件 ### beforeZoomStart - **参数**:无 - **触发时机**:双指接触缩放元素时,不包括直接调用 zoomTo 方法 ### zoomStart - **参数**:无 - **触发时机**:双指缩放距离超过最小阈值 `minimalZoomDistance`,缩放即将开始。不包括直接调用 zoomTo 方法 ### zooming - **参数**:`{ scale }` - **类型**:`{ scale: number }` - **触发时机**:双指缩放行为正在进行时或者直接调用 zoomTo 进行缩放的过程 - **示例**: ```js const bs = new BScroll('.bs-wrapper', { freeScroll: true, scrollX: true, scrollY: true, zoom: { start: 1, min: 0.5, max: 2 } }) bs.on('zooming', ({ scale }) => { // use scale console.log(scale) // 当前 scale 的值 }) ``` ### zoomEnd - **参数**:`{ scale }` - **类型**:`{ scale: number }` - **触发时机**:双指缩放行为结束后(如果有回弹,触发时机在回弹动画结束之后)或者调用 zoomTo 完成缩放之后 :::warning 在 zoom 的场景下,你应该监听 zoomStart、zooming、zoomEnd 等等事件,而不是更底层的 scroll、scrollEnd 事件,要不然可能与你的预期不符。 ::: ================================================ FILE: packages/vuepress-docs/docs-release.sh ================================================ #!/usr/bin/env sh set -e yarn run docs:build cd docs/.vuepress/dist git init git add -A git commit -m 'update docs' git push -f git@github.com:better-scroll/docs.git master:gh-pages cd - ================================================ FILE: packages/vuepress-docs/package.json ================================================ { "name": "vuepress-docs", "version": "2.5.1", "description": "Docs of BetterScroll", "author": { "name": "huangyi", "email": "ustbhuangyi@gmail.com" }, "private": true, "scripts": { "docs:dev": "vuepress dev docs --open", "docs:build": "vuepress build docs", "docs:release": "sh docs-release.sh" }, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios", "vuepress-docs" ], "license": "MIT", "repository": { "type": "git", "url": "git@github.com:ustbhuangyi/better-scroll.git", "directory": "packages/vuepress-docs" }, "devDependencies": { "ts-loader": "^5.4.5", "vuepress": "^1.8.0" }, "dependencies": { "qrcode-js-package": "^1.0.4", "v-tooltip": "^2.0.2" } } ================================================ FILE: packages/wheel/README.md ================================================ # @better-scroll/wheel [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/wheel/README_zh-CN.md) Implement a plugin similar to the effects of the IOS Picker component. ## Usage ```js import BScroll from '@better-scroll/core' import Wheel from '@better-scroll/wheel' BScroll.use(Wheel) const bs = new BScroll('.wheel-wrapper', { wheel: { selectedIndex: 0, wheelWrapperClass: 'wheel-scroll', wheelItemClass: 'wheel-item', wheelDisabledItemClass: 'wheel-disabled-item' }, probeType: 3 }) ``` ================================================ FILE: packages/wheel/README_zh-CN.md ================================================ # @better-scroll/wheel 实现类似于 IOS Picker 组件效果的插件。 ## 使用 ```js import BScroll from '@better-scroll/core' import Wheel from '@better-scroll/wheel' BScroll.use(Wheel) const bs = new BScroll('.wheel-wrapper', { wheel: { selectedIndex: 0, wheelWrapperClass: 'wheel-scroll', wheelItemClass: 'wheel-item', wheelDisabledItemClass: 'wheel-disabled-item' }, probeType: 3 }) ``` ================================================ FILE: packages/wheel/package.json ================================================ { "name": "@better-scroll/wheel", "version": "2.5.1", "description": "a BetterScroll plugin to imitate IOS Picker", "author": { "name": "jizhi", "email": "theniceangel@163.com" }, "main": "dist/wheel.min.js", "module": "dist/wheel.esm.js", "typings": "dist/types/index.d.ts", "publishConfig": { "access": "public" }, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios" ], "license": "MIT", "repository": { "type": "git", "url": "git@github.com:ustbhuangyi/better-scroll.git" }, "dependencies": { "@better-scroll/core": "^2.5.1" }, "gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130" } ================================================ FILE: packages/wheel/src/__tests__/index.spec.ts ================================================ import BScroll, { Boundary } from '@better-scroll/core' import Wheel from '../index' jest.mock('@better-scroll/core') const createWheel = (wheelOptions: Object) => { const wrapper = document.createElement('div') const content = document.createElement('div') wrapper.appendChild(content) const scroll = new BScroll(wrapper, { wheel: wheelOptions }) const wheel = new Wheel(scroll) return { scroll, wheel } } const addPropertiesToWheel = (wheel: Wheel, obj: T) => { for (const key in obj) { ;(wheel as any)[key] = obj[key] } return wheel } describe('wheel plugin tests', () => { let scroll: BScroll let wheel: Wheel beforeEach(() => { const created = createWheel({}) // create DOM wheel = created.wheel scroll = created.scroll }) afterEach(() => { jest.clearAllMocks() }) it('should proxy properties to BScroll instance', () => { expect(scroll.proxy).toBeCalled() expect(scroll.proxy).toHaveBeenLastCalledWith([ { key: 'wheelTo', sourceKey: 'plugins.wheel.wheelTo' }, { key: 'getSelectedIndex', sourceKey: 'plugins.wheel.getSelectedIndex' }, { key: 'restorePosition', sourceKey: 'plugins.wheel.restorePosition' } ]) }) it('should handle options', () => { expect(wheel.options.rotate).toBe(25) expect(wheel.options.adjustTime).toBe(400) expect(wheel.options.selectedIndex).toBe(0) expect(wheel.options.wheelWrapperClass).toBe('wheel-scroll') expect(wheel.options.wheelItemClass).toBe('wheel-item') expect(wheel.options.wheelDisabledItemClass).toBe('wheel-disabled-item') }) it('should refresh BehaviorX and BehaviorY boundary', () => { const { scrollBehaviorX, scrollBehaviorY } = scroll.scroller expect(scrollBehaviorX.refresh).toBeCalled() expect(scrollBehaviorY.refresh).toBeCalled() }) it('should handle selectedIndex', () => { // default expect(wheel.selectedIndex).toBe(0) // specified const { wheel: wheel2 } = createWheel({ selectedIndex: 2 }) expect(wheel2.selectedIndex).toBe(2) }) it('should trigger scroll.scrollTo when invoking wheelTo method', () => { addPropertiesToWheel(wheel, { itemHeight: 40 }) wheel.wheelTo(0) expect(scroll.scrollTo).toBeCalled() expect(scroll.scrollTo).toHaveBeenLastCalledWith(0, -0, 0, undefined) }) it('should return seletedIndex when invoking getSelectedIndex', () => { const { wheel: wheel2 } = createWheel({ selectedIndex: 2 }) expect(wheel2.getSelectedIndex()).toBe(2) }) it('should support scrollTo somewhere by selectedIndex when initialized', () => { addPropertiesToWheel(wheel, { selectedIndex: 1, itemHeight: 50 }) const postion = { x: 100, y: 100 } // manually trigger scroll.hooks.trigger(scroll.hooks.eventTypes.beforeInitialScrollTo, postion) expect(postion).toMatchObject({ x: 0, y: -50 }) }) it('should invoke wheelTo when scroll.scroller trigger checkClick hook', () => { let div = document.createElement('div') addPropertiesToWheel(wheel, { items: [div], target: div, wheelTo: jest.fn() }) scroll.scroller.hooks.trigger('checkClick') expect(wheel.wheelTo).toBeCalled() expect(wheel.wheelTo).toHaveBeenCalledWith(0, 400, expect.anything()) // if target element is not found addPropertiesToWheel(wheel, { items: [div], target: null, wheelTo: jest.fn() }) let ret = scroll.scroller.hooks.trigger('checkClick') expect(ret).toBe(true) }) it('should invoke findNearestValidWheel when scroll.scroller trigger scrollTo hook', () => { let endPoint = { x: 0, y: -20 } let div = document.createElement('div') addPropertiesToWheel(wheel, { items: [div], target: div, itemHeight: 40, wheelTo: jest.fn() }) scroll.scroller.hooks.trigger('scrollTo', endPoint) expect(endPoint.y).toBe(-0) }) it('should change position when scroll.scroller trigger scrollToElement hook', () => { let div = document.createElement('div') addPropertiesToWheel(wheel, { items: [div], target: div, itemHeight: 40 }) let pos = { top: -20, left: 0 } div.className = 'wheel-item' scroll.scroller.hooks.trigger('scrollToElement', div, pos) expect(pos).toEqual({ top: -0, left: 0 }) // mismatch target element let div1 = document.createElement('div') let pos1 = { top: -40, left: 0 } addPropertiesToWheel(wheel, { items: [div1], target: div1, itemHeight: 40 }) let ret = scroll.scroller.hooks.trigger('scrollToElement', div1, pos1) expect(ret).toBe(true) expect(pos1).toMatchObject({ top: -40, left: 0 }) }) it('should change target when scroll.scroller.actionsHandler trigger beforeStart hook', () => { let e = {} as any let div = document.createElement('div') e.target = div scroll.scroller.actionsHandler.hooks.trigger('beforeStart', e) expect(wheel.target).toEqual(div) }) it('should modify boundary when scrollBehaviorY or scrollBehaviorX computedBoundary', () => { let div = document.createElement('div') let cachedXBoundary = {} as Boundary let cachedYBoundary = {} as Boundary addPropertiesToWheel(wheel, { items: [div, div], itemHight: 50 }) const { scrollBehaviorX, scrollBehaviorY } = scroll.scroller // append two element scroll.scroller.content.appendChild(document.createElement('div')) scroll.scroller.content.appendChild(document.createElement('div')) scrollBehaviorY.contentSize = 100 // manually trigger scrollBehaviorX.hooks.trigger( scrollBehaviorX.hooks.eventTypes.computeBoundary, cachedXBoundary ) scrollBehaviorY.hooks.trigger( scrollBehaviorY.hooks.eventTypes.computeBoundary, cachedYBoundary ) expect(cachedXBoundary).toMatchObject({ minScrollPos: 0, maxScrollPos: 0 }) expect(cachedYBoundary).toMatchObject({ minScrollPos: 0, maxScrollPos: -50 }) }) it('should change momentumInfo when scroll.scroller.scrollBehaviorY trigger momentum or end hook', () => { let momentumInfo = { destination: 0, rate: 15 } let div = document.createElement('div') addPropertiesToWheel(wheel, { items: [div], target: div, itemHeight: 40 }) scroll.scroller.scrollBehaviorY.hooks.trigger('momentum', momentumInfo) expect(momentumInfo).toEqual({ destination: -0, rate: 4 }) scroll.scroller.scrollBehaviorY.currentPos = -20 scroll.scroller.scrollBehaviorY.hooks.trigger('end', momentumInfo) expect(momentumInfo).toEqual({ destination: -0, rate: 4, duration: 400 }) scroll.scroller.scrollBehaviorY.hooks.trigger('momentum', momentumInfo, 800) expect(momentumInfo).toEqual({ destination: -0, rate: 4, duration: 400 }) }) it('scroll.hooks.refresh ', () => { let newContent = document.createElement('p') let div = document.createElement('div') addPropertiesToWheel(wheel, { items: [div], target: div, itemHeight: 40 }) wheel.options.selectedIndex = 1 scroll.hooks.trigger(scroll.hooks.eventTypes.refresh, newContent) expect(scroll.scrollTo).toBeCalledWith(0, -40, 0, undefined) }) it('scroll.scroller.animater.hooks.time ', () => { let div = document.createElement('div') addPropertiesToWheel(wheel, { items: [div] }) const animater = scroll.scroller.animater animater.hooks.trigger(animater.hooks.eventTypes.time, 100) expect(div.style.transitionDuration).toBe('100ms') }) it('scroll.scroller.animater.hooks.timeFunction ', () => { let div = document.createElement('div') addPropertiesToWheel(wheel, { items: [div] }) const animater = scroll.scroller.animater animater.hooks.trigger( animater.hooks.eventTypes.timeFunction, 'cubic-bezier(0.23, 1, 0.32, 1)' ) expect(div.style.transitionTimingFunction).toBe( 'cubic-bezier(0.23, 1, 0.32, 1)' ) }) it('scroll.scroller.animater.hooks.callStop', () => { let div1 = document.createElement('div') let div2 = document.createElement('div') addPropertiesToWheel(wheel, { items: [div1, div2], itemHeight: 40, wheelItemsAllDisabled: false }) scroll.y = -41 scroll.maxScrollY = -80 scroll.scroller.animater.hooks.trigger('callStop') expect(scroll.scrollTo).toBeCalledWith(0, -40, 0, undefined) }) it('scroll.scroller.animater.translater.hooks.translate', () => { let div = document.createElement('div') addPropertiesToWheel(wheel, { items: [div], itemHeight: 40, wheelItemsAllDisabled: false }) const translater = scroll.scroller.animater.translater translater.hooks.trigger(translater.hooks.eventTypes.translate, { x: 0, y: -20 }) expect(wheel.selectedIndex).toEqual(0) }) it('scroll.scroller.hooks.minDistanceScroll ', () => { let div = document.createElement('div') addPropertiesToWheel(wheel, { items: [div] }) const scroller = scroll.scroller scroller.animater.forceStopped = true scroller.hooks.trigger(scroller.hooks.eventTypes.minDistanceScroll) expect(scroller.animater.forceStopped).toBe(false) }) it('scrollEnd event', () => { let div1 = document.createElement('div') let div2 = document.createElement('div') addPropertiesToWheel(wheel, { itemHeight: 40, items: [div1, div2] }) scroll.maxScrollY = -80 scroll.scroller.animater.forceStopped = true // stopped from an animation, // prevent user's scrollEnd callback triggered twice const ret = scroll.trigger(scroll.eventTypes.scrollEnd, { y: 0 }) expect(ret).toBe(true) wheel.isAdjustingPosition = true // update selectedIndex scroll.trigger(scroll.eventTypes.scrollEnd, { y: -41 }) expect(wheel.getSelectedIndex()).toBe(1) expect(wheel.isAdjustingPosition).toBe(false) }) it('wheel.restorePosition()', () => { addPropertiesToWheel(wheel, { itemHeight: 40 }) // simulate bs is scrolling scroll.pending = true wheel.restorePosition() expect(scroll.scroller.animater.clearTimer).toBeCalled() expect(scroll.scrollTo).toBeCalledWith(0, -0, 0, undefined) }) it('should support disable wheel items', () => { let div1 = document.createElement('div') let div2 = document.createElement('div') const scroller = scroll.scroller const position = { y: -41 } addPropertiesToWheel(wheel, { items: [div1, div2], itemHeight: 40, wheelItemsAllDisabled: false }) scroll.y = -41 scroll.maxScrollY = -80 div2.className = 'wheel-disabled-item' scroller.hooks.trigger(scroller.hooks.eventTypes.scrollTo, position) expect(position.y).toBe(-0) div1.className = 'wheel-disabled-item' wheel.wheelItemsAllDisabled = true scroller.hooks.trigger(scroller.hooks.eventTypes.scrollTo, position) expect(position.y).toBe(-0) let div3 = document.createElement('div') let position3 = { y: -39 } addPropertiesToWheel(wheel, { items: [div1, div2, div3], itemHeight: 40, wheelItemsAllDisabled: false }) scroller.hooks.trigger(scroller.hooks.eventTypes.scrollTo, position3) expect(position3.y).toBe(-80) }) }) ================================================ FILE: packages/wheel/src/index.ts ================================================ import BScroll, { Boundary } from '@better-scroll/core' import { style, hasClass, ease, EaseItem, extend, Position, HTMLCollectionToArray } from '@better-scroll/shared-utils' import propertiesConfig from './propertiesConfig' export type WheelOptions = Partial | true const WHEEL_INDEX_CHANGED_EVENT_NAME = 'wheelIndexChanged' export interface WheelConfig { selectedIndex: number rotate: number adjustTime: number wheelWrapperClass: string wheelItemClass: string wheelDisabledItemClass: string } declare module '@better-scroll/core' { interface CustomOptions { wheel?: WheelOptions } interface CustomAPI { wheel: PluginAPI } } interface PluginAPI { wheelTo(index?: number, time?: number, ease?: EaseItem): void getSelectedIndex(): number restorePosition(): void } const CONSTANTS = { rate: 4 } export default class Wheel implements PluginAPI { static pluginName = 'wheel' options: WheelConfig wheelItemsAllDisabled: boolean items: HTMLCollection itemHeight: number selectedIndex: number isAdjustingPosition: boolean target: EventTarget | null constructor(public scroll: BScroll) { this.init() } init() { this.handleBScroll() this.handleOptions() this.handleHooks() // init boundary for Wheel this.refreshBoundary() this.setSelectedIndex(this.options.selectedIndex) } private handleBScroll() { this.scroll.proxy(propertiesConfig) this.scroll.registerType([WHEEL_INDEX_CHANGED_EVENT_NAME]) } private handleOptions() { const userOptions = (this.scroll.options.wheel === true ? {} : this.scroll.options.wheel) as Partial const defaultOptions: WheelConfig = { wheelWrapperClass: 'wheel-scroll', wheelItemClass: 'wheel-item', rotate: 25, adjustTime: 400, selectedIndex: 0, wheelDisabledItemClass: 'wheel-disabled-item' } this.options = extend(defaultOptions, userOptions) } private handleHooks() { const scroll = this.scroll const scroller = this.scroll.scroller const { actionsHandler, scrollBehaviorX, scrollBehaviorY, animater } = scroller let prevContent = scroller.content // BScroll scroll.on(scroll.eventTypes.scrollEnd, (position: Position) => { const index = this.findNearestValidWheel(position.y).index if (scroller.animater.forceStopped && !this.isAdjustingPosition) { this.target = this.items[index] // since stopped from an animation. // prevent user's scrollEnd callback triggered twice return true } else { this.setSelectedIndex(index) if (this.isAdjustingPosition) { this.isAdjustingPosition = false } } }) // BScroll.hooks this.scroll.hooks.on( this.scroll.hooks.eventTypes.refresh, (content: HTMLElement) => { if (content !== prevContent) { prevContent = content this.setSelectedIndex(this.options.selectedIndex, true) } // rotate all wheel-items // because position may not change this.rotateX(this.scroll.y) // check we are stop at a disable item or not this.wheelTo(this.selectedIndex, 0) } ) this.scroll.hooks.on( this.scroll.hooks.eventTypes.beforeInitialScrollTo, (position: Position) => { // selectedIndex has higher priority than bs.options.startY position.x = 0 position.y = -(this.selectedIndex * this.itemHeight) } ) // Scroller scroller.hooks.on(scroller.hooks.eventTypes.checkClick, () => { const index = HTMLCollectionToArray(this.items).indexOf(this.target) if (index === -1) return true this.wheelTo(index, this.options.adjustTime, ease.swipe) return true }) scroller.hooks.on( scroller.hooks.eventTypes.scrollTo, (endPoint: Position) => { endPoint.y = this.findNearestValidWheel(endPoint.y).y } ) // when content is scrolling // click wheel-item DOM repeatedly and crazily will cause scrollEnd not triggered // so reset forceStopped scroller.hooks.on(scroller.hooks.eventTypes.minDistanceScroll, () => { const animater = scroller.animater if (animater.forceStopped === true) { animater.forceStopped = false } }) scroller.hooks.on( scroller.hooks.eventTypes.scrollToElement, (el: HTMLElement, pos: { top: number; left: number }) => { if (!hasClass(el, this.options.wheelItemClass)) { return true } else { pos.top = this.findNearestValidWheel(pos.top).y } } ) // ActionsHandler actionsHandler.hooks.on( actionsHandler.hooks.eventTypes.beforeStart, (e: TouchEvent) => { this.target = e.target } ) // ScrollBehaviorX // Wheel has no x direction now scrollBehaviorX.hooks.on( scrollBehaviorX.hooks.eventTypes.computeBoundary, (boundary: Boundary) => { boundary.maxScrollPos = 0 boundary.minScrollPos = 0 } ) // ScrollBehaviorY scrollBehaviorY.hooks.on( scrollBehaviorY.hooks.eventTypes.computeBoundary, (boundary: Boundary) => { this.items = this.scroll.scroller.content.children this.checkWheelAllDisabled() this.itemHeight = this.items.length > 0 ? scrollBehaviorY.contentSize / this.items.length : 0 boundary.maxScrollPos = -this.itemHeight * (this.items.length - 1) boundary.minScrollPos = 0 } ) scrollBehaviorY.hooks.on( scrollBehaviorY.hooks.eventTypes.momentum, (momentumInfo: { destination: number duration: number rate: number }) => { momentumInfo.rate = CONSTANTS.rate momentumInfo.destination = this.findNearestValidWheel( momentumInfo.destination ).y } ) scrollBehaviorY.hooks.on( scrollBehaviorY.hooks.eventTypes.end, (momentumInfo: { destination: number; duration: number }) => { let validWheel = this.findNearestValidWheel(scrollBehaviorY.currentPos) momentumInfo.destination = validWheel.y momentumInfo.duration = this.options.adjustTime } ) // Animater animater.hooks.on(animater.hooks.eventTypes.time, (time: number) => { this.transitionDuration(time) }) animater.hooks.on( animater.hooks.eventTypes.timeFunction, (easing: string) => { this.timeFunction(easing) } ) // bs.stop() to make wheel stop at a correct position when pending animater.hooks.on(animater.hooks.eventTypes.callStop, () => { const { index } = this.findNearestValidWheel(this.scroll.y) this.isAdjustingPosition = true this.wheelTo(index, 0) }) // Translater animater.translater.hooks.on( animater.translater.hooks.eventTypes.translate, (endPoint: Position) => { this.rotateX(endPoint.y) } ) } private refreshBoundary() { const { scrollBehaviorX, scrollBehaviorY, content } = this.scroll.scroller scrollBehaviorX.refresh(content) scrollBehaviorY.refresh(content) } setSelectedIndex(index: number, contentChanged: boolean = false) { const prevSelectedIndex = this.selectedIndex this.selectedIndex = index // if content DOM changed, should not trigger event if (prevSelectedIndex !== index && !contentChanged) { this.scroll.trigger(WHEEL_INDEX_CHANGED_EVENT_NAME, index) } } getSelectedIndex() { return this.selectedIndex } wheelTo(index = 0, time = 0, ease?: EaseItem) { const y = -index * this.itemHeight this.scroll.scrollTo(0, y, time, ease) } restorePosition() { // bs is scrolling const isPending = this.scroll.pending if (isPending) { const selectedIndex = this.getSelectedIndex() this.scroll.scroller.animater.clearTimer() this.wheelTo(selectedIndex, 0) } } private transitionDuration(time: number) { for (let i = 0; i < this.items.length; i++) { ;(this.items[i] as HTMLElement).style[style.transitionDuration as any] = time + 'ms' } } private timeFunction(easing: string) { for (let i = 0; i < this.items.length; i++) { ;(this.items[i] as HTMLElement).style[ style.transitionTimingFunction as any ] = easing } } private rotateX(y: number) { const { rotate = 25 } = this.options for (let i = 0; i < this.items.length; i++) { const deg = rotate * (y / this.itemHeight + i) // Too small value is invalid in some phones, issue 1026 const SafeDeg = deg.toFixed(3) ;(this.items[i] as HTMLElement).style[ style.transform as any ] = `rotateX(${SafeDeg}deg)` } } private findNearestValidWheel(y: number) { y = y > 0 ? 0 : y < this.scroll.maxScrollY ? this.scroll.maxScrollY : y let currentIndex = Math.abs(Math.round(-y / this.itemHeight)) const cacheIndex = currentIndex const items = this.items const wheelDisabledItemClassName = this.options .wheelDisabledItemClass as string // implement web native select element // first, check whether there is a enable item whose index is smaller than currentIndex // then, check whether there is a enable item whose index is bigger than currentIndex // otherwise, there are all disabled items, just keep currentIndex unchange while (currentIndex >= 0) { if ( !hasClass( items[currentIndex] as HTMLElement, wheelDisabledItemClassName ) ) { break } currentIndex-- } if (currentIndex < 0) { currentIndex = cacheIndex while (currentIndex <= items.length - 1) { if ( !hasClass( items[currentIndex] as HTMLElement, wheelDisabledItemClassName ) ) { break } currentIndex++ } } // keep it unchange when all the items are disabled if (currentIndex === items.length) { currentIndex = cacheIndex } // when all the items are disabled, selectedIndex should always be -1 return { index: this.wheelItemsAllDisabled ? -1 : currentIndex, y: -currentIndex * this.itemHeight } } private checkWheelAllDisabled() { const wheelDisabledItemClassName = this.options.wheelDisabledItemClass const items = this.items this.wheelItemsAllDisabled = true for (let i = 0; i < items.length; i++) { if (!hasClass(items[i] as HTMLElement, wheelDisabledItemClassName)) { this.wheelItemsAllDisabled = false break } } } } ================================================ FILE: packages/wheel/src/propertiesConfig.ts ================================================ const sourcePrefix = 'plugins.wheel' const propertiesMap = [ { key: 'wheelTo', name: 'wheelTo', }, { key: 'getSelectedIndex', name: 'getSelectedIndex', }, { key: 'restorePosition', name: 'restorePosition', }, ] export default propertiesMap.map((item) => { return { key: item.key, sourceKey: `${sourcePrefix}.${item.name}`, } }) ================================================ FILE: packages/zoom/README.md ================================================ # @better-scroll/pull-up [中文文档](https://github.com/ustbhuangyi/better-scroll/blob/master/packages/zoom/README_zh-CN.md) Plugin for zooming in or out. ## Usage ```js import BScroll from '@better-scroll/core' import Zoom from '@better-scroll/zoom' BScroll.use(Zoom) const bs = new BScroll('.zoom-wrapper', { freeScroll: true, scrollX: true, scrollY: true, disableMouse: true, useTransition: true, zoom: { start: 1, min: 0.5, max: 2 } }) ``` ================================================ FILE: packages/zoom/README_zh-CN.md ================================================ # @better-scroll/zoom 为 BetterScroll 提供放大或者缩小的效果的插件。 ## 使用 ```js import BScroll from '@better-scroll/core' import Zoom from '@better-scroll/zoom' BScroll.use(Zoom) const bs = new BScroll('.zoom-wrapper', { freeScroll: true, scrollX: true, scrollY: true, disableMouse: true, useTransition: true, zoom: { start: 1, min: 0.5, max: 2 } }) ``` ================================================ FILE: packages/zoom/package.json ================================================ { "name": "@better-scroll/zoom", "version": "2.5.1", "description": "a BetterScroll plugin to enlarge or narrow", "author": { "name": "jizhi", "email": "theniceangel@163.com" }, "main": "dist/zoom.min.js", "module": "dist/zoom.esm.js", "typings": "dist/types/index.d.ts", "publishConfig": { "access": "public" }, "bugs": { "url": "https://github.com/ustbhuangyi/better-scroll/issues" }, "homepage": "https://github.com/ustbhuangyi/better-scroll", "keywords": [ "scroll", "iscroll", "javascript", "typescript", "ios", "image-preview", "zoom" ], "license": "MIT", "repository": { "type": "git", "url": "git@github.com:ustbhuangyi/better-scroll.git", "directory": "packages/zoom" }, "dependencies": { "@better-scroll/core": "^2.5.1" }, "gitHead": "f441227b6137d44ba0b44b97ed4cd49de9386130" } ================================================ FILE: packages/zoom/src/__tests__/__utils__/util.ts ================================================ import { createEvent, CustomTouchEvent } from '@better-scroll/core/src/__tests__/__utils__/event' import { createDiv } from '@better-scroll/core/src/__tests__/__utils__/layout' export function createZoomElements() { const wrapper = createDiv(300, 300) const scaledElement = createDiv(300, 300, 0, 0) wrapper.appendChild(scaledElement) return { wrapper, scaledElement } } export function createTouchEvent( firstFingerPoint: { pageX: number; pageY: number }, secondFingerPoint?: { pageX: number; pageY: number } ): CustomTouchEvent { const e = createEvent('Event', 'touch') as CustomTouchEvent e.touches = [firstFingerPoint] if (secondFingerPoint) { e.touches.push(secondFingerPoint) } return e } ================================================ FILE: packages/zoom/src/__tests__/index.spec.ts ================================================ import BScroll from '@better-scroll/core' import Zoom from '../index' import { ease } from '@better-scroll/shared-utils' import { createTouchEvent, createZoomElements } from './__utils__/util' jest.mock('@better-scroll/core') jest.mock('@better-scroll/core/src/animater/index') describe('zoom plugin', () => { let scroll: BScroll beforeEach(() => { // create DOM const { wrapper } = createZoomElements() scroll = new BScroll(wrapper) }) afterEach(() => { jest.clearAllMocks() }) it('should proxy properties to BScroll instance', () => { new Zoom(scroll) expect(scroll.proxy).toBeCalled() expect(scroll.proxy).toHaveBeenLastCalledWith([ { key: 'zoomTo', sourceKey: 'plugins.zoom.zoomTo', }, ]) }) it('should register hooks to BScroll instance', () => { new Zoom(scroll) expect(scroll.registerType).toBeCalled() expect(scroll.registerType).toHaveBeenLastCalledWith([ 'beforeZoomStart', 'zoomStart', 'zooming', 'zoomEnd', ]) expect(scroll.eventTypes.beforeZoomStart).toEqual('beforeZoomStart') expect(scroll.eventTypes.zoomStart).toEqual('zoomStart') }) it('should handle default options and user options', () => { // case 1 scroll.options.zoom = true let zoom = new Zoom(scroll) expect(zoom.zoomOpt).toMatchObject({ start: 1, min: 1, max: 4, initialOrigin: [0, 0], minimalZoomDistance: 5, bounceTime: 800, }) // case 2 scroll.options.zoom = { initialOrigin: ['center', 'center'], bounceTime: 300, } zoom = new Zoom(scroll) expect(zoom.zoomOpt).toMatchObject({ start: 1, min: 1, max: 4, initialOrigin: ['center', 'center'], minimalZoomDistance: 5, bounceTime: 300, }) }) it('should try initialZoomTo when new zoom()', () => { // start is 1, no zoomTo new Zoom(scroll) expect(scroll.scroller.scrollTo).toBeCalledTimes(0) // start !== 1 scroll.options.zoom = { start: 1.5, initialOrigin: [0, 0], } new Zoom(scroll) expect(scroll.scroller.scrollTo).toBeCalledTimes(1) expect(scroll.scroller.scrollTo).toHaveBeenLastCalledWith( 0, 0, 0, ease.bounce, { start: { scale: 1, }, end: { scale: 1.5, }, } ) // start should <= max scroll.options.zoom = { start: 3.5, max: 3, initialOrigin: [0, 0], } new Zoom(scroll) expect(scroll.scroller.scrollTo).toBeCalledTimes(2) expect(scroll.scroller.scrollTo).toHaveBeenLastCalledWith( 0, 0, 0, ease.bounce, { start: { scale: 1, }, end: { scale: 3, // equals max }, } ) }) it("should set scaled element's transform origin", () => { new Zoom(scroll) expect(scroll.scroller.content.style['transform-origin' as any]).toBe('0 0') }) it('should not response with one finger', () => { const zoom = new Zoom(scroll) const hooks = scroll.scroller.actions.hooks const zoomStartSpy = jest.spyOn(zoom, 'zoomStart') const zoomSpy = jest.spyOn(zoom, 'zoom') const zoomEndSpy = jest.spyOn(zoom, 'zoomEnd') const e = createTouchEvent({ pageX: 0, pageY: 0 }) hooks.trigger(hooks.eventTypes.start, e) expect(zoomStartSpy).not.toHaveBeenCalled() hooks.trigger(hooks.eventTypes.beforeMove, e) expect(zoomSpy).not.toHaveBeenCalled() hooks.trigger(hooks.eventTypes.beforeEnd, e) expect(zoomEndSpy).not.toHaveBeenCalled() }) it('should compute boundary of Behavior when zoom ends', () => { const zoom = new Zoom(scroll) as any // simulate two fingers zoom.numberOfFingers = 2 // allow trigger beforeEnd hooks zoom.zoomed = true const e = createTouchEvent({ pageX: 0, pageY: 0 }, { pageX: 20, pageY: 20 }) const actions = scroll.scroller.actions const behaviorX = scroll.scroller.scrollBehaviorX const behaviorY = scroll.scroller.scrollBehaviorY behaviorX.checkInBoundary = jest.fn().mockImplementation(() => { return { inBoundary: true } }) behaviorY.checkInBoundary = jest.fn().mockImplementation(() => { return { inBoundary: true } }) actions.hooks.trigger(actions.hooks.eventTypes.beforeEnd, e) expect(behaviorX.computeBoundary).toHaveBeenCalled() expect(behaviorY.computeBoundary).toHaveBeenCalled() // we should zoomed before call zoomEnd zoom.zoomed = false actions.hooks.trigger(actions.hooks.eventTypes.beforeEnd, e) expect(behaviorX.computeBoundary).toBeCalledTimes(1) }) it('should fail when zooming distance < minimalZoomDistance', () => { scroll.options.zoom = { minimalZoomDistance: 10, } new Zoom(scroll) const actions = scroll.scroller.actions const mockZoomingFn = jest.fn() scroll.on(scroll.eventTypes.zooming, mockZoomingFn) // zoomStart const e = createTouchEvent( { pageX: 30, pageY: 30 }, { pageX: 130, pageY: 130 } ) actions.hooks.trigger(actions.hooks.eventTypes.start, e) // zoom const e2 = createTouchEvent( { pageX: 30, pageY: 30 }, { pageX: 135, pageY: 135 } ) actions.hooks.trigger(actions.hooks.eventTypes.beforeMove, e2) expect(mockZoomingFn).toHaveBeenCalledTimes(0) }) it('should have correct behavior when zooming out', () => { scroll.options.zoom = { max: 2, } const zoom = new Zoom(scroll) const actions = scroll.scroller.actions const translater = scroll.scroller.translater const mockZoomingFn = jest.fn() scroll.on(scroll.eventTypes.zooming, mockZoomingFn) // zoomStart const e = createTouchEvent( { pageX: 30, pageY: 30 }, { pageX: 130, pageY: 130 } ) actions.hooks.trigger(actions.hooks.eventTypes.start, e) // zoom const e2 = createTouchEvent( { pageX: 30, pageY: 30 }, { pageX: 150, pageY: 150 } ) actions.hooks.trigger(actions.hooks.eventTypes.beforeMove, e2) // triggered zooming hooks expect(mockZoomingFn).toHaveBeenCalled() expect(mockZoomingFn).toHaveBeenCalledTimes(1) // beforeMove hooks use translater.translate, not scroller.scrollTo expect(scroll.scroller.translater.translate).toBeCalledWith({ x: -16, y: -16, scale: 1.2, }) expect(zoom.scale).toBe(1.2) // triggered beforeTranslate hooks const transformString: string[] = [] const transformPoint = { scale: 1.2, } translater.hooks.trigger( translater.hooks.eventTypes.beforeTranslate, transformString, transformPoint ) expect(transformString[0]).toBe('scale(1.2)') // keep zoom const e3 = createTouchEvent( { pageX: 30, pageY: 30 }, { pageX: 170, pageY: 170 } ) actions.hooks.trigger('beforeMove', e3) // triggered zooming hooks expect(mockZoomingFn).toHaveBeenCalledTimes(2) expect(scroll.scroller.translater.translate).toHaveBeenLastCalledWith({ x: -32, y: -32, scale: 1.4, }) expect(zoom.scale).toBe(1.4) // keep zoom, allow zooming exceeds max const e4 = createTouchEvent( { pageX: 30, pageY: 30 }, { pageX: 240, pageY: 240 } ) actions.hooks.trigger('beforeMove', e4) // triggered zooming hooks expect(mockZoomingFn).toHaveBeenCalledTimes(3) expect(scroll.scroller.translater.translate).toHaveBeenLastCalledWith({ x: -85, y: -85, scale: 2 * 2 * Math.pow(0.5, 2 / 2.1), }) expect(zoom.scale).toBeCloseTo(2.067) // zoom end, perform a rebound animation,back to max scale actions.hooks.trigger('beforeEnd') expect(zoom.scale).toBe(2) expect(scroll.scroller.scrollTo).toHaveBeenLastCalledWith( 0, 0, 800, ease.bounce, { start: { scale: 2.0671155660140554, }, end: { scale: 2, }, } ) }) it('should have correct behavior when zooming in', () => { scroll.options.zoom = { min: 0.5, } const zoom = new Zoom(scroll) const actions = scroll.scroller.actions const translater = scroll.scroller.translater const mockZoomingFn = jest.fn() scroll.on(scroll.eventTypes.zooming, mockZoomingFn) // zoomStart const e = createTouchEvent( { pageX: 30, pageY: 30 }, { pageX: 130, pageY: 130 } ) actions.hooks.trigger(actions.hooks.eventTypes.start, e) // zoom const e2 = createTouchEvent( { pageX: 30, pageY: 30 }, { pageX: 110, pageY: 110 } ) actions.hooks.trigger(actions.hooks.eventTypes.beforeMove, e2) // triggered zooming hooks expect(mockZoomingFn).toHaveBeenCalled() expect(mockZoomingFn).toHaveBeenCalledTimes(1) // beforeMove hooks use translater.translate, not scroller.scrollTo expect(scroll.scroller.translater.translate).toBeCalledWith({ x: 16, y: 16, scale: 0.8, }) expect(zoom.scale).toBe(0.8) // triggered beforeTranslate hooks const transformString: string[] = [] const transformPoint = { scale: 0.8, } translater.hooks.trigger( translater.hooks.eventTypes.beforeTranslate, transformString, transformPoint ) expect(transformString[0]).toBe('scale(0.8)') // keep zoom const e3 = createTouchEvent( { pageX: 30, pageY: 30 }, { pageX: 90, pageY: 90 } ) actions.hooks.trigger('beforeMove', e3) // triggered zooming hooks expect(mockZoomingFn).toHaveBeenCalledTimes(2) expect(scroll.scroller.translater.translate).toHaveBeenLastCalledWith({ x: 32, y: 32, scale: 0.6, }) expect(zoom.scale).toBe(0.6) // keep zoom, allow zooming exceeds max const e4 = createTouchEvent( { pageX: 30, pageY: 30 }, { pageX: 40, pageY: 40 } ) actions.hooks.trigger('beforeMove', e4) // triggered zooming hooks expect(mockZoomingFn).toHaveBeenCalledTimes(3) expect(scroll.scroller.translater.translate).toHaveBeenLastCalledWith({ x: 57, y: 57, scale: 0.5 * 0.5 * Math.pow(2, 0.1 / 0.5), }) expect(zoom.scale).toBeCloseTo(0.287) // zoom end, perform a rebound animation,back to max scale actions.hooks.trigger('beforeEnd') expect(zoom.scale).toBe(0.5) expect(scroll.scroller.scrollTo).toHaveBeenLastCalledWith( 0, 0, 800, ease.bounce, { start: { scale: 0.2871745887492588, }, end: { scale: 0.5, }, } ) }) it('should have correct behavior for zoomTo', (done) => { scroll.options.zoom = { min: 0.5, max: 3, start: 1, } const zoom = new Zoom(scroll) const { scrollBehaviorX, scrollBehaviorY } = scroll.scroller scrollBehaviorX.contentSize = 100 scrollBehaviorY.contentSize = 100 scrollBehaviorX.wrapperSize = 100 scrollBehaviorY.wrapperSize = 100 // [0, 0] as origin, scale to 2 zoom.zoomTo(2, 0, 0) expect(scroll.scroller.scrollTo).toHaveBeenCalledWith( 0, 0, 800, ease.bounce, { start: { scale: 1, }, end: { scale: 2, }, } ) // ['center', 'center'] as origin, time is 300, scale to 1.5 zoom.zoomTo(1.5, 'center', 'center', 300) expect(scroll.scroller.scrollTo).toHaveBeenCalledWith( 0, 0, 300, ease.bounce, { start: { scale: 2, }, end: { scale: 1.5, }, } ) // ['left', 'top'] as origin, time is 300, scale to 3 zoom.zoomTo(3, 'left', 'top', 300) expect(scroll.scroller.scrollTo).toHaveBeenCalledWith( 0, 0, 300, ease.bounce, { start: { scale: 1.5, }, end: { scale: 3, }, } ) // ['right', 'bottom'] as origin, time is 300, scale to 3 zoom.zoomTo(2, 'right', 'bottom', 300) expect(scroll.scroller.scrollTo).toHaveBeenCalledWith( 0, 0, 300, ease.bounce, { start: { scale: 3, }, end: { scale: 2, }, } ) // The purpose for improving test coverage setTimeout(() => { done() }, 320) }) it('should support full hooks', () => { scroll.options.zoom = { min: 1, start: 1, max: 4, } new Zoom(scroll) const actions = scroll.scroller.actions const behaviorX = scroll.scroller.scrollBehaviorX const behaviorY = scroll.scroller.scrollBehaviorY behaviorX.checkInBoundary = jest.fn().mockImplementation(() => { return { inBoundary: true } }) behaviorY.checkInBoundary = jest.fn().mockImplementation(() => { return { inBoundary: true } }) const mockBeforeZoomStartFn = jest.fn() const mockZoomStartFn = jest.fn() const mockZoomingFn = jest.fn() const mockZoomEndFn = jest.fn() // tap hooks scroll.on(scroll.eventTypes.beforeZoomStart, mockBeforeZoomStartFn) scroll.on(scroll.eventTypes.zoomStart, mockZoomStartFn) scroll.on(scroll.eventTypes.zooming, mockZoomingFn) scroll.on(scroll.eventTypes.zoomEnd, mockZoomEndFn) // zoomStart const e1 = createTouchEvent( { pageX: 30, pageY: 30 }, { pageX: 130, pageY: 130 } ) actions.hooks.trigger(actions.hooks.eventTypes.start, e1) // zooming const e2 = createTouchEvent( { pageX: 30, pageY: 30 }, { pageX: 150, pageY: 150 } ) actions.hooks.trigger(actions.hooks.eventTypes.beforeMove, e2) // zoomEnd actions.hooks.trigger(actions.hooks.eventTypes.beforeEnd) expect(mockBeforeZoomStartFn).toBeCalledTimes(1) expect(mockZoomStartFn).toBeCalledTimes(1) expect(mockZoomingFn).toBeCalledTimes(1) expect(mockZoomEndFn).toBeCalledTimes(1) }) it('should destroy all events', () => { new Zoom(scroll) const { actions, scrollBehaviorX, scrollBehaviorY, translater, } = scroll.scroller scroll.hooks.trigger(scroll.hooks.eventTypes.destroy) expect(scrollBehaviorX.hooks.events['beforeComputeBoundary'].length).toBe(0) expect(scrollBehaviorY.hooks.events['beforeComputeBoundary'].length).toBe(0) expect(actions.hooks.events['start'].length).toBe(0) expect(actions.hooks.events['beforeMove'].length).toBe(0) expect(actions.hooks.events['beforeEnd'].length).toBe(0) expect(translater.hooks.events['beforeTranslate'].length).toBe(0) }) it('should work well when content DOM has changed', () => { const zoom = new Zoom(scroll) const newContent = document.createElement('p') scroll.hooks.trigger(scroll.hooks.eventTypes.contentChanged, newContent) expect(zoom.scale).toBe(1) expect(newContent.style['transform-origin' as any]).toBe('0 0') }) it('should prevent initial scroll when startScale not equals 1', () => { const { wrapper } = createZoomElements() scroll = new BScroll(wrapper, { zoom: { start: 2, }, }) new Zoom(scroll) const ret = scroll.hooks.trigger( scroll.hooks.eventTypes.beforeInitialScrollTo ) expect(ret).toBeTruthy() }) it('should calculate right size when scrollBehavior triggered beforeComputeBoundary hook', () => { const zoom = new Zoom(scroll) zoom.scale = 1.2 const scrollBehaviorX = scroll.scroller.scrollBehaviorX const scrollBehaviorY = scroll.scroller.scrollBehaviorY scrollBehaviorX.hooks.trigger( scrollBehaviorX.hooks.eventTypes.beforeComputeBoundary ) scrollBehaviorY.hooks.trigger( scrollBehaviorY.hooks.eventTypes.beforeComputeBoundary ) expect(scrollBehaviorX.contentSize).toBe(360) expect(scrollBehaviorY.contentSize).toBe(360) }) it('should dispatch scrollEnd event when two fingers make bs scroll', () => { new Zoom(scroll) let endScale scroll.scroller.actions.hooks.trigger( scroll.scroller.actions.hooks.eventTypes.start, { touches: [ { pageX: 1, pageY: 1, }, { pageX: 2, pageY: 2, }, ], } ) scroll.on(scroll.eventTypes.zoomEnd, ({ scale }: { scale: number }) => { endScale = scale }) scroll.scroller.hooks.trigger(scroll.scroller.hooks.eventTypes.scrollEnd) expect(endScale).toBe(1) }) }) ================================================ FILE: packages/zoom/src/index.ts ================================================ import BScroll, { Behavior, TranslaterPoint } from '@better-scroll/core' import propertiesConfig from './propertiesConfig' import { getDistance, ease, between, offsetToBody, getRect, style, EventEmitter, extend, getNow, requestAnimationFrame, cancelAnimationFrame, } from '@better-scroll/shared-utils' export type ZoomOptions = Partial | true export interface ZoomConfig { start: number min: number max: number initialOrigin: [OriginX, OriginY] minimalZoomDistance: number bounceTime: number } type OriginX = number | 'left' | 'right' | 'center' type OriginY = number | 'top' | 'bottom' | 'center' declare module '@better-scroll/core' { interface CustomOptions { zoom?: ZoomOptions } interface CustomAPI { zoom: PluginAPI } } interface PluginAPI { zoomTo(scale: number, x: OriginX, y: OriginY, bounceTime?: number): void } interface Point { x: number y: number baseScale: number } interface ResolveFormula { left(): number top(): number right(): number bottom(): number center(index: number): number } const TWO_FINGERS = 2 const RAW_SCALE = 1 export default class Zoom implements PluginAPI { static pluginName = 'zoom' origin: Point scale: number = RAW_SCALE zoomOpt: ZoomConfig numberOfFingers: number private zoomed: boolean private startDistance: number private startScale: number private wrapper: HTMLElement private prevScale: number = 1 private hooksFn: Array<[EventEmitter, string, Function]> constructor(public scroll: BScroll) { this.init() } init() { this.handleBScroll() this.handleOptions() this.handleHooks() this.tryInitialZoomTo(this.zoomOpt) } zoomTo(scale: number, x: OriginX, y: OriginY, bounceTime?: number) { const { originX, originY } = this.resolveOrigin(x, y) const origin: Point = { x: originX, y: originY, baseScale: this.scale, } this._doZoomTo(scale, origin, bounceTime, true) } private handleBScroll() { this.scroll.proxy(propertiesConfig) this.scroll.registerType([ 'beforeZoomStart', 'zoomStart', 'zooming', 'zoomEnd', ]) } private handleOptions() { const userOptions = (this.scroll.options.zoom === true ? {} : this.scroll.options.zoom) as Partial const defaultOptions: ZoomConfig = { start: 1, min: 1, max: 4, initialOrigin: [0, 0], minimalZoomDistance: 5, bounceTime: 800, // ms } this.zoomOpt = extend(defaultOptions, userOptions) } private handleHooks() { const scroll = this.scroll const scroller = this.scroll.scroller this.wrapper = this.scroll.scroller.wrapper this.setTransformOrigin(this.scroll.scroller.content) const scrollBehaviorX = scroller.scrollBehaviorX const scrollBehaviorY = scroller.scrollBehaviorY this.hooksFn = [] // BScroll this.registerHooks( scroll.hooks, scroll.hooks.eventTypes.contentChanged, (content: HTMLElement) => { this.setTransformOrigin(content) this.scale = RAW_SCALE this.tryInitialZoomTo(this.zoomOpt) } ) this.registerHooks( scroll.hooks, scroll.hooks.eventTypes.beforeInitialScrollTo, () => { // if perform a zoom action, we should prevent initial scroll when initialised if (this.zoomOpt.start !== RAW_SCALE) { return true } } ) // enlarge boundary this.registerHooks( scrollBehaviorX.hooks, scrollBehaviorX.hooks.eventTypes.beforeComputeBoundary, () => { // content may change, don't cache it's size const contentSize = getRect(this.scroll.scroller.content) scrollBehaviorX.contentSize = Math.floor(contentSize.width * this.scale) } ) this.registerHooks( scrollBehaviorY.hooks, scrollBehaviorY.hooks.eventTypes.beforeComputeBoundary, () => { // content may change, don't cache it's size const contentSize = getRect(this.scroll.scroller.content) scrollBehaviorY.contentSize = Math.floor( contentSize.height * this.scale ) } ) // touch event this.registerHooks( scroller.actions.hooks, scroller.actions.hooks.eventTypes.start, (e: TouchEvent) => { const numberOfFingers = (e.touches && e.touches.length) || 0 this.fingersOperation(numberOfFingers) if (numberOfFingers === TWO_FINGERS) { this.zoomStart(e) } } ) this.registerHooks( scroller.actions.hooks, scroller.actions.hooks.eventTypes.beforeMove, (e: TouchEvent) => { const numberOfFingers = (e.touches && e.touches.length) || 0 this.fingersOperation(numberOfFingers) if (numberOfFingers === TWO_FINGERS) { this.zoom(e) return true } } ) this.registerHooks( scroller.actions.hooks, scroller.actions.hooks.eventTypes.beforeEnd, (e: TouchEvent) => { const numberOfFingers = this.fingersOperation() if (numberOfFingers === TWO_FINGERS) { this.zoomEnd() return true } } ) this.registerHooks( scroller.translater.hooks, scroller.translater.hooks.eventTypes.beforeTranslate, (transformStyle: string[], point: TranslaterPoint) => { const scale = point.scale ? point.scale : this.prevScale this.prevScale = scale transformStyle.push(`scale(${scale})`) } ) this.registerHooks( scroller.hooks, scroller.hooks.eventTypes.scrollEnd, () => { if (this.fingersOperation() === TWO_FINGERS) { this.scroll.trigger(this.scroll.eventTypes.zoomEnd, { scale: this.scale, }) } } ) this.registerHooks(this.scroll.hooks, 'destroy', this.destroy) } private setTransformOrigin(content: HTMLElement) { content.style[style.transformOrigin as any] = '0 0' } private tryInitialZoomTo(options: ZoomConfig) { const { start, initialOrigin } = options const { scrollBehaviorX, scrollBehaviorY } = this.scroll.scroller if (start !== RAW_SCALE) { // Movable plugin may wanna modify minScrollPos or maxScrollPos // so we force Movable to caculate them this.resetBoundaries([scrollBehaviorX, scrollBehaviorY]) this.zoomTo(start, initialOrigin[0], initialOrigin[1], 0) } } // getter or setter operation private fingersOperation(amounts?: number): number | void { if (typeof amounts === 'number') { this.numberOfFingers = amounts } else { return this.numberOfFingers } } private _doZoomTo( scale: number, origin: Point, time: number = this.zoomOpt.bounceTime, useCurrentPos = false ) { const { min, max } = this.zoomOpt const fromScale = this.scale const toScale = between(scale, min, max) // dispatch zooming hooks ;(() => { if (time === 0) { this.scroll.trigger(this.scroll.eventTypes.zooming, { scale: toScale, }) return } if (time > 0) { let timer: number const startTime = getNow() const endTime = startTime + time const scheduler = () => { const now = getNow() if (now >= endTime) { this.scroll.trigger(this.scroll.eventTypes.zooming, { scale: toScale, }) cancelAnimationFrame(timer) return } const ratio = ease.bounce.fn((now - startTime) / time) const currentScale = ratio * (toScale - fromScale) + fromScale this.scroll.trigger(this.scroll.eventTypes.zooming, { scale: currentScale, }) timer = requestAnimationFrame(scheduler) } // start scheduler job scheduler() } })() // suppose you are zooming by two fingers this.fingersOperation(2) this._zoomTo(toScale, fromScale, origin, time, useCurrentPos) } private _zoomTo( toScale: number, fromScale: number, origin: Point, time: number, useCurrentPos = false ) { const ratio = toScale / origin.baseScale this.setScale(toScale) const scroller = this.scroll.scroller const { scrollBehaviorX, scrollBehaviorY } = scroller this.resetBoundaries([scrollBehaviorX, scrollBehaviorY]) // position is restrained in boundary const newX = this.getNewPos( origin.x, ratio, scrollBehaviorX, true, useCurrentPos ) const newY = this.getNewPos( origin.y, ratio, scrollBehaviorY, true, useCurrentPos ) if ( scrollBehaviorX.currentPos !== Math.round(newX) || scrollBehaviorY.currentPos !== Math.round(newY) || toScale !== fromScale ) { scroller.scrollTo(newX, newY, time, ease.bounce, { start: { scale: fromScale, }, end: { scale: toScale, }, }) } } private resolveOrigin(x: OriginX, y: OriginY) { const { scrollBehaviorX, scrollBehaviorY } = this.scroll.scroller const resolveFormula: ResolveFormula = { left() { return 0 }, top() { return 0 }, right() { return scrollBehaviorX.contentSize }, bottom() { return scrollBehaviorY.contentSize }, center(index: number) { const baseSize = index === 0 ? scrollBehaviorX.contentSize : scrollBehaviorY.contentSize return baseSize / 2 }, } return { originX: typeof x === 'number' ? x : resolveFormula[x](0), originY: typeof y === 'number' ? y : resolveFormula[y](1), } } zoomStart(e: TouchEvent) { const firstFinger = e.touches[0] const secondFinger = e.touches[1] this.startDistance = this.getFingerDistance(e) this.startScale = this.scale let { left, top } = offsetToBody(this.wrapper) this.origin = { x: Math.abs(firstFinger.pageX + secondFinger.pageX) / 2 + left - this.scroll.x, y: Math.abs(firstFinger.pageY + secondFinger.pageY) / 2 + top - this.scroll.y, baseScale: this.startScale, } this.scroll.trigger(this.scroll.eventTypes.beforeZoomStart) } zoom(e: TouchEvent) { const currentDistance = this.getFingerDistance(e) // at least minimalZoomDistance pixels for the zoom to initiate if ( !this.zoomed && Math.abs(currentDistance - this.startDistance) < this.zoomOpt.minimalZoomDistance ) { return } // when out of boundary , perform a damping algorithm const endScale = this.dampingScale( (currentDistance / this.startDistance) * this.startScale ) const ratio = endScale / this.startScale this.setScale(endScale) if (!this.zoomed) { this.zoomed = true this.scroll.trigger(this.scroll.eventTypes.zoomStart) } const scroller = this.scroll.scroller const { scrollBehaviorX, scrollBehaviorY } = scroller const x = this.getNewPos( this.origin.x, ratio, scrollBehaviorX, false, false ) const y = this.getNewPos( this.origin.y, ratio, scrollBehaviorY, false, false ) this.scroll.trigger(this.scroll.eventTypes.zooming, { scale: this.scale, }) scroller.translater.translate({ x, y, scale: endScale }) } zoomEnd() { if (!this.zoomed) return // if out of boundary, do rebound! if (this.shouldRebound()) { this._doZoomTo(this.scale, this.origin, this.zoomOpt.bounceTime) return } this.scroll.trigger(this.scroll.eventTypes.zoomEnd, { scale: this.scale }) } private getFingerDistance(e: TouchEvent): number { const firstFinger = e.touches[0] const secondFinger = e.touches[1] const deltaX = Math.abs(firstFinger.pageX - secondFinger.pageX) const deltaY = Math.abs(firstFinger.pageY - secondFinger.pageY) return getDistance(deltaX, deltaY) } private shouldRebound(): boolean { const { min, max } = this.zoomOpt const currentScale = this.scale // scale exceeded! if (currentScale !== between(currentScale, min, max)) { return true } const { scrollBehaviorX, scrollBehaviorY } = this.scroll.scroller // enlarge boundaries manually when zoom is end this.resetBoundaries([scrollBehaviorX, scrollBehaviorY]) const { inBoundary: xInBoundary } = scrollBehaviorX.checkInBoundary() const { inBoundary: yInBoundary } = scrollBehaviorX.checkInBoundary() return !(xInBoundary && yInBoundary) } private dampingScale(scale: number) { const { min, max } = this.zoomOpt if (scale < min) { scale = 0.5 * min * Math.pow(2.0, scale / min) } else if (scale > max) { scale = 2.0 * max * Math.pow(0.5, max / scale) } return scale } private setScale(scale: number) { this.scale = scale } private resetBoundaries(scrollBehaviorPairs: [Behavior, Behavior]) { scrollBehaviorPairs.forEach((behavior) => behavior.computeBoundary()) } private getNewPos( origin: number, lastScale: number, scrollBehavior: Behavior, shouldInBoundary?: boolean, useCurrentPos = false ) { let newPos = origin - origin * lastScale + (useCurrentPos ? scrollBehavior.currentPos : scrollBehavior.startPos) if (shouldInBoundary) { newPos = between( newPos, scrollBehavior.maxScrollPos, scrollBehavior.minScrollPos ) } // maxScrollPos or minScrollPos maybe a negative or positive digital return newPos > 0 ? Math.floor(newPos) : Math.ceil(newPos) } private registerHooks(hooks: EventEmitter, name: string, handler: Function) { hooks.on(name, handler, this) this.hooksFn.push([hooks, name, handler]) } destroy() { this.hooksFn.forEach((item) => { const hooks = item[0] const hooksName = item[1] const handlerFn = item[2] hooks.off(hooksName, handlerFn) }) this.hooksFn.length = 0 } } ================================================ FILE: packages/zoom/src/propertiesConfig.ts ================================================ const sourcePrefix = 'plugins.zoom' const propertiesMap = [ { key: 'zoomTo', name: 'zoomTo' } ] export default propertiesMap.map(item => { return { key: item.key, sourceKey: `${sourcePrefix}.${item.name}` } }) ================================================ FILE: postcss.config.js ================================================ module.exports = { plugins: [ require('autoprefixer')({ browsers: require('./package.json').browserslist }) ] } ================================================ FILE: scripts/build.js ================================================ const fs = require('fs') const path = require('path') const inquirer = require('inquirer') const rollup = require('rollup') const chalk = require('chalk') const zlib = require('zlib') const rimraf = require('rimraf') const typescript = require('rollup-plugin-typescript2') const uglify = require('rollup-plugin-uglify').uglify const execa = require('execa') const ora = require('ora') const spinner = ora({ prefixText: `${chalk.green('\n[building tasks]')}` }) function getPackagesName () { let ret let all = fs.readdirSync(resolve('packages')) // drop hidden file whose name is startWidth '.' // drop packages which would not be published(eg: examples and docs) ret = all .filter(name => { const isHiddenFile = /^\./g.test(name) return !isHiddenFile }).filter(name => { const isPrivatePackages = require(resolve(`packages/${name}/package.json`)).private return !isPrivatePackages }) return ret } function cleanPackagesOldDist(packagesName) { packagesName.forEach(name => { const distPath = resolve(`packages/${name}/dist`) const typePath = resolve(`packages/${name}/dist/types`) if (fs.existsSync(distPath)) { rimraf.sync(distPath) } fs.mkdirSync(distPath) fs.mkdirSync(typePath) }) } function resolve(p) { return path.resolve(__dirname, '../', p) } function PascalCase(str){ const re=/-(\w)/g; const newStr = str.replace(re, function (match, group1){ return group1.toUpperCase(); }) return newStr.charAt(0).toUpperCase() + newStr.slice(1); } const generateBanner = (packageName) => { let ret = '/*!\n' + ' * better-scroll / ' + packageName + '\n' + ' * (c) 2016-' + new Date().getFullYear() + ' ustbhuangyi\n' + ' * Released under the MIT License.\n' + ' */' return ret } const buildType = [ { format: 'umd', ext: '.js' }, { format: 'umd', ext: '.min.js' }, { format: 'es', ext: '.esm.js' } ] function generateBuildConfigs(packagesName) { const result = [] packagesName.forEach(name => { buildType.forEach((type) => { let config = { input: resolve(`packages/${name}/src/index.ts`), output: { file: resolve(`packages/${name}/dist/${name}${type.ext}`), name: PascalCase(name), format: type.format, banner: generateBanner(name) }, plugins: generateBuildPluginsConfigs(type.ext.indexOf('min')>-1, name) } // rename if (name === 'core' && config.output.format !== 'es') { config.output.name = 'BScroll' /** Disable warning for default imports */ config.output.exports = 'named' // it seems the umd bundle can not satisfies our demand config.output.footer = 'if(typeof window !== "undefined" && window.BScroll) { \n' + ' window.BScroll = window.BScroll.default;\n}' } // rollup will valiate config properties of config own and output a warning. // put packageName in prototype to ignore warning. Object.defineProperties(config, { 'packageName': { value: name }, 'ext': { value: type.ext } }) result.push(config) }) }) return result } function generateBuildPluginsConfigs(isMin) { const tsConfig = { verbosity: -1, tsconfig: path.resolve(__dirname, '../tsconfig.json'), } const plugins = [] if (isMin) { plugins.push(uglify()) } plugins.push(typescript(tsConfig)) return plugins } function build(builds) { let built = 0 const total = builds.length const next = () => { buildEntry(builds[built], built + 1, () => { builds[built-1] = null built++ if (built < total) { next() } }) } next() } function buildEntry(config, curIndex, next) { const isProd = /min\.js$/.test(config.output.file) spinner.start(`${config.packageName}${config.ext} is buiding now. \n`) rollup.rollup(config).then((bundle) => { bundle.write(config.output).then(({ output }) => { const code = output[0].code spinner.succeed(`${config.packageName}${config.ext} building has ended.`) function report(extra) { console.log(chalk.magenta(path.relative(process.cwd(), config.output.file)) + ' ' + getSize(code) + (extra || '')) next() } if (isProd) { zlib.gzip(code, (err, zipped) => { if (err) return reject(err) let words = `(gzipped: ${chalk.magenta(getSize(zipped))})` report(words) }) } else { report() } // since we need bundle code for three types // just generate .d.ts only once if (curIndex % 3 === 0) { copyDTSFiles(config.packageName) } }) }).catch((e) => { spinner.fail('buiding is failed') console.log(e) }) } function copyDTSFiles (packageName) { console.log(chalk.cyan('> start copying .d.ts file to dist dir of packages own.')) const sourceDir = resolve(`packages/${packageName}/dist/packages/${packageName}/src/*`) const targetDir = resolve(`packages/${packageName}/dist/types/`) execa.commandSync(`mv ${sourceDir} ${targetDir}`, { shell: true }) console.log(chalk.cyan('> copy job is done.')) rimraf.sync(resolve(`packages/${packageName}/dist/packages`)) rimraf.sync(resolve(`packages/${packageName}/dist/node_modules`)) } function getSize(code) { return (code.length / 1024).toFixed(2) + 'kb' } const getAnswersFromInquirer = async (packagesName) => { const question = { type: 'checkbox', name: 'packages', scroll: false, message: 'Select build repo(Support Multiple selection)', choices: packagesName.map(name => ({ value: name, name })) } let { packages } = await inquirer.prompt(question) // make no choice if (!packages.length) { console.log(chalk.yellow(` It seems that you did't make a choice. Please try it again. `)) return } // chose 'all' option if (packages.some(package => package === 'all')) { packages = getPackagesName() } const { yes } = await inquirer.prompt([{ name: 'yes', message: `Confirm build ${packages.join(' and ')} packages?`, type: 'list', choices: ['Y', 'N'] }]) if (yes === 'N') { console.log(chalk.yellow('[release] cancelled.')) return } return packages } const buildBootstrap = async () => { const packagesName = getPackagesName() // provide 'all' option packagesName.unshift('all') const answers = await getAnswersFromInquirer(packagesName) if (!answers) return cleanPackagesOldDist(answers) const buildConfigs = generateBuildConfigs(answers) build(buildConfigs) } buildBootstrap().catch(err => { console.error(err) process.exit(1) }) ================================================ FILE: scripts/checkYarn.js ================================================ if (!/yarn\.js$/.test(process.env.npm_execpath || '')) { console.warn( '\u001b[33mThis repository requires Yarn 1.x for scripts to work properly.\u001b[39m\n' ) process.exit(1) } ================================================ FILE: scripts/release.js ================================================ const execa = require('execa') const semver = require('semver') const inquirer = require('inquirer') const chalk = require('chalk') const curVersion = require('../lerna.json').version const release = async () => { console.log(chalk.yellow(`Current version: ${curVersion}`)) const bumps = ['patch', 'minor', 'major', 'prerelease-alpha', 'prerelease-beta', 'premajor'] const versions = {} bumps.forEach(b => { const args = b.split('-') versions[b] = semver.inc(curVersion, ...args) }) const bumpChoices = bumps.map(b => ({ name: `${b} (${versions[b]})`, value: b })) function getVersion (answers) { return answers.customVersion || versions[answers.bump] } function getNpmTags (version) { if (isPreRelease(version)) { return ['next'] } return ['latest', 'next'] } function isPreRelease (version) { return !!semver.prerelease(version) } const { bump, customVersion, npmTag } = await inquirer.prompt([ { name: 'bump', message: 'Select release type:', type: 'list', choices: [ ...bumpChoices, { name: 'custom', value: 'custom' } ] }, { name: 'customVersion', message: 'Input version:', type: 'input', when: answers => answers.bump === 'custom' }, { name: 'npmTag', message: 'Input npm tag:', type: 'list', choices: answers => getNpmTags(getVersion(answers)) } ]) const version = customVersion || versions[bump] const { yes } = await inquirer.prompt([{ name: 'yes', message: `Confirm releasing ${version} (${npmTag})?`, type: 'list', choices: ['Y', 'N'] }]) if (yes === 'N') { console.log(chalk.red('[release] cancelled.')) return } const releaseArguments = [ 'publish', version, '--force-publish', '*', '--npm-tag', npmTag ] console.log(chalk.grey(`lerna ${releaseArguments.join(' ')}`)) await execa(require.resolve('lerna/cli'), releaseArguments, { stdio: 'inherit' }) // it seems that sometimes 'gitHead' property in packages/**/package.json will change // but sometimes it won't, at this condition. work tree is clean, 'git commit ' will cause en error // so put it in try/catch, because we want to sync dev from master try { await execa('git', ['add', '-A'], { stdio: 'inherit' }) await execa('git', ['commit', '-m', `chore: ${version} published`], { stdio: 'inherit' }) await execa('git', ['push', 'origin', `master`], { stdio: 'inherit' }) } catch (error) {} // sync dev from master await execa('git', ['checkout', 'dev'], { stdio: 'inherit' }) await execa('git', ['rebase', 'master'], { stdio: 'inherit' }) await execa('git', ['push', 'origin', 'dev'], { stdio: 'inherit' }) await execa('git', ['checkout', 'master'], { stdio: 'inherit' }) } release().catch(err => { console.error(err) process.exit(1) }) ================================================ FILE: test-dts/core.test-d.ts ================================================ import { BScroll, expectFuncArguments, expectFuncReturnValue, Options } from './index' import { EaseItem } from '@better-scroll/shared-utils/src' describe('core api parameter type should be correct', () => { type ExtraTransform = { start: object; end: object } expectFuncArguments<[], BScroll['refresh']>() expectFuncArguments< [number, number, number?, EaseItem?, ExtraTransform?], BScroll['scrollTo'] >() expectFuncArguments< [number, number, number?, EaseItem?], BScroll['scrollBy'] >() expectFuncArguments< [ string | HTMLElement, number, number | boolean, number | boolean, EaseItem? ], BScroll['scrollToElement'] >() expectFuncArguments<[], BScroll['stop']>() expectFuncArguments<[], BScroll['enable']>() expectFuncArguments<[], BScroll['disable']>() expectFuncArguments<[], BScroll['destroy']>() // Events API expectFuncArguments<[string, Function, Object?], BScroll['on']>() expectFuncArguments<[string, Function, Object?], BScroll['once']>() expectFuncArguments<[string?, Function?], BScroll['off']>() expectFuncReturnValue, BScroll['on']>() expectFuncReturnValue, BScroll['once']>() expectFuncReturnValue | undefined, BScroll['off']>() }) ================================================ FILE: test-dts/index.d.ts ================================================ import BScroll from '@better-scroll/core' import Zoom from '@better-scroll/zoom' import Wheel from '@better-scroll/wheel' import Slide from '@better-scroll/slide' import ScrollBar from '@better-scroll/scroll-bar' import PullUp from '@better-scroll/pull-up' import PullDown from '@better-scroll/pull-down' import ObserveDom from '@better-scroll/observe-dom' import NestedScroll from '@better-scroll/nested-scroll' import MouseWheel from '@better-scroll/mouse-wheel' import Infinity from '@better-scroll/infinity' import Movable from '@better-scroll/movable' import { IfEquals } from './util' export * from '@better-scroll/core' export * from '@better-scroll/zoom' export * from '@better-scroll/wheel' export * from '@better-scroll/slide' export * from '@better-scroll/scroll-bar' export * from '@better-scroll/pull-up' export * from '@better-scroll/pull-down' export * from '@better-scroll/observe-dom' export * from '@better-scroll/nested-scroll' export * from '@better-scroll/mouse-wheel' export * from '@better-scroll/infinity' export * from '@better-scroll/movable' export type ArgumentsCheck< T extends any[], U extends (...args: any[]) => any > = ( ...args: any[] ) => U extends (...args: infer P) => any ? IfEquals, T> : never export type ReturnValueCheck any> = ( ...args: any[] ) => U extends (...args: any[]) => infer P ? IfEquals : never export declare function expectType>(): void export declare function expectError(value: T): void export declare function expectAssignable(): void export declare function expectFuncArguments< T extends any[], T1 extends ArgumentsCheck >(): void export declare function expectFuncReturnValue< T, T1 extends ReturnValueCheck >(): void export { BScroll, Zoom, Wheel, Slide, ScrollBar, PullUp, PullDown, ObserveDom, NestedScroll, MouseWheel, Infinity, Movable } ================================================ FILE: test-dts/plugin.test-d.ts ================================================ import { expectType, expectError, expectFuncArguments, expectFuncReturnValue, BScroll, createBScroll, Zoom, Wheel, Slide, ScrollBar, PullUp, PullDown, ObserveDom, NestedScroll, MouseWheel, Movable, ZoomConfig, WheelConfig, SlideConfig, MouseWheelOptions, ScrollbarOptions, ScrollbarConfig, PullUpLoadOptions, PullUpLoadConfig, PullDownRefreshOptions, PullDownRefreshConfig, NestedScrollOptions, } from './index' import { DeepNonNullable, FilterType, FilterUndef, FilterBoolean, ExcludeTrue, } from './util' import { EaseItem } from '@better-scroll/shared-utils/src' describe('BScroll.use should be used normally', () => { // @ts-expect-error expectError(BScroll.use()) // @ts-expect-error expectError(BScroll.use({})) // @ts-expect-error expectError(BScroll.use({ pluginName: 'pluginName' })) // @ts-expect-error expectError(BScroll.use(function () {})) // @ts-expect-error expectError(BScroll.use(class Plugin {})) expectError( BScroll.use( class Plugin { static pluginName = 'pluginName' } ) ) }) describe('zoom plugin options and api type shoule be inferred correctly', () => { BScroll.use(Zoom) const bscroll = createBScroll('', { zoom: { max: 1, min: 1, start: 1, initialOrigin: ['left', 'top'], minimalZoomDistance: 5, bounceTime: 800, }, }) // Options type BSOptions = DeepNonNullable expectType | true, BSOptions['zoom']>() expectType['max']>>() expectType['min']>>() expectType['start']>>() expectType< [OriginX, OriginY], FilterUndef['initialOrigin']> >() expectType< number, FilterUndef['minimalZoomDistance']> >() expectType< number, FilterUndef['bounceTime']> >() // API type ZoomToAPI = typeof bscroll.zoomTo type OriginX = number | 'left' | 'right' | 'center' type OriginY = number | 'top' | 'bottom' | 'center' expectFuncArguments<[number, OriginX, OriginY, number?], ZoomToAPI>() }) describe('whell plugin options and api type shoule be inferred correctly', () => { BScroll.use(Wheel) const bscroll = new BScroll('', { wheel: { selectedIndex: 1, rotate: 1, adjustTime: 1, wheelWrapperClass: 'wheelWrapperClass', wheelItemClass: 'wheelItemClass', wheelDisabledItemClass: 'wheelDisabledItemClass', }, }) // Options type BSOptions = DeepNonNullable expectType | true, BSOptions['wheel']>() expectType['rotate']>>() expectType< number, FilterUndef['adjustTime']> >() expectType< string, FilterUndef['wheelWrapperClass']> >() expectType< string, FilterUndef['wheelItemClass']> >() expectType< string, FilterUndef['wheelDisabledItemClass']> >() // API type WhellToAPI = typeof bscroll.wheelTo type GetSelectedIndexAPI = typeof bscroll.getSelectedIndex expectFuncArguments<[number?, number?, EaseItem?], WhellToAPI>() expectFuncReturnValue() }) describe('slider plugin options and api type shoule be inferred correctly', () => { BScroll.use(Slide) const bscroll = createBScroll('', { slide: { loop: true, }, }) // Options type BSOptions = DeepNonNullable type EaseType = { style: string fn: (t: number) => number } expectType, FilterBoolean>() expectType>>() expectType['loop']>>() expectType< number, FilterUndef['threshold']> >() expectType['speed']>>() expectType< EaseType, FilterUndef['easing']> >() expectType< boolean, FilterUndef['listenFlick']> >() // API type BS = typeof bscroll type Page = { pageX: number pageY: number } expectFuncArguments<[number?, EaseItem?], BS['next']>() expectFuncArguments<[number?, EaseItem?], BS['prev']>() expectFuncArguments<[number, number, number?, EaseItem?], BS['goToPage']>() expectFuncReturnValue() }) describe('scrollBar plugin options and api type shoule be inferred correctly', () => { BScroll.use(ScrollBar) const bscroll = new BScroll('', { scrollbar: { fade: true, interactive: true, }, }) // Options type BSOptions = DeepNonNullable expectType() expectType< boolean, FilterType> >() expectType, FilterBoolean>() expectType< boolean, FilterUndef['fade']> >() expectType< boolean, FilterUndef['interactive']> >() // API }) describe('pullUp plugin options and api type shoule be inferred correctly', () => { BScroll.use(PullUp) const bscroll = new BScroll('', { pullUpLoad: true, }) // Options type BSOptions = DeepNonNullable expectType() expectType< boolean, FilterType> >() expectType< number, FilterUndef['threshold']> >() // API type BS = typeof bscroll expectFuncArguments<[], BS['finishPullUp']>() expectFuncArguments<[(true | Partial)?], BS['openPullUp']>() expectFuncArguments<[], BS['closePullUp']>() }) describe('pullDown plugin options and api type shoule be inferred correctly', () => { BScroll.use(PullDown) const bscroll = new BScroll('', { pullDownRefresh: { threshold: 1, stop: 1, }, }) // Options type BSOptions = DeepNonNullable expectType() expectType< number, FilterUndef['threshold']> >() expectType< number, FilterUndef['stop']> >() // API type BS = typeof bscroll expectFuncArguments<[], BS['finishPullDown']>() expectFuncArguments< [(true | Partial)?], BS['openPullDown'] >() expectFuncArguments<[], BS['closePullDown']>() expectFuncArguments<[], BS['autoPullDownRefresh']>() }) describe('observeDom plugin options and api type shoule be inferred correctly', () => { BScroll.use(ObserveDom) const bscroll = new BScroll('', { observeDOM: true, }) // Options type BSOptions = DeepNonNullable expectType() }) describe('nestedScroll plugin options and api type shoule be inferred correctly', () => { BScroll.use(NestedScroll) const bscroll = new BScroll('', { nestedScroll: { groupId: 1, }, }) // Options type BSOptions = DeepNonNullable expectType< string | number, ExcludeTrue['groupId'] >() }) describe('mouseWheel plugin options and api type shoule be inferred correctly', () => { BScroll.use(MouseWheel) const bscroll = new BScroll('', { mouseWheel: { speed: 1, invert: true, easeTime: 1, discreteTime: 1, throttleTime: 1, dampingFactor: 0.1, }, }) // Options type BSOptions = DeepNonNullable expectType | true, BSOptions['mouseWheel']>() expectType< number, FilterUndef['speed']> >() expectType< boolean, FilterUndef['invert']> >() expectType< number, FilterUndef['easeTime']> >() expectType< number, FilterUndef['discreteTime']> >() expectType< number, FilterUndef['throttleTime']> >() expectType< number, FilterUndef['dampingFactor']> >() }) describe('movable plugin options and api type shoule be inferred correctly', () => { BScroll.use(Movable) const bscroll = new BScroll('', { movable: true, }) // Options type BSOptions = DeepNonNullable expectType>() }) ================================================ FILE: test-dts/tsconfig.json ================================================ { "extends": "../tsconfig.json", "compilerOptions": { "noEmit": true, "declaration": true }, "include": ["../test-dts"], "exclude": [ "../tests", "../packages/*/src/__tests__", "../packages/*/src/__mocks__", "../packages/*/src" ] } ================================================ FILE: test-dts/util.d.ts ================================================ type NonUndefined = T extends undefined ? never : T export type DeepNonNullable = { [P in keyof T]-?: T[P] extends object ? DeepNonNullable> : NonUndefined } export type IfEquals = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? A : B export type FilterType = T extends F ? never : T export type FilterUndef = FilterType export type FilterBoolean = FilterType export type FilterNull = FilterType export type FilterString = FilterType export type FilterNumber = FilterType export type FilterSymbol = FilterType export type FilterArray = FilterType> export type FilterFunc = FilterType export type FilterObject = FilterType export type ExcludeTrue = FilterType ================================================ FILE: tests/dts/index.d.ts ================================================ ================================================ FILE: tests/e2e/compose-plugins/compose-plugins.e2e.ts ================================================ import { Page } from 'puppeteer' import extendsTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(1000000) describe('compose plugins', () => { let page = (global as any).page as Page extendsTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/compose/') }) it('should display 4 items at least', async () => { const itemsCounts = await page.$$eval( '.compose .example-item', (elements) => elements.length ) const itemsContent = await page.$$eval( '.compose .example-item', (elements) => { return elements.map((el) => el.textContent) } ) expect(itemsContent).toEqual([ 'pullup-pulldown', 'pullup-pulldown-slide', 'pullup-pulldown-outnested', 'slide-nested', ]) await expect(itemsCounts).toBeGreaterThanOrEqual(4) }) }) ================================================ FILE: tests/e2e/compose-plugins/pullup-pulldown-nested.e2e.ts ================================================ import { Page } from 'puppeteer' import extendsTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(1000000) describe('Compose/pullup-pulldown-nested', () => { let page = (global as any).page as Page extendsTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/compose/pullup-pulldown-outnested') }) it('should trigger outer scroll pullingdown when BS reached the top', async () => { await page.waitFor(1000) await page.dispatchScroll({ x: 100, y: 150, xDistance: 0, yDistance: 300, gestureSourceType: 'touch', }) const { isShowPullDownTxt, isShowLoading } = await page.$$eval( '.pulldown-wrapper', (elements) => { const isShowPullDownTxt = window.getComputedStyle(elements[0].children[0]).display === 'block' const isShowLoading = window.getComputedStyle(elements[0].children[1].children[0]) .display === 'block' return { isShowPullDownTxt, isShowLoading, } } ) expect(isShowPullDownTxt).toEqual(false) expect(isShowLoading).toEqual(true) }) it('should trigger outer scroll pullingup when BS reached the bottom', async () => { await page.waitFor(1000) await page.dispatchScroll({ x: 200, y: 300, xDistance: 0, yDistance: -500, speed: 1500, gestureSourceType: 'touch', }) await page.waitFor(4000) const itemsCounts = await page.$$eval( '.outer-list-item2', (element) => element.length ) await expect(itemsCounts).toBeGreaterThanOrEqual(16) }) it('the inner scroll should scroll normally', async () => { await page.waitFor(1000) await page.dispatchScroll({ x: 200, y: 200, xDistance: 0, yDistance: -500, speed: 1500, gestureSourceType: 'touch', }) await page.waitFor(1000) const transformText = await page.$eval('.inner-content', (node) => { return window.getComputedStyle(node).transform }) const y = getTranslate(transformText, 'y') expect(y).toBe(-814) }) }) ================================================ FILE: tests/e2e/compose-plugins/pullup-pulldown-slide.e2e.ts ================================================ import { Page } from 'puppeteer' import extendsTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(1000000) describe('Compose/pullup-pulldown-slide', () => { let page = (global as any).page as Page extendsTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/compose/pullup-pulldown-slide') }) it('should trigger pullingdown when BS reached the top', async () => { await page.waitFor(1000) await page.dispatchScroll({ x: 100, y: 150, xDistance: 0, yDistance: 300, gestureSourceType: 'touch', }) const { isShowPullDownTxt, isShowLoading } = await page.$$eval( '.pulldown-wrapper', (elements) => { const isShowPullDownTxt = window.getComputedStyle(elements[0].children[0]).display === 'block' const isShowLoading = window.getComputedStyle(elements[0].children[1].children[0]) .display === 'block' return { isShowPullDownTxt, isShowLoading, } } ) expect(isShowPullDownTxt).toEqual(false) expect(isShowLoading).toEqual(true) await page.waitFor(1000) }) it('should switch next page when BS scroll half page', async () => { await page.waitFor(1000) await page.dispatchScroll({ x: 100, y: 200, xDistance: 0, yDistance: -50, gestureSourceType: 'touch', }) await page.waitFor(1000) const transformText = await page.$eval( '.pullup-pulldown-slide-scroller', (node) => { return window.getComputedStyle(node).transform } ) const y = getTranslate(transformText, 'y') expect(y).toBe(-627) }) it('should trigger pullingup when BS reached the bottom', async () => { await page.waitFor(1000) for (let i = 0; i < 9; i++) { await page.dispatchScroll({ x: 100, y: 200, xDistance: 0, yDistance: -150, gestureSourceType: 'touch', }) await page.waitFor(1500) } await page.dispatchScroll({ x: 100, y: 200, xDistance: 0, yDistance: -150, gestureSourceType: 'touch', }) await page.waitFor(2000) const itemsCounts = await page.$$eval( '.pullup-pulldown-slide-item', (element) => element.length ) await expect(itemsCounts).toBeGreaterThanOrEqual(10) }) }) ================================================ FILE: tests/e2e/compose-plugins/pullup-pulldown.e2e.ts ================================================ import { Page } from 'puppeteer' import extendsTouch from '../../util/extendTouch' jest.setTimeout(1000000) describe('Compose/pullup-pulldown', () => { let page = (global as any).page as Page extendsTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/compose/pullup-pulldown') }) it('should render DOM correctly', async () => { await page.waitFor(300) const itemsCounts = await page.$$eval( '.pullup-down-list-item', element => element.length ) await expect(itemsCounts).toBeGreaterThanOrEqual(30) }) it('should trigger pullingdown when BS reached the top', async () => { await page.waitFor(1000) await page.dispatchScroll({ x: 100, y: 150, xDistance: 0, yDistance: 300, gestureSourceType: 'touch' }) const { isShowPullDownTxt, isShowLoading } = await page.$$eval( '.pulldown-wrapper', elements => { const isShowPullDownTxt = window.getComputedStyle(elements[0].children[0]).display === 'block' const isShowLoading = window.getComputedStyle(elements[0].children[1].children[0]) .display === 'block' return { isShowPullDownTxt, isShowLoading } } ) expect(isShowPullDownTxt).toEqual(false) expect(isShowLoading).toEqual(true) await page.waitFor(1000) }) it('should trigger pullingup when BS reached the bottom', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 200, y: 630, xDistance: 0, yDistance: -1000, speed: 1500, gestureSourceType: 'touch' }) await page.waitFor(4000) const itemsCounts = await page.$$eval( '.pullup-down-list-item', element => element.length ) await expect(itemsCounts).toBeGreaterThanOrEqual(60) }) }) ================================================ FILE: tests/e2e/compose-plugins/slide-nested.e2e.ts ================================================ import { Page } from 'puppeteer' import extendsTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(1000000) describe('Compose/slide-nested', () => { let page = (global as any).page as Page extendsTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/compose/slide-nested') }) it('the outer scroll should scroll normally', async () => { await page.waitFor(1000) await page.dispatchScroll({ x: 200, y: 100, xDistance: 0, yDistance: -70, gestureSourceType: 'touch', }) await page.waitFor(1000) const transformText = await page.$eval('.outer-content', (node) => { return window.getComputedStyle(node).transform }) const y = getTranslate(transformText, 'y') await expect(y).toBeLessThan(-30) }) it('the inner scroll should scroll normally', async () => { await page.waitFor(1000) await page.dispatchScroll({ x: 200, y: 300, xDistance: -100, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(1000) const transformText = await page.$eval('.slide-banner-content', (node) => { return window.getComputedStyle(node).transform }) const x = getTranslate(transformText, 'x') expect(x).toBe(-666) }) }) ================================================ FILE: tests/e2e/core/corescroll.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' // set default timeout jest.setTimeout(1000000) describe('CoreScroll', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/core/') }) it('should display 4 items at least', async () => { const itemsCounts = await page.$$eval( '.core .example-item', (element) => element.length ) const itemsContent = await page.$$eval('.core .example-item', (element) => element.map((el) => el.textContent) ) expect(itemsContent).toEqual([ 'vertical', 'horizontal', 'dynamic-content', 'specified-content', 'freescroll', 'vertical rotated(v2.3.0)', 'horizontal rotated(v2.3.0)', ]) expect(itemsCounts).toBeGreaterThanOrEqual(5) }) it("should display correct items's texts", async () => { const itemsContent = await page.$$eval('.core .example-item', (element) => element.map((el) => el.textContent) ) expect(itemsContent).toEqual([ 'vertical', 'horizontal', 'dynamic-content', 'specified-content', 'freescroll', 'vertical rotated(v2.3.0)', 'horizontal rotated(v2.3.0)', ]) }) describe('CoreScroll/vertical', () => { beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/core/default') }) it('should render corrent DOM', async () => { const wrapper = await page.$('.scroll-wrapper') const content = await page.$('.scroll-content') expect(wrapper).toBeTruthy() expect(content).toBeTruthy() }) it('should trigger eventListener when click wrapper DOM', async () => { let mockHandler = jest.fn() page.once('dialog', async (dialog) => { mockHandler() await dialog.dismiss() }) // wait for router transition ends await page.waitFor(1000) await page.touchscreen.tap(100, 100) expect(mockHandler).toHaveBeenCalled() }) it('should scroll when dispatch touch', async () => { await page.waitFor(1000) await page.dispatchScroll({ x: 100, y: 150, xDistance: 0, yDistance: -70, gestureSourceType: 'touch', }) await page.waitFor(1000) const transformText = await page.$eval('.scroll-content', (node) => { return window.getComputedStyle(node).transform }) const y = getTranslate(transformText, 'y') expect(y).toBeLessThan(0) }) it('should dispatch scroll event', async () => { let mockHandler = jest.fn() page.once('console', async (message) => { mockHandler() }) await page.waitFor(1000) await page.dispatchScroll({ x: 100, y: 150, xDistance: 0, yDistance: -70, gestureSourceType: 'touch', }) await page.waitFor(1000) expect(mockHandler).toBeCalled() }) }) describe('CoreScroll/horizontal', () => { beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/core/horizontal') }) it('should render corrent DOM', async () => { const wrapper = await page.$('.scroll-wrapper') const container = await page.$('.horizontal-container') expect(wrapper).toBeTruthy() expect(container).toBeTruthy() }) it('should scroll to right when finger moves from right to left', async () => { await page.waitFor(1000) await page.dispatchScroll({ x: 100, y: 120, xDistance: -70, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(1000) const transformText = await page.$eval('.scroll-content', (node) => { return window.getComputedStyle(node).transform }) const x = getTranslate(transformText, 'x') expect(x).toBeLessThan(0) }) }) describe('CoreScroll/freescroll', () => { beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/core/freescroll') }) it('should scroll correctly when oblique scrolling occurred', async () => { await page.waitFor(1000) await page.dispatchScroll({ x: 100, y: 100, xDistance: -70, yDistance: -70, gestureSourceType: 'touch', }) await page.waitFor(1000) const transformText = await page.$eval('.scroll-content', (node) => { return window.getComputedStyle(node).transform }) const y = getTranslate(transformText, 'y') const x = getTranslate(transformText, 'x') expect(x).toBeLessThan(0) expect(y).toBeLessThan(0) }) }) describe('CoreScroll/dynamicContent', () => { beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/core/dynamic-content') }) it('should support switching content dynamically', async () => { await page.waitFor(1000) await page.dispatchScroll({ x: 100, y: 100, xDistance: 0, yDistance: -70, gestureSourceType: 'touch', }) await page.waitFor(50) await page.click('.btn') await page.waitFor(100) const itemsCounts = await page.$$eval( '.scroll-content .scroll-item', (element) => element.length ) expect(itemsCounts).toBe(60) }) }) describe('CoreScroll/verticalRotated', () => { beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/core/vertical-rotated') }) it('should support vertical rotated scroll', async () => { await page.waitFor(1000) await page.dispatchScroll({ x: 210, y: 180, xDistance: 70, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(100) const transformText = await page.$eval('.scroll-content', (node) => { return window.getComputedStyle(node).transform }) const y = getTranslate(transformText, 'y') expect(y).toBeLessThan(-20) }) }) describe('CoreScroll/horizontalRotated', () => { beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/core/horizontal-rotated') }) it('should support horizontal rotated scroll', async () => { await page.waitFor(1000) await page.dispatchScroll({ x: 180, y: 100, xDistance: 70, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(100) const transformText = await page.$eval('.scroll-content', (node) => { return window.getComputedStyle(node).transform }) const x = getTranslate(transformText, 'x') expect(x).toBeLessThan(-20) }) }) }) ================================================ FILE: tests/e2e/form/textarea.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' jest.setTimeout(10000000) describe('BetterScroll in Form-Textarea', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/form/textarea') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded' }) }) it('should scroll when not manipulating texatea tag', async () => { await page.waitFor(1000) await page.dispatchScroll({ x: 100, y: 150, xDistance: 0, yDistance: -70, gestureSourceType: 'touch' }) const content = await page.$('.textarea-scroller') await page.waitFor(1000) const boundingBox = await content!.boundingBox() await expect(boundingBox!.y).toBeLessThan(0) }) it('should not scroll when manipulating texatea tag', async () => { await page.reload() await page.waitFor(1000) await page.dispatchScroll({ x: 200, y: 570, xDistance: 0, yDistance: -70, gestureSourceType: 'touch' }) const content = await page.$('.textarea-scroller') await page.waitFor(1000) const boundingBox = await content!.boundingBox() await expect(boundingBox!.y).toBeGreaterThan(0) }) }) ================================================ FILE: tests/e2e/homepage.e2e.ts ================================================ import { Page } from 'puppeteer' jest.setTimeout(10000000) describe('Homepage', () => { let page = (global as any).page as Page beforeAll(async () => { await page.goto('http://0.0.0.0:8932/') }) it('should display "BetterScroll" text on homepage', async () => { await expect(page).toMatch('BetterScroll') }) it('should display seven items at least', async () => { const itemsCounts = await page.$$eval( '.example-item', element => element.length ) await expect(itemsCounts).toBeGreaterThanOrEqual(7) }) }) ================================================ FILE: tests/e2e/indicators/minimap.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Indicators-minimap', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/indicators/minimap') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded' }) }) it('should render correctly', async () => { await page.waitFor(300) const transformText = await page.$eval('.scroll-content', node => { return window.getComputedStyle(node).transform }) const transformBScrollY = getTranslate(transformText, 'y') const indicatorTransformText = await page.$eval( '.scroll-indicator-handle', node => { return window.getComputedStyle(node).transform } ) const indicatorTransformY = getTranslate(indicatorTransformText, 'y') expect(transformBScrollY).toBe(-50) expect(indicatorTransformY).toBe(8) }) it('should trigger BS to move when manipulating indicator', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 70, y: 240, xDistance: 70, yDistance: 70, gestureSourceType: 'touch' }) const transformText = await page.$eval('.scroll-content', node => { return window.getComputedStyle(node).transform }) const transformBScrollY = getTranslate(transformText, 'y') const indicatorTransformText = await page.$eval( '.scroll-indicator-handle', node => { return window.getComputedStyle(node).transform } ) const indicatorTransformY = getTranslate(indicatorTransformText, 'y') expect(transformBScrollY).toBeLessThan(-50) expect(indicatorTransformY).toBeGreaterThan(8) }) it('should make scrollbar scroll in when manipulating BS', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 100, y: 100, xDistance: -50, yDistance: -50, gestureSourceType: 'touch' }) const transformText = await page.$eval('.scroll-content', node => { return window.getComputedStyle(node).transform }) const transformBScrollY = getTranslate(transformText, 'y') const indicatorTransformText = await page.$eval( '.scroll-indicator-handle', node => { return window.getComputedStyle(node).transform } ) const indicatorTransformY = getTranslate(indicatorTransformText, 'y') expect(transformBScrollY).toBeLessThan(-50) expect(indicatorTransformY).toBeGreaterThan(8) }) }) ================================================ FILE: tests/e2e/indicators/parallax-scrolling.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Indicators-parallax-scroll', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/indicators/parallax-scroll') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded' }) }) it('should trigger BS to move when manipulating indicator', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 70, y: 240, xDistance: -70, yDistance: -70, gestureSourceType: 'touch' }) await page.waitFor(800) const transformText = await page.$eval('.scroll-content', node => { return window.getComputedStyle(node).transform }) const transformBScrollY = getTranslate(transformText, 'y') const indicator1TransformText = await page.$eval('.star1-bg', node => { return window.getComputedStyle(node).transform }) const indicator2TransformText = await page.$eval('.star2-bg', node => { return window.getComputedStyle(node).transform }) const indicator1TransformY = getTranslate(indicator1TransformText, 'y') const indicator2TransformY = getTranslate(indicator2TransformText, 'y') expect(transformBScrollY).toBeLessThan(0) expect(indicator1TransformY).toBeLessThan(0) expect(indicator2TransformY).toBeLessThan(0) }) }) ================================================ FILE: tests/e2e/infinity/infinity.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' jest.setTimeout(10000) describe('Infinity', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/infinity/') }) beforeEach(async () => { await page.reload() }) it('should not render all elements when fetch data too mouch', async () => { await page.waitForSelector( '.infinity-timeline .infinity-item:not(.tombstone)' ) // when await page.dispatchScroll({ x: 100, y: 555, xDistance: 0, yDistance: -555, gestureSourceType: 'touch' }) await page.waitFor(1001) // wait fetch data // then const itemNum = await page.$$eval( '.infinity-timeline .infinity-item:not(.tombstone)', items => items.length ) expect(itemNum).toBeLessThan(60) }) it('should render tombstones when data not loaded', async () => { await page.waitForSelector('.infinity-timeline .infinity-item') // when await page.dispatchScroll({ x: 100, y: 555, xDistance: 0, yDistance: -555, gestureSourceType: 'touch' }) // then const itemNum = await page.$$eval( '.infinity-timeline .tombstone', items => items.length ) expect(itemNum).toBeGreaterThan(30) }) }) ================================================ FILE: tests/e2e/mousewheel/mousewheel.e2e.ts ================================================ import { Page } from 'puppeteer' import extendMouseWheel from '../../util/extendMouseWheel' import getTranslate from '../../util/getTranslate' jest.setTimeout(1000000) describe('MouseWheel plugin', () => { let page = (global as any).page as Page extendMouseWheel(page) beforeAll(async () => { // emulate pc scene await page.emulate({ viewport: { isMobile: false, width: 375, height: 667, }, // tslint:disable-next-line: max-line-length userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.80 Safari/537.36', }) }) describe('MouseWheel & Core', () => { it('should scroll correctly in vertical direction when integrating with CoreScroll', async () => { await page.goto('http://0.0.0.0:8932/#/mouse-wheel/vertical-scroll') await page.waitFor(300) await page.dispatchMouseWheel({ type: 'mouseWheel', x: 100, y: 100, deltaX: 0, deltaY: 50, }) await page.waitFor(1000) const transformText = await page.$eval('.mouse-wheel-content', (node) => { return window.getComputedStyle(node).transform }) const y = getTranslate(transformText, 'y') await expect(y).toBeLessThan(0) }) it('should scroll correctly in horizontal direction when integrating with CoreScroll', async () => { await page.goto('http://0.0.0.0:8932/#/mouse-wheel/horizontal-scroll') await page.waitFor(300) await page.dispatchMouseWheel({ type: 'mouseWheel', x: 130, y: 130, deltaX: 0, deltaY: 100, }) await page.waitFor(1000) const transformText = await page.$eval('.mouse-wheel-content', (node) => { return window.getComputedStyle(node).transform }) const x = getTranslate(transformText, 'x') await expect(x).toBeLessThan(0) }) }) describe('MouseWheel & Slide', () => { it('should scroll correctly in vertical direction when integrating with Slide', async () => { await page.goto('http://0.0.0.0:8932/#/mouse-wheel/vertical-slide') await page.waitFor(300) await page.dispatchMouseWheel({ type: 'mouseWheel', x: 100, y: 100, deltaX: 0, deltaY: 200, }) await page.waitFor(3000) const transformText = await page.$eval('.slide-content', (node) => { return window.getComputedStyle(node).transform }) const y = getTranslate(transformText, 'y') await expect(y).toBeLessThan(-667) }) it('should scroll correctly in horizontal direction when integrating with Slide', async () => { await page.goto('http://0.0.0.0:8932/#/mouse-wheel/horizontal-slide') await page.waitFor(300) await page.dispatchMouseWheel({ type: 'mouseWheel', x: 130, y: 130, deltaX: 0, deltaY: 200, }) await page.waitFor(1000) const transformText = await page.$eval('.slide-content', (node) => { return window.getComputedStyle(node).transform }) const x = getTranslate(transformText, 'x') await expect(x).toBeLessThan(-600) }) }) describe('MouseWheel & PullUp', () => { it('should scroll correctly when integrating with PullUp', async () => { await page.goto('http://0.0.0.0:8932/#/mouse-wheel/pullup') await page.waitFor(300) await page.dispatchMouseWheel({ type: 'mouseWheel', x: 100, y: 100, deltaX: 0, deltaY: 10000, }) await page.waitFor(2000) const transformText = await page.$eval('.pullup-content', (node) => { return window.getComputedStyle(node).transform }) const y = getTranslate(transformText, 'y') expect(y).toBeLessThan(0) // wait for loading data await page.waitFor(2000) const itemsCounts = await page.$$eval( '.pullup-list-item', (element) => element.length ) await expect(itemsCounts).toBeGreaterThanOrEqual(30) }) }) describe('MouseWheel & PullDown', () => { it('should scroll correctly when integrating with PullDown', async () => { await page.goto('http://0.0.0.0:8932/#/mouse-wheel/pulldown') await page.waitFor(300) await page.dispatchMouseWheel({ type: 'mouseWheel', x: 100, y: 100, deltaX: 0, deltaY: -1000, }) // wait for loading data await page.waitFor(2000) const itemsCounts = await page.$$eval( '.pulldown-list-item', (element) => element.length ) await expect(itemsCounts).toBeGreaterThanOrEqual(40) }) }) describe('MouseWheel & Wheel', () => { it('should scroll correctly when integrating with Wheel', async () => { await page.goto('http://0.0.0.0:8932/#/mouse-wheel/picker') await page.waitFor(300) await page.click('.open') await page.waitFor(500) await page.dispatchMouseWheel({ type: 'mouseWheel', x: 200, y: 630, deltaX: 0, deltaY: 100, }) await page.waitFor(1000) const transformText = await page.$eval('.wheel-scroll', (node) => { return window.getComputedStyle(node).transform }) const y = getTranslate(transformText, 'y') expect(y).toBeLessThan(-30) }) }) }) ================================================ FILE: tests/e2e/movable/default.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Movable Plugin', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/movable/default') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded', }) }) it('should work well when specify "startX & startY"', async () => { await page.waitFor(300) const scaledElTransformText = await page.$eval( '.scroll-content', (node) => { return window.getComputedStyle(node).transform } ) const x = getTranslate(scaledElTransformText, 'x') const y = getTranslate(scaledElTransformText, 'y') expect(x).toBe(20) expect(y).toBe(20) }) it('should work well when dispatchScroll', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 100, y: 100, xDistance: -70, yDistance: -70, gestureSourceType: 'touch', }) await page.waitFor(2000) const scaledElTransformText = await page.$eval( '.scroll-content', (node) => { return window.getComputedStyle(node).transform } ) const x = getTranslate(scaledElTransformText, 'x') const y = getTranslate(scaledElTransformText, 'y') expect(x).toBe(0) expect(y).toBe(0) }) }) ================================================ FILE: tests/e2e/movable/multi-content-scale.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' import getScale from '../../util/getScale' jest.setTimeout(10000000) describe('Movable & Zoom with multi content', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/movable/multi-content-scale') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded' }) }) it('should work well', async () => { await page.waitFor(300) const transformText1 = await page.$eval('.content1', node => { return window.getComputedStyle(node).transform }) const x1 = getTranslate(transformText1, 'x') const y1 = getTranslate(transformText1, 'y') const scale1 = getScale(transformText1) expect(x1).toBe(47.5) expect(y1).toBe(89) expect(scale1).toBe(1.2) const transformText2 = await page.$eval('.content2', node => { return window.getComputedStyle(node).transform }) const x2 = getTranslate(transformText2, 'x') const y2 = getTranslate(transformText2, 'y') expect(x2).toBe(0) expect(y2).toBe(150) }) it('should work well when call putAt()', async () => { await page.waitFor(300) await page.click('.btn') await page.waitFor(1000) const transformText2 = await page.$eval('.content2', node => { return window.getComputedStyle(node).transform }) const x2 = getTranslate(transformText2, 'x') const y2 = getTranslate(transformText2, 'y') expect(x2).toBe(135) expect(y2).toBe(215) }) it('should work well when perform a zoom-out gesture', async () => { await page.waitFor(300) // zoom out await page.dispatchPinch({ x: 265, y: 165, scaleFactor: 1.5, gestureSourceType: 'touch' }) const transformText1 = await page.$eval('.content1', node => { return window.getComputedStyle(node).transform }) const scale1 = getScale(transformText1) expect(scale1).toBeGreaterThan(1.2) }) }) ================================================ FILE: tests/e2e/movable/multi-content.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Movable with multi content', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/movable/multi-content') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded' }) }) it('should work well', async () => { await page.waitFor(300) const transformText1 = await page.$eval('.content1', node => { return window.getComputedStyle(node).transform }) const x1 = getTranslate(transformText1, 'x') const y1 = getTranslate(transformText1, 'y') expect(x1).toBe(10) expect(y1).toBe(10) const transformText2 = await page.$eval('.content2', node => { return window.getComputedStyle(node).transform }) const x2 = getTranslate(transformText2, 'x') const y2 = getTranslate(transformText2, 'y') expect(x2).toBe(0) expect(y2).toBe(170) }) it('should work well when call putAt()', async () => { await page.waitFor(300) await page.click('.btn') await page.waitFor(1000) const transformText2 = await page.$eval('.content2', node => { return window.getComputedStyle(node).transform }) const x2 = getTranslate(transformText2, 'x') const y2 = getTranslate(transformText2, 'y') expect(x2).toBe(67.5) expect(y2).toBe(107.5) }) }) ================================================ FILE: tests/e2e/movable/scaled.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' import getScale from '../../util/getScale' jest.setTimeout(10000000) describe('Movable & Zoom Plugin', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/movable/scale') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded' }) }) it('should support zoom in', async () => { await page.waitFor(1000) await page.waitFor(1000) // zoom in await page.dispatchPinch({ x: 180, y: 180, scaleFactor: 0.9, gestureSourceType: 'touch' }) const scaledElTransformText = await page.$eval('.scroll-content', node => { return window.getComputedStyle(node).transform }) const x = getTranslate(scaledElTransformText, 'x') const y = getTranslate(scaledElTransformText, 'x') const scale = getScale(scaledElTransformText) expect(x).toBeGreaterThan(0) expect(y).toBeGreaterThan(20) expect(scale).toBeLessThan(1) }) it('should support zoom out', async () => { await page.waitFor(1000) // zoom out await page.dispatchPinch({ x: 180, y: 180, scaleFactor: 1.5, gestureSourceType: 'touch' }) await page.waitFor(1000) const scaledElTransformText = await page.$eval('.scroll-content', node => { return window.getComputedStyle(node).transform }) const x = getTranslate(scaledElTransformText, 'x') const y = getTranslate(scaledElTransformText, 'x') const scale = getScale(scaledElTransformText) expect(x).toBeLessThan(0) expect(y).toBeLessThan(0) expect(scale).toBeGreaterThan(1) }) // it('should work well when dispatchScroll', async () => { // await page.waitFor(300) // await page.dispatchScroll({ // x: 100, // y: 100, // xDistance: -70, // yDistance: -70, // gestureSourceType: 'touch' // }) // await page.waitFor(2000) // const scaledElTransformText = await page.$eval('.scroll-content', node => { // return window.getComputedStyle(node).transform // }) // const x = getTranslate(scaledElTransformText, 'x') // const y = getTranslate(scaledElTransformText, 'x') // expect(x).toBe(0) // expect(y).toBe(0) // }) }) ================================================ FILE: tests/e2e/nested-scroll/horizontal-in-vertical.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Nested horizontal-in-vertical scroll', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto( 'http://0.0.0.0:8932/#/nested-scroll/horizontal-in-vertical' ) }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded', }) }) it('should make outer BScroll scroll when manipulating outerBScroll', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 100, y: 60, xDistance: 0, yDistance: -70, gestureSourceType: 'touch', }) await page.waitFor(1000) const transformText = await page.$eval('.vertical-content', (node) => { return window.getComputedStyle(node).transform }) const translateX = getTranslate(transformText!, 'y') await expect(translateX).toBeLessThan(-30) }) it('should only make innerBScroll scroll when manipulating innerBScroll', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 100, y: 200, xDistance: -300, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(1000) const outerTransformText = await page.$eval('.vertical-content', (node) => { return window.getComputedStyle(node).transform }) const outerTranslateX = getTranslate(outerTransformText!, 'x') await expect(outerTranslateX).toBe(0) const innerTransformText = await page.$eval( '.slide-banner-content', (node) => { return window.getComputedStyle(node).transform } ) const innerTranslateX = getTranslate(innerTransformText!, 'x') await expect(innerTranslateX).toBeLessThan(-100) }) }) ================================================ FILE: tests/e2e/nested-scroll/horizontal.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Nested horizontal scroll', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/nested-scroll/horizontal') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded', }) }) it('should make outer BScroll scroll when manipulating outerBScroll', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 100, y: 110, xDistance: -70, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(2500) const transformText = await page.$eval('.outer-content', (node) => { return window.getComputedStyle(node).transform }) const translateX = getTranslate(transformText!, 'x') await expect(translateX).toBeLessThan(-30) }) it('should only make innerBScroll scroll', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 270, y: 110, xDistance: -70, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(2500) const outerTransformText = await page.$eval('.outer-content', (node) => { return window.getComputedStyle(node).transform }) const outerTranslateX = getTranslate(outerTransformText!, 'x') await expect(outerTranslateX).toBe(0) const innerTransformText = await page.$eval('.inner-content', (node) => { return window.getComputedStyle(node).transform }) const innerTranslateY = getTranslate(innerTransformText!, 'x') await expect(innerTranslateY).toBeLessThan(-30) }) it('should make outer BScroll scroll when innerScroll reached boundary', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 270, y: 110, xDistance: -600, yDistance: 0, speed: 1800, gestureSourceType: 'touch', }) await page.waitFor(2500) const innerTransformText = await page.$eval('.inner-content', (node) => { return window.getComputedStyle(node).transform }) const innerTranslateX = getTranslate(innerTransformText!, 'x') await expect(innerTranslateX).toBeLessThan(-50) await page.dispatchScroll({ x: 270, y: 110, xDistance: -50, yDistance: 0, gestureSourceType: 'touch', }) const outerTransformText = await page.$eval('.outer-content', (node) => { return window.getComputedStyle(node).transform }) const outerTranslateX = getTranslate(outerTransformText!, 'x') await expect(outerTranslateX).toBeLessThan(-20) }) it('should support click handle when use nestedScroll plugin', async () => { const mockOuterHandler = jest.fn() const mockInnerHandler = jest.fn() page.once('dialog', async (dialog) => { mockOuterHandler() await dialog.dismiss() }) // outer click await page.touchscreen.tap(150, 110) expect(mockOuterHandler).toBeCalledTimes(1) await page.waitFor(500) page.once('dialog', async (dialog) => { mockInnerHandler() await dialog.dismiss() }) // inner click await page.touchscreen.tap(300, 110) expect(mockInnerHandler).toBeCalledTimes(1) }) }) ================================================ FILE: tests/e2e/nested-scroll/triple-vertical.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Nested triple-vertical scroll', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/nested-scroll/triple-vertical') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded', }) }) it('should make outer BScroll scroll and others keep unmoved when manipulating outerBScroll', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 160, y: 150, xDistance: 0, yDistance: -70, gestureSourceType: 'touch', }) await page.waitFor(2500) const outerTransformText = await page.$eval('.outer-content', (node) => { return window.getComputedStyle(node).transform }) const middleTransformText = await page.$eval('.middle-content', (node) => { return window.getComputedStyle(node).transform }) const innerTransformText = await page.$eval('.inner-content', (node) => { return window.getComputedStyle(node).transform }) const outerTranslateY = getTranslate(outerTransformText!, 'y') const middleTranslateY = getTranslate(middleTransformText!, 'y') const innerTranslateY = getTranslate(innerTransformText!, 'y') expect(outerTranslateY).toBeLessThan(-30) expect(middleTranslateY).toBeNaN() expect(innerTranslateY).toBeNaN() }) it('should only make middle scroll and others keep unmoved', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 160, y: 250, xDistance: 0, yDistance: -70, gestureSourceType: 'touch', }) await page.waitFor(1000) const outerTransformText = await page.$eval('.outer-content', (node) => { return window.getComputedStyle(node).transform }) const outerTranslateY = getTranslate(outerTransformText!, 'y') const middleTransformText = await page.$eval('.middle-content', (node) => { return window.getComputedStyle(node).transform }) const middleTranslateY = getTranslate(middleTransformText!, 'y') const innerTransformText = await page.$eval('.inner-content', (node) => { return window.getComputedStyle(node).transform }) const innerTranslateY = getTranslate(innerTransformText!, 'y') expect(innerTranslateY).toBeNaN() expect(middleTranslateY).toBeLessThan(-30) expect(outerTranslateY).toBe(0) }) it('should only make inner scroll and others keep unmoved', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 160, y: 450, xDistance: 0, yDistance: -70, gestureSourceType: 'touch', }) await page.waitFor(1000) const outerTransformText = await page.$eval('.outer-content', (node) => { return window.getComputedStyle(node).transform }) const outerTranslateY = getTranslate(outerTransformText!, 'y') const middleTransformText = await page.$eval('.middle-content', (node) => { return window.getComputedStyle(node).transform }) const middleTranslateY = getTranslate(middleTransformText!, 'y') const innerTransformText = await page.$eval('.inner-content', (node) => { return window.getComputedStyle(node).transform }) const innerTranslateY = getTranslate(innerTransformText!, 'y') expect(innerTranslateY).toBeLessThan(-30) expect(middleTranslateY).toBe(0) expect(outerTranslateY).toBe(0) }) it('should make parent BScroll scroll when innerScroll reached boundary', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 160, y: 450, xDistance: 0, yDistance: 50, speed: 3000, gestureSourceType: 'touch', }) const outerTransformText = await page.$eval('.outer-content', (node) => { return window.getComputedStyle(node).transform }) const outerTranslateY = getTranslate(outerTransformText!, 'y') const middleTransformText = await page.$eval('.middle-content', (node) => { return window.getComputedStyle(node).transform }) const middleTranslateY = getTranslate(middleTransformText!, 'y') const innerTransformText = await page.$eval('.inner-content', (node) => { return window.getComputedStyle(node).transform }) const innerTranslateY = getTranslate(innerTransformText!, 'y') expect(outerTranslateY).toBeGreaterThan(0) expect(middleTranslateY).toBeNaN() expect(innerTranslateY).toBeNaN() }) it('click', async () => { const mockOuterHandler = jest.fn() const mockMiddleHandler = jest.fn() const mockInnerHandler = jest.fn() // outer click page.once('dialog', async (dialog) => { mockOuterHandler() await dialog.dismiss() }) await page.touchscreen.tap(150, 100) expect(mockOuterHandler).toBeCalledTimes(1) await page.waitFor(500) // middle click page.once('dialog', async (dialog) => { mockMiddleHandler() await dialog.dismiss() }) await page.touchscreen.tap(150, 250) expect(mockMiddleHandler).toBeCalledTimes(1) await page.waitFor(500) // inner click page.once('dialog', async (dialog) => { mockInnerHandler() await dialog.dismiss() }) await page.touchscreen.tap(150, 400) expect(mockInnerHandler).toBeCalledTimes(1) }) }) ================================================ FILE: tests/e2e/nested-scroll/vertical.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Nested vertical scroll', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/nested-scroll/vertical') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded', }) }) it('should make outer BScroll scroll when manipulating outerBScroll', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 160, y: 150, xDistance: 0, yDistance: -70, gestureSourceType: 'touch', }) await page.waitFor(2500) const transformText = await page.$eval('.outer-content', (node) => { return window.getComputedStyle(node).transform }) const translateY = getTranslate(transformText!, 'y') await expect(translateY).toBeLessThan(-30) }) it('should only make innerBScroll scroll and outerBScroll stop', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 160, y: 300, xDistance: 0, yDistance: -70, gestureSourceType: 'touch', }) await page.waitFor(1000) const outerTransformText = await page.$eval('.outer-content', (node) => { return window.getComputedStyle(node).transform }) const outerTranslateY = getTranslate(outerTransformText!, 'y') await expect(outerTranslateY).toBe(0) const innerTransformText = await page.$eval('.inner-content', (node) => { return window.getComputedStyle(node).transform }) const innerTranslateY = getTranslate(innerTransformText!, 'y') await expect(innerTranslateY).toBeLessThan(-30) }) it('should make outer BScroll scroll when innerScroll reached boundary', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 160, y: 300, xDistance: 0, yDistance: -300, speed: 3000, gestureSourceType: 'touch', }) await page.waitFor(1000) const innerTransformText = await page.$eval('.inner-content', (node) => { return window.getComputedStyle(node).transform }) const innerTranslateY = getTranslate(innerTransformText!, 'y') await expect(innerTranslateY).toBeLessThan(-20) await page.dispatchScroll({ x: 160, y: 300, xDistance: 0, yDistance: -100, gestureSourceType: 'touch', }) const outerTransformText = await page.$eval('.outer-content', (node) => { return window.getComputedStyle(node).transform }) const outerTranslateY = getTranslate(outerTransformText!, 'y') await expect(outerTranslateY).toBeLessThan(-50) }) it('should support click handle when use nestedScroll plugin', async () => { const mockOuterHandler = jest.fn() const mockInnerHandler = jest.fn() page.once('dialog', async (dialog) => { mockOuterHandler() await dialog.dismiss() }) // outer click await page.touchscreen.tap(300, 100) expect(mockOuterHandler).toBeCalledTimes(1) await page.waitFor(500) page.once('dialog', async (dialog) => { mockInnerHandler() await dialog.dismiss() }) // inner click await page.touchscreen.tap(350, 500) expect(mockInnerHandler).toBeCalledTimes(1) }) }) ================================================ FILE: tests/e2e/observe-dom/observe-dom.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('ObserveDOM', () => { let page = (global as any).page as Page extendTouch(page) beforeEach(async () => { await page.goto('http://0.0.0.0:8932/#/observe-dom/') }) it('should observe DOM change and auto refresh bs', async () => { await page.waitFor(300) const preItemsCounts = await page.$$eval( '.scroll-item', (element) => element.length ) expect(preItemsCounts).toBe(10) await page.click('.btn') await page.waitFor(100) const PostItemsCounts = await page.$$eval( '.scroll-item', (element) => element.length ) expect(PostItemsCounts).toBe(12) await page.dispatchScroll({ x: 100, y: 120, xDistance: -150, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(2600) const transformText = await page.$eval('.scroll-content', (node) => { return window.getComputedStyle(node).transform }) const x = getTranslate(transformText, 'x') expect(x).toBeLessThanOrEqual(-361) }) }) ================================================ FILE: tests/e2e/observe-image/observe-image.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('ObserveImage', () => { let page = (global as any).page as Page extendTouch(page) beforeEach(async () => { await page.goto('http://0.0.0.0:8932/#/observe-image/') }) it('should autorefresh when img loaded', async () => { await page.waitFor(3000) await page.dispatchScroll({ x: 100, y: 120, xDistance: 0, yDistance: -50, gestureSourceType: 'touch', }) const transformText = await page.$eval('.scroll-content', (node) => { return window.getComputedStyle(node).transform }) const y = getTranslate(transformText, 'y') expect(y).toBeLessThan(-30) }) }) ================================================ FILE: tests/e2e/picker/double-column.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Double column picker', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/picker/double-column') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded', }) }) it('should render picker DOM correctly', async () => { await page.waitFor(300) await page.click('.open') await page.waitFor(500) const displayText = await page.$eval('.picker-panel', (node) => { return window.getComputedStyle(node).display }) await expect(displayText).toBe('block') }) it('should get correct text when click "confirm" button', async () => { await page.waitFor(300) await page.click('.open') await page.waitFor(1000) const openBtn = await page.$('.open') await page.click('.confirm') // wait for transition ends await page.waitFor(100) const innerText = await page.$eval('.open', (node) => { return node.textContent }) await expect(innerText).toBe('Venomancer-0__Durable-0') }) it('should scroll correctly when simulate touch event on each column', async () => { await page.waitFor(300) await page.click('.open') await page.waitFor(1000) // first column await page.dispatchScroll({ x: 100, y: 630, xDistance: 0, yDistance: -70, gestureSourceType: 'touch', }) // second column await page.dispatchScroll({ x: 270, y: 630, xDistance: 0, yDistance: -70, gestureSourceType: 'touch', }) // wait for transition ends await page.waitFor(1000) const transformTexts = await page.$$eval('.wheel-scroll', (nodes) => { return nodes.map((node) => window.getComputedStyle(node).transform) }) for (const transformText of transformTexts) { const translateY = getTranslate(transformText, 'y') await expect(translateY).toBeLessThan(-72) } }) }) ================================================ FILE: tests/e2e/picker/linkage-column.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' jest.setTimeout(10000000) describe('Linkage column picker', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/picker/linkage-column') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded', }) }) it('should get correct text when click "confirm" button', async () => { await page.waitFor(300) await page.click('.open') await page.waitFor(1000) const openBtn = await page.$('.open') await page.click('.confirm') // wait for transition ends await page.waitFor(100) const innerText = await page.$eval('.open', (node) => { return node.textContent }) await expect(innerText).toBe('北京市-0__北京市-0') }) it('should linkage correctly when click TianJin province', async () => { await page.waitFor(300) await page.click('.open') await page.waitFor(1000) const [firstWheelScroll] = await page.$$('.wheel-scroll') const [, TianJinProvince] = await firstWheelScroll.$$('.wheel-item') await TianJinProvince.tap() // when transition ends await page.waitFor(500) const cityBtnText = await page.$$eval('.wheel-scroll', (nodes) => { return nodes[1].querySelectorAll('.wheel-item')[0].textContent }) await expect(cityBtnText).toBe('天津市') }) it('should linkage correctly when dispatch touch event in first column', async () => { await page.waitFor(300) await page.click('.open') await page.waitFor(1000) // first column await page.dispatchScroll({ x: 100, y: 630, xDistance: 0, yDistance: -70, gestureSourceType: 'touch', }) // when transition ends await page.waitFor(1000) const cityBtnText = await page.$$eval('.wheel-scroll', (nodes) => { return nodes[1].querySelectorAll('.wheel-item')[0].textContent }) await expect(cityBtnText).not.toBe('北京市') }) }) ================================================ FILE: tests/e2e/picker/one-column.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('One column picker', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/picker/one-column') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded', }) }) it('should render picker DOM correctly', async () => { await page.waitFor(300) await page.click('.open') await page.waitFor(500) const displayText = await page.$eval('.picker-panel', (node) => { return window.getComputedStyle(node).display }) await expect(displayText).toBe('block') }) it('should wheelTo third item by default', async () => { await page.waitFor(300) await page.click('.open') await page.waitFor(500) const transformText = await page.$eval('.wheel-scroll', (node) => { return window.getComputedStyle(node).transform }) const translateY = getTranslate(transformText, 'y') await expect(translateY).toBe(-72) }) it('should not select disabled item', async () => { await page.waitFor(300) await page.click('.open') await page.waitFor(1000) await page.tap('.wheel-disabled-item') await page.waitFor(500) const transformText = await page.$eval('.wheel-scroll', (node) => { return window.getComputedStyle(node).transform }) const translateY = getTranslate(transformText, 'y') await expect(translateY).toBe(-36) }) it('should wheel to second item when click second item', async () => { await page.waitFor(300) await page.click('.open') await page.waitFor(1000) const items = await page.$$('.wheel-item') const secondItem = items[1] await secondItem.tap() // wait for transition ends await page.waitFor(1000) const transformText = await page.$eval('.wheel-scroll', (node) => { return window.getComputedStyle(node).transform }) const translateY = getTranslate(transformText, 'y') await expect(translateY).toBe(-36) }) it('should scroll correctly when simulate touch event', async () => { await page.waitFor(300) await page.click('.open') await page.waitFor(1000) await page.dispatchScroll({ x: 200, y: 630, xDistance: 0, yDistance: -70, gestureSourceType: 'touch', }) // wait for transition ends await page.waitFor(1000) const transformText = await page.$eval('.wheel-scroll', (node) => { return window.getComputedStyle(node).transform }) const translateY = getTranslate(transformText, 'y') await expect(translateY).toBeLessThan(-72) }) it('should restore position when bs is scrolling and invoke restorePosition()', async () => { await page.waitFor(300) await page.click('.open') await page.waitFor(1000) await page.dispatchScroll({ x: 200, y: 630, xDistance: 0, yDistance: -100, gestureSourceType: 'touch', }) await page.click('.cancel') await page.waitFor(500) await page.click('.open') await page.waitFor(500) const transformText = await page.$eval('.wheel-scroll', (node) => { return window.getComputedStyle(node).transform }) const translateY = getTranslate(transformText, 'y') expect(translateY).toBe(-72) }) it('should stop at the nearest wheel item when bs is scrolling and invoke stop()', async () => { await page.waitFor(300) await page.click('.open') await page.waitFor(1000) await page.dispatchScroll({ x: 200, y: 630, xDistance: 0, yDistance: -100, gestureSourceType: 'touch', }) await page.click('.confirm') await page.waitFor(500) await page.click('.open') await page.waitFor(500) const transformText = await page.$eval('.wheel-scroll', (node) => { return window.getComputedStyle(node).transform }) const translateY = getTranslate(transformText, 'y') expect(translateY).toBeLessThan(-180) }) }) ================================================ FILE: tests/e2e/pulldown/default.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' jest.setTimeout(10000000) describe('Pulldown', () => { let page = (global as any).page as Page extendTouch(page) beforeEach(async () => { // disable cache await page.setCacheEnabled(false) await page.goto('http://0.0.0.0:8932/#/pulldown/default') }) it('should render DOM correctly', async () => { await page.waitFor(300) const itemsCounts = await page.$$eval( '.pulldown-list-item', (element) => element.length ) await expect(itemsCounts).toBeGreaterThanOrEqual(20) }) it('should trigger pullingdown when BS reached the top', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 200, y: 100, xDistance: 0, yDistance: 400, speed: 1500, gestureSourceType: 'touch', }) // wait for requesting data await page.waitFor(3000) const itemsCounts = await page.$$eval( '.pulldown-list-item', (element) => element.length ) // has loaded await expect(itemsCounts).toBeGreaterThanOrEqual(40) }) }) ================================================ FILE: tests/e2e/pulldown/sina.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' jest.setTimeout(10000000) const chunk = (array: T[], size: number) => { let index = 0 let resIndex = 0 const length = array.length const result = new Array(Math.ceil(length / size)) while (index < length) { result[resIndex++] = array.slice(index, (index += size)) } return result } describe('Pulldown-sina-weibo', () => { let page = (global as any).page as Page extendTouch(page) beforeEach(async () => { // disable cache await page.setCacheEnabled(false) await page.goto('http://0.0.0.0:8932/#/pulldown/sina') }) it('should render DOM correctly', async () => { await page.waitFor(300) const itemsCounts = await page.$$eval( '.pulldown-list-item', (element) => element.length ) await expect(itemsCounts).toBeGreaterThanOrEqual(20) }) it('should go through correct phase', async () => { await page.waitFor(300) await page.dispatchTouch({ type: 'touchStart', touchPoints: [ { x: 200, y: 40, }, ], }) const touchMovePoints = (() => { const start = 70 const step = 5 const end = 550 const x = 200 let ret: Array<{ x: number; y: number }> = [] for (let i = start; i <= end; i += step) { ret.push({ x, y: i, }) } // chrome only allow 16 items in touchPoints array return chunk(ret, 16) })() // touchmove for (const touchPoint of touchMovePoints) { await page.dispatchTouch({ type: 'touchMove', touchPoints: touchPoint, }) } const textContent = await page.$$eval( '.pulldown-wrapper', (element) => element[0].textContent ) expect(textContent).toContain('Release') await page.dispatchTouch({ type: 'touchEnd', touchPoints: [ { x: 200, y: 560, }, ], }) await page.waitFor(300) // loading const textContent2 = await page.$$eval( '.pulldown-wrapper', (element) => element[0].textContent ) expect(textContent2).toContain('Loading...') await page.waitFor(3000) // refresh succeed const textContent3 = await page.$$eval( '.pulldown-wrapper', (element) => element[0].textContent ) expect(textContent3).toContain('Refresh succeed') const itemsCounts = await page.$$eval( '.pulldown-list-item', (element) => element.length ) // has loaded expect(itemsCounts).toBeGreaterThanOrEqual(40) }) }) ================================================ FILE: tests/e2e/pullup/default.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' jest.setTimeout(10000000) describe('Pullup', () => { let page = (global as any).page as Page extendTouch(page) beforeEach(async () => { // disable cache await page.setCacheEnabled(false) await page.goto('http://0.0.0.0:8932/#/pullup/') }) it('should render DOM correctly', async () => { await page.waitFor(300) const itemsCounts = await page.$$eval( '.pullup-list-item', element => element.length ) await expect(itemsCounts).toBeGreaterThanOrEqual(30) }) it('should trigger pullingup when BS reached the bottom', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 200, y: 630, xDistance: 0, yDistance: -1000, speed: 1500, gestureSourceType: 'touch' }) // wait for requesting data await page.waitFor(3000) const itemsCounts = await page.$$eval( '.pullup-list-item', element => element.length ) // has loaded await expect(itemsCounts).toBeGreaterThan(40) }) }) ================================================ FILE: tests/e2e/scrollbar/custom.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Scrollbar-custom', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/scrollbar/custom') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded', }) }) it('should trigger BS to move when manipulating scrollbar', async () => { await page.waitFor(300) // horizontal await page.dispatchScroll({ x: 123, y: 288, xDistance: 70, yDistance: 0, gestureSourceType: 'touch', }) const transformXText = await page.$eval( '.custom-scrollbar-content', (node) => { return window.getComputedStyle(node).transform } ) const x = getTranslate(transformXText, 'x') expect(x).toBeLessThan(0) // vertical await page.dispatchScroll({ x: 288, y: 123, xDistance: 0, yDistance: 70, gestureSourceType: 'touch', }) const transformYText = await page.$eval( '.custom-scrollbar-content', (node) => { return window.getComputedStyle(node).transform } ) const y = getTranslate(transformYText, 'y') expect(y).toBeLessThan(0) }) it('should trigger make scrollbar scroll in when manipulating BS', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 100, y: 100, xDistance: -100, yDistance: 0, gestureSourceType: 'touch', }) const transformXText = await page.$eval( '.custom-horizontal-indicator', (node) => { return window.getComputedStyle(node).transform } ) const x = getTranslate(transformXText, 'x') expect(x).toBeGreaterThan(0) await page.dispatchScroll({ x: 100, y: 100, xDistance: 100, yDistance: -100, gestureSourceType: 'touch', }) const transformYText = await page.$eval( '.custom-vertical-indicator', (node) => { return window.getComputedStyle(node).transform } ) const y = getTranslate(transformYText, 'y') expect(y).toBeGreaterThan(0) }) }) ================================================ FILE: tests/e2e/scrollbar/horizontal.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Scrollbar-horizontal', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/scrollbar/horizontal') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded', }) }) it('should render DOM correctly', async () => { await page.waitFor(300) const itemsCounts = await page.$$eval( '.bscroll-horizontal-scrollbar', (element) => element.length ) await expect(itemsCounts).toBeGreaterThanOrEqual(1) }) it('should trigger BS to move when manipulating scrollbar', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 70, y: 195, xDistance: 70, yDistance: 0, gestureSourceType: 'touch', }) const transformText = await page.$eval('.scroll-content', (node) => { return window.getComputedStyle(node).transform }) const x = getTranslate(transformText, 'x') expect(x).toBeLessThan(0) }) it('should make scrollbar fade in when dispatch touch event in BS', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 100, y: 100, xDistance: -100, yDistance: 0, gestureSourceType: 'touch', }) const opacity = await page.$eval( '.bscroll-horizontal-scrollbar', (node) => { return window.getComputedStyle(node).opacity } ) expect(Number(opacity)).toBeGreaterThan(0) }) it('should make scrollbar scroll in when manipulating BS', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 100, y: 100, xDistance: -100, yDistance: 0, gestureSourceType: 'touch', }) const transformText = await page.$eval('.bscroll-indicator', (node) => { return window.getComputedStyle(node).transform }) const x = getTranslate(transformText, 'x') expect(x).toBeGreaterThan(0) }) }) ================================================ FILE: tests/e2e/scrollbar/mousewheel.e2e.ts ================================================ import { Page } from 'puppeteer' import extendMouseWheel from '../../util/extendMouseWheel' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Scrollbar-mousewheel', () => { let page = (global as any).page as Page extendMouseWheel(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/scrollbar/mousewheel') }) it('should trigger BS to move when using mousewheel', async () => { await page.waitFor(2000) await page.dispatchMouseWheel({ type: 'mouseWheel', x: 100, y: 100, deltaX: 0, deltaY: 100, }) await page.waitFor(1000) const transformText = await page.$eval( '.custom-horizontal-indicator', (node) => { return window.getComputedStyle(node).transform } ) const x = getTranslate(transformText, 'x') expect(x).toBeGreaterThan(0) }) }) ================================================ FILE: tests/e2e/scrollbar/vertical.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Scrollbar-vertical', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/scrollbar/vertical') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded', }) }) it('should render DOM correctly', async () => { await page.waitFor(300) const itemsCounts = await page.$$eval( '.bscroll-vertical-scrollbar', (element) => element.length ) await expect(itemsCounts).toBeGreaterThanOrEqual(1) }) it('should trigger BS to move when manipulating scrollbar', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 325, y: 560, xDistance: 0, yDistance: -70, gestureSourceType: 'touch', }) const transformText = await page.$eval('.scrollbar-content', (node) => { return window.getComputedStyle(node).transform }) const y = getTranslate(transformText, 'y') expect(y).toBeLessThan(0) }) it('should make scrollbar fade in when dispatch touch event in BS', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 100, y: 100, xDistance: 0, yDistance: -100, gestureSourceType: 'touch', }) const opacity = await page.$eval('.bscroll-vertical-scrollbar', (node) => { return window.getComputedStyle(node).opacity }) expect(Number(opacity)).toBeGreaterThan(0) }) it('should make scrollbar scroll in when manipulating BS', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 100, y: 100, xDistance: 0, yDistance: -100, gestureSourceType: 'touch', }) const transformText = await page.$eval('.bscroll-indicator', (node) => { return window.getComputedStyle(node).transform }) const y = getTranslate(transformText, 'y') expect(y).toBeGreaterThan(0) }) }) ================================================ FILE: tests/e2e/slide/banner.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Slider for banner', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/slide/banner') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded', }) }) it('should loop by default', async () => { await page.waitFor(300) // wait for slide autoplay await page.waitFor(4000) const transformText = await page.$eval('.slide-banner-content', (node) => { return window.getComputedStyle(node).transform }) const x = getTranslate(transformText, 'x') expect(x).toBe(-670) }) it('should go nextPage when click nextPage button', async () => { await page.waitFor(300) // simulate click await page.click('.next') // wait for bs to do a transition await page.waitFor(1500) const transformText = await page.$eval('.slide-banner-content', (node) => { return window.getComputedStyle(node).transform }) const x = getTranslate(transformText, 'x') expect(x).toBe(-670) }) it('should go prevPage when click prevPage button', async () => { await page.waitFor(300) await page.click('.next') // wairt for bs to do a transition await page.waitFor(1500) const transformText1 = await page.$eval('.slide-banner-content', (node) => { return window.getComputedStyle(node).transform }) const x1 = getTranslate(transformText1, 'x') expect(x1).toBe(-670) // simulate click await page.click('.prev') await page.waitFor(1500) const transformText2 = await page.$eval('.slide-banner-content', (node) => { return window.getComputedStyle(node).transform }) const x2 = getTranslate(transformText2, 'x') expect(x2).toBe(-335) }) it('should change index when drag slide', async () => { await page.waitFor(300) const currentIndex = await page.$eval('.dots-wrapper', (el) => { const children = el.children let index = 0 for (let i = 0; i < children.length; i++) { if (children[i].className.indexOf('active') > -1) { index = i break } } return index + 1 }) const nextDotsIndex = currentIndex === 3 ? 0 : currentIndex + 1 await page.dispatchScroll({ x: 200, y: 120, xDistance: -150, yDistance: 0, gestureSourceType: 'touch', }) const secondDots = await page.$eval( `.dots-wrapper .dot:nth-child(${nextDotsIndex})`, (el) => el.className ) expect(secondDots).toContain('active') }) }) ================================================ FILE: tests/e2e/slide/dynamic.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Slider for fullpage', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/slide/dynamic') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded', }) }) it('increase', async () => { await page.waitFor(300) await page.click('.increase') await page.waitFor(200) const slidePageLen = await page.$$eval( '.slide-page', (element) => element.length ) const slidePageTexts = await page.$$eval('.slide-page', (node) => node.map((n) => n.textContent) ) expect(slidePageLen).toBe(4) expect(slidePageTexts).toMatchObject([ 'page 2', 'page 1', 'page 2', 'page 1', ]) }) it('decrease', async () => { await page.waitFor(300) await page.click('.increase') await page.waitFor(200) await page.click('.decrease') await page.waitFor(200) const slidePageLen = await page.$$eval( '.slide-page', (element) => element.length ) const slidePageTexts = await page.$$eval('.slide-page', (node) => node.map((n) => n.textContent) ) expect(slidePageLen).toBe(1) expect(slidePageTexts).toMatchObject(['page 1']) }) it('scroll ', async () => { await page.waitFor(300) await page.click('.increase') await page.waitFor(200) await page.dispatchScroll({ x: 200, y: 150, xDistance: -150, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(500) const transformText = await page.$eval('.slide-banner-content', (node) => { return window.getComputedStyle(node).transform }) const x = getTranslate(transformText, 'x') expect(x).toBe(-670) await page.click('.increase') await page.waitFor(200) const transformText2 = await page.$eval('.slide-banner-content', (node) => { return window.getComputedStyle(node).transform }) const x2 = getTranslate(transformText2, 'x') expect(x2).toBe(-670) await page.dispatchScroll({ x: 200, y: 150, xDistance: -150, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(500) const transformText3 = await page.$eval('.slide-banner-content', (node) => { return window.getComputedStyle(node).transform }) const x3 = getTranslate(transformText3, 'x') expect(x3).toBe(-1005) await page.click('.decrease') await page.waitFor(200) const transformText4 = await page.$eval('.slide-banner-content', (node) => { return window.getComputedStyle(node).transform }) const x4 = getTranslate(transformText4, 'x') expect(x4).toBe(-670) await page.click('.decrease') await page.waitFor(200) const transformText5 = await page.$eval('.slide-banner-content', (node) => { return window.getComputedStyle(node).transform }) const x5 = getTranslate(transformText5, 'x') expect(x5).toBe(0) }) }) ================================================ FILE: tests/e2e/slide/fullpage.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Slider for fullpage', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/slide/fullpage') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded', }) }) it('should not allow move to pre page when it is first page and loop is false', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 100, y: 120, xDistance: 110, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(200) const transformText = await page.$eval('.slide-banner-content', (node) => { return window.getComputedStyle(node).transform }) const x = getTranslate(transformText, 'x') expect(x).toBe(0) }) it('should not allow move to next page when it is last page and loop is false', async () => { await page.waitFor(300) // to second page await page.dispatchScroll({ x: 100, y: 120, xDistance: -110, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(1000) // to third page await page.dispatchScroll({ x: 100, y: 120, xDistance: -110, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(1000) // to last page await page.dispatchScroll({ x: 100, y: 120, xDistance: -110, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(1000) // attempts to go next await page.dispatchScroll({ x: 100, y: 120, xDistance: -110, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(1000) const transformText = await page.$eval('.slide-banner-content', (node) => { return window.getComputedStyle(node).transform }) const x = getTranslate(transformText, 'x') expect(x).toBe(-1125) }) it('should work by dispatching touch events', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 100, y: 120, xDistance: -110, yDistance: 0, gestureSourceType: 'touch', }) await page.waitFor(1500) const transformText = await page.$eval('.slide-banner-content', (node) => { return window.getComputedStyle(node).transform }) const x = getTranslate(transformText, 'x') expect(x).toBe(-375) }) }) ================================================ FILE: tests/e2e/slide/specifiedIndex.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Slider for specified index', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/slide/specified') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded', }) }) it('should work well when initialised', async () => { await page.waitFor(300) const textContext = await page.$eval('.description', (node) => { return node.textContent }) expect(textContext).toBe('currentPageIndex is 2') }) it('should go nextPage when click nextPage button', async () => { await page.waitFor(300) // simulate click await page.click('.next') // wait for bs to do a transition await page.waitFor(1500) const textContext = await page.$eval('.description', (node) => { return node.textContent }) expect(textContext).toBe('currentPageIndex is 3') }) it('should go prevPage when click prevPage button', async () => { await page.waitFor(300) await page.click('.prev') // wait for bs to do a transition await page.waitFor(1500) const textContext = await page.$eval('.description', (node) => { return node.textContent }) expect(textContext).toBe('currentPageIndex is 1') }) it('should change index when drap slide', async () => { await page.waitFor(300) await page.dispatchScroll({ x: 200, y: 120, xDistance: -150, yDistance: 0, gestureSourceType: 'touch', }) // wait for bs to do a transition await page.waitFor(1500) const textContext = await page.$eval('.description', (node) => { return node.textContent }) expect(textContext).toBe('currentPageIndex is 3') }) }) ================================================ FILE: tests/e2e/slide/vertical.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Slider for vertical', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/slide/vertical') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded' }) }) it('should work by dispatching touch events', async () => { await page.waitFor(1500) await page.dispatchScroll({ x: 100, y: 200, xDistance: 0, yDistance: -50, gestureSourceType: 'touch' }) await page.waitFor(1000) const transformText = await page.$eval('.slide-vertical-content', node => { return window.getComputedStyle(node).transform }) const y = getTranslate(transformText, 'y') expect(y).toBe(-1334) }) }) ================================================ FILE: tests/e2e/zoom/zoom.e2e.ts ================================================ import { Page } from 'puppeteer' import extendTouch from '../../util/extendTouch' import getScale from '../../util/getScale' import getTranslate from '../../util/getTranslate' jest.setTimeout(10000000) describe('Zoom', () => { let page = (global as any).page as Page extendTouch(page) beforeAll(async () => { await page.goto('http://0.0.0.0:8932/#/zoom') }) beforeEach(async () => { await page.reload({ waitUntil: 'domcontentloaded' }) }) it('should work when initializing to scaled > 1', async () => { await page.waitFor(300) const scaledElTransformText = await page.$eval('.zoom-items', node => { return window.getComputedStyle(node).transform }) const scale = getScale(scaledElTransformText) expect(scale).toBe(1.5) }) it('should work when initialOrigin is set to "center"', async () => { await page.waitFor(300) const scaledElTransformText = await page.$eval('.zoom-items', node => { return window.getComputedStyle(node).transform }) const scale = getScale(scaledElTransformText) const x = getTranslate(scaledElTransformText, 'x') const y = getTranslate(scaledElTransformText, 'x') expect(scale).toBe(1.5) expect(x).toBeLessThan(0) expect(y).toBeLessThan(0) }) it('should work by dispatching zooming out', async () => { await page.waitFor(300) // zoom out await page.dispatchPinch({ x: 100, y: 100, scaleFactor: 1.1, gestureSourceType: 'touch' }) const scaledElTransformText = await page.$eval('.zoom-items', node => { return window.getComputedStyle(node).transform }) const scale = getScale(scaledElTransformText) expect(scale).toBeGreaterThan(1.5) }) it('should work by dispatching zooming in', async () => { await page.waitFor(300) // zoom in await page.dispatchPinch({ x: 200, y: 200, scaleFactor: 0.5, gestureSourceType: 'touch' }) const scaledElTransformText = await page.$eval('.zoom-items', node => { return window.getComputedStyle(node).transform }) const scale = getScale(scaledElTransformText) expect(scale).toBeLessThan(1.5) }) it('should do a rebound animation when scale exceed "max", and recover to "max"', async () => { await page.waitFor(300) // zoom in await page.dispatchPinch({ x: 200, y: 200, scaleFactor: 4, gestureSourceType: 'touch' }) // wait for rebound animation ends await page.waitFor(1000) const scaledElTransformText = await page.$eval('.zoom-items', node => { return window.getComputedStyle(node).transform }) const scale = getScale(scaledElTransformText) expect(scale).toBe(3) }) it('should support zoomTo api', async () => { await page.waitFor(300) // zoomTo scale(0.5) await page.click('.zoom-half') await page.waitFor(1000) const scaledElTransformTextHalf = await page.$eval('.zoom-items', node => { return window.getComputedStyle(node).transform }) let scaleHalf = getScale(scaledElTransformTextHalf) expect(scaleHalf).toBe(0.5) // zoomTo scale(1) await page.click('.zoom-original') await page.waitFor(1000) let scaledElTransformTextOriginal = await page.$eval( '.zoom-items', node => { return window.getComputedStyle(node).transform } ) const scaleOriginal = getScale(scaledElTransformTextOriginal) const xOriginal = getTranslate(scaledElTransformTextOriginal, 'x') const yOriginal = getTranslate(scaledElTransformTextOriginal, 'y') expect(scaleOriginal).toBe(1) expect(xOriginal).toBe(0) expect(yOriginal).toBe(0) // zoomTo scale(2) await page.click('.zoom-double') await page.waitFor(1000) let scaledElTransformTextDouble = await page.$eval('.zoom-items', node => { return window.getComputedStyle(node).transform }) const scaleDouble = getScale(scaledElTransformTextDouble) const xDouble = getTranslate(scaledElTransformTextDouble, 'x') const yDouble = getTranslate(scaledElTransformTextDouble, 'y') expect(scaleDouble).toBe(2) expect(xDouble).toBeLessThan(0) expect(yDouble).toBeLessThan(0) }) it('should allow moving with one finger when scaled out', async () => { await page.waitFor(300) // zoom out await page.dispatchPinch({ x: 100, y: 100, scaleFactor: 1.2, gestureSourceType: 'touch' }) // touchmove await page.dispatchScroll({ x: 100, y: 120, xDistance: 200, yDistance: 0, gestureSourceType: 'touch' }) await page.waitFor(2500) let transformText = await page.$eval('.zoom-items', node => { return window.getComputedStyle(node).transform }) const xDouble = getTranslate(transformText, 'x') expect(xDouble).toEqual(0) }) }) ================================================ FILE: tests/util/extendMouseWheel.ts ================================================ import { Page } from 'puppeteer' interface EventParams { type: string x: number y: number deltaX: number deltaY: number } const DEFAULT_CHROMIUM_MOUSE_WHEEL_NAME = 'Input.dispatchMouseEvent' declare module 'puppeteer' { interface Mouse { _client: { send: (name: string, eventParams: EventParams) => Promise } } interface Page { dispatchMouseWheel: (eventParams: EventParams) => Promise mouse: Mouse } } // puppeteer 1.17.0 has no api to implement MouseWheel // since puppeteer is connected to chromium with chromeDevTools // https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent // so we can do it by ourselves export default (page: Page) => { page.dispatchMouseWheel = async (eventParams: EventParams) => { await page.mouse._client.send( DEFAULT_CHROMIUM_MOUSE_WHEEL_NAME, eventParams ) } } ================================================ FILE: tests/util/extendTouch.ts ================================================ import { Page, Touchscreen } from 'puppeteer' // https://chromedevtools.github.io/devtools-protocol/tot/Input#method-synthesizePinchGesture interface PinchParams { x: number y: number scaleFactor: number gestureSourceType: 'touch' | 'default' | 'mouse' } // https://chromedevtools.github.io/devtools-protocol/tot/Input#method-synthesizeScrollGesture interface ScrollParams { x: number // X coordinate of the start of the gesture in CSS pixels. y: number // Y coordinate of the start of the gesture in CSS pixels. xDistance: number // positive to scroll left yDistance: number // positive to scroll up gestureSourceType: 'touch' | 'default' | 'mouse' speed?: number // Swipe speed in pixels per second xOverscroll?: number yOverscroll?: number preventFling?: boolean repeatCount?: number repeatDelayMs?: number } interface TouchPoint { x: number y: number radiusX?: number radiusY?: number rotationAngle?: number force?: number tangentialPressure?: number tiltX?: number tiltY?: number twist?: number id?: number } interface TouchesParams { type: 'touchStart' | 'touchEnd' | 'touchMove' | 'touchCancel' touchPoints: TouchPoint[] modifiers?: number // Alt=1, Ctrl=2, Meta/Command=4, Shift=8 timestamp?: number } const PINCH_NAME = 'Input.synthesizePinchGesture' const SCROLL_NAME = 'Input.synthesizeScrollGesture' const TOUCHES_NAME = 'Input.dispatchTouchEvent' declare module 'puppeteer' { interface Touchscreen { _client: { send: ( name: T, params: PinchParams | ScrollParams | TouchesParams ) => Promise } } interface Page { dispatchPinch: (pinchParams: PinchParams) => Promise dispatchScroll: (scrollParams: ScrollParams) => Promise dispatchTouch: (touchesParams: TouchesParams) => Promise touchsceen: Touchscreen } } // puppeteer 1.17.0 has no api to implement touchmove // since puppeteer is connected to chromium with chromeDevTools // https://chromedevtools.github.io/devtools-protocol/tot/Input#method-dispatchTouchEvent export default (page: Page) => { page.dispatchPinch = async (pinchParams) => { await page.touchscreen._client.send(PINCH_NAME, pinchParams) } page.dispatchScroll = async (scrollParams) => { await page.touchscreen._client.send(SCROLL_NAME, scrollParams) } page.dispatchTouch = async (touchesParams) => { await page.touchscreen._client.send(TOUCHES_NAME, touchesParams) } } ================================================ FILE: tests/util/getScale.ts ================================================ export default function getScale(transformText: string) { const matrix = transformText.split(')')[0].split(', ') const prefix = matrix[0] return +prefix.split('(')[1] } ================================================ FILE: tests/util/getTranslate.ts ================================================ export default function getTranslate( transformText: string, direction: 'x' | 'y' ) { const matrix = transformText.split(')')[0].split(', ') let ret = direction === 'x' ? +(matrix[12] || matrix[4]) : +(matrix[13] || matrix[5]) return ret } ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "baseUrl": ".", "rootDir": ".", "outDir": "dist", "moduleResolution": "node", "paths": { "@better-scroll/*": ["packages/*/src"] }, "target": "es5", "module": "es2015", "lib": ["es2015", "es2016", "es2017", "dom"], "esModuleInterop": true, "strictPropertyInitialization": false, "strict": true, "preserveSymlinks": true, "declaration": true, "allowSyntheticDefaultImports": true, "experimentalDecorators": true, "emitDecoratorMetadata": false, "typeRoots": ["node_modules/@types"] }, "include": ["packages/*/src"], "exclude": ["packages/*/src/**/__tests__", "packages/*/src/**/__mocks__"] } ================================================ FILE: tslint.json ================================================ { "extends": [ "tslint-config-standard", "tslint-config-prettier" ], "rules": { "max-line-length": { "options": [140] }, "no-empty": false, "deprecation": false, "strict-type-predicates": false, "no-angle-bracket-type-assertion": false, "no-unnecessary-type-assertion": false, "no-unused-expression": false, "no-arg": true, "no-bitwise": true, "no-conditional-assignment": true, "no-consecutive-blank-lines": false, "no-console": false, "await-promise": false } }