diff --git a/src/components/BaseDialog.vue b/src/components/BaseDialog.vue index cb12888..c58dbab 100644 --- a/src/components/BaseDialog.vue +++ b/src/components/BaseDialog.vue @@ -6,6 +6,9 @@ const props = defineProps({ title: { type: String, default: '' }, draggable: { type: Boolean, default: true }, autoFocus: { type: Boolean, default: true }, + // Optional external anchor element (e.g., a day cell) to position the dialog below. + // If not provided, falls back to internal anchorRef span (original behavior). + anchorEl: { type: Object, default: null }, }) const emit = defineEmits(['update:modelValue', 'opened', 'closed', 'submit']) @@ -97,17 +100,17 @@ onMounted(() => document.addEventListener('keydown', handleKeydown)) onUnmounted(() => document.removeEventListener('keydown', handleKeydown)) function positionNearAnchor() { - if (!anchorRef.value) return - const rect = anchorRef.value.getBoundingClientRect() - // Place dialog below anchor with small vertical offset - const offsetY = 8 - // Need dialog dimensions to clamp correctly; measure current or fallback estimates + const anchor = props.anchorEl || anchorRef.value + if (!anchor) return + const rect = anchor.getBoundingClientRect() + const offsetY = 8 // vertical gap below the anchor const w = modalRef.value?.offsetWidth || dialogWidth.value || 320 const h = modalRef.value?.offsetHeight || dialogHeight.value || 200 const vw = window.innerWidth const vh = window.innerHeight let x = rect.left let y = rect.bottom + offsetY + // If anchor is wider than dialog and would overflow right edge, clamp; otherwise keep left align x = clamp(x, margin, Math.max(margin, vw - w - margin)) y = clamp(y, margin, Math.max(margin, vh - h - margin)) modalPosition.value = { x, y } @@ -132,6 +135,16 @@ watch( }, ) +// Reposition if anchorEl changes while open and user hasn't dragged dialog yet +watch( + () => props.anchorEl, + () => { + if (props.modelValue && !hasMoved.value) { + nextTick(() => positionNearAnchor()) + } + }, +) + function handleResize() { if (!props.modelValue) return // Re-clamp current position, and if not moved recalc near anchor diff --git a/src/components/EventDialog.vue b/src/components/EventDialog.vue index c097e39..813ad6f 100644 --- a/src/components/EventDialog.vue +++ b/src/components/EventDialog.vue @@ -23,6 +23,8 @@ const emit = defineEmits(['clear-selection']) const calendarStore = useCalendarStore() const showDialog = ref(false) +// Anchoring: element of the DayCell representing the event's start date. +const anchorElement = ref(null) const dialogMode = ref('create') // 'create' or 'edit' const editingEventId = ref(null) const unsavedCreateId = ref(null) @@ -191,6 +193,12 @@ function loadWeekdayPatternFromStore(storePattern) { recurrenceWeekdays.value = [...storePattern] } +function resolveAnchorFromDate(dateStr) { + if (!dateStr) return null + // Expect day cells to have data-date attribute (see CalendarDay / DayCell components) + return document.querySelector(`[data-date='${dateStr}']`) +} + function openCreateDialog(selectionData = null) { calendarStore.$history?.beginCompound() if (unsavedCreateId.value && !eventSaved.value) { @@ -240,6 +248,8 @@ function openCreateDialog(selectionData = null) { }) unsavedCreateId.value = editingEventId.value + // anchor to the starting day cell + anchorElement.value = resolveAnchorFromDate(start) showDialog.value = true nextTick(() => { @@ -344,6 +354,8 @@ function openEditDialog(payload) { occurrenceContext.value = { baseId, occurrenceIndex, weekday: null, occurrenceDate } } } + // anchor to base event start date + anchorElement.value = resolveAnchorFromDate(event.startDate) showDialog.value = true nextTick(() => { @@ -553,7 +565,7 @@ const recurrenceSummary = computed(() => {