mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Added search support
This commit is contained in:
@@ -265,7 +265,7 @@ export default class Store extends EventEmitter {
|
||||
parentElement = ((this._idToElement.get(parentID): any): Element);
|
||||
parentElement.children = parentElement.children.concat(id);
|
||||
|
||||
this._idToElement.set(id, {
|
||||
const element: Element = {
|
||||
children: [],
|
||||
depth: parentElement.depth + 1,
|
||||
displayName,
|
||||
@@ -274,7 +274,11 @@ export default class Store extends EventEmitter {
|
||||
parentID: parentElement.id,
|
||||
type,
|
||||
weight: 1,
|
||||
});
|
||||
};
|
||||
|
||||
this._idToElement.set(id, element);
|
||||
|
||||
this.emit('elementAdded', element);
|
||||
|
||||
weightDelta = 1;
|
||||
}
|
||||
@@ -303,6 +307,8 @@ export default class Store extends EventEmitter {
|
||||
childID => childID !== id
|
||||
);
|
||||
}
|
||||
|
||||
this.emit('elementRemoved', element);
|
||||
break;
|
||||
case TREE_OPERATION_RESET_CHILDREN:
|
||||
id = ((operations[i + 1]: any): number);
|
||||
|
||||
@@ -4,15 +4,24 @@ import React from 'react';
|
||||
import styles from './ButtonIcon.css';
|
||||
|
||||
type Props = {|
|
||||
type: 'search' | 'view-dom' | 'view-source',
|
||||
type: 'close' | 'down' | 'search' | 'up' | 'view-dom' | 'view-source',
|
||||
|};
|
||||
|
||||
export default function ButtonIcon({ type }: Props) {
|
||||
let pathData = null;
|
||||
switch (type) {
|
||||
case 'close':
|
||||
pathData = PATH_CLOSE;
|
||||
break;
|
||||
case 'down':
|
||||
pathData = PATH_DOWN;
|
||||
break;
|
||||
case 'search':
|
||||
pathData = PATH_SEARCH;
|
||||
break;
|
||||
case 'up':
|
||||
pathData = PATH_UP;
|
||||
break;
|
||||
case 'view-dom':
|
||||
pathData = PATH_VIEW_DOM;
|
||||
break;
|
||||
@@ -38,12 +47,19 @@ export default function ButtonIcon({ type }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const PATH_CLOSE =
|
||||
'M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z';
|
||||
|
||||
const PATH_DOWN = 'M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z';
|
||||
|
||||
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_UP = 'M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z';
|
||||
|
||||
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
|
||||
|
||||
@@ -63,3 +63,10 @@
|
||||
.AttributeValue {
|
||||
color: var(--color-attribute-value);
|
||||
}
|
||||
|
||||
.Highlight {
|
||||
background-color: var(--color-search-match);
|
||||
}
|
||||
.CurrentHighlight {
|
||||
background-color: var(--color-search-match-current);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
// @flow
|
||||
|
||||
import React, { Fragment, useCallback, useContext } from 'react';
|
||||
import React, { Fragment, useCallback, useContext, useMemo } from 'react';
|
||||
import { ElementTypeClassOrFunction } from 'src/devtools/types';
|
||||
import { TreeContext } from './context';
|
||||
import { SearchContext } from './SearchContext';
|
||||
import { SelectedElementContext } from './SelectedElementContext';
|
||||
import Icon from './Icon';
|
||||
|
||||
@@ -56,7 +57,7 @@ export default function Element({ index, style }: Props) {
|
||||
)}
|
||||
|
||||
<span className={styles.Component}>
|
||||
{displayName}
|
||||
<DisplayName displayName={displayName} id={id} />
|
||||
{key && (
|
||||
<Fragment>
|
||||
<span className={styles.AttributeName}>key</span>=
|
||||
@@ -68,3 +69,48 @@ export default function Element({ index, style }: Props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type DisplayNameProps = {|
|
||||
displayName: string | null,
|
||||
id: number,
|
||||
|};
|
||||
|
||||
function DisplayName({ displayName, id }: DisplayNameProps) {
|
||||
const {
|
||||
currentIndex: currentSearchIndex,
|
||||
ids: searchIDs,
|
||||
text: searchText,
|
||||
} = useContext(SearchContext);
|
||||
const isSearchResult = useMemo(() => {
|
||||
return searchIDs.includes(id);
|
||||
}, [id, searchIDs]);
|
||||
const isCurrentResult =
|
||||
currentSearchIndex !== null && id === searchIDs[currentSearchIndex];
|
||||
|
||||
if (!isSearchResult || displayName === null) {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
const startIndex = displayName
|
||||
.toLowerCase()
|
||||
.indexOf(searchText.toLowerCase());
|
||||
const stopIndex = startIndex + searchText.length;
|
||||
|
||||
const children = [];
|
||||
if (startIndex > 0) {
|
||||
children.push(<span key="begin">{displayName.slice(0, startIndex)}</span>);
|
||||
}
|
||||
children.push(
|
||||
<mark
|
||||
key="middle"
|
||||
className={isCurrentResult ? styles.CurrentHighlight : styles.Highlight}
|
||||
>
|
||||
{displayName.slice(startIndex, stopIndex)}
|
||||
</mark>
|
||||
);
|
||||
if (stopIndex < displayName.length) {
|
||||
children.push(<span key="end">{displayName.slice(stopIndex)}</span>);
|
||||
}
|
||||
|
||||
return children;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Store from '../store';
|
||||
import Tree from './Tree';
|
||||
import { BridgeContext, StoreContext, TreeContext } from './context';
|
||||
import SelectedElement from './SelectedElement';
|
||||
import { SearchController } from './SearchContext';
|
||||
import { SelectedElementController } from './SelectedElementContext';
|
||||
import styles from './Elements.css';
|
||||
|
||||
@@ -51,14 +52,16 @@ export default function Elements({ bridge, browserName, themeName }: Props) {
|
||||
<StoreContext.Provider value={store}>
|
||||
<TreeContext.Provider value={treeContext}>
|
||||
<SelectedElementController>
|
||||
<div className={styles.Elements}>
|
||||
<div className={styles.TreeWrapper}>
|
||||
<Tree />
|
||||
<SearchController>
|
||||
<div className={styles.Elements}>
|
||||
<div className={styles.TreeWrapper}>
|
||||
<Tree />
|
||||
</div>
|
||||
<div className={styles.SelectedElementWrapper}>
|
||||
<SelectedElement />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.SelectedElementWrapper}>
|
||||
<SelectedElement />
|
||||
</div>
|
||||
</div>
|
||||
</SearchController>
|
||||
</SelectedElementController>
|
||||
</TreeContext.Provider>
|
||||
</StoreContext.Provider>
|
||||
|
||||
@@ -19,6 +19,6 @@
|
||||
}
|
||||
|
||||
.None {
|
||||
color: var(--color-deemphasized);
|
||||
color: var(--color-dimmer);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -18,12 +18,12 @@
|
||||
}
|
||||
|
||||
.None {
|
||||
color: var(--color-deemphasized);
|
||||
color: var(--color-dimmer);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.Empty {
|
||||
color: var(--color-deemphasized);
|
||||
color: var(--color-dimmer);
|
||||
font-style: italic;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
// @flow
|
||||
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { StoreContext } from './context';
|
||||
import { SelectedElementContext } from './SelectedElementContext';
|
||||
import Store from '../store';
|
||||
|
||||
import type { Element } from 'src/devtools/types';
|
||||
|
||||
export type SearchContextValue = {|
|
||||
get currentIndex(): number | null,
|
||||
get ids(): Array<number>,
|
||||
get text(): string,
|
||||
updateCurrentIndex(newCurrentIndex: number): void,
|
||||
updateText(newText: string): void,
|
||||
|};
|
||||
|
||||
const SearchContext = createContext<SearchContextValue>(
|
||||
((null: any): SearchContextValue)
|
||||
);
|
||||
// $FlowFixMe displayName is a valid attribute of React$Context
|
||||
SearchContext.displayName = 'SearchContext';
|
||||
|
||||
type Props = {|
|
||||
children: React$Node,
|
||||
|};
|
||||
|
||||
// TODO Remove this wrapper element once global Context.write API exists.
|
||||
function SearchController({ children }: Props) {
|
||||
const store = useContext(StoreContext);
|
||||
const selectedElement = useContext(SelectedElementContext);
|
||||
|
||||
const [currentIndex, setCurrentIndex] = useState<number | null>(null);
|
||||
const [ids, setIDs] = useState<Array<number>>([]);
|
||||
const [text, setText] = useState<string>('');
|
||||
|
||||
const updateIndexHelper = (
|
||||
newCurrentIndex: number | null,
|
||||
newOrCurrentIDs: Array<number>
|
||||
) => {
|
||||
setCurrentIndex(newCurrentIndex);
|
||||
|
||||
// Make sure a new search result is also selected.
|
||||
selectedElement.id =
|
||||
newCurrentIndex !== null ? newOrCurrentIDs[newCurrentIndex] : null;
|
||||
};
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
get currentIndex(): number | null {
|
||||
return currentIndex;
|
||||
},
|
||||
|
||||
get ids(): Array<number> {
|
||||
return ids;
|
||||
},
|
||||
|
||||
get text(): string {
|
||||
return text;
|
||||
},
|
||||
|
||||
updateCurrentIndex(newCurrentIndex: number) {
|
||||
updateIndexHelper(newCurrentIndex, ids);
|
||||
},
|
||||
|
||||
updateText(newText: string) {
|
||||
setText(newText);
|
||||
|
||||
// Find all matching elements.
|
||||
const newIDs = [];
|
||||
if (newText !== '') {
|
||||
const regExp = new RegExp(newText, 'i');
|
||||
store.roots.forEach(rootID => {
|
||||
searchTree(store, rootID, regExp, newIDs);
|
||||
});
|
||||
|
||||
setIDs(newIDs);
|
||||
|
||||
// If this is a refinement of a previous search, preserve the current index (unless it's no longer valid).
|
||||
// If it's a new search, reset the index.
|
||||
if (newIDs.length === 0) {
|
||||
updateIndexHelper(null, newIDs);
|
||||
} else if (currentIndex !== null && newText.startsWith(text)) {
|
||||
updateIndexHelper(
|
||||
Math.min(currentIndex, newIDs.length - 1),
|
||||
newIDs
|
||||
);
|
||||
} else {
|
||||
updateIndexHelper(0, newIDs);
|
||||
}
|
||||
} else {
|
||||
setIDs([]);
|
||||
updateIndexHelper(null, newIDs);
|
||||
}
|
||||
},
|
||||
}),
|
||||
[currentIndex, ids, text]
|
||||
);
|
||||
|
||||
// Listen for changes to the tree and incrementally adjust the search results.
|
||||
useLayoutEffect(() => {
|
||||
const handleElementAdded = (element: Element) => {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { displayName, id } = element;
|
||||
|
||||
// Add this item to the search results if it matches.
|
||||
const regExp = new RegExp(text, 'i');
|
||||
if (displayName !== null && regExp.test(displayName)) {
|
||||
const newElementIndex = ((store.getIndexOfElementID(id): any): number);
|
||||
|
||||
let newIDs = null;
|
||||
for (let index = 0; index < ids.length; index++) {
|
||||
const id = ids[index];
|
||||
if (
|
||||
newElementIndex < ((store.getIndexOfElementID(id): any): number)
|
||||
) {
|
||||
newIDs = ids
|
||||
.slice(0, index)
|
||||
.concat(id)
|
||||
.concat(ids.slice(index));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (newIDs === null) {
|
||||
newIDs = ids.concat(id);
|
||||
}
|
||||
|
||||
setIDs(newIDs);
|
||||
updateIndexHelper(currentIndex === null ? 0 : currentIndex, newIDs);
|
||||
}
|
||||
};
|
||||
const handleElementRemoved = (element: Element) => {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prune this item from the search results.
|
||||
const index = ids.indexOf(element.id);
|
||||
if (index >= 0) {
|
||||
const newIDs = ids.slice(0, index).concat(ids.slice(index + 1));
|
||||
setIDs(newIDs);
|
||||
|
||||
// If the results are now empty, also deselect things.
|
||||
if (newIDs.length === 0) {
|
||||
updateIndexHelper(null, newIDs);
|
||||
} else if (((currentIndex: any): number) < newIDs.length) {
|
||||
updateIndexHelper(currentIndex, newIDs);
|
||||
} else {
|
||||
updateIndexHelper(newIDs.length - 1, newIDs);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
store.addListener('elementAdded', handleElementAdded);
|
||||
store.addListener('elementRemoved', handleElementRemoved);
|
||||
|
||||
return () => {
|
||||
store.removeListener('elementAdded', handleElementAdded);
|
||||
store.removeListener('elementRemoved', handleElementRemoved);
|
||||
};
|
||||
}, [currentIndex, ids, store, text]);
|
||||
|
||||
return (
|
||||
<SearchContext.Provider value={value}>{children}</SearchContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
function searchTree(
|
||||
store: Store,
|
||||
elementID: number,
|
||||
regExp: RegExp,
|
||||
ids: Array<number>
|
||||
): void {
|
||||
const { children, displayName } = ((store.getElementByID(
|
||||
elementID
|
||||
): any): Element);
|
||||
if (displayName !== null) {
|
||||
if (regExp.test(displayName)) {
|
||||
ids.push(elementID);
|
||||
}
|
||||
}
|
||||
children.forEach(childID => searchTree(store, childID, regExp, ids));
|
||||
}
|
||||
|
||||
export { SearchContext, SearchController };
|
||||
@@ -0,0 +1,53 @@
|
||||
.SearchInput {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.Input {
|
||||
flex: 1;
|
||||
font-size: 1rem;
|
||||
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;
|
||||
}
|
||||
|
||||
.IndexLabel {
|
||||
color: var(--color-dim);
|
||||
font-size: var(--font-size-normal);
|
||||
}
|
||||
|
||||
.IconButton {
|
||||
border: none;
|
||||
background: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem;
|
||||
cursor: pointer;
|
||||
color: var(--color-button);
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
.IconButton:hover {
|
||||
color: var(--color-button-hover);
|
||||
}
|
||||
.IconButton:disabled {
|
||||
color: var(--color-dimmer);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.LeftVRule,
|
||||
.RightVRule {
|
||||
height: 20px;
|
||||
width: 1px;
|
||||
background-color: var(--color-border);
|
||||
}
|
||||
.LeftVRule {
|
||||
margin: 0 0.25rem 0 0.5rem;
|
||||
}
|
||||
.RightVRule {
|
||||
margin: 0 0.5rem 0 0.25rem;
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback, useContext, useEffect, useRef } from 'react';
|
||||
import { SearchContext } from './SearchContext';
|
||||
import ButtonIcon from './ButtonIcon';
|
||||
|
||||
import styles from './SearchInput.css';
|
||||
|
||||
type Props = {||};
|
||||
|
||||
export default function SearchInput(props: Props) {
|
||||
const {
|
||||
currentIndex,
|
||||
ids,
|
||||
updateCurrentIndex,
|
||||
updateText,
|
||||
text,
|
||||
} = useContext(SearchContext);
|
||||
|
||||
const inputRef = useRef();
|
||||
|
||||
const handleTextChange = useCallback(({ currentTarget }) => {
|
||||
updateText(currentTarget.value);
|
||||
});
|
||||
|
||||
const selectNext = useCallback(() => {
|
||||
if (currentIndex !== null) {
|
||||
updateCurrentIndex(currentIndex + 1);
|
||||
}
|
||||
}, [currentIndex]);
|
||||
|
||||
const selectPrevious = useCallback(() => {
|
||||
if (currentIndex !== null) {
|
||||
updateCurrentIndex(currentIndex - 1);
|
||||
}
|
||||
}, [currentIndex]);
|
||||
|
||||
const resetSearch = useCallback(() => {
|
||||
updateText('');
|
||||
}, [updateText]);
|
||||
|
||||
const handleInputKeyPress = useCallback(
|
||||
({ key }) => {
|
||||
if (key === 'Enter') {
|
||||
if (currentIndex !== null) {
|
||||
if (currentIndex + 1 < ids.length) {
|
||||
updateCurrentIndex(currentIndex + 1);
|
||||
} else {
|
||||
updateCurrentIndex(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
[currentIndex, ids]
|
||||
);
|
||||
|
||||
// Auto-focus search input
|
||||
useEffect(() => {
|
||||
const handleWindowKeyDown = event => {
|
||||
const { key, metaKey } = event;
|
||||
if (key === 'f' && metaKey) {
|
||||
if (inputRef.current !== null) {
|
||||
inputRef.current.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleWindowKeyDown);
|
||||
|
||||
return () => window.removeEventListener('keydown', handleWindowKeyDown);
|
||||
}, [inputRef]);
|
||||
|
||||
return (
|
||||
<div className={styles.SearchInput}>
|
||||
<input
|
||||
className={styles.Input}
|
||||
onKeyPress={handleInputKeyPress}
|
||||
onChange={handleTextChange}
|
||||
placeholder="Search (text or /regex/)"
|
||||
ref={inputRef}
|
||||
value={text}
|
||||
/>
|
||||
{!!text && (
|
||||
<span className={styles.IndexLabel}>
|
||||
{Math.min(currentIndex + 1, ids.length)} | {ids.length}
|
||||
</span>
|
||||
)}
|
||||
<div className={styles.LeftVRule} />
|
||||
<button
|
||||
className={styles.IconButton}
|
||||
disabled={currentIndex === null || currentIndex === 0}
|
||||
onClick={selectPrevious}
|
||||
title="Scroll to previous search result"
|
||||
>
|
||||
<ButtonIcon type="up" />
|
||||
</button>
|
||||
<button
|
||||
className={styles.IconButton}
|
||||
disabled={currentIndex === null || currentIndex === ids.length - 1}
|
||||
onClick={selectNext}
|
||||
title="Scroll to next search result"
|
||||
>
|
||||
<ButtonIcon type="down" />
|
||||
</button>
|
||||
<button
|
||||
className={styles.IconButton}
|
||||
disabled={!text}
|
||||
onClick={resetSearch}
|
||||
title="Reset search"
|
||||
>
|
||||
<ButtonIcon type="close" />
|
||||
</button>
|
||||
<div className={styles.RightVRule} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -78,6 +78,6 @@
|
||||
|
||||
.Loading {
|
||||
padding: 0.25rem;
|
||||
color: var(--color-deemphasized);
|
||||
color: var(--color-dimmer);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@@ -135,13 +135,13 @@ function useInspectedElement(id: number | null): InspectedElement | null {
|
||||
idRef.current = id;
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// Hide previous/stale insepected element to avoid temporarily showing the wrong values.
|
||||
setInspectedElement(null);
|
||||
|
||||
if (id === null) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
// Hide previous/stale insepected element to avoid temporarily showing the wrong values.
|
||||
setInspectedElement(null);
|
||||
|
||||
let timeoutID = null;
|
||||
|
||||
// Update the $r variable.
|
||||
@@ -152,15 +152,17 @@ function useInspectedElement(id: number | null): InspectedElement | null {
|
||||
};
|
||||
|
||||
const onInspectedElement = (inspectedElement: InspectedElement) => {
|
||||
if (idRef.current !== inspectedElement.id) {
|
||||
if (inspectedElement && inspectedElement.id !== idRef.current) {
|
||||
// Ignore bridge updates about previously selected elements.
|
||||
return;
|
||||
}
|
||||
|
||||
inspectedElement.context = hydrateHelper(inspectedElement.context);
|
||||
inspectedElement.hooks = hydrateHelper(inspectedElement.hooks);
|
||||
inspectedElement.props = hydrateHelper(inspectedElement.props);
|
||||
inspectedElement.state = hydrateHelper(inspectedElement.state);
|
||||
if (inspectedElement !== null) {
|
||||
inspectedElement.context = hydrateHelper(inspectedElement.context);
|
||||
inspectedElement.hooks = hydrateHelper(inspectedElement.hooks);
|
||||
inspectedElement.props = hydrateHelper(inspectedElement.props);
|
||||
inspectedElement.state = hydrateHelper(inspectedElement.state);
|
||||
}
|
||||
|
||||
setInspectedElement(inspectedElement);
|
||||
|
||||
|
||||
+12
-24
@@ -6,7 +6,7 @@
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.SearchRow {
|
||||
.SearchInput {
|
||||
flex: 0 0 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -15,16 +15,17 @@
|
||||
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;
|
||||
.AutoSizerWrapper {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
flex: 1 0 auto;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.List {
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-normal);
|
||||
line-height: var(--line-height-data);
|
||||
}
|
||||
|
||||
.IconButton {
|
||||
@@ -40,16 +41,3 @@
|
||||
.IconButton:hover {
|
||||
color: var(--color-button-hover);
|
||||
}
|
||||
|
||||
.AutoSizerWrapper {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
flex: 1 0 auto;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.List {
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-normal);
|
||||
line-height: var(--line-height-data);
|
||||
}
|
||||
|
||||
@@ -4,8 +4,9 @@ import React, { useContext, useEffect, useLayoutEffect, useRef } from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { SelectedElementContext } from './SelectedElementContext';
|
||||
import Element from './Element';
|
||||
import ButtonIcon from './ButtonIcon';
|
||||
import Element from './Element';
|
||||
import SearchInput from './SearchInput';
|
||||
import { TreeContext } from './context';
|
||||
|
||||
import styles from './Tree.css';
|
||||
@@ -15,7 +16,7 @@ type Props = {||};
|
||||
export default function Tree(props: Props) {
|
||||
const selectedElementContext = useContext(SelectedElementContext);
|
||||
const treeContext = useContext(TreeContext);
|
||||
const listRef = useRef();
|
||||
const listRef = useRef<FixedSizeList<any>>();
|
||||
|
||||
// Make sure a newly selected element is visible in the list.
|
||||
// This is helpful for things like the owners list.
|
||||
@@ -59,11 +60,8 @@ export default function Tree(props: Props) {
|
||||
|
||||
return (
|
||||
<div className={styles.Tree}>
|
||||
<div className={styles.SearchRow}>
|
||||
<input
|
||||
className={styles.SearchInput}
|
||||
placeholder="Search (text or /regex/)"
|
||||
/>
|
||||
<div className={styles.SearchInput}>
|
||||
<SearchInput />
|
||||
<button
|
||||
className={styles.IconButton}
|
||||
title="Select an element in the page to inspect it"
|
||||
|
||||
@@ -10,9 +10,12 @@
|
||||
--theme-light-color-border: #eeeeee;
|
||||
--theme-light-component-name: #8155cb;
|
||||
--theme-light-component-name-inverted: #ffffff;
|
||||
--theme-light-deemphasized: #cfd1d5;
|
||||
--theme-light-dim: #777d88;
|
||||
--theme-light-dimmer: #cfd1d5;
|
||||
--theme-light-jsx-arrow-brackets: #333333;
|
||||
--theme-light-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
|
||||
--theme-light-search-match: yellow;
|
||||
--theme-light-search-match-current: orange;
|
||||
--theme-light-tree-node-selected: #0088fa;
|
||||
--theme-light-tree-node-hover: #ebf1fb;
|
||||
|
||||
@@ -29,7 +32,10 @@
|
||||
--color-button-hover: var(--theme-light-button-hover);
|
||||
--color-component-name: var(--theme-light-component-name);
|
||||
--color-component-name-inverted: var(--theme-light-component-name-inverted);
|
||||
--color-deemphasized: var(--theme-light-deemphasized);
|
||||
--color-dim: var(--theme-light-dim);
|
||||
--color-dimmer: var(--theme-light-dimmer);
|
||||
--color-search-match: var(--theme-light-search-match);
|
||||
--color-search-match-current: var(--theme-light-search-match-current);
|
||||
--color-tree-jsx-arrow-brackets: var(--theme-light-jsx-arrow-brackets);
|
||||
--color-tree-jsx-arrow-brackets-inverted: var(
|
||||
--theme-light-jsx-arrow-brackets-inverted
|
||||
|
||||
Reference in New Issue
Block a user