349 lines
8.9 KiB
JavaScript
349 lines
8.9 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @format
|
|
* @flow
|
|
*/
|
|
|
|
'use strict';
|
|
|
|
const Dimensions = require('../Utilities/Dimensions');
|
|
const InspectorOverlay = require('./InspectorOverlay');
|
|
const InspectorPanel = require('./InspectorPanel');
|
|
const Platform = require('../Utilities/Platform');
|
|
const React = require('react');
|
|
const ReactNative = require('../Renderer/shims/ReactNative');
|
|
const StyleSheet = require('../StyleSheet/StyleSheet');
|
|
const Touchable = require('../Components/Touchable/Touchable');
|
|
const View = require('../Components/View/View');
|
|
|
|
const invariant = require('invariant');
|
|
|
|
import type {
|
|
HostComponent,
|
|
TouchedViewDataAtPoint,
|
|
} from '../Renderer/shims/ReactNativeTypes';
|
|
|
|
type HostRef = React.ElementRef<HostComponent<mixed>>;
|
|
|
|
export type ReactRenderer = {
|
|
rendererConfig: {
|
|
getInspectorDataForViewAtPoint: (
|
|
inspectedView: ?HostRef,
|
|
locationX: number,
|
|
locationY: number,
|
|
callback: Function,
|
|
) => void,
|
|
...
|
|
},
|
|
};
|
|
|
|
const hook = window.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
const renderers = findRenderers();
|
|
|
|
// Required for React DevTools to view/edit React Native styles in Flipper.
|
|
// Flipper doesn't inject these values when initializing DevTools.
|
|
hook.resolveRNStyle = require('../StyleSheet/flattenStyle');
|
|
const viewConfig = require('../Components/View/ReactNativeViewViewConfig');
|
|
hook.nativeStyleEditorValidAttributes = Object.keys(
|
|
viewConfig.validAttributes.style,
|
|
);
|
|
|
|
function findRenderers(): $ReadOnlyArray<ReactRenderer> {
|
|
const allRenderers = Array.from(hook.renderers.values());
|
|
invariant(
|
|
allRenderers.length >= 1,
|
|
'Expected to find at least one React Native renderer on DevTools hook.',
|
|
);
|
|
return allRenderers;
|
|
}
|
|
|
|
function getInspectorDataForViewAtPoint(
|
|
inspectedView: ?HostRef,
|
|
locationX: number,
|
|
locationY: number,
|
|
callback: (viewData: TouchedViewDataAtPoint) => void,
|
|
) {
|
|
// Check all renderers for inspector data.
|
|
for (let i = 0; i < renderers.length; i++) {
|
|
const renderer = renderers[i];
|
|
if (renderer?.rendererConfig?.getInspectorDataForViewAtPoint != null) {
|
|
renderer.rendererConfig.getInspectorDataForViewAtPoint(
|
|
inspectedView,
|
|
locationX,
|
|
locationY,
|
|
viewData => {
|
|
// Only return with non-empty view data since only one renderer will have this view.
|
|
if (viewData && viewData.hierarchy.length > 0) {
|
|
callback(viewData);
|
|
}
|
|
},
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
class Inspector extends React.Component<
|
|
{
|
|
inspectedView: ?HostRef,
|
|
onRequestRerenderApp: (callback: (instance: ?HostRef) => void) => void,
|
|
...
|
|
},
|
|
{
|
|
devtoolsAgent: ?Object,
|
|
hierarchy: any,
|
|
panelPos: string,
|
|
inspecting: boolean,
|
|
selection: ?number,
|
|
perfing: boolean,
|
|
inspected: any,
|
|
inspectedView: ?HostRef,
|
|
networking: boolean,
|
|
...
|
|
},
|
|
> {
|
|
_hideTimeoutID: TimeoutID | null = null;
|
|
_subs: ?Array<() => void>;
|
|
_setTouchedViewData: ?(TouchedViewDataAtPoint) => void;
|
|
|
|
constructor(props: Object) {
|
|
super(props);
|
|
|
|
this.state = {
|
|
devtoolsAgent: null,
|
|
hierarchy: null,
|
|
panelPos: 'bottom',
|
|
inspecting: true,
|
|
perfing: false,
|
|
inspected: null,
|
|
selection: null,
|
|
inspectedView: this.props.inspectedView,
|
|
networking: false,
|
|
};
|
|
}
|
|
|
|
componentDidMount() {
|
|
hook.on('react-devtools', this._attachToDevtools);
|
|
// if devtools is already started
|
|
if (hook.reactDevtoolsAgent) {
|
|
this._attachToDevtools(hook.reactDevtoolsAgent);
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this._subs) {
|
|
this._subs.map(fn => fn());
|
|
}
|
|
hook.off('react-devtools', this._attachToDevtools);
|
|
this._setTouchedViewData = null;
|
|
}
|
|
|
|
UNSAFE_componentWillReceiveProps(newProps: Object) {
|
|
this.setState({inspectedView: newProps.inspectedView});
|
|
}
|
|
|
|
_attachToDevtools = (agent: Object) => {
|
|
agent.addListener('hideNativeHighlight', this._onAgentHideNativeHighlight);
|
|
agent.addListener('showNativeHighlight', this._onAgentShowNativeHighlight);
|
|
agent.addListener('shutdown', this._onAgentShutdown);
|
|
|
|
this.setState({
|
|
devtoolsAgent: agent,
|
|
});
|
|
};
|
|
|
|
_onAgentHideNativeHighlight = () => {
|
|
if (this.state.inspected === null) {
|
|
return;
|
|
}
|
|
// we wait to actually hide in order to avoid flicker
|
|
this._hideTimeoutID = setTimeout(() => {
|
|
this.setState({
|
|
inspected: null,
|
|
});
|
|
}, 100);
|
|
};
|
|
|
|
_onAgentShowNativeHighlight = node => {
|
|
clearTimeout(this._hideTimeoutID);
|
|
|
|
node.measure((x, y, width, height, left, top) => {
|
|
this.setState({
|
|
hierarchy: [],
|
|
inspected: {
|
|
frame: {left, top, width, height},
|
|
},
|
|
});
|
|
});
|
|
};
|
|
|
|
_onAgentShutdown = () => {
|
|
const agent = this.state.devtoolsAgent;
|
|
if (agent != null) {
|
|
agent.removeListener(
|
|
'hideNativeHighlight',
|
|
this._onAgentHideNativeHighlight,
|
|
);
|
|
agent.removeListener(
|
|
'showNativeHighlight',
|
|
this._onAgentShowNativeHighlight,
|
|
);
|
|
agent.removeListener('shutdown', this._onAgentShutdown);
|
|
|
|
this.setState({devtoolsAgent: null});
|
|
}
|
|
};
|
|
|
|
setSelection(i: number) {
|
|
const hierarchyItem = this.state.hierarchy[i];
|
|
// we pass in ReactNative.findNodeHandle as the method is injected
|
|
const {measure, props, source} = hierarchyItem.getInspectorData(
|
|
ReactNative.findNodeHandle,
|
|
);
|
|
|
|
measure((x, y, width, height, left, top) => {
|
|
this.setState({
|
|
inspected: {
|
|
frame: {left, top, width, height},
|
|
style: props.style,
|
|
source,
|
|
},
|
|
selection: i,
|
|
});
|
|
});
|
|
}
|
|
|
|
onTouchPoint(locationX: number, locationY: number) {
|
|
this._setTouchedViewData = viewData => {
|
|
const {
|
|
hierarchy,
|
|
props,
|
|
selectedIndex,
|
|
source,
|
|
frame,
|
|
pointerY,
|
|
touchedViewTag,
|
|
} = viewData;
|
|
|
|
// Sync the touched view with React DevTools.
|
|
// Note: This is Paper only. To support Fabric,
|
|
// DevTools needs to be updated to not rely on view tags.
|
|
if (this.state.devtoolsAgent && touchedViewTag) {
|
|
this.state.devtoolsAgent.selectNode(
|
|
ReactNative.findNodeHandle(touchedViewTag),
|
|
);
|
|
}
|
|
|
|
this.setState({
|
|
panelPos:
|
|
pointerY > Dimensions.get('window').height / 2 ? 'top' : 'bottom',
|
|
selection: selectedIndex,
|
|
hierarchy,
|
|
inspected: {
|
|
style: props.style,
|
|
frame,
|
|
source,
|
|
},
|
|
});
|
|
};
|
|
getInspectorDataForViewAtPoint(
|
|
this.state.inspectedView,
|
|
locationX,
|
|
locationY,
|
|
viewData => {
|
|
if (this._setTouchedViewData != null) {
|
|
this._setTouchedViewData(viewData);
|
|
this._setTouchedViewData = null;
|
|
}
|
|
},
|
|
);
|
|
}
|
|
|
|
setPerfing(val: boolean) {
|
|
this.setState({
|
|
perfing: val,
|
|
inspecting: false,
|
|
inspected: null,
|
|
networking: false,
|
|
});
|
|
}
|
|
|
|
setInspecting(val: boolean) {
|
|
this.setState({
|
|
inspecting: val,
|
|
inspected: null,
|
|
});
|
|
}
|
|
|
|
setTouchTargeting(val: boolean) {
|
|
Touchable.TOUCH_TARGET_DEBUG = val;
|
|
this.props.onRequestRerenderApp(inspectedView => {
|
|
this.setState({inspectedView});
|
|
});
|
|
}
|
|
|
|
setNetworking(val: boolean) {
|
|
this.setState({
|
|
networking: val,
|
|
perfing: false,
|
|
inspecting: false,
|
|
inspected: null,
|
|
});
|
|
}
|
|
|
|
render(): React.Node {
|
|
const panelContainerStyle =
|
|
this.state.panelPos === 'bottom'
|
|
? {bottom: 0}
|
|
: {top: Platform.OS === 'ios' ? 20 : 0};
|
|
return (
|
|
<View style={styles.container} pointerEvents="box-none">
|
|
{this.state.inspecting && (
|
|
<InspectorOverlay
|
|
inspected={this.state.inspected}
|
|
onTouchPoint={this.onTouchPoint.bind(this)}
|
|
/>
|
|
)}
|
|
<View style={[styles.panelContainer, panelContainerStyle]}>
|
|
<InspectorPanel
|
|
devtoolsIsOpen={!!this.state.devtoolsAgent}
|
|
inspecting={this.state.inspecting}
|
|
perfing={this.state.perfing}
|
|
setPerfing={this.setPerfing.bind(this)}
|
|
setInspecting={this.setInspecting.bind(this)}
|
|
inspected={this.state.inspected}
|
|
hierarchy={this.state.hierarchy}
|
|
selection={this.state.selection}
|
|
setSelection={this.setSelection.bind(this)}
|
|
touchTargeting={Touchable.TOUCH_TARGET_DEBUG}
|
|
setTouchTargeting={this.setTouchTargeting.bind(this)}
|
|
networking={this.state.networking}
|
|
setNetworking={this.setNetworking.bind(this)}
|
|
/>
|
|
</View>
|
|
</View>
|
|
);
|
|
}
|
|
}
|
|
|
|
const styles = StyleSheet.create({
|
|
container: {
|
|
position: 'absolute',
|
|
backgroundColor: 'transparent',
|
|
top: 0,
|
|
left: 0,
|
|
right: 0,
|
|
bottom: 0,
|
|
},
|
|
panelContainer: {
|
|
position: 'absolute',
|
|
left: 0,
|
|
right: 0,
|
|
},
|
|
});
|
|
|
|
module.exports = Inspector;
|