Files
react/src/backend/agent.js
T

229 lines
6.5 KiB
JavaScript

// @flow
import EventEmitter from 'events';
import { __DEBUG__ } from '../constants';
import { hideOverlay, showOverlay } from './views/Highlighter';
import type { RendererID, RendererInterface } from './types';
import type { Bridge } from '../types';
const debug = (methodName, ...args) => {
if (__DEBUG__) {
console.log(
`%cAgent %c${methodName}`,
'color: purple; font-weight: bold;',
'font-weight: bold;',
...args
);
}
};
type InspectSelectParams = {|
id: number,
rendererID: number,
|};
type OverrideHookParams = {|
id: number,
index: number,
path: Array<string | number>,
rendererID: number,
value: any,
|};
type SetInParams = {|
id: number,
path: Array<string | number>,
rendererID: number,
value: any,
|};
export default class Agent extends EventEmitter {
_bridge: Bridge = ((null: any): Bridge);
_rendererInterfaces: { [key: RendererID]: RendererInterface } = {};
addBridge(bridge: Bridge) {
this._bridge = bridge;
bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
bridge.addListener('inspectElement', this.inspectElement);
bridge.addListener('overrideContext', this.overrideContext);
bridge.addListener('overrideHook', this.overrideHook);
bridge.addListener('overrideProps', this.overrideProps);
bridge.addListener('overrideState', this.overrideState);
bridge.addListener('selectElement', this.selectElement);
bridge.addListener('startInspectingDOM', this.startInspectingDOM);
bridge.addListener('stopInspectingDOM', this.stopInspectingDOM);
bridge.addListener('shutdown', this.shutdown);
}
getIDForNode(node: Object): number | null {
for (let rendererID in this._rendererInterfaces) {
// A renderer will throw if it can't find a fiber for the specified node.
try {
// $FlowFixMe
const renderer = this._rendererInterfaces[rendererID];
return renderer.getFiberIDFromNative(node, true);
} catch (e) {}
}
return null;
}
highlightElementInDOM = ({
displayName,
id,
rendererID,
}: {
displayName: string,
id: number,
rendererID: number,
}) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
}
let node: HTMLElement | null = null;
if (
renderer !== null &&
typeof renderer.getNativeFromReactElement === 'function'
) {
node = ((renderer.getNativeFromReactElement(id): any): HTMLElement);
}
if (node != null) {
if (typeof node.scrollIntoView === 'function') {
// If the node isn't visible show it before highlighting it.
// We may want to reconsider this; it might be a little disruptive.
node.scrollIntoView({ block: 'nearest', inline: 'nearest' });
}
showOverlay(((node: any): HTMLElement), displayName);
} else {
hideOverlay();
}
};
inspectElement = ({ id, rendererID }: InspectSelectParams) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
} else {
this._bridge.send('inspectedElement', renderer.inspectElement(id));
}
};
selectElement = ({ id, rendererID }: InspectSelectParams) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
} else {
this._bridge.send('selectElement', renderer.selectElement(id));
}
};
overrideContext = ({ id, path, rendererID, value }: SetInParams) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
} else {
renderer.setInContext(id, path, value);
}
};
overrideHook = ({
id,
index,
path,
rendererID,
value,
}: OverrideHookParams) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
} else {
renderer.setInHook(id, index, path, value);
}
};
overrideProps = ({ id, path, rendererID, value }: SetInParams) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
} else {
renderer.setInProps(id, path, value);
}
};
overrideState = ({ id, path, rendererID, value }: SetInParams) => {
const renderer = this._rendererInterfaces[rendererID];
if (renderer == null) {
console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
} else {
renderer.setInState(id, path, value);
}
};
setRendererInterface(
rendererID: RendererID,
rendererInterface: RendererInterface
) {
this._rendererInterfaces[rendererID] = rendererInterface;
}
shutdown = () => {
this.emit('shutdown');
};
startInspectingDOM = () => {
window.addEventListener('click', this._onClick);
window.addEventListener('mousedown', this._onMouseDown);
window.addEventListener('mouseover', this._onMouseOver);
};
stopInspectingDOM = () => {
hideOverlay();
window.removeEventListener('click', this._onClick);
window.removeEventListener('mousedown', this._onMouseDown);
window.removeEventListener('mouseover', this._onMouseOver);
};
onHookOperations = (operations: Uint32Array) => {
debug('onHookOperations', operations);
// TODO The chrome.runtime does not currently support transferables; it forces JSON serialization.
// The Store has a fallback in place that parses the message as JSON if the type isn't an array.
this._bridge.send('operations', operations, [operations.buffer]);
};
_onClick = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
this.stopInspectingDOM();
this._bridge.send('stopInspectingDOM');
};
_onMouseDown = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
const target = ((event.target: any): HTMLElement);
const id = this.getIDForNode(target);
if (id !== null) {
this._bridge.send('selectFiber', id);
}
};
_onMouseOver = (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
const target = ((event.target: any): HTMLElement);
showOverlay(target, target.tagName.toLowerCase());
};
}