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} />