From fb62f3e8a9549fb1fc57196cd977fbe7c9a8d897 Mon Sep 17 00:00:00 2001 From: Jack Franklin Date: Thu, 26 Nov 2020 09:24:27 +0000 Subject: [PATCH] [New Linkifier] Add web component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This CL creates a new 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 Reviewed-by: Alfonso CastaƱo Reviewed-by: Paul Lewis --- all_devtools_modules.gni | 1 + devtools_grd_files.gni | 1 + front_end/component_docs/BUILD.gn | 1 + front_end/component_docs/linkifier/BUILD.gn | 14 +++ front_end/component_docs/linkifier/url.html | 50 +++++++++++ front_end/components/Linkifier.js | 31 +++++++ front_end/ui/components/BUILD.gn | 2 + front_end/ui/components/Linkifier.ts | 88 +++++++++++++++++++ front_end/ui/components/components.ts | 2 + .../unittests/front_end/helpers/DOMHelpers.ts | 7 +- .../front_end/ui/components/BUILD.gn | 1 + .../front_end/ui/components/Linkifier_test.ts | 68 ++++++++++++++ 12 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 front_end/component_docs/linkifier/BUILD.gn create mode 100644 front_end/component_docs/linkifier/url.html create mode 100644 front_end/ui/components/Linkifier.ts create mode 100644 test/unittests/front_end/ui/components/Linkifier_test.ts diff --git a/all_devtools_modules.gni b/all_devtools_modules.gni index 25757ceb43..d5c4aba8f5 100644 --- a/all_devtools_modules.gni +++ b/all_devtools_modules.gni @@ -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", diff --git a/devtools_grd_files.gni b/devtools_grd_files.gni index 19acada63d..ca8c6507f0 100644 --- a/devtools_grd_files.gni +++ b/devtools_grd_files.gni @@ -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", diff --git a/front_end/component_docs/BUILD.gn b/front_end/component_docs/BUILD.gn index a36a1edde0..e224b2198b 100644 --- a/front_end/component_docs/BUILD.gn +++ b/front_end/component_docs/BUILD.gn @@ -17,6 +17,7 @@ group("component_docs") { "./issue_survey_link", "./layout_pane", "./linear_memory_inspector", + "./linkifier", "./perf_piechart", "./theme_colors", "./webvitals", diff --git a/front_end/component_docs/linkifier/BUILD.gn b/front_end/component_docs/linkifier/BUILD.gn new file mode 100644 index 0000000000..cd17369a0a --- /dev/null +++ b/front_end/component_docs/linkifier/BUILD.gn @@ -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", + ] +} diff --git a/front_end/component_docs/linkifier/url.html b/front_end/component_docs/linkifier/url.html new file mode 100644 index 0000000000..ae55ab9c7f --- /dev/null +++ b/front_end/component_docs/linkifier/url.html @@ -0,0 +1,50 @@ + + + + + + + Linkifier example with URL + + + + +
+
+ + + + diff --git a/front_end/components/Linkifier.js b/front_end/components/Linkifier.js index 87c64a4150..8dbb98e5be 100644 --- a/front_end/components/Linkifier.js +++ b/front_end/components/Linkifier.js @@ -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 diff --git a/front_end/ui/components/BUILD.gn b/front_end/ui/components/BUILD.gn index 23c65eb561..8734eb8fd0 100644 --- a/front_end/ui/components/BUILD.gn +++ b/front_end/ui/components/BUILD.gn @@ -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", ] diff --git a/front_end/ui/components/Linkifier.ts b/front_end/ui/components/Linkifier.ts new file mode 100644 index 0000000000..8d537b3b48 --- /dev/null +++ b/front_end/ui/components/Linkifier.ts @@ -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` + + ${this.linkText()} + `, this.shadow, { eventContext: this}); + // clang-format on + } +} + +customElements.define('devtools-linkifier', Linkifier); + +declare global { + interface HTMLElementTagNameMap { + 'devtools-linkifier': Linkifier; + } +} diff --git a/front_end/ui/components/components.ts b/front_end/ui/components/components.ts index 9e7a697489..c896af171b 100644 --- a/front_end/ui/components/components.ts +++ b/front_end/ui/components/components.ts @@ -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, }; diff --git a/test/unittests/front_end/helpers/DOMHelpers.ts b/test/unittests/front_end/helpers/DOMHelpers.ts index 9587e2001f..6176c610f5 100644 --- a/test/unittests/front_end/helpers/DOMHelpers.ts +++ b/test/unittests/front_end/helpers/DOMHelpers.ts @@ -137,14 +137,11 @@ export function waitForScrollLeft(element: T, desiredScrollLe } /** - * Dispatches a mouse click event. Errors if the event was not dispatched successfully. + * Dispatches a mouse click event. */ export function dispatchClickEvent(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); } /** diff --git a/test/unittests/front_end/ui/components/BUILD.gn b/test/unittests/front_end/ui/components/BUILD.gn index a893dfb35c..59263c5fb0 100644 --- a/test/unittests/front_end/ui/components/BUILD.gn +++ b/test/unittests/front_end/ui/components/BUILD.gn @@ -6,6 +6,7 @@ ts_library("components") { "DataGridController_test.ts", "DataGridHelpers.ts", "DataGrid_test.ts", + "Linkifier_test.ts", ] deps = [ diff --git a/test/unittests/front_end/ui/components/Linkifier_test.ts b/test/unittests/front_end/ui/components/Linkifier_test.ts new file mode 100644 index 0000000000..ab51e9dfe2 --- /dev/null +++ b/test/unittests/front_end/ui/components/Linkifier_test.ts @@ -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(component, 'linkifier-click'); + dispatchClickEvent(link, { + cancelable: true, + }); + const clickEvent = await clickEventPromise; + assert.deepEqual(clickEvent.data, { + url: 'https://example.com', + lineNumber: 1, + columnNumber: 50, + }); + }); +});