반응형
참고
- withStyles로 TreeView css 커스터마이징 하기
트리 뷰에서 폴더를 클릭해서 파일 브라우저를 갱신했을 때,
트리 뷰가 접히거나 펼쳐지기 때문에 조금 정신이 없어보인다.
따라서 화살표는 클릭을 할 때, 라벨은 더블 클릭을 할 때만 트리 뷰가 펼쳐지거나 접히도록 해보자.
아쉽게도 TreeView에 onDoubleClick을 추가하더라도,
onNodeToggle이 먼저 발생하기 때문에 이 시점에서 이미 트리 뷰가 펼쳐지게 된다.
<TreeView
expanded={expanded}
onNodeToggle={handleToggle}
selected={selected}
onNodeSelect={handleSelect}
aria-label="file system navigator"
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ height: 500, overflowX: "hidden" }}
//sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: "auto" }}
>
따라서 onNodeToggle에 유니티 더블 클릭처럼 타이머를 추가하여 더블 클릭 효과를 구현한다.
withStyles로 라벨 구분하기
라벨을 구분하기 쉽게 withStyles로 TreeItem을 아래와 같이 커스터마이징하였다.
코드는 다음과 같다.
import MyTreeItem from "@mui/lab/TreeItem";
import { withStyles } from "@mui/styles";
const TreeItem = withStyles({
root: {
"&.MuiTreeItem-root > .MuiTreeItem-content:hover": {
background: "red",
},
"&.MuiTreeItem-root > .MuiTreeItem-content:hover > .MuiTreeItem-label": {
background: "pink",
},
"&.MuiTreeItem-root > .Mui-selected": {
background: "grey",
},
"@media (hover: none)": {
backgroundColor: "transparent",
},
},
})(MyTreeItem);
className 구분하기
화살표 버튼과 라벨을 쉽게 구분하는 방법은 className을 알아내는 것이다.
handleToggle에 아래와 같이 로그를 추가해보자.
const handleToggle = (event, ids) => {
console.log(event.target.className);
setExpanded(ids);
};
라벨을 클릭할 때는 MuiTreeItem-label이 출력되고, 화살표 버튼은 여러 css가 출력된다.
따라서 아래와 같이 clickTime을 저장하면서
짧은 시간에 handleToggle이 두 번 호출된 경우에만 setExpanded가 발생하도록 수정한다.
let clickTime = new Date();
const handleToggle = (event, ids) => {
let current = new Date();
if (event.target.className === "MuiTreeItem-label") {
let diff = current.getTime() - clickTime.getTime();
clickTime = new Date();
if (diff > 250) return;
}
clickTime = new Date();
setExpanded(ids);
};
이제 라벨을 더블 클릭하면 트리 뷰가 펼쳐지거나 접히게 된다.
전체 코드는 다음과 같다.
FileBrowser.js
import React, { useEffect, useState, useRef } from "react";
import Box from "@mui/material/Box";
import Button from "@mui/material/Button";
import TreeView from "@mui/lab/TreeView";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import FileUI from "./FileUI";
import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";
import MyTreeItem from "@mui/lab/TreeItem";
import { withStyles } from "@mui/styles";
const TreeItem = withStyles({
root: {
"&.MuiTreeItem-root > .MuiTreeItem-content:hover": {
background: "red",
},
"&.MuiTreeItem-root > .MuiTreeItem-content:hover > .MuiTreeItem-label": {
background: "pink",
},
"&.MuiTreeItem-root > .Mui-selected": {
background: "grey",
},
"@media (hover: none)": {
backgroundColor: "transparent",
},
},
})(MyTreeItem);
const FileBrowser = () => {
const [treeInfo, setTreeInfo] = useState({});
const [fileUi, setFileUI] = useState([]);
const [idMap, setIdMap] = useState({});
const [expanded, setExpanded] = useState([]);
const [selected, setSelected] = useState([]);
const [id, setId] = useState(0);
const [value, setValue] = useState(null);
const [dirList, setDirList] = useState([]);
const treeFocus = useRef([]);
let nodeId = 0;
const appendChild = (arr, info, path, tmpIdMap) => {
if (arr.child === undefined) arr.child = [];
if (arr.child.findIndex((item) => item.label === info) === -1) {
arr.child.push({ label: info, nodeId });
tmpIdMap[path] = nodeId.toString();
nodeId++;
}
};
const makeDirectories = (directories) => {
let tmpTreeInfo = {};
let tmpIdMap = {};
let dir = ["D:", "D:/github", ...directories];
for (let d of dir) {
let split = d.split("/");
let len = split.length;
let current = tmpTreeInfo;
for (let i = 0; i < len; i++) {
appendChild(current, split[i], d, tmpIdMap);
current = current.child.find((item) => item.label === split[i]);
}
}
setDirList(directories);
setIdMap(tmpIdMap);
setTreeInfo(tmpTreeInfo);
setId(nodeId);
};
const getFiles = () => {
// setTreeInfo(localData);
// return;
let server = `http://192.168.55.120:3002`;
let path = `D:\\github\\globfiles\\**`;
fetch(`${server}/useGlob?path=${path}`)
.then((res) => res.json())
.then((data) => makeDirectories(data.findPath));
};
const isFile = (path) => {
let spt = path.split("/");
return spt[spt.length - 1].includes(".");
};
const sortFileUI = (path) => {
let dir = path.filter((val) => isFile(val) === false);
let files = path.filter((val) => isFile(val));
dir.sort();
files.sort();
setFileUI([...dir, ...files]);
};
const getFilesForFileBrowser = (path) => {
let server = `http://192.168.55.120:3002`;
/* /D:/... 앞의 / 삭제 */
path = path.substring(1, path.length);
setValue(path);
fetch(`${server}/useGlob?path=${path}/*`)
.then((res) => res.json())
.then((data) => sortFileUI(data.findPath));
};
const makeTreeItem = (info, parent) => {
if (info.child === undefined) return;
return info.child.map((item, idx) => (
<TreeItem
key={idx}
nodeId={item.nodeId.toString()}
ref={(val) => (treeFocus.current[item.nodeId] = val)}
label={item.label}
onClick={() => getFilesForFileBrowser(`${parent}/${item.label}`)}
>
{makeTreeItem(item, `${parent}/${item.label}`)}
</TreeItem>
));
};
useEffect(() => {
getFiles();
}, []);
useEffect(() => {
if (selected.length === 0) return;
setTimeout(() => {
treeFocus.current[parseInt(idMap[value])].focus();
}, 250);
}, [selected]);
let clickTime = new Date();
const handleToggle = (event, ids) => {
let current = new Date();
if (event.target.className === "MuiTreeItem-label") {
let diff = current.getTime() - clickTime.getTime();
clickTime = new Date();
if (diff > 250) return;
}
clickTime = new Date();
setExpanded(ids);
};
const handleSelect = (event, ids) => {
setSelected(ids);
};
const handleExpandClick = () => {
let fullExpanded = [];
for (let i = 0; i <= id; i++) fullExpanded.push(i.toString());
setExpanded((oldExpanded) =>
oldExpanded.length === 0 ? fullExpanded : []
);
};
const makeExpandedView = (pathInfo) => {
let tmp = ["0", "1", ...expanded];
let spt = pathInfo.split("/");
let tmpPath = "D:";
for (let i = 1; i < spt.length; i++) {
tmpPath += `/${spt[i]}`;
if (idMap[tmpPath] === undefined) continue;
if (tmp.includes(idMap[tmpPath])) continue;
tmp.push(idMap[tmpPath]);
}
setSelected(idMap[pathInfo]);
setExpanded([...tmp]);
};
const defaultProps = {
options: dirList,
getOptionLabel: (option) => option,
};
return (
<div>
<Stack spacing={1} sx={{ width: "80%", marginLeft: 5, paddingBottom: 3 }}>
<Autocomplete
{...defaultProps}
id="auto-complete"
autoComplete
includeInputInList
value={value}
onChange={(event, newValue) => {
setValue(newValue);
getFilesForFileBrowser(`/${newValue}`);
makeExpandedView(newValue);
}}
renderInput={(params) => (
<TextField {...params} label="Search Files" variant="standard" />
)}
/>
</Stack>
<div
style={{
display: "grid",
gridTemplateColumns: "25% 1% auto",
gridGap: "25px",
width: "100%",
}}
>
{/* <button onClick={getFiles}>test</button> */}
<div>
{/* <a
href="https://bloodstrawberry.tistory.com/1175"
target="_blank"
rel="noreferrer"
>
Node Server 구현 필요
</a> */}
<Box sx={{ mb: 1 }}>
<Button onClick={handleExpandClick}>
{expanded.length === 0 ? "Expand all" : "Collapse all"}
</Button>
</Box>
<TreeView
expanded={expanded}
onNodeToggle={handleToggle}
selected={selected}
onNodeSelect={handleSelect}
aria-label="file system navigator"
defaultCollapseIcon={<ExpandMoreIcon />}
defaultExpandIcon={<ChevronRightIcon />}
sx={{ height: 500, overflowX: "hidden" }}
//sx={{ height: 240, flexGrow: 1, maxWidth: 400, overflowY: "auto" }}
>
{makeTreeItem(treeInfo, "")}
</TreeView>
</div>
<div style={{ borderRight: "2px solid black" }} />
<div>
{fileUi.map((f, idx) => (
<FileUI
key={idx}
pathInfo={f}
idMap={idMap}
tvfunc={{ expanded, setExpanded, setSelected, sortFileUI }}
/>
))}
</div>
</div>
</div>
);
};
export default FileBrowser;
반응형
댓글