Skip to content

Commit

Permalink
Move components, defaults and helpers to their own files
Browse files Browse the repository at this point in the history
  • Loading branch information
rpearce committed Sep 19, 2017
1 parent 5bedd22 commit e6f9fa2
Show file tree
Hide file tree
Showing 13 changed files with 1,325 additions and 992 deletions.
1,121 changes: 711 additions & 410 deletions example/build/app.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion example/src/app.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import ImageZoom from '../../lib/react-medium-image-zoom'
import ImageZoom from '../../lib'

class App extends Component {
render() {
Expand Down
Empty file removed lib/.keep
Empty file.
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
"name": "react-medium-image-zoom",
"version": "1.0.4",
"description": "Medium.com style image zoom for React",
"main": "lib/react-medium-image-zoom.js",
"main": "lib/index.js",
"scripts": {
"build:js": "babel src/react-medium-image-zoom.js -o lib/react-medium-image-zoom.js",
"build:js": "babel src --out-dir lib",
"build:example": "browserify example/src/app.js -o example/build/app.js -t [ babelify ]",
"build": "npm run build:js && npm run build:example",
"dev": "nodemon",
Expand Down
83 changes: 83 additions & 0 deletions src/FullWrapper.js
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
}
203 changes: 203 additions & 0 deletions src/ImageZoom.js
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
}

47 changes: 47 additions & 0 deletions src/Overlay.js
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
}
40 changes: 40 additions & 0 deletions src/ResizeWrapper.js
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
}
Loading

0 comments on commit e6f9fa2

Please sign in to comment.