/* 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/. */ import { createFactory, Component, } from "resource://devtools/client/shared/vendor/react.mjs"; import * as dom from "resource://devtools/client/shared/vendor/react-dom-factories.mjs"; import PropTypes from "resource://devtools/client/shared/vendor/react-prop-types.mjs"; import { createFactories } from "resource://devtools/client/shared/react-utils.mjs"; import TreeViewClass from "resource://devtools/client/shared/components/tree/TreeView.mjs"; import { BucketProperty } from "resource://devtools/client/shared/components/tree/ObjectProvider.mjs"; import JsonToolbarClass from "resource://devtools/client/jsonview/components/JsonToolbar.mjs"; import JsonBreadcrumbClass from "resource://devtools/client/jsonview/components/JsonBreadcrumb.mjs"; import { JSON_NUMBER, MODE, } from "resource://devtools/client/shared/components/reps/reps/constants.mjs"; import { Rep } from "resource://devtools/client/shared/components/reps/reps/rep.mjs"; const TreeView = createFactory(TreeViewClass); const { JsonToolbar } = createFactories(JsonToolbarClass); const { JsonBreadcrumb } = createFactories(JsonBreadcrumbClass); const { div } = dom; const MAX_STRING_LENGTH = 250; function isObject(value) { return Object(value) === value; } /** * This template represents the 'JSON' panel. The panel is * responsible for rendering an expandable tree that allows simple * inspection of JSON structure. */ class JsonPanel extends Component { static get propTypes() { return { data: PropTypes.oneOfType([ PropTypes.string, PropTypes.array, PropTypes.object, PropTypes.bool, PropTypes.number, ]), dataSize: PropTypes.number, expandedNodes: PropTypes.instanceOf(Set), searchFilter: PropTypes.string, actions: PropTypes.object, }; } /** * Splits a path string by unescaped forward slashes (`/`). * * Escaped slashes (`\/`) are preserved as literal `/` characters * in the resulting segments. Escape characters (`\`) are removed. * * Examples: * splitPath("a/b/c") → ["a", "b", "c"] * splitPath("a\\/b/c") → ["a/b", "c"] * splitPath("/a/b/") → ["a", "b"] * * @param {string} path * The input path string. Forward slashes can be escaped using `\`. * * @returns {string[]} * An array of non-empty path segments with escape characters removed. */ static splitPath(path) { return path .split(/(? segment.replace(/\\(.)/g, "$1")) .filter(_s => _s.length); } constructor(props) { super(props); this.state = {}; this.onKeyPress = this.onKeyPress.bind(this); this.onFilter = this.onFilter.bind(this); this.renderValue = this.renderValue.bind(this); this.renderTree = this.renderTree.bind(this); this.onRowSelected = this.onRowSelected.bind(this); this.updateBreadcrumbs = this.updateBreadcrumbs.bind(this); } componentDidMount() { document.addEventListener("keypress", this.onKeyPress, true); document.getElementById("json-scrolling-panel").focus(); } componentWillUnmount() { document.removeEventListener("keypress", this.onKeyPress, true); } onKeyPress() { // XXX shortcut for focusing the Filter field (see Bug 1178771). } onRowSelected(selectedRowPath) { if (!selectedRowPath || typeof selectedRowPath !== "string") { return; } const rowPathParts = JsonPanel.splitPath(selectedRowPath); this.updateBreadcrumbs(rowPathParts); } /** * @param {string[]} rowPathParts */ updateBreadcrumbs(rowPathParts) { let jsonData = this.props.data; const breadcrumbs = []; for (let i = 0; i < rowPathParts.length; i++) { if (typeof jsonData !== "object") { break; } const key = rowPathParts[i]; // Skip bucket range segments like "[0…99]" if (jsonData[key] === undefined && Array.isArray(jsonData)) { continue; } jsonData = jsonData[key]; breadcrumbs.push({ type: getJsonValueType(jsonData), text: key }); } if (breadcrumbs.length) { this.setState({ breadcrumbs }); } } onFilter(object) { if (!this.props.searchFilter) { return true; } const searchFilter = this.props.searchFilter.toLowerCase(); // For bucket nodes, check if any of their children match if (object instanceof BucketProperty) { const { object: array, startIndex, endIndex } = object; for (let i = startIndex; i <= endIndex; i++) { const childJson = JSON.stringify(array[i]); if (childJson.toLowerCase().includes(searchFilter)) { return true; } } return false; } const json = object.name + JSON.stringify(object.value); return json.toLowerCase().includes(searchFilter); } renderValue(props) { const member = props.member; // Hide value for bucket nodes (they show ranges like [0…99]) if (member.type === "bucket") { return null; } // Hide object summary when non-empty object is expanded (bug 1244912). if (isObject(member.value) && member.hasChildren && member.open) { return null; } // Render the value (summary) using Reps library. return Rep( Object.assign({}, props, { cropLimit: MAX_STRING_LENGTH, noGrip: true, isInContentPage: true, }) ); } renderTree() { // Append custom column for displaying values. This column // Take all available horizontal space. const columns = [ { id: "value", width: "100%", }, ]; // Render tree component. return TreeView({ object: this.props.data, mode: MODE.LONG, bucketLargeArrays: true, onFilter: this.onFilter, columns, renderValue: this.renderValue, expandedNodes: this.props.expandedNodes, maxStringLength: MAX_STRING_LENGTH, onRowSelected: this.onRowSelected, }); } render() { let content; const data = this.props.data; if (!isObject(data)) { content = div( { className: "jsonPrimitiveValue" }, Rep({ object: data, }) ); } else if (data instanceof Error) { content = div({ className: "jsonParseError" }, data + ""); } else if (data.type === JSON_NUMBER) { content = div( { className: "jsonPrimitiveValue" }, Rep({ object: data, noGrip: true, }) ); } else { content = this.renderTree(); } return div( { className: "jsonPanelBox tab-panel-inner" }, JsonToolbar({ actions: this.props.actions, dataSize: this.props.dataSize, }), div( { className: "panelContent", id: "json-scrolling-panel", tabIndex: 0, }, content ), JsonBreadcrumb({ items: this.state.breadcrumbs, }) ); } } // Helpers /** * Determines the type of a given value. * Returns a string representing the type of the input value. * * @param {*} value - The value whose type is to be determined. * @returns {string} The type of the input value as a string. * `array`, `null`, `object`, `number`, `boolean`, `string` */ function getJsonValueType(value) { if (value?.type === JSON_NUMBER) { return "number"; } if (Array.isArray(value)) { return "array"; } else if (value === null) { return "null"; } return typeof value; } export default { JsonPanel };