├── .gitignore
├── Gruntfile.js
├── LICENSE
├── README.md
├── bower.json
├── dist
├── autocomplete.min.css
└── autocomplete.min.js
├── example
├── basic.html
├── custom-places.html
├── force-selection.html
├── options.html
└── scroll.html
├── karma.conf.js
├── package.json
├── spec
├── autocomplete_spec.js
└── support
│ └── jasmine.json
└── src
├── autocomplete.css
└── autocomplete.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | *.iml
3 | bower_components/
4 | coverage/
5 | node_modules/
6 | example/lib
--------------------------------------------------------------------------------
/Gruntfile.js:
--------------------------------------------------------------------------------
1 | /*
2 | * angular-google-places-autocomplete
3 | *
4 | * Copyright (c) 2014 "kuhnza" David Kuhn
5 | * Licensed under the MIT license.
6 | * https://github.com/kuhnza/angular-google-places-autocomplete/blob/master/LICENSE
7 | */
8 |
9 | 'use strict';
10 |
11 | module.exports = function (grunt) {
12 | grunt.initConfig({
13 | jshint: {
14 | options: {
15 | reporter: require('jshint-stylish'),
16 | jasmine: true,
17 | node: true,
18 | mocha: true,
19 | predef: ['after', 'afterEach', 'angular', 'before', 'beforeEach',
20 | 'describe', 'expect', 'inject', 'it', 'jasmine', 'spyOn',
21 | 'xdescribe', 'xit']
22 | },
23 | files: [
24 | 'Gruntfile.js',
25 | 'src/**/*.js',
26 | 'test/**/*.js'
27 | ]
28 | },
29 |
30 | karma: {
31 | options: {
32 | configFile: 'karma.conf.js'
33 | },
34 | unit: {
35 | browsers: ['PhantomJS'],
36 | reporters: ['mocha'],
37 | autoWatch: false,
38 | singleRun: true
39 | },
40 | dev: {
41 | browsers: ['PhantomJS'],
42 | reporters: ['mocha'],
43 | autoWatch: true,
44 | singleRun: false
45 | },
46 | release: {
47 | browsers: ['PhantomJS', 'Chrome', 'Firefox'],
48 | reporters: ['mocha'],
49 | autoWatch: false,
50 | singleRun: true
51 | },
52 | coverage: {
53 | browsers: ['PhantomJS'],
54 | reporters: ['coverage'],
55 | autoWatch: false,
56 | singleRun: true
57 | }
58 | },
59 |
60 | clean: {
61 | dist: {
62 | src: 'dist',
63 | dot: true
64 | },
65 | lib: {
66 | src: 'example/lib',
67 | dot: true
68 | },
69 | bower: {
70 | src: 'bower_components',
71 | dot: true
72 | }
73 | },
74 |
75 | bower: {
76 | install: {
77 | options: {
78 | targetDir: 'example/lib'
79 | }
80 | }
81 | },
82 |
83 | cssmin: {
84 | dist: {
85 | expand: true,
86 | cwd: 'dist/',
87 | files: {
88 | 'dist/autocomplete.min.css': 'src/autocomplete.css'
89 | }
90 | }
91 |
92 | },
93 |
94 | uglify: {
95 | dist: {
96 | files: {
97 | 'dist/autocomplete.min.js': 'src/autocomplete.js'
98 | }
99 | }
100 | }
101 | });
102 |
103 | require('load-grunt-tasks')(grunt);
104 |
105 | grunt.registerTask('default', ['build']);
106 | grunt.registerTask('test', ['karma:unit']);
107 | grunt.registerTask('build', [
108 | 'jshint',
109 | 'clean',
110 | 'bower',
111 | 'cssmin',
112 | 'uglify'
113 | ]);
114 | };
115 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014 "kuhnza" David Kuhn
2 |
3 | Permission is hereby granted, free of charge, to any person
4 | obtaining a copy of this software and associated documentation
5 | files (the "Software"), to deal in the Software without
6 | restriction, including without limitation the rights to use,
7 | copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the
9 | Software is furnished to do so, subject to the following
10 | conditions:
11 |
12 | The above copyright notice and this permission notice shall be
13 | included in all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **WARNING: This library is deprecated**
2 | Unfortunately I no longer have the need, time or desire to maintain this lib. That said, if anyone wishes to take it on please let me know.
3 |
4 |
5 | angular-google-places-autocomplete
6 | ================
7 |
8 | Angular directive for the Google Places Autocomplete component.
9 |
10 | Installation
11 | ------------
12 |
13 | Install via bower: `bower install angular-google-places-autocomplete`
14 |
15 | Or if you're old skool, copy `src/autocomplete.js` into your project.
16 |
17 | Then add the script to your page (be sure to include the Google Places API as well):
18 |
19 | ```html
20 |
21 |
22 | ```
23 |
24 | You'll probably also want the styles:
25 |
26 | ```html
27 |
28 | ```
29 |
30 | Usage
31 | -----
32 |
33 | First add the dependency to your app:
34 |
35 | ```javascript
36 | angular.module('myApp', ['google.places']);
37 | ```
38 |
39 | Then you can use the directive on text inputs like so:
40 |
41 | ```html
42 |
43 | ```
44 |
45 | The directive also supports the following _optional_ attributes:
46 |
47 | * forceSelection — forces the user to select from the dropdown. Defaults to `false`.
48 | * options — See [google.maps.places.AutocompleteRequest object specification](https://developers.google.com/maps/documentation/javascript/reference#AutocompletionRequest).
49 |
50 | Examples
51 | --------
52 |
53 | * [Basic](example/basic.html)
54 | * [Options](example/options.html)
55 | * [Force selection](example/force-selection.html)
56 | * [Custom Places](example/custom-places.html)
57 |
58 | Issues or feature requests
59 | --------------------------
60 |
61 | Create a ticket [here](https://github.com/kuhnza/angular-google-places-autocomplete/issues)
62 |
63 | Contributing
64 | ------------
65 |
66 | Issue a pull request including any relevant testing and updated any documentation if required.
67 |
--------------------------------------------------------------------------------
/bower.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-google-places-autocomplete",
3 | "version": "0.2.7",
4 | "main": [
5 | "./src/autocomplete.js",
6 | "./src/autocomplete.css"
7 | ],
8 | "dependencies": {
9 | "angular": "^1.2.x"
10 | },
11 | "devDependencies": {
12 | "angular-mocks": "^1.2.x"
13 | },
14 | "ignore": []
15 | }
16 |
--------------------------------------------------------------------------------
/dist/autocomplete.min.css:
--------------------------------------------------------------------------------
1 | .pac-container{background-color:#fff;position:absolute!important;z-index:1000;border-radius:2px;border-top:1px solid #d9d9d9;font-family:Arial,sans-serif;box-shadow:0 2px 6px rgba(0,0,0,.3);-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box;overflow:hidden}.pac-container:after{content:"";padding:1px 1px 1px 0;height:16px;text-align:right;display:block;background-image:url(//maps.gstatic.com/mapfiles/api-3/images/powered-by-google-on-white2.png);background-position:right;background-repeat:no-repeat;background-size:104px 16px}.hdpi.pac-container:after{background-image:url(//maps.gstatic.com/mapfiles/api-3/images/powered-by-google-on-white2_hdpi.png)}.pac-item{cursor:default;padding:0 4px;text-overflow:ellipsis;overflow:hidden;white-space:nowrap;line-height:30px;text-align:left;border-top:1px solid #e6e6e6;font-size:11px;color:#999}.pac-item:hover{background-color:#fafafa}.pac-item-selected,.pac-item-selected:hover{background-color:#ebf2fe}.pac-matched{font-weight:700}.pac-item-query{font-size:13px;padding-right:3px;color:#000}.pac-icon{width:15px;height:20px;margin-right:7px;margin-top:6px;display:inline-block;vertical-align:top;background-image:url(//maps.gstatic.com/mapfiles/api-3/images/autocomplete-icons.png);background-size:34px}.hdpi .pac-icon{background-image:url(//maps.gstatic.com/mapfiles/api-3/images/autocomplete-icons_hdpi.png)}.pac-icon-search{background-position:-1px -1px}.pac-item-selected .pac-icon-search{background-position:-18px -1px}.pac-icon-marker{background-position:-1px -161px}.pac-item-selected .pac-icon-marker{background-position:-18px -161px}.pac-placeholder{color:gray}.custom-prediction-label{font-style:italic}
--------------------------------------------------------------------------------
/dist/autocomplete.min.js:
--------------------------------------------------------------------------------
1 | "use strict";angular.module("google.places",[]).factory("googlePlacesApi",["$window",function(a){if(!a.google)throw"Global `google` var missing. Did you forget to include the places API script?";return a.google}]).directive("gPlacesAutocomplete",["$parse","$compile","$timeout","$document","googlePlacesApi",function(a,b,c,d,e){return{restrict:"A",require:"^ngModel",scope:{model:"=ngModel",options:"=?",forceSelection:"=?",customPlaces:"=?"},controller:["$scope",function(a){}],link:function(a,f,g,h){function i(){f.bind("keydown",l),f.bind("blur",m),f.bind("submit",m),a.$watch("selected",n)}function j(){var c,e=angular.element("
"),f=angular.element(d[0].body);e.attr({input:"input",query:"query",predictions:"predictions",active:"active",selected:"selected"}),c=b(e)(a),f.append(c),a.$on("$destroy",function(){c.remove()})}function k(){h.$parsers.push(o),h.$formatters.push(p),h.$render=q}function l(b){0!==a.predictions.length&&-1!==w(A,b.which)&&(b.preventDefault(),b.which===z.down?(a.active=(a.active+1)%a.predictions.length,a.$digest()):b.which===z.up?(a.active=(a.active?a.active:a.predictions.length)-1,a.$digest()):13===b.which||9===b.which?(a.forceSelection&&(a.active=-1===a.active?0:a.active),a.$apply(function(){a.selected=a.active,-1===a.selected&&r()})):27===b.which&&a.$apply(function(){b.stopPropagation(),r()}))}function m(b){0!==a.predictions.length&&(a.forceSelection&&(a.selected=-1===a.selected?0:a.selected),a.$digest(),a.$apply(function(){-1===a.selected&&r()}))}function n(){var b;b=a.predictions[a.selected],b&&(b.is_custom?a.$apply(function(){a.model=b.place,a.$emit("g-places-autocomplete:select",b.place),c(function(){h.$viewChangeListeners.forEach(function(a){a()})})}):C.getDetails({placeId:b.place_id},function(b,d){d==e.maps.places.PlacesServiceStatus.OK&&a.$apply(function(){a.model=b,a.$emit("g-places-autocomplete:select",b),c(function(){h.$viewChangeListeners.forEach(function(a){a()})})})}),r())}function o(b){var c;return b&&u(b)?(a.query=b,c=angular.extend({input:b},a.options),B.getPlacePredictions(c,function(b,c){a.$apply(function(){var d;r(),a.customPlaces&&(d=s(a.query),a.predictions.push.apply(a.predictions,d)),c==e.maps.places.PlacesServiceStatus.OK&&a.predictions.push.apply(a.predictions,b),a.predictions.length>5&&(a.predictions.length=5)})}),a.forceSelection?h.$modelValue:b):b}function p(a){var b="";return u(a)?b=a:v(a)&&(b=a.formatted_address),b}function q(){return f.val(h.$viewValue)}function r(){a.active=-1,a.selected=-1,a.predictions=[]}function s(b){var c,d,e,f=[];for(e=0;e0&&f.push({is_custom:!0,custom_prediction_label:c.custom_prediction_label||"(Custom Non-Google Result)",description:c.formatted_address,place:c,matched_substrings:d.matched_substrings,terms:d.terms});return f}function t(a,b){var c,d,e,f=a+"",g=[],h=[];for(d=b.formatted_address.split(","),e=0;e0&&(c.length>=f.length?(x(c,f)&&h.push({length:f.length,offset:e}),f=""):x(f,c)?(h.push({length:c.length,offset:e}),f=f.replace(c,"").trim()):f=""),g.push({value:c,offset:b.formatted_address.indexOf(c)});return{matched_substrings:h,terms:g}}function u(a){return"[object String]"==Object.prototype.toString.call(a)}function v(a){return"[object Object]"==Object.prototype.toString.call(a)}function w(a,b){var c,d;if(null==a)return-1;for(d=a.length,c=0;d>c;c++)if(a[c]===b)return c;return-1}function x(a,b){return 0===y(a).lastIndexOf(y(b),0)}function y(a){return null==a?"":a.toLowerCase()}var z={tab:9,enter:13,esc:27,up:38,down:40},A=[z.tab,z.enter,z.esc,z.up,z.down],B=new e.maps.places.AutocompleteService,C=new e.maps.places.PlacesService(f[0]);!function(){a.query="",a.predictions=[],a.input=f,a.options=a.options||{},j(),i(),k()}()}}}]).directive("gPlacesAutocompleteDrawer",["$window","$document",function(a,b){var c=['"];return{restrict:"A",scope:{input:"=",query:"=",predictions:"=",active:"=",selected:"="},template:c.join(""),link:function(c,d){function e(c){var d=c[0],e=d.getBoundingClientRect(),f=b[0].documentElement,g=b[0].body,h=a.pageYOffset||f.scrollTop||g.scrollTop,i=a.pageXOffset||f.scrollLeft||g.scrollLeft;return{width:e.width,height:e.height,top:e.top+e.height+h,left:e.left+i}}d.bind("mousedown",function(a){a.preventDefault()}),a.onresize=function(){c.$apply(function(){c.position=e(c.input)})},c.isOpen=function(){return c.predictions.length>0},c.isActive=function(a){return c.active===a},c.selectActive=function(a){c.active=a},c.selectPrediction=function(a){c.selected=a},c.$watch("predictions",function(){c.position=e(c.input)},!0)}}}]).directive("gPlacesAutocompletePrediction",[function(){var a=[' ',' ','{{term.value | trailingComma:!$last}} ',' {{prediction.custom_prediction_label}} '];return{restrict:"A",scope:{index:"=",prediction:"=",query:"="},template:a.join("")}}]).filter("highlightMatched",["$sce",function(a){return function(b){var c,d="",e="";return b.matched_substrings.length>0&&b.terms.length>0&&(c=b.matched_substrings[0],d=b.terms[0].value.substr(c.offset,c.length),e=b.terms[0].value.substr(c.offset+c.length)),a.trustAsHtml(''+d+" "+e)}}]).filter("unmatchedTermsOnly",[function(){return function(a,b){var c,d,e=[];for(c=0;c0&&d.offset>b.matched_substrings[0].length&&e.push(d);return e}}]).filter("trailingComma",[function(){return function(a,b){return b?a+",":a}}]);
--------------------------------------------------------------------------------
/example/basic.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Basic Usage
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
27 |
28 |
29 |
30 |
31 |
32 |
Basic Usage
33 |
34 |
37 |
38 |
Result:
39 |
{{place | json}}
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/example/custom-places.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Injecting Custom Place Predictions
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
84 |
85 |
86 |
87 |
88 |
89 |
Injecting Custom Place Predictions
90 |
91 |
92 | Three custom results are injected into the Place Predictions. Try searching for "International Airport"
93 | or "Domestic Airport" to see them in action.
94 |
95 |
96 |
97 |
98 | Custom places appear with a label after them as required by the
99 | Google Places API terms . This label can be
100 | overridden by putting a custom_prediction_label
on your custom place results. The label can
101 | also be styled via the .custom-prediction-label
class.
102 |
103 |
104 |
107 |
108 |
Result:
109 |
{{place | json}}
110 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/example/force-selection.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Forcing selection
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
27 |
28 |
29 |
30 |
31 |
32 |
Forcing selection
33 |
34 |
37 |
38 |
Result:
39 |
{{place | json}}
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/example/options.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Usage with Options
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
32 |
33 |
34 |
35 |
36 |
37 |
Usage with Options
38 |
39 |
42 |
43 |
Result:
44 |
{{place | json}}
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/example/scroll.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Basic Usage
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
Test autocomplete results with scroll
34 |
35 |
38 |
39 |
Result:
40 |
{{place | json}}
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/karma.conf.js:
--------------------------------------------------------------------------------
1 | module.exports = function(config) {
2 | config.set({
3 | basePath: '',
4 | frameworks: ['jasmine'],
5 | files: [
6 | 'bower_components/angular/angular.js',
7 | 'bower_components/angular-mocks/angular-mocks.js',
8 | 'src/**/*.js',
9 | 'spec/**/*.js'
10 | ],
11 | exclude: [],
12 | preprocessors: {
13 | 'src/**/*.js': 'coverage'
14 | },
15 | port: 9876,
16 | colors: true,
17 | logLevel: config.LOG_INFO,
18 | coverageReporter: {
19 | dir: 'spec/coverage/',
20 | includeAllSources: true,
21 | reporters: [
22 | { type: 'html', subdir: '.'},
23 | { type: 'json', subdir: '.', file: 'coverage.json' }
24 | ]
25 | }
26 | });
27 | };
28 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-google-places-autocomplete",
3 | "version": "0.2.7",
4 | "repository": {
5 | "type": "git",
6 | "url": "http://github.com/kuhnza/angular-google-places-autocomplete.git"
7 | },
8 | "dependencies": {
9 | "grunt-contrib-cssmin": "^0.10.0"
10 | },
11 | "devDependencies": {
12 | "grunt": "~0.4.1",
13 | "grunt-bower-task": "^0.4.0",
14 | "grunt-contrib-clean": "^0.5.0",
15 | "grunt-contrib-jshint": "^0.11.2",
16 | "grunt-contrib-uglify": "~0.4.0",
17 | "grunt-karma": "~0.11.0",
18 | "jshint-stylish": "^2.0.1",
19 | "karma": "0.12.0",
20 | "karma-chrome-launcher": "~0.1.2",
21 | "karma-coverage": "^0.5.0",
22 | "karma-firefox-launcher": "~0.1.3",
23 | "karma-jasmine": "~0.2.1",
24 | "karma-mocha-reporter": "^1.1.1",
25 | "karma-phantomjs-launcher": "~0.1.1",
26 | "load-grunt-tasks": "0.4.0"
27 | },
28 | "engines": {
29 | "node": ">=0.10.0"
30 | },
31 | "scripts": {
32 | "test": "grunt test"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/spec/autocomplete_spec.js:
--------------------------------------------------------------------------------
1 | /*
2 | * angular-google-places-autocomplete
3 | *
4 | * Copyright (c) 2014 "kuhnza" David Kuhn
5 | * Licensed under the MIT license.
6 | * https://github.com/kuhnza/angular-google-places-autocomplete/blob/master/LICENSE
7 | */
8 |
9 | "use strict";
10 |
11 | // Sample set of AutocompleteService predictions
12 | var PREDICTIONS = [
13 | {
14 | "description": "18 Elizabeth Street, Waterloo, New South Wales, Australia",
15 | "id": "7ff2649b9154e8649a2516c95aa8cf6cc93a813f",
16 | "matched_substrings": [
17 | {
18 | "length": 4,
19 | "offset": 0
20 | }
21 | ],
22 | "place_id": "ChIJTV3MUSCuEmsRmsdcXOWqjqsSOTE4IEVsaXphYmV0aCBTdHJlZXQsIFdhdGVybG9vLCBOZXcgU291dGggV2FsZXMsIEF1c3RyYWxpYQ",
23 | "reference": "CkQ9AAAAy3q7MwWKLBbOECg7_AABUyvfIA_UagPLlfKtRvB-Lb5JiTr_HQ-h_eqJ6kFgKTpkakpVxsnbe-qDa_g7Ot3zUhIQG3deNdBjROdh1GOclW50bBoUCuBA9EPr3XZO9WSsjqJgGzpPXqA",
24 | "terms": [
25 | {
26 | "offset": 0,
27 | "value": "18 Elizabeth Street"
28 | },
29 | {
30 | "offset": 21,
31 | "value": "Waterloo"
32 | },
33 | {
34 | "offset": 31,
35 | "value": "New South Wales"
36 | },
37 | {
38 | "offset": 48,
39 | "value": "Australia"
40 | }
41 | ],
42 | "types": [ "route", "geocode" ]
43 | },
44 | {
45 | "description": "18 Enmore Road, Newtown, New South Wales, Australia",
46 | "id": "565bbf7706c54cce1f892cbafefc6536a8511401",
47 | "matched_substrings": [
48 | {
49 | "length": 4,
50 | "offset": 0
51 | }
52 | ],
53 | "place_id": "ChIJBXTUmUewEmsRLURHiC6_kPESMzE4IEVubW9yZSBSb2FkLCBOZXd0b3duLCBOZXcgU291dGggV2FsZXMsIEF1c3RyYWxpYQ",
54 | "reference": "CkQ3AAAAGQ7efGL9QoxL-F6acCMBSflEN6a0x1ZGJJo5vJcZ0IHjVZVN3O4NUe2r1EQBJeolHTIj1I4A9f_NKlrB6-uNMRIQNJ93FN3UnjdZXe_MyREqdhoUH7lpHAddLM9XD4K08jh3e9KYuGk",
55 | "terms": [
56 | {
57 | "offset": 0,
58 | "value": "18 Enmore Road"
59 | },
60 | {
61 | "offset": 16,
62 | "value": "Newtown"
63 | },
64 | {
65 | "offset": 25,
66 | "value": "New South Wales"
67 | },
68 | {
69 | "offset": 42,
70 | "value": "Australia"
71 | }
72 | ],
73 | "types": [ "route", "geocode" ]
74 | },
75 | {
76 | "description": "18 Edgecliff Road, Woollahra, New South Wales, Australia",
77 | "id": "693a0891e53897fe8927d83b673d173ed34ac6c1",
78 | "matched_substrings": [
79 | {
80 | "length": 4,
81 | "offset": 0
82 | }
83 | ],
84 | "place_id": "ChIJe7rePu-tEmsRUwKubnjKGYUSODE4IEVkZ2VjbGlmZiBSb2FkLCBXb29sbGFocmEsIE5ldyBTb3V0aCBXYWxlcywgQXVzdHJhbGlh",
85 | "reference": "CkQ8AAAAPpSTyRB1rC-zSKLViiDvWaXdXuBOcCDAXhhlF0-STTrGifuUm3ziduX-H8zye8pTMIbvJi-e4JOq5lOadBBiwRIQmeOe9clMU26I1MH26B2LgxoUiGzILgMIhpNZqF50MpDcmYK0D0Q",
86 | "terms": [
87 | {
88 | "offset": 0,
89 | "value": "18 Edgecliff Road"
90 | },
91 | {
92 | "offset": 19,
93 | "value": "Woollahra"
94 | },
95 | {
96 | "offset": 30,
97 | "value": "New South Wales"
98 | },
99 | {
100 | "offset": 47,
101 | "value": "Australia"
102 | }
103 | ],
104 | "types": [ "route", "geocode" ]
105 | },
106 | {
107 | "description": "18 Euston Road, Alexandria, New South Wales, Australia",
108 | "id": "01cfa48f0b27e93394f347a39b80412287e0a013",
109 | "matched_substrings": [
110 | {
111 | "length": 4,
112 | "offset": 0
113 | }
114 | ],
115 | "place_id": "ChIJ52QqfbOxEmsR_wTF_yJgirISNjE4IEV1c3RvbiBSb2FkLCBBbGV4YW5kcmlhLCBOZXcgU291dGggV2FsZXMsIEF1c3RyYWxpYQ",
116 | "reference": "CkQ6AAAAxGKnSZMdZc9kOfUmzUaTf70zXn78P4J9oZCr06YFeAgxV-Y2ulX97fwb6Al4rJzASKaADCzgQiRNFOSuTcS1txIQjXtWiEthGmN-ZM7G2nMu6RoU_ItmcK4Cm59POh3fHhi6e75_Z_c",
117 | "terms": [
118 | {
119 | "offset": 0,
120 | "value": "18 Euston Road"
121 | },
122 | {
123 | "offset": 16,
124 | "value": "Alexandria"
125 | },
126 | {
127 | "offset": 28,
128 | "value": "New South Wales"
129 | },
130 | {
131 | "offset": 45,
132 | "value": "Australia"
133 | }
134 | ],
135 | "types": [ "route", "geocode" ]
136 | },
137 | {
138 | "description": "18 Erskine Street, Sydney, New South Wales, Australia",
139 | "id": "69ebe34986ae44b13267116f8c4107d190ff2f01",
140 | "matched_substrings": [
141 | {
142 | "length": 4,
143 | "offset": 0
144 | }
145 | ],
146 | "place_id": "ChIJ8dg8UEeuEmsRvdt4xsQoZUMSNTE4IEVyc2tpbmUgU3RyZWV0LCBTeWRuZXksIE5ldyBTb3V0aCBXYWxlcywgQXVzdHJhbGlh",
147 | "reference": "CkQ5AAAAWpXzWfHYj6pkpo62wsJYt-3Xyc6hXWQDQSFUZO7rSaUOF_7eyuDf03v1EwSEn8c6O-UtCyKbAzwPxRZeRX-HtRIQvh7iV2yFT2Wr1hMUgIoqfRoUW9VDCDS_bMk8Yhvg1_LVxRIctxA",
148 | "terms": [
149 | {
150 | "offset": 0,
151 | "value": "18 Erskine Street"
152 | },
153 | {
154 | "offset": 19,
155 | "value": "Sydney"
156 | },
157 | {
158 | "offset": 27,
159 | "value": "New South Wales"
160 | },
161 | {
162 | "offset": 44,
163 | "value": "Australia"
164 | }
165 | ],
166 | "types": [ "route", "geocode" ]
167 | }
168 | ];
169 |
170 | describe('Factory: googlePlacesApi', function () {
171 | var googlePlacesApi;
172 |
173 | beforeEach(module('google.places'));
174 | beforeEach(inject(function (_$window_, _googlePlacesApi_) {
175 | googlePlacesApi = _googlePlacesApi_;
176 | }));
177 |
178 | it('should load', function () {
179 | expect(googlePlacesApi).toBeDefined();
180 | });
181 | });
182 |
183 | describe('Directive: gPlacesAutocomplete', function () {
184 | var $parentScope, $isolatedScope, $compile, googlePlacesApi;
185 |
186 | function compileAndDigest(html) {
187 | var element = angular.element(html);
188 | $compile(element)($parentScope);
189 | $parentScope.$digest();
190 | $isolatedScope = element.isolateScope();
191 | }
192 |
193 | beforeEach(module('google.places'));
194 | beforeEach(inject(function ($rootScope, _$compile_) {
195 | $parentScope = $rootScope.$new();
196 | $compile = _$compile_;
197 | $parentScope.place = null;
198 | compileAndDigest(' ');
199 | }));
200 |
201 | // TODO: write more tests!
202 | it('should initialize model', function () {
203 | });
204 | });
205 |
206 | describe('Directive: gPlacesAutocompleteDrawer', function () {
207 | var $parentScope, $isolatedScope, $compile, element;
208 | var template = '
';
209 |
210 | function compileAndDigest(html) {
211 | element = angular.element(html);
212 | $compile(element)($parentScope);
213 | $parentScope.$digest();
214 | $isolatedScope = element.isolateScope();
215 | }
216 |
217 | beforeEach(module('google.places'));
218 | beforeEach(inject(function ($rootScope, _$compile_) {
219 | $parentScope = $rootScope.$new();
220 | $compile = _$compile_;
221 | $parentScope.input = angular.element(' ');
222 | $parentScope.query = '';
223 | $parentScope.predictions = [];
224 | }));
225 |
226 | describe('when there are no predictions', function () {
227 | beforeEach(function () {
228 | compileAndDigest(template);
229 | });
230 |
231 | it('should close drawer', function () {
232 | expect($isolatedScope.isOpen()).toBe(false);
233 | });
234 | });
235 |
236 | describe('when there are predictions', function () {
237 | var predictionElements;
238 |
239 | beforeEach(function () {
240 | $parentScope.predictions = angular.copy(PREDICTIONS);
241 | compileAndDigest(template);
242 | predictionElements = element.children().children();
243 | });
244 |
245 | it('should open drawer', function () {
246 | expect($isolatedScope.isOpen()).toBe(true);
247 | });
248 |
249 | it('should select the active prediction when hovering', function () {
250 | var activeElement = angular.element(predictionElements['1']);
251 | activeElement.triggerHandler('mouseenter');
252 |
253 | expect($isolatedScope.active).toBe(1);
254 | expect($isolatedScope.isActive(1)).toBe(true);
255 | expect($isolatedScope.isActive(0)).toBe(false);
256 | });
257 |
258 | it('should select the prediction on click', function () {
259 | var activeElement = angular.element(predictionElements['2']);
260 | activeElement.triggerHandler('click');
261 |
262 | expect($isolatedScope.selected).toBe(2);
263 | });
264 |
265 | it('should set the drawer position', function () {
266 | expect($isolatedScope.position).toBeDefined();
267 | });
268 | });
269 | });
270 |
271 | describe('Directive: gPlacesAutocompletePrediction', function () {
272 | var $parentScope, $isolatedScope, $compile;
273 |
274 | function compileAndDigest(html) {
275 | var element = angular.element(html);
276 | $compile(element)($parentScope);
277 | $parentScope.$digest();
278 | $isolatedScope = element.isolateScope();
279 | }
280 |
281 | beforeEach(module('google.places'));
282 |
283 | beforeEach(inject(function ($rootScope, _$compile_) {
284 | $parentScope = $rootScope.$new();
285 | $compile = _$compile_;
286 | $parentScope.$index = 0;
287 | $parentScope.prediction = angular.copy(PREDICTIONS[0]);
288 | $parentScope.query = '18';
289 | compileAndDigest('
');
290 | }));
291 |
292 | // TODO: write more tests!
293 | it('should initialize model', function () {
294 | });
295 | });
296 |
297 | describe('Filter: unmatchedTermsOnly', function () {
298 | var unmatchedTermsOnlyFilter;
299 |
300 | beforeEach(module('google.places'));
301 | beforeEach(inject(function (_unmatchedTermsOnlyFilter_) {
302 | unmatchedTermsOnlyFilter = _unmatchedTermsOnlyFilter_;
303 | }));
304 |
305 | it('should only return unmatched terms for a prediction', function () {
306 | var prediction = angular.copy(PREDICTIONS[0]);
307 | var result = unmatchedTermsOnlyFilter(prediction.terms, prediction);
308 |
309 | expect(result).toEqual([
310 | { "offset": 21, "value": "Waterloo" },
311 | { "offset": 31, "value": "New South Wales" },
312 | { "offset": 48, "value": "Australia" }
313 | ]);
314 | });
315 | });
316 |
317 | describe('Filter: trailingComma', function () {
318 | var trailingCommaFilter;
319 |
320 | beforeEach(module('google.places'));
321 | beforeEach(inject(function (_trailingCommaFilter_) {
322 | trailingCommaFilter = _trailingCommaFilter_;
323 | }));
324 |
325 | it('should append a trailing comma if condition is true', function () {
326 | var result = trailingCommaFilter('a string', true);
327 |
328 | expect(result).toEqual('a string,');
329 | });
330 |
331 | it('should omit the trailing comma if condition is false', function () {
332 | var result = trailingCommaFilter('a string', false);
333 |
334 | expect(result).toEqual('a string');
335 | });
336 | });
337 |
--------------------------------------------------------------------------------
/spec/support/jasmine.json:
--------------------------------------------------------------------------------
1 | {
2 | "spec_dir": "spec",
3 | "spec_files": [
4 | "**/*[sS]pec.js"
5 | ],
6 | "helpers": [
7 | "helpers/**/*.js"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/src/autocomplete.css:
--------------------------------------------------------------------------------
1 | /*
2 | * These are the default styles applied to the stock Google Places Autocomplete component. Importantly they preserve
3 | * the required "powered by Google" logo.
4 | */
5 |
6 | .pac-container {
7 | background-color: #fff;
8 | position: absolute !important;
9 | z-index: 1000;
10 | border-radius: 2px;
11 | border-top: 1px solid #d9d9d9;
12 | font-family: Arial, sans-serif;
13 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3);
14 | -moz-box-sizing: border-box;
15 | -webkit-box-sizing: border-box;
16 | box-sizing: border-box;
17 | overflow: hidden
18 | }
19 |
20 | .pac-container:after {
21 | content: "";
22 | padding: 1px 1px 1px 0;
23 | height: 16px;
24 | text-align: right;
25 | display: block;
26 | background-image: url(//maps.gstatic.com/mapfiles/api-3/images/powered-by-google-on-white2.png);
27 | background-position: right;
28 | background-repeat: no-repeat;
29 | background-size: 104px 16px
30 | }
31 |
32 | .hdpi.pac-container:after {
33 | background-image: url(//maps.gstatic.com/mapfiles/api-3/images/powered-by-google-on-white2_hdpi.png)
34 | }
35 |
36 | .pac-item {
37 | cursor: default;
38 | padding: 0 4px;
39 | text-overflow: ellipsis;
40 | overflow: hidden;
41 | white-space: nowrap;
42 | line-height: 30px;
43 | text-align: left;
44 | border-top: 1px solid #e6e6e6;
45 | font-size: 11px;
46 | color: #999
47 | }
48 |
49 | .pac-item:hover {
50 | background-color: #fafafa
51 | }
52 |
53 | .pac-item-selected, .pac-item-selected:hover {
54 | background-color: #ebf2fe
55 | }
56 |
57 | .pac-matched {
58 | font-weight: 700
59 | }
60 |
61 | .pac-item-query {
62 | font-size: 13px;
63 | padding-right: 3px;
64 | color: #000
65 | }
66 |
67 | .pac-icon {
68 | width: 15px;
69 | height: 20px;
70 | margin-right: 7px;
71 | margin-top: 6px;
72 | display: inline-block;
73 | vertical-align: top;
74 | background-image: url(//maps.gstatic.com/mapfiles/api-3/images/autocomplete-icons.png);
75 | background-size: 34px
76 | }
77 |
78 | .hdpi .pac-icon {
79 | background-image: url(//maps.gstatic.com/mapfiles/api-3/images/autocomplete-icons_hdpi.png)
80 | }
81 |
82 | .pac-icon-search {
83 | background-position: -1px -1px
84 | }
85 |
86 | .pac-item-selected .pac-icon-search {
87 | background-position: -18px -1px
88 | }
89 |
90 | .pac-icon-marker {
91 | background-position: -1px -161px
92 | }
93 |
94 | .pac-item-selected .pac-icon-marker {
95 | background-position: -18px -161px
96 | }
97 |
98 | .pac-placeholder {
99 | color: gray
100 | }
101 |
102 | .custom-prediction-label {
103 | font-style: italic;
104 | }
--------------------------------------------------------------------------------
/src/autocomplete.js:
--------------------------------------------------------------------------------
1 | /*
2 | * angular-google-places-autocomplete
3 | *
4 | * Copyright (c) 2014 "kuhnza" David Kuhn
5 | * Licensed under the MIT license.
6 | * https://github.com/kuhnza/angular-google-places-autocomplete/blob/master/LICENSE
7 | */
8 |
9 | 'use strict';
10 |
11 | angular.module('google.places', [])
12 | /**
13 | * DI wrapper around global google places library.
14 | *
15 | * Note: requires the Google Places API to already be loaded on the page.
16 | */
17 | .factory('googlePlacesApi', ['$window', function ($window) {
18 | if (!$window.google) throw 'Global `google` var missing. Did you forget to include the places API script?';
19 |
20 | return $window.google;
21 | }])
22 |
23 | /**
24 | * Autocomplete directive. Use like this:
25 | *
26 | *
27 | */
28 | .directive('gPlacesAutocomplete',
29 | [ '$parse', '$compile', '$timeout', '$document', 'googlePlacesApi',
30 | function ($parse, $compile, $timeout, $document, google) {
31 |
32 | return {
33 | restrict: 'A',
34 | require: '^ngModel',
35 | scope: {
36 | model: '=ngModel',
37 | options: '=?',
38 | forceSelection: '=?',
39 | customPlaces: '=?'
40 | },
41 | controller: ['$scope', function ($scope) {}],
42 | link: function ($scope, element, attrs, controller) {
43 | var keymap = {
44 | tab: 9,
45 | enter: 13,
46 | esc: 27,
47 | up: 38,
48 | down: 40
49 | },
50 | hotkeys = [keymap.tab, keymap.enter, keymap.esc, keymap.up, keymap.down],
51 | autocompleteService = new google.maps.places.AutocompleteService(),
52 | placesService = new google.maps.places.PlacesService(element[0]);
53 |
54 | (function init() {
55 | $scope.query = '';
56 | $scope.predictions = [];
57 | $scope.input = element;
58 | $scope.options = $scope.options || {};
59 |
60 | initAutocompleteDrawer();
61 | initEvents();
62 | initNgModelController();
63 | }());
64 |
65 | function initEvents() {
66 | element.bind('keydown', onKeydown);
67 | element.bind('blur', onBlur);
68 | element.bind('submit', onBlur);
69 |
70 | $scope.$watch('selected', select);
71 | }
72 |
73 | function initAutocompleteDrawer() {
74 | // Drawer element used to display predictions
75 | var drawerElement = angular.element('
'),
76 | body = angular.element($document[0].body),
77 | $drawer;
78 |
79 | drawerElement.attr({
80 | input: 'input',
81 | query: 'query',
82 | predictions: 'predictions',
83 | active: 'active',
84 | selected: 'selected'
85 | });
86 |
87 | $drawer = $compile(drawerElement)($scope);
88 | body.append($drawer); // Append to DOM
89 |
90 | $scope.$on('$destroy', function() {
91 | $drawer.remove();
92 | });
93 | }
94 |
95 | function initNgModelController() {
96 | controller.$parsers.push(parse);
97 | controller.$formatters.push(format);
98 | controller.$render = render;
99 | }
100 |
101 | function onKeydown(event) {
102 | if ($scope.predictions.length === 0 || indexOf(hotkeys, event.which) === -1) {
103 | return;
104 | }
105 |
106 | event.preventDefault();
107 |
108 | if (event.which === keymap.down) {
109 | $scope.active = ($scope.active + 1) % $scope.predictions.length;
110 | $scope.$digest();
111 | } else if (event.which === keymap.up) {
112 | $scope.active = ($scope.active ? $scope.active : $scope.predictions.length) - 1;
113 | $scope.$digest();
114 | } else if (event.which === 13 || event.which === 9) {
115 | if ($scope.forceSelection) {
116 | $scope.active = ($scope.active === -1) ? 0 : $scope.active;
117 | }
118 |
119 | $scope.$apply(function () {
120 | $scope.selected = $scope.active;
121 |
122 | if ($scope.selected === -1) {
123 | clearPredictions();
124 | }
125 | });
126 | } else if (event.which === 27) {
127 | $scope.$apply(function () {
128 | event.stopPropagation();
129 | clearPredictions();
130 | });
131 | }
132 | }
133 |
134 | function onBlur(event) {
135 | if ($scope.predictions.length === 0) {
136 | return;
137 | }
138 |
139 | if ($scope.forceSelection) {
140 | $scope.selected = ($scope.selected === -1) ? 0 : $scope.selected;
141 | }
142 |
143 | $scope.$digest();
144 |
145 | $scope.$apply(function () {
146 | if ($scope.selected === -1) {
147 | clearPredictions();
148 | }
149 | });
150 | }
151 |
152 | function select() {
153 | var prediction;
154 |
155 | prediction = $scope.predictions[$scope.selected];
156 | if (!prediction) return;
157 |
158 | if (prediction.is_custom) {
159 | $scope.$apply(function () {
160 | $scope.model = prediction.place;
161 | $scope.$emit('g-places-autocomplete:select', prediction.place);
162 | $timeout(function () {
163 | controller.$viewChangeListeners.forEach(function (fn) { fn(); });
164 | });
165 | });
166 | } else {
167 | placesService.getDetails({ placeId: prediction.place_id }, function (place, status) {
168 | if (status == google.maps.places.PlacesServiceStatus.OK) {
169 | $scope.$apply(function () {
170 | $scope.model = place;
171 | $scope.$emit('g-places-autocomplete:select', place);
172 | $timeout(function () {
173 | controller.$viewChangeListeners.forEach(function (fn) { fn(); });
174 | });
175 | });
176 | }
177 | });
178 | }
179 |
180 | clearPredictions();
181 | }
182 |
183 | function parse(viewValue) {
184 | var request;
185 |
186 | if (!(viewValue && isString(viewValue))) return viewValue;
187 |
188 | $scope.query = viewValue;
189 |
190 | request = angular.extend({ input: viewValue }, $scope.options);
191 | autocompleteService.getPlacePredictions(request, function (predictions, status) {
192 | $scope.$apply(function () {
193 | var customPlacePredictions;
194 |
195 | clearPredictions();
196 |
197 | if ($scope.customPlaces) {
198 | customPlacePredictions = getCustomPlacePredictions($scope.query);
199 | $scope.predictions.push.apply($scope.predictions, customPlacePredictions);
200 | }
201 |
202 | if (status == google.maps.places.PlacesServiceStatus.OK) {
203 | $scope.predictions.push.apply($scope.predictions, predictions);
204 | }
205 |
206 | if ($scope.predictions.length > 5) {
207 | $scope.predictions.length = 5; // trim predictions down to size
208 | }
209 | });
210 | });
211 |
212 | if ($scope.forceSelection) {
213 | return controller.$modelValue;
214 | } else {
215 | return viewValue;
216 | }
217 | }
218 |
219 | function format(modelValue) {
220 | var viewValue = "";
221 |
222 | if (isString(modelValue)) {
223 | viewValue = modelValue;
224 | } else if (isObject(modelValue)) {
225 | viewValue = modelValue.formatted_address;
226 | }
227 |
228 | return viewValue;
229 | }
230 |
231 | function render() {
232 | return element.val(controller.$viewValue);
233 | }
234 |
235 | function clearPredictions() {
236 | $scope.active = -1;
237 | $scope.selected = -1;
238 | $scope.predictions = [];
239 | }
240 |
241 | function getCustomPlacePredictions(query) {
242 | var predictions = [],
243 | place, match, i;
244 |
245 | for (i = 0; i < $scope.customPlaces.length; i++) {
246 | place = $scope.customPlaces[i];
247 |
248 | match = getCustomPlaceMatches(query, place);
249 | if (match.matched_substrings.length > 0) {
250 | predictions.push({
251 | is_custom: true,
252 | custom_prediction_label: place.custom_prediction_label || '(Custom Non-Google Result)', // required by https://developers.google.com/maps/terms § 10.1.1 (d)
253 | description: place.formatted_address,
254 | place: place,
255 | matched_substrings: match.matched_substrings,
256 | terms: match.terms
257 | });
258 | }
259 | }
260 |
261 | return predictions;
262 | }
263 |
264 | function getCustomPlaceMatches(query, place) {
265 | var q = query + '', // make a copy so we don't interfere with subsequent matches
266 | terms = [],
267 | matched_substrings = [],
268 | fragment,
269 | termFragments,
270 | i;
271 |
272 | termFragments = place.formatted_address.split(',');
273 | for (i = 0; i < termFragments.length; i++) {
274 | fragment = termFragments[i].trim();
275 |
276 | if (q.length > 0) {
277 | if (fragment.length >= q.length) {
278 | if (startsWith(fragment, q)) {
279 | matched_substrings.push({ length: q.length, offset: i });
280 | }
281 | q = ''; // no more matching to do
282 | } else {
283 | if (startsWith(q, fragment)) {
284 | matched_substrings.push({ length: fragment.length, offset: i });
285 | q = q.replace(fragment, '').trim();
286 | } else {
287 | q = ''; // no more matching to do
288 | }
289 | }
290 | }
291 |
292 | terms.push({
293 | value: fragment,
294 | offset: place.formatted_address.indexOf(fragment)
295 | });
296 | }
297 |
298 | return {
299 | matched_substrings: matched_substrings,
300 | terms: terms
301 | };
302 | }
303 |
304 | function isString(val) {
305 | return Object.prototype.toString.call(val) == '[object String]';
306 | }
307 |
308 | function isObject(val) {
309 | return Object.prototype.toString.call(val) == '[object Object]';
310 | }
311 |
312 | function indexOf(array, item) {
313 | var i, length;
314 |
315 | if (array === null) return -1;
316 |
317 | length = array.length;
318 | for (i = 0; i < length; i++) {
319 | if (array[i] === item) return i;
320 | }
321 | return -1;
322 | }
323 |
324 | function startsWith(string1, string2) {
325 | return toLower(string1).lastIndexOf(toLower(string2), 0) === 0;
326 | }
327 |
328 | function toLower(string) {
329 | return (string === null) ? "" : string.toLowerCase();
330 | }
331 | }
332 | };
333 | }
334 | ])
335 |
336 |
337 | .directive('gPlacesAutocompleteDrawer', ['$window', '$document', function ($window, $document) {
338 | var TEMPLATE = [
339 | '',
340 | '
',
343 | '
',
344 | '
'
345 | ];
346 |
347 | return {
348 | restrict: 'A',
349 | scope:{
350 | input: '=',
351 | query: '=',
352 | predictions: '=',
353 | active: '=',
354 | selected: '='
355 | },
356 | template: TEMPLATE.join(''),
357 | link: function ($scope, element) {
358 | element.bind('mousedown', function (event) {
359 | event.preventDefault(); // prevent blur event from firing when clicking selection
360 | });
361 |
362 | $window.onresize = function () {
363 | $scope.$apply(function () {
364 | $scope.position = getDrawerPosition($scope.input);
365 | });
366 | };
367 |
368 | $scope.isOpen = function () {
369 | return $scope.predictions.length > 0;
370 | };
371 |
372 | $scope.isActive = function (index) {
373 | return $scope.active === index;
374 | };
375 |
376 | $scope.selectActive = function (index) {
377 | $scope.active = index;
378 | };
379 |
380 | $scope.selectPrediction = function (index) {
381 | $scope.selected = index;
382 | };
383 |
384 | $scope.$watch('predictions', function () {
385 | $scope.position = getDrawerPosition($scope.input);
386 | }, true);
387 |
388 | function getDrawerPosition(element) {
389 | var domEl = element[0],
390 | rect = domEl.getBoundingClientRect(),
391 | docEl = $document[0].documentElement,
392 | body = $document[0].body,
393 | scrollTop = $window.pageYOffset || docEl.scrollTop || body.scrollTop,
394 | scrollLeft = $window.pageXOffset || docEl.scrollLeft || body.scrollLeft;
395 |
396 | return {
397 | width: rect.width,
398 | height: rect.height,
399 | top: rect.top + rect.height + scrollTop,
400 | left: rect.left + scrollLeft
401 | };
402 | }
403 | }
404 | };
405 | }])
406 |
407 | .directive('gPlacesAutocompletePrediction', [function () {
408 | var TEMPLATE = [
409 | ' ',
410 | ' ',
411 | '{{term.value | trailingComma:!$last}} ',
412 | ' {{prediction.custom_prediction_label}} '
413 | ];
414 |
415 | return {
416 | restrict: 'A',
417 | scope:{
418 | index:'=',
419 | prediction:'=',
420 | query:'='
421 | },
422 | template: TEMPLATE.join('')
423 | };
424 | }])
425 |
426 | .filter('highlightMatched', ['$sce', function ($sce) {
427 | return function (prediction) {
428 | var matchedPortion = '',
429 | unmatchedPortion = '',
430 | matched;
431 |
432 | if (prediction.matched_substrings.length > 0 && prediction.terms.length > 0) {
433 | matched = prediction.matched_substrings[0];
434 | matchedPortion = prediction.terms[0].value.substr(matched.offset, matched.length);
435 | unmatchedPortion = prediction.terms[0].value.substr(matched.offset + matched.length);
436 | }
437 |
438 | return $sce.trustAsHtml('' + matchedPortion + ' ' + unmatchedPortion);
439 | };
440 | }])
441 |
442 | .filter('unmatchedTermsOnly', [function () {
443 | return function (terms, prediction) {
444 | var i, term, filtered = [];
445 |
446 | for (i = 0; i < terms.length; i++) {
447 | term = terms[i];
448 | if (prediction.matched_substrings.length > 0 && term.offset > prediction.matched_substrings[0].length) {
449 | filtered.push(term);
450 | }
451 | }
452 |
453 | return filtered;
454 | };
455 | }])
456 |
457 | .filter('trailingComma', [function () {
458 | return function (input, condition) {
459 | return (condition) ? input + ',' : input;
460 | };
461 | }]);
462 |
--------------------------------------------------------------------------------