mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Added owner tree exploration view
This commit is contained in:
+10
-5
@@ -543,6 +543,10 @@ export function attach(
|
||||
addOperation(operation);
|
||||
} else {
|
||||
const { displayName, key, type } = getDataForFiber(fiber);
|
||||
const { _debugOwner } = fiber;
|
||||
|
||||
const ownerID =
|
||||
_debugOwner !== null ? getFiberID(getPrimaryFiber(_debugOwner)) : 0;
|
||||
|
||||
let encodedDisplayName = ((null: any): Uint8Array);
|
||||
let encodedKey = ((null: any): Uint8Array);
|
||||
@@ -565,19 +569,20 @@ export function attach(
|
||||
const encodedKeySize = key === null ? 0 : encodedKey.length;
|
||||
|
||||
const operation = new Uint32Array(
|
||||
6 + encodedDisplayNameSize + encodedKeySize
|
||||
7 + encodedDisplayNameSize + encodedKeySize
|
||||
);
|
||||
operation[0] = TREE_OPERATION_ADD;
|
||||
operation[1] = id;
|
||||
operation[2] = type;
|
||||
operation[3] = getFiberID(getPrimaryFiber(parentFiber));
|
||||
operation[4] = encodedDisplayNameSize;
|
||||
operation[4] = ownerID;
|
||||
operation[5] = encodedDisplayNameSize;
|
||||
if (displayName !== null) {
|
||||
operation.set(encodedDisplayName, 5);
|
||||
operation.set(encodedDisplayName, 6);
|
||||
}
|
||||
operation[5 + encodedDisplayNameSize] = encodedKeySize;
|
||||
operation[6 + encodedDisplayNameSize] = encodedKeySize;
|
||||
if (key !== null) {
|
||||
operation.set(encodedKey, 5 + encodedDisplayNameSize + 1);
|
||||
operation.set(encodedKey, 6 + encodedDisplayNameSize + 1);
|
||||
}
|
||||
addOperation(operation);
|
||||
}
|
||||
|
||||
@@ -202,6 +202,7 @@ export default class Store extends EventEmitter {
|
||||
while (i < operations.length) {
|
||||
let id: number = ((null: any): number);
|
||||
let element: Element = ((null: any): Element);
|
||||
let ownerID: number = 0;
|
||||
let parentID: number = ((null: any): number);
|
||||
let parentElement: Element = ((null: any): Element);
|
||||
let type: ElementType = ((null: any): ElementType);
|
||||
@@ -229,6 +230,7 @@ export default class Store extends EventEmitter {
|
||||
displayName: null,
|
||||
id,
|
||||
key: null,
|
||||
ownerID: 0,
|
||||
parentID: 0,
|
||||
type,
|
||||
weight: 0,
|
||||
@@ -236,6 +238,9 @@ export default class Store extends EventEmitter {
|
||||
|
||||
haveRootsChanged = true;
|
||||
} else {
|
||||
ownerID = ((operations[i]: any): number);
|
||||
i++;
|
||||
|
||||
const displayNameLength = operations[i];
|
||||
i++;
|
||||
const displayName =
|
||||
@@ -273,6 +278,7 @@ export default class Store extends EventEmitter {
|
||||
displayName,
|
||||
id,
|
||||
key,
|
||||
ownerID,
|
||||
parentID: parentElement.id,
|
||||
type,
|
||||
weight: 1,
|
||||
|
||||
@@ -28,6 +28,9 @@ export type Element = {|
|
||||
displayName: string | null,
|
||||
key: number | string | null,
|
||||
|
||||
// Owner (if available)
|
||||
ownerID: number,
|
||||
|
||||
// How many levels deep within the tree is this element?
|
||||
// This determines how much indentation (left padding) should be used in the Elements tree.
|
||||
depth: number,
|
||||
|
||||
@@ -4,12 +4,22 @@ import React from 'react';
|
||||
import styles from './ButtonIcon.css';
|
||||
|
||||
type Props = {|
|
||||
type: 'close' | 'down' | 'search' | 'up' | 'view-dom' | 'view-source',
|
||||
type:
|
||||
| 'back'
|
||||
| 'close'
|
||||
| 'down'
|
||||
| 'search'
|
||||
| 'up'
|
||||
| 'view-dom'
|
||||
| 'view-source',
|
||||
|};
|
||||
|
||||
export default function ButtonIcon({ type }: Props) {
|
||||
let pathData = null;
|
||||
switch (type) {
|
||||
case 'back':
|
||||
pathData = PATH_BACK;
|
||||
break;
|
||||
case 'close':
|
||||
pathData = PATH_CLOSE;
|
||||
break;
|
||||
@@ -47,6 +57,12 @@ export default function ButtonIcon({ type }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
const PATH_BACK = `
|
||||
M22 3H7c-.69 0-1.23.35-1.59.88L0 12l5.41 8.11c.36.53.9.89 1.59.89h15c1.1 0 2-.9
|
||||
2-2V5c0-1.1-.9-2-2-2zm-3 12.59L17.59 17 14 13.41 10.41 17 9 15.59 12.59 12 9 8.41
|
||||
10.41 7 14 10.59 17.59 7 19 8.41 15.41 12 19 15.59z
|
||||
`;
|
||||
|
||||
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';
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ import { createRegExp } from './utils';
|
||||
import { SearchAndSelectionContext } from './SearchAndSelectionContext';
|
||||
import Icon from './Icon';
|
||||
|
||||
import type { Element } from '../types';
|
||||
|
||||
import styles from './Element.css';
|
||||
|
||||
type Props = {
|
||||
@@ -14,25 +16,32 @@ type Props = {
|
||||
style: Object,
|
||||
};
|
||||
|
||||
export default function Element({ index, style }: Props) {
|
||||
export default function ElementView({ index, style }: Props) {
|
||||
const { store } = useContext(TreeContext);
|
||||
const element = store.getElementAtIndex(index);
|
||||
|
||||
// DevTools are rendered in concurrent mode.
|
||||
// It's possible the store has updated since the commit that triggered this render.
|
||||
// So we need to guard against an undefined element.
|
||||
// TODO: Handle this by switching to a Suspense based approach.
|
||||
if (element == null) {
|
||||
return null;
|
||||
const {
|
||||
ownerList,
|
||||
pushOwnerList,
|
||||
selectedElementID,
|
||||
selectElementWithID,
|
||||
} = useContext(SearchAndSelectionContext);
|
||||
|
||||
const element =
|
||||
ownerList !== null
|
||||
? ((store.getElementByID(ownerList[index]): any): Element)
|
||||
: ((store.getElementAtIndex(index): any): Element);
|
||||
const { children, depth, displayName, id, key, type } = element;
|
||||
|
||||
let calculatedDepth = depth;
|
||||
if (ownerList !== null) {
|
||||
const owner = ((store.getElementByID(ownerList[0]): any): Element);
|
||||
calculatedDepth = depth - owner.depth;
|
||||
}
|
||||
|
||||
const handleDoubleClick = useCallback(() => pushOwnerList(id), [id]);
|
||||
|
||||
// TODO Add click and key handlers for toggling element open/close state.
|
||||
|
||||
const { children, depth, displayName, id, key, type } = element;
|
||||
|
||||
const { selectedElementID, selectElementWithID } = useContext(
|
||||
SearchAndSelectionContext
|
||||
);
|
||||
const handleClick = useCallback(
|
||||
({ metaKey }) => selectElementWithID(metaKey ? null : id),
|
||||
[id]
|
||||
@@ -45,9 +54,10 @@ export default function Element({ index, style }: Props) {
|
||||
<div
|
||||
className={isSelected ? styles.SelectedElement : styles.Element}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
style={{
|
||||
...style, // "style" comes from react-window
|
||||
paddingLeft: `${1 + depth}rem`,
|
||||
paddingLeft: `${1 + calculatedDepth}rem`,
|
||||
}}
|
||||
>
|
||||
{children.length > 0 && (
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
.OwnerStack {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.Component,
|
||||
.FocusedComponent {
|
||||
padding: 0.25rem;
|
||||
margin-right: 0.5rem;
|
||||
color: var(--color-component-name);
|
||||
font-family: var(--font-family-monospace);
|
||||
font-size: var(--font-size-normal);
|
||||
}
|
||||
|
||||
.Component {
|
||||
cursor: pointer;
|
||||
color: var(--color-component-name);
|
||||
background: var(--color-dimmest);
|
||||
border-radius: 0.125rem;
|
||||
}
|
||||
.Component:hover {
|
||||
background: var(--color-dimmer);
|
||||
}
|
||||
|
||||
.VRule {
|
||||
height: 20px;
|
||||
width: 1px;
|
||||
background-color: var(--color-border);
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
// @flow
|
||||
|
||||
import React, { useCallback, useContext } from 'react';
|
||||
import ButtonIcon from './ButtonIcon';
|
||||
import { SearchAndSelectionContext } from './SearchAndSelectionContext';
|
||||
import { TreeContext } from './context';
|
||||
|
||||
import type { Element } from '../types';
|
||||
|
||||
import styles from './OwnersStack.css';
|
||||
|
||||
export default function OwnerStack() {
|
||||
const { clearOwnerList, ownerIDStack } = useContext(
|
||||
SearchAndSelectionContext
|
||||
);
|
||||
|
||||
// $FlowFixMe "Missing type annotation for U" whatever that means
|
||||
const elements = ownerIDStack.map(id => <ElementView key={id} id={id} />);
|
||||
|
||||
return (
|
||||
<div className={styles.OwnerStack}>
|
||||
<button
|
||||
className={styles.IconButton}
|
||||
onClick={clearOwnerList}
|
||||
title="Back to tree view"
|
||||
>
|
||||
<ButtonIcon type="back" />
|
||||
</button>
|
||||
<div className={styles.VRule} />
|
||||
{elements}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type Props = {
|
||||
id: number,
|
||||
};
|
||||
|
||||
function ElementView({ id }: Props) {
|
||||
const { ownerIDStack, popToOwnerList } = useContext(
|
||||
SearchAndSelectionContext
|
||||
);
|
||||
const { store } = useContext(TreeContext);
|
||||
const { displayName } = ((store.getElementByID(id): any): Element);
|
||||
|
||||
const isCurrentlyFocusedOwner = ownerIDStack[ownerIDStack.length - 1] === id;
|
||||
|
||||
const handleClick = useCallback(() => popToOwnerList(id), [
|
||||
id,
|
||||
popToOwnerList,
|
||||
]);
|
||||
|
||||
return (
|
||||
<span
|
||||
className={
|
||||
isCurrentlyFocusedOwner ? styles.FocusedComponent : styles.Component
|
||||
}
|
||||
onClick={handleClick}
|
||||
>
|
||||
{displayName}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
// @flow
|
||||
|
||||
// This context contains both the current search state and the selected element.
|
||||
// These values are combined into a single context because changes in one often impact the other.
|
||||
// This context contains both the search, owners stack, and selected element states.
|
||||
// These values are combined into a single context because changes in one often impact the others.
|
||||
// Combining them enables us to avoid cascading renders.
|
||||
|
||||
import React, {
|
||||
@@ -19,7 +19,9 @@ import Store from '../store';
|
||||
import type { Element } from 'src/devtools/types';
|
||||
|
||||
type Context = {|
|
||||
// Read current context values:
|
||||
// Read current context values
|
||||
ownerIDStack: Array<number>,
|
||||
ownerList: Array<number> | null,
|
||||
searchIndex: number | null,
|
||||
searchResults: Array<number>,
|
||||
searchText: string,
|
||||
@@ -27,10 +29,14 @@ type Context = {|
|
||||
selectedElementIndex: number | null,
|
||||
|
||||
// Update current context values:
|
||||
clearOwnerList: () => void,
|
||||
decrementSearchIndex(): void,
|
||||
incrementSearchIndex(): void,
|
||||
popToOwnerList(elementID: number): void,
|
||||
pushOwnerList(elementID: number): void,
|
||||
selectElementWithID(id: number | null): void,
|
||||
selectElementAtIndex(index: number | null): void,
|
||||
selectNextElementInTree(): void,
|
||||
selectPreviousElementInTree(): void,
|
||||
updateSearchText(searchText: string): void,
|
||||
|};
|
||||
|
||||
@@ -41,6 +47,8 @@ const SearchAndSelectionContext = createContext<Context>(
|
||||
SearchAndSelectionContext.displayName = 'SearchAndSelectionContext';
|
||||
|
||||
type State = {|
|
||||
ownerIDStack: Array<number>,
|
||||
ownerList: Array<number> | null,
|
||||
searchIndex: number | null,
|
||||
searchResults: Array<number>,
|
||||
searchText: string,
|
||||
@@ -50,11 +58,15 @@ type State = {|
|
||||
|
||||
type Action = {|
|
||||
type:
|
||||
| 'CLEAR_OWNER_LIST'
|
||||
| 'DECREMENT_SEARCH_INDEX'
|
||||
| 'INCREMENT_SEARCH_INDEX'
|
||||
| 'REFINE_SEARCH_RESULTS'
|
||||
| 'POP_TO_OWNER_LIST'
|
||||
| 'PUSH_OWNER_LIST'
|
||||
| 'REFINE_AFTER_MUTATION'
|
||||
| 'SELECT_ELEMENT_WITH_ID'
|
||||
| 'SELECT_ELEMENT_AT_INDEX'
|
||||
| 'SELECT_NEXT_ELEMENT_IN_TREE'
|
||||
| 'SELECT_PREVIOUS_ELEMENT_IN_TREE'
|
||||
| 'UPDATE_SEARCH_TEXT',
|
||||
payload?: any,
|
||||
|};
|
||||
@@ -73,16 +85,70 @@ function SearchAndSelectionController({ children }: Props) {
|
||||
const reducer = useMemo(
|
||||
() =>
|
||||
function reducer(state: State, action: Action): State {
|
||||
const { searchIndex, searchResults, searchText } = state;
|
||||
const {
|
||||
ownerIDStack,
|
||||
ownerList,
|
||||
searchIndex,
|
||||
searchResults,
|
||||
searchText,
|
||||
selectedElementID,
|
||||
selectedElementIndex,
|
||||
} = state;
|
||||
const { payload, type } = action;
|
||||
|
||||
let newSearchIndex,
|
||||
let element,
|
||||
elementID,
|
||||
newOwnerList,
|
||||
newOwnerIDStack,
|
||||
newSearchIndex,
|
||||
newSearchResults,
|
||||
newSearchText,
|
||||
newSelectedElementID;
|
||||
newSelectedElementID,
|
||||
newSelectedElementIndex;
|
||||
|
||||
const updateSelectedElementIndexHelper = (
|
||||
newSelectedElementIndex: number
|
||||
) => {
|
||||
if (ownerList !== null) {
|
||||
newSelectedElementID =
|
||||
newSelectedElementIndex !== null
|
||||
? ownerList[newSelectedElementIndex]
|
||||
: null;
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedElementID: newSelectedElementID,
|
||||
selectedElementIndex: newSelectedElementIndex,
|
||||
};
|
||||
} else {
|
||||
element =
|
||||
newSelectedElementIndex !== null
|
||||
? store.getElementAtIndex(newSelectedElementIndex)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedElementID: element !== null ? element.id : null,
|
||||
selectedElementIndex: newSelectedElementIndex,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
switch (type) {
|
||||
case 'CLEAR_OWNER_LIST':
|
||||
return {
|
||||
...state,
|
||||
ownerList: null,
|
||||
ownerIDStack: [],
|
||||
};
|
||||
|
||||
case 'DECREMENT_SEARCH_INDEX':
|
||||
if (ownerList !== null) {
|
||||
throw Error(
|
||||
`Can't update search index while owner list is active`
|
||||
);
|
||||
}
|
||||
|
||||
if (searchResults.length === 0) return state;
|
||||
|
||||
newSearchIndex =
|
||||
@@ -101,6 +167,12 @@ function SearchAndSelectionController({ children }: Props) {
|
||||
};
|
||||
|
||||
case 'INCREMENT_SEARCH_INDEX':
|
||||
if (ownerList !== null) {
|
||||
throw Error(
|
||||
`Can't update search index while owner list is active`
|
||||
);
|
||||
}
|
||||
|
||||
if (searchResults.length === 0) return state;
|
||||
|
||||
newSearchIndex =
|
||||
@@ -118,108 +190,245 @@ function SearchAndSelectionController({ children }: Props) {
|
||||
),
|
||||
};
|
||||
|
||||
case 'REFINE_SEARCH_RESULTS':
|
||||
case 'POP_TO_OWNER_LIST':
|
||||
elementID = ((payload: any): number);
|
||||
return {
|
||||
...state,
|
||||
ownerList: calculateCurrentOwnerList(
|
||||
store,
|
||||
elementID,
|
||||
elementID,
|
||||
[]
|
||||
),
|
||||
ownerIDStack: ownerIDStack.slice(
|
||||
0,
|
||||
ownerIDStack.indexOf(elementID) + 1
|
||||
),
|
||||
selectedElementIndex: null,
|
||||
selectedElementID: null,
|
||||
};
|
||||
|
||||
case 'PUSH_OWNER_LIST':
|
||||
elementID = ((payload: any): number);
|
||||
if (ownerIDStack.includes(elementID)) return state;
|
||||
|
||||
// Add this new owner, and fill in the owners above it as well.
|
||||
const ownerIDsToConcat = [];
|
||||
let currentOwnerID = elementID;
|
||||
while (
|
||||
currentOwnerID !== 0 &&
|
||||
ownerIDStack[ownerIDStack.length - 1] !== currentOwnerID
|
||||
) {
|
||||
ownerIDsToConcat.unshift(currentOwnerID);
|
||||
currentOwnerID = ((store.getElementByID(
|
||||
currentOwnerID
|
||||
): any): Element).ownerID;
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
ownerList: calculateCurrentOwnerList(
|
||||
store,
|
||||
elementID,
|
||||
elementID,
|
||||
[]
|
||||
),
|
||||
ownerIDStack: ownerIDStack.concat(ownerIDsToConcat),
|
||||
selectedElementIndex: null,
|
||||
selectedElementID: null,
|
||||
searchIndex: null,
|
||||
searchResults: [],
|
||||
searchText: '',
|
||||
};
|
||||
|
||||
case 'REFINE_AFTER_MUTATION':
|
||||
const [
|
||||
addedElementIDs,
|
||||
removedElementIDs,
|
||||
] = ((payload: any): Array<Uint32Array>);
|
||||
|
||||
newSearchResults = searchResults;
|
||||
newOwnerList = ownerList;
|
||||
newOwnerIDStack = ownerIDStack;
|
||||
newSearchIndex = searchIndex;
|
||||
newSearchResults = searchResults;
|
||||
newSearchText = searchText;
|
||||
newSelectedElementID = selectedElementID;
|
||||
newSelectedElementIndex = selectedElementIndex;
|
||||
|
||||
removedElementIDs.forEach(id => {
|
||||
// Prune this item from the search results.
|
||||
const index = newSearchResults.indexOf(id);
|
||||
if (index >= 0) {
|
||||
newSearchResults = newSearchResults
|
||||
.slice(0, index)
|
||||
.concat(newSearchResults.slice(index + 1));
|
||||
if (searchText !== '') {
|
||||
removedElementIDs.forEach(id => {
|
||||
// Prune this item from the search results.
|
||||
const index = newSearchResults.indexOf(id);
|
||||
if (index >= 0) {
|
||||
newSearchResults = newSearchResults
|
||||
.slice(0, index)
|
||||
.concat(newSearchResults.slice(index + 1));
|
||||
|
||||
// If the results are now empty, also deselect things.
|
||||
if (newSearchResults.length === 0) {
|
||||
newSearchIndex = null;
|
||||
} else if (
|
||||
((newSearchIndex: any): number) >= newSearchResults.length
|
||||
) {
|
||||
newSearchIndex = newSearchResults.length - 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addedElementIDs.forEach(id => {
|
||||
const { displayName } = ((store.getElementByID(
|
||||
id
|
||||
): any): Element);
|
||||
|
||||
// Add this item to the search results if it matches.
|
||||
const regExp = createRegExp(searchText);
|
||||
if (displayName !== null && regExp.test(displayName)) {
|
||||
const newElementIndex = ((store.getIndexOfElementID(
|
||||
id
|
||||
): any): number);
|
||||
|
||||
let foundMatch = false;
|
||||
for (let index = 0; index < newSearchResults.length; index++) {
|
||||
const id = newSearchResults[index];
|
||||
if (
|
||||
newElementIndex <
|
||||
((store.getIndexOfElementID(id): any): number)
|
||||
// If the results are now empty, also deselect things.
|
||||
if (newSearchResults.length === 0) {
|
||||
newSearchIndex = null;
|
||||
} else if (
|
||||
((newSearchIndex: any): number) >= newSearchResults.length
|
||||
) {
|
||||
foundMatch = true;
|
||||
newSearchResults = newSearchResults
|
||||
.slice(0, index)
|
||||
.concat(id)
|
||||
.concat(newSearchResults.slice(index));
|
||||
break;
|
||||
newSearchIndex = newSearchResults.length - 1;
|
||||
}
|
||||
}
|
||||
if (!foundMatch) {
|
||||
newSearchResults = newSearchResults.concat(id);
|
||||
});
|
||||
|
||||
addedElementIDs.forEach(id => {
|
||||
const { displayName } = ((store.getElementByID(
|
||||
id
|
||||
): any): Element);
|
||||
|
||||
// Add this item to the search results if it matches.
|
||||
const regExp = createRegExp(searchText);
|
||||
if (displayName !== null && regExp.test(displayName)) {
|
||||
const newElementIndex = ((store.getIndexOfElementID(
|
||||
id
|
||||
): any): number);
|
||||
|
||||
let foundMatch = false;
|
||||
for (
|
||||
let index = 0;
|
||||
index < newSearchResults.length;
|
||||
index++
|
||||
) {
|
||||
const id = newSearchResults[index];
|
||||
if (
|
||||
newElementIndex <
|
||||
((store.getIndexOfElementID(id): any): number)
|
||||
) {
|
||||
foundMatch = true;
|
||||
newSearchResults = newSearchResults
|
||||
.slice(0, index)
|
||||
.concat(id)
|
||||
.concat(newSearchResults.slice(index));
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!foundMatch) {
|
||||
newSearchResults = newSearchResults.concat(id);
|
||||
}
|
||||
|
||||
newSearchIndex = newSearchIndex === null ? 0 : newSearchIndex;
|
||||
}
|
||||
});
|
||||
|
||||
newSearchIndex = newSearchIndex === null ? 0 : newSearchIndex;
|
||||
newSelectedElementID =
|
||||
newSearchIndex !== null
|
||||
? newSearchResults[newSearchIndex]
|
||||
: null;
|
||||
newSelectedElementIndex =
|
||||
newSelectedElementID !== null
|
||||
? store.getIndexOfElementID(newSelectedElementID)
|
||||
: null;
|
||||
}
|
||||
|
||||
// If an item in the owner stack has been removed from the tree, unwind it.
|
||||
if (ownerIDStack.length > 0) {
|
||||
let indexOfRemovedItem = -1;
|
||||
for (let i = 0; i < ownerIDStack.length; i++) {
|
||||
if (removedElementIDs.includes(ownerIDStack[i])) {
|
||||
indexOfRemovedItem = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
newSelectedElementID =
|
||||
newSearchIndex !== null ? newSearchResults[newSearchIndex] : null;
|
||||
if (indexOfRemovedItem >= 0) {
|
||||
newOwnerIDStack = ownerIDStack.slice(0, indexOfRemovedItem);
|
||||
}
|
||||
|
||||
if (newOwnerIDStack.length === 0) {
|
||||
newOwnerList = null;
|
||||
} else {
|
||||
elementID = newOwnerIDStack[newOwnerIDStack.length - 1];
|
||||
|
||||
newOwnerList = calculateCurrentOwnerList(
|
||||
store,
|
||||
elementID,
|
||||
elementID,
|
||||
[]
|
||||
);
|
||||
|
||||
newSelectedElementIndex =
|
||||
newSelectedElementID !== null
|
||||
? newOwnerList.indexOf(newSelectedElementID)
|
||||
: null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...state,
|
||||
ownerIDStack: newOwnerIDStack,
|
||||
ownerList: newOwnerList,
|
||||
searchIndex: newSearchIndex,
|
||||
searchResults: newSearchResults,
|
||||
selectedElementID: newSelectedElementID,
|
||||
selectedElementIndex:
|
||||
newSelectedElementID !== null
|
||||
? store.getIndexOfElementID(newSelectedElementID)
|
||||
: null,
|
||||
selectedElementIndex: newSelectedElementIndex,
|
||||
};
|
||||
|
||||
case 'SELECT_ELEMENT_WITH_ID':
|
||||
newSelectedElementID = ((payload: any): number | null);
|
||||
newSelectedElementIndex = null;
|
||||
|
||||
if (newSelectedElementID !== null) {
|
||||
if (ownerList !== null) {
|
||||
newSelectedElementIndex = ownerList.indexOf(
|
||||
newSelectedElementID
|
||||
);
|
||||
} else {
|
||||
newSelectedElementIndex = store.getIndexOfElementID(
|
||||
newSelectedElementID
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const isInOwnerList =
|
||||
newSelectedElementID !== null &&
|
||||
ownerList !== null &&
|
||||
ownerList.includes(newSelectedElementID);
|
||||
|
||||
return {
|
||||
...state,
|
||||
ownerIDStack: isInOwnerList ? ownerIDStack : [],
|
||||
ownerList: isInOwnerList ? ownerList : null,
|
||||
selectedElementID: newSelectedElementID,
|
||||
selectedElementIndex:
|
||||
newSelectedElementID !== null
|
||||
? store.getIndexOfElementID(newSelectedElementID)
|
||||
: null,
|
||||
selectedElementIndex: newSelectedElementIndex,
|
||||
};
|
||||
|
||||
case 'SELECT_ELEMENT_AT_INDEX':
|
||||
newSearchIndex = ((payload: any): number | null);
|
||||
case 'SELECT_NEXT_ELEMENT_IN_TREE':
|
||||
if (selectedElementIndex === null) {
|
||||
return state;
|
||||
} else if (ownerList !== null) {
|
||||
return updateSelectedElementIndexHelper(
|
||||
selectedElementIndex + 1 < ownerList.length
|
||||
? selectedElementIndex + 1
|
||||
: selectedElementIndex
|
||||
);
|
||||
} else {
|
||||
return updateSelectedElementIndexHelper(
|
||||
selectedElementIndex + 1 < store.numElements
|
||||
? selectedElementIndex + 1
|
||||
: selectedElementIndex
|
||||
);
|
||||
}
|
||||
|
||||
const element =
|
||||
newSearchIndex !== null
|
||||
? store.getElementAtIndex(newSearchIndex)
|
||||
: null;
|
||||
|
||||
return {
|
||||
...state,
|
||||
selectedElementID: element !== null ? element.id : null,
|
||||
selectedElementIndex: newSearchIndex,
|
||||
};
|
||||
case 'SELECT_PREVIOUS_ELEMENT_IN_TREE':
|
||||
if (selectedElementIndex === null) {
|
||||
return state;
|
||||
} else if (ownerList !== null) {
|
||||
return updateSelectedElementIndexHelper(
|
||||
selectedElementIndex > 0
|
||||
? selectedElementIndex - 1
|
||||
: selectedElementIndex
|
||||
);
|
||||
} else {
|
||||
return updateSelectedElementIndexHelper(
|
||||
selectedElementIndex > 0
|
||||
? selectedElementIndex - 1
|
||||
: selectedElementIndex
|
||||
);
|
||||
}
|
||||
|
||||
case 'UPDATE_SEARCH_TEXT':
|
||||
newSearchIndex = searchIndex;
|
||||
@@ -275,6 +484,8 @@ function SearchAndSelectionController({ children }: Props) {
|
||||
);
|
||||
|
||||
const [state, dispatch] = useReducer(reducer, {
|
||||
ownerIDStack: [],
|
||||
ownerList: null,
|
||||
searchIndex: null,
|
||||
searchResults: [],
|
||||
searchText: '',
|
||||
@@ -282,6 +493,10 @@ function SearchAndSelectionController({ children }: Props) {
|
||||
selectedElementIndex: null,
|
||||
});
|
||||
|
||||
const clearOwnerList = useCallback(
|
||||
() => dispatch({ type: 'CLEAR_OWNER_LIST' }),
|
||||
[dispatch]
|
||||
);
|
||||
const decrementSearchIndex = useCallback(
|
||||
() => dispatch({ type: 'DECREMENT_SEARCH_INDEX' }),
|
||||
[dispatch]
|
||||
@@ -290,14 +505,27 @@ function SearchAndSelectionController({ children }: Props) {
|
||||
() => dispatch({ type: 'INCREMENT_SEARCH_INDEX' }),
|
||||
[dispatch]
|
||||
);
|
||||
const popToOwnerList = useCallback(
|
||||
(elementID: number) =>
|
||||
dispatch({ type: 'POP_TO_OWNER_LIST', payload: elementID }),
|
||||
[dispatch]
|
||||
);
|
||||
const pushOwnerList = useCallback(
|
||||
(elementID: number) =>
|
||||
dispatch({ type: 'PUSH_OWNER_LIST', payload: elementID }),
|
||||
[dispatch]
|
||||
);
|
||||
const selectElementWithID = useCallback(
|
||||
(id: number | null) =>
|
||||
dispatch({ type: 'SELECT_ELEMENT_WITH_ID', payload: id }),
|
||||
[dispatch]
|
||||
);
|
||||
const selectElementAtIndex = useCallback(
|
||||
(index: number | null) =>
|
||||
dispatch({ type: 'SELECT_ELEMENT_AT_INDEX', payload: index }),
|
||||
const selectNextElementInTree = useCallback(
|
||||
() => dispatch({ type: 'SELECT_NEXT_ELEMENT_IN_TREE' }),
|
||||
[dispatch]
|
||||
);
|
||||
const selectPreviousElementInTree = useCallback(
|
||||
() => dispatch({ type: 'SELECT_PREVIOUS_ELEMENT_IN_TREE' }),
|
||||
[dispatch]
|
||||
);
|
||||
const updateSearchText = useCallback(
|
||||
@@ -308,16 +536,22 @@ function SearchAndSelectionController({ children }: Props) {
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
ownerList: state.ownerList,
|
||||
ownerIDStack: state.ownerIDStack,
|
||||
searchIndex: state.searchIndex,
|
||||
searchResults: state.searchResults,
|
||||
searchText: state.searchText,
|
||||
selectedElementID: state.selectedElementID,
|
||||
selectedElementIndex: state.selectedElementIndex,
|
||||
|
||||
clearOwnerList,
|
||||
decrementSearchIndex,
|
||||
incrementSearchIndex,
|
||||
popToOwnerList,
|
||||
pushOwnerList,
|
||||
selectElementWithID,
|
||||
selectElementAtIndex,
|
||||
selectNextElementInTree,
|
||||
selectPreviousElementInTree,
|
||||
updateSearchText,
|
||||
}),
|
||||
[state]
|
||||
@@ -329,12 +563,12 @@ function SearchAndSelectionController({ children }: Props) {
|
||||
addedElementIDs,
|
||||
removedElementIDs,
|
||||
]: Array<Uint32Array>) => {
|
||||
if (!state.searchText) {
|
||||
if (!state.searchText && state.ownerList === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch({
|
||||
type: 'REFINE_SEARCH_RESULTS',
|
||||
type: 'REFINE_AFTER_MUTATION',
|
||||
payload: [addedElementIDs, removedElementIDs],
|
||||
});
|
||||
};
|
||||
@@ -351,6 +585,33 @@ function SearchAndSelectionController({ children }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function calculateCurrentOwnerList(
|
||||
store: Store,
|
||||
rootOwnerID: number,
|
||||
elementID: number,
|
||||
ownerList: Array<number>
|
||||
): Array<number> {
|
||||
if (elementID === rootOwnerID) {
|
||||
ownerList.push(elementID);
|
||||
const { children } = ((store.getElementByID(elementID): any): Element);
|
||||
children.forEach(childID =>
|
||||
calculateCurrentOwnerList(store, rootOwnerID, childID, ownerList)
|
||||
);
|
||||
} else {
|
||||
const { children, ownerID } = ((store.getElementByID(
|
||||
elementID
|
||||
): any): Element);
|
||||
if (ownerID === rootOwnerID) {
|
||||
ownerList.push(elementID);
|
||||
children.forEach(childID =>
|
||||
calculateCurrentOwnerList(store, rootOwnerID, childID, ownerList)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return ownerList;
|
||||
}
|
||||
|
||||
function recursivelySearchTree(
|
||||
store: Store,
|
||||
elementID: number,
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext, useLayoutEffect, useRef, useState } from 'react';
|
||||
import React, {
|
||||
useCallback,
|
||||
useContext,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { SearchAndSelectionContext } from './SearchAndSelectionContext';
|
||||
import { BridgeContext, StoreContext } from './context';
|
||||
import ButtonIcon from './ButtonIcon';
|
||||
@@ -75,8 +81,6 @@ type InspectedElementViewProps = {|
|
||||
|};
|
||||
|
||||
function InspectedElementView({ inspectedElement }: InspectedElementViewProps) {
|
||||
const { selectElementWithID } = useContext(SearchAndSelectionContext);
|
||||
|
||||
let { context, hooks, owners, props, state } = inspectedElement;
|
||||
|
||||
return (
|
||||
@@ -90,14 +94,11 @@ function InspectedElementView({ inspectedElement }: InspectedElementViewProps) {
|
||||
<div className={styles.Owners}>
|
||||
<div>owner stack</div>
|
||||
{owners.map(owner => (
|
||||
<div
|
||||
<OwnerView
|
||||
key={owner.id}
|
||||
className={styles.Owner}
|
||||
onClick={() => selectElementWithID(owner.id)}
|
||||
title={owner.displayName}
|
||||
>
|
||||
{owner.displayName}
|
||||
</div>
|
||||
displayName={owner.displayName}
|
||||
id={owner.id}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
@@ -105,6 +106,23 @@ function InspectedElementView({ inspectedElement }: InspectedElementViewProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function OwnerView({ displayName, id }: { displayName: string, id: number }) {
|
||||
const { selectElementWithID } = useContext(SearchAndSelectionContext);
|
||||
|
||||
const handleClick = useCallback(() => selectElementWithID(id), [id]);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={styles.Owner}
|
||||
onClick={handleClick}
|
||||
title={displayName}
|
||||
>
|
||||
{displayName}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function hydrateHelper(dehydratedData: DehydratedData | null): Object | null {
|
||||
if (dehydratedData !== null) {
|
||||
return hydrate(dehydratedData.data, dehydratedData.cleaned);
|
||||
|
||||
+37
-17
@@ -1,11 +1,18 @@
|
||||
// @flow
|
||||
|
||||
import React, { useContext, useEffect, useLayoutEffect, useRef } from 'react';
|
||||
import React, {
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
} from 'react';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { SearchAndSelectionContext } from './SearchAndSelectionContext';
|
||||
import ButtonIcon from './ButtonIcon';
|
||||
import Element from './Element';
|
||||
import OwnersStack from './OwnersStack';
|
||||
import SearchInput from './SearchInput';
|
||||
import { TreeContext } from './context';
|
||||
|
||||
@@ -14,9 +21,13 @@ import styles from './Tree.css';
|
||||
type Props = {||};
|
||||
|
||||
export default function Tree(props: Props) {
|
||||
const { selectedElementIndex, selectElementAtIndex } = useContext(
|
||||
SearchAndSelectionContext
|
||||
);
|
||||
const {
|
||||
ownerIDStack,
|
||||
ownerList,
|
||||
selectedElementIndex,
|
||||
selectNextElementInTree,
|
||||
selectPreviousElementInTree,
|
||||
} = useContext(SearchAndSelectionContext);
|
||||
const treeContext = useContext(TreeContext);
|
||||
const listRef = useRef<FixedSizeList<any>>();
|
||||
|
||||
@@ -30,22 +41,17 @@ export default function Tree(props: Props) {
|
||||
|
||||
// Navigate the tree with up/down arrow keys.
|
||||
useEffect(() => {
|
||||
// TODO Use selectNextElement / selectPreviousElement (with dispatch)
|
||||
|
||||
const handleKeyDown = event => {
|
||||
// eslint-disable-next-line default-case
|
||||
switch (event.key) {
|
||||
case 'ArrowDown':
|
||||
if (
|
||||
selectedElementIndex !== null &&
|
||||
selectedElementIndex + 1 < treeContext.size
|
||||
) {
|
||||
selectElementAtIndex(((selectedElementIndex: any): number) + 1);
|
||||
}
|
||||
selectNextElementInTree();
|
||||
event.preventDefault();
|
||||
break;
|
||||
case 'ArrowUp':
|
||||
if (selectedElementIndex !== null && selectedElementIndex > 0) {
|
||||
selectElementAtIndex(((selectedElementIndex: any): number) - 1);
|
||||
}
|
||||
selectPreviousElementInTree();
|
||||
event.preventDefault();
|
||||
break;
|
||||
}
|
||||
@@ -56,12 +62,26 @@ export default function Tree(props: Props) {
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [selectedElementIndex, selectElementAtIndex, treeContext]);
|
||||
}, [
|
||||
selectedElementIndex,
|
||||
selectNextElementInTree,
|
||||
selectPreviousElementInTree,
|
||||
treeContext,
|
||||
]);
|
||||
|
||||
const itemCount = ownerList !== null ? ownerList.length : treeContext.size;
|
||||
|
||||
// Let react-window know to re-render any time the underlying tree data changes.
|
||||
// This includes the owner context, since it controls a filtered view of the tree.
|
||||
const itemData = useMemo(() => ({ treeContext, ownerList }), [
|
||||
treeContext,
|
||||
ownerList,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={styles.Tree}>
|
||||
<div className={styles.SearchInput}>
|
||||
<SearchInput />
|
||||
{ownerIDStack.length > 0 ? <OwnersStack /> : <SearchInput />}
|
||||
<button
|
||||
className={styles.IconButton}
|
||||
title="Select an element in the page to inspect it"
|
||||
@@ -75,8 +95,8 @@ export default function Tree(props: Props) {
|
||||
<FixedSizeList
|
||||
className={styles.List}
|
||||
height={height}
|
||||
itemCount={treeContext.size}
|
||||
itemData={treeContext}
|
||||
itemCount={itemCount}
|
||||
itemData={itemData}
|
||||
itemSize={20}
|
||||
ref={listRef}
|
||||
width={width}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
--theme-light-component-name-inverted: #ffffff;
|
||||
--theme-light-dim: #777d88;
|
||||
--theme-light-dimmer: #cfd1d5;
|
||||
--theme-light-dimmest: #eff0f1;
|
||||
--theme-light-jsx-arrow-brackets: #333333;
|
||||
--theme-light-jsx-arrow-brackets-inverted: rgba(255, 255, 255, 0.7);
|
||||
--theme-light-search-match: yellow;
|
||||
@@ -34,6 +35,7 @@
|
||||
--color-component-name-inverted: var(--theme-light-component-name-inverted);
|
||||
--color-dim: var(--theme-light-dim);
|
||||
--color-dimmer: var(--theme-light-dimmer);
|
||||
--color-dimmest: var(--theme-light-dimmest);
|
||||
--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);
|
||||
|
||||
Reference in New Issue
Block a user