mirror of https://github.com/onivim/oni.git
352 lines
12 KiB
TypeScript
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,
|
|
)
|