Last active
July 4, 2021 15:29
-
-
Save janosh/0bfc51769aa1969ef24f7f557b5a8b86 to your computer and use it in GitHub Desktop.
Google Maps previously used for showing Studenten bilden Schüler chapters across Germany at https://studenten-bilden-schueler.de/standorte
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script> | |
// This component uses the Google Maps Places API to turn user text input into a | |
// formatted address and lat/lng coordinates. | |
import { onMount } from 'svelte' | |
import { session } from '$app/stores' | |
export let selectHandler | |
export let placeholder = `` | |
export let required = false | |
export let name = `` | |
export let inputNode = undefined | |
let autocomplete | |
const autocompleteOptions = { | |
componentRestrictions: { country: `de` }, | |
fields: [`formatted_address`, `geometry`, `name`], | |
types: [`geocode`, `establishment`], | |
} | |
function mountInput() { | |
autocomplete = new window.google.maps.places.Autocomplete( | |
inputNode, | |
autocompleteOptions | |
) | |
autocomplete.addListener(`place_changed`, () => { | |
inputNode.value = `` | |
inputNode.focus() | |
selectHandler(autocomplete.getPlace()) | |
}) | |
} | |
onMount(() => { | |
let script = document.getElementById(`gm-js-api`) | |
if (!script) { | |
script = document.createElement(`script`) // dynamically created scripts are async by default | |
script.src = `https://maps.googleapis.com/maps/api/js?key=${$session.GOOGLE_MAPS_API_KEY}&libraries=places` | |
script.id = `gm-js-api` | |
document.head.append(script) | |
} | |
if (!window.google?.maps) script.addEventListener(`load`, mountInput) | |
else mountInput() | |
return () => window.google.maps.event.clearInstanceListeners(autocomplete) // cleanup function, avoids mem leaks | |
}) | |
</script> | |
<input bind:this={inputNode} {name} type="text" {placeholder} {required} /> | |
<style> | |
input { | |
background: var(--accentBg); | |
width: 100%; | |
text-overflow: ellipsis; | |
height: 2em; | |
margin: 1ex 0; | |
} | |
/* make autocomplete suggestions color-mode responsive */ | |
/* https://developers.google.com/maps/documentation/javascript/places-autocomplete#style-autocomplete */ | |
:global(.pac-container) { | |
background: var(--accentBg); | |
border-radius: 3pt; | |
} | |
:global(.pac-item):where(:hover, .pac-item-selected) { | |
background: var(--bodyBg); | |
} | |
:global(.pac-item span) { | |
color: var(--textColor); | |
} | |
/* hide google logo from autocomplete suggestions */ | |
:global(.pac-logo::after) { | |
display: none; | |
} | |
</style> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script> | |
import { goto, prefetch } from '$app/navigation' | |
import Map from '../components/Map.svelte' | |
export let chapters | |
// chapters can be inside the addMarkers closure because it's static | |
// if chapters might change and we wanted the markers to rerender, | |
// it would need to be part of the map's props | |
const addMarkers = (map) => { | |
chapters.forEach(({ title, slug, coords, acceptsSignups }) => { | |
const icon = { | |
path: `m0 0h24v14h-24z m12 12-3-10h4.56z`, // needed to remove the default Google Maps marker as well as to make the correct map area behind the label clickable | |
fillColor: `transparent`, | |
strokeWeight: 0, // remove this to see the SVG path, helps position it correctly behind the labels | |
anchor: new window.google.maps.Point(11, 30), | |
labelOrigin: new window.google.maps.Point(10, 10), | |
} | |
const marker = new window.google.maps.Marker({ | |
map, | |
icon, | |
position: coords, | |
label: { | |
text: title.slice(0, 2), | |
color: `white`, | |
className: `chap ` + (acceptsSignups ? `old` : `new`), | |
}, | |
title, | |
}) | |
marker.addListener(`mouseover`, () => prefetch(slug)) | |
marker.addListener(`click`, () => goto(slug)) | |
}) | |
} | |
</script> | |
<div> | |
<Map onLoad={addMarkers} /> | |
<legend> | |
<span style="background: var(--darkGreen)" /> aktiver Standort  | |
<span style="background: var(--blue)" /> in Gründung</legend> | |
</div> | |
<style> | |
div { | |
position: relative; | |
} | |
legend { | |
position: absolute; | |
right: 1ex; | |
bottom: 1ex; | |
color: white; | |
background: rgba(0, 0, 0, 0.7); | |
padding: 2pt 4pt; | |
border-radius: 3pt; | |
} | |
span { | |
width: 2ex; | |
height: 2ex; | |
display: inline-block; | |
border-radius: 5pt; | |
box-shadow: 0 0 2pt; | |
vertical-align: text-bottom; | |
} | |
:global(div.chap) { | |
opacity: 0.9; | |
padding: 0 2pt; | |
border-radius: 1pt; | |
position: relative; | |
} | |
:global(div.chap::after) { | |
content: ''; | |
position: absolute; | |
left: 50%; | |
top: 100%; | |
transform: translate(-50%); | |
border: solid; | |
border-width: 10pt 4pt; | |
box-sizing: border-box; | |
} | |
:global(div.chap.old::after) { | |
border-color: var(--darkGreen) transparent transparent transparent; | |
} | |
:global(div.chap.new::after) { | |
border-color: var(--blue) transparent transparent transparent; | |
} | |
:global(div.chap.old) { | |
background: var(--darkGreen); | |
} | |
:global(div.chap.new) { | |
background: var(--blue); | |
border-color: var(--blue) transparent transparent transparent; | |
} | |
</style> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script> | |
import { session } from '$app/stores' | |
import { onMount } from 'svelte' | |
export let map = undefined | |
export let onLoad = () => {} | |
export let mapDiv = undefined | |
export let mapProps = {} | |
export let mapDivCss = `height: 700px; max-height: 75vh; min-height: 530px;` | |
// default map props | |
mapProps = { | |
center: { lat: 51.5, lng: 10 }, | |
zoom: 6, | |
disableDefaultUI: true, | |
...mapProps, | |
} | |
const mountMap = () => (map = new window.google.maps.Map(mapDiv, mapProps)) | |
// This includes the places library to avoid needing to reimport Google Maps in | |
// AutoCompletePlace.svelte which would warn "You have included the Google Maps | |
// JavaScript API multiple times on this page. This may cause unexpected errors." | |
$: if (map && typeof onLoad === `function`) onLoad(map) | |
onMount(() => { | |
let script = document.getElementById(`gm-js-api`) | |
if (!script) { | |
script = document.createElement(`script`) // dynamically created scripts are async by default | |
script.src = `https://maps.googleapis.com/maps/api/js?key=${$session.GOOGLE_MAPS_API_KEY}&libraries=places` | |
script.id = `gm-js-api` | |
document.head.append(script) | |
} | |
if (!window.google?.maps) script.addEventListener(`load`, mountMap) | |
else mountMap() | |
}) | |
</script> | |
<div bind:this={mapDiv} style={mapDivCss} /> | |
<style> | |
div { | |
height: 700px; | |
max-height: 75vh; | |
min-height: 530px; | |
} | |
/* hide footer https://stackoverflow.com/a/22581969 */ | |
:global(.gm-style-cc) { | |
display: none; | |
} | |
</style> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script> | |
// This component uses the Google Maps Places API to turn user text input into a | |
// formatted address and lat/lng coordinates. | |
import Map from './Map.svelte' | |
import AutoCompletePlace from './AutoCompletePlace.svelte' | |
import Delete from '@svicons/material-sharp/delete.svelte' | |
export let placeholder = `` | |
export let required = false | |
export let name = `` | |
export let input = undefined // hidden input field used to store value until it is read when submitting the form | |
let places = [] | |
let markers = [] | |
let map, inputNode | |
function selectHandler(place) { | |
if (!place.geometry?.location) { | |
// User entered the name of a Place that was not suggested and | |
// pressed the Enter key, or the place details request failed. | |
window.alert(`Für '${place.name}' konnte keine Adresse gefunden werden!`) | |
return | |
} | |
const { lat, lng } = place.geometry?.location.toJSON() | |
places = [...places, { address: place.formatted_address, coords: `${lat},${lng}` }] | |
input.value = JSON.stringify(places) | |
const marker = new window.google.maps.Marker({ | |
map, | |
position: { lat, lng }, | |
label: { text: place.name.slice(0, 2), color: `white` }, | |
title: place.name, | |
}) | |
markers.push(marker) | |
const bounds = new window.google.maps.LatLngBounds() | |
for (const marker of markers) { | |
bounds.extend(marker.getPosition()) | |
} | |
map.fitBounds(bounds) | |
} | |
const deletePlace = (idx) => () => { | |
// remove place from list | |
places.splice(idx, 1) | |
places = places | |
// remove marker from map | |
markers[idx].setMap(null) | |
markers.splice(idx, 1) | |
// reset input value without removed place | |
input.value = JSON.stringify(places) | |
} | |
</script> | |
<!-- for holding the component's value in a way accessible to the DOM --> | |
<input | |
bind:this={input} | |
{required} | |
{name} | |
id={name} | |
class="hidden" | |
tabindex="-1" | |
on:focus={() => inputNode.focus()} /> | |
<!-- tabindex="-1" means skip element during tabbing, else we couldn't shift-tab out of filterInput as filterInput.focus() would jump right back --> | |
<AutoCompletePlace bind:inputNode {placeholder} {selectHandler} /> | |
<ol> | |
{#each places as place, idx} | |
<li> | |
<span>{idx + 1}</span><input | |
data-place={idx + 1} | |
type="text" | |
value={place.address} | |
disabled /> | |
<button on:click={deletePlace(idx)} type="button"> | |
<Delete style="width: 3ex; vertical-align: middle;" /></button> | |
</li> | |
{/each} | |
</ol> | |
<Map bind:map /> | |
<style> | |
ol { | |
padding: 0; | |
} | |
ol li { | |
margin: 1ex 0; | |
display: flex; | |
align-items: center; | |
} | |
ol li span { | |
font-size: 2.5ex; | |
padding: 2pt 6pt 2pt 2pt; | |
} | |
ol li span + input + button { | |
color: var(--lightGray); | |
transition: 0.3s; | |
} | |
ol li span + input + button:hover { | |
color: var(--gray); | |
} | |
input { | |
background: var(--accentBg); | |
width: 100%; | |
text-overflow: ellipsis; | |
height: 2em; | |
} | |
input.hidden { | |
border: none; | |
outline: none; | |
background: none; | |
padding: 0; | |
width: 1px; | |
/* needed to hide red shadow around required inputs in some browsers */ | |
box-shadow: none; | |
} | |
</style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment