UNPKG

expo-osm-sdk

Version:

OpenStreetMap component for React Native with Expo

269 lines 11.7 kB
"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