@radarlabs/plugin-autocomplete 5.0.0-beta.5

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.
@@ -0,0 +1,607 @@
1
+ import { RadarAutocompleteContainerNotFound } from './errors';
2
+ import type { RadarAutocompleteUIOptions, RadarAutocompleteConfig } from './types';
3
+ import type { RadarAutocompleteParams, Location, RadarPluginContext } from 'radar-sdk-js';
4
+
5
+ const CLASSNAMES = {
6
+ WRAPPER: 'radar-autocomplete-wrapper',
7
+ INPUT: 'radar-autocomplete-input',
8
+ SEARCH_ICON: 'radar-autocomplete-search-icon',
9
+ RESULTS_LIST: 'radar-autocomplete-results-list',
10
+ RESULTS_ITEM: 'radar-autocomplete-results-item',
11
+ RESULTS_MARKER: 'radar-autocomplete-results-marker',
12
+ SELECTED_ITEM: 'radar-autocomplete-results-item-selected',
13
+ POWERED_BY_RADAR: 'radar-powered',
14
+ NO_RESULTS: 'radar-no-results',
15
+ };
16
+
17
+ const defaultAutocompleteOptions: RadarAutocompleteUIOptions = {
18
+ container: 'autocomplete',
19
+ debounceMS: 200, // Debounce time in milliseconds
20
+ minCharacters: 3, // Minimum number of characters to trigger autocomplete
21
+ limit: 8, // Maximum number of autocomplete results
22
+ placeholder: 'Search address', // Placeholder text for the input field
23
+ responsive: true,
24
+ disabled: false,
25
+ showMarkers: true,
26
+ hideResultsOnBlur: true,
27
+ };
28
+
29
+ // determine whether to use px or CSS string
30
+ const formatCSSValue = (value: string | number) => {
31
+ if (typeof value === 'number') {
32
+ return `${value}px`;
33
+ }
34
+ return value;
35
+ };
36
+
37
+ const DEFAULT_WIDTH = 400;
38
+ const setWidth = (input: HTMLElement, options: RadarAutocompleteUIOptions) => {
39
+ // if responsive and width is provided, treat it as maxWidth
40
+ if (options.responsive) {
41
+ input.style.width = '100%';
42
+ if (options.width) {
43
+ input.style.maxWidth = formatCSSValue(options.width);
44
+ }
45
+ return;
46
+ }
47
+
48
+ // if not responsive, set fixed width and unset maxWidth
49
+ input.style.width = formatCSSValue(options.width || DEFAULT_WIDTH);
50
+ input.style.removeProperty('max-width');
51
+ };
52
+
53
+ const setHeight = (resultsList: HTMLElement, options: RadarAutocompleteUIOptions) => {
54
+ if (options.maxHeight) {
55
+ resultsList.style.maxHeight = formatCSSValue(options.maxHeight);
56
+ resultsList.style.overflowY = 'auto'; /* allow overflow when maxHeight is applied */
57
+ }
58
+ };
59
+
60
+ const getMarkerIcon = (color: string = "#ACBDC8") => {
61
+ const fill = color.replace('#', '%23');
62
+ const svg = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
63
+ <path d="M12.5704 6.57036C12.5704 4.11632 10.6342 2.11257 8.21016 2C8.14262 2 8.06757 2 8.00003 2C7.93249 2 7.85744 2 7.7899 2C5.35838 2.11257 3.42967 4.11632 3.42967 6.57036C3.42967 6.60037 3.42967 6.6379 3.42967 6.66792C3.42967 6.69794 3.42967 6.73546 3.42967 6.76548C3.42967 9.46717 7.09196 13.3621 7.4672 13.7598C7.61729 13.9174 7.84994 14 8.00003 14C8.15012 14 8.38277 13.9174 8.53286 13.7598C8.9156 13.3621 12.5704 9.46717 12.5704 6.76548C12.5704 6.72795 12.5704 6.69794 12.5704 6.66792C12.5704 6.6379 12.5704 6.60037 12.5704 6.57036ZM7.99252 8.28893C7.04693 8.28893 6.27395 7.52345 6.27395 6.57036C6.27395 5.61726 7.03943 4.85178 7.99252 4.85178C8.94562 4.85178 9.7111 5.61726 9.7111 6.57036C9.7111 7.52345 8.94562 8.28893 7.99252 8.28893Z" fill="${fill}"/>
64
+ </svg>`.trim();
65
+ return `data:image/svg+xml;charset=utf-8,${svg}`;
66
+ };
67
+
68
+
69
+ class AutocompleteUI {
70
+ private ctx: RadarPluginContext;
71
+ config: RadarAutocompleteConfig;
72
+ isOpen: boolean;
73
+ results: any[];
74
+ highlightedIndex: number;
75
+ debouncedFetchResults: (...args: any[]) => Promise<any>;
76
+ near?: string;
77
+
78
+ // DOM elements
79
+ container: HTMLElement;
80
+ inputField: HTMLInputElement;
81
+ resultsList: HTMLElement;
82
+ wrapper: HTMLElement;
83
+ poweredByLink?: HTMLElement;
84
+
85
+ constructor(options: Partial<RadarAutocompleteUIOptions>, ctx: RadarPluginContext) {
86
+ this.ctx = ctx;
87
+ const { Logger } = this.ctx;
88
+ this.config = Object.assign({}, defaultAutocompleteOptions, options) as RadarAutocompleteConfig;
89
+
90
+ // setup state
91
+ this.isOpen = false;
92
+ this.debouncedFetchResults = this.debounce(this.fetchResults, this.config.debounceMS);
93
+ this.results = [];
94
+ this.highlightedIndex = -1;
95
+
96
+ // set threshold alias
97
+ if (this.config.threshold !== undefined) {
98
+ this.config.minCharacters = this.config.threshold;
99
+ Logger.warn('AutocompleteUI option "threshold" is deprecated, use "minCharacters" instead.');
100
+ }
101
+
102
+ if (options.near) {
103
+ if (typeof options.near === 'string') {
104
+ this.near = options.near;
105
+ } else {
106
+ this.near = `${options.near.latitude},${options.near.longitude}`;
107
+ }
108
+ }
109
+
110
+ // get container element
111
+ let containerEL;
112
+ if (typeof this.config.container === 'string') { // lookup container element by ID
113
+ containerEL = document.getElementById(this.config.container);
114
+ } else { // use provided element
115
+ containerEL = this.config.container; // HTMLElement
116
+ }
117
+ if (!containerEL) {
118
+ throw new RadarAutocompleteContainerNotFound(`Could not find container element: ${this.config.container}`);
119
+ }
120
+ this.container = containerEL;
121
+
122
+ // create wrapper for input and result list
123
+ this.wrapper = document.createElement('div');
124
+ this.wrapper.classList.add(CLASSNAMES.WRAPPER);
125
+ this.wrapper.style.display = this.config.responsive ? 'block' : 'inline-block';
126
+ setWidth(this.wrapper, this.config);
127
+
128
+ // result list element
129
+ this.resultsList = document.createElement('ul');
130
+ this.resultsList.classList.add(CLASSNAMES.RESULTS_LIST);
131
+ this.resultsList.setAttribute('id', CLASSNAMES.RESULTS_LIST);
132
+ this.resultsList.setAttribute('role', 'listbox');
133
+ this.resultsList.setAttribute('aria-live', 'polite');
134
+ this.resultsList.setAttribute('aria-label', 'Search results');
135
+ setHeight(this.resultsList, this.config);
136
+
137
+ if (containerEL.nodeName === 'INPUT') {
138
+ // if an <input> element is provided, use that as the inputField,
139
+ // and append the resultList to it's parent container
140
+ this.inputField = containerEL as HTMLInputElement;
141
+
142
+ // append to dom
143
+ this.wrapper.appendChild(this.resultsList);
144
+ (containerEL.parentNode as any).appendChild(this.wrapper);
145
+
146
+ } else {
147
+ // if container is not an input, create new input and append to container
148
+
149
+ // create new input
150
+ this.inputField = document.createElement('input');
151
+ this.inputField.classList.add(CLASSNAMES.INPUT);
152
+ this.inputField.placeholder = this.config.placeholder;
153
+ this.inputField.type = 'text';
154
+ this.inputField.disabled = this.config.disabled;
155
+
156
+ // search icon
157
+ const searchIcon = document.createElement('div');
158
+ searchIcon.classList.add(CLASSNAMES.SEARCH_ICON);
159
+
160
+ // append to DOM
161
+ this.wrapper.appendChild(this.inputField);
162
+ this.wrapper.appendChild(this.resultsList);
163
+ this.wrapper.appendChild(searchIcon);
164
+ this.container.appendChild(this.wrapper);
165
+ }
166
+
167
+ // disable browser autofill
168
+ this.inputField.setAttribute('autocomplete', 'off');
169
+
170
+ // set aria roles
171
+ this.inputField.setAttribute('role', 'combobox');
172
+ this.inputField.setAttribute('aria-controls', CLASSNAMES.RESULTS_LIST);
173
+ this.inputField.setAttribute('aria-expanded', 'false');
174
+ this.inputField.setAttribute('aria-haspopup', 'listbox');
175
+ this.inputField.setAttribute('aria-autocomplete', 'list');
176
+ this.inputField.setAttribute('aria-activedescendant', '');
177
+
178
+ // setup event listeners
179
+ this.inputField.addEventListener('input', this.handleInput.bind(this));
180
+ this.inputField.addEventListener('keydown', this.handleKeyboardNavigation.bind(this));
181
+ if (this.config.hideResultsOnBlur) {
182
+ this.inputField.addEventListener('blur', this.close.bind(this), true);
183
+ }
184
+
185
+ Logger.debug('AutocompleteUI initialized with options', this.config);
186
+ }
187
+
188
+ public handleInput() {
189
+ const { Logger } = this.ctx;
190
+
191
+ // Fetch autocomplete results and display them
192
+ const query = this.inputField.value;
193
+ if (query.length < this.config.minCharacters) {
194
+ return;
195
+ }
196
+
197
+ this.debouncedFetchResults(query)
198
+ .then((results: any[]) => {
199
+ const onResults = this.config.onResults;
200
+ if (onResults) {
201
+ onResults(results);
202
+ }
203
+ this.displayResults(results);
204
+ })
205
+ .catch((error) => {
206
+ Logger.warn(`Autocomplete ui error: ${error.message}`);
207
+ const onError = this.config.onError;
208
+ if (onError) {
209
+ onError(error);
210
+ }
211
+ });
212
+ }
213
+
214
+ public debounce(fn: Function, delay: number) {
215
+ let timeoutId: any;
216
+ let resolveFn: any;
217
+ let rejectFn: any;
218
+
219
+ return (...args: any[]) => {
220
+ clearTimeout(timeoutId);
221
+
222
+ timeoutId = setTimeout(() => {
223
+ const result = fn.apply(this, args);
224
+
225
+ if (result instanceof Promise) {
226
+ result
227
+ .then((value) => {
228
+ if (resolveFn) {
229
+ resolveFn(value);
230
+ }
231
+ })
232
+ .catch((error) => {
233
+ if (rejectFn) {
234
+ rejectFn(error);
235
+ }
236
+ });
237
+ }
238
+ }, delay);
239
+
240
+ return new Promise((resolve, reject) => {
241
+ resolveFn = resolve;
242
+ rejectFn = reject;
243
+ });
244
+ };
245
+ }
246
+
247
+ public async fetchResults(query: string) {
248
+ const { apis } = this.ctx;
249
+ const { limit, layers, countryCode, expandUnits, mailable, lang, postalCode, onRequest } = this.config;
250
+
251
+ const params: RadarAutocompleteParams = {
252
+ query,
253
+ limit,
254
+ layers,
255
+ countryCode,
256
+ expandUnits,
257
+ mailable,
258
+ lang,
259
+ postalCode,
260
+ }
261
+
262
+ if (this.near) {
263
+ params.near = this.near;
264
+ }
265
+
266
+ if (onRequest) {
267
+ onRequest(params);
268
+ }
269
+
270
+ const { addresses } = await apis.Search.autocomplete(params, 'autocomplete-ui');
271
+ return addresses;
272
+ }
273
+
274
+ public displayResults(results: any[]) {
275
+ // Clear the previous results
276
+ this.clearResultsList();
277
+ this.results = results;
278
+
279
+ let marker: HTMLElement;
280
+ if (this.config.showMarkers) {
281
+ marker = document.createElement('img');
282
+ marker.classList.add(CLASSNAMES.RESULTS_MARKER);
283
+ marker.setAttribute('src', getMarkerIcon(this.config.markerColor));
284
+ }
285
+
286
+ // Create and append list items for each result
287
+ results.forEach((result, index) => {
288
+ const li = document.createElement('li');
289
+ li.classList.add(CLASSNAMES.RESULTS_ITEM);
290
+ li.setAttribute('role', 'option');
291
+ li.setAttribute('id', `${CLASSNAMES.RESULTS_ITEM}}-${index}`);
292
+
293
+ // construct result with bolded label
294
+ let listContent;
295
+ if (result.formattedAddress.includes(result.addressLabel) && result.layer !== 'postalCode') {
296
+ // if addressLabel is contained in the formatted address, bold the address label
297
+ const regex = new RegExp(`(${result.addressLabel}),?`);
298
+ listContent = result.formattedAddress.replace(regex, '<b>$1</b>');
299
+ } else {
300
+ // otherwise append the address or place label to formatted address
301
+ const label = result.placeLabel || result.addressLabel;
302
+ listContent = `<b>${label}</b> ${result.formattedAddress}`;
303
+ }
304
+ li.innerHTML = listContent;
305
+
306
+ // prepend marker if enabled
307
+ if (marker) {
308
+ li.prepend(marker.cloneNode());
309
+ }
310
+
311
+ // add click handler to each result, use mousedown to fire before blur event
312
+ li.addEventListener('mousedown', () => {
313
+ this.select(index);
314
+ });
315
+
316
+ this.resultsList.appendChild(li);
317
+ });
318
+
319
+ this.open();
320
+
321
+ if (results.length > 0) {
322
+ const link = document.createElement('a');
323
+ link.href = 'https://radar.com?ref=powered_by_radar';
324
+ link.target = '_blank';
325
+ this.poweredByLink = link;
326
+
327
+ const poweredByText = document.createElement('span');
328
+ poweredByText.textContent = 'Powered by';
329
+ link.appendChild(poweredByText);
330
+
331
+ const radarLogo = document.createElement('span');
332
+ radarLogo.id = 'radar-powered-logo';
333
+ radarLogo.textContent = 'Radar';
334
+ link.appendChild(radarLogo);
335
+
336
+ const poweredByContainer = document.createElement('div');
337
+ poweredByContainer.classList.add(CLASSNAMES.POWERED_BY_RADAR);
338
+ poweredByContainer.appendChild(link);
339
+ this.resultsList.appendChild(poweredByContainer);
340
+ } else {
341
+ const noResultsText = document.createElement('div');
342
+ noResultsText.classList.add(CLASSNAMES.NO_RESULTS);
343
+ noResultsText.textContent = 'No results';
344
+
345
+ this.resultsList.appendChild(noResultsText);
346
+ }
347
+ }
348
+
349
+ public open() {
350
+ if (this.isOpen) {
351
+ return;
352
+ }
353
+
354
+ this.inputField.setAttribute('aria-expanded', 'true');
355
+ this.resultsList.removeAttribute('hidden');
356
+ this.isOpen = true;
357
+ }
358
+
359
+ public close(e?: FocusEvent) {
360
+ if (!this.isOpen) {
361
+ return;
362
+ }
363
+
364
+ // run this code async to allow link click to propagate before blur
365
+ // (add 100ms delay if closed from link click)
366
+ const linkClick = e && (e.relatedTarget === this.poweredByLink);
367
+ setTimeout(() => {
368
+ this.inputField.setAttribute('aria-expanded', 'false');
369
+ this.inputField.setAttribute('aria-activedescendant', '');
370
+ this.resultsList.setAttribute('hidden', '');
371
+ this.highlightedIndex = -1;
372
+ this.isOpen = false;
373
+ this.clearResultsList();
374
+ }, linkClick ? 100 : 0);
375
+ }
376
+
377
+ public goTo(index: number) {
378
+ if (!this.isOpen || !this.results.length) {
379
+ return;
380
+ }
381
+
382
+ // wrap around
383
+ if (index < 0) {
384
+ index = this.results.length - 1;
385
+ } else if (index >= this.results.length) {
386
+ index = 0;
387
+ }
388
+
389
+ const resultItems = this.resultsList.getElementsByTagName('li');
390
+
391
+ if (this.highlightedIndex > -1) {
392
+ // clear class names on previously highlighted item
393
+ resultItems[this.highlightedIndex].classList.remove(CLASSNAMES.SELECTED_ITEM);
394
+ }
395
+
396
+ // add class name to newly highlighted item
397
+ resultItems[index].classList.add(CLASSNAMES.SELECTED_ITEM);
398
+
399
+ // set aria active descendant
400
+ this.inputField.setAttribute('aria-activedescendant', `${CLASSNAMES.RESULTS_ITEM}-${index}`);
401
+
402
+ this.highlightedIndex = index;
403
+ }
404
+
405
+ public handleKeyboardNavigation(event: KeyboardEvent) {
406
+ let key = event.key;
407
+
408
+ // allow event to propagate if result list is not open
409
+ if (!this.isOpen) {
410
+ return;
411
+ }
412
+
413
+ // treat shift+tab as up key
414
+ if (key === 'Tab' && event.shiftKey) {
415
+ key = 'ArrowUp';
416
+ };
417
+
418
+ switch (key) {
419
+ // Next item
420
+ case 'Tab':
421
+ case 'ArrowDown':
422
+ event.preventDefault();
423
+ this.goTo(this.highlightedIndex + 1);
424
+ break;
425
+
426
+ // Prev item
427
+ case 'ArrowUp':
428
+ event.preventDefault();
429
+ this.goTo(this.highlightedIndex - 1);
430
+ break;
431
+
432
+ // Select
433
+ case 'Enter':
434
+ this.select(this.highlightedIndex);
435
+ break;
436
+
437
+ // Close
438
+ case 'Esc':
439
+ this.close();
440
+ break;
441
+ }
442
+ }
443
+
444
+ public select(index: number) {
445
+ const { Logger } = this.ctx;
446
+ const result = this.results[index];
447
+ if (!result) {
448
+ Logger.warn(`No autocomplete result found at index: ${index}`);
449
+ return;
450
+ }
451
+
452
+ let inputValue;
453
+ if (result.formattedAddress.includes(result.addressLabel)) {
454
+ inputValue = result.formattedAddress;
455
+ } else {
456
+ const label = result.placeLabel || result.addressLabel;
457
+ inputValue = `${label}, ${result.formattedAddress}`;
458
+ }
459
+ this.inputField.value = inputValue;
460
+
461
+ const onSelection = this.config.onSelection;
462
+ if (onSelection) {
463
+ onSelection(result);
464
+ }
465
+
466
+ // clear results list
467
+ this.close();
468
+ }
469
+
470
+ public clearResultsList() {
471
+ this.resultsList.innerHTML = '';
472
+ this.results = [];
473
+ }
474
+
475
+ // remove elements from DOM
476
+ public remove() {
477
+ const { Logger } = this.ctx;
478
+ Logger.debug('AutocompleteUI removed.');
479
+ this.inputField.remove();
480
+ this.resultsList.remove();
481
+ this.wrapper.remove();
482
+ }
483
+
484
+ public setNear(near: string | Location | undefined | null) {
485
+ if (near === undefined || near === null) {
486
+ this.near = undefined;
487
+ } else if (typeof near === 'string') {
488
+ this.near = near;
489
+ } else {
490
+ this.near = `${near.latitude},${near.longitude}`;
491
+ }
492
+ return this;
493
+ }
494
+
495
+ public setPlaceholder(placeholder: string) {
496
+ this.config.placeholder = placeholder;
497
+ this.inputField.placeholder = placeholder;
498
+ return this;
499
+ }
500
+
501
+ public setDisabled(disabled: boolean) {
502
+ this.config.disabled = disabled;
503
+ this.inputField.disabled = disabled;
504
+ return this;
505
+ }
506
+
507
+ public setResponsive(responsive: boolean) {
508
+ this.config.responsive = responsive;
509
+ setWidth(this.wrapper, this.config);
510
+ return this;
511
+ }
512
+
513
+ public setWidth(width: number | string | null) {
514
+ if (width === null) {
515
+ this.config.width = undefined;
516
+ } else if (typeof width === 'string' || typeof width === 'number') {
517
+ this.config.width = width;
518
+ }
519
+ setWidth(this.wrapper, this.config);
520
+ return this;
521
+ }
522
+
523
+ public setMaxHeight(height: number | string | null) {
524
+ if (height === null) {
525
+ this.config.maxHeight = undefined;
526
+ } else if (typeof height === 'string' || typeof height === 'number') {
527
+ this.config.maxHeight = height;
528
+ }
529
+ setHeight(this.resultsList, this.config);
530
+ return this;
531
+ }
532
+
533
+ public setMinCharacters(minCharacters: number) {
534
+ this.config.minCharacters = minCharacters;
535
+ this.config.threshold = minCharacters;
536
+ return this;
537
+ }
538
+
539
+ public setLimit(limit: number) {
540
+ this.config.limit = limit;
541
+ return this;
542
+ }
543
+
544
+ public setLang(lang: string | null) {
545
+ if (lang === null) {
546
+ this.config.lang = undefined;
547
+ } else if (typeof lang === 'string') {
548
+ this.config.lang = lang;
549
+ }
550
+ return this;
551
+ }
552
+
553
+ public setPostalCode(postalCode: string | null) {
554
+ if (postalCode === null) {
555
+ this.config.postalCode = undefined;
556
+ } else if (typeof postalCode === 'string') {
557
+ this.config.postalCode = postalCode;
558
+ }
559
+ return this;
560
+ }
561
+
562
+ public setShowMarkers(showMarkers: boolean) {
563
+ this.config.showMarkers = showMarkers;
564
+ if (showMarkers) {
565
+ const marker = document.createElement('img');
566
+ marker.classList.add(CLASSNAMES.RESULTS_MARKER);
567
+ marker.setAttribute('src', getMarkerIcon(this.config.markerColor));
568
+ const resultItems = this.resultsList.getElementsByTagName('li');
569
+ for (let i = 0; i < resultItems.length; i++) {
570
+ const currentMarker = resultItems[i].getElementsByClassName(CLASSNAMES.RESULTS_MARKER)[0];
571
+ if (!currentMarker) {
572
+ resultItems[i].prepend(marker.cloneNode());
573
+ }
574
+ }
575
+ } else {
576
+ const resultItems = this.resultsList.getElementsByTagName('li');
577
+ for (let i = 0; i < resultItems.length; i++) {
578
+ const marker = resultItems[i].getElementsByClassName(CLASSNAMES.RESULTS_MARKER)[0];
579
+ if (marker) {
580
+ marker.remove();
581
+ }
582
+ }
583
+ }
584
+ return this;
585
+ }
586
+
587
+ public setMarkerColor(color: string) {
588
+ this.config.markerColor = color;
589
+ const marker = this.resultsList.getElementsByClassName(CLASSNAMES.RESULTS_MARKER);
590
+ for (let i = 0; i < marker.length; i++) {
591
+ marker[i].setAttribute('src', getMarkerIcon(color));
592
+ }
593
+ return this;
594
+ }
595
+
596
+ public setHideResultsOnBlur(hideResultsOnBlur: boolean) {
597
+ this.config.hideResultsOnBlur = hideResultsOnBlur;
598
+ if (hideResultsOnBlur) {
599
+ this.inputField.addEventListener('blur', this.close.bind(this), true);
600
+ } else {
601
+ this.inputField.removeEventListener('blur', this.close.bind(this), true);
602
+ }
603
+ return this;
604
+ }
605
+ }
606
+
607
+ export default AutocompleteUI;
@@ -0,0 +1,4 @@
1
+ import { createAutocompletePlugin } from './index';
2
+ import Radar from "radar-sdk-js"
3
+
4
+ Radar.registerPlugin(createAutocompletePlugin());
package/src/errors.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { RadarError } from 'radar-sdk-js';
2
+
3
+ export class RadarAutocompleteContainerNotFound extends RadarError {
4
+ constructor(message: string) {
5
+ super(message);
6
+ this.name = 'RadarAutocompleteContainerNotFound';
7
+ this.status = 'CONTAINER_NOT_FOUND';
8
+ }
9
+ }
package/src/index.ts ADDED
@@ -0,0 +1,33 @@
1
+ import type { RadarPlugin } from 'radar-sdk-js';
2
+
3
+ import AutocompleteUI from './autocomplete';
4
+ import version from './version';
5
+ import type { RadarAutocompleteUIOptions } from './types';
6
+
7
+ import '../styles/radar-autocomplete.css';
8
+
9
+ export type { RadarAutocompleteUIOptions, RadarAutocompleteConfig } from './types';
10
+
11
+ declare module 'radar-sdk-js' {
12
+ interface RadarUI {
13
+ autocomplete(options: Partial<RadarAutocompleteUIOptions>): AutocompleteUI;
14
+ }
15
+ namespace Radar {
16
+ let ui: RadarUI;
17
+ }
18
+ }
19
+
20
+ export function createAutocompletePlugin(): RadarPlugin {
21
+ return {
22
+ name: 'autocomplete',
23
+ version,
24
+ install(ctx) {
25
+ const existing = ctx.Radar.ui || {};
26
+ // NOTE(jasonl): we're merging with the existing ui namespace since other plugins also add to it like maps
27
+ ctx.Radar.ui = {
28
+ ...existing,
29
+ autocomplete: (options: Partial<RadarAutocompleteUIOptions>) => new AutocompleteUI(options, ctx),
30
+ };
31
+ },
32
+ };
33
+ }
package/src/types.ts ADDED
@@ -0,0 +1,30 @@
1
+ import type { RadarAutocompleteParams } from 'radar-sdk-js';
2
+
3
+ export interface RadarAutocompleteUIOptions extends Omit<RadarAutocompleteParams, 'query'> {
4
+ container: string | HTMLElement;
5
+ debounceMS?: number, // Debounce time in milliseconds
6
+ /** @deprecated use minCharacters instead */
7
+ threshold?: number,
8
+ minCharacters?: number, // Minimum number of characters to trigger autocomplete
9
+ placeholder?: string, // Placeholder text for the input field
10
+ onSelection?: (selection: any) => void,
11
+ onRequest?: (params: RadarAutocompleteParams) => void,
12
+ onResults?: (results: any[]) => void,
13
+ onError?: (error: any) => void,
14
+ disabled?: boolean,
15
+ responsive?: boolean;
16
+ width?: string | number;
17
+ maxHeight?: string | number;
18
+ showMarkers?: boolean;
19
+ markerColor?: string;
20
+ hideResultsOnBlur?: boolean;
21
+ }
22
+
23
+ export interface RadarAutocompleteConfig extends RadarAutocompleteUIOptions {
24
+ debounceMS: number, // Debounce time in milliseconds
25
+ threshold: number, // DEPRECATED(use minCharacters instead)
26
+ minCharacters: number, // Minimum number of characters to trigger autocomplete
27
+ limit: number, // Maximum number of autocomplete results
28
+ placeholder: string, // Placeholder text for the input field
29
+ disabled: boolean,
30
+ }
package/src/version.ts ADDED
@@ -0,0 +1 @@
1
+ export default '5.0.0-beta.5';