From 7ed6fd9b299f37c20a10a4158047e7c253d9f94d Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Mon, 25 Aug 2025 10:59:08 -0600 Subject: [PATCH] Settings dialog inside the settings button. --- src/components/BaseDialog.vue | 90 ++++++++++++++++++++++++++----- src/components/CalendarView.vue | 9 +--- src/components/HeaderControls.vue | 13 ++++- src/components/SettingsDialog.vue | 8 --- 4 files changed, 89 insertions(+), 31 deletions(-) diff --git a/src/components/BaseDialog.vue b/src/components/BaseDialog.vue index 904306c..cb12888 100644 --- a/src/components/BaseDialog.vue +++ b/src/components/BaseDialog.vue @@ -11,12 +11,18 @@ const props = defineProps({ const emit = defineEmits(['update:modelValue', 'opened', 'closed', 'submit']) const modalRef = ref(null) +const anchorRef = ref(null) const isDragging = ref(false) const dragOffset = ref({ x: 0, y: 0 }) const modalPosition = ref({ x: 0, y: 0 }) const dialogWidth = ref(null) const dialogHeight = ref(null) const hasMoved = ref(false) +const margin = 8 // viewport margin in px to keep dialog from touching edges + +function clamp(val, min, max) { + return Math.min(Math.max(val, min), max) +} function startDrag(event) { if (!props.draggable || !modalRef.value) return @@ -41,10 +47,15 @@ function startDrag(event) { } function handleDrag(event) { if (!isDragging.value) return - modalPosition.value = { - x: event.clientX - dragOffset.value.x, - y: event.clientY - dragOffset.value.y, - } + let x = event.clientX - dragOffset.value.x + let y = event.clientY - dragOffset.value.y + const w = dialogWidth.value || modalRef.value?.offsetWidth || 0 + const h = dialogHeight.value || modalRef.value?.offsetHeight || 0 + const vw = window.innerWidth + const vh = window.innerHeight + x = clamp(x, margin, Math.max(margin, vw - w - margin)) + y = clamp(y, margin, Math.max(margin, vh - h - margin)) + modalPosition.value = { x, y } event.preventDefault() } function stopDrag() { @@ -55,16 +66,20 @@ function stopDrag() { } const modalStyle = computed(() => { - if (hasMoved.value) { - return { + // Always position relative to calculated modalPosition once opened + if (modalRef.value && props.modelValue) { + const style = { transform: 'none', - left: `${modalPosition.value.x}px`, - top: `${modalPosition.value.y}px`, + left: modalPosition.value.x + 'px', + top: modalPosition.value.y + 'px', bottom: 'auto', right: 'auto', - width: dialogWidth.value ? dialogWidth.value + 'px' : undefined, - height: dialogHeight.value ? dialogHeight.value + 'px' : undefined, } + if (hasMoved.value) { + style.width = dialogWidth.value ? dialogWidth.value + 'px' : undefined + style.height = dialogHeight.value ? dialogHeight.value + 'px' : undefined + } + return style } return {} }) @@ -81,12 +96,34 @@ function handleKeydown(e) { 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 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 + x = clamp(x, margin, Math.max(margin, vw - w - margin)) + y = clamp(y, margin, Math.max(margin, vh - h - margin)) + modalPosition.value = { x, y } +} + watch( () => props.modelValue, async (v) => { if (v) { emit('opened') await nextTick() + // Reset movement state each time opened + hasMoved.value = false + dialogWidth.value = null + dialogHeight.value = null + positionNearAnchor() if (props.autoFocus) { const el = modalRef.value?.querySelector('[autofocus]') if (el) el.focus() @@ -94,9 +131,33 @@ watch( } }, ) + +function handleResize() { + if (!props.modelValue) return + // Re-clamp current position, and if not moved recalc near anchor + if (!hasMoved.value) positionNearAnchor() + else if (modalRef.value) { + const w = modalRef.value.offsetWidth + const h = modalRef.value.offsetHeight + const vw = window.innerWidth + const vh = window.innerHeight + modalPosition.value = { + x: clamp(modalPosition.value.x, margin, Math.max(margin, vw - w - margin)), + y: clamp(modalPosition.value.y, margin, Math.max(margin, vh - h - margin)), + } + } +} + +onMounted(() => { + window.addEventListener('resize', handleResize) +}) +onUnmounted(() => { + window.removeEventListener('resize', handleResize) +})