├── .gitignore ├── README.md ├── dist ├── gplaces.css ├── gplaces.js └── gplaces.min.js ├── gulpfile.js ├── index.js ├── less ├── app.less └── components │ └── gplaces.less ├── lib ├── .DS_Store ├── base-tmpl.js ├── base-view.js ├── error-tmpl.js ├── errors.js ├── http.js ├── input-model.js ├── noop.js ├── prediction-tmpl.js ├── register-browser-content.js ├── selection-position-model.js ├── server.js └── utils.js ├── package.json ├── standalone-build.js └── test ├── config.js ├── public ├── index.html └── js │ └── app.js └── server.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/config.local.js 3 | test/public/dist/* 4 | .DS_Store 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gplaces 2 | 3 | > Dependency-free, google maps auto completion input. [See Demos](http://gplaces.goodybag.com) 4 | 5 | ![http://storage.j0.hn/gplaces.gif](http://storage.j0.hn/gplaces.gif) 6 | 7 | __install__ 8 | 9 | ``` 10 | npm install -S gplaces 11 | or 12 | bower install gplaces 13 | ``` 14 | 15 | __usage__ 16 | 17 | After importing the script, setting up your proxy, and setting `gplaces.http`, you can simply add the attribute `data-gplaces` to an HTML element that accepts input and can respond to `keyup` events: 18 | 19 | ```html 20 | 21 | 22 | 23 | ``` 24 | 25 | Also, see [browser support](#browser-support). 26 | 27 | ## Detailed Usage guide 28 | 29 | There are two main hurdles to getting this to work: 30 | 31 | 1. Setting up your own proxy to Google's API (see [why do I need a proxy?](#why-do-i-need-a-proxy)) 32 | 2. Overriding gplaces http implementation to make a request to your api 33 | 34 | 35 | __Get Google API Access__ 36 | 37 | Setup a project in the Google Developers Console: https://code.google.com/apis/console 38 | 39 | After creating your project, find the Google Places API Web Service and enable it. Under the credentials section on the left, get an API key. 40 | 41 | __Setup your proxy__ 42 | 43 | Since Google's Autocomplete API isn't very CORS-friendly, you'll need to setup a proxy on your own proxy. This is easy to do with node and express: 44 | 45 | ``` 46 | npm install -S gplaces 47 | ``` 48 | 49 | In your express app, 50 | 51 | ```javascript 52 | var express = require('express'); 53 | var app = express(); 54 | 55 | app.use( require('body-parser')() ); 56 | 57 | // Mount your api endpoint wherever you like 58 | app.get( '/api/places-autocomplete' 59 | , require('gplaces').proxy({ 60 | key: 'my-api-key' 61 | }) 62 | ); 63 | 64 | app.listen( 3000, function( error ){ 65 | /* ... */ 66 | }); 67 | ``` 68 | 69 | If you need help setting up a proxy for other platforms, create an issue and we'll help you out. 70 | 71 | __Implement `gplaces.http`:__ 72 | 73 | A _feature_ of the gplaces library is the fact that the library will piggy-back off of your app's existing http interface to cut down on library size and errors. 74 | 75 | Before making any requests, setup gplaces http method. Here's an example using [superagent](https://github.com/visionmedia/superagent) and [browserify](https://github.com/substack/browserify): 76 | 77 | ``` 78 | npm install -S superagent 79 | ``` 80 | 81 | ```javascript 82 | var request = require('superagent'); 83 | var gplaces = require('gplaces'); 84 | 85 | gplaces.http( function( input, callback ){ 86 | request 87 | .get('/api/places') 88 | .query({ input: input }) 89 | .end( callback ); 90 | }); 91 | ``` 92 | 93 | Here's an example using jquery: 94 | 95 | ```javascript 96 | gplaces.http( function( input, callback ){ 97 | $.getJSON( '/api/places?input=' + input ) 98 | .error( callback ) 99 | .success( callback.bind( null, null ) ); 100 | }); 101 | ``` 102 | 103 | ## API 104 | 105 | You can either use the HTML attribute api or the JavaScript API. 106 | 107 | ### HTML Attribute API 108 | 109 | Adding the `data-gplaces` attribute any html element that has a value and can respond to keyup events will automatically register the gplaces plugin. 110 | 111 | ```html 112 | 113 | ``` 114 | 115 | #### `data-target="[selector]"` 116 | 117 | Optional CSS selector that the autocomplete results will be rendered to: 118 | 119 | ```html 120 | 121 | ... 122 |
123 |
124 |
125 | ``` 126 | 127 | #### `data-variant="[dark flip bouncy]"` 128 | 129 | A space-separated list of variants that will control look-and-feel of the autocomplete popover. 130 | 131 | ### JavaScript API 132 | 133 | The JS api has more functionality than available from the markup. 134 | 135 | The module exports a single function (the Gplaces Object Factory) with the following properties: 136 | 137 | * [`http( Function onRequest( input, callback ) )`](#http) Connect the gplaces api to your proxy 138 | * [`proxy( [Object options] )`](#proxy) returns an express request handler 139 | 140 | __Gplaces Object Factory__ 141 | 142 | The Gplaces Object Factory returns a Gplaces Object: 143 | 144 | ```javascript 145 | var gplaces = require('gplaces'); 146 | 147 | document.addEventListener('DOMContentLoaded', function(){ 148 | // The gplaces module exports a single function( Element el, Object options ) 149 | // Calling it on an element will register the gplaces plugin 150 | // to that particular element 151 | var autocompleter = gplaces( 152 | document.getElementById('my-gplaces-input') 153 | ); 154 | }); 155 | ``` 156 | 157 | #### Gplaces Object Properties: 158 | 159 | ##### `el` 160 | 161 | The element the plugin is registered to (The first arg to the factory) 162 | 163 | ##### `options` 164 | 165 | The options passed to the Object Factory 166 | 167 | ##### `model` 168 | 169 | The underlying input model which has the following interface: 170 | 171 | * val([String value]) - gets or sets current value 172 | * makeRequest([Function callback(error, result)]) - makes request to api 173 | 174 | ##### `selectionPosition` 175 | 176 | A model describing the current position of keyboard selection. Has the following members: 177 | 178 | * `int length: 0` length of list 179 | * `int pos: -1` position in list 180 | * `up()` decrements pos 181 | * `down()` increments pos 182 | * `set(pos)` sets pos 183 | 184 | ##### `popoverEl` 185 | 186 | The element referring to the popover that contains our autocomplete results 187 | 188 | #### Gplaces Object Methods: 189 | 190 | ##### `render([APIRequest result])` 191 | 192 | Renders the current state of the plugin given an optional result from the autocomplete api. 193 | 194 | ##### `renderPosition()` 195 | 196 | Renders the current state of `.selectionPosition` 197 | 198 | ##### `safelySetElementValue()` 199 | 200 | Sets the value of the input element based on the current state `.model` 201 | 202 | ##### `isShowing()` 203 | 204 | Whether or not the popover is showing 205 | 206 | ##### `hide()` 207 | 208 | Hides the popover results 209 | 210 | ##### `show()` 211 | 212 | Shows the popover 213 | 214 | ##### `cursorToEnd()` 215 | 216 | Moves the input elements' cursor position to the end 217 | 218 | #### proxy 219 | 220 | `gplaces.proxy(...)` is a function that will return an express request handler. You need this to setup your proxy. Be sure to pass in your api key: 221 | 222 | ```javascript 223 | app.get('/api/places' 224 | , require('gplaces').proxy({ 225 | key: 'my-api-key' 226 | }) 227 | ); 228 | ``` 229 | 230 | #### HTTP 231 | 232 | Gplaces relies on dependency injection to perform HTTP requests. The reason being that we did not want ot solve cross-browser HTTP requests and most apps will likely already have their own solution. 233 | 234 | If you have not implement an http callback, then gplaces will throw an error if you attempt to get autocomplete results. 235 | 236 | ```javascript 237 | require('gplaces').http( function( input, callback ){ 238 | $.getJSON( '/api/places?input=' + input ) 239 | .error( callback ) 240 | .success( callback.bind( null, null ) ); 241 | }); 242 | ``` 243 | 244 | ## Browser Support 245 | 246 | IE8+ (IE8 with polyfills) and all major browsers 247 | 248 | ### Polyfills for IE8 249 | 250 | * https://cdnjs.cloudflare.com/ajax/libs/classlist/2014.01.31/classList.min.js 251 | * https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.1.1/es5-shim.js 252 | * https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.1.1/es5-sham.js 253 | * http://storage.j0.hn/events-polyfill.js 254 | 255 | ## FAQs 256 | 257 | ### Why do I need a proxy? 258 | 259 | If you perform an HTTP request to another domain [see CORS](http://www.html5rocks.com/en/tutorials/cors/), most browsers will ask the server to implement the `Acccess-Control-Allow-Origin` response header. If the server does not implement the header, then browser will not allow the request. 260 | 261 | Google does not implement this header. You will need to setup a proxy that does not violate Cross Origin Resource Policies. 262 | 263 | ### Why doesn't this library make the HTTP request for me? 264 | 265 | Performing HTTP requests correctly from all browsers is quite frankly out-of-scope of this project. Virtually every project I work includes _at least 1_ one wrapper for XMLHTTPRequest and 266 | XDomain. 267 | 268 | Simply hooking into that made for a smaller library making fewer assumptions and inevitably working in more places. 269 | 270 | ### Why does this library require polyfills to work in IE? 271 | 272 | You're likely using them already. Why double implement wrappers for poor browsers? 273 | -------------------------------------------------------------------------------- /dist/gplaces.css: -------------------------------------------------------------------------------- 1 | .gplaces-popover { 2 | position: absolute; 3 | z-index: 1000; 4 | width: 100%; 5 | max-width: 400px; 6 | box-shadow: 0 0 15px rgba(0, 0, 0, 0.25); 7 | font-size: 13px; 8 | color: #555; 9 | background: #fff; 10 | } 11 | .gplaces-popover.hide { 12 | display: none; 13 | } 14 | .gplaces-popover .error { 15 | padding: 12px; 16 | } 17 | .gplaces-popover .gplaces-popover-item { 18 | display: block; 19 | cursor: pointer; 20 | padding: 6px 24px; 21 | border-bottom-style: solid; 22 | border-bottom-width: 1px; 23 | } 24 | .gplaces-popover .gplaces-popover-item:last-child { 25 | border: none; 26 | } 27 | .gplaces-popover .highlight { 28 | font-weight: 700; 29 | } 30 | .gplaces-popover .google-logo { 31 | padding: 6px 12px; 32 | margin-bottom: -2px; 33 | text-align: right; 34 | } 35 | .gplaces-popover .google-logo:after { 36 | content: ""; 37 | display: inline-block; 38 | width: 104px; 39 | height: 16px; 40 | background: url('//maps.gstatic.com/mapfiles/api-3/images/powered-by-google-on-white2.png'); 41 | } 42 | .gplaces-popover .gplaces-popover-item { 43 | border-color: rgba(85, 85, 85, 0.1); 44 | } 45 | .gplaces-popover .gplaces-popover-item.active, 46 | .gplaces-popover .gplaces-popover-item:hover { 47 | background-color: rgba(85, 85, 85, 0.03); 48 | } 49 | .gplaces-popover .highlight { 50 | color: #333; 51 | } 52 | .gplaces-popover.dark { 53 | color: #e6e9ed; 54 | background: #434A53; 55 | } 56 | .gplaces-popover.dark .gplaces-popover-item { 57 | border-color: rgba(230, 233, 237, 0.1); 58 | } 59 | .gplaces-popover.dark .gplaces-popover-item.active, 60 | .gplaces-popover.dark .gplaces-popover-item:hover { 61 | background-color: rgba(230, 233, 237, 0.03); 62 | } 63 | .gplaces-popover.dark .highlight { 64 | color: #fff; 65 | } 66 | .gplaces-popover.dark .google-logo:after { 67 | background: url('https://maps.gstatic.com/mapfiles/api-3/images/powered-by-google-on-non-white2.png'); 68 | } 69 | .gplaces-popover.bouncy { 70 | -webkit-transition: -webkit-transform 200ms cubic-bezier(0.23, 1.52, 0.82, 1.07), opacity 0.12s linear; 71 | transition: transform 200ms cubic-bezier(0.23, 1.52, 0.82, 1.07), opacity 0.12s linear; 72 | opacity: 1; 73 | -webkit-transform: scale(1); 74 | transform: scale(1); 75 | -webkit-transform-origin: 0 0; 76 | transform-origin: 0 0; 77 | } 78 | .gplaces-popover.bouncy.hide { 79 | display: inherit; 80 | opacity: 0; 81 | -webkit-transform: scale(0); 82 | transform: scale(0); 83 | display: none\9; 84 | } 85 | .gplaces-popover.flip { 86 | -webkit-transition: -webkit-transform 0.3s cubic-bezier(0.65, 0.22, 0.36, 1), opacity 0.2s cubic-bezier(0.01, 0.98, 1, 1); 87 | transition: transform 0.3s cubic-bezier(0.65, 0.22, 0.36, 1), opacity 0.2s cubic-bezier(0.01, 0.98, 1, 1); 88 | opacity: 1; 89 | -webkit-transform: rotateX(0) skewX(0) translate(0) scaleX(1); 90 | transform: rotateX(0) skewX(0) translate(0) scaleX(1); 91 | -webkit-transform-origin: 0% 0; 92 | transform-origin: 0% 0; 93 | } 94 | .gplaces-popover.flip.hide { 95 | display: inherit; 96 | opacity: 0; 97 | -webkit-transform: rotateX(100deg) skewX(10deg) translate(0, -10px) scaleX(0.5); 98 | transform: rotateX(100deg) skewX(10deg) translate(0, -10px) scaleX(0.5); 99 | pointer-events: none; 100 | display: none\9; 101 | } 102 | -------------------------------------------------------------------------------- /dist/gplaces.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o' 13 | , data.predictions.map( function( prediction ){ 14 | return predictionTmpl( prediction ); 15 | }).join('\n') 16 | , ' ' 17 | , '' 18 | ].join('\n'); 19 | }; 20 | },{"./prediction-tmpl":9}],3:[function(require,module,exports){ 21 | var utils = require('./utils'); 22 | var errors = require('./errors'); 23 | var gplaceInput = require('./input-model'); 24 | var selectionPosition = require('./selection-position-model'); 25 | 26 | module.exports = function( el, options ){ 27 | return Object.create({ 28 | el: el 29 | 30 | , options: utils.defaults( options || {}, { 31 | tmpl: require('./base-tmpl') 32 | , errorTmpl: require('./error-tmpl') 33 | }) 34 | 35 | , init: function(){ 36 | this.model = gplaceInput( function( error, result ){ 37 | if ( error ){ 38 | return this.renderError( errors('UNKNOWN') ); 39 | } 40 | 41 | if ( result && result.status !== 'OK' ){ 42 | if ( result.status === 'ZERO_RESULTS' ){ 43 | result.predictions = []; 44 | } else { 45 | return this.renderError( errors( result.status ) ); 46 | } 47 | } 48 | 49 | if ( result && result.predictions ){ 50 | this.selectionPosition = selectionPosition( result.predictions.length, function(){ 51 | this.renderPosition(); 52 | }.bind( this )); 53 | } 54 | 55 | this.render( result ); 56 | }.bind( this )); 57 | 58 | this.selectionPosition = selectionPosition(); 59 | 60 | this.popoverEl = document.createElement('div'); 61 | this.popoverEl.classList.add( 'gplaces-popover', 'hide' ); 62 | 63 | if ( this.el.getAttribute('data-variant') ){ 64 | this.popoverEl.classList.add.apply( 65 | this.popoverEl.classList 66 | , this.el.getAttribute('data-variant').split(' ') 67 | ); 68 | } 69 | 70 | // If there's a target specified, put the popover in there 71 | // otherwise, insert it after the input 72 | if ( this.options.target ){ 73 | this.options.target.appendChild( this.popoverEl ); 74 | } else { 75 | this.el.parentNode.insertBefore( this.popoverEl, this.el.nextSibling ); 76 | } 77 | 78 | // Delegate clicks to predictions 79 | this.popoverEl.addEventListener( 'click', this.onPredictionClickDelegation.bind( this ) ); 80 | 81 | this.el.addEventListener( 'keyup', this.onInputKeyup.bind( this ) ); 82 | this.el.addEventListener( 'blur', this.onBlur.bind( this ) ); 83 | 84 | document.addEventListener( 'click', this.onBodyClick.bind( this ) ); 85 | 86 | return this; 87 | } 88 | 89 | , renderError: function( error ){ 90 | if ( console ) console.log( error ); 91 | this.popoverEl.innerHTML = this.options.errorTmpl( error ); 92 | return this; 93 | } 94 | 95 | , render: function( result ){ 96 | if ( result ){ 97 | this.popoverEl.innerHTML = this.options.tmpl( result ); 98 | } 99 | 100 | this.renderPosition(); 101 | 102 | return this; 103 | } 104 | 105 | , renderPosition: function(){ 106 | if ( this.selectionPosition.pos === -1 ){ 107 | return this; 108 | } 109 | 110 | var activeEl = this.popoverEl.querySelector('.active'); 111 | 112 | if ( activeEl ){ 113 | activeEl.classList.remove('active'); 114 | } 115 | 116 | activeEl = this.popoverEl 117 | .querySelectorAll('.gplaces-prediction') 118 | [ this.selectionPosition.pos ]; 119 | 120 | activeEl.classList.add('active'); 121 | 122 | this.model.value = activeEl.getAttribute('data-value'); 123 | 124 | this.safelySetElementValue(); 125 | 126 | return this; 127 | } 128 | 129 | , safelySetElementValue: function(){ 130 | if ( this.el.value != this.model.val() ){ 131 | this.el.value = this.model.val(); 132 | } 133 | 134 | return this; 135 | } 136 | 137 | , isShowing: function(){ 138 | return !this.popoverEl.classList.contains('hide'); 139 | } 140 | 141 | , hide: function(){ 142 | this.popoverEl.classList.add('hide'); 143 | return this; 144 | } 145 | 146 | , show: function(){ 147 | this.popoverEl.classList.remove('hide'); 148 | return this; 149 | } 150 | 151 | , cursorToEnd: function(){ 152 | this.el.selectionStart = this.el.selectionEnd = this.el.value.length; 153 | return this; 154 | } 155 | 156 | , onInputKeyup: function( e ){ 157 | this.model.val( e.target.value ); 158 | 159 | if ( e.keyCode === 13 ){ 160 | return this.hide(); 161 | } 162 | 163 | if ( this.isShowing() ){ 164 | var direction = utils.parseDirectionalKeyEvent( e ); 165 | 166 | if ( direction ){ 167 | this.selectionPosition[ direction ](); 168 | 169 | if ( direction === 'up' ){ 170 | setTimeout( this.cursorToEnd.bind( this ), 1 ); 171 | } 172 | } 173 | } 174 | 175 | if ( this.model.val().length ){ 176 | if ( !this.isShowing() ){ 177 | setTimeout( this.show.bind( this ), 100 ); 178 | } 179 | } else if ( this.isShowing() ) { 180 | this.hide(); 181 | } 182 | } 183 | 184 | , onPredictionClick: function( e ){ 185 | this.model.val( e.target.getAttribute('data-value') ); 186 | this.safelySetElementValue(); 187 | } 188 | 189 | , onPredictionClickDelegation: function( e ){ 190 | var foundEl = false; 191 | var MAX_ITERATIONS = 0; 192 | 193 | // Always stop at the body element, or > 5 iterations 194 | while ( !e.target.classList.contains('gplaces-popover-body') ){ 195 | if ( ++MAX_ITERATIONS > 5 ) break; 196 | 197 | e.target = e.target.parentElement; 198 | 199 | if ( e.target.classList.contains('gplaces-prediction') ){ 200 | foundEl = true; 201 | break; 202 | } 203 | } 204 | 205 | if ( !foundEl ) return true; 206 | 207 | this.onPredictionClick( e ); 208 | } 209 | 210 | , onBodyClick: function( e ){ 211 | var shouldClose = utils.isOutsideOf( this.popoverEl, this.el ); 212 | 213 | if ( shouldClose( e.target ) ){ 214 | this.hide(); 215 | } 216 | } 217 | 218 | , onBlur: function( e ){ 219 | var this_ = this; 220 | 221 | // Add a timeout both for effect and so that the click delegate 222 | // event handlers fires before the blur event handler 223 | setTimeout(function(){ 224 | this_.hide(); 225 | }, 100 ); 226 | } 227 | }).init(); 228 | }; 229 | },{"./base-tmpl":2,"./error-tmpl":4,"./errors":5,"./input-model":7,"./selection-position-model":11,"./utils":12}],4:[function(require,module,exports){ 230 | module.exports = function( data ){ 231 | var message = data.message || 'There was an error with the request'; 232 | 233 | return [ 234 | '
' + message + '
' 235 | ].join('\n'); 236 | }; 237 | },{}],5:[function(require,module,exports){ 238 | module.exports = function( code ){ 239 | var proto = code in module.exports.errors ? 240 | module.exports.errors[ code ] : 241 | module.exports.errors.UNKNOWN; 242 | 243 | var error = new Error( proto.message ); 244 | error.code = proto.code; 245 | 246 | return error; 247 | }; 248 | 249 | module.exports.errors = {}; 250 | 251 | module.exports.errors.UNKNOWN = { 252 | code: 'UNKNOWN' 253 | , message: 'There was an error with the request' 254 | }; 255 | 256 | module.exports.errors.OVER_QUERY_LIMIT = { 257 | code: 'OVER_QUERY_LIMIT' 258 | , message: 'You are over your quota' 259 | }; 260 | 261 | module.exports.errors.REQUEST_DENIED = { 262 | code: 'REQUEST_DENIED' 263 | , message: 'Request was denied. Perhaps check your api key?' 264 | }; 265 | 266 | module.exports.errors.INVALID_REQUEST = { 267 | code: 'INVALID_REQUEST' 268 | , message: 'Invalid request. Perhaps you\'re missing the input param?' 269 | }; 270 | },{}],6:[function(require,module,exports){ 271 | module.exports = function( makeRequest, options ){ 272 | module.exports.makeRequest = makeRequest; 273 | }; 274 | 275 | module.exports.makeRequest = function(){ 276 | console.warn('Did not implement http function'); 277 | console.warn('Use something like:'); 278 | console.warn(' gplaces.http( function( input, callback ){'); 279 | console.warn(" request.get('/my-api/endpoint')"); 280 | console.warn(" .query({ input: input })"); 281 | console.warn(" .end( callback )"); 282 | console.warn(' }'); 283 | 284 | throw new Error('Must implement http functionality calling gplaces.http( callback )'); 285 | }; 286 | },{}],7:[function(require,module,exports){ 287 | var api = require('./http'); 288 | 289 | module.exports = function( onChange ){ 290 | return Object.create({ 291 | val: function( str ){ 292 | if ( str === undefined ) return this.value; 293 | 294 | if ( str === this.value ) return this; 295 | 296 | this.value = str; 297 | 298 | onChange(); 299 | 300 | if ( [ null, '' ].indexOf( str ) === -1 ){ 301 | this.makeRequest(); 302 | } 303 | 304 | return this; 305 | } 306 | 307 | , makeRequest: function(){ 308 | api.makeRequest( this.val(), function( error, res ){ 309 | if ( error ) return onChange( error ); 310 | 311 | if ( res.body ){ 312 | return onChange( null, res.body ); 313 | } 314 | 315 | // Consumer parsed the body from the response already 316 | if ( Array.isArray( res.predictions ) ){ 317 | return onChange( null, res ); 318 | } 319 | }); 320 | } 321 | }); 322 | }; 323 | },{"./http":6}],8:[function(require,module,exports){ 324 | module.exports = function(){}; 325 | },{}],9:[function(require,module,exports){ 326 | module.exports = function( prediction ){ 327 | var innerText = ''; 328 | var beginningOfMatch = false; 329 | var endOfMatch = false; 330 | var match; 331 | var c; 332 | 333 | for ( var i = 0, l = prediction.description.length, ii, ll; i < l; i++ ){ 334 | c = prediction.description[i]; 335 | 336 | beginningOfMatch = false; 337 | endOfMatch = false; 338 | 339 | for ( ii = 0, ll = prediction.matched_substrings.length; ii < ll; ii++ ){ 340 | match = prediction.matched_substrings[ ii ]; 341 | 342 | if ( match.offset === i ){ 343 | beginningOfMatch = true; 344 | break; 345 | } 346 | 347 | if ( (match.offset + match.length) === i ){ 348 | endOfMatch = true; 349 | break; 350 | } 351 | } 352 | 353 | if ( beginningOfMatch ){ 354 | innerText += '' + c; 355 | } else if ( endOfMatch ){ 356 | innerText += c + ''; 357 | } else { 358 | innerText += c; 359 | } 360 | } 361 | 362 | return [ 363 | '
' 364 | , innerText 365 | , '
' 366 | ].join('\n'); 367 | }; 368 | },{}],10:[function(require,module,exports){ 369 | (function (global){ 370 | module.exports = function( gplaces ){ 371 | if ( !('document' in global) ) return; 372 | 373 | document.addEventListener('DOMContentLoaded', function(){ 374 | var list = document.querySelectorAll('[data-gplaces]'); 375 | 376 | for ( var i = 0, el, options, target; i < list.length; i++ ){ 377 | el = list[ i ]; 378 | options = {}; 379 | 380 | target = el.getAttribute('data-target'); 381 | 382 | if ( target ){ 383 | options.target = document.querySelector( target ); 384 | } 385 | 386 | gplaces( el, options ); 387 | } 388 | }); 389 | }; 390 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 391 | 392 | },{}],11:[function(require,module,exports){ 393 | module.exports = function( length, change ){ 394 | change = change || function(){}; 395 | 396 | return Object.create({ 397 | length: length || 0 398 | 399 | , pos: -1 400 | 401 | , up: function(){ 402 | return this.set( this.pos - 1 ); 403 | } 404 | 405 | , down: function(){ 406 | return this.set( this.pos + 1 ); 407 | } 408 | 409 | , set: function( pos ){ 410 | if ( pos === this.pos ) return this; 411 | this.pos = Math.max( -1, Math.min( pos, this.length - 1 ) ); 412 | change( this.pos ); 413 | return this; 414 | } 415 | }); 416 | }; 417 | },{}],12:[function(require,module,exports){ 418 | module.exports.defaults = function( a, b ){ 419 | for ( var key in b ) 420 | if ( !( key in a ) ) a[ key ] = b[ key ]; 421 | return a; 422 | }; 423 | 424 | module.exports.mixin = function( a ){ 425 | var args = Array.prototype.slice.call( arguments, 1 ); 426 | var b, key; 427 | 428 | while ( args.length ){ 429 | b = args.pop(); 430 | 431 | for ( key in b ){ 432 | a[ key ] = b[ key ]; 433 | } 434 | } 435 | 436 | return a; 437 | }; 438 | 439 | module.exports.isOutsideOf = function(){ 440 | var what = Array.prototype.slice.call( arguments ); 441 | 442 | var isOutside = function( el ){ 443 | if ( what.indexOf( el ) > -1 ) return false; 444 | if ( el === null ) return true; 445 | return isOutside( el.parentElement ); 446 | }; 447 | 448 | return isOutside; 449 | }; 450 | 451 | module.exports.parseDirectionalKeyEvent = function( e ){ 452 | if ( !e.keyCode ) return null; 453 | 454 | return ({ 455 | 38: 'up' 456 | , 40: 'down' 457 | })[ e.keyCode ] || null; 458 | }; 459 | },{}],13:[function(require,module,exports){ 460 | var gplaces = require('./'); 461 | 462 | if ( typeof window.define === 'function' && window.define.amd ){ 463 | window.define( 'gplaces', function(){ return gplaces; } ); 464 | } else { 465 | window.gplaces = gplaces; 466 | } 467 | },{"./":1}]},{},[13]) 468 | //# sourceMappingURL=data:application/json;charset:utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIm5vZGVfbW9kdWxlcy9icm93c2VyLXBhY2svX3ByZWx1ZGUuanMiLCJpbmRleC5qcyIsImxpYi9iYXNlLXRtcGwuanMiLCJsaWIvYmFzZS12aWV3LmpzIiwibGliL2Vycm9yLXRtcGwuanMiLCJsaWIvZXJyb3JzLmpzIiwibGliL2h0dHAuanMiLCJsaWIvaW5wdXQtbW9kZWwuanMiLCJsaWIvbm9vcC5qcyIsImxpYi9wcmVkaWN0aW9uLXRtcGwuanMiLCJsaWIvcmVnaXN0ZXItYnJvd3Nlci1jb250ZW50LmpzIiwibGliL3NlbGVjdGlvbi1wb3NpdGlvbi1tb2RlbC5qcyIsImxpYi91dGlscy5qcyIsInN0YW5kYWxvbmUtYnVpbGQuanMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7QUNBQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ0pBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNYQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUMvTUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDTkE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUMvQkE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOztBQ2RBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7QUNuQ0E7O0FDQUE7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBOzs7QUN6Q0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTs7OztBQ25CQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDdkJBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7O0FDeENBO0FBQ0E7QUFDQTtBQUNBO0FBQ0E7QUFDQTtBQUNBIiwiZmlsZSI6ImdlbmVyYXRlZC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzQ29udGVudCI6WyIoZnVuY3Rpb24gZSh0LG4scil7ZnVuY3Rpb24gcyhvLHUpe2lmKCFuW29dKXtpZighdFtvXSl7dmFyIGE9dHlwZW9mIHJlcXVpcmU9PVwiZnVuY3Rpb25cIiYmcmVxdWlyZTtpZighdSYmYSlyZXR1cm4gYShvLCEwKTtpZihpKXJldHVybiBpKG8sITApO3ZhciBmPW5ldyBFcnJvcihcIkNhbm5vdCBmaW5kIG1vZHVsZSAnXCIrbytcIidcIik7dGhyb3cgZi5jb2RlPVwiTU9EVUxFX05PVF9GT1VORFwiLGZ9dmFyIGw9bltvXT17ZXhwb3J0czp7fX07dFtvXVswXS5jYWxsKGwuZXhwb3J0cyxmdW5jdGlvbihlKXt2YXIgbj10W29dWzFdW2VdO3JldHVybiBzKG4/bjplKX0sbCxsLmV4cG9ydHMsZSx0LG4scil9cmV0dXJuIG5bb10uZXhwb3J0c312YXIgaT10eXBlb2YgcmVxdWlyZT09XCJmdW5jdGlvblwiJiZyZXF1aXJlO2Zvcih2YXIgbz0wO288ci5sZW5ndGg7bysrKXMocltvXSk7cmV0dXJuIHN9KSIsIm1vZHVsZS5leHBvcnRzICAgICAgICA9IHJlcXVpcmUoJy4vbGliL2Jhc2UtdmlldycpO1xubW9kdWxlLmV4cG9ydHMucHJveHkgID0gcmVxdWlyZSgnLi9saWIvc2VydmVyJyk7XG5tb2R1bGUuZXhwb3J0cy5odHRwICAgPSByZXF1aXJlKCcuL2xpYi9odHRwJyk7XG5cbnJlcXVpcmUoJy4vbGliL3JlZ2lzdGVyLWJyb3dzZXItY29udGVudCcpKCBtb2R1bGUuZXhwb3J0cyApOyIsInZhciBwcmVkaWN0aW9uVG1wbCA9IHJlcXVpcmUoJy4vcHJlZGljdGlvbi10bXBsJyk7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24oIGRhdGEgKXtcbiAgcmV0dXJuIFtcbiAgICAnPGRpdiBjbGFzcz1cImdwbGFjZXMtcG9wb3Zlci1ib2R5XCI+J1xuICAsIGRhdGEucHJlZGljdGlvbnMubWFwKCBmdW5jdGlvbiggcHJlZGljdGlvbiApe1xuICAgICAgcmV0dXJuIHByZWRpY3Rpb25UbXBsKCBwcmVkaWN0aW9uICk7XG4gICAgfSkuam9pbignXFxuJylcbiAgLCAnICA8ZGl2IGNsYXNzPVwiZ29vZ2xlLWxvZ29cIj48L2Rpdj4nXG4gICwgJzwvZGl2PidcbiAgXS5qb2luKCdcXG4nKTtcbn07IiwidmFyIHV0aWxzICAgICAgICAgICAgID0gcmVxdWlyZSgnLi91dGlscycpO1xudmFyIGVycm9ycyAgICAgICAgICAgID0gcmVxdWlyZSgnLi9lcnJvcnMnKTtcbnZhciBncGxhY2VJbnB1dCAgICAgICA9IHJlcXVpcmUoJy4vaW5wdXQtbW9kZWwnKTtcbnZhciBzZWxlY3Rpb25Qb3NpdGlvbiA9IHJlcXVpcmUoJy4vc2VsZWN0aW9uLXBvc2l0aW9uLW1vZGVsJyk7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24oIGVsLCBvcHRpb25zICl7XG4gIHJldHVybiBPYmplY3QuY3JlYXRlKHtcbiAgICBlbDogZWxcbiAgICBcbiAgLCBvcHRpb25zOiB1dGlscy5kZWZhdWx0cyggb3B0aW9ucyB8fCB7fSwge1xuICAgICAgdG1wbDogICAgICAgcmVxdWlyZSgnLi9iYXNlLXRtcGwnKVxuICAgICwgZXJyb3JUbXBsOiAgcmVxdWlyZSgnLi9lcnJvci10bXBsJylcbiAgICB9KVxuICAgIFxuICAsIGluaXQ6IGZ1bmN0aW9uKCl7XG4gICAgICB0aGlzLm1vZGVsID0gZ3BsYWNlSW5wdXQoIGZ1bmN0aW9uKCBlcnJvciwgcmVzdWx0ICl7XG4gICAgICAgIGlmICggZXJyb3IgKXtcbiAgICAgICAgICByZXR1cm4gdGhpcy5yZW5kZXJFcnJvciggZXJyb3JzKCdVTktOT1dOJykgKTtcbiAgICAgICAgfVxuXG4gICAgICAgIGlmICggcmVzdWx0ICYmIHJlc3VsdC5zdGF0dXMgIT09ICdPSycgKXtcbiAgICAgICAgICBpZiAoIHJlc3VsdC5zdGF0dXMgPT09ICdaRVJPX1JFU1VMVFMnICl7XG4gICAgICAgICAgICByZXN1bHQucHJlZGljdGlvbnMgPSBbXTtcbiAgICAgICAgICB9IGVsc2Uge1xuICAgICAgICAgICAgcmV0dXJuIHRoaXMucmVuZGVyRXJyb3IoIGVycm9ycyggcmVzdWx0LnN0YXR1cyApICk7XG4gICAgICAgICAgfVxuICAgICAgICB9XG5cbiAgICAgICAgaWYgKCByZXN1bHQgJiYgcmVzdWx0LnByZWRpY3Rpb25zICl7XG4gICAgICAgICAgdGhpcy5zZWxlY3Rpb25Qb3NpdGlvbiA9IHNlbGVjdGlvblBvc2l0aW9uKCByZXN1bHQucHJlZGljdGlvbnMubGVuZ3RoLCBmdW5jdGlvbigpe1xuICAgICAgICAgICAgdGhpcy5yZW5kZXJQb3NpdGlvbigpO1xuICAgICAgICAgIH0uYmluZCggdGhpcyApKTtcbiAgICAgICAgfVxuXG4gICAgICAgIHRoaXMucmVuZGVyKCByZXN1bHQgKTtcbiAgICAgIH0uYmluZCggdGhpcyApKTtcblxuICAgICAgdGhpcy5zZWxlY3Rpb25Qb3NpdGlvbiA9IHNlbGVjdGlvblBvc2l0aW9uKCk7XG4gICAgXG4gICAgICB0aGlzLnBvcG92ZXJFbCA9IGRvY3VtZW50LmNyZWF0ZUVsZW1lbnQoJ2RpdicpO1xuICAgICAgdGhpcy5wb3BvdmVyRWwuY2xhc3NMaXN0LmFkZCggJ2dwbGFjZXMtcG9wb3ZlcicsICdoaWRlJyApO1xuXG4gICAgICBpZiAoIHRoaXMuZWwuZ2V0QXR0cmlidXRlKCdkYXRhLXZhcmlhbnQnKSApe1xuICAgICAgICB0aGlzLnBvcG92ZXJFbC5jbGFzc0xpc3QuYWRkLmFwcGx5KFxuICAgICAgICAgIHRoaXMucG9wb3ZlckVsLmNsYXNzTGlzdFxuICAgICAgICAsIHRoaXMuZWwuZ2V0QXR0cmlidXRlKCdkYXRhLXZhcmlhbnQnKS5zcGxpdCgnICcpXG4gICAgICAgICk7XG4gICAgICB9XG5cbiAgICAgIC8vIElmIHRoZXJlJ3MgYSB0YXJnZXQgc3BlY2lmaWVkLCBwdXQgdGhlIHBvcG92ZXIgaW4gdGhlcmVcbiAgICAgIC8vIG90aGVyd2lzZSwgaW5zZXJ0IGl0IGFmdGVyIHRoZSBpbnB1dFxuICAgICAgaWYgKCB0aGlzLm9wdGlvbnMudGFyZ2V0ICl7XG4gICAgICAgIHRoaXMub3B0aW9ucy50YXJnZXQuYXBwZW5kQ2hpbGQoIHRoaXMucG9wb3ZlckVsICk7XG4gICAgICB9IGVsc2Uge1xuICAgICAgICB0aGlzLmVsLnBhcmVudE5vZGUuaW5zZXJ0QmVmb3JlKCB0aGlzLnBvcG92ZXJFbCwgdGhpcy5lbC5uZXh0U2libGluZyApO1xuICAgICAgfVxuXG4gICAgICAvLyBEZWxlZ2F0ZSBjbGlja3MgdG8gcHJlZGljdGlvbnNcbiAgICAgIHRoaXMucG9wb3ZlckVsLmFkZEV2ZW50TGlzdGVuZXIoICdjbGljaycsIHRoaXMub25QcmVkaWN0aW9uQ2xpY2tEZWxlZ2F0aW9uLmJpbmQoIHRoaXMgKSApO1xuICAgIFxuICAgICAgdGhpcy5lbC5hZGRFdmVudExpc3RlbmVyKCAna2V5dXAnLCB0aGlzLm9uSW5wdXRLZXl1cC5iaW5kKCB0aGlzICkgKTtcbiAgICAgIHRoaXMuZWwuYWRkRXZlbnRMaXN0ZW5lciggJ2JsdXInLCB0aGlzLm9uQmx1ci5iaW5kKCB0aGlzICkgKTtcblxuICAgICAgZG9jdW1lbnQuYWRkRXZlbnRMaXN0ZW5lciggJ2NsaWNrJywgdGhpcy5vbkJvZHlDbGljay5iaW5kKCB0aGlzICkgKTtcbiAgICBcbiAgICAgIHJldHVybiB0aGlzO1xuICAgIH1cblxuICAsIHJlbmRlckVycm9yOiBmdW5jdGlvbiggZXJyb3IgKXtcbiAgICAgIGlmICggY29uc29sZSApIGNvbnNvbGUubG9nKCBlcnJvciApO1xuICAgICAgdGhpcy5wb3BvdmVyRWwuaW5uZXJIVE1MID0gdGhpcy5vcHRpb25zLmVycm9yVG1wbCggZXJyb3IgKTtcbiAgICAgIHJldHVybiB0aGlzO1xuICAgIH1cbiAgICBcbiAgLCByZW5kZXI6IGZ1bmN0aW9uKCByZXN1bHQgKXtcbiAgICAgIGlmICggcmVzdWx0ICl7XG4gICAgICAgIHRoaXMucG9wb3ZlckVsLmlubmVySFRNTCA9IHRoaXMub3B0aW9ucy50bXBsKCByZXN1bHQgKTtcbiAgICAgIH1cblxuICAgICAgdGhpcy5yZW5kZXJQb3NpdGlvbigpO1xuXG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG5cbiAgLCByZW5kZXJQb3NpdGlvbjogZnVuY3Rpb24oKXtcbiAgICAgIGlmICggdGhpcy5zZWxlY3Rpb25Qb3NpdGlvbi5wb3MgPT09IC0xICl7XG4gICAgICAgIHJldHVybiB0aGlzO1xuICAgICAgfVxuXG4gICAgICB2YXIgYWN0aXZlRWwgPSB0aGlzLnBvcG92ZXJFbC5xdWVyeVNlbGVjdG9yKCcuYWN0aXZlJyk7XG5cbiAgICAgIGlmICggYWN0aXZlRWwgKXtcbiAgICAgICAgYWN0aXZlRWwuY2xhc3NMaXN0LnJlbW92ZSgnYWN0aXZlJyk7XG4gICAgICB9XG5cbiAgICAgIGFjdGl2ZUVsID0gdGhpcy5wb3BvdmVyRWxcbiAgICAgICAgLnF1ZXJ5U2VsZWN0b3JBbGwoJy5ncGxhY2VzLXByZWRpY3Rpb24nKVxuICAgICAgICBbIHRoaXMuc2VsZWN0aW9uUG9zaXRpb24ucG9zIF07XG5cbiAgICAgIGFjdGl2ZUVsLmNsYXNzTGlzdC5hZGQoJ2FjdGl2ZScpO1xuXG4gICAgICB0aGlzLm1vZGVsLnZhbHVlID0gYWN0aXZlRWwuZ2V0QXR0cmlidXRlKCdkYXRhLXZhbHVlJyk7XG5cbiAgICAgIHRoaXMuc2FmZWx5U2V0RWxlbWVudFZhbHVlKCk7XG5cbiAgICAgIHJldHVybiB0aGlzO1xuICAgIH1cblxuICAsIHNhZmVseVNldEVsZW1lbnRWYWx1ZTogZnVuY3Rpb24oKXtcbiAgICAgIGlmICggdGhpcy5lbC52YWx1ZSAhPSB0aGlzLm1vZGVsLnZhbCgpICl7XG4gICAgICAgIHRoaXMuZWwudmFsdWUgPSB0aGlzLm1vZGVsLnZhbCgpO1xuICAgICAgfVxuXG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG5cbiAgLCBpc1Nob3dpbmc6IGZ1bmN0aW9uKCl7XG4gICAgICByZXR1cm4gIXRoaXMucG9wb3ZlckVsLmNsYXNzTGlzdC5jb250YWlucygnaGlkZScpO1xuICAgIH1cblxuICAsIGhpZGU6IGZ1bmN0aW9uKCl7XG4gICAgICB0aGlzLnBvcG92ZXJFbC5jbGFzc0xpc3QuYWRkKCdoaWRlJyk7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG5cbiAgLCBzaG93OiBmdW5jdGlvbigpe1xuICAgICAgdGhpcy5wb3BvdmVyRWwuY2xhc3NMaXN0LnJlbW92ZSgnaGlkZScpO1xuICAgICAgcmV0dXJuIHRoaXM7XG4gICAgfVxuXG4gICwgY3Vyc29yVG9FbmQ6IGZ1bmN0aW9uKCl7XG4gICAgICB0aGlzLmVsLnNlbGVjdGlvblN0YXJ0ID0gdGhpcy5lbC5zZWxlY3Rpb25FbmQgPSB0aGlzLmVsLnZhbHVlLmxlbmd0aDtcbiAgICAgIHJldHVybiB0aGlzO1xuICAgIH1cblxuICAsIG9uSW5wdXRLZXl1cDogZnVuY3Rpb24oIGUgKXtcbiAgICAgIHRoaXMubW9kZWwudmFsKCBlLnRhcmdldC52YWx1ZSApO1xuXG4gICAgICBpZiAoIGUua2V5Q29kZSA9PT0gMTMgKXtcbiAgICAgICAgcmV0dXJuIHRoaXMuaGlkZSgpO1xuICAgICAgfVxuXG4gICAgICBpZiAoIHRoaXMuaXNTaG93aW5nKCkgKXtcbiAgICAgICAgdmFyIGRpcmVjdGlvbiA9IHV0aWxzLnBhcnNlRGlyZWN0aW9uYWxLZXlFdmVudCggZSApO1xuXG4gICAgICAgIGlmICggZGlyZWN0aW9uICl7XG4gICAgICAgICAgdGhpcy5zZWxlY3Rpb25Qb3NpdGlvblsgZGlyZWN0aW9uIF0oKTtcblxuICAgICAgICAgIGlmICggZGlyZWN0aW9uID09PSAndXAnICl7XG4gICAgICAgICAgICBzZXRUaW1lb3V0KCB0aGlzLmN1cnNvclRvRW5kLmJpbmQoIHRoaXMgKSwgMSApO1xuICAgICAgICAgIH1cbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICBpZiAoIHRoaXMubW9kZWwudmFsKCkubGVuZ3RoICl7XG4gICAgICAgIGlmICggIXRoaXMuaXNTaG93aW5nKCkgKXtcbiAgICAgICAgICBzZXRUaW1lb3V0KCB0aGlzLnNob3cuYmluZCggdGhpcyApLCAxMDAgKTtcbiAgICAgICAgfVxuICAgICAgfSBlbHNlIGlmICggdGhpcy5pc1Nob3dpbmcoKSApIHtcbiAgICAgICAgdGhpcy5oaWRlKCk7XG4gICAgICB9XG4gICAgfVxuXG4gICwgb25QcmVkaWN0aW9uQ2xpY2s6IGZ1bmN0aW9uKCBlICl7XG4gICAgICB0aGlzLm1vZGVsLnZhbCggZS50YXJnZXQuZ2V0QXR0cmlidXRlKCdkYXRhLXZhbHVlJykgKTtcbiAgICAgIHRoaXMuc2FmZWx5U2V0RWxlbWVudFZhbHVlKCk7XG4gICAgfVxuXG4gICwgb25QcmVkaWN0aW9uQ2xpY2tEZWxlZ2F0aW9uOiBmdW5jdGlvbiggZSApe1xuICAgICAgdmFyIGZvdW5kRWwgPSBmYWxzZTtcbiAgICAgIHZhciBNQVhfSVRFUkFUSU9OUyA9IDA7XG5cbiAgICAgIC8vIEFsd2F5cyBzdG9wIGF0IHRoZSBib2R5IGVsZW1lbnQsIG9yID4gNSBpdGVyYXRpb25zXG4gICAgICB3aGlsZSAoICFlLnRhcmdldC5jbGFzc0xpc3QuY29udGFpbnMoJ2dwbGFjZXMtcG9wb3Zlci1ib2R5JykgKXtcbiAgICAgICAgaWYgKCArK01BWF9JVEVSQVRJT05TID4gNSApIGJyZWFrO1xuXG4gICAgICAgIGUudGFyZ2V0ID0gZS50YXJnZXQucGFyZW50RWxlbWVudDtcblxuICAgICAgICBpZiAoIGUudGFyZ2V0LmNsYXNzTGlzdC5jb250YWlucygnZ3BsYWNlcy1wcmVkaWN0aW9uJykgKXtcbiAgICAgICAgICBmb3VuZEVsID0gdHJ1ZTtcbiAgICAgICAgICBicmVhaztcbiAgICAgICAgfVxuICAgICAgfVxuXG4gICAgICBpZiAoICFmb3VuZEVsICkgcmV0dXJuIHRydWU7XG5cbiAgICAgIHRoaXMub25QcmVkaWN0aW9uQ2xpY2soIGUgKTtcbiAgICB9XG5cbiAgLCBvbkJvZHlDbGljazogZnVuY3Rpb24oIGUgKXtcbiAgICAgIHZhciBzaG91bGRDbG9zZSA9IHV0aWxzLmlzT3V0c2lkZU9mKCB0aGlzLnBvcG92ZXJFbCwgdGhpcy5lbCApO1xuXG4gICAgICBpZiAoIHNob3VsZENsb3NlKCBlLnRhcmdldCApICl7XG4gICAgICAgIHRoaXMuaGlkZSgpO1xuICAgICAgfVxuICAgIH1cblxuICAsIG9uQmx1cjogZnVuY3Rpb24oIGUgKXtcbiAgICAgIHZhciB0aGlzXyA9IHRoaXM7XG5cbiAgICAgIC8vIEFkZCBhIHRpbWVvdXQgYm90aCBmb3IgZWZmZWN0IGFuZCBzbyB0aGF0IHRoZSBjbGljayBkZWxlZ2F0ZVxuICAgICAgLy8gZXZlbnQgaGFuZGxlcnMgZmlyZXMgYmVmb3JlIHRoZSBibHVyIGV2ZW50IGhhbmRsZXJcbiAgICAgIHNldFRpbWVvdXQoZnVuY3Rpb24oKXtcbiAgICAgICAgdGhpc18uaGlkZSgpO1xuICAgICAgfSwgMTAwICk7XG4gICAgfVxuICB9KS5pbml0KCk7XG59OyIsIm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24oIGRhdGEgKXtcbiAgdmFyIG1lc3NhZ2UgPSBkYXRhLm1lc3NhZ2UgfHwgJ1RoZXJlIHdhcyBhbiBlcnJvciB3aXRoIHRoZSByZXF1ZXN0JztcblxuICByZXR1cm4gW1xuICAgICc8ZGl2IGNsYXNzPVwiZXJyb3JcIj4nICsgbWVzc2FnZSArICc8L2Rpdj4nXG4gIF0uam9pbignXFxuJyk7XG59OyIsIm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24oIGNvZGUgKXtcbiAgdmFyIHByb3RvID0gY29kZSBpbiBtb2R1bGUuZXhwb3J0cy5lcnJvcnMgP1xuICAgICAgbW9kdWxlLmV4cG9ydHMuZXJyb3JzWyBjb2RlIF0gOlxuICAgICAgbW9kdWxlLmV4cG9ydHMuZXJyb3JzLlVOS05PV047XG5cbiAgdmFyIGVycm9yID0gbmV3IEVycm9yKCBwcm90by5tZXNzYWdlICk7XG4gIGVycm9yLmNvZGUgPSBwcm90by5jb2RlO1xuXG4gIHJldHVybiBlcnJvcjtcbn07XG5cbm1vZHVsZS5leHBvcnRzLmVycm9ycyA9IHt9O1xuXG5tb2R1bGUuZXhwb3J0cy5lcnJvcnMuVU5LTk9XTiA9IHtcbiAgY29kZTogJ1VOS05PV04nXG4sIG1lc3NhZ2U6ICdUaGVyZSB3YXMgYW4gZXJyb3Igd2l0aCB0aGUgcmVxdWVzdCdcbn07XG5cbm1vZHVsZS5leHBvcnRzLmVycm9ycy5PVkVSX1FVRVJZX0xJTUlUID0ge1xuICBjb2RlOiAnT1ZFUl9RVUVSWV9MSU1JVCdcbiwgbWVzc2FnZTogJ1lvdSBhcmUgb3ZlciB5b3VyIHF1b3RhJ1xufTtcblxubW9kdWxlLmV4cG9ydHMuZXJyb3JzLlJFUVVFU1RfREVOSUVEID0ge1xuICBjb2RlOiAnUkVRVUVTVF9ERU5JRUQnXG4sIG1lc3NhZ2U6ICdSZXF1ZXN0IHdhcyBkZW5pZWQuIFBlcmhhcHMgY2hlY2sgeW91ciBhcGkga2V5Pydcbn07XG5cbm1vZHVsZS5leHBvcnRzLmVycm9ycy5JTlZBTElEX1JFUVVFU1QgPSB7XG4gIGNvZGU6ICdJTlZBTElEX1JFUVVFU1QnXG4sIG1lc3NhZ2U6ICdJbnZhbGlkIHJlcXVlc3QuIFBlcmhhcHMgeW91XFwncmUgbWlzc2luZyB0aGUgaW5wdXQgcGFyYW0/J1xufTsiLCJtb2R1bGUuZXhwb3J0cyA9IGZ1bmN0aW9uKCBtYWtlUmVxdWVzdCwgb3B0aW9ucyApe1xuICBtb2R1bGUuZXhwb3J0cy5tYWtlUmVxdWVzdCA9IG1ha2VSZXF1ZXN0O1xufTtcblxubW9kdWxlLmV4cG9ydHMubWFrZVJlcXVlc3QgPSBmdW5jdGlvbigpe1xuICBjb25zb2xlLndhcm4oJ0RpZCBub3QgaW1wbGVtZW50IGh0dHAgZnVuY3Rpb24nKTtcbiAgY29uc29sZS53YXJuKCdVc2Ugc29tZXRoaW5nIGxpa2U6Jyk7XG4gIGNvbnNvbGUud2FybignICBncGxhY2VzLmh0dHAoIGZ1bmN0aW9uKCBpbnB1dCwgY2FsbGJhY2sgKXsnKTtcbiAgY29uc29sZS53YXJuKFwiICAgIHJlcXVlc3QuZ2V0KCcvbXktYXBpL2VuZHBvaW50JylcIik7XG4gIGNvbnNvbGUud2FybihcIiAgICAgIC5xdWVyeSh7IGlucHV0OiBpbnB1dCB9KVwiKTtcbiAgY29uc29sZS53YXJuKFwiICAgICAgLmVuZCggY2FsbGJhY2sgKVwiKTtcbiAgY29uc29sZS53YXJuKCcgIH0nKTtcblxuICB0aHJvdyBuZXcgRXJyb3IoJ011c3QgaW1wbGVtZW50IGh0dHAgZnVuY3Rpb25hbGl0eSBjYWxsaW5nIGdwbGFjZXMuaHR0cCggY2FsbGJhY2sgKScpO1xufTsiLCJ2YXIgYXBpID0gcmVxdWlyZSgnLi9odHRwJyk7XG5cbm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24oIG9uQ2hhbmdlICl7XG4gIHJldHVybiBPYmplY3QuY3JlYXRlKHtcbiAgICB2YWw6IGZ1bmN0aW9uKCBzdHIgKXtcbiAgICAgIGlmICggc3RyID09PSB1bmRlZmluZWQgKSByZXR1cm4gdGhpcy52YWx1ZTtcblxuICAgICAgaWYgKCBzdHIgPT09IHRoaXMudmFsdWUgKSByZXR1cm4gdGhpcztcblxuICAgICAgdGhpcy52YWx1ZSA9IHN0cjtcblxuICAgICAgb25DaGFuZ2UoKTtcblxuICAgICAgaWYgKCBbIG51bGwsICcnIF0uaW5kZXhPZiggc3RyICkgPT09IC0xICl7XG4gICAgICAgIHRoaXMubWFrZVJlcXVlc3QoKTtcbiAgICAgIH1cbiAgICAgIFxuICAgICAgcmV0dXJuIHRoaXM7XG4gICAgfVxuICAgIFxuICAsIG1ha2VSZXF1ZXN0OiBmdW5jdGlvbigpe1xuICAgICAgYXBpLm1ha2VSZXF1ZXN0KCB0aGlzLnZhbCgpLCBmdW5jdGlvbiggZXJyb3IsIHJlcyApe1xuICAgICAgICBpZiAoIGVycm9yICkgcmV0dXJuIG9uQ2hhbmdlKCBlcnJvciApO1xuXG4gICAgICAgIGlmICggcmVzLmJvZHkgKXtcbiAgICAgICAgICByZXR1cm4gb25DaGFuZ2UoIG51bGwsIHJlcy5ib2R5ICk7XG4gICAgICAgIH1cblxuICAgICAgICAvLyBDb25zdW1lciBwYXJzZWQgdGhlIGJvZHkgZnJvbSB0aGUgcmVzcG9uc2UgYWxyZWFkeVxuICAgICAgICBpZiAoIEFycmF5LmlzQXJyYXkoIHJlcy5wcmVkaWN0aW9ucyApICl7XG4gICAgICAgICAgcmV0dXJuIG9uQ2hhbmdlKCBudWxsLCByZXMgKTtcbiAgICAgICAgfVxuICAgICAgfSk7XG4gICAgfVxuICB9KTtcbn07IiwibW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbigpe307IiwibW9kdWxlLmV4cG9ydHMgPSBmdW5jdGlvbiggcHJlZGljdGlvbiApe1xuICB2YXIgaW5uZXJUZXh0ID0gJyc7XG4gIHZhciBiZWdpbm5pbmdPZk1hdGNoID0gZmFsc2U7XG4gIHZhciBlbmRPZk1hdGNoID0gZmFsc2U7XG4gIHZhciBtYXRjaDtcbiAgdmFyIGM7XG5cbiAgZm9yICggdmFyIGkgPSAwLCBsID0gcHJlZGljdGlvbi5kZXNjcmlwdGlvbi5sZW5ndGgsIGlpLCBsbDsgaSA8IGw7IGkrKyApe1xuICAgIGMgPSBwcmVkaWN0aW9uLmRlc2NyaXB0aW9uW2ldO1xuXG4gICAgYmVnaW5uaW5nT2ZNYXRjaCA9IGZhbHNlO1xuICAgIGVuZE9mTWF0Y2ggPSBmYWxzZTtcblxuICAgIGZvciAoIGlpID0gMCwgbGwgPSBwcmVkaWN0aW9uLm1hdGNoZWRfc3Vic3RyaW5ncy5sZW5ndGg7IGlpIDwgbGw7IGlpKysgKXtcbiAgICAgIG1hdGNoID0gcHJlZGljdGlvbi5tYXRjaGVkX3N1YnN0cmluZ3NbIGlpIF07XG5cbiAgICAgIGlmICggbWF0Y2gub2Zmc2V0ID09PSBpICl7XG4gICAgICAgIGJlZ2lubmluZ09mTWF0Y2ggPSB0cnVlO1xuICAgICAgICBicmVhaztcbiAgICAgIH1cblxuICAgICAgaWYgKCAobWF0Y2gub2Zmc2V0ICsgbWF0Y2gubGVuZ3RoKSA9PT0gaSApe1xuICAgICAgICBlbmRPZk1hdGNoID0gdHJ1ZTtcbiAgICAgICAgYnJlYWs7XG4gICAgICB9XG4gICAgfVxuXG4gICAgaWYgKCBiZWdpbm5pbmdPZk1hdGNoICl7XG4gICAgICBpbm5lclRleHQgKz0gJzxzcGFuIGNsYXNzPVwiaGlnaGxpZ2h0XCI+JyArIGM7XG4gICAgfSBlbHNlIGlmICggZW5kT2ZNYXRjaCApe1xuICAgICAgaW5uZXJUZXh0ICs9IGMgKyAnPC9zcGFuPic7XG4gICAgfSBlbHNlIHtcbiAgICAgIGlubmVyVGV4dCArPSBjO1xuICAgIH1cbiAgfVxuXG4gIHJldHVybiBbXG4gICAgJzxkaXYgY2xhc3M9XCJncGxhY2VzLXBvcG92ZXItaXRlbSBncGxhY2VzLXByZWRpY3Rpb25cIiBkYXRhLXZhbHVlPVwiJyArIHByZWRpY3Rpb24uZGVzY3JpcHRpb24gKyAnXCI+J1xuICAsIGlubmVyVGV4dFxuICAsICc8L2Rpdj4nXG4gIF0uam9pbignXFxuJyk7XG59OyIsIm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24oIGdwbGFjZXMgKXtcbiAgaWYgKCAhKCdkb2N1bWVudCcgaW4gZ2xvYmFsKSApIHJldHVybjtcblxuICBkb2N1bWVudC5hZGRFdmVudExpc3RlbmVyKCdET01Db250ZW50TG9hZGVkJywgZnVuY3Rpb24oKXtcbiAgICB2YXIgbGlzdCA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3JBbGwoJ1tkYXRhLWdwbGFjZXNdJyk7XG5cbiAgICBmb3IgKCB2YXIgaSA9IDAsIGVsLCBvcHRpb25zLCB0YXJnZXQ7IGkgPCBsaXN0Lmxlbmd0aDsgaSsrICl7XG4gICAgICBlbCA9IGxpc3RbIGkgXTtcbiAgICAgIG9wdGlvbnMgPSB7fTtcblxuICAgICAgdGFyZ2V0ID0gZWwuZ2V0QXR0cmlidXRlKCdkYXRhLXRhcmdldCcpO1xuXG4gICAgICBpZiAoIHRhcmdldCApe1xuICAgICAgICBvcHRpb25zLnRhcmdldCA9IGRvY3VtZW50LnF1ZXJ5U2VsZWN0b3IoIHRhcmdldCApO1xuICAgICAgfVxuXG4gICAgICBncGxhY2VzKCBlbCwgb3B0aW9ucyApO1xuICAgIH1cbiAgfSk7XG59OyIsIm1vZHVsZS5leHBvcnRzID0gZnVuY3Rpb24oIGxlbmd0aCwgY2hhbmdlICl7XG4gIGNoYW5nZSA9IGNoYW5nZSB8fCBmdW5jdGlvbigpe307XG5cbiAgcmV0dXJuIE9iamVjdC5jcmVhdGUoe1xuICAgIGxlbmd0aDogbGVuZ3RoIHx8IDBcblxuICAsIHBvczogLTFcblxuICAsIHVwOiBmdW5jdGlvbigpe1xuICAgICAgcmV0dXJuIHRoaXMuc2V0KCB0aGlzLnBvcyAtIDEgKTtcbiAgICB9XG5cbiAgLCBkb3duOiBmdW5jdGlvbigpe1xuICAgICAgcmV0dXJuIHRoaXMuc2V0KCB0aGlzLnBvcyArIDEgKTtcbiAgICB9XG5cbiAgLCBzZXQ6IGZ1bmN0aW9uKCBwb3MgKXtcbiAgICAgIGlmICggcG9zID09PSB0aGlzLnBvcyApIHJldHVybiB0aGlzO1xuICAgICAgdGhpcy5wb3MgPSBNYXRoLm1heCggLTEsIE1hdGgubWluKCBwb3MsIHRoaXMubGVuZ3RoIC0gMSApICk7XG4gICAgICBjaGFuZ2UoIHRoaXMucG9zICk7XG4gICAgICByZXR1cm4gdGhpcztcbiAgICB9XG4gIH0pO1xufTsiLCJtb2R1bGUuZXhwb3J0cy5kZWZhdWx0cyA9IGZ1bmN0aW9uKCBhLCBiICl7XG4gIGZvciAoIHZhciBrZXkgaW4gYiApXG4gICAgaWYgKCAhKCBrZXkgaW4gYSApICkgYVsga2V5IF0gPSBiWyBrZXkgXTtcbiAgcmV0dXJuIGE7XG59O1xuXG5tb2R1bGUuZXhwb3J0cy5taXhpbiA9IGZ1bmN0aW9uKCBhICl7XG4gIHZhciBhcmdzID0gQXJyYXkucHJvdG90eXBlLnNsaWNlLmNhbGwoIGFyZ3VtZW50cywgMSApO1xuICB2YXIgYiwga2V5O1xuXG4gIHdoaWxlICggYXJncy5sZW5ndGggKXtcbiAgICBiID0gYXJncy5wb3AoKTtcblxuICAgIGZvciAoIGtleSBpbiBiICl7XG4gICAgICBhWyBrZXkgXSA9IGJbIGtleSBdO1xuICAgIH1cbiAgfVxuXG4gIHJldHVybiBhO1xufTtcblxubW9kdWxlLmV4cG9ydHMuaXNPdXRzaWRlT2YgPSBmdW5jdGlvbigpe1xuICB2YXIgd2hhdCA9IEFycmF5LnByb3RvdHlwZS5zbGljZS5jYWxsKCBhcmd1bWVudHMgKTtcblxuICB2YXIgaXNPdXRzaWRlID0gZnVuY3Rpb24oIGVsICl7XG4gICAgaWYgKCB3aGF0LmluZGV4T2YoIGVsICkgPiAtMSApIHJldHVybiBmYWxzZTtcbiAgICBpZiAoIGVsID09PSBudWxsICkgcmV0dXJuIHRydWU7XG4gICAgcmV0dXJuIGlzT3V0c2lkZSggZWwucGFyZW50RWxlbWVudCApO1xuICB9O1xuXG4gIHJldHVybiBpc091dHNpZGU7XG59O1xuXG5tb2R1bGUuZXhwb3J0cy5wYXJzZURpcmVjdGlvbmFsS2V5RXZlbnQgPSBmdW5jdGlvbiggZSApe1xuICBpZiAoICFlLmtleUNvZGUgKSByZXR1cm4gbnVsbDtcblxuICByZXR1cm4gKHtcbiAgICAzODogJ3VwJ1xuICAsIDQwOiAnZG93bidcbiAgfSlbIGUua2V5Q29kZSBdIHx8IG51bGw7XG59OyIsInZhciBncGxhY2VzID0gcmVxdWlyZSgnLi8nKTtcblxuaWYgKCB0eXBlb2Ygd2luZG93LmRlZmluZSA9PT0gJ2Z1bmN0aW9uJyAmJiB3aW5kb3cuZGVmaW5lLmFtZCApe1xuICB3aW5kb3cuZGVmaW5lKCAnZ3BsYWNlcycsIGZ1bmN0aW9uKCl7IHJldHVybiBncGxhY2VzOyB9ICk7XG59IGVsc2Uge1xuICB3aW5kb3cuZ3BsYWNlcyA9IGdwbGFjZXM7XG59Il19 469 | -------------------------------------------------------------------------------- /dist/gplaces.min.js: -------------------------------------------------------------------------------- 1 | !function t(e,i,n){function o(s,l){if(!i[s]){if(!e[s]){var a="function"==typeof require&&require;if(!l&&a)return a(s,!0);if(r)return r(s,!0);var u=new Error("Cannot find module '"+s+"'");throw u.code="MODULE_NOT_FOUND",u}var c=i[s]={exports:{}};e[s][0].call(c.exports,function(t){var i=e[s][1][t];return o(i?i:t)},c,c.exports,t,e,i,n)}return i[s].exports}for(var r="function"==typeof require&&require,s=0;s',t.predictions.map(function(t){return n(t)}).join("\n"),' ',""].join("\n")}},{"./prediction-tmpl":9}],3:[function(t,e,i){var n=t("./utils"),o=t("./errors"),r=t("./input-model"),s=t("./selection-position-model");e.exports=function(e,i){return Object.create({el:e,options:n.defaults(i||{},{tmpl:t("./base-tmpl"),errorTmpl:t("./error-tmpl")}),init:function(){return this.model=r(function(t,e){if(t)return this.renderError(o("UNKNOWN"));if(e&&"OK"!==e.status){if("ZERO_RESULTS"!==e.status)return this.renderError(o(e.status));e.predictions=[]}e&&e.predictions&&(this.selectionPosition=s(e.predictions.length,function(){this.renderPosition()}.bind(this))),this.render(e)}.bind(this)),this.selectionPosition=s(),this.popoverEl=document.createElement("div"),this.popoverEl.classList.add("gplaces-popover","hide"),this.el.getAttribute("data-variant")&&this.popoverEl.classList.add.apply(this.popoverEl.classList,this.el.getAttribute("data-variant").split(" ")),this.options.target?this.options.target.appendChild(this.popoverEl):this.el.parentNode.insertBefore(this.popoverEl,this.el.nextSibling),this.popoverEl.addEventListener("click",this.onPredictionClickDelegation.bind(this)),this.el.addEventListener("keyup",this.onInputKeyup.bind(this)),this.el.addEventListener("blur",this.onBlur.bind(this)),document.addEventListener("click",this.onBodyClick.bind(this)),this},renderError:function(t){return console&&console.log(t),this.popoverEl.innerHTML=this.options.errorTmpl(t),this},render:function(t){return t&&(this.popoverEl.innerHTML=this.options.tmpl(t)),this.renderPosition(),this},renderPosition:function(){if(-1===this.selectionPosition.pos)return this;var t=this.popoverEl.querySelector(".active");return t&&t.classList.remove("active"),t=this.popoverEl.querySelectorAll(".gplaces-prediction")[this.selectionPosition.pos],t.classList.add("active"),this.model.value=t.getAttribute("data-value"),this.safelySetElementValue(),this},safelySetElementValue:function(){return this.el.value!=this.model.val()&&(this.el.value=this.model.val()),this},isShowing:function(){return!this.popoverEl.classList.contains("hide")},hide:function(){return this.popoverEl.classList.add("hide"),this},show:function(){return this.popoverEl.classList.remove("hide"),this},cursorToEnd:function(){return this.el.selectionStart=this.el.selectionEnd=this.el.value.length,this},onInputKeyup:function(t){if(this.model.val(t.target.value),13===t.keyCode)return this.hide();if(this.isShowing()){var e=n.parseDirectionalKeyEvent(t);e&&(this.selectionPosition[e](),"up"===e&&setTimeout(this.cursorToEnd.bind(this),1))}this.model.val().length?this.isShowing()||setTimeout(this.show.bind(this),100):this.isShowing()&&this.hide()},onPredictionClick:function(t){this.model.val(t.target.getAttribute("data-value")),this.safelySetElementValue()},onPredictionClickDelegation:function(t){for(var e=!1,i=0;!(t.target.classList.contains("gplaces-popover-body")||++i>5);)if(t.target=t.target.parentElement,t.target.classList.contains("gplaces-prediction")){e=!0;break}return e?void this.onPredictionClick(t):!0},onBodyClick:function(t){var e=n.isOutsideOf(this.popoverEl,this.el);e(t.target)&&this.hide()},onBlur:function(t){var e=this;setTimeout(function(){e.hide()},100)}}).init()}},{"./base-tmpl":2,"./error-tmpl":4,"./errors":5,"./input-model":7,"./selection-position-model":11,"./utils":12}],4:[function(t,e,i){e.exports=function(t){var e=t.message||"There was an error with the request";return['
'+e+"
"].join("\n")}},{}],5:[function(t,e,i){e.exports=function(t){var i=t in e.exports.errors?e.exports.errors[t]:e.exports.errors.UNKNOWN,n=new Error(i.message);return n.code=i.code,n},e.exports.errors={},e.exports.errors.UNKNOWN={code:"UNKNOWN",message:"There was an error with the request"},e.exports.errors.OVER_QUERY_LIMIT={code:"OVER_QUERY_LIMIT",message:"You are over your quota"},e.exports.errors.REQUEST_DENIED={code:"REQUEST_DENIED",message:"Request was denied. Perhaps check your api key?"},e.exports.errors.INVALID_REQUEST={code:"INVALID_REQUEST",message:"Invalid request. Perhaps you're missing the input param?"}},{}],6:[function(t,e,i){e.exports=function(t,i){e.exports.makeRequest=t},e.exports.makeRequest=function(){throw console.warn("Did not implement http function"),console.warn("Use something like:"),console.warn(" gplaces.http( function( input, callback ){"),console.warn(" request.get('/my-api/endpoint')"),console.warn(" .query({ input: input })"),console.warn(" .end( callback )"),console.warn(" }"),new Error("Must implement http functionality calling gplaces.http( callback )")}},{}],7:[function(t,e,i){var n=t("./http");e.exports=function(t){return Object.create({val:function(e){return void 0===e?this.value:e===this.value?this:(this.value=e,t(),-1===[null,""].indexOf(e)&&this.makeRequest(),this)},makeRequest:function(){n.makeRequest(this.val(),function(e,i){return e?t(e):i.body?t(null,i.body):Array.isArray(i.predictions)?t(null,i):void 0})}})}},{"./http":6}],8:[function(t,e,i){e.exports=function(){}},{}],9:[function(t,e,i){e.exports=function(t){for(var e,i,n,o,r="",s=!1,l=!1,a=0,u=t.description.length;u>a;a++){for(i=t.description[a],s=!1,l=!1,n=0,o=t.matched_substrings.length;o>n;n++){if(e=t.matched_substrings[n],e.offset===a){s=!0;break}if(e.offset+e.length===a){l=!0;break}}r+=s?''+i:l?i+"":i}return['
',r,"
"].join("\n")}},{}],10:[function(t,e,i){(function(t){e.exports=function(e){"document"in t&&document.addEventListener("DOMContentLoaded",function(){for(var t,i,n,o=document.querySelectorAll("[data-gplaces]"),r=0;r-1?!1:null===i?!0:e(i.parentElement)};return e},e.exports.parseDirectionalKeyEvent=function(t){return t.keyCode?{38:"up",40:"down"}[t.keyCode]||null:null}},{}],13:[function(t,e,i){var n=t("./");"function"==typeof window.define&&window.define.amd?window.define("gplaces",function(){return n}):window.gplaces=n},{"./":1}]},{},[13]); -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var gulp = require('gulp'); 3 | var pkg = require('./package.json'); 4 | gulp.util = require('gulp-util'); 5 | 6 | var config = { 7 | scripts: ['*.js', 'test/public/js/*.js', 'lib/*.js', 'test/*.js'] 8 | }; 9 | 10 | config.lint = config.scripts.concat(['test/*.js']); 11 | 12 | gulp.task('clear-dist-dir', function(){ 13 | fs.readdirSync('./dist').forEach( function( filename ){ 14 | fs.unlinkSync( './dist/' + filename ); 15 | }); 16 | }); 17 | 18 | gulp.task( 'build-dist', ['lint', 'clear-dist-dir'], function(){ 19 | return require('browserify')({ 20 | debug: true 21 | }) 22 | .add('./standalone-build.js') 23 | .bundle() 24 | .pipe( fs.createWriteStream('./dist/gplaces.js') ); 25 | }); 26 | 27 | gulp.task( 'minify-dist', ['clear-dist-dir', 'build-dist'], function(){ 28 | return gulp.src('./dist/gplaces.js') 29 | .pipe( require('gulp-uglify')() ) 30 | .pipe( require('gulp-concat')('gplaces.min.js') ) 31 | .pipe( gulp.dest('./dist') ); 32 | }); 33 | 34 | gulp.task( 'scripts', function(){ 35 | return require('browserify')({ 36 | debug: true 37 | }) 38 | .add('./test/public/js/app.js') 39 | .bundle() 40 | .pipe( fs.createWriteStream('./test/public/dist/app.js') ); 41 | }); 42 | 43 | gulp.task( 'less', function(){ 44 | return gulp.src('less/app.less') 45 | .pipe( require('gulp-less')() ) 46 | .pipe( require('gulp-autoprefixer')() ) 47 | .pipe( gulp.dest('test/public/dist') ); 48 | }); 49 | 50 | gulp.task( 'less-dist', ['clear-dist-dir'], function(){ 51 | return gulp.src('less/components/gplaces.less') 52 | .pipe( require('gulp-less')() ) 53 | .pipe( require('gulp-autoprefixer')() ) 54 | .pipe( gulp.dest('./dist') ); 55 | }); 56 | 57 | gulp.task( 'lint', function(){ 58 | return gulp.src( config.lint ) 59 | .pipe( require('gulp-jshint')({ 60 | "laxcomma": true, 61 | "sub": true, 62 | "globals": { 63 | "console": true, 64 | "module": true 65 | } 66 | })) 67 | .pipe( require('gulp-jshint').reporter('jshint-stylish') ); 68 | }); 69 | 70 | gulp.task( 'server', function( done ){ 71 | var PORT = 3020; 72 | require('./test/server').listen( PORT, function( error ){ 73 | if ( error ) return done( error ); 74 | 75 | gulp.util.log( 'Server started on port ' + gulp.util.colors.blue( PORT ) ); 76 | 77 | done(); 78 | }); 79 | }); 80 | 81 | gulp.task( 'watch', function(){ 82 | gulp.watch( config.lint, ['lint'] ); 83 | gulp.watch( config.scripts, ['scripts'] ); 84 | gulp.watch( ['less/*.less', 'less/**/*.less'], ['less'] ); 85 | }); 86 | 87 | gulp.task( 'dist', [ 'lint', 'clear-dist-dir', 'less-dist', 'build-dist', 'minify-dist' ] ); 88 | gulp.task( 'build', [ 'less', 'lint', 'scripts' ] ); 89 | gulp.task( 'default', [ 'build', 'server', 'watch' ] ); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/base-view'); 2 | module.exports.proxy = require('./lib/server'); 3 | module.exports.http = require('./lib/http'); 4 | 5 | require('./lib/register-browser-content')( module.exports ); -------------------------------------------------------------------------------- /less/app.less: -------------------------------------------------------------------------------- 1 | ///////// 2 | // App // 3 | ///////// 4 | 5 | @import "./components/gplaces.less"; 6 | 7 | input { 8 | box-sizing: border-box; 9 | width: 300px; 10 | font-size: 16px; 11 | padding: 12px 16px; 12 | color: #666; 13 | } 14 | 15 | html, body { 16 | font-family: sans-serif; 17 | } -------------------------------------------------------------------------------- /less/components/gplaces.less: -------------------------------------------------------------------------------- 1 | ///////////// 2 | // Gplaces // 3 | ///////////// 4 | 5 | .gplaces-popover { 6 | position: absolute; 7 | z-index: 1000; 8 | 9 | &.hide { 10 | display: none; 11 | } 12 | 13 | width: 100%; 14 | max-width: 400px; 15 | box-shadow: 0 0 15px rgba( 0, 0, 0, 0.25 ); 16 | font-size: 13px; 17 | 18 | .error { 19 | padding: 12px; 20 | } 21 | 22 | .gplaces-popover-item { 23 | display: block; 24 | cursor: pointer; 25 | padding: 6px 24px; 26 | border-bottom-style: solid; 27 | border-bottom-width: 1px; 28 | 29 | &:last-child { 30 | border: none; 31 | } 32 | } 33 | 34 | .highlight { 35 | font-weight: 700; 36 | } 37 | 38 | .google-logo { 39 | padding: 6px 12px; 40 | // Make the logo feel more balanced 41 | margin-bottom: -2px; 42 | 43 | text-align: right; 44 | &:after { 45 | content: ""; 46 | display: inline-block; 47 | width: 104px; 48 | height: 16px; 49 | background: url('//maps.gstatic.com/mapfiles/api-3/images/powered-by-google-on-white2.png'); 50 | } 51 | } 52 | 53 | // Themes 54 | 55 | // Default 56 | & { #gplaces > .theme( #fff, #555, #333 ); } 57 | &.dark { #gplaces > .theme( #434A53, #e6e9ed, #fff ) } 58 | 59 | // Themes that need the dark logo 60 | &.dark { 61 | .google-logo { 62 | &:after { 63 | background: url('https://maps.gstatic.com/mapfiles/api-3/images/powered-by-google-on-non-white2.png'); 64 | } 65 | } 66 | } 67 | 68 | // Variants 69 | &.bouncy { 70 | transition: 71 | transform 200ms cubic-bezier( 0.23, 1.52, 0.82, 1.07 ), 72 | opacity 0.12s linear; 73 | 74 | opacity: 1; 75 | transform: scale(1); 76 | transform-origin: 0 0; 77 | 78 | &.hide { 79 | display: inherit; 80 | opacity: 0; 81 | transform: scale(0); 82 | 83 | // IE8 84 | display: none\9; 85 | } 86 | } 87 | 88 | &.flip { 89 | transition: 90 | transform 0.3s cubic-bezier(0.65, 0.22, 0.36, 1), 91 | opacity 0.2s cubic-bezier(0.01, 0.98, 1, 1); 92 | opacity: 1; 93 | transform: rotateX(0) skewX(0) translate(0) scaleX(1); 94 | transform-origin: 0% 0; 95 | 96 | &.hide { 97 | display: inherit; 98 | opacity: 0; 99 | transform: rotateX(100deg) skewX(10deg) translate(-0, -10px) scaleX(0.5); 100 | pointer-events: none; 101 | 102 | // IE8 103 | display: none\9; 104 | } 105 | } 106 | } 107 | 108 | #gplaces { 109 | .theme( @background, @text-color, @highlight-color ){ 110 | & { 111 | color: @text-color; 112 | background: @background; 113 | 114 | .gplaces-popover-item { 115 | border-color: fade( @text-color, 10% ); 116 | 117 | &.active, 118 | &:hover { 119 | background-color: fade( @text-color, 3% ); 120 | } 121 | } 122 | 123 | .highlight { 124 | color: @highlight-color; 125 | } 126 | } 127 | } 128 | } -------------------------------------------------------------------------------- /lib/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/goodybag/gplaces/fafa058ff0511a8e823544eeeb2f95998cd77940/lib/.DS_Store -------------------------------------------------------------------------------- /lib/base-tmpl.js: -------------------------------------------------------------------------------- 1 | var predictionTmpl = require('./prediction-tmpl'); 2 | 3 | module.exports = function( data ){ 4 | return [ 5 | '
' 6 | , data.predictions.map( function( prediction ){ 7 | return predictionTmpl( prediction ); 8 | }).join('\n') 9 | , ' ' 10 | , '
' 11 | ].join('\n'); 12 | }; -------------------------------------------------------------------------------- /lib/base-view.js: -------------------------------------------------------------------------------- 1 | var utils = require('./utils'); 2 | var errors = require('./errors'); 3 | var gplaceInput = require('./input-model'); 4 | var selectionPosition = require('./selection-position-model'); 5 | 6 | module.exports = function( el, options ){ 7 | return Object.create({ 8 | el: el 9 | 10 | , options: utils.defaults( options || {}, { 11 | tmpl: require('./base-tmpl') 12 | , errorTmpl: require('./error-tmpl') 13 | }) 14 | 15 | , init: function(){ 16 | this.model = gplaceInput( function( error, result ){ 17 | if ( error ){ 18 | return this.renderError( errors('UNKNOWN') ); 19 | } 20 | 21 | if ( result && result.status !== 'OK' ){ 22 | if ( result.status === 'ZERO_RESULTS' ){ 23 | result.predictions = []; 24 | } else { 25 | return this.renderError( errors( result.status ) ); 26 | } 27 | } 28 | 29 | if ( result && result.predictions ){ 30 | this.selectionPosition = selectionPosition( result.predictions.length, function(){ 31 | this.renderPosition(); 32 | }.bind( this )); 33 | } 34 | 35 | this.render( result ); 36 | }.bind( this )); 37 | 38 | this.selectionPosition = selectionPosition(); 39 | 40 | this.popoverEl = document.createElement('div'); 41 | this.popoverEl.classList.add( 'gplaces-popover', 'hide' ); 42 | 43 | if ( this.el.getAttribute('data-variant') ){ 44 | this.popoverEl.classList.add.apply( 45 | this.popoverEl.classList 46 | , this.el.getAttribute('data-variant').split(' ') 47 | ); 48 | } 49 | 50 | // If there's a target specified, put the popover in there 51 | // otherwise, insert it after the input 52 | if ( this.options.target ){ 53 | this.options.target.appendChild( this.popoverEl ); 54 | } else { 55 | this.el.parentNode.insertBefore( this.popoverEl, this.el.nextSibling ); 56 | } 57 | 58 | // Delegate clicks to predictions 59 | this.popoverEl.addEventListener( 'click', this.onPredictionClickDelegation.bind( this ) ); 60 | 61 | this.el.addEventListener( 'keyup', this.onInputKeyup.bind( this ) ); 62 | this.el.addEventListener( 'blur', this.onBlur.bind( this ) ); 63 | 64 | document.addEventListener( 'click', this.onBodyClick.bind( this ) ); 65 | 66 | return this; 67 | } 68 | 69 | , renderError: function( error ){ 70 | if ( console ) console.log( error ); 71 | this.popoverEl.innerHTML = this.options.errorTmpl( error ); 72 | return this; 73 | } 74 | 75 | , render: function( result ){ 76 | if ( result ){ 77 | this.popoverEl.innerHTML = this.options.tmpl( result ); 78 | } 79 | 80 | this.renderPosition(); 81 | 82 | return this; 83 | } 84 | 85 | , renderPosition: function(){ 86 | if ( this.selectionPosition.pos === -1 ){ 87 | return this; 88 | } 89 | 90 | var activeEl = this.popoverEl.querySelector('.active'); 91 | 92 | if ( activeEl ){ 93 | activeEl.classList.remove('active'); 94 | } 95 | 96 | activeEl = this.popoverEl 97 | .querySelectorAll('.gplaces-prediction') 98 | [ this.selectionPosition.pos ]; 99 | 100 | activeEl.classList.add('active'); 101 | 102 | this.model.value = activeEl.getAttribute('data-value'); 103 | 104 | this.safelySetElementValue(); 105 | 106 | return this; 107 | } 108 | 109 | , safelySetElementValue: function(){ 110 | if ( this.el.value != this.model.val() ){ 111 | this.el.value = this.model.val(); 112 | } 113 | 114 | return this; 115 | } 116 | 117 | , isShowing: function(){ 118 | return !this.popoverEl.classList.contains('hide'); 119 | } 120 | 121 | , hide: function(){ 122 | this.popoverEl.classList.add('hide'); 123 | return this; 124 | } 125 | 126 | , show: function(){ 127 | this.popoverEl.classList.remove('hide'); 128 | return this; 129 | } 130 | 131 | , cursorToEnd: function(){ 132 | this.el.selectionStart = this.el.selectionEnd = this.el.value.length; 133 | return this; 134 | } 135 | 136 | , onInputKeyup: function( e ){ 137 | this.model.val( e.target.value ); 138 | 139 | if ( e.keyCode === 13 ){ 140 | return this.hide(); 141 | } 142 | 143 | if ( this.isShowing() ){ 144 | var direction = utils.parseDirectionalKeyEvent( e ); 145 | 146 | if ( direction ){ 147 | this.selectionPosition[ direction ](); 148 | 149 | if ( direction === 'up' ){ 150 | setTimeout( this.cursorToEnd.bind( this ), 1 ); 151 | } 152 | } 153 | } 154 | 155 | if ( this.model.val().length ){ 156 | if ( !this.isShowing() ){ 157 | setTimeout( this.show.bind( this ), 100 ); 158 | } 159 | } else if ( this.isShowing() ) { 160 | this.hide(); 161 | } 162 | } 163 | 164 | , onPredictionClick: function( e ){ 165 | this.model.val( e.target.getAttribute('data-value') ); 166 | this.safelySetElementValue(); 167 | } 168 | 169 | , onPredictionClickDelegation: function( e ){ 170 | var foundEl = false; 171 | var MAX_ITERATIONS = 0; 172 | 173 | // Always stop at the body element, or > 5 iterations 174 | while ( !e.target.classList.contains('gplaces-popover-body') ){ 175 | if ( ++MAX_ITERATIONS > 5 ) break; 176 | 177 | e.target = e.target.parentElement; 178 | 179 | if ( e.target.classList.contains('gplaces-prediction') ){ 180 | foundEl = true; 181 | break; 182 | } 183 | } 184 | 185 | if ( !foundEl ) return true; 186 | 187 | this.onPredictionClick( e ); 188 | } 189 | 190 | , onBodyClick: function( e ){ 191 | var shouldClose = utils.isOutsideOf( this.popoverEl, this.el ); 192 | 193 | if ( shouldClose( e.target ) ){ 194 | this.hide(); 195 | } 196 | } 197 | 198 | , onBlur: function( e ){ 199 | var this_ = this; 200 | 201 | // Add a timeout both for effect and so that the click delegate 202 | // event handlers fires before the blur event handler 203 | setTimeout(function(){ 204 | this_.hide(); 205 | }, 100 ); 206 | } 207 | }).init(); 208 | }; -------------------------------------------------------------------------------- /lib/error-tmpl.js: -------------------------------------------------------------------------------- 1 | module.exports = function( data ){ 2 | var message = data.message || 'There was an error with the request'; 3 | 4 | return [ 5 | '
' + message + '
' 6 | ].join('\n'); 7 | }; -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | module.exports = function( code ){ 2 | var proto = code in module.exports.errors ? 3 | module.exports.errors[ code ] : 4 | module.exports.errors.UNKNOWN; 5 | 6 | var error = new Error( proto.message ); 7 | error.code = proto.code; 8 | 9 | return error; 10 | }; 11 | 12 | module.exports.errors = {}; 13 | 14 | module.exports.errors.UNKNOWN = { 15 | code: 'UNKNOWN' 16 | , message: 'There was an error with the request' 17 | }; 18 | 19 | module.exports.errors.OVER_QUERY_LIMIT = { 20 | code: 'OVER_QUERY_LIMIT' 21 | , message: 'You are over your quota' 22 | }; 23 | 24 | module.exports.errors.REQUEST_DENIED = { 25 | code: 'REQUEST_DENIED' 26 | , message: 'Request was denied. Perhaps check your api key?' 27 | }; 28 | 29 | module.exports.errors.INVALID_REQUEST = { 30 | code: 'INVALID_REQUEST' 31 | , message: 'Invalid request. Perhaps you\'re missing the input param?' 32 | }; -------------------------------------------------------------------------------- /lib/http.js: -------------------------------------------------------------------------------- 1 | module.exports = function( makeRequest, options ){ 2 | module.exports.makeRequest = makeRequest; 3 | }; 4 | 5 | module.exports.makeRequest = function(){ 6 | console.warn('Did not implement http function'); 7 | console.warn('Use something like:'); 8 | console.warn(' gplaces.http( function( input, callback ){'); 9 | console.warn(" request.get('/my-api/endpoint')"); 10 | console.warn(" .query({ input: input })"); 11 | console.warn(" .end( callback )"); 12 | console.warn(' }'); 13 | 14 | throw new Error('Must implement http functionality calling gplaces.http( callback )'); 15 | }; -------------------------------------------------------------------------------- /lib/input-model.js: -------------------------------------------------------------------------------- 1 | var api = require('./http'); 2 | 3 | module.exports = function( onChange ){ 4 | return Object.create({ 5 | val: function( str ){ 6 | if ( str === undefined ) return this.value; 7 | 8 | if ( str === this.value ) return this; 9 | 10 | this.value = str; 11 | 12 | onChange(); 13 | 14 | if ( [ null, '' ].indexOf( str ) === -1 ){ 15 | this.makeRequest(); 16 | } 17 | 18 | return this; 19 | } 20 | 21 | , makeRequest: function(){ 22 | api.makeRequest( this.val(), function( error, res ){ 23 | if ( error ) return onChange( error ); 24 | 25 | if ( res.body ){ 26 | return onChange( null, res.body ); 27 | } 28 | 29 | // Consumer parsed the body from the response already 30 | if ( Array.isArray( res.predictions ) ){ 31 | return onChange( null, res ); 32 | } 33 | }); 34 | } 35 | }); 36 | }; -------------------------------------------------------------------------------- /lib/noop.js: -------------------------------------------------------------------------------- 1 | module.exports = function(){}; -------------------------------------------------------------------------------- /lib/prediction-tmpl.js: -------------------------------------------------------------------------------- 1 | module.exports = function( prediction ){ 2 | var innerText = ''; 3 | var beginningOfMatch = false; 4 | var endOfMatch = false; 5 | var match; 6 | var c; 7 | 8 | for ( var i = 0, l = prediction.description.length, ii, ll; i < l; i++ ){ 9 | c = prediction.description[i]; 10 | 11 | beginningOfMatch = false; 12 | endOfMatch = false; 13 | 14 | for ( ii = 0, ll = prediction.matched_substrings.length; ii < ll; ii++ ){ 15 | match = prediction.matched_substrings[ ii ]; 16 | 17 | if ( match.offset === i ){ 18 | beginningOfMatch = true; 19 | break; 20 | } 21 | 22 | if ( (match.offset + match.length) === i ){ 23 | endOfMatch = true; 24 | break; 25 | } 26 | } 27 | 28 | if ( beginningOfMatch ){ 29 | innerText += '' + c; 30 | } else if ( endOfMatch ){ 31 | innerText += c + ''; 32 | } else { 33 | innerText += c; 34 | } 35 | } 36 | 37 | return [ 38 | '
' 39 | , innerText 40 | , '
' 41 | ].join('\n'); 42 | }; -------------------------------------------------------------------------------- /lib/register-browser-content.js: -------------------------------------------------------------------------------- 1 | module.exports = function( gplaces ){ 2 | if ( !('document' in global) ) return; 3 | 4 | document.addEventListener('DOMContentLoaded', function(){ 5 | var list = document.querySelectorAll('[data-gplaces]'); 6 | 7 | for ( var i = 0, el, options, target; i < list.length; i++ ){ 8 | el = list[ i ]; 9 | options = {}; 10 | 11 | target = el.getAttribute('data-target'); 12 | 13 | if ( target ){ 14 | options.target = document.querySelector( target ); 15 | } 16 | 17 | gplaces( el, options ); 18 | } 19 | }); 20 | }; -------------------------------------------------------------------------------- /lib/selection-position-model.js: -------------------------------------------------------------------------------- 1 | module.exports = function( length, change ){ 2 | change = change || function(){}; 3 | 4 | return Object.create({ 5 | length: length || 0 6 | 7 | , pos: -1 8 | 9 | , up: function(){ 10 | return this.set( this.pos - 1 ); 11 | } 12 | 13 | , down: function(){ 14 | return this.set( this.pos + 1 ); 15 | } 16 | 17 | , set: function( pos ){ 18 | if ( pos === this.pos ) return this; 19 | this.pos = Math.max( -1, Math.min( pos, this.length - 1 ) ); 20 | change( this.pos ); 21 | return this; 22 | } 23 | }); 24 | }; -------------------------------------------------------------------------------- /lib/server.js: -------------------------------------------------------------------------------- 1 | var https = require('https'); 2 | var qs = require('querystring'); 3 | 4 | var defaults = { 5 | baseUrl: 'https://maps.googleapis.com/maps/api/place/autocomplete/json?' 6 | , optionsToQueryString: ['key'] 7 | }; 8 | 9 | module.exports = function( options ){ 10 | options = options || {}; 11 | 12 | for ( var key in defaults ){ 13 | if ( !(key in options) ){ 14 | options[ key ] = defaults[ key ]; 15 | } 16 | } 17 | 18 | if ( !options.key ){ 19 | throw new Error('Must provide Google Places Autocomplete API key'); 20 | } 21 | 22 | return function( req, res ){ 23 | res.header( 'Content-Type', 'application/json' ); 24 | 25 | // Copy some properties from options (mainly, `key`) 26 | var query = options.optionsToQueryString.reduce( function( obj, key ){ 27 | obj[ key ] = options[ key ]; 28 | return obj; 29 | }, {} ); 30 | 31 | // Copy the request query string to the new request 32 | for ( var key in req.query ){ 33 | query[ key ] = req.query[ key ]; 34 | } 35 | 36 | var url = options.baseUrl + qs.stringify( query ); 37 | 38 | return https 39 | .get( url, function( gres ){ 40 | gres.pipe( res ); 41 | }); 42 | }; 43 | }; -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | module.exports.defaults = function( a, b ){ 2 | for ( var key in b ) 3 | if ( !( key in a ) ) a[ key ] = b[ key ]; 4 | return a; 5 | }; 6 | 7 | module.exports.mixin = function( a ){ 8 | var args = Array.prototype.slice.call( arguments, 1 ); 9 | var b, key; 10 | 11 | while ( args.length ){ 12 | b = args.pop(); 13 | 14 | for ( key in b ){ 15 | a[ key ] = b[ key ]; 16 | } 17 | } 18 | 19 | return a; 20 | }; 21 | 22 | module.exports.isOutsideOf = function(){ 23 | var what = Array.prototype.slice.call( arguments ); 24 | 25 | var isOutside = function( el ){ 26 | if ( what.indexOf( el ) > -1 ) return false; 27 | if ( el === null ) return true; 28 | return isOutside( el.parentElement ); 29 | }; 30 | 31 | return isOutside; 32 | }; 33 | 34 | module.exports.parseDirectionalKeyEvent = function( e ){ 35 | if ( !e.keyCode ) return null; 36 | 37 | return ({ 38 | 38: 'up' 39 | , 40: 'down' 40 | })[ e.keyCode ] || null; 41 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gplaces", 3 | "version": "1.0.13", 4 | "description": "Dependency-free, google maps auto completion input", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "John Fawcett", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/goodybag/gplaces" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/goodybag/gplaces/issues" 16 | }, 17 | "homepage": "https://github.com/goodybag/gplaces", 18 | "license": "ISC", 19 | "dependencies": {}, 20 | "devDependencies": { 21 | "body-parser": "^1.12.4", 22 | "browserify": "^10.1.3", 23 | "express": "^4.12.3", 24 | "gulp": "^3.8.11", 25 | "gulp-autoprefixer": "^2.2.0", 26 | "gulp-concat": "^2.5.2", 27 | "gulp-jshint": "^1.10.0", 28 | "gulp-less": "^3.0.3", 29 | "gulp-uglify": "^1.2.0", 30 | "gulp-util": "^3.0.4", 31 | "jquery": "^1.11.x", 32 | "jshint-stylish": "^1.0.2", 33 | "superagent": "^1.2.0" 34 | }, 35 | "browser": { 36 | "./lib/server.js": "./lib/noop.js" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /standalone-build.js: -------------------------------------------------------------------------------- 1 | var gplaces = require('./'); 2 | 3 | if ( typeof window.define === 'function' && window.define.amd ){ 4 | window.define( 'gplaces', function(){ return gplaces; } ); 5 | } else { 6 | window.gplaces = gplaces; 7 | } -------------------------------------------------------------------------------- /test/config.js: -------------------------------------------------------------------------------- 1 | var local = require('./config.local'); 2 | 3 | for ( var key in local ){ 4 | module.exports[ key ] = local[ key ]; 5 | } -------------------------------------------------------------------------------- /test/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

Teest!

13 | 14 |
15 |

Default (no variants)

16 | 17 |
18 | 19 |
20 |

Dark

21 | 22 |
23 | 24 |
25 |

Flip

26 | 27 |
28 | 29 |
30 |

Bouncy

31 | 32 |
33 | 34 |
35 |

Dark and Flip

36 | 37 |
38 | 39 |
40 |

Textarea

41 | 42 |
43 | 44 | -------------------------------------------------------------------------------- /test/public/js/app.js: -------------------------------------------------------------------------------- 1 | var gplaces = require('../../../'); 2 | var $ = require('jquery'); 3 | // var request = require('superagent'); 4 | var config = require('../../config'); 5 | 6 | gplaces.http( function( input, callback ){ 7 | // request 8 | // .get('/api/places') 9 | // .query({ input: input }) 10 | // .end( callback ); 11 | $.getJSON( '/api/places?input=' + input ) 12 | .error( callback ) 13 | .success( callback.bind( null, null ) ); 14 | }); -------------------------------------------------------------------------------- /test/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var config = require('./config'); 3 | 4 | var app = module.exports = express(); 5 | 6 | app.use( require('body-parser').json() ); 7 | app.use( require('body-parser').urlencoded({ extended: true }) ); 8 | app.use( express.static( __dirname + '/public' ) ); 9 | 10 | app.options('/api/places', function( req, res ){ 11 | return res.send(204); 12 | }); 13 | 14 | app.get('/api/places' 15 | , require('../').proxy({ 16 | key: config.key 17 | }) 18 | ); --------------------------------------------------------------------------------