refactor(FileExplorer): split logic

This commit is contained in:
EnixCoda 2018-06-22 22:00:07 +08:00
parent 84d1859991
commit 0bfca704a4
2 changed files with 223 additions and 176 deletions

View file

@ -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)

View file

@ -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,
}
}
}