Match-id-5b8faa6494cb8dd94745d7e86e3e771222b7fd0e
This commit is contained in:
commit
8c1b44cde9
|
@ -0,0 +1,57 @@
|
||||||
|
## 文件清单说明:
|
||||||
|
devtools_page: devtool主页面
|
||||||
|
default_popup: 拓展图标点击时弹窗页面
|
||||||
|
content_scripts: 内容脚本,在项目中负责在页面初始化时调用注入全局变量代码和消息传递
|
||||||
|
web_accessible_resources: 注入全局变量代码
|
||||||
|
|
||||||
|
## 打开 panel 页面调试面板的方式
|
||||||
|
|
||||||
|
1. Open the developer tools.
|
||||||
|
1. Undock the developer tools if not already done (via the button in the bottom-left corner).
|
||||||
|
1. Press Ctrl + Shift + J to open the developer tools of the developer tools.
|
||||||
|
Optional: Feel free to dock the developer tools again if you had undocked it at step 2.
|
||||||
|
1. Switch from "<top frame>" to devtoolsBackground.html (or whatever name you have chosen for your devtools). (example)
|
||||||
|
1. Now you can use the Console tab to play with the chrome.devtools API.
|
||||||
|
|
||||||
|
## 全局变量注入
|
||||||
|
通过content_scripts在document初始化时给页面添加script脚本,在新添加的脚本中给window注入全局变量
|
||||||
|
|
||||||
|
## horizon页面判断
|
||||||
|
在页面完成渲染后往全局变量中添加信息,并传递 tabId 给 background 告知这是 horizon 页面
|
||||||
|
|
||||||
|
## 通信方式:
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant web_page
|
||||||
|
participant script_content
|
||||||
|
participant background
|
||||||
|
participant panel
|
||||||
|
|
||||||
|
Note over web_page: window.postMessage
|
||||||
|
web_page ->> script_content : {}
|
||||||
|
Note over script_content: window.addEventListener
|
||||||
|
Note over script_content: chrome.runtime.sendMessage
|
||||||
|
script_content ->> background : {}
|
||||||
|
Note over background: chrome.runtime.onMessage
|
||||||
|
Note over background: port.postMessage
|
||||||
|
background ->> panel : {}
|
||||||
|
Note over panel: connection.onMessage.addListener
|
||||||
|
Note over panel: connection.postMessage
|
||||||
|
panel ->> background : {}
|
||||||
|
Note over background: port.onMessage.addListener
|
||||||
|
Note over background: chrome.tabs.sendMessage
|
||||||
|
background ->> script_content : {}
|
||||||
|
Note over script_content: chrome.runtime.onMessage
|
||||||
|
Note over script_content: window.postMessage
|
||||||
|
script_content ->> web_page : {}
|
||||||
|
Note over web_page: window.addEventListener
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据压缩
|
||||||
|
渲染组件树需要知道组件名和层次信息,如果把整个vNode树传递过来,传递对象太大,最好将数据进行压缩然后传递。
|
||||||
|
- 相同的组件名可以进行压缩
|
||||||
|
- 每个vNode有唯一的 path 属性,可以作为标识使用
|
||||||
|
- 通过解析 path 值可以分析出组件树的结构
|
||||||
|
|
||||||
|
## 滚动动态渲染 Tree
|
||||||
|
考虑到组件树可能很大,所以并不适合一次性全部渲染出来,可以通过滚动渲染的方式减少页面 dom 的数量。我们可以把树看成有不同缩进长度的列表,动态渲染滚动列表的实现可以参考谷歌的这篇文章:https://developers.google.com/web/updates/2016/07/infinite-scroller 这样,我们需要的组件树数据可以由树结构转变为数组,可以减少动态渲染时对树结构进行解析时的计算工作。
|
|
@ -0,0 +1,108 @@
|
||||||
|
import styles from './ComponentsInfo.less';
|
||||||
|
import Eye from '../svgs/Eye';
|
||||||
|
import Debug from '../svgs/Debug';
|
||||||
|
import Copy from '../svgs/Copy';
|
||||||
|
import Arrow from '../svgs/Arrow';
|
||||||
|
import { useState } from 'horizon';
|
||||||
|
|
||||||
|
type IComponentInfo = {
|
||||||
|
name: string;
|
||||||
|
attrs: {
|
||||||
|
props?: IAttr[];
|
||||||
|
context?: IAttr[];
|
||||||
|
state?: IAttr[];
|
||||||
|
hooks?: IAttr[];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type IAttr = {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
value: string | boolean;
|
||||||
|
indentation: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ComponentAttr({ name, attr }: { name: string, attr: IAttr[] }) {
|
||||||
|
const [collapsedNode, setCollapsedNode] = useState(new Set());
|
||||||
|
const handleCollapse = (index: number) => {
|
||||||
|
const newSet = new Set<number>();
|
||||||
|
collapsedNode.forEach(value => {
|
||||||
|
newSet.add(value);
|
||||||
|
});
|
||||||
|
if (newSet.has(index)) {
|
||||||
|
newSet.delete(index);
|
||||||
|
} else {
|
||||||
|
newSet.add(index);
|
||||||
|
}
|
||||||
|
setCollapsedNode(newSet);
|
||||||
|
};
|
||||||
|
|
||||||
|
const showAttr = [];
|
||||||
|
let currentIndentation = null;
|
||||||
|
attr.forEach((item, index) => {
|
||||||
|
const indentation = item.indentation;
|
||||||
|
if (currentIndentation !== null) {
|
||||||
|
if (indentation > currentIndentation) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
currentIndentation = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const nextItem = attr[index + 1];
|
||||||
|
const hasChild = nextItem ? nextItem.indentation - item.indentation > 0 : false;
|
||||||
|
const isCollapsed = collapsedNode.has(index);
|
||||||
|
showAttr.push(
|
||||||
|
<div style={{ paddingLeft: item.indentation * 10 }} key={index} onClick={() => (handleCollapse(index))}>
|
||||||
|
<span className={styles.attrArrow}>{hasChild && <Arrow director={isCollapsed ? 'right' : 'down'} />}</span>
|
||||||
|
<span className={styles.attrName}>{`${item.name}`}</span>
|
||||||
|
{' :'}
|
||||||
|
<span className={styles.attrValue}>{item.value}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
if (isCollapsed) {
|
||||||
|
currentIndentation = indentation;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.attrContainer}>
|
||||||
|
<div className={styles.attrHead}>
|
||||||
|
<span className={styles.attrType}>{name}</span>
|
||||||
|
<span className={styles.attrCopy}>
|
||||||
|
<Copy />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.attrDetail}>
|
||||||
|
{showAttr}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ComponentInfo({ name, attrs }: IComponentInfo) {
|
||||||
|
const { state, props, context, hooks } = attrs;
|
||||||
|
return (
|
||||||
|
<div className={styles.infoContainer} >
|
||||||
|
<div className={styles.componentInfoHead}>
|
||||||
|
<span className={styles.name}>
|
||||||
|
{name}
|
||||||
|
</span>
|
||||||
|
<span className={styles.eye} >
|
||||||
|
<Eye />
|
||||||
|
</span>
|
||||||
|
<span className={styles.debug}>
|
||||||
|
<Debug />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.componentInfoMain}>
|
||||||
|
{context && <ComponentAttr name={'context'} attr={context} />}
|
||||||
|
{props && <ComponentAttr name={'props'} attr={props} />}
|
||||||
|
{state && <ComponentAttr name={'state'} attr={state} />}
|
||||||
|
{hooks && <ComponentAttr name={'hook'} attr={hooks} />}
|
||||||
|
<div className={styles.renderInfo}>
|
||||||
|
rendered by
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
@import 'assets.less';
|
||||||
|
|
||||||
|
.infoContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
|
||||||
|
.componentInfoHead {
|
||||||
|
flex: 0 0 @top-height;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-bottom: @divider-style;
|
||||||
|
|
||||||
|
.name {
|
||||||
|
flex: 1 1 0;
|
||||||
|
padding: 0 1rem 0 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.eye {
|
||||||
|
flex: 0 0 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.debug {
|
||||||
|
flex: 0 0 1rem;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.componentInfoMain {
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
:last-child {
|
||||||
|
border-bottom: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
:first-child {
|
||||||
|
padding: unset;
|
||||||
|
}
|
||||||
|
|
||||||
|
>div {
|
||||||
|
border-bottom: @divider-style;
|
||||||
|
padding: 0.5rem
|
||||||
|
}
|
||||||
|
|
||||||
|
.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 {
|
||||||
|
color: @attr-name-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attrValue {
|
||||||
|
margin-left: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.renderInfo {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
.search {
|
||||||
|
width: 100%;
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
import styles from './Search.less';
|
||||||
|
|
||||||
|
interface SearchProps {
|
||||||
|
onChange: (event: any) => void,
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Search(props: SearchProps) {
|
||||||
|
const { onChange } = props;
|
||||||
|
const handleChange = (event) => {
|
||||||
|
onChange(event.target.value);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<input
|
||||||
|
onChange={handleChange}
|
||||||
|
className={styles.search}
|
||||||
|
placeholder={'Search (text or /regex/)'}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
@import 'assets.less';
|
||||||
|
|
||||||
|
.treeContainer {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.treeItem {
|
||||||
|
width: 100%;
|
||||||
|
position: absolute;
|
||||||
|
line-height: 18px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: @select-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.treeIcon {
|
||||||
|
color: @arrow-color;
|
||||||
|
display: inline-block;
|
||||||
|
width: 12px;
|
||||||
|
padding-left: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.componentName {
|
||||||
|
color: @component-name-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.componentKeyName {
|
||||||
|
color: @component-key-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.componentKeyValue {
|
||||||
|
color: @componentKeyValue-color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
background-color: rgb(141 199 248 / 60%);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,174 @@
|
||||||
|
import { useState } from 'horizon';
|
||||||
|
import styles from './VTree.less';
|
||||||
|
import Arrow from '../svgs/Arrow';
|
||||||
|
import { createRegExp } from './../utils';
|
||||||
|
|
||||||
|
export interface IData {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
indentation: number;
|
||||||
|
userKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
type IItem = {
|
||||||
|
style: any,
|
||||||
|
hasChild: boolean,
|
||||||
|
onCollapse: (id: string) => void,
|
||||||
|
onClick: (id: string) => void,
|
||||||
|
isCollapsed: boolean,
|
||||||
|
isSelect: boolean,
|
||||||
|
highlightValue: string,
|
||||||
|
} & IData
|
||||||
|
|
||||||
|
// TODO: 计算可以展示的最多数量,并且监听显示器高度变化修改数值
|
||||||
|
const showNum = 70;
|
||||||
|
const lineHeight = 18;
|
||||||
|
const indentationLength = 20;
|
||||||
|
|
||||||
|
function Item(props: IItem) {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
style,
|
||||||
|
userKey,
|
||||||
|
hasChild,
|
||||||
|
onCollapse,
|
||||||
|
isCollapsed,
|
||||||
|
id,
|
||||||
|
indentation,
|
||||||
|
onClick,
|
||||||
|
isSelect,
|
||||||
|
highlightValue,
|
||||||
|
} = props;
|
||||||
|
const isShowKey = userKey !== '';
|
||||||
|
const showIcon = hasChild ? <Arrow director={isCollapsed ? 'right' : 'down'} /> : '';
|
||||||
|
const handleClickCollapse = () => {
|
||||||
|
onCollapse(id);
|
||||||
|
};
|
||||||
|
const handleClick = () => {
|
||||||
|
onClick(id);
|
||||||
|
};
|
||||||
|
const itemAttr: any = { style, className: styles.treeItem, onClick: handleClick };
|
||||||
|
if (isSelect) {
|
||||||
|
itemAttr.tabIndex = 0;
|
||||||
|
itemAttr.className = styles.treeItem + ' ' + styles.select;
|
||||||
|
}
|
||||||
|
const reg = createRegExp(highlightValue);
|
||||||
|
const heightCharacters = name.match(reg);
|
||||||
|
let showName;
|
||||||
|
if (heightCharacters) {
|
||||||
|
let cutName = name;
|
||||||
|
showName = [];
|
||||||
|
// 高亮第一次匹配即可
|
||||||
|
const char = heightCharacters[0];
|
||||||
|
const index = name.search(char);
|
||||||
|
const notHighlightStr = cutName.slice(0, index);
|
||||||
|
showName.push(notHighlightStr);
|
||||||
|
showName.push(<mark>{char}</mark>);
|
||||||
|
cutName = cutName.slice(index + char.length);
|
||||||
|
showName.push(cutName);
|
||||||
|
} else {
|
||||||
|
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({ data, highlightValue }: { data: IData[], highlightValue: string }) {
|
||||||
|
const [scrollTop, setScrollTop] = useState(0);
|
||||||
|
const [collapseNode, setCollapseNode] = useState(new Set<string>());
|
||||||
|
const [selectItem, setSelectItem] = useState();
|
||||||
|
const changeCollapseNode = (id: string) => {
|
||||||
|
const nodes = new Set<string>();
|
||||||
|
collapseNode.forEach(value => {
|
||||||
|
nodes.add(value);
|
||||||
|
});
|
||||||
|
if (nodes.has(id)) {
|
||||||
|
nodes.delete(id);
|
||||||
|
} else {
|
||||||
|
nodes.add(id);
|
||||||
|
}
|
||||||
|
setCollapseNode(nodes);
|
||||||
|
};
|
||||||
|
const handleClickItem = (id: string) => {
|
||||||
|
setSelectItem(id);
|
||||||
|
};
|
||||||
|
const showList: any[] = [];
|
||||||
|
|
||||||
|
let totalHeight = 0;
|
||||||
|
let currentCollapseIndentation: null | number = null;
|
||||||
|
data.forEach((item, index) => {
|
||||||
|
// 存在未处理完的收起节点
|
||||||
|
if (currentCollapseIndentation !== null) {
|
||||||
|
const indentation = item.indentation;
|
||||||
|
// 缩进更大,不显示
|
||||||
|
if (indentation > currentCollapseIndentation) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
// 缩进小,说明完成了该收起节点的子节点处理。
|
||||||
|
currentCollapseIndentation = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const id = item.id;
|
||||||
|
const isCollapsed = collapseNode.has(id);
|
||||||
|
if (totalHeight >= scrollTop && showList.length <= showNum) {
|
||||||
|
const nextItem = data[index + 1];
|
||||||
|
// 如果存在下一个节点,并且节点缩进比自己大,说明下个节点是子节点,节点本身需要显示展开收起图标
|
||||||
|
const hasChild = nextItem ? nextItem.indentation > item.indentation : false;
|
||||||
|
showList.push(
|
||||||
|
<Item
|
||||||
|
key={id}
|
||||||
|
hasChild={hasChild}
|
||||||
|
style={{
|
||||||
|
transform: `translateY(${totalHeight}px)`,
|
||||||
|
}}
|
||||||
|
onCollapse={changeCollapseNode}
|
||||||
|
onClick={handleClickItem}
|
||||||
|
isCollapsed={isCollapsed}
|
||||||
|
isSelect={id === selectItem}
|
||||||
|
highlightValue={highlightValue}
|
||||||
|
{...item} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
totalHeight = totalHeight + lineHeight;
|
||||||
|
if (isCollapsed) {
|
||||||
|
// 该节点需要收起子节点
|
||||||
|
currentCollapseIndentation = item.indentation;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleScroll = (event: any) => {
|
||||||
|
const scrollTop = event.target.scrollTop;
|
||||||
|
// 顶部留 100px 冗余高度
|
||||||
|
setScrollTop(Math.max(scrollTop - 100, 0));
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.treeContainer} onScroll={handleScroll}>
|
||||||
|
{showList}
|
||||||
|
{/* 确保有足够的高度 */}
|
||||||
|
<div style={{ marginTop: totalHeight }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default VTree;
|
|
@ -0,0 +1,14 @@
|
||||||
|
@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);
|
||||||
|
@componentKeyValue-color: rgb(26, 26, 166);
|
||||||
|
@component-attr-color: rgb(200, 0, 0);
|
||||||
|
@select-color: rgb(141 199 248 / 60%);
|
||||||
|
|
||||||
|
@top-height: 2.625rem;
|
||||||
|
@divider-width: 0.2px;
|
||||||
|
@common-font-size: 12px;
|
||||||
|
|
||||||
|
@divider-style: @divider-color solid @divider-width;
|
|
@ -0,0 +1,98 @@
|
||||||
|
/**
|
||||||
|
* 用一个纯数据类型的对象 tree 去表示树的结构是非常清晰的,但是它不能准确的模拟 VNode 中存在的引用
|
||||||
|
* 关系,需要进行转换 getMockVNodeTree
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { parseAttr } from '../parser/parseAttr';
|
||||||
|
import parseTreeRoot from '../parser/parseVNode';
|
||||||
|
import { VNode } from './../../../horizon/src/renderer/vnode/VNode';
|
||||||
|
import { FunctionComponent, ClassComponent } from './../../../horizon/src/renderer/vnode/VNodeTags';
|
||||||
|
|
||||||
|
const mockComponentNames = ['Apple', 'Pear', 'Banana', 'Orange', 'Jenny', 'Kiwi', 'Coconut'];
|
||||||
|
|
||||||
|
function MockVNode(tag: string, props = {}, key = null, realNode = {}) {
|
||||||
|
const vNode = new VNode(tag, props, key, realNode);
|
||||||
|
const name = mockComponentNames.shift() || 'MockComponent';
|
||||||
|
vNode.type = { name };
|
||||||
|
return vNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IMockTree {
|
||||||
|
tag: string,
|
||||||
|
children?: IMockTree[],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟树
|
||||||
|
const tree: IMockTree = {
|
||||||
|
tag: ClassComponent,
|
||||||
|
children: [
|
||||||
|
{ tag: FunctionComponent },
|
||||||
|
{ tag: ClassComponent },
|
||||||
|
{ tag: FunctionComponent },
|
||||||
|
{
|
||||||
|
tag: FunctionComponent,
|
||||||
|
children: [
|
||||||
|
{ tag: ClassComponent }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
function addOneThousandNode(node: IMockTree) {
|
||||||
|
const nodes = [];
|
||||||
|
for (let i = 0; i < 1000; i++) {
|
||||||
|
nodes.push({ tag: FunctionComponent });
|
||||||
|
}
|
||||||
|
node?.children.push({ tag: ClassComponent, children: nodes });
|
||||||
|
}
|
||||||
|
|
||||||
|
addOneThousandNode(tree);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将mock数据转变为 VNode 树
|
||||||
|
*
|
||||||
|
* @param node 树节点
|
||||||
|
* @param vNode VNode节点
|
||||||
|
*/
|
||||||
|
function getMockVNodeTree(node: IMockTree, vNode: VNode) {
|
||||||
|
const children = node.children;
|
||||||
|
if (children && children.length !== 0) {
|
||||||
|
const childNode = children[0];
|
||||||
|
let childVNode = MockVNode(childNode.tag);
|
||||||
|
childVNode.key = '0';
|
||||||
|
getMockVNodeTree(childNode, childVNode);
|
||||||
|
// 需要建立双链
|
||||||
|
vNode.child = childVNode;
|
||||||
|
childVNode.parent = vNode;
|
||||||
|
for (let i = 1; i < children.length; i++) {
|
||||||
|
const nextNode = children[i];
|
||||||
|
const nextVNode = MockVNode(nextNode.tag);
|
||||||
|
nextVNode.key = String(i);
|
||||||
|
nextVNode.parent = vNode;
|
||||||
|
getMockVNodeTree(nextNode, nextVNode);
|
||||||
|
childVNode.next = nextVNode;
|
||||||
|
childVNode = nextVNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const rootVNode = MockVNode(tree.tag);
|
||||||
|
getMockVNodeTree(tree, rootVNode);
|
||||||
|
|
||||||
|
export const mockParsedVNodeData = parseTreeRoot(rootVNode);
|
||||||
|
|
||||||
|
const mockState = {
|
||||||
|
str: 'jenny',
|
||||||
|
num: 3,
|
||||||
|
boolean: true,
|
||||||
|
und: undefined,
|
||||||
|
fun: () => ({}),
|
||||||
|
symbol: Symbol('sym'),
|
||||||
|
map: new Map([['a', 'a']]),
|
||||||
|
set: new Set(['a', 1, 2, Symbol('bambi')]),
|
||||||
|
arr: [1, 2, 3, 4],
|
||||||
|
obj: {
|
||||||
|
niko: { jenny: 'jenny' }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parsedMockState = parseAttr(mockState);
|
|
@ -0,0 +1,53 @@
|
||||||
|
@import '../components/assets.less';
|
||||||
|
|
||||||
|
.app {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
height: 100%;
|
||||||
|
font-size: @common-font-size;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
flex: 7;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.left_top {
|
||||||
|
border-bottom: @divider-style;
|
||||||
|
flex: 0 0 @top-height;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.select {
|
||||||
|
padding: 0 0.25rem 0 0.25rem;
|
||||||
|
flex: 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
flex: 0 0 1px;
|
||||||
|
margin: 0 0.25rem 0 0.25rem;
|
||||||
|
border-left: @divider-style;
|
||||||
|
height: calc(100% - 1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
flex: 1 1 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.left_bottom {
|
||||||
|
flex: 1;
|
||||||
|
height: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
flex: 3;
|
||||||
|
border-left: @divider-style;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
outline: none;
|
||||||
|
border-width: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
import { useState, useEffect } from 'horizon';
|
||||||
|
import VTree, { IData } from '../components/VTree';
|
||||||
|
import Search from '../components/Search';
|
||||||
|
import ComponentInfo from '../components/ComponentInfo';
|
||||||
|
import styles from './App.less';
|
||||||
|
import Select from '../svgs/Select';
|
||||||
|
import { mockParsedVNodeData, parsedMockState } from '../devtools/mock';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [parsedVNodeData, setParsedVNodeData] = useState([]);
|
||||||
|
const [componentInfo, setComponentInfo] = useState({ name: null, attrs: {} });
|
||||||
|
const [filterValue, setFilterValue] = useState('');
|
||||||
|
useEffect(() => {
|
||||||
|
if (isDev) {
|
||||||
|
setParsedVNodeData(mockParsedVNodeData);
|
||||||
|
setComponentInfo({
|
||||||
|
name: 'Demo',
|
||||||
|
attrs: {
|
||||||
|
state: parsedMockState,
|
||||||
|
props: parsedMockState,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
const idIndentationMap: {
|
||||||
|
[id: string]: number;
|
||||||
|
} = {};
|
||||||
|
const data: IData[] = [];
|
||||||
|
let i = 0;
|
||||||
|
while (i < parsedVNodeData.length) {
|
||||||
|
const id = parsedVNodeData[i] as string;
|
||||||
|
i++;
|
||||||
|
const name = parsedVNodeData[i] as string;
|
||||||
|
i++;
|
||||||
|
const parentId = parsedVNodeData[i] as string;
|
||||||
|
i++;
|
||||||
|
const userKey = parsedVNodeData[i] as string;
|
||||||
|
i++;
|
||||||
|
const indentation = parentId === '' ? 0 : idIndentationMap[parentId] + 1;
|
||||||
|
idIndentationMap[id] = indentation;
|
||||||
|
const item = {
|
||||||
|
id, name, indentation, userKey
|
||||||
|
};
|
||||||
|
data.push(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSearchChange = (str: string) => {
|
||||||
|
setFilterValue(str);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.app}>
|
||||||
|
<div className={styles.left}>
|
||||||
|
<div className={styles.left_top} >
|
||||||
|
<div className={styles.select} >
|
||||||
|
<Select />
|
||||||
|
</div>
|
||||||
|
<div className={styles.divider} />
|
||||||
|
<div className={styles.search}>
|
||||||
|
<Search onChange={handleSearchChange} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.left_bottom}>
|
||||||
|
<VTree data={data} highlightValue={filterValue} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={styles.right}>
|
||||||
|
<ComponentInfo name={componentInfo.name} attrs={componentInfo.attrs} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { render } from 'horizon';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
|
render(
|
||||||
|
<App />,
|
||||||
|
document.getElementById('root')
|
||||||
|
);
|
|
@ -0,0 +1,34 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html>
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf8">
|
||||||
|
<title>Horizon</title>
|
||||||
|
<script src='horizon.production.js'></script>
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
|
@ -0,0 +1,83 @@
|
||||||
|
|
||||||
|
// 将状态的值解析成固定格式
|
||||||
|
export function parseAttr(rootAttr: any) {
|
||||||
|
const result: {
|
||||||
|
name: string,
|
||||||
|
type: string,
|
||||||
|
value: string,
|
||||||
|
indentation: number
|
||||||
|
}[] = [];
|
||||||
|
const indentation = 0;
|
||||||
|
const parseSubAttr = (attr: any, parentIndentation: number, attrName: string) => {
|
||||||
|
const stateType = typeof attr;
|
||||||
|
let value: any;
|
||||||
|
let showType;
|
||||||
|
let addSubState;
|
||||||
|
if (stateType === 'boolean' ||
|
||||||
|
stateType === 'number' ||
|
||||||
|
stateType === 'string' ||
|
||||||
|
stateType === 'undefined') {
|
||||||
|
value = attr;
|
||||||
|
showType = stateType;
|
||||||
|
} else if (stateType === 'function') {
|
||||||
|
const funName = attr.name;
|
||||||
|
value = `f() ${funName}{}`;
|
||||||
|
} else if (stateType === 'symbol') {
|
||||||
|
value = attr.description;
|
||||||
|
} else if (stateType === 'object') {
|
||||||
|
if (attr === null) {
|
||||||
|
showType = 'null';
|
||||||
|
} else if (attr instanceof Map) {
|
||||||
|
showType = 'map';
|
||||||
|
const size = attr.size;
|
||||||
|
value = `Map(${size})`;
|
||||||
|
addSubState = () => {
|
||||||
|
attr.forEach((value, key) => {
|
||||||
|
parseSubAttr(value, parentIndentation + 2, key);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
} else if (attr instanceof Set) {
|
||||||
|
showType = 'set';
|
||||||
|
const size = attr.size;
|
||||||
|
value = `Set(${size})`;
|
||||||
|
addSubState = () => {
|
||||||
|
let i = 0;
|
||||||
|
attr.forEach((value) => {
|
||||||
|
parseSubAttr(value, parentIndentation + 2, String(i));
|
||||||
|
});
|
||||||
|
i++;
|
||||||
|
};
|
||||||
|
} else if (Array.isArray(attr)) {
|
||||||
|
showType = 'array';
|
||||||
|
value = `Array(${attr.length})`;
|
||||||
|
addSubState = () => {
|
||||||
|
attr.forEach((value, index) => {
|
||||||
|
parseSubAttr(value, parentIndentation + 2, String(index));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
showType = stateType;
|
||||||
|
value = '{...}';
|
||||||
|
addSubState = () => {
|
||||||
|
Object.keys(attr).forEach((key) => {
|
||||||
|
parseSubAttr(attr[key], parentIndentation + 2, key);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result.push({
|
||||||
|
name: attrName,
|
||||||
|
type: showType,
|
||||||
|
value,
|
||||||
|
indentation: parentIndentation + 1,
|
||||||
|
});
|
||||||
|
if (addSubState) {
|
||||||
|
addSubState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Object.keys(rootAttr).forEach(key => {
|
||||||
|
parseSubAttr(rootAttr[key], indentation, key);
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { travelVNodeTree } from '../../../../libs/horizon/src/renderer/vnode/VNodeUtils';
|
||||||
|
import { VNode } from '../../../../libs/horizon/src/renderer/Types';
|
||||||
|
import { ClassComponent, FunctionComponent } from '../../../../libs/horizon/src/renderer/vnode/VNodeTags';
|
||||||
|
|
||||||
|
// 建立双向映射关系,当用户在修改属性值后,可以找到对应的 VNode
|
||||||
|
const VNodeToIdMap = new Map<VNode, number>();
|
||||||
|
const IdToVNodeMap = new Map<number, VNode>();
|
||||||
|
|
||||||
|
let uid = 0;
|
||||||
|
function generateUid () {
|
||||||
|
uid++;
|
||||||
|
return uid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUserComponent(tag: string) {
|
||||||
|
// TODO: 添加其他组件
|
||||||
|
return tag === ClassComponent || tag === FunctionComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getParentUserComponent(node: VNode) {
|
||||||
|
let parent = node.parent;
|
||||||
|
while(parent) {
|
||||||
|
if (isUserComponent(parent.tag)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
parent = parent.parent;
|
||||||
|
}
|
||||||
|
return parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTreeRoot(treeRoot: VNode) {
|
||||||
|
const result: any[] = [];
|
||||||
|
travelVNodeTree(treeRoot, (node: VNode) => {
|
||||||
|
const tag = node.tag;
|
||||||
|
if (isUserComponent(tag)) {
|
||||||
|
const id = generateUid();
|
||||||
|
result.push(id);
|
||||||
|
const name = node.type.name;
|
||||||
|
result.push(name);
|
||||||
|
const parent = getParentUserComponent(node);
|
||||||
|
if (parent) {
|
||||||
|
const parentId = VNodeToIdMap.get(parent);
|
||||||
|
result.push(parentId);
|
||||||
|
} else {
|
||||||
|
result.push('');
|
||||||
|
}
|
||||||
|
const key = node.key;
|
||||||
|
if (key !== null) {
|
||||||
|
result.push(key);
|
||||||
|
} else {
|
||||||
|
result.push('');
|
||||||
|
}
|
||||||
|
VNodeToIdMap.set(node, id);
|
||||||
|
IdToVNodeMap.set(id, node);
|
||||||
|
}
|
||||||
|
}, null, treeRoot, null);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default parseTreeRoot;
|
|
@ -0,0 +1,17 @@
|
||||||
|
interface IArrow {
|
||||||
|
director: 'right' | 'down'
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Arrow({ director }: IArrow) {
|
||||||
|
let d: string;
|
||||||
|
if (director === 'right') {
|
||||||
|
d = 'm2 0l12 8l-12 8 z';
|
||||||
|
} else if (director === 'down') {
|
||||||
|
d = 'm0 2h16 l-8 12 z';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='8px' height='8px'>
|
||||||
|
<path d={d} fill='currentColor' />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
export default function Copy() {
|
||||||
|
return (
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||||
|
<path d='M0 0 H16 V16 H0 z M2 2 H8 V8 H2 V2z' fill='currentColor' fill-rule='evenodd' />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
export default function Debug() {
|
||||||
|
return (
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||||
|
<path d='m2 0l12 8l-12 8 z' fill='#000'/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
export default function Eye() {
|
||||||
|
return (
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||||
|
<ellipse cx="8" cy="8" rx="8" ry="6" />
|
||||||
|
<circle cx="8" cy="8" r="4" fill="rgb(255, 255, 255)" />
|
||||||
|
<circle cx="8" cy="8" r="2" fill="#000000" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
|
||||||
|
export default function Select() {
|
||||||
|
return (
|
||||||
|
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||||
|
<path d='M14 6 V3 C14 2.5 13.5 2 13 2 H3 C2.5 2 2 2.5 2 3 V13 C2 13.5 2.5 14 3 14H6 V13 H3 V3 H13 V6z M7 7 L9 15 L11 12 L14 15 L15 14 L12 11 L15 9z' fill='#000' />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
|
||||||
|
export function createRegExp(expression: string) {
|
||||||
|
let str = expression;
|
||||||
|
if (str[0] === '/') {
|
||||||
|
str = str.slice(1);
|
||||||
|
}
|
||||||
|
if (str[str.length - 1] === '/') {
|
||||||
|
str = str.slice(0, str.length - 1);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return new RegExp(str, 'i');
|
||||||
|
} catch (err) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,76 @@
|
||||||
|
const path = require('path');
|
||||||
|
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||||
|
const webpack = require('webpack');
|
||||||
|
|
||||||
|
// 用于 panel 页面开发
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
mode: 'development',
|
||||||
|
entry: {
|
||||||
|
panel: path.join(__dirname, './src/panel/index.tsx'),
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, 'dist'),
|
||||||
|
filename: '[name].js'
|
||||||
|
},
|
||||||
|
devtool: 'source-map',
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.ts', '.tsx', '.js']
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
exclude: /node_modules/,
|
||||||
|
use: [
|
||||||
|
{
|
||||||
|
loader: 'babel-loader',
|
||||||
|
options: {
|
||||||
|
presets: ['@babel/preset-env',
|
||||||
|
'@babel/preset-typescript',
|
||||||
|
['@babel/preset-react', {
|
||||||
|
runtime: 'classic',
|
||||||
|
'pragma': 'Horizon.createElement',
|
||||||
|
'pragmaFrag': 'Horizon.Fragment',
|
||||||
|
}]],
|
||||||
|
plugins: ['@babel/plugin-proposal-class-properties'],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
test: /\.less/i,
|
||||||
|
use: [
|
||||||
|
'style-loader',
|
||||||
|
{
|
||||||
|
loader: 'css-loader',
|
||||||
|
options: {
|
||||||
|
modules: true,
|
||||||
|
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'less-loader'],
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
externals: {
|
||||||
|
'horizon': 'Horizon',
|
||||||
|
},
|
||||||
|
devServer: {
|
||||||
|
static: {
|
||||||
|
directory: path.join(__dirname, 'dist'),
|
||||||
|
},
|
||||||
|
open: 'panel.html',
|
||||||
|
port: 9000,
|
||||||
|
magicHtml: true,
|
||||||
|
},
|
||||||
|
plugins: [
|
||||||
|
new HtmlWebpackPlugin({
|
||||||
|
filename: 'panel.html',
|
||||||
|
template: './src/panel/panel.html'
|
||||||
|
}),
|
||||||
|
new webpack.DefinePlugin({
|
||||||
|
'process.env.NODE_ENV': '"development"',
|
||||||
|
isDev: 'true',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
|
@ -0,0 +1,131 @@
|
||||||
|
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||||
|
import * as LogUtils from '../jest/logUtils';
|
||||||
|
import * as TestUtils from '../jest/testUtils';
|
||||||
|
|
||||||
|
describe('事件', () => {
|
||||||
|
it('根节点挂载全量事件', () => {
|
||||||
|
const App = () => {
|
||||||
|
return <div />;
|
||||||
|
}
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
console.log(TestUtils.getEventListeners(container));
|
||||||
|
})
|
||||||
|
|
||||||
|
it('事件捕获与冒泡', () => {
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div onClickCapture={() => LogUtils.log('div capture')} onClick={() => LogUtils.log('div bubble')}>
|
||||||
|
<p onClickCapture={() => LogUtils.log('p capture')} onClick={() => LogUtils.log('p bubble')}>
|
||||||
|
<button onClickCapture={() => LogUtils.log('btn capture')} onClick={() => LogUtils.log('btn bubble')} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
const a = container.querySelector('button');
|
||||||
|
a.click();
|
||||||
|
expect(LogUtils.getAndClear()).toEqual([
|
||||||
|
// 从外到内先捕获再冒泡
|
||||||
|
'div capture',
|
||||||
|
'p capture',
|
||||||
|
'btn capture',
|
||||||
|
'btn bubble',
|
||||||
|
'p bubble',
|
||||||
|
'div bubble'
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns 0', () => {
|
||||||
|
let keyCode = null;
|
||||||
|
const node = Horizon.render(
|
||||||
|
<input
|
||||||
|
onKeyPress={e => {
|
||||||
|
keyCode = e.keyCode;
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
node.dispatchEvent(
|
||||||
|
new KeyboardEvent('keypress', {
|
||||||
|
keyCode: 65,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(keyCode).toBe(65);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('阻止事件冒泡', () => {
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div onClickCapture={() => LogUtils.log('div capture')} onClick={() => LogUtils.log('div bubble')}>
|
||||||
|
<p onClickCapture={() => LogUtils.log('p capture')} onClick={() => LogUtils.log('p bubble')}>
|
||||||
|
<button onClickCapture={() => LogUtils.log('btn capture')} onClick={(e) => TestUtils.stopBubbleOrCapture(e, 'btn bubble')} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
container.querySelector('button').click();
|
||||||
|
|
||||||
|
expect(LogUtils.getAndClear()).toEqual([
|
||||||
|
// 到button时停止冒泡
|
||||||
|
'div capture',
|
||||||
|
'p capture',
|
||||||
|
'btn capture',
|
||||||
|
'btn bubble'
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('阻止事件捕获', () => {
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div onClickCapture={(e) => TestUtils.stopBubbleOrCapture(e, 'div capture')} onClick={() => LogUtils.log('div bubble')}>
|
||||||
|
<p onClickCapture={() => LogUtils.log('p capture')} onClick={() => LogUtils.log('p bubble')}>
|
||||||
|
<button onClickCapture={() => LogUtils.log('btn capture')} onClick={() => LogUtils.log('btn bubble')} />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
container.querySelector('button').click();
|
||||||
|
|
||||||
|
expect(LogUtils.getAndClear()).toEqual([
|
||||||
|
// 阻止捕获,不再继续向下执行
|
||||||
|
'div capture'
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
|
||||||
|
it('阻止原生事件冒泡', () => {
|
||||||
|
const App = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<p>
|
||||||
|
<button />
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Horizon.render(<App />, container);
|
||||||
|
container.querySelector('div').addEventListener('click', () => {
|
||||||
|
LogUtils.log('div bubble');
|
||||||
|
}, false);
|
||||||
|
container.querySelector('p').addEventListener('click', () => {
|
||||||
|
LogUtils.log('p bubble');
|
||||||
|
}, false);
|
||||||
|
container.querySelector('button').addEventListener('click', (e) => {
|
||||||
|
LogUtils.log('btn bubble');
|
||||||
|
e.stopPropagation();
|
||||||
|
}, false);
|
||||||
|
container.querySelector('button').click();
|
||||||
|
expect(LogUtils.getAndClear()).toEqual([
|
||||||
|
'btn bubble'
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,46 @@
|
||||||
|
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||||
|
import * as LogUtils from '../jest/logUtils';
|
||||||
|
import { act } from '../jest/customMatcher';
|
||||||
|
|
||||||
|
describe('合成焦点事件', () => {
|
||||||
|
|
||||||
|
it('onFocus', () => {
|
||||||
|
const realNode = Horizon.render(
|
||||||
|
<input
|
||||||
|
onFocus={event => LogUtils.log(`onFocus: ${event.type}`)}
|
||||||
|
onFocusCapture={event => LogUtils.log(`onFocusCapture: ${event.type}`)}
|
||||||
|
/>, container);
|
||||||
|
|
||||||
|
realNode.dispatchEvent(
|
||||||
|
new FocusEvent('focusin', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(LogUtils.getAndClear()).toEqual([
|
||||||
|
'onFocusCapture: focus',
|
||||||
|
'onFocus: focus',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onBlur', () => {
|
||||||
|
const realNode = Horizon.render(
|
||||||
|
<input
|
||||||
|
onBlur={event => LogUtils.log(`onBlur: ${event.type}`)}
|
||||||
|
onBlurCapture={event => LogUtils.log(`onBlurCapture: ${event.type}`)}
|
||||||
|
/>, container);
|
||||||
|
|
||||||
|
realNode.dispatchEvent(
|
||||||
|
new FocusEvent('focusout', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(LogUtils.getAndClear()).toEqual([
|
||||||
|
'onBlurCapture: blur',
|
||||||
|
'onBlur: blur',
|
||||||
|
]);
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,179 @@
|
||||||
|
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||||
|
import * as LogUtils from '../jest/logUtils';
|
||||||
|
|
||||||
|
describe('Keyboard Event', () => {
|
||||||
|
|
||||||
|
it('keydown,keypress,keyup的keycode,charcode', () => {
|
||||||
|
const node = Horizon.render(
|
||||||
|
<input
|
||||||
|
onKeyUp={(e) => {
|
||||||
|
LogUtils.log('onKeyUp: keycode: ' + e.keyCode + ',charcode: ' + e.charCode);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
LogUtils.log('onKeyDown: keycode: ' + e.keyCode + ',charcode: ' + e.charCode)
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
node.dispatchEvent(
|
||||||
|
new KeyboardEvent('keydown', {
|
||||||
|
keyCode: 50,
|
||||||
|
code: 'Digit2',
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
node.dispatchEvent(
|
||||||
|
new KeyboardEvent('keyup', {
|
||||||
|
keyCode: 50,
|
||||||
|
code: 'Digit2',
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(LogUtils.getAndClear()).toEqual([
|
||||||
|
'onKeyDown: keycode: 50,charcode: 0',
|
||||||
|
'onKeyUp: keycode: 50,charcode: 0'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keypress的keycode,charcode', () => {
|
||||||
|
const node = Horizon.render(
|
||||||
|
<input
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
LogUtils.log('onKeyPress: keycode: ' + e.keyCode + ',charcode: ' + e.charCode);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
node.dispatchEvent(
|
||||||
|
new KeyboardEvent('keypress', {
|
||||||
|
charCode: 50,
|
||||||
|
code: 'Digit2',
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(LogUtils.getAndClear()).toEqual([
|
||||||
|
'onKeyPress: keycode: 0,charcode: 50'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('当charcode为13,且不设置keycode的时候', () => {
|
||||||
|
const node = Horizon.render(
|
||||||
|
<input
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
LogUtils.log('onKeyPress: keycode: ' + e.keyCode + ',charcode: ' + e.charCode);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
node.dispatchEvent(
|
||||||
|
new KeyboardEvent('keypress', {
|
||||||
|
charCode: 13,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(LogUtils.getAndClear()).toEqual([
|
||||||
|
'onKeyPress: keycode: 0,charcode: 13'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keydown,keypress,keyup的code', () => {
|
||||||
|
const node = Horizon.render(
|
||||||
|
<input
|
||||||
|
onKeyUp={(e) => {
|
||||||
|
LogUtils.log('onKeyUp: code: ' + e.code);
|
||||||
|
}}
|
||||||
|
onKeyPress={(e) => {
|
||||||
|
LogUtils.log('onKeyPress: code: ' + e.code);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
LogUtils.log('onKeyDown: code: ' + e.code);
|
||||||
|
}}
|
||||||
|
/>,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
node.dispatchEvent(
|
||||||
|
new KeyboardEvent('keydown', {
|
||||||
|
code: 'Digit2',
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
node.dispatchEvent(
|
||||||
|
new KeyboardEvent('keypress', {
|
||||||
|
keyCode: 50,
|
||||||
|
code: 'Digit2',
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
node.dispatchEvent(
|
||||||
|
new KeyboardEvent('keyup', {
|
||||||
|
code: 'Digit2',
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(LogUtils.getAndClear()).toEqual([
|
||||||
|
'onKeyDown: code: Digit2',
|
||||||
|
'onKeyPress: code: Digit2',
|
||||||
|
'onKeyUp: code: Digit2'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('可以执行preventDefault和 stopPropagation', () => {
|
||||||
|
const keyboardProcessing = e => {
|
||||||
|
expect(e.isDefaultPrevented()).toBe(false);
|
||||||
|
e.preventDefault();
|
||||||
|
expect(e.isDefaultPrevented()).toBe(true);
|
||||||
|
|
||||||
|
expect(e.isPropagationStopped()).toBe(false);
|
||||||
|
e.stopPropagation();
|
||||||
|
expect(e.isPropagationStopped()).toBe(true);
|
||||||
|
LogUtils.log(e.type + ' handle');
|
||||||
|
};
|
||||||
|
const div = Horizon.render(
|
||||||
|
<div
|
||||||
|
onKeyDown={keyboardProcessing}
|
||||||
|
onKeyUp={keyboardProcessing}
|
||||||
|
onKeyPress={keyboardProcessing}
|
||||||
|
/>,
|
||||||
|
container,
|
||||||
|
);
|
||||||
|
|
||||||
|
div.dispatchEvent(
|
||||||
|
new KeyboardEvent('keydown', {
|
||||||
|
keyCode: 40,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
div.dispatchEvent(
|
||||||
|
new KeyboardEvent('keyup', {
|
||||||
|
keyCode: 40,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
div.dispatchEvent(
|
||||||
|
new KeyboardEvent('keypress', {
|
||||||
|
charCode: 40,
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(LogUtils.getAndClear()).toEqual([
|
||||||
|
'keydown handle',
|
||||||
|
'keyup handle',
|
||||||
|
'keypress handle'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,160 @@
|
||||||
|
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||||
|
import * as LogUtils from '../jest/logUtils';
|
||||||
|
|
||||||
|
describe('MouseEvent Test', () => {
|
||||||
|
describe('onClick Test', () => {
|
||||||
|
it('绑定this', () => {
|
||||||
|
class App extends Horizon.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {
|
||||||
|
num: this.props.num,
|
||||||
|
price: this.props.price
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
setNum() {
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
num: this.state.num + 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
setPrice = (e) => {
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
num: this.state.price + 1
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<p>{this.state.num}</p>
|
||||||
|
<p id="p">{this.state.price}</p>
|
||||||
|
<button onClick={this.setNum.bind(this)} >button</button>
|
||||||
|
<button id="btn" onClick={() => this.setPrice()} >button</button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Horizon.render(<App num={0} price={100} />, container);
|
||||||
|
expect(container.querySelector('p').innerHTML).toBe('0');
|
||||||
|
expect(container.querySelector('#p').innerHTML).toBe('100');
|
||||||
|
// 点击按钮触发num加1
|
||||||
|
container.querySelector('button').click();
|
||||||
|
expect(container.querySelector('p').innerHTML).toBe('1');
|
||||||
|
|
||||||
|
container.querySelector('#btn').click();
|
||||||
|
expect(container.querySelector('p').innerHTML).toBe('101');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('点击触发', () => {
|
||||||
|
const handleClick = jest.fn();
|
||||||
|
Horizon.render(<button onClick={handleClick}>Click Me</button>, container)
|
||||||
|
container.querySelector('button').click();
|
||||||
|
expect(handleClick).toHaveBeenCalledTimes(1);
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
container.querySelector('button').click();
|
||||||
|
}
|
||||||
|
expect(handleClick).toHaveBeenCalledTimes(6);
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const test = (name, config) => {
|
||||||
|
const node = Horizon.render(config, container);
|
||||||
|
let event = new MouseEvent(name, {
|
||||||
|
relatedTarget: null,
|
||||||
|
bubbles: true,
|
||||||
|
screenX: 1
|
||||||
|
});
|
||||||
|
node.dispatchEvent(event);
|
||||||
|
|
||||||
|
expect(LogUtils.getAndClear()).toEqual([
|
||||||
|
`${name} capture`,
|
||||||
|
`${name} bubble`
|
||||||
|
]);
|
||||||
|
|
||||||
|
event = new MouseEvent(name, {
|
||||||
|
relatedTarget: null,
|
||||||
|
bubbles: true,
|
||||||
|
screenX: 2
|
||||||
|
});
|
||||||
|
node.dispatchEvent(event);
|
||||||
|
|
||||||
|
// 再次触发新事件
|
||||||
|
expect(LogUtils.getAndClear()).toEqual([
|
||||||
|
`${name} capture`,
|
||||||
|
`${name} bubble`
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('合成鼠标事件', () => {
|
||||||
|
it('onMouseMove', () => {
|
||||||
|
const onMouseMove = () => {
|
||||||
|
LogUtils.log('mousemove bubble');
|
||||||
|
};
|
||||||
|
const onMouseMoveCapture = () => {
|
||||||
|
LogUtils.log('mousemove capture');
|
||||||
|
};
|
||||||
|
test('mousemove', <div
|
||||||
|
onMouseMove={onMouseMove}
|
||||||
|
onMouseMoveCapture={onMouseMoveCapture}
|
||||||
|
/>)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onMouseDown', () => {
|
||||||
|
const onMousedown = () => {
|
||||||
|
LogUtils.log('mousedown bubble');
|
||||||
|
};
|
||||||
|
const onMousedownCapture = () => {
|
||||||
|
LogUtils.log('mousedown capture');
|
||||||
|
};
|
||||||
|
test('mousedown', <div
|
||||||
|
onMousedown={onMousedown}
|
||||||
|
onMousedownCapture={onMousedownCapture}
|
||||||
|
/>)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onMouseUp', () => {
|
||||||
|
const onMouseUp = () => {
|
||||||
|
LogUtils.log('mouseup bubble');
|
||||||
|
};
|
||||||
|
const onMouseUpCapture = () => {
|
||||||
|
LogUtils.log('mouseup capture');
|
||||||
|
};
|
||||||
|
test('mouseup', <div
|
||||||
|
onMouseUp={onMouseUp}
|
||||||
|
onMouseUpCapture={onMouseUpCapture}
|
||||||
|
/>)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onMouseOut', () => {
|
||||||
|
const onMouseOut = () => {
|
||||||
|
LogUtils.log('mouseout bubble');
|
||||||
|
};
|
||||||
|
const onMouseOutCapture = () => {
|
||||||
|
LogUtils.log('mouseout capture');
|
||||||
|
};
|
||||||
|
test('mouseout', <div
|
||||||
|
onMouseOut={onMouseOut}
|
||||||
|
onMouseOutCapture={onMouseOutCapture}
|
||||||
|
/>)
|
||||||
|
});
|
||||||
|
|
||||||
|
it('onMouseOver', () => {
|
||||||
|
const onMouseOver = () => {
|
||||||
|
LogUtils.log('mouseover bubble');
|
||||||
|
};
|
||||||
|
const onMouseOverCapture = () => {
|
||||||
|
LogUtils.log('mouseover capture');
|
||||||
|
};
|
||||||
|
test('mouseover', <div
|
||||||
|
onMouseOver={onMouseOver}
|
||||||
|
onMouseOverCapture={onMouseOverCapture}
|
||||||
|
/>)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
|
@ -0,0 +1,52 @@
|
||||||
|
import * as Horizon from '@cloudsop/horizon/index.ts';
|
||||||
|
import * as LogUtils from '../jest/logUtils';
|
||||||
|
|
||||||
|
describe('合成滚轮事件', () => {
|
||||||
|
it('onWheel', () => {
|
||||||
|
const realNode = Horizon.render(
|
||||||
|
<div
|
||||||
|
onWheel={event => LogUtils.log(`onWheel: ${event.type}`)}
|
||||||
|
onWheelCapture={event => LogUtils.log(`onWheelCapture: ${event.type}`)}
|
||||||
|
/>, container);
|
||||||
|
|
||||||
|
realNode.dispatchEvent(
|
||||||
|
new MouseEvent('wheel', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(LogUtils.getAndClear()).toEqual([
|
||||||
|
'onWheelCapture: wheel',
|
||||||
|
'onWheel: wheel'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('可以执行preventDefault和 stopPropagation', () => {
|
||||||
|
const eventHandler = e => {
|
||||||
|
expect(e.isDefaultPrevented()).toBe(false);
|
||||||
|
e.preventDefault();
|
||||||
|
expect(e.isDefaultPrevented()).toBe(true);
|
||||||
|
|
||||||
|
expect(e.isPropagationStopped()).toBe(false);
|
||||||
|
e.stopPropagation();
|
||||||
|
expect(e.isPropagationStopped()).toBe(true);
|
||||||
|
LogUtils.log(e.type + ' handle');
|
||||||
|
};
|
||||||
|
const realNode = Horizon.render(
|
||||||
|
<div onWheel={eventHandler}/>,
|
||||||
|
container
|
||||||
|
);
|
||||||
|
|
||||||
|
realNode.dispatchEvent(
|
||||||
|
new MouseEvent('wheel', {
|
||||||
|
bubbles: true,
|
||||||
|
cancelable: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(LogUtils.getAndClear()).toEqual([
|
||||||
|
'wheel handle'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
})
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { allDelegatedNativeEvents } from '../../../libs/horizon/src/event/EventCollection';
|
||||||
|
import * as LogUtils from './logUtils';
|
||||||
|
|
||||||
|
export const stopBubbleOrCapture = (e, value) => {
|
||||||
|
LogUtils.log(value)
|
||||||
|
e.stopPropagation();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEventListeners = (dom) => {
|
||||||
|
let ret = true
|
||||||
|
let keyArray = [];
|
||||||
|
for (var key in dom) {
|
||||||
|
keyArray.push(key);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
allDelegatedNativeEvents.forEach(event => {
|
||||||
|
if (!keyArray.includes(event)) {
|
||||||
|
ret = false;
|
||||||
|
throw new Error('没有挂载全量事件');
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
};
|
Loading…
Reference in New Issue