@pro6pp/infer-react 0.0.2-beta.6 → 0.0.2-beta.8
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 +46 -7
- package/dist/index.cjs +344 -27
- package/dist/index.d.cts +41 -9
- package/dist/index.d.ts +41 -9
- package/dist/index.js +333 -27
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -11,13 +11,49 @@ npm install @pro6pp/infer-react
|
|
|
11
11
|
|
|
12
12
|
## Usage
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
The `Pro6PPInfer` component provides a styled address autocomplete input.
|
|
15
15
|
|
|
16
16
|
```tsx
|
|
17
17
|
import React from 'react';
|
|
18
|
-
import {
|
|
18
|
+
import { Pro6PPInfer } from '@pro6pp/infer-react';
|
|
19
19
|
|
|
20
20
|
const AddressForm = () => {
|
|
21
|
+
return (
|
|
22
|
+
<div className="form-group">
|
|
23
|
+
<label>Search Address</label>
|
|
24
|
+
<Pro6PPInfer
|
|
25
|
+
authKey="YOUR_AUTH_KEY"
|
|
26
|
+
country="NL"
|
|
27
|
+
onSelect={(selection) => console.log('Selected:', selection)}
|
|
28
|
+
placeholder="Type a Dutch address..."
|
|
29
|
+
/>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
You can customize the appearance of the component via the following props:
|
|
36
|
+
|
|
37
|
+
| Prop | Description |
|
|
38
|
+
| :--------------------- | :---------------------------------------------------------------------------------------- |
|
|
39
|
+
| `className` | Optional CSS class name for the wrapper `div`. |
|
|
40
|
+
| `disableDefaultStyles` | If `true`, prevents the automatic injection of the default CSS theme. |
|
|
41
|
+
| `placeholder` | Custom placeholder text for the input field. |
|
|
42
|
+
| `noResultsText` | The text to display when no suggestions are found. |
|
|
43
|
+
| `renderItem` | A custom render function for suggestion items, receiving the `item` and `isActive` state. |
|
|
44
|
+
| `renderNoResults` | A custom render function for the empty state, receiving the current `state`. |
|
|
45
|
+
| `debounceMs` | Delay in ms before API search. Defaults to `150` (min `50`). |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
Alternatively, you can use the headless `useInfer` hook.
|
|
50
|
+
This hook handles all the logic (state, API calls, debouncing, keyboard navigation), but allows you to build your own custom UI.
|
|
51
|
+
|
|
52
|
+
```tsx
|
|
53
|
+
import React from 'react';
|
|
54
|
+
import { useInfer } from '@pro6pp/infer-react';
|
|
55
|
+
|
|
56
|
+
const CustomAddressForm = () => {
|
|
21
57
|
const { state, inputProps, selectItem } = useInfer({
|
|
22
58
|
authKey: 'YOUR_AUTH_KEY',
|
|
23
59
|
country: 'NL',
|
|
@@ -28,19 +64,22 @@ const AddressForm = () => {
|
|
|
28
64
|
<div className="address-autocomplete">
|
|
29
65
|
<label>Address</label>
|
|
30
66
|
|
|
31
|
-
|
|
67
|
+
{/* inputProps contains value, onChange, and onKeyDown */}
|
|
68
|
+
<input {...inputProps} placeholder="Type an address..." className="my-input-class" />
|
|
69
|
+
|
|
70
|
+
{state.isLoading && <div className="spinner">Loading...</div>}
|
|
32
71
|
|
|
33
72
|
{/* render the dropdown */}
|
|
34
73
|
{(state.suggestions.length > 0 || state.cities.length > 0) && (
|
|
35
74
|
<ul className="my-dropdown-class">
|
|
36
|
-
{/* render cities
|
|
75
|
+
{/* render cities */}
|
|
37
76
|
{state.cities.map((city, i) => (
|
|
38
77
|
<li key={`city-${i}`} onClick={() => selectItem(city)}>
|
|
39
78
|
<strong>{city.label}</strong> (City)
|
|
40
79
|
</li>
|
|
41
80
|
))}
|
|
42
81
|
|
|
43
|
-
{/* render streets
|
|
82
|
+
{/* render streets */}
|
|
44
83
|
{state.streets.map((street, i) => (
|
|
45
84
|
<li key={`street-${i}`} onClick={() => selectItem(street)}>
|
|
46
85
|
<strong>{street.label}</strong> (Street)
|
|
@@ -49,14 +88,14 @@ const AddressForm = () => {
|
|
|
49
88
|
|
|
50
89
|
{/* render general suggestions */}
|
|
51
90
|
{state.suggestions.map((item, i) => (
|
|
52
|
-
<li key={`
|
|
91
|
+
<li key={`suggestion-${i}`} onClick={() => selectItem(item)}>
|
|
53
92
|
{item.label}
|
|
54
93
|
</li>
|
|
55
94
|
))}
|
|
56
95
|
</ul>
|
|
57
96
|
)}
|
|
58
97
|
|
|
59
|
-
{state.isValid && <p>Valid address selected
|
|
98
|
+
{state.isValid && <p>Valid address selected.</p>}
|
|
60
99
|
</div>
|
|
61
100
|
);
|
|
62
101
|
};
|
package/dist/index.cjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
2
3
|
var __defProp = Object.defineProperty;
|
|
3
4
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
5
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
8
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
7
9
|
var __export = (target, all) => {
|
|
@@ -16,22 +18,32 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
18
|
}
|
|
17
19
|
return to;
|
|
18
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
|
+
));
|
|
19
29
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
30
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
21
31
|
|
|
22
|
-
// src/index.
|
|
32
|
+
// src/index.tsx
|
|
23
33
|
var index_exports = {};
|
|
24
34
|
__export(index_exports, {
|
|
35
|
+
Pro6PPInfer: () => Pro6PPInfer,
|
|
25
36
|
useInfer: () => useInfer
|
|
26
37
|
});
|
|
27
38
|
module.exports = __toCommonJS(index_exports);
|
|
28
|
-
var import_react = require("react");
|
|
39
|
+
var import_react = __toESM(require("react"), 1);
|
|
29
40
|
|
|
30
41
|
// ../core/src/core.ts
|
|
31
42
|
var DEFAULTS = {
|
|
32
43
|
API_URL: "https://api.pro6pp.nl/v2",
|
|
33
44
|
LIMIT: 1e3,
|
|
34
|
-
DEBOUNCE_MS:
|
|
45
|
+
DEBOUNCE_MS: 150,
|
|
46
|
+
MIN_DEBOUNCE_MS: 50
|
|
35
47
|
};
|
|
36
48
|
var PATTERNS = {
|
|
37
49
|
DIGITS_1_3: /^[0-9]{1,3}$/
|
|
@@ -48,6 +60,10 @@ var INITIAL_STATE = {
|
|
|
48
60
|
selectedSuggestionIndex: -1
|
|
49
61
|
};
|
|
50
62
|
var InferCore = class {
|
|
63
|
+
/**
|
|
64
|
+
* Initializes a new instance of the Infer engine.
|
|
65
|
+
* @param config The configuration object including API keys and callbacks.
|
|
66
|
+
*/
|
|
51
67
|
constructor(config) {
|
|
52
68
|
__publicField(this, "country");
|
|
53
69
|
__publicField(this, "authKey");
|
|
@@ -56,9 +72,14 @@ var InferCore = class {
|
|
|
56
72
|
__publicField(this, "fetcher");
|
|
57
73
|
__publicField(this, "onStateChange");
|
|
58
74
|
__publicField(this, "onSelect");
|
|
75
|
+
/**
|
|
76
|
+
* The current read-only state of the engine.
|
|
77
|
+
* Use `onStateChange` to react to updates.
|
|
78
|
+
*/
|
|
59
79
|
__publicField(this, "state");
|
|
60
80
|
__publicField(this, "abortController", null);
|
|
61
81
|
__publicField(this, "debouncedFetch");
|
|
82
|
+
__publicField(this, "isSelecting", false);
|
|
62
83
|
this.country = config.country;
|
|
63
84
|
this.authKey = config.authKey;
|
|
64
85
|
this.apiUrl = config.apiUrl || DEFAULTS.API_URL;
|
|
@@ -69,23 +90,40 @@ var InferCore = class {
|
|
|
69
90
|
this.onSelect = config.onSelect || (() => {
|
|
70
91
|
});
|
|
71
92
|
this.state = { ...INITIAL_STATE };
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
);
|
|
93
|
+
const configDebounce = config.debounceMs !== void 0 ? config.debounceMs : DEFAULTS.DEBOUNCE_MS;
|
|
94
|
+
const debounceTime = Math.max(configDebounce, DEFAULTS.MIN_DEBOUNCE_MS);
|
|
95
|
+
this.debouncedFetch = this.debounce((val) => this.executeFetch(val), debounceTime);
|
|
76
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Processes new text input from the user.
|
|
99
|
+
* Triggers a debounced API request and updates the internal state.
|
|
100
|
+
* @param value The raw string from the input field.
|
|
101
|
+
*/
|
|
77
102
|
handleInput(value) {
|
|
103
|
+
if (this.isSelecting) {
|
|
104
|
+
this.isSelecting = false;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
const isEditingFinal = this.state.stage === "final" && value !== this.state.query;
|
|
78
108
|
this.updateState({
|
|
79
109
|
query: value,
|
|
80
110
|
isValid: false,
|
|
81
111
|
isLoading: !!value.trim(),
|
|
82
112
|
selectedSuggestionIndex: -1
|
|
83
113
|
});
|
|
84
|
-
if (
|
|
114
|
+
if (isEditingFinal) {
|
|
85
115
|
this.onSelect(null);
|
|
86
116
|
}
|
|
87
117
|
this.debouncedFetch(value);
|
|
88
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Handles keyboard events for the input field.
|
|
121
|
+
* Supports:
|
|
122
|
+
* - `ArrowUp`/`ArrowDown`: Navigate through the suggestion list.
|
|
123
|
+
* - `Enter`: Select the currently highlighted suggestion.
|
|
124
|
+
* - `Space`: Automatically inserts a comma if a numeric house number is detected.
|
|
125
|
+
* @param event The keyboard event from the input element.
|
|
126
|
+
*/
|
|
89
127
|
handleKeyDown(event) {
|
|
90
128
|
const target = event.target;
|
|
91
129
|
if (!target) return;
|
|
@@ -127,15 +165,38 @@ var InferCore = class {
|
|
|
127
165
|
this.updateQueryAndFetch(next);
|
|
128
166
|
}
|
|
129
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Manually selects a suggestion or a string value.
|
|
170
|
+
* This is typically called when a user clicks a suggestion in the UI.
|
|
171
|
+
* @param item The suggestion object or string to select.
|
|
172
|
+
*/
|
|
130
173
|
selectItem(item) {
|
|
174
|
+
this.debouncedFetch.cancel();
|
|
175
|
+
if (this.abortController) {
|
|
176
|
+
this.abortController.abort();
|
|
177
|
+
}
|
|
131
178
|
const label = typeof item === "string" ? item : item.label;
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
179
|
+
let logicValue = label;
|
|
180
|
+
if (typeof item !== "string" && typeof item.value === "string") {
|
|
181
|
+
logicValue = item.value;
|
|
182
|
+
}
|
|
183
|
+
const valueObj = typeof item !== "string" && typeof item.value === "object" ? item.value : void 0;
|
|
184
|
+
const isFullResult = !!valueObj && Object.keys(valueObj).length > 0;
|
|
185
|
+
this.isSelecting = true;
|
|
186
|
+
if (this.state.stage === "final" || isFullResult) {
|
|
187
|
+
let finalQuery = label;
|
|
188
|
+
if (valueObj && Object.keys(valueObj).length > 0) {
|
|
189
|
+
const { street, street_number, house_number, city } = valueObj;
|
|
190
|
+
const number = street_number || house_number;
|
|
191
|
+
if (street && number && city) {
|
|
192
|
+
finalQuery = `${street} ${number}, ${city}`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
this.finishSelection(finalQuery, valueObj);
|
|
136
196
|
return;
|
|
137
197
|
}
|
|
138
|
-
|
|
198
|
+
const subtitle = typeof item !== "string" ? item.subtitle : null;
|
|
199
|
+
this.processSelection(logicValue, subtitle);
|
|
139
200
|
}
|
|
140
201
|
shouldAutoInsertComma(currentVal) {
|
|
141
202
|
const isStartOfSegmentAndNumeric = !currentVal.includes(",") && PATTERNS.DIGITS_1_3.test(currentVal.trim());
|
|
@@ -147,33 +208,43 @@ var InferCore = class {
|
|
|
147
208
|
return false;
|
|
148
209
|
}
|
|
149
210
|
finishSelection(label, value) {
|
|
150
|
-
this.updateState({
|
|
211
|
+
this.updateState({
|
|
212
|
+
query: label,
|
|
213
|
+
suggestions: [],
|
|
214
|
+
cities: [],
|
|
215
|
+
streets: [],
|
|
216
|
+
isValid: true,
|
|
217
|
+
stage: "final"
|
|
218
|
+
});
|
|
151
219
|
this.onSelect(value || label);
|
|
220
|
+
setTimeout(() => {
|
|
221
|
+
this.isSelecting = false;
|
|
222
|
+
}, 0);
|
|
152
223
|
}
|
|
153
|
-
processSelection(
|
|
224
|
+
processSelection(text, subtitle) {
|
|
154
225
|
const { stage, query } = this.state;
|
|
155
226
|
let nextQuery = query;
|
|
156
227
|
const isContextualSelection = subtitle && (stage === "city" || stage === "street" || stage === "mixed");
|
|
157
228
|
if (isContextualSelection) {
|
|
158
229
|
if (stage === "city") {
|
|
159
|
-
nextQuery = `${subtitle}, ${
|
|
230
|
+
nextQuery = `${subtitle}, ${text}, `;
|
|
160
231
|
} else {
|
|
161
232
|
const prefix = this.getQueryPrefix(query);
|
|
162
|
-
nextQuery = prefix ? `${prefix} ${
|
|
233
|
+
nextQuery = prefix ? `${prefix} ${text}, ${subtitle}, ` : `${text}, ${subtitle}, `;
|
|
163
234
|
}
|
|
164
235
|
this.updateQueryAndFetch(nextQuery);
|
|
165
236
|
return;
|
|
166
237
|
}
|
|
167
238
|
if (stage === "direct" || stage === "addition") {
|
|
168
|
-
this.finishSelection(
|
|
239
|
+
this.finishSelection(text);
|
|
169
240
|
return;
|
|
170
241
|
}
|
|
171
242
|
const hasComma = query.includes(",");
|
|
172
243
|
const isFirstSegment = !hasComma && (stage === "city" || stage === "street" || stage === "house_number_first");
|
|
173
244
|
if (isFirstSegment) {
|
|
174
|
-
nextQuery = `${
|
|
245
|
+
nextQuery = `${text}, `;
|
|
175
246
|
} else {
|
|
176
|
-
nextQuery = this.replaceLastSegment(query,
|
|
247
|
+
nextQuery = this.replaceLastSegment(query, text);
|
|
177
248
|
if (stage !== "house_number") {
|
|
178
249
|
nextQuery += ", ";
|
|
179
250
|
}
|
|
@@ -211,21 +282,52 @@ var InferCore = class {
|
|
|
211
282
|
stage: data.stage,
|
|
212
283
|
isLoading: false
|
|
213
284
|
};
|
|
285
|
+
let autoSelect = false;
|
|
286
|
+
let autoSelectItem = null;
|
|
287
|
+
const rawSuggestions = data.suggestions || [];
|
|
288
|
+
const uniqueSuggestions = [];
|
|
289
|
+
const seen = /* @__PURE__ */ new Set();
|
|
290
|
+
for (const item of rawSuggestions) {
|
|
291
|
+
const key = `${item.label}|${item.subtitle || ""}|${JSON.stringify(item.value || {})}`;
|
|
292
|
+
if (!seen.has(key)) {
|
|
293
|
+
seen.add(key);
|
|
294
|
+
uniqueSuggestions.push(item);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
214
297
|
if (data.stage === "mixed") {
|
|
215
298
|
newState.cities = data.cities || [];
|
|
216
299
|
newState.streets = data.streets || [];
|
|
217
300
|
newState.suggestions = [];
|
|
218
301
|
} else {
|
|
219
|
-
newState.suggestions =
|
|
302
|
+
newState.suggestions = uniqueSuggestions;
|
|
220
303
|
newState.cities = [];
|
|
221
304
|
newState.streets = [];
|
|
305
|
+
if (data.stage === "final" && uniqueSuggestions.length === 1) {
|
|
306
|
+
autoSelect = true;
|
|
307
|
+
autoSelectItem = uniqueSuggestions[0];
|
|
308
|
+
}
|
|
222
309
|
}
|
|
223
310
|
newState.isValid = data.stage === "final";
|
|
224
|
-
|
|
311
|
+
if (autoSelect && autoSelectItem) {
|
|
312
|
+
newState.query = autoSelectItem.label;
|
|
313
|
+
newState.suggestions = [];
|
|
314
|
+
newState.cities = [];
|
|
315
|
+
newState.streets = [];
|
|
316
|
+
newState.isValid = true;
|
|
317
|
+
this.updateState(newState);
|
|
318
|
+
const val = typeof autoSelectItem.value === "object" ? autoSelectItem.value : autoSelectItem.label;
|
|
319
|
+
this.onSelect(val);
|
|
320
|
+
} else {
|
|
321
|
+
this.updateState(newState);
|
|
322
|
+
}
|
|
225
323
|
}
|
|
226
324
|
updateQueryAndFetch(nextQuery) {
|
|
227
325
|
this.updateState({ query: nextQuery, suggestions: [], cities: [], streets: [] });
|
|
228
|
-
this.
|
|
326
|
+
this.updateState({ isLoading: true, isValid: false });
|
|
327
|
+
this.debouncedFetch(nextQuery);
|
|
328
|
+
setTimeout(() => {
|
|
329
|
+
this.isSelecting = false;
|
|
330
|
+
}, 0);
|
|
229
331
|
}
|
|
230
332
|
replaceLastSegment(fullText, newSegment) {
|
|
231
333
|
const lastCommaIndex = fullText.lastIndexOf(",");
|
|
@@ -248,34 +350,249 @@ var InferCore = class {
|
|
|
248
350
|
}
|
|
249
351
|
debounce(func, wait) {
|
|
250
352
|
let timeout;
|
|
251
|
-
|
|
353
|
+
const debounced = (...args) => {
|
|
252
354
|
if (timeout) clearTimeout(timeout);
|
|
253
355
|
timeout = setTimeout(() => func.apply(this, args), wait);
|
|
254
356
|
};
|
|
357
|
+
debounced.cancel = () => {
|
|
358
|
+
if (timeout) {
|
|
359
|
+
clearTimeout(timeout);
|
|
360
|
+
timeout = void 0;
|
|
361
|
+
}
|
|
362
|
+
};
|
|
363
|
+
return debounced;
|
|
255
364
|
}
|
|
256
365
|
};
|
|
257
366
|
|
|
258
|
-
// src/
|
|
367
|
+
// ../core/src/default-styles.ts
|
|
368
|
+
var DEFAULT_STYLES = `
|
|
369
|
+
.pro6pp-wrapper {
|
|
370
|
+
position: relative;
|
|
371
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
372
|
+
box-sizing: border-box;
|
|
373
|
+
width: 100%;
|
|
374
|
+
}
|
|
375
|
+
.pro6pp-wrapper * {
|
|
376
|
+
box-sizing: border-box;
|
|
377
|
+
}
|
|
378
|
+
.pro6pp-input {
|
|
379
|
+
width: 100%;
|
|
380
|
+
padding: 10px 12px;
|
|
381
|
+
border: 1px solid #e0e0e0;
|
|
382
|
+
border-radius: 4px;
|
|
383
|
+
font-size: 16px;
|
|
384
|
+
line-height: 1.5;
|
|
385
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
386
|
+
}
|
|
387
|
+
.pro6pp-input:focus {
|
|
388
|
+
outline: none;
|
|
389
|
+
border-color: #3b82f6;
|
|
390
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
391
|
+
}
|
|
392
|
+
.pro6pp-dropdown {
|
|
393
|
+
position: absolute;
|
|
394
|
+
top: 100%;
|
|
395
|
+
left: 0;
|
|
396
|
+
right: 0;
|
|
397
|
+
z-index: 9999;
|
|
398
|
+
margin-top: 4px;
|
|
399
|
+
background: white;
|
|
400
|
+
border: 1px solid #e0e0e0;
|
|
401
|
+
border-radius: 4px;
|
|
402
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
403
|
+
max-height: 300px;
|
|
404
|
+
overflow-y: auto;
|
|
405
|
+
list-style: none !important;
|
|
406
|
+
padding: 0 !important;
|
|
407
|
+
margin: 0 !important;
|
|
408
|
+
}
|
|
409
|
+
.pro6pp-item {
|
|
410
|
+
padding: 10px 16px;
|
|
411
|
+
cursor: pointer;
|
|
412
|
+
display: flex;
|
|
413
|
+
flex-direction: row;
|
|
414
|
+
align-items: center;
|
|
415
|
+
color: #000000;
|
|
416
|
+
font-size: 14px;
|
|
417
|
+
line-height: 1.2;
|
|
418
|
+
white-space: nowrap;
|
|
419
|
+
overflow: hidden;
|
|
420
|
+
}
|
|
421
|
+
.pro6pp-item:hover, .pro6pp-item--active {
|
|
422
|
+
background-color: #f5f5f5;
|
|
423
|
+
}
|
|
424
|
+
.pro6pp-item__label {
|
|
425
|
+
font-weight: 500;
|
|
426
|
+
flex-shrink: 0;
|
|
427
|
+
}
|
|
428
|
+
.pro6pp-item__subtitle {
|
|
429
|
+
font-size: 14px;
|
|
430
|
+
color: #404040;
|
|
431
|
+
overflow: hidden;
|
|
432
|
+
text-overflow: ellipsis;
|
|
433
|
+
flex-shrink: 1;
|
|
434
|
+
}
|
|
435
|
+
.pro6pp-item__chevron {
|
|
436
|
+
margin-left: auto;
|
|
437
|
+
display: flex;
|
|
438
|
+
align-items: center;
|
|
439
|
+
color: #a3a3a3;
|
|
440
|
+
padding-left: 8px;
|
|
441
|
+
}
|
|
442
|
+
.pro6pp-no-results {
|
|
443
|
+
padding: 12px;
|
|
444
|
+
color: #555555;
|
|
445
|
+
font-size: 14px;
|
|
446
|
+
text-align: center;
|
|
447
|
+
user-select: none;
|
|
448
|
+
pointer-events: none;
|
|
449
|
+
}
|
|
450
|
+
.pro6pp-loader {
|
|
451
|
+
position: absolute;
|
|
452
|
+
right: 12px;
|
|
453
|
+
top: 50%;
|
|
454
|
+
transform: translateY(-50%);
|
|
455
|
+
width: 16px;
|
|
456
|
+
height: 16px;
|
|
457
|
+
border: 2px solid #e0e0e0;
|
|
458
|
+
border-top-color: #404040;
|
|
459
|
+
border-radius: 50%;
|
|
460
|
+
animation: pro6pp-spin 0.6s linear infinite;
|
|
461
|
+
pointer-events: none;
|
|
462
|
+
}
|
|
463
|
+
@keyframes pro6pp-spin {
|
|
464
|
+
to { transform: translateY(-50%) rotate(360deg); }
|
|
465
|
+
}
|
|
466
|
+
`;
|
|
467
|
+
|
|
468
|
+
// src/index.tsx
|
|
259
469
|
function useInfer(config) {
|
|
260
470
|
const [state, setState] = (0, import_react.useState)(INITIAL_STATE);
|
|
261
471
|
const core = (0, import_react.useMemo)(() => {
|
|
262
472
|
return new InferCore({
|
|
263
473
|
...config,
|
|
264
|
-
onStateChange: (newState) =>
|
|
474
|
+
onStateChange: (newState) => {
|
|
475
|
+
setState({ ...newState });
|
|
476
|
+
if (config.onStateChange) {
|
|
477
|
+
config.onStateChange(newState);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
265
480
|
});
|
|
266
|
-
}, [config.country, config.authKey, config.limit]);
|
|
481
|
+
}, [config.country, config.authKey, config.limit, config.debounceMs]);
|
|
267
482
|
return {
|
|
483
|
+
/** The current UI state (suggestions, loading status, query, etc.). */
|
|
268
484
|
state,
|
|
485
|
+
/** The raw InferCore instance for manual control. */
|
|
269
486
|
core,
|
|
487
|
+
/** Pre-configured event handlers to spread onto an <input /> element. */
|
|
270
488
|
inputProps: {
|
|
271
489
|
value: state.query,
|
|
272
490
|
onChange: (e) => core.handleInput(e.target.value),
|
|
273
491
|
onKeyDown: (e) => core.handleKeyDown(e)
|
|
274
492
|
},
|
|
493
|
+
/** Function to manually select a specific suggestion. */
|
|
275
494
|
selectItem: (item) => core.selectItem(item)
|
|
276
495
|
};
|
|
277
496
|
}
|
|
497
|
+
var Pro6PPInfer = ({
|
|
498
|
+
className,
|
|
499
|
+
style,
|
|
500
|
+
inputProps,
|
|
501
|
+
placeholder = "Start typing an address...",
|
|
502
|
+
renderItem,
|
|
503
|
+
disableDefaultStyles = false,
|
|
504
|
+
noResultsText = "No results found",
|
|
505
|
+
renderNoResults,
|
|
506
|
+
...config
|
|
507
|
+
}) => {
|
|
508
|
+
const { state, selectItem, inputProps: coreInputProps } = useInfer(config);
|
|
509
|
+
const [isOpen, setIsOpen] = (0, import_react.useState)(false);
|
|
510
|
+
const inputRef = (0, import_react.useRef)(null);
|
|
511
|
+
const wrapperRef = (0, import_react.useRef)(null);
|
|
512
|
+
(0, import_react.useEffect)(() => {
|
|
513
|
+
if (disableDefaultStyles) return;
|
|
514
|
+
const styleId = "pro6pp-styles";
|
|
515
|
+
if (!document.getElementById(styleId)) {
|
|
516
|
+
const styleEl = document.createElement("style");
|
|
517
|
+
styleEl.id = styleId;
|
|
518
|
+
styleEl.textContent = DEFAULT_STYLES;
|
|
519
|
+
document.head.appendChild(styleEl);
|
|
520
|
+
}
|
|
521
|
+
}, [disableDefaultStyles]);
|
|
522
|
+
(0, import_react.useEffect)(() => {
|
|
523
|
+
const handleClickOutside = (event) => {
|
|
524
|
+
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
|
|
525
|
+
setIsOpen(false);
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
529
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
530
|
+
}, []);
|
|
531
|
+
const items = (0, import_react.useMemo)(() => {
|
|
532
|
+
return [
|
|
533
|
+
...state.cities.map((c) => ({ ...c, type: "city" })),
|
|
534
|
+
...state.streets.map((s) => ({ ...s, type: "street" })),
|
|
535
|
+
...state.suggestions.map((s) => ({ ...s, type: "suggestion" }))
|
|
536
|
+
];
|
|
537
|
+
}, [state.cities, state.streets, state.suggestions]);
|
|
538
|
+
const handleSelect = (item) => {
|
|
539
|
+
selectItem(item);
|
|
540
|
+
setIsOpen(false);
|
|
541
|
+
if (!state.isValid && inputRef.current) {
|
|
542
|
+
inputRef.current.focus();
|
|
543
|
+
}
|
|
544
|
+
};
|
|
545
|
+
const hasResults = items.length > 0;
|
|
546
|
+
const showNoResults = !state.isLoading && !state.isError && state.query.length > 0 && !hasResults && !state.isValid;
|
|
547
|
+
const showDropdown = isOpen && (hasResults || showNoResults);
|
|
548
|
+
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(
|
|
549
|
+
"input",
|
|
550
|
+
{
|
|
551
|
+
ref: inputRef,
|
|
552
|
+
type: "text",
|
|
553
|
+
className: "pro6pp-input",
|
|
554
|
+
placeholder,
|
|
555
|
+
autoComplete: "off",
|
|
556
|
+
...inputProps,
|
|
557
|
+
...coreInputProps,
|
|
558
|
+
onFocus: (e) => {
|
|
559
|
+
setIsOpen(true);
|
|
560
|
+
inputProps?.onFocus?.(e);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
), state.isLoading && /* @__PURE__ */ import_react.default.createElement("div", { className: "pro6pp-loader" })), showDropdown && /* @__PURE__ */ import_react.default.createElement("ul", { className: "pro6pp-dropdown", role: "listbox" }, hasResults ? items.map((item, index) => {
|
|
564
|
+
const isActive = index === state.selectedSuggestionIndex;
|
|
565
|
+
const secondaryText = item.subtitle || (item.count !== void 0 ? item.count : "");
|
|
566
|
+
const showChevron = item.value === void 0 || item.value === null;
|
|
567
|
+
return /* @__PURE__ */ import_react.default.createElement(
|
|
568
|
+
"li",
|
|
569
|
+
{
|
|
570
|
+
key: `${item.label}-${index}`,
|
|
571
|
+
role: "option",
|
|
572
|
+
"aria-selected": isActive,
|
|
573
|
+
className: `pro6pp-item ${isActive ? "pro6pp-item--active" : ""}`,
|
|
574
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
575
|
+
onClick: () => handleSelect(item)
|
|
576
|
+
},
|
|
577
|
+
renderItem ? renderItem(item, isActive) : /* @__PURE__ */ import_react.default.createElement(import_react.default.Fragment, null, /* @__PURE__ */ import_react.default.createElement("span", { className: "pro6pp-item__label" }, item.label), 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(
|
|
578
|
+
"svg",
|
|
579
|
+
{
|
|
580
|
+
width: "16",
|
|
581
|
+
height: "16",
|
|
582
|
+
viewBox: "0 0 24 24",
|
|
583
|
+
fill: "none",
|
|
584
|
+
stroke: "currentColor",
|
|
585
|
+
strokeWidth: "2",
|
|
586
|
+
strokeLinecap: "round",
|
|
587
|
+
strokeLinejoin: "round"
|
|
588
|
+
},
|
|
589
|
+
/* @__PURE__ */ import_react.default.createElement("polyline", { points: "9 18 15 12 9 6" })
|
|
590
|
+
)))
|
|
591
|
+
);
|
|
592
|
+
}) : /* @__PURE__ */ import_react.default.createElement("li", { className: "pro6pp-no-results" }, renderNoResults ? renderNoResults(state) : noResultsText)));
|
|
593
|
+
};
|
|
278
594
|
// Annotate the CommonJS export names for ESM import in node:
|
|
279
595
|
0 && (module.exports = {
|
|
596
|
+
Pro6PPInfer,
|
|
280
597
|
useInfer
|
|
281
598
|
});
|
package/dist/index.d.cts
CHANGED
|
@@ -1,25 +1,57 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import { InferConfig, InferState, InferCore, InferResult } from '@pro6pp/infer-core';
|
|
2
3
|
export { AddressValue, CountryCode, Fetcher, InferConfig, InferResult, InferState, Stage } from '@pro6pp/infer-core';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @param config
|
|
7
|
-
* @returns An object containing the current state,
|
|
8
|
-
* @example
|
|
9
|
-
* const { state, inputProps, selectItem } = useInfer({
|
|
10
|
-
* authKey: 'YOUR_KEY',
|
|
11
|
-
* country: 'NL'
|
|
12
|
-
* });
|
|
6
|
+
* A headless React hook that provides the logic for address search using the Infer API.
|
|
7
|
+
* @param config The engine configuration (authKey, country, etc.).
|
|
8
|
+
* @returns An object containing the current state, the core instance, and pre-bound input props.
|
|
13
9
|
*/
|
|
14
10
|
declare function useInfer(config: InferConfig): {
|
|
11
|
+
/** The current UI state (suggestions, loading status, query, etc.). */
|
|
15
12
|
state: InferState;
|
|
13
|
+
/** The raw InferCore instance for manual control. */
|
|
16
14
|
core: InferCore;
|
|
15
|
+
/** Pre-configured event handlers to spread onto an <input /> element. */
|
|
17
16
|
inputProps: {
|
|
18
17
|
value: string;
|
|
19
18
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
20
19
|
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
21
20
|
};
|
|
21
|
+
/** Function to manually select a specific suggestion. */
|
|
22
22
|
selectItem: (item: InferResult | string) => void;
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Props for the Pro6PPInfer component.
|
|
26
|
+
*/
|
|
27
|
+
interface Pro6PPInferProps extends InferConfig {
|
|
28
|
+
/** Optional CSS class for the wrapper div. */
|
|
29
|
+
className?: string;
|
|
30
|
+
/** Optional inline styles for the wrapper div. */
|
|
31
|
+
style?: React.CSSProperties;
|
|
32
|
+
/** Attributes to pass directly to the underlying input element. */
|
|
33
|
+
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
|
34
|
+
/** * Custom placeholder text.
|
|
35
|
+
* @default 'Start typing an address...'
|
|
36
|
+
*/
|
|
37
|
+
placeholder?: string;
|
|
38
|
+
/** A custom render function for individual suggestion items. */
|
|
39
|
+
renderItem?: (item: InferResult, isActive: boolean) => React.ReactNode;
|
|
40
|
+
/** * If true, prevents the default CSS theme from being injected.
|
|
41
|
+
* @default false
|
|
42
|
+
*/
|
|
43
|
+
disableDefaultStyles?: boolean;
|
|
44
|
+
/** * The text to show when no results are found.
|
|
45
|
+
* @default 'No results found'
|
|
46
|
+
*/
|
|
47
|
+
noResultsText?: string;
|
|
48
|
+
/** A custom render function for the "no results" state. */
|
|
49
|
+
renderNoResults?: (state: InferState) => React.ReactNode;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A styled React component for Pro6PP Infer API.
|
|
53
|
+
* Includes styling, keyboard navigation, and loading states.
|
|
54
|
+
*/
|
|
55
|
+
declare const Pro6PPInfer: React.FC<Pro6PPInferProps>;
|
|
24
56
|
|
|
25
|
-
export { useInfer };
|
|
57
|
+
export { Pro6PPInfer, type Pro6PPInferProps, useInfer };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,25 +1,57 @@
|
|
|
1
|
+
import React from 'react';
|
|
1
2
|
import { InferConfig, InferState, InferCore, InferResult } from '@pro6pp/infer-core';
|
|
2
3
|
export { AddressValue, CountryCode, Fetcher, InferConfig, InferResult, InferState, Stage } from '@pro6pp/infer-core';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
* @param config
|
|
7
|
-
* @returns An object containing the current state,
|
|
8
|
-
* @example
|
|
9
|
-
* const { state, inputProps, selectItem } = useInfer({
|
|
10
|
-
* authKey: 'YOUR_KEY',
|
|
11
|
-
* country: 'NL'
|
|
12
|
-
* });
|
|
6
|
+
* A headless React hook that provides the logic for address search using the Infer API.
|
|
7
|
+
* @param config The engine configuration (authKey, country, etc.).
|
|
8
|
+
* @returns An object containing the current state, the core instance, and pre-bound input props.
|
|
13
9
|
*/
|
|
14
10
|
declare function useInfer(config: InferConfig): {
|
|
11
|
+
/** The current UI state (suggestions, loading status, query, etc.). */
|
|
15
12
|
state: InferState;
|
|
13
|
+
/** The raw InferCore instance for manual control. */
|
|
16
14
|
core: InferCore;
|
|
15
|
+
/** Pre-configured event handlers to spread onto an <input /> element. */
|
|
17
16
|
inputProps: {
|
|
18
17
|
value: string;
|
|
19
18
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
20
19
|
onKeyDown: (e: React.KeyboardEvent<HTMLInputElement>) => void;
|
|
21
20
|
};
|
|
21
|
+
/** Function to manually select a specific suggestion. */
|
|
22
22
|
selectItem: (item: InferResult | string) => void;
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Props for the Pro6PPInfer component.
|
|
26
|
+
*/
|
|
27
|
+
interface Pro6PPInferProps extends InferConfig {
|
|
28
|
+
/** Optional CSS class for the wrapper div. */
|
|
29
|
+
className?: string;
|
|
30
|
+
/** Optional inline styles for the wrapper div. */
|
|
31
|
+
style?: React.CSSProperties;
|
|
32
|
+
/** Attributes to pass directly to the underlying input element. */
|
|
33
|
+
inputProps?: React.InputHTMLAttributes<HTMLInputElement>;
|
|
34
|
+
/** * Custom placeholder text.
|
|
35
|
+
* @default 'Start typing an address...'
|
|
36
|
+
*/
|
|
37
|
+
placeholder?: string;
|
|
38
|
+
/** A custom render function for individual suggestion items. */
|
|
39
|
+
renderItem?: (item: InferResult, isActive: boolean) => React.ReactNode;
|
|
40
|
+
/** * If true, prevents the default CSS theme from being injected.
|
|
41
|
+
* @default false
|
|
42
|
+
*/
|
|
43
|
+
disableDefaultStyles?: boolean;
|
|
44
|
+
/** * The text to show when no results are found.
|
|
45
|
+
* @default 'No results found'
|
|
46
|
+
*/
|
|
47
|
+
noResultsText?: string;
|
|
48
|
+
/** A custom render function for the "no results" state. */
|
|
49
|
+
renderNoResults?: (state: InferState) => React.ReactNode;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* A styled React component for Pro6PP Infer API.
|
|
53
|
+
* Includes styling, keyboard navigation, and loading states.
|
|
54
|
+
*/
|
|
55
|
+
declare const Pro6PPInfer: React.FC<Pro6PPInferProps>;
|
|
24
56
|
|
|
25
|
-
export { useInfer };
|
|
57
|
+
export { Pro6PPInfer, type Pro6PPInferProps, useInfer };
|
package/dist/index.js
CHANGED
|
@@ -2,14 +2,15 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
|
|
5
|
-
// src/index.
|
|
6
|
-
import { useState, useMemo } from "react";
|
|
5
|
+
// src/index.tsx
|
|
6
|
+
import React, { useState, useMemo, useEffect, useRef } from "react";
|
|
7
7
|
|
|
8
8
|
// ../core/src/core.ts
|
|
9
9
|
var DEFAULTS = {
|
|
10
10
|
API_URL: "https://api.pro6pp.nl/v2",
|
|
11
11
|
LIMIT: 1e3,
|
|
12
|
-
DEBOUNCE_MS:
|
|
12
|
+
DEBOUNCE_MS: 150,
|
|
13
|
+
MIN_DEBOUNCE_MS: 50
|
|
13
14
|
};
|
|
14
15
|
var PATTERNS = {
|
|
15
16
|
DIGITS_1_3: /^[0-9]{1,3}$/
|
|
@@ -26,6 +27,10 @@ var INITIAL_STATE = {
|
|
|
26
27
|
selectedSuggestionIndex: -1
|
|
27
28
|
};
|
|
28
29
|
var InferCore = class {
|
|
30
|
+
/**
|
|
31
|
+
* Initializes a new instance of the Infer engine.
|
|
32
|
+
* @param config The configuration object including API keys and callbacks.
|
|
33
|
+
*/
|
|
29
34
|
constructor(config) {
|
|
30
35
|
__publicField(this, "country");
|
|
31
36
|
__publicField(this, "authKey");
|
|
@@ -34,9 +39,14 @@ var InferCore = class {
|
|
|
34
39
|
__publicField(this, "fetcher");
|
|
35
40
|
__publicField(this, "onStateChange");
|
|
36
41
|
__publicField(this, "onSelect");
|
|
42
|
+
/**
|
|
43
|
+
* The current read-only state of the engine.
|
|
44
|
+
* Use `onStateChange` to react to updates.
|
|
45
|
+
*/
|
|
37
46
|
__publicField(this, "state");
|
|
38
47
|
__publicField(this, "abortController", null);
|
|
39
48
|
__publicField(this, "debouncedFetch");
|
|
49
|
+
__publicField(this, "isSelecting", false);
|
|
40
50
|
this.country = config.country;
|
|
41
51
|
this.authKey = config.authKey;
|
|
42
52
|
this.apiUrl = config.apiUrl || DEFAULTS.API_URL;
|
|
@@ -47,23 +57,40 @@ var InferCore = class {
|
|
|
47
57
|
this.onSelect = config.onSelect || (() => {
|
|
48
58
|
});
|
|
49
59
|
this.state = { ...INITIAL_STATE };
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
);
|
|
60
|
+
const configDebounce = config.debounceMs !== void 0 ? config.debounceMs : DEFAULTS.DEBOUNCE_MS;
|
|
61
|
+
const debounceTime = Math.max(configDebounce, DEFAULTS.MIN_DEBOUNCE_MS);
|
|
62
|
+
this.debouncedFetch = this.debounce((val) => this.executeFetch(val), debounceTime);
|
|
54
63
|
}
|
|
64
|
+
/**
|
|
65
|
+
* Processes new text input from the user.
|
|
66
|
+
* Triggers a debounced API request and updates the internal state.
|
|
67
|
+
* @param value The raw string from the input field.
|
|
68
|
+
*/
|
|
55
69
|
handleInput(value) {
|
|
70
|
+
if (this.isSelecting) {
|
|
71
|
+
this.isSelecting = false;
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const isEditingFinal = this.state.stage === "final" && value !== this.state.query;
|
|
56
75
|
this.updateState({
|
|
57
76
|
query: value,
|
|
58
77
|
isValid: false,
|
|
59
78
|
isLoading: !!value.trim(),
|
|
60
79
|
selectedSuggestionIndex: -1
|
|
61
80
|
});
|
|
62
|
-
if (
|
|
81
|
+
if (isEditingFinal) {
|
|
63
82
|
this.onSelect(null);
|
|
64
83
|
}
|
|
65
84
|
this.debouncedFetch(value);
|
|
66
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Handles keyboard events for the input field.
|
|
88
|
+
* Supports:
|
|
89
|
+
* - `ArrowUp`/`ArrowDown`: Navigate through the suggestion list.
|
|
90
|
+
* - `Enter`: Select the currently highlighted suggestion.
|
|
91
|
+
* - `Space`: Automatically inserts a comma if a numeric house number is detected.
|
|
92
|
+
* @param event The keyboard event from the input element.
|
|
93
|
+
*/
|
|
67
94
|
handleKeyDown(event) {
|
|
68
95
|
const target = event.target;
|
|
69
96
|
if (!target) return;
|
|
@@ -105,15 +132,38 @@ var InferCore = class {
|
|
|
105
132
|
this.updateQueryAndFetch(next);
|
|
106
133
|
}
|
|
107
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Manually selects a suggestion or a string value.
|
|
137
|
+
* This is typically called when a user clicks a suggestion in the UI.
|
|
138
|
+
* @param item The suggestion object or string to select.
|
|
139
|
+
*/
|
|
108
140
|
selectItem(item) {
|
|
141
|
+
this.debouncedFetch.cancel();
|
|
142
|
+
if (this.abortController) {
|
|
143
|
+
this.abortController.abort();
|
|
144
|
+
}
|
|
109
145
|
const label = typeof item === "string" ? item : item.label;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
146
|
+
let logicValue = label;
|
|
147
|
+
if (typeof item !== "string" && typeof item.value === "string") {
|
|
148
|
+
logicValue = item.value;
|
|
149
|
+
}
|
|
150
|
+
const valueObj = typeof item !== "string" && typeof item.value === "object" ? item.value : void 0;
|
|
151
|
+
const isFullResult = !!valueObj && Object.keys(valueObj).length > 0;
|
|
152
|
+
this.isSelecting = true;
|
|
153
|
+
if (this.state.stage === "final" || isFullResult) {
|
|
154
|
+
let finalQuery = label;
|
|
155
|
+
if (valueObj && Object.keys(valueObj).length > 0) {
|
|
156
|
+
const { street, street_number, house_number, city } = valueObj;
|
|
157
|
+
const number = street_number || house_number;
|
|
158
|
+
if (street && number && city) {
|
|
159
|
+
finalQuery = `${street} ${number}, ${city}`;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
this.finishSelection(finalQuery, valueObj);
|
|
114
163
|
return;
|
|
115
164
|
}
|
|
116
|
-
|
|
165
|
+
const subtitle = typeof item !== "string" ? item.subtitle : null;
|
|
166
|
+
this.processSelection(logicValue, subtitle);
|
|
117
167
|
}
|
|
118
168
|
shouldAutoInsertComma(currentVal) {
|
|
119
169
|
const isStartOfSegmentAndNumeric = !currentVal.includes(",") && PATTERNS.DIGITS_1_3.test(currentVal.trim());
|
|
@@ -125,33 +175,43 @@ var InferCore = class {
|
|
|
125
175
|
return false;
|
|
126
176
|
}
|
|
127
177
|
finishSelection(label, value) {
|
|
128
|
-
this.updateState({
|
|
178
|
+
this.updateState({
|
|
179
|
+
query: label,
|
|
180
|
+
suggestions: [],
|
|
181
|
+
cities: [],
|
|
182
|
+
streets: [],
|
|
183
|
+
isValid: true,
|
|
184
|
+
stage: "final"
|
|
185
|
+
});
|
|
129
186
|
this.onSelect(value || label);
|
|
187
|
+
setTimeout(() => {
|
|
188
|
+
this.isSelecting = false;
|
|
189
|
+
}, 0);
|
|
130
190
|
}
|
|
131
|
-
processSelection(
|
|
191
|
+
processSelection(text, subtitle) {
|
|
132
192
|
const { stage, query } = this.state;
|
|
133
193
|
let nextQuery = query;
|
|
134
194
|
const isContextualSelection = subtitle && (stage === "city" || stage === "street" || stage === "mixed");
|
|
135
195
|
if (isContextualSelection) {
|
|
136
196
|
if (stage === "city") {
|
|
137
|
-
nextQuery = `${subtitle}, ${
|
|
197
|
+
nextQuery = `${subtitle}, ${text}, `;
|
|
138
198
|
} else {
|
|
139
199
|
const prefix = this.getQueryPrefix(query);
|
|
140
|
-
nextQuery = prefix ? `${prefix} ${
|
|
200
|
+
nextQuery = prefix ? `${prefix} ${text}, ${subtitle}, ` : `${text}, ${subtitle}, `;
|
|
141
201
|
}
|
|
142
202
|
this.updateQueryAndFetch(nextQuery);
|
|
143
203
|
return;
|
|
144
204
|
}
|
|
145
205
|
if (stage === "direct" || stage === "addition") {
|
|
146
|
-
this.finishSelection(
|
|
206
|
+
this.finishSelection(text);
|
|
147
207
|
return;
|
|
148
208
|
}
|
|
149
209
|
const hasComma = query.includes(",");
|
|
150
210
|
const isFirstSegment = !hasComma && (stage === "city" || stage === "street" || stage === "house_number_first");
|
|
151
211
|
if (isFirstSegment) {
|
|
152
|
-
nextQuery = `${
|
|
212
|
+
nextQuery = `${text}, `;
|
|
153
213
|
} else {
|
|
154
|
-
nextQuery = this.replaceLastSegment(query,
|
|
214
|
+
nextQuery = this.replaceLastSegment(query, text);
|
|
155
215
|
if (stage !== "house_number") {
|
|
156
216
|
nextQuery += ", ";
|
|
157
217
|
}
|
|
@@ -189,21 +249,52 @@ var InferCore = class {
|
|
|
189
249
|
stage: data.stage,
|
|
190
250
|
isLoading: false
|
|
191
251
|
};
|
|
252
|
+
let autoSelect = false;
|
|
253
|
+
let autoSelectItem = null;
|
|
254
|
+
const rawSuggestions = data.suggestions || [];
|
|
255
|
+
const uniqueSuggestions = [];
|
|
256
|
+
const seen = /* @__PURE__ */ new Set();
|
|
257
|
+
for (const item of rawSuggestions) {
|
|
258
|
+
const key = `${item.label}|${item.subtitle || ""}|${JSON.stringify(item.value || {})}`;
|
|
259
|
+
if (!seen.has(key)) {
|
|
260
|
+
seen.add(key);
|
|
261
|
+
uniqueSuggestions.push(item);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
192
264
|
if (data.stage === "mixed") {
|
|
193
265
|
newState.cities = data.cities || [];
|
|
194
266
|
newState.streets = data.streets || [];
|
|
195
267
|
newState.suggestions = [];
|
|
196
268
|
} else {
|
|
197
|
-
newState.suggestions =
|
|
269
|
+
newState.suggestions = uniqueSuggestions;
|
|
198
270
|
newState.cities = [];
|
|
199
271
|
newState.streets = [];
|
|
272
|
+
if (data.stage === "final" && uniqueSuggestions.length === 1) {
|
|
273
|
+
autoSelect = true;
|
|
274
|
+
autoSelectItem = uniqueSuggestions[0];
|
|
275
|
+
}
|
|
200
276
|
}
|
|
201
277
|
newState.isValid = data.stage === "final";
|
|
202
|
-
|
|
278
|
+
if (autoSelect && autoSelectItem) {
|
|
279
|
+
newState.query = autoSelectItem.label;
|
|
280
|
+
newState.suggestions = [];
|
|
281
|
+
newState.cities = [];
|
|
282
|
+
newState.streets = [];
|
|
283
|
+
newState.isValid = true;
|
|
284
|
+
this.updateState(newState);
|
|
285
|
+
const val = typeof autoSelectItem.value === "object" ? autoSelectItem.value : autoSelectItem.label;
|
|
286
|
+
this.onSelect(val);
|
|
287
|
+
} else {
|
|
288
|
+
this.updateState(newState);
|
|
289
|
+
}
|
|
203
290
|
}
|
|
204
291
|
updateQueryAndFetch(nextQuery) {
|
|
205
292
|
this.updateState({ query: nextQuery, suggestions: [], cities: [], streets: [] });
|
|
206
|
-
this.
|
|
293
|
+
this.updateState({ isLoading: true, isValid: false });
|
|
294
|
+
this.debouncedFetch(nextQuery);
|
|
295
|
+
setTimeout(() => {
|
|
296
|
+
this.isSelecting = false;
|
|
297
|
+
}, 0);
|
|
207
298
|
}
|
|
208
299
|
replaceLastSegment(fullText, newSegment) {
|
|
209
300
|
const lastCommaIndex = fullText.lastIndexOf(",");
|
|
@@ -226,33 +317,248 @@ var InferCore = class {
|
|
|
226
317
|
}
|
|
227
318
|
debounce(func, wait) {
|
|
228
319
|
let timeout;
|
|
229
|
-
|
|
320
|
+
const debounced = (...args) => {
|
|
230
321
|
if (timeout) clearTimeout(timeout);
|
|
231
322
|
timeout = setTimeout(() => func.apply(this, args), wait);
|
|
232
323
|
};
|
|
324
|
+
debounced.cancel = () => {
|
|
325
|
+
if (timeout) {
|
|
326
|
+
clearTimeout(timeout);
|
|
327
|
+
timeout = void 0;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
return debounced;
|
|
233
331
|
}
|
|
234
332
|
};
|
|
235
333
|
|
|
236
|
-
// src/
|
|
334
|
+
// ../core/src/default-styles.ts
|
|
335
|
+
var DEFAULT_STYLES = `
|
|
336
|
+
.pro6pp-wrapper {
|
|
337
|
+
position: relative;
|
|
338
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
|
339
|
+
box-sizing: border-box;
|
|
340
|
+
width: 100%;
|
|
341
|
+
}
|
|
342
|
+
.pro6pp-wrapper * {
|
|
343
|
+
box-sizing: border-box;
|
|
344
|
+
}
|
|
345
|
+
.pro6pp-input {
|
|
346
|
+
width: 100%;
|
|
347
|
+
padding: 10px 12px;
|
|
348
|
+
border: 1px solid #e0e0e0;
|
|
349
|
+
border-radius: 4px;
|
|
350
|
+
font-size: 16px;
|
|
351
|
+
line-height: 1.5;
|
|
352
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
353
|
+
}
|
|
354
|
+
.pro6pp-input:focus {
|
|
355
|
+
outline: none;
|
|
356
|
+
border-color: #3b82f6;
|
|
357
|
+
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
358
|
+
}
|
|
359
|
+
.pro6pp-dropdown {
|
|
360
|
+
position: absolute;
|
|
361
|
+
top: 100%;
|
|
362
|
+
left: 0;
|
|
363
|
+
right: 0;
|
|
364
|
+
z-index: 9999;
|
|
365
|
+
margin-top: 4px;
|
|
366
|
+
background: white;
|
|
367
|
+
border: 1px solid #e0e0e0;
|
|
368
|
+
border-radius: 4px;
|
|
369
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
|
370
|
+
max-height: 300px;
|
|
371
|
+
overflow-y: auto;
|
|
372
|
+
list-style: none !important;
|
|
373
|
+
padding: 0 !important;
|
|
374
|
+
margin: 0 !important;
|
|
375
|
+
}
|
|
376
|
+
.pro6pp-item {
|
|
377
|
+
padding: 10px 16px;
|
|
378
|
+
cursor: pointer;
|
|
379
|
+
display: flex;
|
|
380
|
+
flex-direction: row;
|
|
381
|
+
align-items: center;
|
|
382
|
+
color: #000000;
|
|
383
|
+
font-size: 14px;
|
|
384
|
+
line-height: 1.2;
|
|
385
|
+
white-space: nowrap;
|
|
386
|
+
overflow: hidden;
|
|
387
|
+
}
|
|
388
|
+
.pro6pp-item:hover, .pro6pp-item--active {
|
|
389
|
+
background-color: #f5f5f5;
|
|
390
|
+
}
|
|
391
|
+
.pro6pp-item__label {
|
|
392
|
+
font-weight: 500;
|
|
393
|
+
flex-shrink: 0;
|
|
394
|
+
}
|
|
395
|
+
.pro6pp-item__subtitle {
|
|
396
|
+
font-size: 14px;
|
|
397
|
+
color: #404040;
|
|
398
|
+
overflow: hidden;
|
|
399
|
+
text-overflow: ellipsis;
|
|
400
|
+
flex-shrink: 1;
|
|
401
|
+
}
|
|
402
|
+
.pro6pp-item__chevron {
|
|
403
|
+
margin-left: auto;
|
|
404
|
+
display: flex;
|
|
405
|
+
align-items: center;
|
|
406
|
+
color: #a3a3a3;
|
|
407
|
+
padding-left: 8px;
|
|
408
|
+
}
|
|
409
|
+
.pro6pp-no-results {
|
|
410
|
+
padding: 12px;
|
|
411
|
+
color: #555555;
|
|
412
|
+
font-size: 14px;
|
|
413
|
+
text-align: center;
|
|
414
|
+
user-select: none;
|
|
415
|
+
pointer-events: none;
|
|
416
|
+
}
|
|
417
|
+
.pro6pp-loader {
|
|
418
|
+
position: absolute;
|
|
419
|
+
right: 12px;
|
|
420
|
+
top: 50%;
|
|
421
|
+
transform: translateY(-50%);
|
|
422
|
+
width: 16px;
|
|
423
|
+
height: 16px;
|
|
424
|
+
border: 2px solid #e0e0e0;
|
|
425
|
+
border-top-color: #404040;
|
|
426
|
+
border-radius: 50%;
|
|
427
|
+
animation: pro6pp-spin 0.6s linear infinite;
|
|
428
|
+
pointer-events: none;
|
|
429
|
+
}
|
|
430
|
+
@keyframes pro6pp-spin {
|
|
431
|
+
to { transform: translateY(-50%) rotate(360deg); }
|
|
432
|
+
}
|
|
433
|
+
`;
|
|
434
|
+
|
|
435
|
+
// src/index.tsx
|
|
237
436
|
function useInfer(config) {
|
|
238
437
|
const [state, setState] = useState(INITIAL_STATE);
|
|
239
438
|
const core = useMemo(() => {
|
|
240
439
|
return new InferCore({
|
|
241
440
|
...config,
|
|
242
|
-
onStateChange: (newState) =>
|
|
441
|
+
onStateChange: (newState) => {
|
|
442
|
+
setState({ ...newState });
|
|
443
|
+
if (config.onStateChange) {
|
|
444
|
+
config.onStateChange(newState);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
243
447
|
});
|
|
244
|
-
}, [config.country, config.authKey, config.limit]);
|
|
448
|
+
}, [config.country, config.authKey, config.limit, config.debounceMs]);
|
|
245
449
|
return {
|
|
450
|
+
/** The current UI state (suggestions, loading status, query, etc.). */
|
|
246
451
|
state,
|
|
452
|
+
/** The raw InferCore instance for manual control. */
|
|
247
453
|
core,
|
|
454
|
+
/** Pre-configured event handlers to spread onto an <input /> element. */
|
|
248
455
|
inputProps: {
|
|
249
456
|
value: state.query,
|
|
250
457
|
onChange: (e) => core.handleInput(e.target.value),
|
|
251
458
|
onKeyDown: (e) => core.handleKeyDown(e)
|
|
252
459
|
},
|
|
460
|
+
/** Function to manually select a specific suggestion. */
|
|
253
461
|
selectItem: (item) => core.selectItem(item)
|
|
254
462
|
};
|
|
255
463
|
}
|
|
464
|
+
var Pro6PPInfer = ({
|
|
465
|
+
className,
|
|
466
|
+
style,
|
|
467
|
+
inputProps,
|
|
468
|
+
placeholder = "Start typing an address...",
|
|
469
|
+
renderItem,
|
|
470
|
+
disableDefaultStyles = false,
|
|
471
|
+
noResultsText = "No results found",
|
|
472
|
+
renderNoResults,
|
|
473
|
+
...config
|
|
474
|
+
}) => {
|
|
475
|
+
const { state, selectItem, inputProps: coreInputProps } = useInfer(config);
|
|
476
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
477
|
+
const inputRef = useRef(null);
|
|
478
|
+
const wrapperRef = useRef(null);
|
|
479
|
+
useEffect(() => {
|
|
480
|
+
if (disableDefaultStyles) return;
|
|
481
|
+
const styleId = "pro6pp-styles";
|
|
482
|
+
if (!document.getElementById(styleId)) {
|
|
483
|
+
const styleEl = document.createElement("style");
|
|
484
|
+
styleEl.id = styleId;
|
|
485
|
+
styleEl.textContent = DEFAULT_STYLES;
|
|
486
|
+
document.head.appendChild(styleEl);
|
|
487
|
+
}
|
|
488
|
+
}, [disableDefaultStyles]);
|
|
489
|
+
useEffect(() => {
|
|
490
|
+
const handleClickOutside = (event) => {
|
|
491
|
+
if (wrapperRef.current && !wrapperRef.current.contains(event.target)) {
|
|
492
|
+
setIsOpen(false);
|
|
493
|
+
}
|
|
494
|
+
};
|
|
495
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
496
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
497
|
+
}, []);
|
|
498
|
+
const items = useMemo(() => {
|
|
499
|
+
return [
|
|
500
|
+
...state.cities.map((c) => ({ ...c, type: "city" })),
|
|
501
|
+
...state.streets.map((s) => ({ ...s, type: "street" })),
|
|
502
|
+
...state.suggestions.map((s) => ({ ...s, type: "suggestion" }))
|
|
503
|
+
];
|
|
504
|
+
}, [state.cities, state.streets, state.suggestions]);
|
|
505
|
+
const handleSelect = (item) => {
|
|
506
|
+
selectItem(item);
|
|
507
|
+
setIsOpen(false);
|
|
508
|
+
if (!state.isValid && inputRef.current) {
|
|
509
|
+
inputRef.current.focus();
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
const hasResults = items.length > 0;
|
|
513
|
+
const showNoResults = !state.isLoading && !state.isError && state.query.length > 0 && !hasResults && !state.isValid;
|
|
514
|
+
const showDropdown = isOpen && (hasResults || showNoResults);
|
|
515
|
+
return /* @__PURE__ */ React.createElement("div", { ref: wrapperRef, className: `pro6pp-wrapper ${className || ""}`, style }, /* @__PURE__ */ React.createElement("div", { style: { position: "relative" } }, /* @__PURE__ */ React.createElement(
|
|
516
|
+
"input",
|
|
517
|
+
{
|
|
518
|
+
ref: inputRef,
|
|
519
|
+
type: "text",
|
|
520
|
+
className: "pro6pp-input",
|
|
521
|
+
placeholder,
|
|
522
|
+
autoComplete: "off",
|
|
523
|
+
...inputProps,
|
|
524
|
+
...coreInputProps,
|
|
525
|
+
onFocus: (e) => {
|
|
526
|
+
setIsOpen(true);
|
|
527
|
+
inputProps?.onFocus?.(e);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
), state.isLoading && /* @__PURE__ */ React.createElement("div", { className: "pro6pp-loader" })), showDropdown && /* @__PURE__ */ React.createElement("ul", { className: "pro6pp-dropdown", role: "listbox" }, hasResults ? items.map((item, index) => {
|
|
531
|
+
const isActive = index === state.selectedSuggestionIndex;
|
|
532
|
+
const secondaryText = item.subtitle || (item.count !== void 0 ? item.count : "");
|
|
533
|
+
const showChevron = item.value === void 0 || item.value === null;
|
|
534
|
+
return /* @__PURE__ */ React.createElement(
|
|
535
|
+
"li",
|
|
536
|
+
{
|
|
537
|
+
key: `${item.label}-${index}`,
|
|
538
|
+
role: "option",
|
|
539
|
+
"aria-selected": isActive,
|
|
540
|
+
className: `pro6pp-item ${isActive ? "pro6pp-item--active" : ""}`,
|
|
541
|
+
onMouseDown: (e) => e.preventDefault(),
|
|
542
|
+
onClick: () => handleSelect(item)
|
|
543
|
+
},
|
|
544
|
+
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(
|
|
545
|
+
"svg",
|
|
546
|
+
{
|
|
547
|
+
width: "16",
|
|
548
|
+
height: "16",
|
|
549
|
+
viewBox: "0 0 24 24",
|
|
550
|
+
fill: "none",
|
|
551
|
+
stroke: "currentColor",
|
|
552
|
+
strokeWidth: "2",
|
|
553
|
+
strokeLinecap: "round",
|
|
554
|
+
strokeLinejoin: "round"
|
|
555
|
+
},
|
|
556
|
+
/* @__PURE__ */ React.createElement("polyline", { points: "9 18 15 12 9 6" })
|
|
557
|
+
)))
|
|
558
|
+
);
|
|
559
|
+
}) : /* @__PURE__ */ React.createElement("li", { className: "pro6pp-no-results" }, renderNoResults ? renderNoResults(state) : noResultsText)));
|
|
560
|
+
};
|
|
256
561
|
export {
|
|
562
|
+
Pro6PPInfer,
|
|
257
563
|
useInfer
|
|
258
564
|
};
|
package/package.json
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
"url": "https://github.com/pro6pp/infer-sdk/issues"
|
|
21
21
|
},
|
|
22
22
|
"sideEffects": false,
|
|
23
|
-
"version": "0.0.2-beta.
|
|
23
|
+
"version": "0.0.2-beta.8",
|
|
24
24
|
"main": "./dist/index.cjs",
|
|
25
25
|
"module": "./dist/index.js",
|
|
26
26
|
"types": "./dist/index.d.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"react": ">=16"
|
|
47
47
|
},
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@pro6pp/infer-core": "0.0.2-beta.
|
|
49
|
+
"@pro6pp/infer-core": "0.0.2-beta.6"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@testing-library/dom": "^10.4.1",
|