656 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			TypeScript
		
	
	
			
		
		
	
	
			656 lines
		
	
	
		
			22 KiB
		
	
	
	
		
			TypeScript
		
	
	
/////////////////////////////////////////////////////////////
 | 
						|
//
 | 
						|
// pgAdmin 4 - PostgreSQL Tools
 | 
						|
//
 | 
						|
// Copyright (C) 2013 - 2025, The pgAdmin Development Team
 | 
						|
// This software is released under the PostgreSQL Licence
 | 
						|
//
 | 
						|
//////////////////////////////////////////////////////////////
 | 
						|
import * as React from 'react';
 | 
						|
import {
 | 
						|
  FileTree,
 | 
						|
  Directory,
 | 
						|
  FileEntry,
 | 
						|
  ItemType,
 | 
						|
  IFileTreeHandle,
 | 
						|
  WatchEvent,
 | 
						|
  FileType,
 | 
						|
  IItemRendererProps,
 | 
						|
  FileOrDir
 | 
						|
} from 'react-aspen';
 | 
						|
import { Decoration, TargetMatchMode } from 'aspen-decorations';
 | 
						|
import { FileTreeItem } from '../FileTreeItem';
 | 
						|
import { Notificar, DisposablesComposite } from 'notificar';
 | 
						|
import { IFileTreeXHandle, IFileTreeXProps, FileTreeXEvent, IFileTreeXTriggerEvents } from '../types';
 | 
						|
import { KeyboardHotkeys } from '../services/keyboardHotkeys';
 | 
						|
import { TreeModelX } from '../TreeModelX';
 | 
						|
import AutoSizer from 'react-virtualized-auto-sizer';
 | 
						|
 | 
						|
