본문 바로가기
개발/React

React Material - 파일 검색 후 Tree View 포커싱 (Focus Tree View with useRef)

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

리액트 전체 링크

 

참고

- useRef로 특정 위치로 포커스 이동하기

- Auto Complete로 목록 관리하기

- 파일 브라우저에 검색 기능 추가하기

- 파일 검색 후 Tree View 포커싱 

 

파일을 검색해서 expand와 select가 정상적으로 동작했지만, 

스크롤보다 아래에 있는 파일의 경우 포커싱이 되지 않는다.

 

 

이런 경우, useRef로 특정 위치로 포커스 이동하면 된다.

모든 TreeItemref를 설정해야하므로 배열로 선언하였다.

const treeFocus = useRef([]);

 

TreeItem에 nodeIdtreeFocuscurrent를 설정한다.

  const makeTreeItem = (info, parent) => {
    if (info.child === undefined) return;

    return info.child.map((item, idx) => (
      <TreeItem
        key={idx}
        nodeId={item.nodeId.toString()}
        
        /* fucos */
        ref={(val) => (treeFocus.current[item.nodeId] = val)}
        
        label={item.label}
        onClick={() => getFilesForFileBrowser(`${parent}/${item.label}`)}
      >
        {makeTreeItem(item, `${parent}/${item.label}`)}
      </TreeItem>
    ));
  };

 

그리고 selected가 변할 때, focus 메서드를 호출하도록 한다.

  useEffect(() => {
    if(selected.length === 0) return;
    setTimeout(() => {
      treeFocus.current[parseInt(idMap[value])].focus();
    }, 250);
  }, [selected])

 

마지막으로 Tree View를 클릭할 때 목록도 연동되도록 한다.

  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));
  };

 

이제 트리 뷰에 포커싱 기능도 추가되었다.

 

전체 코드는 다음과 같다.

 

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 TreeItem from "@mui/lab/TreeItem";
import FileUI from "./FileUI";

import Stack from "@mui/material/Stack";
import TextField from "@mui/material/TextField";
import Autocomplete from "@mui/material/Autocomplete";

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])

  const handleToggle = (event, ids) => {
    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;
반응형

댓글