UNPKG

sussudio

Version:

An unofficial VS Code Internal API

386 lines (385 loc) 18.7 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { $, addDisposableListener, clearNode, EventHelper, EventType, hide, isAncestor, show } from "../../dom.mjs"; import { StandardKeyboardEvent } from "../../keyboardEvent.mjs"; import { ActionBar } from "../actionbar/actionbar.mjs"; import { ButtonBar, ButtonWithDescription } from "../button/button.mjs"; import { Checkbox } from "../toggle/toggle.mjs"; import { InputBox } from "../inputbox/inputBox.mjs"; import { Action } from "../../../common/actions.mjs"; import { Codicon } from "../../../common/codicons.mjs"; import { mnemonicButtonLabel } from "../../../common/labels.mjs"; import { Disposable } from "../../../common/lifecycle.mjs"; import { isLinux, isMacintosh } from "../../../common/platform.mjs"; import "../../../../css!./dialog.mjs"; import * as nls from "../../../../nls.mjs"; export class Dialog extends Disposable { container; message; options; element; shadowElement; modalElement; buttonsContainer; messageDetailElement; messageContainer; iconElement; checkbox; toolbarContainer; buttonBar; focusToReturn; inputs; buttons; buttonStyles; constructor(container, message, buttons, options) { super(); this.container = container; this.message = message; this.options = options; this.modalElement = this.container.appendChild($(`.monaco-dialog-modal-block.dimmed`)); this.shadowElement = this.modalElement.appendChild($('.dialog-shadow')); this.element = this.shadowElement.appendChild($('.monaco-dialog-box')); this.element.setAttribute('role', 'dialog'); this.element.tabIndex = -1; hide(this.element); this.buttonStyles = options.buttonStyles; if (Array.isArray(buttons) && buttons.length > 0) { this.buttons = buttons; } else if (!this.options.disableDefaultAction) { this.buttons = [nls.localize('ok', "OK")]; } else { this.buttons = []; } const buttonsRowElement = this.element.appendChild($('.dialog-buttons-row')); this.buttonsContainer = buttonsRowElement.appendChild($('.dialog-buttons')); const messageRowElement = this.element.appendChild($('.dialog-message-row')); this.iconElement = messageRowElement.appendChild($('#monaco-dialog-icon.dialog-icon')); this.iconElement.setAttribute('aria-label', this.getIconAriaLabel()); this.messageContainer = messageRowElement.appendChild($('.dialog-message-container')); if (this.options.detail || this.options.renderBody) { const messageElement = this.messageContainer.appendChild($('.dialog-message')); const messageTextElement = messageElement.appendChild($('#monaco-dialog-message-text.dialog-message-text')); messageTextElement.innerText = this.message; } this.messageDetailElement = this.messageContainer.appendChild($('#monaco-dialog-message-detail.dialog-message-detail')); if (this.options.detail || !this.options.renderBody) { this.messageDetailElement.innerText = this.options.detail ? this.options.detail : message; } else { this.messageDetailElement.style.display = 'none'; } if (this.options.renderBody) { const customBody = this.messageContainer.appendChild($('#monaco-dialog-message-body.dialog-message-body')); this.options.renderBody(customBody); for (const el of this.messageContainer.querySelectorAll('a')) { el.tabIndex = 0; } } if (this.options.inputs) { this.inputs = this.options.inputs.map(input => { const inputRowElement = this.messageContainer.appendChild($('.dialog-message-input')); const inputBox = this._register(new InputBox(inputRowElement, undefined, { placeholder: input.placeholder, type: input.type ?? 'text', inputBoxStyles: options.inputBoxStyles })); if (input.value) { inputBox.value = input.value; } return inputBox; }); } else { this.inputs = []; } if (this.options.checkboxLabel) { const checkboxRowElement = this.messageContainer.appendChild($('.dialog-checkbox-row')); const checkbox = this.checkbox = this._register(new Checkbox(this.options.checkboxLabel, !!this.options.checkboxChecked, options.checkboxStyles)); checkboxRowElement.appendChild(checkbox.domNode); const checkboxMessageElement = checkboxRowElement.appendChild($('.dialog-checkbox-message')); checkboxMessageElement.innerText = this.options.checkboxLabel; this._register(addDisposableListener(checkboxMessageElement, EventType.CLICK, () => checkbox.checked = !checkbox.checked)); } const toolbarRowElement = this.element.appendChild($('.dialog-toolbar-row')); this.toolbarContainer = toolbarRowElement.appendChild($('.dialog-toolbar')); this.applyStyles(); } getIconAriaLabel() { const typeLabel = nls.localize('dialogInfoMessage', 'Info'); switch (this.options.type) { case 'error': nls.localize('dialogErrorMessage', 'Error'); break; case 'warning': nls.localize('dialogWarningMessage', 'Warning'); break; case 'pending': nls.localize('dialogPendingMessage', 'In Progress'); break; case 'none': case 'info': case 'question': default: break; } return typeLabel; } updateMessage(message) { this.messageDetailElement.innerText = message; } async show() { this.focusToReturn = document.activeElement; return new Promise((resolve) => { clearNode(this.buttonsContainer); const buttonBar = this.buttonBar = this._register(new ButtonBar(this.buttonsContainer)); const buttonMap = this.rearrangeButtons(this.buttons, this.options.cancelId); // Handle button clicks buttonMap.forEach((entry, index) => { const primary = buttonMap[index].index === 0; const button = this.options.buttonDetails ? this._register(buttonBar.addButtonWithDescription({ title: true, secondary: !primary, ...this.buttonStyles })) : this._register(buttonBar.addButton({ title: true, secondary: !primary, ...this.buttonStyles })); button.label = mnemonicButtonLabel(buttonMap[index].label, true); if (button instanceof ButtonWithDescription) { button.description = this.options.buttonDetails[buttonMap[index].index]; } this._register(button.onDidClick(e => { if (e) { EventHelper.stop(e); } 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 globally: Tab, Arrow-Left/Right this._register(addDisposableListener(window, 'keydown', e => { const evt = new StandardKeyboardEvent(e); if (evt.equals(512 /* KeyMod.Alt */)) { evt.preventDefault(); } if (evt.equals(3 /* 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(10 /* KeyCode.Space */)) { return; // leave default handling } let eventHandled = false; // Focus: Next / Previous if (evt.equals(2 /* KeyCode.Tab */) || evt.equals(17 /* KeyCode.RightArrow */) || evt.equals(1024 /* KeyMod.Shift */ | 2 /* KeyCode.Tab */) || evt.equals(15 /* KeyCode.LeftArrow */)) { // Build a list of focusable elements in their visual order const focusableElements = []; let focusedIndex = -1; if (this.messageContainer) { const links = this.messageContainer.querySelectorAll('a'); for (const link of links) { focusableElements.push(link); if (link === document.activeElement) { focusedIndex = focusableElements.length - 1; } } } for (const input of this.inputs) { focusableElements.push(input); if (input.hasFocus()) { focusedIndex = focusableElements.length - 1; } } if (this.checkbox) { focusableElements.push(this.checkbox); if (this.checkbox.hasFocus()) { focusedIndex = focusableElements.length - 1; } } if (this.buttonBar) { for (const button of this.buttonBar.buttons) { focusableElements.push(button); if (button.hasFocus()) { focusedIndex = focusableElements.length - 1; } } } // Focus next element (with wrapping) if (evt.equals(2 /* KeyCode.Tab */) || evt.equals(17 /* KeyCode.RightArrow */)) { if (focusedIndex === -1) { focusedIndex = 0; // default to focus first element if none have focus } 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; } if (eventHandled) { EventHelper.stop(e, true); } else if (this.options.keyEventProcessor) { this.options.keyEventProcessor(evt); } }, true)); this._register(addDisposableListener(window, 'keyup', e => { EventHelper.stop(e, true); const evt = new StandardKeyboardEvent(e); if (!this.options.disableCloseAction && evt.equals(9 /* KeyCode.Escape */)) { resolve({ button: this.options.cancelId || 0, checkboxChecked: this.checkbox ? this.checkbox.checked : undefined }); } }, true)); // Detect focus out this._register(addDisposableListener(this.element, 'focusout', e => { if (!!e.relatedTarget && !!this.element) { if (!isAncestor(e.relatedTarget, this.element)) { this.focusToReturn = e.relatedTarget; if (e.target) { e.target.focus(); EventHelper.stop(e, true); } } } }, false)); const spinModifierClassName = 'codicon-modifier-spin'; this.iconElement.classList.remove(...Codicon.dialogError.classNamesArray, ...Codicon.dialogWarning.classNamesArray, ...Codicon.dialogInfo.classNamesArray, ...Codicon.loading.classNamesArray, spinModifierClassName); if (this.options.icon) { this.iconElement.classList.add(...this.options.icon.classNamesArray); } else { switch (this.options.type) { case 'error': this.iconElement.classList.add(...Codicon.dialogError.classNamesArray); break; case 'warning': this.iconElement.classList.add(...Codicon.dialogWarning.classNamesArray); break; case 'pending': this.iconElement.classList.add(...Codicon.loading.classNamesArray, spinModifierClassName); break; case 'none': case 'info': case 'question': default: this.iconElement.classList.add(...Codicon.dialogInfo.classNamesArray); break; } } if (!this.options.disableCloseAction) { const actionBar = this._register(new ActionBar(this.toolbarContainer, {})); const action = this._register(new Action('dialog.close', nls.localize('dialogClose', "Close Dialog"), Codicon.dialogClose.classNames, true, async () => { resolve({ button: this.options.cancelId || 0, checkboxChecked: this.checkbox ? this.checkbox.checked : undefined }); })); actionBar.push(action, { icon: true, label: false }); } this.applyStyles(); this.element.setAttribute('aria-modal', 'true'); this.element.setAttribute('aria-labelledby', 'monaco-dialog-icon monaco-dialog-message-text'); this.element.setAttribute('aria-describedby', 'monaco-dialog-icon monaco-dialog-message-text monaco-dialog-message-detail monaco-dialog-message-body'); show(this.element); // 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) { buttonBar.buttons[index].focus(); } }); } }); } applyStyles() { const style = this.options.dialogStyles; const fgColor = style.dialogForeground; const bgColor = style.dialogBackground; const shadowColor = style.dialogShadow ? `0 0px 8px ${style.dialogShadow}` : ''; const border = style.dialogBorder ? `1px solid ${style.dialogBorder}` : ''; const linkFgColor = style.textLinkForeground; this.shadowElement.style.boxShadow = shadowColor; this.element.style.color = fgColor?.toString() ?? ''; this.element.style.backgroundColor = bgColor?.toString() ?? ''; this.element.style.border = border; // TODO fix // if (fgColor && bgColor) { // const messageDetailColor = fgColor.transparent(.9); // this.messageDetailElement.style.mixBlendMode = messageDetailColor.makeOpaque(bgColor).toString(); // } if (linkFgColor) { for (const el of this.messageContainer.getElementsByTagName('a')) { el.style.color = linkFgColor; } } 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; } } dispose() { super.dispose(); if (this.modalElement) { this.modalElement.remove(); this.modalElement = undefined; } if (this.focusToReturn && isAncestor(this.focusToReturn, document.body)) { this.focusToReturn.focus(); this.focusToReturn = undefined; } } rearrangeButtons(buttons, cancelId) { const buttonMap = []; if (buttons.length === 0) { return buttonMap; } // 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 }); }); // macOS/linux: reverse button order if `cancelId` is defined if (isMacintosh || isLinux) { if (cancelId !== undefined && cancelId < buttons.length) { const cancelButton = buttonMap.splice(cancelId, 1)[0]; buttonMap.reverse(); buttonMap.splice(buttonMap.length - 1, 0, cancelButton); } } return buttonMap; } }