본문 바로가기
개발/React

리액트 - Hansontable Customizing with GitHub

by 피로물든딸기 2023. 9. 29.
반응형

깃허브 데스크탑으로 프로젝트 관리하기 강의 오픈!! (인프런 바로가기)

 

리액트 전체 링크

 

해당 프로젝트는 링크에서 확인할 수 있다.

 

이전 프로젝트 참고

- Hansontable로 csv 편집기 만들기

 

- Project Settings (전체 코드)

- True / False Options

- Selected Options

- Number Options

- width, height, placeholder, sort

- 주석, comment, memo, tooltip

- Merge Cells, 셀 합치기

- Search 구현

- Columns Data Type

- Cell 커스터마이징

- afterSelection으로 수식 입력줄 구현하기

- Download CSV 구현 (콤마, 줄바꿈, 따옴표 처리)

- Mui Drawer로 Handsontable Option 관리하기

- Column Width, Row Height 로컬 스토리지에 저장하기

- Mui 토글 버튼으로 셀 스타일 편집 기능 만들기 

- 셀 스타일 로컬 스토리지에 저장하기 (전체 코드)

- Handsontable 깃허브 연동하기 (data, style, comment, merge 저장하기)

 


Project Settings

 

handsontable 6.2.2 ver을 프로젝트에 추가하자.

https://github.com/handsontable/handsontable/tree/6.2.2

npm install handsontable@6.2.2

 

handsontable 6.2.2 ver까지만 MIT 라이센스를 지원한다.

https://github.com/handsontable/handsontable/blob/6.2.2/LICENSE

 

그 외 버전에서도 아래 옵션을 추가하면 테스트는 가능하다.

licenseKey: "non-commercial-and-evaluation",

 

이 프로젝트에서는 6.2.2 ver을 사용한다.


전체 코드

 

MyHandsonTable.js

import React, { useState, useEffect } from "react";
import CustomHansOnTable from "./CustomHansOnTable";

import Box from "@mui/material/Box";
import Drawer from "@mui/material/Drawer";
import Button from "@mui/material/Button";

import Divider from "@mui/material/Divider";

import FormControlLabel from "@mui/material/FormControlLabel";
import Checkbox from "@mui/material/Checkbox";

import FormControl from "@mui/material/FormControl";
import FormHelperText from "@mui/material/FormHelperText";
import Input from "@mui/material/Input";
import InputLabel from "@mui/material/InputLabel";

const MY_OPTIONS = "MY_OPTIONS";

const myDefaultOptions = {
  trueFalseOptions: {
    colHeaders: true,
    rowHeaders: true,
    wordWrap: false /* 줄 바꿈 off */,
    manualColumnResize: true,
    manualRowResize: true,
    manualColumnMove: true,
    manualRowMove: true,
    allowInsertColumn: true,
    allowInsertRow: true,
    allowRemoveColumn: true,
    allowRemoveRow: true,
    autoWrapCol: true /* 마지막 셀 아래에서 다음 셀 위로 이동 */,
    autoWrapRow: true /* 마지막 셀 옆에서 다음 셀 처음으로 이동 */,
    dragToScroll: true /* 표를 클릭 후 드래그를 할 때, 같이 스크롤 되는지 여부 */,
    persistentState: false /* 열 정렬 상태, 열 위치 및 열 크기를 로컬 스토리지에 저장 */,
    // outsideClickDeselects: false /* 셀 외부 클릭 시, 셀 선택 해제 */,
    readOnly: false /* true : 모든 셀을 readOnly로 설정*/,
    enterBeginsEditing: true /* true : 엔터 클릭 시 편집 모드, false : 다음 셀로 이동 */,
    copyable: true /* 복사 가능 여부 */,
    copyPaste: true /* 복사, 붙여넣기 가능 여부 */,
    undo: true /* false : ctrl + z 비활성화 */,
    trimWhitespace: false /* 자동 trim() 실행 후 셀에 저장 */,
    contextMenu: true /* 마우스 왼쪽 버튼 클릭 시 컨텍스트 메뉴 */,
    comments: true /* 주석, 메모 기능 context menu에 추가 */,
    manualColumnFreeze: true /* freezeColumn context menu에 추가 */,
    observeChanges: true,
  },

  numberOptions: {
    width: 1000,
    height: 1000,
    startCols: 5 /* data가 없는 경우 기본 설정 */,
    startRows: 5 /* data가 없는 경우 기본 설정 */,
    maxCols: 100 /* 주어진 값보다 큰 Column은 제거 */,
    maxRows: 100 /* 주어진 값보다 큰 Row는 제거 */,
    minCols: 1 /* 최소한의 Column */,
    minRows: 1 /* 최소한의 Row */,
    minSpareRows: 0 /* 빈 열 자동 추가 */,
    minSpareCols: 0 /* 빈 행 자동 추가 */,
    fixedColumnsLeft: 0,
    fixedRowsTop: 0,
    fixedRowsBottom: 0,
    rowHeaderWidth: 55 /* 행 헤더 너비 */,
  },

  cellInfo: {
    colWidths: 60,
    rowHeights: 25,
  },
};

