diff options
Diffstat (limited to 'devtools/client/inspector/changes/components')
3 files changed, 298 insertions, 0 deletions
diff --git a/devtools/client/inspector/changes/components/CSSDeclaration.js b/devtools/client/inspector/changes/components/CSSDeclaration.js new file mode 100644 index 0000000000..c25bf6833f --- /dev/null +++ b/devtools/client/inspector/changes/components/CSSDeclaration.js @@ -0,0 +1,47 @@ +/* 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("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); + +class CSSDeclaration extends PureComponent { + static get propTypes() { + return { + className: PropTypes.string, + property: PropTypes.string.isRequired, + value: PropTypes.string.isRequired, + }; + } + + static get defaultProps() { + return { + className: "", + }; + } + + render() { + const { className, property, value } = this.props; + + return dom.div( + { className: `changes__declaration ${className}` }, + dom.span( + { className: "changes__declaration-name theme-fg-color3" }, + property + ), + ": ", + dom.span( + { className: "changes__declaration-value theme-fg-color1" }, + value + ), + ";" + ); + } +} + +module.exports = CSSDeclaration; diff --git a/devtools/client/inspector/changes/components/ChangesApp.js b/devtools/client/inspector/changes/components/ChangesApp.js new file mode 100644 index 0000000000..9953109e19 --- /dev/null +++ b/devtools/client/inspector/changes/components/ChangesApp.js @@ -0,0 +1,241 @@ +/* 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 { + createFactory, + PureComponent, +} = require("resource://devtools/client/shared/vendor/react.js"); +const dom = require("resource://devtools/client/shared/vendor/react-dom-factories.js"); +const PropTypes = require("resource://devtools/client/shared/vendor/react-prop-types.js"); +const { + connect, +} = require("resource://devtools/client/shared/vendor/react-redux.js"); + +const CSSDeclaration = createFactory( + require("resource://devtools/client/inspector/changes/components/CSSDeclaration.js") +); +const { + getChangesTree, +} = require("resource://devtools/client/inspector/changes/selectors/changes.js"); +const { + getSourceForDisplay, +} = require("resource://devtools/client/inspector/changes/utils/changes-utils.js"); +const { + getStr, +} = require("resource://devtools/client/inspector/changes/utils/l10n.js"); + +class ChangesApp extends PureComponent { + static get propTypes() { + return { + // Nested CSS rule tree structure of CSS changes grouped by source (stylesheet) + changesTree: PropTypes.object.isRequired, + // Event handler for "contextmenu" event + onContextMenu: PropTypes.func.isRequired, + // Event handler for click on "Copy All Changes" button + onCopyAllChanges: PropTypes.func.isRequired, + // Event handler for click on "Copy Rule" button + onCopyRule: PropTypes.func.isRequired, + }; + } + + constructor(props) { + super(props); + } + + renderCopyAllChangesButton() { + const button = dom.button( + { + className: "changes__copy-all-changes-button", + onClick: e => { + e.stopPropagation(); + this.props.onCopyAllChanges(); + }, + title: getStr("changes.contextmenu.copyAllChangesDescription"), + }, + getStr("changes.contextmenu.copyAllChanges") + ); + + return dom.div({ className: "changes__toolbar" }, button); + } + + renderCopyButton(ruleId) { + return dom.button( + { + className: "changes__copy-rule-button", + onClick: e => { + e.stopPropagation(); + this.props.onCopyRule(ruleId); + }, + title: getStr("changes.contextmenu.copyRuleDescription"), + }, + getStr("changes.contextmenu.copyRule") + ); + } + + renderDeclarations(remove = [], add = []) { + const removals = remove + // Sorting changed declarations in the order they appear in the Rules view. + .sort((a, b) => a.index > b.index) + .map(({ property, value, index }) => { + return CSSDeclaration({ + key: "remove-" + property + index, + className: "level diff-remove", + property, + value, + }); + }); + + const additions = add + // Sorting changed declarations in the order they appear in the Rules view. + .sort((a, b) => a.index > b.index) + .map(({ property, value, index }) => { + return CSSDeclaration({ + key: "add-" + property + index, + className: "level diff-add", + property, + value, + }); + }); + + return [removals, additions]; + } + + renderRule(ruleId, rule, level = 0) { + const diffClass = rule.isNew ? "diff-add" : ""; + return dom.div( + { + key: ruleId, + className: "changes__rule devtools-monospace", + "data-rule-id": ruleId, + style: { + "--diff-level": level, + }, + }, + this.renderSelectors(rule.selectors, rule.isNew), + this.renderCopyButton(ruleId), + // Render any nested child rules if they exist. + rule.children.map(childRule => { + return this.renderRule(childRule.ruleId, childRule, level + 1); + }), + // Render any changed CSS declarations. + this.renderDeclarations(rule.remove, rule.add), + // Render the closing bracket with a diff marker if necessary. + dom.div({ className: `level ${diffClass}` }, "}") + ); + } + + /** + * Return an array of React elements for the rule's selector. + * + * @param {Array} selectors + * List of strings as versions of this rule's selector over time. + * @param {Boolean} isNewRule + * Whether the rule was created at runtime. + * @return {Array} + */ + renderSelectors(selectors, isNewRule) { + const selectorDiffClassMap = new Map(); + + // The selectors array has just one item if it hasn't changed. Render it as-is. + // If the rule was created at runtime, mark the single selector as added. + // If it has two or more items, the first item was the original selector (mark as + // removed) and the last item is the current selector (mark as added). + if (selectors.length === 1) { + selectorDiffClassMap.set(selectors[0], isNewRule ? "diff-add" : ""); + } else if (selectors.length >= 2) { + selectorDiffClassMap.set(selectors[0], "diff-remove"); + selectorDiffClassMap.set(selectors[selectors.length - 1], "diff-add"); + } + + const elements = []; + + for (const [selector, diffClass] of selectorDiffClassMap) { + elements.push( + dom.div( + { + key: selector, + className: `level changes__selector ${diffClass}`, + title: selector, + }, + selector, + dom.span({}, " {") + ) + ); + } + + return elements; + } + + renderDiff(changes = {}) { + // Render groups of style sources: stylesheets and element style attributes. + return Object.entries(changes).map(([sourceId, source]) => { + const path = getSourceForDisplay(source); + const { href, rules, isFramed } = source; + + return dom.div( + { + key: sourceId, + "data-source-id": sourceId, + className: "source", + }, + dom.div( + { + className: "href", + title: href, + }, + dom.span({}, path), + isFramed && this.renderFrameBadge(href) + ), + // Render changed rules within this source. + Object.entries(rules).map(([ruleId, rule]) => { + return this.renderRule(ruleId, rule); + }) + ); + }); + } + + renderFrameBadge(href = "") { + return dom.span( + { + className: "inspector-badge", + title: href, + }, + getStr("changes.iframeLabel") + ); + } + + renderEmptyState() { + return dom.div( + { className: "devtools-sidepanel-no-result" }, + dom.p({}, getStr("changes.noChanges")), + dom.p({}, getStr("changes.noChangesDescription")) + ); + } + + render() { + const hasChanges = !!Object.keys(this.props.changesTree).length; + return dom.div( + { + className: "theme-sidebar inspector-tabpanel", + id: "sidebar-panel-changes", + role: "document", + tabIndex: "0", + onContextMenu: this.props.onContextMenu, + }, + !hasChanges && this.renderEmptyState(), + hasChanges && this.renderCopyAllChangesButton(), + hasChanges && this.renderDiff(this.props.changesTree) + ); + } +} + +const mapStateToProps = state => { + return { + changesTree: getChangesTree(state.changes), + }; +}; + +module.exports = connect(mapStateToProps)(ChangesApp); diff --git a/devtools/client/inspector/changes/components/moz.build b/devtools/client/inspector/changes/components/moz.build new file mode 100644 index 0000000000..e8fba36fb8 --- /dev/null +++ b/devtools/client/inspector/changes/components/moz.build @@ -0,0 +1,10 @@ +# -*- 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( + "ChangesApp.js", + "CSSDeclaration.js", +) |