Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/base/browser/ui/iconLabel/iconLabel.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './iconlabel.css';
7
import * as dom from '../../dom.js';
8
import * as css from '../../cssValue.js';
9
import { HighlightedLabel } from '../highlightedlabel/highlightedLabel.js';
10
import { IHoverDelegate } from '../hover/hoverDelegate.js';
11
import { IMatch } from '../../../common/filters.js';
12
import { Disposable, IDisposable } from '../../../common/lifecycle.js';
13
import { equals } from '../../../common/objects.js';
14
import { Range } from '../../../common/range.js';
15
import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';
16
import type { IManagedHoverTooltipMarkdownString } from '../hover/hover.js';
17
import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';
18
import { URI } from '../../../common/uri.js';
19
20
export interface IIconLabelCreationOptions {
21
readonly supportHighlights?: boolean;
22
readonly supportDescriptionHighlights?: boolean;
23
readonly supportIcons?: boolean;
24
readonly hoverDelegate?: IHoverDelegate;
25
readonly hoverTargetOverride?: HTMLElement;
26
}
27
28
export interface IIconLabelValueOptions {
29
title?: string | IManagedHoverTooltipMarkdownString;
30
descriptionTitle?: string | IManagedHoverTooltipMarkdownString;
31
suffix?: string;
32
hideIcon?: boolean;
33
extraClasses?: readonly string[];
34
italic?: boolean;
35
strikethrough?: boolean;
36
matches?: readonly IMatch[];
37
labelEscapeNewLines?: boolean;
38
descriptionMatches?: readonly IMatch[];
39
disabledCommand?: boolean;
40
readonly separator?: string;
41
readonly domId?: string;
42
iconPath?: URI;
43
}
44
45
class FastLabelNode {
46
private disposed: boolean | undefined;
47
private _textContent: string | undefined;
48
private _classNames: string[] | undefined;
49
private _empty: boolean | undefined;
50
51
constructor(private _element: HTMLElement) {
52
}
53
54
get element(): HTMLElement {
55
return this._element;
56
}
57
58
set textContent(content: string) {
59
if (this.disposed || content === this._textContent) {
60
return;
61
}
62
63
this._textContent = content;
64
this._element.textContent = content;
65
}
66
67
set classNames(classNames: string[]) {
68
if (this.disposed || equals(classNames, this._classNames)) {
69
return;
70
}
71
72
this._classNames = classNames;
73
this._element.classList.value = '';
74
this._element.classList.add(...classNames);
75
}
76
77
set empty(empty: boolean) {
78
if (this.disposed || empty === this._empty) {
79
return;
80
}
81
82
this._empty = empty;
83
this._element.style.marginLeft = empty ? '0' : '';
84
}
85
86
dispose(): void {
87
this.disposed = true;
88
}
89
}
90
91
export class IconLabel extends Disposable {
92
93
private readonly creationOptions?: IIconLabelCreationOptions;
94
95
private readonly domNode: FastLabelNode;
96
private readonly nameContainer: HTMLElement;
97
private readonly nameNode: Label | LabelWithHighlights;
98
99
private descriptionNode: FastLabelNode | HighlightedLabel | undefined;
100
private suffixNode: FastLabelNode | undefined;
101
102
private readonly labelContainer: HTMLElement;
103
104
private readonly hoverDelegate: IHoverDelegate;
105
private readonly customHovers: Map<HTMLElement, IDisposable> = new Map();
106
107
constructor(container: HTMLElement, options?: IIconLabelCreationOptions) {
108
super();
109
this.creationOptions = options;
110
111
this.domNode = this._register(new FastLabelNode(dom.append(container, dom.$('.monaco-icon-label'))));
112
113
this.labelContainer = dom.append(this.domNode.element, dom.$('.monaco-icon-label-container'));
114
115
this.nameContainer = dom.append(this.labelContainer, dom.$('span.monaco-icon-name-container'));
116
117
if (options?.supportHighlights || options?.supportIcons) {
118
this.nameNode = this._register(new LabelWithHighlights(this.nameContainer, !!options.supportIcons));
119
} else {
120
this.nameNode = new Label(this.nameContainer);
121
}
122
123
this.hoverDelegate = options?.hoverDelegate ?? getDefaultHoverDelegate('mouse');
124
}
125
126
get element(): HTMLElement {
127
return this.domNode.element;
128
}
129
130
setLabel(label: string | string[], description?: string, options?: IIconLabelValueOptions): void {
131
const labelClasses = ['monaco-icon-label'];
132
const containerClasses = ['monaco-icon-label-container'];
133
let ariaLabel: string = '';
134
if (options) {
135
if (options.extraClasses) {
136
labelClasses.push(...options.extraClasses);
137
}
138
139
if (options.italic) {
140
labelClasses.push('italic');
141
}
142
143
if (options.strikethrough) {
144
labelClasses.push('strikethrough');
145
}
146
147
if (options.disabledCommand) {
148
containerClasses.push('disabled');
149
}
150
if (options.title) {
151
if (typeof options.title === 'string') {
152
ariaLabel += options.title;
153
} else {
154
ariaLabel += label;
155
}
156
}
157
}
158
159
const existingIconNode = this.domNode.element.querySelector('.monaco-icon-label-iconpath');
160
if (options?.iconPath) {
161
let iconNode;
162
if (!existingIconNode || !(dom.isHTMLElement(existingIconNode))) {
163
iconNode = dom.$('.monaco-icon-label-iconpath');
164
this.domNode.element.prepend(iconNode);
165
} else {
166
iconNode = existingIconNode;
167
}
168
iconNode.style.backgroundImage = css.asCSSUrl(options?.iconPath);
169
iconNode.style.backgroundRepeat = 'no-repeat';
170
iconNode.style.backgroundPosition = 'center';
171
iconNode.style.backgroundSize = 'contain';
172
173
} else if (existingIconNode) {
174
existingIconNode.remove();
175
}
176
177
this.domNode.classNames = labelClasses;
178
this.domNode.element.setAttribute('aria-label', ariaLabel);
179
this.labelContainer.classList.value = '';
180
this.labelContainer.classList.add(...containerClasses);
181
this.setupHover(options?.descriptionTitle ? this.labelContainer : this.element, options?.title);
182
183
this.nameNode.setLabel(label, options);
184
185
if (description || this.descriptionNode) {
186
const descriptionNode = this.getOrCreateDescriptionNode();
187
if (descriptionNode instanceof HighlightedLabel) {
188
descriptionNode.set(description || '', options ? options.descriptionMatches : undefined, undefined, options?.labelEscapeNewLines);
189
this.setupHover(descriptionNode.element, options?.descriptionTitle);
190
} else {
191
descriptionNode.textContent = description && options?.labelEscapeNewLines ? HighlightedLabel.escapeNewLines(description, []) : (description || '');
192
this.setupHover(descriptionNode.element, options?.descriptionTitle || '');
193
descriptionNode.empty = !description;
194
}
195
}
196
197
if (options?.suffix || this.suffixNode) {
198
const suffixNode = this.getOrCreateSuffixNode();
199
suffixNode.textContent = options?.suffix ?? '';
200
}
201
}
202
203
private setupHover(htmlElement: HTMLElement, tooltip: string | IManagedHoverTooltipMarkdownString | undefined): void {
204
const previousCustomHover = this.customHovers.get(htmlElement);
205
if (previousCustomHover) {
206
previousCustomHover.dispose();
207
this.customHovers.delete(htmlElement);
208
}
209
210
if (!tooltip) {
211
htmlElement.removeAttribute('title');
212
return;
213
}
214
215
let hoverTarget = htmlElement;
216
if (this.creationOptions?.hoverTargetOverride) {
217
if (!dom.isAncestor(htmlElement, this.creationOptions.hoverTargetOverride)) {
218
throw new Error('hoverTargetOverrride must be an ancestor of the htmlElement');
219
}
220
hoverTarget = this.creationOptions.hoverTargetOverride;
221
}
222
223
const hoverDisposable = getBaseLayerHoverDelegate().setupManagedHover(this.hoverDelegate, hoverTarget, tooltip);
224
if (hoverDisposable) {
225
this.customHovers.set(htmlElement, hoverDisposable);
226
}
227
}
228
229
public override dispose() {
230
super.dispose();
231
for (const disposable of this.customHovers.values()) {
232
disposable.dispose();
233
}
234
this.customHovers.clear();
235
}
236
237
private getOrCreateSuffixNode() {
238
if (!this.suffixNode) {
239
const suffixContainer = this._register(new FastLabelNode(dom.after(this.nameContainer, dom.$('span.monaco-icon-suffix-container'))));
240
this.suffixNode = this._register(new FastLabelNode(dom.append(suffixContainer.element, dom.$('span.label-suffix'))));
241
}
242
243
return this.suffixNode;
244
}
245
246
private getOrCreateDescriptionNode() {
247
if (!this.descriptionNode) {
248
const descriptionContainer = this._register(new FastLabelNode(dom.append(this.labelContainer, dom.$('span.monaco-icon-description-container'))));
249
if (this.creationOptions?.supportDescriptionHighlights) {
250
this.descriptionNode = this._register(new HighlightedLabel(dom.append(descriptionContainer.element, dom.$('span.label-description')), { supportIcons: !!this.creationOptions.supportIcons }));
251
} else {
252
this.descriptionNode = this._register(new FastLabelNode(dom.append(descriptionContainer.element, dom.$('span.label-description'))));
253
}
254
}
255
256
return this.descriptionNode;
257
}
258
}
259
260
class Label {
261
262
private label: string | string[] | undefined = undefined;
263
private singleLabel: HTMLElement | undefined = undefined;
264
private options: IIconLabelValueOptions | undefined;
265
266
constructor(private container: HTMLElement) { }
267
268
setLabel(label: string | string[], options?: IIconLabelValueOptions): void {
269
if (this.label === label && equals(this.options, options)) {
270
return;
271
}
272
273
this.label = label;
274
this.options = options;
275
276
if (typeof label === 'string') {
277
if (!this.singleLabel) {
278
this.container.textContent = '';
279
this.container.classList.remove('multiple');
280
this.singleLabel = dom.append(this.container, dom.$('a.label-name', { id: options?.domId }));
281
}
282
283
this.singleLabel.textContent = label;
284
} else {
285
this.container.textContent = '';
286
this.container.classList.add('multiple');
287
this.singleLabel = undefined;
288
289
for (let i = 0; i < label.length; i++) {
290
const l = label[i];
291
const id = options?.domId && `${options?.domId}_${i}`;
292
293
dom.append(this.container, dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' }, l));
294
295
if (i < label.length - 1) {
296
dom.append(this.container, dom.$('span.label-separator', undefined, options?.separator || '/'));
297
}
298
}
299
}
300
}
301
}
302
303
function splitMatches(labels: string[], separator: string, matches: readonly IMatch[] | undefined): IMatch[][] | undefined {
304
if (!matches) {
305
return undefined;
306
}
307
308
let labelStart = 0;
309
310
return labels.map(label => {
311
const labelRange = { start: labelStart, end: labelStart + label.length };
312
313
const result = matches
314
.map(match => Range.intersect(labelRange, match))
315
.filter(range => !Range.isEmpty(range))
316
.map(({ start, end }) => ({ start: start - labelStart, end: end - labelStart }));
317
318
labelStart = labelRange.end + separator.length;
319
return result;
320
});
321
}
322
323
class LabelWithHighlights extends Disposable {
324
325
private label: string | string[] | undefined = undefined;
326
private singleLabel: HighlightedLabel | undefined = undefined;
327
private options: IIconLabelValueOptions | undefined;
328
329
constructor(private container: HTMLElement, private supportIcons: boolean) {
330
super();
331
}
332
333
setLabel(label: string | string[], options?: IIconLabelValueOptions): void {
334
if (this.label === label && equals(this.options, options)) {
335
return;
336
}
337
338
this.label = label;
339
this.options = options;
340
341
if (typeof label === 'string') {
342
if (!this.singleLabel) {
343
this.container.textContent = '';
344
this.container.classList.remove('multiple');
345
this.singleLabel = this._register(new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), { supportIcons: this.supportIcons }));
346
}
347
348
this.singleLabel.set(label, options?.matches, undefined, options?.labelEscapeNewLines);
349
} else {
350
this.container.textContent = '';
351
this.container.classList.add('multiple');
352
this.singleLabel = undefined;
353
354
const separator = options?.separator || '/';
355
const matches = splitMatches(label, separator, options?.matches);
356
357
for (let i = 0; i < label.length; i++) {
358
const l = label[i];
359
const m = matches ? matches[i] : undefined;
360
const id = options?.domId && `${options?.domId}_${i}`;
361
362
const name = dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' });
363
const highlightedLabel = this._register(new HighlightedLabel(dom.append(this.container, name), { supportIcons: this.supportIcons }));
364
highlightedLabel.set(l, m, undefined, options?.labelEscapeNewLines);
365
366
if (i < label.length - 1) {
367
dom.append(name, dom.$('span.label-separator', undefined, separator));
368
}
369
}
370
}
371
}
372
}
373
374