Files
protoc-frontend/src/component/DualSpreadsheet/components/FormulaBar.tsx

234 lines
9.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Save, Sigma } from 'lucide-react'
import { memo } from 'react'
import { ExcelUploadPanel } from '../../TemplateManager/ExcelUploadPanel'
import { FormulaBarProps } from '../types'
import { getColumnLabel } from '../utils'
export const FormulaBar = memo(
({
selectedCell,
formulaBarValue,
isCalculating,
isEditing,
onValueChange,
onFocus,
onBlur,
onKeyDown,
formulaBarRef,
isSaving = false,
isLoading = false,
hasUnsavedChanges = false,
lastSaveError = null,
onManualSave,
onClearError,
excelFileName,
isExcelLoading = false,
onExcelFileChange,
}: Omit<FormulaBarProps, 'sheetType' | 'activeSheet'>) => {
return (
<div className="formula-bar flex-shrink-0 border-b bg-background px-3 py-1.5">
<div className="flex items-center space-x-3">
{/* Адрес ячейки */}
<div className="flex min-w-[45px] items-center justify-center rounded border bg-muted px-2 py-1">
<span className="text-sm font-medium text-muted-foreground">
{selectedCell
? `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}`
: 'A1'}
</span>
</div>
{/* Строка формул */}
<div className="flex flex-1 items-center space-x-2">
<div className="flex items-center text-muted-foreground">
<Sigma className="h-5 w-5" />
</div>
<input
ref={formulaBarRef}
type="text"
className={`text-m flex-1 rounded border border-input bg-background px-2 py-0.5 outline-none ring-offset-background transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus:outline-none disabled:cursor-not-allowed disabled:opacity-50 ${isCalculating ? 'animate-pulse' : ''}`}
placeholder="Введите формулу или значение..."
value={formulaBarValue}
onChange={e => onValueChange(e.target.value)}
onFocus={onFocus}
onBlur={e => {
const relatedTarget = e.relatedTarget as HTMLElement
if (
relatedTarget &&
relatedTarget.closest('input[type="text"]')
) {
return
}
onBlur(e)
}}
onKeyDown={onKeyDown}
onMouseDown={e => {
// Если поле не в режиме редактирования, нужно запустить редактирование
// но сохранить позицию курсора
if (!isEditing) {
// Вычисляем позицию клика вручную
const input = e.currentTarget
const rect = input.getBoundingClientRect()
const clickX =
e.clientX -
rect.left -
parseInt(getComputedStyle(input).paddingLeft)
// Создаем временный range для определения позиции
const tempSpan = document.createElement('span')
tempSpan.textContent = input.value
tempSpan.style.cssText = getComputedStyle(input).cssText
tempSpan.style.position = 'absolute'
tempSpan.style.visibility = 'hidden'
tempSpan.style.whiteSpace = 'pre'
document.body.appendChild(tempSpan)
let position = 0
let bestDistance = Infinity
for (let i = 0; i <= input.value.length; i++) {
tempSpan.textContent = input.value.substring(0, i)
const distance = Math.abs(tempSpan.offsetWidth - clickX)
if (distance < bestDistance) {
bestDistance = distance
position = i
}
}
document.body.removeChild(tempSpan)
// Сохраняем вычисленную позицию в data-атрибуте
input.dataset.clickPosition = position.toString()
// Запускаем редактирование
onFocus()
}
}}
/>
</div>
{/* Правая группа: индикаторы и действия */}
<div className="flex items-center gap-2">
{/* Индикатор процесса */}
{(isCalculating || isSaving || isLoading) && (
<div className="flex items-center text-muted-foreground">
<svg
className="h-2.5 w-2.5 animate-spin"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<span className="ml-1 text-xs">
{isSaving
? 'Сохранение...'
: isLoading
? 'Загрузка...'
: 'Расчет...'}
</span>
</div>
)}
{/* Ошибка сохранения */}
{lastSaveError && (
<div className="flex items-center gap-1 rounded-md bg-destructive/10 px-2 py-1">
<svg
className="h-3 w-3 text-destructive"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z"
/>
</svg>
<span
className="text-xs text-destructive"
title={lastSaveError}
>
Ошибка
</span>
{onClearError && (
<button
onClick={onClearError}
className="ml-0.5 text-destructive/60 hover:text-destructive"
>
<svg
className="h-2 w-2"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
)}
</div>
)}
{/* ExcelUploadPanel */}
{onExcelFileChange && (
<ExcelUploadPanel
fileName={excelFileName}
isLoading={isExcelLoading}
onUploadClick={() => {}}
onFileChange={onExcelFileChange}
/>
)}
{/* Кнопка сохранения */}
{onManualSave && (
<button
onClick={onManualSave}
disabled={isSaving}
className={`flex items-center gap-1 rounded px-1.5 py-0.5 text-xs transition-colors ${
hasUnsavedChanges
? 'bg-primary text-primary-foreground hover:bg-primary/90'
: lastSaveError
? 'bg-destructive text-destructive-foreground hover:bg-destructive/90'
: 'bg-muted text-muted-foreground'
} disabled:opacity-50`}
title={
lastSaveError
? 'Повторить сохранение'
: hasUnsavedChanges
? 'Есть несохраненные изменения'
: 'Все изменения сохранены'
}
>
<Save className="h-3 w-3" />
<span className="hidden sm:inline">
{lastSaveError
? 'Повторить'
: hasUnsavedChanges
? 'Сохранить'
: 'Сохранено'}
</span>
</button>
)}
</div>
</div>
</div>
)
}
)