| 
									
										
										
										
											2022-12-01 00:17:15 +01:00
										 |  |  | import utils from "./utils.js"; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-19 21:03:38 +02:00
										 |  |  | type ElementType = HTMLElement | Document; | 
					
						
							| 
									
										
										
										
											2025-07-30 23:46:43 +03:00
										 |  |  | type Handler = (e: KeyboardEvent) => void; | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  | interface ShortcutBinding { | 
					
						
							|  |  |  |     element: HTMLElement | Document; | 
					
						
							|  |  |  |     shortcut: string; | 
					
						
							|  |  |  |     handler: Handler; | 
					
						
							|  |  |  |     namespace: string | null; | 
					
						
							|  |  |  |     listener: (evt: Event) => void; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Store all active shortcut bindings for management
 | 
					
						
							|  |  |  | const activeBindings: Map<string, ShortcutBinding[]> = new Map(); | 
					
						
							| 
									
										
										
										
											2024-12-19 21:03:38 +02:00
										 |  |  | 
 | 
					
						
							|  |  |  | function removeGlobalShortcut(namespace: string) { | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  |     bindGlobalShortcut("", null, namespace); | 
					
						
							| 
									
										
										
										
											2022-12-01 13:24:34 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-12-19 21:03:38 +02:00
										 |  |  | function bindGlobalShortcut(keyboardShortcut: string, handler: Handler | null, namespace: string | null = null) { | 
					
						
							| 
									
										
										
										
											2022-12-01 13:07:23 +01:00
										 |  |  |     bindElShortcut($(document), keyboardShortcut, handler, namespace); | 
					
						
							| 
									
										
										
										
											2022-12-01 00:17:15 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-03-16 18:31:31 +02:00
										 |  |  | function bindElShortcut($el: JQuery<ElementType | Element>, keyboardShortcut: string, handler: Handler | null, namespace: string | null = null) { | 
					
						
							| 
									
										
										
										
											2022-12-01 00:17:15 +01:00
										 |  |  |     if (utils.isDesktop()) { | 
					
						
							|  |  |  |         keyboardShortcut = normalizeShortcut(keyboardShortcut); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  |         // If namespace is provided, remove all previous bindings for this namespace
 | 
					
						
							| 
									
										
										
										
											2022-12-01 10:03:04 +01:00
										 |  |  |         if (namespace) { | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  |             removeNamespaceBindings(namespace); | 
					
						
							| 
									
										
										
										
											2022-12-01 10:03:04 +01:00
										 |  |  |         } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  |         // Method can be called to remove the shortcut (e.g. when keyboardShortcut label is deleted)
 | 
					
						
							|  |  |  |         if (keyboardShortcut && handler) { | 
					
						
							|  |  |  |             const element = $el.length > 0 ? $el[0] as (HTMLElement | Document) : document; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             const listener = (evt: Event) => { | 
					
						
							| 
									
										
										
										
											2025-07-30 14:29:59 +03:00
										 |  |  |                 // Only handle keyboard events
 | 
					
						
							|  |  |  |                 if (evt.type !== 'keydown' || !(evt instanceof KeyboardEvent)) { | 
					
						
							|  |  |  |                     return; | 
					
						
							|  |  |  |                 } | 
					
						
							| 
									
										
										
										
											2025-07-30 14:43:37 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  |                 const e = evt as KeyboardEvent; | 
					
						
							|  |  |  |                 if (matchesShortcut(e, keyboardShortcut)) { | 
					
						
							|  |  |  |                     e.preventDefault(); | 
					
						
							|  |  |  |                     e.stopPropagation(); | 
					
						
							| 
									
										
										
										
											2025-07-30 23:46:43 +03:00
										 |  |  |                     handler(e); | 
					
						
							| 
									
										
										
										
											2024-12-19 21:03:38 +02:00
										 |  |  |                 } | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  |             }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             // Add the event listener
 | 
					
						
							|  |  |  |             element.addEventListener('keydown', listener); | 
					
						
							| 
									
										
										
										
											2022-12-01 10:03:04 +01:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  |             // Store the binding for later cleanup
 | 
					
						
							|  |  |  |             const binding: ShortcutBinding = { | 
					
						
							|  |  |  |                 element, | 
					
						
							|  |  |  |                 shortcut: keyboardShortcut, | 
					
						
							|  |  |  |                 handler, | 
					
						
							|  |  |  |                 namespace, | 
					
						
							|  |  |  |                 listener | 
					
						
							|  |  |  |             }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |             const key = namespace || 'global'; | 
					
						
							|  |  |  |             if (!activeBindings.has(key)) { | 
					
						
							|  |  |  |                 activeBindings.set(key, []); | 
					
						
							|  |  |  |             } | 
					
						
							|  |  |  |             activeBindings.get(key)!.push(binding); | 
					
						
							| 
									
										
										
										
											2022-12-01 10:03:04 +01:00
										 |  |  |         } | 
					
						
							| 
									
										
										
										
											2022-12-01 00:17:15 +01:00
										 |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  | function removeNamespaceBindings(namespace: string) { | 
					
						
							|  |  |  |     const bindings = activeBindings.get(namespace); | 
					
						
							|  |  |  |     if (bindings) { | 
					
						
							|  |  |  |         // Remove all event listeners for this namespace
 | 
					
						
							|  |  |  |         bindings.forEach(binding => { | 
					
						
							|  |  |  |             binding.element.removeEventListener('keydown', binding.listener); | 
					
						
							|  |  |  |         }); | 
					
						
							|  |  |  |         activeBindings.delete(namespace); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 19:30:27 +03:00
										 |  |  | export function matchesShortcut(e: KeyboardEvent, shortcut: string): boolean { | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  |     if (!shortcut) return false; | 
					
						
							| 
									
										
										
										
											2025-07-30 14:43:37 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:29:59 +03:00
										 |  |  |     // Ensure we have a proper KeyboardEvent with key property
 | 
					
						
							|  |  |  |     if (!e || typeof e.key !== 'string') { | 
					
						
							|  |  |  |         console.warn('matchesShortcut called with invalid event:', e); | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  | 
 | 
					
						
							|  |  |  |     const parts = shortcut.toLowerCase().split('+'); | 
					
						
							|  |  |  |     const key = parts[parts.length - 1]; // Last part is the actual key
 | 
					
						
							|  |  |  |     const modifiers = parts.slice(0, -1); // Everything before is modifiers
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:29:59 +03:00
										 |  |  |     // Defensive check - ensure we have a valid key
 | 
					
						
							|  |  |  |     if (!key || key.trim() === '') { | 
					
						
							|  |  |  |         console.warn('Invalid shortcut format:', shortcut); | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  |     // Check if the main key matches
 | 
					
						
							|  |  |  |     if (!keyMatches(e, key)) { | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Check modifiers
 | 
					
						
							|  |  |  |     const expectedCtrl = modifiers.includes('ctrl') || modifiers.includes('control'); | 
					
						
							|  |  |  |     const expectedAlt = modifiers.includes('alt'); | 
					
						
							|  |  |  |     const expectedShift = modifiers.includes('shift'); | 
					
						
							|  |  |  |     const expectedMeta = modifiers.includes('meta') || modifiers.includes('cmd') || modifiers.includes('command'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return e.ctrlKey === expectedCtrl && | 
					
						
							|  |  |  |            e.altKey === expectedAlt && | 
					
						
							|  |  |  |            e.shiftKey === expectedShift && | 
					
						
							|  |  |  |            e.metaKey === expectedMeta; | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 19:30:27 +03:00
										 |  |  | export function keyMatches(e: KeyboardEvent, key: string): boolean { | 
					
						
							| 
									
										
										
										
											2025-07-30 14:29:59 +03:00
										 |  |  |     // Defensive check for undefined/null key
 | 
					
						
							|  |  |  |     if (!key) { | 
					
						
							|  |  |  |         console.warn('keyMatches called with undefined/null key'); | 
					
						
							|  |  |  |         return false; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:26:51 +03:00
										 |  |  |     // Handle special key mappings and aliases
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  |     const keyMap: { [key: string]: string[] } = { | 
					
						
							|  |  |  |         'return': ['Enter'], | 
					
						
							| 
									
										
										
										
											2025-07-30 14:26:51 +03:00
										 |  |  |         'enter': ['Enter'],  // alias for return
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  |         'del': ['Delete'], | 
					
						
							| 
									
										
										
										
											2025-07-30 14:26:51 +03:00
										 |  |  |         'delete': ['Delete'], // alias for del
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  |         'esc': ['Escape'], | 
					
						
							| 
									
										
										
										
											2025-07-30 14:26:51 +03:00
										 |  |  |         'escape': ['Escape'], // alias for esc
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  |         'space': [' ', 'Space'], | 
					
						
							|  |  |  |         'tab': ['Tab'], | 
					
						
							|  |  |  |         'backspace': ['Backspace'], | 
					
						
							|  |  |  |         'home': ['Home'], | 
					
						
							|  |  |  |         'end': ['End'], | 
					
						
							|  |  |  |         'pageup': ['PageUp'], | 
					
						
							|  |  |  |         'pagedown': ['PageDown'], | 
					
						
							|  |  |  |         'up': ['ArrowUp'], | 
					
						
							|  |  |  |         'down': ['ArrowDown'], | 
					
						
							|  |  |  |         'left': ['ArrowLeft'], | 
					
						
							|  |  |  |         'right': ['ArrowRight'] | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // Function keys
 | 
					
						
							|  |  |  |     for (let i = 1; i <= 19; i++) { | 
					
						
							|  |  |  |         keyMap[`f${i}`] = [`F${i}`]; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const mappedKeys = keyMap[key.toLowerCase()]; | 
					
						
							|  |  |  |     if (mappedKeys) { | 
					
						
							|  |  |  |         return mappedKeys.includes(e.key) || mappedKeys.includes(e.code); | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:43:37 +03:00
										 |  |  |     // For number keys, use the physical key code regardless of modifiers
 | 
					
						
							|  |  |  |     // This works across all keyboard layouts
 | 
					
						
							|  |  |  |     if (key >= '0' && key <= '9') { | 
					
						
							|  |  |  |         return e.code === `Digit${key}`; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // For letter keys, use the physical key code for consistency
 | 
					
						
							|  |  |  |     if (key.length === 1 && key >= 'a' && key <= 'z') { | 
					
						
							|  |  |  |         return e.code === `Key${key.toUpperCase()}`; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     // For regular keys, check both key and code as fallback
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:11:41 +03:00
										 |  |  |     return e.key.toLowerCase() === key.toLowerCase() || | 
					
						
							|  |  |  |            e.code.toLowerCase() === key.toLowerCase(); | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2022-12-01 00:17:15 +01:00
										 |  |  | /** | 
					
						
							| 
									
										
										
										
											2025-07-30 14:26:51 +03:00
										 |  |  |  * Simple normalization - just lowercase and trim whitespace | 
					
						
							| 
									
										
										
										
											2022-12-01 00:17:15 +01:00
										 |  |  |  */ | 
					
						
							| 
									
										
										
										
											2024-12-19 21:03:38 +02:00
										 |  |  | function normalizeShortcut(shortcut: string): string { | 
					
						
							| 
									
										
										
										
											2022-12-01 10:03:04 +01:00
										 |  |  |     if (!shortcut) { | 
					
						
							|  |  |  |         return shortcut; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:29:59 +03:00
										 |  |  |     const normalized = shortcut.toLowerCase().trim().replace(/\s+/g, ''); | 
					
						
							| 
									
										
										
										
											2025-07-30 14:43:37 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:29:59 +03:00
										 |  |  |     // Warn about potentially problematic shortcuts
 | 
					
						
							|  |  |  |     if (normalized.endsWith('+') || normalized.startsWith('+') || normalized.includes('++')) { | 
					
						
							|  |  |  |         console.warn('Potentially malformed shortcut:', shortcut, '-> normalized to:', normalized); | 
					
						
							|  |  |  |     } | 
					
						
							| 
									
										
										
										
											2025-07-30 14:43:37 +03:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2025-07-30 14:29:59 +03:00
										 |  |  |     return normalized; | 
					
						
							| 
									
										
										
										
											2022-12-01 00:17:15 +01:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export default { | 
					
						
							|  |  |  |     bindGlobalShortcut, | 
					
						
							|  |  |  |     bindElShortcut, | 
					
						
							| 
									
										
										
										
											2022-12-01 13:24:34 +01:00
										 |  |  |     removeGlobalShortcut, | 
					
						
							| 
									
										
										
										
											2022-12-01 00:17:15 +01:00
										 |  |  |     normalizeShortcut | 
					
						
							| 
									
										
										
										
											2025-01-09 18:07:02 +02:00
										 |  |  | }; |