Implemented select-DOM-element button
Brian Vaughn committed
Feb 16, 2019 at 08:29 UTC
1d950981777f2be21e3f8426e96d233e221c5f33
9 files changed
+218
-19
src/backend/agent.js
+56
-2
@@ -28,10 +28,21 @@ export default class Agent extends EventEmitter {
28
bridge.addListener('highlightElementInDOM', this.highlightElementInDOM);
29
bridge.addListener('inspectElement', this.inspectElement);
30
bridge.addListener('selectElement', this.selectElement);
31
+ bridge.addListener('startInspectingDOM', this.startInspectingDOM);
32
+ bridge.addListener('stopInspectingDOM', this.stopInspectingDOM);
33
bridge.addListener('shutdown', this.shutdown);
34
+ }
35
33
- // TODO Listen to bridge for things like selection.
34
- // bridge.addListener('...'), this...);
36
+ getIDForNode(node: Object): number | null {
37
+ for (let rendererID in this._rendererInterfaces) {
38
+ // A renderer will throw if it can't find a fiber for the specified node.
39
+ try {
40
+ // $FlowFixMe
41
+ const renderer = this._rendererInterfaces[rendererID];
42
+ return renderer.getFiberIDFromNative(node, true);
43
+ } catch (e) {}
44
+ }
45
+ return null;
46
}
47
48
highlightElementInDOM = ({
@@ -98,6 +109,20 @@ export default class Agent extends EventEmitter {
109
this.emit('shutdown');
110
};
111
112
+ startInspectingDOM = () => {
113
+ window.addEventListener('click', this._onClick);
114
+ window.addEventListener('mousedown', this._onMouseDown);
115
+ window.addEventListener('mouseover', this._onMouseOver);
116
+ };
117
+
118
+ stopInspectingDOM = () => {
119
+ hideOverlay();
120
+
121
+ window.removeEventListener('click', this._onClick);
122
+ window.removeEventListener('mousedown', this._onMouseDown);
123
+ window.removeEventListener('mouseover', this._onMouseOver);
124
+ };
125
+
126
onHookOperations = (operations: Uint32Array) => {
127
debug('onHookOperations', operations);
128
@@ -107,4 +132,33 @@ export default class Agent extends EventEmitter {
132
// this._bridge.send('operations', operations, [operations.buffer]);
133
this._bridge.send('operations', operations);
134
};
135
+
136
+ _onClick = (event: MouseEvent) => {
137
+ event.preventDefault();
138
+ event.stopPropagation();
139
+
140
+ this.stopInspectingDOM();
141
+ this._bridge.send('stopInspectingDOM');
142
+ };
143
+
144
+ _onMouseDown = (event: MouseEvent) => {
145
+ event.preventDefault();
146
+ event.stopPropagation();
147
+
148
+ const target = ((event.target: any): HTMLElement);
149
+ const id = this.getIDForNode(target);
150
+
151
+ if (id !== null) {
152
+ this._bridge.send('selectFiber', id);
153
+ }
154
+ };
155
+
156
+ _onMouseOver = (event: MouseEvent) => {
157
+ event.preventDefault();
158
+ event.stopPropagation();
159
+
160
+ const target = ((event.target: any): HTMLElement);
161
+
162
+ showOverlay(target, target.tagName.toLowerCase());
163
+ };
164
}
src/backend/renderer.js
+12
-5
@@ -826,11 +826,18 @@ export function attach(
826
return null;
827
}
828
}
829
- function getReactElementFromNative(hostInstance) {
830
- const fiber = renderer.findFiberByHostInstance(hostInstance);
829
+ function getFiberIDFromNative(
830
+ hostInstance,
831
+ findNearestUnfilteredAncestor = false
832
+ ) {
833
+ let fiber = renderer.findFiberByHostInstance(hostInstance);
834
if (fiber != null) {
832
- const primaryFiber = getPrimaryFiber(((fiber: any): Fiber));
833
- return primaryFiber;
835
+ if (findNearestUnfilteredAncestor) {
836
+ while (fiber !== null && shouldFilterFiber(fiber)) {
837
+ fiber = fiber.return;
838
+ }
839
+ }
840
+ return getFiberID(getPrimaryFiber(((fiber: any): Fiber)));
841
}
842
return null;
843
}
@@ -1150,8 +1157,8 @@ export function attach(
1157
}
1158
1159
return {
1160
+ getFiberIDFromNative,
1161
getNativeFromReactElement,
1154
- getReactElementFromNative,
1162
handleCommitFiberRoot,
1163
handleCommitFiberUnmount,
1164
inspectElement,
src/backend/types.js
+4
-1
@@ -46,7 +46,10 @@ export type ReactRenderer = {
46
export type RendererInterface = {
47
cleanup: () => void,
48
getNativeFromReactElement?: ?(component: Fiber) => ?NativeType,
49
- getReactElementFromNative?: ?(component: NativeType) => ?Fiber,
49
+ getFiberIDFromNative: (
50
+ component: NativeType,
51
+ findNearestUnfilteredAncestor?: boolean
52
+ ) => number | null,
53
handleCommitFiberRoot: (fiber: Object) => void,
54
handleCommitFiberUnmount: (fiber: Object) => void,
55
inspectElement: (id: number) => InspectedElement | null,
src/devtools/store.js
+1
-1
@@ -130,7 +130,7 @@ export default class Store extends EventEmitter {
130
const element = this._idToElement.get(id);
131
132
if (element == null) {
133
- console.warn(`No element found with id "${id}`);
133
+ console.warn(`No element found with id "${id}"`);
134
135
return null;
136
}
src/devtools/views/InspectHostNodesToggle.js
new
+41
@@ -0,0 +1,41 @@
1
+// @flow
2
+
3
+import React, { useCallback, useContext, useEffect, useState } from 'react';
4
+import { BridgeContext } from './context';
5
+import Toggle from './Toggle';
6
+import ButtonIcon from './ButtonIcon';
7
+
8
+export default function InspectHostNodesToggle() {
9
+ const [isInspecting, setIsInspecting] = useState(false);
10
+ const bridge = useContext(BridgeContext);
11
+
12
+ const handleChange = useCallback(
13
+ (isChecked: boolean) => {
14
+ setIsInspecting(isChecked);
15
+
16
+ if (isChecked) {
17
+ bridge.send('startInspectingDOM');
18
+ } else {
19
+ bridge.send('stopInspectingDOM');
20
+ }
21
+ },
22
+ [bridge]
23
+ );
24
+
25
+ useEffect(() => {
26
+ const onStopInspectingDOM = () => setIsInspecting(false);
27
+ bridge.addListener('stopInspectingDOM', onStopInspectingDOM);
28
+ return () =>
29
+ bridge.removeListener('stopInspectingDOM', onStopInspectingDOM);
30
+ }, [bridge]);
31
+
32
+ return (
33
+ <Toggle
34
+ onChange={handleChange}
35
+ isChecked={isInspecting}
36
+ title="Select an element in the page to inspect it"
37
+ >
38
+ <ButtonIcon type="search" />
39
+ </Toggle>
40
+ );
41
+}
src/devtools/views/Toggle.css
new
+40
@@ -0,0 +1,40 @@
1
+.ToggleDisabled,
2
+.ToggleOn,
3
+.ToggleOff {
4
+ border-radius: 0.25rem;
5
+ display: inline-flex;
6
+ align-items: center;
7
+ padding: 0.25rem;
8
+ cursor: pointer;
9
+ flex: 0 0 auto;
10
+}
11
+
12
+.ToggleOff {
13
+ border: none;
14
+ background: var(--color-button-background);
15
+ color: var(--color-button);
16
+}
17
+.ToggleOff:hover {
18
+ background: var(--color-button-background-hover);
19
+ color: var(--color-button-hover);
20
+}
21
+
22
+.ToggleOn,
23
+.ToggleOn:active,
24
+.ToggleOn:focus {
25
+ background: var(--color-button-background-focus);
26
+ color: var(--color-button-focus);
27
+ outline: none;
28
+}
29
+
30
+.ToggleDisabled {
31
+ background: var(--color-button-background);
32
+ color: var(--color-button-disabled);
33
+ cursor: default;
34
+}
35
+
36
+.Input {
37
+ width: 0;
38
+ margin: 0;
39
+ opacity: 0;
40
+}
src/devtools/views/Toggle.js
new
+50
@@ -0,0 +1,50 @@
1
+// @flow
2
+
3
+import React, { useCallback } from 'react';
4
+
5
+import styles from './Toggle.css';
6
+
7
+type Props = {
8
+ children: React$Node,
9
+ className?: string,
10
+ isChecked: boolean,
11
+ isDisabled?: boolean,
12
+ onChange: (isChecked: boolean) => void,
13
+};
14
+
15
+export default function Toggle({
16
+ children,
17
+ className = '',
18
+ isDisabled = false,
19
+ isChecked,
20
+ onChange,
21
+}: Props) {
22
+ let defaultClassName;
23
+ if (isDisabled) {
24
+ defaultClassName = styles.ToggleDisabled;
25
+ } else if (isChecked) {
26
+ defaultClassName = styles.ToggleOn;
27
+ } else {
28
+ defaultClassName = styles.ToggleOff;
29
+ }
30
+
31
+ const handleChange = useCallback(
32
+ ({ target }) => {
33
+ onChange(target.checked);
34
+ },
35
+ [onChange]
36
+ );
37
+
38
+ return (
39
+ <label className={`${defaultClassName} ${className}`}>
40
+ <input
41
+ type="checkbox"
42
+ className={styles.Input}
43
+ checked={isChecked}
44
+ disabled={isDisabled}
45
+ onChange={handleChange}
46
+ />
47
+ {children}
48
+ </label>
49
+ );
50
+}
src/devtools/views/Tree.js
+2
-8
@@ -11,9 +11,8 @@ import AutoSizer from 'react-virtualized-auto-sizer';
11
import { FixedSizeList } from 'react-window';
12
import { TreeContext } from './TreeContext';
13
import { SettingsContext } from './SettingsContext';
14
-import Button from './Button';
15
-import ButtonIcon from './ButtonIcon';
14
import Element from './Element';
15
+import InspectHostNodesToggle from './InspectHostNodesToggle';
16
import OwnersStack from './OwnersStack';
17
import SearchInput from './SearchInput';
18
@@ -85,12 +84,7 @@ export default function Tree(props: Props) {
84
<div className={styles.Tree}>
85
<div className={styles.SearchInput}>
86
{ownerStack.length > 0 ? <OwnersStack /> : <SearchInput />}
88
- <Button
89
- className={styles.IconButton}
90
- title="Select an element in the page to inspect it"
91
- >
92
- <ButtonIcon type="search" />
93
- </Button>
87
+ <InspectHostNodesToggle />
88
</div>
89
<div className={styles.AutoSizerWrapper}>
90
<AutoSizer>
src/devtools/views/TreeContext.js
+12
-2
@@ -26,7 +26,7 @@ import React, {
26
useReducer,
27
} from 'react';
28
import { createRegExp } from './utils';
29
-import { StoreContext } from './context';
29
+import { BridgeContext, StoreContext } from './context';
30
import Store from '../store';
31
32
import type { Element } from 'src/devtools/types';
@@ -470,7 +470,9 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
470
471
// TODO Remove TreeContextController wrapper element once global ConsearchText.write API exists.
472
function TreeContextController({ children }: {| children: React$Node |}) {
473
+ const bridge = useContext(BridgeContext);
474
const store = useContext(StoreContext);
475
+
476
const initialRevision = useMemo(() => store.revision, [store]);
477
478
// This reducer is created inline because it needs access to the Store.
@@ -596,6 +598,14 @@ function TreeContextController({ children }: {| children: React$Node |}) {
598
[state]
599
);
600
601
+ // Listen for host element selections.
602
+ useEffect(() => {
603
+ const handleSelectFiber = (id: number) =>
604
+ dispatch({ type: 'SELECT_ELEMENT_BY_ID', payload: id });
605
+ bridge.addListener('selectFiber', handleSelectFiber);
606
+ return () => bridge.removeListener('selectFiber', handleSelectFiber);
607
+ }, [bridge, dispatch]);
608
+
609
// Mutations to the underlying tree may impact this context (e.g. search results, selection state).
610
useEffect(() => {
611
const handleStoreMutated = ([
@@ -622,7 +632,7 @@ function TreeContextController({ children }: {| children: React$Node |}) {
632
store.addListener('mutated', handleStoreMutated);
633
634
return () => store.removeListener('mutated', handleStoreMutated);
625
- }, [store]);
635
+ }, [dispatch, store]);
636
637
return <TreeContext.Provider value={value}>{children}</TreeContext.Provider>;
638
}