flipper-plugin
Version:
Flipper Desktop plugin SDK and components
481 lines • 21.1 kB
JavaScript
"use strict";
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.Elements = exports.ElementsConstants = void 0;
const antd_1 = require("antd");
const react_1 = require("react");
const styled_1 = __importDefault(require("@emotion/styled"));
const react_2 = __importDefault(require("react"));
const theme_1 = require("../theme");
const Layout_1 = require("../Layout");
const icons_1 = require("@ant-design/icons");
const FlipperLib_1 = require("../../plugin/FlipperLib");
const { Text } = antd_1.Typography;
const contextMenuTrigger = ['contextMenu'];
exports.ElementsConstants = {
rowHeight: 23,
};
const backgroundColor = (props) => {
if (props.selected || props.isQueryMatch || props.focused) {
return theme_1.theme.backgroundWash;
}
else {
return '';
}
};
const ElementsRowContainer = (0, styled_1.default)(Layout_1.Layout.Horizontal)((props) => ({
flexDirection: 'row',
alignItems: 'center',
backgroundColor: backgroundColor(props),
color: theme_1.theme.textColorPrimary,
flexShrink: 0,
flexWrap: 'nowrap',
height: exports.ElementsConstants.rowHeight,
paddingLeft: (props.level - 1) * 12,
paddingRight: 20,
position: 'relative',
}));
ElementsRowContainer.displayName = 'Elements:ElementsRowContainer';
const ElementsRowDecoration = (0, styled_1.default)(Layout_1.Layout.Horizontal)({
flexShrink: 0,
justifyContent: 'flex-end',
alignItems: 'center',
marginRight: 4,
position: 'relative',
width: 16,
top: -1,
});
ElementsRowDecoration.displayName = 'Elements:ElementsRowDecoration';
const ElementsLine = styled_1.default.div((props) => ({
backgroundColor: theme_1.theme.backgroundWash,
height: props.childrenCount * exports.ElementsConstants.rowHeight - 4,
position: 'absolute',
right: 3,
top: exports.ElementsConstants.rowHeight - 3,
zIndex: 2,
width: 2,
borderRadius: '999em',
}));
ElementsLine.displayName = 'Elements:ElementsLine';
const DecorationImage = styled_1.default.img({
height: 12,
marginRight: 5,
width: 12,
});
DecorationImage.displayName = 'Elements:DecorationImage';
const NoShrinkText = (0, styled_1.default)(Text)({
flexShrink: 0,
flexWrap: 'nowrap',
overflow: 'hidden',
fontWeight: 400,
font: theme_1.theme.monospace.fontFamily,
fontSize: theme_1.theme.monospace.fontSize,
});
NoShrinkText.displayName = 'Elements:NoShrinkText';
const ElementsRowAttributeContainer = (0, styled_1.default)(NoShrinkText)({
color: theme_1.theme.textColorSecondary,
fontWeight: 300,
marginLeft: 5,
});
ElementsRowAttributeContainer.displayName =
'Elements:ElementsRowAttributeContainer';
const ElementsRowAttributeKey = styled_1.default.span({
color: theme_1.theme.semanticColors.attribute,
});
ElementsRowAttributeKey.displayName = 'Elements:ElementsRowAttributeKey';
const ElementsRowAttributeValue = styled_1.default.span({
color: theme_1.theme.textColorSecondary,
});
ElementsRowAttributeValue.displayName = 'Elements:ElementsRowAttributeValue';
// Merge this functionality with components/Highlight
class PartialHighlight extends react_1.PureComponent {
render() {
const { highlighted, content, selected } = this.props;
if (content &&
highlighted != null &&
highlighted != '' &&
content.toLowerCase().includes(highlighted.toLowerCase())) {
const highlightStart = content
.toLowerCase()
.indexOf(highlighted.toLowerCase());
const highlightEnd = highlightStart + highlighted.length;
const before = content.substring(0, highlightStart);
const match = content.substring(highlightStart, highlightEnd);
const after = content.substring(highlightEnd);
return (react_2.default.createElement("span", null,
before,
react_2.default.createElement(PartialHighlight.HighlightedText, { selected: selected }, match),
after));
}
else {
return react_2.default.createElement("span", null, content);
}
}
}
PartialHighlight.HighlightedText = styled_1.default.span((props) => ({
backgroundColor: theme_1.theme.searchHighlightBackground.yellow,
color: props.selected ? `${theme_1.theme.textColorPrimary} !important` : 'auto',
}));
class ElementsRowAttribute extends react_1.PureComponent {
render() {
const { name, value, matchingSearchQuery, selected } = this.props;
return (react_2.default.createElement(ElementsRowAttributeContainer, null,
react_2.default.createElement(ElementsRowAttributeKey, null, name),
"=",
react_2.default.createElement(ElementsRowAttributeValue, null,
react_2.default.createElement(PartialHighlight, { content: value, highlighted: name === 'id' || name === 'addr' ? matchingSearchQuery : '', selected: selected }))));
}
}
class ElementsRow extends react_1.PureComponent {
constructor(props, context) {
super(props, context);
this.getContextMenu = () => {
const { props } = this;
let items = [
{
label: 'Copy',
click: () => {
(0, FlipperLib_1.getFlipperLib)()?.writeTextToClipboard(props.onCopyExpandedTree(props.element, 0));
},
},
{
label: 'Copy expanded child elements',
click: () => (0, FlipperLib_1.getFlipperLib)()?.writeTextToClipboard(props.onCopyExpandedTree(props.element, 255)),
},
{
label: props.element.expanded ? 'Collapse' : 'Expand',
click: () => {
this.props.onElementExpanded(this.props.id, false);
},
},
{
label: props.element.expanded
? 'Collapse all child elements'
: 'Expand all child elements',
click: () => {
this.props.onElementExpanded(this.props.id, true);
},
},
];
items = items.concat(props.element.attributes.map((o) => {
return {
label: `Copy ${o.name}`,
click: () => {
(0, FlipperLib_1.getFlipperLib)()?.writeTextToClipboard(o.value);
},
};
}));
// Array.isArray check for backward compatibility
const extensions = Array.isArray(props.contextMenuExtensions)
? props.contextMenuExtensions
: props.contextMenuExtensions?.();
extensions?.forEach((extension) => {
items.push({
label: extension.label,
click: () => extension.click(this.props.id),
});
});
return (react_2.default.createElement(antd_1.Menu, null, items.map(({ label, click }) => (react_2.default.createElement(antd_1.Menu.Item, { key: label, onClick: click }, label)))));
};
this.onClick = () => {
this.props.onElementSelected(this.props.id);
};
this.onDoubleClick = (event) => {
this.props.onElementSelected(this.props.id);
this.props.onElementExpanded(this.props.id, event.altKey);
};
this.onMouseEnter = () => {
this.setState({ hovered: true });
if (this.props.onElementHovered) {
this.props.onElementHovered(this.props.id);
}
};
this.onMouseLeave = () => {
this.setState({ hovered: false });
if (this.props.onElementHovered) {
this.props.onElementHovered(null);
}
};
this.state = { hovered: false };
}
render() {
const { element, id, level, selected, focused, style, even, matchingSearchQuery, decorateRow, forwardedRef, } = this.props;
const hasChildren = element.children && element.children.length > 0;
let arrow;
if (hasChildren) {
arrow = (react_2.default.createElement("span", { onClick: this.onDoubleClick, role: "button", tabIndex: -1, style: {
color: theme_1.theme.textColorSecondary,
fontSize: '8px',
} }, element.expanded ? react_2.default.createElement(icons_1.DownOutlined, null) : react_2.default.createElement(icons_1.RightOutlined, null)));
}
const attributes = element.attributes
? element.attributes.map((attr) => (react_2.default.createElement(ElementsRowAttribute, { key: attr.name, name: attr.name, value: attr.value, matchingSearchQuery: matchingSearchQuery, selected: selected })))
: [];
const decoration = decorateRow
? decorateRow(element)
: defaultDecorateRow(element);
// when we hover over or select an expanded element with children, we show a line from the
// bottom of the element to the next sibling
let line;
const shouldShowLine = (selected || this.state.hovered) && hasChildren && element.expanded;
if (shouldShowLine) {
line = react_2.default.createElement(ElementsLine, { childrenCount: this.props.childrenCount });
}
return (react_2.default.createElement(antd_1.Dropdown, { key: id, overlay: this.getContextMenu, trigger: contextMenuTrigger },
react_2.default.createElement(ElementsRowContainer, { level: level, ref: forwardedRef, selected: selected, focused: focused, matchingSearchQuery: matchingSearchQuery, even: even, onClick: this.onClick, onDoubleClick: this.onDoubleClick, onMouseEnter: this.onMouseEnter, onMouseLeave: this.onMouseLeave, isQueryMatch: this.props.isQueryMatch, style: style },
react_2.default.createElement(ElementsRowDecoration, null,
line,
arrow),
react_2.default.createElement(NoShrinkText, { style: {
fontWeight: theme_1.theme.bold,
color: selected ? theme_1.theme.primaryColor : theme_1.theme.textColorPrimary,
} },
decoration,
react_2.default.createElement(PartialHighlight, { content: element.name, highlighted: matchingSearchQuery, selected: selected })),
attributes)));
}
}
function defaultDecorateRow(element) {
switch (element.decoration) {
case 'litho':
return react_2.default.createElement(DecorationImage, { src: "icons/litho-logo.png" });
case 'componentkit':
return react_2.default.createElement(DecorationImage, { src: "icons/componentkit-logo.png" });
case 'accessibility':
return react_2.default.createElement(DecorationImage, { src: "icons/accessibility.png" });
default:
return null;
}
}
function containsKeyInSearchResults(searchResults, key) {
return searchResults != undefined && searchResults.matches.has(key);
}
const ElementsContainer = (0, styled_1.default)('div')({
display: 'table',
backgroundColor: theme_1.theme.backgroundDefault,
});
ElementsContainer.displayName = 'Elements:ElementsContainer';
class Elements extends react_1.PureComponent {
constructor(props, context) {
super(props, context);
this.selectElement = (key) => {
this.props.onElementSelected(key);
};
this.isDarwin = (0, FlipperLib_1.tryGetFlipperLibImplementation)()?.environmentInfo.os.platform === 'darwin';
this.onKeyDown = (e) => {
const { selected } = this.props;
if (selected == null) {
return;
}
const { props } = this;
const { flatElements, flatKeys } = this.state;
const selectedIndex = flatKeys.indexOf(selected);
if (selectedIndex < 0) {
return;
}
const selectedElement = props.elements[selected];
if (!selectedElement) {
return;
}
if (e.key === 'c' &&
((e.metaKey && this.isDarwin) || (e.ctrlKey && this.isDarwin))) {
e.stopPropagation();
e.preventDefault();
(0, FlipperLib_1.getFlipperLib)()?.writeTextToClipboard(selectedElement.name);
return;
}
if (e.key === 'ArrowUp') {
e.stopPropagation();
if (selectedIndex === 0 || flatKeys.length === 1) {
return;
}
e.preventDefault();
this.selectElement(flatKeys[selectedIndex - 1]);
}
if (e.key === 'ArrowDown') {
e.stopPropagation();
if (selectedIndex === flatKeys.length - 1) {
return;
}
e.preventDefault();
this.selectElement(flatKeys[selectedIndex + 1]);
}
if (e.key === 'ArrowLeft') {
e.stopPropagation();
e.preventDefault();
if (selectedElement.expanded) {
// unexpand
props.onElementExpanded(selected, false);
}
else {
// jump to parent
let parentKey;
const targetLevel = flatElements[selectedIndex].level - 1;
for (let i = selectedIndex; i >= 0; i--) {
const { level } = flatElements[i];
if (level === targetLevel) {
parentKey = flatKeys[i];
break;
}
}
if (parentKey) {
this.selectElement(parentKey);
}
}
}
if (e.key === 'ArrowRight' && selectedElement.children.length > 0) {
e.stopPropagation();
e.preventDefault();
if (selectedElement.expanded) {
// go to first child
this.selectElement(selectedElement.children[0]);
}
else {
// expand
props.onElementExpanded(selected, false);
}
}
};
this.onElementHoveredHandler = (key) => {
this.props.onElementHovered?.(key);
};
this.onCopyExpandedTree = (element, maxDepth, depth = 0) => {
const shouldIncludeChildren = element.expanded && depth < maxDepth;
const children = shouldIncludeChildren
? element.children.map((childId) => {
const childElement = this.props.elements[childId];
return childElement == null
? ''
: this.onCopyExpandedTree(childElement, maxDepth, depth + 1);
})
: [];
const childrenValue = children.toString().replace(',', '');
const indentation = depth === 0 ? '' : '\n'.padEnd(depth * 2 + 1, ' ');
const attrs = element.attributes.reduce((acc, val) => `${acc} ${val.name}=${val.value}`, '');
return `${indentation}${element.name}${attrs}${childrenValue}`;
};
this.parentRef = (0, react_1.createRef)();
this.scrollToSelectionRefHandler = (selectedRow) => {
if (selectedRow && this.state.scrolledElement !== this.props.selected) {
if (this.parentRef.current &&
!isInView(this.parentRef.current, selectedRow)) {
// second child is the element containing the element name
// by scrolling to the second element, we make sure padding is addressed and we scroll horizontally as well
selectedRow.children[1]?.scrollIntoView?.({
block: 'center',
inline: 'center',
});
}
this.setState({ scrolledElement: this.props.selected });
}
};
this.buildRow = (row, index) => {
const { onElementExpanded, onElementSelected, selected, focused, searchResults, contextMenuExtensions, decorateRow, } = this.props;
const { flatElements } = this.state;
let childrenCount = 0;
for (let i = index + 1; i < flatElements.length; i++) {
const child = flatElements[i];
if (child.level <= row.level) {
break;
}
else {
childrenCount++;
}
}
let isEven = false;
if (this.props.alternateRowColor) {
isEven = index % 2 === 0;
}
return (react_2.default.createElement(ElementsRow, { level: row.level, id: row.key, key: row.key, even: isEven, onElementExpanded: onElementExpanded, onElementHovered: this.onElementHoveredHandler, onElementSelected: onElementSelected, onCopyExpandedTree: this.onCopyExpandedTree, selected: selected === row.key, focused: focused === row.key, matchingSearchQuery: searchResults && containsKeyInSearchResults(searchResults, row.key)
? searchResults.query
: null, isQueryMatch: containsKeyInSearchResults(searchResults, row.key), element: row.element, childrenCount: childrenCount, contextMenuExtensions: contextMenuExtensions, decorateRow: decorateRow, forwardedRef: selected == row.key // && this.state.scrolledElement !== selected
? this.scrollToSelectionRefHandler
: null }));
};
this.state = {
flatElements: [],
flatKeys: [],
maxDepth: 0,
scrolledElement: null,
};
}
static getDerivedStateFromProps(props) {
const flatElements = [];
const flatKeys = [];
let maxDepth = 0;
function seed(key, level) {
const element = props.elements[key];
if (!element) {
return;
}
maxDepth = Math.max(maxDepth, level);
flatElements.push({
element,
key,
level,
});
flatKeys.push(key);
if (element.children != null &&
element.children.length > 0 &&
element.expanded) {
for (const key of element.children) {
seed(key, level + 1);
}
}
}
if (props.root != null) {
seed(props.root, 1);
}
else {
const virtualRoots = new Set();
Object.keys(props.elements).forEach((id) => virtualRoots.add(id));
for (const [currentId, element] of Object.entries(props.elements)) {
if (!element) {
virtualRoots.delete(currentId);
}
else {
element.children.forEach((id) => virtualRoots.delete(id));
}
}
virtualRoots.forEach((id) => seed(id, 1));
}
return { flatElements, flatKeys, maxDepth };
}
render() {
return (react_2.default.createElement(ElementsContainer, { onKeyDown: this.onKeyDown, tabIndex: 0, ref: this.parentRef }, this.state.flatElements.map(this.buildRow)));
}
}
exports.Elements = Elements;
Elements.defaultProps = {
alternateRowColor: true,
};
function isInView(parent, el) {
// find the scroll container. (This is fragile)
// TODO: Fix this the next time the file is edited.
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const scrollContainer = parent.parentElement.parentElement;
// check vertical scroll
if (el.offsetTop > scrollContainer.scrollTop &&
el.offsetTop < scrollContainer.scrollTop + scrollContainer.clientHeight) {
// check if horizontal scroll is needed,
// do this by checking the indented node, not the row (which is always visible)
const child = el.childNodes[0];
if (child.offsetLeft > scrollContainer.scrollLeft &&
child.offsetLeft <
scrollContainer.scrollLeft + scrollContainer.clientWidth) {
return true;
}
}
return false;
}
//# sourceMappingURL=elements.js.map