217 lines
5.7 KiB
JavaScript
217 lines
5.7 KiB
JavaScript
import React, { memo, useEffect, useRef, useLayoutEffect, useState, useCallback, useMemo } from 'react';
|
||
import { isFormula, extractFormula, calculateFormula, formatNumber } from '../../utils/formulaUtils';
|
||
import { saveToStorage, loadFromStorage } from '../../utils/localStorageUtils';
|
||
import { useRealtime } from '../../contexts/RealtimeContext';
|
||
|
||
const EDITOR_STYLES = {
|
||
position: 'absolute',
|
||
zIndex: 12,
|
||
top: 0,
|
||
};
|
||
|
||
const FORMULA_INDICATOR_STYLES = {
|
||
position: 'absolute',
|
||
top: '-20px',
|
||
left: '4px',
|
||
fontSize: '10px',
|
||
color: '#666',
|
||
background: '#f0f0f0',
|
||
padding: '2px 6px',
|
||
borderRadius: '3px',
|
||
pointerEvents: 'none',
|
||
};
|
||
|
||
const TEXTAREA_STYLES = {
|
||
width: '100%',
|
||
height: '3rem',
|
||
fieldSizing: 'content',
|
||
fontSize: 'inherit',
|
||
};
|
||
|
||
const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, table }) => {
|
||
const { endEditing: contextEndEditing } = useRealtime();
|
||
const storageKey = `formula_${tableId}_${cellId}`;
|
||
|
||
const [state, setState] = useState(() => ({
|
||
currentValue: value,
|
||
displayValue: value,
|
||
isFormulaMode: false,
|
||
}));
|
||
|
||
// Позиция ячейки
|
||
const [position, setPosition] = useState();
|
||
const textareaRef = useRef(null);
|
||
|
||
const refFinished = useRef(false);
|
||
|
||
// Загрузка сохраненной формулы
|
||
useEffect(() => {
|
||
const savedFormula = loadFromStorage(storageKey);
|
||
if (savedFormula && isFormula(savedFormula)) {
|
||
try {
|
||
const formula = extractFormula(savedFormula);
|
||
const result = calculateFormula(formula);
|
||
setState({
|
||
currentValue: savedFormula,
|
||
displayValue: formatNumber(result),
|
||
isFormulaMode: true,
|
||
});
|
||
} catch {
|
||
setState({
|
||
currentValue: savedFormula,
|
||
displayValue: '#ОШИБКА',
|
||
isFormulaMode: true,
|
||
});
|
||
}
|
||
}
|
||
}, [storageKey]);
|
||
|
||
// Вычисление позиции
|
||
useLayoutEffect(() => {
|
||
if (!refCell?.current) return;
|
||
|
||
const td = refCell.current.offsetParent;
|
||
const tr = td?.offsetParent;
|
||
|
||
if (td && tr) {
|
||
setPosition({
|
||
left: td.offsetLeft,
|
||
width: td.offsetWidth,
|
||
translateY: tr.style.transform || '',
|
||
});
|
||
}
|
||
}, [refCell]);
|
||
|
||
// Auto-resize
|
||
useLayoutEffect(() => {
|
||
const el = textareaRef.current;
|
||
if (!el) return;
|
||
|
||
const resize = () => {
|
||
el.style.minHeight = '3rem';
|
||
el.style.height = '3rem';
|
||
el.style.height = el.scrollHeight + 'px';
|
||
el.style.maxHeight = '180px';
|
||
};
|
||
|
||
resize();
|
||
const observer = new ResizeObserver(resize);
|
||
observer.observe(el);
|
||
|
||
return () => observer.disconnect();
|
||
}, [state.currentValue]);
|
||
|
||
const handleSave = useCallback(() => {
|
||
const { currentValue } = state;
|
||
|
||
if (isFormula(currentValue)) {
|
||
saveToStorage(storageKey, currentValue);
|
||
try {
|
||
const formula = extractFormula(currentValue);
|
||
const result = calculateFormula(formula);
|
||
const formattedResult = formatNumber(result);
|
||
setState(prev => ({
|
||
...prev,
|
||
displayValue: formattedResult,
|
||
isFormulaMode: true,
|
||
}));
|
||
onChange?.(formattedResult);
|
||
} catch {
|
||
setState(prev => ({
|
||
...prev,
|
||
displayValue: '#ОШИБКА',
|
||
isFormulaMode: true,
|
||
}));
|
||
onChange?.(currentValue);
|
||
}
|
||
} else {
|
||
localStorage.removeItem(storageKey);
|
||
setState(prev => ({
|
||
...prev,
|
||
displayValue: currentValue,
|
||
isFormulaMode: false,
|
||
}));
|
||
onChange?.(currentValue);
|
||
}
|
||
}, [state, storageKey, onChange]);
|
||
|
||
const finishEditing = useCallback(() => {
|
||
if (refFinished.current) return;
|
||
refFinished.current = true;
|
||
if (!disabled) {
|
||
handleSave();
|
||
}
|
||
const editingCell = table.getState().editingCell;
|
||
if (editingCell) {
|
||
contextEndEditing?.(editingCell.row, editingCell.column);
|
||
}
|
||
}, [disabled, handleSave, table, contextEndEditing]);
|
||
|
||
useEffect(() => {
|
||
const handleClickOutside = (event) => {
|
||
const el = textareaRef.current;
|
||
if (el && !el.contains(event.target)) {
|
||
finishEditing();
|
||
}
|
||
};
|
||
document.addEventListener('mousedown', handleClickOutside);
|
||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||
}, [finishEditing]);
|
||
|
||
const handleChange = useCallback((e) => {
|
||
const value = e.target.value;
|
||
setState(prev => ({
|
||
...prev,
|
||
currentValue: value,
|
||
displayValue: value,
|
||
}));
|
||
}, []);
|
||
|
||
const handleKeyDown = useCallback((e) => {
|
||
if (e.key === 'Enter' && !e.shiftKey) {
|
||
e.preventDefault();
|
||
finishEditing();
|
||
}
|
||
}, [finishEditing]);
|
||
|
||
const editorStyles = useMemo(() => ({
|
||
...EDITOR_STYLES,
|
||
left: position?.left || 0,
|
||
width: position?.width || 0,
|
||
transform: position?.translateY || 0,
|
||
}), [position]);
|
||
|
||
const textareaStyles = useMemo(() => ({
|
||
...TEXTAREA_STYLES,
|
||
backgroundColor: state.isFormulaMode ? '#f8f9fa' : 'white',
|
||
border: state.isFormulaMode ? '2px solid #4a90d9' : undefined,
|
||
}), [state.isFormulaMode]);
|
||
|
||
return (
|
||
<tr>
|
||
<td>
|
||
{position &&
|
||
<div style={editorStyles}>
|
||
{state.isFormulaMode && (
|
||
<div style={FORMULA_INDICATOR_STYLES}>fx</div>
|
||
)}
|
||
<textarea
|
||
ref={textareaRef}
|
||
disabled={disabled}
|
||
style={textareaStyles}
|
||
onChange={handleChange}
|
||
value={state.currentValue}
|
||
onKeyDown={handleKeyDown}
|
||
placeholder={state.isFormulaMode ? 'Введите формулу (начинается с =)' : ''}
|
||
/>
|
||
</div>
|
||
}
|
||
|
||
</td>
|
||
</tr>
|
||
);
|
||
});
|
||
|
||
EditCell.displayName = 'EditCell';
|
||
|
||
export default EditCell; |