161 lines
3.9 KiB
TypeScript
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}
|
|
/>
|
|
);
|
|
};
|