import { Disposable, DisposableCollection, MenuPath, MessageService } from '@theia/core'; import { ContextMenuRenderer, NodeProps, TreeModel, TreeNode, TreeProps, TreeWidget } from '@theia/core/lib/browser'; import { inject, injectable } from '@theia/core/shared/inversify'; import * as React from '@theia/core/shared/react'; import '../../src/browser/styles/treeview-example-widget.css'; import { ExampleTreeLeaf, ExampleTreeNode, TreeViewExampleModel } from './treeview-example-model'; /** Well-known constant for the context menu path */ export const TREEVIEW_EXAMPLE_CONTEXT_MENU: MenuPath = ['theia-examples:treeview-example-context-menu']; /** Implementation of the Tree Widget */ @injectable() export class TreeViewExampleWidget extends TreeWidget { /** The ID of the view */ static readonly ID = 'theia-examples:treeview-example-view'; /** The label of the view */ static readonly LABEL = 'Example Tree View'; /** Used in Drag & Drop code to remember and cancel deferred expansion of hovered nodes */ protected readonly toCancelNodeExpansion = new DisposableCollection(); /** The MessageService to demonstrate the action when a user opens (double-clicks) a node */ @inject(MessageService) private readonly messageService: MessageService; constructor( @inject(TreeProps) public override readonly props: TreeProps, @inject(TreeModel) public override readonly model: TreeViewExampleModel, @inject(ContextMenuRenderer) contextMenuRenderer: ContextMenuRenderer ) { super(props, model, contextMenuRenderer); // set the general properties for the view this.id = TreeViewExampleWidget.ID; this.title.label = TreeViewExampleWidget.LABEL; this.title.caption = TreeViewExampleWidget.LABEL; this.title.closable = true; this.title.iconClass = 'fa fa-smile-o'; // register action on double-click / ENTER key this.toDispose.push(this.model.onOpenNode((node: TreeNode) => { if (ExampleTreeLeaf.is(node) || ExampleTreeNode.is(node)) { this.messageService.info(`Example node ${node.data.name} was opened.`); } })); this.toDispose.push(this.toCancelNodeExpansion); } /** * Enable icon rendering. * * The super implementation is currently empty. * This implementation is taken from `file-tree-widget.tsx`. * * @param node the node to render * @param props the node props (currently transporting the depth of the item in the tree) * @returns */ protected override renderIcon(node: TreeNode, props: NodeProps): React.ReactNode { const icon = this.getIconClass(this.toNodeIcon(node)); if (icon) { return
; } return super.renderIcon(node, props); } /** * Provide CSS class names for a given tree node. * * In our example, we append our own CSS class to all nodes. See/modify the included CSS file for the corresponding style. * * @param node the node to render * @param props the node props (currently transporting the depth of the item in the tree) * @returns the node's CSS classes */ protected override createNodeClassNames(node: TreeNode, props: NodeProps): string[] { return super.createNodeClassNames(node, props).concat('theia-example-tree-node'); } /** * Provide node element attributes for a given tree node. * * In our example, we use this to add Drag & Drop event handlers to the tree nodes. * * Note: the Drag & Drop code has been taken and adapted from `file-tree-widget.tsx` * * @param node the node to render * @param props the node props (currently transporting the depth of the item in the tree) * @returns the HTML element attributes. */ protected override createNodeAttributes(node: TreeNode, props: NodeProps): React.Attributes & React.HTMLAttributes { return { ...super.createNodeAttributes(node, props), ...this.getNodeDragHandlers(node), }; } /** * Returns HTML attributes to install Drag & Drop event handlers for the given tree node. * * Note: the Drag & Drop code has been taken and adapted from `file-tree-widget.tsx` * * @param node the tree node * @returns the drag event handlers to be used as additional HTML element attributes */ protected getNodeDragHandlers(node: TreeNode): React.Attributes & React.HtmlHTMLAttributes { return { onDragStart: event => this.handleDragStartEvent(node, event), onDragEnter: event => this.handleDragEnterEvent(node, event), onDragOver: event => this.handleDragOverEvent(node, event), onDragLeave: event => this.handleDragLeaveEvent(node, event), onDrop: event => this.handleDropEvent(node, event), draggable: ExampleTreeLeaf.is(node), }; } /** * Handler for the _dragStart_ event. * * Stores the ID of the dragged tree node in the Drag & Drop data. * * @param node the tree node * @param event the event */ protected handleDragStartEvent(node: TreeNode, event: React.DragEvent): void { event.stopPropagation(); if (event.dataTransfer) { event.dataTransfer.setData('tree-node', node.id); } } /** * Handler for the _dragOver_ event. * * Registers deferred tree expansion that shall be triggered if the user hovers over an expandable tree item for * some time. * * @param node the tree node * @param event the event */ protected handleDragOverEvent(node: TreeNode | undefined, event: React.DragEvent): void { event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = 'move'; if (!this.toCancelNodeExpansion.disposed) { return; } const timer = setTimeout(() => { if (!!node && ExampleTreeNode.is(node) && !node.expanded) { this.model.expandNode(node); } }, 500); this.toCancelNodeExpansion.push(Disposable.create(() => clearTimeout(timer))); } /** * Handler for the _dragEnter_ event. * * Cancels any pending deferred tree extension, selects the current target node to highlight it in the UI, and * sets the Drag & Drop indicator to "move". * * @param node the tree node * @param event the event */ protected handleDragEnterEvent(node: TreeNode | undefined, event: React.DragEvent): void { event.preventDefault(); event.stopPropagation(); this.toCancelNodeExpansion.dispose(); let target = node; if (target && ExampleTreeLeaf.is(target)) { target = target.parent; } if (!!target && ExampleTreeNode.is(target) && !target.selected) { this.model.selectNode(target); } } /** * Handler for the _dragLeave_ event. * * Cancels any pending deferred tree extension. * * @param node the tree node * @param event the event */ protected handleDragLeaveEvent(node: TreeNode | undefined, event: React.DragEvent): void { event.preventDefault(); event.stopPropagation(); this.toCancelNodeExpansion.dispose(); } /** * Handler for the _drop_ event. * * Calls the code to move the dragged node to the new parent. * * @param node the tree node * @param event the event */ protected async handleDropEvent(node: TreeNode | undefined, event: React.DragEvent): Promise { event.preventDefault(); event.stopPropagation(); event.dataTransfer.dropEffect = 'move'; let target = node; if (target && ExampleTreeLeaf.is(target)) { target = target.parent; } if (!!target && ExampleTreeNode.is(target)) { const draggedNodeId = event.dataTransfer.getData('tree-node'); this.model.reparent(draggedNodeId, target); } } }