Bug 1499049 - (Part 3) Add new reducer logic for tracking CSS changes to nested rules; r=pbro

Depends on D8719

- Add methods to generate unique identifiers for stylesheets and CSS rules changed within those stylesheets. These are used as IDs in the Redux store;
- Add logic to generate entries in the store for each one of the rule's ancestors and assign parent/child dependencies. This single-level structure for all rules in a source helps with quickly identifying a rule on subsequent changes independent of its rule tree (it avoids needless tree traversal). The parent/child references help with rendering of the nested rule structure in the Changes panel;
- Deep clone Redux store state before aggregating tracked changes (no more mutations of previous state).

Differential Revision: https://phabricator.services.mozilla.com/D8720

--HG--
rename : devtools/client/inspector/changes/moz.build => devtools/client/inspector/changes/utils/moz.build
extra : moz-landing-system : lando
This commit is contained in:
Razvan Caliman 2018-10-25 16:20:00 +00:00
Родитель 4c3717a22f
Коммит da9c192a4c
5 изменённых файлов: 237 добавлений и 3 удалений

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

@ -17,10 +17,10 @@ module.exports = {
};
},
trackChange(data) {
trackChange(change) {
return {
type: TRACK_CHANGE,
data,
change,
};
},

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

@ -8,6 +8,7 @@ DIRS += [
'actions',
'components',
'reducers',
'utils',
]
DevToolsModules(

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

@ -4,16 +4,182 @@
"use strict";
const { getSourceHash, getRuleHash } = require("../utils/changes-utils");
const {
RESET_CHANGES,
TRACK_CHANGE,
} = require("../actions/index");
/**
* Return a deep clone of the given state object.
*
* @param {Object} state
* @return {Object}
*/
function cloneState(state = {}) {
return Object.entries(state).reduce((sources, [sourceId, source]) => {
sources[sourceId] = {
...source,
rules: Object.entries(source.rules).reduce((rules, [ruleId, rule]) => {
rules[ruleId] = {
...rule,
children: rule.children.slice(0),
add: { ...rule.add },
remove: { ...rule.remove },
};
return rules;
}, {}),
};
return sources;
}, {});
}
/**
* Given information about a CSS rule and its ancestor rules (@media, @supports, etc),
* create entries in the given rules collection for each rule and assign parent/child
* dependencies.
*
* @param {Object} ruleData
* Information about a CSS rule:
* {
* selector: {String}
* CSS selector text
* ancestors: {Array}
* Flattened CSS rule tree of the rule's ancestors with the root rule
* at the beginning of the array and the leaf rule at the end.
* ruleIndex: {Array}
* Indexes of each ancestor rule within its parent rule.
* }
*
* @param {Object} rules
* Collection of rules to be mutated.
* This is a reference to the corresponding `rules` object from the state.
*
* @return {Object}
* Entry for the CSS rule created the given collection of rules.
*/
function createRule(ruleData, rules) {
// Append the rule data to the flattened CSS rule tree with its ancestors.
const ruleAncestry = [...ruleData.ancestors, { ...ruleData }];
return ruleAncestry
// First, generate a unique identifier for each rule.
.map((rule, index) => {
// Ensure each rule has ancestors excluding itself (expand the flattened rule tree).
rule.ancestors = ruleAncestry.slice(0, index);
// Ensure each rule has a selector text.
// For the purpose of displaying in the UI, we treat at-rules as selectors.
if (!rule.selector) {
rule.selector =
`${rule.typeName} ${(rule.conditionText || rule.name || rule.keyText)}`;
}
return getRuleHash(rule);
})
// Then, create new entries in the rules collection and assign dependencies.
.map((ruleId, index, array) => {
const { selector } = ruleAncestry[index];
const prevRuleId = array[index - 1];
const nextRuleId = array[index + 1];
// Copy or create an entry for this rule.
rules[ruleId] = Object.assign({}, { selector, children: [] }, rules[ruleId]);
// The next ruleId is lower in the rule tree, therefore it's a child of this rule.
if (nextRuleId && !rules[ruleId].children.includes(nextRuleId)) {
rules[ruleId].children.push(nextRuleId);
}
// The previous ruleId is higher in the rule tree, therefore it's the parent.
if (prevRuleId) {
rules[ruleId].parent = prevRuleId;
}
return rules[ruleId];
})
// Finally, return the last rule in the array which is the rule we set out to create.
.pop();
}
/**
* Aggregated changes grouped by sources (stylesheet/element), which contain rules,
* which contain collections of added and removed CSS declarations.
*
* Structure:
* <sourceId>: {
* type: // "stylesheet" or "element"
* href: // Stylesheet or document URL
* rules: {
* <ruleId>: {
* selector: "" // String CSS selector or CSS at-rule text
* children: [] // Array of <ruleId> for child rules of this rule.
* parent: // <ruleId> of the parent rule
* add: {
* <property>: <value> // CSS declaration
* ...
* },
* remove: {
* <property>: <value> // CSS declaration
* ...
* }
* }
* ... // more rules
* }
* }
* ... // more sources
*/
const INITIAL_STATE = {};
const reducers = {
[TRACK_CHANGE](state, { data }) {
[TRACK_CHANGE](state, { change }) {
const defaults = {
selector: null,
source: {},
ancestors: [],
add: {},
remove: {},
};
change = { ...defaults, ...change };
state = cloneState(state);
const { type, href, index } = change.source;
const { selector, ancestors, ruleIndex } = change;
const sourceId = getSourceHash(change.source);
const ruleId = getRuleHash({ selector, ancestors, ruleIndex });
// Copy or create object identifying the source (styelsheet/element) for this change.
const source = Object.assign({}, state[sourceId], { type, href, index });
// Copy or create collection of all rules ever changed in this source.
const rules = Object.assign({}, source.rules);
// Refrence or create object identifying the rule for this change.
let rule = rules[ruleId];
if (!rule) {
rule = createRule({ selector, ancestors, ruleIndex }, rules);
}
// Copy or create collection of all CSS declarations ever added to this rule.
const add = Object.assign({}, rule.add);
// Copy or create collection of all CSS declarations ever removed from this rule.
const remove = Object.assign({}, rule.remove);
// Track the remove operation only if the property was not previously introduced by
// an add operation. This ensures repeated changes of the same property register as
// a single remove operation of its original value.
if (change.remove && change.remove.property && !add[change.remove.property]) {
remove[change.remove.property] = change.remove.value;
}
if (change.add && change.add.property) {
add[change.add.property] = change.add.value;
}
source.rules = { ...rules, [ruleId]: { ...rule, add, remove } };
state[sourceId] = source;
return state;
},

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

@ -0,0 +1,58 @@
/* 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";
/**
* Generate a hash that uniquely identifies a stylesheet or element style attribute.
*
* @param {Object} source
* Information about a stylesheet or element style attribute:
* {
* type: {String}
* One of "stylesheet" or "element".
* index: {Number|String}
* Position of the styleshet in the list of stylesheets in the document.
* If `type` is "element", `index` is the generated selector which
* uniquely identifies the element in the document.
* href: {String|null}
* URL of the stylesheet or of the document when `type` is "element".
* If the stylesheet is inline, `href` is null.
* }
* @return {String}
*/
function getSourceHash(source) {
const { type, index, href = "inline" } = source;
return `${type}${index}${href}`;
}
/**
* Generate a hash that uniquely identifies a CSS rule.
*
* @param {Object} ruleData
* Information about a CSS rule:
* {
* selector: {String}
* CSS selector text
* ancestors: {Array}
* Flattened CSS rule tree of the rule's ancestors with the root rule
* at the beginning of the array and the leaf rule at the end.
* ruleIndex: {Array}
* Indexes of each ancestor rule within its parent rule.
* }
* @return {String}
*/
function getRuleHash(ruleData) {
const { selector = "", ancestors = [], ruleIndex } = ruleData;
const atRules = ancestors.reduce((acc, rule) => {
acc += `${rule.typeName} ${(rule.conditionText || rule.name || rule.keyText)}`;
return acc;
}, "");
return `${atRules}${selector}${ruleIndex}`;
}
module.exports.getSourceHash = getSourceHash;
module.exports.getRuleHash = getRuleHash;

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

@ -0,0 +1,9 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# 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/.
DevToolsModules(
'changes-utils.js',
)