CSSOverviewCompletedView.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174
  1. export default class CSSOverviewCompletedView extends UI.PanelWithSidebar{constructor(controller,target){super('css_overview_completed_view');this.registerRequiredCSS('css_overview/cssOverviewCompletedView.css');this._controller=controller;this._formatter=new Intl.NumberFormat('en-US');this._mainContainer=new UI.SplitWidget(true,true);this._resultsContainer=new UI.VBox();this._elementContainer=new DetailsView();this._elementContainer.addEventListener(UI.TabbedPane.Events.TabClosed,evt=>{if(evt.data===0){this._mainContainer.setSidebarMinimized(true);}});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(20,true);this._viewMap=new Map();this._sideBar.addItem(ls`Overview summary`,'summary');this._sideBar.addItem(ls`Colors`,'colors');this._sideBar.addItem(ls`Font info`,'font-info');this._sideBar.addItem(ls`Unused declarations`,'unused-declarations');this._sideBar.addItem(ls`Media queries`,'media-queries');this._sideBar.select('summary');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._controller.addEventListener(CssOverview.Events.PopulateNodes,this._createElementsView,this);this._resultsContainer.element.addEventListener('click',this._onClick.bind(this));this._data=null;}
  2. wasShown(){super.wasShown();}
  3. _sideBarItemSelected(event){const section=this._fragment.$(event.data);if(!section){return;}
  4. section.scrollIntoView();}
  5. _sideBarReset(){this._controller.dispatchEventToListeners(CssOverview.Events.Reset);}
  6. _reset(){this._resultsContainer.element.removeChildren();this._mainContainer.setSidebarMinimized(true);this._elementContainer.closeTabs();this._viewMap=new Map();}
  7. _onClick(evt){const type=evt.target.dataset.type;if(!type){return;}
  8. let payload;switch(type){case'color':{const color=evt.target.dataset.color;const section=evt.target.dataset.section;if(!color){return;}
  9. let nodes;switch(section){case'text':nodes=this._data.textColors.get(color);break;case'background':nodes=this._data.backgroundColors.get(color);break;case'fill':nodes=this._data.fillColors.get(color);break;case'border':nodes=this._data.borderColors.get(color);break;}
  10. if(!nodes){return;}
  11. nodes=Array.from(nodes).map(nodeId=>({nodeId}));payload={type,color,nodes,section};break;}
  12. case'unused-declarations':{const declaration=evt.target.dataset.declaration;const nodes=this._data.unusedDeclarations.get(declaration);if(!nodes){return;}
  13. payload={type,declaration,nodes};break;}
  14. case'media-queries':{const text=evt.target.dataset.text;const nodes=this._data.mediaQueries.get(text);if(!nodes){return;}
  15. payload={type,text,nodes};break;}
  16. case'font-info':{const value=evt.target.dataset.value;const[fontFamily,fontMetric]=evt.target.dataset.path.split('/');const nodesIds=this._data.fontInfo.get(fontFamily).get(fontMetric).get(value);if(!nodesIds){return;}
  17. const nodes=nodesIds.map(nodeId=>({nodeId}));const name=`${value} (${fontFamily}, ${fontMetric})`;payload={type,name,nodes};break;}
  18. default:return;}
  19. evt.consume();this._controller.dispatchEventToListeners(CssOverview.Events.PopulateNodes,payload);this._mainContainer.setSidebarMinimized(false);}
  20. _onMouseOver(evt){const node=evt.path.find(el=>el.dataset&&el.dataset.backendNodeId);if(!node){return;}
  21. const backendNodeId=Number(node.dataset.backendNodeId);this._controller.dispatchEventToListeners(CssOverview.Events.RequestNodeHighlight,backendNodeId);}
  22. async _render(data){if(!data||!('backgroundColors'in data)||!('textColors'in data)){return;}
  23. this._data=data;const{elementCount,backgroundColors,textColors,fillColors,borderColors,globalStyleStats,mediaQueries,unusedDeclarations,fontInfo}=this._data;const sortedBackgroundColors=this._sortColorsByLuminance(backgroundColors);const sortedTextColors=this._sortColorsByLuminance(textColors);const sortedFillColors=this._sortColorsByLuminance(fillColors);const sortedBorderColors=this._sortColorsByLuminance(borderColors);this._fragment=UI.Fragment.build`
  24. <div class="vbox overview-completed-view">
  25. <div $="summary" class="results-section horizontally-padded summary">
  26. <h1>${ls`Overview summary`}</h1>
  27. <ul>
  28. <li>
  29. <div class="label">${ls`Elements`}</div>
  30. <div class="value">${this._formatter.format(elementCount)}</div>
  31. </li>
  32. <li>
  33. <div class="label">${ls`External stylesheets`}</div>
  34. <div class="value">${this._formatter.format(globalStyleStats.externalSheets)}</div>
  35. </li>
  36. <li>
  37. <div class="label">${ls`Inline style elements`}</div>
  38. <div class="value">${this._formatter.format(globalStyleStats.inlineStyles)}</div>
  39. </li>
  40. <li>
  41. <div class="label">${ls`Style rules`}</div>
  42. <div class="value">${this._formatter.format(globalStyleStats.styleRules)}</div>
  43. </li>
  44. <li>
  45. <div class="label">${ls`Media queries`}</div>
  46. <div class="value">${this._formatter.format(mediaQueries.size)}</div>
  47. </li>
  48. <li>
  49. <div class="label">${ls`Type selectors`}</div>
  50. <div class="value">${this._formatter.format(globalStyleStats.stats.type)}</div>
  51. </li>
  52. <li>
  53. <div class="label">${ls`ID selectors`}</div>
  54. <div class="value">${this._formatter.format(globalStyleStats.stats.id)}</div>
  55. </li>
  56. <li>
  57. <div class="label">${ls`Class selectors`}</div>
  58. <div class="value">${this._formatter.format(globalStyleStats.stats.class)}</div>
  59. </li>
  60. <li>
  61. <div class="label">${ls`Universal selectors`}</div>
  62. <div class="value">${this._formatter.format(globalStyleStats.stats.universal)}</div>
  63. </li>
  64. <li>
  65. <div class="label">${ls`Attribute selectors`}</div>
  66. <div class="value">${this._formatter.format(globalStyleStats.stats.attribute)}</div>
  67. </li>
  68. <li>
  69. <div class="label">${ls`Non-simple selectors`}</div>
  70. <div class="value">${this._formatter.format(globalStyleStats.stats.nonSimple)}</div>
  71. </li>
  72. </ul>
  73. </div>
  74. <div $="colors" class="results-section horizontally-padded colors">
  75. <h1>${ls`Colors`}</h1>
  76. <h2>${ls`Background colors:${sortedBackgroundColors.length}`}</h2>
  77. <ul>
  78. ${sortedBackgroundColors.map(this._colorsToFragment.bind(this, 'background'))}
  79. </ul>
  80. <h2>${ls`Text colors:${sortedTextColors.length}`}</h2>
  81. <ul>
  82. ${sortedTextColors.map(this._colorsToFragment.bind(this, 'text'))}
  83. </ul>
  84. <h2>${ls`Fill colors:${sortedFillColors.length}`}</h2>
  85. <ul>
  86. ${sortedFillColors.map(this._colorsToFragment.bind(this, 'fill'))}
  87. </ul>
  88. <h2>${ls`Border colors:${sortedBorderColors.length}`}</h2>
  89. <ul>
  90. ${sortedBorderColors.map(this._colorsToFragment.bind(this, 'border'))}
  91. </ul>
  92. </div>
  93. <div $="font-info" class="results-section font-info">
  94. <h1>${ls`Font info`}</h1>
  95. ${
  96. fontInfo.size > 0 ? this._fontInfoToFragment(fontInfo) :
  97. UI.Fragment.build`<div>${ls`There are no fonts.`}</div>`}
  98. </div>
  99. <div $="unused-declarations" class="results-section unused-declarations">
  100. <h1>${ls`Unused declarations`}</h1>
  101. ${
  102. unusedDeclarations.size > 0 ?
  103. this._groupToFragment(unusedDeclarations, 'unused-declarations', 'declaration') :
  104. UI.Fragment.build`<div class="horizontally-padded">${ls`There are no unused declarations.`}</div>`}
  105. </div>
  106. <div $="media-queries" class="results-section media-queries">
  107. <h1>${ls`Media queries`}</h1>
  108. ${
  109. mediaQueries.size > 0 ?
  110. this._groupToFragment(mediaQueries, 'media-queries', 'text') :
  111. UI.Fragment.build`<div class="horizontally-padded">${ls`There are no media queries.`}</div>`}
  112. </div>
  113. </div>`;this._resultsContainer.element.appendChild(this._fragment.element());}
  114. _createElementsView(evt){const{type,nodes}=evt.data;let id='';let tabTitle='';switch(type){case'color':const{section,color}=evt.data;id=`${section}-${color}`;tabTitle=`${color.toUpperCase()} (${section})`;break;case'unused-declarations':const{declaration}=evt.data;id=`${declaration}`;tabTitle=`${declaration}`;break;case'media-queries':const{text}=evt.data;id=`${text}`;tabTitle=`${text}`;break;case'font-info':const{name}=evt.data;id=`${name}`;tabTitle=`${name}`;break;}
  115. let view=this._viewMap.get(id);if(!view){view=new ElementDetailsView(this._controller,this._domModel,this._cssModel,this._linkifier);view.populateNodes(nodes);this._viewMap.set(id,view);}
  116. this._elementContainer.appendTab(id,tabTitle,view,true);}
  117. _fontInfoToFragment(fontInfo){const fonts=Array.from(fontInfo.entries());return UI.Fragment.build`
  118. ${fonts.map(([font, fontMetrics]) => {
  119. return UI.Fragment.build
  120. `<section class="font-family"><h2>${font}</h2>${this._fontMetricsToFragment(font,fontMetrics)}</section>`;
  121. })}
  122. `;}
  123. _fontMetricsToFragment(font,fontMetrics){const fontMetricInfo=Array.from(fontMetrics.entries());return UI.Fragment.build`
  124. <div class="font-metric">
  125. ${fontMetricInfo.map(([label, values]) => {
  126. const sanitizedPath = `${font}/${label}`;
  127. return UI.Fragment.build`<div><h3>${label}</h3>${this._groupToFragment(values,'font-info','value',sanitizedPath)}</div>`;
  128. })}
  129. </div>`;}
  130. _groupToFragment(items,type,dataLabel,path=''){const values=Array.from(items.entries()).sort((d1,d2)=>{const v1Nodes=d1[1];const v2Nodes=d2[1];return v2Nodes.length-v1Nodes.length;});const total=values.reduce((prev,curr)=>prev+curr[1].length,0);return UI.Fragment.build`<ul>
  131. ${values.map(([title, nodes]) => {
  132. const width = 100 * nodes.length / total;
  133. const itemLabel = nodes.length === 1 ? ls`occurrence` : ls`occurrences`;
  134. return UI.Fragment.build`<li><div class="title">${title}</div><button data-type="${type}"data-path="${path}"data-${dataLabel}="${title}"><div class="details">${ls`${nodes.length} ${itemLabel}`}</div><div class="bar-container"><div class="bar"style="width: ${width}%"></div></div></button></li>`;
  135. })}
  136. </ul>`;}
  137. _colorsToFragment(section,color){const blockFragment=UI.Fragment.build`<li>
  138. <button data-type="color" data-color="${color}" data-section="${section}" class="block" $="color"></button>
  139. <div class="block-title">${color}</div>
  140. </li>`;const block=blockFragment.$('color');block.style.backgroundColor=color;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);l=Math.max(0,l-15);const borderString=`1px solid hsl(${h}, ${s}%, ${l}%)`;block.style.border=borderString;return blockFragment;}
  141. _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());});}
  142. setOverviewData(data){this._render(data);}}
  143. CSSOverviewCompletedView.pushedNodes=new Set();export class DetailsView extends UI.VBox{constructor(){super();this._tabbedPane=new UI.TabbedPane();this._tabbedPane.show(this.element);this._tabbedPane.addEventListener(UI.TabbedPane.Events.TabClosed,()=>{this.dispatchEventToListeners(UI.TabbedPane.Events.TabClosed,this._tabbedPane.tabIds().length);});}
  144. appendTab(id,tabTitle,view,isCloseable){if(!this._tabbedPane.hasTab(id)){this._tabbedPane.appendTab(id,tabTitle,view,undefined,undefined,isCloseable);}
  145. this._tabbedPane.selectTab(id);}
  146. closeTabs(){this._tabbedPane.closeTabs(this._tabbedPane.tabIds());}}
  147. export class ElementDetailsView extends UI.Widget{constructor(controller,domModel,cssModel,linkifier){super();this._controller=controller;this._domModel=domModel;this._cssModel=cssModel;this._linkifier=linkifier;this._elementGridColumns=[{id:'nodeId',title:ls`Element`,visible:false,sortable:true,hideable:true,weight:50},{id:'declaration',title:ls`Declaration`,visible:false,sortable:true,hideable:true,weight:50},{id:'sourceURL',title:ls`Source`,visible:true,sortable:false,hideable: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._elementGrid.addEventListener(DataGrid.DataGrid.Events.SortingChanged,this._sortMediaQueryDataGrid.bind(this));this.element.appendChild(this._elementGrid.element);}
  148. _sortMediaQueryDataGrid(){const sortColumnId=this._elementGrid.sortColumnId();if(!sortColumnId){return;}
  149. const comparator=DataGrid.SortableDataGrid.StringComparator.bind(null,sortColumnId);this._elementGrid.sortNodes(comparator,!this._elementGrid.isSortOrderAscending());}
  150. _onMouseOver(evt){const node=evt.path.find(el=>el.dataset&&el.dataset.backendNodeId);if(!node){return;}
  151. const backendNodeId=Number(node.dataset.backendNodeId);this._controller.dispatchEventToListeners(CssOverview.Events.RequestNodeHighlight,backendNodeId);}
  152. async populateNodes(data){this._elementGrid.rootNode().removeChildren();if(!data.length){return;}
  153. const[firstItem]=data;const visibility={'nodeId':!!firstItem.nodeId,'declaration':!!firstItem.declaration,'sourceURL':!!firstItem.sourceURL};let relatedNodesMap;if(visibility.nodeId){const nodeIds=data.reduce((prev,curr)=>{if(CssOverview.CSSOverviewCompletedView.pushedNodes.has(curr.nodeId)){return prev;}
  154. CssOverview.CSSOverviewCompletedView.pushedNodes.add(curr.nodeId);return prev.add(curr.nodeId);},new Set());relatedNodesMap=await this._domModel.pushNodesByBackendIdsToFrontend(nodeIds);}
  155. for(const item of data){if(visibility.nodeId){const frontendNode=relatedNodesMap.get(item.nodeId);if(!frontendNode){continue;}
  156. item.node=frontendNode;}
  157. const node=new ElementNode(this._elementGrid,item,this._linkifier,this._cssModel);node.selectable=false;this._elementGrid.insertChild(node);}
  158. this._elementGrid.setColumnsVisiblity(visibility);this._elementGrid.renderInline();this._elementGrid.wasShown();}}
  159. export class ElementNode extends DataGrid.SortableDataGridNode{constructor(dataGrid,data,linkifier,cssModel){super(dataGrid,data.hasChildren);this.data=data;this._linkifier=linkifier;this._cssModel=cssModel;}
  160. createCell(columnId){if(columnId==='nodeId'){const cell=this.createTD(columnId);cell.textContent='...';Common.Linkifier.linkify(this.data.node).then(link=>{cell.textContent='';link.dataset.backendNodeId=this.data.node.backendNodeId();cell.appendChild(link);});return cell;}
  161. if(columnId==='sourceURL'){const cell=this.createTD(columnId);if(this.data.range){const link=this._linkifyRuleLocation(this._cssModel,this._linkifier,this.data.styleSheetId,TextUtils.TextRange.fromObject(this.data.range));if(link.textContent!==''){cell.appendChild(link);}else{cell.textContent=`(unable to link)`;}}else{cell.textContent='(unable to link to inlined styles)';}
  162. return cell;}
  163. return super.createCell(columnId);}
  164. _linkifyRuleLocation(cssModel,linkifier,styleSheetId,ruleLocation){const styleSheetHeader=cssModel.styleSheetHeaderForId(styleSheetId);const lineNumber=styleSheetHeader.lineNumberInSource(ruleLocation.startLine);const columnNumber=styleSheetHeader.columnNumberInSource(ruleLocation.startLine,ruleLocation.startColumn);const matchingSelectorLocation=new SDK.CSSLocation(styleSheetHeader,lineNumber,columnNumber);return linkifier.linkifyCSSLocation(matchingSelectorLocation);}}
  165. self.CssOverview=self.CssOverview||{};CssOverview=CssOverview||{};CssOverview.CSSOverviewCompletedView=CSSOverviewCompletedView;CssOverview.CSSOverviewCompletedView.DetailsView=DetailsView;CssOverview.CSSOverviewCompletedView.ElementDetailsView=ElementDetailsView;CssOverview.CSSOverviewCompletedView.ElementNode=ElementNode;