├── .eslintignore ├── .gitignore ├── .npmignore ├── .prettierrc ├── static └── demo_header.gif ├── src ├── demo │ ├── img │ │ └── img_grid_square.jpg │ ├── App │ │ ├── style.scss │ │ └── index.jsx │ ├── index.jsx │ ├── index.html │ └── css │ │ └── w3.css ├── PinchToZoom │ ├── Rect.ts │ ├── Size.ts │ ├── Point.ts │ └── index.tsx └── __tests__ │ └── algebra.test.ts ├── docs ├── 02b60ba3c2907be57661b44268edab64.jpg ├── index.html └── index.1460d702fface38da93a.js ├── .babelrc ├── .vscode └── settings.json ├── tslint.json ├── tsconfig.json ├── .eslintrc.json ├── webpack.prod.js ├── server.js ├── webpack.dev.js ├── webpack.common.js ├── LICENSE ├── README.md ├── package.json └── CONTRIBUTING.md /.eslintignore: -------------------------------------------------------------------------------- 1 | lib/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | /lib 3 | /local 4 | /dist -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # npm only need the transpiled js file located under ./lib 2 | docs 3 | src 4 | .babelrc -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": false, 4 | "singleQuote": true 5 | } -------------------------------------------------------------------------------- /static/demo_header.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conradlo/react-pinch-and-zoom/HEAD/static/demo_header.gif -------------------------------------------------------------------------------- /src/demo/img/img_grid_square.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conradlo/react-pinch-and-zoom/HEAD/src/demo/img/img_grid_square.jpg -------------------------------------------------------------------------------- /docs/02b60ba3c2907be57661b44268edab64.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/conradlo/react-pinch-and-zoom/HEAD/docs/02b60ba3c2907be57661b44268edab64.jpg -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/env", "@babel/react"], 3 | "env": { 4 | "development": { 5 | "plugins": ["react-hot-loader/babel"] 6 | } 7 | } 8 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.eslintIntegration": true, 3 | "prettier.tslintIntegration": true, 4 | "eslint.autoFixOnSave": true, 5 | "tslint.autoFixOnSave": true, 6 | } -------------------------------------------------------------------------------- /src/demo/App/style.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: sans-serif; 3 | // background: #e00; 4 | } 5 | 6 | #app { 7 | width: 100%; 8 | height: 100%; 9 | } 10 | 11 | img { 12 | width: 100%; 13 | } -------------------------------------------------------------------------------- /src/demo/index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { render } from 'react-dom' 3 | 4 | import App from './App' 5 | import './css/w3.css' 6 | 7 | console.log('[src/demo/script.js] ', document.body.clientWidth) // eslint-disable-line no-console 8 | render(, document.getElementById('app')) 9 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "tslint:latest", 4 | "tslint-eslint-rules", 5 | "tslint-config-prettier" 6 | ], 7 | "rules": { 8 | "interface-name": [false], 9 | "no-unsafe-any": true, 10 | "no-null-keyword": true, 11 | "object-literal-sort-keys": false, 12 | "no-implicit-dependencies": [true, ["src"]] 13 | } 14 | } -------------------------------------------------------------------------------- /src/PinchToZoom/Rect.ts: -------------------------------------------------------------------------------- 1 | import { isEqual as isEqualPoint, Point } from './Point' 2 | import { isEqual as isEqualSize, Size } from './Size' 3 | 4 | interface Rect { 5 | origin: Point 6 | size: Size 7 | } 8 | 9 | function isEqual(m: Rect, n: Rect): boolean { 10 | return isEqualPoint(m.origin, n.origin) && isEqualSize(m.size, n.size) 11 | } 12 | 13 | export { Rect, isEqual } 14 | -------------------------------------------------------------------------------- /src/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Pinch and Zoom 10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | React Pinch and Zoom 10 | 11 | 12 |
13 | 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "strict": true, 5 | "declaration": true, 6 | "module": "commonjs", 7 | "moduleResolution": "node", 8 | "lib": ["dom", "es2015"], 9 | "target": "es5", 10 | "jsx": "react", 11 | "sourceMap": true, 12 | "noImplicitAny": true, 13 | "removeComments": true, 14 | "preserveConstEnums": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "noImplicitReturns": true, 17 | "outDir": "./lib", 18 | }, 19 | "include": [ 20 | "src/PinchToZoom/*" 21 | ], 22 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module", 6 | "ecmaFeatures": { 7 | "modules": true, 8 | "ecmaFeatures": { 9 | "jsx": true 10 | } 11 | } 12 | }, 13 | "env": { 14 | "browser": true, 15 | "node": true 16 | }, 17 | "settings": { 18 | "react": { 19 | "version": "16.0" 20 | } 21 | }, 22 | "plugins": [ 23 | "react" 24 | ], 25 | "extends": [ 26 | "eslint:recommended", 27 | "plugin:prettier/recommended", 28 | "plugin:react/recommended" 29 | ] 30 | } -------------------------------------------------------------------------------- /webpack.prod.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const CleanWebpackPlugin = require('clean-webpack-plugin') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const merge = require('webpack-merge') 5 | const common = require('./webpack.common.js') 6 | 7 | module.exports = merge(common, { 8 | mode: 'production', 9 | devtool: 'source-map', 10 | entry: { 11 | docs: path.resolve(__dirname, 'src/demo'), 12 | }, 13 | output: { 14 | path: path.resolve(__dirname, 'docs'), 15 | filename: 'index.[hash].js', 16 | }, 17 | plugins: [ 18 | new CleanWebpackPlugin(['docs']), 19 | new HtmlWebpackPlugin({ 20 | template: path.resolve(__dirname, 'src/demo/index.html'), 21 | }), 22 | ], 23 | }) 24 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const morgan = require('morgan') 3 | 4 | const webpack = require('webpack') 5 | const webpackDevMiddleware = require('webpack-dev-middleware') 6 | const webpackHotMiddleware = require('webpack-hot-middleware') 7 | const webpackConfigDev = require('./webpack.dev.js') 8 | 9 | const compiler = webpack(webpackConfigDev) 10 | 11 | const app = express() 12 | app.use(morgan('tiny')) 13 | 14 | // Tell express to use the webpack-dev-middleware and use the webpack.config.js 15 | // configuration file as a base. 16 | app.use(webpackDevMiddleware(compiler)) 17 | 18 | app.use(webpackHotMiddleware(compiler)) 19 | 20 | // Serve the files on port 3000. 21 | app.listen(3000, function() { 22 | console.log('app listening on port 3000\n') 23 | }) 24 | -------------------------------------------------------------------------------- /src/demo/App/index.jsx: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { hot } from 'react-hot-loader' 3 | import './style.scss' 4 | 5 | import PinchToZoom from 'PinchToZoom/index.tsx' 6 | import sq from 'demo/img/img_grid_square.jpg' 7 | 8 | class App extends Component { 9 | render() { 10 | return ( 11 |
12 |
13 |

React Pinch and Zoom

