diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..9659ecb --- /dev/null +++ b/.editorconfig @@ -0,0 +1,19 @@ +# EditorConfig is awesome: http://EditorConfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +charset = utf-8 + +# 2 space indentation +indent_style = space +indent_size = 2 + +[**.md] +# trailing spaces have a meaning in markdown +trim_trailing_whitespace = false diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..5960216 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,2 @@ +Please note: This project is not maintained anymore! +Feel free to discuss issues here but please do not expect new features or changes in the default behavior. diff --git a/README.md b/README.md index e10d5ac..627413e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,18 @@ -# $.geocomplete() - Version 1.5 +# $.geocomplete() - Version 1.7.0 ## jQuery Geocoding and Places Autocomplete Plugin An advanced jQuery plugin that wraps the Google Maps API's [Geocoding](https://code.google.com/apis/maps/documentation/javascript/geocoding.html) and [Places Autocomplete](https://code.google.com/apis/maps/documentation/javascript/places.html#places_autocomplete) services. You simply provide an input that lets you search for locations with a nice autocomplete dropdown. Optionally add a container to show an interactive map and a form that will be populated with the address details. View the [annotated source](http://ubilabs.github.com/geocomplete/docs/). +## ⚠️ NOT MAINTAINED + +_Please note:_ **This project is not maintained anymore. We do not use jQuery in production and switched to React about two years ago. Feel free to discuss issues here but please do not expect new features or changes in the default behavior.** + +For a new and way more flexible solution, see our [React Geosuggest](http://ubilabs.github.io/react-geosuggest/) plugin. + +If you would like to take over the project, please let us know! There are many people out there who would be happy to have new supporters. + ## Basic Usage To convert an input into an autocomplete field, simply call the Geocomplete plugin: @@ -25,13 +33,14 @@ Here is a list of basic uses: * [Attribute](http://ubilabs.github.com/geocomplete/examples/custom_attribute.html) - Using custom attributes to populate data. * [Multiple results](http://ubilabs.github.com/geocomplete/examples/multiple_results.html) - Handling multiple results from the geocoder. * [Draggable](http://ubilabs.github.com/geocomplete/examples/draggable.html) - A draggable marker to redefine the position. +* [Styled](http://ubilabs.github.com/geocomplete/examples/styled.html) - A styled map example. ## Requirements Make sure you include the Google Maps API with the Places Library before loading this plugin as described [here](https://developers.google.com/maps/documentation/javascript/places#loading_the_library). -````html - +```html + ``` @@ -41,7 +50,7 @@ If you use the plugin without showing a map you must display the "[powered by Go To trigger a geocoding request from outside (eg. when hitting the "find" button), simply trigger the "geocode" event on the element. -````javascript +```javascript $("input").geocomplete(); // Trigger geocoding request. @@ -70,6 +79,8 @@ By default the plugin analyses the `name` attribute of the containers child node If the element is an input, the value will be replaced otherwise the plugin overrides the current text. +If you have multiple geocomplete fields on a page, use `detailsScope` option scope your 'details' container. + **Note**: Some address components such as "country" return an additional `short_name`. You can access them by simply adding `_short` at the end of the type. Simple Example: @@ -125,6 +136,7 @@ $("#my_input").geocomplete({ * `map` - Might be a selector, a jQuery object or a DOM element. Default is `false` which shows no map. * `details` - The container that should be populated with data. Defaults to `false` which ignores the setting. +* 'detailsScope' - Allows you to scope the 'details' container and have multiple geocomplete fields on one page. Must be a parent of the input. Default is 'null' * `location` - Location to initialize the map on. Might be an address `string` or an `array` with [latitude, longitude] or a `google.maps.LatLng`object. Default is `false` which shows a blank map. * `bounds` - Whether to snap geocode search to map bounds. Default: `true` if false search globally. Alternatively pass a custom LatLngBounds object * `detailsAttribute` - The attribute's name to use as an indicator. Default: `"name"` @@ -144,7 +156,7 @@ $("#my_input").geocomplete({ You can subscribe to events of the geocode plugin by using the default jQuery syntax: -````javascript +```javascript $("input") .geocomplete() .bind("geocode:result", function(event, result){ @@ -159,6 +171,8 @@ The following events are supported: * `"geocode:multiple"` - Fired immediately after the "result" event if multiple results were found. Passes an array of all results. * `"geocode:dragged"` - Fired when the marker's position was modified manually. Passes the updated location. * `"geocode:click"` - Fired when 'click' event occurs on the map. Passes the location where the click had place. +* `"geocode:mapdragged"` - Fired when the map bounds are modified by dragging manually. Passes the location of the current map center. +* `"geocode:idle"` - Fired when the map becomes idle after panning or zooming. Passes the location of the current map center. ## Methods and Properties @@ -166,7 +180,7 @@ You can access all properties and methods of the plugin from outside. Simply add Example: -````javascript +```javascript // Initialize the plugin. $("input").geocomplete({ map: ".map_canvas" }); @@ -197,4 +211,3 @@ More information can be found here: https://developers.google.com/maps/documenta ## About Developed by [Martin Kleppe](http://twitter.com/aemkei) at [Ubilabs](http://ubilabs.net). - diff --git a/bower.json b/bower.json new file mode 100644 index 0000000..e14a30e --- /dev/null +++ b/bower.json @@ -0,0 +1,29 @@ +{ + "name": "geocomplete", + "version": "1.7.0", + "homepage": "http://ubilabs.github.com/geocomplete/", + "authors": [ + "Martin Kleppe " + ], + "description": "jQuery Geocoding and Places Autocomplete Plugin", + "main": "jquery.geocomplete.js", + "keywords": [ + "geocoding", + "map", + "places", + "api", + "search", + "google" + ], + "license": "MIT", + "dependencies": { + "jquery": ">= 1.9.0" + }, + "ignore": [ + "**/.*", + "node_modules", + "bower_components", + "test", + "tests" + ] +} diff --git a/changelog.txt b/changelog.txt index 8e8499d..60b57bc 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,4 +1,34 @@ -# Changelog - $.geocomplete() +Version 1.7 + +* Merged pull requests + +Version 1.6 + +* Cycle through subtypes in address components. +* Add "place_id" to places details. +* Avoid exception if getPlace() returns undefined + +Version 1.5 + +* Added autoselect option. +* Add zoom_changed event support. +* Fixed blur autocomplete. +* Keep result at text input after focusout. +* Add blur boolean option. +* Add marker to map on initialize. +* Add geocode:click event on the map. + +Version 1.4 + +* Use lat/lng location option to set initial map +* Return element after calling methods via API. +* Add option markerOptions.disabled. +* Add support for Places API componentRestrictions option + +Version 1.3 + +* Add "name" to places details. +* Add reference field from Places Details call. Version 1.2 @@ -7,7 +37,7 @@ Version 1.2 Version 1.1 -* Add API to access methods and properties from outside +* Add API to access methods and properties from outside * Add events for draggable marker Version 1.0 diff --git a/examples/form.html b/examples/form.html index 4abe92d..02be33e 100644 --- a/examples/form.html +++ b/examples/form.html @@ -74,6 +74,9 @@

