Files
flipper/src/plugins/layout/Inspector.js
Daniel Büchele a54b02b583 release v2
Summary:
- Moving the new layout plugin from `layout2` to `layout`.
- updating dependencies
- removed beta toolbar

Reviewed By: passy

Differential Revision: D14519490

fbshipit-source-id: d184767e767e1717368f66e2bda2af318b7e63c9
2019-03-19 06:47:06 -07:00

236 lines
6.4 KiB
JavaScript

/**
* Copyright 2018-present Facebook.
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
* @format
*/
import type {
ElementID,
Element,
PluginClient,
ElementSearchResultSet,
} from 'flipper';
import {ElementsInspector} from 'flipper';
import {Component} from 'react';
import debounce from 'lodash.debounce';
import type {PersistedState} from './';
type GetNodesOptions = {
force?: boolean,
ax?: boolean,
forAccessibilityEvent?: boolean,
};
type Props = {
ax?: boolean,
client: PluginClient,
showsSidebar: boolean,
inAlignmentMode?: boolean,
selectedElement: ?ElementID,
selectedAXElement: ?ElementID,
onSelect: (ids: ?ElementID) => void,
onDataValueChanged: (path: Array<string>, value: any) => void,
setPersistedState: (state: $Shape<PersistedState>) => void,
persistedState: PersistedState,
searchResults: ?ElementSearchResultSet,
};
export default class Inspector extends Component<Props> {
call() {
return {
GET_ROOT: this.props.ax ? 'getAXRoot' : 'getRoot',
INVALIDATE: this.props.ax ? 'invalidateAX' : 'invalidate',
GET_NODES: this.props.ax ? 'getAXNodes' : 'getNodes',
SET_HIGHLIGHTED: 'setHighlighted',
SELECT: this.props.ax ? 'selectAX' : 'select',
};
}
selected = () => {
return this.props.ax
? this.props.selectedAXElement
: this.props.selectedElement;
};
root = () => {
return this.props.ax
? this.props.persistedState.rootAXElement
: this.props.persistedState.rootElement;
};
elements = () => {
return this.props.ax
? this.props.persistedState.AXelements
: this.props.persistedState.elements;
};
componentDidMount() {
this.props.client.call(this.call().GET_ROOT).then((root: Element) => {
this.props.setPersistedState({
[this.props.ax ? 'rootAXElement' : 'rootElement']: root.id,
});
this.updateElement(root.id, {...root, expanded: true});
this.performInitialExpand(root);
});
this.props.client.subscribe(
this.call().INVALIDATE,
({
nodes,
}: {
nodes: Array<{id: ElementID, children: Array<ElementID>}>,
}) => {
this.getNodes(
nodes
.map(n => [n.id, ...(n.children || [])])
.reduce((acc, cv) => acc.concat(cv), []),
{},
);
},
);
this.props.client.subscribe(
this.call().SELECT,
({path}: {path: Array<ElementID>}) => {
this.getAndExpandPath(path);
},
);
}
componentDidUpdate(prevProps: Props) {
const {ax, selectedElement, selectedAXElement} = this.props;
if (
ax &&
selectedElement !== prevProps.selectedElement &&
selectedElement
) {
// selected element changed, find linked AX element
const linkedAXNode: ?ElementID = this.props.persistedState.elements[
selectedElement
]?.extraInfo?.linkedAXNode;
this.props.onSelect(linkedAXNode);
} else if (
!ax &&
selectedAXElement !== prevProps.selectedAXElement &&
selectedAXElement
) {
// selected AX element changed, find linked element
// $FlowFixMe Object.values retunes mixed type
const linkedNode: ?Element = Object.values(
this.props.persistedState.elements,
// $FlowFixMe it's an Element not mixed
).find((e: Element) => e.extraInfo?.linkedAXNode === selectedAXElement);
this.props.onSelect(linkedNode?.id);
}
}
updateElement(id: ElementID, data: Object) {
this.props.setPersistedState({
[this.props.ax ? 'AXelements' : 'elements']: {
...this.elements(),
[id]: {
...this.elements()[id],
...data,
},
},
});
}
// When opening the inspector for the first time, expand all elements that
// contain only 1 child recursively.
async performInitialExpand(element: Element): Promise<void> {
if (!element.children.length) {
// element has no children so we're as deep as we can be
return;
}
return this.getChildren(element.id, {}).then((elements: Array<Element>) => {
if (element.children.length >= 2) {
// element has two or more children so we can stop expanding
return;
}
return this.performInitialExpand(this.elements()[element.children[0]]);
});
}
async getChildren(
id: ElementID,
options: GetNodesOptions,
): Promise<Array<Element>> {
if (!this.elements()[id]) {
await this.getNodes([id], options);
}
this.updateElement(id, {expanded: true});
return this.getNodes(this.elements()[id].children, options);
}
getNodes(
ids: Array<ElementID> = [],
options: GetNodesOptions,
): Promise<Array<Element>> {
const {forAccessibilityEvent} = options;
if (ids.length > 0) {
return this.props.client
.call(this.call().GET_NODES, {
ids,
forAccessibilityEvent,
selected: false,
})
.then(({elements}) => {
elements.forEach(e => this.updateElement(e.id, e));
return elements;
});
} else {
return Promise.resolve([]);
}
}
getAndExpandPath(path: Array<ElementID>) {
return Promise.all(path.map(id => this.getChildren(id, {}))).then(() => {
this.onElementSelected(path[path.length - 1]);
});
}
onElementSelected = debounce((selectedKey: ElementID) => {
this.onElementHovered(selectedKey);
this.props.onSelect(selectedKey);
});
onElementHovered = debounce((key: ?ElementID) =>
this.props.client.call(this.call().SET_HIGHLIGHTED, {
id: key,
isAlignmentMode: this.props.inAlignmentMode,
}),
);
onElementExpanded = (id: ElementID, deep: boolean) => {
const expanded = !this.elements()[id].expanded;
this.updateElement(id, {expanded});
if (expanded) {
this.getChildren(id, {}).then(children => {
if (deep) {
children.forEach(child => this.onElementExpanded(child.id, deep));
}
});
}
};
render() {
return this.root() ? (
<ElementsInspector
onElementSelected={this.onElementSelected}
onElementHovered={this.onElementHovered}
onElementExpanded={this.onElementExpanded}
onValueChanged={this.props.onDataValueChanged}
searchResults={this.props.searchResults}
selected={this.selected()}
root={this.root()}
elements={this.elements()}
/>
) : null;
}
}