Added properties panel and selected element context

This commit is contained in:
Brian Vaughn
2019-02-02 13:57:32 -08:00
parent 9aa6e13d5f
commit 406df2a617
15 changed files with 343 additions and 96 deletions
-1
View File
@@ -8,7 +8,6 @@
#target {
flex: 1;
border: none;
border-bottom: 1px solid #ccc;
}
#devtools {
display: flex;
+5 -3
View File
@@ -105,14 +105,16 @@ export default class Store extends EventEmitter {
return ((currentElement: any): Element);
}
getElementByID(id: number): Element {
getElementByID(id: number): Element | null {
const element = this._idToElement.get(id);
if (element == null) {
throw Error(`No element found with id "${id}`);
console.warn(`No element found with id "${id}`);
return null;
}
return ((element: any): Element);
return element;
}
onBridgeOperations = (operations: Uint32Array) => {
@@ -1,4 +1,4 @@
.Icon {
.ButtonIcon {
width: 1rem;
height: 1rem;
margin: 0 0.25rem;
+55
View File
@@ -0,0 +1,55 @@
// @flow
import React from 'react';
import styles from './ButtonIcon.css';
type Props = {|
type: 'search' | 'view-dom' | 'view-source',
|};
export default function ButtonIcon({ type }: Props) {
let pathData = null;
switch (type) {
case 'search':
pathData = PATH_SEARCH;
break;
case 'view-dom':
pathData = PATH_VIEW_DOM;
break;
case 'view-source':
pathData = PATH_VIEW_SOURCE;
break;
default:
console.warn(`Unsupported type "${type}" specified for ButtonIcon`);
break;
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={styles.ButtonIcon}
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path fill="currentColor" d={pathData} />
</svg>
);
}
const PATH_SEARCH = `
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
`;
const PATH_VIEW_DOM = `
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
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
3-1.34 3-3-1.34-3-3-3z
`;
const PATH_VIEW_SOURCE = `
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
`;
+13 -1
View File
@@ -1,14 +1,26 @@
.Element {
.Element,
.SelectedElement {
width: 100%;
border-radius: 0.25em;
cursor: pointer;
position: relative;
white-space: nowrap;
line-height: 20px;
}
.Element:hover {
background-color: var(--color-state03);
}
.SelectedElement {
background-color: var(--color-state00);
/* Invert colors */
--color-component: var(--color-base00);
--color-tree-tag: rgba(255, 255, 255, 0.8);
--color-tree-attr-name: #b3e5fc;
--color-tree-attr-value: #fff;
}
.ArrowClosed,
.ArrowOpen {
position: absolute;
+16 -4
View File
@@ -1,7 +1,8 @@
// @flow
import React, { Fragment, useContext } from 'react';
import React, { Fragment, useCallback, useContext } from 'react';
import { TreeContext } from './context';
import { SelectedElementContext } from './SelectedElementContext';
import styles from './Element.css';
@@ -22,13 +23,24 @@ export default function Element({ index, style }: Props) {
return null;
}
const { children, depth, displayName, key } = element;
const { children, depth, displayName, id, key } = element;
// TODO: Add state for toggling element open/close
const selectedElement = useContext(SelectedElementContext);
const handleClick = useCallback(
({ metaKey }) => {
selectedElement.id = metaKey ? null : id;
},
[id]
);
// TODO: Add click and key handlers for toggling element open/close state.
return (
<div
className={styles.Element}
className={
selectedElement.id === id ? styles.SelectedElement : styles.Element
}
onClick={handleClick}
style={{
...style,
paddingLeft: `${1 + depth}rem`,
+12 -37
View File
@@ -2,49 +2,24 @@
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
flex-direction: row;
font-family: var(--font-family-sans);
-webkit-font-smoothing: antialiased;
}
.SearchRow {
flex: 0 0 auto;
display: flex;
align-items: center;
font-size: 16px;
border-bottom: 1px solid var(--color-base02);
padding: 0.5rem;
.TreeWrapper {
flex: 0 0 65%;
}
.SearchInput {
font-size: 1rem;
flex: 1;
outline: none;
border: none;
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");
background-repeat: no-repeat;
background-position: left center;
background-size: 16px 16px;
padding-left: 1.5rem;
.SelectedElementWrapper {
flex: 0 0 35%;
overflow-x: hidden;
overflow-y: auto;
}
.IconButton {
border: none;
background: none;
display: inline-flex;
align-items: center;
padding: 0.25rem;
cursor: pointer;
color: var(--color-state00);
flex: 0 0 auto;
}
.IconButton:hover {
color: var(--color-special02);
}
.Tree {
width: 100%;
overflow: auto;
flex: 1 0 auto;
padding: 0.25rem;
@media screen and (max-width: 600px) {
.Elements {
flex-direction: column;
}
}
+12 -14
View File
@@ -4,7 +4,8 @@ import React, { useLayoutEffect, useMemo, useState } from 'react';
import Store from '../store';
import Tree from './Tree';
import { StoreContext, TreeContext } from './context';
import SearchIcon from './SearchIcon';
import SelectedElement from './SelectedElement';
import { SelectedElementController } from './SelectedElementContext';
import styles from './Elements.css';
import './root.css';
@@ -38,23 +39,20 @@ export default function Elements({ bridge, browserName, themeName }: Props) {
return () => store.removeListener('rootCommitted', handler);
}, [store]);
// TODO Flex wrappers below should be user resizable.
return (
<StoreContext.Provider value={store}>
<TreeContext.Provider value={treeContext}>
<div className={styles.Elements}>
<div className={styles.SearchRow}>
<input
className={styles.SearchInput}
placeholder="Search (text or /regex/)"
/>
<button className={styles.IconButton}>
<SearchIcon /> Select
</button>
<SelectedElementController>
<div className={styles.Elements}>
<div className={styles.TreeWrapper}>
<Tree />
</div>
<div className={styles.SelectedElementWrapper}>
<SelectedElement />
</div>
</div>
<div className={styles.Tree}>
<Tree />
</div>
</div>
</SelectedElementController>
</TreeContext.Provider>
</StoreContext.Provider>
);
-22
View File
@@ -1,22 +0,0 @@
// @flow
import React from 'react';
import styles from './SearchIcon.css';
export default function SearchIcon() {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
className={styles.Icon}
width="24"
height="24"
viewBox="0 0 24 24"
>
<path d="M0 0h24v24H0z" fill="none" />
<path
fill="currentColor"
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"
/>
</svg>
);
}
+54
View File
@@ -0,0 +1,54 @@
.SelectedElement {
display: flex;
flex-direction: column;
height: 100%;
width: 100%;
border-left: 1px solid var(--color-base02);
border-top: 1px solid var(--color-base02);
}
.TitleRow {
flex: 0 0 42px;
display: flex;
align-items: center;
font-size: 16px;
border-bottom: 1px solid var(--color-base02);
padding: 0.5rem;
}
.IconButton {
flex: 0 0 auto;
border: none;
background: none;
display: inline-flex;
align-items: center;
padding: 0.25rem;
cursor: pointer;
color: var(--color-state00);
}
.IconButton:hover {
color: var(--color-special02);
}
.SelectedComponentName {
flex: 1 1 auto;
overflow: hidden;
text-overflow: ellipsis;
}
.Component {
overflow: hidden;
text-overflow: ellipsis;
color: var(--color-component);
flex: 1 1 auto;
}
.Component:before {
white-space: nowrap;
content: '<';
color: var(--color-tree-tag);
}
.Component:after {
white-space: nowrap;
content: '>';
color: var(--color-tree-tag);
}
+50
View File
@@ -0,0 +1,50 @@
// @flow
import React, { useContext } from 'react';
import { SelectedElementContext } from './SelectedElementContext';
import { StoreContext } from './context';
import ButtonIcon from './ButtonIcon';
import styles from './SelectedElement.css';
export type Props = {||};
export default function SelectedElement(props: Props) {
const { id } = useContext(SelectedElementContext);
const store = useContext(StoreContext);
const element = id !== null ? store.getElementByID(id) : null;
// TODO Show/hide source button based on whether debug "source" is available.
if (element === null) {
return (
<div className={styles.SelectedElement}>
<div className={styles.TitleRow} />
</div>
);
}
return (
<div className={styles.SelectedElement}>
<div className={styles.TitleRow}>
<div className={styles.SelectedComponentName}>
<div className={styles.Component} title={element.displayName}>
{element.displayName}
</div>
</div>
<button
className={styles.IconButton}
title="Highlight this element in the page"
>
<ButtonIcon type="view-dom" /> DOM
</button>
<button
className={styles.IconButton}
title="View source for this element"
>
<ButtonIcon type="view-source" /> Source
</button>
</div>
</div>
);
}
@@ -0,0 +1,41 @@
// @flow
import React, { createContext, useMemo, useState } from 'react';
type SelectedElementContextValue = {|
id: number | null,
|};
const SelectedElementContext = createContext<SelectedElementContextValue>(
((null: any): SelectedElementContextValue)
);
// $FlowFixMe displayName is a valid attribute of React$Context
SelectedElementContext.displayName = 'SelectedElementContext';
type Props = {|
children: React$Node,
|};
// TODO Remove this wrapper element once global Context.write API exists.
function SelectedElementController({ children }: Props) {
const [id, setID] = useState<number | null>(null);
const value = useMemo(
() => ({
get id() {
return id;
},
set id(id: number | null) {
setID(id);
},
}),
[id]
);
return (
<SelectedElementContext.Provider value={value}>
{children}
</SelectedElementContext.Provider>
);
}
export { SelectedElementContext, SelectedElementController };
+50
View File
@@ -1,4 +1,54 @@
.Tree {
height: 100%;
width: 100%;
display: flex;
flex-direction: column;
border-top: 1px solid var(--color-base02);
}
.SearchRow {
flex: 0 0 42px;
display: flex;
align-items: center;
font-size: 16px;
border-bottom: 1px solid var(--color-base02);
padding: 0.5rem;
}
.SearchInput {
font-size: 1rem;
flex: 1;
outline: none;
border: none;
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");
background-repeat: no-repeat;
background-position: left center;
background-size: 16px 16px;
padding-left: 1.5rem;
}
.IconButton {
border: none;
background: none;
display: inline-flex;
align-items: center;
padding: 0.25rem;
cursor: pointer;
color: var(--color-state00);
flex: 0 0 auto;
}
.IconButton:hover {
color: var(--color-special02);
}
.AutoSizerWrapper {
width: 100%;
overflow: auto;
flex: 1 0 auto;
padding: 0.25rem;
}
.List {
font-family: var(--font-family-monospace);
font-size: var(--font-size-tree-compact);
line-height: var(--line-height-tree-compact);
+32 -13
View File
@@ -4,6 +4,7 @@ import React, { useContext } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer';
import { FixedSizeList } from 'react-window';
import Element from './Element';
import ButtonIcon from './ButtonIcon';
import { TreeContext } from './context';
import styles from './Tree.css';
@@ -13,20 +14,38 @@ type Props = {||};
export default function Tree(props: Props) {
const treeContext = useContext(TreeContext);
// TODO Add key handlers for selecting previous/next element.
return (
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
className={styles.Tree}
height={height}
itemCount={treeContext.size}
itemData={treeContext}
itemSize={20}
width={width}
<div className={styles.Tree}>
<div className={styles.SearchRow}>
<input
className={styles.SearchInput}
placeholder="Search (text or /regex/)"
/>
<button
className={styles.IconButton}
title="Select an element in the page to inspect it"
>
{Element}
</FixedSizeList>
)}
</AutoSizer>
<ButtonIcon type="search" /> Select
</button>
</div>
<div className={styles.AutoSizerWrapper}>
<AutoSizer>
{({ height, width }) => (
<FixedSizeList
className={styles.List}
height={height}
itemCount={treeContext.size}
itemData={treeContext}
itemSize={20}
width={width}
>
{Element}
</FixedSizeList>
)}
</AutoSizer>
</div>
</div>
);
}
+2
View File
@@ -33,6 +33,8 @@
/* GitHub.com system fonts */
--font-family-monospace: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo,
Courier, monospace;
--font-family-sans: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica,
Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
--font-size-tree-compact: 12px;
--font-size-tree-normal: 14px;
}