Skip to content

Commit f30db20

Browse files
久戈LeoYuan
authored andcommitted
feat: 大纲树支持节点过滤
1 parent 670eeb9 commit f30db20

File tree

11 files changed

+351
-9
lines changed

11 files changed

+351
-9
lines changed

packages/editor-core/src/widgets/title/index.tsx

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,48 @@
1-
import { Component, isValidElement } from 'react';
1+
import { Component, isValidElement, ReactNode } from 'react';
22
import classNames from 'classnames';
33
import { createIcon } from '@alilc/lowcode-utils';
4-
import { TitleContent, isI18nData } from '@alilc/lowcode-types';
4+
import { TitleContent, isI18nData, I18nData } from '@alilc/lowcode-types';
55
import { intl } from '../../intl';
66
import { Tip } from '../tip';
77
import './title.less';
88

9-
export class Title extends Component<{ title: TitleContent; className?: string; onClick?: () => void }> {
9+
/**
10+
* 根据 keywords 将 label 分割成文字片段
11+
* 示例:title = '自定义页面布局',keywords = '页面',返回结果为 ['自定义', '页面', '布局']
12+
* @param label title
13+
* @param keywords 关键字
14+
* @returns 文字片段列表
15+
*/
16+
function splitLabelByKeywords(label: string, keywords: string): string[] {
17+
const len = keywords.length;
18+
const fragments = [];
19+
let str = label;
20+
21+
while (str.length > 0) {
22+
const index = str.indexOf(keywords);
23+
24+
if (index === 0) {
25+
fragments.push(keywords);
26+
str = str.slice(len);
27+
} else if (index < 0) {
28+
fragments.push(str);
29+
str = '';
30+
} else {
31+
fragments.push(str.slice(0, index));
32+
str = str.slice(index);
33+
}
34+
}
35+
36+
return fragments;
37+
}
38+
39+
export class Title extends Component<{
40+
title: TitleContent;
41+
className?: string;
42+
onClick?: () => void;
43+
match?: boolean;
44+
keywords?: string;
45+
}> {
1046
constructor(props: any) {
1147
super(props);
1248
this.handleClick = this.handleClick.bind(this);
@@ -24,6 +60,32 @@ export class Title extends Component<{ title: TitleContent; className?: string;
2460
onClick && onClick(e);
2561
}
2662

63+
renderLabel = (label: string | I18nData | ReactNode) => {
64+
let { match, keywords } = this.props;
65+
66+
if (!label) {
67+
return null;
68+
}
69+
70+
const intlLabel = intl(label);
71+
72+
if (typeof intlLabel !== 'string') {
73+
return <span className="lc-title-txt">{intlLabel}</span>;
74+
}
75+
76+
let labelToRender: ReactNode = intlLabel;
77+
78+
if (match && keywords) {
79+
const fragments = splitLabelByKeywords(intlLabel as string, keywords);
80+
81+
labelToRender = fragments.map(f => <span style={{ color: f === keywords ? 'red' : 'inherit' }}>{f}</span>);
82+
}
83+
84+
return (
85+
<span className="lc-title-txt">{labelToRender}</span>
86+
);
87+
};
88+
2789
render() {
2890
// eslint-disable-next-line prefer-const
2991
let { title, className } = this.props;
@@ -61,7 +123,7 @@ export class Title extends Component<{ title: TitleContent; className?: string;
61123
onClick={this.handleClick}
62124
>
63125
{icon ? <b className="lc-title-icon">{icon}</b> : null}
64-
{title.label ? <span className="lc-title-txt">{intl(title.label)}</span> : null}
126+
{this.renderLabel(title.label)}
65127
{tip}
66128
</span>
67129
);

packages/editor-skeleton/src/layouts/workbench.less

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ body {
117117
}
118118
*/
119119
}
120-
.lc-outline-pane {
120+
.lc-outline-tree-container {
121121
border-top: 1px solid var(--color-line-normal, rgba(31, 56, 88, 0.1));
122122
}
123123
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import { SVGIcon, IconProps } from '@alilc/lowcode-utils';
2+
3+
export function IconFilter(props: IconProps) {
4+
return (
5+
<SVGIcon viewBox="0 0 1024 1024" {...props}>
6+
<path d="M911.457097 168.557714a35.986286 35.986286 0 0 1-8.009143 40.009143L621.73824 490.276571V914.285714c0 14.848-9.142857 28.013714-22.272 33.718857A42.349714 42.349714 0 0 1 585.166811 950.857143a34.084571 34.084571 0 0 1-25.709714-10.861714l-146.285714-146.285715A36.425143 36.425143 0 0 1 402.309669 768v-277.723429L120.599954 208.566857a35.986286 35.986286 0 0 1-8.009143-40.009143C118.295954 155.428571 131.461669 146.285714 146.309669 146.285714h731.428571c14.848 0 28.013714 9.142857 33.718857 22.272z" fill="#666" p-id="2025" />
7+
</SVGIcon>
8+
);
9+
}
10+
11+
IconFilter.displayName = 'IconFilter';

packages/plugin-outline-pane/src/tree-node.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,20 @@ import { computed, obx, intl, makeObservable, action } from '@alilc/lowcode-edit
33
import { Node, DocumentModel, isLocationChildrenDetail, LocationChildrenDetail, Designer } from '@alilc/lowcode-designer';
44
import { Tree } from './tree';
55

6+
/**
7+
* 大纲树过滤结果
8+
*/
9+
export interface FilterResult {
10+
// 过滤条件是否生效
11+
filterWorking: boolean;
12+
// 命中子节点
13+
matchChild: boolean;
14+
// 命中本节点
15+
matchSelf: boolean;
16+
// 关键字
17+
keywords: string;
18+
}
19+
620
export default class TreeNode {
721
get id(): string {
822
return this.node.id;
@@ -231,4 +245,20 @@ export default class TreeNode {
231245
this._node = node;
232246
}
233247
}
248+
249+
@obx.ref private _filterResult: FilterResult = {
250+
filterWorking: false,
251+
matchChild: false,
252+
matchSelf: false,
253+
keywords: '',
254+
};
255+
256+
get filterReult(): FilterResult {
257+
return this._filterResult;
258+
}
259+
260+
@action
261+
setFilterReult(val: FilterResult) {
262+
this._filterResult = val;
263+
}
234264
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import TreeNode from '../tree-node';
2+
3+
export const FilterType = {
4+
CONDITION: 'CONDITION',
5+
LOOP: 'LOOP',
6+
LOCKED: 'LOCKED',
7+
HIDDEN: 'HIDDEN',
8+
};
9+
10+
export const FILTER_OPTIONS = [{
11+
value: FilterType.CONDITION,
12+
label: '条件渲染',
13+
}, {
14+
value: FilterType.LOOP,
15+
label: '循环渲染',
16+
}, {
17+
value: FilterType.LOCKED,
18+
label: '已锁定',
19+
}, {
20+
value: FilterType.HIDDEN,
21+
label: '已隐藏',
22+
}];
23+
24+
export const matchTreeNode = (
25+
treeNode: TreeNode,
26+
keywords: string,
27+
filterOps: string[],
28+
): boolean => {
29+
// 无效节点
30+
if (!treeNode || !treeNode.node) {
31+
return false;
32+
}
33+
34+
// 过滤条件为空,重置过滤结果
35+
if (!keywords && filterOps.length === 0) {
36+
treeNode.setFilterReult({
37+
filterWorking: false,
38+
matchChild: false,
39+
matchSelf: false,
40+
keywords: '',
41+
});
42+
43+
(treeNode.children || []).concat(treeNode.slots || []).forEach((childNode) => {
44+
matchTreeNode(childNode, keywords, filterOps);
45+
});
46+
47+
return false;
48+
}
49+
50+
const { node } = treeNode;
51+
52+
// 命中过滤选项
53+
const matchFilterOps = filterOps.length === 0 || !!filterOps.find((op: string) => {
54+
switch (op) {
55+
case FilterType.CONDITION:
56+
return node.hasCondition();
57+
case FilterType.LOOP:
58+
return node.hasLoop();
59+
case FilterType.LOCKED:
60+
return treeNode.locked;
61+
case FilterType.HIDDEN:
62+
return treeNode.hidden;
63+
default:
64+
return false;
65+
}
66+
});
67+
68+
// 命中节点名
69+
const matchKeywords = typeof treeNode.titleLabel === 'string' && treeNode.titleLabel.indexOf(keywords) > -1;
70+
71+
// 同时命中才展示(根结点永远命中)
72+
const matchSelf = treeNode.isRoot() || (matchFilterOps && matchKeywords);
73+
74+
// 命中子节点
75+
const matchChild = !!(treeNode.children || []).concat(treeNode.slots || [])
76+
.map((childNode: TreeNode) => {
77+
return matchTreeNode(childNode, keywords, filterOps);
78+
}).find(Boolean);
79+
80+
treeNode.setFilterReult({
81+
filterWorking: true,
82+
matchChild,
83+
matchSelf,
84+
keywords,
85+
});
86+
87+
return matchSelf || matchChild;
88+
};
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import React, { Component } from 'react';
2+
import './style.less';
3+
import { IconFilter } from '../icons/filter';
4+
import { Search, Checkbox, Balloon, Divider } from '@alifd/next';
5+
import TreeNode from '../tree-node';
6+
import { Tree } from '../tree';
7+
import { matchTreeNode, FILTER_OPTIONS } from './filter-tree';
8+
9+
interface IState {
10+
keywords: string;
11+
filterOps: string[];
12+
}
13+
14+
interface IProps {
15+
tree: Tree;
16+
}
17+
18+
export default class Filter extends Component<IProps, IState> {
19+
state = {
20+
keywords: '',
21+
filterOps: [],
22+
};
23+
24+
handleSearchChange = (val: string) => {
25+
this.setState({
26+
keywords: val.trim(),
27+
}, this.filterTree);
28+
};
29+
30+
handleOptionChange = (val: string[]) => {
31+
this.setState({
32+
filterOps: val,
33+
}, this.filterTree);
34+
};
35+
36+
handleCheckAll = () => {
37+
const { filterOps } = this.state;
38+
const final = filterOps.length === FILTER_OPTIONS.length
39+
? [] : FILTER_OPTIONS.map((op) => op.value);
40+
41+
this.handleOptionChange(final);
42+
};
43+
44+
filterTree() {
45+
const { tree } = this.props;
46+
const { keywords, filterOps } = this.state;
47+
48+
matchTreeNode(tree.root as TreeNode, keywords, filterOps);
49+
}
50+
51+
render() {
52+
const { keywords, filterOps } = this.state;
53+
const indeterminate = filterOps.length > 0 && filterOps.length < FILTER_OPTIONS.length;
54+
const checkAll = filterOps.length === FILTER_OPTIONS.length;
55+
56+
return (
57+
<div className="lc-outline-filter">
58+
<Search
59+
hasClear
60+
shape="simple"
61+
placeholder="过滤节点"
62+
className="lc-outline-filter-search-input"
63+
value={keywords}
64+
onChange={this.handleSearchChange}
65+
/>
66+
<Balloon
67+
v2
68+
align="br"
69+
closable={false}
70+
triggerType="hover"
71+
trigger={(
72+
<div className="lc-outline-filter-icon">
73+
<IconFilter />
74+
</div>
75+
)}
76+
>
77+
<Checkbox
78+
checked={checkAll}
79+
indeterminate={indeterminate}
80+
onChange={this.handleCheckAll}
81+
>
82+
全选
83+
</Checkbox>
84+
<Divider />
85+
<Checkbox.Group
86+
value={filterOps}
87+
direction="ver"
88+
onChange={this.handleOptionChange}
89+
>
90+
{FILTER_OPTIONS.map((op) => (
91+
<Checkbox id={op.value} value={op.value}>
92+
{op.label}
93+
</Checkbox>
94+
))}
95+
</Checkbox.Group>
96+
</Balloon>
97+
</div>
98+
);
99+
}
100+
}

packages/plugin-outline-pane/src/views/pane.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { OutlineMain } from '../main';
55
import TreeView from './tree';
66
import './style.less';
77
import { IEditor } from '@alilc/lowcode-types';
8+
import Filter from './filter';
89

910
@observer
1011
export class OutlinePane extends Component<{ config: any; editor: IEditor }> {
@@ -27,6 +28,7 @@ export class OutlinePane extends Component<{ config: any; editor: IEditor }> {
2728

2829
return (
2930
<div className="lc-outline-pane">
31+
<Filter tree={tree} />
3032
<div ref={(shell) => this.main.mount(shell)} className="lc-outline-tree-container">
3133
<TreeView key={tree.id} tree={tree} />
3234
</div>

0 commit comments

Comments
 (0)