|
12 | 12 | @update:model-value="onChange"
|
13 | 13 | />
|
14 | 14 | <InputText
|
15 |
| - :value="String(localValue)" |
| 15 | + v-model="inputDisplayValue" |
16 | 16 | :disabled="readonly"
|
17 | 17 | type="number"
|
18 | 18 | :min="widget.options?.min"
|
19 | 19 | :max="widget.options?.max"
|
20 |
| - :step="widget.options?.step" |
| 20 | + :step="stepValue" |
21 | 21 | class="w-[4em] text-center text-xs px-0"
|
22 | 22 | size="small"
|
23 |
| - @input="handleInputChange" |
| 23 | + @blur="handleInputBlur" |
| 24 | + @keydown="handleInputKeydown" |
24 | 25 | />
|
25 | 26 | </div>
|
26 | 27 | </div>
|
|
29 | 30 | <script setup lang="ts">
|
30 | 31 | import InputText from 'primevue/inputtext'
|
31 | 32 | import Slider from 'primevue/slider'
|
32 |
| -import { computed } from 'vue' |
| 33 | +import { computed, ref, watch } from 'vue' |
33 | 34 |
|
34 | 35 | import { useNumberWidgetValue } from '@/composables/graph/useWidgetValue'
|
35 | 36 | import type { SimplifiedWidget } from '@/types/simplifiedWidget'
|
@@ -59,16 +60,92 @@ const filteredProps = computed(() =>
|
59 | 60 | filterWidgetProps(props.widget.options, STANDARD_EXCLUDED_PROPS)
|
60 | 61 | )
|
61 | 62 |
|
62 |
| -const handleInputChange = (event: Event) => { |
| 63 | +// Get the precision value for proper number formatting |
| 64 | +const precision = computed(() => { |
| 65 | + const p = props.widget.options?.precision |
| 66 | + // Treat negative or non-numeric precision as undefined |
| 67 | + return typeof p === 'number' && p >= 0 ? p : undefined |
| 68 | +}) |
| 69 | +
|
| 70 | +// Calculate the step value based on precision or widget options |
| 71 | +const stepValue = computed(() => { |
| 72 | + // If step is explicitly defined in options, use it |
| 73 | + if (props.widget.options?.step !== undefined) { |
| 74 | + return String(props.widget.options.step) |
| 75 | + } |
| 76 | + // Otherwise, derive from precision |
| 77 | + if (precision.value !== undefined) { |
| 78 | + if (precision.value === 0) { |
| 79 | + return '1' |
| 80 | + } |
| 81 | + // For precision > 0, step = 1 / (10^precision) |
| 82 | + // precision 1 → 0.1, precision 2 → 0.01, etc. |
| 83 | + return (1 / Math.pow(10, precision.value)).toFixed(precision.value) |
| 84 | + } |
| 85 | + // Default to 'any' for unrestricted stepping |
| 86 | + return 'any' |
| 87 | +}) |
| 88 | +
|
| 89 | +// Format a number according to the widget's precision |
| 90 | +const formatNumber = (value: number): string => { |
| 91 | + if (precision.value === undefined) { |
| 92 | + // No precision specified, return as-is |
| 93 | + return String(value) |
| 94 | + } |
| 95 | + // Use toFixed to ensure correct decimal places |
| 96 | + return value.toFixed(precision.value) |
| 97 | +} |
| 98 | +
|
| 99 | +// Apply precision-based rounding to a number |
| 100 | +const applyPrecision = (value: number): number => { |
| 101 | + if (precision.value === undefined) { |
| 102 | + // No precision specified, return as-is |
| 103 | + return value |
| 104 | + } |
| 105 | + if (precision.value === 0) { |
| 106 | + // Integer precision |
| 107 | + return Math.round(value) |
| 108 | + } |
| 109 | + // Round to the specified decimal places |
| 110 | + const multiplier = Math.pow(10, precision.value) |
| 111 | + return Math.round(value * multiplier) / multiplier |
| 112 | +} |
| 113 | +
|
| 114 | +// Keep a separate display value for the input field |
| 115 | +const inputDisplayValue = ref(formatNumber(localValue.value)) |
| 116 | +
|
| 117 | +// Update display value when localValue changes from external sources |
| 118 | +watch(localValue, (newValue) => { |
| 119 | + inputDisplayValue.value = formatNumber(newValue) |
| 120 | +}) |
| 121 | +
|
| 122 | +const handleInputBlur = (event: Event) => { |
63 | 123 | const target = event.target as HTMLInputElement
|
64 |
| - const value = parseFloat(target.value) |
65 |
| -
|
66 |
| - if (!isNaN(value)) { |
67 |
| - const min = props.widget.options?.min ?? -Infinity |
68 |
| - const max = props.widget.options?.max ?? Infinity |
69 |
| - const clampedValue = Math.min(Math.max(value, min), max) |
70 |
| - localValue.value = clampedValue |
71 |
| - onChange(clampedValue) |
| 124 | + const value = target.value || '0' |
| 125 | + const parsed = parseFloat(value) |
| 126 | +
|
| 127 | + if (!isNaN(parsed)) { |
| 128 | + // Apply precision-based rounding |
| 129 | + const roundedValue = applyPrecision(parsed) |
| 130 | + onChange(roundedValue) |
| 131 | + // Update display value with proper formatting |
| 132 | + inputDisplayValue.value = formatNumber(roundedValue) |
| 133 | + } |
| 134 | +} |
| 135 | +
|
| 136 | +const handleInputKeydown = (event: KeyboardEvent) => { |
| 137 | + if (event.key === 'Enter') { |
| 138 | + const target = event.target as HTMLInputElement |
| 139 | + const value = target.value || '0' |
| 140 | + const parsed = parseFloat(value) |
| 141 | +
|
| 142 | + if (!isNaN(parsed)) { |
| 143 | + // Apply precision-based rounding |
| 144 | + const roundedValue = applyPrecision(parsed) |
| 145 | + onChange(roundedValue) |
| 146 | + // Update display value with proper formatting |
| 147 | + inputDisplayValue.value = formatNumber(roundedValue) |
| 148 | + } |
72 | 149 | }
|
73 | 150 | }
|
74 | 151 | </script>
|
|
0 commit comments