Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
168 lines
3.6 KiB
JavaScript
168 lines
3.6 KiB
JavaScript
import { useState, useRef } from 'react'
|
|
import { Input, InputNumber } from 'antd'
|
|
import PropTypes from 'prop-types'
|
|
import FunctionIcon from '../../Icons/FunctionIcon'
|
|
|
|
const OPERATOR_KEYS = ['+', '-', '*', '/']
|
|
|
|
/**
|
|
* Safely evaluate a math expression. Only allows numbers and +, -, *, /
|
|
*/
|
|
function safeEval(expr) {
|
|
const sanitized = String(expr)
|
|
.replace(/\s/g, '')
|
|
.replace(/[^0-9+\-*/().]/g, '')
|
|
if (!sanitized) return null
|
|
try {
|
|
const fn = new Function(`return (${sanitized})`)
|
|
const result = fn()
|
|
return typeof result === 'number' && Number.isFinite(result) ? result : null
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
const InputNumberCal = ({
|
|
value,
|
|
onChange,
|
|
onBlur,
|
|
min,
|
|
max,
|
|
prefix,
|
|
suffix,
|
|
placeholder,
|
|
disabled,
|
|
...rest
|
|
}) => {
|
|
const [isExprMode, setIsExprMode] = useState(false)
|
|
const [exprValue, setExprValue] = useState('')
|
|
const inputRef = useRef(null)
|
|
|
|
const switchToExprMode = (initialValue) => {
|
|
setIsExprMode(true)
|
|
setExprValue(initialValue)
|
|
setTimeout(() => {
|
|
//inputRef.current?.focus()
|
|
const input = inputRef.current.getElementsByTagName('input')[0]
|
|
if (input) {
|
|
input.focus()
|
|
}
|
|
}, 0)
|
|
}
|
|
|
|
const exitExprMode = (result) => {
|
|
setIsExprMode(false)
|
|
setExprValue('')
|
|
if (result != null) {
|
|
const clamped =
|
|
min != null && result < min
|
|
? min
|
|
: max != null && result > max
|
|
? max
|
|
: result
|
|
onChange?.(clamped)
|
|
}
|
|
}
|
|
|
|
const handleNumberKeyDown = (e) => {
|
|
if (OPERATOR_KEYS.includes(e.key)) {
|
|
e.preventDefault()
|
|
const current = value ?? ''
|
|
switchToExprMode(String(current) + e.key)
|
|
}
|
|
}
|
|
|
|
const handleInputChange = (e) => {
|
|
const next = e.target.value
|
|
setExprValue(next)
|
|
const num = parseFloat(next)
|
|
if (!next) {
|
|
onChange?.(null)
|
|
} else if (!next.match(/[+\-*/=]/) && !Number.isNaN(num)) {
|
|
onChange?.(num)
|
|
}
|
|
}
|
|
|
|
const handleInputKeyDown = (e) => {
|
|
if (e.key === '=') {
|
|
e.preventDefault()
|
|
const result = safeEval(exprValue)
|
|
if (result != null) {
|
|
exitExprMode(result)
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleInputBlur = (e) => {
|
|
const expr = exprValue.trim()
|
|
if (expr && expr.match(/[+\-*/]/)) {
|
|
const result = safeEval(expr)
|
|
if (result != null) {
|
|
exitExprMode(result)
|
|
} else {
|
|
exitExprMode(null)
|
|
}
|
|
} else {
|
|
const num = parseFloat(expr)
|
|
if (!Number.isNaN(num)) {
|
|
exitExprMode(num)
|
|
} else {
|
|
exitExprMode(null)
|
|
}
|
|
}
|
|
onBlur?.(e)
|
|
}
|
|
|
|
const commonProps = {
|
|
prefix,
|
|
suffix,
|
|
placeholder,
|
|
disabled,
|
|
min,
|
|
max,
|
|
...rest
|
|
}
|
|
|
|
if (isExprMode) {
|
|
return (
|
|
<div className='input-number-cal' ref={inputRef}>
|
|
<Input
|
|
value={exprValue}
|
|
onChange={handleInputChange}
|
|
onKeyDown={handleInputKeyDown}
|
|
onBlur={handleInputBlur}
|
|
{...commonProps}
|
|
/>
|
|
<div className='input-number-cal-icon'>
|
|
<FunctionIcon style={{ fontSize: 24 }} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<InputNumber
|
|
value={value}
|
|
onChange={onChange}
|
|
onBlur={onBlur}
|
|
onKeyDown={handleNumberKeyDown}
|
|
{...commonProps}
|
|
/>
|
|
)
|
|
}
|
|
|
|
InputNumberCal.propTypes = {
|
|
value: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
|
|
onChange: PropTypes.func,
|
|
onBlur: PropTypes.func,
|
|
min: PropTypes.number,
|
|
max: PropTypes.number,
|
|
step: PropTypes.number,
|
|
prefix: PropTypes.node,
|
|
suffix: PropTypes.node,
|
|
placeholder: PropTypes.string,
|
|
disabled: PropTypes.bool
|
|
}
|
|
|
|
export default InputNumberCal
|