fix: created keyboard shortcut composable to handle shortcut implementations
This commit is contained in:
parent
11d1b3a67a
commit
f2ce3165dd
@ -363,6 +363,7 @@ import {
|
||||
validateTimeRange,
|
||||
parseEventDoc,
|
||||
} from '@/composables/event'
|
||||
import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts'
|
||||
import {
|
||||
TextInput,
|
||||
Switch,
|
||||
@ -376,7 +377,7 @@ import {
|
||||
CalendarActiveEvent as activeEvent,
|
||||
createDocumentResource,
|
||||
} from 'frappe-ui'
|
||||
import { ref, computed, watch, h, onMounted, onBeforeUnmount } from 'vue'
|
||||
import { ref, computed, watch, h } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const props = defineProps({
|
||||
@ -671,68 +672,33 @@ 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
|
||||
|
||||
// Enter in details mode -> switch to edit
|
||||
if (e.key === 'Enter' && props.mode === 'details') {
|
||||
e.preventDefault()
|
||||
editDetails()
|
||||
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 })
|
||||
|
||||
// Keyboard shortcuts
|
||||
useKeyboardShortcuts({
|
||||
active: () => show.value,
|
||||
shortcuts: [
|
||||
{ keys: 'Escape', action: () => close() },
|
||||
{
|
||||
keys: 'Enter',
|
||||
guard: () =>
|
||||
['details', 'edit'].includes(props.mode) && props.mode === 'details',
|
||||
action: () => editDetails(),
|
||||
},
|
||||
{
|
||||
keys: ['Delete', 'Backspace'],
|
||||
guard: () => ['details', 'edit'].includes(props.mode),
|
||||
action: () => deleteEvent(),
|
||||
},
|
||||
{
|
||||
match: (e) =>
|
||||
['details', 'edit'].includes(props.mode) &&
|
||||
(e.metaKey || e.ctrlKey) &&
|
||||
!e.shiftKey &&
|
||||
!e.altKey &&
|
||||
e.key.toLowerCase() === 'd',
|
||||
action: () => duplicateEvent(),
|
||||
},
|
||||
],
|
||||
})
|
||||
</script>
|
||||
|
||||
75
frontend/src/composables/useKeyboardShortcuts.js
Normal file
75
frontend/src/composables/useKeyboardShortcuts.js
Normal file
@ -0,0 +1,75 @@
|
||||
import { onMounted, onBeforeUnmount, unref } from 'vue'
|
||||
|
||||
/**
|
||||
* Generic global keyboard shortcuts composable.
|
||||
*
|
||||
* Usage:
|
||||
* useKeyboardShortcuts({
|
||||
* active: () => true, // boolean | () => boolean (reactive allowed)
|
||||
* shortcuts: [
|
||||
* { keys: 'Escape', action: close },
|
||||
* { keys: ['Delete', 'Backspace'], action: onDelete },
|
||||
* { match: e => (e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'd', action: duplicate }
|
||||
* ],
|
||||
* ignoreTyping: true // skip when focus is in input/textarea/contenteditable (default true)
|
||||
* })
|
||||
*/
|
||||
export function useKeyboardShortcuts(options) {
|
||||
const {
|
||||
active = true,
|
||||
shortcuts = [],
|
||||
ignoreTyping = true,
|
||||
target = typeof window !== 'undefined' ? window : null,
|
||||
} = options || {}
|
||||
|
||||
function isTypingEvent(e) {
|
||||
if (!ignoreTyping) return false
|
||||
const el = e.target
|
||||
if (!el) return false
|
||||
const tag = el.tagName
|
||||
return (
|
||||
el.isContentEditable ||
|
||||
tag === 'INPUT' ||
|
||||
tag === 'TEXTAREA' ||
|
||||
tag === 'SELECT' ||
|
||||
(el.closest && el.closest('[contenteditable="true"]'))
|
||||
)
|
||||
}
|
||||
|
||||
function matchShortcut(def, e) {
|
||||
if (def.match) return def.match(e)
|
||||
let keys = def.keys
|
||||
if (!keys) return false
|
||||
if (!Array.isArray(keys)) keys = [keys]
|
||||
return keys.some((k) => k === e.key)
|
||||
}
|
||||
|
||||
function handler(e) {
|
||||
if (!target) return
|
||||
const isActive = typeof active === 'function' ? active() : unref(active)
|
||||
if (!isActive) return
|
||||
if (isTypingEvent(e)) return
|
||||
|
||||
for (const def of shortcuts) {
|
||||
if (!def) continue
|
||||
if (def.guard && !def.guard(e)) continue
|
||||
if (matchShortcut(def, e)) {
|
||||
if (def.preventDefault !== false) e.preventDefault()
|
||||
if (def.stopPropagation) e.stopPropagation()
|
||||
def.action && def.action(e)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
target && target.addEventListener('keydown', handler)
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
target && target.removeEventListener('keydown', handler)
|
||||
})
|
||||
|
||||
return {
|
||||
stop: () => target && target.removeEventListener('keydown', handler),
|
||||
}
|
||||
}
|
||||
@ -131,6 +131,7 @@ import ViewBreadcrumbs from '@/components/ViewBreadcrumbs.vue'
|
||||
import LayoutHeader from '@/components/LayoutHeader.vue'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { globalStore } from '@/stores/global'
|
||||
import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts'
|
||||
import {
|
||||
Calendar,
|
||||
createListResource,
|
||||
@ -140,7 +141,7 @@ import {
|
||||
CalendarActiveEvent as activeEvent,
|
||||
call,
|
||||
} from 'frappe-ui'
|
||||
import { onMounted, onBeforeUnmount, ref, computed } from 'vue'
|
||||
import { onMounted, ref, computed } from 'vue'
|
||||
|
||||
const { user } = sessionStore()
|
||||
const { $dialog } = globalStore()
|
||||
@ -327,44 +328,23 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
// 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)
|
||||
useKeyboardShortcuts({
|
||||
shortcuts: [
|
||||
{
|
||||
match: (e) =>
|
||||
(e.metaKey || e.ctrlKey) &&
|
||||
!e.shiftKey &&
|
||||
!e.altKey &&
|
||||
e.key.toLowerCase() === 'e',
|
||||
guard: () => !isCreateDisabled.value,
|
||||
action: () =>
|
||||
newEvent({
|
||||
date: dayjs().format('YYYY-MM-DD'),
|
||||
time: dayjs().format('HH:mm'),
|
||||
isFullDay: false,
|
||||
}),
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
function showDetails(e, reloadEvent = false) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user