@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.
package/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # @radarlabs/plugin-autocomplete
2
+
3
+ Autocomplete UI plugin for
4
+ [radar-sdk-js](https://www.npmjs.com/package/radar-sdk-js). Provides a
5
+ drop-in `Radar.ui.autocomplete()` widget with search-as-you-type address
6
+ results powered by the
7
+ [Radar Autocomplete API](https://radar.com/documentation/api#autocomplete).
8
+
9
+ ## Installation
10
+
11
+ ### With npm
12
+
13
+ ```bash
14
+ npm install radar-sdk-js @radarlabs/plugin-autocomplete
15
+ ```
16
+
17
+ ```js
18
+ import Radar from 'radar-sdk-js';
19
+ import { createAutocompletePlugin } from '@radarlabs/plugin-autocomplete';
20
+ import '@radarlabs/plugin-autocomplete/dist/radar-autocomplete.css';
21
+
22
+ Radar.registerPlugin(createAutocompletePlugin());
23
+ Radar.initialize('prj_test_pk_...');
24
+ ```
25
+
26
+ ### With a script tag
27
+
28
+ Load the core SDK first, then the autocomplete plugin. The CDN bundle
29
+ auto-registers with the core SDK.
30
+
31
+ ```html
32
+ <link href="https://js.radar.com/autocomplete/v5.0.0-beta.4/radar-autocomplete.css" rel="stylesheet">
33
+ <script src="https://js.radar.com/v5.0.0-beta.3/radar.min.js"></script>
34
+ <script src="https://js.radar.com/autocomplete/v5.0.0-beta.4/radar-autocomplete.min.js"></script>
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Create an autocomplete input
40
+
41
+ Pass a container element ID (or an `HTMLElement` reference) to render the
42
+ widget:
43
+
44
+ ```js
45
+ const autocomplete = Radar.ui.autocomplete({
46
+ container: 'autocomplete',
47
+ onSelection: (result) => {
48
+ console.log(result);
49
+ },
50
+ });
51
+ ```
52
+
53
+ You can also pass an existing `<input>` element as the container. In that
54
+ case, the widget attaches the results dropdown to the input instead of
55
+ creating a new one.
56
+
57
+ ### Options
58
+
59
+ | Option | Type | Default | Description |
60
+ |--------|------|---------|-------------|
61
+ | `container` | `string \| HTMLElement` | `'autocomplete'` | Container element or ID |
62
+ | `onSelection` | `(result) => void` | — | Called when the user selects a result |
63
+ | `onResults` | `(results) => void` | — | Called after results are fetched |
64
+ | `onRequest` | `(params) => void` | — | Called before each API request |
65
+ | `onError` | `(error) => void` | — | Called on fetch errors |
66
+ | `placeholder` | `string` | `'Search address'` | Input placeholder text |
67
+ | `minCharacters` | `number` | `3` | Minimum characters before searching |
68
+ | `debounceMS` | `number` | `200` | Debounce delay in milliseconds |
69
+ | `limit` | `number` | `8` | Maximum number of results |
70
+ | `responsive` | `boolean` | `true` | Use 100% width (with optional `maxWidth`) |
71
+ | `width` | `string \| number` | `400` | Fixed width, or max-width if responsive |
72
+ | `maxHeight` | `string \| number` | — | Max height for the results dropdown |
73
+ | `disabled` | `boolean` | `false` | Disable the input |
74
+ | `showMarkers` | `boolean` | `true` | Show marker icons next to results |
75
+ | `markerColor` | `string` | `'#ACBDC8'` | Color of result marker icons |
76
+ | `hideResultsOnBlur` | `boolean` | `true` | Close results when input loses focus |
77
+ | `near` | `Location \| string` | — | Bias results near a location |
78
+ | `layers` | `string[]` | — | Filter by geocode layers |
79
+ | `countryCode` | `string` | — | Filter results by country |
80
+ | `lang` | `string` | — | Language for results |
81
+ | `postalCode` | `string` | — | Filter results by postal code |
82
+ | `mailable` | `boolean` | — | Only return mailable addresses |
83
+
84
+ ### Chainable setters
85
+
86
+ All setters return the `AutocompleteUI` instance for chaining:
87
+
88
+ ```js
89
+ autocomplete
90
+ .setNear({ latitude: 40.735, longitude: -73.991 })
91
+ .setPlaceholder('Enter an address')
92
+ .setMinCharacters(2)
93
+ .setLimit(5)
94
+ .setWidth('500px')
95
+ .setResponsive(true);
96
+ ```
97
+
98
+ Available setters: `setNear()`, `setPlaceholder()`, `setDisabled()`,
99
+ `setResponsive()`, `setWidth()`, `setMaxHeight()`, `setMinCharacters()`,
100
+ `setLimit()`, `setLang()`, `setPostalCode()`, `setShowMarkers()`,
101
+ `setMarkerColor()`, `setHideResultsOnBlur()`.
102
+
103
+ ### Remove the widget
104
+
105
+ Call `remove()` to clean up the widget and its DOM elements:
106
+
107
+ ```js
108
+ autocomplete.remove();
109
+ ```
110
+
111
+ ## Accessibility
112
+
113
+ The autocomplete widget includes ARIA attributes for screen readers:
114
+
115
+ - `role="combobox"` on the input
116
+ - `role="listbox"` on the results list
117
+ - `aria-live="polite"` for dynamic result announcements
118
+ - Keyboard navigation with Arrow keys, Tab, Enter, and Escape
119
+
120
+ ## Peer dependencies
121
+
122
+ | Package | Version |
123
+ |---------|---------|
124
+ | `radar-sdk-js` | `>=5.0.0-beta.1` |
125
+
126
+ ## Support
127
+
128
+ Have questions? We're here to help! Email us at [support@radar.com](mailto:support@radar.com).
@@ -0,0 +1 @@
1
+ :root{--radar-gray1:#f6fafc;--radar-gray2:#eaf1f6;--radar-gray3:#dbe5eb;--radar-gray5:#acbdc8;--radar-gray6:#5a6872;--radar-gray8:#051723;--radar-sapphire:#0005fb;--radar-midnight:#000257}@font-face{font-family:Graphik;font-weight:400;src:url(https://static.radar.com/fonts/Graphik-Regular.woff) format("woff")}@font-face{font-family:Graphik;font-weight:600;src:url(https://static.radar.com/fonts/Graphik-Semibold.woff) format("woff")}.radar-autocomplete-wrapper{font-family:Graphik,sans-serif;position:relative}.radar-autocomplete-input{border:1px solid var(--radar-gray3);border-radius:4px;box-sizing:border-box;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;color:var(--radar-gray8);font-size:1rem;font-weight:400;height:2.25rem;line-height:1.5;margin:0;outline:none;padding:.375rem .75rem .375rem 40px;text-overflow:ellipsis;transition:all .3s ease;-webkit-transition:all -webkit-transform .3s ease;width:100%}.radar-autocomplete-input:disabled,.radar-autocomplete-input:hover:disabled{background:var(--radar-gray3);cursor:not-allowed;opacity:1}.radar-autocomplete-input::placeholder{color:var(--radar-gray5);transition:all .3s ease;-webkit-transition:all -webkit-transform .3s ease}.radar-autocomplete-input:focus::placeholder{font-size:15.5px;opacity:.7;padding:.1rem .4rem}.radar-autocomplete-input:hover::placeholder{opacity:.8;transition:all .3s ease;-webkit-transition:all -webkit-transform .3s ease}.radar-autocomplete-input:focus{border:1px solid var(--radar-gray3);box-shadow:0 0 4px #81beff;opacity:1}.radar-autocomplete-search-icon{background:url("data:image/svg+xml;charset=utf-8,%3Csvg width='12' height='12' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11.78 10.717 9.989 8.925l-.54-.54a5.187 5.187 0 0 0 1.05-3.135A5.255 5.255 0 0 0 5.25 0 5.255 5.255 0 0 0 0 5.25a5.255 5.255 0 0 0 5.25 5.25 5.217 5.217 0 0 0 3.134-1.05l1.26 1.26 1.072 1.072c.142.15.337.218.532.218a.747.747 0 0 0 .532-1.275v-.008ZM1.5 5.25c0-2.07 1.68-3.75 3.75-3.75a3.751 3.751 0 0 1 0 7.5C3.18 9 1.5 7.32 1.5 5.25Z' fill='%23ACBDC8'/%3E%3C/svg%3E");display:block;height:12px;left:16px;position:absolute;top:12px;width:12px}.radar-autocomplete-results-list{background-color:#fff;border:1px solid var(--radar-gray3);border-radius:4px;box-shadow:0 4px 12px rgba(5,23,35,.1);box-sizing:border-box;left:0;list-style:none;margin:.5rem 0 0;outline:none;padding:8px 0 0;position:absolute;right:0;transition:opacity .15s ease-in-out;-moz-transition:opacity .15s ease-in-out;-webkit-transition:opacity .15s ease-in-out;z-index:1}.radar-autocomplete-results-list:empty,.radar-autocomplete-results-list[hidden]{display:block;opacity:0;transform:scale(0)}.radar-autocomplete-results-item{align-items:center;color:var(--radar-gray6);display:flex;font-size:14px;line-height:24px;overflow:hidden;padding:8px 16px;text-align:left;text-overflow:ellipsis;transition:all .2s ease;white-space:nowrap}.radar-autocomplete-results-item b{color:var(--radar-gray8);font-weight:600;margin-right:8px}.radar-autocomplete-results-marker{margin-right:16px}.radar-autocomplete-results-item:hover{background-color:var(--radar-gray1);cursor:pointer}.radar-autocomplete-results-item-selected,.radar-autocomplete-results-item[aria-selected=true]{background-color:var(--radar-gray2)}.radar-powered{align-items:center;color:var(--radar-gray6);display:flex;font-size:10px;font-style:normal;font-weight:400;justify-content:flex-end;line-height:10px;padding:8px 16px}.radar-powered a{text-decoration:none!important}.radar-powered a,.radar-powered a:visited{color:var(--radar-gray6)!important}.radar-powered #radar-powered-logo{background:url("data:image/svg+xml;charset=utf-8,%3Csvg width='40' height='10' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill-rule='evenodd' clip-rule='evenodd' d='M11.462 8.836V1.207h2.832c1.89 0 2.942.81 2.942 2.422v.043c0 1.088-.546 1.696-1.304 2.006l1.73 3.158h-2.185L14.01 6.104h-.486v2.732h-2.063Zm2.063-4.15h.668c.697 0 1.041-.31 1.041-.929v-.043c0-.618-.364-.864-1.031-.864h-.678v1.835Zm6.26 4.278c-.95 0-1.79-.491-1.79-1.675 0-1.313 1.052-1.825 2.811-1.825h.506v-.128c0-.533-.122-.885-.698-.885-.495 0-.678.309-.718.661h-1.699c.081-1.344 1.082-2.006 2.528-2.006 1.456 0 2.387.63 2.387 2.113v3.617h-1.76v-.651c-.253.405-.718.779-1.567.779Zm.627-1.291c.515 0 .9-.31.9-.79v-.341h-.476c-.728 0-1.051.15-1.051.608 0 .32.202.523.627.523Zm5.693 1.29c-1.223 0-2.225-.927-2.225-2.859V6.02c0-1.9.992-2.913 2.245-2.913.83 0 1.325.374 1.629.94V.801h1.81v8.034h-1.81v-.907a1.774 1.774 0 0 1-1.649 1.035Zm.637-1.46c.658-.001 1.062-.481 1.062-1.43v-.086c0-.939-.374-1.44-1.041-1.44-.678 0-1.042.48-1.042 1.45v.086c0 .939.394 1.419 1.021 1.419Zm5.38 1.46c-.95 0-1.79-.49-1.79-1.674 0-1.313 1.052-1.825 2.812-1.825h.505v-.128c0-.533-.121-.885-.697-.885-.496 0-.678.309-.718.661h-1.7c.082-1.344 1.083-2.006 2.529-2.006 1.456 0 2.386.63 2.386 2.113v3.617h-1.76v-.651c-.252.405-.717.779-1.567.779Zm.627-1.29c.516 0 .9-.31.9-.79v-.341h-.475c-.728 0-1.052.15-1.052.608 0 .32.203.523.627.523Zm3.732 1.163h1.81V6.19c0-.897.607-1.26 1.709-1.227V3.18c-.82-.01-1.375.352-1.709 1.184v-1.11h-1.81v5.58ZM3.871 0 0 9.99l3.871-1.677L7.743 10 3.87 0Z' fill='%235A6872'/%3E%3C/svg%3E");color:transparent;display:inline-block;height:10px;margin-left:4px;width:40px}.radar-no-results{padding:8px 16px}
@@ -0,0 +1,546 @@
1
+ (function (Radar) {
2
+ 'use strict';
3
+
4
+ class RadarAutocompleteContainerNotFound extends Radar.RadarError {
5
+ constructor(message) {
6
+ super(message);
7
+ this.name = 'RadarAutocompleteContainerNotFound';
8
+ this.status = 'CONTAINER_NOT_FOUND';
9
+ }
10
+ }
11
+
12
+ const CLASSNAMES = {
13
+ WRAPPER: 'radar-autocomplete-wrapper',
14
+ INPUT: 'radar-autocomplete-input',
15
+ SEARCH_ICON: 'radar-autocomplete-search-icon',
16
+ RESULTS_LIST: 'radar-autocomplete-results-list',
17
+ RESULTS_ITEM: 'radar-autocomplete-results-item',
18
+ RESULTS_MARKER: 'radar-autocomplete-results-marker',
19
+ SELECTED_ITEM: 'radar-autocomplete-results-item-selected',
20
+ POWERED_BY_RADAR: 'radar-powered',
21
+ NO_RESULTS: 'radar-no-results',
22
+ };
23
+ const defaultAutocompleteOptions = {
24
+ container: 'autocomplete',
25
+ debounceMS: 200, // Debounce time in milliseconds
26
+ minCharacters: 3, // Minimum number of characters to trigger autocomplete
27
+ limit: 8, // Maximum number of autocomplete results
28
+ placeholder: 'Search address', // Placeholder text for the input field
29
+ responsive: true,
30
+ disabled: false,
31
+ showMarkers: true,
32
+ hideResultsOnBlur: true,
33
+ };
34
+ // determine whether to use px or CSS string
35
+ const formatCSSValue = (value) => {
36
+ if (typeof value === 'number') {
37
+ return `${value}px`;
38
+ }
39
+ return value;
40
+ };
41
+ const DEFAULT_WIDTH = 400;
42
+ const setWidth = (input, options) => {
43
+ // if responsive and width is provided, treat it as maxWidth
44
+ if (options.responsive) {
45
+ input.style.width = '100%';
46
+ if (options.width) {
47
+ input.style.maxWidth = formatCSSValue(options.width);
48
+ }
49
+ return;
50
+ }
51
+ // if not responsive, set fixed width and unset maxWidth
52
+ input.style.width = formatCSSValue(options.width || DEFAULT_WIDTH);
53
+ input.style.removeProperty('max-width');
54
+ };
55
+ const setHeight = (resultsList, options) => {
56
+ if (options.maxHeight) {
57
+ resultsList.style.maxHeight = formatCSSValue(options.maxHeight);
58
+ resultsList.style.overflowY = 'auto'; /* allow overflow when maxHeight is applied */
59
+ }
60
+ };
61
+ const getMarkerIcon = (color = "#ACBDC8") => {
62
+ const fill = color.replace('#', '%23');
63
+ const svg = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
64
+ <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}"/>
65
+ </svg>`.trim();
66
+ return `data:image/svg+xml;charset=utf-8,${svg}`;
67
+ };
68
+ class AutocompleteUI {
69
+ constructor(options, ctx) {
70
+ this.ctx = ctx;
71
+ const { Logger } = this.ctx;
72
+ this.config = Object.assign({}, defaultAutocompleteOptions, options);
73
+ // setup state
74
+ this.isOpen = false;
75
+ this.debouncedFetchResults = this.debounce(this.fetchResults, this.config.debounceMS);
76
+ this.results = [];
77
+ this.highlightedIndex = -1;
78
+ // set threshold alias
79
+ if (this.config.threshold !== undefined) {
80
+ this.config.minCharacters = this.config.threshold;
81
+ Logger.warn('AutocompleteUI option "threshold" is deprecated, use "minCharacters" instead.');
82
+ }
83
+ if (options.near) {
84
+ if (typeof options.near === 'string') {
85
+ this.near = options.near;
86
+ }
87
+ else {
88
+ this.near = `${options.near.latitude},${options.near.longitude}`;
89
+ }
90
+ }
91
+ // get container element
92
+ let containerEL;
93
+ if (typeof this.config.container === 'string') { // lookup container element by ID
94
+ containerEL = document.getElementById(this.config.container);
95
+ }
96
+ else { // use provided element
97
+ containerEL = this.config.container; // HTMLElement
98
+ }
99
+ if (!containerEL) {
100
+ throw new RadarAutocompleteContainerNotFound(`Could not find container element: ${this.config.container}`);
101
+ }
102
+ this.container = containerEL;
103
+ // create wrapper for input and result list
104
+ this.wrapper = document.createElement('div');
105
+ this.wrapper.classList.add(CLASSNAMES.WRAPPER);
106
+ this.wrapper.style.display = this.config.responsive ? 'block' : 'inline-block';
107
+ setWidth(this.wrapper, this.config);
108
+ // result list element
109
+ this.resultsList = document.createElement('ul');
110
+ this.resultsList.classList.add(CLASSNAMES.RESULTS_LIST);
111
+ this.resultsList.setAttribute('id', CLASSNAMES.RESULTS_LIST);
112
+ this.resultsList.setAttribute('role', 'listbox');
113
+ this.resultsList.setAttribute('aria-live', 'polite');
114
+ this.resultsList.setAttribute('aria-label', 'Search results');
115
+ setHeight(this.resultsList, this.config);
116
+ if (containerEL.nodeName === 'INPUT') {
117
+ // if an <input> element is provided, use that as the inputField,
118
+ // and append the resultList to it's parent container
119
+ this.inputField = containerEL;
120
+ // append to dom
121
+ this.wrapper.appendChild(this.resultsList);
122
+ containerEL.parentNode.appendChild(this.wrapper);
123
+ }
124
+ else {
125
+ // if container is not an input, create new input and append to container
126
+ // create new input
127
+ this.inputField = document.createElement('input');
128
+ this.inputField.classList.add(CLASSNAMES.INPUT);
129
+ this.inputField.placeholder = this.config.placeholder;
130
+ this.inputField.type = 'text';
131
+ this.inputField.disabled = this.config.disabled;
132
+ // search icon
133
+ const searchIcon = document.createElement('div');
134
+ searchIcon.classList.add(CLASSNAMES.SEARCH_ICON);
135
+ // append to DOM
136
+ this.wrapper.appendChild(this.inputField);
137
+ this.wrapper.appendChild(this.resultsList);
138
+ this.wrapper.appendChild(searchIcon);
139
+ this.container.appendChild(this.wrapper);
140
+ }
141
+ // disable browser autofill
142
+ this.inputField.setAttribute('autocomplete', 'off');
143
+ // set aria roles
144
+ this.inputField.setAttribute('role', 'combobox');
145
+ this.inputField.setAttribute('aria-controls', CLASSNAMES.RESULTS_LIST);
146
+ this.inputField.setAttribute('aria-expanded', 'false');
147
+ this.inputField.setAttribute('aria-haspopup', 'listbox');
148
+ this.inputField.setAttribute('aria-autocomplete', 'list');
149
+ this.inputField.setAttribute('aria-activedescendant', '');
150
+ // setup event listeners
151
+ this.inputField.addEventListener('input', this.handleInput.bind(this));
152
+ this.inputField.addEventListener('keydown', this.handleKeyboardNavigation.bind(this));
153
+ if (this.config.hideResultsOnBlur) {
154
+ this.inputField.addEventListener('blur', this.close.bind(this), true);
155
+ }
156
+ Logger.debug('AutocompleteUI initialized with options', this.config);
157
+ }
158
+ handleInput() {
159
+ const { Logger } = this.ctx;
160
+ // Fetch autocomplete results and display them
161
+ const query = this.inputField.value;
162
+ if (query.length < this.config.minCharacters) {
163
+ return;
164
+ }
165
+ this.debouncedFetchResults(query)
166
+ .then((results) => {
167
+ const onResults = this.config.onResults;
168
+ if (onResults) {
169
+ onResults(results);
170
+ }
171
+ this.displayResults(results);
172
+ })
173
+ .catch((error) => {
174
+ Logger.warn(`Autocomplete ui error: ${error.message}`);
175
+ const onError = this.config.onError;
176
+ if (onError) {
177
+ onError(error);
178
+ }
179
+ });
180
+ }
181
+ debounce(fn, delay) {
182
+ let timeoutId;
183
+ let resolveFn;
184
+ let rejectFn;
185
+ return (...args) => {
186
+ clearTimeout(timeoutId);
187
+ timeoutId = setTimeout(() => {
188
+ const result = fn.apply(this, args);
189
+ if (result instanceof Promise) {
190
+ result
191
+ .then((value) => {
192
+ if (resolveFn) {
193
+ resolveFn(value);
194
+ }
195
+ })
196
+ .catch((error) => {
197
+ if (rejectFn) {
198
+ rejectFn(error);
199
+ }
200
+ });
201
+ }
202
+ }, delay);
203
+ return new Promise((resolve, reject) => {
204
+ resolveFn = resolve;
205
+ rejectFn = reject;
206
+ });
207
+ };
208
+ }
209
+ async fetchResults(query) {
210
+ const { apis } = this.ctx;
211
+ const { limit, layers, countryCode, expandUnits, mailable, lang, postalCode, onRequest } = this.config;
212
+ const params = {
213
+ query,
214
+ limit,
215
+ layers,
216
+ countryCode,
217
+ expandUnits,
218
+ mailable,
219
+ lang,
220
+ postalCode,
221
+ };
222
+ if (this.near) {
223
+ params.near = this.near;
224
+ }
225
+ if (onRequest) {
226
+ onRequest(params);
227
+ }
228
+ const { addresses } = await apis.Search.autocomplete(params, 'autocomplete-ui');
229
+ return addresses;
230
+ }
231
+ displayResults(results) {
232
+ // Clear the previous results
233
+ this.clearResultsList();
234
+ this.results = results;
235
+ let marker;
236
+ if (this.config.showMarkers) {
237
+ marker = document.createElement('img');
238
+ marker.classList.add(CLASSNAMES.RESULTS_MARKER);
239
+ marker.setAttribute('src', getMarkerIcon(this.config.markerColor));
240
+ }
241
+ // Create and append list items for each result
242
+ results.forEach((result, index) => {
243
+ const li = document.createElement('li');
244
+ li.classList.add(CLASSNAMES.RESULTS_ITEM);
245
+ li.setAttribute('role', 'option');
246
+ li.setAttribute('id', `${CLASSNAMES.RESULTS_ITEM}}-${index}`);
247
+ // construct result with bolded label
248
+ let listContent;
249
+ if (result.formattedAddress.includes(result.addressLabel) && result.layer !== 'postalCode') {
250
+ // if addressLabel is contained in the formatted address, bold the address label
251
+ const regex = new RegExp(`(${result.addressLabel}),?`);
252
+ listContent = result.formattedAddress.replace(regex, '<b>$1</b>');
253
+ }
254
+ else {
255
+ // otherwise append the address or place label to formatted address
256
+ const label = result.placeLabel || result.addressLabel;
257
+ listContent = `<b>${label}</b> ${result.formattedAddress}`;
258
+ }
259
+ li.innerHTML = listContent;
260
+ // prepend marker if enabled
261
+ if (marker) {
262
+ li.prepend(marker.cloneNode());
263
+ }
264
+ // add click handler to each result, use mousedown to fire before blur event
265
+ li.addEventListener('mousedown', () => {
266
+ this.select(index);
267
+ });
268
+ this.resultsList.appendChild(li);
269
+ });
270
+ this.open();
271
+ if (results.length > 0) {
272
+ const link = document.createElement('a');
273
+ link.href = 'https://radar.com?ref=powered_by_radar';
274
+ link.target = '_blank';
275
+ this.poweredByLink = link;
276
+ const poweredByText = document.createElement('span');
277
+ poweredByText.textContent = 'Powered by';
278
+ link.appendChild(poweredByText);
279
+ const radarLogo = document.createElement('span');
280
+ radarLogo.id = 'radar-powered-logo';
281
+ radarLogo.textContent = 'Radar';
282
+ link.appendChild(radarLogo);
283
+ const poweredByContainer = document.createElement('div');
284
+ poweredByContainer.classList.add(CLASSNAMES.POWERED_BY_RADAR);
285
+ poweredByContainer.appendChild(link);
286
+ this.resultsList.appendChild(poweredByContainer);
287
+ }
288
+ else {
289
+ const noResultsText = document.createElement('div');
290
+ noResultsText.classList.add(CLASSNAMES.NO_RESULTS);
291
+ noResultsText.textContent = 'No results';
292
+ this.resultsList.appendChild(noResultsText);
293
+ }
294
+ }
295
+ open() {
296
+ if (this.isOpen) {
297
+ return;
298
+ }
299
+ this.inputField.setAttribute('aria-expanded', 'true');
300
+ this.resultsList.removeAttribute('hidden');
301
+ this.isOpen = true;
302
+ }
303
+ close(e) {
304
+ if (!this.isOpen) {
305
+ return;
306
+ }
307
+ // run this code async to allow link click to propagate before blur
308
+ // (add 100ms delay if closed from link click)
309
+ const linkClick = e && (e.relatedTarget === this.poweredByLink);
310
+ setTimeout(() => {
311
+ this.inputField.setAttribute('aria-expanded', 'false');
312
+ this.inputField.setAttribute('aria-activedescendant', '');
313
+ this.resultsList.setAttribute('hidden', '');
314
+ this.highlightedIndex = -1;
315
+ this.isOpen = false;
316
+ this.clearResultsList();
317
+ }, linkClick ? 100 : 0);
318
+ }
319
+ goTo(index) {
320
+ if (!this.isOpen || !this.results.length) {
321
+ return;
322
+ }
323
+ // wrap around
324
+ if (index < 0) {
325
+ index = this.results.length - 1;
326
+ }
327
+ else if (index >= this.results.length) {
328
+ index = 0;
329
+ }
330
+ const resultItems = this.resultsList.getElementsByTagName('li');
331
+ if (this.highlightedIndex > -1) {
332
+ // clear class names on previously highlighted item
333
+ resultItems[this.highlightedIndex].classList.remove(CLASSNAMES.SELECTED_ITEM);
334
+ }
335
+ // add class name to newly highlighted item
336
+ resultItems[index].classList.add(CLASSNAMES.SELECTED_ITEM);
337
+ // set aria active descendant
338
+ this.inputField.setAttribute('aria-activedescendant', `${CLASSNAMES.RESULTS_ITEM}-${index}`);
339
+ this.highlightedIndex = index;
340
+ }
341
+ handleKeyboardNavigation(event) {
342
+ let key = event.key;
343
+ // allow event to propagate if result list is not open
344
+ if (!this.isOpen) {
345
+ return;
346
+ }
347
+ // treat shift+tab as up key
348
+ if (key === 'Tab' && event.shiftKey) {
349
+ key = 'ArrowUp';
350
+ }
351
+ switch (key) {
352
+ // Next item
353
+ case 'Tab':
354
+ case 'ArrowDown':
355
+ event.preventDefault();
356
+ this.goTo(this.highlightedIndex + 1);
357
+ break;
358
+ // Prev item
359
+ case 'ArrowUp':
360
+ event.preventDefault();
361
+ this.goTo(this.highlightedIndex - 1);
362
+ break;
363
+ // Select
364
+ case 'Enter':
365
+ this.select(this.highlightedIndex);
366
+ break;
367
+ // Close
368
+ case 'Esc':
369
+ this.close();
370
+ break;
371
+ }
372
+ }
373
+ select(index) {
374
+ const { Logger } = this.ctx;
375
+ const result = this.results[index];
376
+ if (!result) {
377
+ Logger.warn(`No autocomplete result found at index: ${index}`);
378
+ return;
379
+ }
380
+ let inputValue;
381
+ if (result.formattedAddress.includes(result.addressLabel)) {
382
+ inputValue = result.formattedAddress;
383
+ }
384
+ else {
385
+ const label = result.placeLabel || result.addressLabel;
386
+ inputValue = `${label}, ${result.formattedAddress}`;
387
+ }
388
+ this.inputField.value = inputValue;
389
+ const onSelection = this.config.onSelection;
390
+ if (onSelection) {
391
+ onSelection(result);
392
+ }
393
+ // clear results list
394
+ this.close();
395
+ }
396
+ clearResultsList() {
397
+ this.resultsList.innerHTML = '';
398
+ this.results = [];
399
+ }
400
+ // remove elements from DOM
401
+ remove() {
402
+ const { Logger } = this.ctx;
403
+ Logger.debug('AutocompleteUI removed.');
404
+ this.inputField.remove();
405
+ this.resultsList.remove();
406
+ this.wrapper.remove();
407
+ }
408
+ setNear(near) {
409
+ if (near === undefined || near === null) {
410
+ this.near = undefined;
411
+ }
412
+ else if (typeof near === 'string') {
413
+ this.near = near;
414
+ }
415
+ else {
416
+ this.near = `${near.latitude},${near.longitude}`;
417
+ }
418
+ return this;
419
+ }
420
+ setPlaceholder(placeholder) {
421
+ this.config.placeholder = placeholder;
422
+ this.inputField.placeholder = placeholder;
423
+ return this;
424
+ }
425
+ setDisabled(disabled) {
426
+ this.config.disabled = disabled;
427
+ this.inputField.disabled = disabled;
428
+ return this;
429
+ }
430
+ setResponsive(responsive) {
431
+ this.config.responsive = responsive;
432
+ setWidth(this.wrapper, this.config);
433
+ return this;
434
+ }
435
+ setWidth(width) {
436
+ if (width === null) {
437
+ this.config.width = undefined;
438
+ }
439
+ else if (typeof width === 'string' || typeof width === 'number') {
440
+ this.config.width = width;
441
+ }
442
+ setWidth(this.wrapper, this.config);
443
+ return this;
444
+ }
445
+ setMaxHeight(height) {
446
+ if (height === null) {
447
+ this.config.maxHeight = undefined;
448
+ }
449
+ else if (typeof height === 'string' || typeof height === 'number') {
450
+ this.config.maxHeight = height;
451
+ }
452
+ setHeight(this.resultsList, this.config);
453
+ return this;
454
+ }
455
+ setMinCharacters(minCharacters) {
456
+ this.config.minCharacters = minCharacters;
457
+ this.config.threshold = minCharacters;
458
+ return this;
459
+ }
460
+ setLimit(limit) {
461
+ this.config.limit = limit;
462
+ return this;
463
+ }
464
+ setLang(lang) {
465
+ if (lang === null) {
466
+ this.config.lang = undefined;
467
+ }
468
+ else if (typeof lang === 'string') {
469
+ this.config.lang = lang;
470
+ }
471
+ return this;
472
+ }
473
+ setPostalCode(postalCode) {
474
+ if (postalCode === null) {
475
+ this.config.postalCode = undefined;
476
+ }
477
+ else if (typeof postalCode === 'string') {
478
+ this.config.postalCode = postalCode;
479
+ }
480
+ return this;
481
+ }
482
+ setShowMarkers(showMarkers) {
483
+ this.config.showMarkers = showMarkers;
484
+ if (showMarkers) {
485
+ const marker = document.createElement('img');
486
+ marker.classList.add(CLASSNAMES.RESULTS_MARKER);
487
+ marker.setAttribute('src', getMarkerIcon(this.config.markerColor));
488
+ const resultItems = this.resultsList.getElementsByTagName('li');
489
+ for (let i = 0; i < resultItems.length; i++) {
490
+ const currentMarker = resultItems[i].getElementsByClassName(CLASSNAMES.RESULTS_MARKER)[0];
491
+ if (!currentMarker) {
492
+ resultItems[i].prepend(marker.cloneNode());
493
+ }
494
+ }
495
+ }
496
+ else {
497
+ const resultItems = this.resultsList.getElementsByTagName('li');
498
+ for (let i = 0; i < resultItems.length; i++) {
499
+ const marker = resultItems[i].getElementsByClassName(CLASSNAMES.RESULTS_MARKER)[0];
500
+ if (marker) {
501
+ marker.remove();
502
+ }
503
+ }
504
+ }
505
+ return this;
506
+ }
507
+ setMarkerColor(color) {
508
+ this.config.markerColor = color;
509
+ const marker = this.resultsList.getElementsByClassName(CLASSNAMES.RESULTS_MARKER);
510
+ for (let i = 0; i < marker.length; i++) {
511
+ marker[i].setAttribute('src', getMarkerIcon(color));
512
+ }
513
+ return this;
514
+ }
515
+ setHideResultsOnBlur(hideResultsOnBlur) {
516
+ this.config.hideResultsOnBlur = hideResultsOnBlur;
517
+ if (hideResultsOnBlur) {
518
+ this.inputField.addEventListener('blur', this.close.bind(this), true);
519
+ }
520
+ else {
521
+ this.inputField.removeEventListener('blur', this.close.bind(this), true);
522
+ }
523
+ return this;
524
+ }
525
+ }
526
+
527
+ var version = '5.0.0-beta.5';
528
+
529
+ function createAutocompletePlugin() {
530
+ return {
531
+ name: 'autocomplete',
532
+ version,
533
+ install(ctx) {
534
+ const existing = ctx.Radar.ui || {};
535
+ // NOTE(jasonl): we're merging with the existing ui namespace since other plugins also add to it like maps
536
+ ctx.Radar.ui = {
537
+ ...existing,
538
+ autocomplete: (options) => new AutocompleteUI(options, ctx),
539
+ };
540
+ },
541
+ };
542
+ }
543
+
544
+ Radar.registerPlugin(createAutocompletePlugin());
545
+
546
+ })(Radar);