/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import 'vs/css!./tree'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { IListOptions, List, IIdentityProvider, IMultipleSelectionController } from 'vs/base/browser/ui/list/listWidget'; import { TreeModel, ITreeNode, ITreeElement, getNodeLocation } from 'vs/base/browser/ui/tree/treeModel'; import { IIterator, empty } from 'vs/base/common/iterator'; import { IDelegate, IRenderer, IListMouseEvent } from 'vs/base/browser/ui/list/list'; import { append, $ } from 'vs/base/browser/dom'; import { Event, Relay, chain } from 'vs/base/common/event'; import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent'; import { KeyCode } from 'vs/base/common/keyCodes'; import { tail2 } from 'vs/base/common/arrays'; function toTreeListOptions(options?: IListOptions): IListOptions> { if (!options) { return undefined; } let identityProvider: IIdentityProvider> | undefined = undefined; let multipleSelectionController: IMultipleSelectionController> | undefined = undefined; if (options.identityProvider) { identityProvider = el => options.identityProvider(el.element); } if (options.multipleSelectionController) { multipleSelectionController = { isSelectionSingleChangeEvent(e) { return options.multipleSelectionController.isSelectionSingleChangeEvent({ ...e, element: e.element } as any); }, isSelectionRangeChangeEvent(e) { return options.multipleSelectionController.isSelectionRangeChangeEvent({ ...e, element: e.element } as any); } }; } return { ...options, identityProvider, multipleSelectionController }; } class TreeDelegate implements IDelegate> { constructor(private delegate: IDelegate) { } getHeight(element: ITreeNode): number { return this.delegate.getHeight(element.element); } getTemplateId(element: ITreeNode): string { return this.delegate.getTemplateId(element.element); } } interface ITreeListTemplateData { twistie: HTMLElement; templateData: T; } function renderTwistie(node: ITreeNode, twistie: HTMLElement): void { if (node.children.length === 0 && !node.collapsible) { twistie.innerText = ''; } else { twistie.innerText = node.collapsed ? '▹' : '◢'; } } class TreeRenderer implements IRenderer, ITreeListTemplateData> { readonly templateId: string; private renderedNodes = new Map, ITreeListTemplateData>(); private disposables: IDisposable[] = []; constructor( private renderer: IRenderer, onDidChangeCollapseState: Event> ) { this.templateId = renderer.templateId; onDidChangeCollapseState(this.onDidChangeCollapseState, this, this.disposables); } renderTemplate(container: HTMLElement): ITreeListTemplateData { const el = append(container, $('.monaco-tl-row')); const twistie = append(el, $('.tl-twistie')); const contents = append(el, $('.tl-contents')); const templateData = this.renderer.renderTemplate(contents); return { twistie, templateData }; } renderElement(node: ITreeNode, index: number, templateData: ITreeListTemplateData): void { this.renderedNodes.set(node, templateData); templateData.twistie.style.width = `${10 + node.depth * 10}px`; renderTwistie(node, templateData.twistie); this.renderer.renderElement(node.element, index, templateData.templateData); } disposeElement(node: ITreeNode): void { this.renderedNodes.delete(node); } disposeTemplate(templateData: ITreeListTemplateData): void { this.renderer.disposeTemplate(templateData.templateData); } private onDidChangeCollapseState(node: ITreeNode): void { const templateData = this.renderedNodes.get(node); if (!templateData) { return; } renderTwistie(node, templateData.twistie); } dispose(): void { this.renderedNodes.clear(); this.disposables = dispose(this.disposables); } } function isInputElement(e: HTMLElement): boolean { return e.tagName === 'INPUT' || e.tagName === 'TEXTAREA'; } export class Tree implements IDisposable { private view: List>; private model: TreeModel; private disposables: IDisposable[] = []; constructor( container: HTMLElement, delegate: IDelegate, renderers: IRenderer[], options?: IListOptions ) { const treeDelegate = new TreeDelegate(delegate); const onDidChangeCollapseStateRelay = new Relay>(); const treeRenderers = renderers.map(r => new TreeRenderer(r, onDidChangeCollapseStateRelay.event)); this.disposables.push(...treeRenderers); const treeOptions = toTreeListOptions(options); this.view = new List(container, treeDelegate, treeRenderers, treeOptions); this.model = new TreeModel(this.view); onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState; this.view.onMouseClick(this.onMouseClick, this, this.disposables); const onKeyDown = chain(this.view.onKeyDown) .filter(e => !isInputElement(e.target as HTMLElement)) .map(e => new StandardKeyboardEvent(e)); onKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow).on(this.onLeftArrow, this, this.disposables); onKeyDown.filter(e => e.keyCode === KeyCode.RightArrow).on(this.onRightArrow, this, this.disposables); onKeyDown.filter(e => e.keyCode === KeyCode.Space).on(this.onSpace, this, this.disposables); } splice(location: number[], deleteCount: number, toInsert: IIterator> = empty()): IIterator> { return this.model.splice(location, deleteCount, toInsert); } private onMouseClick(e: IListMouseEvent>): void { const node = e.element; const location = getNodeLocation(node); this.model.toggleCollapsed(location); } private onLeftArrow(e: StandardKeyboardEvent): void { e.preventDefault(); e.stopPropagation(); const nodes = this.view.getFocusedElements(); if (nodes.length === 0) { return; } const node = nodes[0]; const location = getNodeLocation(node); const didChange = this.model.setCollapsed(location, true); if (!didChange) { if (location.length === 1) { return; } const [parentLocation] = tail2(location); const parentListIndex = this.model.getListIndex(parentLocation); this.view.setFocus([parentListIndex]); } } private onRightArrow(e: StandardKeyboardEvent): void { e.preventDefault(); e.stopPropagation(); const nodes = this.view.getFocusedElements(); if (nodes.length === 0) { return; } const node = nodes[0]; const location = getNodeLocation(node); const didChange = this.model.setCollapsed(location, false); if (!didChange) { if (node.children.length === 0) { return; } const [focusedIndex] = this.view.getFocus(); this.view.setFocus([focusedIndex + 1]); } } private onSpace(e: StandardKeyboardEvent): void { e.preventDefault(); e.stopPropagation(); const nodes = this.view.getFocusedElements(); if (nodes.length === 0) { return; } const node = nodes[0]; const location = getNodeLocation(node); this.model.toggleCollapsed(location); } dispose(): void { this.disposables = dispose(this.disposables); this.view.dispose(); this.view = null; this.model = null; } }