5.0
Implementing core features
Copy Cut Paste

Copy Cut Paste

Support for cut, copy, and paste is enabled by default. You can disable these features by passing disableCut, disableCopy, and disablePaste props to the ReactGrid component, respectively. If you'd like to implement your own logic, the following props are available:

onCut?: (event: React.ClipboardEvent<HTMLDivElement>, cellsRange: NumericalRange, cellsLookup: CellsLookup) => boolean;
onCopy?: (event: React.ClipboardEvent<HTMLDivElement>, cellsRange: NumericalRange, cellsLookup: CellsLookup) => boolean;
onPaste?: (event: React.ClipboardEvent<HTMLDivElement>, cellsRange: NumericalRange, cellsLookup: CellsLookup) => boolean;

Parameters

NameTypeDescription
eventReact.ClipboardEvent<HTMLDivElement>The clipboard event triggered by the cut, copy, or paste action.
cellsRangeNumericalRangeThe range of cells involved in the cut, copy, or paste action.
cellsLookupCellsLookupA map object that provides callbacks to get and set cell values. Each key is a string combining row and column indices, and the value is an object with rowIndex, colIndex, onStringValueRequested (callback to get the cell value), and onStringValueReceived (callback to set the cell value).

Return Value

The return value of these handlers determines whether the custom logic should override the default cut, copy, or paste behavior.

Return ValueTypeDescription
truebooleanPrevent the default behavior.
falsebooleanDefault behavior should proceed without being prevented. It is used in case you want to perform additional actions before allowing the default behavior.

Live example


Below is an example with overridden logic for cut, copy, and paste. Additionally, handlePaste utilizes the setSelectedArea method from the useReactGridAPI hook to highlight the new cell area after pasting the data.


ReactGrid

Code


const peopleData = [
  {
    id: "66d61077035753f369ddbb16",
    name: "Jordan Rodriquez",
    age: 30,
    email: "jordanrodriquez@cincyr.com",
    company: "Zaggles",
  },
  {
    id: "66d61077794e7949ab167fd5",
    email: "allysonrios@satiance.com",
    name: "Allyson Rios",
    age: 30,
    company: "Zoxy",
  },
  {
    id: "66d61077dd754e88981ae434",
    name: "Pickett Lucas",
    age: 25,
    email: "pickettlucas@zoxy.com",
    company: "Techade",
  },
  {
    id: "66d61077115e2f8748c334d9",
    name: "Louella David",
    age: 37,
    email: "louelladavid@techade.com",
    company: "Ginkogene",
  },
  {
    id: "66d61077540d53374b427e4b",
    name: "Tricia Greene",
    age: 27,
    email: "triciagreene@ginkogene.com",
    company: "Naxdis",
  },
];

const cellStyles = {
  header: {
    backgroundColor: "#55bc71",
    display: "flex",
    alignItems: "center",
    justifyContent: "center",
    fontWeight: "bold",
  },
};

const getRows = (people: Person[]): Row[] => [
  // header row
  {
    rowIndex: 0,
    height: 40,
  },
  // data rows
  ...people.map((_, i) => ({
    rowIndex: i + 1,
    height: 40,
  })),
];

const getColumns = (): Column[] => [
  { colIndex: 0, width: 220 },
  { colIndex: 1, width: 220 },
  { colIndex: 2, width: 220 },
  { colIndex: 3, width: 220 },
];

type UpdatePerson = <T>(id: string, key: string, newValue: T) => void;

