Add next/previous navigation in modal for media attachments

This commit is contained in:
Eugen Rochko 2017-02-05 02:48:11 +01:00
parent 21972bb398
commit 44fad0160f
7 changed files with 132 additions and 21 deletions

View File

@ -1,10 +1,14 @@
export const MEDIA_OPEN = 'MEDIA_OPEN'; export const MEDIA_OPEN = 'MEDIA_OPEN';
export const MODAL_CLOSE = 'MODAL_CLOSE'; export const MODAL_CLOSE = 'MODAL_CLOSE';
export function openMedia(url) { export const MODAL_INDEX_DECREASE = 'MODAL_INDEX_DECREASE';
export const MODAL_INDEX_INCREASE = 'MODAL_INDEX_INCREASE';
export function openMedia(media, index) {
return { return {
type: MEDIA_OPEN, type: MEDIA_OPEN,
url: url media,
index
}; };
}; };
@ -13,3 +17,15 @@ export function closeModal() {
type: MODAL_CLOSE type: MODAL_CLOSE
}; };
}; };
export function decreaseIndexInModal() {
return {
type: MODAL_INDEX_DECREASE
};
};
export function increaseIndexInModal() {
return {
type: MODAL_INDEX_INCREASE
};
};

View File

@ -56,6 +56,10 @@ const Lightbox = React.createClass({
window.removeEventListener('keyup', this._listener); window.removeEventListener('keyup', this._listener);
}, },
stopPropagation (e) {
e.stopPropagation();
},
render () { render () {
const { intl, isVisible, onOverlayClicked, onCloseClicked, children } = this.props; const { intl, isVisible, onOverlayClicked, onCloseClicked, children } = this.props;
@ -63,7 +67,7 @@ const Lightbox = React.createClass({
<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 }) }}> <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 }) => {({ backgroundOpacity, opacity, y }) =>
<div className='lightbox' style={{...overlayStyle, background: `rgba(0, 0, 0, ${backgroundOpacity / 100})`, display: Math.floor(backgroundOpacity) === 0 ? 'none' : 'flex'}} onClick={onOverlayClicked}> <div className='lightbox' style={{...overlayStyle, background: `rgba(0, 0, 0, ${backgroundOpacity / 100})`, display: Math.floor(backgroundOpacity) === 0 ? 'none' : 'flex'}} onClick={onOverlayClicked}>
<div style={{...dialogStyle, transform: `translateY(${y}px)`, opacity: opacity / 100 }}> <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} /> <IconButton title={intl.formatMessage({ id: 'lightbox.close', defaultMessage: 'Close' })} icon='times' onClick={onCloseClicked} size={16} style={closeStyle} />
{children} {children}
</div> </div>

View File

@ -57,15 +57,16 @@ const MediaGallery = React.createClass({
sensitive: React.PropTypes.bool, sensitive: React.PropTypes.bool,
media: ImmutablePropTypes.list.isRequired, media: ImmutablePropTypes.list.isRequired,
height: React.PropTypes.number.isRequired, height: React.PropTypes.number.isRequired,
onOpenMedia: React.PropTypes.func.isRequired onOpenMedia: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
}, },
mixins: [PureRenderMixin], mixins: [PureRenderMixin],
handleClick (url, e) { handleClick (index, e) {
if (e.button === 0) { if (e.button === 0) {
e.preventDefault(); e.preventDefault();
this.props.onOpenMedia(url); this.props.onOpenMedia(this.props.media, index);
} }
e.stopPropagation(); e.stopPropagation();
@ -151,7 +152,7 @@ const MediaGallery = React.createClass({
return ( return (
<div key={attachment.get('id')} style={{ boxSizing: 'border-box', position: 'relative', left: left, top: top, right: right, bottom: bottom, float: 'left', border: 'none', display: 'block', width: `${width}%`, height: `${height}%` }}> <div key={attachment.get('id')} style={{ boxSizing: 'border-box', position: 'relative', left: left, top: top, right: right, bottom: bottom, float: 'left', border: 'none', display: 'block', width: `${width}%`, height: `${height}%` }}>
<a href={attachment.get('remote_url') ? attachment.get('remote_url') : attachment.get('url')} onClick={this.handleClick.bind(this, attachment.get('url'))} target='_blank' style={{ display: 'block', width: '100%', height: '100%', background: `url(${attachment.get('preview_url')}) no-repeat center`, textDecoration: 'none', backgroundSize: 'cover', cursor: 'zoom-in' }} /> <a href={attachment.get('remote_url') ? attachment.get('remote_url') : attachment.get('url')} onClick={this.handleClick.bind(this, i)} target='_blank' style={{ display: 'block', width: '100%', height: '100%', background: `url(${attachment.get('preview_url')}) no-repeat center`, textDecoration: 'none', backgroundSize: 'cover', cursor: 'zoom-in' }} />
</div> </div>
); );
}); });

View File

