Match-id-afbd9aba70098fe6a0446b81b44bcd1220e61ee5

This commit is contained in:
* 2022-04-07 19:15:31 +08:00 committed by *
parent cc11ffd17f
commit b3a46f31c5
4 changed files with 210 additions and 100 deletions

View File

@ -1,77 +1,151 @@
// 过滤树的抽象逻辑实现
// 需要知道渲染了哪些数据,搜索的字符串
// 控制Tree组件位置跳转告知搜索文本
// 清空搜索框,告知搜索框当前是第几个结果,跳转搜索结果接口
// 过滤树的抽象逻辑
// 需要知道渲染了哪些数据,过滤的字符串/正则表达式
// 控制Tree组件位置跳转告知匹配结果
// 清空搜索框,告知搜索框当前是第几个结果,跳转搜索结果
//
// 跳转搜索结果的交互逻辑:
// 如果当前页面存在匹配项,页面不动
// 如果当前页面不存在匹配项,页面跳转到第一个匹配项位置
// 如果匹配项被折叠,需要展开其父节点。注意只展开当前匹配项的父节点,其他匹配项的父节点不展开
// 跳转到上一个匹配项或下一个匹配项时,如果匹配项被折叠,需要展开其父节点
//
// 寻找父节点:
// 找到该节点的缩进值和index值在data中向上遍历通过缩进值判断父节点
import { useState, useRef } from 'horizon';
import { createRegExp } from '../utils';
export function FilterTree<T extends {
/**
*
*
* @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
}>(props: { data: T[] }) {
name: string,
indentation: number,
}
export function FilterTree<T extends BaseType>(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,
};
}

View File

@ -8,33 +8,48 @@ interface IProps<T extends { id: string }> {
height: number, // VList 的高度
children: any, // horizon 组件,组件类型是 T
itemHeight: number,
scrollIndex?: number,
onRendered:(renderInfo: renderInfoType) => void;
scrollToItem?: T, // 滚动到指定项位置,如果该项在可见区域内,不滚动,如果不在,则滚动到中间位置
onRendered: (renderInfo: renderInfoType<T>) => void;
filter?(data: T): boolean, // false 表示该行不显示
}
const defaultRenderInfo = {
visibleItems: ([] as string[])
export type renderInfoType<T> = {
visibleItems: T[],
skipItemCountBeforeScrollItem: number,
};
export type renderInfoType = typeof defaultRenderInfo;
export function VList<T extends { id: string }>(props: IProps<T>) {
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<T> } = 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<T extends { id: string }>(props: IProps<T>) {
// 如果最后一个显示不全,不统计在显示 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<T extends { id: string }>(props: IProps<T>) {
</div>
);
if (totalHeight >= scrollTop && totalHeight < maxTop) {
renderInfo.current.visibleItems.push(item);
renderInfoRef.current.visibleItems.push(item);
}
}
totalHeight += itemHeight;
});
return (
<div className={styles.container} onScroll={handleScroll}>
<div ref={containerRef} className={styles.container} onScroll={handleScroll}>
{showList}
<div style={{ marginTop: totalHeight }} />
</div>

View File

@ -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,
@ -38,17 +38,16 @@ function Item(props: IItem) {
const {
name,
userKey,
id,
indentation,
} = data;
const isShowKey = userKey !== '';
const showIcon = hasChild ? <Triangle director={isCollapsed ? 'right' : 'down'} /> : '';
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<IData>) => void,
collapsedNodes?: IData[],
onCollapseNode?: (item: IData[]) => void,
}) {
const [collapseNode, setCollapseNode] = useState(new Set<string>());
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<string>();
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 (
<SizeObserver className={styles.treeContainer}>
{(width, height) => {
{(width: number, height: number) => {
return (
<VList
data={data}
width={width}
height={height}
itemHeight={18}
scrollIndex={data.indexOf(selectItem)}
scrollToItem={scrollToItem}
filter={filter}
onRendered={onRendered}
>
@ -164,8 +170,8 @@ function VTree({ data, highlightValue, selectedId, onRendered }: {
return (
<Item
hasChild={hasChild}
isCollapsed={collapseNode.has(item.id)}
isSelect={selectItem === item.id}
isCollapsed={collapseNode.includes(item)}
isSelect={selectItem === item}
onCollapse={changeCollapseNode}
onClick={handleClickItem}
highlightValue={highlightValue}

View File

@ -9,13 +9,39 @@ import { FilterTree } from '../components/FilterTree';
import Close from '../svgs/Close';
import Arrow from './../svgs/Arrow';
const parseVNodeData = (rawData) => {
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,7 +85,7 @@ function App() {
<Search onChange={handleSearchChange} value={filterValue} />
</div>
{filterValue !== '' && <>
<span className={styles.searchResult}>{`${matchItems.indexOf(selectId) + 1}/${matchItems.length}`}</span>
<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>
@ -86,10 +94,12 @@ function App() {
</div>
<div className={styles.left_bottom}>
<VTree
data={data}
data={parsedVNodeData}
highlightValue={filterValue}
onRendered={onRendered}
selectedId={selectId} />
collapsedNodes={collapsedNodes}
onCollapseNode={setcollapsedNodes}
scrollToItem={currentItem} />
</div>
</div>
<div className={styles.right}>