UNPKG

chrome-devtools-frontend

Version:
156 lines (134 loc) • 6.35 kB
// Copyright 2023 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 * as i18n from '../../../core/i18n/i18n.js'; import * as Trace from '../../../models/trace/trace.js'; import * as ComponentHelpers from '../../../ui/components/helpers/helpers.js'; import * as UI from '../../../ui/legacy/legacy.js'; import * as Lit from '../../../ui/lit/lit.js'; import * as VisualLogging from '../../../ui/visual_logging/visual_logging.js'; import {flattenBreadcrumbs} from './Breadcrumbs.js'; import breadcrumbsUIStyles from './breadcrumbsUI.css.js'; const {render, html} = Lit; const UIStrings = { /** *@description A context menu item in the Minimap Breadcrumb context menu. * This context menu option activates the breadcrumb that the context menu was opened on. */ activateBreadcrumb: 'Activate breadcrumb', /** *@description A context menu item in the Minimap Breadcrumb context menu. * This context menu option removed all the child breadcrumbs and activates * the breadcrumb that the context menu was opened on. */ removeChildBreadcrumbs: 'Remove child breadcrumbs', } as const; const str_ = i18n.i18n.registerUIStrings('panels/timeline/components/BreadcrumbsUI.ts', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); // `initialBreadcrumb` is the first breadcrumb in the breadcrumbs linked list. Since // breadcrumbs are a linked list, the first breadcrumb is enought to be able to iterate through all of them. // // `activeBreadcrumb` is the currently active breadcrumb that the timeline is limited to. export interface BreadcrumbsUIData { initialBreadcrumb: Trace.Types.File.Breadcrumb; activeBreadcrumb: Trace.Types.File.Breadcrumb; } export class BreadcrumbActivatedEvent extends Event { static readonly eventName = 'breadcrumbactivated'; constructor(public breadcrumb: Trace.Types.File.Breadcrumb, public childBreadcrumbsRemoved?: boolean) { super(BreadcrumbActivatedEvent.eventName); } } export class BreadcrumbsUI extends HTMLElement { readonly #shadow = this.attachShadow({mode: 'open'}); #initialBreadcrumb: Trace.Types.File.Breadcrumb|null = null; #activeBreadcrumb: Trace.Types.File.Breadcrumb|null = null; set data(data: BreadcrumbsUIData) { this.#initialBreadcrumb = data.initialBreadcrumb; this.#activeBreadcrumb = data.activeBreadcrumb; void ComponentHelpers.ScheduledRender.scheduleRender(this, this.#render); } #activateBreadcrumb(breadcrumb: Trace.Types.File.Breadcrumb): void { this.#activeBreadcrumb = breadcrumb; this.dispatchEvent(new BreadcrumbActivatedEvent(breadcrumb)); } #showBreadcrumbsAndScrollLastCrumbIntoView(): void { const container = this.#shadow.querySelector<HTMLDivElement>('.breadcrumbs'); if (!container) { return; } // Display Breadcrumbs after at least one was created container.style.display = 'flex'; requestAnimationFrame(() => { // If the width of all the elements is greater than the width of the // container, we need to scroll the last element into view. if (container.scrollWidth - container.clientWidth > 0) { requestAnimationFrame(() => { // For some unknown reason, if we scroll after one rAF, the values // are slightly off by a few pixels which means that the element does // not get properly scrolled fully into view. Therefore we wait for a // second rAF, at which point the values are correct and this will // scroll the container fully to ensure the last breadcrumb is fully // visible. container.scrollLeft = container.scrollWidth - container.clientWidth; }); } }); } #onContextMenu(event: Event, breadcrumb: Trace.Types.File.Breadcrumb): void { const menu = new UI.ContextMenu.ContextMenu(event); menu.defaultSection().appendItem(i18nString(UIStrings.activateBreadcrumb), () => { this.dispatchEvent(new BreadcrumbActivatedEvent(breadcrumb)); }); menu.defaultSection().appendItem(i18nString(UIStrings.removeChildBreadcrumbs), () => { this.dispatchEvent(new BreadcrumbActivatedEvent(breadcrumb, true)); }); void menu.show(); } #renderElement(breadcrumb: Trace.Types.File.Breadcrumb, index: number): Lit.LitTemplate { const breadcrumbRange = Trace.Helpers.Timing.microToMilli(breadcrumb.window.range); // clang-format off return html` <div class="breadcrumb" @contextmenu=${(event: Event) => this.#onContextMenu(event, breadcrumb)} @click=${() => this.#activateBreadcrumb(breadcrumb)} jslog=${VisualLogging.item('timeline.breadcrumb-select').track({click: true})}> <span class="${(breadcrumb === this.#activeBreadcrumb) ? 'active-breadcrumb' : ''} range"> ${(index === 0) ? `Full range (${i18n.TimeUtilities.preciseMillisToString(breadcrumbRange, 2)})` : `${i18n.TimeUtilities.preciseMillisToString(breadcrumbRange, 2)}`} </span> </div> ${breadcrumb.child !== null ? html` <devtools-icon .data=${{ iconName: 'chevron-right', color: 'var(--icon-default)', width: '16px', height: '16px', }}>` : ''} `; // clang-format on } #render(): void { // clang-format off const output = html` <style>${breadcrumbsUIStyles}</style> ${this.#initialBreadcrumb === null ? Lit.nothing : html`<div class="breadcrumbs" jslog=${VisualLogging.section('breadcrumbs')}> ${flattenBreadcrumbs(this.#initialBreadcrumb).map((breadcrumb, index) => this.#renderElement(breadcrumb, index))} </div>`} `; // clang-format on render(output, this.#shadow, {host: this}); if (this.#initialBreadcrumb?.child) { // If we have >1 crumbs show breadcrumbs and ensure the last one is visible by scrolling the container. this.#showBreadcrumbsAndScrollLastCrumbIntoView(); } } } customElements.define('devtools-breadcrumbs-ui', BreadcrumbsUI); declare global { interface HTMLElementTagNameMap { 'devtools-breadcrumbs-ui': BreadcrumbsUI; } }