fix: created keyboard shortcut composable to handle shortcut implementations
This commit is contained in:
parent
11d1b3a67a
commit
f2ce3165dd
@ -363,6 +363,7 @@ import {
|
|||||||
validateTimeRange,
|
validateTimeRange,
|
||||||
parseEventDoc,
|
parseEventDoc,
|
||||||
} from '@/composables/event'
|
} from '@/composables/event'
|
||||||
|
import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts'
|
||||||
import {
|
import {
|
||||||
TextInput,
|
TextInput,
|
||||||
Switch,
|
Switch,
|
||||||
@ -376,7 +377,7 @@ import {
|
|||||||
CalendarActiveEvent as activeEvent,
|
CalendarActiveEvent as activeEvent,
|
||||||
createDocumentResource,
|
createDocumentResource,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { ref, computed, watch, h, onMounted, onBeforeUnmount } from 'vue'
|
import { ref, computed, watch, h } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@ -671,68 +672,33 @@ function updateEvent(_e) {
|
|||||||
Object.assign(_event.value, _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 })
|
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>
|
</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 LayoutHeader from '@/components/LayoutHeader.vue'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { globalStore } from '@/stores/global'
|
import { globalStore } from '@/stores/global'
|
||||||
|
import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts'
|
||||||
import {
|
import {
|
||||||
Calendar,
|
Calendar,
|
||||||
createListResource,
|
createListResource,
|
||||||
@ -140,7 +141,7 @@ import {
|
|||||||
CalendarActiveEvent as activeEvent,
|
CalendarActiveEvent as activeEvent,
|
||||||
call,
|
call,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { onMounted, onBeforeUnmount, ref, computed } from 'vue'
|
import { onMounted, ref, computed } from 'vue'
|
||||||
|
|
||||||
const { user } = sessionStore()
|
const { user } = sessionStore()
|
||||||
const { $dialog } = globalStore()
|
const { $dialog } = globalStore()
|
||||||
@ -327,44 +328,23 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Global shortcut: Cmd/Ctrl + E -> new event (when not already creating/editing)
|
// Global shortcut: Cmd/Ctrl + E -> new event (when not already creating/editing)
|
||||||
function isTypingEvent(e) {
|
useKeyboardShortcuts({
|
||||||
const el = e.target
|
shortcuts: [
|
||||||
if (!el) return false
|
{
|
||||||
const tag = el.tagName
|
match: (e) =>
|
||||||
const editable = el.isContentEditable
|
(e.metaKey || e.ctrlKey) &&
|
||||||
return (
|
!e.shiftKey &&
|
||||||
editable ||
|
!e.altKey &&
|
||||||
tag === 'INPUT' ||
|
e.key.toLowerCase() === 'e',
|
||||||
tag === 'TEXTAREA' ||
|
guard: () => !isCreateDisabled.value,
|
||||||
tag === 'SELECT' ||
|
action: () =>
|
||||||
(el.closest && el.closest('[contenteditable="true"]'))
|
newEvent({
|
||||||
)
|
date: dayjs().format('YYYY-MM-DD'),
|
||||||
}
|
time: dayjs().format('HH:mm'),
|
||||||
|
isFullDay: false,
|
||||||
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) {
|
function showDetails(e, reloadEvent = false) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user