web-dev-qa-db-fra.com

Google maps Places API V3 autocomplete - sélectionnez la première option à entrer

J'ai implémenté avec succès la fonctionnalité de saisie semi-automatique de Google Maps Places V3 sur mon champ de saisie, comme indiqué dans http://code.google.com/intl/sk-SK/apis/maps/documentation/javascript/places.html#places_autocomplete . Cela fonctionne bien, mais j'aimerais savoir comment je peux le faire sélectionner la première option parmi les suggestions lorsqu'un utilisateur appuie sur Entrée. J'imagine que j'aurais besoin de la magie JS, mais je suis très novice dans JS et je ne sais pas par où commencer.

Merci d'avance!

59
Daniel Grezo

J'ai eu le même problème lors de la mise en œuvre de la saisie semi-automatique sur un site sur lequel j'ai travaillé récemment. Voici la solution que j'ai trouvée:

$("input").focusin(function () {
    $(document).keypress(function (e) {
        if (e.which == 13) {
            var firstResult = $(".pac-container .pac-item:first").text();

            var geocoder = new google.maps.Geocoder();
            geocoder.geocode({"address":firstResult }, function(results, status) {
                if (status == google.maps.GeocoderStatus.OK) {
                    var lat = results[0].geometry.location.lat(),
                        lng = results[0].geometry.location.lng(),
                        placeName = results[0].address_components[0].long_name,
                        latlng = new google.maps.LatLng(lat, lng);

                        $(".pac-container .pac-item:first").addClass("pac-selected");
                        $(".pac-container").css("display","none");
                        $("#searchTextField").val(firstResult);
                        $(".pac-container").css("visibility","hidden");

                    moveMarker(placeName, latlng);

                }
            });
        } else {
            $(".pac-container").css("visibility","visible");
        }

    });
});

http://jsfiddle.net/dodger/pbbhH/

42
dodger

Voici une solution qui ne génère pas de requête de géocodage susceptible de renvoyer un résultat incorrect: http://jsfiddle.net/amirnissim/2D6HW/

Il simule une pression sur down-arrow chaque fois que l'utilisateur clique sur return dans le champ de saisie semi-automatique. le  événement est déclenché avant la return événement pour simuler la sélection de la première suggestion à l’aide du clavier.

Voici le code (testé sur Chrome et Firefox):

<script src='https://ajax.googleapis.com/ajax/libs/jquery/1.7.2/jquery.min.js'></script>
<script src="https://maps.googleapis.com/maps/api/js?sensor=false&libraries=places"></script>
<script>
    var pac_input = document.getElementById('searchTextField');

    (function pacSelectFirst(input) {
        // store the original event binding function
        var _addEventListener = (input.addEventListener) ? input.addEventListener : input.attachEvent;

        function addEventListenerWrapper(type, listener) {
            // Simulate a 'down arrow' keypress on hitting 'return' when no pac suggestion is selected,
            // and then trigger the original listener.
            if (type == "keydown") {
                var orig_listener = listener;
                listener = function(event) {
                    var suggestion_selected = $(".pac-item-selected").length > 0;
                    if (event.which == 13 && !suggestion_selected) {
                        var simulated_downarrow = $.Event("keydown", {
                            keyCode: 40,
                            which: 40
                        });
                        orig_listener.apply(input, [simulated_downarrow]);
                    }

                    orig_listener.apply(input, [event]);
                };
            }

            _addEventListener.apply(input, [type, listener]);
        }

        input.addEventListener = addEventListenerWrapper;
        input.attachEvent = addEventListenerWrapper;

        var autocomplete = new google.maps.places.Autocomplete(input);

    })(pac_input);
</script>
160
amirnissim

Voici un exemple de solution réelle, non-hacky. Il n'utilise pas de piratage de navigateur, etc., mais seulement des méthodes de l'API publique fournie par Google et documentée ici: API Google Maps

Le seul inconvénient est que des requêtes supplémentaires à Google sont nécessaires si l'utilisateur ne sélectionne pas d'élément dans la liste. L'avantage est que le résultat sera toujours correct, car la requête est exécutée de manière identique à la requête dans la saisie semi-automatique. Deuxièmement, en n'utilisant que des méthodes d'API publiques et en ne faisant pas appel à la structure HTML interne du widget de saisie semi-automatique, nous pouvons être certains que notre produit ne se cassera pas si Google apporte des modifications.

