Add next/previous navigation in modal for media attachments
parent
21972bb398
commit
44fad0160f
|
@ -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
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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,12 +152,12 @@ 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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ ...outerStyle, height: `${this.props.height}px` }}>
|
<div style={{ ...outerStyle, height: `${this.props.height}px` }}>
|
||||||
<div style={spoilerButtonStyle} >
|
<div style={spoilerButtonStyle} >
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Reference in New Issue