diff --git a/src/entitiy/element/model/implementations/TextElement.tsx b/src/entitiy/element/model/implementations/TextElement.tsx index c1d0320..c47f802 100644 --- a/src/entitiy/element/model/implementations/TextElement.tsx +++ b/src/entitiy/element/model/implementations/TextElement.tsx @@ -1,13 +1,14 @@ +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 { ExtraSettingsBadge } from '@/entitiy/element/ui/extraSettingsBadge' -import { Save, SaveOff, Type } from 'lucide-react' +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 @@ -49,6 +50,7 @@ export const textDefinition: ElementDefinition = { defaultConfig: { placeholder: '', required: false, + showIncrementButton: false, targetCells: [], name: '', }, @@ -65,7 +67,25 @@ export const textDefinition: ElementDefinition = { const cells = config.targetCells || [] return cells.map(target => ({ target, value: value || '' })) }, - Editor: () => , + Editor: ({ config, onChange }) => ( +
+
+ + onChange({ ...config, showIncrementButton: !!checked }) + } + /> + +
+
+ ), Preview: ({ config }) => (
{config.name && ( @@ -74,12 +94,32 @@ export const textDefinition: ElementDefinition = { {config.required && *} )} - +
+ + {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(() => { @@ -133,8 +173,218 @@ export const textDefinition: ElementDefinition = { } } + 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 +
+ + )} + {/* Бейджи в правом верхнем углу */}
{/* Галочка сохранения */} @@ -177,13 +427,46 @@ export const textDefinition: ElementDefinition = { )} - {/* Поле ввода */} - handleValueChange(e.target.value)} - required={config.required} - /> + {/* Поле ввода с кнопкой */} +
+ { + if (!isAnimating) { + handleValueChange(e.target.value) + } + }} + 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 && ( + + )} +
) },