본문 바로가기
개발/React

React Material - Mui Tree View 더블 클릭으로 펼치기 (Expanded Tree View with Double Click)

by 피로물든딸기 2023. 6. 19.
반응형

리액트 전체 링크

 

참고

- 유니티 더블 클릭 구현하기

- 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;
반응형

댓글