/////////////////////////////////////////////////////////////////////
// Module imports //
/////////////////////////////////////////////////////////////////////
import * as React from 'react';
import {
StyleSheet,
SafeAreaView,
ScrollView,
View,
TouchableOpacity,
TouchableHighlight,
Text,
Dimensions,
TextInput,
FlatList,
Button,
Keyboard
} from 'react-native';
import { Icon } from 'react-native-elements';
import { MaterialIndicator } from 'react-native-indicators';
/////////////////////////////////////////////////////////////////////
// Local imports //
/////////////////////////////////////////////////////////////////////
// Theme
import colors from '../../styles/colors';
import typography from '../../styles/typography';
/////////////////////////////////////////////////////////////////////
// Styling //
/////////////////////////////////////////////////////////////////////
const screenWidth = Math.round(Dimensions.get('window').width);
const screenHeight = Math.round(Dimensions.get('window').height);
const styles = StyleSheet.create({
container: {
position: 'absolute',
flexDirection: 'column',
justifyContent: 'center',
zIndex: 5
},
parent: {
marginTop: 12,
borderWidth: 1,
borderRadius: 5,
borderColor: colors.onsurface_disabled,
paddingLeft: 8,
paddingRight: 4,
flexDirection: "row",
justifyContent: "space-between",
width: screenWidth * 0.905,
},
focusedParent: {
marginTop: 12,
borderWidth: 1,
borderRadius: 5,
borderColor: colors.onsurface_disabled,
paddingLeft: 8,
flexDirection: "row",
justifyContent: "space-between",
backgroundColor: colors.onprimary_highEmphasis,
width: screenWidth * 0.905
},
searchBar: {
height: 38,
width: '89%',
backgroundColor: 'transparent',
alignSelf: "center",
},
focusedSearchBar: {
height: 38,
width: '89%',
backgroundColor: colors.onprimary_highEmphasis
},
clearBtn: {
justifyContent: "center",
alignSelf: "center",
marginRight: 4,
},
focusedClearBtn: {
justifyContent: "center",
alignSelf: "center",
marginRight: 8,
marginTop: 2
},
dropdown: {
position: "relative",
backgroundColor: colors.onprimary_highEmphasis,
width: screenWidth * 0.904,
height: screenHeight * 0.18,
alignItems: "flex-start",
justifyContent: "flex-start",
elevation: 8,
paddingTop: 4,
zIndex: 100,
},
ghostInput: {
top: screenHeight,
display: "none"
},
dropdownItem: {
paddingTop: 6,
paddingBottom: 6,
width: screenWidth * 0.91,
},
dropdownItemTitle: {
fontSize: 14,
paddingTop: 2,
paddingLeft: 13,
paddingRight: 13,
paddingBottom: 2
},
loadingIndicator: {
alignItems: "center",
justifyContent: "center",
paddingLeft: (screenWidth * 0.85) / 2
},
noResults: {
width: screenWidth * 0.904,
height: screenHeight * 0.14,
alignItems: "center",
justifyContent: "center"
}
});
/////////////////////////////////////////////////////////////////////
// API Search //
/////////////////////////////////////////////////////////////////////
const searchCharacters = async ( search ) => {
try {
const response = await fetch(
`https://clinicaltables.nlm.nih.gov/api/rxterms/v3/search?terms=${search.toLowerCase()}&ef=STRENGTHS_AND_FORMS`
);
const data = await response.json();
return data;
} catch (error) {
console.log(`error ocurred: ${error}`);
}
}
// Hook
const useDebounce = ( value, delay ) => {
const [debouncedValue, setDebouncedValue] = React.useState(value);
React.useEffect(
() => {
// Update debounced value after delay
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
// Cancel the timeout if value changes (also on delay change or unmount)
// This is how we prevent debounced value from updating if value is changed ...
// .. within the delay period. Timeout gets cleared and restarted.
return () => {
clearTimeout(handler);
};
},
[value, delay] // Only re-call effect if value or delay changes
);
return debouncedValue;
}
/////////////////////////////////////////////////////////////////////
// Auto Complete Search Bar //
/////////////////////////////////////////////////////////////////////
const AutoSearchBar = (props) => {
// Search term
const [searchTerm, setSearchTerm] = React.useState("");
// Text to display in search bar
const [text, onChangeText] = React.useState("");
// API search results
const [searchResults, setSearchResults] = React.useState(['']);
// Searching status (whether there is pending API request)
const [isSearching, setIsSearching] = React.useState(false);
// Keep track of search bar focus
const [hasFocus, onChangeFocus] = React.useState(false);
const [showingDropdown, setShowingDropdown] = React.useState(false);
// Dropdown item id
const [selectedId, setSelectedId] = React.useState(null);
const [dropdownData, setDropdownData] = React.useState(null);
// Debounce search term so that it only gives us latest value ...
// ... if searchTerm has not been updated within last 500ms.
// The goal is to only have the API call fire when user stops typing ...
// ... so that we aren't hitting our API rapidly.
const debouncedSearchTerm = useDebounce(searchTerm, 500);
const toggleDropdown = () => {
setShowingDropdown(!showingDropdown);
};
const hideDropdown = () => {
setShowingDropdown(false);
};
const renderDropdownItem = ({ item }) => (
{
onChangeText(item.name + ' (' + item.dose + ')');
props.onSelectionChange(item.name, item.form)
Keyboard.dismiss();
}}
>
{item.name + ' (' + item.dose + ')'}
);
const renderDropdown = () => {
return (
{isSearching && (
)}
{!isSearching && (
)}
ListEmptyComponent={(
No medications found
)}
keyExtractor={item => item.id}
extraData={searchResults}
keyboardShouldPersistTaps={'handled'}
nestedScrollEnabled
/>
)}
);
};
const onSearchBarFocus = () => {
onChangeFocus(true);
if(text !== '') {
toggleDropdown();
}
}
const onSearchBarBlur = () => {
onChangeFocus(false);
setShowingDropdown(false);
}
// What to do when text is being typed
const onSearchBarTextChange = (text) => {
if(text !== '' && !showingDropdown) {
toggleDropdown();
} else if(text === '' && showingDropdown){
toggleDropdown();
}
}
const clearSearchBarTxt = () => {
onChangeText('');
hideDropdown();
}
const prettifySearchResults = (results) => {
const suggestions = [];
// Only prettify if a non-empty result is returned from API
if (results.toString()[0] !== '0'){
// Get the list of medication names
const medicationNames = results[1].map(result => {
const name = result.match(/^[^ ]+/);
const prettyName = name.toString().toLowerCase();
return prettyName.charAt(0).toUpperCase() + prettyName.slice(1);
});
// Get list of the corresponding strengths and forms of above med list
const strengthsAndForms = results[2].STRENGTHS_AND_FORMS;
// Combine medication name with its corresponding strengths and forms
const medicationDoses = strengthsAndForms.map((medication, index) => {
// Get full name of medication form if needed
medication.map(dose => {
let formArray = dose.match(/(\b[A-Z][a-z]+|\b[A-Z]\b)/g);
let form = 'dose';
let suggestDose = 'none';
if(formArray.length > 1){
form = formArray.join(' ');
} else {
form = formArray.toString();
}
// Some strengths and forms are returned with extra spaces.
// Get rid of those
let prettyDose = dose.trim();
if (form === 'Cap') {
form = "Capsule";
prettyDose = prettyDose.replace("Cap", "Capsule");
}
if (form === 'Tab' || form === 'Microencapsulated Tab') {
form = "Tablet";
prettyDose = prettyDose.replace("Tab", "Tablet");
}
if (form === 'Sol' || form === 'Irrig Sol') {
form = "Solution";
prettyDose = prettyDose.replace("Sol", "Solution");
}
if (form === 'Susp') {
form = "Suspension";
prettyDose = prettyDose.replace("Susp", "Suspension");
}
if (form === 'Pwdr') {
form = "Powder";
prettyDose = prettyDose.replace("Pwdr", "Powder");
}
if (form === 'Sugar Free Pwdr') {
form = "Sugar-Free Powder";
prettyDose = prettyDose.replace("Sugar-Free Pwdr", "Sugar-Free Powder");
}
// Store pretty medication information
suggestions.push({
id: medicationNames[index] + '_' + dose,
name: medicationNames[index],
dose: prettyDose,
form: form,
suggestDose: suggestDose
});
});
return medication;
});
}
// Remove duplicates from results
const prettyResults = suggestions.filter((v, i, a) => a.findIndex(t => (t.id === v.id)) === i)
setSearchResults(prettyResults);
}
// Effect for API call
React.useEffect(
() => {
if (debouncedSearchTerm) {
setIsSearching(true);
searchCharacters(debouncedSearchTerm).then((results) => {
// results is a string. searchCharacters(debouncedSearchTerm) should be an array
setIsSearching(false);
if(results === undefined || results.length === 0) {
setSearchResults([]);
} else {
prettifySearchResults(results);
}
});
} else {
setSearchResults([]);
setIsSearching(false);
}
},
[debouncedSearchTerm] // Only call effect if debounced search term changes
);
return (
{
setSearchTerm(text);
onChangeText(text);
onSearchBarTextChange(text);
}}
value={text}
autoCorrect={false}
placeholder='Medication name'
onFocus={onSearchBarFocus}
onBlur={onSearchBarBlur}
/>
{showingDropdown ? renderDropdown() : null}
);
};
export default AutoSearchBar;