chrome-devtools-frontend
Version:
Chrome DevTools UI
220 lines (201 loc) • 8.69 kB
text/typescript
// Copyright 2021 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no-lit-render-outside-of-view */
import '../icon_button/icon_button.js';
import * as Common from '../../../core/common/common.js';
import * as i18n from '../../../core/i18n/i18n.js';
import * as IssuesManager from '../../../models/issues_manager/issues_manager.js';
import {html, render} from '../../lit/lit.js';
import type * as IconButton from '../icon_button/icon_button.js';
import issueCounterStyles from './issueCounter.css.js';
const UIStrings = {
/**
*@description Label for link to Issues tab, specifying how many issues there are.
*/
pageErrors: '{issueCount, plural, =1 {# page error} other {# page errors}}',
/**
*@description Label for link to Issues tab, specifying how many issues there are.
*/
breakingChanges: '{issueCount, plural, =1 {# breaking change} other {# breaking changes}}',
/**
*@description Label for link to Issues tab, specifying how many issues there are.
*/
possibleImprovements: '{issueCount, plural, =1 {# possible improvement} other {# possible improvements}}',
} as const;
const str_ = i18n.i18n.registerUIStrings('ui/components/issue_counter/IssueCounter.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export function getIssueKindIconData(issueKind: IssuesManager.Issue.IssueKind): IconButton.Icon.IconWithName {
switch (issueKind) {
case IssuesManager.Issue.IssueKind.PAGE_ERROR:
return {iconName: 'issue-cross-filled', color: 'var(--icon-error)', width: '20px', height: '20px'};
case IssuesManager.Issue.IssueKind.BREAKING_CHANGE:
return {iconName: 'issue-exclamation-filled', color: 'var(--icon-warning)', width: '20px', height: '20px'};
case IssuesManager.Issue.IssueKind.IMPROVEMENT:
return {iconName: 'issue-text-filled', color: 'var(--icon-info)', width: '20px', height: '20px'};
}
}
function toIconGroup({iconName, color, width, height}: IconButton.Icon.IconWithName, sizeOverride?: string):
IconButton.IconButton.IconWithTextData {
if (sizeOverride) {
return {iconName, iconColor: color, iconWidth: sizeOverride, iconHeight: sizeOverride};
}
return {iconName, iconColor: color, iconWidth: width, iconHeight: height};
}
export const enum DisplayMode {
OMIT_EMPTY = 'OmitEmpty',
SHOW_ALWAYS = 'ShowAlways',
ONLY_MOST_IMPORTANT = 'OnlyMostImportant',
}
export interface IssueCounterData {
clickHandler?: () => void;
tooltipCallback?: () => void;
leadingText?: string;
displayMode?: DisplayMode;
issuesManager: IssuesManager.IssuesManager.IssuesManager;
throttlerTimeout?: number;
accessibleName?: string;
compact?: boolean;
}
// Lazily instantiate the formatter as the constructor takes 50ms+
// TODO: move me and others like me to i18n module
const listFormatter = (function defineFormatter() {
let intlListFormat: Intl.ListFormat;
return {
format(...args: Parameters<Intl.ListFormat['format']>): ReturnType<Intl.ListFormat['format']> {
if (!intlListFormat) {
const opts: Intl.ListFormatOptions = {type: 'unit', style: 'short'};
intlListFormat = new Intl.ListFormat(i18n.DevToolsLocale.DevToolsLocale.instance().locale, opts);
}
return intlListFormat.format(...args);
},
};
})();
export function getIssueCountsEnumeration(
issuesManager: IssuesManager.IssuesManager.IssuesManager, omitEmpty = true): string {
const counts: [number, number, number] = [
issuesManager.numberOfIssues(IssuesManager.Issue.IssueKind.PAGE_ERROR),
issuesManager.numberOfIssues(IssuesManager.Issue.IssueKind.BREAKING_CHANGE),
issuesManager.numberOfIssues(IssuesManager.Issue.IssueKind.IMPROVEMENT),
];
const phrases = [
i18nString(UIStrings.pageErrors, {issueCount: counts[0]}),
i18nString(UIStrings.breakingChanges, {issueCount: counts[1]}),
i18nString(UIStrings.possibleImprovements, {issueCount: counts[2]}),
];
return listFormatter.format(phrases.filter((_, i) => omitEmpty ? counts[i] > 0 : true));
}
export class IssueCounter extends HTMLElement {
readonly #shadow = this.attachShadow({mode: 'open'});
#clickHandler: undefined|(() => void) = undefined;
#tooltipCallback: undefined|(() => void) = undefined;
#leadingText = '';
#throttler: undefined|Common.Throttler.Throttler;
#counts: [number, number, number] = [0, 0, 0];
#displayMode: DisplayMode = DisplayMode.OMIT_EMPTY;
#issuesManager: IssuesManager.IssuesManager.IssuesManager|undefined = undefined;
#accessibleName: string|undefined = undefined;
#throttlerTimeout: number|undefined;
#compact = false;
scheduleUpdate(): void {
if (this.#throttler) {
void this.#throttler.schedule(async () => this.#render());
} else {
this.#render();
}
}
set data(data: IssueCounterData) {
this.#clickHandler = data.clickHandler;
this.#leadingText = data.leadingText ?? '';
this.#tooltipCallback = data.tooltipCallback;
this.#displayMode = data.displayMode ?? DisplayMode.OMIT_EMPTY;
this.#accessibleName = data.accessibleName;
this.#throttlerTimeout = data.throttlerTimeout;
this.#compact = Boolean(data.compact);
if (this.#issuesManager !== data.issuesManager) {
this.#issuesManager?.removeEventListener(
IssuesManager.IssuesManager.Events.ISSUES_COUNT_UPDATED, this.scheduleUpdate, this);
this.#issuesManager = data.issuesManager;
this.#issuesManager.addEventListener(
IssuesManager.IssuesManager.Events.ISSUES_COUNT_UPDATED, this.scheduleUpdate, this);
}
if (data.throttlerTimeout !== 0) {
this.#throttler = new Common.Throttler.Throttler(data.throttlerTimeout ?? 100);
} else {
this.#throttler = undefined;
}
this.scheduleUpdate();
}
get data(): IssueCounterData {
return {
clickHandler: this.#clickHandler,
leadingText: this.#leadingText,
tooltipCallback: this.#tooltipCallback,
displayMode: this.#displayMode,
accessibleName: this.#accessibleName,
throttlerTimeout: this.#throttlerTimeout,
compact: this.#compact,
issuesManager: this.#issuesManager as IssuesManager.IssuesManager.IssuesManager,
};
}
#render(): void {
if (!this.#issuesManager) {
return;
}
this.#counts = [
this.#issuesManager.numberOfIssues(IssuesManager.Issue.IssueKind.PAGE_ERROR),
this.#issuesManager.numberOfIssues(IssuesManager.Issue.IssueKind.BREAKING_CHANGE),
this.#issuesManager.numberOfIssues(IssuesManager.Issue.IssueKind.IMPROVEMENT),
];
const importance = [
IssuesManager.Issue.IssueKind.PAGE_ERROR,
IssuesManager.Issue.IssueKind.BREAKING_CHANGE,
IssuesManager.Issue.IssueKind.IMPROVEMENT,
];
const mostImportant = importance[this.#counts.findIndex(x => x > 0) ?? 2];
const countToString = (kind: IssuesManager.Issue.IssueKind, count: number): string|undefined => {
switch (this.#displayMode) {
case DisplayMode.OMIT_EMPTY:
return count > 0 ? `${count}` : undefined;
case DisplayMode.SHOW_ALWAYS:
return `${count}`;
case DisplayMode.ONLY_MOST_IMPORTANT:
return kind === mostImportant ? `${count}` : undefined;
}
};
const iconSize = '2ex';
const data: IconButton.IconButton.IconButtonData = {
groups: [
{
...toIconGroup(getIssueKindIconData(IssuesManager.Issue.IssueKind.PAGE_ERROR), iconSize),
text: countToString(IssuesManager.Issue.IssueKind.PAGE_ERROR, this.#counts[0]),
},
{
...toIconGroup(getIssueKindIconData(IssuesManager.Issue.IssueKind.BREAKING_CHANGE), iconSize),
text: countToString(IssuesManager.Issue.IssueKind.BREAKING_CHANGE, this.#counts[1]),
},
{
...toIconGroup(getIssueKindIconData(IssuesManager.Issue.IssueKind.IMPROVEMENT), iconSize),
text: countToString(IssuesManager.Issue.IssueKind.IMPROVEMENT, this.#counts[2]),
},
],
clickHandler: this.#clickHandler,
leadingText: this.#leadingText,
accessibleName: this.#accessibleName,
compact: this.#compact,
};
render(
html`
<style>${issueCounterStyles}</style>
<icon-button .data=${data} .accessibleName=${this.#accessibleName}></icon-button>
`,
this.#shadow, {host: this});
this.#tooltipCallback?.();
}
}
customElements.define('devtools-issue-counter', IssueCounter);
declare global {
interface HTMLElementTagNameMap {
'devtools-issue-counter': IssueCounter;
}
}