diff --git a/.gitignore b/.gitignore
index 3848792e..945e61b9 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.DS_Store
dist/
npm-debug.log
-node_modules
\ No newline at end of file
+node_modules
+.next
\ No newline at end of file
diff --git a/README.md b/README.md
index 127b7829..11142bfd 100644
--- a/README.md
+++ b/README.md
@@ -1,17 +1,23 @@
# react-image-magnify
-A React component displaying side by side enlarged image view, with tinted control-image mask.
+A React component for desktop and touch environments.
-Intended for desktop, but will be updated with touch experience soon.
+Desktop displays a side by side enlarged image view, with tinted control-image mask.
+Supports delaying hover and hover-off, which may help reduce unintentional triggering.
-Supports arbitrary image sizes. Scales magnification automatically.
+Touch displays an in place enlarged image view.
+Supports press to pan. Does not interfere with scrolling.
-Supports delaying hover and hover-off, which can help reduce unintentional triggering.
+Supports arbitrary image sizes. Scales magnification automatically.
## Demo
-[Basic demo](http://www.webpackbin.com/N18FshAaW)
+[Desktop](https://react-image-magnify-egeoxscwwk.now.sh/)
+
+[Touch](https://goo.gl/A6DZog)
-See ReactImageMagnify.js tab.
+
+
+https://goo.gl/A6DZog
## Installation
@@ -20,6 +26,7 @@ npm install --save react-image-magnify
```
## Usage
+### Desktop
```JSX
import ReactImageMagnify from 'react-image-magnify';
@@ -40,6 +47,27 @@ import ReactImageMagnify from 'react-image-magnify';
}}/>
...
```
+### Touch
+
+```JavaScript
+import ReactImageMagnifyTouch from 'react-image-magnify';
+...
+
+...
+```
## Props API
@@ -53,7 +81,7 @@ import ReactImageMagnify from 'react-image-magnify';
| `hoverOffDelayInMs` | Number | No | 150 | Milliseconds to delay hover-off trigger. |
| `fadeDurationInMs` | Number | No | 300 | Milliseconds duration of magnified image fade in/fade out. |
| `imageStyle` | Object | No | | Style applied to small image element. |
-| `lensStyle` | Object | No | | Style applied to tinted lens element. |
+| `lensStyle` | Object | No | | Style applied to tinted lens element. Desktop only |
| `enlargedImageContainerStyle` | Object | No | | Style applied to enlarged image container element. |
| `enlargedImageStyle` | Object | No | | Style applied to enlarged image element. |
diff --git a/demo/package.json b/demo/package.json
new file mode 100644
index 00000000..b404b4f0
--- /dev/null
+++ b/demo/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "react-image-magnify",
+ "version": "1.0.0",
+ "description": "react-image-magnify example and demo",
+ "main": "index.js",
+ "author": "",
+ "license": "ISC",
+ "scripts": {
+ "dev": "next",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "next": "^1.1.0",
+ "lodash.assign": "^4.2.0",
+ "lodash.clamp": "^4.0.3",
+ "react-cursor-position": "^1.1.0",
+ "react-hover-observer": "^1.1.0"
+ }
+}
diff --git a/demo/pages/index.js b/demo/pages/index.js
new file mode 100644
index 00000000..944d0c8d
--- /dev/null
+++ b/demo/pages/index.js
@@ -0,0 +1,29 @@
+import React, { Component } from 'react';
+import ReactImageMagnify from '../src/ReactImageMagnify';
+
+class App extends Component {
+ render() {
+ return (
+
+ );
+ }
+}
+
+export default App;
diff --git a/demo/pages/touch.js b/demo/pages/touch.js
new file mode 100644
index 00000000..5bc1ef97
--- /dev/null
+++ b/demo/pages/touch.js
@@ -0,0 +1,63 @@
+import React, { Component } from 'react';
+import ReactImageMagnifyTouch from '../src/ReactImageMagnifyTouch';
+
+class App extends Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ imageWidth: 0,
+ imageHeight: 0
+ };
+ }
+
+ componentDidMount() {
+ const rect = document.documentElement.getBoundingClientRect();
+ const screenWidth = rect.width;
+ const imageWidth = screenWidth - 300;
+
+ this.setState({
+ imageWidth,
+ imageHeight: imageWidth + (imageWidth / 2)
+ });
+ }
+
+ render() {
+ return (
+
+
+
+
Press (long touch) image to magnify. Pan (drag) to traverse image.
+
Note the page can be scrolled when touch begins on image.
+
+
+ );
+ }
+}
+
+export default App;
diff --git a/demo/src/EnlargedImage.js b/demo/src/EnlargedImage.js
new file mode 100644
index 00000000..4aef92f8
--- /dev/null
+++ b/demo/src/EnlargedImage.js
@@ -0,0 +1,170 @@
+import React, { PropTypes } from 'react';
+import clamp from 'lodash.clamp';
+import { Image } from './ReactImageMagnify';
+
+export const Point = PropTypes.shape({
+ x: PropTypes.number.isRequired,
+ y: PropTypes.number.isRequired
+});
+
+export const ImageShape = PropTypes.shape({
+ alt: PropTypes.string,
+ src: PropTypes.string.isRequired,
+ width: PropTypes.number.isRequired,
+ height: PropTypes.number.isRequired
+});
+
+export default React.createClass({
+
+ displayName: 'EnlargedImage',
+
+ getInitialState() {
+ return {
+ isTransitionEntering: false,
+ isTransitionActive: false,
+ isTransitionLeaving: false,
+ isTransitionDone: false
+ };
+ },
+
+ getDefaultProps() {
+ return {
+ fadeDurationInMs: 0
+ };
+ },
+
+ propTypes: {
+ containerClassName: PropTypes.string,
+ containerStyle: PropTypes.object,
+ cursorOffset: Point,
+ cursorPosition: Point,
+ fadeDurationInMs: PropTypes.number,
+ imageClassName: PropTypes.string,
+ imageStyle: PropTypes.object,
+ isHovering: PropTypes.bool,
+ largeImage: ImageShape,
+ smallImage: ImageShape
+ },
+
+ componentWillReceiveProps(nextProps) {
+ const { isHovering } = nextProps;
+
+ if (isHovering) {
+ this.setState({
+ isTransitionEntering: true
+ });
+
+ setTimeout(() => {
+ this.setState({
+ isTransitionEntering: false,
+ isTransitionActive: true
+ });
+ }, 0);
+ } else {
+ this.setState({
+ isTransitionActive: false,
+ isTransitionLeaving: true
+ });
+
+ setTimeout(() => {
+ this.setState({
+ isTransitionLeaving: false,
+ isTransitionDone: true
+ });
+ }, this.props.fadeDurationInMs);
+ }
+ },
+
+ render() {
+ const {
+ containerClassName,
+ containerStyle,
+ cursorOffset,
+ cursorPosition,
+ fadeDurationInMs,
+ imageClassName,
+ imageStyle,
+ isHovering,
+ largeImage,
+ smallImage,
+ } = this.props;
+
+ const {
+ isTransitionEntering,
+ isTransitionActive,
+ isTransitionLeaving
+ } = this.state;
+
+ const offsetRatio = {
+ x: largeImage.width / smallImage.width,
+ y: largeImage.height / smallImage.height
+ };
+
+ const differentiatedImageCoordinates = {
+ x: (Math.round((cursorPosition.x - cursorOffset.x) * offsetRatio.x) * -1),
+ y: (Math.round((cursorPosition.y - cursorOffset.y) * offsetRatio.y) * -1)
+ };
+
+ const minCoordinates = {
+ x: ((largeImage.width - smallImage.width) * -1),
+ y: ((largeImage.height - smallImage.height) * -1)
+ };
+
+ const maxCoordinate = 0;
+
+ const imageCoordinates = {
+ x: clamp(differentiatedImageCoordinates.x, minCoordinates.x, maxCoordinate),
+ y: clamp(differentiatedImageCoordinates.y, minCoordinates.y, maxCoordinate)
+ };
+
+ let isVisible;
+ if (isTransitionEntering || isTransitionActive || isTransitionLeaving) {
+ isVisible = true;
+ } else {
+ isVisible = false;
+ }
+
+ const defaultContainerStyle = {
+ marginLeft: '10px',
+ position: 'absolute',
+ left: '100%',
+ top: '0px',
+ border: '1px solid #d6d6d6',
+ overflow: 'hidden'
+ };
+
+ const computedContainerStyle = {
+ width: smallImage.width,
+ height: smallImage.height,
+ opacity: this.state.isTransitionActive ? 1 : 0,
+ transition: `opacity ${fadeDurationInMs}ms ease-in`
+ };
+
+ const translate = `translate(${imageCoordinates.x}px, ${imageCoordinates.y}px)`;
+
+ const computedImageStyle = {
+ width: largeImage.width,
+ height: largeImage.height,
+ transform: translate,
+ WebkitTransform: translate,
+ msTransform: translate
+ };
+
+ const component = (
+
+
+
+ );
+
+ return isVisible ? component : null;
+ }
+});
diff --git a/demo/src/Lens.js b/demo/src/Lens.js
new file mode 100644
index 00000000..782105f1
--- /dev/null
+++ b/demo/src/Lens.js
@@ -0,0 +1,49 @@
+import React, { PropTypes } from 'react';
+
+const Lens = (props) => {
+ const {
+ fadeDurationInMs,
+ isHovering,
+ style,
+ translateX,
+ translateY
+ } = props;
+ const translate = `translate(${translateX}px, ${translateY}px)`;
+ const computedStyle = {
+ position: 'absolute',
+ transform: translate,
+ WebkitTransform: translate,
+ msTransform: translate,
+ opacity: isHovering ? 1 : 0,
+ transition: `opacity ${fadeDurationInMs}ms ease-in`
+ };
+ const defaultStyle = {
+ width: 'auto',
+ height: 'auto',
+ top: 'auto',
+ right: 'auto',
+ bottom: 'auto',
+ left: 'auto',
+ display: 'block'
+ };
+
+ return ;
+}
+
+Lens.propTypes = {
+ style: PropTypes.object,
+ fadeDurationInMs: PropTypes.number,
+ isHovering: PropTypes.bool,
+ translateX: PropTypes.number,
+ translateY: PropTypes.number,
+ userStyle: PropTypes.object
+};
+
+Lens.defaultProps = {
+ isHovering: false,
+ fadeDurationInMs: 0,
+ translateX: 0,
+ translateY: 0
+};
+
+export default Lens;
diff --git a/demo/src/LensBottom.js b/demo/src/LensBottom.js
new file mode 100644
index 00000000..37018da5
--- /dev/null
+++ b/demo/src/LensBottom.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import clamp from 'lodash.clamp';
+import Lens from './Lens';
+
+const LensBottom = ({
+ cursorOffset,
+ cursorPosition,
+ fadeDurationInMs,
+ isHovering,
+ smallImage,
+ style
+}) => {
+
+ const maxHeight = smallImage.height - (cursorOffset.y * 2);
+ const height = clamp(smallImage.height - cursorPosition.y - cursorOffset.y, 0, maxHeight);
+ const computedStyle = {
+ height: `${height}px`,
+ width: '100%',
+ bottom: '0px'
+ };
+
+ return (
+
+ );
+};
+
+export default LensBottom;
diff --git a/demo/src/LensLeft.js b/demo/src/LensLeft.js
new file mode 100644
index 00000000..b8f14209
--- /dev/null
+++ b/demo/src/LensLeft.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import clamp from 'lodash.clamp';
+import Lens from './Lens';
+
+const LensLeft = ({
+ cursorOffset,
+ cursorPosition,
+ fadeDurationInMs,
+ isHovering,
+ smallImage,
+ style
+}) => {
+
+ const height = cursorOffset.y * 2;
+ const maxHeight = smallImage.height - height;
+ const maxWidth = smallImage.width - (cursorOffset.x * 2);
+ const width = clamp(cursorPosition.x - cursorOffset.x, 0, maxWidth);
+ const translateY = clamp(cursorPosition.y - cursorOffset.y, 0, maxHeight);
+ const computedStyle = {
+ height: `${height}px`,
+ width: `${width}px`,
+ top: '0px',
+ left: '0px'
+ };
+
+ return (
+
+ );
+};
+
+export default LensLeft;
diff --git a/demo/src/LensRight.js b/demo/src/LensRight.js
new file mode 100644
index 00000000..f9684949
--- /dev/null
+++ b/demo/src/LensRight.js
@@ -0,0 +1,36 @@
+import React from 'react';
+import clamp from 'lodash.clamp';
+import Lens from './Lens';
+
+const LensRight = ({
+ cursorOffset,
+ cursorPosition,
+ fadeDurationInMs,
+ isHovering,
+ smallImage,
+ style
+}) => {
+
+ const height = cursorOffset.y * 2;
+ const maxHeight = smallImage.height - height;
+ const maxWidth = smallImage.width - (cursorOffset.x * 2);
+ const width = clamp(smallImage.width - cursorPosition.x - cursorOffset.x, 0, maxWidth);
+ const translateY = clamp(Math.round(cursorPosition.y - cursorOffset.y), 0, maxHeight);
+ const computedStyle = {
+ height: `${height}px`,
+ width: `${width}px`,
+ top: '0px',
+ right: '0px'
+ };
+
+ return (
+
+ );
+};
+
+export default LensRight;
diff --git a/demo/src/LensTop.js b/demo/src/LensTop.js
new file mode 100644
index 00000000..a60dff63
--- /dev/null
+++ b/demo/src/LensTop.js
@@ -0,0 +1,31 @@
+import React from 'react';
+import clamp from 'lodash.clamp';
+import Lens from './Lens';
+
+const LensTop = ({
+ cursorOffset,
+ cursorPosition,
+ fadeDurationInMs,
+ isHovering,
+ smallImage,
+ style
+}) => {
+
+ const maxHeight = smallImage.height - (cursorOffset.y * 2);
+ const height = clamp(cursorPosition.y - cursorOffset.y, 0, maxHeight);
+ const computedStyle = {
+ height: `${height}px`,
+ width: '100%',
+ top: '0px'
+ };
+
+ return (
+
+ );
+};
+
+export default LensTop;
diff --git a/demo/src/ReactImageMagnify.js b/demo/src/ReactImageMagnify.js
new file mode 100644
index 00000000..04784e33
--- /dev/null
+++ b/demo/src/ReactImageMagnify.js
@@ -0,0 +1,124 @@
+import React, { PropTypes } from 'react';
+import ReactCursorPosition from 'react-cursor-position';
+import ReactHoverObserver from 'react-hover-observer';
+import LensTop from './LensTop';
+import LensLeft from './LensLeft';
+import LensRight from './LensRight';
+import LensBottom from './LensBottom';
+import EnlargedImage from './EnlargedImage';
+
+const ReactImageMagnify = ({
+ className,
+ enlargedImageContainerStyle,
+ enlargedImageStyle,
+ fadeDurationInMs,
+ hoverDelayInMs,
+ hoverOffDelayInMs,
+ imageStyle,
+ largeImage,
+ lensStyle,
+ smallImage,
+ style
+}) => {
+
+ const cursorOffset = {
+ x: Math.round(((smallImage.width / largeImage.width) * smallImage.width) / 2),
+ y: Math.round(((smallImage.height / largeImage.height) * smallImage.height) / 2)
+ };
+ const defaultLensStyle = { backgroundColor: 'rgba(0,0,0,.4)' };
+ const compositLensStyle = Object.assign({}, defaultLensStyle, lensStyle);
+
+ return (
+
+ setIsHovering(),
+ onMouseLeave: ({ unsetIsHovering }) => unsetIsHovering(),
+ onMouseOver: ({ e, unsetIsHovering }) => {
+ if (e.target.getAttribute('data-hover') === 'false') {
+ unsetIsHovering();
+ }
+ }
+ }}>
+
+
+
+
+
+
+
+
+ );
+}
+
+export const ImageShape = PropTypes.shape({
+ alt: PropTypes.string,
+ src: PropTypes.string.isRequired,
+ width: PropTypes.number.isRequired,
+ height: PropTypes.number.isRequired
+});
+
+ReactImageMagnify.propTypes = {
+ className: PropTypes.string,
+ enlargedImageContainerStyle: PropTypes.object,
+ enlargedImageStyle: PropTypes.object,
+ fadeDurationInMs: PropTypes.number,
+ hoverDelayInMs: PropTypes.number,
+ hoverOffDelayInMs: PropTypes.number,
+ imageStyle: PropTypes.object,
+ largeImage: ImageShape,
+ lensStyle: PropTypes.object,
+ smallImage: ImageShape,
+ style: PropTypes.object
+};
+
+ReactImageMagnify.defaultProps = {
+ fadeDurationInMs: 300,
+ hoverDelayInMs: 250,
+ hoverOffDelayInMs: 150
+};
+
+export default ReactImageMagnify;
diff --git a/demo/src/ReactImageMagnifyTouch.js b/demo/src/ReactImageMagnifyTouch.js
new file mode 100644
index 00000000..96d56821
--- /dev/null
+++ b/demo/src/ReactImageMagnifyTouch.js
@@ -0,0 +1,89 @@
+import React, { PropTypes } from 'react';
+import ReactHoverObserver from 'react-hover-observer';
+import ReactTouchPosition from './ReactTouchPosition';
+import LensTop from './LensTop';
+import LensLeft from './LensLeft';
+import LensRight from './LensRight';
+import LensBottom from './LensBottom';
+import EnlargedImage from './EnlargedImage';
+
+const ReactImageMagnify = ({
+ className,
+ enlargedImageContainerStyle,
+ enlargedImageStyle,
+ fadeDurationInMs,
+ hoverDelayInMs,
+ hoverOffDelayInMs,
+ imageStyle,
+ largeImage,
+ lensStyle,
+ smallImage,
+ style
+}) => {
+
+ const cursorOffset = {
+ x: Math.round(((smallImage.width / largeImage.width) * smallImage.width) / 2),
+ y: Math.round(((smallImage.height / largeImage.height) * smallImage.height) / 2)
+ };
+
+ return (
+
+
+
+
+ );
+}
+
+export const ImageShape = PropTypes.shape({
+ alt: PropTypes.string,
+ src: PropTypes.string.isRequired,
+ width: PropTypes.number.isRequired,
+ height: PropTypes.number.isRequired
+});
+
+ReactImageMagnify.propTypes = {
+ className: PropTypes.string,
+ enlargedImageContainerStyle: PropTypes.object,
+ enlargedImageStyle: PropTypes.object,
+ fadeDurationInMs: PropTypes.number,
+ hoverDelayInMs: PropTypes.number,
+ hoverOffDelayInMs: PropTypes.number,
+ imageStyle: PropTypes.object,
+ largeImage: ImageShape,
+ lensStyle: PropTypes.object,
+ smallImage: ImageShape,
+ style: PropTypes.object
+};
+
+ReactImageMagnify.defaultProps = {
+ fadeDurationInMs: 300,
+ hoverDelayInMs: 250,
+ hoverOffDelayInMs: 150
+};
+
+export default ReactImageMagnify;
diff --git a/demo/src/ReactTouchPosition.js b/demo/src/ReactTouchPosition.js
new file mode 100644
index 00000000..00975815
--- /dev/null
+++ b/demo/src/ReactTouchPosition.js
@@ -0,0 +1,231 @@
+import React, {
+ Children,
+ cloneElement,
+ PropTypes
+} from 'react';
+import assign from 'lodash.assign';
+import noop from 'lodash.noop';
+import omit from 'lodash.omit';
+
+export default React.createClass({
+
+ displayName: 'TouchPosition',
+
+ getInitialState() {
+ return {
+ isHovering: false,
+ isTouchOutside: false,
+ cursorPosition: {
+ x: 0,
+ y: 0
+ }
+ };
+ },
+
+ propTypes: {
+ className: PropTypes.string,
+ isActivatedOnTouch: PropTypes.bool,
+ onActivationChanged: PropTypes.func,
+ onPositionChanged: PropTypes.func,
+ pressDuration: PropTypes.number,
+ pressMoveThreshold: PropTypes.number,
+ shouldDecorateChildren: PropTypes.bool,
+ style: PropTypes.object
+ },
+
+ getDefaultProps() {
+ return {
+ isActivatedOnTouch: false,
+ onActivationChanged: noop,
+ onPositionChanged: noop,
+ pressDuration: 500,
+ pressMoveThreshold: 5,
+ shouldDecorateChildren: true
+ };
+ },
+
+ //on every method a different approach is taken on arguments
+ onTouchStart(e) {
+ this.elementOffsetRect = this.getViewportRelativeElementRect(e.currentTarget);
+ this.setPosition(e);
+
+ if (this.props.isActivatedOnTouch) {
+ e.preventDefault();
+
+ this.setState({
+ isHovering: true
+ });
+
+ return;
+ }
+
+ this.initPressEventCriteria(e.touches[0]);
+
+ this.setPressEventTimer()
+ },
+
+ onTouchMove(e) {
+ this.setPressEventCriteria(e.touches[0]);
+
+ if (!this.state.isHovering) {
+ return;
+ }
+
+ this.setPosition(e);
+
+ e.preventDefault();
+ },
+
+ onTouchEnd() {
+ this.clearTimers();
+
+ this.setState({
+ isHovering: false,
+ isTouchOutside: false
+ });
+
+ this.props.onActivationChanged({ isHovering: false });
+ },
+
+ setPosition(e) {
+ const viewportRelativeTouchPosition = this.getViewportRelativeTouchPosition(e);
+ const elementOffsetRect = this.elementOffsetRect;
+ const cursorPosition = this.getElementRelativeTouchPosition(viewportRelativeTouchPosition, elementOffsetRect);
+ const isPositionOutside = this.getIsPositionOutside(viewportRelativeTouchPosition, elementOffsetRect);
+
+ this.setState({
+ cursorPosition,
+ isPositionOutside
+ });
+
+ this.props.onPositionChanged(Object.assign({ isPositionOutside }, cursorPosition));
+ },
+
+ setPressEventTimer() {
+ this.pressDurationTimerId = setTimeout(() => {
+ if (Math.abs(this.currentElTop - this.initialElTop) < this.props.pressMoveThreshold) {
+ this.setState({ isHovering: true });
+ this.props.onActivationChanged({ isHovering: true });
+ }
+ }, this.props.pressDuration);
+ },
+
+ setPressEventCriteria(touch) {
+ if (!this.props.isActivatedOnTouch) {
+ if (!this.state.isHovering) {
+ this.currentElTop = touch.clientY;
+ } else {
+ this.initialElTop = touch.clientY;
+ }
+ }
+ },
+
+ initPressEventCriteria(touch) {
+ const top = touch.clientY;
+ this.initialElTop = top;
+ this.currentElTop = top;
+ },
+
+ getViewportRelativeElementRect(el) {
+ return el.getBoundingClientRect();
+ },
+
+ getIsPositionOutside(viewportRelativeTouchPosition, elementOffsetRect) {
+ const { x: viewportRelativeTouchX, y: viewportRelativeTouchY } = viewportRelativeTouchPosition;
+ const {
+ top: offsetTop,
+ right: offsetRight,
+ bottom: offsetBottom,
+ left: offsetLeft
+ } = elementOffsetRect;
+ return (
+ viewportRelativeTouchX < offsetLeft ||
+ viewportRelativeTouchX > offsetRight ||
+ viewportRelativeTouchY < offsetTop ||
+ viewportRelativeTouchY > offsetBottom
+ );
+ },
+
+ getViewportRelativeTouchPosition(event) {
+ const touch = event.touches[0];
+ return {
+ x: touch.clientX,
+ y: touch.clientY
+ }
+ },
+
+ getElementRelativeTouchPosition(viewportRelativetouchPosition, elementOffsetRect) {
+ const { x: touchX, y: touchY } = viewportRelativetouchPosition;
+ const { left: offsetX, top: offsetY } = elementOffsetRect;
+
+ return {
+ x: touchX - offsetX,
+ y: touchY - offsetY
+ };
+ },
+
+ isReactComponent(reactElement) {
+ return typeof reactElement.type === 'function';
+ },
+
+ shouldDecorateChild(child) {
+ return this.isReactComponent(child) && this.props.shouldDecorateChildren;
+ },
+
+ decorateChild(child, props) {
+ return cloneElement(child, props);
+ },
+
+ renderChildrenWithProps(children, props) {
+ return Children.map(children, (child) => {
+ return this.shouldDecorateChild(child) ? this.decorateChild(child, props) : child;
+ });
+ },
+
+ clearTimers() {
+ clearTimeout(this.pressDurationTimerId);
+ },
+
+ componentWillUnmount() {
+ this.clearTimers();
+ },
+
+ render() {
+ const { children, className, style } = this.props;
+ const { isHovering, isTouchOutside, cursorPosition } = this.state;
+ const childProps = assign(
+ {},
+ {
+ isHovering,
+ isTouchOutside,
+ cursorPosition
+ },
+ omit(this.props, [
+ 'children',
+ 'className',
+ 'isActivatedOnTouch',
+ 'onActivationChanged',
+ 'onPositionChanged',
+ 'onTouchOutside',
+ 'pressDuration',
+ 'pressMoveThreshold',
+ 'shouldDecorateChildren',
+ 'style'
+ ])
+ );
+
+ return (
+
+ { this.renderChildrenWithProps(children, childProps) }
+
+ );
+ }
+});
diff --git a/demo/static/large-a.jpg b/demo/static/large-a.jpg
new file mode 100644
index 00000000..33dda666
Binary files /dev/null and b/demo/static/large-a.jpg differ
diff --git a/demo/static/large-b.jpg b/demo/static/large-b.jpg
new file mode 100644
index 00000000..9218963c
Binary files /dev/null and b/demo/static/large-b.jpg differ
diff --git a/demo/static/small-a.jpg b/demo/static/small-a.jpg
new file mode 100644
index 00000000..021e8325
Binary files /dev/null and b/demo/static/small-a.jpg differ
diff --git a/demo/static/small-b.jpg b/demo/static/small-b.jpg
new file mode 100644
index 00000000..3dd3748f
Binary files /dev/null and b/demo/static/small-b.jpg differ
diff --git a/demo/static/thumb-a.jpg b/demo/static/thumb-a.jpg
new file mode 100644
index 00000000..f61e56a7
Binary files /dev/null and b/demo/static/thumb-a.jpg differ
diff --git a/demo/static/thumb-b.jpg b/demo/static/thumb-b.jpg
new file mode 100644
index 00000000..a322ef4d
Binary files /dev/null and b/demo/static/thumb-b.jpg differ
diff --git a/docs/now-touch-qr-100.jpg b/docs/now-touch-qr-100.jpg
new file mode 100644
index 00000000..6d5a72d6
Binary files /dev/null and b/docs/now-touch-qr-100.jpg differ
diff --git a/docs/now-touch-qr-150.jpg b/docs/now-touch-qr-150.jpg
new file mode 100644
index 00000000..b947986b
Binary files /dev/null and b/docs/now-touch-qr-150.jpg differ
diff --git a/docs/now-touch-qr.jpg b/docs/now-touch-qr.jpg
new file mode 100755
index 00000000..d8e72b95
Binary files /dev/null and b/docs/now-touch-qr.jpg differ
diff --git a/docs/qr-2.png b/docs/qr-2.png
new file mode 100644
index 00000000..4b991a5d
Binary files /dev/null and b/docs/qr-2.png differ
diff --git a/example/src/App.js b/example/src/App.js
index 6a8a7a32..e1ebbdca 100644
--- a/example/src/App.js
+++ b/example/src/App.js
@@ -2,6 +2,7 @@ import React, { Component } from 'react';
import logo from './logo.svg';
import './App.css';
import ReactImageMagnify from '../../dist/ReactImageMagnify';
+import ReactImageMagnifyTouch from '../../dist/ReactImageMagnifyTouch';
class App extends Component {
render() {
@@ -28,6 +29,26 @@ class App extends Component {
height: 168
}
}}/>
+
+
);
}
diff --git a/src/ReactImageMagnifyTouch.js b/src/ReactImageMagnifyTouch.js
new file mode 100644
index 00000000..96d56821
--- /dev/null
+++ b/src/ReactImageMagnifyTouch.js
@@ -0,0 +1,89 @@
+import React, { PropTypes } from 'react';
+import ReactHoverObserver from 'react-hover-observer';
+import ReactTouchPosition from './ReactTouchPosition';
+import LensTop from './LensTop';
+import LensLeft from './LensLeft';
+import LensRight from './LensRight';
+import LensBottom from './LensBottom';
+import EnlargedImage from './EnlargedImage';
+
+const ReactImageMagnify = ({
+ className,
+ enlargedImageContainerStyle,
+ enlargedImageStyle,
+ fadeDurationInMs,
+ hoverDelayInMs,
+ hoverOffDelayInMs,
+ imageStyle,
+ largeImage,
+ lensStyle,
+ smallImage,
+ style
+}) => {
+
+ const cursorOffset = {
+ x: Math.round(((smallImage.width / largeImage.width) * smallImage.width) / 2),
+ y: Math.round(((smallImage.height / largeImage.height) * smallImage.height) / 2)
+ };
+
+ return (
+
+
+
+
+ );
+}
+
+export const ImageShape = PropTypes.shape({
+ alt: PropTypes.string,
+ src: PropTypes.string.isRequired,
+ width: PropTypes.number.isRequired,
+ height: PropTypes.number.isRequired
+});
+
+ReactImageMagnify.propTypes = {
+ className: PropTypes.string,
+ enlargedImageContainerStyle: PropTypes.object,
+ enlargedImageStyle: PropTypes.object,
+ fadeDurationInMs: PropTypes.number,
+ hoverDelayInMs: PropTypes.number,
+ hoverOffDelayInMs: PropTypes.number,
+ imageStyle: PropTypes.object,
+ largeImage: ImageShape,
+ lensStyle: PropTypes.object,
+ smallImage: ImageShape,
+ style: PropTypes.object
+};
+
+ReactImageMagnify.defaultProps = {
+ fadeDurationInMs: 300,
+ hoverDelayInMs: 250,
+ hoverOffDelayInMs: 150
+};
+
+export default ReactImageMagnify;
diff --git a/src/ReactTouchPosition.js b/src/ReactTouchPosition.js
new file mode 100644
index 00000000..7e8b0958
--- /dev/null
+++ b/src/ReactTouchPosition.js
@@ -0,0 +1,230 @@
+import React, {
+ Children,
+ cloneElement,
+ PropTypes
+} from 'react';
+import assign from 'lodash.assign';
+import noop from 'lodash.noop';
+import omit from 'lodash.omit';
+
+export default React.createClass({
+
+ displayName: 'TouchPosition',
+
+ getInitialState() {
+ return {
+ isHovering: false,
+ isTouchOutside: false,
+ cursorPosition: {
+ x: 0,
+ y: 0
+ }
+ };
+ },
+
+ propTypes: {
+ className: PropTypes.string,
+ isActivatedOnTouch: PropTypes.bool,
+ onActivationChanged: PropTypes.func,
+ onPositionChanged: PropTypes.func,
+ pressDuration: PropTypes.number,
+ pressMoveThreshold: PropTypes.number,
+ shouldDecorateChildren: PropTypes.bool,
+ style: PropTypes.object
+ },
+
+ getDefaultProps() {
+ return {
+ isActivatedOnTouch: false,
+ onActivationChanged: noop,
+ onPositionChanged: noop,
+ pressDuration: 500,
+ pressMoveThreshold: 5,
+ shouldDecorateChildren: true
+ };
+ },
+
+ onTouchStart(e) {
+ this.elementOffsetRect = this.getViewportRelativeElementRect(e.currentTarget);
+ this.setPosition(e);
+
+ if (this.props.isActivatedOnTouch) {
+ e.preventDefault();
+
+ this.setState({
+ isHovering: true
+ });
+
+ return;
+ }
+
+ this.initPressEventCriteria(e.touches[0]);
+
+ this.setPressEventTimer()
+ },
+
+ onTouchMove(e) {
+ this.setPressEventCriteria(e.touches[0]);
+
+ if (!this.state.isHovering) {
+ return;
+ }
+
+ this.setPosition(e);
+
+ e.preventDefault();
+ },
+
+ onTouchEnd() {
+ this.clearTimers();
+
+ this.setState({
+ isHovering: false,
+ isTouchOutside: false
+ });
+
+ this.props.onActivationChanged({ isHovering: false });
+ },
+
+ setPosition(e) {
+ const viewportRelativeTouchPosition = this.getViewportRelativeTouchPosition(e);
+ const elementOffsetRect = this.elementOffsetRect;
+ const cursorPosition = this.getElementRelativeTouchPosition(viewportRelativeTouchPosition, elementOffsetRect);
+ const isPositionOutside = this.getIsPositionOutside(viewportRelativeTouchPosition, elementOffsetRect);
+
+ this.setState({
+ cursorPosition,
+ isPositionOutside
+ });
+
+ this.props.onPositionChanged(Object.assign({ isPositionOutside }, cursorPosition));
+ },
+
+ setPressEventTimer() {
+ this.pressDurationTimerId = setTimeout(() => {
+ if (Math.abs(this.currentElTop - this.initialElTop) < this.props.pressMoveThreshold) {
+ this.setState({ isHovering: true });
+ this.props.onActivationChanged({ isHovering: true });
+ }
+ }, this.props.pressDuration);
+ },
+
+ setPressEventCriteria(touch) {
+ if (!this.props.isActivatedOnTouch) {
+ if (!this.state.isHovering) {
+ this.currentElTop = touch.clientY;
+ } else {
+ this.initialElTop = touch.clientY;
+ }
+ }
+ },
+
+ initPressEventCriteria(touch) {
+ const top = touch.clientY;
+ this.initialElTop = top;
+ this.currentElTop = top;
+ },
+
+ getViewportRelativeElementRect(el) {
+ return el.getBoundingClientRect();
+ },
+
+ getIsPositionOutside(viewportRelativeTouchPosition, elementOffsetRect) {
+ const { x: viewportRelativeTouchX, y: viewportRelativeTouchY } = viewportRelativeTouchPosition;
+ const {
+ top: offsetTop,
+ right: offsetRight,
+ bottom: offsetBottom,
+ left: offsetLeft
+ } = elementOffsetRect;
+ return (
+ viewportRelativeTouchX < offsetLeft ||
+ viewportRelativeTouchX > offsetRight ||
+ viewportRelativeTouchY < offsetTop ||
+ viewportRelativeTouchY > offsetBottom
+ );
+ },
+
+ getViewportRelativeTouchPosition(event) {
+ const touch = event.touches[0];
+ return {
+ x: touch.clientX,
+ y: touch.clientY
+ }
+ },
+
+ getElementRelativeTouchPosition(viewportRelativetouchPosition, elementOffsetRect) {
+ const { x: touchX, y: touchY } = viewportRelativetouchPosition;
+ const { left: offsetX, top: offsetY } = elementOffsetRect;
+
+ return {
+ x: touchX - offsetX,
+ y: touchY - offsetY
+ };
+ },
+
+ isReactComponent(reactElement) {
+ return typeof reactElement.type === 'function';
+ },
+
+ shouldDecorateChild(child) {
+ return this.isReactComponent(child) && this.props.shouldDecorateChildren;
+ },
+
+ decorateChild(child, props) {
+ return cloneElement(child, props);
+ },
+
+ renderChildrenWithProps(children, props) {
+ return Children.map(children, (child) => {
+ return this.shouldDecorateChild(child) ? this.decorateChild(child, props) : child;
+ });
+ },
+
+ clearTimers() {
+ clearTimeout(this.pressDurationTimerId);
+ },
+
+ componentWillUnmount() {
+ this.clearTimers();
+ },
+
+ render() {
+ const { children, className, style } = this.props;
+ const { isHovering, isTouchOutside, cursorPosition } = this.state;
+ const childProps = assign(
+ {},
+ {
+ isHovering,
+ isTouchOutside,
+ cursorPosition
+ },
+ omit(this.props, [
+ 'children',
+ 'className',
+ 'isActivatedOnTouch',
+ 'onActivationChanged',
+ 'onPositionChanged',
+ 'onTouchOutside',
+ 'pressDuration',
+ 'pressMoveThreshold',
+ 'shouldDecorateChildren',
+ 'style'
+ ])
+ );
+
+ return (
+
+ { this.renderChildrenWithProps(children, childProps) }
+
+ );
+ }
+});