Rework editableDropdown to not need platform (#5189)

* rework editableDropdown to not need platform

* rework editable dropdown to not depend on platform

* fix compile

* fix focus bluring
This commit is contained in:
Anthony Dresser
2019-04-30 09:25:04 -07:00
committed by GitHub
parent aacc0eca67
commit 56342af140
16 changed files with 53 additions and 58 deletions

View File

@@ -1,20 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Action } from 'vs/base/common/actions';
export class ToggleDropdownAction extends Action {
private static readonly ID = 'dropdownAction.toggle';
private static readonly ICON = 'dropdown-arrow';
constructor(private _fn: () => any, label: string) {
super(ToggleDropdownAction.ID, label, ToggleDropdownAction.ICON);
}
public run(): Promise<boolean> {
this._fn();
return Promise.resolve(true);
}
}

View File

@@ -1,327 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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/dropdownList';
import { ToggleDropdownAction } from './actions';
import { DropdownDataSource, DropdownFilter, DropdownModel, DropdownRenderer, DropdownController } from './dropdownTree';
import { IContextViewProvider, ContextView } from 'vs/base/browser/ui/contextview/contextview';
import { mixin } from 'vs/base/common/objects';
import { InputBox, IInputBoxStyles } from 'sql/base/browser/ui/inputBox/inputBox';
import { IMessage, MessageType } from 'vs/base/browser/ui/inputbox/inputBox';
import { IListStyles } from 'vs/base/browser/ui/list/listWidget';
import * as DOM from 'vs/base/browser/dom';
import { Disposable } from 'vs/base/common/lifecycle';
import { Color } from 'vs/base/common/color';
import * as nls from 'vs/nls';
import { Event, Emitter } from 'vs/base/common/event';
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { Tree } from 'vs/base/parts/tree/browser/treeImpl';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
export interface IDropdownOptions extends IDropdownStyles {
/**
* Whether or not a options in the list must be selected or a "new" option can be set
*/
strictSelection?: boolean;
/**
* Maximum height of the dropdown, defaults to 500
*/
maxHeight?: number;
/**
* Initial values for the dropdown, can be set afterwards
*/
values?: string[];
/**
* Placeholder to use in the input
*/
placeholder?: string;
/**
* Warning message to show when the input is not part of the supplied list, only used if strictSelection = false
*/
warningMessage?: string;
/**
* Error Message to show if input is not part of the supplied list, only used if strictSelection = false
*/
errorMessage?: string;
/**
* Value to use as aria-label for the input box
*/
ariaLabel?: string;
/**
* Label for the dropdown action
*/
actionLabel: string;
}
export interface IDropdownStyles {
contextBackground?: Color;
contextBorder?: Color;
}
const errorMessage = nls.localize('editableDropdown.errorValidate', "Must be an option from the list");
const defaults: IDropdownOptions = {
strictSelection: true,
maxHeight: 300,
errorMessage: errorMessage,
contextBorder: Color.fromHex('#696969'),
actionLabel: nls.localize('dropdownAction.toggle', "Toggle dropdown")
};
export class Dropdown extends Disposable {
private _el: HTMLElement;
private _inputContainer: HTMLElement;
private _treeContainer: HTMLElement;
private _input: InputBox;
private _tree: Tree;
private _options: IDropdownOptions;
private _toggleAction: ToggleDropdownAction;
// we have to create our own contextview since otherwise inputbox will override ours
private _contextView: ContextView;
private _dataSource = new DropdownDataSource();
private _filter = new DropdownFilter();
private _renderer = new DropdownRenderer();
private _controller = new DropdownController();
public fireOnTextChange: boolean;
private _onBlur = this._register(new Emitter<void>());
public onBlur: Event<void> = this._onBlur.event;
private _onValueChange = this._register(new Emitter<string>());
public onValueChange: Event<string> = this._onValueChange.event;
private _onFocus = this._register(new Emitter<void>());
public onFocus: Event<void> = this._onFocus.event;
constructor(
container: HTMLElement,
contextViewService: IContextViewProvider,
readonly layoutService: ILayoutService,
opt?: IDropdownOptions
) {
super();
this._contextView = new ContextView(layoutService.container);
this._options = opt || Object.create(null);
mixin(this._options, defaults, false);
this._el = DOM.append(container, DOM.$('.monaco-dropdown'));
this._el.style.width = '100%';
this._inputContainer = DOM.append(this._el, DOM.$('.dropdown-input'));
this._inputContainer.style.width = '100%';
this._treeContainer = DOM.$('.dropdown-tree');
this._toggleAction = new ToggleDropdownAction(() => {
this._showList();
this._tree.domFocus();
this._tree.focusFirst();
}, this._options.actionLabel);
this._input = new InputBox(this._inputContainer, contextViewService, {
validationOptions: {
// @SQLTODO
//showMessage: false,
validation: v => this._inputValidator(v)
},
placeholder: this._options.placeholder,
actions: [this._toggleAction],
ariaLabel: this._options.ariaLabel
});
// Clear title from input box element (defaults to placeholder value) since we don't want a tooltip for the selected value
// in the text box - we already have tooltips for each item in the dropdown itself.
this._input.inputElement.title = '';
this._register(DOM.addDisposableListener(this._input.inputElement, DOM.EventType.CLICK, () => {
this._showList();
}));
this._register(DOM.addDisposableListener(this._input.inputElement, DOM.EventType.BLUR, () => {
if (!this._tree.isDOMFocused()) {
this._onBlur.fire();
}
}));
this._register(DOM.addStandardDisposableListener(this._input.inputElement, DOM.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => {
switch (e.keyCode) {
case KeyCode.Enter:
if (this._contextView.isVisible()) {
if (this._input.validate()) {
this._onValueChange.fire(this._input.value);
}
} else {
this._showList();
}
e.stopPropagation();
break;
case KeyCode.Escape:
if (this._treeContainer.parentElement) {
this._input.validate();
this._onBlur.fire();
this._contextView.hide();
e.stopPropagation();
}
break;
case KeyCode.Tab:
this._input.validate();
this._onBlur.fire();
this._contextView.hide();
e.stopPropagation();
break;
case KeyCode.DownArrow:
if (!this._treeContainer.parentElement) {
this._showList();
}
this._tree.domFocus();
this._tree.focusFirst();
e.stopPropagation();
e.preventDefault();
break;
}
}));
this._tree = new Tree(this._treeContainer, {
dataSource: this._dataSource,
filter: this._filter,
renderer: this._renderer,
controller: this._controller
}, { paddingOnRow: false, indentPixels: 0, twistiePixels: 0 });
this.values = this._options.values;
this._controller.onSelectionChange(e => {
this.value = e.value;
this._onValueChange.fire(e.value);
this._input.focus();
this._contextView.hide();
});
this._controller.onDropdownEscape(() => {
this._input.focus();
this._contextView.hide();
});
this._input.onDidChange(e => {
if (this._dataSource.options) {
this._filter.filterString = e;
this._layoutTree();
}
if (this.fireOnTextChange) {
this.value = e;
this._onValueChange.fire(e);
}
});
this._register(this._contextView);
this._register(this._tree);
this._register(this._input);
this._register(this._contextView);
}
private _showList(): void {
if (this._input.isEnabled) {
this._onFocus.fire();
this._filter.filterString = '';
this._contextView.show({
getAnchor: () => this._inputContainer,
render: container => {
DOM.append(container, this._treeContainer);
this._layoutTree();
return { dispose: () => { } };
},
onDOMEvent: e => {
if (!DOM.isAncestor((<HTMLElement>e.srcElement), this._el) && !DOM.isAncestor((<HTMLElement>e.srcElement), this._treeContainer)) {
this._input.validate();
this._onBlur.fire();
this._contextView.hide();
}
}
});
}
}
private _layoutTree(): void {
if (this._dataSource && this._dataSource.options && this._dataSource.options.length > 0) {
let filteredLength = this._dataSource.options.reduce((p, i) => {
if (this._filter.isVisible(undefined, i)) {
return p + 1;
} else {
return p;
}
}, 0);
let height = filteredLength * this._renderer.getHeight() > this._options.maxHeight! ? this._options.maxHeight! : filteredLength * this._renderer.getHeight();
this._treeContainer.style.height = height + 'px';
this._treeContainer.style.width = DOM.getContentWidth(this._inputContainer) - 2 + 'px';
this._tree.layout(parseInt(this._treeContainer.style.height));
this._tree.refresh();
}
}
public set values(vals: string[] | undefined) {
if (vals) {
this._filter.filterString = '';
this._dataSource.options = vals.map(i => { return { value: i }; });
let height = this._dataSource.options.length * 22 > this._options.maxHeight! ? this._options.maxHeight! : this._dataSource.options.length * 22;
this._treeContainer.style.height = height + 'px';
this._treeContainer.style.width = DOM.getContentWidth(this._inputContainer) - 2 + 'px';
this._tree.layout(parseInt(this._treeContainer.style.height));
this._tree.setInput(new DropdownModel());
this._input.validate();
}
}
public get value(): string {
return this._input.value;
}
public set value(val: string) {
this._input.value = val;
}
public focus() {
this._input.focus();
}
public blur() {
this._input.blur();
this._contextView.hide();
}
style(style: IListStyles & IInputBoxStyles & IDropdownStyles) {
this._tree.style(style);
this._input.style(style);
this._treeContainer.style.backgroundColor = style.contextBackground ? style.contextBackground.toString() : null;
this._treeContainer.style.outline = `1px solid ${style.contextBorder || this._options.contextBorder}`;
}
private _inputValidator(value: string): IMessage | null {
if (this._dataSource.options && !this._dataSource.options.find(i => i.value === value)) {
if (this._options.strictSelection && this._options.errorMessage) {
return {
content: this._options.errorMessage,
type: MessageType.ERROR
};
} else if (this._options.warningMessage) {
return {
content: this._options.warningMessage,
type: MessageType.WARNING
};
}
}
return null;
}
public set enabled(val: boolean) {
this._input.setEnabled(val);
this._toggleAction.enabled = val;
}
public get enabled(): boolean {
return this._input.isEnabled();
}
}

