myCRM/docs/design/composables/useKeyboardShortcuts.js
olli 82b022ba3b Add comprehensive styles for invoice form components and utilities
- Introduced CSS custom properties for spacing, typography, colors, and shadows.
- Developed styles for form sections, grids, invoice items, and summary components.
- Implemented responsive design adjustments for various screen sizes.
- Added utility classes for text, spacing, and flex layouts.
- Included dark mode and high contrast mode support.
- Established loading states and validation/error styles.
- Enhanced accessibility features with focus styles and screen reader utilities.
2025-12-13 10:02:30 +01:00

387 lines
9.0 KiB
JavaScript

/**
* COMPOSABLE: useKeyboardShortcuts
*
* Provides keyboard shortcut functionality with support for
* modifier keys (Ctrl/Cmd, Shift, Alt) and custom key combinations.
*
* Usage:
* ```js
* import { useKeyboardShortcuts } from '@/composables/useKeyboardShortcuts'
*
* useKeyboardShortcuts({
* 'ctrl+s': saveForm,
* 'ctrl+shift+p': addNewItem,
* 'escape': closeDialog,
* 'alt+n': () => console.log('Alt+N pressed')
* })
* ```
*/
import { onMounted, onUnmounted, ref } from 'vue'
export function useKeyboardShortcuts(shortcuts, options = {}) {
const {
enabled = true,
preventDefault = true,
target = null // null means window, or pass a ref to an element
} = options
const isEnabled = ref(enabled)
const registeredShortcuts = ref(shortcuts)
/**
* Normalizes a key string to lowercase and handles platform differences
* @param {string} key - Key string
* @returns {string}
*/
const normalizeKey = (key) => {
return key.toLowerCase()
.replace('meta', 'ctrl') // Treat Meta (Cmd) as Ctrl for consistency
.replace('command', 'ctrl')
}
/**
* Builds a shortcut string from a KeyboardEvent
* @param {KeyboardEvent} event - Keyboard event
* @returns {string}
*/
const buildShortcutString = (event) => {
const parts = []
// Add modifiers in consistent order
if (event.ctrlKey || event.metaKey) parts.push('ctrl')
if (event.shiftKey) parts.push('shift')
if (event.altKey) parts.push('alt')
// Add the actual key (normalized)
const key = event.key.toLowerCase()
// Handle special keys
const specialKeys = {
' ': 'space',
'arrowup': 'up',
'arrowdown': 'down',
'arrowleft': 'left',
'arrowright': 'right',
'enter': 'enter',
'escape': 'escape',
'tab': 'tab',
'backspace': 'backspace',
'delete': 'delete',
'insert': 'insert',
'home': 'home',
'end': 'end',
'pageup': 'pageup',
'pagedown': 'pagedown'
}
const normalizedKey = specialKeys[key] || key
parts.push(normalizedKey)
return parts.join('+')
}
/**
* Checks if the event should be ignored (e.g., inside an input field)
* @param {KeyboardEvent} event - Keyboard event
* @returns {boolean}
*/
const shouldIgnoreEvent = (event) => {
// Don't trigger shortcuts when typing in input fields (unless specified)
const target = event.target
const tagName = target.tagName.toLowerCase()
// Check if target is an editable element
if (
tagName === 'input' ||
tagName === 'textarea' ||
tagName === 'select' ||
target.isContentEditable
) {
// Allow Escape key even in input fields
if (event.key === 'Escape') return false
// Allow Ctrl+S (save) even in input fields
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
return false
}
return true
}
return false
}
/**
* Handles keyboard events
* @param {KeyboardEvent} event - Keyboard event
*/
const handleKeydown = (event) => {
// Check if shortcuts are enabled
if (!isEnabled.value) return
// Check if we should ignore this event
if (shouldIgnoreEvent(event)) return
// Build the shortcut string
const shortcutString = buildShortcutString(event)
// Find matching shortcut
const matchingShortcut = Object.keys(registeredShortcuts.value).find(
shortcut => normalizeKey(shortcut) === shortcutString
)
if (matchingShortcut) {
const action = registeredShortcuts.value[matchingShortcut]
// Prevent default browser behavior if configured
if (preventDefault) {
event.preventDefault()
}
// Execute the shortcut action
if (typeof action === 'function') {
action(event)
}
}
}
/**
* Registers a new shortcut
* @param {string} shortcut - Shortcut string (e.g., 'ctrl+s')
* @param {Function} action - Action to execute
*/
const registerShortcut = (shortcut, action) => {
registeredShortcuts.value[shortcut] = action
}
/**
* Unregisters a shortcut
* @param {string} shortcut - Shortcut string
*/
const unregisterShortcut = (shortcut) => {
delete registeredShortcuts.value[shortcut]
}
/**
* Enables keyboard shortcuts
*/
const enable = () => {
isEnabled.value = true
}
/**
* Disables keyboard shortcuts
*/
const disable = () => {
isEnabled.value = false
}
/**
* Gets all registered shortcuts
* @returns {object}
*/
const getShortcuts = () => {
return { ...registeredShortcuts.value }
}
// Setup and cleanup
onMounted(() => {
const element = target?.value || window
element.addEventListener('keydown', handleKeydown)
})
onUnmounted(() => {
const element = target?.value || window
element.removeEventListener('keydown', handleKeydown)
})
return {
registerShortcut,
unregisterShortcut,
enable,
disable,
getShortcuts,
isEnabled
}
}
/**
* Composable for displaying keyboard shortcut hints
*/
export function useKeyboardShortcutHints(shortcuts) {
/**
* Formats a shortcut string for display
* @param {string} shortcut - Shortcut string (e.g., 'ctrl+s')
* @returns {string[]} Array of key labels
*/
const formatShortcut = (shortcut) => {
const parts = shortcut.toLowerCase().split('+')
const isMac = navigator.platform.toUpperCase().indexOf('MAC') >= 0
return parts.map(part => {
// Platform-specific labels
if (part === 'ctrl') {
return isMac ? '⌘' : 'Ctrl'
}
if (part === 'shift') {
return isMac ? '⇧' : 'Shift'
}
if (part === 'alt') {
return isMac ? '⌥' : 'Alt'
}
// Special key labels
const specialKeys = {
space: 'Space',
enter: 'Enter',
escape: 'Esc',
tab: 'Tab',
backspace: 'Backspace',
delete: 'Del',
up: '↑',
down: '↓',
left: '←',
right: '→',
pageup: 'PgUp',
pagedown: 'PgDn'
}
return specialKeys[part] || part.toUpperCase()
})
}
/**
* Gets all shortcuts with formatted labels
* @returns {Array} Array of {shortcut, keys, description}
*/
const getFormattedShortcuts = (descriptions = {}) => {
return Object.keys(shortcuts).map(shortcut => ({
shortcut,
keys: formatShortcut(shortcut),
description: descriptions[shortcut] || ''
}))
}
return {
formatShortcut,
getFormattedShortcuts
}
}
/**
* Common keyboard shortcuts presets
*/
export const commonShortcuts = {
// Form actions
save: 'ctrl+s',
cancel: 'escape',
submit: 'ctrl+enter',
// Navigation
nextField: 'tab',
prevField: 'shift+tab',
firstField: 'ctrl+home',
lastField: 'ctrl+end',
// Editing
undo: 'ctrl+z',
redo: 'ctrl+shift+z',
copy: 'ctrl+c',
cut: 'ctrl+x',
paste: 'ctrl+v',
selectAll: 'ctrl+a',
// Item management
addItem: 'ctrl+shift+p',
deleteItem: 'ctrl+d',
duplicateItem: 'ctrl+shift+d',
// Search
search: 'ctrl+f',
searchNext: 'ctrl+g',
searchPrev: 'ctrl+shift+g',
// View
toggleFullscreen: 'f11',
zoomIn: 'ctrl+plus',
zoomOut: 'ctrl+minus',
resetZoom: 'ctrl+0'
}
/**
* Keyboard shortcut descriptions for invoice form
*/
export const invoiceFormShortcuts = {
'ctrl+s': 'Rechnung speichern',
'ctrl+shift+p': 'Position hinzufügen',
'escape': 'Formular schließen',
'ctrl+shift+d': 'Als Entwurf speichern',
'alt+c': 'Kunde auswählen',
'alt+d': 'Datum ändern'
}
/**
* Hook for managing shortcut conflicts
*/
export function useShortcutConflicts() {
const conflicts = ref([])
/**
* Checks for conflicting shortcuts
* @param {object} shortcuts - Shortcuts object
* @returns {Array} Array of conflicts
*/
const checkConflicts = (shortcuts) => {
const normalized = {}
const conflictsList = []
Object.keys(shortcuts).forEach(key => {
const normalizedKey = key.toLowerCase()
if (normalized[normalizedKey]) {
conflictsList.push({
shortcut: key,
conflictsWith: normalized[normalizedKey]
})
} else {
normalized[normalizedKey] = key
}
})
conflicts.value = conflictsList
return conflictsList
}
/**
* Checks if a shortcut conflicts with browser defaults
* @param {string} shortcut - Shortcut string
* @returns {boolean}
*/
const conflictsWithBrowser = (shortcut) => {
const browserShortcuts = [
'ctrl+t', // New tab
'ctrl+w', // Close tab
'ctrl+n', // New window
'ctrl+r', // Reload
'ctrl+p', // Print
'ctrl+o', // Open file
'ctrl+l', // Address bar
'ctrl+k', // Search bar
'ctrl+d', // Bookmark
'ctrl+h', // History
'ctrl+j', // Downloads
'ctrl+shift+t', // Reopen closed tab
'ctrl+shift+n', // New incognito window
'ctrl+shift+delete' // Clear browsing data
]
return browserShortcuts.includes(shortcut.toLowerCase())
}
return {
conflicts,
checkConflicts,
conflictsWithBrowser
}
}