From a09bba0a36e752c7adcd566c511c6da01ba14b30 Mon Sep 17 00:00:00 2001 From: EnixCoda Date: Thu, 21 Feb 2019 15:42:19 +0800 Subject: [PATCH] feat: reveal item from search result --- src/components/FileExplorer.tsx | 38 ++++++++++++++++++++++++--- src/components/Icon.tsx | 5 +++- src/components/Node.tsx | 10 ++++--- src/components/SearchBar.tsx | 6 +++-- src/content.less | 19 +++++++++++++- src/driver/core/FileExplorer.ts | 46 +++++++++++++++++++++++---------- 6 files changed, 101 insertions(+), 23 deletions(-) diff --git a/src/components/FileExplorer.tsx b/src/components/FileExplorer.tsx index b752827..332b83c 100644 --- a/src/components/FileExplorer.tsx +++ b/src/components/FileExplorer.tsx @@ -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 { static defaultProps: Partial = { freeze: false, + searchKey: '', + searched: false, } componentWillMount() { @@ -48,7 +51,9 @@ class FileExplorer extends React.Component { 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 } return ( @@ -61,12 +66,34 @@ class FileExplorer extends React.Component { focused={focusedNode === node} expanded={expandedNodes.has(node)} onClick={onNodeClick} + renderActions={() => + inSearch && + searched && ( +
+ +
+ ) + } /> ))} ) } + revealNode( + goTo: (path: string[]) => void, + node: TreeNode, + ): (event: React.MouseEvent) => void { + return e => { + e.stopPropagation() + e.preventDefault() + goTo(node.path.split('/')) + } + } + render() { const { stateText, @@ -77,6 +104,7 @@ class FileExplorer extends React.Component { onNodeClick, toggleShowSettings, onFocusSearchBar, + searchKey, } = this.props return (
{ ) : ( visibleNodes && ( - + {this.renderFiles(visibleNodes, onNodeClick)} ) diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx index 0ec8fac..a59f335 100644 --- a/src/components/Icon.tsx +++ b/src/components/Icon.tsx @@ -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) => void + onClick?: (event: React.MouseEvent) => void } const Icon: React.SFC = function Icon({ type, className = undefined, ...otherProps }) { diff --git a/src/components/Node.tsx b/src/components/Node.tsx index 0ec8d3a..2f4ced4 100644 --- a/src/components/Node.tsx +++ b/src/components/Node.tsx @@ -21,6 +21,7 @@ type Props = { depth: number expanded: boolean focused: boolean + renderActions?(): React.ReactNode } export default class Node extends React.PureComponent { onClick: React.MouseEventHandler = event => { @@ -31,7 +32,7 @@ export default class Node extends React.PureComponent { } 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 { className={cx('node-item', { expanded })} style={{ paddingLeft: `${10 + 20 * depth}px` }} > - - {name} +
+ + {name} +
+ {renderActions &&
{renderActions()}
}
diff --git a/src/components/SearchBar.tsx b/src/components/SearchBar.tsx index 0300bee..1a59059 100644 --- a/src/components/SearchBar.tsx +++ b/src/components/SearchBar.tsx @@ -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 (
) diff --git a/src/content.less b/src/content.less index d3fe412..9a83372 100644 --- a/src/content.less +++ b/src/content.less @@ -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 { diff --git a/src/driver/core/FileExplorer.ts b/src/driver/core/FileExplorer.ts index dacd793..32b30ba 100644 --- a/src/driver/core/FileExplorer.ts +++ b/src/driver/core/FileExplorer.ts @@ -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 @@ -90,15 +93,7 @@ const setUpTree: MethodCreator = 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] -> = dispatch => { +> = dispatch => async event => { + const searchKey = event.currentTarget.value + await dispatch.call(search, searchKey) +} + +const search: MethodCreator = 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 = 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,