mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-09-29 16:57:00 +08:00
[CSS Overview] Uses snapshot; adds element highlight
Change-Id: If58dd20c6f4b3c8f730b39c48c14826e402018a0 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/1873759 Reviewed-by: Tim van der Lippe <tvanderlippe@chromium.org> Commit-Queue: Paul Lewis <aerotwist@chromium.org>
This commit is contained in:
@@ -12,21 +12,36 @@ CssOverview.CSSOverviewCompletedView = class extends UI.PanelWithSidebar {
|
||||
|
||||
this._controller = controller;
|
||||
this._formatter = new Intl.NumberFormat('en-US');
|
||||
this._mainContainer = new UI.VBox();
|
||||
|
||||
this._mainContainer = new UI.SplitWidget(true, true);
|
||||
this._resultsContainer = new UI.VBox();
|
||||
this._elementContainer = new UI.VBox();
|
||||
|
||||
// Dupe the styles into the main container because of the shadow root will prevent outer styles.
|
||||
this._mainContainer.registerRequiredCSS('css_overview/cssOverviewCompletedView.css');
|
||||
|
||||
this._mainContainer.setMainWidget(this._resultsContainer);
|
||||
this._mainContainer.setSidebarWidget(this._elementContainer);
|
||||
this._mainContainer.setVertical(false);
|
||||
this._mainContainer.setSecondIsSidebar(true);
|
||||
this._mainContainer.setSidebarMinimized(true);
|
||||
|
||||
this._sideBar = new CssOverview.CSSOverviewSidebarPanel();
|
||||
this.splitWidget().setSidebarWidget(this._sideBar);
|
||||
this.splitWidget().setMainWidget(this._mainContainer);
|
||||
|
||||
this._cssModel = target.model(SDK.CSSModel);
|
||||
this._domModel = target.model(SDK.DOMModel);
|
||||
this._domAgent = target.domAgent();
|
||||
this._linkifier = new Components.Linkifier(/* maxLinkLength */ 20, /* useLinkDecorator */ true);
|
||||
this._relatedNodesMap = new Map();
|
||||
|
||||
this._columns = [
|
||||
this._mediaQueryColumns = [
|
||||
{id: 'text', title: ls`Text`, visible: true, sortable: true, weight: 60},
|
||||
{id: 'sourceURL', title: ls`Source`, visible: true, sortable: true, weight: 40}
|
||||
];
|
||||
|
||||
this._mediaQueryGrid = new DataGrid.SortableDataGrid(this._columns);
|
||||
this._mediaQueryGrid = new DataGrid.SortableDataGrid(this._mediaQueryColumns);
|
||||
this._mediaQueryGrid.element.classList.add('media-query-grid');
|
||||
this._mediaQueryGrid.setStriped(true);
|
||||
this._mediaQueryGrid.addEventListener(
|
||||
@@ -37,10 +52,20 @@ CssOverview.CSSOverviewCompletedView = class extends UI.PanelWithSidebar {
|
||||
this._sideBar.addItem(ls`Media queries`, 'media-queries');
|
||||
this._sideBar.select('summary');
|
||||
|
||||
this._elementGridColumns = [{id: 'nodeId', title: ls`Element`, visible: true, sortable: true, weight: 100}];
|
||||
|
||||
this._elementGrid = new DataGrid.SortableDataGrid(this._elementGridColumns);
|
||||
this._elementGrid.element.classList.add('element-grid');
|
||||
this._elementGrid.element.addEventListener('mouseover', this._onMouseOver.bind(this));
|
||||
this._elementGrid.setStriped(true);
|
||||
|
||||
this._sideBar.addEventListener(CssOverview.SidebarEvents.ItemSelected, this._sideBarItemSelected, this);
|
||||
this._sideBar.addEventListener(CssOverview.SidebarEvents.Reset, this._sideBarReset, this);
|
||||
this._controller.addEventListener(CssOverview.Events.Reset, this._reset, this);
|
||||
this._render({});
|
||||
this._controller.addEventListener(CssOverview.Events.PopulateNodes, this._populateNodes, this);
|
||||
this._resultsContainer.element.addEventListener('click', this._onClick.bind(this));
|
||||
|
||||
this._data = null;
|
||||
}
|
||||
|
||||
_sortMediaQueryDataGrid() {
|
||||
@@ -67,20 +92,53 @@ CssOverview.CSSOverviewCompletedView = class extends UI.PanelWithSidebar {
|
||||
}
|
||||
|
||||
_reset() {
|
||||
this._mainContainer.element.removeChildren();
|
||||
this._resultsContainer.element.removeChildren();
|
||||
this._mediaQueryGrid.rootNode().removeChildren();
|
||||
this._elementGrid.rootNode().removeChildren();
|
||||
this._relatedNodesMap = new Map();
|
||||
this._mainContainer.setSidebarMinimized(true);
|
||||
}
|
||||
|
||||
_render(data) {
|
||||
if (!(data && ('textColors' in data) && ('backgroundColors' in data))) {
|
||||
_onClick(evt) {
|
||||
const color = evt.target.dataset.color;
|
||||
const section = evt.target.dataset.section;
|
||||
if (!color) {
|
||||
return;
|
||||
}
|
||||
|
||||
const {elementStyleStats, elementCount, backgroundColors, textColors, globalStyleStats, mediaQueries} = data;
|
||||
const colorNodes =
|
||||
section === 'background' ? this._data.backgroundColors.get(color) : this._data.textColors.get(color);
|
||||
if (!colorNodes) {
|
||||
return;
|
||||
}
|
||||
|
||||
evt.consume();
|
||||
this._controller.dispatchEventToListeners(CssOverview.Events.PopulateNodes, {color, colorNodes});
|
||||
}
|
||||
|
||||
_onMouseOver(evt) {
|
||||
// Traverse the event path on the grid to find the nearest element with a backend node ID attached. Use
|
||||
// that for the highlighting.
|
||||
const node = evt.path.find(el => el.dataset && el.dataset.backendNodeId);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
|
||||
const backendNodeId = Number(node.dataset.backendNodeId);
|
||||
this._controller.dispatchEventToListeners(CssOverview.Events.RequestNodeHighlight, backendNodeId);
|
||||
}
|
||||
|
||||
_render(data) {
|
||||
if (!data || !('backgroundColors' in data) || !('textColors' in data)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._data = data;
|
||||
const {elementCount, backgroundColors, textColors, globalStyleStats, mediaQueries} = this._data;
|
||||
|
||||
// Convert rgb values from the computed styles to either undefined or HEX(A) strings.
|
||||
const nonTransparentBackgroundColors = this._getNonTransparentColorStrings(backgroundColors);
|
||||
const nonTransparentTextColors = this._getNonTransparentColorStrings(textColors);
|
||||
const sortedBackgroundColors = this._sortColorsByLuminance(backgroundColors);
|
||||
const sortedTextColors = this._sortColorsByLuminance(textColors);
|
||||
|
||||
this._fragment = UI.Fragment.build`
|
||||
<div class="vbox overview-completed-view">
|
||||
@@ -110,47 +168,49 @@ CssOverview.CSSOverviewCompletedView = class extends UI.PanelWithSidebar {
|
||||
</li>
|
||||
<li>
|
||||
<div class="label">${ls`Type selectors`}</div>
|
||||
<div class="value">${this._formatter.format(elementStyleStats.type.size)}</div>
|
||||
<div class="value">${this._formatter.format(globalStyleStats.stats.type)}</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="label">${ls`ID selectors`}</div>
|
||||
<div class="value">${this._formatter.format(elementStyleStats.id.size)}</div>
|
||||
<div class="value">${this._formatter.format(globalStyleStats.stats.id)}</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="label">${ls`Class selectors`}</div>
|
||||
<div class="value">${this._formatter.format(elementStyleStats.class.size)}</div>
|
||||
<div class="value">${this._formatter.format(globalStyleStats.stats.class)}</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="label">${ls`Universal selectors`}</div>
|
||||
<div class="value">${this._formatter.format(elementStyleStats.universal.size)}</div>
|
||||
<div class="value">${this._formatter.format(globalStyleStats.stats.universal)}</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="label">${ls`Attribute selectors`}</div>
|
||||
<div class="value">${this._formatter.format(elementStyleStats.attribute.size)}</div>
|
||||
<div class="value">${this._formatter.format(globalStyleStats.stats.attribute)}</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="label">${ls`Non-simple selectors`}</div>
|
||||
<div class="value">${this._formatter.format(elementStyleStats.nonSimple.size)}</div>
|
||||
<div class="value">${this._formatter.format(globalStyleStats.stats.nonSimple)}</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div $="colors" class="results-section colors">
|
||||
<h1>${ls`Colors`}</h1>
|
||||
<h2>${ls`Unique background colors: ${nonTransparentBackgroundColors.length}`}</h2>
|
||||
<h2>${ls`Unique background colors: ${sortedBackgroundColors.length}`}</h2>
|
||||
<ul>
|
||||
${nonTransparentBackgroundColors.map(this._colorsToFragment)}
|
||||
${sortedBackgroundColors.map(this._colorsToFragment.bind(this, 'background'))}
|
||||
</ul>
|
||||
|
||||
<h2>${ls`Unique text colors: ${nonTransparentTextColors.length}`}</h2>
|
||||
<h2>${ls`Unique text colors: ${sortedTextColors.length}`}</h2>
|
||||
<ul>
|
||||
${nonTransparentTextColors.map(this._colorsToFragment)}
|
||||
${sortedTextColors.map(this._colorsToFragment.bind(this, 'text'))}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- TODO: Fonts -->
|
||||
|
||||
<div $="media-queries" class="results-section media-queries">
|
||||
<h1>${ls`Media queries`}</h1>
|
||||
${this._mediaQueryGrid.element}
|
||||
${mediaQueries.length > 0 ? this._mediaQueryGrid.element : `There are no media queries.`}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
@@ -162,23 +222,47 @@ CssOverview.CSSOverviewCompletedView = class extends UI.PanelWithSidebar {
|
||||
this._mediaQueryGrid.insertChild(mediaQueryNode);
|
||||
}
|
||||
|
||||
this._mainContainer.element.appendChild(this._fragment.element());
|
||||
this._resultsContainer.element.appendChild(this._fragment.element());
|
||||
this._elementContainer.element.appendChild(this._elementGrid.element);
|
||||
|
||||
this._mediaQueryGrid.renderInline();
|
||||
this._mediaQueryGrid.wasShown();
|
||||
}
|
||||
|
||||
_colorsToFragment(color) {
|
||||
const colorFormatted =
|
||||
color.hasAlpha() ? color.asString(Common.Color.Format.HEXA) : color.asString(Common.Color.Format.HEX);
|
||||
async _populateNodes(evt) {
|
||||
this._elementGrid.rootNode().removeChildren();
|
||||
|
||||
const {color, colorNodes} = evt.data;
|
||||
let relatedNodesMap = this._relatedNodesMap.get(color);
|
||||
if (!relatedNodesMap) {
|
||||
// This process can't be repeated so we need to now capture the Nodes and hold them in case we need to revisit
|
||||
// the same set again.
|
||||
relatedNodesMap = await this._domModel.pushNodesByBackendIdsToFrontend(colorNodes);
|
||||
this._relatedNodesMap.set(color, relatedNodesMap);
|
||||
}
|
||||
|
||||
for (const domNode of relatedNodesMap.values()) {
|
||||
const colorNode = new CssOverview.CSSOverviewCompletedView.ColorNode(this._elementGrid, domNode, this._linkifier);
|
||||
colorNode.selectable = false;
|
||||
this._elementGrid.insertChild(colorNode);
|
||||
}
|
||||
|
||||
this._mainContainer.setSidebarMinimized(false);
|
||||
this._elementGrid.renderInline();
|
||||
this._elementGrid.wasShown();
|
||||
}
|
||||
|
||||
_colorsToFragment(section, color) {
|
||||
const blockFragment = UI.Fragment.build`<li>
|
||||
<div class="block" $="color"></div>
|
||||
<div class="block-title">${colorFormatted}</div>
|
||||
<button data-color="${color}" data-section="${section}" class="block" $="color"></button>
|
||||
<div class="block-title">${color}</div>
|
||||
</li>`;
|
||||
|
||||
const block = blockFragment.$('color');
|
||||
block.style.backgroundColor = colorFormatted;
|
||||
block.style.backgroundColor = color;
|
||||
|
||||
let [h, s, l] = color.hsla();
|
||||
const borderColor = Common.Color.parse(color);
|
||||
let [h, s, l] = borderColor.hsla();
|
||||
h = Math.round(h * 360);
|
||||
s = Math.round(s * 100);
|
||||
l = Math.round(l * 100);
|
||||
@@ -192,18 +276,10 @@ CssOverview.CSSOverviewCompletedView = class extends UI.PanelWithSidebar {
|
||||
return blockFragment;
|
||||
}
|
||||
|
||||
_getNonTransparentColorStrings(srcColors) {
|
||||
const colors = [];
|
||||
for (const colorText of Array.from(srcColors)) {
|
||||
const color = Common.Color.parse(colorText);
|
||||
if (color.rgba()[3] === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
colors.push(color);
|
||||
}
|
||||
|
||||
return colors.sort((colorA, colorB) => {
|
||||
_sortColorsByLuminance(srcColors) {
|
||||
return Array.from(srcColors.keys()).sort((colA, colB) => {
|
||||
const colorA = Common.Color.parse(colA);
|
||||
const colorB = Common.Color.parse(colB);
|
||||
return Common.Color.luminance(colorB.rgba()) - Common.Color.luminance(colorA.rgba());
|
||||
});
|
||||
}
|
||||
@@ -242,7 +318,7 @@ CssOverview.CSSOverviewCompletedView.MediaQueryNode = class extends DataGrid.Sor
|
||||
if (link.textContent !== '') {
|
||||
cell.appendChild(link);
|
||||
} else {
|
||||
cell.textContent = `${this.data.sourceURL} (not available)`;
|
||||
cell.textContent = `(unable to link)`;
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
@@ -258,3 +334,37 @@ CssOverview.CSSOverviewCompletedView.MediaQueryNode = class extends DataGrid.Sor
|
||||
return linkifier.linkifyCSSLocation(matchingSelectorLocation);
|
||||
}
|
||||
};
|
||||
|
||||
CssOverview.CSSOverviewCompletedView.ColorNode = class extends DataGrid.SortableDataGridNode {
|
||||
/**
|
||||
* @param {!DataGrid.SortableDataGrid} dataGrid
|
||||
* @param {!Object<string,*>} colorData
|
||||
* @param {!Components.Linkifier} linkifier
|
||||
*/
|
||||
constructor(dataGrid, colorData, linkifier) {
|
||||
super(dataGrid, colorData.hasChildren);
|
||||
|
||||
this.data = colorData;
|
||||
this._linkifier = linkifier;
|
||||
}
|
||||
|
||||
/**
|
||||
* @override
|
||||
* @param {string} columnId
|
||||
* @return {!Element}
|
||||
*/
|
||||
createCell(columnId) {
|
||||
if (columnId === 'nodeId') {
|
||||
const cell = this.createTD(columnId);
|
||||
cell.textContent = '...';
|
||||
Common.Linkifier.linkify(this.data).then(link => {
|
||||
cell.textContent = '';
|
||||
link.dataset.backendNodeId = this.data.backendNodeId();
|
||||
cell.appendChild(link);
|
||||
});
|
||||
return cell;
|
||||
}
|
||||
|
||||
return super.createCell(columnId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -19,6 +19,8 @@ CssOverview.OverviewController = class extends Common.Object {
|
||||
|
||||
CssOverview.Events = {
|
||||
RequestOverviewStart: Symbol('RequestOverviewStart'),
|
||||
RequestNodeHighlight: Symbol('RequestNodeHighlight'),
|
||||
PopulateNodes: Symbol('PopulateNodes'),
|
||||
RequestOverviewCancel: Symbol('RequestOverviewCancel'),
|
||||
OverviewCompleted: Symbol('OverviewCompleted'),
|
||||
Reset: Symbol('Reset'),
|
||||
|
||||
@@ -15,12 +15,87 @@ CssOverview.CSSOverviewModel = class extends SDK.SDKModel {
|
||||
this._runtimeAgent = target.runtimeAgent();
|
||||
this._cssAgent = target.cssAgent();
|
||||
this._domAgent = target.domAgent();
|
||||
this._domSnapshotAgent = target.domsnapshotAgent();
|
||||
this._overlayAgent = target.overlayAgent();
|
||||
}
|
||||
|
||||
getFlattenedDocument() {
|
||||
return this._domAgent.getFlattenedDocument(-1, true);
|
||||
}
|
||||
|
||||
highlightNode(node) {
|
||||
const highlightConfig = {contentColor: Common.Color.PageHighlight.Content.toProtocolRGBA(), showInfo: true};
|
||||
|
||||
this._overlayAgent.invoke_hideHighlight({});
|
||||
this._overlayAgent.invoke_highlightNode({backendNodeId: node, highlightConfig});
|
||||
}
|
||||
|
||||
async getNodeStyleStats() {
|
||||
const backgroundColors = new Map();
|
||||
const textColors = new Map();
|
||||
const fontSizes = new Map();
|
||||
const fontWeights = new Map();
|
||||
const snapshotConfig = {computedStyles: ['background-color', 'color', 'font-size', 'font-weight']};
|
||||
|
||||
const storeColor = (id, nodeId, target) => {
|
||||
if (id === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse the color, discard transparent ones.
|
||||
const colorText = strings[id];
|
||||
const color = Common.Color.parse(colorText);
|
||||
if (color.rgba()[3] === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Format the color and use as the key.
|
||||
const colorFormatted =
|
||||
color.hasAlpha() ? color.asString(Common.Color.Format.HEXA) : color.asString(Common.Color.Format.HEX);
|
||||
|
||||
// Get the existing set of nodes with the color, or create a new set.
|
||||
const colorValues = target.get(colorFormatted) || new Set();
|
||||
colorValues.add(nodeId);
|
||||
|
||||
// Store.
|
||||
target.set(colorFormatted, colorValues);
|
||||
};
|
||||
|
||||
const {documents, strings} = await this._domSnapshotAgent.invoke_captureSnapshot(snapshotConfig);
|
||||
for (const {nodes, layout} of documents) {
|
||||
for (let idx = 0; idx < layout.styles.length; idx++) {
|
||||
const styles = layout.styles[idx];
|
||||
const nodeIdx = layout.nodeIndex[idx];
|
||||
const nodeId = nodes.backendNodeId[nodeIdx];
|
||||
|
||||
// Skip text nodes.
|
||||
const nodeType = nodes.nodeType[idx];
|
||||
if (nodeType === Node.TEXT_NODE) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [backgroundColorIdx, textColorIdx, fontSizeIdx, fontWeightIdx] = styles;
|
||||
|
||||
storeColor(backgroundColorIdx, nodeId, backgroundColors);
|
||||
storeColor(textColorIdx, nodeId, textColors);
|
||||
|
||||
if (fontSizeIdx !== -1) {
|
||||
const fontSize = strings[fontSizeIdx];
|
||||
const fontSizeInstances = (fontSizes.get(fontSize) || 0) + 1;
|
||||
fontSizes.set(fontSize, fontSizeInstances);
|
||||
}
|
||||
|
||||
if (fontWeightIdx !== -1) {
|
||||
const fontWeight = strings[fontWeightIdx];
|
||||
const fontWeightInstances = (fontWeights.get(fontWeight) || 0) + 1;
|
||||
fontWeights.set(fontWeight, fontWeightInstances);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {backgroundColors, textColors, fontSizes, fontWeights};
|
||||
}
|
||||
|
||||
getComputedStyleForNode(nodeId) {
|
||||
return this._cssAgent.getComputedStyleForNode(nodeId);
|
||||
}
|
||||
@@ -38,16 +113,65 @@ CssOverview.CSSOverviewModel = class extends SDK.SDKModel {
|
||||
let styleRules = 0;
|
||||
let inlineStyles = 0;
|
||||
let externalSheets = 0;
|
||||
for (const { rules, href } of document.styleSheets) {
|
||||
if (href) {
|
||||
const stats = {
|
||||
// Simple.
|
||||
type: new Set(),
|
||||
class: new Set(),
|
||||
id: new Set(),
|
||||
universal: new Set(),
|
||||
attribute: new Set(),
|
||||
|
||||
// Non-simple.
|
||||
nonSimple: new Set()
|
||||
};
|
||||
|
||||
for (const styleSheet of document.styleSheets) {
|
||||
if (styleSheet.href) {
|
||||
externalSheets++;
|
||||
} else {
|
||||
inlineStyles++;
|
||||
}
|
||||
|
||||
// Attempting to grab rules can trigger a DOMException.
|
||||
// Try it and if it fails skip to the next stylesheet.
|
||||
let rules;
|
||||
try {
|
||||
rules = styleSheet.rules;
|
||||
} catch (err) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const rule of rules) {
|
||||
if ('selectorText' in rule) {
|
||||
styleRules++;
|
||||
|
||||
// Each group that was used.
|
||||
for (const selectorGroup of rule.selectorText.split(',')) {
|
||||
// Each selector in the group.
|
||||
for (const selector of selectorGroup.split(\/[\\t\\n\\f\\r ]+\/g)) {
|
||||
if (selector.startsWith('.')) {
|
||||
// Class.
|
||||
stats.class.add(selector);
|
||||
} else if (selector.startsWith('#')) {
|
||||
// Id.
|
||||
stats.id.add(selector);
|
||||
} else if (selector.startsWith('*')) {
|
||||
// Universal.
|
||||
stats.universal.add(selector);
|
||||
} else if (selector.startsWith('[')) {
|
||||
// Attribute.
|
||||
stats.attribute.add(selector);
|
||||
} else {
|
||||
// Type or non-simple selector.
|
||||
const specialChars = \/[#\.:\\[\\]|\\+>~]\/;
|
||||
if (specialChars.test(selector)) {
|
||||
stats.nonSimple.add(selector);
|
||||
} else {
|
||||
stats.type.add(selector);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,7 +179,18 @@ CssOverview.CSSOverviewModel = class extends SDK.SDKModel {
|
||||
return {
|
||||
styleRules,
|
||||
inlineStyles,
|
||||
externalSheets
|
||||
externalSheets,
|
||||
stats: {
|
||||
// Simple.
|
||||
type: stats.type.size,
|
||||
class: stats.class.size,
|
||||
id: stats.id.size,
|
||||
universal: stats.universal.size,
|
||||
attribute: stats.attribute.size,
|
||||
|
||||
// Non-simple.
|
||||
nonSimple: stats.nonSimple.size
|
||||
}
|
||||
}
|
||||
})()`;
|
||||
const {result} = await this._runtimeAgent.invoke_evaluate({expression, returnByValue: true});
|
||||
@@ -67,70 +202,6 @@ CssOverview.CSSOverviewModel = class extends SDK.SDKModel {
|
||||
|
||||
return result.value;
|
||||
}
|
||||
|
||||
async getStylesStatsForNode(nodeId) {
|
||||
const stats = {
|
||||
// Simple.
|
||||
type: new Set(),
|
||||
class: new Set(),
|
||||
id: new Set(),
|
||||
universal: new Set(),
|
||||
attribute: new Set(),
|
||||
|
||||
// Non-simple.
|
||||
nonSimple: new Set()
|
||||
};
|
||||
|
||||
const matches = await this._cssAgent.invoke_getMatchedStylesForNode({nodeId});
|
||||
if (!matches || !matches.matchedCSSRules || !matches.matchedCSSRules.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
matches.matchedCSSRules.forEach(cssRule => {
|
||||
const {matchingSelectors} = cssRule;
|
||||
const {origin, selectorList} = cssRule.rule;
|
||||
const isExternalSheet = origin === 'regular';
|
||||
if (!isExternalSheet || !selectorList) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const selectors = matchingSelectors.map(idx => selectorList.selectors[idx]);
|
||||
|
||||
// Each group of selectors, e.g. foo.baz, foo .bar, foo { ... }
|
||||
for (const {text} of selectors) {
|
||||
// Each group that was used.
|
||||
for (const selectorGroup of text.split(',')) {
|
||||
// Each selector in the group.
|
||||
for (const selector of selectorGroup.split(/[\t\n\f\r ]+/g)) {
|
||||
if (selector.startsWith('.')) {
|
||||
// Class.
|
||||
stats.class.add(selector);
|
||||
} else if (selector.startsWith('#')) {
|
||||
// Id.
|
||||
stats.id.add(selector);
|
||||
} else if (selector.startsWith('*')) {
|
||||
// Universal.
|
||||
stats.universal.add(selector);
|
||||
} else if (selector.startsWith('[')) {
|
||||
// Attribute.
|
||||
stats.attribute.add(selector);
|
||||
} else {
|
||||
// Type or non-simple selector.
|
||||
const specialChars = /[#\.:\[\]|\+>~]/;
|
||||
if (specialChars.test(selector)) {
|
||||
stats.nonSimple.add(selector);
|
||||
} else {
|
||||
stats.type.add(selector);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return stats;
|
||||
}
|
||||
};
|
||||
|
||||
SDK.SDKModel.register(CssOverview.CSSOverviewModel, SDK.Target.Capability.DOM, false);
|
||||
|
||||
@@ -23,32 +23,42 @@ CssOverview.CSSOverviewPanel = class extends UI.Panel {
|
||||
this._controller.addEventListener(CssOverview.Events.RequestOverviewCancel, this._cancelOverview, this);
|
||||
this._controller.addEventListener(CssOverview.Events.OverviewCompleted, this._overviewCompleted, this);
|
||||
this._controller.addEventListener(CssOverview.Events.Reset, this._reset, this);
|
||||
this._controller.addEventListener(CssOverview.Events.RequestNodeHighlight, this._requestNodeHighlight, this);
|
||||
|
||||
this._reset();
|
||||
}
|
||||
|
||||
_reset() {
|
||||
this._backgroundColors = new Set();
|
||||
this._textColors = new Set();
|
||||
this._backgroundColors = new Map();
|
||||
this._textColors = new Map();
|
||||
this._fontSizes = new Map();
|
||||
this._fontWeights = new Map();
|
||||
this._mediaQueries = [];
|
||||
this._elementCount = 0;
|
||||
this._elementStyleStats = {
|
||||
// Simple.
|
||||
type: new Set(),
|
||||
class: new Set(),
|
||||
id: new Set(),
|
||||
universal: new Set(),
|
||||
attribute: new Set(),
|
||||
|
||||
// Non-simple.
|
||||
nonSimple: new Set()
|
||||
};
|
||||
this._cancelled = false;
|
||||
this._globalStyleStats = {styleRules: 0, inlineStyles: 0, externalSheets: 0};
|
||||
this._globalStyleStats = {
|
||||
styleRules: 0,
|
||||
inlineStyles: 0,
|
||||
externalSheets: 0,
|
||||
stats: {
|
||||
// Simple.
|
||||
type: 0,
|
||||
class: 0,
|
||||
id: 0,
|
||||
universal: 0,
|
||||
attribute: 0,
|
||||
|
||||
// Non-simple.
|
||||
nonSimple: 0
|
||||
}
|
||||
};
|
||||
this._renderInitialView();
|
||||
}
|
||||
|
||||
_requestNodeHighlight(evt) {
|
||||
this._model.highlightNode(evt.data);
|
||||
}
|
||||
|
||||
_renderInitialView() {
|
||||
this._processingView.hideWidget();
|
||||
this._completedView.hideWidget();
|
||||
@@ -56,12 +66,11 @@ CssOverview.CSSOverviewPanel = class extends UI.Panel {
|
||||
this._startView.show(this.contentElement);
|
||||
}
|
||||
|
||||
_renderOverviewStartedView(elementsHandled = 0, total = 0) {
|
||||
_renderOverviewStartedView() {
|
||||
this._startView.hideWidget();
|
||||
this._completedView.hideWidget();
|
||||
|
||||
this._processingView.show(this.contentElement);
|
||||
this._processingView.setElementsHandled(elementsHandled, total);
|
||||
}
|
||||
|
||||
_renderOverviewCompletedView() {
|
||||
@@ -73,8 +82,8 @@ CssOverview.CSSOverviewPanel = class extends UI.Panel {
|
||||
backgroundColors: this._backgroundColors,
|
||||
textColors: this._textColors,
|
||||
globalStyleStats: this._globalStyleStats,
|
||||
elementStyleStats: this._elementStyleStats,
|
||||
fontSizes: this._fontSizes,
|
||||
fontWeights: this._fontWeights,
|
||||
elementCount: this._elementCount,
|
||||
mediaQueries: this._mediaQueries
|
||||
});
|
||||
@@ -83,74 +92,40 @@ CssOverview.CSSOverviewPanel = class extends UI.Panel {
|
||||
async _startOverview() {
|
||||
this._renderOverviewStartedView();
|
||||
|
||||
const document = await this._model.getFlattenedDocument();
|
||||
if (this._cancelled) {
|
||||
this._reset();
|
||||
return;
|
||||
const [nodes, globalStyleStats, {backgroundColors, textColors, fontSizes, fontWeights}, mediaQueries] =
|
||||
await Promise.all([
|
||||
this._model.getFlattenedDocument(), this._model.getGlobalStylesheetStats(), this._model.getNodeStyleStats(),
|
||||
this._model.getMediaQueries()
|
||||
]);
|
||||
|
||||
if (nodes) {
|
||||
this._elementCount = nodes.length;
|
||||
}
|
||||
|
||||
// 1. Get the global style stats.
|
||||
const globalStyleStats = await this._model.getGlobalStylesheetStats();
|
||||
if (globalStyleStats) {
|
||||
this._globalStyleStats = globalStyleStats;
|
||||
}
|
||||
|
||||
const mediaQueries = await this._model.getMediaQueries();
|
||||
if (mediaQueries) {
|
||||
this._mediaQueries = mediaQueries;
|
||||
}
|
||||
|
||||
// 2. Get the total element count.
|
||||
this._elementCount = document.length;
|
||||
|
||||
// 3. Process every element in the doc.
|
||||
for (let idx = 0; idx < document.length; idx++) {
|
||||
if (this._cancelled) {
|
||||
this._reset();
|
||||
return;
|
||||
}
|
||||
|
||||
const node = document[idx];
|
||||
const [computedStyles, styleStats] = await Promise.all(
|
||||
[this._model.getComputedStyleForNode(node.nodeId), this._model.getStylesStatsForNode(node.nodeId)]);
|
||||
|
||||
// 3a. Capture any colors from the computed styles.
|
||||
if (computedStyles) {
|
||||
const backgroundColor = this._getStyleValue(computedStyles, 'background-color');
|
||||
if (backgroundColor) {
|
||||
this._backgroundColors.add(backgroundColor);
|
||||
}
|
||||
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
const textColor = this._getStyleValue(computedStyles, 'color');
|
||||
this._textColors.add(textColor);
|
||||
|
||||
const fontSize = this._getStyleValue(computedStyles, 'font-size');
|
||||
if (!this._fontSizes.has(fontSize)) {
|
||||
this._fontSizes.set(fontSize, 0);
|
||||
}
|
||||
|
||||
this._fontSizes.set(fontSize, this._fontSizes.get(fontSize) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
// 3b. Tally the selector stats.
|
||||
if (styleStats) {
|
||||
for (const section of Object.keys(this._elementStyleStats)) {
|
||||
if (!styleStats[section]) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (const value of styleStats[section]) {
|
||||
this._elementStyleStats[section].add(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this._renderOverviewStartedView(idx + 1, document.length);
|
||||
if (backgroundColors) {
|
||||
this._backgroundColors = backgroundColors;
|
||||
}
|
||||
|
||||
if (textColors) {
|
||||
this._textColors = textColors;
|
||||
}
|
||||
|
||||
if (fontSizes) {
|
||||
this._fontSizes = fontSizes;
|
||||
}
|
||||
|
||||
if (fontWeights) {
|
||||
this._fontWeights = fontWeights;
|
||||
}
|
||||
|
||||
// 4. Finish.
|
||||
this._controller.dispatchEventToListeners(CssOverview.Events.OverviewCompleted);
|
||||
}
|
||||
|
||||
|
||||
@@ -25,20 +25,10 @@ CssOverview.CSSOverviewProcessingView = class extends UI.Widget {
|
||||
<div class="vbox overview-processing-view">
|
||||
<h1>Processing page</h1>
|
||||
<div>${cancelButton}</div>
|
||||
|
||||
<h2 $="processed"></h2>
|
||||
</div>
|
||||
`;
|
||||
|
||||
this.contentElement.appendChild(this.fragment.element());
|
||||
this.contentElement.style.overflow = 'auto';
|
||||
}
|
||||
|
||||
setElementsHandled(handled = 0, total = 0) {
|
||||
// TODO(aerotwist): We might want to switch this to using Intl.PluralRules in the future
|
||||
// @see https://v8.dev/features/intl-pluralrules
|
||||
const elementsTotal = total > 0 ? ls`document elements` : ls`document element`;
|
||||
this.fragment.$('processed').textContent =
|
||||
ls`Processed ${this._formatter.format(handled)} of ${this._formatter.format(total)} ${elementsTotal}.`;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -54,7 +54,14 @@
|
||||
|
||||
.media-query-grid .data-container {
|
||||
top: 0;
|
||||
max-height: 400px;
|
||||
max-height: 350px;
|
||||
}
|
||||
|
||||
.element-grid {
|
||||
flex: 1;
|
||||
border-left: none;
|
||||
border-right: none;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.block {
|
||||
|
||||
@@ -12,18 +12,15 @@
|
||||
<message name="IDS_DEVTOOLS_1f1621cad66c8f661842bf9392d8559b" desc="Label for the summary in the CSS Overview report">
|
||||
Overview summary
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_231afe47f3f37d3808096b36c28b4ded" desc="Label for the column in the element list in the CSS Overview report">
|
||||
Element
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_23717568804f63f75e7596dcced3119e" desc="Label for the number of universal selectors in the CSS Overview report">
|
||||
Universal selectors
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_396ea220fc4d34d57a5a505de918bdde" desc="Singular of 'document element' in the processing update message">
|
||||
document element
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_4b3e6e4359cfb882a3b8b551f38a92c7" desc="Label for unique text colors in the CSS Overview Panel">
|
||||
Unique text colors: <ph name="TEXTCOLORS_LENGTH">$1s<ex>32</ex></ph>
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_52798275481d47b10f5b8e31310b155e" desc="Label for the update message in the processing stage of CSS Overview">
|
||||
Processed <ph name="THIS__FORMATTER_FORMAT_HANDLED_">$1s<ex>23</ex></ph> of <ph name="THIS__FORMATTER_FORMAT_TOTAL_">$2s<ex>1,223</ex></ph> <ph name="ELEMENTSTOTAL">$3s<ex>document elements</ex></ph>.
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_5d50889672f6f860d14f502de3de1957" desc="Title of colors subsection in the CSS Overview Panel">
|
||||
Colors
|
||||
</message>
|
||||
@@ -42,9 +39,6 @@
|
||||
<message name="IDS_DEVTOOLS_95d366a44bd7bad09d9916e05b512cc6" desc="Label for the number of External stylesheets in the CSS Overview report">
|
||||
External stylesheets
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_a9427e421abd9fd7ba45212c25ba55a5" desc="Plural of 'document element' in the processing update message">
|
||||
document elements
|
||||
</message>
|
||||
<message name="IDS_DEVTOOLS_aff1062318f6d07520a0feba60f3f253" desc="Label for the number of type selectors in the CSS Overview report">
|
||||
Type selectors
|
||||
</message>
|
||||
|
||||
@@ -1,35 +1,36 @@
|
||||
{
|
||||
"extensions": [
|
||||
{
|
||||
"type": "view",
|
||||
"location": "panel",
|
||||
"id": "cssoverview",
|
||||
"title": "CSS Overview",
|
||||
"className": "CssOverview.CSSOverviewPanel",
|
||||
"order": 95,
|
||||
"experiment": "cssOverview"
|
||||
}
|
||||
],
|
||||
"dependencies": [
|
||||
"elements",
|
||||
"ui",
|
||||
"sdk",
|
||||
"data_grid"
|
||||
],
|
||||
"scripts": [
|
||||
"CSSOverviewController.js",
|
||||
"CSSOverviewModel.js",
|
||||
"CSSOverviewStartView.js",
|
||||
"CSSOverviewProcessingView.js",
|
||||
"CSSOverviewCompletedView.js",
|
||||
"CSSOverviewSidebarPanel.js",
|
||||
"CSSOverviewPanel.js"
|
||||
"extensions": [
|
||||
{
|
||||
"type": "view",
|
||||
"location": "panel",
|
||||
"id": "cssoverview",
|
||||
"title": "CSS Overview",
|
||||
"className": "CssOverview.CSSOverviewPanel",
|
||||
"order": 95,
|
||||
"experiment": "cssOverview"
|
||||
}
|
||||
],
|
||||
"resources": [
|
||||
"cssOverview.css",
|
||||
"cssOverviewStartView.css",
|
||||
"cssOverviewProcessingView.css",
|
||||
"cssOverviewCompletedView.css",
|
||||
"cssOverviewSidebarPanel.css"
|
||||
]
|
||||
}
|
||||
"dependencies": [
|
||||
"elements",
|
||||
"ui",
|
||||
"sdk",
|
||||
"data_grid",
|
||||
"extensions"
|
||||
],
|
||||
"scripts": [
|
||||
"CSSOverviewController.js",
|
||||
"CSSOverviewModel.js",
|
||||
"CSSOverviewStartView.js",
|
||||
"CSSOverviewProcessingView.js",
|
||||
"CSSOverviewCompletedView.js",
|
||||
"CSSOverviewSidebarPanel.js",
|
||||
"CSSOverviewPanel.js"
|
||||
],
|
||||
"resources": [
|
||||
"cssOverview.css",
|
||||
"cssOverviewStartView.css",
|
||||
"cssOverviewProcessingView.css",
|
||||
"cssOverviewCompletedView.css",
|
||||
"cssOverviewSidebarPanel.css"
|
||||
]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user