Search code examples
reactjsreact-data-grid

React Data Grid - Adding custom copy paste functionality with multiple tables


Need help figuring out how custom copy / paste functionality can be implemented for multiple React Data Grid tables in a single page. As per below code, copy / paste is being triggered for both the tables.

import { connect } from 'react-redux';
import ReactDataGrid from 'fixed-react-data-grid';

const defaultParsePaste = str => str.split(/\r\n|\n|\r/).map(row => row.split('\t'));

class DataGrid extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      rows: [],
      topLeft: {},
      botRight: {},
    };
    this.columns = [
      { key: 'col1', name: 'Col1', editable: true },
      { key: 'col2', name: 'Col2', editable: true },
      { key: 'col3', name: 'Col3', editable: true },
      { key: 'col4', name: 'Col4', editable: true },
    ];
  }

  componentDidMount() {
    document.addEventListener('copy', this.handleCopy);
    document.addEventListener('paste', this.handlePaste);
  }

  componentWillUnmount() {
    document.removeEventListener('copy', this.handleCopy);
    document.removeEventListener('paste', this.handlePaste);
  }

  rowGetter = i => {
    const { rows } = this.state;
    return rows[i];
  };

  handleCopy = e => {
    e.preventDefault();
    e.stopPropagation();

    const { topLeft, botRight } = this.state;
    // Loop through each row
    const text = range(topLeft.rowIdx, botRight.rowIdx + 1)
      .map(
        // Loop through each column
        rowIdx =>
          this.columns
            .slice(topLeft.colIdx, botRight.colIdx + 1)
            .map(
              // Grab the row values and make a text string
              col => this.rowGetter(rowIdx)[col.key],
            )
            .join('\t'),
      )
      .join('\n');
    e.clipboardData.setData('text/plain', text);
  };

  handlePaste = e => {
    e.preventDefault();
    e.stopPropagation();

    const { topLeft } = this.state;
    const newRows = [];
    const pasteData = defaultParsePaste(e.clipboardData.getData('text/plain'));
    pasteData.forEach(row => {
      const rowData = {};
      // Merge the values from pasting and the keys from the columns
      this.columns.slice(topLeft.colIdx, topLeft.colIdx + row.length).forEach((col, j) => {
        // Create the key-value pair for the row
        rowData[col.key] = row[j];
      });
      // Push the new row to the changes
      newRows.push(rowData);
    });
    this.updateRows(topLeft.rowIdx, newRows);
  };

  onGridRowsUpdated = ({ fromRow, toRow, updated }) => {
    const { rows } = this.state;
    this.setState(state => {
      const rows1 = state.rows.slice();
      for (let i = fromRow; i <= toRow; i += 1) {
        rows[i] = { ...rows[i], ...updated };
      }
      return { rows1 };
    });
  };

  setSelection = args => {
    console.log(args, 'setSelection');
    this.setState({
      topLeft: {
        rowIdx: args.topLeft.rowIdx,
        colIdx: args.topLeft.idx,
      },
      botRight: {
        rowIdx: args.bottomRight.rowIdx,
        colIdx: args.bottomRight.idx,
      },
    });
  };

  render() {
    return (
      <div>
        <ReactDataGrid
          columns={this.columns}
          rowGetter={i => this.state.rows[i]}
          rowsCount={this.state.rows.length}
          onGridRowsUpdated={this.onGridRowsUpdated}
          enableCellSelect
          minColumnWidth={40}
          cellRangeSelection={{
            onComplete: this.setSelection,
          }}
          onCellSelected={s => this.setSelection({ topLeft: s, bottomRight: s })}
        />
      </div>
    );
  }
}

export default connect(
  null,
  null,
)(DataGrid);

The above DataGrid component is being imported in the parent component 2 times to solve a business case.

Few methods that I tried -

  1. Tried creating a ref - <div ref={el => this.wrapperRef = el}> and added the event listener to the ref like this.wrapperRef.addEventListener('copy', this.handleCopy) but then the handleCopy function is not being called.
  2. Tried <div onCopy={this.handleCopy} but the handleCopy function is not being called.

