- 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.
387 lines
9.0 KiB
JavaScript
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
|
|
}
|
|
}
|