Merge from master

This commit is contained in:
Raj Musuku
2019-02-21 17:56:04 -08:00
parent 5a146e34fa
commit 666ae11639
11482 changed files with 119352 additions and 255574 deletions

View File

@@ -28,10 +28,6 @@
.monaco-action-bar .action-item {
cursor: pointer;
display: inline-block;
-ms-transition: -ms-transform 50ms ease;
-webkit-transition: -webkit-transform 50ms ease;
-moz-transition: -moz-transform 50ms ease;
-o-transition: -o-transform 50ms ease;
transition: transform 50ms ease;
position: relative; /* DO NOT REMOVE - this is the key to preventing the ghosting icon bug in Chrome 42 */
}
@@ -41,11 +37,7 @@
}
.monaco-action-bar.animated .action-item.active {
-ms-transform: scale(1.272019649, 1.272019649); /* 1.272019649 = √φ */
-webkit-transform: scale(1.272019649, 1.272019649);
-moz-transform: scale(1.272019649, 1.272019649);
-o-transform: scale(1.272019649, 1.272019649);
transform: scale(1.272019649, 1.272019649);
transform: scale(1.272019649, 1.272019649); /* 1.272019649 = √φ */
}
.monaco-action-bar .action-item .icon {
@@ -87,10 +79,6 @@
}
.monaco-action-bar.animated.vertical .action-item.active {
-ms-transform: translate(5px, 0);
-webkit-transform: translate(5px, 0);
-moz-transform: translate(5px, 0);
-o-transform: translate(5px, 0);
transform: translate(5px, 0);
}

View File

@@ -3,14 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./actionbar';
import * as platform from 'vs/base/common/platform';
import * as nls from 'vs/nls';
import * as lifecycle from 'vs/base/common/lifecycle';
import { TPromise } from 'vs/base/common/winjs.base';
import { Builder, $ } from 'vs/base/browser/builder';
import { Disposable, dispose } from 'vs/base/common/lifecycle';
import { SelectBox, ISelectBoxOptions } from 'vs/base/browser/ui/selectBox/selectBox';
import { IAction, IActionRunner, Action, IActionChangeEvent, ActionRunner, IRunEvent } from 'vs/base/common/actions';
import * as DOM from 'vs/base/browser/dom';
@@ -36,77 +32,78 @@ export interface IBaseActionItemOptions {
isMenu?: boolean;
}
export class BaseActionItem implements IActionItem {
export class BaseActionItem extends Disposable implements IActionItem {
public builder: Builder;
public _callOnDispose: lifecycle.IDisposable[];
public _context: any;
public _action: IAction;
element?: HTMLElement;
_context: any;
_action: IAction;
private _actionRunner: IActionRunner;
constructor(context: any, action: IAction, protected options?: IBaseActionItemOptions) {
this._callOnDispose = [];
super();
this._context = context || this;
this._action = action;
if (action instanceof Action) {
this._callOnDispose.push(action.onDidChange(event => {
if (!this.builder) {
this._register(action.onDidChange(event => {
if (!this.element) {
// we have not been rendered yet, so there
// is no point in updating the UI
return;
}
this._handleActionChangeEvent(event);
this.handleActionChangeEvent(event);
}));
}
}
protected _handleActionChangeEvent(event: IActionChangeEvent): void {
private handleActionChangeEvent(event: IActionChangeEvent): void {
if (event.enabled !== void 0) {
this._updateEnabled();
this.updateEnabled();
}
if (event.checked !== void 0) {
this._updateChecked();
this.updateChecked();
}
if (event.class !== void 0) {
this._updateClass();
this.updateClass();
}
if (event.label !== void 0) {
this._updateLabel();
this._updateTooltip();
this.updateLabel();
this.updateTooltip();
}
if (event.tooltip !== void 0) {
this._updateTooltip();
this.updateTooltip();
}
}
public get callOnDispose() {
return this._callOnDispose;
}
public set actionRunner(actionRunner: IActionRunner) {
set actionRunner(actionRunner: IActionRunner) {
this._actionRunner = actionRunner;
}
public get actionRunner(): IActionRunner {
get actionRunner(): IActionRunner {
return this._actionRunner;
}
public getAction(): IAction {
getAction(): IAction {
return this._action;
}
public isEnabled(): boolean {
isEnabled(): boolean {
return this._action.enabled;
}
public setActionContext(newContext: any): void {
setActionContext(newContext: any): void {
this._context = newContext;
}
public render(container: HTMLElement): void {
this.builder = $(container);
render(container: HTMLElement): void {
this.element = container;
Gesture.addTarget(container);
const enableDragging = this.options && this.options.draggable;
@@ -114,20 +111,19 @@ export class BaseActionItem implements IActionItem {
container.draggable = true;
}
this.builder.on(EventType.Tap, e => this.onClick(e));
this._register(DOM.addDisposableListener(this.element, EventType.Tap, e => this.onClick(e)));
this.builder.on(DOM.EventType.MOUSE_DOWN, (e) => {
this._register(DOM.addDisposableListener(this.element, DOM.EventType.MOUSE_DOWN, e => {
if (!enableDragging) {
DOM.EventHelper.stop(e, true); // do not run when dragging is on because that would disable it
}
const mouseEvent = e as MouseEvent;
if (this._action.enabled && mouseEvent.button === 0) {
this.builder.addClass('active');
if (this._action.enabled && e.button === 0 && this.element) {
DOM.addClass(this.element, 'active');
}
});
}));
this.builder.on(DOM.EventType.CLICK, (e) => {
this._register(DOM.addDisposableListener(this.element, DOM.EventType.CLICK, e => {
DOM.EventHelper.stop(e, true);
// See https://developer.mozilla.org/en-US/Add-ons/WebExtensions/Interact_with_the_clipboard
// > Writing to the clipboard
@@ -142,82 +138,90 @@ export class BaseActionItem implements IActionItem {
} else {
platform.setImmediate(() => this.onClick(e));
}
});
}));
this.builder.on([DOM.EventType.MOUSE_UP, DOM.EventType.MOUSE_OUT], (e) => {
DOM.EventHelper.stop(e);
this.builder.removeClass('active');
this._register(DOM.addDisposableListener(this.element, DOM.EventType.DBLCLICK, e => {
DOM.EventHelper.stop(e, true);
}));
[DOM.EventType.MOUSE_UP, DOM.EventType.MOUSE_OUT].forEach(event => {
this._register(DOM.addDisposableListener(this.element!, event, e => {
DOM.EventHelper.stop(e);
DOM.removeClass(this.element!, 'active');
}));
});
}
public onClick(event: DOM.EventLike): void {
onClick(event: DOM.EventLike): void {
DOM.EventHelper.stop(event, true);
let context: any;
if (types.isUndefinedOrNull(this._context) || !types.isObject(this._context)) {
if (types.isUndefinedOrNull(this._context)) {
context = event;
} else {
context = this._context;
context.event = event;
if (types.isObject(context)) {
context.event = event;
}
}
this._actionRunner.run(this._action, context);
}
public focus(): void {
if (this.builder) {
this.builder.domFocus();
this.builder.addClass('focused');
focus(): void {
if (this.element) {
this.element.focus();
DOM.addClass(this.element, 'focused');
}
}
public blur(): void {
if (this.builder) {
this.builder.domBlur();
this.builder.removeClass('focused');
blur(): void {
if (this.element) {
this.element.blur();
DOM.removeClass(this.element, 'focused');
}
}
protected _updateEnabled(): void {
protected updateEnabled(): void {
// implement in subclass
}
protected _updateLabel(): void {
protected updateLabel(): void {
// implement in subclass
}
protected _updateTooltip(): void {
protected updateTooltip(): void {
// implement in subclass
}
protected _updateClass(): void {
protected updateClass(): void {
// implement in subclass
}
protected _updateChecked(): void {
protected updateChecked(): void {
// implement in subclass
}
public dispose(): void {
if (this.builder) {
this.builder.destroy();
this.builder = null;
dispose(): void {
if (this.element) {
DOM.removeNode(this.element);
this.element = undefined;
}
this._callOnDispose = lifecycle.dispose(this._callOnDispose);
super.dispose();
}
}
export class Separator extends Action {
public static readonly ID = 'vs.actions.separator';
static readonly ID = 'vs.actions.separator';
constructor(label?: string, order?: number) {
constructor(label?: string) {
super(Separator.ID, label, label ? 'separator text' : 'separator');
this.checked = false;
this.radio = false;
this.enabled = false;
this.order = order;
}
}
@@ -229,9 +233,10 @@ export interface IActionItemOptions extends IBaseActionItemOptions {
export class ActionItem extends BaseActionItem {
protected $e: Builder;
protected label: HTMLElement;
protected options: IActionItemOptions;
private cssClass: string;
private cssClass?: string;
constructor(context: any, action: IAction, options: IActionItemOptions = {}) {
super(context, action, options);
@@ -242,45 +247,47 @@ export class ActionItem extends BaseActionItem {
this.cssClass = '';
}
public render(container: HTMLElement): void {
render(container: HTMLElement): void {
super.render(container);
this.$e = $('a.action-label').appendTo(this.builder);
if (this.element) {
this.label = DOM.append(this.element, DOM.$('a.action-label'));
}
if (this._action.id === Separator.ID) {
// A separator is a presentation item
this.$e.attr({ role: 'presentation' });
this.label.setAttribute('role', 'presentation'); // A separator is a presentation item
} else {
if (this.options.isMenu) {
this.$e.attr({ role: 'menuitem' });
this.label.setAttribute('role', 'menuitem');
} else {
this.$e.attr({ role: 'button' });
this.label.setAttribute('role', 'button');
}
}
if (this.options.label && this.options.keybinding) {
$('span.keybinding').text(this.options.keybinding).appendTo(this.builder);
if (this.options.label && this.options.keybinding && this.element) {
DOM.append(this.element, DOM.$('span.keybinding')).textContent = this.options.keybinding;
}
this._updateClass();
this._updateLabel();
this._updateTooltip();
this._updateEnabled();
this._updateChecked();
this.updateClass();
this.updateLabel();
this.updateTooltip();
this.updateEnabled();
this.updateChecked();
}
public focus(): void {
focus(): void {
super.focus();
this.$e.domFocus();
this.label.focus();
}
public _updateLabel(): void {
updateLabel(): void {
if (this.options.label) {
this.$e.text(this.getAction().label);
this.label.textContent = this.getAction().label;
}
}
public _updateTooltip(): void {
let title: string = null;
updateTooltip(): void {
let title: string | null = null;
if (this.getAction().tooltip) {
title = this.getAction().tooltip;
@@ -294,54 +301,67 @@ export class ActionItem extends BaseActionItem {
}
if (title) {
this.$e.attr({ title: title });
this.label.title = title;
}
}
public _updateClass(): void {
updateClass(): void {
if (this.cssClass) {
this.$e.removeClass(this.cssClass);
DOM.removeClasses(this.label, this.cssClass);
}
if (this.options.icon) {
this.cssClass = this.getAction().class;
this.$e.addClass('icon');
DOM.addClass(this.label, 'icon');
if (this.cssClass) {
this.$e.addClass(this.cssClass);
DOM.addClasses(this.label, this.cssClass);
}
this._updateEnabled();
this.updateEnabled();
} else {
this.$e.removeClass('icon');
DOM.removeClass(this.label, 'icon');
}
}
public _updateEnabled(): void {
updateEnabled(): void {
if (this.getAction().enabled) {
this.builder.removeClass('disabled');
this.$e.removeClass('disabled');
this.$e.attr({ tabindex: 0 });
this.label.removeAttribute('aria-disabled');
if (this.element) {
DOM.removeClass(this.element, 'disabled');
}
DOM.removeClass(this.label, 'disabled');
this.label.tabIndex = 0;
} else {
this.builder.addClass('disabled');
this.$e.addClass('disabled');
DOM.removeTabIndexAndUpdateFocus(this.$e.getHTMLElement());
this.label.setAttribute('aria-disabled', 'true');
if (this.element) {
DOM.addClass(this.element, 'disabled');
}
DOM.addClass(this.label, 'disabled');
DOM.removeTabIndexAndUpdateFocus(this.label);
}
}
public _updateChecked(): void {
updateChecked(): void {
if (this.getAction().checked) {
this.$e.addClass('checked');
DOM.addClass(this.label, 'checked');
} else {
this.$e.removeClass('checked');
DOM.removeClass(this.label, 'checked');
}
}
}
export enum ActionsOrientation {
export const enum ActionsOrientation {
HORIZONTAL,
HORIZONTAL_REVERSE,
VERTICAL,
VERTICAL_REVERSE,
}
export interface ActionTrigger {
keys: KeyCode[];
keyDown: boolean;
}
export interface IActionItemProvider {
(action: IAction): IActionItem;
}
@@ -353,54 +373,69 @@ export interface IActionBarOptions {
actionRunner?: IActionRunner;
ariaLabel?: string;
animated?: boolean;
isMenu?: boolean;
triggerKeys?: ActionTrigger;
}
let defaultOptions: IActionBarOptions = {
orientation: ActionsOrientation.HORIZONTAL,
context: null
context: null,
triggerKeys: {
keys: [KeyCode.Enter, KeyCode.Space],
keyDown: false
}
};
export interface IActionOptions extends IActionItemOptions {
index?: number;
}
export class ActionBar implements IActionRunner {
export class ActionBar extends Disposable implements IActionRunner {
public options: IActionBarOptions;
options: IActionBarOptions;
private _actionRunner: IActionRunner;
private _context: any;
// Items
public items: IActionItem[];
private focusedItem: number;
items: IActionItem[];
protected focusedItem?: number;
private focusTracker: DOM.IFocusTracker;
// Elements
public domNode: HTMLElement;
private actionsList: HTMLElement;
domNode: HTMLElement;
protected actionsList: HTMLElement;
private toDispose: lifecycle.IDisposable[];
private _onDidBlur = this._register(new Emitter<void>());
get onDidBlur(): Event<void> { return this._onDidBlur.event; }
private _onDidBlur = new Emitter<void>();
private _onDidCancel = new Emitter<void>();
private _onDidRun = new Emitter<IRunEvent>();
private _onDidBeforeRun = new Emitter<IRunEvent>();
private _onDidCancel = this._register(new Emitter<void>());
get onDidCancel(): Event<void> { return this._onDidCancel.event; }
private _onDidRun = this._register(new Emitter<IRunEvent>());
get onDidRun(): Event<IRunEvent> { return this._onDidRun.event; }
private _onDidBeforeRun = this._register(new Emitter<IRunEvent>());
get onDidBeforeRun(): Event<IRunEvent> { return this._onDidBeforeRun.event; }
constructor(container: HTMLElement, options: IActionBarOptions = defaultOptions) {
super();
this.options = options;
this._context = options.context;
this.toDispose = [];
this._actionRunner = this.options.actionRunner;
if (!this._actionRunner) {
this._actionRunner = new ActionRunner();
this.toDispose.push(this._actionRunner);
if (!this.options.triggerKeys) {
this.options.triggerKeys = defaultOptions.triggerKeys;
}
this.toDispose.push(this._actionRunner.onDidRun(e => this._onDidRun.fire(e)));
this.toDispose.push(this._actionRunner.onDidBeforeRun(e => this._onDidBeforeRun.fire(e)));
if (this.options.actionRunner) {
this._actionRunner = this.options.actionRunner;
} else {
this._actionRunner = new ActionRunner();
this._register(this._actionRunner);
}
this._register(this._actionRunner.onDidRun(e => this._onDidRun.fire(e)));
this._register(this._actionRunner.onDidBeforeRun(e => this._onDidBeforeRun.fire(e)));
this.items = [];
this.focusedItem = undefined;
@@ -437,8 +472,8 @@ export class ActionBar implements IActionRunner {
break;
}
$(this.domNode).on(DOM.EventType.KEY_DOWN, (e) => {
let event = new StandardKeyboardEvent(e as KeyboardEvent);
this._register(DOM.addDisposableListener(this.domNode, DOM.EventType.KEY_DOWN, e => {
let event = new StandardKeyboardEvent(e);
let eventHandled = true;
if (event.equals(previousKey)) {
@@ -447,8 +482,11 @@ export class ActionBar implements IActionRunner {
this.focusNext();
} else if (event.equals(KeyCode.Escape)) {
this.cancel();
} else if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
// Nothing, just staying out of the else branch
} else if (this.isTriggerKeyEvent(event)) {
// Staying out of the else branch even if not triggered
if (this.options.triggerKeys && this.options.triggerKeys.keyDown) {
this.doTrigger(event);
}
} else {
eventHandled = false;
}
@@ -457,14 +495,17 @@ export class ActionBar implements IActionRunner {
event.preventDefault();
event.stopPropagation();
}
});
}));
$(this.domNode).on(DOM.EventType.KEY_UP, (e) => {
let event = new StandardKeyboardEvent(e as KeyboardEvent);
this._register(DOM.addDisposableListener(this.domNode, DOM.EventType.KEY_UP, e => {
let event = new StandardKeyboardEvent(e);
// Run action on Enter/Space
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
this.doTrigger(event);
if (this.isTriggerKeyEvent(event)) {
if (this.options.triggerKeys && !this.options.triggerKeys.keyDown) {
this.doTrigger(event);
}
event.preventDefault();
event.stopPropagation();
}
@@ -473,84 +514,32 @@ export class ActionBar implements IActionRunner {
else if (event.equals(KeyCode.Tab) || event.equals(KeyMod.Shift | KeyCode.Tab)) {
this.updateFocusedItem();
}
});
}));
this.focusTracker = DOM.trackFocus(this.domNode);
this.toDispose.push(this.focusTracker.onDidBlur(() => {
this.focusTracker = this._register(DOM.trackFocus(this.domNode));
this._register(this.focusTracker.onDidBlur(() => {
if (document.activeElement === this.domNode || !DOM.isAncestor(document.activeElement, this.domNode)) {
this._onDidBlur.fire();
this.focusedItem = undefined;
}
}));
this.toDispose.push(this.focusTracker.onDidFocus(() => this.updateFocusedItem()));
this._register(this.focusTracker.onDidFocus(() => this.updateFocusedItem()));
this.actionsList = document.createElement('ul');
this.actionsList.className = 'actions-container';
if (this.options.isMenu) {
this.actionsList.setAttribute('role', 'menu');
} else {
this.actionsList.setAttribute('role', 'toolbar');
}
this.actionsList.setAttribute('role', 'toolbar');
if (this.options.ariaLabel) {
this.actionsList.setAttribute('aria-label', this.options.ariaLabel);
}
if (this.options.isMenu) {
this.domNode.tabIndex = 0;
$(this.domNode).on(DOM.EventType.MOUSE_OUT, (e) => {
let relatedTarget = (e as MouseEvent).relatedTarget as HTMLElement;
if (!DOM.isAncestor(relatedTarget, this.domNode)) {
this.focusedItem = undefined;
this.updateFocus();
e.stopPropagation();
}
});
$(this.actionsList).on(DOM.EventType.MOUSE_OVER, (e) => {
let target = e.target as HTMLElement;
if (!target || !DOM.isAncestor(target, this.actionsList) || target === this.actionsList) {
return;
}
while (target.parentElement !== this.actionsList) {
target = target.parentElement;
}
if (DOM.hasClass(target, 'action-item')) {
const lastFocusedItem = this.focusedItem;
this.setFocusedItem(target);
if (lastFocusedItem !== this.focusedItem) {
this.updateFocus();
}
}
});
}
this.domNode.appendChild(this.actionsList);
container.appendChild(this.domNode);
}
public get onDidBlur(): Event<void> {
return this._onDidBlur.event;
}
public get onDidCancel(): Event<void> {
return this._onDidCancel.event;
}
public get onDidRun(): Event<IRunEvent> {
return this._onDidRun.event;
}
public get onDidBeforeRun(): Event<IRunEvent> {
return this._onDidBeforeRun.event;
}
public setAriaLabel(label: string): void {
setAriaLabel(label: string): void {
if (label) {
this.actionsList.setAttribute('aria-label', label);
} else {
@@ -558,14 +547,15 @@ export class ActionBar implements IActionRunner {
}
}
private setFocusedItem(element: HTMLElement): void {
for (let i = 0; i < this.actionsList.children.length; i++) {
let elem = this.actionsList.children[i];
if (element === elem) {
this.focusedItem = i;
break;
}
private isTriggerKeyEvent(event: StandardKeyboardEvent): boolean {
let ret = false;
if (this.options.triggerKeys) {
this.options.triggerKeys.keys.forEach(keyCode => {
ret = ret || event.equals(keyCode);
});
}
return ret;
}
private updateFocusedItem(): void {
@@ -578,32 +568,31 @@ export class ActionBar implements IActionRunner {
}
}
public get context(): any {
get context(): any {
return this._context;
}
public set context(context: any) {
set context(context: any) {
this._context = context;
this.items.forEach(i => i.setActionContext(context));
}
public get actionRunner(): IActionRunner {
get actionRunner(): IActionRunner | undefined {
return this._actionRunner;
}
public set actionRunner(actionRunner: IActionRunner) {
set actionRunner(actionRunner: IActionRunner | undefined) {
if (actionRunner) {
this._actionRunner = actionRunner;
this.items.forEach(item => item.actionRunner = actionRunner);
}
}
public getContainer(): HTMLElement {
getContainer(): HTMLElement {
return this.domNode;
}
public push(arg: IAction | IAction[], options: IActionOptions = {}): void {
push(arg: IAction | IAction[], options: IActionOptions = {}): void {
const actions: IAction[] = !Array.isArray(arg) ? [arg] : arg;
let index = types.isNumber(options.index) ? options.index : null;
@@ -614,12 +603,12 @@ export class ActionBar implements IActionRunner {
actionItemElement.setAttribute('role', 'presentation');
// Prevent native context menu on actions
$(actionItemElement).on(DOM.EventType.CONTEXT_MENU, (e: DOM.EventLike) => {
this._register(DOM.addDisposableListener(actionItemElement, DOM.EventType.CONTEXT_MENU, (e: DOM.EventLike) => {
e.preventDefault();
e.stopPropagation();
});
}));
let item: IActionItem = null;
let item: IActionItem | null = null;
if (this.options.actionItemProvider) {
item = this.options.actionItemProvider(action);
@@ -641,52 +630,73 @@ export class ActionBar implements IActionRunner {
this.items.splice(index, 0, item);
index++;
}
});
}
public getWidth(index: number): number {
getWidth(index: number): number {
if (index >= 0 && index < this.actionsList.children.length) {
return this.actionsList.children.item(index).clientWidth;
const item = this.actionsList.children.item(index);
if (item) {
return item.clientWidth;
}
}
return 0;
}
public getHeight(index: number): number {
getHeight(index: number): number {
if (index >= 0 && index < this.actionsList.children.length) {
return this.actionsList.children.item(index).clientHeight;
const item = this.actionsList.children.item(index);
if (item) {
return item.clientHeight;
}
}
return 0;
}
public pull(index: number): void {
pull(index: number): void {
if (index >= 0 && index < this.items.length) {
this.items.splice(index, 1);
this.actionsList.removeChild(this.actionsList.childNodes[index]);
}
}
public clear(): void {
this.items = lifecycle.dispose(this.items);
$(this.actionsList).empty();
clear(): void {
this.items = dispose(this.items);
DOM.clearNode(this.actionsList);
}
public length(): number {
length(): number {
return this.items.length;
}
public isEmpty(): boolean {
isEmpty(): boolean {
return this.items.length === 0;
}
public focus(selectFirst?: boolean): void {
focus(index?: number): void;
focus(selectFirst?: boolean): void;
focus(arg?: any): void {
let selectFirst: boolean = false;
let index: number | undefined = void 0;
if (arg === undefined) {
selectFirst = true;
} else if (typeof arg === 'number') {
index = arg;
} else if (typeof arg === 'boolean') {
selectFirst = arg;
}
if (selectFirst && typeof this.focusedItem === 'undefined') {
// Focus the first enabled item
this.focusedItem = this.items.length - 1;
this.focusNext();
} else {
if (index !== undefined) {
this.focusedItem = index;
}
this.updateFocus();
}
}
@@ -736,22 +746,22 @@ export class ActionBar implements IActionRunner {
this.updateFocus(true);
}
private updateFocus(fromRight?: boolean): void {
protected updateFocus(fromRight?: boolean): void {
if (typeof this.focusedItem === 'undefined') {
this.domNode.focus();
this.actionsList.focus();
}
for (let i = 0; i < this.items.length; i++) {
let item = this.items[i];
let actionItem = <any>item;
let actionItem = item;
if (i === this.focusedItem) {
if (types.isFunction(actionItem.isEnabled)) {
if (actionItem.isEnabled() && types.isFunction(actionItem.focus)) {
actionItem.focus(fromRight);
} else {
this.domNode.focus();
this.actionsList.focus();
}
}
} else {
@@ -771,7 +781,7 @@ export class ActionBar implements IActionRunner {
let actionItem = this.items[this.focusedItem];
if (actionItem instanceof BaseActionItem) {
const context = (actionItem._context === null || actionItem._context === undefined) ? event : actionItem._context;
this.run(actionItem._action, context).done();
this.run(actionItem._action, context);
}
}
@@ -783,78 +793,63 @@ export class ActionBar implements IActionRunner {
this._onDidCancel.fire();
}
public run(action: IAction, context?: any): TPromise<void> {
run(action: IAction, context?: any): Thenable<void> {
return this._actionRunner.run(action, context);
}
public dispose(): void {
if (this.items !== null) {
lifecycle.dispose(this.items);
}
this.items = null;
dispose(): void {
dispose(this.items);
this.items = [];
if (this.focusTracker) {
this.focusTracker.dispose();
this.focusTracker = null;
}
DOM.removeNode(this.getContainer());
this.toDispose = lifecycle.dispose(this.toDispose);
$(this.getContainer()).destroy();
super.dispose();
}
}
export class SelectActionItem extends BaseActionItem {
protected selectBox: SelectBox;
protected toDispose: lifecycle.IDisposable[];
constructor(ctx: any, action: IAction, options: string[], selected: number, contextViewProvider: IContextViewProvider, selectBoxOptions?: ISelectBoxOptions
) {
constructor(ctx: any, action: IAction, options: string[], selected: number, contextViewProvider: IContextViewProvider, selectBoxOptions?: ISelectBoxOptions) {
super(ctx, action);
this.selectBox = new SelectBox(options, selected, contextViewProvider, null, selectBoxOptions);
this.toDispose = [];
this.toDispose.push(this.selectBox);
this.selectBox = new SelectBox(options, selected, contextViewProvider, undefined, selectBoxOptions);
this._register(this.selectBox);
this.registerListeners();
}
public setOptions(options: string[], selected?: number, disabled?: number): void {
setOptions(options: string[], selected?: number, disabled?: number): void {
this.selectBox.setOptions(options, selected, disabled);
}
public select(index: number): void {
select(index: number): void {
this.selectBox.select(index);
}
private registerListeners(): void {
this.toDispose.push(this.selectBox.onDidSelect(e => {
this.actionRunner.run(this._action, this.getActionContext(e.selected)).done();
this._register(this.selectBox.onDidSelect(e => {
this.actionRunner.run(this._action, this.getActionContext(e.selected, e.index));
}));
}
protected getActionContext(option: string) {
protected getActionContext(option: string, index: number) {
return option;
}
public focus(): void {
focus(): void {
if (this.selectBox) {
this.selectBox.focus();
}
}
public blur(): void {
blur(): void {
if (this.selectBox) {
this.selectBox.blur();
}
}
public render(container: HTMLElement): void {
render(container: HTMLElement): void {
this.selectBox.render(container);
}
public dispose(): void {
this.toDispose = lifecycle.dispose(this.toDispose);
super.dispose();
}
}
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./aria';
import * as nls from 'vs/nls';
import { isMacintosh } from 'vs/base/common/platform';

View File

@@ -23,18 +23,20 @@
outline: none;
}
.monaco-breadcrumbs .monaco-breadcrumb-item:not(:first-child)::before {
background-image: url(./collapsed.svg);
opacity: .7;
width: 16px;
.monaco-breadcrumbs .monaco-breadcrumb-item::before {
width: 14px;
height: 16px;
display: inline-block;
background-size: 16px;
background-position: 50% 50%;
content: ' ';
}
.vs-dark .monaco-breadcrumbs .monaco-breadcrumb-item:not(:first-child)::before {
background-image: url(./collpased-dark.svg);
.monaco-breadcrumbs .monaco-breadcrumb-item:not(:nth-child(2))::before {
background-image: url(./collapsed.svg);
opacity: .7;
background-size: 16px;
background-position: 50% 50%;
}
.vs-dark .monaco-breadcrumbs .monaco-breadcrumb-item:not(:nth-child(2))::before {
background-image: url(./collpased-dark.svg);
}

View File

@@ -3,17 +3,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./breadcrumbsWidget';
import * as dom from 'vs/base/browser/dom';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { Event, Emitter } from 'vs/base/common/event';
import { Color } from 'vs/base/common/color';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { commonPrefixLength } from 'vs/base/common/arrays';
import { Color } from 'vs/base/common/color';
import { Emitter, Event } from 'vs/base/common/event';
import { dispose, IDisposable, combinedDisposable } from 'vs/base/common/lifecycle';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import 'vs/css!./breadcrumbsWidget';
export abstract class BreadcrumbsItem {
dispose(): void { }
@@ -45,10 +43,8 @@ export class SimpleBreadcrumbsItem extends BreadcrumbsItem {
export interface IBreadcrumbsWidgetStyles {
breadcrumbsBackground?: Color;
breadcrumbsForeground?: Color;
breadcrumbsHoverBackground?: Color;
breadcrumbsHoverForeground?: Color;
breadcrumbsFocusForeground?: Color;
breadcrumbsFocusAndSelectionBackground?: Color;
breadcrumbsFocusAndSelectionForeground?: Color;
}
@@ -81,6 +77,9 @@ export class BreadcrumbsWidget {
private _focusedItemIdx: number = -1;
private _selectedItemIdx: number = -1;
private _pendingLayout: IDisposable;
private _dimension: dom.Dimension;
constructor(
container: HTMLElement
) {
@@ -92,7 +91,8 @@ export class BreadcrumbsWidget {
vertical: ScrollbarVisibility.Hidden,
horizontal: ScrollbarVisibility.Auto,
horizontalScrollbarSize: 3,
useShadows: false
useShadows: false,
scrollYToX: true
});
this._disposables.push(this._scrollable);
this._disposables.push(dom.addStandardDisposableListener(this._domNode, 'click', e => this._onClick(e)));
@@ -108,20 +108,50 @@ export class BreadcrumbsWidget {
dispose(): void {
dispose(this._disposables);
dispose(this._pendingLayout);
this._onDidSelectItem.dispose();
this._onDidFocusItem.dispose();
this._onDidChangeFocus.dispose();
this._domNode.remove();
this._disposables.length = 0;
this._nodes.length = 0;
this._freeNodes.length = 0;
}
layout(dim: dom.Dimension): void {
layout(dim: dom.Dimension | undefined): void {
if (dim && dom.Dimension.equals(dim, this._dimension)) {
return;
}
if (this._pendingLayout) {
this._pendingLayout.dispose();
}
if (dim) {
// only meaure
this._pendingLayout = this._updateDimensions(dim);
} else {
this._pendingLayout = this._updateScrollbar();
}
}
private _updateDimensions(dim: dom.Dimension): IDisposable {
let disposables: IDisposable[] = [];
disposables.push(dom.modify(() => {
this._dimension = dim;
this._domNode.style.width = `${dim.width}px`;
this._domNode.style.height = `${dim.height}px`;
}
this._scrollable.setRevealOnScroll(false);
this._scrollable.scanDomNode();
this._scrollable.setRevealOnScroll(true);
disposables.push(this._updateScrollbar());
}));
return combinedDisposable(disposables);
}
private _updateScrollbar(): IDisposable {
return dom.measure(() => {
dom.measure(() => { // double RAF
this._scrollable.setRevealOnScroll(false);
this._scrollable.scanDomNode();
this._scrollable.setRevealOnScroll(true);
});
});
}
style(style: IBreadcrumbsWidgetStyles): void {
@@ -135,15 +165,9 @@ export class BreadcrumbsWidget {
if (style.breadcrumbsFocusForeground) {
content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused { color: ${style.breadcrumbsFocusForeground}}\n`;
}
if (style.breadcrumbsFocusAndSelectionBackground) {
content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused.selected { background-color: ${style.breadcrumbsFocusAndSelectionBackground}}\n`;
}
if (style.breadcrumbsFocusAndSelectionForeground) {
content += `.monaco-breadcrumbs .monaco-breadcrumb-item.focused.selected { color: ${style.breadcrumbsFocusAndSelectionForeground}}\n`;
}
if (style.breadcrumbsHoverBackground) {
content += `.monaco-breadcrumbs .monaco-breadcrumb-item:hover:not(.focused):not(.selected) { background-color: ${style.breadcrumbsHoverBackground}}\n`;
}
if (style.breadcrumbsHoverForeground) {
content += `.monaco-breadcrumbs .monaco-breadcrumb-item:hover:not(.focused):not(.selected) { color: ${style.breadcrumbsHoverForeground}}\n`;
}
@@ -204,23 +228,27 @@ export class BreadcrumbsWidget {
node.focus();
}
}
this._reveal(this._focusedItemIdx);
this._reveal(this._focusedItemIdx, true);
this._onDidFocusItem.fire({ type: 'focus', item: this._items[this._focusedItemIdx], node: this._nodes[this._focusedItemIdx], payload });
}
reveal(item: BreadcrumbsItem): void {
let idx = this._items.indexOf(item);
if (idx >= 0) {
this._reveal(idx);
this._reveal(idx, false);
}
}
private _reveal(nth: number): void {
private _reveal(nth: number, minimal: boolean): void {
const node = this._nodes[nth];
if (node) {
this._scrollable.setRevealOnScroll(false);
this._scrollable.setScrollPosition({ scrollLeft: node.offsetLeft });
this._scrollable.setRevealOnScroll(true);
const { width } = this._scrollable.getScrollDimensions();
const { scrollLeft } = this._scrollable.getScrollPosition();
if (!minimal || node.offsetLeft > scrollLeft + width || node.offsetLeft < scrollLeft) {
this._scrollable.setRevealOnScroll(false);
this._scrollable.setScrollPosition({ scrollLeft: node.offsetLeft });
this._scrollable.setRevealOnScroll(true);
}
}
}
@@ -251,11 +279,20 @@ export class BreadcrumbsWidget {
}
setItems(items: BreadcrumbsItem[]): void {
let prefix = commonPrefixLength(this._items, items, (a, b) => a.equals(b));
let removed = this._items.splice(prefix, this._items.length - prefix, ...items.slice(prefix));
this._render(prefix);
dispose(removed);
this._focus(-1, undefined);
let prefix: number | undefined;
let removed: BreadcrumbsItem[] = [];
try {
prefix = commonPrefixLength(this._items, items, (a, b) => a.equals(b));
removed = this._items.splice(prefix, this._items.length - prefix, ...items.slice(prefix));
this._render(prefix);
dispose(removed);
this._focus(-1, undefined);
} catch (e) {
let newError = new Error(`BreadcrumbsItem#setItems: newItems: ${items.length}, prefix: ${prefix}, removed: ${removed.length}`);
newError.name = e.name;
newError.stack = e.stack;
throw newError;
}
}
private _render(start: number): void {
@@ -265,19 +302,23 @@ export class BreadcrumbsWidget {
this._renderItem(item, node);
}
// case a: more nodes -> remove them
for (; start < this._nodes.length; start++) {
this._nodes[start].remove();
this._freeNodes.push(this._nodes[start]);
while (start < this._nodes.length) {
const free = this._nodes.pop();
if (free) {
this._freeNodes.push(free);
free.remove();
}
}
this._nodes.length = this._items.length;
// case b: more items -> render them
for (; start < this._items.length; start++) {
let item = this._items[start];
let node = this._freeNodes.length > 0 ? this._freeNodes.pop() : document.createElement('div');
this._renderItem(item, node);
this._domNode.appendChild(node);
this._nodes[start] = node;
if (node) {
this._renderItem(item, node);
this._domNode.appendChild(node);
this._nodes.push(node);
}
}
this.layout(undefined);
}
@@ -292,7 +333,7 @@ export class BreadcrumbsWidget {
}
private _onClick(event: IMouseEvent): void {
for (let el = event.target; el; el = el.parentElement) {
for (let el: HTMLElement | null = event.target; el; el = el.parentElement) {
let idx = this._nodes.indexOf(el as any);
if (idx >= 0) {
this._focus(idx, event);

View File

@@ -11,6 +11,7 @@
padding: 4px;
text-align: center;
cursor: pointer;
outline-offset: 2px !important;
}
.monaco-text-button:hover {

View File

@@ -3,18 +3,15 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./button';
import * as DOM from 'vs/base/browser/dom';
import { Builder, $ } from 'vs/base/browser/builder';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Color } from 'vs/base/common/color';
import { mixin } from 'vs/base/common/objects';
import { Event as BaseEvent, Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { Gesture, EventType } from 'vs/base/browser/touch';
import { Gesture } from 'vs/base/browser/touch';
export interface IButtonOptions extends IButtonStyles {
title?: boolean;
@@ -36,13 +33,13 @@ const defaultOptions: IButtonStyles = {
export class Button extends Disposable {
// {{SQL CARBON EDIT}} -- changed access modifier to protected
protected $el: Builder;
protected _element: HTMLElement;
private options: IButtonOptions;
private buttonBackground: Color;
private buttonHoverBackground: Color;
private buttonForeground: Color;
private buttonBorder: Color;
private buttonBackground: Color | undefined;
private buttonHoverBackground: Color | undefined;
private buttonForeground: Color | undefined;
private buttonBorder: Color | undefined;
private _onDidClick = this._register(new Emitter<any>());
get onDidClick(): BaseEvent<Event> { return this._onDidClick.event; }
@@ -60,50 +57,52 @@ export class Button extends Disposable {
this.buttonForeground = this.options.buttonForeground;
this.buttonBorder = this.options.buttonBorder;
this.$el = this._register($('a.monaco-button').attr({
'tabIndex': '0',
'role': 'button'
}).appendTo(container));
this._element = document.createElement('a');
DOM.addClass(this._element, 'monaco-button');
this._element.tabIndex = 0;
this._element.setAttribute('role', 'button');
Gesture.addTarget(this.$el.getHTMLElement());
container.appendChild(this._element);
this.$el.on([DOM.EventType.CLICK, EventType.Tap], e => {
Gesture.addTarget(this._element);
this._register(DOM.addDisposableListener(this._element, DOM.EventType.CLICK, e => {
if (!this.enabled) {
DOM.EventHelper.stop(e);
return;
}
this._onDidClick.fire(e);
});
}));
this.$el.on(DOM.EventType.KEY_DOWN, e => {
const event = new StandardKeyboardEvent(e as KeyboardEvent);
this._register(DOM.addDisposableListener(this._element, DOM.EventType.KEY_DOWN, e => {
const event = new StandardKeyboardEvent(e);
let eventHandled = false;
if (this.enabled && event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
this._onDidClick.fire(e);
eventHandled = true;
} else if (event.equals(KeyCode.Escape)) {
this.$el.domBlur();
this._element.blur();
eventHandled = true;
}
if (eventHandled) {
DOM.EventHelper.stop(event, true);
}
});
}));
this.$el.on(DOM.EventType.MOUSE_OVER, e => {
if (!this.$el.hasClass('disabled')) {
this._register(DOM.addDisposableListener(this._element, DOM.EventType.MOUSE_OVER, e => {
if (!DOM.hasClass(this._element, 'disabled')) {
this.setHoverBackground();
}
});
}));
this.$el.on(DOM.EventType.MOUSE_OUT, e => {
this._register(DOM.addDisposableListener(this._element, DOM.EventType.MOUSE_OUT, e => {
this.applyStyles(); // restore standard styles
});
}));
// Also set hover background when button is focused for feedback
this.focusTracker = this._register(DOM.trackFocus(this.$el.getHTMLElement()));
this.focusTracker = this._register(DOM.trackFocus(this._element));
this._register(this.focusTracker.onDidFocus(() => this.setHoverBackground()));
this._register(this.focusTracker.onDidBlur(() => this.applyStyles())); // restore standard styles
@@ -113,7 +112,7 @@ export class Button extends Disposable {
private setHoverBackground(): void {
const hoverBackground = this.buttonHoverBackground ? this.buttonHoverBackground.toString() : null;
if (hoverBackground) {
this.$el.style('background-color', hoverBackground);
this._element.style.backgroundColor = hoverBackground;
}
}
@@ -128,61 +127,59 @@ export class Button extends Disposable {
// {{SQL CARBON EDIT}} -- removed 'private' access modifier
applyStyles(): void {
if (this.$el) {
if (this._element) {
const background = this.buttonBackground ? this.buttonBackground.toString() : null;
const foreground = this.buttonForeground ? this.buttonForeground.toString() : null;
const border = this.buttonBorder ? this.buttonBorder.toString() : null;
this.$el.style('color', foreground);
this.$el.style('background-color', background);
this._element.style.color = foreground;
this._element.style.backgroundColor = background;
this.$el.style('border-width', border ? '1px' : null);
this.$el.style('border-style', border ? 'solid' : null);
this.$el.style('border-color', border);
this._element.style.borderWidth = border ? '1px' : null;
this._element.style.borderStyle = border ? 'solid' : null;
this._element.style.borderColor = border;
}
}
get element(): HTMLElement {
return this.$el.getHTMLElement();
return this._element;
}
set label(value: string) {
if (!this.$el.hasClass('monaco-text-button')) {
this.$el.addClass('monaco-text-button');
if (!DOM.hasClass(this._element, 'monaco-text-button')) {
DOM.addClass(this._element, 'monaco-text-button');
}
this.$el.text(value);
this._element.textContent = value;
//{{SQL CARBON EDIT}}
this.$el.attr('aria-label', value);
this._element.setAttribute('aria-label', value);
//{{END}}
if (this.options.title) {
this.$el.title(value);
this._element.title = value;
}
}
set icon(iconClassName: string) {
this.$el.addClass(iconClassName);
DOM.addClasses(this._element, ...iconClassName.split(' '));
}
set enabled(value: boolean) {
if (value) {
this.$el.removeClass('disabled');
this.$el.attr({
'aria-disabled': 'false',
'tabIndex': '0'
});
DOM.removeClass(this._element, 'disabled');
this._element.setAttribute('aria-disabled', String(false));
this._element.tabIndex = 0;
} else {
this.$el.addClass('disabled');
this.$el.attr('aria-disabled', String(true));
DOM.removeTabIndexAndUpdateFocus(this.$el.getHTMLElement());
DOM.addClass(this._element, 'disabled');
this._element.setAttribute('aria-disabled', String(true));
DOM.removeTabIndexAndUpdateFocus(this._element);
}
}
get enabled() {
return !this.$el.hasClass('disabled');
return !DOM.hasClass(this._element, 'disabled');
}
focus(): void {
this.$el.domFocus();
this._element.focus();
}
}
@@ -206,12 +203,12 @@ export class ButtonGroup extends Disposable {
// Implement keyboard access in buttons if there are multiple
if (count > 1) {
$(button.element).on(DOM.EventType.KEY_DOWN, e => {
const event = new StandardKeyboardEvent(e as KeyboardEvent);
this._register(DOM.addDisposableListener(button.element, DOM.EventType.KEY_DOWN, e => {
const event = new StandardKeyboardEvent(e);
let eventHandled = true;
// Next / Previous Button
let buttonIndexToFocus: number;
let buttonIndexToFocus: number | undefined;
if (event.equals(KeyCode.LeftArrow)) {
buttonIndexToFocus = index > 0 ? index - 1 : this._buttons.length - 1;
} else if (event.equals(KeyCode.RightArrow)) {
@@ -220,11 +217,12 @@ export class ButtonGroup extends Disposable {
eventHandled = false;
}
if (eventHandled) {
if (eventHandled && typeof buttonIndexToFocus === 'number') {
this._buttons[buttonIndexToFocus].focus();
DOM.EventHelper.stop(e, true);
}
}, this.toDispose);
}));
}
}
}

View File

@@ -50,15 +50,15 @@ export interface ICenteredViewStyles extends ISplitViewStyles {
export class CenteredViewLayout {
private splitView: SplitView;
private splitView?: SplitView;
private width: number = 0;
private height: number = 0;
private style: ICenteredViewStyles;
private didLayout = false;
private emptyViews: ISplitViewView[];
private emptyViews: ISplitViewView[] | undefined;
private splitViewDisposables: IDisposable[] = [];
constructor(private container: HTMLElement, private view: IView, public readonly state: CenteredViewState = GOLDEN_RATIO) {
constructor(private container: HTMLElement, private view: IView, public readonly state: CenteredViewState = { leftMarginRatio: GOLDEN_RATIO.leftMarginRatio, rightMarginRatio: GOLDEN_RATIO.rightMarginRatio }) {
this.container.appendChild(this.view.element);
// Make sure to hide the split view overflow like sashes #52892
this.container.style.overflow = 'hidden';
@@ -84,6 +84,9 @@ export class CenteredViewLayout {
}
private resizeMargins(): void {
if (!this.splitView) {
return;
}
this.splitView.resizeView(0, this.state.leftMarginRatio * this.width);
this.splitView.resizeView(2, this.state.rightMarginRatio * this.width);
}
@@ -94,7 +97,7 @@ export class CenteredViewLayout {
styles(style: ICenteredViewStyles): void {
this.style = style;
if (this.splitView) {
if (this.splitView && this.emptyViews) {
this.splitView.style(this.style);
this.emptyViews[0].element.style.backgroundColor = this.style.background.toString();
this.emptyViews[1].element.style.backgroundColor = this.style.background.toString();
@@ -115,8 +118,10 @@ export class CenteredViewLayout {
});
this.splitViewDisposables.push(this.splitView.onDidSashChange(() => {
this.state.leftMarginRatio = this.splitView.getViewSize(0) / this.width;
this.state.rightMarginRatio = this.splitView.getViewSize(2) / this.width;
if (this.splitView) {
this.state.leftMarginRatio = this.splitView.getViewSize(0) / this.width;
this.state.rightMarginRatio = this.splitView.getViewSize(2) / this.width;
}
}));
this.splitViewDisposables.push(this.splitView.onDidSashReset(() => {
this.state.leftMarginRatio = GOLDEN_RATIO.leftMarginRatio;
@@ -130,15 +135,23 @@ export class CenteredViewLayout {
this.splitView.addView(this.emptyViews[0], this.state.leftMarginRatio * this.width, 0);
this.splitView.addView(this.emptyViews[1], this.state.rightMarginRatio * this.width, 2);
} else {
this.container.removeChild(this.splitView.el);
if (this.splitView) {
this.container.removeChild(this.splitView.el);
}
this.splitViewDisposables = dispose(this.splitViewDisposables);
this.splitView.dispose();
if (this.splitView) {
this.splitView.dispose();
}
this.splitView = undefined;
this.emptyViews = undefined;
this.container.appendChild(this.view.element);
}
}
isDefault(state: CenteredViewState): boolean {
return state.leftMarginRatio === GOLDEN_RATIO.leftMarginRatio && state.rightMarginRatio === GOLDEN_RATIO.rightMarginRatio;
}
dispose(): void {
this.splitViewDisposables = dispose(this.splitViewDisposables);

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./checkbox';
import * as DOM from 'vs/base/browser/dom';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
@@ -13,9 +11,11 @@ import { Color } from 'vs/base/common/color';
import { Emitter, Event } from 'vs/base/common/event';
import { KeyCode } from 'vs/base/common/keyCodes';
import * as objects from 'vs/base/common/objects';
import { BaseActionItem } from 'vs/base/browser/ui/actionbar/actionbar';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
export interface ICheckboxOpts extends ICheckboxStyles {
readonly actionClassName: string;
readonly actionClassName?: string;
readonly title: string;
readonly isChecked: boolean;
}
@@ -28,6 +28,48 @@ const defaultOpts = {
inputActiveOptionBorder: Color.fromHex('#007ACC')
};
export class CheckboxActionItem extends BaseActionItem {
private checkbox: Checkbox;
private disposables: IDisposable[] = [];
render(container: HTMLElement): void {
this.element = container;
this.disposables = dispose(this.disposables);
this.checkbox = new Checkbox({
actionClassName: this._action.class,
isChecked: this._action.checked,
title: this._action.label
});
this.disposables.push(this.checkbox);
this.checkbox.onChange(() => this._action.checked = this.checkbox.checked, this, this.disposables);
this.element.appendChild(this.checkbox.domNode);
}
updateEnabled(): void {
if (this.checkbox) {
if (this.isEnabled()) {
this.checkbox.enable();
} else {
this.checkbox.disable();
}
}
}
updateChecked(): void {
if (this.checkbox) {
this.checkbox.checked = this._action.checked;
}
}
dipsose(): void {
this.disposables = dispose(this.disposables);
super.dispose();
}
}
export class Checkbox extends Widget {
private readonly _onChange = this._register(new Emitter<boolean>());
@@ -50,7 +92,7 @@ export class Checkbox extends Widget {
this.domNode = document.createElement('div');
this.domNode.title = this._opts.title;
this.domNode.className = 'monaco-custom-checkbox ' + this._opts.actionClassName + ' ' + (this._checked ? 'checked' : 'unchecked');
this.domNode.className = 'monaco-custom-checkbox ' + (this._opts.actionClassName || '') + ' ' + (this._checked ? 'checked' : 'unchecked');
this.domNode.tabIndex = 0;
this.domNode.setAttribute('role', 'checkbox');
this.domNode.setAttribute('aria-checked', String(this._checked));

View File

@@ -3,12 +3,10 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./contextview';
import { Builder, $ } from 'vs/base/browser/builder';
import * as DOM from 'vs/base/browser/dom';
import { IDisposable, dispose, toDisposable } from 'vs/base/common/lifecycle';
import { IDisposable, dispose, toDisposable, combinedDisposable, Disposable } from 'vs/base/common/lifecycle';
import { Range } from 'vs/base/common/range';
export interface IAnchor {
x: number;
@@ -17,17 +15,18 @@ export interface IAnchor {
height?: number;
}
export enum AnchorAlignment {
export const enum AnchorAlignment {
LEFT, RIGHT
}
export enum AnchorPosition {
export const enum AnchorPosition {
BELOW, ABOVE
}
export interface IDelegate {
getAnchor(): HTMLElement | IAnchor;
render(container: HTMLElement): IDisposable;
focus?(): void;
layout?(): void;
anchorAlignment?: AnchorAlignment; // default: left
anchorPosition?: AnchorPosition; // default: below
@@ -54,7 +53,7 @@ export interface ISize {
export interface IView extends IPosition, ISize { }
export enum LayoutAnchorPosition {
export const enum LayoutAnchorPosition {
Before,
After
}
@@ -96,44 +95,54 @@ export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAn
}
}
export class ContextView {
export class ContextView extends Disposable {
private static readonly BUBBLE_UP_EVENTS = ['click', 'keydown', 'focus', 'blur'];
private static readonly BUBBLE_DOWN_EVENTS = ['click'];
private $container: Builder;
private $view: Builder;
private delegate: IDelegate;
private toDispose: IDisposable[];
private toDisposeOnClean: IDisposable;
private container: HTMLElement | null;
private view: HTMLElement;
private delegate: IDelegate | null;
private toDisposeOnClean: IDisposable | null;
private toDisposeOnSetContainer: IDisposable;
constructor(container: HTMLElement) {
this.$view = $('.context-view').hide();
super();
this.view = DOM.$('.context-view');
DOM.hide(this.view);
this.setContainer(container);
this.toDispose = [toDisposable(() => {
this.setContainer(null);
})];
this.toDisposeOnClean = null;
this._register(toDisposable(() => this.setContainer(null)));
}
public setContainer(container: HTMLElement): void {
if (this.$container) {
this.$container.getHTMLElement().removeChild(this.$view.getHTMLElement());
this.$container.off(ContextView.BUBBLE_UP_EVENTS);
this.$container.off(ContextView.BUBBLE_DOWN_EVENTS, true);
this.$container = null;
public setContainer(container: HTMLElement | null): void {
if (this.container) {
this.toDisposeOnSetContainer = dispose(this.toDisposeOnSetContainer);
this.container.removeChild(this.view);
this.container = null;
}
if (container) {
this.$container = $(container);
this.$view.appendTo(this.$container);
this.$container.on(ContextView.BUBBLE_UP_EVENTS, (e: Event) => {
this.onDOMEvent(e, <HTMLElement>document.activeElement, false);
this.container = container;
this.container.appendChild(this.view);
const toDisposeOnSetContainer: IDisposable[] = [];
ContextView.BUBBLE_UP_EVENTS.forEach(event => {
toDisposeOnSetContainer.push(DOM.addStandardDisposableListener(this.container!, event, (e: Event) => {
this.onDOMEvent(e, <HTMLElement>document.activeElement, false);
}));
});
this.$container.on(ContextView.BUBBLE_DOWN_EVENTS, (e: Event) => {
this.onDOMEvent(e, <HTMLElement>document.activeElement, true);
}, null, true);
ContextView.BUBBLE_DOWN_EVENTS.forEach(event => {
toDisposeOnSetContainer.push(DOM.addStandardDisposableListener(this.container!, event, (e: Event) => {
this.onDOMEvent(e, <HTMLElement>document.activeElement, true);
}, true));
});
this.toDisposeOnSetContainer = combinedDisposable(toDisposeOnSetContainer);
}
}
@@ -143,16 +152,25 @@ export class ContextView {
}
// Show static box
this.$view.setClass('context-view').empty().style({ top: '0px', left: '0px' }).show();
DOM.clearNode(this.view);
this.view.className = 'context-view';
this.view.style.top = '0px';
this.view.style.left = '0px';
DOM.show(this.view);
// Render content
this.toDisposeOnClean = delegate.render(this.$view.getHTMLElement());
this.toDisposeOnClean = delegate.render(this.view);
// Set active delegate
this.delegate = delegate;
// Layout
this.doLayout();
// Focus
if (this.delegate.focus) {
this.delegate.focus();
}
}
public layout(): void {
@@ -160,21 +178,26 @@ export class ContextView {
return;
}
if (this.delegate.canRelayout === false) {
if (this.delegate!.canRelayout === false) {
this.hide();
return;
}
if (this.delegate.layout) {
this.delegate.layout();
if (this.delegate!.layout) {
this.delegate!.layout!();
}
this.doLayout();
}
private doLayout(): void {
// Check that we still have a delegate - this.delegate.layout may have hidden
if (!this.isVisible()) {
return;
}
// Get anchor
let anchor = this.delegate.getAnchor();
let anchor = this.delegate!.getAnchor();
// Compute around
let around: IView;
@@ -195,16 +218,18 @@ export class ContextView {
around = {
top: realAnchor.y,
left: realAnchor.x,
width: realAnchor.width || 0,
height: realAnchor.height || 0
width: realAnchor.width || 1,
height: realAnchor.height || 2
};
}
const viewSize = this.$view.getTotalSize();
const anchorPosition = this.delegate.anchorPosition || AnchorPosition.BELOW;
const anchorAlignment = this.delegate.anchorAlignment || AnchorAlignment.LEFT;
const viewSizeWidth = DOM.getTotalWidth(this.view);
const viewSizeHeight = DOM.getTotalHeight(this.view);
const verticalAnchor: ILayoutAnchor = { offset: around.top, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
const anchorPosition = this.delegate!.anchorPosition || AnchorPosition.BELOW;
const anchorAlignment = this.delegate!.anchorAlignment || AnchorAlignment.LEFT;
const verticalAnchor: ILayoutAnchor = { offset: around.top - window.pageYOffset, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
let horizontalAnchor: ILayoutAnchor;
@@ -214,14 +239,23 @@ export class ContextView {
horizontalAnchor = { offset: around.left + around.width, size: 0, position: LayoutAnchorPosition.After };
}
const containerPosition = DOM.getDomNodePagePosition(this.$container.getHTMLElement());
const top = layout(window.innerHeight, viewSize.height, verticalAnchor) - containerPosition.top;
const left = layout(window.innerWidth, viewSize.width, horizontalAnchor) - containerPosition.left;
const top = layout(window.innerHeight, viewSizeHeight, verticalAnchor) + window.pageYOffset;
this.$view.removeClass('top', 'bottom', 'left', 'right');
this.$view.addClass(anchorPosition === AnchorPosition.BELOW ? 'bottom' : 'top');
this.$view.addClass(anchorAlignment === AnchorAlignment.LEFT ? 'left' : 'right');
this.$view.style({ top: `${top}px`, left: `${left}px`, width: 'initial' });
// if view intersects vertically with anchor, shift it horizontally
if (Range.intersects({ start: top, end: top + viewSizeHeight }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) {
horizontalAnchor.size = around.width;
}
const left = layout(window.innerWidth, viewSizeWidth, horizontalAnchor);
DOM.removeClasses(this.view, 'top', 'bottom', 'left', 'right');
DOM.addClass(this.view, anchorPosition === AnchorPosition.BELOW ? 'bottom' : 'top');
DOM.addClass(this.view, anchorAlignment === AnchorAlignment.LEFT ? 'left' : 'right');
const containerPosition = DOM.getDomNodePagePosition(this.container!);
this.view.style.top = `${top - containerPosition.top}px`;
this.view.style.left = `${left - containerPosition.left}px`;
this.view.style.width = 'initial';
}
public hide(data?: any): void {
@@ -236,7 +270,7 @@ export class ContextView {
this.toDisposeOnClean = null;
}
this.$view.hide();
DOM.hide(this.view);
}
// {{SQL CARBON EDIT}}
@@ -248,7 +282,7 @@ export class ContextView {
if (this.delegate) {
if (this.delegate.onDOMEvent) {
this.delegate.onDOMEvent(e, <HTMLElement>document.activeElement);
} else if (onCapture && !DOM.isAncestor(<HTMLElement>e.target, this.$container.getHTMLElement())) {
} else if (onCapture && !DOM.isAncestor(<HTMLElement>e.target, this.container)) {
this.hide();
}
}
@@ -257,6 +291,6 @@ export class ContextView {
public dispose(): void {
this.hide();
this.toDispose = dispose(this.toDispose);
super.dispose();
}
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./countBadge';
import { $, append } from 'vs/base/browser/dom';
import { format } from 'vs/base/common/strings';
@@ -35,9 +33,9 @@ export class CountBadge {
private countFormat: string;
private titleFormat: string;
private badgeBackground: Color;
private badgeForeground: Color;
private badgeBorder: Color;
private badgeBackground: Color | undefined;
private badgeForeground: Color | undefined;
private badgeBorder: Color | undefined;
private options: ICountBadgeOptions;

View File

@@ -3,20 +3,17 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./dropdown';
import { Builder, $ } from 'vs/base/browser/builder';
import { TPromise } from 'vs/base/common/winjs.base';
import { Gesture, EventType as GestureEventType } from 'vs/base/browser/touch';
import { ActionRunner, IAction, IActionRunner } from 'vs/base/common/actions';
import { BaseActionItem, IActionItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IContextViewProvider, IAnchor } from 'vs/base/browser/ui/contextview/contextview';
import { IDisposable } from 'vs/base/common/lifecycle';
import { IContextViewProvider, IAnchor, AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
import { IMenuOptions } from 'vs/base/browser/ui/menu/menu';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { EventHelper, EventType, removeClass, addClass } from 'vs/base/browser/dom';
import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes';
import { EventHelper, EventType, removeClass, addClass, append, $, addDisposableListener, addClasses } from 'vs/base/browser/dom';
import { IContextMenuDelegate } from 'vs/base/browser/contextmenu';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
export interface ILabelRenderer {
(container: HTMLElement): IDisposable;
@@ -28,65 +25,77 @@ export interface IBaseDropdownOptions {
}
export class BaseDropdown extends ActionRunner {
private _toDispose: IDisposable[] = [];
private $el: Builder;
private $boxContainer: Builder;
private $label: Builder;
private $contents: Builder;
private _element: HTMLElement;
private boxContainer: HTMLElement;
private _label: HTMLElement;
private contents: HTMLElement;
private visible: boolean;
constructor(container: HTMLElement, options: IBaseDropdownOptions) {
super();
this.$el = $('.monaco-dropdown').appendTo(container);
this._element = append(container, $('.monaco-dropdown'));
this.$label = $('.dropdown-label');
this._label = append(this._element, $('.dropdown-label'));
let labelRenderer = options.labelRenderer;
if (!labelRenderer) {
labelRenderer = (container: HTMLElement): IDisposable => {
$(container).text(options.label || '');
container.textContent = options.label || '';
return null;
};
}
this.$label.on([EventType.CLICK, EventType.MOUSE_DOWN, GestureEventType.Tap], (e: Event) => {
EventHelper.stop(e, true); // prevent default click behaviour to trigger
}).on([EventType.MOUSE_DOWN, GestureEventType.Tap], (e: Event) => {
if (e instanceof MouseEvent && e.detail > 1) {
return; // prevent multiple clicks to open multiple context menus (https://github.com/Microsoft/vscode/issues/41363)
}
[EventType.CLICK, EventType.MOUSE_DOWN, GestureEventType.Tap].forEach(event => {
this._register(addDisposableListener(this._label, event, e => EventHelper.stop(e, true))); // prevent default click behaviour to trigger
});
if (this.visible) {
this.hide();
} else {
this.show();
}
}).appendTo(this.$el);
[EventType.MOUSE_DOWN, GestureEventType.Tap].forEach(event => {
this._register(addDisposableListener(this._label, event, e => {
if (e instanceof MouseEvent && e.detail > 1) {
return; // prevent multiple clicks to open multiple context menus (https://github.com/Microsoft/vscode/issues/41363)
}
const cleanupFn = labelRenderer(this.$label.getHTMLElement());
if (this.visible) {
this.hide();
} else {
this.show();
}
}));
});
this._register(addDisposableListener(this._label, EventType.KEY_UP, e => {
const event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) {
EventHelper.stop(e, true); // https://github.com/Microsoft/vscode/issues/57997
if (this.visible) {
this.hide();
} else {
this.show();
}
}
}));
const cleanupFn = labelRenderer(this._label);
if (cleanupFn) {
this._toDispose.push(cleanupFn);
this._register(cleanupFn);
}
Gesture.addTarget(this.$label.getHTMLElement());
}
get toDispose(): IDisposable[] {
return this._toDispose;
Gesture.addTarget(this._label);
}
get element(): HTMLElement {
return this.$el.getHTMLElement();
return this._element;
}
get label(): HTMLElement {
return this.$label.getHTMLElement();
return this._label;
}
set tooltip(tooltip: string) {
this.$label.title(tooltip);
this._label.title = tooltip;
}
show(): void {
@@ -105,21 +114,19 @@ export class BaseDropdown extends ActionRunner {
super.dispose();
this.hide();
this._toDispose = dispose(this.toDispose);
if (this.$boxContainer) {
this.$boxContainer.destroy();
this.$boxContainer = null;
if (this.boxContainer) {
this.boxContainer.remove();
this.boxContainer = null;
}
if (this.$contents) {
this.$contents.destroy();
this.$contents = null;
if (this.contents) {
this.contents.remove();
this.contents = null;
}
if (this.$label) {
this.$label.destroy();
this.$label = null;
if (this._label) {
this._label.remove();
this._label = null;
}
}
}
@@ -236,13 +243,14 @@ export class DropdownMenu extends BaseDropdown {
this._contextMenuProvider.showContextMenu({
getAnchor: () => this.element,
getActions: () => TPromise.as(this.actions),
getActions: () => this.actions,
getActionsContext: () => this.menuOptions ? this.menuOptions.context : null,
getActionItem: (action) => this.menuOptions && this.menuOptions.actionItemProvider ? this.menuOptions.actionItemProvider(action) : null,
getKeyBinding: (action: IAction) => this.menuOptions && this.menuOptions.getKeyBinding ? this.menuOptions.getKeyBinding(action) : null,
getActionItem: action => this.menuOptions && this.menuOptions.actionItemProvider ? this.menuOptions.actionItemProvider(action) : null,
getKeyBinding: action => this.menuOptions && this.menuOptions.getKeyBinding ? this.menuOptions.getKeyBinding(action) : null,
getMenuClassName: () => this.menuClassName,
onHide: () => this.onHide(),
actionRunner: this.menuOptions ? this.menuOptions.actionRunner : null
actionRunner: this.menuOptions ? this.menuOptions.actionRunner : null,
anchorAlignment: this.menuOptions.anchorAlignment
});
}
@@ -263,10 +271,11 @@ export class DropdownMenuActionItem extends BaseActionItem {
private actionItemProvider: IActionItemProvider;
private keybindings: (action: IAction) => ResolvedKeybinding;
private clazz: string;
private anchorAlignmentProvider: (() => AnchorAlignment) | undefined;
constructor(action: IAction, menuActions: IAction[], contextMenuProvider: IContextMenuProvider, actionItemProvider: IActionItemProvider, actionRunner: IActionRunner, keybindings: (action: IAction) => ResolvedKeybinding, clazz: string);
constructor(action: IAction, actionProvider: IActionProvider, contextMenuProvider: IContextMenuProvider, actionItemProvider: IActionItemProvider, actionRunner: IActionRunner, keybindings: (action: IAction) => ResolvedKeybinding, clazz: string);
constructor(action: IAction, menuActionsOrProvider: any, contextMenuProvider: IContextMenuProvider, actionItemProvider: IActionItemProvider, actionRunner: IActionRunner, keybindings: (action: IAction) => ResolvedKeybinding, clazz: string) {
constructor(action: IAction, menuActions: IAction[], contextMenuProvider: IContextMenuProvider, actionItemProvider: IActionItemProvider, actionRunner: IActionRunner, keybindings: (action: IAction) => ResolvedKeybinding, clazz: string, anchorAlignmentProvider?: () => AnchorAlignment);
constructor(action: IAction, actionProvider: IActionProvider, contextMenuProvider: IContextMenuProvider, actionItemProvider: IActionItemProvider, actionRunner: IActionRunner, keybindings: (action: IAction) => ResolvedKeybinding, clazz: string, anchorAlignmentProvider?: () => AnchorAlignment);
constructor(action: IAction, menuActionsOrProvider: any, contextMenuProvider: IContextMenuProvider, actionItemProvider: IActionItemProvider, actionRunner: IActionRunner, keybindings: (action: IAction) => ResolvedKeybinding, clazz: string, anchorAlignmentProvider?: () => AnchorAlignment) {
super(null, action);
this.menuActionsOrProvider = menuActionsOrProvider;
@@ -275,19 +284,18 @@ export class DropdownMenuActionItem extends BaseActionItem {
this.actionRunner = actionRunner;
this.keybindings = keybindings;
this.clazz = clazz;
this.anchorAlignmentProvider = anchorAlignmentProvider;
}
render(container: HTMLElement): void {
const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable => {
this.builder = $('a.action-label').attr({
tabIndex: '0',
role: 'button',
'aria-haspopup': 'true',
title: this._action.label || '',
class: this.clazz
});
this.element = append(el, $('a.action-label.icon'));
addClasses(this.element, this.clazz);
this.builder.appendTo(el);
this.element.tabIndex = 0;
this.element.setAttribute('role', 'button');
this.element.setAttribute('aria-haspopup', 'true');
this.element.title = this._action.label || '';
return null;
};
@@ -304,7 +312,7 @@ export class DropdownMenuActionItem extends BaseActionItem {
options.actionProvider = this.menuActionsOrProvider;
}
this.dropdownMenu = new DropdownMenu(container, options);
this.dropdownMenu = this._register(new DropdownMenu(container, options));
this.dropdownMenu.menuOptions = {
actionItemProvider: this.actionItemProvider,
@@ -312,6 +320,17 @@ export class DropdownMenuActionItem extends BaseActionItem {
getKeyBinding: this.keybindings,
context: this._context
};
if (this.anchorAlignmentProvider) {
const that = this;
this.dropdownMenu.menuOptions = {
...this.dropdownMenu.menuOptions,
get anchorAlignment(): AnchorAlignment {
return that.anchorAlignmentProvider();
}
};
}
}
setActionContext(newContext: any): void {
@@ -327,10 +346,4 @@ export class DropdownMenuActionItem extends BaseActionItem {
this.dropdownMenu.show();
}
}
dispose(): void {
this.dropdownMenu.dispose();
super.dispose();
}
}
}

View File

@@ -11,7 +11,6 @@
.monaco-findInput .monaco-inputbox {
font-size: 13px;
width: 100%;
height: 25px;
}
.monaco-findInput > .controls {

View File

@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./findInput';
@@ -24,6 +23,7 @@ export interface IFindInputOptions extends IFindInputStyles {
readonly width?: number;
readonly validation?: IInputValidator;
readonly label: string;
readonly flexibleHeight?: boolean;
readonly appendCaseSensitiveLabel?: string;
readonly appendWholeWordsLabel?: string;
@@ -46,6 +46,7 @@ export class FindInput extends Widget {
private placeholder: string;
private validation: IInputValidator;
private label: string;
private fixFocusOnOptionClickEnabled = true;
private inputActiveOptionBorder: Color;
private inputBackground: Color;
@@ -54,10 +55,13 @@ export class FindInput extends Widget {
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;
@@ -86,7 +90,7 @@ export class FindInput extends Widget {
private _onRegexKeyDown = this._register(new Emitter<IKeyboardEvent>());
public readonly onRegexKeyDown: Event<IKeyboardEvent> = this._onRegexKeyDown.event;
constructor(parent: HTMLElement, contextViewProvider: IContextViewProvider, options?: IFindInputOptions) {
constructor(parent: HTMLElement, contextViewProvider: IContextViewProvider, private readonly _showOptionButtons: boolean, options?: IFindInputOptions) {
super();
this.contextViewProvider = contextViewProvider;
this.width = options.width || 100;
@@ -101,10 +105,13 @@ export class FindInput extends Widget {
this.inputValidationInfoBorder = options.inputValidationInfoBorder;
this.inputValidationInfoBackground = options.inputValidationInfoBackground;
this.inputValidationInfoForeground = options.inputValidationInfoForeground;
this.inputValidationWarningBorder = options.inputValidationWarningBorder;
this.inputValidationWarningBackground = options.inputValidationWarningBackground;
this.inputValidationWarningForeground = options.inputValidationWarningForeground;
this.inputValidationErrorBorder = options.inputValidationErrorBorder;
this.inputValidationErrorBackground = options.inputValidationErrorBackground;
this.inputValidationErrorForeground = options.inputValidationErrorForeground;
this.regex = null;
this.wholeWords = null;
@@ -112,7 +119,7 @@ export class FindInput extends Widget {
this.domNode = null;
this.inputBox = null;
this.buildDomNode(options.appendCaseSensitiveLabel || '', options.appendWholeWordsLabel || '', options.appendRegexLabel || '', options.history);
this.buildDomNode(options.appendCaseSensitiveLabel || '', options.appendWholeWordsLabel || '', options.appendRegexLabel || '', options.history, options.flexibleHeight);
if (Boolean(parent)) {
parent.appendChild(this.domNode);
@@ -140,6 +147,10 @@ export class FindInput extends Widget {
this.caseSensitive.disable();
}
public setFocusInputOnOptionClick(value: boolean): void {
this.fixFocusOnOptionClickEnabled = value;
}
public setEnabled(enabled: boolean): void {
if (enabled) {
this.enable();
@@ -171,6 +182,10 @@ export class FindInput extends Widget {
}
}
public onSearchSubmit(): void {
this.inputBox.addToHistory();
}
public style(styles: IFindInputStyles): void {
this.inputActiveOptionBorder = styles.inputActiveOptionBorder;
this.inputBackground = styles.inputBackground;
@@ -178,10 +193,13 @@ export class FindInput extends Widget {
this.inputBorder = styles.inputBorder;
this.inputValidationInfoBackground = styles.inputValidationInfoBackground;
this.inputValidationInfoForeground = styles.inputValidationInfoForeground;
this.inputValidationInfoBorder = styles.inputValidationInfoBorder;
this.inputValidationWarningBackground = styles.inputValidationWarningBackground;
this.inputValidationWarningForeground = styles.inputValidationWarningForeground;
this.inputValidationWarningBorder = styles.inputValidationWarningBorder;
this.inputValidationErrorBackground = styles.inputValidationErrorBackground;
this.inputValidationErrorForeground = styles.inputValidationErrorForeground;
this.inputValidationErrorBorder = styles.inputValidationErrorBorder;
this.applyStyles();
@@ -201,10 +219,13 @@ export class FindInput extends Widget {
inputForeground: this.inputForeground,
inputBorder: this.inputBorder,
inputValidationInfoBackground: this.inputValidationInfoBackground,
inputValidationInfoForeground: this.inputValidationInfoForeground,
inputValidationInfoBorder: this.inputValidationInfoBorder,
inputValidationWarningBackground: this.inputValidationWarningBackground,
inputValidationWarningForeground: this.inputValidationWarningForeground,
inputValidationWarningBorder: this.inputValidationWarningBorder,
inputValidationErrorBackground: this.inputValidationErrorBackground,
inputValidationErrorForeground: this.inputValidationErrorForeground,
inputValidationErrorBorder: this.inputValidationErrorBorder
};
this.inputBox.style(inputBoxStyles);
@@ -265,9 +286,10 @@ export class FindInput extends Widget {
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[]): void {
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');
@@ -282,12 +304,16 @@ export class FindInput extends Widget {
inputForeground: this.inputForeground,
inputBorder: this.inputBorder,
inputValidationInfoBackground: this.inputValidationInfoBackground,
inputValidationInfoForeground: this.inputValidationInfoForeground,
inputValidationInfoBorder: this.inputValidationInfoBorder,
inputValidationWarningBackground: this.inputValidationWarningBackground,
inputValidationWarningForeground: this.inputValidationWarningForeground,
inputValidationWarningBorder: this.inputValidationWarningBorder,
inputValidationErrorBackground: this.inputValidationErrorBackground,
inputValidationErrorForeground: this.inputValidationErrorForeground,
inputValidationErrorBorder: this.inputValidationErrorBorder,
history
history,
flexibleHeight
}));
this.regex = this._register(new RegexCheckbox({
@@ -297,7 +323,7 @@ export class FindInput extends Widget {
}));
this._register(this.regex.onChange(viaKeyboard => {
this._onDidOptionChange.fire(viaKeyboard);
if (!viaKeyboard) {
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
this.inputBox.focus();
}
this.setInputWidth();
@@ -314,7 +340,7 @@ export class FindInput extends Widget {
}));
this._register(this.wholeWords.onChange(viaKeyboard => {
this._onDidOptionChange.fire(viaKeyboard);
if (!viaKeyboard) {
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
this.inputBox.focus();
}
this.setInputWidth();
@@ -328,7 +354,7 @@ export class FindInput extends Widget {
}));
this._register(this.caseSensitive.onChange(viaKeyboard => {
this._onDidOptionChange.fire(viaKeyboard);
if (!viaKeyboard) {
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
this.inputBox.focus();
}
this.setInputWidth();
@@ -370,6 +396,7 @@ export class FindInput extends Widget {
let controls = document.createElement('div');
controls.className = 'controls';
controls.style.display = this._showOptionButtons ? 'block' : 'none';
controls.appendChild(this.caseSensitive.domNode);
controls.appendChild(this.wholeWords.domNode);
controls.appendChild(this.regex.domNode);

View File

@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Checkbox } from 'vs/base/browser/ui/checkbox/checkbox';
import { Color } from 'vs/base/common/color';

View File

@@ -3,18 +3,16 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./gridview';
import { Orientation } from 'vs/base/browser/ui/sash/sash';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { tail2 as tail } from 'vs/base/common/arrays';
import { tail2 as tail, equals } from 'vs/base/common/arrays';
import { orthogonal, IView, GridView, Sizing as GridViewSizing, Box, IGridViewStyles } from './gridview';
import { Event } from 'vs/base/common/event';
export { Orientation } from './gridview';
export enum Direction {
export const enum Direction {
Up,
Down,
Left,
@@ -141,10 +139,15 @@ export function getRelativeLocation(rootOrientation: Orientation, location: numb
function indexInParent(element: HTMLElement): number {
const parentElement = element.parentElement;
if (!parentElement) {
throw new Error('Invalid grid element');
}
let el = parentElement.firstElementChild;
let index = 0;
while (el !== element && el !== parentElement.lastElementChild) {
while (el !== element && el !== parentElement.lastElementChild && el) {
el = el.nextElementSibling;
index++;
}
@@ -159,16 +162,22 @@ function indexInParent(element: HTMLElement): number {
* This will break as soon as DOM structures of the Splitview or Gridview change.
*/
function getGridLocation(element: HTMLElement): number[] {
if (/\bmonaco-grid-view\b/.test(element.parentElement.className)) {
const parentElement = element.parentElement;
if (!parentElement) {
throw new Error('Invalid grid element');
}
if (/\bmonaco-grid-view\b/.test(parentElement.className)) {
return [];
}
const index = indexInParent(element.parentElement);
const ancestor = element.parentElement.parentElement.parentElement.parentElement;
const index = indexInParent(parentElement);
const ancestor = parentElement.parentElement!.parentElement!.parentElement!;
return [...getGridLocation(ancestor), index];
}
export enum Sizing {
export const enum Sizing {
Distribute = 'distribute',
Split = 'split'
}
@@ -195,7 +204,7 @@ export class Grid<T extends IView> implements IDisposable {
get minimumHeight(): number { return this.gridview.minimumHeight; }
get maximumWidth(): number { return this.gridview.maximumWidth; }
get maximumHeight(): number { return this.gridview.maximumHeight; }
get onDidChange(): Event<{ width: number; height: number; }> { return this.gridview.onDidChange; }
get onDidChange(): Event<{ width: number; height: number; } | undefined> { return this.gridview.onDidChange; }
get element(): HTMLElement { return this.gridview.element; }
@@ -256,15 +265,27 @@ export class Grid<T extends IView> implements IDisposable {
throw new Error('Can\'t remove last view');
}
if (!this.views.has(view)) {
throw new Error('View not found');
}
const location = this.getViewLocation(view);
this.gridview.removeView(location, sizing === Sizing.Distribute ? GridViewSizing.Distribute : undefined);
this.views.delete(view);
}
moveView(view: T, sizing: number | Sizing, referenceView: T, direction: Direction): void {
const sourceLocation = this.getViewLocation(view);
const [sourceParentLocation, from] = tail(sourceLocation);
const referenceLocation = this.getViewLocation(referenceView);
const targetLocation = getRelativeLocation(this.gridview.orientation, referenceLocation, direction);
const [targetParentLocation, to] = tail(targetLocation);
if (equals(sourceParentLocation, targetParentLocation)) {
this.gridview.moveView(sourceParentLocation, from, to);
} else {
this.removeView(view, typeof sizing === 'number' ? undefined : sizing);
this.addView(view, sizing, referenceView, direction);
}
}
swapViews(from: T, to: T): void {
const fromLocation = this.getViewLocation(from);
const toLocation = this.getViewLocation(to);
@@ -358,7 +379,7 @@ export interface ISerializableView extends IView {
}
export interface IViewDeserializer<T extends ISerializableView> {
fromJSON(json: object): T;
fromJSON(json: object | null): T;
}
interface InitialLayoutContext<T extends ISerializableView> {
@@ -369,7 +390,7 @@ interface InitialLayoutContext<T extends ISerializableView> {
export interface ISerializedLeafNode {
type: 'leaf';
data: object;
data: object | null;
size: number;
}
@@ -405,18 +426,15 @@ export class SerializableGrid<T extends ISerializableView> extends Grid<T> {
throw new Error('Invalid JSON');
}
const type = json.type;
const data = json.data;
if (type === 'branch') {
if (!Array.isArray(data)) {
if (json.type === 'branch') {
if (!Array.isArray(json.data)) {
throw new Error('Invalid JSON: \'data\' property of branch must be an array.');
}
const children: GridNode<T>[] = [];
let offset = 0;
for (const child of data) {
for (const child of json.data) {
if (typeof child.size !== 'number') {
throw new Error('Invalid JSON: \'size\' property of node must be a number.');
}
@@ -431,8 +449,8 @@ export class SerializableGrid<T extends ISerializableView> extends Grid<T> {
return { children, box };
} else if (type === 'leaf') {
const view = deserializer.fromJSON(data) as T;
} else if (json.type === 'leaf') {
const view = deserializer.fromJSON(json.data) as T;
return { view, box };
}
@@ -517,13 +535,13 @@ export class SerializableGrid<T extends ISerializableView> extends Grid<T> {
const firstLeaves = node.children.map(c => SerializableGrid.getFirstLeaf(c));
for (let i = 1; i < firstLeaves.length; i++) {
const size = orientation === Orientation.VERTICAL ? firstLeaves[i].box.height : firstLeaves[i].box.width;
this.addView(firstLeaves[i].view, size, referenceView, direction);
referenceView = firstLeaves[i].view;
const size = orientation === Orientation.VERTICAL ? firstLeaves[i]!.box.height : firstLeaves[i]!.box.width;
this.addView(firstLeaves[i]!.view, size, referenceView, direction);
referenceView = firstLeaves[i]!.view;
}
for (let i = 0; i < node.children.length; i++) {
this.restoreViews(firstLeaves[i].view, orthogonal(orientation), node.children[i]);
this.restoreViews(firstLeaves[i]!.view, orthogonal(orientation), node.children[i]);
}
}
@@ -601,10 +619,10 @@ function getDimensions(node: ISerializedNode, orientation: Orientation): { width
if (orientation === Orientation.VERTICAL) {
const width = node.size || (childrenDimensions.length === 0 ? undefined : Math.max(...childrenDimensions.map(d => d.width || 0)));
const height = childrenDimensions.length === 0 ? undefined : childrenDimensions.reduce((r, d) => r + d.height, 0);
const height = childrenDimensions.length === 0 ? undefined : childrenDimensions.reduce((r, d) => r + (d.height || 0), 0);
return { width, height };
} else {
const width = childrenDimensions.length === 0 ? undefined : childrenDimensions.reduce((r, d) => r + d.width, 0);
const width = childrenDimensions.length === 0 ? undefined : childrenDimensions.reduce((r, d) => r + (d.width || 0), 0);
const height = node.size || (childrenDimensions.length === 0 ? undefined : Math.max(...childrenDimensions.map(d => d.height || 0)));
return { width, height };
}

View File

@@ -3,18 +3,16 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./gridview';
import { Event, anyEvent, Emitter, mapEvent, Relay } from 'vs/base/common/event';
import { Orientation, Sash } from 'vs/base/browser/ui/sash/sash';
import { SplitView, IView as ISplitView, Sizing, ISplitViewStyles } from 'vs/base/browser/ui/splitview/splitview';
import { SplitView, IView as ISplitView, Sizing, LayoutPriority, ISplitViewStyles } from 'vs/base/browser/ui/splitview/splitview';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { $ } from 'vs/base/browser/dom';
import { tail2 as tail } from 'vs/base/common/arrays';
import { Color } from 'vs/base/common/color';
export { Sizing } from 'vs/base/browser/ui/splitview/splitview';
export { Sizing, LayoutPriority } from 'vs/base/browser/ui/splitview/splitview';
export { Orientation } from 'vs/base/browser/ui/sash/sash';
export interface IView {
@@ -24,6 +22,8 @@ export interface IView {
readonly minimumHeight: number;
readonly maximumHeight: number;
readonly onDidChange: Event<{ width: number; height: number; }>;
readonly priority?: LayoutPriority;
readonly snapSize?: number;
layout(width: number, height: number): void;
}
@@ -62,6 +62,7 @@ const defaultStyles: IGridViewStyles = {
export interface IGridViewOptions {
styles?: IGridViewStyles;
proportionalLayout?: boolean; // default true
}
class BranchNode implements ISplitView, IDisposable {
@@ -137,6 +138,7 @@ class BranchNode implements ISplitView, IDisposable {
constructor(
readonly orientation: Orientation,
styles: IGridViewStyles,
readonly proportionalLayout: boolean,
size: number = 0,
orthogonalSize: number = 0
) {
@@ -181,9 +183,14 @@ class BranchNode implements ISplitView, IDisposable {
throw new Error('Invalid index');
}
const first = index === 0;
const last = index === this.splitview.length;
this.splitview.addView(node, size, index);
this._addChild(node, index);
this.onDidChildrenChange();
}
private _addChild(node: Node, index: number): void {
const first = index === 0;
const last = index === this.children.length;
this.children.splice(index, 0, node);
node.orthogonalStartSash = this.splitview.sashes[index - 1];
node.orthogonalEndSash = this.splitview.sashes[index];
@@ -195,8 +202,6 @@ class BranchNode implements ISplitView, IDisposable {
if (!last) {
this.children[index + 1].orthogonalStartSash = this.splitview.sashes[index];
}
this.onDidChildrenChange();
}
removeChild(index: number, sizing?: Sizing): void {
@@ -204,10 +209,15 @@ class BranchNode implements ISplitView, IDisposable {
throw new Error('Invalid index');
}
const first = index === 0;
const last = index === this.splitview.length - 1;
this.splitview.removeView(index, sizing);
this.children.splice(index, 1);
this._removeChild(index);
this.onDidChildrenChange();
}
private _removeChild(index: number): Node {
const first = index === 0;
const last = index === this.children.length - 1;
const [child] = this.children.splice(index, 1);
if (!first) {
this.children[index - 1].orthogonalEndSash = this.splitview.sashes[index - 1];
@@ -217,7 +227,30 @@ class BranchNode implements ISplitView, IDisposable {
this.children[index].orthogonalStartSash = this.splitview.sashes[Math.max(index - 1, 0)];
}
this.onDidChildrenChange();
return child;
}
moveChild(from: number, to: number): void {
if (from === to) {
return;
}
if (from < 0 || from >= this.children.length) {
throw new Error('Invalid from index');
}
if (to < 0 || to > this.children.length) {
throw new Error('Invalid to index');
}
if (from < to) {
to--;
}
this.splitview.moveView(from, to);
const child = this._removeChild(from);
this._addChild(child, to);
}
swapChildren(from: number, to: number): void {
@@ -332,6 +365,9 @@ class BranchNode implements ISplitView, IDisposable {
child.dispose();
}
this._onDidChange.dispose();
this._onDidSashReset.dispose();
this.splitviewSashResetDisposable.dispose();
this.childrenSashResetDisposable.dispose();
this.childrenChangeDisposable.dispose();
@@ -418,6 +454,14 @@ class LeafNode implements ISplitView, IDisposable {
return this.orientation === Orientation.HORIZONTAL ? this.maximumHeight : this.maximumWidth;
}
get priority(): LayoutPriority | undefined {
return this.view.priority;
}
get snapSize(): number | undefined {
return this.view.snapSize;
}
get minimumOrthogonalSize(): number {
return this.orientation === Orientation.HORIZONTAL ? this.minimumWidth : this.minimumHeight;
}
@@ -451,7 +495,7 @@ type Node = BranchNode | LeafNode;
function flipNode<T extends Node>(node: T, size: number, orthogonalSize: number): T {
if (node instanceof BranchNode) {
const result = new BranchNode(orthogonal(node.orientation), node.styles, size, orthogonalSize);
const result = new BranchNode(orthogonal(node.orientation), node.styles, node.proportionalLayout, size, orthogonalSize);
let totalSize = 0;
@@ -480,6 +524,7 @@ export class GridView implements IDisposable {
readonly element: HTMLElement;
private styles: IGridViewStyles;
private proportionalLayout: boolean;
private _root: BranchNode;
private onDidSashResetRelay = new Relay<number[]>();
@@ -528,13 +573,14 @@ export class GridView implements IDisposable {
get maximumWidth(): number { return this.root.maximumHeight; }
get maximumHeight(): number { return this.root.maximumHeight; }
private _onDidChange = new Relay<{ width: number; height: number; }>();
private _onDidChange = new Relay<{ width: number; height: number; } | undefined>();
readonly onDidChange = this._onDidChange.event;
constructor(options: IGridViewOptions = {}) {
this.element = $('.monaco-grid-view');
this.styles = options.styles || defaultStyles;
this.root = new BranchNode(Orientation.VERTICAL, this.styles);
this.proportionalLayout = typeof options.proportionalLayout !== 'undefined' ? !!options.proportionalLayout : true;
this.root = new BranchNode(Orientation.VERTICAL, this.styles, this.proportionalLayout);
}
style(styles: IGridViewStyles): void {
@@ -564,7 +610,7 @@ export class GridView implements IDisposable {
const [, parentIndex] = tail(rest);
grandParent.removeChild(parentIndex);
const newParent = new BranchNode(parent.orientation, this.styles, parent.size, parent.orthogonalSize);
const newParent = new BranchNode(parent.orientation, this.styles, this.proportionalLayout, parent.size, parent.orthogonalSize);
grandParent.addChild(newParent, parent.size, parentIndex);
newParent.orthogonalLayout(parent.orthogonalSize);
@@ -648,6 +694,16 @@ export class GridView implements IDisposable {
return node.view;
}
moveView(parentLocation: number[], from: number, to: number): void {
const [, parent] = this.getNode(parentLocation);
if (!(parent instanceof BranchNode)) {
throw new Error('Invalid location');
}
parent.moveChild(from, to);
}
swapViews(from: number[], to: number[]): void {
const [fromRest, fromIndex] = tail(from);
const [, fromParent] = this.getNode(fromRest);

View File

@@ -2,12 +2,12 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IDisposable } from 'vs/base/common/lifecycle';
import * as dom from 'vs/base/browser/dom';
import * as objects from 'vs/base/common/objects';
import { renderOcticons } from 'vs/base/browser/ui/octiconLabel/octiconLabel';
import { escape } from 'vs/base/common/strings';
export interface IHighlight {
start: number;
@@ -22,7 +22,7 @@ export class HighlightedLabel implements IDisposable {
private highlights: IHighlight[];
private didEverRender: boolean;
constructor(container: HTMLElement) {
constructor(container: HTMLElement, private supportOcticons: boolean) {
this.domNode = document.createElement('span');
this.domNode.className = 'monaco-highlighted-label';
this.didEverRender = false;
@@ -69,19 +69,22 @@ export class HighlightedLabel implements IDisposable {
}
if (pos < highlight.start) {
htmlContent.push('<span>');
htmlContent.push(renderOcticons(this.text.substring(pos, highlight.start)));
const substring = this.text.substring(pos, highlight.start);
htmlContent.push(this.supportOcticons ? renderOcticons(substring) : escape(substring));
htmlContent.push('</span>');
pos = highlight.end;
}
htmlContent.push('<span class="highlight">');
htmlContent.push(renderOcticons(this.text.substring(highlight.start, highlight.end)));
const substring = this.text.substring(highlight.start, highlight.end);
htmlContent.push(this.supportOcticons ? renderOcticons(substring) : escape(substring));
htmlContent.push('</span>');
pos = highlight.end;
}
if (pos < this.text.length) {
htmlContent.push('<span>');
htmlContent.push(renderOcticons(this.text.substring(pos)));
const substring = this.text.substring(pos);
htmlContent.push(this.supportOcticons ? renderOcticons(substring) : escape(substring));
htmlContent.push('</span>');
}
@@ -91,8 +94,8 @@ export class HighlightedLabel implements IDisposable {
}
dispose() {
this.text = null;
this.highlights = null;
this.text = null!; // StrictNullOverride: nulling out ok in dispose
this.highlights = null!; // StrictNullOverride: nulling out ok in dispose
}
static escapeNewLines(text: string, highlights: IHighlight[]): string {

View File

@@ -3,20 +3,16 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./iconlabel';
import * as dom from 'vs/base/browser/dom';
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
import { IMatch } from 'vs/base/common/filters';
import uri from 'vs/base/common/uri';
import * as paths from 'vs/base/common/paths';
import { IWorkspaceFolderProvider, getPathLabel, IUserHomeProvider, getBaseLabel } from 'vs/base/common/labels';
import { IDisposable, combinedDisposable, Disposable } from 'vs/base/common/lifecycle';
export interface IIconLabelCreationOptions {
supportHighlights?: boolean;
supportDescriptionHighlights?: boolean;
donotSupportOcticons?: boolean;
}
export interface IIconLabelValueOptions {
@@ -26,6 +22,7 @@ export interface IIconLabelValueOptions {
extraClasses?: string[];
italic?: boolean;
matches?: IMatch[];
labelEscapeNewLines?: boolean;
descriptionMatches?: IMatch[];
}
@@ -103,13 +100,13 @@ export class IconLabel extends Disposable {
this.labelDescriptionContainer = this._register(new FastLabelNode(dom.append(this.domNode.element, dom.$('.monaco-icon-label-description-container'))));
if (options && options.supportHighlights) {
this.labelNode = this._register(new HighlightedLabel(dom.append(this.labelDescriptionContainer.element, dom.$('a.label-name'))));
this.labelNode = this._register(new HighlightedLabel(dom.append(this.labelDescriptionContainer.element, dom.$('a.label-name')), !options.donotSupportOcticons));
} else {
this.labelNode = this._register(new FastLabelNode(dom.append(this.labelDescriptionContainer.element, dom.$('a.label-name'))));
}
if (options && options.supportDescriptionHighlights) {
this.descriptionNodeFactory = () => this._register(new HighlightedLabel(dom.append(this.labelDescriptionContainer.element, dom.$('span.label-description'))));
this.descriptionNodeFactory = () => this._register(new HighlightedLabel(dom.append(this.labelDescriptionContainer.element, dom.$('span.label-description')), !options.donotSupportOcticons));
} else {
this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.labelDescriptionContainer.element, dom.$('span.label-description'))));
}
@@ -141,7 +138,7 @@ export class IconLabel extends Disposable {
this.domNode.title = options && options.title ? options.title : '';
if (this.labelNode instanceof HighlightedLabel) {
this.labelNode.set(label || '', options ? options.matches : void 0);
this.labelNode.set(label || '', options ? options.matches : void 0, options && options.title ? options.title : void 0, options && options.labelEscapeNewLines);
} else {
this.labelNode.textContent = label || '';
}
@@ -167,17 +164,3 @@ export class IconLabel extends Disposable {
}
}
export class FileLabel extends IconLabel {
constructor(container: HTMLElement, file: uri, provider: IWorkspaceFolderProvider, userHome?: IUserHomeProvider) {
super(container);
this.setFile(file, provider, userHome);
}
setFile(file: uri, provider: IWorkspaceFolderProvider, userHome: IUserHomeProvider): void {
const parent = paths.dirname(file.fsPath);
this.setValue(getBaseLabel(file), parent && parent !== '.' ? getPathLabel(parent, userHome, provider) : '', { title: file.fsPath });
}
}

View File

@@ -40,6 +40,7 @@
}
.monaco-icon-label > .monaco-icon-label-description-container > .label-description {
opacity: .7;
margin-left: 0.5em;
font-size: 0.9em;
white-space: pre; /* enable to show labels that include multiple whitespaces */
@@ -67,3 +68,10 @@
{
color: inherit !important;
}
.monaco-tree-row.focused.selected .label-description,
.monaco-tree-row.selected .label-description,
.monaco-list-row.focused.selected .label-description,
.monaco-list-row.selected .label-description {
opacity: .8;
}

View File

@@ -74,7 +74,6 @@
box-sizing: border-box;
white-space: pre-wrap;
visibility: hidden;
min-height: 26px;
word-wrap: break-word;
}

View File

@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./inputBox';
@@ -43,10 +42,13 @@ export interface IInputBoxStyles {
inputBorder?: Color;
inputValidationInfoBorder?: Color;
inputValidationInfoBackground?: Color;
inputValidationInfoForeground?: Color;
inputValidationWarningBorder?: Color;
inputValidationWarningBackground?: Color;
inputValidationWarningForeground?: Color;
inputValidationErrorBorder?: Color;
inputValidationErrorBackground?: Color;
inputValidationErrorForeground?: Color;
}
export interface IInputValidator {
@@ -63,7 +65,7 @@ export interface IInputValidationOptions {
validation: IInputValidator;
}
export enum MessageType {
export const enum MessageType {
INFO = 1,
WARNING = 2,
ERROR = 3
@@ -108,10 +110,13 @@ export class InputBox extends Widget {
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;
@@ -136,10 +141,13 @@ export class InputBox extends Widget {
this.inputValidationInfoBorder = this.options.inputValidationInfoBorder;
this.inputValidationInfoBackground = this.options.inputValidationInfoBackground;
this.inputValidationInfoForeground = this.options.inputValidationInfoForeground;
this.inputValidationWarningBorder = this.options.inputValidationWarningBorder;
this.inputValidationWarningBackground = this.options.inputValidationWarningBackground;
this.inputValidationWarningForeground = this.options.inputValidationWarningForeground;
this.inputValidationErrorBorder = this.options.inputValidationErrorBorder;
this.inputValidationErrorBackground = this.options.inputValidationErrorBackground;
this.inputValidationErrorForeground = this.options.inputValidationErrorForeground;
if (this.options.validationOptions) {
this.validation = this.options.validationOptions.validation;
@@ -162,6 +170,7 @@ export class InputBox extends Widget {
if (this.options.flexibleHeight) {
this.mirror = dom.append(wrapper, $('div.mirror'));
this.mirror.innerHTML = '&nbsp;';
} else {
this.input.type = this.options.type || 'text';
this.input.setAttribute('wrap', 'off');
@@ -274,7 +283,7 @@ export class InputBox extends Widget {
return document.activeElement === this.input;
}
public select(range: IRange = null): void {
public select(range: IRange | null = null): void {
this.input.select();
if (range) {
@@ -305,6 +314,9 @@ export class InputBox extends Widget {
public set width(width: number) {
this.input.style.width = width + 'px';
if (this.mirror) {
this.mirror.style.width = width + 'px';
}
}
public showMessage(message: IMessage, force?: boolean): void {
@@ -353,7 +365,7 @@ export class InputBox extends Widget {
}
public validate(): boolean {
let errorMsg: IMessage = null;
let errorMsg: IMessage | null = null;
if (this.validation) {
errorMsg = this.validation(this.value);
@@ -380,11 +392,11 @@ export class InputBox extends Widget {
return errorMsg ? errorMsg.type !== MessageType.ERROR : true;
}
private stylesForType(type: MessageType): { border: Color; background: Color } {
private stylesForType(type: MessageType): { border: Color; background: Color; foreground: Color } {
switch (type) {
case MessageType.INFO: return { border: this.inputValidationInfoBorder, background: this.inputValidationInfoBackground };
case MessageType.WARNING: return { border: this.inputValidationWarningBorder, background: this.inputValidationWarningBackground };
default: return { border: this.inputValidationErrorBorder, background: this.inputValidationErrorBackground };
case MessageType.INFO: return { border: this.inputValidationInfoBorder, background: this.inputValidationInfoBackground, foreground: this.inputValidationInfoForeground };
case MessageType.WARNING: return { border: this.inputValidationWarningBorder, background: this.inputValidationWarningBackground, foreground: this.inputValidationWarningForeground };
default: return { border: this.inputValidationErrorBorder, background: this.inputValidationErrorBackground, foreground: this.inputValidationErrorForeground };
}
}
@@ -426,6 +438,7 @@ export class InputBox extends Widget {
const styles = this.stylesForType(this.message.type);
spanElement.style.backgroundColor = styles.background ? styles.background.toString() : null;
spanElement.style.color = styles.foreground ? styles.foreground.toString() : null;
spanElement.style.border = styles.border ? `1px solid ${styles.border}` : null;
dom.append(div, spanElement);
@@ -463,9 +476,16 @@ export class InputBox extends Widget {
}
const value = this.value || this.placeholder;
let lastCharCode = value.charCodeAt(value.length - 1);
let suffix = lastCharCode === 10 ? ' ' : '';
this.mirror.textContent = value + suffix;
const lastCharCode = value.charCodeAt(value.length - 1);
const suffix = lastCharCode === 10 ? ' ' : '';
const mirrorTextContent = value + suffix;
if (mirrorTextContent) {
this.mirror.textContent = value + suffix;
} else {
this.mirror.innerHTML = '&nbsp;';
}
this.layout();
}
@@ -475,10 +495,13 @@ export class InputBox extends Widget {
this.inputBorder = styles.inputBorder;
this.inputValidationInfoBackground = styles.inputValidationInfoBackground;
this.inputValidationInfoForeground = styles.inputValidationInfoForeground;
this.inputValidationInfoBorder = styles.inputValidationInfoBorder;
this.inputValidationWarningBackground = styles.inputValidationWarningBackground;
this.inputValidationWarningForeground = styles.inputValidationWarningForeground;
this.inputValidationWarningBorder = styles.inputValidationWarningBorder;
this.inputValidationErrorBackground = styles.inputValidationErrorBackground;
this.inputValidationErrorForeground = styles.inputValidationErrorForeground;
this.inputValidationErrorBorder = styles.inputValidationErrorBorder;
this.applyStyles();

View File

@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./keybindingLabel';
import { IDisposable } from 'vs/base/common/lifecycle';
@@ -63,44 +62,43 @@ export class KeybindingLabel implements IDisposable {
this.renderPart(this.domNode, firstPart, this.matches ? this.matches.firstPart : null);
}
if (chordPart) {
dom.append(this.domNode, $('span.monaco-keybinding-key-chord-separator', null, ' '));
dom.append(this.domNode, $('span.monaco-keybinding-key-chord-separator', undefined, ' '));
this.renderPart(this.domNode, chordPart, this.matches ? this.matches.chordPart : null);
}
this.domNode.title = this.keybinding.getAriaLabel();
this.domNode.title = this.keybinding.getAriaLabel() || '';
}
this.didEverRender = true;
}
private renderPart(parent: HTMLElement, part: ResolvedKeybindingPart, match: PartMatches) {
private renderPart(parent: HTMLElement, part: ResolvedKeybindingPart, match: PartMatches | null) {
const modifierLabels = UILabelProvider.modifierLabels[this.os];
if (part.ctrlKey) {
this.renderKey(parent, modifierLabels.ctrlKey, match && match.ctrlKey, modifierLabels.separator);
this.renderKey(parent, modifierLabels.ctrlKey, Boolean(match && match.ctrlKey), modifierLabels.separator);
}
if (part.shiftKey) {
this.renderKey(parent, modifierLabels.shiftKey, match && match.shiftKey, modifierLabels.separator);
this.renderKey(parent, modifierLabels.shiftKey, Boolean(match && match.shiftKey), modifierLabels.separator);
}
if (part.altKey) {
this.renderKey(parent, modifierLabels.altKey, match && match.altKey, modifierLabels.separator);
this.renderKey(parent, modifierLabels.altKey, Boolean(match && match.altKey), modifierLabels.separator);
}
if (part.metaKey) {
this.renderKey(parent, modifierLabels.metaKey, match && match.metaKey, modifierLabels.separator);
this.renderKey(parent, modifierLabels.metaKey, Boolean(match && match.metaKey), modifierLabels.separator);
}
const keyLabel = part.keyLabel;
if (keyLabel) {
this.renderKey(parent, keyLabel, match && match.keyCode, '');
this.renderKey(parent, keyLabel, Boolean(match && match.keyCode), '');
}
}
private renderKey(parent: HTMLElement, label: string, highlight: boolean, separator: string): void {
dom.append(parent, $('span.monaco-keybinding-key' + (highlight ? '.highlight' : ''), null, label));
dom.append(parent, $('span.monaco-keybinding-key' + (highlight ? '.highlight' : ''), undefined, label));
if (separator) {
dom.append(parent, $('span.monaco-keybinding-key-separator', null, separator));
dom.append(parent, $('span.monaco-keybinding-key-separator', undefined, separator));
}
}
dispose() {
this.keybinding = null;
}
private static areSame(a: Matches, b: Matches): boolean {

View File

@@ -8,6 +8,9 @@
height: 100%;
width: 100%;
white-space: nowrap;
}
.monaco-list.mouse-support {
-webkit-user-select: none;
-khtml-user-select: none;
-moz-user-select: -moz-none;
@@ -32,9 +35,12 @@
-o-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
cursor: pointer;
overflow: hidden;
width: 100%;
}
.monaco-list.mouse-support .monaco-list-row {
cursor: pointer;
touch-action: none;
}

View File

@@ -5,50 +5,51 @@
import { GestureEvent } from 'vs/base/browser/touch';
export interface IVirtualDelegate<T> {
export interface IListVirtualDelegate<T> {
getHeight(element: T): number;
getTemplateId(element: T): string;
hasDynamicHeight?(element: T): boolean;
}
export interface IRenderer<TElement, TTemplateData> {
export interface IListRenderer<T, TTemplateData> {
templateId: string;
renderTemplate(container: HTMLElement): TTemplateData;
renderElement(element: TElement, index: number, templateData: TTemplateData): void;
disposeElement(element: TElement, index: number, templateData: TTemplateData): void;
renderElement(element: T, index: number, templateData: TTemplateData): void;
disposeElement(element: T, index: number, templateData: TTemplateData): void;
disposeTemplate(templateData: TTemplateData): void;
}
export interface IListOpenEvent<T> {
elements: T[];
indexes: number[];
browserEvent?: UIEvent;
}
export interface IListEvent<T> {
elements: T[];
indexes: number[];
browserEvent?: UIEvent;
}
export interface IListMouseEvent<T> {
browserEvent: MouseEvent;
element: T | undefined;
index: number;
index: number | undefined;
}
export interface IListTouchEvent<T> {
browserEvent: TouchEvent;
element: T | undefined;
index: number;
index: number | undefined;
}
export interface IListGestureEvent<T> {
browserEvent: GestureEvent;
element: T | undefined;
index: number;
index: number | undefined;
}
export interface IListContextMenuEvent<T> {
element: T;
index: number;
anchor: HTMLElement | { x: number; y: number; };
browserEvent: UIEvent;
element: T | undefined;
index: number | undefined;
anchor: HTMLElement | { x: number; y: number; } | undefined;
}
export interface IIdentityProvider<T> {
getId(element: T): { toString(): string; };
}

View File

@@ -6,21 +6,22 @@
import 'vs/css!./list';
import { IDisposable } from 'vs/base/common/lifecycle';
import { range } from 'vs/base/common/arrays';
import { IVirtualDelegate, IRenderer, IListEvent, IListOpenEvent } from './list';
import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent } from './list';
import { List, IListStyles, IListOptions } from './listWidget';
import { IPagedModel } from 'vs/base/common/paging';
import { Event, mapEvent } from 'vs/base/common/event';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
export interface IPagedRenderer<TElement, TTemplateData> extends IRenderer<TElement, TTemplateData> {
export interface IPagedRenderer<TElement, TTemplateData> extends IListRenderer<TElement, TTemplateData> {
renderPlaceholder(index: number, templateData: TTemplateData): void;
}
export interface ITemplateData<T> {
data: T;
disposable: IDisposable;
data?: T;
disposable?: IDisposable;
}
class PagedRenderer<TElement, TTemplateData> implements IRenderer<number, ITemplateData<TTemplateData>> {
class PagedRenderer<TElement, TTemplateData> implements IListRenderer<number, ITemplateData<TTemplateData>> {
get templateId(): string { return this.renderer.templateId; }
@@ -35,7 +36,13 @@ class PagedRenderer<TElement, TTemplateData> implements IRenderer<number, ITempl
}
renderElement(index: number, _: number, data: ITemplateData<TTemplateData>): void {
data.disposable.dispose();
if (data.disposable) {
data.disposable.dispose();
}
if (!data.data) {
return;
}
const model = this.modelProvider();
@@ -43,11 +50,12 @@ class PagedRenderer<TElement, TTemplateData> implements IRenderer<number, ITempl
return this.renderer.renderElement(model.get(index), index, data.data);
}
const promise = model.resolve(index);
data.disposable = { dispose: () => promise.cancel() };
const cts = new CancellationTokenSource();
const promise = model.resolve(index, cts.token);
data.disposable = { dispose: () => cts.cancel() };
this.renderer.renderPlaceholder(index, data.data);
promise.done(entry => this.renderer.renderElement(entry, index, data.data));
promise.then(entry => this.renderer.renderElement(entry, index, data.data!));
}
disposeElement(): void {
@@ -55,10 +63,14 @@ class PagedRenderer<TElement, TTemplateData> implements IRenderer<number, ITempl
}
disposeTemplate(data: ITemplateData<TTemplateData>): void {
data.disposable.dispose();
data.disposable = null;
this.renderer.disposeTemplate(data.data);
data.data = null;
if (data.disposable) {
data.disposable.dispose();
data.disposable = undefined;
}
if (data.data) {
this.renderer.disposeTemplate(data.data);
data.data = undefined;
}
}
}
@@ -69,7 +81,7 @@ export class PagedList<T> implements IDisposable {
constructor(
container: HTMLElement,
virtualDelegate: IVirtualDelegate<number>,
virtualDelegate: IListVirtualDelegate<number>,
renderers: IPagedRenderer<T, any>[],
options: IListOptions<any> = {}
) {
@@ -109,7 +121,7 @@ export class PagedList<T> implements IDisposable {
return mapEvent(this.list.onFocusChange, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes }));
}
get onOpen(): Event<IListOpenEvent<T>> {
get onOpen(): Event<IListEvent<T>> {
return mapEvent(this.list.onOpen, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent }));
}
@@ -121,6 +133,10 @@ export class PagedList<T> implements IDisposable {
return mapEvent(this.list.onPin, ({ elements, indexes }) => ({ elements: elements.map(e => this._model.get(e)), indexes }));
}
get onContextMenu(): Event<IListContextMenuEvent<T>> {
return mapEvent(this.list.onContextMenu, ({ element, index, anchor, browserEvent }) => (typeof element === 'undefined' ? { element, index, anchor, browserEvent } : { element: this._model.get(element), index, anchor, browserEvent }));
}
get model(): IPagedModel<T> {
return this._model;
}
@@ -158,14 +174,6 @@ export class PagedList<T> implements IDisposable {
this.list.focusPrevious(n, loop);
}
selectNext(n?: number, loop?: boolean): void {
this.list.selectNext(n, loop);
}
selectPrevious(n?: number, loop?: boolean): void {
this.list.selectPrevious(n, loop);
}
focusNextPage(): void {
this.list.focusNextPage();
}

View File

@@ -7,18 +7,19 @@ import { getOrDefault } from 'vs/base/common/objects';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Gesture, EventType as TouchEventType, GestureEvent } from 'vs/base/browser/touch';
import * as DOM from 'vs/base/browser/dom';
import { Event, mapEvent, filterEvent } from 'vs/base/common/event';
import { Event, mapEvent, filterEvent, Emitter, latch } from 'vs/base/common/event';
import { domEvent } from 'vs/base/browser/event';
import { ScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollEvent, ScrollbarVisibility } from 'vs/base/common/scrollable';
import { RangeMap, IRange, relativeComplement, intersect, shift } from './rangeMap';
import { IVirtualDelegate, IRenderer, IListMouseEvent, IListTouchEvent, IListGestureEvent } from './list';
import { ScrollEvent, ScrollbarVisibility, INewScrollDimensions } from 'vs/base/common/scrollable';
import { RangeMap, shift } from './rangeMap';
import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListTouchEvent, IListGestureEvent } from './list';
import { RowCache, IRow } from './rowCache';
import { isWindows } from 'vs/base/common/platform';
import * as browser from 'vs/base/browser/browser';
import { ISpliceable } from 'vs/base/common/sequence';
import { memoize } from 'vs/base/common/decorators';
import { DragMouseEvent } from 'vs/base/browser/mouseEvent';
import { Range, IRange } from 'vs/base/common/range';
function canUseTranslate3d(): boolean {
if (browser.isFirefox) {
@@ -32,48 +33,64 @@ function canUseTranslate3d(): boolean {
return true;
}
interface IItem<T> {
id: string;
element: T;
readonly id: string;
readonly element: T;
readonly templateId: string;
row: IRow | null;
size: number;
templateId: string;
row: IRow;
hasDynamicHeight: boolean;
renderWidth: number | undefined;
}
export interface IListViewOptions {
useShadows?: boolean;
verticalScrollMode?: ScrollbarVisibility;
readonly useShadows?: boolean;
readonly verticalScrollMode?: ScrollbarVisibility;
readonly setRowLineHeight?: boolean;
readonly supportDynamicHeights?: boolean;
readonly mouseSupport?: boolean;
}
const DefaultOptions: IListViewOptions = {
const DefaultOptions = {
useShadows: true,
verticalScrollMode: ScrollbarVisibility.Auto
verticalScrollMode: ScrollbarVisibility.Auto,
setRowLineHeight: true,
supportDynamicHeights: false
};
export class ListView<T> implements ISpliceable<T>, IDisposable {
readonly domNode: HTMLElement;
private items: IItem<T>[];
private itemId: number;
private rangeMap: RangeMap;
private cache: RowCache<T>;
private renderers = new Map<string, IRenderer<T, any>>();
private renderers = new Map<string, IListRenderer<any /* TODO@joao */, any>>();
private lastRenderTop: number;
private lastRenderHeight: number;
private _domNode: HTMLElement;
private renderWidth = 0;
private gesture: Gesture;
private rowsContainer: HTMLElement;
private scrollableElement: ScrollableElement;
private _scrollHeight: number;
private scrollableElementUpdateDisposable: IDisposable | null = null;
private splicing = false;
private dragAndDropScrollInterval: number;
private dragAndDropScrollTimeout: number;
private dragAndDropMouseY: number;
private setRowLineHeight: boolean;
private supportDynamicHeights: boolean;
private disposables: IDisposable[];
private _onDidChangeContentHeight = new Emitter<number>();
readonly onDidChangeContentHeight: Event<number> = latch(this._onDidChangeContentHeight.event);
get contentHeight(): number { return this.rangeMap.size; }
constructor(
container: HTMLElement,
private virtualDelegate: IVirtualDelegate<T>,
renderers: IRenderer<T, any>[],
private virtualDelegate: IListVirtualDelegate<T>,
renderers: IListRenderer<any /* TODO@joao */, any>[],
options: IListViewOptions = DefaultOptions
) {
this.items = [];
@@ -89,8 +106,9 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
this.lastRenderTop = 0;
this.lastRenderHeight = 0;
this._domNode = document.createElement('div');
this._domNode.className = 'monaco-list';
this.domNode = document.createElement('div');
this.domNode.className = 'monaco-list';
DOM.toggleClass(this.domNode, 'mouse-support', typeof options.mouseSupport === 'boolean' ? options.mouseSupport : true);
this.rowsContainer = document.createElement('div');
this.rowsContainer.className = 'monaco-list-rows';
@@ -103,8 +121,8 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
useShadows: getOrDefault(options, o => o.useShadows, DefaultOptions.useShadows)
});
this._domNode.appendChild(this.scrollableElement.getDomNode());
container.appendChild(this._domNode);
this.domNode.appendChild(this.scrollableElement.getDomNode());
container.appendChild(this.domNode);
this.disposables = [this.rangeMap, this.gesture, this.scrollableElement, this.cache];
@@ -119,11 +137,10 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
const onDragOver = mapEvent(domEvent(this.rowsContainer, 'dragover'), e => new DragMouseEvent(e));
onDragOver(this.onDragOver, this, this.disposables);
this.layout();
}
this.setRowLineHeight = getOrDefault(options, o => o.setRowLineHeight, DefaultOptions.setRowLineHeight);
this.supportDynamicHeights = getOrDefault(options, o => o.supportDynamicHeights, DefaultOptions.supportDynamicHeights);
get domNode(): HTMLElement {
return this._domNode;
this.layout();
}
splice(start: number, deleteCount: number, elements: T[] = []): T[] {
@@ -137,72 +154,95 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
return this._splice(start, deleteCount, elements);
} finally {
this.splicing = false;
this._onDidChangeContentHeight.fire(this.contentHeight);
}
}
private _splice(start: number, deleteCount: number, elements: T[] = []): T[] {
const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
const deleteRange = { start, end: start + deleteCount };
const removeRange = intersect(previousRenderRange, deleteRange);
const removeRange = Range.intersect(previousRenderRange, deleteRange);
for (let i = removeRange.start; i < removeRange.end; i++) {
this.removeItemFromDOM(i);
}
const previousRestRange: IRange = { start: start + deleteCount, end: this.items.length };
const previousRenderedRestRange = intersect(previousRestRange, previousRenderRange);
const previousUnrenderedRestRanges = relativeComplement(previousRestRange, previousRenderRange);
const previousRenderedRestRange = Range.intersect(previousRestRange, previousRenderRange);
const previousUnrenderedRestRanges = Range.relativeComplement(previousRestRange, previousRenderRange);
const inserted = elements.map<IItem<T>>(element => ({
id: String(this.itemId++),
element,
size: this.virtualDelegate.getHeight(element),
templateId: this.virtualDelegate.getTemplateId(element),
size: this.virtualDelegate.getHeight(element),
hasDynamicHeight: !!this.virtualDelegate.hasDynamicHeight && this.virtualDelegate.hasDynamicHeight(element),
renderWidth: undefined,
row: null
}));
this.rangeMap.splice(start, deleteCount, ...inserted);
const deleted = this.items.splice(start, deleteCount, ...inserted);
let deleted: IItem<T>[];
// TODO@joao: improve this optimization to catch even more cases
if (start === 0 && deleteCount >= this.items.length) {
this.rangeMap = new RangeMap();
this.rangeMap.splice(0, 0, inserted);
this.items = inserted;
deleted = [];
} else {
this.rangeMap.splice(start, deleteCount, inserted);
deleted = this.items.splice(start, deleteCount, ...inserted);
}
const delta = elements.length - deleteCount;
const renderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
const renderedRestRange = shift(previousRenderedRestRange, delta);
const updateRange = intersect(renderRange, renderedRestRange);
const updateRange = Range.intersect(renderRange, renderedRestRange);
for (let i = updateRange.start; i < updateRange.end; i++) {
this.updateItemInDOM(this.items[i], i);
}
const removeRanges = relativeComplement(renderedRestRange, renderRange);
const removeRanges = Range.relativeComplement(renderedRestRange, renderRange);
for (let r = 0; r < removeRanges.length; r++) {
const removeRange = removeRanges[r];
for (let i = removeRange.start; i < removeRange.end; i++) {
for (const range of removeRanges) {
for (let i = range.start; i < range.end; i++) {
this.removeItemFromDOM(i);
}
}
const unrenderedRestRanges = previousUnrenderedRestRanges.map(r => shift(r, delta));
const elementsRange = { start, end: start + elements.length };
const insertRanges = [elementsRange, ...unrenderedRestRanges].map(r => intersect(renderRange, r));
const insertRanges = [elementsRange, ...unrenderedRestRanges].map(r => Range.intersect(renderRange, r));
const beforeElement = this.getNextToLastElement(insertRanges);
for (let r = 0; r < insertRanges.length; r++) {
const insertRange = insertRanges[r];
for (let i = insertRange.start; i < insertRange.end; i++) {
for (const range of insertRanges) {
for (let i = range.start; i < range.end; i++) {
this.insertItemInDOM(i, beforeElement);
}
}
const scrollHeight = this.getContentHeight();
this.rowsContainer.style.height = `${scrollHeight}px`;
this.scrollableElement.setScrollDimensions({ scrollHeight });
this.updateScrollHeight();
if (this.supportDynamicHeights) {
this.rerender(this.scrollTop, this.renderHeight);
}
return deleted.map(i => i.element);
}
private updateScrollHeight(): void {
this._scrollHeight = this.contentHeight;
this.rowsContainer.style.height = `${this._scrollHeight}px`;
if (!this.scrollableElementUpdateDisposable) {
this.scrollableElementUpdateDisposable = DOM.scheduleAtNextAnimationFrame(() => {
this.scrollableElement.setScrollDimensions({ scrollHeight: this._scrollHeight });
this.scrollableElementUpdateDisposable = null;
});
}
}
get length(): number {
return this.items.length;
}
@@ -216,7 +256,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
return this.items[index].element;
}
domElement(index: number): HTMLElement {
domElement(index: number): HTMLElement | null {
const row = this.items[index].row;
return row && row.domNode;
}
@@ -238,9 +278,25 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
}
layout(height?: number): void {
this.scrollableElement.setScrollDimensions({
height: height || DOM.getContentHeight(this._domNode)
});
let scrollDimensions: INewScrollDimensions = {
height: height || DOM.getContentHeight(this.domNode)
};
if (this.scrollableElementUpdateDisposable) {
this.scrollableElementUpdateDisposable.dispose();
this.scrollableElementUpdateDisposable = null;
scrollDimensions.scrollHeight = this._scrollHeight;
}
this.scrollableElement.setScrollDimensions(scrollDimensions);
}
layoutWidth(width: number): void {
this.renderWidth = width;
if (this.supportDynamicHeights) {
this.rerender(this.scrollTop, this.renderHeight);
}
}
// Render
@@ -249,8 +305,8 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
const previousRenderRange = this.getRenderRange(this.lastRenderTop, this.lastRenderHeight);
const renderRange = this.getRenderRange(renderTop, renderHeight);
const rangesToInsert = relativeComplement(renderRange, previousRenderRange);
const rangesToRemove = relativeComplement(previousRenderRange, renderRange);
const rangesToInsert = Range.relativeComplement(renderRange, previousRenderRange);
const rangesToRemove = Range.relativeComplement(previousRenderRange, renderRange);
const beforeElement = this.getNextToLastElement(rangesToInsert);
for (const range of rangesToInsert) {
@@ -286,15 +342,14 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
item.row = this.cache.alloc(item.templateId);
}
if (!item.row.domNode.parentElement) {
if (!item.row.domNode!.parentElement) {
if (beforeElement) {
this.rowsContainer.insertBefore(item.row.domNode, beforeElement);
this.rowsContainer.insertBefore(item.row.domNode!, beforeElement);
} else {
this.rowsContainer.appendChild(item.row.domNode);
this.rowsContainer.appendChild(item.row.domNode!);
}
}
item.row.domNode.style.height = `${item.size}px`;
this.updateItemInDOM(item, index);
const renderer = this.renderers.get(item.templateId);
@@ -302,11 +357,17 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
}
private updateItemInDOM(item: IItem<T>, index: number): void {
item.row.domNode.style.top = `${this.elementTop(index)}px`;
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!.style.top = `${this.elementTop(index)}px`;
item.row!.domNode!.style.height = `${item.size}px`;
if (this.setRowLineHeight) {
item.row!.domNode!.style.lineHeight = `${item.size}px`;
}
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}`);
}
private removeItemFromDOM(index: number): void {
@@ -314,23 +375,25 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
const renderer = this.renderers.get(item.templateId);
if (renderer.disposeElement) {
renderer.disposeElement(item.element, index, item.row.templateData);
renderer.disposeElement(item.element, index, item.row!.templateData);
}
this.cache.release(item.row);
this.cache.release(item.row!);
item.row = null;
}
getContentHeight(): number {
return this.rangeMap.size;
}
getScrollTop(): number {
const scrollPosition = this.scrollableElement.getScrollPosition();
return scrollPosition.scrollTop;
}
setScrollTop(scrollTop: number): void {
if (this.scrollableElementUpdateDisposable) {
this.scrollableElementUpdateDisposable.dispose();
this.scrollableElementUpdateDisposable = null;
this.scrollableElement.setScrollDimensions({ scrollHeight: this._scrollHeight });
}
this.scrollableElement.setScrollPosition({ scrollTop });
}
@@ -342,36 +405,41 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
this.setScrollTop(scrollTop);
}
get scrollHeight(): number {
return this._scrollHeight;
}
// Events
@memoize get onMouseClick(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'click'), e => this.toMouseEvent(e)), e => e.index >= 0); }
@memoize get onMouseDblClick(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'dblclick'), e => this.toMouseEvent(e)), e => e.index >= 0); }
@memoize get onMouseUp(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'mouseup'), e => this.toMouseEvent(e)), e => e.index >= 0); }
@memoize get onMouseDown(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'mousedown'), e => this.toMouseEvent(e)), e => e.index >= 0); }
@memoize get onMouseOver(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'mouseover'), e => this.toMouseEvent(e)), e => e.index >= 0); }
@memoize get onMouseMove(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'mousemove'), e => this.toMouseEvent(e)), e => e.index >= 0); }
@memoize get onMouseOut(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'mouseout'), e => this.toMouseEvent(e)), e => e.index >= 0); }
@memoize get onContextMenu(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'contextmenu'), e => this.toMouseEvent(e)), e => e.index >= 0); }
@memoize get onTouchStart(): Event<IListTouchEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'touchstart'), e => this.toTouchEvent(e)), e => e.index >= 0); }
@memoize get onTap(): Event<IListGestureEvent<T>> { return filterEvent(mapEvent(domEvent(this.rowsContainer, TouchEventType.Tap), e => this.toGestureEvent(e)), e => e.index >= 0); }
@memoize get onMouseClick(): Event<IListMouseEvent<T>> { return mapEvent(domEvent(this.domNode, 'click'), e => this.toMouseEvent(e)); }
@memoize get onMouseDblClick(): Event<IListMouseEvent<T>> { return mapEvent(domEvent(this.domNode, 'dblclick'), e => this.toMouseEvent(e)); }
@memoize get onMouseMiddleClick(): Event<IListMouseEvent<T>> { return filterEvent(mapEvent(domEvent(this.domNode, 'auxclick'), e => this.toMouseEvent(e as MouseEvent)), e => e.browserEvent.button === 1); }
@memoize get onMouseUp(): Event<IListMouseEvent<T>> { return mapEvent(domEvent(this.domNode, 'mouseup'), e => this.toMouseEvent(e)); }
@memoize get onMouseDown(): Event<IListMouseEvent<T>> { return mapEvent(domEvent(this.domNode, 'mousedown'), e => this.toMouseEvent(e)); }
@memoize get onMouseOver(): Event<IListMouseEvent<T>> { return mapEvent(domEvent(this.domNode, 'mouseover'), e => this.toMouseEvent(e)); }
@memoize get onMouseMove(): Event<IListMouseEvent<T>> { return mapEvent(domEvent(this.domNode, 'mousemove'), e => this.toMouseEvent(e)); }
@memoize get onMouseOut(): Event<IListMouseEvent<T>> { return mapEvent(domEvent(this.domNode, 'mouseout'), e => this.toMouseEvent(e)); }
@memoize get onContextMenu(): Event<IListMouseEvent<T>> { return mapEvent(domEvent(this.domNode, 'contextmenu'), e => this.toMouseEvent(e)); }
@memoize get onTouchStart(): Event<IListTouchEvent<T>> { return mapEvent(domEvent(this.domNode, 'touchstart'), e => this.toTouchEvent(e)); }
@memoize get onTap(): Event<IListGestureEvent<T>> { return mapEvent(domEvent(this.rowsContainer, TouchEventType.Tap), e => this.toGestureEvent(e)); }
private toMouseEvent(browserEvent: MouseEvent): IListMouseEvent<T> {
const index = this.getItemIndexFromEventTarget(browserEvent.target);
const item = index < 0 ? undefined : this.items[index];
const index = this.getItemIndexFromEventTarget(browserEvent.target || null);
const item = typeof index === 'undefined' ? undefined : this.items[index];
const element = item && item.element;
return { browserEvent, index, element };
}
private toTouchEvent(browserEvent: TouchEvent): IListTouchEvent<T> {
const index = this.getItemIndexFromEventTarget(browserEvent.target);
const item = index < 0 ? undefined : this.items[index];
const index = this.getItemIndexFromEventTarget(browserEvent.target || null);
const item = typeof index === 'undefined' ? undefined : this.items[index];
const element = item && item.element;
return { browserEvent, index, element };
}
private toGestureEvent(browserEvent: GestureEvent): IListGestureEvent<T> {
const index = this.getItemIndexFromEventTarget(browserEvent.initialTarget);
const item = index < 0 ? undefined : this.items[index];
const index = this.getItemIndexFromEventTarget(browserEvent.initialTarget || null);
const item = typeof index === 'undefined' ? undefined : this.items[index];
const element = item && item.element;
return { browserEvent, index, element };
}
@@ -379,8 +447,12 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
private onScroll(e: ScrollEvent): void {
try {
this.render(e.scrollTop, e.height);
if (this.supportDynamicHeights) {
this.rerender(e.scrollTop, e.height);
}
} catch (err) {
console.log('Got bad scroll event:', e);
console.error('Got bad scroll event:', e);
throw err;
}
}
@@ -398,7 +470,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
}
private setupDragAndDropScrollInterval(): void {
var viewTop = DOM.getTopLeftOffset(this._domNode).top;
const viewTop = DOM.getTopLeftOffset(this.domNode).top;
if (!this.dragAndDropScrollInterval) {
this.dragAndDropScrollInterval = window.setInterval(() => {
@@ -423,7 +495,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
this.dragAndDropScrollTimeout = window.setTimeout(() => {
this.cancelDragAndDropScrollInterval();
this.dragAndDropScrollTimeout = null;
this.dragAndDropScrollTimeout = -1;
}, 1000);
}
}
@@ -431,7 +503,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
private cancelDragAndDropScrollInterval(): void {
if (this.dragAndDropScrollInterval) {
window.clearInterval(this.dragAndDropScrollInterval);
this.dragAndDropScrollInterval = null;
this.dragAndDropScrollInterval = -1;
}
this.cancelDragAndDropScrollTimeout();
@@ -440,15 +512,16 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
private cancelDragAndDropScrollTimeout(): void {
if (this.dragAndDropScrollTimeout) {
window.clearTimeout(this.dragAndDropScrollTimeout);
this.dragAndDropScrollTimeout = null;
this.dragAndDropScrollTimeout = -1;
}
}
// Util
private getItemIndexFromEventTarget(target: EventTarget): number {
while (target instanceof HTMLElement && target !== this.rowsContainer) {
const element = target as HTMLElement;
private getItemIndexFromEventTarget(target: EventTarget | null): number | undefined {
let element: HTMLElement | null = target as (HTMLElement | null);
while (element instanceof HTMLElement && element !== this.rowsContainer) {
const rawIndex = element.getAttribute('data-index');
if (rawIndex) {
@@ -459,10 +532,10 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
}
}
target = element.parentElement;
element = element.parentElement;
}
return -1;
return undefined;
}
private getRenderRange(renderTop: number, renderHeight: number): IRange {
@@ -472,6 +545,94 @@ 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 {
const previousRenderRange = this.getRenderRange(renderTop, renderHeight);
// Let's remember the second element's position, this helps in scrolling up
// and preserving a linear upwards scroll movement
let secondElementIndex: number | undefined;
let secondElementTopDelta: number | undefined;
if (previousRenderRange.end - previousRenderRange.start > 1) {
secondElementIndex = previousRenderRange.start + 1;
secondElementTopDelta = this.elementTop(secondElementIndex) - renderTop;
}
let heightDiff = 0;
while (true) {
const renderRange = this.getRenderRange(renderTop, renderHeight);
let didChange = false;
for (let i = renderRange.start; i < renderRange.end; i++) {
const diff = this.probeDynamicHeight(i);
if (diff !== 0) {
this.rangeMap.splice(i, 1, [this.items[i]]);
}
heightDiff += diff;
didChange = didChange || diff !== 0;
}
if (!didChange) {
if (heightDiff !== 0) {
this.updateScrollHeight();
}
const unrenderRanges = Range.relativeComplement(previousRenderRange, renderRange);
for (const range of unrenderRanges) {
for (let i = range.start; i < range.end; i++) {
if (this.items[i].row) {
this.removeItemFromDOM(i);
}
}
}
for (let i = renderRange.start; i < renderRange.end; i++) {
if (this.items[i].row) {
this.updateItemInDOM(this.items[i], i);
}
}
if (typeof secondElementIndex === 'number') {
this.scrollTop = this.elementTop(secondElementIndex) - secondElementTopDelta!;
}
this._onDidChangeContentHeight.fire(this.contentHeight);
return;
}
}
}
private probeDynamicHeight(index: number): number {
const item = this.items[index];
if (!item.hasDynamicHeight || item.renderWidth === this.renderWidth) {
return 0;
}
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!);
renderer.renderElement(item.element, index, row.templateData);
item.size = row.domNode!.offsetHeight;
item.renderWidth = this.renderWidth;
this.rowsContainer.removeChild(row.domNode!);
this.cache.release(row);
return item.size - size;
}
private getNextToLastElement(ranges: IRange[]): HTMLElement | null {
const lastRange = ranges[ranges.length - 1];
@@ -500,16 +661,14 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
if (item.row) {
const renderer = this.renderers.get(item.row.templateId);
renderer.disposeTemplate(item.row.templateData);
item.row = null;
}
}
this.items = null;
this.items = [];
}
if (this._domNode && this._domNode.parentElement) {
this._domNode.parentNode.removeChild(this._domNode);
this._domNode = null;
if (this.domNode && this.domNode.parentNode) {
this.domNode.parentNode.removeChild(this.domNode);
}
this.disposables = dispose(this.disposables);

View File

@@ -16,7 +16,7 @@ import { KeyCode } from 'vs/base/common/keyCodes';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { Event, Emitter, EventBufferer, chain, mapEvent, anyEvent } from 'vs/base/common/event';
import { domEvent } from 'vs/base/browser/event';
import { IVirtualDelegate, IRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IListOpenEvent } from './list';
import { IListVirtualDelegate, IListRenderer, IListEvent, IListContextMenuEvent, IListMouseEvent, IListTouchEvent, IListGestureEvent, IIdentityProvider } from './list';
import { ListView, IListViewOptions } from './listView';
import { Color } from 'vs/base/common/color';
import { mixin } from 'vs/base/common/objects';
@@ -25,12 +25,9 @@ import { ISpliceable } from 'vs/base/common/sequence';
import { CombinedSpliceable } from 'vs/base/browser/ui/list/splice';
import { clamp } from 'vs/base/common/numbers';
export interface IIdentityProvider<T> {
(element: T): string;
}
interface ITraitChangeEvent {
indexes: number[];
browserEvent?: UIEvent;
}
type ITraitTemplateData = HTMLElement;
@@ -40,7 +37,7 @@ interface IRenderedContainer {
index: number;
}
class TraitRenderer<T> implements IRenderer<T, ITraitTemplateData>
class TraitRenderer<T> implements IListRenderer<T, ITraitTemplateData>
{
private renderedElements: IRenderedContainer[] = [];
@@ -159,14 +156,14 @@ class Trait<T> implements ISpliceable<boolean>, IDisposable {
* @param indexes Indexes which should have this trait.
* @return The old indexes which had this trait.
*/
set(indexes: number[]): number[] {
set(indexes: number[], browserEvent?: UIEvent): number[] {
const result = this.indexes;
this.indexes = indexes;
const toRender = disjunction(result, indexes);
this.renderer.renderIndexes(toRender);
this._onChange.fire({ indexes });
this._onChange.fire({ indexes, browserEvent });
return result;
}
@@ -179,7 +176,6 @@ class Trait<T> implements ISpliceable<boolean>, IDisposable {
}
dispose() {
this.indexes = null;
this._onChange = dispose(this._onChange);
}
}
@@ -187,7 +183,7 @@ class Trait<T> implements ISpliceable<boolean>, IDisposable {
class FocusTrait<T> extends Trait<T> {
constructor(
private getDomId: IIdentityProvider<number>
private getDomId: (index: number) => string
) {
super('focused');
}
@@ -196,6 +192,12 @@ class FocusTrait<T> extends Trait<T> {
super.renderIndex(index, container);
container.setAttribute('role', 'treeitem');
container.setAttribute('id', this.getDomId(index));
if (this.contains(index)) {
container.setAttribute('aria-selected', 'true');
} else {
container.removeAttribute('aria-selected');
}
}
}
@@ -209,16 +211,16 @@ class TraitSpliceable<T> implements ISpliceable<T> {
constructor(
private trait: Trait<T>,
private view: ListView<T>,
private getId?: IIdentityProvider<T>
private identityProvider?: IIdentityProvider<T>
) { }
splice(start: number, deleteCount: number, elements: T[]): void {
if (!this.getId) {
if (!this.identityProvider) {
return this.trait.splice(start, deleteCount, elements.map(e => false));
}
const pastElementsWithTrait = this.trait.get().map(i => this.getId(this.view.element(i)));
const elementsWithTrait = elements.map(e => pastElementsWithTrait.indexOf(this.getId(e)) > -1);
const pastElementsWithTrait = this.trait.get().map(i => this.identityProvider!.getId(this.view.element(i)).toString());
const elementsWithTrait = elements.map(e => pastElementsWithTrait.indexOf(this.identityProvider!.getId(e).toString()) > -1);
this.trait.splice(start, deleteCount, elementsWithTrait);
}
@@ -262,7 +264,7 @@ class KeyboardController<T> implements IDisposable {
private onEnter(e: StandardKeyboardEvent): void {
e.preventDefault();
e.stopPropagation();
this.list.setSelection(this.list.getFocus());
this.list.setSelection(this.list.getFocus(), e.browserEvent);
if (this.openController.shouldOpen(e.browserEvent)) {
this.list.open(this.list.getFocus(), e.browserEvent);
@@ -272,7 +274,7 @@ class KeyboardController<T> implements IDisposable {
private onUpArrow(e: StandardKeyboardEvent): void {
e.preventDefault();
e.stopPropagation();
this.list.focusPrevious();
this.list.focusPrevious(1, false, e.browserEvent);
this.list.reveal(this.list.getFocus()[0]);
this.view.domNode.focus();
}
@@ -280,7 +282,7 @@ class KeyboardController<T> implements IDisposable {
private onDownArrow(e: StandardKeyboardEvent): void {
e.preventDefault();
e.stopPropagation();
this.list.focusNext();
this.list.focusNext(1, false, e.browserEvent);
this.list.reveal(this.list.getFocus()[0]);
this.view.domNode.focus();
}
@@ -288,7 +290,7 @@ class KeyboardController<T> implements IDisposable {
private onPageUpArrow(e: StandardKeyboardEvent): void {
e.preventDefault();
e.stopPropagation();
this.list.focusPreviousPage();
this.list.focusPreviousPage(e.browserEvent);
this.list.reveal(this.list.getFocus()[0]);
this.view.domNode.focus();
}
@@ -296,7 +298,7 @@ class KeyboardController<T> implements IDisposable {
private onPageDownArrow(e: StandardKeyboardEvent): void {
e.preventDefault();
e.stopPropagation();
this.list.focusNextPage();
this.list.focusNextPage(e.browserEvent);
this.list.reveal(this.list.getFocus()[0]);
this.view.domNode.focus();
}
@@ -304,14 +306,14 @@ class KeyboardController<T> implements IDisposable {
private onCtrlA(e: StandardKeyboardEvent): void {
e.preventDefault();
e.stopPropagation();
this.list.setSelection(range(this.list.length));
this.list.setSelection(range(this.list.length), e.browserEvent);
this.view.domNode.focus();
}
private onEscape(e: StandardKeyboardEvent): void {
e.preventDefault();
e.stopPropagation();
this.list.setSelection([]);
this.list.setSelection([], e.browserEvent);
this.view.domNode.focus();
}
@@ -350,9 +352,14 @@ class DOMFocusController<T> implements IDisposable {
}
const focusedDomElement = this.view.domElement(focus[0]);
if (!focusedDomElement) {
return;
}
const tabIndexElement = focusedDomElement.querySelector('[tabIndex]');
if (!tabIndexElement || !(tabIndexElement instanceof HTMLElement)) {
if (!tabIndexElement || !(tabIndexElement instanceof HTMLElement) || tabIndexElement.tabIndex === -1) {
return;
}
@@ -403,40 +410,8 @@ class MouseController<T> implements IDisposable {
private multipleSelectionSupport: boolean;
private multipleSelectionController: IMultipleSelectionController<T>;
private openController: IOpenController;
private didJustPressContextMenuKey: boolean = false;
private disposables: IDisposable[] = [];
@memoize get onContextMenu(): Event<IListContextMenuEvent<T>> {
const fromKeydown = chain(domEvent(this.view.domNode, 'keydown'))
.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; })
.event as Event<any>;
const fromKeyup = chain(domEvent(this.view.domNode, 'keyup'))
.filter(() => {
const didJustPressContextMenuKey = this.didJustPressContextMenuKey;
this.didJustPressContextMenuKey = false;
return didJustPressContextMenuKey;
})
.filter(() => this.list.getFocus().length > 0)
.map(() => {
const index = this.list.getFocus()[0];
const element = this.view.element(index);
const anchor = this.view.domElement(index);
return { index, element, anchor };
})
.filter(({ anchor }) => !!anchor)
.event;
const fromMouse = chain(this.view.onContextMenu)
.filter(() => !this.didJustPressContextMenuKey)
.map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.clientX + 1, y: browserEvent.clientY } }))
.event;
return anyEvent<IListContextMenuEvent<T>>(fromKeydown, fromKeyup, fromMouse);
}
constructor(
private list: List<T>,
private view: ListView<T>,
@@ -490,13 +465,20 @@ class MouseController<T> implements IDisposable {
const selection = this.list.getSelection();
reference = reference === undefined ? selection[0] : reference;
const focus = e.index;
if (typeof focus === 'undefined') {
this.list.setFocus([], e.browserEvent);
this.list.setSelection([], e.browserEvent);
return;
}
if (this.multipleSelectionSupport && this.isSelectionRangeChangeEvent(e)) {
return this.changeSelection(e, reference);
}
const focus = e.index;
if (selection.every(s => s !== focus)) {
this.list.setFocus([focus]);
this.list.setFocus([focus], e.browserEvent);
}
if (this.multipleSelectionSupport && this.isSelectionChangeEvent(e)) {
@@ -504,7 +486,7 @@ class MouseController<T> implements IDisposable {
}
if (this.options.selectOnMouseDown && !isMouseRightClick(e.browserEvent)) {
this.list.setSelection([focus]);
this.list.setSelection([focus], e.browserEvent);
if (this.openController.shouldOpen(e.browserEvent)) {
this.list.open([focus], e.browserEvent);
@@ -519,7 +501,7 @@ class MouseController<T> implements IDisposable {
if (!this.options.selectOnMouseDown) {
const focus = this.list.getFocus();
this.list.setSelection(focus);
this.list.setSelection(focus, e.browserEvent);
if (this.openController.shouldOpen(e.browserEvent)) {
this.list.open(focus, e.browserEvent);
@@ -533,12 +515,12 @@ class MouseController<T> implements IDisposable {
}
const focus = this.list.getFocus();
this.list.setSelection(focus);
this.list.setSelection(focus, e.browserEvent);
this.list.pin(focus);
}
private changeSelection(e: IListMouseEvent<T> | IListTouchEvent<T>, reference: number | undefined): void {
const focus = e.index;
const focus = e.index!;
if (this.isSelectionRangeChangeEvent(e) && reference !== undefined) {
const min = Math.min(reference, focus);
@@ -552,16 +534,16 @@ class MouseController<T> implements IDisposable {
}
const newSelection = disjunction(rangeSelection, relativeComplement(selection, contiguousRange));
this.list.setSelection(newSelection);
this.list.setSelection(newSelection, e.browserEvent);
} else if (this.isSelectionSingleChangeEvent(e)) {
const selection = this.list.getSelection();
const newSelection = selection.filter(i => i !== focus);
if (selection.length === newSelection.length) {
this.list.setSelection([...newSelection, focus]);
this.list.setSelection([...newSelection, focus], e.browserEvent);
} else {
this.list.setSelection(newSelection);
this.list.setSelection(newSelection, e.browserEvent);
}
}
}
@@ -584,6 +566,21 @@ export interface IStyleController {
style(styles: IListStyles): void;
}
export interface IAccessibilityProvider<T> {
/**
* Given an element in the tree, return the ARIA label that should be associated with the
* item. This helps screen readers to provide a meaningful label for the currently focused
* tree element.
*
* Returning null will not disable ARIA for the element. Instead it is up to the screen reader
* to compute a meaningful label based on the contents of the element in the DOM
*
* See also: https://www.w3.org/TR/wai-aria/states_and_properties#aria-label
*/
getAriaLabel(element: T): string | null;
}
export class DefaultStyleController implements IStyleController {
constructor(private styleElement: HTMLStyleElement, private selectorSuffix?: string) { }
@@ -675,6 +672,7 @@ export interface IListOptions<T> extends IListViewOptions, IListStyles {
multipleSelectionController?: IMultipleSelectionController<T>;
openController?: IOpenController;
styleController?: IStyleController;
accessibilityProvider?: IAccessibilityProvider<T>;
}
export interface IListStyles {
@@ -722,7 +720,7 @@ function getContiguousRangeContaining(range: number[], value: number): number[]
return [];
}
const result = [];
const result: number[] = [];
let i = index - 1;
while (i >= 0 && range[i] === value - (index - i)) {
result.push(range[i--]);
@@ -742,7 +740,7 @@ function getContiguousRangeContaining(range: number[], value: number): number[]
* betweem them (OR).
*/
function disjunction(one: number[], other: number[]): number[] {
const result = [];
const result: number[] = [];
let i = 0, j = 0;
while (i < one.length || j < other.length) {
@@ -770,7 +768,7 @@ function disjunction(one: number[], other: number[]): number[] {
* complement between them (XOR).
*/
function relativeComplement(one: number[], other: number[]): number[] {
const result = [];
const result: number[] = [];
let i = 0, j = 0;
while (i < one.length || j < other.length) {
@@ -794,11 +792,11 @@ function relativeComplement(one: number[], other: number[]): number[] {
const numericSort = (a: number, b: number) => a - b;
class PipelineRenderer<T> implements IRenderer<T, any> {
class PipelineRenderer<T> implements IListRenderer<T, any> {
constructor(
private _templateId: string,
private renderers: IRenderer<T, any>[]
private renderers: IListRenderer<any /* TODO@joao */, any>[]
) { }
get templateId(): string {
@@ -834,6 +832,37 @@ class PipelineRenderer<T> implements IRenderer<T, any> {
}
}
class AccessibiltyRenderer<T> implements IListRenderer<T, HTMLElement> {
templateId: string = 'a18n';
constructor(private accessibilityProvider: IAccessibilityProvider<T>) {
}
renderTemplate(container: HTMLElement): HTMLElement {
return container;
}
renderElement(element: T, index: number, container: HTMLElement): void {
const ariaLabel = this.accessibilityProvider.getAriaLabel(element);
if (ariaLabel) {
container.setAttribute('aria-label', ariaLabel);
} else {
container.removeAttribute('aria-label');
}
}
disposeElement(element: T, index: number, container: HTMLElement): void {
// noop
}
disposeTemplate(templateData: any): void {
// noop
}
}
export class List<T> implements ISpliceable<T>, IDisposable {
private static InstanceCount = 0;
@@ -847,7 +876,6 @@ export class List<T> implements ISpliceable<T>, IDisposable {
protected disposables: IDisposable[];
private styleElement: HTMLStyleElement;
private styleController: IStyleController;
private mouseController: MouseController<T>;
@memoize get onFocusChange(): Event<IListEvent<T>> {
return mapEvent(this.eventBufferer.wrapEvent(this.focus.onChange), e => this.toListEvent(e));
@@ -857,10 +885,8 @@ export class List<T> implements ISpliceable<T>, IDisposable {
return mapEvent(this.eventBufferer.wrapEvent(this.selection.onChange), e => this.toListEvent(e));
}
readonly onContextMenu: Event<IListContextMenuEvent<T>> = Event.None;
private _onOpen = new Emitter<IListOpenEvent<T>>();
readonly onOpen: Event<IListOpenEvent<T>> = this._onOpen.event;
private _onOpen = new Emitter<IListEvent<T>>();
readonly onOpen: Event<IListEvent<T>> = this._onOpen.event;
private _onPin = new Emitter<number[]>();
@memoize get onPin(): Event<IListEvent<T>> {
@@ -869,6 +895,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
get onMouseClick(): Event<IListMouseEvent<T>> { return this.view.onMouseClick; }
get onMouseDblClick(): Event<IListMouseEvent<T>> { return this.view.onMouseDblClick; }
get onMouseMiddleClick(): Event<IListMouseEvent<T>> { return this.view.onMouseMiddleClick; }
get onMouseUp(): Event<IListMouseEvent<T>> { return this.view.onMouseUp; }
get onMouseDown(): Event<IListMouseEvent<T>> { return this.view.onMouseDown; }
get onMouseOver(): Event<IListMouseEvent<T>> { return this.view.onMouseOver; }
@@ -877,6 +904,44 @@ export class List<T> implements ISpliceable<T>, IDisposable {
get onTouchStart(): Event<IListTouchEvent<T>> { return this.view.onTouchStart; }
get onTap(): Event<IListGestureEvent<T>> { return this.view.onTap; }
private didJustPressContextMenuKey: boolean = false;
@memoize get onContextMenu(): Event<IListContextMenuEvent<T>> {
const fromKeydown = chain(domEvent(this.view.domNode, 'keydown'))
.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;
const fromKeyup = chain(domEvent(this.view.domNode, 'keyup'))
.filter(() => {
const didJustPressContextMenuKey = this.didJustPressContextMenuKey;
this.didJustPressContextMenuKey = false;
return didJustPressContextMenuKey;
})
.filter(() => this.getFocus().length > 0)
.map(browserEvent => {
const index = this.getFocus()[0];
const element = this.view.element(index);
const anchor = this.view.domElement(index) || undefined;
return { index, element, anchor, browserEvent };
})
.filter(({ anchor }) => !!anchor)
.event;
const fromMouse = chain(this.view.onContextMenu)
.filter(() => !this.didJustPressContextMenuKey)
.map(({ element, index, browserEvent }) => ({ element, index, anchor: { x: browserEvent.clientX + 1, y: browserEvent.clientY }, browserEvent }))
.event;
return anyEvent<IListContextMenuEvent<T>>(fromKeydown, fromKeyup, fromMouse);
}
get onKeyDown(): Event<KeyboardEvent> { return domEvent(this.view.domNode, 'keydown'); }
get onKeyUp(): Event<KeyboardEvent> { return domEvent(this.view.domNode, 'keyup'); }
get onKeyPress(): Event<KeyboardEvent> { return domEvent(this.view.domNode, 'keypress'); }
@@ -889,8 +954,8 @@ export class List<T> implements ISpliceable<T>, IDisposable {
constructor(
container: HTMLElement,
virtualDelegate: IVirtualDelegate<T>,
renderers: IRenderer<T, any>[],
virtualDelegate: IListVirtualDelegate<T>,
renderers: IListRenderer<any /* TODO@joao */, any>[],
options: IListOptions<T> = DefaultOptions
) {
this.focus = new FocusTrait(i => this.getElementDomId(i));
@@ -898,7 +963,13 @@ export class List<T> implements ISpliceable<T>, IDisposable {
mixin(options, defaultStyles, false);
renderers = renderers.map(r => new PipelineRenderer(r.templateId, [this.focus.renderer, this.selection.renderer, r]));
const baseRenderers: IListRenderer<T, ITraitTemplateData>[] = [this.focus.renderer, this.selection.renderer];
if (options.accessibilityProvider) {
baseRenderers.push(new AccessibiltyRenderer<T>(options.accessibilityProvider));
}
renderers = renderers.map(r => new PipelineRenderer(r.templateId, [...baseRenderers, r]));
this.view = new ListView(container, virtualDelegate, renderers, options);
this.view.domNode.setAttribute('role', 'tree');
@@ -907,10 +978,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
this.styleElement = DOM.createStyleSheet(this.view.domNode);
this.styleController = options.styleController;
if (!this.styleController) {
this.styleController = new DefaultStyleController(this.styleElement, this.idPrefix);
}
this.styleController = options.styleController || new DefaultStyleController(this.styleElement, this.idPrefix);
this.spliceable = new CombinedSpliceable([
new TraitSpliceable(this.focus, this.view, options.identityProvider),
@@ -920,8 +988,8 @@ export class List<T> implements ISpliceable<T>, IDisposable {
this.disposables = [this.focus, this.selection, this.view, this._onDidDispose];
this.onDidFocus = mapEvent(domEvent(this.view.domNode, 'focus', true), () => null);
this.onDidBlur = mapEvent(domEvent(this.view.domNode, 'blur', true), () => null);
this.onDidFocus = mapEvent(domEvent(this.view.domNode, 'focus', true), () => null!);
this.onDidBlur = mapEvent(domEvent(this.view.domNode, 'blur', true), () => null!);
this.disposables.push(new DOMFocusController(this, this.view));
@@ -930,10 +998,8 @@ export class List<T> implements ISpliceable<T>, IDisposable {
this.disposables.push(controller);
}
if (typeof options.mouseSupport !== 'boolean' || options.mouseSupport) {
this.mouseController = new MouseController(this, this.view, options);
this.disposables.push(this.mouseController);
this.onContextMenu = this.mouseController.onContextMenu;
if (typeof options.mouseSupport === 'boolean' ? options.mouseSupport : true) {
this.disposables.push(new MouseController(this, this.view, options));
}
this.onFocusChange(this._onFocusChange, this, this.disposables);
@@ -967,7 +1033,11 @@ export class List<T> implements ISpliceable<T>, IDisposable {
}
get contentHeight(): number {
return this.view.getContentHeight();
return this.view.contentHeight;
}
get onDidChangeContentHeight(): Event<number> {
return this.view.onDidChangeContentHeight;
}
get scrollTop(): number {
@@ -978,6 +1048,14 @@ export class List<T> implements ISpliceable<T>, IDisposable {
this.view.setScrollTop(scrollTop);
}
get scrollHeight(): number {
return this.view.scrollHeight;
}
get renderHeight(): number {
return this.view.renderHeight;
}
domFocus(): void {
this.view.domNode.focus();
}
@@ -986,7 +1064,11 @@ export class List<T> implements ISpliceable<T>, IDisposable {
this.view.layout(height);
}
setSelection(indexes: number[]): void {
layoutWidth(width: number): void {
this.view.layoutWidth(width);
}
setSelection(indexes: number[], browserEvent?: UIEvent): void {
for (const index of indexes) {
if (index < 0 || index >= this.length) {
throw new Error(`Invalid index ${index}`);
@@ -994,24 +1076,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
}
indexes = indexes.sort(numericSort);
this.selection.set(indexes);
}
selectNext(n = 1, loop = false): void {
if (this.length === 0) { return; }
const selection = this.selection.get();
let index = selection.length > 0 ? selection[0] + n : 0;
this.setSelection(loop ? [index % this.length] : [Math.min(index, this.length - 1)]);
}
selectPrevious(n = 1, loop = false): void {
if (this.length === 0) { return; }
const selection = this.selection.get();
let index = selection.length > 0 ? selection[0] - n : 0;
if (loop && index < 0) {
index = this.length + (index % this.length);
}
this.setSelection([Math.max(index, 0)]);
this.selection.set(indexes, browserEvent);
}
getSelection(): number[] {
@@ -1022,7 +1087,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
return this.getSelection().map(i => this.view.element(i));
}
setFocus(indexes: number[]): void {
setFocus(indexes: number[], browserEvent?: UIEvent): void {
for (const index of indexes) {
if (index < 0 || index >= this.length) {
throw new Error(`Invalid index ${index}`);
@@ -1030,44 +1095,44 @@ export class List<T> implements ISpliceable<T>, IDisposable {
}
indexes = indexes.sort(numericSort);
this.focus.set(indexes);
this.focus.set(indexes, browserEvent);
}
focusNext(n = 1, loop = false): void {
focusNext(n = 1, loop = false, browserEvent?: UIEvent): void {
if (this.length === 0) { return; }
const focus = this.focus.get();
let index = focus.length > 0 ? focus[0] + n : 0;
this.setFocus(loop ? [index % this.length] : [Math.min(index, this.length - 1)]);
this.setFocus(loop ? [index % this.length] : [Math.min(index, this.length - 1)], browserEvent);
}
focusPrevious(n = 1, loop = false): void {
focusPrevious(n = 1, loop = false, browserEvent?: UIEvent): void {
if (this.length === 0) { return; }
const focus = this.focus.get();
let index = focus.length > 0 ? focus[0] - n : 0;
if (loop && index < 0) { index = (this.length + (index % this.length)) % this.length; }
this.setFocus([Math.max(index, 0)]);
this.setFocus([Math.max(index, 0)], browserEvent);
}
focusNextPage(): void {
focusNextPage(browserEvent?: UIEvent): void {
let lastPageIndex = this.view.indexAt(this.view.getScrollTop() + this.view.renderHeight);
lastPageIndex = lastPageIndex === 0 ? 0 : lastPageIndex - 1;
const lastPageElement = this.view.element(lastPageIndex);
const currentlyFocusedElement = this.getFocusedElements()[0];
if (currentlyFocusedElement !== lastPageElement) {
this.setFocus([lastPageIndex]);
this.setFocus([lastPageIndex], browserEvent);
} else {
const previousScrollTop = this.view.getScrollTop();
this.view.setScrollTop(previousScrollTop + this.view.renderHeight - this.view.elementHeight(lastPageIndex));
if (this.view.getScrollTop() !== previousScrollTop) {
// Let the scroll event listener run
setTimeout(() => this.focusNextPage(), 0);
setTimeout(() => this.focusNextPage(browserEvent), 0);
}
}
}
focusPreviousPage(): void {
focusPreviousPage(browserEvent?: UIEvent): void {
let firstPageIndex: number;
const scrollTop = this.view.getScrollTop();
@@ -1081,26 +1146,26 @@ export class List<T> implements ISpliceable<T>, IDisposable {
const currentlyFocusedElement = this.getFocusedElements()[0];
if (currentlyFocusedElement !== firstPageElement) {
this.setFocus([firstPageIndex]);
this.setFocus([firstPageIndex], browserEvent);
} else {
const previousScrollTop = scrollTop;
this.view.setScrollTop(scrollTop - this.view.renderHeight);
if (this.view.getScrollTop() !== previousScrollTop) {
// Let the scroll event listener run
setTimeout(() => this.focusPreviousPage(), 0);
setTimeout(() => this.focusPreviousPage(browserEvent), 0);
}
}
}
focusLast(): void {
focusLast(browserEvent?: UIEvent): void {
if (this.length === 0) { return; }
this.setFocus([this.length - 1]);
this.setFocus([this.length - 1], browserEvent);
}
focusFirst(): void {
focusFirst(browserEvent?: UIEvent): void {
if (this.length === 0) { return; }
this.setFocus([0]);
this.setFocus([0], browserEvent);
}
getFocus(): number[] {
@@ -1194,8 +1259,8 @@ export class List<T> implements ISpliceable<T>, IDisposable {
this.styleController.style(styles);
}
private toListEvent({ indexes }: ITraitChangeEvent) {
return { indexes, elements: indexes.map(i => this.view.element(i)) };
private toListEvent({ indexes, browserEvent }: ITraitChangeEvent) {
return { indexes, elements: indexes.map(i => this.view.element(i)), browserEvent };
}
private _onFocusChange(): void {
@@ -1222,5 +1287,9 @@ export class List<T> implements ISpliceable<T>, IDisposable {
dispose(): void {
this._onDidDispose.fire();
this.disposables = dispose(this.disposables);
this._onOpen.dispose();
this._onPin.dispose();
this._onDidDispose.dispose();
}
}

View File

@@ -3,59 +3,17 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IRange, Range } from 'vs/base/common/range';
export interface IItem {
size: number;
}
export interface IRange {
start: number;
end: number;
}
export interface IRangedGroup {
range: IRange;
size: number;
}
/**
* Returns the intersection between two ranges as a range itself.
* Returns `{ start: 0, end: 0 }` if the intersection is empty.
*/
export function intersect(one: IRange, other: IRange): IRange {
if (one.start >= other.end || other.start >= one.end) {
return { start: 0, end: 0 };
}
const start = Math.max(one.start, other.start);
const end = Math.min(one.end, other.end);
if (end - start <= 0) {
return { start: 0, end: 0 };
}
return { start, end };
}
export function isEmpty(range: IRange): boolean {
return range.end - range.start <= 0;
}
export function relativeComplement(one: IRange, other: IRange): IRange[] {
const result: IRange[] = [];
const first = { start: one.start, end: Math.min(other.start, one.end) };
const second = { start: Math.max(other.end, one.start), end: one.end };
if (!isEmpty(first)) {
result.push(first);
}
if (!isEmpty(second)) {
result.push(second);
}
return result;
}
/**
* Returns the intersection between a ranged group and a range.
* Returns `[]` if the intersection is empty.
@@ -72,9 +30,9 @@ export function groupIntersect(range: IRange, groups: IRangedGroup[]): IRangedGr
break;
}
const intersection = intersect(range, r.range);
const intersection = Range.intersect(range, r.range);
if (isEmpty(intersection)) {
if (Range.isEmpty(intersection)) {
continue;
}
@@ -102,7 +60,7 @@ export function shift({ start, end }: IRange, much: number): IRange {
*/
export function consolidate(groups: IRangedGroup[]): IRangedGroup[] {
const result: IRangedGroup[] = [];
let previousGroup: IRangedGroup = null;
let previousGroup: IRangedGroup | null = null;
for (let group of groups) {
const start = group.range.start;
@@ -134,7 +92,7 @@ export class RangeMap {
private groups: IRangedGroup[] = [];
private _size = 0;
splice(index: number, deleteCount: number, ...items: IItem[]): void {
splice(index: number, deleteCount: number, items: IItem[] = []): void {
const diff = items.length - deleteCount;
const before = groupIntersect({ start: 0, end: index }, this.groups);
const after = groupIntersect({ start: index + deleteCount, end: Number.POSITIVE_INFINITY }, this.groups)
@@ -230,6 +188,6 @@ export class RangeMap {
}
dispose() {
this.groups = null;
this.groups = null!; // StrictNullOverride: nulling out ok in dispose
}
}

View File

@@ -3,19 +3,21 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IRenderer } from './list';
import { IListRenderer } from './list';
import { IDisposable } from 'vs/base/common/lifecycle';
import { $, removeClass } from 'vs/base/browser/dom';
export interface IRow {
domNode: HTMLElement;
domNode: HTMLElement | null;
templateId: string;
templateData: any;
}
function removeFromParent(element: HTMLElement): void {
try {
element.parentElement.removeChild(element);
if (element.parentElement) {
element.parentElement.removeChild(element);
}
} catch (e) {
// this will throw if this happens due to a blur event, nasty business
}
@@ -25,7 +27,7 @@ export class RowCache<T> implements IDisposable {
private cache = new Map<string, IRow[]>();
constructor(private renderers: Map<string, IRenderer<T, any>>) { }
constructor(private renderers: Map<string, IListRenderer<T, any>>) { }
/**
* Returns a row either by creating a new one or reusing
@@ -57,8 +59,10 @@ export class RowCache<T> implements IDisposable {
private releaseRow(row: IRow): void {
const { domNode, templateId } = row;
removeClass(domNode, 'scrolling');
removeFromParent(domNode);
if (domNode) {
removeClass(domNode, 'scrolling');
removeFromParent(domNode);
}
const cache = this.getTemplateCache(templateId);
cache.push(row);
@@ -95,6 +99,6 @@ export class RowCache<T> implements IDisposable {
dispose(): void {
this.garbageCollect();
this.cache.clear();
this.renderers = null;
this.renderers = null!; // StrictNullOverride: nulling out ok in dispose
}
}

View File

@@ -3,8 +3,6 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { ISpliceable } from 'vs/base/common/sequence';
export interface ISpreadSpliceable<T> {

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="-2 -2 16 16" enable-background="new -2 -2 16 16"><polygon fill="#424242" points="9,0 4.5,9 3,6 0,6 3,12 6,12 12,0"/></svg>

After

Width:  |  Height:  |  Size: 194 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.icon-canvas-transparent,.icon-vs-out{fill:#f6f6f6;}.icon-canvas-transparent{opacity:0;}.icon-vs-bg{fill:#424242;}</style></defs><title>Ellipsis_bold_16x</title><g id="canvas"><path class="icon-canvas-transparent" d="M16,0V16H0V0Z"/></g><g id="outline" style="display: none;"><path class="icon-vs-out" d="M6,7.5A2.5,2.5,0,1,1,3.5,5,2.5,2.5,0,0,1,6,7.5ZM8.5,5A2.5,2.5,0,1,0,11,7.5,2.5,2.5,0,0,0,8.5,5Zm5,0A2.5,2.5,0,1,0,16,7.5,2.5,2.5,0,0,0,13.5,5Z" style="display: none;"/></g><g id="iconBg"><path class="icon-vs-bg" d="M5,7.5A1.5,1.5,0,1,1,3.5,6,1.5,1.5,0,0,1,5,7.5ZM8.5,6A1.5,1.5,0,1,0,10,7.5,1.5,1.5,0,0,0,8.5,6Zm5,0A1.5,1.5,0,1,0,15,7.5,1.5,1.5,0,0,0,13.5,6Z"/></g></svg>

After

Width:  |  Height:  |  Size: 748 B

View File

@@ -14,34 +14,23 @@
.monaco-menu .monaco-action-bar.vertical .action-item {
padding: 0;
-ms-transform: none;
-webkit-transform: none;
-moz-transform: none;
-o-transform: none;
transform: none;
display: -ms-flexbox;
display: flex;
}
.monaco-menu .monaco-action-bar.vertical .action-item.active {
-ms-transform: none;
-webkit-transform: none;
-moz-transform: none;
-o-transform: none;
transform: none;
}
.monaco-menu .monaco-action-bar.vertical .action-item.focused {
background-color: #E4E4E4;
}
.monaco-menu .monaco-action-bar.vertical .action-menu-item {
-ms-flex: 1 1 auto;
flex: 1 1 auto;
display: -ms-flexbox;
display: flex;
height: 2.6em;
height: 2em;
align-items: center;
position: relative;
}
.monaco-menu .monaco-action-bar.vertical .action-label {
@@ -50,7 +39,7 @@
text-decoration: none;
padding: 0 1em;
background: none;
font-size: inherit;
font-size: 12px;
line-height: 1;
}
@@ -61,10 +50,15 @@
flex: 2 1 auto;
padding: 0 1em;
text-align: right;
font-size: inherit;
font-size: 12px;
line-height: 1;
}
.monaco-menu .monaco-action-bar.vertical .submenu-indicator {
height: 100%;
-webkit-mask: url('submenu.svg') no-repeat 90% 50%/13px 13px;
mask: url('submenu.svg') no-repeat 90% 50%/13px 13px;
}
.monaco-menu .monaco-action-bar.vertical .action-item.disabled .keybinding,
.monaco-menu .monaco-action-bar.vertical .action-item.disabled .submenu-indicator {
@@ -81,6 +75,16 @@
margin: 0;
}
.monaco-menu .monaco-action-bar.vertical .action-item {
position: static;
overflow: visible;
}
.monaco-menu .monaco-action-bar.vertical .action-item .monaco-submenu {
position: absolute;
}
.monaco-menu .monaco-action-bar.vertical .action-label.separator {
padding: 0.5em 0 0 0;
margin-bottom: 0.5em;
@@ -97,19 +101,25 @@
color: inherit;
}
.monaco-menu .monaco-action-bar.vertical .action-label.checked:after {
content: ' \2713';
.monaco-menu .monaco-action-bar.vertical .menu-item-check {
position: absolute;
visibility: hidden;
-webkit-mask: url('check.svg') no-repeat 50% 56%/15px 15px;
mask: url('check.svg') no-repeat 50% 56%/15px 15px;
width: 1em;
height: 100%;
}
.monaco-menu .monaco-action-bar.vertical .action-menu-item.checked .menu-item-check {
visibility: visible;
}
/* Context Menu */
.context-view.monaco-menu-container {
font-family: "Segoe WPC", "Segoe UI", ".SFNSDisplay-Light", "SFUIText-Light", "HelveticaNeue-Light", sans-serif, "Droid Sans Fallback";
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif;
outline: 0;
box-shadow: 0 2px 8px #A8A8A8;
border: none;
color: #646465;
background-color: white;
-webkit-animation: fadeIn 0.083s linear;
-o-animation: fadeIn 0.083s linear;
-moz-animation: fadeIn 0.083s linear;
@@ -127,26 +137,72 @@
border: 1px solid transparent; /* prevents jumping behaviour on hover or focus */
}
/* Dark theme */
.vs-dark .monaco-menu .monaco-action-bar.vertical .action-item.focused {
background-color: #4B4C4D;
}
.vs-dark .context-view.monaco-menu-container {
box-shadow: 0 2px 8px #000;
color: #BBB;
background-color: #2D2F31;
}
/* High Contrast Theming */
.hc-black .context-view.monaco-menu-container {
border: 2px solid #6FC3DF;
color: white;
background-color: #0C141F;
box-shadow: none;
}
.hc-black .monaco-menu .monaco-action-bar.vertical .action-item.focused {
background: none;
border: 1px dotted #f38518;
}
/* Menubar styles */
.menubar {
display: flex;
flex-shrink: 1;
box-sizing: border-box;
height: 30px;
-webkit-app-region: no-drag;
overflow: hidden;
flex-wrap: wrap;
}
.fullscreen .menubar {
margin: 0px;
padding: 0px 5px;
}
.menubar > .menubar-menu-button {
align-items: center;
box-sizing: border-box;
padding: 0px 8px;
cursor: default;
-webkit-app-region: no-drag;
zoom: 1;
white-space: nowrap;
outline: 0;
}
.menubar .menubar-menu-items-holder {
position: absolute;
left: 0px;
opacity: 1;
z-index: 2000;
}
.menubar .menubar-menu-items-holder.monaco-menu-container {
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", "Ubuntu", "Droid Sans", sans-serif;
outline: 0;
border: none;
}
.menubar .menubar-menu-items-holder.monaco-menu-container :focus {
outline: 0;
}
.menubar .toolbar-toggle-more {
background-position: center;
background-repeat: no-repeat;
background-size: 14px;
width: 20px;
height: 100%;
}
.menubar .toolbar-toggle-more {
display: inline-block;
padding: 0;
-webkit-mask: url('ellipsis.svg') no-repeat 50% 55%/14px 14px;
mask: url('ellipsis.svg') no-repeat 50% 55%/14px 14px;
}

View File

@@ -3,19 +3,24 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./menu';
import * as nls from 'vs/nls';
import { IDisposable } from 'vs/base/common/lifecycle';
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 } from 'vs/base/common/keyCodes';
import { Event } from 'vs/base/common/event';
import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor } from 'vs/base/browser/dom';
import { ResolvedKeybinding, KeyCode, KeyCodeUtils } 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 { $, Builder } from 'vs/base/browser/builder';
import { RunOnceScheduler } from 'vs/base/common/async';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Color } from 'vs/base/common/color';
import { DomScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { Event, Emitter } from 'vs/base/common/event';
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
export const MENU_MNEMONIC_REGEX: RegExp = /\(&{1,2}(.)\)|&{1,2}(.)/;
export const MENU_ESCAPED_MNEMONIC_REGEX: RegExp = /(?:&amp;){1,2}(.)/;
export interface IMenuOptions {
context?: any;
@@ -23,8 +28,20 @@ export interface IMenuOptions {
actionRunner?: IActionRunner;
getKeyBinding?: (action: IAction) => ResolvedKeybinding;
ariaLabel?: string;
enableMnemonics?: boolean;
anchorAlignment?: AnchorAlignment;
}
export interface IMenuStyles {
shadowColor?: Color;
borderColor?: Color;
foregroundColor?: Color;
backgroundColor?: Color;
selectionForegroundColor?: Color;
selectionBackgroundColor?: Color;
selectionBorderColor?: Color;
separatorColor?: Color;
}
export class SubmenuAction extends Action {
constructor(label: string, public entries: (SubmenuAction | IAction)[], cssClass?: string) {
@@ -37,43 +54,225 @@ interface ISubMenuData {
submenu?: Menu;
}
export class Menu {
export class Menu extends ActionBar {
private mnemonics: Map<KeyCode, Array<MenuActionItem>>;
private menuDisposables: IDisposable[];
private scrollableElement: DomScrollableElement;
private menuElement: HTMLElement;
private actionBar: ActionBar;
private listener: IDisposable;
private readonly _onScroll: Emitter<void>;
constructor(container: HTMLElement, actions: IAction[], options: IMenuOptions = {}) {
addClass(container, 'monaco-menu-container');
container.setAttribute('role', 'presentation');
const menuElement = document.createElement('div');
addClass(menuElement, 'monaco-menu');
menuElement.setAttribute('role', 'presentation');
let menuContainer = document.createElement('div');
addClass(menuContainer, 'monaco-menu');
menuContainer.setAttribute('role', 'presentation');
container.appendChild(menuContainer);
super(menuElement, {
orientation: ActionsOrientation.VERTICAL,
actionItemProvider: action => this.doGetActionItem(action, options, parentData),
context: options.context,
actionRunner: options.actionRunner,
ariaLabel: options.ariaLabel,
triggerKeys: { keys: [KeyCode.Enter], keyDown: true }
});
this.menuElement = menuElement;
this._onScroll = this._register(new Emitter<void>());
this.actionsList.setAttribute('role', 'menu');
this.actionsList.tabIndex = 0;
this.menuDisposables = [];
if (options.enableMnemonics) {
this.menuDisposables.push(addDisposableListener(menuElement, EventType.KEY_DOWN, (e) => {
const key = KeyCodeUtils.fromString(e.key);
if (this.mnemonics.has(key)) {
EventHelper.stop(e, true);
const actions = this.mnemonics.get(key);
if (actions.length === 1) {
if (actions[0] instanceof SubmenuActionItem) {
this.focusItemByElement(actions[0].container);
}
actions[0].onClick(event);
}
if (actions.length > 1) {
const action = actions.shift();
this.focusItemByElement(action.container);
actions.push(action);
this.mnemonics.set(key, actions);
}
}
}));
}
this._register(addDisposableListener(this.domNode, EventType.MOUSE_OUT, e => {
let relatedTarget = e.relatedTarget as HTMLElement;
if (!isAncestor(relatedTarget, this.domNode)) {
this.focusedItem = undefined;
this.updateFocus();
e.stopPropagation();
}
}));
this._register(addDisposableListener(this.actionsList, EventType.MOUSE_OVER, e => {
let target = e.target as HTMLElement;
if (!target || !isAncestor(target, this.actionsList) || target === this.actionsList) {
return;
}
while (target.parentElement !== this.actionsList) {
target = target.parentElement;
}
if (hasClass(target, 'action-item')) {
const lastFocusedItem = this.focusedItem;
this.setFocusedItem(target);
if (lastFocusedItem !== this.focusedItem) {
this.updateFocus();
}
}
}));
let parentData: ISubMenuData = {
parent: this
};
this.actionBar = new ActionBar(menuContainer, {
orientation: ActionsOrientation.VERTICAL,
actionItemProvider: action => this.doGetActionItem(action, options, parentData),
context: options.context,
actionRunner: options.actionRunner,
isMenu: true,
ariaLabel: options.ariaLabel
});
this.mnemonics = new Map<KeyCode, Array<MenuActionItem>>();
this.actionBar.push(actions, { icon: true, label: true, isMenu: true });
this.push(actions, { icon: true, label: true, isMenu: true });
// Scroll Logic
this.scrollableElement = this._register(new DomScrollableElement(menuElement, {
alwaysConsumeMouseWheel: true,
horizontal: ScrollbarVisibility.Hidden,
vertical: ScrollbarVisibility.Visible,
verticalScrollbarSize: 7,
handleMouseWheel: true,
useShadows: true
}));
const scrollElement = this.scrollableElement.getDomNode();
scrollElement.style.position = null;
menuElement.style.maxHeight = `${Math.max(10, window.innerHeight - container.getBoundingClientRect().top - 30)}px`;
this.scrollableElement.onScroll(() => {
this._onScroll.fire();
}, this, this.menuDisposables);
this._register(addDisposableListener(this.menuElement, EventType.SCROLL, (e) => {
this.scrollableElement.scanDomNode();
}));
container.appendChild(this.scrollableElement.getDomNode());
this.scrollableElement.scanDomNode();
this.items.filter(item => !(item instanceof MenuSeparatorActionItem)).forEach((item: MenuActionItem, index: number, array: any[]) => {
item.updatePositionInSet(index + 1, array.length);
});
}
style(style: IMenuStyles): void {
const container = this.getContainer();
const fgColor = style.foregroundColor ? `${style.foregroundColor}` : null;
const bgColor = style.backgroundColor ? `${style.backgroundColor}` : null;
const border = style.borderColor ? `2px solid ${style.borderColor}` : null;
const shadow = style.shadowColor ? `0 2px 4px ${style.shadowColor}` : null;
container.style.border = border;
this.domNode.style.color = fgColor;
this.domNode.style.backgroundColor = bgColor;
container.style.boxShadow = shadow;
if (this.items) {
this.items.forEach(item => {
if (item instanceof MenuActionItem || item instanceof MenuSeparatorActionItem) {
item.style(style);
}
});
}
}
getContainer(): HTMLElement {
return this.scrollableElement.getDomNode();
}
get onScroll(): Event<void> {
return this._onScroll.event;
}
get scrollOffset(): number {
return this.menuElement.scrollTop;
}
trigger(index: number): void {
if (index <= this.items.length && index >= 0) {
const item = this.items[index];
if (item instanceof SubmenuActionItem) {
super.focus(index);
item.open(true);
} else if (item instanceof MenuActionItem) {
super.run(item._action, item._context);
} else {
return;
}
}
}
private focusItemByElement(element: HTMLElement) {
const lastFocusedItem = this.focusedItem;
this.setFocusedItem(element);
if (lastFocusedItem !== this.focusedItem) {
this.updateFocus();
}
}
private setFocusedItem(element: HTMLElement): void {
for (let i = 0; i < this.actionsList.children.length; i++) {
let elem = this.actionsList.children[i];
if (element === elem) {
this.focusedItem = i;
break;
}
}
}
private doGetActionItem(action: IAction, options: IMenuOptions, parentData: ISubMenuData): BaseActionItem {
if (action instanceof Separator) {
return new ActionItem(options.context, action, { icon: true });
return new MenuSeparatorActionItem(options.context, action, { icon: true });
} else if (action instanceof SubmenuAction) {
return new SubmenuActionItem(action, action.entries, parentData, options);
const menuActionItem = new SubmenuActionItem(action, action.entries, parentData, options);
if (options.enableMnemonics) {
const mnemonic = menuActionItem.getMnemonic();
if (mnemonic && menuActionItem.isEnabled()) {
let actionItems: MenuActionItem[] = [];
if (this.mnemonics.has(mnemonic)) {
actionItems = this.mnemonics.get(mnemonic);
}
actionItems.push(menuActionItem);
this.mnemonics.set(mnemonic, actionItems);
}
}
return menuActionItem;
} else {
const menuItemOptions: IActionItemOptions = {};
const menuItemOptions: IMenuItemOptions = { enableMnemonics: options.enableMnemonics };
if (options.getKeyBinding) {
const keybinding = options.getKeyBinding(action);
if (keybinding) {
@@ -81,46 +280,45 @@ export class Menu {
}
}
return new MenuActionItem(options.context, action, menuItemOptions);
}
}
const menuActionItem = new MenuActionItem(options.context, action, menuItemOptions);
public get onDidCancel(): Event<void> {
return this.actionBar.onDidCancel;
}
if (options.enableMnemonics) {
const mnemonic = menuActionItem.getMnemonic();
if (mnemonic && menuActionItem.isEnabled()) {
let actionItems: MenuActionItem[] = [];
if (this.mnemonics.has(mnemonic)) {
actionItems = this.mnemonics.get(mnemonic);
}
public get onDidBlur(): Event<void> {
return this.actionBar.onDidBlur;
}
actionItems.push(menuActionItem);
public focus(selectFirst = true) {
if (this.actionBar) {
this.actionBar.focus(selectFirst);
}
}
this.mnemonics.set(mnemonic, actionItems);
}
}
public dispose() {
if (this.actionBar) {
this.actionBar.dispose();
this.actionBar = null;
}
if (this.listener) {
this.listener.dispose();
this.listener = null;
return menuActionItem;
}
}
}
interface IMenuItemOptions extends IActionItemOptions {
enableMnemonics?: boolean;
}
class MenuActionItem extends BaseActionItem {
static MNEMONIC_REGEX: RegExp = /&&(.)/g;
protected $e: Builder;
protected $label: Builder;
protected options: IActionItemOptions;
public container: HTMLElement;
protected options: IMenuItemOptions;
protected item: HTMLElement;
private label: HTMLElement;
private check: HTMLElement;
private mnemonic: KeyCode;
private cssClass: string;
protected menuStyle: IMenuStyles;
constructor(ctx: any, action: IAction, options: IActionItemOptions = {}) {
constructor(ctx: any, action: IAction, options: IMenuItemOptions = {}) {
options.isMenu = true;
super(action, action, options);
@@ -128,62 +326,97 @@ class MenuActionItem extends BaseActionItem {
this.options.icon = options.icon !== undefined ? options.icon : false;
this.options.label = options.label !== undefined ? options.label : true;
this.cssClass = '';
// Set mnemonic
if (this.options.label && options.enableMnemonics) {
let label = this.getAction().label;
if (label) {
let matches = MENU_MNEMONIC_REGEX.exec(label);
if (matches) {
this.mnemonic = KeyCodeUtils.fromString((!!matches[1] ? matches[1] : matches[2]).toLocaleLowerCase());
}
}
}
}
public render(container: HTMLElement): void {
render(container: HTMLElement): void {
super.render(container);
this.$e = $('a.action-menu-item').appendTo(this.builder);
this.container = container;
this.item = append(this.element, $('a.action-menu-item'));
if (this._action.id === Separator.ID) {
// A separator is a presentation item
this.$e.attr({ role: 'presentation' });
this.item.setAttribute('role', 'presentation');
} else {
this.$e.attr({ role: 'menuitem' });
this.item.setAttribute('role', 'menuitem');
if (this.mnemonic) {
this.item.setAttribute('aria-keyshortcuts', `${this.mnemonic}`);
}
}
this.$label = $('span.action-label').appendTo(this.$e);
this.check = append(this.item, $('span.menu-item-check'));
this.check.setAttribute('role', 'none');
this.label = append(this.item, $('span.action-label'));
if (this.options.label && this.options.keybinding) {
$('span.keybinding').text(this.options.keybinding).appendTo(this.$e);
append(this.item, $('span.keybinding')).textContent = this.options.keybinding;
}
this._updateClass();
this._updateLabel();
this._updateTooltip();
this._updateEnabled();
this._updateChecked();
this._register(addDisposableListener(this.element, EventType.MOUSE_UP, e => {
EventHelper.stop(e, true);
this.onClick(e);
}));
this.updateClass();
this.updateLabel();
this.updateTooltip();
this.updateEnabled();
this.updateChecked();
}
public focus(): void {
blur(): void {
super.blur();
this.applyStyle();
}
focus(): void {
super.focus();
this.$e.domFocus();
this.item.focus();
this.applyStyle();
}
public _updateLabel(): void {
updatePositionInSet(pos: number, setSize: number): void {
this.item.setAttribute('aria-posinset', `${pos}`);
this.item.setAttribute('aria-setsize', `${setSize}`);
}
updateLabel(): void {
if (this.options.label) {
let label = this.getAction().label;
if (label) {
let matches = MenuActionItem.MNEMONIC_REGEX.exec(label);
if (matches && matches.length === 2) {
let mnemonic = matches[1];
let ariaLabel = label.replace(MenuActionItem.MNEMONIC_REGEX, mnemonic);
this.$e.getHTMLElement().accessKey = mnemonic.toLocaleLowerCase();
this.$label.attr('aria-label', ariaLabel);
} else {
this.$label.attr('aria-label', label);
const cleanLabel = cleanMnemonic(label);
if (!this.options.enableMnemonics) {
label = cleanLabel;
}
label = label.replace(MenuActionItem.MNEMONIC_REGEX, '$1\u0332');
this.label.setAttribute('aria-label', cleanLabel);
const matches = MENU_MNEMONIC_REGEX.exec(label);
if (matches) {
label = strings.escape(label).replace(MENU_ESCAPED_MNEMONIC_REGEX, '<u aria-hidden="true">$1</u>');
this.item.setAttribute('aria-keyshortcuts', (!!matches[1] ? matches[1] : matches[2]).toLocaleLowerCase());
}
}
this.$label.text(label);
this.label.innerHTML = label.trim();
}
}
public _updateTooltip(): void {
let title: string = null;
updateTooltip(): void {
let title: string | null = null;
if (this.getAction().tooltip) {
title = this.getAction().tooltip;
@@ -197,50 +430,81 @@ class MenuActionItem extends BaseActionItem {
}
if (title) {
this.$e.attr({ title: title });
this.item.title = title;
}
}
public _updateClass(): void {
updateClass(): void {
if (this.cssClass) {
this.$e.removeClass(this.cssClass);
removeClasses(this.item, this.cssClass);
}
if (this.options.icon) {
this.cssClass = this.getAction().class;
this.$label.addClass('icon');
addClass(this.label, 'icon');
if (this.cssClass) {
this.$label.addClass(this.cssClass);
addClasses(this.label, this.cssClass);
}
this._updateEnabled();
this.updateEnabled();
} else {
this.$label.removeClass('icon');
removeClass(this.label, 'icon');
}
}
public _updateEnabled(): void {
updateEnabled(): void {
if (this.getAction().enabled) {
this.builder.removeClass('disabled');
this.$e.removeClass('disabled');
this.$e.attr({ tabindex: 0 });
removeClass(this.element, 'disabled');
removeClass(this.item, 'disabled');
this.item.tabIndex = 0;
} else {
this.builder.addClass('disabled');
this.$e.addClass('disabled');
removeTabIndexAndUpdateFocus(this.$e.getHTMLElement());
addClass(this.element, 'disabled');
addClass(this.item, 'disabled');
removeTabIndexAndUpdateFocus(this.item);
}
}
public _updateChecked(): void {
updateChecked(): void {
if (this.getAction().checked) {
this.$label.addClass('checked');
addClass(this.item, 'checked');
this.item.setAttribute('role', 'menuitemcheckbox');
this.item.setAttribute('aria-checked', 'true');
} else {
this.$label.removeClass('checked');
removeClass(this.item, 'checked');
this.item.setAttribute('role', 'menuitem');
this.item.setAttribute('aria-checked', 'false');
}
}
getMnemonic(): KeyCode {
return this.mnemonic;
}
protected applyStyle(): void {
if (!this.menuStyle) {
return;
}
const isSelected = hasClass(this.element, 'focused');
const fgColor = isSelected && this.menuStyle.selectionForegroundColor ? this.menuStyle.selectionForegroundColor : this.menuStyle.foregroundColor;
const bgColor = isSelected && this.menuStyle.selectionBackgroundColor ? this.menuStyle.selectionBackgroundColor : this.menuStyle.backgroundColor;
const border = isSelected && this.menuStyle.selectionBorderColor ? `1px solid ${this.menuStyle.selectionBorderColor}` : null;
this.item.style.color = fgColor ? `${fgColor}` : null;
this.check.style.backgroundColor = fgColor ? `${fgColor}` : null;
this.item.style.backgroundColor = bgColor ? `${bgColor}` : null;
this.container.style.border = border;
}
style(style: IMenuStyles): void {
this.menuStyle = style;
this.applyStyle();
}
}
class SubmenuActionItem extends MenuActionItem {
private mysubmenu: Menu;
private submenuContainer: Builder;
private submenuContainer: HTMLElement;
private submenuIndicator: HTMLElement;
private submenuDisposables: IDisposable[] = [];
private mouseOver: boolean;
private showScheduler: RunOnceScheduler;
private hideScheduler: RunOnceScheduler;
@@ -251,7 +515,7 @@ class SubmenuActionItem extends MenuActionItem {
private parentData: ISubMenuData,
private submenuOptions?: IMenuOptions
) {
super(action, action, { label: true, isMenu: true });
super(action, action, submenuOptions);
this.showScheduler = new RunOnceScheduler(() => {
if (this.mouseOver) {
@@ -261,84 +525,110 @@ class SubmenuActionItem extends MenuActionItem {
}, 250);
this.hideScheduler = new RunOnceScheduler(() => {
if ((!isAncestor(document.activeElement, this.builder.getHTMLElement()) && this.parentData.submenu === this.mysubmenu)) {
if ((!isAncestor(document.activeElement, this.element) && this.parentData.submenu === this.mysubmenu)) {
this.parentData.parent.focus(false);
this.cleanupExistingSubmenu(true);
}
}, 750);
}
public render(container: HTMLElement): void {
render(container: HTMLElement): void {
super.render(container);
this.$e.addClass('monaco-submenu-item');
this.$e.attr('aria-haspopup', 'true');
$('span.submenu-indicator').text('\u25B6').appendTo(this.$e);
addClass(this.item, 'monaco-submenu-item');
this.item.setAttribute('aria-haspopup', 'true');
$(this.builder).on(EventType.KEY_UP, (e) => {
let event = new StandardKeyboardEvent(e as KeyboardEvent);
if (event.equals(KeyCode.RightArrow)) {
this.submenuIndicator = append(this.item, $('span.submenu-indicator'));
this.submenuIndicator.setAttribute('aria-hidden', 'true');
this._register(addDisposableListener(this.element, EventType.KEY_UP, e => {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Enter)) {
EventHelper.stop(e, true);
this.createSubmenu(true);
}
});
}));
$(this.builder).on(EventType.KEY_DOWN, (e) => {
let event = new StandardKeyboardEvent(e as KeyboardEvent);
if (event.equals(KeyCode.RightArrow)) {
this._register(addDisposableListener(this.element, EventType.KEY_DOWN, e => {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.RightArrow) || event.equals(KeyCode.Enter)) {
EventHelper.stop(e, true);
}
});
}));
$(this.builder).on(EventType.MOUSE_OVER, (e) => {
this._register(addDisposableListener(this.element, EventType.MOUSE_OVER, e => {
if (!this.mouseOver) {
this.mouseOver = true;
this.showScheduler.schedule();
}
});
}));
$(this.builder).on(EventType.MOUSE_LEAVE, (e) => {
this._register(addDisposableListener(this.element, EventType.MOUSE_LEAVE, e => {
this.mouseOver = false;
});
}));
$(this.builder).on(EventType.FOCUS_OUT, (e) => {
if (!isAncestor(document.activeElement, this.builder.getHTMLElement())) {
this._register(addDisposableListener(this.element, EventType.FOCUS_OUT, e => {
if (!isAncestor(document.activeElement, this.element)) {
this.hideScheduler.schedule();
}
});
}));
this._register(this.parentData.parent.onScroll(() => {
this.parentData.parent.focus(false);
this.cleanupExistingSubmenu(false);
}));
}
public onClick(e: EventLike) {
open(selectFirst?: boolean): void {
this.cleanupExistingSubmenu(false);
this.createSubmenu(selectFirst);
}
onClick(e: EventLike): void {
// stop clicking from trying to run an action
EventHelper.stop(e, true);
this.cleanupExistingSubmenu(false);
this.createSubmenu(false);
}
private cleanupExistingSubmenu(force: boolean) {
private cleanupExistingSubmenu(force: boolean): void {
if (this.parentData.submenu && (force || (this.parentData.submenu !== this.mysubmenu))) {
this.parentData.submenu.dispose();
this.parentData.submenu = null;
if (this.submenuContainer) {
this.submenuContainer.dispose();
this.submenuDisposables = dispose(this.submenuDisposables);
this.submenuContainer = null;
}
}
}
private createSubmenu(selectFirstItem = true) {
private createSubmenu(selectFirstItem = true): void {
if (!this.parentData.submenu) {
this.submenuContainer = $(this.builder).div({ class: 'monaco-submenu menubar-menu-items-holder context-view' });
this.submenuContainer = append(this.element, $('div.monaco-submenu'));
addClasses(this.submenuContainer, 'menubar-menu-items-holder', 'context-view');
$(this.submenuContainer).style({
'left': `${$(this.builder).getClientArea().width}px`
});
this.parentData.submenu = new Menu(this.submenuContainer, this.submenuActions, this.submenuOptions);
if (this.menuStyle) {
this.parentData.submenu.style(this.menuStyle);
}
$(this.submenuContainer).on(EventType.KEY_UP, (e) => {
let event = new StandardKeyboardEvent(e as KeyboardEvent);
const boundingRect = this.element.getBoundingClientRect();
const childBoundingRect = this.submenuContainer.getBoundingClientRect();
if (window.innerWidth <= boundingRect.right + childBoundingRect.width) {
this.submenuContainer.style.left = '10px';
this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset + boundingRect.height}px`;
} else {
this.submenuContainer.style.left = `${this.element.offsetWidth}px`;
this.submenuContainer.style.top = `${this.element.offsetTop - this.parentData.parent.scrollOffset}px`;
}
this.submenuDisposables.push(addDisposableListener(this.submenuContainer, EventType.KEY_UP, e => {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.LeftArrow)) {
EventHelper.stop(e, true);
@@ -346,20 +636,28 @@ class SubmenuActionItem extends MenuActionItem {
this.parentData.submenu.dispose();
this.parentData.submenu = null;
this.submenuContainer.dispose();
this.submenuDisposables = dispose(this.submenuDisposables);
this.submenuContainer = null;
}
});
}));
$(this.submenuContainer).on(EventType.KEY_DOWN, (e) => {
let event = new StandardKeyboardEvent(e as KeyboardEvent);
this.submenuDisposables.push(addDisposableListener(this.submenuContainer, EventType.KEY_DOWN, e => {
let event = new StandardKeyboardEvent(e);
if (event.equals(KeyCode.LeftArrow)) {
EventHelper.stop(e, true);
}
});
}));
this.parentData.submenu = new Menu(this.submenuContainer.getHTMLElement(), this.submenuActions, this.submenuOptions);
this.submenuDisposables.push(this.parentData.submenu.onDidCancel(() => {
this.parentData.parent.focus();
this.parentData.submenu.dispose();
this.parentData.submenu = null;
this.submenuDisposables = dispose(this.submenuDisposables);
this.submenuContainer = null;
}));
this.parentData.submenu.focus(selectFirstItem);
this.mysubmenu = this.parentData.submenu;
@@ -368,7 +666,24 @@ class SubmenuActionItem extends MenuActionItem {
}
}
public dispose() {
protected applyStyle(): void {
super.applyStyle();
if (!this.menuStyle) {
return;
}
const isSelected = hasClass(this.element, 'focused');
const fgColor = isSelected && this.menuStyle.selectionForegroundColor ? this.menuStyle.selectionForegroundColor : this.menuStyle.foregroundColor;
this.submenuIndicator.style.backgroundColor = fgColor ? `${fgColor}` : null;
if (this.parentData.submenu) {
this.parentData.submenu.style(this.menuStyle);
}
}
dispose(): void {
super.dispose();
this.hideScheduler.dispose();
@@ -379,8 +694,27 @@ class SubmenuActionItem extends MenuActionItem {
}
if (this.submenuContainer) {
this.submenuContainer.dispose();
this.submenuDisposables = dispose(this.submenuDisposables);
this.submenuContainer = null;
}
}
}
class MenuSeparatorActionItem extends ActionItem {
style(style: IMenuStyles): void {
this.label.style.borderBottomColor = style.separatorColor ? `${style.separatorColor}` : null;
}
}
export function cleanMnemonic(label: string): string {
const regex = MENU_MNEMONIC_REGEX;
const matches = regex.exec(label);
if (!matches) {
return label;
}
const mnemonicInText = matches[0].charAt(0) === '&';
return label.replace(regex, mnemonicInText ? '$2' : '').trim();
}

View File

@@ -0,0 +1,974 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as browser from 'vs/base/browser/browser';
import * as DOM from 'vs/base/browser/dom';
import * as strings from 'vs/base/common/strings';
import * as nls from 'vs/nls';
import { domEvent } from 'vs/base/browser/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch';
import { cleanMnemonic, IMenuOptions, Menu, MENU_ESCAPED_MNEMONIC_REGEX, MENU_MNEMONIC_REGEX, SubmenuAction, IMenuStyles } from 'vs/base/browser/ui/menu/menu';
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 { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
const $ = DOM.$;
export interface IMenuBarOptions {
enableMnemonics?: boolean;
visibility?: string;
getKeybinding?: (action: IAction) => ResolvedKeybinding;
}
export interface MenuBarMenu {
actions: IAction[];
label: string;
}
enum MenubarState {
HIDDEN,
VISIBLE,
FOCUSED,
OPEN
}
export class MenuBar extends Disposable {
static readonly OVERFLOW_INDEX: number = -1;
private menuCache: {
buttonElement: HTMLElement;
titleElement: HTMLElement;
label: string;
actions?: IAction[];
}[];
private overflowMenu: {
buttonElement: HTMLElement;
titleElement: HTMLElement;
label: string;
actions?: IAction[];
};
private focusedMenu: {
index: number;
holder?: HTMLElement;
widget?: Menu;
};
private focusToReturn: HTMLElement;
private menuUpdater: RunOnceScheduler;
// Input-related
private _mnemonicsInUse: boolean;
private openedViaKeyboard: boolean;
private awaitingAltRelease: boolean;
private ignoreNextMouseUp: boolean;
private mnemonics: Map<KeyCode, number>;
private updatePending: boolean;
private _focusState: MenubarState;
private actionRunner: IActionRunner;
private _onVisibilityChange: Emitter<boolean>;
private _onFocusStateChange: Emitter<boolean>;
private numMenusShown: number;
private menuStyle: IMenuStyles;
private overflowLayoutScheduled: IDisposable;
constructor(private container: HTMLElement, private options: IMenuBarOptions = {}) {
super();
this.container.attributes['role'] = 'menubar';
this.menuCache = [];
this.mnemonics = new Map<KeyCode, number>();
this._focusState = MenubarState.VISIBLE;
this._onVisibilityChange = this._register(new Emitter<boolean>());
this._onFocusStateChange = this._register(new Emitter<boolean>());
this.createOverflowMenu();
this.menuUpdater = this._register(new RunOnceScheduler(() => this.update(), 200));
this.actionRunner = this._register(new ActionRunner());
this._register(this.actionRunner.onDidBeforeRun(() => {
this.setUnfocusedState();
}));
this._register(ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this));
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;
if (event.equals(KeyCode.LeftArrow)) {
this.focusPrevious();
} else if (event.equals(KeyCode.RightArrow)) {
this.focusNext();
} else if (event.equals(KeyCode.Escape) && this.isFocused && !this.isOpen) {
this.setUnfocusedState();
} else if (!this.isOpen && !event.ctrlKey && this.options.enableMnemonics && this.mnemonicsInUse && this.mnemonics.has(key)) {
const menuIndex = this.mnemonics.get(key);
this.onMenuTriggered(menuIndex, false);
} else {
eventHandled = false;
}
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
}
}));
this._register(DOM.addDisposableListener(window, DOM.EventType.MOUSE_DOWN, () => {
// This mouse event is outside the menubar so it counts as a focus out
if (this.isFocused) {
this.setUnfocusedState();
}
}));
this._register(DOM.addDisposableListener(this.container, DOM.EventType.FOCUS_IN, (e) => {
let event = e as FocusEvent;
if (event.relatedTarget) {
if (!this.container.contains(event.relatedTarget as HTMLElement)) {
this.focusToReturn = event.relatedTarget as HTMLElement;
}
}
}));
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 = null;
this.setUnfocusedState();
}
}
}));
this._register(DOM.addDisposableListener(window, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
if (!this.options.enableMnemonics || !e.altKey || e.ctrlKey || e.defaultPrevented) {
return;
}
const key = KeyCodeUtils.fromString(e.key);
if (!this.mnemonics.has(key)) {
return;
}
this.mnemonicsInUse = true;
this.updateMnemonicVisibility(true);
const menuIndex = this.mnemonics.get(key);
this.onMenuTriggered(menuIndex, false);
}));
this.setUnfocusedState();
}
push(arg: MenuBarMenu | MenuBarMenu[]): void {
const menus: MenuBarMenu[] = !Array.isArray(arg) ? [arg] : arg;
menus.forEach((menuBarMenu) => {
const menuIndex = this.menuCache.length;
const cleanMenuLabel = cleanMnemonic(menuBarMenu.label);
const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': -1, 'aria-label': cleanMenuLabel, 'aria-haspopup': true });
const titleElement = $('div.menubar-menu-title', { 'role': 'none', 'aria-hidden': true });
buttonElement.appendChild(titleElement);
this.container.insertBefore(buttonElement, this.overflowMenu.buttonElement);
let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(menuBarMenu.label);
// Register mnemonics
if (mnemonicMatches) {
let mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[2];
this.registerMnemonic(this.menuCache.length, mnemonic);
}
this.updateLabels(titleElement, buttonElement, menuBarMenu.label);
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.KEY_UP, (e) => {
let event = new StandardKeyboardEvent(e as KeyboardEvent);
let eventHandled = true;
if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) {
this.focusedMenu = { index: menuIndex };
this.openedViaKeyboard = true;
this.focusState = MenubarState.OPEN;
} else {
eventHandled = false;
}
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
}
}));
Gesture.addTarget(buttonElement);
this._register(DOM.addDisposableListener(buttonElement, EventType.Tap, (e: GestureEvent) => {
// Ignore this touch if the menu is touched
if (this.isOpen && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) {
return;
}
this.ignoreNextMouseUp = false;
this.onMenuTriggered(menuIndex, true);
e.preventDefault();
e.stopPropagation();
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e) => {
if (!this.isOpen) {
// Open the menu with mouse down and ignore the following mouse up event
this.ignoreNextMouseUp = true;
this.onMenuTriggered(menuIndex, true);
} else {
this.ignoreNextMouseUp = false;
}
e.preventDefault();
e.stopPropagation();
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_UP, (e) => {
if (!this.ignoreNextMouseUp) {
if (this.isFocused) {
this.onMenuTriggered(menuIndex, true);
}
} else {
this.ignoreNextMouseUp = false;
}
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_ENTER, () => {
if (this.isOpen && !this.isCurrentMenu(menuIndex)) {
this.menuCache[menuIndex].buttonElement.focus();
this.cleanupCustomMenu();
this.showCustomMenu(menuIndex, false);
} else if (this.isFocused && !this.isOpen) {
this.focusedMenu = { index: menuIndex };
buttonElement.focus();
}
}));
this.menuCache.push({
label: menuBarMenu.label,
actions: menuBarMenu.actions,
buttonElement: buttonElement,
titleElement: titleElement
});
});
}
createOverflowMenu(): void {
const label = nls.localize('mMore', "...");
const buttonElement = $('div.menubar-menu-button', { 'role': 'menuitem', 'tabindex': -1, 'aria-label': label, 'aria-haspopup': true });
const titleElement = $('div.menubar-menu-title.toolbar-toggle-more', { 'role': 'none', 'aria-hidden': true });
buttonElement.appendChild(titleElement);
this.container.appendChild(buttonElement);
buttonElement.style.visibility = 'hidden';
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.KEY_UP, (e) => {
let event = new StandardKeyboardEvent(e as KeyboardEvent);
let eventHandled = true;
if ((event.equals(KeyCode.DownArrow) || event.equals(KeyCode.Enter)) && !this.isOpen) {
this.focusedMenu = { index: MenuBar.OVERFLOW_INDEX };
this.openedViaKeyboard = true;
this.focusState = MenubarState.OPEN;
} else {
eventHandled = false;
}
if (eventHandled) {
event.preventDefault();
event.stopPropagation();
}
}));
Gesture.addTarget(buttonElement);
this._register(DOM.addDisposableListener(buttonElement, EventType.Tap, (e: GestureEvent) => {
// Ignore this touch if the menu is touched
if (this.isOpen && this.focusedMenu.holder && DOM.isAncestor(e.initialTarget as HTMLElement, this.focusedMenu.holder)) {
return;
}
this.ignoreNextMouseUp = false;
this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true);
e.preventDefault();
e.stopPropagation();
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e) => {
if (!this.isOpen) {
// Open the menu with mouse down and ignore the following mouse up event
this.ignoreNextMouseUp = true;
this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true);
} else {
this.ignoreNextMouseUp = false;
}
e.preventDefault();
e.stopPropagation();
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_UP, (e) => {
if (!this.ignoreNextMouseUp) {
if (this.isFocused) {
this.onMenuTriggered(MenuBar.OVERFLOW_INDEX, true);
}
} else {
this.ignoreNextMouseUp = false;
}
}));
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_ENTER, () => {
if (this.isOpen && !this.isCurrentMenu(MenuBar.OVERFLOW_INDEX)) {
this.overflowMenu.buttonElement.focus();
this.cleanupCustomMenu();
this.showCustomMenu(MenuBar.OVERFLOW_INDEX, false);
} else if (this.isFocused && !this.isOpen) {
this.focusedMenu = { index: MenuBar.OVERFLOW_INDEX };
buttonElement.focus();
}
}));
this.overflowMenu = {
buttonElement: buttonElement,
titleElement: titleElement,
label: 'More'
};
}
updateMenu(menu: MenuBarMenu): void {
const menuToUpdate = this.menuCache.filter(menuBarMenu => menuBarMenu.label === menu.label);
if (menuToUpdate && menuToUpdate.length) {
menuToUpdate[0].actions = menu.actions;
}
}
dispose(): void {
super.dispose();
this.menuCache.forEach(menuBarMenu => {
DOM.removeNode(menuBarMenu.titleElement);
DOM.removeNode(menuBarMenu.buttonElement);
});
DOM.removeNode(this.overflowMenu.titleElement);
DOM.removeNode(this.overflowMenu.buttonElement);
this.overflowLayoutScheduled = dispose(this.overflowLayoutScheduled);
}
blur(): void {
this.setUnfocusedState();
}
getWidth(): number {
if (this.menuCache) {
const left = this.menuCache[0].buttonElement.getBoundingClientRect().left;
const right = this.hasOverflow ? this.overflowMenu.buttonElement.getBoundingClientRect().right : this.menuCache[this.menuCache.length - 1].buttonElement.getBoundingClientRect().right;
return right - left;
}
return 0;
}
getHeight(): number {
return this.container.clientHeight;
}
private updateOverflowAction(): void {
if (!this.menuCache || !this.menuCache.length) {
return;
}
const sizeAvailable = this.container.offsetWidth;
let currentSize = 0;
let full = false;
const prevNumMenusShown = this.numMenusShown;
this.numMenusShown = 0;
for (let menuBarMenu of this.menuCache) {
if (!full) {
const size = menuBarMenu.buttonElement.offsetWidth;
if (currentSize + size > sizeAvailable) {
full = true;
} else {
currentSize += size;
this.numMenusShown++;
if (this.numMenusShown > prevNumMenusShown) {
menuBarMenu.buttonElement.style.visibility = 'visible';
}
}
}
if (full) {
menuBarMenu.buttonElement.style.visibility = 'hidden';
}
}
// Overflow
if (full) {
// Can't fit the more button, need to remove more menus
while (currentSize + this.overflowMenu.buttonElement.offsetWidth > sizeAvailable && this.numMenusShown > 0) {
this.numMenusShown--;
const size = this.menuCache[this.numMenusShown].buttonElement.offsetWidth;
this.menuCache[this.numMenusShown].buttonElement.style.visibility = 'hidden';
currentSize -= size;
}
this.overflowMenu.actions = [];
for (let idx = this.numMenusShown; idx < this.menuCache.length; idx++) {
this.overflowMenu.actions.push(new SubmenuAction(this.menuCache[idx].label, this.menuCache[idx].actions));
}
DOM.removeNode(this.overflowMenu.buttonElement);
this.container.insertBefore(this.overflowMenu.buttonElement, this.menuCache[this.numMenusShown].buttonElement);
this.overflowMenu.buttonElement.style.visibility = 'visible';
} else {
DOM.removeNode(this.overflowMenu.buttonElement);
this.container.appendChild(this.overflowMenu.buttonElement);
this.overflowMenu.buttonElement.style.visibility = 'hidden';
}
}
private updateLabels(titleElement: HTMLElement, buttonElement: HTMLElement, label: string): void {
const cleanMenuLabel = cleanMnemonic(label);
// 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;
let mnemonicMatches = MENU_MNEMONIC_REGEX.exec(label);
// Register mnemonics
if (mnemonicMatches) {
let mnemonic = !!mnemonicMatches[1] ? mnemonicMatches[1] : mnemonicMatches[2];
if (this.options.enableMnemonics) {
buttonElement.setAttribute('aria-keyshortcuts', 'Alt+' + mnemonic.toLocaleLowerCase());
} else {
buttonElement.removeAttribute('aria-keyshortcuts');
}
}
}
style(style: IMenuStyles): void {
this.menuStyle = style;
}
update(options?: IMenuBarOptions): void {
if (options) {
this.options = options;
}
// Don't update while using the menu
if (this.isFocused) {
this.updatePending = true;
return;
}
this.menuCache.forEach(menuBarMenu => {
this.updateLabels(menuBarMenu.titleElement, menuBarMenu.buttonElement, menuBarMenu.label);
});
if (!this.overflowLayoutScheduled) {
this.overflowLayoutScheduled = DOM.scheduleAtNextAnimationFrame(() => {
this.updateOverflowAction();
this.overflowLayoutScheduled = void 0;
});
}
this.setUnfocusedState();
}
private registerMnemonic(menuIndex: number, mnemonic: string): void {
this.mnemonics.set(KeyCodeUtils.fromString(mnemonic), menuIndex);
}
private hideMenubar(): void {
if (this.container.style.display !== 'none') {
this.container.style.display = 'none';
this._onVisibilityChange.fire(false);
}
}
private showMenubar(): void {
if (this.container.style.display !== 'flex') {
this.container.style.display = 'flex';
this._onVisibilityChange.fire(true);
}
}
private get focusState(): MenubarState {
return this._focusState;
}
private set focusState(value: MenubarState) {
if (this._focusState >= MenubarState.FOCUSED && value < MenubarState.FOCUSED) {
// Losing focus, update the menu if needed
if (this.updatePending) {
this.menuUpdater.schedule();
this.updatePending = false;
}
}
if (value === this._focusState) {
return;
}
const isVisible = this.isVisible;
const isOpen = this.isOpen;
const isFocused = this.isFocused;
this._focusState = value;
switch (value) {
case MenubarState.HIDDEN:
if (isVisible) {
this.hideMenubar();
}
if (isOpen) {
this.cleanupCustomMenu();
}
if (isFocused) {
this.focusedMenu = null;
if (this.focusToReturn) {
this.focusToReturn.focus();
this.focusToReturn = null;
}
}
break;
case MenubarState.VISIBLE:
if (!isVisible) {
this.showMenubar();
}
if (isOpen) {
this.cleanupCustomMenu();
}
if (isFocused) {
if (this.focusedMenu) {
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
this.overflowMenu.buttonElement.blur();
} else {
this.menuCache[this.focusedMenu.index].buttonElement.blur();
}
}
this.focusedMenu = null;
if (this.focusToReturn) {
this.focusToReturn.focus();
this.focusToReturn = null;
}
}
break;
case MenubarState.FOCUSED:
if (!isVisible) {
this.showMenubar();
}
if (isOpen) {
this.cleanupCustomMenu();
}
if (this.focusedMenu) {
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
this.overflowMenu.buttonElement.focus();
} else {
this.menuCache[this.focusedMenu.index].buttonElement.focus();
}
}
break;
case MenubarState.OPEN:
if (!isVisible) {
this.showMenubar();
}
if (this.focusedMenu) {
this.showCustomMenu(this.focusedMenu.index, this.openedViaKeyboard);
}
break;
}
this._focusState = value;
this._onFocusStateChange.fire(this.focusState >= MenubarState.FOCUSED);
}
private get isVisible(): boolean {
return this.focusState >= MenubarState.VISIBLE;
}
private get isFocused(): boolean {
return this.focusState >= MenubarState.FOCUSED;
}
private get isOpen(): boolean {
return this.focusState >= MenubarState.OPEN;
}
private get hasOverflow(): boolean {
return this.numMenusShown < this.menuCache.length;
}
private setUnfocusedState(): void {
if (this.options.visibility === 'toggle' || this.options.visibility === 'hidden') {
this.focusState = MenubarState.HIDDEN;
} else if (this.options.visibility === 'default' && browser.isFullscreen()) {
this.focusState = MenubarState.HIDDEN;
} else {
this.focusState = MenubarState.VISIBLE;
}
this.ignoreNextMouseUp = false;
this.mnemonicsInUse = false;
this.updateMnemonicVisibility(false);
}
private focusPrevious(): void {
if (!this.focusedMenu) {
return;
}
let newFocusedIndex = (this.focusedMenu.index - 1 + this.numMenusShown) % this.numMenusShown;
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
newFocusedIndex = this.numMenusShown - 1;
} else if (this.focusedMenu.index === 0 && this.hasOverflow) {
newFocusedIndex = MenuBar.OVERFLOW_INDEX;
}
if (newFocusedIndex === this.focusedMenu.index) {
return;
}
if (this.isOpen) {
this.cleanupCustomMenu();
this.showCustomMenu(newFocusedIndex);
} else if (this.isFocused) {
this.focusedMenu.index = newFocusedIndex;
if (newFocusedIndex === MenuBar.OVERFLOW_INDEX) {
this.overflowMenu.buttonElement.focus();
} else {
this.menuCache[newFocusedIndex].buttonElement.focus();
}
}
}
private focusNext(): void {
if (!this.focusedMenu) {
return;
}
let newFocusedIndex = (this.focusedMenu.index + 1) % this.numMenusShown;
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
newFocusedIndex = 0;
} else if (this.focusedMenu.index === this.numMenusShown - 1) {
newFocusedIndex = MenuBar.OVERFLOW_INDEX;
}
if (newFocusedIndex === this.focusedMenu.index) {
return;
}
if (this.isOpen) {
this.cleanupCustomMenu();
this.showCustomMenu(newFocusedIndex);
} else if (this.isFocused) {
this.focusedMenu.index = newFocusedIndex;
if (newFocusedIndex === MenuBar.OVERFLOW_INDEX) {
this.overflowMenu.buttonElement.focus();
} else {
this.menuCache[newFocusedIndex].buttonElement.focus();
}
}
}
private updateMnemonicVisibility(visible: boolean): void {
if (this.menuCache) {
this.menuCache.forEach(menuBarMenu => {
if (menuBarMenu.titleElement.children.length) {
let child = menuBarMenu.titleElement.children.item(0) as HTMLElement;
if (child) {
child.style.textDecoration = visible ? 'underline' : null;
}
}
});
}
}
private get mnemonicsInUse(): boolean {
return this._mnemonicsInUse;
}
private set mnemonicsInUse(value: boolean) {
this._mnemonicsInUse = value;
}
public get onVisibilityChange(): Event<boolean> {
return this._onVisibilityChange.event;
}
public get onFocusStateChange(): Event<boolean> {
return this._onFocusStateChange.event;
}
private onMenuTriggered(menuIndex: number, clicked: boolean) {
if (this.isOpen) {
if (this.isCurrentMenu(menuIndex)) {
this.setUnfocusedState();
} else {
this.cleanupCustomMenu();
this.showCustomMenu(menuIndex, this.openedViaKeyboard);
}
} else {
this.focusedMenu = { index: menuIndex };
this.openedViaKeyboard = !clicked;
this.focusState = MenubarState.OPEN;
}
}
private onModifierKeyToggled(modifierKeyStatus: IModifierKeyStatus): void {
const allModifiersReleased = !modifierKeyStatus.altKey && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey;
if (this.options.visibility === 'hidden') {
return;
}
// Alt key pressed while menu is focused. This should return focus away from the menubar
if (this.isFocused && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.altKey) {
this.setUnfocusedState();
this.mnemonicsInUse = false;
this.awaitingAltRelease = true;
}
// Clean alt key press and release
if (allModifiersReleased && modifierKeyStatus.lastKeyPressed === 'alt' && modifierKeyStatus.lastKeyReleased === 'alt') {
if (!this.awaitingAltRelease) {
if (!this.isFocused) {
this.mnemonicsInUse = true;
this.focusedMenu = { index: this.numMenusShown > 0 ? 0 : MenuBar.OVERFLOW_INDEX };
this.focusState = MenubarState.FOCUSED;
} else if (!this.isOpen) {
this.setUnfocusedState();
}
}
}
// Alt key released
if (!modifierKeyStatus.altKey && modifierKeyStatus.lastKeyReleased === 'alt') {
this.awaitingAltRelease = false;
}
if (this.options.enableMnemonics && this.menuCache && !this.isOpen) {
this.updateMnemonicVisibility((!this.awaitingAltRelease && modifierKeyStatus.altKey) || this.mnemonicsInUse);
}
}
private isCurrentMenu(menuIndex: number): boolean {
if (!this.focusedMenu) {
return false;
}
return this.focusedMenu.index === menuIndex;
}
private cleanupCustomMenu(): void {
if (this.focusedMenu) {
// Remove focus from the menus first
if (this.focusedMenu.index === MenuBar.OVERFLOW_INDEX) {
this.overflowMenu.buttonElement.focus();
} else {
this.menuCache[this.focusedMenu.index].buttonElement.focus();
}
if (this.focusedMenu.holder) {
DOM.removeClass(this.focusedMenu.holder.parentElement, 'open');
this.focusedMenu.holder.remove();
}
if (this.focusedMenu.widget) {
this.focusedMenu.widget.dispose();
}
this.focusedMenu = { index: this.focusedMenu.index };
}
}
private showCustomMenu(menuIndex: number, selectFirst = true): void {
const actualMenuIndex = menuIndex >= this.numMenusShown ? MenuBar.OVERFLOW_INDEX : menuIndex;
const customMenu = actualMenuIndex === MenuBar.OVERFLOW_INDEX ? this.overflowMenu : this.menuCache[actualMenuIndex];
const menuHolder = $('div.menubar-menu-items-holder');
DOM.addClass(customMenu.buttonElement, 'open');
menuHolder.style.top = `${this.container.clientHeight}px`;
menuHolder.style.left = `${customMenu.buttonElement.getBoundingClientRect().left}px`;
customMenu.buttonElement.appendChild(menuHolder);
let menuOptions: IMenuOptions = {
getKeyBinding: this.options.getKeybinding,
actionRunner: this.actionRunner,
enableMnemonics: this.mnemonicsInUse && this.options.enableMnemonics,
ariaLabel: customMenu.buttonElement.attributes['aria-label'].value
};
let menuWidget = this._register(new Menu(menuHolder, customMenu.actions, menuOptions));
menuWidget.style(this.menuStyle);
this._register(menuWidget.onDidCancel(() => {
this.focusState = MenubarState.FOCUSED;
}));
this._register(menuWidget.onDidBlur(() => {
setTimeout(() => {
this.cleanupCustomMenu();
}, 100);
}));
if (actualMenuIndex !== menuIndex) {
menuWidget.trigger(menuIndex - this.numMenusShown);
} else {
menuWidget.focus(selectFirst);
}
this.focusedMenu = {
index: actualMenuIndex,
holder: menuHolder,
widget: menuWidget
};
}
}
type ModifierKey = 'alt' | 'ctrl' | 'shift';
interface IModifierKeyStatus {
altKey: boolean;
shiftKey: boolean;
ctrlKey: boolean;
lastKeyPressed?: ModifierKey;
lastKeyReleased?: ModifierKey;
}
class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
private _subscriptions: IDisposable[] = [];
private _keyStatus: IModifierKeyStatus;
private static instance: ModifierKeyEmitter;
private constructor() {
super();
this._keyStatus = {
altKey: false,
shiftKey: false,
ctrlKey: false
};
this._subscriptions.push(domEvent(document.body, 'keydown')(e => {
const event = new StandardKeyboardEvent(e);
if (e.altKey && !this._keyStatus.altKey) {
this._keyStatus.lastKeyPressed = 'alt';
} else if (e.ctrlKey && !this._keyStatus.ctrlKey) {
this._keyStatus.lastKeyPressed = 'ctrl';
} else if (e.shiftKey && !this._keyStatus.shiftKey) {
this._keyStatus.lastKeyPressed = 'shift';
} else if (event.keyCode !== KeyCode.Alt) {
this._keyStatus.lastKeyPressed = undefined;
} else {
return;
}
this._keyStatus.altKey = e.altKey;
this._keyStatus.ctrlKey = e.ctrlKey;
this._keyStatus.shiftKey = e.shiftKey;
if (this._keyStatus.lastKeyPressed) {
this.fire(this._keyStatus);
}
}));
this._subscriptions.push(domEvent(document.body, 'keyup')(e => {
if (!e.altKey && this._keyStatus.altKey) {
this._keyStatus.lastKeyReleased = 'alt';
} else if (!e.ctrlKey && this._keyStatus.ctrlKey) {
this._keyStatus.lastKeyReleased = 'ctrl';
} else if (!e.shiftKey && this._keyStatus.shiftKey) {
this._keyStatus.lastKeyReleased = 'shift';
} else {
this._keyStatus.lastKeyReleased = undefined;
}
if (this._keyStatus.lastKeyPressed !== this._keyStatus.lastKeyReleased) {
this._keyStatus.lastKeyPressed = undefined;
}
this._keyStatus.altKey = e.altKey;
this._keyStatus.ctrlKey = e.ctrlKey;
this._keyStatus.shiftKey = e.shiftKey;
if (this._keyStatus.lastKeyReleased) {
this.fire(this._keyStatus);
}
}));
this._subscriptions.push(domEvent(document.body, 'mousedown')(e => {
this._keyStatus.lastKeyPressed = undefined;
}));
this._subscriptions.push(domEvent(window, 'blur')(e => {
this._keyStatus.lastKeyPressed = undefined;
this._keyStatus.lastKeyReleased = undefined;
this._keyStatus.altKey = false;
this._keyStatus.shiftKey = false;
this._keyStatus.shiftKey = false;
this.fire(this._keyStatus);
}));
}
static getInstance() {
if (!ModifierKeyEmitter.instance) {
ModifierKeyEmitter.instance = new ModifierKeyEmitter();
}
return ModifierKeyEmitter.instance;
}
dispose() {
super.dispose();
this._subscriptions = dispose(this._subscriptions);
}
}

View File

@@ -0,0 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.52051 12.3643L9.87793 7L4.52051 1.635742L5.13574 1.0205078L11.1221 7L5.13574 12.9795L4.52051 12.3643Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 233 B

View File

@@ -1,124 +0,0 @@
// ATTENTION - THIS DIRECTORY CONTAINS THIRD PARTY OPEN SOURCE MATERIALS:
[{
"name": "octicons-code",
"repositoryURL": "https://octicons.github.com",
"version": "3.1.0",
"license": "MIT",
"licenseDetail": [
"The MIT License (MIT)",
"",
"(c) 2012-2015 GitHub",
"",
"Permission is hereby granted, free of charge, to any person obtaining a copy",
"of this software and associated documentation files (the \"Software\"), to deal",
"in the Software without restriction, including without limitation the rights",
"to use, copy, modify, merge, publish, distribute, sublicense, and/or sell",
"copies of the Software, and to permit persons to whom the Software is",
"furnished to do so, subject to the following conditions:",
"",
"The above copyright notice and this permission notice shall be included in",
"all copies or substantial portions of the Software.",
"",
"THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR",
"IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,",
"FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE",
"AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER",
"LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,",
"OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN",
"THE SOFTWARE."
]
},{
"name": "octicons-font",
"repositoryURL": "https://octicons.github.com",
"version": "3.1.0",
"license": "SIL OFL 1.1",
"licenseDetail": [
"(c) 2012-2015 GitHub",
"",
"SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007",
"",
"PREAMBLE",
"The goals of the Open Font License (OFL) are to stimulate worldwide",
"development of collaborative font projects, to support the font creation",
"efforts of academic and linguistic communities, and to provide a free and",
"open framework in which fonts may be shared and improved in partnership",
"with others.",
"",
"The OFL allows the licensed fonts to be used, studied, modified and",
"redistributed freely as long as they are not sold by themselves. The",
"fonts, including any derivative works, can be bundled, embedded,",
"redistributed and/or sold with any software provided that any reserved",
"names are not used by derivative works. The fonts and derivatives,",
"however, cannot be released under any other type of license. The",
"requirement for fonts to remain under this license does not apply",
"to any document created using the fonts or their derivatives.",
"",
"DEFINITIONS",
"\"Font Software\" refers to the set of files released by the Copyright",
"Holder(s) under this license and clearly marked as such. This may",
"include source files, build scripts and documentation.",
"",
"\"Reserved Font Name\" refers to any names specified as such after the",
"copyright statement(s).",
"",
"\"Original Version\" refers to the collection of Font Software components as",
"distributed by the Copyright Holder(s).",
"",
"\"Modified Version\" refers to any derivative made by adding to, deleting,",
"or substituting -- in part or in whole -- any of the components of the",
"Original Version, by changing formats or by porting the Font Software to a",
"new environment.",
"",
"\"Author\" refers to any designer, engineer, programmer, technical",
"writer or other person who contributed to the Font Software.",
"",
"PERMISSION & CONDITIONS",
"Permission is hereby granted, free of charge, to any person obtaining",
"a copy of the Font Software, to use, study, copy, merge, embed, modify,",
"redistribute, and sell modified and unmodified copies of the Font",
"Software, subject to the following conditions:",
"",
"1) Neither the Font Software nor any of its individual components,",
"in Original or Modified Versions, may be sold by itself.",
"",
"2) Original or Modified Versions of the Font Software may be bundled,",
"redistributed and/or sold with any software, provided that each copy",
"contains the above copyright notice and this license. These can be",
"included either as stand-alone text files, human-readable headers or",
"in the appropriate machine-readable metadata fields within text or",
"binary files as long as those fields can be easily viewed by the user.",
"",
"3) No Modified Version of the Font Software may use the Reserved Font",
"Name(s) unless explicit written permission is granted by the corresponding",
"Copyright Holder. This restriction only applies to the primary font name as",
"presented to the users.",
"",
"4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font",
"Software shall not be used to promote, endorse or advertise any",
"Modified Version, except to acknowledge the contribution(s) of the",
"Copyright Holder(s) and the Author(s) or with their explicit written",
"permission.",
"",
"5) The Font Software, modified or unmodified, in part or in whole,",
"must be distributed entirely under this license, and must not be",
"distributed under any other license. The requirement for fonts to",
"remain under this license does not apply to any document created",
"using the Font Software.",
"",
"TERMINATION",
"This license becomes null and void if any of the above conditions are",
"not met.",
"",
"DISCLAIMER",
"THE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,",
"EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF",
"MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT",
"OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE",
"COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,",
"INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL",
"DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING",
"FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM",
"OTHER DEALINGS IN THE FONT SOFTWARE."
]
}]

View File

@@ -0,0 +1,140 @@
{
"registrations": [
{
"component": {
"type": "other",
"other": {
"name": "octicons-code",
"version": "3.1.0",
"downloadUrl": "https://registry.npmjs.org/octicons/-/octicons-3.1.0.tgz"
}
},
"licenseDetail": [
"The MIT License (MIT)",
"",
"(c) 2012-2015 GitHub",
"",
"Permission is hereby granted, free of charge, to any person obtaining a copy",
"of this software and associated documentation files (the \"Software\"), to deal",
"in the Software without restriction, including without limitation the rights",
"to use, copy, modify, merge, publish, distribute, sublicense, and/or sell",
"copies of the Software, and to permit persons to whom the Software is",
"furnished to do so, subject to the following conditions:",
"",
"The above copyright notice and this permission notice shall be included in",
"all copies or substantial portions of the Software.",
"",
"THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR",
"IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,",
"FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE",
"AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER",
"LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,",
"OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN",
"THE SOFTWARE."
],
"license": "MIT",
"version": "3.1.0"
},
{
"component": {
"type": "other",
"other": {
"name": "octicons-font",
"version": "3.1.0",
"downloadUrl": "https://registry.npmjs.org/octicons/-/octicons-3.1.0.tgz"
}
},
"licenseDetail": [
"(c) 2012-2015 GitHub",
"",
"SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007",
"",
"PREAMBLE",
"The goals of the Open Font License (OFL) are to stimulate worldwide",
"development of collaborative font projects, to support the font creation",
"efforts of academic and linguistic communities, and to provide a free and",
"open framework in which fonts may be shared and improved in partnership",
"with others.",
"",
"The OFL allows the licensed fonts to be used, studied, modified and",
"redistributed freely as long as they are not sold by themselves. The",
"fonts, including any derivative works, can be bundled, embedded,",
"redistributed and/or sold with any software provided that any reserved",
"names are not used by derivative works. The fonts and derivatives,",
"however, cannot be released under any other type of license. The",
"requirement for fonts to remain under this license does not apply",
"to any document created using the fonts or their derivatives.",
"",
"DEFINITIONS",
"\"Font Software\" refers to the set of files released by the Copyright",
"Holder(s) under this license and clearly marked as such. This may",
"include source files, build scripts and documentation.",
"",
"\"Reserved Font Name\" refers to any names specified as such after the",
"copyright statement(s).",
"",
"\"Original Version\" refers to the collection of Font Software components as",
"distributed by the Copyright Holder(s).",
"",
"\"Modified Version\" refers to any derivative made by adding to, deleting,",
"or substituting -- in part or in whole -- any of the components of the",
"Original Version, by changing formats or by porting the Font Software to a",
"new environment.",
"",
"\"Author\" refers to any designer, engineer, programmer, technical",
"writer or other person who contributed to the Font Software.",
"",
"PERMISSION & CONDITIONS",
"Permission is hereby granted, free of charge, to any person obtaining",
"a copy of the Font Software, to use, study, copy, merge, embed, modify,",
"redistribute, and sell modified and unmodified copies of the Font",
"Software, subject to the following conditions:",
"",
"1) Neither the Font Software nor any of its individual components,",
"in Original or Modified Versions, may be sold by itself.",
"",
"2) Original or Modified Versions of the Font Software may be bundled,",
"redistributed and/or sold with any software, provided that each copy",
"contains the above copyright notice and this license. These can be",
"included either as stand-alone text files, human-readable headers or",
"in the appropriate machine-readable metadata fields within text or",
"binary files as long as those fields can be easily viewed by the user.",
"",
"3) No Modified Version of the Font Software may use the Reserved Font",
"Name(s) unless explicit written permission is granted by the corresponding",
"Copyright Holder. This restriction only applies to the primary font name as",
"presented to the users.",
"",
"4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font",
"Software shall not be used to promote, endorse or advertise any",
"Modified Version, except to acknowledge the contribution(s) of the",
"Copyright Holder(s) and the Author(s) or with their explicit written",
"permission.",
"",
"5) The Font Software, modified or unmodified, in part or in whole,",
"must be distributed entirely under this license, and must not be",
"distributed under any other license. The requirement for fonts to",
"remain under this license does not apply to any document created",
"using the Font Software.",
"",
"TERMINATION",
"This license becomes null and void if any of the above conditions are",
"not met.",
"",
"DISCLAIMER",
"THE FONT SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,",
"EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF",
"MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT",
"OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE",
"COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,",
"INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL",
"DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING",
"FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM",
"OTHER DEALINGS IN THE FONT SOFTWARE."
],
"license": "SIL OFL 1.1",
"version": "3.1.0"
}
],
"version": 1
}

View File

@@ -24,10 +24,6 @@
.monaco-progress-container.discrete .progress-bit {
left: 0;
transition: width 100ms linear;
-webkit-transition: width 100ms linear;
-o-transition: width 100ms linear;
-moz-transition: width 100ms linear;
-ms-transition: width 100ms linear;
}
.monaco-progress-container.discrete.done .progress-bit {

View File

@@ -3,16 +3,13 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./progressbar';
import { TPromise, ValueCallback } from 'vs/base/common/winjs.base';
import * as assert from 'vs/base/common/assert';
import { Builder, $ } from 'vs/base/browser/builder';
import * as DOM from 'vs/base/browser/dom';
import { Disposable } from 'vs/base/common/lifecycle';
import { Color } from 'vs/base/common/color';
import { mixin } from 'vs/base/common/objects';
import { removeClasses, addClass, hasClass, addClasses, removeClass, hide, show } from 'vs/base/browser/dom';
import { RunOnceScheduler } from 'vs/base/common/async';
const css_done = 'done';
const css_active = 'active';
@@ -38,11 +35,11 @@ const defaultOpts = {
export class ProgressBar extends Disposable {
private options: IProgressBarOptions;
private workedVal: number;
private element: Builder;
private element: HTMLElement;
private bit: HTMLElement;
private totalWork: number;
private animationStopToken: ValueCallback;
private progressBarBackground: Color;
private totalWork: number | undefined;
private progressBarBackground: Color | undefined;
private showDelayedScheduler: RunOnceScheduler;
constructor(container: HTMLElement, options?: IProgressBarOptions) {
super();
@@ -54,26 +51,19 @@ export class ProgressBar extends Disposable {
this.progressBarBackground = this.options.progressBarBackground;
this._register(this.showDelayedScheduler = new RunOnceScheduler(() => show(this.element), 0));
this.create(container);
}
private create(container: HTMLElement): void {
$(container).div({ 'class': css_progress_container }, builder => {
this.element = builder.clone();
this.element = document.createElement('div');
addClass(this.element, css_progress_container);
container.appendChild(this.element);
builder.div({ 'class': css_progress_bit }).on([DOM.EventType.ANIMATION_START, DOM.EventType.ANIMATION_END, DOM.EventType.ANIMATION_ITERATION], (e: Event) => {
switch (e.type) {
case DOM.EventType.ANIMATION_ITERATION:
if (this.animationStopToken) {
this.animationStopToken(null);
}
break;
}
}, this.toDispose);
this.bit = builder.getHTMLElement();
});
this.bit = document.createElement('div');
addClass(this.bit, css_progress_bit);
this.element.appendChild(this.bit);
this.applyStyles();
}
@@ -81,9 +71,7 @@ export class ProgressBar extends Disposable {
private off(): void {
this.bit.style.width = 'inherit';
this.bit.style.opacity = '1';
this.element.removeClass(css_active);
this.element.removeClass(css_infinite);
this.element.removeClass(css_discrete);
removeClasses(this.element, css_active, css_infinite, css_discrete);
this.workedVal = 0;
this.totalWork = undefined;
@@ -104,14 +92,14 @@ export class ProgressBar extends Disposable {
}
private doDone(delayed: boolean): ProgressBar {
this.element.addClass(css_done);
addClass(this.element, css_done);
// let it grow to 100% width and hide afterwards
if (!this.element.hasClass(css_infinite)) {
if (!hasClass(this.element, css_infinite)) {
this.bit.style.width = 'inherit';
if (delayed) {
TPromise.timeout(200).then(() => this.off());
setTimeout(() => this.off(), 200);
} else {
this.off();
}
@@ -121,7 +109,7 @@ export class ProgressBar extends Disposable {
else {
this.bit.style.opacity = '0';
if (delayed) {
TPromise.timeout(200).then(() => this.off());
setTimeout(() => this.off(), 200);
} else {
this.off();
}
@@ -137,10 +125,8 @@ export class ProgressBar extends Disposable {
this.bit.style.width = '2%';
this.bit.style.opacity = '1';
this.element.removeClass(css_discrete);
this.element.removeClass(css_done);
this.element.addClass(css_active);
this.element.addClass(css_infinite);
removeClasses(this.element, css_discrete, css_done);
addClasses(this.element, css_active, css_infinite);
return this;
}
@@ -160,7 +146,7 @@ export class ProgressBar extends Disposable {
* Finds out if this progress bar is configured with total work
*/
hasTotal(): boolean {
return !isNaN(this.totalWork);
return !isNaN(this.totalWork as number);
}
/**
@@ -186,46 +172,49 @@ export class ProgressBar extends Disposable {
}
private doSetWorked(value: number): ProgressBar {
assert.ok(!isNaN(this.totalWork), 'Total work not set');
assert.ok(!isNaN(this.totalWork as number), 'Total work not set');
this.workedVal = value;
this.workedVal = Math.min(this.totalWork, this.workedVal);
this.workedVal = Math.min(this.totalWork as number, this.workedVal);
if (this.element.hasClass(css_infinite)) {
this.element.removeClass(css_infinite);
if (hasClass(this.element, css_infinite)) {
removeClass(this.element, css_infinite);
}
if (this.element.hasClass(css_done)) {
this.element.removeClass(css_done);
if (hasClass(this.element, css_done)) {
removeClass(this.element, css_done);
}
if (!this.element.hasClass(css_active)) {
this.element.addClass(css_active);
if (!hasClass(this.element, css_active)) {
addClass(this.element, css_active);
}
if (!this.element.hasClass(css_discrete)) {
this.element.addClass(css_discrete);
if (!hasClass(this.element, css_discrete)) {
addClass(this.element, css_discrete);
}
this.bit.style.width = 100 * (this.workedVal / this.totalWork) + '%';
this.bit.style.width = 100 * (this.workedVal / (this.totalWork as number)) + '%';
return this;
}
getContainer(): HTMLElement {
return this.element.getHTMLElement();
return this.element;
}
show(delay?: number): void {
this.showDelayedScheduler.cancel();
if (typeof delay === 'number') {
this.element.showDelayed(delay);
this.showDelayedScheduler.schedule(delay);
} else {
this.element.show();
show(this.element);
}
}
hide(): void {
this.element.hide();
hide(this.element);
this.showDelayedScheduler.cancel();
}
style(styles: IProgressBarStyles): void {

View File

@@ -3,10 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./sash';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IDisposable, dispose, Disposable } from 'vs/base/common/lifecycle';
import { isIPad } from 'vs/base/browser/browser';
import { isMacintosh } from 'vs/base/common/platform';
import * as types from 'vs/base/common/types';
@@ -46,25 +44,24 @@ export interface ISashOptions {
orthogonalEndSash?: Sash;
}
export enum Orientation {
export const enum Orientation {
VERTICAL,
HORIZONTAL
}
export enum SashState {
export const enum SashState {
Disabled,
Minimum,
Maximum,
Enabled
}
export class Sash {
export class Sash extends Disposable {
private el: HTMLElement;
private layoutProvider: ISashLayoutProvider;
private hidden: boolean;
private orientation: Orientation;
private disposables: IDisposable[] = [];
private _state: SashState = SashState.Enabled;
get state(): SashState { return this._state; }
@@ -81,19 +78,19 @@ export class Sash {
this._onDidEnablementChange.fire(state);
}
private readonly _onDidEnablementChange = new Emitter<SashState>();
private readonly _onDidEnablementChange = this._register(new Emitter<SashState>());
readonly onDidEnablementChange: Event<SashState> = this._onDidEnablementChange.event;
private readonly _onDidStart = new Emitter<ISashEvent>();
private readonly _onDidStart = this._register(new Emitter<ISashEvent>());
readonly onDidStart: Event<ISashEvent> = this._onDidStart.event;
private readonly _onDidChange = new Emitter<ISashEvent>();
private readonly _onDidChange = this._register(new Emitter<ISashEvent>());
readonly onDidChange: Event<ISashEvent> = this._onDidChange.event;
private readonly _onDidReset = new Emitter<void>();
private readonly _onDidReset = this._register(new Emitter<void>());
readonly onDidReset: Event<void> = this._onDidReset.event;
private readonly _onDidEnd = new Emitter<void>();
private readonly _onDidEnd = this._register(new Emitter<void>());
readonly onDidEnd: Event<void> = this._onDidEnd.event;
linkedSash: Sash | undefined = undefined;
@@ -131,17 +128,19 @@ export class Sash {
}
constructor(container: HTMLElement, layoutProvider: ISashLayoutProvider, options: ISashOptions = {}) {
super();
this.el = append(container, $('.monaco-sash'));
if (isMacintosh) {
addClass(this.el, 'mac');
}
domEvent(this.el, 'mousedown')(this.onMouseDown, this, this.disposables);
domEvent(this.el, 'dblclick')(this.onMouseDoubleClick, this, this.disposables);
this._register(domEvent(this.el, 'mousedown')(this.onMouseDown, this));
this._register(domEvent(this.el, 'dblclick')(this.onMouseDoubleClick, this));
Gesture.addTarget(this.el);
domEvent(this.el, EventType.Start)(this.onTouchStart, this, this.disposables);
this._register(domEvent(this.el, EventType.Start)(this.onTouchStart, this));
if (isIPad) {
// see also http://ux.stackexchange.com/questions/39023/what-is-the-optimum-button-size-of-touch-screen-applications
@@ -265,7 +264,7 @@ export class Sash {
const onMouseMove = (e: MouseEvent) => {
EventHelper.stop(e, false);
const mouseMoveEvent = new StandardMouseEvent(e as MouseEvent);
const mouseMoveEvent = new StandardMouseEvent(e);
const event: ISashEvent = { startX, currentX: mouseMoveEvent.posx, startY, currentY: mouseMoveEvent.posy, altKey };
this._onDidChange.fire(event);
@@ -383,6 +382,8 @@ export class Sash {
}
dispose(): void {
super.dispose();
this.orthogonalStartSashDisposables = dispose(this.orthogonalStartSashDisposables);
this.orthogonalEndSashDisposables = dispose(this.orthogonalEndSashDisposables);
@@ -390,7 +391,6 @@ export class Sash {
this.el.parentElement.removeChild(this.el);
}
this.el = null;
this.disposables = dispose(this.disposables);
this.el = null!; // StrictNullOverride: nulling out ok in dispose
}
}

View File

@@ -2,18 +2,17 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as Platform from 'vs/base/common/platform';
import * as DomUtils from 'vs/base/browser/dom';
import { IMouseEvent, StandardMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor';
import { Widget } from 'vs/base/browser/ui/widget';
import * as dom from 'vs/base/browser/dom';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor';
import { IMouseEvent, StandardWheelEvent } from 'vs/base/browser/mouseEvent';
import { ScrollbarArrow, ScrollbarArrowOptions } from 'vs/base/browser/ui/scrollbar/scrollbarArrow';
import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
import { ScrollbarVisibilityController } from 'vs/base/browser/ui/scrollbar/scrollbarVisibilityController';
import { Scrollable, ScrollbarVisibility, INewScrollPosition } from 'vs/base/common/scrollable';
import { Widget } from 'vs/base/browser/ui/widget';
import * as platform from 'vs/base/common/platform';
import { INewScrollPosition, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
/**
* The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
@@ -26,7 +25,7 @@ export interface ISimplifiedMouseEvent {
}
export interface ScrollbarHost {
onMouseWheel(mouseWheelEvent: StandardMouseWheelEvent): void;
onMouseWheel(mouseWheelEvent: StandardWheelEvent): void;
onDragStart(): void;
onDragEnd(): void;
}
@@ -87,14 +86,18 @@ export abstract class AbstractScrollbar extends Widget {
/**
* Creates the slider dom node, adds it to the container & hooks up the events
*/
protected _createSlider(top: number, left: number, width: number, height: number): void {
protected _createSlider(top: number, left: number, width: number | undefined, height: number | undefined): void {
this.slider = createFastDomNode(document.createElement('div'));
this.slider.setClassName('slider');
this.slider.setPosition('absolute');
this.slider.setTop(top);
this.slider.setLeft(left);
this.slider.setWidth(width);
this.slider.setHeight(height);
if (typeof width === 'number') {
this.slider.setWidth(width);
}
if (typeof height === 'number') {
this.slider.setHeight(height);
}
this.slider.setLayerHinting(true);
this.domNode.domNode.appendChild(this.slider.domNode);
@@ -194,7 +197,7 @@ export abstract class AbstractScrollbar extends Widget {
offsetX = e.browserEvent.offsetX;
offsetY = e.browserEvent.offsetY;
} else {
const domNodePosition = DomUtils.getDomNodePagePosition(this.domNode.domNode);
const domNodePosition = dom.getDomNodePagePosition(this.domNode.domNode);
offsetX = e.posx - domNodePosition.left;
offsetY = e.posy - domNodePosition.top;
}
@@ -217,7 +220,7 @@ export abstract class AbstractScrollbar extends Widget {
const mouseOrthogonalPosition = this._sliderOrthogonalMousePosition(mouseMoveData);
const mouseOrthogonalDelta = Math.abs(mouseOrthogonalPosition - initialMouseOrthogonalPosition);
if (Platform.isWindows && mouseOrthogonalDelta > MOUSE_DRAG_RESET_DISTANCE) {
if (platform.isWindows && mouseOrthogonalDelta > MOUSE_DRAG_RESET_DISTANCE) {
// The mouse has wondered away from the scrollbar => reset dragging
this._setDesiredScrollPositionNow(initialScrollbarState.getScrollPosition());
return;

View File

@@ -2,14 +2,13 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { AbstractScrollbar, ScrollbarHost, ISimplifiedMouseEvent } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
import { StandardMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { StandardWheelEvent } from 'vs/base/browser/mouseEvent';
import { AbstractScrollbar, ISimplifiedMouseEvent, ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
import { ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
import { Scrollable, ScrollEvent, ScrollbarVisibility, INewScrollPosition } from 'vs/base/common/scrollable';
import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
import { ARROW_IMG_SIZE } from 'vs/base/browser/ui/scrollbar/scrollbarArrow';
import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
import { INewScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
export class HorizontalScrollbar extends AbstractScrollbar {
@@ -39,7 +38,7 @@ export class HorizontalScrollbar extends AbstractScrollbar {
right: void 0,
bgWidth: options.arrowSize,
bgHeight: options.horizontalScrollbarSize,
onActivate: () => this._host.onMouseWheel(new StandardMouseWheelEvent(null, 1, 0)),
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 1, 0)),
});
this._createArrow({
@@ -50,11 +49,11 @@ export class HorizontalScrollbar extends AbstractScrollbar {
right: arrowDelta,
bgWidth: options.arrowSize,
bgHeight: options.horizontalScrollbarSize,
onActivate: () => this._host.onMouseWheel(new StandardMouseWheelEvent(null, -1, 0)),
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, -1, 0)),
});
}
this._createSlider(Math.floor((options.horizontalScrollbarSize - options.horizontalSliderSize) / 2), 0, null, options.horizontalSliderSize);
this._createSlider(Math.floor((options.horizontalScrollbarSize - options.horizontalSliderSize) / 2), 0, undefined, options.horizontalSliderSize);
}
protected _updateSlider(sliderSize: number, sliderPosition: number): void {

View File

@@ -44,10 +44,6 @@
/* Background rule added for IE9 - to allow clicks on dom node */
background:rgba(0,0,0,0);
-webkit-transition: opacity 100ms linear;
-o-transition: opacity 100ms linear;
-moz-transition: opacity 100ms linear;
-ms-transition: opacity 100ms linear;
transition: opacity 100ms linear;
}
.monaco-scrollable-element > .invisible {
@@ -55,10 +51,6 @@
pointer-events: none;
}
.monaco-scrollable-element > .invisible.fade {
-webkit-transition: opacity 800ms linear;
-o-transition: opacity 800ms linear;
-moz-transition: opacity 800ms linear;
-ms-transition: opacity 800ms linear;
transition: opacity 800ms linear;
}

View File

@@ -2,23 +2,21 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./media/scrollbars';
import * as DomUtils from 'vs/base/browser/dom';
import * as Platform from 'vs/base/common/platform';
import { StandardMouseWheelEvent, IMouseEvent } from 'vs/base/browser/mouseEvent';
import * as dom from 'vs/base/browser/dom';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { IMouseEvent, StandardWheelEvent, IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
import { HorizontalScrollbar } from 'vs/base/browser/ui/scrollbar/horizontalScrollbar';
import { ScrollableElementChangeOptions, ScrollableElementCreationOptions, ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
import { VerticalScrollbar } from 'vs/base/browser/ui/scrollbar/verticalScrollbar';
import { ScrollableElementCreationOptions, ScrollableElementChangeOptions, ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Scrollable, ScrollEvent, ScrollbarVisibility, INewScrollDimensions, IScrollDimensions, INewScrollPosition, IScrollPosition } from 'vs/base/common/scrollable';
import { Widget } from 'vs/base/browser/ui/widget';
import { TimeoutTimer } from 'vs/base/common/async';
import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode';
import { ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
import { Event, Emitter } from 'vs/base/common/event';
import { Emitter, Event } from 'vs/base/common/event';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import * as platform from 'vs/base/common/platform';
import { INewScrollDimensions, INewScrollPosition, IScrollDimensions, IScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
const HIDE_TIMEOUT = 500;
const SCROLL_WHEEL_SENSITIVITY = 50;
@@ -168,7 +166,7 @@ export abstract class AbstractScrollableElement extends Widget {
private readonly _onScroll = this._register(new Emitter<ScrollEvent>());
public readonly onScroll: Event<ScrollEvent> = this._onScroll.event;
protected constructor(element: HTMLElement, options: ScrollableElementCreationOptions, scrollable?: Scrollable) {
protected constructor(element: HTMLElement, options: ScrollableElementCreationOptions, scrollable: Scrollable) {
super();
element.style.overflow = 'hidden';
this._options = resolveOptions(options);
@@ -180,7 +178,7 @@ export abstract class AbstractScrollableElement extends Widget {
}));
let scrollbarHost: ScrollbarHost = {
onMouseWheel: (mouseWheelEvent: StandardMouseWheelEvent) => this._onMouseWheel(mouseWheelEvent),
onMouseWheel: (mouseWheelEvent: StandardWheelEvent) => this._onMouseWheel(mouseWheelEvent),
onDragStart: () => this._onDragStart(),
onDragEnd: () => this._onDragEnd(),
};
@@ -268,7 +266,7 @@ export abstract class AbstractScrollableElement extends Widget {
public updateClassName(newClassName: string): void {
this._options.className = newClassName;
// Defaults are different on Macs
if (Platform.isMacintosh) {
if (platform.isMacintosh) {
this._options.className += ' mac';
}
this._domNode.className = 'monaco-scrollable-element ' + this._options.className;
@@ -309,17 +307,15 @@ export abstract class AbstractScrollableElement extends Widget {
// Start listening (if necessary)
if (shouldListen) {
let onMouseWheel = (browserEvent: MouseWheelEvent) => {
let e = new StandardMouseWheelEvent(browserEvent);
this._onMouseWheel(e);
let onMouseWheel = (browserEvent: IMouseWheelEvent) => {
this._onMouseWheel(new StandardWheelEvent(browserEvent));
};
this._mouseWheelToDispose.push(DomUtils.addDisposableListener(this._listenOnDomNode, 'mousewheel', onMouseWheel));
this._mouseWheelToDispose.push(DomUtils.addDisposableListener(this._listenOnDomNode, 'DOMMouseScroll', onMouseWheel));
this._mouseWheelToDispose.push(dom.addDisposableListener(this._listenOnDomNode, 'mousewheel', onMouseWheel));
}
}
private _onMouseWheel(e: StandardMouseWheelEvent): void {
private _onMouseWheel(e: StandardWheelEvent): void {
const classifier = MouseWheelClassifier.INSTANCE;
if (SCROLL_WHEEL_SMOOTH_SCROLL_ENABLED) {
@@ -338,7 +334,7 @@ export abstract class AbstractScrollableElement extends Widget {
// Convert vertical scrolling to horizontal if shift is held, this
// is handled at a higher level on Mac
const shiftConvert = !Platform.isMacintosh && e.browserEvent && e.browserEvent.shiftKey;
const shiftConvert = !platform.isMacintosh && e.browserEvent && e.browserEvent.shiftKey;
if ((this._options.scrollYToX || shiftConvert) && !deltaX) {
deltaX = deltaY;
deltaY = 0;
@@ -479,7 +475,7 @@ export class ScrollableElement extends AbstractScrollableElement {
constructor(element: HTMLElement, options: ScrollableElementCreationOptions) {
options = options || {};
options.mouseWheelSmoothScroll = false;
const scrollable = new Scrollable(0, (callback) => DomUtils.scheduleAtNextAnimationFrame(callback));
const scrollable = new Scrollable(0, (callback) => dom.scheduleAtNextAnimationFrame(callback));
super(element, options, scrollable);
this._register(scrollable);
}
@@ -564,7 +560,7 @@ function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableEleme
result.verticalSliderSize = (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : result.verticalScrollbarSize);
// Defaults are different on Macs
if (Platform.isMacintosh) {
if (platform.isMacintosh) {
result.className += ' mac';
}

View File

@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
@@ -121,7 +120,7 @@ export interface ScrollableElementResolvedOptions {
mouseWheelScrollSensitivity: number;
mouseWheelSmoothScroll: boolean;
arrowSize: number;
listenOnDomNode: HTMLElement;
listenOnDomNode: HTMLElement | null;
horizontal: ScrollbarVisibility;
horizontalScrollbarSize: number;
horizontalSliderSize: number;

View File

@@ -2,12 +2,11 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { GlobalMouseMoveMonitor, IStandardMouseMoveEventData, standardMouseMoveMerger } from 'vs/base/browser/globalMouseMoveMonitor';
import { IMouseEvent } from 'vs/base/browser/mouseEvent';
import { Widget } from 'vs/base/browser/ui/widget';
import { TimeoutTimer, IntervalTimer } from 'vs/base/common/async';
import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async';
/**
* The arrow image size.

View File

@@ -2,7 +2,6 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
/**
* The minimal size of the slider (such that it can still be clickable) -- it is artificially enlarged.

View File

@@ -2,18 +2,17 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { Disposable } from 'vs/base/common/lifecycle';
import { TimeoutTimer } from 'vs/base/common/async';
import { FastDomNode } from 'vs/base/browser/fastDomNode';
import { TimeoutTimer } from 'vs/base/common/async';
import { Disposable } from 'vs/base/common/lifecycle';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
export class ScrollbarVisibilityController extends Disposable {
private _visibility: ScrollbarVisibility;
private _visibleClassName: string;
private _invisibleClassName: string;
private _domNode: FastDomNode<HTMLElement>;
private _domNode: FastDomNode<HTMLElement> | null;
private _shouldBeVisible: boolean;
private _isNeeded: boolean;
private _isVisible: boolean;
@@ -90,7 +89,9 @@ export class ScrollbarVisibilityController extends Disposable {
// The CSS animation doesn't play otherwise
this._revealTimer.setIfNotSet(() => {
this._domNode.setClassName(this._visibleClassName);
if (this._domNode) {
this._domNode.setClassName(this._visibleClassName);
}
}, 0);
}
@@ -100,6 +101,8 @@ export class ScrollbarVisibilityController extends Disposable {
return;
}
this._isVisible = false;
this._domNode.setClassName(this._invisibleClassName + (withFadeAway ? ' fade' : ''));
if (this._domNode) {
this._domNode.setClassName(this._invisibleClassName + (withFadeAway ? ' fade' : ''));
}
}
}

View File

@@ -2,14 +2,13 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { AbstractScrollbar, ScrollbarHost, ISimplifiedMouseEvent } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
import { StandardMouseWheelEvent } from 'vs/base/browser/mouseEvent';
import { StandardWheelEvent } from 'vs/base/browser/mouseEvent';
import { AbstractScrollbar, ISimplifiedMouseEvent, ScrollbarHost } from 'vs/base/browser/ui/scrollbar/abstractScrollbar';
import { ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/scrollableElementOptions';
import { Scrollable, ScrollEvent, ScrollbarVisibility, INewScrollPosition } from 'vs/base/common/scrollable';
import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
import { ARROW_IMG_SIZE } from 'vs/base/browser/ui/scrollbar/scrollbarArrow';
import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
import { INewScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
export class VerticalScrollbar extends AbstractScrollbar {
@@ -40,7 +39,7 @@ export class VerticalScrollbar extends AbstractScrollbar {
right: void 0,
bgWidth: options.verticalScrollbarSize,
bgHeight: options.arrowSize,
onActivate: () => this._host.onMouseWheel(new StandardMouseWheelEvent(null, 0, 1)),
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 0, 1)),
});
this._createArrow({
@@ -51,11 +50,11 @@ export class VerticalScrollbar extends AbstractScrollbar {
right: void 0,
bgWidth: options.verticalScrollbarSize,
bgHeight: options.arrowSize,
onActivate: () => this._host.onMouseWheel(new StandardMouseWheelEvent(null, 0, -1)),
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 0, -1)),
});
}
this._createSlider(0, Math.floor((options.verticalScrollbarSize - options.verticalSliderSize) / 2), options.verticalSliderSize, null);
this._createSlider(0, Math.floor((options.verticalScrollbarSize - options.verticalSliderSize) / 2), options.verticalSliderSize, undefined);
}
protected _updateSlider(sliderSize: number, sliderPosition: number): void {

View File

@@ -14,6 +14,7 @@ import { IListStyles } from 'vs/base/browser/ui/list/listWidget';
import { SelectBoxNative } from 'vs/base/browser/ui/selectBox/selectBoxNative';
import { SelectBoxList } from 'vs/base/browser/ui/selectBox/selectBoxCustom';
import { isMacintosh } from 'vs/base/common/platform';
import { IContentActionHandler } from 'vs/base/browser/htmlContentRenderer';
// Public SelectBox interface - Calls routed to appropriate select implementation class
@@ -24,6 +25,7 @@ export interface ISelectBoxDelegate {
setOptions(options: string[], selected?: number, disabled?: number): void;
select(index: number): void;
setAriaLabel(label: string);
setDetailsProvider(provider: (index: number) => { details: string, isMarkdown: boolean });
focus(): void;
blur(): void;
dispose(): void;
@@ -37,6 +39,8 @@ export interface ISelectBoxDelegate {
export interface ISelectBoxOptions {
ariaLabel?: string;
minBottomMargin?: number;
hasDetails?: boolean;
markdownActionHandler?: IContentActionHandler;
}
export interface ISelectBoxStyles extends IListStyles {
@@ -44,6 +48,7 @@ export interface ISelectBoxStyles extends IListStyles {
selectListBackground?: Color;
selectForeground?: Color;
selectBorder?: Color;
selectListBorder?: Color;
focusBorder?: Color;
}
@@ -73,8 +78,7 @@ export class SelectBox extends Widget implements ISelectBoxDelegate {
mixin(this.styles, defaultStyles, false);
// Instantiate select implementation based on platform
if (isMacintosh) {
// {{SQL CARBON EDIT}}
if (isMacintosh && !(selectBoxOptions && selectBoxOptions.hasDetails)) {
this.selectBoxDelegate = new SelectBoxNative(options, selected, styles, selectBoxOptions);
} else {
this.selectBoxDelegate = new SelectBoxList(options, selected, contextViewProvider, styles, selectBoxOptions);
@@ -104,6 +108,10 @@ export class SelectBox extends Widget implements ISelectBoxDelegate {
this.selectBoxDelegate.setAriaLabel(label);
}
public setDetailsProvider(provider: (index: number) => { details: string, isMarkdown: boolean }): void {
this.selectBoxDelegate.setDetailsProvider(provider);
}
public focus(): void {
this.selectBoxDelegate.focus();
}

View File

@@ -16,8 +16,28 @@
.monaco-select-box-dropdown-container {
display: none;
-webkit-box-sizing: border-box;
-o-box-sizing: border-box;
-moz-box-sizing: border-box;
-ms-box-sizing: border-box;
box-sizing: border-box;
}
.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown * {
margin: 0;
}
.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown a:focus {
outline: 1px solid -webkit-focus-ring-color;
outline-offset: -1px;
}
.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown code {
line-height: 15px; /** For some reason, this is needed, otherwise <code> will take up 20px height */
font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback";
}
.monaco-select-box-dropdown-container.visible {
display: flex;
flex-direction: column;
@@ -42,11 +62,19 @@
box-sizing: border-box;
}
.monaco-select-box-dropdown-container > .select-box-details-pane {
padding: 5px;
}
.hc-black .monaco-select-box-dropdown-container > .select-box-dropdown-list-container {
padding-top: var(--dropdown-padding-top);
padding-bottom: var(--dropdown-padding-bottom);
}
.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row {
cursor: pointer;
}
.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row > .option-text {
text-overflow: ellipsis;
overflow: hidden;
@@ -54,6 +82,18 @@
white-space: nowrap;
}
/* Accepted CSS hiding technique for accessibility reader text */
/* https://webaim.org/techniques/css/invisiblecontent/ */
.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row > .visually-hidden {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.monaco-select-box-dropdown-container > .select-box-dropdown-container-width-control {
flex: 1 1 auto;
align-self: flex-start;

View File

@@ -5,7 +5,6 @@
import 'vs/css!./selectBoxCustom';
import * as nls from 'vs/nls';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Event, Emitter, chain } from 'vs/base/common/event';
import { KeyCode, KeyCodeUtils } from 'vs/base/common/keyCodes';
@@ -14,11 +13,12 @@ import * as dom from 'vs/base/browser/dom';
import * as arrays from 'vs/base/common/arrays';
import { IContextViewProvider, AnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
import { List } from 'vs/base/browser/ui/list/listWidget';
import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
import { IListVirtualDelegate, IListRenderer, IListEvent } from 'vs/base/browser/ui/list/list';
import { domEvent } from 'vs/base/browser/event';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { ISelectBoxDelegate, ISelectBoxOptions, ISelectBoxStyles, ISelectData } from 'vs/base/browser/ui/selectBox/selectBox';
import { isMacintosh } from 'vs/base/common/platform';
import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer';
const $ = dom.$;
@@ -26,16 +26,18 @@ const SELECT_OPTION_ENTRY_TEMPLATE_ID = 'selectOption.entry.template';
export interface ISelectOptionItem {
optionText: string;
optionDescriptionText?: string;
optionDisabled: boolean;
}
interface ISelectListTemplateData {
root: HTMLElement;
optionText: HTMLElement;
optionDescriptionText: HTMLElement;
disposables: IDisposable[];
}
class SelectListRenderer implements IRenderer<ISelectOptionItem, ISelectListTemplateData> {
class SelectListRenderer implements IListRenderer<ISelectOptionItem, ISelectListTemplateData> {
get templateId(): string { return SELECT_OPTION_ENTRY_TEMPLATE_ID; }
@@ -46,6 +48,8 @@ class SelectListRenderer implements IRenderer<ISelectOptionItem, ISelectListTemp
data.disposables = [];
data.root = container;
data.optionText = dom.append(container, $('.option-text'));
data.optionDescriptionText = dom.append(container, $('.option-text-description'));
dom.addClass(data.optionDescriptionText, 'visually-hidden');
return data;
}
@@ -56,10 +60,13 @@ class SelectListRenderer implements IRenderer<ISelectOptionItem, ISelectListTemp
const optionDisabled = (<ISelectOptionItem>element).optionDisabled;
data.optionText.textContent = optionText;
data.root.setAttribute('aria-label', nls.localize('selectAriaOption', "{0}", optionText));
// Workaround for list labels
data.root.setAttribute('aria-selected', 'true');
if (typeof element.optionDescriptionText === 'string') {
const optionDescriptionId = (optionText.replace(/ /g, '_').toLowerCase() + '_description_' + data.root.id);
data.optionText.setAttribute('aria-describedby', optionDescriptionId);
data.optionDescriptionText.id = optionDescriptionId;
data.optionDescriptionText.innerText = element.optionDescriptionText;
}
// pseudo-select disabled option
if (optionDisabled) {
@@ -79,10 +86,10 @@ class SelectListRenderer implements IRenderer<ISelectOptionItem, ISelectListTemp
}
}
export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISelectOptionItem> {
export class SelectBoxList implements ISelectBoxDelegate, IListVirtualDelegate<ISelectOptionItem> {
private static readonly DEFAULT_DROPDOWN_MINIMUM_BOTTOM_MARGIN = 32;
private static readonly DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN = 42;
private static readonly DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN = 2;
private static readonly DEFAULT_MINIMUM_VISIBLE_OPTIONS = 3;
private _isVisible: boolean;
@@ -104,6 +111,11 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
private widthControlElement: HTMLElement;
private _currentSelection: number;
private _dropDownPosition: AnchorPosition;
private detailsProvider: (index: number) => { details: string, isMarkdown: boolean };
private selectionDetailsPane: HTMLElement;
private _skipLayout: boolean = false;
private _sticky: boolean = false; // for dev purposes only
constructor(options: string[], selected: number, contextViewProvider: IContextViewProvider, styles: ISelectBoxStyles, selectBoxOptions?: ISelectBoxOptions) {
@@ -118,6 +130,13 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
}
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';
@@ -126,13 +145,19 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
}
this._onDidSelect = new Emitter<ISelectData>();
this.toDispose.push(this._onDidSelect);
this.styles = styles;
this.registerListeners();
this.constructSelectDropDown(contextViewProvider);
this.setOptions(options, selected);
this.selected = selected || 0;
if (options) {
this.setOptions(options, selected);
}
}
// IDelegate - List renderer
@@ -152,8 +177,9 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
this.selectDropDownContainer = dom.$('.monaco-select-box-dropdown-container');
// Use custom CSS vars for padding calculation (shared with parent select)
dom.addClass(this.selectDropDownContainer, 'monaco-select-box-dropdown-padding');
// Setup list for drop-down select
this.createSelectList(this.selectDropDownContainer);
// Setup container for select option details
this.selectionDetailsPane = dom.append(this.selectDropDownContainer, $('.select-box-details-pane'));
// Create span flex box item/div we can measure and control
let widthControlOuterDiv = dom.append(this.selectDropDownContainer, $('.select-box-dropdown-container-width-control'));
@@ -174,7 +200,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
// Parent native select keyboard listeners
this.toDispose.push(dom.addStandardDisposableListener(this.selectElement, 'change', (e) => {
this.selectElement.title = e.target.value;
this.selected = e.target.selectedIndex;
this._onDidSelect.fire({
index: e.target.selectedIndex,
selected: e.target.value
@@ -227,7 +253,6 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
}
public setOptions(options: string[], selected?: number, disabled?: number): void {
if (!this.options || !arrays.equals(this.options, options)) {
this.options = options;
this.selectElement.options.length = 0;
@@ -237,23 +262,8 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
this.selectElement.add(this.createOption(option, i, disabled === i++));
});
// Mirror options in drop-down
// Populate select list for non-native select mode
if (this.selectList && !!this.options) {
let listEntries: ISelectOptionItem[];
listEntries = [];
if (disabled !== undefined) {
this.disabledOptionIndex = disabled;
}
for (let index = 0; index < this.options.length; index++) {
const element = this.options[index];
let optionDisabled: boolean;
index === this.disabledOptionIndex ? optionDisabled = true : optionDisabled = false;
listEntries.push({ optionText: element, optionDisabled: optionDisabled });
}
this.selectList.splice(0, this.selectList.length, listEntries);
if (disabled !== undefined) {
this.disabledOptionIndex = disabled;
}
}
@@ -264,6 +274,27 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
}
}
private setOptionsList() {
// Mirror options in drop-down
// Populate select list for non-native select mode
if (this.selectList && !!this.options) {
let listEntries: ISelectOptionItem[];
listEntries = [];
for (let index = 0; index < this.options.length; index++) {
const element = this.options[index];
let optionDisabled: boolean;
index === this.disabledOptionIndex ? optionDisabled = true : optionDisabled = false;
const optionDescription = this.detailsProvider ? this.detailsProvider(index) : { details: undefined, isMarkdown: false };
listEntries.push({ optionText: element, optionDisabled: optionDisabled, optionDescriptionText: optionDescription.details });
}
this.selectList.splice(0, this.selectList.length, listEntries);
}
}
public select(index: number): void {
if (index >= 0 && index < this.options.length) {
@@ -277,13 +308,15 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
}
this.selectElement.selectedIndex = this.selected;
this.selectElement.title = this.options[this.selected];
}
public setAriaLabel(label: string): void {
this.selectBoxOptions.ariaLabel = label;
this.selectElement.setAttribute('aria-label', this.selectBoxOptions.ariaLabel);
this.selectList.getHTMLElement().setAttribute('aria-label', this.selectBoxOptions.ariaLabel);
}
public setDetailsProvider(provider: (index: number) => { details: string, isMarkdown: boolean }): void {
this.detailsProvider = provider;
}
public focus(): void {
@@ -301,7 +334,6 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
public render(container: HTMLElement): void {
dom.addClass(container, 'select-container');
container.appendChild(this.selectElement);
this.setOptions(this.options, this.selected);
this.applyStyles();
}
@@ -321,6 +353,17 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused:not(:hover) { color: ${this.styles.listFocusForeground} !important; }`);
}
if (this.styles.selectBackground && this.styles.selectBorder && !this.styles.selectBorder.equals(this.styles.selectBackground)) {
content.push(`.monaco-select-box-dropdown-container { border: 1px solid ${this.styles.selectBorder} } `);
content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-top { border-top: 1px solid ${this.styles.selectBorder} } `);
content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-bottom { border-bottom: 1px solid ${this.styles.selectBorder} } `);
}
else if (this.styles.selectListBorder) {
content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-top { border-top: 1px solid ${this.styles.selectListBorder} } `);
content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-bottom { border-bottom: 1px solid ${this.styles.selectListBorder} } `);
}
// Hover foreground - ignore for disabled options
if (this.styles.listHoverForeground) {
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:hover { color: ${this.styles.listHoverForeground} !important; }`);
@@ -336,6 +379,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
// Match quickOpen outline styles - ignore for disabled options
if (this.styles.listFocusOutline) {
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { outline: 1.6px dotted ${this.styles.listFocusOutline} !important; outline-offset: -1.6px !important; }`);
}
if (this.styles.listHoverOutline) {
@@ -375,10 +419,18 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
// Style drop down select list (non-native mode only)
if (this.selectList) {
this.styleList();
}
}
private styleList() {
if (this.selectList) {
let background = this.styles.selectBackground ? this.styles.selectBackground.toString() : null;
this.selectList.style({});
let listBackground = this.styles.selectListBackground ? this.styles.selectListBackground.toString() : background;
this.selectDropDownListContainer.style.backgroundColor = listBackground;
this.selectionDetailsPane.style.backgroundColor = listBackground;
const optionsBorder = this.styles.focusBorder ? this.styles.focusBorder.toString() : null;
this.selectDropDownContainer.style.outlineColor = optionsBorder;
this.selectDropDownContainer.style.outlineOffset = '-1px';
@@ -389,7 +441,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
let option = document.createElement('option');
option.value = value;
option.text = value;
option.disabled = disabled;
option.disabled = !!disabled;
return option;
}
@@ -397,16 +449,38 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
// ContextView dropdown methods
private showSelectDropDown() {
if (!this.contextViewProvider || this._isVisible) {
return;
}
// Set drop-down position above/below from required height and margins
this.layoutSelectDropDown(true);
// Lazily create and populate list only at open, moved from constructor
this.createSelectList(this.selectDropDownContainer);
this.setOptionsList();
this._isVisible = true;
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
this.contextViewProvider.showContextView({
getAnchor: () => this.selectElement,
render: (container: HTMLElement) => this.renderSelectDropDown(container, true),
layout: () => {
this.layoutSelectDropDown();
},
onHide: () => {
dom.toggleClass(this.selectDropDownContainer, 'visible', false);
dom.toggleClass(this.selectElement, 'synthetic-focus', false);
},
anchorPosition: this._dropDownPosition
});
// Hide so we can relay out
this._isVisible = true;
this.hideSelectDropDown(false);
this.contextViewProvider.showContextView({
getAnchor: () => this.selectElement,
render: (container: HTMLElement) => this.renderSelectDropDown(container),
@@ -420,6 +494,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
// Track initial selection the case user escape, blur
this._currentSelection = this.selected;
this._isVisible = true;
}
private hideSelectDropDown(focusSelect: boolean) {
@@ -432,15 +507,16 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
if (focusSelect) {
this.selectElement.focus();
}
this.contextViewProvider.hideContextView();
}
private renderSelectDropDown(container: HTMLElement): IDisposable {
private renderSelectDropDown(container: HTMLElement, preLayoutPosition?: boolean): IDisposable {
container.appendChild(this.selectDropDownContainer);
this.layoutSelectDropDown();
// Pre-Layout allows us to change position
this.layoutSelectDropDown(preLayoutPosition);
// {{SQL CARBON EDIT}}
return {
dispose: () => {
// contextView will dispose itself if moving from one View to another
@@ -454,7 +530,59 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
};
}
private layoutSelectDropDown(preLayoutPosition?: boolean) {
// Iterate over detailed descriptions, find max height
private measureMaxDetailsHeight(): number {
if (!this.detailsProvider) {
return 0;
}
let maxDetailsPaneHeight = 0;
let description = { details: '', isMarkdown: false };
this.options.forEach((option, index) => {
this.selectionDetailsPane.innerText = '';
description = this.detailsProvider ? this.detailsProvider(index) : { details: '', isMarkdown: false };
if (description.details) {
if (description.isMarkdown) {
this.selectionDetailsPane.appendChild(this.renderDescriptionMarkdown(description.details));
} else {
this.selectionDetailsPane.innerText = description.details;
}
this.selectionDetailsPane.style.display = 'block';
} else {
this.selectionDetailsPane.style.display = 'none';
}
if (this.selectionDetailsPane.offsetHeight > maxDetailsPaneHeight) {
maxDetailsPaneHeight = this.selectionDetailsPane.offsetHeight;
}
});
// Reset description to selected
description = this.detailsProvider ? this.detailsProvider(this.selected) : { details: '', isMarkdown: false };
this.selectionDetailsPane.innerText = '';
if (description.details) {
if (description.isMarkdown) {
this.selectionDetailsPane.appendChild(this.renderDescriptionMarkdown(description.details));
} else {
this.selectionDetailsPane.innerText = description.details;
}
this.selectionDetailsPane.style.display = 'block';
}
return maxDetailsPaneHeight;
}
private layoutSelectDropDown(preLayoutPosition?: boolean): boolean {
// Avoid recursion from layout called in onListFocus
if (this._skipLayout) {
return false;
}
// Layout ContextView drop down select list and container
// Have to manage our vertical overflow, sizing, position below or above
@@ -462,50 +590,105 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
if (this.selectList) {
// Make visible to enable measurements
dom.toggleClass(this.selectDropDownContainer, 'visible', true);
const selectPosition = dom.getDomNodePagePosition(this.selectElement);
const styles = getComputedStyle(this.selectElement);
const verticalPadding = parseFloat(styles.getPropertyValue('--dropdown-padding-top')) + parseFloat(styles.getPropertyValue('--dropdown-padding-bottom'));
let maxSelectDropDownHeight = 0;
maxSelectDropDownHeight = (window.innerHeight - selectPosition.top - selectPosition.height - this.selectBoxOptions.minBottomMargin);
const maxSelectDropDownHeightBelow = (window.innerHeight - selectPosition.top - selectPosition.height - (this.selectBoxOptions.minBottomMargin || 0));
const maxSelectDropDownHeightAbove = (selectPosition.top - SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN);
// Determine optimal width - min(longest option), opt(parent select, excluding margins), max(ContextView controlled)
const selectWidth = this.selectElement.offsetWidth;
const selectMinWidth = this.setWidthControlElement(this.widthControlElement);
const selectOptimalWidth = Math.max(selectMinWidth, Math.round(selectWidth)).toString() + 'px';
this.selectDropDownContainer.style.width = selectOptimalWidth;
// Get initial list height and determine space ab1you knowove and below
this.selectList.layout();
let listHeight = this.selectList.contentHeight;
const maxDetailsPaneHeight = this.measureMaxDetailsHeight();
const minRequiredDropDownHeight = listHeight + verticalPadding + maxDetailsPaneHeight;
const maxVisibleOptionsBelow = ((Math.floor((maxSelectDropDownHeightBelow - verticalPadding - maxDetailsPaneHeight) / this.getHeight())));
const maxVisibleOptionsAbove = ((Math.floor((maxSelectDropDownHeightAbove - verticalPadding - maxDetailsPaneHeight) / this.getHeight())));
// If we are only doing pre-layout check/adjust position only
// Calculate vertical space available, flip up if insufficient
// Use reflected padding on parent select, ContextView style properties not available before DOM attachment
// Use reflected padding on parent select, ContextView style
// properties not available before DOM attachment
if (preLayoutPosition) {
// Always show complete list items - never more than Max available vertical height
if (listHeight + verticalPadding > maxSelectDropDownHeight) {
const maxVisibleOptions = ((Math.floor((maxSelectDropDownHeight - verticalPadding) / this.getHeight())));
// Check if select moved out of viewport , do not open
// If at least one option cannot be shown, don't open the drop-down or hide/remove if open
// Check if we can at least show min items otherwise flip above
if (maxVisibleOptions < SelectBoxList.DEFAULT_MINIMUM_VISIBLE_OPTIONS) {
this._dropDownPosition = AnchorPosition.ABOVE;
} else {
this._dropDownPosition = AnchorPosition.BELOW;
}
if ((selectPosition.top + selectPosition.height) > (window.innerHeight - 22)
|| selectPosition.top < SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN
|| ((maxVisibleOptionsBelow < 1) && (maxVisibleOptionsAbove < 1))) {
// Indicate we cannot open
return false;
}
// Determine if we have to flip up
// Always show complete list items - never more than Max available vertical height
if (maxVisibleOptionsBelow < SelectBoxList.DEFAULT_MINIMUM_VISIBLE_OPTIONS
&& maxVisibleOptionsAbove > maxVisibleOptionsBelow
&& this.options.length > maxVisibleOptionsBelow
) {
this._dropDownPosition = AnchorPosition.ABOVE;
this.selectDropDownContainer.removeChild(this.selectDropDownListContainer);
this.selectDropDownContainer.removeChild(this.selectionDetailsPane);
this.selectDropDownContainer.appendChild(this.selectionDetailsPane);
this.selectDropDownContainer.appendChild(this.selectDropDownListContainer);
dom.removeClass(this.selectionDetailsPane, 'border-top');
dom.addClass(this.selectionDetailsPane, 'border-bottom');
} else {
this._dropDownPosition = AnchorPosition.BELOW;
this.selectDropDownContainer.removeChild(this.selectDropDownListContainer);
this.selectDropDownContainer.removeChild(this.selectionDetailsPane);
this.selectDropDownContainer.appendChild(this.selectDropDownListContainer);
this.selectDropDownContainer.appendChild(this.selectionDetailsPane);
dom.removeClass(this.selectionDetailsPane, 'border-bottom');
dom.addClass(this.selectionDetailsPane, 'border-top');
}
// Do full layout on showSelectDropDown only
return;
return true;
}
// Make visible to enable measurements
dom.toggleClass(this.selectDropDownContainer, 'visible', true);
// Check if select out of viewport or cutting into status bar
if ((selectPosition.top + selectPosition.height) > (window.innerHeight - 22)
|| selectPosition.top < SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN
|| (this._dropDownPosition === AnchorPosition.BELOW && maxVisibleOptionsBelow < 1)
|| (this._dropDownPosition === AnchorPosition.ABOVE && maxVisibleOptionsAbove < 1)) {
// Cannot properly layout, close and hide
this.hideSelectDropDown(true);
return false;
}
// SetUp list dimensions and layout - account for container padding
// Use position to check above or below available space
if (this._dropDownPosition === AnchorPosition.BELOW) {
// Set container height to max from select bottom to margin (default/minBottomMargin)
if (listHeight + verticalPadding > maxSelectDropDownHeight) {
listHeight = ((Math.floor((maxSelectDropDownHeight - verticalPadding) / this.getHeight())) * this.getHeight());
if (this._isVisible && maxVisibleOptionsBelow + maxVisibleOptionsAbove < 1) {
// If drop-down is visible, must be doing a DOM re-layout, hide since we don't fit
// Hide drop-down, hide contextview, focus on parent select
this.hideSelectDropDown(true);
return false;
}
// Adjust list height to max from select bottom to margin (default/minBottomMargin)
if (minRequiredDropDownHeight > maxSelectDropDownHeightBelow) {
listHeight = (maxVisibleOptionsBelow * this.getHeight());
}
} else {
// Set container height to max from select top to margin (default/minTopMargin)
maxSelectDropDownHeight = (selectPosition.top - SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN);
if (listHeight + verticalPadding > maxSelectDropDownHeight) {
listHeight = ((Math.floor((maxSelectDropDownHeight - SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN) / this.getHeight())) * this.getHeight());
if (minRequiredDropDownHeight > maxSelectDropDownHeightAbove) {
listHeight = (maxVisibleOptionsAbove * this.getHeight());
}
}
@@ -519,13 +702,12 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
this.selectList.reveal(this.selectList.getFocus()[0] || 0);
}
// Set final container height after adjustments
this.selectDropDownContainer.style.height = (listHeight + verticalPadding) + 'px';
// Determine optimal width - min(longest option), opt(parent select, excluding margins), max(ContextView controlled)
const selectWidth = this.selectElement.offsetWidth;
const selectMinWidth = this.setWidthControlElement(this.widthControlElement);
const selectOptimalWidth = Math.max(selectMinWidth, Math.round(selectWidth)).toString() + 'px';
if (this.detailsProvider) {
// Leave the selectDropDownContainer to size itself according to children (list + details) - #57447
this.selectList.getHTMLElement().style.height = (listHeight + verticalPadding) + 'px';
} else {
this.selectDropDownContainer.style.height = (listHeight + verticalPadding) + 'px';
}
this.selectDropDownContainer.style.width = selectOptimalWidth;
@@ -533,6 +715,10 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
this.selectDropDownListContainer.setAttribute('tabindex', '0');
dom.toggleClass(this.selectElement, 'synthetic-focus', true);
dom.toggleClass(this.selectDropDownContainer, 'synthetic-focus', true);
return true;
} else {
return false;
}
}
@@ -564,6 +750,11 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
private createSelectList(parent: HTMLElement): void {
// If we have already constructive list on open, skip
if (this.selectList) {
return;
}
// SetUp container for list
this.selectDropDownListContainer = dom.append(parent, $('.select-box-dropdown-list-container'));
@@ -599,9 +790,16 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
.filter(() => this.selectList.length > 0)
.on(e => this.onMouseUp(e), this, this.toDispose);
this.toDispose.push(this.selectList.onDidBlur(e => this.onListBlur()));
this.toDispose.push(
this.selectList.onDidBlur(_ => this.onListBlur()),
this.selectList.onMouseOver(e => typeof e.index !== 'undefined' && this.selectList.setFocus([e.index])),
this.selectList.onFocusChange(e => this.onListFocus(e))
);
this.selectList.getHTMLElement().setAttribute('aria-label', this.selectBoxOptions.ariaLabel || '');
this.selectList.getHTMLElement().setAttribute('aria-expanded', 'true');
this.styleList();
}
// List methods
@@ -617,6 +815,9 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
}
const listRowElement = e.toElement.parentElement;
if (!listRowElement) {
return;
}
const index = Number(listRowElement.getAttribute('data-index'));
const disabled = listRowElement.classList.contains('option-disabled');
@@ -632,13 +833,13 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
if (this.selected !== this._currentSelection) {
// Set current = selected
this._currentSelection = this.selected;
// {{SQL CARBON EDIT}} - Update the selection before firing the handler instead of after
this.hideSelectDropDown(true);
this._onDidSelect.fire({
index: this.selectElement.selectedIndex,
selected: this.selectElement.title
selected: this.options[this.selected]
});
}
}
@@ -646,7 +847,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
// List Exit - passive - implicit no selection change, hide drop-down
private onListBlur(): void {
if (this._sticky) { return; }
if (this.selected !== this._currentSelection) {
// Reset selected to current if no change
this.select(this._currentSelection);
@@ -655,6 +856,59 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
this.hideSelectDropDown(false);
}
private renderDescriptionMarkdown(text: string): HTMLElement {
const cleanRenderedMarkdown = (element: Node) => {
for (let i = 0; i < element.childNodes.length; i++) {
const child = element.childNodes.item(i);
const tagName = (<Element>child).tagName && (<Element>child).tagName.toLowerCase();
if (tagName === 'img') {
element.removeChild(child);
} else {
cleanRenderedMarkdown(child);
}
}
};
const renderedMarkdown = renderMarkdown({ value: text }, {
actionHandler: this.selectBoxOptions.markdownActionHandler
});
renderedMarkdown.classList.add('select-box-description-markdown');
cleanRenderedMarkdown(renderedMarkdown);
return renderedMarkdown;
}
// List Focus Change - passive - update details pane with newly focused element's data
private onListFocus(e: IListEvent<ISelectOptionItem>) {
// Skip during initial layout
if (!this._isVisible) {
return;
}
this.selectionDetailsPane.innerText = '';
const selectedIndex = e.indexes[0];
let description = this.detailsProvider ? this.detailsProvider(selectedIndex) : { details: '', isMarkdown: false };
if (description.details) {
if (description.isMarkdown) {
this.selectionDetailsPane.appendChild(this.renderDescriptionMarkdown(description.details));
} else {
this.selectionDetailsPane.innerText = description.details;
}
this.selectionDetailsPane.style.display = 'block';
} else {
this.selectionDetailsPane.style.display = 'none';
}
// Avoid recursion
this._skipLayout = true;
this.contextViewProvider.layout();
this._skipLayout = false;
}
// List keyboard controller
// List exit - active - hide ContextView dropdown, reset selection, return focus to parent select
@@ -675,7 +929,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
this._currentSelection = this.selected;
this._onDidSelect.fire({
index: this.selectElement.selectedIndex,
selected: this.selectElement.title
selected: this.options[this.selected]
});
}
@@ -688,6 +942,8 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
// Skip disabled options
if ((this.selected + 1) === this.disabledOptionIndex && this.options.length > this.selected + 2) {
this.selected += 2;
} else if ((this.selected + 1) === this.disabledOptionIndex) {
return;
} else {
this.selected++;
}

View File

@@ -28,6 +28,13 @@ export class SelectBoxNative implements ISelectBoxDelegate {
this.selectBoxOptions = selectBoxOptions || Object.create(null);
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') {
@@ -35,6 +42,7 @@ export class SelectBoxNative implements ISelectBoxDelegate {
}
this._onDidSelect = new Emitter<ISelectData>();
this.toDispose.push(this._onDidSelect);
this.styles = styles;
@@ -114,6 +122,10 @@ export class SelectBoxNative implements ISelectBoxDelegate {
this.selectElement.setAttribute('aria-label', label);
}
public setDetailsProvider(provider: any): void {
console.error('details are not available for native select boxes');
}
public focus(): void {
if (this.selectElement) {
this.selectElement.focus();
@@ -154,10 +166,10 @@ export class SelectBoxNative implements ISelectBoxDelegate {
}
private createOption(value: string, index: number, disabled?: boolean): HTMLOptionElement {
let option = document.createElement('option');
const option = document.createElement('option');
option.value = value;
option.text = value;
option.disabled = disabled;
option.disabled = !!disabled;
return option;
}

View File

@@ -60,7 +60,7 @@
}
/* TODO: actions should be part of the panel, but they aren't yet */
.monaco-panel-view .panel > .panel-header > .actions .action-label {
.monaco-panel-view .panel > .panel-header > .actions .action-label.icon {
width: 28px;
height: 22px;
background-size: 16px;
@@ -90,21 +90,13 @@
.monaco-panel-view.animated .split-view-view {
transition-duration: 0.15s;
-webkit-transition-duration: 0.15s;
-moz-transition-duration: 0.15s;
transition-timing-function: ease-out;
-webkit-transition-timing-function: ease-out;
-moz-transition-timing-function: ease-out;
}
.monaco-panel-view.animated.vertical .split-view-view {
transition-property: height;
-webkit-transition-property: height;
-moz-transition-property: height;
}
.monaco-panel-view.animated.horizontal .split-view-view {
transition-property: width;
-webkit-transition-property: width;
-moz-transition-property: width;
}

View File

@@ -3,15 +3,13 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./panelview';
import { IDisposable, dispose, combinedDisposable } from 'vs/base/common/lifecycle';
import { Event, Emitter, chain } from 'vs/base/common/event';
import { IDisposable, dispose, combinedDisposable, Disposable } from 'vs/base/common/lifecycle';
import { Event, Emitter, chain, filterEvent } from 'vs/base/common/event';
import { domEvent } from 'vs/base/browser/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { $, append, addClass, removeClass, toggleClass, trackFocus } from 'vs/base/browser/dom';
import { $, append, addClass, removeClass, toggleClass, trackFocus, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom';
import { firstIndex } from 'vs/base/common/arrays';
import { Color, RGBA } from 'vs/base/common/color';
import { SplitView, IView } from './splitview';
@@ -27,7 +25,7 @@ export interface IPanelStyles {
dropBackground?: Color;
headerForeground?: Color;
headerBackground?: Color;
headerHighContrastBorder?: Color;
headerBorder?: Color;
}
/**
@@ -43,7 +41,11 @@ export abstract class Panel implements IView {
private static readonly HEADER_SIZE = 22;
readonly element: HTMLElement;
protected _expanded: boolean;
protected disposables: IDisposable[] = [];
private expandedSize: number | undefined = undefined;
private _headerVisible = true;
private _minimumBodySize: number;
@@ -51,9 +53,7 @@ export abstract class Panel implements IView {
private ariaHeaderLabel: string;
private styles: IPanelStyles = {};
readonly element: HTMLElement;
private header: HTMLElement;
protected disposables: IDisposable[] = [];
private _onDidChange = new Emitter<number | undefined>();
readonly onDidChange: Event<number | undefined> = this._onDidChange.event;
@@ -188,9 +188,9 @@ export abstract class Panel implements IView {
layout(size: number): void {
const headerSize = this.headerVisible ? Panel.HEADER_SIZE : 0;
this.layoutBody(size - headerSize);
if (this.isExpanded()) {
this.layoutBody(size - headerSize);
this.expandedSize = size;
}
}
@@ -216,7 +216,7 @@ export abstract class Panel implements IView {
this.header.style.color = this.styles.headerForeground ? this.styles.headerForeground.toString() : null;
this.header.style.backgroundColor = this.styles.headerBackground ? this.styles.headerBackground.toString() : null;
this.header.style.borderTop = this.styles.headerHighContrastBorder ? `1px solid ${this.styles.headerHighContrastBorder}` : null;
this.header.style.borderTop = this.styles.headerBorder ? `1px solid ${this.styles.headerBorder}` : null;
this._dropBackground = this.styles.dropBackground;
}
@@ -226,6 +226,8 @@ export abstract class Panel implements IView {
dispose(): void {
this.disposables = dispose(this.disposables);
this._onDidChange.dispose();
}
}
@@ -233,28 +235,28 @@ interface IDndContext {
draggable: PanelDraggable | null;
}
class PanelDraggable implements IDisposable {
class PanelDraggable extends Disposable {
private static readonly DefaultDragOverBackgroundColor = new Color(new RGBA(128, 128, 128, 0.5));
// see https://github.com/Microsoft/vscode/issues/14470
private dragOverCounter = 0;
private disposables: IDisposable[] = [];
private dragOverCounter = 0; // see https://github.com/Microsoft/vscode/issues/14470
private _onDidDrop = new Emitter<{ from: Panel, to: Panel }>();
private _onDidDrop = this._register(new Emitter<{ from: Panel, to: Panel }>());
readonly onDidDrop = this._onDidDrop.event;
constructor(private panel: Panel, private dnd: IPanelDndController, private context: IDndContext) {
super();
panel.draggableElement.draggable = true;
domEvent(panel.draggableElement, 'dragstart')(this.onDragStart, this, this.disposables);
domEvent(panel.dropTargetElement, 'dragenter')(this.onDragEnter, this, this.disposables);
domEvent(panel.dropTargetElement, 'dragleave')(this.onDragLeave, this, this.disposables);
domEvent(panel.dropTargetElement, 'dragend')(this.onDragEnd, this, this.disposables);
domEvent(panel.dropTargetElement, 'drop')(this.onDrop, this, this.disposables);
this._register(domEvent(panel.draggableElement, 'dragstart')(this.onDragStart, this));
this._register(domEvent(panel.dropTargetElement, 'dragenter')(this.onDragEnter, this));
this._register(domEvent(panel.dropTargetElement, 'dragleave')(this.onDragLeave, this));
this._register(domEvent(panel.dropTargetElement, 'dragend')(this.onDragEnd, this));
this._register(domEvent(panel.dropTargetElement, 'drop')(this.onDrop, this));
}
private onDragStart(e: DragEvent): void {
if (!this.dnd.canDrag(this.panel)) {
if (!this.dnd.canDrag(this.panel) || !e.dataTransfer) {
e.preventDefault();
e.stopPropagation();
return;
@@ -262,7 +264,7 @@ class PanelDraggable implements IDisposable {
e.dataTransfer.effectAllowed = 'move';
const dragImage = append(document.body, $('.monaco-panel-drag-image', {}, this.panel.draggableElement.textContent));
const dragImage = append(document.body, $('.monaco-panel-drag-image', {}, this.panel.draggableElement.textContent || ''));
e.dataTransfer.setDragImage(dragImage, -10, -10);
setTimeout(() => document.body.removeChild(dragImage), 0);
@@ -324,7 +326,7 @@ class PanelDraggable implements IDisposable {
}
private render(): void {
let backgroundColor: string = null;
let backgroundColor: string | null = null;
if (this.dragOverCounter > 0) {
backgroundColor = (this.panel.dropBackground || PanelDraggable.DefaultDragOverBackgroundColor).toString();
@@ -332,10 +334,6 @@ class PanelDraggable implements IDisposable {
this.panel.dropTargetElement.style.backgroundColor = backgroundColor;
}
dispose(): void {
this.disposables = dispose(this.disposables);
}
}
export interface IPanelDndController {
@@ -363,30 +361,38 @@ interface IPanelItem {
disposable: IDisposable;
}
export class PanelView implements IDisposable {
export class PanelView extends Disposable {
private dnd: IPanelDndController | null;
private dnd: IPanelDndController | undefined;
private dndContext: IDndContext = { draggable: null };
private el: HTMLElement;
private panelItems: IPanelItem[] = [];
private splitview: SplitView;
private animationTimer: number | null = null;
private _onDidDrop = new Emitter<{ from: Panel, to: Panel }>();
private _onDidDrop = this._register(new Emitter<{ from: Panel, to: Panel }>());
readonly onDidDrop: Event<{ from: Panel, to: Panel }> = this._onDidDrop.event;
readonly onDidSashChange: Event<number>;
constructor(container: HTMLElement, options: IPanelViewOptions = {}) {
super();
this.dnd = options.dnd;
this.el = append(container, $('.monaco-panel-view'));
this.splitview = new SplitView(this.el);
this.splitview = this._register(new SplitView(this.el));
this.onDidSashChange = this.splitview.onDidSashChange;
}
addPanel(panel: Panel, size: number, index = this.splitview.length): void {
const disposables: IDisposable[] = [];
panel.onDidChange(this.setupAnimation, this, disposables);
// https://github.com/Microsoft/vscode/issues/59950
let shouldAnimate = false;
disposables.push(scheduleAtNextAnimationFrame(() => shouldAnimate = true));
filterEvent(panel.onDidChange, () => shouldAnimate)
(this.setupAnimation, this, disposables);
const panelItem = { panel, disposable: combinedDisposable(disposables) };
this.panelItems.splice(index, 0, panelItem);
@@ -463,7 +469,8 @@ export class PanelView implements IDisposable {
}
dispose(): void {
super.dispose();
this.panelItems.forEach(i => i.disposable.dispose());
this.splitview.dispose();
}
}

View File

@@ -38,6 +38,7 @@
.monaco-split-view2 > .split-view-container > .split-view-view {
white-space: initial;
flex: none;
position: relative;
}
.monaco-split-view2.vertical > .split-view-container > .split-view-view {
@@ -49,10 +50,6 @@
display: inline-block;
}
.monaco-split-view2.separator-border > .split-view-container > .split-view-view {
position: relative;
}
.monaco-split-view2.separator-border > .split-view-container > .split-view-view:not(:first-child)::before {
content: ' ';
position: absolute;

View File

@@ -3,10 +3,8 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./splitview';
import { IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { IDisposable, combinedDisposable, toDisposable, Disposable } from 'vs/base/common/lifecycle';
import { Event, mapEvent, Emitter } from 'vs/base/common/event';
import * as types from 'vs/base/common/types';
import * as dom from 'vs/base/browser/dom';
@@ -31,6 +29,16 @@ export interface ISplitViewOptions {
orthogonalStartSash?: Sash;
orthogonalEndSash?: Sash;
inverseAltBehavior?: boolean;
proportionalLayout?: boolean; // default true
}
/**
* Only used when `proportionalLayout` is false.
*/
export const enum LayoutPriority {
Normal,
Low,
High
}
export interface IView {
@@ -38,6 +46,8 @@ export interface IView {
readonly minimumSize: number;
readonly maximumSize: number;
readonly onDidChange: Event<number | undefined>;
readonly priority?: LayoutPriority;
readonly snapSize?: number;
layout(size: number, orientation: Orientation): void;
}
@@ -86,10 +96,9 @@ export namespace Sizing {
export function Split(index: number): SplitSizing { return { type: 'split', index }; }
}
export class SplitView implements IDisposable {
export class SplitView extends Disposable {
readonly orientation: Orientation;
// TODO@Joao have the same pattern as grid here
readonly el: HTMLElement;
private sashContainer: HTMLElement;
private viewContainer: HTMLElement;
@@ -101,10 +110,12 @@ export class SplitView implements IDisposable {
private sashDragState: ISashDragState;
private state: State = State.Idle;
private inverseAltBehavior: boolean;
private proportionalLayout: boolean;
private _onDidSashChange = new Emitter<number>();
private _onDidSashChange = this._register(new Emitter<number>());
readonly onDidSashChange = this._onDidSashChange.event;
private _onDidSashReset = new Emitter<number>();
private _onDidSashReset = this._register(new Emitter<number>());
readonly onDidSashReset = this._onDidSashReset.event;
get length(): number {
@@ -144,8 +155,11 @@ export class SplitView implements IDisposable {
}
constructor(container: HTMLElement, options: ISplitViewOptions = {}) {
super();
this.orientation = types.isUndefined(options.orientation) ? Orientation.VERTICAL : options.orientation;
this.inverseAltBehavior = !!options.inverseAltBehavior;
this.proportionalLayout = types.isUndefined(options.proportionalLayout) ? true : !!options.proportionalLayout;
this.el = document.createElement('div');
dom.addClass(this.el, 'monaco-split-view2');
@@ -316,8 +330,10 @@ export class SplitView implements IDisposable {
private relayout(lowPriorityIndex?: number, highPriorityIndex?: number): void {
const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
const lowPriorityIndexes = typeof lowPriorityIndex === 'number' ? [lowPriorityIndex] : undefined;
const highPriorityIndexes = typeof highPriorityIndex === 'number' ? [highPriorityIndex] : undefined;
this.resize(this.viewItems.length - 1, this.size - contentSize, undefined, lowPriorityIndex, highPriorityIndex);
this.resize(this.viewItems.length - 1, this.size - contentSize, undefined, lowPriorityIndexes, highPriorityIndexes);
this.distributeEmptySpace();
this.layoutViews();
this.saveProportions();
@@ -328,11 +344,15 @@ export class SplitView implements IDisposable {
this.size = size;
if (!this.proportions) {
this.resize(this.viewItems.length - 1, size - previousSize);
const indexes = range(this.viewItems.length);
const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].view.priority === LayoutPriority.Low);
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].view.priority === LayoutPriority.High);
this.resize(this.viewItems.length - 1, size - previousSize, undefined, lowPriorityIndexes, highPriorityIndexes);
} else {
for (let i = 0; i < this.viewItems.length; i++) {
const item = this.viewItems[i];
item.size = clamp(Math.round(this.proportions[i] * size), item.view.minimumSize, item.view.maximumSize);
item.size = SplitView.clamp(item, Math.round(this.proportions[i] * size));
}
}
@@ -341,7 +361,7 @@ export class SplitView implements IDisposable {
}
private saveProportions(): void {
if (this.contentSize > 0) {
if (this.proportionalLayout && this.contentSize > 0) {
this.proportions = this.viewItems.map(i => i.size / this.contentSize);
}
}
@@ -424,7 +444,7 @@ export class SplitView implements IDisposable {
}
size = typeof size === 'number' ? size : item.size;
size = clamp(size, item.view.minimumSize, item.view.maximumSize);
size = SplitView.clamp(item, size);
if (this.inverseAltBehavior && index > 0) {
// In this case, we want the view to grow or shrink both sides equally
@@ -499,8 +519,8 @@ export class SplitView implements IDisposable {
index: number,
delta: number,
sizes = this.viewItems.map(i => i.size),
lowPriorityIndex?: number,
highPriorityIndex?: number,
lowPriorityIndexes?: number[],
highPriorityIndexes?: number[],
overloadMinDelta: number = Number.NEGATIVE_INFINITY,
overloadMaxDelta: number = Number.POSITIVE_INFINITY
): number {
@@ -511,14 +531,18 @@ export class SplitView implements IDisposable {
const upIndexes = range(index, -1);
const downIndexes = range(index + 1, this.viewItems.length);
if (typeof highPriorityIndex === 'number') {
pushToStart(upIndexes, highPriorityIndex);
pushToStart(downIndexes, highPriorityIndex);
if (highPriorityIndexes) {
for (const index of highPriorityIndexes) {
pushToStart(upIndexes, index);
pushToStart(downIndexes, index);
}
}
if (typeof lowPriorityIndex === 'number') {
pushToEnd(upIndexes, lowPriorityIndex);
pushToEnd(downIndexes, lowPriorityIndex);
if (lowPriorityIndexes) {
for (const index of lowPriorityIndexes) {
pushToEnd(upIndexes, index);
pushToEnd(downIndexes, index);
}
}
const upItems = upIndexes.map(i => this.viewItems[i]);
@@ -527,27 +551,29 @@ export class SplitView implements IDisposable {
const downItems = downIndexes.map(i => this.viewItems[i]);
const downSizes = downIndexes.map(i => sizes[i]);
const minDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].view.minimumSize - sizes[i]), 0);
const minDeltaUp = upIndexes.reduce((r, i) => r + ((typeof this.viewItems[i].view.snapSize === 'number' ? 0 : 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] - this.viewItems[i].view.minimumSize), 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 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);
delta = clamp(delta, minDelta, maxDelta);
const tentativeDelta = clamp(delta, minDelta, maxDelta);
let actualDelta = 0;
for (let i = 0, deltaUp = delta; i < upItems.length; i++) {
for (let i = 0, deltaUp = tentativeDelta; i < upItems.length; i++) {
const item = upItems[i];
const size = clamp(upSizes[i] + deltaUp, item.view.minimumSize, item.view.maximumSize);
const size = SplitView.clamp(item, upSizes[i] + deltaUp/* , upIndexes[i] === index */);
const viewDelta = size - upSizes[i];
actualDelta += viewDelta;
deltaUp -= viewDelta;
item.size = size;
}
for (let i = 0, deltaDown = delta; i < downItems.length; i++) {
for (let i = 0, deltaDown = actualDelta; i < downItems.length; i++) {
const item = downItems[i];
const size = clamp(downSizes[i] - deltaDown, item.view.minimumSize, item.view.maximumSize);
const size = SplitView.clamp(item, downSizes[i] - deltaDown);
const viewDelta = size - downSizes[i];
deltaDown += viewDelta;
@@ -557,13 +583,24 @@ export class SplitView implements IDisposable {
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 = clamp(item.size + emptyDelta, item.view.minimumSize, item.view.maximumSize);
const size = SplitView.clamp(item, item.size + emptyDelta);
const viewDelta = size - item.size;
emptyDelta -= viewDelta;
@@ -626,6 +663,8 @@ export class SplitView implements IDisposable {
}
dispose(): void {
super.dispose();
this.viewItems.forEach(i => i.disposable.dispose());
this.viewItems = [];

View File

@@ -3,16 +3,14 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import 'vs/css!./toolbar';
import * as nls from 'vs/nls';
import { TPromise } from 'vs/base/common/winjs.base';
import { Action, IActionRunner, IAction } from 'vs/base/common/actions';
import { ActionBar, ActionsOrientation, IActionItemProvider } from 'vs/base/browser/ui/actionbar/actionbar';
import { IContextMenuProvider, DropdownMenuActionItem } from 'vs/base/browser/ui/dropdown/dropdown';
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
import { Disposable } from 'vs/base/common/lifecycle';
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
export const CONTEXT = 'context.toolbar';
@@ -22,6 +20,8 @@ export interface IToolBarOptions {
ariaLabel?: string;
getKeyBinding?: (action: IAction) => ResolvedKeybinding;
actionRunner?: IActionRunner;
toggleMenuTitle?: string;
anchorAlignmentProvider?: () => AnchorAlignment;
}
/**
@@ -41,7 +41,7 @@ export class ToolBar extends Disposable {
this.options = options;
this.lookupKeybindings = typeof this.options.getKeyBinding === 'function';
this.toggleMenuAction = this._register(new ToggleMenuAction(() => this.toggleMenuActionItem && this.toggleMenuActionItem.show()));
this.toggleMenuAction = this._register(new ToggleMenuAction(() => this.toggleMenuActionItem && this.toggleMenuActionItem.show(), options.toggleMenuTitle));
let element = document.createElement('div');
element.className = 'monaco-toolbar';
@@ -69,7 +69,8 @@ export class ToolBar extends Disposable {
this.options.actionItemProvider,
this.actionRunner,
this.options.getKeyBinding,
'toolbar-toggle-more'
'toolbar-toggle-more',
this.options.anchorAlignmentProvider
);
this.toggleMenuActionItem.setActionContext(this.actionBar.context);
@@ -170,16 +171,17 @@ class ToggleMenuAction extends Action {
private _menuActions: IAction[];
private toggleDropdownMenu: () => void;
constructor(toggleDropdownMenu: () => void) {
super(ToggleMenuAction.ID, nls.localize('moreActions', "More Actions..."), null, true);
constructor(toggleDropdownMenu: () => void, title?: string) {
title = title || nls.localize('moreActions', "More Actions...");
super(ToggleMenuAction.ID, title, null, true);
this.toggleDropdownMenu = toggleDropdownMenu;
}
run(): TPromise<any> {
run(): Promise<any> {
this.toggleDropdownMenu();
return TPromise.as(true);
return Promise.resolve(true);
}
get menuActions() {

View File

@@ -0,0 +1,507 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/tree';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IListOptions, List, IMultipleSelectionController, IListStyles, IAccessibilityProvider } from 'vs/base/browser/ui/list/listWidget';
import { IListVirtualDelegate, IListRenderer, IListMouseEvent, IListEvent, IListContextMenuEvent, IIdentityProvider } from 'vs/base/browser/ui/list/list';
import { append, $, toggleClass } from 'vs/base/browser/dom';
import { Event, Relay, chain, mapEvent } from 'vs/base/common/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { ITreeModel, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent, ITreeFilter } from 'vs/base/browser/ui/tree/tree';
import { ISpliceable } from 'vs/base/common/sequence';
function asListOptions<T, TFilterData>(options?: IAbstractTreeOptions<T, TFilterData>): IListOptions<ITreeNode<T, TFilterData>> | undefined {
if (!options) {
return undefined;
}
let identityProvider: IIdentityProvider<ITreeNode<T, TFilterData>> | undefined = undefined;
if (options.identityProvider) {
const ip = options.identityProvider;
identityProvider = {
getId(el) {
return ip.getId(el.element);
}
};
}
let multipleSelectionController: IMultipleSelectionController<ITreeNode<T, TFilterData>> | undefined = undefined;
if (options.multipleSelectionController) {
const msc = options.multipleSelectionController;
multipleSelectionController = {
isSelectionSingleChangeEvent(e) {
return msc.isSelectionSingleChangeEvent({ ...e, element: e.element } as any);
},
isSelectionRangeChangeEvent(e) {
return msc.isSelectionRangeChangeEvent({ ...e, element: e.element } as any);
}
};
}
let accessibilityProvider: IAccessibilityProvider<ITreeNode<T, TFilterData>> | undefined = undefined;
if (options.accessibilityProvider) {
const ap = options.accessibilityProvider;
accessibilityProvider = {
getAriaLabel(e) {
return ap.getAriaLabel(e.element);
}
};
}
return {
...options,
identityProvider,
multipleSelectionController,
accessibilityProvider
};
}
export class ComposedTreeDelegate<T, N extends { element: T }> implements IListVirtualDelegate<N> {
constructor(private delegate: IListVirtualDelegate<T>) { }
getHeight(element: N): number {
return this.delegate.getHeight(element.element);
}
getTemplateId(element: N): string {
return this.delegate.getTemplateId(element.element);
}
hasDynamicHeight(element: N): boolean {
return !!this.delegate.hasDynamicHeight && this.delegate.hasDynamicHeight(element.element);
}
}
interface ITreeListTemplateData<T> {
twistie: HTMLElement;
templateData: T;
}
class TreeRenderer<T, TFilterData, TTemplateData> implements IListRenderer<ITreeNode<T, TFilterData>, ITreeListTemplateData<TTemplateData>> {
readonly templateId: string;
private renderedElements = new Map<T, ITreeNode<T, TFilterData>>();
private renderedNodes = new Map<ITreeNode<T, TFilterData>, ITreeListTemplateData<TTemplateData>>();
private disposables: IDisposable[] = [];
constructor(
private renderer: ITreeRenderer<T, TFilterData, TTemplateData>,
onDidChangeCollapseState: Event<ITreeNode<T, TFilterData>>
) {
this.templateId = renderer.templateId;
onDidChangeCollapseState(this.onDidChangeNodeTwistieState, this, this.disposables);
if (renderer.onDidChangeTwistieState) {
renderer.onDidChangeTwistieState(this.onDidChangeTwistieState, this, this.disposables);
}
}
renderTemplate(container: HTMLElement): ITreeListTemplateData<TTemplateData> {
const el = append(container, $('.monaco-tl-row'));
const twistie = append(el, $('.monaco-tl-twistie'));
const contents = append(el, $('.monaco-tl-contents'));
const templateData = this.renderer.renderTemplate(contents);
return { twistie, templateData };
}
renderElement(node: ITreeNode<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>): void {
this.renderedNodes.set(node, templateData);
this.renderedElements.set(node.element, node);
templateData.twistie.style.width = `${10 + node.depth * 10}px`;
this.renderTwistie(node, templateData.twistie);
this.renderer.renderElement(node, index, templateData.templateData);
}
disposeElement(node: ITreeNode<T, TFilterData>, index: number, templateData: ITreeListTemplateData<TTemplateData>): void {
this.renderer.disposeElement(node, index, templateData.templateData);
this.renderedNodes.delete(node);
this.renderedElements.set(node.element);
}
disposeTemplate(templateData: ITreeListTemplateData<TTemplateData>): void {
this.renderer.disposeTemplate(templateData.templateData);
}
private onDidChangeTwistieState(element: T): void {
const node = this.renderedElements.get(element);
if (!node) {
return;
}
this.onDidChangeNodeTwistieState(node);
}
private onDidChangeNodeTwistieState(node: ITreeNode<T, TFilterData>): void {
const templateData = this.renderedNodes.get(node);
if (!templateData) {
return;
}
this.renderTwistie(node, templateData.twistie);
}
private renderTwistie(node: ITreeNode<T, TFilterData>, twistieElement: HTMLElement) {
if (this.renderer.renderTwistie) {
this.renderer.renderTwistie(node.element, twistieElement);
}
toggleClass(twistieElement, 'collapsible', node.collapsible);
toggleClass(twistieElement, 'collapsed', node.collapsible && node.collapsed);
}
dispose(): void {
this.renderedNodes.clear();
this.renderedElements.clear();
this.disposables = dispose(this.disposables);
}
}
function isInputElement(e: HTMLElement): boolean {
return e.tagName === 'INPUT' || e.tagName === 'TEXTAREA';
}
function asTreeEvent<T>(event: IListEvent<ITreeNode<T, any>>): ITreeEvent<T> {
return {
elements: event.elements.map(node => node.element),
browserEvent: event.browserEvent
};
}
function asTreeMouseEvent<T>(event: IListMouseEvent<ITreeNode<T, any>>): ITreeMouseEvent<T> {
return {
browserEvent: event.browserEvent,
element: event.element ? event.element.element : null
};
}
function asTreeContextMenuEvent<T>(event: IListContextMenuEvent<ITreeNode<T, any>>): ITreeContextMenuEvent<T> {
return {
element: event.element ? event.element.element : null,
browserEvent: event.browserEvent,
anchor: event.anchor
};
}
export interface IAbstractTreeOptions<T, TFilterData = void> extends IListOptions<T> {
filter?: ITreeFilter<T, TFilterData>;
}
export abstract class AbstractTree<T, TFilterData, TRef> implements IDisposable {
private view: List<ITreeNode<T, TFilterData>>;
protected model: ITreeModel<T, TFilterData, TRef>;
protected disposables: IDisposable[] = [];
get onDidChangeFocus(): Event<ITreeEvent<T>> { return mapEvent(this.view.onFocusChange, asTreeEvent); }
get onDidChangeSelection(): Event<ITreeEvent<T>> { return mapEvent(this.view.onSelectionChange, asTreeEvent); }
get onMouseClick(): Event<ITreeMouseEvent<T>> { return mapEvent(this.view.onMouseClick, asTreeMouseEvent); }
get onMouseDblClick(): Event<ITreeMouseEvent<T>> { return mapEvent(this.view.onMouseDblClick, asTreeMouseEvent); }
get onContextMenu(): Event<ITreeContextMenuEvent<T>> { return mapEvent(this.view.onContextMenu, asTreeContextMenuEvent); }
get onDidFocus(): Event<void> { return this.view.onDidFocus; }
get onDidBlur(): Event<void> { return this.view.onDidBlur; }
get onDidChangeCollapseState(): Event<ITreeNode<T, TFilterData>> { return this.model.onDidChangeCollapseState; }
get onDidChangeRenderNodeCount(): Event<ITreeNode<T, TFilterData>> { return this.model.onDidChangeRenderNodeCount; }
get onDidDispose(): Event<void> { return this.view.onDidDispose; }
constructor(
container: HTMLElement,
delegate: IListVirtualDelegate<T>,
renderers: ITreeRenderer<any /* TODO@joao */, TFilterData, any>[],
options: IAbstractTreeOptions<T, TFilterData> = {}
) {
const treeDelegate = new ComposedTreeDelegate<T, ITreeNode<T, TFilterData>>(delegate);
const onDidChangeCollapseStateRelay = new Relay<ITreeNode<T, TFilterData>>();
const treeRenderers = renderers.map(r => new TreeRenderer<T, TFilterData, any>(r, onDidChangeCollapseStateRelay.event));
this.disposables.push(...treeRenderers);
this.view = new List(container, treeDelegate, treeRenderers, asListOptions(options));
this.model = this.createModel(this.view, options);
onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState;
this.view.onMouseClick(this.reactOnMouseClick, this, this.disposables);
if (options.keyboardSupport !== false) {
const onKeyDown = chain(this.view.onKeyDown)
.filter(e => !isInputElement(e.target as HTMLElement))
.map(e => new StandardKeyboardEvent(e));
onKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow).on(this.onLeftArrow, this, this.disposables);
onKeyDown.filter(e => e.keyCode === KeyCode.RightArrow).on(this.onRightArrow, this, this.disposables);
onKeyDown.filter(e => e.keyCode === KeyCode.Space).on(this.onSpace, this, this.disposables);
}
}
// Widget
getHTMLElement(): HTMLElement {
return this.view.getHTMLElement();
}
get contentHeight(): number {
return this.view.contentHeight;
}
get onDidChangeContentHeight(): Event<number> {
return this.view.onDidChangeContentHeight;
}
get scrollTop(): number {
return this.view.scrollTop;
}
set scrollTop(scrollTop: number) {
this.view.scrollTop = scrollTop;
}
get scrollHeight(): number {
return this.view.scrollHeight;
}
get renderHeight(): number {
return this.view.renderHeight;
}
domFocus(): void {
this.view.domFocus();
}
layout(height?: number): void {
this.view.layout(height);
}
layoutWidth(width: number): void {
this.view.layoutWidth(width);
}
style(styles: IListStyles): void {
this.view.style(styles);
}
// Tree navigation
getParentElement(location: TRef): T {
return this.model.getParentElement(location);
}
getFirstElementChild(location: TRef): T | undefined {
return this.model.getFirstElementChild(location);
}
getLastElementAncestor(location?: TRef): T | undefined {
return this.model.getLastElementAncestor(location);
}
// Tree
getNode(location?: TRef): ITreeNode<T, TFilterData> {
return this.model.getNode(location);
}
collapse(location: TRef): boolean {
return this.model.setCollapsed(location, true);
}
expand(location: TRef): boolean {
return this.model.setCollapsed(location, false);
}
toggleCollapsed(location: TRef): void {
this.model.toggleCollapsed(location);
}
collapseAll(): void {
this.model.collapseAll();
}
isCollapsible(location: TRef): boolean {
return this.model.isCollapsible(location);
}
isCollapsed(location: TRef): boolean {
return this.model.isCollapsed(location);
}
isExpanded(location: TRef): boolean {
return !this.isCollapsed(location);
}
refilter(): void {
this.model.refilter();
}
setSelection(elements: TRef[], browserEvent?: UIEvent): void {
const indexes = elements.map(e => this.model.getListIndex(e));
this.view.setSelection(indexes, browserEvent);
}
getSelection(): T[] {
const nodes = this.view.getSelectedElements();
return nodes.map(n => n.element);
}
setFocus(elements: TRef[], browserEvent?: UIEvent): void {
const indexes = elements.map(e => this.model.getListIndex(e));
this.view.setFocus(indexes, browserEvent);
}
focusNext(n = 1, loop = false, browserEvent?: UIEvent): void {
this.view.focusNext(n, loop, browserEvent);
}
focusPrevious(n = 1, loop = false, browserEvent?: UIEvent): void {
this.view.focusPrevious(n, loop, browserEvent);
}
focusNextPage(browserEvent?: UIEvent): void {
this.view.focusNextPage(browserEvent);
}
focusPreviousPage(browserEvent?: UIEvent): void {
this.view.focusPreviousPage(browserEvent);
}
focusLast(browserEvent?: UIEvent): void {
this.view.focusLast(browserEvent);
}
focusFirst(browserEvent?: UIEvent): void {
this.view.focusFirst(browserEvent);
}
getFocus(): T[] {
const nodes = this.view.getFocusedElements();
return nodes.map(n => n.element);
}
open(elements: TRef[]): void {
const indexes = elements.map(e => this.model.getListIndex(e));
this.view.open(indexes);
}
reveal(location: TRef, relativeTop?: number): void {
const index = this.model.getListIndex(location);
this.view.reveal(index, relativeTop);
}
/**
* Returns the relative position of an element rendered in the list.
* Returns `null` if the element isn't *entirely* in the visible viewport.
*/
getRelativeTop(location: TRef): number | null {
const index = this.model.getListIndex(location);
return this.view.getRelativeTop(index);
}
// List
get visibleNodeCount(): number {
return this.view.length;
}
private reactOnMouseClick(e: IListMouseEvent<ITreeNode<T, TFilterData>>): void {
const node = e.element;
if (!node) {
return;
}
const location = this.model.getNodeLocation(node);
this.model.toggleCollapsed(location);
}
private onLeftArrow(e: StandardKeyboardEvent): void {
e.preventDefault();
e.stopPropagation();
const nodes = this.view.getFocusedElements();
if (nodes.length === 0) {
return;
}
const node = nodes[0];
const location = this.model.getNodeLocation(node);
const didChange = this.model.setCollapsed(location, true);
if (!didChange) {
const parentLocation = this.model.getParentNodeLocation(location);
if (parentLocation === null) {
return;
}
const parentListIndex = this.model.getListIndex(parentLocation);
this.view.reveal(parentListIndex);
this.view.setFocus([parentListIndex]);
}
}
private onRightArrow(e: StandardKeyboardEvent): void {
e.preventDefault();
e.stopPropagation();
const nodes = this.view.getFocusedElements();
if (nodes.length === 0) {
return;
}
const node = nodes[0];
const location = this.model.getNodeLocation(node);
const didChange = this.model.setCollapsed(location, false);
if (!didChange) {
if (node.children.length === 0) {
return;
}
const [focusedIndex] = this.view.getFocus();
const firstChildIndex = focusedIndex + 1;
this.view.reveal(firstChildIndex);
this.view.setFocus([firstChildIndex]);
}
}
private onSpace(e: StandardKeyboardEvent): void {
e.preventDefault();
e.stopPropagation();
const nodes = this.view.getFocusedElements();
if (nodes.length === 0) {
return;
}
const node = nodes[0];
const location = this.model.getNodeLocation(node);
this.model.toggleCollapsed(location);
}
protected abstract createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IAbstractTreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, TRef>;
dispose(): void {
this.disposables = dispose(this.disposables);
this.view.dispose();
}
}

View File

@@ -0,0 +1,588 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ComposedTreeDelegate, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abstractTree';
import { ObjectTree, IObjectTreeOptions } from 'vs/base/browser/ui/tree/objectTree';
import { IListVirtualDelegate, IIdentityProvider } from 'vs/base/browser/ui/list/list';
import { ITreeElement, ITreeNode, ITreeRenderer, ITreeEvent, ITreeMouseEvent, ITreeContextMenuEvent } from 'vs/base/browser/ui/tree/tree';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { Emitter, Event, mapEvent } from 'vs/base/common/event';
import { timeout, always } from 'vs/base/common/async';
import { IListStyles } from 'vs/base/browser/ui/list/listWidget';
import { toggleClass } from 'vs/base/browser/dom';
import { Iterator } from 'vs/base/common/iterator';
export interface IDataSource<T extends NonNullable<any>> {
hasChildren(element: T | null): boolean;
getChildren(element: T | null): Thenable<T[]>;
}
enum AsyncDataTreeNodeState {
Uninitialized,
Loaded,
Loading,
Slow
}
interface IAsyncDataTreeNode<T extends NonNullable<any>> {
element: T | null;
readonly parent: IAsyncDataTreeNode<T> | null;
readonly id?: string | null;
readonly children?: IAsyncDataTreeNode<T>[];
state: AsyncDataTreeNodeState;
}
interface IDataTreeListTemplateData<T> {
templateData: T;
}
class AsyncDataTreeNodeWrapper<T, TFilterData> implements ITreeNode<T, TFilterData> {
get element(): T { return this.node.element!.element!; }
get parent(): ITreeNode<T, TFilterData> | undefined { return this.node.parent && new AsyncDataTreeNodeWrapper(this.node.parent); }
get children(): ITreeNode<T, TFilterData>[] { return this.node.children.map(node => new AsyncDataTreeNodeWrapper(node)); }
get depth(): number { return this.node.depth; }
get collapsible(): boolean { return this.node.collapsible; }
get collapsed(): boolean { return this.node.collapsed; }
get visible(): boolean { return this.node.visible; }
get filterData(): TFilterData | undefined { return this.node.filterData; }
constructor(private node: ITreeNode<IAsyncDataTreeNode<T> | null, TFilterData>) { }
}
class DataTreeRenderer<T, TFilterData, TTemplateData> implements ITreeRenderer<IAsyncDataTreeNode<T>, TFilterData, IDataTreeListTemplateData<TTemplateData>> {
readonly templateId: string;
private renderedNodes = new Map<IAsyncDataTreeNode<T>, IDataTreeListTemplateData<TTemplateData>>();
private disposables: IDisposable[] = [];
constructor(
private renderer: ITreeRenderer<T, TFilterData, TTemplateData>,
readonly onDidChangeTwistieState: Event<IAsyncDataTreeNode<T>>
) {
this.templateId = renderer.templateId;
}
renderTemplate(container: HTMLElement): IDataTreeListTemplateData<TTemplateData> {
const templateData = this.renderer.renderTemplate(container);
return { templateData };
}
renderElement(node: ITreeNode<IAsyncDataTreeNode<T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>): void {
this.renderer.renderElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData);
}
renderTwistie(element: IAsyncDataTreeNode<T>, twistieElement: HTMLElement): boolean {
toggleClass(twistieElement, 'loading', element.state === AsyncDataTreeNodeState.Slow);
return false;
}
disposeElement(node: ITreeNode<IAsyncDataTreeNode<T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>): void {
this.renderer.disposeElement(new AsyncDataTreeNodeWrapper(node), index, templateData.templateData);
}
disposeTemplate(templateData: IDataTreeListTemplateData<TTemplateData>): void {
this.renderer.disposeTemplate(templateData.templateData);
}
dispose(): void {
this.renderedNodes.clear();
this.disposables = dispose(this.disposables);
}
}
function asTreeEvent<T>(e: ITreeEvent<IAsyncDataTreeNode<T>>): ITreeEvent<T> {
return {
browserEvent: e.browserEvent,
elements: e.elements.map(e => e.element!)
};
}
function asTreeMouseEvent<T>(e: ITreeMouseEvent<IAsyncDataTreeNode<T>>): ITreeMouseEvent<T> {
return {
browserEvent: e.browserEvent,
element: e.element && e.element.element!
};
}
function asTreeContextMenuEvent<T>(e: ITreeContextMenuEvent<IAsyncDataTreeNode<T>>): ITreeContextMenuEvent<T> {
return {
browserEvent: e.browserEvent,
element: e.element && e.element.element!,
anchor: e.anchor
};
}
export enum ChildrenResolutionReason {
Refresh,
Expand
}
export interface IChildrenResolutionEvent<T> {
readonly element: T | null;
readonly reason: ChildrenResolutionReason;
}
function asObjectTreeOptions<T, TFilterData>(options?: IAsyncDataTreeOptions<T, TFilterData>): IObjectTreeOptions<IAsyncDataTreeNode<T>, TFilterData> | undefined {
return options && {
...options,
identityProvider: options.identityProvider && {
getId(el) {
return options.identityProvider!.getId(el.element!);
}
},
multipleSelectionController: options.multipleSelectionController && {
isSelectionSingleChangeEvent(e) {
return options.multipleSelectionController!.isSelectionSingleChangeEvent({ ...e, element: e.element } as any);
},
isSelectionRangeChangeEvent(e) {
return options.multipleSelectionController!.isSelectionRangeChangeEvent({ ...e, element: e.element } as any);
}
},
accessibilityProvider: options.accessibilityProvider && {
getAriaLabel(e) {
return options.accessibilityProvider!.getAriaLabel(e.element!);
}
},
filter: options.filter && {
filter(element, parentVisibility) {
return options.filter!.filter(element.element!, parentVisibility);
}
}
};
}
function asTreeElement<T>(node: IAsyncDataTreeNode<T>): ITreeElement<IAsyncDataTreeNode<T>> {
return {
element: node,
children: Iterator.map(Iterator.fromArray(node.children!), asTreeElement)
};
}
export interface IAsyncDataTreeOptions<T, TFilterData = void> extends IAbstractTreeOptions<T, TFilterData> {
identityProvider?: IIdentityProvider<T>;
}
export class AsyncDataTree<T extends NonNullable<any>, TFilterData = void> implements IDisposable {
private readonly tree: ObjectTree<IAsyncDataTreeNode<T>, TFilterData>;
private readonly root: IAsyncDataTreeNode<T>;
private readonly nodes = new Map<T | null, IAsyncDataTreeNode<T>>();
private readonly refreshPromises = new Map<IAsyncDataTreeNode<T>, Thenable<void>>();
private readonly identityProvider?: IIdentityProvider<T>;
private readonly _onDidChangeNodeState = new Emitter<IAsyncDataTreeNode<T>>();
protected readonly disposables: IDisposable[] = [];
get onDidChangeFocus(): Event<ITreeEvent<T>> { return mapEvent(this.tree.onDidChangeFocus, asTreeEvent); }
get onDidChangeSelection(): Event<ITreeEvent<T>> { return mapEvent(this.tree.onDidChangeSelection, asTreeEvent); }
get onDidChangeCollapseState(): Event<T> { return mapEvent(this.tree.onDidChangeCollapseState, e => e.element!.element!); }
private readonly _onDidResolveChildren = new Emitter<IChildrenResolutionEvent<T>>();
readonly onDidResolveChildren: Event<IChildrenResolutionEvent<T>> = this._onDidResolveChildren.event;
get onMouseClick(): Event<ITreeMouseEvent<T>> { return mapEvent(this.tree.onMouseClick, asTreeMouseEvent); }
get onMouseDblClick(): Event<ITreeMouseEvent<T>> { return mapEvent(this.tree.onMouseDblClick, asTreeMouseEvent); }
get onContextMenu(): Event<ITreeContextMenuEvent<T>> { return mapEvent(this.tree.onContextMenu, asTreeContextMenuEvent); }
get onDidFocus(): Event<void> { return this.tree.onDidFocus; }
get onDidBlur(): Event<void> { return this.tree.onDidBlur; }
get onDidDispose(): Event<void> { return this.tree.onDidDispose; }
constructor(
container: HTMLElement,
delegate: IListVirtualDelegate<T>,
renderers: ITreeRenderer<any /* TODO@joao */, TFilterData, any>[],
private dataSource: IDataSource<T>,
options?: IAsyncDataTreeOptions<T, TFilterData>
) {
this.identityProvider = options && options.identityProvider;
const objectTreeDelegate = new ComposedTreeDelegate<T | null, IAsyncDataTreeNode<T>>(delegate);
const objectTreeRenderers = renderers.map(r => new DataTreeRenderer(r, this._onDidChangeNodeState.event));
const objectTreeOptions = asObjectTreeOptions(options) || {};
objectTreeOptions.collapseByDefault = true;
this.tree = new ObjectTree(container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions);
this.root = {
element: null,
parent: null,
state: AsyncDataTreeNodeState.Uninitialized,
};
if (this.identityProvider) {
this.root = {
...this.root,
id: null,
children: [],
};
}
this.nodes.set(null, this.root);
this.tree.onDidChangeCollapseState(this._onDidChangeCollapseState, this, this.disposables);
}
// Widget
getHTMLElement(): HTMLElement {
return this.tree.getHTMLElement();
}
get contentHeight(): number {
return this.tree.contentHeight;
}
get onDidChangeContentHeight(): Event<number> {
return this.tree.onDidChangeContentHeight;
}
get scrollTop(): number {
return this.tree.scrollTop;
}
set scrollTop(scrollTop: number) {
this.tree.scrollTop = scrollTop;
}
get scrollHeight(): number {
return this.tree.scrollHeight;
}
get renderHeight(): number {
return this.tree.renderHeight;
}
domFocus(): void {
this.tree.domFocus();
}
layout(height?: number): void {
this.tree.layout(height);
}
style(styles: IListStyles): void {
this.tree.style(styles);
}
// Data Tree
refresh(element: T | null, recursive = true): Thenable<void> {
return this.refreshNode(this.getDataNode(element), recursive, ChildrenResolutionReason.Refresh);
}
// Tree
getNode(element: T | null): ITreeNode<T | null, TFilterData> {
const dataNode = this.getDataNode(element);
const node = this.tree.getNode(dataNode === this.root ? null : dataNode);
return new AsyncDataTreeNodeWrapper<T | null, TFilterData>(node);
}
collapse(element: T): boolean {
return this.tree.collapse(this.getDataNode(element));
}
async expand(element: T): Promise<boolean> {
const node = this.getDataNode(element);
if (!this.tree.isCollapsed(node)) {
return false;
}
this.tree.expand(node);
if (node.state !== AsyncDataTreeNodeState.Loaded) {
await this.refreshNode(node, false, ChildrenResolutionReason.Expand);
}
return true;
}
toggleCollapsed(element: T): void {
this.tree.toggleCollapsed(this.getDataNode(element));
}
collapseAll(): void {
this.tree.collapseAll();
}
isCollapsible(element: T): boolean {
return this.tree.isCollapsible(this.getDataNode(element));
}
isCollapsed(element: T): boolean {
return this.tree.isCollapsed(this.getDataNode(element));
}
isExpanded(element: T): boolean {
return this.tree.isExpanded(this.getDataNode(element));
}
refilter(): void {
this.tree.refilter();
}
setSelection(elements: T[], browserEvent?: UIEvent): void {
const nodes = elements.map(e => this.getDataNode(e));
this.tree.setSelection(nodes, browserEvent);
}
getSelection(): T[] {
const nodes = this.tree.getSelection();
return nodes.map(n => n!.element!);
}
setFocus(elements: T[], browserEvent?: UIEvent): void {
const nodes = elements.map(e => this.getDataNode(e));
this.tree.setFocus(nodes, browserEvent);
}
focusNext(n = 1, loop = false, browserEvent?: UIEvent): void {
this.tree.focusNext(n, loop, browserEvent);
}
focusPrevious(n = 1, loop = false, browserEvent?: UIEvent): void {
this.tree.focusPrevious(n, loop, browserEvent);
}
focusNextPage(browserEvent?: UIEvent): void {
this.tree.focusNextPage(browserEvent);
}
focusPreviousPage(browserEvent?: UIEvent): void {
this.tree.focusPreviousPage(browserEvent);
}
focusLast(browserEvent?: UIEvent): void {
this.tree.focusLast(browserEvent);
}
focusFirst(browserEvent?: UIEvent): void {
this.tree.focusFirst(browserEvent);
}
getFocus(): T[] {
const nodes = this.tree.getFocus();
return nodes.map(n => n!.element!);
}
open(elements: T[]): void {
const nodes = elements.map(e => this.getDataNode(e));
this.tree.open(nodes);
}
reveal(element: T, relativeTop?: number): void {
this.tree.reveal(this.getDataNode(element), relativeTop);
}
getRelativeTop(element: T): number | null {
return this.tree.getRelativeTop(this.getDataNode(element));
}
// Tree navigation
getParentElement(element: T): T | null {
const node = this.tree.getParentElement(this.getDataNode(element));
return node && node.element;
}
getFirstElementChild(element: T | null = null): T | null | undefined {
const dataNode = this.getDataNode(element);
const node = this.tree.getFirstElementChild(dataNode === this.root ? null : dataNode);
return node && node.element;
}
getLastElementAncestor(element: T | null = null): T | null | undefined {
const dataNode = this.getDataNode(element);
const node = this.tree.getLastElementAncestor(dataNode === this.root ? null : dataNode);
return node && node.element;
}
// List
get visibleNodeCount(): number {
return this.tree.visibleNodeCount;
}
// Implementation
private getDataNode(element: T | null): IAsyncDataTreeNode<T> {
const node: IAsyncDataTreeNode<T> = this.nodes.get(element);
if (typeof node === 'undefined') {
throw new Error(`Data tree node not found: ${element}`);
}
return node;
}
private async refreshNode(node: IAsyncDataTreeNode<T>, recursive: boolean, reason: ChildrenResolutionReason): Promise<void> {
await this._refreshNode(node, recursive, reason);
if (recursive && node.children) {
await Promise.all(node.children.map(child => this.refreshNode(child, recursive, reason)));
}
}
private _refreshNode(node: IAsyncDataTreeNode<T>, recursive: boolean, reason: ChildrenResolutionReason): Thenable<void> {
let result = this.refreshPromises.get(node);
if (result) {
return result;
}
result = this.doRefresh(node, recursive, reason);
this.refreshPromises.set(node, result);
return always(result, () => this.refreshPromises.delete(node));
}
private doRefresh(node: IAsyncDataTreeNode<T>, recursive: boolean, reason: ChildrenResolutionReason): Thenable<void> {
const hasChildren = !!this.dataSource.hasChildren(node.element);
if (!hasChildren) {
this.setChildren(node, [], recursive);
return Promise.resolve();
} else if (node !== this.root && (!this.tree.isCollapsible(node) || this.tree.isCollapsed(node))) {
return Promise.resolve();
} else {
node.state = AsyncDataTreeNodeState.Loading;
this._onDidChangeNodeState.fire(node);
const slowTimeout = timeout(800);
slowTimeout.then(() => {
node.state = AsyncDataTreeNodeState.Slow;
this._onDidChangeNodeState.fire(node);
}, _ => null);
return this.dataSource.getChildren(node.element)
.then(children => {
slowTimeout.cancel();
node.state = AsyncDataTreeNodeState.Loaded;
this._onDidChangeNodeState.fire(node);
this.setChildren(node, children, recursive);
this._onDidResolveChildren.fire({ element: node.element, reason });
}, err => {
slowTimeout.cancel();
node.state = AsyncDataTreeNodeState.Uninitialized;
this._onDidChangeNodeState.fire(node);
if (node !== this.root) {
this.tree.collapse(node);
}
return Promise.reject(err);
});
}
}
private _onDidChangeCollapseState(treeNode: ITreeNode<IAsyncDataTreeNode<T>, any>): void {
if (!treeNode.collapsed && treeNode.element.state === AsyncDataTreeNodeState.Uninitialized) {
this.refreshNode(treeNode.element, false, ChildrenResolutionReason.Expand);
}
}
private setChildren(node: IAsyncDataTreeNode<T>, childrenElements: T[], recursive: boolean): void {
const children = childrenElements.map<ITreeElement<IAsyncDataTreeNode<T>>>(element => {
if (!this.identityProvider) {
return {
element: {
element,
parent: node,
state: AsyncDataTreeNodeState.Uninitialized
},
collapsible: !!this.dataSource.hasChildren(element),
collapsed: true
};
}
const nodeChildren = new Map<string, IAsyncDataTreeNode<T>>();
for (const child of node.children!) {
nodeChildren.set(child.id!, child);
}
const id = this.identityProvider.getId(element).toString();
const asyncDataTreeNode = nodeChildren.get(id);
if (!asyncDataTreeNode) {
return {
element: {
element,
parent: node,
id,
children: [],
state: AsyncDataTreeNodeState.Uninitialized
},
collapsible: !!this.dataSource.hasChildren(element),
collapsed: true
};
}
asyncDataTreeNode.element = element;
const collapsible = !!this.dataSource.hasChildren(element);
const collapsed = !collapsible || this.tree.isCollapsed(asyncDataTreeNode);
if (recursive) {
asyncDataTreeNode.state = AsyncDataTreeNodeState.Uninitialized;
if (this.tree.isCollapsed(asyncDataTreeNode)) {
asyncDataTreeNode.children!.length = 0;
return {
element: asyncDataTreeNode,
collapsible,
collapsed
};
}
}
let children: Iterator<ITreeElement<IAsyncDataTreeNode<T>>> | undefined = undefined;
if (collapsible) {
children = Iterator.map(Iterator.fromArray(asyncDataTreeNode.children!), asTreeElement);
}
return {
element: asyncDataTreeNode,
children,
collapsible,
collapsed
};
});
const insertedElements = new Set<T>();
const onDidCreateNode = (treeNode: ITreeNode<IAsyncDataTreeNode<T>, TFilterData>) => {
if (treeNode.element.element) {
insertedElements.add(treeNode.element.element);
this.nodes.set(treeNode.element.element, treeNode.element);
}
};
const onDidDeleteNode = (treeNode: ITreeNode<IAsyncDataTreeNode<T>, TFilterData>) => {
if (treeNode.element.element) {
if (!insertedElements.has(treeNode.element.element)) {
this.nodes.delete(treeNode.element.element);
}
}
};
this.tree.setChildren(node === this.root ? null : node, children, onDidCreateNode, onDidDeleteNode);
if (this.identityProvider) {
node.children!.splice(0, node.children!.length, ...children.map(c => c.element));
}
}
dispose(): void {
dispose(this.disposables);
}
}

View File

@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./media/tree';
import { Iterator, ISequence } from 'vs/base/common/iterator';
import { AbstractTree, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abstractTree';
import { ISpliceable } from 'vs/base/common/sequence';
import { IndexTreeModel } from 'vs/base/browser/ui/tree/indexTreeModel';
import { ITreeElement, ITreeModel, ITreeNode, ITreeRenderer } from 'vs/base/browser/ui/tree/tree';
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
export interface IIndexTreeOptions<T, TFilterData = void> extends IAbstractTreeOptions<T, TFilterData> {
collapseByDefault?: boolean; // defaults to false
}
export class IndexTree<T, TFilterData = void> extends AbstractTree<T, TFilterData, number[]> {
protected model: IndexTreeModel<T, TFilterData>;
constructor(
container: HTMLElement,
delegate: IListVirtualDelegate<T>,
renderers: ITreeRenderer<any /* TODO@joao */, TFilterData, any>[],
private rootElement: T,
options: IIndexTreeOptions<T, TFilterData> = {}
) {
super(container, delegate, renderers, options);
}
splice(location: number[], deleteCount: number, toInsert: ISequence<ITreeElement<T>> = Iterator.empty()): Iterator<ITreeElement<T>> {
return this.model.splice(location, deleteCount, toInsert);
}
protected createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IIndexTreeOptions<T, TFilterData>): ITreeModel<T, TFilterData, number[]> {
return new IndexTreeModel(view, this.rootElement, options);
}
}

View File

@@ -0,0 +1,467 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ISpliceable } from 'vs/base/common/sequence';
import { Iterator, ISequence } from 'vs/base/common/iterator';
import { Emitter, Event, EventBufferer } from 'vs/base/common/event';
import { tail2 } from 'vs/base/common/arrays';
import { ITreeFilterDataResult, TreeVisibility, ITreeFilter, ITreeModel, ITreeNode, ITreeElement } from 'vs/base/browser/ui/tree/tree';
interface IMutableTreeNode<T, TFilterData> extends ITreeNode<T, TFilterData> {
readonly parent: IMutableTreeNode<T, TFilterData> | undefined;
readonly children: IMutableTreeNode<T, TFilterData>[];
collapsible: boolean;
collapsed: boolean;
renderNodeCount: number;
visible: boolean;
filterData: TFilterData | undefined;
}
function isFilterResult<T>(obj: any): obj is ITreeFilterDataResult<T> {
return typeof obj === 'object' && 'visibility' in obj && 'data' in obj;
}
function treeNodeToElement<T>(node: IMutableTreeNode<T, any>): ITreeElement<T> {
const { element, collapsed } = node;
const children = Iterator.map(Iterator.fromArray(node.children), treeNodeToElement);
return { element, children, collapsed };
}
function getVisibleState(visibility: boolean | TreeVisibility): TreeVisibility {
switch (visibility) {
case true: return TreeVisibility.Visible;
case false: return TreeVisibility.Hidden;
default: return visibility;
}
}
export interface IIndexTreeModelOptions<T, TFilterData> {
collapseByDefault?: boolean; // defaults to false
filter?: ITreeFilter<T, TFilterData>;
}
export class IndexTreeModel<T extends Exclude<any, undefined>, TFilterData = void> implements ITreeModel<T, TFilterData, number[]> {
private root: IMutableTreeNode<T, TFilterData>;
private eventBufferer = new EventBufferer();
private _onDidChangeCollapseState = new Emitter<ITreeNode<T, TFilterData>>();
readonly onDidChangeCollapseState: Event<ITreeNode<T, TFilterData>> = this.eventBufferer.wrapEvent(this._onDidChangeCollapseState.event);
private _onDidChangeRenderNodeCount = new Emitter<ITreeNode<T, TFilterData>>();
readonly onDidChangeRenderNodeCount: Event<ITreeNode<T, TFilterData>> = this.eventBufferer.wrapEvent(this._onDidChangeRenderNodeCount.event);
private collapseByDefault: boolean;
private filter?: ITreeFilter<T, TFilterData>;
constructor(private list: ISpliceable<ITreeNode<T, TFilterData>>, rootElement: T, options: IIndexTreeModelOptions<T, TFilterData> = {}) {
this.collapseByDefault = typeof options.collapseByDefault === 'undefined' ? false : options.collapseByDefault;
this.filter = options.filter;
this.root = {
parent: undefined,
element: rootElement,
children: [],
depth: 0,
collapsible: false,
collapsed: false,
renderNodeCount: 0,
visible: true,
filterData: undefined
};
}
splice(
location: number[],
deleteCount: number,
toInsert?: ISequence<ITreeElement<T>>,
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void,
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
): Iterator<ITreeElement<T>> {
if (location.length === 0) {
throw new Error('Invalid tree location');
}
const { parentNode, listIndex, revealed } = this.getParentNodeWithListIndex(location);
const treeListElementsToInsert: ITreeNode<T, TFilterData>[] = [];
const nodesToInsertIterator = Iterator.map(Iterator.from(toInsert), el => this.createTreeNode(el, parentNode, parentNode.visible ? TreeVisibility.Visible : TreeVisibility.Hidden, revealed, treeListElementsToInsert, onDidCreateNode));
const nodesToInsert: IMutableTreeNode<T, TFilterData>[] = [];
let renderNodeCount = 0;
Iterator.forEach(nodesToInsertIterator, node => {
nodesToInsert.push(node);
renderNodeCount += node.renderNodeCount;
});
const lastIndex = location[location.length - 1];
const deletedNodes = parentNode.children.splice(lastIndex, deleteCount, ...nodesToInsert);
if (revealed) {
const visibleDeleteCount = deletedNodes.reduce((r, node) => r + node.renderNodeCount, 0);
this._updateAncestorsRenderNodeCount(parentNode, renderNodeCount - visibleDeleteCount);
this.list.splice(listIndex, visibleDeleteCount, treeListElementsToInsert);
}
if (deletedNodes.length > 0 && onDidDeleteNode) {
const visit = (node: ITreeNode<T, TFilterData>) => {
onDidDeleteNode(node);
node.children.forEach(visit);
};
deletedNodes.forEach(visit);
}
return Iterator.map(Iterator.fromArray(deletedNodes), treeNodeToElement);
}
getListIndex(location: number[]): number {
return this.getTreeNodeWithListIndex(location).listIndex;
}
setCollapsed(location: number[], collapsed: boolean): boolean {
const { node, listIndex, revealed } = this.getTreeNodeWithListIndex(location);
return this.eventBufferer.bufferEvents(() => this._setCollapsed(node, listIndex, revealed, collapsed));
}
toggleCollapsed(location: number[]): void {
const { node, listIndex, revealed } = this.getTreeNodeWithListIndex(location);
this.eventBufferer.bufferEvents(() => this._setCollapsed(node, listIndex, revealed));
}
collapseAll(): void {
const queue = [...this.root.children];
let listIndex = 0;
this.eventBufferer.bufferEvents(() => {
while (queue.length > 0) {
const node = queue.shift()!;
const revealed = listIndex < this.root.children.length;
this._setCollapsed(node, listIndex, revealed, true);
queue.push(...node.children);
listIndex++;
}
});
}
isCollapsible(location: number[]): boolean {
return this.getTreeNode(location).collapsible;
}
isCollapsed(location: number[]): boolean {
return this.getTreeNode(location).collapsed;
}
refilter(): void {
const previousRenderNodeCount = this.root.renderNodeCount;
const toInsert = this.updateNodeAfterFilterChange(this.root);
this.list.splice(0, previousRenderNodeCount, toInsert);
}
private _setCollapsed(node: IMutableTreeNode<T, TFilterData>, listIndex: number, revealed: boolean, collapsed?: boolean | undefined): boolean {
if (!node.collapsible) {
return false;
}
if (typeof collapsed === 'undefined') {
collapsed = !node.collapsed;
}
if (node.collapsed === collapsed) {
return false;
}
node.collapsed = collapsed;
if (revealed) {
const previousRenderNodeCount = node.renderNodeCount;
const toInsert = this.updateNodeAfterCollapseChange(node);
this.list.splice(listIndex + 1, previousRenderNodeCount - 1, toInsert.slice(1));
this._onDidChangeCollapseState.fire(node);
}
return true;
}
private createTreeNode(
treeElement: ITreeElement<T>,
parent: IMutableTreeNode<T, TFilterData>,
parentVisibility: TreeVisibility,
revealed: boolean,
treeListElements: ITreeNode<T, TFilterData>[],
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void
): IMutableTreeNode<T, TFilterData> {
const node: IMutableTreeNode<T, TFilterData> = {
parent,
element: treeElement.element,
children: [],
depth: parent.depth + 1,
collapsible: typeof treeElement.collapsible === 'boolean' ? treeElement.collapsible : (typeof treeElement.collapsed !== 'undefined'),
collapsed: typeof treeElement.collapsed === 'undefined' ? this.collapseByDefault : treeElement.collapsed,
renderNodeCount: 1,
visible: true,
filterData: undefined
};
const visibility = this._filterNode(node, parentVisibility);
if (revealed) {
treeListElements.push(node);
}
const childElements = Iterator.from(treeElement.children);
const childRevealed = revealed && visibility !== TreeVisibility.Hidden && !node.collapsed;
const childNodes = Iterator.map(childElements, el => this.createTreeNode(el, node, visibility, childRevealed, treeListElements, onDidCreateNode));
let hasVisibleDescendants = false;
let renderNodeCount = 1;
Iterator.forEach(childNodes, child => {
node.children.push(child);
hasVisibleDescendants = hasVisibleDescendants || child.visible;
renderNodeCount += child.renderNodeCount;
});
node.collapsible = node.collapsible || node.children.length > 0;
node.visible = visibility === TreeVisibility.Recurse ? hasVisibleDescendants : (visibility === TreeVisibility.Visible);
if (!node.visible) {
node.renderNodeCount = 0;
if (revealed) {
treeListElements.pop();
}
} else if (!node.collapsed) {
node.renderNodeCount = renderNodeCount;
}
if (onDidCreateNode) {
onDidCreateNode(node);
}
return node;
}
private updateNodeAfterCollapseChange(node: IMutableTreeNode<T, TFilterData>): ITreeNode<T, TFilterData>[] {
const previousRenderNodeCount = node.renderNodeCount;
const result: ITreeNode<T, TFilterData>[] = [];
this._updateNodeAfterCollapseChange(node, result);
this._updateAncestorsRenderNodeCount(node.parent, result.length - previousRenderNodeCount);
return result;
}
private _updateNodeAfterCollapseChange(node: IMutableTreeNode<T, TFilterData>, result: ITreeNode<T, TFilterData>[]): number {
if (node.visible === false) {
return 0;
}
result.push(node);
node.renderNodeCount = 1;
if (!node.collapsed) {
for (const child of node.children) {
node.renderNodeCount += this._updateNodeAfterCollapseChange(child, result);
}
}
this._onDidChangeRenderNodeCount.fire(node);
return node.renderNodeCount;
}
private updateNodeAfterFilterChange(node: IMutableTreeNode<T, TFilterData>): ITreeNode<T, TFilterData>[] {
const previousRenderNodeCount = node.renderNodeCount;
const result: ITreeNode<T, TFilterData>[] = [];
this._updateNodeAfterFilterChange(node, node.visible ? TreeVisibility.Visible : TreeVisibility.Hidden, result);
this._updateAncestorsRenderNodeCount(node.parent, result.length - previousRenderNodeCount);
return result;
}
private _updateNodeAfterFilterChange(node: IMutableTreeNode<T, TFilterData>, parentVisibility: TreeVisibility, result: ITreeNode<T, TFilterData>[], revealed = true): boolean {
let visibility: TreeVisibility;
if (node !== this.root) {
visibility = this._filterNode(node, parentVisibility);
if (visibility === TreeVisibility.Hidden) {
node.visible = false;
return false;
}
if (revealed) {
result.push(node);
}
}
const resultStartLength = result.length;
node.renderNodeCount = node === this.root ? 0 : 1;
let hasVisibleDescendants = false;
if (!node.collapsed || visibility! !== TreeVisibility.Hidden) {
for (const child of node.children) {
hasVisibleDescendants = this._updateNodeAfterFilterChange(child, visibility!, result, revealed && !node.collapsed) || hasVisibleDescendants;
}
}
if (node !== this.root) {
node.visible = visibility! === TreeVisibility.Recurse ? hasVisibleDescendants : (visibility! === TreeVisibility.Visible);
}
if (!node.visible) {
node.renderNodeCount = 0;
if (revealed) {
result.pop();
}
} else if (!node.collapsed) {
node.renderNodeCount += result.length - resultStartLength;
}
this._onDidChangeRenderNodeCount.fire(node);
return node.visible;
}
private _updateAncestorsRenderNodeCount(node: IMutableTreeNode<T, TFilterData> | undefined, diff: number): void {
if (diff === 0) {
return;
}
while (node) {
node.renderNodeCount += diff;
this._onDidChangeRenderNodeCount.fire(node);
node = node.parent;
}
}
private _filterNode(node: IMutableTreeNode<T, TFilterData>, parentVisibility: TreeVisibility): TreeVisibility {
const result = this.filter ? this.filter.filter(node.element, parentVisibility) : TreeVisibility.Visible;
if (typeof result === 'boolean') {
node.filterData = undefined;
return result ? TreeVisibility.Visible : TreeVisibility.Hidden;
} else if (isFilterResult<TFilterData>(result)) {
node.filterData = result.data;
return getVisibleState(result.visibility);
} else {
node.filterData = undefined;
return getVisibleState(result);
}
}
// cheap
private getTreeNode(location: number[], node: IMutableTreeNode<T, TFilterData> = this.root): IMutableTreeNode<T, TFilterData> {
if (!location || location.length === 0) {
return node;
}
const [index, ...rest] = location;
if (index < 0 || index > node.children.length) {
throw new Error('Invalid tree location');
}
return this.getTreeNode(rest, node.children[index]);
}
// expensive
private getTreeNodeWithListIndex(location: number[]): { node: IMutableTreeNode<T, TFilterData>, listIndex: number, revealed: boolean } {
const { parentNode, listIndex, revealed } = this.getParentNodeWithListIndex(location);
const index = location[location.length - 1];
if (index < 0 || index > parentNode.children.length) {
throw new Error('Invalid tree location');
}
const node = parentNode.children[index];
return { node, listIndex, revealed };
}
private getParentNodeWithListIndex(location: number[], node: IMutableTreeNode<T, TFilterData> = this.root, listIndex: number = 0, revealed = true): { parentNode: IMutableTreeNode<T, TFilterData>; listIndex: number; revealed: boolean; } {
const [index, ...rest] = location;
if (index < 0 || index > node.children.length) {
throw new Error('Invalid tree location');
}
// TODO@joao perf!
for (let i = 0; i < index; i++) {
listIndex += node.children[i].renderNodeCount;
}
revealed = revealed && !node.collapsed;
if (rest.length === 0) {
return { parentNode: node, listIndex, revealed };
}
return this.getParentNodeWithListIndex(rest, node.children[index], listIndex + 1, revealed);
}
getNode(location: number[] = []): ITreeNode<T, TFilterData> {
return this.getTreeNode(location);
}
// TODO@joao perf!
getNodeLocation(node: ITreeNode<T, TFilterData>): number[] {
const location: number[] = [];
while (node.parent) {
location.push(node.parent.children.indexOf(node));
node = node.parent;
}
return location.reverse();
}
getParentNodeLocation(location: number[]): number[] {
if (location.length <= 1) {
return [];
}
return tail2(location)[0];
}
getParentElement(location: number[]): T {
const parentLocation = this.getParentNodeLocation(location);
const node = this.getTreeNode(parentLocation);
return node.element;
}
getFirstElementChild(location: number[]): T | undefined {
const node = this.getTreeNode(location);
if (node.children.length === 0) {
return undefined;
}
return node.children[0].element;
}
getLastElementAncestor(location: number[] = []): T | undefined {
const node = this.getTreeNode(location);
if (node.children.length === 0) {
return undefined;
}
return this._getLastElementAncestor(node);
}
private _getLastElementAncestor(node: ITreeNode<T, TFilterData>): T {
if (node.children.length === 0) {
return node.element;
}
return this._getLastElementAncestor(node.children[node.children.length - 1]);
}
}

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#E8E8E8" d="M6 4v8l4-4-4-4zm1 2.414L8.586 8 7 9.586V6.414z"/></svg>

After

Width:  |  Height:  |  Size: 139 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#fff" d="M6 4v8l4-4-4-4zm1 2.414l1.586 1.586-1.586 1.586v-3.172z"/></svg>

After

Width:  |  Height:  |  Size: 148 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#646465" d="M6 4v8l4-4-4-4zm1 2.414L8.586 8 7 9.586V6.414z"/></svg>

After

Width:  |  Height:  |  Size: 139 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#E8E8E8" d="M11 10H5.344L11 4.414V10z"/></svg>

After

Width:  |  Height:  |  Size: 118 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16"><path fill="#fff" d="M11 10.07h-5.656l5.656-5.656v5.656z"/></svg>

After

Width:  |  Height:  |  Size: 128 B

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#646465" d="M11 10H5.344L11 4.414V10z"/></svg>

After

Width:  |  Height:  |  Size: 118 B

View File

@@ -0,0 +1,31 @@
<?xml version='1.0' standalone='no' ?>
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
<style>
circle {
animation: ball 0.6s linear infinite;
}
circle:nth-child(2) { animation-delay: 0.075s; }
circle:nth-child(3) { animation-delay: 0.15s; }
circle:nth-child(4) { animation-delay: 0.225s; }
circle:nth-child(5) { animation-delay: 0.3s; }
circle:nth-child(6) { animation-delay: 0.375s; }
circle:nth-child(7) { animation-delay: 0.45s; }
circle:nth-child(8) { animation-delay: 0.525s; }
@keyframes ball {
from { opacity: 1; }
to { opacity: 0.3; }
}
</style>
<g style="fill:grey;">
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,31 @@
<?xml version='1.0' standalone='no' ?>
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
<style>
circle {
animation: ball 0.6s linear infinite;
}
circle:nth-child(2) { animation-delay: 0.075s; }
circle:nth-child(3) { animation-delay: 0.15s; }
circle:nth-child(4) { animation-delay: 0.225s; }
circle:nth-child(5) { animation-delay: 0.3s; }
circle:nth-child(6) { animation-delay: 0.375s; }
circle:nth-child(7) { animation-delay: 0.45s; }
circle:nth-child(8) { animation-delay: 0.525s; }
@keyframes ball {
from { opacity: 1; }
to { opacity: 0.3; }
}
</style>
<g style="fill:white;">
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -0,0 +1,31 @@
<?xml version='1.0' standalone='no' ?>
<svg xmlns='http://www.w3.org/2000/svg' version='1.1' width='10px' height='10px'>
<style>
circle {
animation: ball 0.6s linear infinite;
}
circle:nth-child(2) { animation-delay: 0.075s; }
circle:nth-child(3) { animation-delay: 0.15s; }
circle:nth-child(4) { animation-delay: 0.225s; }
circle:nth-child(5) { animation-delay: 0.3s; }
circle:nth-child(6) { animation-delay: 0.375s; }
circle:nth-child(7) { animation-delay: 0.45s; }
circle:nth-child(8) { animation-delay: 0.525s; }
@keyframes ball {
from { opacity: 1; }
to { opacity: 0.3; }
}
</style>
<g>
<circle cx='5' cy='1' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='2.1716' r='1' style='opacity:0.3;' />
<circle cx='9' cy='5' r='1' style='opacity:0.3;' />
<circle cx='7.8284' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='5' cy='9' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='7.8284' r='1' style='opacity:0.3;' />
<circle cx='1' cy='5' r='1' style='opacity:0.3;' />
<circle cx='2.1716' cy='2.1716' r='1' style='opacity:0.3;' />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -3,14 +3,11 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-tl-row {
display: flex;
height: 100%;
align-items: center;
.monaco-panel-view .panel > .panel-header h3.title {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
font-size: 11px;
-webkit-margin-before: 0;
-webkit-margin-after: 0;
}
.monaco-tl-row > .tl-twistie {
font-size: 10px;
text-align: right;
padding-right: 10px;
}

View File

@@ -0,0 +1,67 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.monaco-tl-row {
display: flex;
height: 100%;
align-items: center;
}
.monaco-tl-twistie,
.monaco-tl-contents {
height: 100%;
}
.monaco-tl-twistie {
font-size: 10px;
text-align: right;
margin-right: 6px;
flex-shrink: 0;
}
.monaco-tl-contents {
flex: 1;
overflow: hidden;
}
.monaco-tl-twistie.collapsible {
background-size: 16px;
background-position: 100% 50%;
background-repeat: no-repeat;
background-image: url("expanded.svg");
}
.monaco-tl-twistie.collapsible.collapsed:not(.loading) {
display: inline-block;
background-image: url("collapsed.svg");
}
.vs-dark .monaco-tl-twistie.collapsible:not(.loading) {
background-image: url("expanded-dark.svg");
}
.vs-dark .monaco-tl-twistie.collapsible.collapsed:not(.loading) {
background-image: url("collapsed-dark.svg");
}
.hc-black .monaco-tl-twistie.collapsible:not(.loading) {
background-image: url("expanded-hc.svg");
}
.hc-black .monaco-tl-twistie.collapsible.collapsed:not(.loading) {
background-image: url("collapsed-hc.svg");
}
.monaco-tl-twistie.loading {
background-image: url("loading.svg");
}
.vs-dark .monaco-tl-twistie.loading {
background-image: url("loading-dark.svg");
}
.hc-black .monaco-tl-twistie.loading {
background-image: url("loading-hc.svg");
}

View File

@@ -0,0 +1,42 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Iterator, ISequence } from 'vs/base/common/iterator';
import { AbstractTree, IAbstractTreeOptions } from 'vs/base/browser/ui/tree/abstractTree';
import { ISpliceable } from 'vs/base/common/sequence';
import { ITreeNode, ITreeModel, ITreeElement, ITreeRenderer } from 'vs/base/browser/ui/tree/tree';
import { ObjectTreeModel } from 'vs/base/browser/ui/tree/objectTreeModel';
import { IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
export interface IObjectTreeOptions<T, TFilterData = void> extends IAbstractTreeOptions<T, TFilterData> {
collapseByDefault?: boolean; // defaults to false
}
export class ObjectTree<T extends NonNullable<any>, TFilterData = void> extends AbstractTree<T | null, TFilterData, T | null> {
protected model: ObjectTreeModel<T, TFilterData>;
constructor(
container: HTMLElement,
delegate: IListVirtualDelegate<T>,
renderers: ITreeRenderer<any /* TODO@joao */, TFilterData, any>[],
options: IObjectTreeOptions<T, TFilterData> = {}
) {
super(container, delegate, renderers, options);
}
setChildren(
element: T | null,
children?: ISequence<ITreeElement<T>>,
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void,
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
): Iterator<ITreeElement<T | null>> {
return this.model.setChildren(element, children, onDidCreateNode, onDidDeleteNode);
}
protected createModel(view: ISpliceable<ITreeNode<T, TFilterData>>, options: IObjectTreeOptions<T, TFilterData>): ITreeModel<T | null, TFilterData, T | null> {
return new ObjectTreeModel(view, options);
}
}

View File

@@ -0,0 +1,169 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ISpliceable } from 'vs/base/common/sequence';
import { Iterator, ISequence, getSequenceIterator } from 'vs/base/common/iterator';
import { IndexTreeModel, IIndexTreeModelOptions } from 'vs/base/browser/ui/tree/indexTreeModel';
import { Event } from 'vs/base/common/event';
import { ITreeModel, ITreeNode, ITreeElement } from 'vs/base/browser/ui/tree/tree';
export interface IObjectTreeModelOptions<T, TFilterData> extends IIndexTreeModelOptions<T, TFilterData> { }
export class ObjectTreeModel<T extends NonNullable<any>, TFilterData extends NonNullable<any> = void> implements ITreeModel<T | null, TFilterData, T | null> {
private model: IndexTreeModel<T | null, TFilterData>;
private nodes = new Map<T | null, ITreeNode<T, TFilterData>>();
readonly onDidChangeCollapseState: Event<ITreeNode<T, TFilterData>>;
readonly onDidChangeRenderNodeCount: Event<ITreeNode<T, TFilterData>>;
get size(): number { return this.nodes.size; }
constructor(list: ISpliceable<ITreeNode<T, TFilterData>>, options: IObjectTreeModelOptions<T, TFilterData> = {}) {
this.model = new IndexTreeModel(list, null, options);
this.onDidChangeCollapseState = this.model.onDidChangeCollapseState as Event<ITreeNode<T, TFilterData>>;
this.onDidChangeRenderNodeCount = this.model.onDidChangeRenderNodeCount as Event<ITreeNode<T, TFilterData>>;
}
setChildren(
element: T | null,
children: ISequence<ITreeElement<T>> | undefined,
onDidCreateNode?: (node: ITreeNode<T, TFilterData>) => void,
onDidDeleteNode?: (node: ITreeNode<T, TFilterData>) => void
): Iterator<ITreeElement<T | null>> {
const location = this.getElementLocation(element);
const insertedElements = new Set<T | null>();
const _onDidCreateNode = (node: ITreeNode<T, TFilterData>) => {
insertedElements.add(node.element);
this.nodes.set(node.element, node);
if (onDidCreateNode) {
onDidCreateNode(node);
}
};
const _onDidDeleteNode = (node: ITreeNode<T, TFilterData>) => {
if (!insertedElements.has(node.element)) {
this.nodes.delete(node.element);
}
if (onDidDeleteNode) {
onDidDeleteNode(node);
}
};
return this.model.splice(
[...location, 0],
Number.MAX_VALUE,
this.preserveCollapseState(children),
_onDidCreateNode,
_onDidDeleteNode
);
}
private preserveCollapseState(elements: ISequence<ITreeElement<T | null>> | undefined): ISequence<ITreeElement<T | null>> {
const iterator = elements ? getSequenceIterator(elements) : Iterator.empty<ITreeElement<T>>();
return Iterator.map(iterator, treeElement => {
const node = this.nodes.get(treeElement.element);
if (!node) {
return treeElement;
}
const collapsible = typeof treeElement.collapsible === 'boolean' ? treeElement.collapsible : node.collapsible;
const collapsed = typeof treeElement.collapsed !== 'undefined' ? treeElement.collapsed : (collapsible && node.collapsed);
return {
...treeElement,
collapsible,
collapsed,
children: this.preserveCollapseState(treeElement.children)
};
});
}
getParentElement(ref: T | null = null): T | null {
const location = this.getElementLocation(ref);
return this.model.getParentElement(location);
}
getFirstElementChild(ref: T | null = null): T | null | undefined {
const location = this.getElementLocation(ref);
return this.model.getFirstElementChild(location);
}
getLastElementAncestor(ref: T | null = null): T | null | undefined {
const location = this.getElementLocation(ref);
return this.model.getLastElementAncestor(location);
}
getListIndex(element: T): number {
const location = this.getElementLocation(element);
return this.model.getListIndex(location);
}
setCollapsed(element: T, collapsed: boolean): boolean {
const location = this.getElementLocation(element);
return this.model.setCollapsed(location, collapsed);
}
toggleCollapsed(element: T): void {
const location = this.getElementLocation(element);
this.model.toggleCollapsed(location);
}
collapseAll(): void {
this.model.collapseAll();
}
isCollapsible(element: T): boolean {
const location = this.getElementLocation(element);
return this.model.isCollapsible(location);
}
isCollapsed(element: T): boolean {
const location = this.getElementLocation(element);
return this.model.isCollapsed(location);
}
refilter(): void {
this.model.refilter();
}
getNode(element: T | null = null): ITreeNode<T | null, TFilterData> {
const location = this.getElementLocation(element);
return this.model.getNode(location);
}
getNodeLocation(node: ITreeNode<T, TFilterData>): T {
return node.element;
}
getParentNodeLocation(element: T): T | null {
const node = this.nodes.get(element);
if (!node) {
throw new Error(`Tree element not found: ${element}`);
}
return node.parent!.element;
}
private getElementLocation(element: T | null): number[] {
if (element === null) {
return [];
}
const node = this.nodes.get(element);
if (!node) {
throw new Error(`Tree element not found: ${element}`);
}
return this.model.getNodeLocation(node);
}
}

View File

@@ -3,250 +3,127 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./tree';
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
import { IListOptions, List, IIdentityProvider, IMultipleSelectionController } from 'vs/base/browser/ui/list/listWidget';
import { TreeModel, ITreeNode, ITreeElement, getNodeLocation } from 'vs/base/browser/ui/tree/treeModel';
import { Iterator, ISequence } from 'vs/base/common/iterator';
import { IVirtualDelegate, IRenderer, IListMouseEvent } from 'vs/base/browser/ui/list/list';
import { append, $ } from 'vs/base/browser/dom';
import { Event, Relay, chain } from 'vs/base/common/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { tail2 } from 'vs/base/common/arrays';
import { Event } from 'vs/base/common/event';
import { Iterator } from 'vs/base/common/iterator';
import { IListRenderer } from 'vs/base/browser/ui/list/list';
function toTreeListOptions<T>(options?: IListOptions<T>): IListOptions<ITreeNode<T>> {
if (!options) {
return undefined;
}
export const enum TreeVisibility {
let identityProvider: IIdentityProvider<ITreeNode<T>> | undefined = undefined;
let multipleSelectionController: IMultipleSelectionController<ITreeNode<T>> | undefined = undefined;
/**
* The tree node should be hidden.
*/
Hidden,
if (options.identityProvider) {
identityProvider = el => options.identityProvider(el.element);
}
/**
* The tree node should be visible.
*/
Visible,
if (options.multipleSelectionController) {
multipleSelectionController = {
isSelectionSingleChangeEvent(e) {
return options.multipleSelectionController.isSelectionSingleChangeEvent({ ...e, element: e.element } as any);
},
isSelectionRangeChangeEvent(e) {
return options.multipleSelectionController.isSelectionRangeChangeEvent({ ...e, element: e.element } as any);
}
};
}
return {
...options,
identityProvider,
multipleSelectionController
};
/**
* The tree node should be visible if any of its descendants is visible.
*/
Recurse
}
class TreeDelegate<T> implements IVirtualDelegate<ITreeNode<T>> {
/**
* A composed filter result containing the visibility result as well as
* metadata.
*/
export interface ITreeFilterDataResult<TFilterData> {
constructor(private delegate: IVirtualDelegate<T>) { }
/**
* Whether the node should be visibile.
*/
visibility: boolean | TreeVisibility;
getHeight(element: ITreeNode<T>): number {
return this.delegate.getHeight(element.element);
}
getTemplateId(element: ITreeNode<T>): string {
return this.delegate.getTemplateId(element.element);
}
/**
* Metadata about the element's visibility which gets forwarded to the
* renderer once the element gets rendered.
*/
data: TFilterData;
}
interface ITreeListTemplateData<T> {
twistie: HTMLElement;
templateData: T;
/**
* The result of a filter call can be a boolean value indicating whether
* the element should be visible or not, a value of type `TreeVisibility` or
* an object composed of the visibility result as well as additional metadata
* which gets forwarded to the renderer once the element gets rendered.
*/
export type TreeFilterResult<TFilterData> = boolean | TreeVisibility | ITreeFilterDataResult<TFilterData>;
/**
* A tree filter is responsible for controlling the visibility of
* elements in a tree.
*/
export interface ITreeFilter<T, TFilterData = void> {
/**
* Returns whether this elements should be visible and, if affirmative,
* additional metadata which gets forwarded to the renderer once the element
* gets rendered.
*
* @param element The tree element.
*/
filter(element: T, parentVisibility: TreeVisibility): TreeFilterResult<TFilterData>;
}
function renderTwistie<T>(node: ITreeNode<T>, twistie: HTMLElement): void {
if (node.children.length === 0 && !node.collapsible) {
twistie.innerText = '';
} else {
twistie.innerText = node.collapsed ? '▹' : '◢';
}
export interface ITreeElement<T> {
readonly element: T;
readonly children?: Iterator<ITreeElement<T>> | ITreeElement<T>[];
readonly collapsible?: boolean;
readonly collapsed?: boolean;
}
class TreeRenderer<T, TTemplateData> implements IRenderer<ITreeNode<T>, ITreeListTemplateData<TTemplateData>> {
readonly templateId: string;
private renderedNodes = new Map<ITreeNode<T>, ITreeListTemplateData<TTemplateData>>();
private disposables: IDisposable[] = [];
constructor(
private renderer: IRenderer<T, TTemplateData>,
onDidChangeCollapseState: Event<ITreeNode<T>>
) {
this.templateId = renderer.templateId;
onDidChangeCollapseState(this.onDidChangeCollapseState, this, this.disposables);
}
renderTemplate(container: HTMLElement): ITreeListTemplateData<TTemplateData> {
const el = append(container, $('.monaco-tl-row'));
const twistie = append(el, $('.tl-twistie'));
const contents = append(el, $('.tl-contents'));
const templateData = this.renderer.renderTemplate(contents);
return { twistie, templateData };
}
renderElement(node: ITreeNode<T>, index: number, templateData: ITreeListTemplateData<TTemplateData>): void {
this.renderedNodes.set(node, templateData);
templateData.twistie.style.width = `${10 + node.depth * 10}px`;
renderTwistie(node, templateData.twistie);
this.renderer.renderElement(node.element, index, templateData.templateData);
}
disposeElement(node: ITreeNode<T>): void {
this.renderedNodes.delete(node);
}
disposeTemplate(templateData: ITreeListTemplateData<TTemplateData>): void {
this.renderer.disposeTemplate(templateData.templateData);
}
private onDidChangeCollapseState(node: ITreeNode<T>): void {
const templateData = this.renderedNodes.get(node);
if (!templateData) {
return;
}
renderTwistie(node, templateData.twistie);
}
dispose(): void {
this.renderedNodes.clear();
this.disposables = dispose(this.disposables);
}
export interface ITreeNode<T, TFilterData = void> {
readonly element: T;
readonly parent: ITreeNode<T, TFilterData> | undefined;
readonly children: ITreeNode<T, TFilterData>[];
readonly depth: number;
readonly collapsible: boolean;
readonly collapsed: boolean;
readonly visible: boolean;
readonly filterData: TFilterData | undefined;
}
function isInputElement(e: HTMLElement): boolean {
return e.tagName === 'INPUT' || e.tagName === 'TEXTAREA';
export interface ITreeModel<T, TFilterData, TRef> {
readonly onDidChangeCollapseState: Event<ITreeNode<T, TFilterData>>;
readonly onDidChangeRenderNodeCount: Event<ITreeNode<T, TFilterData>>;
getListIndex(location: TRef): number;
getNode(location?: TRef): ITreeNode<T, any>;
getNodeLocation(node: ITreeNode<T, any>): TRef;
getParentNodeLocation(location: TRef): TRef;
getParentElement(location: TRef): T;
getFirstElementChild(location: TRef): T | undefined;
getLastElementAncestor(location?: TRef): T | undefined;
isCollapsible(location: TRef): boolean;
isCollapsed(location: TRef): boolean;
setCollapsed(location: TRef, collapsed: boolean): boolean;
toggleCollapsed(location: TRef): void;
collapseAll(): void;
refilter(): void;
}
export interface ITreeOptions<T> extends IListOptions<T> { }
export interface ITreeRenderer<T, TFilterData = void, TTemplateData = void> extends IListRenderer<ITreeNode<T, TFilterData>, TTemplateData> {
renderTwistie?(element: T, twistieElement: HTMLElement): void;
onDidChangeTwistieState?: Event<T>;
}
export class Tree<T> implements IDisposable {
export interface ITreeEvent<T> {
elements: T[];
browserEvent?: UIEvent;
}
private view: List<ITreeNode<T>>;
private model: TreeModel<T>;
private disposables: IDisposable[] = [];
export interface ITreeMouseEvent<T> {
browserEvent: MouseEvent;
element: T | null;
}
constructor(
container: HTMLElement,
delegate: IVirtualDelegate<T>,
renderers: IRenderer<T, any>[],
options?: ITreeOptions<T>
) {
const treeDelegate = new TreeDelegate(delegate);
const onDidChangeCollapseStateRelay = new Relay<ITreeNode<T>>();
const treeRenderers = renderers.map(r => new TreeRenderer(r, onDidChangeCollapseStateRelay.event));
this.disposables.push(...treeRenderers);
const treeOptions = toTreeListOptions(options);
this.view = new List(container, treeDelegate, treeRenderers, treeOptions);
this.model = new TreeModel<T>(this.view);
onDidChangeCollapseStateRelay.input = this.model.onDidChangeCollapseState;
this.view.onMouseClick(this.onMouseClick, this, this.disposables);
const onKeyDown = chain(this.view.onKeyDown)
.filter(e => !isInputElement(e.target as HTMLElement))
.map(e => new StandardKeyboardEvent(e));
onKeyDown.filter(e => e.keyCode === KeyCode.LeftArrow).on(this.onLeftArrow, this, this.disposables);
onKeyDown.filter(e => e.keyCode === KeyCode.RightArrow).on(this.onRightArrow, this, this.disposables);
onKeyDown.filter(e => e.keyCode === KeyCode.Space).on(this.onSpace, this, this.disposables);
}
splice(location: number[], deleteCount: number, toInsert: ISequence<ITreeElement<T>> = Iterator.empty()): Iterator<ITreeElement<T>> {
return this.model.splice(location, deleteCount, toInsert);
}
private onMouseClick(e: IListMouseEvent<ITreeNode<T>>): void {
const node = e.element;
const location = getNodeLocation(node);
this.model.toggleCollapsed(location);
}
private onLeftArrow(e: StandardKeyboardEvent): void {
e.preventDefault();
e.stopPropagation();
const nodes = this.view.getFocusedElements();
if (nodes.length === 0) {
return;
}
const node = nodes[0];
const location = getNodeLocation(node);
const didChange = this.model.setCollapsed(location, true);
if (!didChange) {
if (location.length === 1) {
return;
}
const [parentLocation] = tail2(location);
const parentListIndex = this.model.getListIndex(parentLocation);
this.view.setFocus([parentListIndex]);
}
}
private onRightArrow(e: StandardKeyboardEvent): void {
e.preventDefault();
e.stopPropagation();
const nodes = this.view.getFocusedElements();
if (nodes.length === 0) {
return;
}
const node = nodes[0];
const location = getNodeLocation(node);
const didChange = this.model.setCollapsed(location, false);
if (!didChange) {
if (node.children.length === 0) {
return;
}
const [focusedIndex] = this.view.getFocus();
this.view.setFocus([focusedIndex + 1]);
}
}
private onSpace(e: StandardKeyboardEvent): void {
e.preventDefault();
e.stopPropagation();
const nodes = this.view.getFocusedElements();
if (nodes.length === 0) {
return;
}
const node = nodes[0];
const location = getNodeLocation(node);
this.model.toggleCollapsed(location);
}
dispose(): void {
this.disposables = dispose(this.disposables);
this.view.dispose();
this.view = null;
this.model = null;
}
}
export interface ITreeContextMenuEvent<T> {
browserEvent: UIEvent;
element: T | null;
anchor: HTMLElement | { x: number; y: number; } | undefined;
}

View File

@@ -1,232 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { ISpliceable } from 'vs/base/common/sequence';
import { Iterator, ISequence } from 'vs/base/common/iterator';
import { Emitter, Event } from 'vs/base/common/event';
export interface ITreeElement<T> {
readonly element: T;
readonly children?: Iterator<ITreeElement<T>> | ITreeElement<T>[];
readonly collapsible?: boolean;
readonly collapsed?: boolean;
}
export interface ITreeNode<T> {
readonly parent: IMutableTreeNode<T> | undefined;
readonly element: T;
readonly children: IMutableTreeNode<T>[];
readonly depth: number;
readonly collapsible: boolean;
readonly collapsed: boolean;
readonly visibleCount: number;
}
interface IMutableTreeNode<T> extends ITreeNode<T> {
collapsed: boolean;
visibleCount: number;
}
function visibleCountReducer<T>(result: number, node: IMutableTreeNode<T>): number {
return result + (node.collapsed ? 1 : node.visibleCount);
}
function getVisibleCount<T>(nodes: IMutableTreeNode<T>[]): number {
return nodes.reduce(visibleCountReducer, 0);
}
function getVisibleNodes<T>(nodes: IMutableTreeNode<T>[], result: ITreeNode<T>[] = []): ITreeNode<T>[] {
for (const node of nodes) {
result.push(node);
if (!node.collapsed) {
getVisibleNodes(node.children, result);
}
}
return result;
}
function getTreeElementIterator<T>(elements: Iterator<ITreeElement<T>> | ITreeElement<T>[] | undefined): Iterator<ITreeElement<T>> {
if (!elements) {
return Iterator.empty();
} else if (Array.isArray(elements)) {
return Iterator.iterate(elements);
} else {
return elements;
}
}
function treeElementToNode<T>(treeElement: ITreeElement<T>, parent: IMutableTreeNode<T>, visible: boolean, treeListElements: ITreeNode<T>[]): IMutableTreeNode<T> {
const depth = parent.depth + 1;
const { element, collapsible, collapsed } = treeElement;
const node = { parent, element, children: [], depth, collapsible: !!collapsible, collapsed: !!collapsed, visibleCount: 0 };
if (visible) {
treeListElements.push(node);
}
const children = getTreeElementIterator(treeElement.children);
node.children = Iterator.collect(Iterator.map(children, el => treeElementToNode(el, node, visible && !treeElement.collapsed, treeListElements)));
node.collapsible = node.collapsible || node.children.length > 0;
node.visibleCount = 1 + getVisibleCount(node.children);
return node;
}
function treeNodeToElement<T>(node: IMutableTreeNode<T>): ITreeElement<T> {
const { element, collapsed } = node;
const children = Iterator.map(Iterator.iterate(node.children), treeNodeToElement);
return { element, children, collapsed };
}
export function getNodeLocation<T>(node: ITreeNode<T>): number[] {
const location = [];
while (node.parent) {
location.push(node.parent.children.indexOf(node));
node = node.parent;
}
return location.reverse();
}
export class TreeModel<T> {
private root: IMutableTreeNode<T> = {
parent: undefined,
element: undefined,
children: [],
depth: 0,
collapsible: false,
collapsed: false,
visibleCount: 1
};
private _onDidChangeCollapseState = new Emitter<ITreeNode<T>>();
readonly onDidChangeCollapseState: Event<ITreeNode<T>> = this._onDidChangeCollapseState.event;
constructor(private list: ISpliceable<ITreeNode<T>>) { }
splice(location: number[], deleteCount: number, toInsert?: ISequence<ITreeElement<T>>): Iterator<ITreeElement<T>> {
if (location.length === 0) {
throw new Error('Invalid tree location');
}
const { parentNode, listIndex, visible } = this.findParentNode(location);
const treeListElementsToInsert: ITreeNode<T>[] = [];
const elementsToInsert = getTreeElementIterator(toInsert);
const nodesToInsert = Iterator.collect(Iterator.map(elementsToInsert, el => treeElementToNode(el, parentNode, visible, treeListElementsToInsert)));
const lastIndex = location[location.length - 1];
const deletedNodes = parentNode.children.splice(lastIndex, deleteCount, ...nodesToInsert);
const visibleDeleteCount = getVisibleCount(deletedNodes);
parentNode.visibleCount += getVisibleCount(nodesToInsert) - visibleDeleteCount;
if (visible) {
this.list.splice(listIndex, visibleDeleteCount, treeListElementsToInsert);
}
return Iterator.map(Iterator.iterate(deletedNodes), treeNodeToElement);
}
getListIndex(location: number[]): number {
return this.findNode(location).listIndex;
}
setCollapsed(location: number[], collapsed: boolean): boolean {
return this._setCollapsed(location, collapsed);
}
toggleCollapsed(location: number[]): void {
this._setCollapsed(location);
}
private _setCollapsed(location: number[], collapsed?: boolean | undefined): boolean {
const { node, listIndex, visible } = this.findNode(location);
if (!node.collapsible) {
return false;
}
if (typeof collapsed === 'undefined') {
collapsed = !node.collapsed;
}
if (node.collapsed === collapsed) {
return false;
}
node.collapsed = collapsed;
if (visible) {
this._onDidChangeCollapseState.fire(node);
let visibleCountDiff: number;
if (collapsed) {
const deleteCount = getVisibleCount(node.children);
this.list.splice(listIndex + 1, deleteCount, []);
visibleCountDiff = -deleteCount;
} else {
const toInsert = getVisibleNodes(node.children);
this.list.splice(listIndex + 1, 0, toInsert);
visibleCountDiff = toInsert.length;
}
let mutableNode = node;
while (mutableNode) {
mutableNode.visibleCount += visibleCountDiff;
mutableNode = mutableNode.parent;
}
}
return true;
}
isCollapsed(location: number[]): boolean {
return this.findNode(location).node.collapsed;
}
private findNode(location: number[]): { node: IMutableTreeNode<T>, listIndex: number, visible: boolean } {
const { parentNode, listIndex, visible } = this.findParentNode(location);
const index = location[location.length - 1];
if (index < 0 || index > parentNode.children.length) {
throw new Error('Invalid tree location');
}
const node = parentNode.children[index];
return { node, listIndex, visible };
}
private findParentNode(location: number[], node: IMutableTreeNode<T> = this.root, listIndex: number = 0, visible = true): { parentNode: IMutableTreeNode<T>; listIndex: number; visible: boolean; } {
const [index, ...rest] = location;
if (index < 0 || index > node.children.length) {
throw new Error('Invalid tree location');
}
// TODO@joao perf!
for (let i = 0; i < index; i++) {
listIndex += node.children[i].visibleCount;
}
visible = visible && !node.collapsed;
if (rest.length === 0) {
return { parentNode: node, listIndex, visible };
}
return this.findParentNode(rest, node.children[index], listIndex + 1, visible);
}
}

View File

@@ -3,52 +3,50 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import * as dom from 'vs/base/browser/dom';
import { IKeyboardEvent, StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { Disposable } from 'vs/base/common/lifecycle';
import { StandardMouseEvent, IMouseEvent } from 'vs/base/browser/mouseEvent';
import { StandardKeyboardEvent, IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import * as DomUtils from 'vs/base/browser/dom';
export abstract class Widget extends Disposable {
protected onclick(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
this._register(DomUtils.addDisposableListener(domNode, DomUtils.EventType.CLICK, (e: MouseEvent) => listener(new StandardMouseEvent(e))));
this._register(dom.addDisposableListener(domNode, dom.EventType.CLICK, (e: MouseEvent) => listener(new StandardMouseEvent(e))));
}
protected onmousedown(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
this._register(DomUtils.addDisposableListener(domNode, DomUtils.EventType.MOUSE_DOWN, (e: MouseEvent) => listener(new StandardMouseEvent(e))));
this._register(dom.addDisposableListener(domNode, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => listener(new StandardMouseEvent(e))));
}
protected onmouseover(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
this._register(DomUtils.addDisposableListener(domNode, DomUtils.EventType.MOUSE_OVER, (e: MouseEvent) => listener(new StandardMouseEvent(e))));
this._register(dom.addDisposableListener(domNode, dom.EventType.MOUSE_OVER, (e: MouseEvent) => listener(new StandardMouseEvent(e))));
}
protected onnonbubblingmouseout(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
this._register(DomUtils.addDisposableNonBubblingMouseOutListener(domNode, (e: MouseEvent) => listener(new StandardMouseEvent(e))));
this._register(dom.addDisposableNonBubblingMouseOutListener(domNode, (e: MouseEvent) => listener(new StandardMouseEvent(e))));
}
protected onkeydown(domNode: HTMLElement, listener: (e: IKeyboardEvent) => void): void {
this._register(DomUtils.addDisposableListener(domNode, DomUtils.EventType.KEY_DOWN, (e: KeyboardEvent) => listener(new StandardKeyboardEvent(e))));
this._register(dom.addDisposableListener(domNode, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => listener(new StandardKeyboardEvent(e))));
}
protected onkeyup(domNode: HTMLElement, listener: (e: IKeyboardEvent) => void): void {
this._register(DomUtils.addDisposableListener(domNode, DomUtils.EventType.KEY_UP, (e: KeyboardEvent) => listener(new StandardKeyboardEvent(e))));
this._register(dom.addDisposableListener(domNode, dom.EventType.KEY_UP, (e: KeyboardEvent) => listener(new StandardKeyboardEvent(e))));
}
protected oninput(domNode: HTMLElement, listener: (e: Event) => void): void {
this._register(DomUtils.addDisposableListener(domNode, DomUtils.EventType.INPUT, listener));
this._register(dom.addDisposableListener(domNode, dom.EventType.INPUT, listener));
}
protected onblur(domNode: HTMLElement, listener: (e: Event) => void): void {
this._register(DomUtils.addDisposableListener(domNode, DomUtils.EventType.BLUR, listener));
this._register(dom.addDisposableListener(domNode, dom.EventType.BLUR, listener));
}
protected onfocus(domNode: HTMLElement, listener: (e: Event) => void): void {
this._register(DomUtils.addDisposableListener(domNode, DomUtils.EventType.FOCUS, listener));
this._register(dom.addDisposableListener(domNode, dom.EventType.FOCUS, listener));
}
protected onchange(domNode: HTMLElement, listener: (e: Event) => void): void {
this._register(DomUtils.addDisposableListener(domNode, DomUtils.EventType.CHANGE, listener));
this._register(dom.addDisposableListener(domNode, dom.EventType.CHANGE, listener));
}
}