diff options
8 files changed, 178 insertions, 116 deletions
diff --git a/app/javascript/mastodon/components/media_gallery.js b/app/javascript/mastodon/components/media_gallery.js index 1cef029d8..13e1fcc52 100644 --- a/app/javascript/mastodon/components/media_gallery.js +++ b/app/javascript/mastodon/components/media_gallery.js @@ -14,10 +14,6 @@ const messages = defineMessages({ class Item extends React.PureComponent { - static contextTypes = { - router: PropTypes.object, - }; - static propTypes = { attachment: ImmutablePropTypes.map.isRequired, standalone: PropTypes.bool, @@ -53,7 +49,7 @@ class Item extends React.PureComponent { handleClick = (e) => { const { index, onClick } = this.props; - if (this.context.router && e.button === 0) { + if (e.button === 0) { e.preventDefault(); onClick(index); } diff --git a/app/javascript/mastodon/components/modal_root.js b/app/javascript/mastodon/components/modal_root.js new file mode 100644 index 000000000..114f74937 --- /dev/null +++ b/app/javascript/mastodon/components/modal_root.js @@ -0,0 +1,84 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +export default class ModalRoot extends React.PureComponent { + + static propTypes = { + children: PropTypes.node, + onClose: PropTypes.func.isRequired, + }; + + state = { + revealed: !!this.props.children, + }; + + activeElement = this.state.revealed ? document.activeElement : null; + + handleKeyUp = (e) => { + if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) + && !!this.props.children) { + this.props.onClose(); + } + } + + componentDidMount () { + window.addEventListener('keyup', this.handleKeyUp, false); + } + + componentWillReceiveProps (nextProps) { + if (!!nextProps.children && !this.props.children) { + this.activeElement = document.activeElement; + + this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true)); + } else if (!nextProps.children) { + this.setState({ revealed: false }); + } + } + + componentDidUpdate (prevProps) { + if (!this.props.children && !!prevProps.children) { + this.getSiblings().forEach(sibling => sibling.removeAttribute('inert')); + this.activeElement.focus(); + this.activeElement = null; + } + if (this.props.children) { + requestAnimationFrame(() => { + this.setState({ revealed: true }); + }); + } + } + + componentWillUnmount () { + window.removeEventListener('keyup', this.handleKeyUp); + } + + getSiblings = () => { + return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node); + } + + setRef = ref => { + this.node = ref; + } + + render () { + const { children, onClose } = this.props; + const { revealed } = this.state; + const visible = !!children; + + if (!visible) { + return ( + <div className='modal-root' ref={this.setRef} style={{ opacity: 0 }} /> + ); + } + + return ( + <div className='modal-root' ref={this.setRef} style={{ opacity: revealed ? 1 : 0 }}> + <div style={{ pointerEvents: visible ? 'auto' : 'none' }}> + <div role='presentation' className='modal-root__overlay' onClick={onClose} /> + <div role='dialog' className='modal-root__container'>{children}</div> + </div> + </div> + ); + } + +} diff --git a/app/javascript/mastodon/containers/media_galleries_container.js b/app/javascript/mastodon/containers/media_galleries_container.js new file mode 100644 index 000000000..d77bd688b --- /dev/null +++ b/app/javascript/mastodon/containers/media_galleries_container.js @@ -0,0 +1,68 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import PropTypes from 'prop-types'; +import { IntlProvider, addLocaleData } from 'react-intl'; +import { getLocale } from '../locales'; +import MediaGallery from '../components/media_gallery'; +import ModalRoot from '../components/modal_root'; +import MediaModal from '../features/ui/components/media_modal'; +import { fromJS } from 'immutable'; + +const { localeData, messages } = getLocale(); +addLocaleData(localeData); + +export default class MediaGalleriesContainer extends React.PureComponent { + + static propTypes = { + locale: PropTypes.string.isRequired, + galleries: PropTypes.object.isRequired, + }; + + state = { + media: null, + index: null, + }; + + handleOpenMedia = (media, index) => { + document.body.classList.add('media-gallery-standalone__body'); + this.setState({ media, index }); + } + + handleCloseMedia = () => { + document.body.classList.remove('media-gallery-standalone__body'); + this.setState({ media: null, index: null }); + } + + render () { + const { locale, galleries } = this.props; + + return ( + <IntlProvider locale={locale} messages={messages}> + <React.Fragment> + {[].map.call(galleries, gallery => { + const { media, ...props } = JSON.parse(gallery.getAttribute('data-props')); + + return ReactDOM.createPortal( + <MediaGallery + {...props} + media={fromJS(media)} + onOpenMedia={this.handleOpenMedia} + />, + gallery + ); + })} + <ModalRoot onClose={this.handleCloseMedia}> + {this.state.media === null || this.state.index === null ? null : ( + <MediaModal + media={this.state.media} + index={this.state.index} + onClose={this.handleCloseMedia} + /> + )} + </ModalRoot> + </React.Fragment> + </IntlProvider> + ); + } + +} diff --git a/app/javascript/mastodon/containers/media_gallery_container.js b/app/javascript/mastodon/containers/media_gallery_container.js deleted file mode 100644 index 812c3d4e5..000000000 --- a/app/javascript/mastodon/containers/media_gallery_container.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { IntlProvider, addLocaleData } from 'react-intl'; -import { getLocale } from '../locales'; -import MediaGallery from '../components/media_gallery'; -import { fromJS } from 'immutable'; - -const { localeData, messages } = getLocale(); -addLocaleData(localeData); - -export default class MediaGalleryContainer extends React.PureComponent { - - static propTypes = { - locale: PropTypes.string.isRequired, - media: PropTypes.array.isRequired, - }; - - handleOpenMedia = () => {} - - render () { - const { locale, media, ...props } = this.props; - - return ( - <IntlProvider locale={locale} messages={messages}> - <MediaGallery - {...props} - media={fromJS(media)} - onOpenMedia={this.handleOpenMedia} - /> - </IntlProvider> - ); - } - -} diff --git a/app/javascript/mastodon/features/ui/components/modal_root.js b/app/javascript/mastodon/features/ui/components/modal_root.js index 20bf21153..4185cba32 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.js +++ b/app/javascript/mastodon/features/ui/components/modal_root.js @@ -1,5 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; +import Base from '../../../components/modal_root'; import BundleContainer from '../containers/bundle_container'; import BundleModalError from './bundle_modal_error'; import ModalLoading from './modal_loading'; @@ -39,56 +40,6 @@ export default class ModalRoot extends React.PureComponent { onClose: PropTypes.func.isRequired, }; - state = { - revealed: false, - }; - - handleKeyUp = (e) => { - if ((e.key === 'Escape' || e.key === 'Esc' || e.keyCode === 27) - && !!this.props.type) { - this.props.onClose(); - } - } - - componentDidMount () { - window.addEventListener('keyup', this.handleKeyUp, false); - } - - componentWillReceiveProps (nextProps) { - if (!!nextProps.type && !this.props.type) { - this.activeElement = document.activeElement; - - this.getSiblings().forEach(sibling => sibling.setAttribute('inert', true)); - } else if (!nextProps.type) { - this.setState({ revealed: false }); - } - } - - componentDidUpdate (prevProps) { - if (!this.props.type && !!prevProps.type) { - this.getSiblings().forEach(sibling => sibling.removeAttribute('inert')); - this.activeElement.focus(); - this.activeElement = null; - } - if (this.props.type) { - requestAnimationFrame(() => { - this.setState({ revealed: true }); - }); - } - } - - componentWillUnmount () { - window.removeEventListener('keyup', this.handleKeyUp); - } - - getSiblings = () => { - return Array(...this.node.parentElement.childNodes).filter(node => node !== this.node); - } - - setRef = ref => { - this.node = ref; - } - renderLoading = modalId => () => { return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null; } @@ -101,28 +52,16 @@ export default class ModalRoot extends React.PureComponent { render () { const { type, props, onClose } = this.props; - const { revealed } = this.state; const visible = !!type; - if (!visible) { - return ( - <div className='modal-root' ref={this.setRef} style={{ opacity: 0 }} /> - ); - } - return ( - <div className='modal-root' ref={this.setRef} style={{ opacity: revealed ? 1 : 0 }}> - <div style={{ pointerEvents: visible ? 'auto' : 'none' }}> - <div role='presentation' className='modal-root__overlay' onClick={onClose} /> - <div role='dialog' className='modal-root__container'> - {visible && ( - <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}> - {(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />} - </BundleContainer> - )} - </div> - </div> - </div> + <Base onClose={onClose}> + {visible && ( + <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}> + {(SpecificComponent) => <SpecificComponent {...props} onClose={onClose} />} + </BundleContainer> + )} + </Base> ); } diff --git a/app/javascript/packs/public.js b/app/javascript/packs/public.js index a47fc2830..7096b9b4f 100644 --- a/app/javascript/packs/public.js +++ b/app/javascript/packs/public.js @@ -25,7 +25,6 @@ function main() { const { getLocale } = require('../mastodon/locales'); const { localeData } = getLocale(); const VideoContainer = require('../mastodon/containers/video_container').default; - const MediaGalleryContainer = require('../mastodon/containers/media_gallery_container').default; const CardContainer = require('../mastodon/containers/card_container').default; const React = require('react'); const ReactDOM = require('react-dom'); @@ -76,15 +75,20 @@ function main() { ReactDOM.render(<VideoContainer locale={locale} {...props} />, content); }); - [].forEach.call(document.querySelectorAll('[data-component="MediaGallery"]'), (content) => { - const props = JSON.parse(content.getAttribute('data-props')); - ReactDOM.render(<MediaGalleryContainer locale={locale} {...props} />, content); - }); - [].forEach.call(document.querySelectorAll('[data-component="Card"]'), (content) => { const props = JSON.parse(content.getAttribute('data-props')); ReactDOM.render(<CardContainer locale={locale} {...props} />, content); }); + + const mediaGalleries = document.querySelectorAll('[data-component="MediaGallery"]'); + + if (mediaGalleries.length > 0) { + const MediaGalleriesContainer = require('../mastodon/containers/media_galleries_container').default; + const content = document.createElement('div'); + + ReactDOM.render(<MediaGalleriesContainer locale={locale} galleries={mediaGalleries} />, content); + document.body.appendChild(content); + } }); delegate(document, '.webapp-btn', 'click', ({ target, button }) => { diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 20e07a042..ea6e39392 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -3375,13 +3375,14 @@ a.status-card { } .modal-root { + position: relative; transition: opacity 0.3s linear; will-change: opacity; z-index: 9999; } .modal-root__overlay { - position: absolute; + position: fixed; top: 0; left: 0; right: 0; @@ -3390,7 +3391,7 @@ a.status-card { } .modal-root__container { - position: absolute; + position: fixed; top: 0; left: 0; width: 100%; diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss index 6fa1fa38f..e761f58eb 100644 --- a/app/javascript/styles/mastodon/containers.scss +++ b/app/javascript/styles/mastodon/containers.scss @@ -60,6 +60,10 @@ } } +.media-gallery-standalone__body { + overflow: hidden; +} + .account-header { width: 400px; margin: 0 auto; |