Tom Butcher e4255443a0
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
New number calculator functionality.
2026-03-06 23:38:30 +00:00

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