提交 5352c43a 编写于 作者: B Benjamin Pasero

dialog - add support for showing inputs

上级 e6f2c117
......@@ -2,6 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/** Dialog: Modal Block */
.monaco-dialog-modal-block {
position: fixed;
......@@ -46,7 +47,6 @@
margin-left: 4px;
}
/** Dialog: Message Row */
.monaco-dialog-box .dialog-message-row {
display: flex;
......@@ -100,6 +100,7 @@
outline-style: solid;
}
/** Dialog: Checkbox */
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-checkbox-row {
padding: 15px 0px 0px;
display: flex;
......@@ -112,6 +113,16 @@
-ms-user-select: none;
}
/** Dialog: Input */
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-input {
padding: 15px 0px 0px;
display: flex;
}
.monaco-dialog-box .dialog-message-row .dialog-message-container .dialog-message-input .monaco-inputbox {
flex: 1;
}
/** Dialog: Buttons Row */
.monaco-dialog-box > .dialog-buttons-row {
display: flex;
......
......@@ -18,34 +18,46 @@ import { mnemonicButtonLabel } from 'vs/base/common/labels';
import { isMacintosh, isLinux } from 'vs/base/common/platform';
import { SimpleCheckbox, ISimpleCheckboxStyles } from 'vs/base/browser/ui/checkbox/checkbox';
import { Codicon, registerIcon } from 'vs/base/common/codicons';
import { InputBox } from 'vs/base/browser/ui/inputbox/inputBox';
export interface IDialogInputOptions {
readonly placeholder?: string;
readonly type?: 'text' | 'password';
readonly value?: string;
}
export interface IDialogOptions {
cancelId?: number;
detail?: string;
checkboxLabel?: string;
checkboxChecked?: boolean;
type?: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending';
keyEventProcessor?: (event: StandardKeyboardEvent) => void;
readonly cancelId?: number;
readonly detail?: string;
readonly checkboxLabel?: string;
readonly checkboxChecked?: boolean;
readonly type?: 'none' | 'info' | 'error' | 'question' | 'warning' | 'pending';
readonly inputs?: IDialogInputOptions[];
readonly keyEventProcessor?: (event: StandardKeyboardEvent) => void;
}
export interface IDialogResult {
button: number;
checkboxChecked?: boolean;
readonly button: number;
readonly checkboxChecked?: boolean;
readonly values?: string[];
}
export interface IDialogStyles extends IButtonStyles, ISimpleCheckboxStyles {
dialogForeground?: Color;
dialogBackground?: Color;
dialogShadow?: Color;
dialogBorder?: Color;
errorIconForeground?: Color;
warningIconForeground?: Color;
infoIconForeground?: Color;
readonly dialogForeground?: Color;
readonly dialogBackground?: Color;
readonly dialogShadow?: Color;
readonly dialogBorder?: Color;
readonly errorIconForeground?: Color;
readonly warningIconForeground?: Color;
readonly infoIconForeground?: Color;
readonly inputBackground?: Color;
readonly inputForeground?: Color;
readonly inputBorder?: Color;
}
interface ButtonMapEntry {
label: string;
index: number;
readonly label: string;
readonly index: number;
}
const dialogErrorIcon = registerIcon('dialog-error', Codicon.error);
......@@ -54,30 +66,30 @@ const dialogInfoIcon = registerIcon('dialog-info', Codicon.info);
const dialogCloseIcon = registerIcon('dialog-close', Codicon.close);
export class Dialog extends Disposable {
private element: HTMLElement | undefined;
private shadowElement: HTMLElement | undefined;
private modal: HTMLElement | undefined;
private buttonsContainer: HTMLElement | undefined;
private messageDetailElement: HTMLElement | undefined;
private iconElement: HTMLElement | undefined;
private checkbox: SimpleCheckbox | undefined;
private toolbarContainer: HTMLElement | undefined;
private readonly element: HTMLElement;
private readonly shadowElement: HTMLElement;
private modalElement: HTMLElement | undefined;
private readonly buttonsContainer: HTMLElement;
private readonly messageDetailElement: HTMLElement;
private readonly iconElement: HTMLElement;
private readonly checkbox: SimpleCheckbox | undefined;
private readonly toolbarContainer: HTMLElement;
private buttonGroup: ButtonGroup | undefined;
private styles: IDialogStyles | undefined;
private focusToReturn: HTMLElement | undefined;
private checkboxHasFocus: boolean = false;
private buttons: string[];
private readonly inputs: InputBox[];
private readonly buttons: string[];
constructor(private container: HTMLElement, private message: string, buttons: string[], private options: IDialogOptions) {
super();
this.modal = this.container.appendChild($(`.monaco-dialog-modal-block${options.type === 'pending' ? '.dimmed' : ''}`));
this.shadowElement = this.modal.appendChild($('.dialog-shadow'));
this.modalElement = this.container.appendChild($(`.monaco-dialog-modal-block${options.type === 'pending' ? '.dimmed' : ''}`));
this.shadowElement = this.modalElement.appendChild($('.dialog-shadow'));
this.element = this.shadowElement.appendChild($('.monaco-dialog-box'));
this.element.setAttribute('role', 'dialog');
hide(this.element);
// If no button is provided, default to OK
this.buttons = buttons.length ? buttons : [nls.localize('ok', "OK")];
this.buttons = buttons.length ? buttons : [nls.localize('ok', "OK")]; // If no button is provided, default to OK
const buttonsRowElement = this.element.appendChild($('.dialog-buttons-row'));
this.buttonsContainer = buttonsRowElement.appendChild($('.dialog-buttons'));
......@@ -94,6 +106,25 @@ export class Dialog extends Disposable {
this.messageDetailElement = messageContainer.appendChild($('.dialog-message-detail'));
this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message;
if (this.options.inputs) {
this.inputs = this.options.inputs.map(input => {
const inputRowElement = messageContainer.appendChild($('.dialog-message-input'));
const inputBox = this._register(new InputBox(inputRowElement, undefined, {
placeholder: input.placeholder,
type: input.type ?? 'text',
}));
if (input.value) {
inputBox.value = input.value;
}
return inputBox;
});
} else {
this.inputs = [];
}
if (this.options.checkboxLabel) {
const checkboxRowElement = messageContainer.appendChild($('.dialog-checkbox-row'));
......@@ -133,75 +164,112 @@ export class Dialog extends Disposable {
}
updateMessage(message: string): void {
if (this.messageDetailElement) {
this.messageDetailElement.innerText = message;
}
this.messageDetailElement.innerText = message;
}
async show(): Promise<IDialogResult> {
this.focusToReturn = document.activeElement as HTMLElement;
return new Promise<IDialogResult>((resolve) => {
if (!this.element || !this.buttonsContainer || !this.iconElement || !this.toolbarContainer) {
resolve({ button: 0 });
return;
}
clearNode(this.buttonsContainer);
let focusedButton = 0;
const buttonGroup = this.buttonGroup = new ButtonGroup(this.buttonsContainer, this.buttons.length, { title: true });
const buttonGroup = this.buttonGroup = this._register(new ButtonGroup(this.buttonsContainer, this.buttons.length, { title: true }));
const buttonMap = this.rearrangeButtons(this.buttons, this.options.cancelId);
// Set focused button to UI index
buttonMap.forEach((value, index) => {
if (value.index === 0) {
focusedButton = index;
}
});
// Handle button clicks
buttonGroup.buttons.forEach((button, index) => {
button.label = mnemonicButtonLabel(buttonMap[index].label, true);
this._register(button.onDidClick(e => {
EventHelper.stop(e);
resolve({ button: buttonMap[index].index, checkboxChecked: this.checkbox ? this.checkbox.checked : undefined });
resolve({
button: buttonMap[index].index,
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined,
values: this.inputs.length > 0 ? this.inputs.map(input => input.value) : undefined
});
}));
});
// Handle keyboard events gloably: Tab, Arrow-Left/Right
this._register(domEvent(window, 'keydown', true)((e: KeyboardEvent) => {
const evt = new StandardKeyboardEvent(e);
if (evt.equals(KeyCode.Enter) || evt.equals(KeyCode.Space)) {
return;
if (evt.equals(KeyCode.Enter)) {
// Enter in input field should OK the dialog
if (this.inputs.some(input => input.hasFocus())) {
EventHelper.stop(e);
resolve({
button: buttonMap.find(button => button.index !== this.options.cancelId)?.index ?? 0,
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined,
values: this.inputs.length > 0 ? this.inputs.map(input => input.value) : undefined
});
}
return; // leave default handling
}
if (evt.equals(KeyCode.Space)) {
return; // leave default handling
}
let eventHandled = false;
if (evt.equals(KeyMod.Shift | KeyCode.Tab) || evt.equals(KeyCode.LeftArrow)) {
if (!this.checkboxHasFocus && focusedButton === 0) {
if (this.checkbox) {
this.checkbox.domNode.focus();
// Focus: Next / Previous
if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow) || evt.equals(KeyMod.Shift | KeyCode.Tab) || evt.equals(KeyCode.LeftArrow)) {
// Build a list of focusable elements in their visual order
const focusableElements: { focus: () => void }[] = [];
let focusedIndex = -1;
for (const input of this.inputs) {
focusableElements.push(input);
if (input.hasFocus()) {
focusedIndex = focusableElements.length - 1;
}
this.checkboxHasFocus = true;
} else {
focusedButton = (this.checkboxHasFocus ? 0 : focusedButton) + buttonGroup.buttons.length - 1;
focusedButton = focusedButton % buttonGroup.buttons.length;
buttonGroup.buttons[focusedButton].focus();
this.checkboxHasFocus = false;
}
eventHandled = true;
} else if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow)) {
if (!this.checkboxHasFocus && focusedButton === buttonGroup.buttons.length - 1) {
if (this.checkbox) {
this.checkbox.domNode.focus();
if (this.checkbox) {
focusableElements.push(this.checkbox);
if (this.checkbox.hasFocus()) {
focusedIndex = focusableElements.length - 1;
}
}
if (this.buttonGroup) {
for (const button of this.buttonGroup.buttons) {
focusableElements.push(button);
if (button.hasFocus()) {
focusedIndex = focusableElements.length - 1;
}
}
}
// Focus next element (with wrapping)
if (evt.equals(KeyCode.Tab) || evt.equals(KeyCode.RightArrow)) {
if (focusedIndex === -1) {
focusedIndex = 0; // default to focus first element if none have focus
}
this.checkboxHasFocus = true;
} else {
focusedButton = this.checkboxHasFocus ? 0 : focusedButton + 1;
focusedButton = focusedButton % buttonGroup.buttons.length;
buttonGroup.buttons[focusedButton].focus();
this.checkboxHasFocus = false;
const newFocusedIndex = (focusedIndex + 1) % focusableElements.length;
focusableElements[newFocusedIndex].focus();
}
// Focus previous element (with wrapping)
else {
if (focusedIndex === -1) {
focusedIndex = focusableElements.length; // default to focus last element if none have focus
}
let newFocusedIndex = focusedIndex - 1;
if (newFocusedIndex === -1) {
newFocusedIndex = focusableElements.length - 1;
}
focusableElements[newFocusedIndex].focus();
}
eventHandled = true;
}
......@@ -217,10 +285,14 @@ export class Dialog extends Disposable {
const evt = new StandardKeyboardEvent(e);
if (evt.equals(KeyCode.Escape)) {
resolve({ button: this.options.cancelId || 0, checkboxChecked: this.checkbox ? this.checkbox.checked : undefined });
resolve({
button: this.options.cancelId || 0,
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
});
}
}));
// Detect focus out
this._register(domEvent(this.element, 'focusout', false)((e: FocusEvent) => {
if (!!e.relatedTarget && !!this.element) {
if (!isAncestor(e.relatedTarget as HTMLElement, this.element)) {
......@@ -254,12 +326,14 @@ export class Dialog extends Disposable {
break;
}
const actionBar = new ActionBar(this.toolbarContainer, {});
const actionBar = this._register(new ActionBar(this.toolbarContainer, {}));
const action = new Action('dialog.close', nls.localize('dialogClose', "Close Dialog"), dialogCloseIcon.classNames, true, () => {
resolve({ button: this.options.cancelId || 0, checkboxChecked: this.checkbox ? this.checkbox.checked : undefined });
return Promise.resolve();
});
const action = this._register(new Action('dialog.close', nls.localize('dialogClose', "Close Dialog"), dialogCloseIcon.classNames, true, async () => {
resolve({
button: this.options.cancelId || 0,
checkboxChecked: this.checkbox ? this.checkbox.checked : undefined
});
}));
actionBar.push(action, { icon: true, label: false, });
......@@ -268,8 +342,17 @@ export class Dialog extends Disposable {
this.element.setAttribute('aria-label', this.getAriaLabel());
show(this.element);
// Focus first element
buttonGroup.buttons[focusedButton].focus();
// Focus first element (input or button)
if (this.inputs.length > 0) {
this.inputs[0].focus();
this.inputs[0].select();
} else {
buttonMap.forEach((value, index) => {
if (value.index === 0) {
buttonGroup.buttons[index].focus();
}
});
}
});
}
......@@ -282,60 +365,59 @@ export class Dialog extends Disposable {
const shadowColor = style.dialogShadow ? `0 0px 8px ${style.dialogShadow}` : '';
const border = style.dialogBorder ? `1px solid ${style.dialogBorder}` : '';
if (this.shadowElement) {
this.shadowElement.style.boxShadow = shadowColor;
}
this.shadowElement.style.boxShadow = shadowColor;
if (this.element) {
this.element.style.color = fgColor?.toString() ?? '';
this.element.style.backgroundColor = bgColor?.toString() ?? '';
this.element.style.border = border;
this.element.style.color = fgColor?.toString() ?? '';
this.element.style.backgroundColor = bgColor?.toString() ?? '';
this.element.style.border = border;
if (this.buttonGroup) {
this.buttonGroup.buttons.forEach(button => button.style(style));
}
if (this.buttonGroup) {
this.buttonGroup.buttons.forEach(button => button.style(style));
}
if (this.checkbox) {
this.checkbox.style(style);
}
if (this.checkbox) {
this.checkbox.style(style);
}
if (this.messageDetailElement && fgColor && bgColor) {
const messageDetailColor = fgColor.transparent(.9);
this.messageDetailElement.style.color = messageDetailColor.makeOpaque(bgColor).toString();
}
if (fgColor && bgColor) {
const messageDetailColor = fgColor.transparent(.9);
this.messageDetailElement.style.color = messageDetailColor.makeOpaque(bgColor).toString();
}
if (this.iconElement) {
let color;
switch (this.options.type) {
case 'error':
color = style.errorIconForeground;
break;
case 'warning':
color = style.warningIconForeground;
break;
default:
color = style.infoIconForeground;
break;
}
if (color) {
this.iconElement.style.color = color.toString();
}
}
let color;
switch (this.options.type) {
case 'error':
color = style.errorIconForeground;
break;
case 'warning':
color = style.warningIconForeground;
break;
default:
color = style.infoIconForeground;
break;
}
if (color) {
this.iconElement.style.color = color.toString();
}
for (const input of this.inputs) {
input.style(style);
}
}
}
style(style: IDialogStyles): void {
this.styles = style;
this.applyStyles();
}
dispose(): void {
super.dispose();
if (this.modal) {
this.modal.remove();
this.modal = undefined;
if (this.modalElement) {
this.modalElement.remove();
this.modalElement = undefined;
}
if (this.focusToReturn && isAncestor(this.focusToReturn, document.body)) {
......@@ -346,9 +428,10 @@ export class Dialog extends Disposable {
private rearrangeButtons(buttons: Array<string>, cancelId: number | undefined): ButtonMapEntry[] {
const buttonMap: ButtonMapEntry[] = [];
// Maps each button to its current label and old index so that when we move them around it's not a problem
buttons.forEach((button, index) => {
buttonMap.push({ label: button, index: index });
buttonMap.push({ label: button, index });
});
// macOS/linux: reverse button order
......
......@@ -360,6 +360,9 @@ export interface IDialogStyleOverrides extends IButtonStyleOverrides {
errorIconForeground?: ColorIdentifier;
warningIconForeground?: ColorIdentifier;
infoIconForeground?: ColorIdentifier;
inputBackground?: ColorIdentifier;
inputForeground?: ColorIdentifier;
inputBorder?: ColorIdentifier;
}
export const defaultDialogStyles = <IDialogStyleOverrides>{
......@@ -376,7 +379,10 @@ export const defaultDialogStyles = <IDialogStyleOverrides>{
checkboxForeground: simpleCheckboxForeground,
errorIconForeground: problemsErrorIconForeground,
warningIconForeground: problemsWarningIconForeground,
infoIconForeground: problemsInfoIconForeground
infoIconForeground: problemsInfoIconForeground,
inputBackground: inputBackground,
inputForeground: inputForeground,
inputBorder: inputBorder
};
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册