[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:
Jack Franklin
2020-11-26 09:48:03 +00:00
committed by Commit Bot
parent 7b0711cb76
commit fb62f3e8a9
12 changed files with 261 additions and 5 deletions
+1
View File
@@ -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",
+1
View File
@@ -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",
+1
View File
@@ -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>
+31
View File
@@ -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
+2
View File
@@ -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",
]
+88
View File
@@ -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;
}
}
+2
View File
@@ -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,
});
});
});