var input = /** @type {HTMLInputElement} */(document.getElementById('searchTextField'));
var autocomplete = new google.maps.places.Autocomplete(input);  
// These are my options for the AutoComplete
autocomplete.setTypes(['(cities)']);
autocomplete.setComponentRestrictions({'country': 'es'});

google.maps.event.addListener(autocomplete, 'place_changed', function() {
    result = autocomplete.getPlace();
    if(typeof result.address_components == 'undefined') {
        // The user pressed enter in the input 
        // without selecting a result from the list
        // Let's get the list from the Google API so that
        // we can retrieve the details about the first result
        // and use it (just as if the user had actually selected it)
        autocompleteService = new google.maps.places.AutocompleteService();
        autocompleteService.getPlacePredictions(
            {
                'input': result.name,
                'offset': result.name.length,
                // I repeat the options for my AutoComplete here to get
                // the same results from this query as I got in the 
                // AutoComplete widget
                'componentRestrictions': {'country': 'es'},
                'types': ['(cities)']
            },
            function listentoresult(list, status) {
                if(list == null || list.length == 0) {
                    // There are no suggestions available.
                    // The user saw an empty list and hit enter.
                    console.log("No results");
                } else {
                    // Here's the first result that the user saw
                    // in the list. We can use it and it'll be just
                    // as if the user actually selected it
                    // themselves. But first we need to get its details
                    // to receive the result on the same format as we
                    // do in the AutoComplete.
                    placesService = new google.maps.places.PlacesService(document.getElementById('placesAttribution'));
                    placesService.getDetails(
                        {'reference': list[0].reference},
                        function detailsresult(detailsResult, placesServiceStatus) {
                            // Here's the first result in the AutoComplete with the exact
                            // same data format as you get from the AutoComplete.
                            console.log("We selected the first item from the list automatically because the user didn't select anything");
                            console.log(detailsResult);
                        }
                    );
                }
            }
        );
    } else {
        // The user selected a result from the list, we can 
        // proceed and use it right away
        console.log("User selected an item from the list");
        console.log(result);
    }
});
20
Tomasz Matuszczyk

Il semble qu'il existe une solution bien meilleure et plus propre: utiliser google.maps.places.SearchBox au lieu de google.maps.places.Autocomplete. Un code est presque identique, il suffit d'obtenir le premier à partir de plusieurs emplacements. En appuyant sur la touche Entrer, la liste correcte est renvoyée - elle est donc prête à l'emploi et les hacks ne sont plus nécessaires.

Voir l'exemple de page HTML:

http://rawgithub.com/klokan/8408394/raw/5ab795fb36c67ad73c215269f61c7648633ae53e/places-enter-first-item.html

L'extrait de code pertinent est:

var searchBox = new google.maps.places.SearchBox(document.getElementById('searchinput'));

google.maps.event.addListener(searchBox, 'places_changed', function() {
  var place = searchBox.getPlaces()[0];

  if (!place.geometry) return;

  if (place.geometry.viewport) {
    map.fitBounds(place.geometry.viewport);
  } else {
    map.setCenter(place.geometry.location);
    map.setZoom(16);
  }
});

Le code source complet de l'exemple est à: https://Gist.github.com/klokan/8408394

13
Klokan Technologies

Voici une réponse de travail pour 2018.  

Ceci combine les meilleures réponses sur cette page, utilise uniquement du JS pur et est écrit en clair ES6. Aucune requête jQuery, 2nd API, ou IIFE n'est requise.

Tout d’abord, supposons que vous ayez déjà configuré quelque chose comme ceci pour identifier votre champ d’adresse:

const field = document.getElementById('address-field') 
const autoComplete = new google.maps.places.Autocomplete(field)
autoComplete.setTypes(['address'])

Ajoutez ensuite ceci à la ligne suivante:

enableEnterKey(field)

