谷歌地图API V3自动完成-回车时选择第一个选项

75

不错的问题。我今天也在思考这个问题。我的唯一担忧是强制用户做他们可能不想做的事情。有些大型网站如果有多个选项可见,它们不会强制选择第一个选项,只有在只有一个选项显示时才会强制,这似乎更好。 - luke_mclachlan
19个回答

176

这里有一个解决方案,它不会进行可能返回不正确结果的地理编码请求:http://jsfiddle.net/amirnissim/2D6HW/

每当用户在自动完成字段中按下return键时,它模拟了一个down-arrow按键。触发事件先于return事件,因此它模拟了用户使用键盘选择第一个建议项。

这是代码(已在Chrome和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>

47

最近我在一个网站上实现自动完成时遇到了同样的问题。以下是我想出的解决方案:

$("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/


16
这种方法存在的问题是它不能返回与第一个自动完成结果相同的地址。它对第一个自动完成结果的显示地址进行地理编码,这可能是完全不同的地址。例如,输入“jfk”时,第一个自动完成结果是正确的地址(JFK Access Road,New York,NY,United States),但“JFK Airport,New York,NY”的地理编码结果(它是第一个自动完成的显示地址)将产生一个看起来像是酒店的地址(144-02 135th Ave,Queens,NY 11436,USA)。 - Reed G. Law
1
在我的情况下,我执行了以下操作: var firstResult = $(".pac-container .pac-item:first").text();var stringMatched = $(".pac-container .pac-item:first").find(".pac-item-query").text(); firstResult = firstResult.replace(stringMatched, stringMatched + " "); 这解决了问题。 - Christian Martinez
你不能只是在第一个项目上执行 .click() 来触发与你想要的相同处理吗? - Antony D'Andrea
请问您如何在“.pac-item-query”后添加空格?因为现在当我按ENTER键时,空格会被删除。 - Chetan
谢谢!谢谢!!谢谢!!!我不知道你可以使用Geocoder.geocode()来获取地址组件。你为我节省了数小时的工作时间!再次感谢你! - Sam

41

2020年可行的解决方案。

我将此页面上最佳答案结合起来,用简单的ES6语法编写。无需jQuery、第二个API请求或IIFE。

基本上,每当用户在自动完成字段内按回车键时,我们模拟一个↓(下箭头)按键。

首先,假设您的HTML中有类似<input id="address-field">的内容,请按以下方法进行地址字段的标识设置:

const field = document.getElementById('address-field') 

const autoComplete = new google.maps.places.Autocomplete(field)

autoComplete.setTypes(['address'])

然后在下一行添加:

enableEnterKey(field)

然后在你的脚本中的其他位置,如果你想要将这个功能与其它代码分开,可以添加以下函数:

  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 suggestionSelected = document.getElementsByClassName('pac-item-selected').length
          if (event.key === 'Enter' && !suggestionSelected) {
            const e = new KeyboardEvent('keydown', { 
              key: 'ArrowDown', 
              code: 'ArrowDown', 
              keyCode: 40, 
            })
            _listener.apply(input, [e])
          }
          _listener.apply(input, [event])
        }
      }
      _addEventListener.apply(input, [type, listener])
    }

    input.addEventListener = addEventListenerWrapper
  }

你应该准备就绪了。实质上,该函数捕获字段中的每个按键,并且如果是键,则模拟按下一个键。它还存储并重新绑定监听器和事件,以维护Google Maps Autocomplete()的所有功能。

特别感谢amirnissim和Alexander Schwarzman提供的早期答案。


1
你忘记将enableEnterKey声明为函数了。 function enableEnterKey(input) {} 除此之外,它完美无缺,谢谢! - richardj
1
修改后还可以处理 Tab 键,方法是添加一个范围判断,即: if (event.which >= 9 && event.which <= 13 && !suggestion_selected) { - Isabelle
1
好观点@Shadrix,我已经移除了仅适用于IE8的“attachEvent”。 - Tony Brasunas
2
@Shadrix 现在已经更新到适用于现代浏览器的键盘标识符了。 - Tony Brasunas
5
起初这对我没有用。我改变了模拟键按下事件的方式,解决方案非常成功:const e = new KeyboardEvent("keydown", { key: "ArrowDown", code: "ArrowDown", keyCode: 40 }); - sebbab
显示剩余8条评论

22
这是一个真实的解决方案示例,与浏览器黑客等无关,只使用了Google提供的公共API中记录在此处的方法:Google Maps API
唯一的缺点是,如果用户没有从列表中选择项目,则需要向Google发出额外的请求。优点是,结果始终是正确的,因为查询的执行方式与AutoComplete内部的查询完全相同。第二个好处是,仅使用公共API方法,而不依赖于AutoComplete小部件的内部HTML结构,我们可以确保我们的产品不会因为Google进行更改而破坏。
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);
    }
});

