!79 [inula-dev-tools]<feat>节点树组件合入
Merge pull request !79 from 涂旭辉/master
This commit is contained in:
commit
9f8bd3245f
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
@import 'assets.less';
|
||||
|
||||
.treeContainer {
|
||||
height: 100%;
|
||||
|
||||
.treeItem {
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
line-height: 1.125rem;
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
&:hover {
|
||||
background-color: @select-color;
|
||||
}
|
||||
|
||||
.treeIcon {
|
||||
color: @arrow-color;
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
padding-left: 0.5rem;
|
||||
}
|
||||
|
||||
.componentName {
|
||||
white-space: nowrap;
|
||||
color: @component-name-color;
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.componentKeyName {
|
||||
color: @component-key-color;
|
||||
}
|
||||
|
||||
.componentKeyValue {
|
||||
color: @component-key-value-color;
|
||||
max-width: 100px;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: inline-flex;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.selectedItemChild {
|
||||
background-color: @select-item-child-color;
|
||||
}
|
||||
|
||||
.select {
|
||||
background-color: @select-color;
|
||||
}
|
||||
}
|
||||
|
||||
.Badge {
|
||||
display: inline-block;
|
||||
background-color: rgba(0, 0, 0, 0.1);
|
||||
color: #000000;
|
||||
padding: 0 0.25rem;
|
||||
line-height: normal;
|
||||
border-radius: 0.125rem;
|
||||
margin-left: 0.25rem;
|
||||
font-family: @attr-name-font-family;
|
||||
font-size: 9px;
|
||||
height: 1rem;
|
||||
}
|
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, memo } from 'openinula';
|
||||
import styles from './VTree.less';
|
||||
import Triangle from '../svgs/Triangle';
|
||||
import { createRegExp } from '../utils/regExpUtil';
|
||||
import { SizeObserver } from './SizeObserver';
|
||||
import { RenderInfoType, VList } from './VList';
|
||||
import { postMessageToBackground } from '../panelConnection';
|
||||
import { Highlight, RemoveHighlight } from '../utils/constants';
|
||||
import { NameObj } from '../parser/parseVNode';
|
||||
|
||||
export interface IData {
|
||||
id: number;
|
||||
name: NameObj;
|
||||
indentation: number;
|
||||
userKey: string;
|
||||
}
|
||||
|
||||
interface IItem {
|
||||
indentationLength: number;
|
||||
hasChild: boolean;
|
||||
onCollapse: (data: IData) => void;
|
||||
onClick: (id: IData) => void;
|
||||
onMouseEnter: (id: IData) => void;
|
||||
onMouseLeave: (id: IData) => void;
|
||||
isCollapsed: boolean;
|
||||
isSelect: boolean;
|
||||
highlightValue: string;
|
||||
data: IData;
|
||||
isSelectedItemChild: boolean;
|
||||
}
|
||||
|
||||
function Item(props: IItem) {
|
||||
const {
|
||||
hasChild,
|
||||
onCollapse,
|
||||
isCollapsed,
|
||||
data,
|
||||
onClick,
|
||||
indentationLength,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
isSelect,
|
||||
highlightValue = '',
|
||||
isSelectedItemChild,
|
||||
} = props;
|
||||
|
||||
const { name, userKey, indentation } = data;
|
||||
|
||||
const isShowKey = userKey !== '';
|
||||
const showIcon = hasChild ? <Triangle director={isCollapsed ? 'right' : 'down'} /> : '';
|
||||
const handleClickCollapse = () => {
|
||||
onCollapse(data);
|
||||
};
|
||||
const handleClick = () => {
|
||||
onClick(data);
|
||||
};
|
||||
const handleMouseEnter = () => {
|
||||
onMouseEnter(data);
|
||||
};
|
||||
const handleMouseLeave = () => {
|
||||
onMouseLeave(data);
|
||||
};
|
||||
|
||||
const itemAttr: Record<string, any> = {
|
||||
className: isSelectedItemChild ? styles.selectedItemChild : styles.treeItem,
|
||||
onClick: handleClick,
|
||||
onMouseEnter: handleMouseEnter,
|
||||
onMouseLeave: handleMouseLeave,
|
||||
};
|
||||
|
||||
if (isSelect) {
|
||||
itemAttr.tabIndex = 0;
|
||||
itemAttr.className = styles.treeItem + ' ' + styles.select;
|
||||
}
|
||||
|
||||
if (isSelectedItemChild) {
|
||||
itemAttr.className = styles.treeItem + ' ' + styles.selectedItemChild;
|
||||
}
|
||||
|
||||
const pushBadge = (showName: Array<any>, badgeName: string) => {
|
||||
showName.push(' ');
|
||||
showName.push(<div className={`${styles.Badge}`}>{badgeName}</div>);
|
||||
};
|
||||
|
||||
const pushItemName = (showName: Array<any>, cutName: string, char: string) => {
|
||||
const index = cutName.search(char);
|
||||
if (index > -1) {
|
||||
const notHighlightStr = cutName.slice(0, index);
|
||||
showName.push(`<${notHighlightStr}`);
|
||||
showName.push(<mark>{char}</mark>);
|
||||
showName.push(`${cutName.slice(index + char.length)}>`);
|
||||
} else {
|
||||
showName.push(`<${cutName}`);
|
||||
}
|
||||
};
|
||||
|
||||
const pushBadgeName = (showName: Array<any>, cutName: string, char: string) => {
|
||||
const index = cutName.search(char);
|
||||
if (index > -1) {
|
||||
const notHighlightStr = cutName.slice(0, index);
|
||||
showName.push(
|
||||
<div className={`${styles.Badge}`}>
|
||||
{notHighlightStr}
|
||||
{<mark>{char}</mark>}
|
||||
{cutName.slice(index + char.length)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
pushBadge(showName, cutName);
|
||||
}
|
||||
};
|
||||
|
||||
const reg = createRegExp(highlightValue);
|
||||
const heightCharacters = name.itemName.match(reg);
|
||||
const showName = [];
|
||||
|
||||
const addShowName = (showName: Array<string>, name: NameObj) => {
|
||||
showName.push(`<${name.itemName}>`);
|
||||
name.badge.forEach(key => {
|
||||
showName.push(<div className={`${styles.Badge}`}>{key}</div>);
|
||||
});
|
||||
};
|
||||
|
||||
if (heightCharacters) {
|
||||
// 高亮第一次匹配即可
|
||||
const char = heightCharacters[0];
|
||||
pushItemName(showName, name.itemName, char);
|
||||
if (name.badge.length > 0) {
|
||||
name.badge.forEach(key => {
|
||||
pushBadgeName(showName, key, char);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
addShowName(showName, name);
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...itemAttr}>
|
||||
<div
|
||||
style={{marginLeft: indentation * indentationLength}}
|
||||
className={styles.treeIcon}
|
||||
onclick={handleClickCollapse}
|
||||
>
|
||||
{showIcon}
|
||||
</div>
|
||||
<span className={styles.componentName}>{showName}</span>
|
||||
{isShowKey && (
|
||||
<>
|
||||
<span className={styles.componentKeyName}> key</span>
|
||||
{'="'}
|
||||
<span className={styles.componentKeyValue}>{userKey}</span>
|
||||
{'"'}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VTree(props: {
|
||||
data: IData[];
|
||||
maxDeep: number;
|
||||
highlightValue: string;
|
||||
scrollToItem: IData;
|
||||
onRendered: (renderInfo: RenderInfoType<IData>) => void;
|
||||
collapsedNodes?: IData[];
|
||||
onCollapseNode?: (item: IData[]) => void;
|
||||
selectItem: IData;
|
||||
onSelectItem: (item: IData) => void;
|
||||
}) {
|
||||
const {
|
||||
data,
|
||||
maxDeep,
|
||||
highlightValue,
|
||||
scrollToItem,
|
||||
onRendered,
|
||||
onCollapseNode,
|
||||
onSelectItem
|
||||
} = props;
|
||||
const [collapseNode, setCollapseNode] = useState(props.collapsedNodes || []);
|
||||
const [selectItem, setSelectItem] = useState(props.selectItem);
|
||||
const [childItems, setChildItems] = useState<Array<IData>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectItem(scrollToItem);
|
||||
}, [scrollToItem]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.selectItem !== selectItem) {
|
||||
setSelectItem(props.selectItem);
|
||||
}
|
||||
}, [props.selectItem]);
|
||||
|
||||
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.splice(index, 1);
|
||||
}
|
||||
|
||||
setCollapseNode(nodes);
|
||||
|
||||
if (onCollapseNode) {
|
||||
onCollapseNode(nodes);
|
||||
}
|
||||
};
|
||||
|
||||
const getChildItem = (item: IData): Array<IData> => {
|
||||
const index = data.indexOf(item);
|
||||
const childList: Array<IData> = [];
|
||||
|
||||
for (let i = index + 1; i < data.length; i++) {
|
||||
if (data[i].indentation > item.indentation) {
|
||||
childList.push(data[i]);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return childList;
|
||||
};
|
||||
|
||||
const handleClickItem = useCallback(
|
||||
(item: IData) => {
|
||||
const childItem = getChildItem(item);
|
||||
setSelectItem(item);
|
||||
setChildItems(childItem);
|
||||
if (onSelectItem) {
|
||||
onSelectItem(item);
|
||||
}
|
||||
},
|
||||
[onSelectItem]
|
||||
);
|
||||
|
||||
const handleMouseEnterItem = useCallback(
|
||||
item => {
|
||||
postMessageToBackground(Highlight, item);
|
||||
},
|
||||
null
|
||||
);
|
||||
|
||||
const handleMouseLeaveItem = () => {
|
||||
postMessageToBackground(RemoveHighlight);
|
||||
};
|
||||
|
||||
let currentCollapseIndentation: null | number = null;
|
||||
// 过滤掉折叠的 item,不展示在 VList 中
|
||||
const filter = (item: IData) => {
|
||||
if (currentCollapseIndentation !== null) {
|
||||
// 缩进更大,不显示
|
||||
if (item.indentation > currentCollapseIndentation) {
|
||||
return false;
|
||||
} else {
|
||||
// 缩进小,说明完成了收起节点的子节点处理
|
||||
currentCollapseIndentation = null;
|
||||
}
|
||||
}
|
||||
const isCollapsed = collapseNode.includes(item);
|
||||
if (isCollapsed) {
|
||||
// 该节点需要收起子节点
|
||||
currentCollapseIndentation = item.indentation;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const showList = data.filter(filter);
|
||||
|
||||
return (
|
||||
<SizeObserver className={styles.treeContainer}>
|
||||
{(width: number, height: number) => {
|
||||
return (
|
||||
<VList
|
||||
data={showList}
|
||||
maxDeep={maxDeep}
|
||||
width={width}
|
||||
height={height}
|
||||
itemHeight={17.5}
|
||||
scrollToItem={selectItem}
|
||||
onRendered={onRendered}
|
||||
>
|
||||
{(item: IData, indentationLength: number) => {
|
||||
const isCollapsed = collapseNode.includes(item);
|
||||
const index = showList.indexOf(item);
|
||||
// 如果收起,一定有 child
|
||||
// 不收起场景,如果存在下一个节点,并且节点缩进比自己大,说明下个节点是子节点,节点本身需要显示展开收起图标
|
||||
const hasChild = isCollapsed || showList[index + 1]?.indentation > item.indentation;
|
||||
return (
|
||||
<Item
|
||||
indentationLength={indentationLength}
|
||||
hasChild={hasChild}
|
||||
onCollapse={changeCollapseNode}
|
||||
onClick={handleClickItem}
|
||||
onMouseEnter={handleMouseEnterItem}
|
||||
onMouseLeave={handleMouseLeaveItem}
|
||||
isCollapsed={collapseNode.includes(item)}
|
||||
isSelect={selectItem === item}
|
||||
highlightValue={highlightValue}
|
||||
data={item}
|
||||
isSelectedItemChild={childItems.includes(item)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</VList>
|
||||
);
|
||||
}}
|
||||
</SizeObserver>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(VTree);
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
@arrow-color: rgb(95, 99, 104);
|
||||
@divider-color: rgb(202, 205, 209);
|
||||
@attr-name-color: rgb(200, 0, 0);
|
||||
@component-name-color: rgb(136, 18, 128);
|
||||
@component-key-color: rgb(153, 69, 0);
|
||||
@component-key-value-color: rgb(26, 26, 166);
|
||||
@component-attr-color: rgb(200, 0, 0);
|
||||
@select-color: rgb(144 199 248 / 60%);
|
||||
@select-item-child-color: rgb(141 199 248 / 40%);
|
||||
@hover-color: black;
|
||||
|
||||
@top-height: 2.625rem;
|
||||
@divider-width: 0.2px;
|
||||
@common-font-size: 12px;
|
||||
|
||||
@divider-style: @divider-color solid @divider-width;
|
||||
@attr-name-font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
export * from './panelConnection';
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { packagePayload } from '../utils/transferUtils';
|
||||
import { DevToolPanel, InitDevToolPageConnection } from '../utils/constants';
|
||||
|
||||
let connection;
|
||||
const callbacks = [];
|
||||
|
||||
export function addBackgroundMessageListener(func: (message) => void) {
|
||||
callbacks.push(func);
|
||||
}
|
||||
|
||||
export function removeBackgroundMessageListener(func: (message) => void) {
|
||||
const index = callbacks.indexOf(func);
|
||||
if (index !== -1) {
|
||||
callbacks.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export function initBackgroundConnection(type) {
|
||||
if (!isDev) {
|
||||
try {
|
||||
connection = chrome.runtime.connect({ name: type });
|
||||
const notice = message => {
|
||||
callbacks.forEach(func => {
|
||||
func(message);
|
||||
});
|
||||
};
|
||||
// 监听 background 消息
|
||||
connection.onMessage.addListener(notice);
|
||||
// 页面打开后发送初始化请求
|
||||
postMessageToBackground(InitDevToolPageConnection);
|
||||
} catch (e) {
|
||||
console.error('create connection failer');
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let reconnectionTimes = 0;
|
||||
export function postMessageToBackground(
|
||||
type: string,
|
||||
data?: any,
|
||||
inulaX?: boolean
|
||||
) {
|
||||
try {
|
||||
const payload = data
|
||||
? { type, tabId: chrome.devtools.inspectedWindow.tabId, data }
|
||||
: { type, tabId: chrome.devtools.inspectedWindow.tabId };
|
||||
connection.postMessage(packagePayload(payload, DevToolPanel));
|
||||
} catch (e) {
|
||||
// 可能出现 port 关闭的场景,需要重新建立连接,增加可靠性
|
||||
if (reconnectionTimes === 20) {
|
||||
reconnectionTimes = 0;
|
||||
console.error('reconnect failed');
|
||||
return;
|
||||
}
|
||||
console.error(e);
|
||||
reconnectionTimes++;
|
||||
// 重新连接
|
||||
initBackgroundConnection(inulaX ? 'panelX' : 'panel');
|
||||
// 初始化成功后才会重新发送消息
|
||||
postMessageToBackground(type, data);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@
|
|||
"compilerOptions": {
|
||||
"outDir": "./dist",
|
||||
"allowJs": true,
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"module": "es6",
|
||||
"moduleResolution": "node",
|
||||
|
@ -16,6 +15,6 @@
|
|||
}
|
||||
},
|
||||
"include": [
|
||||
"./src/**/*.ts", "./src/index.d.ts", "./src/**/*.tsx", "./externals.d.ts"
|
||||
"./src/**/*.ts", "./src/index.d.ts", "./src/**/*.tsx", "./externals.d.ts", "./global.d.ts"
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue