UNPKG

flipper-plugin

Version:

Flipper Desktop plugin SDK and components

481 lines 21.1 kB
"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