const MyHandsonTable = () => {
  const [myOptions, setMyOptions] = useState(myDefaultOptions);

  const [state, setState] = useState({ right: false });

  const toggleDrawer = (anchor, open) => (event) => {
    if (
      event.type === "keydown" &&
      (event.key === "Tab" || event.key === "Shift")
    ) {
      return;
    }

    setState({ ...state, [anchor]: open });
  };

  const changeTrueFalseOptions = (option, value) => {
    console.log(myOptions);
    let temp = { ...myOptions };
    temp.trueFalseOptions[option] = !value;
    setMyOptions(temp);
    localStorage.setItem(MY_OPTIONS, JSON.stringify(temp));
  };

  const makeTrueFalseCheckBox = () => {
    let pair = Object.entries(myOptions.trueFalseOptions);

    pair = pair.map((item) => [item[0], Boolean(item[1])]);

    return pair.map((item, idx) => (
      <FormControlLabel
        key={idx}
        control={<Checkbox checked={item[1]} />}
        label={item[0]}
        onChange={() => changeTrueFalseOptions(item[0], item[1])}
      />
    ));
  };

  const changeNumberOptions = (option, value) => {
    let temp = { ...myOptions };

    if (isNaN(Number(value))) return;

    temp.numberOptions[option] = Number(value);
    setMyOptions(temp);
    localStorage.setItem(MY_OPTIONS, JSON.stringify(temp));
  };

  const makeNumberInput = () => {
    let pair = Object.entries(myOptions.numberOptions);

    pair = pair.map((item) => [item[0], Number(item[1])]);

    return pair.map((item, idx) => (
      <FormControl key={idx} sx={{ m: 2 }} variant="standard">
        <InputLabel htmlFor="component-error">{item[0]}</InputLabel>
        <Input
          value={item[1]}
          onChange={(e) => changeNumberOptions(item[0], e.target.value)}
        />
      </FormControl>
    ));
  };

  const list = () => (
    <Box sx={{ width: 600 }}>
      <Box sx={{ m: 2, flexDirection: "row" }}>{makeTrueFalseCheckBox()}</Box>

      <Divider />

      <Box sx={{ m: 2, flexDirection: "row" }}>
        <FormHelperText sx={{ color: "blue" }}>
          0 이상 숫자를 입력하세요.
        </FormHelperText>
        {makeNumberInput()}
      </Box>
    </Box>
  );

  const initLocalStorage = () => {
    let localOptions = localStorage.getItem(MY_OPTIONS);

    if (localOptions === null) return;

    setMyOptions(JSON.parse(localOptions));
  };

  useEffect(() => {
    initLocalStorage();
  }, []);

  return (
    <div>
      <div>
        {["right"].map((anchor) => (
          <React.Fragment key={anchor}>
            <Button
              sx={{ m: 2 }}
              variant="contained"
              color="secondary"
              onClick={toggleDrawer(anchor, true)}
            >
              Options Setting
            </Button>
            <Drawer
              anchor={anchor}
              open={state[anchor]}
              onClose={toggleDrawer(anchor, false)}
            >
              {list()}
            </Drawer>
          </React.Fragment>
        ))}
        <CustomHansOnTable myOptions={myOptions} />
      </div>
    </div>
  );
};

export default MyHandsonTable;

 

CustomHansOnTable.js

import React, { useEffect, useState } from "react";

import Handsontable from "handsontable";
import "handsontable/dist/handsontable.full.min.css";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";

import styled from "styled-components";
import HandsontableToggleButton from "./HandsontableToggleButton";

const DisplayCellStyle = styled.div`
  span {
    background-color: #33ceff;
    position: relative;
    padding: 0.4rem 0.85rem;
    border: 1px solid transparent;
    border-radius: 0.35rem;
  }
`;

// const data = [
//   ["", "Tesla", "Nissan", "Toyota", "Honda", "Mazda", "Ford"],
//   ["2017", 10, 11, 12, 13, 15, 16],
//   ["2018", 10, 11, 12, 13, 15, 16],
//   ["2019", 10, 11, 12, 13, 15, 16],
//   ["2020", 10, 11, 12, 13, 15, 16],
//   ["2021", 10, 11, 12, 13, 15, 16],
// ];

const data = [
  ["", "2017", "2018", "2019", "2020", "2021", "2022"],
  ["Tesla", 10, 5, 5, 10, 14, 5],
  ["Nissan", 15, 2, 7, 11, 13, 4],
  ["Toyota", 11, 1, 10, 12, 12, 3],
  ["Honda", 5, 3, 7, 13, 11, 4],
  ["Mazda", 4, 7, 5, 14, 10, 4],
];

