!79 [inula-dev-tools]<feat>节点树组件合入

Merge pull request !79 from 涂旭辉/master
This commit is contained in:
openInula-robot 2023-11-13 12:20:47 +00:00 committed by Gitee
commit 9f8bd3245f
No known key found for this signature in database
GPG Key ID: 173E9B9CA92EEF8F
6 changed files with 534 additions and 2 deletions

View File

@ -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;
}

View File

@ -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}>&nbsp;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);

View File

@ -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;

View File

@ -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';

View File

@ -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);
}
}

View File

@ -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"
]
}