이전 - (14) handsontable methods
현재 - (15) callback function으로 선택된 Cell 보여주기 / 수정하기
다음 - (16) colWidths 옵션 / csv 파일 파싱 보완
handsontable option으로 colWidths와 wordWrap = false을 추가해보자.
myTable = new Handsontable(container, {
data: lib.makeTable(csvFile, 2, 3),
colHeaders: true, /* column header는 보이게 설정 */
rowHeaders: true, /* row header 보이게 설정 */
colWidths: [60, 60, 60, 60, 60, 60, 60],
wordWrap: false, /* 줄 바꿈 x */
width: "50%",
...
});
colWidths를 강제로 정하였으므로, column의 width가 고정된다.
왼쪽의 경우 wordWrap option이 없는 경우(default : true)이고 오른쪽은 false를 준 경우다.
문자열의 길이가 일정치 않다면, 왼쪽의 경우가 조금 더 산만할 수 있다.
따라서 격자의 길이가 일정한 것을 선호한다면 wordWrap false로 오른쪽이 되도록 하면 된다.
문제는 문자열의 길이가 길 때, cell을 클릭해도 모든 문자열을 보기 힘들다는 것이다.
실제 엑셀에서는 셀을 클릭하면 수식 입력줄에 모든 문자열이 보인다.
handsontable의 method와 callback function을 이용하여 이름 상자 + 수식 입력줄을 구현해보자.
위의 내용을 구현하기 위해 hansdontable의 아래 메서드가 필요하다.
getValue() - 현재 선택된 Cell의 값을 return 한다.
getSelectedLast() - 현재 선택된 Cell의 범위를 return 한다.
(return [row start, column start, row end, column end])
setDataAtCell(row, col, value) - table의 row, column의 값을 value로 변경한다.
그리고 callback function : afterSelection이 필요하다.
afterSelection은 table을 선택할 때마다 발생하는 이벤트이다.
(callback function의 종류는 링크를 참고하자.)
구현 내용은 MyTable을 아래와 같이 수정하면 된다.
//MyTable.js
import React, { useEffect, useState } from "react";
import * as lib from "./library.js";
import "handsontable/dist/handsontable.full.css";
import Handsontable from "handsontable";
let myTable;
let currentRow, currentColumn;
const csvDownLoad = () => { ... };
const MyTable = ({ csvFile }) => {
const [displayIndex, setDisplayIndex] = useState("");
const [displayCell, setDisplayCell] = useState("");
const selectCell = () => {
let selected = myTable.getSelectedLast();
currentRow = selected[0];
currentColumn = selected[1];
if(currentRow < 0 || currentColumn < 0) return;
setDisplayCell(myTable.getValue());
setDisplayIndex(`${lib.rowToAlpha(currentColumn + 1)}${currentRow + 1}`);
}
const setValueCell = (e) => {
if(currentRow < 0 || currentColumn < 0) return;
setDisplayCell(e.target.value);
myTable.setDataAtCell(currentRow, currentColumn, e.target.value);
}
const init = (csvFile) => {
if (csvFile === undefined || csvFile.HEIGHT === 0) return;
const container = document.getElementById("hot-app");
myTable = new Handsontable(container, {
data: lib.makeTable(csvFile, 2, 3),
colHeaders: true, /* column header는 보이게 설정 */
rowHeaders: true, /* row header 보이게 설정 */
colWidths: [60, 60, 60, 60, 60, 60, 60],
wordWrap: false, /* 줄 바꿈 x */
width: "50%",
manualColumnResize: true, /* column 사이즈 조절 */
manualRowResize: true, /* row 사이즈 조절 */
manualColumnMove: true, /* column move 허용 */
manualRowMove: true, /* row move 허용 */
dropdownMenu: true, /* dropdown 메뉴 설정 */
filters: true, /* 필터 기능 on */
contextMenu: true, /* cell 클릭 시 메뉴 설정 */
licenseKey: "non-commercial-and-evaluation",
afterSelection: selectCell,
});
};
useEffect(() => {
init(csvFile);
}, [csvFile]);
return (
<div>
<button onClick={csvDownLoad}>DOWNLOAD</button>
<div>
<span>{displayIndex}</span>
<input value={displayCell} onChange={setValueCell} />
</div>
<div id="hot-app">
</div>
</div>
);
};
export default MyTable;
currentRow와 currentColumn을 선언한다.
let myTable;
let currentRow, currentColumn;
그리고 useState를 추가한다.
displayIndex는 이름 상자의 역할을 하고, displayCell은 수식 입력줄의 역할을 한다.
또한 useState로 선언된 display 변수를 사용하기 위해 init 함수를 MyTable 안으로 옮겼다.
(이전까지 const MyTable = ({ csvFile }) => { ... } 외부에 있었다.)
const [displayIndex, setDisplayIndex] = useState("");
const [displayCell, setDisplayCell] = useState("");
display 변수를 보기 위해 csvDownload 버튼 아래에 span과 input을 추가한다.
input에는 onChange 이벤트에 setValueCell(나중에 설명)을 연결한다.
<div>
<span>{displayIndex}</span>
<input value={displayCell} onChange={setValueCell} />
</div>
myTable에는 afterSelection에 selectCell을 추가하였다.
이제 cell을 Select, 선택하면 selectCell 함수가 호출된다.
myTable = new Handsontable(container, {
data: lib.makeTable(csvFile, 2, 3),
...
wordWrap: false, /* 줄 바꿈 x */
...
licenseKey: "non-commercial-and-evaluation",
afterSelection: selectCell,
});
셀이 선택되면, 이름 상자에 현재 row/column이 보이고, 수식 입력줄에는 셀의 값이 보여야한다.
getValue를 이용해 선택된 셀의 값을 setDisplayCell에 넘겨주면 displayCell이 변한다.
getSelectedLast로 선택된 셀의 row/column을 알 수 있으므로 setDisplayIndex로 displayIndex를 변경한다.
row, col이 음수일 때는 아무 동작도 하지 않도록 방어 코드를 추가한다.
const selectCell = () => {
let selected = myTable.getSelectedLast();
currentRow = selected[0];
currentColumn = selected[1];
if(currentRow < 0 || currentColumn < 0) return;
setDisplayCell(myTable.getValue());
setDisplayIndex(`${lib.rowToAlpha(currentColumn + 1)}${currentRow + 1}`);
}
row와 column은 0부터 시작하지만, table은 1부터 시작하므로 1을 더해준다.
그리고 row는 알파벳으로 보여야하므로, rowToAlpha 함수로 row에 대한 alphabet을 변환한다.
해당 함수는 library에 추가하였다.
// library.js
export const rowToAlpha = (row) => {
const numToAlpha = [
"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",
];
if (row <= 26) return numToAlpha[row - 1];
if (26 < row && row <= 26 * 26) {
let front = numToAlpha[parseInt(row / 26) - 1];
let back = numToAlpha[(row % 26) - 1];
if (row % 26 === 0) {
back = "Z";
front = numToAlpha[parseInt(row / 26) - 2];
}
return front + back;
}
return "too_long";
}
row에 대한 알파벳의 규칙이 까다로워서 A ~ ZZ 까지만 정상적으로 나올 수 있도록 만들었다.
이제 변수가 길어도 셀을 선택하면 display에서 문자열을 모두 확인할 수 있다.
setValueCell은 반대로 input에서 값을 입력하면 table 값이 변경되도록 한다.
위에서 input에 onChange 이벤트에 setValueCell을 연결해두었다.
input값이 변경되면 displayCell도 변경되고, 저장해둔 currentRow/Column으로 table의 값도 수정할 수 있다.
마찬가지로 row, col이 음수일 때는 아무 동작도 하지 않도록 방어 코드를 추가한다.
const setValueCell = (e) => {
if(currentRow < 0 || currentColumn < 0) return;
setDisplayCell(e.target.value);
myTable.setDataAtCell(currentRow, currentColumn, e.target.value);
}
이제 수식 입력줄에서 cell을 변경하면 table도 같이 변경되는 것을 볼 수 있다.
최종 코드는 아래와 같다.
//MyTable.js
import React, { useEffect, useState } from "react";
import * as lib from "./library.js";
import "handsontable/dist/handsontable.full.css";
import Handsontable from "handsontable";
let myTable;
let currentRow, currentColumn;
const getCell = (cell) => {
if(cell === null) return ``;
return cell.includes(",") ? `"${cell}"` : `${cell}`;
}
const csvDownLoad = () => {
let rows = myTable.countRows();
let cols = myTable.countCols();
let tmpTables = myTable.getData(0, 0, rows - 1, cols - 1);
let maxRow, maxCol;
maxCol = 0;
for(let r = 0; r < rows; r++) {
for(let c = cols - 1; c >=0; c--) {
if(!(tmpTables[r][c] === "" || tmpTables[r][c] === null)) {
maxCol = (maxCol < c) ? c : maxCol;
break;
}
}
}
maxRow = 0;
for(let c = 0; c < cols; c++) {
for(let r = rows - 1; r >=0; r--) {
if(!(tmpTables[r][c] === "" || tmpTables[r][c] === null)) {
maxRow = (maxRow < r) ? r : maxRow;
break;
}
}
}
let parsing = myTable.getData(0, 0, maxRow, maxCol)
.map((item) => item.map((cell) => getCell(cell)));
let realTable = parsing.map((item) => item.join(",")).join("\n");
lib.downLoadCsv(realTable);
return;
};
const MyTable = ({ csvFile }) => {
const [displayIndex, setDisplayIndex] = useState("");
const [displayCell, setDisplayCell] = useState("");
const selectCell = () => {
let selected = myTable.getSelectedLast();
currentRow = selected[0];
currentColumn = selected[1];
if(currentRow < 0 || currentColumn < 0) return;
setDisplayCell(myTable.getValue());
setDisplayIndex(`${lib.rowToAlpha(currentColumn + 1)}${currentRow + 1}`);
}
const setValueCell = (e) => {
if(currentRow < 0 || currentColumn < 0) return;
setDisplayCell(e.target.value);
myTable.setDataAtCell(currentRow, currentColumn, e.target.value);
}
const init = (csvFile) => {
if (csvFile === undefined || csvFile.HEIGHT === 0) return;
const container = document.getElementById("hot-app");
myTable = new Handsontable(container, {
data: lib.makeTable(csvFile, 2, 3),
colHeaders: true, /* column header는 보이게 설정 */
rowHeaders: true, /* row header 보이게 설정 */
colWidths: [60, 60, 60, 60, 60, 60, 60],
wordWrap: false, /* 줄 바꿈 x */
width: "50%",
manualColumnResize: true, /* column 사이즈 조절 */
manualRowResize: true, /* row 사이즈 조절 */
manualColumnMove: true, /* column move 허용 */
manualRowMove: true, /* row move 허용 */
dropdownMenu: true, /* dropdown 메뉴 설정 */
filters: true, /* 필터 기능 on */
contextMenu: true, /* cell 클릭 시 메뉴 설정 */
licenseKey: "non-commercial-and-evaluation",
afterSelection: selectCell,
});
};
useEffect(() => {
init(csvFile);
}, [csvFile]);
return (
<div>
<button onClick={csvDownLoad}>DOWNLOAD</button>
<div>
<span>{displayIndex}</span>
<input value={displayCell} onChange={setValueCell} />
</div>
<div id="hot-app">
</div>
</div>
);
};
export default MyTable;
// library.js
const DELIMITER = ',';
const APOSTROPHE = '"';
export const handleOnDragLeave = (e, setState) => {
e.preventDefault();
setState(false);
return false;
};
export const handleDragOver = (e, setState) => {
e.preventDefault();
setState(true);
return false;
};
export const handleOnDrop = (e, setState, setCsvObject) => {
e.preventDefault();
let file = e.dataTransfer.files[0];
let fileReader = new FileReader();
fileReader.readAsText(file, "utf-8"); // or euc-kr
fileReader.onload = function () {
//console.log(fileReader.result);
parsingCsv(fileReader.result, setCsvObject);
return;
};
setState(false);
return false;
};
export const handleUpload = (e, setCsvObject) => {
let file = e.target.files[0];
let fileReader = new FileReader();
if(file === undefined) return; /* 방어 코드 추가 */
fileReader.readAsText(file, "utf-8"); // or euc-kr
fileReader.onload = function () {
//console.log(fileReader.result);
parsingCsv(fileReader.result, setCsvObject);
};
}
export const mySplit = (line, delimiter, ignore) => {
let spt = [];
let tmp = "";
let flag = false;
for(let i = 0; i < line.length; i++)
{
if(ignore === line[i] && flag === true)
{
flag = false;
continue;
}
else if(ignore === line[i])
{
flag = true;
continue;
}
if(line[i] === delimiter && flag === false) {
spt.push(tmp);
tmp = "";
continue;
}
tmp += line[i];
}
spt.push(tmp);
return spt;
}
export const parsingCsv = (file, setCsvObject) => {
let height, width;
let obj = {
HEIGHT: 0,
WIDTH: 0,
csv: [],
};
obj.csv = [];
let sptLine = file.split(/\r\n|\n/);
console.log(sptLine);
height = 0;
for(let line of sptLine)
{
if(line === "") continue;
let spt = mySplit(line, DELIMITER, APOSTROPHE);
obj.csv.push(spt);
height++;
}
width = obj.csv[0].length;
obj.HEIGHT = height;
obj.WIDTH = width;
setCsvObject(obj);
return;
}
export const makeTable = (csvObject, height, width) => {
let table = [];
for(let h = 0; h < csvObject.HEIGHT; h++)
{
let line = [];
for(let w = 0; w < csvObject.WIDTH; w++) line.push(csvObject.csv[h][w]);
for(let w = 0; w < width; w++) line.push("");
table.push(line);
}
for(let h = 0; h < height; h++)
{
let dummy = [];
for(let w = 0; w < csvObject.WIDTH + width; w++) dummy.push("");
table.push(dummy);
}
return table;
}
export const downLoadCsv = (contents, fileName = "MyFile.csv") => {
let fileDown = "data:csv;charset=utf-8," + contents;
let encodedUri = encodeURI(fileDown);
let link = document.createElement("a");
link.setAttribute("href", encodedUri);
link.setAttribute("download", fileName);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
export const rowToAlpha = (row) => {
const numToAlpha = [
"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",
];
if (row <= 26) return numToAlpha[row - 1];
if (26 < row && row <= 26 * 26) {
let front = numToAlpha[parseInt(row / 26) - 1];
let back = numToAlpha[(row % 26) - 1];
if (row % 26 === 0) {
back = "Z";
front = numToAlpha[parseInt(row / 26) - 2];
}
return front + back;
}
return "too_long";
}
'개발 > React' 카테고리의 다른 글
React Handsontable로 csv 편집기 만들기 (17) (0) | 2021.06.26 |
---|---|
React Handsontable로 csv 편집기 만들기 (16) (0) | 2021.06.14 |
React Handsontable로 csv 편집기 만들기 (14) (0) | 2021.06.11 |
React Handsontable로 csv 편집기 만들기 (13) (0) | 2021.06.09 |
React Handsontable로 csv 편집기 만들기 (12) (0) | 2021.06.08 |
댓글