mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Vscode merge (#4582)
* Merge from vscode 37cb23d3dd4f9433d56d4ba5ea3203580719a0bd * fix issues with merges * bump node version in azpipe * replace license headers * remove duplicate launch task * fix build errors * fix build errors * fix tslint issues * working through package and linux build issues * more work * wip * fix packaged builds * working through linux build errors * wip * wip * wip * fix mac and linux file limits * iterate linux pipeline * disable editor typing * revert series to parallel * remove optimize vscode from linux * fix linting issues * revert testing change * add work round for new node * readd packaging for extensions * fix issue with angular not resolving decorator dependencies
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
|
||||
class WindowManager {
|
||||
|
||||
@@ -35,7 +34,7 @@ class WindowManager {
|
||||
}
|
||||
|
||||
// --- Zoom Factor
|
||||
private _zoomFactor: number = 0;
|
||||
private _zoomFactor: number = 1;
|
||||
|
||||
public getZoomFactor(): number {
|
||||
return this._zoomFactor;
|
||||
@@ -72,23 +71,6 @@ class WindowManager {
|
||||
public isFullscreen(): boolean {
|
||||
return this._fullscreen;
|
||||
}
|
||||
|
||||
// --- Accessibility
|
||||
private _accessibilitySupport = platform.AccessibilitySupport.Unknown;
|
||||
private readonly _onDidChangeAccessibilitySupport = new Emitter<void>();
|
||||
|
||||
public readonly onDidChangeAccessibilitySupport: Event<void> = this._onDidChangeAccessibilitySupport.event;
|
||||
public setAccessibilitySupport(accessibilitySupport: platform.AccessibilitySupport): void {
|
||||
if (this._accessibilitySupport === accessibilitySupport) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._accessibilitySupport = accessibilitySupport;
|
||||
this._onDidChangeAccessibilitySupport.fire();
|
||||
}
|
||||
public getAccessibilitySupport(): platform.AccessibilitySupport {
|
||||
return this._accessibilitySupport;
|
||||
}
|
||||
}
|
||||
|
||||
/** A zoom index, e.g. 1, 2, 3 */
|
||||
@@ -126,16 +108,6 @@ export function isFullscreen(): boolean {
|
||||
}
|
||||
export const onDidChangeFullscreen = WindowManager.INSTANCE.onDidChangeFullscreen;
|
||||
|
||||
export function setAccessibilitySupport(accessibilitySupport: platform.AccessibilitySupport): void {
|
||||
WindowManager.INSTANCE.setAccessibilitySupport(accessibilitySupport);
|
||||
}
|
||||
export function getAccessibilitySupport(): platform.AccessibilitySupport {
|
||||
return WindowManager.INSTANCE.getAccessibilitySupport();
|
||||
}
|
||||
export function onDidChangeAccessibilitySupport(callback: () => void): IDisposable {
|
||||
return WindowManager.INSTANCE.onDidChangeAccessibilitySupport(callback);
|
||||
}
|
||||
|
||||
const userAgent = navigator.userAgent;
|
||||
|
||||
export const isIE = (userAgent.indexOf('Trident') >= 0);
|
||||
|
||||
@@ -16,7 +16,9 @@ export class DelayedDragHandler extends Disposable {
|
||||
constructor(container: HTMLElement, callback: () => void) {
|
||||
super();
|
||||
|
||||
this._register(addDisposableListener(container, 'dragover', () => {
|
||||
this._register(addDisposableListener(container, 'dragover', e => {
|
||||
e.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
|
||||
|
||||
if (!this.timeout) {
|
||||
this.timeout = setTimeout(() => {
|
||||
callback();
|
||||
@@ -71,7 +73,7 @@ export const DataTransfers = {
|
||||
TEXT: 'text/plain'
|
||||
};
|
||||
|
||||
export function applyDragImage(event: DragEvent, label: string, clazz: string): void {
|
||||
export function applyDragImage(event: DragEvent, label: string | null, clazz: string): void {
|
||||
const dragImage = document.createElement('div');
|
||||
dragImage.className = clazz;
|
||||
dragImage.textContent = label;
|
||||
|
||||
@@ -615,7 +615,7 @@ export interface IDomNodePagePosition {
|
||||
height: number;
|
||||
}
|
||||
|
||||
export function size(element: HTMLElement, width: number, height: number): void {
|
||||
export function size(element: HTMLElement, width: number | null, height: number | null): void {
|
||||
if (typeof width === 'number') {
|
||||
element.style.width = `${width}px`;
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
|
||||
export interface IHistoryNavigationWidget {
|
||||
|
||||
showPreviousValue();
|
||||
showPreviousValue(): void;
|
||||
|
||||
showNextValue();
|
||||
showNextValue(): void;
|
||||
|
||||
}
|
||||
@@ -179,6 +179,9 @@ export function getCodeForKeyCode(keyCode: KeyCode): number {
|
||||
}
|
||||
|
||||
export interface IKeyboardEvent {
|
||||
|
||||
readonly _standardKeyboardEventBrand: true;
|
||||
|
||||
readonly browserEvent: KeyboardEvent;
|
||||
readonly target: HTMLElement;
|
||||
|
||||
@@ -206,6 +209,8 @@ const metaKeyMod = (platform.isMacintosh ? KeyMod.CtrlCmd : KeyMod.WinCtrl);
|
||||
|
||||
export class StandardKeyboardEvent implements IKeyboardEvent {
|
||||
|
||||
readonly _standardKeyboardEventBrand = true;
|
||||
|
||||
public readonly browserEvent: KeyboardEvent;
|
||||
public readonly target: HTMLElement;
|
||||
|
||||
|
||||
@@ -33,48 +33,50 @@ export function setARIAContainer(parent: HTMLElement) {
|
||||
/**
|
||||
* Given the provided message, will make sure that it is read as alert to screen readers.
|
||||
*/
|
||||
export function alert(msg: string): void {
|
||||
insertMessage(alertContainer, msg);
|
||||
export function alert(msg: string, disableRepeat?: boolean): void {
|
||||
insertMessage(alertContainer, msg, disableRepeat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given the provided message, will make sure that it is read as status to screen readers.
|
||||
*/
|
||||
export function status(msg: string): void {
|
||||
export function status(msg: string, disableRepeat?: boolean): void {
|
||||
if (isMacintosh) {
|
||||
alert(msg); // VoiceOver does not seem to support status role
|
||||
alert(msg, disableRepeat); // VoiceOver does not seem to support status role
|
||||
} else {
|
||||
insertMessage(statusContainer, msg);
|
||||
insertMessage(statusContainer, msg, disableRepeat);
|
||||
}
|
||||
}
|
||||
|
||||
let repeatedTimes = 0;
|
||||
let prevText: string | undefined = undefined;
|
||||
function insertMessage(target: HTMLElement, msg: string): void {
|
||||
function insertMessage(target: HTMLElement, msg: string, disableRepeat?: boolean): void {
|
||||
if (!ariaContainer) {
|
||||
// console.warn('ARIA support needs a container. Call setARIAContainer() first.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevText === msg) {
|
||||
repeatedTimes++;
|
||||
}
|
||||
else {
|
||||
prevText = msg;
|
||||
repeatedTimes = 0;
|
||||
}
|
||||
// If the same message should be inserted that is already present, a screen reader would
|
||||
// not announce this message because it matches the previous one. As a workaround, we
|
||||
// alter the message with the number of occurences unless this is explicitly disabled
|
||||
// via the disableRepeat flag.
|
||||
if (!disableRepeat) {
|
||||
if (prevText === msg) {
|
||||
repeatedTimes++;
|
||||
} else {
|
||||
prevText = msg;
|
||||
repeatedTimes = 0;
|
||||
}
|
||||
|
||||
|
||||
switch (repeatedTimes) {
|
||||
case 0: break;
|
||||
case 1: msg = nls.localize('repeated', "{0} (occurred again)", msg); break;
|
||||
default: msg = nls.localize('repeatedNtimes', "{0} (occurred {1} times)", msg, repeatedTimes); break;
|
||||
switch (repeatedTimes) {
|
||||
case 0: break;
|
||||
case 1: msg = nls.localize('repeated', "{0} (occurred again)", msg); break;
|
||||
default: msg = nls.localize('repeatedNtimes', "{0} (occurred {1} times)", msg, repeatedTimes); break;
|
||||
}
|
||||
}
|
||||
|
||||
dom.clearNode(target);
|
||||
target.textContent = msg;
|
||||
|
||||
|
||||
// See https://www.paciellogroup.com/blog/2012/06/html5-accessibility-chops-aria-rolealert-browser-support/
|
||||
target.style.visibility = 'hidden';
|
||||
target.style.visibility = 'visible';
|
||||
|
||||
@@ -200,8 +200,8 @@ export class BreadcrumbsWidget {
|
||||
return this._items[this._focusedItemIdx];
|
||||
}
|
||||
|
||||
setFocused(item: BreadcrumbsItem, payload?: any): void {
|
||||
this._focus(this._items.indexOf(item), payload);
|
||||
setFocused(item: BreadcrumbsItem | undefined, payload?: any): void {
|
||||
this._focus(this._items.indexOf(item!), payload);
|
||||
}
|
||||
|
||||
focusPrev(payload?: any): any {
|
||||
@@ -256,8 +256,8 @@ export class BreadcrumbsWidget {
|
||||
return this._items[this._selectedItemIdx];
|
||||
}
|
||||
|
||||
setSelection(item: BreadcrumbsItem, payload?: any): void {
|
||||
this._select(this._items.indexOf(item), payload);
|
||||
setSelection(item: BreadcrumbsItem | undefined, payload?: any): void {
|
||||
this._select(this._items.indexOf(item!), payload);
|
||||
}
|
||||
|
||||
private _select(nth: number, payload: any): void {
|
||||
|
||||
@@ -32,7 +32,7 @@ export interface IFindInputOptions extends IFindInputStyles {
|
||||
}
|
||||
|
||||
export interface IFindInputStyles extends IInputBoxStyles {
|
||||
inputActiveOptionBorder?: Color | null;
|
||||
inputActiveOptionBorder?: Color;
|
||||
}
|
||||
|
||||
const NLS_DEFAULT_LABEL = nls.localize('defaultLabel', "input");
|
||||
@@ -42,26 +42,25 @@ export class FindInput extends Widget {
|
||||
static readonly OPTION_CHANGE: string = 'optionChange';
|
||||
|
||||
private contextViewProvider: IContextViewProvider;
|
||||
private width: number;
|
||||
private placeholder: string;
|
||||
private validation?: IInputValidator;
|
||||
private label: string;
|
||||
private fixFocusOnOptionClickEnabled = true;
|
||||
|
||||
private inputActiveOptionBorder?: Color | null;
|
||||
private inputBackground?: Color | null;
|
||||
private inputForeground?: Color | null;
|
||||
private inputBorder?: Color | null;
|
||||
private inputActiveOptionBorder?: Color;
|
||||
private inputBackground?: Color;
|
||||
private inputForeground?: Color;
|
||||
private inputBorder?: Color;
|
||||
|
||||
private inputValidationInfoBorder?: Color | null;
|
||||
private inputValidationInfoBackground?: Color | null;
|
||||
private inputValidationInfoForeground?: Color | null;
|
||||
private inputValidationWarningBorder?: Color | null;
|
||||
private inputValidationWarningBackground?: Color | null;
|
||||
private inputValidationWarningForeground?: Color | null;
|
||||
private inputValidationErrorBorder?: Color | null;
|
||||
private inputValidationErrorBackground?: Color | null;
|
||||
private inputValidationErrorForeground?: Color | null;
|
||||
private inputValidationInfoBorder?: Color;
|
||||
private inputValidationInfoBackground?: Color;
|
||||
private inputValidationInfoForeground?: Color;
|
||||
private inputValidationWarningBorder?: Color;
|
||||
private inputValidationWarningBackground?: Color;
|
||||
private inputValidationWarningForeground?: Color;
|
||||
private inputValidationErrorBorder?: Color;
|
||||
private inputValidationErrorBackground?: Color;
|
||||
private inputValidationErrorForeground?: Color;
|
||||
|
||||
private regex: RegexCheckbox;
|
||||
private wholeWords: WholeWordsCheckbox;
|
||||
@@ -93,7 +92,6 @@ export class FindInput extends Widget {
|
||||
constructor(parent: HTMLElement | null, contextViewProvider: IContextViewProvider, private readonly _showOptionButtons: boolean, options: IFindInputOptions) {
|
||||
super();
|
||||
this.contextViewProvider = contextViewProvider;
|
||||
this.width = options.width || 100;
|
||||
this.placeholder = options.placeholder || '';
|
||||
this.validation = options.validation;
|
||||
this.label = options.label || NLS_DEFAULT_LABEL;
|
||||
@@ -159,13 +157,6 @@ export class FindInput extends Widget {
|
||||
this.focus();
|
||||
}
|
||||
|
||||
public setWidth(newWidth: number): void {
|
||||
this.width = newWidth;
|
||||
this.domNode.style.width = this.width + 'px';
|
||||
this.contextViewProvider.layout();
|
||||
this.setInputWidth();
|
||||
}
|
||||
|
||||
public getValue(): string {
|
||||
return this.inputBox.value;
|
||||
}
|
||||
@@ -202,7 +193,7 @@ export class FindInput extends Widget {
|
||||
protected applyStyles(): void {
|
||||
if (this.domNode) {
|
||||
const checkBoxStyles: ICheckboxStyles = {
|
||||
inputActiveOptionBorder: this.inputActiveOptionBorder || undefined,
|
||||
inputActiveOptionBorder: this.inputActiveOptionBorder,
|
||||
};
|
||||
this.regex.style(checkBoxStyles);
|
||||
this.wholeWords.style(checkBoxStyles);
|
||||
@@ -240,7 +231,6 @@ export class FindInput extends Widget {
|
||||
|
||||
public setCaseSensitive(value: boolean): void {
|
||||
this.caseSensitive.checked = value;
|
||||
this.setInputWidth();
|
||||
}
|
||||
|
||||
public getWholeWords(): boolean {
|
||||
@@ -249,7 +239,6 @@ export class FindInput extends Widget {
|
||||
|
||||
public setWholeWords(value: boolean): void {
|
||||
this.wholeWords.checked = value;
|
||||
this.setInputWidth();
|
||||
}
|
||||
|
||||
public getRegex(): boolean {
|
||||
@@ -258,7 +247,6 @@ export class FindInput extends Widget {
|
||||
|
||||
public setRegex(value: boolean): void {
|
||||
this.regex.checked = value;
|
||||
this.setInputWidth();
|
||||
this.validate();
|
||||
}
|
||||
|
||||
@@ -277,15 +265,8 @@ export class FindInput extends Widget {
|
||||
dom.addClass(this.domNode, 'highlight-' + (this._lastHighlightFindOptions));
|
||||
}
|
||||
|
||||
private setInputWidth(): void {
|
||||
let w = this.width - this.caseSensitive.width() - this.wholeWords.width() - this.regex.width();
|
||||
this.inputBox.width = w;
|
||||
this.inputBox.layout();
|
||||
}
|
||||
|
||||
private buildDomNode(appendCaseSensitiveLabel: string, appendWholeWordsLabel: string, appendRegexLabel: string, history: string[], flexibleHeight: boolean): void {
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.style.width = this.width + 'px';
|
||||
dom.addClass(this.domNode, 'monaco-findInput');
|
||||
|
||||
this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, {
|
||||
@@ -313,14 +294,13 @@ export class FindInput extends Widget {
|
||||
this.regex = this._register(new RegexCheckbox({
|
||||
appendTitle: appendRegexLabel,
|
||||
isChecked: false,
|
||||
inputActiveOptionBorder: this.inputActiveOptionBorder || undefined
|
||||
inputActiveOptionBorder: this.inputActiveOptionBorder
|
||||
}));
|
||||
this._register(this.regex.onChange(viaKeyboard => {
|
||||
this._onDidOptionChange.fire(viaKeyboard);
|
||||
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
this.setInputWidth();
|
||||
this.validate();
|
||||
}));
|
||||
this._register(this.regex.onKeyDown(e => {
|
||||
@@ -330,34 +310,40 @@ export class FindInput extends Widget {
|
||||
this.wholeWords = this._register(new WholeWordsCheckbox({
|
||||
appendTitle: appendWholeWordsLabel,
|
||||
isChecked: false,
|
||||
inputActiveOptionBorder: this.inputActiveOptionBorder || undefined
|
||||
inputActiveOptionBorder: this.inputActiveOptionBorder
|
||||
}));
|
||||
this._register(this.wholeWords.onChange(viaKeyboard => {
|
||||
this._onDidOptionChange.fire(viaKeyboard);
|
||||
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
this.setInputWidth();
|
||||
this.validate();
|
||||
}));
|
||||
|
||||
this.caseSensitive = this._register(new CaseSensitiveCheckbox({
|
||||
appendTitle: appendCaseSensitiveLabel,
|
||||
isChecked: false,
|
||||
inputActiveOptionBorder: this.inputActiveOptionBorder || undefined
|
||||
inputActiveOptionBorder: this.inputActiveOptionBorder
|
||||
}));
|
||||
this._register(this.caseSensitive.onChange(viaKeyboard => {
|
||||
this._onDidOptionChange.fire(viaKeyboard);
|
||||
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
this.setInputWidth();
|
||||
this.validate();
|
||||
}));
|
||||
this._register(this.caseSensitive.onKeyDown(e => {
|
||||
this._onCaseSensitiveKeyDown.fire(e);
|
||||
}));
|
||||
|
||||
if (this._showOptionButtons) {
|
||||
const paddingRight = (this.caseSensitive.width() + this.wholeWords.width() + this.regex.width()) + 'px';
|
||||
this.inputBox.inputElement.style.paddingRight = paddingRight;
|
||||
if (this.inputBox.mirrorElement) {
|
||||
this.inputBox.mirrorElement.style.paddingRight = paddingRight;
|
||||
}
|
||||
}
|
||||
|
||||
// Arrow-Key support to navigate between options
|
||||
let indexes = [this.caseSensitive.domNode, this.wholeWords.domNode, this.regex.domNode];
|
||||
this.onkeydown(this.domNode, (event: IKeyboardEvent) => {
|
||||
@@ -386,7 +372,6 @@ export class FindInput extends Widget {
|
||||
}
|
||||
});
|
||||
|
||||
this.setInputWidth();
|
||||
|
||||
let controls = document.createElement('div');
|
||||
controls.className = 'controls';
|
||||
|
||||
@@ -230,6 +230,10 @@ export class Grid<T extends IView> implements IDisposable {
|
||||
this.gridview.layout(width, height);
|
||||
}
|
||||
|
||||
hasView(view: T): boolean {
|
||||
return this.views.has(view);
|
||||
}
|
||||
|
||||
addView(newView: T, size: number | Sizing, referenceView: T, direction: Direction): void {
|
||||
if (this.views.has(newView)) {
|
||||
throw new Error('Can\'t add same view twice');
|
||||
@@ -258,7 +262,7 @@ export class Grid<T extends IView> implements IDisposable {
|
||||
this._addView(newView, viewSize, location);
|
||||
}
|
||||
|
||||
protected _addView(newView: T, size: number | GridViewSizing, location): void {
|
||||
protected _addView(newView: T, size: number | GridViewSizing, location: number[]): void {
|
||||
this.views.set(newView, newView.element);
|
||||
this.gridview.addView(newView, size, location);
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export class HighlightedLabel {
|
||||
let total = 0;
|
||||
let extra = 0;
|
||||
|
||||
return text.replace(/\r\n|\r|\n/, (match, offset) => {
|
||||
return text.replace(/\r\n|\r|\n/g, (match, offset) => {
|
||||
extra = match === '\r\n' ? -1 : 0;
|
||||
offset += total;
|
||||
|
||||
|
||||
@@ -37,18 +37,18 @@ export interface IInputOptions extends IInputBoxStyles {
|
||||
}
|
||||
|
||||
export interface IInputBoxStyles {
|
||||
readonly inputBackground?: Color | null;
|
||||
readonly inputForeground?: Color | null;
|
||||
readonly inputBorder?: Color | null;
|
||||
readonly inputValidationInfoBorder?: Color | null;
|
||||
readonly inputValidationInfoBackground?: Color | null;
|
||||
readonly inputValidationInfoForeground?: Color | null;
|
||||
readonly inputValidationWarningBorder?: Color | null;
|
||||
readonly inputValidationWarningBackground?: Color | null;
|
||||
readonly inputValidationWarningForeground?: Color | null;
|
||||
readonly inputValidationErrorBorder?: Color | null;
|
||||
readonly inputValidationErrorBackground?: Color | null;
|
||||
readonly inputValidationErrorForeground?: Color | null;
|
||||
readonly inputBackground?: Color;
|
||||
readonly inputForeground?: Color;
|
||||
readonly inputBorder?: Color;
|
||||
readonly inputValidationInfoBorder?: Color;
|
||||
readonly inputValidationInfoBackground?: Color;
|
||||
readonly inputValidationInfoForeground?: Color;
|
||||
readonly inputValidationWarningBorder?: Color;
|
||||
readonly inputValidationWarningBackground?: Color;
|
||||
readonly inputValidationWarningForeground?: Color;
|
||||
readonly inputValidationErrorBorder?: Color;
|
||||
readonly inputValidationErrorBackground?: Color;
|
||||
readonly inputValidationErrorForeground?: Color;
|
||||
}
|
||||
|
||||
export interface IInputValidator {
|
||||
@@ -104,20 +104,20 @@ export class InputBox extends Widget {
|
||||
|
||||
// {{SQL CARBON EDIT}} - Add showValidationMessage and set inputBackground, inputForeground, and inputBorder as protected
|
||||
protected showValidationMessage: boolean;
|
||||
protected inputBackground?: Color | null;
|
||||
protected inputForeground?: Color | null;
|
||||
protected inputBorder?: Color | null;
|
||||
protected inputBackground?: Color;
|
||||
protected inputForeground?: Color;
|
||||
protected inputBorder?: Color;
|
||||
// {{SQL CARBON EDIT}} - End
|
||||
|
||||
private inputValidationInfoBorder?: Color | null;
|
||||
private inputValidationInfoBackground?: Color | null;
|
||||
private inputValidationInfoForeground?: Color | null;
|
||||
private inputValidationWarningBorder?: Color | null;
|
||||
private inputValidationWarningBackground?: Color | null;
|
||||
private inputValidationWarningForeground?: Color | null;
|
||||
private inputValidationErrorBorder?: Color | null;
|
||||
private inputValidationErrorBackground?: Color | null;
|
||||
private inputValidationErrorForeground?: Color | null;
|
||||
private inputValidationInfoBorder?: Color;
|
||||
private inputValidationInfoBackground?: Color;
|
||||
private inputValidationInfoForeground?: Color;
|
||||
private inputValidationWarningBorder?: Color;
|
||||
private inputValidationWarningBackground?: Color;
|
||||
private inputValidationWarningForeground?: Color;
|
||||
private inputValidationErrorBorder?: Color;
|
||||
private inputValidationErrorBackground?: Color;
|
||||
private inputValidationErrorForeground?: Color;
|
||||
|
||||
private _onDidChange = this._register(new Emitter<string>());
|
||||
public readonly onDidChange: Event<string> = this._onDidChange.event;
|
||||
@@ -253,6 +253,10 @@ export class InputBox extends Widget {
|
||||
}
|
||||
}
|
||||
|
||||
public get mirrorElement(): HTMLElement {
|
||||
return this.mirror;
|
||||
}
|
||||
|
||||
public get inputElement(): HTMLInputElement {
|
||||
return this.input;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,15 @@
|
||||
color: #555;
|
||||
font-size: 11px;
|
||||
padding: 3px 5px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.monaco-keybinding > .monaco-keybinding-key:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.monaco-keybinding > .monaco-keybinding-key:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.hc-black .monaco-keybinding > .monaco-keybinding-key,
|
||||
@@ -36,5 +45,5 @@
|
||||
}
|
||||
|
||||
.monaco-keybinding > .monaco-keybinding-key-chord-separator {
|
||||
width: 2px;
|
||||
width: 6px;
|
||||
}
|
||||
@@ -33,7 +33,7 @@ export interface KeybindingLabelOptions {
|
||||
export class KeybindingLabel {
|
||||
|
||||
private domNode: HTMLElement;
|
||||
private keybinding: ResolvedKeybinding;
|
||||
private keybinding: ResolvedKeybinding | null | undefined;
|
||||
private matches: Matches | undefined;
|
||||
private didEverRender: boolean;
|
||||
|
||||
@@ -47,7 +47,7 @@ export class KeybindingLabel {
|
||||
return this.domNode;
|
||||
}
|
||||
|
||||
set(keybinding: ResolvedKeybinding, matches?: Matches) {
|
||||
set(keybinding: ResolvedKeybinding | null | undefined, matches?: Matches) {
|
||||
if (this.didEverRender && this.keybinding === keybinding && KeybindingLabel.areSame(this.matches, matches)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
}
|
||||
|
||||
/* Dnd */
|
||||
.monaco-list-drag-image {
|
||||
.monaco-drag-image {
|
||||
display: inline-block;
|
||||
padding: 1px 7px;
|
||||
border-radius: 10px;
|
||||
|
||||
@@ -16,8 +16,8 @@ export interface IListVirtualDelegate<T> {
|
||||
export interface IListRenderer<T, TTemplateData> {
|
||||
templateId: string;
|
||||
renderTemplate(container: HTMLElement): TTemplateData;
|
||||
renderElement(element: T, index: number, templateData: TTemplateData): void;
|
||||
disposeElement?(element: T, index: number, templateData: TTemplateData): void;
|
||||
renderElement(element: T, index: number, templateData: TTemplateData, dynamicHeightProbing?: boolean): void;
|
||||
disposeElement?(element: T, index: number, templateData: TTemplateData, dynamicHeightProbing?: boolean): void;
|
||||
disposeTemplate(templateData: TTemplateData): void;
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ export interface IListContextMenuEvent<T> {
|
||||
browserEvent: UIEvent;
|
||||
element: T | undefined;
|
||||
index: number | undefined;
|
||||
anchor: HTMLElement | { x: number; y: number; } | undefined;
|
||||
anchor: HTMLElement | { x: number; y: number; };
|
||||
}
|
||||
|
||||
export interface IIdentityProvider<T> {
|
||||
|
||||
@@ -35,7 +35,7 @@ class PagedRenderer<TElement, TTemplateData> implements IListRenderer<number, IT
|
||||
return { data, disposable: { dispose: () => { } } };
|
||||
}
|
||||
|
||||
renderElement(index: number, _: number, data: ITemplateData<TTemplateData>): void {
|
||||
renderElement(index: number, _: number, data: ITemplateData<TTemplateData>, dynamicHeightProbing?: boolean): void {
|
||||
if (data.disposable) {
|
||||
data.disposable.dispose();
|
||||
}
|
||||
@@ -47,7 +47,7 @@ class PagedRenderer<TElement, TTemplateData> implements IListRenderer<number, IT
|
||||
const model = this.modelProvider();
|
||||
|
||||
if (model.isResolved(index)) {
|
||||
return this.renderer.renderElement(model.get(index), index, data.data);
|
||||
return this.renderer.renderElement(model.get(index), index, data.data, dynamicHeightProbing);
|
||||
}
|
||||
|
||||
const cts = new CancellationTokenSource();
|
||||
@@ -55,7 +55,7 @@ class PagedRenderer<TElement, TTemplateData> implements IListRenderer<number, IT
|
||||
data.disposable = { dispose: () => cts.cancel() };
|
||||
|
||||
this.renderer.renderPlaceholder(index, data.data);
|
||||
promise.then(entry => this.renderer.renderElement(entry, index, data.data!));
|
||||
promise.then(entry => this.renderer.renderElement(entry, index, data.data!, dynamicHeightProbing));
|
||||
}
|
||||
|
||||
disposeTemplate(data: ITemplateData<TTemplateData>): void {
|
||||
@@ -194,6 +194,10 @@ export class PagedList<T> implements IDisposable {
|
||||
this.list.layout(height, width);
|
||||
}
|
||||
|
||||
toggleKeyboardNavigation(): void {
|
||||
this.list.toggleKeyboardNavigation();
|
||||
}
|
||||
|
||||
reveal(index: number, relativeTop?: number): void {
|
||||
this.list.reveal(index, relativeTop);
|
||||
}
|
||||
|
||||
@@ -41,6 +41,11 @@ export interface IListViewDragAndDrop<T> extends IListDragAndDrop<T> {
|
||||
getDragElements(element: T): T[];
|
||||
}
|
||||
|
||||
export interface IAriaSetProvider<T> {
|
||||
getSetSize(element: T, index: number, listLength: number): number;
|
||||
getPosInSet(element: T, index: number): number;
|
||||
}
|
||||
|
||||
export interface IListViewOptions<T> {
|
||||
readonly dnd?: IListViewDragAndDrop<T>;
|
||||
readonly useShadows?: boolean;
|
||||
@@ -49,6 +54,7 @@ export interface IListViewOptions<T> {
|
||||
readonly supportDynamicHeights?: boolean;
|
||||
readonly mouseSupport?: boolean;
|
||||
readonly horizontalScrolling?: boolean;
|
||||
readonly ariaSetProvider?: IAriaSetProvider<T>;
|
||||
}
|
||||
|
||||
const DefaultOptions = {
|
||||
@@ -142,6 +148,9 @@ function equalsDragFeedback(f1: number[] | undefined, f2: number[] | undefined):
|
||||
|
||||
export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
|
||||
private static InstanceCount = 0;
|
||||
readonly domId = `list_id_${++ListView.InstanceCount}`;
|
||||
|
||||
readonly domNode: HTMLElement;
|
||||
|
||||
private items: IItem<T>[];
|
||||
@@ -165,6 +174,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
private setRowLineHeight: boolean;
|
||||
private supportDynamicHeights: boolean;
|
||||
private horizontalScrolling: boolean;
|
||||
private ariaSetProvider: IAriaSetProvider<T>;
|
||||
private scrollWidth: number | undefined;
|
||||
private canUseTranslate3d: boolean | undefined = undefined;
|
||||
|
||||
@@ -195,7 +205,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
container: HTMLElement,
|
||||
private virtualDelegate: IListVirtualDelegate<T>,
|
||||
renderers: IListRenderer<any /* TODO@joao */, any>[],
|
||||
options: IListViewOptions<T> = DefaultOptions
|
||||
options: IListViewOptions<T> = DefaultOptions as IListViewOptions<T>
|
||||
) {
|
||||
if (options.horizontalScrolling && options.supportDynamicHeights) {
|
||||
throw new Error('Horizontal scrolling and dynamic heights not supported simultaneously');
|
||||
@@ -216,11 +226,17 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.className = 'monaco-list';
|
||||
|
||||
DOM.addClass(this.domNode, this.domId);
|
||||
this.domNode.tabIndex = 0;
|
||||
|
||||
DOM.toggleClass(this.domNode, 'mouse-support', typeof options.mouseSupport === 'boolean' ? options.mouseSupport : true);
|
||||
|
||||
this.horizontalScrolling = getOrDefault(options, o => o.horizontalScrolling, DefaultOptions.horizontalScrolling);
|
||||
DOM.toggleClass(this.domNode, 'horizontal-scrolling', this.horizontalScrolling);
|
||||
|
||||
this.ariaSetProvider = options.ariaSetProvider || { getSetSize: (e, i, length) => length, getPosInSet: (_, index) => index + 1 };
|
||||
|
||||
this.rowsContainer = document.createElement('div');
|
||||
this.rowsContainer.className = 'monaco-list-rows';
|
||||
Gesture.addTarget(this.rowsContainer);
|
||||
@@ -344,7 +360,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
this.eventuallyUpdateScrollDimensions();
|
||||
|
||||
if (this.supportDynamicHeights) {
|
||||
this.rerender(this.scrollTop, this.renderHeight);
|
||||
this._rerender(this.scrollTop, this.renderHeight);
|
||||
}
|
||||
|
||||
return deleted.map(i => i.element);
|
||||
@@ -406,6 +422,18 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
rerender(): void {
|
||||
if (!this.supportDynamicHeights) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of this.items) {
|
||||
item.lastDynamicHeightWidth = undefined;
|
||||
}
|
||||
|
||||
this._rerender(this.lastRenderTop, this.lastRenderHeight);
|
||||
}
|
||||
|
||||
get length(): number {
|
||||
return this.items.length;
|
||||
}
|
||||
@@ -415,6 +443,25 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
return scrollDimensions.height;
|
||||
}
|
||||
|
||||
get firstVisibleIndex(): number {
|
||||
const range = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
|
||||
const firstElTop = this.rangeMap.positionAt(range.start);
|
||||
const nextElTop = this.rangeMap.positionAt(range.start + 1);
|
||||
if (nextElTop !== -1) {
|
||||
const firstElMidpoint = (nextElTop - firstElTop) / 2 + firstElTop;
|
||||
if (firstElMidpoint < this.scrollTop) {
|
||||
return range.start + 1;
|
||||
}
|
||||
}
|
||||
|
||||
return range.start;
|
||||
}
|
||||
|
||||
get lastVisibleIndex(): number {
|
||||
const range = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
|
||||
return range.end - 1;
|
||||
}
|
||||
|
||||
element(index: number): T {
|
||||
return this.items[index].element;
|
||||
}
|
||||
@@ -457,7 +504,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
this.renderWidth = width;
|
||||
|
||||
if (this.supportDynamicHeights) {
|
||||
this.rerender(this.scrollTop, this.renderHeight);
|
||||
this._rerender(this.scrollTop, this.renderHeight);
|
||||
}
|
||||
|
||||
if (this.horizontalScrolling) {
|
||||
@@ -528,6 +575,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
|
||||
if (!item.row) {
|
||||
item.row = this.cache.alloc(item.templateId);
|
||||
item.row!.domNode!.setAttribute('role', 'treeitem');
|
||||
}
|
||||
|
||||
if (!item.row.domNode!.parentElement) {
|
||||
@@ -595,8 +643,10 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
|
||||
item.row!.domNode!.setAttribute('data-index', `${index}`);
|
||||
item.row!.domNode!.setAttribute('data-last-element', index === this.length - 1 ? 'true' : 'false');
|
||||
item.row!.domNode!.setAttribute('aria-setsize', `${this.length}`);
|
||||
item.row!.domNode!.setAttribute('aria-posinset', `${index + 1}`);
|
||||
item.row!.domNode!.setAttribute('aria-setsize', String(this.ariaSetProvider.getSetSize(item.element, index, this.length)));
|
||||
item.row!.domNode!.setAttribute('aria-posinset', String(this.ariaSetProvider.getPosInSet(item.element, index)));
|
||||
item.row!.domNode!.setAttribute('id', this.getElementDomId(index));
|
||||
|
||||
DOM.toggleClass(item.row!.domNode!, 'drop-target', item.dropTarget);
|
||||
}
|
||||
|
||||
@@ -691,7 +741,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
this.render(e.scrollTop, e.height, e.scrollLeft, e.scrollWidth);
|
||||
|
||||
if (this.supportDynamicHeights) {
|
||||
this.rerender(e.scrollTop, e.height);
|
||||
this._rerender(e.scrollTop, e.height);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Got bad scroll event:', e);
|
||||
@@ -729,7 +779,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
label = String(elements.length);
|
||||
}
|
||||
|
||||
const dragImage = DOM.$('.monaco-list-drag-image');
|
||||
const dragImage = DOM.$('.monaco-drag-image');
|
||||
dragImage.textContent = label;
|
||||
document.body.appendChild(dragImage);
|
||||
event.dataTransfer.setDragImage(dragImage, -10, -10);
|
||||
@@ -745,6 +795,8 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
}
|
||||
|
||||
private onDragOver(event: IListDragEvent<T>): boolean {
|
||||
event.browserEvent.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
|
||||
|
||||
this.onDragLeaveTimeout.dispose();
|
||||
|
||||
if (StaticDND.CurrentDragAndDropData && StaticDND.CurrentDragAndDropData.getData() === 'vscode-ui') {
|
||||
@@ -954,7 +1006,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
* Given a stable rendered state, checks every rendered element whether it needs
|
||||
* to be probed for dynamic height. Adjusts scroll height and top if necessary.
|
||||
*/
|
||||
private rerender(renderTop: number, renderHeight: number): void {
|
||||
private _rerender(renderTop: number, renderHeight: number): void {
|
||||
const previousRenderRange = this.getRenderRange(renderTop, renderHeight);
|
||||
|
||||
// Let's remember the second element's position, this helps in scrolling up
|
||||
@@ -1038,14 +1090,20 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
}
|
||||
|
||||
const size = item.size;
|
||||
const renderer = this.renderers.get(item.templateId);
|
||||
const row = this.cache.alloc(item.templateId);
|
||||
|
||||
row.domNode!.style.height = '';
|
||||
this.rowsContainer.appendChild(row.domNode!);
|
||||
|
||||
const renderer = this.renderers.get(item.templateId);
|
||||
if (renderer) {
|
||||
renderer.renderElement(item.element, index, row.templateData);
|
||||
renderer.renderElement(item.element, index, row.templateData, true);
|
||||
|
||||
if (renderer.disposeElement) {
|
||||
renderer.disposeElement(item.element, index, row.templateData, true);
|
||||
}
|
||||
}
|
||||
|
||||
item.size = row.domNode!.offsetHeight;
|
||||
item.lastDynamicHeightWidth = this.renderWidth;
|
||||
this.rowsContainer.removeChild(row.domNode!);
|
||||
@@ -1074,6 +1132,10 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
return nextToLastItem.row.domNode;
|
||||
}
|
||||
|
||||
getElementDomId(index: number): string {
|
||||
return `${this.domId}_${index}`;
|
||||
}
|
||||
|
||||
// Dispose
|
||||
|
||||
dispose() {
|
||||
|
||||
@@ -17,7 +17,7 @@ import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardE
|
||||
import { Event, Emitter, EventBufferer } from 'vs/base/common/event';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider, IKeyboardNavigationLabelProvider, IListDragAndDrop, IListDragOverReaction, ListAriaRootRole } from './list';
|
||||
import { ListView, IListViewOptions, IListViewDragAndDrop } from './listView';
|
||||
import { ListView, IListViewOptions, IListViewDragAndDrop, IAriaSetProvider } from './listView';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
@@ -179,16 +179,12 @@ class Trait<T> implements ISpliceable<boolean>, IDisposable {
|
||||
|
||||
class FocusTrait<T> extends Trait<T> {
|
||||
|
||||
constructor(
|
||||
private getDomId: (index: number) => string
|
||||
) {
|
||||
constructor() {
|
||||
super('focused');
|
||||
}
|
||||
|
||||
renderIndex(index: number, container: HTMLElement): void {
|
||||
super.renderIndex(index, container);
|
||||
container.setAttribute('role', 'treeitem');
|
||||
container.setAttribute('id', this.getDomId(index));
|
||||
|
||||
if (this.contains(index)) {
|
||||
container.setAttribute('aria-selected', 'true');
|
||||
@@ -338,6 +334,10 @@ class TypeLabelController<T> implements IDisposable {
|
||||
|
||||
private enabled = false;
|
||||
private state: TypeLabelControllerState = TypeLabelControllerState.Idle;
|
||||
|
||||
private automaticKeyboardNavigation = true;
|
||||
private triggered = false;
|
||||
|
||||
private enabledDisposables: IDisposable[] = [];
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
@@ -346,11 +346,10 @@ class TypeLabelController<T> implements IDisposable {
|
||||
private view: ListView<T>,
|
||||
private keyboardNavigationLabelProvider: IKeyboardNavigationLabelProvider<T>
|
||||
) {
|
||||
list.onDidUpdateOptions(this.onDidUpdateListOptions, this, this.disposables);
|
||||
this.onDidUpdateListOptions(list.options);
|
||||
this.updateOptions(list.options);
|
||||
}
|
||||
|
||||
private onDidUpdateListOptions(options: IListOptions<T>): void {
|
||||
updateOptions(options: IListOptions<T>): void {
|
||||
const enableKeyboardNavigation = typeof options.enableKeyboardNavigation === 'undefined' ? true : !!options.enableKeyboardNavigation;
|
||||
|
||||
if (enableKeyboardNavigation) {
|
||||
@@ -358,6 +357,14 @@ class TypeLabelController<T> implements IDisposable {
|
||||
} else {
|
||||
this.disable();
|
||||
}
|
||||
|
||||
if (typeof options.automaticKeyboardNavigation !== 'undefined') {
|
||||
this.automaticKeyboardNavigation = options.automaticKeyboardNavigation;
|
||||
}
|
||||
}
|
||||
|
||||
toggle(): void {
|
||||
this.triggered = !this.triggered;
|
||||
}
|
||||
|
||||
private enable(): void {
|
||||
@@ -367,8 +374,10 @@ class TypeLabelController<T> implements IDisposable {
|
||||
|
||||
const onChar = Event.chain(domEvent(this.view.domNode, 'keydown'))
|
||||
.filter(e => !isInputElement(e.target as HTMLElement))
|
||||
.filter(() => this.automaticKeyboardNavigation || this.triggered)
|
||||
.map(event => new StandardKeyboardEvent(event))
|
||||
.filter(this.keyboardNavigationLabelProvider.mightProducePrintableCharacter ? e => this.keyboardNavigationLabelProvider.mightProducePrintableCharacter!(e) : e => mightProducePrintableCharacter(e))
|
||||
.forEach(e => { e.stopPropagation(); e.preventDefault(); })
|
||||
.map(event => event.browserEvent.key)
|
||||
.event;
|
||||
|
||||
@@ -378,6 +387,7 @@ class TypeLabelController<T> implements IDisposable {
|
||||
onInput(this.onInput, this, this.enabledDisposables);
|
||||
|
||||
this.enabled = true;
|
||||
this.triggered = false;
|
||||
}
|
||||
|
||||
private disable(): void {
|
||||
@@ -387,11 +397,13 @@ class TypeLabelController<T> implements IDisposable {
|
||||
|
||||
this.enabledDisposables = dispose(this.enabledDisposables);
|
||||
this.enabled = false;
|
||||
this.triggered = false;
|
||||
}
|
||||
|
||||
private onInput(word: string | null): void {
|
||||
if (!word) {
|
||||
this.state = TypeLabelControllerState.Idle;
|
||||
this.triggered = false;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -502,37 +514,38 @@ const DefaultOpenController: IOpenController = {
|
||||
}
|
||||
};
|
||||
|
||||
class MouseController<T> implements IDisposable {
|
||||
export class MouseController<T> implements IDisposable {
|
||||
|
||||
private multipleSelectionSupport: boolean;
|
||||
readonly multipleSelectionController: IMultipleSelectionController<T>;
|
||||
private openController: IOpenController;
|
||||
private mouseSupport: boolean;
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
private list: List<T>,
|
||||
private view: ListView<T>,
|
||||
options: IListOptions<T> = {}
|
||||
) {
|
||||
this.multipleSelectionSupport = !(options.multipleSelectionSupport === false);
|
||||
constructor(protected list: List<T>) {
|
||||
this.multipleSelectionSupport = !(list.options.multipleSelectionSupport === false);
|
||||
|
||||
if (this.multipleSelectionSupport) {
|
||||
this.multipleSelectionController = options.multipleSelectionController || DefaultMultipleSelectionContoller;
|
||||
this.multipleSelectionController = list.options.multipleSelectionController || DefaultMultipleSelectionContoller;
|
||||
}
|
||||
|
||||
this.openController = options.openController || DefaultOpenController;
|
||||
this.openController = list.options.openController || DefaultOpenController;
|
||||
this.mouseSupport = typeof list.options.mouseSupport === 'undefined' || !!list.options.mouseSupport;
|
||||
|
||||
view.onMouseDown(this.onMouseDown, this, this.disposables);
|
||||
view.onContextMenu(this.onContextMenu, this, this.disposables);
|
||||
view.onMouseClick(this.onPointer, this, this.disposables);
|
||||
if (this.mouseSupport) {
|
||||
list.onMouseDown(this.onMouseDown, this, this.disposables);
|
||||
list.onContextMenu(this.onContextMenu, this, this.disposables);
|
||||
list.onMouseDblClick(this.onDoubleClick, this, this.disposables);
|
||||
list.onTouchStart(this.onMouseDown, this, this.disposables);
|
||||
Gesture.addTarget(list.getHTMLElement());
|
||||
}
|
||||
|
||||
list.onMouseClick(this.onPointer, this, this.disposables);
|
||||
list.onMouseMiddleClick(this.onPointer, this, this.disposables);
|
||||
view.onMouseDblClick(this.onDoubleClick, this, this.disposables);
|
||||
view.onTouchStart(this.onMouseDown, this, this.disposables);
|
||||
view.onTap(this.onPointer, this, this.disposables);
|
||||
Gesture.addTarget(view.domNode);
|
||||
list.onTap(this.onPointer, this, this.disposables);
|
||||
}
|
||||
|
||||
private isSelectionSingleChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
|
||||
protected isSelectionSingleChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
|
||||
if (this.multipleSelectionController) {
|
||||
return this.multipleSelectionController.isSelectionSingleChangeEvent(event);
|
||||
}
|
||||
@@ -540,7 +553,7 @@ class MouseController<T> implements IDisposable {
|
||||
return platform.isMacintosh ? event.browserEvent.metaKey : event.browserEvent.ctrlKey;
|
||||
}
|
||||
|
||||
private isSelectionRangeChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
|
||||
protected isSelectionRangeChangeEvent(event: IListMouseEvent<any> | IListTouchEvent<any>): boolean {
|
||||
if (this.multipleSelectionController) {
|
||||
return this.multipleSelectionController.isSelectionRangeChangeEvent(event);
|
||||
}
|
||||
@@ -554,16 +567,20 @@ class MouseController<T> implements IDisposable {
|
||||
|
||||
private onMouseDown(e: IListMouseEvent<T> | IListTouchEvent<T>): void {
|
||||
if (document.activeElement !== e.browserEvent.target) {
|
||||
this.view.domNode.focus();
|
||||
this.list.domFocus();
|
||||
}
|
||||
}
|
||||
|
||||
private onContextMenu(e: IListMouseEvent<T> | IListTouchEvent<T>): void {
|
||||
private onContextMenu(e: IListContextMenuEvent<T>): void {
|
||||
const focus = typeof e.index === 'undefined' ? [] : [e.index];
|
||||
this.list.setFocus(focus, e.browserEvent);
|
||||
}
|
||||
|
||||
private onPointer(e: IListMouseEvent<T>): void {
|
||||
protected onPointer(e: IListMouseEvent<T>): void {
|
||||
if (!this.mouseSupport) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInputElement(e.browserEvent.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
@@ -708,14 +725,14 @@ export class DefaultStyleController implements IStyleController {
|
||||
|
||||
if (styles.listFocusAndSelectionBackground) {
|
||||
content.push(`
|
||||
.monaco-list-drag-image,
|
||||
.monaco-drag-image,
|
||||
.monaco-list${suffix}:focus .monaco-list-row.selected.focused { background-color: ${styles.listFocusAndSelectionBackground}; }
|
||||
`);
|
||||
}
|
||||
|
||||
if (styles.listFocusAndSelectionForeground) {
|
||||
content.push(`
|
||||
.monaco-list-drag-image,
|
||||
.monaco-drag-image,
|
||||
.monaco-list${suffix}:focus .monaco-list-row.selected.focused { color: ${styles.listFocusAndSelectionForeground}; }
|
||||
`);
|
||||
}
|
||||
@@ -748,7 +765,7 @@ export class DefaultStyleController implements IStyleController {
|
||||
|
||||
if (styles.listFocusOutline) {
|
||||
content.push(`
|
||||
.monaco-list-drag-image,
|
||||
.monaco-drag-image,
|
||||
.monaco-list${suffix}:focus .monaco-list-row.focused { outline: 1px solid ${styles.listFocusOutline}; outline-offset: -1px; }
|
||||
`);
|
||||
}
|
||||
@@ -795,6 +812,7 @@ export interface IListOptions<T> extends IListStyles {
|
||||
readonly identityProvider?: IIdentityProvider<T>;
|
||||
readonly dnd?: IListDragAndDrop<T>;
|
||||
readonly enableKeyboardNavigation?: boolean;
|
||||
readonly automaticKeyboardNavigation?: boolean;
|
||||
readonly keyboardNavigationLabelProvider?: IKeyboardNavigationLabelProvider<T>;
|
||||
readonly ariaRole?: ListAriaRootRole;
|
||||
readonly ariaLabel?: string;
|
||||
@@ -812,6 +830,7 @@ export interface IListOptions<T> extends IListStyles {
|
||||
readonly supportDynamicHeights?: boolean;
|
||||
readonly mouseSupport?: boolean;
|
||||
readonly horizontalScrolling?: boolean;
|
||||
readonly ariaSetProvider?: IAriaSetProvider<T>;
|
||||
}
|
||||
|
||||
export interface IListStyles {
|
||||
@@ -957,20 +976,20 @@ class PipelineRenderer<T> implements IListRenderer<T, any> {
|
||||
return this.renderers.map(r => r.renderTemplate(container));
|
||||
}
|
||||
|
||||
renderElement(element: T, index: number, templateData: any[]): void {
|
||||
renderElement(element: T, index: number, templateData: any[], dynamicHeightProbing?: boolean): void {
|
||||
let i = 0;
|
||||
|
||||
for (const renderer of this.renderers) {
|
||||
renderer.renderElement(element, index, templateData[i++]);
|
||||
renderer.renderElement(element, index, templateData[i++], dynamicHeightProbing);
|
||||
}
|
||||
}
|
||||
|
||||
disposeElement(element: T, index: number, templateData: any[]): void {
|
||||
disposeElement(element: T, index: number, templateData: any[], dynamicHeightProbing?: boolean): void {
|
||||
let i = 0;
|
||||
|
||||
for (const renderer of this.renderers) {
|
||||
if (renderer.disposeElement) {
|
||||
renderer.disposeElement(element, index, templateData[i]);
|
||||
renderer.disposeElement(element, index, templateData[i], dynamicHeightProbing);
|
||||
}
|
||||
|
||||
i += 1;
|
||||
@@ -1058,13 +1077,11 @@ class ListViewDragAndDrop<T> implements IListViewDragAndDrop<T> {
|
||||
|
||||
export interface IListOptionsUpdate {
|
||||
readonly enableKeyboardNavigation?: boolean;
|
||||
readonly automaticKeyboardNavigation?: boolean;
|
||||
}
|
||||
|
||||
export class List<T> implements ISpliceable<T>, IDisposable {
|
||||
|
||||
private static InstanceCount = 0;
|
||||
private idPrefix = `list_id_${++List.InstanceCount}`;
|
||||
|
||||
private focus: Trait<T>;
|
||||
private selection: Trait<T>;
|
||||
private eventBufferer = new EventBufferer();
|
||||
@@ -1072,14 +1089,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
||||
private spliceable: ISpliceable<T>;
|
||||
private styleElement: HTMLStyleElement;
|
||||
private styleController: IStyleController;
|
||||
private mouseController: MouseController<T> | undefined;
|
||||
|
||||
get multipleSelectionController(): IMultipleSelectionController<T> {
|
||||
return (this.mouseController && this.mouseController.multipleSelectionController) || DefaultMultipleSelectionContoller;
|
||||
}
|
||||
|
||||
private _onDidUpdateOptions = new Emitter<IListOptions<T>>();
|
||||
readonly onDidUpdateOptions = this._onDidUpdateOptions.event;
|
||||
private typeLabelController?: TypeLabelController<T>;
|
||||
|
||||
protected disposables: IDisposable[];
|
||||
|
||||
@@ -1117,13 +1127,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
||||
.map(e => new StandardKeyboardEvent(e))
|
||||
.filter(e => this.didJustPressContextMenuKey = e.keyCode === KeyCode.ContextMenu || (e.shiftKey && e.keyCode === KeyCode.F10))
|
||||
.filter(e => { e.preventDefault(); e.stopPropagation(); return false; })
|
||||
.map(event => {
|
||||
const index = this.getFocus()[0];
|
||||
const element = this.view.element(index);
|
||||
const anchor = this.view.domElement(index) || undefined;
|
||||
return { index, element, anchor, browserEvent: event.browserEvent };
|
||||
})
|
||||
.event;
|
||||
.event as Event<any>;
|
||||
|
||||
const fromKeyup = Event.chain(domEvent(this.view.domNode, 'keyup'))
|
||||
.filter(() => {
|
||||
@@ -1131,14 +1135,13 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
||||
this.didJustPressContextMenuKey = false;
|
||||
return didJustPressContextMenuKey;
|
||||
})
|
||||
.filter(() => this.getFocus().length > 0)
|
||||
.filter(() => this.getFocus().length > 0 && !!this.view.domElement(this.getFocus()[0]))
|
||||
.map(browserEvent => {
|
||||
const index = this.getFocus()[0];
|
||||
const element = this.view.element(index);
|
||||
const anchor = this.view.domElement(index) || undefined;
|
||||
const anchor = this.view.domElement(index) as HTMLElement;
|
||||
return { index, element, anchor, browserEvent };
|
||||
})
|
||||
.filter(({ anchor }) => !!anchor)
|
||||
.event;
|
||||
|
||||
const fromMouse = Event.chain(this.view.onContextMenu)
|
||||
@@ -1165,7 +1168,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
||||
renderers: IListRenderer<any /* TODO@joao */, any>[],
|
||||
private _options: IListOptions<T> = DefaultOptions
|
||||
) {
|
||||
this.focus = new FocusTrait(i => this.getElementDomId(i));
|
||||
this.focus = new FocusTrait();
|
||||
this.selection = new Trait('selected');
|
||||
|
||||
mixin(_options, defaultStyles, false);
|
||||
@@ -1191,12 +1194,9 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
||||
this.view.domNode.setAttribute('role', _options.ariaRole);
|
||||
}
|
||||
|
||||
DOM.addClass(this.view.domNode, this.idPrefix);
|
||||
this.view.domNode.tabIndex = 0;
|
||||
|
||||
this.styleElement = DOM.createStyleSheet(this.view.domNode);
|
||||
|
||||
this.styleController = _options.styleController || new DefaultStyleController(this.styleElement, this.idPrefix);
|
||||
this.styleController = _options.styleController || new DefaultStyleController(this.styleElement, this.view.domId);
|
||||
|
||||
this.spliceable = new CombinedSpliceable([
|
||||
new TraitSpliceable(this.focus, this.view, _options.identityProvider),
|
||||
@@ -1217,14 +1217,11 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
||||
}
|
||||
|
||||
if (_options.keyboardNavigationLabelProvider) {
|
||||
const controller = new TypeLabelController(this, this.view, _options.keyboardNavigationLabelProvider);
|
||||
this.disposables.push(controller);
|
||||
this.typeLabelController = new TypeLabelController(this, this.view, _options.keyboardNavigationLabelProvider);
|
||||
this.disposables.push(this.typeLabelController);
|
||||
}
|
||||
|
||||
if (typeof _options.mouseSupport === 'boolean' ? _options.mouseSupport : true) {
|
||||
this.mouseController = new MouseController(this, this.view, _options);
|
||||
this.disposables.push(this.mouseController);
|
||||
}
|
||||
this.disposables.push(this.createMouseController(_options));
|
||||
|
||||
this.onFocusChange(this._onFocusChange, this, this.disposables);
|
||||
this.onSelectionChange(this._onSelectionChange, this, this.disposables);
|
||||
@@ -1236,9 +1233,16 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
||||
this.style(_options);
|
||||
}
|
||||
|
||||
protected createMouseController(options: IListOptions<T>): MouseController<T> {
|
||||
return new MouseController(this);
|
||||
}
|
||||
|
||||
updateOptions(optionsUpdate: IListOptionsUpdate = {}): void {
|
||||
this._options = { ...this._options, ...optionsUpdate };
|
||||
this._onDidUpdateOptions.fire(this._options);
|
||||
|
||||
if (this.typeLabelController) {
|
||||
this.typeLabelController.updateOptions(this._options);
|
||||
}
|
||||
}
|
||||
|
||||
get options(): IListOptions<T> {
|
||||
@@ -1265,6 +1269,10 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
||||
this.view.updateWidth(index);
|
||||
}
|
||||
|
||||
rerender(): void {
|
||||
this.view.rerender();
|
||||
}
|
||||
|
||||
element(index: number): T {
|
||||
return this.view.element(index);
|
||||
}
|
||||
@@ -1297,6 +1305,14 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
||||
return this.view.renderHeight;
|
||||
}
|
||||
|
||||
get firstVisibleIndex(): number {
|
||||
return this.view.firstVisibleIndex;
|
||||
}
|
||||
|
||||
get lastVisibleIndex(): number {
|
||||
return this.view.lastVisibleIndex;
|
||||
}
|
||||
|
||||
domFocus(): void {
|
||||
this.view.domNode.focus();
|
||||
}
|
||||
@@ -1305,6 +1321,12 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
||||
this.view.layout(height, width);
|
||||
}
|
||||
|
||||
toggleKeyboardNavigation(): void {
|
||||
if (this.typeLabelController) {
|
||||
this.typeLabelController.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
setSelection(indexes: number[], browserEvent?: UIEvent): void {
|
||||
for (const index of indexes) {
|
||||
if (index < 0 || index >= this.length) {
|
||||
@@ -1525,10 +1547,6 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
||||
return Math.abs((scrollTop - elementTop) / m);
|
||||
}
|
||||
|
||||
private getElementDomId(index: number): string {
|
||||
return `${this.idPrefix}_${index}`;
|
||||
}
|
||||
|
||||
isDOMFocused(): boolean {
|
||||
return this.view.domNode === document.activeElement;
|
||||
}
|
||||
@@ -1569,7 +1587,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
|
||||
const focus = this.focus.get();
|
||||
|
||||
if (focus.length > 0) {
|
||||
this.view.domNode.setAttribute('aria-activedescendant', this.getElementDomId(focus[0]));
|
||||
this.view.domNode.setAttribute('aria-activedescendant', this.view.getElementDomId(focus[0]));
|
||||
} else {
|
||||
this.view.domNode.removeAttribute('aria-activedescendant');
|
||||
}
|
||||
|
||||
@@ -117,7 +117,6 @@
|
||||
/* Context Menu */
|
||||
|
||||
.context-view.monaco-menu-container {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif;
|
||||
outline: 0;
|
||||
border: none;
|
||||
-webkit-animation: fadeIn 0.083s linear;
|
||||
@@ -182,7 +181,6 @@
|
||||
}
|
||||
|
||||
.menubar .menubar-menu-items-holder.monaco-menu-container {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "Ubuntu", "Droid Sans", sans-serif;
|
||||
outline: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import * as nls from 'vs/nls';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { IActionRunner, IAction, Action } from 'vs/base/common/actions';
|
||||
import { ActionBar, IActionItemProvider, ActionsOrientation, Separator, ActionItem, IActionItemOptions, BaseActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { ResolvedKeybinding, KeyCode, KeyCodeUtils } from 'vs/base/common/keyCodes';
|
||||
import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, hasClass, addDisposableListener, removeClass, append, $, addClasses, removeClasses } from 'vs/base/browser/dom';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
@@ -20,8 +20,22 @@ import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { isLinux } from 'vs/base/common/platform';
|
||||
|
||||
export const MENU_MNEMONIC_REGEX: RegExp = /\(&{1,2}(.)\)|&{1,2}(.)/;
|
||||
export const MENU_ESCAPED_MNEMONIC_REGEX: RegExp = /(?:&){1,2}(.)/;
|
||||
function createMenuMnemonicRegExp() {
|
||||
try {
|
||||
return new RegExp('\\(&([^\\s&])\\)|(?<!&)&([^\\s&])');
|
||||
} catch (err) {
|
||||
return new RegExp('\uFFFF'); // never match please
|
||||
}
|
||||
}
|
||||
export const MENU_MNEMONIC_REGEX = createMenuMnemonicRegExp();
|
||||
function createMenuEscapedMnemonicRegExp() {
|
||||
try {
|
||||
return new RegExp('(?<!&)(?:&)([^\\s&])');
|
||||
} catch (err) {
|
||||
return new RegExp('\uFFFF'); // never match please
|
||||
}
|
||||
}
|
||||
export const MENU_ESCAPED_MNEMONIC_REGEX: RegExp = createMenuEscapedMnemonicRegExp();
|
||||
|
||||
export interface IMenuOptions {
|
||||
context?: any;
|
||||
@@ -56,7 +70,7 @@ interface ISubMenuData {
|
||||
}
|
||||
|
||||
export class Menu extends ActionBar {
|
||||
private mnemonics: Map<KeyCode, Array<MenuActionItem>>;
|
||||
private mnemonics: Map<string, Array<MenuActionItem>>;
|
||||
private menuDisposables: IDisposable[];
|
||||
private scrollableElement: DomScrollableElement;
|
||||
private menuElement: HTMLElement;
|
||||
@@ -93,7 +107,7 @@ export class Menu extends ActionBar {
|
||||
|
||||
if (options.enableMnemonics) {
|
||||
this.menuDisposables.push(addDisposableListener(menuElement, EventType.KEY_DOWN, (e) => {
|
||||
const key = KeyCodeUtils.fromString(e.key);
|
||||
const key = e.key.toLocaleLowerCase();
|
||||
if (this.mnemonics.has(key)) {
|
||||
EventHelper.stop(e, true);
|
||||
const actions = this.mnemonics.get(key)!;
|
||||
@@ -175,7 +189,7 @@ export class Menu extends ActionBar {
|
||||
parent: this
|
||||
};
|
||||
|
||||
this.mnemonics = new Map<KeyCode, Array<MenuActionItem>>();
|
||||
this.mnemonics = new Map<string, Array<MenuActionItem>>();
|
||||
|
||||
this.push(actions, { icon: true, label: true, isMenu: true });
|
||||
|
||||
@@ -349,7 +363,7 @@ class MenuActionItem extends BaseActionItem {
|
||||
|
||||
private label: HTMLElement;
|
||||
private check: HTMLElement;
|
||||
private mnemonic: KeyCode;
|
||||
private mnemonic: string;
|
||||
private cssClass: string;
|
||||
protected menuStyle: IMenuStyles;
|
||||
|
||||
@@ -368,7 +382,7 @@ class MenuActionItem extends BaseActionItem {
|
||||
if (label) {
|
||||
let matches = MENU_MNEMONIC_REGEX.exec(label);
|
||||
if (matches) {
|
||||
this.mnemonic = KeyCodeUtils.fromString((!!matches[1] ? matches[1] : matches[2]).toLocaleLowerCase());
|
||||
this.mnemonic = (!!matches[1] ? matches[1] : matches[2]).toLocaleLowerCase();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -440,13 +454,16 @@ class MenuActionItem extends BaseActionItem {
|
||||
label = cleanLabel;
|
||||
}
|
||||
|
||||
this.label.setAttribute('aria-label', cleanLabel);
|
||||
this.label.setAttribute('aria-label', cleanLabel.replace(/&&/g, '&'));
|
||||
|
||||
const matches = MENU_MNEMONIC_REGEX.exec(label);
|
||||
|
||||
if (matches) {
|
||||
label = strings.escape(label).replace(MENU_ESCAPED_MNEMONIC_REGEX, '<u aria-hidden="true">$1</u>');
|
||||
label = label.replace(/&&/g, '&');
|
||||
this.item.setAttribute('aria-keyshortcuts', (!!matches[1] ? matches[1] : matches[2]).toLocaleLowerCase());
|
||||
} else {
|
||||
label = label.replace(/&&/g, '&');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,7 +536,7 @@ class MenuActionItem extends BaseActionItem {
|
||||
}
|
||||
}
|
||||
|
||||
getMnemonic(): KeyCode {
|
||||
getMnemonic(): string {
|
||||
return this.mnemonic;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,15 +14,17 @@ import { cleanMnemonic, IMenuOptions, Menu, MENU_ESCAPED_MNEMONIC_REGEX, MENU_MN
|
||||
import { ActionRunner, IAction, IActionRunner } from 'vs/base/common/actions';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { KeyCode, KeyCodeUtils, ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { KeyCode, ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
|
||||
const $ = DOM.$;
|
||||
|
||||
export interface IMenuBarOptions {
|
||||
enableMnemonics?: boolean;
|
||||
visibility?: string;
|
||||
getKeybinding?: (action: IAction) => ResolvedKeybinding;
|
||||
getKeybinding?: (action: IAction) => ResolvedKeybinding | undefined;
|
||||
alwaysOnMnemonics?: boolean;
|
||||
}
|
||||
|
||||
export interface MenuBarMenu {
|
||||
@@ -69,14 +71,14 @@ export class MenuBar extends Disposable {
|
||||
private openedViaKeyboard: boolean;
|
||||
private awaitingAltRelease: boolean;
|
||||
private ignoreNextMouseUp: boolean;
|
||||
private mnemonics: Map<KeyCode, number>;
|
||||
private mnemonics: Map<string, number>;
|
||||
|
||||
private updatePending: boolean;
|
||||
private _focusState: MenubarState;
|
||||
private actionRunner: IActionRunner;
|
||||
|
||||
private _onVisibilityChange: Emitter<boolean>;
|
||||
private _onFocusStateChange: Emitter<boolean>;
|
||||
private readonly _onVisibilityChange: Emitter<boolean>;
|
||||
private readonly _onFocusStateChange: Emitter<boolean>;
|
||||
|
||||
private numMenusShown: number;
|
||||
private menuStyle: IMenuStyles;
|
||||
@@ -85,10 +87,10 @@ export class MenuBar extends Disposable {
|
||||
constructor(private container: HTMLElement, private options: IMenuBarOptions = {}) {
|
||||
super();
|
||||
|
||||
this.container.attributes['role'] = 'menubar';
|
||||
this.container.setAttribute('role', 'menubar');
|
||||
|
||||
this.menuCache = [];
|
||||
this.mnemonics = new Map<KeyCode, number>();
|
||||
this.mnemonics = new Map<string, number>();
|
||||
|
||||
this._focusState = MenubarState.VISIBLE;
|
||||
|
||||
@@ -109,7 +111,7 @@ export class MenuBar extends Disposable {
|
||||
this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_DOWN, (e) => {
|
||||
let event = new StandardKeyboardEvent(e as KeyboardEvent);
|
||||
let eventHandled = true;
|
||||
const key = !!e.key ? KeyCodeUtils.fromString(e.key) : KeyCode.Unknown;
|
||||
const key = !!e.key ? e.key.toLocaleLowerCase() : '';
|
||||
|
||||
if (event.equals(KeyCode.LeftArrow)) {
|
||||
this.focusPrevious();
|
||||
@@ -150,11 +152,14 @@ export class MenuBar extends Disposable {
|
||||
this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_OUT, (e) => {
|
||||
let event = e as FocusEvent;
|
||||
|
||||
if (event.relatedTarget) {
|
||||
if (!this.container.contains(event.relatedTarget as HTMLElement)) {
|
||||
this.focusToReturn = undefined;
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
// We are losing focus and there is no related target, e.g. webview case
|
||||
if (!event.relatedTarget) {
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
// We are losing focus and there is a target, reset focusToReturn value as not to redirect
|
||||
else if (event.relatedTarget && !this.container.contains(event.relatedTarget as HTMLElement)) {
|
||||
this.focusToReturn = undefined;
|
||||
this.setUnfocusedState();
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -163,7 +168,7 @@ export class MenuBar extends Disposable {
|
||||
return;
|
||||
}
|
||||
|
||||
const key = KeyCodeUtils.fromString(e.key);
|
||||
const key = e.key.toLocaleLowerCase();
|
||||
if (!this.mnemonics.has(key)) {
|
||||
return;
|
||||
}
|
||||
@@ -459,8 +464,8 @@ export class MenuBar extends Disposable {
|
||||
|
||||
// Update the button label to reflect mnemonics
|
||||
titleElement.innerHTML = this.options.enableMnemonics ?
|
||||
strings.escape(label).replace(MENU_ESCAPED_MNEMONIC_REGEX, '<mnemonic aria-hidden="true">$1</mnemonic>') :
|
||||
cleanMenuLabel;
|
||||
strings.escape(label).replace(MENU_ESCAPED_MNEMONIC_REGEX, '<mnemonic aria-hidden="true">$1</mnemonic>').replace(/&&/g, '&') :
|
||||
cleanMenuLabel.replace(/&&/g, '&');
|
||||
|
||||
let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(label);
|
||||
|
||||
@@ -506,7 +511,7 @@ export class MenuBar extends Disposable {
|
||||
}
|
||||
|
||||
private registerMnemonic(menuIndex: number, mnemonic: string): void {
|
||||
this.mnemonics.set(KeyCodeUtils.fromString(mnemonic), menuIndex);
|
||||
this.mnemonics.set(mnemonic.toLocaleLowerCase(), menuIndex);
|
||||
}
|
||||
|
||||
private hideMenubar(): void {
|
||||
@@ -725,7 +730,7 @@ export class MenuBar extends Disposable {
|
||||
if (menuBarMenu.titleElement.children.length) {
|
||||
let child = menuBarMenu.titleElement.children.item(0) as HTMLElement;
|
||||
if (child) {
|
||||
child.style.textDecoration = visible ? 'underline' : null;
|
||||
child.style.textDecoration = (this.options.alwaysOnMnemonics || visible) ? 'underline' : null;
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -852,8 +857,8 @@ export class MenuBar extends Disposable {
|
||||
let menuOptions: IMenuOptions = {
|
||||
getKeyBinding: this.options.getKeybinding,
|
||||
actionRunner: this.actionRunner,
|
||||
enableMnemonics: this.mnemonicsInUse && this.options.enableMnemonics,
|
||||
ariaLabel: customMenu.buttonElement.attributes['aria-label'].value
|
||||
enableMnemonics: this.options.alwaysOnMnemonics || (this.mnemonicsInUse && this.options.enableMnemonics),
|
||||
ariaLabel: withNullAsUndefined(customMenu.buttonElement.getAttribute('aria-label'))
|
||||
};
|
||||
|
||||
let menuWidget = this._register(new Menu(menuHolder, customMenu.actions, menuOptions));
|
||||
@@ -955,6 +960,16 @@ class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
}));
|
||||
|
||||
this._subscriptions.push(domEvent(document.body, 'mouseup', true)(e => {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
}));
|
||||
|
||||
this._subscriptions.push(domEvent(document.body, 'mousemove', true)(e => {
|
||||
if (e.buttons) {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
}
|
||||
}));
|
||||
|
||||
this._subscriptions.push(domEvent(window, 'blur')(e => {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
this._keyStatus.lastKeyReleased = undefined;
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
|
||||
.monaco-sash.disabled {
|
||||
cursor: default !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/** Touch **/
|
||||
@@ -98,10 +99,14 @@
|
||||
|
||||
/** Debug **/
|
||||
|
||||
.monaco-sash.debug:not(.disabled) {
|
||||
.monaco-sash.debug {
|
||||
background: cyan;
|
||||
}
|
||||
|
||||
.monaco-sash.debug.disabled {
|
||||
background: rgba(0, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.monaco-sash.debug:not(.disabled).orthogonal-start::before,
|
||||
.monaco-sash.debug:not(.disabled).orthogonal-end::after {
|
||||
background: red;
|
||||
|
||||
@@ -22,9 +22,9 @@ export interface ISelectBoxDelegate {
|
||||
|
||||
// Public SelectBox Interface
|
||||
readonly onDidSelect: Event<ISelectData>;
|
||||
setOptions(options: ISelectOptionItem[], selected?: number);
|
||||
setOptions(options: ISelectOptionItem[], selected?: number): void;
|
||||
select(index: number): void;
|
||||
setAriaLabel(label: string);
|
||||
setAriaLabel(label: string): void;
|
||||
focus(): void;
|
||||
blur(): void;
|
||||
dispose(): void;
|
||||
|
||||
@@ -124,12 +124,6 @@ export class SelectBoxList implements ISelectBoxDelegate, IListVirtualDelegate<I
|
||||
|
||||
this.selectElement = document.createElement('select');
|
||||
|
||||
// Workaround for Electron 2.x
|
||||
// Native select should not require explicit role attribute, however, Electron 2.x
|
||||
// incorrectly exposes select as menuItem which interferes with labeling and results
|
||||
// in the unlabeled not been read. Electron 3 appears to fix.
|
||||
this.selectElement.setAttribute('role', 'combobox');
|
||||
|
||||
// Use custom CSS vars for padding calculation
|
||||
this.selectElement.className = 'monaco-select-box monaco-select-box-dropdown-padding';
|
||||
|
||||
@@ -430,6 +424,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IListVirtualDelegate<I
|
||||
// ContextView dropdown methods
|
||||
|
||||
private showSelectDropDown() {
|
||||
this.selectionDetailsPane.innerText = '';
|
||||
|
||||
if (!this.contextViewProvider || this._isVisible) {
|
||||
return;
|
||||
@@ -439,8 +434,6 @@ export class SelectBoxList implements ISelectBoxDelegate, IListVirtualDelegate<I
|
||||
this.createSelectList(this.selectDropDownContainer);
|
||||
this.setOptionsList();
|
||||
|
||||
this.cloneElementFont(this.selectElement, this.selectDropDownContainer);
|
||||
|
||||
// This allows us to flip the position based on measurement
|
||||
// Set drop-down position above/below from required height and margins
|
||||
// If pre-layout cannot fit at least one option do not show drop-down
|
||||
@@ -581,7 +574,8 @@ export class SelectBoxList implements ISelectBoxDelegate, IListVirtualDelegate<I
|
||||
|
||||
this.selectDropDownContainer.style.width = selectOptimalWidth;
|
||||
|
||||
// Get initial list height and determine space ab1you knowove and below
|
||||
// Get initial list height and determine space above and below
|
||||
this.selectList.getHTMLElement().style.height = '';
|
||||
this.selectList.layout();
|
||||
let listHeight = this.selectList.contentHeight;
|
||||
|
||||
@@ -680,6 +674,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IListVirtualDelegate<I
|
||||
if (this._hasDetails) {
|
||||
// Leave the selectDropDownContainer to size itself according to children (list + details) - #57447
|
||||
this.selectList.getHTMLElement().style.height = (listHeight + verticalPadding) + 'px';
|
||||
this.selectDropDownContainer.style.height = '';
|
||||
} else {
|
||||
this.selectDropDownContainer.style.height = (listHeight + verticalPadding) + 'px';
|
||||
}
|
||||
@@ -720,13 +715,6 @@ export class SelectBoxList implements ISelectBoxDelegate, IListVirtualDelegate<I
|
||||
return elementWidth;
|
||||
}
|
||||
|
||||
private cloneElementFont(source: HTMLElement, target: HTMLElement) {
|
||||
const fontSize = window.getComputedStyle(source, null).getPropertyValue('font-size');
|
||||
const fontFamily = window.getComputedStyle(source, null).getPropertyValue('font-family');
|
||||
target.style.fontFamily = fontFamily;
|
||||
target.style.fontSize = fontSize;
|
||||
}
|
||||
|
||||
private createSelectList(parent: HTMLElement): void {
|
||||
|
||||
// If we have already constructive list on open, skip
|
||||
|
||||
@@ -30,12 +30,6 @@ export class SelectBoxNative implements ISelectBoxDelegate {
|
||||
|
||||
this.selectElement = document.createElement('select');
|
||||
|
||||
// Workaround for Electron 2.x
|
||||
// Native select should not require explicit role attribute, however, Electron 2.x
|
||||
// incorrectly exposes select as menuItem which interferes with labeling and results
|
||||
// in the unlabeled not been read. Electron 3 appears to fix.
|
||||
this.selectElement.setAttribute('role', 'combobox');
|
||||
|
||||
this.selectElement.className = 'monaco-select-box';
|
||||
|
||||
if (typeof this.selectBoxOptions.ariaLabel === 'string') {
|
||||
|
||||
@@ -42,9 +42,10 @@ export abstract class Panel implements IView {
|
||||
private static readonly HEADER_SIZE = 22;
|
||||
|
||||
readonly element: HTMLElement;
|
||||
private header: HTMLElement;
|
||||
private body: HTMLElement;
|
||||
|
||||
protected _expanded: boolean;
|
||||
protected disposables: IDisposable[] = [];
|
||||
|
||||
private expandedSize: number | undefined = undefined;
|
||||
private _headerVisible = true;
|
||||
@@ -52,12 +53,13 @@ export abstract class Panel implements IView {
|
||||
private _maximumBodySize: number;
|
||||
private ariaHeaderLabel: string;
|
||||
private styles: IPanelStyles = {};
|
||||
|
||||
private header: HTMLElement;
|
||||
private animationTimer: number | undefined = undefined;
|
||||
|
||||
private _onDidChange = new Emitter<number | undefined>();
|
||||
readonly onDidChange: Event<number | undefined> = this._onDidChange.event;
|
||||
|
||||
protected disposables: IDisposable[] = [];
|
||||
|
||||
get draggableElement(): HTMLElement {
|
||||
return this.header;
|
||||
}
|
||||
@@ -131,8 +133,19 @@ export abstract class Panel implements IView {
|
||||
|
||||
this._expanded = !!expanded;
|
||||
this.updateHeader();
|
||||
this._onDidChange.fire(expanded ? this.expandedSize : undefined);
|
||||
|
||||
if (expanded) {
|
||||
if (typeof this.animationTimer === 'number') {
|
||||
clearTimeout(this.animationTimer);
|
||||
}
|
||||
append(this.element, this.body);
|
||||
} else {
|
||||
this.animationTimer = window.setTimeout(() => {
|
||||
this.body.remove();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
this._onDidChange.fire(expanded ? this.expandedSize : undefined);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -159,8 +172,9 @@ export abstract class Panel implements IView {
|
||||
this.renderHeader(this.header);
|
||||
|
||||
const focusTracker = trackFocus(this.header);
|
||||
focusTracker.onDidFocus(() => addClass(this.header, 'focused'));
|
||||
focusTracker.onDidBlur(() => removeClass(this.header, 'focused'));
|
||||
this.disposables.push(focusTracker);
|
||||
focusTracker.onDidFocus(() => addClass(this.header, 'focused'), null, this.disposables);
|
||||
focusTracker.onDidBlur(() => removeClass(this.header, 'focused'), null, this.disposables);
|
||||
|
||||
this.updateHeader();
|
||||
|
||||
@@ -179,15 +193,8 @@ export abstract class Panel implements IView {
|
||||
domEvent(this.header, 'click')
|
||||
(() => this.setExpanded(!this.isExpanded()), null, this.disposables);
|
||||
|
||||
// TODO@Joao move this down to panelview
|
||||
// onHeaderKeyDown.filter(e => e.keyCode === KeyCode.UpArrow)
|
||||
// .event(focusPrevious, this, this.disposables);
|
||||
|
||||
// onHeaderKeyDown.filter(e => e.keyCode === KeyCode.DownArrow)
|
||||
// .event(focusNext, this, this.disposables);
|
||||
|
||||
const body = append(this.element, $('.panel-body'));
|
||||
this.renderBody(body);
|
||||
this.body = append(this.element, $('.panel-body'));
|
||||
this.renderBody(this.body);
|
||||
}
|
||||
|
||||
layout(height: number): void {
|
||||
@@ -268,7 +275,7 @@ class PanelDraggable extends Disposable {
|
||||
|
||||
e.dataTransfer.effectAllowed = 'move';
|
||||
|
||||
const dragImage = append(document.body, $('.monaco-panel-drag-image', {}, this.panel.draggableElement.textContent || ''));
|
||||
const dragImage = append(document.body, $('.monaco-drag-image', {}, this.panel.draggableElement.textContent || ''));
|
||||
e.dataTransfer.setDragImage(dragImage, -10, -10);
|
||||
setTimeout(() => document.body.removeChild(dragImage), 0);
|
||||
|
||||
@@ -373,7 +380,7 @@ export class PanelView extends Disposable {
|
||||
private panelItems: IPanelItem[] = [];
|
||||
private width: number;
|
||||
private splitview: SplitView;
|
||||
private animationTimer: number | null = null;
|
||||
private animationTimer: number | undefined = undefined;
|
||||
|
||||
private _onDidDrop = this._register(new Emitter<{ from: Panel, to: Panel }>());
|
||||
readonly onDidDrop: Event<{ from: Panel, to: Panel }> = this._onDidDrop.event;
|
||||
@@ -475,7 +482,7 @@ export class PanelView extends Disposable {
|
||||
addClass(this.el, 'animated');
|
||||
|
||||
this.animationTimer = window.setTimeout(() => {
|
||||
this.animationTimer = null;
|
||||
this.animationTimer = undefined;
|
||||
removeClass(this.el, 'animated');
|
||||
}, 200);
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ export interface IView {
|
||||
readonly maximumSize: number;
|
||||
readonly onDidChange: Event<number | undefined>;
|
||||
readonly priority?: LayoutPriority;
|
||||
readonly snapSize?: number;
|
||||
layout(size: number, orientation: Orientation): void;
|
||||
}
|
||||
|
||||
@@ -227,7 +226,7 @@ export class SplitView extends Disposable {
|
||||
// Add sash
|
||||
if (this.viewItems.length > 1) {
|
||||
const orientation = this.orientation === Orientation.VERTICAL ? Orientation.HORIZONTAL : Orientation.VERTICAL;
|
||||
const layoutProvider = this.orientation === Orientation.VERTICAL ? { getHorizontalSashTop: sash => this.getSashPosition(sash) } : { getVerticalSashLeft: sash => this.getSashPosition(sash) };
|
||||
const layoutProvider = this.orientation === Orientation.VERTICAL ? { getHorizontalSashTop: (sash: Sash) => this.getSashPosition(sash) } : { getVerticalSashLeft: (sash: Sash) => this.getSashPosition(sash) };
|
||||
const sash = new Sash(this.sashContainer, layoutProvider, {
|
||||
orientation,
|
||||
orthogonalStartSash: this.orthogonalStartSash,
|
||||
@@ -352,7 +351,7 @@ export class SplitView extends Disposable {
|
||||
} else {
|
||||
for (let i = 0; i < this.viewItems.length; i++) {
|
||||
const item = this.viewItems[i];
|
||||
item.size = SplitView.clamp(item, Math.round(this.proportions[i] * size));
|
||||
item.size = clamp(Math.round(this.proportions[i] * size), item.view.minimumSize, item.view.maximumSize);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,7 +443,7 @@ export class SplitView extends Disposable {
|
||||
}
|
||||
|
||||
size = typeof size === 'number' ? size : item.size;
|
||||
size = SplitView.clamp(item, size);
|
||||
size = clamp(size, item.view.minimumSize, item.view.maximumSize);
|
||||
|
||||
if (this.inverseAltBehavior && index > 0) {
|
||||
// In this case, we want the view to grow or shrink both sides equally
|
||||
@@ -551,29 +550,27 @@ export class SplitView extends Disposable {
|
||||
const downItems = downIndexes.map(i => this.viewItems[i]);
|
||||
const downSizes = downIndexes.map(i => sizes[i]);
|
||||
|
||||
const minDeltaUp = upIndexes.reduce((r, i) => r + ((typeof this.viewItems[i].view.snapSize === 'number' ? 0 : this.viewItems[i].view.minimumSize) - sizes[i]), 0);
|
||||
const minDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].view.minimumSize - sizes[i]), 0);
|
||||
const maxDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].view.maximumSize - sizes[i]), 0);
|
||||
const maxDeltaDown = downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - (typeof this.viewItems[i].view.snapSize === 'number' ? 0 : this.viewItems[i].view.minimumSize)), 0);
|
||||
const maxDeltaDown = downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].view.minimumSize), 0);
|
||||
const minDeltaDown = downIndexes.length === 0 ? Number.NEGATIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].view.maximumSize), 0);
|
||||
const minDelta = Math.max(minDeltaUp, minDeltaDown, overloadMinDelta);
|
||||
const maxDelta = Math.min(maxDeltaDown, maxDeltaUp, overloadMaxDelta);
|
||||
|
||||
const tentativeDelta = clamp(delta, minDelta, maxDelta);
|
||||
let actualDelta = 0;
|
||||
delta = clamp(delta, minDelta, maxDelta);
|
||||
|
||||
for (let i = 0, deltaUp = tentativeDelta; i < upItems.length; i++) {
|
||||
for (let i = 0, deltaUp = delta; i < upItems.length; i++) {
|
||||
const item = upItems[i];
|
||||
const size = SplitView.clamp(item, upSizes[i] + deltaUp/* , upIndexes[i] === index */);
|
||||
const size = clamp(upSizes[i] + deltaUp, item.view.minimumSize, item.view.maximumSize);
|
||||
const viewDelta = size - upSizes[i];
|
||||
|
||||
actualDelta += viewDelta;
|
||||
deltaUp -= viewDelta;
|
||||
item.size = size;
|
||||
}
|
||||
|
||||
for (let i = 0, deltaDown = actualDelta; i < downItems.length; i++) {
|
||||
for (let i = 0, deltaDown = delta; i < downItems.length; i++) {
|
||||
const item = downItems[i];
|
||||
const size = SplitView.clamp(item, downSizes[i] - deltaDown);
|
||||
const size = clamp(downSizes[i] - deltaDown, item.view.minimumSize, item.view.maximumSize);
|
||||
const viewDelta = size - downSizes[i];
|
||||
|
||||
deltaDown += viewDelta;
|
||||
@@ -583,24 +580,13 @@ export class SplitView extends Disposable {
|
||||
return delta;
|
||||
}
|
||||
|
||||
private static clamp(item: IViewItem, size: number): number {
|
||||
const result = clamp(size, item.view.minimumSize, item.view.maximumSize);
|
||||
|
||||
if (typeof item.view.snapSize !== 'number' || size >= item.view.minimumSize) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const snapSize = Math.min(item.view.snapSize, item.view.minimumSize);
|
||||
return size < snapSize ? 0 : item.view.minimumSize;
|
||||
}
|
||||
|
||||
private distributeEmptySpace(): void {
|
||||
let contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
|
||||
let emptyDelta = this.size - contentSize;
|
||||
|
||||
for (let i = this.viewItems.length - 1; emptyDelta !== 0 && i >= 0; i--) {
|
||||
const item = this.viewItems[i];
|
||||
const size = SplitView.clamp(item, item.size + emptyDelta);
|
||||
const size = clamp(item.size + emptyDelta, item.view.minimumSize, item.view.maximumSize);
|
||||
const viewDelta = size - item.size;
|
||||
|
||||
emptyDelta -= viewDelta;
|
||||
|
||||
@@ -11,6 +11,7 @@ import { IContextMenuProvider, DropdownMenuActionItem } from 'vs/base/browser/ui
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
|
||||
export const CONTEXT = 'context.toolbar';
|
||||
|
||||
@@ -135,7 +136,7 @@ export class ToolBar extends Disposable {
|
||||
private getKeybindingLabel(action: IAction): string | undefined {
|
||||
const key = this.lookupKeybindings && this.options.getKeyBinding ? this.options.getKeyBinding(action) : undefined;
|
||||
|
||||
return (key && key.getLabel()) || undefined;
|
||||
return withNullAsUndefined(key && key.getLabel());
|
||||
}
|
||||
|
||||
addPrimaryAction(primaryAction: IAction): () => void {
|
||||
|
||||
@@ -5,16 +5,16 @@
|
||||
|
||||
import 'vs/css!./media/tree';
|
||||
import { IDisposable, dispose, Disposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { IListOptions, List, IListStyles, mightProducePrintableCharacter } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListEvent, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IKeyboardNavigationLabelProvider } from 'vs/base/browser/ui/list/list';
|
||||
import { IListOptions, List, IListStyles, mightProducePrintableCharacter, MouseController } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListEvent, IListContextMenuEvent, IListDragAndDrop, IListDragOverReaction, IKeyboardNavigationLabelProvider, IIdentityProvider } from 'vs/base/browser/ui/list/list';
|
||||
import { append, $, toggleClass, getDomNodePagePosition, removeClass, addClass, hasClass } from 'vs/base/browser/dom';
|
||||
import { Event, Relay, Emitter, EventBufferer } from 'vs/base/common/event';
|
||||
import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop, TreeDragOverBubble, TreeVisibility, TreeFilterResult } from 'vs/base/browser/ui/tree/tree';
|
||||
import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter, ITreeNavigator, ICollapseStateChangeEvent, ITreeDragAndDrop, TreeDragOverBubble, TreeVisibility, TreeFilterResult, ITreeModelSpliceEvent } from 'vs/base/browser/ui/tree/tree';
|
||||
import { ISpliceable } from 'vs/base/common/sequence';
|
||||
import { IDragAndDropData, StaticDND, DragAndDropData } from 'vs/base/browser/dnd';
|
||||
import { range } from 'vs/base/common/arrays';
|
||||
import { range, equals } from 'vs/base/common/arrays';
|
||||
import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { fuzzyScore, FuzzyScore } from 'vs/base/common/filters';
|
||||
@@ -150,7 +150,15 @@ function asListOptions<T, TFilterData, TRef>(modelProvider: () => ITreeModel<T,
|
||||
return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel(node.element);
|
||||
}
|
||||
},
|
||||
enableKeyboardNavigation: options.simpleKeyboardNavigation
|
||||
enableKeyboardNavigation: options.simpleKeyboardNavigation,
|
||||
ariaSetProvider: {
|
||||
getSetSize(node) {
|
||||
return node.parent!.visibleChildrenCount;
|
||||
},
|
||||
getPosInSet(node) {
|
||||
return node.visibleChildIndex + 1;
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -188,7 +196,7 @@ class TreeRenderer<T, TFilterData, TTemplateData> implements IListRenderer<ITree
|
||||
readonly templateId: string;
|
||||
private renderedElements = new Map<T, ITreeNode<T, TFilterData>>();
|
||||
private renderedNodes = new Map<ITreeNode<T, TFilterData>, ITreeListTemplateData<TTemplateData>>();
|
||||
private indent: number;
|
||||
private indent: number = TreeRenderer.DefaultIndent;
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
constructor(
|
||||
@@ -207,7 +215,9 @@ class TreeRenderer<T, TFilterData, TTemplateData> implements IListRenderer<ITree
|
||||
}
|
||||
|
||||
updateOptions(options: ITreeRendererOptions = {}): void {
|
||||
this.indent = typeof options.indent === 'number' ? clamp(options.indent, 0, 20) : TreeRenderer.DefaultIndent;
|
||||
if (typeof options.indent !== 'undefined') {
|
||||
this.indent = clamp(options.indent, 0, 40);
|
||||
}
|
||||
|
||||
this.renderedNodes.forEach((templateData, node) => {
|
||||
templateData.twistie.style.marginLeft = `${node.depth * this.indent}px`;
|
||||
@@ -223,25 +233,28 @@ class TreeRenderer<T, TFilterData, TTemplateData> implements IListRenderer<ITree
|
||||
return { container, twistie, templateData };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>): void {
|
||||
this.renderedNodes.set(node, templateData);
|
||||
this.renderedElements.set(node.element, node);
|
||||
renderElement(node: ITreeNode<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>, dynamicHeightProbing?: boolean): void {
|
||||
if (!dynamicHeightProbing) {
|
||||
this.renderedNodes.set(node, templateData);
|
||||
this.renderedElements.set(node.element, node);
|
||||
}
|
||||
|
||||
const indent = TreeRenderer.DefaultIndent + (node.depth - 1) * this.indent;
|
||||
templateData.twistie.style.marginLeft = `${indent}px`;
|
||||
templateData.container.setAttribute('aria-posinset', String(node.visibleChildIndex + 1));
|
||||
templateData.container.setAttribute('aria-setsize', String(node.parent!.visibleChildrenCount));
|
||||
this.update(node, templateData);
|
||||
|
||||
this.renderer.renderElement(node, index, templateData.templateData);
|
||||
this.renderer.renderElement(node, index, templateData.templateData, dynamicHeightProbing);
|
||||
}
|
||||
|
||||
disposeElement(node: ITreeNode<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>): void {
|
||||
disposeElement(node: ITreeNode<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>, dynamicHeightProbing?: boolean): void {
|
||||
if (this.renderer.disposeElement) {
|
||||
this.renderer.disposeElement(node, index, templateData.templateData);
|
||||
this.renderer.disposeElement(node, index, templateData.templateData, dynamicHeightProbing);
|
||||
}
|
||||
|
||||
if (!dynamicHeightProbing) {
|
||||
this.renderedNodes.delete(node);
|
||||
this.renderedElements.delete(node.element);
|
||||
}
|
||||
this.renderedNodes.delete(node);
|
||||
this.renderedElements.set(node.element);
|
||||
}
|
||||
|
||||
disposeTemplate(templateData: ITreeListTemplateData<TTemplateData>): void {
|
||||
@@ -386,6 +399,15 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
|
||||
private _pattern = '';
|
||||
get pattern(): string { return this._pattern; }
|
||||
|
||||
private _filterOnType: boolean;
|
||||
get filterOnType(): boolean { return this._filterOnType; }
|
||||
|
||||
private _empty: boolean;
|
||||
get empty(): boolean { return this._empty; }
|
||||
|
||||
private _onDidChangeEmptyState = new Emitter<boolean>();
|
||||
readonly onDidChangeEmptyState: Event<boolean> = Event.latch(this._onDidChangeEmptyState.event);
|
||||
|
||||
private positionClassName = 'ne';
|
||||
private domNode: HTMLElement;
|
||||
private messageDomNode: HTMLElement;
|
||||
@@ -394,9 +416,12 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
|
||||
private clearDomNode: HTMLElement;
|
||||
private keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter;
|
||||
|
||||
private automaticKeyboardNavigation: boolean;
|
||||
private automaticKeyboardNavigation = true;
|
||||
private triggered = false;
|
||||
|
||||
private _onDidChangePattern = new Emitter<string>();
|
||||
readonly onDidChangePattern = this._onDidChangePattern.event;
|
||||
|
||||
private enabledDisposables: IDisposable[] = [];
|
||||
private disposables: IDisposable[] = [];
|
||||
|
||||
@@ -416,9 +441,10 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
|
||||
this.labelDomNode = append(this.domNode, $('span.label'));
|
||||
const controls = append(this.domNode, $('.controls'));
|
||||
|
||||
this._filterOnType = !!tree.options.filterOnType;
|
||||
this.filterOnTypeDomNode = append(controls, $<HTMLInputElement>('input.filter'));
|
||||
this.filterOnTypeDomNode.type = 'checkbox';
|
||||
this.filterOnTypeDomNode.checked = !!tree.options.filterOnType;
|
||||
this.filterOnTypeDomNode.checked = this._filterOnType;
|
||||
this.filterOnTypeDomNode.tabIndex = -1;
|
||||
this.updateFilterOnTypeTitle();
|
||||
domEvent(this.filterOnTypeDomNode, 'input')(this.onDidChangeFilterOnType, this, this.disposables);
|
||||
@@ -440,8 +466,15 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
|
||||
this.enable();
|
||||
}
|
||||
|
||||
this.filterOnTypeDomNode.checked = !!options.filterOnType;
|
||||
this.automaticKeyboardNavigation = typeof options.automaticKeyboardNavigation === 'undefined' ? true : options.automaticKeyboardNavigation;
|
||||
if (typeof options.filterOnType !== 'undefined') {
|
||||
this._filterOnType = !!options.filterOnType;
|
||||
this.filterOnTypeDomNode.checked = this._filterOnType;
|
||||
}
|
||||
|
||||
if (typeof options.automaticKeyboardNavigation !== 'undefined') {
|
||||
this.automaticKeyboardNavigation = options.automaticKeyboardNavigation;
|
||||
}
|
||||
|
||||
this.tree.refilter();
|
||||
this.render();
|
||||
|
||||
@@ -466,10 +499,10 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
|
||||
const isPrintableCharEvent = this.keyboardNavigationLabelProvider.mightProducePrintableCharacter ? (e: IKeyboardEvent) => this.keyboardNavigationLabelProvider.mightProducePrintableCharacter!(e) : (e: IKeyboardEvent) => mightProducePrintableCharacter(e);
|
||||
const onKeyDown = Event.chain(domEvent(this.view.getHTMLElement(), 'keydown'))
|
||||
.filter(e => !isInputElement(e.target as HTMLElement) || e.target === this.filterOnTypeDomNode)
|
||||
.map(e => new StandardKeyboardEvent(e))
|
||||
.filter(this.keyboardNavigationEventFilter || (() => true))
|
||||
.filter(() => this.automaticKeyboardNavigation || this.triggered)
|
||||
.map(e => new StandardKeyboardEvent(e))
|
||||
.filter(e => isPrintableCharEvent(e) || ((this._pattern.length > 0 || this.triggered) && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? e.altKey : e.ctrlKey))))
|
||||
.filter(e => isPrintableCharEvent(e) || ((this.pattern.length > 0 || this.triggered) && ((e.keyCode === KeyCode.Escape || e.keyCode === KeyCode.Backspace) && !e.altKey && !e.ctrlKey && !e.metaKey) || (e.keyCode === KeyCode.Backspace && (isMacintosh ? e.altKey : e.ctrlKey) && !e.shiftKey)))
|
||||
.forEach(e => { e.stopPropagation(); e.preventDefault(); })
|
||||
.event;
|
||||
|
||||
@@ -521,9 +554,14 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
|
||||
}
|
||||
|
||||
this._pattern = pattern;
|
||||
this._onDidChangePattern.fire(pattern);
|
||||
|
||||
this.filter.pattern = pattern;
|
||||
this.tree.refilter();
|
||||
this.tree.focusNext(0, true);
|
||||
|
||||
if (pattern) {
|
||||
this.tree.focusNext(0, true, undefined, node => !FuzzyScore.isDefault(node.filterData as any as FuzzyScore));
|
||||
}
|
||||
|
||||
const focus = this.tree.getFocus();
|
||||
|
||||
@@ -565,6 +603,8 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
|
||||
};
|
||||
|
||||
const onDragOver = (event: DragEvent) => {
|
||||
event.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
|
||||
|
||||
const x = event.screenX - left;
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'none';
|
||||
@@ -619,7 +659,7 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
|
||||
}
|
||||
|
||||
private updateFilterOnTypeTitle(): void {
|
||||
if (this.filterOnTypeDomNode.checked) {
|
||||
if (this.filterOnType) {
|
||||
this.filterOnTypeDomNode.title = localize('disable filter on type', "Disable Filter on Type");
|
||||
} else {
|
||||
this.filterOnTypeDomNode.title = localize('enable filter on type', "Enable Filter on Type");
|
||||
@@ -631,17 +671,34 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
|
||||
|
||||
if (this.pattern && this.tree.options.filterOnType && noMatches) {
|
||||
this.messageDomNode.textContent = localize('empty', "No elements found");
|
||||
this._empty = true;
|
||||
} else {
|
||||
this.messageDomNode.innerHTML = '';
|
||||
this._empty = false;
|
||||
}
|
||||
|
||||
toggleClass(this.domNode, 'no-matches', noMatches);
|
||||
this.domNode.title = localize('found', "Matched {0} out of {1} elements", this.filter.matchCount, this.filter.totalCount);
|
||||
this.labelDomNode.textContent = this.pattern.length > 16 ? '…' + this.pattern.substr(this.pattern.length - 16) : this.pattern;
|
||||
|
||||
this._onDidChangeEmptyState.fire(this._empty);
|
||||
}
|
||||
|
||||
shouldAllowFocus(node: ITreeNode<T, TFilterData>): boolean {
|
||||
if (!this.enabled || !this.pattern || this.filterOnType) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.filter.totalCount > 0 && this.filter.matchCount <= 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !FuzzyScore.isDefault(node.filterData as any as FuzzyScore);
|
||||
}
|
||||
|
||||
dispose() {
|
||||
this.disable();
|
||||
this._onDidChangePattern.dispose();
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
}
|
||||
@@ -673,7 +730,7 @@ function asTreeContextMenuEvent<T>(event: IListContextMenuEvent<ITreeNode<T, any
|
||||
}
|
||||
|
||||
export interface IKeyboardNavigationEventFilter {
|
||||
(e: KeyboardEvent): boolean;
|
||||
(e: StandardKeyboardEvent): boolean;
|
||||
}
|
||||
|
||||
export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions {
|
||||
@@ -689,6 +746,12 @@ export interface IAbstractTreeOptions<T, TFilterData = void> extends IAbstractTr
|
||||
readonly dnd?: ITreeDragAndDrop<T>;
|
||||
readonly autoExpandSingleChildren?: boolean;
|
||||
readonly keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter;
|
||||
readonly expandOnlyOnTwistieClick?: boolean | ((e: T) => boolean);
|
||||
}
|
||||
|
||||
function dfs<T, TFilterData>(node: ITreeNode<T, TFilterData>, fn: (node: ITreeNode<T, TFilterData>) => void): void {
|
||||
fn(node);
|
||||
node.children.forEach(child => dfs(child, fn));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -706,17 +769,19 @@ class Trait<T> {
|
||||
private _nodeSet: Set<ITreeNode<T, any>> | undefined;
|
||||
private get nodeSet(): Set<ITreeNode<T, any>> {
|
||||
if (!this._nodeSet) {
|
||||
this._nodeSet = new Set();
|
||||
|
||||
for (const node of this.nodes) {
|
||||
this._nodeSet.add(node);
|
||||
}
|
||||
this._nodeSet = this.createNodeSet();
|
||||
}
|
||||
|
||||
return this._nodeSet;
|
||||
}
|
||||
|
||||
constructor(private identityProvider?: IIdentityProvider<T>) { }
|
||||
|
||||
set(nodes: ITreeNode<T, any>[], browserEvent?: UIEvent): void {
|
||||
if (equals(this.nodes, nodes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.nodes = [...nodes];
|
||||
this.elements = undefined;
|
||||
this._nodeSet = undefined;
|
||||
@@ -737,27 +802,101 @@ class Trait<T> {
|
||||
return this.nodeSet.has(node);
|
||||
}
|
||||
|
||||
remove(nodes: ITreeNode<T, any>[]): void {
|
||||
if (nodes.length === 0) {
|
||||
onDidModelSplice({ insertedNodes, deletedNodes }: ITreeModelSpliceEvent<T, any>): void {
|
||||
if (!this.identityProvider) {
|
||||
const set = this.createNodeSet();
|
||||
const visit = node => set.delete(node);
|
||||
deletedNodes.forEach(node => dfs(node, visit));
|
||||
this.set(values(set));
|
||||
return;
|
||||
}
|
||||
|
||||
const set = this.nodeSet;
|
||||
const visit = (node: ITreeNode<T, any>) => {
|
||||
set.delete(node);
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
const identityProvider = this.identityProvider;
|
||||
const nodesByIdentity = new Map<string, ITreeNode<T, any>>();
|
||||
this.nodes.forEach(node => nodesByIdentity.set(identityProvider.getId(node.element).toString(), node));
|
||||
|
||||
nodes.forEach(visit);
|
||||
this.set(values(set));
|
||||
const toDeleteByIdentity = new Map<string, ITreeNode<T, any>>();
|
||||
const toRemoveSetter = node => toDeleteByIdentity.set(identityProvider.getId(node.element).toString(), node);
|
||||
const toRemoveDeleter = node => toDeleteByIdentity.delete(identityProvider.getId(node.element).toString());
|
||||
deletedNodes.forEach(node => dfs(node, toRemoveSetter));
|
||||
insertedNodes.forEach(node => dfs(node, toRemoveDeleter));
|
||||
|
||||
toDeleteByIdentity.forEach((_, id) => nodesByIdentity.delete(id));
|
||||
this.set(values(nodesByIdentity));
|
||||
}
|
||||
|
||||
private createNodeSet(): Set<ITreeNode<T, any>> {
|
||||
const set = new Set<ITreeNode<T, any>>();
|
||||
|
||||
for (const node of this.nodes) {
|
||||
set.add(node);
|
||||
}
|
||||
|
||||
return set;
|
||||
}
|
||||
}
|
||||
|
||||
class TreeNodeListMouseController<T, TFilterData, TRef> extends MouseController<ITreeNode<T, TFilterData>> {
|
||||
|
||||
constructor(list: TreeNodeList<T, TFilterData, TRef>, private tree: AbstractTree<T, TFilterData, TRef>) {
|
||||
super(list);
|
||||
}
|
||||
|
||||
protected onPointer(e: IListMouseEvent<ITreeNode<T, TFilterData>>): void {
|
||||
if (isInputElement(e.browserEvent.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = e.element;
|
||||
|
||||
if (!node) {
|
||||
return super.onPointer(e);
|
||||
}
|
||||
|
||||
if (this.isSelectionRangeChangeEvent(e) || this.isSelectionSingleChangeEvent(e)) {
|
||||
return super.onPointer(e);
|
||||
}
|
||||
|
||||
const onTwistie = hasClass(e.browserEvent.target as HTMLElement, 'monaco-tl-twistie');
|
||||
|
||||
if (!this.tree.openOnSingleClick && e.browserEvent.detail !== 2 && !onTwistie) {
|
||||
return super.onPointer(e);
|
||||
}
|
||||
|
||||
let expandOnlyOnTwistieClick = false;
|
||||
|
||||
if (typeof this.tree.expandOnlyOnTwistieClick === 'function') {
|
||||
expandOnlyOnTwistieClick = this.tree.expandOnlyOnTwistieClick(node.element);
|
||||
} else {
|
||||
expandOnlyOnTwistieClick = !!this.tree.expandOnlyOnTwistieClick;
|
||||
}
|
||||
|
||||
if (expandOnlyOnTwistieClick && !onTwistie) {
|
||||
return super.onPointer(e);
|
||||
}
|
||||
|
||||
const model = ((this.tree as any).model as ITreeModel<T, TFilterData, TRef>); // internal
|
||||
const location = model.getNodeLocation(node);
|
||||
const recursive = e.browserEvent.altKey;
|
||||
model.setCollapsed(location, undefined, recursive);
|
||||
|
||||
if (expandOnlyOnTwistieClick && onTwistie) {
|
||||
return;
|
||||
}
|
||||
|
||||
super.onPointer(e);
|
||||
}
|
||||
}
|
||||
|
||||
interface ITreeNodeListOptions<T, TFilterData, TRef> extends IListOptions<ITreeNode<T, TFilterData>> {
|
||||
readonly tree: AbstractTree<T, TFilterData, TRef>;
|
||||
}
|
||||
|
||||
/**
|
||||
* We use this List subclass to restore selection and focus as nodes
|
||||
* get rendered in the list, possibly due to a node expand() call.
|
||||
*/
|
||||
class TreeNodeList<T, TFilterData> extends List<ITreeNode<T, TFilterData>> {
|
||||
class TreeNodeList<T, TFilterData, TRef> extends List<ITreeNode<T, TFilterData>> {
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
@@ -765,11 +904,15 @@ class TreeNodeList<T, TFilterData> extends List<ITreeNode<T, TFilterData>> {
|
||||
renderers: IListRenderer<any /* TODO@joao */, any>[],
|
||||
private focusTrait: Trait<T>,
|
||||
private selectionTrait: Trait<T>,
|
||||
options?: IListOptions<ITreeNode<T, TFilterData>>
|
||||
options: ITreeNodeListOptions<T, TFilterData, TRef>
|
||||
) {
|
||||
super(container, virtualDelegate, renderers, options);
|
||||
}
|
||||
|
||||
protected createMouseController(options: ITreeNodeListOptions<T, TFilterData, TRef>): MouseController<ITreeNode<T, TFilterData>> {
|
||||
return new TreeNodeListMouseController(this, options.tree);
|
||||
}
|
||||
|
||||
splice(start: number, deleteCount: number, elements: ITreeNode<T, TFilterData>[] = []): void {
|
||||
super.splice(start, deleteCount, elements);
|
||||
|
||||
@@ -818,20 +961,20 @@ class TreeNodeList<T, TFilterData> extends List<ITreeNode<T, TFilterData>> {
|
||||
|
||||
export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable {
|
||||
|
||||
private view: TreeNodeList<T, TFilterData>;
|
||||
protected view: TreeNodeList<T, TFilterData, TRef>;
|
||||
private renderers: TreeRenderer<T, TFilterData, any>[];
|
||||
private focusNavigationFilter: ((node: ITreeNode<T, TFilterData>) => boolean) | undefined;
|
||||
protected model: ITreeModel<T, TFilterData, TRef>;
|
||||
private focus = new Trait<T>();
|
||||
private selection = new Trait<T>();
|
||||
private focus: Trait<T>;
|
||||
private selection: Trait<T>;
|
||||
private eventBufferer = new EventBufferer();
|
||||
private typeFilterController?: TypeFilterController<T, TFilterData>;
|
||||
private focusNavigationFilter: ((node: ITreeNode<T, TFilterData>) => boolean) | undefined;
|
||||
protected disposables: IDisposable[] = [];
|
||||
|
||||
get onDidScroll(): Event<void> { return this.view.onDidScroll; }
|
||||
|
||||
readonly onDidChangeFocus: Event<ITreeEvent<T>> = this.eventBufferer.wrapEvent(this.focus.onDidChange);
|
||||
readonly onDidChangeSelection: Event<ITreeEvent<T>> = this.eventBufferer.wrapEvent(this.selection.onDidChange);
|
||||
get onDidChangeFocus(): Event<ITreeEvent<T>> { return this.eventBufferer.wrapEvent(this.focus.onDidChange); }
|
||||
get onDidChangeSelection(): Event<ITreeEvent<T>> { return this.eventBufferer.wrapEvent(this.selection.onDidChange); }
|
||||
get onDidOpen(): Event<ITreeEvent<T>> { return Event.map(this.view.onDidOpen, asTreeEvent); }
|
||||
|
||||
get onMouseClick(): Event<ITreeMouseEvent<T>> { return Event.map(this.view.onMouseClick, asTreeMouseEvent); }
|
||||
@@ -852,7 +995,14 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
readonly onWillRefilter: Event<void> = this._onWillRefilter.event;
|
||||
|
||||
get filterOnType(): boolean { return !!this._options.filterOnType; }
|
||||
get onDidChangeTypeFilterPattern(): Event<string> { return this.typeFilterController ? this.typeFilterController.onDidChangePattern : Event.None; }
|
||||
|
||||
// Options TODO@joao expose options only, not Optional<>
|
||||
get openOnSingleClick(): boolean { return typeof this._options.openOnSingleClick === 'undefined' ? true : this._options.openOnSingleClick; }
|
||||
get expandOnlyOnTwistieClick(): boolean | ((e: T) => boolean) { return typeof this._options.expandOnlyOnTwistieClick === 'undefined' ? false : this._options.expandOnlyOnTwistieClick; }
|
||||
|
||||
private _onDidUpdateOptions = new Emitter<IAbstractTreeOptions<T, TFilterData>>();
|
||||
readonly onDidUpdateOptions: Event<IAbstractTreeOptions<T, TFilterData>> = this._onDidUpdateOptions.event;
|
||||
|
||||
get onDidDispose(): Event<void> { return this.view.onDidDispose; }
|
||||
|
||||
@@ -871,49 +1021,22 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
let filter: TypeFilter<T> | undefined;
|
||||
|
||||
if (_options.keyboardNavigationLabelProvider) {
|
||||
filter = new TypeFilter(this, _options.keyboardNavigationLabelProvider, _options.filter as ITreeFilter<T, FuzzyScore>);
|
||||
filter = new TypeFilter(this, _options.keyboardNavigationLabelProvider, _options.filter as any as ITreeFilter<T, FuzzyScore>);
|
||||
_options = { ..._options, filter: filter as ITreeFilter<T, TFilterData> }; // TODO need typescript help here
|
||||
this.disposables.push(filter);
|
||||
}
|
||||
|
||||
this.view = new TreeNodeList(container, treeDelegate, this.renderers, this.focus, this.selection, asListOptions(() => this.model, _options));
|
||||
this.focus = new Trait(_options.identityProvider);
|
||||
this.selection = new Trait(_options.identityProvider);
|
||||
this.view = new TreeNodeList(container, treeDelegate, this.renderers, this.focus, this.selection, { ...asListOptions(() => this.model, _options), tree: this });
|
||||
|
||||
this.model = this.createModel(this.view, _options);
|
||||
onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState;
|
||||
|
||||
if (this.options.identityProvider) {
|
||||
const identityProvider = this.options.identityProvider;
|
||||
|
||||
this.model.onDidSplice(e => {
|
||||
if (e.deletedNodes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.eventBufferer.bufferEvents(() => {
|
||||
const map = new Map<string, ITreeNode<T, TFilterData>>();
|
||||
|
||||
for (const node of e.deletedNodes) {
|
||||
map.set(identityProvider.getId(node.element).toString(), node);
|
||||
}
|
||||
|
||||
for (const node of e.insertedNodes) {
|
||||
map.delete(identityProvider.getId(node.element).toString());
|
||||
}
|
||||
|
||||
if (map.size === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const deletedNodes = values(map);
|
||||
|
||||
this.focus.remove(deletedNodes);
|
||||
this.selection.remove(deletedNodes);
|
||||
});
|
||||
}, null, this.disposables);
|
||||
}
|
||||
|
||||
this.view.onTap(this.reactOnMouseClick, this, this.disposables);
|
||||
this.view.onMouseClick(this.reactOnMouseClick, this, this.disposables);
|
||||
this.model.onDidSplice(e => {
|
||||
this.focus.onDidModelSplice(e);
|
||||
this.selection.onDidModelSplice(e);
|
||||
}, null, this.disposables);
|
||||
|
||||
if (_options.keyboardSupport !== false) {
|
||||
const onKeyDown = Event.chain(this.view.onKeyDown)
|
||||
@@ -927,17 +1050,7 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
|
||||
if (_options.keyboardNavigationLabelProvider) {
|
||||
this.typeFilterController = new TypeFilterController(this, this.model, this.view, filter!, _options.keyboardNavigationLabelProvider);
|
||||
this.focusNavigationFilter = node => {
|
||||
if (!this.typeFilterController!.enabled || !this.typeFilterController!.pattern) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (filter!.totalCount > 0 && filter!.matchCount <= 1) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !FuzzyScore.isDefault(node.filterData as any as FuzzyScore);
|
||||
};
|
||||
this.focusNavigationFilter = node => this.typeFilterController!.shouldAllowFocus(node);
|
||||
this.disposables.push(this.typeFilterController!);
|
||||
}
|
||||
}
|
||||
@@ -949,11 +1062,16 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
renderer.updateOptions(optionsUpdate);
|
||||
}
|
||||
|
||||
this.view.updateOptions({ enableKeyboardNavigation: this._options.simpleKeyboardNavigation });
|
||||
this.view.updateOptions({
|
||||
enableKeyboardNavigation: this._options.simpleKeyboardNavigation,
|
||||
automaticKeyboardNavigation: this._options.automaticKeyboardNavigation
|
||||
});
|
||||
|
||||
if (this.typeFilterController) {
|
||||
this.typeFilterController.updateOptions(this._options);
|
||||
}
|
||||
|
||||
this._onDidUpdateOptions.fire(this._options);
|
||||
}
|
||||
|
||||
get options(): IAbstractTreeOptions<T, TFilterData> {
|
||||
@@ -977,11 +1095,21 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
}
|
||||
|
||||
get contentHeight(): number {
|
||||
if (this.typeFilterController && this.typeFilterController.filterOnType && this.typeFilterController.empty) {
|
||||
return 100;
|
||||
}
|
||||
|
||||
return this.view.contentHeight;
|
||||
}
|
||||
|
||||
get onDidChangeContentHeight(): Event<number> {
|
||||
return this.view.onDidChangeContentHeight;
|
||||
let result = this.view.onDidChangeContentHeight;
|
||||
|
||||
if (this.typeFilterController) {
|
||||
result = Event.any(result, Event.map(this.typeFilterController.onDidChangeEmptyState, () => this.contentHeight));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
get scrollTop(): number {
|
||||
@@ -1000,6 +1128,18 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
return this.view.renderHeight;
|
||||
}
|
||||
|
||||
get firstVisibleElement(): T {
|
||||
const index = this.view.firstVisibleIndex;
|
||||
const node = this.view.element(index);
|
||||
return node.element;
|
||||
}
|
||||
|
||||
get lastVisibleElement(): T {
|
||||
const index = this.view.lastVisibleIndex;
|
||||
const node = this.view.element(index);
|
||||
return node.element;
|
||||
}
|
||||
|
||||
domFocus(): void {
|
||||
this.view.domFocus();
|
||||
}
|
||||
@@ -1061,11 +1201,11 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
}
|
||||
|
||||
toggleKeyboardNavigation(): void {
|
||||
if (!this.typeFilterController) {
|
||||
return;
|
||||
}
|
||||
this.view.toggleKeyboardNavigation();
|
||||
|
||||
this.typeFilterController.toggle();
|
||||
if (this.typeFilterController) {
|
||||
this.typeFilterController.toggle();
|
||||
}
|
||||
}
|
||||
|
||||
refilter(): void {
|
||||
@@ -1093,40 +1233,42 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
this.view.setFocus(indexes, browserEvent, true);
|
||||
}
|
||||
|
||||
focusNext(n = 1, loop = false, browserEvent?: UIEvent): void {
|
||||
this.view.focusNext(n, loop, browserEvent, this.focusNavigationFilter);
|
||||
focusNext(n = 1, loop = false, browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
|
||||
this.view.focusNext(n, loop, browserEvent, filter);
|
||||
}
|
||||
|
||||
focusPrevious(n = 1, loop = false, browserEvent?: UIEvent): void {
|
||||
this.view.focusPrevious(n, loop, browserEvent, this.focusNavigationFilter);
|
||||
focusPrevious(n = 1, loop = false, browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
|
||||
this.view.focusPrevious(n, loop, browserEvent, filter);
|
||||
}
|
||||
|
||||
focusNextPage(browserEvent?: UIEvent): void {
|
||||
this.view.focusNextPage(browserEvent, this.focusNavigationFilter);
|
||||
focusNextPage(browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
|
||||
this.view.focusNextPage(browserEvent, filter);
|
||||
}
|
||||
|
||||
focusPreviousPage(browserEvent?: UIEvent): void {
|
||||
this.view.focusPreviousPage(browserEvent, this.focusNavigationFilter);
|
||||
focusPreviousPage(browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
|
||||
this.view.focusPreviousPage(browserEvent, filter);
|
||||
}
|
||||
|
||||
focusLast(browserEvent?: UIEvent): void {
|
||||
this.view.focusLast(browserEvent, this.focusNavigationFilter);
|
||||
focusLast(browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
|
||||
this.view.focusLast(browserEvent, filter);
|
||||
}
|
||||
|
||||
focusFirst(browserEvent?: UIEvent): void {
|
||||
this.view.focusFirst(browserEvent, this.focusNavigationFilter);
|
||||
focusFirst(browserEvent?: UIEvent, filter = this.focusNavigationFilter): void {
|
||||
this.view.focusFirst(browserEvent, filter);
|
||||
}
|
||||
|
||||
getFocus(): T[] {
|
||||
return this.focus.get();
|
||||
}
|
||||
|
||||
open(elements: TRef[]): void {
|
||||
open(elements: TRef[], browserEvent?: UIEvent): void {
|
||||
const indexes = elements.map(e => this.model.getListIndex(e));
|
||||
this.view.open(indexes);
|
||||
this.view.open(indexes, browserEvent);
|
||||
}
|
||||
|
||||
reveal(location: TRef, relativeTop?: number): void {
|
||||
this.model.expandTo(location);
|
||||
|
||||
const index = this.model.getListIndex(location);
|
||||
|
||||
if (index === -1) {
|
||||
@@ -1152,37 +1294,6 @@ export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable
|
||||
|
||||
// List
|
||||
|
||||
get visibleNodeCount(): number {
|
||||
return this.view.length;
|
||||
}
|
||||
|
||||
private reactOnMouseClick(e: IListMouseEvent<ITreeNode<T, TFilterData>>): void {
|
||||
if (isInputElement(e.browserEvent.target as HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const node = e.element;
|
||||
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.view.multipleSelectionController.isSelectionRangeChangeEvent(e) || this.view.multipleSelectionController.isSelectionSingleChangeEvent(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const onTwistie = hasClass(e.browserEvent.target as HTMLElement, 'monaco-tl-twistie');
|
||||
|
||||
if (!this.openOnSingleClick && e.browserEvent.detail !== 2 && !onTwistie) {
|
||||
return;
|
||||
}
|
||||
|
||||
const location = this.model.getNodeLocation(node);
|
||||
const recursive = e.browserEvent.altKey;
|
||||
|
||||
this.model.setCollapsed(location, undefined, recursive);
|
||||
}
|
||||
|
||||
private onLeftArrow(e: StandardKeyboardEvent): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -9,7 +9,7 @@ import { IListVirtualDelegate, IIdentityProvider, IListDragAndDrop, IListDragOve
|
||||
import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeSorter, ICollapseStateChangeEvent, IAsyncDataSource, ITreeDragAndDrop } from 'vs/base/browser/ui/tree/tree';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { timeout, always, CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { timeout, CancelablePromise, createCancelablePromise } from 'vs/base/common/async';
|
||||
import { IListStyles } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { Iterator } from 'vs/base/common/iterator';
|
||||
import { IDragAndDropData } from 'vs/base/browser/dnd';
|
||||
@@ -17,24 +17,35 @@ import { ElementsDragAndDropData } from 'vs/base/browser/ui/list/listView';
|
||||
import { isPromiseCanceledError, onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { toggleClass } from 'vs/base/browser/dom';
|
||||
|
||||
const enum AsyncDataTreeNodeState {
|
||||
Uninitialized = 'uninitialized',
|
||||
Loaded = 'loaded',
|
||||
Loading = 'loading'
|
||||
}
|
||||
|
||||
interface IAsyncDataTreeNode<TInput, T> {
|
||||
element: TInput | T;
|
||||
readonly parent: IAsyncDataTreeNode<TInput, T> | null;
|
||||
readonly children: IAsyncDataTreeNode<TInput, T>[];
|
||||
readonly id?: string | null;
|
||||
state: AsyncDataTreeNodeState;
|
||||
loading: boolean;
|
||||
hasChildren: boolean;
|
||||
needsRefresh: boolean;
|
||||
stale: boolean;
|
||||
slow: boolean;
|
||||
disposed: boolean;
|
||||
}
|
||||
|
||||
interface IAsyncDataTreeNodeRequiredProps<TInput, T> extends Partial<IAsyncDataTreeNode<TInput, T>> {
|
||||
readonly element: TInput | T;
|
||||
readonly parent: IAsyncDataTreeNode<TInput, T> | null;
|
||||
readonly hasChildren: boolean;
|
||||
}
|
||||
|
||||
function createAsyncDataTreeNode<TInput, T>(props: IAsyncDataTreeNodeRequiredProps<TInput, T>): IAsyncDataTreeNode<TInput, T> {
|
||||
return {
|
||||
...props,
|
||||
children: [],
|
||||
loading: false,
|
||||
stale: true,
|
||||
disposed: false,
|
||||
slow: false
|
||||
};
|
||||
}
|
||||
|
||||
function isAncestor<TInput, T>(ancestor: IAsyncDataTreeNode<TInput, T>, descendant: IAsyncDataTreeNode<TInput, T>): boolean {
|
||||
if (!descendant.parent) {
|
||||
return false;
|
||||
@@ -87,8 +98,8 @@ class DataTreeRenderer<TInput, T, TFilterData, TTemplateData> implements ITreeRe
|
||||
return { templateData };
|
||||
}
|
||||
|
||||
renderElement(node: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>): void {
|
||||
this.renderer.renderElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData);
|
||||
renderElement(node: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>, dynamicHeightProbing?: boolean): void {
|
||||
this.renderer.renderElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData, dynamicHeightProbing);
|
||||
}
|
||||
|
||||
renderTwistie(element: IAsyncDataTreeNode<TInput, T>, twistieElement: HTMLElement): boolean {
|
||||
@@ -96,9 +107,9 @@ class DataTreeRenderer<TInput, T, TFilterData, TTemplateData> implements ITreeRe
|
||||
return false;
|
||||
}
|
||||
|
||||
disposeElement(node: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>): void {
|
||||
disposeElement(node: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>, dynamicHeightProbing?: boolean): void {
|
||||
if (this.renderer.disposeElement) {
|
||||
this.renderer.disposeElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData);
|
||||
this.renderer.disposeElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData, dynamicHeightProbing);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,20 +228,26 @@ function asObjectTreeOptions<TInput, T, TFilterData>(options?: IAsyncDataTreeOpt
|
||||
return options.keyboardNavigationLabelProvider!.getKeyboardNavigationLabel(e.element as T);
|
||||
}
|
||||
},
|
||||
sorter: undefined
|
||||
sorter: undefined,
|
||||
expandOnlyOnTwistieClick: typeof options.expandOnlyOnTwistieClick === 'undefined' ? undefined : (
|
||||
typeof options.expandOnlyOnTwistieClick !== 'function' ? options.expandOnlyOnTwistieClick : (
|
||||
e => (options.expandOnlyOnTwistieClick as ((e: T) => boolean))(e.element as T)
|
||||
)
|
||||
),
|
||||
ariaSetProvider: undefined
|
||||
};
|
||||
}
|
||||
|
||||
function asTreeElement<TInput, T>(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): ITreeElement<IAsyncDataTreeNode<TInput, T>> {
|
||||
let collapsed: boolean | undefined;
|
||||
|
||||
if (viewStateContext && node.id) {
|
||||
if (viewStateContext && viewStateContext.viewState.expanded && node.id) {
|
||||
collapsed = viewStateContext.viewState.expanded.indexOf(node.id) === -1;
|
||||
}
|
||||
|
||||
return {
|
||||
element: node,
|
||||
children: Iterator.map(Iterator.fromArray(node.children), child => asTreeElement(child, viewStateContext)),
|
||||
children: node.hasChildren ? Iterator.map(Iterator.fromArray(node.children), child => asTreeElement(child, viewStateContext)) : [],
|
||||
collapsible: node.hasChildren,
|
||||
collapsed
|
||||
};
|
||||
@@ -245,9 +262,10 @@ export interface IAsyncDataTreeOptions<T, TFilterData = void> extends IAsyncData
|
||||
}
|
||||
|
||||
export interface IAsyncDataTreeViewState {
|
||||
readonly focus: string[];
|
||||
readonly selection: string[];
|
||||
readonly expanded: string[];
|
||||
readonly focus?: string[];
|
||||
readonly selection?: string[];
|
||||
readonly expanded?: string[];
|
||||
readonly scrollTop?: number;
|
||||
}
|
||||
|
||||
interface IAsyncDataTreeViewStateContext<TInput, T> {
|
||||
@@ -260,7 +278,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
|
||||
private readonly tree: ObjectTree<IAsyncDataTreeNode<TInput, T>, TFilterData>;
|
||||
private readonly root: IAsyncDataTreeNode<TInput, T>;
|
||||
private readonly renderedNodes = new Map<null | T, IAsyncDataTreeNode<TInput, T>>();
|
||||
private readonly nodes = new Map<null | T, IAsyncDataTreeNode<TInput, T>>();
|
||||
private readonly sorter?: ITreeSorter<T>;
|
||||
|
||||
private readonly subTreeRefreshPromises = new Map<IAsyncDataTreeNode<TInput, T>, Promise<void>>();
|
||||
@@ -280,12 +298,15 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
get onDidChangeSelection(): Event<ITreeEvent<T>> { return Event.map(this.tree.onDidChangeSelection, asTreeEvent); }
|
||||
get onDidOpen(): Event<ITreeEvent<T>> { return Event.map(this.tree.onDidOpen, asTreeEvent); }
|
||||
|
||||
get onKeyDown(): Event<KeyboardEvent> { return this.tree.onKeyDown; }
|
||||
get onMouseClick(): Event<ITreeMouseEvent<T>> { return Event.map(this.tree.onMouseClick, asTreeMouseEvent); }
|
||||
get onMouseDblClick(): Event<ITreeMouseEvent<T>> { return Event.map(this.tree.onMouseDblClick, asTreeMouseEvent); }
|
||||
get onContextMenu(): Event<ITreeContextMenuEvent<T>> { return Event.map(this.tree.onContextMenu, asTreeContextMenuEvent); }
|
||||
get onDidFocus(): Event<void> { return this.tree.onDidFocus; }
|
||||
get onDidBlur(): Event<void> { return this.tree.onDidBlur; }
|
||||
|
||||
get onDidUpdateOptions(): Event<IAsyncDataTreeOptionsUpdate> { return this.tree.onDidUpdateOptions; }
|
||||
|
||||
get filterOnType(): boolean { return this.tree.filterOnType; }
|
||||
get openOnSingleClick(): boolean { return this.tree.openOnSingleClick; }
|
||||
|
||||
@@ -308,16 +329,11 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
|
||||
this.tree = new ObjectTree(container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions);
|
||||
|
||||
this.root = {
|
||||
this.root = createAsyncDataTreeNode({
|
||||
element: undefined!,
|
||||
parent: null,
|
||||
children: [],
|
||||
state: AsyncDataTreeNodeState.Uninitialized,
|
||||
hasChildren: true,
|
||||
needsRefresh: false,
|
||||
disposed: false,
|
||||
slow: false
|
||||
};
|
||||
hasChildren: true
|
||||
});
|
||||
|
||||
if (this.identityProvider) {
|
||||
this.root = {
|
||||
@@ -326,7 +342,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
};
|
||||
}
|
||||
|
||||
this.renderedNodes.set(null, this.root);
|
||||
this.nodes.set(null, this.root);
|
||||
|
||||
this.tree.onDidChangeCollapseState(this._onDidChangeCollapseState, this, this.disposables);
|
||||
}
|
||||
@@ -365,6 +381,14 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
return this.tree.renderHeight;
|
||||
}
|
||||
|
||||
get firstVisibleElement(): T {
|
||||
return this.tree.firstVisibleElement!.element as T;
|
||||
}
|
||||
|
||||
get lastVisibleElement(): T {
|
||||
return this.tree.lastVisibleElement!.element as T;
|
||||
}
|
||||
|
||||
domFocus(): void {
|
||||
this.tree.domFocus();
|
||||
}
|
||||
@@ -397,6 +421,10 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
this.tree.setFocus(viewStateContext.focus);
|
||||
this.tree.setSelection(viewStateContext.selection);
|
||||
}
|
||||
|
||||
if (viewState && typeof viewState.scrollTop === 'number') {
|
||||
this.scrollTop = viewState.scrollTop;
|
||||
}
|
||||
}
|
||||
|
||||
async updateChildren(element: TInput | T = this.root.element, recursive = true, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
|
||||
@@ -404,7 +432,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
throw new Error('Tree input not set');
|
||||
}
|
||||
|
||||
if (this.root.state === AsyncDataTreeNodeState.Loading) {
|
||||
if (this.root.loading) {
|
||||
await this.subTreeRefreshPromises.get(this.root)!;
|
||||
await Event.toPromise(this._onDidRender.event);
|
||||
}
|
||||
@@ -412,15 +440,24 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
await this.refreshAndRenderNode(this.getDataNode(element), recursive, ChildrenResolutionReason.Refresh, viewStateContext);
|
||||
}
|
||||
|
||||
resort(element: TInput | T = this.root.element, recursive = true): void {
|
||||
this.tree.resort(this.getDataNode(element), recursive);
|
||||
}
|
||||
|
||||
hasNode(element: TInput | T): boolean {
|
||||
return element === this.root.element || this.renderedNodes.has(element as T);
|
||||
return element === this.root.element || this.nodes.has(element as T);
|
||||
}
|
||||
|
||||
// View
|
||||
|
||||
refresh(element: T): void {
|
||||
rerender(element?: T): void {
|
||||
if (element === undefined) {
|
||||
this.tree.rerender();
|
||||
return;
|
||||
}
|
||||
|
||||
const node = this.getDataNode(element);
|
||||
this.tree.refresh(node);
|
||||
this.tree.rerender(node);
|
||||
}
|
||||
|
||||
updateWidth(element: T): void {
|
||||
@@ -446,20 +483,20 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
throw new Error('Tree input not set');
|
||||
}
|
||||
|
||||
if (this.root.state === AsyncDataTreeNodeState.Loading) {
|
||||
if (this.root.loading) {
|
||||
await this.subTreeRefreshPromises.get(this.root)!;
|
||||
await Event.toPromise(this._onDidRender.event);
|
||||
}
|
||||
|
||||
const node = this.getDataNode(element);
|
||||
|
||||
if (node !== this.root && node.state !== AsyncDataTreeNodeState.Loading && !this.tree.isCollapsed(node)) {
|
||||
if (node !== this.root && !node.loading && !this.tree.isCollapsed(node)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = this.tree.expand(node === this.root ? null : node, recursive);
|
||||
|
||||
if (node.state === AsyncDataTreeNodeState.Loading) {
|
||||
if (node.loading) {
|
||||
await this.subTreeRefreshPromises.get(node)!;
|
||||
await Event.toPromise(this._onDidRender.event);
|
||||
}
|
||||
@@ -565,16 +602,10 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
return (node && node.element)!;
|
||||
}
|
||||
|
||||
// List
|
||||
|
||||
get visibleNodeCount(): number {
|
||||
return this.tree.visibleNodeCount;
|
||||
}
|
||||
|
||||
// Implementation
|
||||
|
||||
private getDataNode(element: TInput | T): IAsyncDataTreeNode<TInput, T> {
|
||||
const node: IAsyncDataTreeNode<TInput, T> | undefined = this.renderedNodes.get((element === this.root.element ? null : element) as T);
|
||||
const node: IAsyncDataTreeNode<TInput, T> | undefined = this.nodes.get((element === this.root.element ? null : element) as T);
|
||||
|
||||
if (!node) {
|
||||
throw new Error(`Data tree node not found: ${element}`);
|
||||
@@ -626,39 +657,19 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
}
|
||||
|
||||
private async doRefreshSubTree(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
|
||||
node.state = AsyncDataTreeNodeState.Loading;
|
||||
node.loading = true;
|
||||
|
||||
try {
|
||||
await this.doRefreshNode(node, recursive, viewStateContext);
|
||||
const childrenToRefresh = await this.doRefreshNode(node, recursive, viewStateContext);
|
||||
node.stale = false;
|
||||
|
||||
if (recursive) {
|
||||
const childrenToRefresh = node.children
|
||||
.filter(child => {
|
||||
if (child.needsRefresh) {
|
||||
child.needsRefresh = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO@joao: is this still needed?
|
||||
if (child.hasChildren && child.state === AsyncDataTreeNodeState.Loaded) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!viewStateContext || !child.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return viewStateContext.viewState.expanded.indexOf(child.id) > -1;
|
||||
});
|
||||
|
||||
await Promise.all(childrenToRefresh.map(child => this.doRefreshSubTree(child, recursive, viewStateContext)));
|
||||
}
|
||||
await Promise.all(childrenToRefresh.map(child => this.doRefreshSubTree(child, recursive, viewStateContext)));
|
||||
} finally {
|
||||
node.state = AsyncDataTreeNodeState.Loaded;
|
||||
node.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async doRefreshNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<void> {
|
||||
private async doRefreshNode(node: IAsyncDataTreeNode<TInput, T>, recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): Promise<IAsyncDataTreeNode<TInput, T>[]> {
|
||||
node.hasChildren = !!this.dataSource.hasChildren(node.element!);
|
||||
|
||||
let childrenPromise: Promise<T[]>;
|
||||
@@ -673,21 +684,20 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
this._onDidChangeNodeSlowState.fire(node);
|
||||
}, _ => null);
|
||||
|
||||
childrenPromise = always(this.doGetChildren(node), () => slowTimeout.cancel());
|
||||
childrenPromise = this.doGetChildren(node)
|
||||
.finally(() => slowTimeout.cancel());
|
||||
}
|
||||
|
||||
try {
|
||||
const children = await childrenPromise;
|
||||
this.setChildren(node, children, recursive, viewStateContext);
|
||||
return this.setChildren(node, children, recursive, viewStateContext);
|
||||
} catch (err) {
|
||||
node.needsRefresh = true;
|
||||
|
||||
if (node !== this.root) {
|
||||
this.tree.collapse(node === this.root ? null : node);
|
||||
}
|
||||
|
||||
if (isPromiseCanceledError(err)) {
|
||||
return;
|
||||
return [];
|
||||
}
|
||||
|
||||
throw err;
|
||||
@@ -715,12 +725,14 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
|
||||
return children;
|
||||
});
|
||||
|
||||
this.refreshPromises.set(node, result);
|
||||
return always(result, () => this.refreshPromises.delete(node));
|
||||
|
||||
return result.finally(() => this.refreshPromises.delete(node));
|
||||
}
|
||||
|
||||
private _onDidChangeCollapseState({ node, deep }: ICollapseStateChangeEvent<IAsyncDataTreeNode<TInput, T>, any>): void {
|
||||
if (!node.collapsed && (node.element.state === AsyncDataTreeNodeState.Uninitialized || node.element.needsRefresh)) {
|
||||
if (!node.collapsed && node.element.stale) {
|
||||
if (deep) {
|
||||
this.collapse(node.element.element as T);
|
||||
} else {
|
||||
@@ -730,7 +742,12 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private setChildren(node: IAsyncDataTreeNode<TInput, T>, childrenElements: T[], recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): void {
|
||||
private setChildren(node: IAsyncDataTreeNode<TInput, T>, childrenElements: T[], recursive: boolean, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): IAsyncDataTreeNode<TInput, T>[] {
|
||||
// perf: if the node was and still is a leaf, avoid all this hassle
|
||||
if (node.children.length === 0 && childrenElements.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let nodeChildren: Map<string, IAsyncDataTreeNode<TInput, T>> | undefined;
|
||||
|
||||
if (this.identityProvider) {
|
||||
@@ -741,66 +758,57 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
let childrenToRefresh: IAsyncDataTreeNode<TInput, T>[] = [];
|
||||
|
||||
const children = childrenElements.map<IAsyncDataTreeNode<TInput, T>>(element => {
|
||||
if (!this.identityProvider) {
|
||||
const hasChildren = !!this.dataSource.hasChildren(element);
|
||||
|
||||
return {
|
||||
return createAsyncDataTreeNode({
|
||||
element,
|
||||
parent: node,
|
||||
children: [],
|
||||
state: AsyncDataTreeNodeState.Uninitialized,
|
||||
hasChildren,
|
||||
needsRefresh: false,
|
||||
disposed: false,
|
||||
slow: false
|
||||
};
|
||||
hasChildren: !!this.dataSource.hasChildren(element),
|
||||
});
|
||||
}
|
||||
|
||||
const id = this.identityProvider.getId(element).toString();
|
||||
const asyncDataTreeNode = nodeChildren!.get(id);
|
||||
|
||||
if (!asyncDataTreeNode) {
|
||||
const childAsyncDataTreeNode: IAsyncDataTreeNode<TInput, T> = {
|
||||
element,
|
||||
parent: node,
|
||||
children: [],
|
||||
id,
|
||||
state: AsyncDataTreeNodeState.Uninitialized,
|
||||
hasChildren: !!this.dataSource.hasChildren(element),
|
||||
needsRefresh: false,
|
||||
disposed: false,
|
||||
slow: false
|
||||
};
|
||||
if (asyncDataTreeNode) {
|
||||
asyncDataTreeNode.element = element;
|
||||
asyncDataTreeNode.stale = asyncDataTreeNode.stale || recursive;
|
||||
asyncDataTreeNode.hasChildren = !!this.dataSource.hasChildren(element);
|
||||
|
||||
if (viewStateContext) {
|
||||
if (viewStateContext.viewState.focus.indexOf(id) > -1) {
|
||||
viewStateContext.focus.push(childAsyncDataTreeNode);
|
||||
}
|
||||
|
||||
if (viewStateContext.viewState.selection.indexOf(id) > -1) {
|
||||
viewStateContext.selection.push(childAsyncDataTreeNode);
|
||||
}
|
||||
if (recursive && !this.tree.isCollapsed(asyncDataTreeNode)) {
|
||||
childrenToRefresh.push(asyncDataTreeNode);
|
||||
}
|
||||
|
||||
return childAsyncDataTreeNode;
|
||||
return asyncDataTreeNode;
|
||||
}
|
||||
|
||||
asyncDataTreeNode.element = element;
|
||||
const childAsyncDataTreeNode = createAsyncDataTreeNode({
|
||||
element,
|
||||
parent: node,
|
||||
id,
|
||||
hasChildren: !!this.dataSource.hasChildren(element)
|
||||
});
|
||||
|
||||
if (asyncDataTreeNode.state === AsyncDataTreeNodeState.Loaded || asyncDataTreeNode.hasChildren !== !!this.dataSource.hasChildren(asyncDataTreeNode.element)) {
|
||||
asyncDataTreeNode.needsRefresh = true;
|
||||
if (viewStateContext && viewStateContext.viewState.focus && viewStateContext.viewState.focus.indexOf(id) > -1) {
|
||||
viewStateContext.focus.push(childAsyncDataTreeNode);
|
||||
}
|
||||
|
||||
return asyncDataTreeNode;
|
||||
if (viewStateContext && viewStateContext.viewState.selection && viewStateContext.viewState.selection.indexOf(id) > -1) {
|
||||
viewStateContext.selection.push(childAsyncDataTreeNode);
|
||||
}
|
||||
|
||||
if (viewStateContext && viewStateContext.viewState.expanded && viewStateContext.viewState.expanded.indexOf(id) > -1) {
|
||||
childrenToRefresh.push(childAsyncDataTreeNode);
|
||||
}
|
||||
|
||||
return childAsyncDataTreeNode;
|
||||
});
|
||||
|
||||
// perf: if the node was and still is a leaf, avoid all these expensive no-ops
|
||||
if (node.children.length === 0 && childrenElements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
node.children.splice(0, node.children.length, ...children);
|
||||
|
||||
return childrenToRefresh;
|
||||
}
|
||||
|
||||
private render(node: IAsyncDataTreeNode<TInput, T>, viewStateContext?: IAsyncDataTreeViewStateContext<TInput, T>): void {
|
||||
@@ -809,7 +817,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
const onDidCreateNode = (treeNode: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>) => {
|
||||
if (treeNode.element.element) {
|
||||
insertedElements.add(treeNode.element.element as T);
|
||||
this.renderedNodes.set(treeNode.element.element as T, treeNode.element);
|
||||
this.nodes.set(treeNode.element.element as T, treeNode.element);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -817,7 +825,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
if (treeNode.element.element) {
|
||||
if (!insertedElements.has(treeNode.element.element as T)) {
|
||||
treeNode.element.disposed = true;
|
||||
this.renderedNodes.delete(treeNode.element.element as T);
|
||||
this.nodes.delete(treeNode.element.element as T);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -853,7 +861,7 @@ export class AsyncDataTree<TInput, T, TFilterData = void> implements IDisposable
|
||||
queue.push(...node.children);
|
||||
}
|
||||
|
||||
return { focus, selection, expanded };
|
||||
return { focus, selection, expanded, scrollTop: this.scrollTop };
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface IDataTreeOptions<T, TFilterData = void> extends IAbstractTreeOp
|
||||
export interface IDataTreeViewState {
|
||||
readonly focus: string[];
|
||||
readonly selection: string[];
|
||||
readonly collapsed: string[];
|
||||
readonly expanded: string[];
|
||||
}
|
||||
|
||||
export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | null, TFilterData, T | null> {
|
||||
@@ -26,6 +26,7 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
|
||||
private input: TInput | undefined;
|
||||
|
||||
private identityProvider: IIdentityProvider<T> | undefined;
|
||||
private nodesByIdentity = new Map<string, ITreeNode<T, TFilterData>>();
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
@@ -61,19 +62,22 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
|
||||
|
||||
const isCollapsed = (element: T) => {
|
||||
const id = this.identityProvider!.getId(element).toString();
|
||||
return viewState.expanded.indexOf(id) === -1;
|
||||
};
|
||||
|
||||
const onDidCreateNode = (node: ITreeNode<T, TFilterData>) => {
|
||||
const id = this.identityProvider!.getId(node.element).toString();
|
||||
|
||||
if (viewState.focus.indexOf(id) > -1) {
|
||||
focus.push(element);
|
||||
focus.push(node.element);
|
||||
}
|
||||
|
||||
if (viewState.selection.indexOf(id) > -1) {
|
||||
selection.push(element);
|
||||
selection.push(node.element);
|
||||
}
|
||||
|
||||
return id in viewState.collapsed;
|
||||
};
|
||||
|
||||
this._refresh(input, isCollapsed);
|
||||
this._refresh(input, isCollapsed, onDidCreateNode);
|
||||
this.setFocus(focus);
|
||||
this.setSelection(selection);
|
||||
}
|
||||
@@ -83,29 +87,81 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
|
||||
throw new Error('Tree input not set');
|
||||
}
|
||||
|
||||
this._refresh(element);
|
||||
let isCollapsed: ((el: T) => boolean | undefined) | undefined;
|
||||
|
||||
if (this.identityProvider) {
|
||||
isCollapsed = element => {
|
||||
const id = this.identityProvider!.getId(element).toString();
|
||||
const node = this.nodesByIdentity.get(id);
|
||||
|
||||
if (!node) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return node.collapsed;
|
||||
};
|
||||
}
|
||||
|
||||
this._refresh(element, isCollapsed);
|
||||
}
|
||||
|
||||
resort(element: T | TInput = this.input!, recursive = true): void {
|
||||
this.model.resort((element === this.input ? null : element) as T, recursive);
|
||||
}
|
||||
|
||||
// View
|
||||
|
||||
refresh(element: T): void {
|
||||
this.model.refresh(element);
|
||||
refresh(element?: T): void {
|
||||
if (element === undefined) {
|
||||
this.view.rerender();
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.rerender(element);
|
||||
}
|
||||
|
||||
// Implementation
|
||||
|
||||
private _refresh(element: TInput | T, isCollapsed?: (el: T) => boolean): void {
|
||||
this.model.setChildren((element === this.input ? null : element) as T, this.createIterator(element, isCollapsed));
|
||||
private _refresh(element: TInput | T, isCollapsed?: (el: T) => boolean | undefined, onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void): void {
|
||||
let onDidDeleteNode: ((node: ITreeNode<T, TFilterData>) => void) | undefined;
|
||||
|
||||
if (this.identityProvider) {
|
||||
const insertedElements = new Set<string>();
|
||||
|
||||
const outerOnDidCreateNode = onDidCreateNode;
|
||||
onDidCreateNode = (node: ITreeNode<T, TFilterData>) => {
|
||||
const id = this.identityProvider!.getId(node.element).toString();
|
||||
|
||||
insertedElements.add(id);
|
||||
this.nodesByIdentity.set(id, node);
|
||||
|
||||
if (outerOnDidCreateNode) {
|
||||
outerOnDidCreateNode(node);
|
||||
}
|
||||
};
|
||||
|
||||
onDidDeleteNode = (node: ITreeNode<T, TFilterData>) => {
|
||||
const id = this.identityProvider!.getId(node.element).toString();
|
||||
|
||||
if (!insertedElements.has(id)) {
|
||||
this.nodesByIdentity.delete(id);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.model.setChildren((element === this.input ? null : element) as T, this.iterate(element, isCollapsed).elements, onDidCreateNode, onDidDeleteNode);
|
||||
}
|
||||
|
||||
private createIterator(element: TInput | T, isCollapsed?: (el: T) => boolean): Iterator<ITreeElement<T>> {
|
||||
const children = Iterator.fromArray(this.dataSource.getChildren(element));
|
||||
private iterate(element: TInput | T, isCollapsed?: (el: T) => boolean | undefined): { elements: Iterator<ITreeElement<T>>, size: number } {
|
||||
const children = this.dataSource.getChildren(element);
|
||||
const elements = Iterator.map<any, ITreeElement<T>>(Iterator.fromArray(children), element => {
|
||||
const { elements: children, size } = this.iterate(element, isCollapsed);
|
||||
const collapsed = size === 0 ? undefined : (isCollapsed && isCollapsed(element));
|
||||
|
||||
return Iterator.map<any, ITreeElement<T>>(children, element => ({
|
||||
element,
|
||||
children: this.createIterator(element),
|
||||
collapsed: isCollapsed && isCollapsed(element)
|
||||
}));
|
||||
return { element, children, collapsed };
|
||||
});
|
||||
|
||||
return { elements, size: children.length };
|
||||
}
|
||||
|
||||
protected createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IDataTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
|
||||
@@ -123,20 +179,20 @@ export class DataTree<TInput, T, TFilterData = void> extends AbstractTree<T | nu
|
||||
const focus = this.getFocus().map(getId);
|
||||
const selection = this.getSelection().map(getId);
|
||||
|
||||
const collapsed: string[] = [];
|
||||
const expanded: string[] = [];
|
||||
const root = this.model.getNode();
|
||||
const queue = [root];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const node = queue.shift()!;
|
||||
|
||||
if (node !== root && node.collapsed) {
|
||||
collapsed.push(getId(node.element!));
|
||||
if (node !== root && node.collapsible && !node.collapsed) {
|
||||
expanded.push(getId(node.element!));
|
||||
}
|
||||
|
||||
queue.push(...node.children);
|
||||
}
|
||||
|
||||
return { focus, selection, collapsed };
|
||||
return { focus, selection, expanded };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,11 +31,16 @@ export class IndexTree<T, TFilterData = void> extends AbstractTree<T, TFilterDat
|
||||
return this.model.splice(location, deleteCount, toInsert);
|
||||
}
|
||||
|
||||
refresh(location: number[]): void {
|
||||
this.model.refresh(location);
|
||||
rerender(location?: number[]): void {
|
||||
if (location === undefined) {
|
||||
this.view.rerender();
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.rerender(location);
|
||||
}
|
||||
|
||||
protected createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IIndexTreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, number[]> {
|
||||
return new IndexTreeModel(view, this.rootElement, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,8 +71,6 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
|
||||
this.filter = options.filter;
|
||||
this.autoExpandSingleChildren = typeof options.autoExpandSingleChildren === 'undefined' ? false : options.autoExpandSingleChildren;
|
||||
|
||||
// this.onDidChangeCollapseState(node => console.log(node.collapsed, node));
|
||||
|
||||
this.root = {
|
||||
parent: undefined,
|
||||
element: rootElement,
|
||||
@@ -177,7 +175,7 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
|
||||
return result;
|
||||
}
|
||||
|
||||
refresh(location: number[]): void {
|
||||
rerender(location: number[]): void {
|
||||
if (location.length === 0) {
|
||||
throw new Error('Invalid tree location');
|
||||
}
|
||||
@@ -280,6 +278,21 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
|
||||
return result;
|
||||
}
|
||||
|
||||
expandTo(location: number[]): void {
|
||||
this.eventBufferer.bufferEvents(() => {
|
||||
let node = this.getTreeNode(location);
|
||||
|
||||
while (node.parent) {
|
||||
node = node.parent;
|
||||
location = location.slice(0, location.length - 1);
|
||||
|
||||
if (node.collapsed) {
|
||||
this._setCollapsed(location, false);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
refilter(): void {
|
||||
const previousRenderNodeCount = this.root.renderNodeCount;
|
||||
const toInsert = this.updateNodeAfterFilterChange(this.root);
|
||||
@@ -582,4 +595,4 @@ export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = voi
|
||||
|
||||
return this._getLastElementAncestor(node.children[node.children.length - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,11 +36,20 @@ export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends
|
||||
return this.model.setChildren(element, children, onDidCreateNode, onDidDeleteNode);
|
||||
}
|
||||
|
||||
refresh(element: T): void {
|
||||
this.model.refresh(element);
|
||||
rerender(element?: T): void {
|
||||
if (element === undefined) {
|
||||
this.view.rerender();
|
||||
return;
|
||||
}
|
||||
|
||||
this.model.rerender(element);
|
||||
}
|
||||
|
||||
resort(element: T, recursive = true): void {
|
||||
this.model.resort(element, recursive);
|
||||
}
|
||||
|
||||
protected createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IObjectTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
|
||||
return new ObjectTreeModel(view, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
|
||||
|
||||
private model: IndexTreeModel<T | null, TFilterData>;
|
||||
private nodes = new Map<T | null, ITreeNode<T, TFilterData>>();
|
||||
private sorter?: ITreeSorter<ITreeElement<T>>;
|
||||
private sorter?: ITreeSorter<{ element: T; }>;
|
||||
|
||||
readonly onDidSplice: Event<ITreeModelSpliceEvent<T | null, TFilterData>>;
|
||||
readonly onDidChangeCollapseState: Event<ICollapseStateChangeEvent<T, TFilterData>>;
|
||||
@@ -49,6 +49,15 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
|
||||
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
|
||||
): Iterator<ITreeElement<T | null>> {
|
||||
const location = this.getElementLocation(element);
|
||||
return this._setChildren(location, this.preserveCollapseState(children), onDidCreateNode, onDidDeleteNode);
|
||||
}
|
||||
|
||||
private _setChildren(
|
||||
location: number[],
|
||||
children: ISequence<ITreeElement<T>> | undefined,
|
||||
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void,
|
||||
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
|
||||
): Iterator<ITreeElement<T | null>> {
|
||||
const insertedElements = new Set<T | null>();
|
||||
|
||||
const _onDidCreateNode = (node: ITreeNode<T, TFilterData>) => {
|
||||
@@ -73,13 +82,13 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
|
||||
return this.model.splice(
|
||||
[...location, 0],
|
||||
Number.MAX_VALUE,
|
||||
this.preserveCollapseState(children),
|
||||
children,
|
||||
_onDidCreateNode,
|
||||
_onDidDeleteNode
|
||||
);
|
||||
}
|
||||
|
||||
private preserveCollapseState(elements: ISequence<ITreeElement<T | null>> | undefined): ISequence<ITreeElement<T | null>> {
|
||||
private preserveCollapseState(elements: ISequence<ITreeElement<T>> | undefined): ISequence<ITreeElement<T>> {
|
||||
let iterator = elements ? getSequenceIterator(elements) : Iterator.empty<ITreeElement<T>>();
|
||||
|
||||
if (this.sorter) {
|
||||
@@ -90,11 +99,14 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
|
||||
const node = this.nodes.get(treeElement.element);
|
||||
|
||||
if (!node) {
|
||||
return treeElement;
|
||||
return {
|
||||
...treeElement,
|
||||
children: this.preserveCollapseState(treeElement.children)
|
||||
};
|
||||
}
|
||||
|
||||
const collapsible = typeof treeElement.collapsible === 'boolean' ? treeElement.collapsible : node.collapsible;
|
||||
const collapsed = typeof treeElement.collapsed !== 'undefined' ? treeElement.collapsed : (collapsible && node.collapsed);
|
||||
const collapsed = typeof treeElement.collapsed !== 'undefined' ? treeElement.collapsed : node.collapsed;
|
||||
|
||||
return {
|
||||
...treeElement,
|
||||
@@ -105,9 +117,35 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
|
||||
});
|
||||
}
|
||||
|
||||
refresh(element: T): void {
|
||||
rerender(element: T): void {
|
||||
const location = this.getElementLocation(element);
|
||||
this.model.refresh(location);
|
||||
this.model.rerender(location);
|
||||
}
|
||||
|
||||
resort(element: T | null = null, recursive = true): void {
|
||||
if (!this.sorter) {
|
||||
return;
|
||||
}
|
||||
|
||||
const location = this.getElementLocation(element);
|
||||
const node = this.model.getNode(location);
|
||||
|
||||
this._setChildren(location, this.resortChildren(node, recursive));
|
||||
}
|
||||
|
||||
private resortChildren(node: ITreeNode<T | null, TFilterData>, recursive: boolean, first = true): ISequence<ITreeElement<T>> {
|
||||
let childrenNodes = Iterator.fromArray(node.children as ITreeNode<T, TFilterData>[]);
|
||||
|
||||
if (recursive || first) {
|
||||
childrenNodes = Iterator.fromArray(Iterator.collect(childrenNodes).sort(this.sorter!.compare.bind(this.sorter)));
|
||||
}
|
||||
|
||||
return Iterator.map<ITreeNode<T | null, TFilterData>, ITreeElement<T>>(childrenNodes, node => ({
|
||||
element: node.element as T,
|
||||
collapsible: node.collapsible,
|
||||
collapsed: node.collapsed,
|
||||
children: this.resortChildren(node, recursive, false)
|
||||
}));
|
||||
}
|
||||
|
||||
getParentElement(ref: T | null = null): T | null {
|
||||
@@ -150,6 +188,11 @@ export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends Non
|
||||
return this.model.setCollapsed(location, collapsed, recursive);
|
||||
}
|
||||
|
||||
expandTo(element: T): void {
|
||||
const location = this.getElementLocation(element);
|
||||
this.model.expandTo(location);
|
||||
}
|
||||
|
||||
refilter(): void {
|
||||
this.model.refilter();
|
||||
}
|
||||
|
||||
@@ -122,6 +122,7 @@ export interface ITreeModel<T, TFilterData, TRef> {
|
||||
isCollapsible(location: TRef): boolean;
|
||||
isCollapsed(location: TRef): boolean;
|
||||
setCollapsed(location: TRef, collapsed?: boolean, recursive?: boolean): boolean;
|
||||
expandTo(location: TRef): void;
|
||||
|
||||
refilter(): void;
|
||||
}
|
||||
@@ -144,7 +145,7 @@ export interface ITreeMouseEvent<T> {
|
||||
export interface ITreeContextMenuEvent<T> {
|
||||
browserEvent: UIEvent;
|
||||
element: T | null;
|
||||
anchor: HTMLElement | { x: number; y: number; } | undefined;
|
||||
anchor: HTMLElement | { x: number; y: number; };
|
||||
}
|
||||
|
||||
export interface ITreeNavigator<T> {
|
||||
|
||||
Reference in New Issue
Block a user