Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions playground/pages/third-parties/google-maps/overlay-popup.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<script setup lang="ts">
import { ref } from 'vue'
const selected = ref<number | null>(null)
const places = [
{ id: 1, name: 'Sydney Opera House', desc: 'Iconic performing arts venue on Bennelong Point.', rating: 4.7, reviews: '82k', position: { lat: -33.8568, lng: 151.2153 } },
{ id: 2, name: 'Harbour Bridge', desc: 'Steel arch bridge opened in 1932, connecting the CBD to the North Shore.', rating: 4.8, reviews: '41k', position: { lat: -33.8523, lng: 151.2108 } },
{ id: 3, name: 'Bondi Beach', desc: 'Famous crescent beach popular with surfers and sunbathers.', rating: 4.6, reviews: '29k', position: { lat: -33.8908, lng: 151.2743 } },
]
</script>

<template>
<div>
<h2 class="text-lg font-bold mb-2">
OverlayView Popup (click to toggle)
</h2>
<p class="mb-4 text-sm text-gray-600">
Click a marker to show a fully custom popup. Click again or press Γ— to close. Uses v-if for multiple markers.
</p>
<ScriptGoogleMaps
:center="{ lat: -33.8688, lng: 151.2093 }"
:zoom="12"
:width="800"
:height="500"
above-the-fold
:map-options="{ mapId: 'DEMO_MAP_ID' }"
>
<ScriptGoogleMapsAdvancedMarkerElement
v-for="place in places"
:key="place.id"
:position="place.position"
@click="selected = selected === place.id ? null : place.id"
>
<ScriptGoogleMapsOverlayView
v-if="selected === place.id"
anchor="bottom-center"
:offset="{ x: 0, y: -50 }"
>
<div class="w-64 rounded-xl bg-white p-4 shadow-lg ring-1 ring-black/5">
<div class="flex items-start justify-between gap-2">
<h3 class="text-sm font-semibold text-gray-900">
{{ place.name }}
</h3>
<button
class="shrink-0 rounded-full p-0.5 text-gray-400 hover:bg-gray-100 hover:text-gray-600"
@click.stop="selected = null"
>
<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</div>
<div class="mt-1 flex items-center gap-1 text-xs text-gray-500">
<span class="font-medium text-yellow-500">β˜… {{ place.rating }}</span>
<span>({{ place.reviews }} reviews)</span>
</div>
<p class="mt-2 text-xs leading-relaxed text-gray-600">
{{ place.desc }}
</p>
<button class="mt-3 w-full rounded-lg bg-blue-600 px-3 py-1.5 text-xs font-medium text-white hover:bg-blue-700">
View details
</button>
</div>
</ScriptGoogleMapsOverlayView>
</ScriptGoogleMapsAdvancedMarkerElement>
</ScriptGoogleMaps>
</div>
</template>
25 changes: 0 additions & 25 deletions playground/pages/third-parties/google-maps/styled.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,5 @@ const mapOptions = {
:map-options="mapOptions"
/>
</div>
<div class="button-container">
<button
class="button"
@click="changeQuery"
>
change query
</button>
</div>
</div>
</template>

<style>
.button-container {
margin: 20px 0;
}

.button {
background-color: orange;
border-radius: 8px;
padding: 4px 8px;
cursor: pointer;
}

.button:not(:last-child) {
margin-right: 8px;
}
</style>
13 changes: 12 additions & 1 deletion src/runtime/components/GoogleMaps/ScriptGoogleMaps.vue
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,18 @@ const googleMaps = {

defineExpose(googleMaps)

provide(MAP_INJECTION_KEY, { map, mapsApi })
// Shared InfoWindow group: only one InfoWindow open at a time within this map
let activeInfoWindow: google.maps.InfoWindow | undefined
provide(MAP_INJECTION_KEY, {
map,
mapsApi,
activateInfoWindow(iw: google.maps.InfoWindow) {
if (activeInfoWindow && activeInfoWindow !== iw) {
activeInfoWindow.close()
}
activeInfoWindow = iw
},
})

onMounted(() => {
watch(ready, (v) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,22 @@ const props = defineProps<{
options?: Omit<google.maps.marker.AdvancedMarkerElementOptions, 'map'>
}>()

const emit = defineEmits<{
(event: typeof eventsWithoutPayload[number]): void
(event: typeof eventsWithMapMouseEventPayload[number], payload: google.maps.MapMouseEvent): void
}>()

// AdvancedMarkerElement supported events only
// See https://developers.google.com/maps/documentation/javascript/reference/advanced-markers
const eventsWithoutPayload = [] as const

const eventsWithMapMouseEventPayload = [
'click',
'drag',
'dragend',
'dragstart',
] as const

const emit = defineEmits(['click', 'drag', 'dragend', 'dragstart'])
const dragEvents = ['drag', 'dragend', 'dragstart'] as const
const slots = useSlots()
const markerContent = useTemplateRef('marker-content')
const markerClustererContext = inject(MARKER_CLUSTERER_INJECTION_KEY, undefined)

// gmp-click handler for cleanup (AdvancedMarkerElement uses DOM gmp-click instead of Maps addListener click)
let gmpClickHandler: ((e: any) => void) | undefined

const advancedMarkerElement = useGoogleMapsResource<google.maps.marker.AdvancedMarkerElement>({
ready: () => !slots.content || !!markerContent.value,
async create({ mapsApi, map }) {
await mapsApi.importLibrary('marker')
const marker = new mapsApi.marker.AdvancedMarkerElement({
...props.options,
gmpClickable: true,
...(props.position ? { position: props.position } : {}),
})

Expand All @@ -48,21 +38,30 @@ const advancedMarkerElement = useGoogleMapsResource<google.maps.marker.AdvancedM
marker.content = markerContent.value
}

bindGoogleMapsEvents(marker, emit, {
noPayload: eventsWithoutPayload,
withPayload: eventsWithMapMouseEventPayload,
})

if (markerClustererContext?.markerClusterer.value) {
markerClustererContext.markerClusterer.value.addMarker(marker, true)
markerClustererContext.requestRerender()
}
else {
marker.map = map
}

// AdvancedMarkerElement: use gmp-click DOM event (addListener('click') is deprecated)
gmpClickHandler = (e: any) => emit('click', e)
marker.addEventListener('gmp-click', gmpClickHandler)

// Drag events still use Maps API addListener
bindGoogleMapsEvents(marker, emit, {
withPayload: dragEvents,
})

return marker
},
cleanup(marker, { mapsApi }) {
if (gmpClickHandler) {
marker.removeEventListener('gmp-click', gmpClickHandler)
gmpClickHandler = undefined
}
mapsApi.event.clearInstanceListeners(marker)
if (markerClustererContext?.markerClusterer.value) {
markerClustererContext.markerClusterer.value.removeMarker(marker, true)
Expand Down
46 changes: 35 additions & 11 deletions src/runtime/components/GoogleMaps/ScriptGoogleMapsInfoWindow.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { inject, useTemplateRef, watch } from 'vue'
import { bindGoogleMapsEvents } from './bindGoogleMapsEvents'
import { ADVANCED_MARKER_ELEMENT_INJECTION_KEY, MARKER_INJECTION_KEY } from './injectionKeys'
import { ADVANCED_MARKER_ELEMENT_INJECTION_KEY, MAP_INJECTION_KEY, MARKER_INJECTION_KEY } from './injectionKeys'
import { useGoogleMapsResource } from './useGoogleMapsResource'

const props = defineProps<{
Expand All @@ -24,13 +24,16 @@ const infoWindowEvents = [
'zindex_changed',
] as const

const mapContext = inject(MAP_INJECTION_KEY, undefined)
const markerContext = inject(MARKER_INJECTION_KEY, undefined)
const advancedMarkerElementContext = inject(ADVANCED_MARKER_ELEMENT_INJECTION_KEY, undefined)

const infoWindowContainer = useTemplateRef('info-window-container')

// Track click listener on parent marker so it can be removed on cleanup
let markerClickListener: google.maps.MapsEventListener | undefined
let gmpClickHandler: ((e: any) => void) | undefined
let isOpen = false

const infoWindow = useGoogleMapsResource<google.maps.InfoWindow>({
ready: () => !!infoWindowContainer.value,
Expand All @@ -40,36 +43,57 @@ const infoWindow = useGoogleMapsResource<google.maps.InfoWindow>({
...props.options,
})

// Track open state for toggle behavior
iw.addListener('closeclick', () => {
isOpen = false
})
iw.addListener('close', () => {
isOpen = false
})

bindGoogleMapsEvents(iw, emit, { noPayload: infoWindowEvents })

const toggleOpen = (anchor: any) => {
if (isOpen) {
iw.close()
isOpen = false
}
else {
mapContext?.activateInfoWindow(iw)
iw.open({ anchor, map })
isOpen = true
}
}

if (markerContext?.marker.value) {
markerClickListener = markerContext.marker.value.addListener('click', () => {
iw.open({
anchor: markerContext.marker.value,
map,
})
toggleOpen(markerContext.marker.value)
})
}
else if (advancedMarkerElementContext?.advancedMarkerElement.value) {
markerClickListener = advancedMarkerElementContext.advancedMarkerElement.value.addListener('click', () => {
iw.open({
anchor: advancedMarkerElementContext.advancedMarkerElement.value,
map,
})
})
const ame = advancedMarkerElementContext.advancedMarkerElement.value
ame.gmpClickable = true
gmpClickHandler = () => toggleOpen(ame)
ame.addEventListener('gmp-click', gmpClickHandler)
}
else {
iw.setPosition(props.options?.position)
iw.open(map)
isOpen = true
}

return iw
},
cleanup(iw, { mapsApi }) {
markerClickListener?.remove()
markerClickListener = undefined
if (gmpClickHandler && advancedMarkerElementContext?.advancedMarkerElement.value) {
advancedMarkerElementContext.advancedMarkerElement.value.removeEventListener('gmp-click', gmpClickHandler)
gmpClickHandler = undefined
}
mapsApi.event.clearInstanceListeners(iw)
iw.close()
isOpen = false
},
})

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const props = withDefaults(defineProps<{
blockMapInteraction: true,
})

const open = defineModel<boolean>('open')
const open = defineModel<boolean>('open', { default: undefined })

const markerContext = inject(MARKER_INJECTION_KEY, undefined)
const advancedMarkerElementContext = inject(ADVANCED_MARKER_ELEMENT_INJECTION_KEY, undefined)
Expand Down
2 changes: 2 additions & 0 deletions src/runtime/components/GoogleMaps/injectionKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import type { InjectionKey, Ref, ShallowRef } from 'vue'
export const MAP_INJECTION_KEY = Symbol('map') as InjectionKey<{
map: ShallowRef<google.maps.Map | undefined>
mapsApi: Ref<typeof google.maps | undefined>
/** Close the previously active InfoWindow and register a new one as active */
activateInfoWindow: (iw: google.maps.InfoWindow) => void
}>

export const ADVANCED_MARKER_ELEMENT_INJECTION_KEY = Symbol('marker') as InjectionKey<{
Expand Down
3 changes: 3 additions & 0 deletions test/unit/__mocks__/google-maps-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ export function createMockAdvancedMarkerElement() {
map: null,
content: null,
position: null,
gmpClickable: false,
addListener: vi.fn(),
addEventListener: vi.fn(),
removeEventListener: vi.fn(),
}
}

Expand Down
Loading
Loading