View File

@@ -1,134 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as tree from 'vs/base/parts/tree/browser/tree';
import * as TreeDefaults from 'vs/base/parts/tree/browser/treeDefaults';
import { generateUuid } from 'vs/base/common/uuid';
import * as DOM from 'vs/base/browser/dom';
import { Event, Emitter } from 'vs/base/common/event';
import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent';
export interface Template {
label: HTMLElement;
row: HTMLElement;
}
export interface Resource {
value: string;
}
export class DropdownModel {
public static ID = generateUuid();
}
export class DropdownRenderer implements tree.IRenderer {
public getHeight(): number {
return 22;
}
public getTemplateId(): string {
return '';
}
public renderTemplate(tree: tree.ITree, templateId: string, container: HTMLElement): Template {
const row = DOM.$('div.list-row');
row.style.height = '22px';
row.style.paddingLeft = '5px';
DOM.append(container, row);
const label = DOM.$('span.label');
label.style.margin = 'auto';
label.style.verticalAlign = 'middle';
DOM.append(row, label);
return { label, row };
}
public renderElement(tree: tree.ITree, element: Resource, templateId: string, templateData: Template): void {
templateData.label.innerText = element.value;
templateData.row.title = element.value;
}
public disposeTemplate(tree: tree.ITree, templateId: string, templateData: Template): void {
// no op
}
}
export class DropdownDataSource implements tree.IDataSource {
public options: Array<Resource>;
public getId(tree: tree.ITree, element: Resource | DropdownModel): string {
if (element instanceof DropdownModel) {
return DropdownModel.ID;
} else {
return (element as Resource).value;
}
}
public hasChildren(tree: tree.ITree, element: Resource | DropdownModel): boolean {
if (element instanceof DropdownModel) {
return true;
} else {
return false;
}
}
public getChildren(tree: tree.ITree, element: Resource | DropdownModel): Promise<any> {
if (element instanceof DropdownModel) {
return Promise.resolve(this.options);
} else {
return Promise.resolve(undefined);
}
}
public getParent(tree: tree.ITree, element: Resource | DropdownModel): Promise<any> {
if (element instanceof DropdownModel) {
return Promise.resolve(undefined);
} else {
return Promise.resolve(new DropdownModel());
}
}
}
export class DropdownFilter extends TreeDefaults.DefaultFilter {
public filterString: string;
public isVisible(tree: tree.ITree | undefined, element: Resource): boolean {
return element.value.toLowerCase().includes(this.filterString.toLowerCase());
}
}
export class DropdownController extends TreeDefaults.DefaultController {
private _onSelectionChange = new Emitter<Resource>();
public readonly onSelectionChange: Event<Resource> = this._onSelectionChange.event;
private _onDropdownEscape = new Emitter<void>();
public readonly onDropdownEscape: Event<void> = this._onDropdownEscape.event;
constructor() {
super();
}
protected onEscape(tree: tree.ITree, event: IKeyboardEvent): boolean {
let response = super.onEscape(tree, event);
this._onDropdownEscape.fire();
return response;
}
protected onLeftClick(tree: tree.ITree, element: any, eventish: TreeDefaults.ICancelableEvent, origin: string): boolean {
let response = super.onLeftClick(tree, element, eventish, origin);
if (response) {
this._onSelectionChange.fire(tree.getSelection()[0]);
}
return response;
}
protected onEnter(tree: tree.ITree, event: IKeyboardEvent): boolean {
let response = super.onEnter(tree, event);
if (response) {
this._onSelectionChange.fire(tree.getSelection()[0]);
}
return response;
}
}

