@nuxt/scripts 0.10.5 → 0.11.0
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.
- package/README.md +71 -71
- package/dist/client/200.html +10 -10
- package/dist/client/404.html +10 -10
- package/dist/client/_nuxt/{BOGJUGGn.js → BFM-Ncmx.js} +1 -1
- package/dist/client/_nuxt/{Bso10INu.js → BSA2bgdb.js} +1 -1
- package/dist/client/_nuxt/CtQNcfGE.js +1 -0
- package/dist/client/_nuxt/DuCkB5R-.js +21 -0
- package/dist/client/_nuxt/builds/latest.json +1 -1
- package/dist/client/_nuxt/builds/meta/e03354ab-f2ce-4d6b-8542-7b38262b3671.json +1 -0
- package/dist/client/_nuxt/{entry.CJckMUzn.css → entry.ipQkUTQD.css} +1 -1
- package/dist/client/_nuxt/error-404.BdjopNsg.css +1 -0
- package/dist/client/_nuxt/error-500.Bd7Z7Q7I.css +1 -0
- package/dist/client/index.html +10 -10
- package/dist/module.json +2 -2
- package/dist/module.mjs +7 -13
- package/dist/runtime/components/ScriptAriaLoadingIndicator.vue +5 -5
- package/dist/runtime/components/ScriptCarbonAds.vue +83 -83
- package/dist/runtime/components/ScriptCrisp.vue +94 -94
- package/dist/runtime/components/ScriptGoogleAdsense.vue +93 -93
- package/dist/runtime/components/ScriptGoogleMaps.vue +469 -469
- package/dist/runtime/components/ScriptIntercom.vue +103 -103
- package/dist/runtime/components/ScriptLemonSqueezy.vue +52 -52
- package/dist/runtime/components/ScriptLoadingIndicator.vue +22 -22
- package/dist/runtime/components/ScriptStripePricingTable.vue +74 -74
- package/dist/runtime/components/ScriptVimeoPlayer.vue +289 -289
- package/dist/runtime/components/ScriptYouTubePlayer.vue +215 -215
- package/dist/runtime/composables/useScript.d.ts +8 -4
- package/dist/runtime/composables/useScript.js +10 -59
- package/dist/runtime/registry/google-analytics.js +0 -3
- package/dist/runtime/registry/google-maps.d.ts +4 -10
- package/dist/runtime/registry/google-tag-manager.js +0 -3
- package/dist/runtime/registry/matomo-analytics.js +0 -4
- package/dist/runtime/registry/segment.js +0 -18
- package/dist/runtime/types.d.ts +2 -2
- package/dist/runtime/utils.js +1 -1
- package/package.json +32 -35
- package/dist/client/_nuxt/BqRljlc8.js +0 -21
- package/dist/client/_nuxt/CgFBdOe6.js +0 -1
- package/dist/client/_nuxt/builds/meta/a79d2e11-f9af-444c-b10e-0675b6cf7f55.json +0 -1
- package/dist/client/_nuxt/error-404.DrjQUyCj.css +0 -1
- package/dist/client/_nuxt/error-500.CIXIBnf9.css +0 -1
|
@@ -1,469 +1,469 @@
|
|
|
1
|
-
<script lang="ts" setup>
|
|
2
|
-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
3
|
-
// @ts-nocheck
|
|
4
|
-
|
|
5
|
-
/// <reference types="google.maps" />
|
|
6
|
-
import { computed, onBeforeUnmount, onMounted, ref, watch, toRaw } from 'vue'
|
|
7
|
-
import type { HTMLAttributes, ImgHTMLAttributes, Ref, ReservedProps } from 'vue'
|
|
8
|
-
import { withQuery } from 'ufo'
|
|
9
|
-
import type { QueryObject } from 'ufo'
|
|
10
|
-
import { defu } from 'defu'
|
|
11
|
-
import { hash } from 'ohash'
|
|
12
|
-
import { useHead } from '@unhead/vue'
|
|
13
|
-
import type { ElementScriptTrigger } from '../types'
|
|
14
|
-
import { scriptRuntimeConfig } from '../utils'
|
|
15
|
-
import { useScriptTriggerElement } from '../composables/useScriptTriggerElement'
|
|
16
|
-
import { useScriptGoogleMaps } from '../registry/google-maps'
|
|
17
|
-
import ScriptAriaLoadingIndicator from './ScriptAriaLoadingIndicator.vue'
|
|
18
|
-
|
|
19
|
-
interface PlaceholderOptions {
|
|
20
|
-
width?: string | number
|
|
21
|
-
height?: string | number
|
|
22
|
-
center?: string
|
|
23
|
-
zoom?: number
|
|
24
|
-
size?: string
|
|
25
|
-
scale?: number
|
|
26
|
-
format?: 'png' | 'jpg' | 'gif' | 'png8' | 'png32' | 'jpg-baseline'
|
|
27
|
-
maptype?: 'roadmap' | 'satellite' | 'terrain' | 'hybrid'
|
|
28
|
-
language?: string
|
|
29
|
-
region?: string
|
|
30
|
-
markers?: string
|
|
31
|
-
path?: string
|
|
32
|
-
visible?: string
|
|
33
|
-
style?: string
|
|
34
|
-
map_id?: string
|
|
35
|
-
key?: string
|
|
36
|
-
signature?: string
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const props = withDefaults(defineProps<{
|
|
40
|
-
/**
|
|
41
|
-
* Defines the trigger event to load the script.
|
|
42
|
-
*/
|
|
43
|
-
trigger?: ElementScriptTrigger
|
|
44
|
-
/**
|
|
45
|
-
* Is Google Maps being rendered above the fold?
|
|
46
|
-
* This will load the placeholder image with higher priority.
|
|
47
|
-
*/
|
|
48
|
-
aboveTheFold?: boolean
|
|
49
|
-
/**
|
|
50
|
-
* Defines the Google Maps API key. Must have access to the Static Maps API as well.
|
|
51
|
-
*/
|
|
52
|
-
apiKey?: string
|
|
53
|
-
/**
|
|
54
|
-
* A latitude / longitude of where to focus the map.
|
|
55
|
-
*/
|
|
56
|
-
center?: google.maps.LatLng | google.maps.LatLngLiteral | `${string},${string}`
|
|
57
|
-
/**
|
|
58
|
-
* Should a marker be displayed on the map where the centre is.
|
|
59
|
-
*/
|
|
60
|
-
centerMarker?: boolean
|
|
61
|
-
/**
|
|
62
|
-
* Options for the map.
|
|
63
|
-
*/
|
|
64
|
-
mapOptions?: google.maps.MapOptions
|
|
65
|
-
/**
|
|
66
|
-
* Defines the width of the map.
|
|
67
|
-
*/
|
|
68
|
-
width?: number | string
|
|
69
|
-
/**
|
|
70
|
-
* Defines the height of the map
|
|
71
|
-
*/
|
|
72
|
-
height?: number | string
|
|
73
|
-
/**
|
|
74
|
-
* Customize the placeholder image attributes.
|
|
75
|
-
*
|
|
76
|
-
* @see https://developers.google.com/maps/documentation/maps-static/start.
|
|
77
|
-
*/
|
|
78
|
-
placeholderOptions?: PlaceholderOptions
|
|
79
|
-
/**
|
|
80
|
-
* Customize the placeholder image attributes.
|
|
81
|
-
*/
|
|
82
|
-
placeholderAttrs?: ImgHTMLAttributes & ReservedProps & Record<string, unknown>
|
|
83
|
-
/**
|
|
84
|
-
* Customize the root element attributes.
|
|
85
|
-
*/
|
|
86
|
-
rootAttrs?: HTMLAttributes & ReservedProps & Record<string, unknown>
|
|
87
|
-
/**
|
|
88
|
-
* Extra Markers to add to the map.
|
|
89
|
-
*/
|
|
90
|
-
markers?: (`${string},${string}` | google.maps.marker.AdvancedMarkerElementOptions)[]
|
|
91
|
-
}>(), {
|
|
92
|
-
// @ts-expect-error untyped
|
|
93
|
-
trigger: ['mouseenter', 'mouseover', 'mousedown'],
|
|
94
|
-
width: 640,
|
|
95
|
-
height: 400,
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
const emits = defineEmits<{
|
|
99
|
-
// our emit
|
|
100
|
-
ready: [e: typeof googleMaps]
|
|
101
|
-
error: []
|
|
102
|
-
}>()
|
|
103
|
-
|
|
104
|
-
const apiKey = props.apiKey || scriptRuntimeConfig('googleMaps')?.apiKey
|
|
105
|
-
|
|
106
|
-
const mapsApi = ref<typeof google.maps | undefined>()
|
|
107
|
-
|
|
108
|
-
if (import.meta.dev && !apiKey)
|
|
109
|
-
throw new Error('GoogleMaps requires an API key. Please provide `apiKey` on the <ScriptGoogleMaps> or globally via `runtimeConfig.public.scripts.googleMaps.apiKey`.')
|
|
110
|
-
|
|
111
|
-
// TODO allow a null center may need to be resolved via an API function
|
|
112
|
-
|
|
113
|
-
const rootEl = ref<HTMLElement>()
|
|
114
|
-
const mapEl = ref<HTMLElement>()
|
|
115
|
-
|
|
116
|
-
const centerOverride = ref()
|
|
117
|
-
|
|
118
|
-
const trigger = useScriptTriggerElement({ trigger: props.trigger, el: rootEl })
|
|
119
|
-
const { load, status, onLoaded } = useScriptGoogleMaps({
|
|
120
|
-
apiKey: props.apiKey,
|
|
121
|
-
scriptOptions: {
|
|
122
|
-
trigger,
|
|
123
|
-
},
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
const options = computed(() => {
|
|
127
|
-
return defu({ center: centerOverride.value }, props.mapOptions, {
|
|
128
|
-
center: props.center,
|
|
129
|
-
zoom: 15,
|
|
130
|
-
mapId: props.mapOptions?.styles ? undefined : 'map',
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
const ready = ref(false)
|
|
134
|
-
|
|
135
|
-
const map: Ref<google.maps.Map | undefined> = ref()
|
|
136
|
-
const mapMarkers: Ref<Map<string, Promise<google.maps.marker.AdvancedMarkerElement>>> = ref(new Map())
|
|
137
|
-
|
|
138
|
-
function isLocationQuery(s: string | any) {
|
|
139
|
-
return typeof s === 'string' && (s.split(',').length > 2 || s.includes('+'))
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function resetMapMarkerMap(_marker: google.maps.marker.AdvancedMarkerElement | Promise<google.maps.marker.AdvancedMarkerElement>) {
|
|
143
|
-
// eslint-disable-next-line no-async-promise-executor
|
|
144
|
-
return new Promise<void>(async (resolve) => {
|
|
145
|
-
const marker = _marker instanceof Promise ? await _marker : _marker
|
|
146
|
-
if (marker) {
|
|
147
|
-
// @ts-expect-error broken type
|
|
148
|
-
marker.setMap(null)
|
|
149
|
-
}
|
|
150
|
-
resolve()
|
|
151
|
-
})
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
async function createAdvancedMapMarker(_options?: google.maps.marker.AdvancedMarkerElementOptions | `${string},${string}`) {
|
|
155
|
-
if (!_options)
|
|
156
|
-
return
|
|
157
|
-
const key = hash(_options)
|
|
158
|
-
if (mapMarkers.value.has(key))
|
|
159
|
-
return mapMarkers.value.get(key)
|
|
160
|
-
// eslint-disable-next-line no-async-promise-executor
|
|
161
|
-
const p = new Promise<google.maps.marker.AdvancedMarkerElement>(async (resolve) => {
|
|
162
|
-
const lib = await importLibrary('marker')
|
|
163
|
-
const options = typeof _options === 'string'
|
|
164
|
-
? {
|
|
165
|
-
position: {
|
|
166
|
-
lat: Number.parseFloat(_options.split(',')[0] || '0'),
|
|
167
|
-
lng: Number.parseFloat(_options.split(',')[1] || '0'),
|
|
168
|
-
},
|
|
169
|
-
}
|
|
170
|
-
: _options
|
|
171
|
-
const mapMarkerOptions = defu(toRaw(options), {
|
|
172
|
-
map: toRaw(map.value!),
|
|
173
|
-
// @ts-expect-error unified API for maps and markers
|
|
174
|
-
position: options.location,
|
|
175
|
-
})
|
|
176
|
-
resolve(new lib.AdvancedMarkerElement(mapMarkerOptions))
|
|
177
|
-
})
|
|
178
|
-
mapMarkers.value.set(key, p)
|
|
179
|
-
return p
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const queryToLatLngCache = new Map<string, google.maps.LatLng>()
|
|
183
|
-
|
|
184
|
-
async function resolveQueryToLatLang(query: string) {
|
|
185
|
-
if (query && typeof query === 'object')
|
|
186
|
-
return Promise.resolve(query)
|
|
187
|
-
if (queryToLatLngCache.has(query)) {
|
|
188
|
-
return Promise.resolve(queryToLatLngCache.get(query))
|
|
189
|
-
}
|
|
190
|
-
// only if the query is a string we need to do a lookup
|
|
191
|
-
// eslint-disable-next-line no-async-promise-executor
|
|
192
|
-
return new Promise<google.maps.LatLng>(async (resolve, reject) => {
|
|
193
|
-
if (!mapsApi.value) {
|
|
194
|
-
await load()
|
|
195
|
-
// await new promise, watch until mapsApi is set
|
|
196
|
-
await new Promise<void>((resolve) => {
|
|
197
|
-
const _ = watch(mapsApi, () => {
|
|
198
|
-
_()
|
|
199
|
-
resolve()
|
|
200
|
-
})
|
|
201
|
-
})
|
|
202
|
-
}
|
|
203
|
-
const placesService = new mapsApi.value!.places.PlacesService(map.value!)
|
|
204
|
-
placesService.findPlaceFromQuery({
|
|
205
|
-
query,
|
|
206
|
-
fields: ['name', 'geometry'],
|
|
207
|
-
}, (results, status) => {
|
|
208
|
-
if (status === 'OK' && results?.[0]?.geometry?.location)
|
|
209
|
-
return resolve(results[0].geometry.location)
|
|
210
|
-
return reject(new Error(`No location found for ${query}`))
|
|
211
|
-
})
|
|
212
|
-
}).then((res) => {
|
|
213
|
-
queryToLatLngCache.set(query, res)
|
|
214
|
-
return res
|
|
215
|
-
})
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
const libraries = new Map<string, any>()
|
|
219
|
-
|
|
220
|
-
function importLibrary(key: 'marker'): Promise<google.maps.MarkerLibrary>
|
|
221
|
-
function importLibrary(key: 'places'): Promise<google.maps.PlacesLibrary>
|
|
222
|
-
function importLibrary(key: 'geometry'): Promise<google.maps.GeometryLibrary>
|
|
223
|
-
function importLibrary(key: 'drawing'): Promise<google.maps.DrawingLibrary>
|
|
224
|
-
function importLibrary(key: 'visualization'): Promise<google.maps.VisualizationLibrary>
|
|
225
|
-
function importLibrary(key: string): Promise<any>
|
|
226
|
-
function importLibrary<T>(key: string): Promise<T> {
|
|
227
|
-
if (libraries.has(key))
|
|
228
|
-
return libraries.get(key)
|
|
229
|
-
const p = mapsApi.value?.importLibrary(key) || new Promise((resolve) => {
|
|
230
|
-
const stop = watch(mapsApi, (api) => {
|
|
231
|
-
if (api) {
|
|
232
|
-
const p = api.importLibrary(key)
|
|
233
|
-
resolve(p)
|
|
234
|
-
stop()
|
|
235
|
-
}
|
|
236
|
-
}, { immediate: true })
|
|
237
|
-
})
|
|
238
|
-
libraries.set(key, p)
|
|
239
|
-
return p as any as Promise<T>
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
const googleMaps = {
|
|
243
|
-
googleMaps: mapsApi,
|
|
244
|
-
map,
|
|
245
|
-
createAdvancedMapMarker,
|
|
246
|
-
resolveQueryToLatLang,
|
|
247
|
-
importLibrary,
|
|
248
|
-
} as const
|
|
249
|
-
|
|
250
|
-
defineExpose(googleMaps)
|
|
251
|
-
|
|
252
|
-
onMounted(() => {
|
|
253
|
-
watch(ready, (v) => {
|
|
254
|
-
if (v) {
|
|
255
|
-
emits('ready', googleMaps)
|
|
256
|
-
}
|
|
257
|
-
})
|
|
258
|
-
watch(status, (v) => {
|
|
259
|
-
if (v === 'error') {
|
|
260
|
-
emits('error')
|
|
261
|
-
}
|
|
262
|
-
})
|
|
263
|
-
watch(options, () => {
|
|
264
|
-
map.value?.setOptions(options.value)
|
|
265
|
-
})
|
|
266
|
-
watch([() => props.markers, map], async () => {
|
|
267
|
-
if (!map.value) {
|
|
268
|
-
return
|
|
269
|
-
}
|
|
270
|
-
// mapMarkers is a map where we hash the next array entry as the map key
|
|
271
|
-
// we need to do a diff to see what we remove or add
|
|
272
|
-
const nextMap = new Map((props.markers || []).map(m => [hash(m), m]))
|
|
273
|
-
// compare idsToMatch in nextMap, if we're missing an id, we need to remove it
|
|
274
|
-
const toRemove = new Set([
|
|
275
|
-
...mapMarkers.value.keys(),
|
|
276
|
-
].filter(k => !nextMap.has(k)))
|
|
277
|
-
// compare to existing
|
|
278
|
-
const toAdd = new Set([...nextMap.keys()].filter(k => !mapMarkers.value.has(k)))
|
|
279
|
-
// do a diff of next and prev
|
|
280
|
-
const centerHash = hash({ position: options.value.center })
|
|
281
|
-
for (const key of toRemove) {
|
|
282
|
-
if (key === centerHash) {
|
|
283
|
-
continue
|
|
284
|
-
}
|
|
285
|
-
const marker = await mapMarkers.value.get(key)
|
|
286
|
-
if (marker) {
|
|
287
|
-
resetMapMarkerMap(marker)
|
|
288
|
-
.then(() => {
|
|
289
|
-
mapMarkers.value.delete(key)
|
|
290
|
-
})
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
for (const k of toAdd) {
|
|
294
|
-
createAdvancedMapMarker(nextMap.get(k))
|
|
295
|
-
}
|
|
296
|
-
}, {
|
|
297
|
-
immediate: true,
|
|
298
|
-
deep: true,
|
|
299
|
-
})
|
|
300
|
-
watch([() => options.value.center, ready, map], async (next, prev) => {
|
|
301
|
-
if (!map.value) {
|
|
302
|
-
return
|
|
303
|
-
}
|
|
304
|
-
let center = toRaw(next[0])
|
|
305
|
-
if (center) {
|
|
306
|
-
if (isLocationQuery(center) && ready.value) {
|
|
307
|
-
// need to resolve center from query
|
|
308
|
-
center = await resolveQueryToLatLang(center as string)
|
|
309
|
-
}
|
|
310
|
-
map.value!.setCenter(center as google.maps.LatLng)
|
|
311
|
-
if (typeof props.centerMarker === 'undefined' || props.centerMarker) {
|
|
312
|
-
if (options.value.mapId) {
|
|
313
|
-
// not allowed to use advanced markers with styles
|
|
314
|
-
return
|
|
315
|
-
}
|
|
316
|
-
if (prev[0]) {
|
|
317
|
-
const prevCenterHash = hash({ position: prev[0] })
|
|
318
|
-
if (mapMarkers.value.has(prevCenterHash)) {
|
|
319
|
-
resetMapMarkerMap(mapMarkers.value.get(prevCenterHash)!)
|
|
320
|
-
.then(() => {
|
|
321
|
-
mapMarkers.value.delete(prevCenterHash)
|
|
322
|
-
})
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
createAdvancedMapMarker({ position: center })
|
|
326
|
-
}
|
|
327
|
-
}
|
|
328
|
-
}, {
|
|
329
|
-
immediate: true,
|
|
330
|
-
})
|
|
331
|
-
onLoaded(async (instance) => {
|
|
332
|
-
mapsApi.value = await instance.maps
|
|
333
|
-
// may need to transform the center before we can init the map
|
|
334
|
-
const center = options.value.center as string
|
|
335
|
-
const _options: google.maps.MapOptions = {
|
|
336
|
-
...options.value,
|
|
337
|
-
// @ts-expect-error broken
|
|
338
|
-
center: !center || isLocationQuery(center) ? undefined : center,
|
|
339
|
-
}
|
|
340
|
-
map.value = new mapsApi.value!.Map(mapEl.value!, _options)
|
|
341
|
-
if (center && isLocationQuery(center)) {
|
|
342
|
-
// need to resolve center
|
|
343
|
-
centerOverride.value = await resolveQueryToLatLang(center)
|
|
344
|
-
map.value?.setCenter(centerOverride.value)
|
|
345
|
-
}
|
|
346
|
-
ready.value = true
|
|
347
|
-
})
|
|
348
|
-
})
|
|
349
|
-
|
|
350
|
-
if (import.meta.server) {
|
|
351
|
-
useHead({
|
|
352
|
-
link: [
|
|
353
|
-
{
|
|
354
|
-
rel: props.aboveTheFold ? 'preconnect' : 'dns-prefetch',
|
|
355
|
-
href: 'https://maps.googleapis.com',
|
|
356
|
-
},
|
|
357
|
-
],
|
|
358
|
-
})
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function transformMapStyles(styles: google.maps.MapTypeStyle[]) {
|
|
362
|
-
return styles.map((style) => {
|
|
363
|
-
const feature = style.featureType ? `feature:${style.featureType}` : ''
|
|
364
|
-
const element = style.elementType ? `element:${style.elementType}` : ''
|
|
365
|
-
const rules = (style.stylers || []).map((styler) => {
|
|
366
|
-
return Object.entries(styler).map(([key, value]) => {
|
|
367
|
-
if (key === 'color' && typeof value === 'string') {
|
|
368
|
-
value = value.replace('#', '0x')
|
|
369
|
-
}
|
|
370
|
-
return `${key}:${value}`
|
|
371
|
-
}).join('|')
|
|
372
|
-
}).filter(Boolean).join('|')
|
|
373
|
-
return [feature, element, rules].filter(Boolean).join('|')
|
|
374
|
-
}).filter(Boolean)
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
const placeholder = computed(() => {
|
|
378
|
-
let center = options.value.center
|
|
379
|
-
if (center && typeof center === 'object') {
|
|
380
|
-
center = `${center.lat},${center.lng}`
|
|
381
|
-
}
|
|
382
|
-
// @ts-expect-error lazy type
|
|
383
|
-
const placeholderOptions: PlaceholderOptions = defu(props.placeholderOptions, {
|
|
384
|
-
// only map option values
|
|
385
|
-
zoom: options.value.zoom,
|
|
386
|
-
center,
|
|
387
|
-
}, {
|
|
388
|
-
size: `${props.width}x${props.height}`,
|
|
389
|
-
key: apiKey,
|
|
390
|
-
scale: 2, // we assume a high DPI to avoid hydration issues
|
|
391
|
-
style: props.mapOptions?.styles ? transformMapStyles(props.mapOptions.styles) : undefined,
|
|
392
|
-
markers: [
|
|
393
|
-
...(props.markers || []),
|
|
394
|
-
center,
|
|
395
|
-
]
|
|
396
|
-
.filter(Boolean)
|
|
397
|
-
.map((m) => {
|
|
398
|
-
if (typeof m === 'object' && m.location) {
|
|
399
|
-
m = m.location
|
|
400
|
-
}
|
|
401
|
-
if (typeof m === 'object' && m.lat) {
|
|
402
|
-
return `${m.lat},${m.lng}`
|
|
403
|
-
}
|
|
404
|
-
return m
|
|
405
|
-
})
|
|
406
|
-
.join('|'),
|
|
407
|
-
})
|
|
408
|
-
return withQuery('https://maps.googleapis.com/maps/api/staticmap', placeholderOptions as QueryObject)
|
|
409
|
-
})
|
|
410
|
-
|
|
411
|
-
const placeholderAttrs = computed(() => {
|
|
412
|
-
return defu(props.placeholderAttrs, {
|
|
413
|
-
src: placeholder.value,
|
|
414
|
-
alt: 'Google Maps Static Map',
|
|
415
|
-
loading: props.aboveTheFold ? 'eager' : 'lazy',
|
|
416
|
-
style: {
|
|
417
|
-
cursor: 'pointer',
|
|
418
|
-
width: '100%',
|
|
419
|
-
objectFit: 'cover',
|
|
420
|
-
height: '100%',
|
|
421
|
-
},
|
|
422
|
-
} satisfies ImgHTMLAttributes)
|
|
423
|
-
})
|
|
424
|
-
|
|
425
|
-
const rootAttrs = computed(() => {
|
|
426
|
-
return defu(props.rootAttrs, {
|
|
427
|
-
'aria-busy': status.value === 'loading',
|
|
428
|
-
'aria-label': status.value === 'awaitingLoad'
|
|
429
|
-
? 'Google Maps Static Map'
|
|
430
|
-
: status.value === 'loading'
|
|
431
|
-
? 'Google Maps Map Embed Loading'
|
|
432
|
-
: 'Google Maps Embed',
|
|
433
|
-
'aria-live': 'polite',
|
|
434
|
-
'role': 'application',
|
|
435
|
-
'style': {
|
|
436
|
-
cursor: 'pointer',
|
|
437
|
-
position: 'relative',
|
|
438
|
-
maxWidth: '100%',
|
|
439
|
-
width: `${props.width}px`,
|
|
440
|
-
height: `'auto'`,
|
|
441
|
-
aspectRatio: `${props.width}/${props.height}`,
|
|
442
|
-
},
|
|
443
|
-
...(trigger instanceof Promise ? trigger.ssrAttrs || {} : {}),
|
|
444
|
-
}) as HTMLAttributes
|
|
445
|
-
})
|
|
446
|
-
|
|
447
|
-
onBeforeUnmount(async () => {
|
|
448
|
-
await Promise.all([...mapMarkers.value.entries()].map(([,marker]) => resetMapMarkerMap(marker)))
|
|
449
|
-
mapMarkers.value.clear()
|
|
450
|
-
map.value?.unbindAll()
|
|
451
|
-
map.value = undefined
|
|
452
|
-
mapEl.value?.firstChild?.remove()
|
|
453
|
-
})
|
|
454
|
-
</script>
|
|
455
|
-
|
|
456
|
-
<template>
|
|
457
|
-
<div ref="rootEl" v-bind="rootAttrs">
|
|
458
|
-
<div v-show="ready" ref="mapEl" :style="{ width: '100%', height: '100%', maxWidth: '100%' }" />
|
|
459
|
-
<slot v-if="!ready" :placeholder="placeholder" name="placeholder">
|
|
460
|
-
<img v-bind="placeholderAttrs">
|
|
461
|
-
</slot>
|
|
462
|
-
<slot v-if="status !== 'awaitingLoad' && !ready" name="loading">
|
|
463
|
-
<ScriptAriaLoadingIndicator />
|
|
464
|
-
</slot>
|
|
465
|
-
<slot v-if="status === 'awaitingLoad'" name="awaitingLoad" />
|
|
466
|
-
<slot v-else-if="status === 'error'" name="error" />
|
|
467
|
-
<slot />
|
|
468
|
-
</div>
|
|
469
|
-
</template>
|
|
1
|
+
<script lang="ts" setup>
|
|
2
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
3
|
+
// @ts-nocheck
|
|
4
|
+
|
|
5
|
+
/// <reference types="google.maps" />
|
|
6
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch, toRaw } from 'vue'
|
|
7
|
+
import type { HTMLAttributes, ImgHTMLAttributes, Ref, ReservedProps } from 'vue'
|
|
8
|
+
import { withQuery } from 'ufo'
|
|
9
|
+
import type { QueryObject } from 'ufo'
|
|
10
|
+
import { defu } from 'defu'
|
|
11
|
+
import { hash } from 'ohash'
|
|
12
|
+
import { useHead } from '@unhead/vue'
|
|
13
|
+
import type { ElementScriptTrigger } from '../types'
|
|
14
|
+
import { scriptRuntimeConfig } from '../utils'
|
|
15
|
+
import { useScriptTriggerElement } from '../composables/useScriptTriggerElement'
|
|
16
|
+
import { useScriptGoogleMaps } from '../registry/google-maps'
|
|
17
|
+
import ScriptAriaLoadingIndicator from './ScriptAriaLoadingIndicator.vue'
|
|
18
|
+
|
|
19
|
+
interface PlaceholderOptions {
|
|
20
|
+
width?: string | number
|
|
21
|
+
height?: string | number
|
|
22
|
+
center?: string
|
|
23
|
+
zoom?: number
|
|
24
|
+
size?: string
|
|
25
|
+
scale?: number
|
|
26
|
+
format?: 'png' | 'jpg' | 'gif' | 'png8' | 'png32' | 'jpg-baseline'
|
|
27
|
+
maptype?: 'roadmap' | 'satellite' | 'terrain' | 'hybrid'
|
|
28
|
+
language?: string
|
|
29
|
+
region?: string
|
|
30
|
+
markers?: string
|
|
31
|
+
path?: string
|
|
32
|
+
visible?: string
|
|
33
|
+
style?: string
|
|
34
|
+
map_id?: string
|
|
35
|
+
key?: string
|
|
36
|
+
signature?: string
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const props = withDefaults(defineProps<{
|
|
40
|
+
/**
|
|
41
|
+
* Defines the trigger event to load the script.
|
|
42
|
+
*/
|
|
43
|
+
trigger?: ElementScriptTrigger
|
|
44
|
+
/**
|
|
45
|
+
* Is Google Maps being rendered above the fold?
|
|
46
|
+
* This will load the placeholder image with higher priority.
|
|
47
|
+
*/
|
|
48
|
+
aboveTheFold?: boolean
|
|
49
|
+
/**
|
|
50
|
+
* Defines the Google Maps API key. Must have access to the Static Maps API as well.
|
|
51
|
+
*/
|
|
52
|
+
apiKey?: string
|
|
53
|
+
/**
|
|
54
|
+
* A latitude / longitude of where to focus the map.
|
|
55
|
+
*/
|
|
56
|
+
center?: google.maps.LatLng | google.maps.LatLngLiteral | `${string},${string}`
|
|
57
|
+
/**
|
|
58
|
+
* Should a marker be displayed on the map where the centre is.
|
|
59
|
+
*/
|
|
60
|
+
centerMarker?: boolean
|
|
61
|
+
/**
|
|
62
|
+
* Options for the map.
|
|
63
|
+
*/
|
|
64
|
+
mapOptions?: google.maps.MapOptions
|
|
65
|
+
/**
|
|
66
|
+
* Defines the width of the map.
|
|
67
|
+
*/
|
|
68
|
+
width?: number | string
|
|
69
|
+
/**
|
|
70
|
+
* Defines the height of the map
|
|
71
|
+
*/
|
|
72
|
+
height?: number | string
|
|
73
|
+
/**
|
|
74
|
+
* Customize the placeholder image attributes.
|
|
75
|
+
*
|
|
76
|
+
* @see https://developers.google.com/maps/documentation/maps-static/start.
|
|
77
|
+
*/
|
|
78
|
+
placeholderOptions?: PlaceholderOptions
|
|
79
|
+
/**
|
|
80
|
+
* Customize the placeholder image attributes.
|
|
81
|
+
*/
|
|
82
|
+
placeholderAttrs?: ImgHTMLAttributes & ReservedProps & Record<string, unknown>
|
|
83
|
+
/**
|
|
84
|
+
* Customize the root element attributes.
|
|
85
|
+
*/
|
|
86
|
+
rootAttrs?: HTMLAttributes & ReservedProps & Record<string, unknown>
|
|
87
|
+
/**
|
|
88
|
+
* Extra Markers to add to the map.
|
|
89
|
+
*/
|
|
90
|
+
markers?: (`${string},${string}` | google.maps.marker.AdvancedMarkerElementOptions)[]
|
|
91
|
+
}>(), {
|
|
92
|
+
// @ts-expect-error untyped
|
|
93
|
+
trigger: ['mouseenter', 'mouseover', 'mousedown'],
|
|
94
|
+
width: 640,
|
|
95
|
+
height: 400,
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const emits = defineEmits<{
|
|
99
|
+
// our emit
|
|
100
|
+
ready: [e: typeof googleMaps]
|
|
101
|
+
error: []
|
|
102
|
+
}>()
|
|
103
|
+
|
|
104
|
+
const apiKey = props.apiKey || scriptRuntimeConfig('googleMaps')?.apiKey
|
|
105
|
+
|
|
106
|
+
const mapsApi = ref<typeof google.maps | undefined>()
|
|
107
|
+
|
|
108
|
+
if (import.meta.dev && !apiKey)
|
|
109
|
+
throw new Error('GoogleMaps requires an API key. Please provide `apiKey` on the <ScriptGoogleMaps> or globally via `runtimeConfig.public.scripts.googleMaps.apiKey`.')
|
|
110
|
+
|
|
111
|
+
// TODO allow a null center may need to be resolved via an API function
|
|
112
|
+
|
|
113
|
+
const rootEl = ref<HTMLElement>()
|
|
114
|
+
const mapEl = ref<HTMLElement>()
|
|
115
|
+
|
|
116
|
+
const centerOverride = ref()
|
|
117
|
+
|
|
118
|
+
const trigger = useScriptTriggerElement({ trigger: props.trigger, el: rootEl })
|
|
119
|
+
const { load, status, onLoaded } = useScriptGoogleMaps({
|
|
120
|
+
apiKey: props.apiKey,
|
|
121
|
+
scriptOptions: {
|
|
122
|
+
trigger,
|
|
123
|
+
},
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
const options = computed(() => {
|
|
127
|
+
return defu({ center: centerOverride.value }, props.mapOptions, {
|
|
128
|
+
center: props.center,
|
|
129
|
+
zoom: 15,
|
|
130
|
+
mapId: props.mapOptions?.styles ? undefined : 'map',
|
|
131
|
+
})
|
|
132
|
+
})
|
|
133
|
+
const ready = ref(false)
|
|
134
|
+
|
|
135
|
+
const map: Ref<google.maps.Map | undefined> = ref()
|
|
136
|
+
const mapMarkers: Ref<Map<string, Promise<google.maps.marker.AdvancedMarkerElement>>> = ref(new Map())
|
|
137
|
+
|
|
138
|
+
function isLocationQuery(s: string | any) {
|
|
139
|
+
return typeof s === 'string' && (s.split(',').length > 2 || s.includes('+'))
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function resetMapMarkerMap(_marker: google.maps.marker.AdvancedMarkerElement | Promise<google.maps.marker.AdvancedMarkerElement>) {
|
|
143
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
144
|
+
return new Promise<void>(async (resolve) => {
|
|
145
|
+
const marker = _marker instanceof Promise ? await _marker : _marker
|
|
146
|
+
if (marker) {
|
|
147
|
+
// @ts-expect-error broken type
|
|
148
|
+
marker.setMap(null)
|
|
149
|
+
}
|
|
150
|
+
resolve()
|
|
151
|
+
})
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function createAdvancedMapMarker(_options?: google.maps.marker.AdvancedMarkerElementOptions | `${string},${string}`) {
|
|
155
|
+
if (!_options)
|
|
156
|
+
return
|
|
157
|
+
const key = hash(_options)
|
|
158
|
+
if (mapMarkers.value.has(key))
|
|
159
|
+
return mapMarkers.value.get(key)
|
|
160
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
161
|
+
const p = new Promise<google.maps.marker.AdvancedMarkerElement>(async (resolve) => {
|
|
162
|
+
const lib = await importLibrary('marker')
|
|
163
|
+
const options = typeof _options === 'string'
|
|
164
|
+
? {
|
|
165
|
+
position: {
|
|
166
|
+
lat: Number.parseFloat(_options.split(',')[0] || '0'),
|
|
167
|
+
lng: Number.parseFloat(_options.split(',')[1] || '0'),
|
|
168
|
+
},
|
|
169
|
+
}
|
|
170
|
+
: _options
|
|
171
|
+
const mapMarkerOptions = defu(toRaw(options), {
|
|
172
|
+
map: toRaw(map.value!),
|
|
173
|
+
// @ts-expect-error unified API for maps and markers
|
|
174
|
+
position: options.location,
|
|
175
|
+
})
|
|
176
|
+
resolve(new lib.AdvancedMarkerElement(mapMarkerOptions))
|
|
177
|
+
})
|
|
178
|
+
mapMarkers.value.set(key, p)
|
|
179
|
+
return p
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const queryToLatLngCache = new Map<string, google.maps.LatLng>()
|
|
183
|
+
|
|
184
|
+
async function resolveQueryToLatLang(query: string) {
|
|
185
|
+
if (query && typeof query === 'object')
|
|
186
|
+
return Promise.resolve(query)
|
|
187
|
+
if (queryToLatLngCache.has(query)) {
|
|
188
|
+
return Promise.resolve(queryToLatLngCache.get(query))
|
|
189
|
+
}
|
|
190
|
+
// only if the query is a string we need to do a lookup
|
|
191
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
192
|
+
return new Promise<google.maps.LatLng>(async (resolve, reject) => {
|
|
193
|
+
if (!mapsApi.value) {
|
|
194
|
+
await load()
|
|
195
|
+
// await new promise, watch until mapsApi is set
|
|
196
|
+
await new Promise<void>((resolve) => {
|
|
197
|
+
const _ = watch(mapsApi, () => {
|
|
198
|
+
_()
|
|
199
|
+
resolve()
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
const placesService = new mapsApi.value!.places.PlacesService(map.value!)
|
|
204
|
+
placesService.findPlaceFromQuery({
|
|
205
|
+
query,
|
|
206
|
+
fields: ['name', 'geometry'],
|
|
207
|
+
}, (results, status) => {
|
|
208
|
+
if (status === 'OK' && results?.[0]?.geometry?.location)
|
|
209
|
+
return resolve(results[0].geometry.location)
|
|
210
|
+
return reject(new Error(`No location found for ${query}`))
|
|
211
|
+
})
|
|
212
|
+
}).then((res) => {
|
|
213
|
+
queryToLatLngCache.set(query, res)
|
|
214
|
+
return res
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const libraries = new Map<string, any>()
|
|
219
|
+
|
|
220
|
+
function importLibrary(key: 'marker'): Promise<google.maps.MarkerLibrary>
|
|
221
|
+
function importLibrary(key: 'places'): Promise<google.maps.PlacesLibrary>
|
|
222
|
+
function importLibrary(key: 'geometry'): Promise<google.maps.GeometryLibrary>
|
|
223
|
+
function importLibrary(key: 'drawing'): Promise<google.maps.DrawingLibrary>
|
|
224
|
+
function importLibrary(key: 'visualization'): Promise<google.maps.VisualizationLibrary>
|
|
225
|
+
function importLibrary(key: string): Promise<any>
|
|
226
|
+
function importLibrary<T>(key: string): Promise<T> {
|
|
227
|
+
if (libraries.has(key))
|
|
228
|
+
return libraries.get(key)
|
|
229
|
+
const p = mapsApi.value?.importLibrary(key) || new Promise((resolve) => {
|
|
230
|
+
const stop = watch(mapsApi, (api) => {
|
|
231
|
+
if (api) {
|
|
232
|
+
const p = api.importLibrary(key)
|
|
233
|
+
resolve(p)
|
|
234
|
+
stop()
|
|
235
|
+
}
|
|
236
|
+
}, { immediate: true })
|
|
237
|
+
})
|
|
238
|
+
libraries.set(key, p)
|
|
239
|
+
return p as any as Promise<T>
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const googleMaps = {
|
|
243
|
+
googleMaps: mapsApi,
|
|
244
|
+
map,
|
|
245
|
+
createAdvancedMapMarker,
|
|
246
|
+
resolveQueryToLatLang,
|
|
247
|
+
importLibrary,
|
|
248
|
+
} as const
|
|
249
|
+
|
|
250
|
+
defineExpose(googleMaps)
|
|
251
|
+
|
|
252
|
+
onMounted(() => {
|
|
253
|
+
watch(ready, (v) => {
|
|
254
|
+
if (v) {
|
|
255
|
+
emits('ready', googleMaps)
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
watch(status, (v) => {
|
|
259
|
+
if (v === 'error') {
|
|
260
|
+
emits('error')
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
watch(options, () => {
|
|
264
|
+
map.value?.setOptions(options.value)
|
|
265
|
+
})
|
|
266
|
+
watch([() => props.markers, map], async () => {
|
|
267
|
+
if (!map.value) {
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
// mapMarkers is a map where we hash the next array entry as the map key
|
|
271
|
+
// we need to do a diff to see what we remove or add
|
|
272
|
+
const nextMap = new Map((props.markers || []).map(m => [hash(m), m]))
|
|
273
|
+
// compare idsToMatch in nextMap, if we're missing an id, we need to remove it
|
|
274
|
+
const toRemove = new Set([
|
|
275
|
+
...mapMarkers.value.keys(),
|
|
276
|
+
].filter(k => !nextMap.has(k)))
|
|
277
|
+
// compare to existing
|
|
278
|
+
const toAdd = new Set([...nextMap.keys()].filter(k => !mapMarkers.value.has(k)))
|
|
279
|
+
// do a diff of next and prev
|
|
280
|
+
const centerHash = hash({ position: options.value.center })
|
|
281
|
+
for (const key of toRemove) {
|
|
282
|
+
if (key === centerHash) {
|
|
283
|
+
continue
|
|
284
|
+
}
|
|
285
|
+
const marker = await mapMarkers.value.get(key)
|
|
286
|
+
if (marker) {
|
|
287
|
+
resetMapMarkerMap(marker)
|
|
288
|
+
.then(() => {
|
|
289
|
+
mapMarkers.value.delete(key)
|
|
290
|
+
})
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
for (const k of toAdd) {
|
|
294
|
+
createAdvancedMapMarker(nextMap.get(k))
|
|
295
|
+
}
|
|
296
|
+
}, {
|
|
297
|
+
immediate: true,
|
|
298
|
+
deep: true,
|
|
299
|
+
})
|
|
300
|
+
watch([() => options.value.center, ready, map], async (next, prev) => {
|
|
301
|
+
if (!map.value) {
|
|
302
|
+
return
|
|
303
|
+
}
|
|
304
|
+
let center = toRaw(next[0])
|
|
305
|
+
if (center) {
|
|
306
|
+
if (isLocationQuery(center) && ready.value) {
|
|
307
|
+
// need to resolve center from query
|
|
308
|
+
center = await resolveQueryToLatLang(center as string)
|
|
309
|
+
}
|
|
310
|
+
map.value!.setCenter(center as google.maps.LatLng)
|
|
311
|
+
if (typeof props.centerMarker === 'undefined' || props.centerMarker) {
|
|
312
|
+
if (options.value.mapId) {
|
|
313
|
+
// not allowed to use advanced markers with styles
|
|
314
|
+
return
|
|
315
|
+
}
|
|
316
|
+
if (prev[0]) {
|
|
317
|
+
const prevCenterHash = hash({ position: prev[0] })
|
|
318
|
+
if (mapMarkers.value.has(prevCenterHash)) {
|
|
319
|
+
resetMapMarkerMap(mapMarkers.value.get(prevCenterHash)!)
|
|
320
|
+
.then(() => {
|
|
321
|
+
mapMarkers.value.delete(prevCenterHash)
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
createAdvancedMapMarker({ position: center })
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}, {
|
|
329
|
+
immediate: true,
|
|
330
|
+
})
|
|
331
|
+
onLoaded(async (instance) => {
|
|
332
|
+
mapsApi.value = await instance.maps
|
|
333
|
+
// may need to transform the center before we can init the map
|
|
334
|
+
const center = options.value.center as string
|
|
335
|
+
const _options: google.maps.MapOptions = {
|
|
336
|
+
...options.value,
|
|
337
|
+
// @ts-expect-error broken
|
|
338
|
+
center: !center || isLocationQuery(center) ? undefined : center,
|
|
339
|
+
}
|
|
340
|
+
map.value = new mapsApi.value!.Map(mapEl.value!, _options)
|
|
341
|
+
if (center && isLocationQuery(center)) {
|
|
342
|
+
// need to resolve center
|
|
343
|
+
centerOverride.value = await resolveQueryToLatLang(center)
|
|
344
|
+
map.value?.setCenter(centerOverride.value)
|
|
345
|
+
}
|
|
346
|
+
ready.value = true
|
|
347
|
+
})
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
if (import.meta.server) {
|
|
351
|
+
useHead({
|
|
352
|
+
link: [
|
|
353
|
+
{
|
|
354
|
+
rel: props.aboveTheFold ? 'preconnect' : 'dns-prefetch',
|
|
355
|
+
href: 'https://maps.googleapis.com',
|
|
356
|
+
},
|
|
357
|
+
],
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function transformMapStyles(styles: google.maps.MapTypeStyle[]) {
|
|
362
|
+
return styles.map((style) => {
|
|
363
|
+
const feature = style.featureType ? `feature:${style.featureType}` : ''
|
|
364
|
+
const element = style.elementType ? `element:${style.elementType}` : ''
|
|
365
|
+
const rules = (style.stylers || []).map((styler) => {
|
|
366
|
+
return Object.entries(styler).map(([key, value]) => {
|
|
367
|
+
if (key === 'color' && typeof value === 'string') {
|
|
368
|
+
value = value.replace('#', '0x')
|
|
369
|
+
}
|
|
370
|
+
return `${key}:${value}`
|
|
371
|
+
}).join('|')
|
|
372
|
+
}).filter(Boolean).join('|')
|
|
373
|
+
return [feature, element, rules].filter(Boolean).join('|')
|
|
374
|
+
}).filter(Boolean)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const placeholder = computed(() => {
|
|
378
|
+
let center = options.value.center
|
|
379
|
+
if (center && typeof center === 'object') {
|
|
380
|
+
center = `${center.lat},${center.lng}`
|
|
381
|
+
}
|
|
382
|
+
// @ts-expect-error lazy type
|
|
383
|
+
const placeholderOptions: PlaceholderOptions = defu(props.placeholderOptions, {
|
|
384
|
+
// only map option values
|
|
385
|
+
zoom: options.value.zoom,
|
|
386
|
+
center,
|
|
387
|
+
}, {
|
|
388
|
+
size: `${props.width}x${props.height}`,
|
|
389
|
+
key: apiKey,
|
|
390
|
+
scale: 2, // we assume a high DPI to avoid hydration issues
|
|
391
|
+
style: props.mapOptions?.styles ? transformMapStyles(props.mapOptions.styles) : undefined,
|
|
392
|
+
markers: [
|
|
393
|
+
...(props.markers || []),
|
|
394
|
+
center,
|
|
395
|
+
]
|
|
396
|
+
.filter(Boolean)
|
|
397
|
+
.map((m) => {
|
|
398
|
+
if (typeof m === 'object' && m.location) {
|
|
399
|
+
m = m.location
|
|
400
|
+
}
|
|
401
|
+
if (typeof m === 'object' && m.lat) {
|
|
402
|
+
return `${m.lat},${m.lng}`
|
|
403
|
+
}
|
|
404
|
+
return m
|
|
405
|
+
})
|
|
406
|
+
.join('|'),
|
|
407
|
+
})
|
|
408
|
+
return withQuery('https://maps.googleapis.com/maps/api/staticmap', placeholderOptions as QueryObject)
|
|
409
|
+
})
|
|
410
|
+
|
|
411
|
+
const placeholderAttrs = computed(() => {
|
|
412
|
+
return defu(props.placeholderAttrs, {
|
|
413
|
+
src: placeholder.value,
|
|
414
|
+
alt: 'Google Maps Static Map',
|
|
415
|
+
loading: props.aboveTheFold ? 'eager' : 'lazy',
|
|
416
|
+
style: {
|
|
417
|
+
cursor: 'pointer',
|
|
418
|
+
width: '100%',
|
|
419
|
+
objectFit: 'cover',
|
|
420
|
+
height: '100%',
|
|
421
|
+
},
|
|
422
|
+
} satisfies ImgHTMLAttributes)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
const rootAttrs = computed(() => {
|
|
426
|
+
return defu(props.rootAttrs, {
|
|
427
|
+
'aria-busy': status.value === 'loading',
|
|
428
|
+
'aria-label': status.value === 'awaitingLoad'
|
|
429
|
+
? 'Google Maps Static Map'
|
|
430
|
+
: status.value === 'loading'
|
|
431
|
+
? 'Google Maps Map Embed Loading'
|
|
432
|
+
: 'Google Maps Embed',
|
|
433
|
+
'aria-live': 'polite',
|
|
434
|
+
'role': 'application',
|
|
435
|
+
'style': {
|
|
436
|
+
cursor: 'pointer',
|
|
437
|
+
position: 'relative',
|
|
438
|
+
maxWidth: '100%',
|
|
439
|
+
width: `${props.width}px`,
|
|
440
|
+
height: `'auto'`,
|
|
441
|
+
aspectRatio: `${props.width}/${props.height}`,
|
|
442
|
+
},
|
|
443
|
+
...(trigger instanceof Promise ? trigger.ssrAttrs || {} : {}),
|
|
444
|
+
}) as HTMLAttributes
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
onBeforeUnmount(async () => {
|
|
448
|
+
await Promise.all([...mapMarkers.value.entries()].map(([,marker]) => resetMapMarkerMap(marker)))
|
|
449
|
+
mapMarkers.value.clear()
|
|
450
|
+
map.value?.unbindAll()
|
|
451
|
+
map.value = undefined
|
|
452
|
+
mapEl.value?.firstChild?.remove()
|
|
453
|
+
})
|
|
454
|
+
</script>
|
|
455
|
+
|
|
456
|
+
<template>
|
|
457
|
+
<div ref="rootEl" v-bind="rootAttrs">
|
|
458
|
+
<div v-show="ready" ref="mapEl" :style="{ width: '100%', height: '100%', maxWidth: '100%' }" />
|
|
459
|
+
<slot v-if="!ready" :placeholder="placeholder" name="placeholder">
|
|
460
|
+
<img v-bind="placeholderAttrs">
|
|
461
|
+
</slot>
|
|
462
|
+
<slot v-if="status !== 'awaitingLoad' && !ready" name="loading">
|
|
463
|
+
<ScriptAriaLoadingIndicator />
|
|
464
|
+
</slot>
|
|
465
|
+
<slot v-if="status === 'awaitingLoad'" name="awaitingLoad" />
|
|
466
|
+
<slot v-else-if="status === 'error'" name="error" />
|
|
467
|
+
<slot />
|
|
468
|
+
</div>
|
|
469
|
+
</template>
|