반응형
참고
- width, height, placeholder, sort
- afterSelection으로 수식 입력줄 구현하기
- Download CSV 구현 (콤마, 줄바꿈, 따옴표 처리)
- Mui Drawer로 Handsontable Option 관리하기
- Column Width, Row Height 로컬 스토리지에 저장하기
- 셀 스타일 로컬 스토리지에 저장하기 (전체 코드)
- Handsontable 깃허브 연동하기 (data, style, comment, merge 저장하기)
아래의 결과는 링크에서 확인할 수 있다.
이전 글에 적용한 셀 스타일 편집 기능을 로컬 스토리지에 저장해보자.
먼저 로컬 스토리지에 저장할 키 값과 초기화 코드를 추가한다.
const CELL_STYLE_KEY = "CELL_STYLE_KEY";
const localCellStyle = localStorage.getItem(CELL_STYLE_KEY)
? JSON.parse(localStorage.getItem(CELL_STYLE_KEY))
: null;
Cell 커스터마이징 옵션에 아래 코드를 추가한다.
로컬 스토리지에 값이 있다면 해당 셀에 적용하는 코드다.
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;
},
이제 편집 버튼에서 로컬 스토리지에 저장하는 코드를 추가한다.
예를 들면 아래와 같다.
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));
}
}
};
나머지 편집 설정은 전체 코드를 참고하자.
전체 코드
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;
반응형
'개발 > React' 카테고리의 다른 글
리액트 - 댓글 기능 만들기 with react-comments-section (React Comments and Reply) (0) | 2023.11.15 |
---|---|
리액트 - Handsontable 깃허브 연동하기 (data, style, comment, merge 저장하기) (1) | 2023.09.30 |
리액트 - Mui 토글 버튼으로 Handsontable 셀 스타일 편집 기능 만들기 (0) | 2023.09.30 |
리액트 - Handsontable Column Width, Row Height 로컬 스토리지에 저장하기 (0) | 2023.09.30 |
리액트 - Mui Drawer로 Handsontable Option 관리하기 (0) | 2023.09.30 |
댓글