oni/browser/src/Services/Explorer/ExplorerView.tsx

352 lines
12 KiB
TypeScript

/**
* ExplorerSplit.tsx
*
*/
import * as React from "react"
import * as DND from "react-dnd"
import HTML5Backend from "react-dnd-html5-backend"
import { connect } from "react-redux"
import { compose } from "redux"
import { CSSTransition, TransitionGroup } from "react-transition-group"
import { css, styled } from "./../../UI/components/common"
import { TextInputView } from "./../../UI/components/LightweightText"
import { SidebarContainerView, SidebarItemView } from "./../../UI/components/SidebarItemView"
import { Sneakable } from "./../../UI/components/Sneakable"
import { VimNavigator } from "./../../UI/components/VimNavigator"
import { DragAndDrop, Droppeable } from "./../DragAndDrop"
import { FileIcon } from "./../FileIcon"
import * as ExplorerSelectors from "./ExplorerSelectors"
import { IExplorerState } from "./ExplorerStore"
type Node = ExplorerSelectors.ExplorerNode
export interface INodeViewProps {
moveFileOrFolder: (source: Node, dest: Node) => void
node: ExplorerSelectors.ExplorerNode
isSelected: boolean
onClick: () => void
onCancelRename: () => void
onCompleteRename: (newName: string) => void
onCancelCreate?: () => void
onCompleteCreate?: (path: string) => void
yanked: string[]
updated?: string[]
isRenaming: Node
isCreating: boolean
}
export const NodeWrapper = styled.div`
&:hover {
text-decoration: underline;
}
`
// tslint:disable-next-line
const noop = (elem: HTMLElement) => {}
const scrollIntoViewIfNeeded = (elem: HTMLElement) => {
// tslint:disable-next-line
elem && elem["scrollIntoViewIfNeeded"] && elem["scrollIntoViewIfNeeded"]()
}
const stopPropagation = (fn: () => void) => {
return (e?: React.MouseEvent<HTMLElement>) => {
if (e) {
e.stopPropagation()
}
fn()
}
}
const Types = {
FILE: "FILE",
FOLDER: "FOLDER",
}
interface IMoveNode {
drop: {
node: ExplorerSelectors.ExplorerNode
}
drag: {
node: ExplorerSelectors.ExplorerNode
}
}
const NodeTransitionWrapper = styled.div`
transition: all 400ms 50ms ease-in-out;
&.move-enter {
opacity: 0.01;
transform: scale(0.9);
}
&.move-enter-active {
transform: scale(1);
opacity: 1;
}
`
interface ITransitionProps {
children: React.ReactNode
updated: boolean
}
const Transition = ({ children, updated }: ITransitionProps) => (
<CSSTransition in={updated} classNames="move" timeout={1000}>
<NodeTransitionWrapper className={updated && "move"}>{children}</NodeTransitionWrapper>
</CSSTransition>
)
const renameStyles = css`
width: 100%;
background-color: inherit;
color: inherit;
font-size: inherit;
font-family: inherit;
padding: 0.5em;
box-sizing: border-box;
border: 2px solid ${p => p.theme["highlight.mode.normal.background"]} !important;
`
const createStyles = css`
${renameStyles};
margin-top: 0.2em;
`
export class NodeView extends React.PureComponent<INodeViewProps, {}> {
public moveFileOrFolder = ({ drag, drop }: IMoveNode) => {
this.props.moveFileOrFolder(drag.node, drop.node)
}
public isSameNode = ({ drag, drop }: IMoveNode) => {
return !(drag.node.name === drop.node.name)
}
public render(): JSX.Element {
const { isCreating, isRenaming, isSelected, node } = this.props
const renameInProgress = isRenaming.name === node.name && isSelected && !isCreating
const creationInProgress = isCreating && isSelected && !renameInProgress
return (
<NodeWrapper
style={{ cursor: "pointer" }}
innerRef={this.props.isSelected ? scrollIntoViewIfNeeded : noop}
>
{renameInProgress ? (
<TextInputView
styles={renameStyles}
onCancel={this.props.onCancelRename}
onComplete={this.props.onCompleteRename}
/>
) : (
<div>
{this.getElement()}
{creationInProgress && (
<TextInputView
styles={createStyles}
onCancel={this.props.onCancelCreate}
onComplete={this.props.onCompleteCreate}
/>
)}
</div>
)}
</NodeWrapper>
)
}
public hasUpdated = (path: string) =>
!!this.props.updated && this.props.updated.some(nodePath => nodePath === path)
public getElement(): JSX.Element {
const { node } = this.props
const yanked = this.props.yanked.includes(node.id)
switch (node.type) {
case "file":
return (
<DragAndDrop
onDrop={this.moveFileOrFolder}
dragTarget={Types.FILE}
accepts={[Types.FILE, Types.FOLDER]}
isValidDrop={this.isSameNode}
node={node}
render={({ canDrop, isDragging, didDrop, isOver }) => {
const updated = this.hasUpdated(node.filePath)
return (
<Transition updated={updated}>
<SidebarItemView
updated={updated}
yanked={yanked}
isOver={isOver && canDrop}
didDrop={didDrop}
canDrop={canDrop}
text={node.name}
isFocused={this.props.isSelected}
isContainer={false}
indentationLevel={node.indentationLevel}
onClick={stopPropagation(this.props.onClick)}
icon={<FileIcon fileName={node.name} isLarge={true} />}
/>
</Transition>
)
}}
/>
)
case "container":
return (
<Droppeable
accepts={[Types.FILE, Types.FOLDER]}
onDrop={this.moveFileOrFolder}
isValidDrop={() => true}
render={({ isOver }) => {
return (
<SidebarContainerView
yanked={yanked}
isOver={isOver}
isContainer={true}
isExpanded={node.expanded}
text={node.name}
isFocused={this.props.isSelected}
onClick={stopPropagation(this.props.onClick)}
/>
)
}}
/>
)
case "folder":
return (
<DragAndDrop
accepts={[Types.FILE, Types.FOLDER]}
dragTarget={Types.FOLDER}
isValidDrop={this.isSameNode}
onDrop={this.moveFileOrFolder}
node={node}
render={({ isOver, didDrop, canDrop }) => {
const updated = this.hasUpdated(node.folderPath)
return (
<Transition updated={updated}>
<SidebarContainerView
yanked={yanked}
updated={updated}
didDrop={didDrop}
isOver={isOver && canDrop}
isContainer={false}
isExpanded={node.expanded}
text={node.name}
isFocused={this.props.isSelected}
indentationLevel={node.indentationLevel}
onClick={stopPropagation(this.props.onClick)}
/>
</Transition>
)
}}
/>
)
default:
return <div>{JSON.stringify(node)}</div>
}
}
}
export interface IExplorerViewContainerProps {
moveFileOrFolder: (source: Node, dest: Node) => void
onSelectionChanged: (id: string) => void
onClick: (id: string) => void
onCancelRename: () => void
onCompleteRename: (newName: string) => void
yanked?: string[]
isCreating?: boolean
isRenaming?: Node
onCancelCreate?: () => void
onCompleteCreate?: (path: string) => void
}
export interface IExplorerViewProps extends IExplorerViewContainerProps {
nodes: ExplorerSelectors.ExplorerNode[]
isActive: boolean
updated: string[]
}
import { SidebarEmptyPaneView } from "./../../UI/components/SidebarEmptyPaneView"
import { commandManager } from "./../CommandManager"
export class ExplorerView extends React.PureComponent<IExplorerViewProps, {}> {
public render(): JSX.Element {
const ids = this.props.nodes.map(node => node.id)
if (!this.props.nodes || !this.props.nodes.length) {
return (
<SidebarEmptyPaneView
active={this.props.isActive}
contentsText="Nothing to show here, yet!"
actionButtonText="Open a Folder"
onClickButton={() => commandManager.executeCommand("workspace.openFolder")}
/>
)
}
return (
<TransitionGroup>
<VimNavigator
ids={ids}
active={this.props.isActive && !this.props.isRenaming && !this.props.isCreating}
onSelectionChanged={this.props.onSelectionChanged}
onSelected={id => this.props.onClick(id)}
render={(selectedId: string) => {
const nodes = this.props.nodes.map(node => (
<Sneakable callback={() => this.props.onClick(node.id)} key={node.id}>
<NodeView
node={node}
isSelected={node.id === selectedId}
isCreating={this.props.isCreating}
onCancelCreate={this.props.onCancelCreate}
onCompleteCreate={this.props.onCompleteCreate}
onCompleteRename={this.props.onCompleteRename}
isRenaming={this.props.isRenaming}
onCancelRename={this.props.onCancelRename}
updated={this.props.updated}
yanked={this.props.yanked}
moveFileOrFolder={this.props.moveFileOrFolder}
onClick={() => this.props.onClick(node.id)}
/>
</Sneakable>
))
return (
<div className="explorer enable-mouse">
<div className="items">{nodes}</div>
</div>
)
}}
/>
</TransitionGroup>
)
}
}
const mapStateToProps = (
state: IExplorerState,
containerProps: IExplorerViewContainerProps,
): IExplorerViewProps => {
const yanked = state.register.yank.map(node => node.id)
const {
register: { updated, rename },
} = state
return {
...containerProps,
isActive: state.hasFocus,
nodes: ExplorerSelectors.mapStateToNodeList(state),
updated,
yanked,
isCreating: state.register.create.active,
isRenaming: rename.active && rename.target,
}
}
export const Explorer = compose(connect(mapStateToProps), DND.DragDropContext(HTML5Backend))(
ExplorerView,
)