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 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 {
type: MEDIA_OPEN,
url: url
media,
index
};
};
@ -13,3 +17,15 @@ export function closeModal() {
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);
},
stopPropagation (e) {
e.stopPropagation();
},
render () {
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 }) }}>
{({ 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 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} />
{children}
</div>

View File

@ -57,15 +57,16 @@ const MediaGallery = React.createClass({
sensitive: React.PropTypes.bool,
media: ImmutablePropTypes.list.isRequired,
height: React.PropTypes.number.isRequired,
onOpenMedia: React.PropTypes.func.isRequired
onOpenMedia: React.PropTypes.func.isRequired,
intl: React.PropTypes.object.isRequired
},
mixins: [PureRenderMixin],
handleClick (url, e) {
handleClick (index, e) {
if (e.button === 0) {
e.preventDefault();
this.props.onOpenMedia(url);
this.props.onOpenMedia(this.props.media, index);
}
e.stopPropagation();
@ -151,12 +152,12 @@ const MediaGallery = React.createClass({
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}%` }}>
<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>
);
});
}
return (
<div style={{ ...outerStyle, height: `${this.props.height}px` }}>
<div style={spoilerButtonStyle} >

View File

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

View File

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

View File

@ -1,12 +1,18 @@
import { connect } from 'react-redux';
import { closeModal } from '../../../actions/modal';
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';
const mapStateToProps = state => ({
url: state.getIn(['modal', 'url']),
media: state.getIn(['modal', 'media']),
index: state.getIn(['modal', 'index']),
isVisible: state.getIn(['modal', 'open'])
});
@ -17,6 +23,14 @@ const mapDispatchToProps = dispatch => ({
onOverlayClicked () {
dispatch(closeModal());
},
onNextClicked () {
dispatch(increaseIndexInModal());
},
onPrevClicked () {
dispatch(decreaseIndexInModal());
}
});
@ -38,27 +52,92 @@ const preloader = () => (
</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({
propTypes: {
url: React.PropTypes.string,
media: ImmutablePropTypes.list,
index: React.PropTypes.number.isRequired,
isVisible: React.PropTypes.bool,
onCloseClicked: React.PropTypes.func,
onOverlayClicked: 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();
},
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 (
<Lightbox {...other}>
{leftNav}
<ImageLoader
src={url}
preloader={preloader}
imgProps={{ style: imageStyle }}
/>
{rightNav}
</Lightbox>
);
}

View File

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