· 5 years ago · Oct 30, 2020, 04:28 PM
1import React, { useCallback, useEffect, useRef, useState } from 'react';
2import PropTypes from 'prop-types';
3import intl from 'react-intl-universal';
4import { GoogleMap, Marker, StandaloneSearchBox, useLoadScript } from '@react-google-maps/api';
5
6const libraries = ['places'];
7const mapOptions = {
8 zoomControl: true,
9 mapTypeControl: false,
10 scaleControl: false,
11 streetViewControl: false,
12 scrollwheel: false,
13 rotateControl: false,
14 fullscreenControl: false,
15};
16
17function Location(props) {
18 // State
19 const [inputType, setInputType] = useState('');
20 const [center, setCenter] = useState(props.center);
21 const [markers, setMarkers] = useState([]);
22 const [searchValue, setSearchValue] = useState(props.searchValue || '');
23 const [map, setMap] = useState(null);
24 const searchBoxRef = useRef();
25 const prevLocationsRef = useRef();
26 const { isLoaded, loadError } = useLoadScript({
27 googleMapsApiKey: props.GoogleAPIKey,
28 libraries,
29 });
30
31 // Methods
32 const fitBoundsCb = useCallback(
33 function fitBounds() {
34 if (
35 map &&
36 props.fitBounds &&
37 props.readOnly &&
38 markers.findIndex((marker) => marker.animation) === -1
39 ) {
40 const bounds = new window.google.maps.LatLngBounds();
41 markers.forEach((marker) => {
42 bounds.extend(marker.position);
43 });
44 map.fitBounds(bounds);
45 }
46 },
47 [map, markers, props.fitBounds, props.readOnly]
48 );
49
50 const getPlacesToMarkersCb = useCallback(
51 function getPlacesToMarkers(places) {
52 const markersData = places.map((place) => ({
53 position: place.geometry.location,
54 data: place.location_data,
55 icon: place.location_icon,
56 animation: place.location_animation,
57 }));
58
59 const mapCenter = markersData?.[0]?.position || center;
60 return { center: mapCenter, markers: markersData };
61 },
62 [center]
63 );
64
65 const onMarkerRightClickCb = useCallback(
66 function onMarkerRightClick(targetMarker) {
67 if (!props.readOnly) {
68 const places = JSON.parse(props.input.value)?.data?.filter(
69 (place) =>
70 place.geometry.location.lat !== targetMarker.position.lat &&
71 place.geometry.location.lng !== targetMarker.position.lng
72 );
73 props.input.onChange(JSON.stringify({ type: 'google', data: places }));
74 const nextMarkers = markers.filter((marker) => marker !== targetMarker);
75 setMarkers(nextMarkers);
76 setInputType('input-google');
77 }
78 },
79 [markers, props.readOnly, props.input]
80 );
81
82 const onPlacesChangedCb = useCallback(
83 function onPlacesChanged() {
84 const places = searchBoxRef.current?.getPlaces();
85 const markersData = getPlacesToMarkersCb(places);
86 setCenter(markersData.center);
87 setMarkers(markersData.markers);
88 setInputType('input-google');
89 setSearchValue(places[0].formatted_address);
90 props.input.onChange(JSON.stringify({ type: 'google', data: places }));
91 },
92 [searchBoxRef, props.input, getPlacesToMarkersCb]
93 );
94
95 const onSearchBoxMountedCb = useCallback(function handleSearchBoxMounted(searchBox) {
96 searchBoxRef.current = searchBox;
97 }, []);
98
99 const onLoadCb = useCallback(
100 function onLoad(mapInstance) {
101 setMap(mapInstance);
102 fitBoundsCb();
103 },
104 [fitBoundsCb]
105 );
106
107 // Effects
108 useEffect(() => {
109 fitBoundsCb();
110 }, [map, markers, fitBoundsCb]);
111 useEffect(() => {
112 if (props.readOnly && props.locations && props.locations !== prevLocationsRef.current) {
113 const places = JSON.parse(props.locations);
114 if (places.type === 'google') {
115 const markersData = getPlacesToMarkersCb(places.data);
116 setCenter(markersData.center);
117 setMarkers(markersData.markers);
118 }
119 }
120 prevLocationsRef.current = props.locations;
121 }, [props.locations, props.readOnly, getPlacesToMarkersCb]);
122
123 // Render
124 if (loadError) {
125 console.log('Error loading Google Maps', loadError);
126 return null;
127 }
128 return (
129 <div
130 className={`company-google-map ${props.displayMap ? '' : 'no-map'} ${props.className} ${
131 searchBoxRef && searchValue === '' ? 'no-input' : inputType
132 } ${props.readOnly ? 'read' : 'edit'} ${
133 !props.readOnly && (props.meta?.touched || !props.meta?.pristine) && props.meta?.error
134 ? 'error'
135 : ''
136 }`}
137 >
138 {isLoaded ? (
139 <GoogleMap
140 center={center}
141 libraries={libraries}
142 mapContainerClassName="company-google-map-container"
143 mapContainerStyle={{ width: '100%' }}
144 onLoad={onLoadCb}
145 options={mapOptions}
146 zoom={props.defaultZoom}
147 >
148 {markers.map((marker) => (
149 <Marker
150 key={marker?.data?.ID}
151 animation={marker.animation ? window.google?.maps.Animation.BOUNCE : null}
152 icon={
153 marker.icon ||
154 'https://static.application.cloud/public_icons/map-pin--solid-grey-900.svg'
155 }
156 onClick={() => props.onLocationClick(marker)}
157 onRightClick={() => onMarkerRightClickCb(marker)}
158 position={marker.position}
159 />
160 ))}
161 {!props.readOnly && props.input && (
162 <button
163 key={`${props.input.name}_clearButton`}
164 className="zs-button zs-button--link company-google-map-clear"
165 onClick={(e) => {
166 e.preventDefault();
167 if (searchBoxRef) {
168 props.input.onChange(null);
169 setMarkers([]);
170 setSearchValue('');
171 }
172 }}
173 >
174 {intl.get('general.clear').d('Clear')}
175 </button>
176 )}
177 {!props.readOnly && (
178 <StandaloneSearchBox
179 key="company-google-map-input"
180 onLoad={onSearchBoxMountedCb}
181 onPlacesChanged={onPlacesChangedCb}
182 >
183 <input
184 className="company-google-map-input"
185 onChange={(e) => {
186 setSearchValue(e.target.value);
187 setMarkers([]);
188 setInputType('input-string');
189 if (!props.onlyGoogleLocation) {
190 props.input.onChange(JSON.stringify({ type: 'string', data: e.target.value }));
191 }
192 }}
193 placeholder={props.placeholder}
194 type="text"
195 value={searchValue}
196 />
197 </StandaloneSearchBox>
198 )}
199 </GoogleMap>
200 ) : (
201 <div className="company-google-map-loader">{props.renderLoading()}</div>
202 )}
203 </div>
204 );
205}
206
207Location.propTypes = {
208 // Google API Key
209 GoogleAPIKey: PropTypes.string.isRequired,
210 // Map center coordinates
211 center: PropTypes.shape({
212 lat: PropTypes.number.isRequired,
213 lng: PropTypes.number.isRequired,
214 }),
215 // Extra classe names to add to the component
216 className: PropTypes.string,
217 defaultZoom: PropTypes.number,
218 // Enable or disable the display of the map when searching a location
219 displayMap: PropTypes.bool,
220 fitBounds: PropTypes.bool,
221 // redux-form inherited props from `<Field.>`
222 input: PropTypes.shape(),
223 // Locations to display on map, `json` formatted, check google-maps api
224 locations: PropTypes.string,
225 // redux-form inherited props from `<Field.>`
226 meta: PropTypes.shape(),
227 onChange: PropTypes.func,
228 // Called when clicking a marker
229 onLocationClick: PropTypes.func,
230 // Enable or disable google only locations. Format input as `{type: 'google'|'string', data: ...}`
231 onlyGoogleLocation: PropTypes.bool,
232 // Placeholder for the map search input
233 placeholder: PropTypes.string,
234 // Only displays the map, no input
235 readOnly: PropTypes.bool,
236 // Function rendering the loading element when the map is loading
237 renderLoading: PropTypes.func,
238 searchValue: PropTypes.string,
239};
240
241Location.defaultProps = {
242 center: { lat: 49.62779, lng: 6.163483 },
243 className: '',
244 defaultZoom: 15,
245 displayMap: true,
246 fitBounds: false,
247 input: {},
248 locations: '',
249 meta: null,
250 onChange: () => null,
251 onLocationClick: () => null,
252 onlyGoogleLocation: false,
253 placeholder: 'Enter location',
254 readOnly: false,
255 renderLoading: () => 'Loading',
256 searchValue: '',
257};