UNPKG

monaco-editor-core

Version:

A browser based code editor

850 lines (849 loc) • 43.3 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as dom from '../../dom.js'; import { DomEmitter } from '../../event.js'; import { StandardKeyboardEvent } from '../../keyboardEvent.js'; import { renderMarkdown } from '../../markdownRenderer.js'; import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js'; import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js'; import { List } from '../list/listWidget.js'; import * as arrays from '../../../common/arrays.js'; import { Emitter, Event } from '../../../common/event.js'; import { KeyCodeUtils } from '../../../common/keyCodes.js'; import { Disposable } from '../../../common/lifecycle.js'; import { isMacintosh } from '../../../common/platform.js'; import './selectBoxCustom.css'; import { localize } from '../../../../nls.js'; const $ = dom.$; const SELECT_OPTION_ENTRY_TEMPLATE_ID = 'selectOption.entry.template'; class SelectListRenderer { get templateId() { return SELECT_OPTION_ENTRY_TEMPLATE_ID; } renderTemplate(container) { const data = Object.create(null); data.root = container; data.text = dom.append(container, $('.option-text')); data.detail = dom.append(container, $('.option-detail')); data.decoratorRight = dom.append(container, $('.option-decorator-right')); return data; } renderElement(element, index, templateData) { const data = templateData; const text = element.text; const detail = element.detail; const decoratorRight = element.decoratorRight; const isDisabled = element.isDisabled; data.text.textContent = text; data.detail.textContent = !!detail ? detail : ''; data.decoratorRight.innerText = !!decoratorRight ? decoratorRight : ''; // pseudo-select disabled option if (isDisabled) { data.root.classList.add('option-disabled'); } else { // Make sure we do class removal from prior template rendering data.root.classList.remove('option-disabled'); } } disposeTemplate(_templateData) { // noop } } export class SelectBoxList extends Disposable { static { this.DEFAULT_DROPDOWN_MINIMUM_BOTTOM_MARGIN = 32; } static { this.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN = 2; } static { this.DEFAULT_MINIMUM_VISIBLE_OPTIONS = 3; } constructor(options, selected, contextViewProvider, styles, selectBoxOptions) { super(); this.options = []; this._currentSelection = 0; this._hasDetails = false; this._skipLayout = false; this._sticky = false; // for dev purposes only this._isVisible = false; this.styles = styles; this.selectBoxOptions = selectBoxOptions || Object.create(null); if (typeof this.selectBoxOptions.minBottomMargin !== 'number') { this.selectBoxOptions.minBottomMargin = SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_BOTTOM_MARGIN; } else if (this.selectBoxOptions.minBottomMargin < 0) { this.selectBoxOptions.minBottomMargin = 0; } this.selectElement = document.createElement('select'); // Use custom CSS vars for padding calculation this.selectElement.className = 'monaco-select-box monaco-select-box-dropdown-padding'; if (typeof this.selectBoxOptions.ariaLabel === 'string') { this.selectElement.setAttribute('aria-label', this.selectBoxOptions.ariaLabel); } if (typeof this.selectBoxOptions.ariaDescription === 'string') { this.selectElement.setAttribute('aria-description', this.selectBoxOptions.ariaDescription); } this._onDidSelect = new Emitter(); this._register(this._onDidSelect); this.registerListeners(); this.constructSelectDropDown(contextViewProvider); this.selected = selected || 0; if (options) { this.setOptions(options, selected); } this.initStyleSheet(); } setTitle(title) { if (!this._hover && title) { this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.selectElement, title)); } else if (this._hover) { this._hover.update(title); } } // IDelegate - List renderer getHeight() { return 22; } getTemplateId() { return SELECT_OPTION_ENTRY_TEMPLATE_ID; } constructSelectDropDown(contextViewProvider) { // SetUp ContextView container to hold select Dropdown this.contextViewProvider = contextViewProvider; this.selectDropDownContainer = dom.$('.monaco-select-box-dropdown-container'); // Use custom CSS vars for padding calculation (shared with parent select) this.selectDropDownContainer.classList.add('monaco-select-box-dropdown-padding'); // Setup container for select option details this.selectionDetailsPane = dom.append(this.selectDropDownContainer, $('.select-box-details-pane')); // Create span flex box item/div we can measure and control const widthControlOuterDiv = dom.append(this.selectDropDownContainer, $('.select-box-dropdown-container-width-control')); const widthControlInnerDiv = dom.append(widthControlOuterDiv, $('.width-control-div')); this.widthControlElement = document.createElement('span'); this.widthControlElement.className = 'option-text-width-control'; dom.append(widthControlInnerDiv, this.widthControlElement); // Always default to below position this._dropDownPosition = 0 /* AnchorPosition.BELOW */; // Inline stylesheet for themes this.styleElement = dom.createStyleSheet(this.selectDropDownContainer); // Prevent dragging of dropdown #114329 this.selectDropDownContainer.setAttribute('draggable', 'true'); this._register(dom.addDisposableListener(this.selectDropDownContainer, dom.EventType.DRAG_START, (e) => { dom.EventHelper.stop(e, true); })); } registerListeners() { // Parent native select keyboard listeners this._register(dom.addStandardDisposableListener(this.selectElement, 'change', (e) => { this.selected = e.target.selectedIndex; this._onDidSelect.fire({ index: e.target.selectedIndex, selected: e.target.value }); if (!!this.options[this.selected] && !!this.options[this.selected].text) { this.setTitle(this.options[this.selected].text); } })); // Have to implement both keyboard and mouse controllers to handle disabled options // Intercept mouse events to override normal select actions on parents this._register(dom.addDisposableListener(this.selectElement, dom.EventType.CLICK, (e) => { dom.EventHelper.stop(e); if (this._isVisible) { this.hideSelectDropDown(true); } else { this.showSelectDropDown(); } })); this._register(dom.addDisposableListener(this.selectElement, dom.EventType.MOUSE_DOWN, (e) => { dom.EventHelper.stop(e); })); // Intercept touch events // The following implementation is slightly different from the mouse event handlers above. // Use the following helper variable, otherwise the list flickers. let listIsVisibleOnTouchStart; this._register(dom.addDisposableListener(this.selectElement, 'touchstart', (e) => { listIsVisibleOnTouchStart = this._isVisible; })); this._register(dom.addDisposableListener(this.selectElement, 'touchend', (e) => { dom.EventHelper.stop(e); if (listIsVisibleOnTouchStart) { this.hideSelectDropDown(true); } else { this.showSelectDropDown(); } })); // Intercept keyboard handling this._register(dom.addDisposableListener(this.selectElement, dom.EventType.KEY_DOWN, (e) => { const event = new StandardKeyboardEvent(e); let showDropDown = false; // Create and drop down select list on keyboard select if (isMacintosh) { if (event.keyCode === 18 /* KeyCode.DownArrow */ || event.keyCode === 16 /* KeyCode.UpArrow */ || event.keyCode === 10 /* KeyCode.Space */ || event.keyCode === 3 /* KeyCode.Enter */) { showDropDown = true; } } else { if (event.keyCode === 18 /* KeyCode.DownArrow */ && event.altKey || event.keyCode === 16 /* KeyCode.UpArrow */ && event.altKey || event.keyCode === 10 /* KeyCode.Space */ || event.keyCode === 3 /* KeyCode.Enter */) { showDropDown = true; } } if (showDropDown) { this.showSelectDropDown(); dom.EventHelper.stop(e, true); } })); } get onDidSelect() { return this._onDidSelect.event; } setOptions(options, selected) { if (!arrays.equals(this.options, options)) { this.options = options; this.selectElement.options.length = 0; this._hasDetails = false; this._cachedMaxDetailsHeight = undefined; this.options.forEach((option, index) => { this.selectElement.add(this.createOption(option.text, index, option.isDisabled)); if (typeof option.description === 'string') { this._hasDetails = true; } }); } if (selected !== undefined) { this.select(selected); // Set current = selected since this is not necessarily a user exit this._currentSelection = this.selected; } } setOptionsList() { // Mirror options in drop-down // Populate select list for non-native select mode this.selectList?.splice(0, this.selectList.length, this.options); } select(index) { if (index >= 0 && index < this.options.length) { this.selected = index; } else if (index > this.options.length - 1) { // Adjust index to end of list // This could make client out of sync with the select this.select(this.options.length - 1); } else if (this.selected < 0) { this.selected = 0; } this.selectElement.selectedIndex = this.selected; if (!!this.options[this.selected] && !!this.options[this.selected].text) { this.setTitle(this.options[this.selected].text); } } focus() { if (this.selectElement) { this.selectElement.tabIndex = 0; this.selectElement.focus(); } } blur() { if (this.selectElement) { this.selectElement.tabIndex = -1; this.selectElement.blur(); } } setFocusable(focusable) { this.selectElement.tabIndex = focusable ? 0 : -1; } render(container) { this.container = container; container.classList.add('select-container'); container.appendChild(this.selectElement); this.styleSelectElement(); } initStyleSheet() { const content = []; // Style non-native select mode if (this.styles.listFocusBackground) { content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { background-color: ${this.styles.listFocusBackground} !important; }`); } if (this.styles.listFocusForeground) { content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { color: ${this.styles.listFocusForeground} !important; }`); } if (this.styles.decoratorRightForeground) { content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:not(.focused) .option-decorator-right { color: ${this.styles.decoratorRightForeground}; }`); } if (this.styles.selectBackground && this.styles.selectBorder && this.styles.selectBorder !== this.styles.selectBackground) { content.push(`.monaco-select-box-dropdown-container { border: 1px solid ${this.styles.selectBorder} } `); content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-top { border-top: 1px solid ${this.styles.selectBorder} } `); content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-bottom { border-bottom: 1px solid ${this.styles.selectBorder} } `); } else if (this.styles.selectListBorder) { content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-top { border-top: 1px solid ${this.styles.selectListBorder} } `); content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-bottom { border-bottom: 1px solid ${this.styles.selectListBorder} } `); } // Hover foreground - ignore for disabled options if (this.styles.listHoverForeground) { content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:not(.option-disabled):not(.focused):hover { color: ${this.styles.listHoverForeground} !important; }`); } // Hover background - ignore for disabled options if (this.styles.listHoverBackground) { content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:not(.option-disabled):not(.focused):hover { background-color: ${this.styles.listHoverBackground} !important; }`); } // Match quick input outline styles - ignore for disabled options if (this.styles.listFocusOutline) { content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { outline: 1.6px dotted ${this.styles.listFocusOutline} !important; outline-offset: -1.6px !important; }`); } if (this.styles.listHoverOutline) { content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:not(.option-disabled):not(.focused):hover { outline: 1.6px dashed ${this.styles.listHoverOutline} !important; outline-offset: -1.6px !important; }`); } // Clear list styles on focus and on hover for disabled options content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.option-disabled.focused { background-color: transparent !important; color: inherit !important; outline: none !important; }`); content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.option-disabled:hover { background-color: transparent !important; color: inherit !important; outline: none !important; }`); this.styleElement.textContent = content.join('\n'); } styleSelectElement() { const background = this.styles.selectBackground ?? ''; const foreground = this.styles.selectForeground ?? ''; const border = this.styles.selectBorder ?? ''; this.selectElement.style.backgroundColor = background; this.selectElement.style.color = foreground; this.selectElement.style.borderColor = border; } styleList() { const background = this.styles.selectBackground ?? ''; const listBackground = dom.asCssValueWithDefault(this.styles.selectListBackground, background); this.selectDropDownListContainer.style.backgroundColor = listBackground; this.selectionDetailsPane.style.backgroundColor = listBackground; const optionsBorder = this.styles.focusBorder ?? ''; this.selectDropDownContainer.style.outlineColor = optionsBorder; this.selectDropDownContainer.style.outlineOffset = '-1px'; this.selectList.style(this.styles); } createOption(value, index, disabled) { const option = document.createElement('option'); option.value = value; option.text = value; option.disabled = !!disabled; return option; } // ContextView dropdown methods showSelectDropDown() { this.selectionDetailsPane.innerText = ''; if (!this.contextViewProvider || this._isVisible) { return; } // Lazily create and populate list only at open, moved from constructor this.createSelectList(this.selectDropDownContainer); this.setOptionsList(); // This allows us to flip the position based on measurement // Set drop-down position above/below from required height and margins // If pre-layout cannot fit at least one option do not show drop-down this.contextViewProvider.showContextView({ getAnchor: () => this.selectElement, render: (container) => this.renderSelectDropDown(container, true), layout: () => { this.layoutSelectDropDown(); }, onHide: () => { this.selectDropDownContainer.classList.remove('visible'); this.selectElement.classList.remove('synthetic-focus'); }, anchorPosition: this._dropDownPosition }, this.selectBoxOptions.optionsAsChildren ? this.container : undefined); // Hide so we can relay out this._isVisible = true; this.hideSelectDropDown(false); this.contextViewProvider.showContextView({ getAnchor: () => this.selectElement, render: (container) => this.renderSelectDropDown(container), layout: () => this.layoutSelectDropDown(), onHide: () => { this.selectDropDownContainer.classList.remove('visible'); this.selectElement.classList.remove('synthetic-focus'); }, anchorPosition: this._dropDownPosition }, this.selectBoxOptions.optionsAsChildren ? this.container : undefined); // Track initial selection the case user escape, blur this._currentSelection = this.selected; this._isVisible = true; this.selectElement.setAttribute('aria-expanded', 'true'); } hideSelectDropDown(focusSelect) { if (!this.contextViewProvider || !this._isVisible) { return; } this._isVisible = false; this.selectElement.setAttribute('aria-expanded', 'false'); if (focusSelect) { this.selectElement.focus(); } this.contextViewProvider.hideContextView(); } renderSelectDropDown(container, preLayoutPosition) { container.appendChild(this.selectDropDownContainer); // Pre-Layout allows us to change position this.layoutSelectDropDown(preLayoutPosition); return { dispose: () => { // contextView will dispose itself if moving from one View to another this.selectDropDownContainer.remove(); // remove to take out the CSS rules we add } }; } // Iterate over detailed descriptions, find max height measureMaxDetailsHeight() { let maxDetailsPaneHeight = 0; this.options.forEach((_option, index) => { this.updateDetail(index); if (this.selectionDetailsPane.offsetHeight > maxDetailsPaneHeight) { maxDetailsPaneHeight = this.selectionDetailsPane.offsetHeight; } }); return maxDetailsPaneHeight; } layoutSelectDropDown(preLayoutPosition) { // Avoid recursion from layout called in onListFocus if (this._skipLayout) { return false; } // Layout ContextView drop down select list and container // Have to manage our vertical overflow, sizing, position below or above // Position has to be determined and set prior to contextView instantiation if (this.selectList) { // Make visible to enable measurements this.selectDropDownContainer.classList.add('visible'); const window = dom.getWindow(this.selectElement); const selectPosition = dom.getDomNodePagePosition(this.selectElement); const styles = dom.getWindow(this.selectElement).getComputedStyle(this.selectElement); const verticalPadding = parseFloat(styles.getPropertyValue('--dropdown-padding-top')) + parseFloat(styles.getPropertyValue('--dropdown-padding-bottom')); const maxSelectDropDownHeightBelow = (window.innerHeight - selectPosition.top - selectPosition.height - (this.selectBoxOptions.minBottomMargin || 0)); const maxSelectDropDownHeightAbove = (selectPosition.top - SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN); // Determine optimal width - min(longest option), opt(parent select, excluding margins), max(ContextView controlled) const selectWidth = this.selectElement.offsetWidth; const selectMinWidth = this.setWidthControlElement(this.widthControlElement); const selectOptimalWidth = Math.max(selectMinWidth, Math.round(selectWidth)).toString() + 'px'; this.selectDropDownContainer.style.width = selectOptimalWidth; // Get initial list height and determine space above and below this.selectList.getHTMLElement().style.height = ''; this.selectList.layout(); let listHeight = this.selectList.contentHeight; if (this._hasDetails && this._cachedMaxDetailsHeight === undefined) { this._cachedMaxDetailsHeight = this.measureMaxDetailsHeight(); } const maxDetailsPaneHeight = this._hasDetails ? this._cachedMaxDetailsHeight : 0; const minRequiredDropDownHeight = listHeight + verticalPadding + maxDetailsPaneHeight; const maxVisibleOptionsBelow = ((Math.floor((maxSelectDropDownHeightBelow - verticalPadding - maxDetailsPaneHeight) / this.getHeight()))); const maxVisibleOptionsAbove = ((Math.floor((maxSelectDropDownHeightAbove - verticalPadding - maxDetailsPaneHeight) / this.getHeight()))); // If we are only doing pre-layout check/adjust position only // Calculate vertical space available, flip up if insufficient // Use reflected padding on parent select, ContextView style // properties not available before DOM attachment if (preLayoutPosition) { // Check if select moved out of viewport , do not open // If at least one option cannot be shown, don't open the drop-down or hide/remove if open if ((selectPosition.top + selectPosition.height) > (window.innerHeight - 22) || selectPosition.top < SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN || ((maxVisibleOptionsBelow < 1) && (maxVisibleOptionsAbove < 1))) { // Indicate we cannot open return false; } // Determine if we have to flip up // Always show complete list items - never more than Max available vertical height if (maxVisibleOptionsBelow < SelectBoxList.DEFAULT_MINIMUM_VISIBLE_OPTIONS && maxVisibleOptionsAbove > maxVisibleOptionsBelow && this.options.length > maxVisibleOptionsBelow) { this._dropDownPosition = 1 /* AnchorPosition.ABOVE */; this.selectDropDownListContainer.remove(); this.selectionDetailsPane.remove(); this.selectDropDownContainer.appendChild(this.selectionDetailsPane); this.selectDropDownContainer.appendChild(this.selectDropDownListContainer); this.selectionDetailsPane.classList.remove('border-top'); this.selectionDetailsPane.classList.add('border-bottom'); } else { this._dropDownPosition = 0 /* AnchorPosition.BELOW */; this.selectDropDownListContainer.remove(); this.selectionDetailsPane.remove(); this.selectDropDownContainer.appendChild(this.selectDropDownListContainer); this.selectDropDownContainer.appendChild(this.selectionDetailsPane); this.selectionDetailsPane.classList.remove('border-bottom'); this.selectionDetailsPane.classList.add('border-top'); } // Do full layout on showSelectDropDown only return true; } // Check if select out of viewport or cutting into status bar if ((selectPosition.top + selectPosition.height) > (window.innerHeight - 22) || selectPosition.top < SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN || (this._dropDownPosition === 0 /* AnchorPosition.BELOW */ && maxVisibleOptionsBelow < 1) || (this._dropDownPosition === 1 /* AnchorPosition.ABOVE */ && maxVisibleOptionsAbove < 1)) { // Cannot properly layout, close and hide this.hideSelectDropDown(true); return false; } // SetUp list dimensions and layout - account for container padding // Use position to check above or below available space if (this._dropDownPosition === 0 /* AnchorPosition.BELOW */) { if (this._isVisible && maxVisibleOptionsBelow + maxVisibleOptionsAbove < 1) { // If drop-down is visible, must be doing a DOM re-layout, hide since we don't fit // Hide drop-down, hide contextview, focus on parent select this.hideSelectDropDown(true); return false; } // Adjust list height to max from select bottom to margin (default/minBottomMargin) if (minRequiredDropDownHeight > maxSelectDropDownHeightBelow) { listHeight = (maxVisibleOptionsBelow * this.getHeight()); } } else { if (minRequiredDropDownHeight > maxSelectDropDownHeightAbove) { listHeight = (maxVisibleOptionsAbove * this.getHeight()); } } // Set adjusted list height and relayout this.selectList.layout(listHeight); this.selectList.domFocus(); // Finally set focus on selected item if (this.selectList.length > 0) { this.selectList.setFocus([this.selected || 0]); this.selectList.reveal(this.selectList.getFocus()[0] || 0); } if (this._hasDetails) { // Leave the selectDropDownContainer to size itself according to children (list + details) - #57447 this.selectList.getHTMLElement().style.height = (listHeight + verticalPadding) + 'px'; this.selectDropDownContainer.style.height = ''; } else { this.selectDropDownContainer.style.height = (listHeight + verticalPadding) + 'px'; } this.updateDetail(this.selected); this.selectDropDownContainer.style.width = selectOptimalWidth; // Maintain focus outline on parent select as well as list container - tabindex for focus this.selectDropDownListContainer.setAttribute('tabindex', '0'); this.selectElement.classList.add('synthetic-focus'); this.selectDropDownContainer.classList.add('synthetic-focus'); return true; } else { return false; } } setWidthControlElement(container) { let elementWidth = 0; if (container) { let longest = 0; let longestLength = 0; this.options.forEach((option, index) => { const detailLength = !!option.detail ? option.detail.length : 0; const rightDecoratorLength = !!option.decoratorRight ? option.decoratorRight.length : 0; const len = option.text.length + detailLength + rightDecoratorLength; if (len > longestLength) { longest = index; longestLength = len; } }); container.textContent = this.options[longest].text + (!!this.options[longest].decoratorRight ? (this.options[longest].decoratorRight + ' ') : ''); elementWidth = dom.getTotalWidth(container); } return elementWidth; } createSelectList(parent) { // If we have already constructive list on open, skip if (this.selectList) { return; } // SetUp container for list this.selectDropDownListContainer = dom.append(parent, $('.select-box-dropdown-list-container')); this.listRenderer = new SelectListRenderer(); this.selectList = this._register(new List('SelectBoxCustom', this.selectDropDownListContainer, this, [this.listRenderer], { useShadows: false, verticalScrollMode: 3 /* ScrollbarVisibility.Visible */, keyboardSupport: false, mouseSupport: false, accessibilityProvider: { getAriaLabel: element => { let label = element.text; if (element.detail) { label += `. ${element.detail}`; } if (element.decoratorRight) { label += `. ${element.decoratorRight}`; } if (element.description) { label += `. ${element.description}`; } return label; }, getWidgetAriaLabel: () => localize({ key: 'selectBox', comment: ['Behave like native select dropdown element.'] }, "Select Box"), getRole: () => isMacintosh ? '' : 'option', getWidgetRole: () => 'listbox' } })); if (this.selectBoxOptions.ariaLabel) { this.selectList.ariaLabel = this.selectBoxOptions.ariaLabel; } // SetUp list keyboard controller - control navigation, disabled items, focus const onKeyDown = this._register(new DomEmitter(this.selectDropDownListContainer, 'keydown')); const onSelectDropDownKeyDown = Event.chain(onKeyDown.event, $ => $.filter(() => this.selectList.length > 0) .map(e => new StandardKeyboardEvent(e))); this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 3 /* KeyCode.Enter */))(this.onEnter, this)); this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 2 /* KeyCode.Tab */))(this.onEnter, this)); // Tab should behave the same as enter, #79339 this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 9 /* KeyCode.Escape */))(this.onEscape, this)); this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 16 /* KeyCode.UpArrow */))(this.onUpArrow, this)); this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 18 /* KeyCode.DownArrow */))(this.onDownArrow, this)); this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 12 /* KeyCode.PageDown */))(this.onPageDown, this)); this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 11 /* KeyCode.PageUp */))(this.onPageUp, this)); this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 14 /* KeyCode.Home */))(this.onHome, this)); this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 13 /* KeyCode.End */))(this.onEnd, this)); this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => (e.keyCode >= 21 /* KeyCode.Digit0 */ && e.keyCode <= 56 /* KeyCode.KeyZ */) || (e.keyCode >= 85 /* KeyCode.Semicolon */ && e.keyCode <= 113 /* KeyCode.NumpadDivide */)))(this.onCharacter, this)); // SetUp list mouse controller - control navigation, disabled items, focus this._register(dom.addDisposableListener(this.selectList.getHTMLElement(), dom.EventType.POINTER_UP, e => this.onPointerUp(e))); this._register(this.selectList.onMouseOver(e => typeof e.index !== 'undefined' && this.selectList.setFocus([e.index]))); this._register(this.selectList.onDidChangeFocus(e => this.onListFocus(e))); this._register(dom.addDisposableListener(this.selectDropDownContainer, dom.EventType.FOCUS_OUT, e => { if (!this._isVisible || dom.isAncestor(e.relatedTarget, this.selectDropDownContainer)) { return; } this.onListBlur(); })); this.selectList.getHTMLElement().setAttribute('aria-label', this.selectBoxOptions.ariaLabel || ''); this.selectList.getHTMLElement().setAttribute('aria-expanded', 'true'); this.styleList(); } // List methods // List mouse controller - active exit, select option, fire onDidSelect if change, return focus to parent select // Also takes in touchend events onPointerUp(e) { if (!this.selectList.length) { return; } dom.EventHelper.stop(e); const target = e.target; if (!target) { return; } // Check our mouse event is on an option (not scrollbar) if (target.classList.contains('slider')) { return; } const listRowElement = target.closest('.monaco-list-row'); if (!listRowElement) { return; } const index = Number(listRowElement.getAttribute('data-index')); const disabled = listRowElement.classList.contains('option-disabled'); // Ignore mouse selection of disabled options if (index >= 0 && index < this.options.length && !disabled) { this.selected = index; this.select(this.selected); this.selectList.setFocus([this.selected]); this.selectList.reveal(this.selectList.getFocus()[0]); // Only fire if selection change if (this.selected !== this._currentSelection) { // Set current = selected this._currentSelection = this.selected; this._onDidSelect.fire({ index: this.selectElement.selectedIndex, selected: this.options[this.selected].text }); if (!!this.options[this.selected] && !!this.options[this.selected].text) { this.setTitle(this.options[this.selected].text); } } this.hideSelectDropDown(true); } } // List Exit - passive - implicit no selection change, hide drop-down onListBlur() { if (this._sticky) { return; } if (this.selected !== this._currentSelection) { // Reset selected to current if no change this.select(this._currentSelection); } this.hideSelectDropDown(false); } renderDescriptionMarkdown(text, actionHandler) { const cleanRenderedMarkdown = (element) => { for (let i = 0; i < element.childNodes.length; i++) { const child = element.childNodes.item(i); const tagName = child.tagName && child.tagName.toLowerCase(); if (tagName === 'img') { child.remove(); } else { cleanRenderedMarkdown(child); } } }; const rendered = renderMarkdown({ value: text, supportThemeIcons: true }, { actionHandler }); rendered.element.classList.add('select-box-description-markdown'); cleanRenderedMarkdown(rendered.element); return rendered.element; } // List Focus Change - passive - update details pane with newly focused element's data onListFocus(e) { // Skip during initial layout if (!this._isVisible || !this._hasDetails) { return; } this.updateDetail(e.indexes[0]); } updateDetail(selectedIndex) { this.selectionDetailsPane.innerText = ''; const option = this.options[selectedIndex]; const description = option?.description ?? ''; const descriptionIsMarkdown = option?.descriptionIsMarkdown ?? false; if (description) { if (descriptionIsMarkdown) { const actionHandler = option.descriptionMarkdownActionHandler; this.selectionDetailsPane.appendChild(this.renderDescriptionMarkdown(description, actionHandler)); } else { this.selectionDetailsPane.innerText = description; } this.selectionDetailsPane.style.display = 'block'; } else { this.selectionDetailsPane.style.display = 'none'; } // Avoid recursion this._skipLayout = true; this.contextViewProvider.layout(); this._skipLayout = false; } // List keyboard controller // List exit - active - hide ContextView dropdown, reset selection, return focus to parent select onEscape(e) { dom.EventHelper.stop(e); // Reset selection to value when opened this.select(this._currentSelection); this.hideSelectDropDown(true); } // List exit - active - hide ContextView dropdown, return focus to parent select, fire onDidSelect if change onEnter(e) { dom.EventHelper.stop(e); // Only fire if selection change if (this.selected !== this._currentSelection) { this._currentSelection = this.selected; this._onDidSelect.fire({ index: this.selectElement.selectedIndex, selected: this.options[this.selected].text }); if (!!this.options[this.selected] && !!this.options[this.selected].text) { this.setTitle(this.options[this.selected].text); } } this.hideSelectDropDown(true); } // List navigation - have to handle a disabled option (jump over) onDownArrow(e) { if (this.selected < this.options.length - 1) { dom.EventHelper.stop(e, true); // Skip disabled options const nextOptionDisabled = this.options[this.selected + 1].isDisabled; if (nextOptionDisabled && this.options.length > this.selected + 2) { this.selected += 2; } else if (nextOptionDisabled) { return; } else { this.selected++; } // Set focus/selection - only fire event when closing drop-down or on blur this.select(this.selected); this.selectList.setFocus([this.selected]); this.selectList.reveal(this.selectList.getFocus()[0]); } } onUpArrow(e) { if (this.selected > 0) { dom.EventHelper.stop(e, true); // Skip disabled options const previousOptionDisabled = this.options[this.selected - 1].isDisabled; if (previousOptionDisabled && this.selected > 1) { this.selected -= 2; } else { this.selected--; } // Set focus/selection - only fire event when closing drop-down or on blur this.select(this.selected); this.selectList.setFocus([this.selected]); this.selectList.reveal(this.selectList.getFocus()[0]); } } onPageUp(e) { dom.EventHelper.stop(e); this.selectList.focusPreviousPage(); // Allow scrolling to settle setTimeout(() => { this.selected = this.selectList.getFocus()[0]; // Shift selection down if we land on a disabled option if (this.options[this.selected].isDisabled && this.selected < this.options.length - 1) { this.selected++; this.selectList.setFocus([this.selected]); } this.selectList.reveal(this.selected); this.select(this.selected); }, 1); } onPageDown(e) { dom.EventHelper.stop(e); this.selectList.focusNextPage(); // Allow scrolling to settle setTimeout(() => { this.selected = this.selectList.getFocus()[0]; // Shift selection up if we land on a disabled option if (this.options[this.selected].isDisabled && this.selected > 0) { this.selected--; this.selectList.setFocus([this.selected]); } this.selectList.reveal(this.selected); this.select(this.selected); }, 1); } onHome(e) { dom.EventHelper.stop(e); if (this.options.length < 2) { return; } this.selected = 0; if (this.options[this.selected].isDisabled && this.selected > 1) { this.selected++; } this.selectList.setFocus([this.selected]); this.selectList.reveal(this.selected); this.select(this.selected); } onEnd(e) { dom.EventHelper.stop(e); if (this.options.length < 2) { return; } this.selected = this.options.length - 1; if (this.options[this.selected].isDisabled && this.selected > 1) { this.selected--; } this.selectList.setFocus([this.selected]); this.selectList.reveal(this.selected); this.select(this.selected); } // Mimic option first character navigation of native select onCharacter(e) { const ch = KeyCodeUtils.toString(e.keyCode); let optionIndex = -1; for (let i = 0; i < this.options.length - 1; i++) { optionIndex = (i + this.selected + 1) % this.options.length; if (this.options[optionIndex].text.charAt(0).toUpperCase() === ch && !this.options[optionIndex].isDisabled) { this.select(optionIndex); this.selectList.setFocus([optionIndex]); this.selectList.reveal(this.selectList.getFocus()[0]); dom.EventHelper.stop(e); break; } } } dispose() { this.hideSelectDropDown(false); super.dispose(); } }