src/vs/base/parts/quickopen/browser/quickOpenWidget.ts (760 lines of code) (raw):
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import 'vs/css!./quickopen';
import * as nls from 'vs/nls';
import * as platform from 'vs/base/common/platform';
import * as types from 'vs/base/common/types';
import { IQuickNavigateConfiguration, IAutoFocus, IEntryRunContext, IModel, Mode, IKeyMods } from 'vs/base/parts/quickopen/common/quickOpen';
import { Filter, Renderer, DataSource, IModelProvider, AccessibilityProvider } from 'vs/base/parts/quickopen/browser/quickOpenViewer';
import { ITree, ContextMenuEvent, IActionProvider, ITreeStyles, ITreeOptions, ITreeConfiguration } from 'vs/base/parts/tree/browser/tree';
import { InputBox, MessageType, IInputBoxStyles, IRange } from 'vs/base/browser/ui/inputbox/inputBox';
import Severity from 'vs/base/common/severity';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { DefaultController, ClickBehavior } from 'vs/base/parts/tree/browser/treeDefaults';
import * as DOM from 'vs/base/browser/dom';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Disposable } from 'vs/base/common/lifecycle';
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
import { Color } from 'vs/base/common/color';
import { mixin } from 'vs/base/common/objects';
import { StandardMouseEvent, IMouseEvent } from 'vs/base/browser/mouseEvent';
export interface IQuickOpenCallbacks {
onOk: () => void;
onCancel: () => void;
onType: (value: string) => void;
onShow?: () => void;
onHide?: (reason: HideReason) => void;
onFocusLost?: () => boolean /* veto close */;
}
export interface IQuickOpenOptions extends IQuickOpenStyles {
minItemsToShow?: number;
maxItemsToShow?: number;
inputPlaceHolder?: string;
inputAriaLabel?: string;
actionProvider?: IActionProvider;
keyboardSupport?: boolean;
treeCreator?: (container: HTMLElement, configuration: ITreeConfiguration, options?: ITreeOptions) => ITree;
}
export interface IQuickOpenStyles extends IInputBoxStyles, ITreeStyles {
background?: Color;
foreground?: Color;
borderColor?: Color;
pickerGroupForeground?: Color;
pickerGroupBorder?: Color;
widgetShadow?: Color;
progressBarBackground?: Color;
}
export interface IShowOptions {
quickNavigateConfiguration?: IQuickNavigateConfiguration;
autoFocus?: IAutoFocus;
inputSelection?: IRange;
value?: string;
}
export class QuickOpenController extends DefaultController {
onContextMenu(tree: ITree, element: any, event: ContextMenuEvent): boolean {
if (platform.isMacintosh) {
return this.onLeftClick(tree, element, event); // https://github.com/Microsoft/vscode/issues/1011
}
return super.onContextMenu(tree, element, event);
}
onMouseMiddleClick(tree: ITree, element: any, event: IMouseEvent): boolean {
return this.onLeftClick(tree, element, event);
}
}
export const enum HideReason {
ELEMENT_SELECTED,
FOCUS_LOST,
CANCELED
}
const defaultStyles = {
background: Color.fromHex('#1E1E1E'),
foreground: Color.fromHex('#CCCCCC'),
pickerGroupForeground: Color.fromHex('#0097FB'),
pickerGroupBorder: Color.fromHex('#3F3F46'),
widgetShadow: Color.fromHex('#000000'),
progressBarBackground: Color.fromHex('#0E70C0')
};
const DEFAULT_INPUT_ARIA_LABEL = nls.localize('quickOpenAriaLabel', "Quick picker. Type to narrow down results.");
export class QuickOpenWidget extends Disposable implements IModelProvider {
private static readonly MAX_WIDTH = 600; // Max total width of quick open widget
private static readonly MAX_ITEMS_HEIGHT = 20 * 22; // Max height of item list below input field
private isDisposed: boolean;
private options: IQuickOpenOptions;
private element: HTMLElement;
private tree: ITree;
private inputBox: InputBox;
private inputContainer: HTMLElement;
private helpText: HTMLElement;
private resultCount: HTMLElement;
private treeContainer: HTMLElement;
private progressBar: ProgressBar;
private visible: boolean;
private isLoosingFocus: boolean;
private callbacks: IQuickOpenCallbacks;
private quickNavigateConfiguration: IQuickNavigateConfiguration | undefined;
private container: HTMLElement;
private treeElement: HTMLElement;
private inputElement: HTMLElement;
private layoutDimensions: DOM.Dimension;
private model: IModel<any> | null;
private inputChangingTimeoutHandle: any;
private styles: IQuickOpenStyles;
private renderer: Renderer;
constructor(container: HTMLElement, callbacks: IQuickOpenCallbacks, options: IQuickOpenOptions) {
super();
this.isDisposed = false;
this.container = container;
this.callbacks = callbacks;
this.options = options;
this.styles = options || Object.create(null);
mixin(this.styles, defaultStyles, false);
this.model = null;
}
getElement(): HTMLElement {
return this.element;
}
getModel(): IModel<any> {
return this.model!;
}
setCallbacks(callbacks: IQuickOpenCallbacks): void {
this.callbacks = callbacks;
}
create(): HTMLElement {
// Container
this.element = document.createElement('div');
DOM.addClass(this.element, 'monaco-quick-open-widget');
this.container.appendChild(this.element);
this._register(DOM.addDisposableListener(this.element, DOM.EventType.CONTEXT_MENU, e => DOM.EventHelper.stop(e, true))); // Do this to fix an issue on Mac where the menu goes into the way
this._register(DOM.addDisposableListener(this.element, DOM.EventType.FOCUS, e => this.gainingFocus(), true));
this._register(DOM.addDisposableListener(this.element, DOM.EventType.BLUR, e => this.loosingFocus(e), true));
this._register(DOM.addDisposableListener(this.element, DOM.EventType.KEY_DOWN, e => {
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
if (keyboardEvent.keyCode === KeyCode.Escape) {
DOM.EventHelper.stop(e, true);
this.hide(HideReason.CANCELED);
} else if (keyboardEvent.keyCode === KeyCode.Tab && !keyboardEvent.altKey && !keyboardEvent.ctrlKey && !keyboardEvent.metaKey) {
const stops = (e.currentTarget as HTMLElement).querySelectorAll('input, .monaco-tree, .monaco-tree-row.focused .action-label.icon') as NodeListOf<HTMLElement>;
if (keyboardEvent.shiftKey && keyboardEvent.target === stops[0]) {
DOM.EventHelper.stop(e, true);
stops[stops.length - 1].focus();
} else if (!keyboardEvent.shiftKey && keyboardEvent.target === stops[stops.length - 1]) {
DOM.EventHelper.stop(e, true);
stops[0].focus();
}
}
}));
// Progress Bar
this.progressBar = this._register(new ProgressBar(this.element, { progressBarBackground: this.styles.progressBarBackground }));
this.progressBar.hide();
// Input Field
this.inputContainer = document.createElement('div');
DOM.addClass(this.inputContainer, 'quick-open-input');
this.element.appendChild(this.inputContainer);
this.inputBox = this._register(new InputBox(this.inputContainer, undefined, {
placeholder: this.options.inputPlaceHolder || '',
ariaLabel: DEFAULT_INPUT_ARIA_LABEL,
inputBackground: this.styles.inputBackground,
inputForeground: this.styles.inputForeground,
inputBorder: this.styles.inputBorder,
inputValidationInfoBackground: this.styles.inputValidationInfoBackground,
inputValidationInfoForeground: this.styles.inputValidationInfoForeground,
inputValidationInfoBorder: this.styles.inputValidationInfoBorder,
inputValidationWarningBackground: this.styles.inputValidationWarningBackground,
inputValidationWarningForeground: this.styles.inputValidationWarningForeground,
inputValidationWarningBorder: this.styles.inputValidationWarningBorder,
inputValidationErrorBackground: this.styles.inputValidationErrorBackground,
inputValidationErrorForeground: this.styles.inputValidationErrorForeground,
inputValidationErrorBorder: this.styles.inputValidationErrorBorder
}));
this.inputElement = this.inputBox.inputElement;
this.inputElement.setAttribute('role', 'combobox');
this.inputElement.setAttribute('aria-haspopup', 'false');
this.inputElement.setAttribute('aria-autocomplete', 'list');
this._register(DOM.addDisposableListener(this.inputBox.inputElement, DOM.EventType.INPUT, (e: Event) => this.onType()));
this._register(DOM.addDisposableListener(this.inputBox.inputElement, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
const shouldOpenInBackground = this.shouldOpenInBackground(keyboardEvent);
// Do not handle Tab: It is used to navigate between elements without mouse
if (keyboardEvent.keyCode === KeyCode.Tab) {
return;
}
// Pass tree navigation keys to the tree but leave focus in input field
else if (keyboardEvent.keyCode === KeyCode.DownArrow || keyboardEvent.keyCode === KeyCode.UpArrow || keyboardEvent.keyCode === KeyCode.PageDown || keyboardEvent.keyCode === KeyCode.PageUp) {
DOM.EventHelper.stop(e, true);
this.navigateInTree(keyboardEvent.keyCode, keyboardEvent.shiftKey);
// Position cursor at the end of input to allow right arrow (open in background)
// to function immediately unless the user has made a selection
if (this.inputBox.inputElement.selectionStart === this.inputBox.inputElement.selectionEnd) {
this.inputBox.inputElement.selectionStart = this.inputBox.value.length;
}
}
// Select element on Enter or on Arrow-Right if we are at the end of the input
else if (keyboardEvent.keyCode === KeyCode.Enter || shouldOpenInBackground) {
DOM.EventHelper.stop(e, true);
const focus = this.tree.getFocus();
if (focus) {
this.elementSelected(focus, e, shouldOpenInBackground ? Mode.OPEN_IN_BACKGROUND : Mode.OPEN);
}
}
}));
// Result count for screen readers
this.resultCount = document.createElement('div');
DOM.addClass(this.resultCount, 'quick-open-result-count');
this.resultCount.setAttribute('aria-live', 'polite');
this.resultCount.setAttribute('aria-atomic', 'true');
this.element.appendChild(this.resultCount);
// Tree
this.treeContainer = document.createElement('div');
DOM.addClass(this.treeContainer, 'quick-open-tree');
this.element.appendChild(this.treeContainer);
const createTree = this.options.treeCreator || ((container, config, opts) => new Tree(container, config, opts));
this.tree = this._register(createTree(this.treeContainer, {
dataSource: new DataSource(this),
controller: new QuickOpenController({ clickBehavior: ClickBehavior.ON_MOUSE_UP, keyboardSupport: this.options.keyboardSupport }),
renderer: (this.renderer = new Renderer(this, this.styles)),
filter: new Filter(this),
accessibilityProvider: new AccessibilityProvider(this)
}, {
twistiePixels: 11,
indentPixels: 0,
alwaysFocused: true,
verticalScrollMode: ScrollbarVisibility.Visible,
horizontalScrollMode: ScrollbarVisibility.Hidden,
ariaLabel: nls.localize('treeAriaLabel', "Quick Picker"),
keyboardSupport: this.options.keyboardSupport,
preventRootFocus: false
}));
this.treeElement = this.tree.getHTMLElement();
// Handle Focus and Selection event
this._register(this.tree.onDidChangeFocus(event => {
this.elementFocused(event.focus, event);
}));
this._register(this.tree.onDidChangeSelection(event => {
if (event.selection && event.selection.length > 0) {
const mouseEvent: StandardMouseEvent = event.payload && event.payload.originalEvent instanceof StandardMouseEvent ? event.payload.originalEvent : undefined;
const shouldOpenInBackground = mouseEvent ? this.shouldOpenInBackground(mouseEvent) : false;
this.elementSelected(event.selection[0], event, shouldOpenInBackground ? Mode.OPEN_IN_BACKGROUND : Mode.OPEN);
}
}));
this._register(DOM.addDisposableListener(this.treeContainer, DOM.EventType.KEY_DOWN, e => {
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
// Only handle when in quick navigation mode
if (!this.quickNavigateConfiguration) {
return;
}
// Support keyboard navigation in quick navigation mode
if (keyboardEvent.keyCode === KeyCode.DownArrow || keyboardEvent.keyCode === KeyCode.UpArrow || keyboardEvent.keyCode === KeyCode.PageDown || keyboardEvent.keyCode === KeyCode.PageUp) {
DOM.EventHelper.stop(e, true);
this.navigateInTree(keyboardEvent.keyCode);
}
}));
this._register(DOM.addDisposableListener(this.treeContainer, DOM.EventType.KEY_UP, e => {
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
const keyCode = keyboardEvent.keyCode;
// Only handle when in quick navigation mode
if (!this.quickNavigateConfiguration) {
return;
}
// Select element when keys are pressed that signal it
const quickNavKeys = this.quickNavigateConfiguration.keybindings;
const wasTriggerKeyPressed = keyCode === KeyCode.Enter || quickNavKeys.some(k => {
const [firstPart, chordPart] = k.getParts();
if (chordPart) {
return false;
}
if (firstPart.shiftKey && keyCode === KeyCode.Shift) {
if (keyboardEvent.ctrlKey || keyboardEvent.altKey || keyboardEvent.metaKey) {
return false; // this is an optimistic check for the shift key being used to navigate back in quick open
}
return true;
}
if (firstPart.altKey && keyCode === KeyCode.Alt) {
return true;
}
if (firstPart.ctrlKey && keyCode === KeyCode.Ctrl) {
return true;
}
if (firstPart.metaKey && keyCode === KeyCode.Meta) {
return true;
}
return false;
});
if (wasTriggerKeyPressed) {
const focus = this.tree.getFocus();
if (focus) {
this.elementSelected(focus, e);
}
}
}));
// Support layout
if (this.layoutDimensions) {
this.layout(this.layoutDimensions);
}
this.applyStyles();
// Allows focus to switch to next/previous entry after tab into an actionbar item
this._register(DOM.addDisposableListener(this.treeContainer, DOM.EventType.KEY_DOWN, (e: KeyboardEvent) => {
const keyboardEvent: StandardKeyboardEvent = new StandardKeyboardEvent(e);
// Only handle when not in quick navigation mode
if (this.quickNavigateConfiguration) {
return;
}
if (keyboardEvent.keyCode === KeyCode.DownArrow || keyboardEvent.keyCode === KeyCode.UpArrow || keyboardEvent.keyCode === KeyCode.PageDown || keyboardEvent.keyCode === KeyCode.PageUp) {
DOM.EventHelper.stop(e, true);
this.navigateInTree(keyboardEvent.keyCode, keyboardEvent.shiftKey);
this.treeElement.focus();
}
}));
return this.element;
}
style(styles: IQuickOpenStyles): void {
this.styles = styles;
this.applyStyles();
}
protected applyStyles(): void {
if (this.element) {
const foreground = this.styles.foreground ? this.styles.foreground.toString() : null;
const background = this.styles.background ? this.styles.background.toString() : null;
const borderColor = this.styles.borderColor ? this.styles.borderColor.toString() : null;
const widgetShadow = this.styles.widgetShadow ? this.styles.widgetShadow.toString() : null;
this.element.style.color = foreground;
this.element.style.backgroundColor = background;
this.element.style.borderColor = borderColor;
this.element.style.borderWidth = borderColor ? '1px' : null;
this.element.style.borderStyle = borderColor ? 'solid' : null;
this.element.style.boxShadow = widgetShadow ? `0 5px 8px ${widgetShadow}` : null;
}
if (this.progressBar) {
this.progressBar.style({
progressBarBackground: this.styles.progressBarBackground
});
}
if (this.inputBox) {
this.inputBox.style({
inputBackground: this.styles.inputBackground,
inputForeground: this.styles.inputForeground,
inputBorder: this.styles.inputBorder,
inputValidationInfoBackground: this.styles.inputValidationInfoBackground,
inputValidationInfoForeground: this.styles.inputValidationInfoForeground,
inputValidationInfoBorder: this.styles.inputValidationInfoBorder,
inputValidationWarningBackground: this.styles.inputValidationWarningBackground,
inputValidationWarningForeground: this.styles.inputValidationWarningForeground,
inputValidationWarningBorder: this.styles.inputValidationWarningBorder,
inputValidationErrorBackground: this.styles.inputValidationErrorBackground,
inputValidationErrorForeground: this.styles.inputValidationErrorForeground,
inputValidationErrorBorder: this.styles.inputValidationErrorBorder
});
}
if (this.tree && !this.options.treeCreator) {
this.tree.style(this.styles);
}
if (this.renderer) {
this.renderer.updateStyles(this.styles);
}
}
private shouldOpenInBackground(e: StandardKeyboardEvent | StandardMouseEvent): boolean {
// Keyboard
if (e instanceof StandardKeyboardEvent) {
if (e.keyCode !== KeyCode.RightArrow) {
return false; // only for right arrow
}
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) {
return false; // no modifiers allowed
}
// validate the cursor is at the end of the input and there is no selection,
// and if not prevent opening in the background such as the selection can be changed
const element = this.inputBox.inputElement;
return element.selectionEnd === this.inputBox.value.length && element.selectionStart === element.selectionEnd;
}
// Mouse
return e.middleButton;
}
private onType(): void {
const value = this.inputBox.value;
// Adjust help text as needed if present
if (this.helpText) {
if (value) {
DOM.hide(this.helpText);
} else {
DOM.show(this.helpText);
}
}
// Send to callbacks
this.callbacks.onType(value);
}
navigate(next: boolean, quickNavigate?: IQuickNavigateConfiguration): void {
if (this.isVisible()) {
// Transition into quick navigate mode if not yet done
if (!this.quickNavigateConfiguration && quickNavigate) {
this.quickNavigateConfiguration = quickNavigate;
this.tree.domFocus();
}
// Navigate
this.navigateInTree(next ? KeyCode.DownArrow : KeyCode.UpArrow);
}
}
private navigateInTree(keyCode: KeyCode, isShift?: boolean): void {
const model: IModel<any> = this.tree.getInput();
const entries = model ? model.entries : [];
const oldFocus = this.tree.getFocus();
// Normal Navigation
switch (keyCode) {
case KeyCode.DownArrow:
this.tree.focusNext();
break;
case KeyCode.UpArrow:
this.tree.focusPrevious();
break;
case KeyCode.PageDown:
this.tree.focusNextPage();
break;
case KeyCode.PageUp:
this.tree.focusPreviousPage();
break;
case KeyCode.Tab:
if (isShift) {
this.tree.focusPrevious();
} else {
this.tree.focusNext();
}
break;
}
let newFocus = this.tree.getFocus();
// Support cycle-through navigation if focus did not change
if (entries.length > 1 && oldFocus === newFocus) {
// Up from no entry or first entry goes down to last
if (keyCode === KeyCode.UpArrow || (keyCode === KeyCode.Tab && isShift)) {
this.tree.focusLast();
}
// Down from last entry goes to up to first
else if (keyCode === KeyCode.DownArrow || keyCode === KeyCode.Tab && !isShift) {
this.tree.focusFirst();
}
}
// Reveal
newFocus = this.tree.getFocus();
if (newFocus) {
this.tree.reveal(newFocus);
}
}
private elementFocused(value: any, event?: any): void {
if (!value || !this.isVisible()) {
return;
}
// ARIA
const arivaActiveDescendant = this.treeElement.getAttribute('aria-activedescendant');
if (arivaActiveDescendant) {
this.inputElement.setAttribute('aria-activedescendant', arivaActiveDescendant);
} else {
this.inputElement.removeAttribute('aria-activedescendant');
}
const context: IEntryRunContext = { event: event, keymods: this.extractKeyMods(event), quickNavigateConfiguration: this.quickNavigateConfiguration };
this.model!.runner.run(value, Mode.PREVIEW, context);
}
private elementSelected(value: any, event?: any, preferredMode?: Mode): void {
let hide = true;
// Trigger open of element on selection
if (this.isVisible()) {
let mode = preferredMode || Mode.OPEN;
const context: IEntryRunContext = { event, keymods: this.extractKeyMods(event), quickNavigateConfiguration: this.quickNavigateConfiguration };
hide = this.model!.runner.run(value, mode, context);
}
// Hide if command was run successfully
if (hide) {
this.hide(HideReason.ELEMENT_SELECTED);
}
}
private extractKeyMods(event: any): IKeyMods {
return {
ctrlCmd: event && (event.ctrlKey || event.metaKey || (event.payload && event.payload.originalEvent && (event.payload.originalEvent.ctrlKey || event.payload.originalEvent.metaKey))),
alt: event && (event.altKey || (event.payload && event.payload.originalEvent && event.payload.originalEvent.altKey))
};
}
show(prefix: string, options?: IShowOptions): void;
show(input: IModel<any>, options?: IShowOptions): void;
show(param: any, options?: IShowOptions): void {
this.visible = true;
this.isLoosingFocus = false;
this.quickNavigateConfiguration = options ? options.quickNavigateConfiguration : undefined;
// Adjust UI for quick navigate mode
if (this.quickNavigateConfiguration) {
DOM.hide(this.inputContainer);
DOM.show(this.element);
this.tree.domFocus();
}
// Otherwise use normal UI
else {
DOM.show(this.inputContainer);
DOM.show(this.element);
this.inputBox.focus();
}
// Adjust Help text for IE
if (this.helpText) {
if (this.quickNavigateConfiguration || types.isString(param)) {
DOM.hide(this.helpText);
} else {
DOM.show(this.helpText);
}
}
// Show based on param
if (types.isString(param)) {
this.doShowWithPrefix(param);
} else {
if (options && options.value) {
this.restoreLastInput(options.value);
}
this.doShowWithInput(param, options && options.autoFocus ? options.autoFocus : {});
}
// Respect selectAll option
if (options && options.inputSelection && !this.quickNavigateConfiguration) {
this.inputBox.select(options.inputSelection);
}
if (this.callbacks.onShow) {
this.callbacks.onShow();
}
}
private restoreLastInput(lastInput: string) {
this.inputBox.value = lastInput;
this.inputBox.select();
this.callbacks.onType(lastInput);
}
private doShowWithPrefix(prefix: string): void {
this.inputBox.value = prefix;
this.callbacks.onType(prefix);
}
private doShowWithInput(input: IModel<any>, autoFocus: IAutoFocus): void {
this.setInput(input, autoFocus);
}
private setInputAndLayout(input: IModel<any>, autoFocus?: IAutoFocus): void {
this.treeContainer.style.height = `${this.getHeight(input)}px`;
this.tree.setInput(null).then(() => {
this.model = input;
// ARIA
this.inputElement.setAttribute('aria-haspopup', String(input && input.entries && input.entries.length > 0));
return this.tree.setInput(input);
}).then(() => {
// Indicate entries to tree
this.tree.layout();
const entries = input ? input.entries.filter(e => this.isElementVisible(input, e)) : [];
this.updateResultCount(entries.length);
// Handle auto focus
if (entries.length) {
this.autoFocus(input, entries, autoFocus);
}
});
}
private isElementVisible<T>(input: IModel<T>, e: T): boolean {
if (!input.filter) {
return true;
}
return input.filter.isVisible(e);
}
private autoFocus(input: IModel<any>, entries: any[], autoFocus: IAutoFocus = {}): void {
// First check for auto focus of prefix matches
if (autoFocus.autoFocusPrefixMatch) {
let caseSensitiveMatch: any;
let caseInsensitiveMatch: any;
const prefix = autoFocus.autoFocusPrefixMatch;
const lowerCasePrefix = prefix.toLowerCase();
for (const entry of entries) {
const label = input.dataSource.getLabel(entry) || '';
if (!caseSensitiveMatch && label.indexOf(prefix) === 0) {
caseSensitiveMatch = entry;
} else if (!caseInsensitiveMatch && label.toLowerCase().indexOf(lowerCasePrefix) === 0) {
caseInsensitiveMatch = entry;
}
if (caseSensitiveMatch && caseInsensitiveMatch) {
break;
}
}
const entryToFocus = caseSensitiveMatch || caseInsensitiveMatch;
if (entryToFocus) {
this.tree.setFocus(entryToFocus);
this.tree.reveal(entryToFocus, 0.5);
return;
}
}
// Second check for auto focus of first entry
if (autoFocus.autoFocusFirstEntry) {
this.tree.focusFirst();
this.tree.reveal(this.tree.getFocus());
}
// Third check for specific index option
else if (typeof autoFocus.autoFocusIndex === 'number') {
if (entries.length > autoFocus.autoFocusIndex) {
this.tree.focusNth(autoFocus.autoFocusIndex);
this.tree.reveal(this.tree.getFocus());
}
}
// Check for auto focus of second entry
else if (autoFocus.autoFocusSecondEntry) {
if (entries.length > 1) {
this.tree.focusNth(1);
}
}
// Finally check for auto focus of last entry
else if (autoFocus.autoFocusLastEntry) {
if (entries.length > 1) {
this.tree.focusLast();
}
}
}
refresh(input?: IModel<any>, autoFocus?: IAutoFocus): void {
if (!this.isVisible()) {
return;
}
if (!input) {
input = this.tree.getInput();
}
if (!input) {
return;
}
// Apply height & Refresh
this.treeContainer.style.height = `${this.getHeight(input)}px`;
this.tree.refresh().then(() => {
// Indicate entries to tree
this.tree.layout();
const entries = input ? input.entries!.filter(e => this.isElementVisible(input!, e)) : [];
this.updateResultCount(entries.length);
// Handle auto focus
if (autoFocus) {
if (entries.length) {
this.autoFocus(input!, entries, autoFocus);
}
}
});
}
private getHeight(input: IModel<any>): number {
const renderer = input.renderer;
if (!input) {
const itemHeight = renderer.getHeight(null);
return this.options.minItemsToShow ? this.options.minItemsToShow * itemHeight : 0;
}
let height = 0;
let preferredItemsHeight: number | undefined;
if (this.layoutDimensions && this.layoutDimensions.height) {
preferredItemsHeight = (this.layoutDimensions.height - 50 /* subtract height of input field (30px) and some spacing (drop shadow) to fit */) * 0.4 /* max 40% of screen */;
}
if (!preferredItemsHeight || preferredItemsHeight > QuickOpenWidget.MAX_ITEMS_HEIGHT) {
preferredItemsHeight = QuickOpenWidget.MAX_ITEMS_HEIGHT;
}
const entries = input.entries.filter(e => this.isElementVisible(input, e));
const maxEntries = this.options.maxItemsToShow || entries.length;
for (let i = 0; i < maxEntries && i < entries.length; i++) {
const entryHeight = renderer.getHeight(entries[i]);
if (height + entryHeight <= preferredItemsHeight) {
height += entryHeight;
} else {
break;
}
}
return height;
}
updateResultCount(count: number) {
this.resultCount.textContent = nls.localize({ key: 'quickInput.visibleCount', comment: ['This tells the user how many items are shown in a list of items to select from. The items can be anything. Currently not visible, but read by screen readers.'] }, "{0} Results", count);
}
hide(reason?: HideReason): void {
if (!this.isVisible()) {
return;
}
this.visible = false;
DOM.hide(this.element);
this.element.blur();
// Clear input field and clear tree
this.inputBox.value = '';
this.tree.setInput(null);
// ARIA
this.inputElement.setAttribute('aria-haspopup', 'false');
// Reset Tree Height
this.treeContainer.style.height = `${this.options.minItemsToShow ? this.options.minItemsToShow * 22 : 0}px`;
// Clear any running Progress
this.progressBar.stop().hide();
// Clear Focus
if (this.tree.isDOMFocused()) {
this.tree.domBlur();
} else if (this.inputBox.hasFocus()) {
this.inputBox.blur();
}
// Callbacks
if (reason === HideReason.ELEMENT_SELECTED) {
this.callbacks.onOk();
} else {
this.callbacks.onCancel();
}
if (this.callbacks.onHide) {
this.callbacks.onHide(reason!);
}
}
getQuickNavigateConfiguration(): IQuickNavigateConfiguration {
return this.quickNavigateConfiguration!;
}
setPlaceHolder(placeHolder: string): void {
if (this.inputBox) {
this.inputBox.setPlaceHolder(placeHolder);
}
}
setValue(value: string, selectionOrStableHint?: [number, number] | null): void {
if (this.inputBox) {
this.inputBox.value = value;
if (selectionOrStableHint === null) {
// null means stable-selection
} else if (Array.isArray(selectionOrStableHint)) {
const [start, end] = selectionOrStableHint;
this.inputBox.select({ start, end });
} else {
this.inputBox.select();
}
}
}
setPassword(isPassword: boolean): void {
if (this.inputBox) {
this.inputBox.inputElement.type = isPassword ? 'password' : 'text';
}
}
setInput(input: IModel<any>, autoFocus?: IAutoFocus, ariaLabel?: string): void {
if (!this.isVisible()) {
return;
}
// If the input changes, indicate this to the tree
if (!!this.getInput()) {
this.onInputChanging();
}
// Adapt tree height to entries and apply input
this.setInputAndLayout(input, autoFocus);
// Apply ARIA
if (this.inputBox) {
this.inputBox.setAriaLabel(ariaLabel || DEFAULT_INPUT_ARIA_LABEL);
}
}
private onInputChanging(): void {
if (this.inputChangingTimeoutHandle) {
clearTimeout(this.inputChangingTimeoutHandle);
this.inputChangingTimeoutHandle = null;
}
// when the input is changing in quick open, we indicate this as CSS class to the widget
// for a certain timeout. this helps reducing some hectic UI updates when input changes quickly
DOM.addClass(this.element, 'content-changing');
this.inputChangingTimeoutHandle = setTimeout(() => {
DOM.removeClass(this.element, 'content-changing');
}, 500);
}
getInput(): IModel<any> {
return this.tree.getInput();
}
showInputDecoration(decoration: Severity): void {
if (this.inputBox) {
this.inputBox.showMessage({ type: decoration === Severity.Info ? MessageType.INFO : decoration === Severity.Warning ? MessageType.WARNING : MessageType.ERROR, content: '' });
}
}
clearInputDecoration(): void {
if (this.inputBox) {
this.inputBox.hideMessage();
}
}
focus(): void {
if (this.isVisible() && this.inputBox) {
this.inputBox.focus();
}
}
accept(): void {
if (this.isVisible()) {
const focus = this.tree.getFocus();
if (focus) {
this.elementSelected(focus);
}
}
}
getProgressBar(): ProgressBar {
return this.progressBar;
}
getInputBox(): InputBox {
return this.inputBox;
}
setExtraClass(clazz: string | null): void {
const previousClass = this.element.getAttribute('quick-open-extra-class');
if (previousClass) {
DOM.removeClasses(this.element, previousClass);
}
if (clazz) {
DOM.addClasses(this.element, clazz);
this.element.setAttribute('quick-open-extra-class', clazz);
} else if (previousClass) {
this.element.removeAttribute('quick-open-extra-class');
}
}
isVisible(): boolean {
return this.visible;
}
layout(dimension: DOM.Dimension): void {
this.layoutDimensions = dimension;
// Apply to quick open width (height is dynamic by number of items to show)
const quickOpenWidth = Math.min(this.layoutDimensions.width * 0.62 /* golden cut */, QuickOpenWidget.MAX_WIDTH);
if (this.element) {
// quick open
this.element.style.width = `${quickOpenWidth}px`;
this.element.style.marginLeft = `-${quickOpenWidth / 2}px`;
// input field
this.inputContainer.style.width = `${quickOpenWidth - 12}px`;
}
}
private gainingFocus(): void {
this.isLoosingFocus = false;
}
private loosingFocus(e: FocusEvent): void {
if (!this.isVisible()) {
return;
}
const relatedTarget = e.relatedTarget as HTMLElement;
if (!this.quickNavigateConfiguration && DOM.isAncestor(relatedTarget, this.element)) {
return; // user clicked somewhere into quick open widget, do not close thereby
}
this.isLoosingFocus = true;
setTimeout(() => {
if (!this.isLoosingFocus || this.isDisposed) {
return;
}
const veto = this.callbacks.onFocusLost && this.callbacks.onFocusLost();
if (!veto) {
this.hide(HideReason.FOCUS_LOST);
}
}, 0);
}
dispose(): void {
super.dispose();
this.isDisposed = true;
}
}