@nuxt/scripts 0.7.1 → 0.7.2

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