mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
fix: update tree on compress option change
This commit is contained in:
parent
9c12d344de
commit
453f3b76d9
6 changed files with 349 additions and 318 deletions
|
|
@ -7,7 +7,7 @@ import { PortalContext } from 'containers/PortalContext'
|
|||
import { RepoContext } from 'containers/RepoContext'
|
||||
import { platform } from 'platforms'
|
||||
import * as React from 'react'
|
||||
import { usePrevious } from 'react-use'
|
||||
import { usePrevious, useUpdateEffect } from 'react-use'
|
||||
import { cx } from 'utils/cx'
|
||||
import { run } from 'utils/general'
|
||||
import { useElementSize } from 'utils/hooks/useElementSize'
|
||||
|
|
@ -86,16 +86,24 @@ function LoadedFileExplorer({
|
|||
visibleNodesGenerator: VisibleNodesGenerator
|
||||
visibleNodes: VisibleNodes
|
||||
}) {
|
||||
const config = useConfigs().value
|
||||
|
||||
const [searchKey, updateSearchKey] = React.useState('')
|
||||
const searched = !!searchKey
|
||||
const onSearch = useOnSearch(updateSearchKey, visibleNodesGenerator)
|
||||
const { focusedNode, nodes, expandedNodes, depths, loading } = visibleNodes
|
||||
|
||||
// re-search when the compress option update
|
||||
const { compressSingletonFolder } = config
|
||||
useUpdateEffect(() => {
|
||||
visibleNodesGenerator.setCompression(compressSingletonFolder)
|
||||
}, [compressSingletonFolder])
|
||||
|
||||
const {
|
||||
ref: filesRef,
|
||||
size: [, height],
|
||||
} = useElementSize<HTMLDivElement>()
|
||||
const { compactFileTree } = useConfigs().value
|
||||
const { compactFileTree } = config
|
||||
const {
|
||||
ref: scrollElementRef,
|
||||
onScroll,
|
||||
|
|
|
|||
49
src/utils/VisibleNodesGenerator/BaseLayer.ts
Normal file
49
src/utils/VisibleNodesGenerator/BaseLayer.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { EventHub } from '../EventHub'
|
||||
import { findNode } from '../general'
|
||||
import { Options } from './index'
|
||||
|
||||
function mergeNodes(target: TreeNode, source: TreeNode) {
|
||||
for (const node of source.contents || []) {
|
||||
const dup = target.contents?.find($node => $node.path === node.path)
|
||||
if (dup) {
|
||||
mergeNodes(dup, node)
|
||||
} else {
|
||||
if (!target.contents) target.contents = []
|
||||
target.contents.push(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class BaseLayer {
|
||||
baseRoot: TreeNode
|
||||
getTreeData: (path: string) => Async<TreeNode>
|
||||
loading: Set<TreeNode['path']> = new Set()
|
||||
defer: boolean
|
||||
|
||||
baseHub = new EventHub<{
|
||||
emit: BaseLayer['baseRoot']
|
||||
loadingChange: BaseLayer['loading']
|
||||
}>()
|
||||
|
||||
constructor({ root, getTreeData, defer = false }: Options) {
|
||||
this.baseRoot = root
|
||||
this.getTreeData = getTreeData
|
||||
this.defer = defer
|
||||
}
|
||||
|
||||
loadTreeData = async (path: string) => {
|
||||
const node = await findNode(this.baseRoot, path)
|
||||
if (node && node.type !== 'tree') return node
|
||||
if (node?.contents?.length) return node // check in memory
|
||||
if (this.loading.has(path)) return
|
||||
|
||||
this.loading.add(path)
|
||||
this.baseHub.emit('loadingChange', this.loading)
|
||||
mergeNodes(this.baseRoot, await this.getTreeData(path))
|
||||
this.loading.delete(path)
|
||||
this.baseHub.emit('loadingChange', this.loading)
|
||||
this.baseHub.emit('emit', this.baseRoot)
|
||||
|
||||
return await findNode(this.baseRoot, path)
|
||||
}
|
||||
}
|
||||
79
src/utils/VisibleNodesGenerator/CompressLayer.ts
Normal file
79
src/utils/VisibleNodesGenerator/CompressLayer.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
import { EventHub } from '../EventHub'
|
||||
import { withEffect } from '../general'
|
||||
import { Options } from './index'
|
||||
import { ShakeLayer } from './ShakeLayer'
|
||||
|
||||
function compressTree(root: TreeNode, prefix: string[] = []): TreeNode {
|
||||
if (root.contents) {
|
||||
if (root.contents.length === 1) {
|
||||
const [singleton] = root.contents
|
||||
if (singleton.type === 'tree') {
|
||||
return compressTree(singleton, [...prefix, root.name])
|
||||
}
|
||||
}
|
||||
|
||||
let compressed = false
|
||||
const contents = []
|
||||
for (const node of root.contents) {
|
||||
const $node = compressTree(node)
|
||||
if ($node !== node) compressed = true
|
||||
contents.push($node)
|
||||
}
|
||||
if (compressed)
|
||||
return {
|
||||
...root,
|
||||
name: [...prefix, root.name].join('/'),
|
||||
contents,
|
||||
}
|
||||
}
|
||||
return prefix.length
|
||||
? {
|
||||
...root,
|
||||
name: [...prefix, root.name].join('/'),
|
||||
}
|
||||
: root
|
||||
}
|
||||
|
||||
export class CompressLayer extends ShakeLayer {
|
||||
private compress: boolean
|
||||
depths = new Map<TreeNode, number>()
|
||||
compressedRoot: TreeNode | null = null
|
||||
compressHub = new EventHub<{ emit: TreeNode | null }>()
|
||||
|
||||
constructor(options: Options) {
|
||||
super(options)
|
||||
this.compress = Boolean(options.compress)
|
||||
|
||||
this.shakeHub.addEventListener('emit', () => this.compressTree())
|
||||
}
|
||||
|
||||
setCompression(compress: CompressLayer['compress']) {
|
||||
this.compress = compress
|
||||
this.compressTree()
|
||||
}
|
||||
|
||||
private compressTree = withEffect(
|
||||
() => {
|
||||
this.compressedRoot =
|
||||
this.compress && this.shackedRoot
|
||||
? {
|
||||
...this.shackedRoot,
|
||||
contents: this.shackedRoot.contents?.map(node => compressTree(node)),
|
||||
}
|
||||
: this.shackedRoot
|
||||
|
||||
if (this.compressedRoot) {
|
||||
const depths = new Map<TreeNode, number>()
|
||||
const recordDepth = (node: TreeNode, depth = 0) => {
|
||||
depths.set(node, depth)
|
||||
for (const $node of node.contents || []) {
|
||||
recordDepth($node, depth + 1)
|
||||
}
|
||||
}
|
||||
recordDepth(this.compressedRoot, -1)
|
||||
this.depths = depths
|
||||
}
|
||||
},
|
||||
() => this.compressHub.emit('emit', this.compressedRoot),
|
||||
)
|
||||
}
|
||||
140
src/utils/VisibleNodesGenerator/FlattenLayer.ts
Normal file
140
src/utils/VisibleNodesGenerator/FlattenLayer.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
import { EventHub } from '../EventHub'
|
||||
import { findNode, traverse, withEffect } from '../general'
|
||||
import { CompressLayer } from './CompressLayer'
|
||||
import { Options, SearchParams } from './index'
|
||||
|
||||
export class FlattenLayer extends CompressLayer {
|
||||
focusedNode: TreeNode | null = null
|
||||
nodes: TreeNode[] = []
|
||||
expandedNodes: Set<TreeNode['path']> = new Set()
|
||||
backupExpandedNodes: Set<TreeNode['path']> = new Set()
|
||||
flattenHub = new EventHub<{ emit: null }>()
|
||||
|
||||
constructor(options: Options) {
|
||||
super(options)
|
||||
|
||||
this.compressHub.addEventListener('emit', () => this.generateVisibleNodes())
|
||||
}
|
||||
|
||||
generateVisibleNodes = withEffect(
|
||||
async () => {
|
||||
const nodes: TreeNode[] = []
|
||||
const focusedNode = this.focusedNode
|
||||
|
||||
if (
|
||||
focusedNode &&
|
||||
this.compressedRoot &&
|
||||
!(await findNode(this.compressedRoot, focusedNode.path))
|
||||
) {
|
||||
// rescue the focus after expanding async singleton folder
|
||||
await traverse(
|
||||
this.compressedRoot.contents,
|
||||
node => {
|
||||
if (node.type === 'tree' && node.path.startsWith(focusedNode.path)) {
|
||||
this.focusNode(node)
|
||||
}
|
||||
|
||||
return node.type === 'tree' && this.expandedNodes.has(node.path)
|
||||
},
|
||||
node => node.contents || [],
|
||||
)
|
||||
}
|
||||
|
||||
await traverse(
|
||||
this.compressedRoot?.contents,
|
||||
node => {
|
||||
nodes.push(node)
|
||||
return node.type === 'tree' && this.expandedNodes.has(node.path)
|
||||
},
|
||||
node => node.contents || [],
|
||||
)
|
||||
this.nodes = nodes
|
||||
},
|
||||
() => this.flattenHub.emit('emit', null),
|
||||
)
|
||||
|
||||
focusNode = (node: TreeNode | null) => {
|
||||
if (this.focusedNode !== node) {
|
||||
this.focusedNode = node
|
||||
this.flattenHub.emit('emit', null)
|
||||
}
|
||||
}
|
||||
|
||||
barelySetExpand = (node: TreeNode, expand: boolean) => {
|
||||
// expanding non-tree node could cause unexpected UX
|
||||
if (node.type === 'tree') {
|
||||
if (expand) this.expandedNodes.add(node.path)
|
||||
else this.expandedNodes.delete(node.path)
|
||||
}
|
||||
}
|
||||
|
||||
$setExpand = (node: TreeNode, expand: boolean) => {
|
||||
this.barelySetExpand(node, expand)
|
||||
// The `node.contents?.length` condition is critical to search performance as it reduces lots of function calls
|
||||
if (expand && node.type === 'tree' && !node.contents?.length)
|
||||
return this.loadTreeData(node.path)
|
||||
}
|
||||
setExpand = withEffect(this.$setExpand, this.generateVisibleNodes)
|
||||
|
||||
toggleExpand = withEffect(async (node: TreeNode, recursive = false) => {
|
||||
const expand = !this.expandedNodes.has(node.path)
|
||||
await traverse(
|
||||
[node],
|
||||
node => {
|
||||
this.$setExpand(node, expand)
|
||||
return recursive
|
||||
},
|
||||
node => node.contents || [],
|
||||
)
|
||||
}, this.generateVisibleNodes)
|
||||
|
||||
expandTo = withEffect(async (path: string) => {
|
||||
const rootNode = this.compressedRoot
|
||||
if (rootNode) {
|
||||
await traverse(
|
||||
rootNode.contents,
|
||||
async node => {
|
||||
const overflowChar = path[node.path.length]
|
||||
const match = path.startsWith(node.path) && (overflowChar === '/' || !overflowChar)
|
||||
if (match) {
|
||||
if (node.path === path) {
|
||||
// do not wait for expansion for the exact node as that will block "jumping from search"
|
||||
this.$setExpand(node, true)
|
||||
} else await this.$setExpand(node, true)
|
||||
}
|
||||
return match
|
||||
},
|
||||
node => node?.contents || [],
|
||||
)
|
||||
|
||||
const node = await findNode(rootNode, path)
|
||||
return node
|
||||
}
|
||||
}, this.generateVisibleNodes)
|
||||
|
||||
search = (
|
||||
searchParams: Pick<SearchParams, 'matchNode'> | null,
|
||||
restoreExpandedFolders?: boolean,
|
||||
) => {
|
||||
if (searchParams) {
|
||||
if (!this.isSearching) {
|
||||
// backup expansion when start search
|
||||
this.backupExpandedNodes.clear()
|
||||
this.expandedNodes.forEach(path => this.backupExpandedNodes.add(path))
|
||||
}
|
||||
this.expandedNodes.clear() // Reset expansion on every search, ensure cleanest search result expansion
|
||||
this.shake({
|
||||
matchNode: searchParams.matchNode,
|
||||
onChildMatch: node => this.$setExpand(node, true),
|
||||
})
|
||||
} else {
|
||||
this.shake(null)
|
||||
// collapse all nodes on clearing search key
|
||||
this.expandedNodes.clear()
|
||||
if (restoreExpandedFolders) {
|
||||
this.backupExpandedNodes.forEach(path => this.expandedNodes.add(path))
|
||||
this.backupExpandedNodes.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
src/utils/VisibleNodesGenerator/ShakeLayer.ts
Normal file
66
src/utils/VisibleNodesGenerator/ShakeLayer.ts
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { EventHub } from '../EventHub'
|
||||
import { withEffect } from '../general'
|
||||
import { BaseLayer } from './BaseLayer'
|
||||
import { Options, SearchParams } from './index'
|
||||
|
||||
function search(
|
||||
root: TreeNode,
|
||||
match: (node: TreeNode) => boolean,
|
||||
onChildMatch: (node: TreeNode) => void,
|
||||
): TreeNode | null {
|
||||
// go traverse no matter whether root matches to make sure find & expand the related nodes
|
||||
// The `related nodes` are the nodes that either itself matches or any of direct or indirect children match
|
||||
const contents = []
|
||||
|
||||
if (root.type === 'tree' && root.contents) {
|
||||
for (const node of root.contents) {
|
||||
const $node = search(node, match, onChildMatch)
|
||||
if ($node) contents.push($node)
|
||||
}
|
||||
|
||||
if (contents.length) onChildMatch(root)
|
||||
}
|
||||
|
||||
// Return root if itself matches
|
||||
if (match(root)) return root
|
||||
|
||||
// Otherwise, but when deeper nodes match, return partial root
|
||||
if (contents.length) {
|
||||
return {
|
||||
...root,
|
||||
contents,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export class ShakeLayer extends BaseLayer {
|
||||
shackedRoot: TreeNode | null = this.baseRoot
|
||||
lastSearchParams: SearchParams | null = null
|
||||
shakeHub = new EventHub<{ emit: TreeNode | null }>()
|
||||
|
||||
get isSearching() {
|
||||
return this.lastSearchParams !== null
|
||||
}
|
||||
|
||||
constructor(options: Options) {
|
||||
super(options)
|
||||
|
||||
this.baseHub.addEventListener('emit', () => this.shake(this.lastSearchParams))
|
||||
}
|
||||
|
||||
shake = withEffect(
|
||||
(searchParams: ShakeLayer['lastSearchParams']) => {
|
||||
this.lastSearchParams = searchParams
|
||||
|
||||
let root: ShakeLayer['shackedRoot'] = this.baseRoot
|
||||
if (searchParams) {
|
||||
const { matchNode, onChildMatch } = searchParams
|
||||
root = search(this.baseRoot, matchNode, onChildMatch)
|
||||
}
|
||||
this.shackedRoot = root
|
||||
},
|
||||
() => this.shakeHub.emit('emit', this.shackedRoot),
|
||||
)
|
||||
}
|
||||
|
|
@ -1,326 +1,14 @@
|
|||
import { EventHub } from '../EventHub'
|
||||
import { findNode, traverse, withEffect } from '../general'
|
||||
|
||||
function search(
|
||||
root: TreeNode,
|
||||
match: (node: TreeNode) => boolean,
|
||||
onChildMatch: (node: TreeNode) => void,
|
||||
): TreeNode | null {
|
||||
// go traverse no matter whether root matches to make sure find & expand the related nodes
|
||||
// The `related nodes` are the nodes that either itself matches or any of direct or indirect children match
|
||||
const contents = []
|
||||
|
||||
if (root.type === 'tree' && root.contents) {
|
||||
for (const node of root.contents) {
|
||||
const $node = search(node, match, onChildMatch)
|
||||
if ($node) contents.push($node)
|
||||
}
|
||||
|
||||
if (contents.length) onChildMatch(root)
|
||||
}
|
||||
|
||||
// Return root if itself matches
|
||||
if (match(root)) return root
|
||||
|
||||
// Otherwise, but when deeper nodes match, return partial root
|
||||
if (contents.length) {
|
||||
return {
|
||||
...root,
|
||||
contents,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function compressTree(root: TreeNode, prefix: string[] = []): TreeNode {
|
||||
if (root.contents) {
|
||||
if (root.contents.length === 1) {
|
||||
const [singleton] = root.contents
|
||||
if (singleton.type === 'tree') {
|
||||
return compressTree(singleton, [...prefix, root.name])
|
||||
}
|
||||
}
|
||||
|
||||
let compressed = false
|
||||
const contents = []
|
||||
for (const node of root.contents) {
|
||||
const $node = compressTree(node)
|
||||
if ($node !== node) compressed = true
|
||||
contents.push($node)
|
||||
}
|
||||
if (compressed)
|
||||
return {
|
||||
...root,
|
||||
name: [...prefix, root.name].join('/'),
|
||||
contents,
|
||||
}
|
||||
}
|
||||
return prefix.length
|
||||
? {
|
||||
...root,
|
||||
name: [...prefix, root.name].join('/'),
|
||||
}
|
||||
: root
|
||||
}
|
||||
|
||||
function mergeNodes(target: TreeNode, source: TreeNode) {
|
||||
for (const node of source.contents || []) {
|
||||
const dup = target.contents?.find($node => $node.path === node.path)
|
||||
if (dup) {
|
||||
mergeNodes(dup, node)
|
||||
} else {
|
||||
if (!target.contents) target.contents = []
|
||||
target.contents.push(node)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class BaseLayer {
|
||||
baseRoot: TreeNode
|
||||
getTreeData: (path: string) => Async<TreeNode>
|
||||
loading: Set<TreeNode['path']> = new Set()
|
||||
defer: boolean
|
||||
|
||||
baseHub = new EventHub<{
|
||||
emit: BaseLayer['baseRoot']
|
||||
loadingChange: BaseLayer['loading']
|
||||
}>()
|
||||
|
||||
constructor({ root, getTreeData, defer = false }: Options) {
|
||||
this.baseRoot = root
|
||||
this.getTreeData = getTreeData
|
||||
this.defer = defer
|
||||
}
|
||||
|
||||
loadTreeData = async (path: string) => {
|
||||
const node = await findNode(this.baseRoot, path)
|
||||
if (node && node.type !== 'tree') return node
|
||||
if (node?.contents?.length) return node // check in memory
|
||||
if (this.loading.has(path)) return
|
||||
|
||||
this.loading.add(path)
|
||||
this.baseHub.emit('loadingChange', this.loading)
|
||||
mergeNodes(this.baseRoot, await this.getTreeData(path))
|
||||
this.loading.delete(path)
|
||||
this.baseHub.emit('loadingChange', this.loading)
|
||||
this.baseHub.emit('emit', this.baseRoot)
|
||||
|
||||
return await findNode(this.baseRoot, path)
|
||||
}
|
||||
}
|
||||
|
||||
class ShakeLayer extends BaseLayer {
|
||||
shackedRoot: TreeNode | null = this.baseRoot
|
||||
lastSearchParams: SearchParams | null = null
|
||||
shakeHub = new EventHub<{ emit: TreeNode | null }>()
|
||||
|
||||
get isSearching() {
|
||||
return this.lastSearchParams !== null
|
||||
}
|
||||
|
||||
constructor(options: Options) {
|
||||
super(options)
|
||||
|
||||
this.baseHub.addEventListener('emit', () => this.shake(this.lastSearchParams))
|
||||
}
|
||||
|
||||
shake = withEffect(
|
||||
(searchParams: ShakeLayer['lastSearchParams']) => {
|
||||
this.lastSearchParams = searchParams
|
||||
|
||||
let root: ShakeLayer['shackedRoot'] = this.baseRoot
|
||||
if (searchParams) {
|
||||
const { matchNode, onChildMatch } = searchParams
|
||||
root = search(this.baseRoot, matchNode, onChildMatch)
|
||||
}
|
||||
this.shackedRoot = root
|
||||
},
|
||||
() => this.shakeHub.emit('emit', this.shackedRoot),
|
||||
)
|
||||
}
|
||||
|
||||
class CompressLayer extends ShakeLayer {
|
||||
private compress: boolean
|
||||
depths = new Map<TreeNode, number>()
|
||||
compressedRoot: TreeNode | null = null
|
||||
compressHub = new EventHub<{ emit: TreeNode | null }>()
|
||||
|
||||
constructor(options: Options) {
|
||||
super(options)
|
||||
this.compress = Boolean(options.compress)
|
||||
|
||||
this.shakeHub.addEventListener('emit', () => this.compressTree())
|
||||
}
|
||||
|
||||
private compressTree = withEffect(
|
||||
() => {
|
||||
this.compressedRoot =
|
||||
this.shackedRoot && this.compress
|
||||
? {
|
||||
...this.shackedRoot,
|
||||
contents: this.shackedRoot.contents?.map(node => compressTree(node)),
|
||||
}
|
||||
: this.shackedRoot
|
||||
|
||||
if (this.compressedRoot) {
|
||||
const depths = new Map<TreeNode, number>()
|
||||
const recordDepth = (node: TreeNode, depth = 0) => {
|
||||
depths.set(node, depth)
|
||||
for (const $node of node.contents || []) {
|
||||
recordDepth($node, depth + 1)
|
||||
}
|
||||
}
|
||||
recordDepth(this.compressedRoot, -1)
|
||||
this.depths = depths
|
||||
}
|
||||
},
|
||||
() => this.compressHub.emit('emit', this.compressedRoot),
|
||||
)
|
||||
}
|
||||
import { BaseLayer } from './BaseLayer'
|
||||
import { CompressLayer } from './CompressLayer'
|
||||
import { FlattenLayer } from './FlattenLayer'
|
||||
|
||||
export type SearchParams = {
|
||||
matchNode(node: TreeNode): boolean
|
||||
onChildMatch(node: TreeNode): void
|
||||
}
|
||||
|
||||
class FlattenLayer extends CompressLayer {
|
||||
focusedNode: TreeNode | null = null
|
||||
nodes: TreeNode[] = []
|
||||
expandedNodes: Set<TreeNode['path']> = new Set()
|
||||
backupExpandedNodes: Set<TreeNode['path']> = new Set()
|
||||
flattenHub = new EventHub<{ emit: null }>()
|
||||
|
||||
constructor(options: Options) {
|
||||
super(options)
|
||||
|
||||
this.compressHub.addEventListener('emit', () => this.generateVisibleNodes())
|
||||
}
|
||||
|
||||
generateVisibleNodes = withEffect(
|
||||
async () => {
|
||||
const nodes: TreeNode[] = []
|
||||
const focusedNode = this.focusedNode
|
||||
|
||||
if (
|
||||
focusedNode &&
|
||||
this.compressedRoot &&
|
||||
!(await findNode(this.compressedRoot, focusedNode.path))
|
||||
) {
|
||||
// rescue the focus after expanding async singleton folder
|
||||
await traverse(
|
||||
this.compressedRoot.contents,
|
||||
node => {
|
||||
if (node.type === 'tree' && node.path.startsWith(focusedNode.path)) {
|
||||
this.focusNode(node)
|
||||
}
|
||||
|
||||
return node.type === 'tree' && this.expandedNodes.has(node.path)
|
||||
},
|
||||
node => node.contents || [],
|
||||
)
|
||||
}
|
||||
|
||||
await traverse(
|
||||
this.compressedRoot?.contents,
|
||||
node => {
|
||||
nodes.push(node)
|
||||
return node.type === 'tree' && this.expandedNodes.has(node.path)
|
||||
},
|
||||
node => node.contents || [],
|
||||
)
|
||||
this.nodes = nodes
|
||||
},
|
||||
() => this.flattenHub.emit('emit', null),
|
||||
)
|
||||
|
||||
focusNode = (node: TreeNode | null) => {
|
||||
if (this.focusedNode !== node) {
|
||||
this.focusedNode = node
|
||||
this.flattenHub.emit('emit', null)
|
||||
}
|
||||
}
|
||||
|
||||
barelySetExpand = (node: TreeNode, expand: boolean) => {
|
||||
// expanding non-tree node could cause unexpected UX
|
||||
if (node.type === 'tree') {
|
||||
if (expand) this.expandedNodes.add(node.path)
|
||||
else this.expandedNodes.delete(node.path)
|
||||
}
|
||||
}
|
||||
|
||||
$setExpand = (node: TreeNode, expand: boolean) => {
|
||||
this.barelySetExpand(node, expand)
|
||||
// The `node.contents?.length` condition is critical to search performance as it reduces lots of function calls
|
||||
if (expand && node.type === 'tree' && !node.contents?.length)
|
||||
return this.loadTreeData(node.path)
|
||||
}
|
||||
setExpand = withEffect(this.$setExpand, this.generateVisibleNodes)
|
||||
|
||||
toggleExpand = withEffect(async (node: TreeNode, recursive = false) => {
|
||||
const expand = !this.expandedNodes.has(node.path)
|
||||
await traverse(
|
||||
[node],
|
||||
node => {
|
||||
this.$setExpand(node, expand)
|
||||
return recursive
|
||||
},
|
||||
node => node.contents || [],
|
||||
)
|
||||
}, this.generateVisibleNodes)
|
||||
|
||||
expandTo = withEffect(async (path: string) => {
|
||||
const rootNode = this.compressedRoot
|
||||
if (rootNode) {
|
||||
await traverse(
|
||||
rootNode.contents,
|
||||
async node => {
|
||||
const overflowChar = path[node.path.length]
|
||||
const match = path.startsWith(node.path) && (overflowChar === '/' || !overflowChar)
|
||||
if (match) {
|
||||
if (node.path === path) {
|
||||
// do not wait for expansion for the exact node as that will block "jumping from search"
|
||||
this.$setExpand(node, true)
|
||||
} else await this.$setExpand(node, true)
|
||||
}
|
||||
return match
|
||||
},
|
||||
node => node?.contents || [],
|
||||
)
|
||||
|
||||
const node = await findNode(rootNode, path)
|
||||
return node
|
||||
}
|
||||
}, this.generateVisibleNodes)
|
||||
|
||||
search = (
|
||||
searchParams: Pick<SearchParams, 'matchNode'> | null,
|
||||
restoreExpandedFolders?: boolean,
|
||||
) => {
|
||||
if (searchParams) {
|
||||
if (!this.isSearching) {
|
||||
// backup expansion when start search
|
||||
this.backupExpandedNodes.clear()
|
||||
this.expandedNodes.forEach(path => this.backupExpandedNodes.add(path))
|
||||
}
|
||||
this.expandedNodes.clear() // Reset expansion on every search, ensure cleanest search result expansion
|
||||
this.shake({
|
||||
matchNode: searchParams.matchNode,
|
||||
onChildMatch: node => this.$setExpand(node, true),
|
||||
})
|
||||
} else {
|
||||
this.shake(null)
|
||||
// collapse all nodes on clearing search key
|
||||
this.expandedNodes.clear()
|
||||
if (restoreExpandedFolders) {
|
||||
this.backupExpandedNodes.forEach(path => this.expandedNodes.add(path))
|
||||
this.backupExpandedNodes.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Options = {
|
||||
export type Options = {
|
||||
root: BaseLayer['baseRoot']
|
||||
defer?: BaseLayer['defer']
|
||||
getTreeData: BaseLayer['getTreeData']
|
||||
|
|
@ -339,6 +27,7 @@ export class VisibleNodesGenerator extends FlattenLayer {
|
|||
hub = new EventHub<{
|
||||
emit: VisibleNodes
|
||||
}>()
|
||||
|
||||
constructor(options: Options) {
|
||||
super(options)
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue