1/** 2 * @namespace WPGMZA 3 * @module Autocomplete 4 * @requires WPGMZA.EventDispatcher 5 */ 6jQuery(function($) { 7 /** 8 * Autocomplete base class 9 */ 10 WPGMZA.Autocomplete = function(element, options) { 11 WPGMZA.EventDispatcher.call(this); 12 13 if(!options){ 14 options = {}; 15 } 16 17 this.element = element; 18 this.options = options; 19 20 this.provider = false; 21 22 this.state = { 23 ready : false, 24 busy : false, 25 internal : false, 26 term : false, 27 items : [] 28 }; 29 30 this.components = {}; 31 32 if(this.canAutoload()){ 33 this.autoload(); 34 } 35 } 36 37 WPGMZA.extend(WPGMZA.Autocomplete, WPGMZA.EventDispatcher); 38 39 /* Valid provider list */ 40 WPGMZA.Autocomplete.Providers = { 41 GOOGLE_AUTOCOMPLETE : 1, 42 GOOGLE_PLACES : 2, 43 CLOUD_API : 3, 44 NOMINATIM : 4, 45 AZURE_MAPS : 5, 46 LOCATION_IQ : 6 47 }; 48 49 /* Default intervals */ 50 WPGMZA.Autocomplete.Intervals = { 51 KEYSTROKE : 700, 52 FOCUS : 400 53 }; 54 55 /** 56 * Static instance creation method 57 * 58 * @param any element 59 * @param object options 60 * 61 * @return any 62 */ 63 WPGMZA.Autocomplete.createInstance = function(element, options) { 64 let constructor; 65 66 let provider = WPGMZA.settings && WPGMZA.settings.address_provider ? WPGMZA.settings.address_provider : false;
67 if(!provider){ 68 /* Provider unknown, use engine default */ 69 if(WPGMZA.settings && WPGMZA.settings.engine){ 70 switch(WPGMZA.settings.engine){ 71 case 'google-maps': 72 /* Google Maps - Prefer autocomplete, with auto-swap to places */ 73 provider = WPGMZA.Autocomplete.Providers.GOOGLE_AUTOCOMPLETE; 74 break; 75 case 'open-layers': 76 case 'open-layers-latest': 77 case 'leaflet': 78 case 'leaflet-stadia': 79 case 'leaflet-maptiler': 80 case 'leaflet-zerocost': 81 /* Nominatim */ 82 provider = WPGMZA.Autocomplete.Providers.NOMINATIM; 83 break; 84 case 'leaflet-azure': 85 /* Azure */ 86 provider = WPGMZA.Autocomplete.Providers.AZURE_MAPS; 87 break; 88 case 'leaflet-locationiq': 89 /* LocationIQ */ 90 provider = WPGMZA.Autocomplete.Providers.LOCATION_IQ; 91 break; 92 } 93 } 94 95 if(WPGMZA.CloudAPI && WPGMZA.CloudAPI.isBeingUsed){ 96 provider = WPGMZA.Autocomplete.Providers.CLOUD_API; 97 } 98 } else if (typeof provider === 'string'){ 99 switch(provider){ 100 case 'google-maps': 101 /* Google Maps - Prefer autocomplete, with auto-swap to places */ 102 provider = WPGMZA.Autocomplete.Providers.GOOGLE_AUTOCOMPLETE; 103 break; 104 case 'nominatim': 105 /* Nominatim */ 106 provider = WPGMZA.Autocomplete.Providers.NOMINATIM; 107 break; 108 case 'azure-maps': 109 /* Azure */ 110 provider = WPGMZA.Autocomplete.Providers.AZURE_MAPS; 111 break; 112 case 'location-iq': 113 /* Azure */ 114 provider = WPGMZA.Autocomplete.Providers.LOCATION_IQ; 115 break; 116 } 117 } 118 119 switch(provider){ 120 case WPGMZA.Autocomplete.Providers.GOOGLE_AUTOCOMPLETE: 121 case WPGMZA.Autocomplete.Providers.GOOGLE_PLACES: 122 /* Google Autocomplete */ 123 constructor = WPGMZA.GoogleAutocomplete; 124 break; 125 case WPGMZA.Autocomplete.Providers.CLOUD_API: 126 /* Cloud (Legacy) Autocomplete */ 127 /* Note : Does not extend autocomplete base - Functions independently */ 128 constructor = WPGMZA.CloudAutocomplete; 129 break; 130 case WPGMZA.Autocomplete.Providers.NOMINATIM: 131 /* Nominatim Autocomplete */ 132 constructor = WPGMZA.NominatimAutocomplete; 133 break; 134 case WPGMZA.Autocomplete.Providers.AZURE_MAPS: 135 /* Azure Autocomplete */ 136 constructor = WPGMZA.AzureAutocomplete; 137 break; 138 case WPGMZA.Autocomplete.Providers.LOCATION_IQ: 139 /* LocationIQ Autocomplete */ 140 constructor = WPGMZA.LocationIQAutocomplete; 141 break; 142 default: 143 /* Use the base - Which will likely do nothing */ 144 constructor = WPGMZA.Autocomplete; 145 break; 146 } 147 148 return new constructor(element, options); 149 } 150 151 /** 152 * Check if we are allowed to autoload 153 * 154 * This is true for all address inputs, with the exception of the marker address admin input 155 * 156 * @return bool 157 */ 158 WPGMZA.Autocomplete.prototype.canAutoload = function(){ 159 if(this.element && this.element.id && this.element.id === 'wpgmza_add_address_map_editor'){ 160 return false; 161 } 162 return true; 163 } 164 165 /** 166 * Autoload the instance 167 * 168 * This is only helpful if you have extended the system into a sub-class as the default class will not 169 * produce any UI 170 * 171 * @return void 172 */ 173 WPGMZA.Autocomplete.prototype.autoload = function(){ 174 this.provider = this.getProvider(); 175 } 176 177 /** 178 * Prepare the system for internal suggestions 179 * 180 * Marks the instance as internal, generates components, links back to element and places placeholders 181 * 182 * Once complete, binds the events to the element 183 * 184 * @return void 185 */ 186 WPGMZA.Autocomplete.prototype.prepare = function(){ 187 this.setInternal(true); 188 this.generate(); 189 190 this.element.wpgmzaAutocomplete = this; 191 this.element.setAttribute('data-address-provider', this.getProviderSlug()); 192 193 let placeholder = this.options.placeholder ? this.placeholder : false;
194 if(!placeholder && !this.element.getAttribute('placeholder')){ 195 placeholder = WPGMZA.localized_strings && WPGMZA.localized_strings.autocomplete_placeholder ? WPGMZA.localized_strings.autocomplete_placeholder : false; 196 } 197 198 if(placeholder){ 199 this.element.setAttribute('placeholder', placeholder); 200 } 201 202 this.element.setAttribute('autocomplete', 'off'); 203 204 this.bind(); 205 } 206 207 /** 208 * Generate the components list element 209 * 210 * We use this for presenting the list to the end user 211 * 212 * @return void 213 */ 214 WPGMZA.Autocomplete.prototype.generate = function(){ 215 if(!this.isInternal()){ 216 return; 217 } 218 219 this.components.list = document.createElement('div'); 220 221 this.components.list.classList.add('wpgmza-hidden'); 222 this.components.list.classList.add('wpgmza-internal-autocomplete-list'); 223 224 document.body.appendChild(this.components.list); 225 } 226 227 /** 228 * Bind event listeners to the UI elements 229 * 230 * Only runs in internal mode 231 * 232 * @return void 233 */ 234 WPGMZA.Autocomplete.prototype.bind = function(){ 235 if(!this.isInternal()){ 236 return; 237 } 238 239 this.element.addEventListener("keyup", (event) => { 240 if(event.key.length > 1){ 241 /* Ignore these as they are likely mutators */ 242 if(event.key !== "Backspace"){ 243 return; 244 } 245 } 246 247 if(this.state.timer){ 248 clearTimeout(this.state.timer); 249 } 250 251 /* We have a valid key so we can go about performing a search */ 252 253 this.state.timer = setTimeout(() => { 254 if(this.element.value && this.element.value.trim() !== this.state.term){ 255 this.setBusy(true); 256 this.find(); 257 } 258 }, this.options.keystrokeInterval ? this.options.keystrokeInterval : WPGMZA.Autocomplete.Intervals.KEYSTROKE); 259 }); 260 261 this.element.addEventListener("focusout", (event) => { 262 setTimeout(() => { 263 if(document.activeElement === this.element){ 264 /* Has not actually lost focus yet */ 265 return; 266 } 267 this.hide(); 268 }, this.options.focusInterval ? this.options.focusInterval : WPGMZA.Autocomplete.Intervals.FOCUS); 269 }); 270 271 this.element.addEventListener("focusin", (event) => { 272 this.show(); 273 this.autoplace(); 274 }); 275 276 this.element.addEventListener("click", (event) => { 277 this.show(); 278 this.autoplace(); 279 }); 280 281 document.addEventListener("scroll", (event) => { 282 this.hide(); 283 }); 284 } 285 286 /** 287 * Find locations based on the search term 288 * 289 * This will only run if/when you have an extension class that contains additional logic for the requests 290 * 291 * The base method will only set the search term, hide the popup and clear any pending items 292 * 293 * @return void 294 */ 295 WPGMZA.Autocomplete.prototype.find = function(){ 296 this.setTerm(this.element.value); 297 298 if(this.getTerm()){ 299 /* We have a term, a request will likely run, clear the list */ 300 this.hide(); 301 302 this.clear(); 303 } else { 304 this.setBusy(false); 305 } 306 } 307 308 /** 309 * Present items to the user to select from the list 310 * 311 * This requires items to be stored in the state, with the correct formatting for our internal modules 312 * 313 * Once ready we will bind event listeners and show the list 314 * 315 * @return void 316 */ 317 WPGMZA.Autocomplete.prototype.present = function(){ 318 if(this.state.items && this.state.items.length){ 319 let html = ''; 320 for(let location of this.state.items){ 321 if(location.address){ 322 let dataAddress = location.address.replace(/<[^>]*>/g, ''); 323 html += `<div class='wpgmza-internal-autocomplete-location' data-address='${dataAddress}' data-coordinates='${location.coordinates ? location.coordinates : ''}'>`; 324 if(location.display){ 325 /* Has a different display value */ 326 html += `<strong data-autocomplete-field='display'>${location.display}</strong>`; 327 html += `<span data-autocomplete-field='adr'>${location.address}</span>`;
328 } else { 329 /* Use address as if it was display */ 330 html += `<strong data-autocomplete-field='display'>${location.address}</strong>`; 331 } 332 333 if(location.type || location.country){ 334 html += `<span data-autocomplete-meta-list>`; 335 if(location.type){ 336 html += `<small data-autocomplete-field='type'>${location.type}</small>`; 337 } 338 339 if(location.country){ 340 html += `<small data-autocomplete-field='country'>${location.country}</small>`; 341 } 342 html += `</span>`; 343 344 } 345 346 html += `</div>`; 347 } 348 349 if(html && html.length){ 350 this.components.list.innerHTML = html; 351 352 this.show(); 353 this.autoplace(); 354 355 /* Now bind the event listener, and make it fill the field on click */ 356 const items = this.components.list.querySelectorAll('.wpgmza-internal-autocomplete-location'); 357 for(let item of items){ 358 item.addEventListener('click', (event) => { 359 event.preventDefault(); 360 361 let address = item.getAttribute('data-address'); 362 if(address){ 363 this.element.value = address; 364 } 365 366 this.hide(); 367 this.clear(); 368 369 this.state.term = ""; 370 }); 371 } 372 } 373 } 374 } else { 375 this.hide(); 376 } 377 } 378 379 /** 380 * Show the suggestion list 381 * 382 * Only works if we have items in the list 383 * 384 * @return void 385 */ 386 WPGMZA.Autocomplete.prototype.show = function(){ 387 if(!this.isInternal()){ 388 return; 389 } 390 391 if(this.components && this.components.list && this.state.items && this.state.items.length){ 392 this.components.list.classList.remove('wpgmza-hidden'); 393 } 394 } 395 396 /** 397 * Hide the suggestion list 398 * 399 * @return void 400 */ 401 WPGMZA.Autocomplete.prototype.hide = function(){ 402 if(!this.isInternal()){ 403 return; 404 } 405 406 if(this.components && this.components.list){ 407 this.components.list.classList.add('wpgmza-hidden'); 408 } 409 } 410 411 /** 412 * Correct placement of the autocomplete list 413 * 414 * Useful as/when a content shift or browser scroll happens 415 * 416 * @return void 417 */ 418 WPGMZA.Autocomplete.prototype.autoplace = function(){ 419 if(!this.isInternal()){ 420 return; 421 } 422 423 if(this.components && this.components.list){ 424 const target = this.element; 425 const boundingRect = target.getBoundingClientRect(); 426 if(boundingRect.width){ 427 this.components.list.style.width = `${boundingRect.width}px`; 428 this.components.list.style.left = `${boundingRect.left}px`; 429 this.components.list.style.top = `${boundingRect.bottom}px`; 430 } 431 } 432 } 433 434 /** 435 * Clear the items and empty the HTML within the list 436 * 437 * @return void 438 */ 439 WPGMZA.Autocomplete.prototype.clear = function(){ 440 if(!this.isInternal()){ 441 return; 442 } 443 444 this.clearItems(); 445 this.components.list.innerHtml = ""; 446 } 447 448 /** 449 * Get the current search term 450 * 451 * @return string 452 */ 453 WPGMZA.Autocomplete.prototype.getTerm = function(){ 454 return this.state.term && typeof this.state.term === 'string' && this.state.term.length ? this.state.term : false; 455 } 456 457 /** 458 * Set the current search term 459 * 460 * @param string term 461 * 462 * @return void 463 */ 464 WPGMZA.Autocomplete.prototype.setTerm = function(term){ 465 if(term && term.trim() !== this.state.term){ 466 this.state.term = term.trim(); 467 } 468 } 469 470 /** 471 * Clear the current items list 472 * 473 * @return void 474 */ 475 WPGMZA.Autocomplete.prototype.clearItems = function(){ 476 this.setItems([]); 477 } 478 479 /** 480 * Set the current items list within the state controller 481 * 482 * This must be an array, and will be passed through the remapItem method for filtering within any sub-class 483 * 484 * @param array items 485 * 486 * @return void 487 */ 488 WPGMZA.Autocomplete.prototype.setItems = function(items){ 489 let remapped = []; 490 if(items && items instanceof Array){ 491 for(let item of items){ 492 let remap = this.remapItem(item); 493 if(remap){ 494 remapped.push(remap); 495 } 496 } 497 } 498 this.state.items = remapped; 499 } 500 501 /** 502 * Remap a location item into our local structure 503 * 504 * In this base method we do nothing with this, but in your sub class, you would convert the returned data into 505 * the same structure we expect (display, address, coordinates, type) 506 * 507 * @param object item 508 * 509 * @return object 510 */ 511 WPGMZA.Autocomplete.prototype.remapItem = function(item){ 512 return item; 513 } 514 515 /** 516 * Get the internal service provider 517 * 518 * This can change internally, as seen in the Google Autocomplete module where we might switch between legacy and the places API 519 * 520 * In this case we return false to prevent any application 521 * 522 * @return int 523 */ 524 WPGMZA.Autocomplete.prototype.getProvider = function() { 525 return false;
526 } 527 528 /** 529 * Converts a provider type (enum) into a string tag 530 * 531 * @return string 532 */ 533 WPGMZA.Autocomplete.prototype.getProviderSlug = function(){ 534 let slug = 'generic' 535 switch(this.getProvider()){ 536 case WPGMZA.Autocomplete.Providers.GOOGLE_AUTOCOMPLETE: 537 slug = 'google-autocomplete'; 538 break; 539 case WPGMZA.Autocomplete.Providers.GOOGLE_PLACES: 540 slug = 'google-places-search'; 541 break; 542 case WPGMZA.Autocomplete.Providers.CLOUD_API: 543 slug = 'legacy-cloud'; 544 break; 545 case WPGMZA.Autocomplete.Providers.GOOGLE_PLACES: 546 slug = 'nominatim'; 547 break; 548 } 549 return slug; 550 } 551 552 /** 553 * Check if this autocomplete instance is being managed internally for it's UI or not 554 * 555 * @return bool 556 */ 557 WPGMZA.Autocomplete.prototype.isInternal = function(){ 558 return this.state.internal; 559 } 560 561 /** 562 * Set this instance as an internal module 563 * 564 * @param bool internal 565 * 566 * @return void 567 */ 568 WPGMZA.Autocomplete.prototype.setInternal = function(internal){ 569 this.state.internal = internal; 570 } 571 572 /** 573 * Check if this instance is ready to be used 574 * 575 * @return bool 576 */ 577 WPGMZA.Autocomplete.prototype.isReady = function(){ 578 return this.state.ready; 579 } 580 581 /** 582 * Set this instance as ready to be used 583 * 584 * You should do this in your sub modules during the prepare or autoload phases 585 * 586 * @param bool ready 587 * 588 * @return void 589 */ 590 WPGMZA.Autocomplete.prototype.setReady = function(ready){ 591 this.state.ready = ready; 592 } 593 594 /** 595 * Mark this instance as busy or not 596 * 597 * We will add a special attribute to the element or it's parent to help with styling this 598 * 599 * @param bool busy 600 * 601 * @return void 602 */ 603 WPGMZA.Autocomplete.prototype.setBusy = function(busy){ 604 this.state.busy = busy; 605 606 if(this.element){ 607 let target = this.element; 608 if(target.parentNode && target.parentNode.classList){ 609 const parentClassList = ['wpgmza-address-container', 'wpgmza-marker-editor-address-field', 'wpgmza-directions-input-row']; 610 for(let parentClass of parentClassList){ 611 if(target.parentNode.classList.contains(parentClass)){ 612 target = target.parentNode; 613 } 614 } 615 } 616 617 target.setAttribute('data-autocomplete-state', busy ? 'busy' : ''); 618 } 619 } 620});
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.