mirror of
https://github.com/EnixCoda/Gitako.git
synced 2026-03-11 08:54:44 +00:00
feat: reveal item from search result
This commit is contained in:
parent
9f3ee8b5b5
commit
a09bba0a36
6 changed files with 101 additions and 23 deletions
|
|
@ -7,7 +7,8 @@ import LoadingIndicator from 'components/LoadingIndicator'
|
|||
import cx from 'utils/cx'
|
||||
import { ConnectorState } from 'driver/core/FileExplorer'
|
||||
import { TreeData, MetaData } from 'utils/GitHubHelper'
|
||||
import { VisibleNodes } from 'utils/VisibleNodesGenerator'
|
||||
import { VisibleNodes, TreeNode } from 'utils/VisibleNodesGenerator'
|
||||
import Icon from './Icon'
|
||||
|
||||
export type Props = {
|
||||
treeData: TreeData
|
||||
|
|
@ -21,6 +22,8 @@ export type Props = {
|
|||
class FileExplorer extends React.Component<Props & ConnectorState> {
|
||||
static defaultProps: Partial<Props & ConnectorState> = {
|
||||
freeze: false,
|
||||
searchKey: '',
|
||||
searched: false,
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
|
@ -48,7 +51,9 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
|
|||
|
||||
renderFiles(visibleNodes: VisibleNodes, onNodeClick: Node['props']['onClick']) {
|
||||
const { nodes, depths, focusedNode, expandedNodes } = visibleNodes
|
||||
if (nodes.length === 0) {
|
||||
const { goTo, searchKey, searched } = this.props
|
||||
const inSearch = searchKey !== ''
|
||||
if (inSearch && nodes.length === 0) {
|
||||
return <label className={'no-results'}>No results found.</label>
|
||||
}
|
||||
return (
|
||||
|
|
@ -61,12 +66,34 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
|
|||
focused={focusedNode === node}
|
||||
expanded={expandedNodes.has(node)}
|
||||
onClick={onNodeClick}
|
||||
renderActions={() =>
|
||||
inSearch &&
|
||||
searched && (
|
||||
<div className={'go-to-wrapper'}>
|
||||
<button className={'go-to-button'} onClick={this.revealNode(goTo, node)}>
|
||||
<Icon type="go-to" />
|
||||
Reveal in file tree
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
revealNode(
|
||||
goTo: (path: string[]) => void,
|
||||
node: TreeNode,
|
||||
): (event: React.MouseEvent<HTMLElement, MouseEvent>) => void {
|
||||
return e => {
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
goTo(node.path.split('/'))
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
stateText,
|
||||
|
|
@ -77,6 +104,7 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
|
|||
onNodeClick,
|
||||
toggleShowSettings,
|
||||
onFocusSearchBar,
|
||||
searchKey,
|
||||
} = this.props
|
||||
return (
|
||||
<div
|
||||
|
|
@ -90,7 +118,11 @@ class FileExplorer extends React.Component<Props & ConnectorState> {
|
|||
) : (
|
||||
visibleNodes && (
|
||||
<React.Fragment>
|
||||
<SearchBar onSearchKeyChange={handleSearchKeyChange} onFocus={onFocusSearchBar} />
|
||||
<SearchBar
|
||||
searchKey={searchKey}
|
||||
onSearchKeyChange={handleSearchKeyChange}
|
||||
onFocus={onFocusSearchBar}
|
||||
/>
|
||||
{this.renderFiles(visibleNodes, onNodeClick)}
|
||||
</React.Fragment>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import FileZip from 'assets/icons/octicons/file-zip.svg?svgr'
|
|||
import Markdown from 'assets/icons/octicons/markdown.svg?svgr'
|
||||
import FileMedia from 'assets/icons/octicons/file-media.svg?svgr'
|
||||
import FileCode from 'assets/icons/octicons/file-code.svg?svgr'
|
||||
import Reply from 'assets/icons/octicons/reply.svg?svgr'
|
||||
// import FileBinary from 'assets/icons/octicons/file-binary.svg?svgr'
|
||||
// import FileSymlinkDirectory from 'assets/icons/octicons/file-symlink-directory.svg?svgr'
|
||||
// import FileSymlinkFile from 'assets/icons/octicons/file-symlink-file.svg?svgr'
|
||||
|
|
@ -33,6 +34,8 @@ function getSVGIconComponent(type: string) {
|
|||
return Gear
|
||||
case 'folder':
|
||||
return TriangleRight
|
||||
case 'go-to':
|
||||
return Reply
|
||||
case '.pdf':
|
||||
return FilePdf
|
||||
case '.zip':
|
||||
|
|
@ -72,7 +75,7 @@ const iconStyle = { width: '100%', height: '100%' }
|
|||
type Props = {
|
||||
type: string
|
||||
className?: string
|
||||
onClick?: (event: React.MouseEvent<HTMLDivElement>) => void
|
||||
onClick?: (event: React.MouseEvent<HTMLElement>) => void
|
||||
}
|
||||
|
||||
const Icon: React.SFC<Props> = function Icon({ type, className = undefined, ...otherProps }) {
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ type Props = {
|
|||
depth: number
|
||||
expanded: boolean
|
||||
focused: boolean
|
||||
renderActions?(): React.ReactNode
|
||||
}
|
||||
export default class Node extends React.PureComponent<Props> {
|
||||
onClick: React.MouseEventHandler = event => {
|
||||
|
|
@ -31,7 +32,7 @@ export default class Node extends React.PureComponent<Props> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const { node, depth, expanded, focused } = this.props
|
||||
const { node, depth, expanded, focused, renderActions } = this.props
|
||||
const { name, path, virtual } = node
|
||||
if (virtual) {
|
||||
// this is not a real node
|
||||
|
|
@ -51,8 +52,11 @@ export default class Node extends React.PureComponent<Props> {
|
|||
className={cx('node-item', { expanded })}
|
||||
style={{ paddingLeft: `${10 + 20 * depth}px` }}
|
||||
>
|
||||
<Icon type={getIconType(node)} />
|
||||
<span className={'node-item-name'}>{name}</span>
|
||||
<div>
|
||||
<Icon type={getIconType(node)} />
|
||||
<span className={'node-item-name'}>{name}</span>
|
||||
</div>
|
||||
{renderActions && <div>{renderActions()}</div>}
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@ import * as React from 'react'
|
|||
type Props = {
|
||||
onSearchKeyChange: React.FormEventHandler
|
||||
onFocus: React.FocusEventHandler
|
||||
searchKey: string
|
||||
}
|
||||
|
||||
export default function SearchBar({ onSearchKeyChange, onFocus }: Props) {
|
||||
export default function SearchBar({ onSearchKeyChange, onFocus, searchKey }: Props) {
|
||||
return (
|
||||
<div className={'search-input-wrapper'}>
|
||||
<input
|
||||
|
|
@ -15,7 +16,8 @@ export default function SearchBar({ onSearchKeyChange, onFocus }: Props) {
|
|||
aria-label="search files"
|
||||
placeholder="Search files (RegEx)"
|
||||
type="text"
|
||||
onInput={onSearchKeyChange}
|
||||
onChange={onSearchKeyChange}
|
||||
value={searchKey}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -304,12 +304,14 @@
|
|||
}
|
||||
|
||||
.node-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
word-wrap: normal;
|
||||
word-break: break-all;
|
||||
margin: 0;
|
||||
color: #0366d6;
|
||||
line-height: 20px;
|
||||
padding: 6px 0;
|
||||
padding: 6px 10px;
|
||||
cursor: pointer;
|
||||
border-top: 1px solid #eaecef;
|
||||
transition: all 0.5s ease;
|
||||
|
|
@ -330,6 +332,21 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.go-to-wrapper {
|
||||
max-width: 16px;
|
||||
overflow: hidden;
|
||||
transition: max-width 0.5s linear;
|
||||
&:hover {
|
||||
max-width: 160px;
|
||||
}
|
||||
.go-to-button {
|
||||
white-space: nowrap;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.@{name}-settings-bar {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import Node from 'components/Node'
|
|||
export type ConnectorState = {
|
||||
stateText: string
|
||||
visibleNodes: VisibleNodes
|
||||
searchKey: string
|
||||
searched: boolean
|
||||
|
||||
init: () => void
|
||||
execAfterRender: () => void
|
||||
|
|
@ -19,6 +21,7 @@ export type ConnectorState = {
|
|||
onNodeClick: Node['props']['onClick']
|
||||
onFocusSearchBar: React.FocusEventHandler
|
||||
setUpTree: (treeData?: TreeData) => void
|
||||
goTo: (path: string[]) => void
|
||||
}
|
||||
|
||||
type DepthMap = Map<TreeNode, number>
|
||||
|
|
@ -90,15 +93,7 @@ const setUpTree: MethodCreator<Props, ConnectorState> = dispatch => () =>
|
|||
|
||||
tasksAfterRender.push(DOMHelper.focusSearchInput)
|
||||
dispatch.call(setStateText, '')
|
||||
const currentPath = URLHelper.getCurrentPath(metaData.branchName)
|
||||
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.call(goTo, URLHelper.getCurrentPath(metaData.branchName))
|
||||
dispatch.call(updateVisibleNodes)
|
||||
})
|
||||
|
||||
|
|
@ -228,17 +223,40 @@ const handleSearchKeyChange: MethodCreator<
|
|||
Props,
|
||||
ConnectorState,
|
||||
[React.FormEvent<HTMLInputElement>]
|
||||
> = dispatch => {
|
||||
> = dispatch => async event => {
|
||||
const searchKey = event.currentTarget.value
|
||||
await dispatch.call(search, searchKey)
|
||||
}
|
||||
|
||||
const search: MethodCreator<Props, ConnectorState, [string]> = dispatch => {
|
||||
let i = 0
|
||||
return async event => {
|
||||
const searchKey = event.currentTarget.value
|
||||
return async searchKey => {
|
||||
dispatch.set({ searchKey })
|
||||
const j = (i += 1)
|
||||
await visibleNodesGenerator.search(searchKey)
|
||||
if (i === j) dispatch.call(updateVisibleNodes)
|
||||
if (i === j) {
|
||||
dispatch.set(({ searched }) => ({ searched: !(searched && searchKey === '') }))
|
||||
dispatch.call(updateVisibleNodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const delayExpandThreshold = 400
|
||||
|
||||
const goTo: MethodCreator<Props, ConnectorState, [string[]]> = dispatch => async currentPath => {
|
||||
if (currentPath.length) {
|
||||
await visibleNodesGenerator.search('')
|
||||
dispatch.set({ searchKey: '', searched: false })
|
||||
const nodeExpandedTo = visibleNodesGenerator.expandTo(currentPath.join('/'))
|
||||
if (nodeExpandedTo) {
|
||||
visibleNodesGenerator.focusNode(nodeExpandedTo)
|
||||
const { nodes } = visibleNodesGenerator.visibleNodes
|
||||
tasksAfterRender.push(() => DOMHelper.scrollToNodeElement(nodes.indexOf(nodeExpandedTo)))
|
||||
dispatch.call(updateVisibleNodes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function shouldDelayExpand(node: TreeNode) {
|
||||
return (
|
||||
visibleNodesGenerator.visibleNodes.expandedNodes.has(node) &&
|
||||
|
|
@ -336,8 +354,10 @@ export default {
|
|||
setStateText,
|
||||
handleKeyDown,
|
||||
onFocusSearchBar,
|
||||
search,
|
||||
handleSearchKeyChange,
|
||||
setExpand,
|
||||
goTo,
|
||||
toggleNodeExpansion,
|
||||
focusNode,
|
||||
onNodeClick,
|
||||
|
|
|
|||
Loading…
Reference in a new issue