Make event dialog movable, allow clicks outside of it and handle dialog close when needed.
This commit is contained in:
parent
f54b6b971b
commit
cea67a1378
@ -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,9 +322,13 @@ 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() {
|
||||
@ -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,139 +544,127 @@ const recurrenceSummary = computed(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ec-modal-backdrop" v-if="showDialog" @click.self="closeDialog">
|
||||
<div class="ec-modal">
|
||||
<form class="ec-form" @submit.prevent="saveEvent">
|
||||
<header class="ec-header">
|
||||
<h2 id="ec-modal-title">{{ dialogMode === 'create' ? 'Create Event' : 'Edit Event' }}</h2>
|
||||
</header>
|
||||
<div class="ec-body">
|
||||
<label class="ec-field">
|
||||
<span>Title</span>
|
||||
<input type="text" v-model="title" autocomplete="off" ref="titleInput" />
|
||||
<div class="ec-modal" v-if="showDialog" ref="modalRef" :style="modalStyle">
|
||||
<form class="ec-form" @submit.prevent="saveEvent">
|
||||
<header class="ec-header" @pointerdown="startDrag">
|
||||
<h2 id="ec-modal-title">{{ dialogMode === 'create' ? 'Create Event' : 'Edit Event' }}</h2>
|
||||
</header>
|
||||
<div class="ec-body">
|
||||
<label class="ec-field">
|
||||
<span>Title</span>
|
||||
<input type="text" v-model="title" autocomplete="off" ref="titleInput" />
|
||||
</label>
|
||||
<div class="ec-color-swatches">
|
||||
<label v-for="i in 8" :key="i - 1" class="swatch-label">
|
||||
<input
|
||||
class="swatch"
|
||||
:class="'event-color-' + (i - 1)"
|
||||
type="radio"
|
||||
name="colorId"
|
||||
:value="i - 1"
|
||||
v-model="selectedColor"
|
||||
/>
|
||||
</label>
|
||||
<div class="ec-color-swatches">
|
||||
<label v-for="i in 8" :key="i - 1" class="swatch-label">
|
||||
<input
|
||||
class="swatch"
|
||||
:class="'event-color-' + (i - 1)"
|
||||
type="radio"
|
||||
name="colorId"
|
||||
:value="i - 1"
|
||||
v-model="selectedColor"
|
||||
/>
|
||||
</div>
|
||||
<div class="recurrence-block">
|
||||
<div class="recurrence-header">
|
||||
<label class="switch">
|
||||
<input type="checkbox" v-model="recurrenceEnabled" />
|
||||
<span>Repeat</span>
|
||||
</label>
|
||||
<span class="recurrence-summary" v-if="recurrenceEnabled">
|
||||
{{ recurrenceSummary }}
|
||||
<template v-if="recurrenceOccurrences > 0">
|
||||
until {{ formattedFinalOccurrence }}</template
|
||||
>
|
||||
</span>
|
||||
<span class="recurrence-summary muted" v-else>Does not recur</span>
|
||||
</div>
|
||||
<div class="recurrence-block">
|
||||
<div class="recurrence-header">
|
||||
<label class="switch">
|
||||
<input type="checkbox" v-model="recurrenceEnabled" />
|
||||
<span>Repeat</span>
|
||||
</label>
|
||||
<span class="recurrence-summary" v-if="recurrenceEnabled">
|
||||
{{ recurrenceSummary }}
|
||||
<template v-if="recurrenceOccurrences > 0">
|
||||
until {{ formattedFinalOccurrence }}</template
|
||||
>
|
||||
</span>
|
||||
<span class="recurrence-summary muted" v-else>Does not recur</span>
|
||||
<div v-if="recurrenceEnabled" class="recurrence-form">
|
||||
<div class="line compact">
|
||||
<Numeric
|
||||
v-model="recurrenceInterval"
|
||||
:prefix-values="[{ value: 1, display: 'Every' }]"
|
||||
:min="2"
|
||||
number-prefix="Every "
|
||||
aria-label="Interval"
|
||||
/>
|
||||
<select v-model="recurrenceFrequency" class="freq-select">
|
||||
<option value="weeks">{{ recurrenceInterval === 1 ? 'week' : 'weeks' }}</option>
|
||||
<option value="months">
|
||||
{{ recurrenceInterval === 1 ? 'month' : 'months' }}
|
||||
</option>
|
||||
</select>
|
||||
<Numeric
|
||||
class="occ-stepper"
|
||||
v-model="recurrenceOccurrences"
|
||||
:min="2"
|
||||
:prefix-values="[{ value: 0, display: '∞' }]"
|
||||
number-postfix=" times"
|
||||
aria-label="Occurrences (0 = no end)"
|
||||
extra-class="occ"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="recurrenceEnabled" class="recurrence-form">
|
||||
<div class="line compact">
|
||||
<Numeric
|
||||
v-model="recurrenceInterval"
|
||||
:prefix-values="[{ value: 1, display: 'Every' }]"
|
||||
:min="2"
|
||||
number-prefix="Every "
|
||||
aria-label="Interval"
|
||||
/>
|
||||
<select v-model="recurrenceFrequency" class="freq-select">
|
||||
<option value="weeks">{{ recurrenceInterval === 1 ? 'week' : 'weeks' }}</option>
|
||||
<option value="months">
|
||||
{{ recurrenceInterval === 1 ? 'month' : 'months' }}
|
||||
</option>
|
||||
</select>
|
||||
<Numeric
|
||||
class="occ-stepper"
|
||||
v-model="recurrenceOccurrences"
|
||||
:min="2"
|
||||
:prefix-values="[{ value: 0, display: '∞' }]"
|
||||
number-postfix=" times"
|
||||
aria-label="Occurrences (0 = no end)"
|
||||
extra-class="occ"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="recurrenceFrequency === 'weeks'" @click.stop>
|
||||
<WeekdaySelector
|
||||
v-model="recurrenceWeekdays"
|
||||
:fallback="fallbackWeekdays"
|
||||
:first-day="calendarStore.config.first_day"
|
||||
/>
|
||||
</div>
|
||||
<div v-if="recurrenceFrequency === 'weeks'" @click.stop>
|
||||
<WeekdaySelector
|
||||
v-model="recurrenceWeekdays"
|
||||
:fallback="fallbackWeekdays"
|
||||
:first-day="calendarStore.config.first_day"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="ec-footer">
|
||||
<template v-if="dialogMode === 'create'">
|
||||
<button type="button" class="ec-btn delete-btn" @click="deleteEventAll">Delete</button>
|
||||
<button type="submit" class="ec-btn save-btn">Save</button>
|
||||
</div>
|
||||
<footer class="ec-footer">
|
||||
<template v-if="dialogMode === 'create'">
|
||||
<button type="button" class="ec-btn delete-btn" @click="deleteEventAll">Delete</button>
|
||||
<button type="submit" class="ec-btn save-btn">Save</button>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="showDeleteVariants">
|
||||
<div class="ec-delete-group">
|
||||
<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="deleteEventAll">All</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="isRepeatingBaseEdit">
|
||||
<div class="ec-delete-group">
|
||||
<button type="button" class="ec-btn delete-btn" @click="deleteEventOne">
|
||||
Delete {{ formattedOccurrenceShort }}
|
||||
</button>
|
||||
<button type="button" class="ec-btn delete-btn" @click="deleteEventAll">All</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<template v-if="showDeleteVariants">
|
||||
<div class="ec-delete-group">
|
||||
<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="deleteEventAll">All</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="isRepeatingBaseEdit">
|
||||
<div class="ec-delete-group">
|
||||
<button type="button" class="ec-btn delete-btn" @click="deleteEventOne">
|
||||
Delete {{ formattedOccurrenceShort }}
|
||||
</button>
|
||||
<button type="button" class="ec-btn delete-btn" @click="deleteEventAll">All</button>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<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>
|
||||
<button type="button" class="ec-btn delete-btn" @click="deleteEventAll">Delete</button>
|
||||
</template>
|
||||
</footer>
|
||||
</form>
|
||||
</div>
|
||||
<button type="button" class="ec-btn close-btn" @click="closeDialog">Close</button>
|
||||
</template>
|
||||
</footer>
|
||||
</form>
|
||||
</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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user