@pro6pp/infer-react 0.0.2-beta.1 → 0.0.2-beta.10
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/README.md +49 -7
- package/dist/index.cjs +777 -0
- package/dist/index.d.cts +68 -0
- package/dist/index.d.ts +54 -10
- package/dist/index.js +732 -32
- package/package.json +31 -8
- package/dist/index.d.mts +0 -24
- package/dist/index.mjs +0 -25
package/dist/index.js
CHANGED
|
@@ -1,50 +1,750 @@
|
|
|
1
|
-
"use strict";
|
|
2
1
|
var __defProp = Object.defineProperty;
|
|
3
|
-
var
|
|
4
|
-
var
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
|
+
|
|
5
|
+
// src/index.tsx
|
|
6
|
+
import React, {
|
|
7
|
+
useState,
|
|
8
|
+
useMemo,
|
|
9
|
+
useEffect,
|
|
10
|
+
useRef,
|
|
11
|
+
forwardRef,
|
|
12
|
+
useImperativeHandle
|
|
13
|
+
} from "react";
|
|
14
|
+
|
|
15
|
+
// ../core/src/core.ts
|
|
16
|
+
var DEFAULTS = {
|
|
17
|
+
API_URL: "https://api.pro6pp.nl/v2",
|
|
18
|
+
LIMIT: 20,
|
|
19
|
+
DEBOUNCE_MS: 150,
|
|
20
|
+
MIN_DEBOUNCE_MS: 50,
|
|
21
|
+
MAX_RETRIES: 0
|
|
22
|
+
};
|
|
23
|
+
var PATTERNS = {
|
|
24
|
+
DIGITS_1_3: /^[0-9]{1,3}$/
|
|
25
|
+
};
|
|
26
|
+
var INITIAL_STATE = {
|
|
27
|
+
query: "",
|
|
28
|
+
stage: null,
|
|
29
|
+
cities: [],
|
|
30
|
+
streets: [],
|
|
31
|
+
suggestions: [],
|
|
32
|
+
isValid: false,
|
|
33
|
+
isError: false,
|
|
34
|
+
isLoading: false,
|
|
35
|
+
hasMore: false,
|
|
36
|
+
selectedSuggestionIndex: -1
|
|
9
37
|
};
|
|
10
|
-
var
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
38
|
+
var InferCore = class {
|
|
39
|
+
/**
|
|
40
|
+
* Initializes a new instance of the Infer engine.
|
|
41
|
+
* @param config The configuration object including API keys and callbacks.
|
|
42
|
+
*/
|
|
43
|
+
constructor(config) {
|
|
44
|
+
__publicField(this, "country");
|
|
45
|
+
__publicField(this, "authKey");
|
|
46
|
+
__publicField(this, "explicitApiUrl");
|
|
47
|
+
__publicField(this, "baseLimit");
|
|
48
|
+
__publicField(this, "currentLimit");
|
|
49
|
+
__publicField(this, "maxRetries");
|
|
50
|
+
__publicField(this, "fetcher");
|
|
51
|
+
__publicField(this, "onStateChange");
|
|
52
|
+
__publicField(this, "onSelect");
|
|
53
|
+
/**
|
|
54
|
+
* The current read-only state of the engine.
|
|
55
|
+
* Use `onStateChange` to react to updates.
|
|
56
|
+
*/
|
|
57
|
+
__publicField(this, "state");
|
|
58
|
+
__publicField(this, "abortController", null);
|
|
59
|
+
__publicField(this, "debouncedFetch");
|
|
60
|
+
__publicField(this, "isSelecting", false);
|
|
61
|
+
this.country = config.country;
|
|
62
|
+
this.authKey = config.authKey;
|
|
63
|
+
this.explicitApiUrl = config.apiUrl;
|
|
64
|
+
this.baseLimit = config.limit || DEFAULTS.LIMIT;
|
|
65
|
+
this.currentLimit = this.baseLimit;
|
|
66
|
+
const configRetries = config.maxRetries !== void 0 ? config.maxRetries : DEFAULTS.MAX_RETRIES;
|
|
67
|
+
this.maxRetries = Math.max(0, Math.min(configRetries, 10));
|
|
68
|
+
this.fetcher = config.fetcher || ((url, init) => fetch(url, init));
|
|
69
|
+
this.onStateChange = config.onStateChange || (() => {
|
|
70
|
+
});
|
|
71
|
+
this.onSelect = config.onSelect || (() => {
|
|
72
|
+
});
|
|
73
|
+
this.state = { ...INITIAL_STATE };
|
|
74
|
+
const configDebounce = config.debounceMs !== void 0 ? config.debounceMs : DEFAULTS.DEBOUNCE_MS;
|
|
75
|
+
const debounceTime = Math.max(configDebounce, DEFAULTS.MIN_DEBOUNCE_MS);
|
|
76
|
+
this.debouncedFetch = this.debounce((val) => this.executeFetch(val), debounceTime);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Processes new text input from the user.
|
|
80
|
+
* Triggers a debounced API request and updates the internal state.
|
|
81
|
+
* @param value The raw string from the input field.
|
|
82
|
+
*/
|
|
83
|
+
handleInput(value) {
|
|
84
|
+
if (this.isSelecting) {
|
|
85
|
+
this.isSelecting = false;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
this.currentLimit = this.baseLimit;
|
|
89
|
+
const isEditingFinal = this.state.stage === "final" && value !== this.state.query;
|
|
90
|
+
this.updateState({
|
|
91
|
+
query: value,
|
|
92
|
+
isValid: false,
|
|
93
|
+
isLoading: !!value.trim(),
|
|
94
|
+
selectedSuggestionIndex: -1,
|
|
95
|
+
hasMore: false
|
|
96
|
+
});
|
|
97
|
+
if (isEditingFinal) {
|
|
98
|
+
this.onSelect(null);
|
|
99
|
+
}
|
|
100
|
+
this.debouncedFetch(value);
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Increases the current limit and re-fetches the query to show more results.
|
|
104
|
+
*/
|
|
105
|
+
loadMore() {
|
|
106
|
+
if (this.state.isLoading) return;
|
|
107
|
+
this.currentLimit += this.baseLimit;
|
|
108
|
+
this.updateState({ isLoading: true });
|
|
109
|
+
this.executeFetch(this.state.query);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Handles keyboard events for the input field.
|
|
113
|
+
* Supports:
|
|
114
|
+
* - `ArrowUp`/`ArrowDown`: Navigate through the suggestion list.
|
|
115
|
+
* - `Enter`: Select the currently highlighted suggestion.
|
|
116
|
+
* - `Space`: Automatically inserts a comma if a numeric house number is detected.
|
|
117
|
+
* @param event The keyboard event from the input element.
|
|
118
|
+
*/
|
|
119
|
+
handleKeyDown(event) {
|
|
120
|
+
const target = event.target;
|
|
121
|
+
if (!target) return;
|
|
122
|
+
const totalItems = this.state.cities.length + this.state.streets.length + this.state.suggestions.length;
|
|
123
|
+
if (totalItems > 0) {
|
|
124
|
+
if (event.key === "ArrowDown") {
|
|
125
|
+
event.preventDefault();
|
|
126
|
+
let nextIndex = this.state.selectedSuggestionIndex + 1;
|
|
127
|
+
if (nextIndex >= totalItems) {
|
|
128
|
+
nextIndex = 0;
|
|
129
|
+
}
|
|
130
|
+
this.updateState({ selectedSuggestionIndex: nextIndex });
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (event.key === "ArrowUp") {
|
|
134
|
+
event.preventDefault();
|
|
135
|
+
let nextIndex = this.state.selectedSuggestionIndex - 1;
|
|
136
|
+
if (nextIndex < 0) {
|
|
137
|
+
nextIndex = totalItems - 1;
|
|
138
|
+
}
|
|
139
|
+
this.updateState({ selectedSuggestionIndex: nextIndex });
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (event.key === "Enter" && this.state.selectedSuggestionIndex >= 0) {
|
|
143
|
+
event.preventDefault();
|
|
144
|
+
const allItems = [...this.state.cities, ...this.state.streets, ...this.state.suggestions];
|
|
145
|
+
const item = allItems[this.state.selectedSuggestionIndex];
|
|
146
|
+
if (item) {
|
|
147
|
+
this.selectItem(item);
|
|
148
|
+
this.updateState({ selectedSuggestionIndex: -1 });
|
|
149
|
+
}
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
const val = target.value;
|
|
154
|
+
if (event.key === " " && this.shouldAutoInsertComma(val)) {
|
|
155
|
+
event.preventDefault();
|
|
156
|
+
const next = `${val.trim()}, `;
|
|
157
|
+
this.updateQueryAndFetch(next);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Manually selects a suggestion or a string value.
|
|
162
|
+
* This is typically called when a user clicks a suggestion in the UI.
|
|
163
|
+
* @param item The suggestion object or string to select.
|
|
164
|
+
* @returns boolean True if the selection is a final address.
|
|
165
|
+
*/
|
|
166
|
+
selectItem(item) {
|
|
167
|
+
this.debouncedFetch.cancel();
|
|
168
|
+
if (this.abortController) {
|
|
169
|
+
this.abortController.abort();
|
|
170
|
+
}
|
|
171
|
+
const label = typeof item === "string" ? item : item.label;
|
|
172
|
+
let logicValue = label;
|
|
173
|
+
if (typeof item !== "string" && typeof item.value === "string") {
|
|
174
|
+
logicValue = item.value;
|
|
175
|
+
}
|
|
176
|
+
const valueObj = typeof item !== "string" && typeof item.value === "object" ? item.value : void 0;
|
|
177
|
+
const isFullResult = !!valueObj && Object.keys(valueObj).length > 0;
|
|
178
|
+
this.isSelecting = true;
|
|
179
|
+
if (this.state.stage === "final" || isFullResult) {
|
|
180
|
+
let finalQuery = label;
|
|
181
|
+
if (valueObj && Object.keys(valueObj).length > 0) {
|
|
182
|
+
const { street, street_number, house_number, city } = valueObj;
|
|
183
|
+
const number = street_number || house_number;
|
|
184
|
+
if (street && number && city) {
|
|
185
|
+
finalQuery = `${street} ${number}, ${city}`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
this.finishSelection(finalQuery, valueObj);
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
const subtitle = typeof item !== "string" ? item.subtitle : null;
|
|
192
|
+
this.processSelection(logicValue, subtitle);
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
shouldAutoInsertComma(currentVal) {
|
|
196
|
+
const isStartOfSegmentAndNumeric = !currentVal.includes(",") && PATTERNS.DIGITS_1_3.test(currentVal.trim());
|
|
197
|
+
if (isStartOfSegmentAndNumeric) return true;
|
|
198
|
+
if (this.state.stage === "house_number") {
|
|
199
|
+
const currentFragment = this.getCurrentFragment(currentVal);
|
|
200
|
+
return PATTERNS.DIGITS_1_3.test(currentFragment);
|
|
201
|
+
}
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
finishSelection(label, value) {
|
|
205
|
+
this.updateState({
|
|
206
|
+
query: label,
|
|
207
|
+
suggestions: [],
|
|
208
|
+
cities: [],
|
|
209
|
+
streets: [],
|
|
210
|
+
isValid: true,
|
|
211
|
+
stage: "final",
|
|
212
|
+
hasMore: false
|
|
213
|
+
});
|
|
214
|
+
this.onSelect(value || label);
|
|
215
|
+
}
|
|
216
|
+
processSelection(text, subtitle) {
|
|
217
|
+
const { stage, query } = this.state;
|
|
218
|
+
let nextQuery = query;
|
|
219
|
+
const isContextualSelection = subtitle && (stage === "city" || stage === "street" || stage === "mixed");
|
|
220
|
+
if (isContextualSelection) {
|
|
221
|
+
if (stage === "city") {
|
|
222
|
+
nextQuery = `${subtitle}, ${text}, `;
|
|
223
|
+
} else {
|
|
224
|
+
const prefix = this.getQueryPrefix(query);
|
|
225
|
+
nextQuery = prefix ? `${prefix} ${text}, ${subtitle}, ` : `${text}, ${subtitle}, `;
|
|
226
|
+
}
|
|
227
|
+
this.updateQueryAndFetch(nextQuery);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (stage === "direct" || stage === "addition") {
|
|
231
|
+
this.finishSelection(text);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const hasComma = query.includes(",");
|
|
235
|
+
const isFirstSegment = !hasComma && (stage === "city" || stage === "street" || stage === "house_number_first");
|
|
236
|
+
if (isFirstSegment) {
|
|
237
|
+
nextQuery = `${text}, `;
|
|
238
|
+
} else {
|
|
239
|
+
nextQuery = this.replaceLastSegment(query, text);
|
|
240
|
+
if (stage !== "house_number") {
|
|
241
|
+
nextQuery += ", ";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
this.updateQueryAndFetch(nextQuery);
|
|
245
|
+
}
|
|
246
|
+
executeFetch(val, attempt = 0) {
|
|
247
|
+
const text = (val || "").toString();
|
|
248
|
+
if (!text.trim()) {
|
|
249
|
+
this.abortController?.abort();
|
|
250
|
+
this.resetState();
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (attempt === 0) {
|
|
254
|
+
this.updateState({ isError: false });
|
|
255
|
+
if (this.abortController) this.abortController.abort();
|
|
256
|
+
this.abortController = new AbortController();
|
|
257
|
+
}
|
|
258
|
+
const currentSignal = this.abortController?.signal;
|
|
259
|
+
const baseUrl = this.explicitApiUrl ? this.explicitApiUrl : `${DEFAULTS.API_URL}/infer/${this.country.toLowerCase()}`;
|
|
260
|
+
const params = new URLSearchParams({
|
|
261
|
+
country: this.country.toLowerCase(),
|
|
262
|
+
query: text,
|
|
263
|
+
limit: this.currentLimit.toString()
|
|
264
|
+
});
|
|
265
|
+
if (this.authKey) {
|
|
266
|
+
params.set("authKey", this.authKey);
|
|
267
|
+
}
|
|
268
|
+
const separator = baseUrl.includes("?") ? "&" : "?";
|
|
269
|
+
const finalUrl = `${baseUrl}${separator}${params.toString()}`;
|
|
270
|
+
this.fetcher(finalUrl, { signal: currentSignal }).then((res) => {
|
|
271
|
+
if (!res.ok) {
|
|
272
|
+
if (attempt < this.maxRetries && (res.status >= 500 || res.status === 429)) {
|
|
273
|
+
return this.retry(val, attempt, currentSignal);
|
|
274
|
+
}
|
|
275
|
+
throw new Error("Network error");
|
|
276
|
+
}
|
|
277
|
+
return res.json();
|
|
278
|
+
}).then((data) => {
|
|
279
|
+
if (data) this.mapResponseToState(data);
|
|
280
|
+
}).catch((e) => {
|
|
281
|
+
if (e.name === "AbortError") return;
|
|
282
|
+
if (attempt < this.maxRetries) {
|
|
283
|
+
return this.retry(val, attempt, currentSignal);
|
|
284
|
+
}
|
|
285
|
+
this.updateState({ isError: true, isLoading: false });
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
retry(val, attempt, signal) {
|
|
289
|
+
if (signal?.aborted) return;
|
|
290
|
+
const delay = Math.pow(2, attempt) * 200;
|
|
291
|
+
setTimeout(() => {
|
|
292
|
+
if (!signal?.aborted) {
|
|
293
|
+
this.executeFetch(val, attempt + 1);
|
|
294
|
+
}
|
|
295
|
+
}, delay);
|
|
296
|
+
}
|
|
297
|
+
mapResponseToState(data) {
|
|
298
|
+
const newState = {
|
|
299
|
+
stage: data.stage,
|
|
300
|
+
isLoading: false
|
|
301
|
+
};
|
|
302
|
+
let autoSelect = false;
|
|
303
|
+
let autoSelectItem = null;
|
|
304
|
+
const rawSuggestions = data.suggestions || [];
|
|
305
|
+
const uniqueSuggestions = [];
|
|
306
|
+
const seen = /* @__PURE__ */ new Set();
|
|
307
|
+
for (const item of rawSuggestions) {
|
|
308
|
+
const key = `${item.label}|${item.subtitle || ""}|${JSON.stringify(item.value || {})}`;
|
|
309
|
+
if (!seen.has(key)) {
|
|
310
|
+
seen.add(key);
|
|
311
|
+
uniqueSuggestions.push(item);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const totalCount = uniqueSuggestions.length + (data.cities?.length || 0) + (data.streets?.length || 0);
|
|
315
|
+
newState.hasMore = totalCount >= this.currentLimit;
|
|
316
|
+
if (data.stage === "mixed") {
|
|
317
|
+
newState.cities = data.cities || [];
|
|
318
|
+
newState.streets = data.streets || [];
|
|
319
|
+
newState.suggestions = [];
|
|
320
|
+
} else {
|
|
321
|
+
newState.suggestions = uniqueSuggestions;
|
|
322
|
+
newState.cities = [];
|
|
323
|
+
newState.streets = [];
|
|
324
|
+
const firstItem = uniqueSuggestions[0];
|
|
325
|
+
const hasFullValue = firstItem && typeof firstItem.value === "object" && firstItem.value !== null && Object.keys(firstItem.value).length > 0;
|
|
326
|
+
if ((data.stage === "final" || hasFullValue) && uniqueSuggestions.length === 1) {
|
|
327
|
+
autoSelect = true;
|
|
328
|
+
autoSelectItem = firstItem;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
newState.isValid = data.stage === "final";
|
|
332
|
+
if (autoSelect && autoSelectItem) {
|
|
333
|
+
newState.query = autoSelectItem.label;
|
|
334
|
+
newState.suggestions = [];
|
|
335
|
+
newState.cities = [];
|
|
336
|
+
newState.streets = [];
|
|
337
|
+
newState.isValid = true;
|
|
338
|
+
newState.hasMore = false;
|
|
339
|
+
this.isSelecting = true;
|
|
340
|
+
this.updateState(newState);
|
|
341
|
+
const val = typeof autoSelectItem.value === "object" ? autoSelectItem.value : autoSelectItem.label;
|
|
342
|
+
this.onSelect(val);
|
|
343
|
+
} else {
|
|
344
|
+
this.updateState(newState);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
updateQueryAndFetch(nextQuery) {
|
|
348
|
+
this.updateState({ query: nextQuery, suggestions: [], cities: [], streets: [] });
|
|
349
|
+
this.updateState({ isLoading: true, isValid: false, hasMore: false });
|
|
350
|
+
this.debouncedFetch(nextQuery);
|
|
351
|
+
}
|
|
352
|
+
replaceLastSegment(fullText, newSegment) {
|
|
353
|
+
const lastCommaIndex = fullText.lastIndexOf(",");
|
|
354
|
+
if (lastCommaIndex === -1) return newSegment;
|
|
355
|
+
return `${fullText.slice(0, lastCommaIndex + 1)} ${newSegment}`.trim();
|
|
356
|
+
}
|
|
357
|
+
getQueryPrefix(q) {
|
|
358
|
+
const lastComma = q.lastIndexOf(",");
|
|
359
|
+
return lastComma === -1 ? "" : q.slice(0, lastComma + 1).trimEnd();
|
|
360
|
+
}
|
|
361
|
+
getCurrentFragment(q) {
|
|
362
|
+
return (q.split(",").slice(-1)[0] ?? "").trim();
|
|
363
|
+
}
|
|
364
|
+
resetState() {
|
|
365
|
+
this.updateState({ ...INITIAL_STATE, query: this.state.query });
|
|
366
|
+
}
|
|
367
|
+
updateState(updates) {
|
|
368
|
+
this.state = { ...this.state, ...updates };
|
|
369
|
+
this.onStateChange(this.state);
|
|
370
|
+
}
|
|
371
|
+
debounce(func, wait) {
|
|
372
|
+
let timeout;
|
|
373
|
+
const debounced = (...args) => {
|
|
374
|
+
if (timeout) clearTimeout(timeout);
|
|
375
|
+
timeout = setTimeout(() => func.apply(this, args), wait);
|
|
376
|
+
};
|
|
377
|
+
debounced.cancel = () => {
|
|
378
|
+
if (timeout) {
|
|
379
|
+
clearTimeout(timeout);
|
|
380
|
+
timeout = void 0;
|
|
381
|
+
}
|
|
382
|
+
};
|
|
383
|
+
return debounced;
|
|
15
384
|
}
|
|
16
|
-
return to;
|
|
17
385
|
};
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
386
|
+
|
|
387
|
+
// ../core/src/default-styles.ts
|
|
388
|
+
var DEFAULT_STYLES = `
|
|
389
|
+
.pro6pp-wrapper {
|
|
390
|
+
position: relative;
|
|
391
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
392
|
+
box-sizing: border-box;
|
|
393
|
+
width: 100%;
|
|
394
|
+
}
|
|
395
|
+
.pro6pp-wrapper * {
|
|
396
|
+
box-sizing: border-box;
|
|
397
|
+
}
|
|
398
|
+
.pro6pp-input {
|
|
399
|
+
width: 100%;
|
|
400
|
+
padding: 10px 12px;
|
|
401
|
+
padding-right: 48px;
|
|
402
|
+
border: 1px solid #e0e0e0;
|
|
403
|
+
border-radius: 4px;
|
|
404
|
+
font-size: 16px;
|
|
405
|
+
line-height: 1.5;
|
|
406
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
407
|
+
}
|
|
408
|
+
.pro6pp-input:focus {
|
|
409
|
+
outline: none;
|
|
410
|
+
border-color: #3b82f6;
|
|
411
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.pro6pp-input-addons {
|
|
415
|
+
position: absolute;
|
|
416
|
+
right: 6px;
|
|
417
|
+
top: 0;
|
|
418
|
+
bottom: 0;
|
|
419
|
+
display: flex;
|
|
420
|
+
align-items: center;
|
|
421
|
+
gap: 2px;
|
|
422
|
+
pointer-events: none;
|
|
423
|
+
}
|
|
424
|
+
.pro6pp-input-addons > * {
|
|
425
|
+
pointer-events: auto;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.pro6pp-clear-button {
|
|
429
|
+
background: none;
|
|
430
|
+
border: none;
|
|
431
|
+
width: 28px;
|
|
432
|
+
height: 28px;
|
|
433
|
+
cursor: pointer;
|
|
434
|
+
color: #a3a3a3;
|
|
435
|
+
display: flex;
|
|
436
|
+
align-items: center;
|
|
437
|
+
justify-content: center;
|
|
438
|
+
border-radius: 50%;
|
|
439
|
+
transition: color 0.2s, background-color 0.2s, transform 0.1s;
|
|
440
|
+
}
|
|
441
|
+
.pro6pp-clear-button:hover {
|
|
442
|
+
color: #1f2937;
|
|
443
|
+
background-color: #f3f4f6;
|
|
444
|
+
}
|
|
445
|
+
.pro6pp-clear-button:active {
|
|
446
|
+
transform: scale(0.92);
|
|
447
|
+
}
|
|
448
|
+
.pro6pp-clear-button svg {
|
|
449
|
+
width: 18px;
|
|
450
|
+
height: 18px;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
.pro6pp-loader {
|
|
454
|
+
width: 18px;
|
|
455
|
+
height: 18px;
|
|
456
|
+
margin: 0 4px;
|
|
457
|
+
border: 2px solid #e0e0e0;
|
|
458
|
+
border-top-color: #6b7280;
|
|
459
|
+
border-radius: 50%;
|
|
460
|
+
animation: pro6pp-spin 0.6s linear infinite;
|
|
461
|
+
flex-shrink: 0;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
.pro6pp-dropdown {
|
|
465
|
+
position: absolute;
|
|
466
|
+
top: 100%;
|
|
467
|
+
left: 0;
|
|
468
|
+
right: 0;
|
|
469
|
+
z-index: 9999;
|
|
470
|
+
margin-top: 4px;
|
|
471
|
+
background: white;
|
|
472
|
+
border: 1px solid #e0e0e0;
|
|
473
|
+
border-radius: 4px;
|
|
474
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
475
|
+
max-height: 300px;
|
|
476
|
+
overflow-y: auto;
|
|
477
|
+
display: flex;
|
|
478
|
+
flex-direction: column;
|
|
479
|
+
}
|
|
480
|
+
.pro6pp-list {
|
|
481
|
+
list-style: none !important;
|
|
482
|
+
padding: 0 !important;
|
|
483
|
+
margin: 0 !important;
|
|
484
|
+
flex-grow: 1;
|
|
485
|
+
}
|
|
486
|
+
.pro6pp-item {
|
|
487
|
+
padding: 12px 16px;
|
|
488
|
+
cursor: pointer;
|
|
489
|
+
display: flex;
|
|
490
|
+
flex-direction: row;
|
|
491
|
+
align-items: center;
|
|
492
|
+
color: #111827;
|
|
493
|
+
font-size: 14px;
|
|
494
|
+
line-height: 1.2;
|
|
495
|
+
white-space: nowrap;
|
|
496
|
+
overflow: hidden;
|
|
497
|
+
}
|
|
498
|
+
.pro6pp-item:hover, .pro6pp-item--active {
|
|
499
|
+
background-color: #f9fafb;
|
|
500
|
+
}
|
|
501
|
+
.pro6pp-item__label {
|
|
502
|
+
font-weight: 500;
|
|
503
|
+
flex-shrink: 0;
|
|
504
|
+
}
|
|
505
|
+
.pro6pp-item__subtitle {
|
|
506
|
+
font-size: 14px;
|
|
507
|
+
color: #6b7280;
|
|
508
|
+
overflow: hidden;
|
|
509
|
+
text-overflow: ellipsis;
|
|
510
|
+
flex-shrink: 1;
|
|
511
|
+
}
|
|
512
|
+
.pro6pp-item__chevron {
|
|
513
|
+
margin-left: auto;
|
|
514
|
+
display: flex;
|
|
515
|
+
align-items: center;
|
|
516
|
+
color: #9ca3af;
|
|
517
|
+
padding-left: 8px;
|
|
518
|
+
}
|
|
519
|
+
.pro6pp-no-results {
|
|
520
|
+
padding: 16px;
|
|
521
|
+
color: #6b7280;
|
|
522
|
+
font-size: 14px;
|
|
523
|
+
text-align: center;
|
|
524
|
+
}
|
|
525
|
+
.pro6pp-load-more {
|
|
526
|
+
width: 100%;
|
|
527
|
+
padding: 10px;
|
|
528
|
+
background: #f9fafb;
|
|
529
|
+
border: none;
|
|
530
|
+
border-top: 1px solid #e0e0e0;
|
|
531
|
+
color: #3b82f6;
|
|
532
|
+
font-size: 13px;
|
|
533
|
+
font-weight: 600;
|
|
534
|
+
cursor: pointer;
|
|
535
|
+
transition: background-color 0.2s;
|
|
536
|
+
flex-shrink: 0;
|
|
537
|
+
}
|
|
538
|
+
.pro6pp-load-more:hover {
|
|
539
|
+
background-color: #f3f4f6;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
@keyframes pro6pp-spin {
|
|
543
|
+
to { transform: rotate(360deg); }
|
|
544
|
+
}
|
|
545
|
+
`;
|
|
546
|
+
|
|
547
|
+
// src/index.tsx
|
|
28
548
|
function useInfer(config) {
|
|
29
|
-
const [state, setState] =
|
|
30
|
-
const
|
|
31
|
-
|
|
549
|
+
const [state, setState] = useState(INITIAL_STATE);
|
|
550
|
+
const callbacksRef = useRef({
|
|
551
|
+
onStateChange: config.onStateChange,
|
|
552
|
+
onSelect: config.onSelect
|
|
553
|
+
});
|
|
554
|
+
useEffect(() => {
|
|
555
|
+
callbacksRef.current = {
|
|
556
|
+
onStateChange: config.onStateChange,
|
|
557
|
+
onSelect: config.onSelect
|
|
558
|
+
};
|
|
559
|
+
}, [config.onStateChange, config.onSelect]);
|
|
560
|
+
const core = useMemo(() => {
|
|
561
|
+
return new InferCore({
|
|
32
562
|
...config,
|
|
33
|
-
onStateChange: (newState) =>
|
|
563
|
+
onStateChange: (newState) => {
|
|
564
|
+
setState({ ...newState });
|
|
565
|
+
callbacksRef.current.onStateChange?.(newState);
|
|
566
|
+
},
|
|
567
|
+
onSelect: (selection) => {
|
|
568
|
+
callbacksRef.current.onSelect?.(selection);
|
|
569
|
+
}
|
|
34
570
|
});
|
|
35
|
-
}, [
|
|
571
|
+
}, [
|
|
572
|
+
config.country,
|
|
573
|
+
config.authKey,
|
|
574
|
+
config.apiUrl,
|
|
575
|
+
config.fetcher,
|
|
576
|
+
config.limit,
|
|
577
|
+
config.debounceMs,
|
|
578
|
+
config.maxRetries
|
|
579
|
+
]);
|
|
36
580
|
return {
|
|
581
|
+
/** The current UI state (suggestions, loading status, query, etc.). */
|
|
37
582
|
state,
|
|
583
|
+
/** The raw InferCore instance for manual control. */
|
|
38
584
|
core,
|
|
585
|
+
/** Pre-configured event handlers to spread onto an <input /> element. */
|
|
39
586
|
inputProps: {
|
|
40
587
|
value: state.query,
|
|
41
588
|
onChange: (e) => core.handleInput(e.target.value),
|
|
42
589
|
onKeyDown: (e) => core.handleKeyDown(e)
|
|
43
590
|
},
|
|
44
|
-
|
|
591
|
+
/** Function to manually select a specific suggestion. */
|
|
592
|
+
selectItem: (item) => core.selectItem(item),
|
|
593
|
+
/** Function to load more results. */
|
|
594
|
+
loadMore: () => core.loadMore()
|
|
45
595
|
};
|
|
46
596
|
}
|
|
47
|
-
|
|
48
|
-
|
|
597
|
+
var Pro6PPInfer = forwardRef(
|
|
598
|
+
({
|
|
599
|
+
className,
|
|
600
|
+
style,
|
|
601
|
+
inputProps,
|
|
602
|
+
placeholder = "Start typing an address...",
|
|
603
|
+
renderItem,
|
|
604
|
+
disableDefaultStyles = false,
|
|
605
|
+
noResultsText = "No results found",
|
|
606
|
+
loadMoreText = "Show more results...",
|
|
607
|
+
renderNoResults,
|
|
608
|
+
showClearButton = true,
|
|
609
|
+
...config
|
|
610
|
+
}, ref) => {
|
|
611
|
+
const { state, selectItem, loadMore, inputProps: coreInputProps, core } = useInfer(config);
|
|
612
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
613
|
+
const internalInputRef = useRef(null);
|
|
614
|
+
const wrapperRef = useRef(null);
|
|
615
|
+
useImperativeHandle(ref, () => internalInputRef.current);
|
|
616
|
+
useEffect(() => {
|
|
617
|
+
if (disableDefaultStyles) return;
|
|
618
|
+
const styleId = "pro6pp-styles";
|
|
619
|
+
if (!document.getElementById(styleId)) {
|
|
620
|
+
const styleEl = document.createElement("style");
|
|
621
|
+
styleEl.id = styleId;
|
|
622
|
+
styleEl.textContent = DEFAULT_STYLES;
|
|
623
|
+
document.head.appendChild(styleEl);
|
|
624
|
+
}
|
|
625
|
+
}, [disableDefaultStyles]);
|
|
626
|
+
useEffect(() => {
|
|
627
|
+
const handleClickOutside = (event) => {
|
|
628
|
+
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
|
|
629
|
+
setIsOpen(false);
|
|
630
|
+
}
|
|
631
|
+
};
|
|
632
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
633
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
634
|
+
}, []);
|
|
635
|
+
const items = useMemo(() => {
|
|
636
|
+
return [
|
|
637
|
+
...state.cities.map((c) => ({ ...c, type: "city" })),
|
|
638
|
+
...state.streets.map((s) => ({ ...s, type: "street" })),
|
|
639
|
+
...state.suggestions.map((s) => ({ ...s, type: "suggestion" }))
|
|
640
|
+
];
|
|
641
|
+
}, [state.cities, state.streets, state.suggestions]);
|
|
642
|
+
const handleSelect = (item) => {
|
|
643
|
+
const isFinal = selectItem(item);
|
|
644
|
+
if (!isFinal) {
|
|
645
|
+
setTimeout(() => internalInputRef.current?.focus(), 0);
|
|
646
|
+
} else {
|
|
647
|
+
setIsOpen(false);
|
|
648
|
+
}
|
|
649
|
+
};
|
|
650
|
+
const handleClear = () => {
|
|
651
|
+
core.handleInput("");
|
|
652
|
+
internalInputRef.current?.focus();
|
|
653
|
+
};
|
|
654
|
+
const hasResults = items.length > 0;
|
|
655
|
+
const showNoResults = !state.isLoading && !state.isError && state.query.length > 0 && !hasResults && !state.isValid;
|
|
656
|
+
const showDropdown = isOpen && (hasResults || state.isLoading || showNoResults);
|
|
657
|
+
return /* @__PURE__ */ React.createElement("div", { ref: wrapperRef, className: `pro6pp-wrapper ${className || ""}`, style }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(
|
|
658
|
+
"input",
|
|
659
|
+
{
|
|
660
|
+
ref: internalInputRef,
|
|
661
|
+
type: "text",
|
|
662
|
+
className: "pro6pp-input",
|
|
663
|
+
placeholder,
|
|
664
|
+
autoComplete: "off",
|
|
665
|
+
...inputProps,
|
|
666
|
+
...coreInputProps,
|
|
667
|
+
onFocus: (e) => {
|
|
668
|
+
setIsOpen(true);
|
|
669
|
+
inputProps?.onFocus?.(e);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
), /* @__PURE__ */ React.createElement("div", { className: "pro6pp-input-addons" }, state.isLoading && /* @__PURE__ */ React.createElement("div", { className: "pro6pp-loader" }), showClearButton && state.query.length > 0 && /* @__PURE__ */ React.createElement(
|
|
673
|
+
"button",
|
|
674
|
+
{
|
|
675
|
+
type: "button",
|
|
676
|
+
className: "pro6pp-clear-button",
|
|
677
|
+
onClick: handleClear,
|
|
678
|
+
"aria-label": "Clear input"
|
|
679
|
+
},
|
|
680
|
+
/* @__PURE__ */ React.createElement(
|
|
681
|
+
"svg",
|
|
682
|
+
{
|
|
683
|
+
width: "14",
|
|
684
|
+
height: "14",
|
|
685
|
+
viewBox: "0 0 24 24",
|
|
686
|
+
fill: "none",
|
|
687
|
+
stroke: "currentColor",
|
|
688
|
+
strokeWidth: "2",
|
|
689
|
+
strokeLinecap: "round",
|
|
690
|
+
strokeLinejoin: "round"
|
|
691
|
+
},
|
|
692
|
+
/* @__PURE__ */ React.createElement("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
|
|
693
|
+
/* @__PURE__ */ React.createElement("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
|
|
694
|
+
)
|
|
695
|
+
))), showDropdown && /* @__PURE__ */ React.createElement(
|
|
696
|
+
"div",
|
|
697
|
+
{
|
|
698
|
+
className: "pro6pp-dropdown",
|
|
699
|
+
onWheel: (e) => e.stopPropagation(),
|
|
700
|
+
onMouseDown: (e) => e.stopPropagation()
|
|
701
|
+
},
|
|
702
|
+
/* @__PURE__ */ React.createElement("ul", { className: "pro6pp-list", role: "listbox" }, hasResults ? items.map((item, index) => {
|
|
703
|
+
const isActive = index === state.selectedSuggestionIndex;
|
|
704
|
+
const secondaryText = item.subtitle || (item.count !== void 0 ? item.count : "");
|
|
705
|
+
const showChevron = item.value === void 0 || item.value === null;
|
|
706
|
+
return /* @__PURE__ */ React.createElement(
|
|
707
|
+
"li",
|
|
708
|
+
{
|
|
709
|
+
key: `${item.label}-${index}`,
|
|
710
|
+
role: "option",
|
|
711
|
+
"aria-selected": isActive,
|
|
712
|
+
className: `pro6pp-item ${isActive ? "pro6pp-item--active" : ""}`,
|
|
713
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
714
|
+
onClick: () => handleSelect(item)
|
|
715
|
+
},
|
|
716
|
+
renderItem ? renderItem(item, isActive) : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { className: "pro6pp-item__label" }, item.label), secondaryText && /* @__PURE__ */ React.createElement("span", { className: "pro6pp-item__subtitle" }, ", ", secondaryText), showChevron && /* @__PURE__ */ React.createElement("div", { className: "pro6pp-item__chevron" }, /* @__PURE__ */ React.createElement(
|
|
717
|
+
"svg",
|
|
718
|
+
{
|
|
719
|
+
width: "16",
|
|
720
|
+
height: "16",
|
|
721
|
+
viewBox: "0 0 24 24",
|
|
722
|
+
fill: "none",
|
|
723
|
+
stroke: "currentColor",
|
|
724
|
+
strokeWidth: "2",
|
|
725
|
+
strokeLinecap: "round",
|
|
726
|
+
strokeLinejoin: "round"
|
|
727
|
+
},
|
|
728
|
+
/* @__PURE__ */ React.createElement("polyline", { points: "9 18 15 12 9 6" })
|
|
729
|
+
)))
|
|
730
|
+
);
|
|
731
|
+
}) : state.isLoading ? /* @__PURE__ */ React.createElement("li", { className: "pro6pp-no-results" }, "Loading suggestions...") : /* @__PURE__ */ React.createElement("li", { className: "pro6pp-no-results" }, renderNoResults ? renderNoResults(state) : noResultsText)),
|
|
732
|
+
state.hasMore && /* @__PURE__ */ React.createElement(
|
|
733
|
+
"button",
|
|
734
|
+
{
|
|
735
|
+
type: "button",
|
|
736
|
+
className: "pro6pp-load-more",
|
|
737
|
+
onClick: (e) => {
|
|
738
|
+
e.preventDefault();
|
|
739
|
+
loadMore();
|
|
740
|
+
}
|
|
741
|
+
},
|
|
742
|
+
loadMoreText
|
|
743
|
+
)
|
|
744
|
+
));
|
|
745
|
+
}
|
|
746
|
+
);
|
|
747
|
+
export {
|
|
748
|
+
Pro6PPInfer,
|
|
49
749
|
useInfer
|
|
50
|
-
}
|
|
750
|
+
};
|