diff --git a/src/components/FileExplorer.js b/src/components/FileExplorer.js index 43f54c9..d50a282 100644 --- a/src/components/FileExplorer.js +++ b/src/components/FileExplorer.js @@ -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 ( -
- - {nodes.length === 0 ? ( +
+ + {!visibleNodes || !nodes || nodes.length === 0 ? ( ) : ( -
- {nodes.map(node => ( - - ))} -
- )} +
+ {nodes.map(node => ( + + ))} +
+ )}
) } } + +export default connect({ + init, + execAfterRender, + handleKeyDown, + handleSearchKeyChange, + setExpand, + toggleNodeExpansion, + focusNode, + onNodeClick, + updateVisibleNodes, +})(List) diff --git a/src/utils/visibleNodesGenerator.js b/src/utils/visibleNodesGenerator.js index 53a4c66..8a735ef 100644 --- a/src/utils/visibleNodesGenerator.js +++ b/src/utils/visibleNodesGenerator.js @@ -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, + } } }