mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
refactor(FileExplorer): split logic
This commit is contained in:
parent
84d1859991
commit
0bfca704a4
2 changed files with 223 additions and 176 deletions
|
|
@ -1,4 +1,8 @@
|
|||
import React from 'react'
|
||||
import PropTypes from "prop-types";
|
||||
|
||||
import connect from '../driver/connect'
|
||||
|
||||
import SearchBar from './SearchBar'
|
||||
import Node from './Node'
|
||||
|
||||
|
|
@ -22,219 +26,259 @@ function getVisibleParentNode(nodes, focusedNode, depths) {
|
|||
return parentNode
|
||||
}
|
||||
|
||||
export default class List extends React.Component {
|
||||
static defaultProps = {
|
||||
treeData: null,
|
||||
metaData: null,
|
||||
freeze: false,
|
||||
}
|
||||
const tasksAfterRender = []
|
||||
const visibleNodesGenerator = new VisibleNodesGenerator()
|
||||
|
||||
state = {
|
||||
// generated by this.visibleNodesGenerator
|
||||
visibleNodes: null,
|
||||
}
|
||||
|
||||
tasksAfterRender = []
|
||||
visibleNodesGenerator = new VisibleNodesGenerator()
|
||||
|
||||
componentWillMount() {
|
||||
const { treeData, metaData } = this.props
|
||||
const { root, nodes } = treeParser.parse(treeData, metaData)
|
||||
this.visibleNodesGenerator.plantTree(root, nodes)
|
||||
const currentPath = URLHelper.getCurrentPath(true)
|
||||
this.tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
if (currentPath.length) {
|
||||
const nodeExpandedTo = this.visibleNodesGenerator.expandTo(currentPath)
|
||||
if (nodeExpandedTo) {
|
||||
this.visibleNodesGenerator.focusNode(nodeExpandedTo)
|
||||
const { nodes } = this.visibleNodesGenerator.visibleNodes
|
||||
this.tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(nodes.indexOf(nodeExpandedTo)))
|
||||
}
|
||||
const init = dispatch => () => dispatch(async (state, { treeData, metaData, accessToken }) => {
|
||||
const { root, nodes } = treeParser.parse(treeData, metaData)
|
||||
visibleNodesGenerator.plantTree(root, nodes)
|
||||
const currentPath = URLHelper.getCurrentPath(true)
|
||||
tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
if (currentPath.length) {
|
||||
const nodeExpandedTo = visibleNodesGenerator.expandTo(currentPath)
|
||||
if (nodeExpandedTo) {
|
||||
visibleNodesGenerator.focusNode(nodeExpandedTo)
|
||||
const { nodes } = visibleNodesGenerator.visibleNodes
|
||||
tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(nodes.indexOf(nodeExpandedTo)))
|
||||
}
|
||||
this.updateVisibleNodes()
|
||||
}
|
||||
dispatch(updateVisibleNodes)
|
||||
})
|
||||
|
||||
componentDidMount() {
|
||||
this.execAfterRender()
|
||||
const execAfterRender = dispatch => () => {
|
||||
for (const task of tasksAfterRender) {
|
||||
task()
|
||||
}
|
||||
tasksAfterRender.length = 0
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
this.execAfterRender()
|
||||
}
|
||||
const handleKeyDown = dispatch => ({ key }) => dispatch(({ visibleNodes: { nodes, focusedNode, expandedNodes, depths } }) => {
|
||||
let shouldStopPropagation = true // prevent body scrolling
|
||||
if (focusedNode) {
|
||||
const focusedNodeIndex = nodes.indexOf(focusedNode)
|
||||
switch (key) {
|
||||
case 'ArrowUp':
|
||||
// focus on previous node
|
||||
if (focusedNodeIndex === 0) {
|
||||
dispatch(focusNode, null)
|
||||
tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
} else {
|
||||
dispatch(focusNode, nodes[focusedNodeIndex - 1])
|
||||
}
|
||||
break
|
||||
|
||||
execAfterRender() {
|
||||
for (const task of this.tasksAfterRender) {
|
||||
task()
|
||||
}
|
||||
this.tasksAfterRender.length = 0
|
||||
}
|
||||
case 'ArrowDown':
|
||||
// focus on next node
|
||||
if (focusedNodeIndex + 1 < nodes.length) {
|
||||
dispatch(focusNode, nodes[focusedNodeIndex + 1])
|
||||
} else {
|
||||
dispatch(focusNode, null)
|
||||
tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
}
|
||||
break
|
||||
|
||||
updateVisibleNodes() {
|
||||
const { visibleNodes } = this.visibleNodesGenerator
|
||||
this.setState({ visibleNodes })
|
||||
}
|
||||
|
||||
handleKeyDown = event => {
|
||||
const { key } = event
|
||||
const { visibleNodes: { nodes, focusedNode, expandedNodes, depths } } = this.state
|
||||
let shouldStopPropagation = true // prevent body scrolling
|
||||
if (focusedNode) {
|
||||
const focusedNodeIndex = nodes.indexOf(focusedNode)
|
||||
switch (key) {
|
||||
case 'ArrowUp':
|
||||
// focus on previous node
|
||||
if (focusedNodeIndex === 0) {
|
||||
this.focusNode(null)
|
||||
this.tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
} else {
|
||||
this.focusNode(nodes[focusedNodeIndex - 1])
|
||||
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
|
||||
}
|
||||
break
|
||||
|
||||
case 'ArrowDown':
|
||||
// focus on next node
|
||||
if (focusedNodeIndex + 1 < nodes.length) {
|
||||
this.focusNode(nodes[focusedNodeIndex + 1])
|
||||
} else {
|
||||
this.focusNode(null)
|
||||
this.tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
}
|
||||
break
|
||||
|
||||
case 'ArrowLeft':
|
||||
// collapse node or go to parent node
|
||||
// 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)) {
|
||||
this.setExpand(focusedNode, false)
|
||||
const nextNode = nodes[focusedNodeIndex + 1]
|
||||
if (depths.get(nextNode) > depths.get(focusedNode)) {
|
||||
dispatch(focusNode, nextNode)
|
||||
}
|
||||
} else {
|
||||
// go forward to the start of the list, find the closest node with lower depth
|
||||
const parentNode = getVisibleParentNode(nodes, focusedNode, depths)
|
||||
if (parentNode) {
|
||||
this.focusNode(parentNode)
|
||||
}
|
||||
dispatch(setExpand, focusedNode, true)
|
||||
}
|
||||
break
|
||||
} else if (focusedNode.type === 'blob') {
|
||||
DOMHelper.loadWithPJAX(focusedNode.url)
|
||||
} else if (focusedNode.type === 'commit') {
|
||||
// redirect to its parent folder
|
||||
DOMHelper.loadWithPJAX(focusedNode.parent.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') {
|
||||
// redirect to its parent folder
|
||||
DOMHelper.loadWithPJAX(focusedNode.parent.url)
|
||||
}
|
||||
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)) {
|
||||
this.focusNode(nextNode)
|
||||
}
|
||||
} else {
|
||||
this.setExpand(focusedNode, true)
|
||||
}
|
||||
} else if (focusedNode.type === 'blob') {
|
||||
DOMHelper.loadWithPJAX(focusedNode.url)
|
||||
} else if (focusedNode.type === 'commit') {
|
||||
// redirect to its parent folder
|
||||
DOMHelper.loadWithPJAX(focusedNode.parent.url)
|
||||
}
|
||||
default:
|
||||
shouldStopPropagation = false
|
||||
}
|
||||
} else {
|
||||
// now search input is focused
|
||||
if (nodes.length) {
|
||||
switch (key) {
|
||||
case 'ArrowDown':
|
||||
dispatch(focusNode, nodes[0])
|
||||
break
|
||||
case 'Enter':
|
||||
// expand node or redirect to file page
|
||||
if (focusedNode.type === 'tree') {
|
||||
this.setExpand(focusedNode, true)
|
||||
} else if (focusedNode.type === 'blob') {
|
||||
DOMHelper.loadWithPJAX(focusedNode.url)
|
||||
} else if (focusedNode.type === 'commit') {
|
||||
// redirect to its parent folder
|
||||
DOMHelper.loadWithPJAX(focusedNode.parent.url)
|
||||
}
|
||||
case 'ArrowUp':
|
||||
dispatch(focusNode, nodes[nodes.length - 1])
|
||||
break
|
||||
|
||||
default:
|
||||
shouldStopPropagation = false
|
||||
}
|
||||
} else {
|
||||
// now search input is focused
|
||||
if (nodes.length) {
|
||||
switch (key) {
|
||||
case 'ArrowDown':
|
||||
this.focusNode(nodes[0])
|
||||
break
|
||||
case 'ArrowUp':
|
||||
this.focusNode(nodes[nodes.length - 1])
|
||||
break
|
||||
default:
|
||||
shouldStopPropagation = false
|
||||
}
|
||||
} else {
|
||||
shouldStopPropagation = false
|
||||
}
|
||||
}
|
||||
if (shouldStopPropagation) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
shouldStopPropagation = false
|
||||
}
|
||||
}
|
||||
if (shouldStopPropagation) {
|
||||
event.stopPropagation()
|
||||
event.preventDefault()
|
||||
}
|
||||
})
|
||||
|
||||
handleSearchKeyChange = async event => {
|
||||
const searchKey = event.target.value
|
||||
await this.visibleNodesGenerator.search(searchKey)
|
||||
this.updateVisibleNodes()
|
||||
const handleSearchKeyChange = dispatch => async event => {
|
||||
const searchKey = event.target.value
|
||||
await visibleNodesGenerator.search(searchKey)
|
||||
dispatch(updateVisibleNodes)
|
||||
}
|
||||
|
||||
const setExpand = dispatch => (node, expand) => {
|
||||
visibleNodesGenerator.setExpand(node, expand)
|
||||
dispatch(focusNode, node)
|
||||
tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
}
|
||||
|
||||
const toggleNodeExpansion = dispatch => (node, skipScrollToNode) => {
|
||||
visibleNodesGenerator.toggleExpand(node)
|
||||
dispatch(focusNode, node, skipScrollToNode)
|
||||
tasksAfterRender.push(DOMHelper.focusFileExplorer)
|
||||
}
|
||||
|
||||
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))
|
||||
tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
}
|
||||
dispatch(updateVisibleNodes)
|
||||
})
|
||||
|
||||
const onNodeClick = dispatch => (node) => {
|
||||
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') {
|
||||
DOMHelper.loadWithPJAX(node.parent.url)
|
||||
}
|
||||
}
|
||||
|
||||
const updateVisibleNodes = dispatch => () => {
|
||||
const { visibleNodes } = visibleNodesGenerator
|
||||
dispatch({ visibleNodes })
|
||||
}
|
||||
|
||||
class List extends React.Component {
|
||||
static propTyps = {
|
||||
treeData: PropTypes.object,
|
||||
metaData: PropTypes.object,
|
||||
freeze: PropTypes.boolean,
|
||||
visibleNodes: PropTypes.object,
|
||||
|
||||
init: PropTypes.func.isRequired,
|
||||
execAfterRender: PropTypes.func.isRequired,
|
||||
handleKeyDown: PropTypes.func.isRequired,
|
||||
handleSearchKeyChange: PropTypes.func.isRequired,
|
||||
setExpand: PropTypes.func.isRequired,
|
||||
toggleNodeExpansion: PropTypes.func.isRequired,
|
||||
focusNode: PropTypes.func.isRequired,
|
||||
onNodeClick: PropTypes.func.isRequired,
|
||||
updateVisibleNodes: PropTypes.func.isRequired,
|
||||
}
|
||||
|
||||
setExpand = (node, expand) => {
|
||||
this.visibleNodesGenerator.setExpand(node, expand)
|
||||
this.focusNode(node)
|
||||
this.tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
static defaultProps = {
|
||||
treeData: null,
|
||||
metaData: null,
|
||||
freeze: false,
|
||||
visibleNodes: null,
|
||||
}
|
||||
|
||||
toggleNodeExpand = (node, skipScrollToNode) => {
|
||||
this.visibleNodesGenerator.toggleExpand(node)
|
||||
this.focusNode(node, skipScrollToNode)
|
||||
this.tasksAfterRender.push(DOMHelper.focusFileExplorer)
|
||||
constructor(props) {
|
||||
super(props)
|
||||
const { init } = props
|
||||
init()
|
||||
}
|
||||
|
||||
focusNode = (node, skipScroll) => {
|
||||
this.visibleNodesGenerator.focusNode(node)
|
||||
if (node && !skipScroll) {
|
||||
// when focus a node not in viewport(by keyboard), scroll to it
|
||||
const { visibleNodes: { nodes } } = this.state
|
||||
const indexOfToBeFocusedNode = nodes.indexOf(node)
|
||||
this.tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(indexOfToBeFocusedNode))
|
||||
this.tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
}
|
||||
this.updateVisibleNodes()
|
||||
componentDidMount() {
|
||||
const { execAfterRender } = this.props
|
||||
execAfterRender()
|
||||
}
|
||||
|
||||
onNodeClick = (node) => {
|
||||
if (node.type === 'tree') {
|
||||
this.toggleNodeExpand(node, true)
|
||||
} else if (node.type === 'blob') {
|
||||
this.focusNode(node, true)
|
||||
DOMHelper.loadWithPJAX(node.url)
|
||||
} else if (node.type === 'commit') {
|
||||
DOMHelper.loadWithPJAX(node.parent.url)
|
||||
}
|
||||
componentDidUpdate() {
|
||||
const { execAfterRender } = this.props
|
||||
execAfterRender()
|
||||
}
|
||||
|
||||
render() {
|
||||
const { visibleNodes: { nodes, depths, focusedNode, expandedNodes } } = this.state
|
||||
const { freeze } = this.props
|
||||
const {
|
||||
visibleNodes,
|
||||
freeze,
|
||||
handleKeyDown,
|
||||
handleSearchKeyChange,
|
||||
onNodeClick
|
||||
} = this.props
|
||||
const {
|
||||
nodes,
|
||||
depths,
|
||||
focusedNode,
|
||||
expandedNodes,
|
||||
} = visibleNodes || {}
|
||||
return (
|
||||
<div className={cx(`file-explorer`, { freeze })} tabIndex={-1} onKeyDown={this.handleKeyDown}>
|
||||
<SearchBar onSearchKeyChange={this.handleSearchKeyChange} />
|
||||
{nodes.length === 0 ? (
|
||||
<div className={cx(`file-explorer`, { freeze })} tabIndex={-1} onKeyDown={handleKeyDown}>
|
||||
<SearchBar onSearchKeyChange={handleSearchKeyChange} />
|
||||
{!visibleNodes || !nodes || nodes.length === 0 ? (
|
||||
<label className={'no-results'}>No results found.</label>
|
||||
) : (
|
||||
<div className={'files'}>
|
||||
{nodes.map(node => (
|
||||
<Node
|
||||
key={node.path}
|
||||
node={node}
|
||||
depth={depths.get(node)}
|
||||
focused={focusedNode === node}
|
||||
expanded={expandedNodes.has(node)}
|
||||
onClick={this.onNodeClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className={'files'}>
|
||||
{nodes.map(node => (
|
||||
<Node
|
||||
key={node.path}
|
||||
node={node}
|
||||
depth={depths.get(node)}
|
||||
focused={focusedNode === node}
|
||||
expanded={expandedNodes.has(node)}
|
||||
onClick={onNodeClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect({
|
||||
init,
|
||||
execAfterRender,
|
||||
handleKeyDown,
|
||||
handleSearchKeyChange,
|
||||
setExpand,
|
||||
toggleNodeExpansion,
|
||||
focusNode,
|
||||
onNodeClick,
|
||||
updateVisibleNodes,
|
||||
})(List)
|
||||
|
|
|
|||
|
|
@ -131,6 +131,9 @@ export default class VisibleNodesGenerator {
|
|||
focusedNode = null
|
||||
focusNode(node) {
|
||||
this.focusedNode = node
|
||||
this.visibleNodes.focusedNode = node
|
||||
this.visibleNodes = {
|
||||
...this.visibleNodes,
|
||||
focusedNode: node,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue