@sanity/google-maps-input 2.30.2-shopify.0 → 3.0.0-studio-v3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +68 -2
  2. package/lib/cjs/index.js +902 -0
  3. package/lib/cjs/index.js.map +1 -0
  4. package/lib/esm/index.js +892 -0
  5. package/lib/esm/index.js.map +1 -0
  6. package/lib/types/index.d.ts +43 -0
  7. package/lib/types/index.d.ts.map +1 -0
  8. package/package.json +71 -19
  9. package/src/@types/css.d.ts +4 -0
  10. package/src/diff/GeopointArrayDiff.tsx +84 -0
  11. package/src/diff/GeopointFieldDiff.styles.tsx +20 -0
  12. package/src/diff/GeopointFieldDiff.tsx +95 -0
  13. package/src/diff/GeopointMove.tsx +48 -0
  14. package/src/diff/resolver.ts +21 -0
  15. package/src/global-workaround.ts +11 -0
  16. package/src/index.tsx +52 -0
  17. package/src/input/GeopointInput.styles.tsx +12 -0
  18. package/src/input/GeopointInput.tsx +176 -0
  19. package/src/input/GeopointSelect.tsx +78 -0
  20. package/src/loader/GoogleMapsLoadProxy.tsx +51 -0
  21. package/src/loader/LoadError.tsx +44 -0
  22. package/src/loader/loadGoogleMapsApi.ts +97 -0
  23. package/src/map/Arrow.tsx +76 -0
  24. package/src/map/Map.styles.tsx +10 -0
  25. package/src/map/Map.tsx +125 -0
  26. package/src/map/Marker.tsx +130 -0
  27. package/src/map/SearchInput.styles.tsx +8 -0
  28. package/src/map/SearchInput.tsx +56 -0
  29. package/src/map/util.ts +14 -0
  30. package/src/types.ts +19 -0
  31. package/.depcheckignore.json +0 -3
  32. package/config.dist.json +0 -9
  33. package/dist/dts/diff/GeopointArrayDiff.d.ts +0 -5
  34. package/dist/dts/diff/GeopointArrayDiff.d.ts.map +0 -1
  35. package/dist/dts/diff/GeopointFieldDiff.d.ts +0 -5
  36. package/dist/dts/diff/GeopointFieldDiff.d.ts.map +0 -1
  37. package/dist/dts/diff/GeopointFieldDiff.styles.d.ts +0 -2
  38. package/dist/dts/diff/GeopointFieldDiff.styles.d.ts.map +0 -1
  39. package/dist/dts/diff/GeopointMove.d.ts +0 -13
  40. package/dist/dts/diff/GeopointMove.d.ts.map +0 -1
  41. package/dist/dts/diff/resolver.d.ts +0 -4
  42. package/dist/dts/diff/resolver.d.ts.map +0 -1
  43. package/dist/dts/input/GeopointInput.d.ts +0 -40
  44. package/dist/dts/input/GeopointInput.d.ts.map +0 -1
  45. package/dist/dts/input/GeopointInput.styles.d.ts +0 -3
  46. package/dist/dts/input/GeopointInput.styles.d.ts.map +0 -1
  47. package/dist/dts/input/GeopointSelect.d.ts +0 -28
  48. package/dist/dts/input/GeopointSelect.d.ts.map +0 -1
  49. package/dist/dts/loader/GoogleMapsLoadProxy.d.ts +0 -14
  50. package/dist/dts/loader/GoogleMapsLoadProxy.d.ts.map +0 -1
  51. package/dist/dts/loader/LoadError.d.ts +0 -10
  52. package/dist/dts/loader/LoadError.d.ts.map +0 -1
  53. package/dist/dts/loader/loadGoogleMapsApi.d.ts +0 -18
  54. package/dist/dts/loader/loadGoogleMapsApi.d.ts.map +0 -1
  55. package/dist/dts/map/Arrow.d.ts +0 -29
  56. package/dist/dts/map/Arrow.d.ts.map +0 -1
  57. package/dist/dts/map/Map.d.ts +0 -37
  58. package/dist/dts/map/Map.d.ts.map +0 -1
  59. package/dist/dts/map/Map.styles.d.ts +0 -2
  60. package/dist/dts/map/Map.styles.d.ts.map +0 -1
  61. package/dist/dts/map/Marker.d.ts +0 -36
  62. package/dist/dts/map/Marker.d.ts.map +0 -1
  63. package/dist/dts/map/SearchInput.d.ts +0 -16
  64. package/dist/dts/map/SearchInput.d.ts.map +0 -1
  65. package/dist/dts/map/SearchInput.styles.d.ts +0 -2
  66. package/dist/dts/map/SearchInput.styles.d.ts.map +0 -1
  67. package/dist/dts/map/util.d.ts +0 -4
  68. package/dist/dts/map/util.d.ts.map +0 -1
  69. package/dist/dts/types.d.ts +0 -14
  70. package/dist/dts/types.d.ts.map +0 -1
  71. package/lib/@types/css.d.js +0 -1
  72. package/lib/diff/GeopointArrayDiff.js +0 -82
  73. package/lib/diff/GeopointFieldDiff.js +0 -97
  74. package/lib/diff/GeopointFieldDiff.styles.js +0 -18
  75. package/lib/diff/GeopointMove.js +0 -55
  76. package/lib/diff/resolver.js +0 -27
  77. package/lib/input/GeopointInput.js +0 -194
  78. package/lib/input/GeopointInput.styles.js +0 -22
  79. package/lib/input/GeopointSelect.js +0 -103
  80. package/lib/loader/GoogleMapsLoadProxy.js +0 -70
  81. package/lib/loader/LoadError.js +0 -43
  82. package/lib/loader/loadGoogleMapsApi.js +0 -81
  83. package/lib/map/Arrow.js +0 -97
  84. package/lib/map/Map.js +0 -147
  85. package/lib/map/Map.styles.js +0 -18
  86. package/lib/map/Marker.js +0 -156
  87. package/lib/map/SearchInput.js +0 -77
  88. package/lib/map/SearchInput.styles.js +0 -18
  89. package/lib/map/util.js +0 -14
  90. package/lib/types.js +0 -5
  91. package/sanity.json +0 -18
  92. package/tsconfig.json +0 -20
