Path: blob/main/src/vs/base/browser/ui/iconLabel/iconLabel.ts
5243 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import './iconlabel.css';6import * as dom from '../../dom.js';7import * as css from '../../cssValue.js';8import { HighlightedLabel } from '../highlightedlabel/highlightedLabel.js';9import { IHoverDelegate } from '../hover/hoverDelegate.js';10import { IMatch } from '../../../common/filters.js';11import { Disposable, IDisposable } from '../../../common/lifecycle.js';12import { equals } from '../../../common/objects.js';13import { Range } from '../../../common/range.js';14import { getDefaultHoverDelegate } from '../hover/hoverDelegateFactory.js';15import type { IManagedHoverTooltipMarkdownString } from '../hover/hover.js';16import { getBaseLayerHoverDelegate } from '../hover/hoverDelegate2.js';17import { URI } from '../../../common/uri.js';18import { ThemeIcon } from '../../../common/themables.js';1920export interface IIconLabelCreationOptions {21readonly supportHighlights?: boolean;22readonly supportDescriptionHighlights?: boolean;23readonly supportIcons?: boolean;24readonly hoverDelegate?: IHoverDelegate;25readonly hoverTargetOverride?: HTMLElement;26}2728export interface IIconLabelValueOptions {29title?: string | IManagedHoverTooltipMarkdownString;30descriptionTitle?: string | IManagedHoverTooltipMarkdownString;31suffix?: string;32hideIcon?: boolean;33extraClasses?: readonly string[];34bold?: boolean;35italic?: boolean;36strikethrough?: boolean;37matches?: readonly IMatch[];38labelEscapeNewLines?: boolean;39descriptionMatches?: readonly IMatch[];40disabledCommand?: boolean;41readonly separator?: string;42readonly domId?: string;43iconPath?: URI | ThemeIcon;44supportIcons?: boolean;45}4647class FastLabelNode {48private disposed: boolean | undefined;49private _textContent: string | undefined;50private _classNames: string[] | undefined;51private _empty: boolean | undefined;5253constructor(private _element: HTMLElement) {54}5556get element(): HTMLElement {57return this._element;58}5960set textContent(content: string) {61if (this.disposed || content === this._textContent) {62return;63}6465this._textContent = content;66this._element.textContent = content;67}6869set classNames(classNames: string[]) {70if (this.disposed || equals(classNames, this._classNames)) {71return;72}7374this._classNames = classNames;75this._element.classList.value = '';76this._element.classList.add(...classNames);77}7879set empty(empty: boolean) {80if (this.disposed || empty === this._empty) {81return;82}8384this._empty = empty;85this._element.style.marginLeft = empty ? '0' : '';86}8788dispose(): void {89this.disposed = true;90}91}9293export class IconLabel extends Disposable {9495private readonly creationOptions?: IIconLabelCreationOptions;9697private readonly domNode: FastLabelNode;98private readonly nameContainer: HTMLElement;99private readonly nameNode: Label | LabelWithHighlights;100101private descriptionNode: FastLabelNode | HighlightedLabel | undefined;102private suffixNode: FastLabelNode | undefined;103104private readonly labelContainer: HTMLElement;105106private readonly hoverDelegate: IHoverDelegate;107private readonly customHovers: Map<HTMLElement, IDisposable> = new Map();108109constructor(container: HTMLElement, options?: IIconLabelCreationOptions) {110super();111this.creationOptions = options;112113this.domNode = this._register(new FastLabelNode(dom.append(container, dom.$('.monaco-icon-label'))));114115this.labelContainer = dom.append(this.domNode.element, dom.$('.monaco-icon-label-container'));116117this.nameContainer = dom.append(this.labelContainer, dom.$('span.monaco-icon-name-container'));118119if (options?.supportHighlights || options?.supportIcons) {120this.nameNode = this._register(new LabelWithHighlights(this.nameContainer, !!options.supportIcons));121} else {122this.nameNode = new Label(this.nameContainer);123}124125this.hoverDelegate = options?.hoverDelegate ?? getDefaultHoverDelegate('mouse');126}127128get element(): HTMLElement {129return this.domNode.element;130}131132setLabel(label: string | string[], description?: string, options?: IIconLabelValueOptions): void {133const labelClasses = ['monaco-icon-label'];134const containerClasses = ['monaco-icon-label-container'];135let ariaLabel: string = '';136if (options) {137if (options.extraClasses) {138labelClasses.push(...options.extraClasses);139}140141if (options.bold) {142labelClasses.push('bold');143}144145if (options.italic) {146labelClasses.push('italic');147}148149if (options.strikethrough) {150labelClasses.push('strikethrough');151}152153if (options.disabledCommand) {154containerClasses.push('disabled');155}156if (options.title) {157if (typeof options.title === 'string') {158ariaLabel += options.title;159} else {160ariaLabel += label;161}162}163}164165// eslint-disable-next-line no-restricted-syntax166const existingIconNode = this.domNode.element.querySelector('.monaco-icon-label-iconpath');167if (options?.iconPath) {168let iconNode;169if (!existingIconNode || !(dom.isHTMLElement(existingIconNode))) {170iconNode = dom.$('.monaco-icon-label-iconpath');171this.domNode.element.prepend(iconNode);172} else {173iconNode = existingIconNode;174}175if (ThemeIcon.isThemeIcon(options.iconPath)) {176const iconClass = ThemeIcon.asClassName(options.iconPath);177iconNode.className = `monaco-icon-label-iconpath ${iconClass}`;178iconNode.style.backgroundImage = '';179} else {180iconNode.style.backgroundImage = css.asCSSUrl(options?.iconPath);181}182iconNode.style.backgroundRepeat = 'no-repeat';183iconNode.style.backgroundPosition = 'center';184iconNode.style.backgroundSize = 'contain';185186} else if (existingIconNode) {187existingIconNode.remove();188}189190this.domNode.classNames = labelClasses;191this.domNode.element.setAttribute('aria-label', ariaLabel);192this.labelContainer.classList.value = '';193this.labelContainer.classList.add(...containerClasses);194this.setupHover(options?.descriptionTitle ? this.labelContainer : this.element, options?.title);195196this.nameNode.setLabel(label, options);197198if (description || this.descriptionNode) {199const descriptionNode = this.getOrCreateDescriptionNode();200if (descriptionNode instanceof HighlightedLabel) {201const supportIcons = options?.supportIcons ?? this.creationOptions?.supportIcons;202descriptionNode.set(description || '', options ? options.descriptionMatches : undefined, undefined, options?.labelEscapeNewLines, supportIcons);203this.setupHover(descriptionNode.element, options?.descriptionTitle);204} else {205descriptionNode.textContent = description && options?.labelEscapeNewLines ? HighlightedLabel.escapeNewLines(description, []) : (description || '');206this.setupHover(descriptionNode.element, options?.descriptionTitle || '');207descriptionNode.empty = !description;208}209}210211if (options?.suffix || this.suffixNode) {212const suffixNode = this.getOrCreateSuffixNode();213suffixNode.textContent = options?.suffix ?? '';214}215}216217private setupHover(htmlElement: HTMLElement, tooltip: string | IManagedHoverTooltipMarkdownString | undefined): void {218const previousCustomHover = this.customHovers.get(htmlElement);219if (previousCustomHover) {220previousCustomHover.dispose();221this.customHovers.delete(htmlElement);222}223224if (!tooltip) {225htmlElement.removeAttribute('title');226return;227}228229let hoverTarget = htmlElement;230if (this.creationOptions?.hoverTargetOverride) {231if (!dom.isAncestor(htmlElement, this.creationOptions.hoverTargetOverride)) {232throw new Error('hoverTargetOverrride must be an ancestor of the htmlElement');233}234hoverTarget = this.creationOptions.hoverTargetOverride;235}236237const hoverDisposable = getBaseLayerHoverDelegate().setupManagedHover(this.hoverDelegate, hoverTarget, tooltip);238if (hoverDisposable) {239this.customHovers.set(htmlElement, hoverDisposable);240}241}242243public override dispose() {244super.dispose();245for (const disposable of this.customHovers.values()) {246disposable.dispose();247}248this.customHovers.clear();249}250251private getOrCreateSuffixNode() {252if (!this.suffixNode) {253const suffixContainer = this._register(new FastLabelNode(dom.after(this.nameContainer, dom.$('span.monaco-icon-suffix-container'))));254this.suffixNode = this._register(new FastLabelNode(dom.append(suffixContainer.element, dom.$('span.label-suffix'))));255}256257return this.suffixNode;258}259260private getOrCreateDescriptionNode() {261if (!this.descriptionNode) {262const descriptionContainer = this._register(new FastLabelNode(dom.append(this.labelContainer, dom.$('span.monaco-icon-description-container'))));263if (this.creationOptions?.supportDescriptionHighlights) {264this.descriptionNode = this._register(new HighlightedLabel(dom.append(descriptionContainer.element, dom.$('span.label-description'))));265} else {266this.descriptionNode = this._register(new FastLabelNode(dom.append(descriptionContainer.element, dom.$('span.label-description'))));267}268}269270return this.descriptionNode;271}272}273274class Label {275276private label: string | string[] | undefined = undefined;277private singleLabel: HTMLElement | undefined = undefined;278private options: IIconLabelValueOptions | undefined;279280constructor(private container: HTMLElement) { }281282setLabel(label: string | string[], options?: IIconLabelValueOptions): void {283if (this.label === label && equals(this.options, options)) {284return;285}286287this.label = label;288this.options = options;289290if (typeof label === 'string') {291if (!this.singleLabel) {292this.container.textContent = '';293this.container.classList.remove('multiple');294this.singleLabel = dom.append(this.container, dom.$('a.label-name', { id: options?.domId }));295}296297this.singleLabel.textContent = label;298} else {299this.container.textContent = '';300this.container.classList.add('multiple');301this.singleLabel = undefined;302303for (let i = 0; i < label.length; i++) {304const l = label[i];305const id = options?.domId && `${options?.domId}_${i}`;306307dom.append(this.container, dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' }, l));308309if (i < label.length - 1) {310dom.append(this.container, dom.$('span.label-separator', undefined, options?.separator || '/'));311}312}313}314}315}316317function splitMatches(labels: string[], separator: string, matches: readonly IMatch[] | undefined): IMatch[][] | undefined {318if (!matches) {319return undefined;320}321322let labelStart = 0;323324return labels.map(label => {325const labelRange = { start: labelStart, end: labelStart + label.length };326327const result = matches328.map(match => Range.intersect(labelRange, match))329.filter(range => !Range.isEmpty(range))330.map(({ start, end }) => ({ start: start - labelStart, end: end - labelStart }));331332labelStart = labelRange.end + separator.length;333return result;334});335}336337class LabelWithHighlights extends Disposable {338339private label: string | string[] | undefined = undefined;340private singleLabel: HighlightedLabel | undefined = undefined;341private options: IIconLabelValueOptions | undefined;342343constructor(private container: HTMLElement, private supportIcons: boolean) {344super();345}346347setLabel(label: string | string[], options?: IIconLabelValueOptions): void {348if (this.label === label && equals(this.options, options)) {349return;350}351352this.label = label;353this.options = options;354355// Determine supportIcons: use option if provided, otherwise use constructor value356const supportIcons = options?.supportIcons ?? this.supportIcons;357358if (typeof label === 'string') {359if (!this.singleLabel) {360this.container.textContent = '';361this.container.classList.remove('multiple');362this.singleLabel = this._register(new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId }))));363}364365this.singleLabel.set(label, options?.matches, undefined, options?.labelEscapeNewLines, supportIcons);366} else {367this.container.textContent = '';368this.container.classList.add('multiple');369this.singleLabel = undefined;370371const separator = options?.separator || '/';372const matches = splitMatches(label, separator, options?.matches);373374for (let i = 0; i < label.length; i++) {375const l = label[i];376const m = matches ? matches[i] : undefined;377const id = options?.domId && `${options?.domId}_${i}`;378379const name = dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' });380const highlightedLabel = this._register(new HighlightedLabel(dom.append(this.container, name)));381highlightedLabel.set(l, m, undefined, options?.labelEscapeNewLines, supportIcons);382383if (i < label.length - 1) {384dom.append(name, dom.$('span.label-separator', undefined, separator));385}386}387}388}389}390391392