export class FileTreeX extends React.Component<IFileTreeXProps> {
 | 
						|
  private fileTreeHandle: IFileTreeXHandle;
 | 
						|
  private readonly activeFileDec: Decoration;
 | 
						|
  private readonly pseudoActiveFileDec: Decoration;
 | 
						|
  private activeFile: FileOrDir;
 | 
						|
  private pseudoActiveFile: FileOrDir;
 | 
						|
  private readonly wrapperRef: React.RefObject<HTMLDivElement | null> = React.createRef();
 | 
						|
  private readonly events: Notificar<FileTreeXEvent>;
 | 
						|
  private readonly disposables: DisposablesComposite;
 | 
						|
  private keyboardHotkeys: KeyboardHotkeys;
 | 
						|
  private fileTreeEvent: IFileTreeXTriggerEvents;
 | 
						|
  private readonly hoverTimeoutId: React.RefObject<number|null> = React.createRef<number|null>();
 | 
						|
  private readonly hoverDispatchId: React.RefObject<number|null> = React.createRef<number|null>();
 | 
						|
  constructor(props: IFileTreeXProps) {
 | 
						|
    super(props);
 | 
						|
    this.events = new Notificar();
 | 
						|
    this.disposables = new DisposablesComposite();
 | 
						|
    this.activeFileDec = new Decoration('active');
 | 
						|
    this.pseudoActiveFileDec = new Decoration('pseudo-active');
 | 
						|
  }
 | 
						|
 | 
						|
  render() {
 | 
						|
    const { height, model, disableCache } = this.props;
 | 
						|
    const { decorations } = model;
 | 
						|
 | 
						|
    return <div
 | 
						|
      onKeyDown={this.handleKeyDown}
 | 
						|
      className='file-tree'
 | 
						|
      onBlur={this.handleBlur}
 | 
						|
      onClick={this.handleClick}
 | 
						|
      onScroll={this.props.onScroll}
 | 
						|
      ref={this.wrapperRef}
 | 
						|
      style={{
 | 
						|
        height: height || 'calc(100vh - 60px)',
 | 
						|
        width: '100%',
 | 
						|
        display: 'flex',
 | 
						|
        flexDirection: 'column',
 | 
						|
        flex: 1
 | 
						|
      }}
 | 
						|
      tabIndex={-1}>
 | 
						|
      <AutoSizer onResize={this.onResize}>
 | 
						|
        {({ width, height }) => (
 | 
						|
          <FileTree
 | 
						|
            height={height}
 | 
						|
            width={width}
 | 
						|
            model={model}
 | 
						|
            itemHeight={FileTreeItem.renderHeight}
 | 
						|
            onReady={this.handleTreeReady}
 | 
						|
            disableCache={disableCache || false}
 | 
						|
          >
 | 
						|
            {(props: IItemRendererProps) => <FileTreeItem
 | 
						|
              item={props.item}
 | 
						|
              itemType={props.itemType}
 | 
						|
              decorations={decorations.getDecorations(props.item as FileEntry|Directory)}
 | 
						|
              onClick={this.handleItemClicked}
 | 
						|
              onDoubleClick={this.handleItemDoubleClicked}
 | 
						|
              onContextMenu={this.handleItemCtxMenu}
 | 
						|
              onMouseEnter={this.onItemMouseEnter}
 | 
						|
              onMouseLeave={this.onItemMouseLeave}
 | 
						|
              changeDirectoryCount={this.changeDirectoryCount}
 | 
						|
              events={this.events}/>}
 | 
						|
          </FileTree>
 | 
						|
        )}
 | 
						|
      </AutoSizer>
 | 
						|
    </div>;
 | 
						|
  }
 | 
						|
 | 
						|
  public componentDidMount() {
 | 
						|
    for(const child of this.props.model.root.children) {
 | 
						|
      this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'loaded', child);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  componentWillUnmount() {
 | 
						|
    const { model } = this.props;
 | 
						|
    model.decorations.removeDecoration(this.activeFileDec);
 | 
						|
    model.decorations.removeDecoration(this.pseudoActiveFileDec);
 | 
						|
    this.disposables.dispose();
 | 
						|
  }
 | 
						|
 | 
						|
  private readonly handleTreeEvent = () => {
 | 
						|
    this.fileTreeEvent = this.props.onEvent;
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly handleTreeReady = (handle: IFileTreeHandle) => {
 | 
						|
    const { onReady, model } = this.props;
 | 
						|
    const scrollDiv = this.wrapperRef.current?.querySelector('div')?.querySelector('div');
 | 
						|
    if(this.props.onScroll) {
 | 
						|
      scrollDiv?.addEventListener('scroll', (ev: any)=>this.props.onScroll?.(ev));
 | 
						|
    }
 | 
						|
 | 
						|
    this.fileTreeHandle = {
 | 
						|
      ...handle,
 | 
						|
      getModel: () => this.props.model,
 | 
						|
      getActiveFile: () => this.activeFile,
 | 
						|
      setActiveFile: this.setActiveFile,
 | 
						|
      getPseudoActiveFile: () => this.pseudoActiveFile,
 | 
						|
      setPseudoActiveFile: this.setPseudoActiveFile,
 | 
						|
      toggleDirectory: this.toggleDirectory,
 | 
						|
      closeDir: this.closeDir,
 | 
						|
      newFile: async (dirOrPath: Directory | string) => this.supervisePrompt(await handle.promptNewFile(dirOrPath as string)),
 | 
						|
      newFolder: async (dirOrPath: Directory | string) => this.supervisePrompt(await handle.promptNewDirectory(dirOrPath as string)),
 | 
						|
      onBlur: (callback) => this.events.add(FileTreeXEvent.OnBlur, callback),
 | 
						|
      hasDirectFocus: () => this.wrapperRef.current === document.activeElement,
 | 
						|
      first: this.first,
 | 
						|
      parent: this.parent,
 | 
						|
      hasParent: this.hasParent,
 | 
						|
      isOpen: this.isOpen,
 | 
						|
      isClosed: this.isClosed,
 | 
						|
      itemData: this.itemData,
 | 
						|
      children: this.children,
 | 
						|
      getItemFromDOM: this.getItemFromDOM,
 | 
						|
      getDOMFromItem: this.getDOMFromItem,
 | 
						|
      onTreeEvents: (callback) => this.events.add(FileTreeXEvent.onTreeEvents, callback),
 | 
						|
      addIcon: this.addIcon,
 | 
						|
      addCssClass: this.addCssClass,
 | 
						|
      create: this.create,
 | 
						|
      remove: this.remove,
 | 
						|
      update: this.update,
 | 
						|
      refresh: this.refresh,
 | 
						|
      setLabel: this.setLabel,
 | 
						|
      unload: this.unload,
 | 
						|
      deSelectActiveFile: this.deSelectActiveFile,
 | 
						|
      resize: this.resize,
 | 
						|
      showLoader: this.showLoader,
 | 
						|
      hideLoader: this.hideLoader,
 | 
						|
      toggleItemLoader: this.toggleItemLoader,
 | 
						|
    };
 | 
						|
 | 
						|
    model.decorations.addDecoration(this.activeFileDec);
 | 
						|
    model.decorations.addDecoration(this.pseudoActiveFileDec);
 | 
						|
 | 
						|
    this.disposables.add(this.fileTreeHandle.onDidChangeModel((prevModel: TreeModelX, newModel: TreeModelX) => {
 | 
						|
      this.setActiveFile(null);
 | 
						|
      this.setPseudoActiveFile(null);
 | 
						|
      prevModel.decorations.removeDecoration(this.activeFileDec);
 | 
						|
      prevModel.decorations.removeDecoration(this.pseudoActiveFileDec);
 | 
						|
      newModel.decorations.addDecoration(this.activeFileDec);
 | 
						|
      newModel.decorations.addDecoration(this.pseudoActiveFileDec);
 | 
						|
    }));
 | 
						|
 | 
						|
    this.disposables.add(this.fileTreeHandle.onBlur(() => {
 | 
						|
      this.setPseudoActiveFile(null);
 | 
						|
    }));
 | 
						|
 | 
						|
    this.keyboardHotkeys = new KeyboardHotkeys(this.fileTreeHandle, this.events);
 | 
						|
 | 
						|
    if (typeof onReady === 'function') {
 | 
						|
      onReady(this.fileTreeHandle);
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly onItemMouseEnter = (ev: React.MouseEvent, item: FileEntry | Directory) => {
 | 
						|
    clearTimeout(this.hoverDispatchId.current??undefined);
 | 
						|
    (this.hoverDispatchId as any).current = setTimeout(()=>{
 | 
						|
      clearTimeout(this.hoverTimeoutId.current??undefined);
 | 
						|
      this.events.dispatch(FileTreeXEvent.onTreeEvents, ev, 'hovered', item);
 | 
						|
    }, 500);
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly onItemMouseLeave = (ev: React.MouseEvent) => {
 | 
						|
    clearTimeout(this.hoverTimeoutId.current??undefined);
 | 
						|
    clearTimeout(this.hoverDispatchId.current??undefined);
 | 
						|
    (this.hoverTimeoutId as any).current = setTimeout(()=>{
 | 
						|
      this.events.dispatch(FileTreeXEvent.onTreeEvents, ev, 'hovered', null);
 | 
						|
    }, 100);
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly setActiveFile = async (fileOrDirOrPath: FileOrDir | string, ensureVisible, align): Promise<void> => {
 | 
						|
    const fileH = typeof fileOrDirOrPath === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
 | 
						|
      : fileOrDirOrPath;
 | 
						|
 | 
						|
    if (fileH === this.props.model.root) { return; }
 | 
						|
    if (this.activeFile !== fileH) {
 | 
						|
      if (this.activeFile) {
 | 
						|
        this.activeFileDec.removeTarget(this.activeFile);
 | 
						|
      }
 | 
						|
      if (fileH) {
 | 
						|
        this.activeFileDec.addTarget(fileH as Directory, TargetMatchMode.Self);
 | 
						|
      }
 | 
						|
      this.activeFile = fileH;
 | 
						|
      this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'selected', fileH);
 | 
						|
 | 
						|
      if (fileH && ensureVisible === true) {
 | 
						|
        const alignTree = align ?? 'auto';
 | 
						|
        await this.fileTreeHandle.ensureVisible(fileH, alignTree);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly ensureVisible = async (fileOrDirOrPath: FileOrDir | string): Promise<void> => {
 | 
						|
    const fileH = typeof fileOrDirOrPath === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
 | 
						|
      : fileOrDirOrPath;
 | 
						|
 | 
						|
    if (fileH) {
 | 
						|
      await this.fileTreeHandle.ensureVisible(fileH);
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly deSelectActiveFile = async (fileOrDirOrPath: FileOrDir | string): Promise<void> => {
 | 
						|
    const fileH = typeof fileOrDirOrPath === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
 | 
						|
      : fileOrDirOrPath;
 | 
						|
 | 
						|
    if (fileH === this.props.model.root) { return; }
 | 
						|
    if (this.activeFile === fileH) {
 | 
						|
      this.activeFileDec.removeTarget(this.activeFile);
 | 
						|
      this.activeFile = null;
 | 
						|
    }
 | 
						|
 | 
						|
    this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'deselected', fileH);
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly setPseudoActiveFile = async (fileOrDirOrPath: FileOrDir | string): Promise<void> => {
 | 
						|
    const fileH = typeof fileOrDirOrPath === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
 | 
						|
      : fileOrDirOrPath;
 | 
						|
 | 
						|
    if (fileH === this.props.model.root) { return; }
 | 
						|
    if (this.pseudoActiveFile !== fileH) {
 | 
						|
      if (this.pseudoActiveFile) {
 | 
						|
        this.pseudoActiveFileDec.removeTarget(this.pseudoActiveFile);
 | 
						|
      }
 | 
						|
      if (fileH) {
 | 
						|
        this.pseudoActiveFileDec.addTarget(fileH as Directory, TargetMatchMode.Self);
 | 
						|
      }
 | 
						|
      this.pseudoActiveFile = fileH;
 | 
						|
    }
 | 
						|
    if (fileH) {
 | 
						|
      await this.fileTreeHandle.ensureVisible(fileH);
 | 
						|
    }
 | 
						|
    this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'selected', fileH);
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly create = async (parentDir, itemData): Promise<void> => {
 | 
						|
    if (parentDir == undefined || parentDir == null) {
 | 
						|
      parentDir = this.props.model.root;
 | 
						|
    }
 | 
						|
    const {create, model } = this.props;
 | 
						|
    const isOpen = parentDir.isExpanded;
 | 
						|
    let maybeFile = undefined;
 | 
						|
 | 
						|
    if (isOpen && (parentDir._children == null || parentDir._children.length == 0)) {
 | 
						|
      await this.fileTreeHandle.closeDirectory(parentDir as Directory);
 | 
						|
    }
 | 
						|
    if (!parentDir.isExpanded && (parentDir._children == null || parentDir._children.length == 0)) {
 | 
						|
      await this.fileTreeHandle.openDirectory(parentDir as Directory);
 | 
						|
    } else {
 | 
						|
      await this.fileTreeHandle.openDirectory(parentDir as Directory);
 | 
						|
      maybeFile = await create(parentDir.path, itemData);
 | 
						|
      if (maybeFile?.type && maybeFile?.name) {
 | 
						|
        model.root.inotify({
 | 
						|
          type: WatchEvent.Added,
 | 
						|
          directory: parentDir.path,
 | 
						|
          file: maybeFile,
 | 
						|
        });
 | 
						|
      }
 | 
						|
    }
 | 
						|
    this.changeDirectoryCount(parentDir);
 | 
						|
    const newItem = parentDir._children.find((c) => c._metadata.data.id === itemData.id);
 | 
						|
    newItem.resolvedPathCache = newItem.parent.path + '/' + newItem._metadata.data.id;
 | 
						|
    return newItem;
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly update = async (item, itemData): Promise<void> => {
 | 
						|
    item._metadata.data = itemData;
 | 
						|
    await this.props.update(item.path, itemData);
 | 
						|
    this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'updated', item);
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly refresh = async (item): Promise<void> => {
 | 
						|
    const isOpen = item.isExpanded;
 | 
						|
    if (item.children && item.children.length > 0) {
 | 
						|
      for(const entry of item.children) {
 | 
						|
        await this.remove(entry).then(() => {/*intentional*/}, () => {console.warn('Error removing item');});
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (isOpen) {
 | 
						|
      const ref = FileTreeItem.itemIdToRefMap.get(item.id);
 | 
						|
 | 
						|
      if (ref) {
 | 
						|
        this.showLoader(ref);
 | 
						|
      }
 | 
						|
 | 
						|
      await this.fileTreeHandle.closeDirectory(item as Directory);
 | 
						|
      await this.fileTreeHandle.openDirectory(item as Directory);
 | 
						|
      await this.changeResolvePath(item as Directory);
 | 
						|
      this.changeDirectoryCount(item);
 | 
						|
 | 
						|
      if (ref) {
 | 
						|
        this.hideLoader(ref);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly unload = async (item): Promise<void> => {
 | 
						|
    const isOpen = item.isExpanded;
 | 
						|
    if (item.children && item.children.length > 0) {
 | 
						|
      for(const entry of item.children) {
 | 
						|
        await this.remove(entry).then(() => {/*intentional*/}, error => {console.warn(error);});
 | 
						|
      }
 | 
						|
    }
 | 
						|
    if (isOpen) {
 | 
						|
      await this.fileTreeHandle.closeDirectory(item as Directory);
 | 
						|
      this.changeDirectoryCount(item);
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly remove = async (item): Promise<void> => {
 | 
						|
    const {remove, model } = this.props;
 | 
						|
    const path = item.path;
 | 
						|
    await remove(path, false);
 | 
						|
    const dirName = model.root.pathfx.dirname(path);
 | 
						|
    const fileName = model.root.pathfx.basename(path);
 | 
						|
    const parent = item.parent;
 | 
						|
    if (dirName === parent.path) {
 | 
						|
      const item_1 = parent._children.find((c) => c._metadata && c._metadata.data.id === fileName);
 | 
						|
      if (item_1) {
 | 
						|
        parent.unlinkItem(item_1);
 | 
						|
        if (parent._children.length == 0) { parent._children = null; }
 | 
						|
        this.changeDirectoryCount(parent);
 | 
						|
        this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'removed', item);
 | 
						|
      }
 | 
						|
      else {
 | 
						|
        console.warn('Item not found');
 | 
						|
      }
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly first = async (fileOrDirOrPath: FileOrDir | string) => {
 | 
						|
    const fileH = typeof fileOrDirOrPath === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
 | 
						|
      : fileOrDirOrPath;
 | 
						|
 | 
						|
    if (fileH === undefined || fileH === null) { return this.props.model.root.children[0]; }
 | 
						|
 | 
						|
    if (fileH.branchSize > 0) {
 | 
						|
      return fileH.children[0];
 | 
						|
    }
 | 
						|
    return null;
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly parent = async (fileOrDirOrPath: FileOrDir | string) => {
 | 
						|
    const fileH = typeof fileOrDirOrPath === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
 | 
						|
      : fileOrDirOrPath;
 | 
						|
 | 
						|
    if (fileH === FileType.Directory || fileH === FileType.File) {
 | 
						|
      return fileH.parent;
 | 
						|
    }
 | 
						|
 | 
						|
    return null;
 | 
						|
  };
 | 
						|
 | 
						|
 | 
						|
  private readonly hasParent = async (fileOrDirOrPath: FileOrDir | string) => {
 | 
						|
    const fileH = typeof fileOrDirOrPath === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
 | 
						|
      : fileOrDirOrPath;
 | 
						|
 | 
						|
    if (fileH === FileType.Directory || fileH === FileType.File) {
 | 
						|
      return fileH.parent;
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly children = async (fileOrDirOrPath: FileOrDir | string) => {
 | 
						|
    const fileH = typeof fileOrDirOrPath === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
 | 
						|
      : fileOrDirOrPath;
 | 
						|
 | 
						|
    if (fileH === FileType.Directory) {
 | 
						|
      return fileH.children;
 | 
						|
    }
 | 
						|
 | 
						|
    return null;
 | 
						|
  };
 | 
						|
 | 
						|
 | 
						|
  private readonly isOpen = async (fileOrDirOrPath: FileOrDir | string) => {
 | 
						|
    const fileH = typeof fileOrDirOrPath === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
 | 
						|
      : fileOrDirOrPath;
 | 
						|
 | 
						|
    if (fileH === FileType.Directory) {
 | 
						|
      return fileH.isExpanded;
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly isClosed = async (fileOrDirOrPath: FileOrDir | string) => {
 | 
						|
    const fileH = typeof fileOrDirOrPath === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
 | 
						|
      : fileOrDirOrPath;
 | 
						|
 | 
						|
    if (fileH === FileType.Directory || fileH === FileType.File) {
 | 
						|
      return !fileH.isExpanded;
 | 
						|
    }
 | 
						|
 | 
						|
    return false;
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly itemData = async (fileOrDirOrPath: FileOrDir | string) => {
 | 
						|
    const fileH = typeof fileOrDirOrPath === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(fileOrDirOrPath)
 | 
						|
      : fileOrDirOrPath;
 | 
						|
 | 
						|
    if (fileH === FileType.Directory || fileH === FileType.File) {
 | 
						|
      return fileH._metadata.data;
 | 
						|
    }
 | 
						|
 | 
						|
    return null;
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly setLabel = async(pathOrDir: string | Directory, label: string): Promise<void> => {
 | 
						|
    const dir = typeof pathOrDir === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(pathOrDir)
 | 
						|
      : pathOrDir;
 | 
						|
 | 
						|
    const ref = FileTreeItem.itemIdToRefMap.get(dir.id);
 | 
						|
    if (ref) {
 | 
						|
      ref.style.background = 'none';
 | 
						|
      const label$ = ref.querySelector('span.file-name') as HTMLDivElement;
 | 
						|
 | 
						|
      if (label$) {
 | 
						|
        if (typeof(label) == 'object' && label.label) {
 | 
						|
          label = label.label;
 | 
						|
        }
 | 
						|
        label$.innerHTML = label;
 | 
						|
      }
 | 
						|
 | 
						|
    }
 | 
						|
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly changeDirectoryCount = async(pathOrDir: string | Directory): Promise<void> => {
 | 
						|
    const dir = typeof pathOrDir === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(pathOrDir)
 | 
						|
      : pathOrDir;
 | 
						|
 | 
						|
    if (dir.type === FileType.Directory && dir._metadata.data && dir._metadata.data.is_collection === true) {
 | 
						|
      const ref = FileTreeItem.itemIdToRefMap.get(dir.id);
 | 
						|
      if (ref) {
 | 
						|
        ref.style.background = 'none';
 | 
						|
        const label$ = ref.querySelector('span.children-count') as HTMLDivElement;
 | 
						|
        if(dir.children && dir.children.length > 0) {
 | 
						|
          label$.innerHTML = '(' + dir.children.length + ')';
 | 
						|
        } else {
 | 
						|
          label$.innerHTML = '';
 | 
						|
        }
 | 
						|
      }
 | 
						|
    }
 | 
						|
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly closeDir = async (pathOrDir: string | Directory) => {
 | 
						|
    const dir = typeof pathOrDir === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(pathOrDir)
 | 
						|
      : pathOrDir;
 | 
						|
 | 
						|
    if (dir.type === FileType.Directory) {
 | 
						|
      if ((dir as Directory).expanded) {
 | 
						|
        this.fileTreeHandle.closeDirectory(dir as Directory);
 | 
						|
        this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'closed', dir);
 | 
						|
 | 
						|
      }
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly toggleDirectory = async (pathOrDir: string | Directory) => {
 | 
						|
    const dir = typeof pathOrDir === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(pathOrDir)
 | 
						|
      : pathOrDir;
 | 
						|
 | 
						|
    if (dir.type === FileType.Directory) {
 | 
						|
      if ((dir as Directory).expanded) {
 | 
						|
        this.fileTreeHandle.closeDirectory(dir as Directory);
 | 
						|
        this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'closed', dir);
 | 
						|
 | 
						|
      } else {
 | 
						|
        const ref = FileTreeItem.itemIdToRefMap.get(dir.id);
 | 
						|
        if (ref) {
 | 
						|
          this.showLoader(ref);
 | 
						|
        }
 | 
						|
 | 
						|
        await this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'beforeopen', dir);
 | 
						|
        await this.fileTreeHandle.openDirectory(dir as Directory);
 | 
						|
        await this.changeResolvePath(dir as Directory);
 | 
						|
 | 
						|
        if (ref) {
 | 
						|
          this.hideLoader(ref);
 | 
						|
        }
 | 
						|
 | 
						|
        this.events.dispatch(FileTreeXEvent.onTreeEvents, window.event, 'opened', dir);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly addIcon = async (pathOrDir: string | Directory, icon) => {
 | 
						|
    const dir = typeof pathOrDir === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(pathOrDir)
 | 
						|
      : pathOrDir;
 | 
						|
 | 
						|
    const ref = FileTreeItem.itemIdToRefMap.get(dir.id);
 | 
						|
    if (ref) {
 | 
						|
      const label$ = ref.querySelector('.file-label i') as HTMLDivElement;
 | 
						|
      label$.className = icon.icon;
 | 
						|
    }
 | 
						|
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly addCssClass = async (pathOrDir: string | Directory, cssClass) => {
 | 
						|
    const dir = typeof pathOrDir === 'string'
 | 
						|
      ? await this.fileTreeHandle.getFileHandle(pathOrDir)
 | 
						|
      : pathOrDir;
 | 
						|
 | 
						|
    const ref = FileTreeItem.itemIdToRefMap.get(dir.id);
 | 
						|
    if (ref) {
 | 
						|
      ref.classList.add(cssClass);
 | 
						|
      if (!dir._metadata.data.extraClasses)
 | 
						|
        dir._metadata.data.extraClasses = [];
 | 
						|
 | 
						|
      dir._metadata.data.extraClasses.push(cssClass);
 | 
						|
    }
 | 
						|
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly toggleItemLoader = (item: FileOrDir, show=false) => {
 | 
						|
    const ref = FileTreeItem.itemIdToRefMap.get(item.id);
 | 
						|
    if (ref) {
 | 
						|
      if (show) {
 | 
						|
        this.showLoader(ref);
 | 
						|
      } else {
 | 
						|
        this.hideLoader(ref);
 | 
						|
      }
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly showLoader = (ref: HTMLDivElement) => {
 | 
						|
    // get label ref and add loading class
 | 
						|
    ref.style.background = 'none';
 | 
						|
    const label$ = ref.querySelector('i.directory-toggle') as HTMLDivElement;
 | 
						|
    if (label$)  label$.classList.add('loading');
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly hideLoader = (ref: HTMLDivElement) => {
 | 
						|
    // remove loading class.
 | 
						|
    ref.style.background = 'none';
 | 
						|
    const label$ = ref.querySelector('i.directory-toggle') as HTMLDivElement;
 | 
						|
    if (label$) label$.classList.remove('loading');
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly handleBlur = () => {
 | 
						|
    this.events.dispatch(FileTreeXEvent.OnBlur);
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly handleItemClicked = async (ev: React.MouseEvent, item: FileOrDir, type: ItemType) => {
 | 
						|
    if (type === ItemType.Directory && ev.target.className.includes('directory-toggle')) {
 | 
						|
      await this.toggleDirectory(item as Directory);
 | 
						|
    }
 | 
						|
    await this.setActiveFile(item as FileEntry);
 | 
						|
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly handleItemDoubleClicked = async (ev: React.MouseEvent, item: FileOrDir) => {
 | 
						|
    await this.toggleDirectory(item as Directory);
 | 
						|
    await this.setActiveFile(item as FileEntry);
 | 
						|
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly getItemFromDOM = (clientReact) => {
 | 
						|
    return FileTreeItem.refToItemIdMap.get(clientReact);
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly getDOMFromItem = (item: FileOrDir) => {
 | 
						|
    return FileTreeItem.itemIdToRefMap.get(item.id);
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly handleClick = (ev: React.MouseEvent) => {
 | 
						|
    // clicked in "blank space"
 | 
						|
    if (ev.currentTarget === ev.target) {
 | 
						|
      this.setPseudoActiveFile(null);
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly handleItemCtxMenu = (ev: React.MouseEvent, item: FileOrDir) => {
 | 
						|
    return this.props.onContextMenu?.(ev, item);
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly handleKeyDown = (ev: React.KeyboardEvent) => {
 | 
						|
    return this.keyboardHotkeys.handleKeyDown(ev);
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly onResize = () => {
 | 
						|
    if (this.wrapperRef.current != null) {
 | 
						|
      this.resize();
 | 
						|
    }
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly resize = (scrollX, scrollY) => {
 | 
						|
    const scrollXPos = scrollX || 0;
 | 
						|
    const scrollYPos = scrollY || this.props.model.state.scrollOffset;
 | 
						|
    const div = this.wrapperRef.current.querySelector('div').querySelector('div') as HTMLDivElement;
 | 
						|
    if (div) {
 | 
						|
      div.scroll(scrollXPos, scrollYPos);
 | 
						|
    }
 | 
						|
 | 
						|
  };
 | 
						|
 | 
						|
  private readonly changeResolvePath = async (item: FileOrDir): Promise<void> => {
 | 
						|
    // Change the path as per pgAdmin requirement: Item Id wise
 | 
						|
    if (item.type === FileType.File) {
 | 
						|
      item.resolvedPathCache = item.parent.path + '/' + item._metadata.data.id;
 | 
						|
    }
 | 
						|
    if (item.type === FileType.Directory && item.children && item.children.length > 0) {
 | 
						|
      for(const entry of item.children) {
 | 
						|
        entry.resolvedPathCache = entry.parent.path + '/' + entry._metadata.data.id;
 | 
						|
      }
 | 
						|
    }
 | 
						|
  };
 | 
						|
}
 | 
						|
 | 
						|
export { IFileTreeXHandle, IFileTreeXProps };
 |