@@ -0,0 +1,12 @@
1
+ import styled from 'styled-components'
2
+
3
+ export const PreviewImage = styled.img`
4
+ width: 100%;
5
+ height: auto;
6
+ vertical-align: top;
7
+ `
8
+
9
+ export const DialogInnerContainer = styled.div`
10
+ height: 40rem;
11
+ width: 50rem;
12
+ `
@@ -0,0 +1,176 @@
1
+ import React from 'react'
2
+ import {uniqueId} from 'lodash'
3
+ import {Box, Button, Dialog, Grid} from '@sanity/ui'
4
+ import {EditIcon, TrashIcon} from '@sanity/icons'
5
+ import {ChangeIndicator, ChangeIndicatorCompareValueProvider} from 'sanity/_unstable'
6
+ import {ObjectInputProps, set, setIfMissing, unset} from 'sanity/form'
7
+ import {GoogleMapsLoadProxy} from '../loader/GoogleMapsLoadProxy'
8
+ import {Geopoint, GeopointSchemaType, LatLng} from '../types'
9
+ import {GeopointSelect} from './GeopointSelect'
10
+ import {DialogInnerContainer, PreviewImage} from './GeopointInput.styles'
11
+ import {GoogleMapsInputConfig} from '../index'
12
+ import {getGeoConfig} from '../global-workaround'
13
+
14
+ const getStaticImageUrl = (value: LatLng, apiKey: string) => {
15
+ const loc = `${value.lat},${value.lng}`
16
+ const params = {
17
+ key: apiKey,
18
+ center: loc,
19
+ markers: loc,
20
+ zoom: 13,
21
+ scale: 2,
22
+ size: '640x300',
23
+ } as const
24
+ const qs = Object.keys(params).reduce((res, param) => {
25
+ return res.concat(`${param}=${encodeURIComponent(params[param as keyof typeof params])}`)
26
+ }, [] as string[])
27
+
28
+ return `https://maps.googleapis.com/maps/api/staticmap?${qs.join('&')}`
29
+ }
30
+
31
+ export type GeopointInputProps = ObjectInputProps<Geopoint, GeopointSchemaType> & {
32
+ geoConfig: GoogleMapsInputConfig
33
+ }
34
+
35
+ type Focusable = any
36
+
37
+ interface InputState {
38
+ modalOpen: boolean
39
+ }
40
+
41
+ class GeopointInput extends React.PureComponent<GeopointInputProps, InputState> {
42
+ _geopointInputId = uniqueId('GeopointInput')
43
+
44
+ editButton: Focusable | undefined
45
+
46
+ constructor(props: GeopointInputProps) {
47
+ super(props)
48
+
49
+ this.state = {
50
+ modalOpen: false,
51
+ }
52
+ }
53
+
54
+ setEditButton = (el: Focusable) => {
55
+ this.editButton = el
56
+ }
57
+
58
+ focus() {
59
+ if (this.editButton) {
60
+ this.editButton.focus()
61
+ }
62
+ }
63
+
64
+ handleToggleModal = () => {
65
+ this.setState((prevState) => ({modalOpen: !prevState.modalOpen}))
66
+ }
67
+
68
+ handleCloseModal = () => {
69
+ this.setState({modalOpen: false})
70
+ }
71
+
72
+ handleChange = (latLng: google.maps.LatLng) => {
73
+ const {schemaType, onChange} = this.props
74
+ onChange([
75
+ setIfMissing({
76
+ _type: schemaType.name,
77
+ }),
78
+ set(latLng.lat(), ['lat']),
79
+ set(latLng.lng(), ['lng']),
80
+ ])
81
+ }
82
+
83
+ handleClear = () => {
84
+ const {onChange} = this.props
85
+ onChange(unset())
86
+ }
87
+
88
+ render() {
89
+ const {value, readOnly, compareValue, geoConfig: config} = this.props
90
+
91
+ const {modalOpen} = this.state
92
+
93
+ if (!config || !config.apiKey) {
94
+ return (
95
+ <div>
96
+ <p>
97
+ The <a href="https://sanity.io/docs/schema-types/geopoint-type">Geopoint type</a> needs
98
+ a Google Maps API key with access to:
99
+ </p>
100
+ <ul>
101
+ <li>Google Maps JavaScript API</li>
102
+ <li>Google Places API Web Service</li>
103
+ <li>Google Static Maps API</li>
104
+ </ul>
105
+ <p>
106
+ Please enter the API key with access to these services in your googleMapsInput plugin
107
+ config.
108
+ </p>
109
+ </div>
110
+ )
111
+ }
112
+
113
+ return (
114
+ <>
115
+ {value && (
116
+ <ChangeIndicatorCompareValueProvider value={value} compareValue={compareValue}>
117
+ <ChangeIndicator compareDeep>
118
+ <PreviewImage src={getStaticImageUrl(value, config.apiKey)} alt="Map location" />
119
+ </ChangeIndicator>
120
+ </ChangeIndicatorCompareValueProvider>
121
+ )}
122
+
123
+ {!readOnly && (
124
+ <Box marginTop={4}>
125
+ <Grid columns={2} gap={2}>
126
+ <Button
127
+ mode="ghost"
128
+ icon={value && EditIcon}
129
+ padding={3}
130
+ ref={this.setEditButton}
131
+ text={value ? 'Edit' : 'Set location'}
132
+ onClick={this.handleToggleModal}
133
+ />
134
+
135
+ {value && (
136
+ <Button
137
+ tone="critical"
138
+ icon={TrashIcon}
139
+ padding={3}
140
+ mode="ghost"
141
+ text={'Remove'}
142
+ onClick={this.handleClear}
143
+ />
144
+ )}
145
+ </Grid>
146
+ </Box>
147
+ )}
148
+
149
+ {modalOpen && (
150
+ <Dialog
151
+ id={`${this._geopointInputId}_dialog`}
152
+ onClose={this.handleCloseModal}
153
+ header="Place the marker on the map"
154
+ width={1}
155
+ >
156
+ <DialogInnerContainer>
157
+ <GoogleMapsLoadProxy config={getGeoConfig()}>
158
+ {(api) => (
159
+ <GeopointSelect
160
+ api={api}
161
+ value={value || undefined}
162
+ onChange={readOnly ? undefined : this.handleChange}
163
+ defaultLocation={config.defaultLocation}
164
+ defaultZoom={config.defaultZoom}
165
+ />
166
+ )}
167
+ </GoogleMapsLoadProxy>
168
+ </DialogInnerContainer>
169
+ </Dialog>
170
+ )}
171
+ </>
172
+ )
173
+ }
174
+ }
175
+
176
+ export default GeopointInput
@@ -0,0 +1,78 @@
1
+ import React from 'react'
2
+ import {SearchInput} from '../map/SearchInput'
3
+ import {GoogleMap} from '../map/Map'
4
+ import {Marker} from '../map/Marker'
5
+ import {LatLng, Geopoint} from '../types'
6
+
7
+ const fallbackLatLng: LatLng = {lat: 40.7058254, lng: -74.1180863}
8
+
9
+ interface SelectProps {
10
+ api: typeof window.google.maps
11
+ value?: Geopoint
12
+ onChange?: (latLng: google.maps.LatLng) => void
13
+ defaultLocation?: LatLng
14
+ defaultZoom?: number
15
+ }
16
+
17
+ export class GeopointSelect extends React.PureComponent<SelectProps> {
18
+ static defaultProps = {
19
+ defaultZoom: 8,
20
+ defaultLocation: {lng: 10.74609, lat: 59.91273},
21
+ }
22
+
23
+ mapRef = React.createRef<HTMLDivElement>()
24
+
25
+ getCenter() {
26
+ const {value = {}, defaultLocation = {}} = this.props
27
+ const point: LatLng = {...fallbackLatLng, ...defaultLocation, ...value}
28
+ return point
29
+ }
30
+
31
+ handlePlaceChanged = (place: google.maps.places.PlaceResult) => {
32
+ if (!place.geometry?.location) {
33
+ return
34
+ }
35
+
36
+ this.setValue(place.geometry.location)
37
+ }
38
+
39
+ handleMarkerDragEnd = (event: google.maps.MapMouseEvent) => {
40
+ if (event.latLng) this.setValue(event.latLng)
41
+ }
42
+
43
+ handleMapClick = (event: google.maps.MapMouseEvent) => {
44
+ if (event.latLng) this.setValue(event.latLng)
45
+ }
46
+
47
+ setValue(geoPoint: google.maps.LatLng) {
48
+ if (this.props.onChange) {
49
+ this.props.onChange(geoPoint)
50
+ }
51
+ }
52
+
53
+ render() {
54
+ const {api, defaultZoom, value, onChange} = this.props
55
+ return (
56
+ <GoogleMap
57
+ api={api}
58
+ location={this.getCenter()}
59
+ onClick={this.handleMapClick}
60
+ defaultZoom={defaultZoom}
61
+ >
62
+ {(map) => (
63
+ <>
64
+ <SearchInput api={api} map={map} onChange={this.handlePlaceChanged} />
65
+ {value && (
66
+ <Marker
67
+ api={api}
68
+ map={map}
69
+ position={value}
70
+ onMove={onChange ? this.handleMarkerDragEnd : undefined}
71
+ />
72
+ )}
73
+ </>
74
+ )}
75
+ </GoogleMap>
76
+ )
77
+ }
78
+ }
@@ -0,0 +1,51 @@
1
+ import React from 'react'
2
+ import {Subscription} from 'rxjs'
3
+ import {loadGoogleMapsApi, GoogleLoadState} from './loadGoogleMapsApi'
4
+ import {LoadError} from './LoadError'
5
+ import {GoogleMapsInputConfig} from '../index'
6
+
7
+ interface LoadProps {
8
+ children: (api: typeof window.google.maps) => React.ReactElement
9
+ config: GoogleMapsInputConfig
10
+ }
11
+
12
+ export class GoogleMapsLoadProxy extends React.Component<LoadProps, GoogleLoadState> {
13
+ loadSubscription: Subscription | undefined
14
+
15
+ constructor(props: LoadProps) {
16
+ super(props)
17
+
18
+ this.state = {loadState: 'loading'}
19
+
20
+ let sync = true
21
+ this.loadSubscription = loadGoogleMapsApi(props.config).subscribe((loadState) => {
22
+ if (sync) {
23
+ this.state = loadState
24
+ } else {
25
+ this.setState(loadState)
26
+ }
27
+ })
28
+ sync = false
29
+ }
30
+
31
+ componentWillUnmount() {
32
+ if (this.loadSubscription) {
33
+ this.loadSubscription.unsubscribe()
34
+ }
35
+ }
36
+
37
+ render() {
38
+ switch (this.state.loadState) {
39
+ case 'loadError':
40
+ return <LoadError error={this.state.error} isAuthError={false} />
41
+ case 'authError':
42
+ return <LoadError isAuthError />
43
+ case 'loading':
44
+ return <div>Loading Google Maps API</div>
45
+ case 'loaded':
46
+ return this.props.children(this.state.api) || null
47
+ default:
48
+ return null
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,44 @@
1
+ import * as React from 'react'
2
+ import {Card, Box, Text, Code} from '@sanity/ui'
3
+
4
+ type Props = {error: Error; isAuthError: false} | {isAuthError: true}
5
+
6
+ export function LoadError(props: Props) {
7
+ return (
8
+ <Card tone="critical" radius={1}>
9
+ <Box as="header" paddingX={4} paddingTop={4} paddingBottom={1}>
10
+ <Text as="h2" weight="bold">
11
+ Google Maps failed to load
12
+ </Text>
13
+ </Box>
14
+
15
+ <Box paddingX={4} paddingTop={4} paddingBottom={1}>
16
+ {props.isAuthError ? (
17
+ <AuthError />
18
+ ) : (
19
+ <>
20
+ <Text as="h3">Error details:</Text>
21
+ <pre>
22
+ <Code size={1}>{'error' in props && props.error?.message}</Code>
23
+ </pre>
24
+ </>
25
+ )}
26
+ </Box>
27
+ </Card>
28
+ )
29
+ }
30
+
31
+ function AuthError() {
32
+ return (
33
+ <Text>
34
+ <p>The error appears to be related to authentication</p>
35
+ <p>Common causes include:</p>
36
+ <ul>
37
+ <li>Incorrect API key</li>
38
+ <li>Referer not allowed</li>
39
+ <li>Missing authentication scope</li>
40
+ </ul>
41
+ <p>Check the browser developer tools for more information.</p>
42
+ </Text>
43
+ )
44
+ }
@@ -0,0 +1,97 @@
1
+ import {Observable, BehaviorSubject} from 'rxjs'
2
+ import {GoogleMapsInputConfig} from '../index'
3
+
4
+ declare global {
5
+ interface Window {
6
+ gm_authFailure: any
7
+ ___sanity_googleMapsApiCallback: any
8
+ }
9
+ }
10
+
11
+ const callbackName = '___sanity_googleMapsApiCallback'
12
+ const authFailureCallbackName = 'gm_authFailure'
13
+ const locale = (typeof window !== 'undefined' && window.navigator.language) || 'en'
14
+
15
+ export interface LoadingState {
16
+ loadState: 'loading'
17
+ }
18
+
19
+ export interface LoadedState {
20
+ loadState: 'loaded'
21
+ api: typeof window.google.maps
22
+ }
23
+
24
+ export interface LoadErrorState {
25
+ loadState: 'loadError'
26
+ error: Error
27
+ }
28
+
29
+ export interface AuthErrorState {
30
+ loadState: 'authError'
31
+ }
32
+
33
+ export type GoogleLoadState = LoadingState | LoadedState | LoadErrorState | AuthErrorState
34
+
35
+ let subject: BehaviorSubject<GoogleLoadState>
36
+
37
+ export function loadGoogleMapsApi(config: GoogleMapsInputConfig): Observable<GoogleLoadState> {
38
+ const selectedLocale = config.defaultLocale || locale || 'en-US'
39
+
40
+ if (subject) {
41
+ return subject
42
+ }
43
+
44
+ subject = new BehaviorSubject<GoogleLoadState>({loadState: 'loading'})
45
+
46
+ window[authFailureCallbackName] = () => {
47
+ delete window[authFailureCallbackName]
48
+ subject.next({loadState: 'authError'})
49
+ }
50
+
51
+ window[callbackName] = () => {
52
+ delete window[callbackName]
53
+ subject.next({loadState: 'loaded', api: window.google.maps})
54
+ }
55
+
56
+ const script = document.createElement('script')
57
+ script.onerror = (
58
+ event: Event | string,
59
+ source?: string,
60
+ lineno?: number,
61
+ colno?: number,
62
+ error?: Error
63
+ ) =>
64
+ subject.next({
65
+ loadState: 'loadError',
66
+ error: coeerceError(event, error),
67
+ } as LoadErrorState)
68
+
69
+ script.src = `https://maps.googleapis.com/maps/api/js?key=${config.apiKey}&libraries=places&callback=${callbackName}&language=${selectedLocale}`
70
+ document.getElementsByTagName('head')[0].appendChild(script)
71
+
72
+ return subject
73
+ }
74
+
75
+ function coeerceError(event: Event | string, error?: Error): Error {
76
+ if (error) {
77
+ return error
78
+ }
79
+
80
+ if (typeof event === 'string') {
81
+ return new Error(event)
82
+ }
83
+
84
+ return new Error(isErrorEvent(event) ? event.message : 'Failed to load Google Maps API')
85
+ }
86
+
87
+ function isErrorEvent(event: unknown): event is ErrorEvent {
88
+ if (typeof event !== 'object' || event === null) {
89
+ return false
90
+ }
91
+
92
+ if (!('message' in event)) {
93
+ return false
94
+ }
95
+
96
+ return typeof (event as ErrorEvent).message === 'string'
97
+ }
@@ -0,0 +1,76 @@
1
+ import * as React from 'react'
2
+ import {LatLng} from '../types'
3
+ import {latLngAreEqual} from './util'
4
+
5
+ interface Props {
6
+ api: typeof window.google.maps
7
+ map: google.maps.Map
8
+ from: LatLng
9
+ to: LatLng
10
+ color?: {background: string; border: string; text: string}
11
+ zIndex?: number
12
+ arrowRef?: React.MutableRefObject<google.maps.Polyline | undefined>
13
+ onClick?: (event: google.maps.MapMouseEvent) => void
14
+ }
15
+
16
+ export class Arrow extends React.PureComponent<Props> {
17
+ line: google.maps.Polyline | undefined
18
+
19
+ eventHandlers: {
20
+ click?: google.maps.MapsEventListener
21
+ } = {}
22
+
23
+ componentDidMount() {
24
+ const {from, to, api, map, zIndex, onClick, color, arrowRef} = this.props
25
+ const lineSymbol = {
26
+ path: api.SymbolPath.FORWARD_OPEN_ARROW,
27
+ }
28
+
29
+ this.line = new api.Polyline({
30
+ map,
31
+ zIndex,
32
+ path: [from, to],
33
+ icons: [{icon: lineSymbol, offset: '50%'}],
34
+ strokeOpacity: 0.55,
35
+ strokeColor: color ? color.text : 'black',
36
+ })
37
+
38
+ if (onClick) {
39
+ this.eventHandlers.click = api.event.addListener(this.line, 'click', onClick)
40
+ }
41
+
42
+ if (arrowRef) {
43
+ arrowRef.current = this.line
44
+ }
45
+ }
46
+
47
+ componentDidUpdate(prevProps: Props) {
48
+ if (!this.line) {
49
+ return
50
+ }
51
+
52
+ const {from, to, map} = this.props
53
+ if (!latLngAreEqual(prevProps.from, from) || !latLngAreEqual(prevProps.to, to)) {
54
+ this.line.setPath([from, to])
55
+ }
56
+
57
+ if (prevProps.map !== map) {
58
+ this.line.setMap(map)
59
+ }
60
+ }
61
+
62
+ componentWillUnmount() {
63
+ if (this.line) {
64
+ this.line.setMap(null)
65
+ }
66
+
67
+ if (this.eventHandlers.click) {
68
+ this.eventHandlers.click.remove()
69
+ }
70
+ }
71
+
72
+ // eslint-disable-next-line class-methods-use-this
73
+ render(): any {
74
+ return null
75
+ }
76
+ }
@@ -0,0 +1,10 @@
1
+ import styled from 'styled-components'
2
+
3
+ export const MapContainer = styled.div`
4
+ position: absolute;
5
+ top: 0;
6
+ left: 0;
7
+ height: 100%;
8
+ width: 100%;
9
+ box-sizing: border-box;
10
+ `
@@ -0,0 +1,125 @@
1
+ import React from 'react'
2
+ import {LatLng} from '../types'
3
+ import {latLngAreEqual} from './util'
4
+ import {MapContainer} from './Map.styles'
5
+
6
+ interface MapProps {
7
+ api: typeof window.google.maps
8
+ location: LatLng
9
+ bounds?: google.maps.LatLngBounds
10
+ defaultZoom?: number
11
+ mapTypeControl?: boolean
12
+ scrollWheel?: boolean
13
+ controlSize?: number
14
+ onClick?: (event: google.maps.MapMouseEvent) => void
15
+ children?: (map: google.maps.Map) => React.ReactElement
16
+ }
17
+
18
+ interface MapState {
19
+ map: google.maps.Map | undefined
20
+ }
21
+
22
+ export class GoogleMap extends React.PureComponent<MapProps, MapState> {
23
+ static defaultProps = {
24
+ defaultZoom: 8,
25
+ scrollWheel: true,
26
+ }
27
+
28
+ state: MapState = {map: undefined}
29
+ clickHandler: google.maps.MapsEventListener | undefined
30
+ mapRef = React.createRef<HTMLDivElement>()
31
+ mapEl: HTMLDivElement | null = null
32
+
33
+ componentDidMount() {
34
+ this.attachClickHandler()
35
+ }
36
+
37
+ attachClickHandler = () => {
38
+ const map = this.state.map
39
+ if (!map) {
40
+ return
41
+ }
42
+
43
+ const {api, onClick} = this.props
44
+ const {event} = api
45
+
46
+ if (this.clickHandler) {
47
+ this.clickHandler.remove()
48
+ }
49
+
50
+ if (onClick) {
51
+ this.clickHandler = event.addListener(map, 'click', onClick)
52
+ }
53
+ }
54
+
55
+ componentDidUpdate(prevProps: MapProps) {
56
+ const map = this.state.map
57
+ if (!map) {
58
+ return
59
+ }
60
+
61
+ const {onClick, location, bounds} = this.props
62
+
63
+ if (prevProps.onClick !== onClick) {
64
+ this.attachClickHandler()
65
+ }
66
+
67
+ if (!latLngAreEqual(prevProps.location, location)) {
68
+ map.panTo(this.getCenter())
69
+ }
70
+
71
+ if (bounds && (!prevProps.bounds || !bounds.equals(prevProps.bounds))) {
72
+ map.fitBounds(bounds)
73
+ }
74
+ }
75
+
76
+ componentWillUnmount() {
77
+ if (this.clickHandler) {
78
+ this.clickHandler.remove()
79
+ }
80
+ }
81
+
82
+ getCenter(): google.maps.LatLng {
83
+ const {location, api} = this.props
84
+ return new api.LatLng(location.lat, location.lng)
85
+ }
86
+
87
+ constructMap(el: HTMLDivElement) {
88
+ const {defaultZoom, api, mapTypeControl, controlSize, bounds, scrollWheel} = this.props
89
+
90
+ const map = new api.Map(el, {
91
+ zoom: defaultZoom,
92
+ center: this.getCenter(),
93
+ scrollwheel: scrollWheel,
94
+ streetViewControl: false,
95
+ mapTypeControl,
96
+ controlSize,
97
+ })
98
+
99
+ if (bounds) {
100
+ map.fitBounds(bounds)
101
+ }
102
+
103
+ return map
104
+ }
105
+
106
+ setMapElement = (element: HTMLDivElement | null) => {
107
+ if (element && element !== this.mapEl) {
108
+ const map = this.constructMap(element)
109
+ this.setState({map}, this.attachClickHandler)
110
+ }
111
+
112
+ this.mapEl = element
113
+ }
114
+
115
+ render() {
116
+ const {children} = this.props
117
+ const {map} = this.state
118
+ return (
119
+ <>
120
+ <MapContainer ref={this.setMapElement} />
121
+ {children && map ? children(map) : null}
122
+ </>
123
+ )
124
+ }
125
+ }