src/vs/base/parts/tree/browser/treeView.ts (1,292 lines of code) (raw):
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as Platform from 'vs/base/common/platform';
import * as Browser from 'vs/base/browser/browser';
import * as Lifecycle from 'vs/base/common/lifecycle';
import * as DOM from 'vs/base/browser/dom';
import * as Diff from 'vs/base/common/diff/diff';
import * as Touch from 'vs/base/browser/touch';
import * as strings from 'vs/base/common/strings';
import * as Mouse from 'vs/base/browser/mouseEvent';
import * as Keyboard from 'vs/base/browser/keyboardEvent';
import * as Model from 'vs/base/parts/tree/browser/treeModel';
import * as dnd from './treeDnd';
import { ArrayIterator, MappedIterator } from 'vs/base/common/iterator';
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { HeightMap, IViewItem } from 'vs/base/parts/tree/browser/treeViewModel';
import * as _ from 'vs/base/parts/tree/browser/tree';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Event, Emitter } from 'vs/base/common/event';
import { DataTransfers, StaticDND, IDragAndDropData } from 'vs/base/browser/dnd';
import { DefaultTreestyler } from './treeDefaults';
import { Delayer, timeout } from 'vs/base/common/async';
export interface IRow {
element: HTMLElement | null;
templateId: string;
templateData: any;
}
function removeFromParent(element: HTMLElement): void {
try {
element.parentElement!.removeChild(element);
} catch (e) {
// this will throw if this happens due to a blur event, nasty business
}
}
export class RowCache implements Lifecycle.IDisposable {
private _cache: { [templateId: string]: IRow[]; } | null;
constructor(private context: _.ITreeContext) {
this._cache = { '': [] };
}
public alloc(templateId: string): IRow {
let result = this.cache(templateId).pop();
if (!result) {
let content = document.createElement('div');
content.className = 'content';
let row = document.createElement('div');
row.appendChild(content);
let templateData: any = null;
try {
templateData = this.context.renderer!.renderTemplate(this.context.tree, templateId, content);
} catch (err) {
console.error('Tree usage error: exception while rendering template');
console.error(err);
}
result = {
element: row,
templateId: templateId,
templateData
};
}
return result;
}
public release(templateId: string, row: IRow): void {
removeFromParent(row.element!);
this.cache(templateId).push(row);
}
private cache(templateId: string): IRow[] {
return this._cache![templateId] || (this._cache![templateId] = []);
}
public garbageCollect(): void {
if (this._cache) {
Object.keys(this._cache).forEach(templateId => {
this._cache![templateId].forEach(cachedRow => {
this.context.renderer!.disposeTemplate(this.context.tree, templateId, cachedRow.templateData);
cachedRow.element = null;
cachedRow.templateData = null;
});
delete this._cache![templateId];
});
}
}
public dispose(): void {
this.garbageCollect();
this._cache = null;
}
}
export interface IViewContext extends _.ITreeContext {
cache: RowCache;
horizontalScrolling: boolean;
}
export class ViewItem implements IViewItem {
private context: IViewContext;
public model: Model.Item;
public id: string;
protected row: IRow | null;
public top: number;
public height: number;
public width: number = 0;
public onDragStart: (e: DragEvent) => void;
public needsRender: boolean;
public uri: string | null;
public unbindDragStart: Lifecycle.IDisposable = Lifecycle.Disposable.None;
public loadingTimer: any;
public _styles: any;
private _draggable: boolean;
constructor(context: IViewContext, model: Model.Item) {
this.context = context;
this.model = model;
this.id = this.model.id;
this.row = null;
this.top = 0;
this.height = model.getHeight();
this._styles = {};
model.getAllTraits().forEach(t => this._styles[t] = true);
if (model.isExpanded()) {
this.addClass('expanded');
}
}
set expanded(value: boolean) {
value ? this.addClass('expanded') : this.removeClass('expanded');
}
set loading(value: boolean) {
value ? this.addClass('loading') : this.removeClass('loading');
}
set draggable(value: boolean) {
this._draggable = value;
this.render(true);
}
get draggable() {
return this._draggable;
}
set dropTarget(value: boolean) {
value ? this.addClass('drop-target') : this.removeClass('drop-target');
}
public get element(): HTMLElement {
return (this.row && this.row.element)!;
}
private _templateId: string;
private get templateId(): string {
return this._templateId || (this._templateId = (this.context.renderer!.getTemplateId && this.context.renderer!.getTemplateId(this.context.tree, this.model.getElement())));
}
public addClass(name: string): void {
this._styles[name] = true;
this.render(true);
}
public removeClass(name: string): void {
delete this._styles[name]; // is this slow?
this.render(true);
}
public render(skipUserRender = false): void {
if (!this.model || !this.element) {
return;
}
let classes = ['monaco-tree-row'];
classes.push.apply(classes, Object.keys(this._styles));
if (this.model.hasChildren()) {
classes.push('has-children');
}
this.element.className = classes.join(' ');
this.element.draggable = this.draggable;
this.element.style.height = this.height + 'px';
// ARIA
this.element.setAttribute('role', 'treeitem');
const accessibility = this.context.accessibilityProvider!;
const ariaLabel = accessibility.getAriaLabel(this.context.tree, this.model.getElement());
if (ariaLabel) {
this.element.setAttribute('aria-label', ariaLabel);
}
if (accessibility.getPosInSet && accessibility.getSetSize) {
this.element.setAttribute('aria-setsize', accessibility.getSetSize());
this.element.setAttribute('aria-posinset', accessibility.getPosInSet(this.context.tree, this.model.getElement()));
}
if (this.model.hasTrait('focused')) {
const base64Id = strings.safeBtoa(this.model.id);
this.element.setAttribute('aria-selected', 'true');
this.element.setAttribute('id', base64Id);
} else {
this.element.setAttribute('aria-selected', 'false');
this.element.removeAttribute('id');
}
if (this.model.hasChildren()) {
this.element.setAttribute('aria-expanded', String(!!this._styles['expanded']));
} else {
this.element.removeAttribute('aria-expanded');
}
this.element.setAttribute('aria-level', String(this.model.getDepth()));
if (this.context.options.paddingOnRow) {
this.element.style.paddingLeft = this.context.options.twistiePixels! + ((this.model.getDepth() - 1) * this.context.options.indentPixels!) + 'px';
} else {
this.element.style.paddingLeft = ((this.model.getDepth() - 1) * this.context.options.indentPixels!) + 'px';
(<HTMLElement>this.row!.element!.firstElementChild).style.paddingLeft = this.context.options.twistiePixels + 'px';
}
let uri = this.context.dnd!.getDragURI(this.context.tree, this.model.getElement());
if (uri !== this.uri) {
if (this.unbindDragStart) {
this.unbindDragStart.dispose();
}
if (uri) {
this.uri = uri;
this.draggable = true;
this.unbindDragStart = DOM.addDisposableListener(this.element, 'dragstart', (e) => {
this.onDragStart(e);
});
} else {
this.uri = null;
}
}
if (!skipUserRender && this.element) {
let paddingLeft: number = 0;
if (this.context.horizontalScrolling) {
const style = window.getComputedStyle(this.element);
paddingLeft = parseFloat(style.paddingLeft!);
}
if (this.context.horizontalScrolling) {
this.element.style.width = 'fit-content';
}
try {
this.context.renderer!.renderElement(this.context.tree, this.model.getElement(), this.templateId, this.row!.templateData);
} catch (err) {
console.error('Tree usage error: exception while rendering element');
console.error(err);
}
if (this.context.horizontalScrolling) {
this.width = DOM.getContentWidth(this.element) + paddingLeft;
this.element.style.width = '';
}
}
}
updateWidth(): any {
if (!this.context.horizontalScrolling || !this.element) {
return;
}
const style = window.getComputedStyle(this.element);
const paddingLeft = parseFloat(style.paddingLeft!);
this.element.style.width = 'fit-content';
this.width = DOM.getContentWidth(this.element) + paddingLeft;
this.element.style.width = '';
}
public insertInDOM(container: HTMLElement, afterElement: HTMLElement | null): void {
if (!this.row) {
this.row = this.context.cache.alloc(this.templateId);
// used in reverse lookup from HTMLElement to Item
(<any>this.element)[TreeView.BINDING] = this;
}
if (this.element.parentElement) {
return;
}
if (afterElement === null) {
container.appendChild(this.element);
} else {
try {
container.insertBefore(this.element, afterElement);
} catch (e) {
console.warn('Failed to locate previous tree element');
container.appendChild(this.element);
}
}
this.render();
}
public removeFromDOM(): void {
if (!this.row) {
return;
}
this.unbindDragStart.dispose();
this.uri = null;
(<any>this.element)[TreeView.BINDING] = null;
this.context.cache.release(this.templateId, this.row);
this.row = null;
}
public dispose(): void {
this.row = null;
}
}
class RootViewItem extends ViewItem {
constructor(context: IViewContext, model: Model.Item, wrapper: HTMLElement) {
super(context, model);
this.row = {
element: wrapper,
templateData: null,
templateId: null!
};
}
public render(): void {
if (!this.model || !this.element) {
return;
}
let classes = ['monaco-tree-wrapper'];
classes.push.apply(classes, Object.keys(this._styles));
if (this.model.hasChildren()) {
classes.push('has-children');
}
this.element.className = classes.join(' ');
}
public insertInDOM(container: HTMLElement, afterElement: HTMLElement): void {
// noop
}
public removeFromDOM(): void {
// noop
}
}
interface IThrottledGestureEvent {
translationX: number;
translationY: number;
}
function reactionEquals(one: _.IDragOverReaction, other: _.IDragOverReaction | null): boolean {
if (!one && !other) {
return true;
} else if (!one || !other) {
return false;
} else if (one.accept !== other.accept) {
return false;
} else if (one.bubble !== other.bubble) {
return false;
} else if (one.effect !== other.effect) {
return false;
} else {
return true;
}
}
export class TreeView extends HeightMap {
static BINDING = 'monaco-tree-row';
static LOADING_DECORATION_DELAY = 800;
private static counter: number = 0;
private instance: number;
private context: IViewContext;
private modelListeners: Lifecycle.IDisposable[];
private model: Model.TreeModel | null = null;
private viewListeners: Lifecycle.IDisposable[];
private domNode: HTMLElement;
private wrapper: HTMLElement;
private styleElement: HTMLStyleElement;
private treeStyler: _.ITreeStyler;
private rowsContainer: HTMLElement;
private scrollableElement: ScrollableElement;
private msGesture: MSGesture;
private lastPointerType: string;
private lastClickTimeStamp: number = 0;
private horizontalScrolling: boolean;
private contentWidthUpdateDelayer = new Delayer<void>(50);
private lastRenderTop: number;
private lastRenderHeight: number;
private inputItem: ViewItem;
private items: { [id: string]: ViewItem; };
private isRefreshing = false;
private refreshingPreviousChildrenIds: { [id: string]: string[] } = {};
private currentDragAndDropData: IDragAndDropData | null = null;
private currentDropElement: any;
private currentDropElementReaction: _.IDragOverReaction;
private currentDropTarget: ViewItem | null = null;
private shouldInvalidateDropReaction: boolean;
private currentDropTargets: ViewItem[] | null = null;
private currentDropDisposable: Lifecycle.IDisposable = Lifecycle.Disposable.None;
private dragAndDropScrollInterval: number | null = null;
private dragAndDropScrollTimeout: number | null = null;
private dragAndDropMouseY: number | null = null;
private didJustPressContextMenuKey: boolean;
private highlightedItemWasDraggable: boolean;
private onHiddenScrollTop: number | null = null;
private readonly _onDOMFocus = new Emitter<void>();
readonly onDOMFocus: Event<void> = this._onDOMFocus.event;
private readonly _onDOMBlur = new Emitter<void>();
readonly onDOMBlur: Event<void> = this._onDOMBlur.event;
private readonly _onDidScroll = new Emitter<void>();
readonly onDidScroll: Event<void> = this._onDidScroll.event;
constructor(context: _.ITreeContext, container: HTMLElement) {
super();
TreeView.counter++;
this.instance = TreeView.counter;
const horizontalScrollMode = typeof context.options.horizontalScrollMode === 'undefined' ? ScrollbarVisibility.Hidden : context.options.horizontalScrollMode;
this.horizontalScrolling = horizontalScrollMode !== ScrollbarVisibility.Hidden;
this.context = {
dataSource: context.dataSource,
renderer: context.renderer,
controller: context.controller,
dnd: context.dnd,
filter: context.filter,
sorter: context.sorter,
tree: context.tree,
accessibilityProvider: context.accessibilityProvider,
options: context.options,
cache: new RowCache(context),
horizontalScrolling: this.horizontalScrolling
};
this.modelListeners = [];
this.viewListeners = [];
this.items = {};
this.domNode = document.createElement('div');
this.domNode.className = `monaco-tree no-focused-item monaco-tree-instance-${this.instance}`;
// to allow direct tabbing into the tree instead of first focusing the tree
this.domNode.tabIndex = context.options.preventRootFocus ? -1 : 0;
this.styleElement = DOM.createStyleSheet(this.domNode);
this.treeStyler = context.styler || new DefaultTreestyler(this.styleElement, `monaco-tree-instance-${this.instance}`);
// ARIA
this.domNode.setAttribute('role', 'tree');
if (this.context.options.ariaLabel) {
this.domNode.setAttribute('aria-label', this.context.options.ariaLabel);
}
if (this.context.options.alwaysFocused) {
DOM.addClass(this.domNode, 'focused');
}
if (!this.context.options.paddingOnRow) {
DOM.addClass(this.domNode, 'no-row-padding');
}
this.wrapper = document.createElement('div');
this.wrapper.className = 'monaco-tree-wrapper';
this.scrollableElement = new ScrollableElement(this.wrapper, {
alwaysConsumeMouseWheel: true,
horizontal: horizontalScrollMode,
vertical: (typeof context.options.verticalScrollMode !== 'undefined' ? context.options.verticalScrollMode : ScrollbarVisibility.Auto),
useShadows: context.options.useShadows
});
this.scrollableElement.onScroll((e) => {
this.render(e.scrollTop, e.height, e.scrollLeft, e.width, e.scrollWidth);
this._onDidScroll.fire();
});
if (Browser.isIE) {
this.wrapper.style.msTouchAction = 'none';
this.wrapper.style.msContentZooming = 'none';
} else {
Touch.Gesture.addTarget(this.wrapper);
}
this.rowsContainer = document.createElement('div');
this.rowsContainer.className = 'monaco-tree-rows';
if (context.options.showTwistie) {
this.rowsContainer.className += ' show-twisties';
}
let focusTracker = DOM.trackFocus(this.domNode);
this.viewListeners.push(focusTracker.onDidFocus(() => this.onFocus()));
this.viewListeners.push(focusTracker.onDidBlur(() => this.onBlur()));
this.viewListeners.push(focusTracker);
this.viewListeners.push(DOM.addDisposableListener(this.domNode, 'keydown', (e) => this.onKeyDown(e)));
this.viewListeners.push(DOM.addDisposableListener(this.domNode, 'keyup', (e) => this.onKeyUp(e)));
this.viewListeners.push(DOM.addDisposableListener(this.domNode, 'mousedown', (e) => this.onMouseDown(e)));
this.viewListeners.push(DOM.addDisposableListener(this.domNode, 'mouseup', (e) => this.onMouseUp(e)));
this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'auxclick', (e: MouseEvent) => {
if (e && e.button === 1) {
this.onMouseMiddleClick(e);
}
}));
this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'click', (e) => this.onClick(e)));
this.viewListeners.push(DOM.addDisposableListener(this.domNode, 'contextmenu', (e) => this.onContextMenu(e)));
this.viewListeners.push(DOM.addDisposableListener(this.wrapper, Touch.EventType.Tap, (e) => this.onTap(e)));
this.viewListeners.push(DOM.addDisposableListener(this.wrapper, Touch.EventType.Change, (e) => this.onTouchChange(e)));
if (Browser.isIE) {
this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'MSPointerDown', (e) => this.onMsPointerDown(e)));
this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'MSGestureTap', (e) => this.onMsGestureTap(e)));
// these events come too fast, we throttle them
this.viewListeners.push(DOM.addDisposableThrottledListener<IThrottledGestureEvent>(this.wrapper, 'MSGestureChange', (e) => this.onThrottledMsGestureChange(e), (lastEvent: IThrottledGestureEvent, event: MSGestureEvent): IThrottledGestureEvent => {
event.stopPropagation();
event.preventDefault();
let result = { translationY: event.translationY, translationX: event.translationX };
if (lastEvent) {
result.translationY += lastEvent.translationY;
result.translationX += lastEvent.translationX;
}
return result;
}));
}
this.viewListeners.push(DOM.addDisposableListener(window, 'dragover', (e) => this.onDragOver(e)));
this.viewListeners.push(DOM.addDisposableListener(this.wrapper, 'drop', (e) => this.onDrop(e)));
this.viewListeners.push(DOM.addDisposableListener(window, 'dragend', (e) => this.onDragEnd(e)));
this.viewListeners.push(DOM.addDisposableListener(window, 'dragleave', (e) => this.onDragOver(e)));
this.wrapper.appendChild(this.rowsContainer);
this.domNode.appendChild(this.scrollableElement.getDomNode());
container.appendChild(this.domNode);
this.lastRenderTop = 0;
this.lastRenderHeight = 0;
this.didJustPressContextMenuKey = false;
this.currentDropTarget = null;
this.currentDropTargets = [];
this.shouldInvalidateDropReaction = false;
this.dragAndDropScrollInterval = null;
this.dragAndDropScrollTimeout = null;
this.onRowsChanged();
this.layout();
this.setupMSGesture();
this.applyStyles(context.options);
}
public applyStyles(styles: _.ITreeStyles): void {
this.treeStyler.style(styles);
}
protected createViewItem(item: Model.Item): IViewItem {
return new ViewItem(this.context, item);
}
public getHTMLElement(): HTMLElement {
return this.domNode;
}
public focus(): void {
this.domNode.focus();
}
public isFocused(): boolean {
return document.activeElement === this.domNode;
}
public blur(): void {
this.domNode.blur();
}
public onVisible(): void {
this.scrollTop = this.onHiddenScrollTop!;
this.onHiddenScrollTop = null;
this.setupMSGesture();
}
private setupMSGesture(): void {
if ((<any>window).MSGesture) {
this.msGesture = new MSGesture();
setTimeout(() => this.msGesture.target = this.wrapper, 100); // TODO@joh, TODO@IETeam
}
}
public onHidden(): void {
this.onHiddenScrollTop = this.scrollTop;
}
private isTreeVisible(): boolean {
return this.onHiddenScrollTop === null;
}
public layout(height?: number, width?: number): void {
if (!this.isTreeVisible()) {
return;
}
this.viewHeight = height || DOM.getContentHeight(this.wrapper); // render
this.scrollHeight = this.getContentHeight();
if (this.horizontalScrolling) {
this.viewWidth = width || DOM.getContentWidth(this.wrapper);
}
}
private render(scrollTop: number, viewHeight: number, scrollLeft: number, viewWidth: number, scrollWidth: number): void {
let i: number;
let stop: number;
let renderTop = scrollTop;
let renderBottom = scrollTop + viewHeight;
let thisRenderBottom = this.lastRenderTop + this.lastRenderHeight;
// when view scrolls down, start rendering from the renderBottom
for (i = this.indexAfter(renderBottom) - 1, stop = this.indexAt(Math.max(thisRenderBottom, renderTop)); i >= stop; i--) {
this.insertItemInDOM(<ViewItem>this.itemAtIndex(i));
}
// when view scrolls up, start rendering from either this.renderTop or renderBottom
for (i = Math.min(this.indexAt(this.lastRenderTop), this.indexAfter(renderBottom)) - 1, stop = this.indexAt(renderTop); i >= stop; i--) {
this.insertItemInDOM(<ViewItem>this.itemAtIndex(i));
}
// when view scrolls down, start unrendering from renderTop
for (i = this.indexAt(this.lastRenderTop), stop = Math.min(this.indexAt(renderTop), this.indexAfter(thisRenderBottom)); i < stop; i++) {
this.removeItemFromDOM(<ViewItem>this.itemAtIndex(i));
}
// when view scrolls up, start unrendering from either renderBottom this.renderTop
for (i = Math.max(this.indexAfter(renderBottom), this.indexAt(this.lastRenderTop)), stop = this.indexAfter(thisRenderBottom); i < stop; i++) {
this.removeItemFromDOM(<ViewItem>this.itemAtIndex(i));
}
let topItem = this.itemAtIndex(this.indexAt(renderTop));
if (topItem) {
this.rowsContainer.style.top = (topItem.top - renderTop) + 'px';
}
if (this.horizontalScrolling) {
this.rowsContainer.style.left = -scrollLeft + 'px';
this.rowsContainer.style.width = `${Math.max(scrollWidth, viewWidth)}px`;
}
this.lastRenderTop = renderTop;
this.lastRenderHeight = renderBottom - renderTop;
}
public setModel(newModel: Model.TreeModel): void {
this.releaseModel();
this.model = newModel;
this.model.onRefresh(this.onRefreshing, this, this.modelListeners);
this.model.onDidRefresh(this.onRefreshed, this, this.modelListeners);
this.model.onSetInput(this.onClearingInput, this, this.modelListeners);
this.model.onDidSetInput(this.onSetInput, this, this.modelListeners);
this.model.onDidFocus(this.onModelFocusChange, this, this.modelListeners);
this.model.onRefreshItemChildren(this.onItemChildrenRefreshing, this, this.modelListeners);
this.model.onDidRefreshItemChildren(this.onItemChildrenRefreshed, this, this.modelListeners);
this.model.onDidRefreshItem(this.onItemRefresh, this, this.modelListeners);
this.model.onExpandItem(this.onItemExpanding, this, this.modelListeners);
this.model.onDidExpandItem(this.onItemExpanded, this, this.modelListeners);
this.model.onCollapseItem(this.onItemCollapsing, this, this.modelListeners);
this.model.onDidRevealItem(this.onItemReveal, this, this.modelListeners);
this.model.onDidAddTraitItem(this.onItemAddTrait, this, this.modelListeners);
this.model.onDidRemoveTraitItem(this.onItemRemoveTrait, this, this.modelListeners);
}
private onRefreshing(): void {
this.isRefreshing = true;
}
private onRefreshed(): void {
this.isRefreshing = false;
this.onRowsChanged();
}
private onRowsChanged(scrollTop: number = this.scrollTop): void {
if (this.isRefreshing) {
return;
}
this.scrollTop = scrollTop;
this.updateScrollWidth();
}
private updateScrollWidth(): void {
if (!this.horizontalScrolling) {
return;
}
this.contentWidthUpdateDelayer.trigger(() => {
const keys = Object.keys(this.items);
let scrollWidth = 0;
for (const key of keys) {
scrollWidth = Math.max(scrollWidth, this.items[key].width);
}
this.scrollWidth = scrollWidth + 10 /* scrollbar */;
});
}
public focusNextPage(eventPayload?: any): void {
let lastPageIndex = this.indexAt(this.scrollTop + this.viewHeight);
lastPageIndex = lastPageIndex === 0 ? 0 : lastPageIndex - 1;
let lastPageElement = this.itemAtIndex(lastPageIndex).model.getElement();
let currentlyFocusedElement = this.model!.getFocus();
if (currentlyFocusedElement !== lastPageElement) {
this.model!.setFocus(lastPageElement, eventPayload);
} else {
let previousScrollTop = this.scrollTop;
this.scrollTop += this.viewHeight;
if (this.scrollTop !== previousScrollTop) {
// Let the scroll event listener run
setTimeout(() => {
this.focusNextPage(eventPayload);
}, 0);
}
}
}
public focusPreviousPage(eventPayload?: any): void {
let firstPageIndex: number;
if (this.scrollTop === 0) {
firstPageIndex = this.indexAt(this.scrollTop);
} else {
firstPageIndex = this.indexAfter(this.scrollTop - 1);
}
let firstPageElement = this.itemAtIndex(firstPageIndex).model.getElement();
let currentlyFocusedElement = this.model!.getFocus();
if (currentlyFocusedElement !== firstPageElement) {
this.model!.setFocus(firstPageElement, eventPayload);
} else {
let previousScrollTop = this.scrollTop;
this.scrollTop -= this.viewHeight;
if (this.scrollTop !== previousScrollTop) {
// Let the scroll event listener run
setTimeout(() => {
this.focusPreviousPage(eventPayload);
}, 0);
}
}
}
public get viewHeight() {
const scrollDimensions = this.scrollableElement.getScrollDimensions();
return scrollDimensions.height;
}
public set viewHeight(height: number) {
this.scrollableElement.setScrollDimensions({ height });
}
private set scrollHeight(scrollHeight: number) {
scrollHeight = scrollHeight + (this.horizontalScrolling ? 10 : 0);
this.scrollableElement.setScrollDimensions({ scrollHeight });
}
public get viewWidth(): number {
const scrollDimensions = this.scrollableElement.getScrollDimensions();
return scrollDimensions.width;
}
public set viewWidth(viewWidth: number) {
this.scrollableElement.setScrollDimensions({ width: viewWidth });
}
private set scrollWidth(scrollWidth: number) {
this.scrollableElement.setScrollDimensions({ scrollWidth });
}
public get scrollTop(): number {
const scrollPosition = this.scrollableElement.getScrollPosition();
return scrollPosition.scrollTop;
}
public set scrollTop(scrollTop: number) {
const scrollHeight = this.getContentHeight() + (this.horizontalScrolling ? 10 : 0);
this.scrollableElement.setScrollDimensions({ scrollHeight });
this.scrollableElement.setScrollPosition({ scrollTop });
}
public getScrollPosition(): number {
const height = this.getContentHeight() - this.viewHeight;
return height <= 0 ? 1 : this.scrollTop / height;
}
public setScrollPosition(pos: number): void {
const height = this.getContentHeight() - this.viewHeight;
this.scrollTop = height * pos;
}
// Events
private onClearingInput(e: Model.IInputEvent): void {
let item = <Model.Item>e.item;
if (item) {
this.onRemoveItems(new MappedIterator(item.getNavigator(), item => item && item.id));
this.onRowsChanged();
}
}
private onSetInput(e: Model.IInputEvent): void {
this.context.cache.garbageCollect();
this.inputItem = new RootViewItem(this.context, <Model.Item>e.item, this.wrapper);
}
private onItemChildrenRefreshing(e: Model.IItemChildrenRefreshEvent): void {
let item = <Model.Item>e.item;
let viewItem = this.items[item.id];
if (viewItem && this.context.options.showLoading) {
viewItem.loadingTimer = setTimeout(() => {
viewItem.loadingTimer = 0;
viewItem.loading = true;
}, TreeView.LOADING_DECORATION_DELAY);
}
if (!e.isNested) {
let childrenIds: string[] = [];
let navigator = item.getNavigator();
let childItem: Model.Item | null;
while (childItem = navigator.next()) {
childrenIds.push(childItem.id);
}
this.refreshingPreviousChildrenIds[item.id] = childrenIds;
}
}
private onItemChildrenRefreshed(e: Model.IItemChildrenRefreshEvent): void {
let item = <Model.Item>e.item;
let viewItem = this.items[item.id];
if (viewItem) {
if (viewItem.loadingTimer) {
clearTimeout(viewItem.loadingTimer);
viewItem.loadingTimer = 0;
}
viewItem.loading = false;
}
if (!e.isNested) {
let previousChildrenIds = this.refreshingPreviousChildrenIds[item.id];
let afterModelItems: Model.Item[] = [];
let navigator = item.getNavigator();
let childItem: Model.Item | null;
while (childItem = navigator.next()) {
afterModelItems.push(childItem);
}
let skipDiff = Math.abs(previousChildrenIds.length - afterModelItems.length) > 1000;
let diff: Diff.IDiffChange[] = [];
let doToInsertItemsAlreadyExist: boolean = false;
if (!skipDiff) {
const lcs = new Diff.LcsDiff(
{
getLength: () => previousChildrenIds.length,
getElementAtIndex: (i: number) => previousChildrenIds[i]
}, {
getLength: () => afterModelItems.length,
getElementAtIndex: (i: number) => afterModelItems[i].id
},
null
);
diff = lcs.ComputeDiff(false);
// this means that the result of the diff algorithm would result
// in inserting items that were already registered. this can only
// happen if the data provider returns bad ids OR if the sorting
// of the elements has changed
doToInsertItemsAlreadyExist = diff.some(d => {
if (d.modifiedLength > 0) {
for (let i = d.modifiedStart, len = d.modifiedStart + d.modifiedLength; i < len; i++) {
if (this.items.hasOwnProperty(afterModelItems[i].id)) {
return true;
}
}
}
return false;
});
}
// 50 is an optimization number, at some point we're better off
// just replacing everything
if (!skipDiff && !doToInsertItemsAlreadyExist && diff.length < 50) {
for (const diffChange of diff) {
if (diffChange.originalLength > 0) {
this.onRemoveItems(new ArrayIterator(previousChildrenIds, diffChange.originalStart, diffChange.originalStart + diffChange.originalLength));
}
if (diffChange.modifiedLength > 0) {
let beforeItem: Model.Item | null = afterModelItems[diffChange.modifiedStart - 1] || item;
beforeItem = beforeItem.getDepth() > 0 ? beforeItem : null;
this.onInsertItems(new ArrayIterator(afterModelItems, diffChange.modifiedStart, diffChange.modifiedStart + diffChange.modifiedLength), beforeItem ? beforeItem.id : null);
}
}
} else if (skipDiff || diff.length) {
this.onRemoveItems(new ArrayIterator(previousChildrenIds));
this.onInsertItems(new ArrayIterator(afterModelItems), item.getDepth() > 0 ? item.id : null);
}
if (skipDiff || diff.length) {
this.onRowsChanged();
}
}
}
private onItemRefresh(item: Model.Item): void {
this.onItemsRefresh([item]);
}
private onItemsRefresh(items: Model.Item[]): void {
this.onRefreshItemSet(items.filter(item => this.items.hasOwnProperty(item.id)));
this.onRowsChanged();
}
private onItemExpanding(e: Model.IItemExpandEvent): void {
let viewItem = this.items[e.item.id];
if (viewItem) {
viewItem.expanded = true;
}
}
private onItemExpanded(e: Model.IItemExpandEvent): void {
let item = <Model.Item>e.item;
let viewItem = this.items[item.id];
if (viewItem) {
viewItem.expanded = true;
let height = this.onInsertItems(item.getNavigator(), item.id) || 0;
let scrollTop = this.scrollTop;
if (viewItem.top + viewItem.height <= this.scrollTop) {
scrollTop += height;
}
this.onRowsChanged(scrollTop);
}
}
private onItemCollapsing(e: Model.IItemCollapseEvent): void {
let item = <Model.Item>e.item;
let viewItem = this.items[item.id];
if (viewItem) {
viewItem.expanded = false;
this.onRemoveItems(new MappedIterator(item.getNavigator(), item => item && item.id));
this.onRowsChanged();
}
}
private onItemReveal(e: Model.IItemRevealEvent): void {
let item = <Model.Item>e.item;
let relativeTop = <number>e.relativeTop;
let viewItem = this.items[item.id];
if (viewItem) {
if (relativeTop !== null) {
relativeTop = relativeTop < 0 ? 0 : relativeTop;
relativeTop = relativeTop > 1 ? 1 : relativeTop;
// y = mx + b
let m = viewItem.height - this.viewHeight;
this.scrollTop = m * relativeTop + viewItem.top;
} else {
let viewItemBottom = viewItem.top + viewItem.height;
let wrapperBottom = this.scrollTop + this.viewHeight;
if (viewItem.top < this.scrollTop) {
this.scrollTop = viewItem.top;
} else if (viewItemBottom >= wrapperBottom) {
this.scrollTop = viewItemBottom - this.viewHeight;
}
}
}
}
private onItemAddTrait(e: Model.IItemTraitEvent): void {
let item = <Model.Item>e.item;
let trait = <string>e.trait;
let viewItem = this.items[item.id];
if (viewItem) {
viewItem.addClass(trait);
}
if (trait === 'highlighted') {
DOM.addClass(this.domNode, trait);
// Ugly Firefox fix: input fields can't be selected if parent nodes are draggable
if (viewItem) {
this.highlightedItemWasDraggable = !!viewItem.draggable;
if (viewItem.draggable) {
viewItem.draggable = false;
}
}
}
}
private onItemRemoveTrait(e: Model.IItemTraitEvent): void {
let item = <Model.Item>e.item;
let trait = <string>e.trait;
let viewItem = this.items[item.id];
if (viewItem) {
viewItem.removeClass(trait);
}
if (trait === 'highlighted') {
DOM.removeClass(this.domNode, trait);
// Ugly Firefox fix: input fields can't be selected if parent nodes are draggable
if (this.highlightedItemWasDraggable) {
viewItem.draggable = true;
}
this.highlightedItemWasDraggable = false;
}
}
private onModelFocusChange(): void {
const focus = this.model && this.model.getFocus();
DOM.toggleClass(this.domNode, 'no-focused-item', !focus);
// ARIA
if (focus) {
this.domNode.setAttribute('aria-activedescendant', strings.safeBtoa(this.context.dataSource.getId(this.context.tree, focus)));
} else {
this.domNode.removeAttribute('aria-activedescendant');
}
}
// HeightMap "events"
public onInsertItem(item: ViewItem): void {
item.onDragStart = (e) => { this.onDragStart(item, e); };
item.needsRender = true;
this.refreshViewItem(item);
this.items[item.id] = item;
}
public onRefreshItem(item: ViewItem, needsRender = false): void {
item.needsRender = item.needsRender || needsRender;
this.refreshViewItem(item);
}
public onRemoveItem(item: ViewItem): void {
this.removeItemFromDOM(item);
item.dispose();
delete this.items[item.id];
}
// ViewItem refresh
private refreshViewItem(item: ViewItem): void {
item.render();
if (this.shouldBeRendered(item)) {
this.insertItemInDOM(item);
} else {
this.removeItemFromDOM(item);
}
}
// DOM Events
private onClick(e: MouseEvent): void {
if (this.lastPointerType && this.lastPointerType !== 'mouse') {
return;
}
let event = new Mouse.StandardMouseEvent(e);
let item = this.getItemAround(event.target);
if (!item) {
return;
}
if (Browser.isIE && Date.now() - this.lastClickTimeStamp < 300) {
// IE10+ doesn't set the detail property correctly. While IE10 simply
// counts the number of clicks, IE11 reports always 1. To align with
// other browser, we set the value to 2 if clicks events come in a 300ms
// sequence.
event.detail = 2;
}
this.lastClickTimeStamp = Date.now();
this.context.controller!.onClick(this.context.tree, item.model.getElement(), event);
}
private onMouseMiddleClick(e: MouseEvent): void {
if (!this.context.controller!.onMouseMiddleClick!) {
return;
}
let event = new Mouse.StandardMouseEvent(e);
let item = this.getItemAround(event.target);
if (!item) {
return;
}
this.context.controller!.onMouseMiddleClick!(this.context.tree, item.model.getElement(), event);
}
private onMouseDown(e: MouseEvent): void {
this.didJustPressContextMenuKey = false;
if (!this.context.controller!.onMouseDown!) {
return;
}
if (this.lastPointerType && this.lastPointerType !== 'mouse') {
return;
}
let event = new Mouse.StandardMouseEvent(e);
if (event.ctrlKey && Platform.isNative && Platform.isMacintosh) {
return;
}
let item = this.getItemAround(event.target);
if (!item) {
return;
}
this.context.controller!.onMouseDown!(this.context.tree, item.model.getElement(), event);
}
private onMouseUp(e: MouseEvent): void {
if (!this.context.controller!.onMouseUp!) {
return;
}
if (this.lastPointerType && this.lastPointerType !== 'mouse') {
return;
}
let event = new Mouse.StandardMouseEvent(e);
if (event.ctrlKey && Platform.isNative && Platform.isMacintosh) {
return;
}
let item = this.getItemAround(event.target);
if (!item) {
return;
}
this.context.controller!.onMouseUp!(this.context.tree, item.model.getElement(), event);
}
private onTap(e: Touch.GestureEvent): void {
let item = this.getItemAround(<HTMLElement>e.initialTarget);
if (!item) {
return;
}
this.context.controller!.onTap(this.context.tree, item.model.getElement(), e);
}
private onTouchChange(event: Touch.GestureEvent): void {
event.preventDefault();
event.stopPropagation();
this.scrollTop -= event.translationY;
}
private onContextMenu(keyboardEvent: KeyboardEvent): void;
private onContextMenu(mouseEvent: MouseEvent): void;
private onContextMenu(event: KeyboardEvent | MouseEvent): void {
let resultEvent: _.ContextMenuEvent;
let element: any;
if (event instanceof KeyboardEvent || this.didJustPressContextMenuKey) {
this.didJustPressContextMenuKey = false;
let keyboardEvent = new Keyboard.StandardKeyboardEvent(<KeyboardEvent>event);
element = this.model!.getFocus();
let position: DOM.IDomNodePagePosition;
if (!element) {
element = this.model!.getInput();
position = DOM.getDomNodePagePosition(this.inputItem.element);
} else {
const id = this.context.dataSource.getId(this.context.tree, element);
const viewItem = this.items[id!];
position = DOM.getDomNodePagePosition(viewItem.element);
}
resultEvent = new _.KeyboardContextMenuEvent(position.left + position.width, position.top, keyboardEvent);
} else {
let mouseEvent = new Mouse.StandardMouseEvent(<MouseEvent>event);
let item = this.getItemAround(mouseEvent.target);
if (!item) {
return;
}
element = item.model.getElement();
resultEvent = new _.MouseContextMenuEvent(mouseEvent);
}
this.context.controller!.onContextMenu(this.context.tree, element, resultEvent);
}
private onKeyDown(e: KeyboardEvent): void {
let event = new Keyboard.StandardKeyboardEvent(e);
this.didJustPressContextMenuKey = event.keyCode === KeyCode.ContextMenu || (event.shiftKey && event.keyCode === KeyCode.F10);
if (event.target && event.target.tagName && event.target.tagName.toLowerCase() === 'input') {
return; // Ignore event if target is a form input field (avoids browser specific issues)
}
if (this.didJustPressContextMenuKey) {
event.preventDefault();
event.stopPropagation();
}
this.context.controller!.onKeyDown(this.context.tree, event);
}
private onKeyUp(e: KeyboardEvent): void {
if (this.didJustPressContextMenuKey) {
this.onContextMenu(e);
}
this.didJustPressContextMenuKey = false;
this.context.controller!.onKeyUp(this.context.tree, new Keyboard.StandardKeyboardEvent(e));
}
private onDragStart(item: ViewItem, e: any): void {
if (this.model!.getHighlight()) {
return;
}
let element = item.model.getElement();
let selection = this.model!.getSelection();
let elements: any[];
if (selection.indexOf(element) > -1) {
elements = selection;
} else {
elements = [element];
}
e.dataTransfer.effectAllowed = 'copyMove';
e.dataTransfer.setData(DataTransfers.RESOURCES, JSON.stringify([item.uri]));
if (e.dataTransfer.setDragImage) {
let label: string;
if (this.context.dnd!.getDragLabel) {
label = this.context.dnd!.getDragLabel!(this.context.tree, elements);
} else {
label = String(elements.length);
}
const dragImage = document.createElement('div');
dragImage.className = 'monaco-tree-drag-image';
dragImage.textContent = label;
document.body.appendChild(dragImage);
e.dataTransfer.setDragImage(dragImage, -10, -10);
setTimeout(() => document.body.removeChild(dragImage), 0);
}
this.currentDragAndDropData = new dnd.ElementsDragAndDropData(elements);
StaticDND.CurrentDragAndDropData = new dnd.ExternalElementsDragAndDropData(elements);
this.context.dnd!.onDragStart(this.context.tree, this.currentDragAndDropData, new Mouse.DragMouseEvent(e));
}
private setupDragAndDropScrollInterval(): void {
let viewTop = DOM.getTopLeftOffset(this.wrapper).top;
if (!this.dragAndDropScrollInterval) {
this.dragAndDropScrollInterval = window.setInterval(() => {
if (this.dragAndDropMouseY === null) {
return;
}
let diff = this.dragAndDropMouseY - viewTop;
let scrollDiff = 0;
let upperLimit = this.viewHeight - 35;
if (diff < 35) {
scrollDiff = Math.max(-14, 0.2 * (diff - 35));
} else if (diff > upperLimit) {
scrollDiff = Math.min(14, 0.2 * (diff - upperLimit));
}
this.scrollTop += scrollDiff;
}, 10);
this.cancelDragAndDropScrollTimeout();
this.dragAndDropScrollTimeout = window.setTimeout(() => {
this.cancelDragAndDropScrollInterval();
this.dragAndDropScrollTimeout = null;
}, 1000);
}
}
private cancelDragAndDropScrollInterval(): void {
if (this.dragAndDropScrollInterval) {
window.clearInterval(this.dragAndDropScrollInterval);
this.dragAndDropScrollInterval = null;
}
this.cancelDragAndDropScrollTimeout();
}
private cancelDragAndDropScrollTimeout(): void {
if (this.dragAndDropScrollTimeout) {
window.clearTimeout(this.dragAndDropScrollTimeout);
this.dragAndDropScrollTimeout = null;
}
}
private onDragOver(e: DragEvent): boolean {
e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
let event = new Mouse.DragMouseEvent(e);
let viewItem = this.getItemAround(event.target);
if (!viewItem || (event.posx === 0 && event.posy === 0 && event.browserEvent.type === DOM.EventType.DRAG_LEAVE)) {
// dragging outside of tree
if (this.currentDropTarget) {
// clear previously hovered element feedback
this.currentDropTargets!.forEach(i => i.dropTarget = false);
this.currentDropTargets = [];
this.currentDropDisposable.dispose();
}
this.cancelDragAndDropScrollInterval();
this.currentDropTarget = null;
this.currentDropElement = null;
this.dragAndDropMouseY = null;
return false;
}
// dragging inside the tree
this.setupDragAndDropScrollInterval();
this.dragAndDropMouseY = event.posy;
if (!this.currentDragAndDropData) {
// just started dragging
if (StaticDND.CurrentDragAndDropData) {
this.currentDragAndDropData = StaticDND.CurrentDragAndDropData;
} else {
if (!event.dataTransfer.types) {
return false;
}
this.currentDragAndDropData = new dnd.DesktopDragAndDropData();
}
}
this.currentDragAndDropData.update((event.browserEvent as DragEvent).dataTransfer!);
let element: any;
let item: Model.Item | null = viewItem.model;
let reaction: _.IDragOverReaction | null;
// check the bubble up behavior
do {
element = item ? item.getElement() : this.model!.getInput();
reaction = this.context.dnd!.onDragOver(this.context.tree, this.currentDragAndDropData, element, event);
if (!reaction || reaction.bubble !== _.DragOverBubble.BUBBLE_UP) {
break;
}
item = item && item.parent;
} while (item);
if (!item) {
this.currentDropElement = null;
return false;
}
let canDrop = reaction && reaction.accept;
if (canDrop) {
this.currentDropElement = item.getElement();
event.preventDefault();
event.dataTransfer.dropEffect = reaction!.effect === _.DragOverEffect.COPY ? 'copy' : 'move';
} else {
this.currentDropElement = null;
}
// item is the model item where drop() should be called
// can be null
let currentDropTarget = item.id === this.inputItem.id ? this.inputItem : this.items[item.id];
if (this.shouldInvalidateDropReaction || this.currentDropTarget !== currentDropTarget || !reactionEquals(this.currentDropElementReaction, reaction)) {
this.shouldInvalidateDropReaction = false;
if (this.currentDropTarget) {
this.currentDropTargets!.forEach(i => i.dropTarget = false);
this.currentDropTargets = [];
this.currentDropDisposable.dispose();
}
this.currentDropTarget = currentDropTarget;
this.currentDropElementReaction = reaction!;
if (canDrop) {
// setup hover feedback for drop target
if (this.currentDropTarget) {
this.currentDropTarget.dropTarget = true;
this.currentDropTargets!.push(this.currentDropTarget);
}
if (reaction!.bubble === _.DragOverBubble.BUBBLE_DOWN) {
let nav = item.getNavigator();
let child: Model.Item | null;
while (child = nav.next()) {
viewItem = this.items[child.id];
if (viewItem) {
viewItem.dropTarget = true;
this.currentDropTargets!.push(viewItem);
}
}
}
if (reaction!.autoExpand) {
const timeoutPromise = timeout(500);
this.currentDropDisposable = Lifecycle.toDisposable(() => timeoutPromise.cancel());
timeoutPromise
.then(() => this.context.tree.expand(this.currentDropElement))
.then(() => this.shouldInvalidateDropReaction = true);
}
}
}
return true;
}
private onDrop(e: DragEvent): void {
if (this.currentDropElement) {
let event = new Mouse.DragMouseEvent(e);
event.preventDefault();
this.currentDragAndDropData!.update((event.browserEvent as DragEvent).dataTransfer!);
this.context.dnd!.drop(this.context.tree, this.currentDragAndDropData!, this.currentDropElement, event);
this.onDragEnd(e);
}
this.cancelDragAndDropScrollInterval();
}
private onDragEnd(e: DragEvent): void {
if (this.currentDropTarget) {
this.currentDropTargets!.forEach(i => i.dropTarget = false);
this.currentDropTargets = [];
}
this.currentDropDisposable.dispose();
this.cancelDragAndDropScrollInterval();
this.currentDragAndDropData = null;
StaticDND.CurrentDragAndDropData = undefined;
this.currentDropElement = null;
this.currentDropTarget = null;
this.dragAndDropMouseY = null;
}
private onFocus(): void {
if (!this.context.options.alwaysFocused) {
DOM.addClass(this.domNode, 'focused');
}
this._onDOMFocus.fire();
}
private onBlur(): void {
if (!this.context.options.alwaysFocused) {
DOM.removeClass(this.domNode, 'focused');
}
this.domNode.removeAttribute('aria-activedescendant'); // ARIA
this._onDOMBlur.fire();
}
// MS specific DOM Events
private onMsPointerDown(event: MSPointerEvent): void {
if (!this.msGesture) {
return;
}
// Circumvent IE11 breaking change in e.pointerType & TypeScript's stale definitions
let pointerType = event.pointerType;
if (pointerType === ((<any>event).MSPOINTER_TYPE_MOUSE || 'mouse')) {
this.lastPointerType = 'mouse';
return;
} else if (pointerType === ((<any>event).MSPOINTER_TYPE_TOUCH || 'touch')) {
this.lastPointerType = 'touch';
} else {
return;
}
event.stopPropagation();
event.preventDefault();
this.msGesture.addPointer(event.pointerId);
}
private onThrottledMsGestureChange(event: IThrottledGestureEvent): void {
this.scrollTop -= event.translationY;
}
private onMsGestureTap(event: MSGestureEvent): void {
(<any>event).initialTarget = document.elementFromPoint(event.clientX, event.clientY);
this.onTap(<any>event);
}
// DOM changes
private insertItemInDOM(item: ViewItem): void {
let elementAfter: HTMLElement | null = null;
let itemAfter = <ViewItem>this.itemAfter(item);
if (itemAfter && itemAfter.element) {
elementAfter = itemAfter.element;
}
item.insertInDOM(this.rowsContainer, elementAfter);
}
private removeItemFromDOM(item: ViewItem): void {
if (!item) {
return;
}
item.removeFromDOM();
}
// Helpers
private shouldBeRendered(item: ViewItem): boolean {
return item.top < this.lastRenderTop + this.lastRenderHeight && item.top + item.height > this.lastRenderTop;
}
private getItemAround(element: HTMLElement): ViewItem | undefined {
let candidate: ViewItem = this.inputItem;
let el: HTMLElement | null = element;
do {
if ((<any>el)[TreeView.BINDING]) {
candidate = (<any>el)[TreeView.BINDING];
}
if (el === this.wrapper || el === this.domNode) {
return candidate;
}
if (el === this.scrollableElement.getDomNode() || el === document.body) {
return undefined;
}
} while (el = el.parentElement);
return undefined;
}
// Cleanup
private releaseModel(): void {
if (this.model) {
this.modelListeners = Lifecycle.dispose(this.modelListeners);
this.model = null;
}
}
public dispose(): void {
// TODO@joao: improve
this.scrollableElement.dispose();
this.releaseModel();
this.viewListeners = Lifecycle.dispose(this.viewListeners);
this._onDOMFocus.dispose();
this._onDOMBlur.dispose();
if (this.domNode.parentNode) {
this.domNode.parentNode.removeChild(this.domNode);
}
if (this.items) {
Object.keys(this.items).forEach(key => this.items[key].removeFromDOM());
}
if (this.context.cache) {
this.context.cache.dispose();
}
super.dispose();
}
}