React使って96well plate表現してみた

menseki.hatenablog.jp

この記事を書いたときに96 well plateのインターフェースをjavascriptで書いてみたいと思っていたのですが、なんとなくReactのドキュメント読んで適当に書いてみたら自分でもよくわからないまま、なんか動くものができたので一旦ここに記録を残しておきたいと思います。

今この記事を書いてる時点ですでに当コードを書いてから時間が経っていて自分でもどうやって開発したかわからなくなってしまったのでコードの添付だけですが、確かcreate-react-appを使ってどうのこうのした気がします。

正直すでに世の中にはreact-well-platesなるものが存在するので実際にはそれを使うのが良い気がしますが、使い方がわかりません。

index.css

body {
  font: 14px "Century Gothic", Futura, sans-serif;
  margin: 20px;
}

ol, ul {
  padding-left: 30px;
}

.plate-row:after {
  clear: both;
  content: "";
  display: table;
}

.well {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin: 2px;
  padding: 0;
  text-align: center;
  width: 34px;
  border-radius: 17px;
}
.selected {
  border: 3px solid #999;
}

.well:focus {
  outline: none;
}
.label {
  background: #fff;
  color: #444;
  border: 0px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin: 2px;
  padding: 0;
  text-align: center;
  width: 34px;
  border-radius: 17px;
}

.label:focus {
  outline: none;
}

.kbd-navigation .well:focus {
  background: #ddd;
}

.wellplate {
  display: flex;
  flex-direction: row;
  user-select: none;
}

.wellplate-plate {
  border: 1px solid #999;
  padding: 30px;
}
.plate {
  border: 2px solid #999;
  padding: 10px;
}

.game-info {
  margin-left: 20px;
}

index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';

function Well(props) {
  const rowLabels = ['\\', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
  let className;
  let value;
  if (props.row === 0 || props.col === 0) {
    className = 'label';
    value = props.col === 0 ? rowLabels[props.row] : props.col;
  } else {
    className = 'well' + (props.isConfirmed || props.isSelected ? " selected" : "");
  }
  return(
    <button
      className={className}
      onMouseDown={props.onMouseDown}
      onMouseUp={props.onMouseUp}
      onMouseOver={props.onMouseOver}
    >
      {value}
    </button>
  );
}

class Plate extends React.Component {
  renderWell(row, col) {
    const rowLabels = ['\\', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
    return (
      <Well
        key={rowLabels[row]+col.toString()}
        row={row}
        col={col}
        isConfirmed={this.props.isConfirmed[(row-1)*12 + (col-1)]}
        isSelected={this.props.isSelected[(row-1)*12 + (col-1)]}
        onMouseDown={(e) => this.props.onMouseDown(e, row, col)}
        onMouseOver={(e) => this.props.onMouseOver(e, row, col)}
        onClick={(e) => e.stopPropagation() }
      />
    );
  }
  
  renderRow(row) {
    const rowLabels = ['\\', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']
    return(
      <div key={row} className="plate-row">
        { Array(13).fill(null).map((_, col) => this.renderWell(row, col)) }
      </div>
    );
  }

  render() {
    return (
      <div className="plate" onMouseLeave={this.props.onMouseLeave} onMouseUp={this.props.onMouseUp}>
        {Array(9).fill(0).map((_, row) => this.renderRow(row))}
      </div>
    );
  }
}

class WellPlate extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      isConfirmed: Array(96).fill(false),
      isSelected: Array(96).fill(false),
      wellMouseDown: [null, null],
    };
  }

  selectWells(row0, col0, row, col) {
    if (row0 > row) [row0, row] = [row, row0];
    if (col0 > col) [col0, col] = [col, col0];

    let isSelected = Array(96).fill(false);
    if (row0 === 0 && col0 === 0) {
      [row, col] = [8, 12];
    } else if (row0 === 0) {
      row0 = 1;
      row = (row === 0 ? 8 : -1);
    } else if (col0 === 0) {
      col0 = 1;
      col = (col === 0 ? 12 : -1);
    }
    for (let r = row0-1; r < row; r++) {
      for (let c = col0-1; c < col; c++) {
        isSelected[r * 12 + c] = true;
      }
    }
    return isSelected;
  }

  handleMouseDown(e, row, col) {
    console.log(['down', row, col, e.ctrlKey]);
    if (!e.ctrlKey) {
      this.setState({
        isConfirmed: Array(96).fill(false),
      });
    }
    this.setState({
      wellMouseDown: [row, col],
    });
    e.stopPropagation();
  }
  handleMouseDown2(e) {

    this.setState({
      isConfirmed: Array(96).fill(false),
    });
  }
  
  handleMouseUp() {
    const isSelected = this.state.isSelected.slice();
    let isConfirmed = this.state.isConfirmed.slice();
    for (let i = 0; i < 96; ++i) {
      isConfirmed[i] |= isSelected[i];
    }
    this.setState({
      isConfirmed: isConfirmed,
      wellMouseDown: [null, null],
    });
  }
  
  handleMouseOver(e, row, col) {
    console.log(['over', row, col]);
    let row0, col0;
    if (this.state.wellMouseDown[0] === null && this.state.wellMouseDown[1] === null) {
      [row0, col0] = [row, col];
    } else {
      [row0, col0] = this.state.wellMouseDown;
    }
    const isSelected = this.selectWells(row0, col0, row, col);
    this.setState({
      isSelected: isSelected,
    })
  }

  handleMouseLeave() {
    this.setState({
      isSelected: Array(96).fill(false),
    })
  }
  
  render() {
    const isConfirmed = this.state.isConfirmed.slice();
    const isSelected = this.state.isSelected.slice();
    return (
      <div className="wellplate" onContextMenu={ (e) => e.preventDefault() }>
        <div className='wellplate-plate' onMouseDown={() => this.handleMouseDown2()}>
          <Plate
            isConfirmed={isConfirmed}
            isSelected={isSelected}
            onMouseDown={(e, row, col) => this.handleMouseDown(e, row, col)}
            onMouseUp={() => this.handleMouseUp()}
            onMouseOver={(e, row, col) => this.handleMouseOver(e, row, col)}
            onMouseLeave={() => this.handleMouseLeave()}
            onMouseDown2={() => this.handleMouseDown2()}
          />
        </div>
      </div>
    );
  }
}

const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<WellPlate />);

実行結果