@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.
@@ -1,6 +1,7 @@
1
1
  (function (Radar) {
2
2
  'use strict';
3
3
 
4
+ /** error thrown when the autocomplete container element is not found in the DOM */
4
5
  class RadarAutocompleteContainerNotFound extends Radar.RadarError {
5
6
  constructor(message) {
6
7
  super(message);
@@ -65,6 +66,7 @@
65
66
  </svg>`.trim();
66
67
  return `data:image/svg+xml;charset=utf-8,${svg}`;
67
68
  };
69
+ /** address autocomplete UI widget with keyboard navigation and result display */
68
70
  class AutocompleteUI {
69
71
  constructor(options, ctx) {
70
72
  this.ctx = ctx;
@@ -72,7 +74,7 @@
72
74
  this.config = Object.assign({}, defaultAutocompleteOptions, options);
73
75
  // setup state
74
76
  this.isOpen = false;
75
- this.debouncedFetchResults = this.debounce(this.fetchResults, this.config.debounceMS);
77
+ this.debouncedFetchResults = this.debounce(this.fetchResults.bind(this), this.config.debounceMS);
76
78
  this.results = [];
77
79
  this.highlightedIndex = -1;
78
80
  // set threshold alias
@@ -119,7 +121,7 @@
119
121
  this.inputField = containerEL;
120
122
  // append to dom
121
123
  this.wrapper.appendChild(this.resultsList);
122
- containerEL.parentNode.appendChild(this.wrapper);
124
+ containerEL.parentNode?.appendChild(this.wrapper);
123
125
  }
124
126
  else {
125
127
  // if container is not an input, create new input and append to container
@@ -155,6 +157,7 @@
155
157
  }
156
158
  Logger.debug('AutocompleteUI initialized with options', this.config);
157
159
  }
160
+ /** handle input field changes and trigger debounced search */
158
161
  handleInput() {
159
162
  const { Logger } = this.ctx;
160
163
  // Fetch autocomplete results and display them
@@ -180,25 +183,18 @@
180
183
  }
181
184
  debounce(fn, delay) {
182
185
  let timeoutId;
183
- let resolveFn;
184
- let rejectFn;
186
+ let resolveFn = null;
187
+ let rejectFn = null;
185
188
  return (...args) => {
186
189
  clearTimeout(timeoutId);
187
190
  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
- }
191
+ fn(...args)
192
+ .then((value) => {
193
+ resolveFn?.(value);
194
+ })
195
+ .catch((error) => {
196
+ rejectFn?.(error);
197
+ });
202
198
  }, delay);
203
199
  return new Promise((resolve, reject) => {
204
200
  resolveFn = resolve;
@@ -206,6 +202,11 @@
206
202
  });
207
203
  };
208
204
  }
205
+ /**
206
+ * fetch autocomplete results from the Radar API
207
+ * @param query - the search query string
208
+ * @returns matching addresses
209
+ */
209
210
  async fetchResults(query) {
210
211
  const { apis } = this.ctx;
211
212
  const { limit, layers, countryCode, expandUnits, mailable, lang, postalCode, onRequest } = this.config;
@@ -228,6 +229,10 @@
228
229
  const { addresses } = await apis.Search.autocomplete(params, 'autocomplete-ui');
229
230
  return addresses;
230
231
  }
232
+ /**
233
+ * render autocomplete results in the dropdown
234
+ * @param results - array of address results to display
235
+ */
231
236
  displayResults(results) {
232
237
  // Clear the previous results
233
238
  this.clearResultsList();
@@ -246,7 +251,7 @@
246
251
  li.setAttribute('id', `${CLASSNAMES.RESULTS_ITEM}}-${index}`);
247
252
  // construct result with bolded label
248
253
  let listContent;
249
- if (result.formattedAddress.includes(result.addressLabel) && result.layer !== 'postalCode') {
254
+ if (result.formattedAddress?.includes(result.addressLabel) && result.layer !== 'postalCode') {
250
255
  // if addressLabel is contained in the formatted address, bold the address label
251
256
  const regex = new RegExp(`(${result.addressLabel}),?`);
252
257
  listContent = result.formattedAddress.replace(regex, '<b>$1</b>');
@@ -292,6 +297,7 @@
292
297
  this.resultsList.appendChild(noResultsText);
293
298
  }
294
299
  }
300
+ /** open the results dropdown */
295
301
  open() {
296
302
  if (this.isOpen) {
297
303
  return;
@@ -300,6 +306,7 @@
300
306
  this.resultsList.removeAttribute('hidden');
301
307
  this.isOpen = true;
302
308
  }
309
+ /** close the results dropdown and clear highlighted state */
303
310
  close(e) {
304
311
  if (!this.isOpen) {
305
312
  return;
@@ -316,6 +323,10 @@
316
323
  this.clearResultsList();
317
324
  }, linkClick ? 100 : 0);
318
325
  }
326
+ /**
327
+ * highlight a result by index with wrap-around navigation
328
+ * @param index - the result index to highlight
329
+ */
319
330
  goTo(index) {
320
331
  if (!this.isOpen || !this.results.length) {
321
332
  return;
@@ -370,6 +381,10 @@
370
381
  break;
371
382
  }
372
383
  }
384
+ /**
385
+ * select a result by index and populate the input field
386
+ * @param index - the result index to select
387
+ */
373
388
  select(index) {
374
389
  const { Logger } = this.ctx;
375
390
  const result = this.results[index];
@@ -378,7 +393,7 @@
378
393
  return;
379
394
  }
380
395
  let inputValue;
381
- if (result.formattedAddress.includes(result.addressLabel)) {
396
+ if (result.formattedAddress?.includes(result.addressLabel)) {
382
397
  inputValue = result.formattedAddress;
383
398
  }
384
399
  else {
@@ -393,11 +408,12 @@
393
408
  // clear results list
394
409
  this.close();
395
410
  }
411
+ /** clear the results list DOM and reset results array */
396
412
  clearResultsList() {
397
413
  this.resultsList.innerHTML = '';
398
414
  this.results = [];
399
415
  }
400
- // remove elements from DOM
416
+ /** remove the autocomplete widget from the DOM */
401
417
  remove() {
402
418
  const { Logger } = this.ctx;
403
419
  Logger.debug('AutocompleteUI removed.');
@@ -405,6 +421,11 @@
405
421
  this.resultsList.remove();
406
422
  this.wrapper.remove();
407
423
  }
424
+ /**
425
+ * set the `near` location bias for autocomplete requests
426
+ * @param near - location string, Location object, or null to clear
427
+ * @returns this instance for chaining
428
+ */
408
429
  setNear(near) {
409
430
  if (near === undefined || near === null) {
410
431
  this.near = undefined;
@@ -417,21 +438,41 @@
417
438
  }
418
439
  return this;
419
440
  }
441
+ /**
442
+ * set the input placeholder text
443
+ * @param placeholder - new placeholder string
444
+ * @returns this instance for chaining
445
+ */
420
446
  setPlaceholder(placeholder) {
421
447
  this.config.placeholder = placeholder;
422
448
  this.inputField.placeholder = placeholder;
423
449
  return this;
424
450
  }
451
+ /**
452
+ * set the disabled state of the input
453
+ * @param disabled - whether to disable the input
454
+ * @returns this instance for chaining
455
+ */
425
456
  setDisabled(disabled) {
426
457
  this.config.disabled = disabled;
427
458
  this.inputField.disabled = disabled;
428
459
  return this;
429
460
  }
461
+ /**
462
+ * toggle responsive width mode
463
+ * @param responsive - whether to use responsive layout
464
+ * @returns this instance for chaining
465
+ */
430
466
  setResponsive(responsive) {
431
467
  this.config.responsive = responsive;
432
468
  setWidth(this.wrapper, this.config);
433
469
  return this;
434
470
  }
471
+ /**
472
+ * set the widget width
473
+ * @param width - width in px, CSS string, or null to reset
474
+ * @returns this instance for chaining
475
+ */
435
476
  setWidth(width) {
436
477
  if (width === null) {
437
478
  this.config.width = undefined;
@@ -442,6 +483,11 @@
442
483
  setWidth(this.wrapper, this.config);
443
484
  return this;
444
485
  }
486
+ /**
487
+ * set the max height of the results dropdown
488
+ * @param height - height in px, CSS string, or null to reset
489
+ * @returns this instance for chaining
490
+ */
445
491
  setMaxHeight(height) {
446
492
  if (height === null) {
447
493
  this.config.maxHeight = undefined;
@@ -452,15 +498,30 @@
452
498
  setHeight(this.resultsList, this.config);
453
499
  return this;
454
500
  }
501
+ /**
502
+ * set the minimum character count to trigger autocomplete
503
+ * @param minCharacters - character threshold
504
+ * @returns this instance for chaining
505
+ */
455
506
  setMinCharacters(minCharacters) {
456
507
  this.config.minCharacters = minCharacters;
457
508
  this.config.threshold = minCharacters;
458
509
  return this;
459
510
  }
511
+ /**
512
+ * set the maximum number of results
513
+ * @param limit - result count limit
514
+ * @returns this instance for chaining
515
+ */
460
516
  setLimit(limit) {
461
517
  this.config.limit = limit;
462
518
  return this;
463
519
  }
520
+ /**
521
+ * set the language for autocomplete results
522
+ * @param lang - language code or null to clear
523
+ * @returns this instance for chaining
524
+ */
464
525
  setLang(lang) {
465
526
  if (lang === null) {
466
527
  this.config.lang = undefined;
@@ -470,6 +531,11 @@
470
531
  }
471
532
  return this;
472
533
  }
534
+ /**
535
+ * set a postal code bias for autocomplete requests
536
+ * @param postalCode - postal code string or null to clear
537
+ * @returns this instance for chaining
538
+ */
473
539
  setPostalCode(postalCode) {
474
540
  if (postalCode === null) {
475
541
  this.config.postalCode = undefined;
@@ -479,6 +545,11 @@
479
545
  }
480
546
  return this;
481
547
  }
548
+ /**
549
+ * toggle marker icons in result items
550
+ * @param showMarkers - whether to show markers
551
+ * @returns this instance for chaining
552
+ */
482
553
  setShowMarkers(showMarkers) {
483
554
  this.config.showMarkers = showMarkers;
484
555
  if (showMarkers) {
@@ -504,6 +575,11 @@
504
575
  }
505
576
  return this;
506
577
  }
578
+ /**
579
+ * set the color of marker icons in result items
580
+ * @param color - CSS color string
581
+ * @returns this instance for chaining
582
+ */
507
583
  setMarkerColor(color) {
508
584
  this.config.markerColor = color;
509
585
  const marker = this.resultsList.getElementsByClassName(CLASSNAMES.RESULTS_MARKER);
@@ -512,6 +588,11 @@
512
588
  }
513
589
  return this;
514
590
  }
591
+ /**
592
+ * toggle hiding results when input loses focus
593
+ * @param hideResultsOnBlur - whether to hide on blur
594
+ * @returns this instance for chaining
595
+ */
515
596
  setHideResultsOnBlur(hideResultsOnBlur) {
516
597
  this.config.hideResultsOnBlur = hideResultsOnBlur;
517
598
  if (hideResultsOnBlur) {
@@ -524,8 +605,19 @@
524
605
  }
525
606
  }
526
607
 
527
- var version = '5.0.0-beta.5';
608
+ var version = '5.0.0-beta.6';
528
609
 
610
+ /**
611
+ * create the Radar autocomplete plugin
612
+ *
613
+ * @returns a plugin that adds `Radar.ui.autocomplete()` method
614
+ *
615
+ * @example
616
+ * ```ts
617
+ * import { createAutocompletePlugin } from '@radarlabs/plugin-autocomplete';
618
+ * Radar.registerPlugin(createAutocompletePlugin());
619
+ * ```
620
+ */
529
621
  function createAutocompletePlugin() {
530
622
  return {
531
623
  name: 'autocomplete',
@@ -1 +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);
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.bind(this),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=null,r=null;return(...n)=>(clearTimeout(s),s=setTimeout((()=>{t(...n).then((t=>{i?.(t)})).catch((t=>{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.6",install(t){const e=t.Radar.ui||{};t.Radar.ui={...e,autocomplete:e=>new f(e,t)}}})}(Radar);
@@ -1,12 +1,13 @@
1
1
  import type { RadarAutocompleteUIOptions, RadarAutocompleteConfig } from './types';
2
- import type { Location, RadarPluginContext } from 'radar-sdk-js';
2
+ import type { RadarAutocompleteAddress, Location, RadarPluginContext } from 'radar-sdk-js';
3
+ /** address autocomplete UI widget with keyboard navigation and result display */
3
4
  declare class AutocompleteUI {
4
5
  private ctx;
5
6
  config: RadarAutocompleteConfig;
6
7
  isOpen: boolean;
7
- results: any[];
8
+ results: RadarAutocompleteAddress[];
8
9
  highlightedIndex: number;
9
- debouncedFetchResults: (...args: any[]) => Promise<any>;
10
+ debouncedFetchResults: (query: string) => Promise<RadarAutocompleteAddress[]>;
10
11
  near?: string;
11
12
  container: HTMLElement;
12
13
  inputField: HTMLInputElement;
@@ -14,29 +15,116 @@ declare class AutocompleteUI {
14
15
  wrapper: HTMLElement;
15
16
  poweredByLink?: HTMLElement;
16
17
  constructor(options: Partial<RadarAutocompleteUIOptions>, ctx: RadarPluginContext);
18
+ /** handle input field changes and trigger debounced search */
17
19
  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;
20
+ debounce<TArgs extends unknown[], TReturn>(fn: (...args: TArgs) => Promise<TReturn>, delay: number): (...args: TArgs) => Promise<TReturn>;
21
+ /**
22
+ * fetch autocomplete results from the Radar API
23
+ * @param query - the search query string
24
+ * @returns matching addresses
25
+ */
26
+ fetchResults(query: string): Promise<RadarAutocompleteAddress[]>;
27
+ /**
28
+ * render autocomplete results in the dropdown
29
+ * @param results - array of address results to display
30
+ */
31
+ displayResults(results: RadarAutocompleteAddress[]): void;
32
+ /** open the results dropdown */
21
33
  open(): void;
34
+ /** close the results dropdown and clear highlighted state */
22
35
  close(e?: FocusEvent): void;
36
+ /**
37
+ * highlight a result by index with wrap-around navigation
38
+ * @param index - the result index to highlight
39
+ */
23
40
  goTo(index: number): void;
24
41
  handleKeyboardNavigation(event: KeyboardEvent): void;
42
+ /**
43
+ * select a result by index and populate the input field
44
+ * @param index - the result index to select
45
+ */
25
46
  select(index: number): void;
47
+ /** clear the results list DOM and reset results array */
26
48
  clearResultsList(): void;
49
+ /** remove the autocomplete widget from the DOM */
27
50
  remove(): void;
51
+ /**
52
+ * set the `near` location bias for autocomplete requests
53
+ * @param near - location string, Location object, or null to clear
54
+ * @returns this instance for chaining
55
+ */
28
56
  setNear(near: string | Location | undefined | null): this;
57
+ /**
58
+ * set the input placeholder text
59
+ * @param placeholder - new placeholder string
60
+ * @returns this instance for chaining
61
+ */
29
62
  setPlaceholder(placeholder: string): this;
63
+ /**
64
+ * set the disabled state of the input
65
+ * @param disabled - whether to disable the input
66
+ * @returns this instance for chaining
67
+ */
30
68
  setDisabled(disabled: boolean): this;
69
+ /**
70
+ * toggle responsive width mode
71
+ * @param responsive - whether to use responsive layout
72
+ * @returns this instance for chaining
73
+ */
31
74
  setResponsive(responsive: boolean): this;
75
+ /**
76
+ * set the widget width
77
+ * @param width - width in px, CSS string, or null to reset
78
+ * @returns this instance for chaining
79
+ */
32
80
  setWidth(width: number | string | null): this;
81
+ /**
82
+ * set the max height of the results dropdown
83
+ * @param height - height in px, CSS string, or null to reset
84
+ * @returns this instance for chaining
85
+ */
33
86
  setMaxHeight(height: number | string | null): this;
87
+ /**
88
+ * set the minimum character count to trigger autocomplete
89
+ * @param minCharacters - character threshold
90
+ * @returns this instance for chaining
91
+ */
34
92
  setMinCharacters(minCharacters: number): this;
93
+ /**
94
+ * set the maximum number of results
95
+ * @param limit - result count limit
96
+ * @returns this instance for chaining
97
+ */
35
98
  setLimit(limit: number): this;
99
+ /**
100
+ * set the language for autocomplete results
101
+ * @param lang - language code or null to clear
102
+ * @returns this instance for chaining
103
+ */
36
104
  setLang(lang: string | null): this;
105
+ /**
106
+ * set a postal code bias for autocomplete requests
107
+ * @param postalCode - postal code string or null to clear
108
+ * @returns this instance for chaining
109
+ */
37
110
  setPostalCode(postalCode: string | null): this;
111
+ /**
112
+ * toggle marker icons in result items
113
+ * @param showMarkers - whether to show markers
114
+ * @returns this instance for chaining
115
+ */
38
116
  setShowMarkers(showMarkers: boolean): this;
117
+ /**
118
+ * set the color of marker icons in result items
119
+ * @param color - CSS color string
120
+ * @returns this instance for chaining
121
+ */
39
122
  setMarkerColor(color: string): this;
123
+ /**
124
+ * toggle hiding results when input loses focus
125
+ * @param hideResultsOnBlur - whether to hide on blur
126
+ * @returns this instance for chaining
127
+ */
40
128
  setHideResultsOnBlur(hideResultsOnBlur: boolean): this;
41
129
  }
42
130
  export default AutocompleteUI;
package/dist/errors.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { RadarError } from 'radar-sdk-js';
2
+ /** error thrown when the autocomplete container element is not found in the DOM */
2
3
  export declare class RadarAutocompleteContainerNotFound extends RadarError {
3
4
  constructor(message: string);
4
5
  }
package/dist/index.d.ts CHANGED
@@ -11,4 +11,15 @@ declare module 'radar-sdk-js' {
11
11
  let ui: RadarUI;
12
12
  }
13
13
  }
14
+ /**
15
+ * create the Radar autocomplete plugin
16
+ *
17
+ * @returns a plugin that adds `Radar.ui.autocomplete()` method
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { createAutocompletePlugin } from '@radarlabs/plugin-autocomplete';
22
+ * Radar.registerPlugin(createAutocompletePlugin());
23
+ * ```
24
+ */
14
25
  export declare function createAutocompletePlugin(): RadarPlugin;