From 557dc1f94cdde9ba4a2d6449497e099c32bd8604 Mon Sep 17 00:00:00 2001 From: Shariq Ansari Date: Tue, 2 Sep 2025 21:26:30 +0530 Subject: [PATCH] fix: add keyboard shortcuts for event creation and management in Calendar components --- .../Calendar/CalendarEventPanel.vue | 59 ++++++++++++++++++- frontend/src/pages/Calendar.vue | 46 ++++++++++++++- 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/Calendar/CalendarEventPanel.vue b/frontend/src/components/Calendar/CalendarEventPanel.vue index f11a6b81..22091b6f 100644 --- a/frontend/src/components/Calendar/CalendarEventPanel.vue +++ b/frontend/src/components/Calendar/CalendarEventPanel.vue @@ -177,6 +177,7 @@ :debounce="500" :placeholder="__('Event title')" @change="sync" + @keyup.enter="saveEvent" />
@@ -372,7 +373,7 @@ import { CalendarActiveEvent as activeEvent, createDocumentResource, } from 'frappe-ui' -import { ref, computed, watch, h } from 'vue' +import { ref, computed, watch, h, onMounted, onBeforeUnmount } from 'vue' import { useRouter } from 'vue-router' const props = defineProps({ @@ -664,5 +665,61 @@ function updateEvent(_e) { Object.assign(_event.value, _e) } +// Keyboard shortcuts +function isTypingEvent(e) { + const el = e.target + if (!el) return false + const tag = el.tagName + const editable = el.isContentEditable + return ( + editable || + tag === 'INPUT' || + tag === 'TEXTAREA' || + tag === 'SELECT' || + (el.closest && el.closest('[contenteditable="true"]')) + ) +} + +function keydownHandler(e) { + if (!show.value) return + + // Esc always closes the panel + if (e.key === 'Escape') { + e.preventDefault() + close() + return + } + + if (!['details', 'edit'].includes(props.mode)) return + if (isTypingEvent(e)) return + + // Delete (no modifier) -> delete event + if (e.key === 'Delete' || e.key === 'Backspace') { + // Avoid capturing Backspace if it would navigate away when no focus + e.preventDefault() + deleteEvent() + return + } + + // Cmd/Ctrl + D -> duplicate event + if ( + (e.metaKey || e.ctrlKey) && + !e.shiftKey && + !e.altKey && + e.key.toLowerCase() === 'd' + ) { + e.preventDefault() + duplicateEvent() + } +} + +onMounted(() => { + window.addEventListener('keydown', keydownHandler) +}) + +onBeforeUnmount(() => { + window.removeEventListener('keydown', keydownHandler) +}) + defineExpose({ updateEvent }) diff --git a/frontend/src/pages/Calendar.vue b/frontend/src/pages/Calendar.vue index d89ea754..db3fd608 100644 --- a/frontend/src/pages/Calendar.vue +++ b/frontend/src/pages/Calendar.vue @@ -113,6 +113,7 @@ v-model="showEventPanel" v-model:event="event" :mode="mode" + @new="newEvent" @save="saveEvent" @edit="editDetails" @delete="deleteEvent" @@ -139,7 +140,7 @@ import { CalendarActiveEvent as activeEvent, call, } from 'frappe-ui' -import { onMounted, ref, computed } from 'vue' +import { onMounted, onBeforeUnmount, ref, computed } from 'vue' const { user } = sessionStore() const { $dialog } = globalStore() @@ -189,7 +190,7 @@ const event = ref({}) const mode = ref('') const isCreateDisabled = computed(() => - ['edit', 'new-event', 'duplicate-event'].includes(mode.value), + ['edit', 'new', 'duplicate'].includes(mode.value), ) // Temp event helpers @@ -325,6 +326,47 @@ onMounted(() => { showEventPanel.value = false }) +// Global shortcut: Cmd/Ctrl + E -> new event (when not already creating/editing) +function isTypingEvent(e) { + const el = e.target + if (!el) return false + const tag = el.tagName + const editable = el.isContentEditable + return ( + editable || + tag === 'INPUT' || + tag === 'TEXTAREA' || + tag === 'SELECT' || + (el.closest && el.closest('[contenteditable="true"]')) + ) +} + +function calendarKeydown(e) { + if (isTypingEvent(e)) return + if ( + (e.metaKey || e.ctrlKey) && + !e.shiftKey && + !e.altKey && + e.key.toLowerCase() === 'e' + ) { + if (isCreateDisabled.value) return + e.preventDefault() + newEvent({ + date: dayjs().format('YYYY-MM-DD'), + time: dayjs().format('HH:mm'), + isFullDay: false, + }) + } +} + +onMounted(() => { + window.addEventListener('keydown', calendarKeydown) +}) + +onBeforeUnmount(() => { + window.removeEventListener('keydown', calendarKeydown) +}) + function showDetails(e, reloadEvent = false) { openEvent(e, 'details', reloadEvent) }