mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-01-22 01:25:38 -05:00
Merge from master
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
|
||||
import 'vs/css!./selectBoxCustom';
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import { IDisposable, dispose } from 'vs/base/common/lifecycle';
|
||||
import { Event, Emitter, chain } from 'vs/base/common/event';
|
||||
import { KeyCode, KeyCodeUtils } from 'vs/base/common/keyCodes';
|
||||
@@ -14,11 +13,12 @@ import * as dom from 'vs/base/browser/dom';
|
||||
import * as arrays from 'vs/base/common/arrays';
|
||||
import { IContextViewProvider, AnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { List } from 'vs/base/browser/ui/list/listWidget';
|
||||
import { IVirtualDelegate, IRenderer } from 'vs/base/browser/ui/list/list';
|
||||
import { IListVirtualDelegate, IListRenderer, IListEvent } from 'vs/base/browser/ui/list/list';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { ISelectBoxDelegate, ISelectBoxOptions, ISelectBoxStyles, ISelectData } from 'vs/base/browser/ui/selectBox/selectBox';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { renderMarkdown } from 'vs/base/browser/htmlContentRenderer';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
@@ -26,16 +26,18 @@ const SELECT_OPTION_ENTRY_TEMPLATE_ID = 'selectOption.entry.template';
|
||||
|
||||
export interface ISelectOptionItem {
|
||||
optionText: string;
|
||||
optionDescriptionText?: string;
|
||||
optionDisabled: boolean;
|
||||
}
|
||||
|
||||
interface ISelectListTemplateData {
|
||||
root: HTMLElement;
|
||||
optionText: HTMLElement;
|
||||
optionDescriptionText: HTMLElement;
|
||||
disposables: IDisposable[];
|
||||
}
|
||||
|
||||
class SelectListRenderer implements IRenderer<ISelectOptionItem, ISelectListTemplateData> {
|
||||
class SelectListRenderer implements IListRenderer<ISelectOptionItem, ISelectListTemplateData> {
|
||||
|
||||
get templateId(): string { return SELECT_OPTION_ENTRY_TEMPLATE_ID; }
|
||||
|
||||
@@ -46,6 +48,8 @@ class SelectListRenderer implements IRenderer<ISelectOptionItem, ISelectListTemp
|
||||
data.disposables = [];
|
||||
data.root = container;
|
||||
data.optionText = dom.append(container, $('.option-text'));
|
||||
data.optionDescriptionText = dom.append(container, $('.option-text-description'));
|
||||
dom.addClass(data.optionDescriptionText, 'visually-hidden');
|
||||
|
||||
return data;
|
||||
}
|
||||
@@ -56,10 +60,13 @@ class SelectListRenderer implements IRenderer<ISelectOptionItem, ISelectListTemp
|
||||
const optionDisabled = (<ISelectOptionItem>element).optionDisabled;
|
||||
|
||||
data.optionText.textContent = optionText;
|
||||
data.root.setAttribute('aria-label', nls.localize('selectAriaOption', "{0}", optionText));
|
||||
|
||||
// Workaround for list labels
|
||||
data.root.setAttribute('aria-selected', 'true');
|
||||
if (typeof element.optionDescriptionText === 'string') {
|
||||
const optionDescriptionId = (optionText.replace(/ /g, '_').toLowerCase() + '_description_' + data.root.id);
|
||||
data.optionText.setAttribute('aria-describedby', optionDescriptionId);
|
||||
data.optionDescriptionText.id = optionDescriptionId;
|
||||
data.optionDescriptionText.innerText = element.optionDescriptionText;
|
||||
}
|
||||
|
||||
// pseudo-select disabled option
|
||||
if (optionDisabled) {
|
||||
@@ -79,10 +86,10 @@ class SelectListRenderer implements IRenderer<ISelectOptionItem, ISelectListTemp
|
||||
}
|
||||
}
|
||||
|
||||
export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISelectOptionItem> {
|
||||
export class SelectBoxList implements ISelectBoxDelegate, IListVirtualDelegate<ISelectOptionItem> {
|
||||
|
||||
private static readonly DEFAULT_DROPDOWN_MINIMUM_BOTTOM_MARGIN = 32;
|
||||
private static readonly DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN = 42;
|
||||
private static readonly DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN = 2;
|
||||
private static readonly DEFAULT_MINIMUM_VISIBLE_OPTIONS = 3;
|
||||
|
||||
private _isVisible: boolean;
|
||||
@@ -104,6 +111,11 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
private widthControlElement: HTMLElement;
|
||||
private _currentSelection: number;
|
||||
private _dropDownPosition: AnchorPosition;
|
||||
private detailsProvider: (index: number) => { details: string, isMarkdown: boolean };
|
||||
private selectionDetailsPane: HTMLElement;
|
||||
private _skipLayout: boolean = false;
|
||||
|
||||
private _sticky: boolean = false; // for dev purposes only
|
||||
|
||||
constructor(options: string[], selected: number, contextViewProvider: IContextViewProvider, styles: ISelectBoxStyles, selectBoxOptions?: ISelectBoxOptions) {
|
||||
|
||||
@@ -118,6 +130,13 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
}
|
||||
|
||||
this.selectElement = document.createElement('select');
|
||||
|
||||
// Workaround for Electron 2.x
|
||||
// Native select should not require explicit role attribute, however, Electron 2.x
|
||||
// incorrectly exposes select as menuItem which interferes with labeling and results
|
||||
// in the unlabeled not been read. Electron 3 appears to fix.
|
||||
this.selectElement.setAttribute('role', 'combobox');
|
||||
|
||||
// Use custom CSS vars for padding calculation
|
||||
this.selectElement.className = 'monaco-select-box monaco-select-box-dropdown-padding';
|
||||
|
||||
@@ -126,13 +145,19 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
}
|
||||
|
||||
this._onDidSelect = new Emitter<ISelectData>();
|
||||
this.toDispose.push(this._onDidSelect);
|
||||
|
||||
this.styles = styles;
|
||||
|
||||
this.registerListeners();
|
||||
this.constructSelectDropDown(contextViewProvider);
|
||||
|
||||
this.setOptions(options, selected);
|
||||
this.selected = selected || 0;
|
||||
|
||||
if (options) {
|
||||
this.setOptions(options, selected);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// IDelegate - List renderer
|
||||
@@ -152,8 +177,9 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
this.selectDropDownContainer = dom.$('.monaco-select-box-dropdown-container');
|
||||
// Use custom CSS vars for padding calculation (shared with parent select)
|
||||
dom.addClass(this.selectDropDownContainer, 'monaco-select-box-dropdown-padding');
|
||||
// Setup list for drop-down select
|
||||
this.createSelectList(this.selectDropDownContainer);
|
||||
|
||||
// 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
|
||||
let widthControlOuterDiv = dom.append(this.selectDropDownContainer, $('.select-box-dropdown-container-width-control'));
|
||||
@@ -174,7 +200,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
// Parent native select keyboard listeners
|
||||
|
||||
this.toDispose.push(dom.addStandardDisposableListener(this.selectElement, 'change', (e) => {
|
||||
this.selectElement.title = e.target.value;
|
||||
this.selected = e.target.selectedIndex;
|
||||
this._onDidSelect.fire({
|
||||
index: e.target.selectedIndex,
|
||||
selected: e.target.value
|
||||
@@ -227,7 +253,6 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
}
|
||||
|
||||
public setOptions(options: string[], selected?: number, disabled?: number): void {
|
||||
|
||||
if (!this.options || !arrays.equals(this.options, options)) {
|
||||
this.options = options;
|
||||
this.selectElement.options.length = 0;
|
||||
@@ -237,23 +262,8 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
this.selectElement.add(this.createOption(option, i, disabled === i++));
|
||||
});
|
||||
|
||||
// Mirror options in drop-down
|
||||
// Populate select list for non-native select mode
|
||||
if (this.selectList && !!this.options) {
|
||||
let listEntries: ISelectOptionItem[];
|
||||
|
||||
listEntries = [];
|
||||
if (disabled !== undefined) {
|
||||
this.disabledOptionIndex = disabled;
|
||||
}
|
||||
for (let index = 0; index < this.options.length; index++) {
|
||||
const element = this.options[index];
|
||||
let optionDisabled: boolean;
|
||||
index === this.disabledOptionIndex ? optionDisabled = true : optionDisabled = false;
|
||||
listEntries.push({ optionText: element, optionDisabled: optionDisabled });
|
||||
}
|
||||
|
||||
this.selectList.splice(0, this.selectList.length, listEntries);
|
||||
if (disabled !== undefined) {
|
||||
this.disabledOptionIndex = disabled;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +274,27 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private setOptionsList() {
|
||||
|
||||
// Mirror options in drop-down
|
||||
// Populate select list for non-native select mode
|
||||
if (this.selectList && !!this.options) {
|
||||
let listEntries: ISelectOptionItem[];
|
||||
listEntries = [];
|
||||
|
||||
for (let index = 0; index < this.options.length; index++) {
|
||||
const element = this.options[index];
|
||||
let optionDisabled: boolean;
|
||||
index === this.disabledOptionIndex ? optionDisabled = true : optionDisabled = false;
|
||||
const optionDescription = this.detailsProvider ? this.detailsProvider(index) : { details: undefined, isMarkdown: false };
|
||||
|
||||
listEntries.push({ optionText: element, optionDisabled: optionDisabled, optionDescriptionText: optionDescription.details });
|
||||
}
|
||||
this.selectList.splice(0, this.selectList.length, listEntries);
|
||||
}
|
||||
}
|
||||
|
||||
public select(index: number): void {
|
||||
|
||||
if (index >= 0 && index < this.options.length) {
|
||||
@@ -277,13 +308,15 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
}
|
||||
|
||||
this.selectElement.selectedIndex = this.selected;
|
||||
this.selectElement.title = this.options[this.selected];
|
||||
}
|
||||
|
||||
public setAriaLabel(label: string): void {
|
||||
this.selectBoxOptions.ariaLabel = label;
|
||||
this.selectElement.setAttribute('aria-label', this.selectBoxOptions.ariaLabel);
|
||||
this.selectList.getHTMLElement().setAttribute('aria-label', this.selectBoxOptions.ariaLabel);
|
||||
}
|
||||
|
||||
public setDetailsProvider(provider: (index: number) => { details: string, isMarkdown: boolean }): void {
|
||||
this.detailsProvider = provider;
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
@@ -301,7 +334,6 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
public render(container: HTMLElement): void {
|
||||
dom.addClass(container, 'select-container');
|
||||
container.appendChild(this.selectElement);
|
||||
this.setOptions(this.options, this.selected);
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
@@ -321,6 +353,17 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused:not(:hover) { color: ${this.styles.listFocusForeground} !important; }`);
|
||||
}
|
||||
|
||||
if (this.styles.selectBackground && this.styles.selectBorder && !this.styles.selectBorder.equals(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:hover { color: ${this.styles.listHoverForeground} !important; }`);
|
||||
@@ -336,6 +379,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
// Match quickOpen 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) {
|
||||
@@ -375,10 +419,18 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
// Style drop down select list (non-native mode only)
|
||||
|
||||
if (this.selectList) {
|
||||
this.styleList();
|
||||
}
|
||||
}
|
||||
|
||||
private styleList() {
|
||||
if (this.selectList) {
|
||||
let background = this.styles.selectBackground ? this.styles.selectBackground.toString() : null;
|
||||
this.selectList.style({});
|
||||
|
||||
let listBackground = this.styles.selectListBackground ? this.styles.selectListBackground.toString() : background;
|
||||
this.selectDropDownListContainer.style.backgroundColor = listBackground;
|
||||
this.selectionDetailsPane.style.backgroundColor = listBackground;
|
||||
const optionsBorder = this.styles.focusBorder ? this.styles.focusBorder.toString() : null;
|
||||
this.selectDropDownContainer.style.outlineColor = optionsBorder;
|
||||
this.selectDropDownContainer.style.outlineOffset = '-1px';
|
||||
@@ -389,7 +441,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
let option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.text = value;
|
||||
option.disabled = disabled;
|
||||
option.disabled = !!disabled;
|
||||
|
||||
return option;
|
||||
}
|
||||
@@ -397,16 +449,38 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
// ContextView dropdown methods
|
||||
|
||||
private showSelectDropDown() {
|
||||
|
||||
if (!this.contextViewProvider || this._isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set drop-down position above/below from required height and margins
|
||||
this.layoutSelectDropDown(true);
|
||||
// Lazily create and populate list only at open, moved from constructor
|
||||
this.createSelectList(this.selectDropDownContainer);
|
||||
this.setOptionsList();
|
||||
|
||||
this._isVisible = true;
|
||||
this.cloneElementFont(this.selectElement, this.selectDropDownContainer);
|
||||
|
||||
// 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: HTMLElement) => this.renderSelectDropDown(container, true),
|
||||
layout: () => {
|
||||
this.layoutSelectDropDown();
|
||||
},
|
||||
onHide: () => {
|
||||
dom.toggleClass(this.selectDropDownContainer, 'visible', false);
|
||||
dom.toggleClass(this.selectElement, 'synthetic-focus', false);
|
||||
},
|
||||
anchorPosition: this._dropDownPosition
|
||||
});
|
||||
|
||||
// Hide so we can relay out
|
||||
this._isVisible = true;
|
||||
this.hideSelectDropDown(false);
|
||||
|
||||
this.contextViewProvider.showContextView({
|
||||
getAnchor: () => this.selectElement,
|
||||
render: (container: HTMLElement) => this.renderSelectDropDown(container),
|
||||
@@ -420,6 +494,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
|
||||
// Track initial selection the case user escape, blur
|
||||
this._currentSelection = this.selected;
|
||||
this._isVisible = true;
|
||||
}
|
||||
|
||||
private hideSelectDropDown(focusSelect: boolean) {
|
||||
@@ -432,15 +507,16 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
if (focusSelect) {
|
||||
this.selectElement.focus();
|
||||
}
|
||||
|
||||
this.contextViewProvider.hideContextView();
|
||||
}
|
||||
|
||||
private renderSelectDropDown(container: HTMLElement): IDisposable {
|
||||
private renderSelectDropDown(container: HTMLElement, preLayoutPosition?: boolean): IDisposable {
|
||||
container.appendChild(this.selectDropDownContainer);
|
||||
|
||||
this.layoutSelectDropDown();
|
||||
// Pre-Layout allows us to change position
|
||||
this.layoutSelectDropDown(preLayoutPosition);
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
return {
|
||||
dispose: () => {
|
||||
// contextView will dispose itself if moving from one View to another
|
||||
@@ -454,7 +530,59 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
};
|
||||
}
|
||||
|
||||
private layoutSelectDropDown(preLayoutPosition?: boolean) {
|
||||
// Iterate over detailed descriptions, find max height
|
||||
private measureMaxDetailsHeight(): number {
|
||||
|
||||
if (!this.detailsProvider) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
let maxDetailsPaneHeight = 0;
|
||||
let description = { details: '', isMarkdown: false };
|
||||
|
||||
this.options.forEach((option, index) => {
|
||||
|
||||
this.selectionDetailsPane.innerText = '';
|
||||
|
||||
description = this.detailsProvider ? this.detailsProvider(index) : { details: '', isMarkdown: false };
|
||||
if (description.details) {
|
||||
if (description.isMarkdown) {
|
||||
this.selectionDetailsPane.appendChild(this.renderDescriptionMarkdown(description.details));
|
||||
} else {
|
||||
this.selectionDetailsPane.innerText = description.details;
|
||||
}
|
||||
this.selectionDetailsPane.style.display = 'block';
|
||||
} else {
|
||||
this.selectionDetailsPane.style.display = 'none';
|
||||
}
|
||||
|
||||
if (this.selectionDetailsPane.offsetHeight > maxDetailsPaneHeight) {
|
||||
maxDetailsPaneHeight = this.selectionDetailsPane.offsetHeight;
|
||||
}
|
||||
});
|
||||
|
||||
// Reset description to selected
|
||||
description = this.detailsProvider ? this.detailsProvider(this.selected) : { details: '', isMarkdown: false };
|
||||
this.selectionDetailsPane.innerText = '';
|
||||
|
||||
if (description.details) {
|
||||
if (description.isMarkdown) {
|
||||
this.selectionDetailsPane.appendChild(this.renderDescriptionMarkdown(description.details));
|
||||
} else {
|
||||
this.selectionDetailsPane.innerText = description.details;
|
||||
}
|
||||
this.selectionDetailsPane.style.display = 'block';
|
||||
}
|
||||
|
||||
return maxDetailsPaneHeight;
|
||||
}
|
||||
|
||||
private layoutSelectDropDown(preLayoutPosition?: boolean): boolean {
|
||||
|
||||
// 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
|
||||
@@ -462,50 +590,105 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
|
||||
if (this.selectList) {
|
||||
|
||||
// Make visible to enable measurements
|
||||
dom.toggleClass(this.selectDropDownContainer, 'visible', true);
|
||||
|
||||
const selectPosition = dom.getDomNodePagePosition(this.selectElement);
|
||||
const styles = getComputedStyle(this.selectElement);
|
||||
const verticalPadding = parseFloat(styles.getPropertyValue('--dropdown-padding-top')) + parseFloat(styles.getPropertyValue('--dropdown-padding-bottom'));
|
||||
let maxSelectDropDownHeight = 0;
|
||||
maxSelectDropDownHeight = (window.innerHeight - selectPosition.top - selectPosition.height - this.selectBoxOptions.minBottomMargin);
|
||||
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 ab1you knowove and below
|
||||
this.selectList.layout();
|
||||
let listHeight = this.selectList.contentHeight;
|
||||
|
||||
const maxDetailsPaneHeight = this.measureMaxDetailsHeight();
|
||||
|
||||
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
|
||||
// Use reflected padding on parent select, ContextView style
|
||||
// properties not available before DOM attachment
|
||||
|
||||
if (preLayoutPosition) {
|
||||
|
||||
// Always show complete list items - never more than Max available vertical height
|
||||
if (listHeight + verticalPadding > maxSelectDropDownHeight) {
|
||||
const maxVisibleOptions = ((Math.floor((maxSelectDropDownHeight - verticalPadding) / this.getHeight())));
|
||||
// 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
|
||||
|
||||
// Check if we can at least show min items otherwise flip above
|
||||
if (maxVisibleOptions < SelectBoxList.DEFAULT_MINIMUM_VISIBLE_OPTIONS) {
|
||||
this._dropDownPosition = AnchorPosition.ABOVE;
|
||||
} else {
|
||||
this._dropDownPosition = AnchorPosition.BELOW;
|
||||
}
|
||||
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 = AnchorPosition.ABOVE;
|
||||
this.selectDropDownContainer.removeChild(this.selectDropDownListContainer);
|
||||
this.selectDropDownContainer.removeChild(this.selectionDetailsPane);
|
||||
this.selectDropDownContainer.appendChild(this.selectionDetailsPane);
|
||||
this.selectDropDownContainer.appendChild(this.selectDropDownListContainer);
|
||||
|
||||
dom.removeClass(this.selectionDetailsPane, 'border-top');
|
||||
dom.addClass(this.selectionDetailsPane, 'border-bottom');
|
||||
|
||||
} else {
|
||||
this._dropDownPosition = AnchorPosition.BELOW;
|
||||
this.selectDropDownContainer.removeChild(this.selectDropDownListContainer);
|
||||
this.selectDropDownContainer.removeChild(this.selectionDetailsPane);
|
||||
this.selectDropDownContainer.appendChild(this.selectDropDownListContainer);
|
||||
this.selectDropDownContainer.appendChild(this.selectionDetailsPane);
|
||||
|
||||
dom.removeClass(this.selectionDetailsPane, 'border-bottom');
|
||||
dom.addClass(this.selectionDetailsPane, 'border-top');
|
||||
}
|
||||
// Do full layout on showSelectDropDown only
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Make visible to enable measurements
|
||||
dom.toggleClass(this.selectDropDownContainer, 'visible', 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 === AnchorPosition.BELOW && maxVisibleOptionsBelow < 1)
|
||||
|| (this._dropDownPosition === 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 === AnchorPosition.BELOW) {
|
||||
// Set container height to max from select bottom to margin (default/minBottomMargin)
|
||||
if (listHeight + verticalPadding > maxSelectDropDownHeight) {
|
||||
listHeight = ((Math.floor((maxSelectDropDownHeight - verticalPadding) / this.getHeight())) * this.getHeight());
|
||||
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 {
|
||||
// Set container height to max from select top to margin (default/minTopMargin)
|
||||
maxSelectDropDownHeight = (selectPosition.top - SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN);
|
||||
if (listHeight + verticalPadding > maxSelectDropDownHeight) {
|
||||
listHeight = ((Math.floor((maxSelectDropDownHeight - SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN) / this.getHeight())) * this.getHeight());
|
||||
if (minRequiredDropDownHeight > maxSelectDropDownHeightAbove) {
|
||||
listHeight = (maxVisibleOptionsAbove * this.getHeight());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -519,13 +702,12 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
this.selectList.reveal(this.selectList.getFocus()[0] || 0);
|
||||
}
|
||||
|
||||
// Set final container height after adjustments
|
||||
this.selectDropDownContainer.style.height = (listHeight + verticalPadding) + 'px';
|
||||
|
||||
// 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';
|
||||
if (this.detailsProvider) {
|
||||
// Leave the selectDropDownContainer to size itself according to children (list + details) - #57447
|
||||
this.selectList.getHTMLElement().style.height = (listHeight + verticalPadding) + 'px';
|
||||
} else {
|
||||
this.selectDropDownContainer.style.height = (listHeight + verticalPadding) + 'px';
|
||||
}
|
||||
|
||||
this.selectDropDownContainer.style.width = selectOptimalWidth;
|
||||
|
||||
@@ -533,6 +715,10 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
this.selectDropDownListContainer.setAttribute('tabindex', '0');
|
||||
dom.toggleClass(this.selectElement, 'synthetic-focus', true);
|
||||
dom.toggleClass(this.selectDropDownContainer, 'synthetic-focus', true);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -564,6 +750,11 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
|
||||
private createSelectList(parent: HTMLElement): void {
|
||||
|
||||
// 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'));
|
||||
|
||||
@@ -599,9 +790,16 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
.filter(() => this.selectList.length > 0)
|
||||
.on(e => this.onMouseUp(e), this, this.toDispose);
|
||||
|
||||
this.toDispose.push(this.selectList.onDidBlur(e => this.onListBlur()));
|
||||
this.toDispose.push(
|
||||
this.selectList.onDidBlur(_ => this.onListBlur()),
|
||||
this.selectList.onMouseOver(e => typeof e.index !== 'undefined' && this.selectList.setFocus([e.index])),
|
||||
this.selectList.onFocusChange(e => this.onListFocus(e))
|
||||
);
|
||||
|
||||
this.selectList.getHTMLElement().setAttribute('aria-label', this.selectBoxOptions.ariaLabel || '');
|
||||
this.selectList.getHTMLElement().setAttribute('aria-expanded', 'true');
|
||||
|
||||
this.styleList();
|
||||
}
|
||||
|
||||
// List methods
|
||||
@@ -617,6 +815,9 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
}
|
||||
|
||||
const listRowElement = e.toElement.parentElement;
|
||||
if (!listRowElement) {
|
||||
return;
|
||||
}
|
||||
const index = Number(listRowElement.getAttribute('data-index'));
|
||||
const disabled = listRowElement.classList.contains('option-disabled');
|
||||
|
||||
@@ -632,13 +833,13 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
if (this.selected !== this._currentSelection) {
|
||||
// Set current = selected
|
||||
this._currentSelection = this.selected;
|
||||
|
||||
|
||||
// {{SQL CARBON EDIT}} - Update the selection before firing the handler instead of after
|
||||
this.hideSelectDropDown(true);
|
||||
|
||||
this._onDidSelect.fire({
|
||||
index: this.selectElement.selectedIndex,
|
||||
selected: this.selectElement.title
|
||||
selected: this.options[this.selected]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -646,7 +847,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
|
||||
// List Exit - passive - implicit no selection change, hide drop-down
|
||||
private onListBlur(): void {
|
||||
|
||||
if (this._sticky) { return; }
|
||||
if (this.selected !== this._currentSelection) {
|
||||
// Reset selected to current if no change
|
||||
this.select(this._currentSelection);
|
||||
@@ -655,6 +856,59 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
this.hideSelectDropDown(false);
|
||||
}
|
||||
|
||||
|
||||
private renderDescriptionMarkdown(text: string): HTMLElement {
|
||||
const cleanRenderedMarkdown = (element: Node) => {
|
||||
for (let i = 0; i < element.childNodes.length; i++) {
|
||||
const child = element.childNodes.item(i);
|
||||
|
||||
const tagName = (<Element>child).tagName && (<Element>child).tagName.toLowerCase();
|
||||
if (tagName === 'img') {
|
||||
element.removeChild(child);
|
||||
} else {
|
||||
cleanRenderedMarkdown(child);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const renderedMarkdown = renderMarkdown({ value: text }, {
|
||||
actionHandler: this.selectBoxOptions.markdownActionHandler
|
||||
});
|
||||
|
||||
renderedMarkdown.classList.add('select-box-description-markdown');
|
||||
cleanRenderedMarkdown(renderedMarkdown);
|
||||
|
||||
return renderedMarkdown;
|
||||
}
|
||||
|
||||
// List Focus Change - passive - update details pane with newly focused element's data
|
||||
private onListFocus(e: IListEvent<ISelectOptionItem>) {
|
||||
// Skip during initial layout
|
||||
if (!this._isVisible) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.selectionDetailsPane.innerText = '';
|
||||
const selectedIndex = e.indexes[0];
|
||||
let description = this.detailsProvider ? this.detailsProvider(selectedIndex) : { details: '', isMarkdown: false };
|
||||
if (description.details) {
|
||||
if (description.isMarkdown) {
|
||||
this.selectionDetailsPane.appendChild(this.renderDescriptionMarkdown(description.details));
|
||||
} else {
|
||||
this.selectionDetailsPane.innerText = description.details;
|
||||
}
|
||||
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
|
||||
@@ -675,7 +929,7 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
this._currentSelection = this.selected;
|
||||
this._onDidSelect.fire({
|
||||
index: this.selectElement.selectedIndex,
|
||||
selected: this.selectElement.title
|
||||
selected: this.options[this.selected]
|
||||
});
|
||||
}
|
||||
|
||||
@@ -688,6 +942,8 @@ export class SelectBoxList implements ISelectBoxDelegate, IVirtualDelegate<ISele
|
||||
// Skip disabled options
|
||||
if ((this.selected + 1) === this.disabledOptionIndex && this.options.length > this.selected + 2) {
|
||||
this.selected += 2;
|
||||
} else if ((this.selected + 1) === this.disabledOptionIndex) {
|
||||
return;
|
||||
} else {
|
||||
this.selected++;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user