import preact from 'preact' /** @jsx preact.h */ import SearchBar from './SearchBar' import Node from './Node' import cx from '../utils/cx' import DOMHelper from '../utils/DOMHelper' import treeParser from '../utils/treeParser' import URLHelper from '../utils/URLHelper' import VisibleNodesGenerator from '../utils/VisibleNodesGenerator' export default class List extends preact.Component { state = { // generated by this.visibleNodesGenerator visibleNodes: null, } props = { treeData: null, metaData: null, freeze: false, } 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) this.visibleNodesGenerator.focusNode(nodeExpandedTo) const { nodes } = this.visibleNodesGenerator.visibleNodes this.tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(nodes.indexOf(nodeExpandedTo))) } this.updateVisibleNodes() } componentDidMount() { this.execAfterRender() } componentDidUpdate(prevProps, prevState) { this.execAfterRender() } execAfterRender() { for (const task of this.tasksAfterRender) { task() } this.tasksAfterRender.length = 0 } updateVisibleNodes() { const { visibleNodes } = this.visibleNodesGenerator this.setState({ visibleNodes }) this.tasksAfterRender.push(() => DOMHelper.attachPJAX('gitako')) } 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]) } 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 if (expandedNodes.has(focusedNode)) { this.setExpand(focusedNode, false) } else { // go forward to the start of the list, find the closest node with lower depth let indexOfParentNode = focusedNodeIndex const focusedNodeDepth = depths.get(nodes[focusedNodeIndex]) while ( indexOfParentNode !== -1 && depths.get(nodes[indexOfParentNode]) >= focusedNodeDepth ) { --indexOfParentNode } if (indexOfParentNode !== -1) { this.focusNode(nodes[indexOfParentNode]) } } break // consider the two keys as 'confirm' key case 'ArrowRight': case 'Enter': // expand node or redirect to file page if (focusedNode.type === 'tree') { this.setExpand(focusedNode, true) } else { // simulate click to trigger pjax DOMHelper.clickOnNodeElement(focusedNodeIndex) } 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() } } handleSearchKeyChange = async event => { const searchKey = event.target.value await this.visibleNodesGenerator.search(searchKey) this.updateVisibleNodes() } setExpand = (node, expand) => { this.visibleNodesGenerator.setExpand(node, expand) this.focusNode(node) this.tasksAfterRender.push(DOMHelper.focusSearchInput) } toggleNodeExpand = node => { this.visibleNodesGenerator.toggleExpand(node) this.focusNode(node) this.tasksAfterRender.push(DOMHelper.focusFileExplorer) } focusNode = node => { this.visibleNodesGenerator.focusNode(node) if (node) { // 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() } render() { const { visibleNodes: { nodes, depths, focusedNode, expandedNodes } } = this.state const { freeze } = this.props return (
{nodes.length === 0 ? ( ) : (
{nodes.map(node => ( ))}
)}
) } }