UNPKG

places-autocomplete-js

Version:

A flexible and customisable vanilla JavaScript library leveraging the Google Maps Places (New) Autocomplete API. This library provides a user-friendly way to search for and retrieve detailed address information in any web application.

13 lines (12 loc) 14.1 kB
(function(p,y){typeof exports=="object"&&typeof module<"u"?y(exports):typeof define=="function"&&define.amd?define(["exports"],y):(p=typeof globalThis<"u"?globalThis:p||self,y(p.PlacesAutocomplete={}))})(this,function(p){"use strict";/** * Initializes a Places Autocomplete widget. * This script dynamically loads the Google Maps JavaScript API, creates the UI elements * for the autocomplete input and suggestions list, handles user input with debouncing, * fetches suggestions, manages keyboard navigation, and calls user-defined callbacks * on place selection or error. * * @author Alexander Pechkarev <alexpechkarev@gmail.com> * @license MIT * */class y{#u;#d;#m;#f;#e;#a;#t;#n;#s;#o;#c;#h;#r=[];#i=-1;#b;#l;_debouncedMakeAcRequest;#y={autofocus:!1,autocomplete:"off",placeholder:"Start typing your address ...",distance:!0,distance_units:"km",label:"",debounce:100,clear_input:!0,debug:!1};#p={section:"",container:"relative z-10 transform rounded-xl mt-4",icon_container:"pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3",icon:'<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8" /><path d="m21 21-4.3-4.3" /></svg>',input:"border-1 w-full rounded-md border-0 shadow-sm bg-gray-100 px-4 py-2.5 pl-10 pr-20 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 sm:text-sm",kbd_container:"absolute inset-y-0 right-0 flex py-1.5 pr-1.5",kbd_escape:"inline-flex items-center rounded border border-gray-300 px-1 font-sans text-xs text-gray-500 w-8 mr-1",kbd_up:"inline-flex items-center justify-center rounded border border-gray-300 px-1 font-sans text-xs text-gray-500 w-6",kbd_down:"inline-flex items-center rounded border border-gray-400 px-1 font-sans text-xs text-gray-500 justify-center w-6",kbd_active:"bg-indigo-500 text-white",ul:"absolute z-50 -mb-2 mt-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:text-sm divide-y divide-gray-100",li:"z-50 cursor-default select-none py-2 px-2 lg:px-4 text-gray-900 hover:bg-indigo-500 hover:text-white",li_current:"bg-indigo-500",li_a:"block w-full flex justify-between",li_a_current:"text-white",li_div_container:"flex min-w-0 gap-x-4",li_div_one:"min-w-0 flex-auto",li_div_one_p:"text-sm/6",li_div_two:"shrink-0 flex flex-col items-end min-w-16",li_div_two_p:"mt-1 text-xs/5",highlight:"font-bold"};#_={input:"",includedRegionCodes:["GB"],language:"en-gb",region:"GB"};#g=["formattedAddress","addressComponents"];#v=["formattedAddress","addressComponents"];constructor(e){if(!e||!e.containerId||!e.googleMapsApiKey)throw new Error("PacAutocomplete: Missing required configuration (containerId, googleMapsApiKey).");if(this.#u=e.containerId,this.#d=document.getElementById(e.containerId),!this.#d)throw new Error(`PacAutocomplete: Container element with ID "${e.containerId}" not found.`);this.#m=e.googleMapsApiKey,this.#f=e.googleMapsApiVersion||"weekly",this.#e={...this.#y,...e.options},e.options&&e.options.classes?this.#e.classes={...this.#p,...e.options.classes}:this.#e.classes=this.#p,this.#e.debug&&(console.log("___debug constructor options:"),console.log(this.#e)),e.fetchFields&&Array.isArray(e.fetchFields)&&this._setFetchFields(e.fetchFields),this.#b=e.onResponse||(t=>{console.info("---------Default onResponse not provided---------"),console.info("Selected Place:",JSON.stringify(t,null,2))}),this.#l=e.onError||(t=>{console.error("---------Default onError not provided---------"),console.error("PAC Error:",t)}),e.requestParams&&Object.keys(e.requestParams).length>0?this.#a={...this.#_,...e.requestParams}:this.#a={...this.#_},this.#e.debug&&console.log("___debug constructor requestParams:",this.#a),this._initialiseDebouncedRequest(),this._init()}async _init(){try{(typeof google>"u"||!google.maps)&&await this._loadGoogleMapsApi({key:this.#m,v:this.#f}),this._createPACStructure(),await this._initializeAutocomplete()}catch(e){this.#l(e)}}_initialiseDebouncedRequest(){this._debouncedMakeAcRequest=this._debounce(async()=>{if(!this.#t||!this.#t.value){this._reset(),this.#t&&this.#t.setAttribute("aria-expanded","false");return}this.#a.input=this.#t.value;try{const{suggestions:e}=await google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions(this.#a);e&&e.length>0?(this.#s.replaceChildren(...this._createSuggestionElements(e)),this.#s.style.display="block",this.#t.setAttribute("aria-expanded","true")):(this._reset(),this.#t.setAttribute("aria-expanded","false"))}catch(e){this.#l(e),this._reset()}},this.#e.debounce)}_setFetchFields(e){Array.isArray(e)&&e.length>0&&(this.#g=[...new Set([...this.#v,...e])].filter(t=>t))}_debounce(e,t){this.#e.debug&&console.log("___debug debounce function called with wait:",t);let s=null;return function(...n){const r=()=>{s=null,e(...n)};s!==null&&clearTimeout(s),s=setTimeout(r,t??100)}}_formatDistance(e,t){if(typeof e!="number"||!this.#e.distance)return null;let s,i;return t==="km"?(s=(e/1e3).toFixed(2),i="km"):(s=(e/1609.34).toFixed(2),i="miles"),s=s.replace(/\.00$/,""),`${s} ${i}`}async _loadGoogleMapsApi(e){var t,s,i,n="The Google Maps JavaScript API",r="google",m="importLibrary",d="__ib__",f=document,l=window;l=l[r]||(l[r]={});var o=l.maps||(l.maps={}),u=new Set,b=new URLSearchParams,_=()=>t||(t=new Promise(async(c,a)=>{s=f.createElement("script"),b.set("libraries",[...u].join(","));for(i in e)b.set(i.replace(/[A-Z]/g,g=>"_"+g[0].toLowerCase()),e[i]);b.set("callback",r+".maps."+d),s.src=`https://maps.${r}apis.com/maps/api/js?`+b,o[d]=c,s.onerror=()=>t=a(new Error(`${n} could not load. Check your API key and network connection.`)),s.nonce=f.querySelector("script[nonce]")?.nonce||"",f.head.append(s)}));o[m]?console.warn(n+" only loads once. Ignoring:",e):o[m]=(c,...a)=>u.add(c)&&_().then(()=>o[m](c,...a))}_createPACStructure(){const e=document.createElement("section");e.className=this.#e.classes.section,this.#n=document.createElement("div"),this.#n.className=this.#e.classes.container,this.#n.setAttribute("id",this.#u+"-div"),e.appendChild(this.#n);const t=document.createElement("div");t.className=this.#e.classes.icon_container,this.#n.appendChild(t);const s=document.createElement("div");if(s.innerHTML=this.#e.classes.icon,t.appendChild(s.firstElementChild),this.#t=document.createElement("input"),this.#t.id=this.#u+"-input",this.#t.type="text",this.#t.name="search",this.#t.placeholder=this.#e.placeholder,this.#t.autocomplete=this.#e.autocomplete,this.#t.className=this.#e.classes.input,this.#t.setAttribute("role","combobox"),this.#t.setAttribute("aria-autocomplete","list"),this.#t.setAttribute("aria-expanded","false"),this.#t.setAttribute("aria-controls","pacSuggestions"),this.#t.setAttribute("aria-activedescendant",""),this.#e.autofocus&&(this.#t.autofocus=!0),this.#e.label){const n=document.createElement("label");n.htmlFor=this.#t.id,n.textContent=this.#e.label,e.prepend(n)}this.#n.appendChild(this.#t);const i=document.createElement("div");i.className=this.#e.classes.kbd_container,this.#o=document.createElement("kbd"),this.#o.className=this.#e.classes.kbd_escape,this.#o.textContent="Esc",i.appendChild(this.#o),this.#c=document.createElement("kbd"),this.#c.className=this.#e.classes.kbd_up,this.#c.innerHTML="&#8593;",i.appendChild(this.#c),this.#h=document.createElement("kbd"),this.#h.className=this.#e.classes.kbd_down,this.#h.innerHTML="&#8595;",i.appendChild(this.#h),this.#n.appendChild(i),this.#s=document.createElement("ul"),this.#s.id="pacSuggestions",this.#s.className=this.#e.classes.ul,this.#s.style.display="none",this.#s.setAttribute("role","listbox"),this.#s.setAttribute("aria-labelledby",this.#t.id),this.#n.appendChild(this.#s),this.#d.appendChild(e),e.addEventListener("keydown",this._onKeyDown.bind(this))}_attachedEventListeners(){this.#t.addEventListener("input",this._debouncedMakeAcRequest),this.#t.addEventListener("blur",()=>{setTimeout(()=>{this.#s&&!this.#s.contains(document.activeElement)&&(this.#s.style.display="none",this.#t.setAttribute("aria-expanded","false"))},200)}),this.#t.addEventListener("focus",()=>{this.#t.value&&this.#r.length>0&&(this.#s.style.display="block",this.#t.setAttribute("aria-expanded","true"))})}_detachEventListeners(){this.#t&&this.#t.removeEventListener("input",this._debouncedMakeAcRequest),this.#d&&this.#n&&this.#d.removeChild(this.#n.parentElement)}async _initializeAutocomplete(){try{await google.maps.importLibrary("places"),this._refreshToken(),this.#t?this._attachedEventListeners():this.#l(new Error("Input element not found during initialization."))}catch(e){console.error("Error initializing Google Places Autocomplete:",e),this.#l(new Error("Google Maps Places library not available."))}}_reset(e=!1,t=null){this.#i=-1,this.#t&&this.#e.clear_input==!1&&t&&t.formattedAddress?this.#t.value=t.formattedAddress:this.#t&&(this.#t.value=""),this.#t&&(this.#t.setAttribute("aria-expanded","false"),this.#t.setAttribute("aria-activedescendant",""),this.#t.blur()),this.#r=[],this.#i=-1,this.#s&&(this.#s.innerHTML="",this.#s.style.display="none"),e&&this._refreshToken()}_resetLiClasses(){this.#s&&Array.from(this.#s.children).forEach(e=>{this.#e.classes.li_current.split(" ").forEach(s=>e.classList.remove(s));const t=e.querySelector("a");t&&this.#e.classes.li_a_current.split(" ").forEach(s=>t.classList.remove(s))})}_onKeyDown(e){if(this._resetLiClasses(),e.key==="Escape"&&(e.preventDefault(),this.#e.classes.kbd_active.split(" ").forEach(t=>this.#o?.classList.add(t)),setTimeout(()=>this.#e.classes.kbd_active.split(" ").forEach(t=>this.#o?.classList.remove(t)),300),this._reset(!0)),!(!this.#r.length||!this.#s||this.#s.style.display==="none"))if(e.key==="ArrowDown"){e.preventDefault(),this.#i=Math.min(this.#i+1,this.#r.length-1),this.#i<0&&(this.#i=0);const t=this.#s.children.item(this.#i);if(t){const s=t.querySelector("a");this.#e.classes.li_current.split(" ").forEach(i=>t.classList.add(i)),s&&this.#e.classes.li_a_current.split(" ").forEach(i=>s.classList.add(i)),t.scrollIntoView({block:"nearest"}),this.#t.setAttribute("aria-activedescendant",t.id)}this.#e.classes.kbd_active.split(" ").forEach(s=>this.#h?.classList.add(s)),setTimeout(()=>this.#e.classes.kbd_active.split(" ").forEach(s=>this.#h?.classList.remove(s)),300)}else if(e.key==="ArrowUp"){e.preventDefault(),this.#i=Math.max(this.#i-1,0);const t=this.#s.children.item(this.#i);if(t){const s=t.querySelector("a");this.#e.classes.li_current.split(" ").forEach(i=>t.classList.add(i)),s&&this.#e.classes.li_a_current.split(" ").forEach(i=>s.classList.add(i)),t.scrollIntoView({block:"nearest"})}this.#e.classes.kbd_active.split(" ").forEach(s=>this.#c?.classList.add(s)),setTimeout(()=>this.#e.classes.kbd_active.split(" ").forEach(s=>this.#c?.classList.remove(s)),300)}else e.key==="Enter"&&(e.preventDefault(),this.#i>=0&&this.#i<this.#r.length&&this._onPlaceSelected(this.#r[this.#i].place))}_createSuggestionElements(e){return this.#r=[],e.map((t,s)=>{this.#r.push({id:s+1,description:t.placePrediction.toString(),place:t.placePrediction.toPlace()});const i=document.createElement("div");i.className=this.#e.classes.li_div_container;const n=document.createElement("div");n.className=this.#e.classes.li_div_one,i.appendChild(n);const r=document.createElement("p");r.className=this.#e.classes.li_div_one_p;const m=t.placePrediction.text,d=m.text,f=m.matches;let l=0;f.sort((h,v)=>h.startOffset-v.startOffset);const o=document.createElement("span"),u=document.createElement("span");u.classList=this.#e.classes.highlight??"font-bold";for(const h of f)o.textContent+=d.substring(l,h.startOffset),h.startOffset>0&&d.charAt(h.startOffset-1)==" "&&(u.textContent+=" "),u.textContent+=d.substring(h.startOffset,h.endOffset),l=h.endOffset;const b=document.createTextNode(d.substring(l));o.appendChild(u),o.appendChild(b),r.appendChild(o),n.appendChild(r),i.appendChild(n);const _=document.createElement("div");_.className=this.#e.classes.li_div_two,i.appendChild(_);const c=document.createElement("p");c.className=this.#e.classes.li_div_two_p,c.textContent=this._formatDistance(t.placePrediction.distanceMeters,this.#e.distance_units??"km"),_.appendChild(c);const a=document.createElement("a");a.href="javascript:void(0)",a.tabIndex=s+1,a.className=this.#e.classes.li_a,a.addEventListener("click",()=>{this._onPlaceSelected(t.placePrediction.toPlace())}),a.appendChild(i),a.appendChild(_);const g=document.createElement("li");return g.id=`option-${s+1}`,g.className=this.#e.classes.li,g.appendChild(a),g})}async _onPlaceSelected(e){let t=null;try{await e.fetchFields({fields:this.#g}),t=e.toJSON(),this.#b(t)}catch(s){console.error("Error fetching place details:",s),this.#l(s)}finally{this._reset(!0,t)}}_refreshToken(){try{this.#a.sessionToken=new google.maps.places.AutocompleteSessionToken}catch(e){console.error("Error creating session token:",e),this.#l(e)}}setFetchFields(e){this._setFetchFields(e)}getFetchFields(){return this.#g}setRequestParams(e){typeof e=="object"&&!Array.isArray(e)&&e!==null&&(e.input&&typeof e.input=="string"&&(this.#t.value=e.input),this.#a={...this.#_,...e})}getRequestParams(){return this.#a}setOptions(e){if(typeof e=="object"&&!Array.isArray(e)&&e!==null){this._detachEventListeners();const t=e.classes||{};delete e.classes,this.#e={...this.#y,...e},t&&typeof t=="object"&&Object.keys(t).length>0?this.#e.classes={...this.#p,...t}:this.#e.classes={...this.#p},this._initialiseDebouncedRequest(),this._createPACStructure(),this.#t&&this._attachedEventListeners()}}getOptions(){return this.#e}clear(){this._reset(!0)}destroy(){this._detachEventListeners(),this.#u=null,this.#d=null,this.#m=null,this.#f=null,this.#e=null,this.#a=null,this.#t=null,this.#n=null,this.#s=null,this.#o=null,this.#c=null,this.#h=null,this.#r=null,this.#i=-1,this.#b=null,this.#l=null,this._debouncedMakeAcRequest=null,console.log("PacAutocomplete instance destroyed.")}}p.PlacesAutocomplete=y,Object.defineProperty(p,Symbol.toStringTag,{value:"Module"})}); //# sourceMappingURL=places-autocomplete.umd.cjs.map