Et puis ailleurs dans votre script, pour garder cette fonctionnalité bien et séparée dans votre code, ajoutez ceci:

  function enableEnterKey(input) {

    /* Store original event listener */
    const _addEventListener = input.addEventListener

    const addEventListenerWrapper = (type, listener) => {
      if (type === "keydown") {
        /* Store existing listener function */
        const _listener = listener
        listener = (event) => {
          /* Simulate a 'down arrow' keypress if no address has been selected */
          const suggestion_selected = document.getElementsByClassName('pac-item-selected').length > 0
          if (event.which === 13 && !suggestion_selected) {
            const e = JSON.parse(JSON.stringify(event))
            e.which = 40
            e.keyCode = 40
            _listener.apply(input, [e])
          }
          _listener.apply(input, [event])
        }
      }
      _addEventListener.apply(input, [type, listener])
    }

    input.addEventListener = addEventListenerWrapper
  }

Vous devriez être prêt à partir. Pour l’essentiel, la fonction enableEnterKey() capture chaque pression de retour/entrée dans le champ input et simule à la place une pression sur une flèche vers le bas. Il stocke et réassocie également des écouteurs et des événements pour conserver toutes les fonctionnalités de votre Autocomplete() de Google Maps.

Avec un merci évident aux réponses précédentes pour la plupart de ce code, notamment amirnissim et Alexander Schwarzman.

12
Tony Brasunas

Pour Google Places Autocomplete V3, la meilleure solution consiste à utiliser deux requêtes d'API. 

Voici le violon

La raison pour laquelle aucune autre réponse n’a suffi est qu’ils ont soit utilisé jquery pour imiter des événements (hacky), soit utilisé Geocoder ou la zone de recherche Google Places qui ne correspond pas toujours aux résultats de complétion automatique. Au lieu de cela, nous allons utiliser le service de saisie semi-automatique de Google décrit ici avec uniquement du javascript (pas de jquery).

Vous trouverez ci-dessous la solution la plus compatible entre navigateurs utilisant les API Google natives pour générer la zone de saisie semi-automatique, puis réexécutez la requête pour sélectionner la première option.

<script type="text/javascript" src="https://maps.googleapis.com/maps/api/js?libraries=places&language=en"></script>

Javascript

// For convenience, although if you are supporting IE8 and below
// bind() is not supported
var $ = document.querySelector.bind(document);

function autoCallback(predictions, status) {
    // *Callback from async google places call
    if (status != google.maps.places.PlacesServiceStatus.OK) {
        // show that this address is an error
        pacInput.className = 'error';
        return;
    }

    // Show a successful return
    pacInput.className = 'success';
    pacInput.value = predictions[0].description;
}


function queryAutocomplete(input) {
    // *Uses Google's autocomplete service to select an address
    var service = new google.maps.places.AutocompleteService();
    service.getPlacePredictions({
        input: input,
        componentRestrictions: {
            country: 'us'
        }
    }, autoCallback);
}

function handleTabbingOnInput(evt) {
    // *Handles Tab event on delivery-location input
    if (evt.target.id == "pac-input") {
        // Remove active class
        evt.target.className = '';

        // Check if a tab was pressed
        if (evt.which == 9 || evt.keyCode == 9) {
            queryAutocomplete(evt.target.value);
        }
    }
}

// ***** Initializations ***** //
// initialize pac search field //
var pacInput = $('#pac-input');
pacInput.focus();

// Initialize Autocomplete
var options = {
    componentRestrictions: {
        country: 'us'
    }
};
var autocomplete = new google.maps.places.Autocomplete(pacInput, options);
// ***** End Initializations ***** //

// ***** Event Listeners ***** //
google.maps.event.addListener(autocomplete, 'place_changed', function () {
    var result = autocomplete.getPlace();
    if (typeof result.address_components == 'undefined') {
        queryAutocomplete(result.name);
    } else {
        // returns native functionality and place object
        console.log(result.address_components);
    }
});

// Tabbing Event Listener
if (document.addEventListener) {
    document.addEventListener('keydown', handleTabbingOnInput, false);
} else if (document.attachEvent) { // IE8 and below
    document.attachEvent("onsubmit", handleTabbingOnInput);
}

// search form listener
var standardForm = $('#search-shop-form');
if (standardForm.addEventListener) {
    standardForm.addEventListener("submit", preventStandardForm, false);
} else if (standardForm.attachEvent) { // IE8 and below
    standardForm.attachEvent("onsubmit", preventStandardForm);
}
// ***** End Event Listeners ***** //

HTML

