├── .gitignore ├── .npmignore ├── .prettierrc.json ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── cjs ├── v1 │ └── index.js └── v2 │ └── index.js ├── css ├── index.css └── index.min.css ├── es └── index.js ├── lib ├── Providers │ ├── BingMap.d.ts │ ├── BingMap.js │ ├── OpenStreetMap.d.ts │ ├── OpenStreetMap.js │ ├── Provider.d.ts │ ├── Provider.js │ ├── index.d.ts │ └── index.js ├── ReactLeafletSearch.d.ts ├── ReactLeafletSearch.js ├── Search-v1.d.ts ├── Search-v1.js ├── Search-v2.d.ts ├── Search-v2.js ├── core │ ├── handler-wrapper.d.ts │ ├── handler-wrapper.js │ ├── search-close-button.d.ts │ ├── search-close-button.js │ ├── search-icon-button.d.ts │ ├── search-icon-button.js │ ├── search-info-list.d.ts │ ├── search-info-list.js │ ├── search-input.d.ts │ └── search-input.js ├── index.d.ts ├── index.js ├── search-control.d.ts └── search-control.js ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── Providers │ ├── BingMap.tsx │ ├── OpenStreetMap.tsx │ ├── Provider.ts │ └── index.tsx ├── ReactLeafletSearch.tsx ├── Search-v1.ts ├── Search-v2.ts ├── core │ ├── handler-wrapper.ts │ ├── search-close-button.tsx │ ├── search-icon-button.tsx │ ├── search-info-list.tsx │ └── search-input.tsx ├── index.ts └── search-control.tsx ├── tsconfig.json └── umd ├── v1 └── index.js └── v2 └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules* 2 | examples/.DS_Store 3 | examples/components/.DS_Store 4 | .DS_Store 5 | node_modules.bak -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | examples 3 | docs 4 | dist 5 | tsconfig.json 6 | rollup.config.json 7 | .prettierrc.json 8 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 150, 3 | "tabWidth": 4, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": false, 7 | "jsxSingleQuote": false, 8 | "trailingComma": "none", 9 | "bracketSpacing": true, 10 | "jsxBracketSameLine": false, 11 | "arrowParens": "always", 12 | "requirePragma": false, 13 | "insertPragma": false, 14 | "proseWrap": "always", 15 | "endOfLine": "lf" 16 | } 17 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Orkun Tümer 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-leaflet-search 2 | 3 | A React component for searching places or global coordinates on leaflet 4 | 5 | Both react-leaflet v1 and v2 are supported. 6 | 7 | With v1 you should import this component from "react-leaflet-search/lib/Search-v1"; 8 | 9 | ```javascript 10 | import Search from "react-leaflet-search/lib/Search-v1"; 11 | ``` 12 | 13 | default import is v2 supported (it uses withLeaflet wrapper internally) 14 | 15 | ```javascript 16 | import Search from "react-leaflet-search"; 17 | ``` 18 | 19 | ## Install 20 | 21 | ```npm 22 | npm install react-leaflet-search 23 | ``` 24 | 25 | css files can be found in "react-leaflet-search/css", there is no need to import when using this package as a module. 26 | 27 | ## Usage 28 | 29 | ```javascript 30 | import Search from "react-leaflet-search"; 31 | ``` 32 | 33 | or 34 | 35 | ```javascript 36 | import ReactLeafletSearch from "react-leaflet-search"; 37 | ``` 38 | 39 | (default import so you can name it what ever you want.) 40 | 41 | This component should be a child to react-leaflet's map component: 42 | 43 | ```javascript 44 | const searchComponent = (props) => ; 45 | ``` 46 | 47 | ### Search providers 48 | 49 | There are 2 search providers, but with scope for adding more. The default provider is OpenStreetMap. If you want to use BingMap as a provider, it can 50 | be done as follows: 51 | 52 | ```javascript 53 | const searchComponent = (props) => ; 54 | ``` 55 | 56 | You can pass in provider-specific options using the providerOptions prop: 57 | 58 | ```javascript 59 | const searchComponent = (props) => ; 60 | ``` 61 | 62 | to create a custom provider just create an Object 63 | 64 | ```typescript 65 | const customProvider = { 66 | search: async (inputValue: string) => { 67 | // do fetch or anything 68 | return { 69 | info: Array<{ 70 | bounds: boundingBox, 71 | latitude: number, 72 | longitude: number, 73 | name: displayName 74 | }> | string, 75 | raw: rawResponse 76 | } 77 | } 78 | } 79 | ``` 80 | 81 | ```javascript 82 | const component = ; 83 | ``` 84 | 85 | ### Search Result Marker 86 | 87 | You can use own marker and Popup: 88 | 89 | if you use this pattern showMarker and showPopup property of ReactLeafletSearch is not used 90 | 91 | ```javascript 92 | 93 | {(info: { latLng: LatLng, info: string | Array, raw: Record }) => ( 94 | 95 | 96 | 97 | )} 98 | 99 | ``` 100 | 101 | To change the marker icon, use the markerIcon prop: 102 | 103 | ```javascript 104 | const myIcon = L.icon({ 105 | iconUrl: "marker-icon.png", 106 | iconRetinaUrl: "marker-icon-2x.png", 107 | shadowUrl: "marker-shadow.png", 108 | iconSize: [25, 41], 109 | iconAnchor: [12, 41], 110 | popupAnchor: [1, -34], 111 | tooltipAnchor: [16, -28], 112 | shadowSize: [41, 41] 113 | }); 114 | 115 | ; 116 | ``` 117 | 118 | To change the Popup displayed by the marker, use the popUp prop: 119 | 120 | ```javascript 121 | myPopup(SearchInfo) { 122 | return( 123 | 124 |
125 |

I am a custom popUp

126 |

latitude and longitude from search component: lat:{SearchInfo.latLng[0]} lng:{SearchInfo.latLng[1]}

127 |

Info from search component: {SearchInfo.info}

128 |

{JSON.stringify(SearchInfo.raw)}

