Changeset View
Changeset View
Standalone View
Standalone View
web/calendar/thread-picker.react.js
// @flow | // @flow | ||||
import invariant from 'invariant'; | import invariant from 'invariant'; | ||||
import * as React from 'react'; | import * as React from 'react'; | ||||
import { createSelector } from 'reselect'; | |||||
import { threadSearchIndex } from 'lib/selectors/nav-selectors'; | |||||
import { onScreenEntryEditableThreadInfos } from 'lib/selectors/thread-selectors'; | import { onScreenEntryEditableThreadInfos } from 'lib/selectors/thread-selectors'; | ||||
import SearchIndex from 'lib/shared/search-index'; | |||||
import type { ThreadInfo } from 'lib/types/thread-types'; | import type { ThreadInfo } from 'lib/types/thread-types'; | ||||
import { useSelector } from '../redux/redux-utils'; | import { useSelector } from '../redux/redux-utils'; | ||||
import { htmlTargetFromEvent } from '../vector-utils'; | import { htmlTargetFromEvent } from '../vector-utils'; | ||||
import css from './thread-picker.css'; | import css from './thread-picker.css'; | ||||
type OptionProps = { | type OptionProps = { | ||||
+threadInfo: ThreadInfo, | +threadInfo: ThreadInfo, | ||||
Show All 19 Lines | |||||
type BaseProps = { | type BaseProps = { | ||||
+createNewEntry: (threadID: string) => void, | +createNewEntry: (threadID: string) => void, | ||||
+closePicker: () => void, | +closePicker: () => void, | ||||
}; | }; | ||||
type Props = { | type Props = { | ||||
...BaseProps, | ...BaseProps, | ||||
+onScreenThreadInfos: $ReadOnlyArray<ThreadInfo>, | +onScreenThreadInfos: $ReadOnlyArray<ThreadInfo>, | ||||
+searchIndex: SearchIndex, | |||||
}; | }; | ||||
type State = { | |||||
+searchText: string, | |||||
+searchResults: Set<string>, | |||||
}; | |||||
type PropsAndState = { ...Props, ...State }; | |||||
class ThreadPicker extends React.PureComponent<Props> { | class ThreadPicker extends React.PureComponent<Props, State> { | ||||
pickerDiv: ?HTMLDivElement; | pickerDiv: ?HTMLDivElement; | ||||
constructor(props: Props) { | constructor(props: Props) { | ||||
super(props); | super(props); | ||||
this.state = { | |||||
searchText: '', | |||||
searchResults: new Set(), | |||||
}; | |||||
invariant( | invariant( | ||||
props.onScreenThreadInfos.length > 0, | props.onScreenThreadInfos.length > 0, | ||||
"ThreadPicker can't be open when onScreenThreadInfos is empty", | "ThreadPicker can't be open when onScreenThreadInfos is empty", | ||||
); | ); | ||||
} | } | ||||
componentDidMount() { | componentDidMount() { | ||||
invariant(this.pickerDiv, 'pickerDiv ref unset'); | invariant(this.pickerDiv, 'pickerDiv ref unset'); | ||||
this.pickerDiv.focus(); | this.pickerDiv.focus(); | ||||
} | } | ||||
listDataSelector = createSelector( | |||||
(propsAndState: PropsAndState) => propsAndState.onScreenThreadInfos, | |||||
(propsAndState: PropsAndState) => propsAndState.searchText, | |||||
(propsAndState: PropsAndState) => propsAndState.searchResults, | |||||
( | |||||
threadInfos: $ReadOnlyArray<ThreadInfo>, | |||||
text: string, | |||||
searchResults: Set<string>, | |||||
) => | |||||
text | |||||
? threadInfos.filter(threadInfo => searchResults.has(threadInfo.id)) | |||||
: [...threadInfos], | |||||
); | |||||
get getListData() { | |||||
return this.listDataSelector({ ...this.props, ...this.state }); | |||||
} | |||||
render() { | render() { | ||||
const length = this.props.onScreenThreadInfos.length; | const length = this.props.onScreenThreadInfos.length; | ||||
invariant( | invariant( | ||||
length > 0, | length > 0, | ||||
"ThreadPicker can't be open when onScreenThreadInfos is empty", | "ThreadPicker can't be open when onScreenThreadInfos is empty", | ||||
); | ); | ||||
const options = this.props.onScreenThreadInfos.map(threadInfo => ( | const options = this.getListData.map(threadInfo => ( | ||||
<ThreadPickerOption | <ThreadPickerOption | ||||
threadInfo={threadInfo} | threadInfo={threadInfo} | ||||
createNewEntry={this.props.createNewEntry} | createNewEntry={this.props.createNewEntry} | ||||
key={threadInfo.id} | key={threadInfo.id} | ||||
/> | /> | ||||
)); | )); | ||||
return ( | return ( | ||||
Show All 24 Lines | class ThreadPicker extends React.PureComponent<Props, State> { | ||||
onMouseDown = (event: SyntheticEvent<HTMLDivElement>) => { | onMouseDown = (event: SyntheticEvent<HTMLDivElement>) => { | ||||
const target = htmlTargetFromEvent(event); | const target = htmlTargetFromEvent(event); | ||||
invariant(this.pickerDiv, 'pickerDiv ref not set'); | invariant(this.pickerDiv, 'pickerDiv ref not set'); | ||||
if (this.pickerDiv.contains(target)) { | if (this.pickerDiv.contains(target)) { | ||||
// This prevents onBlur from firing | // This prevents onBlur from firing | ||||
event.preventDefault(); | event.preventDefault(); | ||||
} | } | ||||
}; | }; | ||||
onChangeSearchText = (searchText: string) => { | |||||
const results = this.props.searchIndex.getSearchResults(searchText); | |||||
this.setState({ searchText, searchResults: new Set(results) }); | |||||
}; | |||||
} | } | ||||
const ConnectedThreadPicker: React.ComponentType<BaseProps> = React.memo<BaseProps>( | const ConnectedThreadPicker: React.ComponentType<BaseProps> = React.memo<BaseProps>( | ||||
function ConnectedThreadPicker(props) { | function ConnectedThreadPicker(props) { | ||||
const onScreenThreadInfos = useSelector(onScreenEntryEditableThreadInfos); | const onScreenThreadInfos = useSelector(onScreenEntryEditableThreadInfos); | ||||
const index = useSelector(state => threadSearchIndex(state)); | |||||
return ( | return ( | ||||
<ThreadPicker {...props} onScreenThreadInfos={onScreenThreadInfos} /> | <ThreadPicker | ||||
{...props} | |||||
onScreenThreadInfos={onScreenThreadInfos} | |||||
searchIndex={index} | |||||
/> | |||||
); | ); | ||||
}, | }, | ||||
); | ); | ||||
export default ConnectedThreadPicker; | export default ConnectedThreadPicker; |