```
### customPlugin
自定义插件配置。允许在插件初始化之前修改插件的配置。
```typescript
customPlugin>(
pluginName: string,
customPluginHook: CustomPluginHook
): void
```
**参数:**
- `pluginName`: 插件名称
- `customPluginHook`: 自定义钩子函数,接收插件实例并返回修改后的插件实例
**示例:**
```tsx
const beforePluginRun = ({ pluginManager }: { pluginManager: PluginManager }) => {
// 自定义设计器插件配置
pluginManager.customPlugin('Designer', (pluginInstance) => {
// 自定义渲染器
pluginInstance.ctx.config.customRender = customRender;
// 自定义 beforeInitRender
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
if (!subWin) return;
// 注入全局变量
(subWin as any).React = window.React;
(subWin as any).ReactDOM = window.ReactDOM;
// 注入自定义全局对象
(subWin as any).myCustomGlobal = {
config: {},
utils: {},
};
};
return pluginInstance;
});
};
;
```
### onPluginReadyOk
等待插件准备完成
```typescript
onPluginReadyOk(
pluginName: string,
cb?: (pluginHandle: PluginInstance) => void
): Promise
```
## 访问 Engine 实例
Engine 实例会被挂载到全局对象上,可以通过以下方式访问:
```typescript
// 方式1: 通过 window.__CHAMELEON_ENG__ (构造函数中挂载)
const engine = (window as any).__CHAMELEON_ENG__;
// 方式2: 通过 window.__C_ENGINE__ (componentDidMount 中挂载)
const engine = (window as any).__C_ENGINE__;
// 方式3: 通过 onReady 回调(推荐)
const onReady = (ctx: EnginContext) => {
const engine = ctx.engine;
};
// 方式4: 通过 ref(如果使用函数组件)
const engineRef = useRef(null);
;
```
:::tip
推荐使用 `onReady` 回调来访问 Engine 实例,这样可以确保所有插件都已加载完成。
:::
## 类型定义
### CPageDataType
页面协议数据类型,定义了页面的完整结构。
```typescript
type CPageDataType = {
version: string;
name: string;
css?: CSSType;
componentsMeta?: ComponentMetaType[];
thirdLibs?: LibMetaType[];
componentsTree: CRootNodeDataType;
assets?: AssetPackage[];
};
```
### CMaterialType
组件物料类型,定义了组件的描述信息。
```typescript
type CMaterialType = {
componentName: string;
title: string;
icon?: React.ReactNode;
props?: CMaterialPropType[];
// ... 更多字段
};
```
### AssetPackage
资源包类型,定义了组件库的资源信息。
```typescript
type AssetPackage = {
package: string;
version: string;
urls: string[];
library?: string;
};
```
更多类型定义请参考 `@chamn/model` 包的文档。
## 完整示例
以下是一个完整的示例,展示了 Engine API 的综合使用:
```tsx
import { Engine, plugins, EnginContext } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
import { Button, message } from 'antd';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const [pageSchema, setPageSchema] = useState(initialPageSchema);
const onMount = (ctx: EnginContext) => {
console.log('Engine mounted');
};
const onReady = async (ctx: EnginContext) => {
console.log('Engine ready!');
const { engine, pluginManager } = ctx;
// 1. 获取工作台实例并自定义界面
const workbench = engine.getWorkbench();
workbench?.replaceTopBarView(
我的编辑器
engine.preview()}>预览
engine.existPreview()}>退出预览
保存
);
// 2. 监听节点选中变化
engine.emitter.on('onSelectNodeChange', ({ node }) => {
if (node) {
console.log('选中节点:', node);
message.info(`选中了 ${node.componentName} 组件`);
}
});
// 3. 监听物料更新
engine.emitter.on('updateMaterials', () => {
message.success('物料已更新');
});
// 4. 获取插件实例
const designerPlugin = await pluginManager.get('Designer');
const historyPlugin = await pluginManager.get('History');
// 5. 添加自定义左侧面板
workbench?.addLeftPanel({
name: 'CustomPanel',
title: '自定义面板',
icon: ,
view: ,
});
};
const handleSave = () => {
const engine = (window as any).__C_ENGINE__;
if (engine) {
const pageData = engine.pageModel.export();
localStorage.setItem('pageSchema', JSON.stringify(pageData));
message.success('保存成功');
}
};
const handleUpdateMaterials = async () => {
const engine = (window as any).__C_ENGINE__;
if (engine) {
const [materials, assetPackages] = await fetchMaterials();
await engine.updateMaterials(materials, assetPackages);
}
};
return (
);
}
```
## 常见问题
### Q: 如何确保在访问 Engine 实例时所有插件都已加载完成?
A: 使用 `onReady` 回调,它会在所有插件加载完成后调用:
```tsx
const onReady = async (ctx: EnginContext) => {
// 此时所有插件都已加载完成
const designerPlugin = await ctx.pluginManager.get('Designer');
};
```
### Q: 如何动态更新页面数据?
A: 使用 `updatePage` 方法:
```tsx
ctx.engine.updatePage(newPageSchema);
```
### Q: 如何监听页面节点的变化?
A: 可以通过页面模型的 `emitter` 监听:
```tsx
ctx.engine.pageModel.emitter.on('onReloadPage', () => {
console.log('页面已重新加载');
});
```
### Q: 如何自定义渲染器?
A: 在 `beforePluginRun` 中自定义设计器插件的渲染配置:
```tsx
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.customRender = customRender;
return pluginInstance;
});
};
```
## beforeInitRender 详解
`beforeInitRender` 是设计器插件配置中的一个重要钩子函数,它在渲染器初始化**之前**被调用,用于准备 iframe 环境。
### 函数签名
```typescript
type BeforeInitRender = (params: { iframe: IframeContainer }) => Promise | void;
```
### 执行时机
```
1. 创建 iframe
2. 加载 iframe URL/HTML
3. ⭐ 调用 beforeInitRender(在这里初始化环境)
4. 注入渲染器 JS
5. 加载组件库资源
6. 渲染页面
```
### 默认实现
Engine 默认的 `beforeInitRender` 实现会将 React 相关库注入到 iframe 窗口中:
```typescript
const beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
if (!subWin) return;
// 注入 React 18 相关库
(subWin as any).React = window.React;
(subWin as any).ReactDOM = window.ReactDOM;
(subWin as any).ReactDOMClient = window.ReactDOMClient;
};
```
### 使用场景
#### 1. 注入全局变量
向 iframe 中注入自定义的全局变量或对象:
```tsx
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
if (!subWin) return;
// 注入配置对象
(subWin as any).APP_CONFIG = {
apiUrl: 'https://api.example.com',
theme: 'light',
};
// 注入工具函数
(subWin as any).utils = {
formatDate: (date) => {
/* ... */
},
request: (url) => {
/* ... */
},
};
};
return pluginInstance;
});
};
```
#### 2. 配置跨窗口通信
设置主窗口和 iframe 之间的通信机制:
```tsx
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
if (!subWin) return;
// 设置消息监听
subWin.addEventListener('message', (event) => {
if (event.data.type === 'customEvent') {
console.log('收到来自组件的消息:', event.data);
}
});
// 注入发送消息的方法
(subWin as any).sendToParent = (data) => {
window.postMessage(data, '*');
};
};
return pluginInstance;
});
};
```
#### 3. 注入第三方库
在渲染前注入必要的第三方库:
```tsx
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
if (!subWin) return;
// 注入 lodash
(subWin as any)._ = window._;
// 注入 moment
(subWin as any).moment = window.moment;
// 注入 axios
(subWin as any).axios = window.axios;
};
return pluginInstance;
});
};
```
#### 4. 初始化样式或主题
预先设置 iframe 的样式或主题:
```tsx
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
const subDoc = iframe.getDocument();
if (!subWin || !subDoc) return;
// 注入主题变量
(subWin as any).THEME = {
primaryColor: '#1890ff',
fontSize: '14px',
};
// 添加全局样式
const style = subDoc.createElement('style');
style.textContent = `
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
}
`;
subDoc.head.appendChild(style);
};
return pluginInstance;
});
};
```
#### 5. 设置调试工具
在开发环境中注入调试工具:
```tsx
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
if (!subWin) return;
if (process.env.NODE_ENV === 'development') {
// 注入调试工具
(subWin as any).__DEBUG__ = {
logProps: (componentName) => {
console.log(`组件 ${componentName} 的 props:` /* ... */);
},
inspect: (node) => {
console.log('节点信息:', node);
},
};
// 启用详细日志
(subWin as any).__ENABLE_VERBOSE_LOGGING__ = true;
}
};
return pluginInstance;
});
};
```
#### 6. 模拟数据或 API
为组件提供模拟数据或 API:
```tsx
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
if (!subWin) return;
// 注入模拟 API
(subWin as any).mockAPI = {
getUserInfo: async () => ({
id: 1,
name: 'Test User',
email: 'test@example.com',
}),
getProducts: async () => [
{ id: 1, name: 'Product 1', price: 100 },
{ id: 2, name: 'Product 2', price: 200 },
],
};
};
return pluginInstance;
});
};
```
### 完整示例
结合多个场景的完整示例:
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
const subDoc = iframe.getDocument();
if (!subWin || !subDoc) return;
// 1. 注入 React(必需)
(subWin as any).React = window.React;
(subWin as any).ReactDOM = window.ReactDOM;
(subWin as any).ReactDOMClient = window.ReactDOMClient;
// 2. 注入应用配置
(subWin as any).APP_CONFIG = {
apiUrl: process.env.REACT_APP_API_URL,
environment: process.env.NODE_ENV,
version: '1.0.0',
};
// 3. 注入第三方库
(subWin as any).axios = window.axios;
(subWin as any).dayjs = window.dayjs;
// 4. 设置通信
(subWin as any).sendToEditor = (data) => {
window.postMessage(
{
source: 'iframe',
...data,
},
'*'
);
};
// 5. 添加全局样式
const style = subDoc.createElement('style');
style.textContent = `
:root {
--primary-color: #1890ff;
--text-color: #333;
}
`;
subDoc.head.appendChild(style);
// 6. 开发环境调试工具
if (process.env.NODE_ENV === 'development') {
(subWin as any).__DEBUG__ = {
log: (...args) => console.log('[iframe]', ...args),
error: (...args) => console.error('[iframe]', ...args),
};
}
};
return pluginInstance;
});
};
return (
);
}
```
### 注意事项
1. **必须注入 React**:如果使用默认渲染器,必须确保注入 React、ReactDOM 和 ReactDOMClient
2. **异步操作**:`beforeInitRender` 支持异步操作,可以使用 `async/await`
3. **错误处理**:建议添加错误处理,确保 iframe 初始化不会失败
4. **性能考虑**:避免在 `beforeInitRender` 中执行耗时操作
5. **安全性**:注意不要注入敏感信息到 iframe 中
### 与 customRender 的区别
| 特性 | beforeInitRender | customRender |
| :----------- | :--------------------- | :----------------- |
| **执行时机** | 渲染器加载之前 | 渲染页面时 |
| **主要用途** | 初始化 iframe 环境 | 自定义渲染逻辑 |
| **是否必须** | 否(有默认实现) | 否(有默认实现) |
| **典型场景** | 注入全局变量、配置环境 | 自定义页面渲染方式 |
### 调试技巧
```tsx
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
if (!subWin) {
console.error('无法获取 iframe window');
return;
}
console.log('beforeInitRender 开始执行');
// 注入变量
(subWin as any).React = window.React;
// 验证注入是否成功
console.log('React 注入成功:', !!(subWin as any).React);
// 监听 iframe 中的错误
subWin.addEventListener('error', (event) => {
console.error('iframe 错误:', event.error);
});
console.log('beforeInitRender 执行完成');
};
return pluginInstance;
});
};
```
### Q: 如何获取当前页面的完整数据?
A: 使用页面模型的 `export` 方法:
```tsx
const pageData = ctx.engine.pageModel.export();
// 或导出特定格式
const designData = ctx.engine.pageModel.export('design');
```
================================================
FILE: packages/docs-app/src/content/docs/reference/Engine/introduction.mdx
================================================
---
title: Engine 介绍
sidebar:
order: 1
---
import { Tabs, TabItem } from '@astrojs/starlight/components';
# Engine 介绍
`@chamn/engine` 是 Chameleon 可视化编程引擎的核心包,提供了一个完整的低代码编辑器解决方案。它基于 React 构建,提供了丰富的 API 和插件系统,让你可以快速构建自己的可视化页面编辑器。
## 核心特性
- 🎨 **可视化编辑**: 通过拖拽的方式快速构建页面
- 🔌 **插件化架构**: 灵活的插件系统,支持自定义扩展
- 📦 **物料管理**: 完善的组件物料管理机制
- 🎯 **实时预览**: 支持实时预览和编辑模式切换
- 🌐 **国际化支持**: 内置国际化能力
- 🎛️ **工作台定制**: 可自定义的工作台布局和组件
## 安装
```shell npm i @chamn/engine @chamn/model @chamn/render ```
```shell pnpm i @chamn/engine @chamn/model @chamn/render ```
```shell yarn i @chamn/engine @chamn/model @chamn/render ```
## 快速开始
最简单的使用方式:
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
return (
{
console.log('Engine ready!', ctx);
}}
/>
);
}
```
## 引擎架构
Chameleon Engine 采用分层架构设计,各模块之间通过清晰的依赖关系组织。以下是引擎的核心架构图:
```mermaid
flowchart TB
subgraph Engine["Engine"]
direction TB
subgraph PluginManager["pluginManager"]
direction TB
OutlineTree["outlineTree"]
PropertyPanel["propertyPanel"]
StylePanel["stylePanel"]
OtherPlugins["..."]
end
Workbench["workbench"]
subgraph Designer["Designer(plugin)"]
direction TB
Layout["layout"]
Render["render"]
end
Model["model"]
Material["material"]
end
style Engine fill:#fff,stroke:#000,stroke-width:2px,color:#000
style PluginManager fill:#fff,stroke:#000,stroke-width:2px,color:#000
style Workbench fill:#fff,stroke:#000,stroke-width:2px,color:#000
style Designer fill:#fff,stroke:#000,stroke-width:2px,color:#000
style Model fill:#fff,stroke:#000,stroke-width:2px,color:#000
style Material fill:#fff,stroke:#000,stroke-width:2px,color:#000
style OutlineTree fill:#fff,stroke:#000,stroke-width:1px,color:#000
style PropertyPanel fill:#fff,stroke:#000,stroke-width:1px,color:#000
style StylePanel fill:#fff,stroke:#000,stroke-width:1px,color:#000
style OtherPlugins fill:#fff,stroke:#000,stroke-width:1px,color:#000
style Layout fill:#fff,stroke:#000,stroke-width:1px,color:#000
style Render fill:#fff,stroke:#000,stroke-width:1px,color:#000
```
### 模块说明
#### Engine(核心容器)
- **职责**: 整个编辑器的核心容器,管理所有子模块
- **功能**:
- 初始化和管理插件系统
- 管理页面模型和物料数据
- 提供统一的事件通信机制
- 协调各模块之间的交互
#### PluginManager(插件管理器)
- **职责**: 管理所有插件的生命周期
- **功能**:
- 插件的注册、初始化、销毁
- 插件之间的通信协调
- 插件配置管理
- 插件依赖关系处理
- **依赖**: Engine、Model、Layout、Material
#### Model(页面模型)
- **职责**: 管理页面数据结构
- **功能**:
- 页面节点的增删改查
- 节点状态管理
- 数据变更通知
- 页面数据序列化/反序列化
- **被依赖**: PluginManager、Render、各插件
#### Material(物料系统)
- **职责**: 管理组件物料信息
- **功能**:
- 组件元数据管理(组件配置、属性定义等)
- 资源包管理(组件运行时资源)
- 组件与资源的关联
- **被依赖**: Engine、PluginManager、Render
#### Layout(工作台布局)
- **职责**: 提供编辑器 UI 布局框架
- **功能**:
- 左侧面板管理(组件库等)
- 中间画布区域
- 右侧面板管理(属性面板等)
- 顶部工具栏
- 布局可定制化
- **被依赖**: PluginManager、各插件
#### Render(渲染器)
- **职责**: 将页面模型渲染为可视化界面
- **功能**:
- 设计时渲染(可编辑模式)
- 预览渲染(只读模式)
- 组件实例化
- 事件处理
- **被依赖**: Designer 插件、Canvas
- **依赖**: Model、Material
## 核心概念
### Engine 组件
`Engine` 是一个 React 类组件,是整个编辑器的核心容器。它管理着页面模型、插件系统、物料库等核心功能。
### 插件系统
Engine 采用插件化架构,所有功能都通过插件实现。内置了多个常用插件:
- `Designer`: 设计器画布
- `ComponentLibrary`: 组件库面板
- `OutlineTree`: 页面结构树
- `PropertyPanel`: 属性面板
- `History`: 历史记录管理
- 等等...
### 页面模型 (PageModel)
Engine 内部使用 `CPage` 来管理页面数据,提供了完整的页面节点操作能力。
### 物料系统
物料系统包括组件描述(Material)和组件库(AssetPackage),通过 `componentName` 进行关联。
## 与阿里 LowCode Engine 对比
Chameleon Engine 和阿里 LowCode Engine 都是优秀的低代码解决方案,但在设计理念和使用场景上有所不同:
### 架构设计
| 特性 | Chameleon Engine | 阿里 LowCode Engine |
| :----------- | :----------------------- | :--------------------------- |
| **架构模式** | 插件化架构,高度可扩展 | 一体化架构,功能完整 |
| **包体积** | 轻量级,按需加载 | 体积较大,功能齐全 |
| **定制性** | 高度可定制,可完全自定义 | 定制性有限,主要面向企业场景 |
| **学习曲线** | 简单直观,易于上手 | 功能丰富,学习成本较高 |
### 核心优势
#### Chameleon Engine 的优势
✅ **轻量灵活**
- 核心包体积小,按需加载插件
- 可以只使用需要的功能模块
- 适合中小型项目和快速原型开发
✅ **高度可定制**
- 插件化架构,所有功能都可通过插件扩展
- 工作台布局完全可定制
- 可以完全按照业务需求定制编辑器
✅ **简单易用**
- API 设计简洁直观
- 文档清晰,上手快速
- 适合快速集成到现有项目
✅ **开源免费**
- Apache License 2.0 协议,完全开源
- 社区活跃,持续更新
- 无商业限制
✅ **技术栈友好**
- 基于 React,与现有 React 生态无缝集成
- 支持 TypeScript
- 使用现代前端技术栈
#### 阿里 LowCode Engine 的优势
✅ **企业级功能**
- 功能完整,开箱即用
- 与阿里云生态深度集成
- 适合大型企业级应用
✅ **商业支持**
- 提供商业技术支持
- 有专业的团队维护
- 适合对稳定性要求极高的项目
✅ **功能丰富**
- 内置大量企业级组件和模板
- 提供完整的解决方案
- 适合快速构建企业应用
### 适用场景
#### Chameleon Engine 适合:
- 🎯 需要高度定制的编辑器场景
- 🎯 中小型项目和快速原型开发
- 🎯 希望完全控制编辑器行为的团队
- 🎯 需要轻量级解决方案的项目
- 🎯 开源项目或需要开源协议的项目
#### 阿里 LowCode Engine 适合:
- 🎯 大型企业级应用开发
- 🎯 需要快速上线,对定制要求不高的场景
- 🎯 需要商业技术支持的团队
- 🎯 与阿里云生态深度集成的项目
### 总结
Chameleon Engine 更适合追求**灵活性、可定制性和轻量级**的团队,而阿里 LowCode Engine 更适合需要**完整解决方案和商业支持**的企业级项目。选择哪个引擎主要取决于你的项目需求、团队规模和技术栈偏好。
如果你需要:
- **完全控制编辑器的行为** → 选择 Chameleon Engine
- **快速集成,开箱即用** → 可以考虑阿里 LowCode Engine
- **轻量级解决方案** → 选择 Chameleon Engine
- **企业级支持和保障** → 可以考虑阿里 LowCode Engine
## 下一步
- 查看 [API 文档](./api/) 了解详细的 API 使用
- 查看 [使用示例](./usage/) 学习更多用法
- 查看 [插件开发](../plugin/plugin-develop/) 了解如何开发自定义插件
================================================
FILE: packages/docs-app/src/content/docs/reference/Engine/usage.mdx
================================================
---
title: Engine 使用示例
sidebar:
order: 3
---
# Engine 使用示例
## 基础使用
最简单的使用方式,使用默认插件列表:
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
return (
);
}
```
## 自定义插件列表
只使用部分插件:
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DesignerPlugin, ComponentLibPlugin, RightPanelPlugin } = plugins;
function App() {
return (
);
}
```
## 生命周期回调
使用 `onReady` 和 `onMount` 回调:
```tsx
import { Engine, plugins, EnginContext } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const onMount = (ctx: EnginContext) => {
console.log('Engine 已挂载');
};
const onReady = async (ctx: EnginContext) => {
console.log('所有插件已加载完成');
// 可以在这里访问 pluginManager 和 engine
const pluginManager = ctx.pluginManager;
const engine = ctx.engine;
// 获取设计器插件
const designerPlugin = await pluginManager.get('Designer');
// 获取历史记录插件
const historyPlugin = await pluginManager.get('History');
};
return (
);
}
```
## 自定义工作台
通过 `workbenchConfig` 配置工作台:
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
return (
);
}
```
## 动态控制工作台
通过 `onReady` 回调动态控制工作台:
```tsx
import { Engine, plugins, EnginContext } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
import { Button } from 'antd';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const onReady = (ctx: EnginContext) => {
const workbench = ctx.engine.getWorkbench();
// 自定义顶部栏
workbench?.replaceTopBarView(
我的编辑器
ctx.engine.preview()}>预览
ctx.engine.existPreview()}>退出预览
);
};
return ;
}
```
## 保存和加载页面数据
```tsx
import { Engine, plugins, EnginContext } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
import { Button, message } from 'antd';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const [pageSchema, setPageSchema] = useState(() => {
// 从本地存储加载
const saved = localStorage.getItem('pageSchema');
return saved ? JSON.parse(saved) : defaultPageSchema;
});
const onReady = (ctx: EnginContext) => {
const workbench = ctx.engine.getWorkbench();
workbench?.replaceTopBarView(
{
// 导出页面数据
const pageData = ctx.engine.pageModel.export();
localStorage.setItem('pageSchema', JSON.stringify(pageData));
message.success('保存成功');
}}
>
保存
);
};
return ;
}
```
## 动态更新页面
```tsx
import { Engine, plugins, EnginContext } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
import { Button } from 'antd';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const onReady = (ctx: EnginContext) => {
const workbench = ctx.engine.getWorkbench();
workbench?.replaceTopBarView(
{
// 从服务器获取新页面数据
fetch('/api/page')
.then((res) => res.json())
.then((newPageSchema) => {
// 更新页面
ctx.engine.updatePage(newPageSchema);
});
}}
>
刷新页面
);
};
return ;
}
```
## 动态更新物料
```tsx
import { Engine, plugins, EnginContext } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
import { Button } from 'antd';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const onReady = async (ctx: EnginContext) => {
const workbench = ctx.engine.getWorkbench();
workbench?.replaceTopBarView(
{
// 从服务器获取新物料
const [newMaterials, newAssetPackages] = await Promise.all([
fetch('/api/materials').then((res) => res.json()),
fetch('/api/asset-packages').then((res) => res.json()),
]);
// 更新物料
await ctx.engine.updateMaterials(newMaterials, newAssetPackages, {
formatComponents: (components) => {
// 自定义组件格式化逻辑
return components;
},
});
}}
>
更新物料
);
};
return (
);
}
```
## 监听节点选中
```tsx
import { Engine, plugins, EnginContext } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const onReady = (ctx: EnginContext) => {
// 监听节点选中变化
ctx.engine.emitter.on('onSelectNodeChange', ({ node }) => {
if (node) {
console.log('选中节点:', node);
console.log('节点ID:', node.id);
console.log('组件名:', node.componentName);
} else {
console.log('取消选中');
}
});
};
return ;
}
```
## 自定义渲染器 JS 地址
如果渲染器 JS 不在默认位置,可以自定义:
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
return (
);
}
```
## 配置 Monaco Editor CDN
如果使用 CDN 加载 Monaco Editor:
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
return (
);
}
```
## 自定义样式
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
import './custom.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
return (
);
}
```
## 使用 beforeInitRender
`beforeInitRender` 用于在渲染器初始化之前配置 iframe 环境。
### 场景 1: 注入全局变量和配置
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
if (!subWin) return;
// 必须:注入 React
(subWin as any).React = window.React;
(subWin as any).ReactDOM = window.ReactDOM;
(subWin as any).ReactDOMClient = window.ReactDOMClient;
// 注入应用配置
(subWin as any).APP_CONFIG = {
apiUrl: process.env.REACT_APP_API_URL,
theme: 'light',
language: 'zh-CN',
};
// 注入工具函数
(subWin as any).utils = {
formatDate: (date) => new Date(date).toLocaleDateString(),
formatCurrency: (amount) => `¥${amount.toFixed(2)}`,
};
};
return pluginInstance;
});
};
return (
);
}
```
### 场景 2: 注入第三方库
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
import axios from 'axios';
import dayjs from 'dayjs';
import _ from 'lodash';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
if (!subWin) return;
// 注入 React(必需)
(subWin as any).React = window.React;
(subWin as any).ReactDOM = window.ReactDOM;
(subWin as any).ReactDOMClient = window.ReactDOMClient;
// 注入第三方库
(subWin as any).axios = axios;
(subWin as any).dayjs = dayjs;
(subWin as any)._ = _;
console.log('第三方库注入成功');
};
return pluginInstance;
});
};
return (
);
}
```
### 场景 3: 设置跨窗口通信
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
import { message } from 'antd';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
if (!subWin) return;
// 注入 React
(subWin as any).React = window.React;
(subWin as any).ReactDOM = window.ReactDOM;
(subWin as any).ReactDOMClient = window.ReactDOMClient;
// 监听来自 iframe 的消息
subWin.addEventListener('message', (event) => {
if (event.data.source === 'component') {
console.log('收到组件消息:', event.data);
// 处理不同类型的消息
switch (event.data.type) {
case 'notification':
message.info(event.data.message);
break;
case 'error':
message.error(event.data.message);
break;
}
}
});
// 注入发送消息到父窗口的方法
(subWin as any).sendToEditor = (data) => {
window.postMessage(
{
source: 'iframe',
timestamp: Date.now(),
...data,
},
'*'
);
};
};
return pluginInstance;
});
};
return (
);
}
```
### 场景 4: 初始化主题和样式
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
const subDoc = iframe.getDocument();
if (!subWin || !subDoc) return;
// 注入 React
(subWin as any).React = window.React;
(subWin as any).ReactDOM = window.ReactDOM;
(subWin as any).ReactDOMClient = window.ReactDOMClient;
// 注入主题配置
(subWin as any).THEME = {
primaryColor: '#1890ff',
successColor: '#52c41a',
warningColor: '#faad14',
errorColor: '#ff4d4f',
fontSize: '14px',
borderRadius: '4px',
};
// 添加全局样式
const style = subDoc.createElement('style');
style.textContent = `
:root {
--primary-color: #1890ff;
--text-color: #333;
--border-color: #d9d9d9;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
'Helvetica Neue', Arial, sans-serif;
color: var(--text-color);
}
`;
subDoc.head.appendChild(style);
};
return pluginInstance;
});
};
return (
);
}
```
### 场景 5: 注入模拟数据和 API
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
if (!subWin) return;
// 注入 React
(subWin as any).React = window.React;
(subWin as any).ReactDOM = window.ReactDOM;
(subWin as any).ReactDOMClient = window.ReactDOMClient;
// 注入模拟 API
(subWin as any).mockAPI = {
// 获取用户信息
getUserInfo: async () => {
await new Promise((resolve) => setTimeout(resolve, 500));
return {
id: 1,
name: '张三',
email: 'zhangsan@example.com',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Felix',
};
},
// 获取产品列表
getProducts: async () => {
await new Promise((resolve) => setTimeout(resolve, 300));
return [
{ id: 1, name: '产品 A', price: 299, stock: 100 },
{ id: 2, name: '产品 B', price: 399, stock: 50 },
{ id: 3, name: '产品 C', price: 199, stock: 200 },
];
},
// 提交订单
submitOrder: async (orderData) => {
await new Promise((resolve) => setTimeout(resolve, 800));
console.log('提交订单:', orderData);
return {
success: true,
orderId: `ORDER-${Date.now()}`,
};
},
};
// 注入模拟数据
(subWin as any).mockData = {
users: [
{ id: 1, name: '张三', role: 'admin' },
{ id: 2, name: '李四', role: 'user' },
],
categories: ['电子产品', '图书', '服装', '食品'],
};
};
return pluginInstance;
});
};
return (
);
}
```
### 场景 6: 开发环境调试工具
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
if (!subWin) return;
// 注入 React
(subWin as any).React = window.React;
(subWin as any).ReactDOM = window.ReactDOM;
(subWin as any).ReactDOMClient = window.ReactDOMClient;
// 只在开发环境注入调试工具
if (process.env.NODE_ENV === 'development') {
// 调试工具对象
(subWin as any).__DEBUG__ = {
// 日志工具
log: (...args) => {
console.log('[iframe]', ...args);
},
error: (...args) => {
console.error('[iframe]', ...args);
},
warn: (...args) => {
console.warn('[iframe]', ...args);
},
// 组件调试
inspectComponent: (componentName) => {
console.log(`检查组件: ${componentName}`);
// 可以添加更多检查逻辑
},
// 性能监控
performance: {
start: (label) => {
console.time(`[iframe] ${label}`);
},
end: (label) => {
console.timeEnd(`[iframe] ${label}`);
},
},
};
// 启用详细日志
(subWin as any).__VERBOSE_LOGGING__ = true;
// 监听错误
subWin.addEventListener('error', (event) => {
console.error('[iframe error]', {
message: event.error?.message,
stack: event.error?.stack,
filename: event.filename,
lineno: event.lineno,
});
});
// 监听未捕获的 Promise 错误
subWin.addEventListener('unhandledrejection', (event) => {
console.error('[iframe unhandled promise]', event.reason);
});
}
};
return pluginInstance;
});
};
return (
);
}
```
### 综合示例:完整的 beforeInitRender 配置
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
import axios from 'axios';
import dayjs from 'dayjs';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
const subWin = iframe.getWindow();
const subDoc = iframe.getDocument();
if (!subWin || !subDoc) {
console.error('无法获取 iframe window 或 document');
return;
}
console.log('开始初始化 iframe 环境...');
try {
// 1. 注入 React(必需)
(subWin as any).React = window.React;
(subWin as any).ReactDOM = window.ReactDOM;
(subWin as any).ReactDOMClient = window.ReactDOMClient;
console.log('✓ React 注入成功');
// 2. 注入应用配置
(subWin as any).APP_CONFIG = {
apiUrl: process.env.REACT_APP_API_URL || 'https://api.example.com',
environment: process.env.NODE_ENV,
version: '1.0.0',
features: {
enableAnalytics: true,
enableDebug: process.env.NODE_ENV === 'development',
},
};
console.log('✓ 应用配置注入成功');
// 3. 注入第三方库
(subWin as any).axios = axios;
(subWin as any).dayjs = dayjs;
console.log('✓ 第三方库注入成功');
// 4. 设置跨窗口通信
(subWin as any).sendToEditor = (data) => {
window.postMessage(
{
source: 'iframe',
timestamp: Date.now(),
...data,
},
'*'
);
};
subWin.addEventListener('message', (event) => {
if (event.data.source === 'component') {
console.log('收到组件消息:', event.data);
}
});
console.log('✓ 跨窗口通信设置成功');
// 5. 初始化样式和主题
(subWin as any).THEME = {
primaryColor: '#1890ff',
fontSize: '14px',
};
const style = subDoc.createElement('style');
style.textContent = `
:root {
--primary-color: #1890ff;
--text-color: #333;
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto;
}
`;
subDoc.head.appendChild(style);
console.log('✓ 主题和样式初始化成功');
// 6. 开发环境配置
if (process.env.NODE_ENV === 'development') {
(subWin as any).__DEBUG__ = {
log: (...args) => console.log('[iframe]', ...args),
error: (...args) => console.error('[iframe]', ...args),
};
subWin.addEventListener('error', (event) => {
console.error('[iframe error]', event.error);
});
console.log('✓ 调试工具初始化成功');
}
console.log('iframe 环境初始化完成!');
} catch (error) {
console.error('iframe 环境初始化失败:', error);
}
};
return pluginInstance;
});
};
return (
);
}
```
:::tip
**注意事项:**
1. 必须注入 React、ReactDOM 和 ReactDOMClient,否则渲染器无法正常工作
2. `beforeInitRender` 会在每次重新加载 iframe 时执行
3. 建议添加错误处理,确保初始化过程的健壮性
4. 可以通过 `console.log` 验证注入是否成功
:::
================================================
FILE: packages/docs-app/src/content/docs/reference/Material/_category_.json
================================================
{
"position": 2,
"label": "组件物料",
"collapsible": true,
"collapsed": false,
"customProps": {
"description": "This description can be used in the swizzled DocCard"
}
}
================================================
FILE: packages/docs-app/src/content/docs/reference/Material/advanceDevelopMaterial.mdx
================================================
---
title: 高级物料开发
sidebar:
order: 3
---
import { Code } from '@astrojs/starlight/components';
import GridLayoutMeta from '../../../../codeSnippets/GridLayoutMeta.tsx?raw';
import GridLayoutComponent from '../../../../codeSnippets/GridLayoutComponent.tsx?raw';
import GridLayoutWrap from '../../../../codeSnippets/GridLayoutWrap.tsx?raw';
import GridItemMeta from '../../../../codeSnippets/GridItemMeta.tsx?raw';
# 高级物料开发
高级物料开发允许你通过 `advanceCustom` 配置项来深度定制组件在设计器中的行为,包括拖拽、选择、工具栏、右侧面板等各个方面。本文档将以 `ReactGridLayout` 组件为例,详细介绍如何使用这些高级特性。
## 什么是高级物料?
高级物料是指那些需要特殊编辑行为的组件,例如:
- **布局容器组件**:需要自定义拖拽和放置逻辑
- **复杂交互组件**:需要定制工具栏或选择框样式
- **特殊渲染组件**:需要在编辑模式下包装或修改组件行为
- **响应式组件**:需要根据断点动态调整布局
通过 `advanceCustom` 配置,你可以完全控制组件在设计器中的编辑体验。
## advanceCustom 配置项概览
`advanceCustom` 提供了丰富的配置选项,主要包括:
### 生命周期钩子
- `onDragStart` / `onDragging` / `onDragEnd`:拖拽生命周期
- `onSelect`:选中时触发
- `onCopy`:复制时触发
- `onDelete`:删除时触发
- `onNewAdd`:首次添加到画布时触发
- `onDrop`:放置到目标位置时触发
### 权限控制
- `canDragNode`:控制节点是否可拖拽
- `canDropNode`:控制节点是否可被放置
- `canAcceptNode`:控制节点是否可接受子节点
- `disableEditorDragDom`:禁用编辑器默认的拖拽行为
### 视图定制
- `wrapComponent`:包装组件,定制编辑模式下的行为
- `toolbarViewRender`:自定义工具栏视图
- `selectRectViewRender`:自定义选中框视图
- `hoverRectViewRender`:自定义悬停框视图
- `dropViewRender`:自定义放置预览视图
- `ghostViewRender`:自定义拖拽占位视图
### 面板配置
- `rightPanel`:配置右侧属性面板的显示和定制
- `autoGetDom`:控制是否自动获取 DOM 元素
## ReactGridLayout 示例
`ReactGridLayout` 是一个完整的高级物料示例,它实现了基于 GridStack 的响应式布局系统。让我们逐步分析它的实现。
### 组件结构
ReactGridLayout 包含两个主要组件:
1. **GridLayout**:布局容器,管理整个网格系统
2. **GridItem**:布局项,每个可拖拽的子元素
### 1. GridLayout 物料定义
#### 关键配置解析
**`isContainer: true`**
- 标记为容器组件,允许放置子元素
**`advanceCustom.wrapComponent`**
- 包装原始组件,注入设计器相关的逻辑
- 获取 iframe 窗口引用(设计器运行在 iframe 中)
- 监听 GridStack 的拖拽事件,与设计器选择状态同步
**`advanceCustom.rightPanel.visual: false`**
- 隐藏可视化面板,因为布局组件不需要可视化编辑
**`advanceCustom.autoGetDom: false`**
- 禁用自动获取 DOM,因为我们需要手动控制 DOM 引用
### 2. GridLayout 组件实现
#### 核心功能
1. **GridStack 初始化**:使用 GridStack 库创建网格布局
2. **响应式断点**:监听窗口大小变化,切换不同的布局断点
3. **Context 提供**:通过 React Context 向子组件提供 GridStack 实例和当前断点信息
### 3. LayoutWrap 包装组件
#### 设计器集成
`LayoutWrap` 是连接组件和设计器的桥梁:
- **监听布局变化**:当用户拖拽 GridItem 时,GridStack 会触发 `change` 事件
- **同步到页面模型**:将布局变化同步到 Chameleon 的页面模型中
- **响应式支持**:根据当前窗口宽度确定断点,更新对应断点的布局信息
### 4. GridItem 物料定义
#### 高级特性解析
**`disableEditorDragDom: true`**
- 禁用编辑器默认的 DOM 拖拽,使用 GridStack 自己的拖拽系统
**`advanceCustom.toolbarViewRender`**
- 自定义工具栏,显示当前布局信息(宽度、高度、位置)
- 实时监听窗口大小变化,更新显示的布局信息
- 移除默认的显示/隐藏按钮(与 GridStack 拖拽冲突)
**`advanceCustom.onDragStart`**
- 返回 `false`,禁用编辑器默认的拖拽行为
**`advanceCustom.wrapComponent`**
- 包装组件,注册组件实例引用
- 用于在工具栏中获取实时的布局信息
**`advanceCustom.canDropNode`**
- 只允许 GridItem 被放置在 GridLayout 中
- 确保组件层级关系的正确性
**`advanceCustom.onCopy`**
- 复制时重置 x、y 坐标
- 避免复制后的元素重叠
**`advanceCustom.onNewAdd`**
- 只允许从 GridLayout 拖入创建新的 GridItem
**`advanceCustom.rightPanel.advanceOptions`**
- 隐藏 loop 和 render 选项
- 简化属性面板,只显示必要的配置
## 常用场景示例
### 场景 1:自定义拖拽行为
```tsx
advanceCustom: {
onDragStart: async (node) => {
// 执行自定义逻辑
console.log('开始拖拽', node.id);
return true; // 返回 true 允许拖拽
},
onDragEnd: async (node) => {
// 拖拽结束后的清理工作
console.log('拖拽结束', node.id);
},
}
```
### 场景 2:自定义工具栏
```tsx
advanceCustom: {
toolbarViewRender: ({ node, toolBarItemList }) => {
return (
自定义信息: {node.id}
{toolBarItemList}
);
},
}
```
### 场景 3:控制放置权限
```tsx
advanceCustom: {
canDropNode: async (node, params) => {
const { dropNode } = params;
// 只允许放置在特定类型的容器中
if (dropNode?.value.componentName === 'MyContainer') {
return true;
}
return false;
},
}
```
### 场景 4:包装组件注入逻辑
```tsx
advanceCustom: {
wrapComponent: (Comp, options) => {
return (props) => {
// 注入设计器相关的逻辑
const designerCtx = options.ctx;
return (
);
};
},
}
```
### 场景 5:自定义右侧面板
```tsx
advanceCustom: {
rightPanel: {
visual: false, // 隐藏可视化面板
advance: true,
advanceOptions: {
loop: false, // 隐藏循环选项
render: true, // 显示渲染选项
},
customTabs: [
{
key: 'custom',
name: '自定义配置',
view: ({ node }) => {
return 自定义配置面板
;
},
},
],
},
}
```
## 最佳实践
### 1. 合理使用 wrapComponent
`wrapComponent` 是一个强大的功能,但要注意:
- ✅ **适合场景**:需要访问设计器上下文、需要监听第三方库事件、需要注入特殊逻辑
- ❌ **避免场景**:简单的样式修改(应该用 CSS)、不需要设计器交互的组件
### 2. 性能优化
- 使用 `debounce` 或 `throttle` 处理频繁的事件(如 resize)
- 避免在 `toolbarViewRender` 中执行重计算
- 合理使用 `useMemo` 和 `useCallback` 优化渲染
### 3. 错误处理
- 所有异步钩子都应该有错误处理
- 返回 `false` 来阻止操作时,确保用户能理解原因
### 4. 类型安全
- 使用 TypeScript 确保类型安全
- 为自定义的 props 和 context 定义类型
## 总结
高级物料开发通过 `advanceCustom` 配置提供了强大的定制能力,让你可以:
- 🎯 **完全控制**组件的编辑行为
- 🎨 **自定义视图**和交互体验
- 🔌 **深度集成**第三方库(如 GridStack)
- 📱 **实现响应式**布局和断点管理
`ReactGridLayout` 是一个很好的参考示例,展示了如何将这些高级特性组合使用,创建一个功能完整的布局组件。
## 相关资源
- [物料开发基础文档](./developMaterial.mdx)
- [物料协议文档](../PageSchema/material.mdx)
- [插件开发指南](../Plugin/plugin-develop.mdx)
- [GridStack 官方文档](https://gridstackjs.com/)
================================================
FILE: packages/docs-app/src/content/docs/reference/Material/developMaterial.mdx
================================================
---
title: 物料开发
sidebar:
order: 2
---
物料开发使用该[【模版工程】](https://github.com/ByteCrazy/chameleon-material-template.git)即可
```bash
git clone https://github.com/ByteCrazy/chameleon-material-template.git
cd chameleon-material-template
pnpm i
// 开发
pnpm run start
// 构建
pnpm run build
```
访问 [http://localhost:3000](http://localhost:3000)
开发完成之后发布 npm 包即可
### 物料包命名规范
统一采用 `chamn-material-xxxx` 的方式命名
================================================
FILE: packages/docs-app/src/content/docs/reference/Material/introduction.mdx
================================================
---
title: 什么是组件物料?
sidebar:
order: 1
---
import { Code } from '@astrojs/starlight/components';
import MetarialImg from './assets/material.png';
import ButtonMeta from '../../../../codeSnippets/ButtonMeta.tsx?raw';
组件物料包括两部分:
- 组件库
- 组件描述
组件库和组件描述之间 通过 componentName 来一一绑定对应
## 组件库
组件库就是组件的 JS 运行库, 比如 Ant Design、ElemtnUI、Vant UI 等等,它们是可以被运行的 JS 代码库。
## 组件描述
如果想要编辑器知道组件的具体功能,比如支持那些属性,那些事件等等,则需要一份对应的描述文件详细描述了组件的 props 的各种行为,以及数据结构,这样编辑器才能对组件进行控制编辑。这份描述文件就是组件描述。
物料协议请参考 Schema 文档
### Example:
对 Ant Design UI 库的 Button 组件的物料描述:
================================================
FILE: packages/docs-app/src/content/docs/reference/PageSchema/built-in-setter.mdx
================================================
---
sidebar:
order: 3
title: Setter
---
## 什么是 setter ?
Setter 是一个输入控件,用于在组件物料协议中描述组件 props 的输入方式,同时提供对应的输入控件让用户可以准确的输入组件的 props 值
## 内置 Setter 列表
### StringSetter
[code link](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/component/CustomSchemaForm/components/Setters/StringSetter)
### ArraySetter
[code link](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/component/CustomSchemaForm/components/Setters/ArraySetter/index.tsx)
Example:
```tsx
const customAttributesMeta: CMaterialPropsType[number] = {
name: '$$attributes',
title: '属性',
valueType: 'object',
setters: [
{
componentName: 'ArraySetter',
props: {
item: {
setters: [
{
componentName: 'ShapeSetter',
props: {
elements: [
{
name: 'key',
title: '属性名',
valueType: 'string',
setters: ['StringSetter'],
},
{
name: 'value',
title: '值',
valueType: 'string',
setters: ['StringSetter', 'NumberSetter', 'JSONSetter', 'FunctionSetter', 'ExpressionSetter'],
},
],
collapse: false,
},
initialValue: {},
},
],
initialValue: {},
},
},
initialValue: [],
},
],
};
```
### ShapeSetter
[code link](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/component/CustomSchemaForm/components/Setters/ShapeSetter/index.tsx)
Example:
```tsx
const CSSBindPropsSchema: CMaterialPropsType<'CSSValueSetter'> = [
{
name: 'css',
title: 'CSS Variable Bind',
valueType: 'array',
setters: [
{
componentName: 'ArraySetter',
props: {
item: {
setters: [
{
componentName: 'ShapeSetter',
props: {
elements: [
{
name: 'key',
title: '属性',
valueType: 'string',
setters: ['StringSetter'],
description: '',
},
{
name: 'value',
title: '置',
valueType: 'string',
setters: ['ExpressionSetter'],
description: '',
},
],
collapse: false,
},
initialValue: {
key: '',
value: {
type: CNodePropsTypeEnum.EXPRESSION,
value: '',
},
},
},
],
initialValue: {
key: '',
value: {
type: CNodePropsTypeEnum.EXPRESSION,
value: '',
},
},
},
},
initialValue: [],
},
],
},
];
```
### NumberSetter
[code link](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/component/CustomSchemaForm/components/Setters/NumberSetter/index.tsx)
### ExpressionSetter
[code link](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/component/CustomSchemaForm/components/Setters/ExpressionSetter/index.tsx)
### BooleanSetter
[code link](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/component/CustomSchemaForm/components/Setters/BooleanSetter/index.tsx)
### SelectSetter
[code link](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/component/CustomSchemaForm/components/Setters/BoolSelectSettereanSetter/index.tsx)
```tsx
{
title: 'HTML 标签',
componentName: 'CNativeTag',
props: [
{
name: 'htmlTag',
title: '标签名',
valueType: 'string',
setters: [
{
componentName: 'SelectSetter',
props: {
options: HTMl_TAGS.map((tag) => {
return {
name: tag,
value: tag,
};
}),
},
},
],
},
customAttributesMeta,
],
```
### JSONSetter
[code link](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/component/CustomSchemaForm/components/Setters/JSONSetter/index.tsx)
### FunctionSetter
[code link](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/component/CustomSchemaForm/components/Setters/FunctionSetter/index.tsx)
### TextAreaSetter
[code link](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/component/CustomSchemaForm/components/Setters/TextAreaSetter/index.tsx)
### CSSValueSetter
[code link](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/component/CustomSchemaForm/components/Setters/CSSValueSetter/index.tsx)
## 自定义 Setter
参考 [自定义 Setter](/plugin/custom-setter)
================================================
FILE: packages/docs-app/src/content/docs/reference/PageSchema/material.mdx
================================================
---
sidebar:
order: 2
title: 物料协议
---
描述请参考 [【 Material 模型定义】](https://github.com/hlerenow/chameleon/blob/master/packages/model/src/types/material.ts#L338)
Example: [【示例物料】](https://github.com/hlerenow/chameleon/tree/master/packages/demo-page/src/material)
================================================
FILE: packages/docs-app/src/content/docs/reference/PageSchema/page.mdx
================================================
---
sidebar:
order: 1
title: 页面协议
---
# PageSchema
| Key | Value Type | Description | Example |
| :------------- | :---------------- | :------------------------------------ | :------- |
| version | string | 协议版本号 | 1.0.0 |
| name | string | 页面名称 | TestPage |
| css | CSSType | 页面 css | — |
| componentsMeta | ComponentMetaType | 页面使用的组件物料 | — |
| thirdLibs | LibMetaType | 页面依赖使用的第三方库描述 | — |
| componentsTree | CRootNodeDataType | 页面结构 | — |
| assets | AssetPackage | 页面使用的所有资源信息,包含 url 信息 | — |
更多描述请参考 [【模型定定义】](https://github.com/hlerenow/chameleon/blob/master/packages/model/src/types/page.ts)
## CRootNodeDataType
| Key | Value Type | Description | Example |
| :------------ | :-------------------------- | :-------------------------------------- | :------------------------- |
| id | string | 节点唯一 id | xassas |
| title | string | 节点名称 | root node |
| componentName | string | 组件名称 | Button |
| type | 'lowcode'、 'normal' | 节点类型, lowcode 表示低码组件 | normal |
| props | CPropObjDataType | 组件传入的属性值 | \{'text': '我是一个按钮'\} |
| state | Record\ | 组件的 state | - |
| nodeName | string | 组件的 state 别名,便于其他组件索引使用 | - |
| schema | CPageDataType | if type is lowcode, schema is required | - |
| classNames | ClassNameType[] | 存储节点的 className | - |
| css | CSSType[] | 存储节点的 css 样式 | - |
| refId | string | 组件的唯一引用标示 | - |
| children | (string \| CNodeDataType)[] | 当前节点的 children 节点 | - |
更多描述请参考 [【模型定定义】](https://github.com/hlerenow/chameleon/blob/master/packages/model/src/types/node.ts)
## ComponentMetaType
更多描述请参考 [【模型定定义】](https://github.com/hlerenow/chameleon/blob/master/packages/model/src/types/page.ts)
## LibMetaType
更多描述请参考 [【模型定定义】](https://github.com/hlerenow/chameleon/blob/master/packages/model/src/types/base.ts)
## CSSType
更多描述请参考 [【模型定定义】](https://github.com/hlerenow/chameleon/blob/master/packages/model/src/types/base.ts)
## AssetPackage
更多描述请参考 [【模型定定义】](https://github.com/hlerenow/chameleon/blob/master/packages/model/src/types/base.ts)
================================================
FILE: packages/docs-app/src/content/docs/reference/Plugin/built-in-plugins-usage.mdx
================================================
---
title: 内置插件使用指南
sidebar:
order: 4
---
# 内置插件使用指南
Chameleon Engine 提供了一套完整的内置插件,覆盖了低代码编辑器的核心功能。本文档详细介绍每个插件的功能、配置和使用方法。
## DesignerPlugin - 设计器画布
设计器画布是编辑器的核心插件,负责渲染和编辑页面内容。
### 功能特性
- 📐 可视化画布渲染
- 🖱️ 拖拽添加组件
- ✏️ 组件选中和编辑
- 📱 响应式预览
- 🎨 实时更新
### 配置选项
```tsx
import { Engine, plugins } from '@chamn/engine';
import { DesignerPluginInstance } from '@chamn/engine';
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
// 自定义渲染器
pluginInstance.ctx.config.customRender = async ({ iframe, assets, page, pageModel, ready, renderJSUrl }) => {
// 自定义渲染逻辑
};
// 初始化前钩子
pluginInstance.ctx.config.beforeInitRender = async ({ iframe }) => {
// 初始化 iframe 环境
};
// 自定义组件
pluginInstance.ctx.config.components = {
Button: MyButton,
Input: MyInput,
};
return pluginInstance;
});
};
```
### 导出方法
```tsx
const onReady = async (ctx: EnginContext) => {
const designerPlugin = await ctx.pluginManager.get('Designer');
const designer = designerPlugin?.export;
// 设置预览模式
designer?.setPreviewMode();
// 设置编辑模式
designer?.setEditMode();
// 设置画布宽度
designer?.setCanvasWidth(1200);
designer?.setCanvasWidth('100%');
// 获取 iframe DOM
const iframeDom = designer?.getIframeDom();
// 获取设计器窗口
const designerWindow = designer?.getDesignerWindow();
// 重新加载
designer?.reload();
};
```
### 使用示例
```tsx
import { Engine, plugins, EnginContext } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
import { Button } from 'antd';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const onReady = async (ctx: EnginContext) => {
const workbench = ctx.engine.getWorkbench();
const designerPlugin = await ctx.pluginManager.get('Designer');
// 自定义工具栏
workbench?.replaceTopBarView(
designerPlugin?.export.setPreviewMode()}>预览
designerPlugin?.export.setEditMode()}>编辑
designerPlugin?.export.setCanvasWidth(375)}>移动端
designerPlugin?.export.setCanvasWidth('100%')}>全屏
);
};
return ;
}
```
## HistoryPlugin - 历史记录
历史记录插件提供撤销/重做功能。
### 功能特性
- ⏮️ 撤销操作
- ⏭️ 重做操作
- 🔄 重置到初始状态
- 📝 自动记录节点变化
### 导出方法
```tsx
const onReady = async (ctx: EnginContext) => {
const historyPlugin = await ctx.pluginManager.get('History');
const history = historyPlugin?.export;
// 撤销(上一步)
history?.preStep();
// 重做(下一步)
history?.nextStep();
// 重置到初始状态
history?.reset();
// 判断是否可以撤销
const canUndo = history?.canGoPreStep();
// 判断是否可以重做
const canRedo = history?.canGoNextStep();
// 手动添加历史记录
history?.addStep();
};
```
### 使用示例
```tsx
import { Engine, plugins, EnginContext } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
import { Button } from 'antd';
import { RollbackOutlined } from '@ant-design/icons';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const onReady = async (ctx: EnginContext) => {
const workbench = ctx.engine.getWorkbench();
const historyPlugin = await ctx.pluginManager.get('History');
const history = historyPlugin?.export;
workbench?.replaceTopBarView(
} disabled={!history?.canGoPreStep()} onClick={() => history?.preStep()}>
撤销
}
disabled={!history?.canGoNextStep()}
onClick={() => history?.nextStep()}
>
重做
history?.reset()}>重置
);
};
return ;
}
```
## HotkeysPlugin - 快捷键
快捷键插件为编辑器提供键盘快捷操作。
### 内置快捷键
| 快捷键 | 功能 | 说明 |
| :----------------- | :----------------- | :------------------------- |
| `Ctrl + C` | 复制节点 | 复制当前选中的节点 |
| `Ctrl + V` | 粘贴节点 | 粘贴已复制的节点 |
| `Ctrl + Z` | 撤销 | 撤销上一步操作 |
| `Ctrl + Shift + Z` | 重做 | 重做下一步操作 |
| `Delete` | 删除节点 | 删除当前选中的节点 |
| `↑` / `W` | 向上移动 | 将节点向上移动 |
| `↓` / `S` | 向下移动 | 将节点向下移动 |
| `←` / `A` | 向左移动 | 将节点向左移动 |
| `→` / `D` | 向右移动 | 将节点向右移动 |
| `Shift + W` | 向上移动到兄弟节点 | 将节点移动到上一个兄弟位置 |
| `Shift + S` | 向下移动到兄弟节点 | 将节点移动到下一个兄弟位置 |
### 功能特性
- ⌨️ 键盘快捷操作
- 🎯 支持组合键
- 🔧 可扩展自定义快捷键
- 🖥️ 支持主窗口和 iframe
### 使用说明
快捷键插件无需额外配置,加入插件列表即可使用:
```tsx
import { Engine, plugins } from '@chamn/engine';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
return ;
}
```
## ComponentLibPlugin - 组件库
组件库插件显示可用的组件列表,支持拖拽添加到画布。
### 功能特性
- 📦 组件分类展示
- 🔍 组件搜索
- 🖱️ 拖拽添加组件
- 🎨 自定义组件图标
### 配置选项
```tsx
import { Engine, plugins } from '@chamn/engine';
import { PluginInstance } from '@chamn/engine';
import { ComponentLibPluginConfig } from '@chamn/engine';
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin>('ComponentLib', (pluginInstance) => {
// 自定义搜索栏
pluginInstance.ctx.config.customSearchBar = ({ defaultInputView, updateState }) => {
return (
{defaultInputView}
{/* 添加自定义元素 */}
);
};
return pluginInstance;
});
};
```
### 使用示例
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
// 定义组件物料
const materials = [
{
componentName: 'Button',
title: '按钮',
icon: ,
category: '基础组件',
snippets: [
{
title: '主要按钮',
snapshot: 'https://example.com/button-primary.png',
schema: {
componentName: 'Button',
props: {
type: 'primary',
children: '主要按钮',
},
},
},
{
title: '次要按钮',
snapshot: 'https://example.com/button-default.png',
schema: {
componentName: 'Button',
props: {
type: 'default',
children: '次要按钮',
},
},
},
],
},
];
function App() {
return ;
}
```
## OutlineTreePlugin - 页面结构树
页面结构树插件以树形结构展示页面的组件层级。
### 功能特性
- 🌳 树形结构展示
- 👁️ 显示/隐藏节点
- 🔒 锁定/解锁节点
- ✏️ 重命名节点
- 🗑️ 删除节点
- 📋 复制节点
### 使用示例
```tsx
import { Engine, plugins, EnginContext } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const onReady = (ctx: EnginContext) => {
// 监听节点选中
ctx.engine.emitter.on('onSelectNodeChange', ({ node }) => {
if (node) {
console.log('选中节点:', node.title, node.id);
}
});
};
return ;
}
```
## RightPanelPlugin - 右侧面板
右侧面板插件提供属性编辑、样式编辑等功能面板的容器。
### 功能特性
- 📝 属性编辑面板
- 🎨 样式编辑面板
- 🔧 高级设置面板
- 📊 组件状态面板
### 子面板
右侧面板包含多个子面板:
- **PropertyPanel**: 属性面板
- **VisualPanelPlus**: 样式面板
- **AdvancePanel**: 高级面板
- **ComponentStatePanel**: 组件状态面板
- **EventPanel**: 事件面板
### 配置选项
```tsx
import { Engine, plugins } from '@chamn/engine';
import { PluginInstance } from '@chamn/engine';
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('RightPanel', (pluginInstance) => {
// 自定义属性设置器
pluginInstance.ctx.config.customPropertySetterMap = {
MyCustomSetter: (props) => {
return 自定义设置器
;
},
};
return pluginInstance;
});
};
```
### 使用示例
```tsx
import { Engine, plugins, EnginContext } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const onReady = (ctx: EnginContext) => {
const workbench = ctx.engine.getWorkbench();
// 可以控制右侧面板的显示/隐藏
workbench?.toggleRightPanel();
};
return ;
}
```
## GlobalStatePanelPlugin - 全局状态管理
全局状态管理插件用于管理页面的全局数据和变量。
### 功能特性
- 📊 管理全局变量
- 🔄 数据绑定
- 📝 JSON 编辑器
- 💾 数据持久化
### 使用示例
```tsx
import { Engine, plugins, EnginContext } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const onReady = (ctx: EnginContext) => {
// 访问页面模型的全局状态
const globalState = ctx.engine.pageModel.state;
console.log('全局状态:', globalState);
// 监听状态变化
ctx.engine.pageModel.emitter.on('onStateChange', () => {
console.log('状态已更新');
});
};
return ;
}
```
## DisplaySourceSchema - 源码展示
源码展示插件用于查看和复制页面的 JSON Schema。
### 功能特性
- 👀 查看页面 Schema
- 📋 复制 Schema
- 🎨 语法高亮
- 💾 导出 JSON
### 使用示例
```tsx
import { Engine, plugins, EnginContext } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
import { Button, Modal } from 'antd';
import { DisplaySourceSchema } from '@chamn/engine';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const onReady = (ctx: EnginContext) => {
const workbench = ctx.engine.getWorkbench();
workbench?.replaceTopBarView(
查看源码
);
};
return ;
}
```
## 自定义插件列表
你可以根据需要选择性地使用插件:
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DesignerPlugin, ComponentLibPlugin, OutlineTreePlugin, RightPanelPlugin, HistoryPlugin, HotkeysPlugin } =
plugins;
function App() {
// 只使用必要的插件
const customPluginList = [
DesignerPlugin, // 必需:设计器画布
ComponentLibPlugin, // 组件库
RightPanelPlugin, // 右侧面板
HistoryPlugin, // 历史记录
];
return ;
}
```
## 插件开发
如果内置插件不能满足需求,可以开发自定义插件。详见 [插件开发文档](./plugin-develop/)。
### 插件结构
```tsx
import { CPlugin } from '@chamn/engine';
const MyPlugin: CPlugin = (ctx) => {
return {
name: 'MyPlugin',
async init(ctx) {
// 初始化逻辑
const workbench = ctx.getWorkbench();
workbench?.addLeftPanel({
name: 'MyPanel',
title: '我的面板',
icon: ,
view: ,
});
ctx.pluginReadyOk();
},
async destroy(ctx) {
// 清理逻辑
},
export: (ctx) => {
// 导出 API
return {
doSomething() {
// ...
},
};
},
meta: {
engine: {
version: '1.0.0',
},
},
};
};
```
## 常见问题
### Q: 如何禁用某个插件?
A: 从 `DEFAULT_PLUGIN_LIST` 中过滤掉不需要的插件,然后将定制后的插件列表传递给引擎。
**禁用单个插件:**
```tsx
import { Engine, plugins } from '@chamn/engine';
const { DEFAULT_PLUGIN_LIST, HotkeysPlugin } = plugins;
function App() {
// 过滤掉快捷键插件
const customPlugins = DEFAULT_PLUGIN_LIST.filter((plugin) => plugin !== HotkeysPlugin);
return ;
}
```
**禁用多个插件:**
```tsx
import { Engine, plugins } from '@chamn/engine';
const { DEFAULT_PLUGIN_LIST, HotkeysPlugin, GlobalStatePanelPlugin } = plugins;
function App() {
// 要禁用的插件列表
const pluginsToDisable = [HotkeysPlugin, GlobalStatePanelPlugin];
// 过滤掉要禁用的插件
const customPlugins = DEFAULT_PLUGIN_LIST.filter((plugin) => !pluginsToDisable.includes(plugin));
return ;
}
```
### Q: 插件加载顺序重要吗?
A: 是的。建议按以下顺序加载:
1. DesignerPlugin(必须最先)
2. 其他功能插件
3. UI 面板插件
### Q: 如何获取插件实例?
A: 使用 `pluginManager.get()` 方法:
```tsx
const onReady = async (ctx: EnginContext) => {
const designerPlugin = await ctx.pluginManager.get('Designer');
const historyPlugin = await ctx.pluginManager.get('History');
};
```
### Q: 插件配置何时生效?
A: 插件配置必须在 `beforePluginRun` 中设置:
```tsx
const beforePluginRun = ({ pluginManager }) => {
pluginManager.customPlugin('Designer', (pluginInstance) => {
// 配置插件
return pluginInstance;
});
};
;
```
================================================
FILE: packages/docs-app/src/content/docs/reference/Plugin/custom-plugin-guide.mdx
================================================
---
sidebar:
order: 5
title: 替换内置插件实战指南
---
# 替换内置插件实战指南
本指南将通过实际案例,教你如何从零开发自定义插件、替换内置插件,以及实现高度定制的引擎。
## CPlugin 类型定义
在开始之前,先了解 `CPlugin` 的类型定义:
```typescript
// 插件对象定义
export type PluginObj = {
name: string; // 插件唯一名称(必需)
PLUGIN_NAME?: string; // 插件静态名称(可选,用于类型安全)
init: (ctx: CPluginCtx) => Promise; // 初始化方法(必需)
destroy: (ctx: CPluginCtx) => Promise; // 销毁方法(必需)
reload?: (ctx: CPluginCtx) => Promise; // 重载方法(可选)
export: (ctx: CPluginCtx) => E; // 导出 API(必需)
meta: {
// 元数据(必需)
engine: {
version: string;
};
};
};
// 插件函数定义
interface PluginFunction {
(ctx: CPluginCtx): PluginObj;
['PLUGIN_NAME']?: string;
}
// CPlugin 可以是对象或函数
export type CPlugin, E = any> =
| PluginObj // 直接是插件对象
| PluginFunction; // 或者是返回插件对象的函数
// 插件上下文定义
export type CPluginCtx = {
name?: string; // 插件名称
globalEmitter: Emitter; // 全局事件发射器
emitter: Emitter; // 插件私有事件发射器
config: C; // 插件配置对象
pluginManager: PluginManager; // 插件管理器
pluginReadyOk: () => void; // 通知插件已准备好
getWorkbench: () => Workbench; // 获取工作台实例
pageModel: CPage; // 页面模型
i18n: CustomI18n; // 国际化对象
assetsPackageListManager: AssetsPackageListManager; // 资源包管理器
engine: Engine; // 引擎实例
};
```
### 两种插件定义方式
**方式 1:函数式定义(推荐)**
```typescript
export const MyPlugin: CPlugin = (ctx) => {
// 可以访问 ctx 并创建闭包变量
let localState = {};
return {
name: 'MyPlugin',
async init(ctx) {
// 初始化逻辑
ctx.pluginReadyOk();
},
async destroy(ctx) {
// 清理逻辑
},
export: (ctx) => ({
// 导出的 API
}),
meta: {
engine: { version: '1.0.0' },
},
};
};
```
**方式 2:对象式定义**
```typescript
export const MyPlugin: CPlugin = {
name: 'MyPlugin',
async init(ctx) {
ctx.pluginReadyOk();
},
async destroy(ctx) {},
export: (ctx) => ({}),
meta: {
engine: { version: '1.0.0' },
},
};
```
## 开发自定义插件
### 场景 1:开发一个自定义工具栏插件
假设我们要开发一个自定义工具栏插件,提供快速操作功能。
#### 第一步:定义插件结构
```typescript
// plugins/CustomToolbar/index.tsx
import { CPlugin } from '@chamn/engine';
import React from 'react';
const PLUGIN_NAME = 'CustomToolbar' as const;
export const CustomToolbarPlugin: CPlugin = (ctx) => {
return {
name: PLUGIN_NAME,
async init(ctx) {
const workbench = ctx.getWorkbench();
// 添加顶部工具栏
workbench.addTopBarView({
name: PLUGIN_NAME,
view: ,
});
// 通知插件已准备好
ctx.pluginReadyOk();
},
async destroy(ctx) {
console.log('CustomToolbar 插件已卸载');
},
export: (ctx) => ({
// 暴露给外部的 API
refresh() {
console.log('刷新工具栏');
},
addButton(config: any) {
console.log('添加自定义按钮', config);
},
}),
meta: {
engine: {
version: '1.0.0',
},
},
};
};
CustomToolbarPlugin.PLUGIN_NAME = PLUGIN_NAME;
```
#### 第二步:实现工具栏 UI
```typescript
// plugins/CustomToolbar/CustomToolbarView.tsx
import React, { useState } from 'react';
import { CPluginCtx } from '@chamn/engine';
interface CustomToolbarViewProps {
ctx: CPluginCtx;
}
export const CustomToolbarView: React.FC = ({ ctx }) => {
const [selectedNode, setSelectedNode] = useState(null);
// 监听节点选中事件
React.useEffect(() => {
const handleSelect = (node: any) => {
setSelectedNode(node);
};
ctx.emitter.on('onSelectNode', handleSelect);
return () => {
ctx.emitter.off('onSelectNode', handleSelect);
};
}, [ctx]);
const handleCopy = () => {
if (selectedNode) {
const nodeData = ctx.pageModel.getNode(selectedNode.id);
console.log('复制节点', nodeData);
// 实现复制逻辑
}
};
const handleDelete = () => {
if (selectedNode) {
ctx.pageModel.deleteNode(selectedNode.id);
ctx.emitter.emit('onDeleteNode', selectedNode);
}
};
return (
复制节点
删除节点
ctx.pageModel.clear()}>清空画布
);
};
```
#### 第三步:使用插件
```tsx
import { Engine, plugins } from '@chamn/engine';
import { CustomToolbarPlugin } from './plugins/CustomToolbar';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const onReady = async (ctx) => {
// 获取自定义工具栏插件
const toolbar = await ctx.pluginManager.get('CustomToolbar');
// 调用插件 API
toolbar?.export.addButton({
label: '自定义按钮',
onClick: () => console.log('点击了自定义按钮'),
});
};
return (
);
}
```
### 场景 2:开发带配置的插件
开发一个可配置的代码生成器插件。
```typescript
// plugins/CodeGenerator/index.tsx
import { CPlugin } from '@chamn/engine';
// 定义插件配置类型
interface CodeGeneratorConfig {
framework: 'react' | 'vue' | 'angular';
typescript: boolean;
outputPath?: string;
}
const PLUGIN_NAME = 'CodeGenerator' as const;
export const CodeGeneratorPlugin: CPlugin = (ctx) => {
// 从上下文获取配置
const config = ctx.config || {
framework: 'react',
typescript: true,
};
return {
name: PLUGIN_NAME,
async init(ctx) {
const workbench = ctx.getWorkbench();
// 添加右侧面板
workbench.addRightPanel({
name: PLUGIN_NAME,
title: '代码生成',
view: ,
});
ctx.pluginReadyOk();
},
async destroy(ctx) {
console.log('CodeGenerator 插件已卸载');
},
export: (ctx) => ({
generate() {
const schema = ctx.pageModel.export();
return generateCode(schema, config);
},
setConfig(newConfig: Partial) {
Object.assign(config, newConfig);
},
}),
meta: {
engine: {
version: '1.0.0',
},
},
};
};
function generateCode(schema: any, config: CodeGeneratorConfig) {
// 根据配置生成代码
if (config.framework === 'react') {
return generateReactCode(schema, config.typescript);
}
// ... 其他框架
}
CodeGeneratorPlugin.PLUGIN_NAME = PLUGIN_NAME;
```
#### 使用带配置的插件
```tsx
import { Engine, plugins } from '@chamn/engine';
import { CodeGeneratorPlugin } from './plugins/CodeGenerator';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const beforePluginRun = ({ pluginManager }) => {
// 在插件初始化前配置插件
pluginManager.customPlugin('CodeGenerator', (pluginInstance) => {
pluginInstance.ctx.config = {
framework: 'react',
typescript: true,
outputPath: './src/generated',
};
return pluginInstance;
});
};
return (
);
}
```
## 替换内置插件
Chameleon Engine 的所有内置插件都可以被替换。替换的核心思路是:**不加载要替换的内置插件,而是加载自己的插件**。
### 内置插件列表
```typescript
import { plugins } from '@chamn/engine';
// 获取默认插件列表
const { DEFAULT_PLUGIN_LIST } = plugins;
// 单独导入内置插件
const {
DesignerPlugin, // 设计器插件
ComponentLibPlugin, // 组件库插件
RightPanelPlugin, // 右侧属性面板插件
OutlineTreePlugin, // 大纲树插件
GlobalStatePanelPlugin, // 全局状态面板插件
HistoryPlugin, // 历史记录插件
HotkeysPlugin, // 快捷键插件
} = plugins;
// 获取插件名称常量
const { DEFAULT_PLUGIN_NAME_MAP } = plugins;
// {
// DesignerPlugin: 'Designer',
// ComponentLibPlugin: 'ComponentLib',
// RightPanelPlugin: 'RightPanel',
// ...
// }
```
### 场景 1:替换属性面板插件
假设内置的 `RightPanel` 不满足需求,我们要完全替换它。
#### 第一步:开发自定义属性面板
```typescript
// plugins/CustomRightPanel/index.tsx
import { CPlugin } from '@chamn/engine';
import React from 'react';
const PLUGIN_NAME = 'CustomRightPanel' as const;
export const CustomRightPanelPlugin: CPlugin = (ctx) => {
return {
name: PLUGIN_NAME,
async init(ctx) {
const workbench = ctx.getWorkbench();
// 添加完全自定义的右侧面板
workbench.replaceRightPanel({
name: PLUGIN_NAME,
view: ,
});
ctx.pluginReadyOk();
},
async destroy(ctx) {
console.log('CustomRightPanel 插件已卸载');
},
export: (ctx) => ({
// 暴露与内置插件兼容的 API
updateProperty(key: string, value: any) {
const selectedNode = ctx.pageModel.getSelectedNode();
if (selectedNode) {
ctx.pageModel.updateNode(selectedNode.id, {
props: {
...selectedNode.props,
[key]: value,
},
});
}
},
}),
meta: {
engine: {
version: '1.0.0',
},
},
};
};
CustomRightPanelPlugin.PLUGIN_NAME = PLUGIN_NAME;
```
#### 第二步:实现自定义属性编辑器
```tsx
// plugins/CustomRightPanel/CustomPropertyEditor.tsx
import React, { useState, useEffect } from 'react';
import { CPluginCtx } from '@chamn/engine';
interface CustomPropertyEditorProps {
ctx: CPluginCtx;
}
export const CustomPropertyEditor: React.FC = ({ ctx }) => {
const [selectedNode, setSelectedNode] = useState(null);
const [properties, setProperties] = useState>({});
useEffect(() => {
const handleSelect = (node: any) => {
setSelectedNode(node);
if (node) {
const nodeData = ctx.pageModel.getNode(node.id);
setProperties(nodeData?.props || {});
}
};
ctx.emitter.on('onSelectNode', handleSelect);
return () => {
ctx.emitter.off('onSelectNode', handleSelect);
};
}, [ctx]);
const handlePropertyChange = (key: string, value: any) => {
if (selectedNode) {
ctx.pageModel.updateNode(selectedNode.id, {
props: {
...properties,
[key]: value,
},
});
setProperties((prev) => ({ ...prev, [key]: value }));
}
};
if (!selectedNode) {
return 请选择一个节点
;
}
return (
自定义属性面板
{Object.entries(properties).map(([key, value]) => (
{key}:
handlePropertyChange(key, e.target.value)}
style={{ width: '100%', marginTop: '4px' }}
/>
))}
);
};
```
#### 第三步:替换内置插件
**核心思路:从 `DEFAULT_PLUGIN_LIST` 中移除要替换的插件,然后添加自定义插件。**
```tsx
import { Engine, plugins } from '@chamn/engine';
import { CustomRightPanelPlugin } from './plugins/CustomRightPanel';
const { DEFAULT_PLUGIN_LIST, RightPanelPlugin } = plugins;
function App() {
// 1. 复制默认插件列表
const customPlugins = [...DEFAULT_PLUGIN_LIST];
// 2. 找到要替换的插件的索引
const rightPanelIndex = customPlugins.findIndex((plugin) => plugin === RightPanelPlugin);
// 3. 如果找到了,移除内置插件
if (rightPanelIndex !== -1) {
customPlugins.splice(rightPanelIndex, 1);
}
// 4. 添加自定义插件
customPlugins.push(CustomRightPanelPlugin);
return ;
}
```
**简化写法(推荐)**
```tsx
import { Engine, plugins } from '@chamn/engine';
import { CustomRightPanelPlugin } from './plugins/CustomRightPanel';
const { DEFAULT_PLUGIN_LIST, RightPanelPlugin } = plugins;
function App() {
// 过滤掉要替换的插件,然后添加自定义插件
const customPlugins = [
...DEFAULT_PLUGIN_LIST.filter((plugin) => plugin !== RightPanelPlugin),
CustomRightPanelPlugin,
];
return ;
}
```
**批量替换多个插件**
```tsx
import { Engine, plugins } from '@chamn/engine';
import { CustomRightPanelPlugin } from './plugins/CustomRightPanel';
import { CustomComponentLibPlugin } from './plugins/CustomComponentLib';
const { DEFAULT_PLUGIN_LIST, RightPanelPlugin, ComponentLibPlugin } = plugins;
function App() {
// 要替换的插件列表
const pluginsToReplace = [RightPanelPlugin, ComponentLibPlugin];
// 过滤掉要替换的插件
const customPlugins = [
...DEFAULT_PLUGIN_LIST.filter((plugin) => !pluginsToReplace.includes(plugin)),
// 添加自定义插件
CustomRightPanelPlugin,
CustomComponentLibPlugin,
];
return ;
}
```
### 场景 2:禁用某个插件
如果不需要某个内置插件,可以从 `DEFAULT_PLUGIN_LIST` 中过滤掉,然后将定制后的插件列表传递给引擎。
**禁用单个插件**
```tsx
import { Engine, plugins } from '@chamn/engine';
const { DEFAULT_PLUGIN_LIST, GlobalStatePanelPlugin } = plugins;
function App() {
// 过滤掉不需要的插件
const customPlugins = DEFAULT_PLUGIN_LIST.filter((plugin) => plugin !== GlobalStatePanelPlugin);
return ;
}
```
**禁用多个插件**
```tsx
import { Engine, plugins } from '@chamn/engine';
const { DEFAULT_PLUGIN_LIST, GlobalStatePanelPlugin, OutlineTreePlugin, HotkeysPlugin } = plugins;
function App() {
// 要禁用的插件列表
const pluginsToDisable = [GlobalStatePanelPlugin, OutlineTreePlugin, HotkeysPlugin];
// 过滤掉要禁用的插件
const customPlugins = DEFAULT_PLUGIN_LIST.filter((plugin) => !pluginsToDisable.includes(plugin));
return ;
}
```
**使用插件名称禁用**
```tsx
import { Engine, plugins } from '@chamn/engine';
const { DEFAULT_PLUGIN_LIST, DEFAULT_PLUGIN_NAME_MAP } = plugins;
function App() {
// 要禁用的插件名称列表
const pluginNamesToDisable = [
DEFAULT_PLUGIN_NAME_MAP.GlobalStatePanelPlugin,
DEFAULT_PLUGIN_NAME_MAP.OutlineTreePlugin,
];
// 通过插件名称过滤
const customPlugins = DEFAULT_PLUGIN_LIST.filter((plugin) => !pluginNamesToDisable.includes(plugin.PLUGIN_NAME));
return ;
}
```
### 场景 3:使用 customPlugin 定制内置插件
如果只需要修改内置插件的部分行为,可以使用 `customPlugin` 而不是完全替换。**重要:`customPlugin` 必须在插件初始化前调用,因此要在 `beforePluginRun` 回调中使用。**
```tsx
import { Engine, plugins } from '@chamn/engine';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const beforePluginRun = ({ pluginManager }) => {
// 在插件初始化前定制它
pluginManager.customPlugin('History', (pluginInstance) => {
// 修改插件配置
pluginInstance.ctx.config = {
...pluginInstance.ctx.config,
maxHistoryLength: 100, // 自定义历史记录长度
enableRedo: true,
};
// 修改插件导出的 API
const originalExport = pluginInstance.export;
pluginInstance.export = {
...originalExport,
// 添加自定义方法
clearHistory() {
console.log('清空历史记录');
originalExport.clear();
},
};
return pluginInstance;
});
};
return (
);
}
```
## 实现高度定制引擎
通过组合自定义插件和内置插件,可以实现完全定制的引擎。
### 场景 1:构建轻量级表单设计器
只保留必要的插件,移除不需要的功能。
```tsx
// FormDesignerEngine.tsx
import React from 'react';
import { Engine, plugins } from '@chamn/engine';
import { CustomFormToolbarPlugin } from './plugins/CustomFormToolbar';
import { FormFieldLibraryPlugin } from './plugins/FormFieldLibrary';
import { FormPropertyEditorPlugin } from './plugins/FormPropertyEditor';
const {
DesignerPlugin, // 保留设计器
HistoryPlugin, // 保留历史记录
HotkeysPlugin, // 保留快捷键
OutlineTreePlugin, // 保留大纲树
// 移除其他不需要的插件
} = plugins;
interface FormDesignerEngineProps {
schema?: any;
materials?: any;
onSave?: (schema: any) => void;
}
export const FormDesignerEngine: React.FC = ({ schema, materials, onSave }) => {
const customPlugins = [
// 内置核心插件
DesignerPlugin,
HistoryPlugin,
HotkeysPlugin,
OutlineTreePlugin,
// 自定义表单插件
CustomFormToolbarPlugin,
FormFieldLibraryPlugin,
FormPropertyEditorPlugin,
];
const beforePluginRun = ({ pluginManager }) => {
// 定制历史记录插件
pluginManager.customPlugin('History', (pluginInstance) => {
pluginInstance.ctx.config = {
maxHistoryLength: 50,
};
return pluginInstance;
});
};
const onReady = async (ctx) => {
// 隐藏不需要的工作台组件
const workbench = ctx.getWorkbench();
workbench.hiddenWidget(['TopBar']);
// 获取自定义工具栏并配置保存功能
const toolbar = await ctx.pluginManager.get('CustomFormToolbar');
toolbar?.export.onSave(() => {
const currentSchema = ctx.pageModel.export();
onSave?.(currentSchema);
});
};
return (
);
};
// 使用表单设计器
function App() {
const handleSave = (schema: any) => {
console.log('保存表单配置', schema);
// 发送到服务器
};
return ;
}
```
### 场景 2:构建移动端页面编辑器
针对移动端场景定制引擎。
```tsx
// MobilePageEditor.tsx
import React from 'react';
import { Engine, plugins } from '@chamn/engine';
import { MobilePreviewPlugin } from './plugins/MobilePreview';
import { MobileComponentLibPlugin } from './plugins/MobileComponentLib';
import { ResponsiveToolbarPlugin } from './plugins/ResponsiveToolbar';
const { DesignerPlugin, HistoryPlugin, HotkeysPlugin } = plugins;
interface MobilePageEditorProps {
schema?: any;
materials?: any;
deviceType?: 'ios' | 'android';
}
export const MobilePageEditor: React.FC = ({ schema, materials, deviceType = 'ios' }) => {
const customPlugins = [
DesignerPlugin,
HistoryPlugin,
HotkeysPlugin,
MobilePreviewPlugin,
MobileComponentLibPlugin,
ResponsiveToolbarPlugin,
];
const beforePluginRun = ({ pluginManager }) => {
// 配置移动端预览插件
pluginManager.customPlugin('MobilePreview', (pluginInstance) => {
pluginInstance.ctx.config = {
deviceType,
screenWidth: 375,
showDeviceFrame: true,
};
return pluginInstance;
});
};
const onReady = async (ctx) => {
const workbench = ctx.getWorkbench();
// 定制工作台布局
workbench.setLayout({
leftPanelWidth: 260,
rightPanelWidth: 320,
showTopBar: false,
});
// 配置移动端特定的快捷键
const hotkeys = await ctx.pluginManager.get('Hotkeys');
hotkeys?.export.register({
'cmd+p': () => {
// 切换预览模式
const preview = await ctx.pluginManager.get('MobilePreview');
preview?.export.togglePreview();
},
'cmd+d': () => {
// 切换设备类型
const preview = await ctx.pluginManager.get('MobilePreview');
preview?.export.switchDevice();
},
});
};
return (
);
};
```
### 场景 3:企业级定制引擎
集成权限控制、团队协作、版本管理等企业功能。
```tsx
// EnterpriseEngine.tsx
import React from 'react';
import { Engine, plugins } from '@chamn/engine';
import { AuthPlugin } from './plugins/Auth';
import { CollaborationPlugin } from './plugins/Collaboration';
import { VersionControlPlugin } from './plugins/VersionControl';
import { AuditLogPlugin } from './plugins/AuditLog';
import { TemplateMarketPlugin } from './plugins/TemplateMarket';
const { DEFAULT_PLUGIN_LIST } = plugins;
interface EnterpriseEngineProps {
user: {
id: string;
name: string;
role: 'admin' | 'editor' | 'viewer';
};
projectId: string;
schema?: any;
materials?: any;
}
export const EnterpriseEngine: React.FC = ({ user, projectId, schema, materials }) => {
const enterprisePlugins = [
...DEFAULT_PLUGIN_LIST,
AuthPlugin,
CollaborationPlugin,
VersionControlPlugin,
AuditLogPlugin,
TemplateMarketPlugin,
];
const beforePluginRun = ({ pluginManager }) => {
// 配置权限插件
pluginManager.customPlugin('Auth', (pluginInstance) => {
pluginInstance.ctx.config = { user };
return pluginInstance;
});
// 配置协作插件
pluginManager.customPlugin('Collaboration', (pluginInstance) => {
pluginInstance.ctx.config = {
projectId,
wsUrl: 'wss://collaboration.example.com',
};
return pluginInstance;
});
// 配置版本控制插件
pluginManager.customPlugin('VersionControl', (pluginInstance) => {
pluginInstance.ctx.config = {
autoSave: true,
saveInterval: 30000,
};
return pluginInstance;
});
};
const onReady = async (ctx) => {
// 初始化权限控制
const auth = await ctx.pluginManager.get('Auth');
const permissions = auth?.export.getPermissions();
// 根据权限控制功能
if (permissions?.role === 'viewer') {
// 只读模式
const designer = await ctx.pluginManager.get('Designer');
designer?.export.setReadonly(true);
const workbench = ctx.getWorkbench();
workbench.hiddenWidget(['LeftPanel', 'RightPanel']);
}
// 启动协作
const collab = await ctx.pluginManager.get('Collaboration');
collab?.export.connect();
// 监听协作事件
ctx.emitter.on('collaboration:userJoin', (user: any) => {
console.log(`${user.name} 加入了协作`);
});
ctx.emitter.on('collaboration:change', (change: any) => {
console.log('收到远程修改', change);
});
// 启动自动保存
const versionControl = await ctx.pluginManager.get('VersionControl');
versionControl?.export.startAutoSave();
// 记录审计日志
const auditLog = await ctx.pluginManager.get('AuditLog');
auditLog?.export.log('PROJECT_OPENED', {
projectId,
user: user.name,
});
};
return (
);
};
// 使用企业版引擎
function App() {
const currentUser = {
id: '123',
name: 'John Doe',
role: 'editor' as const,
};
return (
);
}
```
## 插件开发最佳实践
### 1. 插件命名规范
```typescript
// ✅ 推荐:使用描述性名称和常量
const PLUGIN_NAME = 'CustomFormBuilder' as const;
export const CustomFormBuilderPlugin: CPlugin = (ctx) => {
return {
name: PLUGIN_NAME,
// ...
};
};
CustomFormBuilderPlugin.PLUGIN_NAME = PLUGIN_NAME;
// ❌ 不推荐:使用字符串字面量
export const MyPlugin: CPlugin = (ctx) => {
return {
name: 'my-plugin', // 难以追踪和重构
// ...
};
};
```
### 2. 正确管理生命周期
```typescript
export const MyPlugin: CPlugin = (ctx) => {
let unsubscribeList: Array<() => void> = [];
return {
name: 'MyPlugin',
async init(ctx) {
// 订阅事件
const off1 = ctx.emitter.on('onSelectNode', handleSelect);
const off2 = ctx.globalEmitter.on('onPageChange', handlePageChange);
unsubscribeList.push(off1, off2);
// ✅ 重要:通知插件已准备好
ctx.pluginReadyOk();
},
async destroy(ctx) {
// ✅ 清理所有订阅
unsubscribeList.forEach((off) => off());
unsubscribeList = [];
// ✅ 清理其他资源
// 移除 DOM 节点、取消定时器等
},
export: () => ({}),
meta: {
engine: { version: '1.0.0' },
},
};
};
```
### 3. 使用 TypeScript 类型安全
```typescript
// 定义配置类型
interface MyPluginConfig {
theme: 'light' | 'dark';
locale: string;
features: {
autoSave: boolean;
collaboration: boolean;
};
}
// 定义导出 API 类型
interface MyPluginExport {
setTheme(theme: 'light' | 'dark'): void;
save(): Promise;
load(id: string): Promise;
}
// 使用类型
export const MyPlugin: CPlugin = (ctx) => {
const config = ctx.config || {
theme: 'light',
locale: 'zh-CN',
features: {
autoSave: true,
collaboration: false,
},
};
return {
name: 'MyPlugin',
async init(ctx) {
// ctx.config 现在有类型提示
console.log(config.theme);
ctx.pluginReadyOk();
},
async destroy(ctx) {},
export: (ctx): MyPluginExport => ({
setTheme(theme) {
config.theme = theme;
},
async save() {
// 实现保存逻辑
},
async load(id) {
// 实现加载逻辑
return {};
},
}),
meta: {
engine: { version: '1.0.0' },
},
};
};
```
### 4. 插件间通信
```typescript
// PluginA:发送事件
export const PluginA: CPlugin = (ctx) => {
return {
name: 'PluginA',
async init(ctx) {
// 使用全局事件总线通信
ctx.globalEmitter.emit('pluginA:dataReady', {
data: 'some data',
});
ctx.pluginReadyOk();
},
async destroy(ctx) {},
export: (ctx) => ({
notifyChange(data: any) {
ctx.globalEmitter.emit('pluginA:change', data);
},
}),
meta: {
engine: { version: '1.0.0' },
},
};
};
// PluginB:接收事件
export const PluginB: CPlugin = (ctx) => {
return {
name: 'PluginB',
async init(ctx) {
// 等待 PluginA 准备好
await ctx.pluginManager.onPluginReadyOk('PluginA');
// 监听 PluginA 的事件
ctx.globalEmitter.on('pluginA:change', (data) => {
console.log('收到 PluginA 的变化', data);
});
// 获取 PluginA 实例并调用其 API
const pluginA = await ctx.pluginManager.get('PluginA');
pluginA?.export.notifyChange({ from: 'PluginB' });
ctx.pluginReadyOk();
},
async destroy(ctx) {},
export: () => ({}),
meta: {
engine: { version: '1.0.0' },
},
};
};
```
### 5. 错误处理
```typescript
export const MyPlugin: CPlugin = (ctx) => {
return {
name: 'MyPlugin',
async init(ctx) {
try {
// 可能失败的初始化逻辑
await initializePlugin();
ctx.pluginReadyOk();
} catch (error) {
console.error('MyPlugin 初始化失败', error);
// 发送错误事件
ctx.globalEmitter.emit('plugin:error', {
plugin: 'MyPlugin',
error,
});
// 仍然标记为准备好,避免阻塞其他插件
ctx.pluginReadyOk();
}
},
async destroy(ctx) {
try {
await cleanupPlugin();
} catch (error) {
console.error('MyPlugin 清理失败', error);
}
},
export: (ctx) => ({
async doSomething() {
try {
// 业务逻辑
return { success: true };
} catch (error) {
console.error('操作失败', error);
return { success: false, error };
}
},
}),
meta: {
engine: { version: '1.0.0' },
},
};
};
```
## 总结
通过本指南,你已经学会了:
1. **开发自定义插件**
- 创建插件的基本结构
- 实现插件的生命周期
- 暴露插件 API
- 处理事件和状态
2. **替换内置插件**
- 完全替换内置插件
- 使用 `customPlugin` 定制内置插件
- 保持 API 兼容性
3. **实现高度定制引擎**
- 构建轻量级编辑器
- 针对特定场景定制
- 企业级功能集成
4. **最佳实践**
- 命名规范
- 生命周期管理
- 类型安全
- 插件间通信
- 错误处理
现在你可以根据自己的需求,构建完全定制化的 Chameleon Engine 了!
================================================
FILE: packages/docs-app/src/content/docs/reference/Plugin/custom-setter.mdx
================================================
---
sidebar:
order: 2
title: 自定义 setter
---
### Setter 定义
```tsx
import { ConfigProvider, Switch } from 'antd';
import { CSetter, CSetterProps } from '@chamn/engine';
type CustomSetterProps = {
a?: string;
};
export const CustomSetter: CSetter = ({
onValueChange,
setterContext,
...props
}: CSetterProps) => {
return (
{
// 将数据同步给 engine
onValueChange?.(open);
}}
/>
);
};
CustomSetter.setterName = '自定义设置器';
```
### 注册
```tsx
return (
{
pluginManager.customPlugin('RightPanel', (pluginInstance) => {
pluginInstance.ctx.config.customPropertySetterMap = {
CustomSetter: CustomSetter,
};
return pluginInstance;
});
}}
onReady={onReady}
/>
);
```
### 使用
在物料协议中即可使用该设置器:
```tsx
export const ButtonMeta: CMaterialType = {
componentName: 'Button',
title: '按钮',
props: [
{
name: 'text',
title: '文本',
valueType: 'string',
setters: ['StringSetter', 'CustomSetter'],
},
],
npm: {
package: __PACKAGE_NAME__ || '',
version: __PACKAGE_VERSION__,
destructuring: true,
exportName: 'Button',
},
snippets: [],
};
```
================================================
FILE: packages/docs-app/src/content/docs/reference/Plugin/innder-plugin-list.mdx
================================================
---
sidebar:
order: 3
title: 内置插件列表
---
# 内置插件列表
Chameleon Engine 提供了一套完整的内置插件系统。以下是所有可用的内置插件:
## 核心插件
### 默认插件列表 (DEFAULT_PLUGIN_LIST)
以下插件包含在 `DEFAULT_PLUGIN_LIST` 中,是编辑器的核心功能:
| 插件名 | 描述 | 是否必需 | 使用文档 | 源码链接 |
| :------------------- | :----------------------------- | :------- | :--------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------- |
| **Designer** | 设计器画布,负责页面渲染和编辑 | ✅ 必需 | [查看详情](../built-in-plugins-usage/#designerplugin---设计器画布) | [Github](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/plugins/Designer) |
| **OutlineTree** | 展示页面层级结构的树形视图 | 建议 | [查看详情](../built-in-plugins-usage/#outlinetreeplugin---页面结构树) | [Github](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/plugins/OutlineTree) |
| **ComponentLibrary** | 组件库面板,显示可用组件 | 建议 | [查看详情](../built-in-plugins-usage/#componentlibplugin---组件库) | [Github](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/plugins/ComponentLibrary) |
| **GlobalStatePanel** | 全局数据管理面板 | 可选 | [查看详情](../built-in-plugins-usage/#globalstatepanelplugin---全局状态管理) | [Github](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/plugins/GlobalStatePanel) |
| **RightPanel** | 右侧面板容器 | 建议 | [查看详情](../built-in-plugins-usage/#rightpanelplugin---右侧面板) | [Github](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/plugins/RightPanel) |
| **History** | 操作历史记录管理(撤销/重做) | 建议 | [查看详情](../built-in-plugins-usage/#historyplugin---历史记录) | [Github](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/plugins/History) |
| **Hotkeys** | 快捷键支持 | 建议 | [查看详情](../built-in-plugins-usage/#hotkeysplugin---快捷键) | [Github](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/plugins/Hotkeys) |
## 右侧面板子插件
以下插件在 RightPanel 中使用,提供具体的编辑功能:
| 插件名 | 描述 | 使用文档 | 源码链接 |
| :---------------------- | :--------------- | :-------------------------------------------- | :---------------------------------------------------------------------------------------------------------- |
| **PropertyPanel** | 元素属性编辑面板 | [查看详情](../built-in-plugins-usage/#子面板) | [Github](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/plugins/PropertyPanel) |
| **VisualPanelPlus** | 元素样式编辑面板 | [查看详情](../built-in-plugins-usage/#子面板) | [Github](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/plugins/VisualPanelPlus) |
| **ComponentStatePanel** | 组件状态管理面板 | [查看详情](../built-in-plugins-usage/#子面板) | [Github](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/plugins/ComponentStatePanel) |
| **AdvancePanel** | 组件高级设置面板 | [查看详情](../built-in-plugins-usage/#子面板) | [Github](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/plugins/AdvancePanel) |
| **EventPanel** | 事件绑定面板 | [查看详情](../built-in-plugins-usage/#子面板) | [Github](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/plugins/EventPanel) |
## 独立插件
以下插件可以独立使用,不包含在默认列表中:
| 插件名 | 描述 | 使用文档 | 源码链接 |
| :---------------------- | :--------------------------------- | :-------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------- |
| **DisplaySourceSchema** | 源码展示插件,查看页面 JSON Schema | [查看详情](../built-in-plugins-usage/#displaysourceschema---源码展示) | [Github](https://github.com/hlerenow/chameleon/tree/master/packages/engine/src/plugins/DisplaySourceSchema) |
## 使用方式
### 使用默认插件列表
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
return ;
}
```
### 自定义插件列表
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
const { DesignerPlugin, ComponentLibPlugin, OutlineTreePlugin, RightPanelPlugin, HistoryPlugin } = plugins;
function App() {
// 只使用必要的插件
const customPluginList = [
DesignerPlugin, // 必需
ComponentLibPlugin, // 组件库
OutlineTreePlugin, // 结构树
RightPanelPlugin, // 右侧面板
HistoryPlugin, // 历史记录
];
return ;
}
```
### 添加独立插件
```tsx
import { Engine, plugins } from '@chamn/engine';
import '@chamn/engine/dist/style.css';
import { Button } from 'antd';
import { DisplaySourceSchema } from '@chamn/engine';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const onReady = (ctx) => {
const workbench = ctx.engine.getWorkbench();
// 使用 DisplaySourceSchema 插件
workbench?.replaceTopBarView(
查看源码
);
};
return ;
}
```
## 插件依赖关系
- **DesignerPlugin** 是核心插件,其他插件都依赖它
- **RightPanelPlugin** 是右侧面板的容器,PropertyPanel、VisualPanelPlus 等子面板依赖它
- **HistoryPlugin** 依赖于页面模型的事件系统
- **HotkeysPlugin** 依赖 DesignerPlugin 和 HistoryPlugin
## 下一步
- 查看 [内置插件使用指南](../built-in-plugins-usage/) 了解每个插件的详细用法
- 查看 [插件开发文档](./plugin-develop/) 学习如何开发自定义插件
- 查看 [自定义 Setter](./custom-setter/) 了解如何自定义属性设置器
================================================
FILE: packages/docs-app/src/content/docs/reference/Plugin/plugin-develop.mdx
================================================
---
sidebar:
order: 1
title: 插件开发指南
---
# 插件开发指南
Chameleon Engine 采用插件化架构,所有功能都可以通过插件扩展。本文档将详细介绍如何开发自定义插件。
## 插件架构总览
```mermaid
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px', 'fontFamily':'Arial'}}}%%
graph TB
subgraph Engine["Chameleon Engine"]
direction TB
subgraph CoreAPI["核心 API 层"]
direction LR
CPage["CPage页面模型 "]
Workbench["Workbench工作台 "]
EventBus["Event Emitter事件总线 "]
Assets["Assets Manager资源管理 "]
I18n["I18n国际化 "]
end
subgraph PluginSystem["插件系统架构"]
direction TB
subgraph Manager["PluginManager - 插件管理器"]
direction TB
subgraph Lifecycle["生命周期管理"]
direction LR
Add["add()注册插件 "]
Init["init()初始化 "]
Ready["onPluginReadyOk()就绪通知 "]
Remove["remove()卸载插件 "]
end
subgraph Extension["扩展能力"]
direction LR
Get["get()获取插件 "]
Custom["customPlugin()定制插件 "]
Export["exportAPI 导出 "]
end
subgraph Context["插件上下文 (CPluginCtx)"]
direction LR
CtxEmitter["事件系统"]
CtxConfig["配置对象"]
CtxAPI["核心 API"]
end
end
subgraph PluginLayer["插件实例层 - 可扩展/可替换"]
direction TB
subgraph BuiltIn["内置插件"]
Designer["Designer"]
History["History"]
RightPanel["RightPanel"]
Others["..."]
end
subgraph Custom["自定义插件"]
CustomA["Custom Plugin A"]
CustomB["Custom Plugin B"]
CustomC["..."]
end
end
end
end
style Engine fill:#f8f9fa,stroke:#212529,stroke-width:3px
style CoreAPI fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style PluginSystem fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
style Manager fill:#ede7f6,stroke:#5e35b1,stroke-width:2px
style Lifecycle fill:#fff3e0,stroke:#ef6c00,stroke-width:1.5px
style Extension fill:#e0f2f1,stroke:#00796b,stroke-width:1.5px
style Context fill:#fce4ec,stroke:#c2185b,stroke-width:1.5px
style PluginLayer fill:#e8eaf6,stroke:#3f51b5,stroke-width:1.5px
style BuiltIn fill:#e8f5e9,stroke:#2e7d32,stroke-width:1px
style Custom fill:#fff9c4,stroke:#f57f17,stroke-width:1px
style CPage fill:#bbdefb,stroke:#1976d2
style Workbench fill:#bbdefb,stroke:#1976d2
style EventBus fill:#bbdefb,stroke:#1976d2
style Assets fill:#bbdefb,stroke:#1976d2
style I18n fill:#bbdefb,stroke:#1976d2
```
### 架构说明
上图展示了 Chameleon Engine 的插件系统架构,核心理念是**完全插件化**和**高度可扩展**。
#### 1. 核心 API 层
引擎提供了一组稳定的核心 API,供插件访问和操作:
- **CPage**:页面模型,管理节点树结构,提供节点增删改查能力
- **Workbench**:工作台容器,管理面板、视图的布局和交互
- **Event Emitter**:事件总线,实现模块间的松耦合通信
- **Assets Manager**:资源包管理器,统一管理组件资源
- **I18n**:国际化支持,提供多语言能力
#### 2. PluginManager - 插件管理器
PluginManager 是插件系统的核心,负责插件的全生命周期管理和扩展能力:
**生命周期管理**:
- `add(plugin)`:注册插件到系统
- `init(ctx)`:初始化插件,注入上下文
- `onPluginReadyOk(name)`:等待插件就绪,支持异步依赖
- `remove(name)`:卸载插件,清理资源
**扩展能力**:
- `get(name)`:获取插件实例,访问插件暴露的 API
- `customPlugin(name, hook)`:定制插件配置,实现插件的可替换性
- `export`:插件通过 export 暴露 API,供其他插件或外部调用
**插件上下文 (CPluginCtx)**:
- 每个插件都拥有独立的上下文对象
- 包含全局事件系统和插件私有事件系统
- 可访问所有核心 API
- 支持自定义配置对象
#### 3. 插件实例层 - 可扩展/可替换
**内置插件**:
- 提供开箱即用的核心功能
- 可被自定义插件完全替换
- 包括:Designer(设计器)、History(历史记录)、RightPanel(属性面板)等
**自定义插件**:
- 完全自由的插件开发
- 可替换任何内置插件
- 可扩展新功能
- 与内置插件享有相同的 API 访问权限
### 插件系统的核心特性
#### 可扩展性
- **无限扩展**:通过 `pluginManager.add()` 可以添加任意数量的插件
- **平等访问**:自定义插件与内置插件拥有相同的核心 API 访问权限
- **独立上下文**:每个插件拥有独立的配置和事件系统
#### 可替换性
- **插件定制**:通过 `customPlugin()` 可以在插件初始化前修改其配置
- **完全替换**:移除内置插件,添加自定义插件即可实现功能替换
- **热插拔**:支持运行时动态添加和移除插件
#### 松耦合
- **事件驱动**:插件间通过事件系统通信,避免直接依赖
- **API 导出**:插件通过 `export` 暴露标准 API,降低耦合度
- **上下文隔离**:每个插件的状态和配置相互独立
### 插件生命周期
```mermaid
%%{init: {'theme':'base', 'themeVariables': { 'fontSize':'14px'}}}%%
flowchart LR
Start([pluginManager.add]) --> CreateCtx
subgraph Registration["注册阶段"]
direction LR
CreateCtx[创建上下文 CPluginCtx] --> CustomHook[自定义钩子 customPlugin]
CustomHook --> Init[plugin.init 初始化]
Init --> Ready[pluginReadyOk 就绪通知]
end
Ready --> Running
subgraph RunningPhase["运行阶段"]
direction TB
Running[提供服务]
Running -.get.-> Export[暴露 API]
Running -.emit.-> Events[处理事件]
Running -.call.-> Interact[插件交互]
end
Running --> Remove
subgraph CleanupPhase["清理阶段"]
direction LR
Remove[pluginManager.remove] --> Destroy[plugin.destroy 清理]
Destroy --> Release[释放资源]
end
Release --> End([已卸载])
style Start fill:#c8e6c9,stroke:#388e3c,stroke-width:2px
style End fill:#ffcdd2,stroke:#c62828,stroke-width:2px
style Registration fill:#e3f2fd,stroke:#1976d2,stroke-width:2px
style RunningPhase fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
style CleanupPhase fill:#fff3e0,stroke:#ef6c00,stroke-width:2px
style CreateCtx fill:#bbdefb,stroke:#1976d2
style CustomHook fill:#bbdefb,stroke:#1976d2
style Init fill:#bbdefb,stroke:#1976d2
style Ready fill:#bbdefb,stroke:#1976d2
style Running fill:#ce93d8,stroke:#7b1fa2
style Remove fill:#ffcc80,stroke:#f57c00
style Destroy fill:#ffcc80,stroke:#f57c00
style Release fill:#ffcc80,stroke:#f57c00
```
**生命周期详解**:
1. **注册阶段**(PluginRegistration)
- `pluginManager.add(plugin)`:注册插件到系统
- 创建插件上下文(CPluginCtx):包含核心 API、事件系统、配置对象
- 执行自定义钩子(customPluginHooks):可在初始化前修改插件配置
- 调用 `plugin.init(ctx)`:执行插件初始化逻辑
- 触发 `pluginReadyOk()`:通知系统插件已准备就绪
2. **运行阶段**(Running)
- 插件可被其他插件通过 `pluginManager.get(name)` 获取
- 通过 `export` 暴露 API 供外部调用
- 监听和处理事件
- 与其他插件进行交互
3. **清理阶段**(Cleanup)
- `pluginManager.remove(name)`:触发卸载流程
- 调用 `plugin.destroy(ctx)`:执行清理逻辑
- 注销事件监听,避免内存泄漏
- 清理 UI 组件和释放资源
## 插件定义
插件可以是一个对象,也可以是一个返回插件定义的函数:
```typescript
export type PluginObj = {
/** 插件名称(必需,必须唯一) */
name: string;
/** 插件静态名称(可选,用于类型安全访问) */
PLUGIN_NAME?: string;
/** 初始化方法(必需) */
init: (ctx: CPluginCtx) => Promise;
/** 销毁方法(必需) */
destroy: (ctx: CPluginCtx) => Promise;
/** 重载方法(可选) */
reload?: (ctx: CPluginCtx) => Promise;
/** 导出 API(必需) */
export: (ctx: CPluginCtx) => E;
/** 元数据(必需) */
meta: {
engine: {
version: string;
};
};
};
export type CPlugin = PluginObj | ((ctx: CPluginCtx) => PluginObj);
```
### 插件上下文 (CPluginCtx)
插件上下文提供了访问引擎核心功能的能力:
```typescript
type CPluginCtx = {
name?: string; // 插件名称
globalEmitter: Emitter; // 全局事件发射器
emitter: Emitter; // 插件私有事件发射器
config: C; // 插件配置对象
getWorkbench: () => Workbench; // 获取工作台实例
pluginManager: PluginManager; // 插件管理器
pageModel: CPage; // 页面模型
i18n: CustomI18n; // 国际化对象
assetsPackageListManager: AssetsPackageListManager; // 资源包管理器
engine: Engine; // 引擎实例
pluginReadyOk: () => void; // 通知插件已准备好
};
```
## 快速开始
### 创建一个简单的插件
```typescript
import { CPlugin } from '@chamn/engine';
const PLUGIN_NAME = 'HelloPlugin' as const;
export const HelloPlugin: CPlugin = (ctx) => {
return {
name: PLUGIN_NAME,
PLUGIN_NAME,
async init(ctx) {
console.log('Hello Plugin 初始化!');
// 添加左侧面板
const workbench = ctx.getWorkbench();
workbench.addLeftPanel({
name: PLUGIN_NAME,
title: 'Hello',
icon: 👋 ,
view: Hello World!
,
});
// 通知插件已准备好(重要!)
ctx.pluginReadyOk();
},
async destroy(ctx) {
console.log('Hello Plugin 销毁');
},
export: (ctx) => {
return {
sayHello() {
console.log('Hello from plugin!');
},
};
},
meta: {
engine: {
version: '1.0.0',
},
},
};
};
// 添加静态属性(推荐)
HelloPlugin.PLUGIN_NAME = PLUGIN_NAME;
```
### 使用插件
```tsx
import { Engine, plugins } from '@chamn/engine';
import { HelloPlugin } from './HelloPlugin';
const { DEFAULT_PLUGIN_LIST } = plugins;
function App() {
const onReady = async (ctx) => {
// 获取插件实例
const helloPlugin = await ctx.pluginManager.get('HelloPlugin');
// 调用插件方法
helloPlugin?.export.sayHello();
};
return (
);
}
```
## 实战示例
### 示例 1: 历史记录插件
完整的历史记录插件实现,支持撤销/重做功能:
```typescript
import { waitReactUpdate } from '@/utils';
import { CPageDataType } from '@chamn/model';
import { cloneDeep, debounce } from 'lodash-es';
import { CPlugin, CPluginCtx } from '@chamn/engine';
const PLUGIN_NAME = 'History' as const;
export type HistoryExport = {
addStep: () => void;
reset: () => Promise;
preStep: () => void;
nextStep: () => void;
canGoPreStep: () => boolean;
canGoNextStep: () => boolean;
};
export const HistoryPlugin: CPlugin = (ctx) => {
const CTX: CPluginCtx | null = ctx;
const dataStore = {
historyRecords: [] as CPageDataType[],
currentStepIndex: 0,
};
let originalPageRecord: CPageDataType | null = null;
const pageSchema = ctx.pageModel.export();
originalPageRecord = pageSchema;
dataStore.historyRecords.push(pageSchema);
const loadPage = async (page: CPageDataType) => {
if (!CTX) {
return;
}
CTX.pageModel.reloadPage(page);
await waitReactUpdate();
};
const resObj = {
addStep: () => {
const { currentStepIndex, historyRecords } = dataStore;
const newPage = ctx.pageModel.export();
if (currentStepIndex !== historyRecords.length - 1) {
dataStore.historyRecords = historyRecords.slice(0, currentStepIndex + 1);
}
dataStore.historyRecords.push(newPage);
dataStore.currentStepIndex = historyRecords.length - 1;
},
reset: async () => {
const ctx = CTX;
if (!ctx) {
console.warn('plugin ctx is null, pls check it');
return;
}
if (!originalPageRecord) {
return;
}
dataStore.historyRecords = [];
loadPage(originalPageRecord);
},
preStep: () => {
const { currentStepIndex, historyRecords } = dataStore;
if (!resObj.canGoPreStep()) {
return;
}
const newIndex = currentStepIndex - 1;
dataStore.currentStepIndex = newIndex;
const page = cloneDeep(historyRecords[newIndex]);
loadPage(page);
},
nextStep: () => {
if (!resObj.canGoNextStep()) {
return;
}
const { currentStepIndex, historyRecords } = dataStore;
const newIndex = currentStepIndex + 1;
dataStore.currentStepIndex = newIndex;
const page = cloneDeep(historyRecords[newIndex]);
return loadPage(page);
},
canGoPreStep: () => {
const { currentStepIndex } = dataStore;
if (currentStepIndex <= 0) {
return false;
}
return true;
},
canGoNextStep: () => {
const { currentStepIndex, historyRecords } = dataStore;
if (currentStepIndex >= historyRecords.length - 1) {
return false;
}
return true;
},
};
// 防抖处理,避免频繁记录
const debounceAddStep = debounce(() => {
resObj.addStep();
}, 500);
return {
name: PLUGIN_NAME,
PLUGIN_NAME,
async init(ctx) {
// 监听节点变化
ctx.pageModel.emitter.on('onNodeChange', () => {
debounceAddStep();
});
// 监听页面变化
ctx.pageModel.emitter.on('onPageChange', () => {
resObj.addStep();
});
// !!! 必须调用,通知 engine,插件初始化完成,可以被消费
ctx.pluginReadyOk();
},
async destroy(ctx) {
console.log('destroy', ctx);
},
// 提供给其他插件或者外部使用的方法
export: () => {
return resObj;
},
// 插件元信息,引擎的最低版本要求
meta: {
engine: {
version: '1.0.0',
},
},
};
};
HistoryPlugin.PLUGIN_NAME = PLUGIN_NAME;
```
### 示例 2: 自定义面板插件
```typescript
import React, { useState } from 'react';
import { CPlugin } from '@chamn/engine';
import { Button, Input } from 'antd';
const PLUGIN_NAME = 'CustomPanel' as const;
// 面板视图组件
const CustomPanelView: React.FC<{ pluginCtx: any }> = ({ pluginCtx }) => {
const [text, setText] = useState('');
return (
自定义面板
setText(e.target.value)} />
{
console.log('输入的文本:', text);
}}
>
提交
);
};
export const CustomPanelPlugin: CPlugin = (ctx) => {
return {
name: PLUGIN_NAME,
PLUGIN_NAME,
async init(ctx) {
const workbench = ctx.getWorkbench();
// 添加左侧面板
workbench.addLeftPanel({
name: PLUGIN_NAME,
title: '自定义面板',
icon: 📝 ,
view: ,
});
ctx.pluginReadyOk();
},
async destroy(ctx) {
console.log('清理插件');
},
export: (ctx) => ({}),
meta: {
engine: { version: '1.0.0' },
},
};
};
CustomPanelPlugin.PLUGIN_NAME = PLUGIN_NAME;
```
### 示例 3: 自定义工具栏插件
```typescript
import React from 'react';
import { CPlugin } from '@chamn/engine';
import { Button, Space } from 'antd';
const PLUGIN_NAME = 'CustomToolbar' as const;
export const CustomToolbarPlugin: CPlugin = (ctx) => {
return {
name: PLUGIN_NAME,
PLUGIN_NAME,
async init(ctx) {
const workbench = ctx.getWorkbench();
const engine = ctx.engine;
// 自定义顶部工具栏
workbench.replaceTopBarView(
我的编辑器
engine.preview()}>预览
engine.existPreview()}>编辑
engine.refresh()}>刷新
{
const pageData = engine.pageModel.export();
localStorage.setItem('page', JSON.stringify(pageData));
}}
>
保存
);
ctx.pluginReadyOk();
},
async destroy(ctx) {},
export: (ctx) => ({}),
meta: { engine: { version: '1.0.0' } },
};
};
CustomToolbarPlugin.PLUGIN_NAME = PLUGIN_NAME;
```
## 加载插件
```tsx
import { Engine, EnginContext, plugins } from '@chamn/engine';
import { HistoryPlugin } from './HistoryPlugin';
import { CustomPanelPlugin } from './CustomPanelPlugin';
const { DEFAULT_PLUGIN_LIST } = plugins;
export const App = () => {
const onReady = useCallback(async (ctx: EnginContext) => {
// 等待插件准备完成
const designer = await ctx.pluginManager.onPluginReadyOk('Designer');
const history = await ctx.pluginManager.onPluginReadyOk('History');
// 获取工作台
const workbench = ctx.engine.getWorkbench();
// 使用插件导出的方法
history?.export.preStep();
}, []);
return (
);
};
```
## 插件配置
通过 `beforePluginRun` 自定义插件配置:
```tsx
import { Engine, plugins } from '@chamn/engine';
const beforePluginRun = ({ pluginManager }) => {
// 自定义插件配置
pluginManager.customPlugin('MyPlugin', (pluginInstance) => {
// 修改插件配置
pluginInstance.ctx.config.customOption = 'value';
return pluginInstance;
});
};
;
```
## 最佳实践
### 1. 使用 TypeScript
```typescript
import { CPlugin, CPluginCtx } from '@chamn/engine';
// 定义配置类型
type MyPluginConfig = {
enabled: boolean;
option1: string;
};
// 定义导出类型
type MyPluginExport = {
doSomething: () => void;
};
// 使用泛型
export const MyPlugin: CPlugin = (ctx) => {
return {
name: 'MyPlugin',
async init(ctx) {
// 类型安全的配置访问
const enabled = ctx.config.enabled;
ctx.pluginReadyOk();
},
async destroy(ctx) {},
export: (ctx) => {
return {
doSomething() {
console.log('Do something');
},
};
},
meta: { engine: { version: '1.0.0' } },
};
};
```
### 2. 命名规范
- 插件命名统一采用 `PascalCase`,如 `MyPlugin`
- 插件名称常量使用 `PLUGIN_NAME`
- 导出的插件对象添加静态属性 `PLUGIN_NAME`
### 3. 错误处理
```typescript
export const SafePlugin: CPlugin = (ctx) => {
return {
name: 'SafePlugin',
async init(ctx) {
try {
// 可能出错的操作
const data = await fetchData();
// 验证依赖
const workbench = ctx.getWorkbench();
if (!workbench) {
throw new Error('工作台未初始化');
}
ctx.pluginReadyOk();
} catch (error) {
console.error('插件初始化失败:', error);
ctx.pluginReadyOk(); // 即使失败也通知
}
},
async destroy(ctx) {},
export: (ctx) => ({}),
meta: { engine: { version: '1.0.0' } },
};
};
```
### 4. 内存管理
```typescript
export const MemoryAwarePlugin: CPlugin = (ctx) => {
const disposables: (() => void)[] = [];
return {
name: 'MemoryAwarePlugin',
async init(ctx) {
// 监听事件
const handler = () => {
/* ... */
};
ctx.globalEmitter.on('event', handler);
// 记录清理函数
disposables.push(() => {
ctx.globalEmitter.off('event', handler);
});
ctx.pluginReadyOk();
},
async destroy(ctx) {
// 执行所有清理函数
disposables.forEach((dispose) => dispose());
disposables.length = 0;
},
export: (ctx) => ({}),
meta: { engine: { version: '1.0.0' } },
};
};
```
## 常见问题
### Q: 插件何时初始化?
A: 插件在 Engine 组件 `componentDidMount` 时初始化,按照插件列表的顺序依次执行 `init` 方法。
### Q: 如何确保插件初始化完成?
A: 使用 `ctx.pluginReadyOk()` 通知插件管理器插件已准备好。其他插件可以通过 `ctx.pluginManager.onPluginReadyOk('PluginName')` 等待。
### Q: 插件可以访问其他插件吗?
A: 可以。使用 `ctx.pluginManager.get('PluginName')` 获取其他插件实例。
## 参考资源
- [内置插件列表](../innder-plugin-list/) - 查看所有内置插件
- [内置插件使用指南](../built-in-plugins-usage/) - 学习内置插件的用法
- [Engine API](../../engine/api/) - 了解 Engine 的 API
- [示例项目](https://github.com/ByteCrazy/chameleon-demo) - 查看完整示例
================================================
FILE: packages/docs-app/src/env.d.ts
================================================
/* eslint-disable @typescript-eslint/triple-slash-reference */
///
///
================================================
FILE: packages/docs-app/tsconfig.json
================================================
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}
================================================
FILE: packages/engine/.gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
stats.html
*storybook.log
================================================
FILE: packages/engine/.npmignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
example
node_modules
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
stats.html
================================================
FILE: packages/engine/.storybook/main.js
================================================
import { join, dirname } from 'path';
import { mergeConfig } from 'vite';
import commonConfig from '../build.common.config';
/**
* This function is used to resolve the absolute path of a package.
* It is needed in projects that use Yarn PnP or are set up within a monorepo.
*/
function getAbsolutePath(value) {
return dirname(require.resolve(join(value, 'package.json')));
}
/** @type { import('@storybook/react-vite').StorybookConfig } */
const config = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: [
getAbsolutePath('@storybook/addon-onboarding'),
getAbsolutePath('@storybook/addon-essentials'),
getAbsolutePath('@chromatic-com/storybook'),
getAbsolutePath('@storybook/addon-interactions'),
],
framework: {
name: getAbsolutePath('@storybook/react-vite'),
options: {},
},
viteFinal: (config) => {
const newConfig = mergeConfig(config, commonConfig);
return newConfig;
},
};
export default config;
================================================
FILE: packages/engine/.storybook/preview.js
================================================
/** @type { import('@storybook/react').Preview } */
const preview = {
parameters: {
controls: {
matchers: {
color: /(background|color)$/i,
date: /Date$/i,
},
},
},
};
export default preview;
================================================
FILE: packages/engine/CHANGELOG.md
================================================
# Change Log
All notable changes to this project will be documented in this file.
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
## [0.10.4](https://github.com/ByteCrazy/chameleon/compare/v0.10.3...v0.10.4) (2026-01-17)
### 🐛 Bug Fixes | Bug 修复
* **engine, engine-website-app:** 🐛 fixed pkg deps ([22b0000](https://github.com/ByteCrazy/chameleon/commit/22b0000a921ed3bd63cdf3366687184d51ee9dc5))
## [0.10.3](https://github.com/ByteCrazy/chameleon/compare/v0.10.2...v0.10.3) (2026-01-17)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed engine deps ([c1addf4](https://github.com/ByteCrazy/chameleon/commit/c1addf43214d016e466019a5aabb8960881b6418))
## [0.10.2](https://github.com/ByteCrazy/chameleon/compare/v0.10.1...v0.10.2) (2026-01-17)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed string and text can not input chinese text ([3821d00](https://github.com/ByteCrazy/chameleon/commit/3821d00d5dffd7d70e995690df5358f5a596ae93))
## [0.10.1](https://github.com/ByteCrazy/chameleon/compare/v0.10.0...v0.10.1) (2026-01-11)
### ✨ Features | 新功能
* **build-script, demo-page, engine, render:** 🎸 update build script dts & optimize modal root selector ([024d5ab](https://github.com/ByteCrazy/chameleon/commit/024d5ab26d19fc5f50172c328638a551fb99c129))
## [0.10.0](https://github.com/hlerenow/chameleon/compare/v0.9.3...v0.10.0) (2025-12-25)
### ✨ Features | 新功能
* **engine:** 🎸 optimize emptyValueSetter auto trigger value update ([a2b0409](https://github.com/hlerenow/chameleon/commit/a2b04094035d421638d4657d31fd2b417906e9c6))
### 🐛 Bug Fixes | Bug 修复
* 优化构建配置,修复 ES 模块外部化问题 ([866f0ae](https://github.com/hlerenow/chameleon/commit/866f0ae39472625137d73d143625d88d873f6c00))
## [0.9.3](https://github.com/ByteCrazy/chameleon/compare/v0.9.2...v0.9.3) (2025-07-20)
### ✨ Features | 新功能
* **engine, render:** 🎸 add $EVENT_PARAMS and remove $Event $PARAMS_RUNTIME ([b8e58c1](https://github.com/ByteCrazy/chameleon/commit/b8e58c1e5e98d2e4b3d31e2ba52a26fda256eb47))
## [0.9.2](https://github.com/ByteCrazy/chameleon/compare/v0.9.1...v0.9.2) (2025-07-13)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed call node method setting ([27ea497](https://github.com/ByteCrazy/chameleon/commit/27ea49747ca8dd24f862304ec552e805c9057e62))
## [0.9.1](https://github.com/ByteCrazy/chameleon/compare/v0.9.0...v0.9.1) (2025-07-13)
### 🐛 Bug Fixes | Bug 修复
* **engine, render:** 🐛 fixed CustomSchemaForm title ([178cddd](https://github.com/ByteCrazy/chameleon/commit/178cddd3e8d9147822e91fcd1ce7d8e08c70d924))
## [0.9.0](https://github.com/ByteCrazy/chameleon/compare/v0.8.6...v0.9.0) (2025-07-13)
### ✨ Features | 新功能
* **engine, model:** 🎸 support EmptyValueSetter ([da1e36f](https://github.com/ByteCrazy/chameleon/commit/da1e36f9915282b3f7cb40672cf2a000bd4162e5))
## [0.8.6](https://github.com/ByteCrazy/chameleon/compare/v0.8.5...v0.8.6) (2025-06-21)
### 🐛 Bug Fixes | Bug 修复
* **demo-page, engine, render:** 🐛 fixed action flow failed node not connect correct ([e235a2b](https://github.com/ByteCrazy/chameleon/commit/e235a2be2d1e1935dfc40b56936de763efc4fb67))
## [0.8.5](https://github.com/ByteCrazy/chameleon/compare/v0.8.4...v0.8.5) (2025-04-13)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed custom seeter not register success ([19d739a](https://github.com/ByteCrazy/chameleon/commit/19d739a97000641846c02ec19555926fe88ce3b4))
## [0.8.4](https://github.com/ByteCrazy/chameleon/compare/v0.8.3...v0.8.4) (2025-04-13)
### ✨ Features | 新功能
* **engine:** 🎸 optimize action flow layout ([9c970c9](https://github.com/ByteCrazy/chameleon/commit/9c970c9482274d4d5cf6beb76912a10784e62c79))
### 🐛 Bug Fixes | Bug 修复
* **engine, render:** 🐛 CSS editor value update error ([209106a](https://github.com/ByteCrazy/chameleon/commit/209106a6a71819862626ee39c887d1835a8b5611))
* **engine, render:** 🐛 fixed event not trigger when loop ([e3a10a8](https://github.com/ByteCrazy/chameleon/commit/e3a10a8ab77a3c5321f47440c7bdc2ad52e82ee2))
## [0.8.3](https://github.com/ByteCrazy/chameleon/compare/v0.8.2...v0.8.3) (2025-04-12)
**Note:** Version bump only for package @chamn/engine
## [0.8.2](https://github.com/ByteCrazy/chameleon/compare/v0.8.1...v0.8.2) (2025-04-11)
### 🐛 Bug Fixes | Bug 修复
* **engine, render:** 🐛 fixed functionSeter and expressionSetter dts ([1e326e2](https://github.com/ByteCrazy/chameleon/commit/1e326e288f8d072497c01c1ccb0c06bed0521949))
## [0.8.1](https://github.com/ByteCrazy/chameleon/compare/v0.8.0...v0.8.1) (2025-04-10)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed globalStateDts ([03c0b71](https://github.com/ByteCrazy/chameleon/commit/03c0b714343a7cf76710b164a43008af430b1d7c))
## [0.8.0](https://github.com/ByteCrazy/chameleon/compare/v0.7.0...v0.8.0) (2025-04-10)
### ✨ Features | 新功能
* **demo-page, engine, render:** 🎸 update run time var for function ([d8fb90a](https://github.com/ByteCrazy/chameleon/commit/d8fb90ac72a233d7fdb5fcb36e0b7963f83c5acf))
* **engine, model, render:** 🎸 fixed cycle update when update value in mount ([9b30922](https://github.com/ByteCrazy/chameleon/commit/9b30922ffac4a5b4fb1fe123c4604cc0fff63911))
* **engine, render:** 🎸 optimize expression setter ([07c11e9](https://github.com/ByteCrazy/chameleon/commit/07c11e9bdf011f70763623d6a2f185a3c3830e5e))
* **engine, render:** 🎸 optimize globalState update ([4e7bb6a](https://github.com/ByteCrazy/chameleon/commit/4e7bb6ab68cd69fe6d429abe417587c3ac26eb18))
* **engine:** 🎸 dynamic generate page dts ([2dc1a0b](https://github.com/ByteCrazy/chameleon/commit/2dc1a0bca0a53223b61b8c4ce216b26a70887c96))
* **engine:** 🎸 optimize express setter ([37141f9](https://github.com/ByteCrazy/chameleon/commit/37141f97a98e53e0050e328c2de03341984b3ad5))
## [0.7.0](https://github.com/ByteCrazy/chameleon/compare/v0.6.0...v0.7.0) (2025-04-06)
### ✨ Features | 新功能
* **engine, layout:** 🎸 optimize select node interactive time ([41d105c](https://github.com/ByteCrazy/chameleon/commit/41d105c2408458b38cda0c9fac601dd89fd744aa))
* **engine:** 🎸 add css code editor ([7715f29](https://github.com/ByteCrazy/chameleon/commit/7715f29cd49d530e54941c85659ca3c671be91e3))
* **engine:** 🎸 optimize style UI panel ([0d6f239](https://github.com/ByteCrazy/chameleon/commit/0d6f2394281bf4add5437b0c6e4c681f5d64d6e3))
### 🐛 Bug Fixes | Bug 修复
* **engine, layout:** 🐛 optimize select active box ([b1a6041](https://github.com/ByteCrazy/chameleon/commit/b1a604140652f1c0871a463bc32020c39a07846c))
## [0.6.0](https://github.com/ByteCrazy/chameleon/compare/v0.5.2...v0.6.0) (2025-03-30)
### ✨ Features | 新功能
* **engine:** 🎸 support config monacoEdito cdn url ([32d08bc](https://github.com/ByteCrazy/chameleon/commit/32d08bcaec7ebe8a187d0b2eae8bd5e4b64b3dc9))
## [0.5.2](https://github.com/ByteCrazy/chameleon/compare/v0.5.1...v0.5.2) (2025-03-30)
**Note:** Version bump only for package @chamn/engine
## [0.5.1](https://github.com/ByteCrazy/chameleon/compare/v0.5.0...v0.5.1) (2025-03-30)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed search status reset ([994b36c](https://github.com/ByteCrazy/chameleon/commit/994b36ce744fd10002cf2996ae26702a959ef5b4))
## [0.5.0](https://github.com/ByteCrazy/chameleon/compare/v0.4.0...v0.5.0) (2025-03-30)
### ✨ Features | 新功能
* **engine:** 🎸 componentLib add search and customSearchBar ([4801f40](https://github.com/ByteCrazy/chameleon/commit/4801f403da1af705d5dc5eef579e7dba6560b08f))
## [0.4.0](https://github.com/ByteCrazy/chameleon/compare/v0.3.21...v0.4.0) (2025-03-29)
**Note:** Version bump only for package @chamn/engine
## [0.3.21](https://github.com/ByteCrazy/chameleon/compare/v0.3.20...v0.3.21) (2025-03-26)
### 🐛 Bug Fixes | Bug 修复
* **engine, material:** 🐛 fixed RGLEL cycle update item ([f045818](https://github.com/ByteCrazy/chameleon/commit/f045818d6d1e1b4dbcf107d727976f6d92600b94))
## [0.3.20](https://github.com/ByteCrazy/chameleon/compare/v0.3.19...v0.3.20) (2025-03-26)
**Note:** Version bump only for package @chamn/engine
## [0.3.19](https://github.com/ByteCrazy/chameleon/compare/v0.3.18...v0.3.19) (2025-03-26)
**Note:** Version bump only for package @chamn/engine
## [0.3.18](https://github.com/ByteCrazy/chameleon/compare/v0.3.17...v0.3.18) (2025-03-26)
**Note:** Version bump only for package @chamn/engine
## [0.3.17](https://github.com/ByteCrazy/chameleon/compare/v0.3.16...v0.3.17) (2025-03-25)
### 🐛 Bug Fixes | Bug 修复
* **engine, material, model, render:** 🐛 fixed node update value material is undefined ([969174d](https://github.com/ByteCrazy/chameleon/commit/969174da968aec4a1ee1fec7ca44a5e459be56bc))
## [0.3.16](https://github.com/ByteCrazy/chameleon/compare/v0.3.15...v0.3.16) (2025-03-24)
### ✨ Features | 新功能
* **engine:** 🎸 setter supprot get current nodemodel ([f59a136](https://github.com/ByteCrazy/chameleon/commit/f59a136cc134388c382827d731f683e9d9a298e5))
## [0.3.15](https://github.com/ByteCrazy/chameleon/compare/v0.3.14...v0.3.15) (2025-03-23)
**Note:** Version bump only for package @chamn/engine
## [0.3.14](https://github.com/ByteCrazy/chameleon/compare/v0.3.13...v0.3.14) (2025-03-23)
**Note:** Version bump only for package @chamn/engine
## [0.3.13](https://github.com/ByteCrazy/chameleon/compare/v0.3.12...v0.3.13) (2025-03-23)
**Note:** Version bump only for package @chamn/engine
## [0.3.12](https://github.com/ByteCrazy/chameleon/compare/v0.3.11...v0.3.12) (2025-03-23)
### 🐛 Bug Fixes | Bug 修复
* **engine, layout:** 🐛 fixed drag interactive and fixed accetpNode action ([c15d03c](https://github.com/ByteCrazy/chameleon/commit/c15d03cc0406533e5eab55e27ce5eb1223d91c72))
## [0.3.11](https://github.com/ByteCrazy/chameleon/compare/v0.3.10...v0.3.11) (2025-03-22)
**Note:** Version bump only for package @chamn/engine
## [0.3.10](https://github.com/ByteCrazy/chameleon/compare/v0.3.9...v0.3.10) (2025-03-22)
**Note:** Version bump only for package @chamn/engine
## [0.3.9](https://github.com/ByteCrazy/chameleon/compare/v0.3.8...v0.3.9) (2025-03-22)
### ✨ Features | 新功能
* **engine, layout:** 🎸 optimize drag interactive ([f512f14](https://github.com/ByteCrazy/chameleon/commit/f512f14ecf3caaa148279543999a74d35ae44701))
## [0.3.8](https://github.com/ByteCrazy/chameleon/compare/v0.3.7...v0.3.8) (2025-03-16)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed customAdvanceHook not rect on outline and hotkey ([c21bdb0](https://github.com/ByteCrazy/chameleon/commit/c21bdb0e275ebe4ae096d18b90bd406874c9de79))
## [0.3.7](https://github.com/ByteCrazy/chameleon/compare/v0.3.6...v0.3.7) (2025-03-16)
### ✨ Features | 新功能
* **demo-page, engine, model, render:** 🎸 support inject eng inner env to runtime ([baa5c11](https://github.com/ByteCrazy/chameleon/commit/baa5c11d389019a7e4e4b8e000433a99038b4ae3))
## [0.3.6](https://github.com/ByteCrazy/chameleon/compare/v0.3.5...v0.3.6) (2025-03-09)
### ✨ Features | 新功能
* **engine:** 🎸 add hiddenWidget for eng and optimize setter ([42b9846](https://github.com/ByteCrazy/chameleon/commit/42b984681a9efc7cdee7dc4287cd994fd37c592c))
## [0.3.5](https://github.com/ByteCrazy/chameleon/compare/v0.3.4...v0.3.5) (2025-03-09)
### ✨ Features | 新功能
* **engine, layout:** 🎸 support controll preview mode ([97b83e5](https://github.com/ByteCrazy/chameleon/commit/97b83e58f0f22c76da51e5a3d22db2c82a2f70d2))
* **engine:** 🎸 add workbench widget control config ([0fcab5b](https://github.com/ByteCrazy/chameleon/commit/0fcab5b5ad715762327c26d3f7eae4680604644b))
### 🐛 Bug Fixes | Bug 修复
* **demo-page, engine, render:** 🐛 fixed action flow only run first node ([c4bdb85](https://github.com/ByteCrazy/chameleon/commit/c4bdb85d0ca6ed09c6c66d15847de5bd14da556e))
* **engine, model:** 🐛 update select setter name ([054fd48](https://github.com/ByteCrazy/chameleon/commit/054fd48f49ec1d1bdb79d7bac44acedb601d6fdf))
* **engine:** 🐛 fixed last connect line not save ([2bacb15](https://github.com/ByteCrazy/chameleon/commit/2bacb1541cbc753df15a0216df9d6d6aac86cb1d))
## [0.3.4](https://github.com/ByteCrazy/chameleon/compare/v0.3.3...v0.3.4) (2025-02-16)
### ✨ Features | 新功能
* **engine, layout, render:** 🎸 action node express support $response ([9bf1570](https://github.com/ByteCrazy/chameleon/commit/9bf1570c3b80404be78a5d3ca2c401464e7645d3))
## [0.3.3](https://github.com/ByteCrazy/chameleon/compare/v0.3.2...v0.3.3) (2025-02-16)
### ✨ Features | 新功能
* **engine:** 🎸 JSON setter support reactive value ([0262067](https://github.com/ByteCrazy/chameleon/commit/0262067fc641b8dd3c812cd3cf1f114df7e33f9c))
## [0.3.2](https://github.com/ByteCrazy/chameleon/compare/v0.3.1...v0.3.2) (2025-02-16)
### ✨ Features | 新功能
* **engine:** 🎸 optimize TCustomAPIInput type ([1b33f75](https://github.com/ByteCrazy/chameleon/commit/1b33f75a8b6e324b1f7695a62dfa1ae97d115cc7))
## [0.3.1](https://github.com/ByteCrazy/chameleon/compare/v0.3.0...v0.3.1) (2025-02-16)
### ✨ Features | 新功能
* **engine, render:** 🎸 optimize CustomAPISelectInput from and event list label ([8dbf5af](https://github.com/ByteCrazy/chameleon/commit/8dbf5af08e50c60c5ff7adc5c7e644c4ca1a9c08))
## [0.3.0](https://github.com/ByteCrazy/chameleon/compare/v0.2.4...v0.3.0) (2025-02-15)
### 🐛 Bug Fixes | Bug 修复
* **engine, engine-website-app, material, model, render:** 🐛 fixed GRL hidden offsetY loop add size ([3df132b](https://github.com/ByteCrazy/chameleon/commit/3df132b6493026b42435d4868d77915b9f7316b2))
* **engine:** 🐛 fixed ActionFlowSetter cicle deps ([b9bd177](https://github.com/ByteCrazy/chameleon/commit/b9bd177e38be054a8860d19516651d9ab813e27b))
* **engine:** 🐛 fixed ActionFlowSetter cycle deps ([e69dfd7](https://github.com/ByteCrazy/chameleon/commit/e69dfd7df7129a88c54e238a05b96bb3270fbb2f))
* **engine:** 🐛 fixed ActionFlowSetter update problem ([0034848](https://github.com/ByteCrazy/chameleon/commit/0034848e31c3b30b6782723af10e7f8e0152390a))
* **engine:** 🐛 fixed outline drag excepetion after remove page ([82ff3fd](https://github.com/ByteCrazy/chameleon/commit/82ff3fd80ffce58b2b840ae219d29e61dd34a6e4))
* **engine:** 🐛 resolve hot key confict with action flow setter ([e91898c](https://github.com/ByteCrazy/chameleon/commit/e91898c8deae98805ea2df7a1e8f3bc382bd3873))
### ✨ Features | 新功能
* **demo-page, engine, model, render:** 🎸 action node use link struct ([599ece4](https://github.com/ByteCrazy/chameleon/commit/599ece4927523f7c0e330cac722c9c9e6976ea3c))
* **demo-page, engine, model, render:** 🎸 add event panel ([e8c5648](https://github.com/ByteCrazy/chameleon/commit/e8c5648017b40cbae42c576267d1e3b9d9660918))
* **demo-page, engine, model, render:** 🎸 support TActionLogicItem prop ([e1b9d1e](https://github.com/ByteCrazy/chameleon/commit/e1b9d1e150ae810750249322ddf906b62eee9969))
* **docs-app, engine, engine-website-app, layout, material, model, render:** 🎸 do ActionFlowSetter 50% ([6399466](https://github.com/ByteCrazy/chameleon/commit/6399466c7253436591e071df25828a357bb7089e))
* **engine, engine-website-app, render:** 🎸 RequestAPINode support custom select ([5b72385](https://github.com/ByteCrazy/chameleon/commit/5b72385673bb646aff3ad2c5a9d8fffa142733dd))
* **engine, model:** 🎸 add call node method node ([1a5e79c](https://github.com/ByteCrazy/chameleon/commit/1a5e79c49964c589da167b64985327a3cbb03da8))
* **engine, model:** 🎸 add run code node ([7ac6182](https://github.com/ByteCrazy/chameleon/commit/7ac61829586a19d4fcdc1765fa878d6a16008858))
* **engine, model:** 🎸 request API 70% ([cee1228](https://github.com/ByteCrazy/chameleon/commit/cee1228c2a3265320cb32579f6f7b532b6962908))
* **engine:** 🎸 add react-flow ([fafaaf9](https://github.com/ByteCrazy/chameleon/commit/fafaaf95c589c0c99ce0953f285bf53a0e423e21))
* **engine:** 🎸 JumpLinkNode 100% ([ed0707e](https://github.com/ByteCrazy/chameleon/commit/ed0707e0232bf82bedc7b0db569c908d2001e6d7))
* **engine:** 🎸 optimize ActionFlowSetter ([fa9af2f](https://github.com/ByteCrazy/chameleon/commit/fa9af2fba32f921411ed05e8d7e68293de5dde88))
* **engine:** 🎸 optimize call node method node ([0b00029](https://github.com/ByteCrazy/chameleon/commit/0b00029627d6f90381b658ccc7f125e9b6116dcf))
* **engine:** 🎸 optimize MoveableModal interactive ([2eccb94](https://github.com/ByteCrazy/chameleon/commit/2eccb942b447fa4e54bcd6f9207a2ed053e06526))
* **engine:** 🎸 optimize RequestAPINode custom ([0440695](https://github.com/ByteCrazy/chameleon/commit/044069501608b1a8b62a08600c3c2d293e1ebe54))
* **engine:** 🎸 optimize selectNodeByTree ([b6959d9](https://github.com/ByteCrazy/chameleon/commit/b6959d98714a16669b3896b677ebb78aa080d8f3))
* **engine:** 🎸 optimize SetterSwitcher code struct ([d481fe6](https://github.com/ByteCrazy/chameleon/commit/d481fe68665f4fc7b1034e8b1ef9e2bfbe517012))
* **engine:** 🎸 parseActionLogicToNodeList 30% ([f120154](https://github.com/ByteCrazy/chameleon/commit/f1201549a5537f170dbdce7efca7c85ce3add2ad))
* **engine:** 🎸 support labelAlign config ([9590f86](https://github.com/ByteCrazy/chameleon/commit/9590f861fe8efebcd81ba38df3b159a528e066b6))
* **engine:** 🎸 support render flow by schema data ([e04e76d](https://github.com/ByteCrazy/chameleon/commit/e04e76d23115c64823dc6fbf5d460f589df98b3d))
## [0.2.4](https://github.com/ByteCrazy/chameleon/compare/v0.2.3...v0.2.4) (2024-12-08)
### 🐛 Bug Fixes | Bug 修复
* **engine, material:** 🐛 fixed RGL init layout not correcnt and upgrade gridstack ([6b66b47](https://github.com/ByteCrazy/chameleon/commit/6b66b47b37e1fcd96602132bb44373a01d5de946))
## [0.2.3](https://github.com/ByteCrazy/chameleon/compare/v0.2.2...v0.2.3) (2024-12-08)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed CVideo interactive ([4b1dabf](https://github.com/ByteCrazy/chameleon/commit/4b1dabfe89efbe2a3dfbb642e77ae10e74daaf0e))
* **engine:** 🐛 fixed hotkey not work sometimes ([d029841](https://github.com/ByteCrazy/chameleon/commit/d029841679183d5cffcbb991cf64cfcfea9de34e))
## [0.2.2](https://github.com/ByteCrazy/chameleon/compare/v0.2.1...v0.2.2) (2024-12-07)
**Note:** Version bump only for package @chamn/engine
## [0.2.1](https://github.com/ByteCrazy/chameleon/compare/v0.2.0...v0.2.1) (2024-12-07)
### ✨ Features | 新功能
* **build-script, engine, engine-website-app, material, model, render:** 🎸 fixed RGL component and optimize project struct ([99f0679](https://github.com/ByteCrazy/chameleon/commit/99f0679d93a2fd696034ebed8f1abcd9d9e601d4))
## [0.2.0](https://github.com/ByteCrazy/chameleon/compare/v0.1.1...v0.2.0) (2024-12-07)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed inner meta image drag problem ([f752d9e](https://github.com/ByteCrazy/chameleon/commit/f752d9ecf611cfc3f55576d0758905c6ac322a4e))
### ✨ Features | 新功能
* **build-script, demo-page, engine, engine-website-app, layout, material, model, render:** 🎸 upgrade vite to 6.0 ([bcac2b1](https://github.com/ByteCrazy/chameleon/commit/bcac2b15b83b41a7042ca37368c1b45302ad81d5))
* **engine, layout, render:** 🎸 replace findDOMNode API ([af2531a](https://github.com/ByteCrazy/chameleon/commit/af2531a095124ac55d4f6dc6896d430f52f5da82))
## [0.1.1](https://github.com/ByteCrazy/chameleon/compare/v0.1.0...v0.1.1) (2024-11-11)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed cycle dependencies ([cefa3f0](https://github.com/ByteCrazy/chameleon/commit/cefa3f0a4a9c72c81b5337bbdb6e0429ca247252))
## [0.1.0](https://github.com/ByteCrazy/chameleon/compare/v0.0.46...v0.1.0) (2024-09-07)
### 👷 Continuous Integration | CI 配置
* **engine, engine-website-app, layout, model:** 🎡 update github ci ([259e9db](https://github.com/ByteCrazy/chameleon/commit/259e9db229576cd09c5aae1f28a2c228927011c7))
### 🐛 Bug Fixes | Bug 修复
* **demo-page, docs-app, engine, engine-website-app, layout, material:** 🐛 fixed material meta not correct ([55b755c](https://github.com/ByteCrazy/chameleon/commit/55b755ce0c833e594b46447b2d6608cf56f7a593))
* **docs-website, engine, layout:** 🐛 fixed higligh toolbar pos ([0356109](https://github.com/ByteCrazy/chameleon/commit/0356109e8a11a5a85ab90d1610ef7ef8549db0a9))
* **engine, engine-website-app, material:** 🐛 fixed ReactGridLayout edit mode lable not correct ([9801839](https://github.com/ByteCrazy/chameleon/commit/9801839d9ff6a6548b0e23f1e31850cd56e7fff0))
* **engine, layout:** 🐛 fixed toolbox width not correct ([4dc159a](https://github.com/ByteCrazy/chameleon/commit/4dc159a1931a853ba4cdb664638f27ba71b1ecf2))
* **engine:** 🐛 fixed advanceCustom hook logic ([5fb2619](https://github.com/ByteCrazy/chameleon/commit/5fb261962af141affef0e8e2cf1f0b62d16b0d45))
* **engine:** 🐛 fixed BackgroundInput color input ([944517c](https://github.com/ByteCrazy/chameleon/commit/944517c9078b29c5076784a1df66752494125e9f))
* **engine:** 🐛 fixed CSSUIPanel value not corrent ([1a4dc64](https://github.com/ByteCrazy/chameleon/commit/1a4dc645171253b261a3d2c9ebd3cf27ec692d34))
* **engine:** 🐛 fixed github build ([a4ace81](https://github.com/ByteCrazy/chameleon/commit/a4ace818d20c657a255fd25d5771113e5191d55d))
* **engine:** 🐛 fixed render url ([27736de](https://github.com/ByteCrazy/chameleon/commit/27736de41f239b4911535097c7334b73eea35224))
* **engine:** 🐛 fixed shadow UI Input ([3fbb753](https://github.com/ByteCrazy/chameleon/commit/3fbb753bb26b03dd5a25bce0fc6621f2d8b778e1))
### ✨ Features | 新功能
* **build-script, demo-page, engine, layout, render:** 🎸 add lang switch ([29da65e](https://github.com/ByteCrazy/chameleon/commit/29da65ee1aa09550d910ddfbbcb9d8b4db983373))
* **demo-page, engine, engine-website-app, material:** 🎸 add designerSizer and fixed GridItem bug ([4665aed](https://github.com/ByteCrazy/chameleon/commit/4665aed300d54c77be4abcb9a8cc0f1710ac2145))
* **demo-page, engine, render:** 🎸 add getGlobalState and optimize node udpate ([4d3934f](https://github.com/ByteCrazy/chameleon/commit/4d3934fd8febe616a44e5d39da0e10964f3c800d))
* **docs-app, engine, layout, material, model, render:** 🎸 optimize GL layout ([02274b4](https://github.com/ByteCrazy/chameleon/commit/02274b432903dc247c5613873f14715dd806decd))
* **engine, layout:** 🎸 add set canvas width method ([a18369f](https://github.com/ByteCrazy/chameleon/commit/a18369f0d4bbb4bcf04ce2695be313d767d4bbe5))
* **engine, material:** 🎸 add GRL meterial ([ae15c34](https://github.com/ByteCrazy/chameleon/commit/ae15c34a3f2736db61a933dc7d09166d9a619473))
* **engine, model:** 🎸 add advanceOptions property ([d75f178](https://github.com/ByteCrazy/chameleon/commit/d75f178fa70d4c49b451dff9dddfb30d4c061196))
* **engine, model:** 🎸 add hotKey plugin and fixed reloadPage event not trigge ([1cbf1e1](https://github.com/ByteCrazy/chameleon/commit/1cbf1e1a345d94c3b758b26cfbf1ecde69ce051c))
* **engine:** 🎸 add canvas size change button ([19ebdf8](https://github.com/ByteCrazy/chameleon/commit/19ebdf8d635b6b412979db81dc5d4f1b43d793a9))
* **engine:** 🎸 add hotAction ([c82bd21](https://github.com/ByteCrazy/chameleon/commit/c82bd21243aed9371a8576f572f600f1894f60bf))
* **engine:** 🎸 add width input ([c38422d](https://github.com/ByteCrazy/chameleon/commit/c38422d0848c4d60e8cb5b5b480671515c1d6101))
* **engine:** 🎸 optimize hotkeys methods ([bc83dba](https://github.com/ByteCrazy/chameleon/commit/bc83dba17fa4a23fa25548a284bbd69858bf15a0))
### 📝 Documentation | 文档
* **engine:** ✏️ add layout resource ([15f4325](https://github.com/ByteCrazy/chameleon/commit/15f4325e418d63bb3f046d58766e9bb1d2c96344))
* **engine:** ✏️ update md resource ([de95460](https://github.com/ByteCrazy/chameleon/commit/de954609bbc4dc24dbf704d809ffb1de9a9116b2))
## [0.0.46](https://github.com/ByteCrazy/chameleon/compare/v0.0.45...v0.0.46) (2024-06-30)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed registerCustomSetter method not valid ([9bdaa3b](https://github.com/ByteCrazy/chameleon/commit/9bdaa3b9feb1610a754b9aaa0925530f474dd3c3))
## [0.0.45](https://github.com/ByteCrazy/chameleon/compare/v0.0.44...v0.0.45) (2024-06-30)
**Note:** Version bump only for package @chamn/engine
## [0.0.44](https://github.com/ByteCrazy/chameleon/compare/v0.0.43...v0.0.44) (2024-06-30)
### ✨ Features | 新功能
* **engine, model, render:** 🎸 optimize addMaterials and fixed inner mat schema ([aa77803](https://github.com/ByteCrazy/chameleon/commit/aa77803c75b203ee2a098fbee143f4a9581e15fa))
## [0.0.43](https://github.com/ByteCrazy/chameleon/compare/v0.0.42...v0.0.43) (2024-06-29)
### ✨ Features | 新功能
* **build-script, engine, layout:** 🎸 remove scss dts generate ([9ba7af3](https://github.com/ByteCrazy/chameleon/commit/9ba7af30601804f94e90a0408745fd48de38d8b5))
## [0.0.42](https://github.com/ByteCrazy/chameleon/compare/v0.0.41...v0.0.42) (2024-06-01)
### 🐛 Bug Fixes | Bug 修复
* **engine, layout:** 🐛 fixed drag and drop problem on chrome ([5c655c3](https://github.com/ByteCrazy/chameleon/commit/5c655c3a2f288bd90b70212f0f114adf3c23f8a1))
## [0.0.41](https://github.com/ByteCrazy/chameleon/compare/v0.0.40...v0.0.41) (2024-05-26)
### ✨ Features | 新功能
* **build-script, engine, layout, material, model, render:** 🎸 optimize build script ([0a60cc4](https://github.com/ByteCrazy/chameleon/commit/0a60cc4f5e5635d9e544b5141eaed5b8433901a5))
## [0.0.40](https://github.com/ByteCrazy/chameleon/compare/v0.0.39...v0.0.40) (2024-04-28)
**Note:** Version bump only for package @chamn/engine
## [0.0.39](https://github.com/ByteCrazy/chameleon/compare/v0.0.38...v0.0.39) (2024-04-27)
### 🐛 Bug Fixes | Bug 修复
* **build-script, demo-page, engine, layout, material, model, render:** 🐛 fix flatObject collectVariable refeer pos ([1c4163a](https://github.com/ByteCrazy/chameleon/commit/1c4163aeeb89176a1ff5b50f76930889df7751fe))
## [0.0.38](https://github.com/ByteCrazy/chameleon/compare/v0.0.37...v0.0.38) (2024-04-27)
### 🐛 Bug Fixes | Bug 修复
* **engine, layout:** 🐛 fixed assets load iuess when relaod ([3eccc60](https://github.com/ByteCrazy/chameleon/commit/3eccc60bb7be5a469704a9c4f769161e525481f4))
## [0.0.37](https://github.com/ByteCrazy/chameleon/compare/v0.0.36...v0.0.37) (2024-04-27)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed updateMaterials components map problem ([b868e88](https://github.com/ByteCrazy/chameleon/commit/b868e884a9b65021effdee8872a462fd8539c9c4))
## [0.0.36](https://github.com/ByteCrazy/chameleon/compare/v0.0.35...v0.0.36) (2024-04-27)
**Note:** Version bump only for package @chamn/engine
## [0.0.35](https://github.com/ByteCrazy/chameleon/compare/v0.0.34...v0.0.35) (2024-04-27)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed onPluginReadyOk return value ([8550591](https://github.com/ByteCrazy/chameleon/commit/85505913af29459882caef5b83d17c2a8aca45b3))
## [0.0.34](https://github.com/ByteCrazy/chameleon/compare/v0.0.33...v0.0.34) (2024-04-27)
### ✨ Features | 新功能
* **docs-website, engine, model:** 🎸 add update assetsPackageList logic ([2bce2f4](https://github.com/ByteCrazy/chameleon/commit/2bce2f4758b203695ec119d6e201e3186d7fab84))
## [0.0.33](https://github.com/ByteCrazy/chameleon/compare/v0.0.32...v0.0.33) (2024-04-27)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fix css editor initial value not correct problem ([96c3e58](https://github.com/ByteCrazy/chameleon/commit/96c3e58755ed4fdfa3e200f3049b5024cb9c6719))
### ✨ Features | 新功能
* **engine, layout, render:** 🎸 optimize history step save ([dca964a](https://github.com/ByteCrazy/chameleon/commit/dca964a990a3cc0c5fd853d853a55a4957999644))
* **engine, render:** 🎸 designer plugin add updateComponent method ([f161177](https://github.com/ByteCrazy/chameleon/commit/f16117793402681a1eda8f8cba9c06e2ee5247ad))
## [0.0.32](https://github.com/ByteCrazy/chameleon/compare/v0.0.31...v0.0.32) (2024-01-30)
### ✨ Features | 新功能
* **docs-website, engine, model, render:** 🎸 add updateMaterials、updatePage methods ([39ed2b6](https://github.com/ByteCrazy/chameleon/commit/39ed2b692a8a7379a79b96cb3fd8cdb76a4f01f2))
## [0.0.31](https://github.com/ByteCrazy/chameleon/compare/v0.0.30...v0.0.31) (2023-10-17)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed arraySetter delete bug ([1b53538](https://github.com/ByteCrazy/chameleon/commit/1b53538a67ff35a6cfedffe661126f6912e78bbf))
## [0.0.30](https://github.com/ByteCrazy/chameleon/compare/v0.0.29...v0.0.30) (2023-10-17)
**Note:** Version bump only for package @chamn/engine
## [0.0.29](https://github.com/ByteCrazy/chameleon/compare/v0.0.28...v0.0.29) (2023-10-17)
### ✨ Features | 新功能
* **build-script, engine:** 🎸 optimize css editor ([66c0541](https://github.com/ByteCrazy/chameleon/commit/66c0541eaee12b2eb0ceb4fb3a7748e1bf69768d))
* **demo-page, engine, model:** 🎸 add ant color setter ([830de46](https://github.com/ByteCrazy/chameleon/commit/830de46babdf40a740248c37d8e6dc2043db1434))
* **demo-page, engine, model:** 🎸 add ColorSetter ([e72b7f1](https://github.com/ByteCrazy/chameleon/commit/e72b7f15dd8bba498b776226d5debd07c8fd4234))
* **demo-page, engine, model:** 🎸 add radio setter ([e6f9b1d](https://github.com/ByteCrazy/chameleon/commit/e6f9b1d9dba7cd3a9a82116bf5a1068c06a5a1d6))
* **demo-page, engine:** 🎸 add cssSize setter ([74bf136](https://github.com/ByteCrazy/chameleon/commit/74bf136047000a67795e1360f2a923902366a513))
* **demo-page, engine:** 🎸 add slider setter ([0fe1d29](https://github.com/ByteCrazy/chameleon/commit/0fe1d29257f214e69ca9fed6e5e4c11ccccb56bb))
* **engine, model, render:** 🎸 cssEditor support cssText ([3dc74b4](https://github.com/ByteCrazy/chameleon/commit/3dc74b4895d414a718c00911a39d8491fafcfaee))
* **engine, model:** 🎸 optimize style editor ([9c660ce](https://github.com/ByteCrazy/chameleon/commit/9c660ce694a32059450f19c824bcde9889049db8))
* **engine, model:** 🎸 style varible、c s scss editor support styles text ([c988fcf](https://github.com/ByteCrazy/chameleon/commit/c988fcf17a2dbbc486e809a03c642726f02cf547))
* **engine:** 🎸 add border input ([ae7d3c3](https://github.com/ByteCrazy/chameleon/commit/ae7d3c3e1c07bf68cfa74ef15ab8f499c9b7afc3))
* **engine:** 🎸 add CSSSizeInput componrnt ([cd70933](https://github.com/ByteCrazy/chameleon/commit/cd70933fbb044058b65c402465a69139da4d8a4a))
* **engine:** 🎸 add DimensionInput、FontInput、MarginInput、PaddingInput ([08aa4e5](https://github.com/ByteCrazy/chameleon/commit/08aa4e5a959ace882312b77a15ab5c973d2015b8))
* **engine:** 🎸 CSSUI editor sync value ([549fa7e](https://github.com/ByteCrazy/chameleon/commit/549fa7ea676d9f31d10e3b78888ec2318df2cdc2))
* **engine:** 🎸 optimize style setter ([eba569c](https://github.com/ByteCrazy/chameleon/commit/eba569cb5871f6cf8cce459c69d3d3beb7e0459a))
* **engine:** 🎸 style UI finish ([a38fb84](https://github.com/ByteCrazy/chameleon/commit/a38fb846475631caa1a6253af7de11fd30027c48))
### 🐛 Bug Fixes | Bug 修复
* **build-script, engine:** 🐛 recocer build-script bin file ([5a2ca69](https://github.com/ByteCrazy/chameleon/commit/5a2ca69c4e5c48b3b0686478f6e5c40cd21c08ad))
* **engine, render:** 🐛 fixed build error and add child cache for render ([9026474](https://github.com/ByteCrazy/chameleon/commit/90264746fe99b8cdd4d055f2d778e67aae38af87))
* **engine:** 🐛 fixed MonacoEditor not trigger value change event ([0f6c3ee](https://github.com/ByteCrazy/chameleon/commit/0f6c3ee82a31b1a23388486f4cb12f9d7424ae66))
## [0.0.28](https://github.com/ByteCrazy/chameleon/compare/v0.0.27...v0.0.28) (2023-08-29)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed click can't add component into canvas ([b8e8c2c](https://github.com/ByteCrazy/chameleon/commit/b8e8c2c703f20c2bc2836dc2e1f91b02b994e5ca))
## [0.0.27](https://github.com/ByteCrazy/chameleon/compare/v0.0.26...v0.0.27) (2023-08-28)
### ✨ Features | 新功能
* **engine:** 🎸 plugin system support add addCustomView method ([ce6db8f](https://github.com/ByteCrazy/chameleon/commit/ce6db8fcf1b87e6e3e14ed7464e7615c0ecc852b))
## [0.0.26](https://github.com/ByteCrazy/chameleon/compare/v0.0.25...v0.0.26) (2023-08-27)
**Note:** Version bump only for package @chamn/engine
## [0.0.25](https://github.com/ByteCrazy/chameleon/compare/v0.0.24...v0.0.25) (2023-08-27)
**Note:** Version bump only for package @chamn/engine
## [0.0.24](https://github.com/ByteCrazy/chameleon/compare/v0.0.23...v0.0.24) (2023-08-27)
### ✨ Features | 新功能
* **engine, model, render:** 🎸 fixed node maybe is null on rightPanel ([aee551e](https://github.com/ByteCrazy/chameleon/commit/aee551e77454f61900318003f9e3a3ffb1ef9427))
## [0.0.23](https://github.com/ByteCrazy/chameleon/compare/v0.0.22...v0.0.23) (2023-08-26)
### 🐛 Bug Fixes | Bug 修复
* **engine, render:** 🐛 fixed CSSPropertiesVariableBindEditor value not update ([9084c32](https://github.com/ByteCrazy/chameleon/commit/9084c32bb919c2e6191c29b871f9d1537d139050))
### ✨ Features | 新功能
* **engine, model:** 🎸 support custom rightPanel ([803d731](https://github.com/ByteCrazy/chameleon/commit/803d731819faa03430b2a17a154d3ff1e0daca28))
* **engine, model:** 🎸 support inject custom setter ([a49ec4a](https://github.com/ByteCrazy/chameleon/commit/a49ec4ae4a98b42cf1cfb768990b64c981539881))
## [0.0.22](https://github.com/ByteCrazy/chameleon/compare/v0.0.21...v0.0.22) (2023-08-24)
### ✨ Features | 新功能
* **demo-page, engine, layout, model:** 🎸 add disableEditorDragDom config ([1779f44](https://github.com/ByteCrazy/chameleon/commit/1779f44aff20370ea3336e72352032c6416f7dd3))
## [0.0.21](https://github.com/ByteCrazy/chameleon/compare/v0.0.20...v0.0.21) (2023-08-19)
### ✨ Features | 新功能
* **engine, layout, model, render:** 🎸 optimize wrapComponent config ([d5916a7](https://github.com/ByteCrazy/chameleon/commit/d5916a7e6ee3cf79a32d4d23663b6873d86fe671))
## [0.0.20](https://github.com/ByteCrazy/chameleon/compare/v0.0.19...v0.0.20) (2023-08-19)
**Note:** Version bump only for package @chamn/engine
## [0.0.19](https://github.com/ByteCrazy/chameleon/compare/v0.0.18...v0.0.19) (2023-08-17)
### 🐛 Bug Fixes | Bug 修复
* **engine, render:** 🐛 fixed if assets is empty, loader will not success ([169d9ad](https://github.com/ByteCrazy/chameleon/commit/169d9ad9e5791353ad3a68dd0212c6ecfda64a29))
### ✨ Features | 新功能
* **engine:** 🎸 add HistoryPlugin type definition ([13a66fb](https://github.com/ByteCrazy/chameleon/commit/13a66fb5141b308af191394b6c377a0d387ea628))
* **engine:** 🎸 use @monaco-editor/react replace monaco-editor ([848ee87](https://github.com/ByteCrazy/chameleon/commit/848ee87dedbccc71d3a7366320ad03122ed15d38))
## [0.0.18](https://github.com/ByteCrazy/chameleon/compare/v0.0.17...v0.0.18) (2023-08-14)
### ✨ Features | 新功能
* **engine:** 🎸 add className and style for engine ([c2b7ef3](https://github.com/ByteCrazy/chameleon/commit/c2b7ef3c1c68708963dece239528889701eb0fd7))
* **engine:** 🎸 add onMount for engine ([17c80ae](https://github.com/ByteCrazy/chameleon/commit/17c80aecc09f13da5f33d7a59b5849d73d99d12a))
## [0.0.17](https://github.com/ByteCrazy/chameleon/compare/v0.0.16...v0.0.17) (2023-08-06)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed types error ([1b68777](https://github.com/ByteCrazy/chameleon/commit/1b68777eb58225d750f40b16a7d263acb385477a))
## [0.0.16](https://github.com/ByteCrazy/chameleon/compare/v0.0.15...v0.0.16) (2023-08-06)
### 🐛 Bug Fixes | Bug 修复
* **engine, layout:** 🐛 fixed some node not exits querySelector method ([2ddc186](https://github.com/ByteCrazy/chameleon/commit/2ddc1863e6eb207ae2e2b705a9fef4d23bece37c))
### ✨ Features | 新功能
* **build-script-example, demo-page, engine, layout, material, model, render:** 🎸 support direct import render from package on dev mode ([6bb38e2](https://github.com/ByteCrazy/chameleon/commit/6bb38e29678a8a4d08a7fcf9548fa548399259b0))
* **demo-page, engine, layout, model:** 🎸 support advanceCustom drag and drop hooks ([fb21e15](https://github.com/ByteCrazy/chameleon/commit/fb21e1501f6829e4f13a35ea7be942ff72b0be91))
* **demo-page, engine, layout, model:** 🎸 support toolbarViewRender and ghostViewRedner ([43ced37](https://github.com/ByteCrazy/chameleon/commit/43ced371cfb7dbb58a96fc15e1bf635092307fa8))
* **demo-page, engine, layout:** 🎸 support DropViewRender ([1c025a6](https://github.com/ByteCrazy/chameleon/commit/1c025a6d1f57f450b2de262a787375f3f4891008))
* **demo-page, engine, model:** 🎸 support onDelete onCopy ([b6a76bb](https://github.com/ByteCrazy/chameleon/commit/b6a76bb244bbe2c050de17157bda97cb7ad21abc))
* **engine, layout, model:** 🎸 add customDropViewRender prop ([cea7f1e](https://github.com/ByteCrazy/chameleon/commit/cea7f1e3c2b0a8a13aef7cdcb0191593414615aa))
* **engine, layout, model:** 🎸 finish layout transform ([73ead35](https://github.com/ByteCrazy/chameleon/commit/73ead357b9aded2b5ee2b545da6f2b3677ce8393))
* **engine:** 🎸 optimze plugin type definition ([da9c107](https://github.com/ByteCrazy/chameleon/commit/da9c107b20646714834642ad09b2cbdfcb6eb7cd))
## [0.0.15](https://github.com/ByteCrazy/chameleon/compare/v0.0.14...v0.0.15) (2023-07-01)
### 🐛 Bug Fixes | Bug 修复
* **engine, layout:** 🐛 fixed component will be add repeatedly after reload page ([1507549](https://github.com/ByteCrazy/chameleon/commit/1507549632c3c6e09d8555fd0286fcc72855c358))
### ✨ Features | 新功能
* **demo-page, docs-website, engine, model, render:** 🎸 optimize assets load ([3ee6d58](https://github.com/ByteCrazy/chameleon/commit/3ee6d58a88e5af3fc631723240783d5c97a273b0))
* **demo-page, engine:** 🎸 setter support initialValue ([794d807](https://github.com/ByteCrazy/chameleon/commit/794d8072518cb3b1a897b04a2e96f5ca53fdb365))
* **docs-website, engine, layout, material, render:** 🎸 support inject thridLib on $context ([ca2f074](https://github.com/ByteCrazy/chameleon/commit/ca2f07492b713c32e5a41d1f250f7888763cb665))
* **engine, render:** 🎸 change internal api ([0c0c7c3](https://github.com/ByteCrazy/chameleon/commit/0c0c7c3fa8f86c08ab0cefb0396107ed8c52dcd5))
## [0.0.14](https://github.com/ByteCrazy/chameleon/compare/v0.0.13...v0.0.14) (2023-05-10)
### ✨ Features | 新功能
* **engine:** 🎸 add node id in advance panel ([d457203](https://github.com/ByteCrazy/chameleon/commit/d45720316ab95f75f6f67a5dbf31bb0030ceb3f1))
### 🐛 Bug Fixes | Bug 修复
* **engine, layout:** 🐛 fixed new component can not be add into canvas ([4057fa9](https://github.com/ByteCrazy/chameleon/commit/4057fa925c18bf06325de04b270be5f6c23351c8))
* **engine, layout:** 🐛 fixed type problem ([751a392](https://github.com/ByteCrazy/chameleon/commit/751a39244228f74138acf7f567d23e888b8ff687))
* **engine, render:** 🐛 fixed removeMediaCSS method run failed ([9127ec3](https://github.com/ByteCrazy/chameleon/commit/9127ec3d13f43a1c8763b2350bc0224d683da85c))
## [0.0.13](https://github.com/ByteCrazy/chameleon/compare/v0.0.12...v0.0.13) (2023-05-03)
### ✨ Features | 新功能
* add-nearby-component ([85a301c](https://github.com/ByteCrazy/chameleon/commit/85a301c3d95e785c116ab38c0b3a452ceb33742f))
* **build-script, build-script-example, demo-page, engine, layout, material, model, render:** 🎸 config code style config ([2325504](https://github.com/ByteCrazy/chameleon/commit/23255048fa4a3d4fc8f5cfa1312db5abd6cac70d))
* **build-script, demo-page, engine, layout, model, render:** 🎸 update meterial schema ([14f4e5c](https://github.com/ByteCrazy/chameleon/commit/14f4e5caf0a4f6eca131e139d0c34ea21969dc0b))
* **demo-page, docs-website, engine, layout, model, render:** 🎸 node support methods config ([e080b20](https://github.com/ByteCrazy/chameleon/commit/e080b20cea3cea27b95def196e078c1c225d3a83))
* **demo-page, engine, layout, model:** 🎸 finish customEvent config ([84640e3](https://github.com/ByteCrazy/chameleon/commit/84640e3d06b79858590b9fe92ef4764fbe3f4f7b))
* **demo-page, engine, layout:** 🎸 outline support cancel drag by node material ([cd428d3](https://github.com/ByteCrazy/chameleon/commit/cd428d362a8bfe7d5b6e58ed2b6290e19c00672b))
* **demo-page, engine, layout:** 🎸 support cancel drag and custom node drag event ([825717e](https://github.com/ByteCrazy/chameleon/commit/825717e063846b72f95b0e41d8cba279ef5270c8))
* **demo-page, engine:** 🎸 complete advanceCustom material feature ([98cf273](https://github.com/ByteCrazy/chameleon/commit/98cf273543f2fd534c254990d61052534a6649da))
* **demo-page, engine:** 🎸 supprot onSelectNode ([f496a82](https://github.com/ByteCrazy/chameleon/commit/f496a82920cab7ae8704335b2ecd02a7887de3b2))
* **engine, layout, model:** 🎸 support supportDispatchNativeEvent field ([489fd05](https://github.com/ByteCrazy/chameleon/commit/489fd05b588e85ed4cea81cc33ab27739a1bac59))
* **engine, layout:** 🎸 optimize layout event system ([56f35ef](https://github.com/ByteCrazy/chameleon/commit/56f35ef83cc7e1658bfeba9081f997ff457cf09e))
* **engine, layout:** 🎸 support isSupportDispatchNativeEvent filed ([4f830b4](https://github.com/ByteCrazy/chameleon/commit/4f830b42b6e84d74e9d462c3b3e090de89686e18))
* **engine, render:** 🎸 html native tag component support container filed ([475fdbd](https://github.com/ByteCrazy/chameleon/commit/475fdbd5a34581ab7cafe67b5c9d3b94102b3a16))
* remove self-executing function ([e224524](https://github.com/ByteCrazy/chameleon/commit/e224524198aa3f1339afd3bc9f1bacb72bc4d20e))
* select element after in add node ([0c0d116](https://github.com/ByteCrazy/chameleon/commit/0c0d1164783420da306062292674fbccb0a8ffb0))
### 🐛 Bug Fixes | Bug 修复
* adjust find node logic ([5f9f073](https://github.com/ByteCrazy/chameleon/commit/5f9f07320888b3a83a4a24b04cfbd2e9e82aa4b1))
* **engine, layout, model:** 🐛 fixed type error ([d11e81f](https://github.com/ByteCrazy/chameleon/commit/d11e81f607ef6a41a2dfda0b4ac657a9a87e948c))
* **engine:** 🐛 fixed insert node logic ([7fd4866](https://github.com/ByteCrazy/chameleon/commit/7fd4866d7daaece1c8c3b2a846b55da27a1e0936))
## [0.0.12](https://github.com/ByteCrazy/chameleon/compare/v0.0.11...v0.0.12) (2023-04-23)
### 🐛 Bug Fixes | Bug 修复
* **build-script, build-script-example, engine, layout, material, model, render:** 🐛 fix can not find vite/client.d.ts problem ([8a53540](https://github.com/ByteCrazy/chameleon/commit/8a53540f75737707f69f93d2f701203dbf88084a))
### ✨ Features | 新功能
* **demo-page, docs-website, engine, model:** 🎸 update schema and change err to warn when schema check ([e753862](https://github.com/ByteCrazy/chameleon/commit/e7538626bad6681e4d488ac835fef61a603c0853))
* **demo-page, engine, layout, model, render:** 🎸 node support config self isContainer property ([d94f1f9](https://github.com/ByteCrazy/chameleon/commit/d94f1f9203f2c273856fd4aa489a1ed9cbb0f0ec))
## [0.0.11](https://github.com/ByteCrazy/chameleon/compare/v0.0.10...v0.0.11) (2023-04-21)
### ✨ Features | 新功能
* add reload method of plugin ([3b2cb2a](https://github.com/ByteCrazy/chameleon/commit/3b2cb2aa3804e250ed8ce37027168282f63db879))
* add replaceSubTopBarView method ([da32d9c](https://github.com/ByteCrazy/chameleon/commit/da32d9c67a6fff1a562886c027c1604a54e19b7e))
* **build-script, build-script-example, demo-page, engine, layout, material, model, render:** 🎸 optimize build-script output file format ([eda44d2](https://github.com/ByteCrazy/chameleon/commit/eda44d255bd3c5af19013bdd61342e1f0817413a))
* **build-script, docs-website, engine:** 🎸 add material develop doc ([0aacca0](https://github.com/ByteCrazy/chameleon/commit/0aacca0f726bc13606b814f8890b4e8ff8982142))
* **engine, model:** 🎸 support custom setter ([5236c54](https://github.com/ByteCrazy/chameleon/commit/5236c543559aac517e833741ffa8046bc1c7f1a3))
* RightPanel add replacePanel、removePanel methods ([ee6c2e1](https://github.com/ByteCrazy/chameleon/commit/ee6c2e1b45bf93075342cee060fe3038ec7b8e53))
### 📝 Documentation | 文档
* **docs-website, engine:** ✏️ add plugin、setter develop doc ([288edc4](https://github.com/ByteCrazy/chameleon/commit/288edc4999f732ff2f27b1c6e50900b9b0094700))
## [0.0.10](https://github.com/ByteCrazy/chameleon/compare/v0.0.9...v0.0.10) (2023-04-17)
**Note:** Version bump only for package @chamn/engine
## [0.0.9](https://github.com/ByteCrazy/chameleon/compare/v0.0.8...v0.0.9) (2023-04-17)
### ✨ Features | 新功能
* **docs-website, engine, render:** 🎸 render add collectVariable flatObject util method ([ae25160](https://github.com/ByteCrazy/chameleon/commit/ae25160b568c267041b3827e836c95f60ecaee59))
## [0.0.8](https://github.com/ByteCrazy/chameleon/compare/v0.0.7...v0.0.8) (2023-04-16)
### ✨ Features | 新功能
* **build-script, engine, layout, model, render:** 🎸 optimize pack package ([78d99c5](https://github.com/ByteCrazy/chameleon/commit/78d99c507ad65ca39101f0239467737d2b445c87))
* **docs-website, engine, layout:** 🎸 add defaultRender for engine, add some docs ([91f4257](https://github.com/ByteCrazy/chameleon/commit/91f4257e9f9b9391267e4b8d64e6ed811912381f))
* **docs-website, engine:** 🎸 add docs ([0e6f605](https://github.com/ByteCrazy/chameleon/commit/0e6f6053fa3cd5e13b6a7a8258c27518c30470c5))
### 📝 Documentation | 文档
* **docs-website, engine:** ✏️ add doc ([2fb5cf5](https://github.com/ByteCrazy/chameleon/commit/2fb5cf5fd4dcb3859ecbdc8d42adf787d9e0255d))
## [0.0.7](https://github.com/ByteCrazy/chameleon/compare/v0.0.6...v0.0.7) (2023-03-29)
### ✨ Features | 新功能
* **build-script, build-script-example, demo-page, engine, layout, material, model, render:** 🎸 external monaco eitor and remove style panel ([494d898](https://github.com/ByteCrazy/chameleon/commit/494d898fd75dabe84d867ff45e84ae63f9b59c5e))
## [0.0.6](https://github.com/ByteCrazy/chameleon/compare/v0.0.5...v0.0.6) (2023-03-28)
**Note:** Version bump only for package @chamn/engine
## [0.0.5](https://github.com/ByteCrazy/chameleon/compare/v0.0.4...v0.0.5) (2023-03-28)
### ✨ Features | 新功能
* **build-script, engine, layout, material, model, render:** 🎸 complete package info ([86b095d](https://github.com/ByteCrazy/chameleon/commit/86b095da6c955946300591b1775f59c1a141c065))
## [0.0.4](https://github.com/ByteCrazy/chameleon/compare/v0.0.3...v0.0.4) (2023-03-27)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fixed engine d.ts path not correct problem ([fb20ca6](https://github.com/ByteCrazy/chameleon/commit/fb20ca604fff4e85b0264eff6383154ecfefd437))
## [0.0.2](https://github.com/ByteCrazy/chameleon/compare/v0.0.1...v0.0.2) (2023-03-27)
### 🐛 Bug Fixes | Bug 修复
* **engine:** 🐛 fix engine not incluce dist folder ([fc2c39e](https://github.com/ByteCrazy/chameleon/commit/fc2c39e88aa2eeb82ba5e3989e5bdf244d112dfb))
================================================
FILE: packages/engine/LICENSE
================================================
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
================================================
FILE: packages/engine/README.md
================================================
# chameleon
> Chameleon is ever-changing
A web visual programming engine, help to build a web page with 5 minutes. every people can use it easy.
[Docs](https://hlerenow.github.io/chameleon/documents/) | [Demo](https://hlerenow.github.io/chameleon/)
## Install
```shell
npm i @chamn/engine @chamn/model @chamn/render
```
## Usage
[Example Project](https://github.com/ByteCrazy/chameleon-demo)
### ScreenSnapshot





================================================
FILE: packages/engine/__tests__/demo.test.ts
================================================
test('adds 1 + 2 to equal 3', () => {
expect(3).toBe(3);
});
================================================
FILE: packages/engine/build.common.config.ts
================================================
/* eslint-disable @typescript-eslint/no-var-requires */
import path from 'path';
export default {
resolve: {
alias: [{ find: '@', replacement: path.resolve(__dirname, 'src') }],
},
css: {
preprocessorOptions: {
scss: {
additionalData: '@use "@/assets/styles/mixin.scss" as *;\n',
},
},
},
};
================================================
FILE: packages/engine/build.config.ts
================================================
import { viteStaticCopy } from 'vite-plugin-static-copy';
import pkg from './package.json';
import commonConfig from './build.common.config';
import { BuildScriptConfig } from '@chamn/build-script';
// 开发模式默认读取 index.html 作为开发模式入口
// entry 作为打包库入口
const plugins: any[] = [];
if (process.env.BUILD_TYPE === 'APP') {
plugins.push(
viteStaticCopy({
targets: [
{
src: './node_modules/@chamn/render/dist/index.umd.js',
dest: './',
rename: 'render.umd.js',
},
],
})
);
}
const mainConfig: BuildScriptConfig = {
libMode: true,
entry: './src/index.tsx',
libName: 'ChamnEngine',
fileName: 'index',
global: {
react: 'React',
'react-dom': 'ReactDOM',
},
vite: {
...commonConfig,
plugins,
define: {
global: 'globalThis',
'process.env': JSON.stringify('{}'),
__RUN_MODE__: JSON.stringify(process.env.BUILD_TYPE),
__PACKAGE_VERSION__: JSON.stringify(pkg.version),
__BUILD_VERSION__: JSON.stringify(Date.now()),
},
},
};
const config = mainConfig;
export default config;
================================================
FILE: packages/engine/index.html
================================================
Chameleon Low-Code Engine
================================================
FILE: packages/engine/jest.config.js
================================================
/* eslint-disable no-undef */
/*
* For a detailed explanation regarding each configuration property, visit:
* https://jestjs.io/docs/configuration
*/
module.exports = {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/private/var/folders/l9/th_r5d_12wxdj16859_mctjw0000gn/T/jest_dx",
// Automatically clear mock calls, instances, contexts and results before every test
// clearMocks: false,
// Indicates whether the coverage information should be collected while executing the test
// collectCoverage: false,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
// coverageDirectory: undefined,
// An array of regexp pattern strings used to skip coverage collection
// coveragePathIgnorePatterns: [
// "/node_modules/"
// ],
// Indicates which provider should be used to instrument code for coverage
// coverageProvider: "babel",
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// The default configuration for fake timers
// fakeTimers: {
// "enableGlobally": false
// },
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "mjs",
// "cjs",
// "jsx",
// "ts",
// "tsx",
// "json",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
preset: 'ts-jest',
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state before every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state and implementation before every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
// roots: [
// ""
// ],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: 'jsdom',
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
// testMatch: [
// "**/__tests__/**/*.[jt]s?(x)",
// "**/?(*.)+(spec|test).[tj]s?(x)"
// ],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jest-circus/runner",
// A map from regular expressions to paths to transformers
// transform: undefined,
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};
================================================
FILE: packages/engine/package.json
================================================
{
"name": "@chamn/engine",
"version": "0.10.4",
"type": "module",
"files": [
"dist"
],
"main": "dist/index.cjs.js",
"module": "dist/index.es.js",
"types": "dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"module-sync": "./dist/index.es.js",
"import": "./dist/index.es.js",
"require": "./dist/index.cjs.js"
},
"./dist/*": "./dist/*"
},
"publishConfig": {
"access": "public"
},
"homepage": "https://github.com/hlerenow/chameleon",
"keywords": [
"engine",
"lowcode",
"react",
"web-editor",
"visual-editor",
"vite"
],
"scripts": {
"start": "build-script",
"build": "cross-env BUILD_TYPE=PKG build-script --build",
"build:w": "cross-env BUILD_TYPE=PKG build-script --build --watch",
"build:analyze": "cross-env BUILD_TYPE=PKG build-script --build --analyze",
"lint": "eslint ./src",
"prettier": "prettier --write ./src",
"test": "jest",
"storybook": "storybook dev -p 6006",
"build-storybook": "storybook build"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
},
"dependencies": {
"@ant-design/icons": "^5.4.0",
"@chamn/layout": "workspace:*",
"@chamn/model": "workspace:*",
"@chamn/render": "workspace:*",
"@dagrejs/dagre": "^1.1.4",
"@dnd-kit/core": "^6.0.7",
"@dnd-kit/modifiers": "^6.0.1",
"@dnd-kit/sortable": "^7.0.2",
"@dnd-kit/utilities": "^3.2.1",
"@monaco-editor/react": "^4.5.1",
"@xyflow/react": "^12.3.6",
"ahooks": "^3.7.4",
"antd": "^5.23.2",
"color": "^4.2.3",
"consola": "^3.2.3",
"css-tree": "^3.1.0",
"i18next": "^22.1.5",
"loadjs": "^4.3.0",
"lodash-es": "^4.17.21",
"lucide-react": "^0.487.0",
"mitt": "^3.0.0",
"monaco-editor": "^0.41.0",
"quicktype-core": "^23.0.171",
"react-color": "^2.19.3",
"react-contenteditable": "^3.3.7",
"react-i18next": "^12.1.1"
},
"devDependencies": {
"@babel/core": "^7.21.0",
"@chamn/build-script": "workspace:*",
"@chamn/demo-page": "workspace:*",
"@chromatic-com/storybook": "^3.2.3",
"@storybook/addon-essentials": "^8.4.7",
"@storybook/addon-interactions": "^8.4.7",
"@storybook/addon-onboarding": "^8.4.7",
"@storybook/blocks": "^8.4.7",
"@storybook/react": "^8.4.7",
"@storybook/react-vite": "^8.4.7",
"@storybook/test": "^8.4.7",
"@types/color": "^3.0.4",
"@types/css": "^0.0.34",
"@types/css-tree": "^2.3.10",
"@types/loadjs": "^4.0.4",
"@types/lodash-es": "^4.17.6",
"@types/react": "^18.2.0",
"@types/react-color": "^3.0.6",
"@types/react-dom": "^18.0.6",
"babel-loader": "^8.3.0",
"cross-env": "^7.0.3",
"prop-types": "^15.8.1",
"re-resizable": "^6.9.9",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.7.0",
"sass": "^1.54.0",
"storybook": "^8.4.7",
"vite-plugin-static-copy": "^0.17.0"
},
"config": {},
"gitHead": "dc3e55fdeb903a8012f6ebd3ebc018ed61ad89db"
}
================================================
FILE: packages/engine/src/Engine.module.scss
================================================
.engineContainer {
color: $fontColor;
font-size: $fontSize;
width: 100%;
height: 100%;
overflow: hidden;
* {
box-sizing: border-box;
}
}
================================================
FILE: packages/engine/src/_dev_/index.css
================================================
html,
body,
#root {
width: 100%;
height: 100%;
overflow: hidden;
}
body {
margin: 0;
padding: 0;
}
.logo {
height: 100%;
font-size: 20px;
display: flex;
align-items: center;
margin-left: 20px;
font-weight: bolder;
margin-right: auto;
}
================================================
FILE: packages/engine/src/_dev_/index.tsx
================================================
import ReactDOMClient from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import { router } from './router';
ReactDOMClient.createRoot(document.getElementById('root') as HTMLElement).render( );
================================================
FILE: packages/engine/src/_dev_/lib/index.tsx
================================================
import { Resizable } from 're-resizable';
import { useEffect, useRef, useState } from 'react';
import ContentEditable from 'react-contenteditable';
export const CLayout = () => {
const text = useRef('123123123');
const contentEditableRef = useRef(null);
const handleChange = (evt: any) => {
text.current = evt.target.value;
};
const handleBlur = () => {
console.log(text.current);
};
useEffect(() => {
document.addEventListener('click', (e) => {
if (e.target && contentEditableRef.current) {
if (!(contentEditableRef.current as HTMLDivElement).contains(e.target as any)) {
(contentEditableRef.current as any)?.blur();
setCanEdit(false);
}
}
});
}, []);
const [canEdit, setCanEdit] = useState(false);
return (
CLayout 布局样例
{
setCanEdit(true);
(contentEditableRef.current as any)?.focus();
}}
>
{
console.log('mouse down');
}}
innerRef={contentEditableRef}
html={text.current}
onBlur={handleBlur}
onChange={handleChange}
/>
Sample with default size
);
};
================================================
FILE: packages/engine/src/_dev_/page/Editor/index.tsx
================================================
import { BasePage } from '@chamn/demo-page';
import { Button, message, Modal, Select } from 'antd';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import ReactDOMClient from 'react-dom/client';
import { Engine, PluginInstance } from '../../..';
import '../../index.css';
import { ComponentLibPluginConfig, DEFAULT_PLUGIN_LIST, DEFAULT_PLUGIN_NAME_MAP } from '../../../plugins';
import { DisplaySourceSchema } from '../../../plugins/DisplaySourceSchema';
import { InnerComponentMeta } from '../../../material/innerMaterial';
import { RollbackOutlined } from '@ant-design/icons';
import { LayoutPropsType } from '@chamn/layout';
import { collectVariable, flatObject, getThirdLibs } from '@chamn/render';
import { HistoryPluginInstance } from '@/plugins/History/type';
import { DesignerPluginInstance } from '@/plugins/Designer/type';
import { DesignerSizer } from '@/component/DesignerSizer';
import { EnginContext } from '@/type';
import renderAsURL from '@chamn/render/dist/index.umd.js?url';
const win = window as any;
win.React = React;
win.ReactDOM = ReactDOM;
win.ReactDOMClient = ReactDOMClient;
const customRender: LayoutPropsType['customRender'] = async ({
iframe: iframeContainer,
assets,
page,
pageModel,
beforeInitRender,
ready,
}) => {
await iframeContainer.loadUrl('/src/_dev_/render.html');
// must call
beforeInitRender?.();
const iframeWindow = iframeContainer.getWindow()!;
const iframeDoc = iframeContainer.getDocument()!;
const IframeReact = iframeWindow.React!;
const IframeReactDOM = iframeWindow.ReactDOMClient!;
const CRender = iframeWindow.CRender!;
await new CRender.AssetLoader(assets, {
window: iframeWindow,
}).load();
// 从子窗口获取物料对象
const componentCollection = collectVariable(assets, iframeWindow);
const components = flatObject(componentCollection);
const thirdLibs = getThirdLibs(componentCollection, page?.thirdLibs || []);
const App = IframeReact?.createElement(CRender.DesignRender, {
adapter: CRender?.ReactAdapter,
page: page,
pageModel: pageModel,
components: {
...components,
},
$$context: {
thirdLibs,
getProps: () => {
return {};
},
callEventMethod: (method: string, params: any) => {
console.log(method, params);
},
},
requestAPI: async (params) => {
return console.log(222, params);
},
onMount: (designRenderInstance) => {
ready(designRenderInstance);
},
});
IframeReactDOM.createRoot(iframeDoc.getElementById('app')!).render(App);
};
const buildVersion = `t_${__BUILD_VERSION__}`;
const assetPackagesList = [] as any[];
export const App = () => {
const [ready, setReady] = useState(false);
const [page, setPage] = useState(BasePage);
const [lang, setLang] = useState(() => {
const lang = localStorage.getItem('lang') || 'zh_CN';
return lang;
});
const engineRef = useRef();
useEffect(() => {
// check 本地版本号,如果不一致直接覆盖本地所有的
const localBuildVersion = localStorage.getItem('build_version');
if (localBuildVersion !== buildVersion && !import.meta.env.DEV) {
// 清理 schema, 因为可能 协议不兼容,demo 可以这样粗暴处理
localStorage.setItem('pageSchema', '');
localStorage.setItem('build_version', buildVersion);
}
const localPage = localStorage.getItem('pageSchema');
if (localPage) {
setPage(JSON.parse(localPage));
}
setReady(true);
}, []);
const onReady = useCallback(
async (ctx: EnginContext) => {
engineRef.current = ctx;
engineRef.current?.engine.getI18n()?.changeLanguage(lang);
const designer: DesignerPluginInstance = await ctx.pluginManager.onPluginReadyOk('Designer');
const reloadPage = async () => {
setTimeout(() => {
const designerExport = designer?.export;
console.log('to reload');
designerExport.reload();
}, 0);
};
const workbench = ctx.engine.getWorkbench();
// 添加自定义 view
// const disposeView = workbench?.addCustomView({
// key: 'testView',
// view: (
// console.log('click')}
// >
// 123123
//
// ),
// });
workbench?.replaceTopBarView(
Chameleon EG
{ctx && }
{
setLang(val);
engineRef.current?.engine.getI18n()?.changeLanguage(val);
}}
options={[
{
value: 'zh_CN',
label: 'Chinese',
},
{
value: 'en_US',
label: 'English',
},
]}
/>
Documents
Github
{
const res = await ctx.pluginManager.get('History');
res?.export.preStep();
}}
>
{
const res = await ctx.pluginManager.get('History');
res?.export.nextStep();
}}
>
Source Code
{
reloadPage();
}}
>
Refresh Page
{
let src = '/#/preview';
if (location.href.includes('hlerenow')) {
src = '/chameleon/#/preview';
}
Modal.info({
closable: true,
icon: null,
width: 'calc(100vw - 100px)',
centered: true,
title: (
Preview
{
window.open(src);
}}
>
Open in new window
),
content: (
),
footer: <>>,
});
}}
>
Preview
{
const newPage = ctx.engine.pageModel.export();
localStorage.setItem('pageSchema', JSON.stringify(newPage));
message.success('Save successfully');
}}
>
Save
);
},
[lang]
);
if (!ready) {
return <>loading...>;
}
return (
{
// setTimeout(async () => {
// const res = await ctx.engine.updateMaterials(
// [],
// [
// {
// package: 'lodash',
// globalName: 'lodash',
// resources: [
// {
// src: 'https://cdn.bootcdn.net/ajax/libs/lodash.js/4.17.21/lodash.js',
// },
// ],
// },
// {
// package: 'dayjs',
// globalName: 'dayjs',
// resources: [
// {
// src: 'https://cdn.bootcdn.net/ajax/libs/dayjs/1.11.9/dayjs.min.js',
// },
// ],
// },
// ]
// );
// console.log('add material successfully');
// }, 2 * 1000);
}}
// 传入组件物料
material={[...InnerComponentMeta]}
// 组件物料对应的 js 运行库,只能使用 umd 模式的 js
assetPackagesList={assetPackagesList}
beforePluginRun={({ pluginManager }) => {
pluginManager.customPlugin('RightPanel', (pluginInstance) => {
pluginInstance.ctx.config.customPropertySetterMap = {
TestSetter: (props: any) => {
useEffect(() => {
console.log(props);
const currentNode = props.setterContext.pluginCtx.engine.getActiveNode();
currentNode.value.configure.isContainer = false;
currentNode.value.children = [];
currentNode.updateValue();
}, [props]);
return 123
;
},
};
return pluginInstance;
});
pluginManager.customPlugin('Designer', (pluginInstance: DesignerPluginInstance) => {
if (__RUN_MODE__ !== 'APP') {
pluginInstance.ctx.config.customRender = customRender;
}
return pluginInstance;
});
/** 自定义组件库插件的搜索框 */
pluginManager.customPlugin(
DEFAULT_PLUGIN_NAME_MAP.ComponentLibPlugin,
(pluginInstance: PluginInstance) => {
pluginInstance.ctx.config.customSearchBar = ({ defaultInputView }) => {
return {defaultInputView}
;
};
return pluginInstance;
}
);
}}
monacoEditor={{
cndUrl: 'https://static.hai-fe.com/code-block/web/dist/monaco-editor/min/vs',
}}
renderJSUrl={renderAsURL}
onReady={onReady}
renderProps={{
requestAPI: async (params) => {
return console.log(7788, params);
},
}}
/>
);
};
================================================
FILE: packages/engine/src/_dev_/page/Preview/index.tsx
================================================
import { useEffect, useState } from 'react';
import {
ReactAdapter,
Render,
useRender,
AssetLoader,
collectVariable,
flatObject,
getComponentsLibs,
getThirdLibs,
} from '@chamn/render';
import { AssetPackage, CPageDataType } from '@chamn/model';
const loadAssets = async (assets: AssetPackage[]) => {
// 注入组件物料资源
const assetLoader = new AssetLoader(assets);
try {
await assetLoader.load();
// 从子窗口获取物料对象
const componentCollection = collectVariable(assets, window);
return componentCollection;
} catch {
return null;
}
};
export const Preview = () => {
const [page, setPage] = useState();
const renderHandle = useRender();
const [loading, setLoading] = useState(true);
const [pageComponents, setPageComponents] = useState({});
const [renderContext, setRenderContext] = useState({});
// 需要区分 那些 UI 组件那些第三方库的对象,分别注入
const loadPageAssets = async (pageInfo: CPageDataType) => {
const assets = pageInfo.assets || [];
const allLibs = (await loadAssets(assets)) || {};
const componentsLibs = getComponentsLibs(flatObject(allLibs), pageInfo.componentsMeta);
const thirdLibs = getThirdLibs(allLibs, pageInfo.thirdLibs || []);
if (componentsLibs) {
setPageComponents(componentsLibs);
setRenderContext({ thirdLibs });
setLoading(false);
}
};
useEffect(() => {
const localPage = localStorage.getItem('pageSchema');
if (localPage) {
const page: CPageDataType = JSON.parse(localPage);
setPage(page);
loadPageAssets(page);
}
}, []);
if (loading) {
return <>Not find page info on local, please ensure you save it on editor>;
}
return (
);
};
================================================
FILE: packages/engine/src/_dev_/page/componentEditor/index.tsx
================================================
import { BasePage } from '@chamn/demo-page';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import ReactDOM from 'react-dom';
import ReactDOMClient from 'react-dom/client';
import { Engine } from '../../..';
import '../../index.css';
import { DEFAULT_PLUGIN_LIST } from '../../../plugins';
import { InnerComponentMeta } from '../../../material/innerMaterial';
import {} from '@ant-design/icons';
import { DesignerPluginInstance } from '@/plugins/Designer/type';
import { EnginContext } from '@/type';
import renderAsURL from '@chamn/render/dist/index.umd.js?url';
const win = window as any;
win.React = React;
win.ReactDOM = ReactDOM;
win.ReactDOMClient = ReactDOMClient;
const buildVersion = `t_${__BUILD_VERSION__}`;
const assetPackagesList = [] as any[];
export const ComponentEditor = () => {
const [ready, setReady] = useState(false);
const [page, setPage] = useState(BasePage);
const [lang] = useState(() => {
const lang = localStorage.getItem('lang') || 'zh_CN';
return lang;
});
const engineRef = useRef();
useEffect(() => {
// check 本地版本号,如果不一致直接覆盖本地所有的
const localBuildVersion = localStorage.getItem('build_version');
if (localBuildVersion !== buildVersion && !import.meta.env.DEV) {
// 清理 schema, 因为可能 协议不兼容,demo 可以这样粗暴处理
localStorage.setItem('pageSchema', '');
localStorage.setItem('build_version', buildVersion);
}
const localPage = localStorage.getItem('pageSchema');
if (localPage) {
setPage(JSON.parse(localPage));
}
setReady(true);
}, []);
const onReady = useCallback(
async (ctx: EnginContext) => {
engineRef.current = ctx;
engineRef.current?.engine.getI18n()?.changeLanguage(lang);
// const designer: DesignerPluginInstance = await ctx.pluginManager.onPluginReadyOk('Designer');
// designer.export?.setMode?.(LayoutMode.EDIT);
// setTimeout(() => {
// designer.export?.setMode?.(LayoutMode.PREVIEW);
// setTimeout(() => {
// designer.export?.setMode?.(LayoutMode.EDIT);
// }, 3 * 1000);
// }, 5 * 1000);
engineRef.current?.engine.preview();
setTimeout(() => {
engineRef.current?.engine.existPreview();
setTimeout(() => {
engineRef.current?.engine.preview();
}, 5000);
}, 3000);
const workbench = ctx.engine.getWorkbench();
workbench?.replaceTopBarView(<>>);
},
[lang]
);
if (!ready) {
return <>loading...>;
}
return (
{
pluginManager.customPlugin('Designer', (pluginInstance: DesignerPluginInstance) => {
pluginInstance.ctx.config.toolbarViewRender = () => {
return <>>;
};
return pluginInstance;
});
}}
renderJSUrl={renderAsURL}
onReady={onReady}
renderProps={{
requestAPI: async (params) => {
return console.log(7788, params);
},
}}
/>
);
};
================================================
FILE: packages/engine/src/_dev_/render.html
================================================
Low-code engine
================================================
FILE: packages/engine/src/_dev_/render.tsx
================================================
import renderAsURL from '@chamn/render/dist/index.umd.js?url';
import loadjs from 'loadjs';
loadjs([renderAsURL], () => {
console.log('load render.umd.js success');
});
================================================
FILE: packages/engine/src/_dev_/router.tsx
================================================
import { createHashRouter } from 'react-router-dom';
import { App } from './page/Editor';
import { Preview } from './page/Preview';
import { ComponentEditor } from './page/componentEditor';
export const router: any = createHashRouter([
{
path: '/previewComp',
element: ,
},
{
path: '/',
element: ,
},
{
path: '/preview',
element: ,
},
]);
================================================
FILE: packages/engine/src/assets/styles/mixin.scss
================================================
@use 'sass:color';
$baseColor: #1677ff;
/* base style variables */
$borderColor: rgb(233 233 233);
$baseBackgroundColor: #edeff3;
$fontSizeBigger: 16px;
$fontSize: 14px;
$fontSizeSmall: 12px;
$fontColor: rgb(104, 104, 104);
$hoverColor: rgba(22, 119, 255, 0.3);
$textColor: $fontColor;
$borderRadius: 2px;
================================================
FILE: packages/engine/src/build-script-env.d.ts
================================================
///
/** 包版本号 */
declare const __PACKAGE_VERSION__: string;
/** 构建版本号 */
declare const __BUILD_VERSION__: string;
/** 运行模式 */
declare const __RUN_MODE__: string;
================================================
FILE: packages/engine/src/component/CSSCodeEditor/helper.ts
================================================
import * as csstree from 'css-tree';
export function parseCssToObject(css: string): Record> {
const result: Record> = {};
const ast = csstree.parse(css, {
context: 'stylesheet',
});
csstree.walk(ast, {
visit: 'Rule',
enter(node) {
if (node.type === 'Rule' && node.prelude.type === 'SelectorList') {
const selector = csstree.generate(node.prelude);
const declarations: Record = {};
if (node.block.type === 'Block') {
for (const declaration of node.block.children.toArray()) {
if (declaration.type === 'Declaration') {
const prop = declaration.property;
const value = csstree.generate(declaration.value);
declarations[prop] = value;
}
}
}
result[selector] = declarations;
}
},
});
return result;
}
================================================
FILE: packages/engine/src/component/CSSCodeEditor/index.tsx
================================================
import { forwardRef, useImperativeHandle, useRef } from 'react';
import { MonacoEditor, MonacoEditorInstance } from '../MonacoEditor';
import styles from './style.module.scss';
import { parseCssToObject } from './helper';
import { InputCommonRef } from '../StylePanel/type';
import { isEqual } from 'lodash-es';
const getCssCodeFromStyleObj = (styleObj: Record = {}) => {
let res = `.node {\n`;
Object.keys(styleObj).forEach((key) => {
res += ` ${key}: ${styleObj[key]};\n`;
});
res += '}';
return res;
};
type CSSCodeEditorProps = {
onValueChange?: (newVal: Record) => void;
};
export type CSSCodeEditorRef = InputCommonRef;
export const CSSCodeEditor = forwardRef((props, ref) => {
const editorRef = useRef(null);
const valueRef = useRef({});
useImperativeHandle(
ref,
() => {
return {
setEmptyValue: () => {
valueRef.current = {};
editorRef.current?.setValue(getCssCodeFromStyleObj({}));
},
setValue: (val) => {
if (isEqual(val, valueRef.current)) {
return;
}
valueRef.current = val;
editorRef.current?.setValue(getCssCodeFromStyleObj(val));
},
};
},
[]
);
return (
{
editorRef.current = editor;
editorRef.current?.setValue(getCssCodeFromStyleObj(valueRef.current || {}));
}}
onChange={(newVal) => {
const styleObj = parseCssToObject(newVal || '');
const newValObj = styleObj['.node'] || {};
valueRef.current = newValObj;
props.onValueChange?.(newValObj);
}}
options={{
tabSize: 2,
minimap: { enabled: false },
folding: false,
lineNumbers: 'off',
hover: {
// enabled: false, // ✅ 禁用 hoverWidget
},
}}
/>
);
});
================================================
FILE: packages/engine/src/component/CSSCodeEditor/style.module.scss
================================================
.cssCodeEditor {
:global {
.monaco-editor .suggest-widget {
width: 300px !important;
left: 0 !important;
right: auto !important;
transform: none !important; // 防止默认偏移
}
}
}
================================================
FILE: packages/engine/src/component/CSSEditor/index.tsx
================================================
import { waitReactUpdate } from '@/utils';
import { formatCSSTextProperty, StyleArr, styleList2Text } from '@/utils/css';
import { MinusCircleOutlined, PlusOutlined } from '@ant-design/icons';
import { Card, Collapse, Dropdown, Space } from 'antd';
import CheckableTag from 'antd/es/tag/CheckableTag';
import { MutableRefObject, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { CSSPropertiesEditor, CSSPropertiesEditorRef } from '../CSSPropertiesEditor';
import styles from './style.module.scss';
import { isEmpty } from 'lodash-es';
// state: 'normal' | 'hover' | 'active' | 'focus' | 'first' | 'last' | 'even' | 'odd';
const DOM_CSS_STATUS = [
'normal' as const,
'hover' as const,
'focus' as const,
'focus-within' as const,
'focus-visible' as const,
'checked' as const,
'disable' as const,
'active' as const,
];
type DomCSSStatusType = typeof DOM_CSS_STATUS[number];
const DOM_CSS_STATUS_LIST = DOM_CSS_STATUS.map((el) => {
return {
key: el,
label: el,
};
});
type MediaQueryItem = {
key: string;
maxWidth: string;
label: string;
};
export type CSSVal = Partial<
Record<
DomCSSStatusType,
Record<
/** media query key */
string,
string
>
>
>;
export type CSSEditorRef = {
setValue: (val: CSSVal) => void;
};
export type CSSEditorProps = {
onValueChange?: (val: CSSVal) => void;
initialValue?: CSSVal;
handler?: MutableRefObject;
};
export const CSSEditor = (props: CSSEditorProps) => {
const [selectedStateTag, setSelectedStateTag] = useState('normal');
const [mediaQueryList] = useState([
{
key: '991',
maxWidth: '991',
label: 'Medial Query ( <= 991 px )',
},
{
key: '767',
maxWidth: '767',
label: 'Medial Query ( <= 767 px )',
},
{
key: '479',
maxWidth: '479',
label: 'Medial Query ( <= 479 px )',
},
]);
const cssPropertyRefMap = useRef>({});
const handleChange = (tag: DomCSSStatusType) => {
setSelectedStateTag(tag);
};
const [domStatusList, setDomStatusList] = useState([]);
const cssStatusList = useMemo(() => {
return DOM_CSS_STATUS_LIST.filter((el) => {
return !domStatusList.includes(el.key);
});
}, [domStatusList]);
const selectCssStatusList = useMemo(() => {
return DOM_CSS_STATUS_LIST.filter((el) => {
return domStatusList.includes(el.key);
});
}, [domStatusList]);
const [cssVal, setCssVal] = useState(props.initialValue ?? {});
useEffect(() => {
const list = Object.keys(cssVal);
setDomStatusList(list);
}, [cssVal]);
const currentCssStateVal = useMemo(() => {
const res = cssVal?.[selectedStateTag];
if (!res) {
return {};
}
const newVal: Record> = {};
Object.keys(res).forEach((key) => {
newVal[key] = formatCSSTextProperty(res[key] || '');
});
return newVal;
}, [selectedStateTag, cssVal]);
const initVal = useCallback(() => {
Object.keys(cssPropertyRefMap.current).forEach((key) => {
const ref = cssPropertyRefMap.current?.[key];
const cssVal = currentCssStateVal[key] || [];
if (ref) {
ref.setValue(cssVal);
}
});
}, [currentCssStateVal]);
const initRef = useRef<() => void>();
initRef.current = initVal;
if (props.handler) {
props.handler.current = {
setValue: async (newVal) => {
if (isEmpty(newVal)) {
setCssVal({
normal: {
normal: '',
},
});
} else {
setCssVal(newVal);
}
await waitReactUpdate();
initRef.current?.();
},
};
}
// 初始化赋值
useEffect(() => {
initRef.current?.();
}, [selectedStateTag]);
const updateCss = useCallback(
(mediaKey: string, val: StyleArr) => {
const newVal = {
...cssVal,
[selectedStateTag]: {
...(cssVal[selectedStateTag] || {}),
[mediaKey]: styleList2Text(val),
},
};
props.onValueChange?.(newVal);
},
[cssVal, props, selectedStateTag]
);
return (
<>
CSS}
extra={
{
setDomStatusList((oldVal) => {
return [...oldVal, el.key];
});
},
}}
>
}
>
{selectCssStatusList.map((tag) => {
const checked = selectedStateTag.includes(tag.key);
return (
handleChange(tag.key)}
className={styles.stateTag}
>
{tag.label}
{tag.key !== 'normal' && (
{
e.stopPropagation();
e.preventDefault();
setDomStatusList((oldVal) => {
return oldVal.filter((el) => {
return el !== tag.key;
});
});
setSelectedStateTag('normal');
}}
/>
)}
);
})}
{
await waitReactUpdate();
initRef.current?.();
}}
items={[
{
key: 'normal',
label: Default ,
children: (
{
cssPropertyRefMap.current['normal'] = ref;
}}
onValueChange={(val) => updateCss('normal', val)}
initialValue={currentCssStateVal['normal']}
/>
),
},
...mediaQueryList.map((el) => {
return {
key: el.key,
label: {el.label} ,
children: (
{
cssPropertyRefMap.current[el.key] = ref;
}}
onValueChange={(val) => updateCss(el.key, val)}
/>
),
};
}),
]}
>
>
);
};
================================================
FILE: packages/engine/src/component/CSSEditor/style.module.scss
================================================
.stateTag {
position: relative;
.stateTagClose {
position: absolute;
right: -10px;
top: -10px;
background-color: white;
color: #4d4d4d;
opacity: 0;
font-size: 20px;
transform: scale(0.6);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
overflow: hidden;
border-radius: 10px;
}
&:hover {
.stateTagClose {
opacity: 1;
}
}
}
================================================
FILE: packages/engine/src/component/CSSPropertiesEditor/cssProperties.ts
================================================
const CSSSourceInfo = {
cssColors: {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgreen: '#006400',
darkgrey: '#a9a9a9',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
goldenrod: '#daa520',
gold: '#ffd700',
gray: '#808080',
green: '#008000',
greenyellow: '#adff2f',
grey: '#808080',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavenderblush: '#fff0f5',
lavender: '#e6e6fa',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgreen: '#90ee90',
lightgrey: '#d3d3d3',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370db',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#db7093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
rebeccapurple: '#663399',
red: '#ff0000',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd32',
},
cssKeyValue: {
'align-content': {
values: [
'center',
'flex-end',
'flex-start',
'space-around',
'space-between',
'stretch',
],
},
'align-items': {
values: ['baseline', 'center', 'flex-end', 'flex-start', 'stretch'],
},
'align-self': {
values: [
'auto',
'normal',
'self-start',
'self-end',
'baseline',
'center',
'start',
'end',
'flex-end',
'flex-start',
'safe',
'stretch',
'unsafe',
],
},
all: { values: [] },
animation: { values: [] },
'animation-delay': { values: [] },
'animation-direction': {
values: ['alternate', 'alternate-reverse', 'normal', 'reverse'],
},
'animation-duration': { values: [] },
'animation-fill-mode': {
values: ['backwards', 'both', 'forwards', 'none'],
},
'animation-iteration-count': { values: ['infinite'] },
'animation-name': { values: ['none'] },
'animation-play-state': { values: ['paused', 'running'] },
'animation-timing-function': {
values: [
'cubic-bezier()',
'ease',
'ease-in',
'ease-in-out',
'ease-out',
'linear',
'step-end',
'step-start',
'steps()',
],
},
'backface-visibility': { values: ['hidden', 'visible'] },
background: { values: [], type: 'color' },
'background-attachment': { values: ['fixed', 'local', 'scroll'] },
'background-blend-mode': {
values: [
'color',
'color-burn',
'color-dodge',
'darken',
'difference',
'exclusion',
'hard-light',
'hue',
'lighten',
'luminosity',
'multiply',
'normal',
'overlay',
'saturation',
'screen',
'soft-light',
],
},
'background-clip': { values: ['border-box', 'content-box', 'padding-box'] },
'background-color': { values: [], type: 'color' },
'background-image': {
values: [
'image()',
'linear-gradient()',
'radial-gradient()',
'repeating-linear-gradient()',
'repeating-radial-gradient()',
'url()',
],
},
'background-origin': {
values: ['border-box', 'content-box', 'padding-box'],
},
'background-position': {
values: ['left', 'center', 'right', 'bottom', 'top'],
},
'background-repeat': {
values: ['no-repeat', 'repeat', 'repeat-x', 'repeat-y', 'round', 'space'],
},
'background-size': { values: ['auto', 'contain', 'cover'] },
border: { values: [] },
'border-collapse': { values: ['collapse', 'separate'] },
'border-color': { values: [], type: 'color' },
'border-spacing': { values: [] },
'border-style': {
values: [
'dashed',
'dotted',
'double',
'groove',
'hidden',
'inset',
'none',
'outset',
'ridge',
'solid',
],
},
'border-bottom': { values: [] },
'border-bottom-color': { values: [], type: 'color' },
'border-bottom-left-radius': { values: [] },
'border-bottom-right-radius': { values: [] },
'border-bottom-style': {
values: [
'dashed',
'dotted',
'double',
'groove',
'hidden',
'inset',
'none',
'outset',
'ridge',
'solid',
],
},
'border-bottom-width': { values: ['medium', 'thin', 'thick'] },
'border-image': { values: ['url()'] },
'border-image-outset': { values: [] },
'border-image-slice': { values: [] },
'border-image-source': { values: [] },
'border-image-repeat': { values: ['repeat', 'round', 'space', 'stretch'] },
'border-image-width': { values: ['auto'] },
'border-left': { values: [] },
'border-left-color': { values: [], type: 'color' },
'border-left-style': {
values: [
'dashed',
'dotted',
'double',
'groove',
'hidden',
'inset',
'none',
'outset',
'ridge',
'solid',
],
},
'border-left-width': { values: ['medium', 'thin', 'thick'] },
'border-radius': { values: [] },
'border-right': { values: [] },
'border-right-color': { values: [], type: 'color' },
'border-right-style': {
values: [
'dashed',
'dotted',
'double',
'groove',
'hidden',
'inset',
'none',
'outset',
'ridge',
'solid',
],
},
'border-right-width': { values: ['medium', 'thin', 'thick'] },
'border-top': { values: [] },
'border-top-color': { values: [], type: 'color' },
'border-top-left-radius': { values: [] },
'border-top-right-radius': { values: [] },
'border-top-style': {
values: [
'dashed',
'dotted',
'double',
'groove',
'hidden',
'inset',
'none',
'outset',
'ridge',
'solid',
],
},
'border-top-width': { values: ['medium', 'thin', 'thick'] },
'border-width': { values: ['medium', 'thin', 'thick'] },
'box-decoration-break': { values: ['clone', 'slice'] },
'box-shadow': { values: ['none'] },
'box-sizing': { values: ['border-box', 'content-box'] },
bottom: { values: ['auto'] },
'break-after': {
values: [
'always',
'auto',
'avoid',
'avoid-column',
'avoid-page',
'avoid-region',
'column',
'left',
'page',
'region',
'right',
],
},
'break-before': {
values: [
'always',
'auto',
'avoid',
'avoid-column',
'avoid-page',
'avoid-region',
'column',
'left',
'page',
'region',
'right',
],
},
'break-inside': {
values: ['auto', 'avoid', 'avoid-column', 'avoid-page', 'avoid-region'],
},
'caption-side': { values: ['bottom', 'top'] },
'caret-color': { values: ['auto'], type: 'color' },
clear: { values: ['both', 'left', 'none', 'right'] },
clip: { values: ['auto'] },
color: { values: [], type: 'color' },
columns: { values: [] },
'column-count': { values: [] },
'column-fill': { values: ['auto', 'balance'] },
'column-gap': { values: ['normal'] },
'column-rule': { values: [] },
'column-rule-color': { values: [], type: 'color' },
'column-rule-style': {
values: [
'dashed',
'dotted',
'double',
'groove',
'hidden',
'inset',
'none',
'outset',
'ridge',
'solid',
],
},
'column-rule-width': { values: ['medium', 'thin', 'thick'] },
'column-span': { values: ['all', 'none'] },
'column-width': { values: ['auto'] },
content: {
values: [
'attr()',
'close-quote',
'no-close-quote',
'no-open-quote',
'normal',
'none',
'open-quote',
],
},
'counter-increment': { values: ['none'] },
'counter-reset': { values: ['none'] },
cursor: {
values: [
'alias',
'all-scroll',
'auto',
'cell',
'col-resize',
'context-menu',
'copy',
'crosshair',
'default',
'e-resize',
'ew-resize',
'grab',
'grabbing',
'help',
'move',
'n-resize',
'ne-resize',
'nesw-resize',
'no-drop',
'none',
'not-allowed',
'ns-resize',
'nw-resize',
'nwse-resize',
'pointer',
'progress',
'row-resize',
's-resize',
'se-resize',
'sw-resize',
'text',
'vertical-text',
'w-resize',
'wait',
'zoom-in',
'zoom-out',
],
},
direction: { values: ['ltr', 'rtl'] },
display: {
values: [
'block',
'contents',
'flex',
'flow-root',
'grid',
'inline',
'inline-block',
'inline-flex',
'inline-grid',
'inline-table',
'list-item',
'none',
'run-in',
'subgrid',
'table',
'table-caption',
'table-cell',
'table-column',
'table-column-group',
'table-footer-group',
'table-header-group',
'table-row',
'table-row-group',
],
},
'empty-cells': { values: ['hide', 'show'] },
fill: { values: [] },
filter: {
values: [
'blur()',
'brightness()',
'contrast()',
'custom()',
'drop-shadow()',
'grayscale()',
'hue-rotate()',
'invert()',
'none',
'opacity()',
'sepia()',
'saturate()',
'url()',
],
},
flex: { values: ['auto', 'none'] },
'flex-basis': { values: ['auto'] },
'flex-direction': {
values: ['column', 'column-reverse', 'row', 'row-reverse'],
},
'flex-flow': {
values: [
'column',
'column-reverse',
'nowrap',
'row',
'row-reverse',
'wrap',
'wrap-reverse',
],
},
'flex-grow': { values: [] },
'flex-shrink': { values: [] },
'flex-wrap': { values: ['nowrap', 'wrap', 'wrap-reverse'] },
float: { values: ['left', 'right', 'none', 'inline-start', 'inline-end'] },
'flow-into': { values: ['none'], type: 'named-flow' },
'flow-from': { values: ['none'], type: 'named-flow' },
font: { values: [] },
'font-display': {
values: ['auto', 'block', 'swap', 'fallback', 'optional'],
},
'font-family': {
values: [
'auto',
'cursive',
'fantasy',
'monospace',
'sans-serif',
'serif',
],
},
'font-feature-settings': { values: ['normal'] },
'font-kerning': { values: ['auto', 'none', 'normal'] },
'font-language-override': { values: ['normal'] },
'font-size': {
values: [
'xx-small',
'x-small',
'small',
'medium',
'large',
'x-large',
'xx-large',
'xxx-large',
'larger',
'smaller',
],
},
'font-size-adjust': { values: ['auto', 'none'] },
'font-stretch': {
values: [
'condensed',
'expanded',
'extra-condensed',
'extra-expanded',
'normal',
'semi-condensed',
'semi-expanded',
'ultra-condensed',
'ultra-expanded',
],
},
'font-style': { values: ['italic', 'normal', 'oblique'] },
'font-synthesis': { values: ['none', 'style', 'weight'] },
'font-variant': { values: ['normal', 'small-caps'] },
'font-variant-alternates': { values: ['normal'] },
'font-variant-caps': {
values: [
'normal',
'small-caps',
'all-small-caps',
'petite-caps',
'all-petite-caps',
'unicase',
'titling-caps',
],
},
'font-variant-east-asian': { values: ['normal'] },
'font-variant-ligatures': { values: ['normal', 'none'] },
'font-variant-numeric': { values: ['normal'] },
'font-variant-position': { values: ['normal', 'sub', 'super'] },
'font-weight': {
values: [
'bold',
'bolder',
'lighter',
'normal',
'100',
'200',
'300',
'400',
'500',
'600',
'700',
'800',
'900',
],
},
gap: { values: ['revert-layer'] },
grid: { values: [] },
'grid-area': { values: [] },
'grid-auto-columns': { values: [] },
'grid-auto-flow': { values: ['row', 'column', 'dense'] },
'grid-auto-rows': { values: [] },
'grid-column': { values: ['auto'] },
'grid-column-end': { values: [] },
'grid-column-gap': { values: [] },
'grid-column-start': { values: [] },
'grid-gap': { values: [] },
'grid-row': { values: ['auto'] },
'grid-row-end': { values: [] },
'grid-row-start': { values: [] },
'grid-row-gap': { values: [] },
'grid-template': { values: ['none'] },
'grid-template-areas': { values: [] },
'grid-template-columns': { values: ['auto'] },
'grid-template-rows': { values: ['auto'] },
'hanging-punctuation': {
values: ['allow-end', 'first', 'force-end', 'last', 'none'],
},
height: { values: ['auto', 'max-content', 'min-content', 'fit-content'] },
hyphens: { values: ['auto', 'manual', 'none'] },
'image-orientation': { values: [] },
'image-resolution': { values: ['from-image', 'snap'] },
isolation: { values: ['auto', 'isolate'] },
'justify-content': {
values: [
'center',
'flex-end',
'flex-start',
'space-around',
'space-between',
],
},
'justify-items': {
values: [
'auto',
'normal',
'stretch',
'center',
'start',
'end',
'flex-start',
'flex-end',
'self-start',
'self-end',
'left',
'right',
'baseline',
'first',
'last',
'safe',
'unsafe',
'legacy',
],
},
'justify-self': {
values: [
'auto',
'normal',
'stretch',
'center',
'start',
'end',
'flex-start',
'flex-end',
'self-start',
'self-end',
'left',
'right',
'baseline',
'first',
'last',
'safe',
'unsafe',
],
},
left: { values: ['auto'] },
'letter-spacing': { values: ['normal'] },
'line-height': { values: ['normal'] },
'list-style': {
values: [
'none',
'url()',
'armenian',
'circle',
'decimal',
'decimal-leading-zero',
'disc',
'georgian',
'inside',
'lower-alpha',
'lower-greek',
'lower-latin',
'lower-roman',
'outside',
'square',
'upper-alpha',
'upper-latin',
'upper-roman',
],
},
'list-style-image': { values: ['none', 'url()'] },
'list-style-position': { values: ['inside', 'outside'] },
'list-style-type': {
values: [
'armenian',
'circle',
'decimal',
'decimal-leading-zero',
'disc',
'georgian',
'lower-alpha',
'lower-greek',
'lower-latin',
'lower-roman',
'none',
'square',
'upper-alpha',
'upper-latin',
'upper-roman',
],
},
margin: { values: ['auto'] },
'margin-bottom': { values: ['auto'] },
'margin-left': { values: ['auto'] },
'margin-right': { values: ['auto'] },
'margin-top': { values: ['auto'] },
'max-height': { values: ['none'] },
'max-width': { values: ['none'] },
'min-height': { values: [] },
'min-width': { values: [] },
'mix-blend-mode': {
values: [
'color',
'color-burn',
'color-dodge',
'darken',
'difference',
'exclusion',
'hard-light',
'hue',
'lighten',
'luminosity',
'multiply',
'normal',
'overlay',
'saturation',
'screen',
'soft-light',
],
},
'object-fit': {
values: ['contain', 'cover', 'fill', 'none', 'scale-down'],
},
'object-position': { values: ['left', 'center', 'right', 'bottom', 'top'] },
opacity: { values: [] },
order: { values: [] },
orphans: { values: [] },
outline: { values: [] },
'outline-color': { values: ['invert'], type: 'color' },
'outline-offset': { values: [] },
'outline-style': {
values: [
'dashed',
'dotted',
'double',
'groove',
'hidden',
'inset',
'none',
'outset',
'ridge',
'solid',
],
},
'outline-width': { values: ['medium', 'thin', 'thick'] },
overflow: { values: ['auto', 'hidden', 'scroll', 'visible'] },
'overflow-x': { values: ['auto', 'hidden', 'scroll', 'visible'] },
'overflow-y': { values: ['auto', 'hidden', 'scroll', 'visible'] },
'overflow-wrap': { values: ['normal', 'anywhere', 'break-word'] },
padding: { values: [] },
'padding-bottom': { values: [] },
'padding-left': { values: [] },
'padding-right': { values: [] },
'padding-top': { values: [] },
'page-break-after': {
values: ['always', 'auto', 'avoid', 'left', 'right'],
},
'page-break-before': {
values: ['always', 'auto', 'avoid', 'left', 'right'],
},
'page-break-inside': { values: ['auto', 'avoid'] },
perspective: { values: ['none'] },
'perspective-origin': {
values: ['bottom', 'center', 'left', 'right', 'top'],
},
'pointer-events': {
values: [
'all',
'auto',
'fill',
'none',
'painted',
'stroke',
'visible',
'visibleFill',
'visiblePainted',
'visibleStroke',
],
},
position: { values: ['absolute', 'fixed', 'relative', 'static', 'sticky'] },
quotes: { values: ['none'] },
'region-break-after': {
values: [
'always',
'auto',
'avoid',
'avoid-column',
'avoid-page',
'avoid-region',
'column',
'left',
'page',
'region',
'right',
],
},
'region-break-before': {
values: [
'always',
'auto',
'avoid',
'avoid-column',
'avoid-page',
'avoid-region',
'column',
'left',
'page',
'region',
'right',
],
},
'region-break-inside': {
values: ['auto', 'avoid', 'avoid-column', 'avoid-page', 'avoid-region'],
},
'region-fragment': { values: ['auto', 'break'] },
resize: { values: ['both', 'horizontal', 'none', 'vertical'] },
right: { values: ['auto'] },
'scroll-behavior': { values: ['auto', 'smooth'] },
'scroll-snap-type': {
values: [
'none',
'x',
'y',
'block',
'inline',
'both',
'mandatory',
'proximity',
],
},
src: { values: ['url()'] },
'shape-image-threshold': { values: [] },
'shape-inside': {
values: [
'auto',
'circle()',
'ellipse()',
'outside-shape',
'polygon()',
'rectangle()',
],
},
'shape-margin': { values: [] },
'shape-outside': {
values: [
'none',
'circle()',
'ellipse()',
'polygon()',
'inset()',
'margin-box',
'border-box',
'padding-box',
'content-box',
'url()',
'image()',
'linear-gradient()',
'radial-gradient()',
'repeating-linear-gradient()',
'repeating-radial-gradient()',
],
},
'tab-size': { values: [] },
'table-layout': { values: ['auto', 'fixed'] },
'text-align': {
values: [
'start',
'end',
'center',
'left',
'justify',
'right',
'match-parent',
'justify-all',
],
},
'text-align-last': { values: ['center', 'left', 'justify', 'right'] },
'text-decoration': {
values: ['line-through', 'none', 'overline', 'underline'],
},
'text-decoration-color': { values: [], type: 'color' },
'text-decoration-line': {
values: ['line-through', 'none', 'overline', 'underline'],
},
'text-decoration-skip': {
values: ['edges', 'ink', 'none', 'objects', 'spaces'],
},
'text-decoration-style': {
values: ['dashed', 'dotted', 'double', 'solid', 'wavy'],
},
'text-emphasis': { values: [] },
'text-emphasis-color': { values: [], type: 'color' },
'text-emphasis-position': { values: ['above', 'below', 'left', 'right'] },
'text-emphasis-style': {
values: [
'circle',
'dot',
'double-circle',
'filled',
'none',
'open',
'sesame',
'triangle',
],
},
'text-indent': { values: [] },
'text-justify': {
values: ['auto', 'none', 'inter-word', 'inter-character'],
},
'text-overflow': { values: ['clip', 'ellipsis'] },
'text-shadow': { values: [] },
'text-rendering': {
values: [
'auto',
'geometricPrecision',
'optimizeLegibility',
'optimizeSpeed',
],
},
'text-transform': {
values: ['capitalize', 'full-width', 'lowercase', 'none', 'uppercase'],
},
'text-underline-position': {
values: ['alphabetic', 'auto', 'below', 'left', 'right'],
},
top: { values: ['auto'] },
transform: {
values: [
'matrix()',
'matrix3d()',
'none',
'perspective()',
'rotate()',
'rotate3d()',
'rotateX()',
'rotateY()',
'rotateZ()',
'scale()',
'scale3d()',
'scaleX()',
'scaleY()',
'scaleZ()',
'skewX()',
'skewY()',
'translate()',
'translate3d()',
'translateX()',
'translateY()',
'translateZ()',
],
},
'transform-origin': {
values: ['bottom', 'center', 'left', 'right', 'top'],
},
'transform-style': { values: ['flat', 'preserve-3d'] },
transition: { values: [] },
'transition-delay': { values: [] },
'transition-duration': { values: [] },
'transition-property': { values: ['all', 'none'] },
'transition-timing-function': {
values: [
'cubic-bezier()',
'ease',
'ease-in',
'ease-in-out',
'ease-out',
'linear',
'step-end',
'step-start',
'steps()',
],
},
'unicode-bidi': { values: ['bidi-override', 'embed', 'normal'] },
'unicode-range': { values: [] },
'user-select': { values: ['all', 'auto', 'contain', 'none', 'text'] },
'vertical-align': {
values: [
'baseline',
'bottom',
'middle',
'sub',
'super',
'text-bottom',
'text-top',
'top',
],
},
visibility: { values: ['collapse', 'hidden', 'visible'] },
'white-space': {
values: ['normal', 'nowrap', 'pre', 'pre-line', 'pre-wrap'],
},
widows: { values: [] },
width: { values: ['auto', 'max-content', 'min-content', 'fit-content'] },
'will-change': {
values: ['auto', 'contents', 'opacity', 'scroll-position'],
},
'word-break': { values: ['normal', 'break-all', 'keep-all'] },
'word-spacing': { values: ['normal'] },
'z-index': { values: ['auto'] },
},
};
export const CSSProperties = CSSSourceInfo.cssKeyValue;
export const CSSColors = CSSSourceInfo.cssColors;
export type CSSPropertiesKey = keyof typeof CSSProperties;
export const CSSPropertyList = Object.keys(CSSSourceInfo.cssKeyValue);
================================================
FILE: packages/engine/src/component/CSSPropertiesEditor/index.tsx
================================================
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { CSSPropertyList } from './cssProperties';
import styles from './style.module.scss';
import { ConfigProvider } from 'antd';
import { SinglePropertyEditorRef, SinglePropertyEditor } from './signleProperty';
// eslint-disable-next-line react-refresh/only-export-components
export const defaultPropertyOptions = CSSPropertyList.map((el) => {
return {
value: el,
};
});
export type CSSPropertiesEditorProps = {
initialValue?: { property: string; value: string }[];
onValueChange?: (val: { property: string; value: string }[]) => void;
};
export type CSSPropertiesEditorRef = {
setValue: (val: { property: string; value: string }[]) => void;
};
export const CSSPropertiesEditor = forwardRef(
function CSSPropertiesEditorCore(props, ref) {
const [propertyList, setPropertyList] = useState<{ id?: string; property: string; value: any }[]>([]);
const [newAddVal, setNewAddVal] = useState({
property: '',
value: '',
});
useImperativeHandle(
ref,
() => {
return {
setValue: (val) => {
setPropertyList(val);
},
};
},
[]
);
useEffect(() => {
if (props.initialValue) {
setPropertyList([...props.initialValue]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const innerOnValueChange = (val: typeof propertyList) => {
props.onValueChange?.(val);
};
const createPropertyRef = useRef(null);
return (
{propertyList.map((el, index) => {
return (
{
propertyList[index] = newVal;
const newList = JSON.parse(JSON.stringify(propertyList));
setPropertyList(newList);
innerOnValueChange(newList);
}}
onDelete={() => {
propertyList.splice(index, 1);
const newList = JSON.parse(JSON.stringify(propertyList));
setPropertyList(newList);
innerOnValueChange(newList);
}}
/>
);
})}
{
setNewAddVal({
...newCssVal,
});
}}
onCreate={(newVal) => {
if (newVal.property && newVal.value) {
const newList = [...propertyList];
newList.push(newVal);
setPropertyList(newList);
setNewAddVal({
property: '',
value: '',
});
innerOnValueChange(newList);
createPropertyRef.current?.reset();
}
}}
/>
);
}
);
================================================
FILE: packages/engine/src/component/CSSPropertiesEditor/signleProperty.tsx
================================================
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
import { AutoComplete, Button } from 'antd';
import { InputStatus } from 'antd/es/_util/statusUtils';
import clsx from 'clsx';
import { BaseSelectRef } from 'rc-select';
import { forwardRef, useState, useMemo, useRef, useImperativeHandle } from 'react';
import { defaultPropertyOptions } from '.';
import { CSSProperties, CSSPropertiesKey } from './cssProperties';
import styles from './style.module.scss';
export type SinglePropertyEditorProps = {
mode: 'create' | 'edit';
value?: {
property: string;
value: string;
};
allValues: {
property: string;
value: string;
}[];
onValueChange?: (value: { property: string; value: string }) => void;
onCreate?: (value: { property: string; value: string }) => void;
onDelete?: () => void;
};
export type SinglePropertyEditorRef = {
reset: () => void;
};
export const SinglePropertyEditor = forwardRef(
function SinglePropertyEditorCore(props, ref) {
const [keyFormatStatus] = useState('');
const [valueFormatStatus] = useState('');
const { mode = 'edit' } = props;
const isCreate = useMemo(() => {
return mode === 'create';
}, [mode]);
const innerValue = props.value;
const [propertyOptions, setPropertyOptions] = useState(defaultPropertyOptions);
const [valueOptions, setValueOptions] = useState<{ value: string }[]>([]);
const [allValueOptions, setAllValueOptions] = useState<{ value: string }[]>([]);
const onSearch = (searchText: string) => {
const newOptions = defaultPropertyOptions.filter((el) => el.value.includes(searchText));
if (!searchText) {
setPropertyOptions(defaultPropertyOptions);
} else {
setPropertyOptions(newOptions);
}
};
const updateValueOptions = () => {
let res: any[] = [];
const tempProperty = CSSProperties[innerValue?.property as unknown as CSSPropertiesKey];
if (tempProperty) {
res =
tempProperty.values?.map((val) => {
return {
value: val,
};
}) || [];
}
setValueOptions(res);
setAllValueOptions(res);
};
const onValueSearch = (searchText: string) => {
const newOptions = allValueOptions.filter((el) => el.value.includes(searchText));
if (!searchText) {
setValueOptions(allValueOptions);
} else {
setValueOptions(newOptions);
}
};
const updateKeyValue = (keyVal: string) => {
props.onValueChange?.({
property: keyVal,
value: innerValue?.value || '',
});
return true;
};
const propertyValueRef = useRef(null);
const propertyKeyRef = useRef(null);
const [focusState, setFocusState] = useState({
key: false,
value: false,
});
useImperativeHandle(
ref,
() => {
return {
reset: () => {
propertyKeyRef.current?.focus();
},
};
},
[]
);
return (
{
updateKeyValue(val);
}}
style={{
width: '100%',
position: 'absolute',
left: 0,
top: 0,
}}
onFocus={() => {
setFocusState({
key: true,
value: false,
});
}}
onBlur={() => {
setFocusState({
key: false,
value: false,
});
}}
onKeyDown={(e) => {
if (e.code === 'Enter') {
if (!keyFormatStatus) {
propertyValueRef.current?.focus();
}
}
}}
placeholder="property"
options={propertyOptions}
/>
{innerValue?.property}
:
{
updateValueOptions();
props.onValueChange?.({
property: innerValue?.property || '',
value: val || '',
});
}}
style={{
flex: 1,
}}
onFocus={() => {
setFocusState({
key: false,
value: true,
});
}}
onBlur={() => {
setFocusState({
key: false,
value: false,
});
}}
className={clsx([styles.inputAuto, focusState.value && styles.active])}
placeholder="value"
onSearch={onValueSearch}
options={valueOptions}
onKeyDown={(e) => {
if (e.code === 'Enter') {
if (isCreate) {
props.onCreate?.({
property: innerValue?.property || '',
value: innerValue?.value || '',
});
}
}
}}
>
{props.onDelete && mode === 'edit' && (
{
props.onDelete?.();
}}
>
)}
{props.onCreate && mode === 'create' && (
}
onClick={() => {
props.onCreate?.({
property: innerValue?.property || '',
value: innerValue?.value || '',
});
}}
>
)}
);
}
);
================================================
FILE: packages/engine/src/component/CSSPropertiesEditor/style.module.scss
================================================
.cssFieldBox {
display: flex;
align-items: center;
color: $textColor !important;
:global {
.ant-select-selector {
padding: 0 !important;
}
.ant-select-single .ant-select-selector .ant-select-selection-search {
inset-inline-start: 0px;
inset-inline-end: 0px;
}
.ant-select-selector input {
color: $textColor !important;
}
}
.inputAuto {
border-bottom: 1px solid rgba(128, 128, 128, 0.23);
transition: all 0.1s;
height: 30px;
&.error {
border-bottom: 1px solid red !important;
}
}
.keyField {
position: relative;
font-size: 14px;
height: 30px;
&.notEdit {
border-bottom: 1px solid transparent !important;
}
}
.active {
border-bottom: 1px solid rgb(128, 177, 255);
}
}
.cssBox {
:global {
.ant-collapse-borderless > .ant-collapse-item {
border-bottom: 1px solid #e4e4e4;
}
.ant-collapse-borderless > .ant-collapse-item:last-child {
border-bottom: none;
}
.ant-collapse-content-box {
padding-bottom: 30px !important;
}
}
}
================================================
FILE: packages/engine/src/component/CSSPropertiesEditor/util.ts
================================================
export const getTextWidth = async (text: string, fontSize = 14) => {
return new Promise((resolve) => {
const span = document.createElement('span');
span.innerHTML = text;
span.style.fontSize = `${fontSize}px`;
span.style.position = 'fixed';
span.style.left = '-1000px';
span.style.bottom = '-1000px';
span.style.display = 'inline-block';
document.body.appendChild(span);
setTimeout(() => {
const style = window.getComputedStyle(span);
resolve(style.width);
setTimeout(() => {
document.body.removeChild(span);
});
});
});
};
================================================
FILE: packages/engine/src/component/CSSPropertiesVariableBindEditor/SingleProperty.tsx
================================================
import { AutoComplete, Button, ConfigProvider } from 'antd';
import { MinusOutlined, PlusOutlined } from '@ant-design/icons';
import { BaseSelectRef } from 'rc-select';
import clsx from 'clsx';
import { CNodePropsTypeEnum, JSExpressionPropType } from '@chamn/model';
import { InputStatus } from 'antd/es/_util/statusUtils';
import { CSSPropertyList } from './cssProperties';
import { forwardRef, useState, useEffect, useRef, useImperativeHandle } from 'react';
import { ExpressionSetter } from '../CustomSchemaForm/components/Setters/ExpressionSetter';
import { UseCRightPanelContext } from '@/plugins/RightPanel/context';
import styles from './style.module.scss';
const defaultPropertyOptions = CSSPropertyList.map((el) => {
return {
value: el,
};
});
export type InnerSinglePropertyEditorProps = {
value?: {
property: string;
value: JSExpressionPropType | string;
};
onValueChange: (value: { property: string; value: JSExpressionPropType | string }) => void;
onDelete?: () => void;
onCreate?: (value: { property: string; value: JSExpressionPropType | string }) => {
errorKey?: string[];
} | void;
mod?: 'create' | 'edit';
};
export type InnerSinglePropertyEditorRef = {
reset: () => void;
};
export const SinglePropertyEditor = forwardRef(
function SinglePropertyEditorCore(props, ref) {
const [keyFormatStatus, setKeyFormatStatus] = useState('');
const rightPanelContext = UseCRightPanelContext();
const { mod = 'create' } = props;
const [innerValue, setInnerVal] = useState<{
property: string;
value: JSExpressionPropType | string;
}>({
property: props.value?.property || '',
value: props.value?.value || '',
});
useEffect(() => {
if (props.value) {
setInnerVal(props.value);
}
}, [props.value]);
const [propertyOptions, setPropertyOptions] = useState(defaultPropertyOptions);
const onSearch = (searchText: string) => {
const newOptions = defaultPropertyOptions.filter((el) => el.value.includes(searchText));
if (!searchText) {
setPropertyOptions(defaultPropertyOptions);
} else {
setPropertyOptions(newOptions);
}
};
const updateOuterValue = (newValue: typeof innerValue) => {
props.onValueChange({
...newValue,
});
};
const propertyKeyRef = useRef(null);
useImperativeHandle(
ref,
() => {
return {
reset: () => {
setInnerVal({
property: '',
value: {
type: CNodePropsTypeEnum.EXPRESSION,
value: '',
},
});
propertyKeyRef.current?.focus();
},
};
},
[]
);
/// 创建时
const innerOnCreate = () => {
if (innerValue.property === '') {
setKeyFormatStatus('error');
return;
}
setKeyFormatStatus('');
const res = props.onCreate?.(innerValue);
if (res?.errorKey?.includes('key')) {
setKeyFormatStatus('error');
}
};
return (
<>
Name
{
setKeyFormatStatus('');
const newVal = {
...innerValue,
property: val,
};
setInnerVal(newVal);
updateOuterValue(newVal);
}}
className={clsx([styles.inputBox])}
onBlur={() => {
updateOuterValue(innerValue);
}}
placeholder="property"
options={propertyOptions}
/>
{mod === 'create' && (
}
onClick={innerOnCreate}
>
)}
{mod !== 'create' && (
)}
{props.onDelete && mod === 'edit' && (
{
props.onDelete?.();
}}
>
)}
>
);
}
);
================================================
FILE: packages/engine/src/component/CSSPropertiesVariableBindEditor/cssProperties.ts
================================================
const CSSSourceInfo = {
cssColors: {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgreen: '#006400',
darkgrey: '#a9a9a9',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
goldenrod: '#daa520',
gold: '#ffd700',
gray: '#808080',
green: '#008000',
greenyellow: '#adff2f',
grey: '#808080',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavenderblush: '#fff0f5',
lavender: '#e6e6fa',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgreen: '#90ee90',
lightgrey: '#d3d3d3',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370db',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#db7093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
rebeccapurple: '#663399',
red: '#ff0000',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd32',
},
cssKeyValue: {
'align-content': {
values: ['center', 'flex-end', 'flex-start', 'space-around', 'space-between', 'stretch'],
},
'align-items': {
values: ['baseline', 'center', 'flex-end', 'flex-start', 'stretch'],
},
'align-self': {
values: [
'auto',
'normal',
'self-start',
'self-end',
'baseline',
'center',
'start',
'end',
'flex-end',
'flex-start',
'safe',
'stretch',
'unsafe',
],
},
all: { values: [] },
animation: { values: [] },
'animation-delay': { values: [] },
'animation-direction': {
values: ['alternate', 'alternate-reverse', 'normal', 'reverse'],
},
'animation-duration': { values: [] },
'animation-fill-mode': {
values: ['backwards', 'both', 'forwards', 'none'],
},
'animation-iteration-count': { values: ['infinite'] },
'animation-name': { values: ['none'] },
'animation-play-state': { values: ['paused', 'running'] },
'animation-timing-function': {
values: ['cubic-bezier()', 'ease', 'ease-in', 'ease-in-out', 'ease-out', 'linear', 'step-end', 'step-start', 'steps()'],
},
'backface-visibility': { values: ['hidden', 'visible'] },
background: { values: [], type: 'color' },
'background-attachment': { values: ['fixed', 'local', 'scroll'] },
'background-blend-mode': {
values: [
'color',
'color-burn',
'color-dodge',
'darken',
'difference',
'exclusion',
'hard-light',
'hue',
'lighten',
'luminosity',
'multiply',
'normal',
'overlay',
'saturation',
'screen',
'soft-light',
],
},
'background-clip': { values: ['border-box', 'content-box', 'padding-box'] },
'background-color': { values: [], type: 'color' },
'background-image': {
values: ['image()', 'linear-gradient()', 'radial-gradient()', 'repeating-linear-gradient()', 'repeating-radial-gradient()', 'url()'],
},
'background-origin': {
values: ['border-box', 'content-box', 'padding-box'],
},
'background-position': {
values: ['left', 'center', 'right', 'bottom', 'top'],
},
'background-repeat': {
values: ['no-repeat', 'repeat', 'repeat-x', 'repeat-y', 'round', 'space'],
},
'background-size': { values: ['auto', 'contain', 'cover'] },
border: { values: [] },
'border-collapse': { values: ['collapse', 'separate'] },
'border-color': { values: [], type: 'color' },
'border-spacing': { values: [] },
'border-style': {
values: ['dashed', 'dotted', 'double', 'groove', 'hidden', 'inset', 'none', 'outset', 'ridge', 'solid'],
},
'border-bottom': { values: [] },
'border-bottom-color': { values: [], type: 'color' },
'border-bottom-left-radius': { values: [] },
'border-bottom-right-radius': { values: [] },
'border-bottom-style': {
values: ['dashed', 'dotted', 'double', 'groove', 'hidden', 'inset', 'none', 'outset', 'ridge', 'solid'],
},
'border-bottom-width': { values: ['medium', 'thin', 'thick'] },
'border-image': { values: ['url()'] },
'border-image-outset': { values: [] },
'border-image-slice': { values: [] },
'border-image-source': { values: [] },
'border-image-repeat': { values: ['repeat', 'round', 'space', 'stretch'] },
'border-image-width': { values: ['auto'] },
'border-left': { values: [] },
'border-left-color': { values: [], type: 'color' },
'border-left-style': {
values: ['dashed', 'dotted', 'double', 'groove', 'hidden', 'inset', 'none', 'outset', 'ridge', 'solid'],
},
'border-left-width': { values: ['medium', 'thin', 'thick'] },
'border-radius': { values: [] },
'border-right': { values: [] },
'border-right-color': { values: [], type: 'color' },
'border-right-style': {
values: ['dashed', 'dotted', 'double', 'groove', 'hidden', 'inset', 'none', 'outset', 'ridge', 'solid'],
},
'border-right-width': { values: ['medium', 'thin', 'thick'] },
'border-top': { values: [] },
'border-top-color': { values: [], type: 'color' },
'border-top-left-radius': { values: [] },
'border-top-right-radius': { values: [] },
'border-top-style': {
values: ['dashed', 'dotted', 'double', 'groove', 'hidden', 'inset', 'none', 'outset', 'ridge', 'solid'],
},
'border-top-width': { values: ['medium', 'thin', 'thick'] },
'border-width': { values: ['medium', 'thin', 'thick'] },
'box-decoration-break': { values: ['clone', 'slice'] },
'box-shadow': { values: ['none'] },
'box-sizing': { values: ['border-box', 'content-box'] },
bottom: { values: ['auto'] },
'break-after': {
values: ['always', 'auto', 'avoid', 'avoid-column', 'avoid-page', 'avoid-region', 'column', 'left', 'page', 'region', 'right'],
},
'break-before': {
values: ['always', 'auto', 'avoid', 'avoid-column', 'avoid-page', 'avoid-region', 'column', 'left', 'page', 'region', 'right'],
},
'break-inside': {
values: ['auto', 'avoid', 'avoid-column', 'avoid-page', 'avoid-region'],
},
'caption-side': { values: ['bottom', 'top'] },
'caret-color': { values: ['auto'], type: 'color' },
clear: { values: ['both', 'left', 'none', 'right'] },
clip: { values: ['auto'] },
color: { values: [], type: 'color' },
columns: { values: [] },
'column-count': { values: [] },
'column-fill': { values: ['auto', 'balance'] },
'column-gap': { values: ['normal'] },
'column-rule': { values: [] },
'column-rule-color': { values: [], type: 'color' },
'column-rule-style': {
values: ['dashed', 'dotted', 'double', 'groove', 'hidden', 'inset', 'none', 'outset', 'ridge', 'solid'],
},
'column-rule-width': { values: ['medium', 'thin', 'thick'] },
'column-span': { values: ['all', 'none'] },
'column-width': { values: ['auto'] },
content: {
values: ['attr()', 'close-quote', 'no-close-quote', 'no-open-quote', 'normal', 'none', 'open-quote'],
},
'counter-increment': { values: ['none'] },
'counter-reset': { values: ['none'] },
cursor: {
values: [
'alias',
'all-scroll',
'auto',
'cell',
'col-resize',
'context-menu',
'copy',
'crosshair',
'default',
'e-resize',
'ew-resize',
'grab',
'grabbing',
'help',
'move',
'n-resize',
'ne-resize',
'nesw-resize',
'no-drop',
'none',
'not-allowed',
'ns-resize',
'nw-resize',
'nwse-resize',
'pointer',
'progress',
'row-resize',
's-resize',
'se-resize',
'sw-resize',
'text',
'vertical-text',
'w-resize',
'wait',
'zoom-in',
'zoom-out',
],
},
direction: { values: ['ltr', 'rtl'] },
display: {
values: [
'block',
'contents',
'flex',
'flow-root',
'grid',
'inline',
'inline-block',
'inline-flex',
'inline-grid',
'inline-table',
'list-item',
'none',
'run-in',
'subgrid',
'table',
'table-caption',
'table-cell',
'table-column',
'table-column-group',
'table-footer-group',
'table-header-group',
'table-row',
'table-row-group',
],
},
'empty-cells': { values: ['hide', 'show'] },
fill: { values: [] },
filter: {
values: [
'blur()',
'brightness()',
'contrast()',
'custom()',
'drop-shadow()',
'grayscale()',
'hue-rotate()',
'invert()',
'none',
'opacity()',
'sepia()',
'saturate()',
'url()',
],
},
flex: { values: ['auto', 'none'] },
'flex-basis': { values: ['auto'] },
'flex-direction': {
values: ['column', 'column-reverse', 'row', 'row-reverse'],
},
'flex-flow': {
values: ['column', 'column-reverse', 'nowrap', 'row', 'row-reverse', 'wrap', 'wrap-reverse'],
},
'flex-grow': { values: [] },
'flex-shrink': { values: [] },
'flex-wrap': { values: ['nowrap', 'wrap', 'wrap-reverse'] },
float: { values: ['left', 'right', 'none', 'inline-start', 'inline-end'] },
'flow-into': { values: ['none'], type: 'named-flow' },
'flow-from': { values: ['none'], type: 'named-flow' },
font: { values: [] },
'font-display': {
values: ['auto', 'block', 'swap', 'fallback', 'optional'],
},
'font-family': {
values: ['auto', 'cursive', 'fantasy', 'monospace', 'sans-serif', 'serif'],
},
'font-feature-settings': { values: ['normal'] },
'font-kerning': { values: ['auto', 'none', 'normal'] },
'font-language-override': { values: ['normal'] },
'font-size': {
values: ['xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large', 'xxx-large', 'larger', 'smaller'],
},
'font-size-adjust': { values: ['auto', 'none'] },
'font-stretch': {
values: [
'condensed',
'expanded',
'extra-condensed',
'extra-expanded',
'normal',
'semi-condensed',
'semi-expanded',
'ultra-condensed',
'ultra-expanded',
],
},
'font-style': { values: ['italic', 'normal', 'oblique'] },
'font-synthesis': { values: ['none', 'style', 'weight'] },
'font-variant': { values: ['normal', 'small-caps'] },
'font-variant-alternates': { values: ['normal'] },
'font-variant-caps': {
values: ['normal', 'small-caps', 'all-small-caps', 'petite-caps', 'all-petite-caps', 'unicase', 'titling-caps'],
},
'font-variant-east-asian': { values: ['normal'] },
'font-variant-ligatures': { values: ['normal', 'none'] },
'font-variant-numeric': { values: ['normal'] },
'font-variant-position': { values: ['normal', 'sub', 'super'] },
'font-weight': {
values: ['bold', 'bolder', 'lighter', 'normal', '100', '200', '300', '400', '500', '600', '700', '800', '900'],
},
gap: { values: ['revert-layer'] },
grid: { values: [] },
'grid-area': { values: [] },
'grid-auto-columns': { values: [] },
'grid-auto-flow': { values: ['row', 'column', 'dense'] },
'grid-auto-rows': { values: [] },
'grid-column': { values: ['auto'] },
'grid-column-end': { values: [] },
'grid-column-gap': { values: [] },
'grid-column-start': { values: [] },
'grid-gap': { values: [] },
'grid-row': { values: ['auto'] },
'grid-row-end': { values: [] },
'grid-row-start': { values: [] },
'grid-row-gap': { values: [] },
'grid-template': { values: ['none'] },
'grid-template-areas': { values: [] },
'grid-template-columns': { values: ['auto'] },
'grid-template-rows': { values: ['auto'] },
'hanging-punctuation': {
values: ['allow-end', 'first', 'force-end', 'last', 'none'],
},
height: { values: ['auto', 'max-content', 'min-content', 'fit-content'] },
hyphens: { values: ['auto', 'manual', 'none'] },
'image-orientation': { values: [] },
'image-resolution': { values: ['from-image', 'snap'] },
isolation: { values: ['auto', 'isolate'] },
'justify-content': {
values: ['center', 'flex-end', 'flex-start', 'space-around', 'space-between'],
},
'justify-items': {
values: [
'auto',
'normal',
'stretch',
'center',
'start',
'end',
'flex-start',
'flex-end',
'self-start',
'self-end',
'left',
'right',
'baseline',
'first',
'last',
'safe',
'unsafe',
'legacy',
],
},
'justify-self': {
values: [
'auto',
'normal',
'stretch',
'center',
'start',
'end',
'flex-start',
'flex-end',
'self-start',
'self-end',
'left',
'right',
'baseline',
'first',
'last',
'safe',
'unsafe',
],
},
left: { values: ['auto'] },
'letter-spacing': { values: ['normal'] },
'line-height': { values: ['normal'] },
'list-style': {
values: [
'none',
'url()',
'armenian',
'circle',
'decimal',
'decimal-leading-zero',
'disc',
'georgian',
'inside',
'lower-alpha',
'lower-greek',
'lower-latin',
'lower-roman',
'outside',
'square',
'upper-alpha',
'upper-latin',
'upper-roman',
],
},
'list-style-image': { values: ['none', 'url()'] },
'list-style-position': { values: ['inside', 'outside'] },
'list-style-type': {
values: [
'armenian',
'circle',
'decimal',
'decimal-leading-zero',
'disc',
'georgian',
'lower-alpha',
'lower-greek',
'lower-latin',
'lower-roman',
'none',
'square',
'upper-alpha',
'upper-latin',
'upper-roman',
],
},
margin: { values: ['auto'] },
'margin-bottom': { values: ['auto'] },
'margin-left': { values: ['auto'] },
'margin-right': { values: ['auto'] },
'margin-top': { values: ['auto'] },
'max-height': { values: ['none'] },
'max-width': { values: ['none'] },
'min-height': { values: [] },
'min-width': { values: [] },
'mix-blend-mode': {
values: [
'color',
'color-burn',
'color-dodge',
'darken',
'difference',
'exclusion',
'hard-light',
'hue',
'lighten',
'luminosity',
'multiply',
'normal',
'overlay',
'saturation',
'screen',
'soft-light',
],
},
'object-fit': {
values: ['contain', 'cover', 'fill', 'none', 'scale-down'],
},
'object-position': { values: ['left', 'center', 'right', 'bottom', 'top'] },
opacity: { values: [] },
order: { values: [] },
orphans: { values: [] },
outline: { values: [] },
'outline-color': { values: ['invert'], type: 'color' },
'outline-offset': { values: [] },
'outline-style': {
values: ['dashed', 'dotted', 'double', 'groove', 'hidden', 'inset', 'none', 'outset', 'ridge', 'solid'],
},
'outline-width': { values: ['medium', 'thin', 'thick'] },
overflow: { values: ['auto', 'hidden', 'scroll', 'visible'] },
'overflow-x': { values: ['auto', 'hidden', 'scroll', 'visible'] },
'overflow-y': { values: ['auto', 'hidden', 'scroll', 'visible'] },
'overflow-wrap': { values: ['normal', 'anywhere', 'break-word'] },
padding: { values: [] },
'padding-bottom': { values: [] },
'padding-left': { values: [] },
'padding-right': { values: [] },
'padding-top': { values: [] },
'page-break-after': {
values: ['always', 'auto', 'avoid', 'left', 'right'],
},
'page-break-before': {
values: ['always', 'auto', 'avoid', 'left', 'right'],
},
'page-break-inside': { values: ['auto', 'avoid'] },
perspective: { values: ['none'] },
'perspective-origin': {
values: ['bottom', 'center', 'left', 'right', 'top'],
},
'pointer-events': {
values: ['all', 'auto', 'fill', 'none', 'painted', 'stroke', 'visible', 'visibleFill', 'visiblePainted', 'visibleStroke'],
},
position: { values: ['absolute', 'fixed', 'relative', 'static', 'sticky'] },
quotes: { values: ['none'] },
'region-break-after': {
values: ['always', 'auto', 'avoid', 'avoid-column', 'avoid-page', 'avoid-region', 'column', 'left', 'page', 'region', 'right'],
},
'region-break-before': {
values: ['always', 'auto', 'avoid', 'avoid-column', 'avoid-page', 'avoid-region', 'column', 'left', 'page', 'region', 'right'],
},
'region-break-inside': {
values: ['auto', 'avoid', 'avoid-column', 'avoid-page', 'avoid-region'],
},
'region-fragment': { values: ['auto', 'break'] },
resize: { values: ['both', 'horizontal', 'none', 'vertical'] },
right: { values: ['auto'] },
'scroll-behavior': { values: ['auto', 'smooth'] },
'scroll-snap-type': {
values: ['none', 'x', 'y', 'block', 'inline', 'both', 'mandatory', 'proximity'],
},
src: { values: ['url()'] },
'shape-image-threshold': { values: [] },
'shape-inside': {
values: ['auto', 'circle()', 'ellipse()', 'outside-shape', 'polygon()', 'rectangle()'],
},
'shape-margin': { values: [] },
'shape-outside': {
values: [
'none',
'circle()',
'ellipse()',
'polygon()',
'inset()',
'margin-box',
'border-box',
'padding-box',
'content-box',
'url()',
'image()',
'linear-gradient()',
'radial-gradient()',
'repeating-linear-gradient()',
'repeating-radial-gradient()',
],
},
'tab-size': { values: [] },
'table-layout': { values: ['auto', 'fixed'] },
'text-align': {
values: ['start', 'end', 'center', 'left', 'justify', 'right', 'match-parent', 'justify-all'],
},
'text-align-last': { values: ['center', 'left', 'justify', 'right'] },
'text-decoration': {
values: ['line-through', 'none', 'overline', 'underline'],
},
'text-decoration-color': { values: [], type: 'color' },
'text-decoration-line': {
values: ['line-through', 'none', 'overline', 'underline'],
},
'text-decoration-skip': {
values: ['edges', 'ink', 'none', 'objects', 'spaces'],
},
'text-decoration-style': {
values: ['dashed', 'dotted', 'double', 'solid', 'wavy'],
},
'text-emphasis': { values: [] },
'text-emphasis-color': { values: [], type: 'color' },
'text-emphasis-position': { values: ['above', 'below', 'left', 'right'] },
'text-emphasis-style': {
values: ['circle', 'dot', 'double-circle', 'filled', 'none', 'open', 'sesame', 'triangle'],
},
'text-indent': { values: [] },
'text-justify': {
values: ['auto', 'none', 'inter-word', 'inter-character'],
},
'text-overflow': { values: ['clip', 'ellipsis'] },
'text-shadow': { values: [] },
'text-rendering': {
values: ['auto', 'geometricPrecision', 'optimizeLegibility', 'optimizeSpeed'],
},
'text-transform': {
values: ['capitalize', 'full-width', 'lowercase', 'none', 'uppercase'],
},
'text-underline-position': {
values: ['alphabetic', 'auto', 'below', 'left', 'right'],
},
top: { values: ['auto'] },
transform: {
values: [
'matrix()',
'matrix3d()',
'none',
'perspective()',
'rotate()',
'rotate3d()',
'rotateX()',
'rotateY()',
'rotateZ()',
'scale()',
'scale3d()',
'scaleX()',
'scaleY()',
'scaleZ()',
'skewX()',
'skewY()',
'translate()',
'translate3d()',
'translateX()',
'translateY()',
'translateZ()',
],
},
'transform-origin': {
values: ['bottom', 'center', 'left', 'right', 'top'],
},
'transform-style': { values: ['flat', 'preserve-3d'] },
transition: { values: [] },
'transition-delay': { values: [] },
'transition-duration': { values: [] },
'transition-property': { values: ['all', 'none'] },
'transition-timing-function': {
values: ['cubic-bezier()', 'ease', 'ease-in', 'ease-in-out', 'ease-out', 'linear', 'step-end', 'step-start', 'steps()'],
},
'unicode-bidi': { values: ['bidi-override', 'embed', 'normal'] },
'unicode-range': { values: [] },
'user-select': { values: ['all', 'auto', 'contain', 'none', 'text'] },
'vertical-align': {
values: ['baseline', 'bottom', 'middle', 'sub', 'super', 'text-bottom', 'text-top', 'top'],
},
visibility: { values: ['collapse', 'hidden', 'visible'] },
'white-space': {
values: ['normal', 'nowrap', 'pre', 'pre-line', 'pre-wrap'],
},
widows: { values: [] },
width: { values: ['auto', 'max-content', 'min-content', 'fit-content'] },
'will-change': {
values: ['auto', 'contents', 'opacity', 'scroll-position'],
},
'word-break': { values: ['normal', 'break-all', 'keep-all'] },
'word-spacing': { values: ['normal'] },
'z-index': { values: ['auto'] },
},
};
export const CSSProperties = CSSSourceInfo.cssKeyValue;
export const CSSColors = CSSSourceInfo.cssColors;
export type CSSPropertiesKey = keyof typeof CSSProperties;
export const CSSPropertyList = Object.keys(CSSSourceInfo.cssKeyValue);
================================================
FILE: packages/engine/src/component/CSSPropertiesVariableBindEditor/index.tsx
================================================
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react';
import { ConfigProvider, message } from 'antd';
import { JSExpressionPropType } from '@chamn/model';
import styles from './style.module.scss';
import { InnerSinglePropertyEditorRef, SinglePropertyEditor } from './SingleProperty';
export type CSSPropertiesVariableBindEditorProps = {
initialValue?: { property: string; value: string | JSExpressionPropType }[];
onValueChange?: (val: { property: string; value: string }[]) => void;
};
export type CSSPropertiesVariableBindEditorRef = {
setValue: (val: { property: string; value: string | JSExpressionPropType }[]) => void;
};
export const CSSPropertiesVariableBindEditor = forwardRef<
CSSPropertiesVariableBindEditorRef,
CSSPropertiesVariableBindEditorProps
>(function CSSPropertiesVariableBindEditorCore(props, ref) {
const [propertyList, setPropertyList] = useState<{ property: string; value: any }[]>([]);
useImperativeHandle(
ref,
() => {
return {
setValue: (val) => {
setPropertyList(val);
},
};
},
[]
);
useEffect(() => {
if (props.initialValue) {
setPropertyList(props.initialValue);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const [newProperty, setNewProperty] = useState<{
property: string;
value: JSExpressionPropType | string;
}>({
property: '',
value: {
type: 'EXPRESSION',
value: '',
},
});
const innerOnValueChange = (val: typeof propertyList) => {
props.onValueChange?.(val);
};
const createPropertyRef = useRef(null);
return (
{propertyList.map((el, index) => {
return (
{
if (newVal.property === '') {
propertyList.splice(index, 1);
setPropertyList([...propertyList]);
return;
}
propertyList[index] = newVal;
setPropertyList([...propertyList]);
innerOnValueChange(propertyList);
}}
onDelete={() => {
propertyList.splice(index, 1);
setPropertyList([...propertyList]);
innerOnValueChange(propertyList);
}}
/>
);
})}
{
setNewProperty(newVal);
}}
onCreate={(val) => {
const hasExits = propertyList.find((el) => el.property === val.property);
if (hasExits) {
message.error('The attribute name already exists, please replace');
return {
errorKey: [val.property],
};
}
propertyList.push(val);
setPropertyList([...propertyList]);
innerOnValueChange(propertyList);
setNewProperty({
property: '',
value: {
type: 'EXPRESSION',
value: '',
},
});
}}
/>
);
});
================================================
FILE: packages/engine/src/component/CSSPropertiesVariableBindEditor/style.module.scss
================================================
.cssFieldBox {
display: flex;
color: $textColor !important;
:global {
.ant-select-selector input {
color: $textColor !important;
}
}
.row {
display: flex;
padding-bottom: 6px;
align-items: center;
}
.fieldLabel {
display: inline-block;
width: 45px;
flex-shrink: 0;
font-size: 12px;
}
.leftBox {
flex: 1;
}
.rightBox {
padding-left: 10px;
}
.inputBox {
flex: 1;
}
}
.cssBox {
:global {
.ant-collapse-borderless > .ant-collapse-item {
border-bottom: 1px solid #e4e4e4;
}
.ant-collapse-borderless > .ant-collapse-item:last-child {
border-bottom: none;
}
.ant-collapse-content-box {
padding-bottom: 30px !important;
}
}
}
.switchBtn {
margin-top: 10px;
color: $fontColor;
font-size: 12px;
padding: 0 5px 0 10px;
}
================================================
FILE: packages/engine/src/component/CSSPropertiesVariableBindEditor/util.ts
================================================
export const getTextWidth = async (text: string, fontSize = 14) => {
return new Promise((resolve) => {
const span = document.createElement('span');
span.innerHTML = text;
span.style.fontSize = `${fontSize}px`;
span.style.position = 'fixed';
span.style.left = '-1000px';
span.style.bottom = '-1000px';
span.style.display = 'inline-block';
document.body.appendChild(span);
setTimeout(() => {
const style = window.getComputedStyle(span);
resolve(style.width);
setTimeout(() => {
document.body.removeChild(span);
});
});
});
};
================================================
FILE: packages/engine/src/component/CSSSizeInput/index.tsx
================================================
/* eslint-disable react-refresh/only-export-components */
import { ConfigProvider, InputProps, Select } from 'antd';
import { MouseEventHandler, useCallback, useEffect, useMemo, useRef, useState } from 'react';
import styles from './style.module.scss';
import { addEventListenerReturnCancel } from '@chamn/layout';
import { InputNumberPlus } from '../InputNumberPlus';
import clsx from 'clsx';
type CumulativeInfoType = {
x: number;
y: number;
cumulativeX: number;
cumulativeY: number;
};
const UNIT_LIST = [
{ value: 'px', label: 'px' },
{ value: '%', label: '%' },
{ value: 'vw', label: 'vw' },
{ value: 'vh', label: 'vh' },
{ value: 'rem', label: 'rem' },
];
export const useDragSize = function (options?: {
onStart?: (data: CumulativeInfoType) => void;
onChange?: (data: CumulativeInfoType) => void;
onEnd?: (data: CumulativeInfoType) => void;
limitCumulative?: {
x?: [number | undefined, number | undefined];
y?: [number | undefined, number | undefined];
};
}) {
const cumulativeInfo = useRef<{
status: 'running' | '';
start: CumulativeInfoType;
processing: CumulativeInfoType;
mouseDown: boolean;
}>({
status: '',
start: {
x: 0,
y: 0,
cumulativeX: 0,
cumulativeY: 0,
},
processing: {
x: 0,
y: 0,
cumulativeX: 0,
cumulativeY: 0,
},
mouseDown: false,
});
const processEnd: MouseEventHandler = useCallback(() => {
cumulativeInfo.current.mouseDown = false;
const { processing } = cumulativeInfo.current;
options?.onEnd?.({ ...processing });
cumulativeInfo.current.status = '';
cumulativeInfo.current.processing.cumulativeX = 0;
cumulativeInfo.current.processing.cumulativeY = 0;
}, [options]);
const onMouseDown: MouseEventHandler = useCallback(() => {
cumulativeInfo.current.mouseDown = true;
}, []);
const processMove: MouseEventHandler = useCallback(
(e) => {
if (!cumulativeInfo.current.mouseDown) {
return;
}
const { start, processing } = cumulativeInfo.current;
// 判断是否是拖拽
const tempCumulativeX = e.clientX - start.x;
const tempCumulativeY = e.clientY - start.y;
const shakeDIstance = 10;
if (
cumulativeInfo.current.status !== 'running' &&
(Math.abs(tempCumulativeX) > shakeDIstance || Math.abs(tempCumulativeY) > shakeDIstance)
) {
cumulativeInfo.current.status = 'running';
const startInfo = cumulativeInfo.current.start;
startInfo.x = e.clientX;
startInfo.y = e.clientY;
options?.onStart?.({ ...startInfo });
}
if (cumulativeInfo.current.status !== 'running') {
return;
}
processing.x = e.clientX;
processing.y = e.clientY;
processing.cumulativeX = e.clientX - start.x;
processing.cumulativeY = e.clientY - start.y;
// 超出最大累积便宜,直接结束拖拽
const [minX, maxX] = options?.limitCumulative?.x || [];
if (minX !== undefined && processing.cumulativeX < minX) {
processEnd(e);
return;
}
if (maxX !== undefined && processing.cumulativeX > maxX) {
processEnd(e);
return;
}
options?.onChange?.({ ...processing });
},
[options, processEnd]
);
useEffect(() => {
const globalMoveDispose = addEventListenerReturnCancel(document.body, 'mousemove', (e) => {
processMove(e as any);
});
const globalUpDispose = addEventListenerReturnCancel(document.body, 'mouseup', (e) => {
processEnd(e as any);
});
const globalLeaveDispose = addEventListenerReturnCancel(document.body, 'mouseleave', (e) => {
processEnd(e as any);
});
return () => {
globalMoveDispose();
globalUpDispose();
globalLeaveDispose();
};
}, [processEnd, processMove]);
return {
onMouseDown,
};
};
type MinMaxType = {
px?: number;
vw?: number;
vh?: number;
rem?: number;
};
export type CSSSizeInputProps = {
value?: string;
onValueChange?: (newVal: string | undefined) => void;
min?: MinMaxType | number;
max?: MinMaxType | number;
size?: InputProps['size'];
style?: React.CSSProperties;
className?: string;
unit?: boolean;
/** 累计的偏移量,映射为具体的值,默认 1:1 */
cumulativeTransform?: (params: CumulativeInfoType) => CumulativeInfoType;
unitList?: (keyof MinMaxType)[];
};
export const CSSSizeInput = (props: CSSSizeInputProps) => {
const outValue = String(props.value);
const [dragSizing, setDragSizing] = useState({
value: 0,
status: '',
});
const originalValObj = useMemo(() => {
const res: {
value: number | undefined;
unit: keyof MinMaxType;
} = {
value: undefined,
unit: 'px' as any,
};
if (props.value === undefined) {
return res;
}
const tempVal = parseFloat(props.value || '');
if (!isNaN(tempVal)) {
res.value = tempVal;
}
let unit: keyof MinMaxType = 'px';
if (res.value !== undefined) {
unit = outValue.replace(String(res.value), '').trim() as keyof MinMaxType;
}
if (['px', '%', 'rem', 'vw', 'vx'].includes(unit)) {
res.unit = unit || 'px';
}
return res;
}, [outValue, props.value]);
const currentUnitList = useMemo(() => {
if (!props.unitList) {
return UNIT_LIST;
}
return props.unitList
.map((el) => {
return UNIT_LIST.find((it) => it.value === el);
})
.filter(Boolean) as typeof UNIT_LIST;
}, [props.unitList]);
const currentMinMix = useMemo(() => {
return {
min: typeof props.min === 'number' ? props.min : props.min?.[originalValObj.unit],
max: typeof props.max === 'number' ? props.max : props.max?.[originalValObj.unit],
};
}, [props, originalValObj]);
const valObj = useMemo(() => {
const res = { ...originalValObj };
if (dragSizing.status === 'dragging') {
res.value = dragSizing.value;
}
return res;
}, [originalValObj, dragSizing]);
const updateValue = useCallback(
(val: { value: number | undefined; unit: string }) => {
let valStr: string | undefined = `${val.value}${val.unit}`;
if (val.value === undefined) {
valStr = undefined;
}
props.onValueChange?.(valStr);
},
[props]
);
const processNewVal = useCallback(
(cumulativeData: CumulativeInfoType, value: number | undefined) => {
let data = { ...cumulativeData };
if (props.cumulativeTransform) {
data = props.cumulativeTransform(data);
}
let num = value ?? 0;
if (isNaN(num)) {
num = 0;
}
let newVal = num + data.cumulativeX;
if (currentMinMix.min !== undefined) {
newVal = Math.max(newVal, currentMinMix.min);
}
if (currentMinMix.max !== undefined) {
newVal = Math.min(newVal, currentMinMix.max);
}
return newVal;
},
[currentMinMix.max, currentMinMix.min, props]
);
const onDragStart = useCallback(
(data: CumulativeInfoType) => {
setDragSizing({
status: 'dragging',
value: processNewVal(data, originalValObj.value),
});
},
[originalValObj.value, processNewVal]
);
const onDragMoveChange = useCallback(
(data: CumulativeInfoType) => {
const nV = processNewVal(data, originalValObj.value);
setDragSizing({
status: 'dragging',
value: nV,
});
},
[originalValObj.value, processNewVal]
);
const onDragEnd = useCallback(
(data: CumulativeInfoType) => {
const newVal = processNewVal(data, originalValObj.value);
if (dragSizing.status !== 'dragging') {
return;
}
updateValue({
value: newVal,
unit: originalValObj.unit,
});
setDragSizing({
status: '',
value: 0,
});
},
[dragSizing.status, originalValObj, processNewVal, updateValue]
);
const handleRef = useRef({
onStart: onDragStart,
onChange: onDragMoveChange,
onEnd: onDragEnd,
});
handleRef.current = {
onStart: onDragStart,
onChange: onDragMoveChange,
onEnd: onDragEnd,
};
const dragSizeHandle = useDragSize({
onStart: (data) => handleRef.current.onStart(data),
onChange: (data) => handleRef.current.onChange(data),
onEnd: (data) => handleRef.current.onEnd(data),
limitCumulative: {
x: [-60, undefined],
},
});
const unitSelect = (
{
updateValue({
value: valObj.value,
unit: val,
});
}}
style={{
width: '40px',
}}
popupMatchSelectWidth={false}
options={currentUnitList}
/>
);
return (
{
updateValue({
value: val,
unit: valObj.unit,
});
}}
/>
);
};
================================================
FILE: packages/engine/src/component/CSSSizeInput/style.module.scss
================================================
.cssSizeInput {
display: inline-block;
.unitSelect {
:global {
.ant-select-focused .ant-select-selector {
color: rgba(0, 0, 0, 0.675);
}
.ant-select-selector {
color: #585858 !important;
padding: 0 0 0 3px !important;
font-size: 12px;
}
.ant-select-selection-item {
padding-inline-end: 10px !important;
}
.ant-select-arrow {
inset-inline-end: 3px !important;
}
}
}
}
================================================
FILE: packages/engine/src/component/ClassNameEditor/index.tsx
================================================
import { forwardRef, useImperativeHandle, useRef } from 'react';
import { ConfigProvider } from 'antd';
import { CustomSchemaForm, CustomSchemaFormInstance } from '../CustomSchemaForm';
import { ClassNameType, CMaterialPropsType, CNode } from '@chamn/model';
import { CPluginCtx } from '@/core/pluginManager';
export type ClassNameEditorProps = {
initialValue?: ClassNameType[];
onValueChange?: (val: ClassNameType[]) => void;
pluginContext: CPluginCtx;
nodeModel: CNode;
};
export type ClassNameEditorRef = {
setValue: (val: ClassNameType[]) => void;
};
const properties: CMaterialPropsType = [
{
title: 'Class Names',
name: 'className',
valueType: 'array',
setters: [
{
componentName: 'ArraySetter',
props: {
sortLabelKey: 'name',
collapse: {
open: true,
},
item: {
setters: [
{
componentName: 'ShapeSetter',
props: {
elements: [
{
name: 'name',
title: '类名',
valueType: 'string',
setters: ['StringSetter'],
},
{
name: 'status',
title: '启用',
valueType: 'boolean',
setters: ['BooleanSetter', 'ExpressionSetter'],
},
],
},
initialValue: {},
},
],
initialValue: {
name: '',
status: true,
},
},
},
initialValue: [],
},
],
},
];
export const ClassNameEditor = forwardRef(function CSSPropertiesEditorCore(
props,
ref
) {
const formRef = useRef(null);
useImperativeHandle(ref, () => {
return {
setValue(newValue) {
formRef?.current?.setFields({
className: newValue || [],
});
},
};
});
return (
{
props.onValueChange?.(newVal.className || []);
}}
defaultSetterConfig={{}}
>
);
});
================================================
FILE: packages/engine/src/component/ClassNameEditor/style.module.scss
================================================
================================================
FILE: packages/engine/src/component/CustomColorPicker/index.tsx
================================================
import { ColorPicker, ColorPickerProps, GetProp } from 'antd';
import { forwardRef, useImperativeHandle, useState } from 'react';
export type CustomColorPickerRef = {
updateColor: (color: string) => void;
};
type Color = GetProp;
type CustomColorPickerProps = Omit & {
onChange: (color: string) => void;
};
export const CustomColorPicker = forwardRef(function CustomColorPicker(
props,
ref
) {
const [color, setColor] = useState();
useImperativeHandle(ref, () => {
return {
updateColor: (color: string) => {
setColor(color);
},
};
});
return (
{
setColor(val);
}}
onChangeComplete={(val) => {
props.onChange?.(val.toRgbString());
}}
/>
);
});
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/CFiledWithSwitchSetter/index.tsx
================================================
import { useContext, useMemo, useState } from 'react';
import { SetterObjType, SetterType } from '@chamn/model';
import { Dropdown, MenuProps } from 'antd';
import { SwapOutlined } from '@ant-design/icons';
import { CCustomSchemaFormContext } from '@/component/CustomSchemaForm/context';
import { getSetterList } from '@/component/CustomSchemaForm/utils';
import styles from './style.module.scss';
import { CField, CFieldProps } from '../Form/Field';
import { SetterSwitcherCore } from '../SetterSwitcher/core';
export const CFiledWithSwitchSetter = (
props: Omit & {
setterList: SetterType[];
onSetterChange?: (setterName: string) => void;
defaultSetterName?: string;
}
) => {
const { setterList: setters, defaultSetterName, ...restProps } = props;
const setterList = useMemo(() => {
return getSetterList(setters);
}, [setters]);
const { onSetterChange, defaultSetterConfig, pluginCtx, nodeId, customSetterMap } =
useContext(CCustomSchemaFormContext);
const keyPaths = [props.name];
const [currentSetter, setCurrentSetter] = useState(() => {
const currentSetterName = defaultSetterConfig[keyPaths.join('.')]?.setter || defaultSetterName || '';
return [...setterList].find((el) => el.componentName === currentSetterName) || setterList[0];
});
const menuItems = setterList.map((setter) => {
const setterName = setter?.componentName || '';
const setterRuntime = ({} as any)[setterName];
return {
key: setter.componentName,
label: setterRuntime?.setterName || setter.componentName,
};
});
const onChooseSetter: MenuProps['onClick'] = ({ key }) => {
const targetSetter = setterList.find((setter) => setter.componentName === key);
if (targetSetter) {
setCurrentSetter(targetSetter);
onSetterChange?.(keyPaths, targetSetter.componentName);
props.onSetterChange?.(targetSetter.componentName);
}
};
let switcher: any = (
{
e.preventDefault();
e.stopPropagation();
}}
className={styles.switchBtn}
>
);
if (menuItems.length === 1) {
switcher = null;
}
const setterProps = useMemo(() => {
let newProps = {
...(currentSetter?.props || {}),
initialValue: currentSetter?.initialValue,
};
const target = setterList.find((el) => el.componentName === currentSetter?.componentName);
if (target) {
newProps = {
...newProps,
...target.props,
};
}
return newProps;
}, [setterList, currentSetter]);
return (
{switcher}
);
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/CFiledWithSwitchSetter/style.module.scss
================================================
.fieldBox {
display: flex;
align-items: center;
justify-content: center;
}
.switchBtn {
color: $fontColor;
font-size: 12px;
padding: 0 5px 0 10px;
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Form/Field/index.tsx
================================================
import React, { isValidElement, ReactNode, useEffect, useState } from 'react';
import styles from './style.module.scss';
import { Alert, Popover, Tooltip } from 'antd';
import { CFormContext } from '../context';
import clsx from 'clsx';
import { QuestionCircleOutlined } from '@ant-design/icons';
export type CFiledChildProps = {
onValueChange?: (val: any) => void;
initialValue?: any;
value?: any;
};
export type CFieldProps = {
children: React.ReactNode;
label?: string;
labelWidth?: string;
labelAlign?: 'start' | 'center' | 'end';
tips?: ReactNode | (() => ReactNode);
name: string;
condition?: (formState: Record) => boolean;
onConditionValueChange?: (val: boolean) => void;
/** 不做任何包裹, 不会渲染 switchSetter */
noStyle?: boolean;
/** 隐藏 label, 会渲染 switchSetter */
hiddenLabel?: boolean;
valueChangeEventName?: string;
formatEventValue?: (val: any) => any;
rules?: { validator?: (value: any) => Promise; required?: boolean; msg?: string }[];
};
export const CField = (props: CFieldProps) => {
const { children, label, tips, name, hiddenLabel } = props;
const [errorInfo, setErrorInfo] = useState({
isPass: true,
errorMsg: '',
});
let labelView: ReactNode = label;
const { formState, updateContext, updateConditionConfig } = React.useContext(CFormContext);
useEffect(() => {
if (props.condition) {
updateConditionConfig(name, props.condition);
}
}, [name, props.condition, updateConditionConfig]);
useEffect(() => {
const condition = props.condition ?? (() => true);
const canRender = condition(formState);
props.onConditionValueChange?.(canRender);
}, [formState, props]);
if (tips) {
let newTip: any = tips;
if (typeof tips === 'function') {
newTip = tips();
}
labelView = (
{newTip}}
color="rgba(50,50,50,0.8)"
>
{label}
);
}
let newChildren = children;
if (isValidElement(children)) {
const eventName = props.valueChangeEventName ?? 'onValueChange';
const extraProps: any = {
[eventName]: async (val: any) => {
let tempVal = val;
if (props.formatEventValue) {
tempVal = props.formatEventValue(val);
}
// 先检查数据是否合法
const rules = props.rules || [];
const checkResult = await checkValue(props.name, tempVal, rules);
if (!checkResult.isPass) {
setErrorInfo(checkResult);
} else {
setErrorInfo({
isPass: true,
errorMsg: '',
});
}
updateContext(
{
...formState,
[name]: tempVal,
},
[name]
);
},
};
extraProps.value = (formState as any)[name];
newChildren = React.cloneElement(children, extraProps);
}
const condition = props.condition ?? (() => true);
const canRender = condition(formState);
if (!canRender) {
return null;
}
if (props.noStyle) {
return <>{newChildren}>;
}
return (
{hiddenLabel !== true && (
{labelView}
)}
{newChildren}
{!errorInfo.isPass && (
}>
)}
);
};
const checkValue = async (key: string, value: any, rules: CFieldProps['rules'] = []) => {
let pass = true;
let errorMsg = '';
for (let i = 0; i < rules.length; i++) {
if (rules[i].required && !value) {
pass = false;
errorMsg = `${key} is required.`;
break;
}
if (rules[i].validator) {
try {
const isPass = await rules[i].validator?.(value);
if (!isPass) {
pass = false;
errorMsg = rules[i]?.msg || `${key} is illegal `;
break;
}
} catch (e) {
pass = false;
errorMsg = `${String(e)}`;
break;
}
}
}
return {
isPass: pass,
errorMsg,
};
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Form/Field/style.module.scss
================================================
.fieldBox {
display: flex;
flex-wrap: nowrap;
flex-shrink: 0;
flex-grow: 1;
min-width: 250px;
align-items: center;
.label {
font-size: 12px;
color: $textColor;
padding-right: 10px;
user-select: none;
max-height: 40px;
word-break: break-all;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2; /* 这里是超出几行省略 */
}
.tipsLabel {
text-decoration-line: underline;
text-decoration-style: dashed;
text-underline-offset: 2px;
cursor: help;
word-break: break-all;
}
.content {
flex: 1;
position: relative;
}
}
$errorColor: #ff002dc4;
.error {
outline: 1px solid $errorColor;
outline-offset: 3px;
border-radius: 4px;
}
.errorTipIcon {
color: $errorColor;
margin: 0 5px;
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Form/context.ts
================================================
import React from 'react';
import { CSetter } from '../Setters/type';
export type ContextState = Record;
export type CFormContextData = {
formName: string;
formState: ContextState;
conditionConfig: Record boolean>;
// 自定义 setter 的具体实现,可以覆盖默认 setter
customSetterMap?: Record;
updateContext: (newState: ContextState, changeKeys?: string[]) => void;
updateConditionConfig: (name: string, cb: (state: ContextState) => boolean) => void;
};
export const CFormContext = React.createContext({
formName: '',
formState: {},
conditionConfig: {},
updateContext: () => {},
updateConditionConfig: () => {},
// 自定义 setter 的具体实现,可以覆盖默认 setter
customSetterMap: {},
});
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Form/index.tsx
================================================
import React from 'react';
import { ReactNode } from 'react';
import { CFormContext, CFormContextData, ContextState } from './context';
export type CFormProps = {
name: string;
children?: ReactNode | ReactNode[];
initialValue?: Record;
customSetterMap: CFormContextData['customSetterMap'];
onValueChange?: (formData: Record, changeKeys?: string[]) => void;
} & Partial;
const CUSTOM_SETTER_MAP = {};
let updateState = () => {};
export const registerCustomSetter = (customSetterMap: CFormContextData['customSetterMap']) => {
Object.assign(CUSTOM_SETTER_MAP, customSetterMap);
updateState?.();
};
export class CForm extends React.Component {
updateContext: (newState: ContextState) => void;
isMount = false;
constructor(props: CFormProps) {
super(props);
this.updateContext = (newState: ContextState, changeKeys?: string[]) => {
this.setState({
formState: newState,
customSetterMap: CUSTOM_SETTER_MAP,
});
this.props.onValueChange?.(this.formatValue(newState), changeKeys);
};
updateState = () => {
if (!this.isMount) {
return;
}
this.setState({
customSetterMap: { ...CUSTOM_SETTER_MAP, ...this.props.customSetterMap },
});
};
registerCustomSetter(props.customSetterMap || {});
this.state = {
formName: props.name,
formState: props.initialValue ?? {},
conditionConfig: {},
customSetterMap: props.customSetterMap || {},
updateContext: this.updateContext,
updateConditionConfig: (name: string, cb: (state: Record) => boolean) => {
this.setState({
conditionConfig: {
...this.state.conditionConfig,
[name]: cb,
},
});
},
};
}
componentDidMount(): void {
this.isMount = true;
}
componentWillUnmount(): void {
this.isMount = false;
}
getFieldsValue = () => {
return this.formatValue(this.state.formState);
};
setFields = (state: Record) => {
this.setState({
formState: state,
});
};
formatValue = (data: Record) => {
const res: Record = {};
const conditionConfig = this.state.conditionConfig;
Object.keys(data).forEach((key) => {
const isValid = conditionConfig[key]?.(data) ?? true;
if (isValid) {
res[key] = data[key];
}
});
return res;
};
render(): ReactNode {
const { state } = this;
const { children } = this.props;
return {children} ;
}
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/SetterSwitcher/core.tsx
================================================
import React, { useContext, useEffect } from 'react';
import { SetterObjType } from '@chamn/model';
import InnerSetters from '../Setters/index';
import { CFieldProps } from '../Form/Field';
import { CCustomSchemaFormContext } from '../../context';
import { CFormContext } from '../Form/context';
import { CSetter, CSetterProps } from '../Setters/type';
import { getDefaultSetterByValue } from '../../utils';
export type SetterSwitcherProps = {
// 支持的 setter 列表
setters: SetterObjType[];
// 自定义 setter 的具体实现,可以覆盖默认 setter
customSetterMap?: Record;
keyPaths: string[];
prefix?: React.ReactNode;
suffix?: React.ReactNode;
style?: React.CSSProperties;
/** 是否实用 CFile 包裹 */
useField?: boolean;
} & Omit;
/** 用于渲染切换设置器 */
export const SetterSwitcherCore = ({
setters,
keyPaths,
currentSetter,
setCurrentSetter,
customSetterMap: customSetterMapProp,
...props
}: Pick & {
value?: any;
setterContext?: Partial;
currentSetter: SetterObjType;
customSetterMap?: any;
setCurrentSetter: (setter: SetterObjType) => void;
}) => {
const { customSetterMap } = useContext(CFormContext);
const { defaultSetterConfig } = useContext(CCustomSchemaFormContext);
const allSetterMap = {
...InnerSetters,
...(customSetterMapProp || {}),
...customSetterMap,
};
useEffect(() => {
const currentSetterName = defaultSetterConfig[keyPaths.join('.')]?.setter || '';
const devConfigSetter = setters.find((el) => el.componentName === currentSetterName);
const st = devConfigSetter || getDefaultSetterByValue(props.value, setters) || setters[0];
setCurrentSetter(st);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
let CurrentSetterComp = null;
if (currentSetter?.componentName) {
CurrentSetterComp = allSetterMap[currentSetter?.componentName] || currentSetter.component;
}
if (!CurrentSetterComp) {
CurrentSetterComp = function EmptySetter() {
return (
{`${currentSetter?.componentName} is not found.`}
);
};
}
return ;
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/SetterSwitcher/helper.tsx
================================================
/* eslint-disable react-refresh/only-export-components */
import styles from './style.module.scss';
export function EmptySetter(props: { setterName: string }) {
return (
{`${props?.setterName} is not found.`}
);
}
export const getEmptySetter = (setterName: string) => {
return () => ;
};
// 新增 CollapseHeader 组件
export const CollapseHeader: React.FC<{
label: React.ReactNode;
headerExt?: React.ReactNode;
switcher?: React.ReactNode;
}> = ({ label, headerExt, switcher }) => (
{label}
{headerExt}
{switcher}
);
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/SetterSwitcher/index.tsx
================================================
import React, { useContext, useMemo, useState } from 'react';
import { SetterObjType, SetterTypeEnum } from '@chamn/model';
import InnerSetters from '../Setters/index';
import { CField, CFieldProps } from '../Form/Field';
import { Collapse, Dropdown, MenuProps } from 'antd';
import { SwapOutlined } from '@ant-design/icons';
import styles from './style.module.scss';
import { CCustomSchemaFormContext } from '../../context';
import { CFormContext } from '../Form/context';
import { CSetter } from '../Setters/type';
import { SetterSwitcherCore } from './core';
export type SetterSwitcherProps = {
// 支持的 setter 列表
setters: SetterObjType[];
// 自定义 setter 的具体实现,可以覆盖默认 setter
customSetterMap?: Record;
keyPaths: string[];
prefix?: React.ReactNode;
suffix?: React.ReactNode;
style?: React.CSSProperties;
/** 是否实用 CFile 包裹 */
useField?: boolean;
} & Omit;
export const SetterSwitcher = ({
setters: outerSetter,
keyPaths,
condition,
useField = true,
...props
}: SetterSwitcherProps) => {
const [visible, setVisible] = useState(true);
const { customSetterMap } = useContext(CFormContext);
const { onSetterChange, defaultSetterConfig, formRef, pluginCtx, nodeId } = useContext(CCustomSchemaFormContext);
const allSetterMap = {
...InnerSetters,
...customSetterMap,
};
// 统一添加一些内置的 setter
const setters = useMemo(() => {
return [
...outerSetter,
{
componentName: SetterTypeEnum.EMPTY_VALUE_SETTER,
},
];
}, [outerSetter]);
const [currentSetter, setCurrentSetter] = useState(() => {
const currentSetterName = defaultSetterConfig[keyPaths.join('.')]?.setter || '';
const devConfigSetter = setters.find((el) => el.componentName === currentSetterName);
return devConfigSetter || setters[0];
});
const menuItems = setters.map((setter) => {
const setterName = setter?.componentName || '';
const setterRuntime = allSetterMap[setterName];
return {
key: setter.componentName,
label: setterRuntime?.setterName || setter.componentName,
};
});
const switcher = useMemo(() => {
const onChooseSetter: MenuProps['onClick'] = ({ key }) => {
const targetSetter = setters.find((setter) => setter.componentName === key);
if (targetSetter) {
setCurrentSetter(targetSetter);
onSetterChange?.(keyPaths, targetSetter.componentName);
}
};
if (menuItems.length === 1) {
return null;
}
return (
{
e.preventDefault();
e.stopPropagation();
}}
>
);
}, [menuItems, currentSetter?.componentName, setters, onSetterChange, keyPaths]);
const setterProps = useMemo(() => {
let newProps = {
...(currentSetter?.props || {}),
initialValue: currentSetter?.initialValue,
};
const target = setters.find((el) => el.componentName === currentSetter?.componentName);
if (target) {
newProps = {
...newProps,
...target.props,
};
}
return newProps;
}, [setters, currentSetter]);
const [collapseHeaderExt, setCollapseHeaderExt] = useState([]);
let bodyView: any = null;
const hiddenLabel = currentSetter?.hiddenLabel === true;
const labelWidth = currentSetter?.labelWidth;
const labelAlign = currentSetter?.labelAlign || 'center';
const collapse = (currentSetter?.props as any)?.collapse;
const specialSetter = ['ArraySetter', 'ShapeSetter'].includes(currentSetter?.componentName);
const setterContext = useMemo(
() => ({
formRef,
pluginCtx,
keyPaths: [...keyPaths],
label: props.label,
setCollapseHeaderExt: specialSetter ? setCollapseHeaderExt : undefined,
nodeModel: pluginCtx?.pageModel.getNode(nodeId) as any,
}),
[formRef, keyPaths, nodeId, pluginCtx, props.label, specialSetter]
);
const filedView = useMemo(() => {
const customSetterMap = {
...(props.customSetterMap || {}),
};
const cFiledProps = {
labelWidth,
labelAlign,
hiddenLabel,
condition,
noStyle: specialSetter ? true : false,
onConditionValueChange: (val: boolean) => {
setVisible(val);
},
};
if (useField === false) {
return (
);
}
return (
);
}, [
condition,
currentSetter,
hiddenLabel,
keyPaths,
labelAlign,
labelWidth,
props,
setterContext,
setterProps,
setters,
specialSetter,
useField,
]);
const renderCollapse = useMemo(
function renderCollapse() {
const collapseObj = typeof collapse === 'object' ? collapse : {};
const CollapseComponent = (extraHeaderContent?: React.ReactNode) => (
{props.label}
{extraHeaderContent}
{switcher}
),
children: filedView,
},
]}
/>
);
return CollapseComponent;
},
[collapse, props.name, props.label, switcher, filedView]
);
if (['ArraySetter'].includes(currentSetter?.componentName || '')) {
bodyView = renderCollapse(collapseHeaderExt);
} else if (['ShapeSetter'].includes(currentSetter?.componentName || '')) {
bodyView = (
{props.prefix ?? null}
{(currentSetter?.props as any)?.collapse === false &&
{filedView}
}
{collapse !== false && renderCollapse()}
{props.suffix ?? null}
);
} else {
bodyView = (
{props.prefix ?? null}
{filedView}
{switcher}
{props.suffix ?? null}
);
}
return {bodyView}
;
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/SetterSwitcher/style.module.scss
================================================
.switchBtn {
color: $fontColor;
font-size: 12px;
padding: 0 5px 0 10px;
}
.shapeFieldBox,
.collapseHeader {
color: $fontColor;
display: flex;
font-size: 12px;
}
.collapseHeader {
align-items: center;
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/component/CreateNewNodePopup/index.tsx
================================================
import { Popover } from 'antd';
import { useState } from 'react';
import { DEFAULT_NODE_LIST } from './initData';
import { TLogicItemHandlerFlow } from '@chamn/model';
export const CreateNewNodePopup = (props: {
title?: string;
children: React.ReactNode;
disabled?: boolean;
style?: React.CSSProperties;
onNewNodeAdd: (data: TLogicItemHandlerFlow[number]) => void;
}) => {
const { title = 'Next Step' } = props;
const [open, setOpen] = useState(false);
const handleOpenChange = (newOpen: boolean) => {
setOpen(newOpen);
};
const nodeList = DEFAULT_NODE_LIST.map((el) => {
return (
{
setOpen(false);
props.onNewNodeAdd?.(el.getInitData());
}}
key={el.key}
style={{
padding: '10px 20px',
border: '1px solid #c3c3c3b8',
borderRadius: '4px',
textAlign: 'center',
fontSize: '12px',
cursor: 'pointer',
}}
>
{el.name}
);
});
// 禁用弹窗
if (props.disabled) {
return props.children;
}
return (
{nodeList.map((el) => (
{el}
))}
}
title={title}
>
{props.children}
);
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/component/CreateNewNodePopup/initData.ts
================================================
import {
AssignValueType,
getRandomStr,
LogicType,
TLogicAssignValueItem,
TLogicCallNodeMethodItem,
TLogicJumpLinkItem,
TLogicRequestAPIItem,
TLogicRunCodeItem,
} from '@chamn/model';
export const DEFAULT_NODE_LIST = [
{
key: LogicType.ASSIGN_VALUE,
name: '赋值/创建变量',
getInitData: () => {
const id = getRandomStr();
const assignValueItem: TLogicAssignValueItem = {
id: id,
type: LogicType.ASSIGN_VALUE,
valueType: AssignValueType.STATE,
currentValue: '',
targetValueName: '',
};
return assignValueItem;
},
},
{
key: LogicType.JUMP_LINK,
name: '页面跳转',
getInitData: () => {
const id = getRandomStr();
const assignValueItem: TLogicJumpLinkItem = {
id: id,
type: LogicType.JUMP_LINK,
link: '',
};
return assignValueItem;
},
},
{
key: LogicType.REQUEST_API,
name: '请求数据',
getInitData: () => {
const id = getRandomStr();
const requestAPIItem: TLogicRequestAPIItem = {
id: id,
type: LogicType.REQUEST_API,
apiPath: '',
method: 'GET',
header: {},
query: {},
responseVarName: `responseData_${id}`,
afterSuccessResponse: [],
afterFailedResponse: [
{
id: getRandomStr(),
type: LogicType.RUN_CODE,
value: 'console.error("API request failed:", $$response)',
},
],
};
return requestAPIItem;
},
},
{
key: LogicType.CALL_NODE_METHOD,
name: '调用组件方法',
getInitData: () => {
const id = getRandomStr();
const callNodeMethodItem: TLogicCallNodeMethodItem = {
id: id,
type: LogicType.CALL_NODE_METHOD,
nodeId: '',
methodName: '',
args: [],
returnVarName: '',
};
return callNodeMethodItem;
},
},
{
key: LogicType.RUN_CODE,
name: '运行代码',
getInitData: () => {
const id = getRandomStr();
const runCodeItem: TLogicRunCodeItem = {
id: id,
type: LogicType.RUN_CODE,
name: `run_code_${id}`,
value: `console.log('hello world');`,
};
return runCodeItem;
},
},
];
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/component/InputHandle/index.tsx
================================================
import { Handle, HandleProps, Position } from '@xyflow/react';
import { INPUT_HANDLE_ID } from '../../config';
export const InputHandle = (props: Partial) => {
return ;
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/component/NodeCard/index.tsx
================================================
import { Card, CardProps } from 'antd';
import { OUTPUT_HANDLE_ID, REACT_FLOW_DRAG_CLASS_NAME } from '../../config';
import { HolderOutlined } from '@ant-design/icons';
import styles from './style.module.scss';
import { CreateNewNodePopup } from '../CreateNewNodePopup';
import { InputHandle } from '../InputHandle';
import { OutputHandle } from '../OutputHandle';
import { NodeProps, useReactFlow } from '@xyflow/react';
import { getNewNodePosInfo, UseNodeHasConnected } from '../../util';
import clsx from 'clsx';
export const NodeCard = ({
nodeProps,
customHandle,
handleNewNodeAdd: outHandleNewNodeAdd,
useCardStyle,
outputHandle,
inputHandle,
...props
}: CardProps & {
inputHandle?: boolean;
outputHandle?: boolean;
useCardStyle?: boolean;
nodeProps: NodeProps;
customHandle?: React.ReactNode;
handleNewNodeAdd?: (newNodeData: any) => void;
}) => {
const { data, isConnectable, selected } = nodeProps;
const reactFlowInstance = useReactFlow();
const outputNodeHasConnected = UseNodeHasConnected(data, OUTPUT_HANDLE_ID);
const handleNewNodeAdd = (newNodeData: any) => {
const currentNode = reactFlowInstance.getNode(String(data.id));
if (!currentNode) {
console.error(`Not found node by id ${data.id}`);
return;
}
const { newEdge, newNode } = getNewNodePosInfo(currentNode, newNodeData);
// 更新流程图
reactFlowInstance.addNodes(newNode);
reactFlowInstance.addEdges(newEdge);
};
let customHandleView: any = (
<>
{inputHandle !== false && }
{outputHandle !== false && (
{
/** 外部有自定义 handle 时,取消事件触发 */
if (customHandle) {
return;
}
if (outHandleNewNodeAdd) {
outHandleNewNodeAdd(data);
} else {
handleNewNodeAdd(data);
}
}}
disabled={outputNodeHasConnected}
>
)}
>
);
if (customHandle) {
customHandleView = customHandle;
}
if (useCardStyle === false) {
return (
{props.children}
{customHandleView}
);
}
return (
{props.extra}
}
>
{props.children}
{customHandleView}
);
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/component/NodeCard/style.module.scss
================================================
.l {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 20px;
cursor: grab;
}
.r {
position: absolute;
right: 0;
top: 0;
height: 100%;
width: 20px;
cursor: grab;
}
.b {
position: absolute;
right: 0;
bottom: 0;
width: 100%;
height: 20px;
cursor: grab;
}
.dragBox {
background-color: rgb(203 203 203 / 16%);
padding: 0 4px;
border-radius: 4px;
color: rgb(126 126 126 / 54%);
}
.selectStatus {
outline: 1px solid #f57dbd;
border-radius: 4px;
box-shadow: 0px 3.54px 4.55px 0px #00000005, 0px 3.54px 4.55px 0px #0000000d, 0px 0.51px 1.01px 0px #0000001a;
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/component/OutputHandle/index.tsx
================================================
import { Handle, HandleProps, Position } from '@xyflow/react';
import { OUTPUT_HANDLE_ID } from '../../config';
export const OutputHandle = (props: Partial) => {
return ;
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/component/SelectNodeByTree/index.tsx
================================================
import { Button } from 'antd';
import { useMemo, useState } from 'react';
import { CPage } from '@chamn/model';
import { SelectNodeModal } from './modal';
export const SelectNodeByTree = (props: {
pageModel: CPage;
onChange?: (data: { nodeId: string; title: string }) => void;
value?: any;
}) => {
const [modalOpen, setModalOpen] = useState(false);
const [innerValue, setInnerValue] = useState<{ nodeId: string; title: string }>(props.value);
const nodeTitle = useMemo(() => {
const nodeInfo = props.pageModel.getNode(innerValue?.nodeId || props.value);
if (nodeInfo) {
return nodeInfo.value.title || nodeInfo.material?.value.title || innerValue?.title || '';
}
return '';
}, [props.pageModel, props?.value, innerValue?.nodeId, innerValue?.title]);
return (
<>
setModalOpen(true)}
>
{nodeTitle ?? '选择节点'}
setModalOpen(false)}
onOk={(data) => {
props.onChange?.(data);
setInnerValue(data);
setModalOpen(false);
}}
pageModel={props.pageModel}
value={innerValue?.nodeId || props.value}
/>
>
);
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/component/SelectNodeByTree/modal.tsx
================================================
import { Modal, TreeSelect } from 'antd';
import { CPage } from '@chamn/model';
import { useRef, useMemo } from 'react';
import { transformPageSchemaToTreeData, traverseTree } from '@/plugins/OutlineTree/util';
import { getNodeInfo } from '../SelectNodeByTree/util';
interface SelectNodeModalProps {
open: boolean;
onCancel: () => void;
onOk: (data: { nodeId: string; title: string }) => void;
pageModel: CPage;
value?: string;
}
export const SelectNodeModal = (props: SelectNodeModalProps) => {
const { open, onCancel, onOk, pageModel, value } = props;
const boxDomRef = useRef(null);
const treeData = useMemo(() => {
if (!pageModel) return;
const treeData = transformPageSchemaToTreeData(pageModel?.export(), pageModel);
traverseTree(treeData, (el: any) => {
el.sourceData = {
title: el.title,
value: el.value,
};
el.value = el.key;
el.title = {el.title}
;
return false;
});
return treeData;
}, [pageModel]);
return (
onOk({ nodeId: value || '', title: '' })}>
{
onOk({
nodeId: '',
title: '',
});
}}
filterTreeNode={(inputValue, treeNode: any) => {
return treeNode.sourceData?.title.toLowerCase().indexOf(inputValue.toLowerCase()) > -1;
}}
getPopupContainer={() => boxDomRef.current!}
treeDefaultExpandAll
onChange={(newVal) => {
const nodeInfo = getNodeInfo(newVal, (treeData as any) ?? []);
onOk({
nodeId: newVal,
title: (nodeInfo as any)?.sourceData?.title,
});
}}
treeData={treeData}
/>
);
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/component/SelectNodeByTree/util.ts
================================================
import { TreeDataNode } from 'antd';
export const getParentKey = (key: React.Key, tree: TreeDataNode[]): React.Key => {
let parentKey: React.Key;
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node.children) {
if (node.children.some((item) => item.key === key)) {
parentKey = node.key;
} else if (getParentKey(key, node.children)) {
parentKey = getParentKey(key, node.children);
}
}
}
return parentKey!;
};
export const getNodeInfo = (key: string, tree: TreeDataNode[]): TreeDataNode | null => {
const traverse = (nodes: TreeDataNode[]): TreeDataNode | null => {
for (const node of nodes) {
if (node.key === key) {
return node;
}
if (node.children?.length) {
const result = traverse(node.children);
if (result) {
return result;
}
}
}
return null;
};
return traverse(tree);
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/component/SelectNodeState/index.tsx
================================================
import { CPage } from '@chamn/model';
import { SelectNodeByTree } from '../SelectNodeByTree';
import { Input, Space } from 'antd';
export const SelectNodeState = (props: {
value?: {
nodeId: string;
keyPath: string;
};
pageModel: CPage;
onChange?: (data: { nodeId: string; keyPath: string }) => void;
}) => {
return (
{
props.onChange?.({
nodeId: data.nodeId,
keyPath: props.value?.keyPath || '',
});
}}
/>
{
const { value: inputValue } = e.target;
props.onChange?.({
nodeId: props.value?.nodeId || '',
keyPath: String(inputValue),
});
}}
placeholder="variable name. support '.'"
/>
);
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/component/SelectNodeState/util.ts
================================================
import { TreeDataNode } from 'antd';
import { Key } from 'react';
export const getParentKey = (key: React.Key, tree: TreeDataNode[]): React.Key => {
let parentKey: React.Key;
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node.children) {
if (node.children.some((item) => item.key === key)) {
parentKey = node.key;
} else if (getParentKey(key, node.children)) {
parentKey = getParentKey(key, node.children);
}
}
}
return parentKey!;
};
export const generateKeyList = (data: TreeDataNode[]) => {
let dataList: { title: string; key: Key }[] = [];
for (let i = 0; i < data.length; i++) {
const node = data[i];
const { key } = node;
dataList.push({ key, title: node.title as string });
if (node.children) {
const res = generateKeyList(node.children);
dataList = [...dataList, ...res];
}
}
return dataList;
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/config.ts
================================================
export const REACT_FLOW_DRAG_CLASS_NAME = 'chamn-action-drag-handler';
export const INPUT_HANDLE_ID = 'INPUT_HANDLE_ID';
export const OUTPUT_HANDLE_ID = 'OUTPUT_HANDLE_ID';
export const REQUEST_API_FAILED_HANDLE_ID = 'REQUEST_API_FAILED_HANDLE_ID';
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/context.ts
================================================
import { createContext, useContext } from 'react';
import { CNode, CPage } from '@chamn/model';
import { CPluginCtx } from '@/core/pluginManager';
interface ActionFlowContextType {
pluginCtx: CPluginCtx;
pageModel: CPage;
/** 数据有改变时,包含节点内部的数据 */
onDataChange: () => void;
/** 当前节点 */
nodeModel: CNode | null;
}
export const ActionFlowContext = createContext({
pluginCtx: null as any,
pageModel: null as any,
onDataChange: () => {},
nodeModel: null,
});
export const useActionFlow = () => {
const context = useContext(ActionFlowContext);
if (!context) {
throw new Error('useActionFlow must be used within ActionFlowProvider');
}
return context;
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/index.tsx
================================================
import {
addEdge,
Background,
Controls,
MiniMap,
OnConnect,
ReactFlow,
ReactFlowProvider,
useEdgesState,
useNodesState,
useReactFlow,
Node,
} from '@xyflow/react';
import '@xyflow/react/dist/style.css';
import { useCallback, useEffect, useRef, useState } from 'react';
import { TActionLogicItem } from '@chamn/model';
import { calculateElementLayout, parseActionLogicToNodeList, revertNodeToActionLogic } from './util';
import { NODE_MAP, NODE_TYPE } from './node';
import { CSetterProps } from '../type';
import { REACT_FLOW_DRAG_CLASS_NAME } from './config';
import { ActionFlowContext } from './context';
import { Button } from 'antd';
import { MoveableModal } from '@/component/MoveableModal';
import { HotKeysPluginInstance } from '@/plugins/Hotkeys/type';
export type TActionFlowSetterCore = CSetterProps<{
value?: TActionLogicItem;
children?: React.ReactNode;
}>;
export const ActionFlowSetterCore = (props: TActionFlowSetterCore) => {
const { fitView } = useReactFlow();
const [flowMount, setFlowMount] = useState(false);
const [nodes, setNodes, onNodesChange] = useNodesState([
{
id: NODE_TYPE.START_NODE,
data: { id: NODE_TYPE.START_NODE },
position: { x: 0, y: 0 },
type: NODE_TYPE.START_NODE,
dragHandle: `.${REACT_FLOW_DRAG_CLASS_NAME}`,
selectable: false,
},
]);
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
const latesEdgesRef = useRef([]);
latesEdgesRef.current = edges;
const onConnect = useCallback((params) => setEdges((eds) => addEdge({ ...params }, eds)), [setEdges]);
const latestNodeRef = useRef([]);
latestNodeRef.current = nodes;
const [dataReady, setDataReady] = useState(false);
/** 重新布局 */
const layoutGraph = useCallback(
(options?: { fitView: boolean }) => {
const layoutInfo = calculateElementLayout(latestNodeRef.current, edges, { direction: 'TB' });
setNodes([...layoutInfo.nodes]);
setEdges([...layoutInfo.edges]);
setTimeout(() => {
setFlowMount(true);
if (options?.fitView !== false) {
fitView({});
}
});
},
[edges, fitView, setEdges, setNodes]
);
useEffect(() => {
// 将 value 转换为 nodes 以及 edges
const { nodes, edges } = parseActionLogicToNodeList(props.value);
setNodes(nodes);
setEdges(edges);
setTimeout(() => {
setDataReady(true);
}, 300);
}, [props.value, setEdges, setNodes]);
const saveData = useCallback(() => {
setTimeout(() => {
const newSchemaValue = revertNodeToActionLogic({ nodes: latestNodeRef.current, edges: latesEdgesRef.current });
props.onValueChange?.(newSchemaValue);
});
}, [props]);
return (
layoutGraph({ fitView: false })}
>
Reset Layout
{!flowMount && (
)}
{dataReady && (
{
onNodesChange(changes);
saveData();
}}
onEdgesChange={(changes) => {
onEdgesChange(changes);
saveData();
}}
onConnect={(connection) => {
onConnect(connection);
saveData();
}}
defaultEdgeOptions={{
type: 'smoothstep',
}}
minZoom={0.2}
maxZoom={1}
onInit={() => {
layoutGraph();
}}
fitView
nodeTypes={NODE_MAP}
>
)}
);
};
export const ActionFlowSetter = (props: TActionFlowSetterCore) => {
const [open, setOpen] = useState(false);
const newValueRef = useRef(props.value);
const disableLowcodeHotKey = async (status: boolean) => {
// 启用 lowcode 编辑器热键
const hotkey = await props.setterContext?.pluginCtx?.pluginManager?.get('Hotkeys');
hotkey?.export.disable(status);
};
const triggerView = props.children || (
Edit Flow
);
return (
<>
{
// 禁用 lowcode 编辑器热键
await disableLowcodeHotKey(true);
setOpen(true);
}}
>
{triggerView}
{
setOpen(false);
disableLowcodeHotKey(false);
}}
onOk={async () => {
props.onValueChange?.(newValueRef.current);
await disableLowcodeHotKey(false);
setOpen(false);
}}
>
{
newValueRef.current = newVal;
}}
>
>
);
};
ActionFlowSetter.setterName = '逻辑流设置器';
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/AssignValueNode/index.tsx
================================================
import { BUILD_IN_SETTER_MAP, CustomSchemaFormInstance } from '@/component/CustomSchemaForm';
import { AssignValueType, DEV_CONFIG_KEY, TLogicAssignValueItem } from '@chamn/model';
import { NodeProps, Node } from '@xyflow/react';
import { Radio } from 'antd';
import { useEffect, useRef, useState } from 'react';
import { CForm } from '../../../../Form';
import { CField } from '../../../../Form/Field';
import { SelectNodeState } from '../../component/SelectNodeState/index';
import styles from './style.module.scss';
import { TTextAreaSetterProps } from '../../../TextAreaSetter';
import { isValidJSVariableName } from './util';
import { CCustomSchemaFormContext } from '@/component/CustomSchemaForm/context';
import { ensureKeyExist } from '@/utils';
import { NodeCard } from '../../component/NodeCard';
import { CommonDynamicValueSetter } from '../../util';
import { useActionFlow } from '../../context';
import { CFiledWithSwitchSetter } from '@/component/CustomSchemaForm/components/CFiledWithSwitchSetter';
export type TAssignValueNode = Node;
export const AssignValueNode = (props: NodeProps) => {
const { data } = props;
const { onDataChange, pageModel, pluginCtx, nodeModel } = useActionFlow();
ensureKeyExist(data, DEV_CONFIG_KEY, {});
const devConfigObj = data[DEV_CONFIG_KEY]!;
const [isReady, setIsReady] = useState(false);
const formRef = useRef(null);
const [formValue, setFormValue] = useState();
useEffect(() => {
const newVal = {
id: data.id,
type: data.type,
valueType: data.valueType,
currentValue: data.currentValue,
targetValueName: data.targetValueName || '',
};
formRef.current?.setFields(newVal);
setFormValue(newVal);
setIsReady(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
{
if (!devConfigObj.defaultSetterMap) {
devConfigObj.defaultSetterMap = {};
}
devConfigObj.defaultSetterMap[keyPaths.join('.')] = {
name: keyPaths.join('.'),
setter: setterName,
};
},
pluginCtx: pluginCtx,
nodeId: nodeModel?.id,
customSetterMap: {},
}}
>
{
Object.assign(data, newVal);
setFormValue(newVal as any);
onDataChange();
}}
>
{isReady && (
<>
{
return value;
}}
>
{formValue?.valueType === 'STATE' && (
)}
{formValue?.valueType === 'MEMORY' && (
)}
>
)}
);
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/AssignValueNode/style.module.scss
================================================
.line {
padding-bottom: 10px;
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/AssignValueNode/util.ts
================================================
export function isValidJSVariableName(name: string) {
// 1. 使用正则表达式验证变量名规则
const identifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
if (!identifierRegex.test(name)) {
return false;
}
// 2. 检测是否为保留关键字
try {
new Function(`let ${name};`);
return true;
} catch {
return false;
}
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/CallNodeMethodNode/index.tsx
================================================
import { BUILD_IN_SETTER_MAP, CustomSchemaForm, CustomSchemaFormInstance } from '@/component/CustomSchemaForm';
import { DEV_CONFIG_KEY, TLogicCallNodeMethodItem } from '@chamn/model';
import { NodeProps, Node } from '@xyflow/react';
import { Input, Select } from 'antd';
import { useEffect, useMemo, useRef, useState } from 'react';
import { CForm } from '../../../../Form';
import styles from './style.module.scss';
import { CCustomSchemaFormContext } from '@/component/CustomSchemaForm/context';
import { SelectNodeByTree } from '../../component/SelectNodeByTree';
import { CField } from '@/component/CustomSchemaForm/components/Form/Field';
import { formatArgsObjToArray, formatArgsToObject, getArgsObjFormSchema, isValidJSVariableName } from './util';
import { ensureKeyExist } from '@/utils';
import { NodeCard } from '../../component/NodeCard';
import { useActionFlow } from '../../context';
export type TCallNodeMethodNode = Node;
export const CallNodeMethodNode = (props: NodeProps) => {
const { data } = props;
const { pageModel, onDataChange, pluginCtx, nodeModel } = useActionFlow();
ensureKeyExist(data, DEV_CONFIG_KEY, {});
const devConfigObj = data[DEV_CONFIG_KEY]!;
const formRef = useRef(null);
const [formValue, setFormValue] = useState();
useEffect(() => {
const newVal = {
id: data.id || '',
type: data.type,
nodeId: data.nodeId,
methodName: data.methodName,
args: data.args,
returnVarName: data.returnVarName,
};
formRef.current?.setFields(newVal);
setFormValue(newVal);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const methodList = useMemo(() => {
const targetNode = pageModel.getNode(data.nodeId);
const list = targetNode?.material?.value.methods || [];
return list;
}, [data.nodeId, pageModel]);
const methodListOptions = useMemo(() => {
return methodList?.map((el) => {
return {
value: el.name,
label: el.title,
};
});
}, [methodList]);
const argsFormSchema = useMemo(() => {
return getArgsObjFormSchema(pageModel.getNode(formValue?.nodeId)!, formValue?.methodName || '');
}, [pageModel, formValue?.nodeId, formValue?.methodName]);
const updateKeySetterConfig = (keyPaths: string[], setterName: string) => {
if (!devConfigObj.defaultSetterMap) {
devConfigObj.defaultSetterMap = {};
}
devConfigObj.defaultSetterMap[keyPaths.join('.')] = {
name: keyPaths.join('.'),
setter: setterName,
};
};
return (
{
if (changeKeys?.includes('nodeId')) {
newVal = {
...newVal,
methodName: '',
args: [],
};
setTimeout(() => {
formRef.current?.setFields({
...formValue,
...newVal,
});
});
}
Object.assign(data, newVal);
setFormValue(newVal as any);
onDataChange();
}}
>
{
return el.nodeId;
}}
>
Boolean(argsFormSchema.length)}
noStyle
formatEventValue={(val) => {
const newVal = formatArgsObjToArray(val);
return newVal;
}}
>
{
if (val === '') {
return true;
}
return isValidJSVariableName(val);
},
},
]}
valueChangeEventName="onChange"
formatEventValue={(e) => e.target.value}
>
);
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/CallNodeMethodNode/style.module.scss
================================================
.line {
padding-bottom: 10px;
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/CallNodeMethodNode/util.ts
================================================
import { CMaterialPropsType, CPageNode, isNodeModel } from '@chamn/model';
import { CommonDynamicValueSetter } from '../../util';
export function isValidJSVariableName(name: string) {
// 1. 使用正则表达式验证变量名规则
const identifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
if (!identifierRegex.test(name)) {
return false;
}
// 2. 检测是否为保留关键字
try {
new Function(`let ${name};`);
return true;
} catch {
return false;
}
}
const ARGS_PREFIX = 'args.';
/**
*
* @param node 结合 method 描述,合并为一个 FormSchema, 加 key 前缀是为了避免 key 冲突
* @param args
* @returns
*/
export const getArgsObjFormSchema = (node: CPageNode, methodName: string) => {
if (isNodeModel(node)) {
const methodList = node?.material?.value.methods || [];
const formSchema = methodList
.find((el) => el.name === methodName)
?.params?.map((el, index) => {
return {
name: `${ARGS_PREFIX}${index}`,
title: {
label: el.name || `arg[${index}]`,
tip: el.description,
},
valueType: 'string',
setters: CommonDynamicValueSetter,
} as CMaterialPropsType[number];
});
return formSchema || [];
}
return [];
};
export const formatArgsObjToArray = (val: Record) => {
const res = Object.keys(val)
.map((key) => parseInt(key.replace(ARGS_PREFIX, '')))
.sort()
.map((index) => val[`${ARGS_PREFIX}${index}`]);
return res;
};
export const formatArgsToObject = (valArr: any[]) => {
const res: any = {};
valArr.forEach((item, index) => {
res[`${ARGS_PREFIX}${index}`] = item;
});
return res;
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/JumpLinkNode.tsx
================================================
import { BUILD_IN_SETTER_MAP, CustomSchemaFormInstance } from '@/component/CustomSchemaForm';
import { isExpression, isFunction, TLogicJumpLinkItem } from '@chamn/model';
import { NodeProps, Node } from '@xyflow/react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { CForm } from '../../../Form';
import { NodeCard } from '../component/NodeCard';
import { CommonDynamicValueSetter } from '../util';
import { useActionFlow } from '../context';
import { CFiledWithSwitchSetter } from '../../../CFiledWithSwitchSetter';
export type TJumpLinkNode = Node;
export const JumpLinkNode = (props: NodeProps) => {
const { data } = props;
const { onDataChange } = useActionFlow();
const formRef = useRef(null);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
formRef.current?.setFields({
link: data.link,
});
setIsReady(true);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const defaultLinkSetter = useMemo(() => {
if (isFunction(data.link)) {
return 'FunctionSetter';
} else if (isExpression(data.link)) {
return 'ExpressionSetter';
} else {
return 'TextAreaSetter';
}
}, [data.link]);
return (
{
Object.assign(data, newVal);
onDataChange();
}}
>
{isReady && (
<>
>
)}
);
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/RequestAPINode/helper.ts
================================================
import { SetterType } from '@chamn/model';
export const requestParamsSchemaSetterList: SetterType[] = [
{
componentName: 'JSONSetter',
labelAlign: 'start',
props: {
mode: 'inline',
lineNumbers: 'off',
editorOptions: {
lineNumbers: 'off',
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
glyphMargin: false,
},
},
},
{
componentName: 'FunctionSetter',
labelAlign: 'start',
props: {
mode: 'inline',
minimap: false,
containerStyle: {
paddingTop: '10px',
width: '470px',
height: '150px',
},
lineNumber: false,
},
},
];
export const methodOptions = [
{ value: 'GET', label: 'GET' },
{ value: 'POST', label: 'POST' },
{ value: 'PUT', label: 'PUT' },
{ value: 'PATCH', label: 'PATCH' },
{ value: 'DELETE', label: 'DELETE' },
];
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/RequestAPINode/index.tsx
================================================
import { DEV_CONFIG_KEY, TLogicRequestAPIItem } from '@chamn/model';
import { useReactFlow, NodeProps, Node } from '@xyflow/react';
import { useEffect, useMemo, useRef, useState } from 'react';
import { Input, Select, Tabs, TabsProps } from 'antd';
import { BUILD_IN_SETTER_MAP, CustomSchemaFormInstance } from '@/component/CustomSchemaForm';
import { ensureKeyExist } from '@/utils';
import { NodeCard } from '../../component/NodeCard';
import { CForm } from '@/component/CustomSchemaForm/components/Form';
import { CCustomSchemaFormContext } from '@/component/CustomSchemaForm/context';
import { CField } from '@/component/CustomSchemaForm/components/Form/Field';
import { methodOptions, requestParamsSchemaSetterList } from './helper';
import { CreateNewNodePopup } from '../../component/CreateNewNodePopup';
import { InputHandle } from '../../component/InputHandle';
import { OutputHandle } from '../../component/OutputHandle';
import {
INPUT_HANDLE_ID,
OUTPUT_HANDLE_ID,
REACT_FLOW_DRAG_CLASS_NAME,
REQUEST_API_FAILED_HANDLE_ID,
} from '../../config';
import styles from './style.module.scss';
import { useActionFlow } from '../../context';
import { CFiledWithSwitchSetter } from '@/component/CustomSchemaForm/components/CFiledWithSwitchSetter';
import { RightPanelConfig } from '@/plugins/RightPanel/type';
export type TRequestAPINode = Node;
export const RequestAPINode = (props: NodeProps) => {
const { data, isConnectable } = props;
const { onDataChange, pluginCtx } = useActionFlow();
const pluginConfig: RightPanelConfig = pluginCtx.config || {};
const CustomAPISelectInput: any = useMemo(() => {
if (pluginConfig.requestAPINode?.customAPIInput) {
return pluginConfig.requestAPINode?.customAPIInput;
}
return Input;
}, [pluginConfig.requestAPINode?.customAPIInput]);
const reactFlowInstance = useReactFlow();
ensureKeyExist(data, DEV_CONFIG_KEY, {});
const devConfigObj = data[DEV_CONFIG_KEY]!;
const formRef = useRef(null);
const [formValue, setFormValue] = useState();
const checkHandleConnection = (handleId: string) => {
const edges = reactFlowInstance.getEdges();
return edges.some(
(edge) => edge.source === String(data.id) && (edge.sourceHandle || OUTPUT_HANDLE_ID) === handleId
);
};
const isOutputHandleConnected = checkHandleConnection(OUTPUT_HANDLE_ID);
const isAfterFailedResponseHandleConnected = checkHandleConnection(REQUEST_API_FAILED_HANDLE_ID);
useEffect(() => {
const newVal = {
id: data.id,
type: data.type,
apiPath: data.apiPath,
body: data.body,
query: data.query,
header: data.header,
method: data.method || 'GET',
responseVarName: data.responseVarName || '',
afterSuccessResponse: data.afterSuccessResponse || [],
afterFailedResponse: data.afterFailedResponse || [],
} as TLogicRequestAPIItem;
formRef.current?.setFields(newVal);
setFormValue(newVal);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const formHandler = useMemo(() => {
return {
updateFields: (newValue: any) => {
const newValueObj = {
...formValue,
...newValue,
};
formRef.current?.setFields({
...newValueObj,
});
setFormValue(newValueObj);
},
getFields: () => {
return formRef.current?.getFieldsValue();
},
};
}, [formValue]);
const updateKeySetterConfig = (keyPaths: string[], setterName: string) => {
if (!devConfigObj.defaultSetterMap) {
devConfigObj.defaultSetterMap = {};
}
devConfigObj.defaultSetterMap[keyPaths.join('.')] = {
name: keyPaths.join('.'),
setter: setterName,
};
};
const tabItems = useMemo(() => {
const tabTagList = [
{ key: 'header', label: 'Header' },
{ key: 'query', label: 'Query' },
{ key: 'body', label: 'Body' },
];
const items: TabsProps['items'] = tabTagList.map((el) => {
return {
...el,
disabled: el.key === 'body' && formValue?.method === 'GET',
children: (
),
};
});
return items;
}, [formValue]);
const handleNewNodeAdd = (newNodeData: any, handleType: string) => {
const currentNode = reactFlowInstance.getNode(String(data.id));
if (!currentNode) return;
let offsetX = 0;
if (handleType !== OUTPUT_HANDLE_ID) {
offsetX = (currentNode.measured?.width ?? 0) + 150;
}
// 计算新节点位置
const newNodePosition = {
x: currentNode.position.x + offsetX,
y: currentNode.position.y + (currentNode.measured?.height ?? 0) + 150,
};
// 创建新节点
const newNode = {
id: newNodeData.id,
type: newNodeData.type,
position: newNodePosition,
/** 必须 */
dragHandle: `.${REACT_FLOW_DRAG_CLASS_NAME}`,
data: {
...newNodeData,
},
};
// 创建连线
const newEdge = {
id: `${data.id}_${newNode.id}`,
source: String(data.id),
sourceHandle: handleType,
target: newNode.id,
targetHandle: INPUT_HANDLE_ID,
};
// 更新流程图
reactFlowInstance.addNodes(newNode);
reactFlowInstance.addEdges(newEdge);
if (handleType === OUTPUT_HANDLE_ID) {
// 更新当前节点的 afterSuccessResponse
if (!data.afterSuccessResponse) {
data.afterSuccessResponse = [];
}
data.afterSuccessResponse.push(newNodeData);
} else {
// 更新当前节点的 afterFailedResponse
if (!data.afterFailedResponse) {
data.afterFailedResponse = [];
}
data.afterFailedResponse.push(newNodeData);
}
};
return (
handleNewNodeAdd(data, OUTPUT_HANDLE_ID)}
disabled={isOutputHandleConnected}
>
handleNewNodeAdd(data, REQUEST_API_FAILED_HANDLE_ID)}
disabled={isAfterFailedResponseHandleConnected}
>
>
}
nodeProps={props}
handleNewNodeAdd={() => {}}
>
{
setFormValue(newFormData as any);
Object.assign(data, newFormData);
onDataChange();
}}
>
el.target.value}
>
el.target.value}
>
);
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/RequestAPINode/style.module.scss
================================================
.line {
padding-bottom: 10px;
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/RequestAPINode/util.ts
================================================
export function isValidJSVariableName(name: string) {
// 1. 使用正则表达式验证变量名规则
const identifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
if (!identifierRegex.test(name)) {
return false;
}
// 2. 检测是否为保留关键字
try {
new Function(`let ${name};`);
return true;
} catch {
return false;
}
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/RunCodeNode/index.tsx
================================================
import { DEV_CONFIG_KEY, TLogicRunCodeItem } from '@chamn/model';
import { NodeProps, Node } from '@xyflow/react';
import { ensureKeyExist } from '@/utils';
import { FunctionSetter } from '../../../FunctionSetter';
import { NodeCard } from '../../component/NodeCard';
import { useActionFlow } from '../../context';
export type TRunCodeNode = Node;
export const RunCodeNode = (props: NodeProps) => {
const { data } = props;
ensureKeyExist(data, DEV_CONFIG_KEY, {});
const { onDataChange, pluginCtx, nodeModel } = useActionFlow();
return (
{
data.value = newVal.value;
onDataChange();
}}
setterContext={{
pluginCtx: pluginCtx,
setCollapseHeaderExt: undefined,
onSetterChange: function () {},
keyPaths: [],
label: '',
nodeModel: nodeModel as any,
}}
/>
);
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/RunCodeNode/style.module.scss
================================================
.line {
padding-bottom: 10px;
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/RunCodeNode/util.ts
================================================
export function isValidJSVariableName(name: string) {
// 1. 使用正则表达式验证变量名规则
const identifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
if (!identifierRegex.test(name)) {
return false;
}
// 2. 检测是否为保留关键字
try {
new Function(`let ${name};`);
return true;
} catch {
return false;
}
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/StartNode.tsx
================================================
import { NodeProps, Node } from '@xyflow/react';
import { NodeCard } from '../component/NodeCard';
export type CounterNode = Node;
export const StartNode = (props: NodeProps) => {
return (
Start
);
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/node/index.ts
================================================
import { LogicType } from '@chamn/model';
import { AssignValueNode } from './AssignValueNode';
import { StartNode } from './StartNode';
import { CallNodeMethodNode } from './CallNodeMethodNode';
import { JumpLinkNode } from './JumpLinkNode';
import { RequestAPINode } from './RequestAPINode';
import { RunCodeNode } from './RunCodeNode';
export enum NODE_TYPE {
START_NODE = 'START_NODE',
JUMP_LINK = LogicType.JUMP_LINK,
ASSIGN_VALUE = LogicType.ASSIGN_VALUE,
CALL_NODE_METHOD = LogicType.CALL_NODE_METHOD,
RUN_CODE = LogicType.RUN_CODE,
REQUEST_API = LogicType.REQUEST_API,
}
export const NODE_MAP = {
[NODE_TYPE.START_NODE]: StartNode,
[NODE_TYPE.JUMP_LINK]: JumpLinkNode,
[NODE_TYPE.ASSIGN_VALUE]: AssignValueNode,
[NODE_TYPE.CALL_NODE_METHOD]: CallNodeMethodNode,
[NODE_TYPE.RUN_CODE]: RunCodeNode,
[NODE_TYPE.REQUEST_API]: RequestAPINode,
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ActionFlowSetter/util.ts
================================================
import { Edge, Node, useReactFlow } from '@xyflow/react';
import Dagre from '@dagrejs/dagre';
import { getRandomStr, SetterType, TActionLogicItem, TLogicItemHandlerFlow } from '@chamn/model';
import { INPUT_HANDLE_ID, OUTPUT_HANDLE_ID, REACT_FLOW_DRAG_CLASS_NAME, REQUEST_API_FAILED_HANDLE_ID } from './config';
import { NODE_TYPE } from './node';
import { message } from 'antd';
/** 自动布局 flow node */
export const calculateElementLayout = (
nodes: Node[],
edges: Edge[],
options: {
direction: 'TB' | 'LR';
}
): { nodes: Node[]; edges: Edge[] } => {
const g = new Dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));
g.setGraph({ rankdir: options.direction });
edges.forEach((edge) => g.setEdge(edge.source, edge.target));
nodes.forEach((node) =>
g.setNode(node.id, {
...node,
width: node.measured?.width ?? 0,
height: node.measured?.height ?? 0,
})
);
Dagre.layout(g);
return {
nodes: nodes.map((node) => {
const position = g.node(node.id);
// We are shifting the dagre node position (anchor=center center) to the top left
// so it matches the React Flow node anchor point (top left).
const x = position.x - (node.measured?.width ?? 0) / 2;
const y = position.y - (node.measured?.height ?? 0) / 2;
return { ...node, position: { x, y } };
}),
edges,
};
};
const createFlowNode = (nodeData: Partial): Node => {
const nodeId = nodeData.id || getRandomStr();
return {
id: nodeId,
type: nodeData.type,
position: { x: 0, y: 0 },
dragHandle: `.${REACT_FLOW_DRAG_CLASS_NAME}`,
selectable: nodeData.type === NODE_TYPE.START_NODE ? false : true,
data: {
...nodeData,
id: nodeId,
},
};
};
/** 创建边 */
const createFlowEdge = (source: string, target: string, sourceHandle = OUTPUT_HANDLE_ID) => {
return {
id: `${source}_${target}`,
source,
sourceHandle,
target,
targetHandle: INPUT_HANDLE_ID,
};
};
/** 将 json schema 转换为 react-flow 节点和边信息 */
export const parseActionLogicToNodeList = (value: TActionLogicItem) => {
const nodes: Node[] = [
createFlowNode({
id: NODE_TYPE.START_NODE,
type: NODE_TYPE.START_NODE,
}),
];
const edges: Edge[] = [];
if (!value?.handler?.length) {
return { nodes: nodes, edges: edges };
}
const processNodes = (
items: TLogicItemHandlerFlow,
options?: {
/**
* 用于标记是从那种类型的 output flag id 导出的 flow
*/
sourceHandler: string;
}
) => {
items.forEach((item) => {
const currentNode = createFlowNode(item);
nodes.push(currentNode);
if (item.next) {
edges.push(createFlowEdge(item.id, String(item.next), options?.sourceHandler));
}
if (item.type === 'REQUEST_API') {
// 处理成功分支节点
if (item.afterSuccessResponse?.length) {
edges.push(createFlowEdge(item.id, String(item.afterSuccessResponse[0].id)));
processNodes(item.afterSuccessResponse);
}
// 处理失败分支节点
if (item.afterFailedResponse?.length) {
edges.push(createFlowEdge(item.id, String(item.afterFailedResponse[0].id), REQUEST_API_FAILED_HANDLE_ID));
processNodes(item.afterFailedResponse, {
sourceHandler: REQUEST_API_FAILED_HANDLE_ID,
});
}
}
});
};
const nextItem = value.handler[0];
edges.push(createFlowEdge(NODE_TYPE.START_NODE, nextItem.id));
processNodes(value.handler);
return { nodes, edges };
};
/** 将节点格式化json schema */
export const revertNodeToActionLogic = (params: { nodes: Node[]; edges: Edge[] }): TActionLogicItem => {
const { nodes, edges } = params;
const result: TActionLogicItem = {
type: 'ACTION',
handler: [],
};
// 找到起始节点
const startNode = nodes.find((node) => node.type === NODE_TYPE.START_NODE);
if (!startNode) return result;
const nodeMap = new Map(nodes.map((node) => [node.id, node]));
const visited = new Set();
const traverseNodes = (currentNodeId: string) => {
if (visited.has(currentNodeId)) return [];
visited.add(currentNodeId);
const currentNode = nodeMap.get(currentNodeId);
if (!currentNode) return [];
const handlers = [];
const nodeData: any = { ...currentNode.data, id: currentNodeId };
if (currentNode.type === 'REQUEST_API') {
// 获取成功分支节点
const successTargets = edges
.filter((e) => e.source === currentNodeId && e.sourceHandle === OUTPUT_HANDLE_ID)
.map((e) => traverseNodes(e.target))
.flat();
// 获取失败分支节点
const failedTargets = edges
.filter((e) => e.source === currentNodeId && e.sourceHandle === REQUEST_API_FAILED_HANDLE_ID)
.map((e) => traverseNodes(e.target))
.flat();
nodeData.afterSuccessResponse = successTargets;
nodeData.afterFailedResponse = failedTargets;
handlers.push(nodeData);
return handlers;
}
handlers.push(nodeData);
// 获取下一个主流程节点, 理论上只有能一个, 分叉逻辑需要使用特殊节点处理
const nextEdges = edges.filter((e) => e.source === currentNodeId && e.sourceHandle === OUTPUT_HANDLE_ID);
if (nextEdges.length > 1) {
message.error('不允许存在一个节点连接多个节点');
throw new Error('不允许存在一个节点连接多个节点');
}
if (nextEdges.length !== 0) {
/** 理论上只会有一个元素 */
nextEdges.forEach((edge) => {
const nextHandlers = traverseNodes(edge.target);
handlers.push(...nextHandlers);
nodeData.next = nextHandlers[0]?.id ?? null;
});
}
return handlers;
};
const handler = traverseNodes(startNode.id);
// 移除 start node
handler.shift();
result.handler = handler;
return result;
};
/** 通用的 flow action 画布中的 setter 配置 */
export const CommonDynamicValueSetter: SetterType[] = [
'StringSetter',
'NumberSetter',
'ExpressionSetter',
'JSONSetter',
{
componentName: 'FunctionSetter',
props: {
mode: 'inline',
minimap: false,
lineNumber: false,
containerStyle: {
width: '500px',
height: '250px',
},
},
},
];
export const UseNodeHasConnected = function (data: any, handleId: string) {
const reactFlowInstance = useReactFlow();
const edges = reactFlowInstance.getEdges();
return edges.some((edge) => edge.source === String(data.id) && (edge.sourceHandle || OUTPUT_HANDLE_ID) === handleId);
};
export const getNewNodePosInfo = function (currentNode: Node, newNodeData: any, sourceHandle?: string) {
// 计算新节点位置
const newNodePosition = {
x: currentNode.position.x,
y: currentNode.position.y + (currentNode.measured?.height ?? 0) + 150,
};
// 创建新节点
const newNode = {
id: newNodeData.id,
type: newNodeData.type,
position: newNodePosition,
/** 必须 */
dragHandle: `.${REACT_FLOW_DRAG_CLASS_NAME}`,
data: {
...newNodeData,
},
};
// 创建连线
const newEdge = {
id: `${currentNode.data.id}_${newNode.id}`,
source: String(currentNode.data.id),
sourceHandle: sourceHandle ?? OUTPUT_HANDLE_ID,
target: newNode.id,
targetHandle: INPUT_HANDLE_ID,
};
return {
newEdge,
newNode,
};
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/AdvanceSetterList.ts
================================================
import { ActionFlowSetter } from './ActionFlowSetter';
import { CSetter } from './type';
/** 需要单独导出避免循环以来,因为 ActionFlowSetter 使用了大量内置 setter */
export const BUILD_IN_ADVANCE_SETTER_MAP = {
ActionFlowSetter,
} as Record;
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/AntDColorSetter/index.tsx
================================================
import { ColorPicker, ConfigProvider } from 'antd';
import { CSetter, CSetterProps } from '../type';
import { DEFAULT_PRESET_COLORS } from '@/config/colorPickerColorList';
type ColorSetterProps = {
initialValue: string;
};
export const AntDColorSetter: CSetter = ({
onValueChange,
initialValue,
value,
setterContext,
...restProps
}: CSetterProps) => {
return (
{
onValueChange?.(color.toRgbString());
}}
presets={DEFAULT_PRESET_COLORS}
{...restProps}
/>
);
};
AntDColorSetter.setterName = '颜色设置器';
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ArraySetter/ArrayItem.tsx
================================================
import React, { useEffect, useRef } from 'react';
import { CForm } from '../../Form';
import { SetterSwitcher } from '../../SetterSwitcher';
import { DeleteOutlined } from '@ant-design/icons';
import { SetterObjType } from '@chamn/model';
export function ArrayItem(props: {
index: number;
labelPrefix?: string;
keyPaths: string[];
value: Record;
setters: SetterObjType[];
style: React.CSSProperties;
onValueChange: (val: Record) => void;
onDelete: () => void;
}) {
const { index, keyPaths, setters } = props;
const style = {
...props.style,
};
const objectValue = {
[index]: props.value,
};
const formRef = useRef(null);
useEffect(() => {
formRef.current?.setFields({
[index]: props.value,
});
}, [index, props.value]);
return (
}
name={String(index)}
label={`${props.labelPrefix ?? `元素-${index}`}`}
keyPaths={[...keyPaths, String(index)]}
setters={setters}
>
);
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ArraySetter/SortItemOrderModal.tsx
================================================
import React, { useEffect, useRef, useState } from 'react';
import { CSS, Transform } from '@dnd-kit/utilities';
import { Modal, ModalProps } from 'antd';
import {
DndContext,
DragEndEvent,
KeyboardSensor,
PointerSensor,
useDraggable,
useSensor,
useSensors,
} from '@dnd-kit/core';
import { arrayMove, SortableContext, sortableKeyboardCoordinates, useSortable } from '@dnd-kit/sortable';
import { restrictToVerticalAxis } from '@dnd-kit/modifiers';
import { getRandomStr } from '@chamn/model';
import styles from './style.module.scss';
export type SortItemOrderProps = {
list: any[];
keyPaths: string[];
label: string;
sortLabelKey?: string;
onValueChange?: (newList: any[]) => void;
} & ModalProps;
export const SortItemOrderModal = ({
list,
onValueChange,
keyPaths,
label,
sortLabelKey,
...modalProps
}: SortItemOrderProps) => {
const [listValue, setListValue] = useState<{ val: any; id: string }[]>([]);
useEffect(() => {
const innerList = list.map((el, index) => ({
val: el,
oldIndex: index,
id: getRandomStr(),
}));
setListValue(innerList);
}, [list, modalProps.open]);
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 15,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
);
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (active.id !== over?.id) {
const oldIndex = listValue.findIndex((el) => el.id === active?.id);
const newIndex = listValue.findIndex((el) => el.id === over?.id);
const newInnerListVal = arrayMove(listValue, oldIndex, newIndex);
const newVal = newInnerListVal.map((el) => {
return el.val;
});
setListValue(newInnerListVal);
onValueChange?.(newVal);
}
}
const [modalTransform, setModalTransform] = useState({
x: 0,
y: 0,
scaleX: 1,
scaleY: 1,
});
return (
{
return (
{
const res = {
...modalTransform,
...delta,
x: modalTransform.x + (delta?.x || 0),
y: modalTransform.y + (delta?.y || 0),
};
setModalTransform(res);
}}
>
);
}}
>
{listValue.map(({ id, val }, index) => {
return ;
})}
);
};
const ModalDragView = ({ modal, transform }: { modal: React.ReactNode; transform: Transform }) => {
const id = useRef(getRandomStr());
const {
setNodeRef,
attributes,
listeners,
transform: tempTransform,
} = useDraggable({
id: id.current,
});
const finalTransform = {
...transform,
...tempTransform,
x: transform.x + (tempTransform?.x || 0),
y: transform.y + (tempTransform?.y || 0),
};
return (
{modal}
);
};
const SortableItem = (props: { id: string; index: string | number; label?: string }) => {
const { attributes, listeners, setNodeRef, transform, transition } = useSortable({ id: props.id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
const labelText = props.label || `Ele ${props.index}`;
return (
{labelText}
);
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ArraySetter/index.tsx
================================================
import { useEffect, useMemo, useState } from 'react';
import { Button, ConfigProvider } from 'antd';
import { CSetterProps } from '../type';
import { getSetterList } from '../../../utils';
import { SetterType } from '@chamn/model';
import { ArrayItem } from './ArrayItem';
import { SortItemOrderModal } from './SortItemOrderModal';
import styles from './style.module.scss';
export type CArraySetterProps = {
item: {
setters: SetterType[];
initialValue?: any;
};
itemLabelPrefix?: string;
sortLabelKey?: string;
itemLabelPrefixKey?: string;
};
function formatValue(value: unknown) {
if (Array.isArray(value)) {
return value;
} else {
return [];
}
}
export const ArraySetter = ({
onValueChange,
setterContext,
item: { setters, initialValue: itemInitialValue },
sortLabelKey,
initialValue,
itemLabelPrefix,
itemLabelPrefixKey,
...props
}: CSetterProps) => {
const { keyPaths, label } = setterContext;
const listValue: any[] = useMemo(() => {
return formatValue(props.value ?? initialValue);
}, [initialValue, props.value]);
const [sortVisible, setSortVisible] = useState(false);
const innerSetters = getSetterList(
setters || [
{
component: 'StringSetter',
},
]
);
useEffect(() => {
if (setterContext.setCollapseHeaderExt) {
setterContext.setCollapseHeaderExt?.(
{
e.preventDefault();
e.stopPropagation();
setSortVisible(true);
}}
>
sort
);
}
}, [setterContext]);
return (
{listValue.map((val, index) => {
return (
{
listValue[index] = val[index];
onValueChange?.([...listValue]);
}}
setters={innerSetters}
onDelete={() => {
const newVal = [...listValue];
newVal.splice(index, 1);
onValueChange?.(newVal);
}}
/>
);
})}
{
const newVal = [...listValue];
onValueChange?.([...newVal, itemInitialValue ?? '']);
}}
>
Add One
{
onValueChange?.([...newVal]);
}}
open={sortVisible}
list={listValue}
keyPaths={keyPaths}
onCancel={() => {
setSortVisible(false);
}}
onOk={() => {
setSortVisible(false);
}}
/>
);
};
ArraySetter.setterName = '数组设置器';
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ArraySetter/style.module.scss
================================================
.dragItem {
cursor: grab;
border: 1px solid $borderColor;
background-color: white;
padding: 10px;
text-align: center;
margin-bottom: 10px;
font-size: $fontSizeSmall;
border-radius: $borderRadius;
}
.sortModalBox {
padding: 20px 10px 10px;
overflow: auto;
max-height: 500px;
}
.addOneBtn {
width: 100%;
font-size: $fontSizeSmall !important;
color: $fontColor;
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/BooleanSetter/index.tsx
================================================
import React from 'react';
import { ConfigProvider, Switch, SwitchProps } from 'antd';
import { CSetter, CSetterProps } from '../type';
type BooleanSetterProps = SwitchProps;
export const BooleanSetter: CSetter = ({
onValueChange,
initialValue,
setterContext,
...props
}: CSetterProps) => {
return (
{
props.onChange?.(open, e);
onValueChange?.(open);
}}
/>
);
};
BooleanSetter.setterName = 'Bool 设置器';
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/CSSSizeSetter/index.tsx
================================================
import { ConfigProvider } from 'antd';
import { CSetter, CSetterProps } from '../type';
import { CSSSizeInput, CSSSizeInputProps } from '@/component/CSSSizeInput';
export const CSSSizeSetter: CSetter = ({
onValueChange,
setterContext,
initialValue,
...props
}: CSetterProps) => {
return (
{
onValueChange?.(newVal);
}}
/>
);
};
CSSSizeSetter.setterName = 'CSS size 设置器';
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/CSSValueSetter/index.tsx
================================================
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { AutoComplete, ConfigProvider } from 'antd';
import { CSetter, CSetterProps } from '../type';
import { BaseSelectRef } from 'rc-select';
import clsx from 'clsx';
import styles from './style.module.scss';
import { CSSProperties, CSSPropertiesKey } from '../../../../CSSPropertiesEditor/cssProperties';
type CSSValueSetterProps = {
propertyKey: string;
};
export const CSSValueSetter: CSetter = ({
onValueChange,
propertyKey = '',
value,
initialValue,
}: CSetterProps) => {
// const { keyPaths, onSetterChange } = setterContext;
const propertyValueRef = useRef(null);
const [innerValue, setInnerVal] = useState(value ?? (initialValue || ''));
const [focusState, setFocusState] = useState(false);
const updateOuterValue = () => {
onValueChange?.(innerValue);
};
useEffect(() => {
setInnerVal(value);
}, [value]);
const optionsValue = useMemo(() => {
const list = CSSProperties[propertyKey as unknown as CSSPropertiesKey]?.values || [];
return list.map((el) => {
return {
value: el,
};
});
}, [propertyKey]);
return (
{
setInnerVal(val);
updateOuterValue();
}}
style={{
flex: 1,
}}
onFocus={() => {
setFocusState(true);
}}
onBlur={() => {
setFocusState(false);
}}
className={clsx([styles.inputAuto, focusState && styles.active])}
placeholder="value"
options={optionsValue}
>
);
};
CSSValueSetter.setterName = 'CSS值设置器';
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/CSSValueSetter/style.module.scss
================================================
.inputAuto {
border-bottom: 1px solid rgba(128, 128, 128, 0.23);
}
.active {
border-bottom: 1px solid rgb(128, 177, 255);
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ColorSetter/index.tsx
================================================
import { ConfigProvider, Input, Popover } from 'antd';
import { CSetter, CSetterProps } from '../type';
import { SketchPicker } from 'react-color';
type ColorSetterProps = {
initialValue: string;
};
export const ColorSetter: CSetter = ({ onValueChange, initialValue, value }: CSetterProps) => {
return (
{
const newColorStr = `rgba(${newColor.rgb.r},${newColor.rgb.g},${newColor.rgb.b}, ${newColor.rgb.a || 1})`;
onValueChange?.(newColorStr);
}}
/>
}
placement={'bottomLeft'}
styles={{
body: {
padding: 0,
},
}}
arrow={{
pointAtCenter: false,
}}
>
{
onValueChange?.(e.target.value);
}}
prefix={
}
/>
);
};
ColorSetter.setterName = '颜色设置器';
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/EmptyValueSetter/index.tsx
================================================
import { ConfigProvider, Radio } from 'antd';
import { CSetter, CSetterProps } from '../type';
import { CheckboxGroupProps } from 'antd/es/checkbox';
import { useEffect, useMemo } from 'react';
const emptyValMap = {
_null_: null,
_undefined_: undefined,
};
export const EmptyValueSetter: CSetter<{ emptyValue?: any }> = ({
onValueChange,
setterContext,
initialValue,
emptyValue,
hiddenDefaultOption,
...props
}: CSetterProps<{ emptyValue?: string; hiddenDefaultOption?: boolean }>) => {
const options = useMemo['options']>(() => {
const tempValue = [
{ label: 'Undefined', value: '_undefined_' },
{ label: 'Null', value: '_null_' },
];
if (emptyValue) {
if (hiddenDefaultOption) {
return [{ label: emptyValue, value: emptyValue }];
} else {
tempValue.push({ label: emptyValue, value: emptyValue });
}
}
return tempValue;
}, [emptyValue, hiddenDefaultOption]);
const innerValue = useMemo(() => {
const tempValue = initialValue || props.value;
if (tempValue === undefined) {
return '_undefined_';
}
if (tempValue === null) {
return '_null_';
}
return tempValue || '_undefined_';
}, [initialValue, props.value]);
// 切换初始化时,直接强制重置为 undefined
useEffect(() => {
onValueChange?.(emptyValue ?? undefined);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
{
let newVal = e.target.value;
if (['_null_', '_undefined_'].includes(newVal)) {
newVal = emptyValMap[newVal as keyof typeof emptyValMap];
}
onValueChange?.(newVal);
}}
/>
);
};
EmptyValueSetter.setterName = '空值设置器';
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ExpressionSetter/index.tsx
================================================
import React, { useRef, useState } from 'react';
import { Button, ConfigProvider, Modal } from 'antd';
import { CSetter, CSetterProps } from '../type';
import { EditorType, MonacoEditor, MonacoEditorInstance } from '../../../../MonacoEditor';
import DefaultTslibSource from '../FunctionSetter//defaultDts?raw';
import { CNodePropsTypeEnum } from '@chamn/model';
import styles from './style.module.scss';
import { getPageTypeDefined } from '../FunctionSetter/helper';
export type ExpressionSetterProps = CSetterProps<{
value: {
type: string;
value: string;
};
mode: 'modal' | 'inline';
containerStyle?: React.CSSProperties;
minimap?: boolean;
lineNumber?: boolean;
editorOptions?: EditorType['options'];
}>;
export const ExpressionSetter: CSetter = ({
onValueChange,
initialValue,
setterContext,
editorOptions,
mode = 'modal',
...props
}) => {
const editorRef = useRef(null);
const [open, setOpen] = useState(false);
const onInnerValueChange = () => {
const newValStr = editorRef.current?.getValue() || '';
onValueChange?.({
type: CNodePropsTypeEnum.EXPRESSION,
value: newValStr,
});
};
let lineNumberOptions = {};
if (props.lineNumber === false || mode === 'inline') {
lineNumberOptions = {
lineNumbers: 'off',
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
glyphMargin: false,
};
}
const editorView = (
{
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
noSemanticValidation: true,
noSyntaxValidation: false,
});
// compiler options
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES5,
allowNonTsExtensions: true,
});
const realtimeDts = await getPageTypeDefined(setterContext.pluginCtx.pageModel, setterContext.nodeModel);
const libUri = 'ts:filename/chameleon.default.variable.d.ts';
monaco.languages.typescript.javascriptDefaults.addExtraLib(realtimeDts, libUri);
// When resolving definitions and references, the editor will try to use created models.
// Creating a model for the library allows "peek definition/references" commands to work with the library.
const model = monaco.editor.getModel(monaco.Uri.parse(libUri));
if (!model) {
monaco.editor.createModel(DefaultTslibSource, 'typescript', monaco.Uri.parse(libUri));
}
}}
onDidMount={(editor) => {
editorRef.current = editor;
}}
initialValue={props.value?.value ?? initialValue}
language={'javascript'}
options={{
automaticLayout: true,
tabSize: 2,
minimap: { enabled: false },
folding: false,
hover: {
// enabled: false, // ✅ 禁用 hoverWidget
},
...lineNumberOptions,
...(editorOptions || {}),
}}
onChange={() => {
if (mode === 'inline') {
// TODO: 需要节流每 1 秒触发一次
onInnerValueChange();
}
}}
/>
);
if (mode === 'inline') {
return (
{editorView}
);
}
return (
{
setOpen(true);
}}
>
{props.value?.value || 'Edit Expression'}
setOpen(false)}
width="calc(100vw - 100px)"
onOk={() => {
onInnerValueChange();
setOpen(false);
}}
style={{
maxWidth: '800px',
}}
styles={{
body: {
height: '300px',
},
}}
>
{open && editorView}
);
};
ExpressionSetter.setterName = '表达式';
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/ExpressionSetter/style.module.scss
================================================
.expressionCodeEditor {
:global {
.monaco-editor .suggest-widget {
width: 300px !important;
left: 0 !important;
right: auto !important;
transform: none !important; // 防止默认偏移
}
}
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/FastLayoutSetter/index.tsx
================================================
import { CSSSizeInputProps } from '@/component/CSSSizeInput';
import { CSetter, CSetterProps } from '../type';
import { StyleUIPanel, StyleUIPanelRef } from '@/component/StylePanel';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { formatStyleProperty, styleArr2Obj, styleObjToArr } from '@/utils';
import { CNode, CRootNode } from '@chamn/model';
import { isEqual } from 'lodash-es';
export const FastLayoutSetter: CSetter = ({
value,
setterContext,
initialValue,
...resetProps
}: CSetterProps) => {
const cssUIRef = useRef(null);
const node = setterContext.nodeModel;
const lastNode = useRef();
const initialValueInner = useMemo(() => {
const newStyle = node.value.style || [];
const { normalProperty } = formatStyleProperty(newStyle);
return styleArr2Obj(normalProperty);
}, [node.value.style]);
const updatePanelValue = useCallback(() => {
lastNode.current = node;
const newStyle = node.value.style || [];
const { normalProperty } = formatStyleProperty(newStyle);
cssUIRef.current?.setValue(styleArr2Obj(normalProperty) || {});
}, [node]);
useEffect(() => {
updatePanelValue();
node.emitter.on('onNodeChange', updatePanelValue);
node.emitter.on('onReloadPage', updatePanelValue);
return () => {
node.emitter.off('onNodeChange', updatePanelValue);
node.emitter.off('onReloadPage', updatePanelValue);
};
}, [node.emitter, node.id, updatePanelValue]);
return (
{
const newStyle = styleObjToArr(newNormaCss);
const { expressionProperty } = formatStyleProperty(node.value.style || []);
const newStyleList = [...newStyle, ...expressionProperty];
if (isEqual(node.value.style, newStyleList)) {
return;
}
node.value.style = newStyleList;
node.updateValue();
}}
/>
);
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/FunctionSetter/defaultDts.ts
================================================
type SetStateInternal = {
_(
partial:
| T
| Partial
| {
_(state: T): T | Partial;
}['_'],
replace?: boolean | undefined
): void;
}['_'];
export interface StoreApi {
setState: SetStateInternal;
getState: () => T;
subscribe: (listener: (state: T, prevState: T) => void) => () => void;
/**
* @deprecated Use `unsubscribe` returned by `subscribe`
*/
destroy: () => void;
}
type PageState = any;
type NodeId = keyof PageState;
type MethodsManager = any;
type GlobalState = any;
type CurrentNodeState = any;
type PageStateManager = {
state: PageState[K];
updateState: (newState: Partial) => void;
};
type PageStateManagerMap = {
[K in keyof PageState]: PageStateManager;
};
type ContextType = {
/** 渲染函数的入口参数 */
params?: Record;
/** 全局状态 */
globalState?: GlobalState;
/** 更新全局状态 */
updateGlobalState?: (newState: Partial) => void;
/** 存储当前节点的数据,不具有响应性, 可能不是最新的值, 可以直接赋值 **/
staticVar?: Record;
methods?: Record any>;
/** 当前节点状态 **/
state?: CurrentNodeState;
/** 更新当前节点状态 */
updateState?: (newState: Partial) => void;
/** 获取当前节点状态最新 */
/** 用于访访问和管理页面被注册为全局的局部 state 快照,在闭包中使用会存在值不是最新的情况 */
stateManager: PageStateManagerMap;
/** 循环数据 */
loopData?: {
item: any;
index: number;
};
/** 组件节点的 Ref, 可以通过 current 直接调用节点提供的方法,需要判断是否在存在 */
nodeRefs?: { get: (/** 节点 id */ nodeId: NodeId) => { current?: any } };
/** 运行时全局的 store 管理 */
storeManager?: StoreManager;
/** 第三方辅助库 */
thirdLibs?: Record;
};
declare class StoreManager {
storeMap: Map>;
getStore(storeName: T): StoreApi | undefined;
getState(nodeId: T): any;
getStateObj(nodeId: T): {
state: PageState[T];
updateState: (newState: Partial) => void;
};
setState(nodeId: NodeId, newState: Partial): void | undefined;
connect(name: string, cb: (newState: PageState[T]) => void): () => void;
getStateSnapshot(): PageState;
destroy(): void;
}
type UpdaterMap = {
[K in NodeId /** 更新对应 nodeId 的数据 **/]: (newState: Partial) => void;
};
declare global {
/** 运行时上下文 */
const $CTX: ContextType;
/** 当前节点的 state */
const $STATE: CurrentNodeState;
/** global state */
const $G_STATE: GlobalState;
/** 当前页面所有节点的 state */
const $ALL_STATE: PageState;
/** 状态更新函数 */
const $U_STATE: UpdaterMap;
/** 节点方法调用 */
const $M: MethodsManager;
/** 当前节点 ID */
const $N_ID: string;
/** 存储所有节点的 id */
const $IDS: Record;
/** 上一个 API 返回的数据,可能不存在 */
const $RESPONSE: any;
/** 循环数据,如果存在 */
const $LOOP_DATA: ContextType['loopData'];
/** 函数在配置面板传入的参数 */
const $PARAMS: ContextType['params'];
/** 事件传入的第一个参数,如果需要获取第二个参数,请使用 $ARGS 数组获取*/
const $EVENT_PARAMS: any;
/** 事件对象 */
const $EVENT: MouseEvent;
/** 函数执行时传入的入参 */
const $ARGS: any[];
/** action flow 中的局部变量 */
const $ACTION_VAR_SPACE: any;
}
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/FunctionSetter/helper.ts
================================================
import { CNode, CPage, InnerComponentNameEnum, traversePageNode } from '@chamn/model';
import DefaultTslibSource from './defaultDts?raw';
import { quicktype, InputData, jsonInputForTargetLanguage } from 'quicktype-core';
export async function quicktypeJSON(typeName: string, jsonString: string) {
const jsonInput = jsonInputForTargetLanguage('typescript');
// We could add multiple samples for the same desired
// type, or many sources for other types. Here we're
// just making one type from one piece of sample JSON.
await jsonInput.addSource({
name: typeName,
samples: [jsonString],
});
const inputData = new InputData();
inputData.addInput(jsonInput);
return await quicktype({
inputData,
lang: 'typescript',
rendererOptions: {
'just-types': 'true',
'nice-property-names': 'false',
'acronym-style': 'original',
},
});
}
export const getPageTypeDefined = async (pageModel: CPage, currentNode: CNode) => {
const stateTypeMap: Record<
string,
{
stateTypeDefined: string;
methodsTypeDefined: string;
}
> = {} as any;
const pList: any[] = [];
traversePageNode(pageModel, (node) => {
stateTypeMap[node.id] = {
stateTypeDefined: '',
methodsTypeDefined: '',
};
if (node.value.state) {
const cb = async () => {
let typeName = `CNode${node.id.toUpperCase()}`;
let id = node.id;
if (node.value.componentName === InnerComponentNameEnum.ROOT_CONTAINER) {
typeName = 'GlobalState';
id = 'GlobalState';
}
const tempRes = await quicktypeJSON(typeName, JSON.stringify(node.value.state));
stateTypeMap[id] = {
stateTypeDefined: tempRes.lines.join('\n'),
methodsTypeDefined: '',
};
};
pList.push(cb());
}
});
await Promise.all(pList);
const globalStateDts = stateTypeMap['GlobalState'];
delete stateTypeMap.GlobalState;
// 拼接 整体的 page state 类型定义
let pageStateDts = '';
pageStateDts += `type PageState = {\n`;
const nodeIdList: string[] = [];
Object.keys(stateTypeMap).forEach((k) => {
if (stateTypeMap[k].stateTypeDefined) {
const body = getBodyDefined(`CNode${k.toUpperCase()}`, stateTypeMap[k].stateTypeDefined);
pageStateDts += ` '${k}': ${body},\n`;
nodeIdList.push(`'${k}'`);
}
});
pageStateDts += `};\n\n`;
let dtsContent = DefaultTslibSource.replace('type PageState = any;', pageStateDts);
dtsContent = dtsContent.replace('type NodeId = any;', `type NodeId = ${nodeIdList.join(' | ')};`);
if (globalStateDts?.stateTypeDefined) {
dtsContent = dtsContent.replace('type GlobalState = any;', globalStateDts.stateTypeDefined);
dtsContent = dtsContent.replace(
'Partial',
getBodyDefined('GlobalState', globalStateDts.stateTypeDefined)
);
}
// 处理当前 node 的 types
const currentNodeDts = await quicktypeJSON('CurrentNodeState', JSON.stringify(currentNode.value.state || {}));
const currentNodeDtsText = currentNodeDts.lines.join('\n');
dtsContent = dtsContent.replace('type CurrentNodeState = any;', currentNodeDtsText);
dtsContent = dtsContent.replace('Partial', getBodyDefined('CurrentNodeState', currentNodeDtsText));
// 处理 methods 调用
return dtsContent;
};
const getBodyDefined = (
typeName: string,
dtsStr: string,
options?: {
isPartial: true;
}
) => {
const body = dtsStr.replace(`export interface ${typeName}`, '').trim();
if (!options?.isPartial) {
return body;
}
return `Partial<${body}>`;
};
================================================
FILE: packages/engine/src/component/CustomSchemaForm/components/Setters/FunctionSetter/index.tsx
================================================
import React, { useRef, useState } from 'react';
import { Button, ConfigProvider, Modal } from 'antd';
import { CSetter } from '../type';
import { EditorType, MonacoEditor, MonacoEditorInstance } from '../../../../MonacoEditor';
import DefaultTslibSource from './defaultDts?raw';
import { CNodePropsTypeEnum } from '@chamn/model';
import { getPageTypeDefined } from './helper';
export const FunctionSetter: CSetter<{
mode: 'modal' | 'inline';
containerStyle?: React.CSSProperties;
minimap?: boolean;
lineNumber?: boolean;
editorOptions?: EditorType['options'];
}> = ({ onValueChange, initialValue, setterContext, editorOptions, ...props }) => {
getPageTypeDefined(setterContext.pluginCtx.pageModel, setterContext.nodeModel);
const editorRef = useRef(null);
const [open, setOpen] = useState(false);
const onInnerValueChange = () => {
const newValStr = editorRef.current?.getValue() || '';
onValueChange?.({
type: CNodePropsTypeEnum.FUNCTION,
value: newValStr,
});
};
let lineNumberOptions = {};
if (props.lineNumber === false) {
lineNumberOptions = {
lineNumbers: 'off',
lineDecorationsWidth: 0,
lineNumbersMinChars: 0,
glyphMargin: false,
};
}
const editorView = (
{
monaco.languages.typescript.javascriptDefaults.setDiagnosticsOptions({
noSemanticValidation: true,
noSyntaxValidation: false,
});
// compiler options
monaco.languages.typescript.javascriptDefaults.setCompilerOptions({
target: monaco.languages.typescript.ScriptTarget.ES5,
allowNonTsExtensions: true,
});
const realtimeDts = await getPageTypeDefined(setterContext.pluginCtx.pageModel, setterContext.nodeModel);
const libUri = 'ts:filename/chameleon.default.variable.d.ts';
monaco.languages.typescript.javascriptDefaults.addExtraLib(realtimeDts, libUri);
// When resolving definitions and references, the editor will try to use created models.
// Creating a model for the library allows "peek definition/references" commands to work with the library.
const model = monaco.editor.getModel(monaco.Uri.parse(libUri));
if (!model) {
monaco.editor.createModel(DefaultTslibSource, 'typescript', monaco.Uri.parse(libUri));
}
}}
onDidMount={(editor) => {
editorRef.current = editor;
}}
initialValue={props.value?.value ?? (initialValue || 'function run() {\n}')}
language={'javascript'}
options={{
automaticLayout: true,
tabSize: 2,
minimap: {
enabled: props.minimap ?? true,
},
...lineNumberOptions,
...(editorOptions || {}),
}}
onChange={() => {
if (props.mode === 'inline') {
// TODO: 需要节流每 1 秒触发一次
onInnerValueChange();
}
}}
/>
);
if (props.mode === 'inline') {
return (
{editorView}
);
}
return (
{
setOpen(true);
}}
>
Edit Function
setOpen(false)}
width="calc(100vw - 100px)"
onOk={() => {
onInnerValueChange();
setOpen(false);
}}
style={{
maxWidth: '1300px',
}}
styles={{
body: {
minHeight: '500px',
height: 'calc(100vh - 280px)',
},
}}
>