Version 1.0
This commit is contained in:
+57
@@ -0,0 +1,57 @@
|
||||
import { mainWindow } from './window.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class WindowManager {
|
||||
constructor() {
|
||||
// --- Zoom Factor
|
||||
this.mapWindowIdToZoomFactor = new Map();
|
||||
}
|
||||
static { this.INSTANCE = new WindowManager(); }
|
||||
getZoomFactor(targetWindow) {
|
||||
return this.mapWindowIdToZoomFactor.get(this.getWindowId(targetWindow)) ?? 1;
|
||||
}
|
||||
getWindowId(targetWindow) {
|
||||
return targetWindow.vscodeWindowId;
|
||||
}
|
||||
}
|
||||
function addMatchMediaChangeListener(targetWindow, query, callback) {
|
||||
if (typeof query === 'string') {
|
||||
query = targetWindow.matchMedia(query);
|
||||
}
|
||||
query.addEventListener('change', callback);
|
||||
}
|
||||
/** The zoom scale for an index, e.g. 1, 1.2, 1.4 */
|
||||
function getZoomFactor(targetWindow) {
|
||||
return WindowManager.INSTANCE.getZoomFactor(targetWindow);
|
||||
}
|
||||
const userAgent = navigator.userAgent;
|
||||
const isFirefox = (userAgent.indexOf('Firefox') >= 0);
|
||||
const isWebKit = (userAgent.indexOf('AppleWebKit') >= 0);
|
||||
const isChrome = (userAgent.indexOf('Chrome') >= 0);
|
||||
const isSafari = (!isChrome && (userAgent.indexOf('Safari') >= 0));
|
||||
const isWebkitWebView = (!isChrome && !isSafari && isWebKit);
|
||||
(userAgent.indexOf('Electron/') >= 0);
|
||||
const isAndroid = (userAgent.indexOf('Android') >= 0);
|
||||
let standalone = false;
|
||||
if (typeof mainWindow.matchMedia === 'function') {
|
||||
const standaloneMatchMedia = mainWindow.matchMedia('(display-mode: standalone) or (display-mode: window-controls-overlay)');
|
||||
const fullScreenMatchMedia = mainWindow.matchMedia('(display-mode: fullscreen)');
|
||||
standalone = standaloneMatchMedia.matches;
|
||||
addMatchMediaChangeListener(mainWindow, standaloneMatchMedia, ({ matches }) => {
|
||||
// entering fullscreen would change standaloneMatchMedia.matches to false
|
||||
// if standalone is true (running as PWA) and entering fullscreen, skip this change
|
||||
if (standalone && fullScreenMatchMedia.matches) {
|
||||
return;
|
||||
}
|
||||
// otherwise update standalone (browser to PWA or PWA to browser)
|
||||
standalone = matches;
|
||||
});
|
||||
}
|
||||
function getMonacoEnvironment() {
|
||||
return globalThis.MonacoEnvironment;
|
||||
}
|
||||
|
||||
export { addMatchMediaChangeListener, getMonacoEnvironment, getZoomFactor, isAndroid, isChrome, isFirefox, isSafari, isWebKit, isWebkitWebView };
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
import './browser.js';
|
||||
import { mainWindow } from './window.js';
|
||||
import { isNative } from '../common/platform.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
/**
|
||||
* Browser feature we can support in current platform, browser and environment.
|
||||
*/
|
||||
const BrowserFeatures = {
|
||||
clipboard: {
|
||||
writeText: (isNative
|
||||
|| (document.queryCommandSupported && document.queryCommandSupported('copy'))
|
||||
|| !!(navigator && navigator.clipboard && navigator.clipboard.writeText)),
|
||||
readText: (isNative
|
||||
|| !!(navigator && navigator.clipboard && navigator.clipboard.readText))
|
||||
},
|
||||
pointerEvents: mainWindow.PointerEvent && ('ontouchstart' in mainWindow || navigator.maxTouchPoints > 0)
|
||||
};
|
||||
|
||||
export { BrowserFeatures };
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
import { FileAccess } from '../common/network.js';
|
||||
|
||||
function asFragment(raw) {
|
||||
return raw;
|
||||
}
|
||||
function asCssValueWithDefault(cssPropertyValue, dflt) {
|
||||
if (cssPropertyValue !== undefined) {
|
||||
const variableMatch = cssPropertyValue.match(/^\s*var\((.+)\)$/);
|
||||
if (variableMatch) {
|
||||
const varArguments = variableMatch[1].split(',', 2);
|
||||
if (varArguments.length === 2) {
|
||||
dflt = asCssValueWithDefault(varArguments[1].trim(), dflt);
|
||||
}
|
||||
return `var(${varArguments[0]}, ${dflt})`;
|
||||
}
|
||||
return cssPropertyValue;
|
||||
}
|
||||
return dflt;
|
||||
}
|
||||
function identValue(value) {
|
||||
const out = value.replaceAll(/[^_\-a-z0-9]/gi, '');
|
||||
if (out !== value) {
|
||||
console.warn(`CSS ident value ${value} modified to ${out} to be safe for CSS`);
|
||||
}
|
||||
return asFragment(out);
|
||||
}
|
||||
function stringValue(value) {
|
||||
return asFragment(`'${value.replaceAll(/'/g, '\\000027')}'`);
|
||||
}
|
||||
/**
|
||||
* returns url('...')
|
||||
*/
|
||||
function asCSSUrl(uri) {
|
||||
if (!uri) {
|
||||
return asFragment(`url('')`);
|
||||
}
|
||||
return inline `url('${asFragment(CSS.escape(FileAccess.uriToBrowserUri(uri).toString(true)))}')`;
|
||||
}
|
||||
function className(value, escapingExpected = false) {
|
||||
const out = CSS.escape(value);
|
||||
if (!escapingExpected && out !== value) {
|
||||
console.warn(`CSS class name ${value} modified to ${out} to be safe for CSS`);
|
||||
}
|
||||
return asFragment(out);
|
||||
}
|
||||
/**
|
||||
* Template string tag that that constructs a CSS fragment.
|
||||
*
|
||||
* All expressions in the template must be css safe values.
|
||||
*/
|
||||
function inline(strings, ...values) {
|
||||
return asFragment(strings.reduce((result, str, i) => {
|
||||
const value = values[i] || '';
|
||||
return result + str + value;
|
||||
}, ''));
|
||||
}
|
||||
class Builder {
|
||||
constructor() {
|
||||
this._parts = [];
|
||||
}
|
||||
push(...parts) {
|
||||
this._parts.push(...parts);
|
||||
}
|
||||
join(joiner = '\n') {
|
||||
return asFragment(this._parts.join(joiner));
|
||||
}
|
||||
}
|
||||
|
||||
export { Builder, asCSSUrl, asCssValueWithDefault, className, identValue, inline, stringValue };
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
import { Mimes } from '../common/mime.js';
|
||||
|
||||
// Common data transfers
|
||||
const DataTransfers = {
|
||||
/**
|
||||
* Application specific resource transfer type
|
||||
*/
|
||||
RESOURCES: 'ResourceURLs',
|
||||
/**
|
||||
* Typically transfer type for copy/paste transfers.
|
||||
*/
|
||||
TEXT: Mimes.text,
|
||||
/**
|
||||
* Internal type used to pass around text/uri-list data.
|
||||
*
|
||||
* This is needed to work around https://bugs.chromium.org/p/chromium/issues/detail?id=239745.
|
||||
*/
|
||||
INTERNAL_URI_LIST: 'application/vnd.code.uri-list',
|
||||
};
|
||||
|
||||
export { DataTransfers };
|
||||
+1454
File diff suppressed because it is too large
Load Diff
+312
@@ -0,0 +1,312 @@
|
||||
import { Schemas } from '../common/network.js';
|
||||
import { reset } from './dom.js';
|
||||
import purify from './dompurify/dompurify.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
/**
|
||||
* List of safe, non-input html tags.
|
||||
*/
|
||||
const basicMarkupHtmlTags = Object.freeze([
|
||||
'a',
|
||||
'abbr',
|
||||
'b',
|
||||
'bdo',
|
||||
'blockquote',
|
||||
'br',
|
||||
'caption',
|
||||
'cite',
|
||||
'code',
|
||||
'col',
|
||||
'colgroup',
|
||||
'dd',
|
||||
'del',
|
||||
'details',
|
||||
'dfn',
|
||||
'div',
|
||||
'dl',
|
||||
'dt',
|
||||
'em',
|
||||
'figcaption',
|
||||
'figure',
|
||||
'h1',
|
||||
'h2',
|
||||
'h3',
|
||||
'h4',
|
||||
'h5',
|
||||
'h6',
|
||||
'hr',
|
||||
'i',
|
||||
'img',
|
||||
'ins',
|
||||
'kbd',
|
||||
'label',
|
||||
'li',
|
||||
'mark',
|
||||
'ol',
|
||||
'p',
|
||||
'pre',
|
||||
'q',
|
||||
'rp',
|
||||
'rt',
|
||||
'ruby',
|
||||
's',
|
||||
'samp',
|
||||
'small',
|
||||
'small',
|
||||
'source',
|
||||
'span',
|
||||
'strike',
|
||||
'strong',
|
||||
'sub',
|
||||
'summary',
|
||||
'sup',
|
||||
'table',
|
||||
'tbody',
|
||||
'td',
|
||||
'tfoot',
|
||||
'th',
|
||||
'thead',
|
||||
'time',
|
||||
'tr',
|
||||
'tt',
|
||||
'u',
|
||||
'ul',
|
||||
'var',
|
||||
'video',
|
||||
'wbr',
|
||||
]);
|
||||
const defaultAllowedAttrs = Object.freeze([
|
||||
'href',
|
||||
'target',
|
||||
'src',
|
||||
'alt',
|
||||
'title',
|
||||
'for',
|
||||
'name',
|
||||
'role',
|
||||
'tabindex',
|
||||
'x-dispatch',
|
||||
'required',
|
||||
'checked',
|
||||
'placeholder',
|
||||
'type',
|
||||
'start',
|
||||
'width',
|
||||
'height',
|
||||
'align',
|
||||
]);
|
||||
const fakeRelativeUrlProtocol = 'vscode-relative-path';
|
||||
function validateLink(value, allowedProtocols) {
|
||||
if (allowedProtocols.override === '*') {
|
||||
return true; // allow all protocols
|
||||
}
|
||||
try {
|
||||
const url = new URL(value, fakeRelativeUrlProtocol + '://');
|
||||
if (allowedProtocols.override.includes(url.protocol.replace(/:$/, ''))) {
|
||||
return true;
|
||||
}
|
||||
if (allowedProtocols.allowRelativePaths
|
||||
&& url.protocol === fakeRelativeUrlProtocol + ':'
|
||||
&& !value.trim().toLowerCase().startsWith(fakeRelativeUrlProtocol)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Hooks dompurify using `afterSanitizeAttributes` to check that all `href` and `src`
|
||||
* attributes are valid.
|
||||
*/
|
||||
function hookDomPurifyHrefAndSrcSanitizer(allowedLinkProtocols, allowedMediaProtocols) {
|
||||
purify.addHook('afterSanitizeAttributes', (node) => {
|
||||
// check all href/src attributes for validity
|
||||
for (const attr of ['href', 'src']) {
|
||||
if (node.hasAttribute(attr)) {
|
||||
const attrValue = node.getAttribute(attr);
|
||||
if (attr === 'href') {
|
||||
if (!attrValue.startsWith('#') && !validateLink(attrValue, allowedLinkProtocols)) {
|
||||
node.removeAttribute(attr);
|
||||
}
|
||||
}
|
||||
else { // 'src'
|
||||
if (!validateLink(attrValue, allowedMediaProtocols)) {
|
||||
node.removeAttribute(attr);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
const defaultDomPurifyConfig = Object.freeze({
|
||||
ALLOWED_TAGS: [...basicMarkupHtmlTags],
|
||||
ALLOWED_ATTR: [...defaultAllowedAttrs],
|
||||
// We sanitize the src/href attributes later if needed
|
||||
ALLOW_UNKNOWN_PROTOCOLS: true,
|
||||
});
|
||||
/**
|
||||
* Sanitizes an html string.
|
||||
*
|
||||
* @param untrusted The HTML string to sanitize.
|
||||
* @param config Optional configuration for sanitization. If not provided, defaults to a safe configuration.
|
||||
*
|
||||
* @returns A sanitized string of html.
|
||||
*/
|
||||
function sanitizeHtml(untrusted, config) {
|
||||
return doSanitizeHtml(untrusted, config, 'trusted');
|
||||
}
|
||||
function doSanitizeHtml(untrusted, config, outputType) {
|
||||
try {
|
||||
const resolvedConfig = { ...defaultDomPurifyConfig };
|
||||
if (config?.allowedTags) {
|
||||
if (config.allowedTags.override) {
|
||||
resolvedConfig.ALLOWED_TAGS = [...config.allowedTags.override];
|
||||
}
|
||||
if (config.allowedTags.augment) {
|
||||
resolvedConfig.ALLOWED_TAGS = [...(resolvedConfig.ALLOWED_TAGS ?? []), ...config.allowedTags.augment];
|
||||
}
|
||||
}
|
||||
let resolvedAttributes = [...defaultAllowedAttrs];
|
||||
if (config?.allowedAttributes) {
|
||||
if (config.allowedAttributes.override) {
|
||||
resolvedAttributes = [...config.allowedAttributes.override];
|
||||
}
|
||||
if (config.allowedAttributes.augment) {
|
||||
resolvedAttributes = [...resolvedAttributes, ...config.allowedAttributes.augment];
|
||||
}
|
||||
}
|
||||
// All attr names are lower-case in the sanitizer hooks
|
||||
resolvedAttributes = resolvedAttributes.map((attr) => {
|
||||
if (typeof attr === 'string') {
|
||||
return attr.toLowerCase();
|
||||
}
|
||||
return {
|
||||
attributeName: attr.attributeName.toLowerCase(),
|
||||
shouldKeep: attr.shouldKeep,
|
||||
};
|
||||
});
|
||||
const allowedAttrNames = new Set(resolvedAttributes.map(attr => typeof attr === 'string' ? attr : attr.attributeName));
|
||||
const allowedAttrPredicates = new Map();
|
||||
for (const attr of resolvedAttributes) {
|
||||
if (typeof attr === 'string') {
|
||||
// New string attribute value clears previously set predicates
|
||||
allowedAttrPredicates.delete(attr);
|
||||
}
|
||||
else {
|
||||
allowedAttrPredicates.set(attr.attributeName, attr);
|
||||
}
|
||||
}
|
||||
resolvedConfig.ALLOWED_ATTR = Array.from(allowedAttrNames);
|
||||
hookDomPurifyHrefAndSrcSanitizer({
|
||||
override: config?.allowedLinkProtocols?.override ?? [Schemas.http, Schemas.https],
|
||||
allowRelativePaths: config?.allowRelativeLinkPaths ?? false
|
||||
}, {
|
||||
override: config?.allowedMediaProtocols?.override ?? [Schemas.http, Schemas.https],
|
||||
allowRelativePaths: config?.allowRelativeMediaPaths ?? false
|
||||
});
|
||||
if (config?.replaceWithPlaintext) {
|
||||
purify.addHook('uponSanitizeElement', replaceWithPlainTextHook);
|
||||
}
|
||||
if (allowedAttrPredicates.size) {
|
||||
purify.addHook('uponSanitizeAttribute', (node, e) => {
|
||||
const predicate = allowedAttrPredicates.get(e.attrName);
|
||||
if (predicate) {
|
||||
const result = predicate.shouldKeep(node, e);
|
||||
if (typeof result === 'string') {
|
||||
e.keepAttr = true;
|
||||
e.attrValue = result;
|
||||
}
|
||||
else {
|
||||
e.keepAttr = result;
|
||||
}
|
||||
}
|
||||
else {
|
||||
e.keepAttr = allowedAttrNames.has(e.attrName);
|
||||
}
|
||||
});
|
||||
}
|
||||
if (outputType === 'dom') {
|
||||
return purify.sanitize(untrusted, {
|
||||
...resolvedConfig,
|
||||
RETURN_DOM_FRAGMENT: true
|
||||
});
|
||||
}
|
||||
else {
|
||||
return purify.sanitize(untrusted, {
|
||||
...resolvedConfig,
|
||||
RETURN_TRUSTED_TYPE: true
|
||||
}); // Cast from lib TrustedHTML to global TrustedHTML
|
||||
}
|
||||
}
|
||||
finally {
|
||||
purify.removeAllHooks();
|
||||
}
|
||||
}
|
||||
const selfClosingTags = ['area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr'];
|
||||
const replaceWithPlainTextHook = (node, data, _config) => {
|
||||
if (!data.allowedTags[data.tagName] && data.tagName !== 'body') {
|
||||
const replacement = convertTagToPlaintext(node);
|
||||
if (replacement) {
|
||||
if (node.nodeType === Node.COMMENT_NODE) {
|
||||
// Workaround for https://github.com/cure53/DOMPurify/issues/1005
|
||||
// The comment will be deleted in the next phase. However if we try to remove it now, it will cause
|
||||
// an exception. Instead we insert the text node before the comment.
|
||||
node.parentElement?.insertBefore(replacement, node);
|
||||
}
|
||||
else {
|
||||
node.parentElement?.replaceChild(replacement, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
function convertTagToPlaintext(node) {
|
||||
if (!node.ownerDocument) {
|
||||
return;
|
||||
}
|
||||
let startTagText;
|
||||
let endTagText;
|
||||
if (node.nodeType === Node.COMMENT_NODE) {
|
||||
startTagText = `<!--${node.textContent}-->`;
|
||||
}
|
||||
else if (node instanceof Element) {
|
||||
const tagName = node.tagName.toLowerCase();
|
||||
const isSelfClosing = selfClosingTags.includes(tagName);
|
||||
const attrString = node.attributes.length ?
|
||||
' ' + Array.from(node.attributes)
|
||||
.map(attr => `${attr.name}="${attr.value}"`)
|
||||
.join(' ')
|
||||
: '';
|
||||
startTagText = `<${tagName}${attrString}>`;
|
||||
if (!isSelfClosing) {
|
||||
endTagText = `</${tagName}>`;
|
||||
}
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
const fragment = document.createDocumentFragment();
|
||||
const textNode = node.ownerDocument.createTextNode(startTagText);
|
||||
fragment.appendChild(textNode);
|
||||
while (node.firstChild) {
|
||||
fragment.appendChild(node.firstChild);
|
||||
}
|
||||
const endTagTextNode = endTagText ? node.ownerDocument.createTextNode(endTagText) : undefined;
|
||||
if (endTagTextNode) {
|
||||
fragment.appendChild(endTagTextNode);
|
||||
}
|
||||
return fragment;
|
||||
}
|
||||
/**
|
||||
* Sanitizes the given `value` and reset the given `node` with it.
|
||||
*/
|
||||
function safeSetInnerHtml(node, untrusted, config) {
|
||||
const fragment = doSanitizeHtml(untrusted, config, 'dom');
|
||||
reset(node, fragment);
|
||||
}
|
||||
|
||||
export { basicMarkupHtmlTags, convertTagToPlaintext, defaultAllowedAttrs, safeSetInnerHtml, sanitizeHtml };
|
||||
+145
@@ -0,0 +1,145 @@
|
||||
import { DisposableStore, toDisposable } from '../common/lifecycle.js';
|
||||
import '../common/observableInternal/index.js';
|
||||
import { isFirefox } from './browser.js';
|
||||
import { getWindows, sharedMutationObserver } from './dom.js';
|
||||
import { mainWindow } from './window.js';
|
||||
import { autorun } from '../common/observableInternal/reactions/autorun.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const globalStylesheets = new Map();
|
||||
/**
|
||||
* A version of createStyleSheet which has a unified API to initialize/set the style content.
|
||||
*/
|
||||
function createStyleSheet2() {
|
||||
return new WrappedStyleElement();
|
||||
}
|
||||
class WrappedStyleElement {
|
||||
constructor() {
|
||||
this._currentCssStyle = '';
|
||||
this._styleSheet = undefined;
|
||||
}
|
||||
setStyle(cssStyle) {
|
||||
if (cssStyle === this._currentCssStyle) {
|
||||
return;
|
||||
}
|
||||
this._currentCssStyle = cssStyle;
|
||||
if (!this._styleSheet) {
|
||||
this._styleSheet = createStyleSheet(mainWindow.document.head, (s) => s.textContent = cssStyle);
|
||||
}
|
||||
else {
|
||||
this._styleSheet.textContent = cssStyle;
|
||||
}
|
||||
}
|
||||
dispose() {
|
||||
if (this._styleSheet) {
|
||||
this._styleSheet.remove();
|
||||
this._styleSheet = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
function createStyleSheet(container = mainWindow.document.head, beforeAppend, disposableStore) {
|
||||
const style = document.createElement('style');
|
||||
style.type = 'text/css';
|
||||
style.media = 'screen';
|
||||
beforeAppend?.(style);
|
||||
container.appendChild(style);
|
||||
if (disposableStore) {
|
||||
disposableStore.add(toDisposable(() => style.remove()));
|
||||
}
|
||||
// With <head> as container, the stylesheet becomes global and is tracked
|
||||
// to support auxiliary windows to clone the stylesheet.
|
||||
if (container === mainWindow.document.head) {
|
||||
const globalStylesheetClones = new Set();
|
||||
globalStylesheets.set(style, globalStylesheetClones);
|
||||
if (disposableStore) {
|
||||
disposableStore.add(toDisposable(() => globalStylesheets.delete(style)));
|
||||
}
|
||||
for (const { window: targetWindow, disposables } of getWindows()) {
|
||||
if (targetWindow === mainWindow) {
|
||||
continue; // main window is already tracked
|
||||
}
|
||||
const cloneDisposable = disposables.add(cloneGlobalStyleSheet(style, globalStylesheetClones, targetWindow));
|
||||
disposableStore?.add(cloneDisposable);
|
||||
}
|
||||
}
|
||||
return style;
|
||||
}
|
||||
function cloneGlobalStyleSheet(globalStylesheet, globalStylesheetClones, targetWindow) {
|
||||
const disposables = new DisposableStore();
|
||||
const clone = globalStylesheet.cloneNode(true);
|
||||
targetWindow.document.head.appendChild(clone);
|
||||
disposables.add(toDisposable(() => clone.remove()));
|
||||
for (const rule of getDynamicStyleSheetRules(globalStylesheet)) {
|
||||
clone.sheet?.insertRule(rule.cssText, clone.sheet?.cssRules.length);
|
||||
}
|
||||
disposables.add(sharedMutationObserver.observe(globalStylesheet, disposables, { childList: true, subtree: isFirefox, characterData: isFirefox })(() => {
|
||||
clone.textContent = globalStylesheet.textContent;
|
||||
}));
|
||||
globalStylesheetClones.add(clone);
|
||||
disposables.add(toDisposable(() => globalStylesheetClones.delete(clone)));
|
||||
return disposables;
|
||||
}
|
||||
let _sharedStyleSheet = null;
|
||||
function getSharedStyleSheet() {
|
||||
if (!_sharedStyleSheet) {
|
||||
_sharedStyleSheet = createStyleSheet();
|
||||
}
|
||||
return _sharedStyleSheet;
|
||||
}
|
||||
function getDynamicStyleSheetRules(style) {
|
||||
if (style?.sheet?.rules) {
|
||||
// Chrome, IE
|
||||
return style.sheet.rules;
|
||||
}
|
||||
if (style?.sheet?.cssRules) {
|
||||
// FF
|
||||
return style.sheet.cssRules;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
function createCSSRule(selector, cssText, style = getSharedStyleSheet()) {
|
||||
if (!style || !cssText) {
|
||||
return;
|
||||
}
|
||||
style.sheet?.insertRule(`${selector} {${cssText}}`, 0);
|
||||
// Apply rule also to all cloned global stylesheets
|
||||
for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) {
|
||||
createCSSRule(selector, cssText, clonedGlobalStylesheet);
|
||||
}
|
||||
}
|
||||
function removeCSSRulesContainingSelector(ruleName, style = getSharedStyleSheet()) {
|
||||
if (!style) {
|
||||
return;
|
||||
}
|
||||
const rules = getDynamicStyleSheetRules(style);
|
||||
const toDelete = [];
|
||||
for (let i = 0; i < rules.length; i++) {
|
||||
const rule = rules[i];
|
||||
if (isCSSStyleRule(rule) && rule.selectorText.indexOf(ruleName) !== -1) {
|
||||
toDelete.push(i);
|
||||
}
|
||||
}
|
||||
for (let i = toDelete.length - 1; i >= 0; i--) {
|
||||
style.sheet?.deleteRule(toDelete[i]);
|
||||
}
|
||||
// Remove rules also from all cloned global stylesheets
|
||||
for (const clonedGlobalStylesheet of globalStylesheets.get(style) ?? []) {
|
||||
removeCSSRulesContainingSelector(ruleName, clonedGlobalStylesheet);
|
||||
}
|
||||
}
|
||||
function isCSSStyleRule(rule) {
|
||||
return typeof rule.selectorText === 'string';
|
||||
}
|
||||
function createStyleSheetFromObservable(css) {
|
||||
const store = new DisposableStore();
|
||||
const w = store.add(createStyleSheet2());
|
||||
store.add(autorun(reader => {
|
||||
w.setStyle(css.read(reader));
|
||||
}));
|
||||
return store;
|
||||
}
|
||||
|
||||
export { createCSSRule, createStyleSheet, createStyleSheet2, createStyleSheetFromObservable, removeCSSRulesContainingSelector };
|
||||
+1347
File diff suppressed because it is too large
Load Diff
+23
@@ -0,0 +1,23 @@
|
||||
import { Emitter } from '../common/event.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class DomEmitter {
|
||||
get event() {
|
||||
return this.emitter.event;
|
||||
}
|
||||
constructor(element, type, useCapture) {
|
||||
const fn = (e) => this.emitter.fire(e);
|
||||
this.emitter = new Emitter({
|
||||
onWillAddFirstListener: () => element.addEventListener(type, fn, useCapture),
|
||||
onDidRemoveLastListener: () => element.removeEventListener(type, fn, useCapture)
|
||||
});
|
||||
}
|
||||
dispose() {
|
||||
this.emitter.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export { DomEmitter };
|
||||
+255
@@ -0,0 +1,255 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class FastDomNode {
|
||||
constructor(domNode) {
|
||||
this.domNode = domNode;
|
||||
this._maxWidth = '';
|
||||
this._width = '';
|
||||
this._height = '';
|
||||
this._top = '';
|
||||
this._left = '';
|
||||
this._bottom = '';
|
||||
this._right = '';
|
||||
this._paddingLeft = '';
|
||||
this._fontFamily = '';
|
||||
this._fontWeight = '';
|
||||
this._fontSize = '';
|
||||
this._fontStyle = '';
|
||||
this._fontFeatureSettings = '';
|
||||
this._fontVariationSettings = '';
|
||||
this._textDecoration = '';
|
||||
this._lineHeight = '';
|
||||
this._letterSpacing = '';
|
||||
this._className = '';
|
||||
this._display = '';
|
||||
this._position = '';
|
||||
this._visibility = '';
|
||||
this._color = '';
|
||||
this._backgroundColor = '';
|
||||
this._layerHint = false;
|
||||
this._contain = 'none';
|
||||
this._boxShadow = '';
|
||||
}
|
||||
focus() {
|
||||
this.domNode.focus();
|
||||
}
|
||||
setMaxWidth(_maxWidth) {
|
||||
const maxWidth = numberAsPixels(_maxWidth);
|
||||
if (this._maxWidth === maxWidth) {
|
||||
return;
|
||||
}
|
||||
this._maxWidth = maxWidth;
|
||||
this.domNode.style.maxWidth = this._maxWidth;
|
||||
}
|
||||
setWidth(_width) {
|
||||
const width = numberAsPixels(_width);
|
||||
if (this._width === width) {
|
||||
return;
|
||||
}
|
||||
this._width = width;
|
||||
this.domNode.style.width = this._width;
|
||||
}
|
||||
setHeight(_height) {
|
||||
const height = numberAsPixels(_height);
|
||||
if (this._height === height) {
|
||||
return;
|
||||
}
|
||||
this._height = height;
|
||||
this.domNode.style.height = this._height;
|
||||
}
|
||||
setTop(_top) {
|
||||
const top = numberAsPixels(_top);
|
||||
if (this._top === top) {
|
||||
return;
|
||||
}
|
||||
this._top = top;
|
||||
this.domNode.style.top = this._top;
|
||||
}
|
||||
setLeft(_left) {
|
||||
const left = numberAsPixels(_left);
|
||||
if (this._left === left) {
|
||||
return;
|
||||
}
|
||||
this._left = left;
|
||||
this.domNode.style.left = this._left;
|
||||
}
|
||||
setBottom(_bottom) {
|
||||
const bottom = numberAsPixels(_bottom);
|
||||
if (this._bottom === bottom) {
|
||||
return;
|
||||
}
|
||||
this._bottom = bottom;
|
||||
this.domNode.style.bottom = this._bottom;
|
||||
}
|
||||
setRight(_right) {
|
||||
const right = numberAsPixels(_right);
|
||||
if (this._right === right) {
|
||||
return;
|
||||
}
|
||||
this._right = right;
|
||||
this.domNode.style.right = this._right;
|
||||
}
|
||||
setPaddingLeft(_paddingLeft) {
|
||||
const paddingLeft = numberAsPixels(_paddingLeft);
|
||||
if (this._paddingLeft === paddingLeft) {
|
||||
return;
|
||||
}
|
||||
this._paddingLeft = paddingLeft;
|
||||
this.domNode.style.paddingLeft = this._paddingLeft;
|
||||
}
|
||||
setFontFamily(fontFamily) {
|
||||
if (this._fontFamily === fontFamily) {
|
||||
return;
|
||||
}
|
||||
this._fontFamily = fontFamily;
|
||||
this.domNode.style.fontFamily = this._fontFamily;
|
||||
}
|
||||
setFontWeight(fontWeight) {
|
||||
if (this._fontWeight === fontWeight) {
|
||||
return;
|
||||
}
|
||||
this._fontWeight = fontWeight;
|
||||
this.domNode.style.fontWeight = this._fontWeight;
|
||||
}
|
||||
setFontSize(_fontSize) {
|
||||
const fontSize = numberAsPixels(_fontSize);
|
||||
if (this._fontSize === fontSize) {
|
||||
return;
|
||||
}
|
||||
this._fontSize = fontSize;
|
||||
this.domNode.style.fontSize = this._fontSize;
|
||||
}
|
||||
setFontStyle(fontStyle) {
|
||||
if (this._fontStyle === fontStyle) {
|
||||
return;
|
||||
}
|
||||
this._fontStyle = fontStyle;
|
||||
this.domNode.style.fontStyle = this._fontStyle;
|
||||
}
|
||||
setFontFeatureSettings(fontFeatureSettings) {
|
||||
if (this._fontFeatureSettings === fontFeatureSettings) {
|
||||
return;
|
||||
}
|
||||
this._fontFeatureSettings = fontFeatureSettings;
|
||||
this.domNode.style.fontFeatureSettings = this._fontFeatureSettings;
|
||||
}
|
||||
setFontVariationSettings(fontVariationSettings) {
|
||||
if (this._fontVariationSettings === fontVariationSettings) {
|
||||
return;
|
||||
}
|
||||
this._fontVariationSettings = fontVariationSettings;
|
||||
this.domNode.style.fontVariationSettings = this._fontVariationSettings;
|
||||
}
|
||||
setTextDecoration(textDecoration) {
|
||||
if (this._textDecoration === textDecoration) {
|
||||
return;
|
||||
}
|
||||
this._textDecoration = textDecoration;
|
||||
this.domNode.style.textDecoration = this._textDecoration;
|
||||
}
|
||||
setLineHeight(_lineHeight) {
|
||||
const lineHeight = numberAsPixels(_lineHeight);
|
||||
if (this._lineHeight === lineHeight) {
|
||||
return;
|
||||
}
|
||||
this._lineHeight = lineHeight;
|
||||
this.domNode.style.lineHeight = this._lineHeight;
|
||||
}
|
||||
setLetterSpacing(_letterSpacing) {
|
||||
const letterSpacing = numberAsPixels(_letterSpacing);
|
||||
if (this._letterSpacing === letterSpacing) {
|
||||
return;
|
||||
}
|
||||
this._letterSpacing = letterSpacing;
|
||||
this.domNode.style.letterSpacing = this._letterSpacing;
|
||||
}
|
||||
setClassName(className) {
|
||||
if (this._className === className) {
|
||||
return;
|
||||
}
|
||||
this._className = className;
|
||||
this.domNode.className = this._className;
|
||||
}
|
||||
toggleClassName(className, shouldHaveIt) {
|
||||
this.domNode.classList.toggle(className, shouldHaveIt);
|
||||
this._className = this.domNode.className;
|
||||
}
|
||||
setDisplay(display) {
|
||||
if (this._display === display) {
|
||||
return;
|
||||
}
|
||||
this._display = display;
|
||||
this.domNode.style.display = this._display;
|
||||
}
|
||||
setPosition(position) {
|
||||
if (this._position === position) {
|
||||
return;
|
||||
}
|
||||
this._position = position;
|
||||
this.domNode.style.position = this._position;
|
||||
}
|
||||
setVisibility(visibility) {
|
||||
if (this._visibility === visibility) {
|
||||
return;
|
||||
}
|
||||
this._visibility = visibility;
|
||||
this.domNode.style.visibility = this._visibility;
|
||||
}
|
||||
setColor(color) {
|
||||
if (this._color === color) {
|
||||
return;
|
||||
}
|
||||
this._color = color;
|
||||
this.domNode.style.color = this._color;
|
||||
}
|
||||
setBackgroundColor(backgroundColor) {
|
||||
if (this._backgroundColor === backgroundColor) {
|
||||
return;
|
||||
}
|
||||
this._backgroundColor = backgroundColor;
|
||||
this.domNode.style.backgroundColor = this._backgroundColor;
|
||||
}
|
||||
setLayerHinting(layerHint) {
|
||||
if (this._layerHint === layerHint) {
|
||||
return;
|
||||
}
|
||||
this._layerHint = layerHint;
|
||||
this.domNode.style.transform = this._layerHint ? 'translate3d(0px, 0px, 0px)' : '';
|
||||
}
|
||||
setBoxShadow(boxShadow) {
|
||||
if (this._boxShadow === boxShadow) {
|
||||
return;
|
||||
}
|
||||
this._boxShadow = boxShadow;
|
||||
this.domNode.style.boxShadow = boxShadow;
|
||||
}
|
||||
setContain(contain) {
|
||||
if (this._contain === contain) {
|
||||
return;
|
||||
}
|
||||
this._contain = contain;
|
||||
this.domNode.style.contain = this._contain;
|
||||
}
|
||||
setAttribute(name, value) {
|
||||
this.domNode.setAttribute(name, value);
|
||||
}
|
||||
removeAttribute(name) {
|
||||
this.domNode.removeAttribute(name);
|
||||
}
|
||||
appendChild(child) {
|
||||
this.domNode.appendChild(child.domNode);
|
||||
}
|
||||
removeChild(child) {
|
||||
this.domNode.removeChild(child.domNode);
|
||||
}
|
||||
}
|
||||
function numberAsPixels(value) {
|
||||
return (typeof value === 'number' ? `${value}px` : value);
|
||||
}
|
||||
function createFastDomNode(domNode) {
|
||||
return new FastDomNode(domNode);
|
||||
}
|
||||
|
||||
export { FastDomNode, createFastDomNode };
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { isWindows, isMacintosh } from '../common/platform.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
/**
|
||||
* The best font-family to be used in CSS based on the platform:
|
||||
* - Windows: Segoe preferred, fallback to sans-serif
|
||||
* - macOS: standard system font, fallback to sans-serif
|
||||
* - Linux: standard system font preferred, fallback to Ubuntu fonts
|
||||
*
|
||||
* Note: this currently does not adjust for different locales.
|
||||
*/
|
||||
const DEFAULT_FONT_FAMILY = isWindows ? '"Segoe WPC", "Segoe UI", sans-serif' : isMacintosh ? '-apple-system, BlinkMacSystemFont, sans-serif' : 'system-ui, "Ubuntu", "Droid Sans", sans-serif';
|
||||
|
||||
export { DEFAULT_FONT_FAMILY };
|
||||
+160
@@ -0,0 +1,160 @@
|
||||
import { addStandardDisposableListener } from './dom.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
function renderText(text, _options, target) {
|
||||
const element = target ?? document.createElement('div');
|
||||
element.textContent = text;
|
||||
return element;
|
||||
}
|
||||
function renderFormattedText(formattedText, options, target) {
|
||||
const element = target ?? document.createElement('div');
|
||||
element.textContent = '';
|
||||
_renderFormattedText(element, parseFormattedText(formattedText), options?.actionHandler, options?.renderCodeSegments);
|
||||
return element;
|
||||
}
|
||||
class StringStream {
|
||||
constructor(source) {
|
||||
this.source = source;
|
||||
this.index = 0;
|
||||
}
|
||||
eos() {
|
||||
return this.index >= this.source.length;
|
||||
}
|
||||
next() {
|
||||
const next = this.peek();
|
||||
this.advance();
|
||||
return next;
|
||||
}
|
||||
peek() {
|
||||
return this.source[this.index];
|
||||
}
|
||||
advance() {
|
||||
this.index++;
|
||||
}
|
||||
}
|
||||
function _renderFormattedText(element, treeNode, actionHandler, renderCodeSegments) {
|
||||
let child;
|
||||
if (treeNode.type === 2 /* FormatType.Text */) {
|
||||
child = document.createTextNode(treeNode.content || '');
|
||||
}
|
||||
else if (treeNode.type === 3 /* FormatType.Bold */) {
|
||||
child = document.createElement('b');
|
||||
}
|
||||
else if (treeNode.type === 4 /* FormatType.Italics */) {
|
||||
child = document.createElement('i');
|
||||
}
|
||||
else if (treeNode.type === 7 /* FormatType.Code */ && renderCodeSegments) {
|
||||
child = document.createElement('code');
|
||||
}
|
||||
else if (treeNode.type === 5 /* FormatType.Action */ && actionHandler) {
|
||||
const a = document.createElement('a');
|
||||
actionHandler.disposables.add(addStandardDisposableListener(a, 'click', (event) => {
|
||||
actionHandler.callback(String(treeNode.index), event);
|
||||
}));
|
||||
child = a;
|
||||
}
|
||||
else if (treeNode.type === 8 /* FormatType.NewLine */) {
|
||||
child = document.createElement('br');
|
||||
}
|
||||
else if (treeNode.type === 1 /* FormatType.Root */) {
|
||||
child = element;
|
||||
}
|
||||
if (child && element !== child) {
|
||||
element.appendChild(child);
|
||||
}
|
||||
if (child && Array.isArray(treeNode.children)) {
|
||||
treeNode.children.forEach((nodeChild) => {
|
||||
_renderFormattedText(child, nodeChild, actionHandler, renderCodeSegments);
|
||||
});
|
||||
}
|
||||
}
|
||||
function parseFormattedText(content, parseCodeSegments) {
|
||||
const root = {
|
||||
type: 1 /* FormatType.Root */,
|
||||
children: []
|
||||
};
|
||||
let actionViewItemIndex = 0;
|
||||
let current = root;
|
||||
const stack = [];
|
||||
const stream = new StringStream(content);
|
||||
while (!stream.eos()) {
|
||||
let next = stream.next();
|
||||
const isEscapedFormatType = (next === '\\' && formatTagType(stream.peek()) !== 0 /* FormatType.Invalid */);
|
||||
if (isEscapedFormatType) {
|
||||
next = stream.next(); // unread the backslash if it escapes a format tag type
|
||||
}
|
||||
if (!isEscapedFormatType && isFormatTag(next) && next === stream.peek()) {
|
||||
stream.advance();
|
||||
if (current.type === 2 /* FormatType.Text */) {
|
||||
current = stack.pop();
|
||||
}
|
||||
const type = formatTagType(next);
|
||||
if (current.type === type || (current.type === 5 /* FormatType.Action */ && type === 6 /* FormatType.ActionClose */)) {
|
||||
current = stack.pop();
|
||||
}
|
||||
else {
|
||||
const newCurrent = {
|
||||
type: type,
|
||||
children: []
|
||||
};
|
||||
if (type === 5 /* FormatType.Action */) {
|
||||
newCurrent.index = actionViewItemIndex;
|
||||
actionViewItemIndex++;
|
||||
}
|
||||
current.children.push(newCurrent);
|
||||
stack.push(current);
|
||||
current = newCurrent;
|
||||
}
|
||||
}
|
||||
else if (next === '\n') {
|
||||
if (current.type === 2 /* FormatType.Text */) {
|
||||
current = stack.pop();
|
||||
}
|
||||
current.children.push({
|
||||
type: 8 /* FormatType.NewLine */
|
||||
});
|
||||
}
|
||||
else {
|
||||
if (current.type !== 2 /* FormatType.Text */) {
|
||||
const textCurrent = {
|
||||
type: 2 /* FormatType.Text */,
|
||||
content: next
|
||||
};
|
||||
current.children.push(textCurrent);
|
||||
stack.push(current);
|
||||
current = textCurrent;
|
||||
}
|
||||
else {
|
||||
current.content += next;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (current.type === 2 /* FormatType.Text */) {
|
||||
current = stack.pop();
|
||||
}
|
||||
return root;
|
||||
}
|
||||
function isFormatTag(char, supportCodeSegments) {
|
||||
return formatTagType(char) !== 0 /* FormatType.Invalid */;
|
||||
}
|
||||
function formatTagType(char, supportCodeSegments) {
|
||||
switch (char) {
|
||||
case '*':
|
||||
return 3 /* FormatType.Bold */;
|
||||
case '_':
|
||||
return 4 /* FormatType.Italics */;
|
||||
case '[':
|
||||
return 5 /* FormatType.Action */;
|
||||
case ']':
|
||||
return 6 /* FormatType.ActionClose */;
|
||||
case '`':
|
||||
return 0 /* FormatType.Invalid */;
|
||||
default:
|
||||
return 0 /* FormatType.Invalid */;
|
||||
}
|
||||
}
|
||||
|
||||
export { renderFormattedText, renderText };
|
||||
Generated
Vendored
+83
@@ -0,0 +1,83 @@
|
||||
import { getWindow, addDisposableListener, EventType } from './dom.js';
|
||||
import { DisposableStore, toDisposable } from '../common/lifecycle.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class GlobalPointerMoveMonitor {
|
||||
constructor() {
|
||||
this._hooks = new DisposableStore();
|
||||
this._pointerMoveCallback = null;
|
||||
this._onStopCallback = null;
|
||||
}
|
||||
dispose() {
|
||||
this.stopMonitoring(false);
|
||||
this._hooks.dispose();
|
||||
}
|
||||
stopMonitoring(invokeStopCallback, browserEvent) {
|
||||
if (!this.isMonitoring()) {
|
||||
// Not monitoring
|
||||
return;
|
||||
}
|
||||
// Unhook
|
||||
this._hooks.clear();
|
||||
this._pointerMoveCallback = null;
|
||||
const onStopCallback = this._onStopCallback;
|
||||
this._onStopCallback = null;
|
||||
if (invokeStopCallback && onStopCallback) {
|
||||
onStopCallback(browserEvent);
|
||||
}
|
||||
}
|
||||
isMonitoring() {
|
||||
return !!this._pointerMoveCallback;
|
||||
}
|
||||
startMonitoring(initialElement, pointerId, initialButtons, pointerMoveCallback, onStopCallback) {
|
||||
if (this.isMonitoring()) {
|
||||
this.stopMonitoring(false);
|
||||
}
|
||||
this._pointerMoveCallback = pointerMoveCallback;
|
||||
this._onStopCallback = onStopCallback;
|
||||
let eventSource = initialElement;
|
||||
try {
|
||||
initialElement.setPointerCapture(pointerId);
|
||||
this._hooks.add(toDisposable(() => {
|
||||
try {
|
||||
initialElement.releasePointerCapture(pointerId);
|
||||
}
|
||||
catch (err) {
|
||||
// See https://github.com/microsoft/vscode/issues/161731
|
||||
//
|
||||
// `releasePointerCapture` sometimes fails when being invoked with the exception:
|
||||
// DOMException: Failed to execute 'releasePointerCapture' on 'Element':
|
||||
// No active pointer with the given id is found.
|
||||
//
|
||||
// There's no need to do anything in case of failure
|
||||
}
|
||||
}));
|
||||
}
|
||||
catch (err) {
|
||||
// See https://github.com/microsoft/vscode/issues/144584
|
||||
// See https://github.com/microsoft/vscode/issues/146947
|
||||
// `setPointerCapture` sometimes fails when being invoked
|
||||
// from a `mousedown` listener on macOS and Windows
|
||||
// and it always fails on Linux with the exception:
|
||||
// DOMException: Failed to execute 'setPointerCapture' on 'Element':
|
||||
// No active pointer with the given id is found.
|
||||
// In case of failure, we bind the listeners on the window
|
||||
eventSource = getWindow(initialElement);
|
||||
}
|
||||
this._hooks.add(addDisposableListener(eventSource, EventType.POINTER_MOVE, (e) => {
|
||||
if (e.buttons !== initialButtons) {
|
||||
// Buttons state has changed in the meantime
|
||||
this.stopMonitoring(true);
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
this._pointerMoveCallback(e);
|
||||
}));
|
||||
this._hooks.add(addDisposableListener(eventSource, EventType.POINTER_UP, (e) => this.stopMonitoring(true)));
|
||||
}
|
||||
}
|
||||
|
||||
export { GlobalPointerMoveMonitor };
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const sameOriginWindowChainCache = new WeakMap();
|
||||
function getParentWindowIfSameOrigin(w) {
|
||||
if (!w.parent || w.parent === w) {
|
||||
return null;
|
||||
}
|
||||
// Cannot really tell if we have access to the parent window unless we try to access something in it
|
||||
try {
|
||||
const location = w.location;
|
||||
const parentLocation = w.parent.location;
|
||||
if (location.origin !== 'null' && parentLocation.origin !== 'null' && location.origin !== parentLocation.origin) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
return null;
|
||||
}
|
||||
return w.parent;
|
||||
}
|
||||
class IframeUtils {
|
||||
/**
|
||||
* Returns a chain of embedded windows with the same origin (which can be accessed programmatically).
|
||||
* Having a chain of length 1 might mean that the current execution environment is running outside of an iframe or inside an iframe embedded in a window with a different origin.
|
||||
*/
|
||||
static getSameOriginWindowChain(targetWindow) {
|
||||
let windowChainCache = sameOriginWindowChainCache.get(targetWindow);
|
||||
if (!windowChainCache) {
|
||||
windowChainCache = [];
|
||||
sameOriginWindowChainCache.set(targetWindow, windowChainCache);
|
||||
let w = targetWindow;
|
||||
let parent;
|
||||
do {
|
||||
parent = getParentWindowIfSameOrigin(w);
|
||||
if (parent) {
|
||||
windowChainCache.push({
|
||||
window: new WeakRef(w),
|
||||
iframeElement: w.frameElement || null
|
||||
});
|
||||
}
|
||||
else {
|
||||
windowChainCache.push({
|
||||
window: new WeakRef(w),
|
||||
iframeElement: null
|
||||
});
|
||||
}
|
||||
w = parent;
|
||||
} while (w);
|
||||
}
|
||||
return windowChainCache.slice(0);
|
||||
}
|
||||
/**
|
||||
* Returns the position of `childWindow` relative to `ancestorWindow`
|
||||
*/
|
||||
static getPositionOfChildWindowRelativeToAncestorWindow(childWindow, ancestorWindow) {
|
||||
if (!ancestorWindow || childWindow === ancestorWindow) {
|
||||
return {
|
||||
top: 0,
|
||||
left: 0
|
||||
};
|
||||
}
|
||||
let top = 0, left = 0;
|
||||
const windowChain = this.getSameOriginWindowChain(childWindow);
|
||||
for (const windowChainEl of windowChain) {
|
||||
const windowInChain = windowChainEl.window.deref();
|
||||
top += windowInChain?.scrollY ?? 0;
|
||||
left += windowInChain?.scrollX ?? 0;
|
||||
if (windowInChain === ancestorWindow) {
|
||||
break;
|
||||
}
|
||||
if (!windowChainEl.iframeElement) {
|
||||
break;
|
||||
}
|
||||
const boundingRect = windowChainEl.iframeElement.getBoundingClientRect();
|
||||
top += boundingRect.top;
|
||||
left += boundingRect.left;
|
||||
}
|
||||
return {
|
||||
top: top,
|
||||
left: left
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export { IframeUtils };
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
import { isFirefox, isWebKit } from './browser.js';
|
||||
import { KeyCodeUtils, EVENT_KEY_CODE_MAP } from '../common/keyCodes.js';
|
||||
import { KeyCodeChord } from '../common/keybindings.js';
|
||||
import { isMacintosh, isLinux } from '../common/platform.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
function extractKeyCode(e) {
|
||||
if (e.charCode) {
|
||||
// "keypress" events mostly
|
||||
const char = String.fromCharCode(e.charCode).toUpperCase();
|
||||
return KeyCodeUtils.fromString(char);
|
||||
}
|
||||
const keyCode = e.keyCode;
|
||||
// browser quirks
|
||||
if (keyCode === 3) {
|
||||
return 7 /* KeyCode.PauseBreak */;
|
||||
}
|
||||
else if (isFirefox) {
|
||||
switch (keyCode) {
|
||||
case 59: return 85 /* KeyCode.Semicolon */;
|
||||
case 60:
|
||||
if (isLinux) {
|
||||
return 97 /* KeyCode.IntlBackslash */;
|
||||
}
|
||||
break;
|
||||
case 61: return 86 /* KeyCode.Equal */;
|
||||
// based on: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/keyCode#numpad_keys
|
||||
case 107: return 109 /* KeyCode.NumpadAdd */;
|
||||
case 109: return 111 /* KeyCode.NumpadSubtract */;
|
||||
case 173: return 88 /* KeyCode.Minus */;
|
||||
case 224:
|
||||
if (isMacintosh) {
|
||||
return 57 /* KeyCode.Meta */;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (isWebKit) {
|
||||
if (isMacintosh && keyCode === 93) {
|
||||
// the two meta keys in the Mac have different key codes (91 and 93)
|
||||
return 57 /* KeyCode.Meta */;
|
||||
}
|
||||
else if (!isMacintosh && keyCode === 92) {
|
||||
return 57 /* KeyCode.Meta */;
|
||||
}
|
||||
}
|
||||
// cross browser keycodes:
|
||||
return EVENT_KEY_CODE_MAP[keyCode] || 0 /* KeyCode.Unknown */;
|
||||
}
|
||||
const ctrlKeyMod = (isMacintosh ? 256 /* KeyMod.WinCtrl */ : 2048 /* KeyMod.CtrlCmd */);
|
||||
const altKeyMod = 512 /* KeyMod.Alt */;
|
||||
const shiftKeyMod = 1024 /* KeyMod.Shift */;
|
||||
const metaKeyMod = (isMacintosh ? 2048 /* KeyMod.CtrlCmd */ : 256 /* KeyMod.WinCtrl */);
|
||||
class StandardKeyboardEvent {
|
||||
constructor(source) {
|
||||
this._standardKeyboardEventBrand = true;
|
||||
const e = source;
|
||||
this.browserEvent = e;
|
||||
this.target = e.target;
|
||||
this.ctrlKey = e.ctrlKey;
|
||||
this.shiftKey = e.shiftKey;
|
||||
this.altKey = e.altKey;
|
||||
this.metaKey = e.metaKey;
|
||||
this.altGraphKey = e.getModifierState?.('AltGraph');
|
||||
this.keyCode = extractKeyCode(e);
|
||||
this.code = e.code;
|
||||
// console.info(e.type + ": keyCode: " + e.keyCode + ", which: " + e.which + ", charCode: " + e.charCode + ", detail: " + e.detail + " ====> " + this.keyCode + ' -- ' + KeyCode[this.keyCode]);
|
||||
this.ctrlKey = this.ctrlKey || this.keyCode === 5 /* KeyCode.Ctrl */;
|
||||
this.altKey = this.altKey || this.keyCode === 6 /* KeyCode.Alt */;
|
||||
this.shiftKey = this.shiftKey || this.keyCode === 4 /* KeyCode.Shift */;
|
||||
this.metaKey = this.metaKey || this.keyCode === 57 /* KeyCode.Meta */;
|
||||
this._asKeybinding = this._computeKeybinding();
|
||||
this._asKeyCodeChord = this._computeKeyCodeChord();
|
||||
// console.log(`code: ${e.code}, keyCode: ${e.keyCode}, key: ${e.key}`);
|
||||
}
|
||||
preventDefault() {
|
||||
if (this.browserEvent && this.browserEvent.preventDefault) {
|
||||
this.browserEvent.preventDefault();
|
||||
}
|
||||
}
|
||||
stopPropagation() {
|
||||
if (this.browserEvent && this.browserEvent.stopPropagation) {
|
||||
this.browserEvent.stopPropagation();
|
||||
}
|
||||
}
|
||||
toKeyCodeChord() {
|
||||
return this._asKeyCodeChord;
|
||||
}
|
||||
equals(other) {
|
||||
return this._asKeybinding === other;
|
||||
}
|
||||
_computeKeybinding() {
|
||||
let key = 0 /* KeyCode.Unknown */;
|
||||
if (this.keyCode !== 5 /* KeyCode.Ctrl */ && this.keyCode !== 4 /* KeyCode.Shift */ && this.keyCode !== 6 /* KeyCode.Alt */ && this.keyCode !== 57 /* KeyCode.Meta */) {
|
||||
key = this.keyCode;
|
||||
}
|
||||
let result = 0;
|
||||
if (this.ctrlKey) {
|
||||
result |= ctrlKeyMod;
|
||||
}
|
||||
if (this.altKey) {
|
||||
result |= altKeyMod;
|
||||
}
|
||||
if (this.shiftKey) {
|
||||
result |= shiftKeyMod;
|
||||
}
|
||||
if (this.metaKey) {
|
||||
result |= metaKeyMod;
|
||||
}
|
||||
result |= key;
|
||||
return result;
|
||||
}
|
||||
_computeKeyCodeChord() {
|
||||
let key = 0 /* KeyCode.Unknown */;
|
||||
if (this.keyCode !== 5 /* KeyCode.Ctrl */ && this.keyCode !== 4 /* KeyCode.Shift */ && this.keyCode !== 6 /* KeyCode.Alt */ && this.keyCode !== 57 /* KeyCode.Meta */) {
|
||||
key = this.keyCode;
|
||||
}
|
||||
return new KeyCodeChord(this.ctrlKey, this.shiftKey, this.altKey, this.metaKey, key);
|
||||
}
|
||||
}
|
||||
|
||||
export { StandardKeyboardEvent };
|
||||
+901
@@ -0,0 +1,901 @@
|
||||
import { onUnexpectedError } from '../common/errors.js';
|
||||
import { removeMarkdownEscapes, escapeDoubleQuotes, parseHrefAndDimensions } from '../common/htmlContent.js';
|
||||
import { markdownEscapeEscapedIcons } from '../common/iconLabels.js';
|
||||
import { defaultGenerator } from '../common/idGenerator.js';
|
||||
import { Lazy } from '../common/lazy.js';
|
||||
import { DisposableStore } from '../common/lifecycle.js';
|
||||
import { Renderer as _Renderer, Marked, lexer, parse as parse$1 } from '../common/marked/marked.js';
|
||||
import { parse } from '../common/marshalling.js';
|
||||
import { Schemas, FileAccess } from '../common/network.js';
|
||||
import { cloneAndChange } from '../common/objects.js';
|
||||
import { resolvePath, dirname } from '../common/resources.js';
|
||||
import { escape } from '../common/strings.js';
|
||||
import { URI } from '../common/uri.js';
|
||||
import { reset, addDisposableListener, $, getWindow, isHTMLElement } from './dom.js';
|
||||
import { basicMarkupHtmlTags, safeSetInnerHtml, convertTagToPlaintext, sanitizeHtml } from './domSanitize.js';
|
||||
import { StandardKeyboardEvent } from './keyboardEvent.js';
|
||||
import { StandardMouseEvent } from './mouseEvent.js';
|
||||
import { renderLabelWithIcons, renderIcon } from './ui/iconLabel/iconLabels.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const defaultMarkedRenderers = Object.freeze({
|
||||
image: ({ href, title, text }) => {
|
||||
let dimensions = [];
|
||||
let attributes = [];
|
||||
if (href) {
|
||||
({ href, dimensions } = parseHrefAndDimensions(href));
|
||||
attributes.push(`src="${escapeDoubleQuotes(href)}"`);
|
||||
}
|
||||
if (text) {
|
||||
attributes.push(`alt="${escapeDoubleQuotes(text)}"`);
|
||||
}
|
||||
if (title) {
|
||||
attributes.push(`title="${escapeDoubleQuotes(title)}"`);
|
||||
}
|
||||
if (dimensions.length) {
|
||||
attributes = attributes.concat(dimensions);
|
||||
}
|
||||
return '<img ' + attributes.join(' ') + '>';
|
||||
},
|
||||
paragraph({ tokens }) {
|
||||
return `<p>${this.parser.parseInline(tokens)}</p>`;
|
||||
},
|
||||
link({ href, title, tokens }) {
|
||||
let text = this.parser.parseInline(tokens);
|
||||
if (typeof href !== 'string') {
|
||||
return '';
|
||||
}
|
||||
// Remove markdown escapes. Workaround for https://github.com/chjj/marked/issues/829
|
||||
if (href === text) { // raw link case
|
||||
text = removeMarkdownEscapes(text);
|
||||
}
|
||||
title = typeof title === 'string' ? escapeDoubleQuotes(removeMarkdownEscapes(title)) : '';
|
||||
href = removeMarkdownEscapes(href);
|
||||
// HTML Encode href
|
||||
href = href.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
return `<a href="${href}" title="${title || href}" draggable="false">${text}</a>`;
|
||||
},
|
||||
});
|
||||
/**
|
||||
* Blockquote renderer that processes GitHub-style alert syntax.
|
||||
* Transforms blockquotes like "> [!NOTE]" into structured alert markup with icons.
|
||||
*
|
||||
* Based on GitHub's alert syntax: https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/basic-writing-and-formatting-syntax#alerts
|
||||
*/
|
||||
function createAlertBlockquoteRenderer(fallbackRenderer) {
|
||||
return function (token) {
|
||||
const { tokens } = token;
|
||||
// Check if this blockquote starts with alert syntax [!TYPE]
|
||||
const firstToken = tokens[0];
|
||||
if (firstToken?.type !== 'paragraph') {
|
||||
return fallbackRenderer.call(this, token);
|
||||
}
|
||||
const paragraphTokens = firstToken.tokens;
|
||||
if (!paragraphTokens || paragraphTokens.length === 0) {
|
||||
return fallbackRenderer.call(this, token);
|
||||
}
|
||||
const firstTextToken = paragraphTokens[0];
|
||||
if (firstTextToken?.type !== 'text') {
|
||||
return fallbackRenderer.call(this, token);
|
||||
}
|
||||
const pattern = /^\s*\[!(NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\s*?\n*/i;
|
||||
const match = firstTextToken.raw.match(pattern);
|
||||
if (!match) {
|
||||
return fallbackRenderer.call(this, token);
|
||||
}
|
||||
// Remove the alert marker from the token
|
||||
firstTextToken.raw = firstTextToken.raw.replace(pattern, '');
|
||||
firstTextToken.text = firstTextToken.text.replace(pattern, '');
|
||||
const alertIcons = {
|
||||
'note': 'info',
|
||||
'tip': 'light-bulb',
|
||||
'important': 'comment',
|
||||
'warning': 'alert',
|
||||
'caution': 'stop'
|
||||
};
|
||||
const type = match[1];
|
||||
const typeCapitalized = type.charAt(0).toUpperCase() + type.slice(1).toLowerCase();
|
||||
const severity = type.toLowerCase();
|
||||
const iconHtml = renderIcon({ id: alertIcons[severity] }).outerHTML;
|
||||
// Render the remaining content
|
||||
const content = this.parser.parse(tokens);
|
||||
// Return alert markup with icon and severity (skipping the first 3 characters: `<p>`)
|
||||
return `<blockquote data-severity="${severity}"><p><span>${iconHtml}${typeCapitalized}</span>${content.substring(3)}</blockquote>\n`;
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Low-level way create a html element from a markdown string.
|
||||
*
|
||||
* **Note** that for most cases you should be using {@link import('../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js').MarkdownRenderer MarkdownRenderer}
|
||||
* which comes with support for pretty code block rendering and which uses the default way of handling links.
|
||||
*/
|
||||
function renderMarkdown(markdown, options = {}, target) {
|
||||
const disposables = new DisposableStore();
|
||||
let isDisposed = false;
|
||||
const markedInstance = new Marked(...(options.markedExtensions ?? []));
|
||||
const { renderer, codeBlocks, syncCodeBlocks } = createMarkdownRenderer(markedInstance, options, markdown);
|
||||
const value = preprocessMarkdownString(markdown);
|
||||
let renderedMarkdown;
|
||||
if (options.fillInIncompleteTokens) {
|
||||
// The defaults are applied by parse but not lexer()/parser(), and they need to be present
|
||||
const opts = {
|
||||
...markedInstance.defaults,
|
||||
...options.markedOptions,
|
||||
renderer
|
||||
};
|
||||
const tokens = markedInstance.lexer(value, opts);
|
||||
const newTokens = fillInIncompleteTokens(tokens);
|
||||
renderedMarkdown = markedInstance.parser(newTokens, opts);
|
||||
}
|
||||
else {
|
||||
renderedMarkdown = markedInstance.parse(value, { ...options?.markedOptions, renderer, async: false });
|
||||
}
|
||||
// Rewrite theme icons
|
||||
if (markdown.supportThemeIcons) {
|
||||
const elements = renderLabelWithIcons(renderedMarkdown);
|
||||
renderedMarkdown = elements.map(e => typeof e === 'string' ? e : e.outerHTML).join('');
|
||||
}
|
||||
const renderedContent = document.createElement('div');
|
||||
const sanitizerConfig = getDomSanitizerConfig(markdown, options.sanitizerConfig ?? {});
|
||||
safeSetInnerHtml(renderedContent, renderedMarkdown, sanitizerConfig);
|
||||
// Rewrite links and images before potentially inserting them into the real dom
|
||||
rewriteRenderedLinks(markdown, options, renderedContent);
|
||||
let outElement;
|
||||
if (target) {
|
||||
outElement = target;
|
||||
reset(target, ...renderedContent.children);
|
||||
}
|
||||
else {
|
||||
outElement = renderedContent;
|
||||
}
|
||||
if (codeBlocks.length > 0) {
|
||||
Promise.all(codeBlocks).then((tuples) => {
|
||||
if (isDisposed) {
|
||||
return;
|
||||
}
|
||||
const renderedElements = new Map(tuples);
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const placeholderElements = outElement.querySelectorAll(`div[data-code]`);
|
||||
for (const placeholderElement of placeholderElements) {
|
||||
const renderedElement = renderedElements.get(placeholderElement.dataset['code'] ?? '');
|
||||
if (renderedElement) {
|
||||
reset(placeholderElement, renderedElement);
|
||||
}
|
||||
}
|
||||
options.asyncRenderCallback?.();
|
||||
});
|
||||
}
|
||||
else if (syncCodeBlocks.length > 0) {
|
||||
const renderedElements = new Map(syncCodeBlocks);
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const placeholderElements = outElement.querySelectorAll(`div[data-code]`);
|
||||
for (const placeholderElement of placeholderElements) {
|
||||
const renderedElement = renderedElements.get(placeholderElement.dataset['code'] ?? '');
|
||||
if (renderedElement) {
|
||||
reset(placeholderElement, renderedElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Signal size changes for image tags
|
||||
if (options.asyncRenderCallback) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const img of outElement.getElementsByTagName('img')) {
|
||||
const listener = disposables.add(addDisposableListener(img, 'load', () => {
|
||||
listener.dispose();
|
||||
options.asyncRenderCallback();
|
||||
}));
|
||||
}
|
||||
}
|
||||
// Add event listeners for links
|
||||
if (options.actionHandler) {
|
||||
const clickCb = (e) => {
|
||||
const mouseEvent = new StandardMouseEvent(getWindow(outElement), e);
|
||||
if (!mouseEvent.leftButton && !mouseEvent.middleButton) {
|
||||
return;
|
||||
}
|
||||
activateLink(markdown, options, mouseEvent);
|
||||
};
|
||||
disposables.add(addDisposableListener(outElement, 'click', clickCb));
|
||||
disposables.add(addDisposableListener(outElement, 'auxclick', clickCb));
|
||||
disposables.add(addDisposableListener(outElement, 'keydown', (e) => {
|
||||
const keyboardEvent = new StandardKeyboardEvent(e);
|
||||
if (!keyboardEvent.equals(10 /* KeyCode.Space */) && !keyboardEvent.equals(3 /* KeyCode.Enter */)) {
|
||||
return;
|
||||
}
|
||||
activateLink(markdown, options, keyboardEvent);
|
||||
}));
|
||||
}
|
||||
// Remove/disable inputs
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const input of [...outElement.getElementsByTagName('input')]) {
|
||||
if (input.attributes.getNamedItem('type')?.value === 'checkbox') {
|
||||
input.setAttribute('disabled', '');
|
||||
}
|
||||
else {
|
||||
if (options.sanitizerConfig?.replaceWithPlaintext) {
|
||||
const replacement = convertTagToPlaintext(input);
|
||||
if (replacement) {
|
||||
input.parentElement?.replaceChild(replacement, input);
|
||||
}
|
||||
else {
|
||||
input.remove();
|
||||
}
|
||||
}
|
||||
else {
|
||||
input.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
element: outElement,
|
||||
dispose: () => {
|
||||
isDisposed = true;
|
||||
disposables.dispose();
|
||||
}
|
||||
};
|
||||
}
|
||||
function rewriteRenderedLinks(markdown, options, root) {
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const el of root.querySelectorAll('img, audio, video, source')) {
|
||||
const src = el.getAttribute('src'); // Get the raw 'src' attribute value as text, not the resolved 'src'
|
||||
if (src) {
|
||||
let href = src;
|
||||
try {
|
||||
if (markdown.baseUri) { // absolute or relative local path, or file: uri
|
||||
href = resolveWithBaseUri(URI.from(markdown.baseUri), href);
|
||||
}
|
||||
}
|
||||
catch (err) { }
|
||||
el.setAttribute('src', massageHref(markdown, href, true));
|
||||
if (options.sanitizerConfig?.remoteImageIsAllowed) {
|
||||
const uri = URI.parse(href);
|
||||
if (uri.scheme !== Schemas.file && uri.scheme !== Schemas.data && !options.sanitizerConfig.remoteImageIsAllowed(uri)) {
|
||||
el.replaceWith($('', undefined, el.outerHTML));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
for (const el of root.querySelectorAll('a')) {
|
||||
const href = el.getAttribute('href'); // Get the raw 'href' attribute value as text, not the resolved 'href'
|
||||
el.setAttribute('href', ''); // Clear out href. We use the `data-href` for handling clicks instead
|
||||
if (!href
|
||||
|| /^data:|javascript:/i.test(href)
|
||||
|| (/^command:/i.test(href) && !markdown.isTrusted)
|
||||
|| /^command:(\/\/\/)?_workbench\.downloadResource/i.test(href)) {
|
||||
// drop the link
|
||||
el.replaceWith(...el.childNodes);
|
||||
}
|
||||
else {
|
||||
let resolvedHref = massageHref(markdown, href, false);
|
||||
if (markdown.baseUri) {
|
||||
resolvedHref = resolveWithBaseUri(URI.from(markdown.baseUri), href);
|
||||
}
|
||||
el.dataset.href = resolvedHref;
|
||||
}
|
||||
}
|
||||
}
|
||||
function createMarkdownRenderer(marked, options, markdown) {
|
||||
const renderer = new marked.Renderer(options.markedOptions);
|
||||
renderer.image = defaultMarkedRenderers.image;
|
||||
renderer.link = defaultMarkedRenderers.link;
|
||||
renderer.paragraph = defaultMarkedRenderers.paragraph;
|
||||
if (markdown.supportAlertSyntax) {
|
||||
renderer.blockquote = createAlertBlockquoteRenderer(renderer.blockquote);
|
||||
}
|
||||
// Will collect [id, renderedElement] tuples
|
||||
const codeBlocks = [];
|
||||
const syncCodeBlocks = [];
|
||||
if (options.codeBlockRendererSync) {
|
||||
renderer.code = ({ text, lang, raw }) => {
|
||||
const id = defaultGenerator.nextId();
|
||||
const value = options.codeBlockRendererSync(postProcessCodeBlockLanguageId(lang), text, raw);
|
||||
syncCodeBlocks.push([id, value]);
|
||||
return `<div class="code" data-code="${id}">${escape(text)}</div>`;
|
||||
};
|
||||
}
|
||||
else if (options.codeBlockRenderer) {
|
||||
renderer.code = ({ text, lang }) => {
|
||||
const id = defaultGenerator.nextId();
|
||||
const value = options.codeBlockRenderer(postProcessCodeBlockLanguageId(lang), text);
|
||||
codeBlocks.push(value.then(element => [id, element]));
|
||||
return `<div class="code" data-code="${id}">${escape(text)}</div>`;
|
||||
};
|
||||
}
|
||||
if (!markdown.supportHtml) {
|
||||
// Note: we always pass the output through dompurify after this so that we don't rely on
|
||||
// marked for real sanitization.
|
||||
renderer.html = ({ text }) => {
|
||||
if (options.sanitizerConfig?.replaceWithPlaintext) {
|
||||
return escape(text);
|
||||
}
|
||||
const match = markdown.isTrusted ? text.match(/^(<span[^>]+>)|(<\/\s*span>)$/) : undefined;
|
||||
return match ? text : '';
|
||||
};
|
||||
}
|
||||
return { renderer, codeBlocks, syncCodeBlocks };
|
||||
}
|
||||
function preprocessMarkdownString(markdown) {
|
||||
let value = markdown.value;
|
||||
// values that are too long will freeze the UI
|
||||
if (value.length > 100_000) {
|
||||
value = `${value.substr(0, 100_000)}…`;
|
||||
}
|
||||
// escape theme icons
|
||||
if (markdown.supportThemeIcons) {
|
||||
value = markdownEscapeEscapedIcons(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function activateLink(mdStr, options, event) {
|
||||
const target = event.target.closest('a[data-href]');
|
||||
if (!isHTMLElement(target)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let href = target.dataset['href'];
|
||||
if (href) {
|
||||
if (mdStr.baseUri) {
|
||||
href = resolveWithBaseUri(URI.from(mdStr.baseUri), href);
|
||||
}
|
||||
options.actionHandler?.(href, mdStr);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
onUnexpectedError(err);
|
||||
}
|
||||
finally {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
function uriMassage(markdown, part) {
|
||||
let data;
|
||||
try {
|
||||
data = parse(decodeURIComponent(part));
|
||||
}
|
||||
catch (e) {
|
||||
// ignore
|
||||
}
|
||||
if (!data) {
|
||||
return part;
|
||||
}
|
||||
data = cloneAndChange(data, value => {
|
||||
if (markdown.uris && markdown.uris[value]) {
|
||||
return URI.revive(markdown.uris[value]);
|
||||
}
|
||||
else {
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
return encodeURIComponent(JSON.stringify(data));
|
||||
}
|
||||
function massageHref(markdown, href, isDomUri) {
|
||||
const data = markdown.uris && markdown.uris[href];
|
||||
let uri = URI.revive(data);
|
||||
if (isDomUri) {
|
||||
if (href.startsWith(Schemas.data + ':')) {
|
||||
return href;
|
||||
}
|
||||
if (!uri) {
|
||||
uri = URI.parse(href);
|
||||
}
|
||||
// this URI will end up as "src"-attribute of a dom node
|
||||
// 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 FileAccess.uriToBrowserUri(uri).toString(true);
|
||||
}
|
||||
if (!uri) {
|
||||
return href;
|
||||
}
|
||||
if (URI.parse(href).toString() === uri.toString()) {
|
||||
return href; // no transformation performed
|
||||
}
|
||||
if (uri.query) {
|
||||
uri = uri.with({ query: uriMassage(markdown, uri.query) });
|
||||
}
|
||||
return uri.toString();
|
||||
}
|
||||
function postProcessCodeBlockLanguageId(lang) {
|
||||
if (!lang) {
|
||||
return '';
|
||||
}
|
||||
const parts = lang.split(/[\s+|:|,|\{|\?]/, 1);
|
||||
if (parts.length) {
|
||||
return parts[0];
|
||||
}
|
||||
return lang;
|
||||
}
|
||||
function resolveWithBaseUri(baseUri, href) {
|
||||
const hasScheme = /^\w[\w\d+.-]*:/.test(href);
|
||||
if (hasScheme) {
|
||||
return href;
|
||||
}
|
||||
if (baseUri.path.endsWith('/')) {
|
||||
return resolvePath(baseUri, href).toString();
|
||||
}
|
||||
else {
|
||||
return resolvePath(dirname(baseUri), href).toString();
|
||||
}
|
||||
}
|
||||
function sanitizeRenderedMarkdown(renderedMarkdown, originalMdStrConfig, options = {}) {
|
||||
const sanitizerConfig = getDomSanitizerConfig(originalMdStrConfig, options);
|
||||
return sanitizeHtml(renderedMarkdown, sanitizerConfig);
|
||||
}
|
||||
const allowedMarkdownHtmlTags = Object.freeze([
|
||||
...basicMarkupHtmlTags,
|
||||
'input', // Allow inputs for rendering checkboxes. Other types of inputs are removed and the inputs are always disabled
|
||||
]);
|
||||
const allowedMarkdownHtmlAttributes = Object.freeze([
|
||||
'align',
|
||||
'autoplay',
|
||||
'alt',
|
||||
'colspan',
|
||||
'controls',
|
||||
'draggable',
|
||||
'height',
|
||||
'href',
|
||||
'loop',
|
||||
'muted',
|
||||
'playsinline',
|
||||
'poster',
|
||||
'rowspan',
|
||||
'src',
|
||||
'target',
|
||||
'title',
|
||||
'type',
|
||||
'width',
|
||||
'start',
|
||||
// Input (For disabled inputs)
|
||||
'checked',
|
||||
'disabled',
|
||||
'value',
|
||||
// Custom markdown attributes
|
||||
'data-code',
|
||||
'data-href',
|
||||
'data-severity',
|
||||
// Only allow very specific styles
|
||||
{
|
||||
attributeName: 'style',
|
||||
shouldKeep: (element, data) => {
|
||||
if (element.tagName === 'SPAN') {
|
||||
if (data.attrName === 'style') {
|
||||
return /^(color\:(#[0-9a-fA-F]+|var\(--vscode(-[a-zA-Z0-9]+)+\));)?(background-color\:(#[0-9a-fA-F]+|var\(--vscode(-[a-zA-Z0-9]+)+\));)?(border-radius:[0-9]+px;)?$/.test(data.attrValue);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
},
|
||||
// Only allow codicons for classes
|
||||
{
|
||||
attributeName: 'class',
|
||||
shouldKeep: (element, data) => {
|
||||
if (element.tagName === 'SPAN') {
|
||||
if (data.attrName === 'class') {
|
||||
return /^codicon codicon-[a-z\-]+( codicon-modifier-[a-z\-]+)?$/.test(data.attrValue);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},
|
||||
]);
|
||||
function getDomSanitizerConfig(mdStrConfig, options) {
|
||||
const isTrusted = mdStrConfig.isTrusted ?? false;
|
||||
const allowedLinkSchemes = [
|
||||
Schemas.http,
|
||||
Schemas.https,
|
||||
Schemas.mailto,
|
||||
Schemas.file,
|
||||
Schemas.vscodeFileResource,
|
||||
Schemas.vscodeRemote,
|
||||
Schemas.vscodeRemoteResource,
|
||||
Schemas.vscodeNotebookCell
|
||||
];
|
||||
if (isTrusted) {
|
||||
allowedLinkSchemes.push(Schemas.command);
|
||||
}
|
||||
if (options.allowedLinkSchemes?.augment) {
|
||||
allowedLinkSchemes.push(...options.allowedLinkSchemes.augment);
|
||||
}
|
||||
return {
|
||||
// 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 dompurify make sure.
|
||||
// HTML tags that can result from markdown are from reading https://spec.commonmark.org/0.29/
|
||||
// HTML table tags that can result from markdown are from https://github.github.com/gfm/#tables-extension-
|
||||
allowedTags: {
|
||||
override: options.allowedTags?.override ?? allowedMarkdownHtmlTags
|
||||
},
|
||||
allowedAttributes: {
|
||||
override: options.allowedAttributes?.override ?? allowedMarkdownHtmlAttributes,
|
||||
},
|
||||
allowedLinkProtocols: {
|
||||
override: allowedLinkSchemes,
|
||||
},
|
||||
allowRelativeLinkPaths: !!mdStrConfig.baseUri,
|
||||
allowedMediaProtocols: {
|
||||
override: [
|
||||
Schemas.http,
|
||||
Schemas.https,
|
||||
Schemas.data,
|
||||
Schemas.file,
|
||||
Schemas.vscodeFileResource,
|
||||
Schemas.vscodeRemote,
|
||||
Schemas.vscodeRemoteResource,
|
||||
]
|
||||
},
|
||||
allowRelativeMediaPaths: !!mdStrConfig.baseUri,
|
||||
replaceWithPlaintext: options.replaceWithPlaintext,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Renders `str` as plaintext, stripping out Markdown syntax if it's a {@link IMarkdownString}.
|
||||
*
|
||||
* For example `# Header` would be output as `Header`.
|
||||
*/
|
||||
function renderAsPlaintext(str, options) {
|
||||
if (typeof str === 'string') {
|
||||
return str;
|
||||
}
|
||||
// values that are too long will freeze the UI
|
||||
let value = str.value ?? '';
|
||||
if (value.length > 100_000) {
|
||||
value = `${value.substr(0, 100_000)}…`;
|
||||
}
|
||||
const html = parse$1(value, { async: false, renderer: plainTextRenderer.value });
|
||||
return sanitizeRenderedMarkdown(html, { isTrusted: false }, {})
|
||||
.toString()
|
||||
.replace(/&(#\d+|[a-zA-Z]+);/g, m => unescapeInfo.get(m) ?? m)
|
||||
.trim();
|
||||
}
|
||||
const unescapeInfo = new Map([
|
||||
['"', '"'],
|
||||
[' ', ' '],
|
||||
['&', '&'],
|
||||
[''', '\''],
|
||||
['<', '<'],
|
||||
['>', '>'],
|
||||
]);
|
||||
function createPlainTextRenderer() {
|
||||
const renderer = new _Renderer();
|
||||
renderer.code = ({ text }) => {
|
||||
return escape(text);
|
||||
};
|
||||
renderer.blockquote = ({ text }) => {
|
||||
return text + '\n';
|
||||
};
|
||||
renderer.html = (_) => {
|
||||
return '';
|
||||
};
|
||||
renderer.heading = function ({ tokens }) {
|
||||
return this.parser.parseInline(tokens) + '\n';
|
||||
};
|
||||
renderer.hr = () => {
|
||||
return '';
|
||||
};
|
||||
renderer.list = function ({ items }) {
|
||||
return items.map(x => this.listitem(x)).join('\n') + '\n';
|
||||
};
|
||||
renderer.listitem = ({ text }) => {
|
||||
return text + '\n';
|
||||
};
|
||||
renderer.paragraph = function ({ tokens }) {
|
||||
return this.parser.parseInline(tokens) + '\n';
|
||||
};
|
||||
renderer.table = function ({ header, rows }) {
|
||||
return header.map(cell => this.tablecell(cell)).join(' ') + '\n' + rows.map(cells => cells.map(cell => this.tablecell(cell)).join(' ')).join('\n') + '\n';
|
||||
};
|
||||
renderer.tablerow = ({ text }) => {
|
||||
return text;
|
||||
};
|
||||
renderer.tablecell = function ({ tokens }) {
|
||||
return this.parser.parseInline(tokens);
|
||||
};
|
||||
renderer.strong = ({ text }) => {
|
||||
return text;
|
||||
};
|
||||
renderer.em = ({ text }) => {
|
||||
return text;
|
||||
};
|
||||
renderer.codespan = ({ text }) => {
|
||||
return escape(text);
|
||||
};
|
||||
renderer.br = (_) => {
|
||||
return '\n';
|
||||
};
|
||||
renderer.del = ({ text }) => {
|
||||
return text;
|
||||
};
|
||||
renderer.image = (_) => {
|
||||
return '';
|
||||
};
|
||||
renderer.text = ({ text }) => {
|
||||
return text;
|
||||
};
|
||||
renderer.link = ({ text }) => {
|
||||
return text;
|
||||
};
|
||||
return renderer;
|
||||
}
|
||||
const plainTextRenderer = new Lazy(createPlainTextRenderer);
|
||||
new Lazy(() => {
|
||||
const renderer = createPlainTextRenderer();
|
||||
renderer.code = ({ text }) => {
|
||||
return `\n\`\`\`\n${escape(text)}\n\`\`\`\n`;
|
||||
};
|
||||
return renderer;
|
||||
});
|
||||
function mergeRawTokenText(tokens) {
|
||||
let mergedTokenText = '';
|
||||
tokens.forEach(token => {
|
||||
mergedTokenText += token.raw;
|
||||
});
|
||||
return mergedTokenText;
|
||||
}
|
||||
function completeSingleLinePattern(token) {
|
||||
if (!token.tokens) {
|
||||
return undefined;
|
||||
}
|
||||
for (let i = token.tokens.length - 1; i >= 0; i--) {
|
||||
const subtoken = token.tokens[i];
|
||||
if (subtoken.type === 'text') {
|
||||
const lines = subtoken.raw.split('\n');
|
||||
const lastLine = lines[lines.length - 1];
|
||||
if (lastLine.includes('`')) {
|
||||
return completeCodespan(token);
|
||||
}
|
||||
else if (lastLine.includes('**')) {
|
||||
return completeDoublestar(token);
|
||||
}
|
||||
else if (lastLine.match(/\*\w/)) {
|
||||
return completeStar(token);
|
||||
}
|
||||
else if (lastLine.match(/(^|\s)__\w/)) {
|
||||
return completeDoubleUnderscore(token);
|
||||
}
|
||||
else if (lastLine.match(/(^|\s)_\w/)) {
|
||||
return completeUnderscore(token);
|
||||
}
|
||||
else if (
|
||||
// Text with start of link target
|
||||
hasLinkTextAndStartOfLinkTarget(lastLine) ||
|
||||
// This token doesn't have the link text, eg if it contains other markdown constructs that are in other subtokens.
|
||||
// But some preceding token does have an unbalanced [ at least
|
||||
hasStartOfLinkTargetAndNoLinkText(lastLine) && token.tokens.slice(0, i).some(t => t.type === 'text' && t.raw.match(/\[[^\]]*$/))) {
|
||||
const nextTwoSubTokens = token.tokens.slice(i + 1);
|
||||
// A markdown link can look like
|
||||
// [link text](https://microsoft.com "more text")
|
||||
// Where "more text" is a title for the link or an argument to a vscode command link
|
||||
if (
|
||||
// If the link was parsed as a link, then look for a link token and a text token with a quote
|
||||
nextTwoSubTokens[0]?.type === 'link' && nextTwoSubTokens[1]?.type === 'text' && nextTwoSubTokens[1].raw.match(/^ *"[^"]*$/) ||
|
||||
// And if the link was not parsed as a link (eg command link), just look for a single quote in this token
|
||||
lastLine.match(/^[^"]* +"[^"]*$/)) {
|
||||
return completeLinkTargetArg(token);
|
||||
}
|
||||
return completeLinkTarget(token);
|
||||
}
|
||||
// Contains the start of link text, and no following tokens contain the link target
|
||||
else if (lastLine.match(/(^|\s)\[\w*[^\]]*$/)) {
|
||||
return completeLinkText(token);
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
function hasLinkTextAndStartOfLinkTarget(str) {
|
||||
return !!str.match(/(^|\s)\[.*\]\(\w*/);
|
||||
}
|
||||
function hasStartOfLinkTargetAndNoLinkText(str) {
|
||||
return !!str.match(/^[^\[]*\]\([^\)]*$/);
|
||||
}
|
||||
function completeListItemPattern(list) {
|
||||
// Patch up this one list item
|
||||
const lastListItem = list.items[list.items.length - 1];
|
||||
const lastListSubToken = lastListItem.tokens ? lastListItem.tokens[lastListItem.tokens.length - 1] : undefined;
|
||||
/*
|
||||
Example list token structures:
|
||||
|
||||
list
|
||||
list_item
|
||||
text
|
||||
text
|
||||
codespan
|
||||
link
|
||||
list_item
|
||||
text
|
||||
code // Complete indented codeblock
|
||||
list_item
|
||||
text
|
||||
space
|
||||
text
|
||||
text // Incomplete indented codeblock
|
||||
list_item
|
||||
text
|
||||
list // Nested list
|
||||
list_item
|
||||
text
|
||||
text
|
||||
|
||||
Contrast with paragraph:
|
||||
paragraph
|
||||
text
|
||||
codespan
|
||||
*/
|
||||
const listEndsInHeading = (list) => {
|
||||
// A list item can be rendered as a heading for some reason when it has a subitem where we haven't rendered the text yet like this:
|
||||
// 1. list item
|
||||
// -
|
||||
const lastItem = list.items.at(-1);
|
||||
const lastToken = lastItem?.tokens.at(-1);
|
||||
return lastToken?.type === 'heading' || lastToken?.type === 'list' && listEndsInHeading(lastToken);
|
||||
};
|
||||
let newToken;
|
||||
if (lastListSubToken?.type === 'text' && !('inRawBlock' in lastListItem)) { // Why does Tag have a type of 'text'
|
||||
newToken = completeSingleLinePattern(lastListSubToken);
|
||||
}
|
||||
else if (listEndsInHeading(list)) {
|
||||
const newList = lexer(list.raw.trim() + ' ')[0];
|
||||
if (newList.type !== 'list') {
|
||||
// Something went wrong
|
||||
return;
|
||||
}
|
||||
return newList;
|
||||
}
|
||||
if (!newToken || newToken.type !== 'paragraph') { // 'text' item inside the list item turns into paragraph
|
||||
// Nothing to fix, or not a pattern we were expecting
|
||||
return;
|
||||
}
|
||||
const previousListItemsText = mergeRawTokenText(list.items.slice(0, -1));
|
||||
// Grabbing the `- ` or `1. ` or `* ` off the list item because I can't find a better way to do this
|
||||
const lastListItemLead = lastListItem.raw.match(/^(\s*(-|\d+\.|\*) +)/)?.[0];
|
||||
if (!lastListItemLead) {
|
||||
// Is badly formatted
|
||||
return;
|
||||
}
|
||||
const newListItemText = lastListItemLead +
|
||||
mergeRawTokenText(lastListItem.tokens.slice(0, -1)) +
|
||||
newToken.raw;
|
||||
const newList = lexer(previousListItemsText + newListItemText)[0];
|
||||
if (newList.type !== 'list') {
|
||||
// Something went wrong
|
||||
return;
|
||||
}
|
||||
return newList;
|
||||
}
|
||||
function completeHeading(token, fullRawText) {
|
||||
if (token.raw.match(/-\s*$/)) {
|
||||
return lexer(fullRawText + ' ');
|
||||
}
|
||||
}
|
||||
const maxIncompleteTokensFixRounds = 3;
|
||||
function fillInIncompleteTokens(tokens) {
|
||||
for (let i = 0; i < maxIncompleteTokensFixRounds; i++) {
|
||||
const newTokens = fillInIncompleteTokensOnce(tokens);
|
||||
if (newTokens) {
|
||||
tokens = newTokens;
|
||||
}
|
||||
else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return tokens;
|
||||
}
|
||||
function fillInIncompleteTokensOnce(tokens) {
|
||||
let i;
|
||||
let newTokens;
|
||||
for (i = 0; i < tokens.length; i++) {
|
||||
const token = tokens[i];
|
||||
if (token.type === 'paragraph' && token.raw.match(/(\n|^)\|/)) {
|
||||
newTokens = completeTable(tokens.slice(i));
|
||||
break;
|
||||
}
|
||||
}
|
||||
const lastToken = tokens.at(-1);
|
||||
if (!newTokens && lastToken?.type === 'list') {
|
||||
const newListToken = completeListItemPattern(lastToken);
|
||||
if (newListToken) {
|
||||
newTokens = [newListToken];
|
||||
i = tokens.length - 1;
|
||||
}
|
||||
}
|
||||
if (!newTokens && lastToken?.type === 'paragraph') {
|
||||
// Only operates on a single token, because any newline that follows this should break these patterns
|
||||
const newToken = completeSingleLinePattern(lastToken);
|
||||
if (newToken) {
|
||||
newTokens = [newToken];
|
||||
i = tokens.length - 1;
|
||||
}
|
||||
}
|
||||
if (newTokens) {
|
||||
const newTokensList = [
|
||||
...tokens.slice(0, i),
|
||||
...newTokens
|
||||
];
|
||||
newTokensList.links = tokens.links;
|
||||
return newTokensList;
|
||||
}
|
||||
if (lastToken?.type === 'heading') {
|
||||
const completeTokens = completeHeading(lastToken, mergeRawTokenText(tokens));
|
||||
if (completeTokens) {
|
||||
return completeTokens;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
function completeCodespan(token) {
|
||||
return completeWithString(token, '`');
|
||||
}
|
||||
function completeStar(tokens) {
|
||||
return completeWithString(tokens, '*');
|
||||
}
|
||||
function completeUnderscore(tokens) {
|
||||
return completeWithString(tokens, '_');
|
||||
}
|
||||
function completeLinkTarget(tokens) {
|
||||
return completeWithString(tokens, ')', false);
|
||||
}
|
||||
function completeLinkTargetArg(tokens) {
|
||||
return completeWithString(tokens, '")', false);
|
||||
}
|
||||
function completeLinkText(tokens) {
|
||||
return completeWithString(tokens, '](https://microsoft.com)', false);
|
||||
}
|
||||
function completeDoublestar(tokens) {
|
||||
return completeWithString(tokens, '**');
|
||||
}
|
||||
function completeDoubleUnderscore(tokens) {
|
||||
return completeWithString(tokens, '__');
|
||||
}
|
||||
function completeWithString(tokens, closingString, shouldTrim = true) {
|
||||
const mergedRawText = mergeRawTokenText(Array.isArray(tokens) ? tokens : [tokens]);
|
||||
// If it was completed correctly, this should be a single token.
|
||||
// Expecting either a Paragraph or a List
|
||||
const trimmedRawText = shouldTrim ? mergedRawText.trimEnd() : mergedRawText;
|
||||
return lexer(trimmedRawText + closingString)[0];
|
||||
}
|
||||
function completeTable(tokens) {
|
||||
const mergedRawText = mergeRawTokenText(tokens);
|
||||
const lines = mergedRawText.split('\n');
|
||||
let numCols; // The number of line1 col headers
|
||||
let hasSeparatorRow = false;
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
if (typeof numCols === 'undefined' && line.match(/^\s*\|/)) {
|
||||
const line1Matches = line.match(/(\|[^\|]+)(?=\||$)/g);
|
||||
if (line1Matches) {
|
||||
numCols = line1Matches.length;
|
||||
}
|
||||
}
|
||||
else if (typeof numCols === 'number') {
|
||||
if (line.match(/^\s*\|/)) {
|
||||
if (i !== lines.length - 1) {
|
||||
// We got the line1 header row, and the line2 separator row, but there are more lines, and it wasn't parsed as a table!
|
||||
// That's strange and means that the table is probably malformed in the source, so I won't try to patch it up.
|
||||
return undefined;
|
||||
}
|
||||
// Got a line2 separator row- partial or complete, doesn't matter, we'll replace it with a correct one
|
||||
hasSeparatorRow = true;
|
||||
}
|
||||
else {
|
||||
// The line after the header row isn't a valid separator row, so the table is malformed, don't fix it up
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof numCols === 'number' && numCols > 0) {
|
||||
const prefixText = hasSeparatorRow ? lines.slice(0, -1).join('\n') : mergedRawText;
|
||||
const line1EndsInPipe = !!prefixText.match(/\|\s*$/);
|
||||
const newRawText = prefixText + (line1EndsInPipe ? '' : '|') + `\n|${' --- |'.repeat(numCols)}`;
|
||||
return lexer(newRawText);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export { allowedMarkdownHtmlAttributes, allowedMarkdownHtmlTags, fillInIncompleteTokens, renderAsPlaintext, renderMarkdown };
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
import { isChrome, isFirefox, isSafari } from './browser.js';
|
||||
import { IframeUtils } from './iframe.js';
|
||||
import { isMacintosh, isWindows } from '../common/platform.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class StandardMouseEvent {
|
||||
constructor(targetWindow, e) {
|
||||
this.timestamp = Date.now();
|
||||
this.browserEvent = e;
|
||||
this.leftButton = e.button === 0;
|
||||
this.middleButton = e.button === 1;
|
||||
this.rightButton = e.button === 2;
|
||||
this.buttons = e.buttons;
|
||||
this.defaultPrevented = e.defaultPrevented;
|
||||
this.target = e.target;
|
||||
this.detail = e.detail || 1;
|
||||
if (e.type === 'dblclick') {
|
||||
this.detail = 2;
|
||||
}
|
||||
this.ctrlKey = e.ctrlKey;
|
||||
this.shiftKey = e.shiftKey;
|
||||
this.altKey = e.altKey;
|
||||
this.metaKey = e.metaKey;
|
||||
if (typeof e.pageX === 'number') {
|
||||
this.posx = e.pageX;
|
||||
this.posy = e.pageY;
|
||||
}
|
||||
else {
|
||||
// Probably hit by MSGestureEvent
|
||||
this.posx = e.clientX + this.target.ownerDocument.body.scrollLeft + this.target.ownerDocument.documentElement.scrollLeft;
|
||||
this.posy = e.clientY + this.target.ownerDocument.body.scrollTop + this.target.ownerDocument.documentElement.scrollTop;
|
||||
}
|
||||
// Find the position of the iframe this code is executing in relative to the iframe where the event was captured.
|
||||
const iframeOffsets = IframeUtils.getPositionOfChildWindowRelativeToAncestorWindow(targetWindow, e.view);
|
||||
this.posx -= iframeOffsets.left;
|
||||
this.posy -= iframeOffsets.top;
|
||||
}
|
||||
preventDefault() {
|
||||
this.browserEvent.preventDefault();
|
||||
}
|
||||
stopPropagation() {
|
||||
this.browserEvent.stopPropagation();
|
||||
}
|
||||
}
|
||||
class StandardWheelEvent {
|
||||
constructor(e, deltaX = 0, deltaY = 0) {
|
||||
this.browserEvent = e || null;
|
||||
// eslint-disable-next-line local/code-no-any-casts
|
||||
this.target = e ? (e.target || e.targetNode || e.srcElement) : null;
|
||||
this.deltaY = deltaY;
|
||||
this.deltaX = deltaX;
|
||||
let shouldFactorDPR = false;
|
||||
if (isChrome) {
|
||||
// Chrome version >= 123 contains the fix to factor devicePixelRatio into the wheel event.
|
||||
// See https://chromium.googlesource.com/chromium/src.git/+/be51b448441ff0c9d1f17e0f25c4bf1ab3f11f61
|
||||
const chromeVersionMatch = navigator.userAgent.match(/Chrome\/(\d+)/);
|
||||
const chromeMajorVersion = chromeVersionMatch ? parseInt(chromeVersionMatch[1]) : 123;
|
||||
shouldFactorDPR = chromeMajorVersion <= 122;
|
||||
}
|
||||
if (e) {
|
||||
// Old (deprecated) wheel events
|
||||
// eslint-disable-next-line local/code-no-any-casts
|
||||
const e1 = e;
|
||||
// eslint-disable-next-line local/code-no-any-casts
|
||||
const e2 = e;
|
||||
const devicePixelRatio = e.view?.devicePixelRatio || 1;
|
||||
// vertical delta scroll
|
||||
if (typeof e1.wheelDeltaY !== 'undefined') {
|
||||
if (shouldFactorDPR) {
|
||||
// Refs https://github.com/microsoft/vscode/issues/146403#issuecomment-1854538928
|
||||
this.deltaY = e1.wheelDeltaY / (120 * devicePixelRatio);
|
||||
}
|
||||
else {
|
||||
this.deltaY = e1.wheelDeltaY / 120;
|
||||
}
|
||||
}
|
||||
else if (typeof e2.VERTICAL_AXIS !== 'undefined' && e2.axis === e2.VERTICAL_AXIS) {
|
||||
this.deltaY = -e2.detail / 3;
|
||||
}
|
||||
else if (e.type === 'wheel') {
|
||||
// Modern wheel event
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
|
||||
const ev = e;
|
||||
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
|
||||
// the deltas are expressed in lines
|
||||
if (isFirefox && !isMacintosh) {
|
||||
this.deltaY = -e.deltaY / 3;
|
||||
}
|
||||
else {
|
||||
this.deltaY = -e.deltaY;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.deltaY = -e.deltaY / 40;
|
||||
}
|
||||
}
|
||||
// horizontal delta scroll
|
||||
if (typeof e1.wheelDeltaX !== 'undefined') {
|
||||
if (isSafari && isWindows) {
|
||||
this.deltaX = -(e1.wheelDeltaX / 120);
|
||||
}
|
||||
else if (shouldFactorDPR) {
|
||||
// Refs https://github.com/microsoft/vscode/issues/146403#issuecomment-1854538928
|
||||
this.deltaX = e1.wheelDeltaX / (120 * devicePixelRatio);
|
||||
}
|
||||
else {
|
||||
this.deltaX = e1.wheelDeltaX / 120;
|
||||
}
|
||||
}
|
||||
else if (typeof e2.HORIZONTAL_AXIS !== 'undefined' && e2.axis === e2.HORIZONTAL_AXIS) {
|
||||
this.deltaX = -e.detail / 3;
|
||||
}
|
||||
else if (e.type === 'wheel') {
|
||||
// Modern wheel event
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/WheelEvent
|
||||
const ev = e;
|
||||
if (ev.deltaMode === ev.DOM_DELTA_LINE) {
|
||||
// the deltas are expressed in lines
|
||||
if (isFirefox && !isMacintosh) {
|
||||
this.deltaX = -e.deltaX / 3;
|
||||
}
|
||||
else {
|
||||
this.deltaX = -e.deltaX;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.deltaX = -e.deltaX / 40;
|
||||
}
|
||||
}
|
||||
// Assume a vertical scroll if nothing else worked
|
||||
if (this.deltaY === 0 && this.deltaX === 0 && e.wheelDelta) {
|
||||
if (shouldFactorDPR) {
|
||||
// Refs https://github.com/microsoft/vscode/issues/146403#issuecomment-1854538928
|
||||
this.deltaY = e.wheelDelta / (120 * devicePixelRatio);
|
||||
}
|
||||
else {
|
||||
this.deltaY = e.wheelDelta / 120;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
preventDefault() {
|
||||
this.browserEvent?.preventDefault();
|
||||
}
|
||||
stopPropagation() {
|
||||
this.browserEvent?.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
export { StandardMouseEvent, StandardWheelEvent };
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
var inputLatency;
|
||||
(function (inputLatency) {
|
||||
const totalKeydownTime = { total: 0, min: Number.MAX_VALUE, max: 0 };
|
||||
const totalInputTime = { ...totalKeydownTime };
|
||||
const totalRenderTime = { ...totalKeydownTime };
|
||||
const totalInputLatencyTime = { ...totalKeydownTime };
|
||||
let measurementsCount = 0;
|
||||
const state = {
|
||||
keydown: 0 /* EventPhase.Before */,
|
||||
input: 0 /* EventPhase.Before */,
|
||||
render: 0 /* EventPhase.Before */,
|
||||
};
|
||||
/**
|
||||
* Record the start of the keydown event.
|
||||
*/
|
||||
function onKeyDown() {
|
||||
/** Direct Check C. See explanation in {@link recordIfFinished} */
|
||||
recordIfFinished();
|
||||
performance.mark('inputlatency/start');
|
||||
performance.mark('keydown/start');
|
||||
state.keydown = 1 /* EventPhase.InProgress */;
|
||||
queueMicrotask(markKeyDownEnd);
|
||||
}
|
||||
inputLatency.onKeyDown = onKeyDown;
|
||||
/**
|
||||
* Mark the end of the keydown event.
|
||||
*/
|
||||
function markKeyDownEnd() {
|
||||
if (state.keydown === 1 /* EventPhase.InProgress */) {
|
||||
performance.mark('keydown/end');
|
||||
state.keydown = 2 /* EventPhase.Finished */;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Record the start of the beforeinput event.
|
||||
*/
|
||||
function onBeforeInput() {
|
||||
performance.mark('input/start');
|
||||
state.input = 1 /* EventPhase.InProgress */;
|
||||
/** Schedule Task A. See explanation in {@link recordIfFinished} */
|
||||
scheduleRecordIfFinishedTask();
|
||||
}
|
||||
inputLatency.onBeforeInput = onBeforeInput;
|
||||
/**
|
||||
* Record the start of the input event.
|
||||
*/
|
||||
function onInput() {
|
||||
if (state.input === 0 /* EventPhase.Before */) {
|
||||
// it looks like we didn't receive a `beforeinput`
|
||||
onBeforeInput();
|
||||
}
|
||||
queueMicrotask(markInputEnd);
|
||||
}
|
||||
inputLatency.onInput = onInput;
|
||||
function markInputEnd() {
|
||||
if (state.input === 1 /* EventPhase.InProgress */) {
|
||||
performance.mark('input/end');
|
||||
state.input = 2 /* EventPhase.Finished */;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Record the start of the keyup event.
|
||||
*/
|
||||
function onKeyUp() {
|
||||
/** Direct Check D. See explanation in {@link recordIfFinished} */
|
||||
recordIfFinished();
|
||||
}
|
||||
inputLatency.onKeyUp = onKeyUp;
|
||||
/**
|
||||
* Record the start of the selectionchange event.
|
||||
*/
|
||||
function onSelectionChange() {
|
||||
/** Direct Check E. See explanation in {@link recordIfFinished} */
|
||||
recordIfFinished();
|
||||
}
|
||||
inputLatency.onSelectionChange = onSelectionChange;
|
||||
/**
|
||||
* Record the start of the animation frame performing the rendering.
|
||||
*/
|
||||
function onRenderStart() {
|
||||
// Render may be triggered during input, but we only measure the following animation frame
|
||||
if (state.keydown === 2 /* EventPhase.Finished */ && state.input === 2 /* EventPhase.Finished */ && state.render === 0 /* EventPhase.Before */) {
|
||||
// Only measure the first render after keyboard input
|
||||
performance.mark('render/start');
|
||||
state.render = 1 /* EventPhase.InProgress */;
|
||||
queueMicrotask(markRenderEnd);
|
||||
/** Schedule Task B. See explanation in {@link recordIfFinished} */
|
||||
scheduleRecordIfFinishedTask();
|
||||
}
|
||||
}
|
||||
inputLatency.onRenderStart = onRenderStart;
|
||||
/**
|
||||
* Mark the end of the animation frame performing the rendering.
|
||||
*/
|
||||
function markRenderEnd() {
|
||||
if (state.render === 1 /* EventPhase.InProgress */) {
|
||||
performance.mark('render/end');
|
||||
state.render = 2 /* EventPhase.Finished */;
|
||||
}
|
||||
}
|
||||
function scheduleRecordIfFinishedTask() {
|
||||
// Here we can safely assume that the `setTimeout` will not be
|
||||
// artificially delayed by 4ms because we schedule it from
|
||||
// event handlers
|
||||
setTimeout(recordIfFinished);
|
||||
}
|
||||
/**
|
||||
* Record the input latency sample if input handling and rendering are finished.
|
||||
*
|
||||
* The challenge here is that we want to record the latency in such a way that it includes
|
||||
* also the layout and painting work the browser does during the animation frame task.
|
||||
*
|
||||
* Simply scheduling a new task (via `setTimeout`) from the animation frame task would
|
||||
* schedule the new task at the end of the task queue (after other code that uses `setTimeout`),
|
||||
* so we need to use multiple strategies to make sure our task runs before others:
|
||||
*
|
||||
* We schedule tasks (A and B):
|
||||
* - we schedule a task A (via a `setTimeout` call) when the input starts in `markInputStart`.
|
||||
* If the animation frame task is scheduled quickly by the browser, then task A has a very good
|
||||
* chance of being the very first task after the animation frame and thus will record the input latency.
|
||||
* - however, if the animation frame task is scheduled a bit later, then task A might execute
|
||||
* before the animation frame task. We therefore schedule another task B from `markRenderStart`.
|
||||
*
|
||||
* We do direct checks in browser event handlers (C, D, E):
|
||||
* - if the browser has multiple keydown events queued up, they will be scheduled before the `setTimeout` tasks,
|
||||
* so we do a direct check in the keydown event handler (C).
|
||||
* - depending on timing, sometimes the animation frame is scheduled even before the `keyup` event, so we
|
||||
* do a direct check there too (E).
|
||||
* - the browser oftentimes emits a `selectionchange` event after an `input`, so we do a direct check there (D).
|
||||
*/
|
||||
function recordIfFinished() {
|
||||
if (state.keydown === 2 /* EventPhase.Finished */ && state.input === 2 /* EventPhase.Finished */ && state.render === 2 /* EventPhase.Finished */) {
|
||||
performance.mark('inputlatency/end');
|
||||
performance.measure('keydown', 'keydown/start', 'keydown/end');
|
||||
performance.measure('input', 'input/start', 'input/end');
|
||||
performance.measure('render', 'render/start', 'render/end');
|
||||
performance.measure('inputlatency', 'inputlatency/start', 'inputlatency/end');
|
||||
addMeasure('keydown', totalKeydownTime);
|
||||
addMeasure('input', totalInputTime);
|
||||
addMeasure('render', totalRenderTime);
|
||||
addMeasure('inputlatency', totalInputLatencyTime);
|
||||
// console.info(
|
||||
// `input latency=${performance.getEntriesByName('inputlatency')[0].duration.toFixed(1)} [` +
|
||||
// `keydown=${performance.getEntriesByName('keydown')[0].duration.toFixed(1)}, ` +
|
||||
// `input=${performance.getEntriesByName('input')[0].duration.toFixed(1)}, ` +
|
||||
// `render=${performance.getEntriesByName('render')[0].duration.toFixed(1)}` +
|
||||
// `]`
|
||||
// );
|
||||
measurementsCount++;
|
||||
reset();
|
||||
}
|
||||
}
|
||||
function addMeasure(entryName, cumulativeMeasurement) {
|
||||
const duration = performance.getEntriesByName(entryName)[0].duration;
|
||||
cumulativeMeasurement.total += duration;
|
||||
cumulativeMeasurement.min = Math.min(cumulativeMeasurement.min, duration);
|
||||
cumulativeMeasurement.max = Math.max(cumulativeMeasurement.max, duration);
|
||||
}
|
||||
/**
|
||||
* Clear the current sample.
|
||||
*/
|
||||
function reset() {
|
||||
performance.clearMarks('keydown/start');
|
||||
performance.clearMarks('keydown/end');
|
||||
performance.clearMarks('input/start');
|
||||
performance.clearMarks('input/end');
|
||||
performance.clearMarks('render/start');
|
||||
performance.clearMarks('render/end');
|
||||
performance.clearMarks('inputlatency/start');
|
||||
performance.clearMarks('inputlatency/end');
|
||||
performance.clearMeasures('keydown');
|
||||
performance.clearMeasures('input');
|
||||
performance.clearMeasures('render');
|
||||
performance.clearMeasures('inputlatency');
|
||||
state.keydown = 0 /* EventPhase.Before */;
|
||||
state.input = 0 /* EventPhase.Before */;
|
||||
state.render = 0 /* EventPhase.Before */;
|
||||
}
|
||||
/**
|
||||
* Gets all input latency samples and clears the internal buffers to start recording a new set
|
||||
* of samples.
|
||||
*/
|
||||
function getAndClearMeasurements() {
|
||||
if (measurementsCount === 0) {
|
||||
return undefined;
|
||||
}
|
||||
// Assemble the result
|
||||
const result = {
|
||||
keydown: cumulativeToFinalMeasurement(totalKeydownTime),
|
||||
input: cumulativeToFinalMeasurement(totalInputTime),
|
||||
render: cumulativeToFinalMeasurement(totalRenderTime),
|
||||
total: cumulativeToFinalMeasurement(totalInputLatencyTime),
|
||||
sampleCount: measurementsCount
|
||||
};
|
||||
// Clear the cumulative measurements
|
||||
clearCumulativeMeasurement(totalKeydownTime);
|
||||
clearCumulativeMeasurement(totalInputTime);
|
||||
clearCumulativeMeasurement(totalRenderTime);
|
||||
clearCumulativeMeasurement(totalInputLatencyTime);
|
||||
measurementsCount = 0;
|
||||
return result;
|
||||
}
|
||||
inputLatency.getAndClearMeasurements = getAndClearMeasurements;
|
||||
function cumulativeToFinalMeasurement(cumulative) {
|
||||
return {
|
||||
average: cumulative.total / measurementsCount,
|
||||
max: cumulative.max,
|
||||
min: cumulative.min,
|
||||
};
|
||||
}
|
||||
function clearCumulativeMeasurement(cumulative) {
|
||||
cumulative.total = 0;
|
||||
cumulative.min = Number.MAX_VALUE;
|
||||
cumulative.max = 0;
|
||||
}
|
||||
})(inputLatency || (inputLatency = {}));
|
||||
|
||||
export { inputLatency };
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import { getWindowId, onDidUnregisterWindow } from './dom.js';
|
||||
import { Event, Emitter } from '../common/event.js';
|
||||
import { markAsSingleton, Disposable } from '../common/lifecycle.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
/**
|
||||
* See https://developer.mozilla.org/en-US/docs/Web/API/Window/devicePixelRatio#monitoring_screen_resolution_or_zoom_level_changes
|
||||
*/
|
||||
class DevicePixelRatioMonitor extends Disposable {
|
||||
constructor(targetWindow) {
|
||||
super();
|
||||
this._onDidChange = this._register(new Emitter());
|
||||
this.onDidChange = this._onDidChange.event;
|
||||
this._listener = () => this._handleChange(targetWindow, true);
|
||||
this._mediaQueryList = null;
|
||||
this._handleChange(targetWindow, false);
|
||||
}
|
||||
_handleChange(targetWindow, fireEvent) {
|
||||
this._mediaQueryList?.removeEventListener('change', this._listener);
|
||||
this._mediaQueryList = targetWindow.matchMedia(`(resolution: ${targetWindow.devicePixelRatio}dppx)`);
|
||||
this._mediaQueryList.addEventListener('change', this._listener);
|
||||
if (fireEvent) {
|
||||
this._onDidChange.fire();
|
||||
}
|
||||
}
|
||||
}
|
||||
class PixelRatioMonitorImpl extends Disposable {
|
||||
get value() {
|
||||
return this._value;
|
||||
}
|
||||
constructor(targetWindow) {
|
||||
super();
|
||||
this._onDidChange = this._register(new Emitter());
|
||||
this.onDidChange = this._onDidChange.event;
|
||||
this._value = this._getPixelRatio(targetWindow);
|
||||
const dprMonitor = this._register(new DevicePixelRatioMonitor(targetWindow));
|
||||
this._register(dprMonitor.onDidChange(() => {
|
||||
this._value = this._getPixelRatio(targetWindow);
|
||||
this._onDidChange.fire(this._value);
|
||||
}));
|
||||
}
|
||||
_getPixelRatio(targetWindow) {
|
||||
const ctx = document.createElement('canvas').getContext('2d');
|
||||
const dpr = targetWindow.devicePixelRatio || 1;
|
||||
const bsr = ctx.webkitBackingStorePixelRatio ||
|
||||
ctx.mozBackingStorePixelRatio ||
|
||||
ctx.msBackingStorePixelRatio ||
|
||||
ctx.oBackingStorePixelRatio ||
|
||||
ctx.backingStorePixelRatio || 1;
|
||||
return dpr / bsr;
|
||||
}
|
||||
}
|
||||
class PixelRatioMonitorFacade {
|
||||
constructor() {
|
||||
this.mapWindowIdToPixelRatioMonitor = new Map();
|
||||
}
|
||||
_getOrCreatePixelRatioMonitor(targetWindow) {
|
||||
const targetWindowId = getWindowId(targetWindow);
|
||||
let pixelRatioMonitor = this.mapWindowIdToPixelRatioMonitor.get(targetWindowId);
|
||||
if (!pixelRatioMonitor) {
|
||||
pixelRatioMonitor = markAsSingleton(new PixelRatioMonitorImpl(targetWindow));
|
||||
this.mapWindowIdToPixelRatioMonitor.set(targetWindowId, pixelRatioMonitor);
|
||||
markAsSingleton(Event.once(onDidUnregisterWindow)(({ vscodeWindowId }) => {
|
||||
if (vscodeWindowId === targetWindowId) {
|
||||
pixelRatioMonitor?.dispose();
|
||||
this.mapWindowIdToPixelRatioMonitor.delete(targetWindowId);
|
||||
}
|
||||
}));
|
||||
}
|
||||
return pixelRatioMonitor;
|
||||
}
|
||||
getInstance(targetWindow) {
|
||||
return this._getOrCreatePixelRatioMonitor(targetWindow);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Returns the pixel ratio.
|
||||
*
|
||||
* This is useful for rendering <canvas> elements at native screen resolution or for being used as
|
||||
* a cache key when storing font measurements. Fonts might render differently depending on resolution
|
||||
* and any measurements need to be discarded for example when a window is moved from a monitor to another.
|
||||
*/
|
||||
const PixelRatio = new PixelRatioMonitorFacade();
|
||||
|
||||
export { PixelRatio };
|
||||
+270
@@ -0,0 +1,270 @@
|
||||
import { onDidRegisterWindow, addDisposableListener, scheduleAtNextAnimationFrame } from './dom.js';
|
||||
import { mainWindow } from './window.js';
|
||||
import { memoize } from '../common/decorators.js';
|
||||
import { Event } from '../common/event.js';
|
||||
import { Disposable, markAsSingleton, toDisposable } from '../common/lifecycle.js';
|
||||
import { LinkedList } from '../common/linkedList.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
var EventType;
|
||||
(function (EventType) {
|
||||
EventType.Tap = '-monaco-gesturetap';
|
||||
EventType.Change = '-monaco-gesturechange';
|
||||
EventType.Start = '-monaco-gesturestart';
|
||||
EventType.End = '-monaco-gesturesend';
|
||||
EventType.Contextmenu = '-monaco-gesturecontextmenu';
|
||||
})(EventType || (EventType = {}));
|
||||
class Gesture extends Disposable {
|
||||
static { this.SCROLL_FRICTION = -5e-3; }
|
||||
static { this.HOLD_DELAY = 700; }
|
||||
static { this.CLEAR_TAP_COUNT_TIME = 400; } // ms
|
||||
constructor() {
|
||||
super();
|
||||
this.dispatched = false;
|
||||
this.targets = new LinkedList();
|
||||
this.ignoreTargets = new LinkedList();
|
||||
this.activeTouches = {};
|
||||
this.handle = null;
|
||||
this._lastSetTapCountTime = 0;
|
||||
this._register(Event.runAndSubscribe(onDidRegisterWindow, ({ window, disposables }) => {
|
||||
disposables.add(addDisposableListener(window.document, 'touchstart', (e) => this.onTouchStart(e), { passive: false }));
|
||||
disposables.add(addDisposableListener(window.document, 'touchend', (e) => this.onTouchEnd(window, e)));
|
||||
disposables.add(addDisposableListener(window.document, 'touchmove', (e) => this.onTouchMove(e), { passive: false }));
|
||||
}, { window: mainWindow, disposables: this._store }));
|
||||
}
|
||||
static addTarget(element) {
|
||||
if (!Gesture.isTouchDevice()) {
|
||||
return Disposable.None;
|
||||
}
|
||||
if (!Gesture.INSTANCE) {
|
||||
Gesture.INSTANCE = markAsSingleton(new Gesture());
|
||||
}
|
||||
const remove = Gesture.INSTANCE.targets.push(element);
|
||||
return toDisposable(remove);
|
||||
}
|
||||
static ignoreTarget(element) {
|
||||
if (!Gesture.isTouchDevice()) {
|
||||
return Disposable.None;
|
||||
}
|
||||
if (!Gesture.INSTANCE) {
|
||||
Gesture.INSTANCE = markAsSingleton(new Gesture());
|
||||
}
|
||||
const remove = Gesture.INSTANCE.ignoreTargets.push(element);
|
||||
return toDisposable(remove);
|
||||
}
|
||||
static isTouchDevice() {
|
||||
// `'ontouchstart' in window` always evaluates to true with typescript's modern typings. This causes `window` to be
|
||||
// `never` later in `window.navigator`. That's why we need the explicit `window as Window` cast
|
||||
return 'ontouchstart' in mainWindow || navigator.maxTouchPoints > 0;
|
||||
}
|
||||
dispose() {
|
||||
if (this.handle) {
|
||||
this.handle.dispose();
|
||||
this.handle = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
onTouchStart(e) {
|
||||
const timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
|
||||
if (this.handle) {
|
||||
this.handle.dispose();
|
||||
this.handle = null;
|
||||
}
|
||||
for (let i = 0, len = e.targetTouches.length; i < len; i++) {
|
||||
const touch = e.targetTouches.item(i);
|
||||
this.activeTouches[touch.identifier] = {
|
||||
id: touch.identifier,
|
||||
initialTarget: touch.target,
|
||||
initialTimeStamp: timestamp,
|
||||
initialPageX: touch.pageX,
|
||||
initialPageY: touch.pageY,
|
||||
rollingTimestamps: [timestamp],
|
||||
rollingPageX: [touch.pageX],
|
||||
rollingPageY: [touch.pageY]
|
||||
};
|
||||
const evt = this.newGestureEvent(EventType.Start, touch.target);
|
||||
evt.pageX = touch.pageX;
|
||||
evt.pageY = touch.pageY;
|
||||
this.dispatchEvent(evt);
|
||||
}
|
||||
if (this.dispatched) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dispatched = false;
|
||||
}
|
||||
}
|
||||
onTouchEnd(targetWindow, e) {
|
||||
const timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
|
||||
const activeTouchCount = Object.keys(this.activeTouches).length;
|
||||
for (let i = 0, len = e.changedTouches.length; i < len; i++) {
|
||||
const touch = e.changedTouches.item(i);
|
||||
if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) {
|
||||
console.warn('move of an UNKNOWN touch', touch);
|
||||
continue;
|
||||
}
|
||||
const data = this.activeTouches[touch.identifier], holdTime = Date.now() - data.initialTimeStamp;
|
||||
if (holdTime < Gesture.HOLD_DELAY
|
||||
&& Math.abs(data.initialPageX - data.rollingPageX.at(-1)) < 30
|
||||
&& Math.abs(data.initialPageY - data.rollingPageY.at(-1)) < 30) {
|
||||
const evt = this.newGestureEvent(EventType.Tap, data.initialTarget);
|
||||
evt.pageX = data.rollingPageX.at(-1);
|
||||
evt.pageY = data.rollingPageY.at(-1);
|
||||
this.dispatchEvent(evt);
|
||||
}
|
||||
else if (holdTime >= Gesture.HOLD_DELAY
|
||||
&& Math.abs(data.initialPageX - data.rollingPageX.at(-1)) < 30
|
||||
&& Math.abs(data.initialPageY - data.rollingPageY.at(-1)) < 30) {
|
||||
const evt = this.newGestureEvent(EventType.Contextmenu, data.initialTarget);
|
||||
evt.pageX = data.rollingPageX.at(-1);
|
||||
evt.pageY = data.rollingPageY.at(-1);
|
||||
this.dispatchEvent(evt);
|
||||
}
|
||||
else if (activeTouchCount === 1) {
|
||||
const finalX = data.rollingPageX.at(-1);
|
||||
const finalY = data.rollingPageY.at(-1);
|
||||
const deltaT = data.rollingTimestamps.at(-1) - data.rollingTimestamps[0];
|
||||
const deltaX = finalX - data.rollingPageX[0];
|
||||
const deltaY = finalY - data.rollingPageY[0];
|
||||
// We need to get all the dispatch targets on the start of the inertia event
|
||||
const dispatchTo = [...this.targets].filter(t => data.initialTarget instanceof Node && t.contains(data.initialTarget));
|
||||
this.inertia(targetWindow, dispatchTo, timestamp, // time now
|
||||
Math.abs(deltaX) / deltaT, // speed
|
||||
deltaX > 0 ? 1 : -1, // x direction
|
||||
finalX, // x now
|
||||
Math.abs(deltaY) / deltaT, // y speed
|
||||
deltaY > 0 ? 1 : -1, // y direction
|
||||
finalY // y now
|
||||
);
|
||||
}
|
||||
this.dispatchEvent(this.newGestureEvent(EventType.End, data.initialTarget));
|
||||
// forget about this touch
|
||||
delete this.activeTouches[touch.identifier];
|
||||
}
|
||||
if (this.dispatched) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dispatched = false;
|
||||
}
|
||||
}
|
||||
newGestureEvent(type, initialTarget) {
|
||||
const event = document.createEvent('CustomEvent');
|
||||
event.initEvent(type, false, true);
|
||||
event.initialTarget = initialTarget;
|
||||
event.tapCount = 0;
|
||||
return event;
|
||||
}
|
||||
dispatchEvent(event) {
|
||||
if (event.type === EventType.Tap) {
|
||||
const currentTime = (new Date()).getTime();
|
||||
let setTapCount = 0;
|
||||
if (currentTime - this._lastSetTapCountTime > Gesture.CLEAR_TAP_COUNT_TIME) {
|
||||
setTapCount = 1;
|
||||
}
|
||||
else {
|
||||
setTapCount = 2;
|
||||
}
|
||||
this._lastSetTapCountTime = currentTime;
|
||||
event.tapCount = setTapCount;
|
||||
}
|
||||
else if (event.type === EventType.Change || event.type === EventType.Contextmenu) {
|
||||
// tap is canceled by scrolling or context menu
|
||||
this._lastSetTapCountTime = 0;
|
||||
}
|
||||
if (event.initialTarget instanceof Node) {
|
||||
for (const ignoreTarget of this.ignoreTargets) {
|
||||
if (ignoreTarget.contains(event.initialTarget)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const targets = [];
|
||||
for (const target of this.targets) {
|
||||
if (target.contains(event.initialTarget)) {
|
||||
let depth = 0;
|
||||
let now = event.initialTarget;
|
||||
while (now && now !== target) {
|
||||
depth++;
|
||||
now = now.parentElement;
|
||||
}
|
||||
targets.push([depth, target]);
|
||||
}
|
||||
}
|
||||
targets.sort((a, b) => a[0] - b[0]);
|
||||
for (const [_, target] of targets) {
|
||||
target.dispatchEvent(event);
|
||||
this.dispatched = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
inertia(targetWindow, dispatchTo, t1, vX, dirX, x, vY, dirY, y) {
|
||||
this.handle = scheduleAtNextAnimationFrame(targetWindow, () => {
|
||||
const now = Date.now();
|
||||
// velocity: old speed + accel_over_time
|
||||
const deltaT = now - t1;
|
||||
let delta_pos_x = 0, delta_pos_y = 0;
|
||||
let stopped = true;
|
||||
vX += Gesture.SCROLL_FRICTION * deltaT;
|
||||
vY += Gesture.SCROLL_FRICTION * deltaT;
|
||||
if (vX > 0) {
|
||||
stopped = false;
|
||||
delta_pos_x = dirX * vX * deltaT;
|
||||
}
|
||||
if (vY > 0) {
|
||||
stopped = false;
|
||||
delta_pos_y = dirY * vY * deltaT;
|
||||
}
|
||||
// dispatch translation event
|
||||
const evt = this.newGestureEvent(EventType.Change);
|
||||
evt.translationX = delta_pos_x;
|
||||
evt.translationY = delta_pos_y;
|
||||
dispatchTo.forEach(d => d.dispatchEvent(evt));
|
||||
if (!stopped) {
|
||||
this.inertia(targetWindow, dispatchTo, now, vX, dirX, x + delta_pos_x, vY, dirY, y + delta_pos_y);
|
||||
}
|
||||
});
|
||||
}
|
||||
onTouchMove(e) {
|
||||
const timestamp = Date.now(); // use Date.now() because on FF e.timeStamp is not epoch based.
|
||||
for (let i = 0, len = e.changedTouches.length; i < len; i++) {
|
||||
const touch = e.changedTouches.item(i);
|
||||
if (!this.activeTouches.hasOwnProperty(String(touch.identifier))) {
|
||||
console.warn('end of an UNKNOWN touch', touch);
|
||||
continue;
|
||||
}
|
||||
const data = this.activeTouches[touch.identifier];
|
||||
const evt = this.newGestureEvent(EventType.Change, data.initialTarget);
|
||||
evt.translationX = touch.pageX - data.rollingPageX.at(-1);
|
||||
evt.translationY = touch.pageY - data.rollingPageY.at(-1);
|
||||
evt.pageX = touch.pageX;
|
||||
evt.pageY = touch.pageY;
|
||||
this.dispatchEvent(evt);
|
||||
// only keep a few data points, to average the final speed
|
||||
if (data.rollingPageX.length > 3) {
|
||||
data.rollingPageX.shift();
|
||||
data.rollingPageY.shift();
|
||||
data.rollingTimestamps.shift();
|
||||
}
|
||||
data.rollingPageX.push(touch.pageX);
|
||||
data.rollingPageY.push(touch.pageY);
|
||||
data.rollingTimestamps.push(timestamp);
|
||||
}
|
||||
if (this.dispatched) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
this.dispatched = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
__decorate([
|
||||
memoize
|
||||
], Gesture, "isTouchDevice", null);
|
||||
|
||||
export { EventType, Gesture };
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
import { onUnexpectedError } from '../common/errors.js';
|
||||
import { getMonacoEnvironment } from './browser.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
function createTrustedTypesPolicy(policyName, policyOptions) {
|
||||
const monacoEnvironment = getMonacoEnvironment();
|
||||
if (monacoEnvironment?.createTrustedTypesPolicy) {
|
||||
try {
|
||||
return monacoEnvironment.createTrustedTypesPolicy(policyName, policyOptions);
|
||||
}
|
||||
catch (err) {
|
||||
onUnexpectedError(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
try {
|
||||
// eslint-disable-next-line local/code-no-any-casts
|
||||
return globalThis.trustedTypes?.createPolicy(policyName, policyOptions);
|
||||
}
|
||||
catch (err) {
|
||||
onUnexpectedError(err);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export { createTrustedTypesPolicy };
|
||||
Generated
Vendored
+376
@@ -0,0 +1,376 @@
|
||||
import { isFirefox } from '../../browser.js';
|
||||
import { DataTransfers } from '../../dnd.js';
|
||||
import { addDisposableListener, EventType, EventHelper } from '../../dom.js';
|
||||
import { Gesture, EventType as EventType$1 } from '../../touch.js';
|
||||
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
|
||||
import { SelectBox } from '../selectBox/selectBox.js';
|
||||
import { Action, ActionRunner, Separator } from '../../../common/actions.js';
|
||||
import { Disposable } from '../../../common/lifecycle.js';
|
||||
import { isMacintosh } from '../../../common/platform.js';
|
||||
import { isUndefinedOrNull, assertType } from '../../../common/types.js';
|
||||
import './actionbar.css';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class BaseActionViewItem extends Disposable {
|
||||
get action() {
|
||||
return this._action;
|
||||
}
|
||||
constructor(context, action, options = {}) {
|
||||
super();
|
||||
this.options = options;
|
||||
this._context = context || this;
|
||||
this._action = action;
|
||||
if (action instanceof Action) {
|
||||
this._register(action.onDidChange(event => {
|
||||
if (!this.element) {
|
||||
// we have not been rendered yet, so there
|
||||
// is no point in updating the UI
|
||||
return;
|
||||
}
|
||||
this.handleActionChangeEvent(event);
|
||||
}));
|
||||
}
|
||||
}
|
||||
handleActionChangeEvent(event) {
|
||||
if (event.enabled !== undefined) {
|
||||
this.updateEnabled();
|
||||
}
|
||||
if (event.checked !== undefined) {
|
||||
this.updateChecked();
|
||||
}
|
||||
if (event.class !== undefined) {
|
||||
this.updateClass();
|
||||
}
|
||||
if (event.label !== undefined) {
|
||||
this.updateLabel();
|
||||
this.updateTooltip();
|
||||
}
|
||||
if (event.tooltip !== undefined) {
|
||||
this.updateTooltip();
|
||||
}
|
||||
}
|
||||
get actionRunner() {
|
||||
if (!this._actionRunner) {
|
||||
this._actionRunner = this._register(new ActionRunner());
|
||||
}
|
||||
return this._actionRunner;
|
||||
}
|
||||
set actionRunner(actionRunner) {
|
||||
this._actionRunner = actionRunner;
|
||||
}
|
||||
isEnabled() {
|
||||
return this._action.enabled;
|
||||
}
|
||||
setActionContext(newContext) {
|
||||
this._context = newContext;
|
||||
}
|
||||
render(container) {
|
||||
const element = this.element = container;
|
||||
this._register(Gesture.addTarget(container));
|
||||
const enableDragging = this.options && this.options.draggable;
|
||||
if (enableDragging) {
|
||||
container.draggable = true;
|
||||
if (isFirefox) {
|
||||
// Firefox: requires to set a text data transfer to get going
|
||||
this._register(addDisposableListener(container, EventType.DRAG_START, e => e.dataTransfer?.setData(DataTransfers.TEXT, this._action.label)));
|
||||
}
|
||||
}
|
||||
this._register(addDisposableListener(element, EventType$1.Tap, e => this.onClick(e, true))); // Preserve focus on tap #125470
|
||||
this._register(addDisposableListener(element, EventType.MOUSE_DOWN, e => {
|
||||
if (!enableDragging) {
|
||||
EventHelper.stop(e, true); // do not run when dragging is on because that would disable it
|
||||
}
|
||||
if (this._action.enabled && e.button === 0) {
|
||||
element.classList.add('active');
|
||||
}
|
||||
}));
|
||||
if (isMacintosh) {
|
||||
// macOS: allow to trigger the button when holding Ctrl+key and pressing the
|
||||
// main mouse button. This is for scenarios where e.g. some interaction forces
|
||||
// the Ctrl+key to be pressed and hold but the user still wants to interact
|
||||
// with the actions (for example quick access in quick navigation mode).
|
||||
this._register(addDisposableListener(element, EventType.CONTEXT_MENU, e => {
|
||||
if (e.button === 0 && e.ctrlKey === true) {
|
||||
this.onClick(e);
|
||||
}
|
||||
}));
|
||||
}
|
||||
this._register(addDisposableListener(element, EventType.CLICK, e => {
|
||||
EventHelper.stop(e, true);
|
||||
// menus do not use the click event
|
||||
if (!(this.options && this.options.isMenu)) {
|
||||
this.onClick(e);
|
||||
}
|
||||
}));
|
||||
this._register(addDisposableListener(element, EventType.DBLCLICK, e => {
|
||||
EventHelper.stop(e, true);
|
||||
}));
|
||||
[EventType.MOUSE_UP, EventType.MOUSE_OUT].forEach(event => {
|
||||
this._register(addDisposableListener(element, event, e => {
|
||||
EventHelper.stop(e);
|
||||
element.classList.remove('active');
|
||||
}));
|
||||
});
|
||||
}
|
||||
onClick(event, preserveFocus = false) {
|
||||
EventHelper.stop(event, true);
|
||||
const context = isUndefinedOrNull(this._context) ? this.options?.useEventAsContext ? event : { preserveFocus } : this._context;
|
||||
this.actionRunner.run(this._action, context);
|
||||
}
|
||||
// Only set the tabIndex on the element once it is about to get focused
|
||||
// That way this element wont be a tab stop when it is not needed #106441
|
||||
focus() {
|
||||
if (this.element) {
|
||||
this.element.tabIndex = 0;
|
||||
this.element.focus();
|
||||
this.element.classList.add('focused');
|
||||
}
|
||||
}
|
||||
blur() {
|
||||
if (this.element) {
|
||||
this.element.blur();
|
||||
this.element.tabIndex = -1;
|
||||
this.element.classList.remove('focused');
|
||||
}
|
||||
}
|
||||
setFocusable(focusable) {
|
||||
if (this.element) {
|
||||
this.element.tabIndex = focusable ? 0 : -1;
|
||||
}
|
||||
}
|
||||
get trapsArrowNavigation() {
|
||||
return false;
|
||||
}
|
||||
updateEnabled() {
|
||||
// implement in subclass
|
||||
}
|
||||
updateLabel() {
|
||||
// implement in subclass
|
||||
}
|
||||
getClass() {
|
||||
return this.action.class;
|
||||
}
|
||||
getTooltip() {
|
||||
return this.action.tooltip;
|
||||
}
|
||||
getHoverContents() {
|
||||
return this.getTooltip();
|
||||
}
|
||||
updateTooltip() {
|
||||
if (!this.element) {
|
||||
return;
|
||||
}
|
||||
const title = this.getHoverContents() ?? '';
|
||||
this.updateAriaLabel();
|
||||
if (!this.customHover && title !== '') {
|
||||
const hoverDelegate = this.options.hoverDelegate ?? getDefaultHoverDelegate('element');
|
||||
this.customHover = this._store.add(getBaseLayerHoverDelegate().setupManagedHover(hoverDelegate, this.element, title));
|
||||
}
|
||||
else if (this.customHover) {
|
||||
this.customHover.update(title);
|
||||
}
|
||||
}
|
||||
updateAriaLabel() {
|
||||
if (this.element) {
|
||||
const title = this.getTooltip() ?? '';
|
||||
this.element.setAttribute('aria-label', title);
|
||||
}
|
||||
}
|
||||
updateClass() {
|
||||
// implement in subclass
|
||||
}
|
||||
updateChecked() {
|
||||
// implement in subclass
|
||||
}
|
||||
dispose() {
|
||||
if (this.element) {
|
||||
this.element.remove();
|
||||
this.element = undefined;
|
||||
}
|
||||
this._context = undefined;
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
class ActionViewItem extends BaseActionViewItem {
|
||||
constructor(context, action, options) {
|
||||
options = {
|
||||
...options,
|
||||
icon: options.icon !== undefined ? options.icon : false,
|
||||
label: options.label !== undefined ? options.label : true,
|
||||
};
|
||||
super(context, action, options);
|
||||
this.options = options;
|
||||
this.cssClass = '';
|
||||
}
|
||||
render(container) {
|
||||
super.render(container);
|
||||
assertType(this.element);
|
||||
const label = document.createElement('a');
|
||||
label.classList.add('action-label');
|
||||
label.setAttribute('role', this.getDefaultAriaRole());
|
||||
this.label = label;
|
||||
this.element.appendChild(label);
|
||||
if (this.options.label && this.options.keybinding && !this.options.keybindingNotRenderedWithLabel) {
|
||||
const kbLabel = document.createElement('span');
|
||||
kbLabel.classList.add('keybinding');
|
||||
kbLabel.textContent = this.options.keybinding;
|
||||
this.element.appendChild(kbLabel);
|
||||
}
|
||||
this.updateClass();
|
||||
this.updateLabel();
|
||||
this.updateTooltip();
|
||||
this.updateEnabled();
|
||||
this.updateChecked();
|
||||
}
|
||||
getDefaultAriaRole() {
|
||||
if (this._action.id === Separator.ID) {
|
||||
return 'presentation'; // A separator is a presentation item
|
||||
}
|
||||
else {
|
||||
if (this.options.isMenu) {
|
||||
return 'menuitem';
|
||||
}
|
||||
else if (this.options.isTabList) {
|
||||
return 'tab';
|
||||
}
|
||||
else {
|
||||
return 'button';
|
||||
}
|
||||
}
|
||||
}
|
||||
// Only set the tabIndex on the element once it is about to get focused
|
||||
// That way this element wont be a tab stop when it is not needed #106441
|
||||
focus() {
|
||||
if (this.label) {
|
||||
this.label.tabIndex = 0;
|
||||
this.label.focus();
|
||||
}
|
||||
}
|
||||
blur() {
|
||||
if (this.label) {
|
||||
this.label.tabIndex = -1;
|
||||
}
|
||||
}
|
||||
setFocusable(focusable) {
|
||||
if (this.label) {
|
||||
this.label.tabIndex = focusable ? 0 : -1;
|
||||
}
|
||||
}
|
||||
updateLabel() {
|
||||
if (this.options.label && this.label) {
|
||||
this.label.textContent = this.action.label;
|
||||
}
|
||||
}
|
||||
getTooltip() {
|
||||
let title = null;
|
||||
if (this.action.tooltip) {
|
||||
title = this.action.tooltip;
|
||||
}
|
||||
else if (this.action.label) {
|
||||
title = this.action.label;
|
||||
if (this.options.keybinding) {
|
||||
title = localize(0, "{0} ({1})", title, this.options.keybinding);
|
||||
}
|
||||
}
|
||||
return title ?? undefined;
|
||||
}
|
||||
updateClass() {
|
||||
if (this.cssClass && this.label) {
|
||||
this.label.classList.remove(...this.cssClass.split(' '));
|
||||
}
|
||||
if (this.options.icon) {
|
||||
this.cssClass = this.getClass();
|
||||
if (this.label) {
|
||||
this.label.classList.add('codicon');
|
||||
if (this.cssClass) {
|
||||
this.label.classList.add(...this.cssClass.split(' '));
|
||||
}
|
||||
}
|
||||
this.updateEnabled();
|
||||
}
|
||||
else {
|
||||
this.label?.classList.remove('codicon');
|
||||
}
|
||||
}
|
||||
updateEnabled() {
|
||||
if (this.action.enabled) {
|
||||
if (this.label) {
|
||||
this.label.removeAttribute('aria-disabled');
|
||||
this.label.classList.remove('disabled');
|
||||
}
|
||||
this.element?.classList.remove('disabled');
|
||||
}
|
||||
else {
|
||||
if (this.label) {
|
||||
this.label.setAttribute('aria-disabled', 'true');
|
||||
this.label.classList.add('disabled');
|
||||
}
|
||||
this.element?.classList.add('disabled');
|
||||
}
|
||||
}
|
||||
updateAriaLabel() {
|
||||
if (this.label) {
|
||||
const title = this.getTooltip() ?? '';
|
||||
this.label.setAttribute('aria-label', title);
|
||||
}
|
||||
}
|
||||
updateChecked() {
|
||||
if (this.label) {
|
||||
if (this.action.checked !== undefined) {
|
||||
this.label.classList.toggle('checked', this.action.checked);
|
||||
if (this.options.isTabList) {
|
||||
this.label.setAttribute('aria-selected', this.action.checked ? 'true' : 'false');
|
||||
}
|
||||
else {
|
||||
this.label.setAttribute('aria-checked', this.action.checked ? 'true' : 'false');
|
||||
this.label.setAttribute('role', 'checkbox');
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.label.classList.remove('checked');
|
||||
this.label.removeAttribute(this.options.isTabList ? 'aria-selected' : 'aria-checked');
|
||||
this.label.setAttribute('role', this.getDefaultAriaRole());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
class SelectActionViewItem extends BaseActionViewItem {
|
||||
constructor(ctx, action, options, selected, contextViewProvider, styles, selectBoxOptions) {
|
||||
super(ctx, action);
|
||||
this.selectBox = new SelectBox(options, selected, contextViewProvider, styles, selectBoxOptions);
|
||||
this.selectBox.setFocusable(false);
|
||||
this._register(this.selectBox);
|
||||
this.registerListeners();
|
||||
}
|
||||
select(index) {
|
||||
this.selectBox.select(index);
|
||||
}
|
||||
registerListeners() {
|
||||
this._register(this.selectBox.onDidSelect(e => this.runAction(e.selected, e.index)));
|
||||
}
|
||||
runAction(option, index) {
|
||||
this.actionRunner.run(this._action, this.getActionContext(option, index));
|
||||
}
|
||||
getActionContext(option, index) {
|
||||
return option;
|
||||
}
|
||||
setFocusable(focusable) {
|
||||
this.selectBox.setFocusable(focusable);
|
||||
}
|
||||
focus() {
|
||||
this.selectBox?.focus();
|
||||
}
|
||||
blur() {
|
||||
this.selectBox?.blur();
|
||||
}
|
||||
render(container) {
|
||||
this.selectBox.render(container);
|
||||
}
|
||||
}
|
||||
|
||||
export { ActionViewItem, BaseActionViewItem, SelectActionViewItem };
|
||||
Generated
Vendored
+124
@@ -0,0 +1,124 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-action-bar {
|
||||
white-space: nowrap;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-action-bar .actions-container {
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-action-bar.vertical .actions-container {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item {
|
||||
display: block;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
position: relative; /* DO NOT REMOVE - this is the key to preventing the ghosting icon bug in Chrome 42 */
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item .icon,
|
||||
.monaco-action-bar .action-item .codicon {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item .codicon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-label {
|
||||
display: flex;
|
||||
font-size: 11px;
|
||||
padding: 3px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item.disabled .action-label:not(.icon) ,
|
||||
.monaco-action-bar .action-item.disabled .action-label:not(.icon)::before,
|
||||
.monaco-action-bar .action-item.disabled .action-label:not(.icon):hover {
|
||||
color: var(--vscode-disabledForeground);
|
||||
}
|
||||
|
||||
/* Unable to change color of SVGs, hence opacity is used */
|
||||
.monaco-action-bar .action-item.disabled .action-label.icon ,
|
||||
.monaco-action-bar .action-item.disabled .action-label.icon::before,
|
||||
.monaco-action-bar .action-item.disabled .action-label.icon:hover {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Vertical actions */
|
||||
|
||||
.monaco-action-bar.vertical {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.monaco-action-bar.vertical .action-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.monaco-action-bar.vertical .action-label.separator {
|
||||
display: block;
|
||||
border-bottom: 1px solid var(--vscode-disabledForeground);
|
||||
padding-top: 1px;
|
||||
margin-left: .8em;
|
||||
margin-right: .8em;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item .action-label.separator {
|
||||
width: 1px;
|
||||
height: 16px;
|
||||
margin: 5px 4px !important;
|
||||
cursor: default;
|
||||
min-width: 1px;
|
||||
padding: 0;
|
||||
background-color: var(--vscode-disabledForeground);
|
||||
}
|
||||
|
||||
.secondary-actions .monaco-action-bar .action-label {
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* Action Items */
|
||||
.monaco-action-bar .action-item.select-container {
|
||||
overflow: hidden; /* somehow the dropdown overflows its container, we prevent it here to not push */
|
||||
flex: 1;
|
||||
max-width: 170px;
|
||||
min-width: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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-dropdown-item-separator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item.action-dropdown-item > .action-dropdown-item-separator > div {
|
||||
width: 1px;
|
||||
}
|
||||
Generated
Vendored
+462
@@ -0,0 +1,462 @@
|
||||
import { addDisposableListener, EventType, trackFocus, getActiveElement, isAncestor, isHTMLElement, EventHelper, clearNode } from '../../dom.js';
|
||||
import { StandardKeyboardEvent } from '../../keyboardEvent.js';
|
||||
import { BaseActionViewItem, ActionViewItem } from './actionViewItems.js';
|
||||
import { createInstantHoverDelegate } from '../hover/hoverDelegateFactory.js';
|
||||
import { ActionRunner, Separator } from '../../../common/actions.js';
|
||||
import { Emitter } from '../../../common/event.js';
|
||||
import { Disposable, DisposableStore, DisposableMap, dispose } from '../../../common/lifecycle.js';
|
||||
import { isNumber, isFunction } from '../../../common/types.js';
|
||||
import './actionbar.css';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class ActionBar extends Disposable {
|
||||
get onDidBlur() { return this._onDidBlur.event; }
|
||||
get onDidCancel() { return this._onDidCancel.event; }
|
||||
get onDidRun() { return this._onDidRun.event; }
|
||||
get onWillRun() { return this._onWillRun.event; }
|
||||
constructor(container, options = {}) {
|
||||
super();
|
||||
this._actionRunnerDisposables = this._register(new DisposableStore());
|
||||
this.viewItemDisposables = this._register(new DisposableMap());
|
||||
// Trigger Key Tracking
|
||||
this.triggerKeyDown = false;
|
||||
this.focusable = true;
|
||||
this._onDidBlur = this._register(new Emitter());
|
||||
this._onDidCancel = this._register(new Emitter({ onWillAddFirstListener: () => this.cancelHasListener = true }));
|
||||
this.cancelHasListener = false;
|
||||
this._onDidRun = this._register(new Emitter());
|
||||
this._onWillRun = this._register(new Emitter());
|
||||
this.options = options;
|
||||
this._context = options.context ?? null;
|
||||
this._orientation = this.options.orientation ?? 0 /* ActionsOrientation.HORIZONTAL */;
|
||||
this._triggerKeys = {
|
||||
keyDown: this.options.triggerKeys?.keyDown ?? false,
|
||||
keys: this.options.triggerKeys?.keys ?? [3 /* KeyCode.Enter */, 10 /* KeyCode.Space */]
|
||||
};
|
||||
this._hoverDelegate = options.hoverDelegate ?? this._register(createInstantHoverDelegate());
|
||||
if (this.options.actionRunner) {
|
||||
this._actionRunner = this.options.actionRunner;
|
||||
}
|
||||
else {
|
||||
this._actionRunner = new ActionRunner();
|
||||
this._actionRunnerDisposables.add(this._actionRunner);
|
||||
}
|
||||
this._actionRunnerDisposables.add(this._actionRunner.onDidRun(e => this._onDidRun.fire(e)));
|
||||
this._actionRunnerDisposables.add(this._actionRunner.onWillRun(e => this._onWillRun.fire(e)));
|
||||
this.viewItems = [];
|
||||
this.focusedItem = undefined;
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.className = 'monaco-action-bar';
|
||||
let previousKeys;
|
||||
let nextKeys;
|
||||
switch (this._orientation) {
|
||||
case 0 /* ActionsOrientation.HORIZONTAL */:
|
||||
previousKeys = [15 /* KeyCode.LeftArrow */];
|
||||
nextKeys = [17 /* KeyCode.RightArrow */];
|
||||
break;
|
||||
case 1 /* ActionsOrientation.VERTICAL */:
|
||||
previousKeys = [16 /* KeyCode.UpArrow */];
|
||||
nextKeys = [18 /* KeyCode.DownArrow */];
|
||||
this.domNode.className += ' vertical';
|
||||
break;
|
||||
}
|
||||
this._register(addDisposableListener(this.domNode, EventType.KEY_DOWN, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
let eventHandled = true;
|
||||
const focusedItem = typeof this.focusedItem === 'number' ? this.viewItems[this.focusedItem] : undefined;
|
||||
if (previousKeys && (event.equals(previousKeys[0]) || event.equals(previousKeys[1]))) {
|
||||
eventHandled = this.focusPrevious();
|
||||
}
|
||||
else if (nextKeys && (event.equals(nextKeys[0]) || event.equals(nextKeys[1]))) {
|
||||
eventHandled = this.focusNext();
|
||||
}
|
||||
else if (event.equals(9 /* KeyCode.Escape */) && this.cancelHasListener) {
|
||||
this._onDidCancel.fire();
|
||||
}
|
||||
else if (event.equals(14 /* KeyCode.Home */)) {
|
||||
eventHandled = this.focusFirst();
|
||||
}
|
||||
else if (event.equals(13 /* KeyCode.End */)) {
|
||||
eventHandled = this.focusLast();
|
||||
}
|
||||
else if (event.equals(2 /* KeyCode.Tab */) && focusedItem instanceof BaseActionViewItem && focusedItem.trapsArrowNavigation) {
|
||||
// Tab, so forcibly focus next #219199
|
||||
eventHandled = this.focusNext(undefined, true);
|
||||
}
|
||||
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;
|
||||
}
|
||||
if (eventHandled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}));
|
||||
this._register(addDisposableListener(this.domNode, EventType.KEY_UP, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
// Run action on Enter/Space
|
||||
if (this.isTriggerKeyEvent(event)) {
|
||||
if (!this._triggerKeys.keyDown && this.triggerKeyDown) {
|
||||
this.triggerKeyDown = false;
|
||||
this.doTrigger(event);
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
// Recompute focused item
|
||||
else if (event.equals(2 /* KeyCode.Tab */) || event.equals(1024 /* KeyMod.Shift */ | 2 /* KeyCode.Tab */) || event.equals(16 /* KeyCode.UpArrow */) || event.equals(18 /* KeyCode.DownArrow */) || event.equals(15 /* KeyCode.LeftArrow */) || event.equals(17 /* KeyCode.RightArrow */)) {
|
||||
this.updateFocusedItem();
|
||||
}
|
||||
}));
|
||||
this.focusTracker = this._register(trackFocus(this.domNode));
|
||||
this._register(this.focusTracker.onDidBlur(() => {
|
||||
if (getActiveElement() === this.domNode || !isAncestor(getActiveElement(), this.domNode)) {
|
||||
this._onDidBlur.fire();
|
||||
this.previouslyFocusedItem = this.focusedItem;
|
||||
this.focusedItem = undefined;
|
||||
this.triggerKeyDown = false;
|
||||
}
|
||||
}));
|
||||
this._register(this.focusTracker.onDidFocus(() => this.updateFocusedItem()));
|
||||
this.actionsList = document.createElement('ul');
|
||||
this.actionsList.className = 'actions-container';
|
||||
if (this.options.highlightToggledItems) {
|
||||
this.actionsList.classList.add('highlight-toggled');
|
||||
}
|
||||
this.actionsList.setAttribute('role', this.options.ariaRole || 'toolbar');
|
||||
if (this.options.ariaLabel) {
|
||||
this.actionsList.setAttribute('aria-label', this.options.ariaLabel);
|
||||
}
|
||||
this.domNode.appendChild(this.actionsList);
|
||||
container.appendChild(this.domNode);
|
||||
}
|
||||
refreshRole() {
|
||||
if (this.length() >= 1) {
|
||||
this.actionsList.setAttribute('role', this.options.ariaRole || 'toolbar');
|
||||
}
|
||||
else {
|
||||
this.actionsList.setAttribute('role', 'presentation');
|
||||
}
|
||||
}
|
||||
// Some action bars should not be focusable at times
|
||||
// When an action bar is not focusable make sure to make all the elements inside it not focusable
|
||||
// When an action bar is focusable again, make sure the first item can be focused
|
||||
setFocusable(focusable) {
|
||||
this.focusable = focusable;
|
||||
if (this.focusable) {
|
||||
const firstEnabled = this.viewItems.find(vi => vi instanceof BaseActionViewItem && vi.isEnabled());
|
||||
if (firstEnabled instanceof BaseActionViewItem) {
|
||||
firstEnabled.setFocusable(true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.viewItems.forEach(vi => {
|
||||
if (vi instanceof BaseActionViewItem) {
|
||||
vi.setFocusable(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
isTriggerKeyEvent(event) {
|
||||
let ret = false;
|
||||
this._triggerKeys.keys.forEach(keyCode => {
|
||||
ret = ret || event.equals(keyCode);
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
updateFocusedItem() {
|
||||
for (let i = 0; i < this.actionsList.children.length; i++) {
|
||||
const elem = this.actionsList.children[i];
|
||||
if (isAncestor(getActiveElement(), elem)) {
|
||||
this.focusedItem = i;
|
||||
this.viewItems[this.focusedItem]?.showHover?.();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
get context() {
|
||||
return this._context;
|
||||
}
|
||||
set context(context) {
|
||||
this._context = context;
|
||||
this.viewItems.forEach(i => i.setActionContext(context));
|
||||
}
|
||||
get actionRunner() {
|
||||
return this._actionRunner;
|
||||
}
|
||||
set actionRunner(actionRunner) {
|
||||
this._actionRunner = actionRunner;
|
||||
// when setting a new `IActionRunner` make sure to dispose old listeners and
|
||||
// start to forward events from the new listener
|
||||
this._actionRunnerDisposables.clear();
|
||||
this._actionRunnerDisposables.add(this._actionRunner.onDidRun(e => this._onDidRun.fire(e)));
|
||||
this._actionRunnerDisposables.add(this._actionRunner.onWillRun(e => this._onWillRun.fire(e)));
|
||||
this.viewItems.forEach(item => item.actionRunner = actionRunner);
|
||||
}
|
||||
getContainer() {
|
||||
return this.domNode;
|
||||
}
|
||||
getAction(indexOrElement) {
|
||||
// by index
|
||||
if (typeof indexOrElement === 'number') {
|
||||
return this.viewItems[indexOrElement]?.action;
|
||||
}
|
||||
// by element
|
||||
if (isHTMLElement(indexOrElement)) {
|
||||
while (indexOrElement.parentElement !== this.actionsList) {
|
||||
if (!indexOrElement.parentElement) {
|
||||
return undefined;
|
||||
}
|
||||
indexOrElement = indexOrElement.parentElement;
|
||||
}
|
||||
for (let i = 0; i < this.actionsList.childNodes.length; i++) {
|
||||
if (this.actionsList.childNodes[i] === indexOrElement) {
|
||||
return this.viewItems[i].action;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
push(arg, options = {}) {
|
||||
const actions = Array.isArray(arg) ? arg : [arg];
|
||||
let index = isNumber(options.index) ? options.index : null;
|
||||
actions.forEach((action) => {
|
||||
const actionViewItemElement = document.createElement('li');
|
||||
actionViewItemElement.className = 'action-item';
|
||||
actionViewItemElement.setAttribute('role', 'presentation');
|
||||
let item;
|
||||
const viewItemOptions = { hoverDelegate: this._hoverDelegate, ...options, isTabList: this.options.ariaRole === 'tablist' };
|
||||
if (this.options.actionViewItemProvider) {
|
||||
item = this.options.actionViewItemProvider(action, viewItemOptions);
|
||||
}
|
||||
if (!item) {
|
||||
item = new ActionViewItem(this.context, action, viewItemOptions);
|
||||
}
|
||||
// Prevent native context menu on actions
|
||||
if (!this.options.allowContextMenu) {
|
||||
this.viewItemDisposables.set(item, addDisposableListener(actionViewItemElement, EventType.CONTEXT_MENU, (e) => {
|
||||
EventHelper.stop(e, true);
|
||||
}));
|
||||
}
|
||||
item.actionRunner = this._actionRunner;
|
||||
item.setActionContext(this.context);
|
||||
item.render(actionViewItemElement);
|
||||
if (index === null || index < 0 || index >= this.actionsList.children.length) {
|
||||
this.actionsList.appendChild(actionViewItemElement);
|
||||
this.viewItems.push(item);
|
||||
}
|
||||
else {
|
||||
this.actionsList.insertBefore(actionViewItemElement, this.actionsList.children[index]);
|
||||
this.viewItems.splice(index, 0, item);
|
||||
index++;
|
||||
}
|
||||
});
|
||||
// We need to allow for the first enabled item to be focused on using tab navigation #106441
|
||||
if (this.focusable) {
|
||||
let didFocus = false;
|
||||
for (const item of this.viewItems) {
|
||||
if (!(item instanceof BaseActionViewItem)) {
|
||||
continue;
|
||||
}
|
||||
let focus;
|
||||
if (didFocus) {
|
||||
focus = false; // already focused an item
|
||||
}
|
||||
else if (item.action.id === Separator.ID) {
|
||||
focus = false; // never focus a separator
|
||||
}
|
||||
else if (!item.isEnabled() && this.options.focusOnlyEnabledItems) {
|
||||
focus = false; // never focus a disabled item
|
||||
}
|
||||
else {
|
||||
focus = true;
|
||||
}
|
||||
if (focus) {
|
||||
item.setFocusable(true);
|
||||
didFocus = true;
|
||||
}
|
||||
else {
|
||||
item.setFocusable(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (typeof this.focusedItem === 'number') {
|
||||
// After a clear actions might be re-added to simply toggle some actions. We should preserve focus #97128
|
||||
this.focus(this.focusedItem);
|
||||
}
|
||||
this.refreshRole();
|
||||
}
|
||||
getWidth(index) {
|
||||
if (index >= 0 && index < this.actionsList.children.length) {
|
||||
const item = this.actionsList.children.item(index);
|
||||
if (item) {
|
||||
return item.clientWidth;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
pull(index) {
|
||||
if (index >= 0 && index < this.viewItems.length) {
|
||||
this.actionsList.childNodes[index].remove();
|
||||
this.viewItemDisposables.deleteAndDispose(this.viewItems[index]);
|
||||
dispose(this.viewItems.splice(index, 1));
|
||||
this.refreshRole();
|
||||
}
|
||||
}
|
||||
clear() {
|
||||
if (this.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
this.viewItems = dispose(this.viewItems);
|
||||
this.viewItemDisposables.clearAndDisposeAll();
|
||||
clearNode(this.actionsList);
|
||||
this.refreshRole();
|
||||
}
|
||||
length() {
|
||||
return this.viewItems.length;
|
||||
}
|
||||
isEmpty() {
|
||||
return this.viewItems.length === 0;
|
||||
}
|
||||
focus(arg) {
|
||||
let selectFirst = false;
|
||||
let index = undefined;
|
||||
if (arg === undefined) {
|
||||
selectFirst = true;
|
||||
}
|
||||
else if (typeof arg === 'number') {
|
||||
index = arg;
|
||||
}
|
||||
else if (typeof arg === 'boolean') {
|
||||
selectFirst = arg;
|
||||
}
|
||||
if (selectFirst && typeof this.focusedItem === 'undefined') {
|
||||
const firstEnabled = this.viewItems.findIndex(item => item.isEnabled());
|
||||
// Focus the first enabled item
|
||||
this.focusedItem = firstEnabled === -1 ? undefined : firstEnabled;
|
||||
this.updateFocus(undefined, undefined, true);
|
||||
}
|
||||
else {
|
||||
if (index !== undefined) {
|
||||
this.focusedItem = index;
|
||||
}
|
||||
this.updateFocus(undefined, undefined, true);
|
||||
}
|
||||
}
|
||||
focusFirst() {
|
||||
this.focusedItem = this.length() - 1;
|
||||
return this.focusNext(true);
|
||||
}
|
||||
focusLast() {
|
||||
this.focusedItem = 0;
|
||||
return this.focusPrevious(true);
|
||||
}
|
||||
focusNext(forceLoop, forceFocus) {
|
||||
if (typeof this.focusedItem === 'undefined') {
|
||||
this.focusedItem = this.viewItems.length - 1;
|
||||
}
|
||||
else if (this.viewItems.length <= 1) {
|
||||
return false;
|
||||
}
|
||||
const startIndex = this.focusedItem;
|
||||
let item;
|
||||
do {
|
||||
if (!forceLoop && this.options.preventLoopNavigation && this.focusedItem + 1 >= this.viewItems.length) {
|
||||
this.focusedItem = startIndex;
|
||||
return false;
|
||||
}
|
||||
this.focusedItem = (this.focusedItem + 1) % this.viewItems.length;
|
||||
item = this.viewItems[this.focusedItem];
|
||||
} while (this.focusedItem !== startIndex && ((this.options.focusOnlyEnabledItems && !item.isEnabled()) || item.action.id === Separator.ID));
|
||||
this.updateFocus(undefined, undefined, forceFocus);
|
||||
return true;
|
||||
}
|
||||
focusPrevious(forceLoop) {
|
||||
if (typeof this.focusedItem === 'undefined') {
|
||||
this.focusedItem = 0;
|
||||
}
|
||||
else if (this.viewItems.length <= 1) {
|
||||
return false;
|
||||
}
|
||||
const startIndex = this.focusedItem;
|
||||
let item;
|
||||
do {
|
||||
this.focusedItem = this.focusedItem - 1;
|
||||
if (this.focusedItem < 0) {
|
||||
if (!forceLoop && this.options.preventLoopNavigation) {
|
||||
this.focusedItem = startIndex;
|
||||
return false;
|
||||
}
|
||||
this.focusedItem = this.viewItems.length - 1;
|
||||
}
|
||||
item = this.viewItems[this.focusedItem];
|
||||
} while (this.focusedItem !== startIndex && ((this.options.focusOnlyEnabledItems && !item.isEnabled()) || item.action.id === Separator.ID));
|
||||
this.updateFocus(true);
|
||||
return true;
|
||||
}
|
||||
updateFocus(fromRight, preventScroll, forceFocus = false) {
|
||||
if (typeof this.focusedItem === 'undefined') {
|
||||
this.actionsList.focus({ preventScroll });
|
||||
}
|
||||
if (this.previouslyFocusedItem !== undefined && this.previouslyFocusedItem !== this.focusedItem) {
|
||||
this.viewItems[this.previouslyFocusedItem]?.blur();
|
||||
}
|
||||
const actionViewItem = this.focusedItem !== undefined ? this.viewItems[this.focusedItem] : undefined;
|
||||
if (actionViewItem) {
|
||||
let focusItem = true;
|
||||
if (!isFunction(actionViewItem.focus)) {
|
||||
focusItem = false;
|
||||
}
|
||||
if (this.options.focusOnlyEnabledItems && isFunction(actionViewItem.isEnabled) && !actionViewItem.isEnabled()) {
|
||||
focusItem = false;
|
||||
}
|
||||
if (actionViewItem.action.id === Separator.ID) {
|
||||
focusItem = false;
|
||||
}
|
||||
if (!focusItem) {
|
||||
this.actionsList.focus({ preventScroll });
|
||||
this.previouslyFocusedItem = undefined;
|
||||
}
|
||||
else if (forceFocus || this.previouslyFocusedItem !== this.focusedItem) {
|
||||
actionViewItem.focus(fromRight);
|
||||
this.previouslyFocusedItem = this.focusedItem;
|
||||
}
|
||||
if (focusItem) {
|
||||
actionViewItem.showHover?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
doTrigger(event) {
|
||||
if (typeof this.focusedItem === 'undefined') {
|
||||
return; //nothing to focus
|
||||
}
|
||||
// trigger action
|
||||
const actionViewItem = this.viewItems[this.focusedItem];
|
||||
if (actionViewItem instanceof BaseActionViewItem) {
|
||||
const context = (actionViewItem._context === null || actionViewItem._context === undefined) ? event : actionViewItem._context;
|
||||
this.run(actionViewItem._action, context);
|
||||
}
|
||||
}
|
||||
async run(action, context) {
|
||||
await this._actionRunner.run(action, context);
|
||||
}
|
||||
dispose() {
|
||||
this._context = undefined;
|
||||
this.viewItems = dispose(this.viewItems);
|
||||
this.getContainer().remove();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export { ActionBar };
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-aria-container {
|
||||
position: absolute; /* try to hide from window but not from screen readers */
|
||||
left:-999em;
|
||||
}
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
import { clearNode } from '../../dom.js';
|
||||
import './aria.css';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
// Use a max length since we are inserting the whole msg in the DOM and that can cause browsers to freeze for long messages #94233
|
||||
const MAX_MESSAGE_LENGTH = 20000;
|
||||
let ariaContainer;
|
||||
let alertContainer;
|
||||
let alertContainer2;
|
||||
let statusContainer;
|
||||
let statusContainer2;
|
||||
function setARIAContainer(parent) {
|
||||
ariaContainer = document.createElement('div');
|
||||
ariaContainer.className = 'monaco-aria-container';
|
||||
const createAlertContainer = () => {
|
||||
const element = document.createElement('div');
|
||||
element.className = 'monaco-alert';
|
||||
element.setAttribute('role', 'alert');
|
||||
element.setAttribute('aria-atomic', 'true');
|
||||
ariaContainer.appendChild(element);
|
||||
return element;
|
||||
};
|
||||
alertContainer = createAlertContainer();
|
||||
alertContainer2 = createAlertContainer();
|
||||
const createStatusContainer = () => {
|
||||
const element = document.createElement('div');
|
||||
element.className = 'monaco-status';
|
||||
element.setAttribute('aria-live', 'polite');
|
||||
element.setAttribute('aria-atomic', 'true');
|
||||
ariaContainer.appendChild(element);
|
||||
return element;
|
||||
};
|
||||
statusContainer = createStatusContainer();
|
||||
statusContainer2 = createStatusContainer();
|
||||
parent.appendChild(ariaContainer);
|
||||
}
|
||||
/**
|
||||
* Given the provided message, will make sure that it is read as alert to screen readers.
|
||||
*/
|
||||
function alert(msg) {
|
||||
if (!ariaContainer) {
|
||||
return;
|
||||
}
|
||||
// Use alternate containers such that duplicated messages get read out by screen readers #99466
|
||||
if (alertContainer.textContent !== msg) {
|
||||
clearNode(alertContainer2);
|
||||
insertMessage(alertContainer, msg);
|
||||
}
|
||||
else {
|
||||
clearNode(alertContainer);
|
||||
insertMessage(alertContainer2, msg);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Given the provided message, will make sure that it is read as status to screen readers.
|
||||
*/
|
||||
function status(msg) {
|
||||
if (!ariaContainer) {
|
||||
return;
|
||||
}
|
||||
if (statusContainer.textContent !== msg) {
|
||||
clearNode(statusContainer2);
|
||||
insertMessage(statusContainer, msg);
|
||||
}
|
||||
else {
|
||||
clearNode(statusContainer);
|
||||
insertMessage(statusContainer2, msg);
|
||||
}
|
||||
}
|
||||
function insertMessage(target, msg) {
|
||||
clearNode(target);
|
||||
if (msg.length > MAX_MESSAGE_LENGTH) {
|
||||
msg = msg.substr(0, MAX_MESSAGE_LENGTH);
|
||||
}
|
||||
target.textContent = msg;
|
||||
// See https://www.paciellogroup.com/blog/2012/06/html5-accessibility-chops-aria-rolealert-browser-support/
|
||||
target.style.visibility = 'hidden';
|
||||
target.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
export { alert, setARIAContainer, status };
|
||||
+174
@@ -0,0 +1,174 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-text-button {
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
padding: 4px;
|
||||
border-radius: 2px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border: 1px solid var(--vscode-button-border, transparent);
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.monaco-text-button:focus {
|
||||
outline-offset: 2px !important;
|
||||
}
|
||||
|
||||
.monaco-text-button:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.monaco-button.disabled:focus,
|
||||
.monaco-button.disabled {
|
||||
opacity: 0.4 !important;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-text-button .codicon {
|
||||
margin: 0 0.2em;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.monaco-text-button.monaco-text-button-with-short-label {
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
padding: 0 4px;
|
||||
overflow: hidden;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
.monaco-text-button.monaco-text-button-with-short-label > .monaco-button-label {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.monaco-text-button.monaco-text-button-with-short-label > .monaco-button-label-short {
|
||||
flex-grow: 1;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-text-button.monaco-text-button-with-short-label > .monaco-button-label,
|
||||
.monaco-text-button.monaco-text-button-with-short-label > .monaco-button-label-short {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-weight: normal;
|
||||
font-style: inherit;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.monaco-button-dropdown {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-button-dropdown.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-button-dropdown > .monaco-button:focus {
|
||||
outline-offset: -1px !important;
|
||||
}
|
||||
|
||||
.monaco-button-dropdown.disabled > .monaco-button.disabled,
|
||||
.monaco-button-dropdown.disabled > .monaco-button.disabled:focus,
|
||||
.monaco-button-dropdown.disabled > .monaco-button-dropdown-separator {
|
||||
opacity: 0.4 !important;
|
||||
}
|
||||
|
||||
.monaco-button-dropdown > .monaco-button.monaco-text-button {
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
|
||||
.monaco-button-dropdown .monaco-button-dropdown-separator {
|
||||
padding: 4px 0;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-button-dropdown .monaco-button-dropdown-separator > div {
|
||||
height: 100%;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
.monaco-button-dropdown > .monaco-button.monaco-dropdown-button {
|
||||
border: 1px solid var(--vscode-button-border, transparent);
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 2px 2px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-button-dropdown > .monaco-button.monaco-text-button {
|
||||
border-radius: 2px 0 0 2px;
|
||||
}
|
||||
|
||||
.monaco-description-button {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 4px 5px; /* allows button focus outline to be visible */
|
||||
}
|
||||
|
||||
.monaco-description-button .monaco-button-description {
|
||||
font-style: italic;
|
||||
font-size: 11px;
|
||||
padding: 4px 20px;
|
||||
}
|
||||
|
||||
.monaco-description-button .monaco-button-label,
|
||||
.monaco-description-button .monaco-button-description {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.monaco-description-button .monaco-button-label > .codicon,
|
||||
.monaco-description-button .monaco-button-description > .codicon {
|
||||
margin: 0 0.2em;
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
/* default color styles - based on CSS variables */
|
||||
|
||||
.monaco-button.default-colors,
|
||||
.monaco-button-dropdown.default-colors > .monaco-button{
|
||||
color: var(--vscode-button-foreground);
|
||||
background-color: var(--vscode-button-background);
|
||||
}
|
||||
|
||||
.monaco-button.default-colors:hover,
|
||||
.monaco-button-dropdown.default-colors > .monaco-button:hover {
|
||||
background-color: var(--vscode-button-hoverBackground);
|
||||
}
|
||||
|
||||
.monaco-button.default-colors.secondary,
|
||||
.monaco-button-dropdown.default-colors > .monaco-button.secondary {
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
}
|
||||
|
||||
.monaco-button.default-colors.secondary:hover,
|
||||
.monaco-button-dropdown.default-colors > .monaco-button.secondary:hover {
|
||||
background-color: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
.monaco-button-dropdown.default-colors .monaco-button-dropdown-separator {
|
||||
background-color: var(--vscode-button-background);
|
||||
border-top: 1px solid var(--vscode-button-border);
|
||||
border-bottom: 1px solid var(--vscode-button-border);
|
||||
}
|
||||
|
||||
.monaco-button-dropdown.default-colors .monaco-button.secondary + .monaco-button-dropdown-separator {
|
||||
background-color: var(--vscode-button-secondaryBackground);
|
||||
}
|
||||
|
||||
.monaco-button-dropdown.default-colors .monaco-button-dropdown-separator > div {
|
||||
background-color: var(--vscode-button-separator);
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
import { EventType, addDisposableListener, EventHelper, trackFocus, reset } from '../../dom.js';
|
||||
import { StandardKeyboardEvent } from '../../keyboardEvent.js';
|
||||
import { renderMarkdown, renderAsPlaintext } from '../../markdownRenderer.js';
|
||||
import { Gesture, EventType as EventType$1 } from '../../touch.js';
|
||||
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
|
||||
import { renderLabelWithIcons } from '../iconLabel/iconLabels.js';
|
||||
import { Color } from '../../../common/color.js';
|
||||
import { Emitter } from '../../../common/event.js';
|
||||
import { isMarkdownString, markdownStringEqual } from '../../../common/htmlContent.js';
|
||||
import { Disposable } from '../../../common/lifecycle.js';
|
||||
import { ThemeIcon } from '../../../common/themables.js';
|
||||
import './button.css';
|
||||
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
|
||||
import { safeSetInnerHtml } from '../../domSanitize.js';
|
||||
|
||||
({
|
||||
buttonSeparator: Color.white.toString(),
|
||||
buttonForeground: Color.white.toString()});
|
||||
// Only allow a very limited set of inline html tags
|
||||
const buttonSanitizerConfig = Object.freeze({
|
||||
allowedTags: {
|
||||
override: ['b', 'i', 'u', 'code', 'span'],
|
||||
},
|
||||
allowedAttributes: {
|
||||
override: ['class'],
|
||||
},
|
||||
});
|
||||
class Button extends Disposable {
|
||||
get onDidClick() { return this._onDidClick.event; }
|
||||
constructor(container, options) {
|
||||
super();
|
||||
this._label = '';
|
||||
this._onDidClick = this._register(new Emitter());
|
||||
this._onDidEscape = this._register(new Emitter());
|
||||
this.options = options;
|
||||
this._element = document.createElement('a');
|
||||
this._element.classList.add('monaco-button');
|
||||
this._element.tabIndex = 0;
|
||||
this._element.setAttribute('role', 'button');
|
||||
this._element.classList.toggle('secondary', !!options.secondary);
|
||||
const background = options.secondary ? options.buttonSecondaryBackground : options.buttonBackground;
|
||||
const foreground = options.secondary ? options.buttonSecondaryForeground : options.buttonForeground;
|
||||
this._element.style.color = foreground || '';
|
||||
this._element.style.backgroundColor = background || '';
|
||||
if (options.supportShortLabel) {
|
||||
this._labelShortElement = document.createElement('div');
|
||||
this._labelShortElement.classList.add('monaco-button-label-short');
|
||||
this._element.appendChild(this._labelShortElement);
|
||||
this._labelElement = document.createElement('div');
|
||||
this._labelElement.classList.add('monaco-button-label');
|
||||
this._element.appendChild(this._labelElement);
|
||||
this._element.classList.add('monaco-text-button-with-short-label');
|
||||
}
|
||||
if (typeof options.title === 'string') {
|
||||
this.setTitle(options.title);
|
||||
}
|
||||
if (typeof options.ariaLabel === 'string') {
|
||||
this._element.setAttribute('aria-label', options.ariaLabel);
|
||||
}
|
||||
container.appendChild(this._element);
|
||||
this.enabled = !options.disabled;
|
||||
this._register(Gesture.addTarget(this._element));
|
||||
[EventType.CLICK, EventType$1.Tap].forEach(eventType => {
|
||||
this._register(addDisposableListener(this._element, eventType, e => {
|
||||
if (!this.enabled) {
|
||||
EventHelper.stop(e);
|
||||
return;
|
||||
}
|
||||
this._onDidClick.fire(e);
|
||||
}));
|
||||
});
|
||||
this._register(addDisposableListener(this._element, EventType.KEY_DOWN, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
let eventHandled = false;
|
||||
if (this.enabled && (event.equals(3 /* KeyCode.Enter */) || event.equals(10 /* KeyCode.Space */))) {
|
||||
this._onDidClick.fire(e);
|
||||
eventHandled = true;
|
||||
}
|
||||
else if (event.equals(9 /* KeyCode.Escape */)) {
|
||||
this._onDidEscape.fire(e);
|
||||
this._element.blur();
|
||||
eventHandled = true;
|
||||
}
|
||||
if (eventHandled) {
|
||||
EventHelper.stop(event, true);
|
||||
}
|
||||
}));
|
||||
this._register(addDisposableListener(this._element, EventType.MOUSE_OVER, e => {
|
||||
if (!this._element.classList.contains('disabled')) {
|
||||
this.updateBackground(true);
|
||||
}
|
||||
}));
|
||||
this._register(addDisposableListener(this._element, EventType.MOUSE_OUT, e => {
|
||||
this.updateBackground(false); // restore standard styles
|
||||
}));
|
||||
// Also set hover background when button is focused for feedback
|
||||
this.focusTracker = this._register(trackFocus(this._element));
|
||||
this._register(this.focusTracker.onDidFocus(() => { if (this.enabled) {
|
||||
this.updateBackground(true);
|
||||
} }));
|
||||
this._register(this.focusTracker.onDidBlur(() => { if (this.enabled) {
|
||||
this.updateBackground(false);
|
||||
} }));
|
||||
}
|
||||
dispose() {
|
||||
super.dispose();
|
||||
this._element.remove();
|
||||
}
|
||||
getContentElements(content) {
|
||||
const elements = [];
|
||||
for (let segment of renderLabelWithIcons(content)) {
|
||||
if (typeof (segment) === 'string') {
|
||||
segment = segment.trim();
|
||||
// Ignore empty segment
|
||||
if (segment === '') {
|
||||
continue;
|
||||
}
|
||||
// Convert string segments to <span> nodes
|
||||
const node = document.createElement('span');
|
||||
node.textContent = segment;
|
||||
elements.push(node);
|
||||
}
|
||||
else {
|
||||
elements.push(segment);
|
||||
}
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
updateBackground(hover) {
|
||||
let background;
|
||||
if (this.options.secondary) {
|
||||
background = hover ? this.options.buttonSecondaryHoverBackground : this.options.buttonSecondaryBackground;
|
||||
}
|
||||
else {
|
||||
background = hover ? this.options.buttonHoverBackground : this.options.buttonBackground;
|
||||
}
|
||||
if (background) {
|
||||
this._element.style.backgroundColor = background;
|
||||
}
|
||||
}
|
||||
get element() {
|
||||
return this._element;
|
||||
}
|
||||
set label(value) {
|
||||
if (this._label === value) {
|
||||
return;
|
||||
}
|
||||
if (isMarkdownString(this._label) && isMarkdownString(value) && markdownStringEqual(this._label, value)) {
|
||||
return;
|
||||
}
|
||||
this._element.classList.add('monaco-text-button');
|
||||
const labelElement = this.options.supportShortLabel ? this._labelElement : this._element;
|
||||
if (isMarkdownString(value)) {
|
||||
const rendered = renderMarkdown(value, undefined, document.createElement('span'));
|
||||
rendered.dispose();
|
||||
// Don't include outer `<p>`
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const root = rendered.element.querySelector('p')?.innerHTML;
|
||||
if (root) {
|
||||
safeSetInnerHtml(labelElement, root, buttonSanitizerConfig);
|
||||
}
|
||||
else {
|
||||
reset(labelElement);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (this.options.supportIcons) {
|
||||
reset(labelElement, ...this.getContentElements(value));
|
||||
}
|
||||
else {
|
||||
labelElement.textContent = value;
|
||||
}
|
||||
}
|
||||
let title = '';
|
||||
if (typeof this.options.title === 'string') {
|
||||
title = this.options.title;
|
||||
}
|
||||
else if (this.options.title) {
|
||||
title = renderAsPlaintext(value);
|
||||
}
|
||||
this.setTitle(title);
|
||||
this._setAriaLabel();
|
||||
this._label = value;
|
||||
}
|
||||
get label() {
|
||||
return this._label;
|
||||
}
|
||||
_setAriaLabel() {
|
||||
if (typeof this.options.ariaLabel === 'string') {
|
||||
this._element.setAttribute('aria-label', this.options.ariaLabel);
|
||||
}
|
||||
else if (typeof this.options.title === 'string') {
|
||||
this._element.setAttribute('aria-label', this.options.title);
|
||||
}
|
||||
}
|
||||
set icon(icon) {
|
||||
this._setAriaLabel();
|
||||
const oldIcons = Array.from(this._element.classList).filter(item => item.startsWith('codicon-'));
|
||||
this._element.classList.remove(...oldIcons);
|
||||
this._element.classList.add(...ThemeIcon.asClassNameArray(icon));
|
||||
}
|
||||
set enabled(value) {
|
||||
if (value) {
|
||||
this._element.classList.remove('disabled');
|
||||
this._element.setAttribute('aria-disabled', String(false));
|
||||
this._element.tabIndex = 0;
|
||||
}
|
||||
else {
|
||||
this._element.classList.add('disabled');
|
||||
this._element.setAttribute('aria-disabled', String(true));
|
||||
}
|
||||
}
|
||||
get enabled() {
|
||||
return !this._element.classList.contains('disabled');
|
||||
}
|
||||
setTitle(title) {
|
||||
if (!this._hover && title !== '') {
|
||||
this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('element'), this._element, title));
|
||||
}
|
||||
else if (this._hover) {
|
||||
this._hover.update(title);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { Button };
|
||||
Generated
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.codicon-wrench-subaction {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
@keyframes codicon-spin {
|
||||
100% {
|
||||
transform:rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.codicon-sync.codicon-modifier-spin,
|
||||
.codicon-loading.codicon-modifier-spin,
|
||||
.codicon-gear.codicon-modifier-spin,
|
||||
.codicon-notebook-state-executing.codicon-modifier-spin {
|
||||
/* Use steps to throttle FPS to reduce CPU usage */
|
||||
animation: codicon-spin 1.5s steps(30) infinite;
|
||||
}
|
||||
|
||||
.codicon-modifier-disabled {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
/* custom speed & easing for loading icon */
|
||||
.codicon-loading,
|
||||
.codicon-tree-item-loading::before {
|
||||
animation-duration: 1s !important;
|
||||
animation-timing-function: cubic-bezier(0.53, 0.21, 0.29, 0.67) !important;
|
||||
}
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {}
|
||||
Generated
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
@font-face {
|
||||
font-family: "codicon";
|
||||
font-display: block;
|
||||
src: url(./codicon.ttf) format("truetype");
|
||||
}
|
||||
|
||||
.codicon[class*='codicon-'] {
|
||||
font: normal normal normal 16px/1 codicon;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
text-rendering: auto;
|
||||
text-align: center;
|
||||
text-transform: none;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
/* icon rules are dynamically created by the platform theme service (see iconsStyleSheet.ts) */
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
export {}
|
||||
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.context-view {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.context-view.fixed {
|
||||
all: initial;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
position: fixed;
|
||||
color: inherit;
|
||||
}
|
||||
Generated
Vendored
+298
@@ -0,0 +1,298 @@
|
||||
import { BrowserFeatures } from '../../canIUse.js';
|
||||
import { $, hide, addStandardDisposableListener, clearNode, show, isHTMLElement, getDomNodePagePosition, getDomNodeZoomLevel, getTotalWidth, getTotalHeight, getActiveWindow, getWindow, isAncestor } from '../../dom.js';
|
||||
import { Disposable, toDisposable, DisposableStore } from '../../../common/lifecycle.js';
|
||||
import { isIOS } from '../../../common/platform.js';
|
||||
import { Range } from '../../../common/range.js';
|
||||
import './contextview.css';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
function isAnchor(obj) {
|
||||
const anchor = obj;
|
||||
return !!anchor && typeof anchor.x === 'number' && typeof anchor.y === 'number';
|
||||
}
|
||||
var LayoutAnchorMode;
|
||||
(function (LayoutAnchorMode) {
|
||||
LayoutAnchorMode[LayoutAnchorMode["AVOID"] = 0] = "AVOID";
|
||||
LayoutAnchorMode[LayoutAnchorMode["ALIGN"] = 1] = "ALIGN";
|
||||
})(LayoutAnchorMode || (LayoutAnchorMode = {}));
|
||||
/**
|
||||
* Lays out a one dimensional view next to an anchor in a viewport.
|
||||
*
|
||||
* @returns The view offset within the viewport.
|
||||
*/
|
||||
function layout(viewportSize, viewSize, anchor) {
|
||||
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 === 0 /* LayoutAnchorPosition.Before */) {
|
||||
if (viewSize <= viewportSize - layoutAfterAnchorBoundary) {
|
||||
return layoutAfterAnchorBoundary; // happy case, lay it out after 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 <= layoutBeforeAnchorBoundary) {
|
||||
return layoutBeforeAnchorBoundary - viewSize; // happy case, lay it out before 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
|
||||
}
|
||||
}
|
||||
class ContextView extends Disposable {
|
||||
static { this.BUBBLE_UP_EVENTS = ['click', 'keydown', 'focus', 'blur']; }
|
||||
static { this.BUBBLE_DOWN_EVENTS = ['click']; }
|
||||
constructor(container, domPosition) {
|
||||
super();
|
||||
this.container = null;
|
||||
this.useFixedPosition = false;
|
||||
this.useShadowDOM = false;
|
||||
this.delegate = null;
|
||||
this.toDisposeOnClean = Disposable.None;
|
||||
this.toDisposeOnSetContainer = Disposable.None;
|
||||
this.shadowRoot = null;
|
||||
this.shadowRootHostElement = null;
|
||||
this.view = $('.context-view');
|
||||
hide(this.view);
|
||||
this.setContainer(container, domPosition);
|
||||
this._register(toDisposable(() => this.setContainer(null, 1 /* ContextViewDOMPosition.ABSOLUTE */)));
|
||||
}
|
||||
setContainer(container, domPosition) {
|
||||
this.useFixedPosition = domPosition !== 1 /* ContextViewDOMPosition.ABSOLUTE */;
|
||||
const usedShadowDOM = this.useShadowDOM;
|
||||
this.useShadowDOM = domPosition === 3 /* ContextViewDOMPosition.FIXED_SHADOW */;
|
||||
if (container === this.container && usedShadowDOM === this.useShadowDOM) {
|
||||
return; // container is the same and no shadow DOM usage has changed
|
||||
}
|
||||
if (this.container) {
|
||||
this.toDisposeOnSetContainer.dispose();
|
||||
this.view.remove();
|
||||
if (this.shadowRoot) {
|
||||
this.shadowRoot = null;
|
||||
this.shadowRootHostElement?.remove();
|
||||
this.shadowRootHostElement = null;
|
||||
}
|
||||
this.container = null;
|
||||
}
|
||||
if (container) {
|
||||
this.container = container;
|
||||
if (this.useShadowDOM) {
|
||||
this.shadowRootHostElement = $('.shadow-root-host');
|
||||
this.container.appendChild(this.shadowRootHostElement);
|
||||
this.shadowRoot = this.shadowRootHostElement.attachShadow({ mode: 'open' });
|
||||
const style = document.createElement('style');
|
||||
style.textContent = SHADOW_ROOT_CSS;
|
||||
this.shadowRoot.appendChild(style);
|
||||
this.shadowRoot.appendChild(this.view);
|
||||
this.shadowRoot.appendChild($('slot'));
|
||||
}
|
||||
else {
|
||||
this.container.appendChild(this.view);
|
||||
}
|
||||
const toDisposeOnSetContainer = new DisposableStore();
|
||||
ContextView.BUBBLE_UP_EVENTS.forEach(event => {
|
||||
toDisposeOnSetContainer.add(addStandardDisposableListener(this.container, event, e => {
|
||||
this.onDOMEvent(e, false);
|
||||
}));
|
||||
});
|
||||
ContextView.BUBBLE_DOWN_EVENTS.forEach(event => {
|
||||
toDisposeOnSetContainer.add(addStandardDisposableListener(this.container, event, e => {
|
||||
this.onDOMEvent(e, true);
|
||||
}, true));
|
||||
});
|
||||
this.toDisposeOnSetContainer = toDisposeOnSetContainer;
|
||||
}
|
||||
}
|
||||
show(delegate) {
|
||||
if (this.isVisible()) {
|
||||
this.hide();
|
||||
}
|
||||
// Show static box
|
||||
clearNode(this.view);
|
||||
this.view.className = 'context-view monaco-component';
|
||||
this.view.style.top = '0px';
|
||||
this.view.style.left = '0px';
|
||||
this.view.style.zIndex = `${2575 + (delegate.layer ?? 0)}`;
|
||||
this.view.style.position = this.useFixedPosition ? 'fixed' : 'absolute';
|
||||
show(this.view);
|
||||
// Render content
|
||||
this.toDisposeOnClean = delegate.render(this.view) || Disposable.None;
|
||||
// Set active delegate
|
||||
this.delegate = delegate;
|
||||
// Layout
|
||||
this.doLayout();
|
||||
// Focus
|
||||
this.delegate.focus?.();
|
||||
}
|
||||
getViewElement() {
|
||||
return this.view;
|
||||
}
|
||||
layout() {
|
||||
if (!this.isVisible()) {
|
||||
return;
|
||||
}
|
||||
if (this.delegate.canRelayout === false && !(isIOS && BrowserFeatures.pointerEvents)) {
|
||||
this.hide();
|
||||
return;
|
||||
}
|
||||
this.delegate?.layout?.();
|
||||
this.doLayout();
|
||||
}
|
||||
doLayout() {
|
||||
// Check that we still have a delegate - this.delegate.layout may have hidden
|
||||
if (!this.isVisible()) {
|
||||
return;
|
||||
}
|
||||
// Get anchor
|
||||
const anchor = this.delegate.getAnchor();
|
||||
// Compute around
|
||||
let around;
|
||||
// Get the element's position and size (to anchor the view)
|
||||
if (isHTMLElement(anchor)) {
|
||||
const elementPosition = getDomNodePagePosition(anchor);
|
||||
// In areas where zoom is applied to the element or its ancestors, we need to adjust the size of the element
|
||||
// e.g. The title bar has counter zoom behavior meaning it applies the inverse of zoom level.
|
||||
// Window Zoom Level: 1.5, Title Bar Zoom: 1/1.5, Size Multiplier: 1.5
|
||||
const zoom = getDomNodeZoomLevel(anchor);
|
||||
around = {
|
||||
top: elementPosition.top * zoom,
|
||||
left: elementPosition.left * zoom,
|
||||
width: elementPosition.width * zoom,
|
||||
height: elementPosition.height * zoom
|
||||
};
|
||||
}
|
||||
else if (isAnchor(anchor)) {
|
||||
around = {
|
||||
top: anchor.y,
|
||||
left: anchor.x,
|
||||
width: anchor.width || 1,
|
||||
height: anchor.height || 2
|
||||
};
|
||||
}
|
||||
else {
|
||||
around = {
|
||||
top: anchor.posy,
|
||||
left: anchor.posx,
|
||||
// We are about to position the context view where the mouse
|
||||
// cursor is. To prevent the view being exactly under the mouse
|
||||
// when showing and thus potentially triggering an action within,
|
||||
// we treat the mouse location like a small sized block element.
|
||||
width: 2,
|
||||
height: 2
|
||||
};
|
||||
}
|
||||
const viewSizeWidth = getTotalWidth(this.view);
|
||||
const viewSizeHeight = getTotalHeight(this.view);
|
||||
const anchorPosition = this.delegate.anchorPosition ?? 0 /* AnchorPosition.BELOW */;
|
||||
const anchorAlignment = this.delegate.anchorAlignment ?? 0 /* AnchorAlignment.LEFT */;
|
||||
const anchorAxisAlignment = this.delegate.anchorAxisAlignment ?? 0 /* AnchorAxisAlignment.VERTICAL */;
|
||||
let top;
|
||||
let left;
|
||||
const activeWindow = getActiveWindow();
|
||||
if (anchorAxisAlignment === 0 /* AnchorAxisAlignment.VERTICAL */) {
|
||||
const verticalAnchor = { offset: around.top - activeWindow.pageYOffset, size: around.height, position: anchorPosition === 0 /* AnchorPosition.BELOW */ ? 0 /* LayoutAnchorPosition.Before */ : 1 /* LayoutAnchorPosition.After */ };
|
||||
const horizontalAnchor = { offset: around.left, size: around.width, position: anchorAlignment === 0 /* AnchorAlignment.LEFT */ ? 0 /* LayoutAnchorPosition.Before */ : 1 /* LayoutAnchorPosition.After */, mode: LayoutAnchorMode.ALIGN };
|
||||
top = layout(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset;
|
||||
// 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(activeWindow.innerWidth, viewSizeWidth, horizontalAnchor);
|
||||
}
|
||||
else {
|
||||
const horizontalAnchor = { offset: around.left, size: around.width, position: anchorAlignment === 0 /* AnchorAlignment.LEFT */ ? 0 /* LayoutAnchorPosition.Before */ : 1 /* LayoutAnchorPosition.After */ };
|
||||
const verticalAnchor = { offset: around.top, size: around.height, position: anchorPosition === 0 /* AnchorPosition.BELOW */ ? 0 /* LayoutAnchorPosition.Before */ : 1 /* LayoutAnchorPosition.After */, mode: LayoutAnchorMode.ALIGN };
|
||||
left = layout(activeWindow.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(activeWindow.innerHeight, viewSizeHeight, verticalAnchor) + activeWindow.pageYOffset;
|
||||
}
|
||||
this.view.classList.remove('top', 'bottom', 'left', 'right');
|
||||
this.view.classList.add(anchorPosition === 0 /* AnchorPosition.BELOW */ ? 'bottom' : 'top');
|
||||
this.view.classList.add(anchorAlignment === 0 /* AnchorAlignment.LEFT */ ? 'left' : 'right');
|
||||
this.view.classList.toggle('fixed', this.useFixedPosition);
|
||||
const containerPosition = getDomNodePagePosition(this.container);
|
||||
// Account for container scroll when positioning the context view
|
||||
const containerScrollTop = this.container.scrollTop || 0;
|
||||
const containerScrollLeft = this.container.scrollLeft || 0;
|
||||
this.view.style.top = `${top - (this.useFixedPosition ? getDomNodePagePosition(this.view).top : containerPosition.top) + containerScrollTop}px`;
|
||||
this.view.style.left = `${left - (this.useFixedPosition ? getDomNodePagePosition(this.view).left : containerPosition.left) + containerScrollLeft}px`;
|
||||
this.view.style.width = 'initial';
|
||||
}
|
||||
hide(data) {
|
||||
const delegate = this.delegate;
|
||||
this.delegate = null;
|
||||
if (delegate?.onHide) {
|
||||
delegate.onHide(data);
|
||||
}
|
||||
this.toDisposeOnClean.dispose();
|
||||
hide(this.view);
|
||||
}
|
||||
isVisible() {
|
||||
return !!this.delegate;
|
||||
}
|
||||
onDOMEvent(e, onCapture) {
|
||||
if (this.delegate) {
|
||||
if (this.delegate.onDOMEvent) {
|
||||
this.delegate.onDOMEvent(e, getWindow(e).document.activeElement);
|
||||
}
|
||||
else if (onCapture && !isAncestor(e.target, this.container)) {
|
||||
this.hide();
|
||||
}
|
||||
}
|
||||
}
|
||||
dispose() {
|
||||
this.hide();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
const SHADOW_ROOT_CSS = /* css */ `
|
||||
:host {
|
||||
all: initial; /* 1st rule so subsequent properties are reset. */
|
||||
}
|
||||
|
||||
.codicon[class*='codicon-'] {
|
||||
font: normal normal normal 16px/1 codicon;
|
||||
display: inline-block;
|
||||
text-decoration: none;
|
||||
text-rendering: auto;
|
||||
text-align: center;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-ms-user-select: none;
|
||||
}
|
||||
|
||||
:host {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe WPC", "Segoe UI", "HelveticaNeue-Light", system-ui, "Ubuntu", "Droid Sans", sans-serif;
|
||||
}
|
||||
|
||||
:host-context(.mac) { font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
|
||||
:host-context(.mac:lang(zh-Hans)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Hiragino Sans GB", sans-serif; }
|
||||
:host-context(.mac:lang(zh-Hant)) { font-family: -apple-system, BlinkMacSystemFont, "PingFang TC", sans-serif; }
|
||||
:host-context(.mac:lang(ja)) { font-family: -apple-system, BlinkMacSystemFont, "Hiragino Kaku Gothic Pro", sans-serif; }
|
||||
:host-context(.mac:lang(ko)) { font-family: -apple-system, BlinkMacSystemFont, "Apple SD Gothic Neo", "Nanum Gothic", "AppleGothic", sans-serif; }
|
||||
|
||||
:host-context(.windows) { font-family: "Segoe WPC", "Segoe UI", sans-serif; }
|
||||
:host-context(.windows:lang(zh-Hans)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft YaHei", sans-serif; }
|
||||
:host-context(.windows:lang(zh-Hant)) { font-family: "Segoe WPC", "Segoe UI", "Microsoft Jhenghei", sans-serif; }
|
||||
:host-context(.windows:lang(ja)) { font-family: "Segoe WPC", "Segoe UI", "Yu Gothic UI", "Meiryo UI", sans-serif; }
|
||||
:host-context(.windows:lang(ko)) { font-family: "Segoe WPC", "Segoe UI", "Malgun Gothic", "Dotom", sans-serif; }
|
||||
|
||||
:host-context(.linux) { font-family: system-ui, "Ubuntu", "Droid Sans", sans-serif; }
|
||||
:host-context(.linux:lang(zh-Hans)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans SC", "Source Han Sans CN", "Source Han Sans", sans-serif; }
|
||||
:host-context(.linux:lang(zh-Hant)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans TC", "Source Han Sans TW", "Source Han Sans", sans-serif; }
|
||||
:host-context(.linux:lang(ja)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans J", "Source Han Sans JP", "Source Han Sans", sans-serif; }
|
||||
:host-context(.linux:lang(ko)) { font-family: system-ui, "Ubuntu", "Droid Sans", "Source Han Sans K", "Source Han Sans JR", "Source Han Sans", "UnDotum", "FBaekmuk Gulim", sans-serif; }
|
||||
`;
|
||||
|
||||
export { ContextView, LayoutAnchorMode, isAnchor, layout };
|
||||
Generated
Vendored
+24
@@ -0,0 +1,24 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-count-badge {
|
||||
padding: 3px 5px;
|
||||
border-radius: 11px;
|
||||
font-size: 11px;
|
||||
min-width: 18px;
|
||||
min-height: 18px;
|
||||
line-height: 11px;
|
||||
font-weight: normal;
|
||||
text-align: center;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monaco-count-badge.long {
|
||||
padding: 2px 3px;
|
||||
border-radius: 2px;
|
||||
min-height: auto;
|
||||
line-height: normal;
|
||||
}
|
||||
Generated
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
import { append, $ } from '../../dom.js';
|
||||
import { format } from '../../../common/strings.js';
|
||||
import './countBadge.css';
|
||||
import { Disposable, MutableDisposable, toDisposable } from '../../../common/lifecycle.js';
|
||||
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class CountBadge extends Disposable {
|
||||
constructor(container, options, styles) {
|
||||
super();
|
||||
this.options = options;
|
||||
this.styles = styles;
|
||||
this.count = 0;
|
||||
this.hover = this._register(new MutableDisposable());
|
||||
this.element = append(container, $('.monaco-count-badge'));
|
||||
this._register(toDisposable(() => container.removeChild(this.element)));
|
||||
this.countFormat = this.options.countFormat || '{0}';
|
||||
this.titleFormat = this.options.titleFormat || '';
|
||||
this.setCount(this.options.count || 0);
|
||||
this.updateHover();
|
||||
}
|
||||
setCount(count) {
|
||||
this.count = count;
|
||||
this.render();
|
||||
}
|
||||
setTitleFormat(titleFormat) {
|
||||
this.titleFormat = titleFormat;
|
||||
this.updateHover();
|
||||
this.render();
|
||||
}
|
||||
updateHover() {
|
||||
if (this.titleFormat !== '' && !this.hover.value) {
|
||||
this.hover.value = getBaseLayerHoverDelegate().setupDelayedHoverAtMouse(this.element, () => ({ content: format(this.titleFormat, this.count), appearance: { compact: true } }));
|
||||
}
|
||||
else if (this.titleFormat === '' && this.hover.value) {
|
||||
this.hover.value = undefined;
|
||||
}
|
||||
}
|
||||
render() {
|
||||
this.element.textContent = format(this.countFormat, this.count);
|
||||
this.element.style.backgroundColor = this.styles.badgeBackground ?? '';
|
||||
this.element.style.color = this.styles.badgeForeground ?? '';
|
||||
if (this.styles.badgeBorder) {
|
||||
this.element.style.border = `1px solid ${this.styles.badgeBorder}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { CountBadge };
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-drag-image {
|
||||
display: inline-block;
|
||||
padding: 1px 7px;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
|
||||
/* Default styles */
|
||||
background-color: var(--vscode-list-activeSelectionBackground);
|
||||
color: var(--vscode-list-activeSelectionForeground);
|
||||
outline: 1px solid var(--vscode-list-focusOutline);
|
||||
outline-offset: -1px;
|
||||
|
||||
/*
|
||||
* Browsers apply an effect to the drag image when the div becomes too
|
||||
* large which makes them unreadable. Use max width so it does not happen
|
||||
*/
|
||||
max-width: 120px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { $ } from '../../dom.js';
|
||||
import './dnd.css';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
function applyDragImage(event, container, label, extraClasses = []) {
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
const dragImage = $('.monaco-drag-image');
|
||||
dragImage.textContent = label;
|
||||
dragImage.classList.add(...extraClasses);
|
||||
const getDragImageContainer = (e) => {
|
||||
while (e && !e.classList.contains('monaco-workbench')) {
|
||||
e = e.parentElement;
|
||||
}
|
||||
return e || container.ownerDocument.body;
|
||||
};
|
||||
const dragContainer = getDragImageContainer(container);
|
||||
dragContainer.appendChild(dragImage);
|
||||
event.dataTransfer.setDragImage(dragImage, -10, -10);
|
||||
// Removes the element when the DND operation is done
|
||||
setTimeout(() => dragImage.remove(), 0);
|
||||
}
|
||||
|
||||
export { applyDragImage };
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-dropdown {
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.monaco-dropdown > .dropdown-label {
|
||||
cursor: pointer;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.monaco-dropdown > .dropdown-label > .action-label.disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-dropdown-with-primary {
|
||||
display: flex !important;
|
||||
flex-direction: row;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.monaco-dropdown-with-primary > .action-container > .action-label {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.monaco-dropdown-with-primary > .dropdown-action-container > .monaco-dropdown > .dropdown-label .codicon[class*='codicon-'] {
|
||||
font-size: 12px;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
line-height: 16px;
|
||||
margin-left: -3px;
|
||||
}
|
||||
|
||||
.monaco-dropdown-with-primary > .dropdown-action-container > .monaco-dropdown > .dropdown-label > .action-label {
|
||||
display: block;
|
||||
background-size: 16px;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
import { append, $, EventType, addDisposableListener, EventHelper, isMouseEvent } from '../../dom.js';
|
||||
import { StandardKeyboardEvent } from '../../keyboardEvent.js';
|
||||
import { EventType as EventType$1, Gesture } from '../../touch.js';
|
||||
import { ActionRunner } from '../../../common/actions.js';
|
||||
import { Emitter } from '../../../common/event.js';
|
||||
import './dropdown.css';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class BaseDropdown extends ActionRunner {
|
||||
constructor(container, options) {
|
||||
super();
|
||||
this._onDidChangeVisibility = this._register(new Emitter());
|
||||
this.onDidChangeVisibility = this._onDidChangeVisibility.event;
|
||||
this._element = append(container, $('.monaco-dropdown'));
|
||||
this._label = append(this._element, $('.dropdown-label'));
|
||||
let labelRenderer = options.labelRenderer;
|
||||
if (!labelRenderer) {
|
||||
labelRenderer = (container) => {
|
||||
container.textContent = options.label || '';
|
||||
return null;
|
||||
};
|
||||
}
|
||||
for (const event of [EventType.CLICK, EventType.MOUSE_DOWN, EventType$1.Tap]) {
|
||||
this._register(addDisposableListener(this.element, event, e => EventHelper.stop(e, true))); // prevent default click behaviour to trigger
|
||||
}
|
||||
for (const event of [EventType.MOUSE_DOWN, EventType$1.Tap]) {
|
||||
this._register(addDisposableListener(this._label, event, e => {
|
||||
if (isMouseEvent(e) && e.button !== 0) {
|
||||
// prevent right click trigger to allow separate context menu (https://github.com/microsoft/vscode/issues/151064)
|
||||
return;
|
||||
}
|
||||
if (this.visible) {
|
||||
this.hide();
|
||||
}
|
||||
else {
|
||||
this.show();
|
||||
}
|
||||
}));
|
||||
}
|
||||
this._register(addDisposableListener(this._label, EventType.KEY_DOWN, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
if (event.equals(3 /* KeyCode.Enter */) || event.equals(10 /* KeyCode.Space */)) {
|
||||
EventHelper.stop(e, true); // https://github.com/microsoft/vscode/issues/57997
|
||||
if (this.visible) {
|
||||
this.hide();
|
||||
}
|
||||
else {
|
||||
this.show();
|
||||
}
|
||||
}
|
||||
}));
|
||||
const cleanupFn = labelRenderer(this._label);
|
||||
if (cleanupFn) {
|
||||
this._register(cleanupFn);
|
||||
}
|
||||
this._register(Gesture.addTarget(this._label));
|
||||
}
|
||||
get element() {
|
||||
return this._element;
|
||||
}
|
||||
show() {
|
||||
if (!this.visible) {
|
||||
this.visible = true;
|
||||
this._onDidChangeVisibility.fire(true);
|
||||
}
|
||||
}
|
||||
hide() {
|
||||
if (this.visible) {
|
||||
this.visible = false;
|
||||
this._onDidChangeVisibility.fire(false);
|
||||
}
|
||||
}
|
||||
dispose() {
|
||||
super.dispose();
|
||||
this.hide();
|
||||
if (this.boxContainer) {
|
||||
this.boxContainer.remove();
|
||||
this.boxContainer = undefined;
|
||||
}
|
||||
if (this.contents) {
|
||||
this.contents.remove();
|
||||
this.contents = undefined;
|
||||
}
|
||||
if (this._label) {
|
||||
this._label.remove();
|
||||
this._label = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
class DropdownMenu extends BaseDropdown {
|
||||
constructor(container, _options) {
|
||||
super(container, _options);
|
||||
this._options = _options;
|
||||
this._actions = [];
|
||||
this.actions = _options.actions || [];
|
||||
}
|
||||
set menuOptions(options) {
|
||||
this._menuOptions = options;
|
||||
}
|
||||
get menuOptions() {
|
||||
return this._menuOptions;
|
||||
}
|
||||
get actions() {
|
||||
if (this._options.actionProvider) {
|
||||
return this._options.actionProvider.getActions();
|
||||
}
|
||||
return this._actions;
|
||||
}
|
||||
set actions(actions) {
|
||||
this._actions = actions;
|
||||
}
|
||||
show() {
|
||||
super.show();
|
||||
this.element.classList.add('active');
|
||||
this._options.contextMenuProvider.showContextMenu({
|
||||
getAnchor: () => this.element,
|
||||
getActions: () => this.actions,
|
||||
getActionsContext: () => this.menuOptions ? this.menuOptions.context : null,
|
||||
getActionViewItem: (action, options) => this.menuOptions && this.menuOptions.actionViewItemProvider ? this.menuOptions.actionViewItemProvider(action, options) : undefined,
|
||||
getKeyBinding: action => this.menuOptions && this.menuOptions.getKeyBinding ? this.menuOptions.getKeyBinding(action) : undefined,
|
||||
getMenuClassName: () => this._options.menuClassName || '',
|
||||
onHide: () => this.onHide(),
|
||||
actionRunner: this.menuOptions ? this.menuOptions.actionRunner : undefined,
|
||||
anchorAlignment: this.menuOptions ? this.menuOptions.anchorAlignment : 0 /* AnchorAlignment.LEFT */,
|
||||
domForShadowRoot: this._options.menuAsChild ? this.element : undefined,
|
||||
skipTelemetry: this._options.skipTelemetry
|
||||
});
|
||||
}
|
||||
hide() {
|
||||
super.hide();
|
||||
}
|
||||
onHide() {
|
||||
this.hide();
|
||||
this.element.classList.remove('active');
|
||||
}
|
||||
}
|
||||
|
||||
export { BaseDropdown, DropdownMenu };
|
||||
Generated
Vendored
+109
@@ -0,0 +1,109 @@
|
||||
import { Emitter } from '../../../common/event.js';
|
||||
import { append, $ } from '../../dom.js';
|
||||
import { BaseActionViewItem } from '../actionbar/actionViewItems.js';
|
||||
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
|
||||
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
|
||||
import './dropdown.css';
|
||||
import { DropdownMenu } from './dropdown.js';
|
||||
|
||||
class DropdownMenuActionViewItem extends BaseActionViewItem {
|
||||
get onDidChangeVisibility() { return this._onDidChangeVisibility.event; }
|
||||
constructor(action, menuActionsOrProvider, contextMenuProvider, options = Object.create(null)) {
|
||||
super(null, action, options);
|
||||
this.actionItem = null;
|
||||
this._onDidChangeVisibility = this._register(new Emitter());
|
||||
this.menuActionsOrProvider = menuActionsOrProvider;
|
||||
this.contextMenuProvider = contextMenuProvider;
|
||||
this.options = options;
|
||||
if (this.options.actionRunner) {
|
||||
this.actionRunner = this.options.actionRunner;
|
||||
}
|
||||
}
|
||||
render(container) {
|
||||
this.actionItem = container;
|
||||
const labelRenderer = (el) => {
|
||||
this.element = append(el, $('a.action-label'));
|
||||
return this.renderLabel(this.element);
|
||||
};
|
||||
const isActionsArray = Array.isArray(this.menuActionsOrProvider);
|
||||
const options = {
|
||||
contextMenuProvider: this.contextMenuProvider,
|
||||
labelRenderer: labelRenderer,
|
||||
menuAsChild: this.options.menuAsChild,
|
||||
actions: isActionsArray ? this.menuActionsOrProvider : undefined,
|
||||
actionProvider: isActionsArray ? undefined : this.menuActionsOrProvider,
|
||||
skipTelemetry: this.options.skipTelemetry
|
||||
};
|
||||
this.dropdownMenu = this._register(new DropdownMenu(container, options));
|
||||
this._register(this.dropdownMenu.onDidChangeVisibility(visible => {
|
||||
this.element?.setAttribute('aria-expanded', `${visible}`);
|
||||
this._onDidChangeVisibility.fire(visible);
|
||||
}));
|
||||
this.dropdownMenu.menuOptions = {
|
||||
actionViewItemProvider: this.options.actionViewItemProvider,
|
||||
actionRunner: this.actionRunner,
|
||||
getKeyBinding: this.options.keybindingProvider,
|
||||
context: this._context
|
||||
};
|
||||
if (this.options.anchorAlignmentProvider) {
|
||||
const that = this;
|
||||
this.dropdownMenu.menuOptions = {
|
||||
...this.dropdownMenu.menuOptions,
|
||||
get anchorAlignment() {
|
||||
return that.options.anchorAlignmentProvider();
|
||||
}
|
||||
};
|
||||
}
|
||||
this.updateTooltip();
|
||||
this.updateEnabled();
|
||||
}
|
||||
renderLabel(element) {
|
||||
let classNames = [];
|
||||
if (typeof this.options.classNames === 'string') {
|
||||
classNames = this.options.classNames.split(/\s+/g).filter(s => !!s);
|
||||
}
|
||||
else if (this.options.classNames) {
|
||||
classNames = this.options.classNames;
|
||||
}
|
||||
// todo@aeschli: remove codicon, should come through `this.options.classNames`
|
||||
if (!classNames.find(c => c === 'icon')) {
|
||||
classNames.push('codicon');
|
||||
}
|
||||
element.classList.add(...classNames);
|
||||
if (this._action.label) {
|
||||
this._register(getBaseLayerHoverDelegate().setupManagedHover(this.options.hoverDelegate ?? getDefaultHoverDelegate('mouse'), element, this._action.label));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
getTooltip() {
|
||||
let title = null;
|
||||
if (this.action.tooltip) {
|
||||
title = this.action.tooltip;
|
||||
}
|
||||
else if (this.action.label) {
|
||||
title = this.action.label;
|
||||
}
|
||||
return title ?? undefined;
|
||||
}
|
||||
setActionContext(newContext) {
|
||||
super.setActionContext(newContext);
|
||||
if (this.dropdownMenu) {
|
||||
if (this.dropdownMenu.menuOptions) {
|
||||
this.dropdownMenu.menuOptions.context = newContext;
|
||||
}
|
||||
else {
|
||||
this.dropdownMenu.menuOptions = { context: newContext };
|
||||
}
|
||||
}
|
||||
}
|
||||
show() {
|
||||
this.dropdownMenu?.show();
|
||||
}
|
||||
updateEnabled() {
|
||||
const disabled = !this.action.enabled;
|
||||
this.actionItem?.classList.toggle('disabled', disabled);
|
||||
this.element?.classList.toggle('disabled', disabled);
|
||||
}
|
||||
}
|
||||
|
||||
export { DropdownMenuActionViewItem };
|
||||
Generated
Vendored
+70
@@ -0,0 +1,70 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
/* ---------- Find input ---------- */
|
||||
|
||||
.monaco-findInput {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.monaco-findInput .monaco-inputbox {
|
||||
font-size: 13px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-findInput > .controls {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
right: 2px;
|
||||
}
|
||||
|
||||
.vs .monaco-findInput.disabled {
|
||||
background-color: #E1E1E1;
|
||||
}
|
||||
|
||||
/* Theming */
|
||||
.vs-dark .monaco-findInput.disabled {
|
||||
background-color: #333;
|
||||
}
|
||||
|
||||
/* Highlighting */
|
||||
.monaco-findInput.highlight-0 .controls,
|
||||
.hc-light .monaco-findInput.highlight-0 .controls {
|
||||
animation: monaco-findInput-highlight-0 100ms linear 0s;
|
||||
}
|
||||
|
||||
.monaco-findInput.highlight-1 .controls,
|
||||
.hc-light .monaco-findInput.highlight-1 .controls {
|
||||
animation: monaco-findInput-highlight-1 100ms linear 0s;
|
||||
}
|
||||
|
||||
.hc-black .monaco-findInput.highlight-0 .controls,
|
||||
.vs-dark .monaco-findInput.highlight-0 .controls {
|
||||
animation: monaco-findInput-highlight-dark-0 100ms linear 0s;
|
||||
}
|
||||
|
||||
.hc-black .monaco-findInput.highlight-1 .controls,
|
||||
.vs-dark .monaco-findInput.highlight-1 .controls {
|
||||
animation: monaco-findInput-highlight-dark-1 100ms linear 0s;
|
||||
}
|
||||
|
||||
@keyframes monaco-findInput-highlight-0 {
|
||||
0% { background: rgba(253, 255, 0, 0.8); }
|
||||
100% { background: transparent; }
|
||||
}
|
||||
@keyframes monaco-findInput-highlight-1 {
|
||||
0% { background: rgba(253, 255, 0, 0.8); }
|
||||
/* Made intentionally different such that the CSS minifier does not collapse the two animations into a single one*/
|
||||
99% { background: transparent; }
|
||||
}
|
||||
|
||||
@keyframes monaco-findInput-highlight-dark-0 {
|
||||
0% { background: rgba(255, 255, 255, 0.44); }
|
||||
100% { background: transparent; }
|
||||
}
|
||||
@keyframes monaco-findInput-highlight-dark-1 {
|
||||
0% { background: rgba(255, 255, 255, 0.44); }
|
||||
/* Made intentionally different such that the CSS minifier does not collapse the two animations into a single one*/
|
||||
99% { background: transparent; }
|
||||
}
|
||||
Generated
Vendored
+294
@@ -0,0 +1,294 @@
|
||||
import { EventHelper, addDisposableListener } from '../../dom.js';
|
||||
import { RegexToggle, WholeWordsToggle, CaseSensitiveToggle } from './findInputToggles.js';
|
||||
import { HistoryInputBox } from '../inputbox/inputBox.js';
|
||||
import { Widget } from '../widget.js';
|
||||
import { Emitter } from '../../../common/event.js';
|
||||
import './findInput.css';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { MutableDisposable, DisposableStore } from '../../../common/lifecycle.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const NLS_DEFAULT_LABEL = localize(1, "input");
|
||||
class FindInput extends Widget {
|
||||
get onDidOptionChange() { return this._onDidOptionChange.event; }
|
||||
get onKeyDown() { return this._onKeyDown.event; }
|
||||
get onMouseDown() { return this._onMouseDown.event; }
|
||||
get onCaseSensitiveKeyDown() { return this._onCaseSensitiveKeyDown.event; }
|
||||
get onRegexKeyDown() { return this._onRegexKeyDown.event; }
|
||||
constructor(parent, contextViewProvider, options) {
|
||||
super();
|
||||
this.fixFocusOnOptionClickEnabled = true;
|
||||
this.imeSessionInProgress = false;
|
||||
this.additionalTogglesDisposables = this._register(new MutableDisposable());
|
||||
this.additionalToggles = [];
|
||||
this._onDidOptionChange = this._register(new Emitter());
|
||||
this._onKeyDown = this._register(new Emitter());
|
||||
this._onMouseDown = this._register(new Emitter());
|
||||
this._onInput = this._register(new Emitter());
|
||||
this._onKeyUp = this._register(new Emitter());
|
||||
this._onCaseSensitiveKeyDown = this._register(new Emitter());
|
||||
this._onRegexKeyDown = this._register(new Emitter());
|
||||
this._lastHighlightFindOptions = 0;
|
||||
this.placeholder = options.placeholder || '';
|
||||
this.validation = options.validation;
|
||||
this.label = options.label || NLS_DEFAULT_LABEL;
|
||||
this.showCommonFindToggles = !!options.showCommonFindToggles;
|
||||
const appendCaseSensitiveLabel = options.appendCaseSensitiveLabel || '';
|
||||
const appendWholeWordsLabel = options.appendWholeWordsLabel || '';
|
||||
const appendRegexLabel = options.appendRegexLabel || '';
|
||||
const flexibleHeight = !!options.flexibleHeight;
|
||||
const flexibleWidth = !!options.flexibleWidth;
|
||||
const flexibleMaxHeight = options.flexibleMaxHeight;
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.classList.add('monaco-findInput');
|
||||
this.inputBox = this._register(new HistoryInputBox(this.domNode, contextViewProvider, {
|
||||
placeholder: this.placeholder || '',
|
||||
ariaLabel: this.label || '',
|
||||
validationOptions: {
|
||||
validation: this.validation
|
||||
},
|
||||
showHistoryHint: options.showHistoryHint,
|
||||
flexibleHeight,
|
||||
flexibleWidth,
|
||||
flexibleMaxHeight,
|
||||
inputBoxStyles: options.inputBoxStyles,
|
||||
history: options.history
|
||||
}));
|
||||
if (this.showCommonFindToggles) {
|
||||
const hoverLifecycleOptions = options?.hoverLifecycleOptions || { groupId: 'find-input' };
|
||||
this.regex = this._register(new RegexToggle({
|
||||
appendTitle: appendRegexLabel,
|
||||
isChecked: false,
|
||||
hoverLifecycleOptions,
|
||||
...options.toggleStyles
|
||||
}));
|
||||
this._register(this.regex.onChange(viaKeyboard => {
|
||||
this._onDidOptionChange.fire(viaKeyboard);
|
||||
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
this.validate();
|
||||
}));
|
||||
this._register(this.regex.onKeyDown(e => {
|
||||
this._onRegexKeyDown.fire(e);
|
||||
}));
|
||||
this.wholeWords = this._register(new WholeWordsToggle({
|
||||
appendTitle: appendWholeWordsLabel,
|
||||
isChecked: false,
|
||||
hoverLifecycleOptions,
|
||||
...options.toggleStyles
|
||||
}));
|
||||
this._register(this.wholeWords.onChange(viaKeyboard => {
|
||||
this._onDidOptionChange.fire(viaKeyboard);
|
||||
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
this.validate();
|
||||
}));
|
||||
this.caseSensitive = this._register(new CaseSensitiveToggle({
|
||||
appendTitle: appendCaseSensitiveLabel,
|
||||
isChecked: false,
|
||||
hoverLifecycleOptions,
|
||||
...options.toggleStyles
|
||||
}));
|
||||
this._register(this.caseSensitive.onChange(viaKeyboard => {
|
||||
this._onDidOptionChange.fire(viaKeyboard);
|
||||
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
this.validate();
|
||||
}));
|
||||
this._register(this.caseSensitive.onKeyDown(e => {
|
||||
this._onCaseSensitiveKeyDown.fire(e);
|
||||
}));
|
||||
// Arrow-Key support to navigate between options
|
||||
const indexes = [this.caseSensitive.domNode, this.wholeWords.domNode, this.regex.domNode];
|
||||
this.onkeydown(this.domNode, (event) => {
|
||||
if (event.equals(15 /* KeyCode.LeftArrow */) || event.equals(17 /* KeyCode.RightArrow */) || event.equals(9 /* KeyCode.Escape */)) {
|
||||
const index = indexes.indexOf(this.domNode.ownerDocument.activeElement);
|
||||
if (index >= 0) {
|
||||
let newIndex = -1;
|
||||
if (event.equals(17 /* KeyCode.RightArrow */)) {
|
||||
newIndex = (index + 1) % indexes.length;
|
||||
}
|
||||
else if (event.equals(15 /* KeyCode.LeftArrow */)) {
|
||||
if (index === 0) {
|
||||
newIndex = indexes.length - 1;
|
||||
}
|
||||
else {
|
||||
newIndex = index - 1;
|
||||
}
|
||||
}
|
||||
if (event.equals(9 /* KeyCode.Escape */)) {
|
||||
indexes[index].blur();
|
||||
this.inputBox.focus();
|
||||
}
|
||||
else if (newIndex >= 0) {
|
||||
indexes[newIndex].focus();
|
||||
}
|
||||
EventHelper.stop(event, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
this.controls = document.createElement('div');
|
||||
this.controls.className = 'controls';
|
||||
this.controls.style.display = this.showCommonFindToggles ? '' : 'none';
|
||||
if (this.caseSensitive) {
|
||||
this.controls.append(this.caseSensitive.domNode);
|
||||
}
|
||||
if (this.wholeWords) {
|
||||
this.controls.appendChild(this.wholeWords.domNode);
|
||||
}
|
||||
if (this.regex) {
|
||||
this.controls.appendChild(this.regex.domNode);
|
||||
}
|
||||
this.setAdditionalToggles(options?.additionalToggles);
|
||||
if (this.controls) {
|
||||
this.domNode.appendChild(this.controls);
|
||||
}
|
||||
parent?.appendChild(this.domNode);
|
||||
this._register(addDisposableListener(this.inputBox.inputElement, 'compositionstart', (e) => {
|
||||
this.imeSessionInProgress = true;
|
||||
}));
|
||||
this._register(addDisposableListener(this.inputBox.inputElement, 'compositionend', (e) => {
|
||||
this.imeSessionInProgress = false;
|
||||
this._onInput.fire();
|
||||
}));
|
||||
this.onkeydown(this.inputBox.inputElement, (e) => this._onKeyDown.fire(e));
|
||||
this.onkeyup(this.inputBox.inputElement, (e) => this._onKeyUp.fire(e));
|
||||
this.oninput(this.inputBox.inputElement, (e) => this._onInput.fire());
|
||||
this.onmousedown(this.inputBox.inputElement, (e) => this._onMouseDown.fire(e));
|
||||
}
|
||||
get onDidChange() {
|
||||
return this.inputBox.onDidChange;
|
||||
}
|
||||
layout(style) {
|
||||
this.inputBox.layout();
|
||||
this.updateInputBoxPadding(style.collapsedFindWidget);
|
||||
}
|
||||
enable() {
|
||||
this.domNode.classList.remove('disabled');
|
||||
this.inputBox.enable();
|
||||
this.regex?.enable();
|
||||
this.wholeWords?.enable();
|
||||
this.caseSensitive?.enable();
|
||||
for (const toggle of this.additionalToggles) {
|
||||
toggle.enable();
|
||||
}
|
||||
}
|
||||
disable() {
|
||||
this.domNode.classList.add('disabled');
|
||||
this.inputBox.disable();
|
||||
this.regex?.disable();
|
||||
this.wholeWords?.disable();
|
||||
this.caseSensitive?.disable();
|
||||
for (const toggle of this.additionalToggles) {
|
||||
toggle.disable();
|
||||
}
|
||||
}
|
||||
setFocusInputOnOptionClick(value) {
|
||||
this.fixFocusOnOptionClickEnabled = value;
|
||||
}
|
||||
setEnabled(enabled) {
|
||||
if (enabled) {
|
||||
this.enable();
|
||||
}
|
||||
else {
|
||||
this.disable();
|
||||
}
|
||||
}
|
||||
setAdditionalToggles(toggles) {
|
||||
for (const currentToggle of this.additionalToggles) {
|
||||
currentToggle.domNode.remove();
|
||||
}
|
||||
this.additionalToggles = [];
|
||||
this.additionalTogglesDisposables.value = new DisposableStore();
|
||||
for (const toggle of toggles ?? []) {
|
||||
this.additionalTogglesDisposables.value.add(toggle);
|
||||
this.controls.appendChild(toggle.domNode);
|
||||
this.additionalTogglesDisposables.value.add(toggle.onChange(viaKeyboard => {
|
||||
this._onDidOptionChange.fire(viaKeyboard);
|
||||
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
}));
|
||||
this.additionalToggles.push(toggle);
|
||||
}
|
||||
if (this.additionalToggles.length > 0) {
|
||||
this.controls.style.display = '';
|
||||
}
|
||||
this.updateInputBoxPadding();
|
||||
}
|
||||
updateInputBoxPadding(controlsHidden = false) {
|
||||
if (controlsHidden) {
|
||||
this.inputBox.paddingRight = 0;
|
||||
}
|
||||
else {
|
||||
this.inputBox.paddingRight =
|
||||
((this.caseSensitive?.width() ?? 0) + (this.wholeWords?.width() ?? 0) + (this.regex?.width() ?? 0))
|
||||
+ this.additionalToggles.reduce((r, t) => r + t.width(), 0);
|
||||
}
|
||||
}
|
||||
getValue() {
|
||||
return this.inputBox.value;
|
||||
}
|
||||
setValue(value) {
|
||||
if (this.inputBox.value !== value) {
|
||||
this.inputBox.value = value;
|
||||
}
|
||||
}
|
||||
select() {
|
||||
this.inputBox.select();
|
||||
}
|
||||
focus() {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
getCaseSensitive() {
|
||||
return this.caseSensitive?.checked ?? false;
|
||||
}
|
||||
setCaseSensitive(value) {
|
||||
if (this.caseSensitive) {
|
||||
this.caseSensitive.checked = value;
|
||||
}
|
||||
}
|
||||
getWholeWords() {
|
||||
return this.wholeWords?.checked ?? false;
|
||||
}
|
||||
setWholeWords(value) {
|
||||
if (this.wholeWords) {
|
||||
this.wholeWords.checked = value;
|
||||
}
|
||||
}
|
||||
getRegex() {
|
||||
return this.regex?.checked ?? false;
|
||||
}
|
||||
setRegex(value) {
|
||||
if (this.regex) {
|
||||
this.regex.checked = value;
|
||||
this.validate();
|
||||
}
|
||||
}
|
||||
focusOnCaseSensitive() {
|
||||
this.caseSensitive?.focus();
|
||||
}
|
||||
highlightFindOptions() {
|
||||
this.domNode.classList.remove('highlight-' + (this._lastHighlightFindOptions));
|
||||
this._lastHighlightFindOptions = 1 - this._lastHighlightFindOptions;
|
||||
this.domNode.classList.add('highlight-' + (this._lastHighlightFindOptions));
|
||||
}
|
||||
validate() {
|
||||
this.inputBox.validate();
|
||||
}
|
||||
showMessage(message) {
|
||||
this.inputBox.showMessage(message);
|
||||
}
|
||||
clearMessage() {
|
||||
this.inputBox.hideMessage();
|
||||
}
|
||||
}
|
||||
|
||||
export { FindInput };
|
||||
Generated
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
import { Toggle } from '../toggle/toggle.js';
|
||||
import { Codicon } from '../../../common/codicons.js';
|
||||
import { localize } from '../../../../nls.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const NLS_CASE_SENSITIVE_TOGGLE_LABEL = localize(2, "Match Case");
|
||||
const NLS_WHOLE_WORD_TOGGLE_LABEL = localize(3, "Match Whole Word");
|
||||
const NLS_REGEX_TOGGLE_LABEL = localize(4, "Use Regular Expression");
|
||||
class CaseSensitiveToggle extends Toggle {
|
||||
constructor(opts) {
|
||||
super({
|
||||
icon: Codicon.caseSensitive,
|
||||
title: NLS_CASE_SENSITIVE_TOGGLE_LABEL + opts.appendTitle,
|
||||
isChecked: opts.isChecked,
|
||||
hoverLifecycleOptions: opts.hoverLifecycleOptions,
|
||||
inputActiveOptionBorder: opts.inputActiveOptionBorder,
|
||||
inputActiveOptionForeground: opts.inputActiveOptionForeground,
|
||||
inputActiveOptionBackground: opts.inputActiveOptionBackground
|
||||
});
|
||||
}
|
||||
}
|
||||
class WholeWordsToggle extends Toggle {
|
||||
constructor(opts) {
|
||||
super({
|
||||
icon: Codicon.wholeWord,
|
||||
title: NLS_WHOLE_WORD_TOGGLE_LABEL + opts.appendTitle,
|
||||
isChecked: opts.isChecked,
|
||||
hoverLifecycleOptions: opts.hoverLifecycleOptions,
|
||||
inputActiveOptionBorder: opts.inputActiveOptionBorder,
|
||||
inputActiveOptionForeground: opts.inputActiveOptionForeground,
|
||||
inputActiveOptionBackground: opts.inputActiveOptionBackground
|
||||
});
|
||||
}
|
||||
}
|
||||
class RegexToggle extends Toggle {
|
||||
constructor(opts) {
|
||||
super({
|
||||
icon: Codicon.regex,
|
||||
title: NLS_REGEX_TOGGLE_LABEL + opts.appendTitle,
|
||||
isChecked: opts.isChecked,
|
||||
hoverLifecycleOptions: opts.hoverLifecycleOptions,
|
||||
inputActiveOptionBorder: opts.inputActiveOptionBorder,
|
||||
inputActiveOptionForeground: opts.inputActiveOptionForeground,
|
||||
inputActiveOptionBackground: opts.inputActiveOptionBackground
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { CaseSensitiveToggle, RegexToggle, WholeWordsToggle };
|
||||
Generated
Vendored
+176
@@ -0,0 +1,176 @@
|
||||
import { EventHelper } from '../../dom.js';
|
||||
import { Toggle } from '../toggle/toggle.js';
|
||||
import { HistoryInputBox } from '../inputbox/inputBox.js';
|
||||
import { Widget } from '../widget.js';
|
||||
import { Codicon } from '../../../common/codicons.js';
|
||||
import { Emitter } from '../../../common/event.js';
|
||||
import './findInput.css';
|
||||
import { localize } from '../../../../nls.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const NLS_DEFAULT_LABEL = localize(5, "input");
|
||||
const NLS_PRESERVE_CASE_LABEL = localize(6, "Preserve Case");
|
||||
class PreserveCaseToggle extends Toggle {
|
||||
constructor(opts) {
|
||||
super({
|
||||
// TODO: does this need its own icon?
|
||||
icon: Codicon.preserveCase,
|
||||
title: NLS_PRESERVE_CASE_LABEL + opts.appendTitle,
|
||||
isChecked: opts.isChecked,
|
||||
hoverLifecycleOptions: opts.hoverLifecycleOptions,
|
||||
inputActiveOptionBorder: opts.inputActiveOptionBorder,
|
||||
inputActiveOptionForeground: opts.inputActiveOptionForeground,
|
||||
inputActiveOptionBackground: opts.inputActiveOptionBackground,
|
||||
});
|
||||
}
|
||||
}
|
||||
class ReplaceInput extends Widget {
|
||||
get onDidOptionChange() { return this._onDidOptionChange.event; }
|
||||
get onKeyDown() { return this._onKeyDown.event; }
|
||||
get onPreserveCaseKeyDown() { return this._onPreserveCaseKeyDown.event; }
|
||||
constructor(parent, contextViewProvider, _showOptionButtons, options) {
|
||||
super();
|
||||
this._showOptionButtons = _showOptionButtons;
|
||||
this.fixFocusOnOptionClickEnabled = true;
|
||||
this.cachedOptionsWidth = 0;
|
||||
this._onDidOptionChange = this._register(new Emitter());
|
||||
this._onKeyDown = this._register(new Emitter());
|
||||
this._onMouseDown = this._register(new Emitter());
|
||||
this._onInput = this._register(new Emitter());
|
||||
this._onKeyUp = this._register(new Emitter());
|
||||
this._onPreserveCaseKeyDown = this._register(new Emitter());
|
||||
this.contextViewProvider = contextViewProvider;
|
||||
this.placeholder = options.placeholder || '';
|
||||
this.validation = options.validation;
|
||||
this.label = options.label || NLS_DEFAULT_LABEL;
|
||||
const appendPreserveCaseLabel = options.appendPreserveCaseLabel || '';
|
||||
const history = options.history || new Set([]);
|
||||
const flexibleHeight = !!options.flexibleHeight;
|
||||
const flexibleWidth = !!options.flexibleWidth;
|
||||
const flexibleMaxHeight = options.flexibleMaxHeight;
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.classList.add('monaco-findInput');
|
||||
this.inputBox = this._register(new HistoryInputBox(this.domNode, this.contextViewProvider, {
|
||||
ariaLabel: this.label || '',
|
||||
placeholder: this.placeholder || '',
|
||||
validationOptions: {
|
||||
validation: this.validation
|
||||
},
|
||||
history,
|
||||
showHistoryHint: options.showHistoryHint,
|
||||
flexibleHeight,
|
||||
flexibleWidth,
|
||||
flexibleMaxHeight,
|
||||
inputBoxStyles: options.inputBoxStyles
|
||||
}));
|
||||
this.preserveCase = this._register(new PreserveCaseToggle({
|
||||
appendTitle: appendPreserveCaseLabel,
|
||||
isChecked: false,
|
||||
hoverLifecycleOptions: options.hoverLifecycleOptions,
|
||||
...options.toggleStyles
|
||||
}));
|
||||
this._register(this.preserveCase.onChange(viaKeyboard => {
|
||||
this._onDidOptionChange.fire(viaKeyboard);
|
||||
if (!viaKeyboard && this.fixFocusOnOptionClickEnabled) {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
this.validate();
|
||||
}));
|
||||
this._register(this.preserveCase.onKeyDown(e => {
|
||||
this._onPreserveCaseKeyDown.fire(e);
|
||||
}));
|
||||
if (this._showOptionButtons) {
|
||||
this.cachedOptionsWidth = this.preserveCase.width();
|
||||
}
|
||||
else {
|
||||
this.cachedOptionsWidth = 0;
|
||||
}
|
||||
// Arrow-Key support to navigate between options
|
||||
const indexes = [this.preserveCase.domNode];
|
||||
this.onkeydown(this.domNode, (event) => {
|
||||
if (event.equals(15 /* KeyCode.LeftArrow */) || event.equals(17 /* KeyCode.RightArrow */) || event.equals(9 /* KeyCode.Escape */)) {
|
||||
const index = indexes.indexOf(this.domNode.ownerDocument.activeElement);
|
||||
if (index >= 0) {
|
||||
let newIndex = -1;
|
||||
if (event.equals(17 /* KeyCode.RightArrow */)) {
|
||||
newIndex = (index + 1) % indexes.length;
|
||||
}
|
||||
else if (event.equals(15 /* KeyCode.LeftArrow */)) {
|
||||
if (index === 0) {
|
||||
newIndex = indexes.length - 1;
|
||||
}
|
||||
else {
|
||||
newIndex = index - 1;
|
||||
}
|
||||
}
|
||||
if (event.equals(9 /* KeyCode.Escape */)) {
|
||||
indexes[index].blur();
|
||||
this.inputBox.focus();
|
||||
}
|
||||
else if (newIndex >= 0) {
|
||||
indexes[newIndex].focus();
|
||||
}
|
||||
EventHelper.stop(event, true);
|
||||
}
|
||||
}
|
||||
});
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'controls';
|
||||
controls.style.display = this._showOptionButtons ? 'block' : 'none';
|
||||
controls.appendChild(this.preserveCase.domNode);
|
||||
this.domNode.appendChild(controls);
|
||||
parent?.appendChild(this.domNode);
|
||||
this.onkeydown(this.inputBox.inputElement, (e) => this._onKeyDown.fire(e));
|
||||
this.onkeyup(this.inputBox.inputElement, (e) => this._onKeyUp.fire(e));
|
||||
this.oninput(this.inputBox.inputElement, (e) => this._onInput.fire());
|
||||
this.onmousedown(this.inputBox.inputElement, (e) => this._onMouseDown.fire(e));
|
||||
}
|
||||
enable() {
|
||||
this.domNode.classList.remove('disabled');
|
||||
this.inputBox.enable();
|
||||
this.preserveCase.enable();
|
||||
}
|
||||
disable() {
|
||||
this.domNode.classList.add('disabled');
|
||||
this.inputBox.disable();
|
||||
this.preserveCase.disable();
|
||||
}
|
||||
setEnabled(enabled) {
|
||||
if (enabled) {
|
||||
this.enable();
|
||||
}
|
||||
else {
|
||||
this.disable();
|
||||
}
|
||||
}
|
||||
select() {
|
||||
this.inputBox.select();
|
||||
}
|
||||
focus() {
|
||||
this.inputBox.focus();
|
||||
}
|
||||
getPreserveCase() {
|
||||
return this.preserveCase.checked;
|
||||
}
|
||||
setPreserveCase(value) {
|
||||
this.preserveCase.checked = value;
|
||||
}
|
||||
focusOnPreserve() {
|
||||
this.preserveCase.focus();
|
||||
}
|
||||
validate() {
|
||||
this.inputBox?.validate();
|
||||
}
|
||||
set width(newWidth) {
|
||||
this.inputBox.paddingRight = this.cachedOptionsWidth;
|
||||
this.domNode.style.width = newWidth + 'px';
|
||||
}
|
||||
dispose() {
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export { ReplaceInput };
|
||||
Generated
Vendored
+129
@@ -0,0 +1,129 @@
|
||||
import { append, $, reset } from '../../dom.js';
|
||||
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
|
||||
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
|
||||
import { renderLabelWithIcons } from '../iconLabel/iconLabels.js';
|
||||
import { Disposable } from '../../../common/lifecycle.js';
|
||||
import { equals } from '../../../common/objects.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
/**
|
||||
* A widget which can render a label with substring highlights, often
|
||||
* originating from a filter function like the fuzzy matcher.
|
||||
*/
|
||||
class HighlightedLabel extends Disposable {
|
||||
/**
|
||||
* Create a new {@link HighlightedLabel}.
|
||||
*
|
||||
* @param container The parent container to append to.
|
||||
*/
|
||||
constructor(container, options) {
|
||||
super();
|
||||
this.options = options;
|
||||
this.text = '';
|
||||
this.title = '';
|
||||
this.highlights = [];
|
||||
this.didEverRender = false;
|
||||
this.domNode = append(container, $('span.monaco-highlighted-label'));
|
||||
}
|
||||
/**
|
||||
* The label's DOM node.
|
||||
*/
|
||||
get element() {
|
||||
return this.domNode;
|
||||
}
|
||||
/**
|
||||
* Set the label and highlights.
|
||||
*
|
||||
* @param text The label to display.
|
||||
* @param highlights The ranges to highlight.
|
||||
* @param title An optional title for the hover tooltip.
|
||||
* @param escapeNewLines Whether to escape new lines.
|
||||
* @returns
|
||||
*/
|
||||
set(text, highlights = [], title = '', escapeNewLines, supportIcons) {
|
||||
if (!text) {
|
||||
text = '';
|
||||
}
|
||||
if (escapeNewLines) {
|
||||
// adjusts highlights inplace
|
||||
text = HighlightedLabel.escapeNewLines(text, highlights);
|
||||
}
|
||||
if (this.didEverRender && this.text === text && this.title === title && equals(this.highlights, highlights)) {
|
||||
return;
|
||||
}
|
||||
this.text = text;
|
||||
this.title = title;
|
||||
this.highlights = highlights;
|
||||
this.render(supportIcons);
|
||||
}
|
||||
render(supportIcons) {
|
||||
const children = [];
|
||||
let pos = 0;
|
||||
for (const highlight of this.highlights) {
|
||||
if (highlight.end === highlight.start) {
|
||||
continue;
|
||||
}
|
||||
if (pos < highlight.start) {
|
||||
const substring = this.text.substring(pos, highlight.start);
|
||||
if (supportIcons) {
|
||||
children.push(...renderLabelWithIcons(substring));
|
||||
}
|
||||
else {
|
||||
children.push(substring);
|
||||
}
|
||||
pos = highlight.start;
|
||||
}
|
||||
const substring = this.text.substring(pos, highlight.end);
|
||||
const element = $('span.highlight', undefined, ...supportIcons ? renderLabelWithIcons(substring) : [substring]);
|
||||
if (highlight.extraClasses) {
|
||||
element.classList.add(...highlight.extraClasses);
|
||||
}
|
||||
children.push(element);
|
||||
pos = highlight.end;
|
||||
}
|
||||
if (pos < this.text.length) {
|
||||
const substring = this.text.substring(pos);
|
||||
if (supportIcons) {
|
||||
children.push(...renderLabelWithIcons(substring));
|
||||
}
|
||||
else {
|
||||
children.push(substring);
|
||||
}
|
||||
}
|
||||
reset(this.domNode, ...children);
|
||||
if (!this.customHover && this.title !== '') {
|
||||
const hoverDelegate = this.options?.hoverDelegate ?? getDefaultHoverDelegate('mouse');
|
||||
this.customHover = this._register(getBaseLayerHoverDelegate().setupManagedHover(hoverDelegate, this.domNode, this.title));
|
||||
}
|
||||
else if (this.customHover) {
|
||||
this.customHover.update(this.title);
|
||||
}
|
||||
this.didEverRender = true;
|
||||
}
|
||||
static escapeNewLines(text, highlights) {
|
||||
let total = 0;
|
||||
let extra = 0;
|
||||
return text.replace(/\r\n|\r|\n/g, (match, offset) => {
|
||||
extra = match === '\r\n' ? -1 : 0;
|
||||
offset += total;
|
||||
for (const highlight of highlights) {
|
||||
if (highlight.end <= offset) {
|
||||
continue;
|
||||
}
|
||||
if (highlight.start >= offset) {
|
||||
highlight.start += extra;
|
||||
}
|
||||
if (highlight.end >= offset) {
|
||||
highlight.end += extra;
|
||||
}
|
||||
}
|
||||
total += extra;
|
||||
return '\u23CE';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { HighlightedLabel };
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
function isManagedHoverTooltipMarkdownString(obj) {
|
||||
const candidate = obj;
|
||||
return typeof candidate === 'object' && 'markdown' in candidate && 'markdownNotSupportedFallback' in candidate;
|
||||
}
|
||||
// #endregion Managed hover
|
||||
|
||||
export { isManagedHoverTooltipMarkdownString };
|
||||
Generated
Vendored
+39
@@ -0,0 +1,39 @@
|
||||
import { Disposable } from '../../../common/lifecycle.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
let baseHoverDelegate = {
|
||||
showInstantHover: () => undefined,
|
||||
showDelayedHover: () => undefined,
|
||||
setupDelayedHover: () => Disposable.None,
|
||||
setupDelayedHoverAtMouse: () => Disposable.None,
|
||||
hideHover: () => undefined,
|
||||
showAndFocusLastHover: () => undefined,
|
||||
setupManagedHover: () => ({
|
||||
dispose: () => undefined,
|
||||
show: () => undefined,
|
||||
hide: () => undefined,
|
||||
update: () => undefined,
|
||||
}),
|
||||
showManagedHover: () => undefined
|
||||
};
|
||||
/**
|
||||
* Sets the hover delegate for use **only in the `base/` layer**.
|
||||
*/
|
||||
function setBaseLayerHoverDelegate(hoverDelegate) {
|
||||
baseHoverDelegate = hoverDelegate;
|
||||
}
|
||||
/**
|
||||
* Gets the hover delegate for use **only in the `base/` layer**.
|
||||
*
|
||||
* Since the hover service depends on various platform services, this delegate essentially bypasses
|
||||
* the standard dependency injection mechanism by injecting a global hover service at start up. The
|
||||
* only reason this should be used is if `IHoverService` is not available.
|
||||
*/
|
||||
function getBaseLayerHoverDelegate() {
|
||||
return baseHoverDelegate;
|
||||
}
|
||||
|
||||
export { getBaseLayerHoverDelegate, setBaseLayerHoverDelegate };
|
||||
Generated
Vendored
+34
@@ -0,0 +1,34 @@
|
||||
import { Lazy } from '../../../common/lazy.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const nullHoverDelegateFactory = () => ({
|
||||
get delay() { return -1; },
|
||||
dispose: () => { },
|
||||
showHover: () => { return undefined; },
|
||||
});
|
||||
let hoverDelegateFactory = nullHoverDelegateFactory;
|
||||
const defaultHoverDelegateMouse = new Lazy(() => hoverDelegateFactory('mouse', false));
|
||||
const defaultHoverDelegateElement = new Lazy(() => hoverDelegateFactory('element', false));
|
||||
// TODO: Remove when getDefaultHoverDelegate is no longer used
|
||||
function setHoverDelegateFactory(hoverDelegateProvider) {
|
||||
hoverDelegateFactory = hoverDelegateProvider;
|
||||
}
|
||||
// TODO: Refine type for use in new IHoverService interface
|
||||
function getDefaultHoverDelegate(placement) {
|
||||
if (placement === 'element') {
|
||||
return defaultHoverDelegateElement.value;
|
||||
}
|
||||
return defaultHoverDelegateMouse.value;
|
||||
}
|
||||
// TODO: Create equivalent in IHoverService
|
||||
function createInstantHoverDelegate() {
|
||||
// Creates a hover delegate with instant hover enabled.
|
||||
// This hover belongs to the consumer and requires the them to dispose it.
|
||||
// Instant hover only makes sense for 'element' placement.
|
||||
return hoverDelegateFactory('element', true);
|
||||
}
|
||||
|
||||
export { createInstantHoverDelegate, getDefaultHoverDelegate, setHoverDelegateFactory };
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-hover {
|
||||
cursor: default;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
box-sizing: border-box;
|
||||
line-height: 1.5em;
|
||||
white-space: var(--vscode-hover-whiteSpace, normal);
|
||||
}
|
||||
|
||||
.monaco-hover.fade-in {
|
||||
animation: fadein 100ms linear;
|
||||
}
|
||||
|
||||
.monaco-hover.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-hover a:hover:not(.disabled) {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-contents:not(.html-hover-contents) {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.monaco-hover .markdown-hover > .hover-contents:not(.code-hover-contents) {
|
||||
max-width: var(--vscode-hover-maxWidth, 500px);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.monaco-hover .markdown-hover > .hover-contents:not(.code-hover-contents) hr {
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.monaco-hover p,
|
||||
.monaco-hover .code,
|
||||
.monaco-hover ul,
|
||||
.monaco-hover h1,
|
||||
.monaco-hover h2,
|
||||
.monaco-hover h3,
|
||||
.monaco-hover h4,
|
||||
.monaco-hover h5,
|
||||
.monaco-hover h6 {
|
||||
margin: 8px 0;
|
||||
}
|
||||
|
||||
.monaco-hover h1,
|
||||
.monaco-hover h2,
|
||||
.monaco-hover h3,
|
||||
.monaco-hover h4,
|
||||
.monaco-hover h5,
|
||||
.monaco-hover h6 {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.monaco-hover code {
|
||||
font-family: var(--monaco-monospace-font);
|
||||
}
|
||||
|
||||
.monaco-hover hr {
|
||||
box-sizing: border-box;
|
||||
border-left: 0px;
|
||||
border-right: 0px;
|
||||
margin-top: 4px;
|
||||
margin-bottom: -4px;
|
||||
margin-left: -8px;
|
||||
margin-right: -8px;
|
||||
height: 1px;
|
||||
}
|
||||
|
||||
.monaco-hover p:first-child,
|
||||
.monaco-hover .code:first-child,
|
||||
.monaco-hover ul:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.monaco-hover p:last-child,
|
||||
.monaco-hover .code:last-child,
|
||||
.monaco-hover ul:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
/* MarkupContent Layout */
|
||||
.monaco-hover ul {
|
||||
padding-left: 20px;
|
||||
}
|
||||
.monaco-hover ol {
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.monaco-hover li > p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.monaco-hover li > ul {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.monaco-hover code {
|
||||
border-radius: 3px;
|
||||
padding: 0 0.4em;
|
||||
}
|
||||
|
||||
.monaco-hover .monaco-tokenized-source {
|
||||
white-space: var(--vscode-hover-sourceWhiteSpace, pre-wrap);
|
||||
}
|
||||
|
||||
.monaco-hover .hover-row.status-bar {
|
||||
font-size: 12px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-row.status-bar .info {
|
||||
font-style: italic;
|
||||
padding: 0px 8px;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-row.status-bar .actions {
|
||||
display: flex;
|
||||
padding: 0px 8px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-row.status-bar .actions .action-container {
|
||||
margin-right: 16px;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
text-wrap: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-row.status-bar .actions .action-container .action .icon {
|
||||
padding-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-row.status-bar .actions .action-container a {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
text-decoration: var(--text-link-decoration);
|
||||
}
|
||||
|
||||
.monaco-hover .hover-row.status-bar .actions .action-container a .icon.codicon {
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
.monaco-hover .markdown-hover .hover-contents .codicon {
|
||||
color: inherit;
|
||||
font-size: inherit;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-contents a.code-link:hover,
|
||||
.monaco-hover .hover-contents a.code-link {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.monaco-hover .hover-contents a.code-link:before {
|
||||
content: '(';
|
||||
}
|
||||
|
||||
.monaco-hover .hover-contents a.code-link:after {
|
||||
content: ')';
|
||||
}
|
||||
|
||||
.monaco-hover .hover-contents a.code-link > span {
|
||||
text-decoration: underline;
|
||||
/** Hack to force underline to show **/
|
||||
border-bottom: 1px solid transparent;
|
||||
text-underline-position: under;
|
||||
color: var(--vscode-textLink-foreground);
|
||||
}
|
||||
|
||||
.monaco-hover .hover-contents a.code-link > span:hover {
|
||||
color: var(--vscode-textLink-activeForeground);
|
||||
}
|
||||
|
||||
/**
|
||||
* Spans in markdown hovers need a margin-bottom to avoid looking cramped:
|
||||
* https://github.com/microsoft/vscode/issues/101496
|
||||
|
||||
* This was later refined to only apply when the last child of a rendered markdown block (before the
|
||||
* border or a `hr`) uses background color:
|
||||
* https://github.com/microsoft/vscode/issues/228136
|
||||
*/
|
||||
.monaco-hover .markdown-hover .hover-contents:not(.code-hover-contents):not(.html-hover-contents) p:last-child [style*="background-color"] {
|
||||
margin-bottom: 4px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a slight margin to try vertically align codicons with any text
|
||||
* https://github.com/microsoft/vscode/issues/221359
|
||||
*/
|
||||
.monaco-hover .markdown-hover .hover-contents:not(.code-hover-contents):not(.html-hover-contents) span.codicon {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.monaco-hover-content .action-container a {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.monaco-hover-content .action-container.disabled {
|
||||
pointer-events: none;
|
||||
opacity: 0.4;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
/* Prevent text selection in all button-like elements within hovers */
|
||||
.monaco-hover .action-container,
|
||||
.monaco-hover .action,
|
||||
.monaco-hover button,
|
||||
.monaco-hover .monaco-button,
|
||||
.monaco-hover .monaco-text-button,
|
||||
.monaco-hover [role="button"] {
|
||||
-webkit-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
import { append, $ as $$1, addDisposableListener, EventType } from '../../dom.js';
|
||||
import { StandardKeyboardEvent } from '../../keyboardEvent.js';
|
||||
import { DomScrollableElement } from '../scrollbar/scrollableElement.js';
|
||||
import { Disposable } from '../../../common/lifecycle.js';
|
||||
import './hoverWidget.css';
|
||||
import { localize } from '../../../../nls.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const $ = $$1;
|
||||
class HoverWidget extends Disposable {
|
||||
constructor(fadeIn) {
|
||||
super();
|
||||
this.containerDomNode = document.createElement('div');
|
||||
this.containerDomNode.className = 'monaco-hover';
|
||||
this.containerDomNode.classList.toggle('fade-in', !!fadeIn);
|
||||
this.containerDomNode.tabIndex = 0;
|
||||
this.containerDomNode.setAttribute('role', 'tooltip');
|
||||
this.contentsDomNode = document.createElement('div');
|
||||
this.contentsDomNode.className = 'monaco-hover-content';
|
||||
this.scrollbar = this._register(new DomScrollableElement(this.contentsDomNode, {
|
||||
consumeMouseWheelIfScrollbarIsNeeded: true
|
||||
}));
|
||||
this.containerDomNode.appendChild(this.scrollbar.getDomNode());
|
||||
}
|
||||
onContentsChanged() {
|
||||
this.scrollbar.scanDomNode();
|
||||
}
|
||||
}
|
||||
class HoverAction extends Disposable {
|
||||
static render(parent, actionOptions, keybindingLabel) {
|
||||
return new HoverAction(parent, actionOptions, keybindingLabel);
|
||||
}
|
||||
constructor(parent, actionOptions, keybindingLabel) {
|
||||
super();
|
||||
this.actionLabel = actionOptions.label;
|
||||
this.actionKeybindingLabel = keybindingLabel;
|
||||
this.actionContainer = append(parent, $('div.action-container'));
|
||||
this.actionContainer.setAttribute('tabindex', '0');
|
||||
this.action = append(this.actionContainer, $('a.action'));
|
||||
this.action.setAttribute('role', 'button');
|
||||
if (actionOptions.iconClass) {
|
||||
const iconElement = append(this.action, $(`span.icon`));
|
||||
iconElement.classList.add(...actionOptions.iconClass.split(' '));
|
||||
}
|
||||
this.actionRenderedLabel = keybindingLabel ? `${actionOptions.label} (${keybindingLabel})` : actionOptions.label;
|
||||
const label = append(this.action, $('span'));
|
||||
label.textContent = this.actionRenderedLabel;
|
||||
this._store.add(new ClickAction(this.actionContainer, actionOptions.run));
|
||||
this._store.add(new KeyDownAction(this.actionContainer, actionOptions.run, [3 /* KeyCode.Enter */, 10 /* KeyCode.Space */]));
|
||||
this.setEnabled(true);
|
||||
}
|
||||
setEnabled(enabled) {
|
||||
if (enabled) {
|
||||
this.actionContainer.classList.remove('disabled');
|
||||
this.actionContainer.removeAttribute('aria-disabled');
|
||||
}
|
||||
else {
|
||||
this.actionContainer.classList.add('disabled');
|
||||
this.actionContainer.setAttribute('aria-disabled', 'true');
|
||||
}
|
||||
}
|
||||
}
|
||||
function getHoverAccessibleViewHint(shouldHaveHint, keybinding) {
|
||||
return shouldHaveHint && keybinding ? localize(7, "Inspect this in the accessible view with {0}.", keybinding) : shouldHaveHint ? localize(8, "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding.") : '';
|
||||
}
|
||||
class ClickAction extends Disposable {
|
||||
constructor(container, run) {
|
||||
super();
|
||||
this._register(addDisposableListener(container, EventType.CLICK, e => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
run(container);
|
||||
}));
|
||||
}
|
||||
}
|
||||
class KeyDownAction extends Disposable {
|
||||
constructor(container, run, keyCodes) {
|
||||
super();
|
||||
this._register(addDisposableListener(container, EventType.KEY_DOWN, e => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
if (keyCodes.some(keyCode => event.equals(keyCode))) {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
run(container);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
export { ClickAction, HoverAction, HoverWidget, KeyDownAction, getHoverAccessibleViewHint };
|
||||
Generated
Vendored
+291
@@ -0,0 +1,291 @@
|
||||
import './iconlabel.css';
|
||||
import { append, $, isHTMLElement, isAncestor, after } from '../../dom.js';
|
||||
import { asCSSUrl } from '../../cssValue.js';
|
||||
import { HighlightedLabel } from '../highlightedlabel/highlightedLabel.js';
|
||||
import { Disposable } from '../../../common/lifecycle.js';
|
||||
import { equals } from '../../../common/objects.js';
|
||||
import { Range } from '../../../common/range.js';
|
||||
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
|
||||
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
|
||||
import { ThemeIcon } from '../../../common/themables.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class FastLabelNode {
|
||||
constructor(_element) {
|
||||
this._element = _element;
|
||||
}
|
||||
get element() {
|
||||
return this._element;
|
||||
}
|
||||
set textContent(content) {
|
||||
if (this.disposed || content === this._textContent) {
|
||||
return;
|
||||
}
|
||||
this._textContent = content;
|
||||
this._element.textContent = content;
|
||||
}
|
||||
set classNames(classNames) {
|
||||
if (this.disposed || equals(classNames, this._classNames)) {
|
||||
return;
|
||||
}
|
||||
this._classNames = classNames;
|
||||
this._element.classList.value = '';
|
||||
this._element.classList.add(...classNames);
|
||||
}
|
||||
set empty(empty) {
|
||||
if (this.disposed || empty === this._empty) {
|
||||
return;
|
||||
}
|
||||
this._empty = empty;
|
||||
this._element.style.marginLeft = empty ? '0' : '';
|
||||
}
|
||||
dispose() {
|
||||
this.disposed = true;
|
||||
}
|
||||
}
|
||||
class IconLabel extends Disposable {
|
||||
constructor(container, options) {
|
||||
super();
|
||||
this.customHovers = new Map();
|
||||
this.creationOptions = options;
|
||||
this.domNode = this._register(new FastLabelNode(append(container, $('.monaco-icon-label'))));
|
||||
this.labelContainer = append(this.domNode.element, $('.monaco-icon-label-container'));
|
||||
this.nameContainer = append(this.labelContainer, $('span.monaco-icon-name-container'));
|
||||
if (options?.supportHighlights || options?.supportIcons) {
|
||||
this.nameNode = this._register(new LabelWithHighlights(this.nameContainer, !!options.supportIcons));
|
||||
}
|
||||
else {
|
||||
this.nameNode = new Label(this.nameContainer);
|
||||
}
|
||||
this.hoverDelegate = options?.hoverDelegate ?? getDefaultHoverDelegate('mouse');
|
||||
}
|
||||
get element() {
|
||||
return this.domNode.element;
|
||||
}
|
||||
setLabel(label, description, options) {
|
||||
const labelClasses = ['monaco-icon-label'];
|
||||
const containerClasses = ['monaco-icon-label-container'];
|
||||
let ariaLabel = '';
|
||||
if (options) {
|
||||
if (options.extraClasses) {
|
||||
labelClasses.push(...options.extraClasses);
|
||||
}
|
||||
if (options.bold) {
|
||||
labelClasses.push('bold');
|
||||
}
|
||||
if (options.italic) {
|
||||
labelClasses.push('italic');
|
||||
}
|
||||
if (options.strikethrough) {
|
||||
labelClasses.push('strikethrough');
|
||||
}
|
||||
if (options.disabledCommand) {
|
||||
containerClasses.push('disabled');
|
||||
}
|
||||
if (options.title) {
|
||||
if (typeof options.title === 'string') {
|
||||
ariaLabel += options.title;
|
||||
}
|
||||
else {
|
||||
ariaLabel += label;
|
||||
}
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const existingIconNode = this.domNode.element.querySelector('.monaco-icon-label-iconpath');
|
||||
if (options?.iconPath) {
|
||||
let iconNode;
|
||||
if (!existingIconNode || !(isHTMLElement(existingIconNode))) {
|
||||
iconNode = $('.monaco-icon-label-iconpath');
|
||||
this.domNode.element.prepend(iconNode);
|
||||
}
|
||||
else {
|
||||
iconNode = existingIconNode;
|
||||
}
|
||||
if (ThemeIcon.isThemeIcon(options.iconPath)) {
|
||||
const iconClass = ThemeIcon.asClassName(options.iconPath);
|
||||
iconNode.className = `monaco-icon-label-iconpath ${iconClass}`;
|
||||
iconNode.style.backgroundImage = '';
|
||||
}
|
||||
else {
|
||||
iconNode.style.backgroundImage = asCSSUrl(options?.iconPath);
|
||||
}
|
||||
iconNode.style.backgroundRepeat = 'no-repeat';
|
||||
iconNode.style.backgroundPosition = 'center';
|
||||
iconNode.style.backgroundSize = 'contain';
|
||||
}
|
||||
else if (existingIconNode) {
|
||||
existingIconNode.remove();
|
||||
}
|
||||
this.domNode.classNames = labelClasses;
|
||||
this.domNode.element.setAttribute('aria-label', ariaLabel);
|
||||
this.labelContainer.classList.value = '';
|
||||
this.labelContainer.classList.add(...containerClasses);
|
||||
this.setupHover(options?.descriptionTitle ? this.labelContainer : this.element, options?.title);
|
||||
this.nameNode.setLabel(label, options);
|
||||
if (description || this.descriptionNode) {
|
||||
const descriptionNode = this.getOrCreateDescriptionNode();
|
||||
if (descriptionNode instanceof HighlightedLabel) {
|
||||
const supportIcons = options?.supportIcons ?? this.creationOptions?.supportIcons;
|
||||
descriptionNode.set(description || '', options ? options.descriptionMatches : undefined, undefined, options?.labelEscapeNewLines, supportIcons);
|
||||
this.setupHover(descriptionNode.element, options?.descriptionTitle);
|
||||
}
|
||||
else {
|
||||
descriptionNode.textContent = description && options?.labelEscapeNewLines ? HighlightedLabel.escapeNewLines(description, []) : (description || '');
|
||||
this.setupHover(descriptionNode.element, options?.descriptionTitle || '');
|
||||
descriptionNode.empty = !description;
|
||||
}
|
||||
}
|
||||
if (options?.suffix || this.suffixNode) {
|
||||
const suffixNode = this.getOrCreateSuffixNode();
|
||||
suffixNode.textContent = options?.suffix ?? '';
|
||||
}
|
||||
}
|
||||
setupHover(htmlElement, tooltip) {
|
||||
const previousCustomHover = this.customHovers.get(htmlElement);
|
||||
if (previousCustomHover) {
|
||||
previousCustomHover.dispose();
|
||||
this.customHovers.delete(htmlElement);
|
||||
}
|
||||
if (!tooltip) {
|
||||
htmlElement.removeAttribute('title');
|
||||
return;
|
||||
}
|
||||
let hoverTarget = htmlElement;
|
||||
if (this.creationOptions?.hoverTargetOverride) {
|
||||
if (!isAncestor(htmlElement, this.creationOptions.hoverTargetOverride)) {
|
||||
throw new Error('hoverTargetOverrride must be an ancestor of the htmlElement');
|
||||
}
|
||||
hoverTarget = this.creationOptions.hoverTargetOverride;
|
||||
}
|
||||
const hoverDisposable = getBaseLayerHoverDelegate().setupManagedHover(this.hoverDelegate, hoverTarget, tooltip);
|
||||
if (hoverDisposable) {
|
||||
this.customHovers.set(htmlElement, hoverDisposable);
|
||||
}
|
||||
}
|
||||
dispose() {
|
||||
super.dispose();
|
||||
for (const disposable of this.customHovers.values()) {
|
||||
disposable.dispose();
|
||||
}
|
||||
this.customHovers.clear();
|
||||
}
|
||||
getOrCreateSuffixNode() {
|
||||
if (!this.suffixNode) {
|
||||
const suffixContainer = this._register(new FastLabelNode(after(this.nameContainer, $('span.monaco-icon-suffix-container'))));
|
||||
this.suffixNode = this._register(new FastLabelNode(append(suffixContainer.element, $('span.label-suffix'))));
|
||||
}
|
||||
return this.suffixNode;
|
||||
}
|
||||
getOrCreateDescriptionNode() {
|
||||
if (!this.descriptionNode) {
|
||||
const descriptionContainer = this._register(new FastLabelNode(append(this.labelContainer, $('span.monaco-icon-description-container'))));
|
||||
if (this.creationOptions?.supportDescriptionHighlights) {
|
||||
this.descriptionNode = this._register(new HighlightedLabel(append(descriptionContainer.element, $('span.label-description'))));
|
||||
}
|
||||
else {
|
||||
this.descriptionNode = this._register(new FastLabelNode(append(descriptionContainer.element, $('span.label-description'))));
|
||||
}
|
||||
}
|
||||
return this.descriptionNode;
|
||||
}
|
||||
}
|
||||
class Label {
|
||||
constructor(container) {
|
||||
this.container = container;
|
||||
this.label = undefined;
|
||||
this.singleLabel = undefined;
|
||||
}
|
||||
setLabel(label, options) {
|
||||
if (this.label === label && equals(this.options, options)) {
|
||||
return;
|
||||
}
|
||||
this.label = label;
|
||||
this.options = options;
|
||||
if (typeof label === 'string') {
|
||||
if (!this.singleLabel) {
|
||||
this.container.textContent = '';
|
||||
this.container.classList.remove('multiple');
|
||||
this.singleLabel = append(this.container, $('a.label-name', { id: options?.domId }));
|
||||
}
|
||||
this.singleLabel.textContent = label;
|
||||
}
|
||||
else {
|
||||
this.container.textContent = '';
|
||||
this.container.classList.add('multiple');
|
||||
this.singleLabel = undefined;
|
||||
for (let i = 0; i < label.length; i++) {
|
||||
const l = label[i];
|
||||
const id = options?.domId && `${options?.domId}_${i}`;
|
||||
append(this.container, $('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' }, l));
|
||||
if (i < label.length - 1) {
|
||||
append(this.container, $('span.label-separator', undefined, options?.separator || '/'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
function splitMatches(labels, separator, matches) {
|
||||
if (!matches) {
|
||||
return undefined;
|
||||
}
|
||||
let labelStart = 0;
|
||||
return labels.map(label => {
|
||||
const labelRange = { start: labelStart, end: labelStart + label.length };
|
||||
const result = matches
|
||||
.map(match => Range.intersect(labelRange, match))
|
||||
.filter(range => !Range.isEmpty(range))
|
||||
.map(({ start, end }) => ({ start: start - labelStart, end: end - labelStart }));
|
||||
labelStart = labelRange.end + separator.length;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
class LabelWithHighlights extends Disposable {
|
||||
constructor(container, supportIcons) {
|
||||
super();
|
||||
this.container = container;
|
||||
this.supportIcons = supportIcons;
|
||||
this.label = undefined;
|
||||
this.singleLabel = undefined;
|
||||
}
|
||||
setLabel(label, options) {
|
||||
if (this.label === label && equals(this.options, options)) {
|
||||
return;
|
||||
}
|
||||
this.label = label;
|
||||
this.options = options;
|
||||
// Determine supportIcons: use option if provided, otherwise use constructor value
|
||||
const supportIcons = options?.supportIcons ?? this.supportIcons;
|
||||
if (typeof label === 'string') {
|
||||
if (!this.singleLabel) {
|
||||
this.container.textContent = '';
|
||||
this.container.classList.remove('multiple');
|
||||
this.singleLabel = this._register(new HighlightedLabel(append(this.container, $('a.label-name', { id: options?.domId }))));
|
||||
}
|
||||
this.singleLabel.set(label, options?.matches, undefined, options?.labelEscapeNewLines, supportIcons);
|
||||
}
|
||||
else {
|
||||
this.container.textContent = '';
|
||||
this.container.classList.add('multiple');
|
||||
this.singleLabel = undefined;
|
||||
const separator = options?.separator || '/';
|
||||
const matches = splitMatches(label, separator, options?.matches);
|
||||
for (let i = 0; i < label.length; i++) {
|
||||
const l = label[i];
|
||||
const m = matches ? matches[i] : undefined;
|
||||
const id = options?.domId && `${options?.domId}_${i}`;
|
||||
const name = $('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' });
|
||||
const highlightedLabel = this._register(new HighlightedLabel(append(this.container, name)));
|
||||
highlightedLabel.set(l, m, undefined, options?.labelEscapeNewLines, supportIcons);
|
||||
if (i < label.length - 1) {
|
||||
append(name, $('span.label-separator', undefined, separator));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export { IconLabel };
|
||||
Generated
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
import { $ } from '../../dom.js';
|
||||
import { ThemeIcon } from '../../../common/themables.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const labelWithIconsRegex = new RegExp(`(\\\\)?\\$\\((${ThemeIcon.iconNameExpression}(?:${ThemeIcon.iconModifierExpression})?)\\)`, 'g');
|
||||
function renderLabelWithIcons(text) {
|
||||
const elements = new Array();
|
||||
let match;
|
||||
let textStart = 0, textStop = 0;
|
||||
while ((match = labelWithIconsRegex.exec(text)) !== null) {
|
||||
textStop = match.index || 0;
|
||||
if (textStart < textStop) {
|
||||
elements.push(text.substring(textStart, textStop));
|
||||
}
|
||||
textStart = (match.index || 0) + match[0].length;
|
||||
const [, escaped, codicon] = match;
|
||||
elements.push(escaped ? `$(${codicon})` : renderIcon({ id: codicon }));
|
||||
}
|
||||
if (textStart < text.length) {
|
||||
elements.push(text.substring(textStart));
|
||||
}
|
||||
return elements;
|
||||
}
|
||||
function renderIcon(icon) {
|
||||
const node = $(`span`);
|
||||
node.classList.add(...ThemeIcon.asClassNameArray(icon));
|
||||
return node;
|
||||
}
|
||||
|
||||
export { renderIcon, renderLabelWithIcons };
|
||||
Generated
Vendored
+119
@@ -0,0 +1,119 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* ---------- Icon label ---------- */
|
||||
|
||||
.monaco-icon-label {
|
||||
display: flex; /* required for icons support :before rule */
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.monaco-icon-label::before {
|
||||
|
||||
/* svg icons rendered as background image */
|
||||
background-size: 16px;
|
||||
background-position: left center;
|
||||
background-repeat: no-repeat;
|
||||
padding-right: 6px;
|
||||
width: 16px;
|
||||
height: 22px;
|
||||
line-height: inherit !important;
|
||||
display: inline-block;
|
||||
|
||||
/* fonts icons */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
vertical-align: top;
|
||||
|
||||
flex-shrink: 0; /* fix for https://github.com/microsoft/vscode/issues/13787 */
|
||||
}
|
||||
|
||||
.monaco-icon-label-iconpath {
|
||||
width: 16px;
|
||||
height: 22px;
|
||||
margin-right: 6px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.monaco-icon-label-container.disabled {
|
||||
color: var(--vscode-disabledForeground);
|
||||
}
|
||||
.monaco-icon-label > .monaco-icon-label-container {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container > .label-name {
|
||||
color: inherit;
|
||||
white-space: pre; /* enable to show labels that include multiple whitespaces */
|
||||
}
|
||||
|
||||
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-name-container > .label-name > .label-separator {
|
||||
margin: 0 2px;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-suffix-container > .label-suffix {
|
||||
opacity: .7;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.monaco-icon-label > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
|
||||
opacity: .7;
|
||||
margin-left: 0.5em;
|
||||
font-size: 0.9em;
|
||||
white-space: pre; /* enable to show labels that include multiple whitespaces */
|
||||
}
|
||||
|
||||
.monaco-icon-label.nowrap > .monaco-icon-label-container > .monaco-icon-description-container > .label-description{
|
||||
white-space: nowrap
|
||||
}
|
||||
|
||||
.vs .monaco-icon-label > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
|
||||
opacity: .95;
|
||||
}
|
||||
|
||||
.monaco-icon-label.bold > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
|
||||
.monaco-icon-label.bold > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.monaco-icon-label.italic > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
|
||||
.monaco-icon-label.italic > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.monaco-icon-label.deprecated {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.66;
|
||||
}
|
||||
|
||||
.monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-name-container > .label-name,
|
||||
.monaco-icon-label.strikethrough > .monaco-icon-label-container > .monaco-icon-description-container > .label-description {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
|
||||
.monaco-icon-label::after {
|
||||
opacity: 0.75;
|
||||
font-size: 90%;
|
||||
font-weight: 600;
|
||||
margin: auto 16px 0 5px; /* https://github.com/microsoft/vscode/issues/113223 */
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* 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
|
||||
{
|
||||
color: inherit !important;
|
||||
}
|
||||
|
||||
.monaco-list-row.focused.selected .label-description,
|
||||
.monaco-list-row.selected .label-description {
|
||||
opacity: .8;
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-inputbox {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
border-radius: 2px;
|
||||
|
||||
/* Customizable */
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.monaco-inputbox > .ibwrapper > .input,
|
||||
.monaco-inputbox > .ibwrapper > .mirror {
|
||||
|
||||
/* Customizable */
|
||||
padding: 4px 6px;
|
||||
}
|
||||
|
||||
.monaco-inputbox > .ibwrapper {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-inputbox > .ibwrapper > .input {
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
line-height: inherit;
|
||||
border: none;
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
resize: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.monaco-inputbox > .ibwrapper > input {
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.monaco-inputbox > .ibwrapper > textarea.input {
|
||||
display: block;
|
||||
scrollbar-width: none; /* Firefox: hide scrollbars */
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.monaco-inputbox > .ibwrapper > textarea.input::-webkit-scrollbar {
|
||||
display: none; /* Chrome + Safari: hide scrollbar */
|
||||
}
|
||||
|
||||
.monaco-inputbox > .ibwrapper > textarea.input.empty {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monaco-inputbox > .ibwrapper > .mirror {
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
box-sizing: border-box;
|
||||
white-space: pre-wrap;
|
||||
visibility: hidden;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Context view */
|
||||
|
||||
.monaco-inputbox-container {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.monaco-inputbox-container .monaco-inputbox-message {
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 0.4em;
|
||||
font-size: 12px;
|
||||
line-height: 17px;
|
||||
margin-top: -1px;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Action bar support */
|
||||
.monaco-inputbox .monaco-action-bar {
|
||||
position: absolute;
|
||||
right: 2px;
|
||||
top: 4px;
|
||||
}
|
||||
|
||||
.monaco-inputbox .monaco-action-bar .action-item {
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
.monaco-inputbox .monaco-action-bar .action-item .codicon {
|
||||
background-repeat: no-repeat;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
+510
@@ -0,0 +1,510 @@
|
||||
import { append, $ as $$1, getTotalHeight, isActiveElement, getTotalWidth } from '../../dom.js';
|
||||
import { asCssValueWithDefault } from '../../cssValue.js';
|
||||
import { DomEmitter } from '../../event.js';
|
||||
import { renderFormattedText, renderText } from '../../formattedTextRenderer.js';
|
||||
import { ActionBar } from '../actionbar/actionbar.js';
|
||||
import { status, alert } from '../aria/aria.js';
|
||||
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
|
||||
import { ScrollableElement } from '../scrollbar/scrollableElement.js';
|
||||
import { Widget } from '../widget.js';
|
||||
import { Emitter, Event } from '../../../common/event.js';
|
||||
import { HistoryNavigator } from '../../../common/history.js';
|
||||
import { equals } from '../../../common/objects.js';
|
||||
import './inputBox.css';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { MutableDisposable } from '../../../common/lifecycle.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const $ = $$1;
|
||||
class InputBox extends Widget {
|
||||
get onDidChange() { return this._onDidChange.event; }
|
||||
get onDidHeightChange() { return this._onDidHeightChange.event; }
|
||||
constructor(container, contextViewProvider, options) {
|
||||
super();
|
||||
this.state = 'idle';
|
||||
this.maxHeight = Number.POSITIVE_INFINITY;
|
||||
this.hover = this._register(new MutableDisposable());
|
||||
this._onDidChange = this._register(new Emitter());
|
||||
this._onDidHeightChange = this._register(new Emitter());
|
||||
this.contextViewProvider = contextViewProvider;
|
||||
this.options = options;
|
||||
this.message = null;
|
||||
this.placeholder = this.options.placeholder || '';
|
||||
this.tooltip = this.options.tooltip ?? (this.placeholder || '');
|
||||
this.ariaLabel = this.options.ariaLabel || '';
|
||||
if (this.options.validationOptions) {
|
||||
this.validation = this.options.validationOptions.validation;
|
||||
}
|
||||
this.element = append(container, $('.monaco-inputbox.idle'));
|
||||
const tagName = this.options.flexibleHeight ? 'textarea' : 'input';
|
||||
const wrapper = append(this.element, $('.ibwrapper'));
|
||||
this.input = append(wrapper, $(tagName + '.input.empty'));
|
||||
this.input.setAttribute('autocorrect', 'off');
|
||||
this.input.setAttribute('autocapitalize', 'off');
|
||||
this.input.setAttribute('spellcheck', 'false');
|
||||
this.onfocus(this.input, () => this.element.classList.add('synthetic-focus'));
|
||||
this.onblur(this.input, () => this.element.classList.remove('synthetic-focus'));
|
||||
if (this.options.flexibleHeight) {
|
||||
this.maxHeight = typeof this.options.flexibleMaxHeight === 'number' ? this.options.flexibleMaxHeight : Number.POSITIVE_INFINITY;
|
||||
this.mirror = append(wrapper, $('div.mirror'));
|
||||
this.mirror.innerText = '\u00a0';
|
||||
this.scrollableElement = new ScrollableElement(this.element, { vertical: 1 /* ScrollbarVisibility.Auto */ });
|
||||
if (this.options.flexibleWidth) {
|
||||
this.input.setAttribute('wrap', 'off');
|
||||
this.mirror.style.whiteSpace = 'pre';
|
||||
this.mirror.style.wordWrap = 'initial';
|
||||
}
|
||||
append(container, this.scrollableElement.getDomNode());
|
||||
this._register(this.scrollableElement);
|
||||
// from ScrollableElement to DOM
|
||||
this._register(this.scrollableElement.onScroll(e => this.input.scrollTop = e.scrollTop));
|
||||
const onSelectionChange = this._register(new DomEmitter(container.ownerDocument, 'selectionchange'));
|
||||
const onAnchoredSelectionChange = Event.filter(onSelectionChange.event, () => {
|
||||
const selection = container.ownerDocument.getSelection();
|
||||
return selection?.anchorNode === wrapper;
|
||||
});
|
||||
// from DOM to ScrollableElement
|
||||
this._register(onAnchoredSelectionChange(this.updateScrollDimensions, this));
|
||||
this._register(this.onDidHeightChange(this.updateScrollDimensions, this));
|
||||
}
|
||||
else {
|
||||
this.input.type = this.options.type || 'text';
|
||||
this.input.setAttribute('wrap', 'off');
|
||||
}
|
||||
if (this.ariaLabel) {
|
||||
this.input.setAttribute('aria-label', this.ariaLabel);
|
||||
}
|
||||
if (this.placeholder && !this.options.showPlaceholderOnFocus) {
|
||||
this.setPlaceHolder(this.placeholder);
|
||||
}
|
||||
if (this.tooltip) {
|
||||
this.setTooltip(this.tooltip);
|
||||
}
|
||||
this.oninput(this.input, () => this.onValueChange());
|
||||
this.onblur(this.input, () => this.onBlur());
|
||||
this.onfocus(this.input, () => this.onFocus());
|
||||
this._register(this.ignoreGesture(this.input));
|
||||
setTimeout(() => this.updateMirror(), 0);
|
||||
// Support actions
|
||||
if (this.options.actions) {
|
||||
this.actionbar = this._register(new ActionBar(this.element));
|
||||
this.actionbar.push(this.options.actions, { icon: true, label: false });
|
||||
}
|
||||
this.applyStyles();
|
||||
}
|
||||
onBlur() {
|
||||
this._hideMessage();
|
||||
if (this.options.showPlaceholderOnFocus) {
|
||||
this.input.setAttribute('placeholder', '');
|
||||
}
|
||||
}
|
||||
onFocus() {
|
||||
this._showMessage();
|
||||
if (this.options.showPlaceholderOnFocus) {
|
||||
this.input.setAttribute('placeholder', this.placeholder || '');
|
||||
}
|
||||
}
|
||||
setPlaceHolder(placeHolder) {
|
||||
this.placeholder = placeHolder;
|
||||
this.input.setAttribute('placeholder', placeHolder);
|
||||
}
|
||||
setTooltip(tooltip) {
|
||||
this.tooltip = tooltip;
|
||||
if (!this.hover.value) {
|
||||
this.hover.value = this._register(getBaseLayerHoverDelegate().setupDelayedHoverAtMouse(this.input, () => ({
|
||||
content: this.tooltip,
|
||||
appearance: {
|
||||
compact: true,
|
||||
}
|
||||
})));
|
||||
}
|
||||
}
|
||||
get inputElement() {
|
||||
return this.input;
|
||||
}
|
||||
get value() {
|
||||
return this.input.value;
|
||||
}
|
||||
set value(newValue) {
|
||||
if (this.input.value !== newValue) {
|
||||
this.input.value = newValue;
|
||||
this.onValueChange();
|
||||
}
|
||||
}
|
||||
get height() {
|
||||
return typeof this.cachedHeight === 'number' ? this.cachedHeight : getTotalHeight(this.element);
|
||||
}
|
||||
focus() {
|
||||
this.input.focus();
|
||||
}
|
||||
blur() {
|
||||
this.input.blur();
|
||||
}
|
||||
hasFocus() {
|
||||
return isActiveElement(this.input);
|
||||
}
|
||||
select(range = null) {
|
||||
this.input.select();
|
||||
if (range) {
|
||||
this.input.setSelectionRange(range.start, range.end);
|
||||
if (range.end === this.input.value.length) {
|
||||
this.input.scrollLeft = this.input.scrollWidth;
|
||||
}
|
||||
}
|
||||
}
|
||||
isSelectionAtEnd() {
|
||||
return this.input.selectionEnd === this.input.value.length && this.input.selectionStart === this.input.selectionEnd;
|
||||
}
|
||||
getSelection() {
|
||||
const selectionStart = this.input.selectionStart;
|
||||
if (selectionStart === null) {
|
||||
return null;
|
||||
}
|
||||
const selectionEnd = this.input.selectionEnd ?? selectionStart;
|
||||
return {
|
||||
start: selectionStart,
|
||||
end: selectionEnd,
|
||||
};
|
||||
}
|
||||
enable() {
|
||||
this.input.removeAttribute('disabled');
|
||||
}
|
||||
disable() {
|
||||
this.blur();
|
||||
this.input.disabled = true;
|
||||
this._hideMessage();
|
||||
}
|
||||
set paddingRight(paddingRight) {
|
||||
// Set width to avoid hint text overlapping buttons
|
||||
this.input.style.width = `calc(100% - ${paddingRight}px)`;
|
||||
if (this.mirror) {
|
||||
this.mirror.style.paddingRight = paddingRight + 'px';
|
||||
}
|
||||
}
|
||||
updateScrollDimensions() {
|
||||
if (typeof this.cachedContentHeight !== 'number' || typeof this.cachedHeight !== 'number' || !this.scrollableElement) {
|
||||
return;
|
||||
}
|
||||
const scrollHeight = this.cachedContentHeight;
|
||||
const height = this.cachedHeight;
|
||||
const scrollTop = this.input.scrollTop;
|
||||
this.scrollableElement.setScrollDimensions({ scrollHeight, height });
|
||||
this.scrollableElement.setScrollPosition({ scrollTop });
|
||||
}
|
||||
showMessage(message, force) {
|
||||
if (this.state === 'open' && equals(this.message, message)) {
|
||||
// Already showing
|
||||
return;
|
||||
}
|
||||
this.message = message;
|
||||
this.element.classList.remove('idle');
|
||||
this.element.classList.remove('info');
|
||||
this.element.classList.remove('warning');
|
||||
this.element.classList.remove('error');
|
||||
this.element.classList.add(this.classForType(message.type));
|
||||
const styles = this.stylesForType(this.message.type);
|
||||
this.element.style.border = `1px solid ${asCssValueWithDefault(styles.border, 'transparent')}`;
|
||||
if (this.message.content && (this.hasFocus() || force)) {
|
||||
this._showMessage();
|
||||
}
|
||||
}
|
||||
hideMessage() {
|
||||
this.message = null;
|
||||
this.element.classList.remove('info');
|
||||
this.element.classList.remove('warning');
|
||||
this.element.classList.remove('error');
|
||||
this.element.classList.add('idle');
|
||||
this._hideMessage();
|
||||
this.applyStyles();
|
||||
}
|
||||
validate() {
|
||||
let errorMsg = null;
|
||||
if (this.validation) {
|
||||
errorMsg = this.validation(this.value);
|
||||
if (errorMsg) {
|
||||
this.inputElement.setAttribute('aria-invalid', 'true');
|
||||
this.showMessage(errorMsg);
|
||||
}
|
||||
else if (this.inputElement.hasAttribute('aria-invalid')) {
|
||||
this.inputElement.removeAttribute('aria-invalid');
|
||||
this.hideMessage();
|
||||
}
|
||||
}
|
||||
return errorMsg?.type;
|
||||
}
|
||||
stylesForType(type) {
|
||||
const styles = this.options.inputBoxStyles;
|
||||
switch (type) {
|
||||
case 1 /* MessageType.INFO */: return { border: styles.inputValidationInfoBorder, background: styles.inputValidationInfoBackground, foreground: styles.inputValidationInfoForeground };
|
||||
case 2 /* MessageType.WARNING */: return { border: styles.inputValidationWarningBorder, background: styles.inputValidationWarningBackground, foreground: styles.inputValidationWarningForeground };
|
||||
default: return { border: styles.inputValidationErrorBorder, background: styles.inputValidationErrorBackground, foreground: styles.inputValidationErrorForeground };
|
||||
}
|
||||
}
|
||||
classForType(type) {
|
||||
switch (type) {
|
||||
case 1 /* MessageType.INFO */: return 'info';
|
||||
case 2 /* MessageType.WARNING */: return 'warning';
|
||||
default: return 'error';
|
||||
}
|
||||
}
|
||||
_showMessage() {
|
||||
if (!this.contextViewProvider || !this.message) {
|
||||
return;
|
||||
}
|
||||
let div;
|
||||
const layout = () => div.style.width = getTotalWidth(this.element) + 'px';
|
||||
this.contextViewProvider.showContextView({
|
||||
getAnchor: () => this.element,
|
||||
anchorAlignment: 1 /* AnchorAlignment.RIGHT */,
|
||||
render: (container) => {
|
||||
if (!this.message) {
|
||||
return null;
|
||||
}
|
||||
div = append(container, $('.monaco-inputbox-container'));
|
||||
layout();
|
||||
const spanElement = $('span.monaco-inputbox-message');
|
||||
if (this.message.formatContent) {
|
||||
renderFormattedText(this.message.content, undefined, spanElement);
|
||||
}
|
||||
else {
|
||||
renderText(this.message.content, undefined, spanElement);
|
||||
}
|
||||
spanElement.classList.add(this.classForType(this.message.type));
|
||||
const styles = this.stylesForType(this.message.type);
|
||||
spanElement.style.backgroundColor = styles.background ?? '';
|
||||
spanElement.style.color = styles.foreground ?? '';
|
||||
spanElement.style.border = styles.border ? `1px solid ${styles.border}` : '';
|
||||
append(div, spanElement);
|
||||
return null;
|
||||
},
|
||||
onHide: () => {
|
||||
this.state = 'closed';
|
||||
},
|
||||
layout: layout
|
||||
});
|
||||
// ARIA Support
|
||||
let alertText;
|
||||
if (this.message.type === 3 /* MessageType.ERROR */) {
|
||||
alertText = localize(9, "Error: {0}", this.message.content);
|
||||
}
|
||||
else if (this.message.type === 2 /* MessageType.WARNING */) {
|
||||
alertText = localize(10, "Warning: {0}", this.message.content);
|
||||
}
|
||||
else {
|
||||
alertText = localize(11, "Info: {0}", this.message.content);
|
||||
}
|
||||
alert(alertText);
|
||||
this.state = 'open';
|
||||
}
|
||||
_hideMessage() {
|
||||
if (!this.contextViewProvider) {
|
||||
return;
|
||||
}
|
||||
if (this.state === 'open') {
|
||||
this.contextViewProvider.hideContextView();
|
||||
}
|
||||
this.state = 'idle';
|
||||
}
|
||||
onValueChange() {
|
||||
this._onDidChange.fire(this.value);
|
||||
this.validate();
|
||||
this.updateMirror();
|
||||
this.input.classList.toggle('empty', !this.value);
|
||||
if (this.state === 'open' && this.contextViewProvider) {
|
||||
this.contextViewProvider.layout();
|
||||
}
|
||||
}
|
||||
updateMirror() {
|
||||
if (!this.mirror) {
|
||||
return;
|
||||
}
|
||||
const value = this.value;
|
||||
const lastCharCode = value.charCodeAt(value.length - 1);
|
||||
const suffix = lastCharCode === 10 ? ' ' : '';
|
||||
const mirrorTextContent = (value + suffix)
|
||||
.replace(/\u000c/g, ''); // Don't measure with the form feed character, which messes up sizing
|
||||
if (mirrorTextContent) {
|
||||
this.mirror.textContent = value + suffix;
|
||||
}
|
||||
else {
|
||||
this.mirror.innerText = '\u00a0';
|
||||
}
|
||||
this.layout();
|
||||
}
|
||||
applyStyles() {
|
||||
const styles = this.options.inputBoxStyles;
|
||||
const background = styles.inputBackground ?? '';
|
||||
const foreground = styles.inputForeground ?? '';
|
||||
const border = styles.inputBorder ?? '';
|
||||
this.element.style.backgroundColor = background;
|
||||
this.element.style.color = foreground;
|
||||
this.input.style.backgroundColor = 'inherit';
|
||||
this.input.style.color = foreground;
|
||||
// there's always a border, even if the color is not set.
|
||||
this.element.style.border = `1px solid ${asCssValueWithDefault(border, 'transparent')}`;
|
||||
}
|
||||
layout() {
|
||||
if (!this.mirror) {
|
||||
return;
|
||||
}
|
||||
const previousHeight = this.cachedContentHeight;
|
||||
this.cachedContentHeight = getTotalHeight(this.mirror);
|
||||
if (previousHeight !== this.cachedContentHeight) {
|
||||
this.cachedHeight = Math.min(this.cachedContentHeight, this.maxHeight);
|
||||
this.input.style.height = this.cachedHeight + 'px';
|
||||
this._onDidHeightChange.fire(this.cachedContentHeight);
|
||||
}
|
||||
}
|
||||
insertAtCursor(text) {
|
||||
const inputElement = this.inputElement;
|
||||
const start = inputElement.selectionStart;
|
||||
const end = inputElement.selectionEnd;
|
||||
const content = inputElement.value;
|
||||
if (start !== null && end !== null) {
|
||||
this.value = content.substr(0, start) + text + content.substr(end);
|
||||
inputElement.setSelectionRange(start + 1, start + 1);
|
||||
this.layout();
|
||||
}
|
||||
}
|
||||
dispose() {
|
||||
this._hideMessage();
|
||||
this.message = null;
|
||||
this.actionbar?.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
class HistoryInputBox extends InputBox {
|
||||
constructor(container, contextViewProvider, options) {
|
||||
const NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_NO_PARENS = localize(12, ' or {0} for history', `\u21C5`);
|
||||
|
||||
|
||||
|
||||
const NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS = localize(13, ' ({0} for history)', `\u21C5`);
|
||||
|
||||
|
||||
|
||||
super(container, contextViewProvider, options);
|
||||
this._onDidFocus = this._register(new Emitter());
|
||||
this.onDidFocus = this._onDidFocus.event;
|
||||
this._onDidBlur = this._register(new Emitter());
|
||||
this.onDidBlur = this._onDidBlur.event;
|
||||
this.history = this._register(new HistoryNavigator(options.history, 100));
|
||||
// Function to append the history suffix to the placeholder if necessary
|
||||
const addSuffix = () => {
|
||||
if (options.showHistoryHint && options.showHistoryHint() && !this.placeholder.endsWith(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_NO_PARENS) && !this.placeholder.endsWith(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS) && this.history.getHistory().length) {
|
||||
const suffix = this.placeholder.endsWith(')') ? NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_NO_PARENS : NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS;
|
||||
const suffixedPlaceholder = this.placeholder + suffix;
|
||||
if (options.showPlaceholderOnFocus && !isActiveElement(this.input)) {
|
||||
this.placeholder = suffixedPlaceholder;
|
||||
}
|
||||
else {
|
||||
this.setPlaceHolder(suffixedPlaceholder);
|
||||
}
|
||||
}
|
||||
};
|
||||
// Spot the change to the textarea class attribute which occurs when it changes between non-empty and empty,
|
||||
// and add the history suffix to the placeholder if not yet present
|
||||
this.observer = new MutationObserver((mutationList, observer) => {
|
||||
mutationList.forEach((mutation) => {
|
||||
if (!mutation.target.textContent) {
|
||||
addSuffix();
|
||||
}
|
||||
});
|
||||
});
|
||||
this.observer.observe(this.input, { attributeFilter: ['class'] });
|
||||
this.onfocus(this.input, () => addSuffix());
|
||||
this.onblur(this.input, () => {
|
||||
const resetPlaceholder = (historyHint) => {
|
||||
if (!this.placeholder.endsWith(historyHint)) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
const revertedPlaceholder = this.placeholder.slice(0, this.placeholder.length - historyHint.length);
|
||||
if (options.showPlaceholderOnFocus) {
|
||||
this.placeholder = revertedPlaceholder;
|
||||
}
|
||||
else {
|
||||
this.setPlaceHolder(revertedPlaceholder);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
if (!resetPlaceholder(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_IN_PARENS)) {
|
||||
resetPlaceholder(NLS_PLACEHOLDER_HISTORY_HINT_SUFFIX_NO_PARENS);
|
||||
}
|
||||
});
|
||||
}
|
||||
dispose() {
|
||||
super.dispose();
|
||||
if (this.observer) {
|
||||
this.observer.disconnect();
|
||||
this.observer = undefined;
|
||||
}
|
||||
}
|
||||
addToHistory(always) {
|
||||
if (this.value && (always || this.value !== this.getCurrentValue())) {
|
||||
this.history.add(this.value);
|
||||
}
|
||||
}
|
||||
isAtLastInHistory() {
|
||||
return this.history.isLast();
|
||||
}
|
||||
isNowhereInHistory() {
|
||||
return this.history.isNowhere();
|
||||
}
|
||||
showNextValue() {
|
||||
if (!this.history.has(this.value)) {
|
||||
this.addToHistory();
|
||||
}
|
||||
let next = this.getNextValue();
|
||||
if (next) {
|
||||
next = next === this.value ? this.getNextValue() : next;
|
||||
}
|
||||
this.value = next ?? '';
|
||||
status(this.value ? this.value : localize(14, "Cleared Input"));
|
||||
}
|
||||
showPreviousValue() {
|
||||
if (!this.history.has(this.value)) {
|
||||
this.addToHistory();
|
||||
}
|
||||
let previous = this.getPreviousValue();
|
||||
if (previous) {
|
||||
previous = previous === this.value ? this.getPreviousValue() : previous;
|
||||
}
|
||||
if (previous) {
|
||||
this.value = previous;
|
||||
status(this.value);
|
||||
}
|
||||
}
|
||||
setPlaceHolder(placeHolder) {
|
||||
super.setPlaceHolder(placeHolder);
|
||||
this.setTooltip(placeHolder);
|
||||
}
|
||||
onBlur() {
|
||||
super.onBlur();
|
||||
this._onDidBlur.fire();
|
||||
}
|
||||
onFocus() {
|
||||
super.onFocus();
|
||||
this._onDidFocus.fire();
|
||||
}
|
||||
getCurrentValue() {
|
||||
let currentValue = this.history.current();
|
||||
if (!currentValue) {
|
||||
currentValue = this.history.last();
|
||||
this.history.next();
|
||||
}
|
||||
return currentValue;
|
||||
}
|
||||
getPreviousValue() {
|
||||
return this.history.previous() || this.history.first();
|
||||
}
|
||||
getNextValue() {
|
||||
return this.history.next();
|
||||
}
|
||||
}
|
||||
|
||||
export { HistoryInputBox, InputBox };
|
||||
Generated
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-keybinding {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 10px;
|
||||
}
|
||||
|
||||
.monaco-keybinding > .monaco-keybinding-key {
|
||||
display: inline-block;
|
||||
border-style: solid;
|
||||
border-width: 1px;
|
||||
border-radius: 3px;
|
||||
vertical-align: middle;
|
||||
font-size: 11px;
|
||||
padding: 3px 5px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.monaco-keybinding > .monaco-keybinding-key:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.monaco-keybinding > .monaco-keybinding-key:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
.monaco-keybinding > .monaco-keybinding-key-separator {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.monaco-keybinding > .monaco-keybinding-key-chord-separator {
|
||||
width: 6px;
|
||||
}
|
||||
Generated
Vendored
+122
@@ -0,0 +1,122 @@
|
||||
import { append, $ as $$1, clearNode } from '../../dom.js';
|
||||
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
|
||||
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
|
||||
import { UILabelProvider } from '../../../common/keybindingLabels.js';
|
||||
import { Disposable } from '../../../common/lifecycle.js';
|
||||
import { equals } from '../../../common/objects.js';
|
||||
import './keybindingLabel.css';
|
||||
import { localize } from '../../../../nls.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const $ = $$1;
|
||||
const unthemedKeybindingLabelOptions = {
|
||||
keybindingLabelBackground: undefined,
|
||||
keybindingLabelForeground: undefined,
|
||||
keybindingLabelBorder: undefined,
|
||||
keybindingLabelBottomBorder: undefined,
|
||||
keybindingLabelShadow: undefined
|
||||
};
|
||||
class KeybindingLabel extends Disposable {
|
||||
constructor(container, os, options) {
|
||||
super();
|
||||
this.os = os;
|
||||
this.keyElements = new Set();
|
||||
this.options = options || Object.create(null);
|
||||
const labelForeground = this.options.keybindingLabelForeground;
|
||||
this.domNode = append(container, $('.monaco-keybinding'));
|
||||
if (labelForeground) {
|
||||
this.domNode.style.color = labelForeground;
|
||||
}
|
||||
this.hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.domNode, ''));
|
||||
this.didEverRender = false;
|
||||
container.appendChild(this.domNode);
|
||||
}
|
||||
set(keybinding, matches) {
|
||||
if (this.didEverRender && this.keybinding === keybinding && KeybindingLabel.areSame(this.matches, matches)) {
|
||||
return;
|
||||
}
|
||||
this.keybinding = keybinding;
|
||||
this.matches = matches;
|
||||
this.render();
|
||||
}
|
||||
render() {
|
||||
this.clear();
|
||||
if (this.keybinding) {
|
||||
const chords = this.keybinding.getChords();
|
||||
if (chords[0]) {
|
||||
this.renderChord(this.domNode, chords[0], this.matches ? this.matches.firstPart : null);
|
||||
}
|
||||
for (let i = 1; i < chords.length; i++) {
|
||||
append(this.domNode, $('span.monaco-keybinding-key-chord-separator', undefined, ' '));
|
||||
this.renderChord(this.domNode, chords[i], this.matches ? this.matches.chordPart : null);
|
||||
}
|
||||
const title = (this.options.disableTitle ?? false) ? undefined : this.keybinding.getAriaLabel() || undefined;
|
||||
this.hover.update(title);
|
||||
this.domNode.setAttribute('aria-label', title || '');
|
||||
}
|
||||
else if (this.options && this.options.renderUnboundKeybindings) {
|
||||
this.renderUnbound(this.domNode);
|
||||
}
|
||||
this.didEverRender = true;
|
||||
}
|
||||
clear() {
|
||||
clearNode(this.domNode);
|
||||
this.keyElements.clear();
|
||||
}
|
||||
renderChord(parent, chord, match) {
|
||||
const modifierLabels = UILabelProvider.modifierLabels[this.os];
|
||||
if (chord.ctrlKey) {
|
||||
this.renderKey(parent, modifierLabels.ctrlKey, Boolean(match?.ctrlKey), modifierLabels.separator);
|
||||
}
|
||||
if (chord.shiftKey) {
|
||||
this.renderKey(parent, modifierLabels.shiftKey, Boolean(match?.shiftKey), modifierLabels.separator);
|
||||
}
|
||||
if (chord.altKey) {
|
||||
this.renderKey(parent, modifierLabels.altKey, Boolean(match?.altKey), modifierLabels.separator);
|
||||
}
|
||||
if (chord.metaKey) {
|
||||
this.renderKey(parent, modifierLabels.metaKey, Boolean(match?.metaKey), modifierLabels.separator);
|
||||
}
|
||||
const keyLabel = chord.keyLabel;
|
||||
if (keyLabel) {
|
||||
this.renderKey(parent, keyLabel, Boolean(match?.keyCode), '');
|
||||
}
|
||||
}
|
||||
renderKey(parent, label, highlight, separator) {
|
||||
append(parent, this.createKeyElement(label, highlight ? '.highlight' : ''));
|
||||
if (separator) {
|
||||
append(parent, $('span.monaco-keybinding-key-separator', undefined, separator));
|
||||
}
|
||||
}
|
||||
renderUnbound(parent) {
|
||||
append(parent, this.createKeyElement(localize(15, "Unbound")));
|
||||
}
|
||||
createKeyElement(label, extraClass = '') {
|
||||
const keyElement = $('span.monaco-keybinding-key' + extraClass, undefined, label);
|
||||
this.keyElements.add(keyElement);
|
||||
if (this.options.keybindingLabelBackground) {
|
||||
keyElement.style.backgroundColor = this.options.keybindingLabelBackground;
|
||||
}
|
||||
if (this.options.keybindingLabelBorder) {
|
||||
keyElement.style.borderColor = this.options.keybindingLabelBorder;
|
||||
}
|
||||
if (this.options.keybindingLabelBottomBorder) {
|
||||
keyElement.style.borderBottomColor = this.options.keybindingLabelBottomBorder;
|
||||
}
|
||||
if (this.options.keybindingLabelShadow) {
|
||||
keyElement.style.boxShadow = `inset 0 -1px 0 ${this.options.keybindingLabelShadow}`;
|
||||
}
|
||||
return keyElement;
|
||||
}
|
||||
static areSame(a, b) {
|
||||
if (a === b || (!a && !b)) {
|
||||
return true;
|
||||
}
|
||||
return !!a && !!b && equals(a.firstPart, b.firstPart) && equals(a.chordPart, b.chordPart);
|
||||
}
|
||||
}
|
||||
|
||||
export { KeybindingLabel, unthemedKeybindingLabelOptions };
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-list {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.monaco-list.mouse-support {
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.monaco-list > .monaco-scrollable-element {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-list-rows {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-list.horizontal-scrolling .monaco-list-rows {
|
||||
width: auto;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.monaco-list-row {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-list.mouse-support .monaco-list-row {
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
/* Make sure the scrollbar renders above overlays (sticky scroll) */
|
||||
.monaco-list .monaco-scrollable-element > .scrollbar.vertical,
|
||||
.monaco-pane-view > .monaco-split-view2.vertical > .monaco-scrollable-element > .scrollbar.vertical {
|
||||
z-index: 14;
|
||||
}
|
||||
|
||||
/* for OS X ballistic scrolling */
|
||||
.monaco-list-row.scrolling {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Focus */
|
||||
.monaco-list.element-focused,
|
||||
.monaco-list.selection-single,
|
||||
.monaco-list.selection-multiple {
|
||||
outline: 0 !important;
|
||||
}
|
||||
|
||||
/* Filter */
|
||||
|
||||
.monaco-list-type-filter-message {
|
||||
position: absolute;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
top: 0;
|
||||
left: 0;
|
||||
padding: 40px 1em 1em 1em;
|
||||
text-align: center;
|
||||
white-space: normal;
|
||||
opacity: 0.7;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.monaco-list-type-filter-message:empty {
|
||||
display: none;
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class ListError extends Error {
|
||||
constructor(user, message) {
|
||||
super(`ListError [${user}] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
export { ListError };
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
import { range } from '../../../common/arrays.js';
|
||||
import { CancellationTokenSource } from '../../../common/cancellation.js';
|
||||
import { Event } from '../../../common/event.js';
|
||||
import { DisposableStore, Disposable } from '../../../common/lifecycle.js';
|
||||
import './list.css';
|
||||
import { List } from './listWidget.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class PagedRenderer {
|
||||
get templateId() { return this.renderer.templateId; }
|
||||
constructor(renderer, modelProvider) {
|
||||
this.renderer = renderer;
|
||||
this.modelProvider = modelProvider;
|
||||
}
|
||||
renderTemplate(container) {
|
||||
const data = this.renderer.renderTemplate(container);
|
||||
return { data, disposable: Disposable.None };
|
||||
}
|
||||
renderElement(index, _, data, details) {
|
||||
data.disposable?.dispose();
|
||||
if (!data.data) {
|
||||
return;
|
||||
}
|
||||
const model = this.modelProvider();
|
||||
if (model.isResolved(index)) {
|
||||
return this.renderer.renderElement(model.get(index), index, data.data, details);
|
||||
}
|
||||
const cts = new CancellationTokenSource();
|
||||
const promise = model.resolve(index, cts.token);
|
||||
data.disposable = { dispose: () => cts.cancel() };
|
||||
this.renderer.renderPlaceholder(index, data.data);
|
||||
promise.then(entry => this.renderer.renderElement(entry, index, data.data, details));
|
||||
}
|
||||
disposeTemplate(data) {
|
||||
if (data.disposable) {
|
||||
data.disposable.dispose();
|
||||
data.disposable = undefined;
|
||||
}
|
||||
if (data.data) {
|
||||
this.renderer.disposeTemplate(data.data);
|
||||
data.data = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
class PagedAccessibilityProvider {
|
||||
constructor(modelProvider, accessibilityProvider) {
|
||||
this.modelProvider = modelProvider;
|
||||
this.accessibilityProvider = accessibilityProvider;
|
||||
}
|
||||
getWidgetAriaLabel() {
|
||||
return this.accessibilityProvider.getWidgetAriaLabel();
|
||||
}
|
||||
getAriaLabel(index) {
|
||||
const model = this.modelProvider();
|
||||
if (!model.isResolved(index)) {
|
||||
return null;
|
||||
}
|
||||
return this.accessibilityProvider.getAriaLabel(model.get(index));
|
||||
}
|
||||
}
|
||||
function fromPagedListOptions(modelProvider, options) {
|
||||
return {
|
||||
...options,
|
||||
accessibilityProvider: options.accessibilityProvider && new PagedAccessibilityProvider(modelProvider, options.accessibilityProvider)
|
||||
};
|
||||
}
|
||||
class PagedList {
|
||||
constructor(user, container, virtualDelegate, renderers, options = {}) {
|
||||
this.modelDisposables = new DisposableStore();
|
||||
const modelProvider = () => this.model;
|
||||
const pagedRenderers = renderers.map(r => new PagedRenderer(r, modelProvider));
|
||||
this.list = new List(user, container, virtualDelegate, pagedRenderers, fromPagedListOptions(modelProvider, options));
|
||||
}
|
||||
updateOptions(options) {
|
||||
this.list.updateOptions(options);
|
||||
}
|
||||
getHTMLElement() {
|
||||
return this.list.getHTMLElement();
|
||||
}
|
||||
get onDidFocus() {
|
||||
return this.list.onDidFocus;
|
||||
}
|
||||
get widget() {
|
||||
return this.list;
|
||||
}
|
||||
get onDidDispose() {
|
||||
return this.list.onDidDispose;
|
||||
}
|
||||
get onMouseDblClick() {
|
||||
return Event.map(this.list.onMouseDblClick, ({ element, index, browserEvent }) => ({ element: element === undefined ? undefined : this._model.get(element), index, browserEvent }));
|
||||
}
|
||||
get onPointer() {
|
||||
return Event.map(this.list.onPointer, ({ element, index, browserEvent }) => ({ element: element === undefined ? undefined : this._model.get(element), index, browserEvent }));
|
||||
}
|
||||
get onDidChangeSelection() {
|
||||
return Event.map(this.list.onDidChangeSelection, ({ elements, indexes, browserEvent }) => ({ elements: elements.map(e => this._model.get(e)), indexes, browserEvent }));
|
||||
}
|
||||
get model() {
|
||||
return this._model;
|
||||
}
|
||||
set model(model) {
|
||||
this.modelDisposables.clear();
|
||||
this._model = model;
|
||||
this.list.splice(0, this.list.length, range(model.length));
|
||||
this.modelDisposables.add(model.onDidIncrementLength(newLength => this.list.splice(this.list.length, 0, range(this.list.length, newLength))));
|
||||
}
|
||||
getFocus() {
|
||||
return this.list.getFocus();
|
||||
}
|
||||
getSelection() {
|
||||
return this.list.getSelection();
|
||||
}
|
||||
getSelectedElements() {
|
||||
return this.getSelection().map(i => this.model.get(i));
|
||||
}
|
||||
style(styles) {
|
||||
this.list.style(styles);
|
||||
}
|
||||
dispose() {
|
||||
this.list.dispose();
|
||||
this.modelDisposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export { PagedList };
|
||||
+1173
File diff suppressed because it is too large
Load Diff
+1535
File diff suppressed because it is too large
Load Diff
+161
@@ -0,0 +1,161 @@
|
||||
import { Range } from '../../../common/range.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
/**
|
||||
* Returns the intersection between a ranged group and a range.
|
||||
* Returns `[]` if the intersection is empty.
|
||||
*/
|
||||
function groupIntersect(range, groups) {
|
||||
const result = [];
|
||||
for (const r of groups) {
|
||||
if (range.start >= r.range.end) {
|
||||
continue;
|
||||
}
|
||||
if (range.end < r.range.start) {
|
||||
break;
|
||||
}
|
||||
const intersection = Range.intersect(range, r.range);
|
||||
if (Range.isEmpty(intersection)) {
|
||||
continue;
|
||||
}
|
||||
result.push({
|
||||
range: intersection,
|
||||
size: r.size
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Shifts a range by that `much`.
|
||||
*/
|
||||
function shift({ start, end }, much) {
|
||||
return { start: start + much, end: end + much };
|
||||
}
|
||||
/**
|
||||
* Consolidates a collection of ranged groups.
|
||||
*
|
||||
* Consolidation is the process of merging consecutive ranged groups
|
||||
* that share the same `size`.
|
||||
*/
|
||||
function consolidate(groups) {
|
||||
const result = [];
|
||||
let previousGroup = null;
|
||||
for (const group of groups) {
|
||||
const start = group.range.start;
|
||||
const end = group.range.end;
|
||||
const size = group.size;
|
||||
if (previousGroup && size === previousGroup.size) {
|
||||
previousGroup.range.end = end;
|
||||
continue;
|
||||
}
|
||||
previousGroup = { range: { start, end }, size };
|
||||
result.push(previousGroup);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
/**
|
||||
* Concatenates several collections of ranged groups into a single
|
||||
* collection.
|
||||
*/
|
||||
function concat(...groups) {
|
||||
return consolidate(groups.reduce((r, g) => r.concat(g), []));
|
||||
}
|
||||
class RangeMap {
|
||||
get paddingTop() {
|
||||
return this._paddingTop;
|
||||
}
|
||||
set paddingTop(paddingTop) {
|
||||
this._size = this._size + paddingTop - this._paddingTop;
|
||||
this._paddingTop = paddingTop;
|
||||
}
|
||||
constructor(topPadding) {
|
||||
this.groups = [];
|
||||
this._size = 0;
|
||||
this._paddingTop = 0;
|
||||
this._paddingTop = topPadding ?? 0;
|
||||
this._size = this._paddingTop;
|
||||
}
|
||||
splice(index, deleteCount, items = []) {
|
||||
const diff = items.length - deleteCount;
|
||||
const before = groupIntersect({ start: 0, end: index }, this.groups);
|
||||
const after = groupIntersect({ start: index + deleteCount, end: Number.POSITIVE_INFINITY }, this.groups)
|
||||
.map(g => ({ range: shift(g.range, diff), size: g.size }));
|
||||
const middle = items.map((item, i) => ({
|
||||
range: { start: index + i, end: index + i + 1 },
|
||||
size: item.size
|
||||
}));
|
||||
this.groups = concat(before, middle, after);
|
||||
this._size = this._paddingTop + this.groups.reduce((t, g) => t + (g.size * (g.range.end - g.range.start)), 0);
|
||||
}
|
||||
/**
|
||||
* Returns the number of items in the range map.
|
||||
*/
|
||||
get count() {
|
||||
const len = this.groups.length;
|
||||
if (!len) {
|
||||
return 0;
|
||||
}
|
||||
return this.groups[len - 1].range.end;
|
||||
}
|
||||
/**
|
||||
* Returns the sum of the sizes of all items in the range map.
|
||||
*/
|
||||
get size() {
|
||||
return this._size;
|
||||
}
|
||||
/**
|
||||
* Returns the index of the item at the given position.
|
||||
*/
|
||||
indexAt(position) {
|
||||
if (position < 0) {
|
||||
return -1;
|
||||
}
|
||||
if (position < this._paddingTop) {
|
||||
return 0;
|
||||
}
|
||||
let index = 0;
|
||||
let size = this._paddingTop;
|
||||
for (const group of this.groups) {
|
||||
const count = group.range.end - group.range.start;
|
||||
const newSize = size + (count * group.size);
|
||||
if (position < newSize) {
|
||||
return index + Math.floor((position - size) / group.size);
|
||||
}
|
||||
index += count;
|
||||
size = newSize;
|
||||
}
|
||||
return index;
|
||||
}
|
||||
/**
|
||||
* Returns the index of the item right after the item at the
|
||||
* index of the given position.
|
||||
*/
|
||||
indexAfter(position) {
|
||||
return Math.min(this.indexAt(position) + 1, this.count);
|
||||
}
|
||||
/**
|
||||
* Returns the start position of the item at the given index.
|
||||
*/
|
||||
positionAt(index) {
|
||||
if (index < 0) {
|
||||
return -1;
|
||||
}
|
||||
let position = 0;
|
||||
let count = 0;
|
||||
for (const group of this.groups) {
|
||||
const groupCount = group.range.end - group.range.start;
|
||||
const newCount = count + groupCount;
|
||||
if (index < newCount) {
|
||||
return this._paddingTop + position + ((index - count) * group.size);
|
||||
}
|
||||
position += groupCount * group.size;
|
||||
count = newCount;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
export { RangeMap, consolidate, groupIntersect, shift };
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
import { $ } from '../../dom.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class RowCache {
|
||||
constructor(renderers) {
|
||||
this.renderers = renderers;
|
||||
this.cache = new Map();
|
||||
this.transactionNodesPendingRemoval = new Set();
|
||||
this.inTransaction = false;
|
||||
}
|
||||
/**
|
||||
* Returns a row either by creating a new one or reusing
|
||||
* a previously released row which shares the same templateId.
|
||||
*
|
||||
* @returns A row and `isReusingConnectedDomNode` if the row's node is already in the dom in a stale position.
|
||||
*/
|
||||
alloc(templateId) {
|
||||
let result = this.getTemplateCache(templateId).pop();
|
||||
let isStale = false;
|
||||
if (result) {
|
||||
isStale = this.transactionNodesPendingRemoval.has(result.domNode);
|
||||
if (isStale) {
|
||||
this.transactionNodesPendingRemoval.delete(result.domNode);
|
||||
}
|
||||
}
|
||||
else {
|
||||
const domNode = $('.monaco-list-row');
|
||||
const renderer = this.getRenderer(templateId);
|
||||
const templateData = renderer.renderTemplate(domNode);
|
||||
result = { domNode, templateId, templateData };
|
||||
}
|
||||
return { row: result, isReusingConnectedDomNode: isStale };
|
||||
}
|
||||
/**
|
||||
* Releases the row for eventual reuse.
|
||||
*/
|
||||
release(row) {
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
this.releaseRow(row);
|
||||
}
|
||||
/**
|
||||
* Begin a set of changes that use the cache. This lets us skip work when a row is removed and then inserted again.
|
||||
*/
|
||||
transact(makeChanges) {
|
||||
if (this.inTransaction) {
|
||||
throw new Error('Already in transaction');
|
||||
}
|
||||
this.inTransaction = true;
|
||||
try {
|
||||
makeChanges();
|
||||
}
|
||||
finally {
|
||||
for (const domNode of this.transactionNodesPendingRemoval) {
|
||||
this.doRemoveNode(domNode);
|
||||
}
|
||||
this.transactionNodesPendingRemoval.clear();
|
||||
this.inTransaction = false;
|
||||
}
|
||||
}
|
||||
releaseRow(row) {
|
||||
const { domNode, templateId } = row;
|
||||
if (domNode) {
|
||||
if (this.inTransaction) {
|
||||
this.transactionNodesPendingRemoval.add(domNode);
|
||||
}
|
||||
else {
|
||||
this.doRemoveNode(domNode);
|
||||
}
|
||||
}
|
||||
const cache = this.getTemplateCache(templateId);
|
||||
cache.push(row);
|
||||
}
|
||||
doRemoveNode(domNode) {
|
||||
domNode.classList.remove('scrolling');
|
||||
domNode.remove();
|
||||
}
|
||||
getTemplateCache(templateId) {
|
||||
let result = this.cache.get(templateId);
|
||||
if (!result) {
|
||||
result = [];
|
||||
this.cache.set(templateId, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
dispose() {
|
||||
this.cache.forEach((cachedRows, templateId) => {
|
||||
for (const cachedRow of cachedRows) {
|
||||
const renderer = this.getRenderer(templateId);
|
||||
renderer.disposeTemplate(cachedRow.templateData);
|
||||
cachedRow.templateData = null;
|
||||
}
|
||||
});
|
||||
this.cache.clear();
|
||||
this.transactionNodesPendingRemoval.clear();
|
||||
}
|
||||
getRenderer(templateId) {
|
||||
const renderer = this.renderers.get(templateId);
|
||||
if (!renderer) {
|
||||
throw new Error(`No renderer found for ${templateId}`);
|
||||
}
|
||||
return renderer;
|
||||
}
|
||||
}
|
||||
|
||||
export { RowCache };
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class CombinedSpliceable {
|
||||
constructor(spliceables) {
|
||||
this.spliceables = spliceables;
|
||||
}
|
||||
splice(start, deleteCount, elements) {
|
||||
this.spliceables.forEach(s => s.splice(start, deleteCount, elements));
|
||||
}
|
||||
}
|
||||
|
||||
export { CombinedSpliceable };
|
||||
+1147
File diff suppressed because it is too large
Load Diff
Generated
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-mouse-cursor-text {
|
||||
cursor: text;
|
||||
}
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
import './mouseCursor.css';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const MOUSE_CURSOR_TEXT_CSS_CLASS_NAME = `monaco-mouse-cursor-text`;
|
||||
|
||||
export { MOUSE_CURSOR_TEXT_CSS_CLASS_NAME };
|
||||
Generated
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-progress-container {
|
||||
width: 100%;
|
||||
height: 2px;
|
||||
overflow: hidden; /* keep progress bit in bounds */
|
||||
}
|
||||
|
||||
.monaco-progress-container .progress-bit {
|
||||
width: 2%;
|
||||
height: 2px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-progress-container.active .progress-bit {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
.monaco-progress-container.discrete .progress-bit {
|
||||
left: 0;
|
||||
transition: width 100ms linear;
|
||||
}
|
||||
|
||||
.monaco-progress-container.discrete.done .progress-bit {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-progress-container.infinite .progress-bit {
|
||||
animation-name: progress;
|
||||
animation-duration: 4s;
|
||||
animation-iteration-count: infinite;
|
||||
transform: translate3d(0px, 0px, 0px);
|
||||
animation-timing-function: linear;
|
||||
}
|
||||
|
||||
.monaco-progress-container.infinite.infinite-long-running .progress-bit {
|
||||
/*
|
||||
The more smooth `linear` timing function can cause
|
||||
higher GPU consumption as indicated in
|
||||
https://github.com/microsoft/vscode/issues/97900 &
|
||||
https://github.com/microsoft/vscode/issues/138396
|
||||
*/
|
||||
animation-timing-function: steps(100);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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:
|
||||
* 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(4900%) scaleX(1) } }
|
||||
Generated
Vendored
+105
@@ -0,0 +1,105 @@
|
||||
import { show } from '../../dom.js';
|
||||
import { RunOnceScheduler } from '../../../common/async.js';
|
||||
import { Disposable, MutableDisposable } from '../../../common/lifecycle.js';
|
||||
import './progressbar.css';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const CSS_DONE = 'done';
|
||||
const CSS_ACTIVE = 'active';
|
||||
const CSS_INFINITE = 'infinite';
|
||||
const CSS_INFINITE_LONG_RUNNING = 'infinite-long-running';
|
||||
const CSS_DISCRETE = 'discrete';
|
||||
/**
|
||||
* A progress bar with support for infinite or discrete progress.
|
||||
*/
|
||||
class ProgressBar extends Disposable {
|
||||
/**
|
||||
* After a certain time of showing the progress bar, switch
|
||||
* to long-running mode and throttle animations to reduce
|
||||
* the pressure on the GPU process.
|
||||
*
|
||||
* https://github.com/microsoft/vscode/issues/97900
|
||||
* https://github.com/microsoft/vscode/issues/138396
|
||||
*/
|
||||
static { this.LONG_RUNNING_INFINITE_THRESHOLD = 10000; }
|
||||
constructor(container, options) {
|
||||
super();
|
||||
this.progressSignal = this._register(new MutableDisposable());
|
||||
this.workedVal = 0;
|
||||
this.showDelayedScheduler = this._register(new RunOnceScheduler(() => show(this.element), 0));
|
||||
this.longRunningScheduler = this._register(new RunOnceScheduler(() => this.infiniteLongRunning(), ProgressBar.LONG_RUNNING_INFINITE_THRESHOLD));
|
||||
this.create(container, options);
|
||||
}
|
||||
create(container, options) {
|
||||
this.element = document.createElement('div');
|
||||
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');
|
||||
this.bit.classList.add('progress-bit');
|
||||
this.bit.style.backgroundColor = options?.progressBarBackground || '#0E70C0';
|
||||
this.element.appendChild(this.bit);
|
||||
}
|
||||
off() {
|
||||
this.bit.style.width = 'inherit';
|
||||
this.bit.style.opacity = '1';
|
||||
this.element.classList.remove(CSS_ACTIVE, CSS_INFINITE, CSS_INFINITE_LONG_RUNNING, CSS_DISCRETE);
|
||||
this.workedVal = 0;
|
||||
this.totalWork = undefined;
|
||||
this.longRunningScheduler.cancel();
|
||||
this.progressSignal.clear();
|
||||
}
|
||||
/**
|
||||
* Stops the progressbar from showing any progress instantly without fading out.
|
||||
*/
|
||||
stop() {
|
||||
return this.doDone(false);
|
||||
}
|
||||
doDone(delayed) {
|
||||
this.element.classList.add(CSS_DONE);
|
||||
// discrete: let it grow to 100% width and hide afterwards
|
||||
if (!this.element.classList.contains(CSS_INFINITE)) {
|
||||
this.bit.style.width = 'inherit';
|
||||
if (delayed) {
|
||||
setTimeout(() => this.off(), 200);
|
||||
}
|
||||
else {
|
||||
this.off();
|
||||
}
|
||||
}
|
||||
// infinite: let it fade out and hide afterwards
|
||||
else {
|
||||
this.bit.style.opacity = '0';
|
||||
if (delayed) {
|
||||
setTimeout(() => this.off(), 200);
|
||||
}
|
||||
else {
|
||||
this.off();
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
/**
|
||||
* Use this mode to indicate progress that has no total number of work units.
|
||||
*/
|
||||
infinite() {
|
||||
this.bit.style.width = '2%';
|
||||
this.bit.style.opacity = '1';
|
||||
this.element.classList.remove(CSS_DISCRETE, CSS_DONE, CSS_INFINITE_LONG_RUNNING);
|
||||
this.element.classList.add(CSS_ACTIVE, CSS_INFINITE);
|
||||
this.longRunningScheduler.schedule();
|
||||
return this;
|
||||
}
|
||||
infiniteLongRunning() {
|
||||
this.element.classList.add(CSS_INFINITE_LONG_RUNNING);
|
||||
}
|
||||
getContainer() {
|
||||
return this.element;
|
||||
}
|
||||
}
|
||||
|
||||
export { ProgressBar };
|
||||
Generated
Vendored
+150
@@ -0,0 +1,150 @@
|
||||
import { Dimension } from '../../dom.js';
|
||||
import { Sash, OrthogonalEdge } from '../sash/sash.js';
|
||||
import { Emitter, Event } from '../../../common/event.js';
|
||||
import { DisposableStore } from '../../../common/lifecycle.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class ResizableHTMLElement {
|
||||
get onDidWillResize() { return this._onDidWillResize.event; }
|
||||
get onDidResize() { return this._onDidResize.event; }
|
||||
constructor() {
|
||||
this._onDidWillResize = new Emitter();
|
||||
this._onDidResize = new Emitter();
|
||||
this._sashListener = new DisposableStore();
|
||||
this._size = new Dimension(0, 0);
|
||||
this._minSize = new Dimension(0, 0);
|
||||
this._maxSize = new Dimension(Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER);
|
||||
this.domNode = document.createElement('div');
|
||||
this._eastSash = new Sash(this.domNode, { getVerticalSashLeft: () => this._size.width }, { orientation: 0 /* Orientation.VERTICAL */ });
|
||||
this._westSash = new Sash(this.domNode, { getVerticalSashLeft: () => 0 }, { orientation: 0 /* Orientation.VERTICAL */ });
|
||||
this._northSash = new Sash(this.domNode, { getHorizontalSashTop: () => 0 }, { orientation: 1 /* Orientation.HORIZONTAL */, orthogonalEdge: OrthogonalEdge.North });
|
||||
this._southSash = new Sash(this.domNode, { getHorizontalSashTop: () => this._size.height }, { orientation: 1 /* Orientation.HORIZONTAL */, orthogonalEdge: OrthogonalEdge.South });
|
||||
this._northSash.orthogonalStartSash = this._westSash;
|
||||
this._northSash.orthogonalEndSash = this._eastSash;
|
||||
this._southSash.orthogonalStartSash = this._westSash;
|
||||
this._southSash.orthogonalEndSash = this._eastSash;
|
||||
let currentSize;
|
||||
let deltaY = 0;
|
||||
let deltaX = 0;
|
||||
this._sashListener.add(Event.any(this._northSash.onDidStart, this._eastSash.onDidStart, this._southSash.onDidStart, this._westSash.onDidStart)(() => {
|
||||
if (currentSize === undefined) {
|
||||
this._onDidWillResize.fire();
|
||||
currentSize = this._size;
|
||||
deltaY = 0;
|
||||
deltaX = 0;
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(Event.any(this._northSash.onDidEnd, this._eastSash.onDidEnd, this._southSash.onDidEnd, this._westSash.onDidEnd)(() => {
|
||||
if (currentSize !== undefined) {
|
||||
currentSize = undefined;
|
||||
deltaY = 0;
|
||||
deltaX = 0;
|
||||
this._onDidResize.fire({ dimension: this._size, done: true });
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(this._eastSash.onDidChange(e => {
|
||||
if (currentSize) {
|
||||
deltaX = e.currentX - e.startX;
|
||||
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
|
||||
this._onDidResize.fire({ dimension: this._size, done: false, east: true });
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(this._westSash.onDidChange(e => {
|
||||
if (currentSize) {
|
||||
deltaX = -(e.currentX - e.startX);
|
||||
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
|
||||
this._onDidResize.fire({ dimension: this._size, done: false, west: true });
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(this._northSash.onDidChange(e => {
|
||||
if (currentSize) {
|
||||
deltaY = -(e.currentY - e.startY);
|
||||
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
|
||||
this._onDidResize.fire({ dimension: this._size, done: false, north: true });
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(this._southSash.onDidChange(e => {
|
||||
if (currentSize) {
|
||||
deltaY = e.currentY - e.startY;
|
||||
this.layout(currentSize.height + deltaY, currentSize.width + deltaX);
|
||||
this._onDidResize.fire({ dimension: this._size, done: false, south: true });
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(Event.any(this._eastSash.onDidReset, this._westSash.onDidReset)(e => {
|
||||
if (this._preferredSize) {
|
||||
this.layout(this._size.height, this._preferredSize.width);
|
||||
this._onDidResize.fire({ dimension: this._size, done: true });
|
||||
}
|
||||
}));
|
||||
this._sashListener.add(Event.any(this._northSash.onDidReset, this._southSash.onDidReset)(e => {
|
||||
if (this._preferredSize) {
|
||||
this.layout(this._preferredSize.height, this._size.width);
|
||||
this._onDidResize.fire({ dimension: this._size, done: true });
|
||||
}
|
||||
}));
|
||||
}
|
||||
dispose() {
|
||||
this._northSash.dispose();
|
||||
this._southSash.dispose();
|
||||
this._eastSash.dispose();
|
||||
this._westSash.dispose();
|
||||
this._sashListener.dispose();
|
||||
this._onDidResize.dispose();
|
||||
this._onDidWillResize.dispose();
|
||||
this.domNode.remove();
|
||||
}
|
||||
enableSashes(north, east, south, west) {
|
||||
this._northSash.state = north ? 3 /* SashState.Enabled */ : 0 /* SashState.Disabled */;
|
||||
this._eastSash.state = east ? 3 /* SashState.Enabled */ : 0 /* SashState.Disabled */;
|
||||
this._southSash.state = south ? 3 /* SashState.Enabled */ : 0 /* SashState.Disabled */;
|
||||
this._westSash.state = west ? 3 /* SashState.Enabled */ : 0 /* SashState.Disabled */;
|
||||
}
|
||||
layout(height = this.size.height, width = this.size.width) {
|
||||
const { height: minHeight, width: minWidth } = this._minSize;
|
||||
const { height: maxHeight, width: maxWidth } = this._maxSize;
|
||||
height = Math.max(minHeight, Math.min(maxHeight, height));
|
||||
width = Math.max(minWidth, Math.min(maxWidth, width));
|
||||
const newSize = new Dimension(width, height);
|
||||
if (!Dimension.equals(newSize, this._size)) {
|
||||
this.domNode.style.height = height + 'px';
|
||||
this.domNode.style.width = width + 'px';
|
||||
this._size = newSize;
|
||||
this._northSash.layout();
|
||||
this._eastSash.layout();
|
||||
this._southSash.layout();
|
||||
this._westSash.layout();
|
||||
}
|
||||
}
|
||||
clearSashHoverState() {
|
||||
this._eastSash.clearSashHoverState();
|
||||
this._westSash.clearSashHoverState();
|
||||
this._northSash.clearSashHoverState();
|
||||
this._southSash.clearSashHoverState();
|
||||
}
|
||||
get size() {
|
||||
return this._size;
|
||||
}
|
||||
set maxSize(value) {
|
||||
this._maxSize = value;
|
||||
}
|
||||
get maxSize() {
|
||||
return this._maxSize;
|
||||
}
|
||||
set minSize(value) {
|
||||
this._minSize = value;
|
||||
}
|
||||
get minSize() {
|
||||
return this._minSize;
|
||||
}
|
||||
set preferredSize(value) {
|
||||
this._preferredSize = value;
|
||||
}
|
||||
get preferredSize() {
|
||||
return this._preferredSize;
|
||||
}
|
||||
}
|
||||
|
||||
export { ResizableHTMLElement };
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
:root {
|
||||
--vscode-sash-size: 4px;
|
||||
--vscode-sash-hover-size: 4px;
|
||||
}
|
||||
|
||||
.monaco-sash {
|
||||
position: absolute;
|
||||
z-index: 35;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.monaco-sash.disabled {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.monaco-sash.mac.vertical {
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.vertical.minimum {
|
||||
cursor: e-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.vertical.maximum {
|
||||
cursor: w-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.mac.horizontal {
|
||||
cursor: row-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal.minimum {
|
||||
cursor: s-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal.maximum {
|
||||
cursor: n-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.disabled {
|
||||
cursor: default !important;
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
.monaco-sash.vertical {
|
||||
cursor: ew-resize;
|
||||
top: 0;
|
||||
width: var(--vscode-sash-size);
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal {
|
||||
cursor: ns-resize;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: var(--vscode-sash-size);
|
||||
}
|
||||
|
||||
.monaco-sash:not(.disabled) > .orthogonal-drag-handle {
|
||||
content: " ";
|
||||
height: calc(var(--vscode-sash-size) * 2);
|
||||
width: calc(var(--vscode-sash-size) * 2);
|
||||
z-index: 100;
|
||||
display: block;
|
||||
cursor: all-scroll;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal.orthogonal-edge-north:not(.disabled)
|
||||
> .orthogonal-drag-handle.start,
|
||||
.monaco-sash.horizontal.orthogonal-edge-south:not(.disabled)
|
||||
> .orthogonal-drag-handle.end {
|
||||
cursor: nwse-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal.orthogonal-edge-north:not(.disabled)
|
||||
> .orthogonal-drag-handle.end,
|
||||
.monaco-sash.horizontal.orthogonal-edge-south:not(.disabled)
|
||||
> .orthogonal-drag-handle.start {
|
||||
cursor: nesw-resize;
|
||||
}
|
||||
|
||||
.monaco-sash.vertical > .orthogonal-drag-handle.start {
|
||||
left: calc(var(--vscode-sash-size) * -0.5);
|
||||
top: calc(var(--vscode-sash-size) * -1);
|
||||
}
|
||||
.monaco-sash.vertical > .orthogonal-drag-handle.end {
|
||||
left: calc(var(--vscode-sash-size) * -0.5);
|
||||
bottom: calc(var(--vscode-sash-size) * -1);
|
||||
}
|
||||
.monaco-sash.horizontal > .orthogonal-drag-handle.start {
|
||||
top: calc(var(--vscode-sash-size) * -0.5);
|
||||
left: calc(var(--vscode-sash-size) * -1);
|
||||
}
|
||||
.monaco-sash.horizontal > .orthogonal-drag-handle.end {
|
||||
top: calc(var(--vscode-sash-size) * -0.5);
|
||||
right: calc(var(--vscode-sash-size) * -1);
|
||||
}
|
||||
|
||||
.monaco-sash:before {
|
||||
content: '';
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
.monaco-enable-motion .monaco-sash:before {
|
||||
transition: background-color 0.1s ease-out;
|
||||
}
|
||||
|
||||
.monaco-sash.hover:before,
|
||||
.monaco-sash.active:before {
|
||||
background: var(--vscode-sash-hoverBorder);
|
||||
}
|
||||
|
||||
.monaco-sash.vertical:before {
|
||||
width: var(--vscode-sash-hover-size);
|
||||
left: calc(50% - (var(--vscode-sash-hover-size) / 2));
|
||||
}
|
||||
|
||||
.monaco-sash.horizontal:before {
|
||||
height: var(--vscode-sash-hover-size);
|
||||
top: calc(50% - (var(--vscode-sash-hover-size) / 2));
|
||||
}
|
||||
|
||||
.pointer-events-disabled {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
|
||||
/** Debug **/
|
||||
|
||||
.monaco-sash.debug {
|
||||
background: cyan;
|
||||
}
|
||||
|
||||
.monaco-sash.debug.disabled {
|
||||
background: rgba(0, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.monaco-sash.debug:not(.disabled) > .orthogonal-drag-handle {
|
||||
background: red;
|
||||
}
|
||||
+451
@@ -0,0 +1,451 @@
|
||||
import { getWindow, append, $, addDisposableListener, EventHelper, isHTMLElement } from '../../dom.js';
|
||||
import { createStyleSheet } from '../../domStylesheets.js';
|
||||
import { DomEmitter } from '../../event.js';
|
||||
import { EventType, Gesture } from '../../touch.js';
|
||||
import { Delayer } from '../../../common/async.js';
|
||||
import { memoize } from '../../../common/decorators.js';
|
||||
import { Emitter } from '../../../common/event.js';
|
||||
import { DisposableStore, Disposable, toDisposable } from '../../../common/lifecycle.js';
|
||||
import { isMacintosh } from '../../../common/platform.js';
|
||||
import './sash.css';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
||||
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
||||
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
||||
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
||||
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
||||
};
|
||||
/**
|
||||
* Allow the sashes to be visible at runtime.
|
||||
* @remark Use for development purposes only.
|
||||
*/
|
||||
const DEBUG = false;
|
||||
var OrthogonalEdge;
|
||||
(function (OrthogonalEdge) {
|
||||
OrthogonalEdge["North"] = "north";
|
||||
OrthogonalEdge["South"] = "south";
|
||||
OrthogonalEdge["East"] = "east";
|
||||
OrthogonalEdge["West"] = "west";
|
||||
})(OrthogonalEdge || (OrthogonalEdge = {}));
|
||||
let globalSize = 4;
|
||||
const onDidChangeGlobalSize = new Emitter();
|
||||
let globalHoverDelay = 300;
|
||||
const onDidChangeHoverDelay = new Emitter();
|
||||
class MouseEventFactory {
|
||||
constructor(el) {
|
||||
this.el = el;
|
||||
this.disposables = new DisposableStore();
|
||||
}
|
||||
get onPointerMove() {
|
||||
return this.disposables.add(new DomEmitter(getWindow(this.el), 'mousemove')).event;
|
||||
}
|
||||
get onPointerUp() {
|
||||
return this.disposables.add(new DomEmitter(getWindow(this.el), 'mouseup')).event;
|
||||
}
|
||||
dispose() {
|
||||
this.disposables.dispose();
|
||||
}
|
||||
}
|
||||
__decorate([
|
||||
memoize
|
||||
], MouseEventFactory.prototype, "onPointerMove", null);
|
||||
__decorate([
|
||||
memoize
|
||||
], MouseEventFactory.prototype, "onPointerUp", null);
|
||||
class GestureEventFactory {
|
||||
get onPointerMove() {
|
||||
return this.disposables.add(new DomEmitter(this.el, EventType.Change)).event;
|
||||
}
|
||||
get onPointerUp() {
|
||||
return this.disposables.add(new DomEmitter(this.el, EventType.End)).event;
|
||||
}
|
||||
constructor(el) {
|
||||
this.el = el;
|
||||
this.disposables = new DisposableStore();
|
||||
}
|
||||
dispose() {
|
||||
this.disposables.dispose();
|
||||
}
|
||||
}
|
||||
__decorate([
|
||||
memoize
|
||||
], GestureEventFactory.prototype, "onPointerMove", null);
|
||||
__decorate([
|
||||
memoize
|
||||
], GestureEventFactory.prototype, "onPointerUp", null);
|
||||
class OrthogonalPointerEventFactory {
|
||||
get onPointerMove() {
|
||||
return this.factory.onPointerMove;
|
||||
}
|
||||
get onPointerUp() {
|
||||
return this.factory.onPointerUp;
|
||||
}
|
||||
constructor(factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
dispose() {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
__decorate([
|
||||
memoize
|
||||
], OrthogonalPointerEventFactory.prototype, "onPointerMove", null);
|
||||
__decorate([
|
||||
memoize
|
||||
], OrthogonalPointerEventFactory.prototype, "onPointerUp", null);
|
||||
const PointerEventsDisabledCssClass = 'pointer-events-disabled';
|
||||
/**
|
||||
* The {@link Sash} is the UI component which allows the user to resize other
|
||||
* components. It's usually an invisible horizontal or vertical line which, when
|
||||
* hovered, becomes highlighted and can be dragged along the perpendicular dimension
|
||||
* to its direction.
|
||||
*
|
||||
* Features:
|
||||
* - Touch event handling
|
||||
* - Corner sash support
|
||||
* - Hover with different mouse cursor support
|
||||
* - Configurable hover size
|
||||
* - Linked sash support, for 2x2 corner sashes
|
||||
*/
|
||||
class Sash extends Disposable {
|
||||
get state() { return this._state; }
|
||||
get orthogonalStartSash() { return this._orthogonalStartSash; }
|
||||
get orthogonalEndSash() { return this._orthogonalEndSash; }
|
||||
/**
|
||||
* The state of a sash defines whether it can be interacted with by the user
|
||||
* as well as what mouse cursor to use, when hovered.
|
||||
*/
|
||||
set state(state) {
|
||||
if (this._state === state) {
|
||||
return;
|
||||
}
|
||||
this.el.classList.toggle('disabled', state === 0 /* SashState.Disabled */);
|
||||
this.el.classList.toggle('minimum', state === 1 /* SashState.AtMinimum */);
|
||||
this.el.classList.toggle('maximum', state === 2 /* SashState.AtMaximum */);
|
||||
this._state = state;
|
||||
this.onDidEnablementChange.fire(state);
|
||||
}
|
||||
/**
|
||||
* An event which fires whenever the user starts dragging this sash.
|
||||
*/
|
||||
get onDidStart() { return this._onDidStart.event; }
|
||||
/**
|
||||
* An event which fires whenever the user moves the mouse while
|
||||
* dragging this sash.
|
||||
*/
|
||||
get onDidChange() { return this._onDidChange.event; }
|
||||
/**
|
||||
* An event which fires whenever the user double clicks this sash.
|
||||
*/
|
||||
get onDidReset() { return this._onDidReset.event; }
|
||||
/**
|
||||
* An event which fires whenever the user stops dragging this sash.
|
||||
*/
|
||||
get onDidEnd() { return this._onDidEnd.event; }
|
||||
/**
|
||||
* A reference to another sash, perpendicular to this one, which
|
||||
* aligns at the start of this one. A corner sash will be created
|
||||
* automatically at that location.
|
||||
*
|
||||
* The start of a horizontal sash is its left-most position.
|
||||
* The start of a vertical sash is its top-most position.
|
||||
*/
|
||||
set orthogonalStartSash(sash) {
|
||||
if (this._orthogonalStartSash === sash) {
|
||||
return;
|
||||
}
|
||||
this.orthogonalStartDragHandleDisposables.clear();
|
||||
this.orthogonalStartSashDisposables.clear();
|
||||
if (sash) {
|
||||
const onChange = (state) => {
|
||||
this.orthogonalStartDragHandleDisposables.clear();
|
||||
if (state !== 0 /* SashState.Disabled */) {
|
||||
this._orthogonalStartDragHandle = append(this.el, $('.orthogonal-drag-handle.start'));
|
||||
this.orthogonalStartDragHandleDisposables.add(toDisposable(() => this._orthogonalStartDragHandle.remove()));
|
||||
this.orthogonalStartDragHandleDisposables.add(addDisposableListener(this._orthogonalStartDragHandle, 'mouseenter', () => Sash.onMouseEnter(sash)));
|
||||
this.orthogonalStartDragHandleDisposables.add(addDisposableListener(this._orthogonalStartDragHandle, 'mouseleave', () => Sash.onMouseLeave(sash)));
|
||||
}
|
||||
};
|
||||
this.orthogonalStartSashDisposables.add(sash.onDidEnablementChange.event(onChange, this));
|
||||
onChange(sash.state);
|
||||
}
|
||||
this._orthogonalStartSash = sash;
|
||||
}
|
||||
/**
|
||||
* A reference to another sash, perpendicular to this one, which
|
||||
* aligns at the end of this one. A corner sash will be created
|
||||
* automatically at that location.
|
||||
*
|
||||
* The end of a horizontal sash is its right-most position.
|
||||
* The end of a vertical sash is its bottom-most position.
|
||||
*/
|
||||
set orthogonalEndSash(sash) {
|
||||
if (this._orthogonalEndSash === sash) {
|
||||
return;
|
||||
}
|
||||
this.orthogonalEndDragHandleDisposables.clear();
|
||||
this.orthogonalEndSashDisposables.clear();
|
||||
if (sash) {
|
||||
const onChange = (state) => {
|
||||
this.orthogonalEndDragHandleDisposables.clear();
|
||||
if (state !== 0 /* SashState.Disabled */) {
|
||||
this._orthogonalEndDragHandle = append(this.el, $('.orthogonal-drag-handle.end'));
|
||||
this.orthogonalEndDragHandleDisposables.add(toDisposable(() => this._orthogonalEndDragHandle.remove()));
|
||||
this.orthogonalEndDragHandleDisposables.add(addDisposableListener(this._orthogonalEndDragHandle, 'mouseenter', () => Sash.onMouseEnter(sash)));
|
||||
this.orthogonalEndDragHandleDisposables.add(addDisposableListener(this._orthogonalEndDragHandle, 'mouseleave', () => Sash.onMouseLeave(sash)));
|
||||
}
|
||||
};
|
||||
this.orthogonalEndSashDisposables.add(sash.onDidEnablementChange.event(onChange, this));
|
||||
onChange(sash.state);
|
||||
}
|
||||
this._orthogonalEndSash = sash;
|
||||
}
|
||||
constructor(container, layoutProvider, options) {
|
||||
super();
|
||||
this.hoverDelay = globalHoverDelay;
|
||||
this.hoverDelayer = this._register(new Delayer(this.hoverDelay));
|
||||
this._state = 3 /* SashState.Enabled */;
|
||||
this.onDidEnablementChange = this._register(new Emitter());
|
||||
this._onDidStart = this._register(new Emitter());
|
||||
this._onDidChange = this._register(new Emitter());
|
||||
this._onDidReset = this._register(new Emitter());
|
||||
this._onDidEnd = this._register(new Emitter());
|
||||
this.orthogonalStartSashDisposables = this._register(new DisposableStore());
|
||||
this.orthogonalStartDragHandleDisposables = this._register(new DisposableStore());
|
||||
this.orthogonalEndSashDisposables = this._register(new DisposableStore());
|
||||
this.orthogonalEndDragHandleDisposables = this._register(new DisposableStore());
|
||||
/**
|
||||
* A linked sash will be forwarded the same user interactions and events
|
||||
* so it moves exactly the same way as this sash.
|
||||
*
|
||||
* Useful in 2x2 grids. Not meant for widespread usage.
|
||||
*/
|
||||
this.linkedSash = undefined;
|
||||
this.el = append(container, $('.monaco-sash'));
|
||||
if (options.orthogonalEdge) {
|
||||
this.el.classList.add(`orthogonal-edge-${options.orthogonalEdge}`);
|
||||
}
|
||||
if (isMacintosh) {
|
||||
this.el.classList.add('mac');
|
||||
}
|
||||
this._register(addDisposableListener(this.el, 'mousedown', e => this.onPointerStart(e, new MouseEventFactory(container))));
|
||||
this._register(addDisposableListener(this.el, 'dblclick', e => this.onPointerDoublePress(e)));
|
||||
this._register(addDisposableListener(this.el, 'mouseenter', () => Sash.onMouseEnter(this)));
|
||||
this._register(addDisposableListener(this.el, 'mouseleave', () => Sash.onMouseLeave(this)));
|
||||
this._register(Gesture.addTarget(this.el));
|
||||
this._register(addDisposableListener(this.el, EventType.Start, e => this.onPointerStart(e, new GestureEventFactory(this.el))));
|
||||
let doubleTapTimeout = undefined;
|
||||
this._register(addDisposableListener(this.el, EventType.Tap, event => {
|
||||
if (doubleTapTimeout) {
|
||||
clearTimeout(doubleTapTimeout);
|
||||
doubleTapTimeout = undefined;
|
||||
this.onPointerDoublePress(event);
|
||||
return;
|
||||
}
|
||||
clearTimeout(doubleTapTimeout);
|
||||
doubleTapTimeout = setTimeout(() => doubleTapTimeout = undefined, 250);
|
||||
}));
|
||||
if (typeof options.size === 'number') {
|
||||
this.size = options.size;
|
||||
if (options.orientation === 0 /* Orientation.VERTICAL */) {
|
||||
this.el.style.width = `${this.size}px`;
|
||||
}
|
||||
else {
|
||||
this.el.style.height = `${this.size}px`;
|
||||
}
|
||||
}
|
||||
else {
|
||||
this.size = globalSize;
|
||||
this._register(onDidChangeGlobalSize.event(size => {
|
||||
this.size = size;
|
||||
this.layout();
|
||||
}));
|
||||
}
|
||||
this._register(onDidChangeHoverDelay.event(delay => this.hoverDelay = delay));
|
||||
this.layoutProvider = layoutProvider;
|
||||
this.orthogonalStartSash = options.orthogonalStartSash;
|
||||
this.orthogonalEndSash = options.orthogonalEndSash;
|
||||
this.orientation = options.orientation || 0 /* Orientation.VERTICAL */;
|
||||
if (this.orientation === 1 /* Orientation.HORIZONTAL */) {
|
||||
this.el.classList.add('horizontal');
|
||||
this.el.classList.remove('vertical');
|
||||
}
|
||||
else {
|
||||
this.el.classList.remove('horizontal');
|
||||
this.el.classList.add('vertical');
|
||||
}
|
||||
this.el.classList.toggle('debug', DEBUG);
|
||||
this.layout();
|
||||
}
|
||||
onPointerStart(event, pointerEventFactory) {
|
||||
EventHelper.stop(event);
|
||||
let isMultisashResize = false;
|
||||
// eslint-disable-next-line local/code-no-any-casts
|
||||
if (!event.__orthogonalSashEvent) {
|
||||
const orthogonalSash = this.getOrthogonalSash(event);
|
||||
if (orthogonalSash) {
|
||||
isMultisashResize = true;
|
||||
// eslint-disable-next-line local/code-no-any-casts
|
||||
event.__orthogonalSashEvent = true;
|
||||
orthogonalSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory));
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line local/code-no-any-casts
|
||||
if (this.linkedSash && !event.__linkedSashEvent) {
|
||||
// eslint-disable-next-line local/code-no-any-casts
|
||||
event.__linkedSashEvent = true;
|
||||
this.linkedSash.onPointerStart(event, new OrthogonalPointerEventFactory(pointerEventFactory));
|
||||
}
|
||||
if (!this.state) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
const iframes = this.el.ownerDocument.getElementsByTagName('iframe');
|
||||
for (const iframe of iframes) {
|
||||
iframe.classList.add(PointerEventsDisabledCssClass); // disable mouse events on iframes as long as we drag the sash
|
||||
}
|
||||
const startX = event.pageX;
|
||||
const startY = event.pageY;
|
||||
const altKey = event.altKey;
|
||||
const startEvent = { startX, currentX: startX, startY, currentY: startY, altKey };
|
||||
this.el.classList.add('active');
|
||||
this._onDidStart.fire(startEvent);
|
||||
// fix https://github.com/microsoft/vscode/issues/21675
|
||||
const style = createStyleSheet(this.el);
|
||||
const updateStyle = () => {
|
||||
let cursor = '';
|
||||
if (isMultisashResize) {
|
||||
cursor = 'all-scroll';
|
||||
}
|
||||
else if (this.orientation === 1 /* Orientation.HORIZONTAL */) {
|
||||
if (this.state === 1 /* SashState.AtMinimum */) {
|
||||
cursor = 's-resize';
|
||||
}
|
||||
else if (this.state === 2 /* SashState.AtMaximum */) {
|
||||
cursor = 'n-resize';
|
||||
}
|
||||
else {
|
||||
cursor = isMacintosh ? 'row-resize' : 'ns-resize';
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (this.state === 1 /* SashState.AtMinimum */) {
|
||||
cursor = 'e-resize';
|
||||
}
|
||||
else if (this.state === 2 /* SashState.AtMaximum */) {
|
||||
cursor = 'w-resize';
|
||||
}
|
||||
else {
|
||||
cursor = isMacintosh ? 'col-resize' : 'ew-resize';
|
||||
}
|
||||
}
|
||||
style.textContent = `* { cursor: ${cursor} !important; }`;
|
||||
};
|
||||
const disposables = new DisposableStore();
|
||||
updateStyle();
|
||||
if (!isMultisashResize) {
|
||||
this.onDidEnablementChange.event(updateStyle, null, disposables);
|
||||
}
|
||||
const onPointerMove = (e) => {
|
||||
EventHelper.stop(e, false);
|
||||
const event = { startX, currentX: e.pageX, startY, currentY: e.pageY, altKey };
|
||||
this._onDidChange.fire(event);
|
||||
};
|
||||
const onPointerUp = (e) => {
|
||||
EventHelper.stop(e, false);
|
||||
style.remove();
|
||||
this.el.classList.remove('active');
|
||||
this._onDidEnd.fire();
|
||||
disposables.dispose();
|
||||
for (const iframe of iframes) {
|
||||
iframe.classList.remove(PointerEventsDisabledCssClass);
|
||||
}
|
||||
};
|
||||
pointerEventFactory.onPointerMove(onPointerMove, null, disposables);
|
||||
pointerEventFactory.onPointerUp(onPointerUp, null, disposables);
|
||||
disposables.add(pointerEventFactory);
|
||||
}
|
||||
onPointerDoublePress(e) {
|
||||
const orthogonalSash = this.getOrthogonalSash(e);
|
||||
if (orthogonalSash) {
|
||||
orthogonalSash._onDidReset.fire();
|
||||
}
|
||||
if (this.linkedSash) {
|
||||
this.linkedSash._onDidReset.fire();
|
||||
}
|
||||
this._onDidReset.fire();
|
||||
}
|
||||
static onMouseEnter(sash, fromLinkedSash = false) {
|
||||
if (sash.el.classList.contains('active')) {
|
||||
sash.hoverDelayer.cancel();
|
||||
sash.el.classList.add('hover');
|
||||
}
|
||||
else {
|
||||
sash.hoverDelayer.trigger(() => sash.el.classList.add('hover'), sash.hoverDelay).then(undefined, () => { });
|
||||
}
|
||||
if (!fromLinkedSash && sash.linkedSash) {
|
||||
Sash.onMouseEnter(sash.linkedSash, true);
|
||||
}
|
||||
}
|
||||
static onMouseLeave(sash, fromLinkedSash = false) {
|
||||
sash.hoverDelayer.cancel();
|
||||
sash.el.classList.remove('hover');
|
||||
if (!fromLinkedSash && sash.linkedSash) {
|
||||
Sash.onMouseLeave(sash.linkedSash, true);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Forcefully stop any user interactions with this sash.
|
||||
* Useful when hiding a parent component, while the user is still
|
||||
* interacting with the sash.
|
||||
*/
|
||||
clearSashHoverState() {
|
||||
Sash.onMouseLeave(this);
|
||||
}
|
||||
/**
|
||||
* Layout the sash. The sash will size and position itself
|
||||
* based on its provided {@link ISashLayoutProvider layout provider}.
|
||||
*/
|
||||
layout() {
|
||||
if (this.orientation === 0 /* Orientation.VERTICAL */) {
|
||||
const verticalProvider = this.layoutProvider;
|
||||
this.el.style.left = verticalProvider.getVerticalSashLeft(this) - (this.size / 2) + 'px';
|
||||
if (verticalProvider.getVerticalSashTop) {
|
||||
this.el.style.top = verticalProvider.getVerticalSashTop(this) + 'px';
|
||||
}
|
||||
if (verticalProvider.getVerticalSashHeight) {
|
||||
this.el.style.height = verticalProvider.getVerticalSashHeight(this) + 'px';
|
||||
}
|
||||
}
|
||||
else {
|
||||
const horizontalProvider = this.layoutProvider;
|
||||
this.el.style.top = horizontalProvider.getHorizontalSashTop(this) - (this.size / 2) + 'px';
|
||||
if (horizontalProvider.getHorizontalSashLeft) {
|
||||
this.el.style.left = horizontalProvider.getHorizontalSashLeft(this) + 'px';
|
||||
}
|
||||
if (horizontalProvider.getHorizontalSashWidth) {
|
||||
this.el.style.width = horizontalProvider.getHorizontalSashWidth(this) + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
getOrthogonalSash(e) {
|
||||
const target = e.initialTarget ?? e.target;
|
||||
if (!target || !(isHTMLElement(target))) {
|
||||
return undefined;
|
||||
}
|
||||
if (target.classList.contains('orthogonal-drag-handle')) {
|
||||
return target.classList.contains('start') ? this.orthogonalStartSash : this.orthogonalEndSash;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
dispose() {
|
||||
super.dispose();
|
||||
this.el.remove();
|
||||
}
|
||||
}
|
||||
|
||||
export { OrthogonalEdge, Sash };
|
||||
Generated
Vendored
+214
@@ -0,0 +1,214 @@
|
||||
import { addDisposableListener, EventType, getDomNodePagePosition } from '../../dom.js';
|
||||
import { createFastDomNode } from '../../fastDomNode.js';
|
||||
import { GlobalPointerMoveMonitor } from '../../globalPointerMoveMonitor.js';
|
||||
import { ScrollbarArrow } from './scrollbarArrow.js';
|
||||
import { ScrollbarVisibilityController } from './scrollbarVisibilityController.js';
|
||||
import { Widget } from '../widget.js';
|
||||
import { isWindows } from '../../../common/platform.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
/**
|
||||
* The orthogonal distance to the slider at which dragging "resets". This implements "snapping"
|
||||
*/
|
||||
const POINTER_DRAG_RESET_DISTANCE = 140;
|
||||
class AbstractScrollbar extends Widget {
|
||||
constructor(opts) {
|
||||
super();
|
||||
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());
|
||||
this._pointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
|
||||
this._shouldRender = true;
|
||||
this.domNode = createFastDomNode(document.createElement('div'));
|
||||
this.domNode.setAttribute('role', 'presentation');
|
||||
this.domNode.setAttribute('aria-hidden', 'true');
|
||||
this._visibilityController.setDomNode(this.domNode);
|
||||
this.domNode.setPosition('absolute');
|
||||
this._register(addDisposableListener(this.domNode.domNode, EventType.POINTER_DOWN, (e) => this._domNodePointerDown(e)));
|
||||
}
|
||||
// ----------------- creation
|
||||
/**
|
||||
* Creates the dom node for an arrow & adds it to the container
|
||||
*/
|
||||
_createArrow(opts) {
|
||||
const arrow = this._register(new ScrollbarArrow(opts));
|
||||
this.domNode.domNode.appendChild(arrow.bgDomNode);
|
||||
this.domNode.domNode.appendChild(arrow.domNode);
|
||||
}
|
||||
/**
|
||||
* Creates the slider dom node, adds it to the container & hooks up the events
|
||||
*/
|
||||
_createSlider(top, left, width, height) {
|
||||
this.slider = createFastDomNode(document.createElement('div'));
|
||||
this.slider.setClassName('slider');
|
||||
this.slider.setPosition('absolute');
|
||||
this.slider.setTop(top);
|
||||
this.slider.setLeft(left);
|
||||
if (typeof width === 'number') {
|
||||
this.slider.setWidth(width);
|
||||
}
|
||||
if (typeof height === 'number') {
|
||||
this.slider.setHeight(height);
|
||||
}
|
||||
this.slider.setLayerHinting(true);
|
||||
this.slider.setContain('strict');
|
||||
this.domNode.domNode.appendChild(this.slider.domNode);
|
||||
this._register(addDisposableListener(this.slider.domNode, EventType.POINTER_DOWN, (e) => {
|
||||
if (e.button === 0) {
|
||||
e.preventDefault();
|
||||
this._sliderPointerDown(e);
|
||||
}
|
||||
}));
|
||||
this.onclick(this.slider.domNode, e => {
|
||||
if (e.leftButton) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
// ----------------- Update state
|
||||
_onElementSize(visibleSize) {
|
||||
if (this._scrollbarState.setVisibleSize(visibleSize)) {
|
||||
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
|
||||
this._shouldRender = true;
|
||||
if (!this._lazyRender) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
return this._shouldRender;
|
||||
}
|
||||
_onElementScrollSize(elementScrollSize) {
|
||||
if (this._scrollbarState.setScrollSize(elementScrollSize)) {
|
||||
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
|
||||
this._shouldRender = true;
|
||||
if (!this._lazyRender) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
return this._shouldRender;
|
||||
}
|
||||
_onElementScrollPosition(elementScrollPosition) {
|
||||
if (this._scrollbarState.setScrollPosition(elementScrollPosition)) {
|
||||
this._visibilityController.setIsNeeded(this._scrollbarState.isNeeded());
|
||||
this._shouldRender = true;
|
||||
if (!this._lazyRender) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
return this._shouldRender;
|
||||
}
|
||||
// ----------------- rendering
|
||||
beginReveal() {
|
||||
this._visibilityController.setShouldBeVisible(true);
|
||||
}
|
||||
beginHide() {
|
||||
this._visibilityController.setShouldBeVisible(false);
|
||||
}
|
||||
render() {
|
||||
if (!this._shouldRender) {
|
||||
return;
|
||||
}
|
||||
this._shouldRender = false;
|
||||
this._renderDomNode(this._scrollbarState.getRectangleLargeSize(), this._scrollbarState.getRectangleSmallSize());
|
||||
this._updateSlider(this._scrollbarState.getSliderSize(), this._scrollbarState.getArrowSize() + this._scrollbarState.getSliderPosition());
|
||||
}
|
||||
// ----------------- DOM events
|
||||
_domNodePointerDown(e) {
|
||||
if (e.target !== this.domNode.domNode) {
|
||||
return;
|
||||
}
|
||||
this._onPointerDown(e);
|
||||
}
|
||||
delegatePointerDown(e) {
|
||||
const domTop = this.domNode.domNode.getClientRects()[0].top;
|
||||
const sliderStart = domTop + this._scrollbarState.getSliderPosition();
|
||||
const sliderStop = domTop + this._scrollbarState.getSliderPosition() + this._scrollbarState.getSliderSize();
|
||||
const pointerPos = this._sliderPointerPosition(e);
|
||||
if (sliderStart <= pointerPos && pointerPos <= sliderStop) {
|
||||
// Act as if it was a pointer down on the slider
|
||||
if (e.button === 0) {
|
||||
e.preventDefault();
|
||||
this._sliderPointerDown(e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Act as if it was a pointer down on the scrollbar
|
||||
this._onPointerDown(e);
|
||||
}
|
||||
}
|
||||
_onPointerDown(e) {
|
||||
let offsetX;
|
||||
let offsetY;
|
||||
if (e.target === this.domNode.domNode && typeof e.offsetX === 'number' && typeof e.offsetY === 'number') {
|
||||
offsetX = e.offsetX;
|
||||
offsetY = e.offsetY;
|
||||
}
|
||||
else {
|
||||
const domNodePosition = getDomNodePagePosition(this.domNode.domNode);
|
||||
offsetX = e.pageX - domNodePosition.left;
|
||||
offsetY = e.pageY - domNodePosition.top;
|
||||
}
|
||||
const isMouse = (e.pointerType === 'mouse');
|
||||
const isLeftClick = (e.button === 0);
|
||||
if (isLeftClick || !isMouse) {
|
||||
const offset = this._pointerDownRelativePosition(offsetX, offsetY);
|
||||
this._setDesiredScrollPositionNow(this._scrollByPage
|
||||
? this._scrollbarState.getDesiredScrollPositionFromOffsetPaged(offset)
|
||||
: this._scrollbarState.getDesiredScrollPositionFromOffset(offset));
|
||||
}
|
||||
if (isLeftClick) {
|
||||
// left button
|
||||
e.preventDefault();
|
||||
this._sliderPointerDown(e);
|
||||
}
|
||||
}
|
||||
_sliderPointerDown(e) {
|
||||
if (!e.target || !(e.target instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
const initialPointerPosition = this._sliderPointerPosition(e);
|
||||
const initialPointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(e);
|
||||
const initialScrollbarState = this._scrollbarState.clone();
|
||||
this.slider.toggleClassName('active', true);
|
||||
this._pointerMoveMonitor.startMonitoring(e.target, e.pointerId, e.buttons, (pointerMoveData) => {
|
||||
const pointerOrthogonalPosition = this._sliderOrthogonalPointerPosition(pointerMoveData);
|
||||
const pointerOrthogonalDelta = Math.abs(pointerOrthogonalPosition - initialPointerOrthogonalPosition);
|
||||
if (isWindows && pointerOrthogonalDelta > POINTER_DRAG_RESET_DISTANCE) {
|
||||
// The pointer has wondered away from the scrollbar => reset dragging
|
||||
this._setDesiredScrollPositionNow(initialScrollbarState.getScrollPosition());
|
||||
return;
|
||||
}
|
||||
const pointerPosition = this._sliderPointerPosition(pointerMoveData);
|
||||
const pointerDelta = pointerPosition - initialPointerPosition;
|
||||
this._setDesiredScrollPositionNow(initialScrollbarState.getDesiredScrollPositionFromDelta(pointerDelta));
|
||||
}, () => {
|
||||
this.slider.toggleClassName('active', false);
|
||||
this._host.onDragEnd();
|
||||
});
|
||||
this._host.onDragStart();
|
||||
}
|
||||
_setDesiredScrollPositionNow(_desiredScrollPosition) {
|
||||
const desiredScrollPosition = {};
|
||||
this.writeScrollPosition(desiredScrollPosition, _desiredScrollPosition);
|
||||
this._scrollable.setScrollPositionNow(desiredScrollPosition);
|
||||
}
|
||||
updateScrollbarSize(scrollbarSize) {
|
||||
this._updateScrollbarSize(scrollbarSize);
|
||||
this._scrollbarState.setScrollbarSize(scrollbarSize);
|
||||
this._shouldRender = true;
|
||||
if (!this._lazyRender) {
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
isNeeded() {
|
||||
return this._scrollbarState.isNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
export { AbstractScrollbar };
|
||||
Generated
Vendored
+91
@@ -0,0 +1,91 @@
|
||||
import { StandardWheelEvent } from '../../mouseEvent.js';
|
||||
import { AbstractScrollbar } from './abstractScrollbar.js';
|
||||
import { ARROW_IMG_SIZE } from './scrollbarArrow.js';
|
||||
import { ScrollbarState } from './scrollbarState.js';
|
||||
import { Codicon } from '../../../common/codicons.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class HorizontalScrollbar extends AbstractScrollbar {
|
||||
constructor(scrollable, options, host) {
|
||||
const scrollDimensions = scrollable.getScrollDimensions();
|
||||
const scrollPosition = scrollable.getCurrentScrollPosition();
|
||||
super({
|
||||
lazyRender: options.lazyRender,
|
||||
host: host,
|
||||
scrollbarState: new ScrollbarState((options.horizontalHasArrows ? options.arrowSize : 0), (options.horizontal === 2 /* ScrollbarVisibility.Hidden */ ? 0 : options.horizontalScrollbarSize), (options.vertical === 2 /* ScrollbarVisibility.Hidden */ ? 0 : options.verticalScrollbarSize), scrollDimensions.width, scrollDimensions.scrollWidth, scrollPosition.scrollLeft),
|
||||
visibility: options.horizontal,
|
||||
extraScrollbarClassName: 'horizontal',
|
||||
scrollable: scrollable,
|
||||
scrollByPage: options.scrollByPage
|
||||
});
|
||||
if (options.horizontalHasArrows) {
|
||||
const arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
|
||||
const scrollbarDelta = (options.horizontalScrollbarSize - ARROW_IMG_SIZE) / 2;
|
||||
this._createArrow({
|
||||
className: 'scra',
|
||||
icon: Codicon.scrollbarButtonLeft,
|
||||
top: scrollbarDelta,
|
||||
left: arrowDelta,
|
||||
bottom: undefined,
|
||||
right: undefined,
|
||||
bgWidth: options.arrowSize,
|
||||
bgHeight: options.horizontalScrollbarSize,
|
||||
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 1, 0)),
|
||||
});
|
||||
this._createArrow({
|
||||
className: 'scra',
|
||||
icon: Codicon.scrollbarButtonRight,
|
||||
top: scrollbarDelta,
|
||||
left: undefined,
|
||||
bottom: undefined,
|
||||
right: arrowDelta,
|
||||
bgWidth: options.arrowSize,
|
||||
bgHeight: options.horizontalScrollbarSize,
|
||||
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, -1, 0)),
|
||||
});
|
||||
}
|
||||
this._createSlider(Math.floor((options.horizontalScrollbarSize - options.horizontalSliderSize) / 2), 0, undefined, options.horizontalSliderSize);
|
||||
}
|
||||
_updateSlider(sliderSize, sliderPosition) {
|
||||
this.slider.setWidth(sliderSize);
|
||||
this.slider.setLeft(sliderPosition);
|
||||
}
|
||||
_renderDomNode(largeSize, smallSize) {
|
||||
this.domNode.setWidth(largeSize);
|
||||
this.domNode.setHeight(smallSize);
|
||||
this.domNode.setLeft(0);
|
||||
this.domNode.setBottom(0);
|
||||
}
|
||||
onDidScroll(e) {
|
||||
this._shouldRender = this._onElementScrollSize(e.scrollWidth) || this._shouldRender;
|
||||
this._shouldRender = this._onElementScrollPosition(e.scrollLeft) || this._shouldRender;
|
||||
this._shouldRender = this._onElementSize(e.width) || this._shouldRender;
|
||||
return this._shouldRender;
|
||||
}
|
||||
_pointerDownRelativePosition(offsetX, offsetY) {
|
||||
return offsetX;
|
||||
}
|
||||
_sliderPointerPosition(e) {
|
||||
return e.pageX;
|
||||
}
|
||||
_sliderOrthogonalPointerPosition(e) {
|
||||
return e.pageY;
|
||||
}
|
||||
_updateScrollbarSize(size) {
|
||||
this.slider.setHeight(size);
|
||||
}
|
||||
writeScrollPosition(target, scrollPosition) {
|
||||
target.scrollLeft = scrollPosition;
|
||||
}
|
||||
updateOptions(options) {
|
||||
this.updateScrollbarSize(options.horizontal === 2 /* ScrollbarVisibility.Hidden */ ? 0 : options.horizontalScrollbarSize);
|
||||
this._scrollbarState.setOppositeScrollbarSize(options.vertical === 2 /* ScrollbarVisibility.Hidden */ ? 0 : options.verticalScrollbarSize);
|
||||
this._visibilityController.setVisibility(options.horizontal);
|
||||
this._scrollByPage = options.scrollByPage;
|
||||
}
|
||||
}
|
||||
|
||||
export { HorizontalScrollbar };
|
||||
Generated
Vendored
+76
@@ -0,0 +1,76 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Arrows */
|
||||
.monaco-scrollable-element > .scrollbar > .scra {
|
||||
cursor: pointer;
|
||||
font-size: 11px !important;
|
||||
}
|
||||
|
||||
.monaco-scrollable-element > .visible {
|
||||
opacity: 1;
|
||||
|
||||
/* Background rule added for IE9 - to allow clicks on dom node */
|
||||
background:rgba(0,0,0,0);
|
||||
|
||||
transition: opacity 100ms linear;
|
||||
/* In front of peek view */
|
||||
z-index: 11;
|
||||
}
|
||||
.monaco-scrollable-element > .invisible {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.monaco-scrollable-element > .invisible.fade {
|
||||
transition: opacity 800ms linear;
|
||||
}
|
||||
|
||||
/* Scrollable Content Inset Shadow */
|
||||
.monaco-scrollable-element > .shadow {
|
||||
position: absolute;
|
||||
display: none;
|
||||
}
|
||||
.monaco-scrollable-element > .shadow.top {
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 3px;
|
||||
height: 3px;
|
||||
width: 100%;
|
||||
box-shadow: var(--vscode-scrollbar-shadow) 0 6px 6px -6px inset;
|
||||
}
|
||||
.monaco-scrollable-element > .shadow.left {
|
||||
display: block;
|
||||
top: 3px;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 3px;
|
||||
box-shadow: var(--vscode-scrollbar-shadow) 6px 0 6px -6px inset;
|
||||
}
|
||||
.monaco-scrollable-element > .shadow.top-left-corner {
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 3px;
|
||||
width: 3px;
|
||||
}
|
||||
.monaco-scrollable-element > .shadow.top.left {
|
||||
box-shadow: var(--vscode-scrollbar-shadow) 6px 0 6px -6px inset;
|
||||
}
|
||||
|
||||
.monaco-scrollable-element > .scrollbar {
|
||||
background: var(--vscode-scrollbar-background);
|
||||
}
|
||||
|
||||
.monaco-scrollable-element > .scrollbar > .slider {
|
||||
background: var(--vscode-scrollbarSlider-background);
|
||||
}
|
||||
|
||||
.monaco-scrollable-element > .scrollbar > .slider:hover {
|
||||
background: var(--vscode-scrollbarSlider-hoverBackground);
|
||||
}
|
||||
|
||||
.monaco-scrollable-element > .scrollbar > .slider.active {
|
||||
background: var(--vscode-scrollbarSlider-activeBackground);
|
||||
}
|
||||
Generated
Vendored
+603
@@ -0,0 +1,603 @@
|
||||
import { isChrome, getZoomFactor } from '../../browser.js';
|
||||
import { getWindow, addDisposableListener, EventType, scheduleAtNextAnimationFrame } from '../../dom.js';
|
||||
import { createFastDomNode } from '../../fastDomNode.js';
|
||||
import { StandardWheelEvent } from '../../mouseEvent.js';
|
||||
import { HorizontalScrollbar } from './horizontalScrollbar.js';
|
||||
import { VerticalScrollbar } from './verticalScrollbar.js';
|
||||
import { Widget } from '../widget.js';
|
||||
import { TimeoutTimer } from '../../../common/async.js';
|
||||
import { Emitter } from '../../../common/event.js';
|
||||
import { dispose } from '../../../common/lifecycle.js';
|
||||
import { isMacintosh } from '../../../common/platform.js';
|
||||
import { Scrollable } from '../../../common/scrollable.js';
|
||||
import './media/scrollbars.css';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const HIDE_TIMEOUT = 500;
|
||||
const SCROLL_WHEEL_SENSITIVITY = 50;
|
||||
class MouseWheelClassifierItem {
|
||||
constructor(timestamp, deltaX, deltaY) {
|
||||
this.timestamp = timestamp;
|
||||
this.deltaX = deltaX;
|
||||
this.deltaY = deltaY;
|
||||
this.score = 0;
|
||||
}
|
||||
}
|
||||
class MouseWheelClassifier {
|
||||
static { this.INSTANCE = new MouseWheelClassifier(); }
|
||||
constructor() {
|
||||
this._capacity = 5;
|
||||
this._memory = [];
|
||||
this._front = -1;
|
||||
this._rear = -1;
|
||||
}
|
||||
isPhysicalMouseWheel() {
|
||||
if (this._front === -1 && this._rear === -1) {
|
||||
// no elements
|
||||
return false;
|
||||
}
|
||||
// 0.5 * last + 0.25 * 2nd last + 0.125 * 3rd last + ...
|
||||
let remainingInfluence = 1;
|
||||
let score = 0;
|
||||
let iteration = 1;
|
||||
let index = this._rear;
|
||||
do {
|
||||
const influence = (index === this._front ? remainingInfluence : Math.pow(2, -iteration));
|
||||
remainingInfluence -= influence;
|
||||
score += this._memory[index].score * influence;
|
||||
if (index === this._front) {
|
||||
break;
|
||||
}
|
||||
index = (this._capacity + index - 1) % this._capacity;
|
||||
iteration++;
|
||||
} while (true);
|
||||
return (score <= 0.5);
|
||||
}
|
||||
acceptStandardWheelEvent(e) {
|
||||
if (isChrome) {
|
||||
const targetWindow = getWindow(e.browserEvent);
|
||||
const pageZoomFactor = getZoomFactor(targetWindow);
|
||||
// On Chrome, the incoming delta events are multiplied with the OS zoom factor.
|
||||
// The OS zoom factor can be reverse engineered by using the device pixel ratio and the configured zoom factor into account.
|
||||
this.accept(Date.now(), e.deltaX * pageZoomFactor, e.deltaY * pageZoomFactor);
|
||||
}
|
||||
else {
|
||||
this.accept(Date.now(), e.deltaX, e.deltaY);
|
||||
}
|
||||
}
|
||||
accept(timestamp, deltaX, deltaY) {
|
||||
let previousItem = null;
|
||||
const item = new MouseWheelClassifierItem(timestamp, deltaX, deltaY);
|
||||
if (this._front === -1 && this._rear === -1) {
|
||||
this._memory[0] = item;
|
||||
this._front = 0;
|
||||
this._rear = 0;
|
||||
}
|
||||
else {
|
||||
previousItem = this._memory[this._rear];
|
||||
this._rear = (this._rear + 1) % this._capacity;
|
||||
if (this._rear === this._front) {
|
||||
// Drop oldest
|
||||
this._front = (this._front + 1) % this._capacity;
|
||||
}
|
||||
this._memory[this._rear] = item;
|
||||
}
|
||||
item.score = this._computeScore(item, previousItem);
|
||||
}
|
||||
/**
|
||||
* A score between 0 and 1 for `item`.
|
||||
* - a score towards 0 indicates that the source appears to be a physical mouse wheel
|
||||
* - a score towards 1 indicates that the source appears to be a touchpad or magic mouse, etc.
|
||||
*/
|
||||
_computeScore(item, previousItem) {
|
||||
if (Math.abs(item.deltaX) > 0 && Math.abs(item.deltaY) > 0) {
|
||||
// both axes exercised => definitely not a physical mouse wheel
|
||||
return 1;
|
||||
}
|
||||
let score = 0.5;
|
||||
if (!this._isAlmostInt(item.deltaX) || !this._isAlmostInt(item.deltaY)) {
|
||||
// non-integer deltas => indicator that this is not a physical mouse wheel
|
||||
score += 0.25;
|
||||
}
|
||||
// Non-accelerating scroll => indicator that this is a physical mouse wheel
|
||||
// These can be identified by seeing whether they are the module of one another.
|
||||
if (previousItem) {
|
||||
const absDeltaX = Math.abs(item.deltaX);
|
||||
const absDeltaY = Math.abs(item.deltaY);
|
||||
const absPreviousDeltaX = Math.abs(previousItem.deltaX);
|
||||
const absPreviousDeltaY = Math.abs(previousItem.deltaY);
|
||||
// Min 1 to avoid division by zero, module 1 will still be 0.
|
||||
const minDeltaX = Math.max(Math.min(absDeltaX, absPreviousDeltaX), 1);
|
||||
const minDeltaY = Math.max(Math.min(absDeltaY, absPreviousDeltaY), 1);
|
||||
const maxDeltaX = Math.max(absDeltaX, absPreviousDeltaX);
|
||||
const maxDeltaY = Math.max(absDeltaY, absPreviousDeltaY);
|
||||
const isSameModulo = (maxDeltaX % minDeltaX === 0 && maxDeltaY % minDeltaY === 0);
|
||||
if (isSameModulo) {
|
||||
score -= 0.5;
|
||||
}
|
||||
}
|
||||
return Math.min(Math.max(score, 0), 1);
|
||||
}
|
||||
_isAlmostInt(value) {
|
||||
const epsilon = Number.EPSILON * 100; // Use a small tolerance factor for floating-point errors
|
||||
const delta = Math.abs(Math.round(value) - value);
|
||||
return (delta < 0.01 + epsilon);
|
||||
}
|
||||
}
|
||||
class AbstractScrollableElement extends Widget {
|
||||
get onScroll() { return this._onScroll.event; }
|
||||
get options() {
|
||||
return this._options;
|
||||
}
|
||||
constructor(element, options, scrollable) {
|
||||
super();
|
||||
this._inertialTimeout = null;
|
||||
this._inertialSpeed = { X: 0, Y: 0 };
|
||||
this._onScroll = this._register(new Emitter());
|
||||
this._onWillScroll = this._register(new Emitter());
|
||||
element.style.overflow = 'hidden';
|
||||
this._options = resolveOptions(options);
|
||||
this._scrollable = scrollable;
|
||||
this._register(this._scrollable.onScroll((e) => {
|
||||
this._onWillScroll.fire(e);
|
||||
this._onDidScroll(e);
|
||||
this._onScroll.fire(e);
|
||||
}));
|
||||
const scrollbarHost = {
|
||||
onMouseWheel: (mouseWheelEvent) => this._onMouseWheel(mouseWheelEvent),
|
||||
onDragStart: () => this._onDragStart(),
|
||||
onDragEnd: () => this._onDragEnd(),
|
||||
};
|
||||
this._verticalScrollbar = this._register(new VerticalScrollbar(this._scrollable, this._options, scrollbarHost));
|
||||
this._horizontalScrollbar = this._register(new HorizontalScrollbar(this._scrollable, this._options, scrollbarHost));
|
||||
this._domNode = document.createElement('div');
|
||||
this._domNode.className = 'monaco-scrollable-element ' + this._options.className;
|
||||
this._domNode.setAttribute('role', 'presentation');
|
||||
this._domNode.style.position = 'relative';
|
||||
this._domNode.style.overflow = 'hidden';
|
||||
this._domNode.appendChild(element);
|
||||
this._domNode.appendChild(this._horizontalScrollbar.domNode.domNode);
|
||||
this._domNode.appendChild(this._verticalScrollbar.domNode.domNode);
|
||||
if (this._options.useShadows) {
|
||||
this._leftShadowDomNode = createFastDomNode(document.createElement('div'));
|
||||
this._leftShadowDomNode.setClassName('shadow');
|
||||
this._domNode.appendChild(this._leftShadowDomNode.domNode);
|
||||
this._topShadowDomNode = createFastDomNode(document.createElement('div'));
|
||||
this._topShadowDomNode.setClassName('shadow');
|
||||
this._domNode.appendChild(this._topShadowDomNode.domNode);
|
||||
this._topLeftShadowDomNode = createFastDomNode(document.createElement('div'));
|
||||
this._topLeftShadowDomNode.setClassName('shadow');
|
||||
this._domNode.appendChild(this._topLeftShadowDomNode.domNode);
|
||||
}
|
||||
else {
|
||||
this._leftShadowDomNode = null;
|
||||
this._topShadowDomNode = null;
|
||||
this._topLeftShadowDomNode = null;
|
||||
}
|
||||
this._listenOnDomNode = this._options.listenOnDomNode || this._domNode;
|
||||
this._mouseWheelToDispose = [];
|
||||
this._setListeningToMouseWheel(this._options.handleMouseWheel);
|
||||
this.onmouseover(this._listenOnDomNode, (e) => this._onMouseOver(e));
|
||||
this.onmouseleave(this._listenOnDomNode, (e) => this._onMouseLeave(e));
|
||||
this._hideTimeout = this._register(new TimeoutTimer());
|
||||
this._isDragging = false;
|
||||
this._mouseIsOver = false;
|
||||
this._shouldRender = true;
|
||||
this._revealOnScroll = true;
|
||||
}
|
||||
dispose() {
|
||||
this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
|
||||
if (this._inertialTimeout) {
|
||||
this._inertialTimeout.dispose();
|
||||
this._inertialTimeout = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
/**
|
||||
* Get the generated 'scrollable' dom node
|
||||
*/
|
||||
getDomNode() {
|
||||
return this._domNode;
|
||||
}
|
||||
getOverviewRulerLayoutInfo() {
|
||||
return {
|
||||
parent: this._domNode,
|
||||
insertBefore: this._verticalScrollbar.domNode.domNode,
|
||||
};
|
||||
}
|
||||
/**
|
||||
* Delegate a pointer down event to the vertical scrollbar.
|
||||
* This is to help with clicking somewhere else and having the scrollbar react.
|
||||
*/
|
||||
delegateVerticalScrollbarPointerDown(browserEvent) {
|
||||
this._verticalScrollbar.delegatePointerDown(browserEvent);
|
||||
}
|
||||
getScrollDimensions() {
|
||||
return this._scrollable.getScrollDimensions();
|
||||
}
|
||||
setScrollDimensions(dimensions) {
|
||||
this._scrollable.setScrollDimensions(dimensions, false);
|
||||
}
|
||||
/**
|
||||
* Update the class name of the scrollable element.
|
||||
*/
|
||||
updateClassName(newClassName) {
|
||||
this._options.className = newClassName;
|
||||
// Defaults are different on Macs
|
||||
if (isMacintosh) {
|
||||
this._options.className += ' mac';
|
||||
}
|
||||
this._domNode.className = 'monaco-scrollable-element ' + this._options.className;
|
||||
}
|
||||
/**
|
||||
* Update configuration options for the scrollbar.
|
||||
*/
|
||||
updateOptions(newOptions) {
|
||||
if (typeof newOptions.handleMouseWheel !== 'undefined') {
|
||||
this._options.handleMouseWheel = newOptions.handleMouseWheel;
|
||||
this._setListeningToMouseWheel(this._options.handleMouseWheel);
|
||||
}
|
||||
if (typeof newOptions.mouseWheelScrollSensitivity !== 'undefined') {
|
||||
this._options.mouseWheelScrollSensitivity = newOptions.mouseWheelScrollSensitivity;
|
||||
}
|
||||
if (typeof newOptions.fastScrollSensitivity !== 'undefined') {
|
||||
this._options.fastScrollSensitivity = newOptions.fastScrollSensitivity;
|
||||
}
|
||||
if (typeof newOptions.scrollPredominantAxis !== 'undefined') {
|
||||
this._options.scrollPredominantAxis = newOptions.scrollPredominantAxis;
|
||||
}
|
||||
if (typeof newOptions.horizontal !== 'undefined') {
|
||||
this._options.horizontal = newOptions.horizontal;
|
||||
}
|
||||
if (typeof newOptions.vertical !== 'undefined') {
|
||||
this._options.vertical = newOptions.vertical;
|
||||
}
|
||||
if (typeof newOptions.horizontalScrollbarSize !== 'undefined') {
|
||||
this._options.horizontalScrollbarSize = newOptions.horizontalScrollbarSize;
|
||||
}
|
||||
if (typeof newOptions.verticalScrollbarSize !== 'undefined') {
|
||||
this._options.verticalScrollbarSize = newOptions.verticalScrollbarSize;
|
||||
}
|
||||
if (typeof newOptions.scrollByPage !== 'undefined') {
|
||||
this._options.scrollByPage = newOptions.scrollByPage;
|
||||
}
|
||||
this._horizontalScrollbar.updateOptions(this._options);
|
||||
this._verticalScrollbar.updateOptions(this._options);
|
||||
if (!this._options.lazyRender) {
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
delegateScrollFromMouseWheelEvent(browserEvent) {
|
||||
this._onMouseWheel(new StandardWheelEvent(browserEvent));
|
||||
}
|
||||
async _periodicSync() {
|
||||
let scheduleAgain = false;
|
||||
if (this._inertialSpeed.X !== 0 || this._inertialSpeed.Y !== 0) {
|
||||
this._scrollable.setScrollPositionNow({
|
||||
scrollTop: this._scrollable.getCurrentScrollPosition().scrollTop - this._inertialSpeed.Y * 100,
|
||||
scrollLeft: this._scrollable.getCurrentScrollPosition().scrollLeft - this._inertialSpeed.X * 100
|
||||
});
|
||||
this._inertialSpeed.X *= 0.9;
|
||||
this._inertialSpeed.Y *= 0.9;
|
||||
if (Math.abs(this._inertialSpeed.X) < 0.01) {
|
||||
this._inertialSpeed.X = 0;
|
||||
}
|
||||
if (Math.abs(this._inertialSpeed.Y) < 0.01) {
|
||||
this._inertialSpeed.Y = 0;
|
||||
}
|
||||
scheduleAgain = (this._inertialSpeed.X !== 0 || this._inertialSpeed.Y !== 0);
|
||||
}
|
||||
if (scheduleAgain) {
|
||||
if (!this._inertialTimeout) {
|
||||
this._inertialTimeout = new TimeoutTimer();
|
||||
}
|
||||
this._inertialTimeout.cancelAndSet(() => this._periodicSync(), 1000 / 60);
|
||||
}
|
||||
else {
|
||||
this._inertialTimeout?.dispose();
|
||||
this._inertialTimeout = null;
|
||||
}
|
||||
}
|
||||
// -------------------- mouse wheel scrolling --------------------
|
||||
_setListeningToMouseWheel(shouldListen) {
|
||||
const isListening = (this._mouseWheelToDispose.length > 0);
|
||||
if (isListening === shouldListen) {
|
||||
// No change
|
||||
return;
|
||||
}
|
||||
// Stop listening (if necessary)
|
||||
this._mouseWheelToDispose = dispose(this._mouseWheelToDispose);
|
||||
// Start listening (if necessary)
|
||||
if (shouldListen) {
|
||||
const onMouseWheel = (browserEvent) => {
|
||||
this._onMouseWheel(new StandardWheelEvent(browserEvent));
|
||||
};
|
||||
this._mouseWheelToDispose.push(addDisposableListener(this._listenOnDomNode, EventType.MOUSE_WHEEL, onMouseWheel, { passive: false }));
|
||||
}
|
||||
}
|
||||
_onMouseWheel(e) {
|
||||
if (e.browserEvent?.defaultPrevented) {
|
||||
return;
|
||||
}
|
||||
const classifier = MouseWheelClassifier.INSTANCE;
|
||||
{
|
||||
classifier.acceptStandardWheelEvent(e);
|
||||
}
|
||||
// useful for creating unit tests:
|
||||
// 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;
|
||||
if (this._options.scrollPredominantAxis) {
|
||||
if (this._options.scrollYToX && deltaX + deltaY === 0) {
|
||||
// when configured to map Y to X and we both see
|
||||
// no dominant axis and X and Y are competing with
|
||||
// identical values into opposite directions, we
|
||||
// ignore the delta as we cannot make a decision then
|
||||
deltaX = deltaY = 0;
|
||||
}
|
||||
else if (Math.abs(deltaY) >= Math.abs(deltaX)) {
|
||||
deltaX = 0;
|
||||
}
|
||||
else {
|
||||
deltaY = 0;
|
||||
}
|
||||
}
|
||||
if (this._options.flipAxes) {
|
||||
[deltaY, deltaX] = [deltaX, deltaY];
|
||||
}
|
||||
// Convert vertical scrolling to horizontal if shift is held, this
|
||||
// is handled at a higher level on Mac
|
||||
const shiftConvert = !isMacintosh && e.browserEvent && e.browserEvent.shiftKey;
|
||||
if ((this._options.scrollYToX || shiftConvert) && !deltaX) {
|
||||
deltaX = deltaY;
|
||||
deltaY = 0;
|
||||
}
|
||||
if (e.browserEvent && e.browserEvent.altKey) {
|
||||
// fastScrolling
|
||||
deltaX = deltaX * this._options.fastScrollSensitivity;
|
||||
deltaY = deltaY * this._options.fastScrollSensitivity;
|
||||
}
|
||||
const futureScrollPosition = this._scrollable.getFutureScrollPosition();
|
||||
let desiredScrollPosition = {};
|
||||
if (deltaY) {
|
||||
const deltaScrollTop = SCROLL_WHEEL_SENSITIVITY * deltaY;
|
||||
// Here we convert values such as -0.3 to -1 or 0.3 to 1, otherwise low speed scrolling will never scroll
|
||||
const desiredScrollTop = futureScrollPosition.scrollTop - (deltaScrollTop < 0 ? Math.floor(deltaScrollTop) : Math.ceil(deltaScrollTop));
|
||||
this._verticalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollTop);
|
||||
}
|
||||
if (deltaX) {
|
||||
const deltaScrollLeft = SCROLL_WHEEL_SENSITIVITY * deltaX;
|
||||
// Here we convert values such as -0.3 to -1 or 0.3 to 1, otherwise low speed scrolling will never scroll
|
||||
const desiredScrollLeft = futureScrollPosition.scrollLeft - (deltaScrollLeft < 0 ? Math.floor(deltaScrollLeft) : Math.ceil(deltaScrollLeft));
|
||||
this._horizontalScrollbar.writeScrollPosition(desiredScrollPosition, desiredScrollLeft);
|
||||
}
|
||||
// Check that we are scrolling towards a location which is valid
|
||||
desiredScrollPosition = this._scrollable.validateScrollPosition(desiredScrollPosition);
|
||||
if (this._options.inertialScroll && (deltaX || deltaY) && !classifier.isPhysicalMouseWheel()) {
|
||||
let startPeriodic = false;
|
||||
// Only start periodic if it's not running
|
||||
if (this._inertialSpeed.X === 0 && this._inertialSpeed.Y === 0) {
|
||||
startPeriodic = true;
|
||||
}
|
||||
this._inertialSpeed.Y = (deltaY < 0 ? -1 : 1) * (Math.abs(deltaY) ** 1.02);
|
||||
this._inertialSpeed.X = (deltaX < 0 ? -1 : 1) * (Math.abs(deltaX) ** 1.02);
|
||||
if (startPeriodic) {
|
||||
this._periodicSync();
|
||||
}
|
||||
}
|
||||
if (futureScrollPosition.scrollLeft !== desiredScrollPosition.scrollLeft || futureScrollPosition.scrollTop !== desiredScrollPosition.scrollTop) {
|
||||
const canPerformSmoothScroll = (this._options.mouseWheelSmoothScroll
|
||||
&& classifier.isPhysicalMouseWheel());
|
||||
if (canPerformSmoothScroll) {
|
||||
this._scrollable.setScrollPositionSmooth(desiredScrollPosition);
|
||||
}
|
||||
else {
|
||||
this._scrollable.setScrollPositionNow(desiredScrollPosition);
|
||||
}
|
||||
didScroll = true;
|
||||
}
|
||||
}
|
||||
let consumeMouseWheel = didScroll;
|
||||
if (!consumeMouseWheel && this._options.alwaysConsumeMouseWheel) {
|
||||
consumeMouseWheel = true;
|
||||
}
|
||||
if (!consumeMouseWheel && this._options.consumeMouseWheelIfScrollbarIsNeeded && (this._verticalScrollbar.isNeeded() || this._horizontalScrollbar.isNeeded())) {
|
||||
consumeMouseWheel = true;
|
||||
}
|
||||
if (consumeMouseWheel) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
}
|
||||
_onDidScroll(e) {
|
||||
this._shouldRender = this._horizontalScrollbar.onDidScroll(e) || this._shouldRender;
|
||||
this._shouldRender = this._verticalScrollbar.onDidScroll(e) || this._shouldRender;
|
||||
if (this._options.useShadows) {
|
||||
this._shouldRender = true;
|
||||
}
|
||||
if (this._revealOnScroll) {
|
||||
this._reveal();
|
||||
}
|
||||
if (!this._options.lazyRender) {
|
||||
this._render();
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Render / mutate the DOM now.
|
||||
* Should be used together with the ctor option `lazyRender`.
|
||||
*/
|
||||
renderNow() {
|
||||
if (!this._options.lazyRender) {
|
||||
throw new Error('Please use `lazyRender` together with `renderNow`!');
|
||||
}
|
||||
this._render();
|
||||
}
|
||||
_render() {
|
||||
if (!this._shouldRender) {
|
||||
return;
|
||||
}
|
||||
this._shouldRender = false;
|
||||
this._horizontalScrollbar.render();
|
||||
this._verticalScrollbar.render();
|
||||
if (this._options.useShadows) {
|
||||
const scrollState = this._scrollable.getCurrentScrollPosition();
|
||||
const enableTop = scrollState.scrollTop > 0;
|
||||
const enableLeft = scrollState.scrollLeft > 0;
|
||||
const leftClassName = (enableLeft ? ' left' : '');
|
||||
const topClassName = (enableTop ? ' top' : '');
|
||||
const topLeftClassName = (enableLeft || enableTop ? ' top-left-corner' : '');
|
||||
this._leftShadowDomNode.setClassName(`shadow${leftClassName}`);
|
||||
this._topShadowDomNode.setClassName(`shadow${topClassName}`);
|
||||
this._topLeftShadowDomNode.setClassName(`shadow${topLeftClassName}${topClassName}${leftClassName}`);
|
||||
}
|
||||
}
|
||||
// -------------------- fade in / fade out --------------------
|
||||
_onDragStart() {
|
||||
this._isDragging = true;
|
||||
this._reveal();
|
||||
}
|
||||
_onDragEnd() {
|
||||
this._isDragging = false;
|
||||
this._hide();
|
||||
}
|
||||
_onMouseLeave(e) {
|
||||
this._mouseIsOver = false;
|
||||
this._hide();
|
||||
}
|
||||
_onMouseOver(e) {
|
||||
this._mouseIsOver = true;
|
||||
this._reveal();
|
||||
}
|
||||
_reveal() {
|
||||
this._verticalScrollbar.beginReveal();
|
||||
this._horizontalScrollbar.beginReveal();
|
||||
this._scheduleHide();
|
||||
}
|
||||
_hide() {
|
||||
if (!this._mouseIsOver && !this._isDragging) {
|
||||
this._verticalScrollbar.beginHide();
|
||||
this._horizontalScrollbar.beginHide();
|
||||
}
|
||||
}
|
||||
_scheduleHide() {
|
||||
if (!this._mouseIsOver && !this._isDragging) {
|
||||
this._hideTimeout.cancelAndSet(() => this._hide(), HIDE_TIMEOUT);
|
||||
}
|
||||
}
|
||||
}
|
||||
class ScrollableElement extends AbstractScrollableElement {
|
||||
constructor(element, options) {
|
||||
options = options || {};
|
||||
options.mouseWheelSmoothScroll = false;
|
||||
const scrollable = new Scrollable({
|
||||
forceIntegerValues: true,
|
||||
smoothScrollDuration: 0,
|
||||
scheduleAtNextAnimationFrame: (callback) => scheduleAtNextAnimationFrame(getWindow(element), callback)
|
||||
});
|
||||
super(element, options, scrollable);
|
||||
this._register(scrollable);
|
||||
}
|
||||
setScrollPosition(update) {
|
||||
this._scrollable.setScrollPositionNow(update);
|
||||
}
|
||||
}
|
||||
class SmoothScrollableElement extends AbstractScrollableElement {
|
||||
constructor(element, options, scrollable) {
|
||||
super(element, options, scrollable);
|
||||
}
|
||||
setScrollPosition(update) {
|
||||
if (update.reuseAnimation) {
|
||||
this._scrollable.setScrollPositionSmooth(update, update.reuseAnimation);
|
||||
}
|
||||
else {
|
||||
this._scrollable.setScrollPositionNow(update);
|
||||
}
|
||||
}
|
||||
getScrollPosition() {
|
||||
return this._scrollable.getCurrentScrollPosition();
|
||||
}
|
||||
}
|
||||
class DomScrollableElement extends AbstractScrollableElement {
|
||||
constructor(element, options) {
|
||||
options = options || {};
|
||||
options.mouseWheelSmoothScroll = false;
|
||||
const scrollable = new Scrollable({
|
||||
forceIntegerValues: false, // See https://github.com/microsoft/vscode/issues/139877
|
||||
smoothScrollDuration: 0,
|
||||
scheduleAtNextAnimationFrame: (callback) => scheduleAtNextAnimationFrame(getWindow(element), callback)
|
||||
});
|
||||
super(element, options, scrollable);
|
||||
this._register(scrollable);
|
||||
this._element = element;
|
||||
this._register(this.onScroll((e) => {
|
||||
if (e.scrollTopChanged) {
|
||||
this._element.scrollTop = e.scrollTop;
|
||||
}
|
||||
if (e.scrollLeftChanged) {
|
||||
this._element.scrollLeft = e.scrollLeft;
|
||||
}
|
||||
}));
|
||||
this.scanDomNode();
|
||||
}
|
||||
setScrollPosition(update) {
|
||||
this._scrollable.setScrollPositionNow(update);
|
||||
}
|
||||
getScrollPosition() {
|
||||
return this._scrollable.getCurrentScrollPosition();
|
||||
}
|
||||
scanDomNode() {
|
||||
// width, scrollLeft, scrollWidth, height, scrollTop, scrollHeight
|
||||
this.setScrollDimensions({
|
||||
width: this._element.clientWidth,
|
||||
scrollWidth: this._element.scrollWidth,
|
||||
height: this._element.clientHeight,
|
||||
scrollHeight: this._element.scrollHeight
|
||||
});
|
||||
this.setScrollPosition({
|
||||
scrollLeft: this._element.scrollLeft,
|
||||
scrollTop: this._element.scrollTop,
|
||||
});
|
||||
}
|
||||
}
|
||||
function resolveOptions(opts) {
|
||||
const result = {
|
||||
lazyRender: (typeof opts.lazyRender !== 'undefined' ? opts.lazyRender : false),
|
||||
className: (typeof opts.className !== 'undefined' ? opts.className : ''),
|
||||
useShadows: (typeof opts.useShadows !== 'undefined' ? opts.useShadows : true),
|
||||
handleMouseWheel: (typeof opts.handleMouseWheel !== 'undefined' ? opts.handleMouseWheel : true),
|
||||
flipAxes: (typeof opts.flipAxes !== 'undefined' ? opts.flipAxes : false),
|
||||
consumeMouseWheelIfScrollbarIsNeeded: (typeof opts.consumeMouseWheelIfScrollbarIsNeeded !== 'undefined' ? opts.consumeMouseWheelIfScrollbarIsNeeded : false),
|
||||
alwaysConsumeMouseWheel: (typeof opts.alwaysConsumeMouseWheel !== 'undefined' ? opts.alwaysConsumeMouseWheel : false),
|
||||
scrollYToX: (typeof opts.scrollYToX !== 'undefined' ? opts.scrollYToX : false),
|
||||
mouseWheelScrollSensitivity: (typeof opts.mouseWheelScrollSensitivity !== 'undefined' ? opts.mouseWheelScrollSensitivity : 1),
|
||||
fastScrollSensitivity: (typeof opts.fastScrollSensitivity !== 'undefined' ? opts.fastScrollSensitivity : 5),
|
||||
scrollPredominantAxis: (typeof opts.scrollPredominantAxis !== 'undefined' ? opts.scrollPredominantAxis : true),
|
||||
mouseWheelSmoothScroll: (typeof opts.mouseWheelSmoothScroll !== 'undefined' ? opts.mouseWheelSmoothScroll : true),
|
||||
inertialScroll: (typeof opts.inertialScroll !== 'undefined' ? opts.inertialScroll : false),
|
||||
arrowSize: (typeof opts.arrowSize !== 'undefined' ? opts.arrowSize : 11),
|
||||
listenOnDomNode: (typeof opts.listenOnDomNode !== 'undefined' ? opts.listenOnDomNode : null),
|
||||
horizontal: (typeof opts.horizontal !== 'undefined' ? opts.horizontal : 1 /* ScrollbarVisibility.Auto */),
|
||||
horizontalScrollbarSize: (typeof opts.horizontalScrollbarSize !== 'undefined' ? opts.horizontalScrollbarSize : 10),
|
||||
horizontalSliderSize: (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : 0),
|
||||
horizontalHasArrows: (typeof opts.horizontalHasArrows !== 'undefined' ? opts.horizontalHasArrows : false),
|
||||
vertical: (typeof opts.vertical !== 'undefined' ? opts.vertical : 1 /* 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),
|
||||
scrollByPage: (typeof opts.scrollByPage !== 'undefined' ? opts.scrollByPage : false)
|
||||
};
|
||||
result.horizontalSliderSize = (typeof opts.horizontalSliderSize !== 'undefined' ? opts.horizontalSliderSize : result.horizontalScrollbarSize);
|
||||
result.verticalSliderSize = (typeof opts.verticalSliderSize !== 'undefined' ? opts.verticalSliderSize : result.verticalScrollbarSize);
|
||||
// Defaults are different on Macs
|
||||
if (isMacintosh) {
|
||||
result.className += ' mac';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export { AbstractScrollableElement, DomScrollableElement, MouseWheelClassifier, ScrollableElement, SmoothScrollableElement };
|
||||
Generated
Vendored
+78
@@ -0,0 +1,78 @@
|
||||
import { GlobalPointerMoveMonitor } from '../../globalPointerMoveMonitor.js';
|
||||
import { Widget } from '../widget.js';
|
||||
import { TimeoutTimer } from '../../../common/async.js';
|
||||
import { ThemeIcon } from '../../../common/themables.js';
|
||||
import { addStandardDisposableListener, EventType, WindowIntervalTimer, getWindow } from '../../dom.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
/**
|
||||
* The arrow image size.
|
||||
*/
|
||||
const ARROW_IMG_SIZE = 11;
|
||||
class ScrollbarArrow extends Widget {
|
||||
constructor(opts) {
|
||||
super();
|
||||
this._onActivate = opts.onActivate;
|
||||
this.bgDomNode = document.createElement('div');
|
||||
this.bgDomNode.className = 'arrow-background';
|
||||
this.bgDomNode.style.position = 'absolute';
|
||||
this.bgDomNode.style.width = opts.bgWidth + 'px';
|
||||
this.bgDomNode.style.height = opts.bgHeight + 'px';
|
||||
if (typeof opts.top !== 'undefined') {
|
||||
this.bgDomNode.style.top = '0px';
|
||||
}
|
||||
if (typeof opts.left !== 'undefined') {
|
||||
this.bgDomNode.style.left = '0px';
|
||||
}
|
||||
if (typeof opts.bottom !== 'undefined') {
|
||||
this.bgDomNode.style.bottom = '0px';
|
||||
}
|
||||
if (typeof opts.right !== 'undefined') {
|
||||
this.bgDomNode.style.right = '0px';
|
||||
}
|
||||
this.domNode = document.createElement('div');
|
||||
this.domNode.className = opts.className;
|
||||
this.domNode.classList.add(...ThemeIcon.asClassNameArray(opts.icon));
|
||||
this.domNode.style.position = 'absolute';
|
||||
this.domNode.style.width = ARROW_IMG_SIZE + 'px';
|
||||
this.domNode.style.height = ARROW_IMG_SIZE + 'px';
|
||||
if (typeof opts.top !== 'undefined') {
|
||||
this.domNode.style.top = opts.top + 'px';
|
||||
}
|
||||
if (typeof opts.left !== 'undefined') {
|
||||
this.domNode.style.left = opts.left + 'px';
|
||||
}
|
||||
if (typeof opts.bottom !== 'undefined') {
|
||||
this.domNode.style.bottom = opts.bottom + 'px';
|
||||
}
|
||||
if (typeof opts.right !== 'undefined') {
|
||||
this.domNode.style.right = opts.right + 'px';
|
||||
}
|
||||
this._pointerMoveMonitor = this._register(new GlobalPointerMoveMonitor());
|
||||
this._register(addStandardDisposableListener(this.bgDomNode, EventType.POINTER_DOWN, (e) => this._arrowPointerDown(e)));
|
||||
this._register(addStandardDisposableListener(this.domNode, EventType.POINTER_DOWN, (e) => this._arrowPointerDown(e)));
|
||||
this._pointerdownRepeatTimer = this._register(new WindowIntervalTimer());
|
||||
this._pointerdownScheduleRepeatTimer = this._register(new TimeoutTimer());
|
||||
}
|
||||
_arrowPointerDown(e) {
|
||||
if (!e.target || !(e.target instanceof Element)) {
|
||||
return;
|
||||
}
|
||||
const scheduleRepeater = () => {
|
||||
this._pointerdownRepeatTimer.cancelAndSet(() => this._onActivate(), 1000 / 24, getWindow(e));
|
||||
};
|
||||
this._onActivate();
|
||||
this._pointerdownRepeatTimer.cancel();
|
||||
this._pointerdownScheduleRepeatTimer.cancelAndSet(scheduleRepeater, 200);
|
||||
this._pointerMoveMonitor.startMonitoring(e.target, e.pointerId, e.buttons, (pointerMoveData) => { }, () => {
|
||||
this._pointerdownRepeatTimer.cancel();
|
||||
this._pointerdownScheduleRepeatTimer.cancel();
|
||||
});
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
export { ARROW_IMG_SIZE, ScrollbarArrow };
|
||||
Generated
Vendored
+163
@@ -0,0 +1,163 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
/**
|
||||
* The minimal size of the slider (such that it can still be clickable) -- it is artificially enlarged.
|
||||
*/
|
||||
const MINIMUM_SLIDER_SIZE = 20;
|
||||
class ScrollbarState {
|
||||
constructor(arrowSize, scrollbarSize, oppositeScrollbarSize, visibleSize, scrollSize, scrollPosition) {
|
||||
this._scrollbarSize = Math.round(scrollbarSize);
|
||||
this._oppositeScrollbarSize = Math.round(oppositeScrollbarSize);
|
||||
this._arrowSize = Math.round(arrowSize);
|
||||
this._visibleSize = visibleSize;
|
||||
this._scrollSize = scrollSize;
|
||||
this._scrollPosition = scrollPosition;
|
||||
this._computedAvailableSize = 0;
|
||||
this._computedIsNeeded = false;
|
||||
this._computedSliderSize = 0;
|
||||
this._computedSliderRatio = 0;
|
||||
this._computedSliderPosition = 0;
|
||||
this._refreshComputedValues();
|
||||
}
|
||||
clone() {
|
||||
return new ScrollbarState(this._arrowSize, this._scrollbarSize, this._oppositeScrollbarSize, this._visibleSize, this._scrollSize, this._scrollPosition);
|
||||
}
|
||||
setVisibleSize(visibleSize) {
|
||||
const iVisibleSize = Math.round(visibleSize);
|
||||
if (this._visibleSize !== iVisibleSize) {
|
||||
this._visibleSize = iVisibleSize;
|
||||
this._refreshComputedValues();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
setScrollSize(scrollSize) {
|
||||
const iScrollSize = Math.round(scrollSize);
|
||||
if (this._scrollSize !== iScrollSize) {
|
||||
this._scrollSize = iScrollSize;
|
||||
this._refreshComputedValues();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
setScrollPosition(scrollPosition) {
|
||||
const iScrollPosition = Math.round(scrollPosition);
|
||||
if (this._scrollPosition !== iScrollPosition) {
|
||||
this._scrollPosition = iScrollPosition;
|
||||
this._refreshComputedValues();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
setScrollbarSize(scrollbarSize) {
|
||||
this._scrollbarSize = Math.round(scrollbarSize);
|
||||
}
|
||||
setOppositeScrollbarSize(oppositeScrollbarSize) {
|
||||
this._oppositeScrollbarSize = Math.round(oppositeScrollbarSize);
|
||||
}
|
||||
static _computeValues(oppositeScrollbarSize, arrowSize, visibleSize, scrollSize, scrollPosition) {
|
||||
const computedAvailableSize = Math.max(0, visibleSize - oppositeScrollbarSize);
|
||||
const computedRepresentableSize = Math.max(0, computedAvailableSize - 2 * arrowSize);
|
||||
const computedIsNeeded = (scrollSize > 0 && scrollSize > visibleSize);
|
||||
if (!computedIsNeeded) {
|
||||
// There is no need for a slider
|
||||
return {
|
||||
computedAvailableSize: Math.round(computedAvailableSize),
|
||||
computedIsNeeded: computedIsNeeded,
|
||||
computedSliderSize: Math.round(computedRepresentableSize),
|
||||
computedSliderRatio: 0,
|
||||
computedSliderPosition: 0,
|
||||
};
|
||||
}
|
||||
// We must artificially increase the size of the slider if needed, since the slider would be too small to grab with the mouse otherwise
|
||||
const computedSliderSize = Math.round(Math.max(MINIMUM_SLIDER_SIZE, Math.floor(visibleSize * computedRepresentableSize / scrollSize)));
|
||||
// The slider can move from 0 to `computedRepresentableSize` - `computedSliderSize`
|
||||
// in the same way `scrollPosition` can move from 0 to `scrollSize` - `visibleSize`.
|
||||
const computedSliderRatio = (computedRepresentableSize - computedSliderSize) / (scrollSize - visibleSize);
|
||||
const computedSliderPosition = (scrollPosition * computedSliderRatio);
|
||||
return {
|
||||
computedAvailableSize: Math.round(computedAvailableSize),
|
||||
computedIsNeeded: computedIsNeeded,
|
||||
computedSliderSize: Math.round(computedSliderSize),
|
||||
computedSliderRatio: computedSliderRatio,
|
||||
computedSliderPosition: Math.round(computedSliderPosition),
|
||||
};
|
||||
}
|
||||
_refreshComputedValues() {
|
||||
const r = ScrollbarState._computeValues(this._oppositeScrollbarSize, this._arrowSize, this._visibleSize, this._scrollSize, this._scrollPosition);
|
||||
this._computedAvailableSize = r.computedAvailableSize;
|
||||
this._computedIsNeeded = r.computedIsNeeded;
|
||||
this._computedSliderSize = r.computedSliderSize;
|
||||
this._computedSliderRatio = r.computedSliderRatio;
|
||||
this._computedSliderPosition = r.computedSliderPosition;
|
||||
}
|
||||
getArrowSize() {
|
||||
return this._arrowSize;
|
||||
}
|
||||
getScrollPosition() {
|
||||
return this._scrollPosition;
|
||||
}
|
||||
getRectangleLargeSize() {
|
||||
return this._computedAvailableSize;
|
||||
}
|
||||
getRectangleSmallSize() {
|
||||
return this._scrollbarSize;
|
||||
}
|
||||
isNeeded() {
|
||||
return this._computedIsNeeded;
|
||||
}
|
||||
getSliderSize() {
|
||||
return this._computedSliderSize;
|
||||
}
|
||||
getSliderPosition() {
|
||||
return this._computedSliderPosition;
|
||||
}
|
||||
/**
|
||||
* Compute a desired `scrollPosition` such that `offset` ends up in the center of the slider.
|
||||
* `offset` is based on the same coordinate system as the `sliderPosition`.
|
||||
*/
|
||||
getDesiredScrollPositionFromOffset(offset) {
|
||||
if (!this._computedIsNeeded) {
|
||||
// no need for a slider
|
||||
return 0;
|
||||
}
|
||||
const desiredSliderPosition = offset - this._arrowSize - this._computedSliderSize / 2;
|
||||
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.
|
||||
*/
|
||||
getDesiredScrollPositionFromOffsetPaged(offset) {
|
||||
if (!this._computedIsNeeded) {
|
||||
// no need for a slider
|
||||
return 0;
|
||||
}
|
||||
const 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`.
|
||||
*/
|
||||
getDesiredScrollPositionFromDelta(delta) {
|
||||
if (!this._computedIsNeeded) {
|
||||
// no need for a slider
|
||||
return 0;
|
||||
}
|
||||
const desiredSliderPosition = this._computedSliderPosition + delta;
|
||||
return Math.round(desiredSliderPosition / this._computedSliderRatio);
|
||||
}
|
||||
}
|
||||
|
||||
export { ScrollbarState };
|
||||
Generated
Vendored
+93
@@ -0,0 +1,93 @@
|
||||
import { TimeoutTimer } from '../../../common/async.js';
|
||||
import { Disposable } from '../../../common/lifecycle.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class ScrollbarVisibilityController extends Disposable {
|
||||
constructor(visibility, visibleClassName, invisibleClassName) {
|
||||
super();
|
||||
this._visibility = visibility;
|
||||
this._visibleClassName = visibleClassName;
|
||||
this._invisibleClassName = invisibleClassName;
|
||||
this._domNode = null;
|
||||
this._isVisible = false;
|
||||
this._isNeeded = false;
|
||||
this._rawShouldBeVisible = false;
|
||||
this._shouldBeVisible = false;
|
||||
this._revealTimer = this._register(new TimeoutTimer());
|
||||
}
|
||||
setVisibility(visibility) {
|
||||
if (this._visibility !== visibility) {
|
||||
this._visibility = visibility;
|
||||
this._updateShouldBeVisible();
|
||||
}
|
||||
}
|
||||
// ----------------- Hide / Reveal
|
||||
setShouldBeVisible(rawShouldBeVisible) {
|
||||
this._rawShouldBeVisible = rawShouldBeVisible;
|
||||
this._updateShouldBeVisible();
|
||||
}
|
||||
_applyVisibilitySetting() {
|
||||
if (this._visibility === 2 /* ScrollbarVisibility.Hidden */) {
|
||||
return false;
|
||||
}
|
||||
if (this._visibility === 3 /* ScrollbarVisibility.Visible */) {
|
||||
return true;
|
||||
}
|
||||
return this._rawShouldBeVisible;
|
||||
}
|
||||
_updateShouldBeVisible() {
|
||||
const shouldBeVisible = this._applyVisibilitySetting();
|
||||
if (this._shouldBeVisible !== shouldBeVisible) {
|
||||
this._shouldBeVisible = shouldBeVisible;
|
||||
this.ensureVisibility();
|
||||
}
|
||||
}
|
||||
setIsNeeded(isNeeded) {
|
||||
if (this._isNeeded !== isNeeded) {
|
||||
this._isNeeded = isNeeded;
|
||||
this.ensureVisibility();
|
||||
}
|
||||
}
|
||||
setDomNode(domNode) {
|
||||
this._domNode = domNode;
|
||||
this._domNode.setClassName(this._invisibleClassName);
|
||||
// Now that the flags & the dom node are in a consistent state, ensure the Hidden/Visible configuration
|
||||
this.setShouldBeVisible(false);
|
||||
}
|
||||
ensureVisibility() {
|
||||
if (!this._isNeeded) {
|
||||
// Nothing to be rendered
|
||||
this._hide(false);
|
||||
return;
|
||||
}
|
||||
if (this._shouldBeVisible) {
|
||||
this._reveal();
|
||||
}
|
||||
else {
|
||||
this._hide(true);
|
||||
}
|
||||
}
|
||||
_reveal() {
|
||||
if (this._isVisible) {
|
||||
return;
|
||||
}
|
||||
this._isVisible = true;
|
||||
// The CSS animation doesn't play otherwise
|
||||
this._revealTimer.setIfNotSet(() => {
|
||||
this._domNode?.setClassName(this._visibleClassName);
|
||||
}, 0);
|
||||
}
|
||||
_hide(withFadeAway) {
|
||||
this._revealTimer.cancel();
|
||||
if (!this._isVisible) {
|
||||
return;
|
||||
}
|
||||
this._isVisible = false;
|
||||
this._domNode?.setClassName(this._invisibleClassName + (withFadeAway ? ' fade' : ''));
|
||||
}
|
||||
}
|
||||
|
||||
export { ScrollbarVisibilityController };
|
||||
Generated
Vendored
+94
@@ -0,0 +1,94 @@
|
||||
import { StandardWheelEvent } from '../../mouseEvent.js';
|
||||
import { AbstractScrollbar } from './abstractScrollbar.js';
|
||||
import { ARROW_IMG_SIZE } from './scrollbarArrow.js';
|
||||
import { ScrollbarState } from './scrollbarState.js';
|
||||
import { Codicon } from '../../../common/codicons.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class VerticalScrollbar extends AbstractScrollbar {
|
||||
constructor(scrollable, options, host) {
|
||||
const scrollDimensions = scrollable.getScrollDimensions();
|
||||
const scrollPosition = scrollable.getCurrentScrollPosition();
|
||||
super({
|
||||
lazyRender: options.lazyRender,
|
||||
host: host,
|
||||
scrollbarState: new ScrollbarState((options.verticalHasArrows ? options.arrowSize : 0), (options.vertical === 2 /* ScrollbarVisibility.Hidden */ ? 0 : options.verticalScrollbarSize),
|
||||
// give priority to vertical scroll bar over horizontal and let it scroll all the way to the bottom
|
||||
0, scrollDimensions.height, scrollDimensions.scrollHeight, scrollPosition.scrollTop),
|
||||
visibility: options.vertical,
|
||||
extraScrollbarClassName: 'vertical',
|
||||
scrollable: scrollable,
|
||||
scrollByPage: options.scrollByPage
|
||||
});
|
||||
if (options.verticalHasArrows) {
|
||||
const arrowDelta = (options.arrowSize - ARROW_IMG_SIZE) / 2;
|
||||
const scrollbarDelta = (options.verticalScrollbarSize - ARROW_IMG_SIZE) / 2;
|
||||
this._createArrow({
|
||||
className: 'scra',
|
||||
icon: Codicon.scrollbarButtonUp,
|
||||
top: arrowDelta,
|
||||
left: scrollbarDelta,
|
||||
bottom: undefined,
|
||||
right: undefined,
|
||||
bgWidth: options.verticalScrollbarSize,
|
||||
bgHeight: options.arrowSize,
|
||||
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 0, 1)),
|
||||
});
|
||||
this._createArrow({
|
||||
className: 'scra',
|
||||
icon: Codicon.scrollbarButtonDown,
|
||||
top: undefined,
|
||||
left: scrollbarDelta,
|
||||
bottom: arrowDelta,
|
||||
right: undefined,
|
||||
bgWidth: options.verticalScrollbarSize,
|
||||
bgHeight: options.arrowSize,
|
||||
onActivate: () => this._host.onMouseWheel(new StandardWheelEvent(null, 0, -1)),
|
||||
});
|
||||
}
|
||||
this._createSlider(0, Math.floor((options.verticalScrollbarSize - options.verticalSliderSize) / 2), options.verticalSliderSize, undefined);
|
||||
}
|
||||
_updateSlider(sliderSize, sliderPosition) {
|
||||
this.slider.setHeight(sliderSize);
|
||||
this.slider.setTop(sliderPosition);
|
||||
}
|
||||
_renderDomNode(largeSize, smallSize) {
|
||||
this.domNode.setWidth(smallSize);
|
||||
this.domNode.setHeight(largeSize);
|
||||
this.domNode.setRight(0);
|
||||
this.domNode.setTop(0);
|
||||
}
|
||||
onDidScroll(e) {
|
||||
this._shouldRender = this._onElementScrollSize(e.scrollHeight) || this._shouldRender;
|
||||
this._shouldRender = this._onElementScrollPosition(e.scrollTop) || this._shouldRender;
|
||||
this._shouldRender = this._onElementSize(e.height) || this._shouldRender;
|
||||
return this._shouldRender;
|
||||
}
|
||||
_pointerDownRelativePosition(offsetX, offsetY) {
|
||||
return offsetY;
|
||||
}
|
||||
_sliderPointerPosition(e) {
|
||||
return e.pageY;
|
||||
}
|
||||
_sliderOrthogonalPointerPosition(e) {
|
||||
return e.pageX;
|
||||
}
|
||||
_updateScrollbarSize(size) {
|
||||
this.slider.setWidth(size);
|
||||
}
|
||||
writeScrollPosition(target, scrollPosition) {
|
||||
target.scrollTop = scrollPosition;
|
||||
}
|
||||
updateOptions(options) {
|
||||
this.updateScrollbarSize(options.vertical === 2 /* ScrollbarVisibility.Hidden */ ? 0 : options.verticalScrollbarSize);
|
||||
// give priority to vertical scroll bar over horizontal and let it scroll all the way to the bottom
|
||||
this._scrollbarState.setOppositeScrollbarSize(0);
|
||||
this._visibilityController.setVisibility(options.vertical);
|
||||
this._scrollByPage = options.scrollByPage;
|
||||
}
|
||||
}
|
||||
|
||||
export { VerticalScrollbar };
|
||||
Generated
Vendored
+35
@@ -0,0 +1,35 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-select-box {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.monaco-select-box-dropdown-container {
|
||||
font-size: 13px;
|
||||
font-weight: normal;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/** Actions */
|
||||
|
||||
.monaco-action-bar .action-item.select-container {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.monaco-action-bar .action-item .monaco-select-box {
|
||||
cursor: pointer;
|
||||
min-width: 100px;
|
||||
min-height: 18px;
|
||||
padding: 2px 23px 2px 8px;
|
||||
}
|
||||
|
||||
.mac .monaco-action-bar .action-item .monaco-select-box {
|
||||
font-size: 11px;
|
||||
border-radius: 3px;
|
||||
min-height: 24px;
|
||||
}
|
||||
Generated
Vendored
+47
@@ -0,0 +1,47 @@
|
||||
import { isMacintosh } from '../../../common/platform.js';
|
||||
import { Widget } from '../widget.js';
|
||||
import './selectBox.css';
|
||||
import { SelectBoxList } from './selectBoxCustom.js';
|
||||
import { SelectBoxNative } from './selectBoxNative.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class SelectBox extends Widget {
|
||||
constructor(options, selected, contextViewProvider, styles, selectBoxOptions) {
|
||||
super();
|
||||
// Default to native SelectBox for OSX unless overridden
|
||||
if (isMacintosh && !selectBoxOptions?.useCustomDrawn) {
|
||||
this.selectBoxDelegate = new SelectBoxNative(options, selected, styles, selectBoxOptions);
|
||||
}
|
||||
else {
|
||||
this.selectBoxDelegate = new SelectBoxList(options, selected, contextViewProvider, styles, selectBoxOptions);
|
||||
}
|
||||
this._register(this.selectBoxDelegate);
|
||||
}
|
||||
// Public SelectBox Methods - routed through delegate interface
|
||||
get onDidSelect() {
|
||||
return this.selectBoxDelegate.onDidSelect;
|
||||
}
|
||||
setOptions(options, selected) {
|
||||
this.selectBoxDelegate.setOptions(options, selected);
|
||||
}
|
||||
select(index) {
|
||||
this.selectBoxDelegate.select(index);
|
||||
}
|
||||
focus() {
|
||||
this.selectBoxDelegate.focus();
|
||||
}
|
||||
blur() {
|
||||
this.selectBoxDelegate.blur();
|
||||
}
|
||||
setFocusable(focusable) {
|
||||
this.selectBoxDelegate.setFocusable(focusable);
|
||||
}
|
||||
render(container) {
|
||||
this.selectBoxDelegate.render(container);
|
||||
}
|
||||
}
|
||||
|
||||
export { SelectBox };
|
||||
Generated
Vendored
+126
@@ -0,0 +1,126 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/* Use custom CSS vars to expose padding into parent select for padding calculation */
|
||||
.monaco-select-box-dropdown-padding {
|
||||
--dropdown-padding-top: 1px;
|
||||
--dropdown-padding-bottom: 1px;
|
||||
}
|
||||
|
||||
.hc-black .monaco-select-box-dropdown-padding,
|
||||
.hc-light .monaco-select-box-dropdown-padding {
|
||||
--dropdown-padding-top: 3px;
|
||||
--dropdown-padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.monaco-select-box-dropdown-container {
|
||||
display: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown * {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown a:focus {
|
||||
outline: 1px solid -webkit-focus-ring-color;
|
||||
outline-offset: -1px;
|
||||
}
|
||||
|
||||
.monaco-select-box-dropdown-container > .select-box-details-pane > .select-box-description-markdown code {
|
||||
line-height: 15px; /** For some reason, this is needed, otherwise <code> will take up 20px height */
|
||||
font-family: var(--monaco-monospace-font);
|
||||
}
|
||||
|
||||
|
||||
.monaco-select-box-dropdown-container.visible {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
width: 1px;
|
||||
overflow: hidden;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
}
|
||||
|
||||
.monaco-select-box-dropdown-container > .select-box-dropdown-list-container {
|
||||
flex: 0 0 auto;
|
||||
align-self: flex-start;
|
||||
padding-top: var(--dropdown-padding-top);
|
||||
padding-bottom: var(--dropdown-padding-bottom);
|
||||
padding-left: 1px;
|
||||
padding-right: 1px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.monaco-select-box-dropdown-container > .select-box-details-pane {
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.hc-black .monaco-select-box-dropdown-container > .select-box-dropdown-list-container {
|
||||
padding-top: var(--dropdown-padding-top);
|
||||
padding-bottom: var(--dropdown-padding-bottom);
|
||||
}
|
||||
|
||||
.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row > .option-text {
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
padding-left: 3.5px;
|
||||
white-space: nowrap;
|
||||
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;
|
||||
padding-right: 10px;
|
||||
white-space: nowrap;
|
||||
float: right;
|
||||
}
|
||||
|
||||
|
||||
/* Accepted CSS hiding technique for accessibility reader text */
|
||||
/* https://webaim.org/techniques/css/invisiblecontent/ */
|
||||
|
||||
.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row > .visually-hidden {
|
||||
position: absolute;
|
||||
left: -10000px;
|
||||
top: auto;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-select-box-dropdown-container > .select-box-dropdown-container-width-control {
|
||||
flex: 1 1 auto;
|
||||
align-self: flex-start;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.monaco-select-box-dropdown-container > .select-box-dropdown-container-width-control > .width-control-div {
|
||||
overflow: hidden;
|
||||
max-height: 0px;
|
||||
}
|
||||
|
||||
.monaco-select-box-dropdown-container > .select-box-dropdown-container-width-control > .width-control-div > .option-text-width-control {
|
||||
padding-left: 4px;
|
||||
padding-right: 8px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
Generated
Vendored
+858
@@ -0,0 +1,858 @@
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { equals } from '../../../common/arrays.js';
|
||||
import { Emitter, Event } from '../../../common/event.js';
|
||||
import { KeyCodeUtils } from '../../../common/keyCodes.js';
|
||||
import { Disposable, DisposableStore } from '../../../common/lifecycle.js';
|
||||
import { isMacintosh } from '../../../common/platform.js';
|
||||
import { asCssValueWithDefault } from '../../cssValue.js';
|
||||
import { $ as $$1, append, addDisposableListener, EventHelper, EventType, addStandardDisposableListener, getWindow, getDomNodePagePosition, getTotalWidth, isAncestor } from '../../dom.js';
|
||||
import { createStyleSheet } from '../../domStylesheets.js';
|
||||
import { DomEmitter } from '../../event.js';
|
||||
import { StandardKeyboardEvent } from '../../keyboardEvent.js';
|
||||
import { renderMarkdown } from '../../markdownRenderer.js';
|
||||
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
|
||||
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
|
||||
import { List } from '../list/listWidget.js';
|
||||
import './selectBoxCustom.css';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const $ = $$1;
|
||||
const SELECT_OPTION_ENTRY_TEMPLATE_ID = 'selectOption.entry.template';
|
||||
class SelectListRenderer {
|
||||
get templateId() { return SELECT_OPTION_ENTRY_TEMPLATE_ID; }
|
||||
renderTemplate(container) {
|
||||
const data = Object.create(null);
|
||||
data.root = container;
|
||||
data.text = append(container, $('.option-text'));
|
||||
data.detail = append(container, $('.option-detail'));
|
||||
data.decoratorRight = append(container, $('.option-decorator-right'));
|
||||
return data;
|
||||
}
|
||||
renderElement(element, index, templateData) {
|
||||
const data = templateData;
|
||||
const text = element.text;
|
||||
const detail = element.detail;
|
||||
const decoratorRight = element.decoratorRight;
|
||||
const isDisabled = element.isDisabled;
|
||||
data.text.textContent = text;
|
||||
data.detail.textContent = !!detail ? detail : '';
|
||||
data.decoratorRight.textContent = !!decoratorRight ? decoratorRight : '';
|
||||
// pseudo-select disabled option
|
||||
if (isDisabled) {
|
||||
data.root.classList.add('option-disabled');
|
||||
}
|
||||
else {
|
||||
// Make sure we do class removal from prior template rendering
|
||||
data.root.classList.remove('option-disabled');
|
||||
}
|
||||
}
|
||||
disposeTemplate(_templateData) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
class SelectBoxList extends Disposable {
|
||||
static { this.DEFAULT_DROPDOWN_MINIMUM_BOTTOM_MARGIN = 32; }
|
||||
static { this.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN = 2; }
|
||||
static { this.DEFAULT_MINIMUM_VISIBLE_OPTIONS = 3; }
|
||||
constructor(options, selected, contextViewProvider, styles, selectBoxOptions) {
|
||||
super();
|
||||
this.options = [];
|
||||
this._currentSelection = 0;
|
||||
this._hasDetails = false;
|
||||
this._selectionDetailsDisposables = this._register(new DisposableStore());
|
||||
this._skipLayout = false;
|
||||
this._sticky = false; // for dev purposes only
|
||||
this._isVisible = false;
|
||||
this.styles = styles;
|
||||
this.selectBoxOptions = selectBoxOptions || Object.create(null);
|
||||
if (typeof this.selectBoxOptions.minBottomMargin !== 'number') {
|
||||
this.selectBoxOptions.minBottomMargin = SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_BOTTOM_MARGIN;
|
||||
}
|
||||
else if (this.selectBoxOptions.minBottomMargin < 0) {
|
||||
this.selectBoxOptions.minBottomMargin = 0;
|
||||
}
|
||||
this.selectElement = document.createElement('select');
|
||||
// Use custom CSS vars for padding calculation
|
||||
this.selectElement.className = 'monaco-select-box monaco-select-box-dropdown-padding';
|
||||
if (typeof this.selectBoxOptions.ariaLabel === 'string') {
|
||||
this.selectElement.setAttribute('aria-label', this.selectBoxOptions.ariaLabel);
|
||||
}
|
||||
if (typeof this.selectBoxOptions.ariaDescription === 'string') {
|
||||
this.selectElement.setAttribute('aria-description', this.selectBoxOptions.ariaDescription);
|
||||
}
|
||||
this._onDidSelect = new Emitter();
|
||||
this._register(this._onDidSelect);
|
||||
this.registerListeners();
|
||||
this.constructSelectDropDown(contextViewProvider);
|
||||
this.selected = selected || 0;
|
||||
if (options) {
|
||||
this.setOptions(options, selected);
|
||||
}
|
||||
this.initStyleSheet();
|
||||
}
|
||||
setTitle(title) {
|
||||
if (!this._hover && title) {
|
||||
this._hover = this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.selectElement, title));
|
||||
}
|
||||
else if (this._hover) {
|
||||
this._hover.update(title);
|
||||
}
|
||||
}
|
||||
// IDelegate - List renderer
|
||||
getHeight() {
|
||||
return 22;
|
||||
}
|
||||
getTemplateId() {
|
||||
return SELECT_OPTION_ENTRY_TEMPLATE_ID;
|
||||
}
|
||||
constructSelectDropDown(contextViewProvider) {
|
||||
// SetUp ContextView container to hold select Dropdown
|
||||
this.contextViewProvider = contextViewProvider;
|
||||
this.selectDropDownContainer = $$1('.monaco-select-box-dropdown-container');
|
||||
// Use custom CSS vars for padding calculation (shared with parent select)
|
||||
this.selectDropDownContainer.classList.add('monaco-select-box-dropdown-padding');
|
||||
// Setup container for select option details
|
||||
this.selectionDetailsPane = append(this.selectDropDownContainer, $('.select-box-details-pane'));
|
||||
// Create span flex box item/div we can measure and control
|
||||
const widthControlOuterDiv = append(this.selectDropDownContainer, $('.select-box-dropdown-container-width-control'));
|
||||
const widthControlInnerDiv = append(widthControlOuterDiv, $('.width-control-div'));
|
||||
this.widthControlElement = document.createElement('span');
|
||||
this.widthControlElement.className = 'option-text-width-control';
|
||||
append(widthControlInnerDiv, this.widthControlElement);
|
||||
// Always default to below position
|
||||
this._dropDownPosition = 0 /* AnchorPosition.BELOW */;
|
||||
// Inline stylesheet for themes
|
||||
this.styleElement = createStyleSheet(this.selectDropDownContainer);
|
||||
// Prevent dragging of dropdown #114329
|
||||
this.selectDropDownContainer.setAttribute('draggable', 'true');
|
||||
this._register(addDisposableListener(this.selectDropDownContainer, EventType.DRAG_START, (e) => {
|
||||
EventHelper.stop(e, true);
|
||||
}));
|
||||
}
|
||||
registerListeners() {
|
||||
// Parent native select keyboard listeners
|
||||
this._register(addStandardDisposableListener(this.selectElement, 'change', (e) => {
|
||||
this.selected = e.target.selectedIndex;
|
||||
this._onDidSelect.fire({
|
||||
index: e.target.selectedIndex,
|
||||
selected: e.target.value
|
||||
});
|
||||
if (!!this.options[this.selected] && !!this.options[this.selected].text) {
|
||||
this.setTitle(this.options[this.selected].text);
|
||||
}
|
||||
}));
|
||||
// Have to implement both keyboard and mouse controllers to handle disabled options
|
||||
// Intercept mouse events to override normal select actions on parents
|
||||
this._register(addDisposableListener(this.selectElement, EventType.CLICK, (e) => {
|
||||
EventHelper.stop(e);
|
||||
if (this._isVisible) {
|
||||
this.hideSelectDropDown(true);
|
||||
}
|
||||
else {
|
||||
this.showSelectDropDown();
|
||||
}
|
||||
}));
|
||||
this._register(addDisposableListener(this.selectElement, EventType.MOUSE_DOWN, (e) => {
|
||||
EventHelper.stop(e);
|
||||
}));
|
||||
// Intercept touch events
|
||||
// The following implementation is slightly different from the mouse event handlers above.
|
||||
// Use the following helper variable, otherwise the list flickers.
|
||||
let listIsVisibleOnTouchStart;
|
||||
this._register(addDisposableListener(this.selectElement, 'touchstart', (e) => {
|
||||
listIsVisibleOnTouchStart = this._isVisible;
|
||||
}));
|
||||
this._register(addDisposableListener(this.selectElement, 'touchend', (e) => {
|
||||
EventHelper.stop(e);
|
||||
if (listIsVisibleOnTouchStart) {
|
||||
this.hideSelectDropDown(true);
|
||||
}
|
||||
else {
|
||||
this.showSelectDropDown();
|
||||
}
|
||||
}));
|
||||
// Intercept keyboard handling
|
||||
this._register(addDisposableListener(this.selectElement, EventType.KEY_DOWN, (e) => {
|
||||
const event = new StandardKeyboardEvent(e);
|
||||
let showDropDown = false;
|
||||
// Create and drop down select list on keyboard select
|
||||
if (isMacintosh) {
|
||||
if (event.keyCode === 18 /* KeyCode.DownArrow */ || event.keyCode === 16 /* KeyCode.UpArrow */ || event.keyCode === 10 /* KeyCode.Space */ || event.keyCode === 3 /* KeyCode.Enter */) {
|
||||
showDropDown = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (event.keyCode === 18 /* KeyCode.DownArrow */ && event.altKey || event.keyCode === 16 /* KeyCode.UpArrow */ && event.altKey || event.keyCode === 10 /* KeyCode.Space */ || event.keyCode === 3 /* KeyCode.Enter */) {
|
||||
showDropDown = true;
|
||||
}
|
||||
}
|
||||
if (showDropDown) {
|
||||
this.showSelectDropDown();
|
||||
EventHelper.stop(e, true);
|
||||
}
|
||||
}));
|
||||
}
|
||||
get onDidSelect() {
|
||||
return this._onDidSelect.event;
|
||||
}
|
||||
setOptions(options, selected) {
|
||||
if (!equals(this.options, options)) {
|
||||
this.options = options;
|
||||
this.selectElement.options.length = 0;
|
||||
this._hasDetails = false;
|
||||
this._cachedMaxDetailsHeight = undefined;
|
||||
this.options.forEach((option, index) => {
|
||||
this.selectElement.add(this.createOption(option.text, index, option.isDisabled));
|
||||
if (typeof option.description === 'string') {
|
||||
this._hasDetails = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
if (selected !== undefined) {
|
||||
this.select(selected);
|
||||
// Set current = selected since this is not necessarily a user exit
|
||||
this._currentSelection = this.selected;
|
||||
}
|
||||
}
|
||||
setOptionsList() {
|
||||
// Mirror options in drop-down
|
||||
// Populate select list for non-native select mode
|
||||
this.selectList?.splice(0, this.selectList.length, this.options);
|
||||
}
|
||||
select(index) {
|
||||
if (index >= 0 && index < this.options.length) {
|
||||
this.selected = index;
|
||||
}
|
||||
else if (index > this.options.length - 1) {
|
||||
// Adjust index to end of list
|
||||
// This could make client out of sync with the select
|
||||
this.select(this.options.length - 1);
|
||||
}
|
||||
else if (this.selected < 0) {
|
||||
this.selected = 0;
|
||||
}
|
||||
this.selectElement.selectedIndex = this.selected;
|
||||
if (!!this.options[this.selected] && !!this.options[this.selected].text) {
|
||||
this.setTitle(this.options[this.selected].text);
|
||||
}
|
||||
}
|
||||
focus() {
|
||||
if (this.selectElement) {
|
||||
this.selectElement.tabIndex = 0;
|
||||
this.selectElement.focus();
|
||||
}
|
||||
}
|
||||
blur() {
|
||||
if (this.selectElement) {
|
||||
this.selectElement.tabIndex = -1;
|
||||
this.selectElement.blur();
|
||||
}
|
||||
}
|
||||
setFocusable(focusable) {
|
||||
this.selectElement.tabIndex = focusable ? 0 : -1;
|
||||
}
|
||||
render(container) {
|
||||
this.container = container;
|
||||
container.classList.add('select-container');
|
||||
container.appendChild(this.selectElement);
|
||||
this.styleSelectElement();
|
||||
}
|
||||
initStyleSheet() {
|
||||
const content = [];
|
||||
// Style non-native select mode
|
||||
if (this.styles.listFocusBackground) {
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { background-color: ${this.styles.listFocusBackground} !important; }`);
|
||||
}
|
||||
if (this.styles.listFocusForeground) {
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { color: ${this.styles.listFocusForeground} !important; }`);
|
||||
}
|
||||
if (this.styles.decoratorRightForeground) {
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:not(.focused) .option-decorator-right { color: ${this.styles.decoratorRightForeground}; }`);
|
||||
}
|
||||
if (this.styles.selectBackground && this.styles.selectBorder && this.styles.selectBorder !== this.styles.selectBackground) {
|
||||
content.push(`.monaco-select-box-dropdown-container { border: 1px solid ${this.styles.selectBorder} } `);
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-top { border-top: 1px solid ${this.styles.selectBorder} } `);
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-bottom { border-bottom: 1px solid ${this.styles.selectBorder} } `);
|
||||
}
|
||||
else if (this.styles.selectListBorder) {
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-top { border-top: 1px solid ${this.styles.selectListBorder} } `);
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-details-pane.border-bottom { border-bottom: 1px solid ${this.styles.selectListBorder} } `);
|
||||
}
|
||||
// Hover foreground - ignore for disabled options
|
||||
if (this.styles.listHoverForeground) {
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:not(.option-disabled):not(.focused):hover { color: ${this.styles.listHoverForeground} !important; }`);
|
||||
}
|
||||
// Hover background - ignore for disabled options
|
||||
if (this.styles.listHoverBackground) {
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:not(.option-disabled):not(.focused):hover { background-color: ${this.styles.listHoverBackground} !important; }`);
|
||||
}
|
||||
// Match quick input outline styles - ignore for disabled options
|
||||
if (this.styles.listFocusOutline) {
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.focused { outline: 1.6px dotted ${this.styles.listFocusOutline} !important; outline-offset: -1.6px !important; }`);
|
||||
}
|
||||
if (this.styles.listHoverOutline) {
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row:not(.option-disabled):not(.focused):hover { outline: 1.6px dashed ${this.styles.listHoverOutline} !important; outline-offset: -1.6px !important; }`);
|
||||
}
|
||||
// Clear list styles on focus and on hover for disabled options
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.option-disabled.focused { background-color: transparent !important; color: inherit !important; outline: none !important; }`);
|
||||
content.push(`.monaco-select-box-dropdown-container > .select-box-dropdown-list-container .monaco-list .monaco-list-row.option-disabled:hover { background-color: transparent !important; color: inherit !important; outline: none !important; }`);
|
||||
this.styleElement.textContent = content.join('\n');
|
||||
}
|
||||
styleSelectElement() {
|
||||
const background = this.styles.selectBackground ?? '';
|
||||
const foreground = this.styles.selectForeground ?? '';
|
||||
const border = this.styles.selectBorder ?? '';
|
||||
this.selectElement.style.backgroundColor = background;
|
||||
this.selectElement.style.color = foreground;
|
||||
this.selectElement.style.borderColor = border;
|
||||
}
|
||||
styleList() {
|
||||
const background = this.styles.selectBackground ?? '';
|
||||
const listBackground = asCssValueWithDefault(this.styles.selectListBackground, background);
|
||||
this.selectDropDownListContainer.style.backgroundColor = listBackground;
|
||||
this.selectionDetailsPane.style.backgroundColor = listBackground;
|
||||
const optionsBorder = this.styles.focusBorder ?? '';
|
||||
this.selectDropDownContainer.style.outlineColor = optionsBorder;
|
||||
this.selectDropDownContainer.style.outlineOffset = '-1px';
|
||||
this.selectList.style(this.styles);
|
||||
}
|
||||
createOption(value, index, disabled) {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.text = value;
|
||||
option.disabled = !!disabled;
|
||||
return option;
|
||||
}
|
||||
// ContextView dropdown methods
|
||||
showSelectDropDown() {
|
||||
this.selectionDetailsPane.textContent = '';
|
||||
if (!this.contextViewProvider || this._isVisible) {
|
||||
return;
|
||||
}
|
||||
// Lazily create and populate list only at open, moved from constructor
|
||||
this.createSelectList(this.selectDropDownContainer);
|
||||
this.setOptionsList();
|
||||
// This allows us to flip the position based on measurement
|
||||
// Set drop-down position above/below from required height and margins
|
||||
// If pre-layout cannot fit at least one option do not show drop-down
|
||||
this.contextViewProvider.showContextView({
|
||||
getAnchor: () => this.selectElement,
|
||||
render: (container) => this.renderSelectDropDown(container, true),
|
||||
layout: () => {
|
||||
this.layoutSelectDropDown();
|
||||
},
|
||||
onHide: () => {
|
||||
this.selectDropDownContainer.classList.remove('visible');
|
||||
this.selectElement.classList.remove('synthetic-focus');
|
||||
},
|
||||
anchorPosition: this._dropDownPosition
|
||||
}, this.selectBoxOptions.optionsAsChildren ? this.container : undefined);
|
||||
// Hide so we can relay out
|
||||
this._isVisible = true;
|
||||
this.hideSelectDropDown(false);
|
||||
this.contextViewProvider.showContextView({
|
||||
getAnchor: () => this.selectElement,
|
||||
render: (container) => this.renderSelectDropDown(container),
|
||||
layout: () => this.layoutSelectDropDown(),
|
||||
onHide: () => {
|
||||
this.selectDropDownContainer.classList.remove('visible');
|
||||
this.selectElement.classList.remove('synthetic-focus');
|
||||
},
|
||||
anchorPosition: this._dropDownPosition
|
||||
}, this.selectBoxOptions.optionsAsChildren ? this.container : undefined);
|
||||
// Track initial selection the case user escape, blur
|
||||
this._currentSelection = this.selected;
|
||||
this._isVisible = true;
|
||||
this.selectElement.setAttribute('aria-expanded', 'true');
|
||||
}
|
||||
hideSelectDropDown(focusSelect) {
|
||||
if (!this.contextViewProvider || !this._isVisible) {
|
||||
return;
|
||||
}
|
||||
this._isVisible = false;
|
||||
this.selectElement.setAttribute('aria-expanded', 'false');
|
||||
if (focusSelect) {
|
||||
this.selectElement.focus();
|
||||
}
|
||||
this.contextViewProvider.hideContextView();
|
||||
}
|
||||
renderSelectDropDown(container, preLayoutPosition) {
|
||||
container.appendChild(this.selectDropDownContainer);
|
||||
// Pre-Layout allows us to change position
|
||||
this.layoutSelectDropDown(preLayoutPosition);
|
||||
return {
|
||||
dispose: () => {
|
||||
// contextView will dispose itself if moving from one View to another
|
||||
this.selectDropDownContainer.remove(); // remove to take out the CSS rules we add
|
||||
}
|
||||
};
|
||||
}
|
||||
// Iterate over detailed descriptions, find max height
|
||||
measureMaxDetailsHeight() {
|
||||
let maxDetailsPaneHeight = 0;
|
||||
this.options.forEach((_option, index) => {
|
||||
this.updateDetail(index);
|
||||
if (this.selectionDetailsPane.offsetHeight > maxDetailsPaneHeight) {
|
||||
maxDetailsPaneHeight = this.selectionDetailsPane.offsetHeight;
|
||||
}
|
||||
});
|
||||
return maxDetailsPaneHeight;
|
||||
}
|
||||
layoutSelectDropDown(preLayoutPosition) {
|
||||
// Avoid recursion from layout called in onListFocus
|
||||
if (this._skipLayout) {
|
||||
return false;
|
||||
}
|
||||
// Layout ContextView drop down select list and container
|
||||
// Have to manage our vertical overflow, sizing, position below or above
|
||||
// Position has to be determined and set prior to contextView instantiation
|
||||
if (this.selectList) {
|
||||
// Make visible to enable measurements
|
||||
this.selectDropDownContainer.classList.add('visible');
|
||||
const window = getWindow(this.selectElement);
|
||||
const selectPosition = getDomNodePagePosition(this.selectElement);
|
||||
const styles = getWindow(this.selectElement).getComputedStyle(this.selectElement);
|
||||
const verticalPadding = parseFloat(styles.getPropertyValue('--dropdown-padding-top')) + parseFloat(styles.getPropertyValue('--dropdown-padding-bottom'));
|
||||
const maxSelectDropDownHeightBelow = (window.innerHeight - selectPosition.top - selectPosition.height - (this.selectBoxOptions.minBottomMargin || 0));
|
||||
const maxSelectDropDownHeightAbove = (selectPosition.top - SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN);
|
||||
// Determine optimal width - min(longest option), opt(parent select, excluding margins), max(ContextView controlled)
|
||||
const selectWidth = this.selectElement.offsetWidth;
|
||||
const selectMinWidth = this.setWidthControlElement(this.widthControlElement);
|
||||
const selectOptimalWidth = Math.max(selectMinWidth, Math.round(selectWidth)).toString() + 'px';
|
||||
this.selectDropDownContainer.style.width = selectOptimalWidth;
|
||||
// Get initial list height and determine space above and below
|
||||
this.selectList.getHTMLElement().style.height = '';
|
||||
this.selectList.layout();
|
||||
let listHeight = this.selectList.contentHeight;
|
||||
if (this._hasDetails && this._cachedMaxDetailsHeight === undefined) {
|
||||
this._cachedMaxDetailsHeight = this.measureMaxDetailsHeight();
|
||||
}
|
||||
const maxDetailsPaneHeight = this._hasDetails ? this._cachedMaxDetailsHeight : 0;
|
||||
const minRequiredDropDownHeight = listHeight + verticalPadding + maxDetailsPaneHeight;
|
||||
const maxVisibleOptionsBelow = ((Math.floor((maxSelectDropDownHeightBelow - verticalPadding - maxDetailsPaneHeight) / this.getHeight())));
|
||||
const maxVisibleOptionsAbove = ((Math.floor((maxSelectDropDownHeightAbove - verticalPadding - maxDetailsPaneHeight) / this.getHeight())));
|
||||
// If we are only doing pre-layout check/adjust position only
|
||||
// Calculate vertical space available, flip up if insufficient
|
||||
// Use reflected padding on parent select, ContextView style
|
||||
// properties not available before DOM attachment
|
||||
if (preLayoutPosition) {
|
||||
// Check if select moved out of viewport , do not open
|
||||
// If at least one option cannot be shown, don't open the drop-down or hide/remove if open
|
||||
if ((selectPosition.top + selectPosition.height) > (window.innerHeight - 22)
|
||||
|| selectPosition.top < SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN
|
||||
|| ((maxVisibleOptionsBelow < 1) && (maxVisibleOptionsAbove < 1))) {
|
||||
// Indicate we cannot open
|
||||
return false;
|
||||
}
|
||||
// Determine if we have to flip up
|
||||
// Always show complete list items - never more than Max available vertical height
|
||||
if (maxVisibleOptionsBelow < SelectBoxList.DEFAULT_MINIMUM_VISIBLE_OPTIONS
|
||||
&& maxVisibleOptionsAbove > maxVisibleOptionsBelow
|
||||
&& this.options.length > maxVisibleOptionsBelow) {
|
||||
this._dropDownPosition = 1 /* AnchorPosition.ABOVE */;
|
||||
this.selectDropDownListContainer.remove();
|
||||
this.selectionDetailsPane.remove();
|
||||
this.selectDropDownContainer.appendChild(this.selectionDetailsPane);
|
||||
this.selectDropDownContainer.appendChild(this.selectDropDownListContainer);
|
||||
this.selectionDetailsPane.classList.remove('border-top');
|
||||
this.selectionDetailsPane.classList.add('border-bottom');
|
||||
}
|
||||
else {
|
||||
this._dropDownPosition = 0 /* AnchorPosition.BELOW */;
|
||||
this.selectDropDownListContainer.remove();
|
||||
this.selectionDetailsPane.remove();
|
||||
this.selectDropDownContainer.appendChild(this.selectDropDownListContainer);
|
||||
this.selectDropDownContainer.appendChild(this.selectionDetailsPane);
|
||||
this.selectionDetailsPane.classList.remove('border-bottom');
|
||||
this.selectionDetailsPane.classList.add('border-top');
|
||||
}
|
||||
// Do full layout on showSelectDropDown only
|
||||
return true;
|
||||
}
|
||||
// Check if select out of viewport or cutting into status bar
|
||||
if ((selectPosition.top + selectPosition.height) > (window.innerHeight - 22)
|
||||
|| selectPosition.top < SelectBoxList.DEFAULT_DROPDOWN_MINIMUM_TOP_MARGIN
|
||||
|| (this._dropDownPosition === 0 /* AnchorPosition.BELOW */ && maxVisibleOptionsBelow < 1)
|
||||
|| (this._dropDownPosition === 1 /* AnchorPosition.ABOVE */ && maxVisibleOptionsAbove < 1)) {
|
||||
// Cannot properly layout, close and hide
|
||||
this.hideSelectDropDown(true);
|
||||
return false;
|
||||
}
|
||||
// SetUp list dimensions and layout - account for container padding
|
||||
// Use position to check above or below available space
|
||||
if (this._dropDownPosition === 0 /* AnchorPosition.BELOW */) {
|
||||
if (this._isVisible && maxVisibleOptionsBelow + maxVisibleOptionsAbove < 1) {
|
||||
// If drop-down is visible, must be doing a DOM re-layout, hide since we don't fit
|
||||
// Hide drop-down, hide contextview, focus on parent select
|
||||
this.hideSelectDropDown(true);
|
||||
return false;
|
||||
}
|
||||
// Adjust list height to max from select bottom to margin (default/minBottomMargin)
|
||||
if (minRequiredDropDownHeight > maxSelectDropDownHeightBelow) {
|
||||
listHeight = (maxVisibleOptionsBelow * this.getHeight());
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (minRequiredDropDownHeight > maxSelectDropDownHeightAbove) {
|
||||
listHeight = (maxVisibleOptionsAbove * this.getHeight());
|
||||
}
|
||||
}
|
||||
// Set adjusted list height and relayout
|
||||
this.selectList.layout(listHeight);
|
||||
this.selectList.domFocus();
|
||||
// Finally set focus on selected item
|
||||
if (this.selectList.length > 0) {
|
||||
this.selectList.setFocus([this.selected || 0]);
|
||||
this.selectList.reveal(this.selectList.getFocus()[0] || 0);
|
||||
}
|
||||
if (this._hasDetails) {
|
||||
// Leave the selectDropDownContainer to size itself according to children (list + details) - #57447
|
||||
this.selectList.getHTMLElement().style.height = (listHeight + verticalPadding) + 'px';
|
||||
this.selectDropDownContainer.style.height = '';
|
||||
}
|
||||
else {
|
||||
this.selectDropDownContainer.style.height = (listHeight + verticalPadding) + 'px';
|
||||
}
|
||||
this.updateDetail(this.selected);
|
||||
this.selectDropDownContainer.style.width = selectOptimalWidth;
|
||||
// Maintain focus outline on parent select as well as list container - tabindex for focus
|
||||
this.selectDropDownListContainer.setAttribute('tabindex', '0');
|
||||
this.selectElement.classList.add('synthetic-focus');
|
||||
this.selectDropDownContainer.classList.add('synthetic-focus');
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
setWidthControlElement(container) {
|
||||
let elementWidth = 0;
|
||||
if (container) {
|
||||
let longest = 0;
|
||||
let longestLength = 0;
|
||||
this.options.forEach((option, index) => {
|
||||
const detailLength = !!option.detail ? option.detail.length : 0;
|
||||
const rightDecoratorLength = !!option.decoratorRight ? option.decoratorRight.length : 0;
|
||||
const len = option.text.length + detailLength + rightDecoratorLength;
|
||||
if (len > longestLength) {
|
||||
longest = index;
|
||||
longestLength = len;
|
||||
}
|
||||
});
|
||||
container.textContent = this.options[longest].text + (!!this.options[longest].decoratorRight ? (this.options[longest].decoratorRight + ' ') : '');
|
||||
elementWidth = getTotalWidth(container);
|
||||
}
|
||||
return elementWidth;
|
||||
}
|
||||
createSelectList(parent) {
|
||||
// If we have already constructive list on open, skip
|
||||
if (this.selectList) {
|
||||
return;
|
||||
}
|
||||
// SetUp container for list
|
||||
this.selectDropDownListContainer = append(parent, $('.select-box-dropdown-list-container'));
|
||||
this.listRenderer = new SelectListRenderer();
|
||||
this.selectList = this._register(new List('SelectBoxCustom', this.selectDropDownListContainer, this, [this.listRenderer], {
|
||||
useShadows: false,
|
||||
verticalScrollMode: 3 /* ScrollbarVisibility.Visible */,
|
||||
keyboardSupport: false,
|
||||
mouseSupport: false,
|
||||
accessibilityProvider: {
|
||||
getAriaLabel: element => {
|
||||
let label = element.text;
|
||||
if (element.detail) {
|
||||
label += `. ${element.detail}`;
|
||||
}
|
||||
if (element.decoratorRight) {
|
||||
label += `. ${element.decoratorRight}`;
|
||||
}
|
||||
if (element.description) {
|
||||
label += `. ${element.description}`;
|
||||
}
|
||||
return label;
|
||||
},
|
||||
getWidgetAriaLabel: () => localize(16, "Select Box"),
|
||||
getRole: () => isMacintosh ? '' : 'option',
|
||||
getWidgetRole: () => 'listbox'
|
||||
}
|
||||
}));
|
||||
if (this.selectBoxOptions.ariaLabel) {
|
||||
this.selectList.ariaLabel = this.selectBoxOptions.ariaLabel;
|
||||
}
|
||||
// SetUp list keyboard controller - control navigation, disabled items, focus
|
||||
const onKeyDown = this._register(new DomEmitter(this.selectDropDownListContainer, 'keydown'));
|
||||
const onSelectDropDownKeyDown = Event.chain(onKeyDown.event, $ => $.filter(() => this.selectList.length > 0)
|
||||
.map(e => new StandardKeyboardEvent(e)));
|
||||
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 3 /* KeyCode.Enter */))(this.onEnter, this));
|
||||
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 2 /* KeyCode.Tab */))(this.onEnter, this)); // Tab should behave the same as enter, #79339
|
||||
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 9 /* KeyCode.Escape */))(this.onEscape, this));
|
||||
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 16 /* KeyCode.UpArrow */))(this.onUpArrow, this));
|
||||
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 18 /* KeyCode.DownArrow */))(this.onDownArrow, this));
|
||||
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 12 /* KeyCode.PageDown */))(this.onPageDown, this));
|
||||
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 11 /* KeyCode.PageUp */))(this.onPageUp, this));
|
||||
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 14 /* KeyCode.Home */))(this.onHome, this));
|
||||
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => e.keyCode === 13 /* KeyCode.End */))(this.onEnd, this));
|
||||
this._register(Event.chain(onSelectDropDownKeyDown, $ => $.filter(e => (e.keyCode >= 21 /* KeyCode.Digit0 */ && e.keyCode <= 56 /* KeyCode.KeyZ */) || (e.keyCode >= 85 /* KeyCode.Semicolon */ && e.keyCode <= 113 /* KeyCode.NumpadDivide */)))(this.onCharacter, this));
|
||||
// SetUp list mouse controller - control navigation, disabled items, focus
|
||||
this._register(addDisposableListener(this.selectList.getHTMLElement(), EventType.POINTER_UP, e => this.onPointerUp(e)));
|
||||
this._register(this.selectList.onMouseOver(e => typeof e.index !== 'undefined' && this.selectList.setFocus([e.index])));
|
||||
this._register(this.selectList.onDidChangeFocus(e => this.onListFocus(e)));
|
||||
this._register(addDisposableListener(this.selectDropDownContainer, EventType.FOCUS_OUT, e => {
|
||||
if (!this._isVisible || isAncestor(e.relatedTarget, this.selectDropDownContainer)) {
|
||||
return;
|
||||
}
|
||||
this.onListBlur();
|
||||
}));
|
||||
this.selectList.getHTMLElement().setAttribute('aria-label', this.selectBoxOptions.ariaLabel || '');
|
||||
this.selectList.getHTMLElement().setAttribute('aria-expanded', 'true');
|
||||
this.styleList();
|
||||
}
|
||||
// List methods
|
||||
// List mouse controller - active exit, select option, fire onDidSelect if change, return focus to parent select
|
||||
// Also takes in touchend events
|
||||
onPointerUp(e) {
|
||||
if (!this.selectList.length) {
|
||||
return;
|
||||
}
|
||||
EventHelper.stop(e);
|
||||
const target = e.target;
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
// Check our mouse event is on an option (not scrollbar)
|
||||
if (target.classList.contains('slider')) {
|
||||
return;
|
||||
}
|
||||
const listRowElement = target.closest('.monaco-list-row');
|
||||
if (!listRowElement) {
|
||||
return;
|
||||
}
|
||||
const index = Number(listRowElement.getAttribute('data-index'));
|
||||
const disabled = listRowElement.classList.contains('option-disabled');
|
||||
// Ignore mouse selection of disabled options
|
||||
if (index >= 0 && index < this.options.length && !disabled) {
|
||||
this.selected = index;
|
||||
this.select(this.selected);
|
||||
this.selectList.setFocus([this.selected]);
|
||||
this.selectList.reveal(this.selectList.getFocus()[0]);
|
||||
// Only fire if selection change
|
||||
if (this.selected !== this._currentSelection) {
|
||||
// Set current = selected
|
||||
this._currentSelection = this.selected;
|
||||
this._onDidSelect.fire({
|
||||
index: this.selectElement.selectedIndex,
|
||||
selected: this.options[this.selected].text
|
||||
});
|
||||
if (!!this.options[this.selected] && !!this.options[this.selected].text) {
|
||||
this.setTitle(this.options[this.selected].text);
|
||||
}
|
||||
}
|
||||
this.hideSelectDropDown(true);
|
||||
}
|
||||
}
|
||||
// List Exit - passive - implicit no selection change, hide drop-down
|
||||
onListBlur() {
|
||||
if (this._sticky) {
|
||||
return;
|
||||
}
|
||||
if (this.selected !== this._currentSelection) {
|
||||
// Reset selected to current if no change
|
||||
this.select(this._currentSelection);
|
||||
}
|
||||
this.hideSelectDropDown(false);
|
||||
}
|
||||
renderDescriptionMarkdown(text, actionHandler) {
|
||||
const cleanRenderedMarkdown = (element) => {
|
||||
for (let i = 0; i < element.childNodes.length; i++) {
|
||||
const child = element.childNodes.item(i);
|
||||
const tagName = child.tagName && child.tagName.toLowerCase();
|
||||
if (tagName === 'img') {
|
||||
child.remove();
|
||||
}
|
||||
else {
|
||||
cleanRenderedMarkdown(child);
|
||||
}
|
||||
}
|
||||
};
|
||||
const rendered = renderMarkdown({ value: text, supportThemeIcons: true }, { actionHandler });
|
||||
rendered.element.classList.add('select-box-description-markdown');
|
||||
cleanRenderedMarkdown(rendered.element);
|
||||
return rendered;
|
||||
}
|
||||
// List Focus Change - passive - update details pane with newly focused element's data
|
||||
onListFocus(e) {
|
||||
// Skip during initial layout
|
||||
if (!this._isVisible || !this._hasDetails) {
|
||||
return;
|
||||
}
|
||||
this.updateDetail(e.indexes[0]);
|
||||
}
|
||||
updateDetail(selectedIndex) {
|
||||
// Reset
|
||||
this._selectionDetailsDisposables.clear();
|
||||
this.selectionDetailsPane.textContent = '';
|
||||
const option = this.options[selectedIndex];
|
||||
const description = option?.description ?? '';
|
||||
const descriptionIsMarkdown = option?.descriptionIsMarkdown ?? false;
|
||||
if (description) {
|
||||
if (descriptionIsMarkdown) {
|
||||
const actionHandler = option.descriptionMarkdownActionHandler;
|
||||
const result = this._selectionDetailsDisposables.add(this.renderDescriptionMarkdown(description, actionHandler));
|
||||
this.selectionDetailsPane.appendChild(result.element);
|
||||
}
|
||||
else {
|
||||
this.selectionDetailsPane.textContent = description;
|
||||
}
|
||||
this.selectionDetailsPane.style.display = 'block';
|
||||
}
|
||||
else {
|
||||
this.selectionDetailsPane.style.display = 'none';
|
||||
}
|
||||
// Avoid recursion
|
||||
this._skipLayout = true;
|
||||
this.contextViewProvider.layout();
|
||||
this._skipLayout = false;
|
||||
}
|
||||
// List keyboard controller
|
||||
// List exit - active - hide ContextView dropdown, reset selection, return focus to parent select
|
||||
onEscape(e) {
|
||||
EventHelper.stop(e);
|
||||
// Reset selection to value when opened
|
||||
this.select(this._currentSelection);
|
||||
this.hideSelectDropDown(true);
|
||||
}
|
||||
// List exit - active - hide ContextView dropdown, return focus to parent select, fire onDidSelect if change
|
||||
onEnter(e) {
|
||||
EventHelper.stop(e);
|
||||
// Only fire if selection change
|
||||
if (this.selected !== this._currentSelection) {
|
||||
this._currentSelection = this.selected;
|
||||
this._onDidSelect.fire({
|
||||
index: this.selectElement.selectedIndex,
|
||||
selected: this.options[this.selected].text
|
||||
});
|
||||
if (!!this.options[this.selected] && !!this.options[this.selected].text) {
|
||||
this.setTitle(this.options[this.selected].text);
|
||||
}
|
||||
}
|
||||
this.hideSelectDropDown(true);
|
||||
}
|
||||
// List navigation - have to handle a disabled option (jump over)
|
||||
onDownArrow(e) {
|
||||
if (this.selected < this.options.length - 1) {
|
||||
EventHelper.stop(e, true);
|
||||
// Skip disabled options
|
||||
const nextOptionDisabled = this.options[this.selected + 1].isDisabled;
|
||||
if (nextOptionDisabled && this.options.length > this.selected + 2) {
|
||||
this.selected += 2;
|
||||
}
|
||||
else if (nextOptionDisabled) {
|
||||
return;
|
||||
}
|
||||
else {
|
||||
this.selected++;
|
||||
}
|
||||
// Set focus/selection - only fire event when closing drop-down or on blur
|
||||
this.select(this.selected);
|
||||
this.selectList.setFocus([this.selected]);
|
||||
this.selectList.reveal(this.selectList.getFocus()[0]);
|
||||
}
|
||||
}
|
||||
onUpArrow(e) {
|
||||
if (this.selected > 0) {
|
||||
EventHelper.stop(e, true);
|
||||
// Skip disabled options
|
||||
const previousOptionDisabled = this.options[this.selected - 1].isDisabled;
|
||||
if (previousOptionDisabled && this.selected > 1) {
|
||||
this.selected -= 2;
|
||||
}
|
||||
else {
|
||||
this.selected--;
|
||||
}
|
||||
// Set focus/selection - only fire event when closing drop-down or on blur
|
||||
this.select(this.selected);
|
||||
this.selectList.setFocus([this.selected]);
|
||||
this.selectList.reveal(this.selectList.getFocus()[0]);
|
||||
}
|
||||
}
|
||||
onPageUp(e) {
|
||||
EventHelper.stop(e);
|
||||
this.selectList.focusPreviousPage();
|
||||
// Allow scrolling to settle
|
||||
setTimeout(() => {
|
||||
this.selected = this.selectList.getFocus()[0];
|
||||
// Shift selection down if we land on a disabled option
|
||||
if (this.options[this.selected].isDisabled && this.selected < this.options.length - 1) {
|
||||
this.selected++;
|
||||
this.selectList.setFocus([this.selected]);
|
||||
}
|
||||
this.selectList.reveal(this.selected);
|
||||
this.select(this.selected);
|
||||
}, 1);
|
||||
}
|
||||
onPageDown(e) {
|
||||
EventHelper.stop(e);
|
||||
this.selectList.focusNextPage();
|
||||
// Allow scrolling to settle
|
||||
setTimeout(() => {
|
||||
this.selected = this.selectList.getFocus()[0];
|
||||
// Shift selection up if we land on a disabled option
|
||||
if (this.options[this.selected].isDisabled && this.selected > 0) {
|
||||
this.selected--;
|
||||
this.selectList.setFocus([this.selected]);
|
||||
}
|
||||
this.selectList.reveal(this.selected);
|
||||
this.select(this.selected);
|
||||
}, 1);
|
||||
}
|
||||
onHome(e) {
|
||||
EventHelper.stop(e);
|
||||
if (this.options.length < 2) {
|
||||
return;
|
||||
}
|
||||
this.selected = 0;
|
||||
if (this.options[this.selected].isDisabled && this.selected > 1) {
|
||||
this.selected++;
|
||||
}
|
||||
this.selectList.setFocus([this.selected]);
|
||||
this.selectList.reveal(this.selected);
|
||||
this.select(this.selected);
|
||||
}
|
||||
onEnd(e) {
|
||||
EventHelper.stop(e);
|
||||
if (this.options.length < 2) {
|
||||
return;
|
||||
}
|
||||
this.selected = this.options.length - 1;
|
||||
if (this.options[this.selected].isDisabled && this.selected > 1) {
|
||||
this.selected--;
|
||||
}
|
||||
this.selectList.setFocus([this.selected]);
|
||||
this.selectList.reveal(this.selected);
|
||||
this.select(this.selected);
|
||||
}
|
||||
// Mimic option first character navigation of native select
|
||||
onCharacter(e) {
|
||||
const ch = KeyCodeUtils.toString(e.keyCode);
|
||||
let optionIndex = -1;
|
||||
for (let i = 0; i < this.options.length - 1; i++) {
|
||||
optionIndex = (i + this.selected + 1) % this.options.length;
|
||||
if (this.options[optionIndex].text.charAt(0).toUpperCase() === ch && !this.options[optionIndex].isDisabled) {
|
||||
this.select(optionIndex);
|
||||
this.selectList.setFocus([optionIndex]);
|
||||
this.selectList.reveal(this.selectList.getFocus()[0]);
|
||||
EventHelper.stop(e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
dispose() {
|
||||
this.hideSelectDropDown(false);
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export { SelectBoxList };
|
||||
Generated
Vendored
+142
@@ -0,0 +1,142 @@
|
||||
import { addDisposableListener, addStandardDisposableListener, EventHelper } from '../../dom.js';
|
||||
import { Gesture, EventType } from '../../touch.js';
|
||||
import { equals } from '../../../common/arrays.js';
|
||||
import { Emitter } from '../../../common/event.js';
|
||||
import { Disposable } from '../../../common/lifecycle.js';
|
||||
import { isMacintosh } from '../../../common/platform.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class SelectBoxNative extends Disposable {
|
||||
constructor(options, selected, styles, selectBoxOptions) {
|
||||
super();
|
||||
this.selected = 0;
|
||||
this.selectBoxOptions = selectBoxOptions || Object.create(null);
|
||||
this.options = [];
|
||||
this.selectElement = document.createElement('select');
|
||||
this.selectElement.className = 'monaco-select-box';
|
||||
if (typeof this.selectBoxOptions.ariaLabel === 'string') {
|
||||
this.selectElement.setAttribute('aria-label', this.selectBoxOptions.ariaLabel);
|
||||
}
|
||||
if (typeof this.selectBoxOptions.ariaDescription === 'string') {
|
||||
this.selectElement.setAttribute('aria-description', this.selectBoxOptions.ariaDescription);
|
||||
}
|
||||
this._onDidSelect = this._register(new Emitter());
|
||||
this.styles = styles;
|
||||
this.registerListeners();
|
||||
this.setOptions(options, selected);
|
||||
}
|
||||
registerListeners() {
|
||||
this._register(Gesture.addTarget(this.selectElement));
|
||||
[EventType.Tap].forEach(eventType => {
|
||||
this._register(addDisposableListener(this.selectElement, eventType, (e) => {
|
||||
this.selectElement.focus();
|
||||
}));
|
||||
});
|
||||
this._register(addStandardDisposableListener(this.selectElement, 'click', (e) => {
|
||||
EventHelper.stop(e, true);
|
||||
}));
|
||||
this._register(addStandardDisposableListener(this.selectElement, 'change', (e) => {
|
||||
this.selectElement.title = e.target.value;
|
||||
this._onDidSelect.fire({
|
||||
index: e.target.selectedIndex,
|
||||
selected: e.target.value
|
||||
});
|
||||
}));
|
||||
this._register(addStandardDisposableListener(this.selectElement, 'keydown', (e) => {
|
||||
let showSelect = false;
|
||||
if (isMacintosh) {
|
||||
if (e.keyCode === 18 /* KeyCode.DownArrow */ || e.keyCode === 16 /* KeyCode.UpArrow */ || e.keyCode === 10 /* KeyCode.Space */) {
|
||||
showSelect = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (e.keyCode === 18 /* KeyCode.DownArrow */ && e.altKey || e.keyCode === 10 /* KeyCode.Space */ || e.keyCode === 3 /* KeyCode.Enter */) {
|
||||
showSelect = true;
|
||||
}
|
||||
}
|
||||
if (showSelect) {
|
||||
// Space, Enter, is used to expand select box, do not propagate it (prevent action bar action run)
|
||||
e.stopPropagation();
|
||||
}
|
||||
}));
|
||||
}
|
||||
get onDidSelect() {
|
||||
return this._onDidSelect.event;
|
||||
}
|
||||
setOptions(options, selected) {
|
||||
if (!this.options || !equals(this.options, options)) {
|
||||
this.options = options;
|
||||
this.selectElement.options.length = 0;
|
||||
this.options.forEach((option, index) => {
|
||||
this.selectElement.add(this.createOption(option.text, index, option.isDisabled));
|
||||
});
|
||||
}
|
||||
if (selected !== undefined) {
|
||||
this.select(selected);
|
||||
}
|
||||
}
|
||||
select(index) {
|
||||
if (this.options.length === 0) {
|
||||
this.selected = 0;
|
||||
}
|
||||
else if (index >= 0 && index < this.options.length) {
|
||||
this.selected = index;
|
||||
}
|
||||
else if (index > this.options.length - 1) {
|
||||
// Adjust index to end of list
|
||||
// This could make client out of sync with the select
|
||||
this.select(this.options.length - 1);
|
||||
}
|
||||
else if (this.selected < 0) {
|
||||
this.selected = 0;
|
||||
}
|
||||
this.selectElement.selectedIndex = this.selected;
|
||||
if ((this.selected < this.options.length) && typeof this.options[this.selected].text === 'string') {
|
||||
this.selectElement.title = this.options[this.selected].text;
|
||||
}
|
||||
else {
|
||||
this.selectElement.title = '';
|
||||
}
|
||||
}
|
||||
focus() {
|
||||
if (this.selectElement) {
|
||||
this.selectElement.tabIndex = 0;
|
||||
this.selectElement.focus();
|
||||
}
|
||||
}
|
||||
blur() {
|
||||
if (this.selectElement) {
|
||||
this.selectElement.tabIndex = -1;
|
||||
this.selectElement.blur();
|
||||
}
|
||||
}
|
||||
setFocusable(focusable) {
|
||||
this.selectElement.tabIndex = focusable ? 0 : -1;
|
||||
}
|
||||
render(container) {
|
||||
container.classList.add('select-container');
|
||||
container.appendChild(this.selectElement);
|
||||
this.setOptions(this.options, this.selected);
|
||||
this.applyStyles();
|
||||
}
|
||||
applyStyles() {
|
||||
// Style native select
|
||||
if (this.selectElement) {
|
||||
this.selectElement.style.backgroundColor = this.styles.selectBackground ?? '';
|
||||
this.selectElement.style.color = this.styles.selectForeground ?? '';
|
||||
this.selectElement.style.borderColor = this.styles.selectBorder ?? '';
|
||||
}
|
||||
}
|
||||
createOption(value, index, disabled) {
|
||||
const option = document.createElement('option');
|
||||
option.value = value;
|
||||
option.text = value;
|
||||
option.disabled = !!disabled;
|
||||
return option;
|
||||
}
|
||||
}
|
||||
|
||||
export { SelectBoxNative };
|
||||
Generated
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-editor .zone-widget .codicon.codicon-error,
|
||||
.markers-panel .marker-icon.error, .markers-panel .marker-icon .codicon.codicon-error,
|
||||
.text-search-provider-messages .providerMessage .codicon.codicon-error,
|
||||
.extensions-viewlet > .extensions .codicon.codicon-error,
|
||||
.extension-editor .codicon.codicon-error,
|
||||
.chat-attached-context-attachment .codicon.codicon-error {
|
||||
color: var(--vscode-problemsErrorIcon-foreground);
|
||||
}
|
||||
|
||||
.monaco-editor .zone-widget .codicon.codicon-warning,
|
||||
.markers-panel .marker-icon.warning, .markers-panel .marker-icon .codicon.codicon-warning,
|
||||
.text-search-provider-messages .providerMessage .codicon.codicon-warning,
|
||||
.extensions-viewlet > .extensions .codicon.codicon-warning,
|
||||
.extension-editor .codicon.codicon-warning,
|
||||
.preferences-editor .codicon.codicon-warning {
|
||||
color: var(--vscode-problemsWarningIcon-foreground);
|
||||
}
|
||||
|
||||
.monaco-editor .zone-widget .codicon.codicon-info,
|
||||
.markers-panel .marker-icon.info, .markers-panel .marker-icon .codicon.codicon-info,
|
||||
.text-search-provider-messages .providerMessage .codicon.codicon-info,
|
||||
.extensions-viewlet > .extensions .codicon.codicon-info,
|
||||
.extension-editor .codicon.codicon-info {
|
||||
color: var(--vscode-problemsInfoIcon-foreground);
|
||||
}
|
||||
Generated
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
import './media/severityIcon.css';
|
||||
import { Codicon } from '../../../common/codicons.js';
|
||||
import { ThemeIcon } from '../../../common/themables.js';
|
||||
import Severity from '../../../common/severity.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
var SeverityIcon;
|
||||
(function (SeverityIcon) {
|
||||
function className(severity) {
|
||||
switch (severity) {
|
||||
case Severity.Ignore:
|
||||
return 'severity-ignore ' + ThemeIcon.asClassName(Codicon.info);
|
||||
case Severity.Info:
|
||||
return ThemeIcon.asClassName(Codicon.info);
|
||||
case Severity.Warning:
|
||||
return ThemeIcon.asClassName(Codicon.warning);
|
||||
case Severity.Error:
|
||||
return ThemeIcon.asClassName(Codicon.error);
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
SeverityIcon.className = className;
|
||||
})(SeverityIcon || (SeverityIcon = {}));
|
||||
|
||||
export { SeverityIcon };
|
||||
Generated
Vendored
+70
@@ -0,0 +1,70 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-split-view2 {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-split-view2 > .sash-container {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.monaco-split-view2 > .sash-container > .monaco-sash {
|
||||
pointer-events: initial;
|
||||
}
|
||||
|
||||
.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 > .monaco-scrollable-element > .split-view-container > .split-view-view {
|
||||
white-space: initial;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.monaco-split-view2 > .monaco-scrollable-element > .split-view-container > .split-view-view:not(.visible) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.monaco-split-view2.vertical > .monaco-scrollable-element > .split-view-container > .split-view-view {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.monaco-split-view2.horizontal > .monaco-scrollable-element > .split-view-container > .split-view-view {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-split-view2.separator-border > .monaco-scrollable-element > .split-view-container > .split-view-view:not(:first-child)::before {
|
||||
content: ' ';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 5;
|
||||
pointer-events: none;
|
||||
background-color: var(--separator-border);
|
||||
}
|
||||
|
||||
.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 > .monaco-scrollable-element > .split-view-container > .split-view-view:not(:first-child)::before {
|
||||
height: 1px;
|
||||
width: 100%;
|
||||
}
|
||||
Generated
Vendored
+829
@@ -0,0 +1,829 @@
|
||||
import { append, $, scheduleAtNextAnimationFrame, getWindow, addDisposableListener } from '../../dom.js';
|
||||
import { DomEmitter } from '../../event.js';
|
||||
import { Sash } from '../sash/sash.js';
|
||||
import { SmoothScrollableElement } from '../scrollbar/scrollableElement.js';
|
||||
import { range, pushToStart, pushToEnd } from '../../../common/arrays.js';
|
||||
import { Color } from '../../../common/color.js';
|
||||
import { Emitter, Event } from '../../../common/event.js';
|
||||
import { Disposable, combinedDisposable, toDisposable, dispose } from '../../../common/lifecycle.js';
|
||||
import { clamp } from '../../../common/numbers.js';
|
||||
import { Scrollable } from '../../../common/scrollable.js';
|
||||
import { isUndefined } from '../../../common/types.js';
|
||||
import './splitview.css';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const defaultStyles = {
|
||||
separatorBorder: Color.transparent
|
||||
};
|
||||
class ViewItem {
|
||||
set size(size) {
|
||||
this._size = size;
|
||||
}
|
||||
get size() {
|
||||
return this._size;
|
||||
}
|
||||
get visible() {
|
||||
return typeof this._cachedVisibleSize === 'undefined';
|
||||
}
|
||||
setVisible(visible, size) {
|
||||
if (visible === this.visible) {
|
||||
return;
|
||||
}
|
||||
if (visible) {
|
||||
this.size = clamp(this._cachedVisibleSize, this.viewMinimumSize, this.viewMaximumSize);
|
||||
this._cachedVisibleSize = undefined;
|
||||
}
|
||||
else {
|
||||
this._cachedVisibleSize = typeof size === 'number' ? size : this.size;
|
||||
this.size = 0;
|
||||
}
|
||||
this.container.classList.toggle('visible', visible);
|
||||
try {
|
||||
this.view.setVisible?.(visible);
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Splitview: Failed to set visible view');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
get minimumSize() { return this.visible ? this.view.minimumSize : 0; }
|
||||
get viewMinimumSize() { return this.view.minimumSize; }
|
||||
get maximumSize() { return this.visible ? this.view.maximumSize : 0; }
|
||||
get viewMaximumSize() { return this.view.maximumSize; }
|
||||
get priority() { return this.view.priority; }
|
||||
get proportionalLayout() { return this.view.proportionalLayout ?? true; }
|
||||
get snap() { return !!this.view.snap; }
|
||||
set enabled(enabled) {
|
||||
this.container.style.pointerEvents = enabled ? '' : 'none';
|
||||
}
|
||||
constructor(container, view, size, disposable) {
|
||||
this.container = container;
|
||||
this.view = view;
|
||||
this.disposable = disposable;
|
||||
this._cachedVisibleSize = undefined;
|
||||
if (typeof size === 'number') {
|
||||
this._size = size;
|
||||
this._cachedVisibleSize = undefined;
|
||||
container.classList.add('visible');
|
||||
}
|
||||
else {
|
||||
this._size = 0;
|
||||
this._cachedVisibleSize = size.cachedVisibleSize;
|
||||
}
|
||||
}
|
||||
layout(offset, layoutContext) {
|
||||
this.layoutContainer(offset);
|
||||
try {
|
||||
this.view.layout(this.size, offset, layoutContext);
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Splitview: Failed to layout view');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
dispose() {
|
||||
this.disposable.dispose();
|
||||
}
|
||||
}
|
||||
class VerticalViewItem extends ViewItem {
|
||||
layoutContainer(offset) {
|
||||
this.container.style.top = `${offset}px`;
|
||||
this.container.style.height = `${this.size}px`;
|
||||
}
|
||||
}
|
||||
class HorizontalViewItem extends ViewItem {
|
||||
layoutContainer(offset) {
|
||||
this.container.style.left = `${offset}px`;
|
||||
this.container.style.width = `${this.size}px`;
|
||||
}
|
||||
}
|
||||
var State;
|
||||
(function (State) {
|
||||
State[State["Idle"] = 0] = "Idle";
|
||||
State[State["Busy"] = 1] = "Busy";
|
||||
})(State || (State = {}));
|
||||
var Sizing;
|
||||
(function (Sizing) {
|
||||
/**
|
||||
* When adding or removing views, distribute the delta space among
|
||||
* all other views.
|
||||
*/
|
||||
Sizing.Distribute = { type: 'distribute' };
|
||||
/**
|
||||
* When adding or removing views, split the delta space with another
|
||||
* specific view, indexed by the provided `index`.
|
||||
*/
|
||||
function Split(index) { return { type: 'split', index }; }
|
||||
Sizing.Split = Split;
|
||||
/**
|
||||
* When adding a view, use DistributeSizing when all pre-existing views are
|
||||
* distributed evenly, otherwise use SplitSizing.
|
||||
*/
|
||||
function Auto(index) { return { type: 'auto', index }; }
|
||||
Sizing.Auto = Auto;
|
||||
/**
|
||||
* When adding or removing views, assume the view is invisible.
|
||||
*/
|
||||
function Invisible(cachedVisibleSize) { return { type: 'invisible', cachedVisibleSize }; }
|
||||
Sizing.Invisible = Invisible;
|
||||
})(Sizing || (Sizing = {}));
|
||||
/**
|
||||
* The {@link SplitView} is the UI component which implements a one dimensional
|
||||
* flex-like layout algorithm for a collection of {@link IView} instances, which
|
||||
* are essentially HTMLElement instances with the following size constraints:
|
||||
*
|
||||
* - {@link IView.minimumSize}
|
||||
* - {@link IView.maximumSize}
|
||||
* - {@link IView.priority}
|
||||
* - {@link IView.snap}
|
||||
*
|
||||
* In case the SplitView doesn't have enough size to fit all views, it will overflow
|
||||
* its content with a scrollbar.
|
||||
*
|
||||
* In between each pair of views there will be a {@link Sash} allowing the user
|
||||
* to resize the views, making sure the constraints are respected.
|
||||
*
|
||||
* An optional {@link TLayoutContext layout context type} may be used in order to
|
||||
* pass along layout contextual data from the {@link SplitView.layout} method down
|
||||
* to each view's {@link IView.layout} calls.
|
||||
*
|
||||
* Features:
|
||||
* - Flex-like layout algorithm
|
||||
* - Snap support
|
||||
* - Orthogonal sash support, for corner sashes
|
||||
* - View hide/show support
|
||||
* - View swap/move support
|
||||
* - Alt key modifier behavior, macOS style
|
||||
*/
|
||||
class SplitView extends Disposable {
|
||||
get orthogonalStartSash() { return this._orthogonalStartSash; }
|
||||
get orthogonalEndSash() { return this._orthogonalEndSash; }
|
||||
get startSnappingEnabled() { return this._startSnappingEnabled; }
|
||||
get endSnappingEnabled() { return this._endSnappingEnabled; }
|
||||
/**
|
||||
* A reference to a sash, perpendicular to all sashes in this {@link SplitView},
|
||||
* located at the left- or top-most side of the SplitView.
|
||||
* Corner sashes will be created automatically at the intersections.
|
||||
*/
|
||||
set orthogonalStartSash(sash) {
|
||||
for (const sashItem of this.sashItems) {
|
||||
sashItem.sash.orthogonalStartSash = sash;
|
||||
}
|
||||
this._orthogonalStartSash = sash;
|
||||
}
|
||||
/**
|
||||
* A reference to a sash, perpendicular to all sashes in this {@link SplitView},
|
||||
* located at the right- or bottom-most side of the SplitView.
|
||||
* Corner sashes will be created automatically at the intersections.
|
||||
*/
|
||||
set orthogonalEndSash(sash) {
|
||||
for (const sashItem of this.sashItems) {
|
||||
sashItem.sash.orthogonalEndSash = sash;
|
||||
}
|
||||
this._orthogonalEndSash = sash;
|
||||
}
|
||||
/**
|
||||
* Enable/disable snapping at the beginning of this {@link SplitView}.
|
||||
*/
|
||||
set startSnappingEnabled(startSnappingEnabled) {
|
||||
if (this._startSnappingEnabled === startSnappingEnabled) {
|
||||
return;
|
||||
}
|
||||
this._startSnappingEnabled = startSnappingEnabled;
|
||||
this.updateSashEnablement();
|
||||
}
|
||||
/**
|
||||
* Enable/disable snapping at the end of this {@link SplitView}.
|
||||
*/
|
||||
set endSnappingEnabled(endSnappingEnabled) {
|
||||
if (this._endSnappingEnabled === endSnappingEnabled) {
|
||||
return;
|
||||
}
|
||||
this._endSnappingEnabled = endSnappingEnabled;
|
||||
this.updateSashEnablement();
|
||||
}
|
||||
/**
|
||||
* Create a new {@link SplitView} instance.
|
||||
*/
|
||||
constructor(container, options = {}) {
|
||||
super();
|
||||
this.size = 0;
|
||||
this._contentSize = 0;
|
||||
this.proportions = undefined;
|
||||
this.viewItems = [];
|
||||
this.sashItems = []; // used in tests
|
||||
this.state = State.Idle;
|
||||
this._onDidSashChange = this._register(new Emitter());
|
||||
this._onDidSashReset = this._register(new Emitter());
|
||||
this._startSnappingEnabled = true;
|
||||
this._endSnappingEnabled = true;
|
||||
/**
|
||||
* Fires whenever the user resizes a {@link Sash sash}.
|
||||
*/
|
||||
this.onDidSashChange = this._onDidSashChange.event;
|
||||
/**
|
||||
* Fires whenever the user double clicks a {@link Sash sash}.
|
||||
*/
|
||||
this.onDidSashReset = this._onDidSashReset.event;
|
||||
this.orientation = options.orientation ?? 0 /* Orientation.VERTICAL */;
|
||||
this.inverseAltBehavior = options.inverseAltBehavior ?? false;
|
||||
this.proportionalLayout = options.proportionalLayout ?? true;
|
||||
this.getSashOrthogonalSize = options.getSashOrthogonalSize;
|
||||
this.el = document.createElement('div');
|
||||
this.el.classList.add('monaco-split-view2');
|
||||
this.el.classList.add(this.orientation === 0 /* Orientation.VERTICAL */ ? 'vertical' : 'horizontal');
|
||||
container.appendChild(this.el);
|
||||
this.sashContainer = append(this.el, $('.sash-container'));
|
||||
this.viewContainer = $('.split-view-container');
|
||||
this.scrollable = this._register(new Scrollable({
|
||||
forceIntegerValues: true,
|
||||
smoothScrollDuration: 125,
|
||||
scheduleAtNextAnimationFrame: callback => scheduleAtNextAnimationFrame(getWindow(this.el), callback),
|
||||
}));
|
||||
this.scrollableElement = this._register(new SmoothScrollableElement(this.viewContainer, {
|
||||
vertical: this.orientation === 0 /* Orientation.VERTICAL */ ? (options.scrollbarVisibility ?? 1 /* ScrollbarVisibility.Auto */) : 2 /* ScrollbarVisibility.Hidden */,
|
||||
horizontal: this.orientation === 1 /* Orientation.HORIZONTAL */ ? (options.scrollbarVisibility ?? 1 /* ScrollbarVisibility.Auto */) : 2 /* ScrollbarVisibility.Hidden */
|
||||
}, this.scrollable));
|
||||
// https://github.com/microsoft/vscode/issues/157737
|
||||
const onDidScrollViewContainer = this._register(new DomEmitter(this.viewContainer, 'scroll')).event;
|
||||
this._register(onDidScrollViewContainer(_ => {
|
||||
const position = this.scrollableElement.getScrollPosition();
|
||||
const scrollLeft = Math.abs(this.viewContainer.scrollLeft - position.scrollLeft) <= 1 ? undefined : this.viewContainer.scrollLeft;
|
||||
const scrollTop = Math.abs(this.viewContainer.scrollTop - position.scrollTop) <= 1 ? undefined : this.viewContainer.scrollTop;
|
||||
if (scrollLeft !== undefined || scrollTop !== undefined) {
|
||||
this.scrollableElement.setScrollPosition({ scrollLeft, scrollTop });
|
||||
}
|
||||
}));
|
||||
this.onDidScroll = this.scrollableElement.onScroll;
|
||||
this._register(this.onDidScroll(e => {
|
||||
if (e.scrollTopChanged) {
|
||||
this.viewContainer.scrollTop = e.scrollTop;
|
||||
}
|
||||
if (e.scrollLeftChanged) {
|
||||
this.viewContainer.scrollLeft = e.scrollLeft;
|
||||
}
|
||||
}));
|
||||
append(this.el, this.scrollableElement.getDomNode());
|
||||
this.style(options.styles || defaultStyles);
|
||||
// We have an existing set of view, add them now
|
||||
if (options.descriptor) {
|
||||
this.size = options.descriptor.size;
|
||||
options.descriptor.views.forEach((viewDescriptor, index) => {
|
||||
const sizing = isUndefined(viewDescriptor.visible) || viewDescriptor.visible ? viewDescriptor.size : { type: 'invisible', cachedVisibleSize: viewDescriptor.size };
|
||||
const view = viewDescriptor.view;
|
||||
this.doAddView(view, sizing, index, true);
|
||||
});
|
||||
// Initialize content size and proportions for first layout
|
||||
this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
|
||||
this.saveProportions();
|
||||
}
|
||||
}
|
||||
style(styles) {
|
||||
if (styles.separatorBorder.isTransparent()) {
|
||||
this.el.classList.remove('separator-border');
|
||||
this.el.style.removeProperty('--separator-border');
|
||||
}
|
||||
else {
|
||||
this.el.classList.add('separator-border');
|
||||
this.el.style.setProperty('--separator-border', styles.separatorBorder.toString());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Add a {@link IView view} to this {@link SplitView}.
|
||||
*
|
||||
* @param view The view to add.
|
||||
* @param size Either a fixed size, or a dynamic {@link Sizing} strategy.
|
||||
* @param index The index to insert the view on.
|
||||
* @param skipLayout Whether layout should be skipped.
|
||||
*/
|
||||
addView(view, size, index = this.viewItems.length, skipLayout) {
|
||||
this.doAddView(view, size, index, skipLayout);
|
||||
}
|
||||
/**
|
||||
* Layout the {@link SplitView}.
|
||||
*
|
||||
* @param size The entire size of the {@link SplitView}.
|
||||
* @param layoutContext An optional layout context to pass along to {@link IView views}.
|
||||
*/
|
||||
layout(size, layoutContext) {
|
||||
const previousSize = Math.max(this.size, this._contentSize);
|
||||
this.size = size;
|
||||
this.layoutContext = layoutContext;
|
||||
if (!this.proportions) {
|
||||
const indexes = range(this.viewItems.length);
|
||||
const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */);
|
||||
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
|
||||
this.resize(this.viewItems.length - 1, size - previousSize, undefined, lowPriorityIndexes, highPriorityIndexes);
|
||||
}
|
||||
else {
|
||||
let total = 0;
|
||||
for (let i = 0; i < this.viewItems.length; i++) {
|
||||
const item = this.viewItems[i];
|
||||
const proportion = this.proportions[i];
|
||||
if (typeof proportion === 'number') {
|
||||
total += proportion;
|
||||
}
|
||||
else {
|
||||
size -= item.size;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < this.viewItems.length; i++) {
|
||||
const item = this.viewItems[i];
|
||||
const proportion = this.proportions[i];
|
||||
if (typeof proportion === 'number' && total > 0) {
|
||||
item.size = clamp(Math.round(proportion * size / total), item.minimumSize, item.maximumSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
this.distributeEmptySpace();
|
||||
this.layoutViews();
|
||||
}
|
||||
saveProportions() {
|
||||
if (this.proportionalLayout && this._contentSize > 0) {
|
||||
this.proportions = this.viewItems.map(v => v.proportionalLayout && v.visible ? v.size / this._contentSize : undefined);
|
||||
}
|
||||
}
|
||||
onSashStart({ sash, start, alt }) {
|
||||
for (const item of this.viewItems) {
|
||||
item.enabled = false;
|
||||
}
|
||||
const index = this.sashItems.findIndex(item => item.sash === sash);
|
||||
// This way, we can press Alt while we resize a sash, macOS style!
|
||||
const disposable = combinedDisposable(addDisposableListener(this.el.ownerDocument.body, 'keydown', e => resetSashDragState(this.sashDragState.current, e.altKey)), addDisposableListener(this.el.ownerDocument.body, 'keyup', () => resetSashDragState(this.sashDragState.current, false)));
|
||||
const resetSashDragState = (start, alt) => {
|
||||
const sizes = this.viewItems.map(i => i.size);
|
||||
let minDelta = Number.NEGATIVE_INFINITY;
|
||||
let maxDelta = Number.POSITIVE_INFINITY;
|
||||
if (this.inverseAltBehavior) {
|
||||
alt = !alt;
|
||||
}
|
||||
if (alt) {
|
||||
// When we're using the last sash with Alt, we're resizing
|
||||
// the view to the left/up, instead of right/down as usual
|
||||
// Thus, we must do the inverse of the usual
|
||||
const isLastSash = index === this.sashItems.length - 1;
|
||||
if (isLastSash) {
|
||||
const viewItem = this.viewItems[index];
|
||||
minDelta = (viewItem.minimumSize - viewItem.size) / 2;
|
||||
maxDelta = (viewItem.maximumSize - viewItem.size) / 2;
|
||||
}
|
||||
else {
|
||||
const viewItem = this.viewItems[index + 1];
|
||||
minDelta = (viewItem.size - viewItem.maximumSize) / 2;
|
||||
maxDelta = (viewItem.size - viewItem.minimumSize) / 2;
|
||||
}
|
||||
}
|
||||
let snapBefore;
|
||||
let snapAfter;
|
||||
if (!alt) {
|
||||
const upIndexes = range(index, -1);
|
||||
const downIndexes = range(index + 1, this.viewItems.length);
|
||||
const minDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].minimumSize - sizes[i]), 0);
|
||||
const maxDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].viewMaximumSize - sizes[i]), 0);
|
||||
const maxDeltaDown = downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].minimumSize), 0);
|
||||
const minDeltaDown = downIndexes.length === 0 ? Number.NEGATIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].viewMaximumSize), 0);
|
||||
const minDelta = Math.max(minDeltaUp, minDeltaDown);
|
||||
const maxDelta = Math.min(maxDeltaDown, maxDeltaUp);
|
||||
const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
|
||||
const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
|
||||
if (typeof snapBeforeIndex === 'number') {
|
||||
const viewItem = this.viewItems[snapBeforeIndex];
|
||||
const halfSize = Math.floor(viewItem.viewMinimumSize / 2);
|
||||
snapBefore = {
|
||||
index: snapBeforeIndex,
|
||||
limitDelta: viewItem.visible ? minDelta - halfSize : minDelta + halfSize,
|
||||
size: viewItem.size
|
||||
};
|
||||
}
|
||||
if (typeof snapAfterIndex === 'number') {
|
||||
const viewItem = this.viewItems[snapAfterIndex];
|
||||
const halfSize = Math.floor(viewItem.viewMinimumSize / 2);
|
||||
snapAfter = {
|
||||
index: snapAfterIndex,
|
||||
limitDelta: viewItem.visible ? maxDelta + halfSize : maxDelta - halfSize,
|
||||
size: viewItem.size
|
||||
};
|
||||
}
|
||||
}
|
||||
this.sashDragState = { start, current: start, index, sizes, minDelta, maxDelta, alt, snapBefore, snapAfter, disposable };
|
||||
};
|
||||
resetSashDragState(start, alt);
|
||||
}
|
||||
onSashChange({ current }) {
|
||||
const { index, start, sizes, alt, minDelta, maxDelta, snapBefore, snapAfter } = this.sashDragState;
|
||||
this.sashDragState.current = current;
|
||||
const delta = current - start;
|
||||
const newDelta = this.resize(index, delta, sizes, undefined, undefined, minDelta, maxDelta, snapBefore, snapAfter);
|
||||
if (alt) {
|
||||
const isLastSash = index === this.sashItems.length - 1;
|
||||
const newSizes = this.viewItems.map(i => i.size);
|
||||
const viewItemIndex = isLastSash ? index : index + 1;
|
||||
const viewItem = this.viewItems[viewItemIndex];
|
||||
const newMinDelta = viewItem.size - viewItem.maximumSize;
|
||||
const newMaxDelta = viewItem.size - viewItem.minimumSize;
|
||||
const resizeIndex = isLastSash ? index - 1 : index + 1;
|
||||
this.resize(resizeIndex, -newDelta, newSizes, undefined, undefined, newMinDelta, newMaxDelta);
|
||||
}
|
||||
this.distributeEmptySpace();
|
||||
this.layoutViews();
|
||||
}
|
||||
onSashEnd(index) {
|
||||
this._onDidSashChange.fire(index);
|
||||
this.sashDragState.disposable.dispose();
|
||||
this.saveProportions();
|
||||
for (const item of this.viewItems) {
|
||||
item.enabled = true;
|
||||
}
|
||||
}
|
||||
onViewChange(item, size) {
|
||||
const index = this.viewItems.indexOf(item);
|
||||
if (index < 0 || index >= this.viewItems.length) {
|
||||
return;
|
||||
}
|
||||
size = typeof size === 'number' ? size : item.size;
|
||||
size = clamp(size, item.minimumSize, item.maximumSize);
|
||||
if (this.inverseAltBehavior && index > 0) {
|
||||
// In this case, we want the view to grow or shrink both sides equally
|
||||
// so we just resize the "left" side by half and let `resize` do the clamping magic
|
||||
this.resize(index - 1, Math.floor((item.size - size) / 2));
|
||||
this.distributeEmptySpace();
|
||||
this.layoutViews();
|
||||
}
|
||||
else {
|
||||
item.size = size;
|
||||
this.relayout([index], undefined);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Resize a {@link IView view} within the {@link SplitView}.
|
||||
*
|
||||
* @param index The {@link IView view} index.
|
||||
* @param size The {@link IView view} size.
|
||||
*/
|
||||
resizeView(index, size) {
|
||||
if (index < 0 || index >= this.viewItems.length) {
|
||||
return;
|
||||
}
|
||||
if (this.state !== State.Idle) {
|
||||
throw new Error('Cant modify splitview');
|
||||
}
|
||||
this.state = State.Busy;
|
||||
try {
|
||||
const indexes = range(this.viewItems.length).filter(i => i !== index);
|
||||
const lowPriorityIndexes = [...indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */), index];
|
||||
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
|
||||
const item = this.viewItems[index];
|
||||
size = Math.round(size);
|
||||
size = clamp(size, item.minimumSize, Math.min(item.maximumSize, this.size));
|
||||
item.size = size;
|
||||
this.relayout(lowPriorityIndexes, highPriorityIndexes);
|
||||
}
|
||||
finally {
|
||||
this.state = State.Idle;
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Distribute the entire {@link SplitView} size among all {@link IView views}.
|
||||
*/
|
||||
distributeViewSizes() {
|
||||
const flexibleViewItems = [];
|
||||
let flexibleSize = 0;
|
||||
for (const item of this.viewItems) {
|
||||
if (item.maximumSize - item.minimumSize > 0) {
|
||||
flexibleViewItems.push(item);
|
||||
flexibleSize += item.size;
|
||||
}
|
||||
}
|
||||
const size = Math.floor(flexibleSize / flexibleViewItems.length);
|
||||
for (const item of flexibleViewItems) {
|
||||
item.size = clamp(size, item.minimumSize, item.maximumSize);
|
||||
}
|
||||
const indexes = range(this.viewItems.length);
|
||||
const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */);
|
||||
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
|
||||
this.relayout(lowPriorityIndexes, highPriorityIndexes);
|
||||
}
|
||||
/**
|
||||
* Returns the size of a {@link IView view}.
|
||||
*/
|
||||
getViewSize(index) {
|
||||
if (index < 0 || index >= this.viewItems.length) {
|
||||
return -1;
|
||||
}
|
||||
return this.viewItems[index].size;
|
||||
}
|
||||
doAddView(view, size, index = this.viewItems.length, skipLayout) {
|
||||
if (this.state !== State.Idle) {
|
||||
throw new Error('Cant modify splitview');
|
||||
}
|
||||
this.state = State.Busy;
|
||||
try {
|
||||
// Add view
|
||||
const container = $('.split-view-view');
|
||||
if (index === this.viewItems.length) {
|
||||
this.viewContainer.appendChild(container);
|
||||
}
|
||||
else {
|
||||
this.viewContainer.insertBefore(container, this.viewContainer.children.item(index));
|
||||
}
|
||||
const onChangeDisposable = view.onDidChange(size => this.onViewChange(item, size));
|
||||
const containerDisposable = toDisposable(() => container.remove());
|
||||
const disposable = combinedDisposable(onChangeDisposable, containerDisposable);
|
||||
let viewSize;
|
||||
if (typeof size === 'number') {
|
||||
viewSize = size;
|
||||
}
|
||||
else {
|
||||
if (size.type === 'auto') {
|
||||
if (this.areViewsDistributed()) {
|
||||
size = { type: 'distribute' };
|
||||
}
|
||||
else {
|
||||
size = { type: 'split', index: size.index };
|
||||
}
|
||||
}
|
||||
if (size.type === 'split') {
|
||||
viewSize = this.getViewSize(size.index) / 2;
|
||||
}
|
||||
else if (size.type === 'invisible') {
|
||||
viewSize = { cachedVisibleSize: size.cachedVisibleSize };
|
||||
}
|
||||
else {
|
||||
viewSize = view.minimumSize;
|
||||
}
|
||||
}
|
||||
const item = this.orientation === 0 /* Orientation.VERTICAL */
|
||||
? new VerticalViewItem(container, view, viewSize, disposable)
|
||||
: new HorizontalViewItem(container, view, viewSize, disposable);
|
||||
this.viewItems.splice(index, 0, item);
|
||||
// Add sash
|
||||
if (this.viewItems.length > 1) {
|
||||
const opts = { orthogonalStartSash: this.orthogonalStartSash, orthogonalEndSash: this.orthogonalEndSash };
|
||||
const sash = this.orientation === 0 /* Orientation.VERTICAL */
|
||||
? new Sash(this.sashContainer, { getHorizontalSashTop: s => this.getSashPosition(s), getHorizontalSashWidth: this.getSashOrthogonalSize }, { ...opts, orientation: 1 /* Orientation.HORIZONTAL */ })
|
||||
: new Sash(this.sashContainer, { getVerticalSashLeft: s => this.getSashPosition(s), getVerticalSashHeight: this.getSashOrthogonalSize }, { ...opts, orientation: 0 /* Orientation.VERTICAL */ });
|
||||
const sashEventMapper = this.orientation === 0 /* Orientation.VERTICAL */
|
||||
? (e) => ({ sash, start: e.startY, current: e.currentY, alt: e.altKey })
|
||||
: (e) => ({ sash, start: e.startX, current: e.currentX, alt: e.altKey });
|
||||
const onStart = Event.map(sash.onDidStart, sashEventMapper);
|
||||
const onStartDisposable = onStart(this.onSashStart, this);
|
||||
const onChange = Event.map(sash.onDidChange, sashEventMapper);
|
||||
const onChangeDisposable = onChange(this.onSashChange, this);
|
||||
const onEnd = Event.map(sash.onDidEnd, () => this.sashItems.findIndex(item => item.sash === sash));
|
||||
const onEndDisposable = onEnd(this.onSashEnd, this);
|
||||
const onDidResetDisposable = sash.onDidReset(() => {
|
||||
const index = this.sashItems.findIndex(item => item.sash === sash);
|
||||
const upIndexes = range(index, -1);
|
||||
const downIndexes = range(index + 1, this.viewItems.length);
|
||||
const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
|
||||
const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
|
||||
if (typeof snapBeforeIndex === 'number' && !this.viewItems[snapBeforeIndex].visible) {
|
||||
return;
|
||||
}
|
||||
if (typeof snapAfterIndex === 'number' && !this.viewItems[snapAfterIndex].visible) {
|
||||
return;
|
||||
}
|
||||
this._onDidSashReset.fire(index);
|
||||
});
|
||||
const disposable = combinedDisposable(onStartDisposable, onChangeDisposable, onEndDisposable, onDidResetDisposable, sash);
|
||||
const sashItem = { sash, disposable };
|
||||
this.sashItems.splice(index - 1, 0, sashItem);
|
||||
}
|
||||
container.appendChild(view.element);
|
||||
let highPriorityIndexes;
|
||||
if (typeof size !== 'number' && size.type === 'split') {
|
||||
highPriorityIndexes = [size.index];
|
||||
}
|
||||
if (!skipLayout) {
|
||||
this.relayout([index], highPriorityIndexes);
|
||||
}
|
||||
if (!skipLayout && typeof size !== 'number' && size.type === 'distribute') {
|
||||
this.distributeViewSizes();
|
||||
}
|
||||
}
|
||||
finally {
|
||||
this.state = State.Idle;
|
||||
}
|
||||
}
|
||||
relayout(lowPriorityIndexes, highPriorityIndexes) {
|
||||
const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
|
||||
this.resize(this.viewItems.length - 1, this.size - contentSize, undefined, lowPriorityIndexes, highPriorityIndexes);
|
||||
this.distributeEmptySpace();
|
||||
this.layoutViews();
|
||||
this.saveProportions();
|
||||
}
|
||||
resize(index, delta, sizes = this.viewItems.map(i => i.size), lowPriorityIndexes, highPriorityIndexes, overloadMinDelta = Number.NEGATIVE_INFINITY, overloadMaxDelta = Number.POSITIVE_INFINITY, snapBefore, snapAfter) {
|
||||
if (index < 0 || index >= this.viewItems.length) {
|
||||
return 0;
|
||||
}
|
||||
const upIndexes = range(index, -1);
|
||||
const downIndexes = range(index + 1, this.viewItems.length);
|
||||
if (highPriorityIndexes) {
|
||||
for (const index of highPriorityIndexes) {
|
||||
pushToStart(upIndexes, index);
|
||||
pushToStart(downIndexes, index);
|
||||
}
|
||||
}
|
||||
if (lowPriorityIndexes) {
|
||||
for (const index of lowPriorityIndexes) {
|
||||
pushToEnd(upIndexes, index);
|
||||
pushToEnd(downIndexes, index);
|
||||
}
|
||||
}
|
||||
const upItems = upIndexes.map(i => this.viewItems[i]);
|
||||
const upSizes = upIndexes.map(i => sizes[i]);
|
||||
const downItems = downIndexes.map(i => this.viewItems[i]);
|
||||
const downSizes = downIndexes.map(i => sizes[i]);
|
||||
const minDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].minimumSize - sizes[i]), 0);
|
||||
const maxDeltaUp = upIndexes.reduce((r, i) => r + (this.viewItems[i].maximumSize - sizes[i]), 0);
|
||||
const maxDeltaDown = downIndexes.length === 0 ? Number.POSITIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].minimumSize), 0);
|
||||
const minDeltaDown = downIndexes.length === 0 ? Number.NEGATIVE_INFINITY : downIndexes.reduce((r, i) => r + (sizes[i] - this.viewItems[i].maximumSize), 0);
|
||||
const minDelta = Math.max(minDeltaUp, minDeltaDown, overloadMinDelta);
|
||||
const maxDelta = Math.min(maxDeltaDown, maxDeltaUp, overloadMaxDelta);
|
||||
let snapped = false;
|
||||
if (snapBefore) {
|
||||
const snapView = this.viewItems[snapBefore.index];
|
||||
const visible = delta >= snapBefore.limitDelta;
|
||||
snapped = visible !== snapView.visible;
|
||||
snapView.setVisible(visible, snapBefore.size);
|
||||
}
|
||||
if (!snapped && snapAfter) {
|
||||
const snapView = this.viewItems[snapAfter.index];
|
||||
const visible = delta < snapAfter.limitDelta;
|
||||
snapped = visible !== snapView.visible;
|
||||
snapView.setVisible(visible, snapAfter.size);
|
||||
}
|
||||
if (snapped) {
|
||||
return this.resize(index, delta, sizes, lowPriorityIndexes, highPriorityIndexes, overloadMinDelta, overloadMaxDelta);
|
||||
}
|
||||
delta = clamp(delta, minDelta, maxDelta);
|
||||
for (let i = 0, deltaUp = delta; i < upItems.length; i++) {
|
||||
const item = upItems[i];
|
||||
const size = clamp(upSizes[i] + deltaUp, item.minimumSize, item.maximumSize);
|
||||
const viewDelta = size - upSizes[i];
|
||||
deltaUp -= viewDelta;
|
||||
item.size = size;
|
||||
}
|
||||
for (let i = 0, deltaDown = delta; i < downItems.length; i++) {
|
||||
const item = downItems[i];
|
||||
const size = clamp(downSizes[i] - deltaDown, item.minimumSize, item.maximumSize);
|
||||
const viewDelta = size - downSizes[i];
|
||||
deltaDown += viewDelta;
|
||||
item.size = size;
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
distributeEmptySpace(lowPriorityIndex) {
|
||||
const contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
|
||||
let emptyDelta = this.size - contentSize;
|
||||
const indexes = range(this.viewItems.length - 1, -1);
|
||||
const lowPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 1 /* LayoutPriority.Low */);
|
||||
const highPriorityIndexes = indexes.filter(i => this.viewItems[i].priority === 2 /* LayoutPriority.High */);
|
||||
for (const index of highPriorityIndexes) {
|
||||
pushToStart(indexes, index);
|
||||
}
|
||||
for (const index of lowPriorityIndexes) {
|
||||
pushToEnd(indexes, index);
|
||||
}
|
||||
if (typeof lowPriorityIndex === 'number') {
|
||||
pushToEnd(indexes, lowPriorityIndex);
|
||||
}
|
||||
for (let i = 0; emptyDelta !== 0 && i < indexes.length; i++) {
|
||||
const item = this.viewItems[indexes[i]];
|
||||
const size = clamp(item.size + emptyDelta, item.minimumSize, item.maximumSize);
|
||||
const viewDelta = size - item.size;
|
||||
emptyDelta -= viewDelta;
|
||||
item.size = size;
|
||||
}
|
||||
}
|
||||
layoutViews() {
|
||||
// Save new content size
|
||||
this._contentSize = this.viewItems.reduce((r, i) => r + i.size, 0);
|
||||
// Layout views
|
||||
let offset = 0;
|
||||
for (const viewItem of this.viewItems) {
|
||||
viewItem.layout(offset, this.layoutContext);
|
||||
offset += viewItem.size;
|
||||
}
|
||||
// Layout sashes
|
||||
this.sashItems.forEach(item => item.sash.layout());
|
||||
this.updateSashEnablement();
|
||||
this.updateScrollableElement();
|
||||
}
|
||||
updateScrollableElement() {
|
||||
if (this.orientation === 0 /* Orientation.VERTICAL */) {
|
||||
this.scrollableElement.setScrollDimensions({
|
||||
height: this.size,
|
||||
scrollHeight: this._contentSize
|
||||
});
|
||||
}
|
||||
else {
|
||||
this.scrollableElement.setScrollDimensions({
|
||||
width: this.size,
|
||||
scrollWidth: this._contentSize
|
||||
});
|
||||
}
|
||||
}
|
||||
updateSashEnablement() {
|
||||
let previous = false;
|
||||
const collapsesDown = this.viewItems.map(i => previous = (i.size - i.minimumSize > 0) || previous);
|
||||
previous = false;
|
||||
const expandsDown = this.viewItems.map(i => previous = (i.maximumSize - i.size > 0) || previous);
|
||||
const reverseViews = [...this.viewItems].reverse();
|
||||
previous = false;
|
||||
const collapsesUp = reverseViews.map(i => previous = (i.size - i.minimumSize > 0) || previous).reverse();
|
||||
previous = false;
|
||||
const expandsUp = reverseViews.map(i => previous = (i.maximumSize - i.size > 0) || previous).reverse();
|
||||
let position = 0;
|
||||
for (let index = 0; index < this.sashItems.length; index++) {
|
||||
const { sash } = this.sashItems[index];
|
||||
const viewItem = this.viewItems[index];
|
||||
position += viewItem.size;
|
||||
const min = !(collapsesDown[index] && expandsUp[index + 1]);
|
||||
const max = !(expandsDown[index] && collapsesUp[index + 1]);
|
||||
if (min && max) {
|
||||
const upIndexes = range(index, -1);
|
||||
const downIndexes = range(index + 1, this.viewItems.length);
|
||||
const snapBeforeIndex = this.findFirstSnapIndex(upIndexes);
|
||||
const snapAfterIndex = this.findFirstSnapIndex(downIndexes);
|
||||
const snappedBefore = typeof snapBeforeIndex === 'number' && !this.viewItems[snapBeforeIndex].visible;
|
||||
const snappedAfter = typeof snapAfterIndex === 'number' && !this.viewItems[snapAfterIndex].visible;
|
||||
if (snappedBefore && collapsesUp[index] && (position > 0 || this.startSnappingEnabled)) {
|
||||
sash.state = 1 /* SashState.AtMinimum */;
|
||||
}
|
||||
else if (snappedAfter && collapsesDown[index] && (position < this._contentSize || this.endSnappingEnabled)) {
|
||||
sash.state = 2 /* SashState.AtMaximum */;
|
||||
}
|
||||
else {
|
||||
sash.state = 0 /* SashState.Disabled */;
|
||||
}
|
||||
}
|
||||
else if (min && !max) {
|
||||
sash.state = 1 /* SashState.AtMinimum */;
|
||||
}
|
||||
else if (!min && max) {
|
||||
sash.state = 2 /* SashState.AtMaximum */;
|
||||
}
|
||||
else {
|
||||
sash.state = 3 /* SashState.Enabled */;
|
||||
}
|
||||
}
|
||||
}
|
||||
getSashPosition(sash) {
|
||||
let position = 0;
|
||||
for (let i = 0; i < this.sashItems.length; i++) {
|
||||
position += this.viewItems[i].size;
|
||||
if (this.sashItems[i].sash === sash) {
|
||||
return position;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
findFirstSnapIndex(indexes) {
|
||||
// visible views first
|
||||
for (const index of indexes) {
|
||||
const viewItem = this.viewItems[index];
|
||||
if (!viewItem.visible) {
|
||||
continue;
|
||||
}
|
||||
if (viewItem.snap) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
// then, hidden views
|
||||
for (const index of indexes) {
|
||||
const viewItem = this.viewItems[index];
|
||||
if (viewItem.visible && viewItem.maximumSize - viewItem.minimumSize > 0) {
|
||||
return undefined;
|
||||
}
|
||||
if (!viewItem.visible && viewItem.snap) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
areViewsDistributed() {
|
||||
let min = undefined, max = undefined;
|
||||
for (const view of this.viewItems) {
|
||||
min = min === undefined ? view.size : Math.min(min, view.size);
|
||||
max = max === undefined ? view.size : Math.max(max, view.size);
|
||||
if (max - min > 2) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
dispose() {
|
||||
this.sashDragState?.disposable.dispose();
|
||||
dispose(this.viewItems);
|
||||
this.viewItems = [];
|
||||
this.sashItems.forEach(i => i.disposable.dispose());
|
||||
this.sashItems = [];
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export { Sizing, SplitView };
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-table {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.monaco-table > .monaco-split-view2 {
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.monaco-table > .monaco-list {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.monaco-table-tr {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-table-th {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.monaco-table-th,
|
||||
.monaco-table-td {
|
||||
box-sizing: border-box;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.monaco-table > .monaco-split-view2 .monaco-sash.vertical::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: calc(var(--vscode-sash-size) / 2);
|
||||
width: 0;
|
||||
border-left: 1px solid transparent;
|
||||
}
|
||||
|
||||
.monaco-enable-motion .monaco-table > .monaco-split-view2,
|
||||
.monaco-enable-motion .monaco-table > .monaco-split-view2 .monaco-sash.vertical::before {
|
||||
transition: border-color 0.2s ease-out;
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
import { append, $, clearNode } from '../../dom.js';
|
||||
import { createStyleSheet } from '../../domStylesheets.js';
|
||||
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
|
||||
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
|
||||
import { List, unthemedListStyles } from '../list/listWidget.js';
|
||||
import { SplitView } from '../splitview/splitview.js';
|
||||
import { Event, Emitter } from '../../../common/event.js';
|
||||
import { DisposableStore, Disposable } from '../../../common/lifecycle.js';
|
||||
import './table.css';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
class TableListRenderer {
|
||||
static { this.TemplateId = 'row'; }
|
||||
constructor(columns, renderers, getColumnSize) {
|
||||
this.columns = columns;
|
||||
this.getColumnSize = getColumnSize;
|
||||
this.templateId = TableListRenderer.TemplateId;
|
||||
this.renderedTemplates = new Set();
|
||||
const rendererMap = new Map(renderers.map(r => [r.templateId, r]));
|
||||
this.renderers = [];
|
||||
for (const column of columns) {
|
||||
const renderer = rendererMap.get(column.templateId);
|
||||
if (!renderer) {
|
||||
throw new Error(`Table cell renderer for template id ${column.templateId} not found.`);
|
||||
}
|
||||
this.renderers.push(renderer);
|
||||
}
|
||||
}
|
||||
renderTemplate(container) {
|
||||
const rowContainer = append(container, $('.monaco-table-tr'));
|
||||
const cellContainers = [];
|
||||
const cellTemplateData = [];
|
||||
for (let i = 0; i < this.columns.length; i++) {
|
||||
const renderer = this.renderers[i];
|
||||
const cellContainer = append(rowContainer, $('.monaco-table-td', { 'data-col-index': i }));
|
||||
cellContainer.style.width = `${this.getColumnSize(i)}px`;
|
||||
cellContainers.push(cellContainer);
|
||||
cellTemplateData.push(renderer.renderTemplate(cellContainer));
|
||||
}
|
||||
const result = { container, cellContainers, cellTemplateData };
|
||||
this.renderedTemplates.add(result);
|
||||
return result;
|
||||
}
|
||||
renderElement(element, index, templateData, renderDetails) {
|
||||
for (let i = 0; i < this.columns.length; i++) {
|
||||
const column = this.columns[i];
|
||||
const cell = column.project(element);
|
||||
const renderer = this.renderers[i];
|
||||
renderer.renderElement(cell, index, templateData.cellTemplateData[i], renderDetails);
|
||||
}
|
||||
}
|
||||
disposeElement(element, index, templateData, renderDetails) {
|
||||
for (let i = 0; i < this.columns.length; i++) {
|
||||
const renderer = this.renderers[i];
|
||||
if (renderer.disposeElement) {
|
||||
const column = this.columns[i];
|
||||
const cell = column.project(element);
|
||||
renderer.disposeElement(cell, index, templateData.cellTemplateData[i], renderDetails);
|
||||
}
|
||||
}
|
||||
}
|
||||
disposeTemplate(templateData) {
|
||||
for (let i = 0; i < this.columns.length; i++) {
|
||||
const renderer = this.renderers[i];
|
||||
renderer.disposeTemplate(templateData.cellTemplateData[i]);
|
||||
}
|
||||
clearNode(templateData.container);
|
||||
this.renderedTemplates.delete(templateData);
|
||||
}
|
||||
layoutColumn(index, size) {
|
||||
for (const { cellContainers } of this.renderedTemplates) {
|
||||
cellContainers[index].style.width = `${size}px`;
|
||||
}
|
||||
}
|
||||
}
|
||||
function asListVirtualDelegate(delegate) {
|
||||
return {
|
||||
getHeight(row) { return delegate.getHeight(row); },
|
||||
getTemplateId() { return TableListRenderer.TemplateId; },
|
||||
};
|
||||
}
|
||||
class ColumnHeader extends Disposable {
|
||||
get minimumSize() { return this.column.minimumWidth ?? 120; }
|
||||
get maximumSize() { return this.column.maximumWidth ?? Number.POSITIVE_INFINITY; }
|
||||
get onDidChange() { return this.column.onDidChangeWidthConstraints ?? Event.None; }
|
||||
constructor(column, index) {
|
||||
super();
|
||||
this.column = column;
|
||||
this.index = index;
|
||||
this._onDidLayout = new Emitter();
|
||||
this.onDidLayout = this._onDidLayout.event;
|
||||
this.element = $('.monaco-table-th', { 'data-col-index': index }, column.label);
|
||||
if (column.tooltip) {
|
||||
this._register(getBaseLayerHoverDelegate().setupManagedHover(getDefaultHoverDelegate('mouse'), this.element, column.tooltip));
|
||||
}
|
||||
}
|
||||
layout(size) {
|
||||
this._onDidLayout.fire([this.index, size]);
|
||||
}
|
||||
}
|
||||
class Table {
|
||||
static { this.InstanceCount = 0; }
|
||||
get onDidChangeFocus() { return this.list.onDidChangeFocus; }
|
||||
get onDidChangeSelection() { return this.list.onDidChangeSelection; }
|
||||
get onDidScroll() { return this.list.onDidScroll; }
|
||||
get onMouseDblClick() { return this.list.onMouseDblClick; }
|
||||
get onPointer() { return this.list.onPointer; }
|
||||
get onDidFocus() { return this.list.onDidFocus; }
|
||||
get scrollTop() { return this.list.scrollTop; }
|
||||
set scrollTop(scrollTop) { this.list.scrollTop = scrollTop; }
|
||||
get scrollHeight() { return this.list.scrollHeight; }
|
||||
get renderHeight() { return this.list.renderHeight; }
|
||||
get onDidDispose() { return this.list.onDidDispose; }
|
||||
constructor(user, container, virtualDelegate, columns, renderers, _options) {
|
||||
this.virtualDelegate = virtualDelegate;
|
||||
this.columns = columns;
|
||||
this.domId = `table_id_${++Table.InstanceCount}`;
|
||||
this.disposables = new DisposableStore();
|
||||
this.cachedWidth = 0;
|
||||
this.cachedHeight = 0;
|
||||
this.domNode = append(container, $(`.monaco-table.${this.domId}`));
|
||||
const headers = columns.map((c, i) => this.disposables.add(new ColumnHeader(c, i)));
|
||||
const descriptor = {
|
||||
size: headers.reduce((a, b) => a + b.column.weight, 0),
|
||||
views: headers.map(view => ({ size: view.column.weight, view }))
|
||||
};
|
||||
this.splitview = this.disposables.add(new SplitView(this.domNode, {
|
||||
orientation: 1 /* Orientation.HORIZONTAL */,
|
||||
scrollbarVisibility: 2 /* ScrollbarVisibility.Hidden */,
|
||||
getSashOrthogonalSize: () => this.cachedHeight,
|
||||
descriptor
|
||||
}));
|
||||
this.splitview.el.style.height = `${virtualDelegate.headerRowHeight}px`;
|
||||
this.splitview.el.style.lineHeight = `${virtualDelegate.headerRowHeight}px`;
|
||||
const renderer = new TableListRenderer(columns, renderers, i => this.splitview.getViewSize(i));
|
||||
this.list = this.disposables.add(new List(user, this.domNode, asListVirtualDelegate(virtualDelegate), [renderer], _options));
|
||||
Event.any(...headers.map(h => h.onDidLayout))(([index, size]) => renderer.layoutColumn(index, size), null, this.disposables);
|
||||
this.splitview.onDidSashReset(index => {
|
||||
const totalWeight = columns.reduce((r, c) => r + c.weight, 0);
|
||||
const size = columns[index].weight / totalWeight * this.cachedWidth;
|
||||
this.splitview.resizeView(index, size);
|
||||
}, null, this.disposables);
|
||||
this.styleElement = createStyleSheet(this.domNode);
|
||||
this.style(unthemedListStyles);
|
||||
}
|
||||
updateOptions(options) {
|
||||
this.list.updateOptions(options);
|
||||
}
|
||||
splice(start, deleteCount, elements = []) {
|
||||
this.list.splice(start, deleteCount, elements);
|
||||
}
|
||||
getHTMLElement() {
|
||||
return this.domNode;
|
||||
}
|
||||
style(styles) {
|
||||
const content = [];
|
||||
content.push(`.monaco-table.${this.domId} > .monaco-split-view2 .monaco-sash.vertical::before {
|
||||
top: ${this.virtualDelegate.headerRowHeight + 1}px;
|
||||
height: calc(100% - ${this.virtualDelegate.headerRowHeight}px);
|
||||
}`);
|
||||
this.styleElement.textContent = content.join('\n');
|
||||
this.list.style(styles);
|
||||
}
|
||||
getSelectedElements() {
|
||||
return this.list.getSelectedElements();
|
||||
}
|
||||
getSelection() {
|
||||
return this.list.getSelection();
|
||||
}
|
||||
getFocus() {
|
||||
return this.list.getFocus();
|
||||
}
|
||||
dispose() {
|
||||
this.disposables.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
export { Table };
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-custom-toggle {
|
||||
margin-left: 2px;
|
||||
float: left;
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid transparent;
|
||||
padding: 1px;
|
||||
box-sizing: border-box;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
}
|
||||
|
||||
.monaco-custom-toggle:hover {
|
||||
background-color: var(--vscode-inputOption-hoverBackground);
|
||||
}
|
||||
|
||||
.hc-black .monaco-custom-toggle:hover,
|
||||
.hc-light .monaco-custom-toggle:hover {
|
||||
border: 1px dashed var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.hc-black .monaco-custom-toggle,
|
||||
.hc-light .monaco-custom-toggle {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.hc-black .monaco-custom-toggle:hover,
|
||||
.hc-light .monaco-custom-toggle:hover {
|
||||
background: none;
|
||||
}
|
||||
|
||||
.monaco-custom-toggle.monaco-checkbox {
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 3px;
|
||||
margin-right: 9px;
|
||||
margin-left: 0px;
|
||||
padding: 0px;
|
||||
opacity: 1;
|
||||
background-size: 16px !important;
|
||||
}
|
||||
|
||||
.monaco-action-bar .checkbox-action-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-radius: 2px;
|
||||
padding-right: 2px;
|
||||
}
|
||||
|
||||
.monaco-action-bar .checkbox-action-item:hover {
|
||||
background-color: var(--vscode-toolbar-hoverBackground);
|
||||
}
|
||||
|
||||
.monaco-action-bar .checkbox-action-item > .monaco-custom-toggle.monaco-checkbox {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.monaco-action-bar .checkbox-action-item > .checkbox-label {
|
||||
font-size: 12px;
|
||||
}
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
import { Codicon } from '../../../common/codicons.js';
|
||||
import { Emitter } from '../../../common/event.js';
|
||||
import { ThemeIcon } from '../../../common/themables.js';
|
||||
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
|
||||
import { Widget } from '../widget.js';
|
||||
import './toggle.css';
|
||||
|
||||
const unthemedToggleStyles = {
|
||||
inputActiveOptionBorder: '#007ACC00',
|
||||
inputActiveOptionForeground: '#FFFFFF',
|
||||
inputActiveOptionBackground: '#0E639C50'
|
||||
};
|
||||
class Toggle extends Widget {
|
||||
get onChange() { return this._onChange.event; }
|
||||
get onKeyDown() { return this._onKeyDown.event; }
|
||||
constructor(opts) {
|
||||
super();
|
||||
this._onChange = this._register(new Emitter());
|
||||
this._onKeyDown = this._register(new Emitter());
|
||||
this._opts = opts;
|
||||
this._title = this._opts.title;
|
||||
this._checked = this._opts.isChecked;
|
||||
const classes = ['monaco-custom-toggle'];
|
||||
if (this._opts.icon) {
|
||||
this._icon = this._opts.icon;
|
||||
classes.push(...ThemeIcon.asClassNameArray(this._icon));
|
||||
}
|
||||
if (this._opts.actionClassName) {
|
||||
classes.push(...this._opts.actionClassName.split(' '));
|
||||
}
|
||||
if (this._checked) {
|
||||
classes.push('checked');
|
||||
}
|
||||
this.domNode = document.createElement('div');
|
||||
this._register(getBaseLayerHoverDelegate().setupDelayedHover(this.domNode, () => ({
|
||||
content: this._title,
|
||||
style: 1 /* HoverStyle.Pointer */,
|
||||
}), this._opts.hoverLifecycleOptions));
|
||||
this.domNode.classList.add(...classes);
|
||||
if (!this._opts.notFocusable) {
|
||||
this.domNode.tabIndex = 0;
|
||||
}
|
||||
this.domNode.setAttribute('role', 'checkbox');
|
||||
this.domNode.setAttribute('aria-checked', String(this._checked));
|
||||
this.setTitle(this._opts.title);
|
||||
this.applyStyles();
|
||||
this.onclick(this.domNode, (ev) => {
|
||||
if (this.enabled) {
|
||||
this.checked = !this._checked;
|
||||
this._onChange.fire(false);
|
||||
ev.preventDefault();
|
||||
}
|
||||
});
|
||||
this._register(this.ignoreGesture(this.domNode));
|
||||
this.onkeydown(this.domNode, (keyboardEvent) => {
|
||||
if (!this.enabled) {
|
||||
return;
|
||||
}
|
||||
if (keyboardEvent.keyCode === 10 /* KeyCode.Space */ || keyboardEvent.keyCode === 3 /* KeyCode.Enter */) {
|
||||
this.checked = !this._checked;
|
||||
this._onChange.fire(true);
|
||||
keyboardEvent.preventDefault();
|
||||
keyboardEvent.stopPropagation();
|
||||
return;
|
||||
}
|
||||
this._onKeyDown.fire(keyboardEvent);
|
||||
});
|
||||
}
|
||||
get enabled() {
|
||||
return this.domNode.getAttribute('aria-disabled') !== 'true';
|
||||
}
|
||||
focus() {
|
||||
this.domNode.focus();
|
||||
}
|
||||
get checked() {
|
||||
return this._checked;
|
||||
}
|
||||
set checked(newIsChecked) {
|
||||
this._checked = newIsChecked;
|
||||
this.domNode.setAttribute('aria-checked', String(this._checked));
|
||||
this.domNode.classList.toggle('checked', this._checked);
|
||||
this.applyStyles();
|
||||
}
|
||||
setIcon(icon) {
|
||||
if (this._icon) {
|
||||
this.domNode.classList.remove(...ThemeIcon.asClassNameArray(this._icon));
|
||||
}
|
||||
this._icon = icon;
|
||||
if (this._icon) {
|
||||
this.domNode.classList.add(...ThemeIcon.asClassNameArray(this._icon));
|
||||
}
|
||||
}
|
||||
width() {
|
||||
return 2 /*margin left*/ + 2 /*border*/ + 2 /*padding*/ + 16 /* icon width */;
|
||||
}
|
||||
applyStyles() {
|
||||
if (this.domNode) {
|
||||
this.domNode.style.borderColor = (this._checked && this._opts.inputActiveOptionBorder) || '';
|
||||
this.domNode.style.color = (this._checked && this._opts.inputActiveOptionForeground) || 'inherit';
|
||||
this.domNode.style.backgroundColor = (this._checked && this._opts.inputActiveOptionBackground) || '';
|
||||
}
|
||||
}
|
||||
enable() {
|
||||
this.domNode.setAttribute('aria-disabled', String(false));
|
||||
this.domNode.classList.remove('disabled');
|
||||
}
|
||||
disable() {
|
||||
this.domNode.setAttribute('aria-disabled', String(true));
|
||||
this.domNode.classList.add('disabled');
|
||||
}
|
||||
setTitle(newTitle) {
|
||||
this._title = newTitle;
|
||||
this.domNode.setAttribute('aria-label', newTitle);
|
||||
}
|
||||
set visible(visible) {
|
||||
this.domNode.style.display = visible ? '' : 'none';
|
||||
}
|
||||
get visible() {
|
||||
return this.domNode.style.display !== 'none';
|
||||
}
|
||||
}
|
||||
class BaseCheckbox extends Widget {
|
||||
static { this.CLASS_NAME = 'monaco-checkbox'; }
|
||||
constructor(checkbox, domNode, styles) {
|
||||
super();
|
||||
this.checkbox = checkbox;
|
||||
this.domNode = domNode;
|
||||
this.styles = styles;
|
||||
this._onChange = this._register(new Emitter());
|
||||
this.onChange = this._onChange.event;
|
||||
this.applyStyles();
|
||||
}
|
||||
get enabled() {
|
||||
return this.checkbox.enabled;
|
||||
}
|
||||
enable() {
|
||||
this.checkbox.enable();
|
||||
this.applyStyles(true);
|
||||
}
|
||||
disable() {
|
||||
this.checkbox.disable();
|
||||
this.applyStyles(false);
|
||||
}
|
||||
setTitle(newTitle) {
|
||||
this.checkbox.setTitle(newTitle);
|
||||
}
|
||||
applyStyles(enabled = this.enabled) {
|
||||
this.domNode.style.color = (enabled ? this.styles.checkboxForeground : this.styles.checkboxDisabledForeground) || '';
|
||||
this.domNode.style.backgroundColor = (enabled ? this.styles.checkboxBackground : this.styles.checkboxDisabledBackground) || '';
|
||||
this.domNode.style.borderColor = (enabled ? this.styles.checkboxBorder : this.styles.checkboxDisabledBackground) || '';
|
||||
const size = this.styles.size || 18;
|
||||
this.domNode.style.width =
|
||||
this.domNode.style.height =
|
||||
this.domNode.style.fontSize = `${size}px`;
|
||||
this.domNode.style.fontSize = `${size - 2}px`;
|
||||
}
|
||||
}
|
||||
class Checkbox extends BaseCheckbox {
|
||||
constructor(title, isChecked, styles) {
|
||||
const toggle = new Toggle({ title, isChecked, icon: Codicon.check, actionClassName: BaseCheckbox.CLASS_NAME, hoverLifecycleOptions: styles.hoverLifecycleOptions, ...unthemedToggleStyles });
|
||||
super(toggle, toggle.domNode, styles);
|
||||
this._register(toggle);
|
||||
this._register(this.checkbox.onChange(keyboard => {
|
||||
this.applyStyles();
|
||||
this._onChange.fire(keyboard);
|
||||
}));
|
||||
}
|
||||
get checked() {
|
||||
return this.checkbox.checked;
|
||||
}
|
||||
set checked(newIsChecked) {
|
||||
this.checkbox.checked = newIsChecked;
|
||||
this.applyStyles();
|
||||
}
|
||||
applyStyles(enabled) {
|
||||
if (this.checkbox.checked) {
|
||||
this.checkbox.setIcon(Codicon.check);
|
||||
}
|
||||
else {
|
||||
this.checkbox.setIcon(undefined);
|
||||
}
|
||||
super.applyStyles(enabled);
|
||||
}
|
||||
}
|
||||
class TriStateCheckbox extends BaseCheckbox {
|
||||
constructor(title, _state, styles) {
|
||||
let icon;
|
||||
switch (_state) {
|
||||
case true:
|
||||
icon = Codicon.check;
|
||||
break;
|
||||
case 'mixed':
|
||||
icon = Codicon.dash;
|
||||
break;
|
||||
case false:
|
||||
icon = undefined;
|
||||
break;
|
||||
}
|
||||
const checkbox = new Toggle({
|
||||
title,
|
||||
isChecked: _state === true,
|
||||
icon,
|
||||
actionClassName: Checkbox.CLASS_NAME,
|
||||
hoverLifecycleOptions: styles.hoverLifecycleOptions,
|
||||
...unthemedToggleStyles
|
||||
});
|
||||
super(checkbox, checkbox.domNode, styles);
|
||||
this._state = _state;
|
||||
this._register(checkbox);
|
||||
this._register(this.checkbox.onChange(keyboard => {
|
||||
this._state = this.checkbox.checked;
|
||||
this.applyStyles();
|
||||
this._onChange.fire(keyboard);
|
||||
}));
|
||||
}
|
||||
get checked() {
|
||||
return this._state;
|
||||
}
|
||||
set checked(newState) {
|
||||
if (this._state !== newState) {
|
||||
this._state = newState;
|
||||
this.checkbox.checked = newState === true;
|
||||
this.applyStyles();
|
||||
}
|
||||
}
|
||||
applyStyles(enabled) {
|
||||
switch (this._state) {
|
||||
case true:
|
||||
this.checkbox.setIcon(Codicon.check);
|
||||
break;
|
||||
case 'mixed':
|
||||
this.checkbox.setIcon(Codicon.dash);
|
||||
break;
|
||||
case false:
|
||||
this.checkbox.setIcon(undefined);
|
||||
break;
|
||||
}
|
||||
super.applyStyles(enabled);
|
||||
}
|
||||
}
|
||||
|
||||
export { Checkbox, Toggle, TriStateCheckbox, unthemedToggleStyles };
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.monaco-toolbar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.monaco-toolbar .toolbar-toggle-more {
|
||||
display: inline-block;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.monaco-toolbar.responsive {
|
||||
.monaco-action-bar > .actions-container > .action-item {
|
||||
flex-shrink: 1;
|
||||
min-width: 20px;
|
||||
}
|
||||
}
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
import { ActionBar } from '../actionbar/actionbar.js';
|
||||
import { DropdownMenuActionViewItem } from '../dropdown/dropdownActionViewItem.js';
|
||||
import { Action, SubmenuAction, Separator } from '../../../common/actions.js';
|
||||
import { Codicon } from '../../../common/codicons.js';
|
||||
import { ThemeIcon } from '../../../common/themables.js';
|
||||
import { EventMultiplexer } from '../../../common/event.js';
|
||||
import { Disposable, DisposableStore, toDisposable } from '../../../common/lifecycle.js';
|
||||
import './toolbar.css';
|
||||
import { localize } from '../../../../nls.js';
|
||||
import { createInstantHoverDelegate } from '../hover/hoverDelegateFactory.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
const ACTION_MIN_WIDTH = 24; /* 20px codicon + 4px left padding*/
|
||||
/**
|
||||
* A widget that combines an action bar for primary actions and a dropdown for secondary actions.
|
||||
*/
|
||||
class ToolBar extends Disposable {
|
||||
get onDidChangeDropdownVisibility() { return this._onDidChangeDropdownVisibility.event; }
|
||||
constructor(container, contextMenuProvider, options = { orientation: 0 /* ActionsOrientation.HORIZONTAL */ }) {
|
||||
super();
|
||||
this.submenuActionViewItems = [];
|
||||
this.hasSecondaryActions = false;
|
||||
this._onDidChangeDropdownVisibility = this._register(new EventMultiplexer());
|
||||
this.originalPrimaryActions = [];
|
||||
this.originalSecondaryActions = [];
|
||||
this.hiddenActions = [];
|
||||
this.disposables = this._register(new DisposableStore());
|
||||
options.hoverDelegate = options.hoverDelegate ?? this._register(createInstantHoverDelegate());
|
||||
this.options = options;
|
||||
this.toggleMenuAction = this._register(new ToggleMenuAction(() => this.toggleMenuActionViewItem?.show(), options.toggleMenuTitle));
|
||||
this.element = document.createElement('div');
|
||||
this.element.className = 'monaco-toolbar';
|
||||
container.appendChild(this.element);
|
||||
this.actionBar = this._register(new ActionBar(this.element, {
|
||||
orientation: options.orientation,
|
||||
ariaLabel: options.ariaLabel,
|
||||
actionRunner: options.actionRunner,
|
||||
allowContextMenu: options.allowContextMenu,
|
||||
highlightToggledItems: options.highlightToggledItems,
|
||||
hoverDelegate: options.hoverDelegate,
|
||||
actionViewItemProvider: (action, viewItemOptions) => {
|
||||
if (action.id === ToggleMenuAction.ID) {
|
||||
this.toggleMenuActionViewItem = new DropdownMenuActionViewItem(action, { getActions: () => this.toggleMenuAction.menuActions }, contextMenuProvider, {
|
||||
actionViewItemProvider: this.options.actionViewItemProvider,
|
||||
actionRunner: this.actionRunner,
|
||||
keybindingProvider: this.options.getKeyBinding,
|
||||
classNames: ThemeIcon.asClassNameArray(options.moreIcon ?? Codicon.toolBarMore),
|
||||
anchorAlignmentProvider: this.options.anchorAlignmentProvider,
|
||||
menuAsChild: !!this.options.renderDropdownAsChildElement,
|
||||
skipTelemetry: this.options.skipTelemetry,
|
||||
isMenu: true,
|
||||
hoverDelegate: this.options.hoverDelegate
|
||||
});
|
||||
this.toggleMenuActionViewItem.setActionContext(this.actionBar.context);
|
||||
this.disposables.add(this._onDidChangeDropdownVisibility.add(this.toggleMenuActionViewItem.onDidChangeVisibility));
|
||||
return this.toggleMenuActionViewItem;
|
||||
}
|
||||
if (options.actionViewItemProvider) {
|
||||
const result = options.actionViewItemProvider(action, viewItemOptions);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (action instanceof SubmenuAction) {
|
||||
const result = new DropdownMenuActionViewItem(action, action.actions, contextMenuProvider, {
|
||||
actionViewItemProvider: this.options.actionViewItemProvider,
|
||||
actionRunner: this.actionRunner,
|
||||
keybindingProvider: this.options.getKeyBinding,
|
||||
classNames: action.class,
|
||||
anchorAlignmentProvider: this.options.anchorAlignmentProvider,
|
||||
menuAsChild: !!this.options.renderDropdownAsChildElement,
|
||||
skipTelemetry: this.options.skipTelemetry,
|
||||
hoverDelegate: this.options.hoverDelegate
|
||||
});
|
||||
result.setActionContext(this.actionBar.context);
|
||||
this.submenuActionViewItems.push(result);
|
||||
this.disposables.add(this._onDidChangeDropdownVisibility.add(result.onDidChangeVisibility));
|
||||
return result;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}));
|
||||
// Responsive support
|
||||
if (this.options.responsive) {
|
||||
this.element.classList.add('responsive');
|
||||
const observer = new ResizeObserver(() => {
|
||||
this.setToolbarMaxWidth(this.element.getBoundingClientRect().width);
|
||||
});
|
||||
observer.observe(this.element);
|
||||
this._store.add(toDisposable(() => observer.disconnect()));
|
||||
}
|
||||
}
|
||||
set actionRunner(actionRunner) {
|
||||
this.actionBar.actionRunner = actionRunner;
|
||||
}
|
||||
get actionRunner() {
|
||||
return this.actionBar.actionRunner;
|
||||
}
|
||||
set context(context) {
|
||||
this.actionBar.context = context;
|
||||
this.toggleMenuActionViewItem?.setActionContext(context);
|
||||
for (const actionViewItem of this.submenuActionViewItems) {
|
||||
actionViewItem.setActionContext(context);
|
||||
}
|
||||
}
|
||||
getElement() {
|
||||
return this.element;
|
||||
}
|
||||
getItemAction(indexOrElement) {
|
||||
return this.actionBar.getAction(indexOrElement);
|
||||
}
|
||||
getItemWidth(index) {
|
||||
return this.actionBar.getWidth(index);
|
||||
}
|
||||
setActions(primaryActions, secondaryActions) {
|
||||
this.clear();
|
||||
// Store primary and secondary actions as rendered initially
|
||||
this.originalPrimaryActions = primaryActions ? primaryActions.slice(0) : [];
|
||||
this.originalSecondaryActions = secondaryActions ? secondaryActions.slice(0) : [];
|
||||
const primaryActionsToSet = primaryActions ? primaryActions.slice(0) : [];
|
||||
// Inject additional action to open secondary actions if present
|
||||
this.hasSecondaryActions = !!(secondaryActions && secondaryActions.length > 0);
|
||||
if (this.hasSecondaryActions && secondaryActions) {
|
||||
this.toggleMenuAction.menuActions = secondaryActions.slice(0);
|
||||
primaryActionsToSet.push(this.toggleMenuAction);
|
||||
}
|
||||
if (primaryActionsToSet.length > 0 && this.options.trailingSeparator) {
|
||||
primaryActionsToSet.push(new Separator());
|
||||
}
|
||||
primaryActionsToSet.forEach(action => {
|
||||
this.actionBar.push(action, { icon: this.options.icon ?? true, label: this.options.label ?? false, keybinding: this.getKeybindingLabel(action) });
|
||||
});
|
||||
if (this.options.responsive) {
|
||||
// Reset hidden actions
|
||||
this.hiddenActions.length = 0;
|
||||
// Update toolbar to fit with container width
|
||||
this.setToolbarMaxWidth(this.element.getBoundingClientRect().width);
|
||||
}
|
||||
}
|
||||
getKeybindingLabel(action) {
|
||||
const key = this.options.getKeyBinding?.(action);
|
||||
return key?.getLabel() ?? undefined;
|
||||
}
|
||||
getItemsWidthResponsive() {
|
||||
// Each action is assumed to have a minimum width so that actions with a label
|
||||
// can shrink to the action's minimum width. We do this so that action visibility
|
||||
// takes precedence over the action label.
|
||||
return this.actionBar.length() * ACTION_MIN_WIDTH;
|
||||
}
|
||||
setToolbarMaxWidth(maxWidth) {
|
||||
if (this.actionBar.isEmpty() ||
|
||||
(this.getItemsWidthResponsive() <= maxWidth && this.hiddenActions.length === 0)) {
|
||||
return;
|
||||
}
|
||||
if (this.getItemsWidthResponsive() > maxWidth) {
|
||||
// Hide actions from the right
|
||||
while (this.getItemsWidthResponsive() > maxWidth && this.actionBar.length() > 0) {
|
||||
const index = this.originalPrimaryActions.length - this.hiddenActions.length - 1;
|
||||
if (index < 0) {
|
||||
break;
|
||||
}
|
||||
// Store the action and its size
|
||||
const size = Math.min(ACTION_MIN_WIDTH, this.getItemWidth(index));
|
||||
const action = this.originalPrimaryActions[index];
|
||||
this.hiddenActions.unshift({ action, size });
|
||||
// Remove the action
|
||||
this.actionBar.pull(index);
|
||||
// There are no secondary actions, but we have actions that we need to hide so we
|
||||
// create the overflow menu. This will ensure that another primary action will be
|
||||
// removed making space for the overflow menu.
|
||||
if (this.originalSecondaryActions.length === 0 && this.hiddenActions.length === 1) {
|
||||
this.actionBar.push(this.toggleMenuAction, {
|
||||
icon: this.options.icon ?? true,
|
||||
label: this.options.label ?? false,
|
||||
keybinding: this.getKeybindingLabel(this.toggleMenuAction),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Show actions from the top of the toggle menu
|
||||
while (this.hiddenActions.length > 0) {
|
||||
const entry = this.hiddenActions.shift();
|
||||
if (this.getItemsWidthResponsive() + entry.size > maxWidth) {
|
||||
// Not enough space to show the action
|
||||
this.hiddenActions.unshift(entry);
|
||||
break;
|
||||
}
|
||||
// Add the action
|
||||
this.actionBar.push(entry.action, {
|
||||
icon: this.options.icon ?? true,
|
||||
label: this.options.label ?? false,
|
||||
keybinding: this.getKeybindingLabel(entry.action),
|
||||
index: this.originalPrimaryActions.length - this.hiddenActions.length - 1
|
||||
});
|
||||
// There are no secondary actions, and there is only one hidden item left so we
|
||||
// remove the overflow menu making space for the last hidden action to be shown.
|
||||
if (this.originalSecondaryActions.length === 0 && this.hiddenActions.length === 1) {
|
||||
this.toggleMenuAction.menuActions = [];
|
||||
this.actionBar.pull(this.actionBar.length() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Update overflow menu
|
||||
const hiddenActions = this.hiddenActions.map(entry => entry.action);
|
||||
if (this.originalSecondaryActions.length > 0 || hiddenActions.length > 0) {
|
||||
const secondaryActions = this.originalSecondaryActions.slice(0);
|
||||
this.toggleMenuAction.menuActions = Separator.join(hiddenActions, secondaryActions);
|
||||
}
|
||||
}
|
||||
clear() {
|
||||
this.submenuActionViewItems = [];
|
||||
this.disposables.clear();
|
||||
this.actionBar.clear();
|
||||
}
|
||||
dispose() {
|
||||
this.clear();
|
||||
this.disposables.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
class ToggleMenuAction extends Action {
|
||||
static { this.ID = 'toolbar.toggle.more'; }
|
||||
constructor(toggleDropdownMenu, title) {
|
||||
title = title || localize(17, "More Actions...");
|
||||
super(ToggleMenuAction.ID, title, undefined, true);
|
||||
this._menuActions = [];
|
||||
this.toggleDropdownMenu = toggleDropdownMenu;
|
||||
}
|
||||
async run() {
|
||||
this.toggleDropdownMenu();
|
||||
}
|
||||
get menuActions() {
|
||||
return this._menuActions;
|
||||
}
|
||||
set menuActions(actions) {
|
||||
this._menuActions = actions;
|
||||
}
|
||||
}
|
||||
|
||||
export { ToggleMenuAction, ToolBar };
|
||||
+2027
File diff suppressed because it is too large
Load Diff
+946
@@ -0,0 +1,946 @@
|
||||
import { ElementsDragAndDropData } from '../list/listView.js';
|
||||
import { ComposedTreeDelegate, FindFilter, TreeFindMode, FindController } from './abstractTree.js';
|
||||
import { isFilterResult, getVisibleState } from './indexTreeModel.js';
|
||||
import { ObjectTree, CompressibleObjectTree } from './objectTree.js';
|
||||
import { WeakMapper, ObjectTreeElementCollapseState, TreeError } from './tree.js';
|
||||
import { createCancelablePromise, Promises, timeout } from '../../../common/async.js';
|
||||
import { Codicon } from '../../../common/codicons.js';
|
||||
import { ThemeIcon } from '../../../common/themables.js';
|
||||
import { isCancellationError, onUnexpectedError } from '../../../common/errors.js';
|
||||
import { Event, Emitter } from '../../../common/event.js';
|
||||
import { Iterable } from '../../../common/iterator.js';
|
||||
import { DisposableStore, toDisposable, dispose } from '../../../common/lifecycle.js';
|
||||
import { isIterable } from '../../../common/types.js';
|
||||
import { FuzzyScore } from '../../../common/filters.js';
|
||||
import { splice } from '../../../common/arrays.js';
|
||||
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
function createAsyncDataTreeNode(props) {
|
||||
return {
|
||||
...props,
|
||||
children: [],
|
||||
refreshPromise: undefined,
|
||||
stale: true,
|
||||
slow: false,
|
||||
forceExpanded: false
|
||||
};
|
||||
}
|
||||
function isAncestor(ancestor, descendant) {
|
||||
if (!descendant.parent) {
|
||||
return false;
|
||||
}
|
||||
else if (descendant.parent === ancestor) {
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
return isAncestor(ancestor, descendant.parent);
|
||||
}
|
||||
}
|
||||
function intersects(node, other) {
|
||||
return node === other || isAncestor(node, other) || isAncestor(other, node);
|
||||
}
|
||||
class AsyncDataTreeNodeWrapper {
|
||||
get element() { return this.node.element.element; }
|
||||
get children() { return this.node.children.map(node => new AsyncDataTreeNodeWrapper(node)); }
|
||||
get depth() { return this.node.depth; }
|
||||
get visibleChildrenCount() { return this.node.visibleChildrenCount; }
|
||||
get visibleChildIndex() { return this.node.visibleChildIndex; }
|
||||
get collapsible() { return this.node.collapsible; }
|
||||
get collapsed() { return this.node.collapsed; }
|
||||
get visible() { return this.node.visible; }
|
||||
get filterData() { return this.node.filterData; }
|
||||
constructor(node) {
|
||||
this.node = node;
|
||||
}
|
||||
}
|
||||
class AsyncDataTreeRenderer {
|
||||
constructor(renderer, nodeMapper, onDidChangeTwistieState) {
|
||||
this.renderer = renderer;
|
||||
this.nodeMapper = nodeMapper;
|
||||
this.onDidChangeTwistieState = onDidChangeTwistieState;
|
||||
this.renderedNodes = new Map();
|
||||
this.templateId = renderer.templateId;
|
||||
}
|
||||
renderTemplate(container) {
|
||||
const templateData = this.renderer.renderTemplate(container);
|
||||
return { templateData };
|
||||
}
|
||||
renderElement(node, index, templateData, details) {
|
||||
this.renderer.renderElement(this.nodeMapper.map(node), index, templateData.templateData, details);
|
||||
}
|
||||
renderTwistie(element, twistieElement) {
|
||||
if (element.slow) {
|
||||
twistieElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.treeItemLoading));
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
twistieElement.classList.remove(...ThemeIcon.asClassNameArray(Codicon.treeItemLoading));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
disposeElement(node, index, templateData, details) {
|
||||
this.renderer.disposeElement?.(this.nodeMapper.map(node), index, templateData.templateData, details);
|
||||
}
|
||||
disposeTemplate(templateData) {
|
||||
this.renderer.disposeTemplate(templateData.templateData);
|
||||
}
|
||||
dispose() {
|
||||
this.renderedNodes.clear();
|
||||
}
|
||||
}
|
||||
function asTreeEvent(e) {
|
||||
return {
|
||||
browserEvent: e.browserEvent,
|
||||
elements: e.elements.map(e => e.element)
|
||||
};
|
||||
}
|
||||
function asTreeMouseEvent(e) {
|
||||
return {
|
||||
browserEvent: e.browserEvent,
|
||||
element: e.element && e.element.element,
|
||||
target: e.target
|
||||
};
|
||||
}
|
||||
class AsyncDataTreeElementsDragAndDropData extends ElementsDragAndDropData {
|
||||
constructor(data) {
|
||||
super(data.elements.map(node => node.element));
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
function asAsyncDataTreeDragAndDropData(data) {
|
||||
if (data instanceof ElementsDragAndDropData) {
|
||||
return new AsyncDataTreeElementsDragAndDropData(data);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
class AsyncDataTreeNodeListDragAndDrop {
|
||||
constructor(dnd) {
|
||||
this.dnd = dnd;
|
||||
}
|
||||
getDragURI(node) {
|
||||
return this.dnd.getDragURI(node.element);
|
||||
}
|
||||
getDragLabel(nodes, originalEvent) {
|
||||
if (this.dnd.getDragLabel) {
|
||||
return this.dnd.getDragLabel(nodes.map(node => node.element), originalEvent);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
onDragStart(data, originalEvent) {
|
||||
this.dnd.onDragStart?.(asAsyncDataTreeDragAndDropData(data), originalEvent);
|
||||
}
|
||||
onDragOver(data, targetNode, targetIndex, targetSector, originalEvent, raw = true) {
|
||||
return this.dnd.onDragOver(asAsyncDataTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, targetSector, originalEvent);
|
||||
}
|
||||
drop(data, targetNode, targetIndex, targetSector, originalEvent) {
|
||||
this.dnd.drop(asAsyncDataTreeDragAndDropData(data), targetNode && targetNode.element, targetIndex, targetSector, originalEvent);
|
||||
}
|
||||
onDragEnd(originalEvent) {
|
||||
this.dnd.onDragEnd?.(originalEvent);
|
||||
}
|
||||
dispose() {
|
||||
this.dnd.dispose();
|
||||
}
|
||||
}
|
||||
class AsyncFindFilter extends FindFilter {
|
||||
constructor(findProvider, // remove public
|
||||
keyboardNavigationLabelProvider, filter) {
|
||||
super(keyboardNavigationLabelProvider, filter);
|
||||
this.findProvider = findProvider;
|
||||
this.isFindSessionActive = false;
|
||||
}
|
||||
filter(element, parentVisibility) {
|
||||
const filterResult = super.filter(element, parentVisibility);
|
||||
if (!this.isFindSessionActive || this.findMode === TreeFindMode.Highlight || !this.findProvider.isVisible) {
|
||||
return filterResult;
|
||||
}
|
||||
const visibility = isFilterResult(filterResult) ? filterResult.visibility : filterResult;
|
||||
if (getVisibleState(visibility) === 0 /* TreeVisibility.Hidden */) {
|
||||
return 0 /* TreeVisibility.Hidden */;
|
||||
}
|
||||
return this.findProvider.isVisible(element) ? filterResult : 0 /* TreeVisibility.Hidden */;
|
||||
}
|
||||
}
|
||||
// TODO Fix types
|
||||
class AsyncFindController extends FindController {
|
||||
constructor(tree, findProvider, filter, contextViewProvider, options) {
|
||||
super(tree, filter, contextViewProvider, options);
|
||||
this.findProvider = findProvider;
|
||||
this.filter = filter;
|
||||
this.activeSession = false;
|
||||
this.asyncWorkInProgress = false;
|
||||
// Always make sure to end the session before disposing
|
||||
this.disposables.add(toDisposable(async () => {
|
||||
if (this.activeSession) {
|
||||
await this.findProvider.endSession?.();
|
||||
}
|
||||
}));
|
||||
}
|
||||
render() {
|
||||
if (this.asyncWorkInProgress || !this.activeFindMetadata) {
|
||||
return;
|
||||
}
|
||||
const showNotFound = this.activeFindMetadata.matchCount === 0 && this.pattern.length > 0;
|
||||
this.renderMessage(showNotFound);
|
||||
if (this.pattern.length) {
|
||||
this.alertResults(this.activeFindMetadata.matchCount);
|
||||
}
|
||||
}
|
||||
shouldAllowFocus(node) {
|
||||
return this.shouldFocusWhenNavigating(node);
|
||||
}
|
||||
shouldFocusWhenNavigating(node) {
|
||||
if (!this.activeSession || !this.activeFindMetadata) {
|
||||
return true;
|
||||
}
|
||||
const element = node.element?.element;
|
||||
if (element && this.activeFindMetadata.isMatch(element)) {
|
||||
return true;
|
||||
}
|
||||
return !FuzzyScore.isDefault(node.filterData);
|
||||
}
|
||||
}
|
||||
function asObjectTreeOptions(options) {
|
||||
return options && {
|
||||
...options,
|
||||
collapseByDefault: true,
|
||||
identityProvider: options.identityProvider && {
|
||||
getId(el) {
|
||||
return options.identityProvider.getId(el.element);
|
||||
}
|
||||
},
|
||||
dnd: options.dnd && new AsyncDataTreeNodeListDragAndDrop(options.dnd),
|
||||
multipleSelectionController: options.multipleSelectionController && {
|
||||
isSelectionSingleChangeEvent(e) {
|
||||
// eslint-disable-next-line local/code-no-dangerous-type-assertions
|
||||
return options.multipleSelectionController.isSelectionSingleChangeEvent({ ...e, element: e.element });
|
||||
},
|
||||
isSelectionRangeChangeEvent(e) {
|
||||
// eslint-disable-next-line local/code-no-dangerous-type-assertions
|
||||
return options.multipleSelectionController.isSelectionRangeChangeEvent({ ...e, element: e.element });
|
||||
}
|
||||
},
|
||||
accessibilityProvider: options.accessibilityProvider && {
|
||||
...options.accessibilityProvider,
|
||||
getPosInSet: undefined,
|
||||
getSetSize: undefined,
|
||||
getRole: options.accessibilityProvider.getRole ? (el) => {
|
||||
return options.accessibilityProvider.getRole(el.element);
|
||||
} : () => 'treeitem',
|
||||
isChecked: options.accessibilityProvider.isChecked ? (e) => {
|
||||
return !!(options.accessibilityProvider?.isChecked(e.element));
|
||||
} : undefined,
|
||||
getAriaLabel(e) {
|
||||
return options.accessibilityProvider.getAriaLabel(e.element);
|
||||
},
|
||||
getWidgetAriaLabel() {
|
||||
return options.accessibilityProvider.getWidgetAriaLabel();
|
||||
},
|
||||
getWidgetRole: options.accessibilityProvider.getWidgetRole ? () => options.accessibilityProvider.getWidgetRole() : () => 'tree',
|
||||
getAriaLevel: options.accessibilityProvider.getAriaLevel && (node => {
|
||||
return options.accessibilityProvider.getAriaLevel(node.element);
|
||||
}),
|
||||
getActiveDescendantId: options.accessibilityProvider.getActiveDescendantId && (node => {
|
||||
return options.accessibilityProvider.getActiveDescendantId(node.element);
|
||||
})
|
||||
},
|
||||
filter: options.filter && {
|
||||
filter(e, parentVisibility) {
|
||||
return options.filter.filter(e.element, parentVisibility);
|
||||
}
|
||||
},
|
||||
keyboardNavigationLabelProvider: options.keyboardNavigationLabelProvider && {
|
||||
...options.keyboardNavigationLabelProvider,
|
||||
getKeyboardNavigationLabel(e) {
|
||||
return options.keyboardNavigationLabelProvider.getKeyboardNavigationLabel(e.element);
|
||||
}
|
||||
},
|
||||
sorter: undefined,
|
||||
expandOnlyOnTwistieClick: typeof options.expandOnlyOnTwistieClick === 'undefined' ? undefined : (typeof options.expandOnlyOnTwistieClick !== 'function' ? options.expandOnlyOnTwistieClick : ((e) => options.expandOnlyOnTwistieClick(e.element))),
|
||||
defaultFindVisibility: (e) => {
|
||||
if (e.hasChildren && e.stale) {
|
||||
return 1 /* TreeVisibility.Visible */;
|
||||
}
|
||||
else if (typeof options.defaultFindVisibility === 'number') {
|
||||
return options.defaultFindVisibility;
|
||||
}
|
||||
else if (typeof options.defaultFindVisibility === 'undefined') {
|
||||
return 2 /* TreeVisibility.Recurse */;
|
||||
}
|
||||
else {
|
||||
return options.defaultFindVisibility(e.element);
|
||||
}
|
||||
},
|
||||
stickyScrollDelegate: options.stickyScrollDelegate
|
||||
};
|
||||
}
|
||||
function dfs(node, fn) {
|
||||
fn(node);
|
||||
node.children.forEach(child => dfs(child, fn));
|
||||
}
|
||||
class AsyncDataTree {
|
||||
get onDidScroll() { return this.tree.onDidScroll; }
|
||||
get onDidChangeFocus() { return Event.map(this.tree.onDidChangeFocus, asTreeEvent); }
|
||||
get onDidChangeSelection() { return Event.map(this.tree.onDidChangeSelection, asTreeEvent); }
|
||||
get onMouseDblClick() { return Event.map(this.tree.onMouseDblClick, asTreeMouseEvent); }
|
||||
get onPointer() { return Event.map(this.tree.onPointer, asTreeMouseEvent); }
|
||||
get onDidFocus() { return this.tree.onDidFocus; }
|
||||
/**
|
||||
* To be used internally only!
|
||||
* @deprecated
|
||||
*/
|
||||
get onDidChangeModel() { return this.tree.onDidChangeModel; }
|
||||
get onDidChangeCollapseState() { return this.tree.onDidChangeCollapseState; }
|
||||
get onDidChangeStickyScrollFocused() { return this.tree.onDidChangeStickyScrollFocused; }
|
||||
get onDidDispose() { return this.tree.onDidDispose; }
|
||||
constructor(user, container, delegate, renderers, dataSource, options = {}) {
|
||||
this.user = user;
|
||||
this.dataSource = dataSource;
|
||||
this.nodes = new Map();
|
||||
this.subTreeRefreshPromises = new Map();
|
||||
this.refreshPromises = new Map();
|
||||
this._onDidRender = new Emitter();
|
||||
this._onDidChangeNodeSlowState = new Emitter();
|
||||
this.nodeMapper = new WeakMapper(node => new AsyncDataTreeNodeWrapper(node));
|
||||
this.disposables = new DisposableStore();
|
||||
this.identityProvider = options.identityProvider;
|
||||
this.autoExpandSingleChildren = typeof options.autoExpandSingleChildren === 'undefined' ? false : options.autoExpandSingleChildren;
|
||||
this.sorter = options.sorter;
|
||||
this.getDefaultCollapseState = e => options.collapseByDefault ? (options.collapseByDefault(e) ? ObjectTreeElementCollapseState.PreserveOrCollapsed : ObjectTreeElementCollapseState.PreserveOrExpanded) : undefined;
|
||||
let asyncFindEnabled = false;
|
||||
let findFilter;
|
||||
if (options.findProvider && (options.findWidgetEnabled ?? true) && options.keyboardNavigationLabelProvider && options.contextViewProvider) {
|
||||
asyncFindEnabled = true;
|
||||
findFilter = new AsyncFindFilter(options.findProvider, options.keyboardNavigationLabelProvider, options.filter);
|
||||
}
|
||||
this.tree = this.createTree(user, container, delegate, renderers, { ...options, findWidgetEnabled: !asyncFindEnabled, filter: findFilter ?? options.filter });
|
||||
this.root = createAsyncDataTreeNode({
|
||||
element: undefined,
|
||||
parent: null,
|
||||
hasChildren: true,
|
||||
defaultCollapseState: undefined
|
||||
});
|
||||
if (this.identityProvider) {
|
||||
this.root = {
|
||||
...this.root,
|
||||
id: null
|
||||
};
|
||||
}
|
||||
this.nodes.set(null, this.root);
|
||||
this.tree.onDidChangeCollapseState(this._onDidChangeCollapseState, this, this.disposables);
|
||||
if (asyncFindEnabled) {
|
||||
const findOptions = {
|
||||
styles: options.findWidgetStyles,
|
||||
showNotFoundMessage: options.showNotFoundMessage,
|
||||
defaultFindMatchType: options.defaultFindMatchType,
|
||||
defaultFindMode: options.defaultFindMode,
|
||||
};
|
||||
this.findController = this.disposables.add(new AsyncFindController(this.tree, options.findProvider, findFilter, this.tree.options.contextViewProvider, findOptions));
|
||||
this.focusNavigationFilter = node => this.findController.shouldFocusWhenNavigating(node);
|
||||
this.onDidChangeFindOpenState = this.findController.onDidChangeOpenState;
|
||||
this.onDidChangeFindMode = this.findController.onDidChangeMode;
|
||||
this.onDidChangeFindMatchType = this.findController.onDidChangeMatchType;
|
||||
}
|
||||
else {
|
||||
this.onDidChangeFindOpenState = this.tree.onDidChangeFindOpenState;
|
||||
this.onDidChangeFindMode = this.tree.onDidChangeFindMode;
|
||||
this.onDidChangeFindMatchType = this.tree.onDidChangeFindMatchType;
|
||||
}
|
||||
}
|
||||
createTree(user, container, delegate, renderers, options) {
|
||||
const objectTreeDelegate = new ComposedTreeDelegate(delegate);
|
||||
const objectTreeRenderers = renderers.map(r => new AsyncDataTreeRenderer(r, this.nodeMapper, this._onDidChangeNodeSlowState.event));
|
||||
const objectTreeOptions = asObjectTreeOptions(options) || {};
|
||||
return new ObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions);
|
||||
}
|
||||
updateOptions(optionsUpdate = {}) {
|
||||
if (this.findController) {
|
||||
if (optionsUpdate.defaultFindMode !== undefined) {
|
||||
this.findController.mode = optionsUpdate.defaultFindMode;
|
||||
}
|
||||
if (optionsUpdate.defaultFindMatchType !== undefined) {
|
||||
this.findController.matchType = optionsUpdate.defaultFindMatchType;
|
||||
}
|
||||
}
|
||||
this.tree.updateOptions(optionsUpdate);
|
||||
}
|
||||
// Widget
|
||||
getHTMLElement() {
|
||||
return this.tree.getHTMLElement();
|
||||
}
|
||||
get scrollTop() {
|
||||
return this.tree.scrollTop;
|
||||
}
|
||||
set scrollTop(scrollTop) {
|
||||
this.tree.scrollTop = scrollTop;
|
||||
}
|
||||
get scrollHeight() {
|
||||
return this.tree.scrollHeight;
|
||||
}
|
||||
get renderHeight() {
|
||||
return this.tree.renderHeight;
|
||||
}
|
||||
domFocus() {
|
||||
this.tree.domFocus();
|
||||
}
|
||||
layout(height, width) {
|
||||
this.tree.layout(height, width);
|
||||
}
|
||||
style(styles) {
|
||||
this.tree.style(styles);
|
||||
}
|
||||
// Model
|
||||
getInput() {
|
||||
return this.root.element;
|
||||
}
|
||||
async setInput(input, viewState) {
|
||||
this.cancelAllRefreshPromises();
|
||||
this.root.element = input;
|
||||
const viewStateContext = viewState && { viewState, focus: [], selection: [] };
|
||||
await this._updateChildren(input, true, false, viewStateContext);
|
||||
if (viewStateContext) {
|
||||
this.tree.setFocus(viewStateContext.focus);
|
||||
this.tree.setSelection(viewStateContext.selection);
|
||||
}
|
||||
if (viewState && typeof viewState.scrollTop === 'number') {
|
||||
this.scrollTop = viewState.scrollTop;
|
||||
}
|
||||
}
|
||||
cancelAllRefreshPromises(includeSubTrees = false) {
|
||||
this.refreshPromises.forEach(promise => promise.cancel());
|
||||
this.refreshPromises.clear();
|
||||
if (includeSubTrees) {
|
||||
this.subTreeRefreshPromises.forEach(promise => promise.cancel());
|
||||
this.subTreeRefreshPromises.clear();
|
||||
}
|
||||
}
|
||||
async _updateChildren(element = this.root.element, recursive = true, rerender = false, viewStateContext, options) {
|
||||
if (typeof this.root.element === 'undefined') {
|
||||
throw new TreeError(this.user, 'Tree input not set');
|
||||
}
|
||||
if (this.root.refreshPromise) {
|
||||
await this.root.refreshPromise;
|
||||
await Event.toPromise(this._onDidRender.event);
|
||||
}
|
||||
const node = this.getDataNode(element);
|
||||
await this.refreshAndRenderNode(node, recursive, viewStateContext, options);
|
||||
if (rerender) {
|
||||
try {
|
||||
this.tree.rerender(node);
|
||||
}
|
||||
catch {
|
||||
// missing nodes are fine, this could've resulted from
|
||||
// parallel refresh calls, removing `node` altogether
|
||||
}
|
||||
}
|
||||
}
|
||||
// View
|
||||
rerender(element) {
|
||||
if (element === undefined || element === this.root.element) {
|
||||
this.tree.rerender();
|
||||
return;
|
||||
}
|
||||
const node = this.getDataNode(element);
|
||||
this.tree.rerender(node);
|
||||
}
|
||||
// Tree
|
||||
getNode(element = this.root.element) {
|
||||
const dataNode = this.getDataNode(element);
|
||||
const node = this.tree.getNode(dataNode === this.root ? null : dataNode);
|
||||
return this.nodeMapper.map(node);
|
||||
}
|
||||
collapse(element, recursive = false) {
|
||||
const node = this.getDataNode(element);
|
||||
return this.tree.collapse(node === this.root ? null : node, recursive);
|
||||
}
|
||||
async expand(element, recursive = false) {
|
||||
if (typeof this.root.element === 'undefined') {
|
||||
throw new TreeError(this.user, 'Tree input not set');
|
||||
}
|
||||
if (this.root.refreshPromise) {
|
||||
await this.root.refreshPromise;
|
||||
await Event.toPromise(this._onDidRender.event);
|
||||
}
|
||||
const node = this.getDataNode(element);
|
||||
if (this.tree.hasElement(node) && !this.tree.isCollapsible(node)) {
|
||||
return false;
|
||||
}
|
||||
if (node.refreshPromise) {
|
||||
await node.refreshPromise;
|
||||
await Event.toPromise(this._onDidRender.event);
|
||||
}
|
||||
if (node !== this.root && !node.refreshPromise && !this.tree.isCollapsed(node)) {
|
||||
return false;
|
||||
}
|
||||
const result = this.tree.expand(node === this.root ? null : node, recursive);
|
||||
if (node.refreshPromise) {
|
||||
await node.refreshPromise;
|
||||
await Event.toPromise(this._onDidRender.event);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
setSelection(elements, browserEvent) {
|
||||
const nodes = elements.map(e => this.getDataNode(e));
|
||||
this.tree.setSelection(nodes, browserEvent);
|
||||
}
|
||||
getSelection() {
|
||||
const nodes = this.tree.getSelection();
|
||||
return nodes.map(n => n.element);
|
||||
}
|
||||
setFocus(elements, browserEvent) {
|
||||
const nodes = elements.map(e => this.getDataNode(e));
|
||||
this.tree.setFocus(nodes, browserEvent);
|
||||
}
|
||||
getFocus() {
|
||||
const nodes = this.tree.getFocus();
|
||||
return nodes.map(n => n.element);
|
||||
}
|
||||
reveal(element, relativeTop) {
|
||||
this.tree.reveal(this.getDataNode(element), relativeTop);
|
||||
}
|
||||
// Tree navigation
|
||||
getParentElement(element) {
|
||||
const node = this.tree.getParentElement(this.getDataNode(element));
|
||||
return (node && node.element);
|
||||
}
|
||||
getFirstElementChild(element = this.root.element) {
|
||||
const dataNode = this.getDataNode(element);
|
||||
const node = this.tree.getFirstElementChild(dataNode === this.root ? null : dataNode);
|
||||
return (node && node.element);
|
||||
}
|
||||
// Implementation
|
||||
getDataNode(element) {
|
||||
const node = this.nodes.get((element === this.root.element ? null : element));
|
||||
if (!node) {
|
||||
const nodeIdentity = this.identityProvider?.getId(element).toString();
|
||||
throw new TreeError(this.user, `Data tree node not found${nodeIdentity ? `: ${nodeIdentity}` : ''}`);
|
||||
}
|
||||
return node;
|
||||
}
|
||||
async refreshAndRenderNode(node, recursive, viewStateContext, options) {
|
||||
if (this.disposables.isDisposed) {
|
||||
return; // tree disposed during refresh, again (#228211)
|
||||
}
|
||||
await this.refreshNode(node, recursive, viewStateContext);
|
||||
if (this.disposables.isDisposed) {
|
||||
return; // tree disposed during refresh (#199264)
|
||||
}
|
||||
this.render(node, viewStateContext, options);
|
||||
}
|
||||
async refreshNode(node, recursive, viewStateContext) {
|
||||
let result;
|
||||
this.subTreeRefreshPromises.forEach((refreshPromise, refreshNode) => {
|
||||
if (!result && intersects(refreshNode, node)) {
|
||||
result = refreshPromise.then(() => this.refreshNode(node, recursive, viewStateContext));
|
||||
}
|
||||
});
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
if (node !== this.root) {
|
||||
const treeNode = this.tree.getNode(node);
|
||||
if (treeNode.collapsed) {
|
||||
node.hasChildren = !!this.dataSource.hasChildren(node.element);
|
||||
node.stale = true;
|
||||
this.setChildren(node, [], recursive, viewStateContext);
|
||||
return;
|
||||
}
|
||||
}
|
||||
return this.doRefreshSubTree(node, recursive, viewStateContext);
|
||||
}
|
||||
async doRefreshSubTree(node, recursive, viewStateContext) {
|
||||
const cancelablePromise = createCancelablePromise(async () => {
|
||||
const childrenToRefresh = await this.doRefreshNode(node, recursive, viewStateContext);
|
||||
node.stale = false;
|
||||
await Promises.settled(childrenToRefresh.map(child => this.doRefreshSubTree(child, recursive, viewStateContext)));
|
||||
});
|
||||
node.refreshPromise = cancelablePromise;
|
||||
this.subTreeRefreshPromises.set(node, cancelablePromise);
|
||||
cancelablePromise.finally(() => {
|
||||
node.refreshPromise = undefined;
|
||||
this.subTreeRefreshPromises.delete(node);
|
||||
});
|
||||
return cancelablePromise;
|
||||
}
|
||||
async doRefreshNode(node, recursive, viewStateContext) {
|
||||
node.hasChildren = !!this.dataSource.hasChildren(node.element);
|
||||
let childrenPromise;
|
||||
if (!node.hasChildren) {
|
||||
childrenPromise = Promise.resolve(Iterable.empty());
|
||||
}
|
||||
else {
|
||||
const children = this.doGetChildren(node);
|
||||
if (isIterable(children)) {
|
||||
childrenPromise = Promise.resolve(children);
|
||||
}
|
||||
else {
|
||||
const slowTimeout = timeout(800);
|
||||
slowTimeout.then(() => {
|
||||
node.slow = true;
|
||||
this._onDidChangeNodeSlowState.fire(node);
|
||||
}, _ => null);
|
||||
childrenPromise = children.finally(() => slowTimeout.cancel());
|
||||
}
|
||||
}
|
||||
try {
|
||||
const children = await childrenPromise;
|
||||
return this.setChildren(node, children, recursive, viewStateContext);
|
||||
}
|
||||
catch (err) {
|
||||
if (node !== this.root && this.tree.hasElement(node)) {
|
||||
this.tree.collapse(node);
|
||||
}
|
||||
if (isCancellationError(err)) {
|
||||
return [];
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
finally {
|
||||
if (node.slow) {
|
||||
node.slow = false;
|
||||
this._onDidChangeNodeSlowState.fire(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
doGetChildren(node) {
|
||||
let result = this.refreshPromises.get(node);
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
const children = this.dataSource.getChildren(node.element);
|
||||
if (isIterable(children)) {
|
||||
return this.processChildren(children);
|
||||
}
|
||||
else {
|
||||
result = createCancelablePromise(async () => this.processChildren(await children));
|
||||
this.refreshPromises.set(node, result);
|
||||
return result.finally(() => { this.refreshPromises.delete(node); });
|
||||
}
|
||||
}
|
||||
_onDidChangeCollapseState({ node, deep }) {
|
||||
if (node.element === null) {
|
||||
return;
|
||||
}
|
||||
if (!node.collapsed && node.element.stale) {
|
||||
if (deep) {
|
||||
this.collapse(node.element.element);
|
||||
}
|
||||
else {
|
||||
this.refreshAndRenderNode(node.element, false)
|
||||
.catch(onUnexpectedError);
|
||||
}
|
||||
}
|
||||
}
|
||||
setChildren(node, childrenElementsIterable, recursive, viewStateContext) {
|
||||
const childrenElements = [...childrenElementsIterable];
|
||||
// perf: if the node was and still is a leaf, avoid all this hassle
|
||||
if (node.children.length === 0 && childrenElements.length === 0) {
|
||||
return [];
|
||||
}
|
||||
const nodesToForget = new Map();
|
||||
const childrenTreeNodesById = new Map();
|
||||
for (const child of node.children) {
|
||||
nodesToForget.set(child.element, child);
|
||||
if (this.identityProvider) {
|
||||
childrenTreeNodesById.set(child.id, { node: child, collapsed: this.tree.hasElement(child) && this.tree.isCollapsed(child) });
|
||||
}
|
||||
}
|
||||
const childrenToRefresh = [];
|
||||
const children = childrenElements.map(element => {
|
||||
const hasChildren = !!this.dataSource.hasChildren(element);
|
||||
if (!this.identityProvider) {
|
||||
const asyncDataTreeNode = createAsyncDataTreeNode({ element, parent: node, hasChildren, defaultCollapseState: this.getDefaultCollapseState(element) });
|
||||
if (hasChildren && asyncDataTreeNode.defaultCollapseState === ObjectTreeElementCollapseState.PreserveOrExpanded) {
|
||||
childrenToRefresh.push(asyncDataTreeNode);
|
||||
}
|
||||
return asyncDataTreeNode;
|
||||
}
|
||||
const id = this.identityProvider.getId(element).toString();
|
||||
const result = childrenTreeNodesById.get(id);
|
||||
if (result) {
|
||||
const asyncDataTreeNode = result.node;
|
||||
nodesToForget.delete(asyncDataTreeNode.element);
|
||||
this.nodes.delete(asyncDataTreeNode.element);
|
||||
this.nodes.set(element, asyncDataTreeNode);
|
||||
asyncDataTreeNode.element = element;
|
||||
asyncDataTreeNode.hasChildren = hasChildren;
|
||||
if (recursive) {
|
||||
if (result.collapsed) {
|
||||
asyncDataTreeNode.children.forEach(node => dfs(node, node => this.nodes.delete(node.element)));
|
||||
asyncDataTreeNode.children.splice(0, asyncDataTreeNode.children.length);
|
||||
asyncDataTreeNode.stale = true;
|
||||
}
|
||||
else {
|
||||
childrenToRefresh.push(asyncDataTreeNode);
|
||||
}
|
||||
}
|
||||
else if (hasChildren && !result.collapsed) {
|
||||
childrenToRefresh.push(asyncDataTreeNode);
|
||||
}
|
||||
return asyncDataTreeNode;
|
||||
}
|
||||
const childAsyncDataTreeNode = createAsyncDataTreeNode({ element, parent: node, id, hasChildren, defaultCollapseState: this.getDefaultCollapseState(element) });
|
||||
if (viewStateContext && viewStateContext.viewState.focus && viewStateContext.viewState.focus.indexOf(id) > -1) {
|
||||
viewStateContext.focus.push(childAsyncDataTreeNode);
|
||||
}
|
||||
if (viewStateContext && viewStateContext.viewState.selection && viewStateContext.viewState.selection.indexOf(id) > -1) {
|
||||
viewStateContext.selection.push(childAsyncDataTreeNode);
|
||||
}
|
||||
if (viewStateContext && viewStateContext.viewState.expanded && viewStateContext.viewState.expanded.indexOf(id) > -1) {
|
||||
childrenToRefresh.push(childAsyncDataTreeNode);
|
||||
}
|
||||
else if (hasChildren && childAsyncDataTreeNode.defaultCollapseState === ObjectTreeElementCollapseState.PreserveOrExpanded) {
|
||||
childrenToRefresh.push(childAsyncDataTreeNode);
|
||||
}
|
||||
return childAsyncDataTreeNode;
|
||||
});
|
||||
for (const node of nodesToForget.values()) {
|
||||
dfs(node, node => this.nodes.delete(node.element));
|
||||
}
|
||||
for (const child of children) {
|
||||
this.nodes.set(child.element, child);
|
||||
}
|
||||
splice(node.children, 0, node.children.length, children);
|
||||
// TODO@joao this doesn't take filter into account
|
||||
if (node !== this.root && this.autoExpandSingleChildren && children.length === 1 && childrenToRefresh.length === 0) {
|
||||
children[0].forceExpanded = true;
|
||||
childrenToRefresh.push(children[0]);
|
||||
}
|
||||
return childrenToRefresh;
|
||||
}
|
||||
render(node, viewStateContext, options) {
|
||||
const children = node.children.map(node => this.asTreeElement(node, viewStateContext));
|
||||
const objectTreeOptions = options && {
|
||||
...options,
|
||||
diffIdentityProvider: options.diffIdentityProvider && {
|
||||
getId(node) {
|
||||
return options.diffIdentityProvider.getId(node.element);
|
||||
}
|
||||
}
|
||||
};
|
||||
this.tree.setChildren(node === this.root ? null : node, children, objectTreeOptions);
|
||||
if (node !== this.root) {
|
||||
this.tree.setCollapsible(node, node.hasChildren);
|
||||
}
|
||||
this._onDidRender.fire();
|
||||
}
|
||||
asTreeElement(node, viewStateContext) {
|
||||
if (node.stale) {
|
||||
return {
|
||||
element: node,
|
||||
collapsible: node.hasChildren,
|
||||
collapsed: true
|
||||
};
|
||||
}
|
||||
let collapsed;
|
||||
if (viewStateContext && viewStateContext.viewState.expanded && node.id && viewStateContext.viewState.expanded.indexOf(node.id) > -1) {
|
||||
collapsed = false;
|
||||
}
|
||||
else if (node.forceExpanded) {
|
||||
collapsed = false;
|
||||
node.forceExpanded = false;
|
||||
}
|
||||
else {
|
||||
collapsed = node.defaultCollapseState;
|
||||
}
|
||||
return {
|
||||
element: node,
|
||||
children: node.hasChildren ? Iterable.map(node.children, child => this.asTreeElement(child, viewStateContext)) : [],
|
||||
collapsible: node.hasChildren,
|
||||
collapsed
|
||||
};
|
||||
}
|
||||
processChildren(children) {
|
||||
if (this.sorter) {
|
||||
children = [...children].sort(this.sorter.compare.bind(this.sorter));
|
||||
}
|
||||
return children;
|
||||
}
|
||||
dispose() {
|
||||
this.disposables.dispose();
|
||||
this.tree.dispose();
|
||||
}
|
||||
}
|
||||
class CompressibleAsyncDataTreeNodeWrapper {
|
||||
get element() {
|
||||
return {
|
||||
elements: this.node.element.elements.map(e => e.element),
|
||||
incompressible: this.node.element.incompressible
|
||||
};
|
||||
}
|
||||
get children() { return this.node.children.map(node => new CompressibleAsyncDataTreeNodeWrapper(node)); }
|
||||
get depth() { return this.node.depth; }
|
||||
get visibleChildrenCount() { return this.node.visibleChildrenCount; }
|
||||
get visibleChildIndex() { return this.node.visibleChildIndex; }
|
||||
get collapsible() { return this.node.collapsible; }
|
||||
get collapsed() { return this.node.collapsed; }
|
||||
get visible() { return this.node.visible; }
|
||||
get filterData() { return this.node.filterData; }
|
||||
constructor(node) {
|
||||
this.node = node;
|
||||
}
|
||||
}
|
||||
class CompressibleAsyncDataTreeRenderer {
|
||||
constructor(renderer, nodeMapper, compressibleNodeMapperProvider, onDidChangeTwistieState) {
|
||||
this.renderer = renderer;
|
||||
this.nodeMapper = nodeMapper;
|
||||
this.compressibleNodeMapperProvider = compressibleNodeMapperProvider;
|
||||
this.onDidChangeTwistieState = onDidChangeTwistieState;
|
||||
this.renderedNodes = new Map();
|
||||
this.disposables = [];
|
||||
this.templateId = renderer.templateId;
|
||||
}
|
||||
renderTemplate(container) {
|
||||
const templateData = this.renderer.renderTemplate(container);
|
||||
return { templateData };
|
||||
}
|
||||
renderElement(node, index, templateData, details) {
|
||||
this.renderer.renderElement(this.nodeMapper.map(node), index, templateData.templateData, details);
|
||||
}
|
||||
renderCompressedElements(node, index, templateData, details) {
|
||||
this.renderer.renderCompressedElements(this.compressibleNodeMapperProvider().map(node), index, templateData.templateData, details);
|
||||
}
|
||||
renderTwistie(element, twistieElement) {
|
||||
if (element.slow) {
|
||||
twistieElement.classList.add(...ThemeIcon.asClassNameArray(Codicon.treeItemLoading));
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
twistieElement.classList.remove(...ThemeIcon.asClassNameArray(Codicon.treeItemLoading));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
disposeElement(node, index, templateData, details) {
|
||||
this.renderer.disposeElement?.(this.nodeMapper.map(node), index, templateData.templateData, details);
|
||||
}
|
||||
disposeCompressedElements(node, index, templateData, details) {
|
||||
this.renderer.disposeCompressedElements?.(this.compressibleNodeMapperProvider().map(node), index, templateData.templateData, details);
|
||||
}
|
||||
disposeTemplate(templateData) {
|
||||
this.renderer.disposeTemplate(templateData.templateData);
|
||||
}
|
||||
dispose() {
|
||||
this.renderedNodes.clear();
|
||||
this.disposables = dispose(this.disposables);
|
||||
}
|
||||
}
|
||||
function asCompressibleObjectTreeOptions(options) {
|
||||
const objectTreeOptions = options && asObjectTreeOptions(options);
|
||||
return objectTreeOptions && {
|
||||
...objectTreeOptions,
|
||||
keyboardNavigationLabelProvider: objectTreeOptions.keyboardNavigationLabelProvider && {
|
||||
...objectTreeOptions.keyboardNavigationLabelProvider,
|
||||
getCompressedNodeKeyboardNavigationLabel(els) {
|
||||
return options.keyboardNavigationLabelProvider.getCompressedNodeKeyboardNavigationLabel(els.map(e => e.element));
|
||||
}
|
||||
},
|
||||
stickyScrollDelegate: objectTreeOptions.stickyScrollDelegate
|
||||
};
|
||||
}
|
||||
class CompressibleAsyncDataTree extends AsyncDataTree {
|
||||
constructor(user, container, virtualDelegate, compressionDelegate, renderers, dataSource, options = {}) {
|
||||
super(user, container, virtualDelegate, renderers, dataSource, options);
|
||||
this.compressionDelegate = compressionDelegate;
|
||||
this.compressibleNodeMapper = new WeakMapper(node => new CompressibleAsyncDataTreeNodeWrapper(node));
|
||||
this.filter = options.filter;
|
||||
}
|
||||
createTree(user, container, delegate, renderers, options) {
|
||||
const objectTreeDelegate = new ComposedTreeDelegate(delegate);
|
||||
const objectTreeRenderers = renderers.map(r => new CompressibleAsyncDataTreeRenderer(r, this.nodeMapper, () => this.compressibleNodeMapper, this._onDidChangeNodeSlowState.event));
|
||||
const objectTreeOptions = asCompressibleObjectTreeOptions(options) || {};
|
||||
return new CompressibleObjectTree(user, container, objectTreeDelegate, objectTreeRenderers, objectTreeOptions);
|
||||
}
|
||||
asTreeElement(node, viewStateContext) {
|
||||
return {
|
||||
incompressible: this.compressionDelegate.isIncompressible(node.element),
|
||||
...super.asTreeElement(node, viewStateContext)
|
||||
};
|
||||
}
|
||||
render(node, viewStateContext, options) {
|
||||
if (!this.identityProvider) {
|
||||
return super.render(node, viewStateContext);
|
||||
}
|
||||
// Preserve traits across compressions. Hacky but does the trick.
|
||||
// This is hard to fix properly since it requires rewriting the traits
|
||||
// across trees and lists. Let's just keep it this way for now.
|
||||
const getId = (element) => this.identityProvider.getId(element).toString();
|
||||
const getUncompressedIds = (nodes) => {
|
||||
const result = new Set();
|
||||
for (const node of nodes) {
|
||||
const compressedNode = this.tree.getCompressedTreeNode(node === this.root ? null : node);
|
||||
if (!compressedNode.element) {
|
||||
continue;
|
||||
}
|
||||
for (const node of compressedNode.element.elements) {
|
||||
result.add(getId(node.element));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const oldSelection = getUncompressedIds(this.tree.getSelection());
|
||||
const oldFocus = getUncompressedIds(this.tree.getFocus());
|
||||
super.render(node, viewStateContext, options);
|
||||
const selection = this.getSelection();
|
||||
let didChangeSelection = false;
|
||||
const focus = this.getFocus();
|
||||
let didChangeFocus = false;
|
||||
const visit = (node) => {
|
||||
const compressedNode = node.element;
|
||||
if (compressedNode) {
|
||||
for (let i = 0; i < compressedNode.elements.length; i++) {
|
||||
const id = getId(compressedNode.elements[i].element);
|
||||
const element = compressedNode.elements[compressedNode.elements.length - 1].element;
|
||||
// github.com/microsoft/vscode/issues/85938
|
||||
if (oldSelection.has(id) && selection.indexOf(element) === -1) {
|
||||
selection.push(element);
|
||||
didChangeSelection = true;
|
||||
}
|
||||
if (oldFocus.has(id) && focus.indexOf(element) === -1) {
|
||||
focus.push(element);
|
||||
didChangeFocus = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
node.children.forEach(visit);
|
||||
};
|
||||
visit(this.tree.getCompressedTreeNode(node === this.root ? null : node));
|
||||
if (didChangeSelection) {
|
||||
this.setSelection(selection);
|
||||
}
|
||||
if (didChangeFocus) {
|
||||
this.setFocus(focus);
|
||||
}
|
||||
}
|
||||
// For compressed async data trees, `TreeVisibility.Recurse` doesn't currently work
|
||||
// and we have to filter everything beforehand
|
||||
// Related to #85193 and #85835
|
||||
processChildren(children) {
|
||||
if (this.filter) {
|
||||
children = Iterable.filter(children, e => {
|
||||
const result = this.filter.filter(e, 1 /* TreeVisibility.Visible */);
|
||||
const visibility = getVisibility(result);
|
||||
if (visibility === 2 /* TreeVisibility.Recurse */) {
|
||||
throw new Error('Recursive tree visibility not supported in async data compressed trees');
|
||||
}
|
||||
return visibility === 1 /* TreeVisibility.Visible */;
|
||||
});
|
||||
}
|
||||
return super.processChildren(children);
|
||||
}
|
||||
}
|
||||
function getVisibility(filterResult) {
|
||||
if (typeof filterResult === 'boolean') {
|
||||
return filterResult ? 1 /* TreeVisibility.Visible */ : 0 /* TreeVisibility.Hidden */;
|
||||
}
|
||||
else if (isFilterResult(filterResult)) {
|
||||
return getVisibleState(filterResult.visibility);
|
||||
}
|
||||
else {
|
||||
return getVisibleState(filterResult);
|
||||
}
|
||||
}
|
||||
|
||||
export { AsyncDataTree, CompressibleAsyncDataTree };
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user