107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
import clsx from 'clsx';
|
|
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
|
import throttle from 'lodash/throttle';
|
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { defineMessages, useIntl } from 'react-intl';
|
|
|
|
import { locationSearch } from 'pl-fe/actions/events';
|
|
import AutosuggestInput, { AutoSuggestion } from 'pl-fe/components/autosuggest-input';
|
|
import Icon from 'pl-fe/components/icon';
|
|
import { useAppDispatch } from 'pl-fe/hooks';
|
|
|
|
import AutosuggestLocation from './autosuggest-location';
|
|
|
|
const noOp = () => {};
|
|
|
|
const messages = defineMessages({
|
|
placeholder: { id: 'location_search.placeholder', defaultMessage: 'Find an address' },
|
|
});
|
|
|
|
interface ILocationSearch {
|
|
onSelected: (locationId: string) => void;
|
|
}
|
|
|
|
const LocationSearch: React.FC<ILocationSearch> = ({ onSelected }) => {
|
|
const intl = useIntl();
|
|
const dispatch = useAppDispatch();
|
|
const [locationIds, setLocationIds] = useState(ImmutableOrderedSet<string>());
|
|
const controller = useRef(new AbortController());
|
|
|
|
const [value, setValue] = useState('');
|
|
|
|
const empty = !(value.length > 0);
|
|
|
|
const handleChange: React.ChangeEventHandler<HTMLInputElement> = ({ target }) => {
|
|
refreshCancelToken();
|
|
handleLocationSearch(target.value);
|
|
setValue(target.value);
|
|
};
|
|
|
|
const handleSelected = (_tokenStart: number, _lastToken: string | null, suggestion: AutoSuggestion) => {
|
|
if (typeof suggestion === 'string') {
|
|
onSelected(suggestion);
|
|
}
|
|
};
|
|
|
|
const handleClear: React.MouseEventHandler = e => {
|
|
e.preventDefault();
|
|
|
|
if (!empty) {
|
|
setValue('');
|
|
}
|
|
};
|
|
|
|
const handleKeyDown: React.KeyboardEventHandler = e => {
|
|
if (e.key === 'Escape') {
|
|
document.querySelector('.ui')?.parentElement?.focus();
|
|
}
|
|
};
|
|
|
|
const refreshCancelToken = () => {
|
|
controller.current.abort();
|
|
controller.current = new AbortController();
|
|
};
|
|
|
|
const clearResults = () => {
|
|
setLocationIds(ImmutableOrderedSet());
|
|
};
|
|
|
|
const handleLocationSearch = useCallback(throttle(q => {
|
|
dispatch(locationSearch(q, controller.current.signal))
|
|
.then((locations: { origin_id: string }[]) => {
|
|
const locationIds = locations.map(location => location.origin_id);
|
|
setLocationIds(ImmutableOrderedSet(locationIds));
|
|
})
|
|
.catch(noOp);
|
|
}, 900, { leading: true, trailing: true }), []);
|
|
|
|
useEffect(() => {
|
|
if (value === '') {
|
|
clearResults();
|
|
}
|
|
}, [value]);
|
|
|
|
return (
|
|
<div className='relative'>
|
|
<AutosuggestInput
|
|
className='rounded-full'
|
|
placeholder={intl.formatMessage(messages.placeholder)}
|
|
value={value}
|
|
onChange={handleChange}
|
|
suggestions={locationIds.toList()}
|
|
onSuggestionsFetchRequested={noOp}
|
|
onSuggestionsClearRequested={noOp}
|
|
onSuggestionSelected={handleSelected}
|
|
searchTokens={[]}
|
|
onKeyDown={handleKeyDown}
|
|
renderSuggestion={AutosuggestLocation}
|
|
/>
|
|
<div role='button' tabIndex={0} className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-3 rtl:left-0 rtl:right-auto' onClick={handleClear}>
|
|
<Icon src={require('@tabler/icons/outline/search.svg')} className={clsx('h-5 w-5 text-gray-600', { 'hidden': !empty })} />
|
|
<Icon src={require('@tabler/icons/outline/backspace.svg')} className={clsx('h-5 w-5 text-gray-600', { 'hidden': empty })} aria-label={intl.formatMessage(messages.placeholder)} />
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export { LocationSearch as default };
|