mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Merge from vscode 2c306f762bf9c3db82dc06c7afaa56ef46d72f79 (#14050)
* Merge from vscode 2c306f762bf9c3db82dc06c7afaa56ef46d72f79 * Fix breaks * Extension management fixes * Fix breaks in windows bundling * Fix/skip failing tests * Update distro * Add clear to nuget.config * Add hygiene task * Bump distro * Fix hygiene issue * Add build to hygiene exclusion * Update distro * Update hygiene * Hygiene exclusions * Update tsconfig * Bump distro for server breaks * Update build config * Update darwin path * Add done calls to notebook tests * Skip failing tests * Disable smoke tests
This commit is contained in:
@@ -18,7 +18,7 @@ export function renderCodicons(text: string): Array<HTMLSpanElement | string> {
|
||||
textStart = (match.index || 0) + match[0].length;
|
||||
|
||||
const [, escaped, codicon, name, animation] = match;
|
||||
elements.push(escaped ? `$(${codicon})` : dom.$(`span.codicon.codicon-${name}${animation ? `.codicon-animation-${animation}` : ''}`));
|
||||
elements.push(escaped ? `$(${codicon})` : renderCodicon(name, animation));
|
||||
}
|
||||
|
||||
if (textStart < text.length) {
|
||||
@@ -26,3 +26,7 @@ export function renderCodicons(text: string): Array<HTMLSpanElement | string> {
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
|
||||
export function renderCodicon(name: string, animation: string): HTMLSpanElement {
|
||||
return dom.$(`span.codicon.codicon-${name}${animation ? `.codicon-animation-${animation}` : ''}`);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import { IAction, IActionRunner, IActionViewItem } from 'vs/base/common/actions';
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { AnchorAlignment, AnchorAxisAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
|
||||
export interface IContextMenuEvent {
|
||||
readonly shiftKey?: boolean;
|
||||
@@ -26,6 +26,7 @@ export interface IContextMenuDelegate {
|
||||
actionRunner?: IActionRunner;
|
||||
autoSelectFirstItem?: boolean;
|
||||
anchorAlignment?: AnchorAlignment;
|
||||
anchorAxisAlignment?: AnchorAxisAlignment;
|
||||
domForShadowRoot?: HTMLElement;
|
||||
}
|
||||
|
||||
|
||||
@@ -10,11 +10,13 @@ import { IMouseEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { TimeoutTimer } from 'vs/base/common/async';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Schemas, RemoteAuthorities } from 'vs/base/common/network';
|
||||
import { FileAccess, RemoteAuthorities } from 'vs/base/common/network';
|
||||
import { BrowserFeatures } from 'vs/base/browser/canIUse';
|
||||
import { insane, InsaneOptions } from 'vs/base/common/insane/insane';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
|
||||
export function clearNode(node: HTMLElement): void {
|
||||
while (node.firstChild) {
|
||||
@@ -31,6 +33,13 @@ export function removeNode(node: HTMLElement): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function trustedInnerHTML(node: Element, value: TrustedHTML): void {
|
||||
// this is a workaround for innerHTML not allowing for "asymetric" accessors
|
||||
// see https://github.com/microsoft/vscode/issues/106396#issuecomment-692625393
|
||||
// and https://github.com/microsoft/TypeScript/issues/30024
|
||||
node.innerHTML = value as unknown as string;
|
||||
}
|
||||
|
||||
export function isInDOM(node: Node | null): boolean {
|
||||
while (node) {
|
||||
if (node === document.body) {
|
||||
@@ -517,6 +526,26 @@ export class Dimension implements IDimension {
|
||||
public readonly height: number,
|
||||
) { }
|
||||
|
||||
with(width: number = this.width, height: number = this.height): Dimension {
|
||||
if (width !== this.width || height !== this.height) {
|
||||
return new Dimension(width, height);
|
||||
} else {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
static is(obj: unknown): obj is IDimension {
|
||||
return typeof obj === 'object' && typeof (<IDimension>obj).height === 'number' && typeof (<IDimension>obj).width === 'number';
|
||||
}
|
||||
|
||||
static lift(obj: IDimension): Dimension {
|
||||
if (obj instanceof Dimension) {
|
||||
return obj;
|
||||
} else {
|
||||
return new Dimension(obj.width, obj.height);
|
||||
}
|
||||
}
|
||||
|
||||
static equals(a: Dimension | undefined, b: Dimension | undefined): boolean {
|
||||
if (a === b) {
|
||||
return true;
|
||||
@@ -702,15 +731,57 @@ export function isAncestor(testChild: Node | null, testAncestor: Node | null): b
|
||||
return false;
|
||||
}
|
||||
|
||||
const parentFlowToDataKey = 'parentFlowToElementId';
|
||||
|
||||
/**
|
||||
* Set an explicit parent to use for nodes that are not part of the
|
||||
* regular dom structure.
|
||||
*/
|
||||
export function setParentFlowTo(fromChildElement: HTMLElement, toParentElement: Element): void {
|
||||
fromChildElement.dataset[parentFlowToDataKey] = toParentElement.id;
|
||||
}
|
||||
|
||||
function getParentFlowToElement(node: HTMLElement): HTMLElement | null {
|
||||
const flowToParentId = node.dataset[parentFlowToDataKey];
|
||||
if (typeof flowToParentId === 'string') {
|
||||
return document.getElementById(flowToParentId);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if `testAncestor` is an ancessor of `testChild`, observing the explicit
|
||||
* parents set by `setParentFlowTo`.
|
||||
*/
|
||||
export function isAncestorUsingFlowTo(testChild: Node, testAncestor: Node): boolean {
|
||||
let node: Node | null = testChild;
|
||||
while (node) {
|
||||
if (node === testAncestor) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (node instanceof HTMLElement) {
|
||||
const flowToParentElement = getParentFlowToElement(node);
|
||||
if (flowToParentElement) {
|
||||
node = flowToParentElement;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
node = node.parentNode;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function findParentWithClass(node: HTMLElement, clazz: string, stopAtClazzOrNode?: string | HTMLElement): HTMLElement | null {
|
||||
while (node && node.nodeType === node.ELEMENT_NODE) {
|
||||
if (hasClass(node, clazz)) {
|
||||
if (node.classList.contains(clazz)) {
|
||||
return node;
|
||||
}
|
||||
|
||||
if (stopAtClazzOrNode) {
|
||||
if (typeof stopAtClazzOrNode === 'string') {
|
||||
if (hasClass(node, stopAtClazzOrNode)) {
|
||||
if (node.classList.contains(stopAtClazzOrNode)) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
@@ -1015,8 +1086,15 @@ export function prepend<T extends Node>(parent: HTMLElement, child: T): T {
|
||||
/**
|
||||
* Removes all children from `parent` and appends `children`
|
||||
*/
|
||||
export function reset(parent: HTMLElement, ...children: Array<Node | string>) {
|
||||
export function reset(parent: HTMLElement, ...children: Array<Node | string>): void {
|
||||
parent.innerText = '';
|
||||
appendChildren(parent, ...children);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends `children` to `parent`
|
||||
*/
|
||||
export function appendChildren(parent: HTMLElement, ...children: Array<Node | string>): void {
|
||||
for (const child of children) {
|
||||
if (child instanceof Node) {
|
||||
parent.appendChild(child);
|
||||
@@ -1196,7 +1274,7 @@ export function computeScreenAwareSize(cssPx: number): number {
|
||||
}
|
||||
|
||||
/**
|
||||
* See https://github.com/Microsoft/monaco-editor/issues/601
|
||||
* See https://github.com/microsoft/monaco-editor/issues/601
|
||||
* To protect against malicious code in the linked site, particularly phishing attempts,
|
||||
* the window.opener should be set to null to prevent the linked site from having access
|
||||
* to change the location of the current page.
|
||||
@@ -1205,7 +1283,7 @@ export function computeScreenAwareSize(cssPx: number): number {
|
||||
export function windowOpenNoOpener(url: string): void {
|
||||
if (platform.isNative || browser.isEdgeWebView) {
|
||||
// In VSCode, window.open() always returns null...
|
||||
// The same is true for a WebView (see https://github.com/Microsoft/monaco-editor/issues/628)
|
||||
// The same is true for a WebView (see https://github.com/microsoft/monaco-editor/issues/628)
|
||||
window.open(url);
|
||||
} else {
|
||||
let newTab = window.open();
|
||||
@@ -1228,16 +1306,6 @@ export function animate(fn: () => void): IDisposable {
|
||||
|
||||
RemoteAuthorities.setPreferredWebSchema(/^https:/.test(window.location.href) ? 'https' : 'http');
|
||||
|
||||
export function asDomUri(uri: URI): URI {
|
||||
if (!uri) {
|
||||
return uri;
|
||||
}
|
||||
if (Schemas.vscodeRemote === uri.scheme) {
|
||||
return RemoteAuthorities.rewrite(uri);
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns url('...')
|
||||
*/
|
||||
@@ -1245,10 +1313,9 @@ export function asCSSUrl(uri: URI): string {
|
||||
if (!uri) {
|
||||
return `url('')`;
|
||||
}
|
||||
return `url('${asDomUri(uri).toString(true).replace(/'/g, '%27')}')`;
|
||||
return `url('${FileAccess.asBrowserUri(uri).toString(true).replace(/'/g, '%27')}')`;
|
||||
}
|
||||
|
||||
|
||||
export function triggerDownload(dataOrUri: Uint8Array | URI, name: string): void {
|
||||
|
||||
// If the data is provided as Buffer, we create a
|
||||
@@ -1340,3 +1407,261 @@ export function detectFullscreen(): IDetectedFullscreen | null {
|
||||
// Not in fullscreen
|
||||
return null;
|
||||
}
|
||||
|
||||
// -- sanitize and trusted html
|
||||
|
||||
function _extInsaneOptions(opts: InsaneOptions, allowedAttributesForAll: string[]): InsaneOptions {
|
||||
|
||||
let allowedAttributes: Record<string, string[]> = opts.allowedAttributes ?? {};
|
||||
|
||||
if (opts.allowedTags) {
|
||||
for (let tag of opts.allowedTags) {
|
||||
let array = allowedAttributes[tag];
|
||||
if (!array) {
|
||||
array = allowedAttributesForAll;
|
||||
} else {
|
||||
array = array.concat(allowedAttributesForAll);
|
||||
}
|
||||
allowedAttributes[tag] = array;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...opts, allowedAttributes };
|
||||
}
|
||||
|
||||
const _ttpSafeInnerHtml = window.trustedTypes?.createPolicy('safeInnerHtml', {
|
||||
createHTML(value, options: InsaneOptions) {
|
||||
return insane(value, options);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Sanitizes the given `value` and reset the given `node` with it.
|
||||
*/
|
||||
export function safeInnerHtml(node: HTMLElement, value: string): void {
|
||||
|
||||
const options = _extInsaneOptions({
|
||||
allowedTags: ['a', 'button', 'blockquote', 'code', 'div', 'h1', 'h2', 'h3', 'input', 'label', 'li', 'p', 'pre', 'select', 'small', 'span', 'strong', 'textarea', 'ul', 'ol'],
|
||||
allowedAttributes: {
|
||||
'a': ['href', 'x-dispatch'],
|
||||
'button': ['data-href', 'x-dispatch'],
|
||||
'input': ['type', 'placeholder', 'checked', 'required'],
|
||||
'label': ['for'],
|
||||
'select': ['required'],
|
||||
'span': ['data-command', 'role'],
|
||||
'textarea': ['name', 'placeholder', 'required'],
|
||||
},
|
||||
allowedSchemes: ['http', 'https', 'command']
|
||||
}, ['class', 'id', 'role', 'tabindex']);
|
||||
|
||||
const html = _ttpSafeInnerHtml?.createHTML(value, options) ?? insane(value, options);
|
||||
node.innerHTML = html as unknown as string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Unicode string to a string in which each 16-bit unit occupies only one byte
|
||||
*
|
||||
* From https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/btoa
|
||||
*/
|
||||
function toBinary(str: string): string {
|
||||
const codeUnits = new Uint16Array(str.length);
|
||||
for (let i = 0; i < codeUnits.length; i++) {
|
||||
codeUnits[i] = str.charCodeAt(i);
|
||||
}
|
||||
return String.fromCharCode(...new Uint8Array(codeUnits.buffer));
|
||||
}
|
||||
|
||||
/**
|
||||
* Version of the global `btoa` function that handles multi-byte characters instead
|
||||
* of throwing an exception.
|
||||
*/
|
||||
export function multibyteAwareBtoa(str: string): string {
|
||||
return btoa(toBinary(str));
|
||||
}
|
||||
|
||||
/**
|
||||
* Typings for the https://wicg.github.io/file-system-access
|
||||
*
|
||||
* Use `supported(window)` to find out if the browser supports this kind of API.
|
||||
*/
|
||||
export namespace WebFileSystemAccess {
|
||||
|
||||
// https://wicg.github.io/file-system-access/#dom-window-showdirectorypicker
|
||||
export interface FileSystemAccess {
|
||||
showDirectoryPicker: () => Promise<FileSystemDirectoryHandle>;
|
||||
}
|
||||
|
||||
// https://wicg.github.io/file-system-access/#api-filesystemdirectoryhandle
|
||||
export interface FileSystemDirectoryHandle {
|
||||
readonly kind: 'directory',
|
||||
readonly name: string,
|
||||
|
||||
getFileHandle: (name: string, options?: { create?: boolean }) => Promise<FileSystemFileHandle>;
|
||||
getDirectoryHandle: (name: string, options?: { create?: boolean }) => Promise<FileSystemDirectoryHandle>;
|
||||
}
|
||||
|
||||
// https://wicg.github.io/file-system-access/#api-filesystemfilehandle
|
||||
export interface FileSystemFileHandle {
|
||||
readonly kind: 'file',
|
||||
readonly name: string,
|
||||
|
||||
createWritable: (options?: { keepExistingData?: boolean }) => Promise<FileSystemWritableFileStream>;
|
||||
}
|
||||
|
||||
// https://wicg.github.io/file-system-access/#api-filesystemwritablefilestream
|
||||
export interface FileSystemWritableFileStream {
|
||||
write: (buffer: Uint8Array) => Promise<void>;
|
||||
close: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function supported(obj: any & Window): obj is FileSystemAccess {
|
||||
const candidate = obj as FileSystemAccess;
|
||||
if (typeof candidate?.showDirectoryPicker === 'function') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
type ModifierKey = 'alt' | 'ctrl' | 'shift' | 'meta';
|
||||
|
||||
export interface IModifierKeyStatus {
|
||||
altKey: boolean;
|
||||
shiftKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
metaKey: boolean;
|
||||
lastKeyPressed?: ModifierKey;
|
||||
lastKeyReleased?: ModifierKey;
|
||||
event?: KeyboardEvent;
|
||||
}
|
||||
|
||||
export class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
|
||||
|
||||
private readonly _subscriptions = new DisposableStore();
|
||||
private _keyStatus: IModifierKeyStatus;
|
||||
private static instance: ModifierKeyEmitter;
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
|
||||
this._keyStatus = {
|
||||
altKey: false,
|
||||
shiftKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false
|
||||
};
|
||||
|
||||
this._subscriptions.add(domEvent(document.body, 'keydown', true)(e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
|
||||
if (e.altKey && !this._keyStatus.altKey) {
|
||||
this._keyStatus.lastKeyPressed = 'alt';
|
||||
} else if (e.ctrlKey && !this._keyStatus.ctrlKey) {
|
||||
this._keyStatus.lastKeyPressed = 'ctrl';
|
||||
} else if (e.metaKey && !this._keyStatus.metaKey) {
|
||||
this._keyStatus.lastKeyPressed = 'meta';
|
||||
} else if (e.shiftKey && !this._keyStatus.shiftKey) {
|
||||
this._keyStatus.lastKeyPressed = 'shift';
|
||||
} else if (event.keyCode !== KeyCode.Alt) {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this._keyStatus.altKey = e.altKey;
|
||||
this._keyStatus.ctrlKey = e.ctrlKey;
|
||||
this._keyStatus.metaKey = e.metaKey;
|
||||
this._keyStatus.shiftKey = e.shiftKey;
|
||||
|
||||
if (this._keyStatus.lastKeyPressed) {
|
||||
this._keyStatus.event = e;
|
||||
this.fire(this._keyStatus);
|
||||
}
|
||||
}));
|
||||
|
||||
this._subscriptions.add(domEvent(document.body, 'keyup', true)(e => {
|
||||
if (!e.altKey && this._keyStatus.altKey) {
|
||||
this._keyStatus.lastKeyReleased = 'alt';
|
||||
} else if (!e.ctrlKey && this._keyStatus.ctrlKey) {
|
||||
this._keyStatus.lastKeyReleased = 'ctrl';
|
||||
} else if (!e.metaKey && this._keyStatus.metaKey) {
|
||||
this._keyStatus.lastKeyReleased = 'meta';
|
||||
} else if (!e.shiftKey && this._keyStatus.shiftKey) {
|
||||
this._keyStatus.lastKeyReleased = 'shift';
|
||||
} else {
|
||||
this._keyStatus.lastKeyReleased = undefined;
|
||||
}
|
||||
|
||||
if (this._keyStatus.lastKeyPressed !== this._keyStatus.lastKeyReleased) {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
}
|
||||
|
||||
this._keyStatus.altKey = e.altKey;
|
||||
this._keyStatus.ctrlKey = e.ctrlKey;
|
||||
this._keyStatus.metaKey = e.metaKey;
|
||||
this._keyStatus.shiftKey = e.shiftKey;
|
||||
|
||||
if (this._keyStatus.lastKeyReleased) {
|
||||
this._keyStatus.event = e;
|
||||
this.fire(this._keyStatus);
|
||||
}
|
||||
}));
|
||||
|
||||
this._subscriptions.add(domEvent(document.body, 'mousedown', true)(e => {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
}));
|
||||
|
||||
this._subscriptions.add(domEvent(document.body, 'mouseup', true)(e => {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
}));
|
||||
|
||||
this._subscriptions.add(domEvent(document.body, 'mousemove', true)(e => {
|
||||
if (e.buttons) {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
}
|
||||
}));
|
||||
|
||||
this._subscriptions.add(domEvent(window, 'blur')(e => {
|
||||
this.resetKeyStatus();
|
||||
}));
|
||||
}
|
||||
|
||||
get keyStatus(): IModifierKeyStatus {
|
||||
return this._keyStatus;
|
||||
}
|
||||
|
||||
get isModifierPressed(): boolean {
|
||||
return this._keyStatus.altKey || this._keyStatus.ctrlKey || this._keyStatus.metaKey || this._keyStatus.shiftKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows to explicitly reset the key status based on more knowledge (#109062)
|
||||
*/
|
||||
resetKeyStatus(): void {
|
||||
this.doResetKeyStatus();
|
||||
this.fire(this._keyStatus);
|
||||
}
|
||||
|
||||
private doResetKeyStatus(): void {
|
||||
this._keyStatus = {
|
||||
altKey: false,
|
||||
shiftKey: false,
|
||||
ctrlKey: false,
|
||||
metaKey: false
|
||||
};
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (!ModifierKeyEmitter.instance) {
|
||||
ModifierKeyEmitter.instance = new ModifierKeyEmitter();
|
||||
}
|
||||
|
||||
return ModifierKeyEmitter.instance;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
this._subscriptions.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
|
||||
export class FastDomNode<T extends HTMLElement> {
|
||||
|
||||
public readonly domNode: T;
|
||||
@@ -176,7 +174,7 @@ export class FastDomNode<T extends HTMLElement> {
|
||||
}
|
||||
|
||||
public toggleClassName(className: string, shouldHaveIt?: boolean): void {
|
||||
dom.toggleClass(this.domNode, className, shouldHaveIt);
|
||||
this.domNode.classList.toggle(className, shouldHaveIt);
|
||||
this._className = this.domNode.className;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,11 +4,9 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import * as platform from 'vs/base/common/platform';
|
||||
import { IframeUtils } from 'vs/base/browser/iframe';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { BrowserFeatures } from 'vs/base/browser/canIUse';
|
||||
|
||||
export interface IStandardMouseMoveEventData {
|
||||
leftButton: boolean;
|
||||
@@ -26,7 +24,7 @@ export interface IMouseMoveCallback<R> {
|
||||
}
|
||||
|
||||
export interface IOnStopCallback {
|
||||
(): void;
|
||||
(browserEvent?: MouseEvent | KeyboardEvent): void;
|
||||
}
|
||||
|
||||
export function standardMouseMoveMerger(lastEvent: IStandardMouseMoveEventData | null, currentEvent: MouseEvent): IStandardMouseMoveEventData {
|
||||
@@ -52,7 +50,7 @@ export class GlobalMouseMoveMonitor<R extends { buttons: number; }> implements I
|
||||
this._hooks.dispose();
|
||||
}
|
||||
|
||||
public stopMonitoring(invokeStopCallback: boolean): void {
|
||||
public stopMonitoring(invokeStopCallback: boolean, browserEvent?: MouseEvent | KeyboardEvent): void {
|
||||
if (!this.isMonitoring()) {
|
||||
// Not monitoring
|
||||
return;
|
||||
@@ -66,7 +64,7 @@ export class GlobalMouseMoveMonitor<R extends { buttons: number; }> implements I
|
||||
this._onStopCallback = null;
|
||||
|
||||
if (invokeStopCallback && onStopCallback) {
|
||||
onStopCallback();
|
||||
onStopCallback(browserEvent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,8 +88,8 @@ export class GlobalMouseMoveMonitor<R extends { buttons: number; }> implements I
|
||||
this._onStopCallback = onStopCallback;
|
||||
|
||||
const windowChain = IframeUtils.getSameOriginWindowChain();
|
||||
const mouseMove = platform.isIOS && BrowserFeatures.pointerEvents ? 'pointermove' : 'mousemove';
|
||||
const mouseUp = platform.isIOS && BrowserFeatures.pointerEvents ? 'pointerup' : 'mouseup';
|
||||
const mouseMove = 'mousemove';
|
||||
const mouseUp = 'mouseup';
|
||||
|
||||
const listenTo: (Document | ShadowRoot)[] = windowChain.map(element => element.window.document);
|
||||
const shadowRoot = dom.getShadowRoot(initialElement);
|
||||
|
||||
25
src/vs/base/browser/hash.ts
Normal file
25
src/vs/base/browser/hash.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { VSBuffer } from 'vs/base/common/buffer';
|
||||
import { StringSHA1, toHexString } from 'vs/base/common/hash';
|
||||
|
||||
export async function sha1Hex(str: string): Promise<string> {
|
||||
|
||||
// Prefer to use browser's crypto module
|
||||
if (globalThis?.crypto?.subtle) {
|
||||
const hash = await globalThis.crypto.subtle.digest({ name: 'sha-1' }, VSBuffer.fromString(str).buffer);
|
||||
|
||||
return toHexString(hash);
|
||||
}
|
||||
|
||||
// Otherwise fallback to `StringSHA1`
|
||||
else {
|
||||
const computer = new StringSHA1();
|
||||
computer.update(str);
|
||||
|
||||
return computer.digest();
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ export interface IWindowChainElement {
|
||||
/**
|
||||
* The iframe element inside the window.parent corresponding to window
|
||||
*/
|
||||
iframeElement: HTMLIFrameElement | null;
|
||||
iframeElement: Element | null;
|
||||
}
|
||||
|
||||
let hasDifferentOriginAncestorFlag: boolean = false;
|
||||
@@ -29,9 +29,11 @@ function getParentWindowIfSameOrigin(w: Window): Window | null {
|
||||
try {
|
||||
let location = w.location;
|
||||
let parentLocation = w.parent.location;
|
||||
if (location.protocol !== parentLocation.protocol || location.hostname !== parentLocation.hostname || location.port !== parentLocation.port) {
|
||||
hasDifferentOriginAncestorFlag = true;
|
||||
return null;
|
||||
if (location.origin !== 'null' && parentLocation.origin !== 'null') {
|
||||
if (location.protocol !== parentLocation.protocol || location.hostname !== parentLocation.hostname || location.port !== parentLocation.port) {
|
||||
hasDifferentOriginAncestorFlag = true;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
hasDifferentOriginAncestorFlag = true;
|
||||
@@ -113,6 +115,9 @@ export class IframeUtils {
|
||||
|
||||
for (const windowChainEl of windowChain) {
|
||||
|
||||
top += windowChainEl.window.scrollY;
|
||||
left += windowChainEl.window.scrollX;
|
||||
|
||||
if (windowChainEl.window === ancestorWindow) {
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3,35 +3,46 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as DOM from 'vs/base/browser/dom'; // {{SQL CARBON EDIT}} added missing import to fix build break
|
||||
import { createElement, FormattedTextRenderOptions } from 'vs/base/browser/formattedTextRenderer';
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { IMarkdownString, parseHrefAndDimensions, removeMarkdownEscapes } from 'vs/base/common/htmlContent';
|
||||
import { defaultGenerator } from 'vs/base/common/idGenerator';
|
||||
import * as marked from 'vs/base/common/marked/marked';
|
||||
import { insane } from 'vs/base/common/insane/insane';
|
||||
import { insane, InsaneOptions } from 'vs/base/common/insane/insane';
|
||||
import { parse } from 'vs/base/common/marshalling';
|
||||
import { cloneAndChange } from 'vs/base/common/objects';
|
||||
import { escape } from 'vs/base/common/strings';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { FileAccess, Schemas } from 'vs/base/common/network';
|
||||
import { markdownEscapeEscapedCodicons } from 'vs/base/common/codicons';
|
||||
import { resolvePath } from 'vs/base/common/resources';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { renderCodicons } from 'vs/base/browser/codicons';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
|
||||
export interface MarkedOptions extends marked.MarkedOptions {
|
||||
baseUrl?: never;
|
||||
}
|
||||
|
||||
export interface MarkdownRenderOptions extends FormattedTextRenderOptions {
|
||||
codeBlockRenderer?: (modeId: string, value: string) => Promise<string>;
|
||||
codeBlockRenderCallback?: () => void;
|
||||
codeBlockRenderer?: (modeId: string, value: string) => Promise<HTMLElement>;
|
||||
asyncRenderCallback?: () => void;
|
||||
baseUrl?: URI;
|
||||
}
|
||||
|
||||
const _ttpInsane = window.trustedTypes?.createPolicy('insane', {
|
||||
createHTML(value, options: InsaneOptions): string {
|
||||
return insane(value, options);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Create html nodes for the given content element.
|
||||
* Low-level way create a html element from a markdown string.
|
||||
*
|
||||
* **Note** that for most cases you should be using [`MarkdownRenderer`](./src/vs/editor/browser/core/markdownRenderer.ts)
|
||||
* which comes with support for pretty code block rendering and which uses the default way of handling links.
|
||||
*/
|
||||
export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRenderOptions = {}, markedOptions: MarkedOptions = {}): HTMLElement {
|
||||
const element = createElement(options);
|
||||
@@ -70,7 +81,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
|
||||
// and because of that special rewriting needs to be done
|
||||
// so that the URI uses a protocol that's understood by
|
||||
// browsers (like http or https)
|
||||
return DOM.asDomUri(uri).toString(true);
|
||||
return FileAccess.asBrowserUri(uri).toString(true);
|
||||
}
|
||||
if (uri.query) {
|
||||
uri = uri.with({ query: _uriMassage(uri.query) });
|
||||
@@ -161,9 +172,9 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
|
||||
// {{SQL CARBON EDIT}} - Promise.all not returning the strValue properly in original code? @todo anthonydresser 4/12/19 investigate a better way to do this.
|
||||
const promise = value.then(strValue => {
|
||||
withInnerHTML.then(e => {
|
||||
const span = element.querySelector(`div[data-code="${id}"]`);
|
||||
const span = <HTMLDivElement>element.querySelector(`div[data-code="${id}"]`);
|
||||
if (span) {
|
||||
span.innerHTML = strValue;
|
||||
span.innerHTML = strValue.innerHTML;
|
||||
}
|
||||
}).catch(err => {
|
||||
// ignore
|
||||
@@ -181,83 +192,115 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
|
||||
// // ignore
|
||||
// });
|
||||
|
||||
if (options.codeBlockRenderCallback) {
|
||||
promise.then(options.codeBlockRenderCallback);
|
||||
if (options.asyncRenderCallback) {
|
||||
promise.then(options.asyncRenderCallback);
|
||||
}
|
||||
|
||||
return `<div class="code" data-code="${id}">${escape(code)}</div>`;
|
||||
};
|
||||
}
|
||||
|
||||
const actionHandler = options.actionHandler;
|
||||
if (actionHandler) {
|
||||
[DOM.EventType.CLICK, DOM.EventType.AUXCLICK].forEach(event => {
|
||||
actionHandler.disposeables.add(DOM.addDisposableListener(element, event, (e: MouseEvent) => {
|
||||
const mouseEvent = new StandardMouseEvent(e);
|
||||
if (!mouseEvent.leftButton && !mouseEvent.middleButton) {
|
||||
if (options.actionHandler) {
|
||||
options.actionHandler.disposeables.add(Event.any<MouseEvent>(domEvent(element, 'click'), domEvent(element, 'auxclick'))(e => {
|
||||
const mouseEvent = new StandardMouseEvent(e);
|
||||
if (!mouseEvent.leftButton && !mouseEvent.middleButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
let target: HTMLElement | null = mouseEvent.target;
|
||||
if (target.tagName !== 'A') {
|
||||
target = target.parentElement;
|
||||
if (!target || target.tagName !== 'A') {
|
||||
return;
|
||||
}
|
||||
|
||||
let target: HTMLElement | null = mouseEvent.target;
|
||||
if (target.tagName !== 'A') {
|
||||
target = target.parentElement;
|
||||
if (!target || target.tagName !== 'A') {
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
const href = target.dataset['href'];
|
||||
if (href) {
|
||||
options.actionHandler!.callback(href, mouseEvent);
|
||||
}
|
||||
try {
|
||||
const href = target.dataset['href'];
|
||||
if (href) {
|
||||
actionHandler.callback(href, mouseEvent);
|
||||
}
|
||||
} catch (err) {
|
||||
onUnexpectedError(err);
|
||||
} finally {
|
||||
mouseEvent.preventDefault();
|
||||
}
|
||||
}));
|
||||
});
|
||||
} catch (err) {
|
||||
onUnexpectedError(err);
|
||||
} finally {
|
||||
mouseEvent.preventDefault();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
// Use our own sanitizer so that we can let through only spans.
|
||||
// Otherwise, we'd be letting all html be rendered.
|
||||
// If we want to allow markdown permitted tags, then we can delete sanitizer and sanitize.
|
||||
// We always pass the output through insane after this so that we don't rely on
|
||||
// marked for sanitization.
|
||||
markedOptions.sanitizer = (html: string): string => {
|
||||
const match = markdown.isTrusted ? html.match(/^(<span[^<]+>)|(<\/\s*span>)$/) : undefined;
|
||||
return match ? html : '';
|
||||
};
|
||||
markedOptions.sanitize = true;
|
||||
markedOptions.renderer = renderer;
|
||||
markedOptions.silent = true;
|
||||
|
||||
const allowedSchemes = [Schemas.http, Schemas.https, Schemas.mailto, Schemas.data, Schemas.file, Schemas.vscodeRemote, Schemas.vscodeRemoteResource];
|
||||
if (markdown.isTrusted) {
|
||||
allowedSchemes.push(Schemas.command);
|
||||
}
|
||||
markedOptions.renderer = renderer;
|
||||
|
||||
// values that are too long will freeze the UI
|
||||
let value = markdown.value ?? '';
|
||||
if (value.length > 100_000) {
|
||||
value = `${value.substr(0, 100_000)}…`;
|
||||
}
|
||||
const renderedMarkdown = marked.parse(
|
||||
markdown.supportThemeIcons ? markdownEscapeEscapedCodicons(value) : value,
|
||||
markedOptions
|
||||
);
|
||||
|
||||
function filter(token: { tag: string, attrs: { readonly [key: string]: string } }): boolean {
|
||||
if (token.tag === 'span' && markdown.isTrusted && (Object.keys(token.attrs).length === 1)) {
|
||||
if (token.attrs['style']) {
|
||||
return !!token.attrs['style'].match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/);
|
||||
} else if (token.attrs['class']) {
|
||||
// The class should match codicon rendering in src\vs\base\common\codicons.ts
|
||||
return !!token.attrs['class'].match(/^codicon codicon-[a-z\-]+( codicon-animation-[a-z\-]+)?$/);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
// escape theme icons
|
||||
if (markdown.supportThemeIcons) {
|
||||
value = markdownEscapeEscapedCodicons(value);
|
||||
}
|
||||
|
||||
element.innerHTML = insane(renderedMarkdown, {
|
||||
const renderedMarkdown = marked.parse(value, markedOptions);
|
||||
|
||||
// sanitize with insane
|
||||
element.innerHTML = sanitizeRenderedMarkdown(markdown, renderedMarkdown);
|
||||
|
||||
// signal that async code blocks can be now be inserted
|
||||
signalInnerHTML!();
|
||||
|
||||
// signal size changes for image tags
|
||||
if (options.asyncRenderCallback) {
|
||||
for (const img of element.getElementsByTagName('img')) {
|
||||
const listener = DOM.addDisposableListener(img, 'load', () => {
|
||||
listener.dispose();
|
||||
options.asyncRenderCallback!();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
function sanitizeRenderedMarkdown(
|
||||
options: { isTrusted?: boolean },
|
||||
renderedMarkdown: string,
|
||||
): string {
|
||||
const insaneOptions = getInsaneOptions(options);
|
||||
if (_ttpInsane) {
|
||||
return _ttpInsane.createHTML(renderedMarkdown, insaneOptions) as unknown as string;
|
||||
} else {
|
||||
return insane(renderedMarkdown, insaneOptions);
|
||||
}
|
||||
}
|
||||
|
||||
function getInsaneOptions(options: { readonly isTrusted?: boolean }): InsaneOptions {
|
||||
const allowedSchemes = [
|
||||
Schemas.http,
|
||||
Schemas.https,
|
||||
Schemas.mailto,
|
||||
Schemas.data,
|
||||
Schemas.file,
|
||||
Schemas.vscodeRemote,
|
||||
Schemas.vscodeRemoteResource,
|
||||
];
|
||||
|
||||
if (options.isTrusted) {
|
||||
allowedSchemes.push(Schemas.command);
|
||||
}
|
||||
|
||||
return {
|
||||
allowedSchemes,
|
||||
// allowedTags should included everything that markdown renders to.
|
||||
// Since we have our own sanitize function for marked, it's possible we missed some tag so let insane make sure.
|
||||
@@ -273,10 +316,91 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
|
||||
'th': ['align'],
|
||||
'td': ['align']
|
||||
},
|
||||
filter
|
||||
});
|
||||
|
||||
signalInnerHTML!();
|
||||
|
||||
return element;
|
||||
filter(token: { tag: string; attrs: { readonly [key: string]: string; }; }): boolean {
|
||||
if (token.tag === 'span' && options.isTrusted && (Object.keys(token.attrs).length === 1)) {
|
||||
if (token.attrs['style']) {
|
||||
return !!token.attrs['style'].match(/^(color\:#[0-9a-fA-F]+;)?(background-color\:#[0-9a-fA-F]+;)?$/);
|
||||
} else if (token.attrs['class']) {
|
||||
// The class should match codicon rendering in src\vs\base\common\codicons.ts
|
||||
return !!token.attrs['class'].match(/^codicon codicon-[a-z\-]+( codicon-animation-[a-z\-]+)?$/);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Strips all markdown from `markdown`. For example `# Header` would be output as `Header`.
|
||||
*/
|
||||
export function renderMarkdownAsPlaintext(markdown: IMarkdownString) {
|
||||
const renderer = new marked.Renderer();
|
||||
|
||||
renderer.code = (code: string): string => {
|
||||
return code;
|
||||
};
|
||||
renderer.blockquote = (quote: string): string => {
|
||||
return quote;
|
||||
};
|
||||
renderer.html = (_html: string): string => {
|
||||
return '';
|
||||
};
|
||||
renderer.heading = (text: string, _level: 1 | 2 | 3 | 4 | 5 | 6, _raw: string): string => {
|
||||
return text + '\n';
|
||||
};
|
||||
renderer.hr = (): string => {
|
||||
return '';
|
||||
};
|
||||
renderer.list = (body: string, _ordered: boolean): string => {
|
||||
return body;
|
||||
};
|
||||
renderer.listitem = (text: string): string => {
|
||||
return text + '\n';
|
||||
};
|
||||
renderer.paragraph = (text: string): string => {
|
||||
return text + '\n';
|
||||
};
|
||||
renderer.table = (header: string, body: string): string => {
|
||||
return header + body + '\n';
|
||||
};
|
||||
renderer.tablerow = (content: string): string => {
|
||||
return content;
|
||||
};
|
||||
renderer.tablecell = (content: string, _flags: {
|
||||
header: boolean;
|
||||
align: 'center' | 'left' | 'right' | null;
|
||||
}): string => {
|
||||
return content + ' ';
|
||||
};
|
||||
renderer.strong = (text: string): string => {
|
||||
return text;
|
||||
};
|
||||
renderer.em = (text: string): string => {
|
||||
return text;
|
||||
};
|
||||
renderer.codespan = (code: string): string => {
|
||||
return code;
|
||||
};
|
||||
renderer.br = (): string => {
|
||||
return '\n';
|
||||
};
|
||||
renderer.del = (text: string): string => {
|
||||
return text;
|
||||
};
|
||||
renderer.image = (_href: string, _title: string, _text: string): string => {
|
||||
return '';
|
||||
};
|
||||
renderer.text = (text: string): string => {
|
||||
return text;
|
||||
};
|
||||
renderer.link = (_href: string, _title: string, text: string): string => {
|
||||
return text;
|
||||
};
|
||||
// values that are too long will freeze the UI
|
||||
let value = markdown.value ?? '';
|
||||
if (value.length > 100_000) {
|
||||
value = `${value.substr(0, 100_000)}…`;
|
||||
}
|
||||
return sanitizeRenderedMarkdown({ isTrusted: false }, marked.parse(value, { renderer })).toString();
|
||||
}
|
||||
|
||||
@@ -167,7 +167,11 @@ export class StandardWheelEvent {
|
||||
|
||||
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
|
||||
// the deltas are expressed in lines
|
||||
this.deltaY = -e.deltaY;
|
||||
if (browser.isFirefox && !platform.isMacintosh) {
|
||||
this.deltaY = -e.deltaY / 3;
|
||||
} else {
|
||||
this.deltaY = -e.deltaY;
|
||||
}
|
||||
} else {
|
||||
this.deltaY = -e.deltaY / 40;
|
||||
}
|
||||
@@ -189,7 +193,11 @@ export class StandardWheelEvent {
|
||||
|
||||
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
|
||||
// the deltas are expressed in lines
|
||||
this.deltaX = -e.deltaX;
|
||||
if (browser.isFirefox && !platform.isMacintosh) {
|
||||
this.deltaX = -e.deltaX / 3;
|
||||
} else {
|
||||
this.deltaX = -e.deltaX;
|
||||
}
|
||||
} else {
|
||||
this.deltaX = -e.deltaX / 40;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { EventType as TouchEventType, Gesture } from 'vs/base/browser/touch';
|
||||
import { IContextViewProvider } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { DataTransfers } from 'vs/base/browser/dnd';
|
||||
import { isFirefox } from 'vs/base/browser/browser';
|
||||
import { $, addClasses, addDisposableListener, append, EventHelper, EventLike, EventType, removeClasses, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom';
|
||||
import { $, addDisposableListener, append, EventHelper, EventLike, EventType, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom';
|
||||
|
||||
export interface IBaseActionViewItemOptions {
|
||||
draggable?: boolean;
|
||||
@@ -304,7 +304,7 @@ export class ActionViewItem extends BaseActionViewItem {
|
||||
|
||||
updateClass(): void {
|
||||
if (this.cssClass && this.label) {
|
||||
removeClasses(this.label, this.cssClass);
|
||||
this.label.classList.remove(...this.cssClass.split(' '));
|
||||
}
|
||||
|
||||
if (this.options.icon) {
|
||||
@@ -313,7 +313,7 @@ export class ActionViewItem extends BaseActionViewItem {
|
||||
if (this.label) {
|
||||
this.label.classList.add('codicon');
|
||||
if (this.cssClass) {
|
||||
addClasses(this.label, this.cssClass);
|
||||
this.label.classList.add(...this.cssClass.split(' '));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -96,3 +96,15 @@
|
||||
justify-content: center;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item.action-dropdown-item {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item.action-dropdown-item > .action-label {
|
||||
margin-right: 1px;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item.action-dropdown-item > .monaco-dropdown {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ export const enum ActionsOrientation {
|
||||
}
|
||||
|
||||
export interface ActionTrigger {
|
||||
keys: KeyCode[];
|
||||
keys?: KeyCode[];
|
||||
keyDown: boolean;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ export interface IActionBarOptions {
|
||||
readonly triggerKeys?: ActionTrigger;
|
||||
readonly allowContextMenu?: boolean;
|
||||
readonly preventLoopNavigation?: boolean;
|
||||
readonly ignoreOrientationForPreviousAndNextKey?: boolean;
|
||||
}
|
||||
|
||||
export interface IActionOptions extends IActionViewItemOptions {
|
||||
@@ -48,7 +49,10 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
private _actionRunner: IActionRunner;
|
||||
private _context: unknown;
|
||||
private readonly _orientation: ActionsOrientation;
|
||||
private readonly _triggerKeys: ActionTrigger;
|
||||
private readonly _triggerKeys: {
|
||||
keys: KeyCode[];
|
||||
keyDown: boolean;
|
||||
};
|
||||
private _actionIds: string[];
|
||||
|
||||
// View Items
|
||||
@@ -56,6 +60,9 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
protected focusedItem?: number;
|
||||
private focusTracker: DOM.IFocusTracker;
|
||||
|
||||
// Trigger Key Tracking
|
||||
private triggerKeyDown: boolean = false;
|
||||
|
||||
// Elements
|
||||
domNode: HTMLElement;
|
||||
protected actionsList: HTMLElement;
|
||||
@@ -63,14 +70,15 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
private _onDidBlur = this._register(new Emitter<void>());
|
||||
readonly onDidBlur = this._onDidBlur.event;
|
||||
|
||||
private _onDidCancel = this._register(new Emitter<void>());
|
||||
private _onDidCancel = this._register(new Emitter<void>({ onFirstListenerAdd: () => this.cancelHasListener = true }));
|
||||
readonly onDidCancel = this._onDidCancel.event;
|
||||
private cancelHasListener = false;
|
||||
|
||||
private _onDidRun = this._register(new Emitter<IRunEvent>());
|
||||
readonly onDidRun = this._onDidRun.event;
|
||||
|
||||
private _onDidBeforeRun = this._register(new Emitter<IRunEvent>());
|
||||
readonly onDidBeforeRun = this._onDidBeforeRun.event;
|
||||
private _onBeforeRun = this._register(new Emitter<IRunEvent>());
|
||||
readonly onBeforeRun = this._onBeforeRun.event;
|
||||
|
||||
constructor(container: HTMLElement, options: IActionBarOptions = {}) {
|
||||
super();
|
||||
@@ -78,9 +86,9 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
this.options = options;
|
||||
this._context = options.context ?? null;
|
||||
this._orientation = this.options.orientation ?? ActionsOrientation.HORIZONTAL;
|
||||
this._triggerKeys = this.options.triggerKeys ?? {
|
||||
keys: [KeyCode.Enter, KeyCode.Space],
|
||||
keyDown: false
|
||||
this._triggerKeys = {
|
||||
keyDown: this.options.triggerKeys?.keyDown ?? false,
|
||||
keys: this.options.triggerKeys?.keys ?? [KeyCode.Enter, KeyCode.Space]
|
||||
};
|
||||
|
||||
if (this.options.actionRunner) {
|
||||
@@ -91,7 +99,7 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
}
|
||||
|
||||
this._register(this._actionRunner.onDidRun(e => this._onDidRun.fire(e)));
|
||||
this._register(this._actionRunner.onDidBeforeRun(e => this._onDidBeforeRun.fire(e)));
|
||||
this._register(this._actionRunner.onBeforeRun(e => this._onBeforeRun.fire(e)));
|
||||
|
||||
this._actionIds = [];
|
||||
this.viewItems = [];
|
||||
@@ -101,30 +109,30 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
this.domNode.className = 'monaco-action-bar';
|
||||
|
||||
if (options.animated !== false) {
|
||||
DOM.addClass(this.domNode, 'animated');
|
||||
this.domNode.classList.add('animated');
|
||||
}
|
||||
|
||||
let previousKey: KeyCode;
|
||||
let nextKey: KeyCode;
|
||||
let previousKeys: KeyCode[];
|
||||
let nextKeys: KeyCode[];
|
||||
|
||||
switch (this._orientation) {
|
||||
case ActionsOrientation.HORIZONTAL:
|
||||
previousKey = KeyCode.LeftArrow;
|
||||
nextKey = KeyCode.RightArrow;
|
||||
previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.LeftArrow];
|
||||
nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.RightArrow];
|
||||
break;
|
||||
case ActionsOrientation.HORIZONTAL_REVERSE:
|
||||
previousKey = KeyCode.RightArrow;
|
||||
nextKey = KeyCode.LeftArrow;
|
||||
previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.RightArrow];
|
||||
nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.LeftArrow];
|
||||
this.domNode.className += ' reverse';
|
||||
break;
|
||||
case ActionsOrientation.VERTICAL:
|
||||
previousKey = KeyCode.UpArrow;
|
||||
nextKey = KeyCode.DownArrow;
|
||||
previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.UpArrow];
|
||||
nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.DownArrow];
|
||||
this.domNode.className += ' vertical';
|
||||
break;
|
||||
case ActionsOrientation.VERTICAL_REVERSE:
|
||||
previousKey = KeyCode.DownArrow;
|
||||
nextKey = KeyCode.UpArrow;
|
||||
previousKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.RightArrow, KeyCode.DownArrow] : [KeyCode.DownArrow];
|
||||
nextKeys = this.options.ignoreOrientationForPreviousAndNextKey ? [KeyCode.LeftArrow, KeyCode.UpArrow] : [KeyCode.UpArrow];
|
||||
this.domNode.className += ' vertical reverse';
|
||||
break;
|
||||
}
|
||||
@@ -133,16 +141,18 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
let eventHandled = true;
|
||||
|
||||
if (event.equals(previousKey)) {
|
||||
if (previousKeys && (event.equals(previousKeys[0]) || event.equals(previousKeys[1]))) {
|
||||
eventHandled = this.focusPrevious();
|
||||
} else if (event.equals(nextKey)) {
|
||||
} else if (nextKeys && (event.equals(nextKeys[0]) || event.equals(nextKeys[1]))) {
|
||||
eventHandled = this.focusNext();
|
||||
} else if (event.equals(KeyCode.Escape)) {
|
||||
} else if (event.equals(KeyCode.Escape) && this.cancelHasListener) {
|
||||
this._onDidCancel.fire();
|
||||
} else if (this.isTriggerKeyEvent(event)) {
|
||||
// Staying out of the else branch even if not triggered
|
||||
if (this._triggerKeys.keyDown) {
|
||||
this.doTrigger(event);
|
||||
} else {
|
||||
this.triggerKeyDown = true;
|
||||
}
|
||||
} else {
|
||||
eventHandled = false;
|
||||
@@ -159,7 +169,8 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
|
||||
// Run action on Enter/Space
|
||||
if (this.isTriggerKeyEvent(event)) {
|
||||
if (!this._triggerKeys.keyDown) {
|
||||
if (!this._triggerKeys.keyDown && this.triggerKeyDown) {
|
||||
this.triggerKeyDown = false;
|
||||
this.doTrigger(event);
|
||||
}
|
||||
|
||||
@@ -178,6 +189,7 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
if (DOM.getActiveElement() === this.domNode || !DOM.isAncestor(DOM.getActiveElement(), this.domNode)) {
|
||||
this._onDidBlur.fire();
|
||||
this.focusedItem = undefined;
|
||||
this.triggerKeyDown = false;
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -358,9 +370,10 @@ export class ActionBar extends Disposable implements IActionRunner {
|
||||
}
|
||||
|
||||
if (selectFirst && typeof this.focusedItem === 'undefined') {
|
||||
const firstEnabled = this.viewItems.findIndex(item => item.isEnabled());
|
||||
// Focus the first enabled item
|
||||
this.focusedItem = -1;
|
||||
this.focusNext();
|
||||
this.focusedItem = firstEnabled === -1 ? undefined : firstEnabled;
|
||||
this.updateFocus();
|
||||
} else {
|
||||
if (index !== undefined) {
|
||||
this.focusedItem = index;
|
||||
|
||||
@@ -11,7 +11,7 @@ import { Color } from 'vs/base/common/color';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { dispose, IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
import { Codicon, registerCodicon } from 'vs/base/common/codicons';
|
||||
import 'vs/css!./breadcrumbsWidget';
|
||||
|
||||
export abstract class BreadcrumbsItem {
|
||||
@@ -56,7 +56,7 @@ export interface IBreadcrumbsItemEvent {
|
||||
payload: any;
|
||||
}
|
||||
|
||||
const breadcrumbSeparatorIcon = registerIcon('breadcrumb-separator', Codicon.chevronRight);
|
||||
const breadcrumbSeparatorIcon = registerCodicon('breadcrumb-separator', Codicon.chevronRight);
|
||||
|
||||
export class BreadcrumbsWidget {
|
||||
|
||||
@@ -336,7 +336,7 @@ export class BreadcrumbsWidget {
|
||||
item.render(container);
|
||||
container.tabIndex = -1;
|
||||
container.setAttribute('role', 'listitem');
|
||||
dom.addClasses(container, 'monaco-breadcrumb-item');
|
||||
container.classList.add('monaco-breadcrumb-item');
|
||||
const iconContainer = dom.$(breadcrumbSeparatorIcon.cssSelector);
|
||||
container.appendChild(iconContainer);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
padding: 4px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
outline-offset: 2px !important;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -24,7 +23,15 @@
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-button > .codicon {
|
||||
.monaco-text-button > .codicon {
|
||||
margin: 0 0.2em;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.monaco-button-dropdown {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monaco-button-dropdown > .monaco-dropdown-button {
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
@@ -9,10 +9,13 @@ import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { Event as BaseEvent, Emitter } from 'vs/base/common/event';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Gesture, EventType as TouchEventType } from 'vs/base/browser/touch';
|
||||
import { renderCodicons } from 'vs/base/browser/codicons';
|
||||
import { addDisposableListener, IFocusTracker, EventType, EventHelper, trackFocus, reset, removeTabIndexAndUpdateFocus } from 'vs/base/browser/dom';
|
||||
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
|
||||
import { IAction, IActionRunner } from 'vs/base/common/actions';
|
||||
import { CSSIcon, Codicon } from 'vs/base/common/codicons';
|
||||
|
||||
export interface IButtonOptions extends IButtonStyles {
|
||||
readonly title?: boolean | string;
|
||||
@@ -41,7 +44,18 @@ const defaultOptions: IButtonStyles = {
|
||||
buttonForeground: Color.white
|
||||
};
|
||||
|
||||
export class Button extends Disposable {
|
||||
export interface IButton extends IDisposable {
|
||||
readonly element: HTMLElement;
|
||||
readonly onDidClick: BaseEvent<Event>;
|
||||
label: string;
|
||||
icon: CSSIcon;
|
||||
enabled: boolean;
|
||||
style(styles: IButtonStyles): void;
|
||||
focus(): void;
|
||||
hasFocus(): boolean;
|
||||
}
|
||||
|
||||
export class Button extends Disposable implements IButton {
|
||||
|
||||
private _element: HTMLElement;
|
||||
private options: IButtonOptions;
|
||||
@@ -262,9 +276,9 @@ export class Button extends Disposable {
|
||||
}
|
||||
|
||||
// {{SQL CARBON EDIT}}
|
||||
set icon(iconClassName: string) {
|
||||
this.hasIcon = iconClassName !== '';
|
||||
this._element.classList.add(...iconClassName.split(' '));
|
||||
set icon(icon: CSSIcon) {
|
||||
this.hasIcon = icon !== undefined;
|
||||
this._element.classList.add(...icon.classNames.split(' '));
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
@@ -288,49 +302,130 @@ export class Button extends Disposable {
|
||||
focus(): void {
|
||||
this._element.focus();
|
||||
}
|
||||
|
||||
hasFocus(): boolean {
|
||||
return this._element === document.activeElement;
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtonGroup extends Disposable {
|
||||
private _buttons: Button[] = [];
|
||||
export interface IButtonWithDropdownOptions extends IButtonOptions {
|
||||
readonly contextMenuProvider: IContextMenuProvider;
|
||||
readonly actions: IAction[];
|
||||
readonly actionRunner?: IActionRunner;
|
||||
}
|
||||
|
||||
constructor(container: HTMLElement, count: number, options?: IButtonOptions) {
|
||||
export class ButtonWithDropdown extends Disposable implements IButton {
|
||||
|
||||
private readonly button: Button;
|
||||
private readonly dropdownButton: Button;
|
||||
|
||||
readonly element: HTMLElement;
|
||||
readonly onDidClick: BaseEvent<Event>;
|
||||
|
||||
constructor(container: HTMLElement, options: IButtonWithDropdownOptions) {
|
||||
super();
|
||||
|
||||
this.create(container, count, options);
|
||||
this.element = document.createElement('div');
|
||||
this.element.classList.add('monaco-button-dropdown');
|
||||
container.appendChild(this.element);
|
||||
|
||||
this.button = this._register(new Button(this.element, options));
|
||||
this.onDidClick = this.button.onDidClick;
|
||||
|
||||
this.dropdownButton = this._register(new Button(this.element, { ...options, title: false, supportCodicons: true }));
|
||||
this.dropdownButton.element.classList.add('monaco-dropdown-button');
|
||||
this.dropdownButton.icon = Codicon.dropDownButton;
|
||||
this._register(this.dropdownButton.onDidClick(() => {
|
||||
options.contextMenuProvider.showContextMenu({
|
||||
getAnchor: () => this.dropdownButton.element,
|
||||
getActions: () => options.actions,
|
||||
actionRunner: options.actionRunner,
|
||||
onHide: () => this.dropdownButton.element.setAttribute('aria-expanded', 'false')
|
||||
});
|
||||
this.dropdownButton.element.setAttribute('aria-expanded', 'true');
|
||||
}));
|
||||
}
|
||||
|
||||
get buttons(): Button[] {
|
||||
set label(value: string) {
|
||||
this.button.label = value;
|
||||
}
|
||||
|
||||
set icon(icon: CSSIcon) {
|
||||
this.button.icon = icon;
|
||||
}
|
||||
|
||||
set enabled(enabled: boolean) {
|
||||
this.button.enabled = enabled;
|
||||
this.dropdownButton.enabled = enabled;
|
||||
}
|
||||
|
||||
get enabled(): boolean {
|
||||
return this.button.enabled;
|
||||
}
|
||||
|
||||
style(styles: IButtonStyles): void {
|
||||
this.button.style(styles);
|
||||
this.dropdownButton.style(styles);
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this.button.focus();
|
||||
}
|
||||
|
||||
hasFocus(): boolean {
|
||||
return this.button.hasFocus() || this.dropdownButton.hasFocus();
|
||||
}
|
||||
}
|
||||
|
||||
export class ButtonBar extends Disposable {
|
||||
|
||||
private _buttons: IButton[] = [];
|
||||
|
||||
constructor(private readonly container: HTMLElement) {
|
||||
super();
|
||||
}
|
||||
|
||||
get buttons(): IButton[] {
|
||||
return this._buttons;
|
||||
}
|
||||
|
||||
private create(container: HTMLElement, count: number, options?: IButtonOptions): void {
|
||||
for (let index = 0; index < count; index++) {
|
||||
const button = this._register(new Button(container, options));
|
||||
this._buttons.push(button);
|
||||
|
||||
// Implement keyboard access in buttons if there are multiple
|
||||
if (count > 1) {
|
||||
this._register(addDisposableListener(button.element, EventType.KEY_DOWN, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
let eventHandled = true;
|
||||
|
||||
// Next / Previous Button
|
||||
let buttonIndexToFocus: number | undefined;
|
||||
if (event.equals(KeyCode.LeftArrow)) {
|
||||
buttonIndexToFocus = index > 0 ? index - 1 : this._buttons.length - 1;
|
||||
} else if (event.equals(KeyCode.RightArrow)) {
|
||||
buttonIndexToFocus = index === this._buttons.length - 1 ? 0 : index + 1;
|
||||
} else {
|
||||
eventHandled = false;
|
||||
}
|
||||
|
||||
if (eventHandled && typeof buttonIndexToFocus === 'number') {
|
||||
this._buttons[buttonIndexToFocus].focus();
|
||||
EventHelper.stop(e, true);
|
||||
}
|
||||
|
||||
}));
|
||||
}
|
||||
}
|
||||
addButton(options?: IButtonOptions): IButton {
|
||||
const button = this._register(new Button(this.container, options));
|
||||
this.pushButton(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
addButtonWithDropdown(options: IButtonWithDropdownOptions): IButton {
|
||||
const button = this._register(new ButtonWithDropdown(this.container, options));
|
||||
this.pushButton(button);
|
||||
return button;
|
||||
}
|
||||
|
||||
private pushButton(button: IButton): void {
|
||||
this._buttons.push(button);
|
||||
|
||||
const index = this._buttons.length - 1;
|
||||
this._register(addDisposableListener(button.element, EventType.KEY_DOWN, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
let eventHandled = true;
|
||||
|
||||
// Next / Previous Button
|
||||
let buttonIndexToFocus: number | undefined;
|
||||
if (event.equals(KeyCode.LeftArrow)) {
|
||||
buttonIndexToFocus = index > 0 ? index - 1 : this._buttons.length - 1;
|
||||
} else if (event.equals(KeyCode.RightArrow)) {
|
||||
buttonIndexToFocus = index === this._buttons.length - 1 ? 0 : index + 1;
|
||||
} else {
|
||||
eventHandled = false;
|
||||
}
|
||||
|
||||
if (eventHandled && typeof buttonIndexToFocus === 'number') {
|
||||
this._buttons[buttonIndexToFocus].focus();
|
||||
EventHelper.stop(e, true);
|
||||
}
|
||||
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -45,6 +45,6 @@
|
||||
}
|
||||
|
||||
/* hide check when unchecked */
|
||||
.monaco-custom-checkbox.monaco-simple-checkbox.unchecked:not(.checked)::before {
|
||||
visibility: hidden;;
|
||||
.monaco-custom-checkbox.monaco-simple-checkbox:not(.checked)::before {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@@ -11,12 +11,12 @@ import { Color } from 'vs/base/common/color';
|
||||
import { Emitter, Event } from 'vs/base/common/event';
|
||||
import { KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { Codicon, CSSIcon } from 'vs/base/common/codicons';
|
||||
import { BaseActionViewItem } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
|
||||
export interface ICheckboxOpts extends ICheckboxStyles {
|
||||
readonly actionClassName?: string;
|
||||
readonly icon?: Codicon;
|
||||
readonly icon?: CSSIcon;
|
||||
readonly title: string;
|
||||
readonly isChecked: boolean;
|
||||
}
|
||||
@@ -108,7 +108,9 @@ export class Checkbox extends Widget {
|
||||
if (this._opts.actionClassName) {
|
||||
classes.push(this._opts.actionClassName);
|
||||
}
|
||||
classes.push(this._checked ? 'checked' : 'unchecked');
|
||||
if (this._checked) {
|
||||
classes.push('checked');
|
||||
}
|
||||
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.title = this._opts.title;
|
||||
@@ -154,12 +156,9 @@ export class Checkbox extends Widget {
|
||||
|
||||
set checked(newIsChecked: boolean) {
|
||||
this._checked = newIsChecked;
|
||||
|
||||
this.domNode.setAttribute('aria-checked', String(this._checked));
|
||||
if (this._checked) {
|
||||
this.domNode.classList.add('checked');
|
||||
} else {
|
||||
this.domNode.classList.remove('checked');
|
||||
}
|
||||
this.domNode.classList.toggle('checked', this._checked);
|
||||
|
||||
this.applyStyles();
|
||||
}
|
||||
@@ -230,6 +229,14 @@ export class SimpleCheckbox extends Widget {
|
||||
this.applyStyles();
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this.domNode.focus();
|
||||
}
|
||||
|
||||
hasFocus(): boolean {
|
||||
return this.domNode === document.activeElement;
|
||||
}
|
||||
|
||||
style(styles: ISimpleCheckboxStyles): void {
|
||||
this.styles = styles;
|
||||
|
||||
|
||||
Binary file not shown.
@@ -7,26 +7,7 @@ import 'vs/css!./codicon/codicon';
|
||||
import 'vs/css!./codicon/codicon-modifications';
|
||||
import 'vs/css!./codicon/codicon-animations';
|
||||
|
||||
import { Codicon, iconRegistry } from 'vs/base/common/codicons';
|
||||
import { createStyleSheet } from 'vs/base/browser/dom';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
|
||||
function initialize() {
|
||||
let codiconStyleSheet = createStyleSheet();
|
||||
codiconStyleSheet.id = 'codiconStyles';
|
||||
|
||||
function updateAll() {
|
||||
const rules = [];
|
||||
for (let c of iconRegistry.all) {
|
||||
rules.push(formatRule(c));
|
||||
}
|
||||
codiconStyleSheet.textContent = rules.join('\n');
|
||||
}
|
||||
|
||||
const delayer = new RunOnceScheduler(updateAll, 0);
|
||||
iconRegistry.onDidRegister(() => delayer.schedule());
|
||||
delayer.schedule();
|
||||
}
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
|
||||
export function formatRule(c: Codicon) {
|
||||
let def = c.definition;
|
||||
@@ -35,5 +16,3 @@ export function formatRule(c: Codicon) {
|
||||
}
|
||||
return `.codicon-${c.id}:before { content: '${def.character}'; }`;
|
||||
}
|
||||
|
||||
initialize();
|
||||
|
||||
@@ -31,6 +31,10 @@ export const enum AnchorPosition {
|
||||
BELOW, ABOVE
|
||||
}
|
||||
|
||||
export const enum AnchorAxisAlignment {
|
||||
VERTICAL, HORIZONTAL
|
||||
}
|
||||
|
||||
export interface IDelegate {
|
||||
getAnchor(): HTMLElement | IAnchor;
|
||||
render(container: HTMLElement): IDisposable | null;
|
||||
@@ -38,6 +42,7 @@ export interface IDelegate {
|
||||
layout?(): void;
|
||||
anchorAlignment?: AnchorAlignment; // default: left
|
||||
anchorPosition?: AnchorPosition; // default: below
|
||||
anchorAxisAlignment?: AnchorAxisAlignment; // default: vertical
|
||||
canRelayout?: boolean; // default: true
|
||||
onDOMEvent?(e: Event, activeElement: HTMLElement): void;
|
||||
onHide?(data?: any): void;
|
||||
@@ -66,9 +71,15 @@ export const enum LayoutAnchorPosition {
|
||||
After
|
||||
}
|
||||
|
||||
export enum LayoutAnchorMode {
|
||||
AVOID,
|
||||
ALIGN
|
||||
}
|
||||
|
||||
export interface ILayoutAnchor {
|
||||
offset: number;
|
||||
size: number;
|
||||
mode?: LayoutAnchorMode; // default: AVOID
|
||||
position: LayoutAnchorPosition;
|
||||
}
|
||||
|
||||
@@ -78,25 +89,26 @@ export interface ILayoutAnchor {
|
||||
* @returns The view offset within the viewport.
|
||||
*/
|
||||
export function layout(viewportSize: number, viewSize: number, anchor: ILayoutAnchor): number {
|
||||
const anchorEnd = anchor.offset + anchor.size;
|
||||
const layoutAfterAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset : anchor.offset + anchor.size;
|
||||
const layoutBeforeAnchorBoundary = anchor.mode === LayoutAnchorMode.ALIGN ? anchor.offset + anchor.size : anchor.offset;
|
||||
|
||||
if (anchor.position === LayoutAnchorPosition.Before) {
|
||||
if (viewSize <= viewportSize - anchorEnd) {
|
||||
return anchorEnd; // happy case, lay it out after the anchor
|
||||
if (viewSize <= viewportSize - layoutAfterAnchorBoundary) {
|
||||
return layoutAfterAnchorBoundary; // happy case, lay it out after the anchor
|
||||
}
|
||||
|
||||
if (viewSize <= anchor.offset) {
|
||||
return anchor.offset - viewSize; // ok case, lay it out before the anchor
|
||||
if (viewSize <= layoutBeforeAnchorBoundary) {
|
||||
return layoutBeforeAnchorBoundary - viewSize; // ok case, lay it out before the anchor
|
||||
}
|
||||
|
||||
return Math.max(viewportSize - viewSize, 0); // sad case, lay it over the anchor
|
||||
} else {
|
||||
if (viewSize <= anchor.offset) {
|
||||
return anchor.offset - viewSize; // happy case, lay it out before the anchor
|
||||
if (viewSize <= layoutBeforeAnchorBoundary) {
|
||||
return layoutBeforeAnchorBoundary - viewSize; // happy case, lay it out before the anchor
|
||||
}
|
||||
|
||||
if (viewSize <= viewportSize - anchorEnd) {
|
||||
return anchorEnd; // ok case, lay it out after the anchor
|
||||
if (viewSize <= viewportSize - layoutAfterAnchorBoundary) {
|
||||
return layoutAfterAnchorBoundary; // ok case, lay it out after the anchor
|
||||
}
|
||||
|
||||
return 0; // sad case, lay it over the anchor
|
||||
@@ -157,11 +169,9 @@ export class ContextView extends Disposable {
|
||||
this.shadowRootHostElement = DOM.$('.shadow-root-host');
|
||||
this.container.appendChild(this.shadowRootHostElement);
|
||||
this.shadowRoot = this.shadowRootHostElement.attachShadow({ mode: 'open' });
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
${SHADOW_ROOT_CSS}
|
||||
</style>
|
||||
`;
|
||||
const style = document.createElement('style');
|
||||
style.textContent = SHADOW_ROOT_CSS;
|
||||
this.shadowRoot.appendChild(style);
|
||||
this.shadowRoot.appendChild(this.view);
|
||||
this.shadowRoot.appendChild(DOM.$('slot'));
|
||||
} else {
|
||||
@@ -272,33 +282,41 @@ export class ContextView extends Disposable {
|
||||
|
||||
const anchorPosition = this.delegate!.anchorPosition || AnchorPosition.BELOW;
|
||||
const anchorAlignment = this.delegate!.anchorAlignment || AnchorAlignment.LEFT;
|
||||
const anchorAxisAlignment = this.delegate!.anchorAxisAlignment || AnchorAxisAlignment.VERTICAL;
|
||||
|
||||
const verticalAnchor: ILayoutAnchor = { offset: around.top - window.pageYOffset, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
|
||||
let top: number;
|
||||
let left: number;
|
||||
|
||||
let horizontalAnchor: ILayoutAnchor;
|
||||
if (anchorAxisAlignment === AnchorAxisAlignment.VERTICAL) {
|
||||
const verticalAnchor: ILayoutAnchor = { offset: around.top - window.pageYOffset, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
|
||||
const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
|
||||
|
||||
if (anchorAlignment === AnchorAlignment.LEFT) {
|
||||
horizontalAnchor = { offset: around.left, size: 0, position: LayoutAnchorPosition.Before };
|
||||
} else {
|
||||
horizontalAnchor = { offset: around.left + around.width, size: 0, position: LayoutAnchorPosition.After };
|
||||
}
|
||||
top = layout(window.innerHeight, viewSizeHeight, verticalAnchor) + window.pageYOffset;
|
||||
|
||||
const top = layout(window.innerHeight, viewSizeHeight, verticalAnchor) + window.pageYOffset;
|
||||
|
||||
// if view intersects vertically with anchor, shift it horizontally
|
||||
if (Range.intersects({ start: top, end: top + viewSizeHeight }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) {
|
||||
horizontalAnchor.size = around.width;
|
||||
if (anchorAlignment === AnchorAlignment.RIGHT) {
|
||||
horizontalAnchor.offset = around.left;
|
||||
// if view intersects vertically with anchor, we must avoid the anchor
|
||||
if (Range.intersects({ start: top, end: top + viewSizeHeight }, { start: verticalAnchor.offset, end: verticalAnchor.offset + verticalAnchor.size })) {
|
||||
horizontalAnchor.mode = LayoutAnchorMode.AVOID;
|
||||
}
|
||||
|
||||
left = layout(window.innerWidth, viewSizeWidth, horizontalAnchor);
|
||||
} else {
|
||||
const horizontalAnchor: ILayoutAnchor = { offset: around.left, size: around.width, position: anchorAlignment === AnchorAlignment.LEFT ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After };
|
||||
const verticalAnchor: ILayoutAnchor = { offset: around.top, size: around.height, position: anchorPosition === AnchorPosition.BELOW ? LayoutAnchorPosition.Before : LayoutAnchorPosition.After, mode: LayoutAnchorMode.ALIGN };
|
||||
|
||||
left = layout(window.innerWidth, viewSizeWidth, horizontalAnchor);
|
||||
|
||||
// if view intersects horizontally with anchor, we must avoid the anchor
|
||||
if (Range.intersects({ start: left, end: left + viewSizeWidth }, { start: horizontalAnchor.offset, end: horizontalAnchor.offset + horizontalAnchor.size })) {
|
||||
verticalAnchor.mode = LayoutAnchorMode.AVOID;
|
||||
}
|
||||
|
||||
top = layout(window.innerHeight, viewSizeHeight, verticalAnchor) + window.pageYOffset;
|
||||
}
|
||||
|
||||
const left = layout(window.innerWidth, viewSizeWidth, horizontalAnchor);
|
||||
|
||||
DOM.removeClasses(this.view, 'top', 'bottom', 'left', 'right');
|
||||
DOM.addClass(this.view, anchorPosition === AnchorPosition.BELOW ? 'bottom' : 'top');
|
||||
DOM.addClass(this.view, anchorAlignment === AnchorAlignment.LEFT ? 'left' : 'right');
|
||||
DOM.toggleClass(this.view, 'fixed', this.useFixedPosition);
|
||||
this.view.classList.remove('top', 'bottom', 'left', 'right');
|
||||
this.view.classList.add(anchorPosition === AnchorPosition.BELOW ? 'bottom' : 'top');
|
||||
this.view.classList.add(anchorAlignment === AnchorAlignment.LEFT ? 'left' : 'right');
|
||||
this.view.classList.toggle('fixed', this.useFixedPosition);
|
||||
|
||||
const containerPosition = DOM.getDomNodePagePosition(this.container!);
|
||||
this.view.style.top = `${top - (this.useFixedPosition ? DOM.getDomNodePagePosition(this.view).top : containerPosition.top)}px`;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. 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;
|
||||
@@ -137,7 +148,8 @@
|
||||
width: fit-content;
|
||||
width: -moz-fit-content;
|
||||
padding: 5px 10px;
|
||||
margin: 4px 5px; /* allows button focus outline to be visible */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin: 4px 5px; /* allows button focus outline to be visible */
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
|
||||
@@ -6,78 +6,90 @@
|
||||
import 'vs/css!./dialog';
|
||||
import * as nls from 'vs/nls';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { $, hide, show, EventHelper, clearNode, removeClasses, addClasses, removeNode, isAncestor, addDisposableListener, EventType } from 'vs/base/browser/dom';
|
||||
import { $, hide, show, EventHelper, clearNode, isAncestor, addDisposableListener, EventType } from 'vs/base/browser/dom';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { KeyCode, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { ButtonGroup, IButtonStyles } from 'vs/base/browser/ui/button/button';
|
||||
import { ButtonBar, IButtonStyles } from 'vs/base/browser/ui/button/button';
|
||||
import { ActionBar } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { Action } from 'vs/base/common/actions';
|
||||
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 { Codicon, registerCodicon } 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);
|
||||
const dialogWarningIcon = registerIcon('dialog-warning', Codicon.warning);
|
||||
const dialogInfoIcon = registerIcon('dialog-info', Codicon.info);
|
||||
const dialogCloseIcon = registerIcon('dialog-close', Codicon.close);
|
||||
const dialogErrorIcon = registerCodicon('dialog-error', Codicon.error);
|
||||
const dialogWarningIcon = registerCodicon('dialog-warning', Codicon.warning);
|
||||
const dialogInfoIcon = registerCodicon('dialog-info', Codicon.info);
|
||||
const dialogCloseIcon = registerCodicon('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 buttonGroup: ButtonGroup | 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 buttonBar: ButtonBar | 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,113 @@ 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 buttonBar = this.buttonBar = this._register(new ButtonBar(this.buttonsContainer));
|
||||
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;
|
||||
}
|
||||
});
|
||||
|
||||
buttonGroup.buttons.forEach((button, index) => {
|
||||
// Handle button clicks
|
||||
buttonMap.forEach((entry, index) => {
|
||||
const button = this._register(buttonBar.addButton({ title: true }));
|
||||
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;
|
||||
}
|
||||
this.checkboxHasFocus = true;
|
||||
} else {
|
||||
focusedButton = this.checkboxHasFocus ? 0 : focusedButton + 1;
|
||||
focusedButton = focusedButton % buttonGroup.buttons.length;
|
||||
buttonGroup.buttons[focusedButton].focus();
|
||||
this.checkboxHasFocus = false;
|
||||
}
|
||||
|
||||
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(KeyCode.Tab) || evt.equals(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;
|
||||
}
|
||||
|
||||
@@ -217,10 +286,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)) {
|
||||
@@ -234,32 +307,34 @@ export class Dialog extends Disposable {
|
||||
}
|
||||
}));
|
||||
|
||||
removeClasses(this.iconElement, dialogErrorIcon.classNames, dialogWarningIcon.classNames, dialogInfoIcon.classNames, Codicon.loading.classNames);
|
||||
this.iconElement.classList.remove(...dialogErrorIcon.classNamesArray, ...dialogWarningIcon.classNamesArray, ...dialogInfoIcon.classNamesArray, ...Codicon.loading.classNamesArray);
|
||||
|
||||
switch (this.options.type) {
|
||||
case 'error':
|
||||
addClasses(this.iconElement, dialogErrorIcon.classNames);
|
||||
this.iconElement.classList.add(...dialogErrorIcon.classNamesArray);
|
||||
break;
|
||||
case 'warning':
|
||||
addClasses(this.iconElement, dialogWarningIcon.classNames);
|
||||
this.iconElement.classList.add(...dialogWarningIcon.classNamesArray);
|
||||
break;
|
||||
case 'pending':
|
||||
addClasses(this.iconElement, Codicon.loading.classNames, 'codicon-animation-spin');
|
||||
this.iconElement.classList.add(...Codicon.loading.classNamesArray, 'codicon-animation-spin');
|
||||
break;
|
||||
case 'none':
|
||||
case 'info':
|
||||
case 'question':
|
||||
default:
|
||||
addClasses(this.iconElement, dialogInfoIcon.classNames);
|
||||
this.iconElement.classList.add(...dialogInfoIcon.classNamesArray);
|
||||
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 +343,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) {
|
||||
buttonBar.buttons[index].focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -277,65 +361,64 @@ export class Dialog extends Disposable {
|
||||
if (this.styles) {
|
||||
const style = this.styles;
|
||||
|
||||
const fgColor = style.dialogForeground ? `${style.dialogForeground}` : '';
|
||||
const bgColor = style.dialogBackground ? `${style.dialogBackground}` : '';
|
||||
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}` : '';
|
||||
|
||||
if (this.shadowElement) {
|
||||
this.shadowElement.style.boxShadow = shadowColor;
|
||||
this.shadowElement.style.boxShadow = shadowColor;
|
||||
|
||||
this.element.style.color = fgColor?.toString() ?? '';
|
||||
this.element.style.backgroundColor = bgColor?.toString() ?? '';
|
||||
this.element.style.border = border;
|
||||
|
||||
if (this.buttonBar) {
|
||||
this.buttonBar.buttons.forEach(button => button.style(style));
|
||||
}
|
||||
|
||||
if (this.element) {
|
||||
this.element.style.color = fgColor;
|
||||
this.element.style.backgroundColor = bgColor;
|
||||
this.element.style.border = border;
|
||||
|
||||
if (this.buttonGroup) {
|
||||
this.buttonGroup.buttons.forEach(button => button.style(style));
|
||||
}
|
||||
|
||||
if (this.checkbox) {
|
||||
this.checkbox.style(style);
|
||||
}
|
||||
|
||||
if (this.messageDetailElement && fgColor && bgColor) {
|
||||
const messageDetailColor = Color.fromHex(fgColor).transparent(.9);
|
||||
this.messageDetailElement.style.color = messageDetailColor.makeOpaque(Color.fromHex(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();
|
||||
}
|
||||
}
|
||||
if (this.checkbox) {
|
||||
this.checkbox.style(style);
|
||||
}
|
||||
|
||||
if (fgColor && bgColor) {
|
||||
const messageDetailColor = fgColor.transparent(.9);
|
||||
this.messageDetailElement.style.color = messageDetailColor.makeOpaque(bgColor).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) {
|
||||
removeNode(this.modal);
|
||||
this.modal = undefined;
|
||||
|
||||
if (this.modalElement) {
|
||||
this.modalElement.remove();
|
||||
this.modalElement = undefined;
|
||||
}
|
||||
|
||||
if (this.focusToReturn && isAncestor(this.focusToReturn, document.body)) {
|
||||
@@ -346,9 +429,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
|
||||
|
||||
@@ -12,3 +12,7 @@
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-dropdown > .dropdown-label > .action-label.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -4,15 +4,16 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import 'vs/css!./dropdown';
|
||||
import { IAction, IActionRunner, IActionViewItemProvider } from 'vs/base/common/actions';
|
||||
import { Action, IAction, IActionRunner, IActionViewItemProvider } from 'vs/base/common/actions';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { append, $ } from 'vs/base/browser/dom';
|
||||
import { Emitter } from 'vs/base/common/event';
|
||||
import { BaseActionViewItem, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
import { ActionViewItem, BaseActionViewItem, IActionViewItemOptions, IBaseActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
import { IActionProvider, DropdownMenu, IDropdownMenuOptions, ILabelRenderer } from 'vs/base/browser/ui/dropdown/dropdown';
|
||||
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
|
||||
export interface IKeybindingProvider {
|
||||
(action: IAction): ResolvedKeybinding | undefined;
|
||||
@@ -35,6 +36,7 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem {
|
||||
private menuActionsOrProvider: readonly IAction[] | IActionProvider;
|
||||
private dropdownMenu: DropdownMenu | undefined;
|
||||
private contextMenuProvider: IContextMenuProvider;
|
||||
private actionItem: HTMLElement | null = null;
|
||||
|
||||
private _onDidChangeVisibility = this._register(new Emitter<boolean>());
|
||||
readonly onDidChangeVisibility = this._onDidChangeVisibility.event;
|
||||
@@ -56,6 +58,8 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem {
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
this.actionItem = container;
|
||||
|
||||
const labelRenderer: ILabelRenderer = (el: HTMLElement): IDisposable | null => {
|
||||
this.element = append(el, $('a.action-label'));
|
||||
|
||||
@@ -115,6 +119,8 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
this.updateEnabled();
|
||||
}
|
||||
|
||||
setActionContext(newContext: unknown): void {
|
||||
@@ -134,4 +140,39 @@ export class DropdownMenuActionViewItem extends BaseActionViewItem {
|
||||
this.dropdownMenu.show();
|
||||
}
|
||||
}
|
||||
|
||||
protected updateEnabled(): void {
|
||||
const disabled = !this.getAction().enabled;
|
||||
this.actionItem?.classList.toggle('disabled', disabled);
|
||||
this.element?.classList.toggle('disabled', disabled);
|
||||
}
|
||||
}
|
||||
|
||||
export interface IActionWithDropdownActionViewItemOptions extends IActionViewItemOptions {
|
||||
readonly menuActionsOrProvider: readonly IAction[] | IActionProvider;
|
||||
readonly menuActionClassNames?: string[];
|
||||
}
|
||||
|
||||
export class ActionWithDropdownActionViewItem extends ActionViewItem {
|
||||
|
||||
protected dropdownMenuActionViewItem: DropdownMenuActionViewItem | undefined;
|
||||
|
||||
constructor(
|
||||
context: unknown,
|
||||
action: IAction,
|
||||
options: IActionWithDropdownActionViewItemOptions,
|
||||
private readonly contextMenuProvider: IContextMenuProvider
|
||||
) {
|
||||
super(context, action, options);
|
||||
}
|
||||
|
||||
render(container: HTMLElement): void {
|
||||
super.render(container);
|
||||
if (this.element) {
|
||||
this.element.classList.add('action-dropdown-item');
|
||||
this.dropdownMenuActionViewItem = new DropdownMenuActionViewItem(new Action('dropdownAction', undefined), (<IActionWithDropdownActionViewItemOptions>this.options).menuActionsOrProvider, this.contextMenuProvider, { classNames: ['dropdown', ...Codicon.dropDownButton.classNamesArray, ...(<IActionWithDropdownActionViewItemOptions>this.options).menuActionClassNames || []] });
|
||||
this.dropdownMenuActionViewItem.render(this.element);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -228,6 +228,7 @@ export class FindInput extends Widget {
|
||||
|
||||
if (event.equals(KeyCode.Escape)) {
|
||||
indexes[index].blur();
|
||||
this.inputBox.focus();
|
||||
} else if (newIndex >= 0) {
|
||||
indexes[newIndex].focus();
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface IReplaceInputOptions extends IReplaceInputStyles {
|
||||
readonly flexibleWidth?: boolean;
|
||||
readonly flexibleMaxHeight?: number;
|
||||
|
||||
readonly appendPreserveCaseLabel?: string;
|
||||
readonly history?: string[];
|
||||
}
|
||||
|
||||
@@ -128,6 +129,7 @@ export class ReplaceInput extends Widget {
|
||||
this.inputValidationErrorBackground = options.inputValidationErrorBackground;
|
||||
this.inputValidationErrorForeground = options.inputValidationErrorForeground;
|
||||
|
||||
const appendPreserveCaseLabel = options.appendPreserveCaseLabel || '';
|
||||
const history = options.history || [];
|
||||
const flexibleHeight = !!options.flexibleHeight;
|
||||
const flexibleWidth = !!options.flexibleWidth;
|
||||
@@ -161,7 +163,7 @@ export class ReplaceInput extends Widget {
|
||||
}));
|
||||
|
||||
this.preserveCase = this._register(new PreserveCaseCheckbox({
|
||||
appendTitle: '',
|
||||
appendTitle: appendPreserveCaseLabel,
|
||||
isChecked: false,
|
||||
inputActiveOptionBorder: this.inputActiveOptionBorder,
|
||||
inputActiveOptionForeground: this.inputActiveOptionForeground,
|
||||
@@ -203,6 +205,7 @@ export class ReplaceInput extends Widget {
|
||||
|
||||
if (event.equals(KeyCode.Escape)) {
|
||||
indexes[index].blur();
|
||||
this.inputBox.focus();
|
||||
} else if (newIndex >= 0) {
|
||||
indexes[newIndex].focus();
|
||||
}
|
||||
|
||||
@@ -175,7 +175,7 @@ function getGridLocation(element: HTMLElement): number[] {
|
||||
}
|
||||
|
||||
const index = indexInParent(parentElement);
|
||||
const ancestor = parentElement.parentElement!.parentElement!.parentElement!;
|
||||
const ancestor = parentElement.parentElement!.parentElement!.parentElement!.parentElement!;
|
||||
return [...getGridLocation(ancestor), index];
|
||||
}
|
||||
|
||||
@@ -215,6 +215,8 @@ export class Grid<T extends IView = IView> extends Disposable {
|
||||
get boundarySashes(): IBoundarySashes { return this.gridview.boundarySashes; }
|
||||
set boundarySashes(boundarySashes: IBoundarySashes) { this.gridview.boundarySashes = boundarySashes; }
|
||||
|
||||
set edgeSnapping(edgeSnapping: boolean) { this.gridview.edgeSnapping = edgeSnapping; }
|
||||
|
||||
get element(): HTMLElement { return this.gridview.element; }
|
||||
|
||||
private didLayout = false;
|
||||
|
||||
@@ -170,6 +170,7 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
|
||||
|
||||
private absoluteOffset: number = 0;
|
||||
private absoluteOrthogonalOffset: number = 0;
|
||||
private absoluteOrthogonalSize: number = 0;
|
||||
|
||||
private _styles: IGridViewStyles;
|
||||
get styles(): IGridViewStyles { return this._styles; }
|
||||
@@ -270,6 +271,24 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
|
||||
}
|
||||
}
|
||||
|
||||
private _edgeSnapping = false;
|
||||
get edgeSnapping(): boolean { return this._edgeSnapping; }
|
||||
set edgeSnapping(edgeSnapping: boolean) {
|
||||
if (this._edgeSnapping === edgeSnapping) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._edgeSnapping = edgeSnapping;
|
||||
|
||||
for (const child of this.children) {
|
||||
if (child instanceof BranchNode) {
|
||||
child.edgeSnapping = edgeSnapping;
|
||||
}
|
||||
}
|
||||
|
||||
this.updateSplitviewEdgeSnappingEnablement();
|
||||
}
|
||||
|
||||
constructor(
|
||||
readonly orientation: Orientation,
|
||||
readonly layoutController: ILayoutController,
|
||||
@@ -277,6 +296,7 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
|
||||
readonly proportionalLayout: boolean,
|
||||
size: number = 0,
|
||||
orthogonalSize: number = 0,
|
||||
edgeSnapping: boolean = false,
|
||||
childDescriptors?: INodeDescriptor[]
|
||||
) {
|
||||
this._styles = styles;
|
||||
@@ -355,6 +375,7 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
|
||||
this._orthogonalSize = size;
|
||||
this.absoluteOffset = ctx.absoluteOffset + offset;
|
||||
this.absoluteOrthogonalOffset = ctx.absoluteOrthogonalOffset;
|
||||
this.absoluteOrthogonalSize = ctx.absoluteOrthogonalSize;
|
||||
|
||||
this.splitview.layout(ctx.orthogonalSize, {
|
||||
orthogonalSize: size,
|
||||
@@ -364,9 +385,7 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
|
||||
absoluteOrthogonalSize: ctx.absoluteSize
|
||||
});
|
||||
|
||||
// Disable snapping on views which sit on the edges of the grid
|
||||
this.splitview.startSnappingEnabled = this.absoluteOrthogonalOffset > 0;
|
||||
this.splitview.endSnappingEnabled = this.absoluteOrthogonalOffset + ctx.orthogonalSize < ctx.absoluteOrthogonalSize;
|
||||
this.updateSplitviewEdgeSnappingEnablement();
|
||||
}
|
||||
|
||||
setVisible(visible: boolean): void {
|
||||
@@ -607,6 +626,11 @@ class BranchNode implements ISplitView<ILayoutContext>, IDisposable {
|
||||
});
|
||||
}
|
||||
|
||||
private updateSplitviewEdgeSnappingEnablement(): void {
|
||||
this.splitview.startSnappingEnabled = this._edgeSnapping || this.absoluteOrthogonalOffset > 0;
|
||||
this.splitview.endSnappingEnabled = this._edgeSnapping || this.absoluteOrthogonalOffset + this._size < this.absoluteOrthogonalSize;
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
for (const child of this.children) {
|
||||
child.dispose();
|
||||
@@ -775,7 +799,7 @@ export interface INodeDescriptor {
|
||||
|
||||
function flipNode<T extends Node>(node: T, size: number, orthogonalSize: number): T {
|
||||
if (node instanceof BranchNode) {
|
||||
const result = new BranchNode(orthogonal(node.orientation), node.layoutController, node.styles, node.proportionalLayout, size, orthogonalSize);
|
||||
const result = new BranchNode(orthogonal(node.orientation), node.layoutController, node.styles, node.proportionalLayout, size, orthogonalSize, node.edgeSnapping);
|
||||
|
||||
let totalSize = 0;
|
||||
|
||||
@@ -863,6 +887,10 @@ export class GridView implements IDisposable {
|
||||
this.root.boundarySashes = fromAbsoluteBoundarySashes(boundarySashes, this.orientation);
|
||||
}
|
||||
|
||||
set edgeSnapping(edgeSnapping: boolean) {
|
||||
this.root.edgeSnapping = edgeSnapping;
|
||||
}
|
||||
|
||||
/**
|
||||
* The first layout controller makes sure layout only propagates
|
||||
* to the views after the very first call to gridview.layout()
|
||||
@@ -932,7 +960,7 @@ export class GridView implements IDisposable {
|
||||
|
||||
grandParent.removeChild(parentIndex);
|
||||
|
||||
const newParent = new BranchNode(parent.orientation, parent.layoutController, this.styles, this.proportionalLayout, parent.size, parent.orthogonalSize);
|
||||
const newParent = new BranchNode(parent.orientation, parent.layoutController, this.styles, this.proportionalLayout, parent.size, parent.orthogonalSize, grandParent.edgeSnapping);
|
||||
grandParent.addChild(newParent, parent.size, parentIndex);
|
||||
|
||||
const newSibling = new LeafNode(parent.view, grandParent.orientation, this.layoutController, parent.size);
|
||||
@@ -1205,7 +1233,7 @@ export class GridView implements IDisposable {
|
||||
} as INodeDescriptor;
|
||||
});
|
||||
|
||||
result = new BranchNode(orientation, this.layoutController, this.styles, this.proportionalLayout, node.size, orthogonalSize, children);
|
||||
result = new BranchNode(orientation, this.layoutController, this.styles, this.proportionalLayout, node.size, orthogonalSize, undefined, children);
|
||||
} else {
|
||||
result = new LeafNode(deserializer.fromJSON(node.data), orientation, this.layoutController, orthogonalSize, node.size);
|
||||
}
|
||||
|
||||
@@ -87,7 +87,6 @@
|
||||
|
||||
.monaco-hover .monaco-tokenized-source {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-row.status-bar {
|
||||
@@ -131,3 +130,9 @@
|
||||
border-bottom: 1px solid transparent;
|
||||
text-underline-position: under;
|
||||
}
|
||||
|
||||
/** Spans in markdown hovers need a margin-bottom to avoid looking cramped: https://github.com/microsoft/vscode/issues/101496 **/
|
||||
.monaco-hover .markdown-hover .hover-contents:not(.code-hover-contents) span {
|
||||
margin-bottom: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
23
src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts
Normal file
23
src/vs/base/browser/ui/iconLabel/iconHoverDelegate.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export interface IHoverDelegateTarget extends IDisposable {
|
||||
readonly targetElements: readonly HTMLElement[];
|
||||
x?: number;
|
||||
}
|
||||
|
||||
export interface IHoverDelegateOptions {
|
||||
text: IMarkdownString | string;
|
||||
target: IHoverDelegateTarget | HTMLElement;
|
||||
anchorPosition?: AnchorPosition;
|
||||
}
|
||||
|
||||
export interface IHoverDelegate {
|
||||
showHover(options: IHoverDelegateOptions): IDisposable | undefined;
|
||||
}
|
||||
@@ -7,18 +7,30 @@ import 'vs/css!./iconlabel';
|
||||
import * as dom from 'vs/base/browser/dom';
|
||||
import { HighlightedLabel } from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
|
||||
import { IMatch } from 'vs/base/common/filters';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { Range } from 'vs/base/common/range';
|
||||
import { equals } from 'vs/base/common/objects';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { IHoverDelegate, IHoverDelegateOptions, IHoverDelegateTarget } from 'vs/base/browser/ui/iconLabel/iconHoverDelegate';
|
||||
import { AnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { IMarkdownString } from 'vs/base/common/htmlContent';
|
||||
import { isString } from 'vs/base/common/types';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
|
||||
export interface IIconLabelCreationOptions {
|
||||
supportHighlights?: boolean;
|
||||
supportDescriptionHighlights?: boolean;
|
||||
supportCodicons?: boolean;
|
||||
hoverDelegate?: IHoverDelegate;
|
||||
}
|
||||
|
||||
export interface IIconLabelMarkdownString {
|
||||
markdown: IMarkdownString | string | undefined | Promise<IMarkdownString | string | undefined>;
|
||||
markdownNotSupportedFallback: string | undefined;
|
||||
}
|
||||
|
||||
export interface IIconLabelValueOptions {
|
||||
title?: string;
|
||||
title?: string | IIconLabelMarkdownString;
|
||||
descriptionTitle?: string;
|
||||
hideIcon?: boolean;
|
||||
extraClasses?: string[];
|
||||
@@ -35,7 +47,6 @@ class FastLabelNode {
|
||||
private disposed: boolean | undefined;
|
||||
private _textContent: string | undefined;
|
||||
private _className: string | undefined;
|
||||
private _title: string | undefined;
|
||||
private _empty: boolean | undefined;
|
||||
|
||||
constructor(private _element: HTMLElement) {
|
||||
@@ -63,19 +74,6 @@ class FastLabelNode {
|
||||
this._element.className = className;
|
||||
}
|
||||
|
||||
set title(title: string) {
|
||||
if (this.disposed || title === this._title) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._title = title;
|
||||
if (this._title) {
|
||||
this._element.title = title;
|
||||
} else {
|
||||
this._element.removeAttribute('title');
|
||||
}
|
||||
}
|
||||
|
||||
set empty(empty: boolean) {
|
||||
if (this.disposed || empty === this._empty) {
|
||||
return;
|
||||
@@ -100,15 +98,20 @@ export class IconLabel extends Disposable {
|
||||
private descriptionNode: FastLabelNode | HighlightedLabel | undefined;
|
||||
private descriptionNodeFactory: () => FastLabelNode | HighlightedLabel;
|
||||
|
||||
private labelContainer: HTMLElement;
|
||||
|
||||
private hoverDelegate: IHoverDelegate | undefined = undefined;
|
||||
private readonly customHovers: Map<HTMLElement, IDisposable> = new Map();
|
||||
|
||||
constructor(container: HTMLElement, options?: IIconLabelCreationOptions) {
|
||||
super();
|
||||
|
||||
this.domNode = this._register(new FastLabelNode(dom.append(container, dom.$('.monaco-icon-label'))));
|
||||
|
||||
const labelContainer = dom.append(this.domNode.element, dom.$('.monaco-icon-label-container'));
|
||||
this.labelContainer = dom.append(this.domNode.element, dom.$('.monaco-icon-label-container'));
|
||||
|
||||
const nameContainer = dom.append(labelContainer, dom.$('span.monaco-icon-name-container'));
|
||||
this.descriptionContainer = this._register(new FastLabelNode(dom.append(labelContainer, dom.$('span.monaco-icon-description-container'))));
|
||||
const nameContainer = dom.append(this.labelContainer, dom.$('span.monaco-icon-name-container'));
|
||||
this.descriptionContainer = this._register(new FastLabelNode(dom.append(this.labelContainer, dom.$('span.monaco-icon-description-container'))));
|
||||
|
||||
if (options?.supportHighlights) {
|
||||
this.nameNode = new LabelWithHighlights(nameContainer, !!options.supportCodicons);
|
||||
@@ -121,6 +124,10 @@ export class IconLabel extends Disposable {
|
||||
} else {
|
||||
this.descriptionNodeFactory = () => this._register(new FastLabelNode(dom.append(this.descriptionContainer.element, dom.$('span.label-description'))));
|
||||
}
|
||||
|
||||
if (options?.hoverDelegate) {
|
||||
this.hoverDelegate = options.hoverDelegate;
|
||||
}
|
||||
}
|
||||
|
||||
get element(): HTMLElement {
|
||||
@@ -144,7 +151,7 @@ export class IconLabel extends Disposable {
|
||||
}
|
||||
|
||||
this.domNode.className = classes.join(' ');
|
||||
this.domNode.title = options?.title || '';
|
||||
this.setupHover(this.labelContainer, options?.title);
|
||||
|
||||
this.nameNode.setLabel(label, options);
|
||||
|
||||
@@ -155,18 +162,95 @@ export class IconLabel extends Disposable {
|
||||
|
||||
if (this.descriptionNode instanceof HighlightedLabel) {
|
||||
this.descriptionNode.set(description || '', options ? options.descriptionMatches : undefined);
|
||||
if (options?.descriptionTitle) {
|
||||
this.descriptionNode.element.title = options.descriptionTitle;
|
||||
} else {
|
||||
this.descriptionNode.element.removeAttribute('title');
|
||||
}
|
||||
this.setupHover(this.descriptionNode.element, options?.descriptionTitle);
|
||||
} else {
|
||||
this.descriptionNode.textContent = description || '';
|
||||
this.descriptionNode.title = options?.descriptionTitle || '';
|
||||
this.setupHover(this.descriptionNode.element, options?.descriptionTitle || '');
|
||||
this.descriptionNode.empty = !description;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setupHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void {
|
||||
const previousCustomHover = this.customHovers.get(htmlElement);
|
||||
if (previousCustomHover) {
|
||||
previousCustomHover.dispose();
|
||||
this.customHovers.delete(htmlElement);
|
||||
}
|
||||
|
||||
if (!tooltip) {
|
||||
htmlElement.removeAttribute('title');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.hoverDelegate) {
|
||||
return this.setupNativeHover(htmlElement, tooltip);
|
||||
} else {
|
||||
return this.setupCustomHover(this.hoverDelegate, htmlElement, tooltip);
|
||||
}
|
||||
}
|
||||
|
||||
private setupCustomHover(hoverDelegate: IHoverDelegate, htmlElement: HTMLElement, markdownTooltip: string | IIconLabelMarkdownString): void {
|
||||
htmlElement.removeAttribute('title');
|
||||
let tooltip = isString(markdownTooltip) ? markdownTooltip : markdownTooltip.markdown;
|
||||
// Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely.
|
||||
// On Mac, the delay is 1500.
|
||||
const hoverDelay = isMacintosh ? 1500 : 500;
|
||||
let hoverOptions: IHoverDelegateOptions | undefined;
|
||||
let mouseX: number | undefined;
|
||||
function mouseOver(this: HTMLElement, e: MouseEvent): any {
|
||||
let isHovering = true;
|
||||
function mouseMove(this: HTMLElement, e: MouseEvent): any {
|
||||
mouseX = e.x;
|
||||
}
|
||||
function mouseLeaveOrDown(this: HTMLElement, e: MouseEvent): any {
|
||||
isHovering = false;
|
||||
}
|
||||
const mouseLeaveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_LEAVE, true)(mouseLeaveOrDown.bind(htmlElement));
|
||||
const mouseDownDisposable = domEvent(htmlElement, dom.EventType.MOUSE_DOWN, true)(mouseLeaveOrDown.bind(htmlElement));
|
||||
const mouseMoveDisposable = domEvent(htmlElement, dom.EventType.MOUSE_MOVE, true)(mouseMove.bind(htmlElement));
|
||||
setTimeout(async () => {
|
||||
if (isHovering && tooltip) {
|
||||
// Re-use the already computed hover options if they exist.
|
||||
if (!hoverOptions) {
|
||||
const target: IHoverDelegateTarget = {
|
||||
targetElements: [this],
|
||||
dispose: () => { }
|
||||
};
|
||||
const resolvedTooltip = await tooltip;
|
||||
if (resolvedTooltip) {
|
||||
hoverOptions = {
|
||||
text: resolvedTooltip,
|
||||
target,
|
||||
anchorPosition: AnchorPosition.BELOW
|
||||
};
|
||||
}
|
||||
}
|
||||
if (hoverOptions) {
|
||||
if (mouseX !== undefined) {
|
||||
(<IHoverDelegateTarget>hoverOptions.target).x = mouseX + 10;
|
||||
}
|
||||
hoverDelegate.showHover(hoverOptions);
|
||||
}
|
||||
}
|
||||
mouseMoveDisposable.dispose();
|
||||
mouseLeaveDisposable.dispose();
|
||||
mouseDownDisposable.dispose();
|
||||
}, hoverDelay);
|
||||
}
|
||||
const mouseOverDisposable = this._register(domEvent(htmlElement, dom.EventType.MOUSE_OVER, true)(mouseOver.bind(htmlElement)));
|
||||
this.customHovers.set(htmlElement, mouseOverDisposable);
|
||||
}
|
||||
|
||||
private setupNativeHover(htmlElement: HTMLElement, tooltip: string | IIconLabelMarkdownString | undefined): void {
|
||||
let stringTooltip: string = '';
|
||||
if (isString(tooltip)) {
|
||||
stringTooltip = tooltip;
|
||||
} else if (tooltip?.markdownNotSupportedFallback) {
|
||||
stringTooltip = tooltip.markdownNotSupportedFallback;
|
||||
}
|
||||
htmlElement.title = stringTooltip;
|
||||
}
|
||||
}
|
||||
|
||||
class Label {
|
||||
@@ -188,14 +272,14 @@ class Label {
|
||||
if (typeof label === 'string') {
|
||||
if (!this.singleLabel) {
|
||||
this.container.innerText = '';
|
||||
dom.removeClass(this.container, 'multiple');
|
||||
this.container.classList.remove('multiple');
|
||||
this.singleLabel = dom.append(this.container, dom.$('a.label-name', { id: options?.domId }));
|
||||
}
|
||||
|
||||
this.singleLabel.textContent = label;
|
||||
} else {
|
||||
this.container.innerText = '';
|
||||
dom.addClass(this.container, 'multiple');
|
||||
this.container.classList.add('multiple');
|
||||
this.singleLabel = undefined;
|
||||
|
||||
for (let i = 0; i < label.length; i++) {
|
||||
@@ -251,15 +335,14 @@ class LabelWithHighlights {
|
||||
if (typeof label === 'string') {
|
||||
if (!this.singleLabel) {
|
||||
this.container.innerText = '';
|
||||
dom.removeClass(this.container, 'multiple');
|
||||
this.container.classList.remove('multiple');
|
||||
this.singleLabel = new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), this.supportCodicons);
|
||||
}
|
||||
|
||||
this.singleLabel.set(label, options?.matches, options?.title, options?.labelEscapeNewLines);
|
||||
this.singleLabel.set(label, options?.matches, undefined, options?.labelEscapeNewLines);
|
||||
} else {
|
||||
|
||||
this.container.innerText = '';
|
||||
dom.addClass(this.container, 'multiple');
|
||||
this.container.classList.add('multiple');
|
||||
this.singleLabel = undefined;
|
||||
|
||||
const separator = options?.separator || '/';
|
||||
@@ -272,7 +355,7 @@ class LabelWithHighlights {
|
||||
|
||||
const name = dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' });
|
||||
const highlightedLabel = new HighlightedLabel(dom.append(this.container, name), this.supportCodicons);
|
||||
highlightedLabel.set(l, m, options?.title, options?.labelEscapeNewLines);
|
||||
highlightedLabel.set(l, m, undefined, options?.labelEscapeNewLines);
|
||||
|
||||
if (i < label.length - 1) {
|
||||
dom.append(name, dom.$('span.label-separator', undefined, separator));
|
||||
|
||||
@@ -60,12 +60,12 @@
|
||||
}
|
||||
|
||||
.monaco-icon-label.italic > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
|
||||
.monaco-icon-label.italic > .monaco-icon-description-container > .label-description {
|
||||
.monaco-icon-label.italic > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
|
||||
.monaco-icon-label.strikethrough > .monaco-icon-description-container > .label-description {
|
||||
.monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
@@ -77,6 +77,10 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.monaco-icon-label.italic::after {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* make sure selection color wins when a label is being selected */
|
||||
.monaco-list:focus .selected .monaco-icon-label, /* list */
|
||||
.monaco-list:focus .selected .monaco-icon-label::after
|
||||
|
||||
@@ -66,11 +66,11 @@ export interface IIdentityProvider<T> {
|
||||
export interface IKeyboardNavigationLabelProvider<T> {
|
||||
|
||||
/**
|
||||
* Return a keyboard navigation label which will be used by the
|
||||
* list for filtering/navigating. Return `undefined` to make an
|
||||
* element always match.
|
||||
* Return a keyboard navigation label(s) which will be used by
|
||||
* the list for filtering/navigating. Return `undefined` to make
|
||||
* an element always match.
|
||||
*/
|
||||
getKeyboardNavigationLabel(element: T): { toString(): string | undefined; } | undefined;
|
||||
getKeyboardNavigationLabel(element: T): { toString(): string | undefined; } | { toString(): string | undefined; }[] | undefined;
|
||||
}
|
||||
|
||||
export interface IKeyboardNavigationDelegate {
|
||||
|
||||
@@ -327,7 +327,6 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
|
||||
this.scrollable = new Scrollable(getOrDefault(options, o => o.smoothScrolling, false) ? 125 : 0, cb => scheduleAtNextAnimationFrame(cb));
|
||||
this.scrollableElement = this.disposables.add(new SmoothScrollableElement(this.rowsContainer, {
|
||||
alwaysConsumeMouseWheel: true,
|
||||
horizontal: ScrollbarVisibility.Auto,
|
||||
vertical: getOrDefault(options, o => o.verticalScrollMode, DefaultOptions.verticalScrollMode),
|
||||
useShadows: getOrDefault(options, o => o.useShadows, DefaultOptions.useShadows),
|
||||
@@ -340,7 +339,7 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
domEvent(this.rowsContainer, TouchEventType.Change)(this.onTouchChange, this, this.disposables);
|
||||
|
||||
// Prevent the monaco-scrollable-element from scrolling
|
||||
// https://github.com/Microsoft/vscode/issues/44181
|
||||
// https://github.com/microsoft/vscode/issues/44181
|
||||
domEvent(this.scrollableElement.getDomNode(), 'scroll')
|
||||
(e => (e.target as HTMLElement).scrollTop = 0, null, this.disposables);
|
||||
|
||||
@@ -1323,6 +1322,9 @@ export class ListView<T> implements ISpliceable<T>, IDisposable {
|
||||
if (item.row) {
|
||||
const renderer = this.renderers.get(item.row.templateId);
|
||||
if (renderer) {
|
||||
if (renderer.disposeElement) {
|
||||
renderer.disposeElement(item.element, -1, item.row.templateData, undefined);
|
||||
}
|
||||
renderer.disposeTemplate(item.row.templateData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,10 +318,12 @@ class KeyboardController<T> implements IDisposable {
|
||||
}
|
||||
|
||||
private onEscape(e: StandardKeyboardEvent): void {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.list.setSelection([], e.browserEvent);
|
||||
this.view.domNode.focus();
|
||||
if (this.list.getSelection().length) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.list.setSelection([], e.browserEvent);
|
||||
this.view.domNode.focus();
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
@@ -1460,6 +1462,8 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
|
||||
this.view.setScrollTop(previousScrollTop + this.view.renderHeight - this.view.elementHeight(lastPageIndex));
|
||||
|
||||
if (this.view.getScrollTop() !== previousScrollTop) {
|
||||
this.setFocus([]);
|
||||
|
||||
// Let the scroll event listener run
|
||||
setTimeout(() => this.focusNextPage(browserEvent, filter), 0);
|
||||
}
|
||||
@@ -1492,6 +1496,8 @@ export class List<T> implements ISpliceable<T>, IThemable, IDisposable {
|
||||
this.view.setScrollTop(scrollTop - this.view.renderHeight);
|
||||
|
||||
if (this.view.getScrollTop() !== previousScrollTop) {
|
||||
this.setFocus([]);
|
||||
|
||||
// Let the scroll event listener run
|
||||
setTimeout(() => this.focusPreviousPage(browserEvent, filter), 0);
|
||||
}
|
||||
|
||||
@@ -5,10 +5,10 @@
|
||||
|
||||
import * as nls from 'vs/nls';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { IActionRunner, IAction, SubmenuAction, Separator, IActionViewItemProvider } from 'vs/base/common/actions';
|
||||
import { IActionRunner, IAction, SubmenuAction, Separator, IActionViewItemProvider, EmptySubmenuAction } from 'vs/base/common/actions';
|
||||
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
|
||||
import { ResolvedKeybinding, KeyCode } from 'vs/base/common/keyCodes';
|
||||
import { addClass, EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, hasClass, addDisposableListener, removeClass, append, $, addClasses, removeClasses, clearNode, createStyleSheet, isInShadowDOM, getActiveElement, Dimension, IDomNodePagePosition } from 'vs/base/browser/dom';
|
||||
import { EventType, EventHelper, EventLike, removeTabIndexAndUpdateFocus, isAncestor, addDisposableListener, append, $, clearNode, createStyleSheet, isInShadowDOM, getActiveElement, Dimension, IDomNodePagePosition } from 'vs/base/browser/dom';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { DisposableStore } from 'vs/base/common/lifecycle';
|
||||
@@ -18,7 +18,7 @@ import { ScrollbarVisibility, ScrollEvent } from 'vs/base/common/scrollable';
|
||||
import { Event } from 'vs/base/common/event';
|
||||
import { AnchorAlignment, layout, LayoutAnchorPosition } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { isLinux, isMacintosh } from 'vs/base/common/platform';
|
||||
import { Codicon, registerIcon, stripCodicons } from 'vs/base/common/codicons';
|
||||
import { Codicon, registerCodicon, stripCodicons } from 'vs/base/common/codicons';
|
||||
import { BaseActionViewItem, ActionViewItem, IActionViewItemOptions } from 'vs/base/browser/ui/actionbar/actionViewItems';
|
||||
import { formatRule } from 'vs/base/browser/ui/codicons/codiconStyles';
|
||||
import { isFirefox } from 'vs/base/browser/browser';
|
||||
@@ -27,8 +27,8 @@ import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
export const MENU_MNEMONIC_REGEX = /\(&([^\s&])\)|(^|[^&])&([^\s&])/;
|
||||
export const MENU_ESCAPED_MNEMONIC_REGEX = /(&)?(&)([^\s&])/g;
|
||||
|
||||
const menuSelectionIcon = registerIcon('menu-selection', Codicon.check);
|
||||
const menuSubmenuIcon = registerIcon('menu-submenu', Codicon.chevronRight);
|
||||
const menuSelectionIcon = registerCodicon('menu-selection', Codicon.check);
|
||||
const menuSubmenuIcon = registerCodicon('menu-submenu', Codicon.chevronRight);
|
||||
|
||||
export enum Direction {
|
||||
Right,
|
||||
@@ -73,10 +73,10 @@ export class Menu extends ActionBar {
|
||||
protected styleSheet: HTMLStyleElement | undefined;
|
||||
|
||||
constructor(container: HTMLElement, actions: ReadonlyArray<IAction>, options: IMenuOptions = {}) {
|
||||
addClass(container, 'monaco-menu-container');
|
||||
container.classList.add('monaco-menu-container');
|
||||
container.setAttribute('role', 'presentation');
|
||||
const menuElement = document.createElement('div');
|
||||
addClass(menuElement, 'monaco-menu');
|
||||
menuElement.classList.add('monaco-menu');
|
||||
menuElement.setAttribute('role', 'presentation');
|
||||
|
||||
super(menuElement, {
|
||||
@@ -170,7 +170,7 @@ export class Menu extends ActionBar {
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
if (hasClass(target, 'action-item')) {
|
||||
if (target.classList.contains('action-item')) {
|
||||
const lastFocusedItem = this.focusedItem;
|
||||
this.setFocusedItem(target);
|
||||
|
||||
@@ -200,7 +200,7 @@ export class Menu extends ActionBar {
|
||||
scrollElement.style.position = '';
|
||||
|
||||
this._register(addDisposableListener(scrollElement, EventType.MOUSE_UP, e => {
|
||||
// Absorb clicks in menu dead space https://github.com/Microsoft/vscode/issues/63575
|
||||
// Absorb clicks in menu dead space https://github.com/microsoft/vscode/issues/63575
|
||||
// We do this on the scroll element so the scroll bar doesn't dismiss the menu either
|
||||
e.preventDefault();
|
||||
}));
|
||||
@@ -448,9 +448,11 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
|
||||
// In all other cases, set timout to allow context menu cancellation to trigger
|
||||
// otherwise the action will destroy the menu and a second context menu
|
||||
// will still trigger for right click.
|
||||
setTimeout(() => {
|
||||
this.onClick(e);
|
||||
}, 0);
|
||||
else {
|
||||
setTimeout(() => {
|
||||
this.onClick(e);
|
||||
}, 0);
|
||||
}
|
||||
}));
|
||||
|
||||
this._register(addDisposableListener(this.element, EventType.CONTEXT_MENU, e => {
|
||||
@@ -596,37 +598,37 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
|
||||
|
||||
updateClass(): void {
|
||||
if (this.cssClass && this.item) {
|
||||
removeClasses(this.item, this.cssClass);
|
||||
this.item.classList.remove(...this.cssClass.split(' '));
|
||||
}
|
||||
if (this.options.icon && this.label) {
|
||||
this.cssClass = this.getAction().class || '';
|
||||
addClass(this.label, 'icon');
|
||||
this.label.classList.add('icon');
|
||||
if (this.cssClass) {
|
||||
addClasses(this.label, this.cssClass);
|
||||
this.label.classList.add(...this.cssClass.split(' '));
|
||||
}
|
||||
this.updateEnabled();
|
||||
} else if (this.label) {
|
||||
removeClass(this.label, 'icon');
|
||||
this.label.classList.remove('icon');
|
||||
}
|
||||
}
|
||||
|
||||
updateEnabled(): void {
|
||||
if (this.getAction().enabled) {
|
||||
if (this.element) {
|
||||
removeClass(this.element, 'disabled');
|
||||
this.element.classList.remove('disabled');
|
||||
}
|
||||
|
||||
if (this.item) {
|
||||
removeClass(this.item, 'disabled');
|
||||
this.item.classList.remove('disabled');
|
||||
this.item.tabIndex = 0;
|
||||
}
|
||||
} else {
|
||||
if (this.element) {
|
||||
addClass(this.element, 'disabled');
|
||||
this.element.classList.add('disabled');
|
||||
}
|
||||
|
||||
if (this.item) {
|
||||
addClass(this.item, 'disabled');
|
||||
this.item.classList.add('disabled');
|
||||
removeTabIndexAndUpdateFocus(this.item);
|
||||
}
|
||||
}
|
||||
@@ -638,11 +640,11 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
|
||||
}
|
||||
|
||||
if (this.getAction().checked) {
|
||||
addClass(this.item, 'checked');
|
||||
this.item.classList.add('checked');
|
||||
this.item.setAttribute('role', 'menuitemcheckbox');
|
||||
this.item.setAttribute('aria-checked', 'true');
|
||||
} else {
|
||||
removeClass(this.item, 'checked');
|
||||
this.item.classList.remove('checked');
|
||||
this.item.setAttribute('role', 'menuitem');
|
||||
this.item.setAttribute('aria-checked', 'false');
|
||||
}
|
||||
@@ -657,7 +659,7 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSelected = this.element && hasClass(this.element, 'focused');
|
||||
const isSelected = this.element && this.element.classList.contains('focused');
|
||||
const fgColor = isSelected && this.menuStyle.selectionForegroundColor ? this.menuStyle.selectionForegroundColor : this.menuStyle.foregroundColor;
|
||||
const bgColor = isSelected && this.menuStyle.selectionBackgroundColor ? this.menuStyle.selectionBackgroundColor : undefined;
|
||||
const border = isSelected && this.menuStyle.selectionBorderColor ? `thin solid ${this.menuStyle.selectionBorderColor}` : '';
|
||||
@@ -725,7 +727,8 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
}
|
||||
|
||||
if (this.item) {
|
||||
addClass(this.item, 'monaco-submenu-item');
|
||||
this.item.classList.add('monaco-submenu-item');
|
||||
this.item.tabIndex = 0;
|
||||
this.item.setAttribute('aria-haspopup', 'true');
|
||||
this.updateAriaExpanded('false');
|
||||
this.submenuIndicator = append(this.item, $('span.submenu-indicator' + menuSubmenuIcon.cssSelector));
|
||||
@@ -775,6 +778,12 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
}));
|
||||
}
|
||||
|
||||
updateEnabled(): void {
|
||||
// override on submenu entry
|
||||
// native menus do not observe enablement on sumbenus
|
||||
// we mimic that behavior
|
||||
}
|
||||
|
||||
open(selectFirst?: boolean): void {
|
||||
this.cleanupExistingSubmenu(false);
|
||||
this.createSubmenu(selectFirst);
|
||||
@@ -840,7 +849,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
if (!this.parentData.submenu) {
|
||||
this.updateAriaExpanded('true');
|
||||
this.submenuContainer = append(this.element, $('div.monaco-submenu'));
|
||||
addClasses(this.submenuContainer, 'menubar-menu-items-holder', 'context-view');
|
||||
this.submenuContainer.classList.add('menubar-menu-items-holder', 'context-view');
|
||||
|
||||
// Set the top value of the menu container before construction
|
||||
// This allows the menu constructor to calculate the proper max height
|
||||
@@ -852,7 +861,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
this.submenuContainer.style.top = '0';
|
||||
this.submenuContainer.style.left = '0';
|
||||
|
||||
this.parentData.submenu = new Menu(this.submenuContainer, this.submenuActions, this.submenuOptions);
|
||||
this.parentData.submenu = new Menu(this.submenuContainer, this.submenuActions.length ? this.submenuActions : [new EmptySubmenuAction()], this.submenuOptions);
|
||||
if (this.menuStyle) {
|
||||
this.parentData.submenu.style(this.menuStyle);
|
||||
}
|
||||
@@ -868,7 +877,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
|
||||
const viewBox = this.submenuContainer.getBoundingClientRect();
|
||||
|
||||
const { top, left } = this.calculateSubmenuMenuLayout({ height: window.innerHeight, width: window.innerWidth }, viewBox, entryBoxUpdated, this.expandDirection);
|
||||
const { top, left } = this.calculateSubmenuMenuLayout(new Dimension(window.innerWidth, window.innerHeight), Dimension.lift(viewBox), entryBoxUpdated, this.expandDirection);
|
||||
this.submenuContainer.style.left = `${left}px`;
|
||||
this.submenuContainer.style.top = `${top}px`;
|
||||
|
||||
@@ -918,7 +927,7 @@ class SubmenuMenuActionViewItem extends BaseMenuActionViewItem {
|
||||
return;
|
||||
}
|
||||
|
||||
const isSelected = this.element && hasClass(this.element, 'focused');
|
||||
const isSelected = this.element && this.element.classList.contains('focused');
|
||||
const fgColor = isSelected && this.menuStyle.selectionForegroundColor ? this.menuStyle.selectionForegroundColor : this.menuStyle.foregroundColor;
|
||||
|
||||
if (this.submenuIndicator) {
|
||||
@@ -1187,6 +1196,7 @@ ${formatRule(menuSubmenuIcon)}
|
||||
outline: 0;
|
||||
border: none;
|
||||
animation: fadeIn 0.083s linear;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.context-view.monaco-menu-container :focus,
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
.menubar.compact .menubar-menu-items-holder {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.menubar .menubar-menu-items-holder.monaco-menu-container {
|
||||
outline: 0;
|
||||
border: none;
|
||||
|
||||
@@ -8,7 +8,6 @@ import * as browser from 'vs/base/browser/browser';
|
||||
import * as DOM from 'vs/base/browser/dom';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import * as nls from 'vs/nls';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { StandardKeyboardEvent } from 'vs/base/browser/keyboardEvent';
|
||||
import { EventType, Gesture, GestureEvent } from 'vs/base/browser/touch';
|
||||
import { cleanMnemonic, IMenuOptions, Menu, MENU_ESCAPED_MNEMONIC_REGEX, MENU_MNEMONIC_REGEX, IMenuStyles, Direction } from 'vs/base/browser/ui/menu/menu';
|
||||
@@ -16,17 +15,17 @@ import { ActionRunner, IAction, IActionRunner, SubmenuAction, Separator } from '
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { Event, Emitter } from 'vs/base/common/event';
|
||||
import { KeyCode, ResolvedKeybinding, KeyMod } from 'vs/base/common/keyCodes';
|
||||
import { Disposable, dispose, IDisposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { Disposable, dispose, IDisposable } from 'vs/base/common/lifecycle';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { asArray } from 'vs/base/common/arrays';
|
||||
import { ScanCodeUtils, ScanCode } from 'vs/base/common/scanCode';
|
||||
import { isMacintosh } from 'vs/base/common/platform';
|
||||
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
import { Codicon, registerCodicon } from 'vs/base/common/codicons';
|
||||
|
||||
const $ = DOM.$;
|
||||
|
||||
const menuBarMoreIcon = registerIcon('menubar-more', Codicon.more);
|
||||
const menuBarMoreIcon = registerCodicon('menubar-more', Codicon.more);
|
||||
|
||||
export interface IMenuBarOptions {
|
||||
enableMnemonics?: boolean;
|
||||
@@ -100,7 +99,7 @@ export class MenuBar extends Disposable {
|
||||
|
||||
this.container.setAttribute('role', 'menubar');
|
||||
if (this.options.compactMode !== undefined) {
|
||||
DOM.addClass(this.container, 'compact');
|
||||
this.container.classList.add('compact');
|
||||
}
|
||||
|
||||
this.menuCache = [];
|
||||
@@ -116,11 +115,11 @@ export class MenuBar extends Disposable {
|
||||
this.menuUpdater = this._register(new RunOnceScheduler(() => this.update(), 200));
|
||||
|
||||
this.actionRunner = this._register(new ActionRunner());
|
||||
this._register(this.actionRunner.onDidBeforeRun(() => {
|
||||
this._register(this.actionRunner.onBeforeRun(() => {
|
||||
this.setUnfocusedState();
|
||||
}));
|
||||
|
||||
this._register(ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this));
|
||||
this._register(DOM.ModifierKeyEmitter.getInstance().event(this.onModifierKeyToggled, this));
|
||||
|
||||
this._register(DOM.addDisposableListener(this.container, DOM.EventType.KEY_DOWN, (e) => {
|
||||
let event = new StandardKeyboardEvent(e as KeyboardEvent);
|
||||
@@ -425,12 +424,12 @@ export class MenuBar extends Disposable {
|
||||
super.dispose();
|
||||
|
||||
this.menuCache.forEach(menuBarMenu => {
|
||||
DOM.removeNode(menuBarMenu.titleElement);
|
||||
DOM.removeNode(menuBarMenu.buttonElement);
|
||||
menuBarMenu.titleElement.remove();
|
||||
menuBarMenu.buttonElement.remove();
|
||||
});
|
||||
|
||||
DOM.removeNode(this.overflowMenu.titleElement);
|
||||
DOM.removeNode(this.overflowMenu.buttonElement);
|
||||
this.overflowMenu.titleElement.remove();
|
||||
this.overflowMenu.buttonElement.remove();
|
||||
|
||||
dispose(this.overflowLayoutScheduled);
|
||||
this.overflowLayoutScheduled = undefined;
|
||||
@@ -509,7 +508,7 @@ export class MenuBar extends Disposable {
|
||||
}
|
||||
|
||||
if (this.overflowMenu.buttonElement.nextElementSibling !== this.menuCache[this.numMenusShown].buttonElement) {
|
||||
DOM.removeNode(this.overflowMenu.buttonElement);
|
||||
this.overflowMenu.buttonElement.remove();
|
||||
this.container.insertBefore(this.overflowMenu.buttonElement, this.menuCache[this.numMenusShown].buttonElement);
|
||||
this.overflowMenu.buttonElement.style.visibility = 'visible';
|
||||
}
|
||||
@@ -520,7 +519,7 @@ export class MenuBar extends Disposable {
|
||||
this.overflowMenu.actions.push(...compactMenuActions);
|
||||
}
|
||||
} else {
|
||||
DOM.removeNode(this.overflowMenu.buttonElement);
|
||||
this.overflowMenu.buttonElement.remove();
|
||||
this.container.appendChild(this.overflowMenu.buttonElement);
|
||||
this.overflowMenu.buttonElement.style.visibility = 'hidden';
|
||||
}
|
||||
@@ -860,8 +859,8 @@ export class MenuBar extends Disposable {
|
||||
}
|
||||
}
|
||||
|
||||
private onModifierKeyToggled(modifierKeyStatus: IModifierKeyStatus): void {
|
||||
const allModifiersReleased = !modifierKeyStatus.altKey && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey;
|
||||
private onModifierKeyToggled(modifierKeyStatus: DOM.IModifierKeyStatus): void {
|
||||
const allModifiersReleased = !modifierKeyStatus.altKey && !modifierKeyStatus.ctrlKey && !modifierKeyStatus.shiftKey && !modifierKeyStatus.metaKey;
|
||||
|
||||
if (this.options.visibility === 'hidden') {
|
||||
return;
|
||||
@@ -923,7 +922,7 @@ export class MenuBar extends Disposable {
|
||||
|
||||
if (this.focusedMenu.holder) {
|
||||
if (this.focusedMenu.holder.parentElement) {
|
||||
DOM.removeClass(this.focusedMenu.holder.parentElement, 'open');
|
||||
this.focusedMenu.holder.parentElement.classList.remove('open');
|
||||
}
|
||||
|
||||
this.focusedMenu.holder.remove();
|
||||
@@ -947,18 +946,20 @@ export class MenuBar extends Disposable {
|
||||
|
||||
const menuHolder = $('div.menubar-menu-items-holder', { 'title': '' });
|
||||
|
||||
DOM.addClass(customMenu.buttonElement, 'open');
|
||||
customMenu.buttonElement.classList.add('open');
|
||||
|
||||
const buttonBoundingRect = customMenu.buttonElement.getBoundingClientRect();
|
||||
|
||||
if (this.options.compactMode === Direction.Right) {
|
||||
menuHolder.style.top = `0px`;
|
||||
menuHolder.style.left = `${customMenu.buttonElement.getBoundingClientRect().left + this.container.clientWidth}px`;
|
||||
menuHolder.style.top = `${buttonBoundingRect.top}px`;
|
||||
menuHolder.style.left = `${buttonBoundingRect.left + this.container.clientWidth}px`;
|
||||
} else if (this.options.compactMode === Direction.Left) {
|
||||
menuHolder.style.top = `0px`;
|
||||
menuHolder.style.top = `${buttonBoundingRect.top}px`;
|
||||
menuHolder.style.right = `${this.container.clientWidth}px`;
|
||||
menuHolder.style.left = 'auto';
|
||||
} else {
|
||||
menuHolder.style.top = `${this.container.clientHeight}px`;
|
||||
menuHolder.style.left = `${customMenu.buttonElement.getBoundingClientRect().left}px`;
|
||||
menuHolder.style.left = `${buttonBoundingRect.left}px`;
|
||||
}
|
||||
|
||||
customMenu.buttonElement.appendChild(menuHolder);
|
||||
@@ -994,119 +995,3 @@ export class MenuBar extends Disposable {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
type ModifierKey = 'alt' | 'ctrl' | 'shift';
|
||||
|
||||
interface IModifierKeyStatus {
|
||||
altKey: boolean;
|
||||
shiftKey: boolean;
|
||||
ctrlKey: boolean;
|
||||
lastKeyPressed?: ModifierKey;
|
||||
lastKeyReleased?: ModifierKey;
|
||||
event?: KeyboardEvent;
|
||||
}
|
||||
|
||||
|
||||
class ModifierKeyEmitter extends Emitter<IModifierKeyStatus> {
|
||||
|
||||
private readonly _subscriptions = new DisposableStore();
|
||||
private _keyStatus: IModifierKeyStatus;
|
||||
private static instance: ModifierKeyEmitter;
|
||||
|
||||
private constructor() {
|
||||
super();
|
||||
|
||||
this._keyStatus = {
|
||||
altKey: false,
|
||||
shiftKey: false,
|
||||
ctrlKey: false
|
||||
};
|
||||
|
||||
this._subscriptions.add(domEvent(document.body, 'keydown', true)(e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
|
||||
if (e.altKey && !this._keyStatus.altKey) {
|
||||
this._keyStatus.lastKeyPressed = 'alt';
|
||||
} else if (e.ctrlKey && !this._keyStatus.ctrlKey) {
|
||||
this._keyStatus.lastKeyPressed = 'ctrl';
|
||||
} else if (e.shiftKey && !this._keyStatus.shiftKey) {
|
||||
this._keyStatus.lastKeyPressed = 'shift';
|
||||
} else if (event.keyCode !== KeyCode.Alt) {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
this._keyStatus.altKey = e.altKey;
|
||||
this._keyStatus.ctrlKey = e.ctrlKey;
|
||||
this._keyStatus.shiftKey = e.shiftKey;
|
||||
|
||||
if (this._keyStatus.lastKeyPressed) {
|
||||
this._keyStatus.event = e;
|
||||
this.fire(this._keyStatus);
|
||||
}
|
||||
}));
|
||||
|
||||
this._subscriptions.add(domEvent(document.body, 'keyup', true)(e => {
|
||||
if (!e.altKey && this._keyStatus.altKey) {
|
||||
this._keyStatus.lastKeyReleased = 'alt';
|
||||
} else if (!e.ctrlKey && this._keyStatus.ctrlKey) {
|
||||
this._keyStatus.lastKeyReleased = 'ctrl';
|
||||
} else if (!e.shiftKey && this._keyStatus.shiftKey) {
|
||||
this._keyStatus.lastKeyReleased = 'shift';
|
||||
} else {
|
||||
this._keyStatus.lastKeyReleased = undefined;
|
||||
}
|
||||
|
||||
if (this._keyStatus.lastKeyPressed !== this._keyStatus.lastKeyReleased) {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
}
|
||||
|
||||
this._keyStatus.altKey = e.altKey;
|
||||
this._keyStatus.ctrlKey = e.ctrlKey;
|
||||
this._keyStatus.shiftKey = e.shiftKey;
|
||||
|
||||
if (this._keyStatus.lastKeyReleased) {
|
||||
this._keyStatus.event = e;
|
||||
this.fire(this._keyStatus);
|
||||
}
|
||||
}));
|
||||
|
||||
this._subscriptions.add(domEvent(document.body, 'mousedown', true)(e => {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
}));
|
||||
|
||||
this._subscriptions.add(domEvent(document.body, 'mouseup', true)(e => {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
}));
|
||||
|
||||
this._subscriptions.add(domEvent(document.body, 'mousemove', true)(e => {
|
||||
if (e.buttons) {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
}
|
||||
}));
|
||||
|
||||
this._subscriptions.add(domEvent(window, 'blur')(e => {
|
||||
this._keyStatus.lastKeyPressed = undefined;
|
||||
this._keyStatus.lastKeyReleased = undefined;
|
||||
this._keyStatus.altKey = false;
|
||||
this._keyStatus.shiftKey = false;
|
||||
this._keyStatus.shiftKey = false;
|
||||
|
||||
this.fire(this._keyStatus);
|
||||
}));
|
||||
}
|
||||
|
||||
static getInstance() {
|
||||
if (!ModifierKeyEmitter.instance) {
|
||||
ModifierKeyEmitter.instance = new ModifierKeyEmitter();
|
||||
}
|
||||
|
||||
return ModifierKeyEmitter.instance;
|
||||
}
|
||||
|
||||
dispose() {
|
||||
super.dispose();
|
||||
this._subscriptions.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,10 @@
|
||||
* The progress bit has a width: 2% (1/50) of the parent container. The animation moves it from 0% to 100% of
|
||||
* that container. Since translateX is relative to the progress bit size, we have to multiple it with
|
||||
* its relative size to the parent container:
|
||||
* 50%: 50 * 50 = 2500%
|
||||
* 100%: 50 * 100 - 50 (do not overflow): 4950%
|
||||
* parent width: 5000%
|
||||
* bit width: 100%
|
||||
* translateX should be as follow:
|
||||
* 50%: 5000% * 50% - 50% (set to center) = 2450%
|
||||
* 100%: 5000% * 100% - 100% (do not overflow) = 4900%
|
||||
*/
|
||||
@keyframes progress { from { transform: translateX(0%) scaleX(1) } 50% { transform: translateX(2500%) scaleX(3) } to { transform: translateX(4950%) scaleX(1) } }
|
||||
@keyframes progress { from { transform: translateX(0%) scaleX(1) } 50% { transform: translateX(2500%) scaleX(3) } to { transform: translateX(4900%) scaleX(1) } }
|
||||
|
||||
@@ -7,16 +7,14 @@ import 'vs/css!./progressbar';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { mixin } from 'vs/base/common/objects';
|
||||
import { removeClasses, addClass, hasClass, addClasses, removeClass, hide, show } from 'vs/base/browser/dom';
|
||||
import { hide, show } from 'vs/base/browser/dom';
|
||||
import { RunOnceScheduler } from 'vs/base/common/async';
|
||||
import { isNumber } from 'vs/base/common/types';
|
||||
|
||||
const css_done = 'done';
|
||||
const css_active = 'active';
|
||||
const css_infinite = 'infinite';
|
||||
const css_discrete = 'discrete';
|
||||
const css_progress_container = 'monaco-progress-container';
|
||||
const css_progress_bit = 'progress-bit';
|
||||
const CSS_DONE = 'done';
|
||||
const CSS_ACTIVE = 'active';
|
||||
const CSS_INFINITE = 'infinite';
|
||||
const CSS_DISCRETE = 'discrete';
|
||||
|
||||
export interface IProgressBarOptions extends IProgressBarStyles {
|
||||
}
|
||||
@@ -58,11 +56,13 @@ export class ProgressBar extends Disposable {
|
||||
|
||||
private create(container: HTMLElement): void {
|
||||
this.element = document.createElement('div');
|
||||
addClass(this.element, css_progress_container);
|
||||
this.element.classList.add('monaco-progress-container');
|
||||
this.element.setAttribute('role', 'progressbar');
|
||||
this.element.setAttribute('aria-valuemin', '0');
|
||||
container.appendChild(this.element);
|
||||
|
||||
this.bit = document.createElement('div');
|
||||
addClass(this.bit, css_progress_bit);
|
||||
this.bit.classList.add('progress-bit');
|
||||
this.element.appendChild(this.bit);
|
||||
|
||||
this.applyStyles();
|
||||
@@ -71,7 +71,7 @@ export class ProgressBar extends Disposable {
|
||||
private off(): void {
|
||||
this.bit.style.width = 'inherit';
|
||||
this.bit.style.opacity = '1';
|
||||
removeClasses(this.element, css_active, css_infinite, css_discrete);
|
||||
this.element.classList.remove(CSS_ACTIVE, CSS_INFINITE, CSS_DISCRETE);
|
||||
|
||||
this.workedVal = 0;
|
||||
this.totalWork = undefined;
|
||||
@@ -92,10 +92,10 @@ export class ProgressBar extends Disposable {
|
||||
}
|
||||
|
||||
private doDone(delayed: boolean): ProgressBar {
|
||||
addClass(this.element, css_done);
|
||||
this.element.classList.add(CSS_DONE);
|
||||
|
||||
// let it grow to 100% width and hide afterwards
|
||||
if (!hasClass(this.element, css_infinite)) {
|
||||
if (!this.element.classList.contains(CSS_INFINITE)) {
|
||||
this.bit.style.width = 'inherit';
|
||||
|
||||
if (delayed) {
|
||||
@@ -125,8 +125,8 @@ export class ProgressBar extends Disposable {
|
||||
this.bit.style.width = '2%';
|
||||
this.bit.style.opacity = '1';
|
||||
|
||||
removeClasses(this.element, css_discrete, css_done);
|
||||
addClasses(this.element, css_active, css_infinite);
|
||||
this.element.classList.remove(CSS_DISCRETE, CSS_DONE);
|
||||
this.element.classList.add(CSS_ACTIVE, CSS_INFINITE);
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -138,6 +138,7 @@ export class ProgressBar extends Disposable {
|
||||
total(value: number): ProgressBar {
|
||||
this.workedVal = 0;
|
||||
this.totalWork = value;
|
||||
this.element.setAttribute('aria-valuemax', value.toString());
|
||||
|
||||
return this;
|
||||
}
|
||||
@@ -173,21 +174,9 @@ export class ProgressBar extends Disposable {
|
||||
this.workedVal = value;
|
||||
this.workedVal = Math.min(totalWork, this.workedVal);
|
||||
|
||||
if (hasClass(this.element, css_infinite)) {
|
||||
removeClass(this.element, css_infinite);
|
||||
}
|
||||
|
||||
if (hasClass(this.element, css_done)) {
|
||||
removeClass(this.element, css_done);
|
||||
}
|
||||
|
||||
if (!hasClass(this.element, css_active)) {
|
||||
addClass(this.element, css_active);
|
||||
}
|
||||
|
||||
if (!hasClass(this.element, css_discrete)) {
|
||||
addClass(this.element, css_discrete);
|
||||
}
|
||||
this.element.classList.remove(CSS_INFINITE, CSS_DONE);
|
||||
this.element.classList.add(CSS_ACTIVE, CSS_DISCRETE);
|
||||
this.element.setAttribute('aria-valuenow', value.toString());
|
||||
|
||||
this.bit.style.width = 100 * (this.workedVal / (totalWork)) + '%';
|
||||
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
:root {
|
||||
--sash-size: 4px;
|
||||
}
|
||||
|
||||
.monaco-sash {
|
||||
position: absolute;
|
||||
z-index: 35;
|
||||
@@ -42,6 +46,63 @@
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.monaco-sash.vertical {
|
||||
cursor: ew-resize;
|
||||
top: 0;
|
||||
width: var(--sash-size);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal {
|
||||
cursor: ns-resize;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: var(--sash-size);
|
||||
}
|
||||
|
||||
.monaco-sash:not(.disabled).orthogonal-start::before,
|
||||
.monaco-sash:not(.disabled).orthogonal-end::after {
|
||||
content: " ";
|
||||
height: calc(var(--sash-size) * 2);
|
||||
width: calc(var(--sash-size) * 2);
|
||||
z-index: 100;
|
||||
display: block;
|
||||
cursor: all-scroll;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal.orthogonal-edge-north:not(.disabled).orthogonal-start::before,
|
||||
.monaco-sash.horizontal.orthogonal-edge-south:not(.disabled).orthogonal-end::after {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal.orthogonal-edge-north:not(.disabled).orthogonal-end::after,
|
||||
.monaco-sash.horizontal.orthogonal-edge-south:not(.disabled).orthogonal-start::before {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.orthogonal-start.vertical::before {
|
||||
left: -calc(var(--sash-size) / 2);
|
||||
top: calc(var(--sash-size) * -1);
|
||||
}
|
||||
.monaco-sash.orthogonal-end.vertical::after {
|
||||
left: -calc(var(--sash-size) / 2);
|
||||
bottom: calc(var(--sash-size) * -1);
|
||||
}
|
||||
.monaco-sash.orthogonal-start.horizontal::before {
|
||||
top: -calc(var(--sash-size) / 2);
|
||||
left: calc(var(--sash-size) * -1);
|
||||
}
|
||||
.monaco-sash.orthogonal-end.horizontal::after {
|
||||
top: -calc(var(--sash-size) / 2);
|
||||
right: calc(var(--sash-size) * -1);
|
||||
}
|
||||
|
||||
.monaco-sash {
|
||||
transition: background-color 0.1s ease-out;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/** Debug **/
|
||||
|
||||
.monaco-sash.debug {
|
||||
|
||||
@@ -37,11 +37,19 @@ export interface ISashEvent {
|
||||
altKey: boolean;
|
||||
}
|
||||
|
||||
export enum OrthogonalEdge {
|
||||
North = 'north',
|
||||
South = 'south',
|
||||
East = 'east',
|
||||
West = 'west'
|
||||
}
|
||||
|
||||
export interface ISashOptions {
|
||||
readonly orientation: Orientation;
|
||||
readonly orthogonalStartSash?: Sash;
|
||||
readonly orthogonalEndSash?: Sash;
|
||||
readonly size?: number;
|
||||
readonly orthogonalEdge?: OrthogonalEdge;
|
||||
}
|
||||
|
||||
export interface IVerticalSashOptions extends ISashOptions {
|
||||
@@ -150,6 +158,10 @@ export class Sash extends Disposable {
|
||||
|
||||
this.el = append(container, $('.monaco-sash'));
|
||||
|
||||
if (options.orthogonalEdge) {
|
||||
this.el.classList.add(`orthogonal-edge-${options.orthogonalEdge}`);
|
||||
}
|
||||
|
||||
if (isMacintosh) {
|
||||
this.el.classList.add('mac');
|
||||
}
|
||||
@@ -241,7 +253,7 @@ export class Sash extends Disposable {
|
||||
this.el.classList.add('active');
|
||||
this._onDidStart.fire(startEvent);
|
||||
|
||||
// fix https://github.com/Microsoft/vscode/issues/21675
|
||||
// fix https://github.com/microsoft/vscode/issues/21675
|
||||
const style = createStyleSheet(this.el);
|
||||
const updateStyle = () => {
|
||||
let cursor = '';
|
||||
|
||||
@@ -38,12 +38,14 @@ export interface AbstractScrollbarOptions {
|
||||
visibility: ScrollbarVisibility;
|
||||
extraScrollbarClassName: string;
|
||||
scrollable: Scrollable;
|
||||
scrollByPage: boolean;
|
||||
}
|
||||
|
||||
export abstract class AbstractScrollbar extends Widget {
|
||||
|
||||
protected _host: ScrollbarHost;
|
||||
protected _scrollable: Scrollable;
|
||||
protected _scrollByPage: boolean;
|
||||
private _lazyRender: boolean;
|
||||
protected _scrollbarState: ScrollbarState;
|
||||
private _visibilityController: ScrollbarVisibilityController;
|
||||
@@ -59,6 +61,7 @@ export abstract class AbstractScrollbar extends Widget {
|
||||
this._lazyRender = opts.lazyRender;
|
||||
this._host = opts.host;
|
||||
this._scrollable = opts.scrollable;
|
||||
this._scrollByPage = opts.scrollByPage;
|
||||
this._scrollbarState = opts.scrollbarState;
|
||||
this._visibilityController = this._register(new ScrollbarVisibilityController(opts.visibility, 'visible scrollbar ' + opts.extraScrollbarClassName, 'invisible scrollbar ' + opts.extraScrollbarClassName));
|
||||
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
|
||||
@@ -210,7 +213,14 @@ export abstract class AbstractScrollbar extends Widget {
|
||||
offsetX = e.posx - domNodePosition.left;
|
||||
offsetY = e.posy - domNodePosition.top;
|
||||
}
|
||||
this._setDesiredScrollPositionNow(this._scrollbarState.getDesiredScrollPositionFromOffset(this._mouseDownRelativePosition(offsetX, offsetY)));
|
||||
|
||||
const offset = this._mouseDownRelativePosition(offsetX, offsetY);
|
||||
this._setDesiredScrollPositionNow(
|
||||
this._scrollByPage
|
||||
? this._scrollbarState.getDesiredScrollPositionFromOffsetPaged(offset)
|
||||
: this._scrollbarState.getDesiredScrollPositionFromOffset(offset)
|
||||
);
|
||||
|
||||
if (e.leftButton) {
|
||||
e.preventDefault();
|
||||
this._sliderMouseDown(e, () => { /*nothing to do*/ });
|
||||
|
||||
@@ -9,11 +9,11 @@ import { ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/s
|
||||
import { ARROW_IMG_SIZE } from 'vs/base/browser/ui/scrollbar/scrollbarArrow';
|
||||
import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
|
||||
import { INewScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
import { Codicon, registerCodicon } from 'vs/base/common/codicons';
|
||||
|
||||
|
||||
const scrollbarButtonLeftIcon = registerIcon('scrollbar-button-left', Codicon.triangleLeft);
|
||||
const scrollbarButtonRightIcon = registerIcon('scrollbar-button-right', Codicon.triangleRight);
|
||||
const scrollbarButtonLeftIcon = registerCodicon('scrollbar-button-left', Codicon.triangleLeft);
|
||||
const scrollbarButtonRightIcon = registerCodicon('scrollbar-button-right', Codicon.triangleRight);
|
||||
|
||||
export class HorizontalScrollbar extends AbstractScrollbar {
|
||||
|
||||
@@ -33,7 +33,8 @@ export class HorizontalScrollbar extends AbstractScrollbar {
|
||||
),
|
||||
visibility: options.horizontal,
|
||||
extraScrollbarClassName: 'horizontal',
|
||||
scrollable: scrollable
|
||||
scrollable: scrollable,
|
||||
scrollByPage: options.scrollByPage
|
||||
});
|
||||
|
||||
if (options.horizontalHasArrows) {
|
||||
|
||||
@@ -361,6 +361,8 @@ export abstract class AbstractScrollableElement extends Widget {
|
||||
|
||||
// console.log(`${Date.now()}, ${e.deltaY}, ${e.deltaX}`);
|
||||
|
||||
let didScroll = false;
|
||||
|
||||
if (e.deltaY || e.deltaX) {
|
||||
let deltaY = e.deltaY * this._options.mouseWheelScrollSensitivity;
|
||||
let deltaX = e.deltaX * this._options.mouseWheelScrollSensitivity;
|
||||
@@ -419,11 +421,12 @@ export abstract class AbstractScrollableElement extends Widget {
|
||||
} else {
|
||||
this._scrollable.setScrollPositionNow(desiredScrollPosition);
|
||||
}
|
||||
this._shouldRender = true;
|
||||
|
||||
didScroll = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (this._options.alwaysConsumeMouseWheel || this._shouldRender) {
|
||||
if (this._options.alwaysConsumeMouseWheel || didScroll) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
@@ -614,7 +617,9 @@ function resolveOptions(opts: ScrollableElementCreationOptions): ScrollableEleme
|
||||
vertical: (typeof opts.vertical !== 'undefined' ? opts.vertical : ScrollbarVisibility.Auto),
|
||||
verticalScrollbarSize: (typeof opts.verticalScrollbarSize !== 'undefined' ? opts.verticalScrollbarSize : 10),
|
||||
verticalHasArrows: (typeof opts.verticalHasArrows !== 'undefined' ? opts.verticalHasArrows : false),
|
||||
verticalSliderSize: (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : 0)
|
||||
verticalSliderSize: (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : 0),
|
||||
|
||||
scrollByPage: (typeof opts.scrollByPage !== 'undefined' ? opts.scrollByPage : false)
|
||||
};
|
||||
|
||||
result.horizontalSliderSize = (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : result.horizontalScrollbarSize);
|
||||
|
||||
@@ -114,6 +114,11 @@ export interface ScrollableElementCreationOptions {
|
||||
* Defaults to false.
|
||||
*/
|
||||
verticalHasArrows?: boolean;
|
||||
/**
|
||||
* Scroll gutter clicks move by page vs. jump to position.
|
||||
* Defaults to false.
|
||||
*/
|
||||
scrollByPage?: boolean;
|
||||
}
|
||||
|
||||
export interface ScrollableElementChangeOptions {
|
||||
@@ -146,4 +151,5 @@ export interface ScrollableElementResolvedOptions {
|
||||
verticalScrollbarSize: number;
|
||||
verticalSliderSize: number;
|
||||
verticalHasArrows: boolean;
|
||||
scrollByPage: boolean;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import { IMouseEvent } from 'vs/base/browser/mouseEvent';
|
||||
import { Widget } from 'vs/base/browser/ui/widget';
|
||||
import { IntervalTimer, TimeoutTimer } from 'vs/base/common/async';
|
||||
import { Codicon } from 'vs/base/common/codicons';
|
||||
import { addClasses } from 'vs/base/browser/dom';
|
||||
|
||||
/**
|
||||
* The arrow image size.
|
||||
@@ -62,7 +61,7 @@ export class ScrollbarArrow extends Widget {
|
||||
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.className = opts.className;
|
||||
addClasses(this.domNode, opts.icon.classNames);
|
||||
this.domNode.classList.add(...opts.icon.classNamesArray);
|
||||
|
||||
this.domNode.style.position = 'absolute';
|
||||
this.domNode.style.width = ARROW_IMG_SIZE + 'px';
|
||||
|
||||
@@ -202,6 +202,28 @@ export class ScrollbarState {
|
||||
return Math.round(desiredSliderPosition / this._computedSliderRatio);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a desired `scrollPosition` from if offset is before or after the slider position.
|
||||
* If offset is before slider, treat as a page up (or left). If after, page down (or right).
|
||||
* `offset` and `_computedSliderPosition` are based on the same coordinate system.
|
||||
* `_visibleSize` corresponds to a "page" of lines in the returned coordinate system.
|
||||
*/
|
||||
public getDesiredScrollPositionFromOffsetPaged(offset: number): number {
|
||||
if (!this._computedIsNeeded) {
|
||||
// no need for a slider
|
||||
return 0;
|
||||
}
|
||||
|
||||
let correctedOffset = offset - this._arrowSize; // compensate if has arrows
|
||||
let desiredScrollPosition = this._scrollPosition;
|
||||
if (correctedOffset < this._computedSliderPosition) {
|
||||
desiredScrollPosition -= this._visibleSize; // page up/left
|
||||
} else {
|
||||
desiredScrollPosition += this._visibleSize; // page down/right
|
||||
}
|
||||
return desiredScrollPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute a desired `scrollPosition` such that the slider moves by `delta`.
|
||||
*/
|
||||
|
||||
@@ -9,10 +9,10 @@ import { ScrollableElementResolvedOptions } from 'vs/base/browser/ui/scrollbar/s
|
||||
import { ARROW_IMG_SIZE } from 'vs/base/browser/ui/scrollbar/scrollbarArrow';
|
||||
import { ScrollbarState } from 'vs/base/browser/ui/scrollbar/scrollbarState';
|
||||
import { INewScrollPosition, ScrollEvent, Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
import { Codicon, registerCodicon } from 'vs/base/common/codicons';
|
||||
|
||||
const scrollbarButtonUpIcon = registerIcon('scrollbar-button-up', Codicon.triangleUp);
|
||||
const scrollbarButtonDownIcon = registerIcon('scrollbar-button-down', Codicon.triangleDown);
|
||||
const scrollbarButtonUpIcon = registerCodicon('scrollbar-button-up', Codicon.triangleUp);
|
||||
const scrollbarButtonDownIcon = registerCodicon('scrollbar-button-down', Codicon.triangleDown);
|
||||
|
||||
export class VerticalScrollbar extends AbstractScrollbar {
|
||||
|
||||
@@ -33,7 +33,8 @@ export class VerticalScrollbar extends AbstractScrollbar {
|
||||
),
|
||||
visibility: options.vertical,
|
||||
extraScrollbarClassName: 'vertical',
|
||||
scrollable: scrollable
|
||||
scrollable: scrollable,
|
||||
scrollByPage: options.scrollByPage
|
||||
});
|
||||
|
||||
if (options.verticalHasArrows) {
|
||||
|
||||
@@ -15,13 +15,18 @@
|
||||
|
||||
/** Actions */
|
||||
|
||||
.monaco-workbench .monaco-action-bar .action-item.select-container {
|
||||
.monaco-action-bar .action-item.select-container {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-workbench .monaco-action-bar .action-item .monaco-select-box {
|
||||
.monaco-action-bar .action-item .monaco-select-box {
|
||||
cursor: pointer;
|
||||
min-width: 110px;
|
||||
min-height: 18px;
|
||||
padding: 2px 23px 2px 8px;
|
||||
}
|
||||
|
||||
.mac .monaco-action-bar .action-item .monaco-select-box {
|
||||
font-size: 11px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ export interface ISelectBoxOptions {
|
||||
// Utilize optionItem interface to capture all option parameters
|
||||
export interface ISelectOptionItem {
|
||||
text: string;
|
||||
detail?: string;
|
||||
decoratorRight?: string;
|
||||
description?: string;
|
||||
descriptionIsMarkdown?: boolean;
|
||||
|
||||
@@ -75,6 +75,15 @@
|
||||
float: left;
|
||||
}
|
||||
|
||||
.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row > .option-detail {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
padding-left: 3.5px;
|
||||
white-space: nowrap;
|
||||
float: left;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row > .option-decorator-right {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -29,7 +29,7 @@ const SELECT_OPTION_ENTRY_TEMPLATE_ID = 'selectOption.entry.template';
|
||||
interface ISelectListTemplateData {
|
||||
root: HTMLElement;
|
||||
text: HTMLElement;
|
||||
itemDescription: HTMLElement;
|
||||
detail: HTMLElement;
|
||||
decoratorRight: HTMLElement;
|
||||
disposables: IDisposable[];
|
||||
}
|
||||
@@ -43,31 +43,27 @@ class SelectListRenderer implements IListRenderer<ISelectOptionItem, ISelectList
|
||||
data.disposables = [];
|
||||
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'));
|
||||
data.itemDescription = dom.append(container, $('.option-text-description'));
|
||||
data.itemDescription.classList.add('visually-hidden');
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
renderElement(element: ISelectOptionItem, index: number, templateData: ISelectListTemplateData): void {
|
||||
const data: ISelectListTemplateData = 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 : '');
|
||||
// {{SQL CARBON EDIT}}
|
||||
data.text.setAttribute('aria-label', text);
|
||||
|
||||
if (typeof element.description === 'string') {
|
||||
const itemDescriptionId = (text.replace(/ /g, '_').toLowerCase() + '_description_' + data.root.id);
|
||||
data.text.setAttribute('aria-describedby', itemDescriptionId);
|
||||
data.itemDescription.id = itemDescriptionId;
|
||||
data.itemDescription.innerText = element.description;
|
||||
}
|
||||
|
||||
// pseudo-select disabled option
|
||||
if (isDisabled) {
|
||||
data.root.classList.add('option-disabled');
|
||||
@@ -234,7 +230,7 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
|
||||
|
||||
if (showDropDown) {
|
||||
this.showSelectDropDown();
|
||||
dom.EventHelper.stop(e);
|
||||
dom.EventHelper.stop(e, true);
|
||||
}
|
||||
}));
|
||||
}
|
||||
@@ -683,7 +679,10 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
|
||||
let longestLength = 0;
|
||||
|
||||
this.options.forEach((option, index) => {
|
||||
const len = option.text.length + (!!option.decoratorRight ? option.decoratorRight.length : 0);
|
||||
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;
|
||||
@@ -716,7 +715,22 @@ export class SelectBoxList extends Disposable implements ISelectBoxDelegate, ILi
|
||||
keyboardSupport: false,
|
||||
mouseSupport: false,
|
||||
accessibilityProvider: {
|
||||
getAriaLabel: (element) => element.text,
|
||||
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: () => 'option',
|
||||
getWidgetRole: () => 'listbox'
|
||||
|
||||
@@ -38,29 +38,18 @@
|
||||
width: 22px;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane > .pane-header > .twisties {
|
||||
width: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transform-origin: center;
|
||||
color: inherit;
|
||||
flex-shrink: 0;
|
||||
.monaco-pane-view .pane > .pane-header > .codicon:first-of-type {
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header > .twisties {
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.monaco-pane-view .pane > .pane-header.expanded > .twisties::before {
|
||||
transform: rotate(90deg);
|
||||
.monaco-pane-view .pane.horizontal:not(.expanded) > .pane-header > .codicon:first-of-type {
|
||||
margin: 2px;
|
||||
}
|
||||
|
||||
/* TODO: actions should be part of the pane, but they aren't yet */
|
||||
.monaco-pane-view .pane > .pane-header > .actions {
|
||||
display: none;
|
||||
flex: 1;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* TODO: actions should be part of the pane, but they aren't yet */
|
||||
@@ -135,7 +124,7 @@
|
||||
transition-property: width;
|
||||
}
|
||||
|
||||
#monaco-workbench-pane-drop-overlay {
|
||||
#monaco-pane-drop-overlay {
|
||||
position: absolute;
|
||||
z-index: 10000;
|
||||
width: 100%;
|
||||
@@ -144,7 +133,7 @@
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
#monaco-workbench-pane-drop-overlay > .pane-overlay-indicator {
|
||||
#monaco-pane-drop-overlay > .pane-overlay-indicator {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -155,6 +144,6 @@
|
||||
transition: opacity 150ms ease-out;
|
||||
}
|
||||
|
||||
#monaco-workbench-pane-drop-overlay > .pane-overlay-indicator.overlay-move-transition {
|
||||
#monaco-pane-drop-overlay > .pane-overlay-indicator.overlay-move-transition {
|
||||
transition: top 70ms ease-out, left 70ms ease-out, width 70ms ease-out, height 70ms ease-out, opacity 150ms ease-out;
|
||||
}
|
||||
|
||||
@@ -298,7 +298,7 @@ class PaneDraggable extends Disposable {
|
||||
|
||||
private static readonly DefaultDragOverBackgroundColor = new Color(new RGBA(128, 128, 128, 0.5));
|
||||
|
||||
private dragOverCounter = 0; // see https://github.com/Microsoft/vscode/issues/14470
|
||||
private dragOverCounter = 0; // see https://github.com/microsoft/vscode/issues/14470
|
||||
|
||||
private _onDidDrop = this._register(new Emitter<{ from: Pane, to: Pane }>());
|
||||
readonly onDidDrop = this._onDidDrop.event;
|
||||
|
||||
@@ -20,31 +20,36 @@
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.monaco-split-view2 > .split-view-container {
|
||||
.monaco-split-view2 > .monaco-scrollable-element {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-split-view2 > .monaco-scrollable-element > .split-view-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.monaco-split-view2 > .split-view-container > .split-view-view {
|
||||
.monaco-split-view2 > .monaco-scrollable-element > .split-view-container > .split-view-view {
|
||||
white-space: initial;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.monaco-split-view2 > .split-view-container > .split-view-view:not(.visible) {
|
||||
.monaco-split-view2 > .monaco-scrollable-element > .split-view-container > .split-view-view:not(.visible) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-split-view2.vertical > .split-view-container > .split-view-view {
|
||||
.monaco-split-view2.vertical > .monaco-scrollable-element > .split-view-container > .split-view-view {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-split-view2.horizontal > .split-view-container > .split-view-view {
|
||||
.monaco-split-view2.horizontal > .monaco-scrollable-element > .split-view-container > .split-view-view {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-split-view2.separator-border > .split-view-container > .split-view-view:not(:first-child)::before {
|
||||
.monaco-split-view2.separator-border > .monaco-scrollable-element > .split-view-container > .split-view-view:not(:first-child)::before {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
@@ -54,12 +59,12 @@
|
||||
background-color: var(--separator-border);
|
||||
}
|
||||
|
||||
.monaco-split-view2.separator-border.horizontal > .split-view-container > .split-view-view:not(:first-child)::before {
|
||||
.monaco-split-view2.separator-border.horizontal > .monaco-scrollable-element > .split-view-container > .split-view-view:not(:first-child)::before {
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.monaco-split-view2.separator-border.vertical > .split-view-container > .split-view-view:not(:first-child)::before {
|
||||
.monaco-split-view2.separator-border.vertical > .monaco-scrollable-element > .split-view-container > .split-view-view:not(:first-child)::before {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,9 @@ import { range, pushToStart, pushToEnd } from 'vs/base/common/arrays';
|
||||
import { Sash, Orientation, ISashEvent as IBaseSashEvent, SashState } from 'vs/base/browser/ui/sash/sash';
|
||||
import { Color } from 'vs/base/common/color';
|
||||
import { domEvent } from 'vs/base/browser/event';
|
||||
import { $, append } from 'vs/base/browser/dom';
|
||||
import { $, append, scheduleAtNextAnimationFrame } from 'vs/base/browser/dom';
|
||||
import { SmoothScrollableElement } from 'vs/base/browser/ui/scrollbar/scrollableElement';
|
||||
import { Scrollable, ScrollbarVisibility } from 'vs/base/common/scrollable';
|
||||
export { Orientation } from 'vs/base/browser/ui/sash/sash';
|
||||
|
||||
export interface ISplitViewStyles {
|
||||
@@ -213,6 +215,8 @@ export class SplitView<TLayoutContext = undefined> extends Disposable {
|
||||
readonly el: HTMLElement;
|
||||
private sashContainer: HTMLElement;
|
||||
private viewContainer: HTMLElement;
|
||||
private scrollable: Scrollable;
|
||||
private scrollableElement: SmoothScrollableElement;
|
||||
private size = 0;
|
||||
private layoutContext: TLayoutContext | undefined;
|
||||
private contentSize = 0;
|
||||
@@ -301,7 +305,20 @@ export class SplitView<TLayoutContext = undefined> extends Disposable {
|
||||
container.appendChild(this.el);
|
||||
|
||||
this.sashContainer = append(this.el, $('.sash-container'));
|
||||
this.viewContainer = append(this.el, $('.split-view-container'));
|
||||
this.viewContainer = $('.split-view-container');
|
||||
|
||||
this.scrollable = new Scrollable(125, scheduleAtNextAnimationFrame);
|
||||
this.scrollableElement = this._register(new SmoothScrollableElement(this.viewContainer, {
|
||||
vertical: this.orientation === Orientation.VERTICAL ? ScrollbarVisibility.Auto : ScrollbarVisibility.Hidden,
|
||||
horizontal: this.orientation === Orientation.HORIZONTAL ? ScrollbarVisibility.Auto : ScrollbarVisibility.Hidden
|
||||
}, this.scrollable));
|
||||
|
||||
this._register(this.scrollableElement.onScroll(e => {
|
||||
this.viewContainer.scrollTop = e.scrollTop;
|
||||
this.viewContainer.scrollLeft = e.scrollLeft;
|
||||
}));
|
||||
|
||||
append(this.el, this.scrollableElement.getDomNode());
|
||||
|
||||
this.style(options.styles || defaultStyles);
|
||||
|
||||
@@ -897,6 +914,21 @@ export class SplitView<TLayoutContext = undefined> extends Disposable {
|
||||
// Layout sashes
|
||||
this.sashItems.forEach(item => item.sash.layout());
|
||||
this.updateSashEnablement();
|
||||
this.updateScrollableElement();
|
||||
}
|
||||
|
||||
private updateScrollableElement(): void {
|
||||
if (this.orientation === Orientation.VERTICAL) {
|
||||
this.scrollableElement.setScrollDimensions({
|
||||
height: this.size,
|
||||
scrollHeight: this.contentSize
|
||||
});
|
||||
} else {
|
||||
this.scrollableElement.setScrollDimensions({
|
||||
width: this.size,
|
||||
scrollWidth: this.contentSize
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private updateSashEnablement(): void {
|
||||
|
||||
@@ -11,12 +11,12 @@ import { ResolvedKeybinding } from 'vs/base/common/keyCodes';
|
||||
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
|
||||
import { AnchorAlignment } from 'vs/base/browser/ui/contextview/contextview';
|
||||
import { withNullAsUndefined } from 'vs/base/common/types';
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
import { Codicon, CSSIcon, registerCodicon } from 'vs/base/common/codicons';
|
||||
import { EventMultiplexer } from 'vs/base/common/event';
|
||||
import { DropdownMenuActionViewItem } from 'vs/base/browser/ui/dropdown/dropdownActionViewItem';
|
||||
import { IContextMenuProvider } from 'vs/base/browser/contextmenu';
|
||||
|
||||
const toolBarMoreIcon = registerIcon('toolbar-more', Codicon.more);
|
||||
const toolBarMoreIcon = registerCodicon('toolbar-more', Codicon.more);
|
||||
|
||||
export interface IToolBarOptions {
|
||||
orientation?: ActionsOrientation;
|
||||
@@ -27,6 +27,7 @@ export interface IToolBarOptions {
|
||||
toggleMenuTitle?: string;
|
||||
anchorAlignmentProvider?: () => AnchorAlignment;
|
||||
renderDropdownAsChildElement?: boolean;
|
||||
moreIcon?: CSSIcon;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +73,7 @@ export class ToolBar extends Disposable {
|
||||
actionViewItemProvider: this.options.actionViewItemProvider,
|
||||
actionRunner: this.actionRunner,
|
||||
keybindingProvider: this.options.getKeyBinding,
|
||||
classNames: toolBarMoreIcon.classNamesArray,
|
||||
classNames: (options.moreIcon ?? toolBarMoreIcon).classNames,
|
||||
anchorAlignmentProvider: this.options.anchorAlignmentProvider,
|
||||
menuAsChild: !!this.options.renderDropdownAsChildElement
|
||||
}
|
||||
|
||||
@@ -186,7 +186,7 @@ function asListOptions<T, TFilterData, TRef>(modelProvider: () => ITreeModel<T,
|
||||
return options.accessibilityProvider!.getWidgetAriaLabel();
|
||||
},
|
||||
getWidgetRole: options.accessibilityProvider && options.accessibilityProvider.getWidgetRole ? () => options.accessibilityProvider!.getWidgetRole!() : () => 'tree',
|
||||
getAriaLevel(node) {
|
||||
getAriaLevel: options.accessibilityProvider && options.accessibilityProvider.getAriaLevel ? (node) => options.accessibilityProvider!.getAriaLevel!(node.element) : (node) => {
|
||||
return node.depth;
|
||||
},
|
||||
getActiveDescendantId: options.accessibilityProvider.getActiveDescendantId && (node => {
|
||||
@@ -400,15 +400,23 @@ class TreeRenderer<T, TFilterData, TRef, TTemplateData> implements IListRenderer
|
||||
}
|
||||
|
||||
private renderTwistie(node: ITreeNode<T, TFilterData>, templateData: ITreeListTemplateData<TTemplateData>) {
|
||||
templateData.twistie.classList.remove(...treeItemExpandedIcon.classNamesArray);
|
||||
|
||||
let twistieRendered = false;
|
||||
|
||||
if (this.renderer.renderTwistie) {
|
||||
this.renderer.renderTwistie(node.element, templateData.twistie);
|
||||
twistieRendered = this.renderer.renderTwistie(node.element, templateData.twistie);
|
||||
}
|
||||
|
||||
if (node.collapsible && (!this.hideTwistiesOfChildlessElements || node.visibleChildrenCount > 0)) {
|
||||
templateData.twistie.classList.add(...treeItemExpandedIcon.classNamesArray, 'collapsible');
|
||||
if (!twistieRendered) {
|
||||
templateData.twistie.classList.add(...treeItemExpandedIcon.classNamesArray);
|
||||
}
|
||||
|
||||
templateData.twistie.classList.add('collapsible');
|
||||
templateData.twistie.classList.toggle('collapsed', node.collapsed);
|
||||
} else {
|
||||
templateData.twistie.classList.remove(...treeItemExpandedIcon.classNamesArray, 'collapsible', 'collapsed');
|
||||
templateData.twistie.classList.remove('collapsible', 'collapsed');
|
||||
}
|
||||
|
||||
if (node.collapsible) {
|
||||
@@ -507,8 +515,9 @@ class TreeRenderer<T, TFilterData, TRef, TTemplateData> implements IListRenderer
|
||||
}
|
||||
}
|
||||
|
||||
class TypeFilter<T> implements ITreeFilter<T, FuzzyScore>, IDisposable {
|
||||
export type LabelFuzzyScore = { label: string; score: FuzzyScore };
|
||||
|
||||
class TypeFilter<T> implements ITreeFilter<T, FuzzyScore | LabelFuzzyScore>, IDisposable {
|
||||
private _totalCount = 0;
|
||||
get totalCount(): number { return this._totalCount; }
|
||||
private _matchCount = 0;
|
||||
@@ -531,7 +540,7 @@ class TypeFilter<T> implements ITreeFilter<T, FuzzyScore>, IDisposable {
|
||||
tree.onWillRefilter(this.reset, this, this.disposables);
|
||||
}
|
||||
|
||||
filter(element: T, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore> {
|
||||
filter(element: T, parentVisibility: TreeVisibility): TreeFilterResult<FuzzyScore | LabelFuzzyScore> {
|
||||
if (this._filter) {
|
||||
const result = this._filter.filter(element, parentVisibility);
|
||||
|
||||
@@ -562,27 +571,28 @@ class TypeFilter<T> implements ITreeFilter<T, FuzzyScore>, IDisposable {
|
||||
}
|
||||
|
||||
const label = this.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(element);
|
||||
const labelStr = label && label.toString();
|
||||
const labels = Array.isArray(label) ? label : [label];
|
||||
|
||||
if (typeof labelStr === 'undefined') {
|
||||
return { data: FuzzyScore.Default, visibility: true };
|
||||
}
|
||||
|
||||
const score = fuzzyScore(this._pattern, this._lowercasePattern, 0, labelStr, labelStr.toLowerCase(), 0, true);
|
||||
|
||||
if (!score) {
|
||||
if (this.tree.options.filterOnType) {
|
||||
return TreeVisibility.Recurse;
|
||||
} else {
|
||||
for (const l of labels) {
|
||||
const labelStr = l && l.toString();
|
||||
if (typeof labelStr === 'undefined') {
|
||||
return { data: FuzzyScore.Default, visibility: true };
|
||||
}
|
||||
|
||||
// DEMO: smarter filter ?
|
||||
// return parentVisibility === TreeVisibility.Visible ? true : TreeVisibility.Recurse;
|
||||
const score = fuzzyScore(this._pattern, this._lowercasePattern, 0, labelStr, labelStr.toLowerCase(), 0, true);
|
||||
if (score) {
|
||||
this._matchCount++;
|
||||
return labels.length === 1 ?
|
||||
{ data: score, visibility: true } :
|
||||
{ data: { label: labelStr, score: score }, visibility: true };
|
||||
}
|
||||
}
|
||||
|
||||
this._matchCount++;
|
||||
return { data: score, visibility: true };
|
||||
if (this.tree.options.filterOnType) {
|
||||
return TreeVisibility.Recurse;
|
||||
} else {
|
||||
return { data: FuzzyScore.Default, visibility: true };
|
||||
}
|
||||
}
|
||||
|
||||
private reset(): void {
|
||||
@@ -809,7 +819,7 @@ class TypeFilterController<T, TFilterData> implements IDisposable {
|
||||
const onDragOver = (event: DragEvent) => {
|
||||
event.preventDefault(); // needed so that the drop event fires (https://stackoverflow.com/questions/21339924/drop-event-not-firing-in-chrome)
|
||||
|
||||
const x = event.screenX - left;
|
||||
const x = event.clientX - left;
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = 'none';
|
||||
}
|
||||
@@ -952,6 +962,7 @@ export interface IAbstractTreeOptionsUpdate extends ITreeRendererOptions {
|
||||
readonly smoothScrolling?: boolean;
|
||||
readonly horizontalScrolling?: boolean;
|
||||
readonly expandOnlyOnDoubleClick?: boolean;
|
||||
readonly expandOnlyOnTwistieClick?: boolean | ((e: any) => boolean); // e is T
|
||||
}
|
||||
|
||||
export interface IAbstractTreeOptions<T, TFilterData = void> extends IAbstractTreeOptionsUpdate, IListOptions<T> {
|
||||
@@ -959,7 +970,6 @@ export interface IAbstractTreeOptions<T, TFilterData = void> extends IAbstractTr
|
||||
readonly filter?: ITreeFilter<T, TFilterData>;
|
||||
readonly dnd?: ITreeDragAndDrop<T>;
|
||||
readonly keyboardNavigationEventFilter?: IKeyboardNavigationEventFilter;
|
||||
readonly expandOnlyOnTwistieClick?: boolean | ((e: T) => boolean);
|
||||
readonly additionalScrollHeight?: number;
|
||||
}
|
||||
|
||||
@@ -992,7 +1002,7 @@ class Trait<T> {
|
||||
constructor(private identityProvider?: IIdentityProvider<T>) { }
|
||||
|
||||
set(nodes: ITreeNode<T, any>[], browserEvent?: UIEvent): void {
|
||||
if (equals(this.nodes, nodes)) {
|
||||
if (!(browserEvent as any)?.__forceEvent && equals(this.nodes, nodes)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1107,7 +1117,9 @@ class TreeNodeListMouseController<T, TFilterData, TRef> extends MouseController<
|
||||
expandOnlyOnTwistieClick = !!this.tree.expandOnlyOnTwistieClick;
|
||||
}
|
||||
|
||||
if (expandOnlyOnTwistieClick && !onTwistie) {
|
||||
const clickedOnFocus = this.tree.getFocus()[0] === node.element;
|
||||
|
||||
if (expandOnlyOnTwistieClick && !onTwistie && e.browserEvent.detail !== 2 && !(clickedOnFocus && !node.collapsed)) {
|
||||
return super.onViewPointer(e);
|
||||
}
|
||||
|
||||
|
||||
@@ -110,10 +110,11 @@ class AsyncDataTreeRenderer<TInput, T, TFilterData, TTemplateData> implements IT
|
||||
renderTwistie(element: IAsyncDataTreeNode<TInput, T>, twistieElement: HTMLElement): boolean {
|
||||
if (element.slow) {
|
||||
twistieElement.classList.add(...treeItemLoadingIcon.classNamesArray);
|
||||
return true;
|
||||
} else {
|
||||
twistieElement.classList.remove(...treeItemLoadingIcon.classNamesArray);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
disposeElement(node: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>, height: number | undefined): void {
|
||||
@@ -1053,10 +1054,11 @@ class CompressibleAsyncDataTreeRenderer<TInput, T, TFilterData, TTemplateData> i
|
||||
renderTwistie(element: IAsyncDataTreeNode<TInput, T>, twistieElement: HTMLElement): boolean {
|
||||
if (element.slow) {
|
||||
twistieElement.classList.add(...treeItemLoadingIcon.classNamesArray);
|
||||
return true;
|
||||
} else {
|
||||
twistieElement.classList.remove(...treeItemLoadingIcon.classNamesArray);
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
disposeElement(node: ITreeNode<IAsyncDataTreeNode<TInput, T>, TFilterData>, index: number, templateData: IDataTreeListTemplateData<TTemplateData>, height: number | undefined): void {
|
||||
|
||||
@@ -38,8 +38,8 @@ export function compress<T>(element: ICompressedTreeElement<T>): ITreeElement<IC
|
||||
const elements = [element.element];
|
||||
const incompressible = element.incompressible || false;
|
||||
|
||||
let childrenIterator: Iterable<ITreeElement<T>>;
|
||||
let children: ITreeElement<T>[];
|
||||
let childrenIterator: Iterable<ICompressedTreeElement<T>>;
|
||||
let children: ICompressedTreeElement<T>[];
|
||||
|
||||
while (true) {
|
||||
[children, childrenIterator] = Iterable.consume(Iterable.from(element.children), 2);
|
||||
@@ -48,12 +48,11 @@ export function compress<T>(element: ICompressedTreeElement<T>): ITreeElement<IC
|
||||
break;
|
||||
}
|
||||
|
||||
element = children[0];
|
||||
|
||||
if (element.incompressible) {
|
||||
if (children[0].incompressible) {
|
||||
break;
|
||||
}
|
||||
|
||||
element = children[0];
|
||||
elements.push(element.element);
|
||||
}
|
||||
|
||||
|
||||
@@ -128,10 +128,11 @@ class CompressibleRenderer<T extends NonNullable<any>, TFilterData, TTemplateDat
|
||||
this.renderer.disposeTemplate(templateData.data);
|
||||
}
|
||||
|
||||
renderTwistie?(element: T, twistieElement: HTMLElement): void {
|
||||
renderTwistie?(element: T, twistieElement: HTMLElement): boolean {
|
||||
if (this.renderer.renderTwistie) {
|
||||
this.renderer.renderTwistie(element, twistieElement);
|
||||
return this.renderer.renderTwistie(element, twistieElement);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ export interface ITreeModel<T, TFilterData, TRef> {
|
||||
}
|
||||
|
||||
export interface ITreeRenderer<T, TFilterData = void, TTemplateData = void> extends IListRenderer<ITreeNode<T, TFilterData>, TTemplateData> {
|
||||
renderTwistie?(element: T, twistieElement: HTMLElement): void;
|
||||
renderTwistie?(element: T, twistieElement: HTMLElement): boolean;
|
||||
onDidChangeTwistieState?: Event<T>;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Codicon, registerIcon } from 'vs/base/common/codicons';
|
||||
import { Codicon, registerCodicon } from 'vs/base/common/codicons';
|
||||
|
||||
export const treeItemExpandedIcon = registerIcon('tree-item-expanded', Codicon.chevronDown); // collapsed is done with rotation
|
||||
export const treeItemExpandedIcon = registerCodicon('tree-item-expanded', Codicon.chevronDown); // collapsed is done with rotation
|
||||
|
||||
export const treeFilterOnTypeOnIcon = registerIcon('tree-filter-on-type-on', Codicon.listFilter);
|
||||
export const treeFilterOnTypeOffIcon = registerIcon('tree-filter-on-type-off', Codicon.listSelection);
|
||||
export const treeFilterClearIcon = registerIcon('tree-filter-clear', Codicon.close);
|
||||
export const treeFilterOnTypeOnIcon = registerCodicon('tree-filter-on-type-on', Codicon.listFilter);
|
||||
export const treeFilterOnTypeOffIcon = registerCodicon('tree-filter-on-type-off', Codicon.listSelection);
|
||||
export const treeFilterClearIcon = registerCodicon('tree-filter-clear', Codicon.close);
|
||||
|
||||
export const treeItemLoadingIcon = registerIcon('tree-item-loading', Codicon.loading);
|
||||
export const treeItemLoadingIcon = registerCodicon('tree-item-loading', Codicon.loading);
|
||||
|
||||
Reference in New Issue
Block a user