Skip to content

Commit 9e21cbd

Browse files
committed
add component
1 parent 3849eae commit 9e21cbd

20 files changed

+1940
-320
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ The `GoogleMap` component can be used to render a Google Map:
6666
};
6767
6868
const handleLoad = (map: google.maps.Map) => {
69-
// do something with the loaded map
69+
7070
};
7171
</script>
7272

package.json

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,49 @@
1818
"url": "https://github.com/skyt-a/svelte-google-maps-api.git"
1919
},
2020
"exports": {
21-
"./LoadScript.svelte": {
22-
"types": "./dist/LoadScript.svelte.d.ts",
23-
"svelte": "./dist/LoadScript.svelte"
21+
".": {
22+
"types": "./dist/index.d.ts",
23+
"svelte": "./dist/index.js"
24+
},
25+
"./APIProvider.svelte": {
26+
"types": "./dist/APIProvider.svelte.d.ts",
27+
"svelte": "./dist/APIProvider.svelte"
28+
},
29+
"./AdvancedMarker.svelte": {
30+
"types": "./dist/AdvancedMarker.svelte.d.ts",
31+
"svelte": "./dist/AdvancedMarker.svelte"
32+
},
33+
"./InfoWindow.svelte": {
34+
"types": "./dist/InfoWindow.svelte.d.ts",
35+
"svelte": "./dist/InfoWindow.svelte"
36+
},
37+
"./Polyline.svelte": {
38+
"types": "./dist/Polyline.svelte.d.ts",
39+
"svelte": "./dist/Polyline.svelte"
40+
},
41+
"./Polygon.svelte": {
42+
"types": "./dist/Polygon.svelte.d.ts",
43+
"svelte": "./dist/Polygon.svelte"
44+
},
45+
"./Circle.svelte": {
46+
"types": "./dist/Circle.svelte.d.ts",
47+
"svelte": "./dist/Circle.svelte"
48+
},
49+
"./Rectangle.svelte": {
50+
"types": "./dist/Rectangle.svelte.d.ts",
51+
"svelte": "./dist/Rectangle.svelte"
52+
},
53+
"./HeatmapLayer.svelte": {
54+
"types": "./dist/HeatmapLayer.svelte.d.ts",
55+
"svelte": "./dist/HeatmapLayer.svelte"
56+
},
57+
"./GroundOverlay.svelte": {
58+
"types": "./dist/GroundOverlay.svelte.d.ts",
59+
"svelte": "./dist/GroundOverlay.svelte"
60+
},
61+
"./DirectionsRenderer.svelte": {
62+
"types": "./dist/DirectionsRenderer.svelte.d.ts",
63+
"svelte": "./dist/DirectionsRenderer.svelte"
2464
},
2565
"./GoogleMap.svelte": {
2666
"types": "./dist/GoogleMap.svelte.d.ts",
@@ -58,7 +98,7 @@
5898
"@sveltejs/kit": "^2.20.8",
5999
"@sveltejs/package": "^2.3.11",
60100
"@sveltejs/vite-plugin-svelte": "^4.0.4",
61-
"@types/google.maps": "^3.53.5",
101+
"@types/google.maps": "^3.58.1",
62102
"@typescript-eslint/eslint-plugin": "^5.45.0",
63103
"@typescript-eslint/parser": "^5.45.0",
64104
"eslint": "^8.28.0",

pnpm-lock.yaml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/lib/APIProvider.svelte

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
<script context="module" lang="ts">
2+
import type { Library } from '$lib/types/googleMap.js';
3+
4+
type LoadStatus = 'loading' | 'loaded' | 'error';
5+
6+
export interface APIProviderContext {
7+
status: Writable<LoadStatus>;
8+
googleMapsApi: typeof google.maps | null;
9+
error: Error | null;
10+
}
11+
</script>
12+
13+
<script lang="ts">
14+
import { BROWSER as browser } from 'esm-env';
15+
import { setContext, onDestroy } from 'svelte';
16+
import { writable, type Writable } from 'svelte/store';
17+
18+
export let apiKey = '';
19+
export let libraries: Library[] = [];
20+
export let language: string | undefined = undefined;
21+
export let region: string | undefined = undefined;
22+
export let version: string | undefined = undefined;
23+
export let solutionChannel: string | undefined = undefined;
24+
25+
let googleMapsApi: typeof google.maps | null = null;
26+
let error: Error | null = null;
27+
28+
const statusStore = writable<LoadStatus>('loading');
29+
30+
const SCRIPT_ID = 'svelte-google-maps-api-script';
31+
const CALLBACK_NAME = '__svelteGoogleMapApiCallback__';
32+
33+
let scriptElement: HTMLScriptElement | null = null;
34+
35+
$: if (browser && !document.getElementById(SCRIPT_ID)) {
36+
if (!apiKey) {
37+
console.error('svelte-google-maps-api: apiKey is required for APIProvider');
38+
statusStore.set('error');
39+
error = new Error('apiKey is required');
40+
} else {
41+
statusStore.set('loading');
42+
error = null;
43+
44+
window[CALLBACK_NAME] = () => {
45+
googleMapsApi = window.google.maps;
46+
statusStore.set('loaded');
47+
48+
try {
49+
delete window[CALLBACK_NAME];
50+
} catch (e) {
51+
window[CALLBACK_NAME] = undefined;
52+
}
53+
};
54+
55+
const script = document.createElement('script');
56+
script.id = SCRIPT_ID;
57+
script.type = 'text/javascript';
58+
const params = new URLSearchParams();
59+
params.set('key', apiKey);
60+
params.set('callback', CALLBACK_NAME);
61+
if (libraries.length > 0) {
62+
params.set('libraries', libraries.sort().join(','));
63+
}
64+
if (language) params.set('language', language);
65+
if (region) params.set('region', region);
66+
if (version) params.set('v', version);
67+
if (solutionChannel) params.set('solution_channel', solutionChannel);
68+
69+
script.src = `https://maps.googleapis.com/maps/api/js?${params.toString()}`;
70+
script.async = true;
71+
script.defer = true;
72+
script.onerror = (event: Event | string) => {
73+
console.error('svelte-google-maps-api: Failed to load Google Maps API script.', event);
74+
statusStore.set('error');
75+
error = new Error(`Failed to load Google Maps script: ${event.toString()}`);
76+
77+
try {
78+
delete window[CALLBACK_NAME];
79+
} catch (e) {
80+
window[CALLBACK_NAME] = undefined;
81+
}
82+
if (scriptElement) document.head.removeChild(scriptElement);
83+
scriptElement = null;
84+
};
85+
86+
scriptElement = script;
87+
document.head.appendChild(scriptElement);
88+
}
89+
} else if (browser && document.getElementById(SCRIPT_ID) && window.google && window.google.maps) {
90+
statusStore.set('loaded');
91+
googleMapsApi = window.google.maps;
92+
}
93+
94+
$: setContext<APIProviderContext>('svelte-google-maps-api', {
95+
status: statusStore,
96+
googleMapsApi,
97+
error
98+
});
99+
100+
onDestroy(() => {
101+
if (browser) {
102+
if (typeof window[CALLBACK_NAME] !== 'undefined') {
103+
try {
104+
delete window[CALLBACK_NAME];
105+
} catch (e) {
106+
window[CALLBACK_NAME] = undefined;
107+
}
108+
}
109+
}
110+
});
111+
</script>
112+
113+
{#if $statusStore === 'loading'}
114+
<slot name="loading">
115+
<!-- Default loading state -->
116+
<!-- You can provide custom loading UI via <div slot="loading">...</div> -->
117+
</slot>
118+
{:else if $statusStore === 'loaded'}
119+
<slot />
120+
{:else if $statusStore === 'error'}
121+
<slot name="error" {error}>
122+
<!-- Default error state -->
123+
<p style="color: red;">Failed to load Google Maps: {error?.message ?? 'Unknown error'}</p>
124+
<!-- You can provide custom error UI via <div slot="error" let:error>...</div> -->
125+
</slot>
126+
{/if}

src/lib/AdvancedMarker.svelte

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<script lang="ts">
2+
import { getContext, onDestroy, onMount, tick } from 'svelte';
3+
import { BROWSER as browser } from 'esm-env';
4+
import type { APIProviderContext } from './APIProvider.svelte';
5+
6+
export let position: google.maps.LatLng | google.maps.LatLngLiteral | undefined = undefined;
7+
export let title: string | undefined = undefined;
8+
export let zIndex: number | undefined = undefined;
9+
export let element: HTMLElement | undefined = undefined;
10+
export let gmpDraggable: boolean | undefined = undefined;
11+
12+
export let onClick: ((e: google.maps.MapMouseEvent) => void) | undefined = undefined;
13+
export let onDrag: ((e: google.maps.MapMouseEvent) => void) | undefined = undefined;
14+
export let onDragEnd: ((e: google.maps.MapMouseEvent) => void) | undefined = undefined;
15+
export let onDragStart: ((e: google.maps.MapMouseEvent) => void) | undefined = undefined;
16+
export let onLoad: ((marker: google.maps.marker.AdvancedMarkerElement) => void) | undefined =
17+
undefined;
18+
export let onUnmount: ((marker: google.maps.marker.AdvancedMarkerElement) => void) | undefined =
19+
undefined;
20+
21+
let markerInstance: google.maps.marker.AdvancedMarkerElement | null = null;
22+
let contentWrapper: HTMLDivElement | null = null;
23+
let clickListener: google.maps.MapsEventListener | null = null;
24+
let dragListener: google.maps.MapsEventListener | null = null;
25+
let dragEndListener: google.maps.MapsEventListener | null = null;
26+
let dragStartListener: google.maps.MapsEventListener | null = null;
27+
28+
const { status, googleMapsApi } = getContext<APIProviderContext>('svelte-google-maps-api');
29+
const map = getContext<google.maps.Map>('map');
30+
31+
onDestroy(() => {
32+
if (markerInstance) {
33+
onUnmount?.(markerInstance);
34+
35+
if (googleMapsApi) {
36+
googleMapsApi.event.clearInstanceListeners(markerInstance);
37+
}
38+
markerInstance.map = null;
39+
markerInstance = null;
40+
}
41+
});
42+
43+
async function initializeMarker() {
44+
if (!browser || $status !== 'loaded' || !googleMapsApi || !map || markerInstance) {
45+
return;
46+
}
47+
48+
if (!googleMapsApi.marker || !googleMapsApi.marker.AdvancedMarkerElement) {
49+
console.error(
50+
'svelte-google-maps-api: AdvancedMarker requires the "marker" library to be loaded. Please include "marker" in the libraries prop of APIProvider.'
51+
);
52+
return;
53+
}
54+
55+
let markerContent: HTMLElement | null = null;
56+
if (element) {
57+
markerContent = element;
58+
} else if (contentWrapper && contentWrapper.childNodes.length > 0) {
59+
markerContent = contentWrapper;
60+
}
61+
62+
const markerOptions: google.maps.marker.AdvancedMarkerElementOptions = {
63+
map,
64+
position,
65+
title,
66+
zIndex,
67+
content: markerContent,
68+
gmpDraggable
69+
};
70+
71+
try {
72+
markerInstance = new googleMapsApi.marker.AdvancedMarkerElement(markerOptions);
73+
onLoad?.(markerInstance);
74+
} catch (error) {
75+
console.error('[AdvancedMarker] Error creating instance:', error);
76+
return;
77+
}
78+
79+
setupListeners();
80+
}
81+
82+
$: if (markerInstance && position) {
83+
markerInstance.position = position;
84+
}
85+
$: if (markerInstance && title !== undefined) {
86+
markerInstance.title = title;
87+
}
88+
$: if (markerInstance && zIndex !== undefined) {
89+
markerInstance.zIndex = zIndex;
90+
}
91+
$: if (markerInstance && gmpDraggable !== undefined) {
92+
markerInstance.gmpDraggable = gmpDraggable;
93+
}
94+
95+
function setupListeners() {
96+
if (!markerInstance || !googleMapsApi) return;
97+
98+
if (clickListener) googleMapsApi.event.removeListener(clickListener);
99+
if (dragListener) googleMapsApi.event.removeListener(dragListener);
100+
if (dragEndListener) googleMapsApi.event.removeListener(dragEndListener);
101+
if (dragStartListener) googleMapsApi.event.removeListener(dragStartListener);
102+
103+
if (onClick) {
104+
clickListener = markerInstance.addListener('click', onClick);
105+
}
106+
if (onDrag) {
107+
dragListener = markerInstance.addListener('gmp-drag', onDrag);
108+
}
109+
if (onDragEnd) {
110+
dragEndListener = markerInstance.addListener('gmp-dragend', onDragEnd);
111+
}
112+
if (onDragStart) {
113+
dragStartListener = markerInstance.addListener('gmp-dragstart', onDragStart);
114+
}
115+
}
116+
117+
$: if (markerInstance && googleMapsApi && browser) {
118+
setupListeners();
119+
}
120+
121+
$: if ($status === 'loaded' && map && !markerInstance) {
122+
tick().then(initializeMarker);
123+
}
124+
125+
export function getMarkerInstance(): google.maps.marker.AdvancedMarkerElement | null {
126+
return markerInstance;
127+
}
128+
</script>
129+
130+
<!-- Wrapper for slot content, only rendered if no element prop is provided -->
131+
{#if !element}
132+
<div bind:this={contentWrapper}>
133+
<slot />
134+
</div>
135+
{/if}
136+
137+
<!-- This component doesn't render directly to the DOM where it's used,
138+
it controls the AdvancedMarkerElement on the map -->

0 commit comments

Comments
 (0)