├── test ├── __mocks__ │ ├── styleMock.js │ └── fileMock.js ├── touchHelper.js └── simple.test.js ├── src ├── utils.js ├── SwiperContext.js ├── SwiperSlider.js ├── styles │ └── Swiper.module.scss ├── Sprite.js └── Swiper.js ├── demo ├── images │ ├── test1.jpg │ ├── test2.jpg │ ├── test3.jpg │ ├── test1_bkg.jpg │ ├── test2_bkg.jpg │ └── test3_bkg.jpg ├── history.js ├── template │ ├── SliderDefaultTemplate.js │ ├── SliderUserTemplate.js │ └── SliderSpriteTemplate.js ├── tpl │ └── index.html ├── index.css ├── index.js ├── nanaDesign.scss ├── pages │ ├── UserDefineSlider.js │ ├── FreeModeSlider.js │ ├── LoopSlider.js │ ├── SpirteSlider.js │ ├── CoverFlowSlider.js │ └── NormalSlider.js ├── App.scss ├── App.js └── serviceWorker.js ├── index.js ├── babel.config.js ├── .npmignore ├── .gitignore ├── webpackDevServer.js ├── .jest.js ├── dist ├── test.html └── nanaSwiper.umd.js ├── package.json ├── webpack.config.bundle.js ├── webpack.config.demo.js ├── webpack.config.build.demo.js └── README.md /test/__mocks__/styleMock.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; -------------------------------------------------------------------------------- /test/__mocks__/fileMock.js: -------------------------------------------------------------------------------- 1 | module.exports = 'test-file-stub'; -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | export function checkTouch(){ 2 | return "ontouchstart" in window 3 | } -------------------------------------------------------------------------------- /demo/images/test1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanaSun/nanaSwiper/HEAD/demo/images/test1.jpg -------------------------------------------------------------------------------- /demo/images/test2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanaSun/nanaSwiper/HEAD/demo/images/test2.jpg -------------------------------------------------------------------------------- /demo/images/test3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanaSun/nanaSwiper/HEAD/demo/images/test3.jpg -------------------------------------------------------------------------------- /demo/images/test1_bkg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanaSun/nanaSwiper/HEAD/demo/images/test1_bkg.jpg -------------------------------------------------------------------------------- /demo/images/test2_bkg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanaSun/nanaSwiper/HEAD/demo/images/test2_bkg.jpg -------------------------------------------------------------------------------- /demo/images/test3_bkg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nanaSun/nanaSwiper/HEAD/demo/images/test3_bkg.jpg -------------------------------------------------------------------------------- /demo/history.js: -------------------------------------------------------------------------------- 1 | import createHashHistory from "history/createHashHistory"; 2 | const history = createHashHistory() 3 | export default history -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import Sprite from "./src/Sprite" 2 | import Swiper from "./src/Swiper" 3 | import SwiperSlider from "./src/SwiperSlider" 4 | export default Swiper 5 | export {Sprite,SwiperSlider} -------------------------------------------------------------------------------- /src/SwiperContext.js: -------------------------------------------------------------------------------- 1 | import {createContext} from 'react'; 2 | 3 | const createNamedContext = name => { 4 | const context = createContext(); 5 | context.Provider.displayName = `${name}.Provider`; 6 | context.Consumer.displayName = `${name}.Consumer`; 7 | return context; 8 | } 9 | 10 | const context = createNamedContext('Swiper'); 11 | export default context; -------------------------------------------------------------------------------- /demo/template/SliderDefaultTemplate.js: -------------------------------------------------------------------------------- 1 | import React,{Fragment} from 'react' 2 | 3 | export function SliderDefaultTemplate(props){ 4 | //if(process.env.NODE_ENV==="development") console.log(props) 5 | return ( 6 | 7 |
8 | slider-{props.text} 9 |
10 |
11 | ) 12 | } -------------------------------------------------------------------------------- /demo/template/SliderUserTemplate.js: -------------------------------------------------------------------------------- 1 | import React,{Fragment} from 'react' 2 | 3 | export function SliderUserTemplate(props){ 4 | if(process.env.NODE_ENV==="development") console.log(props) 5 | return ( 6 | 7 |
8 |
9 |
10 | ) 11 | } -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const presets = [ 2 | [ 3 | "@babel/preset-env", 4 | { 5 | targets: { 6 | "browsers": ["last 10 versions"] 7 | }, 8 | useBuiltIns: "usage", 9 | } 10 | ], 11 | ["@babel/preset-react"] 12 | ]; 13 | const plugins=[ 14 | ["@babel/plugin-proposal-class-properties"] 15 | ] 16 | module.exports = { presets,plugins }; -------------------------------------------------------------------------------- /demo/template/SliderSpriteTemplate.js: -------------------------------------------------------------------------------- 1 | import React,{Fragment,Component} from 'react' 2 | import {Sprite} from '../../index' 3 | 4 | class SliderSpriteTemplate extends Component{ 5 | render(){ 6 | return ( 7 | 8 | {this.props.isActive?:""} 9 | 10 | ) 11 | } 12 | } 13 | export {SliderSpriteTemplate} -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /test/coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | .env 25 | demo 26 | lib 27 | -------------------------------------------------------------------------------- /demo/tpl/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | React App 8 | 9 | 10 | 13 |
14 | 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | /test/coverage 11 | 12 | # production 13 | /build 14 | /demo/dist 15 | # misc 16 | .env 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | 28 | -------------------------------------------------------------------------------- /demo/index.css: -------------------------------------------------------------------------------- 1 | *{ 2 | margin: 0; 3 | padding: 0; 4 | } 5 | body { 6 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 7 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 8 | sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | overflow: hidden; 12 | width: 100%; 13 | } 14 | a,a:visited,a:active{ 15 | color: inherit; 16 | text-decoration: none; 17 | } 18 | ul,li{ 19 | list-style:none; 20 | } 21 | -------------------------------------------------------------------------------- /test/touchHelper.js: -------------------------------------------------------------------------------- 1 | class Touch { 2 | emulateTouchEvent(type,point) { 3 | return new window.TouchEvent(type, { 4 | bubbles: true, 5 | cancelable: true, 6 | touches:[{ 7 | clientX:point[0], 8 | clientY:point[1] 9 | }] 10 | }) 11 | } 12 | emulateMouseEvent(type,point) { 13 | return new window.TouchEvent(type, { 14 | bubbles: true, 15 | cancelable: true, 16 | clientX:point[0], 17 | clientY:point[1] 18 | }) 19 | } 20 | } 21 | 22 | module.exports = new Touch() 23 | -------------------------------------------------------------------------------- /webpackDevServer.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const webpack = require('webpack'); 3 | const webpackDevMiddleware = require('webpack-dev-middleware'); 4 | 5 | const app = express(); 6 | const config = require('./webpack.config.demo.js'); 7 | const compiler = webpack(config); 8 | 9 | app.use(webpackDevMiddleware(compiler, { 10 | publicPath: config.output.publicPath 11 | })); 12 | 13 | app.use(require("webpack-hot-middleware")(compiler,{ 14 | log: false, 15 | path: "/__what", 16 | heartbeat: 2000 17 | })); 18 | 19 | app.listen(8080, function () { 20 | console.log('Example app listening on port 8080!\n'); 21 | }); -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import * as serviceWorker from './serviceWorker'; 6 | import { HashRouter} from 'react-router-dom' 7 | 8 | ReactDOM.render(, document.getElementById('root')); 9 | 10 | // If you want your app to work offline and load faster, you can change 11 | // unregister() to register() below. Note this comes with some pitfalls. 12 | // Learn more about service workers: http://bit.ly/CRA-PWA 13 | serviceWorker.unregister(); 14 | 15 | if(module.hot){ 16 | module.hot.accept() 17 | } 18 | -------------------------------------------------------------------------------- /demo/nanaDesign.scss: -------------------------------------------------------------------------------- 1 | .nanaDesign{ 2 | background: blue; 3 | } 4 | .nanaDesignSlider{ 5 | &.nanaDesignActive{ 6 | background:yellow; 7 | } 8 | & .defaultSlider{ 9 | background-size:auto 100%; 10 | width: 100%; 11 | height: 100%; 12 | } 13 | } 14 | .navigationUser { 15 | width: 100%; 16 | padding: 10px 0; 17 | background: rgba(0, 0, 0, 0.3); 18 | z-index: 10; 19 | .navigationUserItem{ 20 | width: 10px; 21 | height: 10px; 22 | border-radius: 10px; 23 | overflow: hidden; 24 | background: pink; 25 | flex-shrink: 0; 26 | margin:0 10px; 27 | &.navigationUserActive{ 28 | width: 20px; 29 | background: plum; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /demo/pages/UserDefineSlider.js: -------------------------------------------------------------------------------- 1 | import React,{Fragment} from 'react'; 2 | 3 | import Swiper,{SwiperSlider} from '../../index' 4 | 5 | export function UserDefineSlider(){ 6 | return ( 7 | 8 |
9 | 14 | (
15 | slider-ahahah 16 |
)}/> 17 | (
18 | slider-gasdffds 19 |
)}/> 20 | (
21 | slider-werqwerq 22 |
)}/> 23 |
24 |
25 |
26 | ) 27 | } -------------------------------------------------------------------------------- /demo/pages/FreeModeSlider.js: -------------------------------------------------------------------------------- 1 | import React,{Fragment} from 'react'; 2 | 3 | import Swiper,{SwiperSlider} from '../../index' 4 | 5 | export function FreeModeSlider(){ 6 | return ( 7 | 8 |
9 | 15 | (
16 | slider-ahahah 17 |
)}/> 18 | (
19 | slider-gasdffds 20 |
)}/> 21 | (
22 | slider-werqwerq 23 |
)}/> 24 |
25 |
26 |
27 | ) 28 | } -------------------------------------------------------------------------------- /src/SwiperSlider.js: -------------------------------------------------------------------------------- 1 | import React,{Component} from 'react' 2 | import PropTypes from "prop-types"; 3 | import SwiperContext from "./SwiperContext"; 4 | import SwiperCSS from './styles/Swiper.module.scss' 5 | class SwiperSlider extends Component { 6 | render() { 7 | return ( 8 | 9 | {context => { 10 | const {currentSliderIndex,isMoving}=context 11 | const {index,width,height}=this.props 12 | return ( 13 |
16 | {this.props.render({ 17 | ...this.props, 18 | isMoving:isMoving, 19 | isActive:currentSliderIndex===index 20 | })} 21 |
22 | ) 23 | }} 24 |
25 | ) 26 | ; 27 | } 28 | } 29 | SwiperSlider.propTypes = { 30 | render:PropTypes.func.isRequired, 31 | index: PropTypes.number 32 | }; 33 | 34 | export default SwiperSlider -------------------------------------------------------------------------------- /.jest.js: -------------------------------------------------------------------------------- 1 | const transformIgnorePatterns = [ 2 | '/dist/', 3 | 'node_modules/[^/]+?/(?!(es|node_modules)/)', // Ignore modules without es dir 4 | ]; 5 | module.exports = { 6 | verbose: true, 7 | moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'md'], 8 | testPathIgnorePatterns: ['/node_modules/', 'dist'], 9 | transform: { 10 | "^.+\\.js$": "babel-jest" 11 | }, 12 | moduleNameMapper: { 13 | "\\.(css|less|scss)$": "identity-obj-proxy", 14 | "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "./test/__mocks__/fileMock.js" 15 | }, 16 | transformIgnorePatterns, 17 | testURL: 'http://localhost', 18 | globals: { 19 | ontouchstart: null//模拟windows的touchstart 20 | }, 21 | collectCoverage :true, 22 | coverageDirectory: '/test/coverage', 23 | collectCoverageFrom:[ 24 | "**/index.{js,jsx}", 25 | "**/src/*.{js,jsx}" 26 | ], 27 | coveragePathIgnorePatterns :["node_modules","demo","dist"], 28 | coverageThreshold: { // 测试覆盖率通过阈值 29 | global: { 30 | branches: 90, 31 | functions: 90, 32 | lines: 90, 33 | statements: 90 34 | } 35 | } 36 | }; -------------------------------------------------------------------------------- /demo/pages/LoopSlider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Swiper,{SwiperSlider} from '../../index' 4 | import {SliderSpriteTemplate} from '../template/SliderSpriteTemplate' 5 | import {SliderDefaultTemplate} from '../template/SliderDefaultTemplate' 6 | import img1 from "../images/test1.jpg" 7 | import img1Bkg from "../images/test1_bkg.jpg" 8 | import img3 from "../images/test3.jpg" 9 | import img3Bkg from "../images/test3_bkg.jpg" 10 | 11 | const data=[ 12 | { 13 | width:window.innerWidth, 14 | height:300, 15 | spriteImg:img1, 16 | spriteConf:[3,1,222,350], 17 | speed:6, 18 | img:img1Bkg, 19 | tpl:SliderSpriteTemplate 20 | }, 21 | { 22 | text:"text", 23 | tpl:SliderDefaultTemplate 24 | }, 25 | { 26 | width:window.innerWidth, 27 | height:300, 28 | spriteImg:img3, 29 | spriteConf:[3,1,222,350], 30 | speed:6, 31 | img:img3Bkg, 32 | tpl:SliderSpriteTemplate 33 | } 34 | ] 35 | export function LoopSlider(){ 36 | return (
37 | 42 | {data.map((d,index)=>{ 43 | return }/> 44 | })} 45 | 46 |
) 47 | } -------------------------------------------------------------------------------- /demo/pages/SpirteSlider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Swiper,{SwiperSlider} from '../../index' 4 | import {SliderSpriteTemplate} from '../template/SliderSpriteTemplate' 5 | import {SliderDefaultTemplate} from '../template/SliderDefaultTemplate' 6 | import img1 from "../images/test1.jpg" 7 | import img1Bkg from "../images/test1_bkg.jpg" 8 | import img3 from "../images/test3.jpg" 9 | import img3Bkg from "../images/test3_bkg.jpg" 10 | 11 | const data=[ 12 | { 13 | width:window.innerWidth, 14 | height:300, 15 | spriteImg:img1, 16 | spriteConf:[3,1,222,350], 17 | speed:6, 18 | img:img1Bkg, 19 | tpl:SliderSpriteTemplate 20 | }, 21 | { 22 | text:"text", 23 | tpl:SliderDefaultTemplate 24 | }, 25 | { 26 | width:window.innerWidth, 27 | height:300, 28 | spriteImg:img3, 29 | spriteConf:[3,1,222,350], 30 | speed:6, 31 | img:img3Bkg, 32 | tpl:SliderSpriteTemplate 33 | } 34 | ] 35 | export function SpirteSlider(){ 36 | return (
37 | 42 | {data.map((d,index)=>{ 43 | return }/> 44 | })} 45 | 46 |
) 47 | } -------------------------------------------------------------------------------- /src/styles/Swiper.module.scss: -------------------------------------------------------------------------------- 1 | .SwiperWrapper{ 2 | position: relative; 3 | height: 100%; 4 | width: 100%; 5 | z-index: 1; 6 | display: flex; 7 | transition-property: transform; 8 | box-sizing: border-box; 9 | will-change: transition; 10 | touch-action: none; 11 | } 12 | 13 | .SwiperSlider{ 14 | box-sizing: border-box; 15 | /*background: #eee;*/ 16 | background-size:100% 100%; 17 | background-repeat:no-repeat; 18 | color: #333; 19 | width: 100%; 20 | height: 100%; 21 | overflow: hidden; 22 | flex-shrink:0; 23 | display: flex; 24 | justify-content: center; 25 | align-items: center; 26 | } 27 | .flatCoverFlow { 28 | margin:10px; 29 | } 30 | .flatCoverFlow .SwiperSlider{ 31 | transform: scale3d(0.9,0.9,1); 32 | will-change: transform; 33 | box-shadow: 0px 0px 10px #333; 34 | } 35 | .SwiperSlider.SwiperActive{ 36 | transform: scale3d(1,1,1); 37 | } 38 | .SwiperSliderNav { 39 | display:flex; 40 | justify-content: center; 41 | position: absolute; 42 | bottom: 0px; 43 | left:0px; 44 | width: 100%; 45 | padding: 10px 0; 46 | // background: rgba(0, 0, 0, 0.3); 47 | z-index: 10; 48 | .SwiperItemNav{ 49 | width: 10px; 50 | height: 10px; 51 | border-radius: 10px; 52 | overflow: hidden; 53 | background: gray; 54 | flex-shrink: 0; 55 | margin:0 10px; 56 | &.SwiperNavActive{ 57 | background: red; 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /demo/pages/CoverFlowSlider.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Swiper,{SwiperSlider} from '../../index' 4 | import {SliderSpriteTemplate} from '../template/SliderSpriteTemplate' 5 | import {SliderDefaultTemplate} from '../template/SliderDefaultTemplate' 6 | import img1 from "../images/test1.jpg" 7 | import img1Bkg from "../images/test1_bkg.jpg" 8 | import img3 from "../images/test3.jpg" 9 | import img3Bkg from "../images/test3_bkg.jpg" 10 | 11 | const width=window.innerWidth*.8-20 12 | const initMovex=window.innerWidth*.1 13 | 14 | const data=[ 15 | { 16 | width:width, 17 | height:300, 18 | spriteImg:img1, 19 | spriteConf:[3,1,222,350], 20 | speed:6, 21 | img:img1Bkg, 22 | tpl:SliderSpriteTemplate 23 | }, 24 | { 25 | text:"text", 26 | tpl:SliderDefaultTemplate 27 | }, 28 | { 29 | width:width, 30 | height:300, 31 | spriteImg:img3, 32 | spriteConf:[3,1,222,350], 33 | speed:6, 34 | img:img3Bkg, 35 | tpl:SliderSpriteTemplate 36 | } 37 | ] 38 | export function CoverFlowSlider(){ 39 | return (
40 | 47 | {data.map((d,index)=>{ 48 | return }/> 49 | })} 50 | 51 |
) 52 | } -------------------------------------------------------------------------------- /demo/App.scss: -------------------------------------------------------------------------------- 1 | .SwiperContainer{ 2 | width: 100%; 3 | height: 300px; 4 | overflow: hidden; 5 | position: relative; 6 | display: flex; 7 | } 8 | .menu{ 9 | width:10%; 10 | padding-top:10%; 11 | background:pink; 12 | position: fixed; 13 | right: 0px; 14 | z-index: 1000; 15 | &:before,&:after,span{ 16 | content:""; 17 | background:white; 18 | position:absolute; 19 | top:50%; 20 | left:50%; 21 | width:10%; 22 | height:60%; 23 | } 24 | span{ 25 | transform:translate(-50%,-50%) rotate(90deg); 26 | transition:opacity 1s,background 1s; 27 | } 28 | &:before{ 29 | transform:translate(-50%,-20%) rotate(-90deg); 30 | transition:transform 1s,background 1s; 31 | } 32 | &:after{ 33 | transform:translate(-50%,-80%) rotate(90deg); 34 | transition:transform 1s,background 1s; 35 | } 36 | &.close{ 37 | &:before,&:after,span{ 38 | background:red 39 | } 40 | span{ 41 | opacity:0; 42 | } 43 | &:before{ 44 | transform:translate(-50%,-50%) rotate(-135deg); 45 | } 46 | &:after{ 47 | transform:translate(-50%,-50%) rotate(-45deg); 48 | } 49 | } 50 | } 51 | .navigation{ 52 | position:fixed; 53 | width: 100%; 54 | right: -100%; 55 | z-index: 999; 56 | background: pink; 57 | transition: transform 1s ; 58 | &.show{ 59 | transform: translateX(-100%); 60 | } 61 | li{ 62 | line-height: 3; 63 | padding:0 5%; 64 | &.currentPath{ 65 | background: red; 66 | color: #fff; 67 | } 68 | } 69 | } 70 | .intro{ 71 | padding: 40px 5%; 72 | ol { 73 | padding:0 0 0 20px; 74 | } 75 | li{ 76 | list-style-type: decimal; 77 | } 78 | } 79 | 80 | .userDefaultSlider1{ 81 | width: 100%; 82 | height: 100%; 83 | background: pink; 84 | } 85 | .userDefaultSlider2{ 86 | width: 100%; 87 | height: 100%; 88 | background: gainsboro; 89 | } 90 | .userDefaultSlider3{ 91 | width: 100%; 92 | height: 100%; 93 | background: lightsteelblue; 94 | } -------------------------------------------------------------------------------- /dist/test.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Document 8 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 71 | 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nanaswiper", 3 | "version": "0.1.6", 4 | "private": false, 5 | "scripts": { 6 | "test": "jest --config .jest.js", 7 | "start": "node ./webpackDevServer.js", 8 | "broswer": "webpack --config ./webpack.config.bundle.js" 9 | }, 10 | "module": "dist/nanaSwiper.umd.js", 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/nanaSun/nanaSwiper.git" 14 | }, 15 | "eslintConfig": { 16 | "extends": "react-app" 17 | }, 18 | "browserslist": [ 19 | ">0.2%", 20 | "not dead", 21 | "not ie <= 11", 22 | "not op_mini all" 23 | ], 24 | "peerDependencies": { 25 | "prop-types": "^15.7.2", 26 | "react": "^16.8.1" 27 | }, 28 | "dependencies": { 29 | "@babel/polyfill": "^7.2.5", 30 | "react-router": "^4.3.1", 31 | "react-router-dom": "^4.3.1", 32 | "prop-types": "^15.7.2", 33 | "react": "^16.8.1", 34 | "react-dom": "^16.8.1" 35 | }, 36 | "devDependencies": { 37 | "@babel/cli": "^7.2.3", 38 | "@babel/core": "^7.2.2", 39 | "@babel/plugin-proposal-class-properties": "^7.3.3", 40 | "@babel/preset-env": "^7.3.1", 41 | "@babel/preset-react": "^7.0.0", 42 | "babel-eslint": "^9.0.0", 43 | "babel-jest": "^24.1.0", 44 | "babel-loader": "^8.0.5", 45 | "clean-webpack-plugin": "^1.0.1", 46 | "codecov": "^3.2.0", 47 | "css-loader": "^2.1.0", 48 | "enzyme": "^3.9.0", 49 | "enzyme-adapter-react-16": "^1.9.1", 50 | "eslint": "^5.14.1", 51 | "eslint-config-react-app": "^3.0.7", 52 | "eslint-loader": "^2.1.2", 53 | "eslint-plugin-flowtype": "^2.50.3", 54 | "eslint-plugin-import": "^2.16.0", 55 | "eslint-plugin-jsx-a11y": "^6.2.1", 56 | "eslint-plugin-react": "^7.12.4", 57 | "express": "^4.16.4", 58 | "file-loader": "^3.0.1", 59 | "html-webpack-plugin": "^3.2.0", 60 | "identity-obj-proxy": "^3.0.0", 61 | "jest": "^24.1.0", 62 | "mini-css-extract-plugin": "^0.5.0", 63 | "node-sass": "^4.11.0", 64 | "postcss-flexbugs-fixes": "^4.1.0", 65 | "postcss-loader": "^3.0.0", 66 | "postcss-preset-env": "^6.5.0", 67 | "react-test-renderer": "^16.8.3", 68 | "sass-loader": "^7.1.0", 69 | "style-loader": "^0.23.1", 70 | "url-loader": "^1.1.2", 71 | "webpack": "^4.29.3", 72 | "webpack-cli": "^3.2.3", 73 | "webpack-dev-middleware": "^3.6.0", 74 | "webpack-dev-server": "^3.1.14", 75 | "webpack-hot-middleware": "^2.24.3", 76 | "webpack-umd-external": "^1.0.2" 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /demo/pages/NormalSlider.js: -------------------------------------------------------------------------------- 1 | import React,{Fragment} from 'react'; 2 | 3 | import Swiper,{SwiperSlider} from '../../index' 4 | 5 | const data=[ 6 | { 7 | text:"text1" 8 | }, 9 | { 10 | text:"text2" 11 | }, 12 | { 13 | text:"text3" 14 | } 15 | ] 16 | export function NormalSlider(){ 17 | return ( 18 | 19 |

类似于router这样,一个个router子函数写好

20 |
21 | 27 | (
28 | slider-ahahah 29 |
)}/> 30 | (
31 | slider-gasdffds 32 |
)}/> 33 | (
34 | slider-werqwerq 35 |
)}/> 36 |
37 |
38 |

