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)), ]), }); export default @connect(mapStateToProps) @injectIntl 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 = ( ( {filter[3] && } {filter[3] && filter[4] && ', '} {filter[4] && } ) ); } return (
{filter[1]} {warning}
); } renderCreateNew (name) { return (
); } handleSearchChange = ({ target }) => { this.setState({ searchValue: target.value }); } setListRef = c => { this.listNode = c; } 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(); } } 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; } } handleClear = () => { this.setState({ searchValue: '' }); } handleItemClick = e => { const value = e.currentTarget.getAttribute('data-index'); e.preventDefault(); this.props.onSelectFilter(value); } 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 (

{results.map(this.renderItem)} {isSearching && this.renderCreateNew(searchValue) }
); } }