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