181 lines
4.3 KiB
JavaScript
181 lines
4.3 KiB
JavaScript
import { useState, useRef } from 'react'
|
|
import PropTypes from 'prop-types'
|
|
import { DatePicker, Input } from 'antd'
|
|
import dayjs from 'dayjs'
|
|
import FunctionIcon from '../../Icons/FunctionIcon'
|
|
|
|
const OPERATOR_KEYS = ['+', '-']
|
|
|
|
const TIME_UNITS = {
|
|
s: 'second',
|
|
m: 'minute',
|
|
h: 'hour',
|
|
d: 'day',
|
|
w: 'week',
|
|
M: 'month',
|
|
y: 'year'
|
|
}
|
|
|
|
const TERM_REGEX = /([+-])(\d+(?:\.\d+)?)([smhdwMy])/g
|
|
const RELATIVE_TERM_START = /[+-]\d+(?:\.\d+)?[smhdwMy]/
|
|
const DATE_EXPR_FORMAT = 'YYYY-MM-DD HH:mm:ss'
|
|
|
|
function formatExprBase(date) {
|
|
if (date == null || !dayjs(date).isValid()) return ''
|
|
return dayjs(date).format(DATE_EXPR_FORMAT)
|
|
}
|
|
|
|
function applyTimeExpr(fallbackBase, expr) {
|
|
const trimmed = String(expr).trim()
|
|
if (!trimmed) return null
|
|
|
|
const termStart = trimmed.search(RELATIVE_TERM_START)
|
|
if (termStart === -1) return null
|
|
|
|
let base
|
|
let termsPart
|
|
|
|
if (termStart === 0) {
|
|
base =
|
|
fallbackBase && dayjs(fallbackBase).isValid()
|
|
? dayjs(fallbackBase)
|
|
: dayjs()
|
|
termsPart = trimmed
|
|
} else {
|
|
const baseStr = trimmed.slice(0, termStart).trim()
|
|
const parsed = dayjs(baseStr)
|
|
base = parsed.isValid()
|
|
? parsed
|
|
: fallbackBase && dayjs(fallbackBase).isValid()
|
|
? dayjs(fallbackBase)
|
|
: dayjs()
|
|
termsPart = trimmed.slice(termStart)
|
|
}
|
|
|
|
const termsStr = termsPart.replace(/\s/g, '')
|
|
const terms = [...termsStr.matchAll(TERM_REGEX)]
|
|
if (!terms.length) return null
|
|
|
|
const consumed = terms.map((t) => t[0]).join('')
|
|
if (consumed !== termsStr) return null
|
|
|
|
let result = base
|
|
for (const [, sign, amount, unit] of terms) {
|
|
const unitKey = TIME_UNITS[unit]
|
|
if (!unitKey) return null
|
|
const delta = sign === '+' ? parseFloat(amount) : -parseFloat(amount)
|
|
result = result.add(delta, unitKey)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
const TimeEdit = ({
|
|
value,
|
|
onChange,
|
|
onBlur,
|
|
disabled,
|
|
...rest
|
|
}) => {
|
|
const [isExprMode, setIsExprMode] = useState(false)
|
|
const [exprValue, setExprValue] = useState('')
|
|
const inputRef = useRef(null)
|
|
|
|
const dayjsValue =
|
|
value == null ? null : dayjs.isDayjs(value) ? value : dayjs(value)
|
|
const pickerValue =
|
|
dayjsValue && dayjsValue.isValid() ? dayjsValue : null
|
|
|
|
const switchToExprMode = (initialValue) => {
|
|
setIsExprMode(true)
|
|
setExprValue(initialValue)
|
|
setTimeout(() => {
|
|
const input = inputRef.current?.getElementsByTagName('input')[0]
|
|
input?.focus()
|
|
}, 0)
|
|
}
|
|
|
|
const exitExprMode = (result) => {
|
|
setIsExprMode(false)
|
|
setExprValue('')
|
|
if (result != null && result.isValid()) {
|
|
onChange?.(result)
|
|
}
|
|
}
|
|
|
|
const commitExpr = () => {
|
|
const expr = exprValue.trim()
|
|
if (expr && expr.match(/[+-]/) && expr.match(/[smhdwMy]/)) {
|
|
const result = applyTimeExpr(pickerValue, expr)
|
|
if (result != null) {
|
|
exitExprMode(result)
|
|
return
|
|
}
|
|
}
|
|
exitExprMode(null)
|
|
}
|
|
|
|
const handleDatePickerKeyDown = (e) => {
|
|
if (OPERATOR_KEYS.includes(e.key)) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
switchToExprMode(formatExprBase(pickerValue) + e.key)
|
|
}
|
|
}
|
|
|
|
const handleInputKeyDown = (e) => {
|
|
if (e.key === 'Enter' || e.key === '=') {
|
|
e.preventDefault()
|
|
const result = applyTimeExpr(pickerValue, exprValue)
|
|
if (result != null) {
|
|
exitExprMode(result)
|
|
}
|
|
}
|
|
}
|
|
|
|
const handleInputBlur = (e) => {
|
|
commitExpr()
|
|
onBlur?.(e)
|
|
}
|
|
|
|
if (isExprMode) {
|
|
return (
|
|
<div className='input-number-cal' ref={inputRef}>
|
|
<Input
|
|
value={exprValue}
|
|
onChange={(e) => setExprValue(e.target.value)}
|
|
onKeyDown={handleInputKeyDown}
|
|
onBlur={handleInputBlur}
|
|
disabled={disabled}
|
|
placeholder='+1d, -2m, +1h'
|
|
style={{ width: '100%' }}
|
|
/>
|
|
<div className='input-number-cal-icon'>
|
|
<FunctionIcon style={{ fontSize: 24 }} />
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<DatePicker
|
|
showTime
|
|
style={{ width: '100%' }}
|
|
value={pickerValue}
|
|
onChange={onChange}
|
|
onKeyDown={handleDatePickerKeyDown}
|
|
disabled={disabled}
|
|
{...rest}
|
|
/>
|
|
)
|
|
}
|
|
|
|
TimeEdit.propTypes = {
|
|
value: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
|
|
onChange: PropTypes.func,
|
|
onBlur: PropTypes.func,
|
|
disabled: PropTypes.bool
|
|
}
|
|
|
|
export default TimeEdit
|