참고
- https://chonky.io/docs/2.x/file-actions/custom-actions
- 파일 브라우저 만들기
- chonky 기본 설정 확인하기
- 액션 추가하고 다크 모드 구현하기
- 커스텀 액션 추가하기
- Chonky 파일 맵 만들기
- 리포지토리의 폴더 정보 저장하기
- 깃허브 리포지토리를 파일 브라우저로 불러오기
- useNavigate로 Toast UI Editor 이동하기
다음과 같이 커스텀 액션을 추가해 보자.
액션 정의하기
myCustomAction.js를 만들고 defineFileAction과 ChonkyIconName을 import 하자.
import { defineFileAction, ChonkyIconName } from "chonky";
참고로 ChonkyIcon에는 여러 아이콘이 정의되어 있다.
export declare enum ChonkyIconName {
loading = "loading",
dropdown = "dropdown",
placeholder = "placeholder",
...
}
Action을 정의하는 예시는 다음과 같다.
id와 button 구성요소를 정의한다.
requiersSelection을 true로 설정하고 fileFilter 조건을 설정하면 특정 파일 / 폴더에 대해서 액션을 활성화한다.
그리고 toolbar와 contextMenu에 나타낼지 여부를 선택할 수 있다.
const viewFileAction = defineFileAction({
id: "view",
requiresSelection: true,
fileFilter: (file) => file && !file.isDir,
button: {
name: "View",
toolbar: false,
contextMenu: true,
icon: ChonkyIconName.file
}
});
view 이벤트가 발생할 경우 action에서 id가 "view" 인 것을 알 수 있다.
contextMenu가 true이기 때문에 메뉴에 나타난다.
또한 fileFilter 조건에 의해, 폴더에서는 비활성화되고 있다.
다른 액션은 toolbar를 true로 설정해 보자.
const uploadFileAction = defineFileAction({
id: "upload",
button: {
name: "Upload",
toolbar: true,
contextMenu: false,
icon: ChonkyIconName.upload
}
});
툴바에 Upload 액션이 추가되었다.
myCustomActions.js는 다음과 같다.
import { defineFileAction, ChonkyIconName } from "chonky";
const uploadFileAction = defineFileAction({
id: "upload",
button: {
name: "Upload",
toolbar: true,
contextMenu: false,
icon: ChonkyIconName.upload
}
});
const viewFileAction = defineFileAction({
id: "view",
requiresSelection: true,
fileFilter: (file) => !file.isDir,
button: {
name: "View",
toolbar: false,
contextMenu: true,
icon: ChonkyIconName.file
}
});
export const customActions = [
viewFileAction,
uploadFileAction
];
이제 ChonkyBrowser.js에 customAcitons를 import 한다.
import { customActions } from "./myCustomActions";
fileActions에 customActions를 추가하면 된다.
const fileActions = useMemo(
() => [
...customActions,
ChonkyActions.CreateFolder,
ChonkyActions.DeleteFiles,
ChonkyActions.ToggleDarkMode,
],
[]
);
선택한 파일의 전체 경로 출력하기 (View)
선택한 파일의 전체 경로를 로그로 출력해 보자.
커스텀 액션을 추가하였으므로 useFileActionHandler에 구현을 추가하면 된다.
const useFileActionHandler = (...) => {
return useCallback(
(data) => {
...
} else if (data.id === ChonkyActions.ToggleDarkMode.id) {
toggleDarkMode();
} else if (data.id === "view") {
console.log(data);
}
},
[createFolder, deleteFiles, moveFiles, setCurrentFolderId, toggleDarkMode]
);
};
파일을 선택하고 view 액션을 실행하면 아래와 같이 파일의 이름이 전달된다.
그리고 folderChain에는 현재 브라우저에 대한 directory 정보를 알 수 있다.
현재 브라우저는 Chonkyz Demo / Chonky Source Code / packages / chonky-icon-fontawesome 이 된다.
따라서 전체 경로(dirPath) + 파일 이름(fileNames)을 map으로 정리해서 출력하면 된다.
} else if (data.id === "view") {
let fileNames = data.state.selectedFiles.map(item => item.name);
let dirPath = folderChain.map(item => item.name).join("/");
for(let name of fileNames) {
console.log(`${dirPath}/${name}`);
}
}
useFileActionHandler에서 folderChain을 사용하기 위해 파라미터를 추가하였다.
const useFileActionHandler = (
folderChain,
setCurrentFolderId,
...
) => {
...
const handleFileAction = useFileActionHandler(
folderChain,
setCurrentFolderId,
...
);
파일을 선택하고 View 액션을 실행해 보자.
파일 업로드
파일을 업로드하기 위해 useRef를 선언한다.
input을 이용하여 파일을 업로드하는데, input 버튼 자체는 감추기 때문이다.
const fileInputRef = useRef(null);
input은 다음과 같다.
json과 md 확장자만 허용하고, 여러 파일(multiple)을 업로드할 수 있도록 설정한다.
<input
type="file"
ref={fileInputRef}
accept=".json,.md"
style={{ display: "none" }}
multiple
onChange={(e) => fileUpload(e)}
/>
useFileActionHandler에 fileInputRef를 추가한다.
const useFileActionHandler = (
folderChain,
setCurrentFolderId,
...
fileInputRef
) => {
...
const handleFileAction = useFileActionHandler(
folderChain,
setCurrentFolderId,
...
fileInputRef
);
업로드 액션이 실행되면 감춰진 input을 click만 하면 된다.
} else if (data.id === "upload") {
fileInputRef.current.click();
}
즉, 윈도우 파일 업로드 브라우저가 나오게 된다.
이제 구체적으로 fileUpload 이벤트를 구현해 보자.
e.target.files에서 파일의 이름과 파일의 내용을 알 수 있다.
const fileUpload = async (e) => {
let files = e.target.files;
for(let file of files) {
if (file === undefined) continue;
console.log(file.name); // 파일 이름
// 실제 파일 내용 read
let fileReader = new FileReader();
fileReader.readAsText(file, "utf-8"); // or euc-kr
fileReader.onload = function () {
console.log(fileReader.result);
};
}
};
실제 파일을 업로드하지 않으므로, 파일 내용을 읽는 부분은 주석 처리하고 파일을 업로드해보자.
파일의 이름이 정상적으로 출력되는 것을 알 수 있다.
이제 파일 이름을 얻을 수 있으므로, fileMap을 갱신해 보자.
createFile의 원리는 createFolder와 같고 isDir 부분만 제거하면 된다.
const createFile = useCallback((fileName) => {
setFileMap((currentFileMap) => {
const newFileMap = { ...currentFileMap };
const newFolderId = `new-folder-${idCounter.current++}`;
newFileMap[newFolderId] = {
id: newFolderId,
name: fileName,
modDate: new Date(),
parentId: currentFolderIdRef.current,
childrenIds: [],
childrenCount: 0,
};
const parent = newFileMap[currentFolderIdRef.current];
newFileMap[currentFolderIdRef.current] = {
...parent,
childrenIds: [...parent.childrenIds, newFolderId],
};
return newFileMap;
});
}, []);
useCustomFileMap에 createFile도 추가한다.
const useCustomFileMap = () => {
const createFile = useCallback((fileName) => {
...
}, []);
return {
...
createFolder,
createFile,
};
};
...
const {
...
createFolder,
createFile,
} = useCustomFileMap();
fileUpload는 다음과 같이 수정하면 된다.
const fileUpload = async (e) => {
let files = e.target.files;
for(let file of files) {
if (file === undefined) continue;
createFile(file.name);
}
};
이제 파일을 업로드하면 브라우저에 파일이 추가된다.
중복 파일이 있는 경우 등의 예외 처리는 생략하였다.
전체 코드는 다음과 같다.
ChonkyBrowser.js
import React, {
useState,
useCallback,
useEffect,
useRef,
useMemo,
} from "react";
import {
setChonkyDefaults,
ChonkyActions,
FileHelper,
FullFileBrowser,
} from "chonky";
import { ChonkyIconFA } from "chonky-icon-fontawesome";
import { customActions } from "./myCustomActions";
import Box from "@mui/material/Box";
const demoMap = require("./demo.json");
const prepareCustomFileMap = () => {
const baseFileMap = demoMap.fileMap;
const rootFolderId = demoMap.rootFolderId;
return { baseFileMap, rootFolderId };
};
const useCustomFileMap = () => {
const { baseFileMap, rootFolderId } = useMemo(prepareCustomFileMap, []);
const [fileMap, setFileMap] = useState(baseFileMap);
const [currentFolderId, setCurrentFolderId] = useState(rootFolderId);
const resetFileMap = useCallback(() => {
setFileMap(baseFileMap);
setCurrentFolderId(rootFolderId);
}, [baseFileMap, rootFolderId]);
const currentFolderIdRef = useRef(currentFolderId);
useEffect(() => {
currentFolderIdRef.current = currentFolderId;
}, [currentFolderId]);
const deleteFiles = useCallback((files) => {
setFileMap((currentFileMap) => {
const newFileMap = { ...currentFileMap };
files.forEach((file) => {
delete newFileMap[file.id];
if (file.parentId) {
const parent = newFileMap[file.parentId];
const newChildrenIds = parent.childrenIds.filter(
(id) => id !== file.id
);
newFileMap[file.parentId] = {
...parent,
childrenIds: newChildrenIds,
childrenCount: newChildrenIds.length,
};
}
});
return newFileMap;
});
}, []);
const moveFiles = useCallback((files, source, destination) => {
setFileMap((currentFileMap) => {
const newFileMap = { ...currentFileMap };
const moveFileIds = new Set(files.map((f) => f.id));
const newSourceChildrenIds = source.childrenIds.filter(
(id) => !moveFileIds.has(id)
);
newFileMap[source.id] = {
...source,
childrenIds: newSourceChildrenIds,
childrenCount: newSourceChildrenIds.length,
};
const newDestinationChildrenIds = [
...destination.childrenIds,
...files.map((f) => f.id),
];
newFileMap[destination.id] = {
...destination,
childrenIds: newDestinationChildrenIds,
childrenCount: newDestinationChildrenIds.length,
};
files.forEach((file) => {
newFileMap[file.id] = {
...file,
parentId: destination.id,
};
});
return newFileMap;
});
}, []);
const idCounter = useRef(0);
const createFolder = useCallback((folderName) => {
setFileMap((currentFileMap) => {
const newFileMap = { ...currentFileMap };
const newFolderId = `new-folder-${idCounter.current++}`;
newFileMap[newFolderId] = {
id: newFolderId,
name: folderName,
isDir: true,
modDate: new Date(),
parentId: currentFolderIdRef.current,
childrenIds: [],
childrenCount: 0,
};
const parent = newFileMap[currentFolderIdRef.current];
newFileMap[currentFolderIdRef.current] = {
...parent,
childrenIds: [...parent.childrenIds, newFolderId],
};
return newFileMap;
});
}, []);
const createFile = useCallback((fileName) => {
setFileMap((currentFileMap) => {
const newFileMap = { ...currentFileMap };
const newFolderId = `new-folder-${idCounter.current++}`;
newFileMap[newFolderId] = {
id: newFolderId,
name: fileName,
modDate: new Date(),
parentId: currentFolderIdRef.current,
childrenIds: [],
childrenCount: 0,
};
const parent = newFileMap[currentFolderIdRef.current];
newFileMap[currentFolderIdRef.current] = {
...parent,
childrenIds: [...parent.childrenIds, newFolderId],
};
return newFileMap;
});
}, []);
return {
fileMap,
currentFolderId,
setCurrentFolderId,
resetFileMap,
deleteFiles,
moveFiles,
createFolder,
createFile,
};
};
const useFiles = (fileMap, currentFolderId) => {
return useMemo(() => {
const currentFolder = fileMap[currentFolderId];
const childrenIds = currentFolder.childrenIds;
const files = childrenIds.map((fileId) => fileMap[fileId]);
return files;
}, [currentFolderId, fileMap]);
};
const useFolderChain = (fileMap, currentFolderId) => {
return useMemo(() => {
const currentFolder = fileMap[currentFolderId];
const folderChain = [currentFolder];
let parentId = currentFolder.parentId;
while (parentId) {
const parentFile = fileMap[parentId];
if (parentFile) {
folderChain.unshift(parentFile);
parentId = parentFile.parentId;
} else {
break;
}
}
return folderChain;
}, [currentFolderId, fileMap]);
};
const useFileActionHandler = (
folderChain,
setCurrentFolderId,
deleteFiles,
moveFiles,
createFolder,
toggleDarkMode,
fileInputRef
) => {
return useCallback(
(data) => {
if (data.id === ChonkyActions.OpenFiles.id) {
const { targetFile, files } = data.payload;
const fileToOpen = targetFile || files[0];
if (fileToOpen && FileHelper.isDirectory(fileToOpen)) {
setCurrentFolderId(fileToOpen.id);
return;
}
} else if (data.id === ChonkyActions.DeleteFiles.id) {
deleteFiles(data.state.selectedFilesForAction);
} else if (data.id === ChonkyActions.MoveFiles.id) {
moveFiles(
data.payload.files,
data.payload.source,
data.payload.destination
);
} else if (data.id === ChonkyActions.CreateFolder.id) {
const folderName = prompt("Provide the name for your new folder:");
if (folderName) createFolder(folderName);
} else if (data.id === ChonkyActions.ToggleDarkMode.id) {
toggleDarkMode();
} else if (data.id === "view") {
let fileNames = data.state.selectedFiles.map((item) => item.name);
let dirPath = folderChain.map((item) => item.name).join("/");
for (let name of fileNames) {
console.log(`${dirPath}/${name}`);
}
} else if (data.id === "upload") {
fileInputRef.current.click();
}
console.log(data);
},
[createFolder, deleteFiles, moveFiles, setCurrentFolderId, toggleDarkMode]
);
};
const ChonkyBrowser = React.memo((props) => {
const [darkMode, setDarkMode] = useState(false);
const toggleDarkMode = () => {
setDarkMode(!darkMode);
};
const fileInputRef = useRef(null);
const {
fileMap,
currentFolderId,
setCurrentFolderId,
deleteFiles,
moveFiles,
createFolder,
createFile,
} = useCustomFileMap();
setChonkyDefaults({ iconComponent: ChonkyIconFA });
const files = useFiles(fileMap, currentFolderId);
const folderChain = useFolderChain(fileMap, currentFolderId);
const handleFileAction = useFileActionHandler(
folderChain,
setCurrentFolderId,
deleteFiles,
moveFiles,
createFolder,
toggleDarkMode,
fileInputRef
);
const fileActions = useMemo(
() => [
...customActions,
ChonkyActions.CreateFolder,
ChonkyActions.DeleteFiles,
// ChonkyActions.CopyFiles,
// ChonkyActions.UploadFiles,
// ChonkyActions.DownloadFiles,
ChonkyActions.ToggleDarkMode,
],
[]
);
const thumbnailGenerator = useCallback(
(file) =>
file.thumbnailUrl ? `https://chonky.io${file.thumbnailUrl}` : null,
[]
);
const fileUpload = async (e) => {
let files = e.target.files;
for (let file of files) {
if (file === undefined) continue;
console.log(file.name); // 파일 이름
createFile(file.name);
// 실제 파일 내용 read
// let fileReader = new FileReader();
// fileReader.readAsText(file, "utf-8"); // or euc-kr
// fileReader.onload = function () {
// console.log(fileReader.result);
// };
}
};
return (
<Box sx={{ m: 2 }}>
<input
type="file"
ref={fileInputRef}
accept=".json,.md"
style={{ display: "none" }}
multiple
onChange={(e) => fileUpload(e)}
/>
<div style={{ height: 400 }}>
<FullFileBrowser
files={files}
folderChain={folderChain}
fileActions={fileActions}
onFileAction={handleFileAction}
thumbnailGenerator={thumbnailGenerator}
// disableDefaultFileActions={true} // default false
// doubleClickDelay={500} // ms
// disableSelection={true} // default false 파일 선택이 해제됨
// disableDragAndDrop={true} // 드래그 앤 드랍 기능 off
// disableDragAndDropProvider={true} // default false, Provider : 다른 드래그 앤 드롭은 유지
// defaultSortActionId={ChonkyActions.SortFilesByDate.id} // SortFilesByName, SortFilesBySize, SortFilesByDate
// defaultFileViewActionId={ChonkyActions.EnableListView.id} // EnableGridView, EnableListView
// clearSelectionOnOutsideClick={false} // default true 브라우저 외부 클릭 시 파일 선택 해제
darkMode={darkMode}
{...props}
/>
</div>
</Box>
);
});
export default ChonkyBrowser;
myCustomAction.js
import { defineFileAction, ChonkyIconName } from "chonky";
const uploadFileAction = defineFileAction({
id: "upload",
button: {
name: "Upload",
toolbar: true,
contextMenu: false,
icon: ChonkyIconName.upload
}
});
const viewFileAction = defineFileAction({
id: "view",
requiresSelection: true,
fileFilter: (file) => !file.isDir,
button: {
name: "View",
toolbar: false,
contextMenu: true,
icon: ChonkyIconName.file
}
});
export const customActions = [
viewFileAction,
uploadFileAction
];
'개발 > React' 카테고리의 다른 글
리액트 - useNavigate로 Toast UI Editor 이동하기 (Toast UI Editor with Chonky Browser) (1) | 2024.03.16 |
---|---|
리액트 - 깃허브 리포지토리를 파일 브라우저로 불러오기 (Chonky Browser with GitHub Repository) (0) | 2024.03.16 |
리액트 - 액션 추가하고 다크 모드 구현하기 (Add ChonkyActions for Dark Mode) (0) | 2024.03.16 |
리액트 - chonky 기본 설정 확인하기 (Default ConfigOptions) (0) | 2024.03.16 |
리액트 - 파일 브라우저 만들기 (React File Browser with chonky) (0) | 2024.03.16 |
댓글