complete viewer core

This commit is contained in:
infeng 2016-10-15 16:57:30 +08:00
parent 457aaa8178
commit 9ef3b349b0
14 changed files with 427 additions and 279 deletions

4
.gitignore vendored
View File

@ -1 +1,3 @@
node_modules/
node_modules/
dist/
lib/

View File

@ -1,6 +1,6 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import ViewerWrap from '../src/ViewerWrap';
import Viewer from '../src/Viewer';
const img2 = require('./images/landscape2.jpg');
const img = require('./images/landscape.jpg');
const img3 = require('./images/tibet-6.jpg');
@ -18,7 +18,7 @@ class App extends React.Component<any, any> {
return (
<div>
<button onClick={() => { this.setState({ visible: !this.state.visible }); } }>show</button>
<ViewerWrap
<Viewer
visible={this.state.visible}
onClose={() => { this.setState({ visible: false }); } }
images={[img2, img, img3]}

6
getBabelCommonConfig.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = function getBabelCommonConfig() {
const babelConfig = require('atool-build/lib/getBabelCommonConfig')();
babelConfig.plugins.push([require.resolve('babel-plugin-transform-runtime'),
{ polyfill: false }]);
return babelConfig;
};

11
getTSCommonConfig.js Normal file
View File

@ -0,0 +1,11 @@
'use strict';
module.exports = function () {
return {
target: 'es6',
jsx: 'preserve',
moduleResolution: 'node',
declaration: true,
allowSyntheticDefaultImports: true,
};
};

41
gulpfile.js Normal file
View File

@ -0,0 +1,41 @@
const gulp = require('gulp');
const ts = require('gulp-typescript');
const tsConfig = require('./getTSCommonConfig')();
const babelConfig = require('./getBabelCommonConfig')();
delete babelConfig.cacheDirectory;
const babel = require('gulp-babel');
const transformLess = require('atool-build/lib/transformLess');
const through2 = require('through2');
const merge2 = require('merge2');
function babelify(js) {
return js.pipe(babel(babelConfig))
.pipe(gulp.dest('lib'));
}
gulp.task('default', () => {
const less = gulp.src(['src/' + '**/' + '*.less'])
.pipe(through2.obj(function (file, encoding, next) {
this.push(file.clone());
if (file.path.match(/\/style\/index\.less$/)) {
transformLess(file.path).then((css) => {
file.contents = new Buffer(css);
file.path = file.path.replace(/\.less$/, '.css');
this.push(file);
next();
}).catch((e) => {
console.error(e);
});
} else {
next();
}
}))
.pipe(gulp.dest('lib'));
const img = gulp.src(['src/' + '**/' + '*.png']).pipe(gulp.dest('lib'));
const tsResult = gulp.src([
'src/' + '**/' + '*.tsx',
]).pipe(ts(tsConfig));
const tsFiles = babelify(tsResult.js);
const tsd = tsResult.dts.pipe(gulp.dest('lib'));
return merge2([tsFiles, tsd]);
});

View File

@ -2,11 +2,12 @@
"name": "react-viewer",
"version": "0.0.1",
"description": "react image viewer",
"main": "dist/ViewerWrap",
"main": "dist/index",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "dora --port 8001 --plugins \"webpack,webpack-hmr,browser-history?index=/demo/index.html\"",
"lint": "tslint -c tslint.json src/**/*.ts src/**/*.tsx"
"lint": "tslint -c tslint.json src/**/*.ts src/**/*.tsx",
"build": "atool-build --config webpack.config.prop.js && gulp"
},
"repository": {
"type": "git",
@ -22,21 +23,33 @@
"bugs": {
"url": "https://github.com/infeng/react-viewer/issues"
},
"files": [
"dist",
"lib"
],
"typings": "lib/index.d.ts",
"homepage": "https://github.com/infeng/react-viewer#readme",
"devDependencies": {
"@types/node": "^6.0.45",
"@types/react": "^0.14.39",
"@types/react-dom": "^0.14.17",
"atool-build": "^0.8.1",
"babel-plugin-transform-runtime": "^6.15.0",
"babel-runtime": "^6.11.6",
"dora": "^0.4.3",
"dora-plugin-browser-history": "^0.2.0",
"dora-plugin-webpack": "^0.8.1",
"dora-plugin-webpack-hmr": "^0.2.1",
"gulp": "^3.9.1",
"gulp-babel": "^6.1.2",
"gulp-typescript": "^3.0.2",
"html-loader": "^0.4.4",
"html-webpack-plugin": "^2.22.0",
"merge2": "^1.0.2",
"pre-commit": "^1.1.3",
"react": "^15.3.2",
"react-dom": "^15.3.2",
"through2": "^2.0.1",
"tslint": "^3.15.1",
"typescript": "^2.0.3",
"webpack": "^1.13.2"

View File

@ -1,78 +1,67 @@
import * as React from 'react';
import './style/index.less';
import * as ReactDOM from 'react-dom';
import ViewerCore from './ViewerCore';
import ViewerNav from './ViewerNav';
import ViewerProps from './ViewerProps';
function noop() {}
export default class Viewer extends React.Component<ViewerProps, any> {
private container: HTMLDivElement;
private component: React.ReactNode;
interface ViewerState {
activeIndex?: number;
}
constructor() {
super();
export default class Viewer extends React.Component<ViewerProps, ViewerState> {
static defaultProps = {
visible: false,
onClose: noop,
images: [],
activeIndex: 0,
};
private prefixCls: string;
constructor(props) {
super(props);
this.prefixCls = 'react-viewer';
this.state = {
activeIndex: this.props.activeIndex,
};
this.handleChangeImg = this.handleChangeImg.bind(this);
this.container = null;
this.component = null;
}
handleClose(e) {
this.props.onClose();
}
handleChangeImg(newIndex: number) {
let newState = this.state;
newState.activeIndex = newIndex;
this.setState(newState);
}
componentWillReceiveProps(nextProps: ViewerProps) {
if (this.state.activeIndex !== nextProps.activeIndex) {
this.setState({
activeIndex: nextProps.activeIndex,
});
renderViewer() {
if (this.props.visible || this.component) {
if (!this.container) {
this.container = document.createElement('div');
}
document.body.appendChild(this.container);
let instance = this;
ReactDOM.unstable_renderSubtreeIntoContainer(
this,
<ViewerCore
{...this.props}
/>,
this.container,
function () {
instance.component = this;
},
);
}
}
removeViewer() {
if (this.container) {
const container = this.container;
ReactDOM.unmountComponentAtNode(container);
container.parentNode.removeChild(container);
this.container = null;
this.component = null;
}
}
componentWillUnmount() {
if (this.props.visible) {
this.props.onClose();
this.removeViewer();
} else {
this.removeViewer();
}
}
componentDidMount() {
this.renderViewer();
}
componentDidUpdate() {
this.renderViewer();
}
render() {
let activeImgSrc = '';
if (this.props.images.length > 0) {
activeImgSrc = this.props.images[this.state.activeIndex];
}
return (
<div style={{display: this.props.visible ? 'block' : 'none'}}>
<div className={`${this.prefixCls}-mask`}></div>
<div className={`${this.prefixCls}-close`} onClick={this.handleClose.bind(this)}></div>
<ViewerCore
prefixCls={this.prefixCls}
imgSrc={activeImgSrc}
visible={this.props.visible}
/>
<div className={`${this.prefixCls}-footer`}>
<ViewerNav
prefixCls={this.prefixCls}
images={this.props.images}
activeIndex={this.state.activeIndex}
onChangeImg={this.handleChangeImg}
/>
</div>
</div>
);
return null;
}
}

169
src/ViewerCavans.tsx Normal file
View File

@ -0,0 +1,169 @@
import * as React from 'react';
export interface ViewerCavansProps {
prefixCls: string;
imgSrc: string;
visible: boolean;
}
export interface ViewerCavansState {
width?: number;
top?: number;
left?: number;
isMouseDown?: boolean;
mouseX?: number;
mouseY?: number;
}
export default class ViewerCavans extends React.Component<ViewerCavansProps, ViewerCavansState> {
constructor() {
super();
this.state = {
width: 0,
top: 15,
left: null,
isMouseDown: false,
mouseX: 0,
mouseY: 0,
};
this.handleMouseScroll = this.handleMouseScroll.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.bindEvent = this.bindEvent.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleResize = this.handleResize.bind(this);
}
componentDidMount() {
this.bindEvent();
this.resizeImg(this.props.imgSrc);
}
resizeImg(imgSrc) {
let img = new Image();
img.src = imgSrc;
img.onload = () => {
let width = 0;
let height = 0;
let imgWidth = img.width;
let imgHeight = img.height;
let aspectRatio = imgWidth / imgHeight;
if (aspectRatio > 1) {
width = Math.min(window.innerWidth * .9, imgWidth);
height = (width / imgWidth) * imgHeight;
}else {
height = Math.min((window.innerHeight - 52) * .8, imgHeight);
width = (height / imgHeight) * imgWidth;
}
let left = ( window.innerWidth - width ) / 2;
let top = (window.innerHeight - height) / 2;
this.setState({
width: width,
left: left,
top: top,
});
};
}
handleResize(e) {
this.resizeImg(this.props.imgSrc);
}
handleMouseDown(e) {
e.preventDefault();
e.stopPropagation();
this.setState({
isMouseDown: true,
mouseX: e.nativeEvent.pageX,
mouseY: e.nativeEvent.pageY,
});
}
handleMouseMove(e) {
if (this.state.isMouseDown) {
let diffX = e.x - this.state.mouseX;
let diffY = e.y - this.state.mouseY;
this.setState({
top: this.state.top + diffY,
left: this.state.left + diffX,
mouseX: e.x,
mouseY: e.y,
});
}
}
handleMouseUp(e) {
this.setState({
isMouseDown: false,
});
}
handleMouseScroll(e) {
let direct: 0 | 1 | -1 = 0;
if (e.wheelDelta) {
direct = e.wheelDelta > 0 ? 1 : -1;
}else if (e.detail) {
direct = e.detail > 0 ? 1 : -1;
}
if (direct !== 0) {
this.setState({
width: this.state.width + direct * this.state.width * 0.05,
});
}
}
bindEvent(remove?: boolean) {
let funcName = 'addEventListener';
if (remove) {
funcName = 'removeEventListener';
}
document[funcName]('mousewheel', this.handleMouseScroll, false);
document[funcName]('click', this.handleMouseUp, false);
document[funcName]('mousemove', this.handleMouseMove, false);
window[funcName]('resize', this.handleResize, false);
}
componentWillReceiveProps(nextProps: ViewerCavansProps) {
if (this.props.imgSrc !== nextProps.imgSrc) {
this.resizeImg(nextProps.imgSrc);
}
if (!this.props.visible && nextProps.visible) {
this.bindEvent();
this.resizeImg(this.props.imgSrc);
}
if (this.props.visible && !nextProps.visible) {
this.bindEvent(true);
}
}
render() {
let imgStyle: React.CSSProperties = {
width: `${this.state.width}px`,
marginTop: `${this.state.top}px`,
marginLeft: this.state.left ? `${this.state.left}px` : 'auto',
};
let imgClass = '';
if (!this.state.isMouseDown) {
imgClass = `${this.props.prefixCls}-transition`;
}
return (
<div
className={`${this.props.prefixCls}-cavans`}
onMouseDown={this.handleMouseDown}
>
<img
ref="img"
className={imgClass}
src={this.props.imgSrc}
style={imgStyle}
onMouseDown={this.handleMouseDown}
/>
</div>
);
}
}

View File

@ -1,168 +1,78 @@
import * as React from 'react';
import './style/index.less';
import ViewerCavans from './ViewerCavans';
import ViewerNav from './ViewerNav';
import ViewerProps from './ViewerProps';
export interface ViewerCoreProps {
prefixCls: string;
imgSrc: string;
visible: boolean;
function noop() {}
export interface ViewerCoreState {
activeIndex?: number;
}
interface ViewerCoreState {
width?: number;
top?: number;
left?: number;
isMouseDown?: boolean;
mouseX?: number;
mouseY?: number;
}
export default class ViewerCore extends React.Component<ViewerProps, ViewerCoreState> {
static defaultProps = {
visible: false,
onClose: noop,
images: [],
activeIndex: 0,
};
export default class ViewerCore extends React.Component<ViewerCoreProps, ViewerCoreState> {
private prefixCls: string;
constructor() {
super();
constructor(props) {
super(props);
this.prefixCls = 'react-viewer';
this.state = {
width: 0,
top: 15,
left: null,
isMouseDown: false,
mouseX: 0,
mouseY: 0,
activeIndex: this.props.activeIndex,
};
this.handleMouseScroll = this.handleMouseScroll.bind(this);
this.handleMouseDown = this.handleMouseDown.bind(this);
this.bindEvent = this.bindEvent.bind(this);
this.handleMouseMove = this.handleMouseMove.bind(this);
this.handleMouseUp = this.handleMouseUp.bind(this);
this.handleResize = this.handleResize.bind(this);
this.handleChangeImg = this.handleChangeImg.bind(this);
}
componentDidMount() {
this.bindEvent();
this.resizeImg(this.props.imgSrc);
handleClose(e) {
this.props.onClose();
}
resizeImg(imgSrc) {
let img = new Image();
img.src = imgSrc;
img.onload = () => {
let width = 0;
let height = 0;
let imgWidth = img.width;
let imgHeight = img.height;
let aspectRatio = imgWidth / imgHeight;
if (aspectRatio > 1) {
width = Math.min(window.innerWidth * .9, imgWidth);
height = (width / imgWidth) * imgHeight;
}else {
height = Math.min((window.innerHeight - 52) * .8, imgHeight);
width = (height / imgHeight) * imgWidth;
}
let left = ( window.innerWidth - width ) / 2;
let top = (window.innerHeight - height) / 2;
handleChangeImg(newIndex: number) {
let newState = this.state;
newState.activeIndex = newIndex;
this.setState(newState);
}
componentWillReceiveProps(nextProps: ViewerProps) {
if (this.state.activeIndex !== nextProps.activeIndex) {
this.setState({
width: width,
left: left,
top: top,
activeIndex: nextProps.activeIndex,
});
};
}
handleResize(e) {
this.resizeImg(this.props.imgSrc);
}
handleMouseDown(e) {
e.preventDefault();
e.stopPropagation();
this.setState({
isMouseDown: true,
mouseX: e.nativeEvent.pageX,
mouseY: e.nativeEvent.pageY,
});
}
handleMouseMove(e) {
if (this.state.isMouseDown) {
let diffX = e.x - this.state.mouseX;
let diffY = e.y - this.state.mouseY;
this.setState({
top: this.state.top + diffY,
left: this.state.left + diffX,
mouseX: e.x,
mouseY: e.y,
});
}
}
handleMouseUp(e) {
this.setState({
isMouseDown: false,
});
}
handleMouseScroll(e) {
let direct: 0 | 1 | -1 = 0;
if (e.wheelDelta) {
direct = e.wheelDelta > 0 ? 1 : -1;
}else if (e.detail) {
direct = e.detail > 0 ? 1 : -1;
}
if (direct !== 0) {
this.setState({
width: this.state.width + direct * this.state.width * 0.05,
});
}
}
bindEvent(remove?: boolean) {
let funcName = 'addEventListener';
if (remove) {
funcName = 'removeEventListener';
}
document[funcName]('mousewheel', this.handleMouseScroll, false);
document[funcName]('click', this.handleMouseUp, false);
document[funcName]('mousemove', this.handleMouseMove, false);
window[funcName]('resize', this.handleResize, false);
}
componentWillReceiveProps(nextProps: ViewerCoreProps) {
if (this.props.imgSrc !== nextProps.imgSrc) {
this.resizeImg(nextProps.imgSrc);
}
if (!this.props.visible && nextProps.visible) {
this.bindEvent();
this.resizeImg(this.props.imgSrc);
}
if (this.props.visible && !nextProps.visible) {
this.bindEvent(true);
}
}
render() {
let imgStyle: React.CSSProperties = {
width: `${this.state.width}px`,
marginTop: `${this.state.top}px`,
marginLeft: this.state.left ? `${this.state.left}px` : 'auto',
};
let imgClass = '';
if (!this.state.isMouseDown) {
imgClass = `${this.props.prefixCls}-transition`;
let activeImgSrc = '';
if (this.props.images.length > 0) {
activeImgSrc = this.props.images[this.state.activeIndex];
}
return (
<div
className={`${this.props.prefixCls}-core`}
onMouseDown={this.handleMouseDown}
>
<img
ref="img"
className={imgClass}
src={this.props.imgSrc}
style={imgStyle}
onMouseDown={this.handleMouseDown}
<div style={{display: this.props.visible ? 'block' : 'none'}}>
<div className={`${this.prefixCls}-mask`}></div>
<div className={`${this.prefixCls}-close`} onClick={this.handleClose.bind(this)}></div>
<ViewerCavans
prefixCls={this.prefixCls}
imgSrc={activeImgSrc}
visible={this.props.visible}
/>
<div className={`${this.prefixCls}-footer`}>
<ViewerNav
prefixCls={this.prefixCls}
images={this.props.images}
activeIndex={this.state.activeIndex}
onChangeImg={this.handleChangeImg}
/>
</div>
</div>
);
}

View File

@ -8,3 +8,5 @@ interface ViewerProps {
/** 当前图像index */
activeIndex?: number;
}
export default ViewerProps;

View File

@ -1,66 +0,0 @@
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import Viewer from './Viewer';
export default class ViewerWrap extends React.Component<ViewerProps, any> {
private container: HTMLDivElement;
private component: React.ReactNode;
constructor() {
super();
this.container = null;
this.component = null;
}
renderViewer() {
if (this.props.visible || this.component) {
if (!this.container) {
this.container = document.createElement('div');
}
document.body.appendChild(this.container);
let instance = this;
ReactDOM.unstable_renderSubtreeIntoContainer(
this,
<Viewer
{...this.props}
/>,
this.container,
function () {
instance.component = this;
},
);
}
}
removeViewer() {
if (this.container) {
const container = this.container;
ReactDOM.unmountComponentAtNode(container);
container.parentNode.removeChild(container);
this.container = null;
this.component = null;
}
}
componentWillUnmount() {
if (this.props.visible) {
this.props.onClose();
this.removeViewer();
} else {
this.removeViewer();
}
}
componentDidMount() {
this.renderViewer();
}
componentDidUpdate() {
this.renderViewer();
}
render() {
return null;
}
}

3
src/index.tsx Normal file
View File

@ -0,0 +1,3 @@
import Viewer from './Viewer';
export default Viewer;

View File

@ -29,7 +29,7 @@
z-index: @zIndex;
}
&-core {
&-cavans {
position: absolute;
top: 0;
right: 0;
@ -38,7 +38,7 @@
overflow: hidden;
}
&-core > img {
&-cavans > img {
display: block;
margin: 15px auto;
width: auto;

68
webpack.config.prop.js Normal file
View File

@ -0,0 +1,68 @@
const webpack = require('atool-build/lib/webpack');
var path = require('path');
const packageName = require(path.join(process.cwd(), 'package.json')).name;
const entry = './src/index.tsx';
module.exports = function (webpackConfig) {
webpackConfig.entry = Object.assign({}, webpackConfig.entry, {
['index.min']: entry,
});
webpackConfig.externals = {
react: {
root: 'React',
commonjs2: 'react',
commonjs: 'react',
amd: 'react',
},
'react-dom': {
root: 'ReactDOM',
commonjs2: 'react-dom',
commonjs: 'react-dom',
amd: 'react-dom',
},
};
webpackConfig.output = {
path: path.resolve(__dirname, './dist'),
filename: '[name].js',
library: packageName,
libraryTarget: 'umd',
};
webpackConfig.plugins.some(function (plugin, i) {
if (plugin instanceof webpack.optimize.CommonsChunkPlugin) {
webpackConfig.plugins.splice(i, 1);
return true;
}
});
const uncompressedWebpackConfig = Object.assign({}, webpackConfig);
uncompressedWebpackConfig.entry = {
[`index`]: entry,
};
uncompressedWebpackConfig.plugins = webpackConfig.plugins.filter((plugin) => {
const ret = !(plugin instanceof webpack.optimize.UglifyJsPlugin);
return ret;
});
uncompressedWebpackConfig.plugins = uncompressedWebpackConfig.plugins.filter((plugin) => {
const ret = !(plugin instanceof webpack.DefinePlugin);
return ret;
});
uncompressedWebpackConfig.plugins.push(new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('development'),
}));
return [
webpackConfig,
uncompressedWebpackConfig,
];
};