Bug 1788033 - [devtools] Display container, layer and supports in StyleEditor at-rules sidebar. r=jdescottes.

Differential Revision: https://phabricator.services.mozilla.com/D157084
This commit is contained in:
Nicolas Chevobbe 2022-11-09 16:29:51 +00:00
parent 38d79b9eaf
commit 6153f85643
7 changed files with 172 additions and 59 deletions

View File

@ -1329,31 +1329,42 @@ export class StyleEditorUI extends EventEmitter {
inSource = true;
const div = this.#panelDoc.createElementNS(HTML_NS, "div");
div.className = "at-rule-label";
div.classList.add("at-rule-label", rule.type);
div.addEventListener(
"click",
this.#jumpToLocation.bind(this, location)
);
const cond = this.#panelDoc.createElementNS(HTML_NS, "div");
const ruleTextContainer = this.#panelDoc.createElementNS(
HTML_NS,
"div"
);
const type = this.#panelDoc.createElementNS(HTML_NS, "span");
type.className = "at-rule-type";
type.append(this.#panelDoc.createTextNode(`@${rule.type}\u00A0`));
if (rule.type == "layer" && rule.layerName) {
type.append(this.#panelDoc.createTextNode(`${rule.layerName}\u00A0`));
}
const cond = this.#panelDoc.createElementNS(HTML_NS, "span");
cond.className = "at-rule-condition";
if (!rule.matches) {
if (rule.type == "media" && !rule.matches) {
cond.classList.add("media-condition-unmatched");
}
if (this.#commands.descriptorFront.isLocalTab) {
this.#setConditionContents(cond, rule.conditionText);
this.#setConditionContents(cond, rule.conditionText, rule.type);
} else {
cond.textContent = rule.conditionText;
}
div.appendChild(cond);
const link = this.#panelDoc.createElementNS(HTML_NS, "div");
link.className = "at-rule-line theme-link";
if (location.line != -1) {
link.textContent = ":" + location.line;
}
div.appendChild(link);
ruleTextContainer.append(type, cond);
div.append(ruleTextContainer, link);
list.appendChild(div);
}
@ -1366,38 +1377,55 @@ export class StyleEditorUI extends EventEmitter {
};
/**
* Used to safely inject media query links
* Set the condition text for the at-rule element.
* For media queries, it also injects links to open RDM at a specific size.
*
* @param {HTMLElement} element
* The element corresponding to the media sidebar condition
* @param {String} rawText
* The raw condition text to parse
* @param {String} ruleConditionText
* The rule conditionText
* @param {String} type
* The type of the at-rule (e.g. "media", "layer", "supports", )
*/
#setConditionContents(element, rawText) {
#setConditionContents(element, ruleConditionText, type) {
if (!ruleConditionText) {
return;
}
// For non-media rules, we don't do anything more than displaying the conditionText
// as there are no other condition text that would justify opening RDM at a specific
// size (e.g. `@container` condition is relative to a container size, which varies
// depending the node the rule applies to).
if (type !== "media") {
const node = this.#panelDoc.createTextNode(ruleConditionText);
element.appendChild(node);
return;
}
const minMaxPattern = /(min\-|max\-)(width|height):\s\d+(px)/gi;
let match = minMaxPattern.exec(rawText);
let match = minMaxPattern.exec(ruleConditionText);
let lastParsed = 0;
while (match && match.index != minMaxPattern.lastIndex) {
const matchEnd = match.index + match[0].length;
const node = this.#panelDoc.createTextNode(
rawText.substring(lastParsed, match.index)
ruleConditionText.substring(lastParsed, match.index)
);
element.appendChild(node);
const link = this.#panelDoc.createElementNS(HTML_NS, "a");
link.href = "#";
link.className = "media-responsive-mode-toggle";
link.textContent = rawText.substring(match.index, matchEnd);
link.textContent = ruleConditionText.substring(match.index, matchEnd);
link.addEventListener("click", this.#onMediaConditionClick.bind(this));
element.appendChild(link);
match = minMaxPattern.exec(rawText);
match = minMaxPattern.exec(ruleConditionText);
lastParsed = matchEnd;
}
const node = this.#panelDoc.createTextNode(
rawText.substring(lastParsed, rawText.length)
ruleConditionText.substring(lastParsed, ruleConditionText.length)
);
element.appendChild(node);
}

