mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-05-01 11:42:06 +00:00
Implement vue math (#7759)
Adds support for entering math inside number widgets in vue mode  Migrates components to simple html elements (div and button) by borrowing styling from the (reverted) reka-ui migration in #6985. The existing (evil) litegraph eval code is extracted as a utility function and reused. This PR means we're entirely writing our own NumberField. Also adds support for scrubbing widgets like in litegraph  ### Known Issue - Scrubbing causes text to be highlighted, ~~starting a scrub from highlighted text will instead drag the text~~. - It seems this can only be prevented with `pointerdown.prevent`, but this requires a manual `input.focus()` which does not place the cursor at location of mouse click. (Obligatory: _It won't do you a bit of good to review math_) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-7759-Implement-vue-math-2d46d73d365081b9acd4d6422669016e) by [Unito](https://www.unito.io) --------- Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: DrJKL <DrJKL0424@gmail.com>
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import InputNumber from 'primevue/inputnumber'
|
||||
import { computed } from 'vue'
|
||||
import { onClickOutside } from '@vueuse/core'
|
||||
import { computed, ref, useTemplateRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
import { evaluateInput } from '@/lib/litegraph/src/utils/widget'
|
||||
import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
||||
import { cn } from '@/utils/tailwindUtil'
|
||||
import {
|
||||
@@ -12,12 +14,68 @@ import {
|
||||
import { WidgetInputBaseClass } from './layout'
|
||||
import WidgetLayoutField from './layout/WidgetLayoutField.vue'
|
||||
|
||||
const { n } = useI18n()
|
||||
|
||||
const props = defineProps<{
|
||||
widget: SimplifiedWidget<number>
|
||||
}>()
|
||||
|
||||
const widgetContainer = useTemplateRef<HTMLDivElement>('widgetContainer')
|
||||
const inputField = useTemplateRef<HTMLInputElement>('inputField')
|
||||
const textEdit = ref(false)
|
||||
onClickOutside(widgetContainer, () => {
|
||||
if (textEdit.value) {
|
||||
textEdit.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const decimalSeparator = computed(() => n(1.1).replace(/\p{Number}/gu, ''))
|
||||
const groupSeparator = computed(() => n(11111).replace(/\p{Number}/gu, ''))
|
||||
function unformatValue(value: string) {
|
||||
return value
|
||||
.replaceAll(groupSeparator.value, '')
|
||||
.replaceAll(decimalSeparator.value, '.')
|
||||
}
|
||||
|
||||
const modelValue = defineModel<number>({ default: 0 })
|
||||
|
||||
const formattedValue = computed(() => {
|
||||
const unformattedValue = dragValue.value ?? modelValue.value
|
||||
if (!isFinite(unformattedValue)) return `${unformattedValue}`
|
||||
|
||||
return n(unformattedValue, {
|
||||
useGrouping: useGrouping.value,
|
||||
minimumFractionDigits: precision.value,
|
||||
maximumFractionDigits: precision.value
|
||||
})
|
||||
})
|
||||
|
||||
function updateValue(e: UIEvent) {
|
||||
const { target } = e
|
||||
if (!(target instanceof HTMLInputElement)) return
|
||||
const parsed = evaluateInput(unformatValue(target.value))
|
||||
if (parsed !== undefined)
|
||||
modelValue.value = Math.min(
|
||||
filteredProps.value.max,
|
||||
Math.max(filteredProps.value.min, parsed)
|
||||
)
|
||||
else target.value = formattedValue.value
|
||||
|
||||
textEdit.value = false
|
||||
}
|
||||
|
||||
const sharedButtonClass = 'w-8 bg-transparent border-0 text-sm text-smoke-700'
|
||||
const canDecrement = computed(
|
||||
() =>
|
||||
modelValue.value > filteredProps.value.min &&
|
||||
!props.widget.options?.disabled
|
||||
)
|
||||
const canIncrement = computed(
|
||||
() =>
|
||||
modelValue.value < filteredProps.value.max &&
|
||||
!props.widget.options?.disabled
|
||||
)
|
||||
|
||||
const filteredProps = computed(() =>
|
||||
filterWidgetProps(props.widget.options, INPUT_EXCLUDED_PROPS)
|
||||
)
|
||||
@@ -69,6 +127,48 @@ const buttonsDisabled = computed(() => {
|
||||
)
|
||||
})
|
||||
|
||||
function updateValueBy(delta: number) {
|
||||
modelValue.value = Math.min(
|
||||
filteredProps.value.max,
|
||||
Math.max(filteredProps.value.min, modelValue.value + delta)
|
||||
)
|
||||
}
|
||||
|
||||
const dragValue = ref<number>()
|
||||
const dragDelta = ref(0)
|
||||
function handleMouseDown(e: PointerEvent) {
|
||||
if (props.widget.options?.disabled) return
|
||||
const { target } = e
|
||||
if (!(target instanceof HTMLElement)) return
|
||||
target.setPointerCapture(e.pointerId)
|
||||
dragValue.value = modelValue.value
|
||||
dragDelta.value = 0
|
||||
}
|
||||
function handleMouseMove(e: PointerEvent) {
|
||||
if (dragValue.value === undefined) return
|
||||
dragDelta.value += e.movementX
|
||||
const unclippedValue =
|
||||
dragValue.value + ((dragDelta.value / 10) | 0) * stepValue.value
|
||||
dragDelta.value %= 10
|
||||
dragValue.value = Math.min(
|
||||
filteredProps.value.max,
|
||||
Math.max(filteredProps.value.min, unclippedValue)
|
||||
)
|
||||
}
|
||||
function handleMouseUp() {
|
||||
const newValue = dragValue.value
|
||||
if (newValue === undefined) return
|
||||
modelValue.value = newValue
|
||||
dragValue.value = undefined
|
||||
|
||||
if (dragDelta.value === 0) {
|
||||
textEdit.value = true
|
||||
inputField.value?.focus()
|
||||
inputField.value?.setSelectionRange(0, -1)
|
||||
}
|
||||
dragDelta.value = 0
|
||||
}
|
||||
|
||||
const buttonTooltip = computed(() => {
|
||||
if (buttonsDisabled.value) {
|
||||
return 'Increment/decrement disabled: value exceeds JavaScript precision limit (±2^53)'
|
||||
@@ -79,54 +179,80 @@ const buttonTooltip = computed(() => {
|
||||
|
||||
<template>
|
||||
<WidgetLayoutField :widget>
|
||||
<InputNumber
|
||||
v-model="modelValue"
|
||||
<div
|
||||
ref="widgetContainer"
|
||||
v-tooltip="buttonTooltip"
|
||||
v-bind="filteredProps"
|
||||
fluid
|
||||
button-layout="horizontal"
|
||||
size="small"
|
||||
variant="outlined"
|
||||
:step="stepValue"
|
||||
:min-fraction-digits="precision"
|
||||
:max-fraction-digits="precision"
|
||||
:use-grouping="useGrouping"
|
||||
:class="cn(WidgetInputBaseClass, 'grow text-xs')"
|
||||
:aria-label="widget.name"
|
||||
:show-buttons="!buttonsDisabled"
|
||||
:pt="{
|
||||
root: {
|
||||
class: cn(
|
||||
'[&>input]:bg-transparent [&>input]:border-0',
|
||||
'[&>input]:truncate [&>input]:min-w-[4ch]',
|
||||
$slots.default && '[&>input]:pr-7'
|
||||
)
|
||||
},
|
||||
decrementButton: {
|
||||
class: 'w-8 border-0'
|
||||
},
|
||||
incrementButton: {
|
||||
class: 'w-8 border-0'
|
||||
}
|
||||
}"
|
||||
:class="cn(WidgetInputBaseClass, 'grow text-xs flex h-7')"
|
||||
>
|
||||
<template #incrementicon>
|
||||
<span class="pi pi-plus text-sm" />
|
||||
</template>
|
||||
<template #decrementicon>
|
||||
<span class="pi pi-minus text-sm" />
|
||||
</template>
|
||||
</InputNumber>
|
||||
<div class="absolute top-5 right-8 h-4 w-7 -translate-y-4/5 flex">
|
||||
<button
|
||||
v-if="!buttonsDisabled"
|
||||
data-testid="decrement"
|
||||
:class="
|
||||
cn(sharedButtonClass, 'pi pi-minus', !canDecrement && 'opacity-60')
|
||||
"
|
||||
:disabled="!canDecrement"
|
||||
tabindex="-1"
|
||||
@click="modelValue -= stepValue"
|
||||
/>
|
||||
<div class="relative min-w-[4ch] flex-1 py-1.5 my-0.25">
|
||||
<input
|
||||
ref="inputField"
|
||||
:aria-valuenow="dragValue ?? modelValue"
|
||||
:aria-valuemin="filteredProps.min"
|
||||
:aria-valuemax="filteredProps.max"
|
||||
:class="
|
||||
cn(
|
||||
'bg-transparent border-0 focus:outline-0 p-1 truncate text-sm absolute inset-0'
|
||||
)
|
||||
"
|
||||
inputmode="decimal"
|
||||
:value="formattedValue"
|
||||
role="spinbutton"
|
||||
tabindex="0"
|
||||
:disabled="widget.options?.disabled"
|
||||
autocomplete="off"
|
||||
autocorrect="off"
|
||||
spellcheck="false"
|
||||
@blur="updateValue"
|
||||
@keyup.enter="updateValue"
|
||||
@keydown.up.prevent="updateValueBy(stepValue)"
|
||||
@keydown.down.prevent="updateValueBy(-stepValue)"
|
||||
@keydown.page-up.prevent="updateValueBy(10 * stepValue)"
|
||||
@keydown.page-down.prevent="updateValueBy(-10 * stepValue)"
|
||||
@dragstart.prevent
|
||||
/>
|
||||
<div
|
||||
:class="
|
||||
cn(
|
||||
'absolute inset-0 z-10 cursor-ew-resize',
|
||||
textEdit && 'hidden pointer-events-none'
|
||||
)
|
||||
"
|
||||
@pointerdown="handleMouseDown"
|
||||
@pointermove="handleMouseMove"
|
||||
@pointerup="handleMouseUp"
|
||||
@pointercancel="
|
||||
() => {
|
||||
dragValue = undefined
|
||||
dragDelta = 0
|
||||
}
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<slot />
|
||||
<button
|
||||
v-if="!buttonsDisabled"
|
||||
data-testid="increment"
|
||||
:class="
|
||||
cn(sharedButtonClass, 'pi pi-plus', !canIncrement && 'opacity-60')
|
||||
"
|
||||
:disabled="!canIncrement"
|
||||
tabindex="-1"
|
||||
@click="modelValue += stepValue"
|
||||
/>
|
||||
</div>
|
||||
</WidgetLayoutField>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
:deep(.p-inputnumber-input) {
|
||||
height: 1.625rem;
|
||||
margin: 1px 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user