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 Eye from '../svgs/Eye';
|
||||||
import Debug from '../svgs/Debug';
|
import Debug from '../svgs/Debug';
|
||||||
import Copy from '../svgs/Copy';
|
import Copy from '../svgs/Copy';
|
||||||
import Arrow from '../svgs/Arrow';
|
import Triangle from '../svgs/Triangle';
|
||||||
import { useState } from 'horizon';
|
import { useState } from 'horizon';
|
||||||
|
import { IData } from './VTree';
|
||||||
|
|
||||||
type IComponentInfo = {
|
type IComponentInfo = {
|
||||||
name: string;
|
name: string;
|
||||||
|
@ -12,7 +13,9 @@ type IComponentInfo = {
|
||||||
context?: IAttr[];
|
context?: IAttr[];
|
||||||
state?: IAttr[];
|
state?: IAttr[];
|
||||||
hooks?: IAttr[];
|
hooks?: IAttr[];
|
||||||
}
|
};
|
||||||
|
parents: IData[];
|
||||||
|
onClickParent: (item: IData) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type IAttr = {
|
type IAttr = {
|
||||||
|
@ -22,24 +25,29 @@ type IAttr = {
|
||||||
indentation: number;
|
indentation: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ComponentAttr({ name, attr }: { name: string, attr: IAttr[] }) {
|
function collapseAllNodes(attrs: IAttr[]) {
|
||||||
const [collapsedNode, setCollapsedNode] = useState(new Set());
|
return attrs.filter((item, index) => {
|
||||||
const handleCollapse = (index: number) => {
|
const nextItem = attrs[index + 1];
|
||||||
const newSet = new Set<number>();
|
return nextItem ? nextItem.indentation - item.indentation > 0 : false;
|
||||||
collapsedNode.forEach(value => {
|
|
||||||
newSet.add(value);
|
|
||||||
});
|
});
|
||||||
if (newSet.has(index)) {
|
}
|
||||||
newSet.delete(index);
|
|
||||||
|
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 {
|
} else {
|
||||||
newSet.add(index);
|
nodes.splice(i, 1);
|
||||||
}
|
}
|
||||||
setCollapsedNode(newSet);
|
setCollapsedNode(nodes);
|
||||||
};
|
};
|
||||||
|
|
||||||
const showAttr = [];
|
const showAttr = [];
|
||||||
let currentIndentation = null;
|
let currentIndentation = null;
|
||||||
attr.forEach((item, index) => {
|
attrs.forEach((item, index) => {
|
||||||
const indentation = item.indentation;
|
const indentation = item.indentation;
|
||||||
if (currentIndentation !== null) {
|
if (currentIndentation !== null) {
|
||||||
if (indentation > currentIndentation) {
|
if (indentation > currentIndentation) {
|
||||||
|
@ -48,12 +56,12 @@ function ComponentAttr({ name, attr }: { name: string, attr: IAttr[] }) {
|
||||||
currentIndentation = null;
|
currentIndentation = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const nextItem = attr[index + 1];
|
const nextItem = attrs[index + 1];
|
||||||
const hasChild = nextItem ? nextItem.indentation - item.indentation > 0 : false;
|
const hasChild = nextItem ? nextItem.indentation - item.indentation > 0 : false;
|
||||||
const isCollapsed = collapsedNode.has(index);
|
const isCollapsed = collapsedNode.includes(item);
|
||||||
showAttr.push(
|
showAttr.push(
|
||||||
<div style={{ paddingLeft: item.indentation * 10 }} key={index} onClick={() => (handleCollapse(index))}>
|
<div style={{ paddingLeft: item.indentation * 10 }} key={index} onClick={() => (handleCollapse(item))}>
|
||||||
<span className={styles.attrArrow}>{hasChild && <Arrow director={isCollapsed ? 'right' : 'down'} />}</span>
|
<span className={styles.attrArrow}>{hasChild && <Triangle director={isCollapsed ? 'right' : 'down'} />}</span>
|
||||||
<span className={styles.attrName}>{`${item.name}`}</span>
|
<span className={styles.attrName}>{`${item.name}`}</span>
|
||||||
{' :'}
|
{' :'}
|
||||||
<span className={styles.attrValue}>{item.value}</span>
|
<span className={styles.attrValue}>{item.value}</span>
|
||||||
|
@ -79,11 +87,12 @@ 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;
|
const { state, props, context, hooks } = attrs;
|
||||||
return (
|
return (
|
||||||
<div className={styles.infoContainer} >
|
<div className={styles.infoContainer} >
|
||||||
<div className={styles.componentInfoHead}>
|
<div className={styles.componentInfoHead}>
|
||||||
|
{name && <>
|
||||||
<span className={styles.name}>
|
<span className={styles.name}>
|
||||||
{name}
|
{name}
|
||||||
</span>
|
</span>
|
||||||
|
@ -93,14 +102,23 @@ export default function ComponentInfo({ name, attrs }: IComponentInfo) {
|
||||||
<span className={styles.debug}>
|
<span className={styles.debug}>
|
||||||
<Debug />
|
<Debug />
|
||||||
</span>
|
</span>
|
||||||
|
</>}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.componentInfoMain}>
|
<div className={styles.componentInfoMain}>
|
||||||
{context && <ComponentAttr name={'context'} attr={context} />}
|
{context && <ComponentAttr name={'context'} attrs={context} />}
|
||||||
{props && <ComponentAttr name={'props'} attr={props} />}
|
{props && <ComponentAttr name={'props'} attrs={props} />}
|
||||||
{state && <ComponentAttr name={'state'} attr={state} />}
|
{state && <ComponentAttr name={'state'} attrs={state} />}
|
||||||
{hooks && <ComponentAttr name={'hook'} attr={hooks} />}
|
{hooks && <ComponentAttr name={'hook'} attrs={hooks} />}
|
||||||
<div className={styles.renderInfo}>
|
<div className={styles.parentsInfo}>
|
||||||
rendered by
|
{name && <div>
|
||||||
|
parents: {
|
||||||
|
parents.map(item => (<button
|
||||||
|
className={styles.parent}
|
||||||
|
onClick={() => (onClickParent(item))}>
|
||||||
|
{item.name}
|
||||||
|
</button>))
|
||||||
|
}
|
||||||
|
</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -32,11 +32,11 @@
|
||||||
.componentInfoMain {
|
.componentInfoMain {
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
|
||||||
:last-child {
|
>:last-child {
|
||||||
border-bottom: unset;
|
border-bottom: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
:first-child {
|
>:first-child {
|
||||||
padding: unset;
|
padding: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -57,15 +57,11 @@
|
||||||
.attrType {
|
.attrType {
|
||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.attrCopy {
|
.attrCopy {
|
||||||
flex: 0 0 1rem;
|
flex: 0 0 1rem;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.attrDetail {
|
.attrDetail {
|
||||||
padding-bottom: 0.5rem;
|
padding-bottom: 0.5rem;
|
||||||
|
|
||||||
|
@ -84,9 +80,19 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.renderInfo {
|
.parentsInfo {
|
||||||
flex: 1 1 0;
|
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 {
|
interface SearchProps {
|
||||||
onChange: (event: any) => void,
|
onChange: (event: any) => void,
|
||||||
|
value: string,
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Search(props: SearchProps) {
|
export default function Search(props: SearchProps) {
|
||||||
const { onChange } = props;
|
const { onChange, value } = props;
|
||||||
const handleChange = (event) => {
|
const handleChange = (event) => {
|
||||||
onChange(event.target.value);
|
onChange(event.target.value);
|
||||||
};
|
};
|
||||||
|
@ -13,6 +14,7 @@ export default function Search(props: SearchProps) {
|
||||||
<input
|
<input
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
className={styles.search}
|
className={styles.search}
|
||||||
|
value={value}
|
||||||
placeholder={'Search (text or /regex/)'}
|
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';
|
@import 'assets.less';
|
||||||
|
|
||||||
.treeContainer {
|
.treeContainer {
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.treeItem {
|
.treeItem {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -1,7 +1,9 @@
|
||||||
import { useState } from 'horizon';
|
import { useState, useEffect } from 'horizon';
|
||||||
import styles from './VTree.less';
|
import styles from './VTree.less';
|
||||||
import Arrow from '../svgs/Arrow';
|
import Triangle from '../svgs/Triangle';
|
||||||
import { createRegExp } from './../utils';
|
import { createRegExp } from './../utils';
|
||||||
|
import { SizeObserver } from './SizeObserver';
|
||||||
|
import { renderInfoType, VList } from './VList';
|
||||||
|
|
||||||
export interface IData {
|
export interface IData {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -10,44 +12,44 @@ export interface IData {
|
||||||
userKey: string;
|
userKey: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type IItem = {
|
interface IItem {
|
||||||
style: any,
|
|
||||||
hasChild: boolean,
|
hasChild: boolean,
|
||||||
onCollapse: (id: string) => void,
|
onCollapse: (data: IData) => void,
|
||||||
onClick: (id: string) => void,
|
onClick: (id: IData) => void,
|
||||||
isCollapsed: boolean,
|
isCollapsed: boolean,
|
||||||
isSelect: boolean,
|
isSelect: boolean,
|
||||||
highlightValue: string,
|
highlightValue: string,
|
||||||
} & IData
|
data: IData,
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: 计算可以展示的最多数量,并且监听显示器高度变化修改数值
|
|
||||||
const showNum = 70;
|
|
||||||
const lineHeight = 18;
|
|
||||||
const indentationLength = 20;
|
const indentationLength = 20;
|
||||||
|
|
||||||
function Item(props: IItem) {
|
function Item(props: IItem) {
|
||||||
const {
|
const {
|
||||||
name,
|
|
||||||
style,
|
|
||||||
userKey,
|
|
||||||
hasChild,
|
hasChild,
|
||||||
onCollapse,
|
onCollapse,
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
id,
|
data,
|
||||||
indentation,
|
|
||||||
onClick,
|
onClick,
|
||||||
isSelect,
|
isSelect,
|
||||||
highlightValue,
|
highlightValue = '',
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
userKey,
|
||||||
|
indentation,
|
||||||
|
} = data;
|
||||||
|
|
||||||
const isShowKey = userKey !== '';
|
const isShowKey = userKey !== '';
|
||||||
const showIcon = hasChild ? <Arrow director={isCollapsed ? 'right' : 'down'} /> : '';
|
const showIcon = hasChild ? <Triangle director={isCollapsed ? 'right' : 'down'} /> : '';
|
||||||
const handleClickCollapse = () => {
|
const handleClickCollapse = () => {
|
||||||
onCollapse(id);
|
onCollapse(data);
|
||||||
};
|
};
|
||||||
const handleClick = () => {
|
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) {
|
if (isSelect) {
|
||||||
itemAttr.tabIndex = 0;
|
itemAttr.tabIndex = 0;
|
||||||
itemAttr.className = styles.treeItem + ' ' + styles.select;
|
itemAttr.className = styles.treeItem + ' ' + styles.select;
|
||||||
|
@ -93,81 +95,103 @@ function Item(props: IItem) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function VTree({ data, highlightValue }: { data: IData[], highlightValue: string }) {
|
function VTree(props: {
|
||||||
const [scrollTop, setScrollTop] = useState(0);
|
data: IData[],
|
||||||
const [collapseNode, setCollapseNode] = useState(new Set<string>());
|
highlightValue: string,
|
||||||
const [selectItem, setSelectItem] = useState();
|
scrollToItem: IData,
|
||||||
const changeCollapseNode = (id: string) => {
|
onRendered: (renderInfo: renderInfoType<IData>) => void,
|
||||||
const nodes = new Set<string>();
|
collapsedNodes?: IData[],
|
||||||
collapseNode.forEach(value => {
|
onCollapseNode?: (item: IData[]) => void,
|
||||||
nodes.add(value);
|
selectItem: IData[],
|
||||||
});
|
onSelectItem: (item: IData) => void,
|
||||||
if (nodes.has(id)) {
|
}) {
|
||||||
nodes.delete(id);
|
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 {
|
} else {
|
||||||
nodes.add(id);
|
nodes.splice(index, 1);
|
||||||
}
|
}
|
||||||
setCollapseNode(nodes);
|
setCollapseNode(nodes);
|
||||||
|
if (onCollapseNode) {
|
||||||
|
onCollapseNode(nodes);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const handleClickItem = (id: string) => {
|
const handleClickItem = (item: IData) => {
|
||||||
setSelectItem(id);
|
setSelectItem(item);
|
||||||
|
if (onSelectItem) {
|
||||||
|
onSelectItem(item);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
const showList: any[] = [];
|
|
||||||
|
|
||||||
let totalHeight = 0;
|
|
||||||
let currentCollapseIndentation: null | number = null;
|
let currentCollapseIndentation: null | number = null;
|
||||||
data.forEach((item, index) => {
|
// 过滤掉折叠的 item,不展示在 VList 中
|
||||||
// 存在未处理完的收起节点
|
const filter = (item: IData) => {
|
||||||
if (currentCollapseIndentation !== null) {
|
if (currentCollapseIndentation !== null) {
|
||||||
const indentation = item.indentation;
|
|
||||||
// 缩进更大,不显示
|
// 缩进更大,不显示
|
||||||
if (indentation > currentCollapseIndentation) {
|
if (item.indentation > currentCollapseIndentation) {
|
||||||
return;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// 缩进小,说明完成了该收起节点的子节点处理。
|
// 缩进小,说明完成了该收起节点的子节点处理。
|
||||||
currentCollapseIndentation = null;
|
currentCollapseIndentation = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const id = item.id;
|
const isCollapsed = collapseNode.includes(item);
|
||||||
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;
|
|
||||||
if (isCollapsed) {
|
if (isCollapsed) {
|
||||||
// 该节点需要收起子节点
|
// 该节点需要收起子节点
|
||||||
currentCollapseIndentation = item.indentation;
|
currentCollapseIndentation = item.indentation;
|
||||||
}
|
}
|
||||||
});
|
return true;
|
||||||
|
|
||||||
const handleScroll = (event: any) => {
|
|
||||||
const scrollTop = event.target.scrollTop;
|
|
||||||
// 顶部留 100px 冗余高度
|
|
||||||
setScrollTop(Math.max(scrollTop - 100, 0));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.treeContainer} onScroll={handleScroll}>
|
<SizeObserver className={styles.treeContainer}>
|
||||||
{showList}
|
{(width: number, height: number) => {
|
||||||
{/* 确保有足够的高度 */}
|
return (
|
||||||
<div style={{ marginTop: totalHeight }} />
|
<VList
|
||||||
</div>
|
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);
|
@componentKeyValue-color: rgb(26, 26, 166);
|
||||||
@component-attr-color: rgb(200, 0, 0);
|
@component-attr-color: rgb(200, 0, 0);
|
||||||
@select-color: rgb(141 199 248 / 60%);
|
@select-color: rgb(141 199 248 / 60%);
|
||||||
|
@hover-color: black;
|
||||||
|
|
||||||
@top-height: 2.625rem;
|
@top-height: 2.625rem;
|
||||||
@divider-width: 0.2px;
|
@divider-width: 0.2px;
|
||||||
|
|
|
@ -7,6 +7,12 @@
|
||||||
font-size: @common-font-size;
|
font-size: @common-font-size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
button {
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.left {
|
.left {
|
||||||
flex: 7;
|
flex: 7;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -17,6 +23,7 @@
|
||||||
flex: 0 0 @top-height;
|
flex: 0 0 @top-height;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding-right: 0.4rem;
|
||||||
|
|
||||||
.select {
|
.select {
|
||||||
padding: 0 0.25rem 0 0.25rem;
|
padding: 0 0.25rem 0 0.25rem;
|
||||||
|
@ -33,6 +40,20 @@
|
||||||
.search {
|
.search {
|
||||||
flex: 1 1 0;
|
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 {
|
.left_bottom {
|
||||||
|
|
|
@ -5,36 +5,24 @@ import ComponentInfo from '../components/ComponentInfo';
|
||||||
import styles from './App.less';
|
import styles from './App.less';
|
||||||
import Select from '../svgs/Select';
|
import Select from '../svgs/Select';
|
||||||
import { mockParsedVNodeData, parsedMockState } from '../devtools/mock';
|
import { mockParsedVNodeData, parsedMockState } from '../devtools/mock';
|
||||||
|
import { FilterTree } from '../components/FilterTree';
|
||||||
|
import Close from '../svgs/Close';
|
||||||
|
import Arrow from './../svgs/Arrow';
|
||||||
|
|
||||||
function App() {
|
const parseVNodeData = (rawData) => {
|
||||||
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 idIndentationMap: {
|
const idIndentationMap: {
|
||||||
[id: string]: number;
|
[id: string]: number;
|
||||||
} = {};
|
} = {};
|
||||||
const data: IData[] = [];
|
const data: IData[] = [];
|
||||||
let i = 0;
|
let i = 0;
|
||||||
while (i < parsedVNodeData.length) {
|
while (i < rawData.length) {
|
||||||
const id = parsedVNodeData[i] as string;
|
const id = rawData[i] as string;
|
||||||
i++;
|
i++;
|
||||||
const name = parsedVNodeData[i] as string;
|
const name = rawData[i] as string;
|
||||||
i++;
|
i++;
|
||||||
const parentId = parsedVNodeData[i] as string;
|
const parentId = rawData[i] as string;
|
||||||
i++;
|
i++;
|
||||||
const userKey = parsedVNodeData[i] as string;
|
const userKey = rawData[i] as string;
|
||||||
i++;
|
i++;
|
||||||
const indentation = parentId === '' ? 0 : idIndentationMap[parentId] + 1;
|
const indentation = parentId === '' ? 0 : idIndentationMap[parentId] + 1;
|
||||||
idIndentationMap[id] = indentation;
|
idIndentationMap[id] = indentation;
|
||||||
|
@ -43,11 +31,76 @@ function App() {
|
||||||
};
|
};
|
||||||
data.push(item);
|
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) => {
|
const handleSearchChange = (str: string) => {
|
||||||
setFilterValue(str);
|
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 (
|
return (
|
||||||
<div className={styles.app}>
|
<div className={styles.app}>
|
||||||
<div className={styles.left}>
|
<div className={styles.left}>
|
||||||
|
@ -57,15 +110,34 @@ function App() {
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.divider} />
|
<div className={styles.divider} />
|
||||||
<div className={styles.search}>
|
<div className={styles.search}>
|
||||||
<Search onChange={handleSearchChange} />
|
<Search onChange={handleSearchChange} value={filterValue} />
|
||||||
</div>
|
</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>
|
||||||
<div className={styles.left_bottom}>
|
<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>
|
</div>
|
||||||
<div className={styles.right}>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,16 +1,16 @@
|
||||||
interface IArrow {
|
interface IArrow {
|
||||||
director: 'right' | 'down'
|
direction: 'up' | 'down'
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Arrow({ director }: IArrow) {
|
export default function Arrow({ direction: director }: IArrow) {
|
||||||
let d: string;
|
let d: string;
|
||||||
if (director === 'right') {
|
if (director === 'up') {
|
||||||
d = 'm2 0l12 8l-12 8 z';
|
d = 'M4 9.5 L5 10.5 L8 7.5 L11 10.5 L12 9.5 L8 5.5 z';
|
||||||
} else if (director === 'down') {
|
} 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 (
|
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' />
|
<path d={d} fill='currentColor' />
|
||||||
</svg>
|
</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