2022-08-25 02:27:47 +00:00
import React from 'react' ;
import PropTypes from 'prop-types' ;
import { connect } from 'react-redux' ;
import { defineMessages , injectIntl , FormattedMessage } from 'react-intl' ;
import { toServerSideType } from 'mastodon/utils/filters' ;
import { loupeIcon , deleteIcon } from 'mastodon/utils/icons' ;
import Icon from 'mastodon/components/icon' ;
import fuzzysort from 'fuzzysort' ;
const messages = defineMessages ( {
search : { id : 'filter_modal.select_filter.search' , defaultMessage : 'Search or create' } ,
clear : { id : 'emoji_button.clear' , defaultMessage : 'Clear' } ,
} ) ;
const mapStateToProps = ( state , { contextType } ) => ( {
filters : Array . from ( state . get ( 'filters' ) . values ( ) ) . map ( ( filter ) => [
filter . get ( 'id' ) ,
filter . get ( 'title' ) ,
filter . get ( 'keywords' ) ? . map ( ( keyword ) => keyword . get ( 'keyword' ) ) . join ( '\n' ) ,
filter . get ( 'expires_at' ) && filter . get ( 'expires_at' ) < new Date ( ) ,
contextType && ! filter . get ( 'context' ) . includes ( toServerSideType ( contextType ) ) ,
] ) ,
} ) ;
class SelectFilter extends React . PureComponent {
static propTypes = {
onSelectFilter : PropTypes . func . isRequired ,
onNewFilter : PropTypes . func . isRequired ,
filters : PropTypes . arrayOf ( PropTypes . arrayOf ( PropTypes . object ) ) ,
intl : PropTypes . object . isRequired ,
} ;
state = {
searchValue : '' ,
} ;
search ( ) {
const { filters } = this . props ;
const { searchValue } = this . state ;
if ( searchValue === '' ) {
return filters ;
}
return fuzzysort . go ( searchValue , filters , {
keys : [ '1' , '2' ] ,
limit : 5 ,
threshold : - 10000 ,
} ) . map ( result => result . obj ) ;
}
renderItem = filter => {
let warning = null ;
if ( filter [ 3 ] || filter [ 4 ] ) {
warning = (
< span className = 'language-dropdown__dropdown__results__item__common-name' >
(
{ filter [ 3 ] && < FormattedMessage id = 'filter_modal.select_filter.expired' defaultMessage = 'expired' / > }
{ filter [ 3 ] && filter [ 4 ] && ', ' }
{ filter [ 4 ] && < FormattedMessage id = 'filter_modal.select_filter.context_mismatch' defaultMessage = 'does not apply to this context' / > }
)
< / span >
) ;
}
return (
2023-04-04 14:33:44 +00:00
< div key = { filter [ 0 ] } role = 'button' tabIndex = { 0 } data - index = { filter [ 0 ] } className = 'language-dropdown__dropdown__results__item' onClick = { this . handleItemClick } onKeyDown = { this . handleKeyDown } >
2022-08-25 02:27:47 +00:00
< span className = 'language-dropdown__dropdown__results__item__native-name' > { filter [ 1 ] } < / span > { warning }
< / div >
) ;
2023-01-30 00:45:35 +00:00
} ;
2022-08-25 02:27:47 +00:00
renderCreateNew ( name ) {
return (
2023-04-04 14:33:44 +00:00
< div key = 'add-new-filter' role = 'button' tabIndex = { 0 } className = 'language-dropdown__dropdown__results__item' onClick = { this . handleNewFilterClick } onKeyDown = { this . handleKeyDown } >
2022-08-25 02:27:47 +00:00
< Icon id = 'plus' fixedWidth / > < FormattedMessage id = 'filter_modal.select_filter.prompt_new' defaultMessage = 'New category: {name}' values = { { name } } / >
< / div >
) ;
}
handleSearchChange = ( { target } ) => {
this . setState ( { searchValue : target . value } ) ;
2023-01-30 00:45:35 +00:00
} ;
2022-08-25 02:27:47 +00:00
setListRef = c => {
this . listNode = c ;
2023-01-30 00:45:35 +00:00
} ;
2022-08-25 02:27:47 +00:00
handleKeyDown = e => {
const index = Array . from ( this . listNode . childNodes ) . findIndex ( node => node === e . currentTarget ) ;
let element = null ;
switch ( e . key ) {
case ' ' :
case 'Enter' :
e . currentTarget . click ( ) ;
break ;
case 'ArrowDown' :
element = this . listNode . childNodes [ index + 1 ] || this . listNode . firstChild ;
break ;
case 'ArrowUp' :
element = this . listNode . childNodes [ index - 1 ] || this . listNode . lastChild ;
break ;
case 'Tab' :
if ( e . shiftKey ) {
element = this . listNode . childNodes [ index - 1 ] || this . listNode . lastChild ;
} else {
element = this . listNode . childNodes [ index + 1 ] || this . listNode . firstChild ;
}
break ;
case 'Home' :
element = this . listNode . firstChild ;
break ;
case 'End' :
element = this . listNode . lastChild ;
break ;
}
if ( element ) {
element . focus ( ) ;
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
}
2023-01-30 00:45:35 +00:00
} ;
2022-08-25 02:27:47 +00:00
handleSearchKeyDown = e => {
let element = null ;
switch ( e . key ) {
case 'Tab' :
case 'ArrowDown' :
element = this . listNode . firstChild ;
if ( element ) {
element . focus ( ) ;
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
}
break ;
}
2023-01-30 00:45:35 +00:00
} ;
2022-08-25 02:27:47 +00:00
handleClear = ( ) => {
this . setState ( { searchValue : '' } ) ;
2023-01-30 00:45:35 +00:00
} ;
2022-08-25 02:27:47 +00:00
handleItemClick = e => {
const value = e . currentTarget . getAttribute ( 'data-index' ) ;
e . preventDefault ( ) ;
this . props . onSelectFilter ( value ) ;
2023-01-30 00:45:35 +00:00
} ;
2022-08-25 02:27:47 +00:00
handleNewFilterClick = e => {
e . preventDefault ( ) ;
this . props . onNewFilter ( this . state . searchValue ) ;
} ;
render ( ) {
const { intl } = this . props ;
const { searchValue } = this . state ;
const isSearching = searchValue !== '' ;
const results = this . search ( ) ;
return (
< React.Fragment >
< h3 className = 'report-dialog-modal__title' > < FormattedMessage id = 'filter_modal.select_filter.title' defaultMessage = 'Filter this post' / > < / h3 >
< p className = 'report-dialog-modal__lead' > < FormattedMessage id = 'filter_modal.select_filter.subtitle' defaultMessage = 'Use an existing category or create a new one' / > < / p >
< div className = 'emoji-mart-search' >
< input type = 'search' value = { searchValue } onChange = { this . handleSearchChange } onKeyDown = { this . handleSearchKeyDown } placeholder = { intl . formatMessage ( messages . search ) } autoFocus / >
2022-11-05 12:43:37 +00:00
< button type = 'button' className = 'emoji-mart-search-icon' disabled = { ! isSearching } aria - label = { intl . formatMessage ( messages . clear ) } onClick = { this . handleClear } > { ! isSearching ? loupeIcon : deleteIcon } < / button >
2022-08-25 02:27:47 +00:00
< / div >
< div className = 'language-dropdown__dropdown__results emoji-mart-scroll' role = 'listbox' ref = { this . setListRef } >
{ results . map ( this . renderItem ) }
{ isSearching && this . renderCreateNew ( searchValue ) }
< / div >
< / React.Fragment >
) ;
}
}
2023-03-24 02:17:53 +00:00
export default connect ( mapStateToProps ) ( injectIntl ( SelectFilter ) ) ;