diff --git a/packages/inula-dev-tools/src/components/VTree.less b/packages/inula-dev-tools/src/components/VTree.less new file mode 100644 index 00000000..54d23bb7 --- /dev/null +++ b/packages/inula-dev-tools/src/components/VTree.less @@ -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; +} diff --git a/packages/inula-dev-tools/src/components/VTree.tsx b/packages/inula-dev-tools/src/components/VTree.tsx new file mode 100644 index 00000000..77918715 --- /dev/null +++ b/packages/inula-dev-tools/src/components/VTree.tsx @@ -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 ? : ''; + const handleClickCollapse = () => { + onCollapse(data); + }; + const handleClick = () => { + onClick(data); + }; + const handleMouseEnter = () => { + onMouseEnter(data); + }; + const handleMouseLeave = () => { + onMouseLeave(data); + }; + + const itemAttr: Record = { + 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, badgeName: string) => { + showName.push(' '); + showName.push(
{badgeName}
); + }; + + const pushItemName = (showName: Array, cutName: string, char: string) => { + const index = cutName.search(char); + if (index > -1) { + const notHighlightStr = cutName.slice(0, index); + showName.push(`<${notHighlightStr}`); + showName.push({char}); + showName.push(`${cutName.slice(index + char.length)}>`); + } else { + showName.push(`<${cutName}`); + } + }; + + const pushBadgeName = (showName: Array, cutName: string, char: string) => { + const index = cutName.search(char); + if (index > -1) { + const notHighlightStr = cutName.slice(0, index); + showName.push( +
+ {notHighlightStr} + {{char}} + {cutName.slice(index + char.length)} +
+ ); + } else { + pushBadge(showName, cutName); + } + }; + + const reg = createRegExp(highlightValue); + const heightCharacters = name.itemName.match(reg); + const showName = []; + + const addShowName = (showName: Array, name: NameObj) => { + showName.push(`<${name.itemName}>`); + name.badge.forEach(key => { + showName.push(
{key}
); + }); + }; + + 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 ( +
+
+ {showIcon} +
+ {showName} + {isShowKey && ( + <> +  key + {'="'} + {userKey} + {'"'} + + )} +
+ ); +} + +function VTree(props: { + data: IData[]; + maxDeep: number; + highlightValue: string; + scrollToItem: IData; + onRendered: (renderInfo: RenderInfoType) => 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>([]); + + 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 => { + const index = data.indexOf(item); + const childList: Array = []; + + 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 ( + + {(width: number, height: number) => { + return ( + + {(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 ( + + ); + }} + + ); + }} + + ); +} + +export default memo(VTree); diff --git a/packages/inula-dev-tools/src/components/assets.less b/packages/inula-dev-tools/src/components/assets.less new file mode 100644 index 00000000..918c1291 --- /dev/null +++ b/packages/inula-dev-tools/src/components/assets.less @@ -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; diff --git a/packages/inula-dev-tools/src/panelConnection/index.ts b/packages/inula-dev-tools/src/panelConnection/index.ts new file mode 100644 index 00000000..96234936 --- /dev/null +++ b/packages/inula-dev-tools/src/panelConnection/index.ts @@ -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'; diff --git a/packages/inula-dev-tools/src/panelConnection/panelConnection.ts b/packages/inula-dev-tools/src/panelConnection/panelConnection.ts new file mode 100644 index 00000000..56941206 --- /dev/null +++ b/packages/inula-dev-tools/src/panelConnection/panelConnection.ts @@ -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); + } +} diff --git a/packages/inula-dev-tools/tsconfig.json b/packages/inula-dev-tools/tsconfig.json index d0c62d99..d07996f8 100644 --- a/packages/inula-dev-tools/tsconfig.json +++ b/packages/inula-dev-tools/tsconfig.json @@ -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" ] }