带循环的slider

39 |
40 | 45 | {data.map((d,index)=>{ 46 | return (
47 | slider-{d.text} 48 |
)}/> 49 | })} 50 |
51 |
52 |

非循环的slider

53 |
54 | 59 | {data.map((d,index)=>{ 60 | return (
61 | slider-{d.text} 62 |
)}/> 63 | })} 64 |
65 |
66 |
67 | ) 68 | } -------------------------------------------------------------------------------- /webpack.config.bundle.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | 4 | const cssRegex = /\.css$/; 5 | const cssModuleRegex = /\.module\.css$/; 6 | const sassRegex = /\.(scss|sass)$/; 7 | const sassModuleRegex = /\.module\.(scss|sass)$/; 8 | const getStyleLoaders = (cssOptions, preProcessor) => { 9 | const loaders = [ 10 | require.resolve('style-loader'), 11 | { 12 | loader: require.resolve('css-loader'), 13 | options: cssOptions, 14 | }, 15 | { 16 | loader: require.resolve('postcss-loader'), 17 | options: { 18 | ident: 'postcss', 19 | plugins: () => [ 20 | require('postcss-flexbugs-fixes'), 21 | require('postcss-preset-env')({ 22 | browsers: 'last 8 versions' 23 | }), 24 | ], 25 | }, 26 | }, 27 | ]; 28 | if (preProcessor) { 29 | loaders.push(require.resolve(preProcessor)); 30 | } 31 | return loaders; 32 | }; 33 | const config = { 34 | context:__dirname, 35 | mode: 'production', 36 | entry: path.join(__dirname, 'index.js'), 37 | output: { 38 | path: path.join(__dirname,"dist"), 39 | filename: 'nanaSwiper.umd.js', 40 | //filename: 'nanaSwiper.cjs.js', 41 | libraryTarget: "umd", 42 | library: "Swiper", 43 | umdNamedDefine: true 44 | }, 45 | resolve: { 46 | extensions: ['.js', '.jsx'] 47 | }, 48 | plugins: [ 49 | new webpack.DefinePlugin({ 50 | 'process.env': { 51 | NODE_ENV: JSON.stringify("development"), 52 | PUBLIC_URL: JSON.stringify("") 53 | } 54 | }), 55 | ], 56 | externals:{ 57 | react:{ 58 | root: 'React', 59 | amd: 'react', 60 | commonjs: 'react', 61 | commonjs2: 'react' 62 | }, 63 | "prop-types":{ 64 | root: 'prop-types', 65 | amd: 'prop-types', 66 | commonjs: 'prop-types', 67 | commonjs2: 'prop-types' 68 | } 69 | }, 70 | module: { 71 | rules: [ 72 | { 73 | test: /\.(js|mjs|jsx)$/, 74 | exclude: /node_modules/, 75 | loader: 'babel-loader' 76 | }, 77 | { 78 | test: cssRegex, 79 | exclude: cssModuleRegex, 80 | use: getStyleLoaders({ 81 | importLoaders: 1, 82 | }), 83 | }, 84 | { 85 | test: cssModuleRegex, 86 | use: getStyleLoaders({ 87 | importLoaders: 1, 88 | modules: true 89 | }), 90 | }, 91 | { 92 | test: sassRegex, 93 | exclude: sassModuleRegex, 94 | use: getStyleLoaders({ importLoaders: 2 }, 'sass-loader'), 95 | }, 96 | { 97 | test: sassModuleRegex, 98 | use: getStyleLoaders( 99 | { 100 | importLoaders: 2, 101 | modules: true 102 | }, 103 | 'sass-loader' 104 | ), 105 | }, 106 | ] 107 | } 108 | }; 109 | module.exports = config; -------------------------------------------------------------------------------- /webpack.config.demo.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | const cssRegex = /\.css$/; 6 | const cssModuleRegex = /\.module\.css$/; 7 | const sassRegex = /\.(scss|sass)$/; 8 | const sassModuleRegex = /\.module\.(scss|sass)$/; 9 | 10 | const getStyleLoaders = (cssOptions, preProcessor) => { 11 | const loaders = [ 12 | require.resolve('style-loader'), 13 | { 14 | loader: require.resolve('css-loader'), 15 | options: cssOptions, 16 | }, 17 | { 18 | loader: require.resolve('postcss-loader'), 19 | options: { 20 | ident: 'postcss', 21 | plugins: () => [ 22 | require('postcss-flexbugs-fixes'), 23 | require('postcss-preset-env')({ 24 | browsers: 'last 8 versions' 25 | }), 26 | ], 27 | }, 28 | }, 29 | ]; 30 | if (preProcessor) { 31 | loaders.push(require.resolve(preProcessor)); 32 | } 33 | return loaders; 34 | }; 35 | 36 | const config = { 37 | context: __dirname, 38 | mode: 'development', 39 | entry: [ 40 | require.resolve('webpack-hot-middleware/client') + '?path=/__what&timeout=2000&overlay=false', 41 | path.join(__dirname, 'demo/index.js') 42 | ], 43 | output: { 44 | path: path.join(__dirname, 'demo/dist'), 45 | filename: '[name].[hash].js' 46 | }, 47 | resolve: { 48 | extensions: ['.js', '.jsx'] 49 | }, 50 | plugins: [ 51 | new HtmlWebpackPlugin({ 52 | inject: true, 53 | template: path.join(__dirname, '/demo/tpl/index.html'), 54 | }), 55 | new webpack.HotModuleReplacementPlugin(), 56 | ], 57 | module: { 58 | rules: [ 59 | { 60 | enforce: "pre", 61 | test: /\.(js|mjs|jsx)$/, 62 | exclude: /node_modules/, 63 | loader: "eslint-loader" 64 | }, 65 | { 66 | oneOf: [ 67 | { 68 | test: /\.(js|mjs|jsx)$/, 69 | exclude: /node_modules/, 70 | loader: ['babel-loader','eslint-loader'] 71 | }, 72 | { 73 | test: cssRegex, 74 | exclude: cssModuleRegex, 75 | use: getStyleLoaders({ 76 | importLoaders: 1, 77 | }), 78 | }, 79 | { 80 | test: cssModuleRegex, 81 | use: getStyleLoaders({ 82 | importLoaders: 1, 83 | modules: true 84 | }), 85 | }, 86 | { 87 | test: sassRegex, 88 | exclude: sassModuleRegex, 89 | use: getStyleLoaders({ importLoaders: 2 }, 'sass-loader'), 90 | }, 91 | { 92 | test: sassModuleRegex, 93 | use: getStyleLoaders( 94 | { 95 | importLoaders: 2, 96 | modules: true 97 | }, 98 | 'sass-loader' 99 | ), 100 | }, 101 | { 102 | test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], 103 | loader: require.resolve('url-loader'), 104 | options: { 105 | limit: 10000, 106 | name: 'static/media/[name].[hash:8].[ext]', 107 | }, 108 | }] 109 | } 110 | ] 111 | } 112 | }; 113 | 114 | module.exports = config; 115 | -------------------------------------------------------------------------------- /demo/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | import './App.scss'; 4 | import {SpirteSlider} from "./pages/SpirteSlider" 5 | import {NormalSlider} from "./pages/NormalSlider" 6 | import {LoopSlider} from "./pages/LoopSlider" 7 | import {CoverFlowSlider} from "./pages/CoverFlowSlider" 8 | import {FreeModeSlider} from "./pages/FreeModeSlider" 9 | import {UserDefineSlider} from "./pages/UserDefineSlider" 10 | import { Switch,Route, Link } from 'react-router-dom' 11 | import history from './history' 12 | const navs=[ 13 | { 14 | name:"序列帧滚动", 15 | path:"/" 16 | }, 17 | { 18 | name:"普通滚动", 19 | path:"/simple" 20 | } 21 | , 22 | { 23 | name:"无限滚动", 24 | path:"/loop" 25 | } 26 | , 27 | { 28 | name:"coverFlow样式", 29 | path:"/coverflow" 30 | }, 31 | { 32 | name:"freeMode", 33 | path:"/freemode" 34 | }, 35 | { 36 | name:"用户自定义", 37 | path:"/userdefine" 38 | } 39 | ] 40 | function PathNav(props){ 41 | return 44 | } 45 | class App extends Component { 46 | state={ 47 | showMenu:false, 48 | currentPath:history.location.pathname 49 | } 50 | componentDidMount(){ 51 | history.listen(()=>{ 52 | this.setState({ 53 | showMenu:false, 54 | currentPath:history.location.pathname 55 | }) 56 | }) 57 | } 58 | clickMenuBtn(){ 59 | this.setState({ 60 | showMenu:!this.state.showMenu 61 | }) 62 | } 63 | render() { 64 | return ( 65 |
66 |
{this.clickMenuBtn()}}>
67 | 68 | {/* // 用于固定Swiper大小 */} 69 | 70 | { 71 | return ; 72 | }}/> 73 | { 74 | return ; 75 | }}/> 76 | { 77 | return ; 78 | }}/> 79 | { 80 | return ; 81 | }}/> 82 | { 83 | return ; 84 | }}/> 85 | { 86 | return ; 87 | }}/> 88 | 89 | 90 |
91 |

