react-native-google-places-autocomplete 2.6.3 → 2.6.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/GooglePlacesAutocomplete.d.ts +19 -8
- package/GooglePlacesAutocomplete.js +828 -972
- package/package.json +38 -15
- package/src/Row.js +107 -0
- package/src/debounce.js +31 -0
- package/src/queryString.js +49 -0
- package/src/results.js +105 -0
- package/src/styles.js +92 -0
- package/src/uuid.js +60 -0
- package/example/App.js +0 -221
- package/example/README.md +0 -64
- package/example/app.json +0 -24
- package/example/babel.config.js +0 -6
- package/example/package.json +0 -23
|
@@ -1,79 +1,136 @@
|
|
|
1
|
-
/* eslint-disable react-native/no-inline-styles */
|
|
2
|
-
import debounce from 'lodash.debounce';
|
|
3
|
-
import Qs from 'qs';
|
|
4
|
-
import uuid from 'react-native-uuid';
|
|
5
1
|
import React, {
|
|
6
2
|
forwardRef,
|
|
7
|
-
|
|
3
|
+
useCallback,
|
|
8
4
|
useEffect,
|
|
9
5
|
useImperativeHandle,
|
|
6
|
+
useMemo,
|
|
10
7
|
useRef,
|
|
11
8
|
useState,
|
|
12
|
-
useCallback,
|
|
13
9
|
} from 'react';
|
|
14
10
|
import {
|
|
15
|
-
ActivityIndicator,
|
|
16
11
|
FlatList,
|
|
17
12
|
Image,
|
|
18
13
|
Keyboard,
|
|
19
14
|
Platform,
|
|
20
|
-
Pressable,
|
|
21
|
-
ScrollView,
|
|
22
|
-
StyleSheet,
|
|
23
|
-
Text,
|
|
24
15
|
TextInput,
|
|
25
16
|
View,
|
|
26
17
|
} from 'react-native';
|
|
27
18
|
|
|
19
|
+
import Row from './src/Row';
|
|
20
|
+
import { debounce as createDebounce } from './src/debounce';
|
|
21
|
+
import { stringify } from './src/queryString';
|
|
22
|
+
import {
|
|
23
|
+
buildRowsFromResults,
|
|
24
|
+
filterResultsByPlacePredictions,
|
|
25
|
+
filterResultsByTypes,
|
|
26
|
+
getRowKey,
|
|
27
|
+
} from './src/results';
|
|
28
|
+
import { mergeStyles } from './src/styles';
|
|
29
|
+
import { uuidv4 } from './src/uuid';
|
|
30
|
+
|
|
28
31
|
// ============================================================================
|
|
29
32
|
// CONSTANTS
|
|
33
|
+
//
|
|
34
|
+
// Every default that is an object or an array lives out here. Declaring them
|
|
35
|
+
// inline in the destructuring pattern created a fresh identity on every render,
|
|
36
|
+
// which silently defeated every useMemo/useCallback downstream of them.
|
|
30
37
|
// ============================================================================
|
|
31
38
|
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
39
|
+
const GOOGLE_API_BASE = 'https://maps.googleapis.com/maps/api';
|
|
40
|
+
|
|
41
|
+
const EMPTY_ARRAY = Object.freeze([]);
|
|
42
|
+
const EMPTY_OBJECT = Object.freeze({});
|
|
43
|
+
|
|
44
|
+
const DEFAULT_QUERY = Object.freeze({
|
|
45
|
+
key: 'missing api key',
|
|
46
|
+
language: 'en',
|
|
47
|
+
types: 'geocode',
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const DEFAULT_SEARCH_QUERY = Object.freeze({
|
|
51
|
+
rankby: 'distance',
|
|
52
|
+
type: 'restaurant',
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
const noop = () => {};
|
|
56
|
+
|
|
57
|
+
const defaultOnTimeout = () =>
|
|
58
|
+
console.warn('google places autocomplete: request timeout');
|
|
59
|
+
|
|
60
|
+
const getRequestUrl = (requestUrl) => {
|
|
61
|
+
if (requestUrl) {
|
|
62
|
+
if (requestUrl.useOnPlatform === 'all') {
|
|
63
|
+
return requestUrl.url;
|
|
64
|
+
}
|
|
65
|
+
if (requestUrl.useOnPlatform === 'web') {
|
|
66
|
+
return Platform.select({ web: requestUrl.url, default: GOOGLE_API_BASE });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return GOOGLE_API_BASE;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Detaching the handler before aborting keeps `abort()` from reporting itself
|
|
74
|
+
* as a transport failure. Nothing can be observed about a request after this,
|
|
75
|
+
* so only ever call it on a request the component has decided to abandon.
|
|
76
|
+
*/
|
|
77
|
+
const abortRequest = (request) => {
|
|
78
|
+
request.onreadystatechange = null;
|
|
79
|
+
request.abort();
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const setRequestHeaders = (request, headers) => {
|
|
83
|
+
if (!headers) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
Object.keys(headers).forEach((key) =>
|
|
87
|
+
request.setRequestHeader(key, headers[key]),
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Resolve the geolocation provider *without* detaching the method from its
|
|
93
|
+
* receiver — class-based implementations rely on `this`.
|
|
94
|
+
*/
|
|
95
|
+
const getGeolocationProvider = () => {
|
|
96
|
+
const geolocation =
|
|
97
|
+
typeof navigator !== 'undefined' ? navigator.geolocation : undefined;
|
|
98
|
+
|
|
99
|
+
if (geolocation?.getCurrentPosition) {
|
|
100
|
+
return geolocation;
|
|
101
|
+
}
|
|
102
|
+
if (geolocation?.default?.getCurrentPosition) {
|
|
103
|
+
return geolocation.default;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
/** Only the results that came from the API earn the "powered by Google" logo. */
|
|
109
|
+
const hasApiResults = (rows) => {
|
|
110
|
+
for (let i = 0; i < rows.length; i++) {
|
|
111
|
+
if (
|
|
112
|
+
!('isCurrentLocation' in rows[i]) &&
|
|
113
|
+
!('isPredefinedPlace' in rows[i])
|
|
114
|
+
) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const isFocusInsideResultList = ({ relatedTarget }) => {
|
|
122
|
+
if (!relatedTarget) {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let node = relatedTarget.parentNode;
|
|
127
|
+
while (node) {
|
|
128
|
+
if (node.id === 'result-list-id') {
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
node = node.parentNode;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
77
134
|
};
|
|
78
135
|
|
|
79
136
|
// ============================================================================
|
|
@@ -81,11 +138,9 @@ const defaultStyles = {
|
|
|
81
138
|
// ============================================================================
|
|
82
139
|
|
|
83
140
|
export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
|
|
84
|
-
// ==========================================================================
|
|
85
|
-
// PROPS DESTRUCTURING
|
|
86
|
-
// ==========================================================================
|
|
87
141
|
const {
|
|
88
142
|
autoFillOnNotFound = false,
|
|
143
|
+
children,
|
|
89
144
|
currentLocation = false,
|
|
90
145
|
currentLocationLabel = 'Current location',
|
|
91
146
|
debounce: debounceMs = 0,
|
|
@@ -93,655 +148,570 @@ export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
|
|
|
93
148
|
enableHighAccuracyLocation = true,
|
|
94
149
|
enablePoweredByContainer = true,
|
|
95
150
|
fetchDetails = false,
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
151
|
+
fields = '*',
|
|
152
|
+
filterReverseGeocodingByTypes = EMPTY_ARRAY,
|
|
153
|
+
GooglePlacesDetailsQuery = EMPTY_OBJECT,
|
|
154
|
+
GooglePlacesSearchQuery = DEFAULT_SEARCH_QUERY,
|
|
155
|
+
GoogleReverseGeocodingQuery = EMPTY_OBJECT,
|
|
156
|
+
inbetweenCompo,
|
|
157
|
+
isNewPlacesAPI = false,
|
|
103
158
|
isRowScrollable = true,
|
|
159
|
+
keepResultsAfterBlur = false,
|
|
104
160
|
keyboardShouldPersistTaps = 'always',
|
|
161
|
+
listEmptyComponent,
|
|
105
162
|
listHoverColor = '#ececec',
|
|
163
|
+
listLoaderComponent,
|
|
106
164
|
listUnderlayColor = '#c8c7cc',
|
|
107
165
|
listViewDisplayed: listViewDisplayedProp = 'auto',
|
|
108
|
-
keepResultsAfterBlur = false,
|
|
109
166
|
minLength = 0,
|
|
110
167
|
nearbyPlacesAPI = 'GooglePlacesSearch',
|
|
111
168
|
numberOfLines = 1,
|
|
112
|
-
onFail
|
|
113
|
-
onNotFound
|
|
114
|
-
onPress =
|
|
115
|
-
onTimeout =
|
|
116
|
-
console.warn('google places autocomplete: request timeout'),
|
|
169
|
+
onFail,
|
|
170
|
+
onNotFound,
|
|
171
|
+
onPress: onPressProp = noop,
|
|
172
|
+
onTimeout = defaultOnTimeout,
|
|
117
173
|
placeholder = '',
|
|
118
|
-
predefinedPlaces
|
|
174
|
+
predefinedPlaces = EMPTY_ARRAY,
|
|
119
175
|
predefinedPlacesAlwaysVisible = false,
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
176
|
+
preProcess,
|
|
177
|
+
query = DEFAULT_QUERY,
|
|
178
|
+
renderDescription,
|
|
179
|
+
renderHeaderComponent,
|
|
180
|
+
renderLeftButton,
|
|
181
|
+
renderRightButton,
|
|
182
|
+
renderRow,
|
|
183
|
+
requestUrl,
|
|
184
|
+
styles = EMPTY_OBJECT,
|
|
126
185
|
suppressDefaultStyles = false,
|
|
127
186
|
textInputHide = false,
|
|
128
|
-
textInputProps =
|
|
187
|
+
textInputProps = EMPTY_OBJECT,
|
|
129
188
|
timeout = 20000,
|
|
130
|
-
isNewPlacesAPI = false,
|
|
131
|
-
fields = '*',
|
|
132
189
|
...restProps
|
|
133
190
|
} = props;
|
|
134
191
|
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
138
|
-
const predefinedPlaces = useMemo(() => predefinedPlacesProp || [], [
|
|
139
|
-
predefinedPlacesProp,
|
|
140
|
-
]);
|
|
192
|
+
// --------------------------------------------------------------------------
|
|
193
|
+
// Derived values
|
|
194
|
+
// --------------------------------------------------------------------------
|
|
141
195
|
|
|
142
|
-
|
|
143
|
-
const
|
|
196
|
+
const url = useMemo(() => getRequestUrl(requestUrl), [requestUrl]);
|
|
197
|
+
const requestHeaders = requestUrl?.headers;
|
|
198
|
+
const withCredentials = url === GOOGLE_API_BASE;
|
|
144
199
|
|
|
145
|
-
|
|
146
|
-
const requestsRef = useRef([]);
|
|
200
|
+
const isSupportedPlatform = !(Platform.OS === 'web' && !requestUrl);
|
|
147
201
|
|
|
148
|
-
|
|
149
|
-
|
|
202
|
+
const mergedStyles = useMemo(
|
|
203
|
+
() => mergeStyles(styles, suppressDefaultStyles),
|
|
204
|
+
[styles, suppressDefaultStyles],
|
|
205
|
+
);
|
|
150
206
|
|
|
151
|
-
|
|
152
|
-
|
|
207
|
+
const isAutoMode =
|
|
208
|
+
listViewDisplayedProp === 'auto' || listViewDisplayedProp === undefined;
|
|
153
209
|
|
|
154
|
-
|
|
155
|
-
const queryRef = useRef(query);
|
|
210
|
+
const hasGeolocation = currentLocation === true && !!getGeolocationProvider();
|
|
156
211
|
|
|
157
|
-
|
|
158
|
-
|
|
212
|
+
const rowOptions = useMemo(
|
|
213
|
+
() => ({
|
|
214
|
+
predefinedPlaces,
|
|
215
|
+
predefinedPlacesAlwaysVisible,
|
|
216
|
+
currentLocation: hasGeolocation,
|
|
217
|
+
currentLocationLabel,
|
|
218
|
+
}),
|
|
219
|
+
[
|
|
220
|
+
predefinedPlaces,
|
|
221
|
+
predefinedPlacesAlwaysVisible,
|
|
222
|
+
hasGeolocation,
|
|
223
|
+
currentLocationLabel,
|
|
224
|
+
],
|
|
225
|
+
);
|
|
159
226
|
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
227
|
+
// --------------------------------------------------------------------------
|
|
228
|
+
// State & refs
|
|
229
|
+
// --------------------------------------------------------------------------
|
|
163
230
|
|
|
164
231
|
const [stateText, setStateText] = useState('');
|
|
165
|
-
const [dataSource, setDataSource] = useState(
|
|
166
|
-
const [listViewDisplayed, setListViewDisplayed] = useState(
|
|
167
|
-
listViewDisplayedProp === 'auto' ? false : listViewDisplayedProp,
|
|
168
|
-
);
|
|
232
|
+
const [dataSource, setDataSource] = useState(EMPTY_ARRAY);
|
|
169
233
|
const [listWasDismissed, setListWasDismissed] = useState(false);
|
|
170
|
-
const [url, setUrl] = useState('');
|
|
171
234
|
const [listLoaderDisplayed, setListLoaderDisplayed] = useState(false);
|
|
172
|
-
const [
|
|
235
|
+
const [loadingRowKey, setLoadingRowKey] = useState(null);
|
|
236
|
+
const [sessionToken, setSessionToken] = useState(uuidv4);
|
|
173
237
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
238
|
+
const inputRef = useRef(null);
|
|
239
|
+
const resultsRef = useRef(EMPTY_ARRAY);
|
|
240
|
+
const requestsRef = useRef([]);
|
|
241
|
+
const detailsRequestRef = useRef(null);
|
|
242
|
+
const stateTextRef = useRef(stateText);
|
|
243
|
+
const loadingRowKeyRef = useRef(loadingRowKey);
|
|
244
|
+
const prevQueryStringRef = useRef(null);
|
|
245
|
+
// True from the moment a row is chosen until the user edits the text again.
|
|
246
|
+
// Focus alone must not re-open the list while it is set: the field already
|
|
247
|
+
// holds what the user picked, and the predictions behind it are spent.
|
|
248
|
+
const selectionMadeRef = useRef(false);
|
|
249
|
+
// The description of the row that was chosen. The platform writes it into
|
|
250
|
+
// the input and echoes it back through onChangeText, which is indisting-
|
|
251
|
+
// uishable from typing unless we remember what we are expecting.
|
|
252
|
+
const selectedTextRef = useRef(null);
|
|
253
|
+
|
|
254
|
+
stateTextRef.current = stateText;
|
|
255
|
+
loadingRowKeyRef.current = loadingRowKey;
|
|
177
256
|
|
|
178
|
-
const
|
|
179
|
-
if (navigator?.geolocation) {
|
|
180
|
-
return true;
|
|
181
|
-
}
|
|
182
|
-
if (!hasWarnedAboutNavigator.current) {
|
|
183
|
-
if (Platform.OS === 'web') {
|
|
184
|
-
console.warn(
|
|
185
|
-
'Geolocation is not available. For web, ensure your site is served over HTTPS or localhost to use geolocation features.',
|
|
186
|
-
);
|
|
187
|
-
} else {
|
|
188
|
-
console.warn(
|
|
189
|
-
'Geolocation is not available. For React Native, you may need to install and configure @react-native-community/geolocation or expo-location to enable currentLocation.',
|
|
190
|
-
);
|
|
191
|
-
}
|
|
192
|
-
hasWarnedAboutNavigator.current = true;
|
|
193
|
-
}
|
|
194
|
-
return false;
|
|
195
|
-
}, []);
|
|
257
|
+
const queryString = useMemo(() => JSON.stringify(query), [query]);
|
|
196
258
|
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
259
|
+
// --------------------------------------------------------------------------
|
|
260
|
+
// Helpers
|
|
261
|
+
//
|
|
262
|
+
// Declaration order below is load-bearing: every one of these is read by the
|
|
263
|
+
// ones that follow it. Reading a `const` above its declaration is a temporal
|
|
264
|
+
// dead zone error under any bundler that does not downlevel `const` to `var`
|
|
265
|
+
// (Expo web, Vite, webpack with modern targets).
|
|
266
|
+
// --------------------------------------------------------------------------
|
|
267
|
+
|
|
268
|
+
// Searches and the Place Details lookup are tracked separately because their
|
|
269
|
+
// lifetimes are independent: a details request is started by a row press and
|
|
270
|
+
// outlives the search that produced the row. Sharing one list meant the next
|
|
271
|
+
// search aborted the details request that had just been sent, and since the
|
|
272
|
+
// abort detaches the handler the selection was lost without a single
|
|
273
|
+
// callback firing (#998).
|
|
274
|
+
const abortSearchRequests = useCallback(() => {
|
|
275
|
+
requestsRef.current.forEach(abortRequest);
|
|
276
|
+
requestsRef.current = [];
|
|
277
|
+
}, []);
|
|
206
278
|
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
if (requestUrl.useOnPlatform === 'web') {
|
|
213
|
-
return Platform.select({
|
|
214
|
-
web: requestUrl.url,
|
|
215
|
-
default: 'https://maps.googleapis.com/maps/api',
|
|
216
|
-
});
|
|
217
|
-
}
|
|
279
|
+
const abortDetailsRequest = useCallback(() => {
|
|
280
|
+
if (detailsRequestRef.current) {
|
|
281
|
+
abortRequest(detailsRequestRef.current);
|
|
282
|
+
detailsRequestRef.current = null;
|
|
218
283
|
}
|
|
219
|
-
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
const getRequestHeaders = (requestUrl) => {
|
|
223
|
-
return requestUrl?.headers || {};
|
|
224
|
-
};
|
|
284
|
+
}, []);
|
|
225
285
|
|
|
226
|
-
const
|
|
227
|
-
|
|
228
|
-
request.
|
|
229
|
-
|
|
230
|
-
|
|
286
|
+
const prepareRequest = useCallback(
|
|
287
|
+
(request) => {
|
|
288
|
+
request.timeout = timeout;
|
|
289
|
+
request.ontimeout = onTimeout;
|
|
290
|
+
return request;
|
|
291
|
+
},
|
|
292
|
+
[timeout, onTimeout],
|
|
293
|
+
);
|
|
231
294
|
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
295
|
+
const trackSearchRequest = useCallback(
|
|
296
|
+
(request) => {
|
|
297
|
+
requestsRef.current.push(prepareRequest(request));
|
|
298
|
+
return request;
|
|
299
|
+
},
|
|
300
|
+
[prepareRequest],
|
|
301
|
+
);
|
|
235
302
|
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
request
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
303
|
+
const trackDetailsRequest = useCallback(
|
|
304
|
+
(request) => {
|
|
305
|
+
detailsRequestRef.current = prepareRequest(request);
|
|
306
|
+
return request;
|
|
307
|
+
},
|
|
308
|
+
[prepareRequest],
|
|
309
|
+
);
|
|
243
310
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
311
|
+
const buildRows = useCallback(
|
|
312
|
+
(results, text) => buildRowsFromResults(results, text, rowOptions),
|
|
313
|
+
[rowOptions],
|
|
314
|
+
);
|
|
247
315
|
|
|
248
|
-
const
|
|
249
|
-
(results, text) => {
|
|
250
|
-
let res = [];
|
|
251
|
-
// Show predefined places if:
|
|
252
|
-
// 1. No text entered and no results, OR
|
|
253
|
-
// 2. predefinedPlacesAlwaysVisible is true
|
|
254
|
-
const shouldDisplayPredefinedPlaces =
|
|
255
|
-
(!text || text.length === 0) && results.length === 0;
|
|
256
|
-
if (
|
|
257
|
-
shouldDisplayPredefinedPlaces ||
|
|
258
|
-
predefinedPlacesAlwaysVisible === true
|
|
259
|
-
) {
|
|
260
|
-
if (predefinedPlaces.length > 0) {
|
|
261
|
-
res = [
|
|
262
|
-
...predefinedPlaces.filter((place) => place?.description?.length),
|
|
263
|
-
];
|
|
264
|
-
}
|
|
316
|
+
const disableRowLoaders = useCallback(() => setLoadingRowKey(null), []);
|
|
265
317
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
318
|
+
const reportFailure = useCallback(
|
|
319
|
+
(message) => {
|
|
320
|
+
if (onFail) {
|
|
321
|
+
onFail(message);
|
|
322
|
+
} else {
|
|
323
|
+
console.warn('google places autocomplete: ' + message);
|
|
272
324
|
}
|
|
273
|
-
|
|
274
|
-
res = res.map((place) => ({
|
|
275
|
-
...place,
|
|
276
|
-
isPredefinedPlace: true,
|
|
277
|
-
}));
|
|
278
|
-
|
|
279
|
-
return [...res, ...results];
|
|
280
325
|
},
|
|
281
|
-
[
|
|
282
|
-
predefinedPlacesAlwaysVisible,
|
|
283
|
-
predefinedPlaces,
|
|
284
|
-
currentLocation,
|
|
285
|
-
currentLocationLabel,
|
|
286
|
-
hasNavigator,
|
|
287
|
-
],
|
|
326
|
+
[onFail],
|
|
288
327
|
);
|
|
289
328
|
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
for (let i = 0; i < unfilteredResults.length; i++) {
|
|
295
|
-
let found = false;
|
|
296
|
-
|
|
297
|
-
for (let j = 0; j < types.length; j++) {
|
|
298
|
-
if (unfilteredResults[i].types?.indexOf(types[j]) !== -1) {
|
|
299
|
-
found = true;
|
|
300
|
-
break;
|
|
301
|
-
}
|
|
329
|
+
const renderRowDescription = useCallback(
|
|
330
|
+
(rowData) => {
|
|
331
|
+
if (renderDescription) {
|
|
332
|
+
return renderDescription(rowData);
|
|
302
333
|
}
|
|
334
|
+
return rowData.description || rowData.formatted_address || rowData.name;
|
|
335
|
+
},
|
|
336
|
+
[renderDescription],
|
|
337
|
+
);
|
|
303
338
|
|
|
304
|
-
|
|
305
|
-
|
|
339
|
+
const getPredefinedPlace = useCallback(
|
|
340
|
+
(rowData) => {
|
|
341
|
+
if (rowData.isPredefinedPlace !== true) {
|
|
342
|
+
return rowData;
|
|
306
343
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
344
|
+
const match = predefinedPlaces.find(
|
|
345
|
+
(place) => place?.description === rowData.description,
|
|
346
|
+
);
|
|
347
|
+
return match || rowData;
|
|
348
|
+
},
|
|
349
|
+
[predefinedPlaces],
|
|
350
|
+
);
|
|
310
351
|
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
unfilteredResults[i].placePrediction.structuredFormat?.mainText
|
|
322
|
-
?.text,
|
|
323
|
-
secondary_text:
|
|
324
|
-
unfilteredResults[i].placePrediction.structuredFormat
|
|
325
|
-
?.secondaryText?.text,
|
|
326
|
-
},
|
|
327
|
-
types: unfilteredResults[i].placePrediction.types ?? [],
|
|
328
|
-
});
|
|
352
|
+
const showResults = useCallback(
|
|
353
|
+
(results, text) => {
|
|
354
|
+
resultsRef.current = results;
|
|
355
|
+
setDataSource(buildRows(results, text));
|
|
356
|
+
|
|
357
|
+
// A response that lands after the user has already chosen a row is
|
|
358
|
+
// stale by definition: showing it would re-open the list over the
|
|
359
|
+
// selection.
|
|
360
|
+
if (!selectionMadeRef.current) {
|
|
361
|
+
setListWasDismissed(false);
|
|
329
362
|
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
|
|
363
|
+
},
|
|
364
|
+
[buildRows],
|
|
365
|
+
);
|
|
333
366
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
367
|
+
// --------------------------------------------------------------------------
|
|
368
|
+
// Requests
|
|
369
|
+
// --------------------------------------------------------------------------
|
|
338
370
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
371
|
+
const requestNearby = useCallback(
|
|
372
|
+
(latitude, longitude) => {
|
|
373
|
+
abortSearchRequests();
|
|
374
|
+
|
|
375
|
+
if (latitude == null || longitude == null) {
|
|
376
|
+
resultsRef.current = EMPTY_ARRAY;
|
|
377
|
+
setDataSource(buildRows(EMPTY_ARRAY, ''));
|
|
378
|
+
return;
|
|
344
379
|
}
|
|
345
|
-
}
|
|
346
380
|
|
|
347
|
-
|
|
348
|
-
};
|
|
381
|
+
const request = trackSearchRequest(new XMLHttpRequest());
|
|
349
382
|
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
383
|
+
request.onreadystatechange = () => {
|
|
384
|
+
if (request.readyState !== 4) {
|
|
385
|
+
setListLoaderDisplayed(true);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
353
388
|
|
|
354
|
-
|
|
355
|
-
(latitude, longitude) => {
|
|
356
|
-
_abortRequests();
|
|
357
|
-
|
|
358
|
-
if (
|
|
359
|
-
latitude !== undefined &&
|
|
360
|
-
longitude !== undefined &&
|
|
361
|
-
latitude !== null &&
|
|
362
|
-
longitude !== null
|
|
363
|
-
) {
|
|
364
|
-
const request = new XMLHttpRequest();
|
|
365
|
-
requestsRef.current.push(request);
|
|
366
|
-
request.timeout = timeout;
|
|
367
|
-
request.ontimeout = onTimeout;
|
|
368
|
-
request.onreadystatechange = () => {
|
|
369
|
-
if (request.readyState !== 4) {
|
|
370
|
-
setListLoaderDisplayed(true);
|
|
371
|
-
return;
|
|
372
|
-
}
|
|
389
|
+
setListLoaderDisplayed(false);
|
|
373
390
|
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
391
|
+
if (request.status !== 200) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
377
394
|
|
|
378
|
-
|
|
395
|
+
const responseJSON = JSON.parse(request.responseText);
|
|
396
|
+
disableRowLoaders();
|
|
379
397
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
398
|
+
if (typeof responseJSON.results !== 'undefined') {
|
|
399
|
+
const results =
|
|
400
|
+
nearbyPlacesAPI === 'GoogleReverseGeocoding'
|
|
401
|
+
? filterResultsByTypes(
|
|
384
402
|
responseJSON.results,
|
|
385
403
|
filterReverseGeocodingByTypes,
|
|
386
|
-
)
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
if (typeof responseJSON.error_message !== 'undefined') {
|
|
404
|
-
if (!onFail) {
|
|
405
|
-
console.warn(
|
|
406
|
-
'google places autocomplete: ' + responseJSON.error_message,
|
|
407
|
-
);
|
|
408
|
-
} else {
|
|
409
|
-
onFail(responseJSON.error_message);
|
|
410
|
-
}
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
let requestUrl = '';
|
|
416
|
-
if (nearbyPlacesAPI === 'GoogleReverseGeocoding') {
|
|
417
|
-
// your key must be allowed to use Google Maps Geocoding API
|
|
418
|
-
requestUrl =
|
|
419
|
-
`${url}/geocode/json?` +
|
|
420
|
-
Qs.stringify({
|
|
421
|
-
latlng: latitude + ',' + longitude,
|
|
404
|
+
)
|
|
405
|
+
: responseJSON.results;
|
|
406
|
+
|
|
407
|
+
showResults(results, '');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (typeof responseJSON.error_message !== 'undefined') {
|
|
411
|
+
reportFailure(responseJSON.error_message);
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
const requestUrlPath =
|
|
416
|
+
nearbyPlacesAPI === 'GoogleReverseGeocoding'
|
|
417
|
+
? `${url}/geocode/json?` +
|
|
418
|
+
stringify({
|
|
419
|
+
latlng: `${latitude},${longitude}`,
|
|
422
420
|
key: query.key,
|
|
423
421
|
...GoogleReverseGeocodingQuery,
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
Qs.stringify({
|
|
429
|
-
location: latitude + ',' + longitude,
|
|
422
|
+
})
|
|
423
|
+
: `${url}/place/nearbysearch/json?` +
|
|
424
|
+
stringify({
|
|
425
|
+
location: `${latitude},${longitude}`,
|
|
430
426
|
key: query.key,
|
|
431
427
|
...GooglePlacesSearchQuery,
|
|
432
428
|
});
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
request.open('GET', requestUrl);
|
|
436
429
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
} else {
|
|
442
|
-
resultsRef.current = [];
|
|
443
|
-
setDataSource(buildRowsFromResults([]));
|
|
444
|
-
}
|
|
430
|
+
request.open('GET', requestUrlPath);
|
|
431
|
+
request.withCredentials = withCredentials;
|
|
432
|
+
setRequestHeaders(request, requestHeaders);
|
|
433
|
+
request.send();
|
|
445
434
|
},
|
|
446
435
|
[
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
436
|
+
abortSearchRequests,
|
|
437
|
+
trackSearchRequest,
|
|
438
|
+
buildRows,
|
|
439
|
+
disableRowLoaders,
|
|
440
|
+
showResults,
|
|
441
|
+
reportFailure,
|
|
451
442
|
nearbyPlacesAPI,
|
|
452
|
-
_filterResultsByTypes,
|
|
453
443
|
filterReverseGeocodingByTypes,
|
|
454
|
-
buildRowsFromResults,
|
|
455
|
-
listViewDisplayedProp,
|
|
456
|
-
onFail,
|
|
457
444
|
url,
|
|
458
|
-
query,
|
|
445
|
+
query.key,
|
|
459
446
|
GoogleReverseGeocodingQuery,
|
|
460
447
|
GooglePlacesSearchQuery,
|
|
461
|
-
|
|
462
|
-
|
|
448
|
+
withCredentials,
|
|
449
|
+
requestHeaders,
|
|
463
450
|
],
|
|
464
451
|
);
|
|
465
452
|
|
|
466
|
-
const
|
|
467
|
-
|
|
453
|
+
const request = useCallback(
|
|
454
|
+
(text) => {
|
|
455
|
+
abortSearchRequests();
|
|
468
456
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
457
|
+
if (!isSupportedPlatform) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
472
460
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
461
|
+
if (!text || text.length < minLength) {
|
|
462
|
+
resultsRef.current = EMPTY_ARRAY;
|
|
463
|
+
setDataSource(buildRows(EMPTY_ARRAY, ''));
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
476
466
|
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
if (
|
|
467
|
+
const httpRequest = trackSearchRequest(new XMLHttpRequest());
|
|
468
|
+
|
|
469
|
+
httpRequest.onreadystatechange = () => {
|
|
470
|
+
if (httpRequest.readyState !== 4) {
|
|
481
471
|
setListLoaderDisplayed(true);
|
|
482
472
|
return;
|
|
483
473
|
}
|
|
484
474
|
|
|
485
475
|
setListLoaderDisplayed(false);
|
|
486
476
|
|
|
487
|
-
if (
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
responseJSON.suggestions,
|
|
511
|
-
);
|
|
512
|
-
|
|
513
|
-
resultsRef.current = results;
|
|
514
|
-
const newDataSource = buildRowsFromResults(results, text);
|
|
515
|
-
setDataSource(newDataSource);
|
|
516
|
-
// Auto-show list when results arrive if in 'auto' mode
|
|
517
|
-
if (listViewDisplayedProp === 'auto' && newDataSource.length > 0) {
|
|
518
|
-
setListWasDismissed(false);
|
|
519
|
-
setListViewDisplayed(true);
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
if (typeof responseJSON.error_message !== 'undefined') {
|
|
523
|
-
if (!onFail) {
|
|
524
|
-
console.warn(
|
|
525
|
-
'google places autocomplete: ' + responseJSON.error_message,
|
|
526
|
-
);
|
|
527
|
-
} else {
|
|
528
|
-
onFail(responseJSON.error_message);
|
|
529
|
-
}
|
|
530
|
-
}
|
|
531
|
-
} else {
|
|
532
|
-
console.warn(
|
|
533
|
-
'google places autocomplete: request could not be completed or has been aborted',
|
|
477
|
+
if (httpRequest.status !== 200) {
|
|
478
|
+
reportFailure('request could not be completed or has been aborted');
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const responseJSON = JSON.parse(httpRequest.responseText);
|
|
483
|
+
|
|
484
|
+
if (typeof responseJSON.predictions !== 'undefined') {
|
|
485
|
+
const results =
|
|
486
|
+
nearbyPlacesAPI === 'GoogleReverseGeocoding'
|
|
487
|
+
? filterResultsByTypes(
|
|
488
|
+
responseJSON.predictions,
|
|
489
|
+
filterReverseGeocodingByTypes,
|
|
490
|
+
)
|
|
491
|
+
: responseJSON.predictions;
|
|
492
|
+
|
|
493
|
+
showResults(results, text);
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (typeof responseJSON.suggestions !== 'undefined') {
|
|
497
|
+
showResults(
|
|
498
|
+
filterResultsByPlacePredictions(responseJSON.suggestions),
|
|
499
|
+
text,
|
|
534
500
|
);
|
|
535
501
|
}
|
|
502
|
+
|
|
503
|
+
if (typeof responseJSON.error_message !== 'undefined') {
|
|
504
|
+
reportFailure(responseJSON.error_message);
|
|
505
|
+
}
|
|
536
506
|
};
|
|
537
507
|
|
|
538
|
-
if (
|
|
539
|
-
setStateText(
|
|
508
|
+
if (preProcess) {
|
|
509
|
+
setStateText(preProcess(text));
|
|
540
510
|
}
|
|
541
511
|
|
|
542
512
|
if (isNewPlacesAPI) {
|
|
543
513
|
const keyQueryParam = query.key
|
|
544
|
-
? '?' +
|
|
545
|
-
Qs.stringify({
|
|
546
|
-
key: query.key,
|
|
547
|
-
})
|
|
514
|
+
? '?' + stringify({ key: query.key })
|
|
548
515
|
: '';
|
|
549
|
-
|
|
516
|
+
httpRequest.open(
|
|
517
|
+
'POST',
|
|
518
|
+
`${url}/v1/places:autocomplete${keyQueryParam}`,
|
|
519
|
+
);
|
|
550
520
|
} else {
|
|
551
|
-
|
|
521
|
+
httpRequest.open(
|
|
552
522
|
'GET',
|
|
553
523
|
`${url}/place/autocomplete/json?input=` +
|
|
554
524
|
encodeURIComponent(text) +
|
|
555
525
|
'&' +
|
|
556
|
-
|
|
526
|
+
stringify(query),
|
|
557
527
|
);
|
|
558
528
|
}
|
|
559
529
|
|
|
560
|
-
|
|
561
|
-
setRequestHeaders(
|
|
530
|
+
httpRequest.withCredentials = withCredentials;
|
|
531
|
+
setRequestHeaders(httpRequest, requestHeaders);
|
|
562
532
|
|
|
563
533
|
if (isNewPlacesAPI) {
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
534
|
+
// v1 renamed/removed several legacy parameters. `language` in
|
|
535
|
+
// particular is rejected outright — it is `languageCode` now.
|
|
536
|
+
const { key, locationbias, types, language, ...rest } = query;
|
|
537
|
+
const body = { input: text, sessionToken, ...rest };
|
|
538
|
+
|
|
539
|
+
if (language) {
|
|
540
|
+
body.languageCode = language;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
httpRequest.send(JSON.stringify(body));
|
|
572
544
|
} else {
|
|
573
|
-
|
|
545
|
+
httpRequest.send();
|
|
574
546
|
}
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
547
|
+
},
|
|
548
|
+
[
|
|
549
|
+
abortSearchRequests,
|
|
550
|
+
trackSearchRequest,
|
|
551
|
+
buildRows,
|
|
552
|
+
showResults,
|
|
553
|
+
reportFailure,
|
|
554
|
+
isSupportedPlatform,
|
|
555
|
+
minLength,
|
|
556
|
+
nearbyPlacesAPI,
|
|
557
|
+
filterReverseGeocodingByTypes,
|
|
558
|
+
preProcess,
|
|
559
|
+
isNewPlacesAPI,
|
|
560
|
+
url,
|
|
561
|
+
query,
|
|
562
|
+
sessionToken,
|
|
563
|
+
withCredentials,
|
|
564
|
+
requestHeaders,
|
|
565
|
+
],
|
|
566
|
+
);
|
|
567
|
+
|
|
568
|
+
// The debounced caller reads through a ref so it always invokes the current
|
|
569
|
+
// closure without having to be rebuilt (and reset) on every render.
|
|
570
|
+
const requestRef = useRef(null);
|
|
571
|
+
useEffect(() => {
|
|
572
|
+
requestRef.current = request;
|
|
573
|
+
}, [request]);
|
|
574
|
+
|
|
575
|
+
const debouncedRequest = useMemo(
|
|
576
|
+
() => createDebounce((text) => requestRef.current?.(text), debounceMs),
|
|
577
|
+
[debounceMs],
|
|
578
|
+
);
|
|
580
579
|
|
|
581
580
|
const getCurrentLocation = useCallback(() => {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
};
|
|
587
|
-
|
|
588
|
-
if (enableHighAccuracyLocation && Platform.OS === 'android') {
|
|
589
|
-
options = {
|
|
590
|
-
enableHighAccuracy: true,
|
|
591
|
-
timeout: 20000,
|
|
592
|
-
};
|
|
581
|
+
const provider = getGeolocationProvider();
|
|
582
|
+
|
|
583
|
+
if (!provider) {
|
|
584
|
+
return;
|
|
593
585
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
586
|
+
|
|
587
|
+
const options =
|
|
588
|
+
enableHighAccuracyLocation && Platform.OS === 'android'
|
|
589
|
+
? { enableHighAccuracy: true, timeout: 20000 }
|
|
590
|
+
: { enableHighAccuracy: false, timeout: 20000, maximumAge: 1000 };
|
|
591
|
+
|
|
592
|
+
provider.getCurrentPosition(
|
|
593
|
+
(position) => {
|
|
594
|
+
if (nearbyPlacesAPI === 'None') {
|
|
595
|
+
const currentLocationData = {
|
|
596
|
+
description: currentLocationLabel,
|
|
597
|
+
geometry: {
|
|
598
|
+
location: {
|
|
599
|
+
lat: position.coords.latitude,
|
|
600
|
+
lng: position.coords.longitude,
|
|
609
601
|
},
|
|
610
|
-
}
|
|
602
|
+
},
|
|
603
|
+
};
|
|
611
604
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
605
|
+
disableRowLoaders();
|
|
606
|
+
onPressProp(currentLocationData, currentLocationData);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
requestNearby(position.coords.latitude, position.coords.longitude);
|
|
611
|
+
},
|
|
612
|
+
(error) => {
|
|
613
|
+
disableRowLoaders();
|
|
614
|
+
console.error(error.message);
|
|
615
|
+
},
|
|
616
|
+
options,
|
|
617
|
+
);
|
|
625
618
|
}, [
|
|
626
619
|
enableHighAccuracyLocation,
|
|
627
620
|
currentLocationLabel,
|
|
628
621
|
nearbyPlacesAPI,
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
622
|
+
disableRowLoaders,
|
|
623
|
+
onPressProp,
|
|
624
|
+
requestNearby,
|
|
632
625
|
]);
|
|
633
626
|
|
|
634
|
-
//
|
|
635
|
-
//
|
|
636
|
-
//
|
|
637
|
-
|
|
638
|
-
const _enableRowLoader = (rowData) => {
|
|
639
|
-
const rows = buildRowsFromResults(resultsRef.current);
|
|
640
|
-
for (let i = 0; i < rows.length; i++) {
|
|
641
|
-
if (
|
|
642
|
-
rows[i].place_id === rowData.place_id ||
|
|
643
|
-
(rows[i].isCurrentLocation === true &&
|
|
644
|
-
rowData.isCurrentLocation === true)
|
|
645
|
-
) {
|
|
646
|
-
rows[i].isLoading = true;
|
|
647
|
-
setDataSource(rows);
|
|
648
|
-
break;
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
};
|
|
627
|
+
// --------------------------------------------------------------------------
|
|
628
|
+
// Handlers
|
|
629
|
+
// --------------------------------------------------------------------------
|
|
652
630
|
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
if (
|
|
656
|
-
|
|
631
|
+
const hideListView = useCallback(
|
|
632
|
+
(force = false) => {
|
|
633
|
+
if (!keepResultsAfterBlur || force) {
|
|
634
|
+
setListWasDismissed(true);
|
|
657
635
|
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
}, [buildRowsFromResults]);
|
|
636
|
+
},
|
|
637
|
+
[keepResultsAfterBlur],
|
|
638
|
+
);
|
|
662
639
|
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
if (
|
|
666
|
-
// already requesting
|
|
640
|
+
const handleBlur = useCallback(
|
|
641
|
+
(event) => {
|
|
642
|
+
if (event && isFocusInsideResultList(event)) {
|
|
667
643
|
return;
|
|
668
644
|
}
|
|
645
|
+
hideListView();
|
|
646
|
+
inputRef.current?.blur();
|
|
647
|
+
},
|
|
648
|
+
[hideListView],
|
|
649
|
+
);
|
|
669
650
|
|
|
651
|
+
const fetchPlaceDetails = useCallback(
|
|
652
|
+
(rowData) => {
|
|
670
653
|
hideListView(true);
|
|
671
654
|
Keyboard.dismiss();
|
|
655
|
+
abortSearchRequests();
|
|
656
|
+
abortDetailsRequest();
|
|
657
|
+
setLoadingRowKey(getRowKey(rowData));
|
|
672
658
|
|
|
673
|
-
|
|
659
|
+
const detailsRequest = trackDetailsRequest(new XMLHttpRequest());
|
|
674
660
|
|
|
675
|
-
|
|
676
|
-
|
|
661
|
+
detailsRequest.onreadystatechange = () => {
|
|
662
|
+
if (detailsRequest.readyState !== 4) {
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
677
665
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
if (
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
'google places autocomplete: ' + responseJSON.status,
|
|
711
|
-
);
|
|
712
|
-
} else {
|
|
713
|
-
onNotFound(responseJSON);
|
|
714
|
-
}
|
|
715
|
-
}
|
|
666
|
+
// Whatever the outcome, this request is no longer in flight: releasing
|
|
667
|
+
// it here keeps a later abort from reaching a settled request.
|
|
668
|
+
if (detailsRequestRef.current === detailsRequest) {
|
|
669
|
+
detailsRequestRef.current = null;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
if (detailsRequest.status !== 200) {
|
|
673
|
+
disableRowLoaders();
|
|
674
|
+
reportFailure('request could not be completed or has been aborted');
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
const responseJSON = JSON.parse(detailsRequest.responseText);
|
|
679
|
+
const succeeded =
|
|
680
|
+
responseJSON.status === 'OK' || (isNewPlacesAPI && responseJSON.id);
|
|
681
|
+
|
|
682
|
+
disableRowLoaders();
|
|
683
|
+
|
|
684
|
+
if (succeeded) {
|
|
685
|
+
const details = isNewPlacesAPI ? responseJSON : responseJSON.result;
|
|
686
|
+
handleBlur();
|
|
687
|
+
setStateText(renderRowDescription(rowData));
|
|
688
|
+
onPressProp(rowData, details);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
if (autoFillOnNotFound) {
|
|
693
|
+
setStateText(renderRowDescription(rowData));
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
if (onNotFound) {
|
|
697
|
+
onNotFound(responseJSON);
|
|
716
698
|
} else {
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
if (!onFail) {
|
|
720
|
-
console.warn(
|
|
721
|
-
'google places autocomplete: request could not be completed or has been aborted',
|
|
722
|
-
);
|
|
723
|
-
} else {
|
|
724
|
-
onFail('request could not be completed or has been aborted');
|
|
725
|
-
}
|
|
699
|
+
console.warn('google places autocomplete: ' + responseJSON.status);
|
|
726
700
|
}
|
|
727
701
|
};
|
|
728
702
|
|
|
729
703
|
if (isNewPlacesAPI) {
|
|
730
|
-
|
|
704
|
+
detailsRequest.open(
|
|
731
705
|
'GET',
|
|
732
706
|
`${url}/v1/places/${rowData.place_id}?` +
|
|
733
|
-
|
|
734
|
-
key: query.key,
|
|
735
|
-
sessionToken,
|
|
736
|
-
fields,
|
|
737
|
-
}),
|
|
707
|
+
stringify({ key: query.key, sessionToken, fields }),
|
|
738
708
|
);
|
|
739
|
-
setSessionToken(
|
|
709
|
+
setSessionToken(uuidv4());
|
|
740
710
|
} else {
|
|
741
|
-
|
|
711
|
+
detailsRequest.open(
|
|
742
712
|
'GET',
|
|
743
713
|
`${url}/place/details/json?` +
|
|
744
|
-
|
|
714
|
+
stringify({
|
|
745
715
|
key: query.key,
|
|
746
716
|
placeid: rowData.place_id,
|
|
747
717
|
language: query.language,
|
|
@@ -750,465 +720,351 @@ export const GooglePlacesAutocomplete = forwardRef((props, ref) => {
|
|
|
750
720
|
);
|
|
751
721
|
}
|
|
752
722
|
|
|
753
|
-
|
|
754
|
-
setRequestHeaders(
|
|
723
|
+
detailsRequest.withCredentials = withCredentials;
|
|
724
|
+
setRequestHeaders(detailsRequest, requestHeaders);
|
|
725
|
+
detailsRequest.send();
|
|
726
|
+
},
|
|
727
|
+
[
|
|
728
|
+
hideListView,
|
|
729
|
+
abortSearchRequests,
|
|
730
|
+
abortDetailsRequest,
|
|
731
|
+
trackDetailsRequest,
|
|
732
|
+
disableRowLoaders,
|
|
733
|
+
reportFailure,
|
|
734
|
+
handleBlur,
|
|
735
|
+
renderRowDescription,
|
|
736
|
+
onPressProp,
|
|
737
|
+
onNotFound,
|
|
738
|
+
autoFillOnNotFound,
|
|
739
|
+
isNewPlacesAPI,
|
|
740
|
+
url,
|
|
741
|
+
query.key,
|
|
742
|
+
query.language,
|
|
743
|
+
sessionToken,
|
|
744
|
+
fields,
|
|
745
|
+
GooglePlacesDetailsQuery,
|
|
746
|
+
withCredentials,
|
|
747
|
+
requestHeaders,
|
|
748
|
+
],
|
|
749
|
+
);
|
|
755
750
|
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
//
|
|
760
|
-
|
|
751
|
+
const handleRowPress = useCallback(
|
|
752
|
+
(rowData) => {
|
|
753
|
+
// Selecting a row ends the search. The keystroke that armed this timer is
|
|
754
|
+
// already answered by the row the user just tapped, so firing it would
|
|
755
|
+
// only bill another autocomplete call and repaint a dismissed list. A
|
|
756
|
+
// search already on the wire would do the same through showResults(),
|
|
757
|
+
// so it goes too.
|
|
758
|
+
debouncedRequest.cancel();
|
|
759
|
+
abortSearchRequests();
|
|
760
|
+
selectionMadeRef.current = true;
|
|
761
|
+
selectedTextRef.current = renderRowDescription(rowData);
|
|
762
|
+
|
|
763
|
+
if (rowData.isCurrentLocation === true) {
|
|
764
|
+
// Current location is not a terminal choice when it feeds a nearby
|
|
765
|
+
// search: those results are meant to open the list again.
|
|
766
|
+
selectionMadeRef.current = false;
|
|
767
|
+
hideListView(true);
|
|
768
|
+
setLoadingRowKey(getRowKey(rowData));
|
|
769
|
+
setStateText(renderRowDescription(rowData));
|
|
770
|
+
getCurrentLocation();
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
761
773
|
|
|
762
|
-
|
|
774
|
+
if (rowData.isPredefinedPlace !== true && fetchDetails === true) {
|
|
775
|
+
if (loadingRowKeyRef.current === getRowKey(rowData)) {
|
|
776
|
+
return; // already requesting
|
|
777
|
+
}
|
|
778
|
+
fetchPlaceDetails(rowData);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
763
781
|
|
|
764
|
-
delete rowData.isLoading;
|
|
765
|
-
getCurrentLocation();
|
|
766
|
-
} else {
|
|
767
782
|
hideListView(true);
|
|
768
|
-
setStateText(
|
|
769
|
-
|
|
770
|
-
_onBlur();
|
|
771
|
-
delete rowData.isLoading;
|
|
772
|
-
const predefinedPlace = _getPredefinedPlace(rowData);
|
|
783
|
+
setStateText(renderRowDescription(rowData));
|
|
784
|
+
handleBlur();
|
|
773
785
|
|
|
774
|
-
//
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
786
|
+
// Predefined places are handed back as their own details payload.
|
|
787
|
+
const predefinedPlace = getPredefinedPlace(rowData);
|
|
788
|
+
onPressProp(predefinedPlace, predefinedPlace);
|
|
789
|
+
},
|
|
790
|
+
[
|
|
791
|
+
debouncedRequest,
|
|
792
|
+
abortSearchRequests,
|
|
793
|
+
hideListView,
|
|
794
|
+
renderRowDescription,
|
|
795
|
+
getCurrentLocation,
|
|
796
|
+
fetchDetails,
|
|
797
|
+
fetchPlaceDetails,
|
|
798
|
+
handleBlur,
|
|
799
|
+
getPredefinedPlace,
|
|
800
|
+
onPressProp,
|
|
801
|
+
],
|
|
802
|
+
);
|
|
784
803
|
|
|
785
|
-
const
|
|
786
|
-
|
|
804
|
+
const handleChangeText = useCallback(
|
|
805
|
+
(text) => {
|
|
806
|
+
// The platform writes the chosen description into the input and echoes
|
|
807
|
+
// it straight back here. That is our own value returning, not the user
|
|
808
|
+
// editing, so it must not re-open the list or start another search.
|
|
809
|
+
if (selectionMadeRef.current && text === selectedTextRef.current) {
|
|
810
|
+
textInputProps?.onChangeText?.(text);
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
787
813
|
|
|
788
|
-
|
|
814
|
+
// Editing the text supersedes whatever was selected before.
|
|
815
|
+
selectionMadeRef.current = false;
|
|
816
|
+
selectedTextRef.current = null;
|
|
817
|
+
setListWasDismissed(false);
|
|
818
|
+
setStateText(text);
|
|
819
|
+
|
|
820
|
+
// Text that cannot produce a search clears the results now instead of a
|
|
821
|
+
// debounce later. Deferring it left the previous predictions on screen —
|
|
822
|
+
// and un-dismissed the list to show them — until the timer elapsed, so
|
|
823
|
+
// pressing the clear button flashed the old list back for `debounce` ms.
|
|
824
|
+
if (!text || text.length < minLength) {
|
|
825
|
+
debouncedRequest.cancel();
|
|
826
|
+
request(text);
|
|
827
|
+
} else {
|
|
828
|
+
debouncedRequest(text);
|
|
829
|
+
}
|
|
789
830
|
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
831
|
+
textInputProps?.onChangeText?.(text);
|
|
832
|
+
},
|
|
833
|
+
[debouncedRequest, request, minLength, textInputProps],
|
|
834
|
+
);
|
|
794
835
|
|
|
795
|
-
const
|
|
796
|
-
(
|
|
797
|
-
if (!
|
|
798
|
-
setListWasDismissed(
|
|
799
|
-
setListViewDisplayed(false);
|
|
836
|
+
const handleFocus = useCallback(
|
|
837
|
+
(event) => {
|
|
838
|
+
if (!selectionMadeRef.current) {
|
|
839
|
+
setListWasDismissed(false);
|
|
800
840
|
}
|
|
841
|
+
textInputProps?.onFocus?.(event);
|
|
801
842
|
},
|
|
802
|
-
[
|
|
843
|
+
[textInputProps],
|
|
803
844
|
);
|
|
804
845
|
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
while (node) {
|
|
814
|
-
if (node.id === 'result-list-id') return true;
|
|
815
|
-
node = node.parentNode;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
return false;
|
|
819
|
-
};
|
|
820
|
-
|
|
821
|
-
const _onBlur = (e) => {
|
|
822
|
-
if (e && isNewFocusInAutocompleteResultList(e)) return;
|
|
823
|
-
|
|
824
|
-
hideListView();
|
|
825
|
-
inputRef?.current?.blur();
|
|
826
|
-
};
|
|
827
|
-
|
|
828
|
-
const _onFocus = () => {
|
|
829
|
-
setListWasDismissed(false);
|
|
830
|
-
setListViewDisplayed(true);
|
|
831
|
-
};
|
|
846
|
+
const handleInputBlur = useCallback(
|
|
847
|
+
(event) => {
|
|
848
|
+
handleBlur(event);
|
|
849
|
+
textInputProps?.onBlur?.(event);
|
|
850
|
+
},
|
|
851
|
+
[handleBlur, textInputProps],
|
|
852
|
+
);
|
|
832
853
|
|
|
833
|
-
//
|
|
834
|
-
//
|
|
835
|
-
//
|
|
854
|
+
// --------------------------------------------------------------------------
|
|
855
|
+
// Effects
|
|
856
|
+
// --------------------------------------------------------------------------
|
|
836
857
|
|
|
837
|
-
|
|
838
|
-
if (
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
return rowData.description || rowData.formatted_address || rowData.name;
|
|
843
|
-
};
|
|
844
|
-
|
|
845
|
-
const _getRowLoader = () => {
|
|
846
|
-
return <ActivityIndicator animating={true} size='small' />;
|
|
847
|
-
};
|
|
848
|
-
|
|
849
|
-
const _renderLoader = (rowData) => {
|
|
850
|
-
if (rowData.isLoading === true) {
|
|
851
|
-
return (
|
|
852
|
-
<View
|
|
853
|
-
style={[
|
|
854
|
-
suppressDefaultStyles ? {} : defaultStyles.loader,
|
|
855
|
-
styles?.loader,
|
|
856
|
-
]}
|
|
857
|
-
>
|
|
858
|
-
{_getRowLoader()}
|
|
859
|
-
</View>
|
|
858
|
+
useEffect(() => {
|
|
859
|
+
if (Platform.OS === 'web' && !requestUrl) {
|
|
860
|
+
console.warn(
|
|
861
|
+
'This library cannot be used for the web unless you specify the requestUrl prop. See https://git.io/JflFv for more for details.',
|
|
860
862
|
);
|
|
861
863
|
}
|
|
864
|
+
}, [requestUrl]);
|
|
862
865
|
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
return (
|
|
872
|
-
<Text
|
|
873
|
-
style={[
|
|
874
|
-
suppressDefaultStyles ? {} : defaultStyles.description,
|
|
875
|
-
styles?.description,
|
|
876
|
-
rowData.isPredefinedPlace ? styles?.predefinedPlacesDescription : {},
|
|
877
|
-
]}
|
|
878
|
-
numberOfLines={numberOfLines}
|
|
879
|
-
>
|
|
880
|
-
{_renderDescription(rowData)}
|
|
881
|
-
</Text>
|
|
882
|
-
);
|
|
883
|
-
};
|
|
884
|
-
|
|
885
|
-
const _renderRow = (rowData = {}, index) => {
|
|
886
|
-
return (
|
|
887
|
-
<ScrollView
|
|
888
|
-
contentContainerStyle={
|
|
889
|
-
isRowScrollable ? { minWidth: '100%' } : { width: '100%' }
|
|
890
|
-
}
|
|
891
|
-
scrollEnabled={isRowScrollable}
|
|
892
|
-
keyboardShouldPersistTaps={keyboardShouldPersistTaps}
|
|
893
|
-
horizontal={true}
|
|
894
|
-
showsHorizontalScrollIndicator={false}
|
|
895
|
-
showsVerticalScrollIndicator={false}
|
|
896
|
-
>
|
|
897
|
-
<Pressable
|
|
898
|
-
style={({ hovered, pressed }) => [
|
|
899
|
-
isRowScrollable ? { minWidth: '100%' } : { width: '100%' },
|
|
900
|
-
{
|
|
901
|
-
backgroundColor: pressed
|
|
902
|
-
? listUnderlayColor
|
|
903
|
-
: hovered
|
|
904
|
-
? listHoverColor
|
|
905
|
-
: undefined,
|
|
906
|
-
},
|
|
907
|
-
]}
|
|
908
|
-
onPress={() => _onPress(rowData)}
|
|
909
|
-
onBlur={_onBlur}
|
|
910
|
-
>
|
|
911
|
-
<View
|
|
912
|
-
style={[
|
|
913
|
-
suppressDefaultStyles ? {} : defaultStyles.row,
|
|
914
|
-
styles?.row,
|
|
915
|
-
rowData.isPredefinedPlace ? styles?.specialItemRow : {},
|
|
916
|
-
]}
|
|
917
|
-
>
|
|
918
|
-
{_renderLoader(rowData)}
|
|
919
|
-
{_renderRowData(rowData, index)}
|
|
920
|
-
</View>
|
|
921
|
-
</Pressable>
|
|
922
|
-
</ScrollView>
|
|
923
|
-
);
|
|
924
|
-
};
|
|
925
|
-
|
|
926
|
-
const _renderSeparator = (sectionID, rowID) => {
|
|
927
|
-
if (rowID === dataSource.length - 1) {
|
|
928
|
-
return null;
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
return (
|
|
932
|
-
<View
|
|
933
|
-
key={`${sectionID}-${rowID}`}
|
|
934
|
-
style={[
|
|
935
|
-
suppressDefaultStyles ? {} : defaultStyles.separator,
|
|
936
|
-
styles?.separator,
|
|
937
|
-
]}
|
|
938
|
-
/>
|
|
939
|
-
);
|
|
940
|
-
};
|
|
941
|
-
|
|
942
|
-
const _shouldShowPoweredLogo = () => {
|
|
943
|
-
if (!enablePoweredByContainer || dataSource.length === 0) {
|
|
944
|
-
return false;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
for (let i = 0; i < dataSource.length; i++) {
|
|
948
|
-
const row = dataSource[i];
|
|
949
|
-
|
|
950
|
-
if (!('isCurrentLocation' in row) && !('isPredefinedPlace' in row)) {
|
|
951
|
-
return true;
|
|
952
|
-
}
|
|
953
|
-
}
|
|
954
|
-
|
|
955
|
-
return false;
|
|
956
|
-
};
|
|
957
|
-
|
|
958
|
-
const _renderPoweredLogo = () => {
|
|
959
|
-
if (!_shouldShowPoweredLogo()) {
|
|
960
|
-
return null;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
return (
|
|
964
|
-
<View
|
|
965
|
-
style={[
|
|
966
|
-
suppressDefaultStyles ? {} : defaultStyles.row,
|
|
967
|
-
defaultStyles.poweredContainer,
|
|
968
|
-
styles?.poweredContainer,
|
|
969
|
-
]}
|
|
970
|
-
>
|
|
971
|
-
<Image
|
|
972
|
-
style={[
|
|
973
|
-
suppressDefaultStyles ? {} : defaultStyles.powered,
|
|
974
|
-
styles?.powered,
|
|
975
|
-
]}
|
|
976
|
-
resizeMode='contain'
|
|
977
|
-
source={require('./images/powered_by_google_on_white.png')}
|
|
978
|
-
/>
|
|
979
|
-
</View>
|
|
980
|
-
);
|
|
981
|
-
};
|
|
982
|
-
|
|
983
|
-
const _renderLeftButton = () => {
|
|
984
|
-
if (props.renderLeftButton) {
|
|
985
|
-
return props.renderLeftButton();
|
|
986
|
-
}
|
|
987
|
-
return null;
|
|
988
|
-
};
|
|
989
|
-
|
|
990
|
-
const _renderRightButton = () => {
|
|
991
|
-
if (props.renderRightButton) {
|
|
992
|
-
return props.renderRightButton();
|
|
993
|
-
}
|
|
994
|
-
return null;
|
|
995
|
-
};
|
|
996
|
-
|
|
997
|
-
const _getFlatList = () => {
|
|
998
|
-
const keyExtractor = (item, index) => {
|
|
999
|
-
// Use stable keys based on item data
|
|
1000
|
-
if (item.place_id) {
|
|
1001
|
-
return `place_${item.place_id}_${index}`;
|
|
1002
|
-
}
|
|
1003
|
-
if (item.isCurrentLocation) {
|
|
1004
|
-
return 'current_location';
|
|
1005
|
-
}
|
|
1006
|
-
if (item.isPredefinedPlace && item.description) {
|
|
1007
|
-
return `predefined_${item.description}_${index}`;
|
|
1008
|
-
}
|
|
1009
|
-
// Fallback to index-based key (should rarely happen)
|
|
1010
|
-
return `item_${index}`;
|
|
1011
|
-
};
|
|
1012
|
-
|
|
1013
|
-
// Show list if:
|
|
1014
|
-
// 1. Platform is supported
|
|
1015
|
-
// 2. There's data to show (dataSource has items)
|
|
1016
|
-
// 3. listViewDisplayed is true OR we're in 'auto' mode (auto-shows when data exists)
|
|
1017
|
-
const isAutoMode =
|
|
1018
|
-
listViewDisplayedProp === 'auto' || listViewDisplayedProp === undefined;
|
|
1019
|
-
const shouldShowList =
|
|
1020
|
-
supportedPlatform() &&
|
|
1021
|
-
dataSource.length > 0 &&
|
|
1022
|
-
(listViewDisplayed === true || (isAutoMode && !listWasDismissed));
|
|
1023
|
-
|
|
1024
|
-
if (shouldShowList) {
|
|
1025
|
-
return (
|
|
1026
|
-
<FlatList
|
|
1027
|
-
nativeID='result-list-id'
|
|
1028
|
-
scrollEnabled={!disableScroll}
|
|
1029
|
-
nestedScrollEnabled={true}
|
|
1030
|
-
keyboardShouldPersistTaps={keyboardShouldPersistTaps}
|
|
1031
|
-
style={[
|
|
1032
|
-
suppressDefaultStyles ? {} : defaultStyles.listView,
|
|
1033
|
-
styles?.listView,
|
|
1034
|
-
]}
|
|
1035
|
-
data={dataSource}
|
|
1036
|
-
keyExtractor={keyExtractor}
|
|
1037
|
-
extraData={[dataSource, props]}
|
|
1038
|
-
ItemSeparatorComponent={_renderSeparator}
|
|
1039
|
-
renderItem={({ item, index }) => _renderRow(item, index)}
|
|
1040
|
-
ListEmptyComponent={
|
|
1041
|
-
listLoaderDisplayed
|
|
1042
|
-
? props.listLoaderComponent
|
|
1043
|
-
: stateText.length > minLength && props.listEmptyComponent
|
|
1044
|
-
}
|
|
1045
|
-
ListHeaderComponent={
|
|
1046
|
-
props.renderHeaderComponent &&
|
|
1047
|
-
props.renderHeaderComponent(stateText)
|
|
1048
|
-
}
|
|
1049
|
-
ListFooterComponent={_renderPoweredLogo}
|
|
1050
|
-
{...restProps}
|
|
1051
|
-
/>
|
|
866
|
+
useEffect(() => {
|
|
867
|
+
if (currentLocation === true && !hasGeolocation) {
|
|
868
|
+
console.warn(
|
|
869
|
+
Platform.OS === 'web'
|
|
870
|
+
? 'Geolocation is not available. For web, ensure your site is served over HTTPS or localhost to use geolocation features.'
|
|
871
|
+
: 'Geolocation is not available. For React Native, you may need to install and configure @react-native-community/geolocation or expo-location to enable currentLocation.',
|
|
1052
872
|
);
|
|
1053
873
|
}
|
|
874
|
+
}, [currentLocation, hasGeolocation]);
|
|
1054
875
|
|
|
1055
|
-
|
|
1056
|
-
};
|
|
1057
|
-
|
|
1058
|
-
// ==========================================================================
|
|
1059
|
-
// EFFECTS
|
|
1060
|
-
// ==========================================================================
|
|
1061
|
-
|
|
1062
|
-
// Update query ref when query changes
|
|
1063
|
-
useEffect(() => {
|
|
1064
|
-
queryRef.current = query;
|
|
1065
|
-
}, [query]);
|
|
1066
|
-
|
|
1067
|
-
// Initialize URL from requestUrl prop
|
|
1068
|
-
useEffect(() => {
|
|
1069
|
-
setUrl(getRequestUrl(props.requestUrl));
|
|
1070
|
-
}, [props.requestUrl]);
|
|
1071
|
-
|
|
1072
|
-
// Initialize dataSource on mount
|
|
1073
|
-
useEffect(() => {
|
|
1074
|
-
setDataSource(buildRowsFromResults([]));
|
|
1075
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1076
|
-
}, []);
|
|
1077
|
-
|
|
1078
|
-
// Keep requestRef updated
|
|
1079
|
-
requestRef.current = _request;
|
|
1080
|
-
|
|
1081
|
-
// Debounce setup
|
|
1082
|
-
const debounceData = useMemo(() => {
|
|
1083
|
-
return debounce((text) => requestRef.current(text), debounceMs);
|
|
1084
|
-
}, [debounceMs]);
|
|
1085
|
-
|
|
876
|
+
// Rebuild the rows whenever the predefined-place configuration changes.
|
|
1086
877
|
useEffect(() => {
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
if (debounceData.cancel) {
|
|
1090
|
-
debounceData.cancel();
|
|
1091
|
-
}
|
|
1092
|
-
};
|
|
1093
|
-
}, [debounceData]);
|
|
878
|
+
setDataSource(buildRows(resultsRef.current, stateTextRef.current));
|
|
879
|
+
}, [buildRows]);
|
|
1094
880
|
|
|
1095
|
-
//
|
|
881
|
+
// Re-run the current search when the query object changes.
|
|
1096
882
|
useEffect(() => {
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
if (queryChanged) {
|
|
883
|
+
if (prevQueryStringRef.current === null) {
|
|
1100
884
|
prevQueryStringRef.current = queryString;
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (prevQueryStringRef.current === queryString) {
|
|
888
|
+
return;
|
|
1104
889
|
}
|
|
1105
890
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
};
|
|
1109
|
-
}, [queryString, debounceData, stateText, minLength, _abortRequests]);
|
|
891
|
+
prevQueryStringRef.current = queryString;
|
|
892
|
+
const text = stateTextRef.current;
|
|
1110
893
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
if (
|
|
1114
|
-
listViewDisplayedProp === 'auto' &&
|
|
1115
|
-
dataSource.length > 0 &&
|
|
1116
|
-
!listViewDisplayed &&
|
|
1117
|
-
!listWasDismissed
|
|
1118
|
-
) {
|
|
1119
|
-
setListViewDisplayed(true);
|
|
894
|
+
if (text && text.length >= minLength) {
|
|
895
|
+
debouncedRequest(text);
|
|
1120
896
|
}
|
|
1121
|
-
}, [
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
897
|
+
}, [queryString, minLength, debouncedRequest]);
|
|
898
|
+
|
|
899
|
+
// Aborting belongs to the component lifetime, not to every keystroke. It used
|
|
900
|
+
// to be tangled into the query effect, whose `stateText` dependency meant a
|
|
901
|
+
// `preProcess` call would abort the very request it had just sent.
|
|
902
|
+
useEffect(
|
|
903
|
+
() => () => {
|
|
904
|
+
debouncedRequest.cancel();
|
|
905
|
+
abortSearchRequests();
|
|
906
|
+
abortDetailsRequest();
|
|
907
|
+
},
|
|
908
|
+
[debouncedRequest, abortSearchRequests, abortDetailsRequest],
|
|
909
|
+
);
|
|
1127
910
|
|
|
1128
|
-
//
|
|
1129
|
-
//
|
|
1130
|
-
//
|
|
911
|
+
// --------------------------------------------------------------------------
|
|
912
|
+
// Imperative handle
|
|
913
|
+
// --------------------------------------------------------------------------
|
|
1131
914
|
|
|
1132
915
|
useImperativeHandle(
|
|
1133
916
|
ref,
|
|
1134
917
|
() => ({
|
|
1135
|
-
setAddressText: (address) =>
|
|
1136
|
-
|
|
1137
|
-
},
|
|
1138
|
-
getAddressText: () => stateText,
|
|
918
|
+
setAddressText: (address) => setStateText(address),
|
|
919
|
+
getAddressText: () => stateTextRef.current,
|
|
1139
920
|
blur: () => inputRef.current?.blur(),
|
|
1140
921
|
focus: () => inputRef.current?.focus(),
|
|
1141
922
|
isFocused: () => inputRef.current?.isFocused(),
|
|
1142
923
|
clear: () => inputRef.current?.clear(),
|
|
1143
924
|
getCurrentLocation,
|
|
1144
925
|
}),
|
|
1145
|
-
[
|
|
926
|
+
[getCurrentLocation],
|
|
927
|
+
);
|
|
928
|
+
|
|
929
|
+
// --------------------------------------------------------------------------
|
|
930
|
+
// Render
|
|
931
|
+
// --------------------------------------------------------------------------
|
|
932
|
+
|
|
933
|
+
const renderItem = useCallback(
|
|
934
|
+
({ item, index }) => (
|
|
935
|
+
<Row
|
|
936
|
+
rowData={item}
|
|
937
|
+
index={index}
|
|
938
|
+
isLoading={loadingRowKey === getRowKey(item)}
|
|
939
|
+
isRowScrollable={isRowScrollable}
|
|
940
|
+
keyboardShouldPersistTaps={keyboardShouldPersistTaps}
|
|
941
|
+
listHoverColor={listHoverColor}
|
|
942
|
+
listUnderlayColor={listUnderlayColor}
|
|
943
|
+
mergedStyles={mergedStyles}
|
|
944
|
+
numberOfLines={numberOfLines}
|
|
945
|
+
onBlur={handleBlur}
|
|
946
|
+
onPress={handleRowPress}
|
|
947
|
+
renderDescription={renderRowDescription}
|
|
948
|
+
renderRow={renderRow}
|
|
949
|
+
/>
|
|
950
|
+
),
|
|
951
|
+
[
|
|
952
|
+
loadingRowKey,
|
|
953
|
+
isRowScrollable,
|
|
954
|
+
keyboardShouldPersistTaps,
|
|
955
|
+
listHoverColor,
|
|
956
|
+
listUnderlayColor,
|
|
957
|
+
mergedStyles,
|
|
958
|
+
numberOfLines,
|
|
959
|
+
handleBlur,
|
|
960
|
+
handleRowPress,
|
|
961
|
+
renderRowDescription,
|
|
962
|
+
renderRow,
|
|
963
|
+
],
|
|
964
|
+
);
|
|
965
|
+
|
|
966
|
+
const keyExtractor = useCallback(
|
|
967
|
+
(item, index) => `${getRowKey(item)}:${index}`,
|
|
968
|
+
[],
|
|
969
|
+
);
|
|
970
|
+
|
|
971
|
+
const Separator = useMemo(
|
|
972
|
+
() =>
|
|
973
|
+
function ItemSeparator() {
|
|
974
|
+
return <View style={mergedStyles.separator} />;
|
|
975
|
+
},
|
|
976
|
+
[mergedStyles],
|
|
1146
977
|
);
|
|
1147
978
|
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
979
|
+
const poweredComponent = useMemo(() => {
|
|
980
|
+
if (!enablePoweredByContainer || !hasApiResults(dataSource)) {
|
|
981
|
+
return null;
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
return (
|
|
985
|
+
<View style={mergedStyles.poweredContainer}>
|
|
986
|
+
<Image
|
|
987
|
+
style={mergedStyles.powered}
|
|
988
|
+
resizeMode='contain'
|
|
989
|
+
source={require('./images/powered_by_google_on_white.png')}
|
|
990
|
+
/>
|
|
991
|
+
</View>
|
|
992
|
+
);
|
|
993
|
+
}, [enablePoweredByContainer, dataSource, mergedStyles]);
|
|
994
|
+
|
|
995
|
+
// The list used to be gated on `dataSource.length > 0`, which meant
|
|
996
|
+
// ListEmptyComponent — and therefore both listEmptyComponent and
|
|
997
|
+
// listLoaderComponent — could never render.
|
|
998
|
+
const emptyStateComponent = listLoaderDisplayed
|
|
999
|
+
? listLoaderComponent
|
|
1000
|
+
: stateText.length > minLength
|
|
1001
|
+
? listEmptyComponent
|
|
1002
|
+
: null;
|
|
1003
|
+
|
|
1004
|
+
const isListVisible = isAutoMode
|
|
1005
|
+
? !listWasDismissed
|
|
1006
|
+
: listViewDisplayedProp === true;
|
|
1007
|
+
|
|
1008
|
+
const shouldShowList =
|
|
1009
|
+
isSupportedPlatform &&
|
|
1010
|
+
isListVisible &&
|
|
1011
|
+
(dataSource.length > 0 || Boolean(emptyStateComponent));
|
|
1151
1012
|
|
|
1152
1013
|
const {
|
|
1153
|
-
onFocus:
|
|
1154
|
-
onBlur:
|
|
1155
|
-
onChangeText:
|
|
1014
|
+
onFocus: _ignoredOnFocus,
|
|
1015
|
+
onBlur: _ignoredOnBlur,
|
|
1016
|
+
onChangeText: _ignoredOnChangeText,
|
|
1156
1017
|
clearButtonMode,
|
|
1157
1018
|
InputComp,
|
|
1158
|
-
...
|
|
1159
|
-
} = textInputProps ||
|
|
1019
|
+
...userTextInputProps
|
|
1020
|
+
} = textInputProps || EMPTY_OBJECT;
|
|
1021
|
+
|
|
1160
1022
|
const TextInputComp = InputComp || TextInput;
|
|
1161
1023
|
|
|
1162
1024
|
return (
|
|
1163
|
-
<View
|
|
1164
|
-
style={[
|
|
1165
|
-
suppressDefaultStyles ? {} : defaultStyles.container,
|
|
1166
|
-
styles?.container,
|
|
1167
|
-
]}
|
|
1168
|
-
pointerEvents='box-none'
|
|
1169
|
-
>
|
|
1025
|
+
<View style={mergedStyles.container} pointerEvents='box-none'>
|
|
1170
1026
|
{!textInputHide && (
|
|
1171
|
-
<View
|
|
1172
|
-
|
|
1173
|
-
suppressDefaultStyles ? {} : defaultStyles.textInputContainer,
|
|
1174
|
-
styles?.textInputContainer,
|
|
1175
|
-
]}
|
|
1176
|
-
>
|
|
1177
|
-
{_renderLeftButton()}
|
|
1027
|
+
<View style={mergedStyles.textInputContainer}>
|
|
1028
|
+
{renderLeftButton ? renderLeftButton() : null}
|
|
1178
1029
|
<TextInputComp
|
|
1179
1030
|
ref={inputRef}
|
|
1180
|
-
style={
|
|
1181
|
-
suppressDefaultStyles ? {} : defaultStyles.textInput,
|
|
1182
|
-
styles?.textInput,
|
|
1183
|
-
]}
|
|
1031
|
+
style={mergedStyles.textInput}
|
|
1184
1032
|
value={stateText}
|
|
1185
1033
|
placeholder={placeholder}
|
|
1186
|
-
onFocus={
|
|
1187
|
-
|
|
1188
|
-
? (e) => {
|
|
1189
|
-
_onFocus();
|
|
1190
|
-
textInputOnFocus(e);
|
|
1191
|
-
}
|
|
1192
|
-
: _onFocus
|
|
1193
|
-
}
|
|
1194
|
-
onBlur={
|
|
1195
|
-
textInputOnBlur
|
|
1196
|
-
? (e) => {
|
|
1197
|
-
_onBlur(e);
|
|
1198
|
-
textInputOnBlur(e);
|
|
1199
|
-
}
|
|
1200
|
-
: _onBlur
|
|
1201
|
-
}
|
|
1034
|
+
onFocus={handleFocus}
|
|
1035
|
+
onBlur={handleInputBlur}
|
|
1202
1036
|
clearButtonMode={clearButtonMode || 'while-editing'}
|
|
1203
|
-
onChangeText={
|
|
1204
|
-
{...
|
|
1037
|
+
onChangeText={handleChangeText}
|
|
1038
|
+
{...userTextInputProps}
|
|
1205
1039
|
/>
|
|
1206
|
-
{
|
|
1040
|
+
{renderRightButton ? renderRightButton() : null}
|
|
1207
1041
|
</View>
|
|
1208
1042
|
)}
|
|
1209
|
-
|
|
1210
|
-
{
|
|
1211
|
-
|
|
1043
|
+
|
|
1044
|
+
{inbetweenCompo}
|
|
1045
|
+
|
|
1046
|
+
{shouldShowList ? (
|
|
1047
|
+
<FlatList
|
|
1048
|
+
nativeID='result-list-id'
|
|
1049
|
+
scrollEnabled={!disableScroll}
|
|
1050
|
+
nestedScrollEnabled={true}
|
|
1051
|
+
keyboardShouldPersistTaps={keyboardShouldPersistTaps}
|
|
1052
|
+
style={mergedStyles.listView}
|
|
1053
|
+
data={dataSource}
|
|
1054
|
+
extraData={loadingRowKey}
|
|
1055
|
+
keyExtractor={keyExtractor}
|
|
1056
|
+
ItemSeparatorComponent={Separator}
|
|
1057
|
+
renderItem={renderItem}
|
|
1058
|
+
ListEmptyComponent={emptyStateComponent}
|
|
1059
|
+
ListHeaderComponent={
|
|
1060
|
+
renderHeaderComponent ? renderHeaderComponent(stateText) : null
|
|
1061
|
+
}
|
|
1062
|
+
ListFooterComponent={poweredComponent}
|
|
1063
|
+
{...restProps}
|
|
1064
|
+
/>
|
|
1065
|
+
) : null}
|
|
1066
|
+
|
|
1067
|
+
{children}
|
|
1212
1068
|
</View>
|
|
1213
1069
|
);
|
|
1214
1070
|
});
|