mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
The custom context menu provided by the frontend exposes widget specific options. In order to support renaming, promotion, and favoriting, there needs to be a way to access this context menu when targeting a textarea. However, always displaying this custom context menu will cause the user to lose access to browser specific functionality like spell checking, translation, and the ability to copy paste text. This PR updates the behaviour so that the native browser context menu will display when the text area already has focus. Our custom frontend context menu will continue to display when it does not. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-10454-Use-native-context-menu-for-focused-textareas-32d6d73d365081909673d81d6a6ba054) by [Unito](https://www.unito.io)
109 lines
3.0 KiB
Vue
109 lines
3.0 KiB
Vue
<template>
|
|
<div
|
|
:class="
|
|
cn(
|
|
'group relative rounded-lg transition-all focus-within:ring focus-within:ring-component-node-widget-background-highlighted hover:bg-component-node-widget-background-hovered',
|
|
widget.borderStyle
|
|
)
|
|
"
|
|
>
|
|
<label
|
|
v-if="!hideLayoutField"
|
|
:for="id"
|
|
class="pointer-events-none absolute top-1.5 left-3 z-10 text-xxs text-muted-foreground"
|
|
>
|
|
{{ displayName }}
|
|
</label>
|
|
<Textarea
|
|
v-bind="filteredProps"
|
|
:id
|
|
ref="textAreaRef"
|
|
v-model="modelValue"
|
|
:class="
|
|
cn(
|
|
WidgetInputBaseClass,
|
|
'size-full resize-none text-xs',
|
|
!hideLayoutField && 'pt-5'
|
|
)
|
|
"
|
|
:placeholder
|
|
:readonly="isReadOnly"
|
|
data-capture-wheel="true"
|
|
@pointerdown.capture.stop="trackFocus"
|
|
@pointermove.capture.stop
|
|
@pointerup.capture.stop
|
|
@contextmenu.capture="handleContextMenu"
|
|
/>
|
|
<Button
|
|
v-if="isReadOnly"
|
|
variant="textonly"
|
|
size="icon"
|
|
class="invisible absolute top-1.5 right-1.5 z-10 group-focus-within:visible group-hover:visible hover:bg-base-foreground/10"
|
|
:title="$t('g.copyToClipboard')"
|
|
:aria-label="$t('g.copyToClipboard')"
|
|
@click="handleCopy"
|
|
@pointerdown.capture.stop
|
|
>
|
|
<i class="icon-[lucide--copy] size-4 text-component-node-foreground" />
|
|
</Button>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { computed, ref, useId, useTemplateRef } from 'vue'
|
|
|
|
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 type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
|
import { useHideLayoutField } from '@/types/widgetTypes'
|
|
import { cn } from '@/utils/tailwindUtil'
|
|
import {
|
|
INPUT_EXCLUDED_PROPS,
|
|
filterWidgetProps
|
|
} from '@/utils/widgetPropFilter'
|
|
|
|
import { WidgetInputBaseClass } from './layout'
|
|
|
|
const { widget, placeholder = '' } = defineProps<{
|
|
widget: SimplifiedWidget<string>
|
|
placeholder?: string
|
|
}>()
|
|
|
|
const textAreaRef = useTemplateRef('textAreaRef')
|
|
|
|
const modelValue = defineModel<string>({ default: '' })
|
|
|
|
const isFocused = ref(false)
|
|
function trackFocus() {
|
|
isFocused.value = document.activeElement === textAreaRef.value?.$el
|
|
}
|
|
|
|
const hideLayoutField = useHideLayoutField()
|
|
const { copyToClipboard } = useCopyToClipboard()
|
|
|
|
const filteredProps = computed(() =>
|
|
filterWidgetProps(widget.options, INPUT_EXCLUDED_PROPS)
|
|
)
|
|
|
|
const displayName = computed(() => widget.label || widget.name)
|
|
const id = useId()
|
|
|
|
const isReadOnly = computed(() =>
|
|
Boolean(widget.options?.read_only || widget.options?.disabled)
|
|
)
|
|
|
|
function handleContextMenu(e: MouseEvent) {
|
|
if (isNodeOptionsOpen() || isFocused.value) {
|
|
e.stopPropagation()
|
|
return
|
|
}
|
|
e.preventDefault()
|
|
}
|
|
|
|
function handleCopy() {
|
|
copyToClipboard(modelValue.value)
|
|
}
|
|
</script>
|