Solution

  • Solved it by adding a variable in state (inFocus) and changing it based on mousedown and keydown events.

    import React from 'react';
    import ReactDataGrid from 'fixed-react-data-grid';
    import { range } from 'lodash';
    
    const defaultParsePaste = str => str.split(/\r\n|\n|\r/).map(row => row.split('\t'));
    
    // References -
    // https://adazzle.github.io/react-data-grid/docs/examples/simple-grid
    // https://gist.github.com/ZackKnopp/40fc0691feb03f0fba3e25e7353b73ae
    
    // Props -
    // columns = [
    //   { key: 'a', name: 'a', editable: true },
    //   { key: 'b', name: 'b', editable: true },
    //   { key: 'c', name: 'c', editable: true },
    //   { key: 'd', name: 'd', editable: true },
    // ];
    // rows = [{ a: 1, b: 2, c: 3, d: 4 }, { a: 5, b: 6, c: 7, d: 8 }];
    // updateRows = rows => {
    //   this.setState({ rows });
    // };
    
    class CustomDataGrid extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          topLeft: {},
          botRight: {},
          inFocus: false,
        };
      }
    
      componentDidMount() {
        document.addEventListener('mousedown', this.handleMousedown);
        document.addEventListener('keydown', this.handleKeydown);
        document.addEventListener('copy', this.handleCopy);
        document.addEventListener('paste', this.handlePaste);
      }
    
      componentWillUnmount() {
        document.removeEventListener('mousedown', this.handleMouseDown);
        document.removeEventListener('keydown', this.handleKeydown);
        document.removeEventListener('copy', this.handleCopy);
        document.removeEventListener('paste', this.handlePaste);
      }
    
      handleMouseDown = e => {
        if (this.wrapperRef.contains(e.target) && !this.state.inFocus) {
          this.setState({ inFocus: true });
          e.stopPropagation();
          e.preventDefault();
        }
        if (!this.wrapperRef.contains(e.target) && this.state.inFocus) {
          this.setState({ inFocus: false });
        }
      };
    
      handleKeydown = e => {
        if (this.wrapperRef.contains(e.target) && !this.state.inFocus) {
          this.setState({ inFocus: true });
          e.stopPropagation();
          e.preventDefault();
        }
        if (!this.wrapperRef.contains(e.target) && this.state.inFocus) {
          this.setState({ inFocus: false });
        }
      };
    
      rowGetter = i => this.props.rows[i];
    
      handleCopy = e => {
        if (this.state.inFocus) {
          e.preventDefault();
          e.stopPropagation();
          const { topLeft, botRight } = this.state;
          const text = range(topLeft.rowIdx, botRight.rowIdx + 1)
            .map(rowIdx =>
              this.props.columns
                .slice(topLeft.colIdx, botRight.colIdx + 1)
                .map(col => this.rowGetter(rowIdx)[col.key])
                .join('\t'),
            )
            .join('\n');
          e.clipboardData.setData('text/plain', text);
        }
      };
    
      updateRows = (startIdx, newRows) => {
        const rows = this.props.rows.slice();
        for (let i = 0; i < newRows.length; i += 1) {
          rows[startIdx + i] = { ...rows[startIdx + i], ...newRows[i] };
        }
        this.props.updateRows(rows);
      };
    
      handlePaste = e => {
        if (this.state.inFocus) {
          e.preventDefault();
          e.stopPropagation();
          const { topLeft, botRight } = this.state;
          const pasteData = defaultParsePaste(e.clipboardData.getData('text/plain'));
          const newRows = [];
          if (pasteData.length === 1 && pasteData[0] && pasteData[0].length === 1) {
            range(topLeft.rowIdx, botRight.rowIdx + 1).forEach(() => {
              const rowData = {};
              this.props.columns.slice(topLeft.colIdx, botRight.colIdx + 1).forEach(col => {
                rowData[col.key] = pasteData[0][0];
              });
              newRows.push(rowData);
            });
          } else {
            pasteData.forEach(row => {
              const rowData = {};
              this.props.columns.slice(topLeft.colIdx, topLeft.colIdx + row.length).forEach((col, j) => {
                rowData[col.key] = row[j];
              });
              newRows.push(rowData);
            });
          }
          this.updateRows(topLeft.rowIdx, newRows);
        }
      };
    
      onGridRowsUpdated = ({ fromRow, toRow, updated }) => {
        const rows = this.props.rows.slice();
        for (let i = fromRow; i <= toRow; i += 1) {
          rows[i] = { ...rows[i], ...updated };
        }
        this.props.updateRows(rows);
      };
    
      setSelection = async args => {
        await this.setState({
          topLeft: {
            rowIdx: args.topLeft.rowIdx,
            colIdx: args.topLeft.idx,
          },
          botRight: {
            rowIdx: args.bottomRight.rowIdx,
            colIdx: args.bottomRight.idx,
          },
          inFocus: true,
        });
      };
    
      render() {
        return (
          <div ref={el => this.wrapperRef = el}>
            <ReactDataGrid
              columns={this.props.columns}
              rowGetter={i => this.props.rows[i]}
              rowsCount={this.props.rows.length}
              onGridRowsUpdated={this.onGridRowsUpdated}
              enableCellSelect
              minColumnWidth={40}
              cellRangeSelection={{
                onComplete: this.setSelection,
              }}
              onCellSelected={s => this.setSelection({ topLeft: s, bottomRight: s })}
            />
          </div>
        );
      }
    }
    
    export default CustomDataGrid;
    

    If there is any better way to solve this, let me know.