@ -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 { | |||
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 => ({ | |||
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 => ({ | |||
onCloseClicked () { | |||
onClose () { | |||
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'; | |||
const initialState = Immutable.Map({ | |||
media: null, | |||
index: 0, | |||
open: false | |||
}); | |||
const initialState = { | |||
modalType: null, | |||
modalProps: {} | |||
}; | |||
export default function modal(state = initialState, action) { | |||
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: | |||
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: | |||
return state; | |||
} | |||