@radarlabs/plugin-autocomplete 5.0.0-beta.5 → 5.0.0-beta.6
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/cdn/radar-autocomplete.js +114 -22
- package/cdn/radar-autocomplete.min.js +1 -1
- package/dist/autocomplete.d.ts +94 -6
- package/dist/errors.d.ts +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +114 -22
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +32 -5
- package/dist/version.d.ts +1 -1
- package/package.json +1 -1
- package/src/autocomplete.ts +114 -32
- package/src/errors.ts +1 -0
- package/src/index.ts +11 -0
- package/src/types.ts +40 -13
- package/src/version.ts +1 -1
package/src/autocomplete.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RadarAutocompleteContainerNotFound } from './errors';
|
|
2
2
|
import type { RadarAutocompleteUIOptions, RadarAutocompleteConfig } from './types';
|
|
3
|
-
import type { RadarAutocompleteParams, Location, RadarPluginContext } from 'radar-sdk-js';
|
|
3
|
+
import type { RadarAutocompleteAddress, RadarAutocompleteParams, Location, RadarPluginContext } from 'radar-sdk-js';
|
|
4
4
|
|
|
5
5
|
const CLASSNAMES = {
|
|
6
6
|
WRAPPER: 'radar-autocomplete-wrapper',
|
|
@@ -66,13 +66,14 @@ const getMarkerIcon = (color: string = "#ACBDC8") => {
|
|
|
66
66
|
};
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
/** address autocomplete UI widget with keyboard navigation and result display */
|
|
69
70
|
class AutocompleteUI {
|
|
70
71
|
private ctx: RadarPluginContext;
|
|
71
72
|
config: RadarAutocompleteConfig;
|
|
72
73
|
isOpen: boolean;
|
|
73
|
-
results:
|
|
74
|
+
results: RadarAutocompleteAddress[];
|
|
74
75
|
highlightedIndex: number;
|
|
75
|
-
debouncedFetchResults: (
|
|
76
|
+
debouncedFetchResults: (query: string) => Promise<RadarAutocompleteAddress[]>;
|
|
76
77
|
near?: string;
|
|
77
78
|
|
|
78
79
|
// DOM elements
|
|
@@ -89,7 +90,7 @@ class AutocompleteUI {
|
|
|
89
90
|
|
|
90
91
|
// setup state
|
|
91
92
|
this.isOpen = false;
|
|
92
|
-
this.debouncedFetchResults = this.debounce(this.fetchResults, this.config.debounceMS);
|
|
93
|
+
this.debouncedFetchResults = this.debounce(this.fetchResults.bind(this), this.config.debounceMS);
|
|
93
94
|
this.results = [];
|
|
94
95
|
this.highlightedIndex = -1;
|
|
95
96
|
|
|
@@ -141,7 +142,7 @@ class AutocompleteUI {
|
|
|
141
142
|
|
|
142
143
|
// append to dom
|
|
143
144
|
this.wrapper.appendChild(this.resultsList);
|
|
144
|
-
|
|
145
|
+
containerEL.parentNode?.appendChild(this.wrapper);
|
|
145
146
|
|
|
146
147
|
} else {
|
|
147
148
|
// if container is not an input, create new input and append to container
|
|
@@ -185,6 +186,7 @@ class AutocompleteUI {
|
|
|
185
186
|
Logger.debug('AutocompleteUI initialized with options', this.config);
|
|
186
187
|
}
|
|
187
188
|
|
|
189
|
+
/** handle input field changes and trigger debounced search */
|
|
188
190
|
public handleInput() {
|
|
189
191
|
const { Logger } = this.ctx;
|
|
190
192
|
|
|
@@ -195,14 +197,14 @@ class AutocompleteUI {
|
|
|
195
197
|
}
|
|
196
198
|
|
|
197
199
|
this.debouncedFetchResults(query)
|
|
198
|
-
.then((results
|
|
200
|
+
.then((results) => {
|
|
199
201
|
const onResults = this.config.onResults;
|
|
200
202
|
if (onResults) {
|
|
201
203
|
onResults(results);
|
|
202
204
|
}
|
|
203
205
|
this.displayResults(results);
|
|
204
206
|
})
|
|
205
|
-
.catch((error) => {
|
|
207
|
+
.catch((error: Error) => {
|
|
206
208
|
Logger.warn(`Autocomplete ui error: ${error.message}`);
|
|
207
209
|
const onError = this.config.onError;
|
|
208
210
|
if (onError) {
|
|
@@ -211,39 +213,39 @@ class AutocompleteUI {
|
|
|
211
213
|
});
|
|
212
214
|
}
|
|
213
215
|
|
|
214
|
-
public debounce
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
216
|
+
public debounce<TArgs extends unknown[], TReturn>(
|
|
217
|
+
fn: (...args: TArgs) => Promise<TReturn>,
|
|
218
|
+
delay: number,
|
|
219
|
+
): (...args: TArgs) => Promise<TReturn> {
|
|
220
|
+
let timeoutId: ReturnType<typeof setTimeout>;
|
|
221
|
+
let resolveFn: ((value: TReturn) => void) | null = null;
|
|
222
|
+
let rejectFn: ((reason: unknown) => void) | null = null;
|
|
218
223
|
|
|
219
|
-
return (...args:
|
|
224
|
+
return (...args: TArgs) => {
|
|
220
225
|
clearTimeout(timeoutId);
|
|
221
226
|
|
|
222
227
|
timeoutId = setTimeout(() => {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
}
|
|
231
|
-
})
|
|
232
|
-
.catch((error) => {
|
|
233
|
-
if (rejectFn) {
|
|
234
|
-
rejectFn(error);
|
|
235
|
-
}
|
|
236
|
-
});
|
|
237
|
-
}
|
|
228
|
+
fn(...args)
|
|
229
|
+
.then((value) => {
|
|
230
|
+
resolveFn?.(value);
|
|
231
|
+
})
|
|
232
|
+
.catch((error) => {
|
|
233
|
+
rejectFn?.(error);
|
|
234
|
+
});
|
|
238
235
|
}, delay);
|
|
239
236
|
|
|
240
|
-
return new Promise((resolve, reject) => {
|
|
237
|
+
return new Promise<TReturn>((resolve, reject) => {
|
|
241
238
|
resolveFn = resolve;
|
|
242
239
|
rejectFn = reject;
|
|
243
240
|
});
|
|
244
241
|
};
|
|
245
242
|
}
|
|
246
243
|
|
|
244
|
+
/**
|
|
245
|
+
* fetch autocomplete results from the Radar API
|
|
246
|
+
* @param query - the search query string
|
|
247
|
+
* @returns matching addresses
|
|
248
|
+
*/
|
|
247
249
|
public async fetchResults(query: string) {
|
|
248
250
|
const { apis } = this.ctx;
|
|
249
251
|
const { limit, layers, countryCode, expandUnits, mailable, lang, postalCode, onRequest } = this.config;
|
|
@@ -271,7 +273,11 @@ class AutocompleteUI {
|
|
|
271
273
|
return addresses;
|
|
272
274
|
}
|
|
273
275
|
|
|
274
|
-
|
|
276
|
+
/**
|
|
277
|
+
* render autocomplete results in the dropdown
|
|
278
|
+
* @param results - array of address results to display
|
|
279
|
+
*/
|
|
280
|
+
public displayResults(results: RadarAutocompleteAddress[]) {
|
|
275
281
|
// Clear the previous results
|
|
276
282
|
this.clearResultsList();
|
|
277
283
|
this.results = results;
|
|
@@ -292,7 +298,7 @@ class AutocompleteUI {
|
|
|
292
298
|
|
|
293
299
|
// construct result with bolded label
|
|
294
300
|
let listContent;
|
|
295
|
-
if (result.formattedAddress
|
|
301
|
+
if (result.formattedAddress?.includes(result.addressLabel!) && result.layer !== 'postalCode') {
|
|
296
302
|
// if addressLabel is contained in the formatted address, bold the address label
|
|
297
303
|
const regex = new RegExp(`(${result.addressLabel}),?`);
|
|
298
304
|
listContent = result.formattedAddress.replace(regex, '<b>$1</b>');
|
|
@@ -346,6 +352,7 @@ class AutocompleteUI {
|
|
|
346
352
|
}
|
|
347
353
|
}
|
|
348
354
|
|
|
355
|
+
/** open the results dropdown */
|
|
349
356
|
public open() {
|
|
350
357
|
if (this.isOpen) {
|
|
351
358
|
return;
|
|
@@ -356,6 +363,7 @@ class AutocompleteUI {
|
|
|
356
363
|
this.isOpen = true;
|
|
357
364
|
}
|
|
358
365
|
|
|
366
|
+
/** close the results dropdown and clear highlighted state */
|
|
359
367
|
public close(e?: FocusEvent) {
|
|
360
368
|
if (!this.isOpen) {
|
|
361
369
|
return;
|
|
@@ -374,6 +382,10 @@ class AutocompleteUI {
|
|
|
374
382
|
}, linkClick ? 100 : 0);
|
|
375
383
|
}
|
|
376
384
|
|
|
385
|
+
/**
|
|
386
|
+
* highlight a result by index with wrap-around navigation
|
|
387
|
+
* @param index - the result index to highlight
|
|
388
|
+
*/
|
|
377
389
|
public goTo(index: number) {
|
|
378
390
|
if (!this.isOpen || !this.results.length) {
|
|
379
391
|
return;
|
|
@@ -441,6 +453,10 @@ class AutocompleteUI {
|
|
|
441
453
|
}
|
|
442
454
|
}
|
|
443
455
|
|
|
456
|
+
/**
|
|
457
|
+
* select a result by index and populate the input field
|
|
458
|
+
* @param index - the result index to select
|
|
459
|
+
*/
|
|
444
460
|
public select(index: number) {
|
|
445
461
|
const { Logger } = this.ctx;
|
|
446
462
|
const result = this.results[index];
|
|
@@ -450,7 +466,7 @@ class AutocompleteUI {
|
|
|
450
466
|
}
|
|
451
467
|
|
|
452
468
|
let inputValue;
|
|
453
|
-
if (result.formattedAddress
|
|
469
|
+
if (result.formattedAddress?.includes(result.addressLabel!)) {
|
|
454
470
|
inputValue = result.formattedAddress;
|
|
455
471
|
} else {
|
|
456
472
|
const label = result.placeLabel || result.addressLabel;
|
|
@@ -467,12 +483,13 @@ class AutocompleteUI {
|
|
|
467
483
|
this.close();
|
|
468
484
|
}
|
|
469
485
|
|
|
486
|
+
/** clear the results list DOM and reset results array */
|
|
470
487
|
public clearResultsList() {
|
|
471
488
|
this.resultsList.innerHTML = '';
|
|
472
489
|
this.results = [];
|
|
473
490
|
}
|
|
474
491
|
|
|
475
|
-
|
|
492
|
+
/** remove the autocomplete widget from the DOM */
|
|
476
493
|
public remove() {
|
|
477
494
|
const { Logger } = this.ctx;
|
|
478
495
|
Logger.debug('AutocompleteUI removed.');
|
|
@@ -481,6 +498,11 @@ class AutocompleteUI {
|
|
|
481
498
|
this.wrapper.remove();
|
|
482
499
|
}
|
|
483
500
|
|
|
501
|
+
/**
|
|
502
|
+
* set the `near` location bias for autocomplete requests
|
|
503
|
+
* @param near - location string, Location object, or null to clear
|
|
504
|
+
* @returns this instance for chaining
|
|
505
|
+
*/
|
|
484
506
|
public setNear(near: string | Location | undefined | null) {
|
|
485
507
|
if (near === undefined || near === null) {
|
|
486
508
|
this.near = undefined;
|
|
@@ -492,24 +514,44 @@ class AutocompleteUI {
|
|
|
492
514
|
return this;
|
|
493
515
|
}
|
|
494
516
|
|
|
517
|
+
/**
|
|
518
|
+
* set the input placeholder text
|
|
519
|
+
* @param placeholder - new placeholder string
|
|
520
|
+
* @returns this instance for chaining
|
|
521
|
+
*/
|
|
495
522
|
public setPlaceholder(placeholder: string) {
|
|
496
523
|
this.config.placeholder = placeholder;
|
|
497
524
|
this.inputField.placeholder = placeholder;
|
|
498
525
|
return this;
|
|
499
526
|
}
|
|
500
527
|
|
|
528
|
+
/**
|
|
529
|
+
* set the disabled state of the input
|
|
530
|
+
* @param disabled - whether to disable the input
|
|
531
|
+
* @returns this instance for chaining
|
|
532
|
+
*/
|
|
501
533
|
public setDisabled(disabled: boolean) {
|
|
502
534
|
this.config.disabled = disabled;
|
|
503
535
|
this.inputField.disabled = disabled;
|
|
504
536
|
return this;
|
|
505
537
|
}
|
|
506
538
|
|
|
539
|
+
/**
|
|
540
|
+
* toggle responsive width mode
|
|
541
|
+
* @param responsive - whether to use responsive layout
|
|
542
|
+
* @returns this instance for chaining
|
|
543
|
+
*/
|
|
507
544
|
public setResponsive(responsive: boolean) {
|
|
508
545
|
this.config.responsive = responsive;
|
|
509
546
|
setWidth(this.wrapper, this.config);
|
|
510
547
|
return this;
|
|
511
548
|
}
|
|
512
549
|
|
|
550
|
+
/**
|
|
551
|
+
* set the widget width
|
|
552
|
+
* @param width - width in px, CSS string, or null to reset
|
|
553
|
+
* @returns this instance for chaining
|
|
554
|
+
*/
|
|
513
555
|
public setWidth(width: number | string | null) {
|
|
514
556
|
if (width === null) {
|
|
515
557
|
this.config.width = undefined;
|
|
@@ -520,6 +562,11 @@ class AutocompleteUI {
|
|
|
520
562
|
return this;
|
|
521
563
|
}
|
|
522
564
|
|
|
565
|
+
/**
|
|
566
|
+
* set the max height of the results dropdown
|
|
567
|
+
* @param height - height in px, CSS string, or null to reset
|
|
568
|
+
* @returns this instance for chaining
|
|
569
|
+
*/
|
|
523
570
|
public setMaxHeight(height: number | string | null) {
|
|
524
571
|
if (height === null) {
|
|
525
572
|
this.config.maxHeight = undefined;
|
|
@@ -530,17 +577,32 @@ class AutocompleteUI {
|
|
|
530
577
|
return this;
|
|
531
578
|
}
|
|
532
579
|
|
|
580
|
+
/**
|
|
581
|
+
* set the minimum character count to trigger autocomplete
|
|
582
|
+
* @param minCharacters - character threshold
|
|
583
|
+
* @returns this instance for chaining
|
|
584
|
+
*/
|
|
533
585
|
public setMinCharacters(minCharacters: number) {
|
|
534
586
|
this.config.minCharacters = minCharacters;
|
|
535
587
|
this.config.threshold = minCharacters;
|
|
536
588
|
return this;
|
|
537
589
|
}
|
|
538
590
|
|
|
591
|
+
/**
|
|
592
|
+
* set the maximum number of results
|
|
593
|
+
* @param limit - result count limit
|
|
594
|
+
* @returns this instance for chaining
|
|
595
|
+
*/
|
|
539
596
|
public setLimit(limit: number) {
|
|
540
597
|
this.config.limit = limit;
|
|
541
598
|
return this;
|
|
542
599
|
}
|
|
543
600
|
|
|
601
|
+
/**
|
|
602
|
+
* set the language for autocomplete results
|
|
603
|
+
* @param lang - language code or null to clear
|
|
604
|
+
* @returns this instance for chaining
|
|
605
|
+
*/
|
|
544
606
|
public setLang(lang: string | null) {
|
|
545
607
|
if (lang === null) {
|
|
546
608
|
this.config.lang = undefined;
|
|
@@ -550,6 +612,11 @@ class AutocompleteUI {
|
|
|
550
612
|
return this;
|
|
551
613
|
}
|
|
552
614
|
|
|
615
|
+
/**
|
|
616
|
+
* set a postal code bias for autocomplete requests
|
|
617
|
+
* @param postalCode - postal code string or null to clear
|
|
618
|
+
* @returns this instance for chaining
|
|
619
|
+
*/
|
|
553
620
|
public setPostalCode(postalCode: string | null) {
|
|
554
621
|
if (postalCode === null) {
|
|
555
622
|
this.config.postalCode = undefined;
|
|
@@ -559,6 +626,11 @@ class AutocompleteUI {
|
|
|
559
626
|
return this;
|
|
560
627
|
}
|
|
561
628
|
|
|
629
|
+
/**
|
|
630
|
+
* toggle marker icons in result items
|
|
631
|
+
* @param showMarkers - whether to show markers
|
|
632
|
+
* @returns this instance for chaining
|
|
633
|
+
*/
|
|
562
634
|
public setShowMarkers(showMarkers: boolean) {
|
|
563
635
|
this.config.showMarkers = showMarkers;
|
|
564
636
|
if (showMarkers) {
|
|
@@ -584,6 +656,11 @@ class AutocompleteUI {
|
|
|
584
656
|
return this;
|
|
585
657
|
}
|
|
586
658
|
|
|
659
|
+
/**
|
|
660
|
+
* set the color of marker icons in result items
|
|
661
|
+
* @param color - CSS color string
|
|
662
|
+
* @returns this instance for chaining
|
|
663
|
+
*/
|
|
587
664
|
public setMarkerColor(color: string) {
|
|
588
665
|
this.config.markerColor = color;
|
|
589
666
|
const marker = this.resultsList.getElementsByClassName(CLASSNAMES.RESULTS_MARKER);
|
|
@@ -593,6 +670,11 @@ class AutocompleteUI {
|
|
|
593
670
|
return this;
|
|
594
671
|
}
|
|
595
672
|
|
|
673
|
+
/**
|
|
674
|
+
* toggle hiding results when input loses focus
|
|
675
|
+
* @param hideResultsOnBlur - whether to hide on blur
|
|
676
|
+
* @returns this instance for chaining
|
|
677
|
+
*/
|
|
596
678
|
public setHideResultsOnBlur(hideResultsOnBlur: boolean) {
|
|
597
679
|
this.config.hideResultsOnBlur = hideResultsOnBlur;
|
|
598
680
|
if (hideResultsOnBlur) {
|
package/src/errors.ts
CHANGED
package/src/index.ts
CHANGED
|
@@ -17,6 +17,17 @@ declare module 'radar-sdk-js' {
|
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
+
/**
|
|
21
|
+
* create the Radar autocomplete plugin
|
|
22
|
+
*
|
|
23
|
+
* @returns a plugin that adds `Radar.ui.autocomplete()` method
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```ts
|
|
27
|
+
* import { createAutocompletePlugin } from '@radarlabs/plugin-autocomplete';
|
|
28
|
+
* Radar.registerPlugin(createAutocompletePlugin());
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
20
31
|
export function createAutocompletePlugin(): RadarPlugin {
|
|
21
32
|
return {
|
|
22
33
|
name: 'autocomplete',
|
package/src/types.ts
CHANGED
|
@@ -1,30 +1,57 @@
|
|
|
1
|
-
import type { RadarAutocompleteParams } from 'radar-sdk-js';
|
|
1
|
+
import type { RadarAutocompleteAddress, RadarAutocompleteParams } from 'radar-sdk-js';
|
|
2
2
|
|
|
3
|
+
/** configuration options for the autocomplete UI widget */
|
|
3
4
|
export interface RadarAutocompleteUIOptions extends Omit<RadarAutocompleteParams, 'query'> {
|
|
5
|
+
/** element ID or HTMLElement to mount the widget into */
|
|
4
6
|
container: string | HTMLElement;
|
|
5
|
-
|
|
6
|
-
|
|
7
|
+
/** debounce delay in milliseconds before fetching results */
|
|
8
|
+
debounceMS?: number,
|
|
9
|
+
/**
|
|
10
|
+
* @deprecated use minCharacters instead
|
|
11
|
+
*/
|
|
7
12
|
threshold?: number,
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
13
|
+
/** minimum number of characters to trigger autocomplete */
|
|
14
|
+
minCharacters?: number,
|
|
15
|
+
/** placeholder text for the input field */
|
|
16
|
+
placeholder?: string,
|
|
17
|
+
/** callback invoked when a result is selected */
|
|
18
|
+
onSelection?: (selection: RadarAutocompleteAddress) => void,
|
|
19
|
+
/** callback invoked before each autocomplete request */
|
|
11
20
|
onRequest?: (params: RadarAutocompleteParams) => void,
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
/** callback invoked when results are returned */
|
|
22
|
+
onResults?: (results: RadarAutocompleteAddress[]) => void,
|
|
23
|
+
/** callback invoked on autocomplete errors */
|
|
24
|
+
onError?: (error: Error) => void,
|
|
25
|
+
/** whether the input is disabled */
|
|
14
26
|
disabled?: boolean,
|
|
27
|
+
/** whether to use responsive width (100% with optional max-width) */
|
|
15
28
|
responsive?: boolean;
|
|
29
|
+
/** fixed width or max-width (px number or CSS string) */
|
|
16
30
|
width?: string | number;
|
|
31
|
+
/** max height of the results dropdown (px number or CSS string) */
|
|
17
32
|
maxHeight?: string | number;
|
|
33
|
+
/** whether to show marker icons in results */
|
|
18
34
|
showMarkers?: boolean;
|
|
35
|
+
/** color for marker icons in results */
|
|
19
36
|
markerColor?: string;
|
|
37
|
+
/** whether to hide results when the input loses focus */
|
|
20
38
|
hideResultsOnBlur?: boolean;
|
|
21
39
|
}
|
|
22
40
|
|
|
41
|
+
/** resolved configuration with required defaults */
|
|
23
42
|
export interface RadarAutocompleteConfig extends RadarAutocompleteUIOptions {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
43
|
+
/** debounce delay in milliseconds */
|
|
44
|
+
debounceMS: number,
|
|
45
|
+
/**
|
|
46
|
+
* @deprecated use minCharacters instead
|
|
47
|
+
*/
|
|
48
|
+
threshold: number,
|
|
49
|
+
/** minimum characters to trigger autocomplete */
|
|
50
|
+
minCharacters: number,
|
|
51
|
+
/** maximum number of autocomplete results */
|
|
52
|
+
limit: number,
|
|
53
|
+
/** placeholder text for the input field */
|
|
54
|
+
placeholder: string,
|
|
55
|
+
/** whether the input is disabled */
|
|
29
56
|
disabled: boolean,
|
|
30
57
|
}
|
package/src/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export default '5.0.0-beta.
|
|
1
|
+
export default '5.0.0-beta.6';
|