@ -91,8 +91,8 @@ const mapDispatchToProps = (dispatch) => ({
dispatch(mentionCompose(account, router)); dispatch(mentionCompose(account, router));
}, },
onOpenMedia (url) { onOpenMedia (media, index) {
dispatch(openMedia(url)); dispatch(openMedia(media, index));
}, },
onBlock (account) { onBlock (account) {

View File

@ -84,8 +84,8 @@ const Status = React.createClass({
this.props.dispatch(mentionCompose(account, router)); this.props.dispatch(mentionCompose(account, router));
}, },
handleOpenMedia (url) { handleOpenMedia (media, index) {
this.props.dispatch(openMedia(url)); this.props.dispatch(openMedia(media, index));
}, },
renderChildren (list) { renderChildren (list) {

View File

@ -1,12 +1,18 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { closeModal } from '../../../actions/modal'; import {
closeModal,
decreaseIndexInModal,
increaseIndexInModal
} from '../../../actions/modal';
import Lightbox from '../../../components/lightbox'; import Lightbox from '../../../components/lightbox';
import ImageLoader from 'react-imageloader'; import ImageLoader from 'react-imageloader';
import LoadingIndicator from '../../../components/loading_indicator'; import LoadingIndicator from '../../../components/loading_indicator';
import PureRenderMixin from 'react-addons-pure-render-mixin'; import PureRenderMixin from 'react-addons-pure-render-mixin';
import ImmutablePropTypes from 'react-immutable-proptypes';
const mapStateToProps = state => ({ const mapStateToProps = state => ({
url: state.getIn(['modal', 'url']), media: state.getIn(['modal', 'media']),
index: state.getIn(['modal', 'index']),
isVisible: state.getIn(['modal', 'open']) isVisible: state.getIn(['modal', 'open'])
}); });
@ -17,6 +23,14 @@ const mapDispatchToProps = dispatch => ({
onOverlayClicked () { onOverlayClicked () {
dispatch(closeModal()); dispatch(closeModal());
},
onNextClicked () {
dispatch(increaseIndexInModal());
},
onPrevClicked () {
dispatch(decreaseIndexInModal());
} }
}); });
@ -38,27 +52,92 @@ const preloader = () => (
</div> </div>
); );
const leftNavStyle = {
position: 'absolute',
background: 'rgba(0, 0, 0, 0.5)',
padding: '30px 15px',
cursor: 'pointer',
color: '#fff',
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',
color: '#fff',
fontSize: '24px',
top: '0',
right: '-61px',
boxSizing: 'border-box',
height: '100%',
display: 'flex',
alignItems: 'center'
};
const Modal = React.createClass({ const Modal = React.createClass({
propTypes: { propTypes: {
url: React.PropTypes.string, media: ImmutablePropTypes.list,
index: React.PropTypes.number.isRequired,
isVisible: React.PropTypes.bool, isVisible: React.PropTypes.bool,
onCloseClicked: React.PropTypes.func, onCloseClicked: React.PropTypes.func,
onOverlayClicked: React.PropTypes.func onOverlayClicked: React.PropTypes.func,
onNextClicked: React.PropTypes.func,
onPrevClicked: React.PropTypes.func
}, },
mixins: [PureRenderMixin], mixins: [PureRenderMixin],
handleNextClick () {
this.props.onNextClicked();
},
handlePrevClick () {
this.props.onPrevClicked();
},
render () { render () {
const { url, ...other } = this.props; const { media, index, ...other } = this.props;
if (!media) {
return null;
}
const url = media.get(index).get('url');
const hasLeft = index > 0;
const hasRight = index + 1 < media.size;
let leftNav, rightNav;
leftNav = rightNav = '';
if (hasLeft) {
leftNav = <div style={leftNavStyle} onClick={this.handlePrevClick}><i className='fa fa-fw fa-chevron-left' /></div>;
}
if (hasRight) {
rightNav = <div style={rightNavStyle} onClick={this.handleNextClick}><i className='fa fa-fw fa-chevron-right' /></div>;
}
return ( return (
<Lightbox {...other}> <Lightbox {...other}>
{leftNav}
<ImageLoader <ImageLoader
src={url} src={url}
preloader={preloader} preloader={preloader}
imgProps={{ style: imageStyle }} imgProps={{ style: imageStyle }}
/> />
{rightNav}
</Lightbox> </Lightbox>
); );
} }

View File

@ -1,8 +1,14 @@
import { MEDIA_OPEN, MODAL_CLOSE } from '../actions/modal'; import {
import Immutable from 'immutable'; MEDIA_OPEN,
MODAL_CLOSE,
MODAL_INDEX_DECREASE,
MODAL_INDEX_INCREASE
} from '../actions/modal';
import Immutable from 'immutable';
const initialState = Immutable.Map({ const initialState = Immutable.Map({
url: '', media: null,
index: 0,
open: false open: false
}); });
@ -10,11 +16,16 @@ export default function modal(state = initialState, action) {
switch(action.type) { switch(action.type) {
case MEDIA_OPEN: case MEDIA_OPEN:
return state.withMutations(map => { return state.withMutations(map => {
map.set('url', action.url); map.set('media', action.media);
map.set('index', action.index);
map.set('open', true); map.set('open', true);
}); });
case MODAL_CLOSE: case MODAL_CLOSE:
return state.set('open', false); return state.set('open', false);
case MODAL_INDEX_DECREASE:
return state.update('index', index => Math.max(index - 1, 0));
case MODAL_INDEX_INCREASE:
return state.update('index', index => Math.min(index + 1, state.get('media').size - 1));
default: default:
return state; return state;
} }