Match-id-0a69bdf730a5aa30cbb7385fbf94f300b6222654

This commit is contained in:
* 2022-04-12 10:51:42 +08:00 committed by *
commit 72a11c9725
16 changed files with 683 additions and 150 deletions

View File

@ -2,8 +2,9 @@ import styles from './ComponentsInfo.less';
import Eye from '../svgs/Eye';
import Debug from '../svgs/Debug';
import Copy from '../svgs/Copy';
import Arrow from '../svgs/Arrow';
import Triangle from '../svgs/Triangle';
import { useState } from 'horizon';
import { IData } from './VTree';
type IComponentInfo = {
name: string;
@ -12,7 +13,9 @@ type IComponentInfo = {
context?: IAttr[];
state?: IAttr[];
hooks?: IAttr[];
}
};
parents: IData[];
onClickParent: (item: IData) => void;
};
type IAttr = {
@ -22,24 +25,29 @@ type IAttr = {
indentation: number;
}
function ComponentAttr({ name, attr }: { name: string, attr: IAttr[] }) {
const [collapsedNode, setCollapsedNode] = useState(new Set());
const handleCollapse = (index: number) => {
const newSet = new Set<number>();
collapsedNode.forEach(value => {
newSet.add(value);
});
if (newSet.has(index)) {
newSet.delete(index);
function collapseAllNodes(attrs: IAttr[]) {
return attrs.filter((item, index) => {
const nextItem = attrs[index + 1];
return nextItem ? nextItem.indentation - item.indentation > 0 : false;
});
}
function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) {
const [collapsedNode, setCollapsedNode] = useState(collapseAllNodes(attrs));
const handleCollapse = (item: IAttr) => {
const nodes = [...collapsedNode];
const i = nodes.indexOf(item);
if (i === -1) {
nodes.push(item);
} else {
newSet.add(index);
nodes.splice(i, 1);
}
setCollapsedNode(newSet);
setCollapsedNode(nodes);
};
const showAttr = [];
let currentIndentation = null;
attr.forEach((item, index) => {
attrs.forEach((item, index) => {
const indentation = item.indentation;
if (currentIndentation !== null) {
if (indentation > currentIndentation) {
@ -48,12 +56,12 @@ function ComponentAttr({ name, attr }: { name: string, attr: IAttr[] }) {
currentIndentation = null;
}
}
const nextItem = attr[index + 1];
const nextItem = attrs[index + 1];
const hasChild = nextItem ? nextItem.indentation - item.indentation > 0 : false;
const isCollapsed = collapsedNode.has(index);
const isCollapsed = collapsedNode.includes(item);
showAttr.push(
<div style={{ paddingLeft: item.indentation * 10 }} key={index} onClick={() => (handleCollapse(index))}>
<span className={styles.attrArrow}>{hasChild && <Arrow director={isCollapsed ? 'right' : 'down'} />}</span>
<div style={{ paddingLeft: item.indentation * 10 }} key={index} onClick={() => (handleCollapse(item))}>
<span className={styles.attrArrow}>{hasChild && <Triangle director={isCollapsed ? 'right' : 'down'} />}</span>
<span className={styles.attrName}>{`${item.name}`}</span>
{' :'}
<span className={styles.attrValue}>{item.value}</span>
@ -79,28 +87,38 @@ function ComponentAttr({ name, attr }: { name: string, attr: IAttr[] }) {
);
}
export default function ComponentInfo({ name, attrs }: IComponentInfo) {
export default function ComponentInfo({ name, attrs, parents, onClickParent }: IComponentInfo) {
const { state, props, context, hooks } = attrs;
return (
<div className={styles.infoContainer} >
<div className={styles.componentInfoHead}>
<span className={styles.name}>
{name}
</span>
<span className={styles.eye} >
<Eye />
</span>
<span className={styles.debug}>
<Debug />
</span>
{name && <>
<span className={styles.name}>
{name}
</span>
<span className={styles.eye} >
<Eye />
</span>
<span className={styles.debug}>
<Debug />
</span>
</>}
</div>
<div className={styles.componentInfoMain}>
{context && <ComponentAttr name={'context'} attr={context} />}
{props && <ComponentAttr name={'props'} attr={props} />}
{state && <ComponentAttr name={'state'} attr={state} />}
{hooks && <ComponentAttr name={'hook'} attr={hooks} />}
<div className={styles.renderInfo}>
rendered by
{context && <ComponentAttr name={'context'} attrs={context} />}
{props && <ComponentAttr name={'props'} attrs={props} />}
{state && <ComponentAttr name={'state'} attrs={state} />}
{hooks && <ComponentAttr name={'hook'} attrs={hooks} />}
<div className={styles.parentsInfo}>
{name && <div>
parents: {
parents.map(item => (<button
className={styles.parent}
onClick={() => (onClickParent(item))}>
{item.name}
</button>))
}
</div>}
</div>
</div>
</div>

View File

@ -32,11 +32,11 @@
.componentInfoMain {
overflow-y: auto;
:last-child {
>:last-child {
border-bottom: unset;
}
:first-child {
>:first-child {
padding: unset;
}
@ -57,15 +57,11 @@
.attrType {
flex: 1 1 0;
}
.attrCopy {
flex: 0 0 1rem;
padding-right: 1rem;
}
}
.attrDetail {
padding-bottom: 0.5rem;
@ -84,9 +80,19 @@
}
}
}
}
.renderInfo {
flex: 1 1 0;
.parentsInfo {
flex: 1 1 0;
.parent {
display: block;
cursor: pointer;
text-align: left;
color: @component-name-color;
width: 100%;
&:hover {
background-color: @select-color;
}
}
}
}

View File

@ -0,0 +1,147 @@
// 过滤树的抽象逻辑
// 需要知道渲染了哪些数据,过滤的字符串/正则表达式
// 控制Tree组件位置跳转告知匹配结果
// 清空搜索框,告知搜索框当前是第几个结果,跳转搜索结果
//
// 跳转搜索结果的交互逻辑:
// 如果当前页面存在匹配项,页面不动
// 如果当前页面不存在匹配项,页面跳转到第一个匹配项位置
// 如果匹配项被折叠,需要展开其父节点。注意只展开当前匹配项的父节点,其他匹配项的父节点不展开
// 跳转到上一个匹配项或下一个匹配项时,如果匹配项被折叠,需要展开其父节点
//
// 寻找父节点:
// 找到该节点的缩进值和index值在data中向上遍历通过缩进值判断父节点
import { useState, useRef } from 'horizon';
import { createRegExp } from '../utils';
/**
*
*
* @param item
* @param data
* @param collapsedNodes
* @returns
*/
function expandItemParent(item: BaseType, data: BaseType[], collapsedNodes: BaseType[]): BaseType[] {
const index = data.indexOf(item);
let currentIndentation = item.indentation;
// 不对原始数据进行修改
const newCollapsedNodes = [...collapsedNodes];
for (let i = index - 1; i >= 0; i--) {
const lastData = data[i];
const lastIndentation = lastData.indentation;
// 缩进更小,找到了父节点
if (lastIndentation < currentIndentation) {
// 更新缩进值,只找父节点的父节点,避免修改父节点的兄弟节点的展开状态
currentIndentation = lastIndentation;
const cIndex = newCollapsedNodes.indexOf(lastData);
if (cIndex !== -1) {
newCollapsedNodes.splice(cIndex, 1);
}
}
}
return newCollapsedNodes;
}
type BaseType = {
id: string,
name: string,
indentation: number,
}
export function FilterTree<T extends BaseType>(props: { data: T[] }) {
const { data } = props;
const [filterValue, setFilterValue] = useState('');
const [currentItem, setCurrentItem] = useState(null); // 当前选中的匹配项
const showItemsRef = useRef([]); // 页面展示的 items
const matchItemsRef = useRef([]); // 匹配过滤条件的 items
const collapsedNodesRef = useRef([]); // 折叠节点,如果匹配 item 被折叠了,需要展开
const matchItems = matchItemsRef.current;
const collapsedNodes = collapsedNodesRef.current;
const updateCollapsedNodes = (item: BaseType) => {
const newcollapsedNodes = expandItemParent(item, data, collapsedNodes);
// 如果新旧收起节点数组长度不一样,说明存在收起节点
if (newcollapsedNodes.length !== collapsedNodes.length) {
// 更新引用,确保 VTree 拿到新的 collapsedNodes
collapsedNodesRef.current = newcollapsedNodes;
}
};
const onChangeSearchValue = (search: string) => {
const reg = createRegExp(search);
let newCurrentItem = null;
let newMatchItems = [];
if (search !== '') {
const showItems: T[] = showItemsRef.current;
newMatchItems = data.reduce((pre, current) => {
const { name } = current;
if (reg && name.match(reg)) {
pre.push(current);
// 如果当前页面显示的 item 存在匹配项,则把它设置为 currentItem
if (newCurrentItem === null && showItems.includes(current)) {
newCurrentItem = current;
}
}
return pre;
}, []);
if (newMatchItems.length === 0) {
setCurrentItem(null);
} else {
if (newCurrentItem === null) {
const item = newMatchItems[0];
// 不处于当前展示页面,需要展开父节点
updateCollapsedNodes(item);
setCurrentItem(item);
} else {
setCurrentItem(newCurrentItem);
}
}
} else {
setCurrentItem(null);
}
matchItemsRef.current = newMatchItems;
setFilterValue(search);
};
const onSelectNext = () => {
const index = matchItems.indexOf(currentItem);
const nextIndex = index + 1;
const item = nextIndex < matchItemsRef.current.length ? matchItems[nextIndex] : matchItems[0];
// 可能不处于当前展示页面,需要展开父节点
updateCollapsedNodes(item);
setCurrentItem(item);
};
const onSelectLast = () => {
const index = matchItems.indexOf(currentItem);
const last = index - 1;
const item = last >= 0 ? matchItems[last] : matchItems[matchItems.length - 1];
// 可能不处于当前展示页面,需要展开父节点
updateCollapsedNodes(item);
setCurrentItem(item);
};
const setShowItems = (items) => {
showItemsRef.current = [...items];
};
const onClear = () => {
onChangeSearchValue('');
};
const setCollapsedNodes = (items) => {
// 不更新引用,避免子组件的重复渲染
collapsedNodesRef.current.length = 0;
collapsedNodesRef.current.push(...items);
};
return {
filterValue,
onChangeSearchValue,
onClear,
currentItem,
matchItems,
onSelectNext,
onSelectLast,
setShowItems,
collapsedNodes,
setCollapsedNodes,
};
}

View File

@ -0,0 +1,78 @@
/**
*
* ResizeObserver IE
* dom
* window resize object
* window object dom dom dom
* dom window window
* dom
*
* <div id='test'>
* <object> --> div
* <html></html> --> resize
* </object>
* </div>
*
*/
function timeout(fn) {
return setTimeout(fn, 20);
}
function requestFrame(fn) {
const raf = requestAnimationFrame || timeout;
return raf(fn);
}
function cancelFrame(id) {
const cancel = cancelAnimationFrame || clearTimeout;
cancel(id);
}
// 在闲置帧触发回调事件,如果在本次触发前存在未处理回调事件,
// 需要取消未处理的回调事件
function resizeListener(event) {
const win = event.target;
if (win.__resizeRAF__) {
cancelFrame(win.__resizeRAF__);
}
win.__resizeRAF__ = requestFrame(function () {
const observeElement = win.__observeElement__;
observeElement.__resizeCallbacks__.forEach(function (fn) {
fn.call(observeElement, observeElement, event);
});
});
}
function loadObserver() {
// 将待观测元素传递给 object 标签的 window 对象,这样在触发 resize 事件时可以拿到待观测元素
this.contentDocument.defaultView.__observeElement__ = this.__observeElement__;
// 给 html 的 window 对象添加 resize 事件
this.contentDocument.defaultView.addEventListener('resize', resizeListener);
}
export function addResizeListener(element: any, fn: any) {
if (!element.__resizeCallbacks__) {
element.__resizeCallbacks__ = [fn];
element.style.position = 'relative';
const observer = document.createElement('object');
observer.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');
observer.data = 'about:blank';
observer.onload = loadObserver;
observer.type = 'text/html';
observer.__observeElement__ = element;
element.__observer__ = observer;
element.appendChild(observer);
} else {
element.__resizeCallbacks__.push(fn);
}
}
export function removeResizeListener(element, fn) {
element.__resizeCallbacks__.splice(element.__resizeCallbacks__.indexOf(fn), 1);
if (!element.__resizeCallbacks__.length) {
element.__observer__.contentDocument.defaultView.removeEventListener('resize', resizeListener);
element.removeChild(element.__observer__);
element.__observer__ = null;
}
}

View File

@ -2,10 +2,11 @@ import styles from './Search.less';
interface SearchProps {
onChange: (event: any) => void,
value: string,
}
export default function Search(props: SearchProps) {
const { onChange } = props;
const { onChange, value } = props;
const handleChange = (event) => {
onChange(event.target.value);
};
@ -13,6 +14,7 @@ export default function Search(props: SearchProps) {
<input
onChange={handleChange}
className={styles.search}
value={value}
placeholder={'Search (text or /regex/)'}
/>
);

View File

@ -0,0 +1,33 @@
import { useEffect, useState, useRef } from 'horizon';
import { addResizeListener, removeResizeListener } from './ResizeEvent';
export function SizeObserver(props) {
const { children, ...rest } = props;
const containerRef = useRef();
const [size, setSize] = useState();
const notifyChild = (element) => {
setSize({
width: element.offsetWidth,
height: element.offsetHeight,
});
};
useEffect(() => {
const element = containerRef.current;
setSize({
width: element.offsetWidth,
height: element.offsetHeight,
});
addResizeListener(element, notifyChild);
return () => {
removeResizeListener(element, notifyChild);
};
}, []);
const myChild = size ? children(size.width, size.height) : null;
return (
<div ref={containerRef} {...rest}>
{myChild}
</div>
);
}

View File

@ -0,0 +1,11 @@
.container {
position: relative;
overflow-y: auto;
height: 100%;
width: 100%;
}
.item {
position: absolute;
width: 100%;
}

View File

@ -0,0 +1,98 @@
import { useState, useRef, useEffect } from 'horizon';
import styles from './VList.less';
interface IProps<T extends { id: string }> {
data: T[],
width: number, // 暂时未用到,当需要支持横向滚动时使用
height: number, // VList 的高度
children: any, // horizon 组件,组件类型是 T
itemHeight: number,
scrollToItem?: T, // 滚动到指定项位置,如果该项在可见区域内,不滚动,如果不在,则滚动到中间位置
onRendered: (renderInfo: renderInfoType<T>) => void;
filter?(data: T): boolean, // false 表示该行不显示
}
export type renderInfoType<T> = {
visibleItems: T[],
skipItemCountBeforeScrollItem: number,
};
export function VList<T extends { id: string }>(props: IProps<T>) {
const {
data,
height,
children,
itemHeight,
scrollToItem,
filter,
onRendered,
} = props;
const [scrollTop, setScrollTop] = useState(data.indexOf(scrollToItem) * itemHeight);
const renderInfoRef: { current: renderInfoType<T> } = useRef({ visibleItems: [], skipItemCountBeforeScrollItem: 0 });
const containerRef = useRef();
useEffect(() => {
onRendered(renderInfoRef.current);
});
useEffect(() => {
if (scrollToItem) {
const renderInfo = renderInfoRef.current;
// 在滚动区域,不滚动
if (!renderInfo.visibleItems.includes(scrollToItem)) {
const index = data.indexOf(scrollToItem);
// top值计算需要减掉filter条件判定不显示项
const totalCount = index - renderInfoRef.current.skipItemCountBeforeScrollItem;
// 显示在页面中间
const top = totalCount * itemHeight - height / 2;
containerRef.current.scrollTo({ top: top });
}
}
}, [scrollToItem]);
const handleScroll = (event: any) => {
const scrollTop = event.target.scrollTop;
setScrollTop(scrollTop);
};
const showList: any[] = [];
let totalHeight = 0;
// 顶部冗余
const startShowTopValue = Math.max(scrollTop - itemHeight * 4, 0);
// 底部冗余
const showNum = Math.floor(height / itemHeight) + 4;
// 如果最后一个显示不全,不统计在显示 ids 内
const maxTop = scrollTop + height - itemHeight;
// 清空记录的上次渲染的数据
renderInfoRef.current.visibleItems.length = 0;
const scrollItemIndex = data.indexOf(scrollToItem);
renderInfoRef.current.skipItemCountBeforeScrollItem = 0;
data.forEach((item, i) => {
if (filter && !filter(item)) {
if (scrollItemIndex > i) {
renderInfoRef.current.skipItemCountBeforeScrollItem++;
}
return;
}
if (totalHeight >= startShowTopValue && showList.length <= showNum) {
showList.push(
<div
key={String(item.id)}
className={styles.item}
style={{ transform: `translateY(${totalHeight}px)` }} >
{children(i, item)}
</div>
);
if (totalHeight >= scrollTop && totalHeight < maxTop) {
renderInfoRef.current.visibleItems.push(item);
}
}
totalHeight += itemHeight;
});
return (
<div ref={containerRef} className={styles.container} onScroll={handleScroll}>
{showList}
<div style={{ marginTop: totalHeight }} />
</div>
);
}

View File

@ -1,10 +1,7 @@
@import 'assets.less';
.treeContainer {
position: relative;
width: 100%;
height: 100%;
overflow-y: auto;
.treeItem {
width: 100%;

View File

@ -1,7 +1,9 @@
import { useState } from 'horizon';
import { useState, useEffect } from 'horizon';
import styles from './VTree.less';
import Arrow from '../svgs/Arrow';
import Triangle from '../svgs/Triangle';
import { createRegExp } from './../utils';
import { SizeObserver } from './SizeObserver';
import { renderInfoType, VList } from './VList';
export interface IData {
id: string;
@ -10,44 +12,44 @@ export interface IData {
userKey: string;
}
type IItem = {
style: any,
interface IItem {
hasChild: boolean,
onCollapse: (id: string) => void,
onClick: (id: string) => void,
onCollapse: (data: IData) => void,
onClick: (id: IData) => void,
isCollapsed: boolean,
isSelect: boolean,
highlightValue: string,
} & IData
data: IData,
}
// TODO: 计算可以展示的最多数量,并且监听显示器高度变化修改数值
const showNum = 70;
const lineHeight = 18;
const indentationLength = 20;
function Item(props: IItem) {
const {
name,
style,
userKey,
hasChild,
onCollapse,
isCollapsed,
id,
indentation,
data,
onClick,
isSelect,
highlightValue,
highlightValue = '',
} = props;
const {
name,
userKey,
indentation,
} = data;
const isShowKey = userKey !== '';
const showIcon = hasChild ? <Arrow director={isCollapsed ? 'right' : 'down'} /> : '';
const showIcon = hasChild ? <Triangle director={isCollapsed ? 'right' : 'down'} /> : '';
const handleClickCollapse = () => {
onCollapse(id);
onCollapse(data);
};
const handleClick = () => {
onClick(id);
onClick(data);
};
const itemAttr: any = { style, className: styles.treeItem, onClick: handleClick };
const itemAttr: any = { className: styles.treeItem, onClick: handleClick };
if (isSelect) {
itemAttr.tabIndex = 0;
itemAttr.className = styles.treeItem + ' ' + styles.select;
@ -93,81 +95,103 @@ function Item(props: IItem) {
);
}
function VTree({ data, highlightValue }: { data: IData[], highlightValue: string }) {
const [scrollTop, setScrollTop] = useState(0);
const [collapseNode, setCollapseNode] = useState(new Set<string>());
const [selectItem, setSelectItem] = useState();
const changeCollapseNode = (id: string) => {
const nodes = new Set<string>();
collapseNode.forEach(value => {
nodes.add(value);
});
if (nodes.has(id)) {
nodes.delete(id);
function VTree(props: {
data: IData[],
highlightValue: string,
scrollToItem: IData,
onRendered: (renderInfo: renderInfoType<IData>) => void,
collapsedNodes?: IData[],
onCollapseNode?: (item: IData[]) => void,
selectItem: IData[],
onSelectItem: (item: IData) => void,
}) {
const { data, highlightValue, scrollToItem, onRendered, onCollapseNode, onSelectItem } = props;
const [collapseNode, setCollapseNode] = useState(props.collapsedNodes || []);
const [selectItem, setSelectItem] = useState(props.selectItem);
useEffect(() => {
setSelectItem(scrollToItem);
}, [scrollToItem]);
useEffect(() => {
if (props.selectItem !== selectItem) {
setSelectItem(props.selectItem);
}
}, [props.selectItem]);
useEffect(() => {
setCollapseNode(props.collapsedNodes || []);
}, [props.collapsedNodes]);
const changeCollapseNode = (item: IData) => {
const nodes: IData[] = [...collapseNode];
const index = nodes.indexOf(item);
if (index === -1) {
nodes.push(item);
} else {
nodes.add(id);
nodes.splice(index, 1);
}
setCollapseNode(nodes);
if (onCollapseNode) {
onCollapseNode(nodes);
}
};
const handleClickItem = (id: string) => {
setSelectItem(id);
const handleClickItem = (item: IData) => {
setSelectItem(item);
if (onSelectItem) {
onSelectItem(item);
}
};
const showList: any[] = [];
let totalHeight = 0;
let currentCollapseIndentation: null | number = null;
data.forEach((item, index) => {
// 存在未处理完的收起节点
// 过滤掉折叠的 item不展示在 VList 中
const filter = (item: IData) => {
if (currentCollapseIndentation !== null) {
const indentation = item.indentation;
// 缩进更大,不显示
if (indentation > currentCollapseIndentation) {
return;
if (item.indentation > currentCollapseIndentation) {
return false;
} else {
// 缩进小,说明完成了该收起节点的子节点处理。
currentCollapseIndentation = null;
}
}
const id = item.id;
const isCollapsed = collapseNode.has(id);
if (totalHeight >= scrollTop && showList.length <= showNum) {
const nextItem = data[index + 1];
// 如果存在下一个节点,并且节点缩进比自己大,说明下个节点是子节点,节点本身需要显示展开收起图标
const hasChild = nextItem ? nextItem.indentation > item.indentation : false;
showList.push(
<Item
key={id}
hasChild={hasChild}
style={{
transform: `translateY(${totalHeight}px)`,
}}
onCollapse={changeCollapseNode}
onClick={handleClickItem}
isCollapsed={isCollapsed}
isSelect={id === selectItem}
highlightValue={highlightValue}
{...item} />
);
}
totalHeight = totalHeight + lineHeight;
const isCollapsed = collapseNode.includes(item);
if (isCollapsed) {
// 该节点需要收起子节点
currentCollapseIndentation = item.indentation;
}
});
const handleScroll = (event: any) => {
const scrollTop = event.target.scrollTop;
// 顶部留 100px 冗余高度
setScrollTop(Math.max(scrollTop - 100, 0));
return true;
};
return (
<div className={styles.treeContainer} onScroll={handleScroll}>
{showList}
{/* 确保有足够的高度 */}
<div style={{ marginTop: totalHeight }} />
</div>
<SizeObserver className={styles.treeContainer}>
{(width: number, height: number) => {
return (
<VList
data={data}
width={width}
height={height}
itemHeight={18}
scrollToItem={selectItem}
filter={filter}
onRendered={onRendered}
>
{(index: number, item: IData) => {
// 如果存在下一个节点,并且节点缩进比自己大,说明下个节点是子节点,节点本身需要显示展开收起图标
const nextItem = data[index + 1];
const hasChild = nextItem && nextItem.indentation > item.indentation;
return (
<Item
hasChild={hasChild}
isCollapsed={collapseNode.includes(item)}
isSelect={selectItem === item}
onCollapse={changeCollapseNode}
onClick={handleClickItem}
highlightValue={highlightValue}
data={item} />
);
}}
</VList>
);
}}
</SizeObserver>
);
}

View File

@ -6,6 +6,7 @@
@componentKeyValue-color: rgb(26, 26, 166);
@component-attr-color: rgb(200, 0, 0);
@select-color: rgb(141 199 248 / 60%);
@hover-color: black;
@top-height: 2.625rem;
@divider-width: 0.2px;

View File

@ -7,6 +7,12 @@
font-size: @common-font-size;
}
button {
border: none;
background: none;
padding: 0;
}
.left {
flex: 7;
display: flex;
@ -17,6 +23,7 @@
flex: 0 0 @top-height;
display: flex;
align-items: center;
padding-right: 0.4rem;
.select {
padding: 0 0.25rem 0 0.25rem;
@ -33,6 +40,20 @@
.search {
flex: 1 1 0;
}
.searchResult{
flex: 0 0 ;
padding: 0 0.4rem;
}
.searchAction {
flex: 0 0 1rem;
height: 1rem;
color: @arrow-color;
&:hover{
color: @hover-color;
}
}
}
.left_bottom {

View File

@ -5,36 +5,24 @@ import ComponentInfo from '../components/ComponentInfo';
import styles from './App.less';
import Select from '../svgs/Select';
import { mockParsedVNodeData, parsedMockState } from '../devtools/mock';
import { FilterTree } from '../components/FilterTree';
import Close from '../svgs/Close';
import Arrow from './../svgs/Arrow';
function App() {
const [parsedVNodeData, setParsedVNodeData] = useState([]);
const [componentInfo, setComponentInfo] = useState({ name: null, attrs: {} });
const [filterValue, setFilterValue] = useState('');
useEffect(() => {
if (isDev) {
setParsedVNodeData(mockParsedVNodeData);
setComponentInfo({
name: 'Demo',
attrs: {
state: parsedMockState,
props: parsedMockState,
},
});
}
}, []);
const parseVNodeData = (rawData) => {
const idIndentationMap: {
[id: string]: number;
} = {};
const data: IData[] = [];
let i = 0;
while (i < parsedVNodeData.length) {
const id = parsedVNodeData[i] as string;
while (i < rawData.length) {
const id = rawData[i] as string;
i++;
const name = parsedVNodeData[i] as string;
const name = rawData[i] as string;
i++;
const parentId = parsedVNodeData[i] as string;
const parentId = rawData[i] as string;
i++;
const userKey = parsedVNodeData[i] as string;
const userKey = rawData[i] as string;
i++;
const indentation = parentId === '' ? 0 : idIndentationMap[parentId] + 1;
idIndentationMap[id] = indentation;
@ -43,11 +31,76 @@ function App() {
};
data.push(item);
}
return data;
};
const getParents = (item: IData | null, parsedVNodeData: IData[]) => {
const parents: IData[] = [];
if (item) {
const index = parsedVNodeData.indexOf(item);
let indentation = item.indentation;
for (let i = index; i >= 0; i--) {
const last = parsedVNodeData[i];
const lastIndentation = last.indentation;
if (lastIndentation < indentation) {
parents.push(last);
indentation = lastIndentation;
}
}
}
return parents;
};
function App() {
const [parsedVNodeData, setParsedVNodeData] = useState([]);
const [componentAttrs, setComponentAttrs] = useState({});
const [selectComp, setSelectComp] = useState(null);
const {
filterValue,
onChangeSearchValue: setFilterValue,
onClear,
currentItem,
matchItems,
onSelectNext,
onSelectLast,
setShowItems,
collapsedNodes,
setCollapsedNodes,
} = FilterTree({ data: parsedVNodeData });
useEffect(() => {
if (isDev) {
const parsedData = parseVNodeData(mockParsedVNodeData);
setParsedVNodeData(parsedData);
setComponentAttrs({
state: parsedMockState,
props: parsedMockState,
});
}
}, []);
const handleSearchChange = (str: string) => {
setFilterValue(str);
};
const handleSelectComp = (item: IData) => {
setComponentAttrs({
state: parsedMockState,
props: parsedMockState,
});
setSelectComp(item);
};
const handleClickParent = (item: IData) => {
setSelectComp(item);
};
const onRendered = (info) => {
setShowItems(info.visibleItems);
};
const parents = getParents(selectComp, parsedVNodeData);
return (
<div className={styles.app}>
<div className={styles.left}>
@ -57,15 +110,34 @@ function App() {
</div>
<div className={styles.divider} />
<div className={styles.search}>
<Search onChange={handleSearchChange} />
<Search onChange={handleSearchChange} value={filterValue} />
</div>
{filterValue !== '' && <>
<span className={styles.searchResult}>{`${matchItems.indexOf(currentItem) + 1}/${matchItems.length}`}</span>
<div className={styles.divider} />
<button className={styles.searchAction} onClick={onSelectLast}><Arrow direction={'up'} /></button>
<button className={styles.searchAction} onClick={onSelectNext}><Arrow direction={'down'} /></button>
<button className={styles.searchAction} onClick={onClear}><Close /></button>
</>}
</div>
<div className={styles.left_bottom}>
<VTree data={data} highlightValue={filterValue} />
<VTree
data={parsedVNodeData}
highlightValue={filterValue}
onRendered={onRendered}
collapsedNodes={collapsedNodes}
onCollapseNode={setCollapsedNodes}
scrollToItem={currentItem}
selectItem={selectComp}
onSelectItem={handleSelectComp} />
</div>
</div>
<div className={styles.right}>
<ComponentInfo name={componentInfo.name} attrs={componentInfo.attrs} />
<ComponentInfo
name={selectComp ? selectComp.name: null}
attrs={selectComp ? componentAttrs: {}}
parents={parents}
onClickParent={handleClickParent} />
</div>
</div>
);

View File

@ -1,16 +1,16 @@
interface IArrow {
director: 'right' | 'down'
direction: 'up' | 'down'
}
export default function Arrow({ director }: IArrow) {
export default function Arrow({ direction: director }: IArrow) {
let d: string;
if (director === 'right') {
d = 'm2 0l12 8l-12 8 z';
if (director === 'up') {
d = 'M4 9.5 L5 10.5 L8 7.5 L11 10.5 L12 9.5 L8 5.5 z';
} else if (director === 'down') {
d = 'm0 2h16 l-8 12 z';
d = 'M5 5.5 L4 6.5 L8 10.5 L12 6.5 L11 5.5 L8 8.5z';
}
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='8px' height='8px'>
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
<path d={d} fill='currentColor' />
</svg>
);

View File

@ -0,0 +1,8 @@
export default function Close() {
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
<path d='M4 3 L3 4 L7 8 L3 12 L4 13 L8 9 L12 13 L13 12 L9 8 L13 4 L12 3 L8 7z' fill='currentColor' />
</svg>
);
}

View File

@ -0,0 +1,17 @@
interface IArrow {
director: 'right' | 'down'
}
export default function Triangle({ director }: IArrow) {
let d: string;
if (director === 'right') {
d = 'm2 0l12 8l-12 8 z';
} else if (director === 'down') {
d = 'm0 2h16 l-8 12 z';
}
return (
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='8px' height='8px'>
<path d={d} fill='currentColor' />
</svg>
);
}