mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-03 02:21:05 +08:00
[New Linkifier] Add <devtools-linkifier> web component
This CL creates a new <devtools-linkifier> component that will eventually replace the legacy linkifier code. For now this component only supports taking a string URL, but in time (and as we need) we can extend it to support all the inputs that the legacy Linkifier code uses. It works by emitting an event that is picked up by the legacy linkifier and threaded through the legacy system. Bug: 1149403 Change-Id: I76023052d818ab0f296db887fb03d6761ca4796b Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2558297 Commit-Queue: Jack Franklin <jacktfranklin@chromium.org> Reviewed-by: Alfonso Castaño <alcastano@google.com> Reviewed-by: Paul Lewis <aerotwist@chromium.org>
This commit is contained in:
committed by
Commit Bot
parent
7b0711cb76
commit
fb62f3e8a9
@@ -757,6 +757,7 @@ all_typescript_module_sources = [
|
||||
"ui/components/DataGridController.js",
|
||||
"ui/components/DataGridRenderers.js",
|
||||
"ui/components/DataGridUtils.js",
|
||||
"ui/components/Linkifier.js",
|
||||
"ui/components/components.js",
|
||||
"ui/utils/append-style.js",
|
||||
"ui/utils/create-shadow-root-with-core-styles.js",
|
||||
|
||||
@@ -1140,6 +1140,7 @@ grd_files_debug_sources = [
|
||||
"front_end/ui/components/DataGridController.js",
|
||||
"front_end/ui/components/DataGridRenderers.js",
|
||||
"front_end/ui/components/DataGridUtils.js",
|
||||
"front_end/ui/components/Linkifier.js",
|
||||
"front_end/ui/utils/append-style.js",
|
||||
"front_end/ui/utils/create-shadow-root-with-core-styles.js",
|
||||
"front_end/ui/utils/focus-changed.js",
|
||||
|
||||
@@ -17,6 +17,7 @@ group("component_docs") {
|
||||
"./issue_survey_link",
|
||||
"./layout_pane",
|
||||
"./linear_memory_inspector",
|
||||
"./linkifier",
|
||||
"./perf_piechart",
|
||||
"./theme_colors",
|
||||
"./webvitals",
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# Copyright 2020 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.
|
||||
|
||||
import("../../../scripts/build/ninja/copy.gni")
|
||||
|
||||
copy_to_gen("linkifier") {
|
||||
sources = [ "url.html" ]
|
||||
|
||||
deps = [
|
||||
"../../component_helpers:bundle",
|
||||
"../../ui/components:bundle",
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<!--
|
||||
Copyright 2020 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.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Linkifier example with URL</title>
|
||||
<style>
|
||||
body {
|
||||
padding: 50px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="container">
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
import * as ComponentHelpers from '../../component_helpers/component_helpers.js';
|
||||
import * as Components from '../../ui/components/components.js'
|
||||
|
||||
ComponentHelpers.ComponentServerSetup.setup().then(() => renderComponent())
|
||||
|
||||
const renderComponent = () => {
|
||||
const link = new Components.Linkifier.Linkifier();
|
||||
|
||||
link.data = {
|
||||
url: 'example.com',
|
||||
lineNumber: 11,
|
||||
columnNumber: 1,
|
||||
}
|
||||
|
||||
const container = document.getElementById('container');
|
||||
|
||||
container.addEventListener('linkifier-click', function(event) {
|
||||
const data = JSON.stringify(event.data, null, 2)
|
||||
alert(`Linkifier click: ${data}`)
|
||||
|
||||
})
|
||||
container.appendChild(link);
|
||||
}
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -744,6 +744,14 @@ export class Linkifier {
|
||||
return Linkifier.invokeFirstAction(linkInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {!_LinkInfo} linkInfo
|
||||
*/
|
||||
static _handleClickFromNewComponentLand(linkInfo) {
|
||||
Linkifier.invokeFirstAction(linkInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {!_LinkInfo} linkInfo
|
||||
* @return {boolean}
|
||||
@@ -974,6 +982,29 @@ export class LinkHandlerSettingUI {
|
||||
}
|
||||
}
|
||||
|
||||
let listeningToNewEvents = false;
|
||||
function listenForNewComponentLinkifierEvents() {
|
||||
if (listeningToNewEvents) {
|
||||
return;
|
||||
}
|
||||
|
||||
listeningToNewEvents = true;
|
||||
|
||||
window.addEventListener(
|
||||
'linkifier-click',
|
||||
/**
|
||||
*
|
||||
* @param {!Event} event
|
||||
*/
|
||||
function(event) {
|
||||
const unknownEvent = /** @type {?} */ (event);
|
||||
const eventWithData = /** @type {!{data: !_LinkInfo}} */ (unknownEvent);
|
||||
Linkifier._handleClickFromNewComponentLand(eventWithData.data);
|
||||
});
|
||||
}
|
||||
|
||||
listenForNewComponentLinkifierEvents();
|
||||
|
||||
/**
|
||||
* @implements {UI.ContextMenu.Provider}
|
||||
* @unrestricted
|
||||
|
||||
@@ -11,8 +11,10 @@ devtools_module("components") {
|
||||
"DataGridController.ts",
|
||||
"DataGridRenderers.ts",
|
||||
"DataGridUtils.ts",
|
||||
"Linkifier.ts",
|
||||
]
|
||||
deps = [
|
||||
"../../bindings:bundle",
|
||||
"../../text_utils:bundle",
|
||||
"../../third_party/lit-html:bundle",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2020 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.
|
||||
|
||||
import * as Bindings from '../../bindings/bindings.js';
|
||||
import * as LitHtml from '../../third_party/lit-html/lit-html.js';
|
||||
|
||||
export interface LinkifierData {
|
||||
url: string;
|
||||
lineNumber?: number;
|
||||
columnNumber?: number;
|
||||
}
|
||||
|
||||
export class LinkifierClick extends Event {
|
||||
data: LinkifierData;
|
||||
|
||||
constructor(data: LinkifierData) {
|
||||
super('linkifier-click', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
});
|
||||
this.data = data;
|
||||
}
|
||||
}
|
||||
|
||||
export class Linkifier extends HTMLElement {
|
||||
private readonly shadow = this.attachShadow({mode: 'open'});
|
||||
private url: string = '';
|
||||
private lineNumber?: number;
|
||||
private columnNumber?: number;
|
||||
|
||||
set data(data: LinkifierData) {
|
||||
this.url = data.url;
|
||||
this.lineNumber = data.lineNumber;
|
||||
this.columnNumber = data.columnNumber;
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
private onLinkActivation(event: Event) {
|
||||
event.preventDefault();
|
||||
this.dispatchEvent(new LinkifierClick({
|
||||
url: this.url,
|
||||
lineNumber: this.lineNumber,
|
||||
columnNumber: this.columnNumber,
|
||||
}));
|
||||
}
|
||||
|
||||
private linkText(): string {
|
||||
if (this.url) {
|
||||
const displayName = Bindings.ResourceUtils.displayNameForURL(this.url);
|
||||
let text = `${displayName}`;
|
||||
if (typeof this.lineNumber !== 'undefined') {
|
||||
text += `:${this.lineNumber + 1}`;
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
throw new Error('New linkifier component error: don\'t know how to generate link text for given arguments');
|
||||
}
|
||||
|
||||
private render() {
|
||||
if (!this.url) {
|
||||
throw new Error('Cannot construct a Linkifier without providing a valid string URL.');
|
||||
}
|
||||
// Disabled until https://crbug.com/1079231 is fixed.
|
||||
// clang-format off
|
||||
return LitHtml.render(LitHtml.html`
|
||||
<style>
|
||||
.link:link, .link:visited {
|
||||
color: var(--link-color);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
<a class="link" href=${this.url} @click=${this.onLinkActivation}>${this.linkText()}</a>
|
||||
`, this.shadow, { eventContext: this});
|
||||
// clang-format on
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('devtools-linkifier', Linkifier);
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'devtools-linkifier': Linkifier;
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,12 @@ import * as DataGrid from './DataGrid.js';
|
||||
import * as DataGridController from './DataGridController.js';
|
||||
import * as DataGridRenderers from './DataGridRenderers.js';
|
||||
import * as DataGridUtils from './DataGridUtils.js';
|
||||
import * as Linkifier from './Linkifier.js';
|
||||
|
||||
export {
|
||||
DataGrid,
|
||||
DataGridController,
|
||||
DataGridRenderers,
|
||||
DataGridUtils,
|
||||
Linkifier,
|
||||
};
|
||||
|
||||
@@ -137,14 +137,11 @@ export function waitForScrollLeft<T extends Element>(element: T, desiredScrollLe
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a mouse click event. Errors if the event was not dispatched successfully.
|
||||
* Dispatches a mouse click event.
|
||||
*/
|
||||
export function dispatchClickEvent<T extends Element>(element: T, options: MouseEventInit = {}) {
|
||||
const clickEvent = new MouseEvent('click', options);
|
||||
const success = element.dispatchEvent(clickEvent);
|
||||
if (!success) {
|
||||
assert.fail('Failed to trigger click event successfully.');
|
||||
}
|
||||
element.dispatchEvent(clickEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ ts_library("components") {
|
||||
"DataGridController_test.ts",
|
||||
"DataGridHelpers.ts",
|
||||
"DataGrid_test.ts",
|
||||
"Linkifier_test.ts",
|
||||
]
|
||||
|
||||
deps = [
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright 2020 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.
|
||||
|
||||
import * as UIComponents from '../../../../../front_end/ui/components/components.js';
|
||||
|
||||
import {assertElement, assertShadowRoot, dispatchClickEvent, getEventPromise, renderElementIntoDOM} from '../../helpers/DOMHelpers.js';
|
||||
const {assert} = chai;
|
||||
|
||||
describe('Linkifier', () => {
|
||||
it('renders a link when given a URL', () => {
|
||||
const component = new UIComponents.Linkifier.Linkifier();
|
||||
component.data = {
|
||||
url: 'https://example.com',
|
||||
};
|
||||
renderElementIntoDOM(component);
|
||||
assertShadowRoot(component.shadowRoot);
|
||||
const link = component.shadowRoot.querySelector('a');
|
||||
assertElement(link, HTMLAnchorElement);
|
||||
assert.strictEqual(link.innerText, 'example.com');
|
||||
});
|
||||
|
||||
it('throws when given an invalid URL', () => {
|
||||
const component = new UIComponents.Linkifier.Linkifier();
|
||||
assert.throws(() => {
|
||||
component.data = {
|
||||
url: '',
|
||||
};
|
||||
}, 'Cannot construct a Linkifier without providing a valid string URL.');
|
||||
});
|
||||
|
||||
it('appends the line number to the URL if given, and adds one to deal with 0 indexing', () => {
|
||||
const component = new UIComponents.Linkifier.Linkifier();
|
||||
component.data = {
|
||||
url: 'https://example.com',
|
||||
lineNumber: 1,
|
||||
};
|
||||
renderElementIntoDOM(component);
|
||||
assertShadowRoot(component.shadowRoot);
|
||||
const link = component.shadowRoot.querySelector('a');
|
||||
assertElement(link, HTMLAnchorElement);
|
||||
assert.strictEqual(link.innerText, 'example.com:2');
|
||||
});
|
||||
|
||||
it('emits an event when clicked', async () => {
|
||||
const component = new UIComponents.Linkifier.Linkifier();
|
||||
component.data = {
|
||||
url: 'https://example.com',
|
||||
lineNumber: 1,
|
||||
columnNumber: 50,
|
||||
};
|
||||
renderElementIntoDOM(component);
|
||||
assertShadowRoot(component.shadowRoot);
|
||||
const link = component.shadowRoot.querySelector('a');
|
||||
assertElement(link, HTMLAnchorElement);
|
||||
|
||||
const clickEventPromise = getEventPromise<UIComponents.Linkifier.LinkifierClick>(component, 'linkifier-click');
|
||||
dispatchClickEvent(link, {
|
||||
cancelable: true,
|
||||
});
|
||||
const clickEvent = await clickEventPromise;
|
||||
assert.deepEqual(clickEvent.data, {
|
||||
url: 'https://example.com',
|
||||
lineNumber: 1,
|
||||
columnNumber: 50,
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user