129 |
130 |
131 | ); 132 | } 133 | 134 | 135 | ``` 136 | 137 | ### Other props which can be set on the `ReactLeafletSearch` component 138 | 139 | Other aspects can be customized as well: 140 | 141 | ```javascript 142 | , raw: Record }) => { 144 | // this method triggers when user clicks one of the searched items or presses enter to search with global coordinates 145 | }} 146 | position="topleft" 147 | inputPlaceholder="The default text in the search bar" 148 | search={new LatLng(30, 30)} // Setting this to LatLng instance gives initial search input to the component and map flies to that coordinates, its like search from props not from user 149 | zoom={7} // Default value is 10 150 | showMarker={true} 151 | showPopup={false} 152 | openSearchOnLoad={false} // By default there's a search icon which opens the input when clicked. Setting this to true opens the search by default. 153 | closeResultsOnClick={false} // By default, the search results remain when you click on one, and the map flies to the location of the result. But you might want to save space on your map by closing the results when one is clicked. The results are shown again (without another search) when focus is returned to the search input. 154 | providerOptions={{ searchBounds: [new LatLng(10, 10), new LatLng(30, 30)] }} // The BingMap and OpenStreetMap providers both accept bounding coordinates in [sw,ne] format. Note that in the case of OpenStreetMap, this only weights the results and doesn't exclude things out of bounds. 155 | customProvider={undefined | { search: (searchString) => {} }} // see examples to usage details until docs are ready 156 | /> 157 | ``` 158 | 159 | ### Styling Component 160 | 161 | you can add custom style 162 | 163 | ```javascript 164 | 165 | ``` 166 | 167 | ```css 168 | .custom-style { 169 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); 170 | --icon-width: 26px; 171 | --icon-height: 26px; 172 | --active-height: 40px; 173 | --close-button-max-size: 12px; 174 | --icon-button-max-size: 18px; 175 | --primary-color: #000000; 176 | --secondary-color: rgba(141, 141, 141, 0.639); 177 | --border-color: rgba(0, 0, 0, 0.2); 178 | --border-size: 0px; 179 | --main-background-color: #ffffff; 180 | --background-color-candidate: #5a6673; 181 | --background-color-hover: #5a6673b3; 182 | --background-color-active: #50c3bd; 183 | --svg-stroke-width: 5px; 184 | } 185 | ``` 186 | 187 | ## Info about search input 188 | 189 | It has two modes: 190 | 191 | - Search for latitude,longitude as numbers 192 | - Search for a place with its name, city, country, street etc. 193 | 194 | To search with global coordinates, the search input should start with the ':' character and should respect the following format: 195 | `:{LATITUDE},{LONGITUDE}` 196 | 197 | ### You can play with the demo 198 | 199 | [DEMO](https://codesandbox.io/s/react-leaflet-search-uj4d3) 200 | -------------------------------------------------------------------------------- /css/index.css: -------------------------------------------------------------------------------- 1 | .search-control-wrap { 2 | --icon-width: 26px; 3 | --icon-height: 26px; 4 | --active-height: 40px; 5 | --close-button-max-size: 12px; 6 | --icon-button-max-size: 18px; 7 | --primary-color: #000000; 8 | --secondary-color: rgba(141, 141, 141, 0.639); 9 | --border-color: rgba(0, 0, 0, 0.2); 10 | --border-size: 0px; 11 | --main-background-color: #ffffff; 12 | --background-color-candidate: #5a6673; 13 | --background-color-hover: #5a6673b3; 14 | --background-color-active: #50c3bd; 15 | --svg-stroke-width: 5px; 16 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); 17 | position: relative; 18 | z-index: 401; 19 | color: var(--primary-color); 20 | display: inline-grid; 21 | grid-template-rows: 1fr; 22 | grid-template-columns: 1fr; 23 | border: var(--border-size) solid var(--border-color); 24 | border-radius: 4px; 25 | } 26 | 27 | .search-control-wrap ::placeholder { 28 | color: var(--secondary-color); 29 | opacity: 1; 30 | } 31 | 32 | .search-control { 33 | /* width: 100%; */ 34 | position: relative; 35 | height: 100%; 36 | text-align: center; 37 | font: bold 12px/20px Tahoma, Verdana, sans-serif; 38 | background-color: var(--main-background-color); 39 | box-sizing: border-box; 40 | background-clip: padding-box; 41 | cursor: default; 42 | border-radius: 4px; 43 | display: flex; 44 | z-index: 802; 45 | box-shadow: none !important; 46 | } 47 | 48 | .search-control-icon-button { 49 | position: relative; 50 | background-color: transparent; 51 | padding: unset; 52 | display: flex; 53 | justify-content: center; 54 | align-items: center; 55 | outline: unset; 56 | border-radius: 4px; 57 | border: 0 !important; 58 | height: var(--icon-height) !important; 59 | width: var(--icon-width); 60 | line-height: 30px; 61 | z-index: 0; 62 | cursor: pointer; 63 | transition: height 0.1s linear 0s, border-top-right-radius 0s linear 0.1s, border-bottom-right-radius 0s linear 0.1s; 64 | } 65 | 66 | .search-control-input { 67 | position: relative; 68 | background-color: var(--main-background-color); 69 | z-index: 50; 70 | outline: 0; 71 | padding: 0; 72 | border-top-right-radius: 3px; 73 | border-bottom-right-radius: 3px; 74 | font-size: 14px; 75 | border: 0; 76 | height: var(--icon-height); 77 | color: inherit; 78 | box-sizing: border-box; 79 | width: 0; 80 | transition: width 0.1s linear 0s, height 0.1s linear 0s, padding 0.1s linear 0s; 81 | } 82 | 83 | .search-control-close-button { 84 | display: none; 85 | stroke: #f2f2f2; 86 | transform-origin: center; 87 | transform: scale(1); 88 | outline: unset; 89 | border: unset; 90 | padding: unset; 91 | align-content: center; 92 | align-items: center; 93 | justify-content: center; 94 | justify-items: center; 95 | } 96 | 97 | .search-control-close-button-active { 98 | display: flex; 99 | } 100 | 101 | .search-control-active .search-control-icon-button { 102 | border-top-right-radius: 0 !important; 103 | border-bottom-right-radius: 0 !important; 104 | height: var(--active-height) !important; 105 | transition: height 0.1s linear 0s; 106 | } 107 | 108 | .search-control-active .search-control-input { 109 | padding: 0px 26px 0px 0px; 110 | height: var(--active-height); 111 | width: 244px; 112 | } 113 | 114 | .search-control-active .search-control-close-button { 115 | background-color: transparent; 116 | height: var(--active-height); 117 | width: 26px; 118 | font: normal 18px / calc(var(--active-height) - 2px) "Lucida Console", Monaco, monospace; 119 | right: 0; 120 | color: inherit; 121 | cursor: pointer; 122 | z-index: 51; 123 | position: absolute; 124 | } 125 | .search-control-icon-button svg, 126 | .search-control-active .search-control-close-button > svg { 127 | height: 75%; 128 | width: 75%; 129 | transform-origin: center; 130 | stroke-width: var(--svg-stroke-width); 131 | stroke: var(--primary-color); 132 | } 133 | .search-control-icon-button svg { 134 | max-height: var(--icon-button-max-size); 135 | max-width: var(--icon-button-max-size); 136 | } 137 | .search-control-active .search-control-close-button > svg { 138 | max-height: var(--close-button-max-size); 139 | max-width: var(--close-button-max-size); 140 | } 141 | 142 | /* Select */ 143 | .search-control-info-wrapper { 144 | width: 100%; 145 | height: auto; 146 | position: absolute; 147 | top: 100%; 148 | box-sizing: border-box; 149 | padding: 0px 0 0 0; 150 | margin: 7px 0 0 0; 151 | overflow-y: auto; 152 | z-index: 9999; 153 | box-shadow: 0 1px 5px rgba(0, 0, 0, 0.65); 154 | border-radius: 4px; 155 | } 156 | 157 | .search-control-info-wrapper-close { 158 | display: none; 159 | } 160 | 161 | .search-control-info { 162 | height: auto; 163 | display: flex; 164 | position: relative; 165 | background-color: var(--main-background-color); 166 | padding: 0; 167 | border: var(--border-size) solid var(--border-color); 168 | border-radius: 4px; 169 | text-align: center; 170 | overflow-y: auto; 171 | color: inherit; 172 | } 173 | 174 | .search-control-info-span { 175 | margin: 0 auto; 176 | font-weight: normal; 177 | font-size: 12px; 178 | } 179 | 180 | .search-control-info-list { 181 | margin: 0; 182 | padding: 0; 183 | overflow-y: auto; 184 | width: 100%; 185 | position: relative; 186 | display: flex; 187 | flex-direction: column; 188 | background: transparent; 189 | height: 100%; 190 | outline: none; 191 | } 192 | 193 | .search-control-info-list:focus .search-control-info-list-item.candidate, 194 | .search-control-info-list-item:active, 195 | .search-control-info-list-item:focus { 196 | background: var(--background-color-active) !important; 197 | } 198 | .search-control-info-list:focus .search-control-info-list-item:not(.active).candidate, 199 | .search-control-info-list-item:not(.active):active, 200 | .search-control-info-list-item:not(.active):focus { 201 | background: var(--background-color-candidate) !important; 202 | } 203 | .search-control-info-list-item { 204 | border-bottom: 1px solid var(--border-color); 205 | font: normal 12px/16px Tahoma, Verdana, sans-serif; 206 | display: block; 207 | list-style: none; 208 | cursor: pointer; 209 | padding: 5px; 210 | text-align: center; 211 | /* align-items: center; */ 212 | /* display: flex; */ 213 | color: inherit; 214 | white-space: pre-wrap; 215 | } 216 | 217 | .search-control-info-list-item:last-child, 218 | .search-control-info-list-item:hover:last-child { 219 | border-bottom: none; 220 | } 221 | 222 | .search-control-info-list-item.active, 223 | .search-control-info-list-item.active:hover { 224 | background-color: var(--background-color-active); 225 | } 226 | 227 | .search-control-info-list-item:hover { 228 | background-color: var(--background-color-hover); 229 | } 230 | 231 | .search-control-info-list-item:hover p, 232 | .search-control-info-list-item.active p { 233 | margin: 0; 234 | } 235 | 236 | .search-control-info-list-item p, 237 | .search-control-info-list-item p { 238 | margin: 0; 239 | } 240 | 241 | /* popup */ 242 | .search-control-popup-seperator { 243 | width: 100%; 244 | height: 1px; 245 | background-color: #eee; 246 | } 247 | -------------------------------------------------------------------------------- /css/index.min.css: -------------------------------------------------------------------------------- 1 | .search-control-wrap{--icon-width:26px;--icon-height:26px;--active-height:40px;--close-button-max-size:12px;--icon-button-max-size:18px;--primary-color:#000;--secondary-color:hsla(0,0%,55.3%,0.639);--border-color:rgba(0,0,0,0.2);--border-size:0px;--main-background-color:#fff;--background-color-candidate:#5a6673;--background-color-hover:rgba(90,102,115,0.7);--background-color-active:#50c3bd;--svg-stroke-width:5px;box-shadow:0 1px 5px rgba(0,0,0,.65);position:relative;z-index:401;color:var(--primary-color);display:inline-grid;grid-template-rows:1fr;grid-template-columns:1fr;border:var(--border-size) solid var(--border-color);border-radius:4px}.search-control-wrap ::-webkit-input-placeholder{color:var(--secondary-color);opacity:1}.search-control-wrap ::-moz-placeholder{color:var(--secondary-color);opacity:1}.search-control-wrap :-ms-input-placeholder{color:var(--secondary-color);opacity:1}.search-control-wrap ::-ms-input-placeholder{color:var(--secondary-color);opacity:1}.search-control-wrap ::placeholder{color:var(--secondary-color);opacity:1}.search-control{height:100%;text-align:center;font:700 12px/20px Tahoma,Verdana,sans-serif;background-color:var(--main-background-color);box-sizing:border-box;background-clip:padding-box;cursor:default;border-radius:4px;z-index:802;box-shadow:none!important}.search-control,.search-control-icon-button{position:relative;display:-webkit-box;display:flex}.search-control-icon-button{background-color:transparent;padding:unset;-webkit-box-pack:center;justify-content:center;-webkit-box-align:center;align-items:center;outline:unset;border-radius:4px;border:0!important;height:var(--icon-height)!important;width:var(--icon-width);line-height:30px;z-index:0;cursor:pointer;-webkit-transition:height .1s linear 0s,border-top-right-radius 0s linear .1s,border-bottom-right-radius 0s linear .1s;transition:height .1s linear 0s,border-top-right-radius 0s linear .1s,border-bottom-right-radius 0s linear .1s}.search-control-input{position:relative;background-color:var(--main-background-color);z-index:50;outline:0;padding:0;border-top-right-radius:3px;border-bottom-right-radius:3px;font-size:14px;border:0;height:var(--icon-height);color:inherit;box-sizing:border-box;width:0;-webkit-transition:width .1s linear 0s,height .1s linear 0s,padding .1s linear 0s;transition:width .1s linear 0s,height .1s linear 0s,padding .1s linear 0s}.search-control-close-button{display:none;stroke:#f2f2f2;-webkit-transform-origin:center;transform-origin:center;-webkit-transform:scale(1);transform:scale(1);outline:unset;border:unset;padding:unset;align-content:center;-webkit-box-align:center;align-items:center;-webkit-box-pack:center;justify-content:center;justify-items:center}.search-control-close-button-active{display:-webkit-box;display:flex}.search-control-active .search-control-icon-button{border-top-right-radius:0!important;border-bottom-right-radius:0!important;height:var(--active-height)!important;-webkit-transition:height .1s linear 0s;transition:height .1s linear 0s}.search-control-active .search-control-input{padding:0 26px 0 0;height:var(--active-height);width:244px}.search-control-active .search-control-close-button{background-color:transparent;height:var(--active-height);width:26px;font:normal 18px/calc(var(--active-height) - 2px) Lucida Console,Monaco,monospace;right:0;color:inherit;cursor:pointer;z-index:51;position:absolute}.search-control-active .search-control-close-button>svg,.search-control-icon-button svg{height:75%;width:75%;-webkit-transform-origin:center;transform-origin:center;stroke-width:var(--svg-stroke-width);stroke:var(--primary-color)}.search-control-icon-button svg{max-height:var(--icon-button-max-size);max-width:var(--icon-button-max-size)}.search-control-active .search-control-close-button>svg{max-height:var(--close-button-max-size);max-width:var(--close-button-max-size)}.search-control-info-wrapper{width:100%;height:auto;position:absolute;top:100%;box-sizing:border-box;padding:0;margin:7px 0 0;overflow-y:auto;z-index:9999;box-shadow:0 1px 5px rgba(0,0,0,.65);border-radius:4px}.search-control-info-wrapper-close{display:none}.search-control-info{height:auto;display:-webkit-box;display:flex;position:relative;background-color:var(--main-background-color);padding:0;border:var(--border-size) solid var(--border-color);border-radius:4px;text-align:center;overflow-y:auto;color:inherit}.search-control-info-span{margin:0 auto;font-weight:400;font-size:12px}.search-control-info-list{margin:0;padding:0;overflow-y:auto;width:100%;position:relative;display:-webkit-box;display:flex;-webkit-box-orient:vertical;-webkit-box-direction:normal;flex-direction:column;background:transparent;height:100%;outline:none}.search-control-info-list-item:active,.search-control-info-list-item:focus,.search-control-info-list:focus .search-control-info-list-item.candidate{background:var(--background-color-active)!important}.search-control-info-list-item:not(.active):active,.search-control-info-list-item:not(.active):focus,.search-control-info-list:focus .search-control-info-list-item:not(.active).candidate{background:var(--background-color-candidate)!important}.search-control-info-list-item{border-bottom:1px solid var(--border-color);font:normal 12px/16px Tahoma,Verdana,sans-serif;display:block;list-style:none;cursor:pointer;padding:5px;text-align:center;color:inherit;white-space:pre-wrap}.search-control-info-list-item:hover:last-child,.search-control-info-list-item:last-child{border-bottom:none}.search-control-info-list-item.active,.search-control-info-list-item.active:hover{background-color:var(--background-color-active)}.search-control-info-list-item:hover{background-color:var(--background-color-hover)}.search-control-info-list-item.active p,.search-control-info-list-item:hover p,.search-control-info-list-item p{margin:0}.search-control-popup-seperator{width:100%;height:1px;background-color:#eee} -------------------------------------------------------------------------------- /lib/Providers/BingMap.d.ts: -------------------------------------------------------------------------------- 1 | import { Provider, ProviderOptions } from "./Provider"; 2 | export interface BingMapResponse { 3 | resourceSets: Array<{ 4 | resources: Array<{ 5 | bbox: string[]; 6 | point: { 7 | coordinates: string[]; 8 | }; 9 | name: string; 10 | }>; 11 | estimatedTotal: number; 12 | }>; 13 | } 14 | declare class BingMap implements Provider { 15 | key?: string | null; 16 | url: string; 17 | constructor(options?: ProviderOptions); 18 | search(query: string): Promise<{ 19 | info: string; 20 | raw: BingMapResponse; 21 | } | { 22 | error: string; 23 | }>; 24 | formatResponse(response: BingMapResponse): { 25 | info: string; 26 | raw: BingMapResponse; 27 | } | { 28 | error: string; 29 | }; 30 | } 31 | export { BingMap }; 32 | -------------------------------------------------------------------------------- /lib/Providers/BingMap.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | class BingMap { 11 | constructor(options) { 12 | var _a; 13 | this.key = options === null || options === void 0 ? void 0 : options.providerKey; 14 | //Bounds are expected to be a nested array of [[sw_lat, sw_lng],[ne_lat, ne_lng]]. 15 | // We convert them into a string of 'x1,y1,x2,y2' 16 | let boundsUrlComponent = ""; 17 | if ((_a = options === null || options === void 0 ? void 0 : options.searchBounds) === null || _a === void 0 ? void 0 : _a.length) { 18 | const bounds = options.searchBounds.reduce((acc, b) => [...acc, b.lat, b.lng], []); 19 | boundsUrlComponent = `&umv=${bounds.join(",")}`; 20 | } 21 | this.url = `https://dev.virtualearth.net/REST/v1/Locations?output=json${boundsUrlComponent}&key=${this.key}&q=`; 22 | } 23 | search(query) { 24 | return __awaiter(this, void 0, void 0, function* () { 25 | if (typeof this.key === "undefined") { 26 | return { error: "BingMap requires an api key" }; 27 | } 28 | // console.log(this.url + query) 29 | const response = yield fetch(this.url + query).then((res) => res.json()); 30 | return this.formatResponse(response); 31 | }); 32 | } 33 | formatResponse(response) { 34 | const resources = response.resourceSets[0].resources; 35 | const count = response.resourceSets[0].estimatedTotal; 36 | const info = count > 0 37 | ? resources.map((e) => ({ 38 | bounds: e.bbox.map((bound) => Number(bound)), 39 | latitude: Number(e.point.coordinates[0]), 40 | longitude: Number(e.point.coordinates[1]), 41 | name: e.name 42 | })) 43 | : "Not Found"; 44 | return { 45 | info: info, 46 | raw: response 47 | }; 48 | } 49 | } 50 | export { BingMap }; 51 | -------------------------------------------------------------------------------- /lib/Providers/OpenStreetMap.d.ts: -------------------------------------------------------------------------------- 1 | import { Provider, ProviderOptions } from "./Provider"; 2 | export declare type OpenStreetMapResponse = Array<{ 3 | boundingbox: string[]; 4 | lat: number; 5 | lon: number; 6 | display_name: string; 7 | }>; 8 | declare class OpenStreetMap implements Provider { 9 | url: string; 10 | constructor(options?: ProviderOptions); 11 | search(query: string): Promise<{ 12 | info: string; 13 | raw: OpenStreetMapResponse; 14 | } | { 15 | error: string; 16 | }>; 17 | formatResponse(response: OpenStreetMapResponse): { 18 | info: string; 19 | raw: OpenStreetMapResponse; 20 | } | { 21 | error: string; 22 | }; 23 | } 24 | export { OpenStreetMap }; 25 | -------------------------------------------------------------------------------- /lib/Providers/OpenStreetMap.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | class OpenStreetMap { 11 | constructor(options) { 12 | //Bounds are expected to be a nested array of [[sw_lat, sw_lng],[ne_lat, ne_lng]]. 13 | // We convert them into a string of 'x1,y1,x2,y2' which is the opposite way around from lat/lng - it's lng/lat 14 | let boundsUrlComponent = ""; 15 | let regionUrlComponent = ""; 16 | if (options && options.searchBounds && options.searchBounds.length) { 17 | const reversedBounds = options.searchBounds.reduce((acc, b) => [...acc, b.lng, b.lat], []); 18 | boundsUrlComponent = `&bounded=1&viewbox=${reversedBounds.join(",")}`; 19 | } 20 | if (options && "region" in options) { 21 | regionUrlComponent = `&countrycodes=${options.region}`; 22 | } 23 | this.url = `https://nominatim.openstreetmap.org/search?format=json&addressdetails=1&polygon_svg=1&namedetails=1${boundsUrlComponent}${regionUrlComponent}&q=`; 24 | } 25 | search(query) { 26 | return __awaiter(this, void 0, void 0, function* () { 27 | const rawResponse = yield fetch(this.url + query); 28 | const response = yield rawResponse.json(); 29 | return this.formatResponse(response); 30 | }); 31 | } 32 | formatResponse(response) { 33 | const resources = response; 34 | const count = response.length; 35 | const info = count > 0 36 | ? resources.map((e) => ({ 37 | bounds: e.boundingbox.map((bound) => Number(bound)), 38 | latitude: Number(e.lat), 39 | longitude: Number(e.lon), 40 | name: e.display_name, 41 | })) 42 | : "Not Found"; 43 | return { 44 | info: info, 45 | raw: response, 46 | }; 47 | } 48 | } 49 | export { OpenStreetMap }; 50 | -------------------------------------------------------------------------------- /lib/Providers/Provider.d.ts: -------------------------------------------------------------------------------- 1 | import { LatLng } from "leaflet"; 2 | interface ProviderOptions { 3 | providerKey?: string | null; 4 | searchBounds?: [LatLng, LatLng]; 5 | region?: string; 6 | } 7 | interface Provider { 8 | url: string; 9 | search(query: string): Promise<{ 10 | info: string; 11 | raw: ResponseType; 12 | } | { 13 | error: string; 14 | }>; 15 | formatResponse(response: ResponseType): { 16 | info: string; 17 | raw: ResponseType; 18 | } | { 19 | error: string; 20 | }; 21 | } 22 | export type { Provider, ProviderOptions }; 23 | -------------------------------------------------------------------------------- /lib/Providers/Provider.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /lib/Providers/index.d.ts: -------------------------------------------------------------------------------- 1 | import { OpenStreetMap } from "./OpenStreetMap"; 2 | import { BingMap } from "./BingMap"; 3 | export { OpenStreetMap, BingMap }; 4 | declare const _default: { 5 | [key: string]: any; 6 | }; 7 | export default _default; 8 | -------------------------------------------------------------------------------- /lib/Providers/index.js: -------------------------------------------------------------------------------- 1 | import { OpenStreetMap } from "./OpenStreetMap"; 2 | import { BingMap } from "./BingMap"; 3 | export { OpenStreetMap, BingMap }; 4 | export default { 5 | OpenStreetMap, 6 | BingMap 7 | }; 8 | -------------------------------------------------------------------------------- /lib/ReactLeafletSearch.d.ts: -------------------------------------------------------------------------------- 1 | import { Control, Icon, LatLng, Map, ZoomPanOptions } from "leaflet"; 2 | import React from "react"; 3 | import { MapControl, Marker, LeafletContext, MapControlProps } from "react-leaflet"; 4 | import { SearchControlProps } from "./search-control"; 5 | declare type SearchInfo = { 6 | latLng: LatLng; 7 | info: string | Array; 8 | raw: Record; 9 | }; 10 | export declare type ReactLeafletSearchProps = MapControlProps & SearchControlProps & { 11 | showMarker?: boolean; 12 | showPopup?: boolean; 13 | zoom: number; 14 | mapStateModifier?: "flyTo" | "setView" | ((l: LatLng) => void); 15 | zoomPanOptions?: ZoomPanOptions; 16 | customProvider?: { 17 | search: (value: string) => Promise; 18 | }; 19 | markerIcon?: Icon; 20 | popUp?: (i: { 21 | latLng: LatLng; 22 | info: string | Array; 23 | raw: Object; 24 | }) => JSX.Element; 25 | children?: (info: SearchInfo) => JSX.Element | null; 26 | onChange?: (info: SearchInfo) => void; 27 | }; 28 | interface ReactLeafletSearchState { 29 | search: LatLng | false; 30 | info: any; 31 | } 32 | export default class ReactLeafletSearch extends MapControl { 33 | div: HTMLDivElement; 34 | map?: Map; 35 | SearchInfo: SearchInfo | null; 36 | state: ReactLeafletSearchState; 37 | markerRef: React.RefObject; 38 | constructor(props: ReactLeafletSearchProps, context: LeafletContext); 39 | createLeafletElement(props: ReactLeafletSearchProps): { 40 | onAdd: (map: Map) => HTMLDivElement; 41 | onRemove: (map: Map) => void; 42 | } & Control; 43 | handler: ({ event, payload }: { 44 | event: "add" | "remove"; 45 | payload?: SearchInfo | undefined; 46 | }) => void; 47 | latLngHandler(latLng: LatLng, info: string | Array, raw: Record): void; 48 | goToLatLng(latLng: LatLng, info: JSX.Element): void; 49 | flyTo(): void; 50 | componentDidMount(): void; 51 | componentDidUpdate(): void; 52 | defaultPopUp(): JSX.Element; 53 | render(): JSX.Element | null; 54 | static defaultProps: ReactLeafletSearchProps; 55 | } 56 | export {}; 57 | -------------------------------------------------------------------------------- /lib/ReactLeafletSearch.js: -------------------------------------------------------------------------------- 1 | import { Control, DomUtil, DomEvent } from "leaflet"; 2 | import React from "react"; 3 | // import PropTypes from "prop-types"; 4 | import ReactDOM from "react-dom"; 5 | import { Popup, MapControl, Marker } from "react-leaflet"; 6 | import { SearchControl } from "./search-control"; 7 | export default class ReactLeafletSearch extends MapControl { 8 | constructor(props, context) { 9 | var _a; 10 | super(props); 11 | this.handler = ({ event, payload }) => { 12 | var _a, _b; 13 | if (event === "add" && payload) { 14 | (_b = (_a = this.props).onChange) === null || _b === void 0 ? void 0 : _b.call(_a, payload); 15 | this.latLngHandler(payload.latLng, payload.info, payload.raw); 16 | } 17 | else { 18 | this.setState({ search: false }); 19 | } 20 | }; 21 | this.div = DomUtil.create("div", "leaflet-search-wrap"); 22 | DomEvent.disableClickPropagation(this.div); 23 | DomEvent.disableScrollPropagation(this.div); 24 | this.state = { 25 | search: false, 26 | info: false 27 | }; 28 | this.SearchInfo = null; // searched lat,lng or response from provider 29 | this.map = context.map || ((_a = props.leaflet) === null || _a === void 0 ? void 0 : _a.map); 30 | this.markerRef = React.createRef(); 31 | } 32 | createLeafletElement(props) { 33 | const ReactLeafletSearchControl = Control.extend({ 34 | onAdd: (map) => this.div, 35 | onRemove: (map) => { } 36 | }); 37 | return new ReactLeafletSearchControl(props); 38 | } 39 | latLngHandler(latLng, info, raw) { 40 | this.SearchInfo = { info, latLng, raw }; 41 | const popUpStructure = (React.createElement("div", null, 42 | React.createElement("p", null, Array.isArray(info) ? info.toString() : info), 43 | React.createElement("div", { className: "search-control-popup-seperator" }), 44 | React.createElement("div", null, `latitude: ${latLng.lat}`), 45 | React.createElement("div", null, `longitude: ${latLng.lng}`))); 46 | this.goToLatLng(latLng, popUpStructure); 47 | } 48 | goToLatLng(latLng, info) { 49 | this.setState({ search: latLng, info: info }, () => { 50 | this.flyTo(); 51 | }); 52 | } 53 | flyTo() { 54 | if (this.state.search) { 55 | switch (this.props.mapStateModifier) { 56 | case "flyTo": 57 | this.map && this.map.flyTo(this.state.search, this.props.zoom, this.props.zoomPanOptions); 58 | break; 59 | case "setView": 60 | this.map && this.map.setView(this.state.search, this.props.zoom, this.props.zoomPanOptions); 61 | break; 62 | default: 63 | typeof this.props.mapStateModifier === "function" && this.props.mapStateModifier(this.state.search); 64 | } 65 | } 66 | } 67 | componentDidMount() { 68 | super.componentDidMount && super.componentDidMount(); 69 | ReactDOM.render(React.createElement(SearchControl, Object.assign({ className: this.props.className, provider: this.props.provider, customProvider: this.props.customProvider, providerOptions: this.props.providerOptions, openSearchOnLoad: this.props.openSearchOnLoad, closeResultsOnClick: this.props.closeResultsOnClick, inputPlaceholder: this.props.inputPlaceholder, search: this.props.search, map: this.map, handler: this.handler }, (this.props.tabIndex !== undefined ? { tabIndex: this.props.tabIndex } : {}))), this.div); 70 | } 71 | componentDidUpdate() { 72 | this.markerRef.current && this.markerRef.current.leafletElement.openPopup(); 73 | } 74 | defaultPopUp() { 75 | return (React.createElement(Popup, null, 76 | React.createElement("span", null, this.state.info))); 77 | } 78 | render() { 79 | return this.SearchInfo && this.state.search ? (this.props.children ? (this.props.children(this.SearchInfo)) : this.props.showMarker ? (React.createElement(Marker, Object.assign({ ref: this.markerRef, key: `marker-search-${this.state.search.toString()}`, position: this.state.search }, (this.props.markerIcon ? { icon: this.props.markerIcon } : {})), this.props.showPopup && (this.props.popUp ? this.props.popUp(this.SearchInfo) : this.defaultPopUp()))) : null) : null; 80 | } 81 | } 82 | // static propTypes = { 83 | // position: PropTypes.oneOf(["topleft", "topright", "bottomleft", "bottomright"]).isRequired, 84 | // providerKey: PropTypes.string, 85 | // inputPlaceholder: PropTypes.string, 86 | // showMarker: PropTypes.bool, 87 | // showPopup: PropTypes.bool, 88 | // popUp: PropTypes.func, 89 | // zoom: PropTypes.number, 90 | // search: PropTypes.arrayOf(PropTypes.number), 91 | // closeResultsOnClick: PropTypes.bool, 92 | // openSearchOnLoad: PropTypes.bool, 93 | // searchBounds: PropTypes.array, 94 | // provider: PropTypes.string, 95 | // providerOptions: PropTypes.object, 96 | // zoomPanOptions: PropTypes.object, 97 | // mapStateModifier: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), 98 | // customProvider: PropTypes.oneOfType([PropTypes.object]), 99 | // }; 100 | ReactLeafletSearch.defaultProps = { 101 | inputPlaceholder: "Search Lat,Lng", 102 | showMarker: true, 103 | showPopup: true, 104 | zoom: 10, 105 | closeResultsOnClick: false, 106 | openSearchOnLoad: false, 107 | search: undefined, 108 | provider: "OpenStreetMap", 109 | mapStateModifier: "flyTo", 110 | zoomPanOptions: { 111 | animate: true, 112 | duration: 0.25, 113 | easeLinearity: 0.25, 114 | noMoveStart: false 115 | } 116 | }; 117 | -------------------------------------------------------------------------------- /lib/Search-v1.d.ts: -------------------------------------------------------------------------------- 1 | import ReactLeafletSearch from "./ReactLeafletSearch"; 2 | export default ReactLeafletSearch; 3 | -------------------------------------------------------------------------------- /lib/Search-v1.js: -------------------------------------------------------------------------------- 1 | import ReactLeafletSearch from "./ReactLeafletSearch"; 2 | export default ReactLeafletSearch; 3 | -------------------------------------------------------------------------------- /lib/Search-v2.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare const _default: import("react").ComponentType>; 3 | export default _default; 4 | -------------------------------------------------------------------------------- /lib/Search-v2.js: -------------------------------------------------------------------------------- 1 | import { withLeaflet } from "react-leaflet"; 2 | import ReactLeafletSearch from "./ReactLeafletSearch"; 3 | export default withLeaflet(ReactLeafletSearch); 4 | -------------------------------------------------------------------------------- /lib/core/handler-wrapper.d.ts: -------------------------------------------------------------------------------- 1 | import { BaseSyntheticEvent } from "react"; 2 | declare function asyncInputEvent(asyncHandler: (e: T) => any, syncHandler: (e: T) => any, debounceTime?: number): (e: T) => any; 3 | export { asyncInputEvent }; 4 | -------------------------------------------------------------------------------- /lib/core/handler-wrapper.js: -------------------------------------------------------------------------------- 1 | function asyncInputEvent(asyncHandler, syncHandler, debounceTime = 400) { 2 | let t; 3 | return (e) => { 4 | e.persist(); 5 | syncHandler && syncHandler(e); 6 | clearTimeout(t); 7 | t = window.setTimeout(() => { 8 | asyncHandler(e); 9 | }, debounceTime); 10 | }; 11 | } 12 | export { asyncInputEvent }; 13 | -------------------------------------------------------------------------------- /lib/core/search-close-button.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | interface SearchCloseButtonProps { 3 | className?: string; 4 | onClick?: (e: React.SyntheticEvent) => any; 5 | } 6 | declare function SearchCloseButton({ className, onClick }: SearchCloseButtonProps): JSX.Element; 7 | export { SearchCloseButton }; 8 | -------------------------------------------------------------------------------- /lib/core/search-close-button.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | function SearchCloseButton({ className = "", onClick = (e) => { 3 | e.preventDefault(); 4 | e.stopPropagation(); 5 | } }) { 6 | return (React.createElement("button", { className: `search-control-close-button${className ? ` ${className}` : ""}`, onClick: onClick }, 7 | React.createElement("svg", { viewBox: "0 0 50 50" }, 8 | React.createElement("path", { d: "M5 5 L45 45 M45 5 L5 45" }), 9 | "Sorry, your browser does not support inline SVG."))); 10 | } 11 | export { SearchCloseButton }; 12 | -------------------------------------------------------------------------------- /lib/core/search-icon-button.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | interface SearchIconButtonProps { 3 | className?: string; 4 | onClick?: (e: React.SyntheticEvent) => any; 5 | onMouseEnter?: (e: React.MouseEvent) => any; 6 | onMouseLeave?: (e: React.MouseEvent) => any; 7 | } 8 | declare function SearchIconButton({ className, onClick, onMouseEnter, onMouseLeave }: SearchIconButtonProps): JSX.Element; 9 | export { SearchIconButton }; 10 | -------------------------------------------------------------------------------- /lib/core/search-icon-button.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | function SearchIconButton({ className = "", onClick = () => { }, onMouseEnter = () => { }, onMouseLeave = () => { } }) { 3 | return (React.createElement("button", { className: `${className ? className : ""}`, onClick: onClick, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave }, 4 | React.createElement("svg", { viewBox: "0 0 50 50" }, 5 | React.createElement("line", { x1: "35", y1: "35", x2: "46", y2: "46" }), 6 | React.createElement("circle", { cx: "23", cy: "23", r: "16", fill: "none" }), 7 | "Sorry, your browser does not support inline SVG."))); 8 | } 9 | export { SearchIconButton }; 10 | -------------------------------------------------------------------------------- /lib/core/search-info-list.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | declare type Item = { 3 | latitude: number; 4 | longitude: number; 5 | name: string; 6 | checked?: boolean; 7 | }; 8 | declare const SearchInfoList: React.ForwardRefExoticComponent<{ 9 | list: string | Array; 10 | handler: (item: Item, list: Array, index: number) => void; 11 | tabIndex?: number | undefined; 12 | activeIndex?: number | undefined; 13 | } & React.RefAttributes>; 14 | export { SearchInfoList }; 15 | -------------------------------------------------------------------------------- /lib/core/search-info-list.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | const SearchInfoListItem = ({ value, className, candidate, onClick, onKeyDown, children, }) => { 3 | const r = React.useRef(null); 4 | React.useEffect(() => { 5 | if (value === candidate && r.current && r.current.offsetParent) { 6 | r.current.scrollIntoView({ behavior: "smooth", block: "nearest" }); 7 | } 8 | }, [candidate, value]); 9 | return (React.createElement("li", { ref: r, value: value, className: className, onClick: onClick, onKeyDown: onKeyDown }, children)); 10 | }; 11 | const SearchInfoListCore = (props, ref) => { 12 | const { handler, list, tabIndex, activeIndex } = props; 13 | const [cand, setCand] = React.useState(0); 14 | const arrowKeyHandler = React.useCallback((e) => { 15 | if (Array.isArray(list)) { 16 | e.stopPropagation(); 17 | e.keyCode !== 9 && e.preventDefault(); // don't prevent tab 18 | const length = list.length; 19 | // Enter 13, Spacebar 32 20 | if ((!(length <= cand || cand < 0) && e.keyCode === 13) || e.keyCode === 32) { 21 | handler(list[cand], list, cand); 22 | } 23 | else { 24 | const c = length <= cand || cand < 0 ? 0 : cand; 25 | // ArrowUp 38 26 | if (e.keyCode === 38) { 27 | setCand(c === 0 ? list.length - 1 : c - 1); 28 | } 29 | // ArrowDown 40 30 | else if (e.keyCode === 40) { 31 | setCand(c + 1 === list.length ? 0 : c + 1); 32 | } 33 | } 34 | } 35 | }, [setCand, cand, list, handler]); 36 | React.useLayoutEffect(() => setCand(0), [list]); 37 | return Array.isArray(list) ? (React.createElement("ul", Object.assign({ ref: ref }, (tabIndex !== undefined ? { tabIndex: props.tabIndex } : {}), { className: "search-control-info-list", onKeyDown: arrowKeyHandler }), list.map((item, i) => (React.createElement(SearchInfoListItem, { value: i, candidate: cand, key: `${item.name}-${i}`, className: `search-control-info-list-item${i === activeIndex || item.checked ? " active" : ""}${cand === i ? " candidate" : ""}`, onClick: () => { 38 | setCand(i); 39 | handler(item, list, i); 40 | }, onKeyDown: arrowKeyHandler }, item.name))))) : (React.createElement("span", { className: "search-control-info-span" }, list)); 41 | }; 42 | SearchInfoListCore.displayName = "SearchInfoList"; 43 | const SearchInfoList = React.forwardRef(SearchInfoListCore); 44 | export { SearchInfoList }; 45 | -------------------------------------------------------------------------------- /lib/core/search-input.d.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | interface SearchInputProps { 3 | placeholder?: string; 4 | initialValue?: string; 5 | type?: string; 6 | className?: string; 7 | debounceTime?: number; 8 | tabIndex?: number; 9 | getInputValueSetter?: (fn: (v: string) => any) => any; 10 | onClick?: (e: React.MouseEvent) => any; 11 | onDoubleClick?: (e: React.MouseEvent) => any; 12 | onMouseDown?: (e: React.MouseEvent) => any; 13 | onMouseEnter?: (e: React.MouseEvent) => any; 14 | onMouseLeave?: (e: React.MouseEvent) => any; 15 | onChange?: (e: React.FormEvent) => any; 16 | onChangeAsync?: (e: React.FormEvent) => any; 17 | onFocus?: (e: React.FocusEvent) => any; 18 | onBlur?: (e: React.FocusEvent) => any; 19 | onKeyUp?: (e: React.KeyboardEvent) => any; 20 | onKeyDown?: (e: React.KeyboardEvent) => any; 21 | onKeyPress?: (e: React.KeyboardEvent) => any; 22 | onSubmit?: (e: React.FormEvent) => any; 23 | } 24 | declare const SearchInput: React.ForwardRefExoticComponent>; 25 | declare function SearchInputWrapper(): JSX.Element; 26 | export { SearchInputWrapper, SearchInput }; 27 | -------------------------------------------------------------------------------- /lib/core/search-input.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { asyncInputEvent } from "./handler-wrapper"; 3 | const SearchInputCore = ({ placeholder = "PlaceHolder", type = "text", initialValue = "", className = "", debounceTime = 400, getInputValueSetter = () => { }, onClick = () => { }, onDoubleClick = () => { }, onMouseDown = () => { }, onMouseEnter = () => { }, onMouseLeave = () => { }, onChange = () => { }, onChangeAsync = () => { }, onFocus = () => { }, onBlur = () => { }, onKeyUp = () => { }, onKeyDown = () => { }, onKeyPress = () => { }, onSubmit = () => { }, tabIndex = 0 }, ref) => { 4 | const [value, setValue] = React.useState(initialValue); 5 | const handlerDefaults = React.useCallback((e, cb) => { 6 | // e.preventDefault(); 7 | // e.stopPropagation(); 8 | cb(e); 9 | }, []); 10 | const inputHandlers = React.useCallback(asyncInputEvent((e) => { 11 | e.preventDefault(); 12 | e.stopPropagation(); 13 | onChangeAsync(e); 14 | }, (e) => { 15 | e.preventDefault(); 16 | e.stopPropagation(); 17 | setValue(e.target.value); 18 | onChange(e); 19 | }, debounceTime), [setValue]); 20 | React.useLayoutEffect(() => { 21 | getInputValueSetter(setValue); 22 | }, [setValue, getInputValueSetter]); 23 | return (React.createElement("input", { tabIndex: tabIndex, ref: ref, type: type, name: "SearchInput", value: value, placeholder: placeholder, className: `search-input${className ? ` ${className}` : ""}`, onClick: (e) => handlerDefaults(e, onClick), onDoubleClick: (e) => handlerDefaults(e, onDoubleClick), onMouseEnter: (e) => handlerDefaults(e, onMouseEnter), onMouseLeave: (e) => handlerDefaults(e, onMouseLeave), onMouseDown: (e) => handlerDefaults(e, onMouseDown), onChange: inputHandlers, onFocus: (e) => handlerDefaults(e, onFocus), onBlur: (e) => handlerDefaults(e, onBlur), onKeyUp: (e) => handlerDefaults(e, onKeyUp), onKeyDown: (e) => handlerDefaults(e, onKeyDown), onKeyPress: (e) => handlerDefaults(e, onKeyPress), onSubmit: (e) => handlerDefaults(e, onSubmit) })); 24 | }; 25 | const SearchInput = React.forwardRef(SearchInputCore); 26 | function SearchInputWrapper() { 27 | const [state /*setState*/] = React.useState("hebele"); 28 | const inputValueSetter = React.useRef(() => { }); 29 | setTimeout(() => { 30 | // setState('deneme'); 31 | inputValueSetter.current("deneme"); 32 | }, 5000); 33 | return (React.createElement(SearchInput, { initialValue: state, getInputValueSetter: (fn) => (inputValueSetter.current = fn), onClick: (e) => { 34 | // console.log("[CLICK]", "input clicked"); 35 | }, onDoubleClick: (e) => { 36 | // console.log("[DOUBLECLICK]", "input double clicked"); 37 | } })); 38 | } 39 | export { SearchInputWrapper, SearchInput }; 40 | -------------------------------------------------------------------------------- /lib/index.d.ts: -------------------------------------------------------------------------------- 1 | import Providers from "./Providers"; 2 | import ReactLeafletSearch from "./Search-v1"; 3 | import { SearchControl } from "./search-control"; 4 | import Search from "./Search-v2"; 5 | export default Search; 6 | export { ReactLeafletSearch, SearchControl, Providers }; 7 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import Providers from "./Providers"; 2 | import ReactLeafletSearch from "./Search-v1"; 3 | import { SearchControl } from "./search-control"; 4 | import Search from "./Search-v2"; 5 | export default Search; // withLeaflet HOC 6 | export { ReactLeafletSearch, SearchControl, Providers }; 7 | -------------------------------------------------------------------------------- /lib/search-control.d.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { BingMap, OpenStreetMap } from "./Providers"; 3 | import PropTypes from "prop-types"; 4 | import { Map as LeafletMap, LatLng } from "leaflet"; 5 | import "../css/index.css"; 6 | export interface SearchControlProps { 7 | provider?: string; 8 | customProvider?: { 9 | search: (value: string) => Promise; 10 | }; 11 | providerOptions?: { 12 | providerKey?: string | null; 13 | searchBounds?: [LatLng, LatLng]; 14 | region?: string; 15 | }; 16 | search?: LatLng; 17 | openSearchOnLoad?: boolean; 18 | handler?: (obj: { 19 | event: "add" | "remove"; 20 | payload?: { 21 | latLng: LatLng; 22 | info: string; 23 | raw: any; 24 | }; 25 | }) => any; 26 | closeResultsOnClick?: boolean; 27 | inputPlaceholder?: string; 28 | map?: LeafletMap; 29 | className?: string; 30 | tabIndex?: number; 31 | } 32 | interface SearchControlState { 33 | open: boolean | undefined; 34 | closeButton: boolean; 35 | showInfo: boolean; 36 | } 37 | declare type ItemData = { 38 | latitude: number; 39 | longitude: number; 40 | name: string; 41 | }; 42 | declare class SearchControl extends React.Component { 43 | input: React.RefObject; 44 | div: React.RefObject; 45 | provider: OpenStreetMap | BingMap | { 46 | search: (value: string) => Promise; 47 | }; 48 | responseCache: { 49 | [key: string]: any; 50 | }; 51 | SearchResponseInfo: JSX.Element | string | null; 52 | lastInfo: JSX.Element | string | null; 53 | lock: boolean; 54 | inputEventHandler: Function; 55 | inputValueSetter: Function; 56 | selectbox: React.RefObject; 57 | constructor(props: SearchControlProps); 58 | static propTypes: { 59 | provider: PropTypes.Requireable; 60 | providerKey: PropTypes.Requireable; 61 | inputPlaceholder: PropTypes.Requireable; 62 | coords: PropTypes.Requireable<(number | null | undefined)[]>; 63 | closeResultsOnClick: PropTypes.Requireable; 64 | openSearchOnLoad: PropTypes.Requireable; 65 | searchBounds: PropTypes.Requireable; 66 | providerOptions: PropTypes.Requireable; 67 | }; 68 | static defaultProps: SearchControlProps; 69 | setLock: (value: boolean) => void; 70 | openSearch: () => void; 71 | closeSearch: () => void; 72 | searchIconButtonOnClick: (e: React.SyntheticEvent) => void; 73 | inputBlur: (e: React.SyntheticEvent) => void; 74 | inputClick: (e: React.SyntheticEvent) => void; 75 | inputKeyUp: (e: React.KeyboardEvent) => void; 76 | closeClick: (e: React.SyntheticEvent) => void; 77 | sendToAction: (e: React.SyntheticEvent) => Promise; 78 | syncInput: () => void; 79 | beautifyValue(value: string): void; 80 | hideInfo(): void; 81 | showInfo(info: string | Array, activeIndex?: number): void; 82 | listItemClick: (itemData: ItemData, totalInfo: Array, activeIndex: number) => void; 83 | setMaxHeight: () => void; 84 | componentDidMount(): void; 85 | componentDidUpdate(): void; 86 | render(): JSX.Element; 87 | } 88 | export { SearchControl }; 89 | -------------------------------------------------------------------------------- /lib/search-control.js: -------------------------------------------------------------------------------- 1 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 2 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 3 | return new (P || (P = Promise))(function (resolve, reject) { 4 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 5 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 6 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 7 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 8 | }); 9 | }; 10 | import * as React from "react"; 11 | import Providers from "./Providers"; 12 | import PropTypes from "prop-types"; 13 | import { LatLng } from "leaflet"; 14 | import "../css/index.css"; 15 | import { SearchInput } from "./core/search-input"; 16 | import { SearchCloseButton } from "./core/search-close-button"; 17 | import { SearchIconButton } from "./core/search-icon-button"; 18 | import { SearchInfoList } from "./core/search-info-list"; 19 | class SearchControl extends React.Component { 20 | constructor(props) { 21 | super(props); 22 | this.setLock = (value) => { 23 | this.lock = value; 24 | }; 25 | this.openSearch = () => { 26 | this.setState({ open: true }, () => { 27 | var _a; 28 | (_a = this.input.current) === null || _a === void 0 ? void 0 : _a.focus(); 29 | }); 30 | }; 31 | this.closeSearch = () => { 32 | this.setState({ open: this.props.openSearchOnLoad, closeButton: false, showInfo: false }, () => { 33 | this.inputValueSetter(""); 34 | this.SearchResponseInfo = ""; 35 | this.props.handler && this.props.handler({ event: "remove" }); 36 | }); 37 | }; 38 | this.searchIconButtonOnClick = (e) => { 39 | e.preventDefault(); 40 | e.stopPropagation(); 41 | this.state.open ? this.closeSearch() : this.openSearch(); 42 | }; 43 | this.inputBlur = (e) => { 44 | var _a; 45 | ((_a = this.input.current) === null || _a === void 0 ? void 0 : _a.value) === "" && !this.lock && this.closeSearch(); 46 | }; 47 | this.inputClick = (e) => { 48 | var _a, _b, _c; 49 | (_a = this.input.current) === null || _a === void 0 ? void 0 : _a.focus(); 50 | if (!((_b = this.input.current) === null || _b === void 0 ? void 0 : _b.value.startsWith(":")) && 51 | this.lastInfo !== null && 52 | this.lastInfo !== "" && 53 | ((_c = this.input.current) === null || _c === void 0 ? void 0 : _c.value) !== "") { 54 | this.SearchResponseInfo = this.lastInfo; 55 | this.lastInfo = null; 56 | this.setState({ showInfo: true }); 57 | } 58 | }; 59 | this.inputKeyUp = (e) => { 60 | e.keyCode === 13 && this.beautifyValue(this.input.current.value); 61 | }; 62 | this.closeClick = (e) => { 63 | this.closeSearch(); 64 | }; 65 | this.sendToAction = (e) => __awaiter(this, void 0, void 0, function* () { 66 | if (!this.input.current.value.startsWith(":")) { 67 | if (Object.prototype.hasOwnProperty.call(this.responseCache, this.input.current.value)) { 68 | this.showInfo(this.responseCache[this.input.current.value].info); 69 | } 70 | else { 71 | if (this.input.current.value.length >= 3) { 72 | this.showInfo("Searching..."); 73 | const searchValue = this.input.current.value; 74 | const response = yield this.provider.search(searchValue); 75 | if (response.error) { 76 | return false; 77 | } 78 | this.responseCache[searchValue] = response; 79 | this.showInfo(response.info); 80 | } 81 | } 82 | } 83 | }); 84 | this.syncInput = () => { 85 | var _a, _b; 86 | !this.state.closeButton && this.setState({ closeButton: true }); 87 | if (((_a = this.input.current) === null || _a === void 0 ? void 0 : _a.value) === "") { 88 | this.hideInfo(); 89 | this.state.closeButton && this.setState({ closeButton: false }); 90 | } 91 | if (!((_b = this.input.current) === null || _b === void 0 ? void 0 : _b.value.startsWith(":"))) { 92 | } 93 | }; 94 | this.listItemClick = (itemData, totalInfo, activeIndex) => { 95 | this.showInfo(totalInfo, activeIndex); 96 | this.props.handler && 97 | this.props.handler({ 98 | event: "add", 99 | payload: { 100 | latLng: new LatLng(Number(itemData.latitude), Number(itemData.longitude)), 101 | info: itemData.name, 102 | raw: this.responseCache[this.input.current.value].raw, 103 | }, 104 | }); 105 | if (this.props.closeResultsOnClick) { 106 | this.hideInfo(); 107 | } 108 | }; 109 | this.setMaxHeight = () => { 110 | const containerRect = this.props.map 111 | ? this.props.map.getContainer().getBoundingClientRect() 112 | : document.body.getBoundingClientRect(); 113 | const divRect = this.input.current.getBoundingClientRect(); 114 | const maxHeight = `${Math.floor((containerRect.bottom - divRect.bottom - 10) * 0.6)}px`; 115 | this.selectbox.current && this.selectbox.current.style && (this.selectbox.current.style.maxHeight = maxHeight); 116 | }; 117 | this.state = { 118 | open: this.props.openSearchOnLoad, 119 | closeButton: false, 120 | showInfo: false, 121 | }; 122 | this.SearchResponseInfo = ""; 123 | this.responseCache = {}; 124 | this.lastInfo = null; 125 | this.inputValueSetter = () => { }; 126 | this.selectbox = React.createRef(); 127 | this.div = React.createRef(); 128 | this.input = React.createRef(); 129 | // use custom provider if exists any 130 | if (this.props.customProvider) { 131 | this.provider = this.props.customProvider; 132 | } 133 | else if (this.props.provider && Object.keys(Providers).includes(this.props.provider)) { 134 | const Provider = Providers[this.props.provider]; 135 | this.provider = new Provider(this.props.providerOptions); 136 | } 137 | else { 138 | throw new Error(`You set the provider prop to ${this.props.provider} but that isn't recognised. You can choose from ${Object.keys(Providers).join(", ")}`); 139 | } 140 | } 141 | beautifyValue(value) { 142 | if (value.startsWith(":")) { 143 | const latLng = value 144 | .slice(1) 145 | .split(",") 146 | .filter((e) => !isNaN(Number(e))) 147 | .map((e) => Number(e ? e : 0)); 148 | if (latLng.length <= 1) { 149 | this.showInfo("Please enter a valid lat, lng"); 150 | } 151 | else { 152 | this.hideInfo(); 153 | this.props.handler && 154 | this.props.handler({ 155 | event: "add", 156 | payload: { 157 | latLng: new LatLng(Number(latLng[0]), Number(latLng[1])), 158 | info: latLng.join(","), 159 | raw: latLng.join(","), 160 | }, 161 | }); 162 | } 163 | } 164 | else { 165 | if (this.input.current.value.length < 3) { 166 | const response = 'Please enter a valid lat,lng starting with ":" or minimum 3 character to search'; 167 | this.showInfo(response); 168 | } 169 | } 170 | } 171 | hideInfo() { 172 | this.lastInfo = this.SearchResponseInfo; 173 | this.SearchResponseInfo = ""; 174 | this.setState({ showInfo: false }); 175 | } 176 | showInfo(info, activeIndex) { 177 | var _a; 178 | // key changes when info changes so candidate number starts from zero 179 | this.SearchResponseInfo = (React.createElement(SearchInfoList, { ref: this.selectbox, activeIndex: activeIndex, list: info, handler: this.listItemClick, tabIndex: this.props.tabIndex !== undefined ? this.props.tabIndex + 1 : 2 })); 180 | ((_a = this.input.current) === null || _a === void 0 ? void 0 : _a.value) && this.setState({ showInfo: true }); 181 | } 182 | componentDidMount() { 183 | this.setMaxHeight(); 184 | if (this.props.search && !isNaN(Number(this.props.search.lat)) && !isNaN(Number(this.props.search.lng))) { 185 | const inputValue = `:${this.props.search.lat},${this.props.search.lng}`; 186 | this.inputValueSetter(inputValue); 187 | this.openSearch(); 188 | this.syncInput(); // to show close button 189 | this.props.handler && 190 | this.props.handler({ 191 | event: "add", 192 | payload: { 193 | latLng: new LatLng(Number(this.props.search.lat), Number(this.props.search.lng)), 194 | info: inputValue, 195 | raw: this.props.search, 196 | }, 197 | }); 198 | } 199 | } 200 | componentDidUpdate() { 201 | this.setMaxHeight(); 202 | if (this.state.showInfo) { 203 | // this.selectbox.current && this.selectbox.current.focus(); 204 | } 205 | } 206 | render() { 207 | return (React.createElement("article", { className: `${this.props.className ? `${this.props.className} ` : ""}search-control-wrap` }, 208 | React.createElement("section", { className: `search-control${this.state.open ? " search-control-active" : ""}` }, 209 | React.createElement(SearchIconButton, { className: "search-control-icon-button", onClick: this.searchIconButtonOnClick, onMouseEnter: () => this.setLock(true), onMouseLeave: () => this.setLock(false) }), 210 | React.createElement(SearchInput, { tabIndex: this.props.tabIndex !== undefined ? this.props.tabIndex : 1, ref: this.input, getInputValueSetter: (fn) => (this.inputValueSetter = fn), className: "search-control-input", placeholder: this.props.inputPlaceholder, onClick: this.inputClick, onMouseEnter: () => this.setLock(true), onMouseLeave: () => this.setLock(false), onChange: this.syncInput, onChangeAsync: this.sendToAction, onBlur: this.inputBlur, onKeyUp: this.inputKeyUp, onKeyPress: (e) => { 211 | e.stopPropagation(); 212 | e.keyCode === 40 && e.preventDefault(); 213 | }, onKeyDown: (e) => { 214 | var _a; 215 | // ArrowDown 40 216 | if (e.keyCode === 40) { 217 | e.preventDefault(); 218 | e.stopPropagation(); 219 | (_a = this.selectbox.current) === null || _a === void 0 ? void 0 : _a.focus(); 220 | } 221 | // ArrowUp 38 222 | }, onSubmit: (e) => e.preventDefault() }), 223 | React.createElement(SearchCloseButton, { className: this.state.closeButton ? " search-control-close-button-active" : "", onClick: this.closeClick })), 224 | React.createElement("section", { className: `search-control-info-wrapper${this.state.showInfo ? "" : " search-control-info-wrapper-close"}` }, 225 | React.createElement("section", { ref: this.div, className: `search-control-info` }, this.state.showInfo && this.SearchResponseInfo)))); 226 | } 227 | } 228 | SearchControl.propTypes = { 229 | provider: PropTypes.string, 230 | providerKey: PropTypes.string, 231 | inputPlaceholder: PropTypes.string, 232 | coords: PropTypes.arrayOf(PropTypes.number), 233 | closeResultsOnClick: PropTypes.bool, 234 | openSearchOnLoad: PropTypes.bool, 235 | searchBounds: PropTypes.array, 236 | providerOptions: PropTypes.object, 237 | }; 238 | SearchControl.defaultProps = { 239 | inputPlaceholder: "Search Lat,Lng", 240 | closeResultsOnClick: false, 241 | openSearchOnLoad: false, 242 | search: undefined, 243 | provider: "OpenStreetMap", 244 | }; 245 | export { SearchControl }; 246 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-leaflet-search", 3 | "version": "2.0.1", 4 | "description": "React component for search lat lng on leaflet", 5 | "scripts": { 6 | "compile": "tsc -p .", 7 | "build": "rollup -c && tsc -p .", 8 | "npm:publish": "npm publish" 9 | }, 10 | "keywords": [ 11 | "react", 12 | "leaflet", 13 | "search" 14 | ], 15 | "author": "Orkun Tumer", 16 | "funding": { 17 | "type": "patreon", 18 | "url": "https://www.patreon.com/tumerorkun" 19 | }, 20 | "license": "MIT", 21 | "main": "cjs/v2/index.js", 22 | "module": "lib/index.js", 23 | "types": "lib/", 24 | "repository": { 25 | "type": "git", 26 | "url": "https://github.com/tumerorkun/react-leaflet-search.git" 27 | }, 28 | "peerDependencies": { 29 | "leaflet": "^1.6.0", 30 | "prop-types": "^15.7.2", 31 | "react": "^16.12.0", 32 | "react-leaflet": "^2.6.0" 33 | }, 34 | "devDependencies": { 35 | "@types/leaflet": "^1.5.6", 36 | "@types/react": "^16.9.16", 37 | "@types/react-dom": "^16.9.4", 38 | "@types/react-leaflet": "^2.5.0", 39 | "autoprefixer": "^9.7.3", 40 | "cssnano": "^4.1.10", 41 | "leaflet": "^1.6.0", 42 | "postcss": "^7.0.24", 43 | "prop-types": "^15.7.2", 44 | "react": "^16.12.0", 45 | "react-dom": "^16.12.0", 46 | "react-leaflet": "^2.6.0", 47 | "rollup": "^1.27.12", 48 | "rollup-plugin-clear": "^2.0.7", 49 | "rollup-plugin-commonjs": "^10.1.0", 50 | "rollup-plugin-css-only": "^1.0.0", 51 | "rollup-plugin-node-resolve": "^5.2.0", 52 | "rollup-plugin-sass": "^1.2.2", 53 | "rollup-plugin-terser": "^5.1.3", 54 | "rollup-plugin-typescript2": "^0.25.3", 55 | "typescript": "^4.0.2" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import fs from "fs"; 2 | import path from "path"; 3 | import typescript from "rollup-plugin-typescript2"; 4 | import resolve from "rollup-plugin-node-resolve"; 5 | import commonjs from "rollup-plugin-commonjs"; 6 | import clear from "rollup-plugin-clear"; 7 | import pkg from "./package.json"; 8 | import { terser } from "rollup-plugin-terser"; 9 | // import sass from "rollup-plugin-sass"; 10 | import css from "rollup-plugin-css-only"; 11 | import autoprefixer from "autoprefixer"; 12 | import cssnano from "cssnano"; 13 | import postcss from "postcss"; 14 | 15 | const external = [...Object.keys(pkg.peerDependencies || {})]; 16 | const commonPlugins = [ 17 | typescript({ 18 | // typescript: require("typescript"), 19 | useTsconfigDeclarationDir: true, 20 | }), 21 | resolve(), 22 | commonjs(), 23 | css({ 24 | // Write all styles to the bundle destination where .js is replaced by .css 25 | output: (css) => 26 | postcss([autoprefixer, cssnano]) 27 | .process(css, { from: undefined }) 28 | .then((result) => { 29 | if (!fs.existsSync(path.resolve(process.cwd(), "css"))) { 30 | fs.mkdirSync(path.resolve(process.cwd(), "css")); 31 | } 32 | fs.writeFileSync(path.resolve(process.cwd(), "css/index.min.css"), result.css); 33 | }) 34 | // Processor will be called with two arguments: 35 | // - style: the compiled css 36 | // - id: import id 37 | // processor: (css) => 38 | // postcss([autoprefixer, cssnano]) 39 | // .process(css, { from: undefined }) 40 | // .then((result) => fs.writeFileSync('css/index.min.css',result.css)) 41 | }), 42 | terser() // minifies generated bundles 43 | ]; 44 | const plugins = [ 45 | clear({ 46 | // required, point out which directories should be clear. 47 | targets: ["es", "umd", "cjs", "lib", "types"], 48 | // optional, whether clear the directores when rollup recompile on --watch mode. 49 | watch: true // default: false 50 | }), 51 | ...commonPlugins 52 | ]; 53 | 54 | const globals = { 55 | leaflet: "L", 56 | react: "React", 57 | "prop-types": "PropTypes", 58 | "react-dom": "ReactDOM", 59 | "react-leaflet": "ReactLeaflet" 60 | }; 61 | 62 | export default [ 63 | { 64 | input: "src/index.ts", 65 | output: [ 66 | { 67 | globals, 68 | file: "es/index.js", 69 | format: "es" 70 | } 71 | ], 72 | external, 73 | plugins 74 | }, 75 | { 76 | input: "src/Search-v1.ts", 77 | output: [ 78 | { 79 | globals, 80 | file: "cjs/v1/index.js", 81 | format: "cjs" 82 | } 83 | ], 84 | external, 85 | plugins: commonPlugins 86 | }, 87 | { 88 | input: "src/Search-v2.ts", 89 | output: [ 90 | { 91 | globals, 92 | file: "cjs/v2/index.js", 93 | format: "cjs" 94 | } 95 | ], 96 | external, 97 | plugins: commonPlugins 98 | }, 99 | { 100 | input: "src/Search-v1.ts", 101 | output: [ 102 | { 103 | globals, 104 | name: "ReactLeafletSearch", 105 | file: "umd/v1/index.js", 106 | format: "umd" 107 | } 108 | ], 109 | external, 110 | plugins: commonPlugins 111 | }, 112 | { 113 | input: "src/Search-v2.ts", 114 | output: [ 115 | { 116 | globals, 117 | name: "ReactLeafletSearch", 118 | file: "umd/v2/index.js", 119 | format: "umd" 120 | } 121 | ], 122 | external, 123 | plugins: commonPlugins 124 | } 125 | ]; 126 | -------------------------------------------------------------------------------- /src/Providers/BingMap.tsx: -------------------------------------------------------------------------------- 1 | import { Provider, ProviderOptions } from "./Provider"; 2 | 3 | export interface BingMapResponse { 4 | resourceSets: Array<{ 5 | resources: Array<{ bbox: string[]; point: { coordinates: string[] }; name: string }>; 6 | estimatedTotal: number; 7 | }>; 8 | } 9 | class BingMap implements Provider { 10 | key?: string | null; 11 | url: string; 12 | 13 | constructor(options?: ProviderOptions) { 14 | this.key = options?.providerKey; 15 | //Bounds are expected to be a nested array of [[sw_lat, sw_lng],[ne_lat, ne_lng]]. 16 | // We convert them into a string of 'x1,y1,x2,y2' 17 | let boundsUrlComponent = ""; 18 | if (options?.searchBounds?.length) { 19 | const bounds = options.searchBounds.reduce((acc: number[], b) => [...acc, b.lat, b.lng], []); 20 | boundsUrlComponent = `&umv=${bounds.join(",")}`; 21 | } 22 | this.url = `https://dev.virtualearth.net/REST/v1/Locations?output=json${boundsUrlComponent}&key=${this.key}&q=`; 23 | } 24 | 25 | async search(query: string): Promise<{ info: string; raw: BingMapResponse } | { error: string }> { 26 | if (typeof this.key === "undefined") { 27 | return { error: "BingMap requires an api key" }; 28 | } 29 | // console.log(this.url + query) 30 | const response = await fetch(this.url + query).then((res) => res.json()); 31 | return this.formatResponse(response); 32 | } 33 | 34 | formatResponse(response: BingMapResponse): { info: string; raw: BingMapResponse } | { error: string } { 35 | const resources = response.resourceSets[0].resources; 36 | const count = response.resourceSets[0].estimatedTotal; 37 | const info = 38 | count > 0 39 | ? resources.map((e) => ({ 40 | bounds: e.bbox.map((bound) => Number(bound)), 41 | latitude: Number(e.point.coordinates[0]), 42 | longitude: Number(e.point.coordinates[1]), 43 | name: e.name 44 | })) 45 | : "Not Found"; 46 | return { 47 | info: info as string, 48 | raw: response as BingMapResponse 49 | }; 50 | } 51 | } 52 | 53 | export { BingMap }; 54 | -------------------------------------------------------------------------------- /src/Providers/OpenStreetMap.tsx: -------------------------------------------------------------------------------- 1 | import { Provider, ProviderOptions } from "./Provider"; 2 | 3 | export type OpenStreetMapResponse = Array<{ boundingbox: string[]; lat: number; lon: number; display_name: string }>; 4 | 5 | class OpenStreetMap implements Provider { 6 | url: string; 7 | 8 | constructor(options?: ProviderOptions) { 9 | //Bounds are expected to be a nested array of [[sw_lat, sw_lng],[ne_lat, ne_lng]]. 10 | // We convert them into a string of 'x1,y1,x2,y2' which is the opposite way around from lat/lng - it's lng/lat 11 | let boundsUrlComponent = ""; 12 | let regionUrlComponent = ""; 13 | if (options && options.searchBounds && options.searchBounds.length) { 14 | const reversedBounds = options.searchBounds.reduce((acc: number[], b) => [...acc, b.lng, b.lat], []); 15 | boundsUrlComponent = `&bounded=1&viewbox=${reversedBounds.join(",")}`; 16 | } 17 | if (options && "region" in options) { 18 | regionUrlComponent = `&countrycodes=${options.region}`; 19 | } 20 | this.url = `https://nominatim.openstreetmap.org/search?format=json&addressdetails=1&polygon_svg=1&namedetails=1${boundsUrlComponent}${regionUrlComponent}&q=`; 21 | } 22 | 23 | async search(query: string): Promise<{ info: string; raw: OpenStreetMapResponse } | { error: string }> { 24 | const rawResponse = await fetch(this.url + query); 25 | const response = await rawResponse.json(); 26 | return this.formatResponse(response); 27 | } 28 | 29 | formatResponse(response: OpenStreetMapResponse): { info: string; raw: OpenStreetMapResponse } | { error: string } { 30 | const resources = response; 31 | const count = response.length; 32 | const info = 33 | count > 0 34 | ? resources.map((e) => ({ 35 | bounds: e.boundingbox.map((bound) => Number(bound)), 36 | latitude: Number(e.lat), 37 | longitude: Number(e.lon), 38 | name: e.display_name, 39 | })) 40 | : "Not Found"; 41 | return { 42 | info: info as string, 43 | raw: response as OpenStreetMapResponse, 44 | }; 45 | } 46 | } 47 | 48 | export { OpenStreetMap }; 49 | -------------------------------------------------------------------------------- /src/Providers/Provider.ts: -------------------------------------------------------------------------------- 1 | import { LatLng } from "leaflet"; 2 | 3 | interface ProviderOptions { 4 | providerKey?: string | null; 5 | searchBounds?: [LatLng, LatLng]; 6 | region?: string; 7 | } 8 | interface Provider { 9 | // bounds: string; 10 | url: string; 11 | search(query: string): Promise<{ info: string; raw: ResponseType } | { error: string }>; 12 | formatResponse(response: ResponseType): { info: string; raw: ResponseType } | { error: string }; 13 | } 14 | 15 | export type { Provider, ProviderOptions }; 16 | -------------------------------------------------------------------------------- /src/Providers/index.tsx: -------------------------------------------------------------------------------- 1 | import { OpenStreetMap } from "./OpenStreetMap"; 2 | import { BingMap } from "./BingMap"; 3 | 4 | export { OpenStreetMap, BingMap }; 5 | 6 | export default { 7 | OpenStreetMap, 8 | BingMap 9 | } as { [key: string]: any }; 10 | -------------------------------------------------------------------------------- /src/ReactLeafletSearch.tsx: -------------------------------------------------------------------------------- 1 | import { Control, DomUtil, DomEvent, Icon, LatLng, Map, ZoomPanOptions } from "leaflet"; 2 | import React from "react"; 3 | // import PropTypes from "prop-types"; 4 | import ReactDOM from "react-dom"; 5 | import { Popup, MapControl, Marker, LeafletContext, MapControlProps } from "react-leaflet"; 6 | import { SearchControl, SearchControlProps } from "./search-control"; 7 | 8 | type SearchInfo = { 9 | latLng: LatLng; 10 | info: string | Array; 11 | raw: Record; 12 | }; 13 | export type ReactLeafletSearchProps = MapControlProps & 14 | SearchControlProps & { 15 | showMarker?: boolean; 16 | showPopup?: boolean; 17 | zoom: number; 18 | mapStateModifier?: "flyTo" | "setView" | ((l: LatLng) => void); 19 | zoomPanOptions?: ZoomPanOptions; 20 | customProvider?: { search: (value: string) => Promise }; 21 | markerIcon?: Icon; 22 | popUp?: (i: { latLng: LatLng; info: string | Array; raw: Object }) => JSX.Element; 23 | children?: (info: SearchInfo) => JSX.Element | null; 24 | onChange?: (info: SearchInfo) => void; 25 | }; 26 | 27 | interface ReactLeafletSearchState { 28 | search: LatLng | false; 29 | info: any; 30 | } 31 | 32 | export default class ReactLeafletSearch extends MapControl { 33 | div: HTMLDivElement; 34 | map?: Map; 35 | SearchInfo: SearchInfo | null; 36 | state: ReactLeafletSearchState; 37 | markerRef: React.RefObject; 38 | constructor(props: ReactLeafletSearchProps, context: LeafletContext) { 39 | super(props); 40 | this.div = DomUtil.create("div", "leaflet-search-wrap") as HTMLDivElement; 41 | DomEvent.disableClickPropagation(this.div); 42 | DomEvent.disableScrollPropagation(this.div); 43 | this.state = { 44 | search: false, 45 | info: false 46 | }; 47 | this.SearchInfo = null; // searched lat,lng or response from provider 48 | this.map = context.map || props.leaflet?.map; 49 | this.markerRef = React.createRef(); 50 | } 51 | 52 | createLeafletElement(props: ReactLeafletSearchProps) { 53 | const ReactLeafletSearchControl = Control.extend({ 54 | onAdd: (map: Map) => this.div, 55 | onRemove: (map: Map) => {} 56 | }); 57 | return new ReactLeafletSearchControl(props); 58 | } 59 | 60 | handler = ({ event, payload }: { event: "add" | "remove"; payload?: SearchInfo }) => { 61 | if (event === "add" && payload) { 62 | this.props.onChange?.(payload); 63 | this.latLngHandler(payload.latLng, payload.info, payload.raw); 64 | } else { 65 | this.setState({ search: false }); 66 | } 67 | }; 68 | 69 | latLngHandler(latLng: LatLng, info: string | Array, raw: Record) { 70 | this.SearchInfo = { info, latLng, raw }; 71 | const popUpStructure = ( 72 |
73 |

{Array.isArray(info) ? info.toString() : info}

74 |
75 |
{`latitude: ${latLng.lat}`}
76 |
{`longitude: ${latLng.lng}`}
77 |
78 | ); 79 | this.goToLatLng(latLng, popUpStructure); 80 | } 81 | 82 | goToLatLng(latLng: LatLng, info: JSX.Element) { 83 | this.setState({ search: latLng, info: info }, () => { 84 | this.flyTo(); 85 | }); 86 | } 87 | flyTo() { 88 | if (this.state.search) { 89 | switch (this.props.mapStateModifier) { 90 | case "flyTo": 91 | this.map && this.map.flyTo(this.state.search, this.props.zoom, this.props.zoomPanOptions); 92 | break; 93 | case "setView": 94 | this.map && this.map.setView(this.state.search, this.props.zoom, this.props.zoomPanOptions); 95 | break; 96 | default: 97 | typeof this.props.mapStateModifier === "function" && this.props.mapStateModifier(this.state.search); 98 | } 99 | } 100 | } 101 | 102 | componentDidMount() { 103 | super.componentDidMount && super.componentDidMount(); 104 | ReactDOM.render( 105 | , 118 | this.div 119 | ); 120 | } 121 | 122 | componentDidUpdate() { 123 | this.markerRef.current && this.markerRef.current.leafletElement.openPopup(); 124 | } 125 | 126 | defaultPopUp() { 127 | return ( 128 | 129 | {this.state.info} 130 | 131 | ); 132 | } 133 | 134 | render() { 135 | return this.SearchInfo && this.state.search ? ( 136 | this.props.children ? ( 137 | this.props.children(this.SearchInfo) 138 | ) : this.props.showMarker ? ( 139 | 145 | {this.props.showPopup && (this.props.popUp ? this.props.popUp(this.SearchInfo) : this.defaultPopUp())} 146 | 147 | ) : null 148 | ) : null; 149 | } 150 | 151 | // static propTypes = { 152 | // position: PropTypes.oneOf(["topleft", "topright", "bottomleft", "bottomright"]).isRequired, 153 | // providerKey: PropTypes.string, 154 | // inputPlaceholder: PropTypes.string, 155 | // showMarker: PropTypes.bool, 156 | // showPopup: PropTypes.bool, 157 | // popUp: PropTypes.func, 158 | // zoom: PropTypes.number, 159 | // search: PropTypes.arrayOf(PropTypes.number), 160 | // closeResultsOnClick: PropTypes.bool, 161 | // openSearchOnLoad: PropTypes.bool, 162 | // searchBounds: PropTypes.array, 163 | // provider: PropTypes.string, 164 | // providerOptions: PropTypes.object, 165 | // zoomPanOptions: PropTypes.object, 166 | // mapStateModifier: PropTypes.oneOfType([PropTypes.func, PropTypes.string]), 167 | // customProvider: PropTypes.oneOfType([PropTypes.object]), 168 | // }; 169 | static defaultProps: ReactLeafletSearchProps = { 170 | inputPlaceholder: "Search Lat,Lng", 171 | showMarker: true, 172 | showPopup: true, 173 | zoom: 10, 174 | closeResultsOnClick: false, 175 | openSearchOnLoad: false, 176 | search: undefined, 177 | provider: "OpenStreetMap", 178 | mapStateModifier: "flyTo", 179 | zoomPanOptions: { 180 | animate: true, 181 | duration: 0.25, 182 | easeLinearity: 0.25, 183 | noMoveStart: false 184 | } 185 | }; 186 | } 187 | -------------------------------------------------------------------------------- /src/Search-v1.ts: -------------------------------------------------------------------------------- 1 | import ReactLeafletSearch from "./ReactLeafletSearch"; 2 | export default ReactLeafletSearch; 3 | -------------------------------------------------------------------------------- /src/Search-v2.ts: -------------------------------------------------------------------------------- 1 | import { withLeaflet } from "react-leaflet"; 2 | import ReactLeafletSearch from "./ReactLeafletSearch"; 3 | export default withLeaflet(ReactLeafletSearch); 4 | -------------------------------------------------------------------------------- /src/core/handler-wrapper.ts: -------------------------------------------------------------------------------- 1 | import { BaseSyntheticEvent } from "react"; 2 | 3 | function asyncInputEvent( 4 | asyncHandler: (e: T) => any, 5 | syncHandler: (e: T) => any, 6 | debounceTime: number = 400 7 | ): (e: T) => any { 8 | let t: number; 9 | return (e: T) => { 10 | e.persist(); 11 | syncHandler && syncHandler(e); 12 | clearTimeout(t); 13 | t = window.setTimeout(() => { 14 | asyncHandler(e); 15 | }, debounceTime); 16 | }; 17 | } 18 | 19 | export { asyncInputEvent }; 20 | -------------------------------------------------------------------------------- /src/core/search-close-button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface SearchCloseButtonProps { 4 | className?: string; 5 | onClick?: (e: React.SyntheticEvent) => any; 6 | } 7 | 8 | function SearchCloseButton({ 9 | className = "", 10 | onClick = (e: React.SyntheticEvent) => { 11 | e.preventDefault(); 12 | e.stopPropagation(); 13 | } 14 | }: SearchCloseButtonProps) { 15 | return ( 16 | 22 | ); 23 | } 24 | 25 | export { SearchCloseButton }; 26 | -------------------------------------------------------------------------------- /src/core/search-icon-button.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface SearchIconButtonProps { 4 | className?: string; 5 | onClick?: (e: React.SyntheticEvent) => any; 6 | onMouseEnter?: (e: React.MouseEvent) => any; 7 | onMouseLeave?: (e: React.MouseEvent) => any; 8 | } 9 | 10 | function SearchIconButton({ className = "", onClick = () => {}, onMouseEnter = () => {}, onMouseLeave = () => {} }: SearchIconButtonProps) { 11 | return ( 12 | 24 | ); 25 | } 26 | 27 | export { SearchIconButton }; 28 | -------------------------------------------------------------------------------- /src/core/search-info-list.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | type Item = { latitude: number; longitude: number; name: string; checked?: boolean }; 4 | 5 | const SearchInfoListItem = ({ 6 | value, 7 | className, 8 | candidate, 9 | onClick, 10 | onKeyDown, 11 | children, 12 | }: { 13 | value: number; 14 | className: string; 15 | candidate: number; 16 | onClick: (e: React.MouseEvent) => any; 17 | onKeyDown: (e: React.KeyboardEvent) => any; 18 | children: React.ReactNode; 19 | }) => { 20 | const r: React.RefObject = React.useRef(null); 21 | React.useEffect(() => { 22 | if (value === candidate && r.current && r.current.offsetParent) { 23 | r.current.scrollIntoView({ behavior: "smooth", block: "nearest" }); 24 | } 25 | }, [candidate, value]); 26 | return ( 27 |
  • 28 | {children} 29 |
  • 30 | ); 31 | }; 32 | 33 | const SearchInfoListCore = ( 34 | props: { 35 | list: string | Array; 36 | handler: (item: Item, list: Array, index: number) => void; 37 | tabIndex?: number; 38 | activeIndex?: number; 39 | }, 40 | ref: React.Ref, 41 | ) => { 42 | const { handler, list, tabIndex, activeIndex } = props; 43 | const [cand, setCand] = React.useState(0); 44 | const arrowKeyHandler = React.useCallback<(e: React.KeyboardEvent) => void>( 45 | (e: React.KeyboardEvent) => { 46 | if (Array.isArray(list)) { 47 | e.stopPropagation(); 48 | e.keyCode !== 9 && e.preventDefault(); // don't prevent tab 49 | const length = list.length; 50 | 51 | // Enter 13, Spacebar 32 52 | if ((!(length <= cand || cand < 0) && e.keyCode === 13) || e.keyCode === 32) { 53 | handler(list[cand], list, cand); 54 | } else { 55 | const c = length <= cand || cand < 0 ? 0 : cand; 56 | // ArrowUp 38 57 | if (e.keyCode === 38) { 58 | setCand(c === 0 ? list.length - 1 : c - 1); 59 | } 60 | // ArrowDown 40 61 | else if (e.keyCode === 40) { 62 | setCand(c + 1 === list.length ? 0 : c + 1); 63 | } 64 | } 65 | } 66 | }, 67 | [setCand, cand, list, handler], 68 | ); 69 | 70 | React.useLayoutEffect(() => setCand(0), [list]); 71 | 72 | return Array.isArray(list) ? ( 73 |
      79 | {list.map((item, i) => ( 80 | { 88 | setCand(i); 89 | handler(item, list, i); 90 | }} 91 | onKeyDown={arrowKeyHandler} 92 | > 93 | {item.name} 94 | 95 | ))} 96 |
    97 | ) : ( 98 | {list} 99 | ); 100 | }; 101 | 102 | SearchInfoListCore.displayName = "SearchInfoList"; 103 | 104 | const SearchInfoList = React.forwardRef(SearchInfoListCore); 105 | 106 | export { SearchInfoList }; 107 | -------------------------------------------------------------------------------- /src/core/search-input.tsx: -------------------------------------------------------------------------------- 1 | import React, { BaseSyntheticEvent } from "react"; 2 | import { asyncInputEvent } from "./handler-wrapper"; 3 | 4 | interface SearchInputProps { 5 | placeholder?: string; 6 | initialValue?: string; 7 | type?: string; 8 | className?: string; 9 | debounceTime?: number; 10 | tabIndex?: number; 11 | getInputValueSetter?: (fn: (v: string) => any) => any; 12 | onClick?: (e: React.MouseEvent) => any; 13 | onDoubleClick?: (e: React.MouseEvent) => any; 14 | onMouseDown?: (e: React.MouseEvent) => any; 15 | onMouseEnter?: (e: React.MouseEvent) => any; 16 | onMouseLeave?: (e: React.MouseEvent) => any; 17 | onChange?: (e: React.FormEvent) => any; 18 | onChangeAsync?: (e: React.FormEvent) => any; 19 | onFocus?: (e: React.FocusEvent) => any; 20 | onBlur?: (e: React.FocusEvent) => any; 21 | onKeyUp?: (e: React.KeyboardEvent) => any; 22 | onKeyDown?: (e: React.KeyboardEvent) => any; 23 | onKeyPress?: (e: React.KeyboardEvent) => any; 24 | onSubmit?: (e: React.FormEvent) => any; 25 | } 26 | 27 | const SearchInputCore = ( 28 | { 29 | placeholder = "PlaceHolder", 30 | type = "text", 31 | initialValue = "", 32 | className = "", 33 | debounceTime = 400, 34 | getInputValueSetter = () => {}, 35 | onClick = () => {}, 36 | onDoubleClick = () => {}, 37 | onMouseDown = () => {}, 38 | onMouseEnter = () => {}, 39 | onMouseLeave = () => {}, 40 | onChange = () => {}, 41 | onChangeAsync = () => {}, 42 | onFocus = () => {}, 43 | onBlur = () => {}, 44 | onKeyUp = () => {}, 45 | onKeyDown = () => {}, 46 | onKeyPress = () => {}, 47 | onSubmit = () => {}, 48 | tabIndex = 0 49 | }: SearchInputProps, 50 | ref: React.Ref 51 | ) => { 52 | const [value, setValue] = React.useState(initialValue); 53 | const handlerDefaults = React.useCallback((e, cb) => { 54 | // e.preventDefault(); 55 | // e.stopPropagation(); 56 | cb(e); 57 | }, []); 58 | const inputHandlers = React.useCallback( 59 | asyncInputEvent>( 60 | (e) => { 61 | e.preventDefault(); 62 | e.stopPropagation(); 63 | onChangeAsync(e); 64 | }, 65 | (e) => { 66 | e.preventDefault(); 67 | e.stopPropagation(); 68 | setValue(e.target.value); 69 | onChange(e); 70 | }, 71 | debounceTime 72 | ), 73 | [setValue] 74 | ); 75 | React.useLayoutEffect(() => { 76 | getInputValueSetter(setValue); 77 | }, [setValue, getInputValueSetter]); 78 | return ( 79 | handlerDefaults(e, onClick)} 88 | onDoubleClick={(e) => handlerDefaults(e, onDoubleClick)} 89 | onMouseEnter={(e) => handlerDefaults(e, onMouseEnter)} 90 | onMouseLeave={(e) => handlerDefaults(e, onMouseLeave)} 91 | onMouseDown={(e) => handlerDefaults(e, onMouseDown)} 92 | onChange={inputHandlers} // works exactly as onInput because of reacts implementation 93 | onFocus={(e) => handlerDefaults(e, onFocus)} 94 | onBlur={(e) => handlerDefaults(e, onBlur)} 95 | onKeyUp={(e) => handlerDefaults(e, onKeyUp)} 96 | onKeyDown={(e) => handlerDefaults(e, onKeyDown)} 97 | onKeyPress={(e) => handlerDefaults(e, onKeyPress)} 98 | onSubmit={(e) => handlerDefaults(e, onSubmit)} 99 | /> 100 | ); 101 | }; 102 | 103 | const SearchInput = React.forwardRef(SearchInputCore); 104 | 105 | function SearchInputWrapper() { 106 | const [state /*setState*/] = React.useState("hebele"); 107 | const inputValueSetter = React.useRef<(v: string) => any>(() => {}); 108 | setTimeout(() => { 109 | // setState('deneme'); 110 | inputValueSetter.current("deneme"); 111 | }, 5000); 112 | return ( 113 | (inputValueSetter.current = fn)} 116 | onClick={(e) => { 117 | // console.log("[CLICK]", "input clicked"); 118 | }} 119 | onDoubleClick={(e) => { 120 | // console.log("[DOUBLECLICK]", "input double clicked"); 121 | }} 122 | /> 123 | ); 124 | } 125 | 126 | export { SearchInputWrapper, SearchInput }; 127 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import Providers from "./Providers"; 2 | import ReactLeafletSearch from "./Search-v1"; 3 | import { SearchControl } from "./search-control"; 4 | 5 | import Search from "./Search-v2"; 6 | export default Search; // withLeaflet HOC 7 | 8 | export { ReactLeafletSearch, SearchControl, Providers }; 9 | -------------------------------------------------------------------------------- /src/search-control.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Providers from "./Providers"; 3 | import { BingMap, OpenStreetMap } from "./Providers"; 4 | import PropTypes from "prop-types"; 5 | import { Map as LeafletMap, LatLng } from "leaflet"; 6 | import "../css/index.css"; 7 | import { SearchInput } from "./core/search-input"; 8 | import { SearchCloseButton } from "./core/search-close-button"; 9 | import { SearchIconButton } from "./core/search-icon-button"; 10 | import { SearchInfoList } from "./core/search-info-list"; 11 | 12 | export interface SearchControlProps { 13 | provider?: string; 14 | customProvider?: { search: (value: string) => Promise }; 15 | providerOptions?: { 16 | providerKey?: string | null; 17 | searchBounds?: [LatLng, LatLng]; 18 | region?: string; 19 | }; 20 | search?: LatLng; 21 | openSearchOnLoad?: boolean; 22 | handler?: (obj: { 23 | event: "add" | "remove"; 24 | payload?: { 25 | latLng: LatLng; 26 | info: string; 27 | raw: any; 28 | }; 29 | }) => any; 30 | closeResultsOnClick?: boolean; 31 | inputPlaceholder?: string; 32 | map?: LeafletMap; 33 | className?: string; 34 | tabIndex?: number; 35 | } 36 | 37 | interface SearchControlState { 38 | open: boolean | undefined; 39 | closeButton: boolean; 40 | showInfo: boolean; 41 | } 42 | type ItemData = { latitude: number; longitude: number; name: string }; 43 | 44 | class SearchControl extends React.Component { 45 | input: React.RefObject; 46 | div: React.RefObject; 47 | provider: 48 | | OpenStreetMap 49 | | BingMap 50 | | { 51 | search: (value: string) => Promise; 52 | }; 53 | responseCache: { [key: string]: any }; 54 | SearchResponseInfo: JSX.Element | string | null; 55 | lastInfo: JSX.Element | string | null; 56 | lock!: boolean; 57 | inputEventHandler!: Function; 58 | inputValueSetter: Function; 59 | selectbox: React.RefObject; 60 | constructor(props: SearchControlProps) { 61 | super(props); 62 | this.state = { 63 | open: this.props.openSearchOnLoad, 64 | closeButton: false, 65 | showInfo: false, 66 | }; 67 | this.SearchResponseInfo = ""; 68 | this.responseCache = {}; 69 | this.lastInfo = null; 70 | this.inputValueSetter = () => {}; 71 | this.selectbox = React.createRef(); 72 | this.div = React.createRef(); 73 | this.input = React.createRef(); 74 | // use custom provider if exists any 75 | if (this.props.customProvider) { 76 | this.provider = this.props.customProvider; 77 | } else if (this.props.provider && Object.keys(Providers).includes(this.props.provider)) { 78 | const Provider = Providers[this.props.provider]; 79 | this.provider = new Provider(this.props.providerOptions); 80 | } else { 81 | throw new Error( 82 | `You set the provider prop to ${ 83 | this.props.provider 84 | } but that isn't recognised. You can choose from ${Object.keys(Providers).join(", ")}`, 85 | ); 86 | } 87 | } 88 | 89 | static propTypes = { 90 | provider: PropTypes.string, 91 | providerKey: PropTypes.string, 92 | inputPlaceholder: PropTypes.string, 93 | coords: PropTypes.arrayOf(PropTypes.number), 94 | closeResultsOnClick: PropTypes.bool, 95 | openSearchOnLoad: PropTypes.bool, 96 | searchBounds: PropTypes.array, 97 | providerOptions: PropTypes.object, 98 | }; 99 | 100 | static defaultProps: SearchControlProps = { 101 | inputPlaceholder: "Search Lat,Lng", 102 | closeResultsOnClick: false, 103 | openSearchOnLoad: false, 104 | search: undefined, 105 | provider: "OpenStreetMap", 106 | }; 107 | 108 | setLock = (value: boolean) => { 109 | this.lock = value; 110 | }; 111 | 112 | openSearch = () => { 113 | this.setState({ open: true }, () => { 114 | this.input.current?.focus(); 115 | }); 116 | }; 117 | closeSearch = () => { 118 | this.setState({ open: this.props.openSearchOnLoad, closeButton: false, showInfo: false }, () => { 119 | this.inputValueSetter(""); 120 | this.SearchResponseInfo = ""; 121 | this.props.handler && this.props.handler({ event: "remove" }); 122 | }); 123 | }; 124 | 125 | searchIconButtonOnClick = (e: React.SyntheticEvent) => { 126 | e.preventDefault(); 127 | e.stopPropagation(); 128 | this.state.open ? this.closeSearch() : this.openSearch(); 129 | }; 130 | inputBlur = (e: React.SyntheticEvent) => { 131 | this.input.current?.value === "" && !this.lock && this.closeSearch(); 132 | }; 133 | inputClick = (e: React.SyntheticEvent) => { 134 | this.input.current?.focus(); 135 | if ( 136 | !this.input.current?.value.startsWith(":") && 137 | this.lastInfo !== null && 138 | this.lastInfo !== "" && 139 | this.input.current?.value !== "" 140 | ) { 141 | this.SearchResponseInfo = this.lastInfo; 142 | this.lastInfo = null; 143 | this.setState({ showInfo: true }); 144 | } 145 | }; 146 | inputKeyUp = (e: React.KeyboardEvent) => { 147 | e.keyCode === 13 && this.beautifyValue(this.input.current!.value); 148 | }; 149 | closeClick = (e: React.SyntheticEvent) => { 150 | this.closeSearch(); 151 | }; 152 | 153 | sendToAction = async (e: React.SyntheticEvent): Promise => { 154 | if (!this.input.current!.value.startsWith(":")) { 155 | if (Object.prototype.hasOwnProperty.call(this.responseCache, this.input.current!.value)) { 156 | this.showInfo(this.responseCache[this.input.current!.value].info); 157 | } else { 158 | if (this.input.current!.value.length >= 3) { 159 | this.showInfo("Searching..."); 160 | const searchValue = this.input.current!.value; 161 | const response = await this.provider.search(searchValue); 162 | if ((response as { error: string }).error) { 163 | return false; 164 | } 165 | this.responseCache[searchValue] = response; 166 | this.showInfo((response as { info: string }).info); 167 | } 168 | } 169 | } 170 | }; 171 | syncInput = () => { 172 | !this.state.closeButton && this.setState({ closeButton: true }); 173 | if (this.input.current?.value === "") { 174 | this.hideInfo(); 175 | this.state.closeButton && this.setState({ closeButton: false }); 176 | } 177 | if (!this.input.current?.value.startsWith(":")) { 178 | } 179 | }; 180 | 181 | beautifyValue(value: string) { 182 | if (value.startsWith(":")) { 183 | const latLng = value 184 | .slice(1) 185 | .split(",") 186 | .filter((e) => !isNaN(Number(e))) 187 | .map((e) => Number(e ? e : 0)); 188 | if (latLng.length <= 1) { 189 | this.showInfo("Please enter a valid lat, lng"); 190 | } else { 191 | this.hideInfo(); 192 | this.props.handler && 193 | this.props.handler({ 194 | event: "add", 195 | payload: { 196 | latLng: new LatLng(Number(latLng[0]), Number(latLng[1])), 197 | info: latLng.join(","), 198 | raw: latLng.join(","), 199 | }, 200 | }); 201 | } 202 | } else { 203 | if (this.input.current!.value.length < 3) { 204 | const response = 'Please enter a valid lat,lng starting with ":" or minimum 3 character to search'; 205 | this.showInfo(response); 206 | } 207 | } 208 | } 209 | 210 | hideInfo() { 211 | this.lastInfo = this.SearchResponseInfo; 212 | this.SearchResponseInfo = ""; 213 | this.setState({ showInfo: false }); 214 | } 215 | showInfo(info: string | Array, activeIndex?: number) { 216 | // key changes when info changes so candidate number starts from zero 217 | this.SearchResponseInfo = ( 218 | 225 | ); 226 | this.input.current?.value && this.setState({ showInfo: true }); 227 | } 228 | 229 | listItemClick = (itemData: ItemData, totalInfo: Array, activeIndex: number) => { 230 | this.showInfo(totalInfo, activeIndex); 231 | this.props.handler && 232 | this.props.handler({ 233 | event: "add", 234 | payload: { 235 | latLng: new LatLng(Number(itemData.latitude), Number(itemData.longitude)), 236 | info: itemData.name, 237 | raw: this.responseCache[this.input.current!.value].raw, 238 | }, 239 | }); 240 | if (this.props.closeResultsOnClick) { 241 | this.hideInfo(); 242 | } 243 | }; 244 | 245 | setMaxHeight = () => { 246 | const containerRect = this.props.map 247 | ? this.props.map.getContainer().getBoundingClientRect() 248 | : document.body.getBoundingClientRect(); 249 | const divRect = this.input.current!.getBoundingClientRect(); 250 | const maxHeight = `${Math.floor((containerRect.bottom - divRect.bottom - 10) * 0.6)}px`; 251 | this.selectbox.current && this.selectbox.current.style && (this.selectbox.current.style.maxHeight = maxHeight); 252 | }; 253 | 254 | componentDidMount() { 255 | this.setMaxHeight(); 256 | if (this.props.search && !isNaN(Number(this.props.search.lat)) && !isNaN(Number(this.props.search.lng))) { 257 | const inputValue = `:${this.props.search.lat},${this.props.search.lng}`; 258 | this.inputValueSetter(inputValue); 259 | this.openSearch(); 260 | this.syncInput(); // to show close button 261 | this.props.handler && 262 | this.props.handler({ 263 | event: "add", 264 | payload: { 265 | latLng: new LatLng(Number(this.props.search.lat), Number(this.props.search.lng)), 266 | info: inputValue, 267 | raw: this.props.search, 268 | }, 269 | }); 270 | } 271 | } 272 | 273 | componentDidUpdate() { 274 | this.setMaxHeight(); 275 | if (this.state.showInfo) { 276 | // this.selectbox.current && this.selectbox.current.focus(); 277 | } 278 | } 279 | 280 | render() { 281 | return ( 282 |
    283 |
    284 | this.setLock(true)} 288 | onMouseLeave={() => this.setLock(false)} 289 | /> 290 | (this.inputValueSetter = fn)} 294 | className="search-control-input" 295 | placeholder={this.props.inputPlaceholder} 296 | onClick={this.inputClick} 297 | onMouseEnter={() => this.setLock(true)} 298 | onMouseLeave={() => this.setLock(false)} 299 | onChange={this.syncInput} 300 | onChangeAsync={this.sendToAction} 301 | onBlur={this.inputBlur} 302 | onKeyUp={this.inputKeyUp} 303 | onKeyPress={(e) => { 304 | e.stopPropagation(); 305 | e.keyCode === 40 && e.preventDefault(); 306 | }} 307 | onKeyDown={(e) => { 308 | // ArrowDown 40 309 | if (e.keyCode === 40) { 310 | e.preventDefault(); 311 | e.stopPropagation(); 312 | this.selectbox.current?.focus(); 313 | } 314 | // ArrowUp 38 315 | }} 316 | onSubmit={(e) => e.preventDefault()} 317 | /> 318 | 322 |
    323 |
    328 |
    329 | {this.state.showInfo && this.SearchResponseInfo} 330 |
    331 |
    332 |
    333 | ); 334 | } 335 | } 336 | 337 | export { SearchControl }; 338 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es2015" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */, 6 | "module": "es2015" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | "jsx": "react" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 11 | "declaration": true /* Generates corresponding '.d.ts' file. */, 12 | // "declarationDir": "./types", 13 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 14 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 15 | // "outFile": "./", /* Concatenate and emit output to single file. */ 16 | "outDir": "./lib/" /* Redirect output structure to the directory. */, 17 | "rootDir": "./src/" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, 18 | // "composite": true, /* Enable project compilation */ 19 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 20 | // "removeComments": true, /* Do not emit comments to output. */ 21 | // "noEmit": true, /* Do not emit outputs. */ 22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 25 | 26 | /* Strict Type-Checking Options */ 27 | "strict": true /* Enable all strict type-checking options. */, 28 | "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, 29 | // "strictNullChecks": true, /* Enable strict null checks. */ 30 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 31 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 32 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 33 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 34 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 35 | 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | 42 | /* Module Resolution Options */ 43 | "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, 44 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 47 | // "typeRoots": [], /* List of folders to include type definitions from. */ 48 | // "types": [], /* Type declaration files to be included in compilation. */ 49 | "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */, 50 | // "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 51 | "resolveJsonModule": true, 52 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 53 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 54 | 55 | /* Source Map Options */ 56 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 57 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 58 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 59 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 60 | 61 | /* Experimental Options */ 62 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 63 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 64 | 65 | /* Advanced Options */ 66 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 67 | }, 68 | "files": ["src/index.ts"], 69 | "exclude": ["node_modules"] 70 | } 71 | --------------------------------------------------------------------------------