Files
leim-maps/src/components/maps/overlay/MarkerCreator.vue
2024-01-13 16:58:15 +01:00

381 lines
9.0 KiB
Vue

<script lang="ts" setup>
import type { MapMarker } from '@/types/Leaflet';
import { onClickOutside, onLongPress, useCssVar, useFocus, useLocalStorage, useMouse, useTimeout, useTimeoutFn } from '@vueuse/core'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
const markerMenu = ref<HTMLMenuElement>()
type MenuMode = "editing-marker" | "default"
/**
* Appearing behaviour
*/
const isActive = ref<Boolean>(false)
const shouldBeShown = computed(() => isActive.value)
const currentMode = ref<MenuMode>("default")
function switchMenuMode(newMode: MenuMode) {
currentMode.value = newMode
}
// const hasBeenLongPressed = ref<Boolean>(false)
// const longPressDelay = 500
// const longPressOver = 1000
// const longPressTimer = useTimeoutFn(() => {
// hasBeenLongPressed.value = false
// }, longPressOver)
onMounted(() => {
const mapRef = document.getElementById('world')
mapRef?.addEventListener('contextmenu', handleContextMenu)
// onLongPress(document.documentElement, handleLongPress, { delay: longPressDelay })
})
// function handleLongPress(e: Event) {
// hasBeenLongPressed.value = true
// longPressTimer.start()
// showMenu()
// }
function handleClickOutside(e: Event) {
// if (!hasBeenLongPressed.value) {
hideMenu()
// }
}
function handleContextMenu(e: Event) {
e.preventDefault()
showMenu()
updateMenuPosition()
}
function showMenu() {
isActive.value = true
}
function hideMenu() {
isActive.value = false
}
/**
* Positioning
*/
const mouse = useMouse()
const mouseXVar = useCssVar('--mouse-x', markerMenu)
const mouseYVar = useCssVar('--mouse-y', markerMenu)
watch( isActive, (o, n) => {
// If new value is active
if (o) {
updateMenuPosition()
}
})
function updateMenuPosition() {
updateCSSVars()
}
function updateCSSVars() {
mouseXVar.value = `${mouse.x.value.toFixed()}px`
mouseYVar.value = `${mouse.y.value.toFixed()}px`
}
/**
* Adding custom marker
*/
const markerTitle = ref<string>()
const markerTitleInput = ref<HTMLInputElement>()
const markerTitleInputError = ref<Error | null>()
// Data from localStorage
const customMarkersData = useLocalStorage('custom-markers', [])
// Refresh from localStorage
onMounted(() => {
customMarkersData.value = useLocalStorage('custom-markers', []).value
document.addEventListener('refresh-custom-markers', () => {
customMarkersData.value = useLocalStorage('custom-markers', []).value
})
})
const addCustomMarker = ref<HTMLButtonElement>();
const markerModal = ref<HTMLDialogElement>()
const markerModalOpen = ref<boolean>(false)
// Shows modal if mode changes to editing-marker
watch(currentMode, (n, o) => {
if (n === 'editing-marker') {
showMarkerModal()
} else {
hideMarkerModal()
}
})
// Closes the menu if clicked outside
onClickOutside(markerMenu, handleClickOutside, { ignore: [markerModal] })
/**
* Add Custom Marker
*/
function handleAddCustomMarker() {
// If title is empty, don't add anything
if (!markerTitle.value) return
// If marker of the same name already exists, don't add anything
if (
customMarkersData.value.find((cm: MapMarker) => cm.title === markerTitle.value)
) {
setTitleError(new Error('Ce titre est déjà utilisé par un marqueur !'))
return
}
const addCustomMarkerEvent = new CustomEvent(
`add-custom-marker`,
{
bubbles: true,
detail: { title: markerTitle.value }
}
)
addCustomMarker.value?.dispatchEvent(addCustomMarkerEvent)
hideMenu()
hideMarkerModal()
switchMenuMode('default')
markerTitle.value = ""
setTitleError(null)
}
/**
* Hide modal on cancel and resets
*/
function handleMarkerModalCancel() {
showMenu()
hideMarkerModal()
switchMenuMode('default')
}
/**
* Display marker modal
*/
function showMarkerModal() {
markerModal.value?.showModal()
markerModalOpen.value = true
switchMenuMode('editing-marker')
}
/**
* Hide marker modal
* TODO: Implement a timer for animation
*/
function hideMarkerModal() {
markerModal.value?.close()
markerModalOpen.value = false
switchMenuMode('default')
markerTitle.value = ""
setTitleError(null)
}
function setTitleError(error: Error | null) {
markerTitleInputError.value = error
}
</script>
<template>
<div>
<Transition>
<menu v-show="shouldBeShown" ref="markerMenu">
<li>
<button @click="switchMenuMode('editing-marker')">
Nouveau marqueur
</button>
</li>
</menu>
</Transition>
<dialog ref="markerModal" :class="{ 'open': markerModalOpen }" @cancel.prevent="handleMarkerModalCancel">
<form @submit.prevent="handleAddCustomMarker">
<div class="modal-head">
<h2>
Ajouter un marqueur personnel
</h2>
<p class="modal-notice">
<i class="icon ph-light ph-warning"></i>
<span>Le marqueur sera sauvegardé mais n'apparaîtra que sur votre carte !</span>
</p>
</div>
<div class="modal-body">
<div class="form-group">
<label for="marker-name">
Titre
</label>
<div class="form-input">
<input ref="markerTitleInput" type="text" name="marker-name" id="marker-name" v-model="markerTitle">
<div class="form-error" v-if="markerTitleInputError">
{{ markerTitleInputError.message }}
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button
ref="addCustomMarker"
type="submit"
class="btn btn-secondary"
:disabled="!markerTitle"
>
<span>Créer</span>
</button>
</div>
</form>
<div class="modal-actions">
<button class="btn btn-danger btn-icon btn-shadow" @click="hideMarkerModal">
<i class="ph-light ph-x"></i>
</button>
</div>
</dialog>
</div>
</template>
<style lang="scss" scoped>
menu {
--mouse-y: 0px;
--mouse-x: 0px;
position: absolute;
padding-block: .25rem;
top: calc(var(--mouse-y) - .5rem);
left: calc(var(--mouse-x) + .75rem);
background-color: var(--white);
border: 1px solid var(--slate-400);
border-radius: .3rem;
box-shadow: rgba(0, 0, 0, 0.3) 0px 4px 12px;
z-index: 1000;
transition-property: top, left;
transition-duration: .16s;
transition-timing-function: cubic-bezier(0.785, 0.135, 0.15, 0.86);
li {
a, button {
display: block;
padding: .33rem 1rem;
font-size: .77em;
cursor: pointer;
&:hover,
&:focus-within {
background-color: var(--slate-100);
}
}
}
}
dialog {
position: fixed;
max-width: calc(100% - 6rem);
padding: 1.5rem 2rem;
z-index: 9999;
background-color: var(--white);
border: 1px solid var(--slate-400);
border-radius: .75rem;
box-shadow: rgba(0, 0, 0, 0.3) 0px 4px 12px;
overflow: visible;
transform: translateY(1rem);
transition: visibility 0s ease-out .3s, opacity .3s ease-out, transform .3s ease-out;
@media screen and (width >= 900px) {
width: 50%;
}
&,
&::backdrop {
visibility: hidden;
opacity: 0;
}
&::backdrop {
backdrop-filter: blur(1px);
}
&.open {
transform: translateY(0);
}
&.open,
&.open::backdrop {
transition-delay: 0s;
}
&.open,
&.open::backdrop {
visibility: visible;
opacity: 1;
}
.modal-head {
margin-bottom: .75rem;
h2 {
font-size: 1.4em;
}
.modal-notice {
margin-top: .2rem;
display: flex;
align-items: center;
gap: .15rem;
opacity: .85;
font-size: .8em;
.icon {
font-size: 1.3em;
}
}
}
.modal-body {
> * + * {
margin-top: .5em;
}
}
.modal-footer {
margin-top: 1rem;
}
.modal-actions {
display: flex;
gap: 1rem;
position: absolute;
top: -0.8rem;
right: 0;
padding-right: .75rem;
}
}
/**
* ANIMATIONS
*/
.v-enter-active,
.v-leave-active {
transition: all .16s cubic-bezier(0.445, 0.05, 0.55, 0.95);
}
.v-enter-from,
.v-leave-to {
transform: translateY(.5rem);
opacity: 0;
}
</style>