├── .eslintrc ├── .gitignore ├── .npmignore ├── .prettierrc ├── LICENSE ├── README.md ├── example ├── .gitignore ├── README.md ├── cypress.json ├── cypress │ ├── fixtures │ │ └── example.json │ ├── integration │ │ └── basicJourney.spec.js │ ├── plugins │ │ └── index.js │ └── support │ │ ├── commands.js │ │ └── index.js ├── package-lock.json ├── package.json ├── public │ └── index.html └── src │ └── index.js ├── package-lock.json ├── package.json ├── react-map-gl-geocoder-screenshot.png └── src └── index.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "node": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:cypress/recommended"], 7 | "parser": "babel-eslint" 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | .idea 25 | .cache 26 | .vscode 27 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | .idea 25 | .cache 26 | .vscode 27 | *.png 28 | /example 29 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "jsxBracketSameLine": true, 3 | "printWidth": 120, 4 | "semi": false, 5 | "singleQuote": true, 6 | "arrowParens": "always" 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Steven Miyakawa "SAM" 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-map-gl-geocoder 2 | 3 | React wrapper for mapbox-gl-geocoder for use with react-map-gl. 4 | 5 | [![NPM](https://img.shields.io/npm/v/react-map-gl-geocoder.svg)](https://www.npmjs.com/package/react-map-gl-geocoder) [![react-map-gl-geocoder](https://img.shields.io/endpoint?url=https://dashboard.cypress.io/badge/simple/etguoj/master&style=flat&logo=cypress)](https://dashboard.cypress.io/projects/etguoj/runs) 6 | 7 | 8 | ## Demos 9 | * Simple Example - https://codesandbox.io/s/l7p179qr6m 10 | * Ignore Map Events Example - https://codesandbox.io/s/react-map-gl-geocoder-using-containerref-to-ignore-events-rewdh 11 | 12 | ## Installation 13 | npm 14 | ``` 15 | $ npm install react-map-gl-geocoder 16 | ``` 17 | 18 | or 19 | 20 | Yarn 21 | ``` 22 | $ yarn add react-map-gl-geocoder 23 | ``` 24 | 25 | ## Styling 26 | Import: 27 | ```js 28 | import 'react-map-gl-geocoder/dist/mapbox-gl-geocoder.css' 29 | ``` 30 | 31 | or 32 | 33 | Link tag in header: 34 | ```html 35 | 36 | ``` 37 | 38 | 39 | ## Props 40 | Only `mapRef` and `mapboxApiAccessToken` are required. 41 | 42 | All non-primitive prop values besides `mapRef` and `containerRef` should be memoized. 43 | 44 | | Name | Type | Default | Description | 45 | |--- | --- | --- | --- | 46 | | mapRef | Object | | Ref for react-map-gl map component. 47 | | containerRef | Object | | This can be used to place the geocoder outside of the map. The `position` prop is ignored if this is passed in. Example: https://codesandbox.io/s/v0m14q5rly 48 | | onViewportChange | Function | () => {} | Is passed updated viewport values after executing a query. 49 | | mapboxApiAccessToken | String | | https://www.mapbox.com/ 50 | | inputValue | String | | Sets the search input value 51 | | origin | String | "https://api.mapbox.com" | Use to set a custom API origin. 52 | | zoom | Number | 16 | On geocoded result what zoom level should the map animate to when a `bbox` isn't found in the response. If a `bbox` is found the map will fit to the `bbox`. 53 | | placeholder | String | "Search" | Override the default placeholder attribute value. 54 | | proximity | Object | | A proximity argument: this is a geographical point given as an object with latitude and longitude properties. Search results closer to this point will be given higher priority. 55 | | trackProximity | Boolean | false | If true, the geocoder proximity will automatically update based on the map view. 56 | | collapsed | Boolean | false | If true, the geocoder control will collapse until hovered or in focus. 57 | | clearAndBlurOnEsc | Boolean | false | If true, the geocoder control will clear it's contents and blur when user presses the escape key. 58 | | clearOnBlur | Boolean | false | If true, the geocoder control will clear its value when the input blurs. 59 | | bbox | Array | | A bounding box argument: this is a bounding box given as an array in the format [minX, minY, maxX, maxY]. Search results will be limited to the bounding box. 60 | | types | String | | A comma seperated list of types that filter results to match those specified. See for available types. 61 | | countries | String | | A comma separated list of country codes to limit results to specified country or countries. 62 | | minLength | Number | 2 | Minimum number of characters to enter before results are shown. 63 | | limit | Number | 5 | Maximum number of results to show. 64 | | language | String | | Specify the language to use for response text and query result weighting. Options are IETF language tags comprised of a mandatory ISO 639-1 language code and optionally one or more IETF subtags for country or script. More than one value can also be specified, separated by commas. 65 | | filter | Function | | A function which accepts a Feature in the [Carmen GeoJSON](https://github.com/mapbox/carmen/blob/master/carmen-geojson.md) format to filter out results from the Geocoding API response before they are included in the suggestions list. Return `true` to keep the item, `false` otherwise. 66 | | localGeocoder | Function | | A function accepting the query string which performs local geocoding to supplement results from the Mapbox Geocoding API. Expected to return an Array of GeoJSON Features in the [Carmen GeoJSON](https://github.com/mapbox/carmen/blob/master/carmen-geojson.md) format. 67 | | localGeocoderOnly | Boolean | false | If true, indicates that the localGeocoder results should be the only ones returned to the user. If false, indicates that the localGeocoder results should be combined with those from the Mapbox API with the localGeocoder results ranked higher. 68 | | reverseGeocode | Boolean | false | Enable reverse geocoding. Defaults to false. Expects coordinates to be lat, lon. 69 | | enableEventLogging | Boolean | true | Allow Mapbox to collect anonymous usage statistics from the plugin. 70 | | marker | Boolean or Object | true | If true, a [Marker](https://docs.mapbox.com/mapbox-gl-js/api/#marker) will be added to the map at the location of the user-selected result using a default set of Marker options. If the value is an object, the marker will be constructed using these options. If false, no marker will be added to the map. 71 | | render | Function | | A function that specifies how the results should be rendered in the dropdown menu. Accepts a single Carmen GeoJSON object as input and return a string. Any html in the returned string will be rendered. Uses mapbox-gl-geocoder's default rendering if no function provided. 72 | | position | String | "top-right" | Position on the map to which the geocoder control will be added. Valid values are `"top-left"`, `"top-right"`, `"bottom-left"`, and `"bottom-right"`. 73 | | onInit | Function | () => {} | Is passed Mapbox geocoder instance as param and is executed after Mapbox geocoder is initialized. 74 | | onClear | Function | () => {} | Executed when the input is cleared. 75 | | onLoading | Function | () => {} | Is passed `{ query }` as a param and is executed when the geocoder is looking up a query. 76 | | onResults | Function | () => {} | Is passed `{ results }` as a param and is executed when the geocoder returns a response. 77 | | onResult | Function | () => {} | Is passed `{ result }` as a param and is executed when the geocoder input is set. 78 | | onError | Function | () => {} | Is passed `{ error }` as a param and is executed when an error occurs with the geocoder. 79 | 80 | 81 | 82 | ## Examples 83 | 84 | ### Simple Example 85 | ```js 86 | import 'mapbox-gl/dist/mapbox-gl.css' 87 | import 'react-map-gl-geocoder/dist/mapbox-gl-geocoder.css' 88 | import React, { useState, useRef, useCallback } from 'react' 89 | import MapGL from 'react-map-gl' 90 | import Geocoder from 'react-map-gl-geocoder' 91 | 92 | // Ways to set Mapbox token: https://uber.github.io/react-map-gl/#/Documentation/getting-started/about-mapbox-tokens 93 | const MAPBOX_TOKEN = 'REPLACE_WITH_YOUR_MAPBOX_TOKEN' 94 | 95 | const Example = () => { 96 | const [viewport, setViewport] = useState({ 97 | latitude: 37.7577, 98 | longitude: -122.4376, 99 | zoom: 8 100 | }); 101 | const mapRef = useRef(); 102 | const handleViewportChange = useCallback( 103 | (newViewport) => setViewport(newViewport), 104 | [] 105 | ); 106 | 107 | // if you are happy with Geocoder default settings, you can just use handleViewportChange directly 108 | const handleGeocoderViewportChange = useCallback( 109 | (newViewport) => { 110 | const geocoderDefaultOverrides = { transitionDuration: 1000 }; 111 | 112 | return handleViewportChange({ 113 | ...newViewport, 114 | ...geocoderDefaultOverrides 115 | }); 116 | }, 117 | [] 118 | ); 119 | 120 | return ( 121 |
122 | 130 | 136 | 137 |
138 | ); 139 | }; 140 | 141 | export default Example 142 | ``` 143 | 144 | ### Ignore Map Events Example 145 | You can use the `containerRef` prop to place the `Geocoder` component outside of the `MapGL` component to avoid propagating the mouse events to the `MapGL` component. You can use CSS to position it over the map as shown in this example. 146 | ```js 147 | import 'mapbox-gl/dist/mapbox-gl.css' 148 | import 'react-map-gl-geocoder/dist/mapbox-gl-geocoder.css' 149 | import React, { useState, useRef, useCallback } from 'react' 150 | import MapGL from 'react-map-gl' 151 | import Geocoder from 'react-map-gl-geocoder' 152 | 153 | // Ways to set Mapbox token: https://uber.github.io/react-map-gl/#/Documentation/getting-started/about-mapbox-tokens 154 | const MAPBOX_TOKEN = 'REPLACE_WITH_YOUR_MAPBOX_TOKEN' 155 | 156 | const Example = () => { 157 | const [viewport, setViewport] = useState({ 158 | latitude: 37.7577, 159 | longitude: -122.4376, 160 | zoom: 8, 161 | }); 162 | const geocoderContainerRef = useRef(); 163 | const mapRef = useRef(); 164 | const handleViewportChange = useCallback( 165 | (newViewport) => setViewport(newViewport), 166 | [] 167 | ); 168 | 169 | return ( 170 |
171 |
175 | 183 | 190 | 191 |
192 | ); 193 | }; 194 | ``` 195 | 196 | ## Sample Screenshot 197 | ![react-map-gl-geocoder example screenshot](react-map-gl-geocoder-screenshot.png) 198 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | /dist 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | 24 | .idea 25 | .cache 26 | .vscode 27 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # react-map-gl-geocoder-example 2 | This app can be used by developers to manual test local changes to `react-map-gl-geocoder` component. 3 | 4 | ## Getting Started 5 | 1. Install dependencies for this app by running `npm install`. 6 | 1. Run `npm start` to start app. 7 | 1. Run `npm run watch` in root of `react-map-gl-geocoder` folder to rebuild the `react-map-gl-geocoder` dependency when changes are made. 8 | -------------------------------------------------------------------------------- /example/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectId": "etguoj" 3 | } 4 | -------------------------------------------------------------------------------- /example/cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } -------------------------------------------------------------------------------- /example/cypress/integration/basicJourney.spec.js: -------------------------------------------------------------------------------- 1 | describe('basic user journey', () => { 2 | it('can make a query, select a location from the suggestion list, and display marker', () => { 3 | cy.server() 4 | cy.visit('http://localhost:3000') 5 | cy.get('input').type('new york') 6 | cy.contains('New York City').click() 7 | cy.get('.mapboxgl-marker').should('exist') 8 | }) 9 | }) 10 | -------------------------------------------------------------------------------- /example/cypress/plugins/index.js: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************************** 3 | // This example plugins/index.js can be used to load plugins 4 | // 5 | // You can change the location of this file or turn off loading 6 | // the plugins file with the 'pluginsFile' configuration option. 7 | // 8 | // You can read more here: 9 | // https://on.cypress.io/plugins-guide 10 | // *********************************************************** 11 | 12 | // This function is called when a project is opened or re-opened (e.g. due to 13 | // the project's config changing) 14 | 15 | /** 16 | * @type {Cypress.PluginConfig} 17 | */ 18 | // eslint-disable-next-line no-unused-vars 19 | module.exports = (on, config) => { 20 | // `on` is used to hook into various events Cypress emits 21 | // `config` is the resolved Cypress config 22 | } 23 | -------------------------------------------------------------------------------- /example/cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /example/cypress/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-map-gl-geocoder-v2-example", 3 | "version": "1.0.0", 4 | "description": "\nExample usage of react-map-gl-geocoder which is a React wrapper for mapbox-gl-geocoder for use with react-map-gl", 5 | "keywords": [], 6 | "main": "src/index.js", 7 | "devDependencies": { 8 | "cypress": "^5.1.0", 9 | "react": "16.13.1", 10 | "react-dom": "16.13.1", 11 | "react-map-gl": "5.2.3", 12 | "react-map-gl-geocoder": "file:../", 13 | "react-scripts": "^3.4.3" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "test": "cypress open" 18 | }, 19 | "browserslist": { 20 | "production": [ 21 | ">0.2%", 22 | "not dead", 23 | "not op_mini all" 24 | ], 25 | "development": [ 26 | "last 1 chrome version", 27 | "last 1 firefox version", 28 | "last 1 safari version" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /example/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 30 | 31 | 32 | 33 | 36 |
37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /example/src/index.js: -------------------------------------------------------------------------------- 1 | import 'mapbox-gl/dist/mapbox-gl.css' 2 | import 'react-map-gl-geocoder/dist/mapbox-gl-geocoder.css' 3 | import React, { useState, useRef, useCallback } from 'react' 4 | import { render } from 'react-dom' 5 | import MapGL from 'react-map-gl' 6 | import Geocoder from 'react-map-gl-geocoder' 7 | 8 | // Please be a decent human and don't abuse my Mapbox API token. 9 | // If you fork this sandbox, replace my API token with your own. 10 | // Ways to set Mapbox token: https://uber.github.io/react-map-gl/#/Documentation/getting-started/about-mapbox-tokens 11 | const MAPBOX_TOKEN = 'pk.eyJ1Ijoic21peWFrYXdhIiwiYSI6ImNqcGM0d3U4bTB6dWwzcW04ZHRsbHl0ZWoifQ.X9cvdajtPbs9JDMG-CMDsA' 12 | 13 | const App = () => { 14 | const [viewport, setViewport] = useState({ 15 | latitude: 37.7577, 16 | longitude: -122.4376, 17 | zoom: 8 18 | }) 19 | const geocoderContainerRef = useRef() 20 | const mapRef = useRef() 21 | const handleViewportChange = useCallback((newViewport) => setViewport(newViewport), []) 22 | 23 | // if you are happy with Geocoder default settings, you can just use handleViewportChange directly 24 | const handleGeocoderViewportChange = useCallback( 25 | (newViewport) => { 26 | const geocoderDefaultOverrides = { transitionDuration: 1000 } 27 | 28 | return handleViewportChange({ 29 | ...newViewport, 30 | ...geocoderDefaultOverrides 31 | }) 32 | }, 33 | [handleViewportChange] 34 | ) 35 | 36 | return ( 37 |
38 |
39 | 46 | 53 | 54 |
55 | ) 56 | } 57 | 58 | render(, document.getElementById('root')) 59 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-map-gl-geocoder", 3 | "version": "2.2.0", 4 | "description": "React wrapper for mapbox-gl-geocoder for use with react-map-gl", 5 | "source": "src/index.js", 6 | "main": "dist/index.js", 7 | "module": "dist/index.m.js", 8 | "unpkg": "dist/index.umd.js", 9 | "scripts": { 10 | "build": "microbundle build && npm run cp:css", 11 | "watch": "microbundle watch && npm run cp:css", 12 | "cp:css": "cp node_modules/@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css dist/", 13 | "format": "prettier --write 'src/**/*.js'", 14 | "prepublishOnly": "npm run build" 15 | }, 16 | "husky": { 17 | "hooks": { 18 | "pre-commit": "lint-staged" 19 | } 20 | }, 21 | "lint-staged": { 22 | "*.js": [ 23 | "npm run format", 24 | "eslint", 25 | "git add" 26 | ] 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "git@github.com:SamSamskies/react-map-gl-geocoder.git" 31 | }, 32 | "keywords": [ 33 | "react", 34 | "mapbox", 35 | "mapbox-gl", 36 | "mapgl", 37 | "react-map-gl", 38 | "geocoder" 39 | ], 40 | "author": "Sam Samskies ", 41 | "license": "MIT", 42 | "bugs": { 43 | "url": "https://github.com/SamSamskies/react-map-gl-geocoder/issues" 44 | }, 45 | "homepage": "https://github.com/SamSamskies/react-map-gl-geocoder", 46 | "dependencies": { 47 | "@mapbox/mapbox-gl-geocoder": "4.7.0", 48 | "prop-types": "^15.7.2", 49 | "viewport-mercator-project": "6.1.1" 50 | }, 51 | "peerDependencies": { 52 | "react-map-gl": ">= 4.0.0" 53 | }, 54 | "devDependencies": { 55 | "babel-eslint": "^10.0.1", 56 | "eslint": "^6.2.2", 57 | "eslint-plugin-cypress": "^2.11.1", 58 | "eslint-plugin-react": "^7.11.1", 59 | "husky": "^1.2.1", 60 | "lint-staged": "^9.2.5", 61 | "microbundle": "^0.12.3", 62 | "prettier": "1.15.3" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /react-map-gl-geocoder-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SamSamskies/react-map-gl-geocoder/2968c74e730c52f2ecece086e08ed9d518fd9bfd/react-map-gl-geocoder-screenshot.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { PureComponent } from 'react' 2 | import PropTypes from 'prop-types' 3 | import MapboxGeocoder from '@mapbox/mapbox-gl-geocoder' 4 | import { FlyToInterpolator } from 'react-map-gl' 5 | import WebMercatorViewport from 'viewport-mercator-project' 6 | import mapboxgl from 'mapbox-gl' 7 | 8 | const VALID_POSITIONS = ['top-left', 'top-right', 'bottom-left', 'bottom-right'] 9 | 10 | class Geocoder extends PureComponent { 11 | geocoder = null 12 | cachedResult = null 13 | 14 | componentDidMount() { 15 | this.initializeGeocoder() 16 | } 17 | 18 | componentWillUnmount() { 19 | this.removeGeocoder() 20 | } 21 | 22 | componentDidUpdate() { 23 | this.removeGeocoder() 24 | this.initializeGeocoder() 25 | } 26 | 27 | initializeGeocoder = () => { 28 | const mapboxMap = this.getMapboxMap() 29 | const containerNode = this.getContainerNode() 30 | const { 31 | mapboxApiAccessToken, 32 | inputValue, 33 | origin, 34 | zoom, 35 | placeholder, 36 | proximity, 37 | trackProximity, 38 | collapsed, 39 | clearAndBlurOnEsc, 40 | clearOnBlur, 41 | bbox, 42 | types, 43 | countries, 44 | minLength, 45 | limit, 46 | language, 47 | filter, 48 | localGeocoder, 49 | localGeocoderOnly, 50 | reverseGeocode, 51 | enableEventLogging, 52 | marker, 53 | render, 54 | getItemValue, 55 | onInit, 56 | position 57 | } = this.props 58 | const options = { 59 | accessToken: mapboxApiAccessToken, 60 | origin, 61 | zoom, 62 | flyTo: false, 63 | placeholder, 64 | proximity, 65 | trackProximity, 66 | collapsed, 67 | clearAndBlurOnEsc, 68 | clearOnBlur, 69 | bbox, 70 | types, 71 | countries, 72 | minLength, 73 | limit, 74 | language, 75 | filter, 76 | localGeocoder, 77 | localGeocoderOnly, 78 | reverseGeocode, 79 | enableEventLogging, 80 | marker, 81 | mapboxgl 82 | } 83 | 84 | if (render && typeof render === 'function') { 85 | options.render = render 86 | } 87 | 88 | if (getItemValue && typeof getItemValue === 'function') { 89 | options.getItemValue = getItemValue 90 | } 91 | 92 | this.geocoder = new MapboxGeocoder(options) 93 | this.subscribeEvents() 94 | 95 | if (containerNode) { 96 | containerNode.appendChild(this.geocoder.onAdd(mapboxMap)) 97 | } else { 98 | mapboxMap.addControl(this.geocoder, VALID_POSITIONS.find((_position) => position === _position)) 99 | } 100 | 101 | if (inputValue !== undefined && inputValue !== null) { 102 | this.geocoder.setInput(inputValue) 103 | } else if (this.cachedResult) { 104 | this.geocoder.setInput(this.cachedResult.place_name) 105 | } 106 | 107 | if (this.cachedResult || (inputValue !== undefined && inputValue !== null)) { 108 | this.showClearIcon() 109 | } 110 | 111 | onInit(this.geocoder) 112 | } 113 | 114 | showClearIcon = () => { 115 | // this is a hack to force clear icon to show if there is text in the input 116 | this.geocoder._clearEl.style.display = 'block' 117 | } 118 | 119 | getMapboxMap = () => { 120 | const { mapRef } = this.props 121 | 122 | return (mapRef && mapRef.current && mapRef.current.getMap()) || null 123 | } 124 | 125 | getContainerNode = () => { 126 | const { containerRef } = this.props 127 | 128 | return (containerRef && containerRef.current) || null 129 | } 130 | 131 | subscribeEvents = () => { 132 | this.geocoder.on('clear', this.handleClear) 133 | this.geocoder.on('loading', this.handleLoading) 134 | this.geocoder.on('results', this.handleResults) 135 | this.geocoder.on('result', this.handleResult) 136 | this.geocoder.on('error', this.handleError) 137 | } 138 | 139 | unsubscribeEvents = () => { 140 | this.geocoder.off('clear', this.handleClear) 141 | this.geocoder.off('loading', this.handleLoading) 142 | this.geocoder.off('results', this.handleResults) 143 | this.geocoder.off('result', this.handleResult) 144 | this.geocoder.off('error', this.handleError) 145 | } 146 | 147 | removeGeocoder = () => { 148 | const mapboxMap = this.getMapboxMap() 149 | 150 | this.unsubscribeEvents() 151 | 152 | if (mapboxMap && mapboxMap.removeControl) { 153 | this.getMapboxMap().removeControl(this.geocoder) 154 | } 155 | 156 | this.geocoder = null 157 | } 158 | 159 | handleClear = () => { 160 | this.cachedResult = null 161 | this.props.onClear() 162 | } 163 | 164 | handleLoading = (event) => { 165 | this.props.onLoading(event) 166 | } 167 | 168 | handleResults = (event) => { 169 | this.props.onResults(event) 170 | } 171 | 172 | handleResult = (event) => { 173 | const { result } = event 174 | const { onViewportChange, onResult } = this.props 175 | const { bbox, center, properties = {} } = result 176 | const { short_code } = properties 177 | const [longitude, latitude] = center 178 | const bboxExceptions = { 179 | fr: { 180 | name: 'France', 181 | bbox: [[-4.59235, 41.380007], [9.560016, 51.148506]] 182 | }, 183 | us: { 184 | name: 'United States', 185 | bbox: [[-171.791111, 18.91619], [-66.96466, 71.357764]] 186 | }, 187 | ru: { 188 | name: 'Russia', 189 | bbox: [[19.66064, 41.151416], [190.10042, 81.2504]] 190 | }, 191 | ca: { 192 | name: 'Canada', 193 | bbox: [[-140.99778, 41.675105], [-52.648099, 83.23324]] 194 | } 195 | } 196 | const { width, height } = this.getMapboxMap() 197 | .getContainer() 198 | .getBoundingClientRect() 199 | let zoom = this.geocoder.options.zoom 200 | const fitBounds = (bounds, viewport) => new WebMercatorViewport(viewport).fitBounds(bounds) 201 | 202 | try { 203 | if (!bboxExceptions[short_code] && bbox) { 204 | zoom = fitBounds([[bbox[0], bbox[1]], [bbox[2], bbox[3]]], { width, height }).zoom 205 | } else if (bboxExceptions[short_code]) { 206 | zoom = fitBounds(bboxExceptions[short_code].bbox, { width, height }).zoom 207 | } 208 | } catch (e) { 209 | console.warn('following result caused an error when trying to zoom to bbox: ', result) // eslint-disable-line 210 | zoom = this.geocoder.options.zoom 211 | } 212 | 213 | onViewportChange({ 214 | longitude, 215 | latitude, 216 | zoom, 217 | transitionInterpolator: new FlyToInterpolator(), 218 | transitionDuration: 3000 219 | }) 220 | onResult(event) 221 | 222 | this.cachedResult = result 223 | this.geocoder._typeahead.selected = null 224 | this.showClearIcon() 225 | } 226 | 227 | handleError = (event) => { 228 | this.props.onError(event) 229 | } 230 | 231 | getGeocoder() { 232 | return this.geocoder 233 | } 234 | 235 | render() { 236 | return null 237 | } 238 | 239 | static propTypes = { 240 | mapRef: PropTypes.object.isRequired, 241 | containerRef: PropTypes.object, 242 | onViewportChange: PropTypes.func, 243 | mapboxApiAccessToken: PropTypes.string.isRequired, 244 | inputValue: PropTypes.string, 245 | origin: PropTypes.string, 246 | zoom: PropTypes.number, 247 | placeholder: PropTypes.string, 248 | proximity: PropTypes.object, 249 | trackProximity: PropTypes.bool, 250 | collapsed: PropTypes.bool, 251 | clearAndBlurOnEsc: PropTypes.bool, 252 | clearOnBlur: PropTypes.bool, 253 | bbox: PropTypes.array, 254 | types: PropTypes.string, 255 | countries: PropTypes.string, 256 | minLength: PropTypes.number, 257 | limit: PropTypes.number, 258 | language: PropTypes.string, 259 | filter: PropTypes.func, 260 | localGeocoder: PropTypes.func, 261 | localGeocoderOnly: PropTypes.bool, 262 | reverseGeocode: PropTypes.bool, 263 | enableEventLogging: PropTypes.bool, 264 | marker: PropTypes.oneOfType([PropTypes.bool, PropTypes.object]), 265 | render: PropTypes.func, 266 | getItemValue: PropTypes.func, 267 | position: PropTypes.oneOf(VALID_POSITIONS), 268 | onInit: PropTypes.func, 269 | onClear: PropTypes.func, 270 | onLoading: PropTypes.func, 271 | onResults: PropTypes.func, 272 | onResult: PropTypes.func, 273 | onError: PropTypes.func 274 | } 275 | 276 | static defaultProps = { 277 | onViewportChange: () => {}, 278 | origin: 'https://api.mapbox.com', 279 | zoom: 16, 280 | placeholder: 'Search', 281 | trackProximity: false, 282 | collapsed: false, 283 | clearAndBlurOnEsc: false, 284 | clearOnBlur: false, 285 | minLength: 2, 286 | limit: 5, 287 | reverseGeocode: false, 288 | enableEventLogging: true, 289 | marker: true, 290 | position: 'top-right', 291 | onInit: () => {}, 292 | onClear: () => {}, 293 | onLoading: () => {}, 294 | onResults: () => {}, 295 | onResult: () => {}, 296 | onError: () => {} 297 | } 298 | } 299 | 300 | export default Geocoder 301 | --------------------------------------------------------------------------------