Merge from master
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
|
||||
.monaco-text-button:hover {
|
||||
|
||||
@@ -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);
|
||||
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
.monaco-findInput .monaco-inputbox {
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
height: 25px;
|
||||
}
|
||||
|
||||
.monaco-findInput > .controls {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -74,7 +74,6 @@
|
||||
box-sizing: border-box;
|
||||
white-space: pre-wrap;
|
||||
visibility: hidden;
|
||||
min-height: 26px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 = ' ';
|
||||
} 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 = ' ';
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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; };
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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> {
|
||||
|
||||
1
src/vs/base/browser/ui/menu/check.svg
Normal 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 |
1
src/vs/base/browser/ui/menu/ellipsis.svg
Normal 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 |
@@ -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;
|
||||
}
|
||||
@@ -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 = /(?:&){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();
|
||||
}
|
||||
974
src/vs/base/browser/ui/menu/menubar.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
3
src/vs/base/browser/ui/menu/submenu.svg
Normal 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 |
@@ -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."
|
||||
]
|
||||
}]
|
||||
140
src/vs/base/browser/ui/octiconLabel/octicons/cgmanifest.json
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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' : ''));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
507
src/vs/base/browser/ui/tree/abstractTree.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
588
src/vs/base/browser/ui/tree/asyncDataTree.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
39
src/vs/base/browser/ui/tree/indexTree.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
467
src/vs/base/browser/ui/tree/indexTreeModel.ts
Normal 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]);
|
||||
}
|
||||
}
|
||||
1
src/vs/base/browser/ui/tree/media/collapsed-dark.svg
Normal 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 |
1
src/vs/base/browser/ui/tree/media/collapsed-hc.svg
Normal 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 |
1
src/vs/base/browser/ui/tree/media/collapsed.svg
Normal 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 |
1
src/vs/base/browser/ui/tree/media/expanded-dark.svg
Normal 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 |
1
src/vs/base/browser/ui/tree/media/expanded-hc.svg
Normal 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 |
1
src/vs/base/browser/ui/tree/media/expanded.svg
Normal 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 |
31
src/vs/base/browser/ui/tree/media/loading-dark.svg
Normal 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 |
31
src/vs/base/browser/ui/tree/media/loading-hc.svg
Normal 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 |
31
src/vs/base/browser/ui/tree/media/loading.svg
Normal 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 |
@@ -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;
|
||||
}
|
||||
67
src/vs/base/browser/ui/tree/media/tree.css
Normal 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");
|
||||
}
|
||||
42
src/vs/base/browser/ui/tree/objectTree.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
169
src/vs/base/browser/ui/tree/objectTreeModel.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||