chore: fix conflict
This commit is contained in:
commit
c9bb4c07de
|
@ -1,17 +1,8 @@
|
|||
---
|
||||
name: 模板名称,在新建pr时候能看到
|
||||
about: 模板描述,对应的pr模板卡片展示时候能看到,介绍模板
|
||||
---
|
||||
**PR 描述:** [请描述提交此 PR 的背景、目的、所做的更改以及如何测试此 PR]
|
||||
|
||||
**关联的 Issues:** [请列出与此 PR 相关的 issue 编号]
|
||||
|
||||
**TODO(可选)**
|
||||
- [ ] 任务1
|
||||
- [ ] ...
|
||||
|
||||
**检查项:**
|
||||
|
||||
**检查项(无需修改,提交后界面上可勾选):**
|
||||
- [ ] 代码已经被检视
|
||||
- [ ] 代码符合项目的代码标准和最佳实践
|
||||
- [ ] 代码已经通过所有测试用例
|
||||
|
|
|
@ -1,17 +1,8 @@
|
|||
---
|
||||
name: The template name can be seen when creating a new PR
|
||||
about: Template description, which can be seen when displaying the corresponding PR template card
|
||||
---
|
||||
**Description:** [Please describe the background, purpose, changes made, and how to test this PR]
|
||||
|
||||
**Related Issues:** [List the issue numbers related to this PR]
|
||||
|
||||
**TODO(optional)**
|
||||
- [ ] task1
|
||||
- [ ] ...
|
||||
|
||||
**Checklist:**
|
||||
|
||||
- [ ] Code has been reviewed
|
||||
- [ ] Code complies with the project's code standards and best practices
|
||||
- [ ] Code has passed all tests
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
## 代码注释
|
||||
|
||||
为提高代码的可读性,我们希望你能按照以下几条规则编写注释,
|
||||
|
||||
- 核心:```不要解释是什么,而是回答为什么```
|
||||
|
||||
- 统一的注释风格
|
||||
|
||||
``` tsx
|
||||
/**
|
||||
* 这是块级注释
|
||||
*/
|
||||
|
||||
// 这是行内注释
|
||||
|
||||
//============================== 分隔符 ==============================
|
||||
```
|
||||
|
||||
- 注释不要重复代码内容
|
||||
|
||||
``` tsx
|
||||
// 注释和代码含义相同,无需添加
|
||||
// error是个promise
|
||||
if (isPromise(error)) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
- 注释不要解释模糊代码
|
||||
|
||||
``` tsx
|
||||
// 最佳节点
|
||||
let n: Node | null = null;
|
||||
```
|
||||
|
||||
直接在代码中通过名称表达清楚,可以省略掉注释
|
||||
|
||||
``` tsx
|
||||
let bestNode: Node | null = null;
|
||||
```
|
||||
|
||||
如果添加注释则是补充性的额外信息
|
||||
|
||||
```tsx
|
||||
// 全局指针,在遍历时指向当前最佳的节点
|
||||
let bestNode: VNode | null = null;
|
||||
```
|
||||
|
||||
- 巧妙或复杂的代码要添加注释解释逻辑
|
||||
|
||||
- 对所有导出的顶层模块进行注释
|
||||
|
||||
- 注释应该是直接的,不要有不明确的表达或符号
|
77
README.md
77
README.md
|
@ -6,75 +6,82 @@
|
|||
|
||||
## 技术架构
|
||||
|
||||

|
||||

|
||||
|
||||
### 核心能力
|
||||
## 核心能力
|
||||
|
||||
**响应式API**
|
||||
### 响应式API
|
||||
|
||||
* openInula 通过最小化重新渲染的范围,从而进行高效的UI渲染。这种方式避免了虚拟 DOM 的开销,使得 openInula 在性能方面表现出色。
|
||||
* openInula 通过比较变化前后的 JavaScript 对象以细粒度的依赖追踪机制来实现响应式更新,无需用户过度关注性能优化。
|
||||
* 简洁API:
|
||||
1. openInula 提供了两组简洁直观的API--响应式 API 和与 React 一致的传统 API,使得开发者可以轻松地构建复杂的交互式界面。
|
||||
2. openInula 简洁的 API 极大降低了开发者的学习成本,开发者使用响应式API可以快速构建高效的前端界面。
|
||||
openInula 通过监听状态变量的变化,以细粒度的依赖追踪机制来实现响应式更新,避免了虚拟 DOM 的开销。通过最小化重新渲染的范围,从而进行高效的UI渲染。无需用户过度关注性能优化。
|
||||
|
||||
**兼容 ReactAPI**
|
||||
>(实验性功能,可在 `reactive` 分支查看代码或使用 npm 仓中 experimental 版本体验)
|
||||
|
||||
* 与 React 保持一致 API 的特性、可以无缝支持 React 生态。
|
||||
* 使用传统 API 可以无缝将 React 项目切换至 openInula,React 应用可零修改切换至 openInula。
|
||||
### 兼容 React API
|
||||
|
||||
提供与 React 一致的 API,完全支持 React 生态,可将 React 应用可零修改切换至 openInula。
|
||||
|
||||
### openInula 配套组件
|
||||
|
||||
**状态管理器/inula-X**
|
||||
#### 状态管理器 inula-X
|
||||
|
||||
inula-X 是 openInula 默认提供的状态管理器,无需额外引入三方库,就可以简单实现跨组件/页面共享状态。
|
||||
inula-X 与 Redux 比可创建多个 Store,不需要在 Reducer 中返回 state 并且简化了 Action 和 Reducer 的创建步骤,原生支持异步能力,组件能做到精准重渲染。inula-X 均可使用函数组件、class 组件,能提供 redux 的适配接口及支持响应式的特点。
|
||||
inula-X 是 openInula 默认提供的状态管理器。无需额外引入三方库,就可以简单实现跨组件/页面共享状态。
|
||||
|
||||
**路由/inula-router**
|
||||
inula-X 与 Redux 相比,可创建多个 Store,不需要在 Reducer 中返回 state 并且简化了 Action 和 Reducer 的创建步骤,原生支持异步能力,组件能做到精准重渲染。inula-X 均可使用函数组件、class 组件,能提供 redux 的适配接口及支持响应式的特点。
|
||||
|
||||
inula-router 是 openInula 生态组建的一部分,为 openInula 提供前端路由的能力,是构建大型应用必要组件。
|
||||
inula-router 涵盖 react-router、history、connect-react-router 的功能。
|
||||
#### 路由 inula-router
|
||||
|
||||
**请求/inula-request**
|
||||
inula-router 为 openInula 提供前端路由的能力,是构建大型应用必要组件,涵盖 react-router、history、connect-react-router 的功能。
|
||||
|
||||
inula-request 是 openInula 生态组件,涵盖常见的网络请求方式,并提供动态轮询钩子函数给用户更便捷的定制化请求体验。
|
||||
#### 请求 inula-request
|
||||
|
||||
**国际化/inula-intl**
|
||||
inula-request 是 openInula 的网络请求组件,不仅涵盖常见的网络请求方式,还提供动态轮询钩子函数给用户更便捷的定制化请求体验。
|
||||
|
||||
inula-intl 是基于 openInula 生态组件,其主要提供了国际化功能,涵盖了基本的国际化组件和钩子函数,便于用户在构建国际化能力时方便操作。
|
||||
#### 国际化 inula-intl
|
||||
|
||||
**调试工具/inula-dev-tools**
|
||||
inula-intl 是基于 openInula 的国际化组件,涵盖了基本的国际化组件和钩子函数,允许用户更方便地构建国际化能力。
|
||||
|
||||
#### 调试工具 inula-dev-tools
|
||||
|
||||
inula-dev-tools 是一个为 openInula 开发者提供的强大工具集,能够方便地查看和编辑组件树、管理应用状态以及进行性能分析,极大提高了开发效率和诊断问题的便捷性。
|
||||
|
||||
**脚手架/inula-cli**
|
||||
#### 脚手架 create-inula
|
||||
|
||||
inula-cli 是一套针对 openInula 的编译期插件,它支持代码优化、JSX 语法转换以及代码分割,有助于提高应用的性能、可读性和可维护性。
|
||||
create-inula 是一套用于创建 openInula 项目的脚手架工具。它预置了一系列项目模板,允许开发者通过命令行按需快速生成可运行的项目代码。
|
||||
|
||||
## openInula 文档
|
||||
## 参与贡献
|
||||
|
||||
我们鼓励开发者以各种方式参与代码贡献、生态拓展或文档反馈,献您的原创内容,详细请参考[贡献指南](https://docs.openinula.net/docs/%E8%B4%A1%E7%8C%AE%E6%8C%87%E5%8D%97)。
|
||||
|
||||
### 官方链接
|
||||
|
||||
欢迎访问 openInula 官网与文档仓库,参与 openInula 开发者文档开源项目,与我们一起完善开发者文档。
|
||||
|
||||
openInula 官网地址:[https://www.openinula.net/](https://www.openinula.net/)
|
||||
openInula 文档站地址:[https://docs.openinula.net/](https://docs.openinula.net/)
|
||||
* openInula 官网:[https://www.openinula.net/](https://www.openinula.net/)
|
||||
* openInula 文档:[https://docs.openinula.net/](https://docs.openinula.net/)
|
||||
* openInula 仓库地址:[https://gitee.com/openinula/inula](https://gitee.com/openinula/inula)
|
||||
* openInula 社提案备忘录(RFC):[https://gitee.com/openInula/rfcs](https://gitee.com/openInula/rfcs)
|
||||
|
||||
## 代码仓地址
|
||||
### 社区贡献者案例
|
||||
|
||||
openInula 仓库地址:[https://gitee.com/openinula](https://gitee.com/openinula)
|
||||
**[`umi-inula`](https://gitee.com/congxiaochen/inula)**
|
||||
|
||||
## 如何参与
|
||||
基于 umijs 与 openInula 的开发框架,集成官方组件与UI、AIGC等功能,开箱即用。
|
||||
|
||||
**参与贡献**
|
||||
欢迎您参与贡献,我们鼓励开发者以各种方式参与文档反馈和贡献。
|
||||
**[`VoerkaI18n`](https://github.com/zhangfisher/voerka-i18n/)**
|
||||
|
||||
您可以对现有文档进行评价、简单更改、反馈文档质量问题、贡献您的原创内容,详细请参考[贡献指南](https://docs.openinula.net/docs/%E8%B4%A1%E7%8C%AE%E6%8C%87%E5%8D%97)。
|
||||
适用于多框架的 JavaScript 国际化解决方案,提供对 openInula 的适配。
|
||||
|
||||
- [适配示例](https://gitee.com/link?target=https%3A%2F%2Fgithub.com%2Fzhangfisher%2Fvoerka-i18n%2Ftree%2Fmaster%2Fexamples%2Fopeninula)
|
||||
- [适配文档](https://gitee.com/link?target=https%3A%2F%2Fzhangfisher.github.io%2Fvoerka-i18n%2F%23%2Fzh%2Fguide%2Fintegration%2Fopeninula)
|
||||
|
||||
## 许可协议
|
||||
|
||||
openInula 主要遵循 Mulan Permissive Software License v2 协议,详情请参考各代码仓 LICENSE 声明。
|
||||
openInula 主要遵循 [Mulan Permissive Software License v2](http://license.coscl.org.cn/MulanPSL2) 协议,详情请参考各代码仓 LICENSE 声明。
|
||||
|
||||
## 联系方式
|
||||
|
||||
team@inulajs.org
|
||||
* 官方邮箱: [team@inulajs.org](mailto:team@inulajs.org)
|
||||
|
||||
* 微信公众号:
|
||||
|
||||

|
||||
|
|
|
@ -1,3 +1,28 @@
|
|||
# 0.0.2 版本
|
||||
|
||||
## 新特性
|
||||
|
||||
- **inula-request** 新增响应体中获取完整 URL 能力。
|
||||
|
||||
## API变更
|
||||
|
||||
无
|
||||
|
||||
## Bug修复
|
||||
|
||||
- **inula** 解决事件卸载失败问题。
|
||||
- **inula** 解决 mouseover 重复触发 mouseEnter 事件问题。
|
||||
- **inula** 大数组合并使用 concat。
|
||||
- **inula** 事件支持 defaultPrevented 属性
|
||||
|
||||
## CVE漏洞修复
|
||||
|
||||
无
|
||||
|
||||
## 已知问题
|
||||
|
||||
无
|
||||
|
||||
# 0.0.1 版本
|
||||
|
||||
## 新特性
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "openinula",
|
||||
"name": "inula",
|
||||
"description": "OpenInula is a JavaScript framework library.",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
|
|
|
@ -128,9 +128,9 @@ class BasicGenerator extends Generator {
|
|||
if (fs.existsSync(fullpath)) {
|
||||
this.traverseDirCapture(fullpath, dirCallback, fileCallback);
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
fileCallback(fullpath);
|
||||
}
|
||||
fileCallback(fullpath);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -140,9 +140,10 @@ class BasicGenerator extends Generator {
|
|||
if (fs.lstatSync(fullpath).isDirectory()) {
|
||||
this.traverseDirBubble(fullpath, dirCallback, fileCallback);
|
||||
dirCallback(fullpath);
|
||||
continue;
|
||||
}
|
||||
fileCallback(fullpath);
|
||||
else{
|
||||
fileCallback(fullpath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
# openinula + vite
|
||||
|
||||
该模板提供了 `openinula` 工作在 `vite`的基础配置。
|
||||
> 请注意由于Vite插件有node版本限制,请使用`node -v`命令确认node版本大于等于node v18。
|
|
@ -11,7 +11,7 @@
|
|||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"openinula": "^0.0.11"
|
||||
"openinula": "^0.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.4",
|
||||
|
|
|
@ -10,7 +10,7 @@
|
|||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"openinula": "^0.0.11"
|
||||
"openinula": "^0.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.4",
|
||||
|
|
|
@ -17,7 +17,7 @@ const path = require('path');
|
|||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.js',
|
||||
entry: './src/index.jsx',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'bundle.js',
|
||||
|
@ -78,4 +78,7 @@ module.exports = {
|
|||
port: 9000,
|
||||
open: true,
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const BasicGenerator = require('../../BasicGenerator');
|
||||
|
||||
class Generator extends BasicGenerator {
|
||||
prompting() {
|
||||
return this.prompt([
|
||||
{
|
||||
type: 'list',
|
||||
name: 'bundlerType',
|
||||
message: 'Please select the build type',
|
||||
choices: ['webpack', 'vite'],
|
||||
},
|
||||
]).then(props => {
|
||||
this.prompts = props;
|
||||
});
|
||||
}
|
||||
|
||||
writing() {
|
||||
const src = this.templatePath(this.prompts.bundlerType);
|
||||
const dest = this.destinationPath();
|
||||
this.writeFiles(src, dest, {
|
||||
context: {
|
||||
...this.prompts,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Generator;
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"description": "simple reactive app template."
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
# openinula + vite
|
||||
|
||||
该模板提供了 `openinula` 工作在 `vite`的基础配置。
|
||||
> 请注意由于Vite插件有node版本限制,请使用`node -v`命令确认node版本大于等于node v18。
|
|
@ -0,0 +1,11 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>My Inula App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/index.jsx"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "inula-vite-app",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "vite",
|
||||
"build": "vite build"
|
||||
},
|
||||
"keywords": [],
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"openinula": "0.0.0-experimental-20231201"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.4",
|
||||
"@babel/preset-env": "^7.21.4",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"@vitejs/plugin-react": "^3.1.0",
|
||||
"@vitejs/plugin-react-refresh": "^1.3.6",
|
||||
"babel-plugin-import": "^1.13.6",
|
||||
"vite": "^4.2.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 Inula, { useComputed, useReactive, useRef } from 'openinula';
|
||||
|
||||
function ReactiveComponent() {
|
||||
const renderCount = ++useRef(0).current;
|
||||
|
||||
const data = useReactive({ count: 0 });
|
||||
const countText = useComputed(() => {
|
||||
return `计时: ${data.count.get()}`;
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
data.count.set(c => c + 1);
|
||||
}, 1000);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{countText}</div>
|
||||
<div>组件渲染次数:{renderCount}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReactiveComponent;
|
|
@ -0,0 +1,57 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #fff;
|
||||
background: linear-gradient(120deg, #6a11cb 0%, #2575fc 100%);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3em;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 1vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.card h2,
|
||||
.card p {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card a {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* 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 Inula from 'openinula';
|
||||
import ReactiveComponent from './ReactiveComponent';
|
||||
import './index.css';
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div class="container">
|
||||
<div class="hero">
|
||||
<h1 class="hero-title animate__animated animate__bounceInDown">欢迎来到 Inula 项目!</h1>
|
||||
<p class="hero-subtitle animate__animated animate__bounceInUp">你已成功创建你的第一个响应式 Inula 项目</p>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="card animate__animated animate__zoomIn">
|
||||
<ReactiveComponent/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="card animate__animated animate__zoomIn">
|
||||
<h2>了解更多</h2>
|
||||
<p>
|
||||
要了解 Inula,查看{' '}
|
||||
<a href="https://openinula.com/" target="_blank">Inula 官网</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Inula.render(<App />, document.getElementById('root'));
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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 react from '@vitejs/plugin-react';
|
||||
|
||||
let alias = {
|
||||
react: 'openinula', // 新增
|
||||
'react-dom': 'openinula', // 新增
|
||||
'react/jsx-dev-runtime': 'openinula/jsx-dev-runtime',
|
||||
};
|
||||
|
||||
export default {
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias,
|
||||
},
|
||||
};
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"name": "inula-webpack-app",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"start": "webpack serve --mode development",
|
||||
"build": "webpack --mode production"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"openinula": "0.0.0-experimental-20231201"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.4",
|
||||
"@babel/preset-env": "^7.21.4",
|
||||
"@babel/preset-react": "^7.18.6",
|
||||
"babel-loader": "^9.1.2",
|
||||
"css-loader": "^6.7.3",
|
||||
"file-loader": "^6.2.0",
|
||||
"html-webpack-plugin": "^5.5.0",
|
||||
"style-loader": "^3.3.2",
|
||||
"url-loader": "^4.1.1",
|
||||
"webpack": "^5.77.0",
|
||||
"webpack-cli": "^5.0.1",
|
||||
"webpack-dev-server": "^4.13.2"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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 Inula from 'openinula';
|
||||
import ReactiveComponent from './ReactiveComponent';
|
||||
import './styles.css';
|
||||
|
||||
class App extends Inula.Component {
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
<div class="hero">
|
||||
<h1 class="hero-title animate__animated animate__bounceInDown">欢迎来到 Inula 项目!</h1>
|
||||
<p class="hero-subtitle animate__animated animate__bounceInUp">你已成功创建你的第一个响应式 Inula 项目</p>
|
||||
</div>
|
||||
<div className="content">
|
||||
<div className="card animate__animated animate__zoomIn">
|
||||
<ReactiveComponent/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="card animate__animated animate__zoomIn">
|
||||
<h2>了解更多</h2>
|
||||
<p>
|
||||
要了解 Inula,查看{' '}
|
||||
<a href="https://openinula.org" target="_blank">Inula 官网</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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 Inula, { useComputed, useReactive, useRef } from 'openinula';
|
||||
|
||||
function ReactiveComponent() {
|
||||
const renderCount = ++useRef(0).current;
|
||||
|
||||
const data = useReactive({ count: 0 });
|
||||
const countText = useComputed(() => {
|
||||
return `计时: ${data.count.get()}`;
|
||||
});
|
||||
|
||||
setInterval(() => {
|
||||
data.count.set(c => c + 1);
|
||||
}, 1000);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>{countText}</div>
|
||||
<div>组件渲染次数:{renderCount}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReactiveComponent;
|
|
@ -0,0 +1,11 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Inula App</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="../dist/bundle.js"></script>
|
||||
</body>
|
||||
</html>
|
|
@ -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 Inula from 'openinula';
|
||||
import App from './App';
|
||||
|
||||
Inula.render(<App />, document.getElementById('root'));
|
|
@ -0,0 +1,57 @@
|
|||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
line-height: 1.6;
|
||||
color: #fff;
|
||||
background: linear-gradient(120deg, #6a11cb 0%, #2575fc 100%);
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.hero-title {
|
||||
font-size: 3em;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.hero-subtitle {
|
||||
font-size: 1.5em;
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin: 1vh;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
box-shadow: 0px 8px 15px rgba(0, 0, 0, 0.1);
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
|
||||
.card h2,
|
||||
.card p {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card a {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const path = require('path');
|
||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
||||
|
||||
module.exports = {
|
||||
entry: './src/index.jsx',
|
||||
output: {
|
||||
path: path.resolve(__dirname, 'dist'),
|
||||
filename: 'bundle.js',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|jsx)$/,
|
||||
exclude: /node_modules/,
|
||||
use: {
|
||||
loader: 'babel-loader',
|
||||
options: {
|
||||
presets: [
|
||||
'@babel/preset-env',
|
||||
[
|
||||
'@babel/preset-react',
|
||||
{
|
||||
runtime: 'automatic', // 新增
|
||||
importSource: 'openinula', // 新增
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['style-loader', 'css-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.(png|jpe?g|gif)$/i,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[name].[ext]',
|
||||
outputPath: 'images/',
|
||||
publicPath: 'images/',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(woff|woff2|eot|ttf|otf)$/,
|
||||
use: ['file-loader'],
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
template: path.resolve(__dirname, 'src/index.html'),
|
||||
filename: 'index.html',
|
||||
}),
|
||||
],
|
||||
devServer: {
|
||||
static: path.join(__dirname, 'dist'),
|
||||
compress: true,
|
||||
port: 9000,
|
||||
open: true,
|
||||
},
|
||||
resolve: {
|
||||
extensions: ['.mjs', '.js', '.mts', '.ts', '.jsx', '.tsx', '.json'],
|
||||
},
|
||||
};
|
|
@ -62,7 +62,17 @@ const run = async config => {
|
|||
}
|
||||
process.emit('message', { type: 'prompt' });
|
||||
|
||||
let { type } = config;
|
||||
let { type, name } = config;
|
||||
if (!name) {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
name: 'projectName',
|
||||
message: 'Project name',
|
||||
type: 'input'
|
||||
},
|
||||
]);
|
||||
config.name = answers.projectName;
|
||||
}
|
||||
if (!type) {
|
||||
const answers = await inquirer.prompt([
|
||||
{
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
{
|
||||
"name": "create-inula",
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.8",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"bin": {
|
||||
"create-inula": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 18.0.0"
|
||||
},
|
||||
"files": [
|
||||
"bin",
|
||||
"lib",
|
||||
|
|
|
@ -2,23 +2,23 @@
|
|||
|
||||
## 一、安装使用
|
||||
|
||||
### 安装Nodejs
|
||||
### 安装Node.js
|
||||
|
||||
inula-cli的运行需要依赖Nodejs,使用前请确保您的电脑已安装Nodejs,并且版本在16以上。您可以通过在控制台执行以下命令来确认您的版本。
|
||||
inula-cli的运行需要依赖Node.js,使用前请确保您的电脑已安装Node.js,并且版本在16以上。您可以通过在控制台执行以下命令来确认您的版本。
|
||||
|
||||
```
|
||||
```shell
|
||||
>node -v
|
||||
|
||||
v16.4.0
|
||||
```
|
||||
|
||||
如果您没有安装Nodejs,或者Nodejs版本不满足条件,推荐使用nvm工具安装和管理Nodejs版本。
|
||||
如果您没有安装Node.js,或者Node.js版本不满足条件,推荐使用nvm工具安装和管理Node.js版本。
|
||||
|
||||
nvm最新版本下载: [https://github.com/coreybutler/nvm-windows/releases](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2Fcoreybutler%2Fnvm-windows%2Freleases)
|
||||
|
||||
安装nvm之后,可以通过如下命令安装Nodejs:
|
||||
安装nvm之后,可以通过如下命令安装Node.js:
|
||||
|
||||
```
|
||||
```shell
|
||||
>node install 16
|
||||
|
||||
>node use 16
|
||||
|
@ -28,15 +28,15 @@ nvm最新版本下载: [https://github.com/coreybutler/nvm-windows/releases](htt
|
|||
|
||||
### 安装inula-cli
|
||||
|
||||
为了方便使用inula-cli的功能,推荐您全局安装inula-cli。Nodejs安装会自带npm工具用于管理模块,您可以直接运行如下命令:
|
||||
为了方便使用inula-cli的功能,推荐您全局安装inula-cli。Node.js安装会自带npm工具用于管理模块,您可以直接运行如下命令:
|
||||
|
||||
```
|
||||
```shell
|
||||
>npm install -g inula-cli
|
||||
```
|
||||
|
||||
安装完成后,使用inula-cli version命令确认安装是否完成。
|
||||
|
||||
```
|
||||
```shell
|
||||
>inula-cli version
|
||||
1.1.0
|
||||
```
|
||||
|
@ -93,7 +93,7 @@ inula-cli支持用户通过项目根目录下的.inula.ts或者.inula.js文件
|
|||
|
||||
在配置文件中,您需要默认导出一个配置,以下为一个简单的配置文件示例:
|
||||
|
||||
```
|
||||
```typescript
|
||||
// .inula.ts
|
||||
|
||||
export default {
|
||||
|
@ -113,7 +113,7 @@ export default {
|
|||
|
||||
对于TypeScript类型,我们也提供了类型定义以供开发时自动补全:
|
||||
|
||||
```
|
||||
```typescript
|
||||
// .inula.ts
|
||||
|
||||
import { defineConfig } from "inula-cli"
|
||||
|
@ -164,8 +164,8 @@ inula-cli的所有功能都围绕插件展开,插件可以很方便地让用
|
|||
|
||||
内置插件在inula-cli运行时会自动加载,用户可以直接调用这些内置命令,当前支持的内置插件功能如下:
|
||||
|
||||
| 序号 | 插件功能 | 触发命令 |
|
||||
| ---- | :----------------------- | ------------------- |
|
||||
| 序号 | 插件功能 | 触发命令 |
|
||||
| ---- | :----------------------- | ----------------- |
|
||||
| 1、 | 本地开发构建 | inula-cli dev |
|
||||
| 2、 | 生产构建 | inula-cli build |
|
||||
| 3、 | 接口mock能力 | inula-cli dev |
|
||||
|
@ -180,13 +180,13 @@ inula-cli支持用户集成已发布在npm仓库的插件,用户可以按需
|
|||
|
||||
安装可以通过npm安装,这里以插件@inula/add为例:
|
||||
|
||||
```
|
||||
```shell
|
||||
npm i --save-dev @inula/add
|
||||
```
|
||||
|
||||
如果需要运行插件,需要在配置文件中配置对应的插件路径
|
||||
|
||||
```
|
||||
```typescript
|
||||
// .inula.ts
|
||||
|
||||
export default {
|
||||
|
@ -205,7 +205,7 @@ export default {
|
|||
|
||||
1、编写命令插件文件,这里我们自定义了一个conf命令用于展示当前项目的配置信息。
|
||||
|
||||
```
|
||||
```typescript
|
||||
// conf.ts
|
||||
|
||||
import { API } from "inula-cli";
|
||||
|
@ -224,7 +224,7 @@ export default (api: API) => {
|
|||
|
||||
2、在配置文件中加入对插件的引用
|
||||
|
||||
```
|
||||
```typescript
|
||||
// .inula.ts
|
||||
|
||||
export default {
|
||||
|
@ -232,9 +232,9 @@ export default {
|
|||
}
|
||||
```
|
||||
|
||||
3、在项目根目录下执行inula-cli conf即可触发插件运行。
|
||||
3、在项目根目录下执行`inula-cli conf`即可触发插件运行。
|
||||
|
||||
```
|
||||
```shell
|
||||
> inula-cli conf
|
||||
current user config is: {
|
||||
plugins: [ './conf', './showConf' ],
|
||||
|
@ -247,7 +247,7 @@ inula-cli提供了hook机制可以让开发者在执行命令时实现事件监
|
|||
|
||||
1、使用插件注册hook
|
||||
|
||||
```
|
||||
```typescript
|
||||
// modifyConfig.ts
|
||||
|
||||
import { API } from "inula-cli";
|
||||
|
@ -267,7 +267,7 @@ export default (api: API) => {
|
|||
|
||||
2、在插件中触发hook
|
||||
|
||||
```
|
||||
```typescript
|
||||
// conf.ts
|
||||
|
||||
import { API } from "inula-cli";
|
||||
|
@ -287,7 +287,7 @@ export default (api: API) => {
|
|||
|
||||
3、在配置文件中加入插件
|
||||
|
||||
```
|
||||
```typescript
|
||||
// .inula.ts
|
||||
|
||||
export default {
|
||||
|
@ -297,7 +297,7 @@ export default {
|
|||
|
||||
4、触发命令
|
||||
|
||||
```
|
||||
```shell
|
||||
> inula-cli conf
|
||||
current user config is: {
|
||||
plugins: [ './conf', './showConf' ],
|
||||
|
@ -327,7 +327,7 @@ current user config is: {
|
|||
|
||||
registerCommand方法允许用户自定义inula-cli的执行命令,
|
||||
|
||||
```
|
||||
```typescript
|
||||
api.registerCommand({
|
||||
name: string,
|
||||
description?: string,
|
||||
|
@ -343,7 +343,7 @@ registerCommand方法允许用户自定义inula-cli的执行命令,
|
|||
|
||||
使用示例:
|
||||
|
||||
```
|
||||
```typescript
|
||||
import { API } from "inula-cli";
|
||||
|
||||
export default (api: API) => {
|
||||
|
@ -374,7 +374,7 @@ api.registerHook({
|
|||
|
||||
使用示例:
|
||||
|
||||
```
|
||||
```typescript
|
||||
import { API } from "inula-cli";
|
||||
|
||||
export default (api: API) => {
|
||||
|
@ -403,7 +403,7 @@ applyHook(name: string, value?: any})
|
|||
|
||||
使用示例:
|
||||
|
||||
```
|
||||
```typescript
|
||||
import { API } from "inula-cli";
|
||||
|
||||
export default (api: API) => {
|
||||
|
@ -427,7 +427,7 @@ export default (api: API) => {
|
|||
|
||||
inula-cli默认集成生产构建能力,用户可以通过在.inula.ts中配置buildConfig字段启用功能。配置示例如下:
|
||||
|
||||
```
|
||||
```typescript
|
||||
// .inula.ts
|
||||
|
||||
// 使用webpack构建
|
||||
|
@ -460,7 +460,7 @@ export default {
|
|||
|
||||
生产构建支持传入多个配置文件路径,使用webpack构建还支持配置文件以函数方式导出,inula-cli会将配置中的env和args作为参数传递到函数中执行以获取最后的构建配置。
|
||||
|
||||
```
|
||||
```typescript
|
||||
// webpack.config.js
|
||||
|
||||
module.exports = function (env, argv) {
|
||||
|
@ -497,7 +497,7 @@ export default {
|
|||
|
||||
inula-cli默认也支持项目本地构建,用户可以通过在.inula.ts中配置devBuildConfig字段启用功能。配置示例如下:
|
||||
|
||||
```
|
||||
```typescript
|
||||
// .inula.ts
|
||||
|
||||
// 使用webpack构建
|
||||
|
@ -552,7 +552,7 @@ inula-cli自动将项目根路径里/Mock目录下所有文件视为mock文件
|
|||
|
||||
如果您想修改Mock目录位置,可以在配置文件中修改mockPath。如果不配置该参数,默认使用"./mock"。
|
||||
|
||||
```
|
||||
```typescript
|
||||
// .inula.ts
|
||||
export default {
|
||||
...
|
||||
|
@ -569,7 +569,7 @@ export default {
|
|||
|
||||
Mock文件需要默认导出一个对象,key为"请求方式 接口名",值为接口实现。示例如下:
|
||||
|
||||
```
|
||||
```typescript
|
||||
export default {
|
||||
"GET /api/user": (req, res) => {
|
||||
res.status(200).json("admin")
|
||||
|
@ -579,7 +579,7 @@ export default {
|
|||
|
||||
如果想要一次mock多个接口,可以在导出对象中设置多个key,例如:
|
||||
|
||||
```
|
||||
```typescript
|
||||
export default {
|
||||
"GET /api/user": (req, res) => {
|
||||
res.status(200).json("admin");
|
||||
|
@ -597,7 +597,7 @@ export default {
|
|||
|
||||
Mock文件默认导出一个数组,数组每一个成员示例如下:
|
||||
|
||||
```
|
||||
```typescript
|
||||
export default [
|
||||
{
|
||||
url: '/api/get',
|
||||
|
@ -654,7 +654,7 @@ export default [
|
|||
|
||||
在框架配置文件中,开发者需要配置远端服务器地址以及编写自定义的matcher函数提供给框架:
|
||||
|
||||
```
|
||||
```typescript
|
||||
// .inula.ts
|
||||
|
||||
const matcher = (pathname, request) => {
|
||||
|
@ -689,7 +689,7 @@ export default {
|
|||
|
||||
用户可以在.inula.ts中配置remoteProxy字段开启远端静态接口代理能力,完成配置后,使用后执行inula-cli proxy启动该功能。
|
||||
|
||||
```
|
||||
```typescript
|
||||
// .inula.ts
|
||||
|
||||
export default {
|
||||
|
@ -710,6 +710,3 @@ export default {
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -165,4 +165,3 @@ export interface Arguments {
|
|||
'--'?: Array<string | number>;
|
||||
[argName: string]: any;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
module.exports = api => {
|
||||
const isTest = api.env('test');
|
||||
console.log('isTest', isTest);
|
||||
|
||||
const plugins = [
|
||||
['@babel/plugin-proposal-class-properties', { loose: false }],
|
||||
];
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
plugins.push(['@babel/plugin-transform-react-jsx-source']);
|
||||
}
|
||||
|
||||
return {
|
||||
presets: [
|
||||
'@babel/preset-env',
|
||||
'@babel/preset-typescript',
|
||||
[
|
||||
'@babel/preset-react', {
|
||||
runtime: 'classic',
|
||||
'pragma': 'Inula.createElement',
|
||||
'pragmaFrag': 'Inula.Fragment',
|
||||
}]
|
||||
],
|
||||
plugins,
|
||||
};
|
||||
};
|
|
@ -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.
|
||||
*/
|
||||
|
||||
declare module '*.less' {
|
||||
const resource: {[key: string]: string};
|
||||
export = resource;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/*
|
||||
区分是否开发者模式
|
||||
*/
|
||||
declare var isDev: boolean;
|
||||
declare var isTest: boolean;
|
||||
declare const __VERSION__: string;
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"name": "inula-dev-tools",
|
||||
"version": "0.0.1",
|
||||
"description": "Inula chrome dev extension",
|
||||
"main": "",
|
||||
"scripts": {
|
||||
"build": "webpack --config ./webpack.config.js",
|
||||
"watch": "webpack --config ./webpack.config.js --watch",
|
||||
"build-dev": "webpack --config ./webpack.dev.js",
|
||||
"start": "npm run build && webpack serve --config ./webpack.dev.js",
|
||||
"test": "jest"
|
||||
},
|
||||
"keywords": ["inula-dev-tools"],
|
||||
"license": "MulanPSL2",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.12.3",
|
||||
"@babel/plugin-proposal-class-properties": "^7.16.7",
|
||||
"@babel/plugin-transform-react-jsx-source": "^7.18.6",
|
||||
"@babel/preset-env": "^7.21.1",
|
||||
"@babel/preset-react": "^7.12.1",
|
||||
"@babel/preset-typescript": "^7.16.7",
|
||||
"@types/chrome": "0.0.190",
|
||||
"@types/jest": "27.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "4.8.0",
|
||||
"@typescript-eslint/parser": "4.8.0",
|
||||
"babel-jest": "^27.5.1",
|
||||
"eslint": "7.13.0",
|
||||
"eslint-config-prettier": "^6.9.0",
|
||||
"eslint-plugin-jest": "^22.15.0",
|
||||
"eslint-plugin-no-for-of-loops": "^1.0.0",
|
||||
"eslint-plugin-no-function-declare-after-return": "^1.0.0",
|
||||
"eslint-plugin-react": "7.14.3",
|
||||
"babel-loader": "8.1.0",
|
||||
"css-loader": "^6.7.1",
|
||||
"html-webpack-plugin": "5.5.0",
|
||||
"jest": "27.5.1",
|
||||
"less": "4.1.2",
|
||||
"less-loader": "10.2.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"ts-jest": "27.1.4",
|
||||
"ts-loader": "^9.3.1",
|
||||
"typescript": "4.2.3",
|
||||
"webpack": "5.70.0",
|
||||
"webpack-cli": "4.9.2",
|
||||
"webpack-dev-server": "^4.7.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"openinula": "^0.1.1",
|
||||
"flatted-object": "^0.1.2",
|
||||
"json-decycle": "^2.0.1",
|
||||
"lodash": "^4.17.21",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 { checkMessage, packagePayload, changeSource } from "../utils/transferUtils";
|
||||
import { RequestAllVNodeTreeInfos, InitDevToolPageConnection, DevToolBackground } from "../utils/constants";
|
||||
import { DevToolPanel, DevToolContentScript } from "../utils/constants";
|
||||
|
||||
// 多个页面 tab 页共享一个 background,需要建立连接池,给每个 tab 建立连接
|
||||
export const connections = {};
|
||||
|
||||
// panel 代码中调用 let backgroundPageConnection = chrome.runtime.connect({...}) 会触发回调函数
|
||||
chrome.runtime.onConnect.addListener(function (port) {
|
||||
function extensionListener(message) {
|
||||
const isInulaMessage = checkMessage(message, DevToolPanel);
|
||||
if (isInulaMessage) {
|
||||
const { payload } = message;
|
||||
// tabId 值指当前浏览器分配给 web_page 的 id 值。是 panel 页面查询得到,指定向页面发送消息
|
||||
const { type, data, tabId } = payload;
|
||||
let passMessage;
|
||||
if (type === InitDevToolPageConnection) {
|
||||
// 记录 panel 所在 tab 页的 tabId,如果已经记录了,覆盖原有 port,因为原有 port 可能关闭了
|
||||
// 可能这次是 panel 发起的重新建立请求
|
||||
connections[tabId] = port;
|
||||
passMessage = packagePayload({ type: RequestAllVNodeTreeInfos }, DevToolBackground);
|
||||
} else {
|
||||
passMessage = packagePayload({ type, data }, DevToolBackground);
|
||||
}
|
||||
chrome.tabs.sendMessage(tabId, passMessage);
|
||||
}
|
||||
}
|
||||
|
||||
// 监听 dev tools 页面发送的消息
|
||||
port.onMessage.addListener(extensionListener);
|
||||
|
||||
port.onDisconnect.addListener(function (port) {
|
||||
port.onMessage.removeListener(extensionListener);
|
||||
|
||||
const tabs = Object.keys(connections);
|
||||
for (let i = 0, len = tabs.length; i < len; i++) {
|
||||
if (connections[tabs[i]] == port) {
|
||||
delete connections[tabs[i]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 监听来自 content script 的消息,并将消息发送给对应的 dev tools 页面
|
||||
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
|
||||
// 来自 content script 的消息需要预先设置 sender.tab
|
||||
if (sender.tab) {
|
||||
const tabId = sender.tab.id;
|
||||
if (message.payload.type.startsWith('inulax')) {
|
||||
return;
|
||||
}
|
||||
if (tabId && tabId in connections && checkMessage(message, DevToolContentScript)) {
|
||||
changeSource(message, DevToolBackground);
|
||||
connections[tabId].postMessage(message);
|
||||
} else {
|
||||
// TODO: 如果查询失败,发送 chrome message ,请求 panel 主动建立连接
|
||||
console.log('Tab is not found in connection');
|
||||
}
|
||||
} else {
|
||||
console.log('sender.tab is not defined');
|
||||
}
|
||||
// 需要返回消息告知完成通知,否则会出现报错 message port closed before a response was received
|
||||
sendResponse({ status: 'ok' });
|
||||
});
|
|
@ -0,0 +1,296 @@
|
|||
/*
|
||||
* 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 { DevToolBackground, DevToolContentScript, DevToolPanel } from '../utils/constants';
|
||||
import { connections } from './index';
|
||||
import { packagePayload } from '../utils/transferUtils';
|
||||
|
||||
// 监听来自 content script 的消息,并将消息发送给对应的 dev tools page
|
||||
const eventsPerTab = {};
|
||||
const storesPerTab = {};
|
||||
const observedComponents = {};
|
||||
const eventPersistencePerTab = {};
|
||||
let idGenerator = 1;
|
||||
|
||||
// 当 tab 重新加载,需要对该 tab 所监听的 stores 进行重置
|
||||
chrome.tabs.onUpdated.addListener(function (tabId, changeInfo) {
|
||||
if (changeInfo.status === 'loading') {
|
||||
if (!eventPersistencePerTab[tabId]) {
|
||||
eventsPerTab[tabId] = [];
|
||||
}
|
||||
storesPerTab[tabId] = [];
|
||||
}
|
||||
});
|
||||
|
||||
function sendTo(connectionId, message) {
|
||||
if (connections[connectionId]) {
|
||||
connections[connectionId].postMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
function requestObservedComponents(tabId) {
|
||||
setTimeout(() => {
|
||||
chrome.tabs.sendMessage(
|
||||
tabId,
|
||||
packagePayload(
|
||||
{
|
||||
type: 'inulax request observed components',
|
||||
data: {}
|
||||
},
|
||||
'dev tool background'
|
||||
)
|
||||
);
|
||||
}, 1);
|
||||
}
|
||||
|
||||
function executeAction(tabId, storeId, action, params) {
|
||||
chrome.tabs.sendMessage(
|
||||
tabId,
|
||||
packagePayload(
|
||||
{
|
||||
type: 'inulax execute action',
|
||||
data: {
|
||||
action,
|
||||
storeId,
|
||||
params,
|
||||
}
|
||||
},
|
||||
'dev tool background'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function queueAction(tabId, storeId, action, params) {
|
||||
chrome.tabs.sendMessage(
|
||||
tabId,
|
||||
packagePayload(
|
||||
{
|
||||
type: 'inulax queue action',
|
||||
data: {
|
||||
action,
|
||||
storeId,
|
||||
params,
|
||||
}
|
||||
},
|
||||
'sev tool background'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function getObservedComponents(storeId, tabId) {
|
||||
if (!observedComponents[tabId]) {
|
||||
observedComponents[tabId] = {};
|
||||
}
|
||||
if (!observedComponents[tabId][storeId]) {
|
||||
return [];
|
||||
}
|
||||
return observedComponents[tabId][storeId];
|
||||
}
|
||||
|
||||
// 来自 content script 的消息
|
||||
chrome.runtime.onMessage.addListener(function (message, sender) {
|
||||
if (message.payload.type.startsWith('inulax')) {
|
||||
console.log('inulaXHandler message from content script', {
|
||||
payload: { ...message.payload },
|
||||
});
|
||||
|
||||
if (message.from === DevToolContentScript && sender.tab?.id) {
|
||||
if (message.payload.type === 'inulax observed components') {
|
||||
observedComponents[sender.tab.id] = message.payload.data;
|
||||
|
||||
sendTo(sender.tab.id, {
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: 'inulax observed components',
|
||||
data: message.payload.data,
|
||||
},
|
||||
from: DevToolBackground,
|
||||
});
|
||||
return;
|
||||
}
|
||||
requestObservedComponents(sender.tab.id);
|
||||
|
||||
// content script -> inulaXHandler
|
||||
if (!eventsPerTab[sender.tab.id]) {
|
||||
eventsPerTab[sender.tab.id] = [];
|
||||
}
|
||||
eventsPerTab[sender.tab.id].push({
|
||||
id: idGenerator++,
|
||||
timestamp: Date.now(),
|
||||
message: message.payload,
|
||||
});
|
||||
|
||||
sendTo(sender.tab.id, {
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: 'inulax events',
|
||||
events: eventsPerTab[sender.tab.id],
|
||||
},
|
||||
from: DevToolBackground,
|
||||
});
|
||||
|
||||
// 如果当前 tab 没有 store data,则初始化
|
||||
if (!storesPerTab[sender.tab.id]) {
|
||||
storesPerTab[sender.tab.id] = [];
|
||||
}
|
||||
|
||||
let found = false;
|
||||
storesPerTab[sender.tab.id]?.some((store, index) => {
|
||||
if (store.id === message.payload.data.store.id) {
|
||||
found = true;
|
||||
storesPerTab[sender.tab!.id!][index] = message.payload.data.store;
|
||||
requestObservedComponents(sender.tab?.id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (!found) {
|
||||
const tabId = sender.tab.id;
|
||||
if (!storesPerTab[tabId]) {
|
||||
storesPerTab[tabId] = [];
|
||||
}
|
||||
storesPerTab[tabId].push(message.payload.data.store);
|
||||
sendTo(tabId, {
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: 'inulax stores',
|
||||
stores: storesPerTab[tabId]?.map(store => {
|
||||
// 连接被监测的组件
|
||||
requestObservedComponents(tabId);
|
||||
const observedComponents = getObservedComponents(store, tabId);
|
||||
return { ...store, observedComponents };
|
||||
}) || [],
|
||||
newStore: message.payload.data.store.id,
|
||||
},
|
||||
from: DevToolBackground,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
sendTo(sender.tab.id, {
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: 'inulax stores',
|
||||
stores: storesPerTab[sender.tab.id]?.map(store => {
|
||||
// 连接被监测的组件
|
||||
const observedComponents = getObservedComponents(store, sender.tab?.id);
|
||||
return { ...store, observedComponents };
|
||||
}) || [],
|
||||
updated: message.payload.data.store.id,
|
||||
},
|
||||
from: DevToolBackground,
|
||||
});
|
||||
|
||||
requestObservedComponents(message.payload.tabId);
|
||||
}
|
||||
|
||||
if (message.from === DevToolPanel) {
|
||||
// panel -> inulaXHandler
|
||||
if (message.payload.type === 'inulax run action') {
|
||||
executeAction(
|
||||
message.payload.tabId,
|
||||
message.payload.storeId,
|
||||
message.payload.action,
|
||||
message.payload.args
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.payload.type === 'inulax change state') {
|
||||
chrome.tabs.sendMessage(
|
||||
message.payload.tabId,
|
||||
packagePayload(message.payload, 'dev tool background')
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.payload.type === 'inulax queue action') {
|
||||
queueAction(
|
||||
message.payload.tabId,
|
||||
message.payload.storeId,
|
||||
message.payload.action,
|
||||
message.payload.args
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.payload.type === 'inulax resetEvents') {
|
||||
eventsPerTab[message.payload.tabId] = [];
|
||||
sendTo(message.payload.tabId, {
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: 'inulax events',
|
||||
events: eventsPerTab[message.payload.tabId],
|
||||
},
|
||||
from: DevToolBackground,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.payload.type === 'inula setPersistent'){
|
||||
const { tabId, persistent } = message.payload;
|
||||
eventPersistencePerTab[tabId] = persistent;
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.payload.type === 'inulax getPersistence') {
|
||||
sendTo(message.payload.tabId, {
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: 'inulax persistence',
|
||||
persistent: !!eventPersistencePerTab[message.payload.tabId],
|
||||
},
|
||||
from: DevToolBackground,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.payload.type === 'inulax getEvents') {
|
||||
if (!eventsPerTab[message.payload.tabId]) {
|
||||
eventsPerTab[message.payload.tabId] = [];
|
||||
}
|
||||
sendTo(message.payload.tabId, {
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: 'inulax events',
|
||||
events: eventsPerTab[message.payload.tabId],
|
||||
},
|
||||
from: DevToolBackground,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.payload.type === 'inulax getStores') {
|
||||
sendTo(message.payload.tabId, {
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: 'inulax stores',
|
||||
stores: storesPerTab[message.payload.tabId]?.map(store => {
|
||||
requestObservedComponents(message.payload.tabId);
|
||||
const observedComponents = getObservedComponents(
|
||||
store.id,
|
||||
message.payload.tabId
|
||||
);
|
||||
return { ...store, observedComponents };
|
||||
}) || [],
|
||||
},
|
||||
from: DevToolBackground,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.search {
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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 './Search.less';
|
||||
|
||||
interface SearchProps {
|
||||
onKeyUp: () => void;
|
||||
onChange: (event: any) => void;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export default function Search(props: SearchProps) {
|
||||
const { onChange, value, onKeyUp } = props;
|
||||
const handleChange = event => {
|
||||
onChange(event.target.value);
|
||||
};
|
||||
const handleKeyUp = event => {
|
||||
if (event.key === 'Enter') {
|
||||
onKeyUp();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
onkeyup={handleKeyUp}
|
||||
onchange={handleChange}
|
||||
className={styles.search}
|
||||
value={value}
|
||||
placeholder='Search Component'
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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 { useEffect, useState, useRef } from 'openinula';
|
||||
import { addResizeListener, removeResizeListener } from './resizeEvent';
|
||||
|
||||
export function SizeObserver(props) {
|
||||
const { children, ...rest } = props;
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const [size, setSize] = useState<{ width: number; height: number }>();
|
||||
const notifyChild = (element) => {
|
||||
setSize({
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight,
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const element = containerRef.current!;
|
||||
setSize({
|
||||
width: element.offsetWidth,
|
||||
height: element.offsetHeight,
|
||||
});
|
||||
addResizeListener(element, notifyChild);
|
||||
return () => {
|
||||
removeResizeListener(element, notifyChild);
|
||||
};
|
||||
}, []);
|
||||
const myChild = size ? children(size.width, size.height) : null;
|
||||
|
||||
return (
|
||||
<div ref={containerRef} {...rest}>
|
||||
{myChild}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
/*
|
||||
* 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 default class ItemMap<T> {
|
||||
|
||||
// 不要用 indexOf 进行位置计算,它会遍历数组
|
||||
private lastRenderItemToIndexMap: Map<T | undefined, number>;
|
||||
|
||||
constructor() {
|
||||
this.lastRenderItemToIndexMap = new Map();
|
||||
}
|
||||
|
||||
public calculateReSortedItems(nextItems: T[]): (T|undefined)[] {
|
||||
if (this.lastRenderItemToIndexMap.size === 0) {
|
||||
nextItems.forEach((item, index) => {
|
||||
this.lastRenderItemToIndexMap.set(item, index);
|
||||
});
|
||||
return nextItems;
|
||||
}
|
||||
|
||||
const nextRenderItems: (T | undefined)[] = [];
|
||||
const length = nextItems.length;
|
||||
const nextRenderItemToIndexMap = new Map<T | undefined, number>();
|
||||
const addItems: T[] = [];
|
||||
|
||||
// 遍历 nextItems 找到复用 item 和新增 item
|
||||
nextItems.forEach(item => {
|
||||
const lastIndex = this.lastRenderItemToIndexMap.get(item);
|
||||
// 处理旧 item
|
||||
if (lastIndex !== undefined) {
|
||||
// 使用上一次的位置
|
||||
nextRenderItems[lastIndex] = item;
|
||||
// 记录位置
|
||||
nextRenderItemToIndexMap.set(item, lastIndex);
|
||||
} else {
|
||||
// 记录新的 item
|
||||
addItems.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理新增 item,翻转数组,后面在调用 pop 时拿到的时最后一个,以确保顺序
|
||||
addItems.reverse();
|
||||
for (let i = 0; i < length; i++) {
|
||||
// 优先将新增 item 放置在空位置上
|
||||
if (!nextRenderItems[i]) {
|
||||
const item = addItems.pop();
|
||||
nextRenderItems[i] = item;
|
||||
nextRenderItemToIndexMap.set(item, i);
|
||||
}
|
||||
}
|
||||
|
||||
// 剩余新 item 补在数组后面
|
||||
for (let i = addItems.length - 1; i >= 0; i--) {
|
||||
const item = addItems[i];
|
||||
nextRenderItemToIndexMap.set(item, nextRenderItems.length);
|
||||
nextRenderItems.push(item);
|
||||
}
|
||||
|
||||
// 如果 nextRenderItems 中存在空 index,nextItems 已经耗尽,不用处理
|
||||
// 确保新旧数组中 item 的 index 值不会发生变化
|
||||
this.lastRenderItemToIndexMap = nextRenderItemToIndexMap;
|
||||
return nextRenderItems;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.container {
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.item {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
}
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 内部只记录滚动位置状态值
|
||||
* data 数组更新后不修改滚动位置,只有修改 scrollToItem 才会修改滚动位置
|
||||
*/
|
||||
import { useState, useRef, useEffect, useMemo } from 'openinula';
|
||||
import styles from './VList.less';
|
||||
import ItemMap from './ItemMap';
|
||||
import { debounceFunc } from '../../utils/publicUtil';
|
||||
|
||||
interface IProps<T extends { id: number | string }> {
|
||||
data: T[];
|
||||
maxDeep: number;
|
||||
width: number; // 暂时未用到,当需要支持横向滚动时使用
|
||||
height: number; // VList 的高度
|
||||
children?: any; // inula 组件
|
||||
itemHeight: number;
|
||||
scrollToItem?: T; // 滚动到指定项位置,如果该项在可见区域内,不滚动,如果补在,则滚动到中间位置
|
||||
onRendered: (renderInfo: RenderInfoType<T>) => void;
|
||||
filter?: (data: T) => boolean; // false 表示该行不显示
|
||||
}
|
||||
|
||||
export type RenderInfoType<T> = {
|
||||
visibleItems: T[];
|
||||
}
|
||||
|
||||
function parseTranslate<T>(data: T[], itemHeight: number) {
|
||||
const map = new Map<T, number>();
|
||||
data.forEach((item, index) => {
|
||||
map.set(item, index * itemHeight);
|
||||
})
|
||||
return map;
|
||||
}
|
||||
|
||||
export function VList<T extends { id: number | string }>(props: IProps<T>) {
|
||||
const { data, maxDeep, height, width, children, itemHeight, scrollToItem, onRendered } = props;
|
||||
const [scrollTop, setScrollTop] = useState(Math.max(data.indexOf(scrollToItem!), 0) * itemHeight);
|
||||
const renderInfoRef: { current: RenderInfoType<T> } = useRef({
|
||||
visibleItems: [],
|
||||
});
|
||||
const [indentationLength, setIndentationLength] = useState(0);
|
||||
|
||||
// 每个 item 的 translateY 值固定不变
|
||||
const itemToTranslateYMap = useMemo(() => parseTranslate(data, itemHeight), [data]);
|
||||
const itemIndexMap = useMemo(() => new ItemMap<T>(), []);
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
|
||||
useEffect(() => {
|
||||
onRendered(renderInfoRef.current);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
debounceFunc(() => setIndentationLength(Math.min(12, Math.round(width / (2 * maxDeep)))));
|
||||
}, [width]);
|
||||
|
||||
useEffect(() => {
|
||||
if (scrollToItem) {
|
||||
const renderInfo = renderInfoRef.current;
|
||||
// 在显示区域,不滚动
|
||||
if (!renderInfo.visibleItems.includes(scrollToItem)) {
|
||||
const index = data.indexOf(scrollToItem);
|
||||
// 显示在页面中间
|
||||
const top = Math.max(index * itemHeight - height / 2, 0);
|
||||
containerRef.current?.scrollTo({ top: top });
|
||||
}
|
||||
}
|
||||
}, [scrollToItem]);
|
||||
|
||||
// 滚动事件会频繁触发,通过框架提供的代理会有大量计算寻找 dom 元素,直接绑定到原生事件上减少计算量
|
||||
useEffect(() => {
|
||||
const handleScroll = event => {
|
||||
const scrollTop = event.target.scrollTop;
|
||||
setScrollTop(scrollTop);
|
||||
};
|
||||
const container = containerRef.current;
|
||||
container?.addEventListener('scroll', handleScroll);
|
||||
return () => {
|
||||
container?.removeEventListener('scroll', handleScroll);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const totalHeight = itemHeight * data.length;
|
||||
const maxIndex = data.length; // slice 截取渲染 item 数组时最大位置不能超过自然长度
|
||||
// 第一个可见 item index
|
||||
const firstInViewItemIndex = Math.floor(scrollTop / itemHeight);
|
||||
// 可见区域前最多冗余 4 个 item
|
||||
const startRenderIndex = Math.max(firstInViewItemIndex - 4, 0); // index 不能小于 0
|
||||
// 最多可见数量
|
||||
const maxInViewCount = Math.floor(height / itemHeight);
|
||||
// 最后可见 item index
|
||||
const lastInViewIndex = Math.min(firstInViewItemIndex + maxInViewCount, maxIndex);
|
||||
// 记录可见 items
|
||||
renderInfoRef.current.visibleItems = data.slice(firstInViewItemIndex, lastInViewIndex);
|
||||
// 可见区域后冗余 4 个 item
|
||||
const lastRenderIndex = Math.min(lastInViewIndex + 4, maxIndex);
|
||||
// 需要渲染的 item
|
||||
const renderItems = data.slice(startRenderIndex, lastRenderIndex);
|
||||
// 给 items 重新排序,确保未移出渲染数组的 item 在新的渲染数组中位置不变,这样在 diff 算法比较后,这部分的 dom 不会发生更新
|
||||
const nextRenderList = itemIndexMap.calculateReSortedItems(renderItems);
|
||||
const list = nextRenderList.map((item, index) => {
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
key={String(index)} // 固定 key 值,这样就只会更新 translateY 的值
|
||||
className={styles.item}
|
||||
style={{ transform: `translateY(${itemToTranslateYMap.get(item)}px)` }}
|
||||
>
|
||||
{children(item,indentationLength)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className={styles.container}>
|
||||
{list}
|
||||
<div style={{ marginTop: totalHeight }}></div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 { VList } from './VList';
|
||||
export type { RenderInfoType } from './VList';
|
|
@ -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;
|
||||
}
|
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
* Copyright (c) 2023 Huawei Technologies Co.,Ltd.
|
||||
*
|
||||
* openInula is licensed under Mulan PSL v2.
|
||||
* You can use this software according to the terms and conditions of the Mulan PSL v2.
|
||||
* You may obtain a copy of Mulan PSL v2 at:
|
||||
*
|
||||
* http://license.coscl.org.cn/MulanPSL2
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
|
||||
* EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
|
||||
* MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
|
||||
* See the Mulan PSL v2 for more details.
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, memo } from 'openinula';
|
||||
import styles from './VTree.less';
|
||||
import Triangle from '../svgs/Triangle';
|
||||
import { createRegExp } from '../utils/regExpUtil';
|
||||
import { SizeObserver } from './SizeObserver';
|
||||
import { RenderInfoType, VList } from './VList';
|
||||
import { postMessageToBackground } from '../panelConnection';
|
||||
import { Highlight, RemoveHighlight } from '../utils/constants';
|
||||
import { NameObj } from '../parser/parseVNode';
|
||||
|
||||
export interface IData {
|
||||
id: number;
|
||||
name: NameObj;
|
||||
indentation: number;
|
||||
userKey: string;
|
||||
}
|
||||
|
||||
interface IItem {
|
||||
indentationLength: number;
|
||||
hasChild: boolean;
|
||||
onCollapse: (data: IData) => void;
|
||||
onClick: (id: IData) => void;
|
||||
onMouseEnter: (id: IData) => void;
|
||||
onMouseLeave: (id: IData) => void;
|
||||
isCollapsed: boolean;
|
||||
isSelect: boolean;
|
||||
highlightValue: string;
|
||||
data: IData;
|
||||
isSelectedItemChild: boolean;
|
||||
}
|
||||
|
||||
function Item(props: IItem) {
|
||||
const {
|
||||
hasChild,
|
||||
onCollapse,
|
||||
isCollapsed,
|
||||
data,
|
||||
onClick,
|
||||
indentationLength,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
isSelect,
|
||||
highlightValue = '',
|
||||
isSelectedItemChild,
|
||||
} = props;
|
||||
|
||||
const { name, userKey, indentation } = data;
|
||||
|
||||
const isShowKey = userKey !== '';
|
||||
const showIcon = hasChild ? <Triangle director={isCollapsed ? 'right' : 'down'} /> : '';
|
||||
const handleClickCollapse = () => {
|
||||
onCollapse(data);
|
||||
};
|
||||
const handleClick = () => {
|
||||
onClick(data);
|
||||
};
|
||||
const handleMouseEnter = () => {
|
||||
onMouseEnter(data);
|
||||
};
|
||||
const handleMouseLeave = () => {
|
||||
onMouseLeave(data);
|
||||
};
|
||||
|
||||
const itemAttr: Record<string, any> = {
|
||||
className: isSelectedItemChild ? styles.selectedItemChild : styles.treeItem,
|
||||
onClick: handleClick,
|
||||
onMouseEnter: handleMouseEnter,
|
||||
onMouseLeave: handleMouseLeave,
|
||||
};
|
||||
|
||||
if (isSelect) {
|
||||
itemAttr.tabIndex = 0;
|
||||
itemAttr.className = styles.treeItem + ' ' + styles.select;
|
||||
}
|
||||
|
||||
if (isSelectedItemChild) {
|
||||
itemAttr.className = styles.treeItem + ' ' + styles.selectedItemChild;
|
||||
}
|
||||
|
||||
const pushBadge = (showName: Array<any>, badgeName: string) => {
|
||||
showName.push(' ');
|
||||
showName.push(<div className={`${styles.Badge}`}>{badgeName}</div>);
|
||||
};
|
||||
|
||||
const pushItemName = (showName: Array<any>, cutName: string, char: string) => {
|
||||
const index = cutName.search(char);
|
||||
if (index > -1) {
|
||||
const notHighlightStr = cutName.slice(0, index);
|
||||
showName.push(`<${notHighlightStr}`);
|
||||
showName.push(<mark>{char}</mark>);
|
||||
showName.push(`${cutName.slice(index + char.length)}>`);
|
||||
} else {
|
||||
showName.push(`<${cutName}`);
|
||||
}
|
||||
};
|
||||
|
||||
const pushBadgeName = (showName: Array<any>, cutName: string, char: string) => {
|
||||
const index = cutName.search(char);
|
||||
if (index > -1) {
|
||||
const notHighlightStr = cutName.slice(0, index);
|
||||
showName.push(
|
||||
<div className={`${styles.Badge}`}>
|
||||
{notHighlightStr}
|
||||
{<mark>{char}</mark>}
|
||||
{cutName.slice(index + char.length)}
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
pushBadge(showName, cutName);
|
||||
}
|
||||
};
|
||||
|
||||
const reg = createRegExp(highlightValue);
|
||||
const heightCharacters = name.itemName.match(reg);
|
||||
const showName = [];
|
||||
|
||||
const addShowName = (showName: Array<string>, name: NameObj) => {
|
||||
showName.push(`<${name.itemName}>`);
|
||||
name.badge.forEach(key => {
|
||||
showName.push(<div className={`${styles.Badge}`}>{key}</div>);
|
||||
});
|
||||
};
|
||||
|
||||
if (heightCharacters) {
|
||||
// 高亮第一次匹配即可
|
||||
const char = heightCharacters[0];
|
||||
pushItemName(showName, name.itemName, char);
|
||||
if (name.badge.length > 0) {
|
||||
name.badge.forEach(key => {
|
||||
pushBadgeName(showName, key, char);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
addShowName(showName, name);
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...itemAttr}>
|
||||
<div
|
||||
style={{marginLeft: indentation * indentationLength}}
|
||||
className={styles.treeIcon}
|
||||
onclick={handleClickCollapse}
|
||||
>
|
||||
{showIcon}
|
||||
</div>
|
||||
<span className={styles.componentName}>{showName}</span>
|
||||
{isShowKey && (
|
||||
<>
|
||||
<span className={styles.componentKeyName}> key</span>
|
||||
{'="'}
|
||||
<span className={styles.componentKeyValue}>{userKey}</span>
|
||||
{'"'}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function VTree(props: {
|
||||
data: IData[];
|
||||
maxDeep: number;
|
||||
highlightValue: string;
|
||||
scrollToItem: IData;
|
||||
onRendered: (renderInfo: RenderInfoType<IData>) => void;
|
||||
collapsedNodes?: IData[];
|
||||
onCollapseNode?: (item: IData[]) => void;
|
||||
selectItem: IData;
|
||||
onSelectItem: (item: IData) => void;
|
||||
}) {
|
||||
const {
|
||||
data,
|
||||
maxDeep,
|
||||
highlightValue,
|
||||
scrollToItem,
|
||||
onRendered,
|
||||
onCollapseNode,
|
||||
onSelectItem
|
||||
} = props;
|
||||
const [collapseNode, setCollapseNode] = useState(props.collapsedNodes || []);
|
||||
const [selectItem, setSelectItem] = useState(props.selectItem);
|
||||
const [childItems, setChildItems] = useState<Array<IData>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectItem(scrollToItem);
|
||||
}, [scrollToItem]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.selectItem !== selectItem) {
|
||||
setSelectItem(props.selectItem);
|
||||
}
|
||||
}, [props.selectItem]);
|
||||
|
||||
useEffect(() => {
|
||||
setCollapseNode(props.collapsedNodes || []);
|
||||
}, [props.collapsedNodes]);
|
||||
|
||||
const changeCollapseNode = (item: IData) => {
|
||||
const nodes: IData[] = [...collapseNode];
|
||||
const index = nodes.indexOf(item);
|
||||
if (index === -1) {
|
||||
nodes.push(item);
|
||||
} else {
|
||||
nodes.splice(index, 1);
|
||||
}
|
||||
|
||||
setCollapseNode(nodes);
|
||||
|
||||
if (onCollapseNode) {
|
||||
onCollapseNode(nodes);
|
||||
}
|
||||
};
|
||||
|
||||
const getChildItem = (item: IData): Array<IData> => {
|
||||
const index = data.indexOf(item);
|
||||
const childList: Array<IData> = [];
|
||||
|
||||
for (let i = index + 1; i < data.length; i++) {
|
||||
if (data[i].indentation > item.indentation) {
|
||||
childList.push(data[i]);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return childList;
|
||||
};
|
||||
|
||||
const handleClickItem = useCallback(
|
||||
(item: IData) => {
|
||||
const childItem = getChildItem(item);
|
||||
setSelectItem(item);
|
||||
setChildItems(childItem);
|
||||
if (onSelectItem) {
|
||||
onSelectItem(item);
|
||||
}
|
||||
},
|
||||
[onSelectItem]
|
||||
);
|
||||
|
||||
const handleMouseEnterItem = useCallback(
|
||||
item => {
|
||||
postMessageToBackground(Highlight, item);
|
||||
},
|
||||
null
|
||||
);
|
||||
|
||||
const handleMouseLeaveItem = () => {
|
||||
postMessageToBackground(RemoveHighlight);
|
||||
};
|
||||
|
||||
let currentCollapseIndentation: null | number = null;
|
||||
// 过滤掉折叠的 item,不展示在 VList 中
|
||||
const filter = (item: IData) => {
|
||||
if (currentCollapseIndentation !== null) {
|
||||
// 缩进更大,不显示
|
||||
if (item.indentation > currentCollapseIndentation) {
|
||||
return false;
|
||||
} else {
|
||||
// 缩进小,说明完成了收起节点的子节点处理
|
||||
currentCollapseIndentation = null;
|
||||
}
|
||||
}
|
||||
const isCollapsed = collapseNode.includes(item);
|
||||
if (isCollapsed) {
|
||||
// 该节点需要收起子节点
|
||||
currentCollapseIndentation = item.indentation;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const showList = data.filter(filter);
|
||||
|
||||
return (
|
||||
<SizeObserver className={styles.treeContainer}>
|
||||
{(width: number, height: number) => {
|
||||
return (
|
||||
<VList
|
||||
data={showList}
|
||||
maxDeep={maxDeep}
|
||||
width={width}
|
||||
height={height}
|
||||
itemHeight={17.5}
|
||||
scrollToItem={selectItem}
|
||||
onRendered={onRendered}
|
||||
>
|
||||
{(item: IData, indentationLength: number) => {
|
||||
const isCollapsed = collapseNode.includes(item);
|
||||
const index = showList.indexOf(item);
|
||||
// 如果收起,一定有 child
|
||||
// 不收起场景,如果存在下一个节点,并且节点缩进比自己大,说明下个节点是子节点,节点本身需要显示展开收起图标
|
||||
const hasChild = isCollapsed || showList[index + 1]?.indentation > item.indentation;
|
||||
return (
|
||||
<Item
|
||||
indentationLength={indentationLength}
|
||||
hasChild={hasChild}
|
||||
onCollapse={changeCollapseNode}
|
||||
onClick={handleClickItem}
|
||||
onMouseEnter={handleMouseEnterItem}
|
||||
onMouseLeave={handleMouseLeaveItem}
|
||||
isCollapsed={collapseNode.includes(item)}
|
||||
isSelect={selectItem === item}
|
||||
highlightValue={highlightValue}
|
||||
data={item}
|
||||
isSelectedItemChild={childItems.includes(item)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
</VList>
|
||||
);
|
||||
}}
|
||||
</SizeObserver>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(VTree);
|
|
@ -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;
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* 由于 ResizeObserver 对 IE 和低版本主流浏览器不兼容,需要自己实现一套兼容方案
|
||||
* 这是一个不依赖任何框架的监听 dom 元素尺寸变化的解决方案
|
||||
* 浏览器出于性能考虑,只有 window 的 resize 事件会触发,我们通过 object 标签可以得到
|
||||
* 一个 window 对象,让 object dom 元素成为待观测 dom 的子元素,并且和待观测 dom 大小一致。
|
||||
* 这样一旦待观测 dom 的大小发生变化,window 的大小也会发生变化,我们就可以通过监听 window
|
||||
* 大小变化的方式监听待观测 dom 的大小变化
|
||||
*
|
||||
* <div id='test>
|
||||
* <object> --> 和父 div 保持大小一致
|
||||
* <html></html> --> 添加 resize 事件监听
|
||||
* </object>
|
||||
* </div>
|
||||
*
|
||||
*/
|
||||
|
||||
function timeout(func) {
|
||||
return setTimeout(func, 20);
|
||||
}
|
||||
|
||||
function requestFrame(func) {
|
||||
const raf = requestAnimationFrame || timeout;
|
||||
return raf(func);
|
||||
}
|
||||
|
||||
function cancelFrame(id) {
|
||||
const cancel = cancelAnimationFrame || clearTimeout;
|
||||
cancel(id);
|
||||
}
|
||||
|
||||
// 在闲置帧触发回调事件,如果在本次触发前存在未处理回调事件,需要取消未处理回调事件
|
||||
function resizeListener(event) {
|
||||
const win = event.target;
|
||||
if (win.__resizeRAF__) {
|
||||
cancelFrame(win.__resizeRAF__);
|
||||
}
|
||||
win.__resizeRAF__ = requestFrame(function () {
|
||||
const observeElement = win.__observeElement__;
|
||||
observeElement.__resizeCallbacks__.forEach(function (func) {
|
||||
func.call(observeElement, observeElement, event);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function loadObserver(this: any) {
|
||||
// 将待观测元素传递给 object 标签的 window 对象,这样在触发 resize 事件时可以拿到待观测元素
|
||||
this.contentDocument.defaultView.__observeElement__ = this.__observeElement__;
|
||||
// 给 html 的 window 对象添加 resize 事件
|
||||
this.contentDocument.defaultView.addEventListener('resize', resizeListener);
|
||||
}
|
||||
|
||||
export function addResizeListener(element: any, func: any) {
|
||||
if (!element.__resizeCallbacks__) {
|
||||
element.__resizeCallbacks__ = [func];
|
||||
element.style.position = 'relative';
|
||||
const observer = document.createElement('object');
|
||||
observer.setAttribute(
|
||||
'style',
|
||||
'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;'
|
||||
);
|
||||
observer.data = 'about:blank';
|
||||
observer.onload = loadObserver;
|
||||
observer.type = 'text/html';
|
||||
observer['__observeElement__'] = element;
|
||||
element.__observer__ = observer;
|
||||
element.appendChild(observer);
|
||||
} else {
|
||||
element.__resizeCallbacks__.push(func);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeResizeListener(element, func) {
|
||||
element.__resizeCallbacks__.splice(element.__resizeCallbacks__.indexOf(func), 1);
|
||||
if (!element.__resizeCallbacks__.length) {
|
||||
element.__observer__.contentDocument.defaultView.removeEventListener('resize', resizeListener);
|
||||
element.removeChild(element.__observer__);
|
||||
element.__observer__ = null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
* 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 { injectSrc, injectCode } from '../utils/injectUtils';
|
||||
import { checkMessage } from '../utils/transferUtils';
|
||||
import { DevToolContentScript, DevToolHook, DevToolBackground } from '../utils/constants';
|
||||
import { changeSource } from '../utils/transferUtils';
|
||||
|
||||
// 页面的 window 对象不能直接通过 contentScript 代码修改,只能通过添加 js 代码往页面 window 注入 hook
|
||||
const rendererURL = chrome.runtime.getURL('/injector.js');
|
||||
if (window.performance.getEntriesByType('navigation')) {
|
||||
const entryType = (window.performance.getEntriesByType('navigation')[0] as any).type;
|
||||
if (entryType === 'navigate') {
|
||||
injectSrc(rendererURL);
|
||||
} else if (entryType === 'reload' && !(window as any).__INULA_DEV_HOOK__) {
|
||||
let rendererCode;
|
||||
const request = new XMLHttpRequest();
|
||||
request.addEventListener('load', function () {
|
||||
rendererCode = this.responseText;
|
||||
});
|
||||
request.open('GET', rendererURL, false);
|
||||
request.send();
|
||||
injectCode(rendererCode);
|
||||
}
|
||||
}
|
||||
|
||||
// 监听来自页面的信息
|
||||
window.addEventListener(
|
||||
'message',
|
||||
event => {
|
||||
// 只监听来自本页面的消息
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = event.data;
|
||||
if (checkMessage(data, DevToolHook)) {
|
||||
changeSource(data, DevToolContentScript);
|
||||
// 传递给 background
|
||||
chrome.runtime.sendMessage(data);
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
// 监听来自 background 的消息
|
||||
chrome.runtime.onMessage.addListener(function (message, sender, sendResponse) {
|
||||
// 该方法可以监听页面 contentScript 和插件的消息
|
||||
// 没有 tab 信息说明消息来自插件
|
||||
if (!sender.tab && checkMessage(message, DevToolBackground)) {
|
||||
changeSource(message, DevToolContentScript);
|
||||
// 传递消息给页面
|
||||
window.postMessage(message, '*');
|
||||
}
|
||||
sendResponse({ status: 'ok' });
|
||||
});
|
|
@ -0,0 +1,274 @@
|
|||
/*
|
||||
* 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 assign from 'object-assign';
|
||||
import { VNode } from '../../../inula/src/renderer/vnode/VNode';
|
||||
|
||||
const overlayStyles = {
|
||||
background: 'rgba(120, 170, 210, 0.7)',
|
||||
padding: 'rgba(77, 200, 0, 0.3)',
|
||||
margin: 'rgba(255, 155, 0, 0.3)',
|
||||
border: 'rgba(255, 200, 50, 0.3)'
|
||||
};
|
||||
|
||||
type Rect = {
|
||||
bottom: number;
|
||||
height: number;
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
width: number;
|
||||
};
|
||||
|
||||
function setBoxStyle(eleStyle, boxArea, node) {
|
||||
assign(node.style, {
|
||||
borderTopWidth: eleStyle[boxArea + 'Top'] + 'px',
|
||||
borderLeftWidth: eleStyle[boxArea + 'Left'] + 'px',
|
||||
borderRightWidth: eleStyle[boxArea + 'Right'] + 'px',
|
||||
borderBottomWidth: eleStyle[boxArea + 'Bottom'] + 'px',
|
||||
});
|
||||
}
|
||||
|
||||
function getOwnerWindow(node: Element): typeof window | null {
|
||||
if (!node.ownerDocument) {
|
||||
return null;
|
||||
}
|
||||
return node.ownerDocument.defaultView;
|
||||
}
|
||||
|
||||
function getOwnerIframe(node: Element): Element | null {
|
||||
const nodeWindow = getOwnerWindow(node);
|
||||
if (nodeWindow) {
|
||||
return nodeWindow.frameElement;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getElementStyle(domElement: Element) {
|
||||
const style = window.getComputedStyle(domElement);
|
||||
return{
|
||||
marginLeft: parseInt(style.marginLeft, 10),
|
||||
marginRight: parseInt(style.marginRight, 10),
|
||||
marginTop: parseInt(style.marginTop, 10),
|
||||
marginBottom: parseInt(style.marginBottom, 10),
|
||||
borderLeft: parseInt(style.borderLeftWidth, 10),
|
||||
borderRight: parseInt(style.borderRightWidth, 10),
|
||||
borderTop: parseInt(style.borderTopWidth, 10),
|
||||
borderBottom: parseInt(style.borderBottomWidth, 10),
|
||||
paddingLeft: parseInt(style.paddingLeft, 10),
|
||||
paddingRight: parseInt(style.paddingRight, 10),
|
||||
paddingTop: parseInt(style.paddingTop, 10),
|
||||
paddingBottom: parseInt(style.paddingBottom, 10)
|
||||
};
|
||||
}
|
||||
|
||||
function mergeRectOffsets(rects: Array<Rect>): Rect {
|
||||
return rects.reduce((previousRect, rect) => {
|
||||
if (previousRect == null) {
|
||||
return rect;
|
||||
}
|
||||
|
||||
return {
|
||||
top: previousRect.top + rect.top,
|
||||
left: previousRect.left + rect.left,
|
||||
width: previousRect.width + rect.width,
|
||||
height: previousRect.height + rect.height,
|
||||
bottom: previousRect.bottom + rect.bottom,
|
||||
right: previousRect.right + rect.right
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function getBoundingClientRectWithBorderOffset(node: Element) {
|
||||
const dimensions = getElementStyle(node);
|
||||
return mergeRectOffsets([
|
||||
node.getBoundingClientRect(),
|
||||
{
|
||||
top: dimensions.borderTop,
|
||||
left: dimensions.borderLeft,
|
||||
bottom: dimensions.borderBottom,
|
||||
right:dimensions.borderRight,
|
||||
// 高度和宽度不会被使用
|
||||
width: 0,
|
||||
height: 0
|
||||
}
|
||||
]);
|
||||
}
|
||||
|
||||
function getNestedBoundingClientRect(
|
||||
node: HTMLElement,
|
||||
boundaryWindow
|
||||
): Rect {
|
||||
const ownerIframe = getOwnerIframe(node);
|
||||
if (ownerIframe && ownerIframe !== boundaryWindow) {
|
||||
const rects = [node.getBoundingClientRect()] as Rect[];
|
||||
let currentIframe = ownerIframe;
|
||||
let onlyOneMore = false;
|
||||
while (currentIframe) {
|
||||
const rect = getBoundingClientRectWithBorderOffset(currentIframe);
|
||||
rects.push(rect);
|
||||
currentIframe = getOwnerIframe(currentIframe);
|
||||
|
||||
if (onlyOneMore) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (currentIframe &&getOwnerWindow(currentIframe) === boundaryWindow) {
|
||||
onlyOneMore = true;
|
||||
}
|
||||
}
|
||||
|
||||
return mergeRectOffsets(rects);
|
||||
} else {
|
||||
return node.getBoundingClientRect();
|
||||
}
|
||||
}
|
||||
|
||||
// 用来遮罩
|
||||
class OverlayRect {
|
||||
node: HTMLElement;
|
||||
border: HTMLElement;
|
||||
padding: HTMLElement;
|
||||
content: HTMLElement;
|
||||
|
||||
constructor(doc: Document, container: HTMLElement) {
|
||||
this.node = doc.createElement('div');
|
||||
this.border = doc.createElement('div');
|
||||
this.padding = doc.createElement('div');
|
||||
this.content = doc.createElement('div');
|
||||
|
||||
this.border.style.borderColor = overlayStyles.border;
|
||||
this.padding.style.borderColor = overlayStyles.padding;
|
||||
this.content.style.backgroundColor = overlayStyles.background;
|
||||
|
||||
assign(this.node.style, {
|
||||
borderColor: overlayStyles.margin,
|
||||
pointerEvents: 'none',
|
||||
position: 'fixed'
|
||||
});
|
||||
|
||||
this.node.style.zIndex = '10000000';
|
||||
|
||||
this.node.appendChild(this.border);
|
||||
this.border.appendChild(this.padding);
|
||||
this.padding.appendChild(this.content);
|
||||
container.appendChild(this.node);
|
||||
}
|
||||
|
||||
remove() {
|
||||
if (this.node.parentNode) {
|
||||
this.node.parentNode.removeChild(this.node);
|
||||
}
|
||||
}
|
||||
|
||||
update(boxRect: Rect, eleStyle: any) {
|
||||
setBoxStyle(eleStyle, 'margin', this.node);
|
||||
setBoxStyle(eleStyle, 'border', this.border);
|
||||
setBoxStyle(eleStyle, 'padding', this.padding);
|
||||
|
||||
assign(this.content.style, {
|
||||
height: boxRect.height - eleStyle.borderTop - eleStyle.borderBottom - eleStyle.paddingTop - eleStyle.paddingBottom + 'px',
|
||||
width: boxRect.width - eleStyle.borderLeft - eleStyle.borderRight - eleStyle.paddingLeft - eleStyle.paddingRight + 'px'
|
||||
});
|
||||
|
||||
assign(this.node.style, {
|
||||
top: boxRect.top - eleStyle.marginTop + 'px',
|
||||
left: boxRect.left - eleStyle.marginLeft + 'px'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ElementOverlay {
|
||||
window: typeof window;
|
||||
container: HTMLElement;
|
||||
rects: Array<OverlayRect>;
|
||||
|
||||
constructor() {
|
||||
this.window = window;
|
||||
const doc = window.document;
|
||||
this.container = doc.createElement('div');
|
||||
this.container.style.zIndex = '10000000';
|
||||
this.rects = [];
|
||||
|
||||
doc.body.appendChild(this.container);
|
||||
}
|
||||
|
||||
remove() {
|
||||
this.rects.forEach(rect => {
|
||||
rect.remove();
|
||||
});
|
||||
this.rects.length = 0;
|
||||
if (this.container.parentNode) {
|
||||
this.container.parentNode.removeChild(this.container);
|
||||
}
|
||||
}
|
||||
|
||||
execute(nodes: Array<VNode>) {
|
||||
const elements = nodes.filter(node => node.tag === 'DomComponent');
|
||||
|
||||
// 有几个 element 就添加几个 OverlayRect
|
||||
while (this.rects.length > elements.length) {
|
||||
const rect = this.rects.pop();
|
||||
rect.remove();
|
||||
}
|
||||
if (elements.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
while (this.rects.length < elements.length) {
|
||||
this.rects.push(new OverlayRect(this.window.document, this.container));
|
||||
}
|
||||
|
||||
const outerBox = {
|
||||
top: Number.POSITIVE_INFINITY,
|
||||
right: Number.NEGATIVE_INFINITY,
|
||||
bottom: Number.NEGATIVE_INFINITY,
|
||||
left: Number.POSITIVE_INFINITY
|
||||
};
|
||||
|
||||
elements.forEach((element, index) => {
|
||||
const eleStyle = getElementStyle(element.realNode);
|
||||
const boxRect = getNestedBoundingClientRect(element.realNode, this.window);
|
||||
|
||||
outerBox.top = Math.min(outerBox.top, boxRect.top - eleStyle.marginTop);
|
||||
outerBox.right = Math.max(outerBox.right, boxRect.left + boxRect.width + eleStyle.marginRight);
|
||||
outerBox.bottom = Math.max(outerBox.bottom, boxRect.top + boxRect.height + eleStyle.marginBottom);
|
||||
outerBox.left = Math.min(outerBox.left, boxRect.left - eleStyle.marginLeft);
|
||||
|
||||
const rect = this.rects[index];
|
||||
rect.update(boxRect, eleStyle);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let elementOverlay: ElementOverlay | null = null;
|
||||
export function hideHighlight() {
|
||||
if (elementOverlay !== null) {
|
||||
elementOverlay.remove();
|
||||
elementOverlay = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function showHighlight(elements: Array<VNode> | null) {
|
||||
if (window.document == null || elements == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (elementOverlay === null) {
|
||||
elementOverlay = new ElementOverlay();
|
||||
}
|
||||
|
||||
elementOverlay.execute(elements);
|
||||
}
|
|
@ -0,0 +1,171 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* 过滤树的抽象逻辑
|
||||
* 需要知道渲染了哪些数据,过滤的字符串/正则表达式
|
||||
* 控制 Tree 组件位置跳转,告知匹配结果
|
||||
* 清空搜索框,告知搜索框当前是第几个结果,转跳搜索结果
|
||||
*
|
||||
* 转跳搜索结果的交互逻辑:
|
||||
* 如果当前页面存在匹配项,页面不动
|
||||
* 如果当前页面不存在匹配项,页面转跳到第一个匹配项位置
|
||||
* 如果匹配项被折叠,需要展开其父节点。注意只展开当前匹配项的父节点,其他匹配项的父节点不展开
|
||||
* 转跳到上一个匹配或下一个匹配项时,如果匹配项被折叠,需要展开其父节点
|
||||
*
|
||||
* 寻找父节点
|
||||
* 找到该节点的缩进值和 index 值,在 data 中向上遍历,通过缩进值判断父节点
|
||||
*/
|
||||
import { useState, useRef } from 'openinula';
|
||||
import { createRegExp } from '../utils/regExpUtil';
|
||||
import { NameObj } from '../parser/parseVNode';
|
||||
|
||||
type BaseType = {
|
||||
id: number;
|
||||
name: NameObj;
|
||||
indentation: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* 把节点的父节点从收起节点数组中删除,并返回新的收起节点数组
|
||||
*
|
||||
* @param item 需要展开父节点的节点
|
||||
* @param data 全部数据
|
||||
* @param collapsedNodes 收起节点数据
|
||||
* @returns 新的收起节点数组
|
||||
*/
|
||||
function expandItemParent(item: BaseType, data: BaseType[], collapsedNodes: BaseType[]): BaseType[] {
|
||||
const index = data.indexOf(item);
|
||||
let currentIndentation = item.indentation;
|
||||
// 不对原始数据进行修改
|
||||
const newCollapsedNodes = [...collapsedNodes];
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
const lastData = data[i];
|
||||
const lastIndentation = lastData.indentation;
|
||||
// 缩进更小,找到了父节点
|
||||
if (lastIndentation < currentIndentation) {
|
||||
// 更新缩进值,只招父节点的父节点,避免修改父节点的兄弟节点的展开状态
|
||||
currentIndentation = lastIndentation;
|
||||
const cIndex = newCollapsedNodes.indexOf(lastData);
|
||||
if (cIndex !== -1) {
|
||||
newCollapsedNodes.splice(cIndex, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newCollapsedNodes;
|
||||
}
|
||||
|
||||
export function FilterTree<T extends BaseType>(props: { data: T[] }) {
|
||||
const { data } = props;
|
||||
const [filterValue, setFilterValue] = useState('');
|
||||
const [currentItem, setCurrentItem] = useState(null);
|
||||
const showItemsRef = useRef([]); // 页面展示的 items
|
||||
const matchItemsRef = useRef([]); //匹配过滤条件的 items
|
||||
const collapsedNodesRef = useRef([]); // 折叠节点,如果匹配 item 被折叠了,需要展开
|
||||
|
||||
const matchItems = matchItemsRef.current;
|
||||
const collapsedNodes = collapsedNodesRef.current;
|
||||
|
||||
const updateCollapsedNodes = (item: BaseType) => {
|
||||
const newCollapsedNodes = expandItemParent(item, data, collapsedNodes);
|
||||
// 如果新旧收起节点数组长度不一致,说明存在收起节点
|
||||
if (newCollapsedNodes.length !== collapsedNodes.length) {
|
||||
// 更新引用,确保 VTree 拿到新的 collapsedNodes
|
||||
collapsedNodesRef.current = newCollapsedNodes;
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeSearchValue = (search: string) => {
|
||||
const reg = createRegExp(search);
|
||||
let newCurrentItem = null;
|
||||
let newMatchItems = [];
|
||||
if (search !== '') {
|
||||
const showItems: T[] = showItemsRef.current;
|
||||
newMatchItems = data.reduce((pre, current) => {
|
||||
const { name } = current;
|
||||
if (name && reg && name.itemName.match(reg)) {
|
||||
pre.push(current);
|
||||
// 如果当前页面显示的 item 存在匹配项,则把他设置为 currentItem
|
||||
if (newCurrentItem === null && showItems.includes(current)) {
|
||||
newCurrentItem = current;
|
||||
}
|
||||
}
|
||||
return pre;
|
||||
}, []);
|
||||
|
||||
if (newMatchItems.length === 0) {
|
||||
setCurrentItem(null);
|
||||
} else {
|
||||
if (newCurrentItem === null) {
|
||||
const item = newMatchItems[0];
|
||||
// 不处于当前展示页面,需要展开父节点
|
||||
updateCollapsedNodes(item);
|
||||
setCurrentItem(item);
|
||||
} else {
|
||||
setCurrentItem(newCurrentItem);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
setCurrentItem(null);
|
||||
}
|
||||
matchItemsRef.current = newMatchItems;
|
||||
setFilterValue(search);
|
||||
};
|
||||
|
||||
const onSelectNext = () => {
|
||||
const index = matchItems.indexOf(currentItem);
|
||||
const nextIndex = index + 1;
|
||||
const item = nextIndex < matchItemsRef.current.length ? matchItems[nextIndex] : matchItems[0];
|
||||
// 可能不处于当前展示页面,需要展开父节点
|
||||
updateCollapsedNodes(item);
|
||||
setCurrentItem(item);
|
||||
};
|
||||
|
||||
const onSelectLast = () => {
|
||||
const index = matchItems.indexOf(currentItem);
|
||||
const last = index - 1;
|
||||
const item = last >= 0 ? matchItems[last] : matchItems[matchItems.length - 1];
|
||||
// 可能不处于当前展示页面,需要展开父节点
|
||||
updateCollapsedNodes(item);
|
||||
setCurrentItem(item);
|
||||
};
|
||||
|
||||
const setShowItems = items => {
|
||||
showItemsRef.current = [...items];
|
||||
};
|
||||
|
||||
const onClear = () => {
|
||||
onChangeSearchValue('');
|
||||
};
|
||||
|
||||
const setCollapsedNodes = items => {
|
||||
// 不更新引用,避免子组件的重复渲染
|
||||
collapsedNodesRef.current.length = 0;
|
||||
collapsedNodesRef.current.push(...items);
|
||||
};
|
||||
|
||||
return {
|
||||
filterValue,
|
||||
onChangeSearchValue,
|
||||
onClear,
|
||||
currentItem,
|
||||
matchItems,
|
||||
onSelectNext,
|
||||
onSelectLast,
|
||||
setShowItems,
|
||||
collapsedNodes,
|
||||
setCollapsedNodes,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,485 @@
|
|||
/*
|
||||
* 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 parseTreeRoot, { clearVNode, queryVNode, VNodeToIdMap } from '../parser/parseVNode';
|
||||
import { packagePayload, checkMessage } from '../utils/transferUtils';
|
||||
import {
|
||||
RequestAllVNodeTreeInfos,
|
||||
AllVNodeTreeInfos,
|
||||
RequestComponentAttrs,
|
||||
ComponentAttrs,
|
||||
DevToolHook,
|
||||
DevToolContentScript,
|
||||
ModifyAttrs,
|
||||
ModifyHooks,
|
||||
ModifyState,
|
||||
ModifyProps,
|
||||
InspectDom,
|
||||
LogComponentData,
|
||||
Highlight,
|
||||
RemoveHighlight,
|
||||
ViewSource,
|
||||
PickElement,
|
||||
StopPickElement,
|
||||
CopyToConsole,
|
||||
StorageValue,
|
||||
} from '../utils/constants';
|
||||
import { VNode } from '../../../inula/src/renderer/vnode/VNode';
|
||||
import { parseVNodeAttrs } from '../parser/parseAttr';
|
||||
import { showHighlight, hideHighlight } from '../highlight';
|
||||
import {
|
||||
FunctionComponent,
|
||||
ClassComponent,
|
||||
IncompleteClassComponent,
|
||||
ForwardRef,
|
||||
MemoComponent
|
||||
} from '../../../inula/src/renderer/vnode/VNodeTags';
|
||||
import { pickElement } from './pickElement';
|
||||
|
||||
const roots = [];
|
||||
let storeDataCount = 0;
|
||||
|
||||
function addIfNotInclude(treeRoot: VNode) {
|
||||
if (!roots.includes(treeRoot)) {
|
||||
roots.push(treeRoot);
|
||||
}
|
||||
}
|
||||
|
||||
function send() {
|
||||
const result = roots.reduce((pre, current) => {
|
||||
const info = parseTreeRoot(helper.travelVNodeTree, current);
|
||||
pre.push(info);
|
||||
return pre;
|
||||
}, []);
|
||||
postMessage(AllVNodeTreeInfos, result);
|
||||
}
|
||||
|
||||
function deleteVNode(vNode: VNode) {
|
||||
// 开发工具中保存了 vNode 的引用,在清理 vNode 的时候需要一并删除
|
||||
clearVNode(vNode);
|
||||
const index = roots.indexOf(vNode);
|
||||
if (index !== -1) {
|
||||
roots.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export function postMessage(type: string, data) {
|
||||
window.postMessage(
|
||||
packagePayload(
|
||||
{
|
||||
type: type,
|
||||
data: data,
|
||||
},
|
||||
DevToolHook
|
||||
),
|
||||
'*'
|
||||
);
|
||||
}
|
||||
|
||||
function parseCompAttrs(id: number) {
|
||||
const vNode = queryVNode(id);
|
||||
if (!vNode) {
|
||||
console.error('Do not find match vNode, this is a bug, please report us.');
|
||||
return;
|
||||
}
|
||||
const parsedAttrs = parseVNodeAttrs(vNode, helper.getHookInfo);
|
||||
postMessage(ComponentAttrs, parsedAttrs);
|
||||
}
|
||||
|
||||
function calculateNextValue(editValue, value, attrPath) {
|
||||
let nextState;
|
||||
const editValueType = typeof editValue;
|
||||
if (
|
||||
editValueType === 'string' ||
|
||||
editValueType === 'undefined' ||
|
||||
editValueType === 'boolean'
|
||||
) {
|
||||
nextState = value;
|
||||
} else if (editValueType === 'number') {
|
||||
const numValue = Number(value);
|
||||
nextState = isNaN(numValue) ? value : numValue; // 如果能转为数字,转数字,不能转数字旧用原值
|
||||
} else if (editValueType === 'object') {
|
||||
if (editValue === null) {
|
||||
nextState = value;
|
||||
} else {
|
||||
const newValue = Array.isArray(editValue) ? [...editValue] : { ...editValue };
|
||||
// 遍历读取到直接指向需要修改值的对象
|
||||
let attr = newValue;
|
||||
for (let i = 0; i < attrPath.length - 1; i++) {
|
||||
attr = attr[attrPath[i]];
|
||||
}
|
||||
// 修改对象上的值
|
||||
attr[attrPath[attrPath.length - 1]] = value;
|
||||
nextState = newValue;
|
||||
}
|
||||
} else {
|
||||
console.error(
|
||||
'The dev tools tried to edit a non-editable value, this is a bug, please report.',
|
||||
editValue
|
||||
);
|
||||
}
|
||||
return nextState;
|
||||
}
|
||||
|
||||
function modifyVNodeAttrs(data) {
|
||||
const { type, id, value, path } = data;
|
||||
const vNode = queryVNode(id);
|
||||
if (!vNode) {
|
||||
console.error('Do not find match vNode, this is a bug, please report us.');
|
||||
return;
|
||||
}
|
||||
if (type === ModifyProps) {
|
||||
const nextProps = calculateNextValue(vNode.props, value, path);
|
||||
helper.updateProps(vNode, nextProps);
|
||||
} else if (type === ModifyHooks) {
|
||||
const hooks = vNode.hooks;
|
||||
const editHook = hooks[path[0]];
|
||||
const hookInfo = helper.getHookInfo(editHook);
|
||||
if (hookInfo) {
|
||||
const editValue = hookInfo.value;
|
||||
// path 的第一个指向 hIndex,从第二个值才开始指向具体属性访问路径
|
||||
const nextState = calculateNextValue(editValue, value, path.slice(1));
|
||||
helper.updateHooks(vNode, path[0], nextState);
|
||||
} else {
|
||||
console.error(
|
||||
'The dev tools tried to edit a non-editable hook, this is a bug, please report.',
|
||||
hooks
|
||||
);
|
||||
}
|
||||
} else if (type === ModifyState) {
|
||||
const oldState = vNode.state || {};
|
||||
const nextState = { ...oldState };
|
||||
let accessRef = nextState;
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
accessRef = accessRef[path[i]];
|
||||
}
|
||||
accessRef[path[path.length - 1]] = value;
|
||||
helper.updateState(vNode, nextState);
|
||||
}
|
||||
}
|
||||
|
||||
function logComponentData(id: number) {
|
||||
const vNode = queryVNode(id);
|
||||
if (vNode == null) {
|
||||
console.warn(`Could not find vNode with id "${id}"`);
|
||||
return null;
|
||||
}
|
||||
if (vNode) {
|
||||
const info = helper.getComponentInfo(vNode);
|
||||
console.log('vNode: ', vNode);
|
||||
console.log('Component Info: ', info);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 path 在 vNode 拿到对应的值
|
||||
*
|
||||
* @param {VNode} vNode dom 节点
|
||||
* @param {Array<string | number>} path 路径
|
||||
* @param {string} attrsName 值的类型(props 或者 hooks)
|
||||
*/
|
||||
const getValueByPath = (
|
||||
vNode: VNode,
|
||||
path: Array<string | number>,
|
||||
attrsName: string
|
||||
) => {
|
||||
if (attrsName === 'Props') {
|
||||
return path.reduce((previousValue, currentValue) => {
|
||||
return previousValue[currentValue];
|
||||
}, vNode.props);
|
||||
} else {
|
||||
// attrsName 为 Hooks
|
||||
if (path.length > 1) {
|
||||
return path.reduce((previousValue, currentValue) => {
|
||||
return previousValue[currentValue];
|
||||
}, vNode.hooks);
|
||||
}
|
||||
return vNode.hooks[path[0]];
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 通过 path 在 vNode 拿到对应的值,并且在控制台打印出来
|
||||
*
|
||||
* @param {number} id idToVNodeMap 的 key 值,通过 id 拿到 VNode
|
||||
* @param {string} itemName 打印出来值的名称
|
||||
* @param {Array<string | number>} path 值的路径
|
||||
* @param {string} attrsName 值的类型
|
||||
*/
|
||||
function logDataWithPath(
|
||||
id: number,
|
||||
itemName: string,
|
||||
path: Array<string | number>,
|
||||
attrsName: string
|
||||
) {
|
||||
const vNode = queryVNode(id);
|
||||
if (vNode === null) {
|
||||
console.warn(`Could not find vNode with id "${id}"`);
|
||||
return null;
|
||||
}
|
||||
if (vNode) {
|
||||
const value = getValueByPath(vNode, path, attrsName);
|
||||
if (attrsName === 'Hooks') {
|
||||
console.log(itemName, value);
|
||||
} else {
|
||||
console.log(`${path[path.length - 1]}`, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过 path 在 vNode 拿到对应的值,并且存为全局变量
|
||||
*
|
||||
* @param {number} id idToVNodeMap 的 key 值,通过 id 拿到 VNode
|
||||
* @param {Array<string |number>} path 值的路径
|
||||
* @param {string} attrsName 值的类型
|
||||
*/
|
||||
function storeDataWithPath(
|
||||
id: number,
|
||||
path: Array<string | number>,
|
||||
attrsName: string
|
||||
) {
|
||||
const vNode = queryVNode(id);
|
||||
if (vNode === null) {
|
||||
console.warn(`Could not find vNode with id "${id}"`);
|
||||
return null;
|
||||
}
|
||||
if (vNode) {
|
||||
const value = getValueByPath(vNode, path, attrsName);
|
||||
const key = `$InulaTemp${storeDataCount++}`;
|
||||
|
||||
window[key] = value;
|
||||
console.log(key);
|
||||
console.log(value);
|
||||
}
|
||||
}
|
||||
|
||||
export let helper;
|
||||
|
||||
function init(inulaHelper) {
|
||||
helper = inulaHelper;
|
||||
(window as any).__INULA_DEV_HOOK__.isInit = true;
|
||||
}
|
||||
|
||||
export function getElement(travelVNodeTree, treeRoot: VNode) {
|
||||
const result: any[] = [];
|
||||
travelVNodeTree(
|
||||
treeRoot,
|
||||
(node: VNode) => {
|
||||
if (node.realNode) {
|
||||
if (Object.keys(node.realNode).length > 0 || node.realNode.size > 0) {
|
||||
result.push(node);
|
||||
}
|
||||
}
|
||||
},
|
||||
(node: VNode) =>
|
||||
node.realNode != null &&
|
||||
(Object.keys(node.realNode).length > 0 || node.realNode.size > 0)
|
||||
);
|
||||
return result;
|
||||
}
|
||||
|
||||
// dev tools 点击眼睛图标功能
|
||||
const inspectDom = data => {
|
||||
const { id } = data;
|
||||
const vNode = queryVNode(id);
|
||||
if (vNode == null) {
|
||||
console.warn(`Could not find vNode with id "${id}"`);
|
||||
return null;
|
||||
}
|
||||
const info = getElement(helper.travelVNodeTree, vNode);
|
||||
if (info) {
|
||||
showHighlight(info);
|
||||
(window as any).__INULA_DEV_HOOK__.$0 = info[0];
|
||||
}
|
||||
};
|
||||
|
||||
const picker = pickElement(window);
|
||||
|
||||
const actions = new Map([
|
||||
// 请求左树所有数据
|
||||
[
|
||||
RequestAllVNodeTreeInfos,
|
||||
() => {
|
||||
send();
|
||||
},
|
||||
],
|
||||
// 请求某个节点的 props,hooks
|
||||
[
|
||||
RequestComponentAttrs,
|
||||
data => {
|
||||
parseCompAttrs(data);
|
||||
},
|
||||
],
|
||||
// 修改 props,hooks
|
||||
[
|
||||
ModifyAttrs,
|
||||
data => {
|
||||
modifyVNodeAttrs(data);
|
||||
},
|
||||
],
|
||||
// 找到节点对应 element
|
||||
[
|
||||
InspectDom,
|
||||
data => {
|
||||
inspectDom(data);
|
||||
},
|
||||
],
|
||||
// 打印节点数据
|
||||
[
|
||||
LogComponentData,
|
||||
data => {
|
||||
logComponentData(data);
|
||||
},
|
||||
],
|
||||
// 高亮
|
||||
[
|
||||
Highlight,
|
||||
data => {
|
||||
const node = queryVNode(data.id);
|
||||
if (node == null) {
|
||||
console.warn(`Could not find vNode with id "${data.id}"`);
|
||||
return null;
|
||||
}
|
||||
const info = getElement(helper.travelVNodeTree, node);
|
||||
showHighlight(info);
|
||||
},
|
||||
],
|
||||
// 移出高亮
|
||||
[
|
||||
RemoveHighlight,
|
||||
() => {
|
||||
hideHighlight();
|
||||
},
|
||||
],
|
||||
// 查看节点源代码位置
|
||||
[
|
||||
ViewSource,
|
||||
data => {
|
||||
const node = queryVNode(data.id);
|
||||
if (node == null) {
|
||||
console.warn(`Could not find vNode with id "${data.id}"`);
|
||||
return null;
|
||||
}
|
||||
showSource(node);
|
||||
},
|
||||
],
|
||||
// 选中页面元素对应 dev tools 节点
|
||||
[
|
||||
PickElement,
|
||||
() => {
|
||||
picker.startPick();
|
||||
},
|
||||
],
|
||||
[
|
||||
StopPickElement,
|
||||
() => {
|
||||
picker.stopPick();
|
||||
},
|
||||
],
|
||||
// 在控制台打印 Props Hooks State 值
|
||||
[
|
||||
CopyToConsole,
|
||||
data => {
|
||||
const node = queryVNode(data.id);
|
||||
if (node == null) {
|
||||
console.warn(`Could not find vNode with id "${data.id}"`);
|
||||
return null;
|
||||
}
|
||||
logDataWithPath(data.id, data.itemName, data.path, data.attrsName);
|
||||
},
|
||||
],
|
||||
// 把 Props Hooks State 值存为全局变量
|
||||
[
|
||||
StorageValue,
|
||||
data => {
|
||||
const node = queryVNode(data.id);
|
||||
if (node == null) {
|
||||
console.warn(`Could not find vNode with id "${data.id}"`);
|
||||
return null;
|
||||
}
|
||||
storeDataWithPath(data.id, data.path, data.attrsName);
|
||||
},
|
||||
],
|
||||
]);
|
||||
|
||||
const showSource = (node: VNode) => {
|
||||
switch (node.tag) {
|
||||
case ClassComponent:
|
||||
case IncompleteClassComponent:
|
||||
case FunctionComponent:
|
||||
global.$type = node.type;
|
||||
break;
|
||||
case ForwardRef:
|
||||
global.$type = node.type.render;
|
||||
break;
|
||||
case MemoComponent:
|
||||
global.$type = node.type.type;
|
||||
break;
|
||||
default:
|
||||
global.$type = null;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleRequest = (type: string, data) => {
|
||||
const action = actions.get(type);
|
||||
if (action) {
|
||||
action.call(this, data);
|
||||
return null;
|
||||
}
|
||||
console.warn('unknown command', type);
|
||||
};
|
||||
|
||||
function injectHook() {
|
||||
if ((window as any).__INULA_DEV_HOOK__) {
|
||||
return;
|
||||
}
|
||||
Object.defineProperty(window, '__INULA_DEV_HOOK__', {
|
||||
enumerable: false,
|
||||
value: {
|
||||
$0: null,
|
||||
init,
|
||||
isInit: false,
|
||||
addIfNotInclude,
|
||||
send,
|
||||
deleteVNode,
|
||||
// inulaX 使用
|
||||
getVNodeId: vNode => {
|
||||
return VNodeToIdMap.get(vNode);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
window.addEventListener('message', function (event) {
|
||||
// 只接收我们自己的消息
|
||||
if (event.source !== window) {
|
||||
return;
|
||||
}
|
||||
const request = event.data;
|
||||
if (checkMessage(request, DevToolContentScript)) {
|
||||
const { payload } = request;
|
||||
const { type, data } = payload;
|
||||
|
||||
// 忽略 inulaX 的 actions
|
||||
if (type.startsWith('inulax')) {
|
||||
return;
|
||||
}
|
||||
handleRequest(type, data);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
injectHook();
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* 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 { PickElement, StopPickElement } from '../utils/constants';
|
||||
import { getElement, helper, postMessage } from './index';
|
||||
import { queryVNode, VNodeToIdMap } from '../parser/parseVNode';
|
||||
import { isUserComponent } from '../parser/parseVNode';
|
||||
import { throttle } from 'lodash';
|
||||
import { hideHighlight, showHighlight } from '../highlight';
|
||||
|
||||
// 判断鼠标移入节点是否为 dev tools 上的节点,如果不是则找父节点
|
||||
function getUserComponent(target) {
|
||||
if (target.tag && isUserComponent(target.tag)) {
|
||||
return target;
|
||||
}
|
||||
while (target.tag && !isUserComponent(target.tag)) {
|
||||
if (target.parent) {
|
||||
target = target.parent;
|
||||
}
|
||||
}
|
||||
return target;
|
||||
}
|
||||
|
||||
function onMouseEvent(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
const target = (event.target as any)._inula_VNode;
|
||||
if (target) {
|
||||
const id = VNodeToIdMap.get(getUserComponent(target));
|
||||
const vNode = queryVNode(id);
|
||||
if (vNode == null) {
|
||||
console.warn(`Could not find vNode with id "${id}"`);
|
||||
return null;
|
||||
}
|
||||
const info = getElement(helper.travelVNodeTree, vNode);
|
||||
if (info) {
|
||||
showHighlight(info);
|
||||
}
|
||||
|
||||
// 0.5 秒内在节流结束后只触发一次
|
||||
throttle(
|
||||
() => {
|
||||
postMessage(PickElement, id);
|
||||
},
|
||||
500,
|
||||
{ leading: false, trailing: true }
|
||||
)();
|
||||
}
|
||||
}
|
||||
|
||||
export function pickElement(window: Window) {
|
||||
function onClick(event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
stopPick();
|
||||
postMessage(StopPickElement, null);
|
||||
}
|
||||
|
||||
const startPick = () => {
|
||||
if (window && typeof window.addEventListener === 'function') {
|
||||
window.addEventListener('click', onClick, true);
|
||||
window.addEventListener('mousedown', onMouseEvent, true);
|
||||
window.addEventListener('mousemove', onMouseMove, true);
|
||||
window.addEventListener('mouseup', onMouseEvent, true);
|
||||
}
|
||||
};
|
||||
|
||||
const stopPick = () => {
|
||||
hideHighlight();
|
||||
window.removeEventListener('click', onClick, true);
|
||||
window.removeEventListener('mousedown', onMouseEvent, true);
|
||||
window.removeEventListener('mousemove', onMouseMove, true);
|
||||
window.removeEventListener('mouseup', onMouseEvent, true);
|
||||
};
|
||||
|
||||
return { startPick, stopPick };
|
||||
}
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* 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 { render, createElement } from 'openinula';
|
||||
import Panel from '../panel/Panel';
|
||||
import PanelX from '../panelX/PanelX';
|
||||
|
||||
let panelCreated = false;
|
||||
|
||||
const viewSource = () => {
|
||||
setTimeout(() => {
|
||||
chrome.devtools.inspectedWindow.eval(`
|
||||
if (window.$type != null) {
|
||||
if (
|
||||
window.$type &&
|
||||
window.$type.prototype &&
|
||||
window.$type.prototype.render
|
||||
) {
|
||||
// 类组件
|
||||
inspect(window.$type.prototype.render);
|
||||
} else {
|
||||
// 函数组件
|
||||
inspect(window.$type);
|
||||
}
|
||||
}
|
||||
`);
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const inspectVNode = () => {
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
`
|
||||
window.__INULA_DEV_HOOK__ && window.__INULA_DEV_HOOK__.$0 !== $0
|
||||
? (inspect(window.__INULA_DEV_HOOK__.$0.realNode), true)
|
||||
: false
|
||||
`,
|
||||
(_, error) => {
|
||||
if (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
let currentPanel = null;
|
||||
|
||||
chrome.devtools.inspectedWindow.eval(
|
||||
'window.__INULA_DEV_HOOK__',
|
||||
function (isInula, error) {
|
||||
if (!isInula || panelCreated) {
|
||||
return;
|
||||
}
|
||||
|
||||
panelCreated = true;
|
||||
chrome.devtools.panels.create(
|
||||
'Inula',
|
||||
'',
|
||||
'panel.html',
|
||||
(extensionPanel) => {
|
||||
extensionPanel.onShown.addListener((panel) => {
|
||||
if (currentPanel === panel) {
|
||||
return;
|
||||
}
|
||||
currentPanel = panel;
|
||||
const container = panel.document.getElementById('root');
|
||||
const element = createElement(Panel, { viewSource, inspectVNode });
|
||||
render(element, container);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
chrome.devtools.panels.create(
|
||||
'InulaX',
|
||||
'',
|
||||
'panelX.html',
|
||||
(extensionPanel) => {
|
||||
extensionPanel.onShown.addListener((panel) => {
|
||||
if (currentPanel === panel) {
|
||||
return;
|
||||
}
|
||||
currentPanel = panel;
|
||||
const container = panel.document.getElementById('root');
|
||||
const element = createElement(PanelX, {});
|
||||
render(element, container);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
);
|
|
@ -0,0 +1,30 @@
|
|||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Security-Policy"
|
||||
content="default-src *; style-src 'self' 'unsafe-inline'; srcipt-src 'self' 'unsafe-inline' 'unsafe-eval' ">
|
||||
<script src="inula.development.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<p>Inula dev tools!</p>
|
||||
</div>
|
||||
</body>
|
||||
<script> src="main.js"</script>
|
||||
</html>
|
|
@ -0,0 +1,35 @@
|
|||
{
|
||||
"name": "Inula dev tools",
|
||||
"description": "Inula chrome dev extension",
|
||||
"version": "1.0",
|
||||
"minimum_chrome_version": "10.0",
|
||||
"manifest_version": 3,
|
||||
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
|
||||
"background": {
|
||||
"script": [
|
||||
"background.js"
|
||||
],
|
||||
"persistent": true
|
||||
},
|
||||
"permissions": [
|
||||
"file:///*",
|
||||
"http://*/*",
|
||||
"https://*/*"
|
||||
],
|
||||
"devtools_page": "main.html",
|
||||
"content_scripts": [
|
||||
{
|
||||
"matches": [
|
||||
"<all_urls>"
|
||||
],
|
||||
"js": [
|
||||
"contentScript.js"
|
||||
],
|
||||
"run_at": "document_start"
|
||||
}
|
||||
],
|
||||
"web_accessible_resources": [
|
||||
"injector.js",
|
||||
"background.js"
|
||||
]
|
||||
}
|
|
@ -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>
|
|
@ -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';
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
/*
|
||||
* 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 Inula, { useState } from 'openinula';
|
||||
import { Modal } from './Modal';
|
||||
import { highlight, sendMessage } from './utils';
|
||||
|
||||
function executeAction(storeId: string, name: string, args: any[]) {
|
||||
sendMessage({
|
||||
type: 'inulax run action',
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
storeId,
|
||||
action: name,
|
||||
args,
|
||||
});
|
||||
}
|
||||
|
||||
function queryAction(storeId: string, name: string, args: any[]) {
|
||||
sendMessage({
|
||||
type: 'inulax queue action',
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
storeId,
|
||||
action: name,
|
||||
args,
|
||||
});
|
||||
}
|
||||
|
||||
export function ActionRunner({ foo, storeId, actionName }) {
|
||||
const [data, setState] = useState({
|
||||
modal: false,
|
||||
gatheredAttrs: [],
|
||||
query: false,
|
||||
});
|
||||
const modalIsOpen = data.modal;
|
||||
const gatheredAttrs = data.gatheredAttrs;
|
||||
function setData(val) {
|
||||
const newData = {
|
||||
modal: data.modal,
|
||||
gatheredAttrs: data.gatheredAttrs,
|
||||
};
|
||||
|
||||
Object.entries(val).forEach(([key, value]) => (newData[key] = value));
|
||||
|
||||
setState(newData as any);
|
||||
}
|
||||
|
||||
const plainFunction = foo.replace(/\{.*}/gms, '');
|
||||
const attributes = plainFunction
|
||||
.replace(/^.*\(/g, '')
|
||||
.replace(/\).*$/, '')
|
||||
.split(/, ?/)
|
||||
.filter((item, index) => index > 0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<span
|
||||
title={'Run action'}
|
||||
onClick={() => {
|
||||
if (attributes.length > 0) {
|
||||
setData({ modal: false, gatheredAttrs: [], query: false });
|
||||
} else {
|
||||
executeAction(storeId, actionName, gatheredAttrs);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<b
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
☼
|
||||
<span
|
||||
title={'Add to action queue'}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
if (attributes.len > 0) {
|
||||
setData({ modal: true, gatheredAttrs: [], query: true });
|
||||
} else {
|
||||
queryAction(storeId, actionName, gatheredAttrs);
|
||||
}
|
||||
}}
|
||||
>
|
||||
⌛︎{' '}
|
||||
</span>
|
||||
</b>
|
||||
<span>
|
||||
<i>{plainFunction}</i>
|
||||
{' {...}'}
|
||||
</span>
|
||||
</span>
|
||||
{modalIsOpen ? (
|
||||
<Modal
|
||||
closeModal={() => {
|
||||
setData({ modal: false });
|
||||
}}
|
||||
then={data => {
|
||||
if (gatheredAttrs.length === attributes.length - 1) {
|
||||
setData({ modal: false });
|
||||
executeAction(storeId, actionName, gatheredAttrs.concat(data));
|
||||
} else {
|
||||
setData({
|
||||
gatheredAttrs: gatheredAttrs.concat([data]),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
<h3>{data.query ? 'Query action:' : 'Run action:'}</h3>
|
||||
<p>{highlight(plainFunction, attributes[gatheredAttrs.length])}</p>
|
||||
</Modal>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,335 @@
|
|||
/*
|
||||
* 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 } from 'openinula';
|
||||
import styles from './PanelX.less';
|
||||
import { Tree } from './Tree';
|
||||
import {displayValue, omit} from './utils';
|
||||
|
||||
type Mutation = {
|
||||
mutation: boolean;
|
||||
items?: Mutation[];
|
||||
attributes?: { [key: string]: Mutation };
|
||||
values?: Mutation[];
|
||||
entries?: Mutation[];
|
||||
from?: any;
|
||||
to?: any;
|
||||
};
|
||||
|
||||
export function DiffTree({
|
||||
mutation,
|
||||
indent = 0,
|
||||
index = '',
|
||||
expand = false,
|
||||
search = '',
|
||||
forcedExpand = false,
|
||||
omitAttrs = [],
|
||||
doNotDisplayIcon = false,
|
||||
forcedLabel = null,
|
||||
className,
|
||||
}: {
|
||||
mutation: Mutation;
|
||||
indent: number;
|
||||
index?: string | number;
|
||||
expand?: boolean;
|
||||
search: string;
|
||||
forcedExpand?: boolean;
|
||||
omitAttrs: string[];
|
||||
doNotDisplayIcon?: boolean;
|
||||
forcedLabel?: string | number | null;
|
||||
className?: string;
|
||||
}) {
|
||||
if (omitAttrs.length && mutation.attributes) {
|
||||
mutation.attributes = omit(mutation.attributes, ...omitAttrs);
|
||||
mutation.from = mutation.from && omit(mutation.from, ...omitAttrs);
|
||||
mutation.to = mutation.to && omit(mutation.to, ...omitAttrs);
|
||||
}
|
||||
const [expanded, setExpanded] = useState(expand);
|
||||
|
||||
const deleted = mutation.mutation && !('to' in mutation);
|
||||
const newValue = mutation.mutation && !('from' in mutation);
|
||||
const mutated = mutation.mutation;
|
||||
|
||||
const isArray = mutated && mutation.items;
|
||||
const isObject = mutated && mutation.attributes;
|
||||
const isMap = mutated && mutation.entries;
|
||||
const isSet = mutated && mutation.values;
|
||||
const isPrimitive = !isArray && !isObject && !isMap && !isSet;
|
||||
|
||||
if (!mutated) {
|
||||
return (
|
||||
<Tree
|
||||
data={mutation.to}
|
||||
indent={indent}
|
||||
search={search}
|
||||
expand={expand}
|
||||
forcedExpand={forcedExpand}
|
||||
omitAttrs={omitAttrs}
|
||||
forcedLabel={forcedLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (newValue) {
|
||||
return (
|
||||
<Tree
|
||||
data={mutation.to}
|
||||
indent={indent}
|
||||
search={search}
|
||||
expand={expand}
|
||||
forcedExpand={forcedExpand}
|
||||
className={styles.added}
|
||||
omitAttrs={omitAttrs}
|
||||
forcedLabel={forcedLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (deleted) {
|
||||
return (
|
||||
<Tree
|
||||
data={mutation.from}
|
||||
indent={indent}
|
||||
search={search}
|
||||
expand={expand}
|
||||
forcedExpand={forcedExpand}
|
||||
className={styles.deleted}
|
||||
omitAttrs={omitAttrs}
|
||||
forcedLabel={forcedLabel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
fontFamily: 'monospace',
|
||||
}}
|
||||
className={`${
|
||||
expanded
|
||||
? 'expanded'
|
||||
: `not-expanded ${
|
||||
mutated && !isPrimitive && !expanded ? styles.changed : ''
|
||||
}`
|
||||
}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
>
|
||||
{new Array(Math.max(indent, 0)).fill(<span> </span>)}
|
||||
{isPrimitive ? (
|
||||
// 如果两个 value 是基本变量并且不同,则简单显示不同点
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Tree
|
||||
data={mutation.from}
|
||||
indent={indent}
|
||||
search={search}
|
||||
index={index}
|
||||
className={styles.deleted}
|
||||
omitAttrs={omitAttrs}
|
||||
/>
|
||||
<Tree
|
||||
data={mutation.to}
|
||||
indent={indent}
|
||||
search={search}
|
||||
index={index}
|
||||
className={styles.added}
|
||||
omitAttrs={omitAttrs}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
// 如果至少有一个是复杂变量,则需要展开按钮
|
||||
<>
|
||||
{forcedExpand ? '' : expanded ? <span>▼</span> : <span>▶</span>}
|
||||
{index === 0 || index ? (
|
||||
<b className={styles.purple}>{displayValue(index, search)}: </b>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{isArray ? (
|
||||
// 如果都是数组进行比较
|
||||
expanded ? (
|
||||
[
|
||||
Array(Math.max(mutation.from.length, mutation.to.length))
|
||||
.fill(true)
|
||||
.map((i, index) => {
|
||||
return (
|
||||
<div>
|
||||
{mutation.items[index].mutation ? (
|
||||
<DiffTree
|
||||
mutation={{
|
||||
...mutation.items[index],
|
||||
to: mutation.to[index],
|
||||
}}
|
||||
indent={indent}
|
||||
search={search}
|
||||
omitAttrs={omitAttrs}
|
||||
forcedLabel={index}
|
||||
/>
|
||||
) : (
|
||||
<Tree
|
||||
data={mutation.to[index]}
|
||||
indent={indent}
|
||||
search={search}
|
||||
index={index}
|
||||
className={styles.default}
|
||||
omitAttrs={omitAttrs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}),
|
||||
]
|
||||
) : (
|
||||
forcedLabel || `Array(${mutation.to?.length})`
|
||||
)
|
||||
) : isSet ? (
|
||||
expanded ? (
|
||||
<div>
|
||||
<div>
|
||||
{forcedLabel || `Set(${mutation.to?.values.length})`}
|
||||
</div>
|
||||
{Array(
|
||||
Math.max(
|
||||
mutation.from?.values.length,
|
||||
mutation.to?.values.length
|
||||
)
|
||||
)
|
||||
.fill(true)
|
||||
.map((i ,index) => (
|
||||
<div>
|
||||
{mutation.values[index].mutation ? (
|
||||
<DiffTree
|
||||
mutation={{
|
||||
...mutation.values[index],
|
||||
}}
|
||||
indent={indent + 2}
|
||||
search={search}
|
||||
omitAttrs={omitAttrs}
|
||||
/>
|
||||
) : (
|
||||
<Tree
|
||||
data={mutation.to?.values[index]}
|
||||
indent={indent + 2}
|
||||
search={search}
|
||||
className={styles.default}
|
||||
omitAttrs={omitAttrs}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span>
|
||||
{forcedLabel || `Set(${mutation.to?.values.length})`}
|
||||
</span>
|
||||
)
|
||||
) : isMap ? (
|
||||
expanded ? (
|
||||
<>
|
||||
<span>
|
||||
{forcedLabel || `Map(${mutation.to?.entries.length})`}
|
||||
</span>
|
||||
{Array(
|
||||
Math.max(
|
||||
mutation.from?.entries.length,
|
||||
mutation.to?.entries.length
|
||||
)
|
||||
)
|
||||
.fill(true)
|
||||
.map((i, index) =>
|
||||
mutation.entries[index].mutation ? (
|
||||
<div>
|
||||
<DiffTree
|
||||
mutation={{
|
||||
...mutation.entries[index],
|
||||
}}
|
||||
indent={indent + 2}
|
||||
search={search}
|
||||
omitAttrs={omitAttrs}
|
||||
forcedLabel={'[map item]'}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Tree
|
||||
data={mutation.to?.entries[index]}
|
||||
indent={indent + 2}
|
||||
search={search}
|
||||
className={styles.default}
|
||||
omitAttrs={omitAttrs}
|
||||
forcedLabel={'[map item]'}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<span>
|
||||
{forcedLabel || `Map(${mutation.to?.entries.length})`}
|
||||
</span>
|
||||
)
|
||||
) : expanded ? (
|
||||
// 如果都是 object 进行比较
|
||||
Object.entries(mutation.attributes).map(([key, item]) => {
|
||||
return item.mutation ? (
|
||||
<span onClick={e => e.stopPropagation()}>
|
||||
{
|
||||
<DiffTree
|
||||
mutation={item}
|
||||
index={key}
|
||||
indent={indent}
|
||||
search={search}
|
||||
className={!expanded && mutated ? '' : styles.changed}
|
||||
omitAttrs={omitAttrs}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
) : (
|
||||
<span onClick={e => e.stopPropagation()}>
|
||||
{
|
||||
<Tree
|
||||
data={mutation.to[key]}
|
||||
index={key}
|
||||
indent={indent}
|
||||
search={search}
|
||||
className={styles.default}
|
||||
omitAttrs={omitAttrs}
|
||||
/>
|
||||
}
|
||||
</span>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
forcedLabel || '{ ... }'
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,402 @@
|
|||
/*
|
||||
* 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 { useEffect, useState, useRef } from 'openinula';
|
||||
import { DevToolPanel } from '../utils/constants';
|
||||
import {
|
||||
initBackgroundConnection,
|
||||
addBackgroundMessageListener,
|
||||
removeBackgroundMessageListener,
|
||||
} from '../panelConnection';
|
||||
import { Table } from './Table';
|
||||
import { Tree } from './Tree';
|
||||
import {fullTextSearch, omit} from './utils';
|
||||
import styles from './PanelX.less';
|
||||
import { Checkbox } from '../utils/Checkbox';
|
||||
import { DiffTree } from './DiffTree';
|
||||
|
||||
const eventTypes = {
|
||||
INITIALIZED: 'inulax store initialized',
|
||||
STATE_CHANGE: 'inulax state change',
|
||||
SUBSCRIBED: 'inulax subscribed',
|
||||
UNSUBSCRIBED: 'inulax unsubscribed',
|
||||
ACTION: 'inulax action',
|
||||
ACTION_QUEUED: 'inulax action queued',
|
||||
QUEUE_PENDING: 'inulax queue pending',
|
||||
QUEUE_FINISHED: 'inulax queue finished',
|
||||
};
|
||||
|
||||
const otherTypes = {
|
||||
GET_EVENTS: 'inulax getEvents',
|
||||
GET_PERSISTENCE: 'inulax getPersistence',
|
||||
EVENTS: 'inulax events',
|
||||
FLUSH_EVENTS: 'inulax flush events',
|
||||
SET_PERSISTENT: 'inulax setPersistent',
|
||||
RESET_EVENTS: 'inulax resetEvents',
|
||||
};
|
||||
|
||||
function extractDataByType(message, search) {
|
||||
if (message.type === eventTypes.ACTION) {
|
||||
return (
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<Tree
|
||||
data={{
|
||||
Action: `${message.data.action.action}${
|
||||
message.data.fromQueue ? ' (queued)' : ''
|
||||
}`
|
||||
}}
|
||||
expand={true}
|
||||
indent={-4}
|
||||
forcedExpand={true}
|
||||
search={search}
|
||||
omitAttrs={['_inulaObserver']}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (message.type === eventTypes.STATE_CHANGE) {
|
||||
return (
|
||||
<div
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<b>{`${message.data.change.vNodes.length} nodes changed:`}</b>
|
||||
<Tree
|
||||
data={message.data.change.vNodes.map(vNode => {
|
||||
return (
|
||||
<span>
|
||||
<i>{vNode.type}</i>()
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <span className={styles.grey}>N/A</span>
|
||||
}
|
||||
|
||||
export default function EventLog({ setNextStore, setEventFilter, eventFilter }) {
|
||||
const [log, setLog] = useState([]);
|
||||
const [initlized, setInitlized] = useState(false);
|
||||
const [persistent, setPersistent] = useState(false);
|
||||
const filterField = useRef(null);
|
||||
|
||||
const addFilter = (key, value) => {
|
||||
const filters = { ...eventFilter };
|
||||
filters[key] = value;
|
||||
|
||||
setEventFilter(filters);
|
||||
};
|
||||
|
||||
const removeFilter = key => {
|
||||
const filters = { ...eventFilter };
|
||||
delete filters[key];
|
||||
|
||||
setEventFilter(filters);
|
||||
};
|
||||
|
||||
if (!initlized) {
|
||||
setTimeout(() => {
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: otherTypes.GET_EVENTS,
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
},
|
||||
from: DevToolPanel,
|
||||
});
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: otherTypes.GET_PERSISTENCE,
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
},
|
||||
from: DevToolPanel,
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const lisener = message => {
|
||||
if (message.payload.type.startsWith('inulax')) {
|
||||
if (message.payload.type === otherTypes.EVENTS) {
|
||||
setLog(message.payload.events);
|
||||
setInitlized(true);
|
||||
} else if (message.payload.type === otherTypes.SET_PERSISTENT) {
|
||||
setPersistent(message.payload.persistent);
|
||||
} else if (message.payload.type === otherTypes.FLUSH_EVENTS) {
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: otherTypes.GET_EVENTS,
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
},
|
||||
from: DevToolPanel,
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
initBackgroundConnection('panel');
|
||||
addBackgroundMessageListener(lisener);
|
||||
return () => {
|
||||
removeBackgroundMessageListener(lisener);
|
||||
};
|
||||
});
|
||||
|
||||
const filters = Object.entries(eventFilter);
|
||||
const usedTypes = { all: 0 };
|
||||
|
||||
const processedData = log
|
||||
.filter(event => {
|
||||
if (!Object.values(eventTypes).includes(event.message.type)) {
|
||||
return false;
|
||||
}
|
||||
usedTypes.all++;
|
||||
if (!usedTypes[event.message.type]) {
|
||||
usedTypes[event.message.type] = 1;
|
||||
} else {
|
||||
usedTypes[event.message.type]++;
|
||||
}
|
||||
|
||||
if (!filters.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !filters.some(([key, value]) => {
|
||||
if (key === 'fulltext') {
|
||||
const result = fullTextSearch(event, value);
|
||||
return !result;
|
||||
}
|
||||
const keys = key.split('.');
|
||||
let search = event;
|
||||
keys.forEach(attr => {
|
||||
search = search[attr];
|
||||
});
|
||||
return value !== search;
|
||||
});
|
||||
})
|
||||
.map(event => {
|
||||
const date = new Date(event.timestamp);
|
||||
|
||||
return {
|
||||
id: event.id,
|
||||
timestamp: event.timestamp,
|
||||
type: event.message.type,
|
||||
time: `${date.toLocaleTimeString()} - ${date.toLocaleDateString()}`,
|
||||
state: event.message.type === eventTypes.STATE_CHANGE ? (
|
||||
<DiffTree
|
||||
mutation={event.message.data.change.mutation}
|
||||
expand={true}
|
||||
forcedExpand={true}
|
||||
indent={0}
|
||||
search={eventFilter['fulltext']}
|
||||
omitAttrs={['_inulaObserver']}
|
||||
doNotDisplayIcon={true}
|
||||
/>
|
||||
) : (
|
||||
<Tree
|
||||
data={event.message.data.store.$s}
|
||||
expand={true}
|
||||
search={eventFilter['fulltext']}
|
||||
forcedExpand={true}
|
||||
indent={-4}
|
||||
omitAttrs={['_inulaObserver']}
|
||||
/>
|
||||
),
|
||||
storeClick: (
|
||||
<span
|
||||
className={styles.link}
|
||||
onClick={e => {
|
||||
e.preventDefault();
|
||||
setNextStore(event.message.data.store.id);
|
||||
}}
|
||||
>
|
||||
{event.message.data.store.id}
|
||||
</span>
|
||||
),
|
||||
additionalData: extractDataByType(
|
||||
event.message,
|
||||
eventFilter['fulltext']
|
||||
),
|
||||
storeId: event.message.data.store.id,
|
||||
event,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginTop: '0px', margin: '5px' }}>
|
||||
<input
|
||||
ref={filterField}
|
||||
type={'text'}
|
||||
placeholder={'Filter:'}
|
||||
className={`${styles.compositeInput} ${styles.left}`}
|
||||
onKeyUp={() => {
|
||||
if (!filterField.current.value) {
|
||||
removeFilter('fulltext');
|
||||
}
|
||||
addFilter('fulltext', filterField.current.value);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
className={`${styles.bold} ${styles.compositeInput} ${styles.right}`}
|
||||
onClick={() => {
|
||||
filterField.current.value = '';
|
||||
removeFilter('fulltext');
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
<span className={styles.grey}>{' | '}</span>
|
||||
<span
|
||||
style={{
|
||||
cursor: 'pointer'
|
||||
}}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: otherTypes.SET_PERSISTENT,
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
persistent: !persistent,
|
||||
},
|
||||
from: DevToolPanel,
|
||||
});
|
||||
|
||||
setPersistent(!persistent);
|
||||
}}
|
||||
>
|
||||
<Checkbox value={persistent}></Checkbox> Persistent events
|
||||
</span>
|
||||
{' | '}
|
||||
<button
|
||||
onClick={() => {
|
||||
// 重置 events
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload: {
|
||||
type: otherTypes.RESET_EVENTS,
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
},
|
||||
from: DevToolPanel,
|
||||
});
|
||||
}}
|
||||
>
|
||||
Reset
|
||||
</button>
|
||||
{eventFilter['message.data.store.id'] ? (
|
||||
<span>
|
||||
{' | '}
|
||||
<b
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={() => {
|
||||
setNextStore(eventFilter['message.data.store.id']);
|
||||
}}
|
||||
>{` Displaying: [${eventFilter['message.data.store.id']}] `}</b>
|
||||
<button
|
||||
onClick={() => {
|
||||
removeFilter('message.data.store.id');
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div style={{ marginTop: '0px', margin: '5px' }}>
|
||||
<button
|
||||
className={`${styles.filterButton} ${log.length ? '' : styles.grey} ${
|
||||
eventFilter['message.type'] ? '' : styles.active
|
||||
}`}
|
||||
onClick={() => {
|
||||
removeFilter('message.type');
|
||||
}}
|
||||
>
|
||||
All({usedTypes.all})
|
||||
</button>
|
||||
{Object.values(eventTypes).map(eventType => {
|
||||
return (
|
||||
<button
|
||||
className={`${styles.filterButton} ${
|
||||
usedTypes[eventType] ? '' : styles.grey
|
||||
} ${
|
||||
eventFilter['message.type'] === eventType ? styles.active : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
addFilter('message.type', eventType);
|
||||
}}
|
||||
>
|
||||
{`${eventType.replace('inulax ', '')}(${
|
||||
usedTypes[eventType] || 0
|
||||
})`}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Table
|
||||
data={processedData}
|
||||
dataKey={'id'}
|
||||
displayKeys={[
|
||||
['type', 'Event type:'],
|
||||
['storeClick', 'Store:'],
|
||||
['time', 'Time:'],
|
||||
['state', 'State:'],
|
||||
['additionalData', 'Additional data:'],
|
||||
]}
|
||||
displayDataProcessor={data => {
|
||||
const message = data.event.message;
|
||||
return {
|
||||
type: data.type,
|
||||
store: {
|
||||
actions: Object.fromEntries(
|
||||
Object.entries(message.data.store.$config.actions).map(
|
||||
([id, action]) => {
|
||||
return [
|
||||
id,
|
||||
(action as string).replace(/\{.*}/gms, '{...}').replace('function ', ''),
|
||||
];
|
||||
}
|
||||
)
|
||||
),
|
||||
computed: Object.fromEntries(
|
||||
Object.keys(message.data.store.$c).map(key => [
|
||||
key,
|
||||
message.data.store.expanded[key],
|
||||
])
|
||||
),
|
||||
state: message.data.store.$s,
|
||||
id: message.data.store.id,
|
||||
},
|
||||
// data: omit(data, 'storeClick', 'additionalData'),
|
||||
};
|
||||
}}
|
||||
search={eventFilter.fulltext ? eventFilter.fulltext : ''}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 Inula, { useRef, useState } from 'openinula';
|
||||
|
||||
export function Modal({
|
||||
closeModal,
|
||||
then,
|
||||
children,
|
||||
}: {
|
||||
closeModal: () => void;
|
||||
then: (value: any) => void;
|
||||
children?: any[];
|
||||
}) {
|
||||
const inputRef = useRef(null);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
setTimeout(() => {
|
||||
inputRef.current.focus();
|
||||
inputRef.current.value = '';
|
||||
}, 10);
|
||||
|
||||
const tryGatherData = () => {
|
||||
let data;
|
||||
try {
|
||||
data = eval(inputRef.current.value);
|
||||
} catch (err) {
|
||||
setError(err);
|
||||
return;
|
||||
}
|
||||
|
||||
if (then) {
|
||||
then(data);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'fixed',
|
||||
width: '100vw',
|
||||
height: '100vh',
|
||||
top: 0,
|
||||
left: 0,
|
||||
backgroundColor: 'rgba(0, 0, 0 , 0.3)',
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
top: 'calc(50vh - 50px)',
|
||||
left: 'calc(50vw - 125px)',
|
||||
width: '250px',
|
||||
backgroundColor: 'white',
|
||||
border: '1px solid black',
|
||||
position: 'fixed',
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<p>{children}</p>
|
||||
<p>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type={'text'}
|
||||
onKeyPress={({key}) => {
|
||||
if (key === 'Enter') {
|
||||
tryGatherData();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</p>
|
||||
{error ? <p>Variable parsing error</p> : null}
|
||||
<p>
|
||||
<button
|
||||
onClick={() => {
|
||||
tryGatherData();
|
||||
}}
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,197 @@
|
|||
/*
|
||||
* 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';
|
||||
|
||||
.displayData {
|
||||
background-color: rgb(241, 243, 244);
|
||||
}
|
||||
|
||||
.app {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
font-size: @common-font-size;
|
||||
}
|
||||
|
||||
div.wrapper {
|
||||
margin: 15px;
|
||||
position: relative;
|
||||
width: calc(100% - 30px);
|
||||
display: block;
|
||||
}
|
||||
|
||||
div.table {
|
||||
display: table;
|
||||
vertical-align: top;
|
||||
width: calc(100%);
|
||||
background-color: white;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
div.row {
|
||||
display: table-row;
|
||||
|
||||
&:nth-child(2n + 1) {
|
||||
background-color: rgb(241, 243, 244);
|
||||
.default {
|
||||
background-color: rgb(241, 243, 244);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div.cell {
|
||||
display: table-cell;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
div.half {
|
||||
width: calc(50% - 8px);
|
||||
float: left;
|
||||
}
|
||||
|
||||
div.header {
|
||||
background-color: rgb(241, 243, 244);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
div.row.active {
|
||||
background-color: #00a;
|
||||
color: white;
|
||||
}
|
||||
|
||||
button.tab {
|
||||
border: 1px solid grey;
|
||||
border-top-left-radius: 5px;
|
||||
border-top-right-radius: 5px;
|
||||
&.active {
|
||||
border-bottom: none;
|
||||
color: black;
|
||||
font-weight: bold;
|
||||
background-color: white;
|
||||
}
|
||||
}
|
||||
|
||||
span.highlighted {
|
||||
background-color: #ff0;
|
||||
}
|
||||
|
||||
.grey {
|
||||
color: grey;
|
||||
}
|
||||
|
||||
.red {
|
||||
color: #a00;
|
||||
}
|
||||
|
||||
.blue {
|
||||
color: #00a;
|
||||
}
|
||||
|
||||
.purple {
|
||||
color: #909;
|
||||
}
|
||||
|
||||
.bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-weight: bold;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
color: #00a;
|
||||
}
|
||||
|
||||
.compositeInput {
|
||||
background-color: white;
|
||||
border: 1px solid grey;
|
||||
display: inline-block;
|
||||
border-radius: 0;
|
||||
padding: 5px;
|
||||
&.left {
|
||||
border-right: 0;
|
||||
margin-right: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&.right {
|
||||
border-left: 0;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.filterButton {
|
||||
background-color: transparent;
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
border: 0;
|
||||
&.active {
|
||||
background-color: #ddd;
|
||||
}
|
||||
}
|
||||
|
||||
.added {
|
||||
background-color: #afa;
|
||||
&::before {
|
||||
font-weight: bold;
|
||||
color: #0a0;
|
||||
}
|
||||
}
|
||||
|
||||
.deleted {
|
||||
background-color: #faa;
|
||||
text-decoration-line: line-through;
|
||||
&::before {
|
||||
font-weight: bold;
|
||||
color: #a00;
|
||||
}
|
||||
}
|
||||
|
||||
.changed {
|
||||
background-color: #ffa;
|
||||
&::before {
|
||||
font-weight: bold;
|
||||
color: #ca0;
|
||||
}
|
||||
}
|
||||
|
||||
.default {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.floatingButton {
|
||||
right: 5px;
|
||||
position: absolute;
|
||||
height: 17px;
|
||||
width: 17px;
|
||||
font-size: 10px;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.scrollable {
|
||||
max-height: calc(100vh - 65px);
|
||||
overflow: auto;
|
||||
|
||||
div.row {
|
||||
display: block;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* 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 } from 'openinula';
|
||||
import EventLog from './EventLog';
|
||||
import Stores from './Stores';
|
||||
import styles from './PanelX.less';
|
||||
|
||||
export default function PanelX() {
|
||||
const [active, setActive] = useState('stores');
|
||||
const [nextStoreId, setNextStoreId] = useState(null);
|
||||
const [eventFilter, setEventFilter] = useState({});
|
||||
|
||||
function showFilterEvents(filter) {
|
||||
setActive('events');
|
||||
setEventFilter(filter);
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{
|
||||
id: 'stores',
|
||||
title: 'Stores',
|
||||
getComponent: () => (
|
||||
<Stores
|
||||
nextStoreId={nextStoreId}
|
||||
showFilteredEvents={showFilterEvents}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'events',
|
||||
title: 'Events',
|
||||
getComponents: () => (
|
||||
<EventLog
|
||||
setNextStore={id => {
|
||||
setNextStoreId(id);
|
||||
setActive('stores');
|
||||
}}
|
||||
setEventFilter={setEventFilter}
|
||||
eventFilter={eventFilter}
|
||||
/>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: '10px' }}>
|
||||
{tabs.map(tab =>
|
||||
tab.id === active ? (
|
||||
<button
|
||||
className={`${styles.tab} ${styles.active}`}
|
||||
disabled={true}
|
||||
>
|
||||
{tab.title}
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
className={styles.tab}
|
||||
onClick={() => {
|
||||
setActive(tab.id);
|
||||
}}
|
||||
>
|
||||
{tab.title}
|
||||
</button>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{tabs.find(item => item.id === active).getComponent()}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 } from 'openinula';
|
||||
import {
|
||||
initBackgroundConnection,
|
||||
addBackgroundMessageListener,
|
||||
removeBackgroundMessageListener,
|
||||
postMessageToBackground,
|
||||
} from '../panelConnection';
|
||||
import { Table } from './Table';
|
||||
import { omit, sendMessage } from './utils';
|
||||
import styles from './PanelX.less';
|
||||
import { Highlight, RemoveHighlight } from '../utils/constants';
|
||||
import { ActionRunner } from './ActionRunner';
|
||||
import { Tree } from './Tree';
|
||||
|
||||
export default function Stores({ nextStoreId, showFilteredEvents }) {
|
||||
const [stores, setStores] = useState([]);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
if (!initialized) {
|
||||
setTimeout(() => {
|
||||
sendMessage({
|
||||
type: 'inulax getStores',
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
});
|
||||
}, 100);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const listener = message => {
|
||||
if (message.payload.type.startsWith('inulax')) {
|
||||
// 过滤 inula 消息
|
||||
if (message.payload.type === 'inulax stores') {
|
||||
// Stores 更新
|
||||
setStores(message.payload.stores);
|
||||
setInitialized(true);
|
||||
} else if (message.payload.type === 'inulax flush stores') {
|
||||
// Flush store
|
||||
sendMessage({
|
||||
type: 'inulax getStores',
|
||||
tabId: chrome.devtools.inspectedWindow.tabId,
|
||||
});
|
||||
} else if (message.payload.type === 'inulax observed components') {
|
||||
// observed components 更新
|
||||
setStores(
|
||||
stores.map(store => {
|
||||
store.observedComponents = message.payload.data[store.id] || [];
|
||||
return store;
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
initBackgroundConnection('panel');
|
||||
addBackgroundMessageListener(listener);
|
||||
return () => {
|
||||
removeBackgroundMessageListener(listener);
|
||||
};
|
||||
});
|
||||
}
|
|
@ -0,0 +1,164 @@
|
|||
/*
|
||||
* 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 } from 'openinula';
|
||||
import {Tree} from './Tree';
|
||||
import styles from './PanelX.less';
|
||||
|
||||
type displayKeysType = [string, string][];
|
||||
|
||||
export function Table({
|
||||
data,
|
||||
dataKey = 'id',
|
||||
displayKeys,
|
||||
activate,
|
||||
displayDataProcessor,
|
||||
search = '',
|
||||
}: {
|
||||
data;
|
||||
dataKey: string;
|
||||
displayKeys: displayKeysType;
|
||||
activate?: {
|
||||
[key: string]: any;
|
||||
};
|
||||
displayDataProcessor: (data: { [key: string]: any }) => {
|
||||
[key: string]: any;
|
||||
};
|
||||
search: string;
|
||||
}) {
|
||||
const [keyToDisplay, setKeyToDisplay] = useState(false);
|
||||
const [manualOverride, setManualOverride] = useState(false);
|
||||
let displayRow = null;
|
||||
|
||||
if (!manualOverride && activate) {
|
||||
data.forEach(item => {
|
||||
if (displayRow) {
|
||||
return;
|
||||
}
|
||||
let notMatch = false;
|
||||
Object.entries(activate).forEach(([key, value]) => {
|
||||
if (notMatch) {
|
||||
return;
|
||||
}
|
||||
if (item[key] !== value) {
|
||||
notMatch = true;
|
||||
}
|
||||
});
|
||||
if (notMatch) {
|
||||
return;
|
||||
}
|
||||
displayRow = item;
|
||||
});
|
||||
} else if (manualOverride && keyToDisplay) {
|
||||
data.forEach(item => {
|
||||
if (displayRow) {
|
||||
return;
|
||||
}
|
||||
if (item[dataKey] === keyToDisplay) {
|
||||
displayRow = item;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (displayRow) {
|
||||
const [attr, title] = displayKeys[0];
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={`${styles.table} ${styles.half}`}>
|
||||
<div className={styles.row}>
|
||||
<div className={`${styles.cell} ${styles.header}`}>{title}</div>
|
||||
</div>
|
||||
<div className={styles.scrollable}>
|
||||
<span></span>
|
||||
{data.map(row => (
|
||||
<div
|
||||
className={`${styles.row} ${
|
||||
keyToDisplay === row[dataKey] ? styles.active : ''
|
||||
}`}
|
||||
onClick={() => {
|
||||
setManualOverride(true);
|
||||
setKeyToDisplay(
|
||||
keyToDisplay === row[dataKey] ? null : row[dataKey]
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div className={styles.cell}>{row?.[attr] || ''}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`${styles.table} ${styles.half} ${styles.displayData}`}>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.cell}>
|
||||
<b>Data:</b>
|
||||
<button
|
||||
className={styles.floatingButton}
|
||||
onClick={() => {
|
||||
setKeyToDisplay(null);
|
||||
}}
|
||||
>
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.scrollable}>
|
||||
<span></span>
|
||||
<div className={styles.row}>
|
||||
<div className={styles.cell}>
|
||||
<Tree
|
||||
data={
|
||||
displayDataProcessor
|
||||
? displayDataProcessor(displayRow)
|
||||
: displayRow
|
||||
}
|
||||
indent={displayRow[displayKeys[0][0]]}
|
||||
expand={true}
|
||||
search={search}
|
||||
forcedExpand={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.table}>
|
||||
<div className={`${styles.row} ${styles.header}`}>
|
||||
{displayKeys.map(([key, title]) => (
|
||||
<div className={styles.cell}>{title}</div>
|
||||
))}
|
||||
</div>
|
||||
{data.map(item => (
|
||||
<div
|
||||
onClick={() => {
|
||||
setManualOverride(true);
|
||||
setKeyToDisplay(item[dataKey]);
|
||||
}}
|
||||
className={styles.row}
|
||||
>
|
||||
{displayKeys.map(([key, title]) => (
|
||||
<div className={styles.cell}>{item[key]}</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,230 @@
|
|||
/*
|
||||
* 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 } from 'openinula';
|
||||
import styles from './PanelX.less';
|
||||
import {Modal} from './Modal';
|
||||
import { displayValue, omit } from './utils';
|
||||
|
||||
export function Tree({
|
||||
data,
|
||||
indent = 0,
|
||||
index = '',
|
||||
expand = false,
|
||||
search = '',
|
||||
forcedExpand = false,
|
||||
onEdit = null,
|
||||
omitAttrs = [],
|
||||
className,
|
||||
forcedLabel = null,
|
||||
}: {
|
||||
data: any;
|
||||
indent?: number;
|
||||
index?: string | number;
|
||||
expand?: boolean;
|
||||
search?: string;
|
||||
forcedExpand?: boolean;
|
||||
className?: string | undefined
|
||||
omitAttrs?: string[];
|
||||
onEdit?: (path: any[], value: any) => void | null;
|
||||
forcedLabel?: string | number | null;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(expand);
|
||||
const [modal, setModal] = useState(false);
|
||||
const isArray = Array.isArray(data);
|
||||
const isObject = data && !isArray && typeof data === 'object';
|
||||
const isSet = isObject && data?._type === 'Set';
|
||||
const isWeakSet = isObject && data?._type === 'WeakSet';
|
||||
const isMap = isObject && data?._type === 'Map';
|
||||
const isWeakMap = isObject && data?._type === 'WeakMap';
|
||||
const isVNode = isObject && data.vtype;
|
||||
const canBeExpanded = isArray || (isObject && !isWeakSet && !isWeakMap);
|
||||
|
||||
if (isObject && omitAttrs?.length) {
|
||||
data = omit(data, ...omitAttrs);
|
||||
}
|
||||
|
||||
return canBeExpanded ? (
|
||||
<div
|
||||
style={{ fontFamily: 'monoSpace' }}
|
||||
className={`${expanded ? 'expanded' : 'not-expanded'} ${className}`}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setExpanded(!expanded);
|
||||
}}
|
||||
>
|
||||
{new Array(Math.max(indent, 0)).fill(<span> </span>)}
|
||||
{forcedExpand || isVNode ? null : expanded ? (
|
||||
<span>▼</span>
|
||||
) : (
|
||||
<span>▶</span>
|
||||
)}
|
||||
{index === 0 || index ? (
|
||||
<>
|
||||
<b className={styles.purple}>{displayValue(index, search)}: </b>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{forcedLabel
|
||||
? forcedLabel
|
||||
: expanded
|
||||
? isVNode
|
||||
? null
|
||||
: Array.isArray(data)
|
||||
? `Array(${data.length})`
|
||||
: isMap
|
||||
? `Map(${data.entries.length})`
|
||||
: isSet
|
||||
? `Set(${data.values.length})`
|
||||
: '{ ... }'
|
||||
: isWeakMap
|
||||
? 'WeakMap()'
|
||||
: isWeakSet
|
||||
? 'WeakSet()'
|
||||
: isMap
|
||||
? `Map(${data.entries.length})`
|
||||
: isSet
|
||||
? `Set(${data.values.length})`
|
||||
: Array.isArray(data)
|
||||
? `Array(${data.length})`
|
||||
: '{ ... }'}
|
||||
</span>
|
||||
{expanded || isVNode ? (
|
||||
isArray ? (
|
||||
<>
|
||||
{data.map((value, index) => {
|
||||
return (
|
||||
<div>
|
||||
<Tree
|
||||
data={value}
|
||||
indent={indent + 4}
|
||||
index={index}
|
||||
search={search}
|
||||
className={className}
|
||||
onEdit={
|
||||
onEdit
|
||||
? (path, val) => {
|
||||
onEdit(path.concat([index]), val);
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : isVNode ? (
|
||||
data
|
||||
) : isMap ? (
|
||||
<div>
|
||||
{data.entries.map(([key, value]) => {
|
||||
return (
|
||||
<Tree
|
||||
data={{key, value}}
|
||||
indent={indent + 4}
|
||||
search={search}
|
||||
className={className}
|
||||
// TODO: editable sets
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : isSet ? (
|
||||
data.values.map(item => {
|
||||
return (
|
||||
<div>
|
||||
<Tree
|
||||
data={item}
|
||||
indent={indent + 4}
|
||||
search={search}
|
||||
className={className}
|
||||
// TODO: editable sets
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
Object.entries(data).map(([key, value]) => {
|
||||
return (
|
||||
<div>
|
||||
<Tree
|
||||
data={value}
|
||||
indent={indent + 4}
|
||||
index={key}
|
||||
search={search}
|
||||
className={className}
|
||||
onEdit={
|
||||
onEdit
|
||||
? (path, val) => {
|
||||
onEdit(path.concat([key]), val);
|
||||
}
|
||||
: null
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={'not-expanded'}>
|
||||
{new Array(indent).fill(<span> </span>)}
|
||||
<span className={`${className}`}>
|
||||
{typeof index !== 'undefined' ? (
|
||||
<>
|
||||
<b className={styles.purple}>{displayValue(index, search)}: </b>
|
||||
</>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{displayValue(data, search)}
|
||||
{onEdit && !isWeakSet && !isWeakMap ? ( // TODO: editable weak set and map
|
||||
<>
|
||||
<b
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
setModal(true);
|
||||
}}
|
||||
>
|
||||
☼
|
||||
</b>
|
||||
{onEdit && modal ? (
|
||||
<Modal
|
||||
closeModal={() => {
|
||||
setModal(false);
|
||||
}}
|
||||
then={data => {
|
||||
onEdit([], data);
|
||||
setModal(false);
|
||||
}}
|
||||
>
|
||||
<h3>Edit value:</h3> {index}
|
||||
</Modal>
|
||||
) : null}
|
||||
</>
|
||||
) : null}
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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 PanelX from './PanelX';
|
||||
|
||||
export default PanelX;
|
|
@ -0,0 +1,49 @@
|
|||
<!--
|
||||
~ 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.
|
||||
-->
|
||||
|
||||
<!doctype html>
|
||||
<html style="display: flex">
|
||||
|
||||
<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%;
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
<script src="inula.development.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script src="panelX.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -0,0 +1,228 @@
|
|||
/*
|
||||
* 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 * as Inula from 'openinula';
|
||||
import styles from './PanelX.less';
|
||||
import { DevToolPanel } from '../utils/constants';
|
||||
|
||||
export function highlight(source, search) {
|
||||
if (!search || !source?.split) {
|
||||
return source;
|
||||
}
|
||||
|
||||
const parts = source.split(search);
|
||||
const result = [];
|
||||
|
||||
for (let i= 0; i < parts.length * 2 - 1; i++) {
|
||||
if (i % 2) {
|
||||
result.push(<span className={styles.highlighted}>{search}</span>);
|
||||
} else {
|
||||
result.push(parts[i / 2]);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function displayValue(val: any, search = '') {
|
||||
if (typeof val === 'boolean') {
|
||||
return (
|
||||
<span>
|
||||
{highlight(val ? 'true' : 'false', search)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (val === '') {
|
||||
return <span className={styles.red}>{'""'}</span>;
|
||||
}
|
||||
|
||||
if (typeof val === 'undefined') {
|
||||
return <span className={styles.grey}>{highlight('undefined', search)}</span>;
|
||||
}
|
||||
|
||||
if (val === 'null') {
|
||||
return <span className={styles.grey}>{highlight('null', search)}</span>;
|
||||
}
|
||||
|
||||
if (typeof val === 'string') {
|
||||
if (val.match(/^function\s?\(/)) {
|
||||
return (
|
||||
<span>
|
||||
<i>ƒ</i>
|
||||
{highlight(
|
||||
val.match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, ''),
|
||||
search
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return <span className={styles.red}>"{highlight(val, search)}"</span>;
|
||||
}
|
||||
if (typeof val === 'number') {
|
||||
return <span className={styles.blue}>{highlight('' + val, search)}</span>;
|
||||
}
|
||||
if (typeof val === 'function') {
|
||||
const args = val.toString().match(/^function\s?\([\w,]*\)/g)[0].replace(/^function\s?/, '');
|
||||
return (
|
||||
<span>
|
||||
<i>ƒ</i>
|
||||
{highlight(args, search)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (typeof val === 'object') {
|
||||
if (val?._type === 'WeakSet') {
|
||||
return <span>WeakSet()</span>;
|
||||
}
|
||||
|
||||
if (val?._type === 'WeakMap') {
|
||||
return <span>WeakMap()</span>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function fullTextSearch(value, search) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return value.some(val => fullTextSearch(val, search));
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
if (value?._type === 'Set') {
|
||||
return value.values.some(val => fullTextSearch(val, search));
|
||||
}
|
||||
if (value?._type === 'Map') {
|
||||
return value.entries.some(
|
||||
(key, val) => fullTextSearch(key, search) || fullTextSearch(val, search)
|
||||
);
|
||||
}
|
||||
return Object.values(value).some(val => fullTextSearch(val, search));
|
||||
}
|
||||
|
||||
return value.toString().includes(search);
|
||||
}
|
||||
|
||||
export function omit(obj, ...attrs) {
|
||||
const res = { ...obj };
|
||||
attrs.forEach(attr => delete res[attr]);
|
||||
return res;
|
||||
}
|
||||
|
||||
export function stringify(data) {
|
||||
if (typeof data === 'string' && data.startsWith('function(')) {
|
||||
return (
|
||||
<span>
|
||||
<i>ƒ</i>
|
||||
{data.match(/^function\([\w,]*\)/g)[0].substring(8)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (!data) {
|
||||
return displayValue(data);
|
||||
}
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
return `Array(${data.length})`;
|
||||
}
|
||||
|
||||
if (typeof data === 'object') {
|
||||
return `{${Object.entries(data).map(([key, value]) => {
|
||||
if (typeof value === 'string' && value.startsWith('function(')) {
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>
|
||||
<span>
|
||||
<i>ƒ</i>
|
||||
{value.match(/^function\([\w,]*\)/g)[0].substring(8)}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (!value) {
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>:{displayValue(value)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>:{' '}
|
||||
{`Array(${value.length})`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if (typeof value === 'object') {
|
||||
if ((value as any)?._type === 'WeakSet') {
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>: {'WeakSet()'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if ((value as any)?._type === 'WeakMap') {
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>: {'WeakMap'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if ((value as any)?._type === 'Set') {
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>:{' '}
|
||||
{`Set(${(value as Set<any>).size})`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
if ((value as any)?._type === 'Map') {
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>:{' '}
|
||||
{`Map(${(value as Map<any, any>).size})`}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
// object
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>: {'{...}'}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<span>
|
||||
<span className={styles.purple}>{key}</span>: {displayValue(value)}
|
||||
</span>
|
||||
);
|
||||
})}}`;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export function sendMessage(payload) {
|
||||
chrome.runtime.sendMessage({
|
||||
type: 'INULA_DEV_TOOLS',
|
||||
payload,
|
||||
from: DevToolPanel,
|
||||
});
|
||||
}
|
|
@ -0,0 +1,416 @@
|
|||
/*
|
||||
* 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 { Hook } from '../../../inula/src/renderer/hooks/HookType';
|
||||
import { ModifyHooks, ModifyProps, ModifyState } from '../utils/constants';
|
||||
import { VNode } from '../../../inula/src/renderer/vnode/VNode';
|
||||
import {
|
||||
ClassComponent,
|
||||
FunctionComponent,
|
||||
ContextConsumer,
|
||||
ContextProvider,
|
||||
ForwardRef,
|
||||
SuspenseComponent,
|
||||
MemoComponent,
|
||||
} from '../../../inula/src/renderer/vnode/VNodeTags';
|
||||
import { helper } from '../injector';
|
||||
import { JSXElement, ContextType } from '../../../inula/src/renderer/Types';
|
||||
import { decycle } from 'json-decycle';
|
||||
|
||||
// 展示值为 string 的可编辑模型
|
||||
type EditableStringType = 'string' | 'number' | 'undefined' | 'null';
|
||||
// 展示值为 string 的不可编辑类型
|
||||
type UnEditableStringType =
|
||||
| 'function'
|
||||
| 'symbol'
|
||||
| 'object'
|
||||
| 'map'
|
||||
| 'set'
|
||||
| 'array'
|
||||
| 'dom' // 值为 dom 元素的 ref 类型
|
||||
| 'ref'; // 值为其他数据的 ref 类型
|
||||
|
||||
type ShowAsStringType = EditableStringType | UnEditableStringType;
|
||||
|
||||
export type IAttr = {
|
||||
name: string | number;
|
||||
indentation: number;
|
||||
hIndex?: number; // 用于记录 hook 的 hIndex 值
|
||||
} & (
|
||||
| {
|
||||
type: ShowAsStringType;
|
||||
value: string;
|
||||
}
|
||||
| {
|
||||
type: 'boolean';
|
||||
value: boolean;
|
||||
}
|
||||
);
|
||||
|
||||
type ShowType = ShowAsStringType | 'boolean';
|
||||
|
||||
const propsAndStateTag = [ClassComponent];
|
||||
const propsAndHooksTag = [FunctionComponent, ForwardRef];
|
||||
const propsTag = [ContextConsumer, ContextProvider, SuspenseComponent, MemoComponent];
|
||||
const MAX_TITLE_LENGTH = 50;
|
||||
|
||||
function isJSXElement(obj: any): obj is JSXElement {
|
||||
return !!(obj?.type && obj.vtype);
|
||||
}
|
||||
|
||||
const isCycle = (obj: any): boolean => {
|
||||
return obj?.Consumer === obj;
|
||||
};
|
||||
|
||||
const getObjectKeys = (attr: Record<string, any>): Array<string | number | symbol> => {
|
||||
const keys: (string | symbol)[] = [];
|
||||
let current = attr;
|
||||
try {
|
||||
while (current != null) {
|
||||
const currentKeys = [
|
||||
...Object.keys(current),
|
||||
...Object.getOwnPropertySymbols(current)
|
||||
];
|
||||
const descriptors = Object.getOwnPropertyDescriptors(current);
|
||||
currentKeys.forEach(key => {
|
||||
// @ts-ignore key 可以为 symbol 类型
|
||||
if (descriptors[key].enumerable) {
|
||||
keys.push(key);
|
||||
}
|
||||
});
|
||||
current = Object.getPrototypeOf(current);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(attr);
|
||||
}
|
||||
return keys;
|
||||
};
|
||||
|
||||
// 用于比较两个 key 值的顺序
|
||||
export function sortKeys(
|
||||
firstKey: string | number | symbol,
|
||||
secondKey: string | number | symbol
|
||||
): number {
|
||||
if (firstKey.toString() > secondKey.toString()) {
|
||||
return 1;
|
||||
} else if (secondKey.toString() > firstKey.toString()) {
|
||||
return -1;
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
const parseSubTitle = <T>(attr: T) => {
|
||||
const AttrType = typeof attr;
|
||||
|
||||
if (Array.isArray(attr)) {
|
||||
let title = '';
|
||||
// 当 i > 0 时多加一个逗号和空格,例如:Person: { name: 'XXX', age: xxx }
|
||||
for (let i = 0; i < attr.length; i++) {
|
||||
if (i > 0) {
|
||||
title = `${title}, `;
|
||||
}
|
||||
title = `${title}${parseSubTitle(attr[i])}`;
|
||||
if (title.length > MAX_TITLE_LENGTH) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (title.length > MAX_TITLE_LENGTH) {
|
||||
title = `${title.substr(0, MAX_TITLE_LENGTH)}…`;
|
||||
}
|
||||
return `[${title}]`;
|
||||
} else if (AttrType === 'string') {
|
||||
return `"${attr}"`;
|
||||
} else if (AttrType === 'function') {
|
||||
const funcName = attr['name'];
|
||||
return `ƒ ${funcName}() {}`;
|
||||
} else if (
|
||||
AttrType === 'boolean' ||
|
||||
AttrType === 'number' ||
|
||||
AttrType === 'undefined'
|
||||
) {
|
||||
return `${attr}`;
|
||||
} else if (AttrType === 'object') {
|
||||
if (attr === null) {
|
||||
return 'null';
|
||||
}
|
||||
|
||||
if (isCycle(attr)) {
|
||||
attr = JSON.parse(JSON.stringify(attr, decycle()));
|
||||
}
|
||||
const keys = getObjectKeys(attr).sort(sortKeys);
|
||||
let title = '';
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
// 当 i > 0 时多加一个逗号和空格,例如:Person: { name: "xxx", age: xxx }
|
||||
if (i > 0) {
|
||||
title = `${title}, `;
|
||||
}
|
||||
title = `${title}${key.toString()}: ${parseSubTitle(attr[key])}`;
|
||||
if (title.length > MAX_TITLE_LENGTH) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (title.length > MAX_TITLE_LENGTH) {
|
||||
title = `${title.substr(0, MAX_TITLE_LENGTH)}…`;
|
||||
}
|
||||
return `{${title}}`;
|
||||
} else if (isJSXElement(attr)) {
|
||||
let title = '';
|
||||
if (typeof attr.type === 'string') {
|
||||
title = attr.type;
|
||||
} else {
|
||||
title = attr.type?.name ? attr.type.name : helper.getElementTag(attr);
|
||||
}
|
||||
return `${title} />`;
|
||||
}
|
||||
};
|
||||
|
||||
const parseSubAttr = (
|
||||
attr: any,
|
||||
parentIndentation: number,
|
||||
attrName: string,
|
||||
result: IAttr[],
|
||||
hIndex?: number
|
||||
) => {
|
||||
const AttrType = typeof attr;
|
||||
let value: any;
|
||||
let showType: any;
|
||||
let addSubState;
|
||||
|
||||
if (
|
||||
AttrType === 'boolean' ||
|
||||
AttrType === 'number' ||
|
||||
AttrType === 'undefined' ||
|
||||
AttrType === 'string'
|
||||
) {
|
||||
value = attr;
|
||||
showType = AttrType;
|
||||
} else if (AttrType === 'function') {
|
||||
const funcName = attr.name;
|
||||
value = `ƒ ${funcName}() {}`;
|
||||
} else if (AttrType === 'symbol') {
|
||||
value = attr.description;
|
||||
} else if (AttrType === 'object') {
|
||||
if (attr === null) {
|
||||
value = '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, result);
|
||||
});
|
||||
};
|
||||
} 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), result);
|
||||
});
|
||||
i++;
|
||||
};
|
||||
} else if (Array.isArray(attr)) {
|
||||
showType = 'array';
|
||||
value = parseSubTitle(attr);
|
||||
addSubState = () => {
|
||||
attr.forEach((attrValue, index) => {
|
||||
if (isJSXElement(attrValue)) {
|
||||
if (typeof attrValue.type === 'string') {
|
||||
value = attrValue.type + ' />';
|
||||
} else {
|
||||
value = attrValue.type?.name ? attrValue.type.name + ' />' : helper.getElementTag(attrValue) + ' />';
|
||||
}
|
||||
showType = 'string';
|
||||
const arrayItem: IAttr = {
|
||||
name: index,
|
||||
type: showType,
|
||||
value,
|
||||
indentation: parentIndentation + 2,
|
||||
};
|
||||
result.push(arrayItem);
|
||||
} else {
|
||||
parseSubAttr(attrValue, parentIndentation + 2, String(index), result);
|
||||
}
|
||||
});
|
||||
};
|
||||
} else if (attr instanceof Element) {
|
||||
showType = 'dom';
|
||||
value = '<' + attr.tagName.toLowerCase() + ' />';
|
||||
} else {
|
||||
if (isJSXElement(attr)) {
|
||||
if (typeof attr.type === 'string') {
|
||||
value = attr.type + ' />';
|
||||
} else {
|
||||
value = attr.type?.name ? attr.type.name + ' />' : helper.getElementTag(attr) + ' />';
|
||||
}
|
||||
showType = 'string';
|
||||
} else {
|
||||
showType = AttrType;
|
||||
value = Object.keys(attr).length === 0 ? '{}' : parseSubTitle(attr);
|
||||
addSubState = () => {
|
||||
// 判断是否为 Context 循环引用
|
||||
if (isCycle(attr)) {
|
||||
attr = JSON.parse(JSON.stringify(attr, decycle()));
|
||||
}
|
||||
Object.entries(attr).map(([key, val]) => {
|
||||
if (key === '_vNode') {
|
||||
val = JSON.parse(JSON.stringify(val, decycle()));
|
||||
}
|
||||
parseSubAttr(val, parentIndentation + 2, key, result);
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const item: IAttr = {
|
||||
name: attrName,
|
||||
type: showType,
|
||||
value,
|
||||
indentation: parentIndentation + 1,
|
||||
};
|
||||
if (hIndex !== undefined) {
|
||||
item.hIndex = hIndex;
|
||||
}
|
||||
result.push(item);
|
||||
if (addSubState) {
|
||||
addSubState();
|
||||
}
|
||||
};
|
||||
|
||||
// 将属性的值解析成固定格式, props 和类组件的 state 必须是一个对象
|
||||
export function parseAttr(rootAttr: any) {
|
||||
const result: IAttr[] = [];
|
||||
const indentation = 0;
|
||||
if (typeof rootAttr === 'object' && rootAttr !== null) {
|
||||
Object.keys(rootAttr).forEach(key => {
|
||||
parseSubAttr(rootAttr[key], indentation, key, result);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseHooks(
|
||||
hooks: Hook<any, any>[] | null,
|
||||
depContexts: Array<ContextType<any>> | null,
|
||||
getHookInfo
|
||||
) {
|
||||
const result: IAttr[] = [];
|
||||
const indentation = 0;
|
||||
if (depContexts !== null && depContexts?.length > 0) {
|
||||
depContexts.forEach(context => {
|
||||
parseSubAttr(context.value, indentation, 'Context', result);
|
||||
});
|
||||
}
|
||||
hooks?.forEach(hook => {
|
||||
const hookInfo = getHookInfo(hook);
|
||||
if (hookInfo) {
|
||||
const { name, hIndex, value } = hookInfo;
|
||||
parseSubAttr(value, indentation, name, result, hIndex);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
export function parseVNodeAttrs(vNode: VNode, getHookInfo) {
|
||||
const tag = vNode.tag;
|
||||
|
||||
if (propsAndStateTag.includes(tag)) {
|
||||
const { props, state, src } = vNode;
|
||||
const parsedProps = parseAttr(props);
|
||||
const parsedState = parseAttr(state);
|
||||
return {
|
||||
parsedProps,
|
||||
parsedState,
|
||||
src,
|
||||
};
|
||||
} else if (propsAndHooksTag.includes(tag)) {
|
||||
const { props, hooks, depContexts, src } = vNode;
|
||||
const parsedProps = parseAttr(props);
|
||||
const parsedHooks = parseHooks(hooks, depContexts, getHookInfo);
|
||||
return {
|
||||
parsedProps,
|
||||
parsedHooks,
|
||||
src,
|
||||
};
|
||||
} else if (propsTag.includes(tag)) {
|
||||
const { props, src } = vNode;
|
||||
const parsedProps = parseAttr(props);
|
||||
return {
|
||||
parsedProps,
|
||||
src,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 计算属性的访问顺序
|
||||
function calculateAttrAccessPath(item: IAttr, index: number, attrs: IAttr[], isHook: boolean) {
|
||||
let currentIndentation = item.indentation;
|
||||
const path: (string | number | undefined)[] = [item.name];
|
||||
let hookRootItem: IAttr = item;
|
||||
for (let i = index - 1; i >= 0; i--) {
|
||||
const lastItem = attrs[i];
|
||||
const lastIndentation = lastItem.indentation;
|
||||
if (lastIndentation < currentIndentation) {
|
||||
hookRootItem = lastItem;
|
||||
path.push(lastItem.name);
|
||||
currentIndentation = lastIndentation;
|
||||
}
|
||||
}
|
||||
path.reverse();
|
||||
if (isHook) {
|
||||
if (hookRootItem) {
|
||||
path[0] = hookRootItem.hIndex;
|
||||
} else {
|
||||
console.error('There is a bug, please report');
|
||||
}
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
export function buildAttrModifyData(
|
||||
parsedAttrsType: string,
|
||||
attrs: IAttr[],
|
||||
value,
|
||||
item: IAttr,
|
||||
index: number,
|
||||
id: number
|
||||
) {
|
||||
let type;
|
||||
if (parsedAttrsType === 'parsedProps') {
|
||||
type = ModifyProps;
|
||||
} else if (parsedAttrsType === 'parsedState') {
|
||||
type = ModifyState;
|
||||
} else if (parsedAttrsType === 'parsedHooks') {
|
||||
type = ModifyHooks;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
const path = calculateAttrAccessPath(item, index, attrs, parsedAttrsType === 'parsedHooks');
|
||||
|
||||
return {
|
||||
id: id,
|
||||
type: type,
|
||||
value: value,
|
||||
path: path,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
* 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 { VNode } from '../../../inula/src/renderer/vnode/VNode';
|
||||
import {
|
||||
ClassComponent,
|
||||
ContextConsumer,
|
||||
ContextProvider,
|
||||
ForwardRef,
|
||||
FunctionComponent,
|
||||
MemoComponent,
|
||||
SuspenseComponent
|
||||
} from '../../../inula/src/renderer/vnode/VNodeTags';
|
||||
|
||||
export type NameObj = {
|
||||
itemName: string;
|
||||
badge: Array<string>;
|
||||
};
|
||||
|
||||
// 建立双向映射关系,当用户在修改属性值后,可以找到对应的 VNode
|
||||
export let VNodeToIdMap: Map<VNode, number>;
|
||||
export let IdToVNodeMap: Map<number, VNode>;
|
||||
|
||||
if (!VNodeToIdMap) {
|
||||
VNodeToIdMap = new Map<VNode, number>();
|
||||
}
|
||||
|
||||
if (!IdToVNodeMap) {
|
||||
IdToVNodeMap = new Map<number, VNode>();
|
||||
}
|
||||
|
||||
let uid = 0;
|
||||
function generateUid(vNode: VNode) {
|
||||
const id = VNodeToIdMap.get(vNode);
|
||||
if (id !== undefined) {
|
||||
return id;
|
||||
}
|
||||
uid++;
|
||||
return uid;
|
||||
}
|
||||
|
||||
const componentType = [
|
||||
ClassComponent,
|
||||
FunctionComponent,
|
||||
ContextProvider,
|
||||
ContextConsumer,
|
||||
ForwardRef,
|
||||
SuspenseComponent,
|
||||
MemoComponent,
|
||||
];
|
||||
|
||||
const badgeNameArr: Array<string> = [
|
||||
'withRouter(',
|
||||
'SideEffect(',
|
||||
'Connect(',
|
||||
'injectIntl(',
|
||||
'Pure(',
|
||||
];
|
||||
|
||||
export function isUserComponent(tag: string) {
|
||||
return componentType.includes(tag);
|
||||
}
|
||||
|
||||
function getParentUserComponent(node: VNode) {
|
||||
let parent = node.parent;
|
||||
while (parent) {
|
||||
if (isUserComponent(parent.tag)) {
|
||||
break;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
return parent;
|
||||
}
|
||||
|
||||
function getContextName(node: VNode, type: string) {
|
||||
const contextType = type;
|
||||
if (!node.type.displayName) {
|
||||
if (node.type.value) {
|
||||
if (typeof node.type.value === 'object') {
|
||||
return `Context.${contextType}`;
|
||||
} else {
|
||||
return `${node.type.value}.${contextType}`;
|
||||
}
|
||||
} else {
|
||||
if (node.type._context?.displayName) {
|
||||
return `${node.type._context.displayName}.${contextType}`;
|
||||
}
|
||||
return `Context.${contextType}`;
|
||||
}
|
||||
}
|
||||
return `${node.type.displayName}.${contextType}`;
|
||||
}
|
||||
|
||||
const getForwardRefName = (node: VNode): NameObj => {
|
||||
const forwardRefName: NameObj = {
|
||||
itemName: '',
|
||||
badge: ['ForwardRef'],
|
||||
};
|
||||
if (!node.type.render?.name) {
|
||||
if (node.type.render?.name !== '') {
|
||||
forwardRefName.itemName = node.type?.displayName ? node.type?.displayName : 'Anonymous';
|
||||
} else {
|
||||
forwardRefName.itemName = 'Anonymous';
|
||||
}
|
||||
} else {
|
||||
forwardRefName.itemName = node.type.render?.name;
|
||||
}
|
||||
return forwardRefName;
|
||||
};
|
||||
|
||||
// 用于结构组件名,例如: Pure(Memo(xxx)) => xxx 并且把 Pure Memo 加到 NameObj.badge 里
|
||||
const parseComponentName = (name: NameObj): NameObj => {
|
||||
badgeNameArr.forEach(badgeName => {
|
||||
if (name.itemName.startsWith(badgeName)) {
|
||||
// 截断开头的高阶组件名,并把最后一个 ) 替换为 ''。例如: Pure(Memo(xxx)) => Memo(xxx)) => Memo(xxx)
|
||||
name.itemName = name.itemName.substring(badgeName.length).replace(/(\))(?!.*\1)/, '');
|
||||
name.badge.push(badgeName.substring(0, badgeName.length - 1));
|
||||
}
|
||||
});
|
||||
return name;
|
||||
};
|
||||
|
||||
// 取字符串括号里的值
|
||||
const getValuesInParentheses = (name: string) => {
|
||||
let result = name;
|
||||
const regex = /\((.+?)\)/g;
|
||||
const results = name.match(regex);
|
||||
if (results) {
|
||||
const option = results[0];
|
||||
if (option) {
|
||||
result = option.substring(1, option.length - 1);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
function isNullOrUndefined(prop) {
|
||||
return !prop || typeof prop === 'undefined' || prop === 0;
|
||||
}
|
||||
|
||||
function parseTreeRoot(travelVNodeTree, treeRoot: VNode) {
|
||||
const result: any[] = [];
|
||||
travelVNodeTree(treeRoot, (node: VNode) => {
|
||||
const tag = node.tag;
|
||||
|
||||
if (isUserComponent(tag)) {
|
||||
// 添加 ID
|
||||
const id = generateUid(node);
|
||||
result.push(id);
|
||||
let nameObj: NameObj = {
|
||||
itemName: '',
|
||||
badge: [],
|
||||
};
|
||||
// 拿到不同类型的展示名字
|
||||
if (tag === ContextProvider) {
|
||||
nameObj.itemName = getContextName(node, 'Provider');
|
||||
result.push(nameObj);
|
||||
} else if (tag === ContextConsumer) {
|
||||
nameObj.itemName = getContextName(node, 'Consumer');
|
||||
result.push(nameObj);
|
||||
} else if (tag === ForwardRef) {
|
||||
const name = getForwardRefName(node);
|
||||
result.push(name);
|
||||
} else if (tag === SuspenseComponent) {
|
||||
nameObj.itemName = 'Suspense';
|
||||
result.push(nameObj);
|
||||
} else if (tag === MemoComponent) {
|
||||
const name = node.type?.displayName || node.type?.name || node.type.render?.name;
|
||||
nameObj.itemName = !isNullOrUndefined(name) ? name : 'Anonymous';
|
||||
nameObj.badge.push('Memo');
|
||||
nameObj = parseComponentName(nameObj);
|
||||
result.push(nameObj);
|
||||
} else {
|
||||
const name = node.type.displayName || node.type?.name;
|
||||
nameObj.itemName = !isNullOrUndefined(name) ? name : 'Anonymous';
|
||||
nameObj = parseComponentName(nameObj);
|
||||
result.push(nameObj);
|
||||
}
|
||||
|
||||
// 添加父节点 ID
|
||||
const parent = getParentUserComponent(node);
|
||||
if (parent) {
|
||||
const parentId = VNodeToIdMap.get(parent);
|
||||
result.push(parentId);
|
||||
} else {
|
||||
result.push('');
|
||||
}
|
||||
|
||||
// 添加节点 key 值
|
||||
const key = node.key;
|
||||
if (key !== null) {
|
||||
result.push(key);
|
||||
} else {
|
||||
result.push('');
|
||||
}
|
||||
|
||||
VNodeToIdMap.set(node, id);
|
||||
IdToVNodeMap.set(id, node);
|
||||
}
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function queryVNode(id: number): VNode | undefined {
|
||||
return IdToVNodeMap.get(id);
|
||||
}
|
||||
|
||||
export function clearVNode(vNode: VNode) {
|
||||
if (VNodeToIdMap.has(vNode)) {
|
||||
const id = VNodeToIdMap.get(vNode);
|
||||
VNodeToIdMap.delete(vNode);
|
||||
IdToVNodeMap.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
export default parseTreeRoot;
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
interface IArrow {
|
||||
direction: 'up' | 'down'
|
||||
}
|
||||
|
||||
export default function Arrow({ direction: director }: IArrow) {
|
||||
let d = '';
|
||||
if (director === 'up') {
|
||||
d = 'M4 9.5 L5 10.5 L8 7.5 L11 10.5 L12 9.5 L8 5.5 z';
|
||||
} else if (director === 'down') {
|
||||
d = 'M5 5.5 L4 6.5 L8 10.5 L12 6.5 L11 5.5 L8 8.5z';
|
||||
}
|
||||
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||
<path d={d} fill='currentColor' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 default function Close() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='1rem' height='1rem'>
|
||||
<path d='M4 3 L3 4 L7 8 L3 12 L4 13 L8 9 L12 13 L13 12 L9 8 L13 4 L12 3 L8 7z' fill='currentColor' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* 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 default function Debug() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='1.3rem' height='1.3rem' margin-top='0.4rem'>
|
||||
<path d='M0 0h24v24H0z' fill='none'/>
|
||||
<path d='M20 8h-2.81c-.45-.78-1.07-1.45-1.82-1.96L17 4 41 15.59 3l-2.17 2.17c12 96 5.06 12.49 5 12 5c-.49 0-.96.06-1.41.17L8.41
|
||||
3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34 04.67 09 1H4v2h2.81c1.04
|
||||
1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8zm-6
|
||||
8h-4v-2h4v2zm0-4h-4v-2h4v2z'
|
||||
fill='currentColor' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 default function Discover() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='1.3rem' height='1.3rem'>
|
||||
<path d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M15.5 14h-.79l-.28-.27C15 41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91
|
||||
16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99
|
||||
5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z'
|
||||
fill='#343231' p-id='4151' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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 default function Eye() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='1.3rem' height='1.3rem'>
|
||||
<path d='M0 0h24v24H0z' fill='none'/>
|
||||
<path d='M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12
|
||||
17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3
|
||||
3-1.34 3-3-1.34-3-3-3z'
|
||||
fill='currentColor' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 default function Location() {
|
||||
return (
|
||||
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='1.3rem' height='1.3rem'>
|
||||
<path d='M0 0h24v24H0z' fill='none' />
|
||||
<path d='M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z'
|
||||
fill='currentColor' />
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 default function Operation() {
|
||||
return (
|
||||
<svg t="1669105013009" className="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2821" width="1rem" height="1rem">
|
||||
<path d="M177.23392 943.96416c25.93792 0 50.3296-10.13248 68.70528-28.50816l208.18432-208.18944 29.36832-29.36832 38.90688 14.51008a323.70688 323.70688 0 0 0 112.85504 20.18304c86.48704 0 167.90528-33.664 229.05344-94.90432 83.4816-83.39456 113.4592-203.37152 83.99872-312.96512L795.60704 457.4208l-45.77792 45.77792-45.85984-45.77792-137.41568-137.41056-45.77792-45.87008 45.77792-45.7728 152.61696-152.61696a327.44448 327.44448 0 0 0-83.90656-10.99264c-86.48704 0-167.81824 33.66912-228.9664 94.90944C316.96896 248.89856 287.68256 383.13472 331.5712 501.57056l14.42816 38.90688-29.37344 29.36832-208.09728 208.19456c-18.37568 18.37568-28.51328 42.76736-28.51328 68.70528 0 25.93792 10.13248 50.3296 28.51328 68.7104a96.41472 96.41472 0 0 0 68.70528 28.50816m0 64.76288a161.67936 161.67936 0 0 1-114.57024-47.41632c-63.21152-63.29856-63.21152-165.84704 0-229.05344L270.848 524.07296c-51.18976-138.01984-21.30432-299.31008 89.57952-410.27072C436.352 37.9648 535.808 0 635.25888 0c68.4544 0 136.81664 17.95072 197.53472 53.77024l-220.38016 220.37504 137.41568 137.41568 220.38528-220.38016c88.02816 149.0944 68.0192 344.30976-60.12416 472.3712-75.83744 75.83232-175.37536 113.79712-274.83136 113.79712a389.6064 389.6064 0 0 1-135.35232-24.2176l-208.18944 208.18432c-31.60576 31.60064-73.088 47.4112-114.4832 47.4112z" p-id="2822"/>
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* 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 default function Select() {
|
||||
return (
|
||||
<svg width='1rem' height='1rem' viewBox='0 0 16 16' xmlns='http://www.w3.org/2000/svg'>
|
||||
<g fill='none' fill-rule='evenodd'>
|
||||
<g stroke='currentColor'>
|
||||
<path
|
||||
stroke-width='.291'
|
||||
fill='currentColor'
|
||||
fill-rule='nonzero'
|
||||
stroke-linecap='round'
|
||||
stroke-linejoin='round'
|
||||
d='M6 6l3.014 9 2.508-3.533L15 8.791z'
|
||||
></path>
|
||||
<path stroke-width='2' d='M10.417 10.417l2.87 2.87L15 15'></path>
|
||||
</g>
|
||||
<path
|
||||
d='M12.188 0A2.812 2.812 0 0 1 15 2.813V5h-1V2.857A1.857 1.857 0 0 0 12.143 1H2.857A1.857 1.857 0 0 0 1 2.857v9.286C1 13.169 1.831 14 2.857 14H5v1H2.812A2.812 2.812 0 0 1 0 12.187V2.813A2.812 2.812 0 0 1 2.813 0h9.374z'
|
||||
fill='currentColor'
|
||||
fill-rule='nonzero'
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
||||
interface IArrow {
|
||||
director: 'right' | 'down'
|
||||
}
|
||||
|
||||
export default function Triangle({ director }: IArrow) {
|
||||
let d = '';
|
||||
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,20 @@
|
|||
<!--
|
||||
- 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.
|
||||
-->
|
||||
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem">
|
||||
<path d="M720 192h-544A80.096 80.096 0 0 0 96 272v608C96 924.128 131.904 960 176 960h544c44.128 0 80-35.872 80-80v-608C800 227.904 764.128 192 720 192z m16 688c0 8.8-7.2 16-16 16h-544a16 16 0 0 1-16-16v-608a16 16 0 0 1 16-16h544a16 16 0 0 1 16 16v608z" fill="#ffffff"></path>
|
||||
<path d="M848 64h-544a32 32 0 0 0 0 64h544a16 16 0 0 1 16 16v608a32 32 0 1 0 64 0v-608C928 99.904 892.128 64 848 64z" fill="#ffffff"></path>
|
||||
<path d="M608 360H288a32 32 0 0 0 0 64h320a32 32 0 1 0 0-64zM608 520H288a32 32 0 1 0 0 64h320a32 32 0 1 0 0-64zM480 678.656H288a32 32 0 1 0 0 64h192a32 32 0 1 0 0-64z" fill="#ffffff"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 1.3 KiB |
|
@ -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.
|
||||
-->
|
||||
|
||||
<svg class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="1rem" height="1rem">
|
||||
<path d="M224 217h-64v588h64V217z m640 0v588h-64V217h64z" fill="#ffffff"></path>
|
||||
<path d="M732.649 278.902C679.222 303.256 600.918 320 511 320s-168.222-16.744-221.649-41.098C230.212 251.944 224 227.75 224 224s6.212-27.944 65.351-54.902C342.778 144.744 421.082 128 511 128s168.222 16.744 221.649 41.098C791.788 196.056 798 220.25 798 224s-6.212 27.944-65.351 54.902zM511 384c193.852 0 351-71.634 351-160S704.852 64 511 64s-351 71.634-351 160 157.148 160 351 160zM162 416c0 88.366 157.148 160 351 160s351-71.634 351-160h-64c0 3.75-6.212 27.944-65.351 54.902C681.222 495.256 602.918 512 513 512s-168.222-16.744-221.649-41.098C232.212 443.944 226 419.75 226 416h-64zM162 608c0 88.366 157.148 160 351 160s351-71.634 351-160h-64c0 3.75-6.212 27.944-65.351 54.902C681.222 687.256 602.918 704 513 704s-168.222-16.744-221.649-41.098C232.212 635.944 226 611.75 226 608h-64zM161 803c0 88.366 157.148 160 351 160s351-71.634 351-160h-64c0 3.75-6.212 27.944-65.351 54.902C680.222 882.256 601.918 899 512 899s-168.222-16.744-221.649-41.098C231.212 830.944 225 806.75 225 803h-64z" fill="#ffffff"></path>
|
||||
</svg>
|
After Width: | Height: | Size: 1.7 KiB |
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 function Checkbox({ value }) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
border: '1px solid black',
|
||||
borderRadius: '2px',
|
||||
width: '0.75rem',
|
||||
height: '0.75rem',
|
||||
padding: '1px',
|
||||
backgroundColor: 'white',
|
||||
display: 'inline-block',
|
||||
position: 'relative',
|
||||
cursor: 'pointer',
|
||||
verticalAlign: 'sub',
|
||||
marginBottom: '0.1rem'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundColor: value? 'black' : 'white',
|
||||
position: 'relative'
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 { createContext } from "openinula";
|
||||
|
||||
const PickElementContext = createContext(null);
|
||||
PickElementContext.displayName = 'PickElementContext';
|
||||
|
||||
export default PickElementContext;
|
|
@ -0,0 +1,21 @@
|
|||
/*
|
||||
* 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 {createContext} from 'openinula';
|
||||
|
||||
const ViewSourceContext = createContext(null);
|
||||
ViewSourceContext.displayName = 'ViewSourceContext';
|
||||
|
||||
export default ViewSourceContext;
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// panel 页面打开后初始化连接标志
|
||||
export const InitDevToolPageConnection = 'init dev tool page connection';
|
||||
// background 解析全部 root VNodes 标志
|
||||
export const RequestAllVNodeTreeInfos = 'request all vNodes tree infos';
|
||||
// vNodes 全部树解析结果标志
|
||||
export const AllVNodeTreeInfos = 'vNode trees infos';
|
||||
// 一棵树的解析
|
||||
export const OneVNodeTreeInfos = 'one vNode tree';
|
||||
// 获取组件属性
|
||||
export const RequestComponentAttrs = 'get component attrs';
|
||||
// 返回组件属性
|
||||
export const ComponentAttrs = 'component attrs';
|
||||
|
||||
export const ModifyAttrs = 'modify attrs';
|
||||
|
||||
export const ModifyProps = 'modify props';
|
||||
|
||||
export const ModifyState = 'modify state';
|
||||
|
||||
export const ModifyHooks = 'modify hooks';
|
||||
|
||||
export const InspectDom = 'inspect component dom';
|
||||
|
||||
export const LogComponentData = 'log component data';
|
||||
|
||||
export const CopyComponentAttr = 'copy component attr';
|
||||
// 传递消息来源标志
|
||||
export const DevToolPanel = 'dev tool panel';
|
||||
|
||||
export const DevToolBackground = 'dev tool background';
|
||||
|
||||
export const DevToolContentScript = 'dev tool content script';
|
||||
|
||||
export const DevToolHook = 'dev tool hook';
|
||||
|
||||
export const GetStores = 'get stores';
|
||||
|
||||
// 高亮显示与消除
|
||||
export const Highlight = 'highlight';
|
||||
export const RemoveHighlight = 'remove highlight';
|
||||
|
||||
// 跳转元素代码位置
|
||||
export const ViewSource = 'view source';
|
||||
|
||||
// 选择页面元素
|
||||
export const PickElement = 'pick element';
|
||||
export const StopPickElement = 'stop pick element';
|
||||
|
||||
// 复制和存为全局变量
|
||||
export const CopyToConsole = 'copy to console';
|
||||
export const StorageValue = 'storage value';
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
function ifNullThrows(value) {
|
||||
if (value === null) {
|
||||
throw new Error('receive a null');
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
export function injectSrc(src) {
|
||||
const script = document.createElement('script');
|
||||
script.src = src;
|
||||
script.type = 'text/javascript';
|
||||
script.async = false;
|
||||
script.onload = function () {
|
||||
// 加载完毕后需要移除
|
||||
script.remove();
|
||||
};
|
||||
|
||||
ifNullThrows(
|
||||
document.head
|
||||
|| document.getElementsByName('head')[0]
|
||||
|| document.documentElement
|
||||
).appendChild(script);
|
||||
}
|
||||
|
||||
export function injectCode(code) {
|
||||
const script = document.createElement('script');
|
||||
script.textContent = code;
|
||||
|
||||
ifNullThrows(document.documentElement).appendChild(script);
|
||||
ifNullThrows(script.parentNode).removeChild(script);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/**
|
||||
* chrome 通过 iframe 的方式将 panel 页面嵌入到开发者工具中,如果报错无法感知
|
||||
* 同时也无法在运行时打断点,需要适当的日志辅助开发和定位问题
|
||||
*/
|
||||
interface LoggerType {
|
||||
error: typeof console.error,
|
||||
info: typeof console.info,
|
||||
log: typeof console.log,
|
||||
warn: typeof console.warn
|
||||
}
|
||||
|
||||
export function createLogger(id: string): LoggerType {
|
||||
return ['error', 'info', 'log', 'warn'].reduce((pre, current) => {
|
||||
const prefix = `[inula_dev_tools][${id}] `;
|
||||
pre[current] = (...data) => {
|
||||
console[current](prefix, ...data);
|
||||
};
|
||||
return pre;
|
||||
}, {} as LoggerType);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue