Files
label-designer/src/components/CommitInput.tsx
2026-06-14 03:59:45 +08:00

161 lines
3.9 KiB
TypeScript

import React, { useState, useEffect, useRef } from 'react';
export interface CommitInputProps
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'onChange' | 'value' | 'type'> {
value: string | number;
onCommit: (val: string) => void;
/** number 类型在编辑时用文本框,提交时由外部校验 min/max */
type?: 'text' | 'number';
}
/**
* 失焦/回车提交型输入框:编辑过程中不应用 HTML min/max 限制,便于清空后重输。
*/
export const CommitInput: React.FC<CommitInputProps> = ({
value,
onCommit,
type = 'text',
min: _min,
max: _max,
step: _step,
onFocus,
onBlur,
onKeyDown,
...props
}) => {
const [localVal, setLocalVal] = useState<string>(String(value));
const focusedRef = useRef(false);
const localValRef = useRef(localVal);
const valueRef = useRef(value);
const onCommitRef = useRef(onCommit);
localValRef.current = localVal;
valueRef.current = value;
onCommitRef.current = onCommit;
useEffect(() => {
if (!focusedRef.current) {
setLocalVal(String(value));
}
}, [value]);
useEffect(() => {
return () => {
if (
focusedRef.current &&
localValRef.current !== String(valueRef.current)
) {
onCommitRef.current(localValRef.current);
}
};
}, []);
const commitIfChanged = () => {
if (localValRef.current !== String(valueRef.current)) {
onCommitRef.current(localValRef.current);
}
};
const handleBlur = (e: React.FocusEvent<HTMLInputElement>) => {
focusedRef.current = false;
onBlur?.(e);
commitIfChanged();
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
(e.target as HTMLInputElement).blur();
}
onKeyDown?.(e);
};
const inputType = type === 'number' ? 'text' : type;
return (
<input
{...props}
type={inputType}
inputMode={type === 'number' ? 'decimal' : props.inputMode}
value={localVal}
onChange={(e) => setLocalVal(e.target.value)}
onFocus={(e) => {
focusedRef.current = true;
onFocus?.(e);
}}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
/>
);
};
export interface CommitTextareaProps
extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'onChange' | 'value'> {
value: string;
onCommit: (val: string) => void;
}
/** 失焦提交型多行输入框 */
export const CommitTextarea: React.FC<CommitTextareaProps> = ({
value,
onCommit,
onFocus,
onBlur,
onKeyDown,
...props
}) => {
const [localVal, setLocalVal] = useState<string>(value);
const focusedRef = useRef(false);
const localValRef = useRef(localVal);
const valueRef = useRef(value);
const onCommitRef = useRef(onCommit);
localValRef.current = localVal;
valueRef.current = value;
onCommitRef.current = onCommit;
useEffect(() => {
if (!focusedRef.current) {
setLocalVal(value);
}
}, [value]);
useEffect(() => {
return () => {
if (focusedRef.current && localValRef.current !== valueRef.current) {
onCommitRef.current(localValRef.current);
}
};
}, []);
const commitIfChanged = () => {
if (localValRef.current !== valueRef.current) {
onCommitRef.current(localValRef.current);
}
};
const handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => {
focusedRef.current = false;
onBlur?.(e);
commitIfChanged();
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
(e.target as HTMLTextAreaElement).blur();
}
onKeyDown?.(e);
};
return (
<textarea
{...props}
value={localVal}
onChange={(e) => setLocalVal(e.target.value)}
onFocus={(e) => {
focusedRef.current = true;
onFocus?.(e);
}}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
/>
);
};