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