From 60ebfa182f944d5803dd6f3d54aa5e9ef24fc922 Mon Sep 17 00:00:00 2001 From: Eugen Rochko Date: Sat, 1 Apr 2017 22:11:28 +0200 Subject: [PATCH] Made modal system more generic --- .../javascripts/components/actions/modal.jsx | 25 +-- .../components/components/lightbox.jsx | 82 --------- .../containers/status_container.jsx | 4 +- .../components/features/status/index.jsx | 4 +- .../features/ui/components/media_modal.jsx | 134 ++++++++++++++ .../features/ui/components/modal_root.jsx | 80 +++++++++ .../ui/containers/modal_container.jsx | 166 +----------------- .../components/features/ui/index.jsx | 4 +- .../javascripts/components/reducers/modal.jsx | 30 +--- app/assets/stylesheets/components.scss | 46 ++++- 10 files changed, 285 insertions(+), 290 deletions(-) delete mode 100644 app/assets/javascripts/components/components/lightbox.jsx create mode 100644 app/assets/javascripts/components/features/ui/components/media_modal.jsx create mode 100644 app/assets/javascripts/components/features/ui/components/modal_root.jsx diff --git a/app/assets/javascripts/components/actions/modal.jsx b/app/assets/javascripts/components/actions/modal.jsx index d19218c485..615cd6bfe8 100644 --- a/app/assets/javascripts/components/actions/modal.jsx +++ b/app/assets/javascripts/components/actions/modal.jsx @@ -1,14 +1,11 @@ -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 - }; -}; diff --git a/app/assets/javascripts/components/components/lightbox.jsx b/app/assets/javascripts/components/components/lightbox.jsx deleted file mode 100644 index f04ca47bae..0000000000 --- a/app/assets/javascripts/components/components/lightbox.jsx +++ /dev/null @@ -1,82 +0,0 @@ -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 ( - - {({ backgroundOpacity, opacity, y }) => -
-
- - {children} -
-
- } -
- ); - } - -}); - -export default injectIntl(Lightbox); diff --git a/app/assets/javascripts/components/containers/status_container.jsx b/app/assets/javascripts/components/containers/status_container.jsx index e7543bc397..fd3fbe4c32 100644 --- a/app/assets/javascripts/components/containers/status_container.jsx +++ b/app/assets/javascripts/components/containers/status_container.jsx @@ -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) { diff --git a/app/assets/javascripts/components/features/status/index.jsx b/app/assets/javascripts/components/features/status/index.jsx index 6a7635cc66..f98fe1b013 100644 --- a/app/assets/javascripts/components/features/status/index.jsx +++ b/app/assets/javascripts/components/features/status/index.jsx @@ -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) { diff --git a/app/assets/javascripts/components/features/ui/components/media_modal.jsx b/app/assets/javascripts/components/features/ui/components/media_modal.jsx new file mode 100644 index 0000000000..e8b7180944 --- /dev/null +++ b/app/assets/javascripts/components/features/ui/components/media_modal.jsx @@ -0,0 +1,134 @@ +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 =
; + rightNav =
; + } + + if (attachment.get('type') === 'image') { + content = ; + } else if (attachment.get('type') === 'gifv') { + content = ; + } + + return ( +
+ {leftNav} + +
+ + {content} +
+ + {rightNav} +
+ ); + } + +}); + +export default injectIntl(MediaModal); diff --git a/app/assets/javascripts/components/features/ui/components/modal_root.jsx b/app/assets/javascripts/components/features/ui/components/modal_root.jsx new file mode 100644 index 0000000000..d2ae5e145e --- /dev/null +++ b/app/assets/javascripts/components/features/ui/components/modal_root.jsx @@ -0,0 +1,80 @@ +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 ( + + {interpolatedStyles => +
+ {interpolatedStyles.map(({ key, data: { type, props }, style }) => { + const SpecificComponent = MODAL_COMPONENTS[type]; + + return ( +
+
+
+ +
+
+ ); + })} +
+ } + + ); + } + +}); + +export default ModalRoot; diff --git a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx index e3c4281b93..26d77818c2 100644 --- a/app/assets/javascripts/components/features/ui/containers/modal_container.jsx +++ b/app/assets/javascripts/components/features/ui/containers/modal_container.jsx @@ -1,170 +1,16 @@ 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 = () => ( -
- -
-); - -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 =
; - rightNav =
; - } - - if (attachment.get('type') === 'image') { - content = ( - - ); - } else if (attachment.get('type') === 'gifv') { - content = ; - } - - return ( - - {leftNav} - {content} - {rightNav} - - ); - } - -}); - -export default connect(mapStateToProps, mapDispatchToProps)(Modal); +export default connect(mapStateToProps, mapDispatchToProps)(ModalRoot); diff --git a/app/assets/javascripts/components/features/ui/index.jsx b/app/assets/javascripts/components/features/ui/index.jsx index 4b7e4bb460..89fb82568f 100644 --- a/app/assets/javascripts/components/features/ui/index.jsx +++ b/app/assets/javascripts/components/features/ui/index.jsx @@ -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) { diff --git a/app/assets/javascripts/components/reducers/modal.jsx b/app/assets/javascripts/components/reducers/modal.jsx index 37ffbc62b2..3566820ef1 100644 --- a/app/assets/javascripts/components/reducers/modal.jsx +++ b/app/assets/javascripts/components/reducers/modal.jsx @@ -1,31 +1,17 @@ -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; } diff --git a/app/assets/stylesheets/components.scss b/app/assets/stylesheets/components.scss index a4dce7f181..d87e094535 100644 --- a/app/assets/stylesheets/components.scss +++ b/app/assets/stylesheets/components.scss @@ -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; + } +}