Added properties panel and selected element context
Brian Vaughn committed
Feb 2, 2019 at 13:57 UTC
406df2a617c6a9565b1dc053129e07007ea94bae
15 files changed
+343
-96
shells/dev/index.html
-1
@@ -8,7 +8,6 @@
8
#target {
9
flex: 1;
10
border: none;
11
- border-bottom: 1px solid #ccc;
11
}
12
#devtools {
13
display: flex;
src/devtools/store.js
+5
-3
@@ -105,14 +105,16 @@ export default class Store extends EventEmitter {
105
return ((currentElement: any): Element);
106
}
107
108
- getElementByID(id: number): Element {
108
+ getElementByID(id: number): Element | null {
109
const element = this._idToElement.get(id);
110
111
if (element == null) {
112
- throw Error(`No element found with id "${id}`);
112
+ console.warn(`No element found with id "${id}`);
113
+
114
+ return null;
115
}
116
115
- return ((element: any): Element);
117
+ return element;
118
}
119
120
onBridgeOperations = (operations: Uint32Array) => {
src/devtools/views/ButtonIcon.css
renamed
+1
-1
@@ -1,4 +1,4 @@
1
-.Icon {
1
+.ButtonIcon {
2
width: 1rem;
3
height: 1rem;
4
margin: 0 0.25rem;
src/devtools/views/ButtonIcon.js
new
+55
@@ -0,0 +1,55 @@
1
+// @flow
2
+
3
+import React from 'react';
4
+import styles from './ButtonIcon.css';
5
+
6
+type Props = {|
7
+ type: 'search' | 'view-dom' | 'view-source',
8
+|};
9
+
10
+export default function ButtonIcon({ type }: Props) {
11
+ let pathData = null;
12
+ switch (type) {
13
+ case 'search':
14
+ pathData = PATH_SEARCH;
15
+ break;
16
+ case 'view-dom':
17
+ pathData = PATH_VIEW_DOM;
18
+ break;
19
+ case 'view-source':
20
+ pathData = PATH_VIEW_SOURCE;
21
+ break;
22
+ default:
23
+ console.warn(`Unsupported type "${type}" specified for ButtonIcon`);
24
+ break;
25
+ }
26
+
27
+ return (
28
+ <svg
29
+ xmlns="http://www.w3.org/2000/svg"
30
+ className={styles.ButtonIcon}
31
+ width="24"
32
+ height="24"
33
+ viewBox="0 0 24 24"
34
+ >
35
+ <path d="M0 0h24v24H0z" fill="none" />
36
+ <path fill="currentColor" d={pathData} />
37
+ </svg>
38
+ );
39
+}
40
+
41
+const PATH_SEARCH = `
42
+ M20.94 11c-.46-4.17-3.77-7.48-7.94-7.94V1h-2v2.06C6.83 3.52 3.52 6.83 3.06 11H1v2h2.06c.46
43
+ 4.17 3.77 7.48 7.94 7.94V23h2v-2.06c4.17-.46 7.48-3.77 7.94-7.94H23v-2h-2.06zM12 19c-3.87
44
+ 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z
45
+`;
46
+
47
+const PATH_VIEW_DOM = `
48
+ M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12
49
+ 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3
50
+ 3-1.34 3-3-1.34-3-3-3z
51
+`;
52
+
53
+const PATH_VIEW_SOURCE = `
54
+ M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z
55
+ `;
src/devtools/views/Element.css
+13
-1
@@ -1,14 +1,26 @@
1
-.Element {
1
+.Element,
2
+.SelectedElement {
3
width: 100%;
4
border-radius: 0.25em;
5
cursor: pointer;
6
position: relative;
7
white-space: nowrap;
8
+ line-height: 20px;
9
}
10
.Element:hover {
11
background-color: var(--color-state03);
12
}
13
14
+.SelectedElement {
15
+ background-color: var(--color-state00);
16
+
17
+ /* Invert colors */
18
+ --color-component: var(--color-base00);
19
+ --color-tree-tag: rgba(255, 255, 255, 0.8);
20
+ --color-tree-attr-name: #b3e5fc;
21
+ --color-tree-attr-value: #fff;
22
+}
23
+
24
.ArrowClosed,
25
.ArrowOpen {
26
position: absolute;
src/devtools/views/Element.js
+16
-4
@@ -1,7 +1,8 @@
1
// @flow
2
3
-import React, { Fragment, useContext } from 'react';
3
+import React, { Fragment, useCallback, useContext } from 'react';
4
import { TreeContext } from './context';
5
+import { SelectedElementContext } from './SelectedElementContext';
6
7
import styles from './Element.css';
8
@@ -22,13 +23,24 @@ export default function Element({ index, style }: Props) {
23
return null;
24
}
25
25
- const { children, depth, displayName, key } = element;
26
+ const { children, depth, displayName, id, key } = element;
27
27
- // TODO: Add state for toggling element open/close
28
+ const selectedElement = useContext(SelectedElementContext);
29
+ const handleClick = useCallback(
30
+ ({ metaKey }) => {
31
+ selectedElement.id = metaKey ? null : id;
32
+ },
33
+ [id]
34
+ );
35
+
36
+ // TODO: Add click and key handlers for toggling element open/close state.
37
38
return (
39
<div
31
- className={styles.Element}
40
+ className={
41
+ selectedElement.id === id ? styles.SelectedElement : styles.Element
42
+ }
43
+ onClick={handleClick}
44
style={{
45
...style,
46
paddingLeft: `${1 + depth}rem`,
src/devtools/views/Elements.css
+12
-37
@@ -2,49 +2,24 @@
2
width: 100%;
3
height: 100%;
4
display: flex;
5
- flex-direction: column;
5
+ flex-direction: row;
6
7
+ font-family: var(--font-family-sans);
8
-webkit-font-smoothing: antialiased;
9
}
10
10
-.SearchRow {
11
- flex: 0 0 auto;
12
- display: flex;
13
- align-items: center;
14
- font-size: 16px;
15
- border-bottom: 1px solid var(--color-base02);
16
- padding: 0.5rem;
11
+.TreeWrapper {
12
+ flex: 0 0 65%;
13
}
14
19
-.SearchInput {
20
- font-size: 1rem;
21
- flex: 1;
22
- outline: none;
23
- border: none;
24
- background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E");
25
- background-repeat: no-repeat;
26
- background-position: left center;
27
- background-size: 16px 16px;
28
- padding-left: 1.5rem;
15
+.SelectedElementWrapper {
16
+ flex: 0 0 35%;
17
+ overflow-x: hidden;
18
+ overflow-y: auto;
19
}
20
31
-.IconButton {
32
- border: none;
33
- background: none;
34
- display: inline-flex;
35
- align-items: center;
36
- padding: 0.25rem;
37
- cursor: pointer;
38
- color: var(--color-state00);
39
- flex: 0 0 auto;
40
-}
41
-.IconButton:hover {
42
- color: var(--color-special02);
43
-}
44
-
45
-.Tree {
46
- width: 100%;
47
- overflow: auto;
48
- flex: 1 0 auto;
49
- padding: 0.25rem;
21
+@media screen and (max-width: 600px) {
22
+ .Elements {
23
+ flex-direction: column;
24
+ }
25
}
src/devtools/views/Elements.js
+12
-14
@@ -4,7 +4,8 @@ import React, { useLayoutEffect, useMemo, useState } from 'react';
4
import Store from '../store';
5
import Tree from './Tree';
6
import { StoreContext, TreeContext } from './context';
7
-import SearchIcon from './SearchIcon';
7
+import SelectedElement from './SelectedElement';
8
+import { SelectedElementController } from './SelectedElementContext';
9
import styles from './Elements.css';
10
11
import './root.css';
@@ -38,23 +39,20 @@ export default function Elements({ bridge, browserName, themeName }: Props) {
39
return () => store.removeListener('rootCommitted', handler);
40
}, [store]);
41
42
+ // TODO Flex wrappers below should be user resizable.
43
return (
44
<StoreContext.Provider value={store}>
45
<TreeContext.Provider value={treeContext}>
44
- <div className={styles.Elements}>
45
- <div className={styles.SearchRow}>
46
- <input
47
- className={styles.SearchInput}
48
- placeholder="Search (text or /regex/)"
49
- />
50
- <button className={styles.IconButton}>
51
- <SearchIcon /> Select
52
- </button>
46
+ <SelectedElementController>
47
+ <div className={styles.Elements}>
48
+ <div className={styles.TreeWrapper}>
49
+ <Tree />
50
+ </div>
51
+ <div className={styles.SelectedElementWrapper}>
52
+ <SelectedElement />
53
+ </div>
54
</div>
54
- <div className={styles.Tree}>
55
- <Tree />
56
- </div>
57
- </div>
55
+ </SelectedElementController>
56
</TreeContext.Provider>
57
</StoreContext.Provider>
58
);
src/devtools/views/SearchIcon.js
deleted
-22
@@ -1,22 +0,0 @@
1
-// @flow
2
-
3
-import React from 'react';
4
-import styles from './SearchIcon.css';
5
-
6
-export default function SearchIcon() {
7
- return (
8
- <svg
9
- xmlns="http://www.w3.org/2000/svg"
10
- className={styles.Icon}
11
- width="24"
12
- height="24"
13
- viewBox="0 0 24 24"
14
- >
15
- <path d="M0 0h24v24H0z" fill="none" />
16
- <path
17
- fill="currentColor"
18
- d="M20.94 11c-.46-4.17-3.77-7.48-7.94-7.94V1h-2v2.06C6.83 3.52 3.52 6.83 3.06 11H1v2h2.06c.46 4.17 3.77 7.48 7.94 7.94V23h2v-2.06c4.17-.46 7.48-3.77 7.94-7.94H23v-2h-2.06zM12 19c-3.87 0-7-3.13-7-7s3.13-7 7-7 7 3.13 7 7-3.13 7-7 7z"
19
- />
20
- </svg>
21
- );
22
-}
src/devtools/views/SelectedElement.css
new
+54
@@ -0,0 +1,54 @@
1
+.SelectedElement {
2
+ display: flex;
3
+ flex-direction: column;
4
+ height: 100%;
5
+ width: 100%;
6
+ border-left: 1px solid var(--color-base02);
7
+ border-top: 1px solid var(--color-base02);
8
+}
9
+
10
+.TitleRow {
11
+ flex: 0 0 42px;
12
+ display: flex;
13
+ align-items: center;
14
+ font-size: 16px;
15
+ border-bottom: 1px solid var(--color-base02);
16
+ padding: 0.5rem;
17
+}
18
+
19
+.IconButton {
20
+ flex: 0 0 auto;
21
+ border: none;
22
+ background: none;
23
+ display: inline-flex;
24
+ align-items: center;
25
+ padding: 0.25rem;
26
+ cursor: pointer;
27
+ color: var(--color-state00);
28
+}
29
+.IconButton:hover {
30
+ color: var(--color-special02);
31
+}
32
+
33
+.SelectedComponentName {
34
+ flex: 1 1 auto;
35
+ overflow: hidden;
36
+ text-overflow: ellipsis;
37
+}
38
+
39
+.Component {
40
+ overflow: hidden;
41
+ text-overflow: ellipsis;
42
+ color: var(--color-component);
43
+ flex: 1 1 auto;
44
+}
45
+.Component:before {
46
+ white-space: nowrap;
47
+ content: '<';
48
+ color: var(--color-tree-tag);
49
+}
50
+.Component:after {
51
+ white-space: nowrap;
52
+ content: '>';
53
+ color: var(--color-tree-tag);
54
+}
src/devtools/views/SelectedElement.js
new
+50
@@ -0,0 +1,50 @@
1
+// @flow
2
+
3
+import React, { useContext } from 'react';
4
+import { SelectedElementContext } from './SelectedElementContext';
5
+import { StoreContext } from './context';
6
+import ButtonIcon from './ButtonIcon';
7
+import styles from './SelectedElement.css';
8
+
9
+export type Props = {||};
10
+
11
+export default function SelectedElement(props: Props) {
12
+ const { id } = useContext(SelectedElementContext);
13
+ const store = useContext(StoreContext);
14
+ const element = id !== null ? store.getElementByID(id) : null;
15
+
16
+ // TODO Show/hide source button based on whether debug "source" is available.
17
+
18
+ if (element === null) {
19
+ return (
20
+ <div className={styles.SelectedElement}>
21
+ <div className={styles.TitleRow} />
22
+ </div>
23
+ );
24
+ }
25
+
26
+ return (
27
+ <div className={styles.SelectedElement}>
28
+ <div className={styles.TitleRow}>
29
+ <div className={styles.SelectedComponentName}>
30
+ <div className={styles.Component} title={element.displayName}>
31
+ {element.displayName}
32
+ </div>
33
+ </div>
34
+
35
+ <button
36
+ className={styles.IconButton}
37
+ title="Highlight this element in the page"
38
+ >
39
+ <ButtonIcon type="view-dom" /> DOM
40
+ </button>
41
+ <button
42
+ className={styles.IconButton}
43
+ title="View source for this element"
44
+ >
45
+ <ButtonIcon type="view-source" /> Source
46
+ </button>
47
+ </div>
48
+ </div>
49
+ );
50
+}
src/devtools/views/SelectedElementContext.js
new
+41
@@ -0,0 +1,41 @@
1
+// @flow
2
+
3
+import React, { createContext, useMemo, useState } from 'react';
4
+
5
+type SelectedElementContextValue = {|
6
+ id: number | null,
7
+|};
8
+
9
+const SelectedElementContext = createContext<SelectedElementContextValue>(
10
+ ((null: any): SelectedElementContextValue)
11
+);
12
+// $FlowFixMe displayName is a valid attribute of React$Context
13
+SelectedElementContext.displayName = 'SelectedElementContext';
14
+
15
+type Props = {|
16
+ children: React$Node,
17
+|};
18
+
19
+// TODO Remove this wrapper element once global Context.write API exists.
20
+function SelectedElementController({ children }: Props) {
21
+ const [id, setID] = useState<number | null>(null);
22
+ const value = useMemo(
23
+ () => ({
24
+ get id() {
25
+ return id;
26
+ },
27
+ set id(id: number | null) {
28
+ setID(id);
29
+ },
30
+ }),
31
+ [id]
32
+ );
33
+
34
+ return (
35
+ <SelectedElementContext.Provider value={value}>
36
+ {children}
37
+ </SelectedElementContext.Provider>
38
+ );
39
+}
40
+
41
+export { SelectedElementContext, SelectedElementController };
src/devtools/views/Tree.css
+50
@@ -1,4 +1,54 @@
1
.Tree {
2
+ height: 100%;
3
+ width: 100%;
4
+ display: flex;
5
+ flex-direction: column;
6
+ border-top: 1px solid var(--color-base02);
7
+}
8
+
9
+.SearchRow {
10
+ flex: 0 0 42px;
11
+ display: flex;
12
+ align-items: center;
13
+ font-size: 16px;
14
+ border-bottom: 1px solid var(--color-base02);
15
+ padding: 0.5rem;
16
+}
17
+
18
+.SearchInput {
19
+ font-size: 1rem;
20
+ flex: 1;
21
+ outline: none;
22
+ border: none;
23
+ background: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath d='M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'/%3E%3Cpath d='M0 0h24v24H0z' fill='none'/%3E%3C/svg%3E");
24
+ background-repeat: no-repeat;
25
+ background-position: left center;
26
+ background-size: 16px 16px;
27
+ padding-left: 1.5rem;
28
+}
29
+
30
+.IconButton {
31
+ border: none;
32
+ background: none;
33
+ display: inline-flex;
34
+ align-items: center;
35
+ padding: 0.25rem;
36
+ cursor: pointer;
37
+ color: var(--color-state00);
38
+ flex: 0 0 auto;
39
+}
40
+.IconButton:hover {
41
+ color: var(--color-special02);
42
+}
43
+
44
+.AutoSizerWrapper {
45
+ width: 100%;
46
+ overflow: auto;
47
+ flex: 1 0 auto;
48
+ padding: 0.25rem;
49
+}
50
+
51
+.List {
52
font-family: var(--font-family-monospace);
53
font-size: var(--font-size-tree-compact);
54
line-height: var(--line-height-tree-compact);
src/devtools/views/Tree.js
+32
-13
@@ -4,6 +4,7 @@ import React, { useContext } from 'react';
4
import AutoSizer from 'react-virtualized-auto-sizer';
5
import { FixedSizeList } from 'react-window';
6
import Element from './Element';
7
+import ButtonIcon from './ButtonIcon';
8
import { TreeContext } from './context';
9
10
import styles from './Tree.css';
@@ -13,20 +14,38 @@ type Props = {||};
14
export default function Tree(props: Props) {
15
const treeContext = useContext(TreeContext);
16
17
+ // TODO Add key handlers for selecting previous/next element.
18
+
19
return (
17
- <AutoSizer>
18
- {({ height, width }) => (
19
- <FixedSizeList
20
- className={styles.Tree}
21
- height={height}
22
- itemCount={treeContext.size}
23
- itemData={treeContext}
24
- itemSize={20}
25
- width={width}
20
+ <div className={styles.Tree}>
21
+ <div className={styles.SearchRow}>
22
+ <input
23
+ className={styles.SearchInput}
24
+ placeholder="Search (text or /regex/)"
25
+ />
26
+ <button
27
+ className={styles.IconButton}
28
+ title="Select an element in the page to inspect it"
29
>
27
- {Element}
28
- </FixedSizeList>
29
- )}
30
- </AutoSizer>
30
+ <ButtonIcon type="search" /> Select
31
+ </button>
32
+ </div>
33
+ <div className={styles.AutoSizerWrapper}>
34
+ <AutoSizer>
35
+ {({ height, width }) => (
36
+ <FixedSizeList
37
+ className={styles.List}
38
+ height={height}
39
+ itemCount={treeContext.size}
40
+ itemData={treeContext}
41
+ itemSize={20}
42
+ width={width}
43
+ >
44
+ {Element}
45
+ </FixedSizeList>
46
+ )}
47
+ </AutoSizer>
48
+ </div>
49
+ </div>
50
);
51
}
src/devtools/views/root.css
+2
@@ -33,6 +33,8 @@
33
/* GitHub.com system fonts */
34
--font-family-monospace: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo,
35
Courier, monospace;
36
+ --font-family-sans: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica,
37
+ Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
38
--font-size-tree-compact: 12px;
39
--font-size-tree-normal: 14px;
40
}