commit: 44fad0160f4b390a97fae8bb23cdc98ccf5649d9
parent: 21972bb39886942d6946757ff8c8f9fe329bb20f
Author: Eugen Rochko <eugen@zeonfederated.com>
Date: Sun, 5 Feb 2017 02:48:11 +0100
Add next/previous navigation in modal for media attachments
Diffstat:
7 files changed, 132 insertions(+), 21 deletions(-)
diff --git a/app/assets/javascripts/components/actions/modal.jsx b/app/assets/javascripts/components/actions/modal.jsx
@@ -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
+ };
+};
diff --git a/app/assets/javascripts/components/components/lightbox.jsx b/app/assets/javascripts/components/components/lightbox.jsx
@@ -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>
diff --git a/app/assets/javascripts/components/components/media_gallery.jsx b/app/assets/javascripts/components/components/media_gallery.jsx
@@ -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} >
diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx
@@ -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) {
diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx
@@ -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) {
diff --git a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx
@@ -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>
);
}
diff --git a/app/assets/javascripts/components/reducers/modal.jsx b/app/assets/javascripts/components/reducers/modal.jsx
@@ -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;
}