当前演示: {navs.filter((n)=>n.path===this.state.currentPath)[0]["name"]}

92 |

实现了simple的Swiper

93 |

功能有:

94 |
    95 |
  1. 可以滑动
  2. 96 |
  3. 可以自动滑动到下一页
  4. 97 | 98 |
  5. 可以自定义模板
  6. 99 |
  7. 设置灵敏度
  8. 100 |
  9. 支持序列帧
  10. 101 |
  11. 无限轮播图
  12. 102 |
103 |

github地址

104 |
105 |
106 | 107 | ); 108 | } 109 | } 110 | 111 | export default App; 112 | -------------------------------------------------------------------------------- /webpack.config.build.demo.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 4 | 5 | const cssRegex = /\.css$/; 6 | const cssModuleRegex = /\.module\.css$/; 7 | const sassRegex = /\.(scss|sass)$/; 8 | const sassModuleRegex = /\.module\.(scss|sass)$/; 9 | 10 | const getStyleLoaders = (cssOptions, preProcessor) => { 11 | const loaders = [ 12 | require.resolve('style-loader'), 13 | { 14 | loader: require.resolve('css-loader'), 15 | options: cssOptions, 16 | }, 17 | { 18 | loader: require.resolve('postcss-loader'), 19 | options: { 20 | ident: 'postcss', 21 | plugins: () => [ 22 | require('postcss-flexbugs-fixes'), 23 | require('postcss-preset-env')({ 24 | browsers: 'last 8 versions' 25 | }), 26 | ], 27 | }, 28 | }, 29 | ]; 30 | if (preProcessor) { 31 | loaders.push(require.resolve(preProcessor)); 32 | } 33 | return loaders; 34 | }; 35 | 36 | const config = { 37 | context: __dirname, 38 | mode: 'production', 39 | entry: [ 40 | path.join(__dirname, 'demo/index.js') 41 | ], 42 | optimization: { 43 | splitChunks: { 44 | cacheGroups: { 45 | commons: { 46 | chunks: "initial", 47 | minChunks: 2,//最小重复的次数 48 | minSize: 0//最小提取字节数 49 | }, 50 | vendor: { 51 | test: /node_modules/, 52 | chunks: "initial", 53 | name: "vendor", 54 | } 55 | } 56 | } 57 | }, 58 | output: { 59 | path: path.join(__dirname, 'demo/dist'), 60 | filename: '[name].[hash].js' 61 | }, 62 | resolve: { 63 | extensions: ['.js', '.jsx'] 64 | }, 65 | plugins: [ 66 | new HtmlWebpackPlugin({ 67 | inject: true, 68 | template: path.join(__dirname, '/demo/tpl/index.html'), 69 | }), 70 | new webpack.DefinePlugin({ 71 | 'process.env': { 72 | NODE_ENV: JSON.stringify("production") 73 | } 74 | }), 75 | ], 76 | module: { 77 | rules: [ 78 | { 79 | oneOf: [ 80 | { 81 | test: /\.(js|mjs|jsx)$/, 82 | exclude: /node_modules/, 83 | loader: ['babel-loader'] 84 | }, 85 | { 86 | test: cssRegex, 87 | exclude: cssModuleRegex, 88 | use: getStyleLoaders({ 89 | importLoaders: 1, 90 | }), 91 | }, 92 | { 93 | test: cssModuleRegex, 94 | use: getStyleLoaders({ 95 | importLoaders: 1, 96 | modules: true 97 | }), 98 | }, 99 | { 100 | test: sassRegex, 101 | exclude: sassModuleRegex, 102 | use: getStyleLoaders({ importLoaders: 2 }, 'sass-loader'), 103 | }, 104 | { 105 | test: sassModuleRegex, 106 | use: getStyleLoaders( 107 | { 108 | importLoaders: 2, 109 | modules: true 110 | }, 111 | 'sass-loader' 112 | ), 113 | }, 114 | { 115 | test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/], 116 | loader: require.resolve('url-loader'), 117 | options: { 118 | limit: 10000, 119 | name: 'static/media/[name].[hash:8].[ext]', 120 | }, 121 | }] 122 | } 123 | ] 124 | } 125 | }; 126 | 127 | module.exports = config; 128 | -------------------------------------------------------------------------------- /test/simple.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Swiper,{SwiperSlider} from '../index'; 3 | import {SliderDefaultTemplate} from '../demo/template/SliderDefaultTemplate' 4 | //import TestRenderer from 'react-test-renderer'; 5 | import touch from './touchHelper'; 6 | import Adapter from 'enzyme-adapter-react-16'; 7 | import { configure, shallow, mount, render } from 'enzyme'; 8 | configure({ adapter: new Adapter() }); 9 | const winwidth=window.innerWidth; 10 | const data=[ 11 | { 12 | text:"text1", 13 | tpl:SliderDefaultTemplate 14 | }, 15 | { 16 | text:"text2", 17 | tpl:SliderDefaultTemplate 18 | }, 19 | { 20 | text:"text3", 21 | tpl:SliderDefaultTemplate 22 | } 23 | ] 24 | //没有做y上的处理,所以这里全部为0,并不影响结果 25 | /** 26 | * 宽度是整屏宽 27 | * 灵敏度是移动偏差为宽度*0.2的时候,触发滚动 28 | * 非循环,小于第一屏和大于最后一屏幕会回弹 29 | */ 30 | const baseComponent = { 31 | width:winwidth, 32 | sensitive:.2, 33 | componment:null 34 | }; 35 | baseComponent.componment=( 40 | {data.map((d,index)=>{ 41 | return }/> 42 | })} 43 | ) 44 | 45 | describe('Swiper simple test', () => { 46 | const touchPoints=[ 47 | [[winwidth*.5,0],[winwidth*.6,0],[winwidth*.6],0,"sensitive less then .2 and move right slider come back to current"],//偏差小于.2 48 | [[winwidth*.5,0],[winwidth*.29,0],[winwidth*.29],1,"sensitive bigger then .2 and move left slider come to next "],//偏差大于.2,向左 49 | [[winwidth*.5,0],[winwidth*.6,0],[winwidth*.6],1,"sensitive less then .2 and move right slider come back to current "],//偏差大于.2,向右 50 | [[winwidth*.5,0],[winwidth*.2,0],[winwidth*.2],2,"sensitive bigger then .2 and move left slider come to next"],//偏差小于.2,向右 51 | [[winwidth*.5,0],[winwidth*.9,0],[winwidth*.9],1,"sensitive bigger then .2 and move right slider come to prev"],//偏差小于.2,向右 52 | [[winwidth*.5,0],[winwidth*.29,0],[winwidth*.29],2,"sensitive bigger then .2 and move left slider come to next"],//偏差大于.2,向左 53 | [[winwidth*.5,0],[winwidth*.1,0],[winwidth*.1],2,"sensitive bigger then .2 and move left slider come back to current"],//偏差大于.2,向左 54 | [[winwidth*.5,0],[winwidth*.4,0],[winwidth*.4],2,"sensitive less then .2 and move left slider come back to current "],//偏差大于.2,向左 55 | ] 56 | const tree=mount(baseComponent.componment) 57 | const c = tree.instance() 58 | for(let point of touchPoints){ 59 | it(point[4], () => { 60 | c.touchstart(touch.emulateTouchEvent('touchstart',point[0])) 61 | c.touchmove(touch.emulateTouchEvent('touchmove',point[1])) 62 | c.touchend(touch.emulateTouchEvent('touchend',point[2])) 63 | c.transitionend() 64 | expect(c.currentSliderIndex).toBe(point[3]); 65 | }); 66 | } 67 | }); 68 | 69 | /** 70 | * 宽度是整屏宽 71 | * 灵敏度是移动偏差为宽度*0.2的时候,触发滚动 72 | * 循环,小于第一屏会跳转至下一屏 73 | */ 74 | const loopComponent={ 75 | width:winwidth, 76 | sensitive:.2, 77 | componment:null 78 | }; 79 | loopComponent.componment=( 84 | {data.map((d,index)=>{ 85 | return }/> 86 | })} 87 | ) 88 | describe('Swiper loop test', () => { 89 | const loopPoints=[ 90 | [[winwidth*.5,0],[winwidth*.29,0],[winwidth*.29,0],2,"sensitive bigger then .2 and move left slider come to next"],//偏差大于.2,向左 91 | [[winwidth*.5,0],[winwidth*.2,0],[winwidth*.2,0],3,"sensitive bigger then .2 and move left slider come to last"],//偏差小于.2,向左 92 | [[winwidth*.5,0],[winwidth*.1,0],[winwidth*.1,0],1,"sensitive bigger then .2 and move right slider come to first"],//偏差小于.2,向左,loop至第一个 93 | [[winwidth*.5,0],[winwidth*.9,0],[winwidth*.9,0],3,"sensitive bigger then .2 and move right slider come to last"],//偏差小于.2,向右,返回最后一个 94 | ] 95 | const tree=mount(loopComponent.componment) 96 | const c = tree.instance() 97 | it("has plus two slider and init slider's index is 1", () => { 98 | expect(c.currentSliderIndex).toBe(1); 99 | expect(c.sliders.length).toBe(data.length+2); 100 | }); 101 | for(let point of loopPoints){ 102 | it(point[4], () => { 103 | c.touchstart(touch.emulateTouchEvent('touchstart',point[0])) 104 | c.touchmove(touch.emulateTouchEvent('touchmove',point[1])) 105 | c.touchend(touch.emulateTouchEvent('touchend',point[2])) 106 | c.transitionend() 107 | expect(c.currentSliderIndex).toBe(point[3]); 108 | }); 109 | } 110 | }); -------------------------------------------------------------------------------- /src/Sprite.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from "prop-types"; 3 | export class Sprite extends Component { 4 | //canvas的宽高 5 | canvasWidth=0 6 | canvasHeight=0 7 | //图片的加载 8 | image=new Image() 9 | refs=null 10 | loadImagePromise=null 11 | //序列帧的参数 行数列数以及序列帧的范围 12 | row=0 13 | col=0 14 | fpsWidth=0 15 | fpsHeight=0 16 | //动画中序列的位置 17 | currentRow=0 18 | currentCol=0 19 | //控制序列帧的速度 20 | fps=0 21 | //是否停止 22 | stop=true 23 | loaded=false 24 | animationTimeout=null 25 | animating=false 26 | time=0 27 | endtime=0 28 | animationTimer=null 29 | constructor(props){ 30 | super(props) 31 | if(process.env.NODE_ENV==="development") console.log(props) 32 | //初始化 33 | 34 | this.canvasWidth=props.width*2 35 | this.canvasHeight=props.height*2 36 | this.speed=props.speed||1 37 | this.image.src=props.spriteImg 38 | this.loadImagePromise=this.loadImg() 39 | 40 | if(this.props.spriteConf&&this.props.spriteConf.length===4) 41 | [this.row,this.col,this.fpsWidth,this.fpsHeight]=this.props.spriteConf 42 | 43 | //this.canvas=React.createRef() 44 | this.canvas=(refs)=>{ 45 | if(refs) { 46 | //获取绘图环境 47 | this.refs=refs 48 | this.context=refs.getContext("2d") 49 | if(process.env.NODE_ENV==="development") console.log(refs) 50 | } 51 | } 52 | } 53 | shouldComponentUpdate(props){ 54 | console.log("shouldComponentUpdate",props.isMoving) 55 | if(props.isMoving!==0){ 56 | this.stop=true 57 | return false 58 | }else{ 59 | return true 60 | } 61 | } 62 | componentDidUpdate(){//当幻灯片停止滑动的时候,更新组件,重新获取context 63 | window.cancelAnimationFrame(this.animationTimer)//当幻灯片疯狂滑动的时候,停止滑动 64 | this.context=this.refs.getContext("2d") 65 | this.stop=false 66 | this.animating=false 67 | console.log("did update",this.animating) 68 | this.animate() 69 | if(process.env.NODE_ENV==="development") console.log("componentDidUpdate",this.context) 70 | } 71 | componentDidMount(){ 72 | this.currentRow=0 73 | this.currentCol=0 74 | //初始化之后,设定动画不停止 75 | this.stop=false 76 | if(!this.loaded){ 77 | this.loadImagePromise.then(()=>{ 78 | this.animate() 79 | //图片加载完之后才算真正加载完 80 | this.loaded=true 81 | },(e)=>console.log(e)) 82 | }else{ 83 | this.animate() 84 | } 85 | } 86 | componentWillUnmount(){ 87 | if(process.env.NODE_ENV==="development") console.log("canvas unload") 88 | //卸载后,清除canvas 89 | this.stop=true 90 | if(this.context.clearRect) this.context.clearRect(0,0,this.canvasWidth,this.canvasHeight) 91 | this.image=null 92 | } 93 | animate(){ 94 | let _this=this 95 | if(_this.animating) return 96 | _this.animating=true; 97 | animation(); 98 | function animation(){ 99 | // _this.endtime=new Date().getTime() 100 | // console.log("time",_this.endtime-_this.time) 101 | // _this.time=_this.endtime 102 | if(_this.fps>=_this.speed){//此处控制速度 103 | _this.fps=0 104 | let cr=_this.currentRow, cc=_this.currentCol; 105 | if(cr===_this.row){ 106 | _this.currentRow=0 107 | _this.currentCol++ 108 | }else{ 109 | _this.currentRow++ 110 | } 111 | if(cc>_this.col){ 112 | _this.currentCol=0 113 | } 114 | _this.drawSpirt() 115 | } 116 | _this.fps++ 117 | if(!_this.stop&&_this.context){ 118 | _this.animationTimer=window.requestAnimationFrame(animation)//如果未停止以及存在绘图环境 119 | } 120 | else{ if(process.env.NODE_ENV==="development") console.log(_this)} 121 | } 122 | 123 | 124 | } 125 | drawSpirt(){ 126 | if(this.stop) return 127 | let cr=this.currentRow, cc=this.currentCol; 128 | if("drawImage" in this.context){this.context.drawImage(this.image,cr*this.fpsWidth,cc*this.fpsHeight,this.fpsWidth,this.fpsHeight,0,0,this.canvasWidth,this.canvasHeight)} 129 | } 130 | loadImg(){ 131 | return new Promise((rs,rj)=>{ 132 | this.image.onload=rs 133 | this.image.onerror=rj 134 | }) 135 | } 136 | render(){ 137 | let { height,width}=this.props 138 | return 139 | } 140 | } 141 | Sprite.propTypes = { 142 | width:PropTypes.number.isRequired, 143 | height:PropTypes.number.isRequired, 144 | speed:PropTypes.number, 145 | spriteImg:PropTypes.string.isRequired, 146 | spriteConf:PropTypes.node.isRequired 147 | }; 148 | export default Sprite -------------------------------------------------------------------------------- /demo/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read http://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.1/8 is considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | console.log(process.env.NODE_ENV) 25 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 26 | // The URL constructor is available in all browsers that support SW. 27 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 28 | if (publicUrl.origin !== window.location.origin) { 29 | // Our service worker won't work if PUBLIC_URL is on a different origin 30 | // from what our page is served on. This might happen if a CDN is used to 31 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 32 | return; 33 | } 34 | 35 | window.addEventListener('load', () => { 36 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 37 | 38 | if (isLocalhost) { 39 | // This is running on localhost. Let's check if a service worker still exists or not. 40 | checkValidServiceWorker(swUrl, config); 41 | 42 | // Add some additional logging to localhost, pointing developers to the 43 | // service worker/PWA documentation. 44 | navigator.serviceWorker.ready.then(() => { 45 | console.log( 46 | 'This web app is being served cache-first by a service ' + 47 | 'worker. To learn more, visit http://bit.ly/CRA-PWA' 48 | ); 49 | }); 50 | } else { 51 | // Is not localhost. Just register service worker 52 | registerValidSW(swUrl, config); 53 | } 54 | }); 55 | } 56 | } 57 | 58 | function registerValidSW(swUrl, config) { 59 | navigator.serviceWorker 60 | .register(swUrl) 61 | .then(registration => { 62 | registration.onupdatefound = () => { 63 | const installingWorker = registration.installing; 64 | if (installingWorker == null) { 65 | return; 66 | } 67 | installingWorker.onstatechange = () => { 68 | if (installingWorker.state === 'installed') { 69 | if (navigator.serviceWorker.controller) { 70 | // At this point, the updated precached content has been fetched, 71 | // but the previous service worker will still serve the older 72 | // content until all client tabs are closed. 73 | console.log( 74 | 'New content is available and will be used when all ' + 75 | 'tabs for this page are closed. See http://bit.ly/CRA-PWA.' 76 | ); 77 | 78 | // Execute callback 79 | if (config && config.onUpdate) { 80 | config.onUpdate(registration); 81 | } 82 | } else { 83 | // At this point, everything has been precached. 84 | // It's the perfect time to display a 85 | // "Content is cached for offline use." message. 86 | console.log('Content is cached for offline use.'); 87 | 88 | // Execute callback 89 | if (config && config.onSuccess) { 90 | config.onSuccess(registration); 91 | } 92 | } 93 | } 94 | }; 95 | }; 96 | }) 97 | .catch(error => { 98 | console.error('Error during service worker registration:', error); 99 | }); 100 | } 101 | 102 | function checkValidServiceWorker(swUrl, config) { 103 | // Check if the service worker can be found. If it can't reload the page. 104 | fetch(swUrl) 105 | .then(response => { 106 | // Ensure service worker exists, and that we really are getting a JS file. 107 | const contentType = response.headers.get('content-type'); 108 | if ( 109 | response.status === 404 || 110 | (contentType != null && contentType.indexOf('javascript') === -1) 111 | ) { 112 | // No service worker found. Probably a different app. Reload the page. 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister().then(() => { 115 | window.location.reload(); 116 | }); 117 | }); 118 | } else { 119 | // Service worker found. Proceed as normal. 120 | registerValidSW(swUrl, config); 121 | } 122 | }) 123 | .catch(() => { 124 | console.log( 125 | 'No internet connection found. App is running in offline mode.' 126 | ); 127 | }); 128 | } 129 | 130 | export function unregister() { 131 | if ('serviceWorker' in navigator) { 132 | navigator.serviceWorker.ready.then(registration => { 133 | registration.unregister(); 134 | }); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nana's Swiper 2 | 3 | You can install it by npm: 4 | 5 | ``` 6 | npm install --save nanaswiper 7 | ``` 8 | 9 | you can import it use ES6 `import Swiper,{SwiperSlider} from 'nanaswiper'`。 10 | 11 | Or CommonJS 12 | 13 | ``` 14 | const Swiper=require('nanaswiper').default 15 | const SwiperSlider=require('nanaswiper').SwiperSlider` 16 | ``` 17 | 18 | 19 | Major change: 20 | 21 | Change how to set slider, less configuration and makes it like react-router's style 22 | 23 | It's a slider that only supports horizonal slide. Just for now,I'll develop vertical slide in the next version. 24 | 25 | The reason why develop such a swiper,just because a project need sliders with sprit and react. And it's hard to find a 3rd plugin which support for slider,react and canvas. It's easy to find a lot of plugins that support canvas and react or react and slider. That's why I develop such a plugin. 26 | 27 | You can check my demo on [https://www.cherryvenus.com/slider/](https://www.cherryvenus.com/slider/) 28 | 29 | If you want to play with this project, you can clone the repositor and then use following directions to start the project. 30 | 31 | build demo 32 | 33 | `npx webpack --config webpack.config.demo` 34 | 35 | start a webpack server 36 | `npm start` 37 | 38 | This Swiper's config: 39 | 40 | |params|function|default|required| 41 | | ------------- |:-------------:| -----:| -----:| 42 | |children|sliders inside swiper is a must|null|yes||sensitive|how sensitive it is when move the swiper to next or prev slider|0.5|no| 43 | |initMovex|calculator initial slider position|0|no| 44 | |isLoop|whether loop. One slider or freemode does not support loop|false|no| 45 | |width|width|window.innerWidth|no| 46 | |height|height|300|no| 47 | |isFreeMode|is Free Mode and not support loop mode|false|no| 48 | |initSliderIndex|inital slider,range from 0 to slider's number minus 1|0|no| 49 | |slideType|`flatCoverFlow` effect,maybe remove after|""|no| 50 | 51 | ```simple usage 52 | import Swiper,{SwiperSlider} from 'nanaswiper' 53 |
54 | 59 | (
60 | slider-ahahah 61 |
)}/> 62 | (
63 | slider-gasdffds 64 |
)}/> 65 | (
66 | slider-werqwerq 67 |
)}/> 68 |
69 |
70 | ``` 71 | 72 | Here follow react-router's style, so you can design your slider by `render` 73 | 74 | ``` 75 | (
76 | slider-ahahah 77 |
)}/> 78 | ``` 79 | 80 | here `props` params passed to `render` has following important function: 81 | 82 | |params|usage| 83 | |:--:|:--:| 84 | |isMoving|whether the swiper is moving,touchmove -1 for left and 1 for right| 85 | |isActive|whether it's current slider| 86 | 87 | And if you want an array loop, the following example is for you: 88 | 89 | ``` 90 | const data=[ 91 | { 92 | text:"text1" 93 | }, 94 | { 95 | text:"text2" 96 | }, 97 | { 98 | text:"text3" 99 | } 100 | ] 101 | //... 102 | 103 | {data.map((d,index)=>{ 104 | return (
105 | slider-{d.text} 106 |
)}/> 107 | })} 108 |
109 | //... 110 | 111 | ``` 112 | 113 | Next part is how to use Sprite with Swiper 114 | 115 | First, define a sprite template. 116 | 117 | ```SliderSpriteTemplate.js 118 | import React,{Fragment,Component} from 'react' 119 | import Swiper,{SwiperSlider} from 'nanaswiper' 120 | 121 | class SliderSpriteTemplate extends Component{ 122 | render(){ 123 | return ( 124 | 125 | {this.props.isActive?:""} 126 | 127 | ) 128 | } 129 | } 130 | export {SliderSpriteTemplate} 131 | ``` 132 | Next set the sprite's params: 133 | 134 | ``` 135 | const data=[ 136 | { 137 | width:window.innerWidth,//sprite width must 138 | height:300,//sprite height must 139 | spriteImg:img1,//must 140 | spriteConf:[3,1,222,350],//sprite'position must 141 | speed:6,//sprite'speed optional 142 | tpl:SliderSpriteTemplate//you can set any template you like, it's a must for sprite 143 | }, 144 | //... 145 | ] 146 | export function SpirteSlider(){ 147 | return (
148 | 153 | {data.map((d,index)=>{ 154 | return } 157 | /> 158 | })} 159 | 160 |
) 161 | ``` 162 | 163 | Sprite support params: 164 | 165 | |params|function|default|required| 166 | | ------------- |:-------------:| -----:| -----:| 167 | |width|canvas'width|""|yes| 168 | |height|canvas'height|""|yes| 169 | |data.spriteImg|image to animate|""|yes| 170 | |data.spriteConf|sprite's config(row num,col num,fpsWidth:width,fpsHeight:height)|{}|yes| 171 | |data.speed|speed,default 60fps, if set 2 it's 30fps|1|no| 172 | 173 | Browser version, you can get it from ` npm run-script broswer`, and then `dist/nanaSwiper.js` is there for you. 174 | 175 | How to use? You can directly use it in browser. 176 | 177 | ``` 178 | 179 | 180 | 181 | 182 | 216 | ``` 217 | 218 | 219 | 220 | About test, here simulate the touch action, and only test the whether slider is right. 221 | 222 | ``` 223 | npm test 224 | ``` 225 | . 226 | 227 | 感觉自己做的好复杂,溜了溜了 228 | 229 | 下一版计划: 230 | * 修复freemode下,首尾滑动不流畅 231 | * 序列帧支持导入json配置 232 | * 来个垂直滑动 233 | -------------------------------------------------------------------------------- /src/Swiper.js: -------------------------------------------------------------------------------- 1 | import React,{Component,Fragment} from 'react' 2 | import SwiperCSS from './styles/Swiper.module.scss' 3 | import SwiperContext from "./SwiperContext"; 4 | import {checkTouch} from './utils' 5 | import PropTypes from "prop-types"; 6 | 7 | const supportTouch=checkTouch() 8 | 9 | class Swiper extends Component{ 10 | //控制手势 11 | startPoint=0//开始滑动的点touchstart 12 | movePoint=0//移动的点touchmove 13 | endPoint=0//结束的点touchend 14 | moveingStart=false//判断是否开始touchstart 15 | isMoving=0//起始滑动为0 16 | swiperWidth=0//幻灯片的宽度 17 | swiperHeight=0//幻灯片的高度 18 | initMovex=0//幻灯片开始的位置 19 | currentSliderIndex=0//初始幻灯片 20 | sensitive=0.5//灵明度 21 | isFreeMode=false 22 | isLoop=false 23 | slideType="" 24 | sliders=[] 25 | bounds={} 26 | boundsMove={} 27 | constructor(props){ 28 | super(props) 29 | this.slideType=props.slideType||this.slideType; 30 | this.isFreeMode=props.isFreeMode||this.isFreeMode; 31 | this.sensitive=props.sensitive||this.sensitive; 32 | this.sliders=props.children 33 | this.isLoop=(this.sliders.length>1?props.isLoop:false)||this.isLoop 34 | this.currentSliderIndex=props.initSliderIndex||0 35 | this.initMovex=props.initMovex||this.initMovex; 36 | this.initMovex=(this.initMovex<1?this.initMovex*this.props.width:this.initMovex) 37 | this.swiperWidth=this.props.width||window.innerWidth 38 | this.swiperHeight=this.props.height||300 39 | this.init() 40 | this.state={ 41 | moveX:this.initMovex-this.currentSliderIndex*this.swiperWidth, 42 | isMoving:0,//判断是否正在移动touchmove -1 左 1 右 43 | isTransition:false, 44 | isLoaded:true 45 | } 46 | 47 | } 48 | 49 | init(){ 50 | if(this.isLoop){ 51 | this.sliders=[ 52 | React.cloneElement(this.sliders.slice(-1)[0],{key:"SwiperFirst",index:0}), 53 | ...this.sliders.map((s,i)=>{ 54 | return React.cloneElement(s, 55 | {key:"Swiper"+(i+1),index:i+1}) 56 | }), 57 | React.cloneElement(this.sliders.slice(0,1)[0],{key:"SwiperLast",index:this.sliders.length+1})] 58 | this.bounds={ 59 | min:1, 60 | max:this.sliders.length-1 61 | } 62 | this.currentSliderIndex++ 63 | }else{ 64 | this.sliders=this.sliders.map((s,i)=>{ 65 | return React.cloneElement(s, 66 | {key:"Swiper"+i,index:i}) 67 | }) 68 | this.bounds={ 69 | min:0, 70 | max:this.sliders.length-1 71 | } 72 | } 73 | this.boundsMove={ 74 | min:-this.bounds.max*this.swiperWidth-this.initMovex, 75 | max:this.initMovex 76 | } 77 | 78 | } 79 | componentDidMount(){ 80 | } 81 | getPointX(e){ 82 | return supportTouch?e.touches[0].clientX:e.clientX 83 | } 84 | touchstart(e){ 85 | if((e.type==="mousedown"&&!supportTouch)||(e.type==="touchstart"&&supportTouch)){ 86 | //e.preventDefault(); 87 | this.startPoint=this.getPointX(e) 88 | this.movePoint=this.startPoint 89 | this.endPoint=this.startPoint 90 | this.moveingStart=true; 91 | } 92 | } 93 | touchmove(e){ 94 | if(this.moveingStart&&((e.type==="mousemove"&&!supportTouch)||(e.type==="touchmove"&&supportTouch))){ 95 | e.preventDefault(); 96 | this.endPoint=this.getPointX(e) 97 | let changePos=this.endPoint-this.movePoint+this.state.moveX; 98 | //console.log(this.endPoint,this.movePoint,this.state.moveX) 99 | this.movePoint=this.endPoint 100 | this.isMoving=this.movePoint-this.startPoint>0?-1:1 101 | //mark这边触发渲染 102 | //console.log("touchmove",changePos) 103 | this.setState({ 104 | isTransition:false, 105 | moveX:changePos 106 | }) 107 | } 108 | } 109 | touchend(e){ 110 | //console.log("touchend",e.type) 111 | if(((e.type==="mouseleave"||e.type==="mouseup")&&!supportTouch)||(e.type==="touchend"&&supportTouch)){ 112 | this.moveingStart=false; 113 | if(this.startPoint!==this.endPoint){ 114 | e.preventDefault() 115 | this.slideTo(this.calculateSlider()) 116 | }else{ 117 | this.isMoving=0 118 | } 119 | } 120 | } 121 | calculateSlider(){ 122 | let {min,max}=this.bounds 123 | let MoveSlider=Math.abs(this.state.moveX/this.swiperWidth) 124 | let slideIndex=this.currentSliderIndex 125 | //计算当前slider和滑动的差值 126 | let difference=MoveSlider-slideIndex 127 | if(this.isMoving===1&&difference>this.sensitive){//向右边,向左滑动 128 | slideIndex=Math.ceil(MoveSlider) 129 | slideIndex=slideIndex-this.currentSliderIndex>1?slideIndex-1:slideIndex 130 | }else if(this.isMoving===-1&&difference<-this.sensitive){//向左边 131 | slideIndex=Math.floor(MoveSlider) 132 | //fix灵敏度不高的情况下 滑动超过1slider 133 | slideIndex=slideIndex-this.currentSliderIndex<-1?slideIndex+1:slideIndex 134 | } 135 | slideIndex=slideIndexmax?max:slideIndex 136 | return slideIndex 137 | } 138 | fixLoop(){ 139 | if(!this.isLoop) return 140 | let {min,max}=this.bounds 141 | if(this.currentSliderIndex=max){ 149 | this.currentSliderIndex=1 150 | this.setState({ 151 | isTransition:false, 152 | moveX:-this.swiperWidth+this.initMovex 153 | }) 154 | } 155 | } 156 | slideTo(slideIndex,force){ 157 | //若果没有滑动,活着正在滑动,就🔙 158 | if(this.moveingStart||this.isMoving===0) return 159 | let {min,max}=this.boundsMove 160 | this.currentSliderIndex=slideIndex 161 | if(force||!this.isFreeMode||(this.state.moveXmax)){ 162 | this.setState({ 163 | isTransition:true, 164 | moveX:-slideIndex*this.swiperWidth+this.initMovex 165 | }) 166 | }else{ 167 | this.currentSliderIndex=slideIndex 168 | } 169 | } 170 | clickSlideTo(index){ 171 | this.isMoving=1 172 | this.slideTo(index,true) 173 | } 174 | transitionend(){ 175 | this.isMoving=0 176 | this.setState({ 177 | isTransition:false 178 | }) 179 | this.fixLoop() 180 | } 181 | render(){ 182 | if(this.sliders.length<1||!this.state.isLoaded){ 183 | return
184 | } 185 | let {moveX,isTransition}=this.state, 186 | transitionDuration=isTransition?"300ms":"0ms", 187 | transformX=`translate3d(${moveX}px, 0px, 0px)` 188 | const props = { 189 | ...this.state, 190 | isMoving:this.isMoving, 191 | currentSliderIndex:this.currentSliderIndex 192 | }; 193 | return ( 194 | {/* 滑动主体 */} 195 |
{this.touchstart(e)}} 198 | onTouchMove={(e)=>{this.touchmove(e)}} 199 | onTouchEnd={(e)=>{this.touchend(e)}} 200 | onMouseDown={(e)=>{this.touchstart(e)}} 201 | onMouseMove={(e)=>{this.touchmove(e)}} 202 | onMouseUp={(e)=>{this.touchend(e)}} 203 | onMouseLeave={(e)=>{this.touchend(e)}} 204 | onTransitionEnd={(e)=>{this.transitionend()}} 205 | style={{ 206 | WebkitTransitionDuration:transitionDuration, 207 | transitionDuration:transitionDuration, 208 | WebkitTransform:transformX, 209 | transform:transformX, 210 | height:this.swiperHeight 211 | }} 212 | > 213 | 217 |
218 | {/* 导航点 */} 219 |
220 | {this.sliders.map((color,index)=> 221 | {this.isLoop&&(index===0||index===this.bounds.max)?"": 222 |
{this.clickSlideTo(index)}} 224 | >
} 225 |
226 | )} 227 |
228 |
229 | ) 230 | } 231 | } 232 | Swiper.propTypes = { 233 | children: PropTypes.node.isRequired, 234 | }; 235 | 236 | export default Swiper -------------------------------------------------------------------------------- /dist/nanaSwiper.umd.js: -------------------------------------------------------------------------------- 1 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("react"),require("prop-types")):"function"==typeof define&&define.amd?define("Swiper",["react","prop-types"],e):"object"==typeof exports?exports.Swiper=e(require("react"),require("prop-types")):t.Swiper=e(t.React,t["prop-types"])}(window,function(t,e){return function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=105)}([function(e,n){e.exports=t},function(t,e,n){var r=n(28)("wks"),o=n(18),i=n(2).Symbol,s="function"==typeof i;(t.exports=function(t){return r[t]||(r[t]=s&&i[t]||(s?i:o)("Symbol."+t))}).store=r},function(t,e){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},function(t,e,n){var r=n(2),o=n(10),i=n(13),s=n(14),c=n(15),u=function(t,e,n){var a,f,l,p,h=t&u.F,v=t&u.G,d=t&u.S,y=t&u.P,m=t&u.B,b=v?r:d?r[e]||(r[e]={}):(r[e]||{}).prototype,g=v?o:o[e]||(o[e]={}),x=g.prototype||(g.prototype={});for(a in v&&(n=e),n)l=((f=!h&&b&&void 0!==b[a])?b:n)[a],p=m&&f?c(l,r):y&&"function"==typeof l?c(Function.call,l):l,b&&s(b,a,l,t&u.U),g[a]!=l&&i(g,a,p),y&&x[a]!=l&&(x[a]=l)};r.core=o,u.F=1,u.G=2,u.S=4,u.P=8,u.B=16,u.W=32,u.U=64,u.R=128,t.exports=u},function(t,e,n){var r=n(9);t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},function(t,n){t.exports=e},function(t,e,n){var r=n(4),o=n(48),i=n(30),s=Object.defineProperty;e.f=n(7)?Object.defineProperty:function(t,e,n){if(r(t),e=i(e,!0),r(n),o)try{return s(t,e,n)}catch(t){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(t[e]=n.value),t}},function(t,e,n){t.exports=!n(11)(function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a})},function(t,e,n){var r=n(101);"string"==typeof r&&(r=[[t.i,r,""]]);var o={hmr:!0,transform:void 0,insertInto:void 0};n(103)(r,o);r.locals&&(t.exports=r.locals)},function(t,e){t.exports=function(t){return"object"==typeof t?null!==t:"function"==typeof t}},function(t,e){var n=t.exports={version:"2.6.1"};"number"==typeof __e&&(__e=n)},function(t,e){t.exports=function(t){try{return!!t()}catch(t){return!0}}},function(t,e){var n={}.hasOwnProperty;t.exports=function(t,e){return n.call(t,e)}},function(t,e,n){var r=n(6),o=n(19);t.exports=n(7)?function(t,e,n){return r.f(t,e,o(1,n))}:function(t,e,n){return t[e]=n,t}},function(t,e,n){var r=n(2),o=n(13),i=n(12),s=n(18)("src"),c=Function.toString,u=(""+c).split("toString");n(10).inspectSource=function(t){return c.call(t)},(t.exports=function(t,e,n,c){var a="function"==typeof n;a&&(i(n,"name")||o(n,"name",e)),t[e]!==n&&(a&&(i(n,s)||o(n,s,t[e]?""+t[e]:u.join(String(e)))),t===r?t[e]=n:c?t[e]?t[e]=n:o(t,e,n):(delete t[e],o(t,e,n)))})(Function.prototype,"toString",function(){return"function"==typeof this&&this[s]||c.call(this)})},function(t,e,n){var r=n(23);t.exports=function(t,e,n){if(r(t),void 0===e)return t;switch(n){case 1:return function(n){return t.call(e,n)};case 2:return function(n,r){return t.call(e,n,r)};case 3:return function(n,r,o){return t.call(e,n,r,o)}}return function(){return t.apply(e,arguments)}}},function(t,e,n){var r=n(50),o=n(32);t.exports=function(t){return r(o(t))}},function(t,e){t.exports=!1},function(t,e){var n=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++n+r).toString(36))}},function(t,e){t.exports=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}},function(t,e,n){var r=n(49),o=n(35);t.exports=Object.keys||function(t){return r(t,o)}},function(t,e){var n={}.toString;t.exports=function(t){return n.call(t).slice(8,-1)}},function(t,e){t.exports={}},function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},function(t,e,n){var r=n(6).f,o=n(12),i=n(1)("toStringTag");t.exports=function(t,e,n){t&&!o(t=n?t:t.prototype,i)&&r(t,i,{configurable:!0,value:e})}},function(t,e,n){var r=n(33),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},function(t,e,n){var r=n(32);t.exports=function(t){return Object(r(t))}},function(t,e,n){n(46)("asyncIterator")},function(t,e,n){var r=n(10),o=n(2),i=o["__core-js_shared__"]||(o["__core-js_shared__"]={});(t.exports=function(t,e){return i[t]||(i[t]=void 0!==e?e:{})})("versions",[]).push({version:r.version,mode:n(17)?"pure":"global",copyright:"© 2018 Denis Pushkarev (zloirock.ru)"})},function(t,e,n){var r=n(9),o=n(2).document,i=r(o)&&r(o.createElement);t.exports=function(t){return i?o.createElement(t):{}}},function(t,e,n){var r=n(9);t.exports=function(t,e){if(!r(t))return t;var n,o;if(e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;if("function"==typeof(n=t.valueOf)&&!r(o=n.call(t)))return o;if(!e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},function(t,e,n){"use strict";var r=n(2),o=n(12),i=n(7),s=n(3),c=n(14),u=n(68).KEY,a=n(11),f=n(28),l=n(24),p=n(18),h=n(1),v=n(47),d=n(46),y=n(69),m=n(37),b=n(4),g=n(9),x=n(16),w=n(30),S=n(19),O=n(38),_=n(73),j=n(54),P=n(6),k=n(20),E=j.f,M=P.f,T=_.f,A=r.Symbol,I=r.JSON,L=I&&I.stringify,R=h("_hidden"),C=h("toPrimitive"),F={}.propertyIsEnumerable,N=f("symbol-registry"),D=f("symbols"),U=f("op-symbols"),H=Object.prototype,W="function"==typeof A,q=r.QObject,X=!q||!q.prototype||!q.prototype.findChild,B=i&&a(function(){return 7!=O(M({},"a",{get:function(){return M(this,"a",{value:7}).a}})).a})?function(t,e,n){var r=E(H,e);r&&delete H[e],M(t,e,n),r&&t!==H&&M(H,e,r)}:M,z=function(t){var e=D[t]=O(A.prototype);return e._k=t,e},V=W&&"symbol"==typeof A.iterator?function(t){return"symbol"==typeof t}:function(t){return t instanceof A},G=function(t,e,n){return t===H&&G(U,e,n),b(t),e=w(e,!0),b(n),o(D,e)?(n.enumerable?(o(t,R)&&t[R][e]&&(t[R][e]=!1),n=O(n,{enumerable:S(0,!1)})):(o(t,R)||M(t,R,S(1,{})),t[R][e]=!0),B(t,e,n)):M(t,e,n)},Q=function(t,e){b(t);for(var n,r=y(e=x(e)),o=0,i=r.length;i>o;)G(t,n=r[o++],e[n]);return t},J=function(t){var e=F.call(this,t=w(t,!0));return!(this===H&&o(D,t)&&!o(U,t))&&(!(e||!o(this,t)||!o(D,t)||o(this,R)&&this[R][t])||e)},Y=function(t,e){if(t=x(t),e=w(e,!0),t!==H||!o(D,e)||o(U,e)){var n=E(t,e);return!n||!o(D,e)||o(t,R)&&t[R][e]||(n.enumerable=!0),n}},Z=function(t){for(var e,n=T(x(t)),r=[],i=0;n.length>i;)o(D,e=n[i++])||e==R||e==u||r.push(e);return r},K=function(t){for(var e,n=t===H,r=T(n?U:x(t)),i=[],s=0;r.length>s;)!o(D,e=r[s++])||n&&!o(H,e)||i.push(D[e]);return i};W||(c((A=function(){if(this instanceof A)throw TypeError("Symbol is not a constructor!");var t=p(arguments.length>0?arguments[0]:void 0),e=function(n){this===H&&e.call(U,n),o(this,R)&&o(this[R],t)&&(this[R][t]=!1),B(this,t,S(1,n))};return i&&X&&B(H,t,{configurable:!0,set:e}),z(t)}).prototype,"toString",function(){return this._k}),j.f=Y,P.f=G,n(53).f=_.f=Z,n(36).f=J,n(51).f=K,i&&!n(17)&&c(H,"propertyIsEnumerable",J,!0),v.f=function(t){return z(h(t))}),s(s.G+s.W+s.F*!W,{Symbol:A});for(var $="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),tt=0;$.length>tt;)h($[tt++]);for(var et=k(h.store),nt=0;et.length>nt;)d(et[nt++]);s(s.S+s.F*!W,"Symbol",{for:function(t){return o(N,t+="")?N[t]:N[t]=A(t)},keyFor:function(t){if(!V(t))throw TypeError(t+" is not a symbol!");for(var e in N)if(N[e]===t)return e},useSetter:function(){X=!0},useSimple:function(){X=!1}}),s(s.S+s.F*!W,"Object",{create:function(t,e){return void 0===e?O(t):Q(O(t),e)},defineProperty:G,defineProperties:Q,getOwnPropertyDescriptor:Y,getOwnPropertyNames:Z,getOwnPropertySymbols:K}),I&&s(s.S+s.F*(!W||a(function(){var t=A();return"[null]"!=L([t])||"{}"!=L({a:t})||"{}"!=L(Object(t))})),"JSON",{stringify:function(t){for(var e,n,r=[t],o=1;arguments.length>o;)r.push(arguments[o++]);if(n=e=r[1],(g(e)||void 0!==t)&&!V(t))return m(e)||(e=function(t,e){if("function"==typeof n&&(e=n.call(this,t,e)),!V(e))return e}),r[1]=e,L.apply(I,r)}}),A.prototype[C]||n(13)(A.prototype,C,A.prototype.valueOf),l(A,"Symbol"),l(Math,"Math",!0),l(r.JSON,"JSON",!0)},function(t,e){t.exports=function(t){if(null==t)throw TypeError("Can't call method on "+t);return t}},function(t,e){var n=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:n)(t)}},function(t,e,n){var r=n(28)("keys"),o=n(18);t.exports=function(t){return r[t]||(r[t]=o(t))}},function(t,e){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},function(t,e){e.f={}.propertyIsEnumerable},function(t,e,n){var r=n(21);t.exports=Array.isArray||function(t){return"Array"==r(t)}},function(t,e,n){var r=n(4),o=n(72),i=n(35),s=n(34)("IE_PROTO"),c=function(){},u=function(){var t,e=n(29)("iframe"),r=i.length;for(e.style.display="none",n(52).appendChild(e),e.src="javascript:",(t=e.contentWindow.document).open(),t.write("