<form id="search-shop-form" class="search-form" name="searchShopForm" action="/impl_custom/index/search/" method="post">
    <label for="pac-input">Delivery Location</label>
        <input id="pac-input" type="text" placeholder="Los Angeles, Manhattan, Houston" autocomplete="off" />
        <button class="search-btn btn-success" type="submit">Search</button>
</form>

Le seul inconvénient est que l'implémentation native renvoie une structure de données différente même si les informations sont les mêmes. Ajuste en accord. 

9
AdamSchuld

Que dis-tu de ça?

$("input").keypress(function(event) {
  var firstValue = null;
  if (event.keyCode == 13 || event.keyCode == 9) {
    $(event.target).blur();
    if ($(".pac-container .pac-item:first span:eq(3)").text() == "") {
      firstValue = $(".pac-container .pac-item:first .pac-item-query").text();
    } else {
      firstValue = $(".pac-container .pac-item:first .pac-item-query").text() + ", " + $(".pac-container .pac-item:first span:eq(3)").text();
    }
    event.target.value = firstValue;
  } else
    return true;
});
2
Gangadhar Jannu

Je veux juste écrire une petite amélioration pour la réponse de amirnissim
Le script posté ne prend pas en charge IE8, car "event.which" semble toujours vide dans IE8.
Pour résoudre ce problème, il vous suffit de vérifier en plus "event.keyCode":

listener = function (event) {
  if (event.which == 13 || event.keyCode == 13) {
    var suggestion_selected = $(".pac-item.pac-selected").length > 0;
    if(!suggestion_selected){
      var simulated_downarrow = $.Event("keydown", {keyCode:40, which:40})
      orig_listener.apply(input, [simulated_downarrow]);
    }
  }
  orig_listener.apply(input, [event]);
};

JS-Fiddle: http://jsfiddle.net/QW59W/107/

2
Lars-Olof Kreim

Aucune de ces réponses ne semblait fonctionner pour moi. Ils obtiendraient l'emplacement général mais ne se déplaceraient pas réellement vers l'endroit réel que je cherchais. Dans le .pac-item, vous ne pouvez obtenir que l'adresse (nom du lieu exclu) en sélectionnant $ ('. Pac-item: first'). Children () [2] .textContent

Alors voici ma solution:

$("#search_field").on("keyup", function(e) {
    if(e.keyCode == 13) {
        searchPlaces();
    }
});

function searchPlaces() {
    var $firstResult = $('.pac-item:first').children();
    var placeName = $firstResult[1].textContent;
    var placeAddress = $firstResult[2].textContent;

    $("#search_field").val(placeName + ", " + placeAddress);

    var geocoder = new google.maps.Geocoder();
    geocoder.geocode({"address":placeAddress }, function(results, status) {
        if (status == google.maps.GeocoderStatus.OK) {
            var lat = results[0].geometry.location.lat(),
                lng = results[0].geometry.location.lng(),
                placeName = results[0].address_components[0].long_name,
                latlng = new google.maps.LatLng(lat, lng);

            map.panTo(latlng);
        }
    });
}

Je sais que cette question a déjà reçu une réponse, mais je me suis dit que je mettrais mes 2 centimes de dollars au cas où quelqu'un aurait le même problème que moi.

2
CodyEngel

@benregn @amirnissim Je pense que l'erreur de sélection provient de:

var suggestion_selected = $(".pac-item.pac-selected").length > 0;

La classe pac-selected doit être pac-item-selected, ce qui explique pourquoi !suggestion_selected est toujours considéré comme étant true, ce qui entraîne la sélection de l'emplacement incorrect lorsque vous appuyez sur la touche Entrée après avoir utilisé 'keyup' ou 'keydown' pour mettre en surbrillance l'emplacement souhaité.

1
Joe

J'ai travaillé un peu autour de cela et maintenant je peux forcer la sélection de la 1ère option de Google placces en utilisant js angulaire et module angulaire de saisie semi-automatique.
Merci à kuhnza
mon code

