diff --git a/src/sql/base/browser/ui/table/tableCellEditorFactory.ts b/src/sql/base/browser/ui/table/tableCellEditorFactory.ts index 0b12572b8b..5f2387f276 100644 --- a/src/sql/base/browser/ui/table/tableCellEditorFactory.ts +++ b/src/sql/base/browser/ui/table/tableCellEditorFactory.ts @@ -10,12 +10,16 @@ import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview import { KeyCode } from 'vs/base/common/keyCodes'; import * as DOM from 'vs/base/browser/dom'; import { Dropdown } from 'sql/base/browser/ui/editableDropdown/browser/dropdown'; +import { debounce } from 'vs/base/common/decorators'; +import { Event } from 'vs/base/common/event'; +import { Disposable } from 'vs/base/common/lifecycle'; export interface ITableCellEditorOptions { valueGetter?: (item: Slick.SlickData, column: Slick.Column) => string, valueSetter?: (context: any, row: number, item: Slick.SlickData, column: Slick.Column, value: string) => void, optionsGetter?: (item: Slick.SlickData, column: Slick.Column) => string[], - editorStyler: (component: InputBox | SelectBox | Dropdown) => void + editorStyler: (component: InputBox | SelectBox | Dropdown) => void, + onStyleChange: Event; } export class TableCellEditorFactory { @@ -32,18 +36,20 @@ export class TableCellEditorFactory { optionsGetter: options.optionsGetter ?? function (item, column) { return []; }, - editorStyler: options.editorStyler + editorStyler: options.editorStyler, + onStyleChange: options.onStyleChange }; } public getTextEditorClass(context: any, inputType: 'text' | 'number' = 'text'): any { const self = this; - class TextEditor { + class TextEditor extends Disposable { private _originalValue: string; private _input: InputBox; private _keyCaptureList: number[]; constructor(private _args: Slick.Editors.EditorOptions) { + super(); this.init(); const keycodesToCapture = [KeyCode.Home, KeyCode.End, KeyCode.UpArrow, KeyCode.DownArrow, KeyCode.LeftArrow, KeyCode.RightArrow]; this._keyCaptureList = keycodesToCapture.map(keycode => getCodeForKeyCode(keycode)); @@ -63,10 +69,26 @@ export class TableCellEditorFactory { self._options.editorStyler(this._input); this._input.element.style.height = '100%'; this._input.focus(); + this._input.onDidChange(async () => { + await this.commitEdit(); + }); + this._register(this._input); + this._register(self._options.onStyleChange(() => { + self._options.editorStyler(this._input); + })); + } + + @debounce(200) + private async commitEdit(): Promise { + if (this.isValueChanged()) { + const item = this._args.grid.getDataItem(this._args.grid.getActiveCell().row); + await this.applyValue(item, this._input.value); + this._originalValue = this._input.value; + } } public destroy(): void { - this._input.dispose(); + this.dispose(); } public focus(): void { @@ -84,8 +106,7 @@ export class TableCellEditorFactory { } public isValueChanged(): boolean { - return this._input.value !== this._originalValue.toString(); - + return this._input.value !== this._originalValue; } public serializeValue(): any { @@ -104,13 +125,13 @@ export class TableCellEditorFactory { public getDropdownEditorClass(context: any, defaultOptions: string[], isEditable?: boolean): any { const self = this; - class TextEditor { + class DropdownEditor extends Disposable { private _originalValue: string; - private _selectBox: SelectBox; - private _dropdown: Dropdown; + private _component: SelectBox | Dropdown; private _keyCaptureList: number[]; constructor(private _args: Slick.Editors.EditorOptions) { + super(); this.init(); const keycodesToCapture = [KeyCode.Home, KeyCode.End, KeyCode.UpArrow, KeyCode.DownArrow, KeyCode.LeftArrow, KeyCode.RightArrow]; this._keyCaptureList = keycodesToCapture.map(keycode => getCodeForKeyCode(keycode)); @@ -126,37 +147,43 @@ export class TableCellEditorFactory { public init(): void { const container = DOM.$(''); this._args.container.appendChild(container); + container.style.height = '100%'; + container.style.width = '100%'; if (isEditable) { - this._dropdown = new Dropdown(container, self._contextViewProvider); - container.style.height = '100%'; - container.style.width = '100%'; - self._options.editorStyler(this._dropdown); - this._dropdown.focus(); + this._component = new Dropdown(container, self._contextViewProvider); + this._component.onValueChange(async () => { + await this.commitEdit(); + }); } else { - this._selectBox = new SelectBox([], undefined, self._contextViewProvider); - container.style.height = '100%'; - container.style.width = '100%'; - this._selectBox.render(container); - this._selectBox.selectElem.style.height = '100%'; - self._options.editorStyler(this._selectBox); - this._selectBox.focus(); + this._component = new SelectBox([], undefined, self._contextViewProvider); + this._component.render(container); + this._component.selectElem.style.height = '100%'; + this._component.onDidSelect(async () => { + await this.commitEdit(); + }); + } + self._options.editorStyler(this._component); + this._component.focus(); + this._register(this._component); + this._register(self._options.onStyleChange(() => { + self._options.editorStyler(this._component); + })); + } + + private async commitEdit(): Promise { + if (this.isValueChanged()) { + const item = this._args.grid.getDataItem(this._args.grid.getActiveCell().row); + await this.applyValue(item, this._component.value); + this._originalValue = this._component.value; } } public destroy(): void { - if (isEditable) { - this._dropdown.dispose(); - } else { - this._selectBox.dispose(); - } + this.dispose(); } public focus(): void { - if (isEditable) { - this._dropdown.focus(); - } else { - this._selectBox.focus(); - } + this._component.focus(); } public loadValue(item: Slick.SlickData): void { @@ -164,12 +191,12 @@ export class TableCellEditorFactory { const options = self._options.optionsGetter(item, this._args.column) ?? defaultOptions; const idx = options?.indexOf(this._originalValue); if (idx > -1) { - if (isEditable) { - this._dropdown.values = options; - this._dropdown.value = options[idx]; + if (this._component instanceof Dropdown) { + this._component.values = options; + this._component.value = options[idx]; } else { - this._selectBox.setOptions(options); - this._selectBox.select(idx); + this._component.setOptions(options); + this._component.select(idx); } } } @@ -180,19 +207,11 @@ export class TableCellEditorFactory { } public isValueChanged(): boolean { - if (isEditable) { - return this._dropdown.value !== this._originalValue.toString(); - } else { - return this._selectBox.value !== this._originalValue.toString(); - } + return this._component.value !== this._originalValue; } public serializeValue(): any { - if (isEditable) { - return this._dropdown.value; - } else { - return this._selectBox.value; - } + return this._component.value; } public validate(): Slick.ValidateResults { @@ -202,6 +221,6 @@ export class TableCellEditorFactory { }; } } - return TextEditor; + return DropdownEditor; } } diff --git a/src/sql/workbench/browser/designer/designer.ts b/src/sql/workbench/browser/designer/designer.ts index 5ec67eebf9..e11207a818 100644 --- a/src/sql/workbench/browser/designer/designer.ts +++ b/src/sql/workbench/browser/designer/designer.ts @@ -11,7 +11,7 @@ import { from 'sql/workbench/browser/designer/interfaces'; import { IPanelTab, ITabbedPanelStyles, TabbedPanel } from 'sql/base/browser/ui/panel/panel'; import * as DOM from 'vs/base/browser/dom'; -import { Event } from 'vs/base/common/event'; +import { Emitter, Event } from 'vs/base/common/event'; import { Orientation, Sizing, SplitView } from 'vs/base/browser/ui/splitview/splitview'; import { Disposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IInputBoxStyles, InputBox } from 'sql/base/browser/ui/inputBox/inputBox'; @@ -45,6 +45,7 @@ import { alert } from 'vs/base/browser/ui/aria/aria'; import { layoutDesignerTable, TableHeaderRowHeight, TableRowHeight } from 'sql/workbench/browser/designer/designerTableUtil'; import { Dropdown, IDropdownStyles } from 'sql/base/browser/ui/editableDropdown/browser/dropdown'; import { IListStyles } from 'vs/base/browser/ui/list/listWidget'; +import { debounce } from 'vs/base/common/decorators'; export interface IDesignerStyle { tabbedPanelStyles?: ITabbedPanelStyles; @@ -96,6 +97,7 @@ export class Designer extends Disposable implements IThemable { private _groupHeaders: HTMLElement[] = []; private _messagesView: DesignerMessagesTabPanelView; private _scriptEditorView: DesignerScriptEditorTabPanelView; + private _onStyleChangeEventEmitter = new Emitter(); constructor(private readonly _container: HTMLElement, @IInstantiationService private readonly _instantiationService: IInstantiationService, @@ -122,7 +124,8 @@ export class Designer extends Disposable implements IThemable { }, editorStyler: (component) => { this.styleComponent(component); - } + }, + onStyleChange: this._onStyleChangeEventEmitter.event }, this._contextViewProvider ); this._loadingSpinner = new LoadingSpinner(this._container, { showText: true, fullSize: true }); @@ -254,6 +257,7 @@ export class Designer extends Disposable implements IThemable { }); this._propertiesPane.descriptionElement.style.borderColor = styles.paneSeparator.toString(); + this._onStyleChangeEventEmitter.fire(); } public layout(dimension: DOM.Dimension) { @@ -718,9 +722,11 @@ export class Designer extends Disposable implements IThemable { ariaLabel: inputProperties.title, type: inputProperties.inputType, }); - input.onLoseFocus((args) => { - if (args.hasChanged) { - this.handleEdit({ type: DesignerEditType.Update, path: propertyPath, value: args.value, source: view }); + input.onDidChange(() => { + // The supress edit processing check is done in the handleEdit method, but since we have debounce operation on input box we + // have to do it here to avoid treating system originated value setting operation as user edits. + if (!this._supressEditProcessing) { + this.handleInputBoxEdit({ type: DesignerEditType.Update, path: propertyPath, value: input.value, source: view }); } }); input.onInputFocus(() => { @@ -937,6 +943,11 @@ export class Designer extends Disposable implements IThemable { return component; } + @debounce(200) + private handleInputBoxEdit(edit: DesignerEdit) { + this.handleEdit(edit); + } + private startLoading(message: string, timeout: number): void { this._loadingTimeoutHandle = setTimeout(() => { this._loadingSpinner.loadingMessage = message;