Bug 1213767 - Rule-view class toggle panel. r=jdescottes

MozReview-Commit-ID: 2roKEm6Jr26
This commit is contained in:
Patrick Brosset 2017-03-03 14:09:23 +01:00
Родитель 7300a549b0
Коммит f7a1e4b454
15 изменённых файлов: 948 добавлений и 19 удалений

Просмотреть файл

@ -98,13 +98,15 @@
<div id="ruleview-command-toolbar">
<button id="ruleview-add-rule-button" data-localization="title=inspector.addRule.tooltip" class="devtools-button"></button>
<button id="pseudo-class-panel-toggle" data-localization="title=inspector.togglePseudo.tooltip" class="devtools-button"></button>
<button id="class-panel-toggle" data-localization="title=inspector.classPanel.toggleClass.tooltip" class="devtools-button"></button>
</div>
</div>
<div id="pseudo-class-panel" hidden="true">
<div id="pseudo-class-panel" class="ruleview-reveal-panel" hidden="true">
<label><input id="pseudo-hover-toggle" type="checkbox" value=":hover" tabindex="-1" />:hover</label>
<label><input id="pseudo-active-toggle" type="checkbox" value=":active" tabindex="-1" />:active</label>
<label><input id="pseudo-focus-toggle" type="checkbox" value=":focus" tabindex="-1" />:focus</label>
</div>
</div>
<div id="ruleview-class-panel" class="ruleview-reveal-panel" hidden="true"></div>
</div>
<div id="ruleview-container" class="ruleview">

Просмотреть файл