14 |
15 |
16 | 17 | 18 | 19 |
20 | ) 21 | } 22 | } 23 | 24 | App.propTypes = {} 25 | 26 | App.defaultProps = {} 27 | 28 | export default hot(module)(App) 29 | -------------------------------------------------------------------------------- /webpack.dev.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const merge = require('webpack-merge') 3 | const HtmlWebpackPlugin = require('html-webpack-plugin') 4 | const webpack = require('webpack') 5 | const common = require('./webpack.common.js') 6 | 7 | module.exports = merge(common, { 8 | mode: 'development', 9 | devtool: 'inline-source-map', 10 | devServer: { 11 | contentBase: './tmp', 12 | }, 13 | output: { 14 | path: path.resolve(__dirname, 'tmp'), 15 | filename: 'index.[hash].js', 16 | }, 17 | entry: [path.resolve(__dirname, 'src/demo'), 'webpack-hot-middleware/client'], 18 | plugins: [ 19 | new HtmlWebpackPlugin({ 20 | template: path.resolve(__dirname, 'src/demo/index.html'), 21 | }), 22 | new webpack.HotModuleReplacementPlugin(), 23 | ], 24 | }) 25 | -------------------------------------------------------------------------------- /src/PinchToZoom/Size.ts: -------------------------------------------------------------------------------- 1 | import { Point } from './Point' 2 | interface Size { 3 | width: number 4 | height: number 5 | } 6 | 7 | function map(k: Size, f: (n: number) => number): Size { 8 | return { 9 | width: f(k.width), 10 | height: f(k.height), 11 | } 12 | } 13 | 14 | function scale(k: Size, s: number): Size { 15 | return map(k, v => v * s) 16 | } 17 | 18 | function diff(m: Size, n: Size): Size { 19 | return { 20 | width: m.width - n.width, 21 | height: m.height - n.height, 22 | } 23 | } 24 | 25 | function isEqual(m: Size, n: Size): boolean { 26 | return m.width === n.width && m.height === n.height 27 | } 28 | 29 | function toPoint(s: Size): Point { 30 | return { 31 | x: s.width, 32 | y: s.height 33 | } 34 | } 35 | 36 | export { Size, scale, diff, isEqual, toPoint } 37 | -------------------------------------------------------------------------------- /webpack.common.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | module: { 5 | rules: [ 6 | { 7 | test: /\.(js|jsx)$/, 8 | use: ['babel-loader'], 9 | exclude: /node_modules/, 10 | }, 11 | { 12 | test: /\.tsx?$/, 13 | use: 'awesome-typescript-loader', 14 | exclude: /node_modules/, 15 | }, 16 | { 17 | test: /\.css$/, 18 | use: ['style-loader', 'css-loader'], 19 | }, 20 | { 21 | test: /\.scss$/, 22 | use: ['style-loader', 'css-loader', 'sass-loader'], 23 | }, 24 | { 25 | test: /\.(png|svg|jpg|gif)$/, 26 | use: ['file-loader'], 27 | }, 28 | ], 29 | }, 30 | resolve: { 31 | modules: [path.resolve(__dirname, 'src'), 'node_modules'], 32 | extensions: ['.tsx', '.ts', '.js', '.jsx'], 33 | }, 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Conrad Lo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/__tests__/algebra.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | boundWithin, 3 | distance, 4 | midpoint, 5 | } from '../PinchToZoom/Point' 6 | 7 | test('test distance', () => { 8 | const testcases = [ 9 | { 10 | input: [{ x: 3, y: 0 }, { x: 0, y: 4 }], 11 | expected: 5, 12 | }, 13 | { 14 | input: [{ x: -3, y: 2 }, { x: 3, y: 5 }], 15 | expected: 3 * Math.sqrt(5), 16 | }, 17 | { 18 | input: [{ x: -1, y: -1 }, { x: 4, y: -5 }], 19 | expected: Math.sqrt(41), 20 | }, 21 | ] 22 | 23 | for (const t of testcases) { 24 | const { 25 | input: [p1, p2], 26 | expected, 27 | } = t 28 | expect(distance(p1, p2)).toEqual(expected) 29 | } 30 | }) 31 | 32 | test('test midpoint', () => { 33 | const testcases = [ 34 | { 35 | input: [{ x: 3, y: 0 }, { x: 0, y: 4 }], 36 | expected: { x: 1.5, y: 2 }, 37 | } 38 | ] 39 | 40 | for (const t of testcases) { 41 | const { 42 | input: [p1, p2], 43 | expected, 44 | } = t 45 | expect(midpoint(p1, p2)).toEqual(expected) 46 | } 47 | }) 48 | 49 | test('test boundWithin', () => { 50 | const testcases = [ 51 | { 52 | input: [{ x: 0, y: 0 }, { x: 0, y: 0 }, {x: 0, y: 0}], 53 | expected: { x: 0, y: 0 }, 54 | }, 55 | { 56 | input: [{ x: 0, y: 0 }, { x: -10, y: -10 }, {x: 0, y: 0}], 57 | expected: { x: 0, y: 0 }, 58 | }, 59 | { 60 | input: [{ x: -100, y: -100 }, { x: -10, y: -10 }, {x: 0, y: 0}], 61 | expected: { x: -10, y: -10 }, 62 | } 63 | ] 64 | 65 | for (const t of testcases) { 66 | const { 67 | input: [lower, target, upper], 68 | expected, 69 | } = t 70 | expect(boundWithin(lower, target, upper)).toEqual(expected) 71 | } 72 | }) 73 | -------------------------------------------------------------------------------- /src/PinchToZoom/Point.ts: -------------------------------------------------------------------------------- 1 | import { Rect } from './Rect' 2 | import { Size } from './Size' 3 | 4 | interface Point { 5 | x: number 6 | y: number 7 | } 8 | 9 | function newOriginPoint(): Point { 10 | return { 11 | x: 0, 12 | y: 0 13 | } 14 | } 15 | 16 | function distance(p1: Point, p2: Point): number { 17 | // Pythagorean Theorem: c^2 = a^2 + b^2 18 | const { x: x1, y: y1 } = p1 19 | const { x: x2, y: y2 } = p2 20 | return Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)) 21 | } 22 | 23 | function midpoint(p1: Point, p2: Point): Point { 24 | return { 25 | x: (p1.x + p2.x) / 2, 26 | y: (p1.y + p2.y) / 2, 27 | } 28 | } 29 | 30 | function offset(p: Point, origin: Point): Point { 31 | return { 32 | x: p.x - origin.x, 33 | y: p.y - origin.y, 34 | } 35 | } 36 | 37 | function map(p: Point, f: (n: number) => number): Point { 38 | return { 39 | x: f(p.x), 40 | y: f(p.y), 41 | } 42 | } 43 | 44 | function scale(p: Point, s: number): Point { 45 | return map(p, v => v * s) 46 | } 47 | 48 | function sum(p1: Point, p2: Point): Point { 49 | return { 50 | x: p1.x + p2.x, 51 | y: p1.y + p2.y, 52 | } 53 | } 54 | 55 | function isEqual(p1: Point, p2: Point): boolean { 56 | return p1.x === p2.x && p1.y === p2.y 57 | } 58 | 59 | function boundWithin(min: Point, current: Point, max: Point): Point { 60 | const numberWithin = (lower: number, num: number, upper: number) => { 61 | return num > upper ? upper : lower > num ? lower : num 62 | } 63 | return { 64 | x: numberWithin(min.x, current.x, max.x), 65 | y: numberWithin(min.y, current.y, max.y), 66 | } 67 | } 68 | 69 | function normalizePointInRect(point: Point, rect: Rect) { 70 | return { 71 | x: point.x - rect.origin.x, 72 | y: point.y - rect.origin.y, 73 | } 74 | } 75 | 76 | function toSize(p: Point): Size { 77 | return { 78 | width: p.x, 79 | height: p.y 80 | } 81 | } 82 | 83 | export { 84 | Point, 85 | newOriginPoint, 86 | distance, 87 | midpoint, 88 | offset, 89 | sum, 90 | map, 91 | scale, 92 | isEqual, 93 | boundWithin, 94 | toSize, 95 | normalizePointInRect, 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ![license](https://img.shields.io/github/license/mashape/apistatus.svg?style=flat-square) 3 | 4 | # React Pinch and Zoom 5 | A react container component with pinch-to-zoom gesture interaction. 6 | 7 |

8 | 9 |

10 | 11 | Demo 12 | 13 | 14 | ## Getting Started 15 | 16 | 17 | 18 | 19 | ### Installing 20 | 21 | 1. Install this package as dependency 22 | 23 | ```shell 24 | # pwd: ~/project/dir 25 | $ npm install react-pinch-and-zoom 26 | ``` 27 | 28 | 2. Import the component 29 | 30 | ```jsx 31 | import PinchToZoom from 'react-pinch-and-zoom'; 32 | ``` 33 | 3. Wrap the pinch-able component inside `PinchToZoom` component 34 | 35 | ```jsx 36 | render() { 37 | return ( 38 |
39 | 40 | // child node should have intrinsic size 41 | 42 |
43 | ); 44 | } 45 | ``` 46 | 47 | ## Project structure 48 | 49 | ```shell 50 | ./react-pinch-and-zoom (master) 51 | ├── docs // compiled github demo page 52 | ├── lib // compiled react component in commonjs module (git ignored) 53 | ├── package.json 54 | ├── server.js // config local development server 55 | └── src 56 |    ├── PinchToZoom // source code of react-pinch-and-zoom 57 |    └── demo // source code of github demo page 58 | ``` 59 | 60 | ## Develop on local machine 61 | 62 | 1. Pull this repository 63 | ```shell 64 | # pwd: ~/development/dir 65 | $ git pull https://github.com/conradlo/react-pinch-and-zoom.git 66 | ``` 67 | 1. Install dependency 68 | ```shell 69 | $ cd react-pinch-and-zoom 70 | # pwd: ~/development/dir/react-pinch-and-zoom 71 | $ npm install 72 | ``` 73 | 1. Start local development server 74 | ```shell 75 | # pwd: ~/development/dir/react-pinch-and-zoom 76 | $ npm start 77 | ``` 78 | 2. Visit `localhost:3000` and edit `src/PinchToZoom/*` 79 | 3. Consult `package.json` for more npm script tasks 80 | 81 | ## Production build 82 | 83 | run `npm run build` will: 84 | 85 | 1. transpile the component's source code `/lib` 86 | 2. build and bundle the Github page `/docs` 87 | 88 | ```shell 89 | # pwd: ~/development/dir/react-pinch-and-zoom 90 | $ npm run build 91 | ``` 92 | 93 | ## Built With 94 | 95 | * [Reactjs](https://reactjs.org/) - A JavaScript library for building user interfaces 96 | * [Sass](https://sass-lang.com/) - Syntactically Awesome Style Sheets 97 | * [Webpack](https://webpack.js.org/) - JavaScript module bundler 98 | * [Babel](http://babeljs.io/) - JavaScript transpiler/compiler 99 | * [Typescript](https://www.typescriptlang.org/) - For extra type safety 100 | * [eslint](https://eslint.org/), [tslint](https://palantir.github.io/tslint/), [prettier](https://prettier.io/), [vscode](https://code.visualstudio.com/) 101 | 102 | ## Contributing 103 | 104 | Please refer to [CONTRIBUTING.md](https://github.com/conradlo/react-pinch-and-zoom/blob/master/CONTRIBUTING.md) for details on code of conduct, and the process for submitting pull requests. 105 | 106 | ## Versioning 107 | 108 | This project uses [SemVer](http://semver.org/) for versioning. For the versions available, see the [tags on this repository](https://github.com/conradlo/react-pinch-and-zoom/tags). 109 | 110 | ## License 111 | 112 | see the [LICENSE](https://github.com/conradlo/react-pinch-and-zoom/blob/master/LICENSE) file for details 113 | 114 | ## Acknowledgments 115 | 116 | * https://gist.github.com/PurpleBooth/109311bb0361f32d87a2 117 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-pinch-and-zoom", 3 | "version": "1.2.5", 4 | "author": "Conrad Lo", 5 | "description": "A react container component with pinch-to-zoom gesture interaction.", 6 | "keywords": [ 7 | "react", 8 | "reactjs", 9 | "pinch", 10 | "zoom", 11 | "pan" 12 | ], 13 | "files": [ 14 | "lib/" 15 | ], 16 | "main": "lib/index.js", 17 | "types": "lib/index.d.ts", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/conradlo/react-pinch-and-zoom" 21 | }, 22 | "homepage": "https://github.com/conradlo/react-pinch-and-zoom#readme", 23 | "bugs": "https://github.com/conradlo/react-pinch-and-zoom/issues", 24 | "license": "MIT", 25 | "scripts": { 26 | "start": "node server.js", 27 | "test": "jest", 28 | "test:watch": "jest --watch", 29 | "lint": "eslint ./src --ext .js,.jsx && tslint --project .", 30 | "lint:fix": "eslint --fix ./src --ext .js,.jsx && tslint --project . --fix", 31 | "clear:lib": "rimraf lib/", 32 | "clear:docs": "rimraf docs/", 33 | "clear:all": "npm run clear:lib && npm run clear:docs", 34 | "build:lib": "tsc --project .", 35 | "build:docs": "webpack --config webpack.prod.js", 36 | "build:all": "npm run build:lib && npm run build:docs", 37 | "build": "npm run lint:fix && npm run test && npm run clear:all && npm run build:all", 38 | "version": "npm run build && git add .", 39 | "prepublish": "npm run build" 40 | }, 41 | "devDependencies": { 42 | "@babel/core": "^7.3.4", 43 | "@babel/preset-env": "^7.3.4", 44 | "@babel/preset-react": "^7.0.0", 45 | "@types/jest": "^24.0.9", 46 | "@types/react": "^16.8.5", 47 | "@types/react-dom": "^16.8.2", 48 | "awesome-typescript-loader": "^5.2.1", 49 | "babel-cli": "^6.26.0", 50 | "babel-eslint": "^10.0.1", 51 | "babel-loader": "^8.0.5", 52 | "clean-webpack-plugin": "^1.0.1", 53 | "css-loader": "^2.1.0", 54 | "eslint": "^5.14.1", 55 | "eslint-config-prettier": "^4.1.0", 56 | "eslint-loader": "^2.1.2", 57 | "eslint-plugin-prettier": "^3.0.1", 58 | "eslint-plugin-react": "^7.12.4", 59 | "express": "^4.16.4", 60 | "file-loader": "^3.0.1", 61 | "html-webpack-plugin": "^3.2.0", 62 | "jest": "^24.1.0", 63 | "morgan": "^1.9.1", 64 | "node-sass": "^4.13.1", 65 | "prettier": "1.14.3", 66 | "prop-types": "^15.7.2", 67 | "react": "^16.8.3", 68 | "react-dom": "^16.8.3", 69 | "react-hot-loader": "^4.7.1", 70 | "rimraf": "^2.6.3", 71 | "sass-loader": "^7.1.0", 72 | "style-loader": "^0.21.0", 73 | "ts-jest": "^24.0.0", 74 | "tslint": "^5.13.0", 75 | "tslint-config-prettier": "^1.18.0", 76 | "tslint-eslint-rules": "^5.4.0", 77 | "typescript": "^3.3.3333", 78 | "webpack": "^4.29.5", 79 | "webpack-cli": "^3.2.3", 80 | "webpack-dev-middleware": "^3.6.0", 81 | "webpack-hot-middleware": "^2.24.3", 82 | "webpack-merge": "^4.2.1" 83 | }, 84 | "peerDependencies": { 85 | "react": "^16.6.0", 86 | "react-dom": "^16.6.0", 87 | "prop-types": "^15.6.2" 88 | }, 89 | "dependencies": {}, 90 | "jest": { 91 | "testEnvironment": "node", 92 | "roots": [ 93 | "src/" 94 | ], 95 | "moduleFileExtensions": [ 96 | "js", 97 | "jsx", 98 | "json", 99 | "ts", 100 | "tsx" 101 | ], 102 | "moduleDirectories": [ 103 | "node_modules", 104 | "./" 105 | ], 106 | "transform": { 107 | "^.+\\.(ts|tsx)$": "ts-jest", 108 | "^.+\\.(js|jsx)$": "babel-jest" 109 | }, 110 | "globals": { 111 | "ts-jest": { 112 | "tsConfig": "tsconfig.json" 113 | } 114 | }, 115 | "testMatch": [ 116 | "**/__tests__/*.+(ts|tsx|js)" 117 | ] 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | In the interest of fostering an open and welcoming environment, we as 24 | contributors and maintainers pledge to making participation in our project and 25 | our community a harassment-free experience for everyone, regardless of age, body 26 | size, disability, ethnicity, gender identity and expression, level of experience, 27 | nationality, personal appearance, race, religion, or sexual identity and 28 | orientation. 29 | 30 | ### Our Standards 31 | 32 | Examples of behavior that contributes to creating a positive environment 33 | include: 34 | 35 | * Using welcoming and inclusive language 36 | * Being respectful of differing viewpoints and experiences 37 | * Gracefully accepting constructive criticism 38 | * Focusing on what is best for the community 39 | * Showing empathy towards other community members 40 | 41 | Examples of unacceptable behavior by participants include: 42 | 43 | * The use of sexualized language or imagery and unwelcome sexual attention or 44 | advances 45 | * Trolling, insulting/derogatory comments, and personal or political attacks 46 | * Public or private harassment 47 | * Publishing others' private information, such as a physical or electronic 48 | address, without explicit permission 49 | * Other conduct which could reasonably be considered inappropriate in a 50 | professional setting 51 | 52 | ### Our Responsibilities 53 | 54 | Project maintainers are responsible for clarifying the standards of acceptable 55 | behavior and are expected to take appropriate and fair corrective action in 56 | response to any instances of unacceptable behavior. 57 | 58 | Project maintainers have the right and responsibility to remove, edit, or 59 | reject comments, commits, code, wiki edits, issues, and other contributions 60 | that are not aligned to this Code of Conduct, or to ban temporarily or 61 | permanently any contributor for other behaviors that they deem inappropriate, 62 | threatening, offensive, or harmful. 63 | 64 | ### Scope 65 | 66 | This Code of Conduct applies both within project spaces and in public spaces 67 | when an individual is representing the project or its community. Examples of 68 | representing a project or community include using an official project e-mail 69 | address, posting via an official social media account, or acting as an appointed 70 | representative at an online or offline event. Representation of a project may be 71 | further defined and clarified by project maintainers. 72 | 73 | ### Enforcement 74 | 75 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 76 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 77 | complaints will be reviewed and investigated and will result in a response that 78 | is deemed necessary and appropriate to the circumstances. The project team is 79 | obligated to maintain confidentiality with regard to the reporter of an incident. 80 | Further details of specific enforcement policies may be posted separately. 81 | 82 | Project maintainers who do not follow or enforce the Code of Conduct in good 83 | faith may face temporary or permanent repercussions as determined by other 84 | members of the project's leadership. 85 | 86 | ### Attribution 87 | 88 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 89 | available at [http://contributor-covenant.org/version/1/4][version] 90 | 91 | [homepage]: http://contributor-covenant.org 92 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /src/PinchToZoom/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * PinchToZoom react componenets 3 | */ 4 | 5 | import * as PropTypes from 'prop-types' 6 | import * as React from 'react' 7 | import * as Point from './Point' 8 | import * as Size from './Size' 9 | 10 | const truncRound = (num: number): number => Math.trunc(num * 10000) / 10000 11 | 12 | enum GUESTURE_TYPE { 13 | UNSET = 'GUESTURE_TYPE_UNSET', 14 | PAN = 'GUESTURE_TYPE_PAN', 15 | PINCH = 'GUESTURE_TYPE_PINCH', 16 | } 17 | 18 | interface PinchToZoomProps { 19 | debug: boolean 20 | className: string 21 | minZoomScale: number 22 | maxZoomScale: number 23 | boundSize: Size.Size 24 | contentSize: Size.Size 25 | fillContainer?: boolean 26 | 27 | /** Callback to run each time transform is updated */ 28 | onTransform?: ( 29 | transform: { 30 | zoomFactor: number 31 | translate: Point.Point 32 | } 33 | ) => void 34 | } 35 | 36 | interface PinchToZoomState { 37 | lastSingleTouchPoint: Point.Point 38 | } 39 | 40 | class PinchToZoom extends React.Component { 41 | public static defaultProps: {} 42 | public static propTypes: {} 43 | public static getTouchesCoordinate( 44 | syntheticEvent: React.SyntheticEvent 45 | ): Point.Point[] { 46 | /** 47 | * adjust browser touch point coordinate to bounds 48 | */ 49 | const { 50 | currentTarget: { parentNode }, 51 | nativeEvent, 52 | } = syntheticEvent 53 | // DOM node 54 | if ( 55 | !(parentNode instanceof HTMLElement) || 56 | !(nativeEvent instanceof TouchEvent) 57 | ) { 58 | return [] 59 | } 60 | const containerRect = parentNode.getBoundingClientRect() 61 | const rect = { 62 | origin: { x: containerRect.left, y: containerRect.top }, 63 | size: { 64 | width: containerRect.width, 65 | height: containerRect.height, 66 | }, 67 | } 68 | // DOM touch list 69 | const { touches: touchList } = nativeEvent 70 | const coordinates = [] // [{x1, y1}, {x2, y2}...] 71 | for (let i = 0; i < touchList.length; i += 1) { 72 | const touch = touchList.item(i) 73 | if (touch) { 74 | const touchPoint = { 75 | x: touch.clientX, 76 | y: touch.clientY, 77 | } 78 | const p = Point.normalizePointInRect(touchPoint, rect) 79 | coordinates.push(p) 80 | } 81 | } 82 | return coordinates 83 | } 84 | 85 | public transform: { 86 | zoomFactor: number 87 | translate: Point.Point 88 | } 89 | 90 | public currentGesture: GUESTURE_TYPE 91 | 92 | public pinchStartZoomFactor: number 93 | public pinchStartTouchMidpoint: Point.Point 94 | public pinchStartTranslate: Point.Point 95 | public pinchStartTouchPointDist: number 96 | 97 | public panStartPoint: Point.Point 98 | public panStartTranslate: Point.Point 99 | 100 | public zoomAreaContainer?: HTMLDivElement 101 | public zoomArea?: HTMLDivElement 102 | 103 | constructor(props: PinchToZoomProps) { 104 | super(props) 105 | 106 | // instance variable: transform data 107 | this.transform = { 108 | zoomFactor: 1.0, 109 | translate: Point.newOriginPoint(), 110 | } 111 | 112 | // instance variable: guesture 113 | this.currentGesture = GUESTURE_TYPE.UNSET 114 | 115 | // instance variable: pinch 116 | this.pinchStartZoomFactor = 1.0 117 | this.pinchStartTouchMidpoint = Point.newOriginPoint() 118 | this.pinchStartTranslate = Point.newOriginPoint() 119 | this.pinchStartTouchPointDist = 0 120 | 121 | // instance variable: pan 122 | this.panStartPoint = Point.newOriginPoint() 123 | this.panStartTranslate = Point.newOriginPoint() 124 | 125 | // record last touch point 126 | this.state = { 127 | lastSingleTouchPoint: Point.newOriginPoint(), 128 | } 129 | } 130 | 131 | public componentDidUpdate(prevProps: PinchToZoomProps) { 132 | if ( 133 | prevProps.minZoomScale !== this.props.minZoomScale || 134 | prevProps.boundSize.height !== this.props.boundSize.height 135 | ) { 136 | this.zoomContentArea(this.props.minZoomScale) 137 | this.guardZoomAreaTranslate() 138 | } 139 | } 140 | 141 | public componentDidMount(): void { 142 | setTimeout(() => { 143 | const startEvent = new TouchEvent('touchstart', { 144 | cancelable: true, 145 | bubbles: true, 146 | }) 147 | const endEvent = new TouchEvent('touchend', { 148 | cancelable: true, 149 | bubbles: true, 150 | }) 151 | if (this.zoomAreaContainer) { 152 | this.zoomAreaContainer.dispatchEvent(startEvent) 153 | this.zoomAreaContainer.dispatchEvent(endEvent) 154 | } 155 | }, 0) 156 | } 157 | 158 | /* 159 | Pinch event handlers 160 | */ 161 | 162 | public onPinchStart(syntheticEvent: React.SyntheticEvent) { 163 | const [p1, p2] = PinchToZoom.getTouchesCoordinate(syntheticEvent) 164 | 165 | // on pinch start remember the mid point of 2 touch points 166 | this.pinchStartTouchMidpoint = Point.midpoint(p1, p2) 167 | 168 | // on pinch start remember the distance of 2 touch points 169 | this.pinchStartTouchPointDist = Point.distance(p1, p2) 170 | 171 | /* 172 | on pinch start, remember the `origianl zoom factor` 173 | & `origianl plan translate` before pinching 174 | */ 175 | const { currentZoomFactor, currentTranslate } = this.getTransform() 176 | this.pinchStartZoomFactor = currentZoomFactor 177 | this.pinchStartTranslate = currentTranslate 178 | } 179 | 180 | public onPinchMove(syntheticEvent: React.SyntheticEvent) { 181 | // get lastest touch point coordinate 182 | const [p1, p2] = PinchToZoom.getTouchesCoordinate(syntheticEvent) 183 | 184 | // const pinchCurrentTouchMidpoint = SeatingPlan.calculateMidpoint({ x1, y1 }, { x2, y2 }); 185 | 186 | const pinchCurrentTouchPointDist = Point.distance(p1, p2) 187 | 188 | // delta > 0: enlarge(zoon in), delta < 0: diminish(zoom out) 189 | const deltaTouchPointDist = 190 | pinchCurrentTouchPointDist - this.pinchStartTouchPointDist 191 | 192 | // update zoom factor 193 | const newZoomFactor = this.pinchStartZoomFactor + deltaTouchPointDist * 0.01 194 | this.zoomContentArea(newZoomFactor) 195 | } 196 | 197 | public onPinchEnd() { 198 | this.guardZoomAreaScale() 199 | this.guardZoomAreaTranslate() 200 | } 201 | 202 | /* 203 | Pan event handlers 204 | */ 205 | public onPanStart(syntheticEvent: React.SyntheticEvent) { 206 | const [p1] = PinchToZoom.getTouchesCoordinate(syntheticEvent) 207 | const { currentTranslate } = this.getTransform() 208 | 209 | this.panStartPoint = p1 210 | this.panStartTranslate = currentTranslate 211 | } 212 | 213 | public onPanMove(syntheticEvent: React.SyntheticEvent) { 214 | const [dragPoint] = PinchToZoom.getTouchesCoordinate(syntheticEvent) 215 | const { currentZoomFactor } = this.getTransform() 216 | const origin = this.panStartPoint 217 | const prevTranslate = this.panStartTranslate 218 | 219 | const dragOffset = Point.offset(dragPoint, origin) 220 | const adjustedZoomOffset = Point.scale(dragOffset, 1 / currentZoomFactor) 221 | const nextTranslate = Point.sum(adjustedZoomOffset, prevTranslate) 222 | this.panContentArea(nextTranslate) 223 | } 224 | 225 | public onPanEnd() { 226 | this.guardZoomAreaTranslate() 227 | } 228 | 229 | /* validate zoom factor value */ 230 | public guardZoomAreaScale() { 231 | const { currentZoomFactor } = this.getTransform() 232 | const { minZoomScale, maxZoomScale } = this.props 233 | if (currentZoomFactor > maxZoomScale) { 234 | this.zoomContentArea(maxZoomScale) 235 | } else if (currentZoomFactor < minZoomScale) { 236 | this.zoomContentArea(minZoomScale) 237 | } 238 | } 239 | 240 | /* validate translate value */ 241 | public guardZoomAreaTranslate() { 242 | if (!this.zoomAreaContainer || !this.zoomArea) { 243 | return 244 | } 245 | const { currentZoomFactor, currentTranslate } = this.getTransform() 246 | const { minZoomScale } = this.props 247 | const { 248 | clientWidth: containerW, 249 | clientHeight: containerH, 250 | } = this.zoomAreaContainer 251 | const { clientWidth: contentW, clientHeight: contentH } = this.zoomArea 252 | if (currentZoomFactor < minZoomScale) { 253 | return 254 | } 255 | 256 | // container size 257 | const boundSize = { 258 | width: containerW, 259 | height: containerH, 260 | } 261 | 262 | // content size adjusted to zoom factor 263 | const contentSize = Size.scale( 264 | { 265 | width: contentW, 266 | height: contentH, 267 | }, 268 | currentZoomFactor 269 | ) 270 | 271 | const diff = Size.diff(boundSize, contentSize) 272 | const diffInPoint = Size.toPoint(diff) 273 | 274 | const unitScaleLeftTopPoint = Point.scale( 275 | diffInPoint, 276 | 1 / (2 * currentZoomFactor) 277 | ) 278 | 279 | const maxLeftTopPoint = Point.boundWithin( 280 | Point.newOriginPoint(), 281 | unitScaleLeftTopPoint, 282 | Point.map(unitScaleLeftTopPoint, truncRound) 283 | ) 284 | 285 | const unitScaleRightBottomPoint = Point.scale( 286 | diffInPoint, 287 | 1 / currentZoomFactor 288 | ) 289 | 290 | const maxRightBottomPoint = { 291 | x: Math.min(unitScaleRightBottomPoint.x, maxLeftTopPoint.x), 292 | y: Math.min(unitScaleRightBottomPoint.y, maxLeftTopPoint.y), 293 | } 294 | 295 | const validatePos = Point.boundWithin( 296 | maxRightBottomPoint, 297 | currentTranslate, 298 | maxLeftTopPoint 299 | ) 300 | 301 | if (!Point.isEqual(validatePos, currentTranslate)) { 302 | this.panContentArea(validatePos) 303 | } 304 | } 305 | 306 | /* perform pan transfrom */ 307 | public panContentArea(pos: Point.Point) { 308 | this.setTransform({ 309 | translate: pos, 310 | }) 311 | } 312 | 313 | /* perform zooming transfrom */ 314 | public zoomContentArea(zoomFactor: number) { 315 | if (!this.zoomAreaContainer || !this.zoomArea) { 316 | return 317 | } 318 | // calculate delta translate needed 319 | const prevZoomFactor = this.pinchStartZoomFactor 320 | const prevTranslate = this.pinchStartTranslate 321 | const { 322 | clientWidth: containerW, 323 | clientHeight: containerH, 324 | } = this.zoomAreaContainer 325 | 326 | const boundSize = { 327 | width: containerW, 328 | height: containerH, 329 | } 330 | 331 | const prevZoomSize = Size.scale(boundSize, prevZoomFactor) 332 | const nextZoomSize = Size.scale(boundSize, zoomFactor) 333 | 334 | const prevRectCenterPoint = { 335 | x: prevZoomSize.width / 2, 336 | y: prevZoomSize.height / 2, 337 | } 338 | 339 | const nextRectCenterPoint = { 340 | x: nextZoomSize.width / 2, 341 | y: nextZoomSize.height / 2, 342 | } 343 | 344 | const deltaTranslate = Point.scale( 345 | Point.offset(prevRectCenterPoint, nextRectCenterPoint), 346 | 1 / (zoomFactor * prevZoomFactor) 347 | ) 348 | 349 | const accumulateTranslate = Point.sum(deltaTranslate, prevTranslate) 350 | 351 | // update zoom scale and corresponding translate 352 | this.setTransform({ 353 | zoomFactor: truncRound(zoomFactor), 354 | translate: accumulateTranslate, 355 | }) 356 | } 357 | 358 | /* 359 | event handlers 360 | */ 361 | 362 | public handleTouchStart(syntheticEvent: React.SyntheticEvent) { 363 | if (!this.zoomAreaContainer || !this.zoomArea) { 364 | return 365 | } 366 | const { nativeEvent } = syntheticEvent 367 | if (!(nativeEvent instanceof TouchEvent)) { 368 | return 369 | } 370 | this.zoomArea.style.transitionDuration = '0.0s' 371 | // 2 touches == pinch, else all considered as pan 372 | switch (nativeEvent.touches.length) { 373 | case 2: 374 | this.currentGesture = GUESTURE_TYPE.PINCH 375 | this.onPinchStart(syntheticEvent) 376 | break 377 | default: { 378 | /* don't allow pan if zoom factor === minZoomScale */ 379 | const [p1] = PinchToZoom.getTouchesCoordinate(syntheticEvent) 380 | this.setState({ lastSingleTouchPoint: p1 }) 381 | this.currentGesture = GUESTURE_TYPE.PAN 382 | this.onPanStart(syntheticEvent) 383 | } 384 | } 385 | } 386 | 387 | public handleTouchMove(syntheticEvent: React.SyntheticEvent) { 388 | // 2 touches == pinch, else all considered as pan 389 | const { nativeEvent } = syntheticEvent 390 | if (!(nativeEvent instanceof TouchEvent)) { 391 | return 392 | } 393 | switch (nativeEvent.touches.length) { 394 | case 2: 395 | if (this.currentGesture === GUESTURE_TYPE.PINCH) { 396 | this.onPinchMove(syntheticEvent) 397 | } 398 | break 399 | default: 400 | if (this.currentGesture === GUESTURE_TYPE.PAN) { 401 | this.onPanMove(syntheticEvent) 402 | } 403 | } 404 | } 405 | 406 | public handleTouchEnd(syntheticEvent: React.SyntheticEvent) { 407 | if (!this.zoomAreaContainer || !this.zoomArea) { 408 | return 409 | } 410 | this.zoomArea.style.transitionDuration = '0.3s' 411 | if (this.currentGesture === GUESTURE_TYPE.PINCH) { 412 | this.onPinchEnd() 413 | } 414 | if (this.currentGesture === GUESTURE_TYPE.PAN) { 415 | this.onPanEnd() 416 | } 417 | this.currentGesture = GUESTURE_TYPE.UNSET 418 | } 419 | 420 | public autoZoomToLastTouchPoint() { 421 | const { lastSingleTouchPoint } = this.state 422 | if (lastSingleTouchPoint.x === 0 && lastSingleTouchPoint.y === 0) { 423 | return 424 | } 425 | this.autoZoomToPosition(lastSingleTouchPoint) 426 | } 427 | 428 | // auto zoom 429 | public autoZoomToPosition(pos: Point.Point) { 430 | if (!this.zoomAreaContainer || !this.zoomArea) { 431 | return 432 | } 433 | const autoZoomFactor = 2.0 434 | const { currentZoomFactor, currentTranslate } = this.getTransform() 435 | const zoomAreaContainerW = this.zoomAreaContainer.clientWidth 436 | const zoomAreaContainerH = this.zoomAreaContainer.clientHeight 437 | 438 | // calculate target points with respect to the zoomArea coordinate 439 | // & adjust to current zoomFactor + existing translate 440 | const zoomAreaX = 441 | (pos.x / currentZoomFactor - currentTranslate.x) * autoZoomFactor 442 | const zoomAreaY = 443 | (pos.y / currentZoomFactor - currentTranslate.y) * autoZoomFactor 444 | 445 | // calculate distance to translate the target points to zoomAreaContainer's center 446 | const deltaX = zoomAreaContainerW / 2 - zoomAreaX 447 | const deltaY = zoomAreaContainerH / 2 - zoomAreaY 448 | 449 | // adjust to the new zoomFactor 450 | const inScaleTranslate = { 451 | x: deltaX / autoZoomFactor, 452 | y: deltaY / autoZoomFactor, 453 | } 454 | 455 | // update zoom scale and corresponding translate 456 | this.zoomArea.style.transitionDuration = '0.3s' 457 | this.setTransform({ 458 | zoomFactor: autoZoomFactor, 459 | translate: { 460 | x: inScaleTranslate.x, 461 | y: inScaleTranslate.y, 462 | }, 463 | }) 464 | this.guardZoomAreaTranslate() 465 | } 466 | 467 | /* 468 | update zoom area transform 469 | */ 470 | public setTransform({ 471 | zoomFactor = this.transform.zoomFactor, 472 | translate = { 473 | x: this.transform.translate.x, 474 | y: this.transform.translate.y, 475 | }, 476 | } = {}) { 477 | const { onTransform, minZoomScale } = this.props 478 | 479 | if (!this.zoomAreaContainer || !this.zoomArea) { 480 | return 481 | } 482 | const roundTransalteX = Math.round(translate.x * 1000) / 1000 483 | const roundTransalteY = Math.round(translate.y * 1000) / 1000 484 | 485 | // don't allow zoomFactor smaller then this.props.minZoomScale * 0.8 486 | if (zoomFactor < minZoomScale * 0.8) { 487 | return 488 | } 489 | 490 | // update the lastest transform value 491 | this.transform.zoomFactor = zoomFactor 492 | this.transform.translate.x = roundTransalteX 493 | this.transform.translate.y = roundTransalteY 494 | 495 | // run optional onTransform callback 496 | if (onTransform) { 497 | onTransform(this.transform) 498 | } 499 | 500 | // update the transform style 501 | const styleString = ` 502 | scale(${zoomFactor}) 503 | translate(${roundTransalteX}px, ${roundTransalteY}px) 504 | translateZ(${0}) 505 | ` 506 | 507 | this.zoomArea.style.transform = styleString 508 | this.zoomArea.style.webkitTransform = styleString 509 | } 510 | 511 | /* 512 | get a *copy* of current zoom area transformation value 513 | */ 514 | public getTransform() { 515 | const { zoomFactor, translate } = this.transform 516 | return { 517 | currentZoomFactor: zoomFactor, 518 | currentTranslate: { 519 | x: translate.x, 520 | y: translate.y, 521 | }, 522 | } 523 | } 524 | 525 | /* 526 | React render 527 | */ 528 | 529 | public render() { 530 | const { debug, fillContainer, className, children } = this.props 531 | 532 | const classNameList = ['', 'pinch-to-zoom-container'] 533 | 534 | const containerInlineStyle = { 535 | display: 'inline-block', 536 | overflow: 'hidden', 537 | backgroundColor: 'inherit', 538 | width: fillContainer ? '100%' : undefined, 539 | height: fillContainer ? '100%' : undefined, 540 | } 541 | 542 | const zoomAreaInlineStyle = { 543 | display: 'inline-block', 544 | willChange: 'transform', 545 | transformOrigin: '0px 0px 0px', 546 | transition: 'transform 0ms ease', 547 | transitionTimingFunction: 'cubic-bezier(0.1, 0.57, 0.1, 1)', 548 | transitionDuration: '0ms', 549 | perspective: 1000, 550 | width: '100%', // match `pinch-to-zoom-container` width 551 | } 552 | 553 | if (debug) { 554 | classNameList.push('debug') 555 | containerInlineStyle.backgroundColor = 'red' 556 | } 557 | 558 | return ( 559 |
this.handleTouchStart(e)} 563 | onTouchMove={e => this.handleTouchMove(e)} 564 | onTouchEnd={e => this.handleTouchEnd(e)} 565 | ref={c => { 566 | this.zoomAreaContainer = c || undefined 567 | }} 568 | > 569 |
{ 573 | this.zoomArea = c || undefined 574 | }} 575 | > 576 | {children} 577 |
578 |
579 | ) 580 | } 581 | } 582 | 583 | PinchToZoom.defaultProps = { 584 | debug: false, 585 | className: '', 586 | minZoomScale: 1.0, 587 | maxZoomScale: 4.0, 588 | boundSize: { 589 | width: 100, 590 | height: 100, 591 | }, 592 | contentSize: { 593 | width: 100, 594 | height: 100, 595 | }, 596 | fillContainer: false, 597 | } 598 | 599 | PinchToZoom.propTypes = { 600 | debug: PropTypes.bool, 601 | className: PropTypes.string, 602 | minZoomScale: PropTypes.number, 603 | maxZoomScale: PropTypes.number, 604 | boundSize: PropTypes.shape({ 605 | // bound size is the out touch area size 606 | // the width should match device's width e.g. 320 for iphone 5 607 | width: PropTypes.number, // eslint-disable-line 608 | height: PropTypes.number, // eslint-disable-line 609 | }), 610 | contentSize: PropTypes.shape({ 611 | // content size is the inner content initial size 612 | // the width should match the inner content element's width when scale is 1 613 | width: PropTypes.number, // eslint-disable-line 614 | height: PropTypes.number, // eslint-disable-line 615 | }), 616 | fillContainer: PropTypes.bool, 617 | children: PropTypes.node, 618 | } 619 | 620 | export default PinchToZoom 621 | -------------------------------------------------------------------------------- /src/demo/css/w3.css: -------------------------------------------------------------------------------- 1 | /* W3.CSS 4.10 February 2018 by Jan Egil and Borge Refsnes */ 2 | html{box-sizing:border-box}*,*:before,*:after{box-sizing:inherit} 3 | /* Extract from normalize.css by Nicolas Gallagher and Jonathan Neal git.io/normalize */ 4 | html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0} 5 | article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block} 6 | audio,canvas,progress,video{display:inline-block}progress{vertical-align:baseline} 7 | audio:not([controls]){display:none;height:0}[hidden],template{display:none} 8 | a{background-color:transparent;-webkit-text-decoration-skip:objects} 9 | a:active,a:hover{outline-width:0}abbr[title]{border-bottom:none;text-decoration:underline;text-decoration:underline dotted} 10 | dfn{font-style:italic}mark{background:#ff0;color:#000} 11 | small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline} 12 | sub{bottom:-0.25em}sup{top:-0.5em}figure{margin:1em 40px}img{border-style:none}svg:not(:root){overflow:hidden} 13 | code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}hr{box-sizing:content-box;height:0;overflow:visible} 14 | button,input,select,textarea{font:inherit;margin:0}optgroup{font-weight:bold} 15 | button,input{overflow:visible}button,select{text-transform:none} 16 | button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button} 17 | button::-moz-focus-inner, [type=button]::-moz-focus-inner, [type=reset]::-moz-focus-inner, [type=submit]::-moz-focus-inner{border-style:none;padding:0} 18 | button:-moz-focusring, [type=button]:-moz-focusring, [type=reset]:-moz-focusring, [type=submit]:-moz-focusring{outline:1px dotted ButtonText} 19 | fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:.35em .625em .75em} 20 | legend{color:inherit;display:table;max-width:100%;padding:0;white-space:normal}textarea{overflow:auto} 21 | [type=checkbox],[type=radio]{padding:0} 22 | [type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto} 23 | [type=search]{-webkit-appearance:textfield;outline-offset:-2px} 24 | [type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none} 25 | ::-webkit-input-placeholder{color:inherit;opacity:0.54} 26 | ::-webkit-file-upload-button{-webkit-appearance:button;font:inherit} 27 | /* End extract */ 28 | html,body{font-family:Verdana,sans-serif;font-size:15px;line-height:1.5}html{overflow-x:hidden} 29 | h1{font-size:36px}h2{font-size:30px}h3{font-size:24px}h4{font-size:20px}h5{font-size:18px}h6{font-size:16px}.w3-serif{font-family:serif} 30 | h1,h2,h3,h4,h5,h6{font-family:"Segoe UI",Arial,sans-serif;font-weight:400;margin:10px 0}.w3-wide{letter-spacing:4px} 31 | hr{border:0;border-top:1px solid #eee;margin:20px 0} 32 | .w3-image{max-width:100%;height:auto}img{vertical-align:middle}a{color:inherit} 33 | .w3-table,.w3-table-all{border-collapse:collapse;border-spacing:0;width:100%;display:table}.w3-table-all{border:1px solid #ccc} 34 | .w3-bordered tr,.w3-table-all tr{border-bottom:1px solid #ddd}.w3-striped tbody tr:nth-child(even){background-color:#f1f1f1} 35 | .w3-table-all tr:nth-child(odd){background-color:#fff}.w3-table-all tr:nth-child(even){background-color:#f1f1f1} 36 | .w3-hoverable tbody tr:hover,.w3-ul.w3-hoverable li:hover{background-color:#ccc}.w3-centered tr th,.w3-centered tr td{text-align:center} 37 | .w3-table td,.w3-table th,.w3-table-all td,.w3-table-all th{padding:8px 8px;display:table-cell;text-align:left;vertical-align:top} 38 | .w3-table th:first-child,.w3-table td:first-child,.w3-table-all th:first-child,.w3-table-all td:first-child{padding-left:16px} 39 | .w3-btn,.w3-button{border:none;display:inline-block;padding:8px 16px;vertical-align:middle;overflow:hidden;text-decoration:none;color:inherit;background-color:inherit;text-align:center;cursor:pointer;white-space:nowrap} 40 | .w3-btn:hover{box-shadow:0 8px 16px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19)} 41 | .w3-btn,.w3-button{-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none} 42 | .w3-disabled,.w3-btn:disabled,.w3-button:disabled{cursor:not-allowed;opacity:0.3}.w3-disabled *,:disabled *{pointer-events:none} 43 | .w3-btn.w3-disabled:hover,.w3-btn:disabled:hover{box-shadow:none} 44 | .w3-badge,.w3-tag{background-color:#000;color:#fff;display:inline-block;padding-left:8px;padding-right:8px;text-align:center}.w3-badge{border-radius:50%} 45 | .w3-ul{list-style-type:none;padding:0;margin:0}.w3-ul li{padding:8px 16px;border-bottom:1px solid #ddd}.w3-ul li:last-child{border-bottom:none} 46 | .w3-tooltip,.w3-display-container{position:relative}.w3-tooltip .w3-text{display:none}.w3-tooltip:hover .w3-text{display:inline-block} 47 | .w3-ripple:active{opacity:0.5}.w3-ripple{transition:opacity 0s} 48 | .w3-input{padding:8px;display:block;border:none;border-bottom:1px solid #ccc;width:100%} 49 | .w3-select{padding:9px 0;width:100%;border:none;border-bottom:1px solid #ccc} 50 | .w3-dropdown-click,.w3-dropdown-hover{position:relative;display:inline-block;cursor:pointer} 51 | .w3-dropdown-hover:hover .w3-dropdown-content{display:block} 52 | .w3-dropdown-hover:first-child,.w3-dropdown-click:hover{background-color:#ccc;color:#000} 53 | .w3-dropdown-hover:hover > .w3-button:first-child,.w3-dropdown-click:hover > .w3-button:first-child{background-color:#ccc;color:#000} 54 | .w3-dropdown-content{cursor:auto;color:#000;background-color:#fff;display:none;position:absolute;min-width:160px;margin:0;padding:0;z-index:1} 55 | .w3-check,.w3-radio{width:24px;height:24px;position:relative;top:6px} 56 | .w3-sidebar{height:100%;width:200px;background-color:#fff;position:fixed!important;z-index:1;overflow:auto} 57 | .w3-bar-block .w3-dropdown-hover,.w3-bar-block .w3-dropdown-click{width:100%} 58 | .w3-bar-block .w3-dropdown-hover .w3-dropdown-content,.w3-bar-block .w3-dropdown-click .w3-dropdown-content{min-width:100%} 59 | .w3-bar-block .w3-dropdown-hover .w3-button,.w3-bar-block .w3-dropdown-click .w3-button{width:100%;text-align:left;padding:8px 16px} 60 | .w3-main,#main{transition:margin-left .4s} 61 | .w3-modal{z-index:3;display:none;padding-top:100px;position:fixed;left:0;top:0;width:100%;height:100%;overflow:auto;background-color:rgb(0,0,0);background-color:rgba(0,0,0,0.4)} 62 | .w3-modal-content{margin:auto;background-color:#fff;position:relative;padding:0;outline:0;width:600px} 63 | .w3-bar{width:100%;overflow:hidden}.w3-center .w3-bar{display:inline-block;width:auto} 64 | .w3-bar .w3-bar-item{padding:8px 16px;float:left;width:auto;border:none;display:block;outline:0} 65 | .w3-bar .w3-dropdown-hover,.w3-bar .w3-dropdown-click{position:static;float:left} 66 | .w3-bar .w3-button{white-space:normal} 67 | .w3-bar-block .w3-bar-item{width:100%;display:block;padding:8px 16px;text-align:left;border:none;white-space:normal;float:none;outline:0} 68 | .w3-bar-block.w3-center .w3-bar-item{text-align:center}.w3-block{display:block;width:100%} 69 | .w3-responsive{display:block;overflow-x:auto} 70 | .w3-container:after,.w3-container:before,.w3-panel:after,.w3-panel:before,.w3-row:after,.w3-row:before,.w3-row-padding:after,.w3-row-padding:before, 71 | .w3-cell-row:before,.w3-cell-row:after,.w3-clear:after,.w3-clear:before,.w3-bar:before,.w3-bar:after{content:"";display:table;clear:both} 72 | .w3-col,.w3-half,.w3-third,.w3-twothird,.w3-threequarter,.w3-quarter{float:left;width:100%} 73 | .w3-col.s1{width:8.33333%}.w3-col.s2{width:16.66666%}.w3-col.s3{width:24.99999%}.w3-col.s4{width:33.33333%} 74 | .w3-col.s5{width:41.66666%}.w3-col.s6{width:49.99999%}.w3-col.s7{width:58.33333%}.w3-col.s8{width:66.66666%} 75 | .w3-col.s9{width:74.99999%}.w3-col.s10{width:83.33333%}.w3-col.s11{width:91.66666%}.w3-col.s12{width:99.99999%} 76 | @media (min-width:601px){.w3-col.m1{width:8.33333%}.w3-col.m2{width:16.66666%}.w3-col.m3,.w3-quarter{width:24.99999%}.w3-col.m4,.w3-third{width:33.33333%} 77 | .w3-col.m5{width:41.66666%}.w3-col.m6,.w3-half{width:49.99999%}.w3-col.m7{width:58.33333%}.w3-col.m8,.w3-twothird{width:66.66666%} 78 | .w3-col.m9,.w3-threequarter{width:74.99999%}.w3-col.m10{width:83.33333%}.w3-col.m11{width:91.66666%}.w3-col.m12{width:99.99999%}} 79 | @media (min-width:993px){.w3-col.l1{width:8.33333%}.w3-col.l2{width:16.66666%}.w3-col.l3{width:24.99999%}.w3-col.l4{width:33.33333%} 80 | .w3-col.l5{width:41.66666%}.w3-col.l6{width:49.99999%}.w3-col.l7{width:58.33333%}.w3-col.l8{width:66.66666%} 81 | .w3-col.l9{width:74.99999%}.w3-col.l10{width:83.33333%}.w3-col.l11{width:91.66666%}.w3-col.l12{width:99.99999%}} 82 | .w3-content{max-width:980px;margin:auto}.w3-rest{overflow:hidden} 83 | .w3-cell-row{display:table;width:100%}.w3-cell{display:table-cell} 84 | .w3-cell-top{vertical-align:top}.w3-cell-middle{vertical-align:middle}.w3-cell-bottom{vertical-align:bottom} 85 | .w3-hide{display:none!important}.w3-show-block,.w3-show{display:block!important}.w3-show-inline-block{display:inline-block!important} 86 | @media (max-width:600px){.w3-modal-content{margin:0 10px;width:auto!important}.w3-modal{padding-top:30px} 87 | .w3-dropdown-hover.w3-mobile .w3-dropdown-content,.w3-dropdown-click.w3-mobile .w3-dropdown-content{position:relative} 88 | .w3-hide-small{display:none!important}.w3-mobile{display:block;width:100%!important}.w3-bar-item.w3-mobile,.w3-dropdown-hover.w3-mobile,.w3-dropdown-click.w3-mobile{text-align:center} 89 | .w3-dropdown-hover.w3-mobile,.w3-dropdown-hover.w3-mobile .w3-btn,.w3-dropdown-hover.w3-mobile .w3-button,.w3-dropdown-click.w3-mobile,.w3-dropdown-click.w3-mobile .w3-btn,.w3-dropdown-click.w3-mobile .w3-button{width:100%}} 90 | @media (max-width:768px){.w3-modal-content{width:500px}.w3-modal{padding-top:50px}} 91 | @media (min-width:993px){.w3-modal-content{width:900px}.w3-hide-large{display:none!important}.w3-sidebar.w3-collapse{display:block!important}} 92 | @media (max-width:992px) and (min-width:601px){.w3-hide-medium{display:none!important}} 93 | @media (max-width:992px){.w3-sidebar.w3-collapse{display:none}.w3-main{margin-left:0!important;margin-right:0!important}} 94 | .w3-top,.w3-bottom{position:fixed;width:100%;z-index:1}.w3-top{top:0}.w3-bottom{bottom:0} 95 | .w3-overlay{position:fixed;display:none;width:100%;height:100%;top:0;left:0;right:0;bottom:0;background-color:rgba(0,0,0,0.5);z-index:2} 96 | .w3-display-topleft{position:absolute;left:0;top:0}.w3-display-topright{position:absolute;right:0;top:0} 97 | .w3-display-bottomleft{position:absolute;left:0;bottom:0}.w3-display-bottomright{position:absolute;right:0;bottom:0} 98 | .w3-display-middle{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);-ms-transform:translate(-50%,-50%)} 99 | .w3-display-left{position:absolute;top:50%;left:0%;transform:translate(0%,-50%);-ms-transform:translate(-0%,-50%)} 100 | .w3-display-right{position:absolute;top:50%;right:0%;transform:translate(0%,-50%);-ms-transform:translate(0%,-50%)} 101 | .w3-display-topmiddle{position:absolute;left:50%;top:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} 102 | .w3-display-bottommiddle{position:absolute;left:50%;bottom:0;transform:translate(-50%,0%);-ms-transform:translate(-50%,0%)} 103 | .w3-display-container:hover .w3-display-hover{display:block}.w3-display-container:hover span.w3-display-hover{display:inline-block}.w3-display-hover{display:none} 104 | .w3-display-position{position:absolute} 105 | .w3-circle{border-radius:50%} 106 | .w3-round-small{border-radius:2px}.w3-round,.w3-round-medium{border-radius:4px}.w3-round-large{border-radius:8px}.w3-round-xlarge{border-radius:16px}.w3-round-xxlarge{border-radius:32px} 107 | .w3-row-padding,.w3-row-padding>.w3-half,.w3-row-padding>.w3-third,.w3-row-padding>.w3-twothird,.w3-row-padding>.w3-threequarter,.w3-row-padding>.w3-quarter,.w3-row-padding>.w3-col{padding:0 8px} 108 | .w3-container,.w3-panel{padding:0.01em 16px}.w3-panel{margin-top:16px;margin-bottom:16px} 109 | .w3-code,.w3-codespan{font-family:Consolas,"courier new";font-size:16px} 110 | .w3-code{width:auto;background-color:#fff;padding:8px 12px;border-left:4px solid #4CAF50;word-wrap:break-word} 111 | .w3-codespan{color:crimson;background-color:#f1f1f1;padding-left:4px;padding-right:4px;font-size:110%} 112 | .w3-card,.w3-card-2{box-shadow:0 2px 5px 0 rgba(0,0,0,0.16),0 2px 10px 0 rgba(0,0,0,0.12)} 113 | .w3-card-4,.w3-hover-shadow:hover{box-shadow:0 4px 10px 0 rgba(0,0,0,0.2),0 4px 20px 0 rgba(0,0,0,0.19)} 114 | .w3-spin{animation:w3-spin 2s infinite linear}@keyframes w3-spin{0%{transform:rotate(0deg)}100%{transform:rotate(359deg)}} 115 | .w3-animate-fading{animation:fading 10s infinite}@keyframes fading{0%{opacity:0}50%{opacity:1}100%{opacity:0}} 116 | .w3-animate-opacity{animation:opac 0.8s}@keyframes opac{from{opacity:0} to{opacity:1}} 117 | .w3-animate-top{position:relative;animation:animatetop 0.4s}@keyframes animatetop{from{top:-300px;opacity:0} to{top:0;opacity:1}} 118 | .w3-animate-left{position:relative;animation:animateleft 0.4s}@keyframes animateleft{from{left:-300px;opacity:0} to{left:0;opacity:1}} 119 | .w3-animate-right{position:relative;animation:animateright 0.4s}@keyframes animateright{from{right:-300px;opacity:0} to{right:0;opacity:1}} 120 | .w3-animate-bottom{position:relative;animation:animatebottom 0.4s}@keyframes animatebottom{from{bottom:-300px;opacity:0} to{bottom:0;opacity:1}} 121 | .w3-animate-zoom {animation:animatezoom 0.6s}@keyframes animatezoom{from{transform:scale(0)} to{transform:scale(1)}} 122 | .w3-animate-input{transition:width 0.4s ease-in-out}.w3-animate-input:focus{width:100%!important} 123 | .w3-opacity,.w3-hover-opacity:hover{opacity:0.60}.w3-opacity-off,.w3-hover-opacity-off:hover{opacity:1} 124 | .w3-opacity-max{opacity:0.25}.w3-opacity-min{opacity:0.75} 125 | .w3-greyscale-max,.w3-grayscale-max,.w3-hover-greyscale:hover,.w3-hover-grayscale:hover{filter:grayscale(100%)} 126 | .w3-greyscale,.w3-grayscale{filter:grayscale(75%)}.w3-greyscale-min,.w3-grayscale-min{filter:grayscale(50%)} 127 | .w3-sepia{filter:sepia(75%)}.w3-sepia-max,.w3-hover-sepia:hover{filter:sepia(100%)}.w3-sepia-min{filter:sepia(50%)} 128 | .w3-tiny{font-size:10px!important}.w3-small{font-size:12px!important}.w3-medium{font-size:15px!important}.w3-large{font-size:18px!important} 129 | .w3-xlarge{font-size:24px!important}.w3-xxlarge{font-size:36px!important}.w3-xxxlarge{font-size:48px!important}.w3-jumbo{font-size:64px!important} 130 | .w3-left-align{text-align:left!important}.w3-right-align{text-align:right!important}.w3-justify{text-align:justify!important}.w3-center{text-align:center!important} 131 | .w3-border-0{border:0!important}.w3-border{border:1px solid #ccc!important} 132 | .w3-border-top{border-top:1px solid #ccc!important}.w3-border-bottom{border-bottom:1px solid #ccc!important} 133 | .w3-border-left{border-left:1px solid #ccc!important}.w3-border-right{border-right:1px solid #ccc!important} 134 | .w3-topbar{border-top:6px solid #ccc!important}.w3-bottombar{border-bottom:6px solid #ccc!important} 135 | .w3-leftbar{border-left:6px solid #ccc!important}.w3-rightbar{border-right:6px solid #ccc!important} 136 | .w3-section,.w3-code{margin-top:16px!important;margin-bottom:16px!important} 137 | .w3-margin{margin:16px!important}.w3-margin-top{margin-top:16px!important}.w3-margin-bottom{margin-bottom:16px!important} 138 | .w3-margin-left{margin-left:16px!important}.w3-margin-right{margin-right:16px!important} 139 | .w3-padding-small{padding:4px 8px!important}.w3-padding{padding:8px 16px!important}.w3-padding-large{padding:12px 24px!important} 140 | .w3-padding-16{padding-top:16px!important;padding-bottom:16px!important}.w3-padding-24{padding-top:24px!important;padding-bottom:24px!important} 141 | .w3-padding-32{padding-top:32px!important;padding-bottom:32px!important}.w3-padding-48{padding-top:48px!important;padding-bottom:48px!important} 142 | .w3-padding-64{padding-top:64px!important;padding-bottom:64px!important} 143 | .w3-left{float:left!important}.w3-right{float:right!important} 144 | .w3-button:hover{color:#000!important;background-color:#ccc!important} 145 | .w3-transparent,.w3-hover-none:hover{background-color:transparent!important} 146 | .w3-hover-none:hover{box-shadow:none!important} 147 | /* Colors */ 148 | .w3-amber,.w3-hover-amber:hover{color:#000!important;background-color:#ffc107!important} 149 | .w3-aqua,.w3-hover-aqua:hover{color:#000!important;background-color:#00ffff!important} 150 | .w3-blue,.w3-hover-blue:hover{color:#fff!important;background-color:#2196F3!important} 151 | .w3-light-blue,.w3-hover-light-blue:hover{color:#000!important;background-color:#87CEEB!important} 152 | .w3-brown,.w3-hover-brown:hover{color:#fff!important;background-color:#795548!important} 153 | .w3-cyan,.w3-hover-cyan:hover{color:#000!important;background-color:#00bcd4!important} 154 | .w3-blue-grey,.w3-hover-blue-grey:hover,.w3-blue-gray,.w3-hover-blue-gray:hover{color:#fff!important;background-color:#607d8b!important} 155 | .w3-green,.w3-hover-green:hover{color:#fff!important;background-color:#4CAF50!important} 156 | .w3-light-green,.w3-hover-light-green:hover{color:#000!important;background-color:#8bc34a!important} 157 | .w3-indigo,.w3-hover-indigo:hover{color:#fff!important;background-color:#3f51b5!important} 158 | .w3-khaki,.w3-hover-khaki:hover{color:#000!important;background-color:#f0e68c!important} 159 | .w3-lime,.w3-hover-lime:hover{color:#000!important;background-color:#cddc39!important} 160 | .w3-orange,.w3-hover-orange:hover{color:#000!important;background-color:#ff9800!important} 161 | .w3-deep-orange,.w3-hover-deep-orange:hover{color:#fff!important;background-color:#ff5722!important} 162 | .w3-pink,.w3-hover-pink:hover{color:#fff!important;background-color:#e91e63!important} 163 | .w3-purple,.w3-hover-purple:hover{color:#fff!important;background-color:#9c27b0!important} 164 | .w3-deep-purple,.w3-hover-deep-purple:hover{color:#fff!important;background-color:#673ab7!important} 165 | .w3-red,.w3-hover-red:hover{color:#fff!important;background-color:#f44336!important} 166 | .w3-sand,.w3-hover-sand:hover{color:#000!important;background-color:#fdf5e6!important} 167 | .w3-teal,.w3-hover-teal:hover{color:#fff!important;background-color:#009688!important} 168 | .w3-yellow,.w3-hover-yellow:hover{color:#000!important;background-color:#ffeb3b!important} 169 | .w3-white,.w3-hover-white:hover{color:#000!important;background-color:#fff!important} 170 | .w3-black,.w3-hover-black:hover{color:#fff!important;background-color:#000!important} 171 | .w3-grey,.w3-hover-grey:hover,.w3-gray,.w3-hover-gray:hover{color:#000!important;background-color:#9e9e9e!important} 172 | .w3-light-grey,.w3-hover-light-grey:hover,.w3-light-gray,.w3-hover-light-gray:hover{color:#000!important;background-color:#f1f1f1!important} 173 | .w3-dark-grey,.w3-hover-dark-grey:hover,.w3-dark-gray,.w3-hover-dark-gray:hover{color:#fff!important;background-color:#616161!important} 174 | .w3-pale-red,.w3-hover-pale-red:hover{color:#000!important;background-color:#ffdddd!important} 175 | .w3-pale-green,.w3-hover-pale-green:hover{color:#000!important;background-color:#ddffdd!important} 176 | .w3-pale-yellow,.w3-hover-pale-yellow:hover{color:#000!important;background-color:#ffffcc!important} 177 | .w3-pale-blue,.w3-hover-pale-blue:hover{color:#000!important;background-color:#ddffff!important} 178 | .w3-text-amber,.w3-hover-text-amber:hover{color:#ffc107!important} 179 | .w3-text-aqua,.w3-hover-text-aqua:hover{color:#00ffff!important} 180 | .w3-text-blue,.w3-hover-text-blue:hover{color:#2196F3!important} 181 | .w3-text-light-blue,.w3-hover-text-light-blue:hover{color:#87CEEB!important} 182 | .w3-text-brown,.w3-hover-text-brown:hover{color:#795548!important} 183 | .w3-text-cyan,.w3-hover-text-cyan:hover{color:#00bcd4!important} 184 | .w3-text-blue-grey,.w3-hover-text-blue-grey:hover,.w3-text-blue-gray,.w3-hover-text-blue-gray:hover{color:#607d8b!important} 185 | .w3-text-green,.w3-hover-text-green:hover{color:#4CAF50!important} 186 | .w3-text-light-green,.w3-hover-text-light-green:hover{color:#8bc34a!important} 187 | .w3-text-indigo,.w3-hover-text-indigo:hover{color:#3f51b5!important} 188 | .w3-text-khaki,.w3-hover-text-khaki:hover{color:#b4aa50!important} 189 | .w3-text-lime,.w3-hover-text-lime:hover{color:#cddc39!important} 190 | .w3-text-orange,.w3-hover-text-orange:hover{color:#ff9800!important} 191 | .w3-text-deep-orange,.w3-hover-text-deep-orange:hover{color:#ff5722!important} 192 | .w3-text-pink,.w3-hover-text-pink:hover{color:#e91e63!important} 193 | .w3-text-purple,.w3-hover-text-purple:hover{color:#9c27b0!important} 194 | .w3-text-deep-purple,.w3-hover-text-deep-purple:hover{color:#673ab7!important} 195 | .w3-text-red,.w3-hover-text-red:hover{color:#f44336!important} 196 | .w3-text-sand,.w3-hover-text-sand:hover{color:#fdf5e6!important} 197 | .w3-text-teal,.w3-hover-text-teal:hover{color:#009688!important} 198 | .w3-text-yellow,.w3-hover-text-yellow:hover{color:#d2be0e!important} 199 | .w3-text-white,.w3-hover-text-white:hover{color:#fff!important} 200 | .w3-text-black,.w3-hover-text-black:hover{color:#000!important} 201 | .w3-text-grey,.w3-hover-text-grey:hover,.w3-text-gray,.w3-hover-text-gray:hover{color:#757575!important} 202 | .w3-text-light-grey,.w3-hover-text-light-grey:hover,.w3-text-light-gray,.w3-hover-text-light-gray:hover{color:#f1f1f1!important} 203 | .w3-text-dark-grey,.w3-hover-text-dark-grey:hover,.w3-text-dark-gray,.w3-hover-text-dark-gray:hover{color:#3a3a3a!important} 204 | .w3-border-amber,.w3-hover-border-amber:hover{border-color:#ffc107!important} 205 | .w3-border-aqua,.w3-hover-border-aqua:hover{border-color:#00ffff!important} 206 | .w3-border-blue,.w3-hover-border-blue:hover{border-color:#2196F3!important} 207 | .w3-border-light-blue,.w3-hover-border-light-blue:hover{border-color:#87CEEB!important} 208 | .w3-border-brown,.w3-hover-border-brown:hover{border-color:#795548!important} 209 | .w3-border-cyan,.w3-hover-border-cyan:hover{border-color:#00bcd4!important} 210 | .w3-border-blue-grey,.w3-hover-border-blue-grey:hover,.w3-border-blue-gray,.w3-hover-border-blue-gray:hover{border-color:#607d8b!important} 211 | .w3-border-green,.w3-hover-border-green:hover{border-color:#4CAF50!important} 212 | .w3-border-light-green,.w3-hover-border-light-green:hover{border-color:#8bc34a!important} 213 | .w3-border-indigo,.w3-hover-border-indigo:hover{border-color:#3f51b5!important} 214 | .w3-border-khaki,.w3-hover-border-khaki:hover{border-color:#f0e68c!important} 215 | .w3-border-lime,.w3-hover-border-lime:hover{border-color:#cddc39!important} 216 | .w3-border-orange,.w3-hover-border-orange:hover{border-color:#ff9800!important} 217 | .w3-border-deep-orange,.w3-hover-border-deep-orange:hover{border-color:#ff5722!important} 218 | .w3-border-pink,.w3-hover-border-pink:hover{border-color:#e91e63!important} 219 | .w3-border-purple,.w3-hover-border-purple:hover{border-color:#9c27b0!important} 220 | .w3-border-deep-purple,.w3-hover-border-deep-purple:hover{border-color:#673ab7!important} 221 | .w3-border-red,.w3-hover-border-red:hover{border-color:#f44336!important} 222 | .w3-border-sand,.w3-hover-border-sand:hover{border-color:#fdf5e6!important} 223 | .w3-border-teal,.w3-hover-border-teal:hover{border-color:#009688!important} 224 | .w3-border-yellow,.w3-hover-border-yellow:hover{border-color:#ffeb3b!important} 225 | .w3-border-white,.w3-hover-border-white:hover{border-color:#fff!important} 226 | .w3-border-black,.w3-hover-border-black:hover{border-color:#000!important} 227 | .w3-border-grey,.w3-hover-border-grey:hover,.w3-border-gray,.w3-hover-border-gray:hover{border-color:#9e9e9e!important} 228 | .w3-border-light-grey,.w3-hover-border-light-grey:hover,.w3-border-light-gray,.w3-hover-border-light-gray:hover{border-color:#f1f1f1!important} 229 | .w3-border-dark-grey,.w3-hover-border-dark-grey:hover,.w3-border-dark-gray,.w3-hover-border-dark-gray:hover{border-color:#616161!important} 230 | .w3-border-pale-red,.w3-hover-border-pale-red:hover{border-color:#ffe7e7!important}.w3-border-pale-green,.w3-hover-border-pale-green:hover{border-color:#e7ffe7!important} 231 | .w3-border-pale-yellow,.w3-hover-border-pale-yellow:hover{border-color:#ffffcc!important}.w3-border-pale-blue,.w3-hover-border-pale-blue:hover{border-color:#e7ffff!important} -------------------------------------------------------------------------------- /docs/index.1460d702fface38da93a.js: -------------------------------------------------------------------------------- 1 | !function(e){var t={};function n(r){if(t[r])return t[r].exports;var o=t[r]={i:r,l:!1,exports:{}};return e[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}n.m=e,n.c=t,n.d=function(e,t,r){n.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:r})},n.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},n.t=function(e,t){if(1&t&&(e=n(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)n.d(r,o,function(t){return e[t]}.bind(null,o));return r},n.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return n.d(t,"a",t),t},n.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},n.p="",n(n.s=9)}([function(e,t,n){"use strict";e.exports=n(10)},function(e,t,n){"use strict";e.exports=n(16)},function(e,t,n){"use strict"; 2 | /* 3 | object-assign 4 | (c) Sindre Sorhus 5 | @license MIT 6 | */var r=Object.getOwnPropertySymbols,o=Object.prototype.hasOwnProperty,i=Object.prototype.propertyIsEnumerable;function a(e){if(null==e)throw new TypeError("Object.assign cannot be called with null or undefined");return Object(e)}e.exports=function(){try{if(!Object.assign)return!1;var e=new String("abc");if(e[5]="de","5"===Object.getOwnPropertyNames(e)[0])return!1;for(var t={},n=0;n<10;n++)t["_"+String.fromCharCode(n)]=n;if("0123456789"!==Object.getOwnPropertyNames(t).map((function(e){return t[e]})).join(""))return!1;var r={};return"abcdefghijklmnopqrst".split("").forEach((function(e){r[e]=e})),"abcdefghijklmnopqrst"===Object.keys(Object.assign({},r)).join("")}catch(e){return!1}}()?Object.assign:function(e,t){for(var n,l,u=a(e),c=1;c=0&&d.splice(t,1)}function v(e){var t=document.createElement("style");return void 0===e.attrs.type&&(e.attrs.type="text/css"),g(t,e.attrs),h(e,t),t}function g(e,t){Object.keys(t).forEach((function(n){e.setAttribute(n,t[n])}))}function y(e,t){var n,r,o,i;if(t.transform&&e.css){if(!(i=t.transform(e.css)))return function(){};e.css=i}if(t.singleton){var a=s++;n=c||(c=v(t)),r=_.bind(null,n,a,!1),o=_.bind(null,n,a,!0)}else e.sourceMap&&"function"==typeof URL&&"function"==typeof URL.createObjectURL&&"function"==typeof URL.revokeObjectURL&&"function"==typeof Blob&&"function"==typeof btoa?(n=function(e){var t=document.createElement("link");return void 0===e.attrs.type&&(e.attrs.type="text/css"),e.attrs.rel="stylesheet",g(t,e.attrs),h(e,t),t}(t),r=T.bind(null,n,t),o=function(){b(n),n.href&&URL.revokeObjectURL(n.href)}):(n=v(t),r=k.bind(null,n),o=function(){b(n)});return r(e),function(t){if(t){if(t.css===e.css&&t.media===e.media&&t.sourceMap===e.sourceMap)return;r(e=t)}else o()}}e.exports=function(e,t){if("undefined"!=typeof DEBUG&&DEBUG&&"object"!=typeof document)throw new Error("The style-loader cannot be used in a non-browser environment");(t=t||{}).attrs="object"==typeof t.attrs?t.attrs:{},t.singleton||"boolean"==typeof t.singleton||(t.singleton=a()),t.insertInto||(t.insertInto="head"),t.insertAt||(t.insertAt="bottom");var n=m(e,t);return p(n,t),function(e){for(var r=[],o=0;or?this.zoomContentArea(r):eA.length&&A.push(e)}function U(e,t,n){return null==e?0:function e(t,n,r,o){var l=typeof t;"undefined"!==l&&"boolean"!==l||(t=null);var u=!1;if(null===t)u=!0;else switch(l){case"string":case"number":u=!0;break;case"object":switch(t.$$typeof){case i:case a:u=!0}}if(u)return r(o,t,""===n?"."+I(t,0):n),1;if(u=0,n=""===n?".":n+":",Array.isArray(t))for(var c=0;cthis.eventPool.length&&this.eventPool.push(e)}function pe(e){e.eventPool=[],e.getPooled=de,e.release=fe}o(se.prototype,{preventDefault:function(){this.defaultPrevented=!0;var e=this.nativeEvent;e&&(e.preventDefault?e.preventDefault():"unknown"!=typeof e.returnValue&&(e.returnValue=!1),this.isDefaultPrevented=ue)},stopPropagation:function(){var e=this.nativeEvent;e&&(e.stopPropagation?e.stopPropagation():"unknown"!=typeof e.cancelBubble&&(e.cancelBubble=!0),this.isPropagationStopped=ue)},persist:function(){this.isPersistent=ue},isPersistent:ce,destructor:function(){var e,t=this.constructor.Interface;for(e in t)this[e]=null;this.nativeEvent=this._targetInst=this.dispatchConfig=null,this.isPropagationStopped=this.isDefaultPrevented=ce,this._dispatchInstances=this._dispatchListeners=null}}),se.Interface={type:null,target:null,currentTarget:function(){return null},eventPhase:null,bubbles:null,cancelable:null,timeStamp:function(e){return e.timeStamp||Date.now()},defaultPrevented:null,isTrusted:null},se.extend=function(e){function t(){}function n(){return r.apply(this,arguments)}var r=this;t.prototype=r.prototype;var i=new t;return o(i,n.prototype),n.prototype=i,n.prototype.constructor=n,n.Interface=o({},r.Interface,e),n.extend=r.extend,pe(n),n},pe(se);var me=se.extend({data:null}),he=se.extend({data:null}),be=[9,13,27,32],ve=$&&"CompositionEvent"in window,ge=null;$&&"documentMode"in document&&(ge=document.documentMode);var ye=$&&"TextEvent"in window&&!ge,we=$&&(!ve||ge&&8=ge),xe=String.fromCharCode(32),_e={beforeInput:{phasedRegistrationNames:{bubbled:"onBeforeInput",captured:"onBeforeInputCapture"},dependencies:["compositionend","keypress","textInput","paste"]},compositionEnd:{phasedRegistrationNames:{bubbled:"onCompositionEnd",captured:"onCompositionEndCapture"},dependencies:"blur compositionend keydown keypress keyup mousedown".split(" ")},compositionStart:{phasedRegistrationNames:{bubbled:"onCompositionStart",captured:"onCompositionStartCapture"},dependencies:"blur compositionstart keydown keypress keyup mousedown".split(" ")},compositionUpdate:{phasedRegistrationNames:{bubbled:"onCompositionUpdate",captured:"onCompositionUpdateCapture"},dependencies:"blur compositionupdate keydown keypress keyup mousedown".split(" ")}},ke=!1;function Te(e,t){switch(e){case"keyup":return-1!==be.indexOf(t.keyCode);case"keydown":return 229!==t.keyCode;case"keypress":case"mousedown":case"blur":return!0;default:return!1}}function Ee(e){return"object"==typeof(e=e.detail)&&"data"in e?e.data:null}var Ce=!1;var Se={eventTypes:_e,extractEvents:function(e,t,n,r){var o=void 0,i=void 0;if(ve)e:{switch(e){case"compositionstart":o=_e.compositionStart;break e;case"compositionend":o=_e.compositionEnd;break e;case"compositionupdate":o=_e.compositionUpdate;break e}o=void 0}else Ce?Te(e,n)&&(o=_e.compositionEnd):"keydown"===e&&229===n.keyCode&&(o=_e.compositionStart);return o?(we&&"ko"!==n.locale&&(Ce||o!==_e.compositionStart?o===_e.compositionEnd&&Ce&&(i=le()):(ie="value"in(oe=r)?oe.value:oe.textContent,Ce=!0)),o=me.getPooled(o,t,n,r),i?o.data=i:null!==(i=Ee(n))&&(o.data=i),K(o),i=o):i=null,(e=ye?function(e,t){switch(e){case"compositionend":return Ee(t);case"keypress":return 32!==t.which?null:(ke=!0,xe);case"textInput":return(e=t.data)===xe&&ke?null:e;default:return null}}(e,n):function(e,t){if(Ce)return"compositionend"===e||!ve&&Te(e,t)?(e=le(),ae=ie=oe=null,Ce=!1,e):null;switch(e){case"paste":return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1