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