-
-
Notifications
You must be signed in to change notification settings - Fork 102
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Move components, defaults and helpers to their own files
- Loading branch information
Showing
13 changed files
with
1,325 additions
and
992 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import React, { Component } from 'react' | ||
import PropTypes from 'prop-types' | ||
|
||
const { element } = PropTypes | ||
|
||
export default class FullWrapper extends Component { | ||
constructor() { | ||
super() | ||
this._handleScroll = this._handleScroll.bind(this) | ||
this._handleKeyUp = this._handleKeyUp.bind(this) | ||
this._handleTouchStart = this._handleTouchStart.bind(this) | ||
this._handleTouchMove = this._handleTouchMove.bind(this) | ||
this._handleTouchEnd = this._handleTouchEnd.bind(this) | ||
} | ||
|
||
componentDidMount() { | ||
window.addEventListener('scroll', this._handleScroll, true) | ||
window.addEventListener('keyup', this._handleKeyUp) | ||
window.addEventListener('ontouchstart', this._handleTouchStart) | ||
window.addEventListener('ontouchmove', this._handleTouchMove) | ||
window.addEventListener('ontouchend', this._handleTouchEnd) | ||
window.addEventListener('ontouchcancel', this._handleTouchEnd) | ||
} | ||
|
||
componentWillUnmount() { | ||
window.removeEventListener('scroll', this._handleScroll, true) | ||
window.removeEventListener('keyup', this._handleKeyUp) | ||
window.removeEventListener('ontouchstart', this._handleTouchStart) | ||
window.removeEventListener('ontouchmove', this._handleTouchMove) | ||
window.removeEventListener('ontouchend', this._handleTouchEnd) | ||
window.removeEventListener('ontouchcancel', this._handleTouchEnd) | ||
} | ||
|
||
render() { | ||
return ( | ||
<div onClick={this.unzoom.bind(this)}> | ||
{this._cloneChild()} | ||
</div> | ||
) | ||
} | ||
|
||
unzoom() { | ||
this.refs.child.unzoom() | ||
} | ||
|
||
_cloneChild() { | ||
return React.cloneElement( | ||
React.Children.only(this.props.children), | ||
{ ref: 'child' } | ||
) | ||
} | ||
|
||
_handleScroll() { | ||
this.forceUpdate() | ||
this.unzoom() | ||
} | ||
|
||
_handleKeyUp({ which }) { | ||
const opts = { | ||
27: this.unzoom | ||
} | ||
|
||
if(opts[which]) return opts[which]() | ||
} | ||
|
||
_handleTouchStart(e) { | ||
this.yTouchPosition = e.touches[0].clientY | ||
} | ||
|
||
_handleTouchMove(e) { | ||
this.forceUpdate() | ||
const touchChange = Math.abs(e.touches[0].clientY - this.yTouchPosition) | ||
if (touchChange > 10) this.unzoom() | ||
} | ||
|
||
_handleTouchEnd(e) { | ||
delete this.yTouchPosition | ||
} | ||
} | ||
|
||
FullWrapper.propTypes = { | ||
children: element.isRequired | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,203 @@ | ||
import React, { Component } from 'react' | ||
import PropTypes from 'prop-types' | ||
import ReactDOM from 'react-dom' | ||
import defaults from './defaults' | ||
import { createPortal, removePortal } from './helpers' | ||
|
||
import FullWrapper from './FullWrapper' | ||
import Overlay from './Overlay' | ||
import ResizeWrapper from './ResizeWrapper' | ||
import Zoom from './Zoom' | ||
|
||
const { bool, element, func, object, number, shape, string } = PropTypes | ||
|
||
export default class ImageZoom extends Component { | ||
constructor(props) { | ||
super(props) | ||
|
||
this.state = { | ||
isZoomed: false, | ||
image: props.image, | ||
hasAlreadyLoaded: false | ||
} | ||
|
||
this._handleZoom = this._handleZoom.bind(this) | ||
this._handleUnzoom = this._handleUnzoom.bind(this) | ||
} | ||
|
||
static get defaultProps() { | ||
return { | ||
shouldReplaceImage: true, | ||
shouldRespectMaxDimension: false, | ||
zoomMargin: 40, | ||
defaultStyles: { | ||
zoomContainer: {}, | ||
overlay: {}, | ||
image: {}, | ||
zoomImage: {} | ||
}, | ||
shouldHandleZoom: (_) => true, | ||
onZoom: () => {}, | ||
onUnzoom: () => {} | ||
} | ||
} | ||
|
||
componentDidMount() { | ||
if (this.state.isZoomed || this.props.isZoomed) this.renderZoomed() | ||
} | ||
|
||
// Clean up any mess we made of the DOM before we unmount | ||
componentWillUnmount() { | ||
this.removeZoomed() | ||
} | ||
|
||
componentWillReceiveProps(nextProps) { | ||
if (this.props.isZoomed == null && nextProps.isZoomed != null) { | ||
throw new Error(defaults.errors.uncontrolled) | ||
} else if (this.props.isZoomed != null && nextProps.isZoomed == null) { | ||
throw new Error(defaults.errors.controlled) | ||
} | ||
|
||
// If the consumer wants to change the image's src, then so be it. | ||
if (this.props.image.src !== nextProps.image.src) { | ||
this.setState({ image: nextProps.image }) | ||
} | ||
} | ||
|
||
/** | ||
* When the component's state updates, check for changes | ||
* and either zoom or start the unzoom procedure. | ||
* NOTE: We need to differentiate whether this is a | ||
* controlled or uncontrolled component and do the check | ||
* based off of that. | ||
*/ | ||
componentDidUpdate(prevProps, prevState) { | ||
const prevIsZoomed = prevProps.isZoomed != null ? prevProps.isZoomed : prevState.isZoomed | ||
const isZoomed = this.props.isZoomed != null ? this.props.isZoomed : this.state.isZoomed | ||
if (prevIsZoomed !== isZoomed) { | ||
if (isZoomed) this._renderZoomed() | ||
else if (this.portalInstance) this.portalInstance.unzoom() | ||
} | ||
} | ||
|
||
render() { | ||
/** | ||
* Take whatever attributes you want to pass the image | ||
* and then override with the properties we need | ||
*/ | ||
const attrs = Object.assign({}, this.state.image, { | ||
style: this._getImageStyle(), | ||
onClick: this._handleZoom | ||
}) | ||
|
||
return <img ref="image" { ...attrs } /> | ||
} | ||
|
||
_renderZoomed() { | ||
/** | ||
* If it's an uncontrolled component, include all wrap controls. | ||
* If it's a controlled component, only wrap it in the resize controls. | ||
*/ | ||
const innerComponent = ( | ||
<ResizeWrapper> | ||
<Zoom | ||
defaultStyles={ this.props.defaultStyles } | ||
image={ ReactDOM.findDOMNode(this.refs.image) } | ||
hasAlreadyLoaded={ this.state.hasAlreadyLoaded } | ||
shouldRespectMaxDimension={ this.props.shouldRespectMaxDimension } | ||
zoomImage={ this.props.zoomImage } | ||
zoomMargin={ this.props.zoomMargin } | ||
onUnzoom={ this._handleUnzoom } | ||
/> | ||
</ResizeWrapper> | ||
) | ||
const component = this.props.isZoomed == null | ||
? <FullWrapper>{innerComponent}</FullWrapper> | ||
: innerComponent | ||
this.portal = createPortal('div') | ||
this.portalInstance = ReactDOM.render(component, this.portal) | ||
} | ||
|
||
_removeZoomed() { | ||
if (this.portal) { | ||
ReactDOM.unmountComponentAtNode(this.portal) | ||
removePortal(this.portal) | ||
delete this.portalInstance | ||
delete this.portal | ||
} | ||
} | ||
|
||
_getImageStyle() { | ||
const style = Object.assign({}, | ||
this.state.isZoomed && { visibility: 'hidden' } | ||
) | ||
|
||
return Object.assign( | ||
{}, | ||
defaults.styles.image, | ||
style, | ||
this.props.defaultStyles.image, | ||
this.state.image.style | ||
) | ||
} | ||
|
||
_handleZoom(event) { | ||
if (this.props.isZoomed == null && this.props.shouldHandleZoom(event)) { | ||
this.setState({ isZoomed: true }, this.props.onZoom) | ||
} else { | ||
this.props.onZoom() | ||
} | ||
} | ||
|
||
/** | ||
* This gets passed to the zoomed component as a callback | ||
* to trigger when the time is right to shut down the zoom. | ||
* If `shouldReplaceImage`, update the normal image we're showing | ||
* with the zoomed image -- useful when wanting to replace a low-res | ||
* image with a high-res one once it's already been downloaded. | ||
* It also cleans up the zoom references and then updates state. | ||
*/ | ||
_handleUnzoom(src) { | ||
return () => { | ||
const changes = Object.assign({}, { hasAlreadyLoaded: true, isZoomed: false }, | ||
this.props.shouldReplaceImage && { | ||
image: Object.assign({}, this.state.image, { src }) | ||
} | ||
) | ||
|
||
/** | ||
* Lamentable but necessary right now in order to | ||
* remove the portal instance before the next | ||
* `componentDidUpdate` check for the portalInstance. | ||
* The reasoning is so we can differentiate between an | ||
* external `isZoomed` command and an internal one. | ||
*/ | ||
this._removeZoomed() | ||
|
||
this.setState(changes, this.props.onUnzoom) | ||
} | ||
} | ||
} | ||
|
||
ImageZoom.propTypes = { | ||
image: shape({ | ||
src: string.isRequired, | ||
alt: string, | ||
className: string, | ||
style: object | ||
}).isRequired, | ||
zoomImage: shape({ | ||
src: string, | ||
alt: string, | ||
className: string, | ||
style: object | ||
}), | ||
defaultStyles: object, | ||
isZoomed: bool, | ||
shouldHandleZoom: func, | ||
shouldReplaceImage: bool, | ||
shouldRespectMaxDimension: bool.isRequired, | ||
onZoom: func, | ||
onUnzoom: func | ||
} | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import React, { Component } from 'react' | ||
import PropTypes from 'prop-types' | ||
import defaults from './defaults' | ||
|
||
const { bool, object } = PropTypes | ||
|
||
export default class Overlay extends Component { | ||
constructor(props) { | ||
super(props) | ||
|
||
this.state = { | ||
isVisible: false | ||
} | ||
} | ||
|
||
componentDidMount() { | ||
this.setState({ isVisible: true }) | ||
} | ||
|
||
componentWillReceiveProps(nextProps) { | ||
if (!nextProps.isVisible) this.setState({ isVisible: false }) | ||
} | ||
|
||
shouldComponentUpdate(nextProps, nextState) { | ||
return this.props.isVisible !== nextProps.isVisible || | ||
this.state.isVisible !== nextProps.isVisible | ||
} | ||
|
||
render() { | ||
return <div style={ this._getStyle() }></div> | ||
} | ||
|
||
_getStyle() { | ||
const opacity = this.state.isVisible & 1 // bitwise and; converts bool to 0 or 1 | ||
return Object.assign( | ||
{}, | ||
defaults.styles.overlay, | ||
{ opacity }, | ||
this.props.defaultStyles.overlay | ||
) | ||
} | ||
} | ||
|
||
Overlay.propTypes = { | ||
isVisible: bool.isRequired, | ||
defaultStyles: object.isRequired | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import React, { Component } from 'react' | ||
import PropTypes from 'prop-types' | ||
|
||
const { element } = PropTypes | ||
|
||
export default class ResizeWrapper extends Component { | ||
constructor() { | ||
super() | ||
this._handleResize = this._handleResize.bind(this) | ||
} | ||
componentDidMount() { | ||
window.addEventListener('resize', this._handleResize) | ||
} | ||
|
||
componentWillUnmount() { | ||
window.removeEventListener('resize', this._handleResize) | ||
} | ||
|
||
render() { | ||
const cloned = React.cloneElement( | ||
React.Children.only(this.props.children), | ||
{ ref: 'child' } | ||
) | ||
return ( | ||
<div>{cloned}</div> | ||
) | ||
} | ||
|
||
unzoom() { | ||
this.refs.child.unzoom() | ||
} | ||
|
||
_handleResize() { | ||
this.forceUpdate() | ||
} | ||
} | ||
|
||
ResizeWrapper.propTypes = { | ||
children: element.isRequired | ||
} |
Oops, something went wrong.