[inula-dev-tools]<feat> Panel 组件合入
This commit is contained in:
parent
b5e1a137d4
commit
587f61eec3
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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 '../components/assets.less';
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
font-size: @common-font-size;
|
||||
}
|
||||
|
||||
button {
|
||||
border: none;
|
||||
background: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.left {
|
||||
flex: 0 0 var(--horizontal-percentage);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.left-top {
|
||||
border-bottom: @divider-style;
|
||||
flex: 0 0 @top-height;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-right: 0.4rem;
|
||||
|
||||
.select {
|
||||
padding: 0 0.5rem 0 0.8rem;
|
||||
flex: 0 0;
|
||||
|
||||
.Picking {
|
||||
color: #0088fa;
|
||||
}
|
||||
|
||||
.StopPicking {
|
||||
color: #5f6673;
|
||||
}
|
||||
|
||||
.StopPicking :hover {
|
||||
color: #23272f;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
flex: 0 0 1px;
|
||||
margin: 0 0.25rem 0 0.25rem;
|
||||
border-left: @divider-style;
|
||||
height: calc(100% - 1rem);
|
||||
}
|
||||
|
||||
.search {
|
||||
display: flex;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.searchResult {
|
||||
flex: 0 0;
|
||||
padding: 0 0.4rem;
|
||||
}
|
||||
|
||||
.searchAction {
|
||||
flex: 0 0 1rem;
|
||||
height: 1rem;
|
||||
color: @arrow-color;
|
||||
&:hover {
|
||||
color: @hover-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.left-bottom {
|
||||
flex: 1;
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.resizeBar {
|
||||
flex: 0 0 0;
|
||||
position: relative;
|
||||
resize: horizontal;
|
||||
.resizeLine {
|
||||
position: absolute;
|
||||
left: -2px;
|
||||
width: 5px;
|
||||
height: 100%;
|
||||
cursor: ew-resize;
|
||||
}
|
||||
}
|
||||
|
||||
.right {
|
||||
flex: 3;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
border-left: @divider-style;
|
||||
}
|
||||
|
||||
input {
|
||||
outline: none;
|
||||
border-width: 0;
|
||||
padding: 0;
|
||||
}
|
|
@ -0,0 +1,448 @@
|
|||
/*
|
||||
* 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,
|
||||
useRef,
|
||||
memo,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useReducer,
|
||||
} from 'openinula';
|
||||
import VTree, { IData } from '../components/VTree';
|
||||
import Search from '../components/Search';
|
||||
import ComponentInfo from '../components/ComponentInfo';
|
||||
import styles from './Panel.less';
|
||||
import Select from '../svgs/Select';
|
||||
import { FilterTree } from '../hooks/FilterTree';
|
||||
import Close from '../svgs/Close';
|
||||
import Arrow from '../svgs/Arrow';
|
||||
import {
|
||||
AllVNodeTreeInfos,
|
||||
RequestComponentAttrs,
|
||||
ComponentAttrs,
|
||||
PickElement,
|
||||
StopPickElement,
|
||||
} from '../utils/constants';
|
||||
import {
|
||||
addBackgroundMessageListener,
|
||||
initBackgroundConnection,
|
||||
postMessageToBackground,
|
||||
removeBackgroundMessageListener,
|
||||
} from '../panelConnection';
|
||||
import { IAttr } from '../parser/parseAttr';
|
||||
import { NameObj } from '../parser/parseVNode';
|
||||
import { createLogger } from '../utils/logUtil';
|
||||
import type { Source } from '../../../inula/src/renderer/Types';
|
||||
import ViewSourceContext from '../utils/ViewSource';
|
||||
import PickElementContext from '../utils/PickElement';
|
||||
import Discover from '../svgs/Discover';
|
||||
|
||||
type ResizeActionType = 'START_RESIZE' | 'SET_HORIZONTAL_PERCENTAGE';
|
||||
|
||||
type ResizeAction = {
|
||||
type: ResizeActionType;
|
||||
payload: any;
|
||||
};
|
||||
|
||||
type ResizeState = {
|
||||
horizontalPercentage: number;
|
||||
isResizing: boolean;
|
||||
};
|
||||
|
||||
const logger = createLogger('panelApp');
|
||||
let maxDeep = 0;
|
||||
const parseVNodeData = (rawData, idToTreeNodeMap, nextIdToTreeNodeMap) => {
|
||||
const indentationMap: {
|
||||
[id: string]: number;
|
||||
} = {};
|
||||
const data: IData[] = [];
|
||||
let i = 0;
|
||||
while (i < rawData.length) {
|
||||
const id = rawData[i] as number;
|
||||
i++;
|
||||
const name = rawData[i] as NameObj;
|
||||
i++;
|
||||
const parentId = rawData[i] as string;
|
||||
i++;
|
||||
const userKey = rawData[i] as string;
|
||||
i++;
|
||||
const indentation = parentId === '' ? 0 : indentationMap[parentId] + 1;
|
||||
maxDeep = maxDeep >= indentation ? maxDeep : indentation;
|
||||
indentationMap[id] = indentation;
|
||||
const lastItem = idToTreeNodeMap[id];
|
||||
if (lastItem) {
|
||||
// 由于 diff 算法限制,一个 vNode 的 name,userKey,indentation 属性不会发生变化
|
||||
// 但是在跳转到新页面时, id 值重置,此时原有 id 对应的节点都发生了变化,需要更新
|
||||
// 为了让架构尽可能简单,不区分是否是页面挑战,所以每次都需要重新赋值
|
||||
nextIdToTreeNodeMap[id] = lastItem;
|
||||
lastItem.name = name;
|
||||
lastItem.indentation = indentation;
|
||||
lastItem.userKey = userKey;
|
||||
data.push(lastItem);
|
||||
} else {
|
||||
const item = {
|
||||
id,
|
||||
name,
|
||||
indentation,
|
||||
userKey,
|
||||
};
|
||||
nextIdToTreeNodeMap[id] = item;
|
||||
data.push(item);
|
||||
}
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
const getParents = (item: IData | null, parsedVNodeData: IData[]) => {
|
||||
const parents: IData[] = [];
|
||||
if (item) {
|
||||
const index = parsedVNodeData.indexOf(item);
|
||||
let indentation = item.indentation;
|
||||
for (let i = index; i >= 0; i--) {
|
||||
const last = parsedVNodeData[i];
|
||||
const lastIndentation = last.indentation;
|
||||
if (lastIndentation < indentation) {
|
||||
parents.push(last);
|
||||
indentation = lastIndentation;
|
||||
}
|
||||
}
|
||||
}
|
||||
return parents;
|
||||
};
|
||||
|
||||
interface IIdToNodeMap {
|
||||
[id: number]: IData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 dev tools 页面左树占比
|
||||
*
|
||||
* @param {null | HTMLElement} resizeElement 要改变宽度的页面元素
|
||||
* @param {number} percentage 宽度占比
|
||||
*/
|
||||
const setResizePCTForElement = (
|
||||
resizeElement: null | HTMLElement,
|
||||
percentage: number
|
||||
): void => {
|
||||
if (resizeElement !== null) {
|
||||
resizeElement.style.setProperty(
|
||||
'--horizontal-percentage',
|
||||
`${percentage}`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
function resizeReducer(state: ResizeState, action: ResizeAction): ResizeState {
|
||||
switch (action.type) {
|
||||
case "START_RESIZE":
|
||||
return {
|
||||
...state,
|
||||
isResizing: action.payload,
|
||||
};
|
||||
case "SET_HORIZONTAL_PERCENTAGE":
|
||||
return {
|
||||
...state,
|
||||
horizontalPercentage: action.payload,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
function initResizeState(): ResizeState {
|
||||
const horizontalPercentage = 0.62;
|
||||
|
||||
return {
|
||||
horizontalPercentage,
|
||||
isResizing: false,
|
||||
};
|
||||
}
|
||||
|
||||
function Panel({ viewSource, inspectVNode }) {
|
||||
const [parsedVNodeData, setParsedVNodeData] = useState([]);
|
||||
const [componentAttrs, setComponentAttrs] = useState<{
|
||||
parsedProps?: IAttr[];
|
||||
parsedState?: IAttr[];
|
||||
parsedHooks?: IAttr[];
|
||||
}>({});
|
||||
const [selectComp, setSelectComp] = useState<IData>(null);
|
||||
const [isPicking, setPicking] = useState(false);
|
||||
const [source, setSource] = useState<Source>(null);
|
||||
const idToTreeNodeMapref = useRef<IIdToNodeMap>({});
|
||||
const [state, dispatch] = useReducer(
|
||||
resizeReducer,
|
||||
null,
|
||||
initResizeState
|
||||
);
|
||||
const pageRef = useRef<null | HTMLElement>(null);
|
||||
const treeRef = useRef<null | HTMLElement>(null);
|
||||
|
||||
const { horizontalPercentage } = state;
|
||||
const {
|
||||
filterValue,
|
||||
onChangeSearchValue: setFilterValue,
|
||||
onClear,
|
||||
currentItem,
|
||||
matchItems,
|
||||
onSelectNext,
|
||||
onSelectLast,
|
||||
setShowItems,
|
||||
collapsedNodes,
|
||||
setCollapsedNodes,
|
||||
} = FilterTree({ data: parsedVNodeData });
|
||||
|
||||
useEffect(() => {
|
||||
if (isDev) {
|
||||
// const nextIdToTreeNodeMap: IIdToNodeMap = {};
|
||||
} else {
|
||||
const handleBackgroundMessage = message => {
|
||||
const { payload } = message;
|
||||
// 对象数据只是记录了引用,内容可能在后续被修改,打印字符串可以获取当前真正内容,不被后续修改影响
|
||||
logger.info(JSON.stringify(payload));
|
||||
if (payload) {
|
||||
const {type, data} = payload;
|
||||
if (type === AllVNodeTreeInfos) {
|
||||
const idToTreeNodeMap = idToTreeNodeMapref.current;
|
||||
const nextIdToTreeNodeMap: IIdToNodeMap = {};
|
||||
const allTreeData = data.reduce((pre, current) => {
|
||||
const parsedTreeData = parseVNodeData(
|
||||
current,
|
||||
idToTreeNodeMap,
|
||||
nextIdToTreeNodeMap
|
||||
);
|
||||
return pre.concat(parsedTreeData);
|
||||
}, []);
|
||||
idToTreeNodeMapref.current = nextIdToTreeNodeMap;
|
||||
setParsedVNodeData(allTreeData);
|
||||
if (selectComp) {
|
||||
postMessageToBackground(RequestComponentAttrs, selectComp.id);
|
||||
}
|
||||
} else if (type === ComponentAttrs) {
|
||||
const { parsedProps, parsedState, parsedHooks, src } = data;
|
||||
setComponentAttrs({
|
||||
parsedProps,
|
||||
parsedState,
|
||||
parsedHooks,
|
||||
});
|
||||
setSource(src);
|
||||
} else if (type === StopPickElement) {
|
||||
setPicking(false);
|
||||
} else if (type === PickElement) {
|
||||
const target = Object.values(idToTreeNodeMapref.current).find(({ id }) => id == data);
|
||||
setSelectComp(target);
|
||||
}
|
||||
}
|
||||
};
|
||||
// 在页面渲染后初始化连接
|
||||
initBackgroundConnection('panel');
|
||||
// 监听 background 消息
|
||||
addBackgroundMessageListener(handleBackgroundMessage);
|
||||
return () => {
|
||||
removeBackgroundMessageListener(handleBackgroundMessage);
|
||||
};
|
||||
}
|
||||
}, [selectComp]);
|
||||
|
||||
useEffect(() => {
|
||||
const treeElement = treeRef.current;
|
||||
|
||||
setResizePCTForElement(treeElement, horizontalPercentage * 100);
|
||||
}, []);
|
||||
|
||||
const handleSearchChange = (str: string) => {
|
||||
setFilterValue(str);
|
||||
};
|
||||
|
||||
const handleSelectComp = (item: IData) => {
|
||||
setSelectComp(item);
|
||||
if (isDev) {
|
||||
// setComponentAttrs({});
|
||||
} else {
|
||||
postMessageToBackground(RequestComponentAttrs, item.id);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClickParent = useCallback((item: IData) => {
|
||||
setSelectComp(item);
|
||||
}, []);
|
||||
|
||||
const onRendered = info => {
|
||||
setShowItems(info.visibleItems);
|
||||
};
|
||||
|
||||
const parents = useMemo(
|
||||
() => getParents(selectComp, parsedVNodeData),
|
||||
[selectComp, parsedVNodeData]
|
||||
);
|
||||
|
||||
const viewSourceFunction = useMemo(
|
||||
() => ({
|
||||
viewSource: viewSource || null,
|
||||
}),
|
||||
[viewSource]
|
||||
);
|
||||
|
||||
// 选择页面元素对应到 dev tools
|
||||
const pickElementFunction = useMemo(
|
||||
() => ({
|
||||
inspectVNode: inspectVNode || null,
|
||||
}),
|
||||
[inspectVNode]
|
||||
);
|
||||
|
||||
// 选择页面元素图标样式
|
||||
let pickClassName;
|
||||
if (isPicking) {
|
||||
pickClassName = styles.Picking;
|
||||
} else {
|
||||
pickClassName = styles.StopPicking;
|
||||
}
|
||||
|
||||
const MINIMUM_SIZE = 50;
|
||||
const { isResizing } = state;
|
||||
const doResize = () => dispatch({ type: 'START_RESIZE', payload: true });
|
||||
let onResize;
|
||||
let stopResize;
|
||||
if (isResizing) {
|
||||
stopResize = () => dispatch({ type: 'START_RESIZE', payload: false });
|
||||
|
||||
onResize = event => {
|
||||
// 设置横向 resize 百分比区域(左树部分)
|
||||
const treeElement = treeRef.current;
|
||||
// 整个页面(左树部分加节点详情部分),要拿到页面宽度,防止 resize 时移出页面
|
||||
const pageElement = pageRef.current;
|
||||
|
||||
if (isResizing || pageElement === null || treeElement === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 左移时防止左树移出页面
|
||||
event.preventDefault();
|
||||
|
||||
const { width, left } = pageElement.getBoundingClientRect();
|
||||
|
||||
const mouseAbscissa = event.clientX - left;
|
||||
|
||||
const pageSizeMin = MINIMUM_SIZE;
|
||||
const pageSizeMax = width-MINIMUM_SIZE;
|
||||
|
||||
const isMouseInPage = mouseAbscissa > pageSizeMin && mouseAbscissa < pageSizeMax;
|
||||
|
||||
if (isMouseInPage) {
|
||||
const resizedElementWidth = width;
|
||||
const actionType = 'SET_HORIZONTAL_PERCENTAGE';
|
||||
const percentage = (mouseAbscissa / resizedElementWidth) * 100;
|
||||
|
||||
setResizePCTForElement(treeElement, percentage);
|
||||
|
||||
dispatch({
|
||||
type: actionType,
|
||||
payload: mouseAbscissa / resizedElementWidth,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<ViewSourceContext.Provider value={{ viewSourceFunction }}>
|
||||
<PickElementContext.Provider value={{ pickElementFunction }}>
|
||||
<div
|
||||
ref={pageRef}
|
||||
onMouseMove={onResize}
|
||||
onMouseLeave={stopResize}
|
||||
onMouseUp={stopResize}
|
||||
className={styles.app}
|
||||
>
|
||||
<div ref={treeRef} className={styles.left}>
|
||||
<div className={styles.leftTop}>
|
||||
<div className={styles.select}>
|
||||
<button className={`${pickClassName}`}>
|
||||
<span
|
||||
className={styles.eye}
|
||||
title={'Pick an element from the page'}
|
||||
onClick={() => {
|
||||
postMessageToBackground(!isPicking ? PickElement : StopPickElement);
|
||||
setPicking(!isPicking);
|
||||
}}
|
||||
>
|
||||
<Select />
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.divider} />
|
||||
<div className={styles.search}>
|
||||
<Discover />
|
||||
<Search onKeyUp={onSelectNext} onChange={handleSearchChange} value={filterValue} />
|
||||
</div>
|
||||
{filterValue !== '' && (
|
||||
<>
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<VTree
|
||||
data={parsedVNodeData}
|
||||
maxDeep={maxDeep}
|
||||
highlightValue={filterValue}
|
||||
onRendered={onRendered}
|
||||
collapsedNodes={collapsedNodes}
|
||||
onCollapseNode={setCollapsedNodes}
|
||||
scrollToItem={currentItem}
|
||||
selectItem={selectComp}
|
||||
onSelectItem={handleSelectComp}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div onMouseDown={doResize} className={styles.resizeLine} />
|
||||
</div>
|
||||
<div>
|
||||
<ComponentInfo
|
||||
name={selectComp ? selectComp.name.itemName : null}
|
||||
attrs={selectComp ? componentAttrs : {}}
|
||||
parents={parents}
|
||||
id={selectComp ? selectComp.id : null}
|
||||
source={selectComp ? source : null}
|
||||
onClickParent={handleClickParent}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PickElementContext.Provider>
|
||||
</ViewSourceContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(Panel);
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* 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 Panel from './Panel';
|
||||
|
||||
// 这里导出 Panel 为了加载 Panel.less
|
||||
export default Panel;
|
|
@ -0,0 +1,35 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf8">
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src *; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline' 'unsafe-eval' ">
|
||||
<style>
|
||||
html {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="inula.development.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="panel.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
Loading…
Reference in New Issue