копирование
This commit is contained in:
@@ -101,6 +101,13 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Состояние для копирования/вставки
|
||||||
|
const [copiedCells, setCopiedCells] = useState<{
|
||||||
|
data: { row: number; col: number; value: string; rawValue: string }[]
|
||||||
|
sourceSheet: SheetType
|
||||||
|
isCut: boolean
|
||||||
|
} | null>(null)
|
||||||
|
|
||||||
// React Query хуки для оптимизации API запросов
|
// React Query хуки для оптимизации API запросов
|
||||||
const { data: latestFile, isLoading: isLoadingLatestFile } =
|
const { data: latestFile, isLoading: isLoadingLatestFile } =
|
||||||
useLatestFileForTemplate(templateId)
|
useLatestFileForTemplate(templateId)
|
||||||
@@ -354,6 +361,24 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
}
|
}
|
||||||
}, [isEditing, editingValue, updateCellHighlights])
|
}, [isEditing, editingValue, updateCellHighlights])
|
||||||
|
|
||||||
|
// Сбрасываем скопированные ячейки при смене активного листа
|
||||||
|
useEffect(() => {
|
||||||
|
if (copiedCells) {
|
||||||
|
setCopiedCells(null)
|
||||||
|
}
|
||||||
|
}, [activeSheet])
|
||||||
|
|
||||||
|
// Автоматический сброс скопированных ячеек через 30 секунд
|
||||||
|
useEffect(() => {
|
||||||
|
if (!copiedCells) return
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
setCopiedCells(null)
|
||||||
|
}, 30000) // 30 секунд
|
||||||
|
|
||||||
|
return () => clearTimeout(timeout)
|
||||||
|
}, [copiedCells])
|
||||||
|
|
||||||
const startEditing = useCallback(() => {
|
const startEditing = useCallback(() => {
|
||||||
if (!selectedCell) return
|
if (!selectedCell) return
|
||||||
// Сбрасываем множественное выделение при начале редактирования
|
// Сбрасываем множественное выделение при начале редактирования
|
||||||
@@ -427,6 +452,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
stopEditing(true)
|
stopEditing(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сбрасываем скопированные ячейки при навигации
|
||||||
|
if (copiedCells) {
|
||||||
|
setCopiedCells(null)
|
||||||
|
}
|
||||||
|
|
||||||
const newRow = Math.max(
|
const newRow = Math.max(
|
||||||
0,
|
0,
|
||||||
Math.min(ROW_COUNT - 1, selectedCell.row + deltaRow)
|
Math.min(ROW_COUNT - 1, selectedCell.row + deltaRow)
|
||||||
@@ -440,7 +470,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
// Сбрасываем множественное выделение при навигации
|
// Сбрасываем множественное выделение при навигации
|
||||||
setMultiSelection([])
|
setMultiSelection([])
|
||||||
},
|
},
|
||||||
[selectedCell, isEditing, stopEditing]
|
[selectedCell, isEditing, stopEditing, copiedCells]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Функция для очистки множества выделенных ячеек
|
// Функция для очистки множества выделенных ячеек
|
||||||
@@ -460,6 +490,152 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
setMultiSelection([])
|
setMultiSelection([])
|
||||||
}, [multiSelection, activeSheet, multiSheetEngine, updateRevision])
|
}, [multiSelection, activeSheet, multiSheetEngine, updateRevision])
|
||||||
|
|
||||||
|
// Функция копирования выделенных ячеек
|
||||||
|
const handleCopy = useCallback(
|
||||||
|
async (isCut = false) => {
|
||||||
|
if (!selectedCell && multiSelection.length === 0) return
|
||||||
|
|
||||||
|
const sheetName = SHEET_NAMES[activeSheet]
|
||||||
|
let cellsToCopy: { row: number; col: number }[] = []
|
||||||
|
|
||||||
|
// Определяем какие ячейки копировать
|
||||||
|
if (multiSelection.length > 0) {
|
||||||
|
cellsToCopy = getAllCellsInSelections(multiSelection)
|
||||||
|
} else if (selectedCell) {
|
||||||
|
cellsToCopy = [selectedCell]
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cellsToCopy.length === 0) return
|
||||||
|
|
||||||
|
// Собираем данные ячеек
|
||||||
|
const copiedData = cellsToCopy.map(({ row, col }) => ({
|
||||||
|
row,
|
||||||
|
col,
|
||||||
|
value: String(multiSheetEngine.getCellValue(sheetName, row, col) || ''),
|
||||||
|
rawValue: String(
|
||||||
|
multiSheetEngine.getCellRawValue(sheetName, row, col) || ''
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Сохраняем в state
|
||||||
|
setCopiedCells({
|
||||||
|
data: copiedData,
|
||||||
|
sourceSheet: activeSheet,
|
||||||
|
isCut,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Подготавливаем данные для буфера обмена в формате TSV (Tab-Separated Values)
|
||||||
|
// Находим границы выделения
|
||||||
|
const minRow = Math.min(...cellsToCopy.map(c => c.row))
|
||||||
|
const maxRow = Math.max(...cellsToCopy.map(c => c.row))
|
||||||
|
const minCol = Math.min(...cellsToCopy.map(c => c.col))
|
||||||
|
const maxCol = Math.max(...cellsToCopy.map(c => c.col))
|
||||||
|
|
||||||
|
// Создаем двумерный массив для TSV
|
||||||
|
const rows: string[][] = []
|
||||||
|
for (let row = minRow; row <= maxRow; row++) {
|
||||||
|
const rowData: string[] = []
|
||||||
|
for (let col = minCol; col <= maxCol; col++) {
|
||||||
|
const cellData = copiedData.find(c => c.row === row && c.col === col)
|
||||||
|
rowData.push(cellData ? String(cellData.value) : '')
|
||||||
|
}
|
||||||
|
rows.push(rowData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Конвертируем в TSV строку
|
||||||
|
const tsvData = rows.map(row => row.join('\t')).join('\n')
|
||||||
|
|
||||||
|
// Копируем в буфер обмена
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(tsvData)
|
||||||
|
|
||||||
|
// Если это вырезание, очищаем ячейки
|
||||||
|
if (isCut) {
|
||||||
|
cellsToCopy.forEach(({ row, col }) => {
|
||||||
|
multiSheetEngine.setCellValueWithoutRecalc(sheetName, row, col, '')
|
||||||
|
})
|
||||||
|
setUserEdited(true)
|
||||||
|
updateRevision()
|
||||||
|
multiSheetEngine.debouncedRecalc()
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при копировании в буфер обмена:', error)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
selectedCell,
|
||||||
|
multiSelection,
|
||||||
|
activeSheet,
|
||||||
|
multiSheetEngine,
|
||||||
|
updateRevision,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Функция вставки из буфера обмена
|
||||||
|
const handlePaste = useCallback(async () => {
|
||||||
|
if (!selectedCell) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const clipboardText = await navigator.clipboard.readText()
|
||||||
|
|
||||||
|
if (!clipboardText.trim()) return
|
||||||
|
|
||||||
|
const sheetName = SHEET_NAMES[activeSheet]
|
||||||
|
const startRow = selectedCell.row
|
||||||
|
const startCol = selectedCell.col
|
||||||
|
|
||||||
|
// Парсим TSV/CSV данные
|
||||||
|
const rows = clipboardText.split('\n').filter(row => row !== '')
|
||||||
|
const data: string[][] = rows.map(row => {
|
||||||
|
// Поддерживаем как табуляции (TSV), так и запятые (CSV)
|
||||||
|
if (row.includes('\t')) {
|
||||||
|
return row.split('\t')
|
||||||
|
} else {
|
||||||
|
// Простой парсинг CSV (без учета кавычек с запятыми внутри)
|
||||||
|
return row.split(',')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (data.length === 0) return
|
||||||
|
|
||||||
|
// Вставляем данные
|
||||||
|
let hasChanges = false
|
||||||
|
for (let rowIndex = 0; rowIndex < data.length; rowIndex++) {
|
||||||
|
for (let colIndex = 0; colIndex < data[rowIndex].length; colIndex++) {
|
||||||
|
const targetRow = startRow + rowIndex
|
||||||
|
const targetCol = startCol + colIndex
|
||||||
|
|
||||||
|
// Проверяем границы
|
||||||
|
if (targetRow >= ROW_COUNT || targetCol >= COL_COUNT) continue
|
||||||
|
|
||||||
|
const cellValue = data[rowIndex][colIndex]
|
||||||
|
|
||||||
|
// Устанавливаем значение ячейки
|
||||||
|
multiSheetEngine.setCellValueWithoutRecalc(
|
||||||
|
sheetName,
|
||||||
|
targetRow,
|
||||||
|
targetCol,
|
||||||
|
cellValue
|
||||||
|
)
|
||||||
|
hasChanges = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChanges) {
|
||||||
|
setUserEdited(true)
|
||||||
|
updateRevision()
|
||||||
|
multiSheetEngine.debouncedRecalc()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Очищаем состояние копирования если это была операция вырезания
|
||||||
|
if (copiedCells?.isCut) {
|
||||||
|
setCopiedCells(null)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Ошибка при вставке из буфера обмена:', error)
|
||||||
|
}
|
||||||
|
}, [selectedCell, activeSheet, multiSheetEngine, updateRevision, copiedCells])
|
||||||
|
|
||||||
const handleKeyDown = useCallback(
|
const handleKeyDown = useCallback(
|
||||||
(e: React.KeyboardEvent) => {
|
(e: React.KeyboardEvent) => {
|
||||||
if (isEditing) return
|
if (isEditing) return
|
||||||
@@ -519,6 +695,27 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ctrl+C - копировать
|
||||||
|
if (e.key === 'c' && (e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault()
|
||||||
|
handleCopy(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+X - вырезать
|
||||||
|
if (e.key === 'x' && (e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault()
|
||||||
|
handleCopy(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ctrl+V - вставить
|
||||||
|
if (e.key === 'v' && (e.ctrlKey || e.metaKey)) {
|
||||||
|
e.preventDefault()
|
||||||
|
handlePaste()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (keyHandlers[e.key]) {
|
if (keyHandlers[e.key]) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
keyHandlers[e.key]()
|
keyHandlers[e.key]()
|
||||||
@@ -538,6 +735,8 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
handleClearCell,
|
handleClearCell,
|
||||||
multiSelection,
|
multiSelection,
|
||||||
handleClearMultipleCells,
|
handleClearMultipleCells,
|
||||||
|
handleCopy,
|
||||||
|
handlePaste,
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -547,6 +746,11 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
stopEditing(true)
|
stopEditing(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Сбрасываем скопированные ячейки при клике на новую ячейку
|
||||||
|
if (copiedCells) {
|
||||||
|
setCopiedCells(null)
|
||||||
|
}
|
||||||
|
|
||||||
// Если удерживается Ctrl (или Cmd на Mac), добавляем к множественному выделению
|
// Если удерживается Ctrl (или Cmd на Mac), добавляем к множественному выделению
|
||||||
if (event && (event.ctrlKey || event.metaKey)) {
|
if (event && (event.ctrlKey || event.metaKey)) {
|
||||||
const newSelection = createSelectionFromRange(row, col, row, col)
|
const newSelection = createSelectionFromRange(row, col, row, col)
|
||||||
@@ -572,7 +776,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
setSelectedCell({ row, col })
|
setSelectedCell({ row, col })
|
||||||
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
||||||
},
|
},
|
||||||
[isEditing, stopEditing, activeSheet, selectedCell]
|
[isEditing, stopEditing, activeSheet, selectedCell, copiedCells]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Новый обработчик для начала drag selection
|
// Новый обработчик для начала drag selection
|
||||||
@@ -584,12 +788,17 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
// Если удерживаются модификаторы, не начинаем drag
|
// Если удерживаются модификаторы, не начинаем drag
|
||||||
if (event.ctrlKey || event.metaKey || event.shiftKey) return
|
if (event.ctrlKey || event.metaKey || event.shiftKey) return
|
||||||
|
|
||||||
|
// Сбрасываем скопированные ячейки при drag selection
|
||||||
|
if (copiedCells) {
|
||||||
|
setCopiedCells(null)
|
||||||
|
}
|
||||||
|
|
||||||
setIsDragging(true)
|
setIsDragging(true)
|
||||||
setDragStart({ row, col })
|
setDragStart({ row, col })
|
||||||
setMultiSelection([])
|
setMultiSelection([])
|
||||||
setSelectedCell({ row, col })
|
setSelectedCell({ row, col })
|
||||||
},
|
},
|
||||||
[]
|
[copiedCells]
|
||||||
)
|
)
|
||||||
|
|
||||||
// Обработчик для drag selection
|
// Обработчик для drag selection
|
||||||
@@ -983,6 +1192,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
|||||||
onGridScroll={onGridScroll}
|
onGridScroll={onGridScroll}
|
||||||
handleColumnResize={handleColumnResize}
|
handleColumnResize={handleColumnResize}
|
||||||
mergedCells={mergedCells}
|
mergedCells={mergedCells}
|
||||||
|
copiedCells={copiedCells}
|
||||||
/>
|
/>
|
||||||
{index === 0 && (
|
{index === 0 && (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ export const Cell = memo(
|
|||||||
style, // Добавляем style для react-window
|
style, // Добавляем style для react-window
|
||||||
mergedCellInfo,
|
mergedCellInfo,
|
||||||
highlightColor, // Цвет подсветки для формул
|
highlightColor, // Цвет подсветки для формул
|
||||||
|
isCopied, // Является ли ячейка скопированной
|
||||||
|
isCut, // Является ли ячейка вырезанной
|
||||||
}: CellProps) => {
|
}: CellProps) => {
|
||||||
// Если ячейка объединена, но не является основной, не рендерим её
|
// Если ячейка объединена, но не является основной, не рендерим её
|
||||||
if (mergedCellInfo?.isMerged && !mergedCellInfo.isMainCell) {
|
if (mergedCellInfo?.isMerged && !mergedCellInfo.isMainCell) {
|
||||||
@@ -67,6 +69,17 @@ export const Cell = memo(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Стили для скопированных/вырезанных ячеек
|
||||||
|
if (isCopied || isCut) {
|
||||||
|
cellStyle = {
|
||||||
|
...cellStyle,
|
||||||
|
border: '2px dashed hsl(var(--primary))',
|
||||||
|
backgroundColor: isCut
|
||||||
|
? 'rgba(239, 68, 68, 0.1)'
|
||||||
|
: 'rgba(34, 197, 94, 0.1)', // красноватый для вырезанных, зеленоватый для скопированных
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Отладка стилей для измененных ячеек
|
// Отладка стилей для измененных ячеек
|
||||||
if (isModified) {
|
if (isModified) {
|
||||||
// console.log(`🎨 Стили для измененной ячейки ${rowIndex},${colIndex}:`, //{
|
// console.log(`🎨 Стили для измененной ячейки ${rowIndex},${colIndex}:`, //{
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export const CellRenderer = ({
|
|||||||
cachedData,
|
cachedData,
|
||||||
mergedCells,
|
mergedCells,
|
||||||
highlightedCells,
|
highlightedCells,
|
||||||
|
copiedCells,
|
||||||
handleCellClick,
|
handleCellClick,
|
||||||
handleCellDoubleClick,
|
handleCellDoubleClick,
|
||||||
handleCellChange,
|
handleCellChange,
|
||||||
@@ -121,6 +122,15 @@ export const CellRenderer = ({
|
|||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
|
// Проверяем, является ли ячейка скопированной
|
||||||
|
const isCopied =
|
||||||
|
copiedCells &&
|
||||||
|
copiedCells.sourceSheet === sheetType &&
|
||||||
|
copiedCells.data.some(
|
||||||
|
cell => cell.row === rowIndex && cell.col === columnIndex
|
||||||
|
)
|
||||||
|
const isCut = isCopied && copiedCells?.isCut
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Cell
|
<Cell
|
||||||
style={style}
|
style={style}
|
||||||
@@ -155,6 +165,8 @@ export const CellRenderer = ({
|
|||||||
activeCellRef={activeCellInputRef}
|
activeCellRef={activeCellInputRef}
|
||||||
mergedCellInfo={mergedCellProps}
|
mergedCellInfo={mergedCellProps}
|
||||||
highlightColor={highlightColor}
|
highlightColor={highlightColor}
|
||||||
|
isCopied={isCopied}
|
||||||
|
isCut={isCut}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export const Spreadsheet = ({
|
|||||||
onGridScroll,
|
onGridScroll,
|
||||||
handleColumnResize,
|
handleColumnResize,
|
||||||
mergedCells,
|
mergedCells,
|
||||||
|
copiedCells,
|
||||||
}: SpreadsheetProps) => {
|
}: SpreadsheetProps) => {
|
||||||
const widths = columnWidths
|
const widths = columnWidths
|
||||||
const cachedData = getCachedCellData(sheetType)
|
const cachedData = getCachedCellData(sheetType)
|
||||||
@@ -53,6 +54,7 @@ export const Spreadsheet = ({
|
|||||||
cachedData,
|
cachedData,
|
||||||
mergedCells,
|
mergedCells,
|
||||||
highlightedCells,
|
highlightedCells,
|
||||||
|
copiedCells,
|
||||||
multiSelectionActive:
|
multiSelectionActive:
|
||||||
activeSheet === sheetType && multiSelection.length > 0,
|
activeSheet === sheetType && multiSelection.length > 0,
|
||||||
handleCellClick,
|
handleCellClick,
|
||||||
|
|||||||
@@ -98,6 +98,8 @@ export interface CellProps {
|
|||||||
rowSpan?: number
|
rowSpan?: number
|
||||||
colSpan?: number
|
colSpan?: number
|
||||||
}
|
}
|
||||||
|
isCopied?: boolean // Является ли ячейка скопированной
|
||||||
|
isCut?: boolean // Является ли ячейка вырезанной
|
||||||
highlightColor?: string // Цвет подсветки для формул
|
highlightColor?: string // Цвет подсветки для формул
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,6 +155,11 @@ export interface CellRendererData {
|
|||||||
stopEditing: (save: boolean) => void
|
stopEditing: (save: boolean) => void
|
||||||
setActiveSheet: (type: SheetType) => void
|
setActiveSheet: (type: SheetType) => void
|
||||||
activeCellInputRef: React.RefObject<HTMLInputElement | null>
|
activeCellInputRef: React.RefObject<HTMLInputElement | null>
|
||||||
|
copiedCells?: {
|
||||||
|
data: { row: number; col: number; value: string; rawValue: string }[]
|
||||||
|
sourceSheet: SheetType
|
||||||
|
isCut: boolean
|
||||||
|
} | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CellRendererProps {
|
export interface CellRendererProps {
|
||||||
@@ -211,6 +218,11 @@ export interface SpreadsheetProps {
|
|||||||
newWidth: number
|
newWidth: number
|
||||||
) => void
|
) => void
|
||||||
mergedCells?: MergedCell[] // Добавляем поддержку объединенных ячеек
|
mergedCells?: MergedCell[] // Добавляем поддержку объединенных ячеек
|
||||||
|
copiedCells?: {
|
||||||
|
data: { row: number; col: number; value: string; rawValue: string }[]
|
||||||
|
sourceSheet: SheetType
|
||||||
|
isCut: boolean
|
||||||
|
} | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MergedCellInfo {
|
export interface MergedCellInfo {
|
||||||
|
|||||||
@@ -83,16 +83,6 @@ export const TemplateManager: React.FC<TemplateManagerProps> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// const handleTemplateUpdate = async (updatedTemplate: Template) => {
|
|
||||||
// try {
|
|
||||||
// await updateTemplate(updatedTemplate)
|
|
||||||
// setSelectedTemplate(updatedTemplate)
|
|
||||||
// } catch (err) {
|
|
||||||
// console.error('Error updating template:', err)
|
|
||||||
// alert('Ошибка при обновлении шаблона. Попробуйте еще раз.')
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
const handleDeleteTemplates = () => {
|
const handleDeleteTemplates = () => {
|
||||||
templatesToDelete.forEach(t => deleteTemplate(t.id))
|
templatesToDelete.forEach(t => deleteTemplate(t.id))
|
||||||
setTemplatesToDelete([])
|
setTemplatesToDelete([])
|
||||||
|
|||||||
Reference in New Issue
Block a user