View File

@@ -1,75 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import {
Component, Inject, forwardRef, ElementRef, OnInit, Input,
Output, OnChanges, SimpleChanges, EventEmitter
} from '@angular/core';
import { Dropdown, IDropdownOptions } from 'sql/base/browser/ui/editableDropdown/dropdown';
import { AngularDisposable } from 'sql/base/node/lifecycle';
import { attachEditableDropdownStyler } from 'sql/platform/theme/common/styler';
import { IContextViewService } from 'vs/platform/contextview/browser/contextView';
import { IThemeService } from 'vs/platform/theme/common/themeService';
import { ILayoutService } from 'vs/platform/layout/browser/layoutService';
@Component({
selector: 'editable-select-box',
template: ''
})
export class EditableDropDown extends AngularDisposable implements OnInit, OnChanges {
private _selectbox: Dropdown;
@Input() options: string[];
@Input() selectedOption: string;
@Input() onlyEmitOnChange = false;
@Output() onDidSelect = new EventEmitter<string>();
private _previousVal: string;
constructor(
@Inject(forwardRef(() => ElementRef)) private _el: ElementRef,
@Inject(IThemeService) private themeService: IThemeService,
@Inject(IContextViewService) private contextViewService: IContextViewService,
@Inject(ILayoutService) private layoutService: ILayoutService
) {
super();
}
ngOnInit(): void {
let dropdownOptions: IDropdownOptions = {
values: [],
strictSelection: false,
placeholder: '',
maxHeight: 125,
ariaLabel: '',
actionLabel: ''
};
this._selectbox = new Dropdown(this._el.nativeElement, this.contextViewService, this.layoutService, dropdownOptions);
this._selectbox.values = this.options;
this._selectbox.value = this.selectedOption;
this._selectbox.onValueChange(e => {
if (this.onlyEmitOnChange) {
if (this._previousVal !== e) {
this.onDidSelect.emit(e);
this._previousVal = e;
}
} else {
this.onDidSelect.emit(e);
}
});
this._register(attachEditableDropdownStyler(this._selectbox, this.themeService));
}
ngOnChanges(changes: SimpleChanges): void {
}
public get value(): string {
return this._selectbox.value;
}
}

