2022-03-21 11:09:01 -07:00
import classNames from 'classnames' ;
2022-01-10 14:17:52 -08:00
import { createBrowserHistory } from 'history' ;
2020-03-27 13:59:38 -07:00
import PropTypes from 'prop-types' ;
2022-01-10 14:17:52 -08:00
import React from 'react' ;
2021-08-28 05:09:29 -07:00
import 'wicg-inert' ;
2020-03-27 13:59:38 -07:00
import { injectIntl , FormattedMessage , defineMessages } from 'react-intl' ;
import { connect } from 'react-redux' ;
2022-03-17 18:17:28 -07:00
import { withRouter } from 'react-router-dom' ;
2022-01-10 14:25:06 -08:00
2020-03-27 13:59:38 -07:00
import { cancelReplyCompose } from '../actions/compose' ;
2022-02-02 05:33:12 -08:00
import { openModal , closeModal } from '../actions/modals' ;
2020-03-27 13:59:38 -07:00
const messages = defineMessages ( {
confirm : { id : 'confirmations.delete.confirm' , defaultMessage : 'Delete' } ,
} ) ;
2020-06-07 14:40:56 -07:00
const checkComposeContent = compose => {
return [
2022-06-20 10:59:51 -07:00
compose . text . length > 0 ,
compose . spoiler _text . length > 0 ,
compose . media _attachments . size > 0 ,
compose . in _reply _to !== null ,
compose . quote !== null ,
compose . poll !== null ,
2020-06-07 14:40:56 -07:00
] . some ( check => check === true ) ;
} ;
2020-03-27 13:59:38 -07:00
const mapStateToProps = state => ( {
2022-06-20 10:59:51 -07:00
hasComposeContent : checkComposeContent ( state . compose ) ,
isEditing : state . compose . id !== null ,
2020-03-27 13:59:38 -07:00
} ) ;
const mapDispatchToProps = ( dispatch ) => ( {
onOpenModal ( type , opts ) {
dispatch ( openModal ( type , opts ) ) ;
} ,
2022-01-30 09:46:57 -08:00
onCloseModal ( type ) {
dispatch ( closeModal ( type ) ) ;
} ,
2020-03-27 13:59:38 -07:00
onCancelReplyCompose ( ) {
2022-01-30 09:46:57 -08:00
dispatch ( closeModal ( 'COMPOSE' ) ) ;
2020-03-27 13:59:38 -07:00
dispatch ( cancelReplyCompose ( ) ) ;
2020-04-14 11:44:40 -07:00
} ,
2020-03-27 13:59:38 -07:00
} ) ;
2022-03-17 18:17:28 -07:00
@ withRouter
2020-03-27 13:59:38 -07:00
class ModalRoot extends React . PureComponent {
static propTypes = {
children : PropTypes . node ,
onClose : PropTypes . func . isRequired ,
onOpenModal : PropTypes . func . isRequired ,
2022-01-30 09:46:57 -08:00
onCloseModal : PropTypes . func . isRequired ,
2020-03-27 13:59:38 -07:00
onCancelReplyCompose : PropTypes . func . isRequired ,
intl : PropTypes . object . isRequired ,
2020-06-07 14:40:56 -07:00
hasComposeContent : PropTypes . bool ,
2022-06-11 14:36:40 -07:00
isEditing : PropTypes . bool ,
2020-03-27 13:59:38 -07:00
type : PropTypes . string ,
2022-01-06 08:45:10 -08:00
onCancel : PropTypes . func ,
2022-03-17 18:17:28 -07:00
history : PropTypes . object ,
2022-03-06 14:20:13 -08:00
} ;
2020-03-27 13:59:38 -07:00
state = {
revealed : ! ! this . props . children ,
} ;
activeElement = this . state . revealed ? document . activeElement : null ;
handleKeyUp = ( e ) => {
if ( ( e . key === 'Escape' || e . key === 'Esc' || e . keyCode === 27 )
2022-07-22 10:30:16 -07:00
&& ! ! this . props . children ) {
2020-03-27 13:59:38 -07:00
this . handleOnClose ( ) ;
}
}
handleOnClose = ( ) => {
2022-06-11 14:36:40 -07:00
const { onOpenModal , onCloseModal , hasComposeContent , isEditing , intl , type , onCancelReplyCompose } = this . props ;
2020-03-27 13:59:38 -07:00
2020-06-07 14:40:56 -07:00
if ( hasComposeContent && type === 'COMPOSE' ) {
2020-03-27 13:59:38 -07:00
onOpenModal ( 'CONFIRM' , {
2022-07-09 09:20:02 -07:00
icon : require ( '@tabler/icons/trash.svg' ) ,
2022-06-11 14:36:40 -07:00
heading : isEditing ? < FormattedMessage id = 'confirmations.cancel_editing.heading' defaultMessage = 'Cancel post editing' / > : < FormattedMessage id = 'confirmations.delete.heading' defaultMessage = 'Delete post' / > ,
message : isEditing ? < FormattedMessage id = 'confirmations.cancel_editing.message' defaultMessage = 'Are you sure you want to cancel editing this post? All changes will be lost.' / > : < FormattedMessage id = 'confirmations.delete.message' defaultMessage = 'Are you sure you want to delete this post?' / > ,
2020-03-27 13:59:38 -07:00
confirm : intl . formatMessage ( messages . confirm ) ,
onConfirm : ( ) => onCancelReplyCompose ( ) ,
2022-01-30 09:46:57 -08:00
onCancel : ( ) => onCloseModal ( 'CONFIRM' ) ,
2020-03-27 13:59:38 -07:00
} ) ;
2020-06-07 14:40:56 -07:00
} else if ( hasComposeContent && type === 'CONFIRM' ) {
2022-01-30 09:46:57 -08:00
onCloseModal ( 'CONFIRM' ) ;
2020-04-14 11:44:40 -07:00
} else {
2020-03-27 13:59:38 -07:00
this . props . onClose ( ) ;
}
} ;
2021-08-28 05:30:58 -07:00
handleKeyDown = ( e ) => {
if ( e . key === 'Tab' ) {
const focusable = Array . from ( this . node . querySelectorAll ( 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])' ) ) . filter ( ( x ) => window . getComputedStyle ( x ) . display !== 'none' ) ;
const index = focusable . indexOf ( e . target ) ;
let element ;
if ( e . shiftKey ) {
element = focusable [ index - 1 ] || focusable [ focusable . length - 1 ] ;
} else {
element = focusable [ index + 1 ] || focusable [ 0 ] ;
}
if ( element ) {
element . focus ( ) ;
e . stopPropagation ( ) ;
e . preventDefault ( ) ;
}
}
}
2020-04-14 14:47:35 -07:00
componentDidMount ( ) {
2020-03-27 13:59:38 -07:00
window . addEventListener ( 'keyup' , this . handleKeyUp , false ) ;
2021-08-28 05:30:58 -07:00
window . addEventListener ( 'keydown' , this . handleKeyDown , false ) ;
2022-03-17 18:17:28 -07:00
this . history = this . props . history || createBrowserHistory ( ) ;
2020-03-27 13:59:38 -07:00
}
2020-07-04 16:41:41 -07:00
componentDidUpdate ( prevProps ) {
if ( ! ! this . props . children && ! prevProps . children ) {
2020-03-27 13:59:38 -07:00
this . activeElement = document . activeElement ;
this . getSiblings ( ) . forEach ( sibling => sibling . setAttribute ( 'inert' , true ) ) ;
2022-01-06 08:45:10 -08:00
this . _handleModalOpen ( ) ;
2020-07-04 16:41:41 -07:00
} else if ( ! prevProps . children ) {
2020-03-27 13:59:38 -07:00
this . setState ( { revealed : false } ) ;
}
2020-07-04 16:41:41 -07:00
if ( ! this . props . children && ! ! prevProps . children ) {
2020-03-27 13:59:38 -07:00
this . activeElement . focus ( ) ;
this . activeElement = null ;
this . getSiblings ( ) . forEach ( sibling => sibling . removeAttribute ( 'inert' ) ) ;
2022-01-06 08:45:10 -08:00
2022-01-30 09:46:57 -08:00
this . _handleModalClose ( prevProps . type ) ;
2020-03-27 13:59:38 -07:00
}
2020-07-04 16:41:41 -07:00
2020-03-27 13:59:38 -07:00
if ( this . props . children ) {
requestAnimationFrame ( ( ) => {
this . setState ( { revealed : true } ) ;
} ) ;
2022-01-06 08:45:10 -08:00
this . _ensureHistoryBuffer ( ) ;
2020-03-27 13:59:38 -07:00
}
}
2020-04-14 14:47:35 -07:00
componentWillUnmount ( ) {
2020-03-27 13:59:38 -07:00
window . removeEventListener ( 'keyup' , this . handleKeyUp ) ;
2021-08-28 05:30:58 -07:00
window . removeEventListener ( 'keydown' , this . handleKeyDown ) ;
2020-03-27 13:59:38 -07:00
}
2022-01-06 08:45:10 -08:00
_handleModalOpen ( ) {
this . _modalHistoryKey = Date . now ( ) ;
this . unlistenHistory = this . history . listen ( ( _ , action ) => {
if ( action === 'POP' ) {
this . handleOnClose ( ) ;
if ( this . props . onCancel ) this . props . onCancel ( ) ;
}
} ) ;
}
2022-01-30 09:46:57 -08:00
_handleModalClose ( type ) {
2022-01-06 08:45:10 -08:00
if ( this . unlistenHistory ) {
this . unlistenHistory ( ) ;
}
2022-02-21 23:52:33 -08:00
if ( ! [ 'FAVOURITES' , 'MENTIONS' , 'REACTIONS' , 'REBLOGS' , 'MEDIA' ] . includes ( type ) ) {
2022-01-30 09:46:57 -08:00
const { state } = this . history . location ;
if ( state && state . soapboxModalKey === this . _modalHistoryKey ) {
this . history . goBack ( ) ;
}
2022-01-06 08:45:10 -08:00
}
}
_ensureHistoryBuffer ( ) {
const { pathname , state } = this . history . location ;
if ( ! state || state . soapboxModalKey !== this . _modalHistoryKey ) {
this . history . push ( pathname , { ... state , soapboxModalKey : this . _modalHistoryKey } ) ;
}
}
2020-03-27 13:59:38 -07:00
getSiblings = ( ) => {
return Array ( ... this . node . parentElement . childNodes ) . filter ( node => node !== this . node ) ;
}
setRef = ref => {
this . node = ref ;
}
2020-04-14 14:47:35 -07:00
render ( ) {
2022-03-21 11:09:01 -07:00
const { children , type } = this . props ;
2020-03-27 13:59:38 -07:00
const { revealed } = this . state ;
const visible = ! ! children ;
if ( ! visible ) {
return (
2022-03-21 11:09:01 -07:00
< div className = 'z-50 transition-all' ref = { this . setRef } style = { { opacity : 0 } } / >
2020-03-27 13:59:38 -07:00
) ;
}
return (
2022-03-21 11:09:01 -07:00
< div
ref = { this . setRef }
className = { classNames ( {
2022-03-28 07:36:43 -07:00
'fixed top-0 left-0 z-[100] w-full h-full overflow-x-hidden overflow-y-auto' : true ,
2022-03-21 11:09:01 -07:00
'pointer-events-none' : ! visible ,
} ) }
style = { { opacity : revealed ? 1 : 0 } }
>
2022-05-11 06:25:48 -07:00
< div
role = 'presentation'
id = 'modal-overlay'
2022-07-22 10:30:16 -07:00
className = 'fixed inset-0 bg-gray-500/90 dark:bg-gray-700/90'
2022-05-11 06:25:48 -07:00
onClick = { this . handleOnClose }
/ >
2022-03-21 11:09:01 -07:00
< div
role = 'dialog'
className = { classNames ( {
'my-2 mx-auto relative pointer-events-none flex items-center' : true ,
'p-4 md:p-0' : type !== 'MEDIA' ,
} ) }
style = { { minHeight : 'calc(100% - 3.5rem)' } }
>
{ children }
2020-03-27 13:59:38 -07:00
< / d i v >
< / d i v >
) ;
}
2020-04-14 11:44:40 -07:00
2020-03-27 13:59:38 -07:00
}
export default injectIntl ( connect ( mapStateToProps , mapDispatchToProps ) ( ModalRoot ) ) ;