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
* file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */
"use strict";
// Make this available to both AMD and CJS environments
define(function (require, exports, module) {
// ReactJS
const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
const { span } = require("devtools/client/shared/vendor/react-dom-factories");
// Dependencies
const {
interleave,
wrapRender,
} = require("devtools/client/shared/components/reps/reps/rep-utils");
const PropRep = require("devtools/client/shared/components/reps/reps/prop-rep");
const {
MODE,
} = require("devtools/client/shared/components/reps/reps/constants");
/**
* Renders generic grip. Grip is client representation
* of remote JS object and is used as an input object
* for this rep component.
*/
GripRep.propTypes = {
object: PropTypes.object.isRequired,
mode: PropTypes.oneOf(Object.values(MODE)),
isInterestingProp: PropTypes.func,
title: PropTypes.string,
onDOMNodeMouseOver: PropTypes.func,
onDOMNodeMouseOut: PropTypes.func,
onInspectIconClick: PropTypes.func,
noGrip: PropTypes.bool,
shouldRenderTooltip: PropTypes.bool,
};
const DEFAULT_TITLE = "Object";
function GripRep(props) {
const { mode = MODE.SHORT, object, shouldRenderTooltip } = props;
const config = {
"data-link-actor-id": object.actor,
className: "objectBox objectBox-object",
};
if (mode === MODE.TINY) {
const propertiesLength = getPropertiesLength(object);
const tinyModeItems = [];
if (getTitle(props, object) !== DEFAULT_TITLE) {
tinyModeItems.push(getTitleElement(props, object));
} else {
tinyModeItems.push(
span(
{
className: "objectLeftBrace",
},
"{"
),
propertiesLength > 0
? span(
{
key: "more",
className: "more-ellipsis",
},
"…"
)
: null,
span(
{
className: "objectRightBrace",
},
"}"
)
);
}
config.title = shouldRenderTooltip ? getTitle(props, object) : null;
return span(config, ...tinyModeItems);
}
if (mode === MODE.HEADER) {
config.title = shouldRenderTooltip ? getTitle(props, object) : null;
return span(config, getTitleElement(props, object));
}
const propsArray = safePropIterator(props, object, maxLengthMap.get(mode));
config.title = shouldRenderTooltip ? getTitle(props, object) : null;
return span(
config,
getTitleElement(props, object),
span(
{
className: "objectLeftBrace",
},
" { "
),
...interleave(propsArray, ", "),
span(
{
className: "objectRightBrace",
},
" }"
)
);
}
function getTitleElement(props, object) {
return span(
{
className: "objectTitle",
},
getTitle(props, object)
);
}
function getTitle(props, object) {
return props.title || object.class || DEFAULT_TITLE;
}
function getPropertiesLength(object) {
let propertiesLength =
object.preview && object.preview.ownPropertiesLength
? object.preview.ownPropertiesLength
: object.ownPropertyLength;
if (object.preview && object.preview.safeGetterValues) {
propertiesLength += Object.keys(object.preview.safeGetterValues).length;
}
if (object.preview && object.preview.ownSymbols) {
propertiesLength += object.preview.ownSymbolsLength;
}
if (object.preview && object.preview.privateProperties) {
propertiesLength += object.preview.privatePropertiesLength;
}
return propertiesLength;
}
function safePropIterator(props, object, max) {
max = typeof max === "undefined" ? maxLengthMap.get(MODE.SHORT) : max;
try {
return propIterator(props, object, max);
} catch (err) {
console.error(err);
}
return [];
}
function propIterator(props, object, max) {
if (
object.preview &&
Object.keys(object.preview).includes("wrappedValue")
) {
const {
Rep,
} = require("devtools/client/shared/components/reps/reps/rep");
return [
Rep({
object: object.preview.wrappedValue,
mode: props.mode || MODE.TINY,
defaultRep: Grip,
}),
];
}
// Property filter. Show only interesting properties to the user.
const isInterestingProp =
props.isInterestingProp ||
((type, value) => {
return (
type == "boolean" ||
type == "number" ||
(type == "string" && !!value.length)
);
});
let properties = object.preview ? object.preview.ownProperties || {} : {};
const propertiesLength = getPropertiesLength(object);
if (object.preview && object.preview.safeGetterValues) {
properties = { ...properties, ...object.preview.safeGetterValues };
}
let indexes = getPropIndexes(properties, max, isInterestingProp);
if (indexes.length < max && indexes.length < propertiesLength) {
// There are not enough props yet.
// Then add uninteresting props to display them.
indexes = indexes.concat(
getPropIndexes(properties, max - indexes.length, (t, value, name) => {
return !isInterestingProp(t, value, name);
})
);
}
// The server synthesizes some property names for a Proxy, like
// <target> and <handler>; we don't want to quote these because,
// as synthetic properties, they appear more natural when
// unquoted. Analogous for a Promise.
const suppressQuotes = ["Proxy", "Promise"].includes(object.class);
const propsArray = getProps(props, properties, indexes, suppressQuotes);
// Show private properties
if (object.preview && object.preview.privateProperties) {
const { privateProperties } = object.preview;
const length = max - indexes.length;
const privateProps = privateProperties.slice(0, length).map(item => {
const value = item.descriptor.value;
const grip = value && value.getGrip ? value.getGrip() : value;
return PropRep({
...props,
keyClassName: "private",
mode: MODE.TINY,
name: item.name,
object: grip,
equal: ": ",
defaultRep: Grip,
title: null,
suppressQuotes: true,
});
});
propsArray.push(...privateProps);
}
// Show symbols.
if (object.preview && object.preview.ownSymbols) {
const { ownSymbols } = object.preview;
const length = max - indexes.length;
const symbolsProps = ownSymbols.slice(0, length).map(symbolItem => {
const symbolValue = symbolItem.descriptor.value;
const symbolGrip =
symbolValue && symbolValue.getGrip
? symbolValue.getGrip()
: symbolValue;
return PropRep({
...props,
mode: MODE.TINY,
name: symbolItem,
object: symbolGrip,
equal: ": ",
defaultRep: Grip,
title: null,
suppressQuotes,
});
});
propsArray.push(...symbolsProps);
}
if (
Object.keys(properties).length > max ||
propertiesLength > max ||
// When the object has non-enumerable properties, we don't have them in the
// packet, but we might want to show there's something in the object.
propertiesLength > propsArray.length
) {
// There are some undisplayed props. Then display "more...".
propsArray.push(
span(
{
key: "more",
className: "more-ellipsis",
},
"…"
)
);
}
return propsArray;
}
/**
* Get props ordered by index.
*
* @param {Object} componentProps Grip Component props.
* @param {Object} properties Properties of the object the Grip describes.
* @param {Array} indexes Indexes of properties.
* @param {Boolean} suppressQuotes true if we should suppress quotes
* on property names.
* @return {Array} Props.
*/
function getProps(componentProps, properties, indexes, suppressQuotes) {
// Make indexes ordered by ascending.
indexes.sort(function (a, b) {
return a - b;
});
const propertiesKeys = Object.keys(properties);
return indexes.map(i => {
const name = propertiesKeys[i];
const value = getPropValue(properties[name]);
return PropRep({
...componentProps,
mode: MODE.TINY,
name,
object: value,
equal: ": ",
defaultRep: Grip,
title: null,
suppressQuotes,
});
});
}
/**
* Get the indexes of props in the object.
*
* @param {Object} properties Props object.
* @param {Number} max The maximum length of indexes array.
* @param {Function} filter Filter the props you want.
* @return {Array} Indexes of interesting props in the object.
*/
function getPropIndexes(properties, max, filter) {
const indexes = [];
try {
let i = 0;
for (const name in properties) {
if (indexes.length >= max) {
return indexes;
}
// Type is specified in grip's "class" field and for primitive
// values use typeof.
const value = getPropValue(properties[name]);
let type = value.class || typeof value;
type = type.toLowerCase();
if (filter(type, value, name)) {
indexes.push(i);
}
i++;
}
} catch (err) {
console.error(err);
}
return indexes;
}
/**
* Get the actual value of a property.
*
* @param {Object} property
* @return {Object} Value of the property.
*/
function getPropValue(property) {
let value = property;
if (typeof property === "object") {
const keys = Object.keys(property);
if (keys.includes("value")) {
value = property.value;
} else if (keys.includes("getterValue")) {
value = property.getterValue;
}
}
return value;
}
// Registration
function supportsObject(object) {
if (object?.class === "DeadObject") {
return true;
}
return object?.preview
? typeof object.preview.ownProperties !== "undefined"
: typeof object?.ownPropertyLength !== "undefined";
}
const maxLengthMap = new Map();
maxLengthMap.set(MODE.SHORT, 3);
maxLengthMap.set(MODE.LONG, 10);
// Grip is used in propIterator and has to be defined here.
const Grip = {
rep: wrapRender(GripRep),
supportsObject,
maxLengthMap,
};
// Exports from this module
module.exports = Grip;
});