@sanity/google-maps-input 4.2.1 → 5.0.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 (39) hide show
  1. package/LICENSE +1 -1
  2. package/assets/google-maps-input.png +0 -0
  3. package/assets/google-maps-radius-input.png +0 -0
  4. package/dist/index.d.ts +54 -84
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +736 -652
  7. package/dist/index.js.map +1 -1
  8. package/package.json +36 -80
  9. package/dist/index.cjs +0 -1014
  10. package/dist/index.cjs.map +0 -1
  11. package/dist/index.d.cts +0 -85
  12. package/sanity.json +0 -8
  13. package/src/diff/GeopointArrayDiff.tsx +0 -86
  14. package/src/diff/GeopointFieldDiff.styles.tsx +0 -20
  15. package/src/diff/GeopointFieldDiff.tsx +0 -97
  16. package/src/diff/GeopointMove.tsx +0 -48
  17. package/src/diff/GeopointRadiusFieldDiff.tsx +0 -136
  18. package/src/diff/GeopointRadiusMove.tsx +0 -92
  19. package/src/diff/resolver.ts +0 -26
  20. package/src/global-workaround.ts +0 -11
  21. package/src/index.ts +0 -20
  22. package/src/input/GeopointInput.styles.tsx +0 -11
  23. package/src/input/GeopointInput.tsx +0 -179
  24. package/src/input/GeopointRadiusInput.tsx +0 -258
  25. package/src/input/GeopointRadiusSelect.tsx +0 -193
  26. package/src/input/GeopointSelect.tsx +0 -79
  27. package/src/loader/GoogleMapsLoadProxy.tsx +0 -62
  28. package/src/loader/LoadError.tsx +0 -43
  29. package/src/loader/loadGoogleMapsApi.ts +0 -77
  30. package/src/map/Arrow.tsx +0 -76
  31. package/src/map/Map.styles.tsx +0 -10
  32. package/src/map/Map.tsx +0 -125
  33. package/src/map/Marker.tsx +0 -130
  34. package/src/map/SearchInput.styles.tsx +0 -8
  35. package/src/map/SearchInput.tsx +0 -56
  36. package/src/map/util.ts +0 -14
  37. package/src/plugin.tsx +0 -92
  38. package/src/types.ts +0 -46
  39. package/v2-incompatible.js +0 -10
