forked from 77media/video-flow
173 lines
5.0 KiB
TypeScript
173 lines
5.0 KiB
TypeScript
/**
|
|
* 编辑输入框组件
|
|
* 实现编辑描述输入功能,支持键盘操作和动画效果
|
|
*/
|
|
|
|
import React, { useState, useRef, useEffect, useCallback } from 'react';
|
|
import { motion, AnimatePresence } from 'framer-motion';
|
|
import { Send, X, Loader2 } from 'lucide-react';
|
|
import { EditPoint, EditPointStatus } from './types';
|
|
|
|
interface EditInputProps {
|
|
/** 编辑点数据 */
|
|
editPoint: EditPoint;
|
|
/** 输入框位置 */
|
|
position: { x: number; y: number };
|
|
/** 是否显示 */
|
|
isVisible: boolean;
|
|
/** 是否正在提交 */
|
|
isSubmitting?: boolean;
|
|
/** 提交回调 */
|
|
onSubmit: (description: string) => void;
|
|
/** 取消回调 */
|
|
onCancel: () => void;
|
|
/** 输入框尺寸 */
|
|
size?: { width: number; height: number };
|
|
/** 占位符文本 */
|
|
placeholder?: string;
|
|
}
|
|
|
|
/**
|
|
* 编辑输入框组件
|
|
*/
|
|
export const EditInput: React.FC<EditInputProps> = ({
|
|
editPoint,
|
|
position,
|
|
isVisible,
|
|
isSubmitting = false,
|
|
onSubmit,
|
|
onCancel,
|
|
size = { width: 300, height: 50 },
|
|
placeholder = "Describe your edit..."
|
|
}) => {
|
|
const [description, setDescription] = useState(editPoint.description || '');
|
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// 自动聚焦
|
|
useEffect(() => {
|
|
if (isVisible && textareaRef.current) {
|
|
// 延迟聚焦,等待动画完成
|
|
const timer = setTimeout(() => {
|
|
textareaRef.current?.focus();
|
|
}, 300);
|
|
return () => clearTimeout(timer);
|
|
}
|
|
}, [isVisible]);
|
|
|
|
// 键盘事件处理
|
|
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
if (e.key === 'Escape') {
|
|
e.preventDefault();
|
|
onCancel();
|
|
} else if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
|
|
e.preventDefault();
|
|
handleSubmit();
|
|
}
|
|
}, [onCancel]);
|
|
|
|
// 提交处理
|
|
const handleSubmit = useCallback(() => {
|
|
const trimmedDescription = description.trim();
|
|
if (trimmedDescription && !isSubmitting) {
|
|
onSubmit(trimmedDescription);
|
|
}
|
|
}, [description, isSubmitting, onSubmit]);
|
|
|
|
// 点击外部关闭
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
// 如果有内容,自动提交;否则取消
|
|
if (description.trim()) {
|
|
handleSubmit();
|
|
} else {
|
|
onCancel();
|
|
}
|
|
}
|
|
};
|
|
|
|
if (isVisible) {
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}
|
|
}, [isVisible, description, handleSubmit, onCancel]);
|
|
|
|
// 自动调整高度
|
|
const adjustHeight = useCallback(() => {
|
|
if (textareaRef.current) {
|
|
textareaRef.current.style.height = 'auto';
|
|
textareaRef.current.style.height = `${Math.min(textareaRef.current.scrollHeight, 120)}px`;
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
adjustHeight();
|
|
}, [description, adjustHeight]);
|
|
|
|
return (
|
|
<AnimatePresence>
|
|
{isVisible && (
|
|
<motion.div
|
|
ref={containerRef}
|
|
className="absolute z-30"
|
|
data-edit-input="true"
|
|
style={{
|
|
left: position.x,
|
|
top: position.y,
|
|
}}
|
|
initial={{
|
|
opacity: 0,
|
|
scale: 0.8,
|
|
y: -10
|
|
}}
|
|
animate={{
|
|
opacity: 1,
|
|
scale: 1,
|
|
y: 0
|
|
}}
|
|
exit={{
|
|
opacity: 0,
|
|
scale: 0.8,
|
|
y: -10
|
|
}}
|
|
transition={{
|
|
type: "spring",
|
|
stiffness: 300,
|
|
damping: 25,
|
|
duration: 0.3
|
|
}}
|
|
>
|
|
{/* Input interface - focused on input functionality only */}
|
|
<div className="flex items-center bg-white/90 backdrop-blur-sm rounded-lg px-3 py-2 shadow-lg">
|
|
<input
|
|
ref={textareaRef as any}
|
|
type="text"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
onKeyDown={handleKeyDown}
|
|
placeholder={placeholder}
|
|
disabled={isSubmitting}
|
|
className="flex-1 bg-transparent text-gray-800 placeholder-gray-400 text-sm border-none outline-none min-w-[200px]"
|
|
autoFocus
|
|
/>
|
|
|
|
{/* Submit button */}
|
|
<button
|
|
onClick={handleSubmit}
|
|
disabled={!description.trim() || isSubmitting}
|
|
className="ml-2 w-8 h-8 rounded-full bg-blue-500 hover:bg-blue-600 disabled:bg-gray-400 disabled:cursor-not-allowed text-white flex items-center justify-center flex-shrink-0"
|
|
>
|
|
{isSubmitting ? (
|
|
<div className="w-3 h-3 border border-white border-t-transparent rounded-full animate-spin"></div>
|
|
) : (
|
|
<span className="text-xs">→</span>
|
|
)}
|
|
</button>
|
|
</div>
|
|
</motion.div>
|
|
)}
|
|
</AnimatePresence>
|
|
);
|
|
};
|