Address-Details

+ + + diff --git a/examples/multiple_fields.html b/examples/multiple_fields.html new file mode 100644 index 0000000..e00263d --- /dev/null +++ b/examples/multiple_fields.html @@ -0,0 +1,77 @@ + + + + $.geocomplete() + + + + + + +
+ +
+ + + +
+

Address-Details

+ + + + + + + + + +
+
+ +
+ + + +
+

Address-Details

+ + + + + + + + +
+
+ +
+ + + + + + + + + + + diff --git a/examples/styled.html b/examples/styled.html new file mode 100644 index 0000000..da1aea5 --- /dev/null +++ b/examples/styled.html @@ -0,0 +1,42 @@ + + + + GeoComplete + + + + + +
+ +
+ +
+ + + + + + + + + + + diff --git a/jquery.geocomplete.js b/jquery.geocomplete.js index 4984c6a..e130d4f 100644 --- a/jquery.geocomplete.js +++ b/jquery.geocomplete.js @@ -1,5 +1,6 @@ /** - * jQuery Geocoding and Places Autocomplete Plugin - V 1.5.0 + * jQuery Geocoding and Places Autocomplete Plugin - V 1.6.0-tableau + * (Tableau fork: https://github.com/tableau-mkt/geocomplete) * * @author Martin Kleppe , 2012 * @author Ubilabs http://ubilabs.net, 2012 @@ -7,12 +8,15 @@ */ // # $.geocomplete() -// ## jQuery Geocoding and Places Autocomplete Plugin - V 1.5.0 +// ## jQuery Geocoding and Places Autocomplete Plugin - V 1.6.0-tableau // // * https://github.com/ubilabs/geocomplete/ // * by Martin Kleppe -(function($, window, document, undefined){ +/* jshint camelcase:false */ +/* global google:false */ + +(function($, window, document, undefined) { // ## Options // The default options for this plugin. @@ -22,32 +26,32 @@ // * `location` - Location to initialize the map on. Might be an address `string` or an `array` with [latitude, longitude] or a `google.maps.LatLng`object. Default is `false` which shows a blank map. // * `bounds` - Whether to snap geocode search to map bounds. Default: `true` if false search globally. Alternatively pass a custom `LatLngBounds object. // * `autoselect` - Automatically selects the highlighted item or the first item from the suggestions list on Enter. - // * `detailsAttribute` - The attribute's name to use as an indicator. Default: `"name"` + // * `detailsAttribute` - The attribute's name to use as an indicator. Default: `'name'` // * `mapOptions` - Options to pass to the `google.maps.Map` constructor. See the full list [here](http://code.google.com/apis/maps/documentation/javascript/reference.html#MapOptions). // * `mapOptions.zoom` - The inital zoom level. Default: `14` // * `mapOptions.scrollwheel` - Whether to enable the scrollwheel to zoom the map. Default: `false` - // * `mapOptions.mapTypeId` - The map type. Default: `"roadmap"` + // * `mapOptions.mapTypeId` - The map type. Default: `'roadmap'` // * `markerOptions` - The options to pass to the `google.maps.Marker` constructor. See the full list [here](http://code.google.com/apis/maps/documentation/javascript/reference.html#MarkerOptions). // * `markerOptions.draggable` - If the marker is draggable. Default: `false`. Set to true to enable dragging. // * `markerOptions.disabled` - Do not show marker. Default: `false`. Set to true to disable marker. // * `maxZoom` - The maximum zoom level too zoom in after a geocoding response. Default: `16` // * `types` - An array containing one or more of the supported types for the places request. Default: `['geocode']` See the full list [here](http://code.google.com/apis/maps/documentation/javascript/places.html#place_search_requests). // * `blur` - Trigger geocode when input loses focus. - // * `geocodeAfterResult` - If blur is set to true, choose whether to geocode if user has explicitly selected a result before blur. var defaults = { bounds: true, country: null, map: false, details: false, - detailsAttribute: "name", + detailsAttribute: 'name', autoselect: true, location: false, + debug: false, mapOptions: { zoom: 14, scrollwheel: false, - mapTypeId: "roadmap" + mapTypeId: 'roadmap' }, markerOptions: { @@ -56,24 +60,23 @@ maxZoom: 16, types: ['geocode'], - blur: false, - geocodeAfterResult: false + blur: false }; // See: [Geocoding Types](https://developers.google.com/maps/documentation/geocoding/#Types) // on Google Developers. - var componentTypes = ("street_address route intersection political " + - "country administrative_area_level_1 administrative_area_level_2 " + - "administrative_area_level_3 colloquial_area locality sublocality " + - "neighborhood premise subpremise postal_code natural_feature airport " + - "park point_of_interest post_box street_number floor room " + - "lat lng viewport location " + - "formatted_address location_type bounds").split(" "); + var componentTypes = ('street_address route intersection political ' + + 'country administrative_area_level_1 administrative_area_level_2 ' + + 'administrative_area_level_3 colloquial_area locality sublocality ' + + 'neighborhood premise subpremise postal_code natural_feature airport ' + + 'park point_of_interest post_box street_number floor room ' + + 'lat lng viewport location ' + + 'formatted_address location_type bounds').split(' '); // See: [Places Details Responses](https://developers.google.com/maps/documentation/javascript/places#place_details_responses) // on Google Developers. - var placesDetails = ("id url website vicinity reference name rating " + - "international_phone_number icon formatted_phone_number").split(" "); + var placesDetails = ('id url website vicinity reference name rating ' + + 'international_phone_number icon formatted_phone_number').split(' '); // The actual plugin constructor. function GeoComplete(input, options) { @@ -91,7 +94,7 @@ // Initialize all parts of the plugin. $.extend(GeoComplete.prototype, { - init: function(){ + init: function() { this.initMap(); this.initMarker(); this.initGeocoder(); @@ -102,10 +105,10 @@ // Initialize the map but only if the option `map` was set. // This will create a `map` within the given container // using the provided `mapOptions` or link to the existing map instance. - initMap: function(){ - if (!this.options.map){ return; } + initMap: function() { + if (!this.options.map) { return; } - if (typeof this.options.map.setCenter == "function"){ + if (typeof this.options.map.setCenter === 'function') { this.map = this.options.map; return; } @@ -132,11 +135,11 @@ // Add a marker with the provided `markerOptions` but only // if the option was set. Additionally it listens for the `dragend` event // to notify the plugin about changes. - initMarker: function(){ - if (!this.map){ return; } + initMarker: function() { + if (!this.map) { return; } var options = $.extend(this.options.markerOptions, { map: this.map }); - if (options.disabled){ return; } + if (options.disabled) { return; } this.marker = new google.maps.Marker(options); @@ -149,18 +152,17 @@ // Associate the input with the autocompleter and create a geocoder // to fall back when the autocompleter does not return a value. - initGeocoder: function(){ - - // Indicates is user did select a result from the dropdown. - var selected = false; + initGeocoder: function() { var options = { - types: this.options.types, - bounds: this.options.bounds === true ? null : this.options.bounds, - componentRestrictions: this.options.componentRestrictions - }; - - if (this.options.country){ + types: this.options.types, + bounds: this.options.bounds === true ? null : this.options.bounds, + componentRestrictions: this.options.componentRestrictions, + // Whitelist the necessary fields to reduce API costs. + fields: ['geometry', 'formatted_address', 'address_components'] + }; + + if (this.options.country) { options.componentRestrictions = {country: this.options.country}; } @@ -172,7 +174,7 @@ // Bind autocomplete to map bounds but only if there is a map // and `options.bindToMap` is set to true. - if (this.map && this.options.bounds === true){ + if (this.map && this.options.bounds === true) { this.autocomplete.bindTo('bounds', this.map); } @@ -184,22 +186,12 @@ ); // Prevent parent form from being submitted if user hit enter. - this.$input.keypress(function(event){ - if (event.keyCode === 13){ return false; } + this.$input.keypress(function(event) { + if (event.keyCode === 13) { return false; } }); - // Assume that if user types anything after having selected a result, - // the selected location is not valid any more. - if (this.options.geocodeAfterResult === true){ - this.$input.bind('keypress', $.proxy(function(){ - if (event.keyCode != 9 && this.selected === true){ - this.selected = false; - } - }, this)); - } - - // Listen for "geocode" events and trigger find action. - this.$input.bind("geocode", $.proxy(function(){ + // Listen for 'geocode' events and trigger find action. + this.$input.bind('geocode', $.proxy(function() { this.find(); }, this)); @@ -207,34 +199,66 @@ // not explicitly selected a result. // (Useful for typing partial location and tabbing to the next field // or clicking somewhere else.) - if (this.options.blur === true){ - this.$input.blur($.proxy(function(){ - if (this.options.geocodeAfterResult === true && this.selected === true){ return; } - this.find(); - }, this)); + if (this.options.blur === true) { + this.$input.blur($.proxy(this.blurHandler, this)); + } + + // Bind keydown handler on input so we can flag up/down arrow key selection. + this.$input.keydown($.proxy(this.keydownHandler, this)); + + // Bind mousedown handler on Places Autocomplete container so we can flag a mouse selection. + $(document).on('mousedown.geocomplete', '.pac-container', $.proxy(this.pacMousedownHandler, this)); + }, + + pacMousedownHandler: function pacMousedownHandler() { + this.pacSelected = true; + }, + + keydownHandler: function keydownHandler(event) { + if (event.which === 38 || event.which === 40) { + this.pacSelected = true; + } + }, + + blurHandler: function blurHandler(event) { + this.log('blurHandler', event); + + // Ignore if previously selected place matches the input value. + if (this.selectedPlace && this.selectedPlace.formatted_address === this.$input.val()) { + return; + } + + // Ignore empty input. + if (this.$input.val() === '') { + return; + } + + // Geocode the first autocomplete result if no selection was made. + if (!this.pacSelected) { + this.placeChanged({name: this.$input.val()}); } }, // Prepare a given DOM structure to be populated when we got some data. // This will cycle through the list of component types and map the // corresponding elements. - initDetails: function(){ - if (!this.options.details){ return; } + initDetails: function() { + if (!this.options.details) { return; } var $details = $(this.options.details), attribute = this.options.detailsAttribute, details = {}; - function setDetail(value){ - details[value] = $details.find("[" + attribute + "=" + value + "]"); + function setDetail(value) { + details[value] = $details.find('[' + attribute + '=' + value + ']'); } - $.each(componentTypes, function(index, key){ + $.each(componentTypes, function(index, key) { setDetail(key); - setDetail(key + "_short"); + setDetail(key + '_short'); }); - $.each(placesDetails, function(index, key){ + $.each(placesDetails, function(index, key) { setDetail(key); }); @@ -250,7 +274,7 @@ if (!location) { return; } - if (typeof location == 'string') { + if (typeof location === 'string') { this.find(location); return; } @@ -259,19 +283,19 @@ latLng = new google.maps.LatLng(location[0], location[1]); } - if (location instanceof google.maps.LatLng){ + if (location instanceof google.maps.LatLng) { latLng = location; } - if (latLng){ - if (this.map){ this.map.setCenter(latLng); } - if (this.marker){ this.marker.setPosition(latLng); } + if (latLng) { + if (this.map) { this.map.setCenter(latLng); } + if (this.marker) { this.marker.setPosition(latLng); } } }, // Look up a given address. If no `address` was specified it uses // the current value of the input. - find: function(address){ + find: function(address) { this.geocode({ address: address || this.$input.val() }); @@ -279,78 +303,52 @@ // Requests details about a given location. // Additionally it will bias the requests to the provided bounds. - geocode: function(request){ - if (this.options.bounds && !request.bounds){ - if (this.options.bounds === true){ + geocode: function(request) { + if (this.options.bounds && !request.bounds) { + if (this.options.bounds === true) { request.bounds = this.map && this.map.getBounds(); } else { request.bounds = this.options.bounds; } } - if (this.options.country){ + if (this.options.country) { request.region = this.options.country; } this.geocoder.geocode(request, $.proxy(this.handleGeocode, this)); }, - // Get the selected result. If no result is selected on the list, then get - // the first result from the list. - selectFirstResult: function() { - //$(".pac-container").hide(); - - var selected = ''; - // Check if any result is selected. - if ($(".pac-item-selected")['0']) { - selected = '-selected'; - } - - // Get the first suggestion's text. - var $span1 = $(".pac-container .pac-item" + selected + ":first span:nth-child(2)").text(); - var $span2 = $(".pac-container .pac-item" + selected + ":first span:nth-child(3)").text(); - - // Adds the additional information, if available. - var firstResult = $span1; - if ($span2) { - firstResult += " - " + $span2; - } - - this.$input.val(firstResult); - - return firstResult; - }, - // Handles the geocode response. If more than one results was found - // it triggers the "geocode:multiple" events. If there was an error - // the "geocode:error" event is fired. - handleGeocode: function(results, status){ + // it triggers the 'geocode:multiple' events. If there was an error + // the 'geocode:error' event is fired. + handleGeocode: function(results, status) { if (status === google.maps.GeocoderStatus.OK) { var result = results[0]; this.$input.val(result.formatted_address); this.update(result); - if (results.length > 1){ - this.trigger("geocode:multiple", results); + if (results.length > 1) { + this.trigger('geocode:multiple', results); } } else { - this.trigger("geocode:error", status); + this.trigger('geocode:error', status); } }, // Triggers a given `event` with optional `arguments` on the input. - trigger: function(event, argument){ + trigger: function(event, argument) { this.$input.trigger(event, [argument]); }, // Set the map to a new center by passing a `geometry`. // If the geometry has a viewport, the map zooms out to fit the bounds. // Additionally it updates the marker position. - center: function(geometry){ - if (geometry.viewport){ + center: function(geometry) { + if (geometry.viewport) { this.map.fitBounds(geometry.viewport); - if (this.map.getZoom() > this.options.maxZoom){ + if (this.map.getZoom() > this.options.maxZoom) { this.map.setZoom(this.options.maxZoom); } } else { @@ -358,31 +356,33 @@ this.map.setCenter(geometry.location); } - if (this.marker){ + if (this.marker) { this.marker.setPosition(geometry.location); this.marker.setAnimation(this.options.markerOptions.animation); } }, // Update the elements based on a single places or geocoding response - // and trigger the "geocode:result" event on the input. - update: function(result){ + // and trigger the 'geocode:result' event on the input. + update: function(result) { + + this.$input.val(result.formatted_address); - if (this.map){ + if (this.map) { this.center(result.geometry); } - if (this.$details){ + if (this.$details) { this.fillDetails(result); } - this.trigger("geocode:result", result); + this.trigger('geocode:result', result); }, // Populate the provided elements with new `result` data. // This will lookup all elements that has an attribute with the given // component type. - fillDetails: function(result){ + fillDetails: function(result) { var data = {}, geometry = result.geometry, @@ -390,21 +390,21 @@ bounds = geometry.bounds; // Create a simplified version of the address components. - $.each(result.address_components, function(index, object){ + $.each(result.address_components, function(index, object) { var name = object.types[0]; data[name] = object.long_name; - data[name + "_short"] = object.short_name; + data[name + '_short'] = object.short_name; }); // Add properties of the places details. - $.each(placesDetails, function(index, key){ + $.each(placesDetails, function(index, key) { data[key] = result[key]; }); // Add infos about the address and geometry. $.extend(data, { formatted_address: result.formatted_address, - location_type: geometry.location_type || "PLACES", + location_type: geometry.location_type || 'PLACES', viewport: viewport, bounds: bounds, location: geometry.location, @@ -413,7 +413,7 @@ }); // Set the values for all details. - $.each(this.details, $.proxy(function(key, $detail){ + $.each(this.details, $.proxy(function(key, $detail) { var value = data[key]; this.setDetail($detail, value); }, this)); @@ -424,58 +424,100 @@ // Assign a given `value` to a single `$element`. // If the element is an input, the value is set, otherwise it updates // the text content. - setDetail: function($element, value){ + setDetail: function($element, value) { - if (value === undefined){ - value = ""; - } else if (typeof value.toUrlValue == "function"){ + if (value === undefined) { + value = ''; + } else if (typeof value.toUrlValue === 'function') { value = value.toUrlValue(); } - if ($element.is(":input")){ + if ($element.is(':input')) { $element.val(value); } else { $element.text(value); } }, - // Fire the "geocode:dragged" event and pass the new position. - markerDragged: function(event){ - this.trigger("geocode:dragged", event.latLng); + // Fire the 'geocode:dragged' event and pass the new position. + markerDragged: function(event) { + this.trigger('geocode:dragged', event.latLng); }, mapClicked: function(event) { - this.trigger("geocode:click", event.latLng); + this.trigger('geocode:click', event.latLng); }, - mapZoomed: function(event) { - this.trigger("geocode:zoom", this.map.getZoom()); + mapZoomed: function() { + this.trigger('geocode:zoom', this.map.getZoom()); }, // Restore the old position of the marker to the last now location. - resetMarker: function(){ + resetMarker: function() { this.marker.setPosition(this.data.location); this.setDetail(this.details.lat, this.data.location.lat()); this.setDetail(this.details.lng, this.data.location.lng()); }, + // Log something to the console with debug option. + log: function log() { + if (this.options.debug) { + if (typeof console.log === 'function') { + console.log(Array.prototype.slice.call(arguments)); + } + } + }, + // Update the plugin after the user has selected an autocomplete entry. - // If the place has no geometry it passes it to the geocoder. - placeChanged: function(){ - var place = this.autocomplete.getPlace(); - this.selected = true; + placeChanged: function placeChanged(place) { + var _this = this; + + this.selectedPlace = place || this.autocomplete.getPlace(); - if (!place.geometry){ + _this.$input.trigger('geocomplete:update', {place: this.selectedPlace}); + + if (typeof this.selectedPlace !== 'object') { + return; + } + + this.log('placeChanged', this.selectedPlace); + + if (this.selectedPlace.formatted_address) { + this.update(this.selectedPlace); + } + else if (this.selectedPlace.name) { if (this.options.autoselect) { - // Automatically selects the highlighted item or the first item from the - // suggestions list. - var autoSelection = this.selectFirstResult(); - this.find(autoSelection); + // If we don't have a fully populate place, we need to fetch one based + // on the value of the input. See http://stackoverflow.com/a/17505006 + this.autocompleteService = new google.maps.places.AutocompleteService(); + this.autocompleteService.getPlacePredictions({ + input: this.selectedPlace.name, + offset: this.selectedPlace.name.length, + componentRestrictions: this.options.componentRestrictions, + types: this.options.types + }, + function predictionCallback(list, status) { + if (list && status === google.maps.GeocoderStatus.OK) { + _this.placesService = new google.maps.places.PlacesService($('
')[0]); + _this.placesService.getDetails({ + 'reference': list[0].reference, + fields: ['geometry', 'formatted_address', 'address_components'] + }, + function detailsResult(result, status) { + _this.update(result); + } + ); + } + else { + _this.$input.trigger('geocode:error', status); + } + } + ); } - } else { - // Use the input text if it already gives geometry. - this.update(place); } + + // Reset selection flag. + this.pacSelected = false; } }); @@ -489,16 +531,16 @@ // If you call `.geocomplete()` with a string as the first parameter // it returns the corresponding property or calls the method with the // following arguments. - if (typeof options == "string"){ + if (typeof options === 'string') { var instance = $(this).data(attribute) || $(this).geocomplete().data(attribute), prop = instance[options]; - if (typeof prop == "function"){ + if (typeof prop === 'function') { prop.apply(instance, Array.prototype.slice.call(arguments, 1)); return $(this); } else { - if (arguments.length == 2){ + if (arguments.length === 2) { prop = arguments[1]; } return prop; @@ -515,4 +557,4 @@ } }; -})( jQuery, window, document ); +})(jQuery, window, document); diff --git a/jquery.geocomplete.min.js b/jquery.geocomplete.min.js index fd218ad..15a6304 100644 --- a/jquery.geocomplete.min.js +++ b/jquery.geocomplete.min.js @@ -1,8 +1 @@ -/** - * jQuery Geocoding and Places Autocomplete Plugin - V 1.5.0 - * - * @author Martin Kleppe , 2012 - * @author Ubilabs http://ubilabs.net, 2012 - * @license MIT License - */ -(function($,window,document,undefined){var defaults={bounds:true,country:null,map:false,details:false,detailsAttribute:"name",autoselect:true,location:false,mapOptions:{zoom:14,scrollwheel:false,mapTypeId:"roadmap"},markerOptions:{draggable:false},maxZoom:16,types:["geocode"],blur:false};var componentTypes=("street_address route intersection political "+"country administrative_area_level_1 administrative_area_level_2 "+"administrative_area_level_3 colloquial_area locality sublocality "+"neighborhood premise subpremise postal_code natural_feature airport "+"park point_of_interest post_box street_number floor room "+"lat lng viewport location "+"formatted_address location_type bounds").split(" ");var placesDetails=("id url website vicinity reference name rating "+"international_phone_number icon formatted_phone_number").split(" ");function GeoComplete(input,options){this.options=$.extend(true,{},defaults,options);this.input=input;this.$input=$(input);this._defaults=defaults;this._name="geocomplete";this.init()}$.extend(GeoComplete.prototype,{init:function(){this.initMap();this.initMarker();this.initGeocoder();this.initDetails();this.initLocation()},initMap:function(){if(!this.options.map){return}if(typeof this.options.map.setCenter=="function"){this.map=this.options.map;return}this.map=new google.maps.Map($(this.options.map)[0],this.options.mapOptions);google.maps.event.addListener(this.map,"click",$.proxy(this.mapClicked,this));google.maps.event.addListener(this.map,"zoom_changed",$.proxy(this.mapZoomed,this))},initMarker:function(){if(!this.map){return}var options=$.extend(this.options.markerOptions,{map:this.map});if(options.disabled){return}this.marker=new google.maps.Marker(options);google.maps.event.addListener(this.marker,"dragend",$.proxy(this.markerDragged,this))},initGeocoder:function(){var options={types:this.options.types,bounds:this.options.bounds===true?null:this.options.bounds,componentRestrictions:this.options.componentRestrictions};if(this.options.country){options.componentRestrictions={country:this.options.country}}this.autocomplete=new google.maps.places.Autocomplete(this.input,options);this.geocoder=new google.maps.Geocoder;if(this.map&&this.options.bounds===true){this.autocomplete.bindTo("bounds",this.map)}google.maps.event.addListener(this.autocomplete,"place_changed",$.proxy(this.placeChanged,this));this.$input.keypress(function(event){if(event.keyCode===13){return false}});this.$input.bind("geocode",$.proxy(function(){this.find()},this));if(this.options.blur===true){this.$input.blur($.proxy(function(){this.find()},this))}},initDetails:function(){if(!this.options.details){return}var $details=$(this.options.details),attribute=this.options.detailsAttribute,details={};function setDetail(value){details[value]=$details.find("["+attribute+"="+value+"]")}$.each(componentTypes,function(index,key){setDetail(key);setDetail(key+"_short")});$.each(placesDetails,function(index,key){setDetail(key)});this.$details=$details;this.details=details},initLocation:function(){var location=this.options.location,latLng;if(!location){return}if(typeof location=="string"){this.find(location);return}if(location instanceof Array){latLng=new google.maps.LatLng(location[0],location[1])}if(location instanceof google.maps.LatLng){latLng=location}if(latLng){if(this.map){this.map.setCenter(latLng)}if(this.marker){this.marker.setPosition(latLng)}}},find:function(address){this.geocode({address:address||this.$input.val()})},geocode:function(request){if(this.options.bounds&&!request.bounds){if(this.options.bounds===true){request.bounds=this.map&&this.map.getBounds()}else{request.bounds=this.options.bounds}}if(this.options.country){request.region=this.options.country}this.geocoder.geocode(request,$.proxy(this.handleGeocode,this))},selectFirstResult:function(){var selected="";if($(".pac-item-selected")["0"]){selected="-selected"}var $span1=$(".pac-container .pac-item"+selected+":first span:nth-child(2)").text();var $span2=$(".pac-container .pac-item"+selected+":first span:nth-child(3)").text();var firstResult=$span1;if($span2){firstResult+=" - "+$span2}this.$input.val(firstResult);return firstResult},handleGeocode:function(results,status){if(status===google.maps.GeocoderStatus.OK){var result=results[0];this.$input.val(result.formatted_address);this.update(result);if(results.length>1){this.trigger("geocode:multiple",results)}}else{this.trigger("geocode:error",status)}},trigger:function(event,argument){this.$input.trigger(event,[argument])},center:function(geometry){if(geometry.viewport){this.map.fitBounds(geometry.viewport);if(this.map.getZoom()>this.options.maxZoom){this.map.setZoom(this.options.maxZoom)}}else{this.map.setZoom(this.options.maxZoom);this.map.setCenter(geometry.location)}if(this.marker){this.marker.setPosition(geometry.location);this.marker.setAnimation(this.options.markerOptions.animation)}},update:function(result){if(this.map){this.center(result.geometry)}if(this.$details){this.fillDetails(result)}this.trigger("geocode:result",result)},fillDetails:function(result){var data={},geometry=result.geometry,viewport=geometry.viewport,bounds=geometry.bounds;$.each(result.address_components,function(index,object){var name=object.types[0];data[name]=object.long_name;data[name+"_short"]=object.short_name});$.each(placesDetails,function(index,key){data[key]=result[key]});$.extend(data,{formatted_address:result.formatted_address,location_type:geometry.location_type||"PLACES",viewport:viewport,bounds:bounds,location:geometry.location,lat:geometry.location.lat(),lng:geometry.location.lng()});$.each(this.details,$.proxy(function(key,$detail){var value=data[key];this.setDetail($detail,value)},this));this.data=data},setDetail:function($element,value){if(value===undefined){value=""}else if(typeof value.toUrlValue=="function"){value=value.toUrlValue()}if($element.is(":input")){$element.val(value)}else{$element.text(value)}},markerDragged:function(event){this.trigger("geocode:dragged",event.latLng)},mapClicked:function(event){this.trigger("geocode:click",event.latLng)},mapZoomed:function(event){this.trigger("geocode:zoom",this.map.getZoom())},resetMarker:function(){this.marker.setPosition(this.data.location);this.setDetail(this.details.lat,this.data.location.lat());this.setDetail(this.details.lng,this.data.location.lng())},placeChanged:function(){var place=this.autocomplete.getPlace();if(!place.geometry){if(this.options.autoselect){var autoSelection=this.selectFirstResult();this.find(autoSelection)}}else{this.update(place)}}});$.fn.geocomplete=function(options){var attribute="plugin_geocomplete";if(typeof options=="string"){var instance=$(this).data(attribute)||$(this).geocomplete().data(attribute),prop=instance[options];if(typeof prop=="function"){prop.apply(instance,Array.prototype.slice.call(arguments,1));return $(this)}else{if(arguments.length==2){prop=arguments[1]}return prop}}else{return this.each(function(){var instance=$.data(this,attribute);if(!instance){instance=new GeoComplete(this,options);$.data(this,attribute,instance)}})}}})(jQuery,window,document); \ No newline at end of file +(function($,window,document,undefined){var defaults={bounds:true,country:null,map:false,details:false,detailsAttribute:"name",autoselect:true,location:false,debug:false,mapOptions:{zoom:14,scrollwheel:false,mapTypeId:"roadmap"},markerOptions:{draggable:false},maxZoom:16,types:["geocode"],blur:false};var componentTypes=("street_address route intersection political "+"country administrative_area_level_1 administrative_area_level_2 "+"administrative_area_level_3 colloquial_area locality sublocality "+"neighborhood premise subpremise postal_code natural_feature airport "+"park point_of_interest post_box street_number floor room "+"lat lng viewport location "+"formatted_address location_type bounds").split(" ");var placesDetails=("id url website vicinity reference name rating "+"international_phone_number icon formatted_phone_number").split(" ");function GeoComplete(input,options){this.options=$.extend(true,{},defaults,options);this.input=input;this.$input=$(input);this._defaults=defaults;this._name="geocomplete";this.init()}$.extend(GeoComplete.prototype,{init:function(){this.initMap();this.initMarker();this.initGeocoder();this.initDetails();this.initLocation()},initMap:function(){if(!this.options.map){return}if(typeof this.options.map.setCenter==="function"){this.map=this.options.map;return}this.map=new google.maps.Map($(this.options.map)[0],this.options.mapOptions);google.maps.event.addListener(this.map,"click",$.proxy(this.mapClicked,this));google.maps.event.addListener(this.map,"zoom_changed",$.proxy(this.mapZoomed,this))},initMarker:function(){if(!this.map){return}var options=$.extend(this.options.markerOptions,{map:this.map});if(options.disabled){return}this.marker=new google.maps.Marker(options);google.maps.event.addListener(this.marker,"dragend",$.proxy(this.markerDragged,this))},initGeocoder:function(){var options={types:this.options.types,bounds:this.options.bounds===true?null:this.options.bounds,componentRestrictions:this.options.componentRestrictions,fields:["geometry","formatted_address","address_components"]};if(this.options.country){options.componentRestrictions={country:this.options.country}}this.autocomplete=new google.maps.places.Autocomplete(this.input,options);this.geocoder=new google.maps.Geocoder;if(this.map&&this.options.bounds===true){this.autocomplete.bindTo("bounds",this.map)}google.maps.event.addListener(this.autocomplete,"place_changed",$.proxy(this.placeChanged,this));this.$input.keypress(function(event){if(event.keyCode===13){return false}});this.$input.bind("geocode",$.proxy(function(){this.find()},this));if(this.options.blur===true){this.$input.blur($.proxy(this.blurHandler,this))}this.$input.keydown($.proxy(this.keydownHandler,this));$(document).on("mousedown.geocomplete",".pac-container",$.proxy(this.pacMousedownHandler,this))},pacMousedownHandler:function pacMousedownHandler(){this.pacSelected=true},keydownHandler:function keydownHandler(event){if(event.which===38||event.which===40){this.pacSelected=true}},blurHandler:function blurHandler(event){this.log("blurHandler",event);if(this.selectedPlace&&this.selectedPlace.formatted_address===this.$input.val()){return}if(this.$input.val()===""){return}if(!this.pacSelected){this.placeChanged({name:this.$input.val()})}},initDetails:function(){if(!this.options.details){return}var $details=$(this.options.details),attribute=this.options.detailsAttribute,details={};function setDetail(value){details[value]=$details.find("["+attribute+"="+value+"]")}$.each(componentTypes,function(index,key){setDetail(key);setDetail(key+"_short")});$.each(placesDetails,function(index,key){setDetail(key)});this.$details=$details;this.details=details},initLocation:function(){var location=this.options.location,latLng;if(!location){return}if(typeof location==="string"){this.find(location);return}if(location instanceof Array){latLng=new google.maps.LatLng(location[0],location[1])}if(location instanceof google.maps.LatLng){latLng=location}if(latLng){if(this.map){this.map.setCenter(latLng)}if(this.marker){this.marker.setPosition(latLng)}}},find:function(address){this.geocode({address:address||this.$input.val()})},geocode:function(request){if(this.options.bounds&&!request.bounds){if(this.options.bounds===true){request.bounds=this.map&&this.map.getBounds()}else{request.bounds=this.options.bounds}}if(this.options.country){request.region=this.options.country}this.geocoder.geocode(request,$.proxy(this.handleGeocode,this))},handleGeocode:function(results,status){if(status===google.maps.GeocoderStatus.OK){var result=results[0];this.$input.val(result.formatted_address);this.update(result);if(results.length>1){this.trigger("geocode:multiple",results)}}else{this.trigger("geocode:error",status)}},trigger:function(event,argument){this.$input.trigger(event,[argument])},center:function(geometry){if(geometry.viewport){this.map.fitBounds(geometry.viewport);if(this.map.getZoom()>this.options.maxZoom){this.map.setZoom(this.options.maxZoom)}}else{this.map.setZoom(this.options.maxZoom);this.map.setCenter(geometry.location)}if(this.marker){this.marker.setPosition(geometry.location);this.marker.setAnimation(this.options.markerOptions.animation)}},update:function(result){this.$input.val(result.formatted_address);if(this.map){this.center(result.geometry)}if(this.$details){this.fillDetails(result)}this.trigger("geocode:result",result)},fillDetails:function(result){var data={},geometry=result.geometry,viewport=geometry.viewport,bounds=geometry.bounds;$.each(result.address_components,function(index,object){var name=object.types[0];data[name]=object.long_name;data[name+"_short"]=object.short_name});$.each(placesDetails,function(index,key){data[key]=result[key]});$.extend(data,{formatted_address:result.formatted_address,location_type:geometry.location_type||"PLACES",viewport:viewport,bounds:bounds,location:geometry.location,lat:geometry.location.lat(),lng:geometry.location.lng()});$.each(this.details,$.proxy(function(key,$detail){var value=data[key];this.setDetail($detail,value)},this));this.data=data},setDetail:function($element,value){if(value===undefined){value=""}else if(typeof value.toUrlValue==="function"){value=value.toUrlValue()}if($element.is(":input")){$element.val(value)}else{$element.text(value)}},markerDragged:function(event){this.trigger("geocode:dragged",event.latLng)},mapClicked:function(event){this.trigger("geocode:click",event.latLng)},mapZoomed:function(){this.trigger("geocode:zoom",this.map.getZoom())},resetMarker:function(){this.marker.setPosition(this.data.location);this.setDetail(this.details.lat,this.data.location.lat());this.setDetail(this.details.lng,this.data.location.lng())},log:function log(){if(this.options.debug){if(typeof console.log==="function"){console.log(Array.prototype.slice.call(arguments))}}},placeChanged:function placeChanged(place){var _this=this;this.selectedPlace=place||this.autocomplete.getPlace();_this.$input.trigger("geocomplete:update",{place:this.selectedPlace});if(typeof this.selectedPlace!=="object"){return}this.log("placeChanged",this.selectedPlace);if(this.selectedPlace.formatted_address){this.update(this.selectedPlace)}else if(this.selectedPlace.name){if(this.options.autoselect){this.autocompleteService=new google.maps.places.AutocompleteService;this.autocompleteService.getPlacePredictions({input:this.selectedPlace.name,offset:this.selectedPlace.name.length,componentRestrictions:this.options.componentRestrictions,types:this.options.types},function predictionCallback(list,status){if(list&&status===google.maps.GeocoderStatus.OK){_this.placesService=new google.maps.places.PlacesService($('
')[0]);_this.placesService.getDetails({reference:list[0].reference,fields:["geometry","formatted_address","address_components"]},function detailsResult(result,status){_this.update(result)})}else{_this.$input.trigger("geocode:error",status)}})}}this.pacSelected=false}});$.fn.geocomplete=function(options){var attribute="plugin_geocomplete";if(typeof options==="string"){var instance=$(this).data(attribute)||$(this).geocomplete().data(attribute),prop=instance[options];if(typeof prop==="function"){prop.apply(instance,Array.prototype.slice.call(arguments,1));return $(this)}else{if(arguments.length===2){prop=arguments[1]}return prop}}else{return this.each(function(){var instance=$.data(this,attribute);if(!instance){instance=new GeoComplete(this,options);$.data(this,attribute,instance)}})}}})(jQuery,window,document); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..70e0dee --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "geocomplete", + "version": "1.7.0", + "description": "An advanced jQuery plugin that wraps the Google Maps API's [Geocoding](https://code.google.com/apis/maps/documentation/javascript/geocoding.html) and [Places Autocomplete](https://code.google.com/apis/maps/documentation/javascript/places.html#places_autocomplete) services. You simply provide an input that lets you search for locations with a nice autocomplete dropdown. Optionally add a container to show an interactive map and a form that will be populated with the address details.", + "main": "jquery.geocomplete.js", + "directories": { + "example": "examples" + }, + "dependencies": { + "docco": "^0.6.3", + "uglifyjs": "^2.3.6" + }, + "devDependencies": { + "grunt": "^0.4.5", + "grunt-release": "^0.7.0" + }, + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://github.com/ubilabs/geocomplete.git" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/ubilabs/geocomplete/issues" + }, + "homepage": "https://github.com/ubilabs/geocomplete" +}