New number calculator functionality.
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
Some checks failed
farmcontrol/farmcontrol-ui/pipeline/head There was a failure building this commit
This commit is contained in:
parent
8150372fda
commit
e4255443a0
8
assets/icons/functionicon.svg
Normal file
8
assets/icons/functionicon.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
||||||
|
<svg width="100%" height="100%" viewBox="0 0 64 64" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
||||||
|
<g transform="matrix(0.624686,0,0,0.624686,3,17.237995)">
|
||||||
|
<path d="M45.17,47.212C46.58,47.212 47.578,46.42 47.578,45.039C47.578,44.432 47.455,44.033 47.022,43.038C43.627,36.904 41.807,30.56 41.807,23.653C41.807,16.937 43.541,10.39 47.022,4.224C47.455,3.24 47.578,2.84 47.578,2.233C47.578,0.895 46.58,0.05 45.17,0.05C43.622,0.05 42.412,0.807 41.094,2.698C36.763,8.399 34.637,15.598 34.637,23.632C34.637,31.696 36.75,38.726 41.094,44.574C42.402,46.475 43.622,47.212 45.17,47.212ZM53.441,39.915C54.789,39.915 55.627,39.472 56.736,37.85L63.381,28.189L63.526,28.189L70.367,38.036C71.337,39.448 72.177,39.915 73.434,39.915C75.47,39.915 76.876,38.574 76.876,36.677C76.876,35.855 76.63,35.133 76.075,34.383L68.175,23.647L76.006,13.227C76.629,12.367 76.927,11.646 76.927,10.722C76.927,8.851 75.474,7.596 73.587,7.596C72.104,7.596 71.3,8.297 70.379,9.72L64.033,19.158L63.877,19.158L57.501,9.689C56.548,8.256 55.66,7.596 54.075,7.596C52.07,7.596 50.568,9.08 50.568,10.866C50.568,11.932 50.899,12.683 51.435,13.403L59.08,23.63L51.105,34.459C50.429,35.36 50.226,36.071 50.226,36.964C50.226,38.669 51.625,39.915 53.441,39.915ZM82.304,47.212C83.862,47.212 85.062,46.465 86.38,44.574C90.754,38.736 92.847,31.696 92.847,23.632C92.847,15.598 90.682,8.419 86.38,2.698C85.072,0.797 83.862,0.05 82.304,0.05C80.894,0.05 79.896,0.895 79.896,2.233C79.896,2.84 80.029,3.24 80.452,4.224C83.933,10.39 85.667,16.937 85.667,23.653C85.667,30.56 83.848,36.904 80.452,43.038C80.029,44.033 79.896,44.432 79.896,45.039C79.896,46.377 80.894,47.212 82.304,47.212Z" style="fill-rule:nonzero;"/>
|
||||||
|
<path d="M5.252,47.17C12.053,47.17 15.203,44.455 16.783,37.027L20.127,21.046L25.664,21.046C27.685,21.046 29.099,19.888 29.099,17.909C29.099,16.108 27.887,15.025 26.141,15.025L21.395,15.025L22.161,11.302C22.935,7.691 24.066,6.295 27.165,6.295C27.697,6.295 28.187,6.263 28.56,6.222C30.37,5.986 31.317,5.049 31.317,3.437C31.317,1.14 29.52,0.071 25.884,0.071C19.19,0.071 15.859,3.04 14.354,10.214L13.352,15.025L9.606,15.025C7.544,15.025 6.149,16.193 6.149,18.163C6.149,19.974 7.342,21.046 9.139,21.046L12.084,21.046L8.945,35.949C8.159,39.645 7.007,40.946 3.961,40.946C3.534,40.946 3.096,40.987 2.775,41.019C0.93,41.266 0,42.325 0,43.856C0,46.1 1.765,47.17 5.252,47.17Z" style="fill-rule:nonzero;"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.7 KiB |
@ -417,3 +417,32 @@ body {
|
|||||||
.ant-badge .ant-badge-count-sm {
|
.ant-badge .ant-badge-count-sm {
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-number-cal {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-number-cal .ant-input-suffix {
|
||||||
|
margin-right: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-number-cal .ant-input-outlined {
|
||||||
|
border-color: var(--color-purple);
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--color-purple) 31%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-number-cal .ant-input {
|
||||||
|
font-family: 'DM Mono';
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-number-cal-icon {
|
||||||
|
position: absolute;
|
||||||
|
right: 10px;
|
||||||
|
top: 0;
|
||||||
|
bottom: 1px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|||||||
167
src/components/Dashboard/common/InputNumberCal.jsx
Normal file
167
src/components/Dashboard/common/InputNumberCal.jsx
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
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
|
||||||
@ -27,6 +27,7 @@ import { getPropertyValue } from '../../../database/ObjectModels'
|
|||||||
import PropertyChanges from './PropertyChanges'
|
import PropertyChanges from './PropertyChanges'
|
||||||
import NetGrossDisplay from './NetGrossDisplay'
|
import NetGrossDisplay from './NetGrossDisplay'
|
||||||
import NetGrossInput from './NetGrossInput'
|
import NetGrossInput from './NetGrossInput'
|
||||||
|
import InputNumberCal from './InputNumberCal'
|
||||||
import ObjectList from './ObjectList'
|
import ObjectList from './ObjectList'
|
||||||
import VarianceDisplay from './VarianceDisplay'
|
import VarianceDisplay from './VarianceDisplay'
|
||||||
import OperationDisplay from './OperationDisplay'
|
import OperationDisplay from './OperationDisplay'
|
||||||
@ -713,7 +714,7 @@ const ObjectProperty = ({
|
|||||||
)
|
)
|
||||||
case 'number':
|
case 'number':
|
||||||
return (
|
return (
|
||||||
<InputNumber
|
<InputNumberCal
|
||||||
placeholder={label}
|
placeholder={label}
|
||||||
prefix={prefix}
|
prefix={prefix}
|
||||||
suffix={suffix}
|
suffix={suffix}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ const COLORS = {
|
|||||||
colorLink: '#5AC8F5',
|
colorLink: '#5AC8F5',
|
||||||
colorCyan: '#5AC8F5',
|
colorCyan: '#5AC8F5',
|
||||||
colorPink: '#FF69B4',
|
colorPink: '#FF69B4',
|
||||||
colorPurple: '#800080',
|
colorPurple: '#6B5DFF',
|
||||||
colorMagenta: '#FF00FF',
|
colorMagenta: '#FF00FF',
|
||||||
colorVolcano: '#FF4500'
|
colorVolcano: '#FF4500'
|
||||||
}
|
}
|
||||||
@ -114,7 +114,10 @@ export const ThemeProvider = ({ children }) => {
|
|||||||
root.style.setProperty('--color-purple', COLORS.colorPurple)
|
root.style.setProperty('--color-purple', COLORS.colorPurple)
|
||||||
root.style.setProperty('--color-magenta', COLORS.colorMagenta)
|
root.style.setProperty('--color-magenta', COLORS.colorMagenta)
|
||||||
root.style.setProperty('--color-volcano', COLORS.colorVolcano)
|
root.style.setProperty('--color-volcano', COLORS.colorVolcano)
|
||||||
root.style.setProperty('--layout-header-bg', isDarkMode ? '#141414' : '#ffffff')
|
root.style.setProperty(
|
||||||
|
'--layout-header-bg',
|
||||||
|
isDarkMode ? '#141414' : '#ffffff'
|
||||||
|
)
|
||||||
}, [isDarkMode])
|
}, [isDarkMode])
|
||||||
|
|
||||||
const themeConfig = {
|
const themeConfig = {
|
||||||
|
|||||||
6
src/components/Icons/FunctionIcon.jsx
Normal file
6
src/components/Icons/FunctionIcon.jsx
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import Icon from '@ant-design/icons'
|
||||||
|
import CustomIconSvg from '../../../assets/icons/functionicon.svg?react'
|
||||||
|
|
||||||
|
const FunctionIcon = (props) => <Icon component={CustomIconSvg} {...props} />
|
||||||
|
|
||||||
|
export default FunctionIcon
|
||||||
Loading…
x
Reference in New Issue
Block a user