View File

@ -79,6 +79,7 @@ support-files =
!/devtools/client/shared/test/highlighter-test-actor.js
[browser_styleeditor_add_stylesheet.js]
[browser_styleeditor_at_rules_sidebar.js]
[browser_styleeditor_autocomplete.js]
[browser_styleeditor_autocomplete-disabled.js]
[browser_styleeditor_bom.js]
@ -101,7 +102,6 @@ skip-if = !debug && (os == "win") || (os == "linux" && os_version == "18.04") #b
[browser_styleeditor_inline_friendly_names.js]
[browser_styleeditor_loading.js]
[browser_styleeditor_loading_with_containers.js]
[browser_styleeditor_media_sidebar.js]
[browser_styleeditor_media_sidebar_links.js]
[browser_styleeditor_media_sidebar_sourcemaps.js]
[browser_styleeditor_missing_stylesheet.js]

View File

@ -34,6 +34,8 @@ const NEW_RULE = `
waitForExplicitFinish();
add_task(async function() {
await pushPref("layout.css.container-queries.enabled", true);
const { ui } = await openStyleEditorForURL(TESTCASE_URI);
is(ui.editors.length, 4, "correct number of editors");
@ -82,7 +84,7 @@ async function testInlineMediaEditor(ui, editor) {
is(sidebar.hidden, false, "sidebar is showing on editor with @media");
const entries = sidebar.querySelectorAll(".at-rule-label");
is(entries.length, 2, "2 @media rules displayed in sidebar");
is(entries.length, 5, "5 @media rules displayed in sidebar");
await testRule({
ui,
@ -91,15 +93,45 @@ async function testInlineMediaEditor(ui, editor) {
conditionText: "screen",
matches: true,
line: 2,
type: "media",
});
await testRule({
ui,
editor,
rule: entries[1],
conditionText: "(display: flex)",
line: 7,
type: "support",
});
await testRule({
ui,
editor,
rule: entries[2],
conditionText: "(1px < height < 10000px)",
matches: true,
line: 8,
type: "media",
});
await testRule({
ui,
editor,
rule: entries[3],
conditionText: "",
line: 16,
type: "layer",
layerName: "myLayer",
});
await testRule({
ui,
editor,
rule: entries[4],
conditionText: "(min-width: 1px)",
line: 17,
type: "container",
});
}
@ -215,24 +247,45 @@ async function testMediaRuleAdded(ui, editor) {
* @param {Element} options.rule: The rule element in the media sidebar
* @param {String} options.conditionText: media query condition text
* @param {Boolean} options.matches: Whether or not the document matches the rule
* @param {String} options.layerName: Optional name of the @layer
* @param {Number} options.line: Line of the rule
* @param {String} options.type: The type of the rule (container, layer, media, support ).
* Defaults to "media".
*/
async function testRule({ ui, editor, rule, conditionText, matches, line }) {
async function testRule({
ui,
editor,
rule,
conditionText,
matches,
layerName,
line,
type = "media",
}) {
const atTypeEl = rule.querySelector(".at-rule-type");
is(
atTypeEl.textContent,
`@${type}\u00A0${layerName ? `${layerName}\u00A0` : ""}`,
"label for at-rule type is correct"
);
const cond = rule.querySelector(".at-rule-condition");
is(
cond.textContent,
conditionText,
"media label is correct for " + conditionText
"condition label is correct for " + conditionText
);
const matched = !cond.classList.contains("media-condition-unmatched");
ok(
matches ? matched : !matched,
"media rule is " + (matches ? "matched" : "unmatched")
);
if (type == "media") {
const matched = !cond.classList.contains("media-condition-unmatched");
ok(
matches ? matched : !matched,
"media rule is " + (matches ? "matched" : "unmatched")
);
}
const mediaRuleLine = rule.querySelector(".at-rule-line");
is(mediaRuleLine.textContent, ":" + line, "correct line number shown");
const ruleLine = rule.querySelector(".at-rule-line");
is(ruleLine.textContent, ":" + line, "correct line number shown");
info(
"Check that clicking on the rule jumps to the expected position in the stylesheet"

View File

@ -25,12 +25,12 @@ add_task(async function() {
// Test editor with @media rules
const mediaEditor = ui.editors[0];
await openEditor(mediaEditor);
testMediaEditor(mediaEditor);
testAtRulesEditor(mediaEditor);
Services.prefs.clearUserPref(MAP_PREF);
});
function testMediaEditor(editor) {
function testAtRulesEditor(editor) {
const sidebar = editor.details.querySelector(".stylesheet-sidebar");
is(sidebar.hidden, false, "sidebar is showing on editor with @media");

View File

@ -24,6 +24,9 @@
}
}
}
@layer myLayer {}
@container (min-width: 1px) {}
</style>
</head>
<body>

View File

@ -132,10 +132,17 @@ li.error > .stylesheet-info > .stylesheet-more > .stylesheet-error-message {
}
.at-rule-label {
display: flex;
display: grid;
/*
* +----------------------------------------------+
* | Rule information | line number |
* +----------------------------------------------+
*/
grid-template-columns: 1fr max-content;
padding: 4px;
cursor: pointer;
border-bottom: 1px solid var(--theme-splitter-color);
cursor: pointer;
word-break: break-word;
}
.media-responsive-mode-toggle {
@ -151,11 +158,6 @@ li.error > .stylesheet-info > .stylesheet-more > .stylesheet-error-message {
opacity: 0.4;
}
.at-rule-condition {
flex: 1;
overflow: hidden;
}
.stylesheet-toggle {
display: -moz-box;
cursor: pointer;

View File

@ -597,8 +597,55 @@ class StyleSheetsManager extends EventEmitter {
continue;
}
if (rule.type === CSSRule.MEDIA_RULE) {
rules.push(rule);
const line = InspectorUtils.getRelativeRuleLine(rule);
const column = InspectorUtils.getRuleColumn(rule);
const className = ChromeUtils.getClassName(rule);
if (className === "CSSMediaRule") {
let matches = false;
try {
const mql = win.matchMedia(rule.media.mediaText);
matches = mql.matches;
mql.onchange = this._onMatchesChange.bind(
this,
resourceId,
rules.length
);
this._mqlList.push(mql);
} catch (e) {
// Ignored
}
rules.push({
type: "media",
mediaText: rule.media.mediaText,
conditionText: rule.conditionText,
matches,
line,
column,
});
} else if (className === "CSSContainerRule") {
rules.push({
type: "container",
conditionText: rule.conditionText,
line,
column,
});
} else if (className === "CSSSupportsRule") {
rules.push({
type: "support",
conditionText: rule.conditionText,
line,
column,
});
} else if (className === "CSSLayerBlockRule") {
rules.push({
type: "layer",
layerName: rule.name,
line,
column,
});
}
if (rule.cssRules) {
@ -607,27 +654,7 @@ class StyleSheetsManager extends EventEmitter {
}
};
traverseRules(styleSheetRules);
return rules.map((rule, index) => {
let matches = false;
try {
const mql = win.matchMedia(rule.media.mediaText);
matches = mql.matches;
mql.onchange = this._onMatchesChange.bind(this, resourceId, index);
this._mqlList.push(mql);
} catch (e) {
// Ignored
}
return {
mediaText: rule.media.mediaText,
conditionText: rule.conditionText,
matches,
line: InspectorUtils.getRelativeRuleLine(rule),
column: InspectorUtils.getRuleColumn(rule),
};
});
return rules;
}
/**
@ -637,7 +664,7 @@ class StyleSheetsManager extends EventEmitter {
* @param {String} resourceId
* The id associated with the stylesheet
* @param {Number} index
* The index of the media rule relatively to all the other media rules of the stylesheet
* The index of the media rule relatively to all the other at-rules of the stylesheet
* @param {MediaQueryList} mql
* The result of matchMedia for the given media rule
*/