summaryrefslogtreecommitdiffstats
path: root/devtools/client/dom/content
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/dom/content')
-rw-r--r--devtools/client/dom/content/actions/filter.js19
-rw-r--r--devtools/client/dom/content/actions/grips.js54
-rw-r--r--devtools/client/dom/content/actions/moz.build9
-rw-r--r--devtools/client/dom/content/components/DomTree.js132
-rw-r--r--devtools/client/dom/content/components/MainFrame.js74
-rw-r--r--devtools/client/dom/content/components/MainToolbar.js76
-rw-r--r--devtools/client/dom/content/components/moz.build6
-rw-r--r--devtools/client/dom/content/constants.js7
-rw-r--r--devtools/client/dom/content/dom-decorator.js46
-rw-r--r--devtools/client/dom/content/dom-view.css109
-rw-r--r--devtools/client/dom/content/dom-view.js65
-rw-r--r--devtools/client/dom/content/grip-provider.js100
-rw-r--r--devtools/client/dom/content/moz.build18
-rw-r--r--devtools/client/dom/content/reducers/filter.js27
-rw-r--r--devtools/client/dom/content/reducers/grips.js123
-rw-r--r--devtools/client/dom/content/reducers/index.js12
-rw-r--r--devtools/client/dom/content/reducers/moz.build10
-rw-r--r--devtools/client/dom/content/utils.js25
18 files changed, 912 insertions, 0 deletions
diff --git a/devtools/client/dom/content/actions/filter.js b/devtools/client/dom/content/actions/filter.js
new file mode 100644
index 0000000000..83de5a95ce
--- /dev/null
+++ b/devtools/client/dom/content/actions/filter.js
@@ -0,0 +1,19 @@
+/* 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 constants = require("devtools/client/dom/content/constants");
+
+/**
+ * Used to filter DOM panel content.
+ */
+function setVisibilityFilter(filter) {
+ return {
+ filter: filter,
+ type: constants.SET_VISIBILITY_FILTER,
+ };
+}
+
+// Exports from this module
+exports.setVisibilityFilter = setVisibilityFilter;
diff --git a/devtools/client/dom/content/actions/grips.js b/devtools/client/dom/content/actions/grips.js
new file mode 100644
index 0000000000..a49f01f566
--- /dev/null
+++ b/devtools/client/dom/content/actions/grips.js
@@ -0,0 +1,54 @@
+/* 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/. */
+/* globals DomProvider */
+"use strict";
+
+const constants = require("devtools/client/dom/content/constants");
+
+/**
+ * Used to fetch grip prototype and properties from the backend.
+ */
+function requestProperties(grip) {
+ return {
+ grip: grip,
+ type: constants.FETCH_PROPERTIES,
+ status: "start",
+ error: false,
+ };
+}
+
+/**
+ * Executed when grip properties are received from the backend.
+ */
+function receiveProperties(grip, response, error) {
+ return {
+ grip: grip,
+ type: constants.FETCH_PROPERTIES,
+ status: "end",
+ response: response,
+ error: error,
+ };
+}
+
+/**
+ * Used to get properties from the backend and fire an action
+ * when they are received.
+ */
+function fetchProperties(grip) {
+ return async ({ dispatch }) => {
+ try {
+ // Use 'DomProvider' object exposed from the chrome scope.
+ const response = await DomProvider.getPrototypeAndProperties(grip);
+ dispatch(receiveProperties(grip, response));
+ } catch (e) {
+ console.error("Error while fetching properties", e);
+ }
+ DomProvider.onPropertiesFetched();
+ };
+}
+
+// Exports from this module
+exports.requestProperties = requestProperties;
+exports.receiveProperties = receiveProperties;
+exports.fetchProperties = fetchProperties;
diff --git a/devtools/client/dom/content/actions/moz.build b/devtools/client/dom/content/actions/moz.build
new file mode 100644
index 0000000000..8c1c56d39a
--- /dev/null
+++ b/devtools/client/dom/content/actions/moz.build
@@ -0,0 +1,9 @@
+# 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(
+ "filter.js",
+ "grips.js",
+)
diff --git a/devtools/client/dom/content/components/DomTree.js b/devtools/client/dom/content/components/DomTree.js
new file mode 100644
index 0000000000..88faa6f2a8
--- /dev/null
+++ b/devtools/client/dom/content/components/DomTree.js
@@ -0,0 +1,132 @@
+/* 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/. */
+
+/* global DomProvider */
+
+"use strict";
+
+// React & Redux
+const {
+ Component,
+ createFactory,
+} = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+
+const { connect } = require("devtools/client/shared/vendor/react-redux");
+
+const TreeView = createFactory(
+ require("devtools/client/shared/components/tree/TreeView")
+);
+// Reps
+const { REPS, MODE } = require("devtools/client/shared/components/reps/index");
+const { Rep } = REPS;
+
+const Grip = REPS.Grip;
+// DOM Panel
+const { GripProvider } = require("devtools/client/dom/content/grip-provider");
+
+const { DomDecorator } = require("devtools/client/dom/content/dom-decorator");
+
+/**
+ * Renders DOM panel tree.
+ */
+class DomTree extends Component {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ filter: PropTypes.string,
+ grips: PropTypes.object,
+ object: PropTypes.any,
+ openLink: PropTypes.func,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onFilter = this.onFilter.bind(this);
+ }
+
+ /**
+ * Filter DOM properties. Return true if the object
+ * should be visible in the tree.
+ */
+ onFilter(object) {
+ if (!this.props.filter) {
+ return true;
+ }
+
+ return object.name && object.name.indexOf(this.props.filter) > -1;
+ }
+
+ /**
+ * Render DOM panel content
+ */
+ render() {
+ const { dispatch, grips, object, openLink } = this.props;
+
+ const columns = [
+ {
+ id: "value",
+ },
+ ];
+
+ let onDOMNodeMouseOver;
+ let onDOMNodeMouseOut;
+ let onInspectIconClick;
+ const toolbox = DomProvider.getToolbox();
+ if (toolbox) {
+ const highlighter = toolbox.getHighlighter();
+ onDOMNodeMouseOver = async (grip, options = {}) => {
+ return highlighter.highlight(grip, options);
+ };
+ onDOMNodeMouseOut = async () => {
+ return highlighter.unhighlight();
+ };
+ onInspectIconClick = async grip => {
+ return toolbox.viewElementInInspector(grip, "inspect_dom");
+ };
+ }
+
+ // This is the integration point with Reps. The DomTree is using
+ // Reps to render all values. The code also specifies default rep
+ // used for data types that don't have its own specific template.
+ const renderValue = props => {
+ const repProps = Object.assign({}, props, {
+ onDOMNodeMouseOver,
+ onDOMNodeMouseOut,
+ onInspectIconClick,
+ defaultRep: Grip,
+ cropLimit: 50,
+ });
+
+ // Object can be an objectFront, while Rep always expect grips.
+ if (props?.object?.getGrip) {
+ repProps.object = props.object.getGrip();
+ }
+
+ return Rep(repProps);
+ };
+
+ return TreeView({
+ columns,
+ decorator: new DomDecorator(),
+ mode: MODE.SHORT,
+ object,
+ onFilter: this.onFilter,
+ openLink,
+ provider: new GripProvider(grips, dispatch),
+ renderValue,
+ });
+ }
+}
+
+const mapStateToProps = state => {
+ return {
+ grips: state.grips,
+ filter: state.filter,
+ };
+};
+
+// Exports from this module
+module.exports = connect(mapStateToProps)(DomTree);
diff --git a/devtools/client/dom/content/components/MainFrame.js b/devtools/client/dom/content/components/MainFrame.js
new file mode 100644
index 0000000000..16aee4856e
--- /dev/null
+++ b/devtools/client/dom/content/components/MainFrame.js
@@ -0,0 +1,74 @@
+/* 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/. */
+/* globals DomProvider */
+
+"use strict";
+
+// React & Redux
+const {
+ Component,
+ createFactory,
+} = 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");
+// DOM Panel
+const DomTree = createFactory(
+ require("devtools/client/dom/content/components/DomTree")
+);
+
+const MainToolbar = createFactory(
+ require("devtools/client/dom/content/components/MainToolbar")
+);
+// Shortcuts
+const { div } = dom;
+
+/**
+ * Renders basic layout of the DOM panel. The DOM panel content consists
+ * from two main parts: toolbar and tree.
+ */
+class MainFrame extends Component {
+ static get propTypes() {
+ return {
+ dispatch: PropTypes.func.isRequired,
+ filter: PropTypes.string,
+ object: PropTypes.any,
+ };
+ }
+
+ /**
+ * Render DOM panel content
+ */
+ render() {
+ const { filter, object } = this.props;
+
+ return div(
+ { className: "mainFrame" },
+ MainToolbar({
+ dispatch: this.props.dispatch,
+ object: this.props.object,
+ }),
+ div(
+ { className: "treeTableBox devtools-monospace" },
+ DomTree({
+ filter,
+ object,
+ openLink: url => DomProvider.openLink(url),
+ })
+ )
+ );
+ }
+}
+
+// Transform state into props
+// Note: use https://github.com/faassen/reselect for better performance.
+const mapStateToProps = state => {
+ return {
+ filter: state.filter,
+ };
+};
+
+// Exports from this module
+module.exports = connect(mapStateToProps)(MainFrame);
diff --git a/devtools/client/dom/content/components/MainToolbar.js b/devtools/client/dom/content/components/MainToolbar.js
new file mode 100644
index 0000000000..342ba97e47
--- /dev/null
+++ b/devtools/client/dom/content/components/MainToolbar.js
@@ -0,0 +1,76 @@
+/* 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";
+
+// React
+const {
+ Component,
+ createFactory,
+} = 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 SearchBox = createFactory(
+ require("devtools/client/shared/components/SearchBox")
+);
+
+const { l10n } = require("devtools/client/dom/content/utils");
+
+// Actions
+const {
+ fetchProperties,
+} = require("devtools/client/dom/content/actions/grips");
+const {
+ setVisibilityFilter,
+} = require("devtools/client/dom/content/actions/filter");
+
+/**
+ * This template is responsible for rendering a toolbar
+ * within the 'Headers' panel.
+ */
+class MainToolbar extends Component {
+ static get propTypes() {
+ return {
+ object: PropTypes.any.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ };
+ }
+
+ constructor(props) {
+ super(props);
+ this.onRefresh = this.onRefresh.bind(this);
+ this.onSearch = this.onSearch.bind(this);
+ }
+
+ onRefresh() {
+ this.props.dispatch(fetchProperties(this.props.object));
+ }
+
+ onSearch(value) {
+ this.props.dispatch(setVisibilityFilter(value));
+ }
+
+ render() {
+ return dom.div(
+ { className: "devtools-toolbar devtools-input-toolbar" },
+ SearchBox({
+ key: "filter",
+ delay: 250,
+ onChange: this.onSearch,
+ placeholder: l10n.getStr("dom.filterDOMPanel"),
+ type: "filter",
+ }),
+ dom.span({ className: "devtools-separator" }),
+ dom.button({
+ key: "refresh",
+ className: "refresh devtools-button",
+ id: "dom-refresh-button",
+ title: l10n.getStr("dom.refresh"),
+ onClick: this.onRefresh,
+ })
+ );
+ }
+}
+
+// Exports from this module
+module.exports = MainToolbar;
diff --git a/devtools/client/dom/content/components/moz.build b/devtools/client/dom/content/components/moz.build
new file mode 100644
index 0000000000..5b669bac24
--- /dev/null
+++ b/devtools/client/dom/content/components/moz.build
@@ -0,0 +1,6 @@
+# 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("DomTree.js", "MainFrame.js", "MainToolbar.js")
diff --git a/devtools/client/dom/content/constants.js b/devtools/client/dom/content/constants.js
new file mode 100644
index 0000000000..c7bb8b325c
--- /dev/null
+++ b/devtools/client/dom/content/constants.js
@@ -0,0 +1,7 @@
+/* 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";
+
+exports.FETCH_PROPERTIES = "FETCH_PROPERTIES";
+exports.SET_VISIBILITY_FILTER = "SET_VISIBILITY_FILTER";
diff --git a/devtools/client/dom/content/dom-decorator.js b/devtools/client/dom/content/dom-decorator.js
new file mode 100644
index 0000000000..39c18330da
--- /dev/null
+++ b/devtools/client/dom/content/dom-decorator.js
@@ -0,0 +1,46 @@
+/* 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 { Property } = require("devtools/client/dom/content/reducers/grips");
+
+// Implementation
+
+function DomDecorator() {}
+
+/**
+ * Decorator for DOM panel tree component. It's responsible for
+ * appending an icon to read only properties.
+ */
+DomDecorator.prototype = {
+ getRowClass: function(object) {
+ if (object instanceof Property) {
+ const value = object.value;
+ const names = [];
+
+ if (value.enumerable) {
+ names.push("enumerable");
+ }
+ if (value.writable) {
+ names.push("writable");
+ }
+ if (value.configurable) {
+ names.push("configurable");
+ }
+
+ return names;
+ }
+
+ return null;
+ },
+
+ /**
+ * Return custom React template for specified object. The template
+ * might depend on specified column.
+ */
+ getValueRep: function(value, colId) {},
+};
+
+// Exports from this module
+exports.DomDecorator = DomDecorator;
diff --git a/devtools/client/dom/content/dom-view.css b/devtools/client/dom/content/dom-view.css
new file mode 100644
index 0000000000..d2fd77e525
--- /dev/null
+++ b/devtools/client/dom/content/dom-view.css
@@ -0,0 +1,109 @@
+/* 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/. */
+
+/******************************************************************************/
+/* General */
+
+body {
+ padding: 0;
+ margin: 0;
+ overflow: hidden;
+ background-color: var(--theme-body-background);
+}
+
+.mainFrame {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+}
+
+.mainFrame > .treeTableBox {
+ flex: 1 1 auto;
+ overflow: auto;
+}
+
+/******************************************************************************/
+/* TreeView Customization */
+
+.treeTable {
+ width: 100%;
+}
+
+/* Space for read only properties icon */
+.treeTable td.treeValueCell {
+ padding-inline-start: 16px;
+}
+
+.treeTable .treeLabel,
+.treeTable td.treeValueCell .objectBox {
+ direction: ltr; /* Don't change the direction of english labels */
+}
+
+/* Read only properties have a padlock icon */
+.treeTable tr:not(.writable) td.treeValueCell {
+ background: url("chrome://devtools/skin/images/lock.svg") no-repeat;
+ background-position: 1px 4px;
+ background-size: 12px 12px;
+ -moz-context-properties: fill;
+ fill: var(--theme-icon-dimmed-color);
+}
+
+.treeTable tr:not(.writable) td.treeValueCell:dir(rtl) {
+ background-position-x: right 1px;
+}
+
+.treeTable tr:not(.writable).selected td.treeValueCell {
+ fill: var(--theme-selection-color);
+}
+
+/* Non-enumerable properties are grayed out */
+.treeTable tr:not(.enumerable) td.treeValueCell {
+ opacity: 0.7;
+}
+
+.theme-light .treeTable > tbody > tr > td {
+ border-bottom: 1px solid var(--grey-20);
+}
+
+/* Label Types */
+.treeTable .userLabel,
+.treeTable .userClassLabel,
+.treeTable .userFunctionLabel {
+ font-weight: bold;
+}
+
+.treeTable .userLabel {
+ color: #000000;
+}
+
+.treeTable .userClassLabel {
+ color: #E90000;
+}
+
+.treeTable .userFunctionLabel {
+ color: #025E2A;
+}
+
+.treeTable .domLabel {
+ color: #000000;
+}
+
+.treeTable .domClassLabel {
+ color: #E90000;
+}
+
+.treeTable .domFunctionLabel {
+ color: #025E2A;
+}
+
+.treeTable .ordinalLabel {
+ color: SlateBlue;
+ font-weight: bold;
+}
+
+/******************************************************************************/
+/* Refresh button */
+#dom-refresh-button::before {
+ background-image: url("chrome://devtools/skin/images/reload.svg");
+}
diff --git a/devtools/client/dom/content/dom-view.js b/devtools/client/dom/content/dom-view.js
new file mode 100644
index 0000000000..69aa489279
--- /dev/null
+++ b/devtools/client/dom/content/dom-view.js
@@ -0,0 +1,65 @@
+/* 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";
+
+// React & Redux
+const React = require("devtools/client/shared/vendor/react");
+const ReactDOM = require("devtools/client/shared/vendor/react-dom");
+const { Provider } = require("devtools/client/shared/vendor/react-redux");
+
+// DOM Panel
+const MainFrame = React.createFactory(
+ require("devtools/client/dom/content/components/MainFrame")
+);
+
+// Store
+const createStore = require("devtools/client/shared/redux/create-store");
+
+const { reducers } = require("devtools/client/dom/content/reducers/index");
+const store = createStore(reducers);
+
+/**
+ * This object represents view of the DOM panel and is responsible
+ * for rendering the content. It renders the top level ReactJS
+ * component: the MainFrame.
+ */
+function DomView(localStore) {
+ addEventListener("devtools/chrome/message", this.onMessage.bind(this), true);
+
+ // Make it local so, tests can access it.
+ this.store = localStore;
+}
+
+DomView.prototype = {
+ initialize: function(rootGrip) {
+ const content = document.querySelector("#content");
+ const mainFrame = MainFrame({
+ object: rootGrip,
+ });
+
+ // Render top level component
+ const provider = React.createElement(
+ Provider,
+ {
+ store: this.store,
+ },
+ mainFrame
+ );
+
+ this.mainFrame = ReactDOM.render(provider, content);
+ },
+
+ onMessage: function(event) {
+ const data = event.data;
+ const method = data.type;
+
+ if (typeof this[method] == "function") {
+ this[method](data.args);
+ }
+ },
+};
+
+// Construct DOM panel view object and expose it to tests.
+// Tests can access it through: |panel.panelWin.view|
+window.view = new DomView(store);
diff --git a/devtools/client/dom/content/grip-provider.js b/devtools/client/dom/content/grip-provider.js
new file mode 100644
index 0000000000..1988369a77
--- /dev/null
+++ b/devtools/client/dom/content/grip-provider.js
@@ -0,0 +1,100 @@
+/* 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 {
+ fetchProperties,
+} = require("devtools/client/dom/content/actions/grips");
+const { Property } = require("devtools/client/dom/content/reducers/grips");
+
+// Implementation
+function GripProvider(grips, dispatch) {
+ this.grips = grips;
+ this.dispatch = dispatch;
+}
+
+/**
+ * This object provides data for the tree displayed in the tooltip
+ * content.
+ */
+GripProvider.prototype = {
+ /**
+ * Fetches properties from the backend. These properties might be
+ * displayed as child objects in e.g. a tree UI widget.
+ */
+ getChildren: function(object) {
+ let grip = object;
+ if (object instanceof Property) {
+ grip = this.getValue(object);
+ }
+
+ if (!grip || !grip.actorID) {
+ return [];
+ }
+
+ const props = this.grips.get(grip.actorID);
+ if (!props) {
+ // Fetch missing data from the backend. Returning a promise
+ // from data provider causes the tree to show a spinner.
+ return this.dispatch(fetchProperties(grip));
+ }
+
+ return props;
+ },
+
+ hasChildren: function(object) {
+ if (object instanceof Property) {
+ const value = this.getValue(object);
+ if (!value) {
+ return false;
+ }
+ const grip = value?.getGrip ? value.getGrip() : value;
+
+ let hasChildren = grip.ownPropertyLength > 0;
+
+ if (grip.preview) {
+ hasChildren = hasChildren || grip.preview.ownPropertiesLength > 0;
+ }
+
+ if (grip.preview) {
+ const preview = grip.preview;
+ const k = preview.kind;
+ const objectsWithProps = ["DOMNode", "ObjectWithURL"];
+ hasChildren = hasChildren || objectsWithProps.includes(k);
+ hasChildren = hasChildren || (k == "ArrayLike" && preview.length > 0);
+ }
+
+ return grip.type == "object" && hasChildren;
+ }
+
+ return null;
+ },
+
+ getValue: function(object) {
+ if (object instanceof Property) {
+ const value = object.value;
+ return typeof value.value != "undefined"
+ ? value.value
+ : value.getterValue;
+ }
+
+ return object;
+ },
+
+ getLabel: function(object) {
+ return object instanceof Property ? object.name : null;
+ },
+
+ getKey: function(object) {
+ return object instanceof Property ? object.key : null;
+ },
+
+ getType: function(object) {
+ const grip = object?.getGrip ? object.getGrip() : object;
+ return grip.class ? grip.class : "";
+ },
+};
+
+// Exports from this module
+exports.GripProvider = GripProvider;
diff --git a/devtools/client/dom/content/moz.build b/devtools/client/dom/content/moz.build
new file mode 100644
index 0000000000..04e0ac4efc
--- /dev/null
+++ b/devtools/client/dom/content/moz.build
@@ -0,0 +1,18 @@
+# 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(
+ "constants.js",
+ "dom-decorator.js",
+ "dom-view.js",
+ "grip-provider.js",
+ "utils.js",
+)
diff --git a/devtools/client/dom/content/reducers/filter.js b/devtools/client/dom/content/reducers/filter.js
new file mode 100644
index 0000000000..428dca2e4c
--- /dev/null
+++ b/devtools/client/dom/content/reducers/filter.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 constants = require("devtools/client/dom/content/constants");
+
+/**
+ * Initial state definition
+ */
+function getInitialState() {
+ return "";
+}
+
+/**
+ * Filter displayed object properties.
+ */
+function filter(state = getInitialState(), action) {
+ if (action.type == constants.SET_VISIBILITY_FILTER) {
+ return action.filter;
+ }
+
+ return state;
+}
+
+// Exports from this module
+exports.filter = filter;
diff --git a/devtools/client/dom/content/reducers/grips.js b/devtools/client/dom/content/reducers/grips.js
new file mode 100644
index 0000000000..acfb1146f2
--- /dev/null
+++ b/devtools/client/dom/content/reducers/grips.js
@@ -0,0 +1,123 @@
+/* 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 constants = require("devtools/client/dom/content/constants");
+
+/**
+ * Initial state definition
+ */
+function getInitialState() {
+ return new Map();
+}
+
+/**
+ * Maintain a cache of received grip responses from the backend.
+ */
+function grips(state = getInitialState(), action) {
+ // This reducer supports only one action, fetching actor properties
+ // from the backend so, bail out if we are dealing with any other
+ // action.
+ if (action.type != constants.FETCH_PROPERTIES) {
+ return state;
+ }
+
+ switch (action.status) {
+ case "start":
+ return onRequestProperties(state, action);
+ case "end":
+ return onReceiveProperties(state, action);
+ }
+
+ return state;
+}
+
+/**
+ * Handle requestProperties action
+ */
+function onRequestProperties(state, action) {
+ return state;
+}
+
+/**
+ * Handle receiveProperties action
+ */
+function onReceiveProperties(cache, action) {
+ const response = action.response;
+ const from = response.from;
+ const className = action.grip?.getGrip
+ ? action.grip.getGrip().class
+ : action.grip.class;
+
+ // Properly deal with getters.
+ mergeProperties(response);
+
+ // Compute list of requested children.
+ const previewProps = response.preview ? response.preview.ownProperties : null;
+ const ownProps = response.ownProperties || previewProps || [];
+
+ const props = Object.keys(ownProps).map(key => {
+ // Array indexes as a special case. We convert any keys that are string
+ // representations of integers to integers.
+ if (className === "Array" && isInteger(key)) {
+ key = parseInt(key, 10);
+ }
+ return new Property(key, ownProps[key], key);
+ });
+
+ props.sort(sortName);
+
+ // Return new state/map.
+ const newCache = new Map(cache);
+ newCache.set(from, props);
+
+ return newCache;
+}
+
+// Helpers
+
+function mergeProperties(response) {
+ const { ownProperties } = response;
+
+ // 'safeGetterValues' is new and isn't necessary defined on old grips.
+ const safeGetterValues = response.safeGetterValues || {};
+
+ // Merge the safe getter values into one object such that we can use it
+ // in variablesView.
+ for (const name of Object.keys(safeGetterValues)) {
+ if (name in ownProperties) {
+ const { getterValue, getterPrototypeLevel } = safeGetterValues[name];
+ ownProperties[name].getterValue = getterValue;
+ ownProperties[name].getterPrototypeLevel = getterPrototypeLevel;
+ } else {
+ ownProperties[name] = safeGetterValues[name];
+ }
+ }
+}
+
+function sortName(a, b) {
+ // Display non-enumerable properties at the end.
+ if (!a.value.enumerable && b.value.enumerable) {
+ return 1;
+ }
+ if (a.value.enumerable && !b.value.enumerable) {
+ return -1;
+ }
+ return a.name > b.name ? 1 : -1;
+}
+
+function isInteger(n) {
+ // We use parseInt(n, 10) == n to disregard scientific notation e.g. "3e24"
+ return isFinite(n) && parseInt(n, 10) == n;
+}
+
+function Property(name, value, key) {
+ this.name = name;
+ this.value = value;
+ this.key = key;
+}
+
+// Exports from this module
+exports.grips = grips;
+exports.Property = Property;
diff --git a/devtools/client/dom/content/reducers/index.js b/devtools/client/dom/content/reducers/index.js
new file mode 100644
index 0000000000..a6d1fa47b8
--- /dev/null
+++ b/devtools/client/dom/content/reducers/index.js
@@ -0,0 +1,12 @@
+/* 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 { grips } = require("devtools/client/dom/content/reducers/grips");
+const { filter } = require("devtools/client/dom/content/reducers/filter");
+
+exports.reducers = {
+ grips,
+ filter,
+};
diff --git a/devtools/client/dom/content/reducers/moz.build b/devtools/client/dom/content/reducers/moz.build
new file mode 100644
index 0000000000..8d98444e34
--- /dev/null
+++ b/devtools/client/dom/content/reducers/moz.build
@@ -0,0 +1,10 @@
+# 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(
+ "filter.js",
+ "grips.js",
+ "index.js",
+)
diff --git a/devtools/client/dom/content/utils.js b/devtools/client/dom/content/utils.js
new file mode 100644
index 0000000000..c0632d0e97
--- /dev/null
+++ b/devtools/client/dom/content/utils.js
@@ -0,0 +1,25 @@
+/* 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";
+
+/**
+ * The default localization just returns the last part of the key
+ * (all after the last dot).
+ */
+const DefaultL10N = {
+ getStr: function(key) {
+ const index = key.lastIndexOf(".");
+ return key.substr(index + 1);
+ },
+};
+
+/**
+ * The 'l10n' object is set by main.js in case the DOM panel content
+ * runs within a scope with chrome privileges.
+ *
+ * Note that DOM panel content can also run within a scope with no chrome
+ * privileges, e.g. in an iframe with type 'content' or in a browser tab,
+ * which allows using our own tools for development.
+ */
+exports.l10n = window.l10n || DefaultL10N;