import { useEffect } from "react"; import { IPaste } from "../IPaste"; import { usePropsRef } from "../context/PropsContext"; import { selectionContext } from "../context/SelectionContext"; // This triggers the onPaste callback when the user pastes using the keyboard // The logic is a little complex here for a couple of reasons: // - We want to make sure that only one onChange is called per paste to enable // undo to work nicely // - Sometimes pastes affect multiple columns on the same piece of data. This // can happen when pasting across mutliple columns, or when using subRows and // pasting across mutliple rows // Because of this, we need to call the onPastes carefully, and we use the // applyPastes callback to combine the pasted data in the way the user wants. // // There is also complexity because pasting gets repeated in a pattern. // Basically, we tile the copied values as many times as we can across the // pasted selection. We cut off some values if the pasted area is smaller than // the copied area. export const usePaste = () => { const propsRef = usePropsRef(); const selectionRef = selectionContext.useRef(); const setSelection = selectionContext.useSetter(); useEffect(() => { const listener = (e: ClipboardEvent) => { if (!selectionRef.current) { return; } const data = e.clipboardData?.getData("text"); if (!data) { return; } const pastedRows = data.split("\n").map((row) => row.split("\t")); if (pastedRows.length === 0) { return; } // Tile the copied data across the pastes. const repeatX = Math.floor(selectionRef.current.width / pastedRows[0].length) || 1; const repeatY = Math.floor(selectionRef.current.height / pastedRows.length) || 1; // Generate the pasted data, allow the caller to determine how pastes are // combined using applyPastes const pastes: IPaste[] = []; for ( let y = 0; y < Math.min( pastedRows.length * repeatY, propsRef.current.rows.length - selectionRef.current.start.y ); y++ ) { const row = propsRef.current.rows[selectionRef.current.start.y + y]; const paste: IPaste = { row, changers: [] }; for ( let x = 0; x < Math.min( pastedRows[0].length * repeatX, propsRef.current.columnProps.length - selectionRef.current.start.x ); x++ ) { const isReadonly = propsRef.current.columnProps[ selectionRef.current.start.x + x ].isReadonly?.(row) ?? false; if (!isReadonly) { const value = pastedRows[y % pastedRows.length][x % pastedRows[0].length]; const onPaste = propsRef.current.columnProps[selectionRef.current.start.x + x] .onPaste; paste.changers.push((row: TRow) => onPaste(row, value)); } } if (paste.changers.length > 0) { pastes.push(paste); } } const changes = propsRef.current.applyPastes(pastes); propsRef.current.onChange(changes); // Set the selection to be the same size as the pasted data setSelection({ start: selectionRef.current.start, primary: { x: Math.min( selectionRef.current.primary.x, selectionRef.current.start.x + pastedRows[0].length * repeatX ), y: Math.min( selectionRef.current.primary.y, selectionRef.current.start.y + pastedRows.length * repeatY ), }, width: pastedRows[0].length * repeatX, height: pastedRows.length * repeatY, }); }; document.addEventListener("paste", listener); return () => document.removeEventListener("paste", listener); }, []); };