@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.
Files changed (41) hide show
  1. package/README.md +71 -71
  2. package/dist/client/200.html +10 -10
  3. package/dist/client/404.html +10 -10
  4. package/dist/client/_nuxt/{BOGJUGGn.js → BFM-Ncmx.js} +1 -1
  5. package/dist/client/_nuxt/{Bso10INu.js → BSA2bgdb.js} +1 -1
  6. package/dist/client/_nuxt/CtQNcfGE.js +1 -0
  7. package/dist/client/_nuxt/DuCkB5R-.js +21 -0
  8. package/dist/client/_nuxt/builds/latest.json +1 -1
  9. package/dist/client/_nuxt/builds/meta/e03354ab-f2ce-4d6b-8542-7b38262b3671.json +1 -0
  10. package/dist/client/_nuxt/{entry.CJckMUzn.css → entry.ipQkUTQD.css} +1 -1
  11. package/dist/client/_nuxt/error-404.BdjopNsg.css +1 -0
  12. package/dist/client/_nuxt/error-500.Bd7Z7Q7I.css +1 -0
  13. package/dist/client/index.html +10 -10
  14. package/dist/module.json +2 -2
  15. package/dist/module.mjs +7 -13
  16. package/dist/runtime/components/ScriptAriaLoadingIndicator.vue +5 -5
  17. package/dist/runtime/components/ScriptCarbonAds.vue +83 -83
  18. package/dist/runtime/components/ScriptCrisp.vue +94 -94
  19. package/dist/runtime/components/ScriptGoogleAdsense.vue +93 -93
  20. package/dist/runtime/components/ScriptGoogleMaps.vue +469 -469
  21. package/dist/runtime/components/ScriptIntercom.vue +103 -103
  22. package/dist/runtime/components/ScriptLemonSqueezy.vue +52 -52
  23. package/dist/runtime/components/ScriptLoadingIndicator.vue +22 -22
  24. package/dist/runtime/components/ScriptStripePricingTable.vue +74 -74
  25. package/dist/runtime/components/ScriptVimeoPlayer.vue +289 -289
  26. package/dist/runtime/components/ScriptYouTubePlayer.vue +215 -215
  27. package/dist/runtime/composables/useScript.d.ts +8 -4
  28. package/dist/runtime/composables/useScript.js +10 -59
  29. package/dist/runtime/registry/google-analytics.js +0 -3
  30. package/dist/runtime/registry/google-maps.d.ts +4 -10
  31. package/dist/runtime/registry/google-tag-manager.js +0 -3
  32. package/dist/runtime/registry/matomo-analytics.js +0 -4
  33. package/dist/runtime/registry/segment.js +0 -18
  34. package/dist/runtime/types.d.ts +2 -2
  35. package/dist/runtime/utils.js +1 -1
  36. package/package.json +32 -35
  37. package/dist/client/_nuxt/BqRljlc8.js +0 -21
  38. package/dist/client/_nuxt/CgFBdOe6.js +0 -1
  39. package/dist/client/_nuxt/builds/meta/a79d2e11-f9af-444c-b10e-0675b6cf7f55.json +0 -1
  40. package/dist/client/_nuxt/error-404.DrjQUyCj.css +0 -1
  41. 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 as any as typeof google.maps // some weird type issue here
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>