Match-id-0a69bdf730a5aa30cbb7385fbf94f300b6222654
This commit is contained in:
commit
72a11c9725
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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/)'}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
.container {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -1,10 +1,7 @@
|
|||
@import 'assets.less';
|
||||
|
||||
.treeContainer {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
|
||||
.treeItem {
|
||||
width: 100%;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
}
|
Loading…
Reference in New Issue