View File

@@ -1,49 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
.vs .dropdown-arrow.icon {
background-image: url("dropdownarrow.svg");
}
.vs-dark .dropdown-arrow.icon,
.hc-black .dropdown-arrow.icon {
background-image: url("dropdownarrow_inverse.svg");
}
.monaco-dropdown .monaco-action-bar .action-label.icon.dropdown-arrow {
padding: 0;
background-size: 10px;
background-position: 50%;
}
.monaco-dropdown .monaco-action-bar .action-item {
margin: 0;
}
.dropdown-tree .list-row {
width: 100%;
box-sizing: border-box;
}
.dropdown-tree .content {
width: 100%;
}
.dropdown-tree .list-row .label {
width: 100%;
display: inline-block;
text-overflow: ellipsis;
overflow: hidden;
}
.connection-input .monaco-dropdown .monaco-action-bar .action-label.icon.dropdown-arrow {
background-position: 50%;
background-size: cover;
}
.connection-input .monaco-inputbox .monaco-action-bar .action-item .icon {
width: 8px;
height: 8px;
}

View File

@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12"><defs><style>.cls-1{fill:#333;}</style></defs><title>dropdownarrow</title><path class="cls-1" d="M0,3H12L6,9Z"/></svg>

Before

Width:  |  Height:  |  Size: 211 B

View File

@@ -1 +0,0 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12"><defs><style>.cls-1{fill:#fff;}</style></defs><title>dropdownarrow_inverse</title><path class="cls-1" d="M0,3H12L6,9Z"/></svg>

Before

Width:  |  Height:  |  Size: 219 B