Show list of grid elements in Layout pane
This CL adds a list of grid elements to the Layout pane and
synchronises the overlay state with adorners. The ability to navigate
to an element or configure the colors will be done in follow-up CLs.
Screenshot: https://i.imgur.com/y8bsLzk.png
Bug: 1109177
Change-Id: I7700a37e1e8b03b48e3e4394f3e6c95a41d543f0
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2352712
Reviewed-by: Patrick Brosset <[email protected]>
Reviewed-by: Changhao Han <[email protected]>
Reviewed-by: Mathias Bynens <[email protected]>
Commit-Queue: Alex Rudenko <[email protected]>
diff --git a/front_end/elements/ElementsBreadcrumbs.ts b/front_end/elements/ElementsBreadcrumbs.ts
index 3fd618d2..1abf540 100644
--- a/front_end/elements/ElementsBreadcrumbs.ts
+++ b/front_end/elements/ElementsBreadcrumbs.ts
@@ -84,7 +84,7 @@
}
if (title.extras.classes && title.extras.classes.length > 0) {
- const text = title.extras.classes.map(c => `.${c}`).join('');
+ const text = title.extras.classes.map(c => `.${CSS.escape(c)}`).join('');
parts.push(LitHtml.html`<span class="extra node-label-class">${text}</span>`);
}
diff --git a/front_end/elements/ElementsTreeElement.js b/front_end/elements/ElementsTreeElement.js
index 789a086..9aded91 100644
--- a/front_end/elements/ElementsTreeElement.js
+++ b/front_end/elements/ElementsTreeElement.js
@@ -1971,6 +1971,14 @@
node.domModel().overlayModel().addEventListener(SDK.OverlayModel.Events.PersistentGridOverlayCleared, () => {
gridAdorner.toggle(false /* force inactive state */);
});
+ node.domModel().overlayModel().addEventListener(
+ SDK.OverlayModel.Events.PersistentGridOverlayStateChanged, event => {
+ const {nodeId: eventNodeId, enabled} = event.data;
+ if (eventNodeId !== nodeId) {
+ return;
+ }
+ gridAdorner.toggle(enabled);
+ });
}
}
}
diff --git a/front_end/elements/LayoutPane.ts b/front_end/elements/LayoutPane.ts
index dd07115..4fcbf38 100644
--- a/front_end/elements/LayoutPane.ts
+++ b/front_end/elements/LayoutPane.ts
@@ -8,8 +8,9 @@
const {render, html} = LitHtml;
const ls = Common.ls;
+const getStyleSheets = ComponentHelpers.GetStylesheet.getStyleSheets;
-import {BooleanSetting, EnumSetting, Setting, SettingType} from './LayoutPaneUtils.js';
+import {BooleanSetting, Element, EnumSetting, Setting, SettingType} from './LayoutPaneUtils.js';
export class SettingChangedEvent extends Event {
data: {setting: string, value: string|boolean};
@@ -20,6 +21,15 @@
}
}
+export class OverlayChangedEvent extends Event {
+ data: {id: number, value: boolean};
+
+ constructor(id: number, value: boolean) {
+ super('overlay-changed', {});
+ this.data = {id, value};
+ }
+}
+
interface HTMLInputElementEvent extends Event {
target: HTMLInputElement;
}
@@ -35,15 +45,19 @@
export class LayoutPane extends HTMLElement {
private readonly shadow = this.attachShadow({mode: 'open'});
private settings: Readonly<Setting[]> = [];
+ private gridElements: Readonly<Element[]> = [];
constructor() {
super();
- this.shadow.adoptedStyleSheets =
- ComponentHelpers.GetStylesheet.getStyleSheets('ui/inspectorCommon.css', {patchThemeSupport: true});
+ this.shadow.adoptedStyleSheets = [
+ ...getStyleSheets('ui/inspectorCommon.css', {patchThemeSupport: true}),
+ ...getStyleSheets('ui/inspectorSyntaxHighlight.css', {patchThemeSupport: true}),
+ ];
}
- set data(data: {settings: Setting[]}) {
+ set data(data: {settings: Setting[], gridElements: Element[]}) {
this.settings = data.settings;
+ this.gridElements = data.gridElements;
this.render();
}
@@ -52,6 +66,10 @@
// clang-format off
render(html`
<style>
+ * {
+ box-sizing: border-box;
+ font-size: 12px;
+ }
.header {
align-items: center;
background-color: var(--toolbar-bg-color, #f3f3f3);
@@ -65,6 +83,7 @@
}
.content-section {
padding: 16px;
+ border-bottom: var(--divider-border, 1px solid #d0d0d0);
}
.content-section-title {
font-size: 12px;
@@ -82,6 +101,11 @@
.checkbox-label {
display: flex;
flex-direction: row;
+ align-items: start;
+ margin-bottom: 8px;
+ }
+ .checkbox-label:last-child {
+ margin-bottom: 0;
}
.checkbox-label input {
margin: 0 6px 0 0;
@@ -100,11 +124,22 @@
.select-label span {
margin-bottom: 4px;
}
+ .elements {
+ margin-top: 12px;
+ color: var(--dom-tag-name-color);
+ }
</style>
<details open>
<summary class="header">
${ls`Grid`}
</summary>
+ ${this.gridElements ?
+ html`<div class="content-section">
+ <h3 class="content-section-title">${ls`Grid overlays`}</h3>
+ <div class="elements">
+ ${this.gridElements.map(element => this.renderElement(element))}
+ </div>
+ </div>` : ''}
<div class="content-section">
<h3 class="content-section-title">${ls`Overlay display settings`}</h3>
<div class="checkbox-settings">
@@ -139,6 +174,31 @@
this.dispatchEvent(new SettingChangedEvent(setting.name, event.target.value));
}
+ private onElementToggle(element: Element, event: HTMLInputElementEvent) {
+ event.preventDefault();
+ this.dispatchEvent(new OverlayChangedEvent(element.id, event.target.checked));
+ }
+
+ private renderElement(element: Element) {
+ const name = this.buildElementName(element);
+ const onElementToggle = this.onElementToggle.bind(this, element);
+ return html`<label data-element="true" class="checkbox-label" title=${name}>
+ <input data-input="true" type="checkbox" .checked=${element.enabled} @change=${onElementToggle} />
+ <span data-label="true">${name}</span>
+ </label>`;
+ }
+
+ private buildElementName(element: Element) {
+ const parts = [element.name];
+ if (element.domId) {
+ parts.push(`#${CSS.escape(element.domId)}`);
+ }
+ if (element.domClasses) {
+ parts.push(...element.domClasses.map(cls => `.${CSS.escape(cls)}`));
+ }
+ return parts.join('');
+ }
+
private renderBooleanSetting(setting: BooleanSetting) {
const onBooleanSettingChange = this.onBooleanSettingChange.bind(this, setting);
return html`<label data-boolean-setting="true" class="checkbox-label" title=${setting.title}>
diff --git a/front_end/elements/LayoutPaneUtils.ts b/front_end/elements/LayoutPaneUtils.ts
index 1450f2a..2e1fd15 100644
--- a/front_end/elements/LayoutPaneUtils.ts
+++ b/front_end/elements/LayoutPaneUtils.ts
@@ -24,6 +24,14 @@
title: string;
}
-export type BooleanSetting = BaseSetting&{type: SettingType.BOOLEAN, options: BooleanSettingOption[], value: boolean};
-export type EnumSetting = BaseSetting&{type: SettingType.ENUM, options: EnumSettingOption[], value: string};
+export type BooleanSetting = BaseSetting&{options: BooleanSettingOption[], value: boolean};
+export type EnumSetting = BaseSetting&{options: EnumSettingOption[], value: string};
export type Setting = EnumSetting|BooleanSetting;
+
+export interface Element {
+ id: number;
+ name: string;
+ domId?: string;
+ domClasses?: string[];
+ enabled: boolean;
+}
diff --git a/front_end/elements/LayoutPane_bridge.js b/front_end/elements/LayoutPane_bridge.js
index 3cc3720..6d8a556 100644
--- a/front_end/elements/LayoutPane_bridge.js
+++ b/front_end/elements/LayoutPane_bridge.js
@@ -17,10 +17,21 @@
export let Setting;
/**
* @typedef {{
+* id:number,
* name:string,
-* type:string,
+* domId:(string|undefined),
+* domClasses:(!Array.<string>|undefined),
+* enabled:boolean,
+* }}
+*/
+// @ts-ignore we export this for Closure not TS
+export let Element;
+/**
+* @typedef {{
+* name:string,
+* type:!SettingType,
* title:string,
-* options:Array.<!EnumSettingOption>,
+* options:!Array.<!EnumSettingOption>,
* value:string,
* }}
*/
@@ -29,21 +40,25 @@
/**
* @typedef {{
* name:string,
-* type:string,
+* type:!SettingType,
* title:string,
-* options:Array.<!BooleanSettingOption>,
+* options:!Array.<!BooleanSettingOption>,
* value:boolean,
* }}
*/
// @ts-ignore we export this for Closure not TS
export let BooleanSetting;
/**
-* @typedef {string}
+* @enum {string}
*/
// @ts-ignore we export this for Closure not TS
-export let SettingType;
+export const SettingType = {
+ BOOLEAN: 'boolean',
+ ENUM: 'enum',
+};
/**
* @typedef {{
+* title:string,
* value:string,
* }}
*/
@@ -51,6 +66,7 @@
export let EnumSettingOption;
/**
* @typedef {{
+* title:string,
* value:boolean,
* }}
*/
@@ -59,7 +75,7 @@
// eslint-disable-next-line no-unused-vars
export class LayoutPaneClosureInterface extends HTMLElement {
/**
- * @param {{settings: !Array.<!Setting>}} data
+ * @param {{settings: !Array.<!Setting>, gridElements: !Array.<!Element>}} data
*/
set data(data) {
}
diff --git a/front_end/elements/LayoutSidebarPane.js b/front_end/elements/LayoutSidebarPane.js
index 294cf39..e86ac7d 100644
--- a/front_end/elements/LayoutSidebarPane.js
+++ b/front_end/elements/LayoutSidebarPane.js
@@ -9,7 +9,21 @@
import * as SDK from '../sdk/sdk.js';
import * as UI from '../ui/ui.js';
-import {createLayoutPane} from './LayoutPane_bridge.js';
+import {createLayoutPane, Element} from './LayoutPane_bridge.js'; // eslint-disable-line no-unused-vars
+
+/**
+ * @param {!Array<!SDK.DOMModel.DOMNode>} nodes
+ * @return {!Array<!Element>}
+ */
+const gridNodesToElements = nodes => {
+ return nodes.map(node => ({
+ id: node.id,
+ name: node.localName(),
+ domId: node.getAttribute('id'),
+ domClasses: (node.getAttribute('class') || '').split(/\s+/),
+ enabled: node.domModel().overlayModel().isHighlightedGridInPersistentOverlay(node.id)
+ }));
+};
/**
* @unrestricted
@@ -22,12 +36,39 @@
this._settings = [
'showGridBorder', 'showGridLines', 'showGridLineNumbers', 'showGridGaps', 'showGridAreas', 'showGridTrackSizes'
];
- this._layoutPane.addEventListener('setting-changed', event => {
- Common.Settings.Settings.instance().moduleSetting(event.data.setting).set(event.data.value);
- });
- this._settings.forEach(setting => {
- Common.Settings.Settings.instance().moduleSetting(setting).addChangeListener(this.update, this);
- });
+ this._node = self.UI.context.flavor(SDK.DOMModel.DOMNode);
+ this._boundOnSettingChanged = this.onSettingChanged.bind(this);
+ this._boundOnOverlayChanged = this.onOverlayChanged.bind(this);
+ }
+
+ async _fetchGridNodes() {
+ const nodeIds = await this._node.domModel().getNodesByStyle(
+ [{name: 'display', value: 'grid'}, {name: 'display', value: 'inline-grid'}], true /* pierce */);
+ const nodes = nodeIds.map(id => this._node.domModel().nodeForId(id)).filter(node => node !== null);
+ return nodes;
+ }
+
+ _mapSettings() {
+ return this._settings
+ .map(settingName => {
+ const setting = Common.Settings.Settings.instance().moduleSetting(settingName);
+ const ext = setting.extension();
+ if (!ext) {
+ return null;
+ }
+ const descriptor = ext.descriptor();
+ return {
+ type: descriptor.settingType,
+ name: descriptor.settingName,
+ title: descriptor.title,
+ value: setting.get(),
+ options: descriptor.options.map(opt => ({
+ title: opt.text,
+ value: opt.value,
+ }))
+ };
+ })
+ .filter(descriptor => descriptor !== null);
}
/**
@@ -37,34 +78,42 @@
*/
async doUpdate() {
this._layoutPane.data = {
- settings: this._settings
- .map(settingName => {
- const setting = Common.Settings.Settings.instance().moduleSetting(settingName);
- const ext = setting.extension();
- if (!ext) {
- return null;
- }
- const descriptor = ext.descriptor();
- return {
- type: descriptor.settingType,
- name: descriptor.settingName,
- title: descriptor.title,
- value: setting.get(),
- options: descriptor.options.map(opt => ({
- title: opt.text,
- value: opt.value,
- }))
- };
- })
- .filter(descriptor => descriptor !== null)
+ gridElements: gridNodesToElements(await this._fetchGridNodes()),
+ settings: this._mapSettings(),
};
}
/**
+ * @param {*} event
+ */
+ onSettingChanged(event) {
+ Common.Settings.Settings.instance().moduleSetting(event.data.setting).set(event.data.value);
+ }
+
+ /**
+ * @param {*} event
+ */
+ onOverlayChanged(event) {
+ const node = this._node.domModel().nodeForId(event.data.id);
+ if (event.data.value) {
+ node.domModel().overlayModel().highlightGridInPersistentOverlay(event.data.id);
+ } else {
+ node.domModel().overlayModel().hideGridInPersistentOverlay(event.data.id);
+ }
+ }
+
+ /**
* @override
*/
wasShown() {
- self.UI.context.addFlavorChangeListener(SDK.DOMModel.DOMNode, this.update, this);
+ for (const setting of this._settings) {
+ Common.Settings.Settings.instance().moduleSetting(setting).addChangeListener(this.update, this);
+ }
+ this._layoutPane.addEventListener('setting-changed', this._boundOnSettingChanged);
+ this._layoutPane.addEventListener('overlay-changed', this._boundOnOverlayChanged);
+ const overlayModel = this._node.domModel().overlayModel();
+ overlayModel.addEventListener(SDK.OverlayModel.Events.PersistentGridOverlayCleared, this.update, this);
+ overlayModel.addEventListener(SDK.OverlayModel.Events.PersistentGridOverlayStateChanged, this.update, this);
this.update();
}
@@ -72,9 +121,13 @@
* @override
*/
willHide() {
- self.UI.context.removeFlavorChangeListener(SDK.DOMModel.DOMNode, this.update, this);
- this._settings.forEach(setting => {
- Common.Settings.Settings.instance().moduleSetting(setting).removeChangeListener(this.update.bind, this);
- });
+ for (const setting of this._settings) {
+ Common.Settings.Settings.instance().moduleSetting(setting).removeChangeListener(this.update, this);
+ }
+ this._layoutPane.removeEventListener('setting-changed', this._boundOnSettingChanged);
+ this._layoutPane.removeEventListener('overlay-changed', this._boundOnOverlayChanged);
+ const overlayModel = this._node.domModel().overlayModel();
+ overlayModel.removeEventListener(SDK.OverlayModel.Events.PersistentGridOverlayCleared, this.update, this);
+ overlayModel.removeEventListener(SDK.OverlayModel.Events.PersistentGridOverlayStateChanged, this.update, this);
}
}
diff --git a/front_end/elements/elements_strings.grdp b/front_end/elements/elements_strings.grdp
index cbf9caa..f6a9bf7 100644
--- a/front_end/elements/elements_strings.grdp
+++ b/front_end/elements/elements_strings.grdp
@@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<grit-part>
+ <message name="IDS_DEVTOOLS_03f126ba6fa92a1218920ff632546eb1" desc="Title of a section in Layout sidebar pane">
+ Grid overlays
+ </message>
<message name="IDS_DEVTOOLS_05ffd37b74b38636b25349b7a8c1ebba" desc="ARIA label for Elements Tree adorners">
Enable grid mode
</message>