Match-id-afbd9aba70098fe6a0446b81b44bcd1220e61ee5
This commit is contained in:
parent
cc11ffd17f
commit
b3a46f31c5
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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,19 +85,21 @@ 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>
|
||||
<button className={styles.searchAction} onClick={onClear}><Close/></button>
|
||||
<button className={styles.searchAction} onClick={onSelectLast}><Arrow direction={'up'} /></button>
|
||||
<button className={styles.searchAction} onClick={onSelectNext}><Arrow direction={'down'} /></button>
|
||||
<button className={styles.searchAction} onClick={onClear}><Close /></button>
|
||||
</>}
|
||||
</div>
|
||||
<div className={styles.left_bottom}>
|
||||
<VTree
|
||||
data={data}
|
||||
data={parsedVNodeData}
|
||||
highlightValue={filterValue}
|
||||
onRendered={onRendered}
|
||||
selectedId={selectId} />
|
||||
collapsedNodes={collapsedNodes}
|
||||
onCollapseNode={setcollapsedNodes}
|
||||
scrollToItem={currentItem} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.right}>
|
||||
|
|
Loading…
Reference in New Issue