mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-26 09:19:43 +00:00
[refactor] Extract keybinding functionality into @comfyorg/keybinding package
Create framework-agnostic keybinding package following domain-driven design patterns. Move pure business logic to package while keeping Vue integration in workbench layer. Changes: - Add @comfyorg/keybinding package with KeyComboImpl and KeybindingImpl classes - Move core keybindings and reserved key constants to package - Update workbench layer to import from package with backward compatibility - Update all imports across codebase to use package exports - Maintain existing API surface for consumers
This commit is contained in:
29
packages/keybinding/package.json
Normal file
29
packages/keybinding/package.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"name": "@comfyorg/keybinding",
|
||||
"version": "1.0.0",
|
||||
"type": "module",
|
||||
"description": "Framework-agnostic keybinding system for ComfyUI",
|
||||
"main": "./src/index.ts",
|
||||
"types": "./src/index.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./src/index.ts",
|
||||
"types": "./src/index.ts"
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"typecheck": "tsc --noEmit"
|
||||
},
|
||||
"nx": {
|
||||
"tags": [
|
||||
"scope:shared",
|
||||
"type:lib"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
212
packages/keybinding/src/constants/coreKeybindings.ts
Normal file
212
packages/keybinding/src/constants/coreKeybindings.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import type { Keybinding } from '../types/keybinding'
|
||||
|
||||
export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
key: 'Enter'
|
||||
},
|
||||
commandId: 'Comfy.QueuePrompt'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
key: 'Enter'
|
||||
},
|
||||
commandId: 'Comfy.QueuePromptFront'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
alt: true,
|
||||
key: 'Enter'
|
||||
},
|
||||
commandId: 'Comfy.Interrupt'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'r'
|
||||
},
|
||||
commandId: 'Comfy.RefreshNodeDefinitions'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'q'
|
||||
},
|
||||
commandId: 'Workspace.ToggleSidebarTab.queue'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'w'
|
||||
},
|
||||
commandId: 'Workspace.ToggleSidebarTab.workflows'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'n'
|
||||
},
|
||||
commandId: 'Workspace.ToggleSidebarTab.node-library'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'm'
|
||||
},
|
||||
commandId: 'Workspace.ToggleSidebarTab.model-library'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 's',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.SaveWorkflow'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'o',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.OpenWorkflow'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'g',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.Graph.GroupSelectedNodes'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: ',',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.ShowSettingsDialog'
|
||||
},
|
||||
// For '=' both holding shift and not holding shift
|
||||
{
|
||||
combo: {
|
||||
key: '=',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ZoomIn',
|
||||
targetElementId: 'graph-canvas'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: '+',
|
||||
alt: true,
|
||||
shift: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ZoomIn',
|
||||
targetElementId: 'graph-canvas'
|
||||
},
|
||||
// For number pad '+'
|
||||
{
|
||||
combo: {
|
||||
key: '+',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ZoomIn',
|
||||
targetElementId: 'graph-canvas'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: '-',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ZoomOut',
|
||||
targetElementId: 'graph-canvas'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: '.'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.FitView',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'p'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelected.Pin',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'c',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Collapse',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'b',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Bypass',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'm',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleSelectedNodes.Mute',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: '`',
|
||||
ctrl: true
|
||||
},
|
||||
commandId: 'Workspace.ToggleBottomPanelTab.logs-terminal'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'f'
|
||||
},
|
||||
commandId: 'Workspace.ToggleFocusMode'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'e',
|
||||
ctrl: true,
|
||||
shift: true
|
||||
},
|
||||
commandId: 'Comfy.Graph.ConvertToSubgraph'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'm',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleMinimap'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
key: 'k'
|
||||
},
|
||||
commandId: 'Workspace.ToggleBottomPanel.Shortcuts'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'v'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.Unlock'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'h'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.Lock'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'Escape'
|
||||
},
|
||||
commandId: 'Comfy.Graph.ExitSubgraph'
|
||||
}
|
||||
]
|
||||
35
packages/keybinding/src/constants/reservedKeyCombos.ts
Normal file
35
packages/keybinding/src/constants/reservedKeyCombos.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
export const RESERVED_BY_TEXT_INPUT = new Set([
|
||||
'Ctrl + a',
|
||||
'Ctrl + c',
|
||||
'Ctrl + v',
|
||||
'Ctrl + x',
|
||||
'Ctrl + z',
|
||||
'Ctrl + y',
|
||||
'Ctrl + p',
|
||||
'Enter',
|
||||
'Shift + Enter',
|
||||
'Ctrl + Backspace',
|
||||
'Ctrl + Delete',
|
||||
'Home',
|
||||
'Ctrl + Home',
|
||||
'Ctrl + Shift + Home',
|
||||
'End',
|
||||
'Ctrl + End',
|
||||
'Ctrl + Shift + End',
|
||||
'PageUp',
|
||||
'PageDown',
|
||||
'Shift + PageUp',
|
||||
'Shift + PageDown',
|
||||
'ArrowLeft',
|
||||
'Ctrl + ArrowLeft',
|
||||
'Shift + ArrowLeft',
|
||||
'Ctrl + Shift + ArrowLeft',
|
||||
'ArrowRight',
|
||||
'Ctrl + ArrowRight',
|
||||
'Shift + ArrowRight',
|
||||
'Ctrl + Shift + ArrowRight',
|
||||
'ArrowUp',
|
||||
'Shift + ArrowUp',
|
||||
'ArrowDown',
|
||||
'Shift + ArrowDown'
|
||||
])
|
||||
11
packages/keybinding/src/index.ts
Normal file
11
packages/keybinding/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// Types
|
||||
export type * from './types/keybinding'
|
||||
export { zKeybinding } from './types/keybinding'
|
||||
|
||||
// Models (Implementation classes)
|
||||
export { KeyComboImpl } from './models/KeyCombo'
|
||||
export { KeybindingImpl } from './models/Keybinding'
|
||||
|
||||
// Constants
|
||||
export { CORE_KEYBINDINGS } from './constants/coreKeybindings'
|
||||
export { RESERVED_BY_TEXT_INPUT } from './constants/reservedKeyCombos'
|
||||
83
packages/keybinding/src/models/KeyCombo.ts
Normal file
83
packages/keybinding/src/models/KeyCombo.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { RESERVED_BY_TEXT_INPUT } from '../constants/reservedKeyCombos'
|
||||
import type { KeyCombo } from '../types/keybinding'
|
||||
|
||||
export class KeyComboImpl implements KeyCombo {
|
||||
key: string
|
||||
// ctrl or meta(cmd on mac)
|
||||
ctrl: boolean
|
||||
alt: boolean
|
||||
shift: boolean
|
||||
|
||||
constructor(obj: KeyCombo) {
|
||||
this.key = obj.key
|
||||
this.ctrl = obj.ctrl ?? false
|
||||
this.alt = obj.alt ?? false
|
||||
this.shift = obj.shift ?? false
|
||||
}
|
||||
|
||||
static fromEvent(event: KeyboardEvent) {
|
||||
return new KeyComboImpl({
|
||||
key: event.key,
|
||||
ctrl: event.ctrlKey || event.metaKey,
|
||||
alt: event.altKey,
|
||||
shift: event.shiftKey
|
||||
})
|
||||
}
|
||||
|
||||
equals(other: unknown): boolean {
|
||||
return other instanceof KeyComboImpl
|
||||
? this.key.toUpperCase() === other.key.toUpperCase() &&
|
||||
this.ctrl === other.ctrl &&
|
||||
this.alt === other.alt &&
|
||||
this.shift === other.shift
|
||||
: false
|
||||
}
|
||||
|
||||
serialize(): string {
|
||||
return `${this.key.toUpperCase()}:${this.ctrl}:${this.alt}:${this.shift}`
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.getKeySequences().join(' + ')
|
||||
}
|
||||
|
||||
get hasModifier(): boolean {
|
||||
return this.ctrl || this.alt || this.shift
|
||||
}
|
||||
|
||||
get isModifier(): boolean {
|
||||
return ['Control', 'Meta', 'Alt', 'Shift'].includes(this.key)
|
||||
}
|
||||
|
||||
get modifierCount(): number {
|
||||
const modifiers = [this.ctrl, this.alt, this.shift]
|
||||
return modifiers.reduce((acc, cur) => acc + Number(cur), 0)
|
||||
}
|
||||
|
||||
get isShiftOnly(): boolean {
|
||||
return this.shift && this.modifierCount === 1
|
||||
}
|
||||
|
||||
get isReservedByTextInput(): boolean {
|
||||
return (
|
||||
!this.hasModifier ||
|
||||
this.isShiftOnly ||
|
||||
RESERVED_BY_TEXT_INPUT.has(this.toString())
|
||||
)
|
||||
}
|
||||
|
||||
getKeySequences(): string[] {
|
||||
const sequences: string[] = []
|
||||
if (this.ctrl) {
|
||||
sequences.push('Ctrl')
|
||||
}
|
||||
if (this.alt) {
|
||||
sequences.push('Alt')
|
||||
}
|
||||
if (this.shift) {
|
||||
sequences.push('Shift')
|
||||
}
|
||||
sequences.push(this.key)
|
||||
return sequences
|
||||
}
|
||||
}
|
||||
22
packages/keybinding/src/models/Keybinding.ts
Normal file
22
packages/keybinding/src/models/Keybinding.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { Keybinding } from '../types/keybinding'
|
||||
import { KeyComboImpl } from './KeyCombo'
|
||||
|
||||
export class KeybindingImpl implements Keybinding {
|
||||
commandId: string
|
||||
combo: KeyComboImpl
|
||||
targetElementId?: string
|
||||
|
||||
constructor(obj: Keybinding) {
|
||||
this.commandId = obj.commandId
|
||||
this.combo = new KeyComboImpl(obj.combo)
|
||||
this.targetElementId = obj.targetElementId
|
||||
}
|
||||
|
||||
equals(other: unknown): boolean {
|
||||
return other instanceof KeybindingImpl
|
||||
? this.commandId === other.commandId &&
|
||||
this.combo.equals(other.combo) &&
|
||||
this.targetElementId === other.targetElementId
|
||||
: false
|
||||
}
|
||||
}
|
||||
25
packages/keybinding/src/types/keybinding.ts
Normal file
25
packages/keybinding/src/types/keybinding.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
// KeyCombo schema
|
||||
const zKeyCombo = z.object({
|
||||
key: z.string(),
|
||||
ctrl: z.boolean().optional(),
|
||||
alt: z.boolean().optional(),
|
||||
shift: z.boolean().optional(),
|
||||
meta: z.boolean().optional()
|
||||
})
|
||||
|
||||
// Keybinding schema
|
||||
export const zKeybinding = z.object({
|
||||
commandId: z.string(),
|
||||
combo: zKeyCombo,
|
||||
// Optional target element ID to limit keybinding to.
|
||||
// Note: Currently only used to distinguish between global keybindings
|
||||
// and litegraph canvas keybindings.
|
||||
// Do NOT use this field in extensions as it has no effect.
|
||||
targetElementId: z.string().optional()
|
||||
})
|
||||
|
||||
// Infer types from schemas
|
||||
export type KeyCombo = z.infer<typeof zKeyCombo>
|
||||
export type Keybinding = z.infer<typeof zKeybinding>
|
||||
12
packages/keybinding/tsconfig.json
Normal file
12
packages/keybinding/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
Reference in New Issue
Block a user