mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-04 15:10:06 +00:00
[feat] Prevent browser zoom on UI components with canvas wheel event forwarding (#4574)
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<ButtonGroup
|
||||
class="p-buttongroup-vertical absolute bottom-[10px] right-[10px] z-[1000]"
|
||||
@wheel="canvasInteractions.handleWheel"
|
||||
>
|
||||
<Button
|
||||
v-tooltip.left="t('graphCanvasMenu.zoomIn')"
|
||||
@@ -75,6 +76,7 @@ import ButtonGroup from 'primevue/buttongroup'
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
@@ -83,6 +85,7 @@ const { t } = useI18n()
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const settingStore = useSettingStore()
|
||||
const canvasInteractions = useCanvasInteractions()
|
||||
|
||||
const minimapVisible = computed(() => settingStore.get('Comfy.Minimap.Visible'))
|
||||
const linkHidden = computed(
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
header: 'hidden',
|
||||
content: 'p-0 flex flex-row'
|
||||
}"
|
||||
@wheel="canvasInteractions.handleWheel"
|
||||
>
|
||||
<ExecuteButton />
|
||||
<ColorPickerButton />
|
||||
@@ -39,6 +40,7 @@ import HelpButton from '@/components/graph/selectionToolbox/HelpButton.vue'
|
||||
import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'
|
||||
import PinButton from '@/components/graph/selectionToolbox/PinButton.vue'
|
||||
import RefreshButton from '@/components/graph/selectionToolbox/RefreshButton.vue'
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { useExtensionService } from '@/services/extensionService'
|
||||
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
@@ -46,6 +48,7 @@ import { useCanvasStore } from '@/stores/graphStore'
|
||||
const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const extensionService = useExtensionService()
|
||||
const canvasInteractions = useCanvasInteractions()
|
||||
|
||||
const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
|
||||
const commandIds = new Set<string>(
|
||||
|
||||
59
src/composables/graph/useCanvasInteractions.ts
Normal file
59
src/composables/graph/useCanvasInteractions.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { computed } from 'vue'
|
||||
|
||||
import { app } from '@/scripts/app'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
/**
|
||||
* Composable for handling canvas interactions from Vue components.
|
||||
* This provides a unified way to forward events to the LiteGraph canvas
|
||||
* and will be the foundation for migrating canvas interactions to Vue.
|
||||
*/
|
||||
export function useCanvasInteractions() {
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const isStandardNavMode = computed(
|
||||
() => settingStore.get('Comfy.Canvas.NavigationMode') === 'standard'
|
||||
)
|
||||
|
||||
/**
|
||||
* Handles wheel events from UI components that should be forwarded to canvas
|
||||
* when appropriate (e.g., Ctrl+wheel for zoom in standard mode)
|
||||
*/
|
||||
const handleWheel = (event: WheelEvent) => {
|
||||
// In standard mode, Ctrl+wheel should go to canvas for zoom
|
||||
if (isStandardNavMode.value && (event.ctrlKey || event.metaKey)) {
|
||||
event.preventDefault() // Prevent browser zoom
|
||||
forwardEventToCanvas(event)
|
||||
return
|
||||
}
|
||||
|
||||
// In legacy mode, all wheel events go to canvas for zoom
|
||||
if (!isStandardNavMode.value) {
|
||||
event.preventDefault()
|
||||
forwardEventToCanvas(event)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, let the component handle it normally
|
||||
}
|
||||
|
||||
/**
|
||||
* Forwards an event to the LiteGraph canvas
|
||||
*/
|
||||
const forwardEventToCanvas = (
|
||||
event: WheelEvent | PointerEvent | MouseEvent
|
||||
) => {
|
||||
const canvasEl = app.canvas?.canvas
|
||||
if (!canvasEl) return
|
||||
|
||||
// Create new event with same properties
|
||||
const EventConstructor = event.constructor as typeof WheelEvent
|
||||
const newEvent = new EventConstructor(event.type, event)
|
||||
canvasEl.dispatchEvent(newEvent)
|
||||
}
|
||||
|
||||
return {
|
||||
handleWheel,
|
||||
forwardEventToCanvas
|
||||
}
|
||||
}
|
||||
126
tests-ui/tests/composables/graph/useCanvasInteractions.test.ts
Normal file
126
tests-ui/tests/composables/graph/useCanvasInteractions.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||
import { app } from '@/scripts/app'
|
||||
import * as settingStore from '@/stores/settingStore'
|
||||
|
||||
// Mock the app and canvas
|
||||
vi.mock('@/scripts/app', () => ({
|
||||
app: {
|
||||
canvas: {
|
||||
canvas: null as HTMLCanvasElement | null
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
// Mock the setting store
|
||||
vi.mock('@/stores/settingStore', () => ({
|
||||
useSettingStore: vi.fn()
|
||||
}))
|
||||
|
||||
describe('useCanvasInteractions', () => {
|
||||
let mockCanvas: HTMLCanvasElement
|
||||
let mockSettingStore: { get: ReturnType<typeof vi.fn> }
|
||||
let canvasInteractions: ReturnType<typeof useCanvasInteractions>
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear mocks
|
||||
vi.clearAllMocks()
|
||||
|
||||
// Create mock canvas element
|
||||
mockCanvas = document.createElement('canvas')
|
||||
mockCanvas.dispatchEvent = vi.fn()
|
||||
app.canvas!.canvas = mockCanvas
|
||||
|
||||
// Mock setting store
|
||||
mockSettingStore = { get: vi.fn() }
|
||||
vi.mocked(settingStore.useSettingStore).mockReturnValue(
|
||||
mockSettingStore as any
|
||||
)
|
||||
|
||||
canvasInteractions = useCanvasInteractions()
|
||||
})
|
||||
|
||||
describe('handleWheel', () => {
|
||||
it('should check navigation mode from settings', () => {
|
||||
mockSettingStore.get.mockReturnValue('standard')
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', {
|
||||
ctrlKey: true,
|
||||
deltaY: -100
|
||||
})
|
||||
|
||||
canvasInteractions.handleWheel(wheelEvent)
|
||||
|
||||
expect(mockSettingStore.get).toHaveBeenCalledWith(
|
||||
'Comfy.Canvas.NavigationMode'
|
||||
)
|
||||
})
|
||||
|
||||
it('should not forward regular wheel events in standard mode', () => {
|
||||
mockSettingStore.get.mockReturnValue('standard')
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', {
|
||||
deltaY: -100
|
||||
})
|
||||
|
||||
canvasInteractions.handleWheel(wheelEvent)
|
||||
|
||||
expect(mockCanvas.dispatchEvent).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should forward all wheel events to canvas in legacy mode', () => {
|
||||
mockSettingStore.get.mockReturnValue('legacy')
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', {
|
||||
deltaY: -100,
|
||||
cancelable: true
|
||||
})
|
||||
|
||||
canvasInteractions.handleWheel(wheelEvent)
|
||||
|
||||
expect(mockCanvas.dispatchEvent).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should handle missing canvas gracefully', () => {
|
||||
;(app.canvas as any).canvas = null
|
||||
mockSettingStore.get.mockReturnValue('standard')
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', {
|
||||
ctrlKey: true,
|
||||
deltaY: -100
|
||||
})
|
||||
|
||||
expect(() => {
|
||||
canvasInteractions.handleWheel(wheelEvent)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('forwardEventToCanvas', () => {
|
||||
it('should dispatch event to canvas element', () => {
|
||||
const wheelEvent = new WheelEvent('wheel', {
|
||||
deltaY: -100,
|
||||
ctrlKey: true
|
||||
})
|
||||
|
||||
canvasInteractions.forwardEventToCanvas(wheelEvent)
|
||||
|
||||
expect(mockCanvas.dispatchEvent).toHaveBeenCalledWith(
|
||||
expect.any(WheelEvent)
|
||||
)
|
||||
})
|
||||
|
||||
it('should handle missing canvas gracefully', () => {
|
||||
;(app.canvas as any).canvas = null
|
||||
|
||||
const wheelEvent = new WheelEvent('wheel', {
|
||||
deltaY: -100
|
||||
})
|
||||
|
||||
expect(() => {
|
||||
canvasInteractions.forwardEventToCanvas(wheelEvent)
|
||||
}).not.toThrow()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user