@ -1,82 +0,0 @@ | |||||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | |||||
import IconButton from './icon_button'; | |||||
import { Motion, spring } from 'react-motion'; | |||||
import { injectIntl } from 'react-intl'; | |||||
const overlayStyle = { | |||||
position: 'fixed', | |||||
top: '0', | |||||
left: '0', | |||||
width: '100%', | |||||
height: '100%', | |||||
background: 'rgba(0, 0, 0, 0.5)', | |||||
display: 'flex', | |||||
justifyContent: 'center', | |||||
alignContent: 'center', | |||||
flexDirection: 'row', | |||||
zIndex: '9999' | |||||
}; | |||||
const dialogStyle = { | |||||
color: '#282c37', | |||||
boxShadow: '0 0 30px rgba(0, 0, 0, 0.8)', | |||||
margin: 'auto', | |||||
position: 'relative' | |||||
}; | |||||
const closeStyle = { | |||||
position: 'absolute', | |||||
top: '4px', | |||||
right: '4px' | |||||
}; | |||||
const Lightbox = React.createClass({ | |||||
propTypes: { | |||||
isVisible: React.PropTypes.bool, | |||||
onOverlayClicked: React.PropTypes.func, | |||||
onCloseClicked: React.PropTypes.func, | |||||
intl: React.PropTypes.object.isRequired, | |||||
children: React.PropTypes.node | |||||
}, | |||||
mixins: [PureRenderMixin], | |||||
componentDidMount () { | |||||
this._listener = e => { | |||||
if (this.props.isVisible && e.key === 'Escape') { | |||||
this.props.onCloseClicked(); | |||||
} | |||||
}; | |||||
window.addEventListener('keyup', this._listener); | |||||
}, | |||||
componentWillUnmount () { | |||||
window.removeEventListener('keyup', this._listener); | |||||
}, | |||||
stopPropagation (e) { | |||||
e.stopPropagation(); | |||||
}, | |||||
render () { | |||||
const { intl, isVisible, onOverlayClicked, onCloseClicked, children } = this.props; | |||||
return ( | |||||
<Motion defaultStyle={{ backgroundOpacity: 0, opacity: 0, y: -400 }} style={{ backgroundOpacity: spring(isVisible ? 50 : 0), opacity: isVisible ? spring(200) : 0, y: spring(isVisible ? 0 : -400, { stiffness: 150, damping: 12 }) }}> | |||||
{({ backgroundOpacity, opacity, y }) => | |||||
<div className='lightbox' style={{...overlayStyle, background: `rgba(0, 0, 0, ${backgroundOpacity / 100})`, display: Math.floor(backgroundOpacity) === 0 ? 'none' : 'flex', pointerEvents: !isVisible ? 'none' : 'auto'}} onClick={onOverlayClicked}> | |||||
<div style={{...dialogStyle, transform: `translateY(${y}px)`, opacity: opacity / 100 }} onClick={this.stopPropagation}> | |||||
<IconButton title={intl.formatMessage({ id: 'lightbox.close', defaultMessage: 'Close' })} icon='times' onClick={onCloseClicked} size={16} style={closeStyle} /> | |||||
{children} | |||||
</div> | |||||
</div> | |||||
} | |||||
</Motion> | |||||
); | |||||
} | |||||
}); | |||||
export default injectIntl(Lightbox); |
@ -0,0 +1,134 @@ | |||||
import Lightbox from '../../../components/lightbox'; | |||||
import LoadingIndicator from '../../../components/loading_indicator'; | |||||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | |||||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||||
import ExtendedVideoPlayer from '../../../components/extended_video_player'; | |||||
import ImageLoader from 'react-imageloader'; | |||||
import { defineMessages, injectIntl } from 'react-intl'; | |||||
import IconButton from '../../../components/icon_button'; | |||||
const messages = defineMessages({ | |||||
close: { id: 'lightbox.close', defaultMessage: 'Close' } | |||||
}); | |||||
const leftNavStyle = { | |||||
position: 'absolute', | |||||
background: 'rgba(0, 0, 0, 0.5)', | |||||
padding: '30px 15px', | |||||
cursor: 'pointer', | |||||
fontSize: '24px', | |||||
top: '0', | |||||
left: '-61px', | |||||
boxSizing: 'border-box', | |||||
height: '100%', | |||||
display: 'flex', | |||||
alignItems: 'center' | |||||
}; | |||||
const rightNavStyle = { | |||||
position: 'absolute', | |||||
background: 'rgba(0, 0, 0, 0.5)', | |||||
padding: '30px 15px', | |||||
cursor: 'pointer', | |||||
fontSize: '24px', | |||||
top: '0', | |||||
right: '-61px', | |||||
boxSizing: 'border-box', | |||||
height: '100%', | |||||
display: 'flex', | |||||
alignItems: 'center' | |||||
}; | |||||
const closeStyle = { | |||||
position: 'absolute', | |||||
top: '4px', | |||||
right: '4px' | |||||
}; | |||||
const MediaModal = React.createClass({ | |||||
propTypes: { | |||||
media: ImmutablePropTypes.list.isRequired, | |||||
index: React.PropTypes.number.isRequired, | |||||
onClose: React.PropTypes.func.isRequired, | |||||
intl: React.PropTypes.object.isRequired | |||||
}, | |||||
getInitialState () { | |||||
return { | |||||
index: null | |||||
}; | |||||
}, | |||||
mixins: [PureRenderMixin], | |||||
handleNextClick () { | |||||
this.setState({ index: (this.getIndex() + 1) % this.props.media.size}); | |||||
}, | |||||
handlePrevClick () { | |||||
this.setState({ index: (this.getIndex() - 1) % this.props.media.size}); | |||||
}, | |||||
handleKeyUp (e) { | |||||
switch(e.key) { | |||||
case 'ArrowLeft': | |||||
this.handlePrevClick(); | |||||
break; | |||||
case 'ArrowRight': | |||||
this.handleNextClick(); | |||||
break; | |||||
} | |||||
}, | |||||
componentDidMount () { | |||||
window.addEventListener('keyup', this.handleKeyUp, false); | |||||
}, | |||||
componentWillUnmount () { | |||||
window.removeEventListener('keyup', this.handleKeyUp); | |||||
}, | |||||
getIndex () { | |||||
return this.state.index !== null ? this.state.index : this.props.index; | |||||
}, | |||||
render () { | |||||
const { media, intl, onClose } = this.props; | |||||
const index = this.getIndex(); | |||||
const attachment = media.get(index); | |||||
const url = attachment.get('url'); | |||||
let leftNav, rightNav, content; | |||||
leftNav = rightNav = content = ''; | |||||
if (media.size > 1) { | |||||
leftNav = <div style={leftNavStyle} className='modal-container__nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>; | |||||
rightNav = <div style={rightNavStyle} className='modal-container__nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>; | |||||
} | |||||
if (attachment.get('type') === 'image') { | |||||
content = <ImageLoader src={url} imgProps={{ style: { display: 'block' } }} />; | |||||
} else if (attachment.get('type') === 'gifv') { | |||||
content = <ExtendedVideoPlayer src={url} />; | |||||
} | |||||
return ( | |||||
<div className='modal-root__modal media-modal'> | |||||
{leftNav} | |||||
<div> | |||||
<IconButton title={intl.formatMessage(messages.close)} icon='times' onClick={onClose} size={16} style={closeStyle} /> | |||||
{content} | |||||
</div> | |||||
{rightNav} | |||||
</div> | |||||
); | |||||
} | |||||
}); | |||||
export default injectIntl(MediaModal); |
@ -0,0 +1,80 @@ | |||||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | |||||
import MediaModal from './media_modal'; | |||||
import { TransitionMotion, spring } from 'react-motion'; | |||||
const MODAL_COMPONENTS = { | |||||
'MEDIA': MediaModal | |||||
}; | |||||
const ModalRoot = React.createClass({ | |||||
propTypes: { | |||||
type: React.PropTypes.string, | |||||
props: React.PropTypes.object, | |||||
onClose: React.PropTypes.func.isRequired | |||||
}, | |||||
mixins: [PureRenderMixin], | |||||
handleKeyUp (e) { | |||||
if (e.key === 'Escape' && !!this.props.type) { | |||||
this.props.onClose(); | |||||
} | |||||
}, | |||||
componentDidMount () { | |||||
window.addEventListener('keyup', this.handleKeyUp, false); | |||||
}, | |||||
componentWillUnmount () { | |||||
window.removeEventListener('keyup', this.handleKeyUp); | |||||
}, | |||||
willEnter () { | |||||
return { opacity: 0, scale: 0.98 }; | |||||
}, | |||||
willLeave () { | |||||
return { opacity: spring(0), scale: spring(0.98) }; | |||||
}, | |||||
render () { | |||||
const { type, props, onClose } = this.props; | |||||
const items = []; | |||||
if (!!type) { | |||||
items.push({ | |||||
key: type, | |||||
data: { type, props }, | |||||
style: { opacity: spring(1), scale: spring(1, { stiffness: 120, damping: 14 }) } | |||||
}); | |||||
} | |||||
return ( | |||||
<TransitionMotion | |||||
styles={items} | |||||
willEnter={this.willEnter} | |||||
willLeave={this.willLeave}> | |||||
{interpolatedStyles => | |||||
<div className='modal-root'> | |||||
{interpolatedStyles.map(({ key, data: { type, props }, style }) => { | |||||
const SpecificComponent = MODAL_COMPONENTS[type]; | |||||
return ( | |||||
<div key={key}> | |||||
<div className='modal-root__overlay' style={{ opacity: style.opacity, transform: `translateZ(0px)` }} onClick={onClose} /> | |||||
<div className='modal-root__container' style={{ opacity: style.opacity, transform: `translateZ(0px) scale(${style.scale})` }}> | |||||
<SpecificComponent {...props} onClose={onClose} /> | |||||
</div> | |||||
</div> | |||||
); | |||||
})} | |||||
</div> | |||||
} | |||||
</TransitionMotion> | |||||
); | |||||
} | |||||
}); | |||||
export default ModalRoot; |
@ -1,170 +1,16 @@ | |||||
import { connect } from 'react-redux'; | import { connect } from 'react-redux'; | ||||
import { | |||||
closeModal, | |||||
decreaseIndexInModal, | |||||
increaseIndexInModal | |||||
} from '../../../actions/modal'; | |||||
import Lightbox from '../../../components/lightbox'; | |||||
import ImageLoader from 'react-imageloader'; | |||||
import LoadingIndicator from '../../../components/loading_indicator'; | |||||
import PureRenderMixin from 'react-addons-pure-render-mixin'; | |||||
import ImmutablePropTypes from 'react-immutable-proptypes'; | |||||
import ExtendedVideoPlayer from '../../../components/extended_video_player'; | |||||
import { closeModal } from '../../../actions/modal'; | |||||
import ModalRoot from '../components/modal_root'; | |||||
const mapStateToProps = state => ({ | const mapStateToProps = state => ({ | ||||
media: state.getIn(['modal', 'media']), | |||||
index: state.getIn(['modal', 'index']), | |||||
isVisible: state.getIn(['modal', 'open']) | |||||
type: state.get('modal').modalType, | |||||
props: state.get('modal').modalProps | |||||
}); | }); | ||||
const mapDispatchToProps = dispatch => ({ | const mapDispatchToProps = dispatch => ({ | ||||
onCloseClicked () { | |||||
onClose () { | |||||
dispatch(closeModal()); | dispatch(closeModal()); | ||||
}, | }, | ||||
onOverlayClicked () { | |||||
dispatch(closeModal()); | |||||
}, | |||||
onNextClicked () { | |||||
dispatch(increaseIndexInModal()); | |||||
}, | |||||
onPrevClicked () { | |||||
dispatch(decreaseIndexInModal()); | |||||
} | |||||
}); | |||||
const imageStyle = { | |||||
display: 'block', | |||||
maxWidth: '80vw', | |||||
maxHeight: '80vh' | |||||
}; | |||||
const loadingStyle = { | |||||
width: '400px', | |||||
paddingBottom: '120px' | |||||
}; | |||||
const preloader = () => ( | |||||
<div className='modal-container--preloader' style={loadingStyle}> | |||||
<LoadingIndicator /> | |||||
</div> | |||||
); | |||||
const leftNavStyle = { | |||||
position: 'absolute', | |||||
background: 'rgba(0, 0, 0, 0.5)', | |||||
padding: '30px 15px', | |||||
cursor: 'pointer', | |||||
fontSize: '24px', | |||||
top: '0', | |||||
left: '-61px', | |||||
boxSizing: 'border-box', | |||||
height: '100%', | |||||
display: 'flex', | |||||
alignItems: 'center' | |||||
}; | |||||
const rightNavStyle = { | |||||
position: 'absolute', | |||||
background: 'rgba(0, 0, 0, 0.5)', | |||||
padding: '30px 15px', | |||||
cursor: 'pointer', | |||||
fontSize: '24px', | |||||
top: '0', | |||||
right: '-61px', | |||||
boxSizing: 'border-box', | |||||
height: '100%', | |||||
display: 'flex', | |||||
alignItems: 'center' | |||||
}; | |||||
const Modal = React.createClass({ | |||||
propTypes: { | |||||
media: ImmutablePropTypes.list, | |||||
index: React.PropTypes.number.isRequired, | |||||
isVisible: React.PropTypes.bool, | |||||
onCloseClicked: React.PropTypes.func, | |||||
onOverlayClicked: React.PropTypes.func, | |||||
onNextClicked: React.PropTypes.func, | |||||
onPrevClicked: React.PropTypes.func | |||||
}, | |||||
mixins: [PureRenderMixin], | |||||
handleNextClick () { | |||||
this.props.onNextClicked(); | |||||
}, | |||||
handlePrevClick () { | |||||
this.props.onPrevClicked(); | |||||
}, | |||||
componentDidMount () { | |||||
this._listener = e => { | |||||
if (!this.props.isVisible) { | |||||
return; | |||||
} | |||||
switch(e.key) { | |||||
case 'ArrowLeft': | |||||
this.props.onPrevClicked(); | |||||
break; | |||||
case 'ArrowRight': | |||||
this.props.onNextClicked(); | |||||
break; | |||||
} | |||||
}; | |||||
window.addEventListener('keyup', this._listener); | |||||
}, | |||||
componentWillUnmount () { | |||||
window.removeEventListener('keyup', this._listener); | |||||
}, | |||||
render () { | |||||
const { media, index, ...other } = this.props; | |||||
if (!media) { | |||||
return null; | |||||
} | |||||
const attachment = media.get(index); | |||||
const url = attachment.get('url'); | |||||
let leftNav, rightNav, content; | |||||
leftNav = rightNav = content = ''; | |||||
if (media.size > 1) { | |||||
leftNav = <div style={leftNavStyle} className='modal-container--nav' onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>; | |||||
rightNav = <div style={rightNavStyle} className='modal-container--nav' onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>; | |||||
} | |||||
if (attachment.get('type') === 'image') { | |||||
content = ( | |||||
<ImageLoader | |||||
src={url} | |||||
preloader={preloader} | |||||
imgProps={{ style: imageStyle }} | |||||
/> | |||||
); | |||||
} else if (attachment.get('type') === 'gifv') { | |||||
content = <ExtendedVideoPlayer src={url} />; | |||||
} | |||||
return ( | |||||
<Lightbox {...other}> | |||||
{leftNav} | |||||
{content} | |||||
{rightNav} | |||||
</Lightbox> | |||||
); | |||||
} | |||||
}); | }); | ||||
export default connect(mapStateToProps, mapDispatchToProps)(Modal); | |||||
export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot); |
@ -1,31 +1,17 @@ | |||||
import { | |||||
MEDIA_OPEN, | |||||
MODAL_CLOSE, | |||||
MODAL_INDEX_DECREASE, | |||||
MODAL_INDEX_INCREASE | |||||
} from '../actions/modal'; | |||||
import { MODAL_OPEN, MODAL_CLOSE } from '../actions/modal'; | |||||
import Immutable from 'immutable'; | import Immutable from 'immutable'; | ||||
const initialState = Immutable.Map({ | |||||
media: null, | |||||
index: 0, | |||||
open: false | |||||
}); | |||||
const initialState = { | |||||
modalType: null, | |||||
modalProps: {} | |||||
}; | |||||
export default function modal(state = initialState, action) { | export default function modal(state = initialState, action) { | ||||
switch(action.type) { | switch(action.type) { | ||||
case MEDIA_OPEN: | |||||
return state.withMutations(map => { | |||||
map.set('media', action.media); | |||||
map.set('index', action.index); | |||||
map.set('open', true); | |||||
}); | |||||
case MODAL_OPEN: | |||||
return { modalType: action.modalType, modalProps: action.modalProps }; | |||||
case MODAL_CLOSE: | case MODAL_CLOSE: | ||||
return state.set('open', false); | |||||
case MODAL_INDEX_DECREASE: | |||||
return state.update('index', index => (index - 1) % state.get('media').size); | |||||
case MODAL_INDEX_INCREASE: | |||||
return state.update('index', index => (index + 1) % state.get('media').size); | |||||
return initialState; | |||||
default: | default: | ||||
return state; | return state; | ||||
} | } | ||||