Initial pass at adding Settings panel to browser extension
Brian Vaughn committed
Feb 13, 2019 at 10:59 UTC
4ea81337f382a75226d619b1d8eca98f4010dd7b
13 files changed
+197
-55
shells/browser/chrome/manifest.json
+6
-1
@@ -27,7 +27,12 @@
27
"devtools_page": "main.html",
28
29
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
30
- "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"],
30
+ "web_accessible_resources": [
31
+ "main.html",
32
+ "panel.html",
33
+ "settings.html",
34
+ "build/backend.js"
35
+ ],
36
37
"background": {
38
"scripts": ["build/background.js"],
shells/browser/firefox/manifest.json
+6
-1
@@ -33,7 +33,12 @@
33
"devtools_page": "main.html",
34
35
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
36
- "web_accessible_resources": ["main.html", "panel.html", "build/backend.js"],
36
+ "web_accessible_resources": [
37
+ "main.html",
38
+ "panel.html",
39
+ "settings.html",
40
+ "build/backend.js"
41
+ ],
42
43
"background": {
44
"scripts": ["build/background.js"],
shells/browser/shared/build.js
+7
-1
@@ -7,7 +7,13 @@ 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 = ['icons', 'popups', 'main.html', 'panel.html'];
10
+const STATIC_FILES = [
11
+ 'icons',
12
+ 'popups',
13
+ 'main.html',
14
+ 'panel.html',
15
+ 'settings.html',
16
+];
17
18
const preProcess = async (destinationPath, tempPath) => {
19
await remove(destinationPath); // Clean up from previously completed builds
shells/browser/shared/settings.html
new
+32
@@ -0,0 +1,32 @@
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
+9
-3
@@ -6,6 +6,7 @@ function createPanelIfReactLoaded() {
6
if (panelCreated) {
7
return;
8
}
9
+
10
chrome.devtools.inspectedWindow.eval(
11
'window.__REACT_DEVTOOLS_GLOBAL_HOOK__ && window.__REACT_DEVTOOLS_GLOBAL_HOOK__.renderers.size > 0',
12
function(pageHasReact, err) {
@@ -15,9 +16,7 @@ function createPanelIfReactLoaded() {
16
17
clearInterval(loadCheckInterval);
18
panelCreated = true;
18
- chrome.devtools.panels.create('⚛ Elements', '', 'panel.html', function(
19
- panel
20
- ) {
19
+ chrome.devtools.panels.create('⚛ Elements', '', 'panel.html', panel => {
20
panel.onShown.addListener(function(window) {
21
// TODO: When the user switches to the panel, check for an Elements tab selection.
22
});
@@ -25,6 +24,13 @@ function createPanelIfReactLoaded() {
24
// TODO: Stop highlighting and stuff.
25
});
26
});
27
+
28
+ chrome.devtools.panels.create(
29
+ '⚛ Settings',
30
+ '',
31
+ 'settings.html',
32
+ panel => {}
33
+ );
34
}
35
);
36
}
shells/browser/shared/src/panel.js
+4
-30
@@ -5,34 +5,7 @@ import { createRoot, flushSync } from 'react-dom';
5
import Bridge from 'src/bridge';
6
import DevTools from 'src/devtools/views/DevTools';
7
import inject from './inject';
8
-
9
-const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0;
10
-
11
-let browserName;
12
-let themeName;
13
-
14
-if (IS_CHROME) {
15
- browserName = 'Chrome';
16
-
17
- // chrome.devtools.panels added in Chrome 18.
18
- // chrome.devtools.panels.themeName added in Chrome 54.
19
- themeName = chrome.devtools.panels.themeName === 'dark' ? 'dark' : 'light';
20
-} else {
21
- browserName = 'Firefox';
22
-
23
- // chrome.devtools.panels.themeName added in Firefox 55.
24
- // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.panels/themeName
25
- if (chrome.devtools && chrome.devtools.panels) {
26
- switch (chrome.devtools.panels.themeName) {
27
- case 'dark':
28
- themeName = 'dark';
29
- break;
30
- default:
31
- themeName = 'light';
32
- break;
33
- }
34
- }
35
-}
8
+import { getBrowserName, getBrowserTheme } from './utils';
9
10
const container = ((document.getElementById('container'): any): HTMLElement);
11
@@ -65,8 +38,9 @@ function injectAndInit() {
38
root.render(
39
createElement(DevTools, {
40
bridge,
68
- browserName,
69
- browserTheme: themeName,
41
+ browserName: getBrowserName(),
42
+ browserTheme: getBrowserTheme(),
43
+ defaultTab: 'elements',
44
showTabBar: false,
45
})
46
);
shells/browser/shared/src/settings.js
new
+48
@@ -0,0 +1,48 @@
1
+/* global chrome */
2
+
3
+import { createElement } from 'react';
4
+import { createRoot, flushSync } from 'react-dom';
5
+import Bridge from 'src/bridge';
6
+import DevTools from 'src/devtools/views/DevTools';
7
+import inject from './inject';
8
+import { getBrowserName, getBrowserTheme } from './utils';
9
+
10
+const container = ((document.getElementById('container'): any): HTMLElement);
11
+
12
+function injectAndInit() {
13
+ // Noop Bridge for the Settings panel
14
+ const bridge = new Bridge({
15
+ listen(fn) {},
16
+ send(event: string, payload: any, transferable?: Array<any>) {},
17
+ });
18
+
19
+ // Clear the "React not found" initial message before rendering.
20
+ container.innerHTML = '';
21
+
22
+ const root = createRoot(container);
23
+ root.render(
24
+ createElement(DevTools, {
25
+ bridge,
26
+ browserName: getBrowserName(),
27
+ browserTheme: getBrowserTheme(),
28
+ defaultTab: 'settings',
29
+ showTabBar: false,
30
+ })
31
+ );
32
+
33
+ // Initialize the backend only once the DevTools frontend Store has been initialized.
34
+ // Otherwise the Store may miss important initial tree op codes.
35
+ inject(chrome.runtime.getURL('build/backend.js'));
36
+
37
+ // Reload the DevTools extension when the user navigates to a new page.
38
+ function onNavigated() {
39
+ chrome.devtools.network.onNavigated.removeListener(onNavigated);
40
+
41
+ bridge.send('shutdown');
42
+
43
+ flushSync(() => root.unmount(injectAndInit));
44
+ }
45
+ chrome.devtools.network.onNavigated.addListener(onNavigated);
46
+}
47
+
48
+injectAndInit();
shells/browser/shared/src/utils.js
new
+26
@@ -0,0 +1,26 @@
1
+/* global chrome */
2
+
3
+const IS_CHROME = navigator.userAgent.indexOf('Firefox') < 0;
4
+
5
+export function getBrowserName() {
6
+ return IS_CHROME ? 'Chrome' : 'Firefox';
7
+}
8
+
9
+export function getBrowserTheme() {
10
+ if (IS_CHROME) {
11
+ // chrome.devtools.panels added in Chrome 18.
12
+ // chrome.devtools.panels.themeName added in Chrome 54.
13
+ return chrome.devtools.panels.themeName === 'dark' ? 'dark' : 'light';
14
+ } else {
15
+ // chrome.devtools.panels.themeName added in Firefox 55.
16
+ // https://developer.mozilla.org/en-US/Add-ons/WebExtensions/API/devtools.panels/themeName
17
+ if (chrome.devtools && chrome.devtools.panels) {
18
+ switch (chrome.devtools.panels.themeName) {
19
+ case 'dark':
20
+ return 'dark';
21
+ default:
22
+ return 'light';
23
+ }
24
+ }
25
+ }
26
+}
shells/browser/shared/webpack.config.js
+1
@@ -17,6 +17,7 @@ module.exports = {
17
inject: './src/GlobalHook.js',
18
main: './src/main.js',
19
panel: './src/panel.js',
20
+ settings: './src/settings.js',
21
},
22
output: {
23
path: __dirname + '/build',
src/devtools/views/DevTools.css
new
+17
@@ -0,0 +1,17 @@
1
+.DevTools {
2
+ width: 100%;
3
+ height: 100%;
4
+ display: flex;
5
+ flex-direction: column;
6
+ background-color: var(--color-background);
7
+ color: var(--color-text-color);
8
+}
9
+
10
+.TabBar {
11
+ flex: 0 0 auto;
12
+}
13
+
14
+.TabContent {
15
+ flex: 1 1 100%;
16
+ overflow: auto;
17
+}
src/devtools/views/DevTools.js
+13
-4
@@ -10,16 +10,19 @@ import TabBar from './TabBar';
10
import { SettingsContextController } from './SettingsContext';
11
import { TreeContextController } from './TreeContext';
12
13
+import styles from './DevTools.css';
14
+
15
import './root.css';
16
17
import type { Bridge } from '../../types';
18
17
-export type TabID = 'elements' | 'profiler' | 'settings';
19
+export type BrowserName = 'Chrome' | 'Firefox';
20
export type BrowserTheme = 'dark' | 'light';
21
+export type TabID = 'elements' | 'profiler' | 'settings';
22
23
export type Props = {|
24
bridge: Bridge,
22
- browserName: string,
25
+ browserName: BrowserName,
26
defaultTab?: TabID,
27
browserTheme: BrowserTheme,
28
showTabBar?: boolean,
@@ -54,8 +57,14 @@ export default function DevTools({
57
<StoreContext.Provider value={store}>
58
<SettingsContextController browserTheme={browserTheme}>
59
<TreeContextController>
57
- {showTabBar && <TabBar currentTab={tab} selectTab={setTab} />}
58
- {tabElement}
60
+ <div className={styles.DevTools}>
61
+ {showTabBar && (
62
+ <div className={styles.TabBar}>
63
+ <TabBar currentTab={tab} selectTab={setTab} />
64
+ </div>
65
+ )}
66
+ <div className={styles.TabContent}>{tabElement}</div>
67
+ </div>
68
</TreeContextController>
69
</SettingsContextController>
70
</StoreContext.Provider>
src/devtools/views/Element.js
+6
@@ -24,6 +24,12 @@ export default function ElementView({ index, style }: Props) {
24
} = useContext(TreeContext);
25
26
const element = getElementAtIndex(index);
27
+
28
+ if (element == null) {
29
+ console.warn(`<ElementView> Could not find element at index ${index}`);
30
+ return null;
31
+ }
32
+
33
const { depth, displayName, id, key, type } = ((element: any): Element);
34
35
const handleDoubleClick = useCallback(() => selectOwner(id), [id]);
src/devtools/views/hooks.js
+22
-15
@@ -1,43 +1,50 @@
1
// @flow
2
3
-import { useState } from 'react';
3
+import { useLayoutEffect, useState } from 'react';
4
5
-// https://usehooks.com/useLocalStorage/
5
+// Forked from https://usehooks.com/useLocalStorage/
6
export function useLocalStorage<T>(
7
key: string,
8
initialValue: T
9
): [T, (value: T | (() => T)) => void] {
10
- // State to store our value
11
- // Pass initial state function to useState so logic is only executed once
12
- const [storedValue, setStoredValue] = useState(() => {
10
+ const getValueFromLocalStorage = () => {
11
try {
14
- // Get from local storage by key
12
const item = window.localStorage.getItem(key);
16
- // Parse stored json or if none return initialValue
13
return item ? JSON.parse(item) : initialValue;
14
} catch (error) {
19
- // If error also return initialValue
15
console.log(error);
16
return initialValue;
17
}
23
- });
18
+ };
19
+
20
+ const [storedValue, setStoredValue] = useState(getValueFromLocalStorage);
21
25
- // Return a wrapped version of useState's setter function that ...
26
- // ... persists the new value to localStorage.
22
const setValue = value => {
23
try {
29
- // Allow value to be a function so we have same API as useState
24
const valueToStore =
25
value instanceof Function ? value(storedValue) : value;
32
- // Save state
26
setStoredValue(valueToStore);
34
- // Save to local storage
27
window.localStorage.setItem(key, JSON.stringify(valueToStore));
28
} catch (error) {
37
- // A more advanced implementation would handle the error case
29
console.log(error);
30
}
31
};
32
33
+ // Listen for changes in other extension panels.
34
+ useLayoutEffect(() => {
35
+ const onStorage = event => {
36
+ const newValue = getValueFromLocalStorage();
37
+ if (key === event.key && storedValue !== newValue) {
38
+ setValue(newValue);
39
+ }
40
+ };
41
+
42
+ window.addEventListener('storage', onStorage);
43
+
44
+ return () => {
45
+ window.removeEventListener('storage', onStorage);
46
+ };
47
+ }, [key, storedValue]);
48
+
49
return [storedValue, setValue];
50
}