mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-03 11:09:10 +00:00
Compare commits
1 Commits
fix/codera
...
fix/codera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8db2739bc |
@@ -51,17 +51,6 @@ export function createMonotoneInterpolator(
|
||||
}
|
||||
}
|
||||
|
||||
const segCount = n - 1
|
||||
const segDx = new Float64Array(segCount)
|
||||
const segM0dx = new Float64Array(segCount)
|
||||
const segM1dx = new Float64Array(segCount)
|
||||
for (let i = 0; i < segCount; i++) {
|
||||
const dx = xs[i + 1] - xs[i]
|
||||
segDx[i] = dx
|
||||
segM0dx[i] = slopes[i] * dx
|
||||
segM1dx[i] = slopes[i + 1] * dx
|
||||
}
|
||||
|
||||
return (x: number): number => {
|
||||
if (x <= xs[0]) return ys[0]
|
||||
if (x >= xs[n - 1]) return ys[n - 1]
|
||||
@@ -74,7 +63,7 @@ export function createMonotoneInterpolator(
|
||||
else hi = mid
|
||||
}
|
||||
|
||||
const dx = segDx[lo]
|
||||
const dx = xs[hi] - xs[lo]
|
||||
if (dx === 0) return ys[lo]
|
||||
|
||||
const t = (x - xs[lo]) / dx
|
||||
@@ -86,7 +75,12 @@ export function createMonotoneInterpolator(
|
||||
const h01 = -2 * t3 + 3 * t2
|
||||
const h11 = t3 - t2
|
||||
|
||||
return h00 * ys[lo] + h10 * segM0dx[lo] + h01 * ys[hi] + h11 * segM1dx[lo]
|
||||
return (
|
||||
h00 * ys[lo] +
|
||||
h10 * dx * slopes[lo] +
|
||||
h01 * ys[hi] +
|
||||
h11 * dx * slopes[hi]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
78
src/composables/element/useDomValueBridge.test.ts
Normal file
78
src/composables/element/useDomValueBridge.test.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import { nextTick } from 'vue'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { useDomValueBridge } from './useDomValueBridge'
|
||||
|
||||
function createInput(initialValue = ''): HTMLInputElement {
|
||||
const el = document.createElement('input')
|
||||
el.value = initialValue
|
||||
return el
|
||||
}
|
||||
|
||||
function createTextarea(initialValue = ''): HTMLTextAreaElement {
|
||||
const el = document.createElement('textarea')
|
||||
el.value = initialValue
|
||||
return el
|
||||
}
|
||||
|
||||
describe('useDomValueBridge', () => {
|
||||
it('initializes the ref with the current element value', () => {
|
||||
const el = createInput('hello')
|
||||
const bridged = useDomValueBridge(el)
|
||||
expect(bridged.value).toBe('hello')
|
||||
})
|
||||
|
||||
it('updates the ref when element.value is set programmatically', () => {
|
||||
const el = createInput('')
|
||||
const bridged = useDomValueBridge(el)
|
||||
|
||||
el.value = 'updated'
|
||||
expect(bridged.value).toBe('updated')
|
||||
})
|
||||
|
||||
it('updates the ref on user input events', () => {
|
||||
const el = createInput('')
|
||||
const bridged = useDomValueBridge(el)
|
||||
|
||||
// Simulate user typing by using the original descriptor to set value,
|
||||
// then dispatching an input event
|
||||
const proto = Object.getPrototypeOf(el)
|
||||
const desc = Object.getOwnPropertyDescriptor(proto, 'value')
|
||||
desc?.set?.call(el, 'typed')
|
||||
el.dispatchEvent(new Event('input'))
|
||||
|
||||
expect(bridged.value).toBe('typed')
|
||||
})
|
||||
|
||||
it('updates the DOM element when the ref is written to', async () => {
|
||||
const el = createInput('initial')
|
||||
const bridged = useDomValueBridge(el)
|
||||
|
||||
bridged.value = 'from-ref'
|
||||
await nextTick()
|
||||
|
||||
expect(el.value).toBe('from-ref')
|
||||
})
|
||||
|
||||
it('works with textarea elements', () => {
|
||||
const el = createTextarea('initial')
|
||||
const bridged = useDomValueBridge(el)
|
||||
|
||||
expect(bridged.value).toBe('initial')
|
||||
el.value = 'new text'
|
||||
expect(bridged.value).toBe('new text')
|
||||
})
|
||||
|
||||
it('reads element value through the intercepted getter', async () => {
|
||||
const el = createInput('start')
|
||||
const bridged = useDomValueBridge(el)
|
||||
|
||||
// The getter on element.value should still work
|
||||
expect(el.value).toBe('start')
|
||||
|
||||
bridged.value = 'changed'
|
||||
await nextTick()
|
||||
// The element getter should reflect the latest set
|
||||
expect(el.value).toBe('changed')
|
||||
})
|
||||
})
|
||||
79
src/composables/element/useDomValueBridge.ts
Normal file
79
src/composables/element/useDomValueBridge.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { ref, watch } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
|
||||
type ValueElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement
|
||||
|
||||
/**
|
||||
* Bridges a DOM element's `.value` property to a Vue reactive ref.
|
||||
*
|
||||
* This composable provides a clean, public API for extension authors to
|
||||
* synchronize DOM widget values with Vue reactivity (and by extension, the
|
||||
* `widgetValueStore`). It works by:
|
||||
*
|
||||
* 1. Intercepting programmatic `.value` writes via `Object.defineProperty`
|
||||
* 2. Listening for user-driven `input` events on the element
|
||||
* 3. Exposing a reactive `Ref<string>` that stays in sync with the DOM
|
||||
*
|
||||
* When the returned ref is written to, the DOM element's value is updated.
|
||||
* When the DOM element's value changes (programmatically or via user input),
|
||||
* the ref is updated.
|
||||
*
|
||||
* @param element - The DOM element to bridge (input, textarea, or select)
|
||||
* @returns A reactive ref that stays in sync with the element's value
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // In a custom widget's getValue/setValue:
|
||||
* const bridgedValue = useDomValueBridge(inputEl)
|
||||
* const widget = node.addDOMWidget(name, type, inputEl, {
|
||||
* getValue: () => bridgedValue.value,
|
||||
* setValue: (v) => { bridgedValue.value = v }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export function useDomValueBridge(element: ValueElement): Ref<string> {
|
||||
const bridgedValue = ref(element.value)
|
||||
|
||||
// Capture the original property descriptor so we can chain through it
|
||||
const proto = Object.getPrototypeOf(element)
|
||||
const originalDescriptor =
|
||||
Object.getOwnPropertyDescriptor(element, 'value') ??
|
||||
Object.getOwnPropertyDescriptor(proto, 'value')
|
||||
|
||||
// Intercept programmatic .value writes on the element
|
||||
// This catches cases where extensions or libraries set element.value directly
|
||||
try {
|
||||
Object.defineProperty(element, 'value', {
|
||||
get() {
|
||||
return originalDescriptor?.get?.call(this) ?? bridgedValue.value
|
||||
},
|
||||
set(newValue: string) {
|
||||
originalDescriptor?.set?.call(this, newValue)
|
||||
bridgedValue.value = newValue
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true
|
||||
})
|
||||
} catch {
|
||||
// If the descriptor is non-configurable, fall back to polling-free sync
|
||||
// via input events only
|
||||
}
|
||||
|
||||
// Listen for user-driven input events
|
||||
element.addEventListener('input', () => {
|
||||
// Read through the original descriptor to avoid infinite loops
|
||||
const currentValue = originalDescriptor?.get?.call(element) ?? element.value
|
||||
bridgedValue.value = currentValue
|
||||
})
|
||||
|
||||
// When the ref is written to externally, update the DOM element
|
||||
watch(bridgedValue, (newValue) => {
|
||||
const currentDomValue =
|
||||
originalDescriptor?.get?.call(element) ?? element.value
|
||||
if (currentDomValue !== newValue) {
|
||||
originalDescriptor?.set?.call(element, newValue)
|
||||
}
|
||||
})
|
||||
|
||||
return bridgedValue
|
||||
}
|
||||
@@ -905,14 +905,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
app.canvas.pasteFromClipboard()
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.PasteFromClipboardWithConnect',
|
||||
icon: 'icon-[lucide--clipboard-paste]',
|
||||
label: () => t('Paste with Connect'),
|
||||
function: () => {
|
||||
app.canvas.pasteFromClipboard({ connectInputs: true })
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.SelectAll',
|
||||
icon: 'icon-[lucide--lasso-select]',
|
||||
@@ -927,12 +919,6 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
label: 'Delete Selected Items',
|
||||
versionAdded: '1.10.5',
|
||||
function: () => {
|
||||
if (app.canvas.selectedItems.size === 0) {
|
||||
app.canvas.canvas.dispatchEvent(
|
||||
new CustomEvent('litegraph:no-items-selected', { bubbles: true })
|
||||
)
|
||||
return
|
||||
}
|
||||
app.canvas.deleteSelected()
|
||||
app.canvas.setDirty(true, true)
|
||||
}
|
||||
|
||||
@@ -189,10 +189,11 @@ export function useWorkflowActionsMenu(
|
||||
|
||||
addItem({
|
||||
id: 'share',
|
||||
label: t('breadcrumbsMenu.share'),
|
||||
label: t('menuLabels.Share'),
|
||||
icon: 'icon-[comfy--send]',
|
||||
command: async () => {},
|
||||
visible: false
|
||||
disabled: true,
|
||||
visible: isRoot
|
||||
})
|
||||
|
||||
addItem({
|
||||
|
||||
@@ -3791,6 +3791,13 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
return
|
||||
}
|
||||
|
||||
private _noItemsSelected(): void {
|
||||
const event = new CustomEvent('litegraph:no-items-selected', {
|
||||
bubbles: true
|
||||
})
|
||||
this.canvas.dispatchEvent(event)
|
||||
}
|
||||
|
||||
/**
|
||||
* process a key event
|
||||
*/
|
||||
@@ -3835,6 +3842,31 @@ export class LGraphCanvas implements CustomEventDispatcher<LGraphCanvasEventMap>
|
||||
this.node_panel?.close()
|
||||
this.options_panel?.close()
|
||||
if (this.node_panel || this.options_panel) block_default = true
|
||||
} else if (e.keyCode === 65 && e.ctrlKey) {
|
||||
// select all Control A
|
||||
this.selectItems()
|
||||
block_default = true
|
||||
} else if (e.keyCode === 67 && (e.metaKey || e.ctrlKey) && !e.shiftKey) {
|
||||
// copy
|
||||
if (this.selected_nodes) {
|
||||
this.copyToClipboard()
|
||||
block_default = true
|
||||
}
|
||||
} else if (e.keyCode === 86 && (e.metaKey || e.ctrlKey)) {
|
||||
// paste
|
||||
this.pasteFromClipboard({ connectInputs: e.shiftKey })
|
||||
} else if (e.key === 'Delete' || e.key === 'Backspace') {
|
||||
// delete or backspace
|
||||
// @ts-expect-error EventTarget.localName is not in standard types
|
||||
if (e.target.localName != 'input' && e.target.localName != 'textarea') {
|
||||
if (this.selectedItems.size === 0) {
|
||||
this._noItemsSelected()
|
||||
return
|
||||
}
|
||||
|
||||
this.deleteSelected()
|
||||
block_default = true
|
||||
}
|
||||
}
|
||||
|
||||
// TODO
|
||||
|
||||
@@ -1262,7 +1262,6 @@
|
||||
"Move Selected Nodes Right": "Move Selected Nodes Right",
|
||||
"Move Selected Nodes Up": "Move Selected Nodes Up",
|
||||
"Paste": "Paste",
|
||||
"Paste with Connect": "Paste with Connect",
|
||||
"Reset View": "Reset View",
|
||||
"Resize Selected Nodes": "Resize Selected Nodes",
|
||||
"Select All": "Select All",
|
||||
@@ -2604,8 +2603,7 @@
|
||||
"deleteWorkflow": "Delete Workflow",
|
||||
"deleteBlueprint": "Delete Blueprint",
|
||||
"enterNewName": "Enter new name",
|
||||
"missingNodesWarning": "Workflow contains unsupported nodes (highlighted red).",
|
||||
"share": "Share"
|
||||
"missingNodesWarning": "Workflow contains unsupported nodes (highlighted red)."
|
||||
},
|
||||
"shortcuts": {
|
||||
"shortcuts": "Shortcuts",
|
||||
|
||||
@@ -208,52 +208,5 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
||||
key: 'Escape'
|
||||
},
|
||||
commandId: 'Comfy.Graph.ExitSubgraph'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
key: 'a'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.SelectAll',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
key: 'c'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.CopySelected',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
key: 'v'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.PasteFromClipboard',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
ctrl: true,
|
||||
shift: true,
|
||||
key: 'v'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.PasteFromClipboardWithConnect',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'Delete'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.DeleteSelectedItems',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'Backspace'
|
||||
},
|
||||
commandId: 'Comfy.Canvas.DeleteSelectedItems',
|
||||
targetElementId: 'graph-canvas-container'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useKeybindingService } from '@/platform/keybindings/keybindingService'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
vi.mock('@/scripts/app', () => {
|
||||
return {
|
||||
app: {
|
||||
canvas: {
|
||||
processKey: vi.fn()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/platform/settings/settingStore', () => ({
|
||||
useSettingStore: vi.fn(() => ({
|
||||
get: vi.fn(() => [])
|
||||
@@ -25,15 +36,13 @@ function createTestKeyboardEvent(
|
||||
ctrlKey?: boolean
|
||||
altKey?: boolean
|
||||
metaKey?: boolean
|
||||
shiftKey?: boolean
|
||||
} = {}
|
||||
): KeyboardEvent {
|
||||
const {
|
||||
target = document.body,
|
||||
ctrlKey = false,
|
||||
altKey = false,
|
||||
metaKey = false,
|
||||
shiftKey = false
|
||||
metaKey = false
|
||||
} = options
|
||||
|
||||
const event = new KeyboardEvent('keydown', {
|
||||
@@ -41,7 +50,6 @@ function createTestKeyboardEvent(
|
||||
ctrlKey,
|
||||
altKey,
|
||||
metaKey,
|
||||
shiftKey,
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
})
|
||||
@@ -52,10 +60,8 @@ function createTestKeyboardEvent(
|
||||
return event
|
||||
}
|
||||
|
||||
describe('keybindingService - Canvas Keybindings', () => {
|
||||
describe('keybindingService - Event Forwarding', () => {
|
||||
let keybindingService: ReturnType<typeof useKeybindingService>
|
||||
let canvasContainer: HTMLDivElement
|
||||
let canvasChild: HTMLCanvasElement
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
@@ -70,156 +76,94 @@ describe('keybindingService - Canvas Keybindings', () => {
|
||||
typeof useDialogStore
|
||||
>)
|
||||
|
||||
canvasContainer = document.createElement('div')
|
||||
canvasContainer.id = 'graph-canvas-container'
|
||||
canvasChild = document.createElement('canvas')
|
||||
canvasContainer.appendChild(canvasChild)
|
||||
document.body.appendChild(canvasContainer)
|
||||
|
||||
keybindingService = useKeybindingService()
|
||||
keybindingService.registerCoreKeybindings()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
canvasContainer.remove()
|
||||
})
|
||||
|
||||
it('should execute DeleteSelectedItems for Delete key on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('Delete', {
|
||||
target: canvasChild
|
||||
})
|
||||
it('should forward Delete key to canvas when no keybinding exists', async () => {
|
||||
const event = createTestKeyboardEvent('Delete')
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.DeleteSelectedItems'
|
||||
)
|
||||
expect(vi.mocked(app.canvas.processKey)).toHaveBeenCalledWith(event)
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should execute DeleteSelectedItems for Backspace key on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('Backspace', {
|
||||
target: canvasChild
|
||||
})
|
||||
it('should forward Backspace key to canvas when no keybinding exists', async () => {
|
||||
const event = createTestKeyboardEvent('Backspace')
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.DeleteSelectedItems'
|
||||
)
|
||||
expect(vi.mocked(app.canvas.processKey)).toHaveBeenCalledWith(event)
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not execute DeleteSelectedItems when typing in input field', async () => {
|
||||
it('should not forward Delete key when typing in input field', async () => {
|
||||
const inputElement = document.createElement('input')
|
||||
const event = createTestKeyboardEvent('Delete', { target: inputElement })
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not execute DeleteSelectedItems when typing in textarea', async () => {
|
||||
it('should not forward Delete key when typing in textarea', async () => {
|
||||
const textareaElement = document.createElement('textarea')
|
||||
const event = createTestKeyboardEvent('Delete', {
|
||||
target: textareaElement
|
||||
})
|
||||
const event = createTestKeyboardEvent('Delete', { target: textareaElement })
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should execute SelectAll for Ctrl+A on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('a', {
|
||||
ctrlKey: true,
|
||||
target: canvasChild
|
||||
})
|
||||
it('should not forward Delete key when canvas processKey is not available', async () => {
|
||||
// Temporarily replace processKey with undefined - testing edge case
|
||||
const originalProcessKey = vi.mocked(app.canvas).processKey
|
||||
vi.mocked(app.canvas).processKey = undefined!
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
const event = createTestKeyboardEvent('Delete')
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.SelectAll'
|
||||
)
|
||||
try {
|
||||
await keybindingService.keybindHandler(event)
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
// Restore processKey for other tests
|
||||
vi.mocked(app.canvas).processKey = originalProcessKey
|
||||
}
|
||||
})
|
||||
|
||||
it('should execute CopySelected for Ctrl+C on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('c', {
|
||||
ctrlKey: true,
|
||||
target: canvasChild
|
||||
})
|
||||
it('should not forward Delete key when canvas is not available', async () => {
|
||||
const originalCanvas = vi.mocked(app).canvas
|
||||
vi.mocked(app).canvas = null!
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
const event = createTestKeyboardEvent('Delete')
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.CopySelected'
|
||||
)
|
||||
try {
|
||||
await keybindingService.keybindHandler(event)
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
// Restore canvas for other tests
|
||||
vi.mocked(app).canvas = originalCanvas
|
||||
}
|
||||
})
|
||||
|
||||
it('should execute PasteFromClipboard for Ctrl+V on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('v', {
|
||||
ctrlKey: true,
|
||||
target: canvasChild
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.PasteFromClipboard'
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute PasteFromClipboardWithConnect for Ctrl+Shift+V on canvas', async () => {
|
||||
const event = createTestKeyboardEvent('v', {
|
||||
ctrlKey: true,
|
||||
shiftKey: true,
|
||||
target: canvasChild
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.PasteFromClipboardWithConnect'
|
||||
)
|
||||
})
|
||||
|
||||
it('should execute graph-canvas bindings by normalizing to graph-canvas-container', async () => {
|
||||
const event = createTestKeyboardEvent('=', {
|
||||
altKey: true,
|
||||
target: canvasChild
|
||||
})
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(useCommandStore().execute)).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.ZoomIn'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not execute graph-canvas bindings when target is outside canvas', async () => {
|
||||
const outsideDiv = document.createElement('div')
|
||||
document.body.appendChild(outsideDiv)
|
||||
|
||||
const event = createTestKeyboardEvent('=', {
|
||||
altKey: true,
|
||||
target: outsideDiv
|
||||
})
|
||||
it('should not forward non-canvas keys', async () => {
|
||||
const event = createTestKeyboardEvent('Enter')
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
outsideDiv.remove()
|
||||
})
|
||||
|
||||
it('should not execute canvas commands when target is outside canvas container', async () => {
|
||||
const outsideDiv = document.createElement('div')
|
||||
document.body.appendChild(outsideDiv)
|
||||
|
||||
const event = createTestKeyboardEvent('Delete', {
|
||||
target: outsideDiv
|
||||
})
|
||||
it('should not forward when modifier keys are pressed', async () => {
|
||||
const event = createTestKeyboardEvent('Delete', { ctrlKey: true })
|
||||
|
||||
await keybindingService.keybindHandler(event)
|
||||
|
||||
expect(vi.mocked(app.canvas.processKey)).not.toHaveBeenCalled()
|
||||
expect(vi.mocked(useCommandStore().execute)).not.toHaveBeenCalled()
|
||||
outsideDiv.remove()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { isCloud } from '@/platform/distribution/types'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { app } from '@/scripts/app'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useDialogStore } from '@/stores/dialogStore'
|
||||
|
||||
@@ -14,6 +15,16 @@ export function useKeybindingService() {
|
||||
const settingStore = useSettingStore()
|
||||
const dialogStore = useDialogStore()
|
||||
|
||||
function shouldForwardToCanvas(event: KeyboardEvent): boolean {
|
||||
if (event.ctrlKey || event.altKey || event.metaKey) {
|
||||
return false
|
||||
}
|
||||
|
||||
const canvasKeys = ['Delete', 'Backspace']
|
||||
|
||||
return canvasKeys.includes(event.key)
|
||||
}
|
||||
|
||||
async function keybindHandler(event: KeyboardEvent) {
|
||||
const keyCombo = KeyComboImpl.fromEvent(event)
|
||||
if (keyCombo.isModifier) {
|
||||
@@ -33,17 +44,7 @@ export function useKeybindingService() {
|
||||
}
|
||||
|
||||
const keybinding = keybindingStore.getKeybinding(keyCombo)
|
||||
if (keybinding) {
|
||||
const targetElementId =
|
||||
keybinding.targetElementId === 'graph-canvas'
|
||||
? 'graph-canvas-container'
|
||||
: keybinding.targetElementId
|
||||
if (targetElementId) {
|
||||
const container = document.getElementById(targetElementId)
|
||||
if (!container?.contains(target)) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if (keybinding && keybinding.targetElementId !== 'graph-canvas') {
|
||||
if (
|
||||
event.key === 'Escape' &&
|
||||
!event.ctrlKey &&
|
||||
@@ -73,6 +74,18 @@ export function useKeybindingService() {
|
||||
return
|
||||
}
|
||||
|
||||
if (!keybinding && shouldForwardToCanvas(event)) {
|
||||
const canvas = app.canvas
|
||||
if (
|
||||
canvas &&
|
||||
canvas.processKey &&
|
||||
typeof canvas.processKey === 'function'
|
||||
) {
|
||||
canvas.processKey(event)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (event.ctrlKey || event.altKey || event.metaKey) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -676,6 +676,20 @@ export class ComfyApp {
|
||||
e.stopImmediatePropagation()
|
||||
return
|
||||
}
|
||||
|
||||
// Ctrl+C Copy
|
||||
if (e.key === 'c' && (e.metaKey || e.ctrlKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
// Ctrl+V Paste
|
||||
if (
|
||||
(e.key === 'v' || e.key == 'V') &&
|
||||
(e.metaKey || e.ctrlKey) &&
|
||||
!e.shiftKey
|
||||
) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Fall through to Litegraph defaults
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { type Component, toRaw } from 'vue'
|
||||
|
||||
import { useDomValueBridge } from '@/composables/element/useDomValueBridge'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import {
|
||||
LGraphNode,
|
||||
@@ -379,6 +380,16 @@ export const addWidget = <W extends BaseDOMWidget<object | string>>(
|
||||
})
|
||||
}
|
||||
|
||||
function isValueElement(
|
||||
el: HTMLElement
|
||||
): el is HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement {
|
||||
return (
|
||||
el instanceof HTMLInputElement ||
|
||||
el instanceof HTMLTextAreaElement ||
|
||||
el instanceof HTMLSelectElement
|
||||
)
|
||||
}
|
||||
|
||||
LGraphNode.prototype.addDOMWidget = function <
|
||||
T extends HTMLElement,
|
||||
V extends object | string
|
||||
@@ -389,6 +400,19 @@ LGraphNode.prototype.addDOMWidget = function <
|
||||
element: T,
|
||||
options: DOMWidgetOptions<V> = {}
|
||||
): DOMWidget<T, V> {
|
||||
// Auto-bridge value-bearing elements when no getValue/setValue provided.
|
||||
// This gives extension authors automatic widgetValueStore integration.
|
||||
if (!options.getValue && !options.setValue && isValueElement(element)) {
|
||||
const bridgedValue = useDomValueBridge(element)
|
||||
options = {
|
||||
...options,
|
||||
getValue: (() => bridgedValue.value) as () => V,
|
||||
setValue: ((v: V) => {
|
||||
bridgedValue.value = String(v)
|
||||
}) as (v: V) => void
|
||||
}
|
||||
}
|
||||
|
||||
const widget = new DOMWidgetImpl({
|
||||
node: this,
|
||||
name,
|
||||
|
||||
Reference in New Issue
Block a user