[inula-dev-tools]<feat> 信息组件合入

This commit is contained in:
13659257719 2023-11-14 16:47:04 +08:00
parent 8c559f644d
commit 6b59405236
2 changed files with 714 additions and 0 deletions

View File

@ -0,0 +1,279 @@
/*
* 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';
.infoContainer {
display: flex;
flex-direction: column;
height: 100%;
.button {
border: none;
padding: 0;
border-radius: 0.25rem;
flex: 0 0 auto;
cursor: pointer;
color: #5f6673;
}
.button :hover {
color: #23272f;
}
.componentInfoHead {
flex: 0 0 @top-height;
display: flex;
align-items: center;
border-bottom: @divider-style;
.name {
flex: 1 1 auto;
padding: 0 1rem 0 1rem;
.text {
display: block;
}
}
.eye {
flex: 0 0 1rem;
cursor: pointer;
display: inline-flex;
align-items: center;
padding: 0.25rem 0.5rem 0.25rem 0.25rem;
}
.debug {
flex: 0 0 1rem;
cursor: pointer;
display: inline-flex;
align-items: center;
padding: 0.25rem 0.5rem 0.25rem 0;
}
.location {
flex: 0 0 1rem;
cursor: pointer;
display: inline-flex;
align-items: center;
padding: 0.25rem 0.5rem 0.25rem 0;
}
}
.componentInfoMain {
overflow-y: auto;
> :last-child {
border-bottom: unset;
}
> div {
border-bottom: @divider-style;
}
.attrContainer {
flex: 0 0;
.attrHead {
display: flex;
flex-direction: row;
align-items: center;
padding: 0.5rem 0.5rem 0 0.5rem;
.attrType {
flex: 1 1 0;
}
.attrCopy {
flex: 0 0 1rem;
padding-right: 1rem;
}
}
.attrDetail {
padding-bottom: 0.5rem;
.attrArrow {
color: @arrow-color;
width: 12px;
display: inline-block;
}
.attrName {
margin-top: 1px;
color: @attr-name-color;
font-family: @attr-name-font-family;
}
.colon {
margin-top: 1px;
transform: translateY(-8%);
margin-right: 0.5rem;
}
.info {
display: flex;
&:hover {
.operation {
visibility: visible;
.operationIcon :hover {
border: none;
border-radius: 5px;
background-color: lightskyblue;
}
}
}
}
.attrValue {
width: 26rem;
height: 1rem;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo,
Courier, monospace;
&:focus {
color: unset;
background-color: #f0f0f0;
}
}
.attrValue[data-type='string'] {
color: #009906;
}
.attrValue[data-type='function'] {
color: royalblue;
}
.attrValue[data-type='number'] {
color: #ff5722;
}
.attrValue[data-type='boolean'] {
color: #03a9f4;
}
.operation {
cursor: pointer;
visibility: hidden;
}
.checkBox {
margin: 2px 3px 0 auto;
justify-content: flex-end;
}
}
}
.dropdown.active {
display: unset;
top: var(--content-top);
left: var(--content-left);
position: absolute;
ul {
margin-block-start: 0;
padding-inline-start: 0;
li {
padding: 10px;
border-top: 1px lighten(#333, 2%) solid;
height: auto;
overflow: auto;
opacity: 1;
}
}
}
.dropdown {
display: none;
ul {
display: block;
position: relative;
list-style: none;
}
li {
padding: 0 10px;
background: darken(#333, 2%);
color: darken(#EEE, 40%);
text-align: left;
border: 0;
width: 100%;
height: 0;
overflow: hidden;
cursor: pointer;
opacity: 0;
transition-property: all, background-color;
transition-duration: 0.2s, 0.4s;
&:hover, &.selected {
background-color: darken(#333, 10%);
}
&:active {
background: #03a9f4;
}
&:first-child {
border-radius: 5px 5px 0 0;
}
&:last-child {
border-radius: 0 0 5px 5px;
}
&:before {
margin-top: -2px;
margin-right: 10px;
display: inline-block;
border-radius: 5px;
vertical-align: middle;
width: 16px;
height: 16px;
}
&:nth-child(1) {
&:before {
content: url('../svgs/copy.svg');
}
}
&:nth-child(2) {
&:before {
content: url('../svgs/storage.svg');
}
}
}
}
}
.parentsInfo {
flex: 1 1 0;
.parentName {
padding: 0.5rem 0.5rem 0 0.5rem;
}
.parent {
margin-left: 1.4rem;
display: block;
cursor: pointer;
text-align: left;
color: @component-name-color;
&:hover {
background-color: @select-color;
}
}
}
}

View File

@ -0,0 +1,435 @@
/*
* 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 styles from './ComponentInfo.less';
import Eye from '../svgs/Eye';
import Debug from '../svgs/Debug';
import Location from '../svgs/Location';
import Triangle from '../svgs/Triangle';
import { memo, useContext, useEffect, useState, useRef, useMemo, createRef } from 'openinula';
import { IData } from './VTree';
import { buildAttrModifyData, IAttr } from '../parser/parseAttr';
import { postMessageToBackground } from '../panelConnection';
import { CopyToConsole, InspectDom, LogComponentData, ModifyAttrs, StorageValue } from '../utils/constants';
import type { Source } from '../../../inula/src/renderer/Types';
import ViewSourceContext from '../utils/ViewSource';
import PickElementContext from '../utils/PickElement';
import Operation from '../svgs/Operation';
type IComponentInfo = {
name: string;
attrs: {
parsedProps?: IAttr[];
parsedState?: IAttr[];
parsedHooks?: IAttr[];
};
parents: IData[];
id: number;
source?: Source;
onClickParent: (item: IData) => void;
};
const ComponentAttr = memo(function ComponentAttr({
attrsName,
attrsType,
attrs,
id,
dropdownRef,
}: {
attrsName: string;
attrsType: string;
attrs: IAttr[];
id: number;
dropdownRef: null | HTMLElement;
}) {
const [editableAttrs, setEditableAttrs] = useState(attrs);
const [expandNodes, setExpandNodes] = useState([]);
useEffect(() => {
setEditableAttrs(attrs);
}, [attrs]);
const handleCollapse = (item: IAttr) => {
const nodes = [...expandNodes];
const expandItem = `${item.name}_${editableAttrs.indexOf(item)}`;
const i = nodes.indexOf(expandItem);
if (i === -1) {
nodes.push(expandItem);
} else {
nodes.splice(i, 1);
}
setExpandNodes(nodes);
};
// props 展示的 key: value 中的 value 值
const getShowName = item => {
let retStr;
if (item === undefined) {
retStr = String(item);
} else if (typeof item === 'number') {
retStr = item;
} else if (typeof item === 'string') {
retStr = item.endsWith('>') ? `<${item}` : item;
} else {
retStr = `"${item}"`;
}
return retStr;
};
/**
* props hooks VNode
*
* @param {Array<IAttr>} editableAttrs props hooks
* @param {number} index editableAttrs
* @param {string} attrsType props hooks
* @return {Array} vNode
*/
const getPath = (editableAttrs: IAttr[], index: number, attrsType: string): Array<string | number> => {
const path: Array<string | number> = [];
let local = editableAttrs[index].indentation;
if (local === 1) {
path.push(attrsType === 'Hooks' ? editableAttrs[index].hIndex : editableAttrs[index].name);
} else {
let location = local;
let id = index;
while (location > 0) {
// local === 1 时处于 vNode.hooks 的子元素最外层
if (location < local || id === index || local === 1) {
if (local === 1) {
attrsType === 'Hooks'
? path.unshift(editableAttrs[id + 1].hIndex, 'state')
: path.unshift(editableAttrs[id + 1].name);
break;
} else {
if (editableAttrs[id]?.indentation === 1) {
if (editableAttrs[id]?.name === 'State') {
path.unshift('stateValue');
}
if (editableAttrs[id]?.name === 'Ref') {
path.unshift('current');
}
} else {
path.unshift(editableAttrs[id].name);
}
}
// 跳过同级
local = location;
}
location = id >= 1 ? editableAttrs[id - 1].indentation : -1;
id = -1;
}
}
return path;
};
const showAttr = [];
let currentIndentation = null;
// 为每一行数据添加一个 ref
const refsById = useMemo(() => {
const refs = {};
editableAttrs.forEach((item, index) => {
refs[index] = createRef();
});
return refs;
}, [editableAttrs]);
editableAttrs.forEach((item, index) => {
const operationRef = refsById[index];
const indentation = item.indentation;
if (currentIndentation !== null) {
if (indentation > currentIndentation) {
return;
} else {
currentIndentation = null;
}
}
const nextItem = editableAttrs[index + 1];
const hasChild = nextItem ? nextItem.indentation - item.indentation > 0 : false;
const isCollapsed = !expandNodes.includes(`${item.name}_${index}`);
// 按钮点击事件
const operationClick = (e: Event, operationRef: any) => {
// 防止点击按钮触发展开或者合起数据
e.stopPropagation();
if (operationRef.current) {
const operationRect = operationRef.current.getBoundingClientRect();
// 19.2 为图标按钮高度85 为弹框高度的一半
dropdownRef.style.setProperty('--content-top', `${operationRect.top + 19.2}px`);
dropdownRef.style.setProperty('--content-left', `${operationRect.left - 85}px`);
}
dropdownRef.classList.toggle(styles['active']);
const attrInfo = {
id: { id },
itemName: item.name,
attrsName: attrsName,
path: getPath(editableAttrs, index, attrsName),
};
(dropdownRef as any).attrInfo = attrInfo;
console.log(dropdownRef);
};
showAttr.push(
<div
className={styles.info}
style={{ paddingLeft: item.indentation * 10 }}
key={index}
onclick={() => handleCollapse(item)}
>
<span className={styles.attrArrow}>{hasChild && <Triangle director={isCollapsed ? 'right' : 'down'} />}</span>
<span className={styles.attrName}>{`${item.name}`}</span>
<div className={styles.colon}>{':'}</div>
{item.type === 'string' || item.type === 'number' || item.type === 'undefined' || item.type === 'null' ? (
<>
<input
value={getShowName(item.value)}
data-type={item.type}
className={styles.attrValue}
onChange={event => {
const nextAttrs = [...editableAttrs];
const nextItem = { ...item };
nextItem.value = event.target.value;
nextAttrs[index] = nextItem;
setEditableAttrs(nextAttrs);
}}
onKeyUp={event => {
const value = (event.target as HTMLInputElement).value;
if (event.key === 'Enter') {
if (isDev) {
console.log('post attr change', value);
} else {
const data = buildAttrModifyData(attrsType, attrs, value, item, index, id);
postMessageToBackground(ModifyAttrs, data);
}
}
}}
/>
<div className={styles.operation} ref={operationRef}>
<span className={styles.operationIcon} onclick={event => operationClick(event, operationRef)}>
<Operation />
</span>
</div>
</>
) : item.type === 'boolean' ? (
<>
<span data-type={item.type} className={styles.attrValue}>
{item.value.toString()}
</span>
<input
type={'checkbox'}
checked={item.value}
className={styles.checkBox}
onChange={event => {
const nextAttrs = [...editableAttrs];
const nextItem = { ...item };
nextItem.value = event.target.checked;
nextAttrs[index] = nextItem;
setEditableAttrs(nextAttrs);
if (!isDev) {
const data = buildAttrModifyData(attrsType, attrs, nextItem.value, item, index, id);
postMessageToBackground(ModifyAttrs, data);
}
}}
/>
</>
) : (
<>
<span data-type={item.type} className={styles.attrValue}>
{item.value}
</span>
<div className={styles.operation} ref={operationRef}>
<span className={styles.operationIcon} onClick={event => operationClick(event, operationRef)}>
<Operation />
</span>
</div>
</>
)}
</div>
);
if (isCollapsed) {
currentIndentation = indentation;
}
});
return (
<div className={styles.attrContainer}>
<div className={styles.attrHead}>
<span className={styles.attrType}>{attrsName}</span>
</div>
<div className={styles.attrDetail}>{showAttr}</div>
</div>
);
});
function ComponentInfo({ name, attrs, parents, id, source, onClickParent }: IComponentInfo) {
const view = useContext(ViewSourceContext) as any;
const viewSource = view.viewSourceFunction.viewSource;
const pick = useContext(PickElementContext) as any;
const inspectVNode = pick.pickElementFunction.inspectVNode;
const dropdownRef = useRef<null | HTMLElement>(null);
const doViewSource = (id: number) => {
postMessageToBackground(InspectDom, { id });
setTimeout(function () {
inspectVNode();
}, 100);
};
const doInspectDom = (id: number) => {
postMessageToBackground(InspectDom, { id });
setTimeout(function () {
inspectVNode();
}, 100);
};
const sourceFormatted = (fileName: string, lineNumber: number) => {
const pathWithoutLastName = /^(.*)[\\/]/;
let realName = fileName.replace(pathWithoutLastName, '');
if (/^index\./.test(realName)) {
const fileNameMatch = fileName.match(pathWithoutLastName);
if (fileNameMatch) {
const pathBeforeName = fileNameMatch[1];
if (pathBeforeName) {
const folderName = pathBeforeName.replace(pathWithoutLastName, '');
realName = folderName + '/' + realName;
}
}
}
return `${realName}:${lineNumber}`;
};
const copyToConsole = (itemName: string | number, attrsName: string, path: Array<string | number>) => {
postMessageToBackground(CopyToConsole, { id, itemName, attrsName, path });
dropdownRef.current.classList.toggle(styles['active']);
};
const storeVariable = (attrsName: string, path: Array<string | number>) => {
postMessageToBackground(StorageValue, { id, attrsName, path });
dropdownRef.current.classList.toggle(styles['active']);
};
return (
<div className={styles.infoContainer}>
<div className={styles.componentInfoHead}>
{name && (
<>
<div className={styles.name}>
<div className={styles.text}>{name}</div>
</div>
<button className={styles.button}>
<span
className={styles.eye}
title={'Inspect dom element'}
onClick={() => {
doInspectDom(id);
}}
>
<Eye />
</span>
</button>
<button className={styles.button} disabled={false}>
<span
className={styles.location}
onClick={() => {
doViewSource(id);
}}
title={'View source for this element'}
>
<Location />
</span>
</button>
<button className={styles.button}>
<span
className={styles.debug}
title={'Log this component data'}
onClick={() => {
postMessageToBackground(LogComponentData, id);
}}
>
<Debug />
</span>
</button>
</>
)}
</div>
<div className={styles.componentInfoMain}>
{Object.keys(attrs).map(attrsType => {
const parsedAttrs = attrs[attrsType];
if (parsedAttrs && parsedAttrs.length !== 0) {
const attrsName = attrsType.slice(6); // parsedState => State
return (
<ComponentAttr
attrsName={attrsName}
attrsType={attrsType}
attrs={parsedAttrs}
id={id}
dropdownRef={dropdownRef.current}
/>
);
}
return null;
})}
<div className={styles.parentsInfo}>
{name && (
<div>
<div className={styles.parentName}>Parents</div>
{parents.map(item => (
<button className={styles.parent} onClick={() => onClickParent(item)}>
{`<${item.name.itemName}>`}
</button>
))}
</div>
)}
</div>
<div className={styles.parentsInfo}>
{source && (
<>
<div>source: {''}</div>
<div style={{ marginLeft: '1rem' }}>{sourceFormatted(source.fileName, source.lineNumber)}</div>
</>
)}
</div>
<div ref={dropdownRef} className={styles.dropdown}>
<ul>
<li
onClick={() =>
copyToConsole(
(dropdownRef.current as any).attrInfo.itemName,
(dropdownRef.current as any).attrInfo.attrsName,
(dropdownRef.current as any).attrInfo.path
)
}
>
<b>Copy value to console</b>
</li>
<li
onClick={() => storeVariable((dropdownRef.current as any).attrInfo.attrsName, (dropdownRef.current as any).attrInfo.path)}
>
<b>Store as global variable</b>
</li>
</ul>
</div>
</div>
</div>
);
}
export default memo(ComponentInfo);