mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
refactor: transform FileExplorer
This commit is contained in:
parent
8b0f331f27
commit
6de067a72f
5 changed files with 322 additions and 300 deletions
|
|
@ -23,6 +23,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@types/chrome": "^0.0.77",
|
||||
"@types/ini": "^1.3.30",
|
||||
"@types/nprogress": "^0.0.29",
|
||||
"@types/react": "^16.8.2",
|
||||
"@types/react-dom": "^16.8.0",
|
||||
|
|
|
|||
|
|
@ -6,28 +6,19 @@ import Node from 'components/Node'
|
|||
import LoadingIndicator from 'components/LoadingIndicator'
|
||||
import cx from 'utils/cx'
|
||||
import { TreeData, MetaData, VisibleNodes } from './SideBar'
|
||||
import { ConnectorState } from 'driver/core/FileExplorer'
|
||||
|
||||
type Props = {
|
||||
export type Props = {
|
||||
treeData: TreeData
|
||||
metaData: MetaData
|
||||
visibleNodes: VisibleNodes
|
||||
freeze: boolean
|
||||
|
||||
stateText: string
|
||||
|
||||
init: () => void
|
||||
execAfterRender: () => void
|
||||
handleKeyDown: React.KeyboardEventHandler
|
||||
handleSearchKeyChange: React.FormEventHandler
|
||||
onNodeClick: Node['props']['onClick']
|
||||
compressSingletonFolder: boolean
|
||||
accessToken: string
|
||||
toggleShowSettings: React.MouseEventHandler
|
||||
onFocusSearchBar: React.FocusEventHandler
|
||||
setUpTree: (treeData?: TreeData) => void
|
||||
}
|
||||
|
||||
@(connect(FileExplorerCore) as any)
|
||||
export default class FileExplorer extends React.Component<Props> {
|
||||
static defaultProps: Partial<Props> = {
|
||||
class FileExplorer extends React.Component<Props & ConnectorState> {
|
||||
static defaultProps: Partial<Props & ConnectorState> = {
|
||||
treeData: null,
|
||||
metaData: null,
|
||||
freeze: false,
|
||||
|
|
@ -45,7 +36,7 @@ export default class FileExplorer extends React.Component<Props> {
|
|||
execAfterRender()
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps: Props) {
|
||||
componentWillReceiveProps(nextProps: Props & ConnectorState) {
|
||||
if (nextProps.treeData !== this.props.treeData) {
|
||||
const { setUpTree } = nextProps
|
||||
setUpTree()
|
||||
|
|
@ -110,3 +101,5 @@ export default class FileExplorer extends React.Component<Props> {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect<Props, ConnectorState>(FileExplorerCore)(FileExplorer)
|
||||
|
|
|
|||
|
|
@ -1,284 +0,0 @@
|
|||
import ini from 'ini'
|
||||
import DOMHelper from 'utils/DOMHelper'
|
||||
import treeParser from 'utils/treeParser'
|
||||
import URLHelper from 'utils/URLHelper'
|
||||
import VisibleNodesGenerator from 'utils/VisibleNodesGenerator'
|
||||
import GitHubHelper from 'utils/GitHubHelper'
|
||||
|
||||
function getVisibleParentNode(nodes, focusedNode, depths) {
|
||||
const focusedNodeIndex = nodes.indexOf(focusedNode)
|
||||
const focusedNodeDepth = depths.get(focusedNode)
|
||||
let indexOfParentNode = focusedNodeIndex - 1
|
||||
while (
|
||||
indexOfParentNode !== -1 &&
|
||||
depths.get(nodes[indexOfParentNode]) >= focusedNodeDepth
|
||||
) {
|
||||
--indexOfParentNode
|
||||
}
|
||||
const parentNode = nodes[indexOfParentNode]
|
||||
return parentNode
|
||||
}
|
||||
|
||||
const tasksAfterRender = []
|
||||
const visibleNodesGenerator = new VisibleNodesGenerator()
|
||||
|
||||
const init = dispatch => () => dispatch(async () => {
|
||||
dispatch(setStateText, 'Fetching File List...')
|
||||
})
|
||||
|
||||
function resolveGitModules(root, blobData) {
|
||||
if (blobData) {
|
||||
if (blobData.encoding === 'base64') {
|
||||
const content = atob(blobData.content)
|
||||
const parsed = ini.parse(content)
|
||||
Object.values(parsed).map(value => {
|
||||
const { url, path } = value
|
||||
// for now, handle modules at root only
|
||||
const node = root.contents.find(node => node.path === path)
|
||||
if (node) {
|
||||
node.url = url
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setUpTree = dispatch => () => dispatch(async (state, { treeData, metaData, compressSingletonFolder, accessToken }) => {
|
||||
if (!treeData) return
|
||||
dispatch(setStateText, 'Rendering File List...')
|
||||
const { root, gitModules } = treeParser.parse(treeData, metaData)
|
||||
|
||||
if (gitModules) {
|
||||
const blobData = await GitHubHelper.getBlobData({
|
||||
userName: metaData.userName,
|
||||
repoName: metaData.repoName,
|
||||
fileSHA: gitModules.sha,
|
||||
accessToken,
|
||||
})
|
||||
|
||||
resolveGitModules(root, blobData)
|
||||
}
|
||||
|
||||
visibleNodesGenerator.setCompress(compressSingletonFolder)
|
||||
await visibleNodesGenerator.plantTree(root)
|
||||
|
||||
tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
dispatch(setStateText, null)
|
||||
const currentPath = URLHelper.getCurrentPath(metaData.branchName)
|
||||
if (currentPath.length) {
|
||||
const nodeExpandedTo = visibleNodesGenerator.expandTo(currentPath.join('/'))
|
||||
if (nodeExpandedTo) {
|
||||
visibleNodesGenerator.focusNode(nodeExpandedTo)
|
||||
const { nodes } = visibleNodesGenerator.visibleNodes
|
||||
tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(nodes.indexOf(nodeExpandedTo)))
|
||||
}
|
||||
}
|
||||
dispatch(updateVisibleNodes)
|
||||
})
|
||||
|
||||
const execAfterRender = dispatch => () => {
|
||||
for (const task of tasksAfterRender) {
|
||||
task()
|
||||
}
|
||||
tasksAfterRender.length = 0
|
||||
}
|
||||
|
||||
const setStateText = dispatch => text => dispatch({
|
||||
stateText: text,
|
||||
})
|
||||
|
||||
const handleKeyDown = dispatch => event => dispatch(({ visibleNodes: { nodes, focusedNode, expandedNodes, depths } }) => {
|
||||
function handleVerticalMove(index) {
|
||||
if (0 <= index && index < nodes.length) {
|
||||
DOMHelper.focusFileExplorer()
|
||||
dispatch(focusNode, nodes[index])
|
||||
} else {
|
||||
DOMHelper.focusSearchInput()
|
||||
dispatch(focusNode, null)
|
||||
}
|
||||
}
|
||||
|
||||
const { key } = event
|
||||
// prevent document body scrolling if the keypress results in Gitako action
|
||||
let muteEvent = true
|
||||
if (focusedNode) {
|
||||
const focusedNodeIndex = nodes.indexOf(focusedNode)
|
||||
switch (key) {
|
||||
case 'ArrowUp':
|
||||
// focus on previous node
|
||||
handleVerticalMove(focusedNodeIndex - 1)
|
||||
break
|
||||
|
||||
case 'ArrowDown':
|
||||
// focus on next node
|
||||
handleVerticalMove(focusedNodeIndex + 1)
|
||||
break
|
||||
|
||||
case 'ArrowLeft':
|
||||
// collapse node or go to parent node
|
||||
if (expandedNodes.has(focusedNode)) {
|
||||
dispatch(setExpand, focusedNode, false)
|
||||
} else {
|
||||
// go forward to the start of the list, find the closest node with lower depth
|
||||
const parentNode = getVisibleParentNode(nodes, focusedNode, depths)
|
||||
if (parentNode) {
|
||||
dispatch(focusNode, parentNode)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
// consider the two keys as 'confirm' key
|
||||
case 'ArrowRight':
|
||||
// expand node or focus on first content node or redirect to file page
|
||||
if (focusedNode.type === 'tree') {
|
||||
if (expandedNodes.has(focusedNode)) {
|
||||
const nextNode = nodes[focusedNodeIndex + 1]
|
||||
if (depths.get(nextNode) > depths.get(focusedNode)) {
|
||||
dispatch(focusNode, nextNode)
|
||||
}
|
||||
} else {
|
||||
dispatch(setExpand, focusedNode, true)
|
||||
}
|
||||
} else if (focusedNode.type === 'blob') {
|
||||
DOMHelper.loadWithPJAX(focusedNode.url)
|
||||
} else if (focusedNode.type === 'commit') {
|
||||
window.open(focusedNode.url)
|
||||
}
|
||||
break
|
||||
case 'Enter':
|
||||
// expand node or redirect to file page
|
||||
if (focusedNode.type === 'tree') {
|
||||
dispatch(setExpand, focusedNode, true)
|
||||
} else if (focusedNode.type === 'blob') {
|
||||
DOMHelper.loadWithPJAX(focusedNode.url)
|
||||
} else if (focusedNode.type === 'commit') {
|
||||
window.open(focusedNode.url)
|
||||
}
|
||||
break
|
||||
default:
|
||||
muteEvent = false
|
||||
}
|
||||
if (muteEvent) {
|
||||
event.preventDefault()
|
||||
}
|
||||
} else {
|
||||
// now search input is focused
|
||||
if (nodes.length) {
|
||||
switch (key) {
|
||||
case 'ArrowDown':
|
||||
DOMHelper.focusFileExplorer()
|
||||
dispatch(focusNode, nodes[0])
|
||||
break
|
||||
case 'ArrowUp':
|
||||
DOMHelper.focusFileExplorer()
|
||||
dispatch(focusNode, nodes[nodes.length - 1])
|
||||
break
|
||||
default:
|
||||
muteEvent = false
|
||||
}
|
||||
if (muteEvent) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const onFocusSearchBar = dispatch => () => dispatch(focusNode, null)
|
||||
|
||||
const handleSearchKeyChange = dispatch => {
|
||||
let i = 0
|
||||
return async event => {
|
||||
const searchKey = event.target.value
|
||||
const j = i += 1
|
||||
await visibleNodesGenerator.search(searchKey)
|
||||
if (i === j) dispatch(updateVisibleNodes)
|
||||
}
|
||||
}
|
||||
|
||||
const delayExpandThreshold = 400
|
||||
function shouldDelayExpand(node) {
|
||||
return visibleNodesGenerator.visibleNodes.expandedNodes.has(node)
|
||||
&& node.contents.length > delayExpandThreshold
|
||||
}
|
||||
|
||||
const setExpand = dispatch => (node, expand) => {
|
||||
visibleNodesGenerator.setExpand(node, expand)
|
||||
const applyChanges = () => dispatch(focusNode, node)
|
||||
if (shouldDelayExpand(node)) {
|
||||
dispatch(mountExpandingIndicator, node)
|
||||
tasksAfterRender.push(() => setTimeout(applyChanges, 0))
|
||||
} else {
|
||||
applyChanges()
|
||||
}
|
||||
}
|
||||
|
||||
const toggleNodeExpansion = dispatch => (node, skipScrollToNode) => {
|
||||
visibleNodesGenerator.toggleExpand(node)
|
||||
const applyChanges = () => {
|
||||
dispatch(focusNode, node, skipScrollToNode)
|
||||
tasksAfterRender.push(DOMHelper.focusFileExplorer)
|
||||
}
|
||||
if (shouldDelayExpand(node)) {
|
||||
dispatch(mountExpandingIndicator, node)
|
||||
tasksAfterRender.push(() => setTimeout(applyChanges, 0))
|
||||
} else {
|
||||
applyChanges()
|
||||
}
|
||||
}
|
||||
|
||||
const focusNode = dispatch => (node, skipScroll) => dispatch(({ visibleNodes: { nodes } }) => {
|
||||
visibleNodesGenerator.focusNode(node)
|
||||
if (node && !skipScroll) {
|
||||
// when focus a node not in viewport(by keyboard), scroll to it
|
||||
const indexOfToBeFocusedNode = nodes.indexOf(node)
|
||||
tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(indexOfToBeFocusedNode))
|
||||
}
|
||||
dispatch(updateVisibleNodes)
|
||||
})
|
||||
|
||||
const onNodeClick = dispatch => (node) => dispatch((state, { metaData, accessToken }) => {
|
||||
if (node.type === 'tree') {
|
||||
dispatch(toggleNodeExpansion, node, true)
|
||||
} else if (node.type === 'blob') {
|
||||
dispatch(focusNode, node, true)
|
||||
DOMHelper.loadWithPJAX(node.url)
|
||||
} else if (node.type === 'commit') {
|
||||
window.open(node.url, '_blank')
|
||||
}
|
||||
})
|
||||
|
||||
const mountExpandingIndicator = dispatch => node => dispatch(({ visibleNodes }) => {
|
||||
const dummyVisibleNodes = {
|
||||
...visibleNodes,
|
||||
nodes: visibleNodes.nodes.slice(),
|
||||
}
|
||||
dummyVisibleNodes.nodes.splice(
|
||||
dummyVisibleNodes.nodes.indexOf(node) + 1,
|
||||
0,
|
||||
{ virtual: true, name: 'Loading', path: '-' },
|
||||
)
|
||||
return {
|
||||
visibleNodes: dummyVisibleNodes
|
||||
}
|
||||
})
|
||||
|
||||
const updateVisibleNodes = dispatch => () => {
|
||||
const { visibleNodes } = visibleNodesGenerator
|
||||
dispatch({ visibleNodes })
|
||||
}
|
||||
|
||||
export default {
|
||||
init,
|
||||
setUpTree,
|
||||
execAfterRender,
|
||||
setStateText,
|
||||
handleKeyDown,
|
||||
onFocusSearchBar,
|
||||
handleSearchKeyChange,
|
||||
setExpand,
|
||||
toggleNodeExpansion,
|
||||
focusNode,
|
||||
onNodeClick,
|
||||
updateVisibleNodes,
|
||||
mountExpandingIndicator,
|
||||
}
|
||||
307
src/driver/core/FileExplorer.ts
Normal file
307
src/driver/core/FileExplorer.ts
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
import * as ini from 'ini'
|
||||
import DOMHelper from 'utils/DOMHelper'
|
||||
import treeParser from 'utils/treeParser'
|
||||
import URLHelper from 'utils/URLHelper'
|
||||
import VisibleNodesGenerator, { TreeNode } from 'utils/VisibleNodesGenerator'
|
||||
import GitHubHelper, { BlobData, TreeData, MetaData } from 'utils/GitHubHelper'
|
||||
import { MethodCreator } from 'driver/connect'
|
||||
import { Props } from 'components/FileExplorer'
|
||||
import Node from 'components/Node'
|
||||
import { VisibleNodes } from 'components/SideBar'
|
||||
|
||||
export type ConnectorState = {
|
||||
stateText: string
|
||||
visibleNodes: VisibleNodes
|
||||
|
||||
init: () => void
|
||||
execAfterRender: () => void
|
||||
handleKeyDown: React.KeyboardEventHandler
|
||||
handleSearchKeyChange: React.FormEventHandler
|
||||
onNodeClick: Node['props']['onClick']
|
||||
onFocusSearchBar: React.FocusEventHandler
|
||||
setUpTree: (treeData?: TreeData) => void
|
||||
}
|
||||
|
||||
type DepthMap = Map<TreeNode, number>
|
||||
|
||||
function getVisibleParentNode(nodes: TreeNode[], focusedNode: TreeNode, depths: DepthMap) {
|
||||
const focusedNodeIndex = nodes.indexOf(focusedNode)
|
||||
const focusedNodeDepth = depths.get(focusedNode)
|
||||
let indexOfParentNode = focusedNodeIndex - 1
|
||||
while (indexOfParentNode !== -1 && depths.get(nodes[indexOfParentNode]) >= focusedNodeDepth) {
|
||||
--indexOfParentNode
|
||||
}
|
||||
const parentNode = nodes[indexOfParentNode]
|
||||
return parentNode
|
||||
}
|
||||
|
||||
type Task = () => void
|
||||
const tasksAfterRender: (Task)[] = []
|
||||
const visibleNodesGenerator = new VisibleNodesGenerator()
|
||||
|
||||
const init: MethodCreator = dispatch => () => dispatch.for(setStateText, 'Fetching File List...')
|
||||
|
||||
function resolveGitModules(root: TreeNode, blobData: BlobData) {
|
||||
if (blobData) {
|
||||
if (blobData.encoding === 'base64') {
|
||||
const content = atob(blobData.content)
|
||||
const parsed = ini.parse(content)
|
||||
Object.values(parsed).map((value: { url: string; path: string }) => {
|
||||
const { url, path } = value
|
||||
// for now, handle modules at root only
|
||||
const node = root.contents.find(node => node.path === path)
|
||||
if (node) {
|
||||
node.url = url
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const setUpTree: MethodCreator<Props> = dispatch => () =>
|
||||
dispatch.prepare(async ({ treeData, metaData, compressSingletonFolder, accessToken }) => {
|
||||
if (!treeData) return
|
||||
dispatch.for(setStateText, 'Rendering File List...')
|
||||
const { root, gitModules } = treeParser.parse(treeData, metaData)
|
||||
|
||||
if (gitModules) {
|
||||
const blobData = await GitHubHelper.getBlobData({
|
||||
userName: metaData.userName,
|
||||
repoName: metaData.repoName,
|
||||
fileSHA: gitModules.sha,
|
||||
accessToken,
|
||||
})
|
||||
|
||||
resolveGitModules(root as TreeNode, blobData)
|
||||
}
|
||||
|
||||
visibleNodesGenerator.setCompress(compressSingletonFolder)
|
||||
await visibleNodesGenerator.plantTree(root as TreeNode)
|
||||
|
||||
tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
dispatch.for(setStateText, null)
|
||||
const currentPath = URLHelper.getCurrentPath(metaData.branchName)
|
||||
if (currentPath.length) {
|
||||
const nodeExpandedTo = visibleNodesGenerator.expandTo(currentPath.join('/'))
|
||||
if (nodeExpandedTo) {
|
||||
visibleNodesGenerator.focusNode(nodeExpandedTo)
|
||||
const { nodes } = visibleNodesGenerator.visibleNodes
|
||||
tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(nodes.indexOf(nodeExpandedTo)))
|
||||
}
|
||||
}
|
||||
dispatch.for(updateVisibleNodes)
|
||||
})
|
||||
|
||||
const execAfterRender: MethodCreator = dispatch => () => {
|
||||
for (const task of tasksAfterRender) {
|
||||
task()
|
||||
}
|
||||
tasksAfterRender.length = 0
|
||||
}
|
||||
|
||||
const setStateText: MethodCreator<Props> = dispatch => (text: string) =>
|
||||
dispatch.state({
|
||||
stateText: text,
|
||||
})
|
||||
|
||||
const handleKeyDown: MethodCreator = dispatch => event =>
|
||||
dispatch.prepare((_, { visibleNodes: { nodes, focusedNode, expandedNodes, depths } }) => {
|
||||
function handleVerticalMove(index: number) {
|
||||
if (0 <= index && index < nodes.length) {
|
||||
DOMHelper.focusFileExplorer()
|
||||
dispatch.for(focusNode, nodes[index])
|
||||
} else {
|
||||
DOMHelper.focusSearchInput()
|
||||
dispatch.for(focusNode, null)
|
||||
}
|
||||
}
|
||||
|
||||
const { key } = event
|
||||
// prevent document body scrolling if the keypress results in Gitako action
|
||||
let muteEvent = true
|
||||
if (focusedNode) {
|
||||
const focusedNodeIndex = nodes.indexOf(focusedNode)
|
||||
switch (key) {
|
||||
case 'ArrowUp':
|
||||
// focus on previous node
|
||||
handleVerticalMove(focusedNodeIndex - 1)
|
||||
break
|
||||
|
||||
case 'ArrowDown':
|
||||
// focus on next node
|
||||
handleVerticalMove(focusedNodeIndex + 1)
|
||||
break
|
||||
|
||||
case 'ArrowLeft':
|
||||
// collapse node or go to parent node
|
||||
if (expandedNodes.has(focusedNode)) {
|
||||
dispatch.for(setExpand, focusedNode, false)
|
||||
} else {
|
||||
// go forward to the start of the list, find the closest node with lower depth
|
||||
const parentNode = getVisibleParentNode(nodes, focusedNode, depths)
|
||||
if (parentNode) {
|
||||
dispatch.for(focusNode, parentNode)
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
// consider the two keys as 'confirm' key
|
||||
case 'ArrowRight':
|
||||
// expand node or focus on first content node or redirect to file page
|
||||
if (focusedNode.type === 'tree') {
|
||||
if (expandedNodes.has(focusedNode)) {
|
||||
const nextNode = nodes[focusedNodeIndex + 1]
|
||||
if (depths.get(nextNode) > depths.get(focusedNode)) {
|
||||
dispatch.for(focusNode, nextNode)
|
||||
}
|
||||
} else {
|
||||
dispatch.for(setExpand, focusedNode, true)
|
||||
}
|
||||
} else if (focusedNode.type === 'blob') {
|
||||
DOMHelper.loadWithPJAX(focusedNode.url)
|
||||
} else if (focusedNode.type === 'commit') {
|
||||
window.open(focusedNode.url)
|
||||
}
|
||||
break
|
||||
case 'Enter':
|
||||
// expand node or redirect to file page
|
||||
if (focusedNode.type === 'tree') {
|
||||
dispatch.for(setExpand, focusedNode, true)
|
||||
} else if (focusedNode.type === 'blob') {
|
||||
DOMHelper.loadWithPJAX(focusedNode.url)
|
||||
} else if (focusedNode.type === 'commit') {
|
||||
window.open(focusedNode.url)
|
||||
}
|
||||
break
|
||||
default:
|
||||
muteEvent = false
|
||||
}
|
||||
if (muteEvent) {
|
||||
event.preventDefault()
|
||||
}
|
||||
} else {
|
||||
// now search input is focused
|
||||
if (nodes.length) {
|
||||
switch (key) {
|
||||
case 'ArrowDown':
|
||||
DOMHelper.focusFileExplorer()
|
||||
dispatch.for(focusNode, nodes[0])
|
||||
break
|
||||
case 'ArrowUp':
|
||||
DOMHelper.focusFileExplorer()
|
||||
dispatch.for(focusNode, nodes[nodes.length - 1])
|
||||
break
|
||||
default:
|
||||
muteEvent = false
|
||||
}
|
||||
if (muteEvent) {
|
||||
event.preventDefault()
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const onFocusSearchBar: MethodCreator = dispatch => () => dispatch.for(focusNode, null)
|
||||
|
||||
const handleSearchKeyChange: MethodCreator = dispatch => {
|
||||
let i = 0
|
||||
return async event => {
|
||||
const searchKey = event.target.value
|
||||
const j = (i += 1)
|
||||
await visibleNodesGenerator.search(searchKey)
|
||||
if (i === j) dispatch.for(updateVisibleNodes)
|
||||
}
|
||||
}
|
||||
|
||||
const delayExpandThreshold = 400
|
||||
function shouldDelayExpand(node: TreeNode) {
|
||||
return (
|
||||
visibleNodesGenerator.visibleNodes.expandedNodes.has(node) &&
|
||||
node.contents.length > delayExpandThreshold
|
||||
)
|
||||
}
|
||||
|
||||
const setExpand: MethodCreator = dispatch => (node, expand) => {
|
||||
visibleNodesGenerator.setExpand(node, expand)
|
||||
const applyChanges = () => dispatch.for(focusNode, node)
|
||||
if (shouldDelayExpand(node)) {
|
||||
dispatch.for(mountExpandingIndicator, node)
|
||||
tasksAfterRender.push(() => setTimeout(applyChanges, 0))
|
||||
} else {
|
||||
applyChanges()
|
||||
}
|
||||
}
|
||||
|
||||
const toggleNodeExpansion: MethodCreator = dispatch => (node, skipScrollToNode) => {
|
||||
visibleNodesGenerator.toggleExpand(node)
|
||||
const applyChanges = () => {
|
||||
dispatch.for(focusNode, node, skipScrollToNode)
|
||||
tasksAfterRender.push(DOMHelper.focusFileExplorer)
|
||||
}
|
||||
if (shouldDelayExpand(node)) {
|
||||
dispatch.for(mountExpandingIndicator, node)
|
||||
tasksAfterRender.push(() => setTimeout(applyChanges, 0))
|
||||
} else {
|
||||
applyChanges()
|
||||
}
|
||||
}
|
||||
|
||||
const focusNode: MethodCreator = dispatch => (node, skipScroll) =>
|
||||
dispatch.prepare((_, { visibleNodes: { nodes } }) => {
|
||||
visibleNodesGenerator.focusNode(node)
|
||||
if (node && !skipScroll) {
|
||||
// when focus a node not in viewport(by keyboard), scroll to it
|
||||
const indexOfToBeFocusedNode = nodes.indexOf(node)
|
||||
tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(indexOfToBeFocusedNode))
|
||||
}
|
||||
dispatch.for(updateVisibleNodes)
|
||||
})
|
||||
|
||||
const onNodeClick: MethodCreator = dispatch => node => {
|
||||
if (node.type === 'tree') {
|
||||
dispatch.for(toggleNodeExpansion, node, true)
|
||||
} else if (node.type === 'blob') {
|
||||
dispatch.for(focusNode, node, true)
|
||||
DOMHelper.loadWithPJAX(node.url)
|
||||
} else if (node.type === 'commit') {
|
||||
window.open(node.url, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
const mountExpandingIndicator: MethodCreator = dispatch => node =>
|
||||
dispatch.prepare((_, { visibleNodes }) => {
|
||||
const dummyVisibleNodes = {
|
||||
...visibleNodes,
|
||||
nodes: visibleNodes.nodes.slice(),
|
||||
}
|
||||
dummyVisibleNodes.nodes.splice(dummyVisibleNodes.nodes.indexOf(node) + 1, 0, {
|
||||
virtual: true,
|
||||
name: 'Loading',
|
||||
path: '-',
|
||||
type: 'virtual',
|
||||
})
|
||||
dispatch.state({
|
||||
visibleNodes: dummyVisibleNodes,
|
||||
})
|
||||
})
|
||||
|
||||
const updateVisibleNodes: MethodCreator = dispatch => () => {
|
||||
const { visibleNodes } = visibleNodesGenerator
|
||||
dispatch.state({ visibleNodes })
|
||||
}
|
||||
|
||||
export default {
|
||||
init,
|
||||
setUpTree,
|
||||
execAfterRender,
|
||||
setStateText,
|
||||
handleKeyDown,
|
||||
onFocusSearchBar,
|
||||
handleSearchKeyChange,
|
||||
setExpand,
|
||||
toggleNodeExpansion,
|
||||
focusNode,
|
||||
onNodeClick,
|
||||
updateVisibleNodes,
|
||||
mountExpandingIndicator,
|
||||
}
|
||||
|
|
@ -762,6 +762,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/filewriter/-/filewriter-0.0.28.tgz#c054e8af4d9dd75db4e63abc76f885168714d4b3"
|
||||
integrity sha1-wFTor02d11205jq8dviFFocU1LM=
|
||||
|
||||
"@types/ini@^1.3.30":
|
||||
version "1.3.30"
|
||||
resolved "https://registry.yarnpkg.com/@types/ini/-/ini-1.3.30.tgz#d1485459c9fad84e937414b832a2adb649eab379"
|
||||
integrity sha512-2+iF8zPSbpU83UKE+PNd4r/MhwNAdyGpk3H+VMgEH3EhjFZq1kouLgRoZrmIcmoGX97xFvqdS44DkICR5Nz3tQ==
|
||||
|
||||
"@types/nprogress@^0.0.29":
|
||||
version "0.0.29"
|
||||
resolved "https://registry.yarnpkg.com/@types/nprogress/-/nprogress-0.0.29.tgz#060bd510022a005f1840234030d3132fb9195471"
|
||||
|
|
|
|||
Loading…
Reference in a new issue