You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
148 lines
3.9 KiB
JavaScript
148 lines
3.9 KiB
JavaScript
import { useSignal } from '@preact/signals'
|
|
import clsx from 'clsx'
|
|
import { useEffect, useState } from 'preact/hooks'
|
|
|
|
export const EXAMPLE_TREE = {
|
|
type: 'root',
|
|
children: [
|
|
{
|
|
type: 'folder',
|
|
name: 'bin/',
|
|
children: [
|
|
{
|
|
type: 'file',
|
|
name: 'a.out',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
type: 'file',
|
|
name: 'main.c',
|
|
},
|
|
{
|
|
type: 'file',
|
|
name: 'data.csv',
|
|
},
|
|
],
|
|
}
|
|
|
|
const flattenTree = (node, depth = 0, path = []) => {
|
|
if (node.type === 'root') {
|
|
return node.children.flatMap(entry => flattenTree(entry, depth, []))
|
|
}
|
|
if (node.type === 'folder')
|
|
return [
|
|
{
|
|
depth,
|
|
type: 'folder',
|
|
name: node.name,
|
|
path: path.join('/') + '/',
|
|
},
|
|
...node.children.flatMap(entry => flattenTree(entry, depth + 1, [...path, node.name])),
|
|
]
|
|
if (node.type === 'file') {
|
|
return [
|
|
{
|
|
depth,
|
|
type: 'file',
|
|
name: node.name,
|
|
path: path.join('/'),
|
|
},
|
|
]
|
|
}
|
|
|
|
throw new Error(`invalid node type "${node?.type ?? '<unknown>'}"`)
|
|
}
|
|
|
|
const TreeViewNode = ({ listDir, actionOpenFile, node, depth, path }) => {
|
|
if (node.type === 'root') {
|
|
const [children, setChildren] = useState([])
|
|
|
|
useEffect(async () => {
|
|
setChildren(await listDir(path))
|
|
}, [])
|
|
|
|
return children.flatMap(entry => (
|
|
<TreeViewNode
|
|
listDir={listDir}
|
|
actionOpenFile={actionOpenFile}
|
|
node={entry}
|
|
depth={0}
|
|
path={'/' + entry.name}
|
|
/>
|
|
))
|
|
}
|
|
|
|
if (node.type === 'folder') {
|
|
const [collapsed, setCollapsed] = useState(true)
|
|
|
|
const [children, setChildren] = useState([])
|
|
|
|
useEffect(async () => {
|
|
setChildren(collapsed ? [] : await listDir(path))
|
|
}, [collapsed])
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
class="entry folder"
|
|
style={{ '--depth': depth }}
|
|
title={path}
|
|
onClick={() => setCollapsed(c => !c)}
|
|
>
|
|
<div class="material-symbols-outlined">folder</div>
|
|
<div class="name">{node.name}/</div>
|
|
<div class="actions"></div>
|
|
</div>
|
|
{!collapsed &&
|
|
children.flatMap(entry => (
|
|
<TreeViewNode
|
|
listDir={listDir}
|
|
actionOpenFile={actionOpenFile}
|
|
node={entry}
|
|
depth={depth + 1}
|
|
path={path + '/' + entry.name}
|
|
/>
|
|
))}
|
|
</>
|
|
)
|
|
}
|
|
|
|
if (node.type === 'file') {
|
|
return (
|
|
<div
|
|
class="entry file"
|
|
style={{ '--depth': depth }}
|
|
title={path}
|
|
onClick={() => {
|
|
console.log('open', path)
|
|
actionOpenFile?.(path)
|
|
}}
|
|
>
|
|
<div class="material-symbols-outlined">description</div>
|
|
<div class="name">{node.name}</div>
|
|
<div class="actions"></div>
|
|
</div>
|
|
)
|
|
}
|
|
}
|
|
|
|
const ICONS = {
|
|
['folder']: 'folder',
|
|
['file']: 'description',
|
|
}
|
|
|
|
export const TreeView = ({ listDir, actionOpenFile, rootPath }) => {
|
|
return (
|
|
<div class="tree-view">
|
|
<TreeViewNode
|
|
listDir={listDir}
|
|
actionOpenFile={actionOpenFile}
|
|
node={{ type: 'root' }}
|
|
depth={0}
|
|
path={rootPath}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|