Make event dialog movable, allow clicks outside of it and handle dialog close when needed.

This commit is contained in:
Leo Vasanko 2025-08-23 09:42:46 -06:00
parent f54b6b971b
commit cea67a1378

View File

@ -16,6 +16,8 @@ const calendarStore = useCalendarStore()
const showDialog = ref(false)
const dialogMode = ref('create') // 'create' or 'edit'
const editingEventId = ref(null) // base event id if repeating occurrence clicked
// Track the id of an unsaved event created in the current create dialog session
const unsavedCreateId = ref(null)
const occurrenceContext = ref(null) // { baseId, occurrenceIndex, weekday, occurrenceDate }
const title = ref('')
const recurrenceEnabled = ref(false)
@ -26,6 +28,66 @@ const recurrenceOccurrences = ref(0) // 0 = unlimited
const colorId = ref(0)
const eventSaved = ref(false)
const titleInput = ref(null)
const modalRef = ref(null)
// Drag functionality
const isDragging = ref(false)
const dragOffset = ref({ x: 0, y: 0 })
const modalPosition = ref({ x: 0, y: 0 })
function startDrag(event) {
if (!modalRef.value) return
isDragging.value = true
const rect = modalRef.value.getBoundingClientRect()
dragOffset.value = {
x: event.clientX - rect.left,
y: event.clientY - rect.top,
}
// Set pointer capture for better touch handling
if (event.pointerId !== undefined) {
try {
event.target.setPointerCapture(event.pointerId)
} catch (e) {
console.warn('Could not set pointer capture:', e)
}
}
document.addEventListener('pointermove', handleDrag, { passive: false })
document.addEventListener('pointerup', stopDrag)
document.addEventListener('pointercancel', stopDrag)
event.preventDefault()
}
function handleDrag(event) {
if (!isDragging.value || !modalRef.value) return
modalPosition.value = {
x: event.clientX - dragOffset.value.x,
y: event.clientY - dragOffset.value.y,
}
event.preventDefault()
}
function stopDrag() {
isDragging.value = false
document.removeEventListener('pointermove', handleDrag)
document.removeEventListener('pointerup', stopDrag)
document.removeEventListener('pointercancel', stopDrag)
}
const modalStyle = computed(() => {
if (modalPosition.value.x !== 0 || modalPosition.value.y !== 0) {
return {
transform: 'none',
left: `${modalPosition.value.x}px`,
top: `${modalPosition.value.y}px`,
}
}
return {}
})
// Helper to get starting weekday (Sunday-first index)
function getStartingWeekday(selectionData = null) {
@ -94,6 +156,14 @@ const selectedColor = computed({
})
function openCreateDialog(selectionData = null) {
// Start a fresh create dialog; delete any prior unsaved create
if (unsavedCreateId.value && !eventSaved.value) {
if (calendarStore.events?.has(unsavedCreateId.value)) {
calendarStore.deleteEvent(unsavedCreateId.value)
}
unsavedCreateId.value = null
}
// Reset saved flag for new create
const currentSelection = selectionData || props.selection
// Convert new format to start/end for compatibility with existing logic
@ -135,6 +205,8 @@ function openCreateDialog(selectionData = null) {
recurrenceOccurrences.value === 0 ? 'for now' : String(recurrenceOccurrences.value),
repeatWeekdays: buildStoreWeekdayPattern(),
})
// Track unsaved create id until saved or discarded
unsavedCreateId.value = editingEventId.value
showDialog.value = true
@ -150,6 +222,20 @@ function openCreateDialog(selectionData = null) {
}
function openEditDialog(payload) {
// If we are switching away from an unsaved create to a DIFFERENT event, remove the temporary one.
// If user clicked the same newly created event again, keep it (no deletion, no dialog flicker).
if (
dialogMode.value === 'create' &&
unsavedCreateId.value &&
!eventSaved.value &&
payload &&
unsavedCreateId.value !== payload.id
) {
if (calendarStore.events?.has(unsavedCreateId.value)) {
calendarStore.deleteEvent(unsavedCreateId.value)
}
unsavedCreateId.value = null
}
occurrenceContext.value = null
if (!payload) return
// Payload expected: { id: baseId, instanceId, occurrenceIndex }
@ -236,10 +322,14 @@ function openEditDialog(payload) {
function closeDialog() {
showDialog.value = false
// If we were creating a new event and user cancels (didn't save), delete it
if (dialogMode.value === 'create' && editingEventId.value && !eventSaved.value) {
calendarStore.deleteEvent(editingEventId.value)
if (unsavedCreateId.value && !eventSaved.value) {
if (calendarStore.events?.has(unsavedCreateId.value)) {
calendarStore.deleteEvent(unsavedCreateId.value)
}
}
editingEventId.value = null
unsavedCreateId.value = null
}
function updateEventInStore() {
if (!editingEventId.value) return
@ -260,16 +350,13 @@ function updateEventInStore() {
}
function saveEvent() {
if (editingEventId.value) {
updateEventInStore()
}
if (editingEventId.value) updateEventInStore()
eventSaved.value = true
if (dialogMode.value === 'create') {
emit('clear-selection')
// This create is now saved; clear tracking so it won't be auto-deleted
unsavedCreateId.value = null
}
if (dialogMode.value === 'create') emit('clear-selection')
closeDialog()
}
@ -304,6 +391,23 @@ watch(title, (newTitle) => {
}
})
// Watch for dialog being closed unexpectedly and cleanup unsaved events
// Removed placeholder watcher
// Watch for editingEventId changes (switching to a different event while create in progress)
// Removed editingEventId placeholder cleanup watcher
// Cleanup drag listeners on unmount
onUnmounted(() => {
if (unsavedCreateId.value && !eventSaved.value) {
if (calendarStore.events?.has(unsavedCreateId.value))
calendarStore.deleteEvent(unsavedCreateId.value)
}
document.removeEventListener('pointermove', handleDrag)
document.removeEventListener('pointerup', stopDrag)
document.removeEventListener('pointercancel', stopDrag)
})
watch([recurrenceEnabled, recurrenceInterval, recurrenceFrequency], () => {
if (editingEventId.value && showDialog.value) updateEventInStore()
})
@ -440,10 +544,9 @@ const recurrenceSummary = computed(() => {
</script>
<template>
<div class="ec-modal-backdrop" v-if="showDialog" @click.self="closeDialog">
<div class="ec-modal">
<div class="ec-modal" v-if="showDialog" ref="modalRef" :style="modalStyle">
<form class="ec-form" @submit.prevent="saveEvent">
<header class="ec-header">
<header class="ec-header" @pointerdown="startDrag">
<h2 id="ec-modal-title">{{ dialogMode === 'create' ? 'Create Event' : 'Edit Event' }}</h2>
</header>
<div class="ec-body">
@ -523,9 +626,7 @@ const recurrenceSummary = computed(() => {
<button type="button" class="ec-btn delete-btn" @click="deleteEventOne">
Delete {{ formattedOccurrenceShort }}
</button>
<button type="button" class="ec-btn delete-btn" @click="deleteEventFrom">
Rest
</button>
<button type="button" class="ec-btn delete-btn" @click="deleteEventFrom">Rest</button>
<button type="button" class="ec-btn delete-btn" @click="deleteEventAll">All</button>
</div>
</template>
@ -538,41 +639,32 @@ const recurrenceSummary = computed(() => {
</div>
</template>
<template v-else>
<button type="button" class="ec-btn delete-btn" @click="deleteEventAll">
Delete
</button>
<button type="button" class="ec-btn delete-btn" @click="deleteEventAll">Delete</button>
</template>
<button type="button" class="ec-btn close-btn" @click="closeDialog">Close</button>
</template>
</footer>
</form>
</div>
</div>
</template>
<style scoped>
/* Modal dialog */
.ec-modal-backdrop[hidden] {
display: none;
}
.ec-modal-backdrop {
position: fixed;
inset: 0;
background: color-mix(in srgb, var(--strong) 30%, transparent);
display: grid;
place-items: center;
z-index: 1000;
}
.ec-modal {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: color-mix(in srgb, var(--panel) 85%, transparent);
backdrop-filter: blur(0.1em);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
color: var(--ink);
border-radius: 0.6rem;
min-width: 320px;
max-width: min(520px, 90vw);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
border: 1px solid color-mix(in srgb, var(--muted) 40%, transparent);
z-index: 1000;
}
.ec-form {
@ -581,6 +673,13 @@ const recurrenceSummary = computed(() => {
gap: 0.75rem;
}
.ec-header {
cursor: move;
user-select: none;
padding: 0.5rem 0;
touch-action: none;
}
.ec-header h2 {
margin: 0;
font-size: 1.1rem;