import { action, computed, makeObservable, observable } from 'mobx'; import type { CellBorder } from './types'; import type { FiltersMap } from '@wix/bex-core'; import type { EditableTableState } from './EditableTableState'; import { cellKey, parseCellKey, noBorder } from './utils'; export class ClipboardState { copiedCells = new Set(); isCut = false; private _copiedDataString = ''; private _copiedValues = new Map(); private readonly _parent: EditableTableState; constructor(parent: EditableTableState) { this._parent = parent; makeObservable(this, { copiedCells: observable, isCut: observable, hasCopiedCells: computed, copy: action, cut: action, paste: action, clear: action, }); } /** * Copy selected cells. Returns serialized TSV string for clipboard. */ copy(): string { this.clear(); return this._storeCells(); } /** * Cut selected cells. Returns serialized TSV string for clipboard. */ cut(): string { this.clear(); this.isCut = true; return this._storeCells(); } /** * Paste data starting at target position. * If the clipboard text matches what we stored at copy time, uses the rich * typed values instead of deserializing from TSV (avoids lossy round-trip). */ paste(rawData: string) { const { cellValue, cellInteraction } = this._parent; const target = cellInteraction.focusedCell; if (!target) { return; } const targetPos = this._parent._toIndices(target); if (!targetPos) { return; } const isInternal = this._copiedDataString !== '' && this._copiedDataString === rawData; const pastedKeys = this._pasteGrid( rawData, targetPos, isInternal, cellValue, ); if (this.isCut && isInternal) { this._clearCutSources(pastedKeys, cellValue); } this.clear(); } private _pasteGrid( rawData: string, targetPos: { row: number; col: number }, useRichValues: boolean, cellValue: EditableTableState['cellValue'], ): Set { const pastedKeys = new Set(); const richChanges: { rowKey: string; columnId: string; value: any }[] = []; const rawChanges: { rowKey: string; columnId: string; rawValue: string }[] = []; const rows = rawData.split(/\r?\n/).filter((r) => r.length > 0); for (let r = 0; r < rows.length; r++) { const cols = rows[r].split('\t'); for (let c = 0; c < cols.length; c++) { const dest = this._parent._toPosition( targetPos.row + r, targetPos.col + c, ); if (!dest) { continue; } const richValue = useRichValues ? this._copiedValues.get(`${r}:${c}`) : undefined; if (richValue !== undefined) { richChanges.push({ rowKey: dest.rowKey, columnId: dest.columnId, value: richValue, }); } else { rawChanges.push({ rowKey: dest.rowKey, columnId: dest.columnId, rawValue: cols[c], }); } pastedKeys.add(cellKey(dest.rowKey, dest.columnId)); } } if (richChanges.length > 0) { cellValue.applyBulkValues(richChanges); } if (rawChanges.length > 0) { cellValue.applyBulkRawValues(rawChanges); } return pastedKeys; } private _clearCutSources( pastedKeys: Set, cellValue: EditableTableState['cellValue'], ) { const rawChanges: { rowKey: string; columnId: string; rawValue: string; }[] = []; for (const key of this.copiedCells) { if (pastedKeys.has(key)) { continue; } const pos = parseCellKey(key); if (pos) { rawChanges.push({ rowKey: pos.rowKey, columnId: pos.columnId, rawValue: '', }); } } cellValue.applyBulkRawValues(rawChanges); } clear() { this.copiedCells.clear(); this.isCut = false; this._copiedDataString = ''; this._copiedValues.clear(); } isCellCopied(rowKey: string, columnId: string): boolean { return this.copiedCells.has(cellKey(rowKey, columnId)); } getCopiedBorder(rowKey: string, columnId: string): CellBorder { if (!this.isCellCopied(rowKey, columnId)) { return noBorder; } const pos = this._parent._toIndices({ rowKey, columnId }); if (!pos) { return noBorder; } const isNeighborCopied = (row: number, col: number) => { const neighbor = this._parent._toPosition(row, col); return ( neighbor !== null && this.copiedCells.has(cellKey(neighbor.rowKey, neighbor.columnId)) ); }; return { top: !isNeighborCopied(pos.row - 1, pos.col), bottom: !isNeighborCopied(pos.row + 1, pos.col), left: !isNeighborCopied(pos.row, pos.col - 1), right: !isNeighborCopied(pos.row, pos.col + 1), }; } get hasCopiedCells(): boolean { return this.copiedCells.size > 0; } private _storeCells(): string { const cells = this._parent.cellInteraction.selectedCellsList; if (cells.length === 0) { return ''; } const { cellValue } = this._parent; for (const cell of cells) { this.copiedCells.add(cellKey(cell.rowKey, cell.columnId)); } const indices = cells .map((c) => this._parent._toIndices(c)) .filter((idx): idx is { row: number; col: number } => idx !== null); if (indices.length === 0) { return ''; } const minRow = Math.min(...indices.map((i) => i.row)); const maxRow = Math.max(...indices.map((i) => i.row)); const minCol = Math.min(...indices.map((i) => i.col)); const maxCol = Math.max(...indices.map((i) => i.col)); const data: string[][] = []; for (let r = minRow; r <= maxRow; r++) { const row: string[] = []; for (let c = minCol; c <= maxCol; c++) { const pos = this._parent._toPosition(r, c); if (pos && this.copiedCells.has(cellKey(pos.rowKey, pos.columnId))) { const serialized = cellValue.serializeCellValue( pos.rowKey, pos.columnId, ); row.push(serialized); this._copiedValues.set( `${r - minRow}:${c - minCol}`, cellValue.getCellDisplayValue(pos.rowKey, pos.columnId), ); } else { row.push(''); } } data.push(row); } const tsv = data.map((row) => row.join('\t')).join('\n'); this._copiedDataString = tsv; return tsv; } }