@theia/core
Version:
Theia is a cloud & desktop IDE framework implemented in TypeScript.
1,245 lines • 50.3 kB
JavaScript
"use strict";
// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
// *****************************************************************************
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
var __metadata = (this && this.__metadata) || function (k, v) {
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
};
var __param = (this && this.__param) || function (paramIndex, decorator) {
return function (target, key) { decorator(target, key, paramIndex); }
};
var TreeWidget_1;
Object.defineProperty(exports, "__esModule", { value: true });
exports.TreeWidget = exports.defaultTreeProps = exports.TreeProps = exports.TREE_NODE_INDENT_GUIDE_CLASS = exports.TREE_NODE_CAPTION_CLASS = exports.COMPOSITE_TREE_NODE_CLASS = exports.EXPANDABLE_TREE_NODE_CLASS = exports.TREE_NODE_SEGMENT_GROW_CLASS = exports.TREE_NODE_SEGMENT_CLASS = exports.TREE_NODE_TAIL_CLASS = exports.TREE_NODE_INFO_CLASS = exports.TREE_NODE_CONTENT_CLASS = exports.TREE_NODE_CLASS = exports.TREE_CONTAINER_CLASS = exports.TREE_CLASS = void 0;
const inversify_1 = require("inversify");
const common_1 = require("../../common");
const keys_1 = require("../keyboard/keys");
const context_menu_renderer_1 = require("../context-menu-renderer");
const widgets_1 = require("../widgets");
const tree_1 = require("./tree");
const tree_model_1 = require("./tree-model");
const tree_expansion_1 = require("./tree-expansion");
const tree_selection_1 = require("./tree-selection");
const tree_decorator_1 = require("./tree-decorator");
const objects_1 = require("../../common/objects");
const os_1 = require("../../common/os");
const react_widget_1 = require("../widgets/react-widget");
const React = require("react");
const react_virtuoso_1 = require("react-virtuoso");
const tree_iterator_1 = require("./tree-iterator");
const search_box_1 = require("./search-box");
const tree_search_1 = require("./tree-search");
const domutils_1 = require("@phosphor/domutils");
const tree_widget_selection_1 = require("./tree-widget-selection");
const label_provider_1 = require("../label-provider");
const core_preferences_1 = require("../core-preferences");
const tree_focus_service_1 = require("./tree-focus-service");
const react_1 = require("react");
const debounce = require('lodash.debounce');
exports.TREE_CLASS = 'theia-Tree';
exports.TREE_CONTAINER_CLASS = 'theia-TreeContainer';
exports.TREE_NODE_CLASS = 'theia-TreeNode';
exports.TREE_NODE_CONTENT_CLASS = 'theia-TreeNodeContent';
exports.TREE_NODE_INFO_CLASS = 'theia-TreeNodeInfo';
exports.TREE_NODE_TAIL_CLASS = 'theia-TreeNodeTail';
exports.TREE_NODE_SEGMENT_CLASS = 'theia-TreeNodeSegment';
exports.TREE_NODE_SEGMENT_GROW_CLASS = 'theia-TreeNodeSegmentGrow';
exports.EXPANDABLE_TREE_NODE_CLASS = 'theia-ExpandableTreeNode';
exports.COMPOSITE_TREE_NODE_CLASS = 'theia-CompositeTreeNode';
exports.TREE_NODE_CAPTION_CLASS = 'theia-TreeNodeCaption';
exports.TREE_NODE_INDENT_GUIDE_CLASS = 'theia-tree-node-indent';
exports.TreeProps = Symbol('TreeProps');
/**
* The default tree properties.
*/
exports.defaultTreeProps = {
leftPadding: 8,
expansionTogglePadding: 22
};
let TreeWidget = TreeWidget_1 = class TreeWidget extends react_widget_1.ReactWidget {
constructor(props, model, contextMenuRenderer) {
super();
this.props = props;
this.model = model;
this.contextMenuRenderer = contextMenuRenderer;
this.decorations = new Map();
this.shouldScrollToRow = true;
this.rows = new Map();
this.updateRows = debounce(() => this.doUpdateRows(), 10);
this.scheduleUpdateScrollToRow = debounce(this.updateScrollToRow);
/**
* Update tree decorations.
* - Updating decorations are debounced in order to limit the number of expensive updates.
*/
this.updateDecorations = debounce(() => this.doUpdateDecorations(), 150);
this.ScrollingRowRenderer = ({ rows }) => {
(0, react_1.useEffect)(() => this.scrollToSelected());
return React.createElement(React.Fragment, null, rows.map(row => React.createElement("div", { key: row.index }, this.renderNodeRow(row))));
};
this.scrollArea = this.node;
/**
* Render the node row.
*/
this.renderNodeRow = (row) => this.doRenderNodeRow(row);
/**
* Toggle the node.
*/
this.toggle = (event) => this.doToggle(event);
/**
* Handle the double-click mouse event on the expansion toggle.
*/
this.handleExpansionToggleDblClickEvent = (event) => this.doHandleExpansionToggleDblClickEvent(event);
this.scrollOptions = {
suppressScrollX: true,
minScrollbarLength: 35
};
this.addClass(exports.TREE_CLASS);
this.node.tabIndex = 0;
}
init() {
if (this.props.search) {
this.searchBox = this.searchBoxFactory(Object.assign(Object.assign({}, search_box_1.SearchBoxProps.DEFAULT), { showButtons: true, showFilter: true }));
this.searchBox.node.addEventListener('focus', () => {
this.node.focus();
});
this.toDispose.pushAll([
this.searchBox,
this.searchBox.onTextChange(async (data) => {
await this.treeSearch.filter(data);
this.searchHighlights = this.treeSearch.getHighlights();
this.searchBox.updateHighlightInfo({
filterText: data,
total: this.rows.size,
matched: this.searchHighlights.size
});
this.update();
}),
this.searchBox.onClose(data => this.treeSearch.filter(undefined)),
this.searchBox.onNext(() => {
// Enable next selection if there are currently highlights.
if (this.searchHighlights.size > 1) {
this.model.selectNextNode();
}
}),
this.searchBox.onPrevious(() => {
// Enable previous selection if there are currently highlights.
if (this.searchHighlights.size > 1) {
this.model.selectPrevNode();
}
}),
this.searchBox.onFilterToggled(e => {
this.updateRows();
}),
this.treeSearch,
this.treeSearch.onFilteredNodesChanged(nodes => {
if (this.searchBox.isFiltering) {
this.updateRows();
}
const node = nodes.find(tree_selection_1.SelectableTreeNode.is);
if (node) {
this.model.selectNode(node);
}
}),
]);
}
this.toDispose.pushAll([
this.model,
this.model.onChanged(() => this.updateRows()),
this.model.onSelectionChanged(() => this.scheduleUpdateScrollToRow({ resize: false })),
this.focusService.onDidChangeFocus(() => this.scheduleUpdateScrollToRow({ resize: false })),
this.model.onDidChangeBusy(() => this.update()),
this.model.onNodeRefreshed(() => this.updateDecorations()),
this.model.onExpansionChanged(() => this.updateDecorations()),
this.decoratorService,
this.decoratorService.onDidChangeDecorations(() => this.updateDecorations()),
this.labelProvider.onDidChange(e => {
for (const row of this.rows.values()) {
if (e.affects(row)) {
this.update();
return;
}
}
})
]);
setTimeout(() => {
this.updateRows();
this.updateDecorations();
});
if (this.props.globalSelection) {
this.toDispose.pushAll([
this.model.onSelectionChanged(() => {
if (this.node.contains(document.activeElement)) {
this.updateGlobalSelection();
}
}),
this.focusService.onDidChangeFocus(focus => {
if (focus && this.node.contains(document.activeElement) && this.model.selectedNodes[0] !== focus && this.model.selectedNodes.includes(focus)) {
this.updateGlobalSelection();
}
}),
common_1.Disposable.create(() => {
const selection = this.selectionService.selection;
if (tree_widget_selection_1.TreeWidgetSelection.isSource(selection, this)) {
this.selectionService.selection = undefined;
}
})
]);
}
this.toDispose.push(this.corePreferences.onPreferenceChanged(preference => {
if (preference.preferenceName === 'workbench.tree.renderIndentGuides') {
this.update();
}
}));
}
/**
* Update the global selection for the tree.
*/
updateGlobalSelection() {
this.selectionService.selection = tree_widget_selection_1.TreeWidgetSelection.create(this);
}
doUpdateRows() {
const root = this.model.root;
const rowsToUpdate = [];
if (root) {
const depths = new Map();
let index = 0;
for (const node of new tree_iterator_1.TopDownTreeIterator(root, {
pruneCollapsed: true,
pruneSiblings: true
})) {
if (this.shouldDisplayNode(node)) {
const depth = this.getDepthForNode(node, depths);
if (tree_1.CompositeTreeNode.is(node)) {
depths.set(node, depth);
}
rowsToUpdate.push([node.id, this.toNodeRow(node, index++, depth)]);
}
}
}
this.rows = new Map(rowsToUpdate);
this.updateScrollToRow();
}
getDepthForNode(node, depths) {
const parentDepth = depths.get(node.parent);
return parentDepth === undefined ? 0 : tree_1.TreeNode.isVisible(node.parent) ? parentDepth + 1 : parentDepth;
}
toNodeRow(node, index, depth) {
return { node, index, depth };
}
shouldDisplayNode(node) {
var _a;
return tree_1.TreeNode.isVisible(node) && (!((_a = this.searchBox) === null || _a === void 0 ? void 0 : _a.isFiltering) || this.treeSearch.passesFilters(node));
}
/**
* Update the `scrollToRow`.
* @param updateOptions the tree widget force update options.
*/
updateScrollToRow() {
this.scrollToRow = this.getScrollToRow();
this.update();
}
/**
* Get the `scrollToRow`.
*
* @returns the `scrollToRow` if available.
*/
getScrollToRow() {
var _a;
if (!this.shouldScrollToRow) {
return undefined;
}
const { focusedNode } = this.focusService;
return focusedNode && ((_a = this.rows.get(focusedNode.id)) === null || _a === void 0 ? void 0 : _a.index);
}
async doUpdateDecorations() {
this.decorations = await this.decoratorService.getDecorations(this.model);
this.update();
}
onActivateRequest(msg) {
super.onActivateRequest(msg);
this.node.focus({ preventScroll: true });
}
/**
* Actually focus the tree node.
*/
doFocus() {
if (!this.model.selectedNodes.length) {
const node = this.getNodeToFocus();
if (tree_selection_1.SelectableTreeNode.is(node)) {
this.model.selectNode(node);
}
}
}
/**
* Get the tree node to focus.
*
* @returns the node to focus if available.
*/
getNodeToFocus() {
const { focusedNode } = this.focusService;
if (focusedNode) {
return focusedNode;
}
const { root } = this.model;
if (tree_selection_1.SelectableTreeNode.isVisible(root)) {
return root;
}
return this.model.getNextSelectableNode(root);
}
onUpdateRequest(msg) {
if (!this.isAttached || !this.isVisible) {
return;
}
super.onUpdateRequest(msg);
}
onResize(msg) {
super.onResize(msg);
this.update();
}
render() {
return React.createElement('div', this.createContainerAttributes(), this.renderTree(this.model));
}
/**
* Create the container attributes for the widget.
*/
createContainerAttributes() {
const classNames = [exports.TREE_CONTAINER_CLASS];
if (!this.rows.size) {
classNames.push('empty');
}
if (this.model.selectedNodes.length === 0 && !this.focusService.focusedNode) {
classNames.push('focused');
}
return {
className: classNames.join(' '),
onContextMenu: event => this.handleContextMenuEvent(this.getContainerTreeNode(), event)
};
}
/**
* Get the container tree node.
*
* @returns the tree node for the container if available.
*/
getContainerTreeNode() {
return this.model.root;
}
/**
* Render the tree widget.
* @param model the tree model.
*/
renderTree(model) {
if (model.root) {
const rows = Array.from(this.rows.values());
if (this.props.virtualized === false) {
return React.createElement(this.ScrollingRowRenderer, { rows: rows });
}
return React.createElement(TreeWidget_1.View, { ref: view => this.view = (view || undefined), width: this.node.offsetWidth, height: this.node.offsetHeight, rows: rows, renderNodeRow: this.renderNodeRow, scrollToRow: this.scrollToRow });
}
// eslint-disable-next-line no-null/no-null
return null;
}
/**
* Scroll to the selected tree node.
*/
scrollToSelected() {
if (this.props.scrollIfActive === true && !this.node.contains(document.activeElement)) {
return;
}
const focus = this.node.getElementsByClassName(widgets_1.FOCUS_CLASS)[0];
if (focus) {
domutils_1.ElementExt.scrollIntoViewIfNeeded(this.scrollArea, focus);
}
else {
const selected = this.node.getElementsByClassName(widgets_1.SELECTED_CLASS)[0];
if (selected) {
domutils_1.ElementExt.scrollIntoViewIfNeeded(this.scrollArea, selected);
}
}
}
/**
* Actually render the node row.
*/
doRenderNodeRow({ node, depth }) {
return React.createElement(React.Fragment, null,
this.renderIndent(node, { depth }),
this.renderNode(node, { depth }));
}
/**
* Render the tree node given the node properties.
* @param node the tree node.
* @param props the node properties.
*/
renderIcon(node, props) {
// eslint-disable-next-line no-null/no-null
return null;
}
/**
* Actually toggle the tree node.
* @param event the mouse click event.
*/
doToggle(event) {
const nodeId = event.currentTarget.getAttribute('data-node-id');
if (nodeId) {
const node = this.model.getNode(nodeId);
if (node && this.props.expandOnlyOnExpansionToggleClick) {
if (this.isExpandable(node) && !this.hasShiftMask(event) && !this.hasCtrlCmdMask(event)) {
this.model.toggleNodeExpansion(node);
}
}
else {
this.handleClickEvent(node, event);
}
}
event.stopPropagation();
}
/**
* Render the node expansion toggle.
* @param node the tree node.
* @param props the node properties.
*/
renderExpansionToggle(node, props) {
if (!this.isExpandable(node)) {
// eslint-disable-next-line no-null/no-null
return null;
}
const classes = [exports.TREE_NODE_SEGMENT_CLASS, widgets_1.EXPANSION_TOGGLE_CLASS];
if (!node.expanded) {
classes.push(widgets_1.COLLAPSED_CLASS);
}
if (node.busy) {
classes.push(widgets_1.BUSY_CLASS, ...widgets_1.CODICON_LOADING_CLASSES);
}
else {
classes.push(...widgets_1.CODICON_TREE_ITEM_CLASSES);
}
const className = classes.join(' ');
return React.createElement("div", { "data-node-id": node.id, className: className, onClick: this.toggle, onDoubleClick: this.handleExpansionToggleDblClickEvent });
}
/**
* Render the tree node caption given the node properties.
* @param node the tree node.
* @param props the node properties.
*/
renderCaption(node, props) {
const attrs = this.getCaptionAttributes(node, props);
const children = this.getCaptionChildren(node, props);
return React.createElement('div', attrs, children);
}
getCaptionAttributes(node, props) {
const tooltip = this.getDecorationData(node, 'tooltip').filter(objects_1.notEmpty).join(' • ');
const classes = [exports.TREE_NODE_SEGMENT_CLASS];
if (!this.hasTrailingSuffixes(node)) {
classes.push(exports.TREE_NODE_SEGMENT_GROW_CLASS);
}
const className = classes.join(' ');
let attrs = this.decorateCaption(node, {
className, id: node.id
});
if (tooltip.length > 0) {
attrs = Object.assign(Object.assign({}, attrs), { title: tooltip });
}
return attrs;
}
getCaptionChildren(node, props) {
const children = [];
const caption = this.toNodeName(node);
const highlight = this.getDecorationData(node, 'highlight')[0];
if (highlight) {
children.push(this.toReactNode(caption, highlight));
}
const searchHighlight = this.searchHighlights ? this.searchHighlights.get(node.id) : undefined;
if (searchHighlight) {
children.push(...this.toReactNode(caption, searchHighlight));
}
else if (!highlight) {
children.push(caption);
}
return children;
}
/**
* Update the node given the caption and highlight.
* @param caption the caption.
* @param highlight the tree decoration caption highlight.
*/
toReactNode(caption, highlight) {
let style = {};
if (highlight.color) {
style = Object.assign(Object.assign({}, style), { color: highlight.color });
}
if (highlight.backgroundColor) {
style = Object.assign(Object.assign({}, style), { backgroundColor: highlight.backgroundColor });
}
const createChildren = (fragment, index) => {
const { data } = fragment;
if (fragment.highlight) {
return React.createElement("mark", { className: tree_decorator_1.TreeDecoration.Styles.CAPTION_HIGHLIGHT_CLASS, style: style, key: index }, data);
}
else {
return data;
}
};
return tree_decorator_1.TreeDecoration.CaptionHighlight.split(caption, highlight).map(createChildren);
}
/**
* Decorate the tree caption.
* @param node the tree node.
* @param attrs the additional attributes.
*/
decorateCaption(node, attrs) {
const style = this.getDecorationData(node, 'fontData')
.filter(objects_1.notEmpty)
.reverse()
.map(fontData => this.applyFontStyles({}, fontData))
.reduce((acc, current) => (Object.assign(Object.assign({}, acc), current)), {});
return Object.assign(Object.assign({}, attrs), { style });
}
/**
* Determine if the tree node contains trailing suffixes.
* @param node the tree node.
*
* @returns `true` if the tree node contains trailing suffices.
*/
hasTrailingSuffixes(node) {
return this.getDecorationData(node, 'captionSuffixes').filter(objects_1.notEmpty).reduce((acc, current) => acc.concat(current), []).length > 0;
}
/**
* Apply font styles to the tree.
* @param original the original css properties.
* @param fontData the optional `fontData`.
*/
applyFontStyles(original, fontData) {
if (fontData === undefined) {
return original;
}
const modified = Object.assign({}, original); // make a copy to mutate
const { color, style } = fontData;
if (color) {
modified.color = color;
}
if (style) {
(Array.isArray(style) ? style : [style]).forEach(s => {
switch (s) {
case 'bold':
modified.fontWeight = s;
break;
case 'normal':
case 'oblique':
case 'italic':
modified.fontStyle = s;
break;
case 'underline':
case 'line-through':
modified.textDecoration = s;
break;
default:
throw new Error(`Unexpected font style: "${s}".`);
}
});
}
return modified;
}
/**
* Render caption affixes for the given tree node.
* @param node the tree node.
* @param props the node properties.
* @param affixKey the affix key.
*/
renderCaptionAffixes(node, props, affixKey) {
const suffix = affixKey === 'captionSuffixes';
const affixClass = suffix ? tree_decorator_1.TreeDecoration.Styles.CAPTION_SUFFIX_CLASS : tree_decorator_1.TreeDecoration.Styles.CAPTION_PREFIX_CLASS;
const classes = [exports.TREE_NODE_SEGMENT_CLASS, affixClass];
const affixes = this.getDecorationData(node, affixKey).filter(objects_1.notEmpty).reduce((acc, current) => acc.concat(current), []);
const children = [];
for (let i = 0; i < affixes.length; i++) {
const affix = affixes[i];
if (suffix && i === affixes.length - 1) {
classes.push(exports.TREE_NODE_SEGMENT_GROW_CLASS);
}
const style = this.applyFontStyles({}, affix.fontData);
const className = classes.join(' ');
const key = node.id + '_' + i;
const attrs = {
className,
style,
key
};
children.push(React.createElement('div', attrs, affix.data));
}
return React.createElement(React.Fragment, null, children);
}
/**
* Decorate the tree node icon.
* @param node the tree node.
* @param icon the icon.
*/
decorateIcon(node, icon) {
if (!icon) {
return;
}
const overlayIcons = [];
// if multiple overlays have the same overlay.position attribute, we'll de-duplicate those and only process the first one from the decoration array
const seenPositions = new Set();
const overlays = this.getDecorationData(node, 'iconOverlay').filter(objects_1.notEmpty);
for (const overlay of overlays) {
if (!seenPositions.has(overlay.position)) {
seenPositions.add(overlay.position);
const iconClasses = [tree_decorator_1.TreeDecoration.Styles.DECORATOR_SIZE_CLASS, tree_decorator_1.TreeDecoration.IconOverlayPosition.getStyle(overlay.position)];
const style = (color) => color === undefined ? {} : { color };
if (overlay.background) {
overlayIcons.push(React.createElement("span", { key: node.id + 'bg', className: this.getIconClass(overlay.background.shape, iconClasses), style: style(overlay.background.color) }));
}
const overlayIcon = 'icon' in overlay ? overlay.icon : overlay.iconClass;
overlayIcons.push(React.createElement("span", { key: node.id, className: this.getIconClass(overlayIcon, iconClasses), style: style(overlay.color) }));
}
}
if (overlayIcons.length > 0) {
return React.createElement("div", { className: tree_decorator_1.TreeDecoration.Styles.ICON_WRAPPER_CLASS },
icon,
overlayIcons);
}
return icon;
}
/**
* Render the tree node tail decorations.
* @param node the tree node.
* @param props the node properties.
*/
renderTailDecorations(node, props) {
const tailDecorations = this.getDecorationData(node, 'tailDecorations').reduce((acc, current) => acc.concat(current), []);
if (tailDecorations.length === 0) {
return;
}
return this.renderTailDecorationsForNode(node, props, tailDecorations);
}
renderTailDecorationsForNode(node, props, tailDecorations) {
let dotDecoration;
const otherDecorations = [];
tailDecorations.reverse().forEach(decoration => {
if (tree_decorator_1.TreeDecoration.TailDecoration.isDotDecoration(decoration)) {
dotDecoration || (dotDecoration = decoration);
}
else if (decoration.data || decoration.icon || decoration.iconClass) {
otherDecorations.push(decoration);
}
});
const decorationsToRender = dotDecoration ? [dotDecoration, ...otherDecorations] : otherDecorations;
return React.createElement(React.Fragment, null, decorationsToRender.map((decoration, index) => {
const { tooltip, data, fontData, color, icon, iconClass } = decoration;
const iconToRender = icon !== null && icon !== void 0 ? icon : iconClass;
const className = [exports.TREE_NODE_SEGMENT_CLASS, exports.TREE_NODE_TAIL_CLASS, 'flex'].join(' ');
const style = fontData ? this.applyFontStyles({}, fontData) : color ? { color } : undefined;
const content = data ? data : iconToRender
? React.createElement("span", { key: node.id + 'icon' + index, className: this.getIconClass(iconToRender, iconToRender === 'circle' ? [tree_decorator_1.TreeDecoration.Styles.DECORATOR_SIZE_CLASS] : []) })
: '';
return React.createElement("div", { key: node.id + className + index, className: className, style: style, title: tooltip },
content,
index !== decorationsToRender.length - 1 ? ',' : '');
}));
}
/**
* Determine the classes to use for an icon
* - Assumes a Font Awesome name when passed a single string, otherwise uses the passed string array
* @param iconName the icon name or list of icon names.
* @param additionalClasses additional CSS classes.
*
* @returns the icon class name.
*/
getIconClass(iconName, additionalClasses = []) {
const iconClass = (typeof iconName === 'string') ? ['a', 'fa', `fa-${iconName}`] : ['a'].concat(iconName);
return iconClass.concat(additionalClasses).join(' ');
}
/**
* Render indent for the file tree based on the depth
* @param node the tree node.
* @param depth the depth of the tree node.
*/
renderIndent(node, props) {
const renderIndentGuides = this.corePreferences['workbench.tree.renderIndentGuides'];
if (renderIndentGuides === 'none') {
return undefined;
}
const indentDivs = [];
let current = node;
let depth = props.depth;
while (current && depth) {
const classNames = [exports.TREE_NODE_INDENT_GUIDE_CLASS];
if (this.needsActiveIndentGuideline(current)) {
classNames.push('active');
}
else {
classNames.push(renderIndentGuides === 'onHover' ? 'hover' : 'always');
}
const paddingLeft = this.getDepthPadding(depth);
indentDivs.unshift(React.createElement("div", { key: depth, className: classNames.join(' '), style: {
paddingLeft: `${paddingLeft}px`
} }));
current = current.parent;
depth--;
}
return indentDivs;
}
needsActiveIndentGuideline(node) {
const parent = node.parent;
if (!parent || !this.isExpandable(parent)) {
return false;
}
if (tree_selection_1.SelectableTreeNode.isSelected(parent)) {
return true;
}
if (parent.expanded) {
for (const sibling of parent.children) {
if (tree_selection_1.SelectableTreeNode.isSelected(sibling) && !(this.isExpandable(sibling) && sibling.expanded)) {
return true;
}
}
}
return false;
}
/**
* Render the node given the tree node and node properties.
* @param node the tree node.
* @param props the node properties.
*/
renderNode(node, props) {
if (!tree_1.TreeNode.isVisible(node)) {
return undefined;
}
const attributes = this.createNodeAttributes(node, props);
const content = React.createElement("div", { className: exports.TREE_NODE_CONTENT_CLASS },
this.renderExpansionToggle(node, props),
this.decorateIcon(node, this.renderIcon(node, props)),
this.renderCaptionAffixes(node, props, 'captionPrefixes'),
this.renderCaption(node, props),
this.renderCaptionAffixes(node, props, 'captionSuffixes'),
this.renderTailDecorations(node, props));
return React.createElement('div', attributes, content);
}
/**
* Create node attributes for the tree node given the node properties.
* @param node the tree node.
* @param props the node properties.
*/
createNodeAttributes(node, props) {
const className = this.createNodeClassNames(node, props).join(' ');
const style = this.createNodeStyle(node, props);
return {
className,
style,
onClick: event => this.handleClickEvent(node, event),
onDoubleClick: event => this.handleDblClickEvent(node, event),
onContextMenu: event => this.handleContextMenuEvent(node, event),
};
}
/**
* Create the node class names.
* @param node the tree node.
* @param props the node properties.
*
* @returns the list of tree node class names.
*/
createNodeClassNames(node, props) {
const classNames = [exports.TREE_NODE_CLASS];
if (tree_1.CompositeTreeNode.is(node)) {
classNames.push(exports.COMPOSITE_TREE_NODE_CLASS);
}
if (this.isExpandable(node)) {
classNames.push(exports.EXPANDABLE_TREE_NODE_CLASS);
}
if (this.rowIsSelected(node, props)) {
classNames.push(widgets_1.SELECTED_CLASS);
}
if (this.focusService.hasFocus(node)) {
classNames.push(widgets_1.FOCUS_CLASS);
}
return classNames;
}
rowIsSelected(node, props) {
return tree_selection_1.SelectableTreeNode.isSelected(node);
}
/**
* Get the default node style.
* @param node the tree node.
* @param props the node properties.
*
* @returns the CSS properties if available.
*/
getDefaultNodeStyle(node, props) {
const paddingLeft = this.getPaddingLeft(node, props) + 'px';
return { paddingLeft };
}
getPaddingLeft(node, props) {
return this.getDepthPadding(props.depth) + (this.needsExpansionTogglePadding(node) ? this.props.expansionTogglePadding : 0);
}
/**
* If the node is a composite, a toggle will be rendered.
* Otherwise we need to add the width and the left, right padding => 18px
*/
needsExpansionTogglePadding(node) {
return !this.isExpandable(node);
}
/**
* Create the tree node style.
* @param node the tree node.
* @param props the node properties.
*/
createNodeStyle(node, props) {
return this.decorateNodeStyle(node, this.getDefaultNodeStyle(node, props));
}
/**
* Decorate the node style.
* @param node the tree node.
* @param style the optional CSS properties.
*
* @returns the CSS styles if available.
*/
decorateNodeStyle(node, style) {
const backgroundColor = this.getDecorationData(node, 'backgroundColor').filter(objects_1.notEmpty).shift();
if (backgroundColor) {
style = Object.assign(Object.assign({}, (style || {})), { backgroundColor });
}
return style;
}
/**
* Determine if the tree node is expandable.
* @param node the tree node.
*
* @returns `true` if the tree node is expandable.
*/
isExpandable(node) {
return tree_expansion_1.ExpandableTreeNode.is(node);
}
/**
* Get the tree node decorations.
* @param node the tree node.
*
* @returns the list of tree decoration data.
*/
getDecorations(node) {
const decorations = [];
if (tree_decorator_1.DecoratedTreeNode.is(node)) {
decorations.push(node.decorationData);
}
if (this.decorations.has(node.id)) {
decorations.push(...this.decorations.get(node.id));
}
return decorations.sort(tree_decorator_1.TreeDecoration.Data.comparePriority);
}
/**
* Get the tree decoration data for the given key.
* @param node the tree node.
* @param key the tree decoration data key.
*
* @returns the tree decoration data at the given key.
*/
getDecorationData(node, key) {
return this.getDecorations(node).filter(data => data[key] !== undefined).map(data => data[key]);
}
/**
* Get the scroll container.
*/
getScrollContainer() {
this.toDisposeOnDetach.push(common_1.Disposable.create(() => {
const { scrollTop, scrollLeft } = this.node;
this.lastScrollState = { scrollTop, scrollLeft };
}));
if (this.lastScrollState) {
const { scrollTop, scrollLeft } = this.lastScrollState;
this.node.scrollTop = scrollTop;
this.node.scrollLeft = scrollLeft;
}
return this.node;
}
onAfterAttach(msg) {
const up = [
keys_1.Key.ARROW_UP,
keys_1.KeyCode.createKeyCode({ first: keys_1.Key.ARROW_UP, modifiers: [keys_1.KeyModifier.Shift] })
];
const down = [
keys_1.Key.ARROW_DOWN,
keys_1.KeyCode.createKeyCode({ first: keys_1.Key.ARROW_DOWN, modifiers: [keys_1.KeyModifier.Shift] })
];
if (this.props.search) {
if (this.searchBox.isAttached) {
widgets_1.Widget.detach(this.searchBox);
}
widgets_1.UnsafeWidgetUtilities.attach(this.searchBox, this.node.parentElement);
this.addKeyListener(this.node, this.searchBox.keyCodePredicate.bind(this.searchBox), this.searchBox.handle.bind(this.searchBox));
this.toDisposeOnDetach.push(common_1.Disposable.create(() => {
widgets_1.Widget.detach(this.searchBox);
}));
}
super.onAfterAttach(msg);
this.addKeyListener(this.node, keys_1.Key.ARROW_LEFT, event => this.handleLeft(event));
this.addKeyListener(this.node, keys_1.Key.ARROW_RIGHT, event => this.handleRight(event));
this.addKeyListener(this.node, up, event => this.handleUp(event));
this.addKeyListener(this.node, down, event => this.handleDown(event));
this.addKeyListener(this.node, keys_1.Key.ENTER, event => this.handleEnter(event));
this.addKeyListener(this.node, keys_1.Key.SPACE, event => this.handleSpace(event));
this.addKeyListener(this.node, keys_1.Key.ESCAPE, event => this.handleEscape(event));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.addEventListener(this.node, 'ps-scroll-y', (e) => {
if (this.view && this.view.list) {
const { scrollTop } = e.target;
this.view.list.scrollTo({
top: scrollTop
});
}
});
}
/**
* Handle the `left arrow` keyboard event.
* @param event the `left arrow` keyboard event.
*/
async handleLeft(event) {
if (!!this.props.multiSelect && (this.hasCtrlCmdMask(event) || this.hasShiftMask(event))) {
return;
}
if (!await this.model.collapseNode()) {
this.model.selectParent();
}
}
/**
* Handle the `right arrow` keyboard event.
* @param event the `right arrow` keyboard event.
*/
async handleRight(event) {
if (!!this.props.multiSelect && (this.hasCtrlCmdMask(event) || this.hasShiftMask(event))) {
return;
}
if (!await this.model.expandNode()) {
this.model.selectNextNode();
}
}
/**
* Handle the `up arrow` keyboard event.
* @param event the `up arrow` keyboard event.
*/
handleUp(event) {
if (!!this.props.multiSelect && this.hasShiftMask(event)) {
this.model.selectPrevNode(tree_selection_1.TreeSelection.SelectionType.RANGE);
}
else {
this.model.selectPrevNode();
}
this.node.focus();
}
/**
* Handle the `down arrow` keyboard event.
* @param event the `down arrow` keyboard event.
*/
handleDown(event) {
if (!!this.props.multiSelect && this.hasShiftMask(event)) {
this.model.selectNextNode(tree_selection_1.TreeSelection.SelectionType.RANGE);
}
else {
this.model.selectNextNode();
}
this.node.focus();
}
/**
* Handle the `enter key` keyboard event.
* - `enter` opens the tree node.
* @param event the `enter key` keyboard event.
*/
handleEnter(event) {
this.model.openNode();
}
/**
* Handle the `space key` keyboard event.
* - By default should be similar to a single-click action.
* @param event the `space key` keyboard event.
*/
handleSpace(event) {
const { focusedNode } = this.focusService;
if (!this.props.multiSelect || (!event.ctrlKey && !event.metaKey && !event.shiftKey)) {
this.tapNode(focusedNode);
}
}
handleEscape(event) {
if (this.model.selectedNodes.length <= 1) {
this.focusService.setFocus(undefined);
this.node.focus();
}
this.model.clearSelection();
}
/**
* Handle the single-click mouse event.
* @param node the tree node if available.
* @param event the mouse single-click event.
*/
handleClickEvent(node, event) {
if (node) {
event.stopPropagation();
const shiftMask = this.hasShiftMask(event);
const ctrlCmdMask = this.hasCtrlCmdMask(event);
if (this.props.multiSelect && (shiftMask || ctrlCmdMask) && tree_selection_1.SelectableTreeNode.is(node)) {
if (shiftMask) {
this.model.selectRange(node);
}
else if (ctrlCmdMask) {
this.model.toggleNode(node);
}
}
else {
this.tapNode(node);
}
}
}
/**
* The effective handler of an unmodified single-click event.
*/
tapNode(node) {
if (tree_selection_1.SelectableTreeNode.is(node)) {
this.model.selectNode(node);
}
if (node && !this.props.expandOnlyOnExpansionToggleClick && this.isExpandable(node)) {
this.model.toggleNodeExpansion(node);
}
}
/**
* Handle the double-click mouse event.
* @param node the tree node if available.
* @param event the double-click mouse event.
*/
handleDblClickEvent(node, event) {
this.model.openNode(node);
event.stopPropagation();
}
/**
* Handle the context menu click event.
* - The context menu click event is triggered by the right-click.
* @param node the tree node if available.
* @param event the right-click mouse event.
*/
handleContextMenuEvent(node, event) {
if (tree_selection_1.SelectableTreeNode.is(node)) {
// Keep the selection for the context menu, if the widget support multi-selection and the right click happens on an already selected node.
if (!this.props.multiSelect || !node.selected) {
const type = !!this.props.multiSelect && this.hasCtrlCmdMask(event) ? tree_selection_1.TreeSelection.SelectionType.TOGGLE : tree_selection_1.TreeSelection.SelectionType.DEFAULT;
this.model.addSelection({ node, type });
}
this.focusService.setFocus(node);
const contextMenuPath = this.props.contextMenuPath;
if (contextMenuPath) {
const { x, y } = event.nativeEvent;
const args = this.toContextMenuArgs(node);
setTimeout(() => this.contextMenuRenderer.render({
menuPath: contextMenuPath,
anchor: { x, y },
args
}), 10);
}
}
event.stopPropagation();
event.preventDefault();
}
/**
* Actually handle the double-click mouse event on the expansion toggle.
* @param event the double-click mouse event.
*/
doHandleExpansionToggleDblClickEvent(event) {
if (this.props.expandOnlyOnExpansionToggleClick) {
// Ignore the double-click event.
event.stopPropagation();
}
}
/**
* Convert the tree node to context menu arguments.
* @param node the selectable tree node.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
toContextMenuArgs(node) {
return undefined;
}
/**
* Determine if the tree modifier aware event has a `ctrlcmd` mask.
* @param event the tree modifier aware event.
*
* @returns `true` if the tree modifier aware event contains the `ctrlcmd` mask.
*/
hasCtrlCmdMask(event) {
return os_1.isOSX ? event.metaKey : event.ctrlKey;
}
/**
* Determine if the tree modifier aware event has a `shift` mask.
* @param event the tree modifier aware event.
*
* @returns `true` if the tree modifier aware event contains the `shift` mask.
*/
hasShiftMask(event) {
// Ctrl/Cmd mask overrules the Shift mask.
if (this.hasCtrlCmdMask(event)) {
return false;
}
return event.shiftKey;
}
/**
* Deflate the tree node for storage.
* @param node the tree node.
*/
deflateForStorage(node) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const copy = Object.assign({}, node);
if (copy.parent) {
delete copy.parent;
}
if ('previousSibling' in copy) {
delete copy.previousSibling;
}
if ('nextSibling' in copy) {
delete copy.nextSibling;
}
if ('busy' in copy) {
delete copy.busy;
}
if (tree_1.CompositeTreeNode.is(node)) {
copy.children = [];
for (const child of node.children) {
copy.children.push(this.deflateForStorage(child));
}
}
return copy;
}
/**
* Inflate the tree node from storage.
* @param node the tree node.
* @param parent the optional tree node.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
inflateFromStorage(node, parent) {
if (node.selected) {
node.selected = false;
}
if (parent) {
node.parent = parent;
}
if (Array.isArray(node.children)) {
for (const child of node.children) {
this.inflateFromStorage(child, node);
}
}
return node;
}
/**
* Store the tree state.
*/
storeState() {
var _a;
const decorations = this.decoratorService.deflateDecorators(this.decorations);
let state = {
decorations
};
if (this.model.root) {
state = Object.assign(Object.assign({}, state), { root: this.deflateForStorage(this.model.root), model: this.model.storeState(), focusedNodeId: (_a = this.focusService.focusedNode) === null || _a === void 0 ? void 0 : _a.id });
}
return state;
}
/**
* Restore the state.
* @param oldState the old state object.
*/
restoreState(oldState) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { root, decorations, model, focusedNodeId } = oldState;
if (root) {
this.model.root = this.inflateFromStorage(root);
}
if (decorations) {
this.decorations = this.decoratorService.inflateDecorators(decorations);
}
if (model) {
this.model.restoreState(model);
}
if (focusedNodeId) {
const candidate = this.model.getNode(focusedNodeId);
if (tree_selection_1.SelectableTreeNode.is(candidate)) {
this.focusService.setFocus(candidate);
}
}
}
toNodeIcon(node) {
return this.labelProvider.getIcon(node);
}
toNodeName(node) {
return this.labelProvider.getName(node);
}
toNodeDescription(node) {
return this.labelProvider.getLongName(node);
}
getDepthPadding(depth) {
return depth * this.props.leftPadding;
}
};
__decorate([
(0, inversify_1.inject)(tree_decorator_1.TreeDecoratorService),
__metadata("design:type", Object)
], TreeWidget.prototype, "decoratorService", void 0);
__decorate([
(0, inversify_1.inject)(tree_search_1.TreeSearch),
__metadata("design:type", tree_search_1.TreeSearch)
], TreeWidget.prototype, "treeSearch", void 0);
__decorate([
(0, inversify_1.inject)(search_box_1.SearchBoxFactory),
__metadata("design:type", Function)
], TreeWidget.prototype, "searchBoxFactory", void 0);
__decorate([
(0, inversify_1.inject)(tree_focus_service_1.TreeFocusService),
__metadata("design:type", Object)
], TreeWidget.prototype, "focusService", void 0);
__decorate([
(0, inversify_1.inject)(common_1.SelectionService),
__metadata("design:type", common_1.SelectionService)
], TreeWidget.prototype, "selectionService", void 0);
__decorate([
(0, inversify_1.inject)(label_provider_1.LabelProvider),
__metadata("design:type", label_provider_1.LabelProvider)
], TreeWidget.prototype, "labelProvider", void 0);
__decorate([
(0, inversify_1.inject)(core_preferences_1.CorePreferences),
__metadata("design:type", Object)
], TreeWidget.prototype, "corePreferences", void 0);
__decorate([
(0, inversify_1.postConstruct)(),
__metadata("design:type", Function),
__metadata("design:paramtypes", []),
__metadata("design:returntype", void 0)
], TreeWidget.prototype, "init", null);
TreeWidget = TreeWidget_1 = __decorate([
(0, inversify_1.injectable)(),
__param(0, (0, inversify_1.inject)(exports.TreeProps)),
__param(1, (0, inversify_1.inject)(tree_model_1.TreeModel)),
__param(2, (0, inversify_1.inject)(context_menu_renderer_1.ContextMenuRenderer)),
__metadata("design:paramtypes", [Object, Object, context_menu_renderer_1.ContextMenuRenderer])
], TreeWidget);
exports.TreeWidget = TreeWidget;
(function (TreeWidget) {
class View extends React.Component {
render() {
const { rows, width, height, scrollToRow } = this.props;
return React.createElement(react_virtuoso_1.Virtuoso, { ref: list => {
this.list = (list || undefined);
if (this.list && scrollToRow !== undefined) {
this.list.scrollIntoView({
index: scrollToRow,
align: 'center'
});
}
}, totalCount: rows.length, itemContent: index => this.props.renderNodeRow(rows[index]), width: width, h