Bug 1520389 - Implement the selector highlighter in the new rules view. r=rcaliman

This commit is contained in:
Gabriel Luong 2019-01-17 10:29:38 -05:00
Родитель e42f7abbcf
Коммит a5f46f0927
11 изменённых файлов: 247 добавлений и 3 удалений

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

@ -19,6 +19,9 @@ createEnum([
// Toggles on or off the given pseudo class value for the current selected element. // Toggles on or off the given pseudo class value for the current selected element.
"TOGGLE_PSEUDO_CLASS", "TOGGLE_PSEUDO_CLASS",
// Updates the highlighted selector.
"UPDATE_HIGHLIGHTED_SELECTOR",
// Updates the rules state with the new list of CSS rules for the selected element. // Updates the rules state with the new list of CSS rules for the selected element.
"UPDATE_RULES", "UPDATE_RULES",

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

@ -5,11 +5,25 @@
"use strict"; "use strict";
const { const {
UPDATE_HIGHLIGHTED_SELECTOR,
UPDATE_RULES, UPDATE_RULES,
} = require("./index"); } = require("./index");
module.exports = { module.exports = {
/**
* Updates the highlighted selector.
*
* @param {String} highlightedSelector
* The selector of the element to be highlighted by the selector highlighter.
*/
updateHighlightedSelector(highlightedSelector) {
return {
type: UPDATE_HIGHLIGHTED_SELECTOR,
highlightedSelector,
};
},
/** /**
* Updates the rules state with the new list of CSS rules for the selected element. * Updates the rules state with the new list of CSS rules for the selected element.
* *

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

@ -10,6 +10,7 @@ const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const Declarations = createFactory(require("./Declarations")); const Declarations = createFactory(require("./Declarations"));
const Selector = createFactory(require("./Selector")); const Selector = createFactory(require("./Selector"));
const SelectorHighlighter = createFactory(require("./SelectorHighlighter"));
const SourceLink = createFactory(require("./SourceLink")); const SourceLink = createFactory(require("./SourceLink"));
const Types = require("../types"); const Types = require("../types");
@ -18,6 +19,7 @@ class Rule extends PureComponent {
static get propTypes() { static get propTypes() {
return { return {
onToggleDeclaration: PropTypes.func.isRequired, onToggleDeclaration: PropTypes.func.isRequired,
onToggleSelectorHighlighter: PropTypes.func.isRequired,
rule: PropTypes.shape(Types.rule).isRequired, rule: PropTypes.shape(Types.rule).isRequired,
}; };
} }
@ -25,6 +27,7 @@ class Rule extends PureComponent {
render() { render() {
const { const {
onToggleDeclaration, onToggleDeclaration,
onToggleSelectorHighlighter,
rule, rule,
} = this.props; } = this.props;
const { const {
@ -44,6 +47,13 @@ class Rule extends PureComponent {
selector, selector,
type, type,
}), }),
type !== CSSRule.KEYFRAME_RULE ?
SelectorHighlighter({
onToggleSelectorHighlighter,
selector,
})
:
null,
dom.span({ className: "ruleview-ruleopen" }, " {") dom.span({ className: "ruleview-ruleopen" }, " {")
), ),
Declarations({ Declarations({

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

@ -15,6 +15,7 @@ class Rules extends PureComponent {
static get propTypes() { static get propTypes() {
return { return {
onToggleDeclaration: PropTypes.func.isRequired, onToggleDeclaration: PropTypes.func.isRequired,
onToggleSelectorHighlighter: PropTypes.func.isRequired,
rules: PropTypes.arrayOf(PropTypes.shape(Types.rule)).isRequired, rules: PropTypes.arrayOf(PropTypes.shape(Types.rule)).isRequired,
}; };
} }
@ -22,6 +23,7 @@ class Rules extends PureComponent {
render() { render() {
const { const {
onToggleDeclaration, onToggleDeclaration,
onToggleSelectorHighlighter,
rules, rules,
} = this.props; } = this.props;
@ -29,6 +31,7 @@ class Rules extends PureComponent {
return Rule({ return Rule({
key: rule.id, key: rule.id,
onToggleDeclaration, onToggleDeclaration,
onToggleSelectorHighlighter,
rule, rule,
}); });
}); });

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

@ -30,6 +30,7 @@ class RulesApp extends PureComponent {
return { return {
onToggleDeclaration: PropTypes.func.isRequired, onToggleDeclaration: PropTypes.func.isRequired,
onTogglePseudoClass: PropTypes.func.isRequired, onTogglePseudoClass: PropTypes.func.isRequired,
onToggleSelectorHighlighter: PropTypes.func.isRequired,
rules: PropTypes.arrayOf(PropTypes.shape(Types.rule)).isRequired, rules: PropTypes.arrayOf(PropTypes.shape(Types.rule)).isRequired,
}; };
} }
@ -53,6 +54,7 @@ class RulesApp extends PureComponent {
output.push(Rule({ output.push(Rule({
onToggleDeclaration: this.props.onToggleDeclaration, onToggleDeclaration: this.props.onToggleDeclaration,
onToggleSelectorHighlighter: this.props.onToggleSelectorHighlighter,
rule, rule,
})); }));
} }
@ -80,6 +82,7 @@ class RulesApp extends PureComponent {
component: Rules, component: Rules,
componentProps: { componentProps: {
onToggleDeclaration: this.props.onToggleDeclaration, onToggleDeclaration: this.props.onToggleDeclaration,
onToggleSelectorHighlighter: this.props.onToggleSelectorHighlighter,
rules: rules.filter(r => r.keyframesRule.id === lastKeyframes), rules: rules.filter(r => r.keyframesRule.id === lastKeyframes),
}, },
header: rule.keyframesRule.keyframesName, header: rule.keyframesRule.keyframesName,
@ -100,6 +103,7 @@ class RulesApp extends PureComponent {
return Rules({ return Rules({
onToggleDeclaration: this.props.onToggleDeclaration, onToggleDeclaration: this.props.onToggleDeclaration,
onToggleSelectorHighlighter: this.props.onToggleSelectorHighlighter,
rules, rules,
}); });
} }
@ -114,6 +118,7 @@ class RulesApp extends PureComponent {
component: Rules, component: Rules,
componentProps: { componentProps: {
onToggleDeclaration: this.props.onToggleDeclaration, onToggleDeclaration: this.props.onToggleDeclaration,
onToggleSelectorHighlighter: this.props.onToggleSelectorHighlighter,
rules, rules,
}, },
header: getStr("rule.pseudoElement"), header: getStr("rule.pseudoElement"),

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

@ -0,0 +1,84 @@
/* 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 { PureComponent } = require("devtools/client/shared/vendor/react");
const dom = require("devtools/client/shared/vendor/react-dom-factories");
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const { connect } = require("devtools/client/shared/vendor/react-redux");
const { getStr } = require("../utils/l10n");
const Types = require("../types");
class SelectorHighlighter extends PureComponent {
static get propTypes() {
return {
highlightedSelector: PropTypes.string.isRequired,
onToggleSelectorHighlighter: PropTypes.func.isRequired,
selector: PropTypes.shape(Types.selector).isRequired,
};
}
constructor(props) {
super(props);
this.state = {
// A unique selector to the current Rule. This is checked against the value
// of any existing highlighted selector in order mark the toggled state of
// the component.
uniqueSelector: "",
};
this.onToggleHighlighterClick = this.onToggleHighlighterClick.bind(this);
}
componentDidMount() {
// Only fetch the unique selector if a selector is highlighted.
if (this.props.highlightedSelector) {
this.updateState();
}
}
async onToggleHighlighterClick(event) {
event.stopPropagation();
const {
onToggleSelectorHighlighter,
selector,
} = this.props;
const uniqueSelector = await selector.getUniqueSelector();
this.setState({ uniqueSelector });
onToggleSelectorHighlighter(uniqueSelector);
}
async updateState() {
const uniqueSelector = await this.props.selector.getUniqueSelector();
this.setState({ uniqueSelector });
}
render() {
const { highlightedSelector } = this.props;
const { uniqueSelector } = this.state;
return (
dom.span({
className: "ruleview-selectorhighlighter" +
(highlightedSelector && highlightedSelector === uniqueSelector ?
" highlighted" : ""),
onClick: this.onToggleHighlighterClick,
title: getStr("rule.selectorHighlighter.tooltip"),
})
);
}
}
const mapStateToProps = state => {
return {
highlightedSelector: state.rules.highlightedSelector,
};
};
module.exports = connect(mapStateToProps)(SelectorHighlighter);

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

@ -12,6 +12,7 @@ DevToolsModules(
'RulesApp.js', 'RulesApp.js',
'SearchBox.js', 'SearchBox.js',
'Selector.js', 'Selector.js',
'SelectorHighlighter.js',
'SourceLink.js', 'SourceLink.js',
'Toolbar.js', 'Toolbar.js',
) )

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

@ -57,6 +57,8 @@ function Rule(elementStyle, options) {
// value, and add in any disabled properties from the store. // value, and add in any disabled properties from the store.
this.textProps = this._getTextProperties(); this.textProps = this._getTextProperties();
this.textProps = this.textProps.concat(this._getDisabledProperties()); this.textProps = this.textProps.concat(this._getDisabledProperties());
this.getUniqueSelector = this.getUniqueSelector.bind(this);
} }
Rule.prototype = { Rule.prototype = {
@ -79,6 +81,7 @@ Rule.prototype = {
get selector() { get selector() {
return { return {
getUniqueSelector: this.getUniqueSelector,
matchedSelectors: this.matchedSelectors, matchedSelectors: this.matchedSelectors,
selectors: this.domRule.selectors, selectors: this.domRule.selectors,
selectorText: this.keyframes ? this.domRule.keyText : this.selectorText, selectorText: this.keyframes ? this.domRule.keyText : this.selectorText,
@ -180,6 +183,27 @@ Rule.prototype = {
return this.textProps.find(textProp => textProp.id === id); return this.textProps.find(textProp => textProp.id === id);
}, },
/**
* Returns an unique selector for the CSS rule.
*/
async getUniqueSelector() {
let selector = "";
if (this.domRule.selectors) {
// This is a style rule with a selector.
selector = this.domRule.selectors.join(", ");
} else if (this.inherited) {
// This is an inline style from an inherited rule. Need to resolve the unique
// selector from the node which rule this is inherited from.
selector = await this.inherited.getUniqueSelector();
} else {
// This is an inline style from the current node.
selector = this.elementStyle.ruleView.inspector.selectionCssSelector;
}
return selector;
},
/** /**
* Returns true if the rule matches the creation options * Returns true if the rule matches the creation options
* specified. * specified.

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

@ -8,13 +8,17 @@ const Services = require("Services");
const ElementStyle = require("devtools/client/inspector/rules/models/element-style"); const ElementStyle = require("devtools/client/inspector/rules/models/element-style");
const { createFactory, createElement } = require("devtools/client/shared/vendor/react"); const { createFactory, createElement } = require("devtools/client/shared/vendor/react");
const { Provider } = require("devtools/client/shared/vendor/react-redux"); const { Provider } = require("devtools/client/shared/vendor/react-redux");
const EventEmitter = require("devtools/shared/event-emitter");
const { const {
disableAllPseudoClasses, disableAllPseudoClasses,
setPseudoClassLocks, setPseudoClassLocks,
togglePseudoClass, togglePseudoClass,
} = require("./actions/pseudo-classes"); } = require("./actions/pseudo-classes");
const { updateRules } = require("./actions/rules"); const {
updateHighlightedSelector,
updateRules,
} = require("./actions/rules");
const RulesApp = createFactory(require("./components/RulesApp")); const RulesApp = createFactory(require("./components/RulesApp"));
@ -40,6 +44,7 @@ class RulesView {
this.onSelection = this.onSelection.bind(this); this.onSelection = this.onSelection.bind(this);
this.onToggleDeclaration = this.onToggleDeclaration.bind(this); this.onToggleDeclaration = this.onToggleDeclaration.bind(this);
this.onTogglePseudoClass = this.onTogglePseudoClass.bind(this); this.onTogglePseudoClass = this.onTogglePseudoClass.bind(this);
this.onToggleSelectorHighlighter = this.onToggleSelectorHighlighter.bind(this);
this.updateRules = this.updateRules.bind(this); this.updateRules = this.updateRules.bind(this);
this.inspector.sidebar.on("select", this.onSelection); this.inspector.sidebar.on("select", this.onSelection);
@ -47,6 +52,8 @@ class RulesView {
this.selection.on("new-node-front", this.onSelection); this.selection.on("new-node-front", this.onSelection);
this.init(); this.init();
EventEmitter.decorate(this);
} }
init() { init() {
@ -57,6 +64,7 @@ class RulesView {
const rulesApp = RulesApp({ const rulesApp = RulesApp({
onToggleDeclaration: this.onToggleDeclaration, onToggleDeclaration: this.onToggleDeclaration,
onTogglePseudoClass: this.onTogglePseudoClass, onTogglePseudoClass: this.onTogglePseudoClass,
onToggleSelectorHighlighter: this.onToggleSelectorHighlighter,
}); });
const provider = createElement(Provider, { const provider = createElement(Provider, {
@ -75,6 +83,11 @@ class RulesView {
this.selection.off("detached-front", this.onSelection); this.selection.off("detached-front", this.onSelection);
this.selection.off("new-node-front", this.onSelection); this.selection.off("new-node-front", this.onSelection);
if (this._selectHighlighter) {
this._selectorHighlighter.finalize();
this._selectorHighlighter = null;
}
if (this.elementStyle) { if (this.elementStyle) {
this.elementStyle.destroy(); this.elementStyle.destroy();
} }
@ -109,6 +122,41 @@ class RulesView {
return this._dummyElement; return this._dummyElement;
} }
/**
* Get the highlighters overlay from the Inspector.
*
* @return {HighlighterOverlay}.
*/
get highlighters() {
return this.inspector.highlighters;
}
/**
* Get an instance of SelectorHighlighter (used to highlight nodes that match
* selectors in the rule-view).
*
* @return {Promise} resolves to the instance of the highlighter.
*/
async getSelectorHighlighter() {
if (!this.inspector) {
return null;
}
if (this._selectorHighlighter) {
return this._selectorHighlighter;
}
try {
const front = this.inspector.inspector;
this._selectorHighlighter = await front.getHighlighterByType("SelectorHighlighter");
return this._selectorHighlighter;
} catch (e) {
// The SelectorHighlighter type could not be created in the
// current target. It could be an older server, or a XUL page.
return null;
}
}
/** /**
* Returns true if the rules panel is visible, and false otherwise. * Returns true if the rules panel is visible, and false otherwise.
*/ */
@ -164,6 +212,45 @@ class RulesView {
this.inspector.togglePseudoClass(value); this.inspector.togglePseudoClass(value);
} }
/**
* Handler for toggling the selector highlighter for the given selector.
* Highlight/unhighlight all the nodes that match a given set of selectors inside the
* document of the current selected node. Only one selector can be highlighted at a
* time, so calling the method a second time with a different selector will first
* unhighlight the previously highlighted nodes. Calling the method a second time with
* the same select will unhighlight the highlighted nodes.
*
* @param {String} selector
* The selector used to find nodes in the page.
*/
async onToggleSelectorHighlighter(selector) {
const highlighter = await this.getSelectorHighlighter();
if (!highlighter) {
return;
}
await highlighter.hide();
if (selector !== this.highlighters.selectorHighlighterShown) {
this.store.dispatch(updateHighlightedSelector(selector));
await highlighter.show(this.selection.nodeFront, {
hideInfoBar: true,
hideGuides: true,
selector,
});
this.highlighters.selectorHighlighterShown = selector;
// This event is emitted for testing purposes.
this.emit("ruleview-selectorhighlighter-toggled", true);
} else {
this.highlighters.selectorHighlighterShown = null;
this.store.dispatch(updateHighlightedSelector(""));
// This event is emitted for testing purposes.
this.emit("ruleview-selectorhighlighter-toggled", false);
}
}
/** /**
* Updates the rules view by dispatching the new rules data of the newly selected * Updates the rules view by dispatching the new rules data of the newly selected
* element. This is called when the rules view becomes visible or upon new node * element. This is called when the rules view becomes visible or upon new node

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

@ -6,9 +6,12 @@
const { const {
UPDATE_RULES, UPDATE_RULES,
UPDATE_HIGHLIGHTED_SELECTOR,
} = require("../actions/index"); } = require("../actions/index");
const INITIAL_RULES = { const INITIAL_RULES = {
// The selector of the node that is highlighted by the selector highlighter.
highlightedSelector: "",
// Array of CSS rules. // Array of CSS rules.
rules: [], rules: [],
}; };
@ -81,9 +84,17 @@ function getRuleState(rule) {
const reducers = { const reducers = {
[UPDATE_RULES](_, { rules }) { [UPDATE_HIGHLIGHTED_SELECTOR](rules, { highlightedSelector }) {
return { return {
rules: rules.map(rule => getRuleState(rule)), ...rules,
highlightedSelector,
};
},
[UPDATE_RULES](rules, { rules: newRules }) {
return {
highlightedSelector: rules.highlightedSelector,
rules: newRules.map(rule => getRuleState(rule)),
}; };
}, },

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

@ -85,6 +85,8 @@ exports.pseudoClasses = {
* A CSS selector. * A CSS selector.
*/ */
const selector = exports.selector = { const selector = exports.selector = {
// Function that returns a Promise containing an unique CSS selector.
getUniqueSelector: PropTypes.func,
// Array of the selectors that match the selected element. // Array of the selectors that match the selected element.
matchedSelectors: PropTypes.arrayOf(PropTypes.string), matchedSelectors: PropTypes.arrayOf(PropTypes.string),
// The CSS rule's selector text content. // The CSS rule's selector text content.