mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +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>
|
<template>
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
class="p-buttongroup-vertical absolute bottom-[10px] right-[10px] z-[1000]"
|
class="p-buttongroup-vertical absolute bottom-[10px] right-[10px] z-[1000]"
|
||||||
|
@wheel="canvasInteractions.handleWheel"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
v-tooltip.left="t('graphCanvasMenu.zoomIn')"
|
v-tooltip.left="t('graphCanvasMenu.zoomIn')"
|
||||||
@@ -75,6 +76,7 @@ import ButtonGroup from 'primevue/buttongroup'
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import { useCanvasStore } from '@/stores/graphStore'
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
@@ -83,6 +85,7 @@ const { t } = useI18n()
|
|||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
const canvasInteractions = useCanvasInteractions()
|
||||||
|
|
||||||
const minimapVisible = computed(() => settingStore.get('Comfy.Minimap.Visible'))
|
const minimapVisible = computed(() => settingStore.get('Comfy.Minimap.Visible'))
|
||||||
const linkHidden = computed(
|
const linkHidden = computed(
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
header: 'hidden',
|
header: 'hidden',
|
||||||
content: 'p-0 flex flex-row'
|
content: 'p-0 flex flex-row'
|
||||||
}"
|
}"
|
||||||
|
@wheel="canvasInteractions.handleWheel"
|
||||||
>
|
>
|
||||||
<ExecuteButton />
|
<ExecuteButton />
|
||||||
<ColorPickerButton />
|
<ColorPickerButton />
|
||||||
@@ -39,6 +40,7 @@ import HelpButton from '@/components/graph/selectionToolbox/HelpButton.vue'
|
|||||||
import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'
|
import MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'
|
||||||
import PinButton from '@/components/graph/selectionToolbox/PinButton.vue'
|
import PinButton from '@/components/graph/selectionToolbox/PinButton.vue'
|
||||||
import RefreshButton from '@/components/graph/selectionToolbox/RefreshButton.vue'
|
import RefreshButton from '@/components/graph/selectionToolbox/RefreshButton.vue'
|
||||||
|
import { useCanvasInteractions } from '@/composables/graph/useCanvasInteractions'
|
||||||
import { useExtensionService } from '@/services/extensionService'
|
import { useExtensionService } from '@/services/extensionService'
|
||||||
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
|
import { type ComfyCommandImpl, useCommandStore } from '@/stores/commandStore'
|
||||||
import { useCanvasStore } from '@/stores/graphStore'
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
@@ -46,6 +48,7 @@ import { useCanvasStore } from '@/stores/graphStore'
|
|||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
const extensionService = useExtensionService()
|
const extensionService = useExtensionService()
|
||||||
|
const canvasInteractions = useCanvasInteractions()
|
||||||
|
|
||||||
const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
|
const extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {
|
||||||
const commandIds = new Set<string>(
|
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