codekingpro/portable-devtools
114k
1import cn from 'classnames';2import * as React from 'react';3import { ClasslistComposite } from 'aspen-decorations';4import { Directory, FileEntry, IItemRendererProps, ItemType, RenamePromptHandle, FileType, FileOrDir} from 'react-aspen';5import {IFileTreeXTriggerEvents, FileTreeXEvent } from '../types';6import _ from 'lodash';7import { Notificar } from 'notificar';8 9interface IItemRendererXProps {10 /**11 * In this implementation, decoration are null when item is `PromptHandle`12 *13 * If you would like decorations for `PromptHandle`s, then get them using `DecorationManager#getDecorations(<target>)`.14 * Where `<target>` can be either `NewFilePromptHandle.parent` or `RenamePromptHandle.target` depending on type of `PromptHandle`15 *16 * To determine the type of `PromptHandle`, use `IItemRendererProps.itemType`17 */18 decorations: ClasslistComposite19 onClick: (ev: React.MouseEvent, item: FileEntry | Directory, type: ItemType) => void20 onContextMenu: (ev: React.MouseEvent, item: FileEntry | Directory) => void21 onMouseEnter: (ev: React.MouseEvent, item: FileEntry | Directory) => void22 onMouseLeave: (ev: React.MouseEvent, item: FileEntry | Directory) => void23 onItemHovered: (ev: React.MouseEvent, item: FileEntry | Directory, type: ItemType) => void24 events: Notificar<FileTreeXEvent>25}26 27// DO NOT EXTEND FROM PureComponent!!! You might miss critical changes made deep within `item` prop28// as far as efficiency is concerned, `react-aspen` works hard to ensure unnecessary updates are ignored29export class FileTreeItem extends React.Component<IItemRendererXProps & IItemRendererProps> {30 public static getBoundingClientRectForItem(item: FileEntry | Directory): DOMRect {31 const divRef = FileTreeItem.itemIdToRefMap.get(item.id);32 if (divRef) {33 return divRef.getBoundingClientRect();34 }35 return null;36 }37 38 // ensure this syncs up with what goes in CSS, (em, px, % etc.) and what ultimately renders on the page39 public static readonly renderHeight: number = 24;40 private static readonly itemIdToRefMap: Map<number, HTMLDivElement> = new Map();41 private static readonly refToItemIdMap: Map<number, HTMLDivElement> = new Map();42 private fileTreeEvent: IFileTreeXTriggerEvents;43 44 constructor(props) {45 super(props);46 // used to apply decoration changes, you're welcome to use setState or other mechanisms as you see fit47 this.forceUpdate = this.forceUpdate.bind(this);48 }49 50 public render() {51 const { item, itemType, decorations } = this.props;52 53 const isRenamePrompt = itemType === ItemType.RenamePrompt;54 const isNewPrompt = itemType === ItemType.NewDirectoryPrompt || itemType === ItemType.NewFilePrompt;55 const isDirExpanded = itemType === ItemType.Directory56 ? (item as Directory).expanded57 : itemType === ItemType.RenamePrompt && (item as RenamePromptHandle).target.type === FileType.Directory58 ? ((item as RenamePromptHandle).target as Directory).expanded59 : false;60 61 const fileOrDir =62 (itemType === ItemType.File ||63 itemType === ItemType.NewFilePrompt ||64 (itemType === ItemType.RenamePrompt && (item as RenamePromptHandle).target.constructor === FileEntry))65 ? 'file'66 : 'directory';67 68 if (this.props.item.parent && this.props.item.parent.path) {69 this.props.item.resolvedPathCache = this.props.item.parent.path + '/' + this.props.item._metadata.data.id;70 }71 72 const itemChildren = item.children && item.children.length > 0 && item._metadata.data._type.indexOf('coll-') !== -1 ? '(' + item.children.length + ')' : '';73 const extraClasses = item._metadata.data.extraClasses ? item._metadata.data.extraClasses.join(' ') : '';74 75 return (76 <div77 className={cn('file-entry', {78 renaming: isRenamePrompt,79 prompt: isRenamePrompt || isNewPrompt,80 new: isNewPrompt,81 }, fileOrDir, decorations ? decorations.classlist : null, `depth-${item.depth}`, extraClasses)}82 data-depth={item.depth}83 onContextMenu={this.handleContextMenu}84 onClick={this.handleClick}85 onDoubleClick={this.handleDoubleClick}86 onDragStart={this.handleDragStartItem}87 onMouseEnter={this.handleMouseEnter}88 onMouseLeave={this.handleMouseLeave}89 onKeyDown={()=>{/* taken care by parent */}}90 // required for rendering context menus when opened through context menu button on keyboard91 ref={this.handleDivRef}92 draggable={true}>93 94 {!isNewPrompt && fileOrDir === 'directory' ?95 <i className={cn('directory-toggle', isDirExpanded ? 'open' : '')} />96 : null97 }98 99 <span className='file-label'>100 {101 item._metadata && item._metadata.data.icon ?102 <i className={cn('file-icon', item._metadata && item._metadata.data.icon ? item._metadata.data.icon : fileOrDir)} /> : null103 }104 <span className='file-name'>105 { _.unescape(this.props.item.getMetadata('data')._label)}106 <span className='children-count'>{itemChildren}</span>107 </span>108 109 </span>110 </div>);111 }112 113 public componentDidMount() {114 this.events = this.props.events;115 this.props.item.resolvedPathCache = this.props.item.parent.path + '/' + this.props.item._metadata.data.id;116 if (this.props.decorations) {117 this.props.decorations.addChangeListener(this.forceUpdate);118 }119 this.setActiveFile(this.props.item);120 }121 122 private setActiveFile = async (FileOrDir): Promise<void> => {123 this.props.changeDirectoryCount(FileOrDir.parent);124 if(FileOrDir._loaded !== true) {125 this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'added', FileOrDir);126 }127 FileOrDir._loaded = true;128 };129 130 public componentWillUnmount() {131 if (this.props.decorations) {132 this.props.decorations.removeChangeListener(this.forceUpdate);133 }134 }135 136 public componentDidUpdate(prevProps: IItemRendererXProps) {137 if (prevProps.decorations) {138 prevProps.decorations.removeChangeListener(this.forceUpdate);139 }140 if (this.props.decorations) {141 this.props.decorations.addChangeListener(this.forceUpdate);142 }143 }144 145 private handleDivRef = (r: HTMLDivElement) => {146 if (r === null) {147 FileTreeItem.itemIdToRefMap.delete(this.props.item.id);148 } else {149 FileTreeItem.itemIdToRefMap.set(this.props.item.id, r);150 FileTreeItem.refToItemIdMap.set(r, this.props.item);151 }152 };153 154 private handleContextMenu = (ev: React.MouseEvent) => {155 const { item, itemType, onContextMenu } = this.props;156 if (itemType === ItemType.File || itemType === ItemType.Directory) {157 onContextMenu(ev, item as FileOrDir);158 }159 };160 161 private handleClick = (ev: React.MouseEvent) => {162 const { item, itemType, onClick } = this.props;163 if (itemType === ItemType.File || itemType === ItemType.Directory) {164 onClick(ev, item as FileEntry, itemType);165 }166 };167 168 private handleDoubleClick = (ev: React.MouseEvent) => {169 const { item, itemType, onDoubleClick } = this.props;170 if (itemType === ItemType.File || itemType === ItemType.Directory) {171 onDoubleClick(ev, item as FileEntry, itemType);172 }173 };174 175 private handleMouseEnter = (ev: React.MouseEvent) => {176 const { item, itemType, onMouseEnter } = this.props;177 if (itemType === ItemType.File || itemType === ItemType.Directory) {178 onMouseEnter?.(ev, item as FileEntry);179 }180 };181 182 private handleMouseLeave = (ev: React.MouseEvent) => {183 const { item, itemType, onMouseLeave } = this.props;184 if (itemType === ItemType.File || itemType === ItemType.Directory) {185 onMouseLeave?.(ev, item as FileEntry);186 }187 };188 189 private handleDragStartItem = (e: React.DragEvent) => {190 const { item, itemType, events } = this.props;191 if (itemType === ItemType.File || itemType === ItemType.Directory) {192 const ref = FileTreeItem.itemIdToRefMap.get(item.id);193 if (ref) {194 events.dispatch(FileTreeXEvent.onTreeEvents, e, 'dragstart', item);195 }196 }197 };198}199 