Implemented select-DOM-element button

This commit is contained in:
Brian Vaughn committed 2019-02-16 08:29:26 -08:00
1 parent 2fbacbff01
commit 1d95098177
9 files changed
+218 -19

No files matched your search

+56 -2
View File
@@ -28,10 +28,21 @@ export default class Agent extends EventEmitter {
bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
bridge.addListener('inspectElement', this.inspectElement);
bridge.addListener('selectElement', this.selectElement);
bridge.addListener('startInspectingDOM', this.startInspectingDOM);
bridge.addListener('stopInspectingDOM', this.stopInspectingDOM);
bridge.addListener('shutdown', this.shutdown);
}
// TODO Listen to bridge for things like selection.
// bridge.addListener('...'), this...);
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 = ({
@@ -98,6 +109,20 @@ export default class Agent extends EventEmitter {
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);
@@ -107,4 +132,33 @@ export default class Agent extends EventEmitter {
// this._bridge.send('operations', operations, [operations.buffer]);
this._bridge.send('operations', operations);
};
_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());
};
}
+12 -5
View File
@@ -826,11 +826,18 @@ export function attach(
return null;
}
}
function getReactElementFromNative(hostInstance) {
const fiber = renderer.findFiberByHostInstance(hostInstance);
function getFiberIDFromNative(
hostInstance,
findNearestUnfilteredAncestor = false
) {
let fiber = renderer.findFiberByHostInstance(hostInstance);
if (fiber != null) {
const primaryFiber = getPrimaryFiber(((fiber: any): Fiber));
return primaryFiber;
if (findNearestUnfilteredAncestor) {
while (fiber !== null && shouldFilterFiber(fiber)) {
fiber = fiber.return;
}
}
return getFiberID(getPrimaryFiber(((fiber: any): Fiber)));
}
return null;
}
@@ -1150,8 +1157,8 @@ export function attach(
}
return {
getFiberIDFromNative,
getNativeFromReactElement,
getReactElementFromNative,
handleCommitFiberRoot,
handleCommitFiberUnmount,
inspectElement,
+4 -1
View File
@@ -46,7 +46,10 @@ export type ReactRenderer = {
export type RendererInterface = {
cleanup: () => void,
getNativeFromReactElement?: ?(component: Fiber) => ?NativeType,
getReactElementFromNative?: ?(component: NativeType) => ?Fiber,
getFiberIDFromNative: (
component: NativeType,
findNearestUnfilteredAncestor?: boolean
) => number | null,
handleCommitFiberRoot: (fiber: Object) => void,
handleCommitFiberUnmount: (fiber: Object) => void,
inspectElement: (id: number) => InspectedElement | null,
+1 -1
View File
@@ -130,7 +130,7 @@ export default class Store extends EventEmitter {
const element = this._idToElement.get(id);
if (element == null) {
console.warn(`No element found with id "${id}`);
console.warn(`No element found with id "${id}"`);
return null;
}
@@ -0,0 +1,41 @@
// @flow
import React, { useCallback, useContext, useEffect, useState } from 'react';
import { BridgeContext } from './context';
import Toggle from './Toggle';
import ButtonIcon from './ButtonIcon';
export default function InspectHostNodesToggle() {
const [isInspecting, setIsInspecting] = useState(false);
const bridge = useContext(BridgeContext);
const handleChange = useCallback(
(isChecked: boolean) => {
setIsInspecting(isChecked);
if (isChecked) {
bridge.send('startInspectingDOM');
} else {
bridge.send('stopInspectingDOM');
}
},
[bridge]
);
useEffect(() => {
const onStopInspectingDOM = () => setIsInspecting(false);
bridge.addListener('stopInspectingDOM', onStopInspectingDOM);
return () =>
bridge.removeListener('stopInspectingDOM', onStopInspectingDOM);
}, [bridge]);
return (
<Toggle
onChange={handleChange}
isChecked={isInspecting}
title="Select an element in the page to inspect it"
>
<ButtonIcon type="search" />
</Toggle>
);
}
+40
View File
@@ -0,0 +1,40 @@
.ToggleDisabled,
.ToggleOn,
.ToggleOff {
border-radius: 0.25rem;
display: inline-flex;
align-items: center;
padding: 0.25rem;
cursor: pointer;
flex: 0 0 auto;
}
.ToggleOff {
border: none;
background: var(--color-button-background);
color: var(--color-button);
}
.ToggleOff:hover {
background: var(--color-button-background-hover);
color: var(--color-button-hover);
}
.ToggleOn,
.ToggleOn:active,
.ToggleOn:focus {
background: var(--color-button-background-focus);
color: var(--color-button-focus);
outline: none;
}
.ToggleDisabled {
background: var(--color-button-background);
color: var(--color-button-disabled);
cursor: default;
}
.Input {
width: 0;
margin: 0;
opacity: 0;
}
+50
View File
@@ -0,0 +1,50 @@
// @flow
import React, { useCallback } from 'react';
import styles from './Toggle.css';
type Props = {
children: React$Node,
className?: string,
isChecked: boolean,
isDisabled?: boolean,
onChange: (isChecked: boolean) => void,
};
export default function Toggle({
children,
className = '',
isDisabled = false,
isChecked,
onChange,
}: Props) {
let defaultClassName;
if (isDisabled) {
defaultClassName = styles.ToggleDisabled;
} else if (isChecked) {
defaultClassName = styles.ToggleOn;
} else {
defaultClassName = styles.ToggleOff;
}
const handleChange = useCallback(
({ target }) => {
onChange(target.checked);
},
[onChange]
);
return (
<label className={`${defaultClassName} ${className}`}>
<input
type="checkbox"
className={styles.Input}
checked={isChecked}
disabled={isDisabled}
onChange={handleChange}
/>
{children}
</label>
);
}
+2 -8
View File
@@ -11,9 +11,8 @@ import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList } from 'react-window';
import { TreeContext } from './TreeContext';
import { SettingsContext } from './SettingsContext';
import Button from './Button';
import ButtonIcon from './ButtonIcon';
import Element from './Element';
import InspectHostNodesToggle from './InspectHostNodesToggle';
import OwnersStack from './OwnersStack';
import SearchInput from './SearchInput';
@@ -85,12 +84,7 @@ export default function Tree(props: Props) {
<div className={styles.Tree}>
<div className={styles.SearchInput}>
{ownerStack.length > 0 ? <OwnersStack /> : <SearchInput />}
<Button
className={styles.IconButton}
title="Select an element in the page to inspect it"
>
<ButtonIcon type="search" />
</Button>
<InspectHostNodesToggle />
</div>
<div className={styles.AutoSizerWrapper}>
<AutoSizer>
+12 -2
View File
@@ -26,7 +26,7 @@ import React, {
useReducer,
} from 'react';
import { createRegExp } from './utils';
import { StoreContext } from './context';
import { BridgeContext, StoreContext } from './context';
import Store from '../store';
import type { Element } from 'src/devtools/types';
@@ -470,7 +470,9 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
// TODO Remove TreeContextController wrapper element once global ConsearchText.write API exists.
function TreeContextController({ children }: {| children: React$Node |}) {
const bridge = useContext(BridgeContext);
const store = useContext(StoreContext);
const initialRevision = useMemo(() => store.revision, [store]);
// This reducer is created inline because it needs access to the Store.
@@ -596,6 +598,14 @@ function TreeContextController({ children }: {| children: React$Node |}) {
[state]
);
// Listen for host element selections.
useEffect(() => {
const handleSelectFiber = (id: number) =>
dispatch({ type: 'SELECT_ELEMENT_BY_ID', payload: id });
bridge.addListener('selectFiber', handleSelectFiber);
return () => bridge.removeListener('selectFiber', handleSelectFiber);
}, [bridge, dispatch]);
// Mutations to the underlying tree may impact this context (e.g. search results, selection state).
useEffect(() => {
const handleStoreMutated = ([
@@ -622,7 +632,7 @@ function TreeContextController({ children }: {| children: React$Node |}) {
store.addListener('mutated', handleStoreMutated);
return () => store.removeListener('mutated', handleStoreMutated);
}, [store]);
}, [dispatch, store]);
return <TreeContext.Provider value={value}>{children}</TreeContext.Provider>;
}