381 lines
9.0 KiB
Vue
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> |