From 871fd3bd9b5bbd4b4ba0ebd1b50a01e9e033850e Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 7 Apr 2022 11:49:21 +0800 Subject: [PATCH 01/11] Match-id-dc2d118d676e9ef910e7da00850f961c33cf80b4 --- libs/extension/src/svgs/Arrow.tsx | 12 ++++++------ libs/extension/src/svgs/Close.tsx | 8 ++++++++ libs/extension/src/svgs/Triangle.tsx | 17 +++++++++++++++++ 3 files changed, 31 insertions(+), 6 deletions(-) create mode 100644 libs/extension/src/svgs/Close.tsx create mode 100644 libs/extension/src/svgs/Triangle.tsx diff --git a/libs/extension/src/svgs/Arrow.tsx b/libs/extension/src/svgs/Arrow.tsx index 0cb49d0d..4ee84159 100644 --- a/libs/extension/src/svgs/Arrow.tsx +++ b/libs/extension/src/svgs/Arrow.tsx @@ -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 ( - + ); diff --git a/libs/extension/src/svgs/Close.tsx b/libs/extension/src/svgs/Close.tsx new file mode 100644 index 00000000..621082fd --- /dev/null +++ b/libs/extension/src/svgs/Close.tsx @@ -0,0 +1,8 @@ + +export default function Close() { + return ( + + + + ); +} diff --git a/libs/extension/src/svgs/Triangle.tsx b/libs/extension/src/svgs/Triangle.tsx new file mode 100644 index 00000000..ecb6b3a0 --- /dev/null +++ b/libs/extension/src/svgs/Triangle.tsx @@ -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 ( + + + + ); +} \ No newline at end of file From 5dabd922ba3e5d2a904ce29eca5b4bc5598a3b2b Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 7 Apr 2022 11:49:39 +0800 Subject: [PATCH 02/11] Match-id-7c00d7de1207a0fdf7279258b7a41ca480e7b798 --- libs/extension/src/components/assets.less | 1 + 1 file changed, 1 insertion(+) diff --git a/libs/extension/src/components/assets.less b/libs/extension/src/components/assets.less index 7e74bacf..45e1b0c4 100644 --- a/libs/extension/src/components/assets.less +++ b/libs/extension/src/components/assets.less @@ -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; From e851c5a37ea2d375ccc72e2e66cf71c4b4345cfb Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 7 Apr 2022 11:50:27 +0800 Subject: [PATCH 03/11] Match-id-0f71aa2d32776927b1e526631295e8268fb0b82f --- libs/extension/src/components/ComponentInfo.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/extension/src/components/ComponentInfo.tsx b/libs/extension/src/components/ComponentInfo.tsx index 9c76b503..f9ab06c6 100644 --- a/libs/extension/src/components/ComponentInfo.tsx +++ b/libs/extension/src/components/ComponentInfo.tsx @@ -2,7 +2,7 @@ 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'; type IComponentInfo = { @@ -53,7 +53,7 @@ function ComponentAttr({ name, attr }: { name: string, attr: IAttr[] }) { const isCollapsed = collapsedNode.has(index); showAttr.push(
(handleCollapse(index))}> - {hasChild && } + {hasChild && } {`${item.name}`} {' :'} {item.value} From f5600dd58c5ceeaa957f33bf6099037cf31155d8 Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 7 Apr 2022 11:50:51 +0800 Subject: [PATCH 04/11] Match-id-165f210a5dda1d08fe348888121abc112590ceac --- libs/extension/src/components/ResizeEvent.ts | 78 +++++++++++++++++++ .../extension/src/components/SizeObserver.tsx | 33 ++++++++ 2 files changed, 111 insertions(+) create mode 100644 libs/extension/src/components/ResizeEvent.ts create mode 100644 libs/extension/src/components/SizeObserver.tsx diff --git a/libs/extension/src/components/ResizeEvent.ts b/libs/extension/src/components/ResizeEvent.ts new file mode 100644 index 00000000..0150d0ad --- /dev/null +++ b/libs/extension/src/components/ResizeEvent.ts @@ -0,0 +1,78 @@ +/** + * + * 由于 ResizeObserver 对 IE 和低版本主流浏览器不兼容,需要我们自己解决这个问题。 + * 这是一个不依赖任何框架的监听 dom 元素尺寸变化的解决方案。 + * 浏览器出于性能的考虑,只有 window 的 resize 事件会触发。我们通过 object 标签可以得到 + * 一个 window 对象,让 object dom 元素成为待观测 dom 的子元素,并且和待观测 dom 大小一致。 + * 这样一旦待观测 dom 的大小发生变化, window 的大小也会发生变化,我们就可以通过监听 window + * 大小变化的方式监听待观测 dom 的大小变化。 + * + *
+ * --> 和父 div 保持大小一致 + * --> 添加 resize 事件监听 + * + *
+ * + */ + +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; + } +} diff --git a/libs/extension/src/components/SizeObserver.tsx b/libs/extension/src/components/SizeObserver.tsx new file mode 100644 index 00000000..3d430093 --- /dev/null +++ b/libs/extension/src/components/SizeObserver.tsx @@ -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 ( +
+ {myChild} +
+ ); +} \ No newline at end of file From d490707e78ca11f01f1876d0e1fe83e4be04c4fc Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 7 Apr 2022 11:51:43 +0800 Subject: [PATCH 05/11] Match-id-d9d1111f6e1761d1de56351a998c5c7fff5cb483 --- libs/extension/src/components/VList.less | 11 +++ libs/extension/src/components/VList.tsx | 78 +++++++++++++++++ libs/extension/src/components/VTree.less | 3 - libs/extension/src/components/VTree.tsx | 104 ++++++++++++----------- 4 files changed, 142 insertions(+), 54 deletions(-) create mode 100644 libs/extension/src/components/VList.less create mode 100644 libs/extension/src/components/VList.tsx diff --git a/libs/extension/src/components/VList.less b/libs/extension/src/components/VList.less new file mode 100644 index 00000000..8c14e471 --- /dev/null +++ b/libs/extension/src/components/VList.less @@ -0,0 +1,11 @@ +.container { + position: relative; + overflow-y: auto; + height: 100%; + width: 100%; +} + +.item { + position: absolute; + width: 100%; +} \ No newline at end of file diff --git a/libs/extension/src/components/VList.tsx b/libs/extension/src/components/VList.tsx new file mode 100644 index 00000000..84b71943 --- /dev/null +++ b/libs/extension/src/components/VList.tsx @@ -0,0 +1,78 @@ + +import { useState, useRef, useEffect } from 'horizon'; +import styles from './VList.less'; + +interface IProps { + data: T[], + width: number, // 暂时未用到,当需要支持横向滚动时使用 + height: number, // VList 的高度 + children: any, // horizon 组件,组件类型是 T + itemHeight: number, + scrollIndex?: number, + onRendered:(renderInfo: renderInfoType) => void; + filter?(data: T): boolean, // false 表示该行不显示 +} + +const defaultRenderInfo = { + visibleItems: ([] as string[]) +}; + +export type renderInfoType = typeof defaultRenderInfo; + +export function VList(props: IProps) { + const { + data, + height, + children, + itemHeight, + scrollIndex = 0, + filter, + onRendered, + } = props; + const [scrollTop, setScrollTop] = useState(scrollIndex * itemHeight); + const renderInfo = useRef({visibleItems: []}); + useEffect(() => { + onRendered(renderInfo.current); + }); + + 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; + // 清空记录的上次渲染的数据 + renderInfo.current.visibleItems.length = 0; + data.forEach((item, i) => { + if (filter && !filter(item)) { + return; + } + if (totalHeight >= startShowTopValue && showList.length <= showNum) { + showList.push( +
+ {children(i, item)} +
+ ); + if (totalHeight >= scrollTop && totalHeight < maxTop) { + renderInfo.current.visibleItems.push(item); + } + } + totalHeight += itemHeight; + }); + + return ( +
+ {showList} +
+
+ ); +} diff --git a/libs/extension/src/components/VTree.less b/libs/extension/src/components/VTree.less index 1460ce0f..0f34f9cd 100644 --- a/libs/extension/src/components/VTree.less +++ b/libs/extension/src/components/VTree.less @@ -1,10 +1,7 @@ @import 'assets.less'; .treeContainer { - position: relative; - width: 100%; height: 100%; - overflow-y: auto; .treeItem { width: 100%; diff --git a/libs/extension/src/components/VTree.tsx b/libs/extension/src/components/VTree.tsx index 86f1daf7..5b04385e 100644 --- a/libs/extension/src/components/VTree.tsx +++ b/libs/extension/src/components/VTree.tsx @@ -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; @@ -11,7 +13,6 @@ export interface IData { } type IItem = { - style: any, hasChild: boolean, onCollapse: (id: string) => void, onClick: (id: string) => void, @@ -20,15 +21,11 @@ type IItem = { highlightValue: string, } & IData -// TODO: 计算可以展示的最多数量,并且监听显示器高度变化修改数值 -const showNum = 70; -const lineHeight = 18; const indentationLength = 20; function Item(props: IItem) { const { name, - style, userKey, hasChild, onCollapse, @@ -37,17 +34,17 @@ function Item(props: IItem) { indentation, onClick, isSelect, - highlightValue, + highlightValue = '', } = props; const isShowKey = userKey !== ''; - const showIcon = hasChild ? : ''; + const showIcon = hasChild ? : ''; const handleClickCollapse = () => { onCollapse(id); }; const handleClick = () => { onClick(id); }; - 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,10 +90,17 @@ function Item(props: IItem) { ); } -function VTree({ data, highlightValue }: { data: IData[], highlightValue: string }) { - const [scrollTop, setScrollTop] = useState(0); +function VTree({ data, highlightValue, selectedId, onRendered }: { + data: IData[], + highlightValue: string, + selectedId: number, + onRendered: (renderInfo: renderInfoType) => void +}) { const [collapseNode, setCollapseNode] = useState(new Set()); - const [selectItem, setSelectItem] = useState(); + const [selectItem, setSelectItem] = useState(selectedId); + useEffect(() => { + setSelectItem(selectedId); + }, [selectedId]); const changeCollapseNode = (id: string) => { const nodes = new Set(); collapseNode.forEach(value => { @@ -112,17 +116,14 @@ function VTree({ data, highlightValue }: { data: IData[], highlightValue: string const handleClickItem = (id: string) => { setSelectItem(id); }; - 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; @@ -130,44 +131,45 @@ function VTree({ data, highlightValue }: { data: IData[], highlightValue: string } 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( - - ); - } - totalHeight = totalHeight + lineHeight; if (isCollapsed) { // 该节点需要收起子节点 currentCollapseIndentation = item.indentation; } - }); - - const handleScroll = (event: any) => { - const scrollTop = event.target.scrollTop; - // 顶部留 100px 冗余高度 - setScrollTop(Math.max(scrollTop - 100, 0)); + return true; }; return ( -
- {showList} - {/* 确保有足够的高度 */} -
-
+ + {(width, height) => { + return ( + + {(index: number, item: IData) => { + // 如果存在下一个节点,并且节点缩进比自己大,说明下个节点是子节点,节点本身需要显示展开收起图标 + const nextItem = data[index + 1]; + const hasChild = nextItem && nextItem.indentation > item.indentation; + return ( + + ); + }} + + ); + }} + ); } From baf13b7c5972333da1683e9d47b4cfe458354d1a Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 7 Apr 2022 11:52:07 +0800 Subject: [PATCH 06/11] Match-id-1ded90506bca2ba868af23776da7f666859a3b06 --- libs/extension/src/components/FilterTree.ts | 77 +++++++++++++++++++++ libs/extension/src/components/Search.tsx | 4 +- libs/extension/src/panel/App.less | 18 +++++ libs/extension/src/panel/App.tsx | 34 ++++++++- 4 files changed, 129 insertions(+), 4 deletions(-) create mode 100644 libs/extension/src/components/FilterTree.ts diff --git a/libs/extension/src/components/FilterTree.ts b/libs/extension/src/components/FilterTree.ts new file mode 100644 index 00000000..4660c1b0 --- /dev/null +++ b/libs/extension/src/components/FilterTree.ts @@ -0,0 +1,77 @@ +// 过滤树的抽象逻辑实现 +// 需要知道渲染了哪些数据,搜索的字符串 +// 控制Tree组件位置跳转,告知搜索文本 +// 清空搜索框,告知搜索框当前是第几个结果,跳转搜索结果接口 + +import { useState, useRef } from 'horizon'; +import { createRegExp } from '../utils'; + +export function FilterTree(props: { data: T[] }) { + const { data } = props; + const [filterValue, setFilterValue] = useState(''); + const [selectId, setSelectId] = useState(null); + const showItems = useRef([]); + const matchItemsRef = useRef([]); + const matchItems = matchItemsRef.current; + const onChangeSearchValue = (search: string) => { + const reg = createRegExp(search); + let matchShowId = null; + let newMatchItems = []; + if (search !== '') { + newMatchItems = data.reduce((pre, current) => { + const { id, name } = current; + if (reg && name.match(reg)) { + pre.push(id); + if (matchShowId === null) { + matchShowId = id; + } + } + return pre; + }, []); + if (newMatchItems.length === 0) { + setSelectId(null); + } else { + if (matchShowId === null) { + setSelectId(newMatchItems[0]); + } else { + setSelectId(matchShowId); + } + } + } + matchItemsRef.current = newMatchItems; + setFilterValue(search); + }; + const onSelectNext = () => { + const index = matchItems.indexOf(selectId); + const nextIndex = index + 1; + if (nextIndex < matchItemsRef.current.length) { + setSelectId(matchItems[nextIndex]); + } + }; + const onSelectLast = () => { + const index = matchItems.indexOf(selectId); + const last = index - 1; + if (last >= 0) { + setSelectId(matchItems[last]); + } + }; + const setShowItems = (items) => { + showItems.current = [...items]; + }; + const onClear = () => { + onChangeSearchValue(''); + }; + return { + filterValue, + setFilterValue: onChangeSearchValue, + onClear, + selectId, + matchItems, + onSelectNext, + onSelectLast, + setShowItems, + }; +} diff --git a/libs/extension/src/components/Search.tsx b/libs/extension/src/components/Search.tsx index ce644325..1ea264d3 100644 --- a/libs/extension/src/components/Search.tsx +++ b/libs/extension/src/components/Search.tsx @@ -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) { ); diff --git a/libs/extension/src/panel/App.less b/libs/extension/src/panel/App.less index f48e225a..dc0da330 100644 --- a/libs/extension/src/panel/App.less +++ b/libs/extension/src/panel/App.less @@ -17,6 +17,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 +34,23 @@ .search { flex: 1 1 0; } + + .searchResult{ + flex: 0 0 ; + padding: 0 0.4rem; + } + + .searchAction { + padding: 0; + flex: 0 0 1rem; + border: none; + background: none; + height: 1rem; + color: @arrow-color; + &:hover{ + color: @hover-color; + } + } } .left_bottom { diff --git a/libs/extension/src/panel/App.tsx b/libs/extension/src/panel/App.tsx index 2b9ef6bc..1e9a9ae0 100644 --- a/libs/extension/src/panel/App.tsx +++ b/libs/extension/src/panel/App.tsx @@ -5,11 +5,14 @@ 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); @@ -43,11 +46,25 @@ function App() { }; data.push(item); } + const { + filterValue, + setFilterValue, + onClear, + selectId, + matchItems, + onSelectNext, + onSelectLast, + setShowItems, + } = FilterTree({ data }); const handleSearchChange = (str: string) => { setFilterValue(str); }; + const onRendered = (info) => { + setShowItems(info.visibleItems); + }; + return (
@@ -57,11 +74,22 @@ function App() {
- +
+ {filterValue !== '' && <> + {`${matchItems.indexOf(selectId) + 1}/${matchItems.length}`} +
+ + + + }
- +
From cc11ffd17fb92f6e8bdc3eee76e72b49a6dc00fe Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 7 Apr 2022 11:56:48 +0800 Subject: [PATCH 07/11] Match-id-e266428ff041c68ffd2851a12262c612f5a354c3 --- libs/extension/src/components/VTree.tsx | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/libs/extension/src/components/VTree.tsx b/libs/extension/src/components/VTree.tsx index 5b04385e..8835aede 100644 --- a/libs/extension/src/components/VTree.tsx +++ b/libs/extension/src/components/VTree.tsx @@ -12,30 +12,36 @@ export interface IData { userKey: string; } -type IItem = { +interface IItem { hasChild: boolean, onCollapse: (id: string) => void, onClick: (id: string) => void, isCollapsed: boolean, isSelect: boolean, highlightValue: string, -} & IData + data: IData, +} const indentationLength = 20; function Item(props: IItem) { const { - name, - userKey, hasChild, onCollapse, isCollapsed, - id, - indentation, + data, onClick, isSelect, highlightValue = '', } = props; + + const { + name, + userKey, + id, + indentation, + } = data; + const isShowKey = userKey !== ''; const showIcon = hasChild ? : ''; const handleClickCollapse = () => { @@ -163,7 +169,7 @@ function VTree({ data, highlightValue, selectedId, onRendered }: { onCollapse={changeCollapseNode} onClick={handleClickItem} highlightValue={highlightValue} - {...item} /> + data={item} /> ); }} From b3a46f31c51d3f45259c809bb667c4a277e48657 Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 7 Apr 2022 19:15:31 +0800 Subject: [PATCH 08/11] Match-id-afbd9aba70098fe6a0446b81b44bcd1220e61ee5 --- libs/extension/src/components/FilterTree.ts | 126 ++++++++++++++++---- libs/extension/src/components/VList.tsx | 46 +++++-- libs/extension/src/components/VTree.tsx | 66 +++++----- libs/extension/src/panel/App.tsx | 72 ++++++----- 4 files changed, 210 insertions(+), 100 deletions(-) diff --git a/libs/extension/src/components/FilterTree.ts b/libs/extension/src/components/FilterTree.ts index 4660c1b0..66dd101a 100644 --- a/libs/extension/src/components/FilterTree.ts +++ b/libs/extension/src/components/FilterTree.ts @@ -1,77 +1,151 @@ -// 过滤树的抽象逻辑实现 -// 需要知道渲染了哪些数据,搜索的字符串 -// 控制Tree组件位置跳转,告知搜索文本 -// 清空搜索框,告知搜索框当前是第几个结果,跳转搜索结果接口 +// 过滤树的抽象逻辑 +// 需要知道渲染了哪些数据,过滤的字符串/正则表达式 +// 控制Tree组件位置跳转,告知匹配结果 +// 清空搜索框,告知搜索框当前是第几个结果,跳转搜索结果 +// +// 跳转搜索结果的交互逻辑: +// 如果当前页面存在匹配项,页面不动 +// 如果当前页面不存在匹配项,页面跳转到第一个匹配项位置 +// 如果匹配项被折叠,需要展开其父节点。注意只展开当前匹配项的父节点,其他匹配项的父节点不展开 +// 跳转到上一个匹配项或下一个匹配项时,如果匹配项被折叠,需要展开其父节点 +// +// 寻找父节点: +// 找到该节点的缩进值,和index值,在data中向上遍历,通过缩进值判断父节点 import { useState, useRef } from 'horizon'; import { createRegExp } from '../utils'; -export function FilterTree= 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 -}>(props: { data: T[] }) { + name: string, + indentation: number, +} + +export function FilterTree(props: { data: T[] }) { const { data } = props; const [filterValue, setFilterValue] = useState(''); - const [selectId, setSelectId] = useState(null); - const showItems = useRef([]); - const matchItemsRef = useRef([]); + 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 matchShowId = null; + let newCurrentItem = null; let newMatchItems = []; if (search !== '') { + const showItems: T[] = showItemsRef.current; newMatchItems = data.reduce((pre, current) => { - const { id, name } = current; + const { name } = current; if (reg && name.match(reg)) { - pre.push(id); - if (matchShowId === null) { - matchShowId = id; + pre.push(current); + // 如果当前页面显示的 item 存在匹配项,则把它设置为 currentItem + if (newCurrentItem === null && showItems.includes(current)) { + newCurrentItem = current; } } return pre; }, []); if (newMatchItems.length === 0) { - setSelectId(null); + setCurrentItem(null); } else { - if (matchShowId === null) { - setSelectId(newMatchItems[0]); + if (newCurrentItem === null) { + const item = newMatchItems[0]; + // 不处于当前展示页面,需要展开父节点 + updatecollapsedNodes(item); + setCurrentItem(item); } else { - setSelectId(matchShowId); + setCurrentItem(newCurrentItem); } } + } else { + setCurrentItem(null); } matchItemsRef.current = newMatchItems; setFilterValue(search); }; const onSelectNext = () => { - const index = matchItems.indexOf(selectId); + const index = matchItems.indexOf(currentItem); const nextIndex = index + 1; if (nextIndex < matchItemsRef.current.length) { - setSelectId(matchItems[nextIndex]); + const item = matchItems[nextIndex]; + // 不处于当前展示页面,需要展开父节点 + updatecollapsedNodes(item); + setCurrentItem(item); } }; const onSelectLast = () => { - const index = matchItems.indexOf(selectId); + const index = matchItems.indexOf(currentItem); const last = index - 1; if (last >= 0) { - setSelectId(matchItems[last]); + const item = matchItems[last]; + // 不处于当前展示页面,需要展开父节点 + updatecollapsedNodes(item); + setCurrentItem(matchItems[last]); } }; const setShowItems = (items) => { - showItems.current = [...items]; + showItemsRef.current = [...items]; }; const onClear = () => { onChangeSearchValue(''); }; + const setcollapsedNodes = (items) => { + // 不更新引用,避免子组件的重复渲染 + collapsedNodesRef.current.length = 0; + collapsedNodesRef.current.push(...items); + }; return { filterValue, - setFilterValue: onChangeSearchValue, + onChangeSearchValue, onClear, - selectId, + currentItem, matchItems, onSelectNext, onSelectLast, setShowItems, + collapsedNodes, + setcollapsedNodes, }; } diff --git a/libs/extension/src/components/VList.tsx b/libs/extension/src/components/VList.tsx index 84b71943..c932a492 100644 --- a/libs/extension/src/components/VList.tsx +++ b/libs/extension/src/components/VList.tsx @@ -8,33 +8,48 @@ interface IProps { height: number, // VList 的高度 children: any, // horizon 组件,组件类型是 T itemHeight: number, - scrollIndex?: number, - onRendered:(renderInfo: renderInfoType) => void; + scrollToItem?: T, // 滚动到指定项位置,如果该项在可见区域内,不滚动,如果不在,则滚动到中间位置 + onRendered: (renderInfo: renderInfoType) => void; filter?(data: T): boolean, // false 表示该行不显示 } -const defaultRenderInfo = { - visibleItems: ([] as string[]) +export type renderInfoType = { + visibleItems: T[], + skipItemCountBeforeScrollItem: number, }; -export type renderInfoType = typeof defaultRenderInfo; - export function VList(props: IProps) { const { data, height, children, itemHeight, - scrollIndex = 0, + scrollToItem, filter, onRendered, } = props; - const [scrollTop, setScrollTop] = useState(scrollIndex * itemHeight); - const renderInfo = useRef({visibleItems: []}); + const [scrollTop, setScrollTop] = useState(data.indexOf(scrollToItem) * itemHeight); + const renderInfoRef: { current: renderInfoType } = useRef({ visibleItems: [], skipItemCountBeforeScrollItem: 0 }); + const containerRef = useRef(); useEffect(() => { - onRendered(renderInfo.current); + 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); @@ -48,9 +63,14 @@ export function VList(props: IProps) { // 如果最后一个显示不全,不统计在显示 ids 内 const maxTop = scrollTop + height - itemHeight; // 清空记录的上次渲染的数据 - renderInfo.current.visibleItems.length = 0; + 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) { @@ -63,14 +83,14 @@ export function VList(props: IProps) {
); if (totalHeight >= scrollTop && totalHeight < maxTop) { - renderInfo.current.visibleItems.push(item); + renderInfoRef.current.visibleItems.push(item); } } totalHeight += itemHeight; }); return ( -
+
{showList}
diff --git a/libs/extension/src/components/VTree.tsx b/libs/extension/src/components/VTree.tsx index 8835aede..044e64dc 100644 --- a/libs/extension/src/components/VTree.tsx +++ b/libs/extension/src/components/VTree.tsx @@ -14,8 +14,8 @@ export interface IData { 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, @@ -34,21 +34,20 @@ function Item(props: IItem) { isSelect, highlightValue = '', } = props; - + const { name, userKey, - id, - indentation, + indentation, } = data; const isShowKey = userKey !== ''; const showIcon = hasChild ? : ''; const handleClickCollapse = () => { - onCollapse(id); + onCollapse(data); }; const handleClick = () => { - onClick(id); + onClick(data); }; const itemAttr: any = { className: styles.treeItem, onClick: handleClick }; if (isSelect) { @@ -96,31 +95,39 @@ function Item(props: IItem) { ); } -function VTree({ data, highlightValue, selectedId, onRendered }: { +function VTree(props: { data: IData[], highlightValue: string, - selectedId: number, - onRendered: (renderInfo: renderInfoType) => void + scrollToItem: IData, + onRendered: (renderInfo: renderInfoType) => void, + collapsedNodes?: IData[], + onCollapseNode?: (item: IData[]) => void, }) { - const [collapseNode, setCollapseNode] = useState(new Set()); - const [selectItem, setSelectItem] = useState(selectedId); + const { data, highlightValue, scrollToItem, onRendered, onCollapseNode } = props; + const [collapseNode, setCollapseNode] = useState(props.collapsedNodes || []); + const [selectItem, setSelectItem] = useState(scrollToItem); useEffect(() => { - setSelectItem(selectedId); - }, [selectedId]); - const changeCollapseNode = (id: string) => { - const nodes = new Set(); - collapseNode.forEach(value => { - nodes.add(value); - }); - if (nodes.has(id)) { - nodes.delete(id); + setSelectItem(scrollToItem); + }, [scrollToItem]); + 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); }; let currentCollapseIndentation: null | number = null; @@ -135,8 +142,7 @@ function VTree({ data, highlightValue, selectedId, onRendered }: { currentCollapseIndentation = null; } } - const id = item.id; - const isCollapsed = collapseNode.has(id); + const isCollapsed = collapseNode.includes(item); if (isCollapsed) { // 该节点需要收起子节点 currentCollapseIndentation = item.indentation; @@ -146,14 +152,14 @@ function VTree({ data, highlightValue, selectedId, onRendered }: { return ( - {(width, height) => { + {(width: number, height: number) => { return ( @@ -164,8 +170,8 @@ function VTree({ data, highlightValue, selectedId, onRendered }: { return ( { + const idIndentationMap: { + [id: string]: number; + } = {}; + const data: IData[] = []; + let i = 0; + while (i < rawData.length) { + const id = rawData[i] as string; + i++; + const name = rawData[i] as string; + i++; + const parentId = rawData[i] as string; + i++; + const userKey = rawData[i] as string; + i++; + const indentation = parentId === '' ? 0 : idIndentationMap[parentId] + 1; + idIndentationMap[id] = indentation; + const item = { + id, name, indentation, userKey + }; + data.push(item); + } + return data; +}; + function App() { const [parsedVNodeData, setParsedVNodeData] = useState([]); const [componentInfo, setComponentInfo] = useState({ name: null, attrs: {} }); useEffect(() => { if (isDev) { - setParsedVNodeData(mockParsedVNodeData); + const parsedData = parseVNodeData(mockParsedVNodeData); + setParsedVNodeData(parsedData); setComponentInfo({ name: 'Demo', attrs: { @@ -25,37 +51,19 @@ function App() { }); } }, []); - const idIndentationMap: { - [id: string]: number; - } = {}; - const data: IData[] = []; - let i = 0; - while (i < parsedVNodeData.length) { - const id = parsedVNodeData[i] as string; - i++; - const name = parsedVNodeData[i] as string; - i++; - const parentId = parsedVNodeData[i] as string; - i++; - const userKey = parsedVNodeData[i] as string; - i++; - const indentation = parentId === '' ? 0 : idIndentationMap[parentId] + 1; - idIndentationMap[id] = indentation; - const item = { - id, name, indentation, userKey - }; - data.push(item); - } + const { filterValue, - setFilterValue, + onChangeSearchValue: setFilterValue, onClear, - selectId, + currentItem, matchItems, onSelectNext, onSelectLast, setShowItems, - } = FilterTree({ data }); + collapsedNodes, + setcollapsedNodes, + } = FilterTree({ data: parsedVNodeData }); const handleSearchChange = (str: string) => { setFilterValue(str); @@ -77,19 +85,21 @@ function App() {
{filterValue !== '' && <> - {`${matchItems.indexOf(selectId) + 1}/${matchItems.length}`} + {`${matchItems.indexOf(currentItem) + 1}/${matchItems.length}`}
- - - + + + }
+ collapsedNodes={collapsedNodes} + onCollapseNode={setcollapsedNodes} + scrollToItem={currentItem} />
From e7dbc1c1bf04d0dab2f61dfa62702cb96a342891 Mon Sep 17 00:00:00 2001 From: * <8> Date: Thu, 7 Apr 2022 19:23:39 +0800 Subject: [PATCH 09/11] Match-id-5fb1eed20c25112c3ca480cb9f1f793fcfb1c046 --- libs/extension/src/components/FilterTree.ts | 28 +++++++++------------ libs/extension/src/panel/App.tsx | 4 +-- 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/libs/extension/src/components/FilterTree.ts b/libs/extension/src/components/FilterTree.ts index 66dd101a..58706902 100644 --- a/libs/extension/src/components/FilterTree.ts +++ b/libs/extension/src/components/FilterTree.ts @@ -61,7 +61,7 @@ export function FilterTree(props: { data: T[] }) { const matchItems = matchItemsRef.current; const collapsedNodes = collapsedNodesRef.current; - const updatecollapsedNodes = (item: BaseType) => { + const updateCollapsedNodes = (item: BaseType) => { const newcollapsedNodes = expandItemParent(item, data, collapsedNodes); // 如果新旧收起节点数组长度不一样,说明存在收起节点 if (newcollapsedNodes.length !== collapsedNodes.length) { @@ -93,7 +93,7 @@ export function FilterTree(props: { data: T[] }) { if (newCurrentItem === null) { const item = newMatchItems[0]; // 不处于当前展示页面,需要展开父节点 - updatecollapsedNodes(item); + updateCollapsedNodes(item); setCurrentItem(item); } else { setCurrentItem(newCurrentItem); @@ -108,22 +108,18 @@ export function FilterTree(props: { data: T[] }) { const onSelectNext = () => { const index = matchItems.indexOf(currentItem); const nextIndex = index + 1; - if (nextIndex < matchItemsRef.current.length) { - const item = matchItems[nextIndex]; - // 不处于当前展示页面,需要展开父节点 - updatecollapsedNodes(item); - setCurrentItem(item); - } + 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; - if (last >= 0) { - const item = matchItems[last]; - // 不处于当前展示页面,需要展开父节点 - updatecollapsedNodes(item); - setCurrentItem(matchItems[last]); - } + const item = last >= 0 ? matchItems[last] : matchItems[matchItems.length - 1]; + // 可能不处于当前展示页面,需要展开父节点 + updateCollapsedNodes(item); + setCurrentItem(item); }; const setShowItems = (items) => { showItemsRef.current = [...items]; @@ -131,7 +127,7 @@ export function FilterTree(props: { data: T[] }) { const onClear = () => { onChangeSearchValue(''); }; - const setcollapsedNodes = (items) => { + const setCollapsedNodes = (items) => { // 不更新引用,避免子组件的重复渲染 collapsedNodesRef.current.length = 0; collapsedNodesRef.current.push(...items); @@ -146,6 +142,6 @@ export function FilterTree(props: { data: T[] }) { onSelectLast, setShowItems, collapsedNodes, - setcollapsedNodes, + setCollapsedNodes, }; } diff --git a/libs/extension/src/panel/App.tsx b/libs/extension/src/panel/App.tsx index af5a8990..dcc18a67 100644 --- a/libs/extension/src/panel/App.tsx +++ b/libs/extension/src/panel/App.tsx @@ -62,7 +62,7 @@ function App() { onSelectLast, setShowItems, collapsedNodes, - setcollapsedNodes, + setCollapsedNodes, } = FilterTree({ data: parsedVNodeData }); const handleSearchChange = (str: string) => { @@ -98,7 +98,7 @@ function App() { highlightValue={filterValue} onRendered={onRendered} collapsedNodes={collapsedNodes} - onCollapseNode={setcollapsedNodes} + onCollapseNode={setCollapsedNodes} scrollToItem={currentItem} />
From 64a78f34ba05c89ecbd5e235bce1a214898e715b Mon Sep 17 00:00:00 2001 From: * <8> Date: Sat, 9 Apr 2022 15:55:40 +0800 Subject: [PATCH 10/11] Match-id-76e847694efa871d9b2bb281012e3ba581f963f8 --- .../src/components/ComponentInfo.tsx | 43 +++++++++++-------- libs/extension/src/components/FilterTree.ts | 8 ++-- 2 files changed, 28 insertions(+), 23 deletions(-) diff --git a/libs/extension/src/components/ComponentInfo.tsx b/libs/extension/src/components/ComponentInfo.tsx index f9ab06c6..80127470 100644 --- a/libs/extension/src/components/ComponentInfo.tsx +++ b/libs/extension/src/components/ComponentInfo.tsx @@ -22,24 +22,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(); - 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,11 +53,11 @@ 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( -
(handleCollapse(index))}> +
(handleCollapse(item))}> {hasChild && } {`${item.name}`} {' :'} @@ -95,10 +100,10 @@ export default function ComponentInfo({ name, attrs }: IComponentInfo) {
- {context && } - {props && } - {state && } - {hooks && } + {context && } + {props && } + {state && } + {hooks && }
rendered by
diff --git a/libs/extension/src/components/FilterTree.ts b/libs/extension/src/components/FilterTree.ts index 58706902..446bd05d 100644 --- a/libs/extension/src/components/FilterTree.ts +++ b/libs/extension/src/components/FilterTree.ts @@ -27,7 +27,7 @@ function expandItemParent(item: BaseType, data: BaseType[], collapsedNodes: Base const index = data.indexOf(item); let currentIndentation = item.indentation; // 不对原始数据进行修改 - const newcollapsedNodes = [...collapsedNodes]; + const newCollapsedNodes = [...collapsedNodes]; for (let i = index - 1; i >= 0; i--) { const lastData = data[i]; const lastIndentation = lastData.indentation; @@ -35,13 +35,13 @@ function expandItemParent(item: BaseType, data: BaseType[], collapsedNodes: Base if (lastIndentation < currentIndentation) { // 更新缩进值,只找父节点的父节点,避免修改父节点的兄弟节点的展开状态 currentIndentation = lastIndentation; - const cIndex = newcollapsedNodes.indexOf(lastData); + const cIndex = newCollapsedNodes.indexOf(lastData); if (cIndex !== -1) { - newcollapsedNodes.splice(cIndex, 1); + newCollapsedNodes.splice(cIndex, 1); } } } - return newcollapsedNodes; + return newCollapsedNodes; } type BaseType = { From a81e49833d6493431fee44c3541a13e7f45be5a5 Mon Sep 17 00:00:00 2001 From: * <8> Date: Mon, 11 Apr 2022 11:04:30 +0800 Subject: [PATCH 11/11] Match-id-059b810a64127ac4b754b8a924dd931f129dafa2 --- .../src/components/ComponentInfo.tsx | 39 +++++++---- .../src/components/ComponentsInfo.less | 22 +++--- libs/extension/src/components/VTree.tsx | 16 ++++- libs/extension/src/panel/App.less | 9 ++- libs/extension/src/panel/App.tsx | 68 ++++++++++++++----- 5 files changed, 110 insertions(+), 44 deletions(-) diff --git a/libs/extension/src/components/ComponentInfo.tsx b/libs/extension/src/components/ComponentInfo.tsx index 80127470..10a1f637 100644 --- a/libs/extension/src/components/ComponentInfo.tsx +++ b/libs/extension/src/components/ComponentInfo.tsx @@ -4,6 +4,7 @@ import Debug from '../svgs/Debug'; import Copy from '../svgs/Copy'; 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 = { @@ -84,28 +87,38 @@ function ComponentAttr({ name, attrs }: { name: string, attrs: IAttr[] }) { ); } -export default function ComponentInfo({ name, attrs }: IComponentInfo) { +export default function ComponentInfo({ name, attrs, parents, onClickParent }: IComponentInfo) { const { state, props, context, hooks } = attrs; return (
- - {name} - - - - - - - + {name && <> + + {name} + + + + + + + + }
{context && } {props && } {state && } {hooks && } -
- rendered by +
+ {name &&
+ parents: { + parents.map(item => ()) + } +
}
diff --git a/libs/extension/src/components/ComponentsInfo.less b/libs/extension/src/components/ComponentsInfo.less index 3622363e..9a52e2fb 100644 --- a/libs/extension/src/components/ComponentsInfo.less +++ b/libs/extension/src/components/ComponentsInfo.less @@ -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; + } } } } diff --git a/libs/extension/src/components/VTree.tsx b/libs/extension/src/components/VTree.tsx index 044e64dc..a286e411 100644 --- a/libs/extension/src/components/VTree.tsx +++ b/libs/extension/src/components/VTree.tsx @@ -102,13 +102,20 @@ function VTree(props: { onRendered: (renderInfo: renderInfoType) => void, collapsedNodes?: IData[], onCollapseNode?: (item: IData[]) => void, + selectItem: IData[], + onSelectItem: (item: IData) => void, }) { - const { data, highlightValue, scrollToItem, onRendered, onCollapseNode } = props; + const { data, highlightValue, scrollToItem, onRendered, onCollapseNode, onSelectItem } = props; const [collapseNode, setCollapseNode] = useState(props.collapsedNodes || []); - const [selectItem, setSelectItem] = useState(scrollToItem); + 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]); @@ -128,6 +135,9 @@ function VTree(props: { }; const handleClickItem = (item: IData) => { setSelectItem(item); + if (onSelectItem) { + onSelectItem(item); + } }; let currentCollapseIndentation: null | number = null; @@ -159,7 +169,7 @@ function VTree(props: { width={width} height={height} itemHeight={18} - scrollToItem={scrollToItem} + scrollToItem={selectItem} filter={filter} onRendered={onRendered} > diff --git a/libs/extension/src/panel/App.less b/libs/extension/src/panel/App.less index dc0da330..b974c557 100644 --- a/libs/extension/src/panel/App.less +++ b/libs/extension/src/panel/App.less @@ -7,6 +7,12 @@ font-size: @common-font-size; } +button { + border: none; + background: none; + padding: 0; +} + .left { flex: 7; display: flex; @@ -41,10 +47,7 @@ } .searchAction { - padding: 0; flex: 0 0 1rem; - border: none; - background: none; height: 1rem; color: @arrow-color; &:hover{ diff --git a/libs/extension/src/panel/App.tsx b/libs/extension/src/panel/App.tsx index dcc18a67..7cea62e9 100644 --- a/libs/extension/src/panel/App.tsx +++ b/libs/extension/src/panel/App.tsx @@ -34,23 +34,27 @@ const parseVNodeData = (rawData) => { 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 [componentInfo, setComponentInfo] = useState({ name: null, attrs: {} }); - - useEffect(() => { - if (isDev) { - const parsedData = parseVNodeData(mockParsedVNodeData); - setParsedVNodeData(parsedData); - setComponentInfo({ - name: 'Demo', - attrs: { - state: parsedMockState, - props: parsedMockState, - }, - }); - } - }, []); + const [componentAttrs, setComponentAttrs] = useState({}); + const [selectComp, setSelectComp] = useState(null); const { filterValue, @@ -65,13 +69,37 @@ function App() { 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 (
@@ -99,11 +127,17 @@ function App() { onRendered={onRendered} collapsedNodes={collapsedNodes} onCollapseNode={setCollapsedNodes} - scrollToItem={currentItem} /> + scrollToItem={currentItem} + selectItem={selectComp} + onSelectItem={handleSelectComp} />
- +
);