V13-FE-006: consolidate approved UI and contract hardening
This commit is contained in:
+52
@@ -0,0 +1,52 @@
|
||||
import type { KbxShortcutDefinition, KbxShortcutScope } from '@kbx/contracts'
|
||||
|
||||
const scopeRank:Record<KbxShortcutScope,number>={application:1,page:2,grid:3,dialog:4,editor:5}
|
||||
const protectedBrowserShortcuts=new Set(['F5','CTRL+L','CTRL+T','CTRL+W','CTRL+R'])
|
||||
let nextId=1
|
||||
const registrations=new Map<number,KbxShortcutDefinition[]>()
|
||||
let listening=false
|
||||
|
||||
function normalizeConfiguredKey(value:string){return value.trim().replace(/\s+/g,'').toUpperCase()}
|
||||
function eventKey(event:KeyboardEvent){
|
||||
const parts:string[]=[]
|
||||
if(event.ctrlKey||event.metaKey)parts.push('CTRL')
|
||||
if(event.altKey)parts.push('ALT')
|
||||
if(event.shiftKey)parts.push('SHIFT')
|
||||
const key=event.key===' '?'SPACE':event.key.toUpperCase()
|
||||
parts.push(key==='ESC'?'ESCAPE':key)
|
||||
return parts.join('+')
|
||||
}
|
||||
function isTextEditingTarget(target:EventTarget|null){
|
||||
const el=target instanceof HTMLElement?target:null
|
||||
if(!el)return false
|
||||
const tag=el.tagName.toLowerCase()
|
||||
return tag==='input'||tag==='textarea'||tag==='select'||el.isContentEditable
|
||||
}
|
||||
function candidates(event:KeyboardEvent){
|
||||
const key=eventKey(event)
|
||||
return [...registrations.values()].flat().filter(x=>normalizeConfiguredKey(x.key)===key&&x.enabled?.()!==false)
|
||||
.sort((a,b)=>(scopeRank[b.scope]-scopeRank[a.scope])||((b.priority??0)-(a.priority??0)))
|
||||
}
|
||||
function handle(event:KeyboardEvent){
|
||||
const key=eventKey(event)
|
||||
if(protectedBrowserShortcuts.has(key))return
|
||||
const matches=candidates(event)
|
||||
if(!matches.length)return
|
||||
const editing=isTextEditingTarget(event.target)
|
||||
const match=matches.find(x=>!editing||x.scope==='editor'||!['ENTER','SPACE','ARROWUP','ARROWDOWN','ARROWLEFT','ARROWRIGHT'].includes(key))
|
||||
if(!match)return
|
||||
event.preventDefault();void match.execute()
|
||||
}
|
||||
function ensureListener(){if(listening||typeof window==='undefined')return;window.addEventListener('keydown',handle);listening=true}
|
||||
function releaseListener(){if(!listening||registrations.size||typeof window==='undefined')return;window.removeEventListener('keydown',handle);listening=false}
|
||||
|
||||
export function registerKbxShortcuts(shortcuts:KbxShortcutDefinition[]){
|
||||
for(const shortcut of shortcuts){
|
||||
const key=normalizeConfiguredKey(shortcut.key)
|
||||
if(protectedBrowserShortcuts.has(key))throw new Error(`KBX browser shortcut is protected: ${shortcut.key}`)
|
||||
}
|
||||
const id=nextId++;registrations.set(id,shortcuts);ensureListener()
|
||||
return ()=>{registrations.delete(id);releaseListener()}
|
||||
}
|
||||
|
||||
export function getKbxShortcutDebugSnapshot(){return [...registrations.values()].flat().map(x=>({key:x.key,scope:x.scope,priority:x.priority??0}))}
|
||||
Reference in New Issue
Block a user