Source code
Revision control
Copy as Markdown
Other Tools
/* 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
/* eslint-disable react/prop-types */
"use strict";
const {
Component,
createFactory,
} = 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/redux/visibility-handler-connect.js");
const {
setTargetSearchResult,
} = require("resource://devtools/client/netmonitor/src/actions/search.js");
// Components
const TreeViewClass = require("resource://devtools/client/shared/components/tree/TreeView.js");
const TreeView = createFactory(TreeViewClass);
const PropertiesViewContextMenu = require("resource://devtools/client/netmonitor/src/widgets/PropertiesViewContextMenu.js");
loader.lazyGetter(this, "Rep", function () {
return require("resource://devtools/client/shared/components/reps/index.js")
.REPS.Rep;
});
loader.lazyGetter(this, "MODE", function () {
return require("resource://devtools/client/shared/components/reps/index.js")
.MODE;
});
// Constants
const {
AUTO_EXPAND_MAX_LEVEL,
AUTO_EXPAND_MAX_NODES,
} = require("resource://devtools/client/netmonitor/src/constants.js");
const { div } = dom;
/**
* Properties View component
* A scrollable tree view component which provides some useful features for
* representing object properties.
*
* Tree view
* Rep
*/
class PropertiesView extends Component {
static get propTypes() {
return {
object: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
provider: PropTypes.object,
enableInput: PropTypes.bool,
expandableStrings: PropTypes.bool,
expandedNodes: PropTypes.object,
useBaseTreeViewExpand: PropTypes.bool,
filterText: PropTypes.string,
cropLimit: PropTypes.number,
targetSearchResult: PropTypes.object,
resetTargetSearchResult: PropTypes.func,
selectPath: PropTypes.func,
mode: PropTypes.symbol,
defaultSelectFirstNode: PropTypes.bool,
useQuotes: PropTypes.bool,
onClickRow: PropTypes.func,
contextMenuFormatters: PropTypes.object,
};
}
static get defaultProps() {
return {
enableInput: true,
enableFilter: true,
expandableStrings: false,
cropLimit: 1024,
useQuotes: true,
contextMenuFormatters: {},
useBaseTreeViewExpand: false,
};
}
constructor(props) {
super(props);
this.onFilter = this.onFilter.bind(this);
this.renderValueWithRep = this.renderValueWithRep.bind(this);
this.getSelectedPath = this.getSelectedPath.bind(this);
}
/**
* Update only if:
* 1) The rendered object has changed
* 2) The filter text has changed
* 3) The user selected another search result target.
*/
shouldComponentUpdate(nextProps) {
return (
this.props.object !== nextProps.object ||
this.props.filterText !== nextProps.filterText ||
(this.props.targetSearchResult !== nextProps.targetSearchResult &&
nextProps.targetSearchResult !== null)
);
}
onFilter(props) {
const { name, value } = props;
const { filterText } = this.props;
if (!filterText) {
return true;
}
const jsonString = JSON.stringify({ [name]: value }).toLowerCase();
return jsonString.includes(filterText.toLowerCase());
}
getSelectedPath(targetSearchResult) {
if (!targetSearchResult) {
return null;
}
return `/${targetSearchResult.label}`;
}
/**
* If target is selected, let's scroll the content
* so the property is visible. This is used for search result navigation,
* which happens when the user clicks on a search result.
*/
scrollSelectedIntoView() {
const { targetSearchResult, resetTargetSearchResult, selectPath } =
this.props;
if (!targetSearchResult) {
return;
}
const path =
typeof selectPath == "function"
? selectPath(targetSearchResult)
: this.getSelectedPath(targetSearchResult);
const element = document.getElementById(path);
if (element) {
element.scrollIntoView({ block: "center" });
}
resetTargetSearchResult();
}
onContextMenuRow(member, evt) {
evt.preventDefault();
const { object } = member;
// Select the right clicked row
this.selectRow({ props: { member } });
// if data exists and can be copied, then show the contextmenu
if (typeof object === "object") {
if (!this.contextMenu) {
this.contextMenu = new PropertiesViewContextMenu({
customFormatters: this.props.contextMenuFormatters,
});
}
this.contextMenu.open(evt, window.getSelection(), {
member,
object: this.props.object,
});
}
}
renderValueWithRep(props) {
const { member } = props;
/* Hide strings with following conditions
* - the `value` object has a `value` property (only happens in Cookies panel)
*/
if (typeof member.value === "object" && member.value?.value) {
return null;
}
return Rep(
Object.assign(props, {
// FIXME: A workaround for the issue in StringRep
// Force StringRep to crop the text every time
member: Object.assign({}, member, { open: false }),
mode: this.props.mode || MODE.TINY,
cropLimit: this.props.cropLimit,
noGrip: true,
})
);
}
render() {
const {
useBaseTreeViewExpand,
expandedNodes,
object,
renderValue,
targetSearchResult,
selectPath,
} = this.props;
let currentExpandedNodes;
// In the TreeView, when the component is re-rendered
// the state of `expandedNodes` is persisted by default
// e.g. when you open a node and filter the properties list,
// the node remains open.
// We have the prop `useBaseTreeViewExpand` to flag when we want to use
// this functionality or not.
if (!useBaseTreeViewExpand) {
currentExpandedNodes =
expandedNodes ||
TreeViewClass.getExpandedNodes(object, {
maxLevel: AUTO_EXPAND_MAX_LEVEL,
maxNodes: AUTO_EXPAND_MAX_NODES,
});
}
return div(
{ className: "properties-view" },
div(
{ className: "tree-container" },
TreeView({
...this.props,
ref: () => this.scrollSelectedIntoView(),
columns: [{ id: "value", width: "100%" }],
expandedNodes: currentExpandedNodes,
onFilter: props => this.onFilter(props),
renderValue: renderValue || this.renderValueWithRep,
onContextMenuRow: this.onContextMenuRow,
selected:
typeof selectPath == "function"
? selectPath(targetSearchResult)
: this.getSelectedPath(targetSearchResult),
})
)
);
}
}
module.exports = connect(null, dispatch => ({
resetTargetSearchResult: () => dispatch(setTargetSearchResult(null)),
}))(PropertiesView);