const generateCells = (people: Person[], updatePerson: UpdatePerson): Cell[] => {
  const generateHeaderCells = () => {
    const titles = ["Name", "Age", "Email", "Company"];

    return titles.map((title, colIndex) => ({
      rowIndex: 0,
      colIndex,
      Template: NonEditableCell,
      props: {
        value: title,
        style: cellStyles.header,
      },
    }));
  };

  const generateRowCells = (rowIndex: number, person: Person): Cell[] => {
    const { id, name, age, email, company } = person;

    return [
      {
        rowIndex,
        colIndex: 0,
        Template: TextCell,
        props: {
          text: name,
          onTextChanged: (newText: string) => updatePerson(id, "name", newText),
        },
      },
      {
        rowIndex,
        colIndex: 1,
        Template: NumberCell,
        props: {
          value: age,
          onValueChanged: (newValue: number) => updatePerson(id, "age", newValue),
        },
      },
      {
        rowIndex,
        colIndex: 2,
        Template: TextCell,
        props: {
          text: email,
          onTextChanged: (newText: string) => updatePerson(id, "email", newText),
        },
      },
      {
        rowIndex,
        colIndex: 3,
        Template: TextCell,
        props: {
          text: company,
          onTextChanged: (newText: string) => updatePerson(id, "company", newText),
        },
      },
    ];
  };

  const headerCells = generateHeaderCells();
  const rowCells = people.flatMap((person, idx) => generateRowCells(idx + 1, person));
  return [...headerCells, ...rowCells];
};

const ReactGridExample = () => {
  const [people, setPeople] = useState(peopleData);

  const updatePerson = (id, key, newValue) => {
    setPeople((prev) => {
      return prev.map((p) => (p.id === id ? { ...p, [key]: newValue } : p));
    });
  };

  const rows = getRows(people);
  const columns = getColumns();
  const cells = generateCells(people, updatePerson);

  const gridAPI = useReactGridAPI("cut-copy-paste-example");

  return (
    <div>
      <ReactGrid
        id="cut-copy-paste-example"
        rows={rows}
        columns={columns}
        cells={cells}
        onCut={(event, cellsRange, cellsLookup) => handleCut(event, cellsRange, cellsLookup)}
        onCopy={(event, cellsRange, cellsLookup) => handleCopy(event, cellsRange, cellsLookup)}
        onPaste={(event, cellsRange, cellsLookup) => handlePaste(event, cellsRange, cellsLookup, gridAPI)}
      />
    </div>
  );
};


const handleCut = (
  event: React.ClipboardEvent<HTMLDivElement>,
  cellsRange: NumericalRange,
  cellsLookup: CellsLookup
) => {
  const { startRowIdx, endRowIdx, startColIdx, endColIdx } = cellsRange;
  const cellsLookupCallbacks: CellsLookupCallbacks[] = [];

  for (let rowIdx = startRowIdx; rowIdx < endRowIdx; rowIdx++) {
    for (let colIdx = startColIdx; colIdx < endColIdx; colIdx++) {
      const element = cellsLookup.get(`${rowIdx} ${colIdx}`);
      if (element) {
        cellsLookupCallbacks.push(element);
      }
    }
  }

  const values = cellsLookupCallbacks
    .filter((element) => element && Object.keys(element).length > 0)
    .map((element) => element.onStringValueRequested());

  cellsLookupCallbacks.forEach((element) => element && element.onStringValueReceived?.(""));

  const htmlData = `
  <table>
    ${Array.from(
      { length: cellsRange.endRowIdx - cellsRange.startRowIdx },
      (_, rowIndex) => `
      <tr>
        ${Array.from({ length: cellsRange.endColIdx - cellsRange.startColIdx }, (_, colIndex) => {
          const cell = cellsLookup.get(`${cellsRange.startRowIdx + rowIndex} ${cellsRange.startColIdx + colIndex}`);
          const value = cell?.onStringValueRequested?.() || "";
          return `<td>${value}</td>`;
        }).join("")}
      </tr>
    `
    ).join("")}
  </table>
`;

  event.clipboardData.setData("text/html", htmlData);
  event.clipboardData.setData("text/plain", values.join("	"));

  // Prevent the default cut behavior
  return true;
};

