Files
ComfyUI_frontend/src/composables/node/useNodeAnimatedImage.ts
pythongosssss 4a05d89fdb fix: detach DOM widget event listeners on widget removal (#11724)
## Summary

Fixes leaked event listeners

## Changes

- **What**: 
- update all listeners to use AbortController to signal removal on
widget remove

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11724-fix-detach-DOM-widget-event-listeners-on-widget-removal-3506d73d3650811dae81c034c1098759)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-05-01 00:17:18 +00:00

84 lines
2.6 KiB
TypeScript

import { useChainCallback } from '@/composables/functional/useChainCallback'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'
import { ANIM_PREVIEW_WIDGET } from '@/scripts/app'
import { isDOMWidget } from '@/scripts/domWidget'
/**
* Composable for handling animated image previews in nodes
*/
export function useNodeAnimatedImage() {
/**
* Shows animated image preview for a node
* @param node The graph node to show the preview for
*/
function showAnimatedPreview(node: LGraphNode) {
if (!node.imgs?.length) return
if (!node.widgets) return
const widgetIdx = node.widgets.findIndex(
(w) => w.name === ANIM_PREVIEW_WIDGET
)
node.imgs[0].classList.value = 'block size-full object-contain'
if (widgetIdx > -1) {
// Replace content in existing widget
const widget = node.widgets[widgetIdx]
if (!isDOMWidget(widget)) return
widget.element.replaceChildren(node.imgs[0])
} else {
// Create new widget
const element = document.createElement('div')
element.appendChild(node.imgs[0])
const widget = node.addDOMWidget(ANIM_PREVIEW_WIDGET, 'img', element, {
hideOnZoom: false,
canvasOnly: true
})
node.overIndex = 0
// Add event listeners for canvas interactions
const { handleWheel, handlePointer, forwardEventToCanvas } =
useCanvasInteractions()
node.imgs[0].style.pointerEvents = 'none'
const controller = new AbortController()
const { signal } = controller
element.addEventListener('wheel', handleWheel, { signal })
element.addEventListener('pointermove', handlePointer, { signal })
element.addEventListener('pointerup', handlePointer, { signal })
element.addEventListener(
'pointerdown',
(e) => (e.button !== 2 ? handlePointer(e) : forwardEventToCanvas(e)),
{ capture: true, signal }
)
widget.onRemove = useChainCallback(widget.onRemove, () => {
controller.abort()
})
widget.serialize = false
widget.serializeValue = () => undefined
}
}
/**
* Removes animated image preview from a node
* @param node The graph node to remove the preview from
*/
function removeAnimatedPreview(node: LGraphNode) {
if (!node.widgets) return
const widgetIdx = node.widgets.findIndex(
(w) => w.name === ANIM_PREVIEW_WIDGET
)
if (widgetIdx > -1) {
node.widgets[widgetIdx].onRemove?.()
node.widgets.splice(widgetIdx, 1)
}
}
return {
showAnimatedPreview,
removeAnimatedPreview
}
}