2
您的解决方案不正确,因为当place_changed事件未触发时它会出现问题。也就是说,当用户选择了“无”选项,或者按下回车键、Tab键,甚至在选项之外点击时,都会出现这种情况。 - sabotero
有人发现了更新自动完成对象的“place”的方法吗?目前,如果使用此方法,autocomplete.getPlace()将会返回undefined - Ulad Kasach
为了回答Ulad的问题,只需将AutocompleteService代码移出“place_changed”事件处理程序,并设置状态,如果用户选择一个值,则将其设置为true。如果在预期时状态没有改变(我正在使用输入字段上的on bur),则使用输入作为搜索关键字触发AutocompleteService。然而,我发现使用该服务的主要问题是返回的结果不能保证与完成API相同或按相同顺序排列,因此您必须使用服务结果实现自己的预测UI。 - verboze

10

看起来有一个更好、更清晰的解决方案: 使用 google.maps.places.SearchBox 替代 google.maps.places.Autocomplete

代码几乎相同,只需获取多个地点中的第一个。按下 Enter 键后会返回正确的列表 - 所以它可以直接使用,没有必要进行任何 hack。

请参阅示例 HTML 页面:

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

相关的代码片段如下:

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);
  }
});

这个示例的完整源代码可以在以下链接中找到:https://gist.github.com/klokan/8408394


6
它的工作方式与按下回车键时的"自动完成"完全相同,所以...它不起作用。 - klimat
7
需要注意的是,您无法为SearchBox设置componentRestrictions选项。因此,如果您需要将建议限制在特定国家,则没有这个选择。 - Felix Wienberg

9
对于Google Places Autocomplete V3,最好的解决方案是使用两个API请求。以下是示例代码。其他答案不够好的原因是它们要么使用jQuery来模拟事件(hacky),要么使用Geocoder或Google Places Search box,这些并不总是匹配自动完成结果。相反,我们将使用Google的Autocomplete Service,如此处所述,只使用JavaScript(没有jQuery)。

以下是详细说明,使用本机Google API生成自动完成框,然后重新运行查询以选择第一个选项,这是最兼容跨浏览器的解决方案。

<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>

唯一的抱怨是原生实现返回了不同的数据结构,尽管信息是相同的。请相应地进行调整。

使用 AutocompleteService API 是个不错的选择。我通常会和 blur 事件一起使用,以便考虑到移动用户的操作。 - Josh
1
这是关于这个问题最干净、最可靠的答案。如果您需要有关地址的更多详细信息,例如 GPS 坐标(并获得与自动完成相同的对象),则可以在 Google.maps.places.PlacesService 上进行另一个 getDetails() 调用。 - Tdy
只是提醒一下:getDetails 很耗费资源。其额外的经济成本是 Autocomplete 本身的 6 倍。 - bart

3

关于你们所有的答案,我已经创建了一个完美适合我的解决方案。

/**
 * 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>

希望这能对某人有所帮助。请随意讨论最佳实现方式。


2

这些答案似乎都对我无效。它们只能得到大致位置,但不能实际移动到我搜索的确切位置。在.pac-item中,您可以通过选择$('.pac-item:first').children()[2].textContent来获取地址(不包括地点名称)。

因此,这是我的解决方案:

$("#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);
        }
    });
}

我知道这个问题已经有答案了,但是为了防止其他人像我一样遇到同样的问题,我也想发表一下我的看法。


$('.pac-item:first') - 不幸的是,用户选择的不一定是列表中的第一项。 - Malcolm Swaine

2
这个怎么样?
$("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

我只是想为amirnissim的答案写一个小增强。发布的脚本不支持IE8,因为在IE8中“event.which”似乎总是为空。
要解决这个问题,您只需要另外检查“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/


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接