const handleCopy = (
  event: React.ClipboardEvent<HTMLDivElement>,
  cellsRange: NumericalRange,
  cellsLookup: CellsLookup
) => {
  const { startRowIdx, endRowIdx, startColIdx, endColIdx } = cellsRange;
  const cellsLookupCallbacks: CellsLookupCallbacks[] = [];

  for (let rowIdx = startRowIdx; rowIdx < endRowIdx; rowIdx++) {
    for (let colIdx = startColIdx; colIdx < endColIdx; colIdx++) {
      const element = cellsLookup.get(`${rowIdx} ${colIdx}`);
      if (element) {
        cellsLookupCallbacks.push(element);
      }
    }
  }

  const values = cellsLookupCallbacks
    .filter((element) => element && Object.keys(element).length > 0)
    .map((element) => element.onStringValueRequested());

  const htmlData = `
  <table>
    ${Array.from(
      { length: cellsRange.endRowIdx - cellsRange.startRowIdx },
      (_, rowIndex) => `
      <tr>
        ${Array.from({ length: cellsRange.endColIdx - cellsRange.startColIdx }, (_, colIndex) => {
          const cell = cellsLookup.get(`${cellsRange.startRowIdx + rowIndex} ${cellsRange.startColIdx + colIndex}`);
          const value = cell?.onStringValueRequested?.() || "";
          return `<td>${value}</td>`;
        }).join("")}
      </tr>
    `
    ).join("")}
  </table>
`;

  event.clipboardData.setData("text/html", htmlData);
  event.clipboardData.setData("text/plain", values.join("	"));

  // Prevent the default copy behavior
  return true;
};

const handlePaste = (
  event: React.ClipboardEvent<HTMLDivElement>,
  cellsRange: NumericalRange,
  cellsLookup: CellsLookup,
  gridAPI?: ReactGridAPI
) => {
  const html = event.clipboardData.getData("text/html");

  const parser = new DOMParser();
  const doc = parser.parseFromString(html, "text/html");

  const rows = doc.querySelectorAll("tr");

  // Prevent the default paste behavior - no action will be performed
    if (rows.length === 0) return true;

  const firstRowCells = rows[0].querySelectorAll("td");

  if (rows.length === 1 && firstRowCells.length === 1) {
    const singleValue = firstRowCells[0].textContent || "";
    for (let rowIndex = cellsRange.startRowIdx; rowIndex < cellsRange.endRowIdx; rowIndex++) {
      for (let colIndex = cellsRange.startColIdx; colIndex < cellsRange.endColIdx; colIndex++) {
        const gridCell = cellsLookup.get(`${rowIndex} ${colIndex}`);
        gridCell?.onStringValueReceived(singleValue);
      }
    }
  } else {
    rows.forEach((row, rowIndex) => {
      const cells = row.querySelectorAll("td");
      cells.forEach((cell, colIndex) => {
        const value = cell.textContent || "";
        const gridCell = cellsLookup.get(`${cellsRange.startRowIdx + rowIndex} ${cellsRange.startColIdx + colIndex}`);
        if (gridCell) {
          gridCell.onStringValueReceived?.(value);
        }
      });
    });
  }

  if (gridAPI) {
    let newSelectedArea;

    // If only one cell was pasted
    if (rows.length === 1 && firstRowCells.length === 1) {
      newSelectedArea = {
        startRowIdx: cellsRange.startRowIdx,
        endRowIdx: cellsRange.endRowIdx,
        startColIdx: cellsRange.startColIdx,
        endColIdx: cellsRange.endColIdx,
      };
    }
    // If multiple cells were pasted
    else {
      const endRowIdx = Math.min(cellsRange.startRowIdx + rows.length, gridAPI.getRows().length);
      const endColIdx = Math.min(
        cellsRange.startColIdx + rows[0].querySelectorAll("td").length,
        gridAPI.getColumns().length
      );

      newSelectedArea = {
        startRowIdx: cellsRange.startRowIdx,
        endRowIdx: endRowIdx,
        startColIdx: cellsRange.startColIdx,
        endColIdx: endColIdx,
      };
    }

    gridAPI.setSelectedArea(newSelectedArea);
  }

  // Prevent the default paste behavior
  return true;
};

render(<ReactGridExample />, document.getElementById("root"));
  
ReactGrid

Preview