mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
fix: migrate DOM widgets to ComponentWidgetImpl and add element value bridge
- Add useDomValueBridge composable that bridges DOM element .value to Vue reactivity via Object.defineProperty interception + input events - Wire bridge into addDOMWidget for extension widget compatibility - Migrate useStringWidget from raw textarea + addDOMWidget to ComponentWidgetImpl + WidgetTextarea.vue - Migrate useMarkdownWidget from Tiptap + addDOMWidget to ComponentWidgetImpl + WidgetMarkdown.vue - Remove all Tiptap dependencies (no longer used anywhere) - Add spellcheck setting support to WidgetTextarea.vue Fixes #9194 Amp-Thread-ID: https://ampcode.com/threads/T-019c977f-02f8-701d-b258-95157da8c261
This commit is contained in:
@@ -71,14 +71,6 @@
|
||||
"@primevue/themes": "catalog:",
|
||||
"@sentry/vue": "catalog:",
|
||||
"@sparkjsdev/spark": "catalog:",
|
||||
"@tiptap/core": "catalog:",
|
||||
"@tiptap/extension-link": "catalog:",
|
||||
"@tiptap/extension-table": "catalog:",
|
||||
"@tiptap/extension-table-cell": "catalog:",
|
||||
"@tiptap/extension-table-header": "catalog:",
|
||||
"@tiptap/extension-table-row": "catalog:",
|
||||
"@tiptap/pm": "catalog:",
|
||||
"@tiptap/starter-kit": "catalog:",
|
||||
"@vueuse/core": "catalog:",
|
||||
"@vueuse/integrations": "catalog:",
|
||||
"@vueuse/router": "^14.2.0",
|
||||
@@ -108,7 +100,6 @@
|
||||
"reka-ui": "catalog:",
|
||||
"semver": "^7.7.2",
|
||||
"three": "^0.170.0",
|
||||
"tiptap-markdown": "^0.8.10",
|
||||
"typegpu": "catalog:",
|
||||
"vue": "catalog:",
|
||||
"vue-i18n": "catalog:",
|
||||
|
||||
3030
pnpm-lock.yaml
generated
3030
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
155
src/composables/element/useDomValueBridge.test.ts
Normal file
155
src/composables/element/useDomValueBridge.test.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { effectScope, watch } from 'vue'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { useDomValueBridge } from './useDomValueBridge'
|
||||
|
||||
describe('useDomValueBridge', () => {
|
||||
let element: HTMLTextAreaElement
|
||||
|
||||
beforeEach(() => {
|
||||
element = document.createElement('textarea')
|
||||
element.value = 'initial'
|
||||
})
|
||||
|
||||
it('reads initial element value', () => {
|
||||
const scope = effectScope()
|
||||
scope.run(() => {
|
||||
const ref = useDomValueBridge(element)
|
||||
expect(ref.value).toBe('initial')
|
||||
})
|
||||
scope.stop()
|
||||
})
|
||||
|
||||
it('detects programmatic element.value writes', () => {
|
||||
const scope = effectScope()
|
||||
scope.run(() => {
|
||||
const ref = useDomValueBridge(element)
|
||||
const spy = vi.fn()
|
||||
watch(ref, spy, { flush: 'sync' })
|
||||
|
||||
element.value = 'programmatic'
|
||||
|
||||
expect(ref.value).toBe('programmatic')
|
||||
expect(spy).toHaveBeenCalledWith(
|
||||
'programmatic',
|
||||
'initial',
|
||||
expect.anything()
|
||||
)
|
||||
})
|
||||
scope.stop()
|
||||
})
|
||||
|
||||
it('detects user input events', () => {
|
||||
const scope = effectScope()
|
||||
scope.run(() => {
|
||||
const ref = useDomValueBridge(element)
|
||||
const spy = vi.fn()
|
||||
watch(ref, spy, { flush: 'sync' })
|
||||
|
||||
const nativeDesc = Object.getOwnPropertyDescriptor(
|
||||
HTMLTextAreaElement.prototype,
|
||||
'value'
|
||||
)!
|
||||
nativeDesc.set!.call(element, 'typed')
|
||||
element.dispatchEvent(new Event('input'))
|
||||
|
||||
expect(ref.value).toBe('typed')
|
||||
expect(spy).toHaveBeenCalled()
|
||||
})
|
||||
scope.stop()
|
||||
})
|
||||
|
||||
it('setting ref updates element value', () => {
|
||||
const scope = effectScope()
|
||||
scope.run(() => {
|
||||
const ref = useDomValueBridge(element)
|
||||
ref.value = 'from-ref'
|
||||
expect(element.value).toBe('from-ref')
|
||||
})
|
||||
scope.stop()
|
||||
})
|
||||
|
||||
it('chains through existing Object.defineProperty on element', () => {
|
||||
const existingSetter = vi.fn()
|
||||
|
||||
const nativeDesc = Object.getOwnPropertyDescriptor(
|
||||
HTMLTextAreaElement.prototype,
|
||||
'value'
|
||||
)!
|
||||
Object.defineProperty(element, 'value', {
|
||||
configurable: true,
|
||||
get() {
|
||||
return nativeDesc.get!.call(element)
|
||||
},
|
||||
set(v: string) {
|
||||
existingSetter(v)
|
||||
nativeDesc.set!.call(element, v)
|
||||
}
|
||||
})
|
||||
|
||||
const scope = effectScope()
|
||||
scope.run(() => {
|
||||
const ref = useDomValueBridge(element)
|
||||
|
||||
element.value = 'new'
|
||||
expect(existingSetter).toHaveBeenCalledWith('new')
|
||||
expect(ref.value).toBe('new')
|
||||
})
|
||||
scope.stop()
|
||||
})
|
||||
|
||||
it('restores previous descriptor on scope dispose', () => {
|
||||
const scope = effectScope()
|
||||
scope.run(() => {
|
||||
useDomValueBridge(element)
|
||||
})
|
||||
|
||||
const duringDesc = Object.getOwnPropertyDescriptor(element, 'value')
|
||||
expect(duringDesc).toBeDefined()
|
||||
|
||||
scope.stop()
|
||||
|
||||
const afterDesc = Object.getOwnPropertyDescriptor(element, 'value')
|
||||
expect(afterDesc).toBeUndefined()
|
||||
})
|
||||
|
||||
it('restores existing override descriptor on scope dispose', () => {
|
||||
const nativeDesc = Object.getOwnPropertyDescriptor(
|
||||
HTMLTextAreaElement.prototype,
|
||||
'value'
|
||||
)!
|
||||
const customGetter = vi.fn(() => nativeDesc.get!.call(element))
|
||||
|
||||
Object.defineProperty(element, 'value', {
|
||||
configurable: true,
|
||||
get: customGetter,
|
||||
set(v: string) {
|
||||
nativeDesc.set!.call(element, v)
|
||||
}
|
||||
})
|
||||
|
||||
const scope = effectScope()
|
||||
scope.run(() => {
|
||||
useDomValueBridge(element)
|
||||
})
|
||||
scope.stop()
|
||||
|
||||
element.value
|
||||
expect(customGetter).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('works with HTMLInputElement', () => {
|
||||
const input = document.createElement('input')
|
||||
input.value = 'input-initial'
|
||||
|
||||
const scope = effectScope()
|
||||
scope.run(() => {
|
||||
const ref = useDomValueBridge(input)
|
||||
expect(ref.value).toBe('input-initial')
|
||||
|
||||
input.value = 'input-updated'
|
||||
expect(ref.value).toBe('input-updated')
|
||||
})
|
||||
scope.stop()
|
||||
})
|
||||
})
|
||||
70
src/composables/element/useDomValueBridge.ts
Normal file
70
src/composables/element/useDomValueBridge.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import type { Ref } from 'vue'
|
||||
import { customRef, onScopeDispose } from 'vue'
|
||||
|
||||
type ValueElement = HTMLInputElement | HTMLTextAreaElement
|
||||
|
||||
export function useDomValueBridge(element: ValueElement): Ref<string> {
|
||||
const proto = Object.getPrototypeOf(element)
|
||||
const nativeDescriptor = Object.getOwnPropertyDescriptor(proto, 'value')
|
||||
const existingDescriptor = Object.getOwnPropertyDescriptor(element, 'value')
|
||||
|
||||
const prevGet = existingDescriptor?.get ?? nativeDescriptor?.get
|
||||
const prevSet = existingDescriptor?.set ?? nativeDescriptor?.set
|
||||
|
||||
if (!prevGet || !prevSet) {
|
||||
return customRef((track, trigger) => ({
|
||||
get() {
|
||||
track()
|
||||
return element.value
|
||||
},
|
||||
set(v: string) {
|
||||
element.value = v
|
||||
trigger()
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
let notifyChange: (() => void) | undefined
|
||||
|
||||
const ref = customRef<string>((track, trigger) => {
|
||||
notifyChange = trigger
|
||||
return {
|
||||
get() {
|
||||
track()
|
||||
return prevGet.call(element)
|
||||
},
|
||||
set(v: string) {
|
||||
prevSet.call(element, v)
|
||||
trigger()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Object.defineProperty(element, 'value', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
get() {
|
||||
return prevGet.call(element)
|
||||
},
|
||||
set(v: string) {
|
||||
prevSet.call(element, v)
|
||||
notifyChange?.()
|
||||
}
|
||||
})
|
||||
|
||||
function onInput() {
|
||||
notifyChange?.()
|
||||
}
|
||||
element.addEventListener('input', onInput)
|
||||
|
||||
onScopeDispose(() => {
|
||||
element.removeEventListener('input', onInput)
|
||||
if (existingDescriptor) {
|
||||
Object.defineProperty(element, 'value', existingDescriptor)
|
||||
} else {
|
||||
delete (element as unknown as Record<string, unknown>).value
|
||||
}
|
||||
})
|
||||
|
||||
return ref
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { createTestingPinia } from '@pinia/testing'
|
||||
import { mount } from '@vue/test-utils'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
@@ -39,6 +40,9 @@ function mountComponent(
|
||||
placeholder?: string
|
||||
) {
|
||||
return mount(WidgetTextarea, {
|
||||
global: {
|
||||
plugins: [createTestingPinia()]
|
||||
},
|
||||
props: {
|
||||
widget,
|
||||
modelValue,
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
"
|
||||
:placeholder
|
||||
:readonly="isReadOnly"
|
||||
:spellcheck
|
||||
data-testid="dom-widget-textarea"
|
||||
data-capture-wheel="true"
|
||||
@pointerdown.capture.stop="trackFocus"
|
||||
@pointermove.capture.stop
|
||||
@@ -56,6 +58,7 @@ import Button from '@/components/ui/button/Button.vue'
|
||||
import Textarea from '@/components/ui/textarea/Textarea.vue'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
import { isNodeOptionsOpen } from '@/composables/graph/useMoreOptionsMenu'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { useHideLayoutField } from '@/types/widgetTypes'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
@@ -102,6 +105,11 @@ function handleContextMenu(e: MouseEvent) {
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
const settingStore = useSettingStore()
|
||||
const spellcheck = computed(() =>
|
||||
settingStore.get('Comfy.TextareaWidget.Spellcheck')
|
||||
)
|
||||
|
||||
function handleCopy() {
|
||||
copyToClipboard(modelValue.value)
|
||||
}
|
||||
|
||||
@@ -1,113 +1,30 @@
|
||||
import { Editor as TiptapEditor } from '@tiptap/core'
|
||||
import TiptapLink from '@tiptap/extension-link'
|
||||
import TiptapTable from '@tiptap/extension-table'
|
||||
import TiptapTableCell from '@tiptap/extension-table-cell'
|
||||
import TiptapTableHeader from '@tiptap/extension-table-header'
|
||||
import TiptapTableRow from '@tiptap/extension-table-row'
|
||||
import TiptapStarterKit from '@tiptap/starter-kit'
|
||||
import { Markdown as TiptapMarkdown } from 'tiptap-markdown'
|
||||
|
||||
import { resolveNodeRootGraphId } from '@/lib/litegraph/src/litegraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { app } from '@/scripts/app'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import type { BaseDOMWidget } from '@/scripts/domWidget'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
|
||||
// TODO: This widget manually syncs with widgetValueStore via getValue/setValue.
|
||||
// Consolidate with useStringWidget into shared helpers (domWidgetHelpers.ts).
|
||||
import WidgetMarkdown from '../components/WidgetMarkdown.vue'
|
||||
|
||||
function addMarkdownWidget(
|
||||
node: LGraphNode,
|
||||
name: string,
|
||||
inputSpec: InputSpec,
|
||||
opts: { defaultVal: string }
|
||||
) {
|
||||
TiptapMarkdown.configure({
|
||||
html: false,
|
||||
breaks: true,
|
||||
transformPastedText: true
|
||||
})
|
||||
const editor = new TiptapEditor({
|
||||
extensions: [
|
||||
TiptapStarterKit,
|
||||
TiptapMarkdown,
|
||||
TiptapLink,
|
||||
TiptapTable,
|
||||
TiptapTableCell,
|
||||
TiptapTableHeader,
|
||||
TiptapTableRow
|
||||
],
|
||||
content: opts.defaultVal,
|
||||
editable: false
|
||||
})
|
||||
|
||||
const widgetStore = useWidgetValueStore()
|
||||
|
||||
const inputEl = editor.options.element as HTMLElement
|
||||
inputEl.classList.add('comfy-markdown')
|
||||
const textarea = document.createElement('textarea')
|
||||
inputEl.append(textarea)
|
||||
|
||||
const widget = node.addDOMWidget(name, 'MARKDOWN', inputEl, {
|
||||
getValue(): string {
|
||||
const graphId = resolveNodeRootGraphId(node, app.rootGraph.id)
|
||||
const storedValue = widgetStore.getWidget(graphId, node.id, name)?.value
|
||||
return typeof storedValue === 'string' ? storedValue : textarea.value
|
||||
},
|
||||
setValue(v: string) {
|
||||
textarea.value = v
|
||||
editor.commands.setContent(v)
|
||||
const graphId = resolveNodeRootGraphId(node, app.rootGraph.id)
|
||||
const widgetState = widgetStore.getWidget(graphId, node.id, name)
|
||||
if (widgetState) widgetState.value = v
|
||||
}
|
||||
})
|
||||
widget.element = inputEl
|
||||
widget.options.minNodeSize = [400, 200]
|
||||
|
||||
inputEl.addEventListener('input', (event) => {
|
||||
if (event.target instanceof HTMLTextAreaElement) {
|
||||
widget.value = event.target.value
|
||||
}
|
||||
widget.callback?.(widget.value)
|
||||
})
|
||||
|
||||
inputEl.addEventListener('dblclick', () => {
|
||||
inputEl.classList.add('editing')
|
||||
setTimeout(() => {
|
||||
textarea.focus()
|
||||
}, 0)
|
||||
})
|
||||
|
||||
textarea.addEventListener('blur', () => {
|
||||
inputEl.classList.remove('editing')
|
||||
})
|
||||
|
||||
textarea.addEventListener('change', () => {
|
||||
editor.commands.setContent(textarea.value)
|
||||
widget.callback?.(widget.value)
|
||||
})
|
||||
|
||||
inputEl.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||
event.stopPropagation()
|
||||
})
|
||||
|
||||
inputEl.addEventListener('pointerdown', (event: PointerEvent) => {
|
||||
if (event.button === 1) {
|
||||
app.canvas.processMouseDown(event)
|
||||
const widget = new ComponentWidgetImpl({
|
||||
node,
|
||||
name,
|
||||
component: WidgetMarkdown,
|
||||
inputSpec,
|
||||
type: 'MARKDOWN',
|
||||
options: {
|
||||
minNodeSize: [400, 200]
|
||||
}
|
||||
})
|
||||
|
||||
inputEl.addEventListener('pointermove', (event: PointerEvent) => {
|
||||
if ((event.buttons & 4) === 4) {
|
||||
app.canvas.processMouseMove(event)
|
||||
}
|
||||
})
|
||||
|
||||
inputEl.addEventListener('pointerup', (event: PointerEvent) => {
|
||||
if (event.button === 1) {
|
||||
app.canvas.processMouseUp(event)
|
||||
}
|
||||
})
|
||||
addWidget(node, widget as BaseDOMWidget<object | string>)
|
||||
widget.value = opts.defaultVal
|
||||
|
||||
return widget
|
||||
}
|
||||
@@ -117,7 +34,7 @@ export const useMarkdownWidget = () => {
|
||||
node: LGraphNode,
|
||||
inputSpec: InputSpec
|
||||
) => {
|
||||
return addMarkdownWidget(node, inputSpec.name, {
|
||||
return addMarkdownWidget(node, inputSpec.name, inputSpec, {
|
||||
defaultVal: inputSpec.default ?? ''
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,124 +1,32 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { resolveNodeRootGraphId } from '@/lib/litegraph/src/litegraph'
|
||||
import { useSettingStore } from '@/platform/settings/settingStore'
|
||||
import { isStringInputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
|
||||
import { app } from '@/scripts/app'
|
||||
import { ComponentWidgetImpl, addWidget } from '@/scripts/domWidget'
|
||||
import type { BaseDOMWidget } from '@/scripts/domWidget'
|
||||
import type { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
|
||||
import { useWidgetValueStore } from '@/stores/widgetValueStore'
|
||||
|
||||
const TRACKPAD_DETECTION_THRESHOLD = 50
|
||||
import WidgetTextarea from '../components/WidgetTextarea.vue'
|
||||
|
||||
// TODO: This widget manually syncs with widgetValueStore via getValue/setValue.
|
||||
// Consolidate with useMarkdownWidget into shared helpers (domWidgetHelpers.ts).
|
||||
function addMultilineWidget(
|
||||
node: LGraphNode,
|
||||
name: string,
|
||||
inputSpec: InputSpec,
|
||||
opts: { defaultVal: string; placeholder?: string }
|
||||
) {
|
||||
const widgetStore = useWidgetValueStore()
|
||||
const inputEl = document.createElement('textarea')
|
||||
inputEl.className = 'comfy-multiline-input'
|
||||
inputEl.dataset.testid = 'dom-widget-textarea'
|
||||
inputEl.value = opts.defaultVal
|
||||
inputEl.placeholder = opts.placeholder || name
|
||||
inputEl.spellcheck = useSettingStore().get('Comfy.TextareaWidget.Spellcheck')
|
||||
|
||||
const widget = node.addDOMWidget(name, 'customtext', inputEl, {
|
||||
getValue(): string {
|
||||
const graphId = resolveNodeRootGraphId(node, app.rootGraph.id)
|
||||
const widgetState = widgetStore.getWidget(graphId, node.id, name)
|
||||
|
||||
return (widgetState?.value as string) ?? inputEl.value
|
||||
},
|
||||
setValue(v: string) {
|
||||
inputEl.value = v
|
||||
const graphId = resolveNodeRootGraphId(node, app.rootGraph.id)
|
||||
const widgetState = widgetStore.getWidget(graphId, node.id, name)
|
||||
if (widgetState) widgetState.value = v
|
||||
const widget = new ComponentWidgetImpl({
|
||||
node,
|
||||
name,
|
||||
component: WidgetTextarea,
|
||||
inputSpec,
|
||||
type: 'customtext',
|
||||
props: { placeholder: opts.placeholder || name },
|
||||
options: {
|
||||
minNodeSize: [400, 200]
|
||||
}
|
||||
})
|
||||
|
||||
widget.element = inputEl
|
||||
widget.inputEl = inputEl
|
||||
widget.options.minNodeSize = [400, 200]
|
||||
|
||||
inputEl.addEventListener('input', (event) => {
|
||||
if (event.target instanceof HTMLTextAreaElement) {
|
||||
widget.value = event.target.value
|
||||
}
|
||||
widget.callback?.(widget.value)
|
||||
})
|
||||
|
||||
// Allow middle mouse button panning
|
||||
inputEl.addEventListener('pointerdown', (event: PointerEvent) => {
|
||||
if (event.button === 1) {
|
||||
app.canvas.processMouseDown(event)
|
||||
}
|
||||
})
|
||||
|
||||
inputEl.addEventListener('pointermove', (event: PointerEvent) => {
|
||||
if ((event.buttons & 4) === 4) {
|
||||
app.canvas.processMouseMove(event)
|
||||
}
|
||||
})
|
||||
|
||||
inputEl.addEventListener('pointerup', (event: PointerEvent) => {
|
||||
if (event.button === 1) {
|
||||
app.canvas.processMouseUp(event)
|
||||
}
|
||||
})
|
||||
|
||||
inputEl.addEventListener('wheel', (event: WheelEvent) => {
|
||||
const gesturesEnabled = useSettingStore().get(
|
||||
'LiteGraph.Pointer.TrackpadGestures'
|
||||
)
|
||||
const deltaX = event.deltaX
|
||||
const deltaY = event.deltaY
|
||||
|
||||
const canScrollY = inputEl.scrollHeight > inputEl.clientHeight
|
||||
const isHorizontal = Math.abs(deltaX) > Math.abs(deltaY)
|
||||
|
||||
// Prevent pinch zoom from zooming the page
|
||||
if (event.ctrlKey) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
app.canvas.processMouseWheel(event)
|
||||
return
|
||||
}
|
||||
|
||||
// Detect if this is likely a trackpad gesture vs mouse wheel
|
||||
// Trackpads usually have deltaX or smaller deltaY values (< TRACKPAD_DETECTION_THRESHOLD)
|
||||
// Mouse wheels typically have larger discrete deltaY values (>= TRACKPAD_DETECTION_THRESHOLD)
|
||||
const isLikelyTrackpad =
|
||||
Math.abs(deltaX) > 0 || Math.abs(deltaY) < TRACKPAD_DETECTION_THRESHOLD
|
||||
|
||||
// Trackpad gestures: when enabled, trackpad panning goes to canvas
|
||||
if (gesturesEnabled && isLikelyTrackpad) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
app.canvas.processMouseWheel(event)
|
||||
return
|
||||
}
|
||||
|
||||
// When gestures disabled: horizontal always goes to canvas (no horizontal scroll in textarea)
|
||||
if (isHorizontal) {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
app.canvas.processMouseWheel(event)
|
||||
return
|
||||
}
|
||||
|
||||
// Vertical scrolling when gestures disabled: let textarea scroll if scrollable
|
||||
if (canScrollY) {
|
||||
event.stopPropagation()
|
||||
return
|
||||
}
|
||||
|
||||
// If textarea can't scroll vertically, pass to canvas
|
||||
event.preventDefault()
|
||||
app.canvas.processMouseWheel(event)
|
||||
})
|
||||
addWidget(node, widget as BaseDOMWidget<object | string>)
|
||||
widget.value = opts.defaultVal
|
||||
|
||||
return widget
|
||||
}
|
||||
@@ -136,7 +44,7 @@ export const useStringWidget = () => {
|
||||
const multiline = inputSpec.multiline
|
||||
|
||||
const widget = multiline
|
||||
? addMultilineWidget(node, inputSpec.name, {
|
||||
? addMultilineWidget(node, inputSpec.name, inputSpec, {
|
||||
defaultVal,
|
||||
placeholder: inputSpec.placeholder
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import _ from 'es-toolkit/compat'
|
||||
import { type Component, toRaw } from 'vue'
|
||||
import { type Component, effectScope, toRaw, watch } from 'vue'
|
||||
|
||||
import { useDomValueBridge } from '@/composables/element/useDomValueBridge'
|
||||
import { useChainCallback } from '@/composables/functional/useChainCallback'
|
||||
import {
|
||||
LGraphNode,
|
||||
@@ -411,5 +412,24 @@ LGraphNode.prototype.addDOMWidget = function <
|
||||
}
|
||||
})
|
||||
|
||||
if (
|
||||
element instanceof HTMLInputElement ||
|
||||
element instanceof HTMLTextAreaElement
|
||||
) {
|
||||
const bridgeScope = effectScope()
|
||||
bridgeScope.run(() => {
|
||||
const bridgedValue = useDomValueBridge(element)
|
||||
watch(bridgedValue, (newVal) => {
|
||||
if (widget.value !== newVal) {
|
||||
widget.value = newVal as V
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
widget.onRemove = useChainCallback(widget.onRemove, () => {
|
||||
bridgeScope.stop()
|
||||
})
|
||||
}
|
||||
|
||||
return widget
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user