1(function($){ 2 3 "use strict"; 4 5 /** 6 * initialize_field 7 * 8 * This function will initialize the $field. 9 * 10 * @date 30/11/17 11 * @since 5.6.5 12 * 13 * @param n/a 14 * @return n/a 15 */ 16 17 function initialize_field( $field ) { 18 // Nothing here yet. 19 } 20 21 22 if( typeof acf.add_action !== 'undefined' ) { 23 24 /* 25 * ready & append (ACF5) 26 * 27 * These two events are called when a field element is ready for initizliation. 28 * - ready: on page load similar to $(document).ready() 29 * - append: on new DOM elements appended via repeater field or other AJAX calls 30 * 31 * @param n/a 32 * @return n/a 33 */ 34 35 acf.add_action('ready_field/type=google_map_multi', initialize_field); 36 acf.add_action('append_field/type=google_map_multi', initialize_field); 37 38 39 } else { 40 41 /* 42 * acf/setup_fields (ACF4) 43 * 44 * These single event is called when a field element is ready for initizliation. 45 * 46 * @param event an event object. This can be ignored 47 * @param element An element which contains the new HTML 48 * @return n/a 49 */ 50 51 $(document).on('acf/setup_fields', function(e, postbox){ 52 53 // find all relevant fields 54 $(postbox).find('.field[data-field_type="google_map_multi"]').each(function(){ 55 56 // initialize 57 initialize_field( $(this) ); 58 59 }); 60 61 }); 62 63 } 64 65 var Field = acf.Field.extend({ 66 67 type: 'google_map_multi', 68 69 map: false, 70 71 wait: 'load', 72 73 events: { 74 'click a[data-name="clear"]': 'onClickClear', 75 'click a[data-name="locate"]': 'onClickLocate', 76 'click a[data-name="search"]': 'onClickSearch', 77 'keydown .search': 'onKeydownSearch', 78 'keyup .search': 'onKeyupSearch', 79 'focus .search': 'onFocusSearch', 80 'blur .search': 'onBlurSearch', 81 'showField': 'onShow', 82 }, 83 84 // List of values in parsed JSON. 85 // Used only for the hidden ACF field that holds the field's values. 86 vals: [], 87 88 markers: [], 89 90 draggedMarkerIndex: '', 91 92 $control: function(){ 93 return this.$('.acf-google-map-multi'); 94 }, 95 96 $search: function(){ 97 return this.$('.search'); 98 }, 99 100 $canvas: function(){ 101 return this.$('.canvas'); 102 }, 103 104 $markerList: function(){ 105 return this.$('.gmm-markers-list'); 106 }, 107 108 setState: function( state ){ 109 110 // Remove previous state classes. 111 this.$control().removeClass( '-value -loading -searching' ); 112 113 // Determine auto state based of current value. 114 if( state === 'default' ) { 115 state = this.val() ? 'value' : ''; 116 } 117 118 // Update state class. 119 if( state ) { 120 this.$control().addClass( '-' + state ); 121 } 122 }, 123 124 getValue: function(){ 125 var val = this.$input().val(); 126 if( val ) { 127 return JSON.parse( val ) 128 } else { 129 return false; 130 } 131 }, 132 133 setValue: function( val, silent ){ 134 135 // Convert input value. 136 var valAttr = ''; 137 if( val ) { 138 valAttr = JSON.stringify( val ); 139 } 140 141 // Update input (with change). 142 acf.val( this.$input(), valAttr ); 143 144 // Bail early if silent update. 145 if( silent ) { 146 return; 147 } 148 149 // Render. 150 this.renderVal( val ); 151 152 /** 153 * Fires immediately after the value has changed. 154 * 155 * @date 12/02/2014 156 * @since 5.0.0 157 * 158 * @param object|string val The new value. 159 * @param object map The Google Map isntance. 160 * @param object field The field instance. 161 */ 162 acf.doAction('google_map_multi_change', val, this.map, this); 163 }, 164 165 renderVal: function( val ){ 166 167 // Value. 168 if( val ) { 169 this.setState( 'value' ); 170 this.$search().val( val.address ); 171 // this.setPosition( val.lat, val.lng ); 172 173 // No value. 174 } else { 175 this.setState( '' ); 176 this.$search().val( '' ); 177 this.map.marker.setVisible( false ); 178 } 179 }, 180 181 newLatLng: function( lat, lng ){ 182 return new google.maps.LatLng( parseFloat(lat), parseFloat(lng) ); 183 }, 184 185 initialize: function(){ 186 // Ensure Google API is loaded and then initialize map. 187 gmmWithAPI( this.initializeMap.bind(this) ); 188 }, 189 190 initializeMap: function(){ 191 if (!gmmGeocoder) { 192 gmmGeocoder = new google.maps.Geocoder(); 193 } 194 195 // Get value ignoring conditional logic status. 196 var val = this.getValue(); 197 198 // Construct default args. 199 var args = acf.parseArgs(val, { 200 zoom: this.get('zoom'), 201 lat: this.get('lat'), 202 lng: this.get('lng') 203 }); 204 205 // Create Map. 206 var mapArgs = { 207 scrollwheel: false, 208 mapTypeId: google.maps.MapTypeId.ROADMAP, 209 marker: { 210 draggable: true, 211 raiseOnDrag: true 212 }, 213 autocomplete: {} 214 }; 215 mapArgs = acf.applyFilters( 'google_map_multi_args', mapArgs, this ); 216 var map = new google.maps.Map( this.$canvas()[0], mapArgs ); 217 218 // Maybe Create Autocomplete. 219 var autocomplete = false;
220 if( acf.isset(google, 'maps', 'places', 'Autocomplete') ) { 221 var autocompleteArgs = mapArgs.autocomplete || {}; 222 autocompleteArgs = acf.applyFilters('google_map_autocomplete_args', autocompleteArgs, this); 223 autocomplete = new google.maps.places.Autocomplete( this.$search()[0], autocompleteArgs ); 224 autocomplete.bindTo('bounds', map); 225 } 226 227 // Add map events. 228 this.addMapEvents( this, map, autocomplete ); 229 230 // Remove any residual data from our marker list. At this point, we shouldn't have anything in the list. 231 this.markers = []; 232 233 // Sets up all the markers if this field has values. 234 if( val ) { 235 // Create bounds. 236 var bounds = new google.maps.LatLngBounds(); 237 238 this.vals = val; 239 for (let i = 0; i < this.vals.length; i++) { 240 var latLng = { lat: this.vals[i].lat, lng: this.vals[i].lng }; 241 242 this.markers.push({ 243 values: this.vals[i], 244 marker: {}, 245 }); 246 247 this.addMarker( latLng, map ); 248 bounds.extend( latLng ); 249 } 250 251 if (this.vals.length === 1) { 252 // If we only have 1 marker, set our map's zoom and center to that marker's values. 253 map.setZoom(this.vals[0].zoom); 254 map.setCenter( {lat: this.vals[0].lat, lng: this.vals[0].lng} ); 255 } 256 else { 257 // Tell our map to fit the bounds defined by our markers. 258 map.fitBounds( bounds ); 259 } 260 261 } 262 else { 263 // We set the zoom and center to default values if there are no markers. 264 map.setZoom(8); 265 map.setCenter( {lat: 51.4934, lng: 0.0098} ); 266 267 // Reset our vals storage for the new field with no data yet. 268 this.vals = []; 269 } 270 271 // Append references. 272 map.acf = this; 273 map.autocomplete = autocomplete; 274 275 // Render the initial markers list under the map. 276 this.renderMarkerList(); 277 278 // Set this object's map property to the map we've just created. 279 // Don't change the map in this function after this line. 280 this.map = map; 281 282 /** 283 * Fires immediately after the Google Map has been initialized. 284 * 285 * @date 12/02/2014 286 * @since 5.0.0 287 * 288 * @param object map The Google Map instance. 289 * @param object marker The Google Map marker instance. 290 * @param object field The field instance. 291 */ 292 acf.doAction('google_map_multi_init', map, this); 293 294 // Remove the loading icon. 295 $('.acf-field-google-map-multi .acf-loading').remove(); 296 }, 297 298 addMapEvents: function( field, map, autocomplete ){ 299 300 // Click map. 301 google.maps.event.addListener( map, 'click', function( e ) { 302 if (field.validateMaxPins()) { 303 var lat = e.latLng.lat(); 304 var lng = e.latLng.lng(); 305 306 field.searchPosition( lat, lng ); 307 field.addMarker( e.latLng ); 308 } 309 }); 310 311 // Autocomplete search. 312 if( autocomplete ) { 313 google.maps.event.addListener(autocomplete, 'place_changed', function() { 314 if (field.validateMaxPins()) { 315 var place = this.getPlace(); 316 field.searchPlace(place); 317 318 // If places.geometry doesn't exist, it means we've had to geocode an address. Adding the marker and 319 // the rest would be done in that geocode callback. 320 if (place.geometry !== undefined) { 321 var latLng = {lat: place.geometry.location.lat(), lng: place.geometry.location.lng()}; 322 field.addMarker(latLng); 323 324 // Add the new place just searched to the bounds and fit the map to these new bounds so the map 325 // expands to show the new marker that's just been added. 326 map.fitBounds(field.createMapBounds(field)); 327 328 // Re-render the list of markers under the map. 329 field.renderMarkerList(); 330 } 331 } 332 }); 333 } 334 335 // Detect zoom change. 336 google.maps.event.addListener( map, 'zoom_changed', function(){ 337 var val = field.val(); 338 if( val ) { 339 val.zoom = map.getZoom(); 340 field.setValue( val, true ); 341 } 342 }); 343 }, 344 345 /** 346 * 347 * @param latLng latLng object. 348 * @param map The Google Map object. If not given, it will try to use this field object's map property. 349 * @param field 350 * @returns {*} 351 */ 352 addMarker: function( latLng, map = this.map, field = this ) { 353 let markerArgs = { 354 draggable: true, 355 raiseOnDrag: true, 356 position: latLng, 357 map: map 358 }; 359 markerArgs = acf.applyFilters('google_map_multi_marker_args', markerArgs, field); 360 let marker = new google.maps.Marker( markerArgs ); 361 362 // Add the new marker to our list of markers. 363 field.markers[field.markers.length - 1].marker = marker; 364 365 // Add a right-click event to allow users to remove this marker. 366 marker.addListener('rightclick', function() { 367 field.removeMarker( marker, field ); 368 }); 369 370 // Remove the marker from the data at the start of dragging a marker. 371 marker.addListener('dragstart', function () { 372 let markerIndex = field.markers.findIndex(element => marker === element.marker ); 373 field.removeMarkerDataFromFieldData( marker, field, markerIndex ); 374 field.markers.splice( markerIndex, 1 ); 375 }); 376 377 // Add a drag-end event to update the marker's data when it's dragged on the map. 378 marker.addListener('dragend', function () { 379 // TODO Try to update the existing marker that's being dragged instead of creating a new item in the list. 380 field.searchPosition( marker.position.lat(), marker.position.lng() ); 381 field.markers[field.markers.length - 1].marker = marker; 382 }); 383 384 return marker; 385 }, 386 387 removeMarker: function( marker, field = this ) { 388 // Remove the marker from this object's markersArray property. 389 var markerIndex = field.markers.findIndex(element => marker === element.marker ); 390 391 // Check if an index was found before we remove anything. 392 if (markerIndex >= 0) { 393 // Remove the marker's data from the hidden field of JSON. 394 field.removeMarkerDataFromFieldData(marker, field, markerIndex); 395 field.markers.splice(markerIndex, 1); 396
397 // Re-render the list of markers under the map after removing it from our values. 398 this.renderMarkerList(); 399 } 400 401 // Remove the marker from the map. 402 marker.setMap(null); 403 }, 404 405 removeMarkerDataFromFieldData: function( marker, field = this, markerIndex = false ) { 406 if (!markerIndex) { 407 // If the markerIndex hasn't been provided, we need to find it. 408 markerIndex = field.markers.findIndex(element => marker === element.marker ); 409 } 410 field.vals.splice(markerIndex, 1); 411 field.val( this.vals ); 412 }, 413 414 /** 415 * Checks if the maximum number of pins has been added to the current map. 416 * 417 * @return boolean 418 */ 419 validateMaxPins: function() { 420 // Check for no limit. 421 if (this.data.maxPins <= 0) { 422 return true; 423 } 424 425 // Check the limit against the current amount of markers. 426 if (this.markers.length < this.data.maxPins) { 427 return true; 428 } 429 else { 430 this.showNotice({ 431 text: acf.__('You can\'t add anymore pins. The maximum number of pins is ' + this.data.maxPins), 432 type: 'warning' 433 }); 434 435 return false; 436 } 437 }, 438 439 /** 440 * Creates Google Map LatLngBounds for all the markers in this object's markersArray property. 441 * 442 * @param field 443 */ 444 createMapBounds: function( field = this ) { 445 var newBounds = new google.maps.LatLngBounds(); 446 447 for ( let i = 0; i < field.markers.length; i++ ) { 448 newBounds.extend( {lat: field.markers[i].marker.position.lat(), lng: field.markers[i].marker.position.lng()} ); 449 } 450 451 return newBounds; 452 }, 453 454 renderMarkerList: function() { 455 this.$markerList().empty(); 456 for (let i = 0; i < this.markers.length; i++) { 457 var $markerWrap = $('<div>', { 458 'class': 'gmm-marker' 459 }).mouseover({field: this}, function (event) { 460 event.data.field.markers[i].marker.setOptions({ 461 icon: pluginData.pluginUrl + 'assets/images/marker-big.png', 462 }); 463 acf.doAction('google_map_multi_marker_list_mouseover', event.data.field.markers[i], event.data.field); 464 }).mouseout({field: this}, function (event) { 465 event.data.field.markers[i].marker.setOptions({ 466 icon: null 467 }); 468 acf.doAction('google_map_multi_marker_list_mouseout', event.data.field.markers[i], event.data.field); 469 }); 470 471 var $address = $('<span>', { 472 'class': 'gmm-marker--address', 473 }).text(this.markers[i].values.address).appendTo($markerWrap); 474 475 var $deleteIcon = $('<span>', { 476 'class': 'gmm-marker--delete-icon dashicons dashicons-trash', 477 }).click({field: this}, function (event) { 478 event.data.field.removeMarker(event.data.field.markers[i].marker); 479 }).appendTo($markerWrap); 480 481 acf.doAction('google_map_multi_render_marker_list', $markerWrap, this); 482 483 this.$markerList().append($markerWrap); 484 } 485 486 // Remove any notices from the field. 487 this.removeNotice(); 488 }, 489 490 searchPosition: function( lat, lng ){ 491 // Start Loading. 492 this.setState( 'loading' ); 493 494 // Set up the new marker object to contain the values and marker object. 495 this.markers.push({ 496 values: {}, 497 marker: {} 498 }); 499 500 // Query Geocoder. 501 var latLng = { lat: lat, lng: lng }; 502 gmmGeocoder.geocode({ location: latLng }, function( results, status ){ 503 console.log('geocode callback'); 504 // End Loading. 505 this.setState( '' ); 506 507 // Status failure. 508 if( status !== 'OK' ) { 509 this.showNotice({ 510 text: acf.__('Location not found: %s').replace('%s', status), 511 type: 'warning' 512 }); 513 514 // Remove the empty item we just added to this.markers. 515 this.markers.splice(this.markers.length - 1, 1); 516 517 // Re-render the list of markers under the map. 518 this.renderMarkerList(); 519 520 // Success. 521 } else { 522 var val = this.parseResult( results[0] ); 523 524 // Override lat/lng to match user defined marker location. 525 // Avoids issue where marker "snaps" to nearest result. 526 val.lat = lat; 527 val.lng = lng; 528 529 // Pushing the newest place to the list of values. 530 this.vals.push( val ); 531 532 this.markers[this.markers.length - 1].values = val; 533 534 // Re-render the list of markers under the map. 535 this.renderMarkerList(); 536 537 // Setting the value of the hidden field to the JSON list of markers. 538 this.val( this.vals ); 539 } 540 541 }.bind( this )); 542 }, 543 544 searchPlace: function( place ){ 545 // Bail early if no place. 546 if( !place ) { 547 return; 548 } 549 550 // Set up the new marker object to contain the values and marker object. 551 this.markers.push({ 552 values: {}, 553 marker: {} 554 }); 555 556 // Selecting from the autocomplete dropdown will return a rich PlaceResult object. 557 // Be sure to over-write the "formatted_address" value with the one displayed to the user for best UX. 558 if( place.geometry ) { 559 place.formatted_address = this.$search().val(); 560 var val = this.parseResult( place ); 561 562 this.markers[this.markers.length - 1].values = val; 563 564 // Pushing the newest place to the list of values. 565 this.vals.push( val ); 566 567 // Setting the value of the hidden field to the JSON list of markers. 568 this.val( this.vals ); 569 570 // Searching a custom address will return an empty PlaceResult object. 571 } else if( place.name ) { 572 this.searchAddress( place.name ); 573 } 574 }, 575 576 searchAddress: function( address ){ 577 // Bail early if no address. 578 if( !address ) { 579 return; 580 } 581 582 // Allow "lat,lng" search. 583 var latLng = address.split(','); 584 if( latLng.length == 2 ) { 585 var lat = parseFloat(latLng[0]); 586 var lng = parseFloat(latLng[1]); 587 if( lat && lng ) { 588 return this.searchPosition( lat, lng ); 589 } 590 } 591 592 // Start Loading. 593 this.setState( 'loading' ); 594 595 // Query Geocoder. 596 gmmGeocoder.geocode({ address: address }, function( results, status ){ 597 // End Loading. 598 this.setState( '' ); 599 600 // Status failure. 601 if( status !== 'OK' ) { 602 this.showNotice({ 603 text: acf.__('Location not found: %s').replace('%s', status), 604 type: 'warning' 605 }); 606 607 // Success. 608 } else { 609 var val = this.parseResult( results[0] ); 610 611 // Override address data with parameter allowing custom address to be defined in search. 612 val.address = address; 613 614 // Pushing the newest place to the list of values. 615 this.vals.push( val ); 616 617 this.markers[this.markers.length - 1].values = val; 618 this.addMarker({lat: val.lat, lng: val.lng}); 619 620 // Add the new place just searched to the bounds and fit the map to these new bounds so the map 621 // expands to show the new marker that's just been added.
622 this.map.fitBounds( this.createMapBounds() ); 623 624 // Re-render the list of markers under the map. 625 this.renderMarkerList(); 626 627 // Setting the value of the hidden field to the JSON list of markers. 628 this.val( this.vals ); 629 } 630 631 }.bind( this )); 632 }, 633 634 searchLocation: function(){ 635 // Check HTML5 geolocation. 636 if( !navigator.geolocation ) { 637 return alert( acf.__('Sorry, this browser does not support geolocation') ); 638 } 639 640 // Start Loading. 641 this.setState( 'loading' ); 642 643 // Query Geolocation. 644 navigator.geolocation.getCurrentPosition( 645 646 // Success. 647 function( results ){ 648 649 // End Loading. 650 this.setState( '' ); 651 652 // Search position. 653 var lat = results.coords.latitude; 654 var lng = results.coords.longitude; 655 this.searchPosition( lat, lng ); 656 657 }.bind(this), 658 659 // Failure. 660 function( error ){ 661 this.setState( '' ); 662 }.bind(this) 663 ); 664 }, 665 666 /** 667 * parseResult 668 * 669 * Returns location data for the given GeocoderResult object. 670 * 671 * @date 15/10/19 672 * @since 5.8.6 673 * 674 * @param object obj A GeocoderResult object. 675 * @return object 676 */ 677 parseResult: function( obj ) { 678 679 // Construct basic data. 680 var result = { 681 address: obj.formatted_address, 682 lat: obj.geometry.location.lat(), 683 lng: obj.geometry.location.lng(), 684 }; 685 686 // Add zoom level. 687 result.zoom = this.map.getZoom(); 688 689 // Add place ID. 690 if( obj.place_id ) { 691 result.place_id = obj.place_id; 692 } 693 694 // Add place name. 695 if( obj.name ) { 696 result.name = obj.name; 697 } 698 699 // Create search map for address component data. 700 var map = { 701 street_number: [ 'street_number' ], 702 street_name: [ 'street_address', 'route' ], 703 city: [ 'locality' ], 704 state: [ 705 'administrative_area_level_1', 706 'administrative_area_level_2', 707 'administrative_area_level_3', 708 'administrative_area_level_4', 709 'administrative_area_level_5' 710 ], 711 post_code: [ 'postal_code' ], 712 country: [ 'country' ] 713 }; 714 715 // Loop over map. 716 for( var k in map ) { 717 var keywords = map[ k ]; 718 719 // Loop over address components. 720 for( var i = 0; i < obj.address_components.length; i++ ) { 721 var component = obj.address_components[ i ]; 722 var component_type = component.types[0]; 723 724 // Look for matching component type. 725 if( keywords.indexOf(component_type) !== -1 ) { 726 727 // Append to result. 728 result[ k ] = component.long_name; 729 730 // Append short version. 731 if( component.long_name !== component.short_name ) { 732 result[ k + '_short' ] = component.short_name; 733 } 734 } 735 } 736 } 737 738 /** 739 * Filters the parsed result. 740 * 741 * @date 18/10/19 742 * @since 5.8.6 743 * 744 * @param object result The parsed result value. 745 * @param object obj The GeocoderResult object. 746 */ 747 return acf.applyFilters('google_map_multi_result', result, obj, this.map, this); 748 }, 749 750 onClickClear: function(){ 751 this.val( false ); 752 }, 753 754 onClickLocate: function(){ 755 this.searchLocation(); 756 }, 757 758 onClickSearch: function(){ 759 this.searchAddress( this.$search().val() ); 760 }, 761 762 onFocusSearch: function( e, $el ){ 763 this.setState( 'searching' ); 764 }, 765 766 onBlurSearch: function( e, $el ){ 767 768 // Get saved address value. 769 var val = this.val(); 770 var address = val ? val.address : ''; 771 772 // Remove 'is-searching' if value has not changed. 773 if( $el.val() === address ) { 774 this.setState( 'default' ); 775 } 776 }, 777 778 onKeyupSearch: function( e, $el ){ 779 780 // Clear empty value. 781 if( !$el.val() ) { 782 this.val( false ); 783 } 784 }, 785 786 // Prevent form from submitting. 787 onKeydownSearch: function( e, $el ){ 788 if( e.which == 13 ) { 789 e.preventDefault(); 790 $el.blur(); 791 } 792 }, 793 794 // Center map once made visible. 795 onShow: function(){ 796 if( this.map ) { 797 this.setTimeout( this.center ); 798 } 799 }, 800 }); 801 802 acf.registerFieldType( Field ); 803 804 // Vars. 805 var gmmLoading = false; 806 var gmmGeocoder = false;
807 808 /** 809 * gmmWithAPI 810 * 811 * Loads the Google Maps API library and triggers callback. 812 * 813 * @date 28/3/19 814 * @since 5.7.14 815 * 816 * @param function callback The callback to excecute. 817 * @return void 818 */ 819 820 function gmmWithAPI( callback ) { 821 // Check if geocoder exists. 822 if( gmmGeocoder ) { 823 return callback(); 824 } 825 826 // Check if geocoder API exists. 827 if( acf.isset(window, 'google', 'maps', 'Geocoder') ) { 828 gmmGeocoder = new google.maps.Geocoder(); 829 return callback(); 830 } 831 832 // Check if we're trying to load 1 or more Google Map fields. We need to execute our callback differently 833 // depending on this. 834 let otherMapFields = acf.findFields({ 835 type: 'google_map' 836 }); 837 838 // Check if the google_map_api_loaded action has been added. 839 if ( !otherMapFields.length ) { 840 // In this case, the there's no other Google Map field used, meaning we need to load the maps API ourselves. 841 acf.addAction( 'google_map_multi_api_loaded', callback ); 842 843 // Bail early if already loading API. 844 if( gmmLoading ) { 845 return; 846 } 847 848 // load api 849 var url = acf.get('google_map_api'); 850 if( url ) { 851 852 // Set loading status. 853 gmmLoading = true; 854 855 // Load API 856 $.ajax({ 857 url: url, 858 dataType: 'script', 859 cache: true, 860 success: function() { 861 gmmGeocoder = new google.maps.Geocoder(); 862 acf.doAction('google_map_multi_api_loaded'); 863 } 864 }); 865 } 866 } 867 else { 868 // The Maps API will be loaded by the original Google Map field. 869 // All we need to do is add our callback to the the action used in the original Google Map field so it gets 870 // triggered together with that. 871 acf.addAction( 'google_map_api_loaded', callback ); 872 } 873 } 874 875})(jQuery);
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.