Implemented view-source button for DOM extension
Brian Vaughn committed
Feb 22, 2019 at 13:24 UTC
53632883b6dc3c40394f44873ce4e22a9f7e5290
8 files changed
+117
-15
shells/browser/shared/src/panels/utils.js
+26
@@ -1,3 +1,5 @@
1
+/* global chrome */
2
+
3
import { createElement } from 'react';
4
import { createRoot, flushSync } from 'react-dom';
5
import DevTools from 'src/devtools/views/DevTools';
@@ -23,6 +25,29 @@ export function createPanel(defaultTab) {
25
}
26
};
27
28
+ function viewElementSource(id) {
29
+ if (injectedBridge == null || injectedStore == null) {
30
+ return;
31
+ }
32
+
33
+ const rendererID = injectedStore.getRendererIDForElement(id);
34
+ if (rendererID != null) {
35
+ // Ask the renderer interface to determine the component function,
36
+ // and store it as a global variable on the window
37
+ injectedBridge.send('viewElementSource', { id, rendererID });
38
+
39
+ setTimeout(() => {
40
+ // Ask Chrome to display the location of the component function,
41
+ // assuming the renderer found one.
42
+ chrome.devtools.inspectedWindow.eval(`
43
+ if (window.$type != null) {
44
+ inspect(window.$type);
45
+ }
46
+ `);
47
+ }, 100);
48
+ }
49
+ }
50
+
51
function injectAndInit() {
52
const container = ((document.getElementById(
53
'container'
@@ -40,6 +65,7 @@ export function createPanel(defaultTab) {
65
defaultTab,
66
showTabBar: false,
67
store: injectedStore,
68
+ viewElementSource,
69
})
70
);
71
}
src/backend/agent.js
+10
@@ -55,6 +55,7 @@ export default class Agent extends EventEmitter {
55
bridge.addListener('startInspectingDOM', this.startInspectingDOM);
56
bridge.addListener('stopInspectingDOM', this.stopInspectingDOM);
57
bridge.addListener('shutdown', this.shutdown);
58
+ bridge.addListener('viewElementSource', this.viewElementSource);
59
}
60
61
getIDForNode(node: Object): number | null {
@@ -189,6 +190,15 @@ export default class Agent extends EventEmitter {
190
window.removeEventListener('mouseover', this._onMouseOver);
191
};
192
193
+ viewElementSource = ({ id, rendererID }: InspectSelectParams) => {
194
+ const renderer = this._rendererInterfaces[rendererID];
195
+ if (renderer == null) {
196
+ console.warn(`Invalid renderer id "${rendererID}" for element "${id}"`);
197
+ } else {
198
+ renderer.prepareViewElementSource(id);
199
+ }
200
+ };
201
+
202
onHookOperations = (operations: Uint32Array) => {
203
debug('onHookOperations', operations);
204
src/backend/renderer.js
+46
-7
@@ -1028,16 +1028,24 @@ export function attach(
1028
return;
1029
}
1030
1031
- switch (fiber.tag) {
1031
+ const { memoizedProps, stateNode, tag, type } = fiber;
1032
+
1033
+ switch (tag) {
1034
case ClassComponent:
1035
case IncompleteClassComponent:
1036
case IndeterminateComponent:
1035
- global.$r = fiber.stateNode;
1037
+ global.$r = stateNode;
1038
break;
1039
case FunctionComponent:
1040
global.$r = {
1039
- props: fiber.memoizedProps,
1040
- type: fiber.type,
1041
+ props: memoizedProps,
1042
+ type,
1043
+ };
1044
+ break;
1045
+ case ForwardRef:
1046
+ global.$r = {
1047
+ props: memoizedProps,
1048
+ type: type.render,
1049
};
1050
break;
1051
default:
@@ -1046,6 +1054,30 @@ export function attach(
1054
}
1055
}
1056
1057
+ function prepareViewElementSource(id: number): void {
1058
+ let fiber = idToFiberMap.get(id);
1059
+
1060
+ if (fiber == null) {
1061
+ console.warn(`Could not find Fiber with id "${id}"`);
1062
+ return;
1063
+ }
1064
+
1065
+ switch (fiber.tag) {
1066
+ case ClassComponent:
1067
+ case IncompleteClassComponent:
1068
+ case IndeterminateComponent:
1069
+ case FunctionComponent:
1070
+ global.$type = fiber.type;
1071
+ break;
1072
+ case ForwardRef:
1073
+ global.$type = fiber.type.render;
1074
+ break;
1075
+ default:
1076
+ global.$type = null;
1077
+ break;
1078
+ }
1079
+ }
1080
+
1081
function inspectElement(id: number): InspectedElement | null {
1082
let fiber = idToFiberMap.get(id);
1083
@@ -1075,13 +1107,16 @@ export function attach(
1107
1108
const typeSymbol = getTypeSymbol(fiber);
1109
1110
+ let canViewSource = false;
1111
let context = null;
1112
if (
1113
tag === ClassComponent ||
1114
tag === FunctionComponent ||
1115
tag === IncompleteClassComponent ||
1083
- tag === IndeterminateComponent
1116
+ tag === IndeterminateComponent ||
1117
+ tag === ForwardRef
1118
) {
1119
+ canViewSource = true;
1120
if (stateNode && stateNode.context != null) {
1121
context = stateNode.context;
1122
}
@@ -1150,6 +1185,9 @@ export function attach(
1185
// Does the current renderer support editable function props?
1186
canEditFunctionProps: typeof overrideProps === 'function',
1187
1188
+ // Can view component source location.
1189
+ canViewSource,
1190
+
1191
// Inspectable properties.
1192
// TODO Review sanitization approach for the below inspectable values.
1193
context,
@@ -1227,14 +1265,15 @@ export function attach(
1265
}
1266
1267
return {
1268
+ cleanup,
1269
getFiberIDFromNative,
1270
getNativeFromReactElement,
1271
handleCommitFiberRoot,
1272
handleCommitFiberUnmount,
1273
inspectElement,
1235
- selectElement,
1236
- cleanup,
1274
+ prepareViewElementSource,
1275
renderer,
1276
+ selectElement,
1277
setInContext,
1278
setInHook,
1279
setInProps,
src/backend/types.js
+1
@@ -63,6 +63,7 @@ export type RendererInterface = {
63
handleCommitFiberRoot: (fiber: Object) => void,
64
handleCommitFiberUnmount: (fiber: Object) => void,
65
inspectElement: (id: number) => InspectedElement | null,
66
+ prepareViewElementSource: (id: number) => void,
67
renderer: ReactRenderer | null,
68
selectElement: (id: number) => void,
69
setInContext: (id: number, path: Array<string | number>, value: any) => void,
src/devtools/types.js
+3
@@ -54,6 +54,9 @@ export type InspectedElement = {|
54
// Does the current renderer support editable function props?
55
canEditFunctionProps: boolean,
56
57
+ // Can view component source location.
58
+ canViewSource: boolean,
59
+
60
// Inspectable properties.
61
context: Object | null,
62
hooks: Object | null,
src/devtools/views/DevTools.js
+3
-1
@@ -27,6 +27,7 @@ export type Props = {|
27
browserTheme: BrowserTheme,
28
showTabBar?: boolean,
29
store: Store,
30
+ viewElementSource?: ?Function,
31
|};
32
33
export default function DevTools({
@@ -36,6 +37,7 @@ export default function DevTools({
37
browserTheme = 'light',
38
showTabBar = false,
39
store,
40
+ viewElementSource = null,
41
}: Props) {
42
const [tab, setTab] = useState(defaultTab);
43
@@ -57,7 +59,7 @@ export default function DevTools({
59
<BridgeContext.Provider value={bridge}>
60
<StoreContext.Provider value={store}>
61
<SettingsContextController browserTheme={browserTheme}>
60
- <TreeContextController>
62
+ <TreeContextController viewElementSource={viewElementSource}>
63
<div className={styles.DevTools}>
64
{showTabBar && (
65
<div className={styles.TabBar}>
src/devtools/views/SelectedElement.js
+15
-6
@@ -23,15 +23,16 @@ import type { DehydratedData, Element } from 'src/devtools/types';
23
export type Props = {||};
24
25
export default function SelectedElement(_: Props) {
26
- const { selectedElementID } = useContext(TreeContext);
26
+ const { selectedElementID, viewElementSource } = useContext(TreeContext);
27
const bridge = useContext(BridgeContext);
28
const store = useContext(StoreContext);
29
+
30
const element =
31
selectedElementID !== null ? store.getElementByID(selectedElementID) : null;
32
33
const inspectedElement = useInspectedElement(selectedElementID);
34
34
- const handleClick = useCallback(() => {
35
+ const highlightElement = useCallback(() => {
36
if (element !== null && selectedElementID !== null) {
37
const rendererID =
38
store.getRendererIDForElement(selectedElementID) || null;
@@ -45,7 +46,11 @@ export default function SelectedElement(_: Props) {
46
}
47
}, [bridge, element, selectedElementID, store]);
48
48
- // TODO Make "view source" button work
49
+ const viewSource = useCallback(() => {
50
+ if (viewElementSource != null && selectedElementID !== null) {
51
+ viewElementSource(selectedElementID);
52
+ }
53
+ }, [selectedElementID, viewElementSource]);
54
55
if (element === null) {
56
return (
@@ -55,7 +60,10 @@ export default function SelectedElement(_: Props) {
60
);
61
}
62
58
- const source = inspectedElement ? inspectedElement.source : null;
63
+ const canViewSource =
64
+ inspectedElement &&
65
+ inspectedElement.canViewSource &&
66
+ viewElementSource !== null;
67
68
return (
69
<div className={styles.SelectedElement}>
@@ -68,14 +76,15 @@ export default function SelectedElement(_: Props) {
76
77
<Button
78
className={styles.IconButton}
71
- onClick={handleClick}
79
+ onClick={highlightElement}
80
title="Highlight this element in the page"
81
>
82
<ButtonIcon type="view-dom" />
83
</Button>
76
- {source !== null && (
84
+ {canViewSource && (
85
<Button
86
className={styles.IconButton}
87
+ onClick={viewSource}
88
title="View source for this element"
89
>
90
<ButtonIcon type="view-source" />
src/devtools/views/TreeContext.js
+13
-1
@@ -56,6 +56,9 @@ type Context = {|
56
ownerStackIndex: number | null,
57
resetOwnerStack(): void,
58
selectOwner(id: number): void,
59
+
60
+ // Injected by parent HTML/JavaScript
61
+ viewElementSource: Function | null,
62
|};
63
64
const TreeContext = createContext<Context>(((null: any): Context));
@@ -473,8 +476,13 @@ function reduceOwnersState(store: Store, state: State, action: Action): State {
476
};
477
}
478
479
+type Props = {|
480
+ children: React$Node,
481
+ viewElementSource: Function | null,
482
+|};
483
+
484
// TODO Remove TreeContextController wrapper element once global ConsearchText.write API exists.
477
-function TreeContextController({ children }: {| children: React$Node |}) {
485
+function TreeContextController({ children, viewElementSource }: Props) {
486
const bridge = useContext(BridgeContext);
487
const store = useContext(StoreContext);
488
@@ -599,6 +607,9 @@ function TreeContextController({ children }: {| children: React$Node |}) {
607
ownerStackIndex: state.ownerStackIndex,
608
resetOwnerStack,
609
selectOwner,
610
+
611
+ // Injected by parent HTML/JavaScript
612
+ viewElementSource,
613
}),
614
[
615
getElementAtIndex,
@@ -612,6 +623,7 @@ function TreeContextController({ children }: {| children: React$Node |}) {
623
selectPreviousElementInTree,
624
setSearchText,
625
state,
626
+ viewElementSource,
627
]
628
);
629