<form method="get" ng-app="StarterApp"  ng-controller="AppCtrl" action="searchresults.html" id="target" autocomplete="off">
   <br/>
    <div class="row">
    <div class="col-md-4"><input class="form-control" tabindex="1" autofocus g-places-autocomplete force-selection="true"  ng-model="user.fromPlace" placeholder="From Place" autocomplete="off"   required>
    </div>
        <div class="col-md-4"><input class="form-control" tabindex="2"  g-places-autocomplete force-selection="true"  placeholder="To Place" autocomplete="off" ng-model="user.toPlace" required>
    </div>
    <div class="col-md-4"> <input class="btn btn-primary"  type="submit" value="submit"></div></div><br /><br/>
    <input class="form-control"  style="width:40%" type="text" name="sourceAddressLat" placeholder="From Place Lat" id="fromLat">
    <input class="form-control"  style="width:40%"type="text" name="sourceAddressLang" placeholder="From Place Long" id="fromLong">
    <input class="form-control"  style="width:40%"type="text" name="sourceAddress" placeholder="From Place City" id="fromCity">
    <input class="form-control"  style="width:40%"type="text" name="destinationAddressLat" placeholder="To Place Lat" id="toLat">
    <input class="form-control"  style="width:40%"type="text" name="destinationAddressLang" placeholder="To Place Long"id="toLong">
    <input class="form-control"  style="width:40%"type="text" name="destinationAddress"placeholder="To Place City" id="toCity">
</form>

Voici un Plunker
Je vous remercie.

1
Murali

En ce qui concerne toutes vos réponses, j'ai créé une solution qui fonctionne parfaitement pour moi.

/**
 * Function that add the google places functionality to the search inputs
 * @private
 */
function _addGooglePlacesInputsAndListeners() {
    var self = this;
    var input = document.getElementById('searchBox');
    var options = {
        componentRestrictions: {country: "es"}
    };

    self.addInputEventListenersToAvoidAutocompleteProblem(input);
    var searchBox = new google.maps.places.Autocomplete(input, options);
    self.addPlacesChangedListener(searchBox, self.SimulatorMapStorage.map);
}

/**
 * A problem exists with google.maps.places.Autocomplete when the user write an address and doesn't selectany options that autocomplete gives him so we have to add some events to the two inputs that we have to simulate the behavior that it should have. First, we get the keydown 13 (Enter) and if it's not a suggested option, we simulate a keydown 40 (keydownArrow) to select the first option that Autocomplete gives. Then, we dispatch the event to complete the request.
 * @param input
 * @private
 */
function _addInputEventListenersToAvoidAutocompleteProblem(input) {
    input.addEventListener('keydown', function(event) {
        if (event.keyCode === 13 && event.which === 13) {
            var suggestion_selected = $(".pac-item-selected").length > 0;
            if (!suggestion_selected) {
                var keyDownArrowEvent = new Event('keydown');
                keyDownArrowEvent.keyCode = 40;
                keyDownArrowEvent.which = keyDownArrowEvent.keyCode;

                input.dispatchEvent(keyDownArrowEvent);
            }
        }
    });
}
<input id="searchBox" class="search-input initial-input" type="text" autofocus>

J'espère que cela peut aider quelqu'un. S'il vous plaît, n'hésitez pas à discuter de la meilleure façon de faire.

1
Diego Galocha

Sur la base de la réponse de amimissim , je présente une légère alternative, utilisant l’API de Google pour gérer les événements de manière multinavigation (la solution d’amimissim ne semble pas fonctionner dans IE8).

J'ai également dû changer pac-item.pac-selected en pac-item-refresh.pac-selected car il semble que la classe div des résultats ait changé. Cela rend pressant ENTER sur une suggestion (plutôt que de sélectionner la suivante).

var input = document.getElementById('MyFormField');
var autocomplete = new google.maps.places.Autocomplete(input);
google.maps.event.addListener(autocomplete, 'keydown', function(event) {
    var suggestion_selected = $(".pac-item-refesh.pac-selected").length > 0;
    if (event.which == 13 && !suggestion_selected) {
        var simulated_downarrow = $.Event("keydown", {
                    keyCode: 40,
                    which: 40
        });
        this.apply(autocomplete, [simulated_downarrow]);
    }
    this.apply(autocomplete, [event]);
});
0
alt

La solution de @ Alexander est celle que je cherchais. Mais cela causait une erreur - TypeError: a.stopPropagation is not a function.

J'ai donc créé l'événement avec KeyboardEvent. Voici le code de travail et la version Javascript est très pratique pour les projets React.js. Je l'ai également utilisé pour mon projet React.js.

