Path: blob/main/src/vs/base/browser/ui/iconLabel/iconLabel.ts
3296 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';1819export interface IIconLabelCreationOptions {20readonly supportHighlights?: boolean;21readonly supportDescriptionHighlights?: boolean;22readonly supportIcons?: boolean;23readonly hoverDelegate?: IHoverDelegate;24readonly hoverTargetOverride?: HTMLElement;25}2627export interface IIconLabelValueOptions {28title?: string | IManagedHoverTooltipMarkdownString;29descriptionTitle?: string | IManagedHoverTooltipMarkdownString;30suffix?: string;31hideIcon?: boolean;32extraClasses?: readonly string[];33italic?: boolean;34strikethrough?: boolean;35matches?: readonly IMatch[];36labelEscapeNewLines?: boolean;37descriptionMatches?: readonly IMatch[];38disabledCommand?: boolean;39readonly separator?: string;40readonly domId?: string;41iconPath?: URI;42}4344class FastLabelNode {45private disposed: boolean | undefined;46private _textContent: string | undefined;47private _classNames: string[] | undefined;48private _empty: boolean | undefined;4950constructor(private _element: HTMLElement) {51}5253get element(): HTMLElement {54return this._element;55}5657set textContent(content: string) {58if (this.disposed || content === this._textContent) {59return;60}6162this._textContent = content;63this._element.textContent = content;64}6566set classNames(classNames: string[]) {67if (this.disposed || equals(classNames, this._classNames)) {68return;69}7071this._classNames = classNames;72this._element.classList.value = '';73this._element.classList.add(...classNames);74}7576set empty(empty: boolean) {77if (this.disposed || empty === this._empty) {78return;79}8081this._empty = empty;82this._element.style.marginLeft = empty ? '0' : '';83}8485dispose(): void {86this.disposed = true;87}88}8990export class IconLabel extends Disposable {9192private readonly creationOptions?: IIconLabelCreationOptions;9394private readonly domNode: FastLabelNode;95private readonly nameContainer: HTMLElement;96private readonly nameNode: Label | LabelWithHighlights;9798private descriptionNode: FastLabelNode | HighlightedLabel | undefined;99private suffixNode: FastLabelNode | undefined;100101private readonly labelContainer: HTMLElement;102103private readonly hoverDelegate: IHoverDelegate;104private readonly customHovers: Map<HTMLElement, IDisposable> = new Map();105106constructor(container: HTMLElement, options?: IIconLabelCreationOptions) {107super();108this.creationOptions = options;109110this.domNode = this._register(new FastLabelNode(dom.append(container, dom.$('.monaco-icon-label'))));111112this.labelContainer = dom.append(this.domNode.element, dom.$('.monaco-icon-label-container'));113114this.nameContainer = dom.append(this.labelContainer, dom.$('span.monaco-icon-name-container'));115116if (options?.supportHighlights || options?.supportIcons) {117this.nameNode = this._register(new LabelWithHighlights(this.nameContainer, !!options.supportIcons));118} else {119this.nameNode = new Label(this.nameContainer);120}121122this.hoverDelegate = options?.hoverDelegate ?? getDefaultHoverDelegate('mouse');123}124125get element(): HTMLElement {126return this.domNode.element;127}128129setLabel(label: string | string[], description?: string, options?: IIconLabelValueOptions): void {130const labelClasses = ['monaco-icon-label'];131const containerClasses = ['monaco-icon-label-container'];132let ariaLabel: string = '';133if (options) {134if (options.extraClasses) {135labelClasses.push(...options.extraClasses);136}137138if (options.italic) {139labelClasses.push('italic');140}141142if (options.strikethrough) {143labelClasses.push('strikethrough');144}145146if (options.disabledCommand) {147containerClasses.push('disabled');148}149if (options.title) {150if (typeof options.title === 'string') {151ariaLabel += options.title;152} else {153ariaLabel += label;154}155}156}157158const existingIconNode = this.domNode.element.querySelector('.monaco-icon-label-iconpath');159if (options?.iconPath) {160let iconNode;161if (!existingIconNode || !(dom.isHTMLElement(existingIconNode))) {162iconNode = dom.$('.monaco-icon-label-iconpath');163this.domNode.element.prepend(iconNode);164} else {165iconNode = existingIconNode;166}167iconNode.style.backgroundImage = css.asCSSUrl(options?.iconPath);168iconNode.style.backgroundRepeat = 'no-repeat';169iconNode.style.backgroundPosition = 'center';170iconNode.style.backgroundSize = 'contain';171172} else if (existingIconNode) {173existingIconNode.remove();174}175176this.domNode.classNames = labelClasses;177this.domNode.element.setAttribute('aria-label', ariaLabel);178this.labelContainer.classList.value = '';179this.labelContainer.classList.add(...containerClasses);180this.setupHover(options?.descriptionTitle ? this.labelContainer : this.element, options?.title);181182this.nameNode.setLabel(label, options);183184if (description || this.descriptionNode) {185const descriptionNode = this.getOrCreateDescriptionNode();186if (descriptionNode instanceof HighlightedLabel) {187descriptionNode.set(description || '', options ? options.descriptionMatches : undefined, undefined, options?.labelEscapeNewLines);188this.setupHover(descriptionNode.element, options?.descriptionTitle);189} else {190descriptionNode.textContent = description && options?.labelEscapeNewLines ? HighlightedLabel.escapeNewLines(description, []) : (description || '');191this.setupHover(descriptionNode.element, options?.descriptionTitle || '');192descriptionNode.empty = !description;193}194}195196if (options?.suffix || this.suffixNode) {197const suffixNode = this.getOrCreateSuffixNode();198suffixNode.textContent = options?.suffix ?? '';199}200}201202private setupHover(htmlElement: HTMLElement, tooltip: string | IManagedHoverTooltipMarkdownString | undefined): void {203const previousCustomHover = this.customHovers.get(htmlElement);204if (previousCustomHover) {205previousCustomHover.dispose();206this.customHovers.delete(htmlElement);207}208209if (!tooltip) {210htmlElement.removeAttribute('title');211return;212}213214let hoverTarget = htmlElement;215if (this.creationOptions?.hoverTargetOverride) {216if (!dom.isAncestor(htmlElement, this.creationOptions.hoverTargetOverride)) {217throw new Error('hoverTargetOverrride must be an ancestor of the htmlElement');218}219hoverTarget = this.creationOptions.hoverTargetOverride;220}221222const hoverDisposable = getBaseLayerHoverDelegate().setupManagedHover(this.hoverDelegate, hoverTarget, tooltip);223if (hoverDisposable) {224this.customHovers.set(htmlElement, hoverDisposable);225}226}227228public override dispose() {229super.dispose();230for (const disposable of this.customHovers.values()) {231disposable.dispose();232}233this.customHovers.clear();234}235236private getOrCreateSuffixNode() {237if (!this.suffixNode) {238const suffixContainer = this._register(new FastLabelNode(dom.after(this.nameContainer, dom.$('span.monaco-icon-suffix-container'))));239this.suffixNode = this._register(new FastLabelNode(dom.append(suffixContainer.element, dom.$('span.label-suffix'))));240}241242return this.suffixNode;243}244245private getOrCreateDescriptionNode() {246if (!this.descriptionNode) {247const descriptionContainer = this._register(new FastLabelNode(dom.append(this.labelContainer, dom.$('span.monaco-icon-description-container'))));248if (this.creationOptions?.supportDescriptionHighlights) {249this.descriptionNode = this._register(new HighlightedLabel(dom.append(descriptionContainer.element, dom.$('span.label-description')), { supportIcons: !!this.creationOptions.supportIcons }));250} else {251this.descriptionNode = this._register(new FastLabelNode(dom.append(descriptionContainer.element, dom.$('span.label-description'))));252}253}254255return this.descriptionNode;256}257}258259class Label {260261private label: string | string[] | undefined = undefined;262private singleLabel: HTMLElement | undefined = undefined;263private options: IIconLabelValueOptions | undefined;264265constructor(private container: HTMLElement) { }266267setLabel(label: string | string[], options?: IIconLabelValueOptions): void {268if (this.label === label && equals(this.options, options)) {269return;270}271272this.label = label;273this.options = options;274275if (typeof label === 'string') {276if (!this.singleLabel) {277this.container.textContent = '';278this.container.classList.remove('multiple');279this.singleLabel = dom.append(this.container, dom.$('a.label-name', { id: options?.domId }));280}281282this.singleLabel.textContent = label;283} else {284this.container.textContent = '';285this.container.classList.add('multiple');286this.singleLabel = undefined;287288for (let i = 0; i < label.length; i++) {289const l = label[i];290const id = options?.domId && `${options?.domId}_${i}`;291292dom.append(this.container, dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' }, l));293294if (i < label.length - 1) {295dom.append(this.container, dom.$('span.label-separator', undefined, options?.separator || '/'));296}297}298}299}300}301302function splitMatches(labels: string[], separator: string, matches: readonly IMatch[] | undefined): IMatch[][] | undefined {303if (!matches) {304return undefined;305}306307let labelStart = 0;308309return labels.map(label => {310const labelRange = { start: labelStart, end: labelStart + label.length };311312const result = matches313.map(match => Range.intersect(labelRange, match))314.filter(range => !Range.isEmpty(range))315.map(({ start, end }) => ({ start: start - labelStart, end: end - labelStart }));316317labelStart = labelRange.end + separator.length;318return result;319});320}321322class LabelWithHighlights extends Disposable {323324private label: string | string[] | undefined = undefined;325private singleLabel: HighlightedLabel | undefined = undefined;326private options: IIconLabelValueOptions | undefined;327328constructor(private container: HTMLElement, private supportIcons: boolean) {329super();330}331332setLabel(label: string | string[], options?: IIconLabelValueOptions): void {333if (this.label === label && equals(this.options, options)) {334return;335}336337this.label = label;338this.options = options;339340if (typeof label === 'string') {341if (!this.singleLabel) {342this.container.textContent = '';343this.container.classList.remove('multiple');344this.singleLabel = this._register(new HighlightedLabel(dom.append(this.container, dom.$('a.label-name', { id: options?.domId })), { supportIcons: this.supportIcons }));345}346347this.singleLabel.set(label, options?.matches, undefined, options?.labelEscapeNewLines);348} else {349this.container.textContent = '';350this.container.classList.add('multiple');351this.singleLabel = undefined;352353const separator = options?.separator || '/';354const matches = splitMatches(label, separator, options?.matches);355356for (let i = 0; i < label.length; i++) {357const l = label[i];358const m = matches ? matches[i] : undefined;359const id = options?.domId && `${options?.domId}_${i}`;360361const name = dom.$('a.label-name', { id, 'data-icon-label-count': label.length, 'data-icon-label-index': i, 'role': 'treeitem' });362const highlightedLabel = this._register(new HighlightedLabel(dom.append(this.container, name), { supportIcons: this.supportIcons }));363highlightedLabel.set(l, m, undefined, options?.labelEscapeNewLines);364365if (i < label.length - 1) {366dom.append(name, dom.$('span.label-separator', undefined, separator));367}368}369}370}371}372373374