Skip to content
Extraits de code Groupes Projets
Valider 60ebfa18 rédigé par Eugen Rochko's avatar Eugen Rochko
Parcourir les fichiers

Made modal system more generic

parent 13dfd8d1
Aucune requête de fusion associée trouvée
Affichage de
avec 285 ajouts et 290 suppressions
export const MEDIA_OPEN = 'MEDIA_OPEN';
export const MODAL_OPEN = 'MODAL_OPEN';
export const MODAL_CLOSE = 'MODAL_CLOSE';
export const MODAL_INDEX_DECREASE = 'MODAL_INDEX_DECREASE';
export const MODAL_INDEX_INCREASE = 'MODAL_INDEX_INCREASE';
export function openMedia(media, index) {
export function openModal(type, props) {
return {
type: MEDIA_OPEN,
media,
index
type: MODAL_OPEN,
modalType: type,
modalProps: props
};
};
......@@ -17,15 +14,3 @@ export function closeModal() {
type: MODAL_CLOSE
};
};
export function decreaseIndexInModal() {
return {
type: MODAL_INDEX_DECREASE
};
};
export function increaseIndexInModal() {
return {
type: MODAL_INDEX_INCREASE
};
};
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);
......@@ -17,7 +17,7 @@ import {
} from '../actions/accounts';
import { deleteStatus } from '../actions/statuses';
import { initReport } from '../actions/reports';
import { openMedia } from '../actions/modal';
import { openModal } from '../actions/modal';
import { createSelector } from 'reselect'
import { isMobile } from '../is_mobile'
......@@ -63,7 +63,7 @@ const mapDispatchToProps = (dispatch) => ({
},
onOpenMedia (media, index) {
dispatch(openMedia(media, index));
dispatch(openModal('MEDIA', { media, index }));
},
onBlock (account) {
......
......@@ -28,7 +28,7 @@ import {
import { ScrollContainer } from 'react-router-scroll';
import ColumnBackButton from '../../components/column_back_button';
import StatusContainer from '../../containers/status_container';
import { openMedia } from '../../actions/modal';
import { openModal } from '../../actions/modal';
import { isMobile } from '../../is_mobile'
const makeMapStateToProps = () => {
......@@ -99,7 +99,7 @@ const Status = React.createClass({
},
handleOpenMedia (media, index) {
this.props.dispatch(openMedia(media, index));
this.props.dispatch(openModal('MEDIA', { media, index }));
},
handleReport (status) {
......
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);
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;
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);
......@@ -47,7 +47,9 @@ const UI = React.createClass({
this.dragTargets.push(e.target);
}
this.setState({ draggingOver: true });
if (e.dataTransfer && e.dataTransfer.files.length > 0) {
this.setState({ draggingOver: true });
}
},
handleDragOver (e) {
......
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;
}
......
......@@ -1311,7 +1311,7 @@ button.active i.fa-retweet {
color: $color3;
}
.modal-container--nav {
.modal-container__nav {
color: $color5;
}
......@@ -1848,3 +1848,47 @@ button.active i.fa-retweet {
text-decoration: underline;
}
}
.modal-root__overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 9999;
opacity: 0;
background: rgba($color8, 0.7);
}
.modal-root__container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
align-content: space-around;
z-index: 9999;
opacity: 0;
pointer-events: none;
user-select: none;
}
.modal-root__modal {
pointer-events: auto;
display: flex;
}
.media-modal {
max-width: 80vw;
max-height: 80vh;
position: relative;
img, video {
max-width: 80vw;
max-height: 80vh;
}
}
0% Chargement en cours ou .
You are about to add 0 people to the discussion. Proceed with caution.
Veuillez vous inscrire ou vous pour commenter