// dummy data for test
const initData = () => {
  let row = [];
  for (let i = 0; i < 100; i++) {
    row.push(String.fromCharCode("A".charCodeAt() + (i % 26)));
  }

  let table = [];
  for (let k = 0; k < 100; k++) {
    let tmp = JSON.parse(JSON.stringify(row));
    let number = `${k + 1}`;
    for (let i = 0; i < 100; i++)
      tmp[i] = `${tmp[i]}${number.padStart(3, "0")}`;
    table.push(tmp);
  }

  return table;
};

let searchResultCount = 0;
function searchResultCounter(instance, row, col, value, result) {
  const DEFAULT_CALLBACK = function(instance, row, col, data, testResult) {
    instance.getCellMeta(row, col).isSearchResult = testResult;
  };

  DEFAULT_CALLBACK.apply(this, arguments);

  if (result) {
    searchResultCount++;
  }
}

function redRenderer(instance, td) {
  Handsontable.renderers.TextRenderer.apply(this, arguments);
  td.style.backgroundColor = "red";
  td.style.fontWeight = "bold";
}

const MY_OPTIONS = "MY_OPTIONS";
const COMMENTS_KEY = "COMMENTS_KEY";
const MERGE_CELLS_KEY = "MERGE_CELLS_KEY";
const CELL_STYLE_KEY = "CELL_STYLE_KEY";

