Skip to content

filetree arrow key navigation #10

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Oct 11, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 17 additions & 9 deletions app/components/FileTree/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,31 @@ import api from '../../api'
import * as TabActions from '../Tab/actions'

export const FILETREE_SELECT_NODE = 'FILETREE_SELECT_NODE'
export const FILETREE_SELECT_NODE_KEY = 'FILETREE_SELECT_NODE_KEY'
export function selectNode (node, multiSelect = false) {
return {
type: FILETREE_SELECT_NODE,
node,
multiSelect
if (typeof node === 'number') {
return {
type: FILETREE_SELECT_NODE_KEY,
offset: node
}
} else {
return {
type: FILETREE_SELECT_NODE,
node,
multiSelect
}
}
}

export function openNode (node, shoudlBeFolded = null, deep = false) {
export function openNode (node, shouldBeFolded = null, deep = false) {
return (dispatch, getState) => {
if (node.isDir) {
if (node.shouldBeUpdated) {
api.fetchPath(node.path)
.then(data => dispatch(loadNodeData(data, node)))
.then(() => dispatch(toggleNodeFold(node, shoudlBeFolded, deep)))
.then(() => dispatch(toggleNodeFold(node, shouldBeFolded, deep)))
} else {
dispatch(toggleNodeFold(node, shoudlBeFolded, deep))
dispatch(toggleNodeFold(node, shouldBeFolded, deep))
}
} else {
api.readFile(node.path)
Expand Down Expand Up @@ -52,11 +60,11 @@ export function openNode (node, shoudlBeFolded = null, deep = false) {
}

export const FILETREE_FOLD_NODE = 'FILETREE_FOLD_NODE'
export function toggleNodeFold (node, shoudlBeFolded = null, deep = false) {
export function toggleNodeFold (node, shouldBeFolded = null, deep = false) {
return {
type: FILETREE_FOLD_NODE,
node,
shoudlBeFolded,
shouldBeFolded,
deep
}
}
Expand Down
84 changes: 61 additions & 23 deletions app/components/FileTree/index.jsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/* @flow weak */
import _ from 'lodash';
import React, { Component, PropTypes } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import cx from 'classnames';
import ContextMenu from '../ContextMenu';
import * as FileTreeActions from './actions';
import FileTreeContextMenuItems from './contextMenuItems';
import _ from 'lodash'
import React, { Component, PropTypes } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import cx from 'classnames'
import ContextMenu from '../ContextMenu'
import * as FileTreeActions from './actions'
import FileTreeContextMenuItems from './contextMenuItems'
import subscribeToFileChange from './subscribeToFileChange'


Expand All @@ -23,14 +23,14 @@ class FileTree extends Component {

componentDidMount() {
subscribeToFileChange()
this.props.initializeFileTree();
this.props.initializeFileTree()
}

render() {
const {FileTreeState, ...actionProps} = this.props;
const {isContextMenuActive, contextMenuPos} = this.state;
const {FileTreeState, ...actionProps} = this.props
const {isContextMenuActive, contextMenuPos} = this.state
return (
<div className='filetree-container'>
<div className='filetree-container' tabIndex={1} onKeyDown={this.onKeyDown}>
<FileTreeNode node={FileTreeState.rootNode}
onContextMenu={this.onContextMenu} {...actionProps} />
<ContextMenu items={FileTreeContextMenuItems}
Expand All @@ -39,7 +39,7 @@ class FileTree extends Component {
context={this.state.contextNode}
deactivate={this.setState.bind(this, {isContextMenuActive: false})} />
</div>
);
)
}

onContextMenu = (e, node) => {
Expand All @@ -51,30 +51,62 @@ class FileTree extends Component {
contextNode: node
})
}

onKeyDown = (e) => {
var curNode = this.props.FileTreeState.focusedNodes[0]
if (e.keyCode === 13 || 37 <= e.keyCode && e.keyCode <= 40) e.preventDefault()
switch (e.key) {
case 'ArrowDown':
this.props.selectNode(1)
break
case 'ArrowUp':
this.props.selectNode(-1)
break
case 'ArrowRight':
if (!curNode.isDir) break
if (curNode.isFolded) {
this.props.openNode(curNode, false)
} else {
this.props.selectNode(1)
}
break
case 'ArrowLeft':
if (!curNode.isDir || curNode.isFolded) {
this.props.selectNode(curNode.parent)
break
}
if (curNode.isDir) this.props.openNode(curNode, true)
break
case 'Enter':
this.props.openNode(curNode)
break
default:
}
}
}

FileTree = connect(
state => {
return {FileTreeState: state.FileTreeState}
}, dispatch => {
return bindActionCreators(FileTreeActions, dispatch);
return bindActionCreators(FileTreeActions, dispatch)
}
)(FileTree);
)(FileTree)


class FileTreeNode extends Component {
constructor(props) {
super(props);
super(props)
}

render() {
const {node, ...actionProps} = this.props;
const {openNode, selectNode, onContextMenu} = actionProps;
const {node, ...actionProps} = this.props
const {openNode, selectNode, onContextMenu} = actionProps
return (
<div className='filetree-node-container'
onContextMenu={e => {selectNode(node);onContextMenu(e, node)} }>
<div
className={cx('filetree-node', {'focus':node.isFocused})}
onContextMenu={e => {selectNode(node); onContextMenu(e, node)} }>
<div className={cx('filetree-node', {'focus':node.isFocused})}
ref={r => this.nodeDOM = r}
onDoubleClick={e => openNode(node)}
onClick={e => selectNode(node)} >
<span className='filetree-node-arrow'
Expand Down Expand Up @@ -109,8 +141,14 @@ class FileTreeNode extends Component {
: null }

</div>
);
)
}

componentDidUpdate () {
if (this.props.node.isFocused) {
this.nodeDOM.scrollIntoViewIfNeeded && this.nodeDOM.scrollIntoViewIfNeeded()
}
}
}

export default FileTree;
export default FileTree
114 changes: 102 additions & 12 deletions app/components/FileTree/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import {
FILETREE_LOAD_DATA,
FILETREE_FOLD_NODE,
FILETREE_SELECT_NODE,
FILETREE_SELECT_NODE_KEY,
FILETREE_REMOVE_NODE
} from './actions'


let focusedNodes = []
class Node {
constructor (nodeInfo) {
const {
Expand Down Expand Up @@ -51,12 +54,81 @@ class Node {

set depth (v) { /* no-op */ }

findChildNodeByPathComponents (pathComponents) {
focus () {
this.isFocused = true
if (focusedNodes.indexOf(this) === -1) focusedNodes.push(this)
}

unfocus () {
this.isFocused = false
_.remove(focusedNodes, this)
}

getSiblings () {
if (this.isRoot) return [this]
return this.parent.children
}

prev (jump) {
if (this.isRoot) return this
var siblings = this.getSiblings()
var curIndex = siblings.indexOf(this)
var prevNode = siblings[curIndex - 1]

if (prevNode) {
if (!jump) return prevNode
if (!prevNode.isDir || prevNode.isFolded) return prevNode
if (prevNode.lastChild()) {
return prevNode.lastVisibleDescendant()
} else {
return prevNode
}
} else {
return this.parent
}
}

next (jump) {
if (jump && this.isDir && !this.isFolded) {
if (this.firstChild()) return this.firstChild()
} else if (this.isRoot) {
return this
}

var siblings = this.getSiblings()
var curIndex = siblings.indexOf(this)
var nextNode = siblings[curIndex + 1]

if (nextNode) {
return nextNode
} else {
if (this.parent.isRoot) return this
return this.parent.next()
}
}

firstChild () {
return this.children[0]
}

lastChild () {
return this.children[this.children.length - 1]
}

lastVisibleDescendant () {
var lastChild = this.children[this.children.length - 1]
if (!lastChild) return this
if (!lastChild.isDir) return lastChild
if (lastChild.isFolded) return lastChild
return lastChild.lastVisibleDescendant()
}

_findChildNodeByPathComponents (pathComponents) {
var pathComponent = pathComponents[0]
var childNode = _.filter(this.children, {name: pathComponent})[0]
var nextPathComponents = pathComponents.slice(1)
if (nextPathComponents.length === 0) { return childNode }
return childNode.findChildNodeByPathComponents(nextPathComponents)
return childNode._findChildNodeByPathComponents(nextPathComponents)
}

// apply to not only direct children but all descendants
Expand Down Expand Up @@ -117,15 +189,16 @@ var RootNode = new Node({
const findNodeByPath = (path) => {
if (path === '/') return Node.rootNode
const pathComponents = path.split('/').slice(1)
return Node.rootNode.findChildNodeByPathComponents(pathComponents)
return Node.rootNode._findChildNodeByPathComponents(pathComponents)
}

var _state = {}
_state.rootNode = RootNode

const normalizeState = (_state) => {
var state = {
findNodeByPath: findNodeByPath
findNodeByPath,
focusedNodes
}
state.rootNode = _state.rootNode
state.rootNode.name = config.projectName
Expand All @@ -143,23 +216,40 @@ export default function FileTreeReducer (state = _state, action) {
state.rootNode = RootNode
return normalizeState(state)

case FILETREE_SELECT_NODE_KEY:
var node

if (action.offset === 1) {
node = focusedNodes[0].next(true)
} else if (action.offset === -1 ) {
node = focusedNodes[0].prev(true)
}

if (!multiSelect) {
RootNode.unfocus()
RootNode.forEachDescendant(childNode => childNode.unfocus())
}
node.focus()

return normalizeState(state)

case FILETREE_SELECT_NODE:
var {node, multiSelect} = action

if (!multiSelect) {
RootNode.isFocused = false
RootNode.forEachDescendant(childNode => {
childNode.isFocused = false
})
RootNode.unfocus()
RootNode.forEachDescendant(childNode => childNode.unfocus())
}
node.isFocused = true
node.focus()

return normalizeState(state)

case FILETREE_FOLD_NODE:
var {node, shoudBeFolded, deep} = action
var {node, shouldBeFolded, deep} = action
if (!node.isDir) return state

if (typeof shoudBeFolded === 'boolean') {
var isFolded = shoudBeFolded
if (typeof shouldBeFolded === 'boolean') {
var isFolded = shouldBeFolded
} else {
var isFolded = !node.isFolded
}
Expand Down
4 changes: 4 additions & 0 deletions app/styles/main.styl
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ html, body, #root {
background-color: transparent;
}

*[tabindex]:focus {
outline: 0 none
}

// ::-webkit-scrollbar-thumb {
// background-color: rgba(91, 91, 91, 0.50);
// }
Expand Down