(function selectFirst(input) {
  let _addEventListener = input.addEventListener
    ? input.addEventListener
    : input.attachEvent;

  function addEventListenerWrapper(type, listener) {
    if (type === 'keydown') {
      console.log('keydown');

      let orig_listener = listener;
      listener = event => {
        let suggestion_selected =
          document.getElementsByClassName('pac-item-selected').length > 0;

        if (event.keyCode === 13 && !suggestion_selected) {
          let simulated_downarrow = new KeyboardEvent('keydown', {
            bubbles: true,
            cancelable: true,
            keyCode: 40
          });

          orig_listener.apply(input, [simulated_downarrow]);
        }

        orig_listener.apply(input, [event]);
      };
    }

    _addEventListener.apply(input, [type, listener]);
  }

  if (input.addEventListener) input.addEventListener = addEventListenerWrapper;
  else if (input.attachEvent) input.attachEvent = addEventListenerWrapper;
})(addressInput);

this.autocomplete = new window.google.maps.places.Autocomplete(addressInput, options);

J'espère que cela peut aider quelqu'un :)

0
Roman M.

Juste une version javascript pure (sans jquery) de la solution géniale d’Amirnissim:

listener = function(event) {
      var suggestion_selected = document.getElementsByClassName('.pac-item-selected').length > 0;
      if (event.which === 13 && !suggestion_selected) {
        var e = JSON.parse(JSON.stringify(event));
        e.which = 40;
        e.keyCode = 40;
        orig_listener.apply(input, [e]);
      }
      orig_listener.apply(input, [event]);
    };
0

J'ai un peu enquêté là-dessus car j'ai le même problème. Ce que je n’aimais pas dans les solutions précédentes, c’est que le système de saisie semi-automatique a déjà lancé le service AutocompleteService pour afficher les prédictions. Par conséquent, les prévisions devraient être quelque part et ne devraient pas être chargées à nouveau.

J'ai découvert que les prédictions du lieu place_id est stocké dans

Autocomplete.gm_accessors_.place.Kc.l

et vous pourrez obtenir beaucoup de données à partir des enregistrements [0].data. À mon humble avis, il est plus rapide et préférable d’obtenir l’emplacement en utilisant place_id au lieu des données d’adresse. Cette sélection d'objet très étrange ne me semble pas très bonne, cependant.

Savez-vous s'il existe un meilleur moyen de récupérer la première prédiction de la saisie semi-automatique?

0
Tobias Hartmann

Solution de travail qui écoute si l'utilisateur a commencé à naviguer dans la liste avec le clavier plutôt que de déclencher la fausse navigation à chaque fois

https://codepen.io/callam/pen/RgzxZB

Voici les bits importants

// search input
const searchInput = document.getElementById('js-search-input');

// Google Maps autocomplete
const autocomplete = new google.maps.places.Autocomplete(searchInput);

// Has user pressed the down key to navigate autocomplete options?
let hasDownBeenPressed = false;

// Listener outside to stop nested loop returning odd results
searchInput.addEventListener('keydown', (e) => {
    if (e.keyCode === 40) {
        hasDownBeenPressed = true;
    }
});

// GoogleMaps API custom eventlistener method
google.maps.event.addDomListener(searchInput, 'keydown', (e) => {

    // Maps API e.stopPropagation();
    e.cancelBubble = true;

    // If enter key, or tab key
    if (e.keyCode === 13 || e.keyCode === 9) {
        // If user isn't navigating using arrows and this hasn't ran yet
        if (!hasDownBeenPressed && !e.hasRanOnce) {
            google.maps.event.trigger(e.target, 'keydown', {
                keyCode: 40,
                hasRanOnce: true,
            });
        }
    }
});

 // Clear the input on focus, reset hasDownBeenPressed
searchInput.addEventListener('focus', () => {
    hasDownBeenPressed = false;
    searchInput.value = '';
});

// place_changed GoogleMaps listener when we do submit
google.maps.event.addListener(autocomplete, 'place_changed', function() {

    // Get the place info from the autocomplete Api
    const place = autocomplete.getPlace();

    //If we can find the place lets go to it
    if (typeof place.address_components !== 'undefined') {          
        // reset hasDownBeenPressed in case they don't unfocus
        hasDownBeenPressed = false;
    }

});
0
Callam