@samitouri / QOS-React / commits / 1d95098177

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 }