Enhance InputNumberCal Component with Expression Highlighting and Improved Layout
- Added expression highlighting functionality to the InputNumberCal component, allowing operators and brackets to be visually distinct. - Implemented layout synchronization for highlighted expressions to match input field dimensions. - Updated styles in App.css for dropdown and input number components to improve visual hierarchy and user experience. - Refactored input handling to support expression mode with keyboard interactions for better usability.
This commit is contained in:
parent
b590890bd3
commit
1597370a16
@ -750,6 +750,39 @@ body {
|
|||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-number-cal-highlight {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: pre;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-number-cal-operator {
|
||||||
|
color: var(--color-purple);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-number-cal-bracket {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-number-cal-expr-input,
|
||||||
|
.input-number-cal-expr-input.ant-input-affix-wrapper {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-number-cal-expr-input.ant-input,
|
||||||
|
.input-number-cal-expr-input .ant-input {
|
||||||
|
background: transparent !important;
|
||||||
|
color: transparent !important;
|
||||||
|
caret-color: var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
.input-number-cal .ant-input-suffix {
|
.input-number-cal .ant-input-suffix {
|
||||||
margin-right: 28px;
|
margin-right: 28px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,55 @@
|
|||||||
import { useState, useRef } from 'react'
|
import { useCallback, useLayoutEffect, useRef, useState } from 'react'
|
||||||
import { Input, InputNumber } from 'antd'
|
import { Input, InputNumber } from 'antd'
|
||||||
import PropTypes from 'prop-types'
|
import PropTypes from 'prop-types'
|
||||||
import FunctionIcon from '../../Icons/FunctionIcon'
|
import FunctionIcon from '../../Icons/FunctionIcon'
|
||||||
|
|
||||||
const OPERATOR_KEYS = ['+', '-', '*', '/']
|
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 (
|
||||||
|
<span key={index} className='input-number-cal-operator'>
|
||||||
|
{token.value}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (token.type === 'bracket') {
|
||||||
|
return (
|
||||||
|
<span key={index} className='input-number-cal-bracket'>
|
||||||
|
{token.value}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return <span key={index}>{token.value}</span>
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Safely evaluate a math expression. Only allows numbers and +, -, *, /
|
* Safely evaluate a math expression. Only allows numbers and +, -, *, /
|
||||||
@ -32,21 +78,54 @@ const InputNumberCal = ({
|
|||||||
suffix,
|
suffix,
|
||||||
placeholder,
|
placeholder,
|
||||||
disabled,
|
disabled,
|
||||||
|
style,
|
||||||
...rest
|
...rest
|
||||||
}) => {
|
}) => {
|
||||||
const [isExprMode, setIsExprMode] = useState(false)
|
const [isExprMode, setIsExprMode] = useState(false)
|
||||||
const [exprValue, setExprValue] = useState('')
|
const [exprValue, setExprValue] = useState('')
|
||||||
const inputRef = useRef(null)
|
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) => {
|
const switchToExprMode = (initialValue) => {
|
||||||
setIsExprMode(true)
|
setIsExprMode(true)
|
||||||
setExprValue(initialValue)
|
setExprValue(initialValue)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
//inputRef.current?.focus()
|
const input = wrapperRef.current?.getElementsByTagName('input')[0]
|
||||||
const input = inputRef.current.getElementsByTagName('input')[0]
|
input?.focus()
|
||||||
if (input) {
|
|
||||||
input.focus()
|
|
||||||
}
|
|
||||||
}, 0)
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -83,13 +162,23 @@ const InputNumberCal = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const commitExpr = () => {
|
||||||
|
const result = safeEval(exprValue)
|
||||||
|
if (result != null) {
|
||||||
|
exitExprMode(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleInputKeyDown = (e) => {
|
const handleInputKeyDown = (e) => {
|
||||||
if (e.key === '=') {
|
if (e.key === 'Enter' || e.key === '=') {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
const result = safeEval(exprValue)
|
commitExpr()
|
||||||
if (result != null) {
|
}
|
||||||
exitExprMode(result)
|
}
|
||||||
}
|
|
||||||
|
const handleInputScroll = (e) => {
|
||||||
|
if (highlightRef.current) {
|
||||||
|
highlightRef.current.scrollLeft = e.target.scrollLeft
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,13 +214,26 @@ const InputNumberCal = ({
|
|||||||
|
|
||||||
if (isExprMode) {
|
if (isExprMode) {
|
||||||
return (
|
return (
|
||||||
<div className='input-number-cal' ref={inputRef}>
|
<div className='input-number-cal' ref={wrapperRef} style={style}>
|
||||||
|
<div
|
||||||
|
ref={highlightRef}
|
||||||
|
className='input-number-cal-highlight'
|
||||||
|
aria-hidden
|
||||||
|
>
|
||||||
|
{renderHighlightedExpr(exprValue)}
|
||||||
|
</div>
|
||||||
<Input
|
<Input
|
||||||
|
ref={(node) => {
|
||||||
|
inputElRef.current = node?.input ?? null
|
||||||
|
}}
|
||||||
|
className='input-number-cal-expr-input'
|
||||||
value={exprValue}
|
value={exprValue}
|
||||||
onChange={handleInputChange}
|
onChange={handleInputChange}
|
||||||
onKeyDown={handleInputKeyDown}
|
onKeyDown={handleInputKeyDown}
|
||||||
onBlur={handleInputBlur}
|
onBlur={handleInputBlur}
|
||||||
|
onScroll={handleInputScroll}
|
||||||
{...commonProps}
|
{...commonProps}
|
||||||
|
style={style}
|
||||||
/>
|
/>
|
||||||
<div className='input-number-cal-icon'>
|
<div className='input-number-cal-icon'>
|
||||||
<FunctionIcon style={{ fontSize: 24 }} />
|
<FunctionIcon style={{ fontSize: 24 }} />
|
||||||
@ -147,6 +249,7 @@ const InputNumberCal = ({
|
|||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
onKeyDown={handleNumberKeyDown}
|
onKeyDown={handleNumberKeyDown}
|
||||||
|
style={style}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -161,6 +264,7 @@ InputNumberCal.propTypes = {
|
|||||||
prefix: PropTypes.node,
|
prefix: PropTypes.node,
|
||||||
suffix: PropTypes.node,
|
suffix: PropTypes.node,
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
|
style: PropTypes.object,
|
||||||
disabled: PropTypes.bool
|
disabled: PropTypes.bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user