Experimenting with portals
Brian Vaughn committed
Mar 17, 2019 at 13:52 UTC
f3f4643a3af686b1e4d8711df17f6465a855925c
15 files changed
+136
-223
shells/browser/chrome/manifest.json
+1
-7
@@ -27,13 +27,7 @@
27
"devtools_page": "main.html",
28
29
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
30
- "web_accessible_resources": [
31
- "elements.html",
32
- "main.html",
33
- "profiler.html",
34
- "settings.html",
35
- "build/backend.js"
36
- ],
30
+ "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"],
31
32
"background": {
33
"scripts": ["build/background.js"],
shells/browser/firefox/manifest.json
+1
-7
@@ -33,13 +33,7 @@
33
"devtools_page": "main.html",
34
35
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
36
- "web_accessible_resources": [
37
- "elements.html",
38
- "main.html",
39
- "profiler.html",
40
- "settings.html",
41
- "build/backend.js"
42
- ],
36
+ "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"],
37
38
"background": {
39
"scripts": ["build/background.js"],
shells/browser/shared/build.js
+1
-8
@@ -7,14 +7,7 @@ const { join } = require('path');
7
8
// These files are copied along with Webpack-bundled files
9
// to produce the final web extension
10
-const STATIC_FILES = [
11
- 'icons',
12
- 'popups',
13
- 'elements.html',
14
- 'main.html',
15
- 'profiler.html',
16
- 'settings.html',
17
-];
10
+const STATIC_FILES = ['icons', 'popups', 'main.html', 'panel.html'];
11
12
const preProcess = async (destinationPath, tempPath) => {
13
await remove(destinationPath); // Clean up from previously completed builds
shells/browser/shared/panel.html
renamed
+1
-1
@@ -27,6 +27,6 @@
27
<body>
28
<!-- main react mount point -->
29
<div id="container">Unable to find React on the page.</div>
30
- <script src="./build/elements.js"></script>
30
+ <script src="./build/panel.js"></script>
31
</body>
32
</html>
shells/browser/shared/profiler.html
deleted
-32
@@ -1,32 +0,0 @@
1
-<!doctype html>
2
-<html style="display: flex">
3
- <head>
4
- <meta charset="utf8">
5
- <style>
6
- html {
7
- display: flex;
8
- }
9
- body {
10
- margin: 0;
11
- padding: 0;
12
- flex: 1;
13
- display: flex;
14
- }
15
- #container {
16
- display: flex;
17
- flex: 1;
18
- width: 100%;
19
- position: fixed;
20
- top: 0;
21
- left: 0;
22
- right: 0;
23
- bottom: 0;
24
- }
25
- </style>
26
- </head>
27
- <body>
28
- <!-- main react mount point -->
29
- <div id="container">Unable to find React on the page.</div>
30
- <script src="./build/profiler.js"></script>
31
- </body>
32
-</html>
shells/browser/shared/settings.html
deleted
-32
@@ -1,32 +0,0 @@
1
-<!doctype html>
2
-<html style="display: flex">
3
- <head>
4
- <meta charset="utf8">
5
- <style>
6
- html {
7
- display: flex;
8
- }
9
- body {
10
- margin: 0;
11
- padding: 0;
12
- flex: 1;
13
- display: flex;
14
- }
15
- #container {
16
- display: flex;
17
- flex: 1;
18
- width: 100%;
19
- position: fixed;
20
- top: 0;
21
- left: 0;
22
- right: 0;
23
- bottom: 0;
24
- }
25
- </style>
26
- </head>
27
- <body>
28
- <!-- main react mount point -->
29
- <div id="container">Unable to find React on the page.</div>
30
- <script src="./build/settings.js"></script>
31
- </body>
32
-</html>
shells/browser/shared/src/main.js
+76
-51
@@ -1,8 +1,16 @@
1
/* global chrome */
2
3
+import { createElement } from 'react';
4
+import { unstable_createRoot as createRoot } from 'react-dom';
5
import Bridge from 'src/bridge';
6
import Store from 'src/devtools/Store';
7
import inject from './inject';
8
+import {
9
+ createViewElementSource,
10
+ getBrowserName,
11
+ getBrowserTheme,
12
+} from './utils';
13
+import DevTools from 'src/devtools/views/DevTools';
14
15
let panelCreated = false;
16
@@ -22,6 +30,7 @@ function createPanelIfReactLoaded() {
30
31
clearInterval(loadCheckInterval);
32
33
+ let renderRootToPortal = null;
34
let bridge = null;
35
let store = null;
36
let elementsPanel = null;
@@ -54,69 +63,85 @@ function createPanelIfReactLoaded() {
63
// Otherwise the Store may miss important initial tree op codes.
64
inject(chrome.runtime.getURL('build/backend.js'));
65
66
+ const viewElementSource = createViewElementSource(bridge, store);
67
+
68
+ const container = document.createElement('div');
69
+ const root = createRoot(container);
70
+
71
+ renderRootToPortal = ({ overrideTab, portalContainer }) => {
72
+ root.render(
73
+ createElement(DevTools, {
74
+ bridge,
75
+ browserName: getBrowserName(),
76
+ browserTheme: getBrowserTheme(),
77
+ overrideTab,
78
+ portalContainer,
79
+ showTabBar: false,
80
+ store,
81
+ viewElementSource,
82
+ })
83
+ );
84
+
85
+ const oldLinkTags = document.getElementsByTagName('link');
86
+ const newLinkTags = [];
87
+ for (let oldLinkTag of oldLinkTags) {
88
+ if (oldLinkTag.rel === 'stylesheet') {
89
+ const newLinkTag = document.createElement('link');
90
+ for (let attribute of oldLinkTag.attributes) {
91
+ newLinkTag.setAttribute(
92
+ attribute.nodeName,
93
+ attribute.nodeValue
94
+ );
95
+ }
96
+ newLinkTags.push(newLinkTag);
97
+ }
98
+ }
99
+
100
+ return newLinkTags;
101
+ };
102
+
103
if (elementsPanel !== null) {
58
- elementsPanel.injectBridgeAndStore(bridge, store);
59
- }
60
- if (profilerPanel !== null) {
61
- profilerPanel.injectBridgeAndStore(bridge, store);
62
- }
63
- if (settingsPanel !== null) {
64
- settingsPanel.injectBridgeAndStore(bridge, store);
104
+ elementsPanel.render(renderRootToPortal, 'elements');
105
}
106
}
107
108
initBridgeAndStore();
109
70
- chrome.devtools.panels.create(
71
- '⚛ Elements',
72
- '',
73
- 'elements.html',
74
- panel => {
75
- panel.onShown.addListener(panel => {
76
- if (elementsPanel === null) {
77
- panel.injectBridgeAndStore(bridge, store);
78
- }
110
+ chrome.devtools.panels.create('⚛ Elements', '', 'panel.html', panel => {
111
+ panel.onShown.addListener(panel => {
112
+ elementsPanel = panel;
113
80
- elementsPanel = panel;
114
+ if (renderRootToPortal !== null) {
115
+ elementsPanel.render(renderRootToPortal, 'elements');
116
+ }
117
82
- // TODO: When the user switches to the panel, check for an Elements tab selection.
83
- });
84
- panel.onHidden.addListener(() => {
85
- // TODO: Stop highlighting and stuff.
86
- });
87
- }
88
- );
118
+ // TODO: When the user switches to the panel, check for an Elements tab selection.
119
+ });
120
+ panel.onHidden.addListener(() => {
121
+ // TODO: Stop highlighting and stuff.
122
+ });
123
+ });
124
125
// TODO (profiling) Is there a way to detect profiling support and conditionally register this panel?
91
- chrome.devtools.panels.create(
92
- '⚛ Profiler',
93
- '',
94
- 'profiler.html',
95
- panel => {
96
- panel.onShown.addListener(panel => {
97
- if (settingsPanel === null) {
98
- panel.injectBridgeAndStore(bridge, store);
99
- }
126
+ chrome.devtools.panels.create('⚛ Profiler', '', 'panel.html', panel => {
127
+ panel.onShown.addListener(panel => {
128
+ profilerPanel = panel;
129
101
- profilerPanel = panel;
102
- });
103
- }
104
- );
105
-
106
- chrome.devtools.panels.create(
107
- '⚛ Settings',
108
- '',
109
- 'settings.html',
110
- panel => {
111
- panel.onShown.addListener(panel => {
112
- if (settingsPanel === null) {
113
- panel.injectBridgeAndStore(bridge, store);
114
- }
130
+ if (renderRootToPortal !== null) {
131
+ profilerPanel.render(renderRootToPortal, 'profiler');
132
+ }
133
+ });
134
+ });
135
116
- settingsPanel = panel;
117
- });
118
- }
119
- );
136
+ chrome.devtools.panels.create('⚛ Settings', '', 'panel.html', panel => {
137
+ panel.onShown.addListener(panel => {
138
+ settingsPanel = panel;
139
+
140
+ if (renderRootToPortal !== null) {
141
+ settingsPanel.render(renderRootToPortal, 'settings');
142
+ }
143
+ });
144
+ });
145
146
chrome.devtools.network.onNavigated.removeListener(checkPageForReact);
147
shells/browser/shared/src/panel.js
new
+20
@@ -0,0 +1,20 @@
1
+const container = document.getElementById('container');
2
+
3
+let hasInjectedStyles = false;
4
+
5
+window.render = (renderRootToPortal, tab) => {
6
+ container.innerHTML = '';
7
+
8
+ const linkTags = renderRootToPortal({
9
+ overrideTab: tab,
10
+ portalContainer: container,
11
+ });
12
+
13
+ if (!hasInjectedStyles) {
14
+ hasInjectedStyles = true;
15
+
16
+ for (let linkTag of linkTags) {
17
+ document.head.appendChild(linkTag);
18
+ }
19
+ }
20
+};
shells/browser/shared/src/panels/elements.js
deleted
-3
@@ -1,3 +0,0 @@
1
-import { createPanel } from './utils';
2
-
3
-createPanel('elements');
shells/browser/shared/src/panels/profiler.js
deleted
-3
@@ -1,3 +0,0 @@
1
-import { createPanel } from './utils';
2
-
3
-createPanel('profiler');
shells/browser/shared/src/panels/settings.js
deleted
-3
@@ -1,3 +0,0 @@
1
-import { createPanel } from './utils';
2
-
3
-createPanel('settings');
shells/browser/shared/src/panels/utils.js
deleted
-72
@@ -1,72 +0,0 @@
1
-/* global chrome */
2
-
3
-import { createElement } from 'react';
4
-import { unstable_createRoot as createRoot, flushSync } from 'react-dom';
5
-import DevTools from 'src/devtools/views/DevTools';
6
-import { getBrowserName, getBrowserTheme } from '../utils';
7
-
8
-export function createPanel(defaultTab) {
9
- let injectedBridge = null;
10
- let injectedStore = null;
11
- let root = null;
12
-
13
- // All DevTools panel share a single Bridge and Store instance.
14
- // The main script will inject those shared instances using this method.
15
- window.injectBridgeAndStore = (bridge, store) => {
16
- injectedBridge = bridge;
17
- injectedStore = store;
18
-
19
- if (root === null) {
20
- injectAndInit();
21
- } else {
22
- // It's easiest to recreate the DevTools panel (to clean up potential stale state).
23
- // We can revisit this in the future as a small optimization.
24
- flushSync(() => root.unmount(injectAndInit));
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'
54
- ): any): HTMLElement);
55
-
56
- // Clear the "React not found" initial message before rendering.
57
- container.innerHTML = '';
58
-
59
- root = createRoot(container);
60
- root.render(
61
- createElement(DevTools, {
62
- bridge: injectedBridge,
63
- browserName: getBrowserName(),
64
- browserTheme: getBrowserTheme(),
65
- defaultTab,
66
- showTabBar: false,
67
- store: injectedStore,
68
- viewElementSource,
69
- })
70
- );
71
- }
72
-}
shells/browser/shared/src/utils.js
+21
@@ -2,6 +2,27 @@
2
3
const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0;
4
5
+export function createViewElementSource(bridge: Bridge, store: Store) {
6
+ return function viewElementSource(id) {
7
+ const rendererID = store.getRendererIDForElement(id);
8
+ if (rendererID != null) {
9
+ // Ask the renderer interface to determine the component function,
10
+ // and store it as a global variable on the window
11
+ bridge.send('viewElementSource', { id, rendererID });
12
+
13
+ setTimeout(() => {
14
+ // Ask Chrome to display the location of the component function,
15
+ // assuming the renderer found one.
16
+ chrome.devtools.inspectedWindow.eval(`
17
+ if (window.$type != null) {
18
+ inspect(window.$type);
19
+ }
20
+ `);
21
+ }, 100);
22
+ }
23
+ };
24
+}
25
+
26
export function getBrowserName() {
27
return IS_CHROME ? 'Chrome' : 'Firefox';
28
}
shells/browser/shared/webpack.config.js
+1
-3
@@ -16,9 +16,7 @@ module.exports = {
16
contentScript: './src/contentScript.js',
17
inject: './src/GlobalHook.js',
18
main: './src/main.js',
19
- elements: './src/panels/elements.js',
20
- profiler: './src/panels/profiler.js',
21
- settings: './src/panels/settings.js',
19
+ panel: './src/panel.js',
20
},
21
output: {
22
path: __dirname + '/build',
src/devtools/views/DevTools.js
+14
-1
@@ -1,6 +1,7 @@
1
// @flow
2
3
import React, { useEffect, useState } from 'react';
4
+import { createPortal } from 'react-dom';
5
import Store from '../store';
6
import { BridgeContext, StoreContext } from './context';
7
import Elements from './Elements/Elements';
@@ -27,6 +28,8 @@ export type Props = {|
28
browserName: BrowserName,
29
defaultTab?: TabID,
30
browserTheme: BrowserTheme,
31
+ overrideTab?: TabID,
32
+ portalContainer?: Element,
33
showTabBar?: boolean,
34
store: Store,
35
viewElementSource?: ?Function,
@@ -59,11 +62,17 @@ export default function DevTools({
62
browserName,
63
defaultTab = 'elements',
64
browserTheme = 'light',
65
+ overrideTab,
66
+ portalContainer,
67
showTabBar = false,
68
store,
69
viewElementSource = null,
70
}: Props) {
71
const [tab, setTab] = useState(defaultTab);
72
+ if (overrideTab != null && overrideTab !== tab) {
73
+ setTab(overrideTab);
74
+ }
75
+
76
const [supportsProfiling, setSupportsProfiling] = useState(
77
store.supportsProfiling
78
);
@@ -100,7 +109,7 @@ export default function DevTools({
109
break;
110
}
111
103
- return (
112
+ const children = (
113
<BridgeContext.Provider value={bridge}>
114
<StoreContext.Provider value={store}>
115
<SettingsContextController browserTheme={browserTheme}>
@@ -135,4 +144,8 @@ export default function DevTools({
144
</StoreContext.Provider>
145
</BridgeContext.Provider>
146
);
147
+
148
+ return portalContainer != null
149
+ ? createPortal(children, portalContainer)
150
+ : children;
151
}