├── .editorconfig ├── .eslintrc ├── .jshintrc ├── LICENSE.txt ├── README.md ├── example ├── bundle.js ├── example.css ├── index.html └── site.js ├── index.js ├── package.json └── search.js /.editorconfig: -------------------------------------------------------------------------------- 1 | [**.js] 2 | indent_style = space 3 | indent_size = 2 4 | trim_trailing_whitespace = true 5 | [**.jsx] 6 | indent_style = space 7 | indent_size = 2 8 | trim_trailing_whitespace = true 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true 6 | }, 7 | "plugins": [ 8 | "react" 9 | ], 10 | "rules": { 11 | "strict": [0], 12 | "curly": [0], 13 | "react/no-multi-comp": 1, 14 | "no-alert": [0], 15 | "no-unused-vars": [0], 16 | "no-redeclare": [1], 17 | "new-cap": [0], 18 | "quotes": [2, "single"], 19 | "eol-last": [0], 20 | "no-mixed-requires": [0], 21 | "camelcase": [0], 22 | "consistent-return": [0], 23 | "no-underscore-dangle": [0], 24 | "comma-spacing": [0], 25 | "key-spacing": [0], 26 | "valid-jsdoc": [2], 27 | "no-use-before-define": [0] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "evil": true, 3 | "smarttabs": true, 4 | "strict": false, 5 | "globalstrict": false, 6 | "asi": false, 7 | "browser": true, 8 | "noarg": true, 9 | "undef": true, 10 | "eqeqeq": true, 11 | "esnext": true, 12 | "freeze": true, 13 | "quotmark": "single", 14 | "node": true, 15 | "globals": { 16 | "require": true, 17 | "console": true, 18 | "Raven": true, 19 | "module": true, 20 | "confirm": true, 21 | "prompt": true, 22 | "alert": true, 23 | "mapboxgl": true 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 2 | ISC License 3 | 4 | Copyright (c) 2017, Mapbox 5 | 6 | Permission to use, copy, modify, and/or distribute this software for any 7 | purpose with or without fee is hereby granted, provided that the above 8 | copyright notice and this permission notice appear in all copies. 9 | 10 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 11 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 12 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 13 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 14 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 15 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 16 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-geocoder 2 | 3 | ``` 4 | npm install @mapbox/react-geocoder 5 | ``` 6 | 7 | A geocoder component using Mapbox. 8 | 9 | ## api 10 | 11 | An `accessToken` is assumed to be a valid Mapbox accessToken. 12 | 13 | ``` 14 | 32 | ``` 33 | -------------------------------------------------------------------------------- /example/example.css: -------------------------------------------------------------------------------- 1 | .loud { 2 | font-weight:bold; 3 | } 4 | ul.loading { 5 | position: relative; 6 | } 7 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | example 6 | 7 | 8 | 9 | 10 |
11 |
12 |

react-geocoder

13 |
14 |
15 |
16 | source on github 17 |
18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /example/site.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | Geocoder = require('../'); 3 | 4 | var Example = React.createClass({ 5 | getInitialState: function() { 6 | return { value: null }; 7 | }, 8 | onSelect: function(value) { 9 | this.setState({ value: value }); 10 | }, 11 | render: function() { 12 | /* jshint ignore:start */ 13 | return ( 14 |
15 |
16 | {/* Geocoder: 17 | accessToken -- Mapbox developer access token (required) 18 | onSelect -- function called after selecting result (required) 19 | showLoader -- Boolean to attach `.loading` class to results list 20 | */} 21 | 26 |
27 | {this.state.value &&
{JSON.stringify(this.state.value, null, 2)}
} 28 |
29 | ); 30 | /* jshint ignore:end */ 31 | } 32 | }); 33 | 34 | /* jshint ignore:start */ 35 | React.render(, document.getElementById('app')); 36 | /* jshint ignore:end */ 37 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var React = require('react'), 2 | ReactDOM = require('react-dom'), 3 | search = require('./search'); 4 | 5 | /** 6 | * Geocoder component: connects to Mapbox.com Geocoding API 7 | * and provides an autocompleting interface for finding locations. 8 | */ 9 | var Geocoder = React.createClass({ 10 | getDefaultProps() { 11 | return { 12 | endpoint: 'https://api.tiles.mapbox.com', 13 | inputClass: '', 14 | resultClass: '', 15 | resultsClass: '', 16 | resultFocusClass: 'strong', 17 | inputPosition: 'top', 18 | inputPlaceholder: 'Search', 19 | showLoader: false, 20 | source: 'mapbox.places', 21 | proximity: '', 22 | bbox: '', 23 | types: '', 24 | onSuggest: function() {}, 25 | focusOnMount: true 26 | }; 27 | }, 28 | getInitialState() { 29 | return { 30 | results: [], 31 | focus: null, 32 | loading: false, 33 | searchTime: new Date() 34 | }; 35 | }, 36 | propTypes: { 37 | endpoint: React.PropTypes.string, 38 | source: React.PropTypes.string, 39 | inputClass: React.PropTypes.string, 40 | resultClass: React.PropTypes.string, 41 | resultsClass: React.PropTypes.string, 42 | inputPosition: React.PropTypes.string, 43 | inputPlaceholder: React.PropTypes.string, 44 | resultFocusClass: React.PropTypes.string, 45 | onSelect: React.PropTypes.func.isRequired, 46 | onSuggest: React.PropTypes.func, 47 | accessToken: React.PropTypes.string.isRequired, 48 | proximity: React.PropTypes.string, 49 | bbox: React.PropTypes.string, 50 | showLoader: React.PropTypes.bool, 51 | focusOnMount: React.PropTypes.bool, 52 | types: React.PropTypes.string 53 | }, 54 | componentDidMount() { 55 | if (this.props.focusOnMount) ReactDOM.findDOMNode(this.refs.input).focus(); 56 | }, 57 | onInput(e) { 58 | this.setState({loading:true}); 59 | var value = e.target.value; 60 | if (value === '') { 61 | this.setState({ 62 | results: [], 63 | focus: null, 64 | loading:false 65 | }); 66 | } else { 67 | search( 68 | this.props.endpoint, 69 | this.props.source, 70 | this.props.accessToken, 71 | this.props.proximity, 72 | this.props.bbox, 73 | this.props.types, 74 | value, 75 | this.onResult); 76 | } 77 | }, 78 | moveFocus(dir) { 79 | if(this.state.loading) return; 80 | this.setState({ 81 | focus: this.state.focus === null ? 82 | 0 : Math.max(0, 83 | Math.min( 84 | this.state.results.length - 1, 85 | this.state.focus + dir)) 86 | }); 87 | }, 88 | acceptFocus() { 89 | if (this.state.focus !== null) { 90 | this.props.onSelect(this.state.results[this.state.focus]); 91 | } 92 | }, 93 | onKeyDown(e) { 94 | switch (e.which) { 95 | // up 96 | case 38: 97 | e.preventDefault(); 98 | this.moveFocus(-1); 99 | break; 100 | // down 101 | case 40: 102 | this.moveFocus(1); 103 | break; 104 | // accept 105 | case 13: 106 | if( this.state.results.length > 0 && this.state.focus == null) { 107 | this.clickOption(this.state.results[0],0); 108 | } 109 | this.acceptFocus(); 110 | break; 111 | } 112 | }, 113 | onResult(err, res, body, searchTime) { 114 | // searchTime is compared with the last search to set the state 115 | // to ensure that a slow xhr response does not scramble the 116 | // sequence of autocomplete display. 117 | if (!err && body && body.features && this.state.searchTime <= searchTime) { 118 | this.setState({ 119 | searchTime: searchTime, 120 | loading: false, 121 | results: body.features, 122 | focus: null 123 | }); 124 | this.props.onSuggest(this.state.results); 125 | } 126 | }, 127 | clickOption(place, listLocation) { 128 | this.props.onSelect(place); 129 | this.setState({focus:listLocation}); 130 | // focus on the input after click to maintain key traversal 131 | ReactDOM.findDOMNode(this.refs.input).focus(); 132 | return false; 133 | }, 134 | render() { 135 | var input = ; 142 | return ( 143 |
144 | {this.props.inputPosition === 'top' && input} 145 | {this.state.results.length > 0 && ( 146 |
    147 | {this.state.results.map((result, i) => ( 148 |
  • 149 | {result.place_name} 153 |
  • 154 | ))} 155 |
156 | )} 157 | {this.props.inputPosition === 'bottom' && input} 158 |
159 | ); 160 | } 161 | }); 162 | 163 | module.exports = Geocoder; 164 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@mapbox/react-geocoder", 3 | "version": "2.5.0", 4 | "description": "a mapbox geocoder component", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "tape test.js", 8 | "start": "watchify example/site.js -o example/bundle.js & st -nc" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git@github.com:mapbox/react-geocoder.git" 13 | }, 14 | "browserify": { 15 | "transform": [ 16 | "babelify" 17 | ] 18 | }, 19 | "keywords": [ 20 | "react", 21 | "react-component", 22 | "geocoder", 23 | "flux", 24 | "mapbox", 25 | "geo" 26 | ], 27 | "author": "Tom MacWright", 28 | "license": "ISC", 29 | "bugs": { 30 | "url": "https://github.com/mapbox/react-geocoder/issues" 31 | }, 32 | "homepage": "https://github.com/mapbox/react-geocoder", 33 | "devDependencies": { 34 | "browserify": "^6.2.0", 35 | "st": "^0.5.2", 36 | "tape": "^4.6.2", 37 | "watchify": "^2.1.0" 38 | }, 39 | "dependencies": { 40 | "xhr": "^1.17.0", 41 | "babelify": "^6.0.0", 42 | "react": "0.13.x" 43 | } 44 | } -------------------------------------------------------------------------------- /search.js: -------------------------------------------------------------------------------- 1 | var xhr = require('xhr'); 2 | 3 | function search(endpoint, source, accessToken, proximity, bbox, types, query, callback) { 4 | var searchTime = new Date(); 5 | var uri = endpoint + '/geocoding/v5/' + 6 | source + '/' + encodeURIComponent(query) + '.json' + 7 | '?access_token=' + accessToken + 8 | (proximity ? '&proximity=' + proximity : '') + 9 | (bbox ? '&bbox=' + bbox : '') + 10 | (types ? '&types=' + encodeURIComponent(types) : ''); 11 | xhr({ 12 | uri: uri, 13 | json: true 14 | }, function(err, res, body) { 15 | callback(err, res, body, searchTime); 16 | }); 17 | } 18 | 19 | module.exports = search; 20 | --------------------------------------------------------------------------------