const CustomHansOnTable = ({ myOptions }) => {
  const [myHandsOnTable, setMyHandsOnTable] = useState();
  const [displayCellInfo, setDisplaySetInfo] = useState("");
  const [selectedCell, setSelectedCell] = useState([0, 0]);

  const getComments = () => {
    let comments = localStorage.getItem(COMMENTS_KEY);

    if (comments === null) return [];

    return JSON.parse(comments);
  };

  const getMergeCells = () => {
    let mergeCells = localStorage.getItem(MERGE_CELLS_KEY);

    if (mergeCells === null) return [];

    return JSON.parse(mergeCells);
  };

  const myNewQueryMethod = (searchValue, dataValue) => {
    if (!searchValue) return false;

    dataValue = dataValue || "";
    return searchValue.toString() === dataValue.toString();
  };

  const cellSelected = () => {
    let selectedLast = myTable.getSelectedLast();

    if (selectedLast[0] < 0 || selectedLast[1] < 0) return;

    let value = myTable.getValue() || "";
    setDisplaySetInfo(value);

    setSelectedCell([selectedLast[0], selectedLast[1]]);
  };

  const localCellStyle = localStorage.getItem(CELL_STYLE_KEY)
    ? JSON.parse(localStorage.getItem(CELL_STYLE_KEY))
    : null;

  const options = {
    data, // initData(),

    /* true or false options */
    colHeaders: true,
    rowHeaders: true,
    wordWrap: false /* 줄 바꿈 off */,
    manualColumnResize: true,
    manualRowResize: true,
    manualColumnMove: true,
    manualRowMove: true,
    allowInsertColumn: true,
    allowInsertRow: true,
    allowRemoveColumn: true,
    allowRemoveRow: true,
    autoWrapCol: true /* 마지막 셀 아래에서 다음 셀 위로 이동 */,
    autoWrapRow: true /* 마지막 셀 옆에서 다음 셀 처음으로 이동 */,
    dragToScroll: true /* 표를 클릭 후 드래그를 할 때, 같이 스크롤 되는지 여부 */,
    persistentState: false /* 열 정렬 상태, 열 위치 및 열 크기를 로컬 스토리지에 저장 */,
    outsideClickDeselects: false /* 셀 외부 클릭 시, 셀 선택 해제 */,
    readOnly: false /* true : 모든 셀을 readOnly로 설정*/,
    enterBeginsEditing: true /* true : 엔터 클릭 시 편집 모드, false : 다음 셀로 이동 */,
    copyable: true /* 복사 가능 여부 */,
    copyPaste: true /* 복사, 붙여넣기 가능 여부 */,
    undo: true /* false : ctrl + z 비활성화 */,
    trimWhitespace: false /* 자동 trim() 실행 후 셀에 저장 */,
    contextMenu: true /* 마우스 왼쪽 버튼 클릭 시 컨텍스트 메뉴 */,
    comments: true /* 주석, 메모 기능 context menu에 추가 */,
    manualColumnFreeze: true /* freezeColumn context menu에 추가 */,

    observeChanges: true,
    afterChangesObserved: () => {
      //console.log("change !!");
    },

    // filters: true, /* 필터 기능 on 6.2.2 pro  */,
    // dropdownMenu: true, /* dropdown 메뉴 설정 6.2.2 pro */

    /* Selected Options */
    className: "htMiddle htCenter" /* Cell Alignment */,
    // stretchH: "none", /* 빈 공간을 채우는 방법 : none, last, all */
    // selectionMode: "multiple", /* Ctrl 키 + 선택 가능한 셀 : multiple, range, single */
    // fillHandle : true, /* 드래그로 자동 채움 : true, false, vertical, horizontal 옵션 */
    // disableVisualSelection: "current", /* 셀 선택 활성화 여부 : false, true, current, area, header, [option1, option2, ...] */

    /* Number Options */
    width: 1000,
    height: 1000,

    startCols: 5 /* data가 없는 경우 기본 설정 */,
    startRows: 3 /* data가 없는 경우 기본 설정 */,
    afterSelection: cellSelected,
    // maxCols: 2, /* 주어진 값보다 큰 Column은 제거 */
    // maxRows: 3, /* 주어진 값보다 큰 Row는 제거 */
    // minCols: 10, /* 최소한의 Column */
    // minRows: 10, /* 최소한의 Row */
    // minSpareRows: 1, /* 빈 열 자동 추가 */
    // minSpareCols: 2, /* 빈 행 자동 추가 */
    // fixedColumnsLeft: 2,
    // fixedRowsTop: 3,
    // fixedRowsBottom: 2,
    // rowHeaderWidth: 250, /* 행 헤더 너비 */

    /* Customizing Options */
    colWidths: 60 /* 특정 위치 너비 변경 : [60, 120, 60, 60, 60, 60, 60] */,
    rowHeights: 25,
    // placeholder: 'Empty',
    // columnSorting: {
    //   indicator: true, /* default true, 정렬 순서 표시 마크 (↑↓) on / off */
    //   sortEmptyCells: true, /* true : 빈 셀도 정렬, false : 모든 빈 셀은 테이블 끝으로 이동 */
    //   headerAction: true, /* default true, 헤더 클릭 시 정렬 기능 on / off */
    //   initialConfig: {
    //     column: 2, /* column : 2를 기준으로 정렬 */
    //     sortOrder: "asc", /* 내림차순 desc */
    //   },

    //   /* 비교함수 구현. -1, 0, 1을 return. */
    //   // compareFunctionFactory: function(sortOrder, columnMeta) {
    //   //   return function(value, nextValue) {
    //   //     if(value > 2000) return -1;
    //   //     return value - nextValue;
    //   //   }
    //   // },
    // },

    comments: {
      displayDelay: 1000 /* 1초 뒤에 메모가 on */,
    },

    cell: getComments(),
    afterSetCellMeta: (row, col, key, obj) => {
      if (key === "comment") {
        // 기존 데이터 삭제
        let temp = getComments().filter(
          (item) => (item.row === row && item.col === col) === false
        );
        if (obj !== undefined)
          temp.push({ row, col, comment: { value: obj.value } });
        localStorage.setItem(COMMENTS_KEY, JSON.stringify([...temp]));
      }
    },

    // 6.2.2 미지원
    // beforeSetCellMeta:(row, col, key, value) => {
    //   console.log("before",row, col, key, value);
    // },

    // afterChange: function(change, source) {
    //   console.log(change, source);
    //   //change [row, col, before, after];
    // },

    mergeCells: getMergeCells(),
    afterUnmergeCells: (cellRange, auto) => {
      let temp = getMergeCells().filter(
        (item) =>
          (item.row === cellRange.from.row &&
            item.col === cellRange.from.col) === false
      );

      localStorage.setItem(MERGE_CELLS_KEY, JSON.stringify([...temp]));
    },

    // search: {
    //   callback: searchResultCounter,
    //   queryMethod: myNewQueryMethod,
    //   //searchResultClass: 'customClass'
    // },

    // columns: [
    //     {data: "id", type: 'numeric'},
    //     {data: "name", renderer: redRenderer},
    //     {data: "isActive", type: 'checkbox'},
    //     {data: "date", type: 'date', dateFormat: 'YYYY-MM-DD'},
    //     {data: "color",
    //       type: 'autocomplete', // dropdown
    //       source: ["yellow", "red", "orange", "green", "blue", "gray", "black", "white"]
    //     },
    //     {
    //       editor: 'select',
    //       selectOptions: ['Kia', 'Nissan', 'Toyota', 'Honda']
    //     },
    //   ],
    
    cells: function(row, col, prop) {
      if (localCellStyle === null) return {};

      let cellProperties = {};

      cellProperties.className =
        localCellStyle[row][col].className || "htCenter htMiddle"; // undefined 처리

      cellProperties.renderer = function(instance, td) {
        Handsontable.renderers.TextRenderer.apply(this, arguments);
        td.style.fontWeight = localCellStyle[row][col].style.fontWeight || "";
        td.style.fontStyle = localCellStyle[row][col].style.fontStyle || "";
        td.style.textDecoration = localCellStyle[row][col].style.textDecoration || "";
        td.style.color = localCellStyle[row][col].style.color || "#000000";
        td.style.backgroundColor = localCellStyle[row][col].style.backgroundColor || "#FFFFFF";
      };

      return cellProperties;
    },

    licenseKey: "non-commercial-and-evaluation",
  };

  const setColWidths = (table, setOptions) => {
    let colLength = table.getData()[0].length;
    let widths = [];

    for (let i = 0; i < colLength; i++) widths.push(table.getColWidth(i));

    setOptions.cellInfo.colWidths = widths;

    localStorage.setItem(MY_OPTIONS, JSON.stringify(setOptions));
  };

  const setRowHeights = (table, setOptions) => {
    let rowLength = table.getData().length;
    let heights = [];

    for (let i = 0; i < rowLength; i++) heights.push(table.getRowHeight(i));

    setOptions.cellInfo.rowHeights = heights;

    localStorage.setItem(MY_OPTIONS, JSON.stringify(setOptions));
  };

  let myTable;
  const makeTable = () => {
    const container = document.getElementById("hot-app");
    container.innerHTML = "";

    myTable = new Handsontable(container, {
      ...options,
      ...myOptions.trueFalseOptions,
      ...myOptions.numberOptions,
      ...myOptions.cellInfo,
    });

    myTable.addHook("afterMergeCells", function(cellRange, mergeParent, auto) {
      let temp = getMergeCells();
      temp.push(mergeParent);
      temp = temp.filter(
        (item) => myTable.getCellMeta(item.row, item.col).spanned === true
      );

      localStorage.setItem(MERGE_CELLS_KEY, JSON.stringify([...temp]));
    });

    myTable.addHook("afterColumnResize", function(col, width) {
      let localOptions = localStorage.getItem(MY_OPTIONS);

      if (localOptions === null) {
        setColWidths(this, myOptions);
        return;
      }

      localOptions = JSON.parse(localOptions);
      if (Array.isArray(localOptions.cellInfo.colWidths) === false) {
        setColWidths(this, localOptions);
        return;
      }

      localOptions.cellInfo.colWidths[col] = width;
      localStorage.setItem(MY_OPTIONS, JSON.stringify(localOptions));
    });

    myTable.addHook("afterRowResize", function(row, height) {
      let localOptions = localStorage.getItem(MY_OPTIONS);

      if (localOptions === null) {
        setRowHeights(this, myOptions);
        return;
      }

      localOptions = JSON.parse(localOptions);
      if (Array.isArray(localOptions.cellInfo.rowHeights) === false) {
        setRowHeights(this, localOptions);
        return;
      }

      localOptions.cellInfo.rowHeights[row] = height;
      localStorage.setItem(MY_OPTIONS, JSON.stringify(localOptions));
    });

    myTable.render();
    setMyHandsOnTable(myTable);

    // search 구현
    // let searchField = document.getElementById("search_field");
    // let resultCount = document.getElementById("resultCount");

    // Handsontable.dom.addEvent(searchField, "keyup", function(event) {
    //   searchResultCount = 0;

    //   let search = myTable.getPlugin("search");
    //   let queryResult = search.query(this.value);

    //   console.log(queryResult);

    //   resultCount.innerText = searchResultCount.toString();
    //   myTable.render();
    // });
  };

  useEffect(() => {
    makeTable();
  }, [myOptions]);

  const changeFormat = (value) => {
    value = value || "";
    value = value.toString();
    if (value.includes('"')) return '"' + value.replace(/"/g, '""') + '"';
    if (value.includes(",") || value.includes("\n")) return '"' + value + '"';
    return value;
  };

  const downloadCSV = () => {
    let data = myHandsOnTable.getData();

    let csv = "";
    for (let r = 0; r < data.length; r++) {
      let row = data[r].map(changeFormat).join(",");
      csv += row + "\n";
    }

    let fileDown = "data:csv;charset=utf-8," + csv;
    let encodedUri = encodeURI(fileDown);
    let link = document.createElement("a");

    link.setAttribute("href", encodedUri);
    link.setAttribute("download", "handsontable.csv");

    document.body.appendChild(link);

    link.click();

    document.body.removeChild(link);
  };

  return (
    <div>
      <Box sx={{ m: 2 }}>
        <Button
          sx={{ m: 2 }}
          variant="outlined"
          color="primary"
          onClick={downloadCSV}
        >
          Download CSV
        </Button>
        {/* <input id="search_field" type="search" placeholder="search" />
        <p>
        <span id="resultCount">0</span> results
      </p> */}
        <HandsontableToggleButton
          myHandsOnTable={myHandsOnTable}
          selectedCell={selectedCell}
        />
        <DisplayCellStyle>
          <span>{displayCellInfo}</span>
        </DisplayCellStyle>
        <div id="hot-app" style={{ marginTop: "13px" }}></div>
      </Box>
    </div>
  );
};

export default CustomHansOnTable;

 

HandsontableToggleButton.js

import React, { useState } from "react";

import { styled } from "@mui/material/styles";

import Box from "@mui/material/Box";
import FormatAlignLeftIcon from "@mui/icons-material/FormatAlignLeft";
import FormatAlignCenterIcon from "@mui/icons-material/FormatAlignCenter";
import FormatAlignRightIcon from "@mui/icons-material/FormatAlignRight";
//import FormatAlignJustifyIcon from "@mui/icons-material/FormatAlignJustify";
import VerticalAlignBottomIcon from "@mui/icons-material/VerticalAlignBottom";
import VerticalAlignCenterIcon from "@mui/icons-material/VerticalAlignCenter";
import VerticalAlignTopIcon from "@mui/icons-material/VerticalAlignTop";
import FormatBoldIcon from "@mui/icons-material/FormatBold";
import FormatItalicIcon from "@mui/icons-material/FormatItalic";
import FormatUnderlinedIcon from "@mui/icons-material/FormatUnderlined";
import FormatStrikethroughIcon from "@mui/icons-material/FormatStrikethrough";
import FormatColorFillIcon from "@mui/icons-material/FormatColorFill";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import Divider from "@mui/material/Divider";
import Paper from "@mui/material/Paper";
import ColorizeIcon from "@mui/icons-material/Colorize";
import ToggleButton from "@mui/material/ToggleButton";
import ToggleButtonGroup from "@mui/material/ToggleButtonGroup";

import { CompactPicker } from "react-color";
import { useEffect } from "react";

const StyledToggleButtonGroup = styled(ToggleButtonGroup)(({ theme }) => ({
  "& .MuiToggleButtonGroup-grouped": {
    margin: theme.spacing(0.5),
    border: 0,
    "&.Mui-disabled": {
      border: 0,
    },
    "&:not(:first-of-type)": {
      borderRadius: theme.shape.borderRadius,
    },
    "&:first-of-type": {
      borderRadius: theme.shape.borderRadius,
    },
  },
}));

const CELL_STYLE_KEY = "CELL_STYLE_KEY";

const HandsontableToggleButton = ({ myHandsOnTable, selectedCell }) => {
  const [horizontalAlignment, setHorizontalAlignment] = useState("");
  const [verticalAlignment, setVerticalAlignment] = useState("");
  const [formats, setFormats] = useState(() => []);

  const [showCompactPicker, setShowCompactPicker] = useState(false);
  const [pickerPosition, setPickerPosition] = useState({ top: 0, left: 0 });
  const [fontColor, setFontColor] = useState("#000000");
  const [bgColor, setBgColor] = useState("#FFFFFF");

  const getCellInfoBase = () => {
    let selecetedRangeCells = myHandsOnTable.getSelectedRange();
    if (selecetedRangeCells === undefined) return undefined;

    let baseCell = selecetedRangeCells[0].from;
    return myHandsOnTable.getCell(baseCell.row, baseCell.col);
  };

  const getCellInfoRange = () => {
    let selecetedRangeCells = myHandsOnTable.getSelectedRange();
    if (selecetedRangeCells === undefined) return undefined;

    let cellPositions = [];
    for (let cell of selecetedRangeCells) {
      for (let r = cell.from.row; r <= cell.to.row; r++) {
        for (let c = cell.from.col; c <= cell.to.col; c++)
          cellPositions.push([r, c]);
      }
    }

    return cellPositions;
  };

  const getHorizontalStatus = (className) => {
    let status = ["htLeft", "htCenter", "htRight"];
    let current = className.split(" ");

    return current.filter((item) => status.includes(item))[0];
  };

  const getVerticalStatus = (className) => {
    let status = ["htTop", "htMiddle", "htBottom"];
    let current = className.split(" ");

    return current.filter((item) => status.includes(item))[0];
  };

  const getEmptyArray = () => {
    let row = myHandsOnTable.getData().length;
    let col = myHandsOnTable.getData()[0].length;
    let emptyArray = [];

    for (let r = 0; r < row; r++) {
      emptyArray[r] = [];
      for (let c = 0; c < col; c++)
        emptyArray[r][c] = {
          className: undefined,
          style: {
            fontWeight: undefined,
            fontStyle: undefined,
            textDecoration: undefined,
            color: undefined,
            backgroundColor: undefined,
          },
        };
    }
    return emptyArray;
  };

  const handleAlignment = (event, newAlignment, type) => {
    console.log(newAlignment, type);

    let cellPositions = getCellInfoRange();
    if (cellPositions === undefined) return;

    if (type === "horizontal") setHorizontalAlignment(newAlignment);
    else if (type === "vertical") setVerticalAlignment(newAlignment);

    for (let pos of cellPositions) {
      let cellInfo = myHandsOnTable.getCell(pos[0], pos[1]);
      let className = cellInfo.className;
      let split = className.split(" ");
      if (type === "horizontal") {
        let horizontal = getHorizontalStatus(className);
        split = split.filter((item) => item !== horizontal); // 현재 설정 값 삭제
      } else if (type === "vertical") {
        let vertical = getVerticalStatus(className);
        split = split.filter((item) => item !== vertical); // 현재 설정 값 삭제
      }

      if (newAlignment) split.push(newAlignment); // 새로 설정된 값 추가.

      cellInfo.className = split.join(" ");

      let localCellStyle = localStorage.getItem(CELL_STYLE_KEY);
      if (localCellStyle === null) {
        let emptyArray = getEmptyArray();
        emptyArray[pos[0]][pos[1]].className = cellInfo.className;
        localStorage.setItem(CELL_STYLE_KEY, JSON.stringify(emptyArray));
      } else {
        localCellStyle = JSON.parse(localCellStyle);
        localCellStyle[pos[0]][pos[1]].className = cellInfo.className;
        localStorage.setItem(CELL_STYLE_KEY, JSON.stringify(localCellStyle));
      }
    }
  };

  const handleFormat = (event, newFormats) => {
    console.log(newFormats);

    let cellPositions = getCellInfoRange();
    if (cellPositions === undefined) return;

    setFormats(newFormats);

    for (let pos of cellPositions) {
      let cellInfo = myHandsOnTable.getCell(pos[0], pos[1]);

      cellInfo.style.fontWeight = newFormats.includes("bold") ? "bold" : "";
      cellInfo.style.fontStyle = newFormats.includes("italic") ? "italic" : "";

      let deco = [];
      if (newFormats.includes("underline")) deco.push("underline");
      if (newFormats.includes("line-through")) deco.push("line-through");

      cellInfo.style.textDecoration = deco.join(" ");

      let localCellStyle = localStorage.getItem(CELL_STYLE_KEY);
      if (localCellStyle === null) {
        let emptyArray = getEmptyArray();
        emptyArray[pos[0]][pos[1]].style.fontWeight = cellInfo.style.fontWeight;
        emptyArray[pos[0]][pos[1]].style.fontStyle = cellInfo.style.fontStyle;
        emptyArray[pos[0]][pos[1]].style.textDecoration =
          cellInfo.style.textDecoration;
        localStorage.setItem(CELL_STYLE_KEY, JSON.stringify(emptyArray));
      } else {
        localCellStyle = JSON.parse(localCellStyle);
        localCellStyle[pos[0]][pos[1]].style.fontWeight =
          cellInfo.style.fontWeight;
        localCellStyle[pos[0]][pos[1]].style.fontStyle =
          cellInfo.style.fontStyle;
        localCellStyle[pos[0]][pos[1]].style.textDecoration =
          cellInfo.style.textDecoration;
        localStorage.setItem(CELL_STYLE_KEY, JSON.stringify(localCellStyle));
      }
    }
  };

  const handleToggleCompactPicker = (event, type) => {
    let cellPositions = getCellInfoRange();
    if (cellPositions === undefined) return;

    const iconButton = event.currentTarget;
    const rect = iconButton.getBoundingClientRect();
    const pickerTop = rect.bottom + window.scrollY;
    const pickerLeft = rect.left + window.scrollX;

    setPickerPosition({ top: pickerTop, left: pickerLeft });
    setShowCompactPicker((prev) => !prev);
  };

  const handleChangeComplete = (color, event) => {
    let cellPositions = getCellInfoRange();
    if (cellPositions === undefined) return;

    let colorType = formats.includes("fontColor") ? "fontColor" : "bgColor";

    console.log(colorType, color.hex);

    if (colorType === "fontColor") setFontColor(color.hex);
    else setBgColor(color.hex);

    for (let pos of cellPositions) {
      let cellInfo = myHandsOnTable.getCell(pos[0], pos[1]);

      if (colorType === "fontColor") {
        cellInfo.style.color = color.hex;

        let localCellStyle = localStorage.getItem(CELL_STYLE_KEY);
        if (localCellStyle === null) {
          let emptyArray = getEmptyArray();
          emptyArray[pos[0]][pos[1]].style.color = color.hex;
          localStorage.setItem(CELL_STYLE_KEY, JSON.stringify(emptyArray));
        } else {
          localCellStyle = JSON.parse(localCellStyle);
          localCellStyle[pos[0]][pos[1]].style.color = color.hex;
          localStorage.setItem(CELL_STYLE_KEY, JSON.stringify(localCellStyle));
        }
      } else {
        cellInfo.style.backgroundColor = color.hex;

        let localCellStyle = localStorage.getItem(CELL_STYLE_KEY);
        if (localCellStyle === null) {
          let emptyArray = getEmptyArray();
          emptyArray[pos[0]][pos[1]].style.backgroundColor = color.hex;
          localStorage.setItem(CELL_STYLE_KEY, JSON.stringify(emptyArray));
        } else {
          localCellStyle = JSON.parse(localCellStyle);
          localCellStyle[pos[0]][pos[1]].style.backgroundColor = color.hex;
          localStorage.setItem(CELL_STYLE_KEY, JSON.stringify(localCellStyle));
        }
      }
    }
  };

  const getColorPicker = () => {
    let colorType = formats.includes("fontColor") ? "fontColor" : "bgColor";
    return (
      <CompactPicker
        color={colorType === "fontColor" ? fontColor : bgColor}
        onChangeComplete={handleChangeComplete}
      />
    );
  };

  const handleClose = () => {
    let fms = formats.filter(
      (item) => (item === "fontColor" || item === "bgColor") === false
    );
    setFormats(fms);

    setShowCompactPicker(false);
  };

  const setButtonState = () => {
    if (myHandsOnTable === undefined) return;

    let cellInfo = getCellInfoBase();
    let className = cellInfo.className;
    let horizontal = getHorizontalStatus(className) || ""; // undefined 처리
    let vertical = getVerticalStatus(className) || "";

    setHorizontalAlignment(horizontal);
    setVerticalAlignment(vertical);

    let fontWeight = cellInfo.style.fontWeight;
    let fontStyle = cellInfo.style.fontStyle;
    let textDecoration = cellInfo.style.textDecoration.split(" ");

    setFormats([fontWeight, fontStyle, ...textDecoration]);
    setFontColor(cellInfo.style.color);
    setBgColor(cellInfo.style.backgroundColor);
  };

  useEffect(() => {
    setButtonState();
  }, [selectedCell]);

  return (
    <div>
      <Box sx={{ m: 2, marginBottom: 5 }}>
        <Paper
          elevation={0}
          sx={{
            display: "flex",
            border: (theme) => `1px solid ${theme.palette.divider}`,
            flexWrap: "wrap",
            width: "580px",
          }}
        >
          <StyledToggleButtonGroup
            size="small"
            value={horizontalAlignment}
            exclusive
            onChange={(e, alignment) =>
              handleAlignment(e, alignment, "horizontal")
            }
            aria-label="text alignment"
          >
            <ToggleButton value="htLeft" aria-label="left aligned">
              <FormatAlignLeftIcon />
            </ToggleButton>
            <ToggleButton value="htCenter" aria-label="centered">
              <FormatAlignCenterIcon />
            </ToggleButton>
            <ToggleButton value="htRight" aria-label="right aligned">
              <FormatAlignRightIcon />
            </ToggleButton>
            {/* <ToggleButton value="justify" aria-label="justified">
              <FormatAlignJustifyIcon />
            </ToggleButton> */}
          </StyledToggleButtonGroup>

          <StyledToggleButtonGroup
            size="small"
            value={verticalAlignment}
            exclusive
            onChange={(e, alignment) =>
              handleAlignment(e, alignment, "vertical")
            }
            aria-label="text alignment"
          >
            <ToggleButton value="htTop" aria-label="top aligned">
              <VerticalAlignTopIcon />
            </ToggleButton>
            <ToggleButton value="htMiddle" aria-label="middle">
              <VerticalAlignCenterIcon />
            </ToggleButton>
            <ToggleButton value="htBottom" aria-label="bottom aligned">
              <VerticalAlignBottomIcon />
            </ToggleButton>
          </StyledToggleButtonGroup>

          <Divider flexItem orientation="vertical" sx={{ mx: 0.5, my: 1 }} />

          <StyledToggleButtonGroup
            size="small"
            value={formats}
            onChange={handleFormat}
            aria-label="text formatting"
          >
            <ToggleButton value="bold" aria-label="bold">
              <FormatBoldIcon />
            </ToggleButton>
            <ToggleButton value="italic" aria-label="italic">
              <FormatItalicIcon />
            </ToggleButton>
            <ToggleButton value="underline" aria-label="underline">
              <FormatUnderlinedIcon />
            </ToggleButton>
            <ToggleButton value="line-through" aria-label="line-through">
              <FormatStrikethroughIcon />
            </ToggleButton>

            <ToggleButton
              value="fontColor"
              aria-label="fontColor"
              onClick={(e) => handleToggleCompactPicker(e, "fontColor")}
            >
              <ColorizeIcon />
              <ArrowDropDownIcon />
            </ToggleButton>
            <ToggleButton
              value="bgColor"
              aria-label="bgColor"
              onClick={(e) => handleToggleCompactPicker(e, "bgColor")}
            >
              <FormatColorFillIcon />
              <ArrowDropDownIcon />
            </ToggleButton>
          </StyledToggleButtonGroup>
        </Paper>

        {showCompactPicker && (
          <div
            className="compact-picker-container"
            style={{
              position: "absolute",
              top: pickerPosition.top + "px",
              left: pickerPosition.left + "px",
              zIndex: 1000,
            }}
          >
            <div
              style={{
                position: "fixed",
                top: "0px",
                right: "0px",
                bottom: "0px",
                left: "0px",
              }}
              onClick={handleClose}
            />
            {getColorPicker()}
          </div>
        )}
      </Box>
    </div>
  );
};

export default HandsontableToggleButton;
반응형

댓글