mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
refactor: extract cores
This commit is contained in:
parent
10a239acec
commit
5695771fff
9 changed files with 203 additions and 201 deletions
1
.babelrc
1
.babelrc
|
|
@ -11,6 +11,7 @@
|
|||
"react"
|
||||
],
|
||||
"plugins": [
|
||||
"transform-decorators-legacy",
|
||||
"transform-class-properties",
|
||||
["transform-object-rest-spread", { "useBuiltIns": true }]
|
||||
]
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@
|
|||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-react-require": "^3.0.0",
|
||||
"babel-plugin-transform-class-properties": "^6.24.1",
|
||||
"babel-plugin-transform-decorators-legacy": "^1.3.5",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
|
|
|
|||
|
|
@ -2,187 +2,15 @@ import React from 'react'
|
|||
import PropTypes from "prop-types";
|
||||
|
||||
import connect from '../driver/connect'
|
||||
import { FileExplorer as FileExplorerCore } from '../driver/core'
|
||||
|
||||
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'
|
||||
|
||||
function getVisibleParentNode(nodes, focusedNode, depths) {
|
||||
const focusedNodeIndex = nodes.indexOf(focusedNode)
|
||||
const focusedNodeDepth = depths.get(focusedNode)
|
||||
let indexOfParentNode = focusedNodeIndex - 1
|
||||
while (
|
||||
indexOfParentNode !== -1 &&
|
||||
depths.get(nodes[indexOfParentNode]) >= focusedNodeDepth
|
||||
) {
|
||||
--indexOfParentNode
|
||||
}
|
||||
const parentNode = nodes[indexOfParentNode]
|
||||
return parentNode
|
||||
}
|
||||
|
||||
const tasksAfterRender = []
|
||||
const visibleNodesGenerator = new VisibleNodesGenerator()
|
||||
|
||||
const init = dispatch => () => dispatch(async (state, { treeData, metaData, compressSingletonFolder }) => {
|
||||
const { root } = treeParser.parse(treeData, metaData)
|
||||
visibleNodesGenerator.setCompress(compressSingletonFolder)
|
||||
await visibleNodesGenerator.plantTree(root)
|
||||
const currentPath = URLHelper.getCurrentPath(true)
|
||||
tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
if (currentPath.length) {
|
||||
const nodeExpandedTo = visibleNodesGenerator.expandTo(currentPath.join('/'))
|
||||
if (nodeExpandedTo) {
|
||||
visibleNodesGenerator.focusNode(nodeExpandedTo)
|
||||
const { nodes } = visibleNodesGenerator.visibleNodes
|
||||
tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(nodes.indexOf(nodeExpandedTo)))
|
||||
}
|
||||
}
|
||||
dispatch(updateVisibleNodes)
|
||||
})
|
||||
|
||||
const execAfterRender = dispatch => () => {
|
||||
for (const task of tasksAfterRender) {
|
||||
task()
|
||||
}
|
||||
tasksAfterRender.length = 0
|
||||
}
|
||||
|
||||
const handleKeyDown = dispatch => ({ key }) => dispatch(({ visibleNodes: { nodes, focusedNode, expandedNodes, depths } }) => {
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
// 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)) {
|
||||
dispatch(focusNode, nextNode)
|
||||
}
|
||||
} else {
|
||||
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
|
||||
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
|
||||
|
||||
}
|
||||
} else {
|
||||
// now search input is focused
|
||||
if (nodes.length) {
|
||||
switch (key) {
|
||||
case 'ArrowDown':
|
||||
dispatch(focusNode, nodes[0])
|
||||
break
|
||||
case 'ArrowUp':
|
||||
dispatch(focusNode, nodes[nodes.length - 1])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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 {
|
||||
@connect(FileExplorerCore)
|
||||
export default class FileExplorer extends React.Component {
|
||||
static propTyps = {
|
||||
treeData: PropTypes.object,
|
||||
metaData: PropTypes.object,
|
||||
|
|
@ -260,15 +88,3 @@ class List extends React.Component {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default connect({
|
||||
init,
|
||||
execAfterRender,
|
||||
handleKeyDown,
|
||||
handleSearchKeyChange,
|
||||
setExpand,
|
||||
toggleNodeExpansion,
|
||||
focusNode,
|
||||
onNodeClick,
|
||||
updateVisibleNodes,
|
||||
})(List)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
import React from 'react'
|
||||
import PropTypes from 'prop-types'
|
||||
|
||||
import { SideBar as SideBarCore } from '../driver/core'
|
||||
import connect from '../driver/connect'
|
||||
|
||||
import FileExplorer from './FileExplorer'
|
||||
import ToggleShowButton from './ToggleShowButton'
|
||||
import MetaBar from './MetaBar'
|
||||
|
|
@ -12,6 +15,7 @@ import cx from '../utils/cx'
|
|||
|
||||
const baseSize = 260
|
||||
|
||||
@connect(SideBarCore)
|
||||
export default class Gitako extends React.PureComponent {
|
||||
static propTypes = {
|
||||
// initial width of side bar
|
||||
|
|
|
|||
|
|
@ -1,15 +1,11 @@
|
|||
import React from 'react'
|
||||
import ReactDOM from 'react-dom'
|
||||
|
||||
import connect from './driver/connect'
|
||||
import core from './driver/core'
|
||||
import SideBar from './components/SideBar'
|
||||
|
||||
import './content.less'
|
||||
|
||||
const ConnectedSideBar = connect(core)(SideBar)
|
||||
|
||||
const SideBarElement = document.createElement('div')
|
||||
document.body.appendChild(SideBarElement)
|
||||
|
||||
ReactDOM.render(<ConnectedSideBar />, SideBarElement)
|
||||
ReactDOM.render(<SideBar />, SideBarElement)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,5 @@
|
|||
import React from 'react'
|
||||
|
||||
function isObject(target) {
|
||||
return typeof target === 'object' && target !== null
|
||||
}
|
||||
|
||||
function link(instance, sources) {
|
||||
const wrappedMethods = {/* sources[key] -> wrappedMethods.method */}
|
||||
const map = new Map(/* sources.creator -> wrappedMethods.method */)
|
||||
|
|
|
|||
186
src/driver/core/FileExplorer.js
Normal file
186
src/driver/core/FileExplorer.js
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import DOMHelper from '../../utils/DOMHelper'
|
||||
import treeParser from '../../utils/treeParser'
|
||||
import URLHelper from '../../utils/URLHelper'
|
||||
import VisibleNodesGenerator from '../../utils/VisibleNodesGenerator'
|
||||
|
||||
function getVisibleParentNode(nodes, focusedNode, depths) {
|
||||
const focusedNodeIndex = nodes.indexOf(focusedNode)
|
||||
const focusedNodeDepth = depths.get(focusedNode)
|
||||
let indexOfParentNode = focusedNodeIndex - 1
|
||||
while (
|
||||
indexOfParentNode !== -1 &&
|
||||
depths.get(nodes[indexOfParentNode]) >= focusedNodeDepth
|
||||
) {
|
||||
--indexOfParentNode
|
||||
}
|
||||
const parentNode = nodes[indexOfParentNode]
|
||||
return parentNode
|
||||
}
|
||||
|
||||
const tasksAfterRender = []
|
||||
const visibleNodesGenerator = new VisibleNodesGenerator()
|
||||
|
||||
const init = dispatch => () => dispatch(async (state, { treeData, metaData, compressSingletonFolder }) => {
|
||||
const { root } = treeParser.parse(treeData, metaData)
|
||||
visibleNodesGenerator.setCompress(compressSingletonFolder)
|
||||
await visibleNodesGenerator.plantTree(root)
|
||||
const currentPath = URLHelper.getCurrentPath(true)
|
||||
tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
if (currentPath.length) {
|
||||
const nodeExpandedTo = visibleNodesGenerator.expandTo(currentPath.join('/'))
|
||||
if (nodeExpandedTo) {
|
||||
visibleNodesGenerator.focusNode(nodeExpandedTo)
|
||||
const { nodes } = visibleNodesGenerator.visibleNodes
|
||||
tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(nodes.indexOf(nodeExpandedTo)))
|
||||
}
|
||||
}
|
||||
dispatch(updateVisibleNodes)
|
||||
})
|
||||
|
||||
const execAfterRender = dispatch => () => {
|
||||
for (const task of tasksAfterRender) {
|
||||
task()
|
||||
}
|
||||
tasksAfterRender.length = 0
|
||||
}
|
||||
|
||||
const handleKeyDown = dispatch => ({ key }) => dispatch(({ visibleNodes: { nodes, focusedNode, expandedNodes, depths } }) => {
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
// 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)) {
|
||||
dispatch(focusNode, nextNode)
|
||||
}
|
||||
} else {
|
||||
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
|
||||
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
|
||||
|
||||
}
|
||||
} else {
|
||||
// now search input is focused
|
||||
if (nodes.length) {
|
||||
switch (key) {
|
||||
case 'ArrowDown':
|
||||
dispatch(focusNode, nodes[0])
|
||||
break
|
||||
case 'ArrowUp':
|
||||
dispatch(focusNode, nodes[nodes.length - 1])
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
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 })
|
||||
}
|
||||
|
||||
export default {
|
||||
init,
|
||||
execAfterRender,
|
||||
handleKeyDown,
|
||||
handleSearchKeyChange,
|
||||
setExpand,
|
||||
toggleNodeExpansion,
|
||||
focusNode,
|
||||
onNodeClick,
|
||||
updateVisibleNodes,
|
||||
}
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
import DOMHelper, { REPO_TYPE_PRIVATE } from '../utils/DOMHelper'
|
||||
import GitHubHelper, { NOT_FOUND, BAD_CREDENTIALS } from '../utils/GitHubHelper'
|
||||
import configHelper from '../utils/configHelper'
|
||||
import URLHelper from '../utils/URLHelper'
|
||||
import keyHelper from '../utils/keyHelper'
|
||||
import DOMHelper, { REPO_TYPE_PRIVATE } from '../../utils/DOMHelper'
|
||||
import GitHubHelper, { NOT_FOUND, BAD_CREDENTIALS } from '../../utils/GitHubHelper'
|
||||
import configHelper from '../../utils/configHelper'
|
||||
import URLHelper from '../../utils/URLHelper'
|
||||
import keyHelper from '../../utils/keyHelper'
|
||||
|
||||
const init = dispatch => async () => {
|
||||
try {
|
||||
2
src/driver/core/index.js
Normal file
2
src/driver/core/index.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
export { default as SideBar } from './SideBar'
|
||||
export { default as FileExplorer } from './FileExplorer'
|
||||
Loading…
Reference in a new issue