Files
Endoleap/src/lib/canvas-bindings/input-router.ts
T

39 lines
3.0 KiB
TypeScript

/**
* 归一化键盘事件的消费顺序:文本编辑 → 浮层 → 手势 → 工具 → 快捷键。
* 输入来源由宿主传入,路由器只报告获胜层,不直接 preventDefault、执行命令或访问浏览器全局。
*/
/** Host normalizes DOM/native input; only the winning layer consumes an event. */
/** 将 DOM/native 键盘上下文压成只读值;composing 保护输入法组合,modifier 供具体处理器解释。 */
export type CanvasKeyInput={readonly key:string;readonly composing?:boolean;readonly textEditing?:boolean;readonly overlay?:boolean;readonly defaultPrevented?:boolean;readonly ctrl?:boolean;readonly meta?:boolean;readonly alt?:boolean;readonly shift?:boolean};
/** 只有一个层获得消费权;unclaimed 表示宿主不应据此拦截事件。 */
export type InputClaim='text'|'overlay'|'gesture'|'tool'|'shortcut'|'unclaimed';
/** 返回 true 即获得事件;后续低优先级处理器不会再运行。 */
export type KeyHandler=(input:CanvasKeyInput)=>boolean;
// 标准文本控件、可编辑内容和 textbox 角色均视为正在输入,不让画布快捷键抢走按键。
const textTarget = 'input,textarea,select,[contenteditable]:not([contenteditable="false"]),[role="textbox"]';
// 画布浮层标记和交互 ARIA 角色优先于全局画布命令。
const overlayTarget = '[data-canvas-overlay-view],[role="dialog"],[role="menu"],[role="listbox"],[role="combobox"]';
/** Inspect the composed path as well as the retargeted host (portals/shadow DOM).
* This uses a structural DOM port so pure consumers need no browser globals. */
/** 检查 target 和 composedPath 上所有可 closest 的节点,识别 shadow DOM 重定向后的输入上下文。
* portal 需要自身携带浮层标记/角色;这里只检查实际路径,不推断不存在于 DOM 路径的 React 父关系。 */
export function canvasKeyContext(event:{readonly target?:unknown;composedPath?:()=>readonly unknown[]}):Pick<CanvasKeyInput,'textEditing'|'overlay'>{
let textEditing=false,overlay=false;
for(const target of [event.target,...(event.composedPath?.()??[])]){
// 使用结构能力判断,跳过 Window、文本节点等无 closest 的路径项,无需 instanceof Element。
if(!target||typeof (target as {closest?:unknown}).closest!=='function')continue;
const element=target as {closest(selector:string):unknown};
textEditing ||= !!element.closest(textTarget);
overlay ||= !!element.closest(overlayTarget);
}
return {textEditing,overlay};
}
/** 已被宿主处理的事件不再认领;输入法/文本与浮层直接截住,随后按固定优先级短路调用处理器。 */
export function routeCanvasKey(input:CanvasKeyInput,handlers:{gesture?:KeyHandler;tool?:KeyHandler;shortcut?:KeyHandler}):InputClaim{
if(input.defaultPrevented)return 'unclaimed';
if(input.composing||input.textEditing)return 'text';
if(input.overlay)return 'overlay';
for(const layer of ['gesture','tool','shortcut'] as const)if(handlers[layer]?.(input))return layer;
return 'unclaimed';
}