import { Checkbox } from '@/component/ui/checkbox' import { Input } from '@/component/ui/input' import { ElementDefinition } from '@/entitiy/element/model/interface' import { CellsBadge } from '@/entitiy/element/ui/cellsBadge' import { Plus, Save, SaveOff, Type } from 'lucide-react' import { useEffect, useState } from 'react' interface TextConfig { placeholder?: string required?: boolean showIncrementButton?: boolean // Новый параметр для отображения кнопки +1 targetCells: any[] name?: string id?: string // Добавляем id для совместимости с TemplateElement } // Утилиты для работы с localStorage const getLocalStorageKey = (type: string, id: string) => `${type}_${id}` const saveToLocalStorage = (key: string, value: string) => { try { localStorage.setItem(key, value) } catch (error) { console.warn('Failed to save to localStorage:', error) } } const getFromLocalStorage = (key: string): string => { try { return localStorage.getItem(key) || '' } catch (error) { console.warn('Failed to get from localStorage:', error) return '' } } const removeFromLocalStorage = (key: string) => { try { localStorage.removeItem(key) } catch (error) { console.warn('Failed to remove from localStorage:', error) } } export const textDefinition: ElementDefinition = { type: 'text', label: 'Текстовое поле', icon: , version: 1, defaultConfig: { placeholder: '', required: false, showIncrementButton: false, targetCells: [], name: '', }, getInitialValue: config => { // Используем id из config (который приходит из TemplateElement) if (config.id) { const key = getLocalStorageKey('text', config.id) return getFromLocalStorage(key) } return '' }, mapToCells: config => config.targetCells || [], mapToCellValues: (config, value) => { const cells = config.targetCells || [] return cells.map(target => ({ target, value: value || '' })) }, Editor: ({ config, onChange }) => (
onChange({ ...config, showIncrementButton: !!checked }) } />
), Preview: ({ config }) => (
{config.name && ( )}
{config.showIncrementButton && ( )}
), Render: ({ config, value, onChange }) => { const [localStorageEnabled, setLocalStorageEnabled] = useState(false) const [currentValue, setCurrentValue] = useState(value || '') const [isAnimating, setIsAnimating] = useState(false) const [rollingDigits, setRollingDigits] = useState([]) const [flyingDigit, setFlyingDigit] = useState<{ show: boolean x: number y: number }>({ show: false, x: 0, y: 0 }) // Инициализация состояния localStorage из сохраненных данных useEffect(() => { if (config.id) { const key = getLocalStorageKey('text', config.id) const savedValue = getFromLocalStorage(key) // Если есть сохраненное значение, включаем localStorage и загружаем его if (savedValue) { setLocalStorageEnabled(true) if (savedValue !== currentValue) { setCurrentValue(savedValue) onChange?.(savedValue) } } } }, [config.id]) // Синхронизация внешнего значения useEffect(() => { if (value !== currentValue) { setCurrentValue(value || '') } }, [value]) const handleValueChange = (newValue: string) => { setCurrentValue(newValue) onChange?.(newValue) // Сохранение в localStorage если включено if (localStorageEnabled && config.id) { const key = getLocalStorageKey('text', config.id) saveToLocalStorage(key, newValue) } } const handleLocalStorageToggle = () => { const newEnabled = !localStorageEnabled setLocalStorageEnabled(newEnabled) if (config.id) { const key = getLocalStorageKey('text', config.id) if (newEnabled) { // Включаем localStorage - сохраняем текущее значение saveToLocalStorage(key, currentValue) } else { // Выключаем localStorage - удаляем сохраненное значение removeFromLocalStorage(key) } } } const handleIncrementClick = () => { if (isAnimating) return const currentNum = parseFloat(currentValue) || 0 const newNum = currentNum + 1 const currentStr = currentNum.toString() const newStr = newNum.toString() setIsAnimating(true) // Получаем позицию кнопки для анимации вылета const buttonElement = document.querySelector( '[data-increment-button]' ) as HTMLElement if (buttonElement) { const rect = buttonElement.getBoundingClientRect() const inputElement = buttonElement .closest('.relative') ?.querySelector('input') const inputRect = inputElement?.getBoundingClientRect() if (inputRect) { setFlyingDigit({ show: true, x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, }) } } // Проверяем, увеличивается ли количество разрядов const isAddingDigit = newStr.length > currentStr.length if (isAddingDigit) { // Если добавляется разряд, делаем анимацию сдвига setRollingDigits(currentStr.split('')) // Анимация сдвига при добавлении разряда const duration = 800 const startTime = Date.now() const animateSlide = () => { const elapsed = Date.now() - startTime const progress = Math.min(elapsed / duration, 1) // Убираем летящую цифру через 400мс if (elapsed > 400) { setFlyingDigit(prev => ({ ...prev, show: false })) } if (progress < 1) { // Плавное появление новой цифры слева и изменение старых const easeOutQuad = (t: number) => t * (2 - t) const smoothProgress = easeOutQuad(progress) // Разбираем целевое число на цифры const targetDigits = newStr.split('') const currentDigits = currentStr.split('') if (smoothProgress > 0.2) { // Создаем анимированный массив цифр const animatedDigits = targetDigits.map((targetDigit, index) => { if (index === 0) { // Первая цифра (новая) - плавно появляется return targetDigit } else { // Остальные цифры - плавно меняются от старых к новым const oldDigit = currentDigits[index - 1] || '0' const oldNum = parseInt(oldDigit) const targetNum = parseInt(targetDigit) // Интерполяция между старой и новой цифрой const currentNum = oldNum + (targetNum - oldNum) * smoothProgress // В конце анимации фиксируем на целевой цифре if (progress > 0.9) { return targetDigit } return Math.round(currentNum).toString() } }) setRollingDigits(animatedDigits) } } else { // Финал setRollingDigits([]) setIsAnimating(false) handleValueChange(newStr) } if (progress < 1) { requestAnimationFrame(animateSlide) } } requestAnimationFrame(animateSlide) } else { // Обычная анимация для случаев без добавления разряда const maxLength = Math.max(currentStr.length, newStr.length) const currentPadded = currentStr.padStart(maxLength, '0') const newPadded = newStr.padStart(maxLength, '0') // Создаем массив с начальными значениями setRollingDigits(currentPadded.split('')) // Анимация плавного перехода цифр const duration = 800 const startTime = Date.now() const animateTransition = () => { const elapsed = Date.now() - startTime const progress = Math.min(elapsed / duration, 1) // Убираем летящую цифру через 400мс if (elapsed > 400) { setFlyingDigit(prev => ({ ...prev, show: false })) } if (progress < 1) { // Используем функцию сглаживания для более плавной анимации const easeOutQuad = (t: number) => t * (2 - t) const smoothProgress = easeOutQuad(progress) const newRolling = currentPadded .split('') .map((oldDigit, index) => { const targetDigit = newPadded[index] // Если цифра не изменилась, оставляем как есть if (oldDigit === targetDigit) { return oldDigit } // Для изменившихся цифр - прямой переход с интерполяцией const oldNum = parseInt(oldDigit) const targetNum = parseInt(targetDigit) // Прямая интерполяция между старой и новой цифрой const currentNum = oldNum + (targetNum - oldNum) * smoothProgress // В конце анимации фиксируем на целевой цифре if (progress > 0.95) { return targetDigit } return Math.round(currentNum).toString() }) setRollingDigits(newRolling) } else { // Финал setRollingDigits([]) setIsAnimating(false) handleValueChange(newStr) } if (progress < 1) { requestAnimationFrame(animateTransition) } } requestAnimationFrame(animateTransition) } } // Определяем отображаемое значение const displayValue = isAnimating && rollingDigits.length > 0 ? rollingDigits.join('').replace(/^0+/, '') || '0' : currentValue return (
{/* Летящая единичка */} {flyingDigit.show && ( <>
+1
)} {/* Бейджи в правом верхнем углу */}
{/* Галочка сохранения */} {config.id && (
{localStorageEnabled ? 'Автосохранение включено' : 'Автосохранение выключено'}
{localStorageEnabled ? 'Значение сохраняется в браузере' : 'Нажмите, чтобы включить автосохранение'}
)} {/* Бейдж ячеек */}
{/* Лейбл поля */} {config.name && ( )} {/* Поле ввода с кнопкой */}
{ if (!isAnimating) { // Автоматически заменяем запятые на точки const normalizedValue = e.target.value.replace(/,/g, '.') handleValueChange(normalizedValue) } }} required={config.required} className={`${config.showIncrementButton ? 'pr-10' : ''} ${ isAnimating ? 'font-mono text-lg font-bold tracking-wider text-blue-600' : '' } transition-all duration-300`} readOnly={isAnimating} /> {/* Кнопка +1 */} {config.showIncrementButton && ( )}
) }, }