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;
|
||||
}
|
||||
|
||||
.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 {
|
||||
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 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 (
|
||||
<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 +, -, *, /
|
||||
@ -32,21 +78,54 @@ const InputNumberCal = ({
|
||||
suffix,
|
||||
placeholder,
|
||||
disabled,
|
||||
style,
|
||||
...rest
|
||||
}) => {
|
||||
const [isExprMode, setIsExprMode] = useState(false)
|
||||
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) => {
|
||||
setIsExprMode(true)
|
||||
setExprValue(initialValue)
|
||||
setTimeout(() => {
|
||||
//inputRef.current?.focus()
|
||||
const input = inputRef.current.getElementsByTagName('input')[0]
|
||||
if (input) {
|
||||
input.focus()
|
||||
}
|
||||
const input = wrapperRef.current?.getElementsByTagName('input')[0]
|
||||
input?.focus()
|
||||
}, 0)
|
||||
}
|
||||
|
||||
@ -83,13 +162,23 @@ const InputNumberCal = ({
|
||||
}
|
||||
}
|
||||
|
||||
const commitExpr = () => {
|
||||
const result = safeEval(exprValue)
|
||||
if (result != null) {
|
||||
exitExprMode(result)
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputKeyDown = (e) => {
|
||||
if (e.key === '=') {
|
||||
if (e.key === 'Enter' || e.key === '=') {
|
||||
e.preventDefault()
|
||||
const result = safeEval(exprValue)
|
||||
if (result != null) {
|
||||
exitExprMode(result)
|
||||
}
|
||||
commitExpr()
|
||||
}
|
||||
}
|
||||
|
||||
const handleInputScroll = (e) => {
|
||||
if (highlightRef.current) {
|
||||
highlightRef.current.scrollLeft = e.target.scrollLeft
|
||||
}
|
||||
}
|
||||
|
||||
@ -125,13 +214,26 @@ const InputNumberCal = ({
|
||||
|
||||
if (isExprMode) {
|
||||
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
|
||||
ref={(node) => {
|
||||
inputElRef.current = node?.input ?? null
|
||||
}}
|
||||
className='input-number-cal-expr-input'
|
||||
value={exprValue}
|
||||
onChange={handleInputChange}
|
||||
onKeyDown={handleInputKeyDown}
|
||||
onBlur={handleInputBlur}
|
||||
onScroll={handleInputScroll}
|
||||
{...commonProps}
|
||||
style={style}
|
||||
/>
|
||||
<div className='input-number-cal-icon'>
|
||||
<FunctionIcon style={{ fontSize: 24 }} />
|
||||
@ -147,6 +249,7 @@ const InputNumberCal = ({
|
||||
onChange={onChange}
|
||||
onBlur={onBlur}
|
||||
onKeyDown={handleNumberKeyDown}
|
||||
style={style}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -161,6 +264,7 @@ InputNumberCal.propTypes = {
|
||||
prefix: PropTypes.node,
|
||||
suffix: PropTypes.node,
|
||||
placeholder: PropTypes.string,
|
||||
style: PropTypes.object,
|
||||
disabled: PropTypes.bool
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user