import { useCallback, useLayoutEffect, useRef, useState } from 'react' import { Input, InputNumber } from 'antd' import PropTypes from 'prop-types' import FunctionIcon from '../../Icons/FunctionIcon' const OPERATOR_KEYS = ['+', '-', '*', '/'] const OPERATORS = new Set(OPERATOR_KEYS) const BRACKETS = new Set(['(', ')']) const tokenizeExpr = (raw = '') => { const value = String(raw) const tokens = [] let text = '' for (let i = 0; i < value.length; i += 1) { const char = value[i] if (OPERATORS.has(char) || BRACKETS.has(char)) { if (text) { tokens.push({ type: 'text', value: text }) text = '' } tokens.push({ type: OPERATORS.has(char) ? 'operator' : 'bracket', value: char }) } else { text += char } } if (text) tokens.push({ type: 'text', value: text }) return tokens } const renderHighlightedExpr = (raw = '') => tokenizeExpr(raw).map((token, index) => { if (token.type === 'operator') { return ( {token.value} ) } if (token.type === 'bracket') { return ( {token.value} ) } return {token.value} }) /** * 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, style, ...rest }) => { const [isExprMode, setIsExprMode] = useState(false) const [exprValue, setExprValue] = useState('') const wrapperRef = useRef(null) const highlightRef = useRef(null) const inputElRef = useRef(null) const syncHighlightLayout = useCallback(() => { const wrapper = wrapperRef.current const highlight = highlightRef.current const inputEl = inputElRef.current if (!wrapper || !highlight || !inputEl) return const wrapperRect = wrapper.getBoundingClientRect() const inputRect = inputEl.getBoundingClientRect() const inputStyle = window.getComputedStyle(inputEl) highlight.style.top = `${inputRect.top - wrapperRect.top}px` highlight.style.left = `${inputRect.left - wrapperRect.left}px` highlight.style.width = `${inputRect.width}px` highlight.style.height = `${inputRect.height}px` highlight.style.paddingLeft = inputStyle.paddingLeft highlight.style.paddingRight = inputStyle.paddingRight highlight.style.font = inputStyle.font highlight.style.letterSpacing = inputStyle.letterSpacing highlight.scrollLeft = inputEl.scrollLeft }, []) useLayoutEffect(() => { if (!isExprMode) return undefined syncHighlightLayout() const wrapper = wrapperRef.current if (!wrapper) return undefined const observer = new ResizeObserver(syncHighlightLayout) observer.observe(wrapper) return () => observer.disconnect() }, [isExprMode, exprValue, prefix, suffix, syncHighlightLayout]) const switchToExprMode = (initialValue) => { setIsExprMode(true) setExprValue(initialValue) setTimeout(() => { const input = wrapperRef.current?.getElementsByTagName('input')[0] 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 commitExpr = () => { const result = safeEval(exprValue) if (result != null) { exitExprMode(result) } } const handleInputKeyDown = (e) => { if (e.key === 'Enter' || e.key === '=') { e.preventDefault() commitExpr() } } const handleInputScroll = (e) => { if (highlightRef.current) { highlightRef.current.scrollLeft = e.target.scrollLeft } } 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 (