@@ -1,258 +0,0 @@
1
- import React, {useCallback, useEffect, useId, useRef, useState} from 'react'
2
- import {Box, Button, Dialog, Grid, Stack, TextInput, Label} from '@sanity/ui'
3
- import {EditIcon, TrashIcon} from '@sanity/icons'
4
- import {ObjectInputProps, set, setIfMissing, unset, ChangeIndicator, Path} from 'sanity'
5
- import {GoogleMapsLoadProxy} from '../loader/GoogleMapsLoadProxy'
6
- import type {
7
- GeopointRadius,
8
- GeopointRadiusSchemaType,
9
- GoogleMapsInputConfig,
10
- LatLng,
11
- } from '../types'
12
- import {getGeoConfig} from '../global-workaround'
13
- import {DialogInnerContainer, PreviewImage} from './GeopointInput.styles'
14
- import {GeopointRadiusSelect} from './GeopointRadiusSelect'
15
-
16
- const EMPTY_PATH: Path = []
17
-
18
- // Helper function to generate circle points
19
- const generateCirclePoints = (
20
- lat: number,
21
- lng: number,
22
- radius: number,
23
- ): Array<{lat: number; lng: number}> => {
24
- const points = []
25
- const steps = 32 // Number of points to create the circle
26
-
27
- for (let i = 0; i <= steps; i++) {
28
- const angle = (i / steps) * 2 * Math.PI
29
- const latOffset = (radius / 111000) * Math.cos(angle) // Rough conversion to degrees
30
- const lngOffset = (radius / (111000 * Math.cos((lat * Math.PI) / 180))) * Math.sin(angle)
31
-
32
- points.push({
33
- lat: lat + latOffset,
34
- lng: lng + lngOffset,
35
- })
36
- }
37
-
38
- return points
39
- }
40
-
41
- const getStaticImageUrl = (value: LatLng & {radius?: number}, apiKey: string) => {
42
- const loc = `${value.lat},${value.lng}`
43
-
44
- // Calculate appropriate zoom level based on radius
45
- let zoom = 13
46
- if (value.radius) {
47
- // Use logarithmic formula for better zoom calculation
48
- // Add padding to ensure circle is fully visible
49
- const radius = value.radius + value.radius / 2
50
- const scale = radius / 500
51
- const calculatedZoom = 16 - Math.log(scale) / Math.log(2)
52
- // Add small offset to ensure circle fits well in view
53
- zoom = Math.max(8, Math.min(16, Math.round(calculatedZoom - 0.4)))
54
- }
55
-
56
- const qs = new URLSearchParams({
57
- key: apiKey,
58
- center: loc,
59
- markers: loc,
60
- zoom: zoom.toString(),
61
- scale: '2',
62
- size: '640x300',
63
- })
64
-
65
- // Add circle if radius is present
66
- if (value.radius) {
67
- // Create a circle path using multiple points
68
- const points = generateCirclePoints(value.lat, value.lng, value.radius)
69
- const path = points.map((p) => `${p.lat},${p.lng}`).join('|')
70
- qs.append('path', `fillcolor:0x4285F480|color:0x4285F4|weight:2|${path}`)
71
- }
72
-
73
- return `https://maps.googleapis.com/maps/api/staticmap?${qs.toString()}`
74
- }
75
-
76
- export type GeopointRadiusInputProps = ObjectInputProps<
77
- GeopointRadius,
78
- GeopointRadiusSchemaType
79
- > & {
80
- geoConfig: GoogleMapsInputConfig
81
- }
82
-
83
- export function GeopointRadiusInput(props: GeopointRadiusInputProps) {
84
- const {
85
- changed,
86
- elementProps,
87
- focused,
88
- geoConfig: config,
89
- onChange,
90
- onPathFocus,
91
- path,
92
- readOnly,
93
- schemaType,
94
- value,
95
- } = props
96
-
97
- const {
98
- id,
99
- ref: inputRef,
100
- onBlur: handleBlur,
101
- onFocus: handleFocus,
102
- 'aria-describedby': ariaDescribedBy,
103
- } = elementProps
104
-
105
- const schemaTypeName = schemaType.name
106
- const dialogId = useId()
107
- const dialogRef = useRef<HTMLDivElement | null>(null)
108
- const handleFocusButton = useCallback(() => inputRef?.current?.focus(), [inputRef])
109
- const [modalOpen, setModalOpen] = useState(false)
110
-
111
- const handleCloseModal = useCallback(() => {
112
- if (dialogRef.current) dialogRef.current.blur()
113
- setModalOpen(false)
114
- handleFocusButton()
115
- }, [setModalOpen, handleFocusButton])
116
-
117
- const handleToggleModal = useCallback(
118
- () => setModalOpen((currentState) => !currentState),
119
- [setModalOpen],
120
- )
121
-
122
- const handleChange = useCallback(
123
- (latLng: google.maps.LatLng, radius?: number) => {
124
- const currentRadius = radius ?? value?.radius ?? config.defaultRadius ?? 1000
125
- onChange([
126
- setIfMissing({_type: schemaTypeName}),
127
- set(latLng.lat(), ['lat']),
128
- set(latLng.lng(), ['lng']),
129
- set(currentRadius, ['radius']),
130
- ])
131
- },
132
- [schemaTypeName, onChange, value?.radius, config.defaultRadius],
133
- )
134
-
135
- const handleRadiusChange = useCallback(
136
- (event: React.ChangeEvent<HTMLInputElement>) => {
137
- if (value) {
138
- onChange([set(Math.round(Number(event.currentTarget.value)), ['radius'])])
139
- }
140
- },
141
- [onChange, value],
142
- )
143
-
144
- const handleClear = useCallback(() => {
145
- onChange(unset())
146
- }, [onChange])
147
-
148
- useEffect(() => {
149
- if (modalOpen) {
150
- onPathFocus(EMPTY_PATH)
151
- }
152
- }, [modalOpen, onPathFocus])
153
-
154
- if (!config || !config.apiKey) {
155
- return (
156
- <div>
157
- <p>
158
- The <a href="https://sanity.io/docs/schema-types/geopoint-type">Geopoint Radius type</a>{' '}
159
- needs a Google Maps API key with access to:
160
- </p>
161
- <ul>
162
- <li>Google Maps JavaScript API</li>
163
- <li>Google Places API Web Service</li>
164
- <li>Google Static Maps API</li>
165
- </ul>
166
- <p>
167
- Please enter the API key with access to these services in your googleMapsInput plugin
168
- config.
169
- </p>
170
- </div>
171
- )
172
- }
173
-
174
- return (
175
- <Stack space={3}>
176
- {value && (
177
- <ChangeIndicator path={path} isChanged={changed} hasFocus={!!focused}>
178
- <PreviewImage
179
- src={getStaticImageUrl(value, config.apiKey)}
180
- alt="Map location with radius"
181
- onClick={handleFocusButton}
182
- onDoubleClick={handleToggleModal}
183
- />
184
- </ChangeIndicator>
185
- )}
186
-
187
- {value && (
188
- <Stack space={2}>
189
- <Label>Radius (meters)</Label>
190
- <TextInput
191
- type="number"
192
- value={Math.round(value.radius || config.defaultRadius || 1000)}
193
- onChange={handleRadiusChange}
194
- disabled={readOnly}
195
- min={1}
196
- max={50000}
197
- step={1}
198
- />
199
- </Stack>
200
- )}
201
-
202
- <Box>
203
- <Grid columns={value ? 2 : 1} gap={3}>
204
- <Button
205
- aria-describedby={ariaDescribedBy}
206
- disabled={readOnly}
207
- icon={value && EditIcon}
208
- id={id}
209
- mode="ghost"
210
- onClick={handleToggleModal}
211
- onFocus={handleFocus}
212
- padding={3}
213
- ref={inputRef}
214
- text={value ? 'Edit' : 'Set location and radius'}
215
- />
216
-
217
- {value && (
218
- <Button
219
- disabled={readOnly}
220
- icon={TrashIcon}
221
- mode="ghost"
222
- onClick={handleClear}
223
- padding={3}
224
- text="Remove"
225
- tone="critical"
226
- />
227
- )}
228
- </Grid>
229
- </Box>
230
-
231
- {modalOpen && (
232
- <Dialog
233
- header="Place the marker and set radius on the map"
234
- id={`${dialogId}_dialog`}
235
- onBlur={handleBlur}
236
- onClose={handleCloseModal}
237
- ref={dialogRef}
238
- width={1}
239
- >
240
- <DialogInnerContainer>
241
- <GoogleMapsLoadProxy config={getGeoConfig()}>
242
- {(api) => (
243
- <GeopointRadiusSelect
244
- api={api}
245
- value={value || undefined}
246
- onChange={readOnly ? undefined : handleChange}
247
- defaultLocation={config.defaultLocation}
248
- defaultRadiusZoom={config.defaultRadiusZoom}
249
- defaultRadius={config.defaultRadius}
250
- />
251
- )}
252
- </GoogleMapsLoadProxy>
253
- </DialogInnerContainer>
254
- </Dialog>
255
- )}
256
- </Stack>
257
- )
258
- }
@@ -1,193 +0,0 @@
1
- import React, {type FC, useCallback, useEffect, useRef} from 'react'
2
- import {SearchInput} from '../map/SearchInput'
3
- import {GoogleMap} from '../map/Map'
4
- import {Marker} from '../map/Marker'
5
- import type {LatLng, GeopointRadius} from '../types'
6
-
7
- const fallbackLatLng: LatLng = {lat: 40.7058254, lng: -74.1180863}
8
-
9
- // Component to sync marker drag with circle position
10
- const MarkerDragSync: FC<{
11
- api: typeof window.google.maps
12
- marker: google.maps.Marker
13
- circleRef: React.MutableRefObject<google.maps.Circle | null>
14
- isMarkerDragging: React.MutableRefObject<boolean>
15
- }> = ({api, marker, circleRef, isMarkerDragging}) => {
16
- useEffect(() => {
17
- const handleDrag = () => {
18
- isMarkerDragging.current = true
19
- }
20
-
21
- const handleDragEnd = () => {
22
- isMarkerDragging.current = false
23
- }
24
-
25
- const dragListener = api.event.addListener(marker, 'drag', handleDrag)
26
- const dragEndListener = api.event.addListener(marker, 'dragend', handleDragEnd)
27
-
28
- return () => {
29
- api.event.removeListener(dragListener)
30
- api.event.removeListener(dragEndListener)
31
- }
32
- }, [api, marker, circleRef, isMarkerDragging])
33
-
34
- return null
35
- }
36
-
37
- interface SelectProps {
38
- api: typeof window.google.maps
39
- value?: GeopointRadius
40
- onChange?: (latLng: google.maps.LatLng, radius?: number) => void
41
- defaultLocation?: LatLng
42
- defaultRadiusZoom?: number
43
- defaultRadius?: number
44
- }
45
-
46
- export const GeopointRadiusSelect: FC<SelectProps> = ({
47
- api,
48
- value,
49
- onChange,
50
- defaultLocation = {lng: 10.74609, lat: 59.91273},
51
- defaultRadiusZoom = 12,
52
- defaultRadius = 1000,
53
- }) => {
54
- const circleRef = useRef<google.maps.Circle | null>(null)
55
- const markerRef = useRef<google.maps.Marker | undefined>(undefined)
56
- const isMarkerDragging = useRef(false)
57
-
58
- const getCenter = useCallback(() => {
59
- const point: LatLng = {...fallbackLatLng, ...defaultLocation, ...value}
60
- return point
61
- }, [value, defaultLocation])
62
-
63
- const setValue = useCallback(
64
- (geoPoint: google.maps.LatLng, radius?: number) => {
65
- if (onChange) {
66
- const roundedRadius = radius ? Math.round(radius) : undefined
67
- onChange(geoPoint, roundedRadius)
68
- }
69
- },
70
- [onChange],
71
- )
72
-
73
- const handlePlaceChanged = useCallback(
74
- (place: google.maps.places.PlaceResult) => {
75
- if (!place.geometry?.location) {
76
- return
77
- }
78
- setValue(place.geometry.location, value?.radius || defaultRadius)
79
- },
80
- [setValue, value?.radius, defaultRadius],
81
- )
82
-
83
- const handleMarkerDragEnd = useCallback(
84
- (event: google.maps.MapMouseEvent) => {
85
- if (event.latLng) {
86
- // Update circle position when marker drag ends
87
- if (circleRef.current) {
88
- circleRef.current.setCenter(event.latLng)
89
- }
90
- setValue(event.latLng, value?.radius || defaultRadius)
91
- }
92
- },
93
- [setValue, value?.radius, defaultRadius],
94
- )
95
-
96
- const handleMapClick = useCallback(
97
- (event: google.maps.MapMouseEvent) => {
98
- if (event.latLng) {
99
- setValue(event.latLng, value?.radius || defaultRadius)
100
- }
101
- },
102
- [setValue, value?.radius, defaultRadius],
103
- )
104
-
105
- // Create or update circle when value changes
106
- useEffect(() => {
107
- if (value && circleRef.current) {
108
- circleRef.current.setCenter({lat: value.lat, lng: value.lng})
109
- circleRef.current.setRadius(value.radius)
110
- }
111
- }, [value])
112
-
113
- return (
114
- <GoogleMap
115
- api={api}
116
- location={getCenter()}
117
- onClick={handleMapClick}
118
- defaultZoom={defaultRadiusZoom}
119
- >
120
- {(map) => {
121
- // Create circle if it doesn't exist and we have a value
122
- if (value && !circleRef.current) {
123
- circleRef.current = new api.Circle({
124
- map,
125
- center: {lat: value.lat, lng: value.lng},
126
- radius: value.radius,
127
- fillColor: '#4285F4',
128
- fillOpacity: 0.2,
129
- strokeColor: '#4285F4',
130
- strokeOpacity: 0.8,
131
- strokeWeight: 2,
132
- editable: true,
133
- })
134
-
135
- // Add event listeners for circle interactions
136
- circleRef.current.addListener('center_changed', () => {
137
- if (circleRef.current && markerRef.current && !isMarkerDragging.current) {
138
- // When circle center is dragged, move the marker to match
139
- const circleCenter = circleRef.current.getCenter()
140
- if (circleCenter) {
141
- markerRef.current.setPosition(circleCenter)
142
- }
143
- }
144
- })
145
-
146
- circleRef.current.addListener('radius_changed', () => {
147
- if (circleRef.current) {
148
- const center = circleRef.current.getCenter()
149
- const radius = circleRef.current.getRadius()
150
- if (center) {
151
- setValue(center, Math.round(radius))
152
- }
153
- }
154
- })
155
-
156
- circleRef.current.addListener('dragend', () => {
157
- if (circleRef.current) {
158
- const center = circleRef.current.getCenter()
159
- const radius = circleRef.current.getRadius()
160
- if (center) {
161
- setValue(center, Math.round(radius))
162
- }
163
- }
164
- })
165
- }
166
-
167
- return (
168
- <>
169
- <SearchInput api={api} map={map} onChange={handlePlaceChanged} />
170
- {value && (
171
- <Marker
172
- api={api}
173
- map={map}
174
- position={value}
175
- onMove={onChange ? handleMarkerDragEnd : undefined}
176
- markerRef={markerRef}
177
- />
178
- )}
179
- {/* Add drag event listener to marker for circle sync */}
180
- {value && markerRef.current && (
181
- <MarkerDragSync
182
- api={api}
183
- marker={markerRef.current}
184
- circleRef={circleRef}
185
- isMarkerDragging={isMarkerDragging}
186
- />
187
- )}
188
- </>
189
- )
190
- }}
191
- </GoogleMap>
192
- )
193
- }
@@ -1,79 +0,0 @@
1
- import {type FC, useCallback} from 'react'
2
- import {SearchInput} from '../map/SearchInput'
3
- import {GoogleMap} from '../map/Map'
4
- import {Marker} from '../map/Marker'
5
- import type {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 const GeopointSelect: FC<SelectProps> = ({
18
- api,
19
- value,
20
- onChange,
21
- defaultLocation = {lng: 10.74609, lat: 59.91273},
22
- defaultZoom = 8,
23
- }) => {
24
- const getCenter = useCallback(() => {
25
- const point: LatLng = {...fallbackLatLng, ...defaultLocation, ...value}
26
- return point
27
- }, [value, defaultLocation])
28
-
29
- const setValue = useCallback(
30
- (geoPoint: google.maps.LatLng) => {
31
- if (onChange) {
32
- onChange(geoPoint)
33
- }
34
- },
35
- [onChange],
36
- )
37
-
38
- const handlePlaceChanged = useCallback(
39
- (place: google.maps.places.PlaceResult) => {
40
- if (!place.geometry?.location) {
41
- return
42
- }
43
- setValue(place.geometry.location)
44
- },
45
- [setValue],
46
- )
47
-
48
- const handleMarkerDragEnd = useCallback(
49
- (event: google.maps.MapMouseEvent) => {
50
- if (event.latLng) setValue(event.latLng)
51
- },
52
- [setValue],
53
- )
54
-
55
- const handleMapClick = useCallback(
56
- (event: google.maps.MapMouseEvent) => {
57
- if (event.latLng) setValue(event.latLng)
58
- },
59
- [setValue],
60
- )
61
-
62
- return (
63
- <GoogleMap api={api} location={getCenter()} onClick={handleMapClick} defaultZoom={defaultZoom}>
64
- {(map) => (
65
- <>
66
- <SearchInput api={api} map={map} onChange={handlePlaceChanged} />
67
- {value && (
68
- <Marker
69
- api={api}
70
- map={map}
71
- position={value}
72
- onMove={onChange ? handleMarkerDragEnd : undefined}
73
- />
74
- )}
75
- </>
76
- )}
77
- </GoogleMap>
78
- )
79
- }
@@ -1,62 +0,0 @@
1
- import {type ReactElement, useEffect, useState} from 'react'
2
- import type {GoogleMapsInputConfig} from '../types'
3
- import {AuthError, loadGoogleMapsApi} from './loadGoogleMapsApi'
4
- import {LoadError as LoadErrorView} from './LoadError'
5
-
6
- interface LoadProps {
7
- children: (api: typeof window.google.maps) => ReactElement
8
- config: GoogleMapsInputConfig
9
- }
10
-
11
- const browserLocale = (typeof window !== 'undefined' && window.navigator.language) || 'en'
12
-
13
- type LoadState =
14
- | {
15
- type: 'loading'
16
- }
17
- | {
18
- type: 'loaded'
19
- api: typeof window.google.maps
20
- }
21
- | {
22
- type: 'error'
23
- error: {type: 'loadError' | 'authError'; message: string}
24
- }
25
-
26
- function useLoadGoogleMapsApi(config: {defaultLocale?: string; apiKey: string}): LoadState {
27
- const locale = config.defaultLocale || browserLocale || 'en-US'
28
-
29
- const [state, setState] = useState<LoadState>({type: 'loading'})
30
-
31
- useEffect(() => {
32
- if (typeof window === 'undefined') {
33
- return
34
- }
35
-
36
- loadGoogleMapsApi({locale, apiKey: config.apiKey}).then(
37
- (api) => setState({type: 'loaded', api}),
38
- (err) =>
39
- setState({
40
- type: 'error',
41
- error: {type: err instanceof AuthError ? 'authError' : 'loadError', message: err.message},
42
- }),
43
- )
44
- }, [locale, config.apiKey])
45
- return state
46
- }
47
-
48
- export function GoogleMapsLoadProxy(props: LoadProps) {
49
- const loadState = useLoadGoogleMapsApi(props.config)
50
- switch (loadState.type) {
51
- case 'error':
52
- return (
53
- <LoadErrorView error={loadState.error} isAuthError={loadState.error.type === 'authError'} />
54
- )
55
- case 'loading':
56
- return <div>Loading Google Maps API</div>
57
- case 'loaded':
58
- return props.children(loadState.api)
59
- default:
60
- return null
61
- }
62
- }
@@ -1,43 +0,0 @@
1
- import {Card, Box, Text, Code} from '@sanity/ui'
2
-
3
- type Props = {error: {message?: string}; isAuthError: false} | {isAuthError: true}
4
-
5
- export function LoadError(props: Props) {
6
- return (
7
- <Card tone="critical" radius={1}>
8
- <Box as="header" paddingX={4} paddingTop={4} paddingBottom={1}>
9
- <Text as="h2" weight="bold">
10
- Google Maps failed to load
11
- </Text>
12
- </Box>
13
-
14
- <Box paddingX={4} paddingTop={4} paddingBottom={1}>
15
- {props.isAuthError ? (
16
- <AuthError />
17
- ) : (
18
- <>
19
- <Text as="h3">Error details:</Text>
20
- <pre>
21
- <Code size={1}>{'error' in props && props.error?.message}</Code>
22
- </pre>
23
- </>
24
- )}
25
- </Box>
26
- </Card>
27
- )
28
- }
29
-
30
- function AuthError() {
31
- return (
32
- <Text>
33
- <p>The error appears to be related to authentication</p>
34
- <p>Common causes include:</p>
35
- <ul>
36
- <li>Incorrect API key</li>
37
- <li>Referer not allowed</li>
38
- <li>Missing authentication scope</li>
39
- </ul>
40
- <p>Check the browser developer tools for more information.</p>
41
- </Text>
42
- )
43
- }