UNPKG

react-konva-grid

Version:

Declarative React Canvas Grid primitive for Data table, Pivot table, Excel Worksheets

202 lines (185 loc) 5.52 kB
import React, { useCallback, useEffect, useRef } from "react"; import { CellInterface, GridRef, SelectionArea } from "../Grid"; import { selectionFromActiveCell, prepareClipboardData } from "./../helpers"; import { MimeType } from "../types"; export interface CopyProps { /** * Selection bounds */ selections: SelectionArea[]; /** * Active cell */ activeCell?: CellInterface | null; /** * Value getter of a cell */ getValue: (cell: CellInterface) => any; /** * Grid reference to access grid methods */ gridRef: React.MutableRefObject<GridRef>; /** * Callback when a paste is executed */ onPaste?: ( rows: (string | null)[][], activeCell: CellInterface | null ) => void; /** * When user tries to cut a selection */ onCut: (selection: SelectionArea) => void; } export interface CopyResults { copy: () => void; paste: () => void; } /** * Copy paste hook * Usage * * useCopyPaste ({ * onPaste: (text) => { * } * }) */ const useCopyPaste = ({ selections = [], activeCell = null, getValue, gridRef, onPaste, onCut, }: CopyProps): CopyResults => { const selectionRef = useRef({ selections, activeCell, getValue }); const cutSelections = useRef<SelectionArea | null>(null); /* Keep selections and activeCell upto date */ useEffect(() => { selectionRef.current = { selections, activeCell, getValue }; }); const currentSelections = () => { const sel = selectionRef.current.selections.length ? selectionRef.current.selections : selectionFromActiveCell(selectionRef.current.activeCell); return sel[sel.length - 1]; }; useEffect(() => { if (!gridRef.current) return; document.addEventListener("copy", (e) => { if (gridRef.current?.container !== document.activeElement) return; handleCopy(e); }); document.addEventListener("paste", (e) => { if (gridRef.current?.container !== document.activeElement) return; handlePaste(e); }); document.addEventListener("cut", (e) => { if (gridRef.current?.container !== document.activeElement) return; cutSelections.current = currentSelections(); handleCopy(e); }); }, []); const handleCopy = useCallback( (e: ClipboardEvent) => { /* Only copy the last selection */ const { bounds } = currentSelections(); const { top, left, right, bottom } = bounds; const rows = []; for (let i = top; i <= bottom; i++) { const row = []; for (let j = left; j <= right; j++) { const value = selectionRef.current.getValue({ rowIndex: i, columnIndex: j }) ?? ""; row.push(value); } rows.push(row); } const [html, csv] = prepareClipboardData(rows); e.clipboardData?.setData(MimeType.html, html); e.clipboardData?.setData(MimeType.plain, csv); e.clipboardData?.setData(MimeType.csv, csv); e.clipboardData?.setData(MimeType.json, JSON.stringify(rows)); e.preventDefault(); }, [currentSelections] ); const handlePaste = (e: ClipboardEvent) => { const items = e.clipboardData?.items; if (!items) return; const mimeTypes = [MimeType.html, MimeType.csv, MimeType.plain]; let type; let value; for (type of mimeTypes) { value = e.clipboardData?.getData(type); if (value) break; } if (!type || !value) { console.warn("No clipboard data to paste"); return; } const rows = []; if (/^text\/html/.test(type)) { const domparser = new DOMParser(); const doc = domparser.parseFromString(value, type as SupportedType); const supportedNodes = "table, p, h1, h2, h3, h4, h5, h6"; const nodes = doc.querySelectorAll(supportedNodes); for (let i = 0; i < nodes.length; i++) { const node = nodes[i]; if (node.nodeName === "TABLE") { const tableRows = doc.querySelectorAll("tr"); for (const tableRow of tableRows) { const row = []; const cells = tableRow.querySelectorAll("td"); for (const cell of cells) { row.push(cell.textContent); } rows.push(row); } } else { // Single nodes rows.push([node.textContent]); } } } else { const values = value.split("\n"); for (const val of values) { const row = []; for (const cell of val.split(",")) { row.push(cell.replace(/^\"|\"$/gi, "")); } rows.push(row); } } onPaste && onPaste(rows, selectionRef.current.activeCell); /* Clear all values in cut */ if (cutSelections.current) { onCut && onCut(cutSelections.current); cutSelections.current = null; } }; /** * User is trying to copy from outisde the app */ const handleProgramaticCopy = useCallback(() => { gridRef.current.focus(); document.execCommand("copy"); }, []); /** * User is trying to paste from outisde the app */ const handleProgramaticPaste = useCallback(async () => { gridRef.current.focus(); const text = await navigator.clipboard.readText(); const clipboardData = new DataTransfer(); clipboardData.setData(MimeType.plain, text); const event = new ClipboardEvent("paste", { clipboardData }); handlePaste(event); }, []); return { copy: handleProgramaticCopy, paste: handleProgramaticPaste, }; }; export default useCopyPaste;