@ -17,6 +17,7 @@ const {PrefObserver} = require("devtools/client/shared/prefs");
const ElementStyle = require("devtools/client/inspector/rules/models/element-style");
const Rule = require("devtools/client/inspector/rules/models/rule");
const RuleEditor = require("devtools/client/inspector/rules/views/rule-editor");
const ClassListPreviewer = require("devtools/client/inspector/rules/views/class-list-previewer");
const {gDevTools} = require("devtools/client/framework/devtools");
const {getCssProperties} = require("devtools/shared/fronts/css-properties");
const {
@ -120,6 +121,7 @@ function CssRuleView(inspector, document, store, pageStyle) {
this._onClearSearch = this._onClearSearch.bind(this);
this._onTogglePseudoClassPanel = this._onTogglePseudoClassPanel.bind(this);
this._onTogglePseudoClass = this._onTogglePseudoClass.bind(this);
this._onToggleClassPanel = this._onToggleClassPanel.bind(this);
let doc = this.styleDocument;
this.element = doc.getElementById("ruleview-container-focusable");
@ -128,6 +130,8 @@ function CssRuleView(inspector, document, store, pageStyle) {
this.searchClearButton = doc.getElementById("ruleview-searchinput-clear");
this.pseudoClassPanel = doc.getElementById("pseudo-class-panel");
this.pseudoClassToggle = doc.getElementById("pseudo-class-panel-toggle");
this.classPanel = doc.getElementById("ruleview-class-panel");
this.classToggle = doc.getElementById("class-panel-toggle");
this.hoverCheckbox = doc.getElementById("pseudo-hover-toggle");
this.activeCheckbox = doc.getElementById("pseudo-active-toggle");
this.focusCheckbox = doc.getElementById("pseudo-focus-toggle");
@ -146,8 +150,8 @@ function CssRuleView(inspector, document, store, pageStyle) {
this.searchField.addEventListener("input", this._onFilterStyles);
this.searchField.addEventListener("contextmenu", this.inspector.onTextBoxContextMenu);
this.searchClearButton.addEventListener("click", this._onClearSearch);
this.pseudoClassToggle.addEventListener("click",
this._onTogglePseudoClassPanel);
this.pseudoClassToggle.addEventListener("click", this._onTogglePseudoClassPanel);
this.classToggle.addEventListener("click", this._onToggleClassPanel);
this.hoverCheckbox.addEventListener("click", this._onTogglePseudoClass);
this.activeCheckbox.addEventListener("click", this._onTogglePseudoClass);
this.focusCheckbox.addEventListener("click", this._onTogglePseudoClass);
@ -181,6 +185,8 @@ function CssRuleView(inspector, document, store, pageStyle) {
this.highlighters.addToView(this);
this.classListPreviewer = new ClassListPreviewer(this.inspector, this.classPanel);
EventEmitter.decorate(this);
}
@ -673,6 +679,7 @@ CssRuleView.prototype = {
this.tooltips.destroy();
this.highlighters.removeFromView(this);
this.classListPreviewer.destroy();
// Remove bound listeners
this.shortcuts.destroy();
@ -683,8 +690,8 @@ CssRuleView.prototype = {
this.searchField.removeEventListener("contextmenu",
this.inspector.onTextBoxContextMenu);
this.searchClearButton.removeEventListener("click", this._onClearSearch);
this.pseudoClassToggle.removeEventListener("click",
this._onTogglePseudoClassPanel);
this.pseudoClassToggle.removeEventListener("click", this._onTogglePseudoClassPanel);
this.classToggle.removeEventListener("click", this._onToggleClassPanel);
this.hoverCheckbox.removeEventListener("click", this._onTogglePseudoClass);
this.activeCheckbox.removeEventListener("click", this._onTogglePseudoClass);
this.focusCheckbox.removeEventListener("click", this._onTogglePseudoClass);
@ -693,6 +700,8 @@ CssRuleView.prototype = {
this.searchClearButton = null;
this.pseudoClassPanel = null;
this.pseudoClassToggle = null;
this.classPanel = null;
this.classToggle = null;
this.hoverCheckbox = null;
this.activeCheckbox = null;
this.focusCheckbox = null;
@ -1372,18 +1381,30 @@ CssRuleView.prototype = {
*/
_onTogglePseudoClassPanel: function () {
if (this.pseudoClassPanel.hidden) {
this.pseudoClassToggle.classList.add("checked");
this.hoverCheckbox.setAttribute("tabindex", "0");
this.activeCheckbox.setAttribute("tabindex", "0");
this.focusCheckbox.setAttribute("tabindex", "0");
this.showPseudoClassPanel();
} else {
this.pseudoClassToggle.classList.remove("checked");
this.hoverCheckbox.setAttribute("tabindex", "-1");
this.activeCheckbox.setAttribute("tabindex", "-1");
this.focusCheckbox.setAttribute("tabindex", "-1");
this.hidePseudoClassPanel();
}
},
this.pseudoClassPanel.hidden = !this.pseudoClassPanel.hidden;
showPseudoClassPanel: function () {
this.hideClassPanel();
this.pseudoClassToggle.classList.add("checked");
this.hoverCheckbox.setAttribute("tabindex", "0");
this.activeCheckbox.setAttribute("tabindex", "0");
this.focusCheckbox.setAttribute("tabindex", "0");
this.pseudoClassPanel.hidden = false;
},
hidePseudoClassPanel: function () {
this.pseudoClassToggle.classList.remove("checked");
this.hoverCheckbox.setAttribute("tabindex", "-1");
this.activeCheckbox.setAttribute("tabindex", "-1");
this.focusCheckbox.setAttribute("tabindex", "-1");
this.pseudoClassPanel.hidden = true;
},
/**
@ -1395,6 +1416,32 @@ CssRuleView.prototype = {
this.inspector.togglePseudoClass(target.value);
},
/**
* Called when the class panel button is clicked and toggles the display of the class
* panel.
*/
_onToggleClassPanel: function () {
if (this.classPanel.hidden) {
this.showClassPanel();
} else {
this.hideClassPanel();
}
},
showClassPanel: function () {
this.hidePseudoClassPanel();
this.classToggle.classList.add("checked");
this.classPanel.hidden = false;
this.classListPreviewer.focusAddClassField();
},
hideClassPanel: function () {
this.classToggle.classList.remove("checked");
this.classPanel.hidden = true;
},
/**
* Handle the keypress event in the rule view.
*/

Просмотреть файл

@ -61,6 +61,13 @@ support-files =
[browser_rules_authored_color.js]
[browser_rules_authored_override.js]
[browser_rules_blob_stylesheet.js]
[browser_rules_class_panel_add.js]
[browser_rules_class_panel_content.js]
[browser_rules_class_panel_edit.js]
[browser_rules_class_panel_invalid_nodes.js]
[browser_rules_class_panel_mutation.js]
[browser_rules_class_panel_state_preserved.js]
[browser_rules_class_panel_toggle.js]
[browser_rules_colorpicker-and-image-tooltip_01.js]
[browser_rules_colorpicker-and-image-tooltip_02.js]
[browser_rules_colorpicker-appears-on-swatch-click.js]

Просмотреть файл

@ -0,0 +1,91 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that classes can be added in the class panel
// This array contains the list of test cases. Each test case contains these properties:
// - {String} textEntered The text to be entered in the field
// - {Boolean} expectNoMutation Set to true if we shouldn't wait for a DOM mutation
// - {Array} expectedClasses The expected list of classes to be applied to the DOM and to
// be found in the class panel
const TEST_ARRAY = [{
textEntered: "",
expectNoMutation: true,
expectedClasses: []
}, {
textEntered: "class",
expectedClasses: ["class"]
}, {
textEntered: "class",
expectNoMutation: true,
expectedClasses: ["class"]
}, {
textEntered: "a a a a a a a a a a",
expectedClasses: ["class", "a"]
}, {
textEntered: "class2 class3",
expectedClasses: ["class", "a", "class2", "class3"]
}, {
textEntered: " ",
expectNoMutation: true,
expectedClasses: ["class", "a", "class2", "class3"]
}, {
textEntered: " class4",
expectedClasses: ["class", "a", "class2", "class3", "class4"]
}, {
textEntered: " \t class5 \t \t\t ",
expectedClasses: ["class", "a", "class2", "class3", "class4", "class5"]
}];
add_task(function* () {
yield addTab("data:text/html;charset=utf-8,");
let {testActor, inspector, view} = yield openRuleView();
info("Open the class panel");
view.showClassPanel();
const textField = inspector.panelDoc.querySelector("#ruleview-class-panel .add-class");
ok(textField, "The input field exists in the class panel");
textField.focus();
let onMutation;
for (let {textEntered, expectNoMutation, expectedClasses} of TEST_ARRAY) {
if (!expectNoMutation) {
onMutation = inspector.once("markupmutation");
}
info(`Enter the test string in the field: ${textEntered}`);
for (let key of textEntered.split("")) {
EventUtils.synthesizeKey(key, {}, view.styleWindow);
}
info("Submit the change and wait for the textfield to become empty");
let onEmpty = waitForFieldToBeEmpty(textField);
EventUtils.synthesizeKey("VK_RETURN", {}, view.styleWindow);
if (!expectNoMutation) {
info("Wait for the DOM to change");
yield onMutation;
}
yield onEmpty;
info("Check the state of the DOM node");
let className = yield testActor.getAttribute("body", "class");
let expectedClassName = expectedClasses.length ? expectedClasses.join(" ") : null;
is(className, expectedClassName, "The DOM node has the right className");
info("Check the content of the class panel");
checkClassPanelContent(view, expectedClasses.map(name => {
return {name, state: true};
}));
}
});
function waitForFieldToBeEmpty(textField) {
return waitForSuccess(() => !textField.value);
}

Просмотреть файл

@ -0,0 +1,65 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that class panel shows the right content when selecting various nodes.
// This array contains the list of test cases. Each test case contains these properties:
// - {String} inputClassName The className on a node
// - {Array} expectedClasses The expected list of classes in the class panel
const TEST_ARRAY = [{
inputClassName: "",
expectedClasses: []
}, {
inputClassName: " a a a a a a a a a",
expectedClasses: ["a"]
}, {
inputClassName: "c1 c2 c3 c4 c5",
expectedClasses: ["c1", "c2", "c3", "c4", "c5"]
}, {
inputClassName: "a a b b c c a a b b c c",
expectedClasses: ["a", "b", "c"]
}, {
inputClassName: "ajdhfkasjhdkjashdkjghaskdgkauhkbdhvliashdlghaslidghasldgliashdglhasli",
expectedClasses: [
"ajdhfkasjhdkjashdkjghaskdgkauhkbdhvliashdlghaslidghasldgliashdglhasli"
]
}, {
inputClassName: "c0 c1 c2 c3 c4 c5 c6 c7 c8 c9 " +
"c10 c11 c12 c13 c14 c15 c16 c17 c18 c19 " +
"c20 c21 c22 c23 c24 c25 c26 c27 c28 c29 " +
"c30 c31 c32 c33 c34 c35 c36 c37 c38 c39 " +
"c40 c41 c42 c43 c44 c45 c46 c47 c48 c49",
expectedClasses: ["c0", "c1", "c2", "c3", "c4", "c5", "c6", "c7", "c8", "c9",
"c10", "c11", "c12", "c13", "c14", "c15", "c16", "c17", "c18", "c19",
"c20", "c21", "c22", "c23", "c24", "c25", "c26", "c27", "c28", "c29",
"c30", "c31", "c32", "c33", "c34", "c35", "c36", "c37", "c38", "c39",
"c40", "c41", "c42", "c43", "c44", "c45", "c46", "c47", "c48", "c49"]
}, {
inputClassName: " \n \n class1 \t class2 \t\tclass3\t",
expectedClasses: ["class1", "class2", "class3"]
}];
add_task(function* () {
yield addTab("data:text/html;charset=utf-8,<div>");
let {testActor, inspector, view} = yield openRuleView();
yield selectNode("div", inspector);
info("Open the class panel");
view.showClassPanel();
for (let {inputClassName, expectedClasses} of TEST_ARRAY) {
info(`Apply the '${inputClassName}' className to the node`);
const onMutation = inspector.once("markupmutation");
yield testActor.setAttribute("div", "class", inputClassName);
yield onMutation;
info("Check the content of the class panel");
checkClassPanelContent(view, expectedClasses.map(name => {
return {name, state: true};
}));
}
});

Просмотреть файл

@ -0,0 +1,51 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that classes can be toggled in the class panel
add_task(function* () {
yield addTab("data:text/html;charset=utf-8,<body class='class1 class2'>");
let {view, testActor} = yield openRuleView();
info("Open the class panel");
view.showClassPanel();
info("Click on class1 and check that the checkbox is unchecked and the DOM is updated");
yield toggleClassPanelCheckBox(view, "class1");
checkClassPanelContent(view, [
{name: "class1", state: false},
{name: "class2", state: true}
]);
let newClassName = yield testActor.getAttribute("body", "class");
is(newClassName, "class2", "The class attribute has been updated in the DOM");
info("Click on class2 and check the same thing");
yield toggleClassPanelCheckBox(view, "class2");
checkClassPanelContent(view, [
{name: "class1", state: false},
{name: "class2", state: false}
]);
newClassName = yield testActor.getAttribute("body", "class");
is(newClassName, "", "The class attribute has been updated in the DOM");
info("Click on class2 and checks that the class is added again");
yield toggleClassPanelCheckBox(view, "class2");
checkClassPanelContent(view, [
{name: "class1", state: false},
{name: "class2", state: true}
]);
newClassName = yield testActor.getAttribute("body", "class");
is(newClassName, "class2", "The class attribute has been updated in the DOM");
info("And finally, click on class1 again and checks it is added again");
yield toggleClassPanelCheckBox(view, "class1");
checkClassPanelContent(view, [
{name: "class1", state: true},
{name: "class2", state: true}
]);
newClassName = yield testActor.getAttribute("body", "class");
is(newClassName, "class1 class2", "The class attribute has been updated in the DOM");
});

Просмотреть файл

@ -0,0 +1,50 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test the class panel shows a message when invalid nodes are selected.
// text nodes, pseudo-elements, DOCTYPE, comment nodes.
add_task(function* () {
yield addTab(`data:text/html;charset=utf-8,
<body>
<style>div::after {content: "test";}</style>
<!-- comment -->
Some text
<div></div>
</body>`);
info("Open the class panel");
let {inspector, view} = yield openRuleView();
view.showClassPanel();
info("Selecting the DOCTYPE node");
let {nodes} = yield inspector.walker.children(inspector.walker.rootNode);
yield selectNode(nodes[0], inspector);
checkMessageIsDisplayed(view);
info("Selecting the comment node");
let styleNode = yield getNodeFront("style", inspector);
let commentNode = yield inspector.walker.nextSibling(styleNode);
yield selectNode(commentNode, inspector);
checkMessageIsDisplayed(view);
info("Selecting the text node");
let textNode = yield inspector.walker.nextSibling(commentNode);
yield selectNode(textNode, inspector);
checkMessageIsDisplayed(view);
info("Selecting the ::after pseudo-element");
let divNode = yield getNodeFront("div", inspector);
let pseudoElement = (yield inspector.walker.children(divNode)).nodes[0];
yield selectNode(pseudoElement, inspector);
checkMessageIsDisplayed(view);
});
function checkMessageIsDisplayed(view) {
ok(view.classListPreviewer.classesEl.querySelector(".no-classes"),
"The message is displayed");
checkClassPanelContent(view, []);
}

Просмотреть файл

@ -0,0 +1,74 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that class panel updates on markup mutations
add_task(function* () {
yield addTab("data:text/html;charset=utf-8,<div class='c1 c2'>");
let {inspector, view, testActor} = yield openRuleView();
yield selectNode("div", inspector);
info("Open the class panel");
view.showClassPanel();
info("Trigger an unrelated mutation on the div (id attribute change)");
let onMutation = view.inspector.once("markupmutation");
yield testActor.setAttribute("div", "id", "test-id");
yield onMutation;
info("Check that the panel still contains the right classes");
checkClassPanelContent(view, [
{name: "c1", state: true},
{name: "c2", state: true}
]);
info("Trigger a class mutation on a different, unknown, node");
onMutation = view.inspector.once("markupmutation");
yield testActor.setAttribute("body", "class", "test-class");
yield onMutation;
info("Check that the panel still contains the right classes");
checkClassPanelContent(view, [
{name: "c1", state: true},
{name: "c2", state: true}
]);
info("Trigger a class mutation on the current node");
onMutation = view.inspector.once("markupmutation");
yield testActor.setAttribute("div", "class", "c3 c4");
yield onMutation;
info("Check that the panel now contains the new classes");
checkClassPanelContent(view, [
{name: "c3", state: true},
{name: "c4", state: true}
]);
info("Change the state of one of the new classes");
yield toggleClassPanelCheckBox(view, "c4");
checkClassPanelContent(view, [
{name: "c3", state: true},
{name: "c4", state: false}
]);
info("Select another node");
yield selectNode("body", inspector);
info("Trigger a class mutation on the div");
onMutation = view.inspector.once("markupmutation");
yield testActor.setAttribute("div", "class", "c5 c6 c7");
yield onMutation;
info("Go back to the previous node and check the content of the class panel." +
"Even if hidden, it should have refreshed when we changed the DOM");
yield selectNode("div", inspector);
checkClassPanelContent(view, [
{name: "c5", state: true},
{name: "c6", state: true},
{name: "c7", state: true}
]);
});

Просмотреть файл

@ -0,0 +1,37 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that class states are preserved when switching to other nodes
add_task(function* () {
yield addTab("data:text/html;charset=utf-8,<body class='class1 class2 class3'><div>");
let {inspector, view} = yield openRuleView();
info("Open the class panel");
view.showClassPanel();
info("With the <body> selected, uncheck class2 and class3 in the panel");
yield toggleClassPanelCheckBox(view, "class2");
yield toggleClassPanelCheckBox(view, "class3");
info("Now select the <div> so the panel gets refreshed");
yield selectNode("div", inspector);
is(view.classPanel.querySelectorAll("[type=checkbox]").length, 0,
"The panel content doesn't contain any checkboxes anymore");
info("Select the <body> again");
yield selectNode("body", inspector);
const checkBoxes = view.classPanel.querySelectorAll("[type=checkbox]");
is(checkBoxes[0].dataset.name, "class1", "The first checkbox is class1");
is(checkBoxes[0].checked, true, "The first checkbox is still checked");
is(checkBoxes[1].dataset.name, "class2", "The second checkbox is class2");
is(checkBoxes[1].checked, false, "The second checkbox is still unchecked");
is(checkBoxes[2].dataset.name, "class3", "The third checkbox is class3");
is(checkBoxes[2].checked, false, "The third checkbox is still unchecked");
});

Просмотреть файл

@ -0,0 +1,45 @@
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
// Test that the class panel can be toggled.
add_task(function* () {
yield addTab("data:text/html;charset=utf-8,<body class='class1 class2'>");
let {inspector, view} = yield openRuleView();
info("Check that the toggle button exists");
const button = inspector.panelDoc.querySelector("#class-panel-toggle");
ok(button, "The class panel toggle button exists");
is(view.classToggle, button, "The rule-view refers to the right element");
info("Check that the panel exists and is hidden by default");
const panel = inspector.panelDoc.querySelector("#ruleview-class-panel");
ok(panel, "The class panel exists");
is(view.classPanel, panel, "The rule-view refers to the right element");
ok(panel.hasAttribute("hidden"), "The panel is hidden");
info("Click on the button to show the panel");
button.click();
ok(!panel.hasAttribute("hidden"), "The panel is shown");
ok(button.classList.contains("checked"), "The button is checked");
info("Click again to hide the panel");
button.click();
ok(panel.hasAttribute("hidden"), "The panel is hidden");
ok(!button.classList.contains("checked"), "The button is unchecked");
info("Open the pseudo-class panel first, then the class panel");
view.pseudoClassToggle.click();
ok(!view.pseudoClassPanel.hasAttribute("hidden"), "The pseudo-class panel is shown");
button.click();
ok(!panel.hasAttribute("hidden"), "The panel is shown");
ok(view.pseudoClassPanel.hasAttribute("hidden"), "The pseudo-class panel is hidden");
info("Click again on the pseudo-class button");
view.pseudoClassToggle.click();
ok(panel.hasAttribute("hidden"), "The panel is hidden");
ok(!view.pseudoClassPanel.hasAttribute("hidden"), "The pseudo-class panel is shown");
});

Просмотреть файл

@ -506,3 +506,40 @@ function focusAndSendKey(win, key) {
win.document.documentElement.focus();
EventUtils.sendKey(key, win);
}
/**
* Toggle one of the checkboxes inside the class-panel. Resolved after the DOM mutation
* has been recorded.
* @param {CssRuleView} view The rule-view instance.
* @param {String} name The class name to find the checkbox.
*/
function* toggleClassPanelCheckBox(view, name) {
info(`Clicking on checkbox for class ${name}`);
const checkBox = [...view.classPanel.querySelectorAll("[type=checkbox]")].find(box => {
return box.dataset.name === name;
});
const onMutation = view.inspector.once("markupmutation");
checkBox.click();
info("Waiting for a markupmutation as a result of toggling this class");
yield onMutation;
}
/**
* Verify the content of the class-panel.
* @param {CssRuleView} view The rule-view isntance
* @param {Array} classes The list of expected classes. Each item in this array is an
* object with the following properties: {name: {String}, state: {Boolean}}
*/
function checkClassPanelContent(view, classes) {
const checkBoxNodeList = view.classPanel.querySelectorAll("[type=checkbox]");
is(checkBoxNodeList.length, classes.length,
"The panel contains the expected number of checkboxes");
for (let i = 0; i < classes.length; i++) {
is(checkBoxNodeList[i].dataset.name, classes[i].name,
`Checkbox ${i} has the right class name`);
is(checkBoxNodeList[i].checked, classes[i].state,
`Checkbox ${i} has the right state`);
}
}

Просмотреть файл

@ -0,0 +1,356 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const EventEmitter = require("devtools/shared/event-emitter");
const {LocalizationHelper} = require("devtools/shared/l10n");
const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties");
// This serves as a local cache for the classes applied to each of the node we care about
// here.
// The map is indexed by NodeFront. Any time a new node is selected in the inspector, an
// entry is added here, indexed by the corresponding NodeFront.
// The value for each entry is an array of each of the class this node has. Items of this
// array are objects like: { name, isApplied } where the name is the class itself, and
// isApplied is a Boolean indicating if the class is applied on the node or not.
const CLASSES = new WeakMap();
/**
* Manages the list classes per DOM elements we care about.
* The actual list is stored in the CLASSES const, indexed by NodeFront objects.
* The responsibility of this class is to be the source of truth for anyone who wants to
* know which classes a given NodeFront has, and which of these are enabled and which are
* disabled.
* It also reacts to DOM mutations so the list of classes is up to date with what is in
* the DOM.
* It can also be used to enable/disable a given class, or add classes.
*
* @param {Inspector} inspector
* The current inspector instance.
*/
function ClassListPreviewerModel(inspector) {
EventEmitter.decorate(this);
this.inspector = inspector;
this.onMutations = this.onMutations.bind(this);
this.inspector.on("markupmutation", this.onMutations);
this.classListProxyNode = this.inspector.panelDoc.createElement("div");
}
ClassListPreviewerModel.prototype = {
destroy() {
this.inspector.off("markupmutation", this.onMutations);
this.inspector = null;
this.classListProxyNode = null;
},
/**
* The current node selection (which only returns if the node is an ELEMENT_NODE type
* since that's the only type this model can work with.)
*/
get currentNode() {
if (this.inspector.selection.isElementNode() &&
!this.inspector.selection.isPseudoElementNode()) {
return this.inspector.selection.nodeFront;
}
return null;
},
/**
* The class states for the current node selection. See the documentation of the CLASSES
* constant.
*/
get currentClasses() {
if (!this.currentNode) {
return [];
}
if (!CLASSES.has(this.currentNode)) {
// Use the proxy node to get a clean list of classes.
this.classListProxyNode.className = this.currentNode.className;
let nodeClasses = [...new Set([...this.classListProxyNode.classList])].map(name => {
return { name, isApplied: true };
});
CLASSES.set(this.currentNode, nodeClasses);
}
return CLASSES.get(this.currentNode);
},
/**
* Same as currentClasses, but returns it in the form of a className string, where only
* enabled classes are added.
*/
get currentClassesPreview() {
return this.currentClasses.filter(({ isApplied }) => isApplied)
.map(({ name }) => name)
.join(" ");
},
/**
* Set the state for a given class on the current node.
*
* @param {String} name
* The class which state should be changed.
* @param {Boolean} isApplied
* True if the class should be enabled, false otherwise.
* @return {Promise} Resolves when the change has been made in the DOM.
*/
setClassState(name, isApplied) {
// Do the change in our local model.
let nodeClasses = this.currentClasses;
nodeClasses.find(({ name: cName }) => cName === name).isApplied = isApplied;
return this.applyClassState();
},
/**
* Add several classes to the current node at once.
*
* @param {String} classNameString
* The string that contains all classes.
* @return {Promise} Resolves when the change has been made in the DOM.
*/
addClassName(classNameString) {
this.classListProxyNode.className = classNameString;
return Promise.all([...new Set([...this.classListProxyNode.classList])].map(name => {
return this.addClass(name);
}));
},
/**
* Add a class to the current node at once.
*
* @param {String} name
* The class to be added.
* @return {Promise} Resolves when the change has been made in the DOM.
*/
addClass(name) {
// Avoid adding the same class again.
if (this.currentClasses.some(({ name: cName }) => cName === name)) {
return Promise.resolve();
}
// Change the local model, so we retain the state of the existing classes.
this.currentClasses.push({ name, isApplied: true });
return this.applyClassState();
},
/**
* Used internally by other functions like addClass or setClassState. Actually applies
* the class change to the DOM.
*
* @return {Promise} Resolves when the change has been made in the DOM.
*/
applyClassState() {
// If there is no valid inspector selection, bail out silently. No need to report an
// error here.
if (!this.currentNode) {
return Promise.resolve();
}
// Remember which node we changed and the className we applied, so we can filter out
// dom mutations that are caused by us in onMutations.
this.lastStateChange = {
node: this.currentNode,
className: this.currentClassesPreview
};
// Apply the change to the node.
let mod = this.currentNode.startModifyingAttributes();
mod.setAttribute("class", this.currentClassesPreview);
return mod.apply();
},
onMutations(e, mutations) {
for (let {type, target, attributeName} of mutations) {
// Only care if this mutation is for the class attribute.
if (type !== "attributes" || attributeName !== "class") {
continue;
}
let isMutationForOurChange = this.lastStateChange &&
target === this.lastStateChange.node &&
target.className === this.lastStateChange.className;
if (!isMutationForOurChange) {
CLASSES.delete(target);
if (target === this.currentNode) {
this.emit("current-node-class-changed");
}
}
}
}
};
/**
* This UI widget shows a textfield and a series of checkboxes in the rule-view. It is
* used to toggle classes on the current node selection, and add new classes.
*
* @param {Inspector} inspector
* The current inspector instance.
* @param {DomNode} containerEl
* The element in the rule-view where the widget should go.
*/
function ClassListPreviewer(inspector, containerEl) {
this.inspector = inspector;
this.containerEl = containerEl;
this.model = new ClassListPreviewerModel(inspector);
this.onNewSelection = this.onNewSelection.bind(this);
this.onCheckBoxChanged = this.onCheckBoxChanged.bind(this);
this.onKeyPress = this.onKeyPress.bind(this);
this.onCurrentNodeClassChanged = this.onCurrentNodeClassChanged.bind(this);
// Create the add class text field.
this.addEl = this.doc.createElement("input");
this.addEl.classList.add("devtools-textinput");
this.addEl.classList.add("add-class");
this.addEl.setAttribute("placeholder",
L10N.getStr("inspector.classPanel.newClass.placeholder"));
this.addEl.addEventListener("keypress", this.onKeyPress);
this.containerEl.appendChild(this.addEl);
// Create the class checkboxes container.
this.classesEl = this.doc.createElement("div");
this.classesEl.classList.add("classes");
this.containerEl.appendChild(this.classesEl);
// Start listening for interesting events.
this.inspector.selection.on("new-node-front", this.onNewSelection);
this.containerEl.addEventListener("input", this.onCheckBoxChanged);
this.model.on("current-node-class-changed", this.onCurrentNodeClassChanged);
}
ClassListPreviewer.prototype = {
destroy() {
this.inspector.selection.off("new-node-front", this.onNewSelection);
this.addEl.removeEventListener("keypress", this.onKeyPress);
this.containerEl.removeEventListener("input", this.onCheckBoxChanged);
this.containerEl.innerHTML = "";
this.model.destroy();
this.containerEl = null;
this.inspector = null;
this.addEl = null;
this.classesEl = null;
},
get doc() {
return this.containerEl.ownerDocument;
},
/**
* Render the content of the panel. You typically don't need to call this as the panel
* renders itself on inspector selection changes.
*/
render() {
this.classesEl.innerHTML = "";
for (let { name, isApplied } of this.model.currentClasses) {
let checkBox = this.renderCheckBox(name, isApplied);
this.classesEl.appendChild(checkBox);
}
if (!this.model.currentClasses.length) {
this.classesEl.appendChild(this.renderNoClassesMessage());
}
},
/**
* Render a single checkbox for a given classname.
*
* @param {String} name
* The name of this class.
* @param {Boolean} isApplied
* Is this class currently applied on the DOM node.
* @return {DOMNode} The DOM element for this checkbox.
*/
renderCheckBox(name, isApplied) {
let box = this.doc.createElement("input");
box.setAttribute("type", "checkbox");
if (isApplied) {
box.setAttribute("checked", "checked");
}
box.dataset.name = name;
let labelWrapper = this.doc.createElement("label");
labelWrapper.setAttribute("title", name);
labelWrapper.appendChild(box);
// A child element is required to do the ellipsis.
let label = this.doc.createElement("span");
label.textContent = name;
labelWrapper.appendChild(label);
return labelWrapper;
},
/**
* Render the message displayed in the panel when the current element has no classes.
*
* @return {DOMNode} The DOM element for the message.
*/
renderNoClassesMessage() {
let msg = this.doc.createElement("p");
msg.classList.add("no-classes");
msg.textContent = L10N.getStr("inspector.classPanel.noClasses");
return msg;
},
/**
* Focus the add-class text field.
*/
focusAddClassField() {
if (this.addEl) {
this.addEl.focus();
}
},
onCheckBoxChanged({ target }) {
if (!target.dataset.name) {
return;
}
this.model.setClassState(target.dataset.name, target.checked).catch(e => {
// Only log the error if the panel wasn't destroyed in the meantime.
if (this.containerEl) {
console.error(e);
}
});
},
onKeyPress(event) {
if (event.key !== "Enter" || this.addEl.value === "") {
return;
}
this.model.addClassName(this.addEl.value).then(() => {
this.render();
this.addEl.value = "";
}).catch(e => {
// Only log the error if the panel wasn't destroyed in the meantime.
if (this.containerEl) {
console.error(e);
}
});
},
onNewSelection() {
this.render();
},
onCurrentNodeClassChanged() {
this.render();
}
};
module.exports = ClassListPreviewer;

Просмотреть файл

@ -3,6 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DevToolsModules(
'class-list-previewer.js',
'rule-editor.js',
'text-property-editor.js',
)

Просмотреть файл

@ -377,6 +377,19 @@ inspector.addRule.tooltip=Add new rule
# rule view toolbar.
inspector.togglePseudo.tooltip=Toggle pseudo-classes
# LOCALIZATION NOTE (inspector.classPanel.toggleClass.tooltip): This is the tooltip
# shown when hovering over the `Toggle Class Panel` button in the
# rule view toolbar.
inspector.classPanel.toggleClass.tooltip=Toggle classes
# LOCALIZATION NOTE (inspector.classPanel.newClass.placeholder): This is the placeholder
# shown inside the text field used to add a new class in the rule-view.
inspector.classPanel.newClass.placeholder=Add new class
# LOCALIZATION NOTE (inspector.classPanel.noClasses): This is the text displayed in the
# class panel when the current element has no classes applied.
inspector.classPanel.noClasses=No classes on this element
# LOCALIZATION NOTE (inspector.noProperties): In the case where there are no CSS
# properties to display e.g. due to search criteria this message is
# displayed.

Просмотреть файл

@ -50,24 +50,71 @@
display: flex;
}
#pseudo-class-panel {
.ruleview-reveal-panel {
display: flex;
height: 24px;
overflow: hidden;
transition: height 150ms ease;
}
#pseudo-class-panel[hidden] {
.ruleview-reveal-panel[hidden] {
height: 0px;
}
#pseudo-class-panel > label {
#pseudo-class-panel:not([hidden]) {
height: 24px;
}
.ruleview-reveal-panel label {
-moz-user-select: none;
flex-grow: 1;
display: flex;
align-items: center;
}
/* Class toggle panel */
#ruleview-class-panel:not([hidden]) {
/* The class panel can contain 0 to N classes, so we can't hardcode a height here like
we do for the pseudo-class panel. Unfortunately, that means we don't get the height
transition when toggling the panel */
flex-direction: column;
}
#ruleview-class-panel .add-class {
margin: 0;
border-width: 0 0 1px 0;
padding: 2px 6px;
border-radius: 0;
}
#ruleview-class-panel .classes {
display: flex;
flex-direction: row;
flex-wrap: wrap;
}
#ruleview-class-panel .classes {
max-height: 100px;
overflow-y: auto;
}
#ruleview-class-panel .classes label {
flex: 0 0;
max-width: 50%;
}
#ruleview-class-panel .classes label span {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
#ruleview-class-panel .no-classes {
flex: 1;
color: var(--theme-body-color-inactive);
margin: 0;
text-align: center;
}
/* Rule View Container */
#ruleview-container {
@ -559,6 +606,12 @@
background-size: cover;
}
#class-panel-toggle::before {
content: ".cls";
direction: ltr;
font-size: 11px;
}
.ruleview-overridden-rule-filter {
opacity: 0.8;
}