mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-11 14:14:36 +08:00
ui/Tooltip.js overrides the HTMLElement prototype to override the title property. Rather than using this property, we shoul be calling Tooltip.install directly. This makes sure that new components are not relying on the behavior of the legacy prototype patching. These usages have been manually audited using the following regexes: Search: ([\S]+)\.title = ([^;]+); Replace: UI.Tooltip.Tooltip.install($1, $2); Note that there are classes in DevTools that also have a title property. Most notably `TreeElement`. We should not be replacing these, as they do not inherit from HTMLElement. Luckily, we are running TypeScript to make sure we don't call `Tooltip.install` with a non-HTMLElement. A follow-up CL will clean up the getters. R=jacktfranklin@chromium.org Bug: 1150762 No-presubmit: True Change-Id: I5928e75c70293531849e0576f4fb2a2a8b3e02d2 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2555060 Commit-Queue: Tim van der Lippe <tvanderlippe@chromium.org> Auto-Submit: Tim van der Lippe <tvanderlippe@chromium.org> Reviewed-by: Jack Franklin <jacktfranklin@chromium.org>
357 lines
11 KiB
JavaScript
357 lines
11 KiB
JavaScript
// Copyright (c) 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 DataGrid from '../data_grid/data_grid.js';
|
|
import * as Host from '../host/host.js';
|
|
import * as i18n from '../i18n/i18n.js';
|
|
import * as SDK from '../sdk/sdk.js'; // eslint-disable-line no-unused-vars
|
|
import * as TextUtils from '../text_utils/text_utils.js';
|
|
import * as UI from '../ui/ui.js';
|
|
|
|
export const UIStrings = {
|
|
/**
|
|
*@description Text for the status of something
|
|
*/
|
|
status: 'Status',
|
|
/**
|
|
*@description Text for web URLs
|
|
*/
|
|
url: 'URL',
|
|
/**
|
|
*@description Text for the initiator of something
|
|
*/
|
|
initiator: 'Initiator',
|
|
/**
|
|
*@description Text in Coverage List View of the Coverage tab
|
|
*/
|
|
totalBytes: 'Total Bytes',
|
|
/**
|
|
*@description Text for errors
|
|
*/
|
|
error: 'Error',
|
|
/**
|
|
*@description Title for the developer resources tab
|
|
*/
|
|
developerResources: 'Developer Resources',
|
|
/**
|
|
*@description Text for a context menu entry
|
|
*/
|
|
copyUrl: 'Copy URL',
|
|
/**
|
|
*@description Text for a context menu entry
|
|
*/
|
|
copyInitiatorUrl: 'Copy initiator URL',
|
|
/**
|
|
*@description Text for the status column of a list view
|
|
*/
|
|
pending: 'pending',
|
|
/**
|
|
*@description Text for the status column of a list view
|
|
*/
|
|
success: 'success',
|
|
/**
|
|
*@description Text for the status column of a list view
|
|
*/
|
|
failure: 'failure',
|
|
/**
|
|
*@description Accessible text for a file size of 1 byte
|
|
*/
|
|
Byte: '1 byte',
|
|
/**
|
|
*@description Accessible text for the value in bytes in memory allocation or coverage view.
|
|
*@example {12345} PH1
|
|
*/
|
|
sBytes: '{PH1} bytes',
|
|
};
|
|
const str_ = i18n.i18n.registerUIStrings('developer_resources/DeveloperResourcesListView.js', UIStrings);
|
|
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
|
|
|
|
export class DeveloperResourcesListView extends UI.Widget.VBox {
|
|
/**
|
|
* @param {function(!SDK.PageResourceLoader.PageResource):boolean} isVisibleFilter
|
|
*/
|
|
constructor(isVisibleFilter) {
|
|
super(true);
|
|
/** @type {!Map<!SDK.PageResourceLoader.PageResource, !GridNode>} */
|
|
this._nodeForItem = new Map();
|
|
this._isVisibleFilter = isVisibleFilter;
|
|
/** @type {?RegExp} */
|
|
this._highlightRegExp = null;
|
|
this.registerRequiredCSS('developer_resources/developerResourcesListView.css', {enableLegacyPatching: true});
|
|
|
|
const columns = /** @type {!Array<!DataGrid.DataGrid.ColumnDescriptor>} */ ([
|
|
{id: 'status', title: i18nString(UIStrings.status), width: '60px', fixedWidth: true, sortable: true},
|
|
{id: 'url', title: i18nString(UIStrings.url), width: '250px', fixedWidth: false, sortable: true},
|
|
{id: 'initiator', title: i18nString(UIStrings.initiator), width: '80px', fixedWidth: false, sortable: true}, {
|
|
id: 'size',
|
|
title: i18nString(UIStrings.totalBytes),
|
|
width: '80px',
|
|
fixedWidth: true,
|
|
sortable: true,
|
|
align: DataGrid.DataGrid.Align.Right
|
|
},
|
|
{
|
|
id: 'errorMessage',
|
|
title: i18nString(UIStrings.error),
|
|
width: '200px',
|
|
fixedWidth: false,
|
|
sortable: true,
|
|
}
|
|
]);
|
|
/** @type {!DataGrid.SortableDataGrid.SortableDataGrid<!GridNode>} */
|
|
this._dataGrid = new DataGrid.SortableDataGrid.SortableDataGrid({
|
|
displayName: i18nString(UIStrings.developerResources),
|
|
columns,
|
|
editCallback: undefined,
|
|
refreshCallback: undefined,
|
|
deleteCallback: undefined
|
|
});
|
|
this._dataGrid.setResizeMethod(DataGrid.DataGrid.ResizeMethod.Last);
|
|
this._dataGrid.element.classList.add('flex-auto');
|
|
this._dataGrid.addEventListener(DataGrid.DataGrid.Events.SortingChanged, this._sortingChanged, this);
|
|
this._dataGrid.setRowContextMenuCallback(this._populateContextMenu.bind(this));
|
|
|
|
const dataGridWidget = this._dataGrid.asWidget();
|
|
dataGridWidget.show(this.contentElement);
|
|
this.setDefaultFocusedChild(dataGridWidget);
|
|
}
|
|
|
|
|
|
/**
|
|
* @param {!UI.ContextMenu.ContextMenu} contextMenu
|
|
* @param {!DataGrid.DataGrid.DataGridNode<!DataGrid.ViewportDataGrid.ViewportDataGridNode<!DataGrid.SortableDataGrid.SortableDataGridNode<!GridNode>>>} gridNode
|
|
*/
|
|
_populateContextMenu(contextMenu, gridNode) {
|
|
const item = (/** @type {!GridNode} */ (gridNode)).item;
|
|
contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyUrl), () => {
|
|
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(item.url);
|
|
});
|
|
if (item.initiator.initiatorUrl) {
|
|
contextMenu.clipboardSection().appendItem(i18nString(UIStrings.copyInitiatorUrl), () => {
|
|
Host.InspectorFrontendHost.InspectorFrontendHostInstance.copyText(item.initiator.initiatorUrl);
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {!Iterable<!SDK.PageResourceLoader.PageResource>} items
|
|
*/
|
|
update(items) {
|
|
let hadUpdates = false;
|
|
const rootNode = this._dataGrid.rootNode();
|
|
for (const item of items) {
|
|
let node = this._nodeForItem.get(item);
|
|
if (node) {
|
|
if (this._isVisibleFilter(node.item)) {
|
|
hadUpdates = node._refreshIfNeeded() || hadUpdates;
|
|
}
|
|
continue;
|
|
}
|
|
node = new GridNode(item);
|
|
this._nodeForItem.set(item, node);
|
|
if (this._isVisibleFilter(node.item)) {
|
|
rootNode.appendChild(node);
|
|
hadUpdates = true;
|
|
}
|
|
}
|
|
if (hadUpdates) {
|
|
this._sortingChanged();
|
|
}
|
|
}
|
|
|
|
reset() {
|
|
this._nodeForItem.clear();
|
|
this._dataGrid.rootNode().removeChildren();
|
|
}
|
|
|
|
/**
|
|
* @param {?RegExp} highlightRegExp
|
|
*/
|
|
updateFilterAndHighlight(highlightRegExp) {
|
|
this._highlightRegExp = highlightRegExp;
|
|
let hadTreeUpdates = false;
|
|
for (const node of this._nodeForItem.values()) {
|
|
const shouldBeVisible = this._isVisibleFilter(node.item);
|
|
const isVisible = !!node.parent;
|
|
if (shouldBeVisible) {
|
|
node._setHighlight(this._highlightRegExp);
|
|
}
|
|
if (shouldBeVisible === isVisible) {
|
|
continue;
|
|
}
|
|
hadTreeUpdates = true;
|
|
if (!shouldBeVisible) {
|
|
node.remove();
|
|
} else {
|
|
this._dataGrid.rootNode().appendChild(node);
|
|
}
|
|
}
|
|
if (hadTreeUpdates) {
|
|
this._sortingChanged();
|
|
}
|
|
}
|
|
|
|
_sortingChanged() {
|
|
const columnId = this._dataGrid.sortColumnId();
|
|
if (!columnId) {
|
|
return;
|
|
}
|
|
|
|
const sortFunction =
|
|
/** @type {null|function(!DataGrid.SortableDataGrid.SortableDataGridNode<!GridNode>, !DataGrid.SortableDataGrid.SortableDataGridNode<!GridNode>):number} */
|
|
(GridNode.sortFunctionForColumn(columnId));
|
|
if (sortFunction) {
|
|
this._dataGrid.sortNodes(sortFunction, !this._dataGrid.isSortOrderAscending());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @extends {DataGrid.SortableDataGrid.SortableDataGridNode<!GridNode>}
|
|
*/
|
|
class GridNode extends DataGrid.SortableDataGrid.SortableDataGridNode {
|
|
/**
|
|
* @param {!SDK.PageResourceLoader.PageResource} item
|
|
*/
|
|
constructor(item) {
|
|
super();
|
|
this.item = item;
|
|
/** @type {?RegExp} */
|
|
this._highlightRegExp = null;
|
|
}
|
|
|
|
/**
|
|
* @param {?RegExp} highlightRegExp
|
|
*/
|
|
_setHighlight(highlightRegExp) {
|
|
if (this._highlightRegExp === highlightRegExp) {
|
|
return;
|
|
}
|
|
this._highlightRegExp = highlightRegExp;
|
|
this.refresh();
|
|
}
|
|
|
|
/**
|
|
* @return {boolean}
|
|
*/
|
|
_refreshIfNeeded() {
|
|
this.refresh();
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* @override
|
|
* @param {string} columnId
|
|
* @return {!HTMLElement}
|
|
*/
|
|
createCell(columnId) {
|
|
const cell = /** @type {!HTMLElement} */ (this.createTD(columnId));
|
|
switch (columnId) {
|
|
case 'url': {
|
|
UI.Tooltip.Tooltip.install(cell, this.item.url);
|
|
const outer = cell.createChild('div', 'url-outer');
|
|
const prefix = outer.createChild('div', 'url-prefix');
|
|
const suffix = outer.createChild('div', 'url-suffix');
|
|
const splitURL = /^(.*)(\/[^/]*)$/.exec(this.item.url);
|
|
prefix.textContent = splitURL ? splitURL[1] : this.item.url;
|
|
suffix.textContent = splitURL ? splitURL[2] : '';
|
|
if (this._highlightRegExp) {
|
|
this._highlight(outer, this.item.url);
|
|
}
|
|
this.setCellAccessibleName(this.item.url, cell, columnId);
|
|
break;
|
|
}
|
|
case 'initiator': {
|
|
const url = this.item.initiator.initiatorUrl || '';
|
|
cell.textContent = url;
|
|
UI.Tooltip.Tooltip.install(cell, url);
|
|
this.setCellAccessibleName(url, cell, columnId);
|
|
cell.onmouseenter = () => {
|
|
const frame = SDK.FrameManager.FrameManager.instance().getFrame(this.item.initiator.frameId || '');
|
|
if (frame) {
|
|
frame.highlight();
|
|
}
|
|
};
|
|
cell.onmouseleave = () => SDK.OverlayModel.OverlayModel.hideDOMNodeHighlight();
|
|
break;
|
|
}
|
|
case 'status': {
|
|
if (this.item.success === null) {
|
|
cell.textContent = i18nString(UIStrings.pending);
|
|
} else {
|
|
cell.textContent = this.item.success ? i18nString(UIStrings.success) : i18nString(UIStrings.failure);
|
|
}
|
|
break;
|
|
}
|
|
case 'size': {
|
|
const size = this.item.size;
|
|
if (size !== null) {
|
|
const sizeSpan = cell.createChild('span');
|
|
sizeSpan.textContent = Number.withThousandsSeparator(size);
|
|
const sizeAccessibleName =
|
|
(size === 1) ? i18nString(UIStrings.Byte) : i18nString(UIStrings.sBytes, {PH1: size});
|
|
this.setCellAccessibleName(sizeAccessibleName, cell, columnId);
|
|
}
|
|
break;
|
|
}
|
|
case 'errorMessage': {
|
|
cell.classList.add('error-message');
|
|
if (this.item.errorMessage) {
|
|
cell.textContent = this.item.errorMessage;
|
|
if (this._highlightRegExp) {
|
|
this._highlight(cell, this.item.errorMessage);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
return cell;
|
|
}
|
|
|
|
/**
|
|
* @param {!Element} element
|
|
* @param {string} textContent
|
|
*/
|
|
_highlight(element, textContent) {
|
|
if (!this._highlightRegExp) {
|
|
return;
|
|
}
|
|
const matches = this._highlightRegExp.exec(textContent);
|
|
if (!matches || !matches.length) {
|
|
return;
|
|
}
|
|
const range = new TextUtils.TextRange.SourceRange(matches.index, matches[0].length);
|
|
UI.UIUtils.highlightRangesWithStyleClass(element, [range], 'filter-highlight');
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {string} columnId
|
|
* @returns {null|function(!GridNode, !GridNode):number}
|
|
*/
|
|
static sortFunctionForColumn(columnId) {
|
|
/**
|
|
* @param {*} x
|
|
*/
|
|
const nullToNegative = x => x === null ? -1 : Number(x);
|
|
switch (columnId) {
|
|
case 'url':
|
|
return (a, b) => a.item.url.localeCompare(b.item.url);
|
|
case 'status':
|
|
return (a, b) => {
|
|
return nullToNegative(a.item.success) - nullToNegative(b.item.success);
|
|
};
|
|
case 'size':
|
|
return (a, b) => nullToNegative(a.item.size) - nullToNegative(b.item.size);
|
|
case 'initiator':
|
|
return (a, b) => (a.item.initiator.initiatorUrl || '').localeCompare(b.item.initiator.initiatorUrl || '');
|
|
case 'errorMessage':
|
|
return (a, b) => (a.item.errorMessage || '').localeCompare(b.item.errorMessage || '');
|
|
default:
|
|
console.assert(false, 'Unknown sort field: ' + columnId);
|
|
return null;
|
|
}
|
|
}
|
|
}
|