Bug 1478448 - (Part 2) Add Changes panel, Redux store config, React components and styles for rendering tracked changes. r=gl
MozReview-Commit-ID: 92tu74KMEhP
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/actions/changes.js
@@ -0,0 +1,27 @@
+/* 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 {
+ RESET_CHANGES,
+ TRACK_CHANGE,
+} = require("./index");
+
+module.exports = {
+
+ resetChanges() {
+ return {
+ type: RESET_CHANGES,
+ };
+ },
+
+ trackChange(data) {
+ return {
+ type: TRACK_CHANGE,
+ data,
+ };
+ },
+
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/actions/index.js
@@ -0,0 +1,17 @@
+/* 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 { createEnum } = require("devtools/client/shared/enum");
+
+createEnum([
+
+ // Remove all changes
+ "RESET_CHANGES",
+
+ // Track a style change
+ "TRACK_CHANGE",
+
+], module.exports);
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/actions/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(
+ 'changes.js',
+ 'index.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/changes.js
@@ -0,0 +1,63 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* 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, createElement } = require("devtools/client/shared/vendor/react");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+
+const ChangesApp = createFactory(require("./components/ChangesApp"));
+
+const {
+ resetChanges,
+} = require("./actions/changes");
+
+class ChangesView {
+ constructor(inspector) {
+ this.inspector = inspector;
+ this.store = this.inspector.store;
+
+ this.destroy = this.destroy.bind(this);
+
+ this.init();
+ }
+
+ init() {
+ const changesApp = ChangesApp({});
+
+ // Expose the provider to let inspector.js use it in setupSidebar.
+ this.provider = createElement(Provider, {
+ id: "changesview",
+ key: "changesview",
+ store: this.store,
+ }, changesApp);
+
+ // TODO: save store and restore/replay on refresh.
+ // Bug 1478439 - https://bugzilla.mozilla.org/show_bug.cgi?id=1478439
+ this.inspector.target.once("will-navigate", this.destroy);
+ }
+
+ /**
+ * Destruction function called when the inspector is destroyed.
+ */
+ destroy() {
+ this.store.dispatch(resetChanges());
+ this.inspector = null;
+ this.store = null;
+ }
+ /**
+ * Whether the Changes sidebar panel is selected in the Inspector.
+ *
+ * @return {Boolean}
+ */
+ isPanelVisible() {
+ return this.inspector &&
+ this.inspector.sidebar &&
+ this.inspector.sidebar.getCurrentTabID() === "changesview";
+ }
+}
+
+module.exports = ChangesView;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/components/ChangesApp.js
@@ -0,0 +1,75 @@
+/* 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");
+
+class ChangesApp extends PureComponent {
+ static get propTypes() {
+ return {
+ changes: PropTypes.object.isRequired
+ };
+ }
+
+ renderMutations(remove = {}, add = {}) {
+ const removals = Object.keys(remove).map(key => {
+ return dom.div(
+ { className: "line diff-remove"},
+ `${key}: ${remove[key]};`
+ );
+ });
+
+ const additions = Object.keys(add).map(key => {
+ return dom.div(
+ { className: "line diff-add"},
+ `${key}: ${add[key]};`
+ );
+ });
+
+ return [removals, additions];
+ }
+
+ renderSelectors(selectors = {}) {
+ return Object.keys(selectors).map(sel => {
+ return dom.details(
+ { className: "selector", open: true },
+ dom.summary({}, sel),
+ this.renderMutations(selectors[sel].remove, selectors[sel].add)
+ );
+ });
+ }
+
+ renderDiff(diff = {}) {
+ // Render groups of style sources: stylesheets, embedded styles and inline styles
+ return Object.keys(diff).map(href => {
+ return dom.details(
+ { className: "source", open: true },
+ dom.summary({}, href),
+ // Render groups of selectors
+ this.renderSelectors(diff[href])
+ );
+ });
+ }
+
+ render() {
+ return dom.div(
+ {
+ className: "theme-sidebar inspector-tabpanel",
+ id: "sidebar-panel-changes"
+ },
+ dom.div(
+ {
+ id: "diff-container"
+ },
+ this.renderDiff(this.props.changes.diff)
+ )
+ );
+ }
+}
+
+module.exports = connect(state => state)(ChangesApp);
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/components/moz.build
@@ -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(
+ 'ChangesApp.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/moz.build
@@ -0,0 +1,16 @@
+# -*- 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/.
+
+DIRS += [
+ 'actions',
+ 'components',
+ 'reducers',
+]
+
+DevToolsModules(
+ 'change-manager.js',
+ 'changes.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/reducers/changes.js
@@ -0,0 +1,98 @@
+/* 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 {
+ RESET_CHANGES,
+ TRACK_CHANGE,
+} = require("../actions/index");
+
+const INITIAL_STATE = {
+ /**
+ * Diff of changes groupped by stylesheet href, then by selector, then into add/remove
+ * objects with CSS property names and values for corresponding changes.
+ *
+ * Structure:
+ *
+ * diff = {
+ * "href": {
+ * "selector": {
+ * add: {
+ * "property": value
+ * ... // more properties
+ * },
+ * remove: {
+ * "property": value
+ * ...
+ * }
+ * },
+ * ... // more selectors
+ * }
+ * ... // more stylesheet hrefs
+ * }
+ */
+ diff: {},
+};
+
+/**
+ * Mutate the given diff object with data about a new change.
+ *
+ * @param {Object} d
+ * Diff object from the store.
+ * @param {Object} c
+ * Data about the change: which property was added or removed, on which selector,
+ * in which stylesheet or whether the source is an element's inline style.
+ * @return {Object}
+ * Mutated diff object.
+ */
+function updateDiff(d = {}, c = {}) {
+ // Ensure expected diff structure exists
+ d[c.href] = d[c.href] || {};
+ d[c.href][c.selector] = d[c.href][c.selector] || {};
+ d[c.href][c.selector].add = d[c.href][c.selector].add || {};
+ d[c.href][c.selector].remove = d[c.href][c.selector].remove || {};
+
+ // If the property to remove was previously added by a change, skip removing it.
+ // Doing so makes repeated changes of the same property coalesce into a single change
+ // when shwon in the UI.
+ if (c.remove && c.remove.property && !d[c.href][c.selector].add[c.remove.property]) {
+ d[c.href][c.selector].remove[c.remove.property] = c.remove.value;
+ }
+
+ // Set or overwrite any existing property value
+ if (c.add && c.add.property) {
+ d[c.href][c.selector].add[c.add.property] = c.add.value;
+ }
+
+ d[c.href][c.selector].tag = c.tag;
+
+ return d;
+}
+
+const reducers = {
+
+ [TRACK_CHANGE](state, { data }) {
+ // Shallow clone of state.
+ const newState = { ...state };
+ // Deep clone of previous diff object.
+ const newDiff = JSON.parse(JSON.stringify(state.diff));
+ // Update the diff with information about this change
+ newState.diff = updateDiff(newDiff, data);
+ return newState;
+ },
+
+ [RESET_CHANGES](state) {
+ return { ...INITIAL_STATE };
+ },
+
+};
+
+module.exports = function(state = INITIAL_STATE, action) {
+ const reducer = reducers[action.type];
+ if (!reducer) {
+ return state;
+ }
+ return reducer(state, action);
+};
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/changes/reducers/moz.build
@@ -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.js',
+)
--- a/devtools/client/inspector/index.xhtml
+++ b/devtools/client/inspector/index.xhtml
@@ -7,16 +7,17 @@
<html xmlns="http://www.w3.org/1999/xhtml" dir="">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="stylesheet" href="chrome://devtools/skin/breadcrumbs.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/inspector.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/rules.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/computed.css"/>
+ <link rel="stylesheet" href="chrome://devtools/skin/changes.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/fonts.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/boxmodel.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/layout.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/animation.css"/>
<link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/Tabs.css"/>
<link rel="stylesheet" href="resource://devtools/client/shared/components/tabs/TabBar.css"/>
<link rel="stylesheet" href="resource://devtools/client/shared/components/SidebarToggle.css"/>
<link rel="stylesheet" href="resource://devtools/client/inspector/components/InspectorTabPanel.css"/>
--- a/devtools/client/inspector/inspector.js
+++ b/devtools/client/inspector/inspector.js
@@ -61,16 +61,17 @@ const PORTRAIT_MODE_WIDTH_THRESHOLD = 70
const SIDE_PORTAIT_MODE_WIDTH_THRESHOLD = 1000;
const THREE_PANE_FIRST_RUN_PREF = "devtools.inspector.three-pane-first-run";
const SHOW_THREE_PANE_ONBOARDING_PREF = "devtools.inspector.show-three-pane-tooltip";
const THREE_PANE_ENABLED_PREF = "devtools.inspector.three-pane-enabled";
const THREE_PANE_ENABLED_SCALAR = "devtools.inspector.three_pane_enabled";
const THREE_PANE_CHROME_ENABLED_PREF = "devtools.inspector.chrome.three-pane-enabled";
const TELEMETRY_EYEDROPPER_OPENED = "devtools.toolbar.eyedropper.opened";
+const TRACK_CHANGES_ENABLED = "devtools.inspector.changes.enabled";
/**
* Represents an open instance of the Inspector for a tab.
* The inspector controls the breadcrumbs, the markup view, and the sidebar
* (computed view, rule view, font view and animation inspector).
*
* Events:
* - ready
@@ -976,16 +977,42 @@ Inspector.prototype = {
this.fontinspector = new FontInspector(this, this.panelWin);
}
return this.fontinspector.provider;
}
},
defaultTab == fontId);
+ if (Services.prefs.getBoolPref(TRACK_CHANGES_ENABLED)) {
+ // Inject a lazy loaded react tab by exposing a fake React object
+ // with a lazy defined Tab thanks to `panel` being a function
+ const changesId = "changesview";
+ const changesTitle = INSPECTOR_L10N.getStr("inspector.sidebar.changesViewTitle");
+ this.sidebar.queueTab(
+ changesId,
+ changesTitle,
+ {
+ props: {
+ id: changesId,
+ title: changesTitle
+ },
+ panel: () => {
+ if (!this.changesview) {
+ const ChangesView =
+ this.browserRequire("devtools/client/inspector/changes/changes");
+ this.changesview = new ChangesView(this, this.panelWin);
+ }
+
+ return this.changesview.provider;
+ }
+ },
+ defaultTab == changesId);
+ }
+
this.sidebar.addAllQueuedTabs();
// Persist splitter state in preferences.
this.sidebar.on("show", this.onSidebarShown);
this.sidebar.on("hide", this.onSidebarHidden);
this.sidebar.on("destroy", this.onSidebarHidden);
this.sidebar.show(defaultTab);
@@ -1414,16 +1441,20 @@ Inspector.prototype = {
panel.destroy();
}
this._panels.clear();
if (this.layoutview) {
this.layoutview.destroy();
}
+ if (this.changesview) {
+ this.changesview.destroy();
+ }
+
if (this.fontinspector) {
this.fontinspector.destroy();
}
if (this.animationinspector) {
this.animationinspector.destroy();
}
--- a/devtools/client/inspector/moz.build
+++ b/devtools/client/inspector/moz.build
@@ -1,16 +1,17 @@
# 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/.
DIRS += [
'animation',
'animation-old',
'boxmodel',
+ 'changes',
'components',
'computed',
'extensions',
'flexbox',
'fonts',
'grids',
'layout',
'markup',
--- a/devtools/client/inspector/reducers.js
+++ b/devtools/client/inspector/reducers.js
@@ -4,15 +4,16 @@
"use strict";
// This file exposes the Redux reducers of the box model, grid and grid highlighter
// settings.
exports.animations = require("devtools/client/inspector/animation/reducers/animations");
exports.boxModel = require("devtools/client/inspector/boxmodel/reducers/box-model");
+exports.changes = require("devtools/client/inspector/changes/reducers/changes");
exports.extensionsSidebar = require("devtools/client/inspector/extensions/reducers/sidebar");
exports.flexbox = require("devtools/client/inspector/flexbox/reducers/flexbox");
exports.fontOptions = require("devtools/client/inspector/fonts/reducers/font-options");
exports.fontData = require("devtools/client/inspector/fonts/reducers/fonts");
exports.fontEditor = require("devtools/client/inspector/fonts/reducers/font-editor");
exports.grids = require("devtools/client/inspector/grids/reducers/grids");
exports.highlighterSettings = require("devtools/client/inspector/grids/reducers/highlighter-settings");
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -226,16 +226,17 @@ devtools.jar:
skin/images/vview-lock@2x.png (themes/images/vview-lock@2x.png)
skin/images/vview-open-inspector.png (themes/images/vview-open-inspector.png)
skin/images/vview-open-inspector@2x.png (themes/images/vview-open-inspector@2x.png)
skin/images/sort-ascending-arrow.svg (themes/images/sort-ascending-arrow.svg)
skin/images/sort-descending-arrow.svg (themes/images/sort-descending-arrow.svg)
skin/images/cubic-bezier-swatch.png (themes/images/cubic-bezier-swatch.png)
skin/images/cubic-bezier-swatch@2x.png (themes/images/cubic-bezier-swatch@2x.png)
skin/fonts.css (themes/fonts.css)
+ skin/changes.css (themes/changes.css)
skin/computed.css (themes/computed.css)
skin/layout.css (themes/layout.css)
skin/images/arrow-e.png (themes/images/arrow-e.png)
skin/images/arrow-e@2x.png (themes/images/arrow-e@2x.png)
skin/images/search-clear-failed.svg (themes/images/search-clear-failed.svg)
skin/images/search-clear-light.svg (themes/images/search-clear-light.svg)
skin/images/search-clear-dark.svg (themes/images/search-clear-dark.svg)
skin/tooltip/arrow-horizontal-dark.png (themes/tooltip/arrow-horizontal-dark.png)
--- a/devtools/client/locales/en-US/inspector.properties
+++ b/devtools/client/locales/en-US/inspector.properties
@@ -383,16 +383,21 @@ markupView.edit.key=F2
# is visible
markupView.scrollInto.key=s
# LOCALIZATION NOTE (inspector.sidebar.fontInspectorTitle):
# This is the title shown in a tab in the side panel of the Inspector panel
# that corresponds to the tool displaying the list of fonts used in the page.
inspector.sidebar.fontInspectorTitle=Fonts
+# LOCALIZATION NOTE (inspector.sidebar.changesViewTitle):
+# Title shown in a tab in the side panel of the Inspector panel that corresponds
+# to the tool which shows a collection of style changes made.
+inspector.sidebar.changesViewTitle=Changes
+
# LOCALIZATION NOTE (inspector.sidebar.ruleViewTitle):
# This is the title shown in a tab in the side panel of the Inspector panel
# that corresponds to the tool displaying the list of CSS rules used
# in the page.
inspector.sidebar.ruleViewTitle=Rules
# LOCALIZATION NOTE (inspector.sidebar.computedViewTitle):
# This is the title shown in a tab in the side panel of the Inspector panel
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/changes.css
@@ -0,0 +1,90 @@
+/* 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/. */
+
+#sidebar-panel-changes {
+ margin: 0;
+ width: 100%;
+ height: 100%;
+ overflow: auto;
+}
+
+details {
+ font-family: monospace;
+ font-size: 12px;
+}
+
+summary {
+ user-select: none;
+ outline: none;
+ padding: 5px 0;
+ -moz-user-select: none;
+ cursor: pointer;
+}
+
+details.source[open] {
+ padding-bottom: 10px;
+}
+
+details.source > summary {
+ background: #f9f9fa;
+ border-top: 1px solid #dee1e4;
+ border-bottom: 1px solid #dee1e4;
+ padding-left: 5px;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+}
+
+details.selector > summary {
+ padding-left: 10px;
+}
+
+details.selector summary::after{
+ content: "{...}";
+ display: inline-block;
+ padding-left: 5px;
+}
+
+details.selector[open]{
+ margin-bottom: 5px;
+}
+
+details.selector[open] summary::after{
+ content: "{";
+}
+
+details.selector[open]::after{
+ content: "}";
+ display: block;
+ padding-left: 10px;
+}
+
+.line {
+ padding: 3px 5px 3px 15px;
+ position: relative;
+}
+
+.diff-add::before,
+.diff-remove::before{
+ position: absolute;
+ left: 5px;
+}
+
+.diff-add {
+ background-color: #f1feec;
+}
+
+.diff-add::before {
+ content: "+";
+ color: #54983f;
+}
+
+.diff-remove {
+ background-color: #fbf2f5;
+}
+
+.diff-remove::before{
+ content: "-";
+ color: #bf7173;
+}