expo-osm-sdk
Version:
OpenStreetMap component for React Native with Expo
269 lines • 11.7 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SearchBox = void 0;
const jsx_runtime_1 = require("react/jsx-runtime");
const react_1 = require("react");
const react_native_1 = require("react-native");
const useNominatimSearch_1 = require("../hooks/useNominatimSearch");
/**
* SearchBox Component - Simple, clean search with autocomplete
*
* @example
* <SearchBox
* onLocationSelected={(location) => {
* mapRef.current?.animateToLocation(
* location.coordinate.latitude,
* location.coordinate.longitude,
* 15
* );
* }}
* maxResults={5}
* autoComplete={true}
* />
*/
const SearchBox = ({ onLocationSelected, onResultsChanged, onClear, placeholder = "Search for places...", placeholderTextColor = '#999999', value, editable = true, style, containerStyle, maxResults = 5, showCurrentLocation = true, autoComplete = true, debounceMs = 300, }) => {
const [query, setQuery] = (0, react_1.useState)(value || '');
const [showResults, setShowResults] = (0, react_1.useState)(false);
const { search, isLoading, error, lastResults, clearResults } = (0, useNominatimSearch_1.useNominatimSearch)();
const debounceTimeout = (0, react_1.useRef)(null);
const inputRef = (0, react_1.useRef)(null);
const onResultsChangedRef = (0, react_1.useRef)(onResultsChanged);
const isSelectingRef = (0, react_1.useRef)(false); // Track if we're handling a selection
// Keep callback ref updated
(0, react_1.useEffect)(() => {
onResultsChangedRef.current = onResultsChanged;
}, [onResultsChanged]);
// Update query when value prop changes
(0, react_1.useEffect)(() => {
if (value !== undefined) {
setQuery(value);
}
}, [value]);
// Debounced search effect
(0, react_1.useEffect)(() => {
// Skip search if we're handling a selection to prevent duplicate events
if (isSelectingRef.current) {
isSelectingRef.current = false;
return;
}
if (!autoComplete || !query.trim() || !editable) {
clearResults();
setShowResults(false);
onResultsChangedRef.current?.([]);
return;
}
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
debounceTimeout.current = setTimeout(async () => {
try {
const results = await search(query, { limit: maxResults });
console.log('🔍 SearchBox: Search completed', { query, resultsCount: results.length, showResults: results.length > 0 });
setShowResults(results.length > 0);
onResultsChangedRef.current?.(results);
}
catch (err) {
console.error('Search error:', err);
setShowResults(false);
onResultsChangedRef.current?.([]);
}
}, debounceMs);
return () => {
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
}
};
}, [query, autoComplete, maxResults, debounceMs, search, clearResults, editable]);
const handleLocationSelect = (location) => {
// Set flag to prevent useEffect from triggering a new search
isSelectingRef.current = true;
// Clear any pending search
if (debounceTimeout.current) {
clearTimeout(debounceTimeout.current);
debounceTimeout.current = null;
}
// Set query first to show selected location
setQuery(location.displayName);
// Clear results and close dropdown
clearResults();
setShowResults(false);
onResultsChangedRef.current?.([]);
// Trigger selection callback
onLocationSelected?.(location);
// Blur input after a short delay to ensure selection callback fires
setTimeout(() => {
inputRef.current?.blur();
// Reset flag after blur
setTimeout(() => {
isSelectingRef.current = false;
}, 100);
}, 100);
};
const handleSearchPress = async () => {
if (!query.trim())
return;
try {
const results = await search(query, { limit: maxResults });
setShowResults(results.length > 0);
onResultsChanged?.(results);
}
catch (err) {
console.error('Search error:', err);
}
};
const handleClear = () => {
setQuery('');
setShowResults(false);
clearResults();
onResultsChanged?.([]);
// If value prop is provided, notify parent to clear it
if (value !== undefined) {
onClear?.();
}
inputRef.current?.focus();
};
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: [styles.container, containerStyle], children: [(0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.searchContainer, children: [(0, jsx_runtime_1.jsx)(react_native_1.TextInput, { ref: inputRef, style: [styles.textInput, style], placeholder: placeholder, placeholderTextColor: placeholderTextColor, value: value !== undefined ? value : query, onChangeText: editable ? setQuery : undefined, editable: editable, onFocus: () => {
if (lastResults.length > 0 && autoComplete) {
setShowResults(true);
}
}, onBlur: () => {
// Delay closing to allow time for result selection
// Check if we're selecting to prevent premature closing
setTimeout(() => {
if (!isSelectingRef.current) {
setShowResults(false);
}
}, 200);
}, returnKeyType: "search", onSubmitEditing: handleSearchPress }), (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: styles.actionContainer, children: [isLoading && (0, jsx_runtime_1.jsx)(react_native_1.ActivityIndicator, { style: styles.loader }), (() => {
// Check the displayed value (value prop takes precedence over query)
const displayedValue = value !== undefined ? value : query;
return displayedValue.length > 0 && !isLoading && ((0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { style: styles.clearButton, onPress: handleClear, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.clearButtonText, children: "\u2715" }) }));
})(), !autoComplete && ((0, jsx_runtime_1.jsx)(react_native_1.TouchableOpacity, { style: styles.searchButton, onPress: handleSearchPress, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.searchButtonText, children: "\uD83D\uDD0D" }) }))] })] }), error && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.errorContainer, children: (0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.errorText, children: ["\u26A0\uFE0F ", error] }) })), showResults && lastResults.length > 0 && ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.resultsContainer, children: lastResults.slice(0, maxResults).map((result, index) => {
const displayNameParts = result.displayName.split(',');
const title = displayNameParts[0];
const subtitle = displayNameParts.slice(1).join(',').trim();
return ((0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { onPress: () => {
// Prevent blur from closing dropdown during selection
isSelectingRef.current = true;
handleLocationSelect(result);
}, onPressIn: () => {
// Prevent input blur during press
isSelectingRef.current = true;
}, style: ({ pressed }) => [
styles.resultItem,
{ backgroundColor: pressed ? '#F8F8F8' : '#FFFFFF' },
index === lastResults.slice(0, maxResults).length - 1 && styles.lastResultItem,
], children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.resultIcon, children: getCategoryIcon(result.category || '') }), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: styles.resultTextContainer, children: (0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.resultText, numberOfLines: 2, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: styles.resultTextBold, children: title }), subtitle ? (0, jsx_runtime_1.jsxs)(react_native_1.Text, { style: styles.resultTextRegular, children: [" ", subtitle] }) : null] }) })] }, result.placeId || index));
}) }))] }));
};
exports.SearchBox = SearchBox;
const getCategoryIcon = (category) => {
const iconMap = {
'amenity': '🏪', 'shop': '🛍️', 'tourism': '🏛️', 'leisure': '🎯',
'natural': '🌲', 'place': '📍', 'highway': '🛣️', 'building': '🏢',
'landuse': '🏞️', 'waterway': '🌊',
};
return iconMap[category] || '📍';
};
const styles = react_native_1.StyleSheet.create({
container: {
position: 'relative',
zIndex: 1000,
},
searchContainer: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#FFFFFF',
borderRadius: 48,
borderWidth: 0,
paddingHorizontal: 0,
paddingVertical: 0,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
elevation: 2,
},
textInput: {
flex: 1,
fontSize: 15,
color: '#333333',
paddingVertical: react_native_1.Platform.OS === 'ios' ? 12 : 10,
paddingHorizontal: 16,
paddingRight: 8,
},
actionContainer: {
flexDirection: 'row',
alignItems: 'center',
marginLeft: 8,
},
loader: {
marginRight: 4,
},
clearButton: {
padding: 4,
},
clearButtonText: {
fontSize: 18,
color: '#999999',
},
searchButton: {
padding: 4,
marginLeft: 4,
},
searchButtonText: {
fontSize: 18,
},
errorContainer: {
backgroundColor: '#FFF0F0',
borderRadius: 6,
padding: 10,
marginTop: 6,
},
errorText: {
color: '#D32F2F',
fontSize: 13,
},
resultsContainer: {
backgroundColor: '#FFFFFF',
borderRadius: 8,
marginTop: 6,
borderWidth: 1,
borderColor: '#DDDDDD',
maxHeight: 280,
overflow: 'hidden',
zIndex: 10000, // Ensure results are above other elements
elevation: 10, // Android elevation
},
resultItem: {
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 12,
paddingVertical: 10,
borderBottomWidth: 1,
borderBottomColor: '#F0F0F0',
},
lastResultItem: {
borderBottomWidth: 0,
},
resultIcon: {
fontSize: 18,
marginRight: 10,
},
resultTextContainer: {
flex: 1,
},
resultText: {
fontSize: 14,
color: '#333333',
},
resultTextBold: {
fontWeight: '600',
color: '#000000',
},
resultTextRegular: {
fontWeight: '400',
color: '#666666',
},
});
//# sourceMappingURL=SearchBox.js.map