| // Copyright 2025 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| /* eslint-disable @devtools/no-lit-render-outside-of-view */ |
| |
| import * as Platform from '../../../core/platform/platform.js'; |
| import * as UIHelpers from '../../helpers/helpers.js'; |
| import {html, render} from '../../lit/lit.js'; |
| import * as VisualLogging from '../../visual_logging/visual_logging.js'; |
| |
| import linkStyles from './link.css.js'; |
| |
| /** |
| * A simple icon component to handle external links. |
| * Handles both normal links `https://example.com` |
| * and chrome links `chrome://flags`. |
| * |
| * html` |
| * <devtools-link href=""></devtools-link> |
| * `; |
| * ``` |
| * |
| * @property href - The href to the place the link wants to navigate |
| * @property jslogContext - The `"jslogcontext"` attribute is reflected as a property. |
| * |
| * @attribute href - The href to the place the link wants to navigate |
| * @attribute jslogcontext - |
| * The context for the `jslog` attribute. A `jslog` |
| * attribute is generated automatically with the |
| * provided context. |
| */ |
| export class Link extends HTMLElement { |
| readonly #shadow = this.attachShadow({mode: 'open'}); |
| |
| static readonly observedAttributes = ['href', 'jslogcontext']; |
| |
| connectedCallback(): void { |
| if (!this.hasAttribute('tabindex')) { |
| this.setAttribute('tabindex', '0'); |
| } |
| this.#setDefaultTitle(); |
| this.#onJslogContextChange(); |
| |
| this.setAttribute('role', 'link'); |
| this.setAttribute('target', '_blank'); |
| |
| this.addEventListener('click', this.#onClick); |
| this.addEventListener('keydown', this.#onKeyDown); |
| this.#render(); |
| } |
| |
| disconnectedCallback(): void { |
| this.removeEventListener('click', this.#onClick); |
| this.removeEventListener('keydown', this.#onKeyDown); |
| } |
| |
| #handleOpeningLink(event: Event): void { |
| const href = this.href as Platform.DevToolsPath.UrlString | undefined; |
| if (!href) { |
| return; |
| } |
| |
| UIHelpers.openInNewTab(href); |
| |
| event.consume(); |
| } |
| |
| get href(): string|null { |
| return this.getAttribute('href'); |
| } |
| |
| set href(href: Platform.DevToolsPath.UrlString) { |
| this.setAttribute('href', href); |
| } |
| |
| get jslogContext(): string|null { |
| return this.getAttribute('jslogcontext'); |
| } |
| |
| set jslogContext(jslogContext: string) { |
| this.setAttribute('jslogcontext', jslogContext); |
| } |
| |
| #onJslogContextChange(): void { |
| const jslogContext = this.jslogContext ?? undefined; |
| const jslog = VisualLogging.link().track({click: true, keydown: 'Enter|Space'}).context(jslogContext); |
| this.setAttribute('jslog', jslog.toString()); |
| } |
| |
| #setDefaultTitle(): void { |
| if (!this.hasAttribute('title') && this.href) { |
| this.setAttribute('title', this.href); |
| } |
| } |
| |
| attributeChangedCallback( |
| name: string, |
| oldValue: string|null, |
| newValue: string|null, |
| ): void { |
| if (oldValue !== newValue) { |
| return; |
| } |
| if (name === 'jslogcontext') { |
| return this.#onJslogContextChange(); |
| } |
| |
| if (name === 'href') { |
| this.#setDefaultTitle(); |
| } |
| |
| this.#render(); |
| } |
| |
| #onClick = (event: Event): void => { |
| this.#handleOpeningLink(event); |
| }; |
| |
| #onKeyDown = (event: KeyboardEvent): void => { |
| if (Platform.KeyboardUtilities.isEnterOrSpaceKey(event)) { |
| this.#handleOpeningLink(event); |
| } |
| }; |
| |
| #render(): void { |
| // clang-format off |
| render( |
| html`<style> |
| ${linkStyles} |
| </style><slot></slot>`, |
| this.#shadow, |
| { host: this }, |
| ); |
| // clang-format on |
| } |
| |
| /** |
| * Should be used only by old code relying on imperative API, |
| * which we are activly migrating away from. |
| * @deprecated |
| */ |
| static create( |
| url: string, |
| linkText?: string, |
| className?: string, |
| jsLogContext?: string, |
| tabindex = 0, |
| ): Link { |
| const link = new Link(); |
| link.href = url as Platform.DevToolsPath.UrlString; |
| linkText = linkText ?? url; |
| link.textContent = Platform.StringUtilities.trimMiddle(linkText, 150); |
| |
| const classes = className ? `${className} devtools-link` : 'devtools-link'; |
| link.setAttribute('class', classes); |
| |
| if (jsLogContext) { |
| link.setAttribute('jslogcontext', jsLogContext); |
| } |
| |
| if (tabindex !== 0) { |
| link.setAttribute('tabindex', String(tabindex)); |
| } |
| |
| return link; |
| } |
| } |
| |
| customElements.define('devtools-link', Link); |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'devtools-link': Link; |
| } |
| } |