ChangesApp.js (7236B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const { 8 createFactory, 9 PureComponent, 10 } = require("resource://devtools/client/shared/vendor/react.mjs"); 11 const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); 12 const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.mjs"); 13 const { 14 connect, 15 } = require("resource://devtools/client/shared/vendor/react-redux.js"); 16 17 const CSSDeclaration = createFactory( 18 require("resource://devtools/client/inspector/changes/components/CSSDeclaration.js") 19 ); 20 const { 21 getChangesTree, 22 } = require("resource://devtools/client/inspector/changes/selectors/changes.js"); 23 const { 24 getSourceForDisplay, 25 } = require("resource://devtools/client/inspector/changes/utils/changes-utils.js"); 26 const { 27 getStr, 28 } = require("resource://devtools/client/inspector/changes/utils/l10n.js"); 29 30 class ChangesApp extends PureComponent { 31 static get propTypes() { 32 return { 33 // Nested CSS rule tree structure of CSS changes grouped by source (stylesheet) 34 changesTree: PropTypes.object.isRequired, 35 // Event handler for "contextmenu" event 36 onContextMenu: PropTypes.func.isRequired, 37 // Event handler for click on "Copy All Changes" button 38 onCopyAllChanges: PropTypes.func.isRequired, 39 // Event handler for click on "Copy Rule" button 40 onCopyRule: PropTypes.func.isRequired, 41 }; 42 } 43 44 constructor(props) { 45 super(props); 46 } 47 48 renderCopyAllChangesButton() { 49 const button = dom.button( 50 { 51 className: "changes__copy-all-changes-button", 52 onClick: e => { 53 e.stopPropagation(); 54 this.props.onCopyAllChanges(); 55 }, 56 title: getStr("changes.contextmenu.copyAllChangesDescription"), 57 }, 58 getStr("changes.contextmenu.copyAllChanges") 59 ); 60 61 return dom.div({ className: "changes__toolbar" }, button); 62 } 63 64 renderCopyButton(ruleId) { 65 return dom.button( 66 { 67 className: "changes__copy-rule-button", 68 onClick: e => { 69 e.stopPropagation(); 70 this.props.onCopyRule(ruleId); 71 }, 72 title: getStr("changes.contextmenu.copyRuleDescription"), 73 }, 74 getStr("changes.contextmenu.copyRule") 75 ); 76 } 77 78 renderDeclarations(remove = [], add = []) { 79 const removals = remove 80 // Sorting changed declarations in the order they appear in the Rules view. 81 .sort((a, b) => a.index > b.index) 82 .map(({ property, value, index }) => { 83 return CSSDeclaration({ 84 key: "remove-" + property + index, 85 className: "level diff-remove", 86 property, 87 value, 88 }); 89 }); 90 91 const additions = add 92 // Sorting changed declarations in the order they appear in the Rules view. 93 .sort((a, b) => a.index > b.index) 94 .map(({ property, value, index }) => { 95 return CSSDeclaration({ 96 key: "add-" + property + index, 97 className: "level diff-add", 98 property, 99 value, 100 }); 101 }); 102 103 return [removals, additions]; 104 } 105 106 renderRule(ruleId, rule, level = 0) { 107 const diffClass = rule.isNew ? "diff-add" : ""; 108 return dom.div( 109 { 110 key: ruleId, 111 className: "changes__rule devtools-monospace", 112 "data-rule-id": ruleId, 113 style: { 114 "--diff-level": level, 115 }, 116 }, 117 this.renderSelectors(rule.selectors, rule.isNew), 118 this.renderCopyButton(ruleId), 119 // Render any nested child rules if they exist. 120 rule.children.map(childRule => { 121 return this.renderRule(childRule.ruleId, childRule, level + 1); 122 }), 123 // Render any changed CSS declarations. 124 this.renderDeclarations(rule.remove, rule.add), 125 // Render the closing bracket with a diff marker if necessary. 126 dom.div({ className: `level ${diffClass}` }, "}") 127 ); 128 } 129 130 /** 131 * Return an array of React elements for the rule's selector. 132 * 133 * @param {Array} selectors 134 * List of strings as versions of this rule's selector over time. 135 * @param {boolean} isNewRule 136 * Whether the rule was created at runtime. 137 * @return {Array} 138 */ 139 renderSelectors(selectors, isNewRule) { 140 const selectorDiffClassMap = new Map(); 141 142 // The selectors array has just one item if it hasn't changed. Render it as-is. 143 // If the rule was created at runtime, mark the single selector as added. 144 // If it has two or more items, the first item was the original selector (mark as 145 // removed) and the last item is the current selector (mark as added). 146 if (selectors.length === 1) { 147 selectorDiffClassMap.set(selectors[0], isNewRule ? "diff-add" : ""); 148 } else if (selectors.length >= 2) { 149 selectorDiffClassMap.set(selectors[0], "diff-remove"); 150 selectorDiffClassMap.set(selectors[selectors.length - 1], "diff-add"); 151 } 152 153 const elements = []; 154 155 for (const [selector, diffClass] of selectorDiffClassMap) { 156 elements.push( 157 dom.div( 158 { 159 key: selector, 160 className: `level changes__selector ${diffClass}`, 161 title: selector, 162 }, 163 selector, 164 dom.span({}, " {") 165 ) 166 ); 167 } 168 169 return elements; 170 } 171 172 renderDiff(changes = {}) { 173 // Render groups of style sources: stylesheets and element style attributes. 174 return Object.entries(changes).map(([sourceId, source]) => { 175 const path = getSourceForDisplay(source); 176 const { href, rules, isFramed } = source; 177 178 return dom.div( 179 { 180 key: sourceId, 181 "data-source-id": sourceId, 182 className: "source", 183 }, 184 dom.div( 185 { 186 className: "href", 187 title: href, 188 }, 189 dom.span({}, path), 190 isFramed && this.renderFrameBadge(href) 191 ), 192 // Render changed rules within this source. 193 Object.entries(rules).map(([ruleId, rule]) => { 194 return this.renderRule(ruleId, rule); 195 }) 196 ); 197 }); 198 } 199 200 renderFrameBadge(href = "") { 201 return dom.span( 202 { 203 className: "inspector-badge", 204 title: href, 205 }, 206 getStr("changes.iframeLabel") 207 ); 208 } 209 210 renderEmptyState() { 211 return dom.div( 212 { className: "devtools-sidepanel-no-result" }, 213 dom.p({}, getStr("changes.noChanges")), 214 dom.p({}, getStr("changes.noChangesDescription")) 215 ); 216 } 217 218 render() { 219 const hasChanges = !!Object.keys(this.props.changesTree).length; 220 return dom.div( 221 { 222 className: "theme-sidebar inspector-tabpanel", 223 id: "sidebar-panel-changes", 224 role: "document", 225 tabIndex: "0", 226 onContextMenu: this.props.onContextMenu, 227 }, 228 !hasChanges && this.renderEmptyState(), 229 hasChanges && this.renderCopyAllChangesButton(), 230 hasChanges && this.renderDiff(this.props.changesTree) 231 ); 232 } 233 } 234 235 const mapStateToProps = state => { 236 return { 237 changesTree: getChangesTree(state.changes), 238 }; 239 }; 240 241 module.exports = connect(mapStateToProps)(ChangesApp);