mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-04-23 10:18:55 +00:00
Compare commits
38 Commits
v1.7.8-bet
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bf42f9007 | ||
|
|
3875ccde33 | ||
|
|
62a488ac66 | ||
|
|
27bb886054 | ||
|
|
fa09ab0b30 | ||
|
|
31f4d2b4e2 | ||
|
|
b2888962df | ||
|
|
b3f4e42716 | ||
|
|
6e07d5762b | ||
|
|
cfd5998242 | ||
|
|
26dc70d70c | ||
|
|
99c8274a1e | ||
|
|
334569e2d7 | ||
|
|
f583c7daec | ||
|
|
172a7a1c92 | ||
|
|
73c676931f | ||
|
|
df2d635682 | ||
|
|
0c2f2fd2b5 | ||
|
|
a7274198bf | ||
|
|
6f2e8d8d74 | ||
|
|
637a5bb69a | ||
|
|
42e7ac1b2e | ||
|
|
a3cdad9d91 | ||
|
|
711af79d72 | ||
|
|
728fbc035c | ||
|
|
01795455e9 | ||
|
|
9b56223359 | ||
|
|
984cea7ca3 | ||
|
|
9921ed8a2d | ||
|
|
e36d8d7cf8 | ||
|
|
35aa81514b | ||
|
|
450376872e | ||
|
|
e8714c96c9 | ||
|
|
feefd3779e | ||
|
|
1ae023db8c | ||
|
|
55eb546ad6 | ||
|
|
1664559d8f | ||
|
|
a34d0cdccc |
56
AGENTS.md
Normal file
56
AGENTS.md
Normal file
@ -0,0 +1,56 @@
|
||||
# AGENTS.md — TMagic编辑器
|
||||
|
||||
> 魔方平台可视化编辑器核心,提供拖拽式活动页面编辑能力。
|
||||
> 负责人:roymondchen | 创建:2026-04-03
|
||||
|
||||
## 项目概述
|
||||
|
||||
TMagic Editor 是魔方平台的可视化编辑器核心库,提供拖拽式组件编辑、配置面板、预览发布等能力。支持 Vue 和 React 双框架 Runtime,采用 pnpm monorepo 管理多个核心包。开源项目,同时支持内部业务定制。
|
||||
|
||||
**技术栈:** Vue 3, Element Plus, TypeScript, Vite, vitest, VitePress
|
||||
**主仓库:** `https://git.woa.com/vft-magic/tmagic-editor.git`
|
||||
**开源仓库:** `https://github.com/Tencent/tmagic-editor.git`
|
||||
|
||||
## 架构地图
|
||||
|
||||
关键目录:
|
||||
- `packages/` — 核心编辑器包(202 *.vue, 194 *.ts)
|
||||
- `runtime/` — Vue/React Runtime 实现
|
||||
- `vue-components/` — Vue 组件封装
|
||||
- `react-components/` — React 组件封装
|
||||
- `playground/` — 演示 playground
|
||||
- `docs/` — VitePress 文档(100 *.md)
|
||||
- `scripts/` — 构建和发布脚本
|
||||
- `eslint-config/` — 共享 ESLint 配置
|
||||
|
||||
## 开发约定
|
||||
|
||||
**分支策略:** dev=dev, test/prod=master
|
||||
**提交规范:** commitlint + husky,`type: 描述`
|
||||
|
||||
**禁止事项:**
|
||||
- 禁止在核心包中引入腾讯内部专有依赖(开源项目)
|
||||
- 禁止直接修改 CHANGELOG.md,应通过 `pnpm changelog` 生成
|
||||
|
||||
## 常用命令
|
||||
|
||||
pnpm bootstrap # 安装依赖并构建
|
||||
pnpm pg # 启动 Vue playground
|
||||
pnpm pg:react # 启动 React playground
|
||||
pnpm build # 完整构建(DTS + 包)
|
||||
pnpm test # 运行测试
|
||||
pnpm lint-fix # ESLint 修复
|
||||
pnpm docs:dev # 启动文档开发
|
||||
pnpm release # 发版
|
||||
|
||||
## 当前状态
|
||||
|
||||
**当前里程碑:** {待人工填写}
|
||||
|
||||
## 深入阅读
|
||||
|
||||
| 文档 | 说明 |
|
||||
|------|------|
|
||||
| docs/ | VitePress 文档站 |
|
||||
| CONTRIBUTING.md | 贡献指南 |
|
||||
| CHANGELOG.md | 变更日志 |
|
||||
68
CHANGELOG.md
68
CHANGELOG.md
@ -1,3 +1,71 @@
|
||||
## [1.7.10](https://github.com/Tencent/tmagic-editor/compare/v1.7.9...v1.7.10) (2026-04-13)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** 优化 StageOverlay 双击行为,仅在元素被滚动容器裁剪时打开 overlay ([df2d635](https://github.com/Tencent/tmagic-editor/commit/df2d635682034f2f478c575543ed63f94b7a291c))
|
||||
* **editor:** 修复 getTMagicAppPrimise 变量名拼写错误 ([99c8274](https://github.com/Tencent/tmagic-editor/commit/99c8274a1ee25c8e155498e43520c5d7ba99c4b1))
|
||||
* **editor:** 历史记录信息中添加页面信息 ([26dc70d](https://github.com/Tencent/tmagic-editor/commit/26dc70d70c329d9a83a671d92bbfaf77d7abbcef))
|
||||
* **editor:** 数据源方法选择器展示所有数据源并支持字段非叶子节点选择 ([31f4d2b](https://github.com/Tencent/tmagic-editor/commit/31f4d2b4e26f5a941041c41c0843828d70ed3cd8))
|
||||
* **stage:** 修复隐藏标尺后无法显示问题 ([6f2e8d8](https://github.com/Tencent/tmagic-editor/commit/6f2e8d8d74ed0d8a7ef1eefd0b39b08a90abc6e7))
|
||||
* **stage:** 新增组件后等待渲染完后选中 ([cfd5998](https://github.com/Tencent/tmagic-editor/commit/cfd5998242031f393bc1003c40b55cde35f3c515))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **editor,data-source:** 数据源支持内置"设置数据"方法 ([f583c7d](https://github.com/Tencent/tmagic-editor/commit/f583c7daec76518bbb2e7044740337e0c15997e1))
|
||||
* **editor,stage:** 支持双击穿透选中鼠标下方的下一个可选中元素 ([172a7a1](https://github.com/Tencent/tmagic-editor/commit/172a7a1c92e85266114c628c0fc0534b6b76aca3))
|
||||
* **editor:** 样式配置添加变形项 ([fa09ab0](https://github.com/Tencent/tmagic-editor/commit/fa09ab0b301d9bc9f6a9462fc022418bbf21cee3))
|
||||
* **editor:** 添加 stage beforeDblclick 钩子,支持拦截默认双击行为 ([334569e](https://github.com/Tencent/tmagic-editor/commit/334569e2d784d708f4fde7e9f37f5cac50b2e396))
|
||||
* **stage:** 支持将指定id的dom生成图片 ([b3f4e42](https://github.com/Tencent/tmagic-editor/commit/b3f4e42716fe1d998d093e7556897bdb2e9037a9))
|
||||
|
||||
|
||||
|
||||
## [1.7.9](https://github.com/Tencent/tmagic-editor/compare/v1.7.8...v1.7.9) (2026-03-23)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **form:** row容器中如果配置没有type显示异常 ([711af79](https://github.com/Tencent/tmagic-editor/commit/711af79d72c202a7bc961b0639ce62c318952ee6))
|
||||
|
||||
|
||||
|
||||
## [1.7.8](https://github.com/Tencent/tmagic-editor/compare/v1.7.8-beta.4...v1.7.8) (2026-03-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **editor:** 组件配置样式显示出错 ([9b56223](https://github.com/Tencent/tmagic-editor/commit/9b56223359f4ce826597f8f5d440626869e8b650))
|
||||
|
||||
|
||||
|
||||
## [1.7.8-beta.4](https://github.com/Tencent/tmagic-editor/compare/v1.7.8-beta.3...v1.7.8-beta.4) (2026-03-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **form-schema:** 表单 schema 中 display 与 component 部分字段改为可选 ([e36d8d7](https://github.com/Tencent/tmagic-editor/commit/e36d8d7cf874fe5e307eee5f3906499a86337b85))
|
||||
|
||||
|
||||
|
||||
## [1.7.8-beta.3](https://github.com/Tencent/tmagic-editor/compare/v1.7.8-beta.2...v1.7.8-beta.3) (2026-03-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **form-schema,form,editor,table:** 完善表单配置类型 ([e8714c9](https://github.com/Tencent/tmagic-editor/commit/e8714c96c9f3ae4644d830d232b2732adbcd05fa))
|
||||
|
||||
|
||||
|
||||
## [1.7.8-beta.2](https://github.com/Tencent/tmagic-editor/compare/v1.7.8-beta.1...v1.7.8-beta.2) (2026-03-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **form-schema,form,editor:** 完善表单配置类型 ([55eb546](https://github.com/Tencent/tmagic-editor/commit/55eb546ad6dd67a920093353f2de9bd891db3f70))
|
||||
|
||||
|
||||
|
||||
## [1.7.8-beta.1](https://github.com/Tencent/tmagic-editor/compare/v1.7.7...v1.7.8-beta.1) (2026-03-19)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@ -16,9 +16,9 @@ https://tencent.github.io/tmagic-editor/playground/index.html
|
||||
|
||||
## 环境准备
|
||||
|
||||
node.js >= 18
|
||||
node.js ^20.19.0 || >=22.12.0
|
||||
|
||||
pnpm >= 9
|
||||
pnpm >= 10
|
||||
|
||||
先安装 pnpm
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@tmagic/eslint-config",
|
||||
"version": "0.0.4",
|
||||
"version": "0.1.0",
|
||||
"main": "index.mjs",
|
||||
"type": "module",
|
||||
"repository": {
|
||||
@ -10,17 +10,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@typescript-eslint/parser": "^8.57.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.57.1",
|
||||
"@typescript-eslint/parser": "^8.58.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
||||
"@stylistic/eslint-plugin": "^5.10.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-import": "^2.32.0",
|
||||
"eslint-plugin-simple-import-sort": "^12.1.1",
|
||||
"eslint-plugin-simple-import-sort": "^13.0.0",
|
||||
"eslint-plugin-vue": "^10.8.0",
|
||||
"vue-eslint-parser": "^10.3.0",
|
||||
"eslint-plugin-prettier": "^5.5.4",
|
||||
"vue-eslint-parser": "^10.4.0",
|
||||
"eslint-plugin-prettier": "^5.5.5",
|
||||
"globals": "^17.4.0",
|
||||
"typescript-eslint": "^8.57.1"
|
||||
"typescript-eslint": "^8.58.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=10.0.0",
|
||||
|
||||
@ -16,6 +16,7 @@ export default defineConfig([
|
||||
'*/**/public/**/*',
|
||||
'*/**/types/**/*',
|
||||
'*/**/*.config.ts',
|
||||
'./tepm/**/*',
|
||||
'vite-env.d.ts',
|
||||
]),
|
||||
...eslintConfig(path.join(path.dirname(fileURLToPath(import.meta.url)), 'tsconfig.json')),
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"name": "tmagic",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/cli",
|
||||
"main": "lib/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"moduleResolution": "Node",
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "node16",
|
||||
"module": "node16",
|
||||
"rootDir": "./src",
|
||||
"outDir": "./lib",
|
||||
"declaration": true,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/core",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -297,7 +297,7 @@ class App extends EventEmitter {
|
||||
}
|
||||
|
||||
if (typeof dataSource[methodName] === 'function') {
|
||||
return await dataSource[methodName]();
|
||||
return await dataSource[methodName]({ params });
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (this.errorHandler) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/data-source",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -20,7 +20,7 @@ import EventEmitter from 'events';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import type { CodeBlockContent, DataSchema, DataSourceSchema, default as TMagicApp } from '@tmagic/core';
|
||||
import { getDefaultValueFromFields } from '@tmagic/core';
|
||||
import { DATA_SOURCE_SET_DATA_METHOD_NAME, getDefaultValueFromFields } from '@tmagic/core';
|
||||
|
||||
import { ObservedData } from '@data-source/observed-data/ObservedData';
|
||||
import { SimpleObservedData } from '@data-source/observed-data/SimpleObservedData';
|
||||
@ -51,6 +51,7 @@ export default class DataSource<T extends DataSourceSchema = DataSourceSchema> e
|
||||
super();
|
||||
|
||||
this.#id = options.schema.id;
|
||||
this.#type = options.schema.type;
|
||||
this.#schema = options.schema;
|
||||
|
||||
this.app = options.app;
|
||||
@ -58,6 +59,11 @@ export default class DataSource<T extends DataSourceSchema = DataSourceSchema> e
|
||||
this.setFields(options.schema.fields);
|
||||
this.setMethods(options.schema.methods || []);
|
||||
|
||||
// @ts-ignore
|
||||
this[DATA_SOURCE_SET_DATA_METHOD_NAME] = ({ params }: { params: { field?: string[]; data: any } }) => {
|
||||
this.setData(params.data, params.field?.join('.'));
|
||||
};
|
||||
|
||||
let data = options.initialData;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const ObservedDataClass = options.ObservedDataClass || SimpleObservedData;
|
||||
|
||||
@ -79,9 +79,9 @@ export default class HttpDataSource extends DataSource<HttpDataSourceSchema> {
|
||||
/** 请求函数 */
|
||||
#fetch?: RequestFunction;
|
||||
/** 请求前需要执行的函数队列 */
|
||||
#beforeRequest: ((...args: any[]) => any)[] = [];
|
||||
#beforeRequest: (Function | ((...args: any[]) => any))[] = [];
|
||||
/** 请求后需要执行的函数队列 */
|
||||
#afterRequest: ((...args: any[]) => any)[] = [];
|
||||
#afterRequest: (Function | ((...args: any[]) => any))[] = [];
|
||||
|
||||
#type = 'http';
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/dep",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -71,7 +71,7 @@ export default class Target {
|
||||
|
||||
this.deps[id] = dep;
|
||||
|
||||
if (dep.keys.indexOf(key) === -1) {
|
||||
if (!dep.keys.includes(key)) {
|
||||
dep.keys.push(key);
|
||||
}
|
||||
}
|
||||
@ -115,7 +115,7 @@ export default class Target {
|
||||
public hasDep(id: string | number, key: string | number) {
|
||||
const dep = this.deps[id];
|
||||
|
||||
return Boolean(dep?.keys.find((d) => d === key));
|
||||
return dep?.keys.includes(key) ?? false;
|
||||
}
|
||||
|
||||
public destroy() {
|
||||
|
||||
@ -5,6 +5,12 @@ import type Target from './Target';
|
||||
import { type DepExtendedData, DepTargetType, type TargetList, TargetNode } from './types';
|
||||
import { traverseTarget } from './utils';
|
||||
|
||||
const DATA_SOURCE_TARGET_TYPES: Set<string> = new Set([
|
||||
DepTargetType.DATA_SOURCE,
|
||||
DepTargetType.DATA_SOURCE_COND,
|
||||
DepTargetType.DATA_SOURCE_METHOD,
|
||||
]);
|
||||
|
||||
export default class Watcher {
|
||||
private targetsList: TargetList = {};
|
||||
private childrenProp = 'items';
|
||||
@ -159,7 +165,7 @@ export default class Watcher {
|
||||
};
|
||||
}
|
||||
|
||||
const clearedItemsNodeIds: (string | number)[] = [];
|
||||
const clearedItemsNodeIds = new Set<string | number>();
|
||||
traverseTarget(targetsList, (target) => {
|
||||
if (nodes) {
|
||||
for (const node of nodes) {
|
||||
@ -168,9 +174,9 @@ export default class Watcher {
|
||||
if (
|
||||
Array.isArray(node[this.childrenProp]) &&
|
||||
node[this.childrenProp].length &&
|
||||
!clearedItemsNodeIds.includes(node[this.idProp])
|
||||
!clearedItemsNodeIds.has(node[this.idProp])
|
||||
) {
|
||||
clearedItemsNodeIds.push(node[this.idProp]);
|
||||
clearedItemsNodeIds.add(node[this.idProp]);
|
||||
this.clear(node[this.childrenProp]);
|
||||
}
|
||||
}
|
||||
@ -190,12 +196,7 @@ export default class Watcher {
|
||||
}
|
||||
|
||||
public collectItem(node: TargetNode, target: Target, depExtendedData: DepExtendedData = {}, deep = false) {
|
||||
const dataSourceTargetTypes: string[] = [
|
||||
DepTargetType.DATA_SOURCE,
|
||||
DepTargetType.DATA_SOURCE_COND,
|
||||
DepTargetType.DATA_SOURCE_METHOD,
|
||||
];
|
||||
if (node[NODE_DISABLE_DATA_SOURCE_KEY] && dataSourceTargetTypes.includes(target.type)) {
|
||||
if (node[NODE_DISABLE_DATA_SOURCE_KEY] && DATA_SOURCE_TARGET_TYPES.has(target.type)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -18,6 +18,8 @@ import {
|
||||
import Target from './Target';
|
||||
import { DepTargetType, type TargetList } from './types';
|
||||
|
||||
const INTEGER_REGEXP = /^\d+$/;
|
||||
|
||||
export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent, initialDeps: DepData = {}) =>
|
||||
new Target({
|
||||
type: DepTargetType.CODE_BLOCK,
|
||||
@ -30,8 +32,7 @@ export const createCodeBlockTarget = (id: Id, codeBlock: CodeBlockContent, initi
|
||||
}
|
||||
|
||||
if (value?.hookType === HookType.CODE && Array.isArray(value.hookData)) {
|
||||
const index = value.hookData.findIndex((item: HookData) => item.codeId === id);
|
||||
return Boolean(index > -1);
|
||||
return value.hookData.some((item: HookData) => item.codeId === id);
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -54,12 +55,7 @@ export const isIncludeArrayField = (keys: string[], fields: DataSchema[]) => {
|
||||
f = field?.fields || [];
|
||||
|
||||
// 字段类型为数组并且后面没有数字索引
|
||||
return (
|
||||
field?.type === 'array' &&
|
||||
// 不是整数
|
||||
/^(?!\d+$).*$/.test(`${keys[index + 1]}`) &&
|
||||
index < keys.length - 1
|
||||
);
|
||||
return field?.type === 'array' && index < keys.length - 1 && !INTEGER_REGEXP.test(keys[index + 1]);
|
||||
});
|
||||
};
|
||||
|
||||
@ -78,33 +74,25 @@ export const isDataSourceTemplate = (value: any, ds: Pick<DataSourceSchema, 'id'
|
||||
return false;
|
||||
}
|
||||
|
||||
const arrayFieldTemplates = [];
|
||||
const fieldTemplates = [];
|
||||
|
||||
templates.forEach((tpl) => {
|
||||
for (const tpl of templates) {
|
||||
// 将${dsId.xxxx} 转成 dsId.xxxx
|
||||
const expression = tpl.substring(2, tpl.length - 1);
|
||||
const keys = getKeysArray(expression);
|
||||
const dsId = keys.shift();
|
||||
|
||||
if (!dsId || dsId !== ds.id) {
|
||||
return;
|
||||
continue;
|
||||
}
|
||||
|
||||
// ${dsId.array} ${dsId.array[0]} ${dsId.array[0].a} 这种是依赖
|
||||
// ${dsId.array.a} 这种不是依赖,这种需要再迭代器容器中的组件才能使用,依赖由迭代器处理
|
||||
if (isIncludeArrayField(keys, ds.fields)) {
|
||||
arrayFieldTemplates.push(tpl);
|
||||
} else {
|
||||
fieldTemplates.push(tpl);
|
||||
const includesArray = isIncludeArrayField(keys, ds.fields);
|
||||
if (hasArray === includesArray) {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (hasArray) {
|
||||
return arrayFieldTemplates.length > 0;
|
||||
}
|
||||
|
||||
return fieldTemplates.length > 0;
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
@ -184,7 +172,12 @@ export const isDataSourceTarget = (
|
||||
value: any,
|
||||
hasArray = false,
|
||||
) => {
|
||||
if (!value || !['string', 'object'].includes(typeof value)) {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const valueType = typeof value;
|
||||
if (valueType !== 'string' && valueType !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -193,13 +186,13 @@ export const isDataSourceTarget = (
|
||||
}
|
||||
|
||||
// 或者在模板在使用数据源
|
||||
if (typeof value === 'string') {
|
||||
if (valueType === 'string') {
|
||||
return isDataSourceTemplate(value, ds, hasArray);
|
||||
}
|
||||
|
||||
// 关联数据源对象,如:{ isBindDataSource: true, dataSourceId: 'xxx'}
|
||||
// 使用data-source-select value: 'value' 可以配置出来
|
||||
if (isObject(value) && value?.isBindDataSource && value.dataSourceId && value.dataSourceId === ds.id) {
|
||||
if (isObject(value) && value.isBindDataSource && value.dataSourceId === ds.id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -210,10 +203,7 @@ export const isDataSourceTarget = (
|
||||
if (isUseDataSourceField(value, ds.id)) {
|
||||
const [, ...keys] = value;
|
||||
const includeArray = isIncludeArrayField(keys, ds.fields);
|
||||
if (hasArray) {
|
||||
return includeArray;
|
||||
}
|
||||
return !includeArray;
|
||||
return hasArray ? includeArray : !includeArray;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -235,12 +225,9 @@ export const isDataSourceCondTarget = (
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ds.fields?.find((field) => field.name === keys[0])) {
|
||||
if (ds.fields?.some((field) => field.name === keys[0])) {
|
||||
const includeArray = isIncludeArrayField(keys, ds.fields);
|
||||
if (hasArray) {
|
||||
return includeArray;
|
||||
}
|
||||
return !includeArray;
|
||||
return hasArray ? includeArray : !includeArray;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -282,12 +269,12 @@ export const createDataSourceMethodTarget = (
|
||||
return false;
|
||||
}
|
||||
|
||||
if (ds.methods?.find((method) => method.name === methodName)) {
|
||||
if (ds.methods?.some((method) => method.name === methodName)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 配置的方法名称可能会是数据源类中定义的,并不存在于methods中,所以这里判断如果methodName如果是字段名称,就表示配置的不是方法
|
||||
if (ds.fields?.find((field) => field.name === methodName)) {
|
||||
if (ds.fields?.some((field) => field.name === methodName)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -300,11 +287,18 @@ export const traverseTarget = (
|
||||
cb: (target: Target) => void,
|
||||
type?: DepTargetType | string,
|
||||
) => {
|
||||
if (type) {
|
||||
const targets = targetsList[type];
|
||||
if (targets) {
|
||||
for (const target of Object.values(targets)) {
|
||||
cb(target);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (const targets of Object.values(targetsList)) {
|
||||
for (const target of Object.values(targets)) {
|
||||
if (type && target.type !== type) {
|
||||
continue;
|
||||
}
|
||||
cb(target);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/design",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/editor",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
|
||||
@ -207,6 +207,7 @@ const stageOptions: StageOptions = {
|
||||
renderType: props.renderType,
|
||||
guidesOptions: props.guidesOptions,
|
||||
disabledMultiSelect: props.disabledMultiSelect,
|
||||
beforeDblclick: props.beforeDblclick,
|
||||
};
|
||||
|
||||
stageOverlayService.set('stageOptions', stageOptions);
|
||||
|
||||
@ -65,8 +65,9 @@ import type { CodeBlockContent } from '@tmagic/core';
|
||||
import { TMagicButton, TMagicDialog, tMagicMessage, tMagicMessageBox, TMagicTag } from '@tmagic/design';
|
||||
import {
|
||||
type ContainerChangeEventData,
|
||||
defineFormConfig,
|
||||
defineFormItem,
|
||||
type FormConfig,
|
||||
type FormState,
|
||||
MFormBox,
|
||||
type TableColumnConfig,
|
||||
} from '@tmagic/form';
|
||||
@ -87,7 +88,7 @@ const width = defineModel<number>('width', { default: 670 });
|
||||
const boxVisible = defineModel<boolean>('visible', { default: false });
|
||||
|
||||
const props = defineProps<{
|
||||
content: CodeBlockContent;
|
||||
content: Omit<CodeBlockContent, 'content'> & { content: string };
|
||||
disabled?: boolean;
|
||||
isDataSource?: boolean;
|
||||
dataSourceType?: string;
|
||||
@ -118,7 +119,7 @@ const diffChange = () => {
|
||||
difVisible.value = false;
|
||||
};
|
||||
|
||||
const defaultParamColConfig: TableColumnConfig = {
|
||||
const defaultParamColConfig = defineFormItem<TableColumnConfig>({
|
||||
type: 'row',
|
||||
label: '参数类型',
|
||||
items: [
|
||||
@ -146,9 +147,11 @@ const defaultParamColConfig: TableColumnConfig = {
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const functionConfig = computed<FormConfig>(() => [
|
||||
const functionConfig = computed(
|
||||
() =>
|
||||
defineFormConfig([
|
||||
{
|
||||
text: '名称',
|
||||
name: 'name',
|
||||
@ -202,7 +205,7 @@ const functionConfig = computed<FormConfig>(() => [
|
||||
type: 'vs-code',
|
||||
options: inject('codeOptions', {}),
|
||||
autosize: { minRows: 10, maxRows: 30 },
|
||||
onChange: (formState: FormState | undefined, code: string) => {
|
||||
onChange: (_formState, code: string) => {
|
||||
try {
|
||||
// 检测js代码是否存在语法错误
|
||||
getEditorConfig('parseDSL')(code);
|
||||
@ -215,7 +218,8 @@ const functionConfig = computed<FormConfig>(() => [
|
||||
}
|
||||
},
|
||||
},
|
||||
]);
|
||||
]) as FormConfig,
|
||||
);
|
||||
|
||||
const parseContent = (content: any) => {
|
||||
if (typeof content === 'string') {
|
||||
|
||||
@ -13,7 +13,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, useTemplateRef } from 'vue';
|
||||
|
||||
import { type ContainerChangeEventData, type FormConfig, type FormValue, MForm } from '@tmagic/form';
|
||||
import { type ContainerChangeEventData, type FormItemConfig, type FormValue, MForm } from '@tmagic/form';
|
||||
|
||||
import type { CodeParamStatement } from '@editor/type';
|
||||
import { error } from '@editor/utils';
|
||||
@ -34,7 +34,7 @@ const emit = defineEmits(['change']);
|
||||
|
||||
const formRef = useTemplateRef<InstanceType<typeof MForm>>('form');
|
||||
|
||||
const getFormConfig = (items: FormConfig = []) => [
|
||||
const getFormConfig = (items: FormItemConfig[] = []) => [
|
||||
{
|
||||
type: 'fieldset',
|
||||
items,
|
||||
@ -46,13 +46,29 @@ const getFormConfig = (items: FormConfig = []) => [
|
||||
|
||||
const codeParamsConfig = computed(() =>
|
||||
getFormConfig(
|
||||
props.paramsConfig.map(({ name, text, extra, ...config }) => ({
|
||||
type: 'data-source-field-select',
|
||||
props.paramsConfig.map(({ name, text, extra, ...config }) => {
|
||||
let { type } = config;
|
||||
if (typeof type === 'function') {
|
||||
type = type(undefined, {
|
||||
model: props.model[props.name],
|
||||
});
|
||||
}
|
||||
if (type && ['data-source-field-select', 'vs-code'].includes(type)) {
|
||||
return {
|
||||
...config,
|
||||
name,
|
||||
text,
|
||||
extra,
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 'data-source-field-select' as const,
|
||||
name,
|
||||
text,
|
||||
extra,
|
||||
fieldConfig: config,
|
||||
})),
|
||||
};
|
||||
}) as FormItemConfig[],
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@ -98,6 +98,8 @@ export interface EditorProps {
|
||||
isContainer?: (el: HTMLElement) => boolean | Promise<boolean>;
|
||||
/** 用于自定义组件树与画布的右键菜单 */
|
||||
customContentMenu?: CustomContentMenuFunction;
|
||||
/** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
|
||||
beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
|
||||
extendFormState?: (state: FormState) => Record<string, any> | Promise<Record<string, any>>;
|
||||
/** 页面顺序拖拽配置参数 */
|
||||
pageBarSortOptions?: PageBarSortOptions;
|
||||
|
||||
@ -50,6 +50,7 @@ import {
|
||||
createValues,
|
||||
type FieldProps,
|
||||
filterFunction,
|
||||
type FormItemConfig,
|
||||
type FormState,
|
||||
MSelect,
|
||||
type SelectConfig,
|
||||
@ -115,7 +116,7 @@ watch(
|
||||
const selectConfig: SelectConfig = {
|
||||
type: 'select',
|
||||
name: props.name,
|
||||
disable: props.disabled,
|
||||
disabled: props.disabled,
|
||||
options: () => {
|
||||
if (codeDsl.value) {
|
||||
return map(codeDsl.value, (value, key) => ({
|
||||
@ -141,7 +142,9 @@ const onCodeIdChangeHandler = (value: any) => {
|
||||
|
||||
changeRecords.push({
|
||||
propPath: props.prop.replace(`${props.name}`, 'params'),
|
||||
value: paramsConfig.value.length ? createValues(mForm, paramsConfig.value, {}, props.model.params) : {},
|
||||
value: paramsConfig.value.length
|
||||
? createValues(mForm, paramsConfig.value as unknown as FormItemConfig[], {}, props.model.params)
|
||||
: {},
|
||||
});
|
||||
|
||||
emit('change', value, {
|
||||
|
||||
@ -1,6 +1,21 @@
|
||||
<template>
|
||||
<div class="m-editor-data-source-field-select">
|
||||
<template v-if="checkStrictly">
|
||||
<template v-if="dataSourceId">
|
||||
<TMagicCascader
|
||||
:model-value="selectFieldsId"
|
||||
clearable
|
||||
filterable
|
||||
:size="size"
|
||||
:disabled="disabled"
|
||||
:options="fieldsOptions"
|
||||
:props="{
|
||||
checkStrictly,
|
||||
}"
|
||||
@change="fieldChangeHandler"
|
||||
></TMagicCascader>
|
||||
</template>
|
||||
|
||||
<template v-else-if="checkStrictly">
|
||||
<TMagicSelect
|
||||
:model-value="selectDataSourceId"
|
||||
clearable
|
||||
@ -92,6 +107,8 @@ const props = defineProps<{
|
||||
dataSourceFieldType?: DataSourceFieldType[];
|
||||
/** 是否可以编辑数据源,disable表示的是是否可以选择数据源 */
|
||||
notEditable?: boolean | FilterFunction;
|
||||
/** 指定数据源ID,限定只能选择该数据源的字段 */
|
||||
dataSourceId?: string;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -106,7 +123,12 @@ const { dataSourceService, uiService } = useServices();
|
||||
const mForm = inject<FormState | undefined>('mForm');
|
||||
const eventBus = inject<EventBus>('eventBus');
|
||||
|
||||
const dataSources = computed(() => dataSourceService.get('dataSources') || []);
|
||||
const allDataSources = computed(() => dataSourceService.get('dataSources') || []);
|
||||
|
||||
const dataSources = computed(() => {
|
||||
if (!props.dataSourceId) return allDataSources.value;
|
||||
return allDataSources.value.filter((ds) => ds.id === props.dataSourceId);
|
||||
});
|
||||
|
||||
const valueIsKey = computed(() => props.value === 'key');
|
||||
const notEditable = computed(() => filterFunction(mForm, props.notEditable, props));
|
||||
@ -125,7 +147,13 @@ const selectFieldsId = ref<string[]>([]);
|
||||
watch(
|
||||
modelValue,
|
||||
(value) => {
|
||||
if (Array.isArray(value)) {
|
||||
if (props.dataSourceId) {
|
||||
const dsIdValue = valueIsKey.value
|
||||
? props.dataSourceId
|
||||
: `${DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX}${props.dataSourceId}`;
|
||||
selectDataSourceId.value = dsIdValue;
|
||||
selectFieldsId.value = Array.isArray(value) ? value : [];
|
||||
} else if (Array.isArray(value) && value.length) {
|
||||
const [dsId, ...fields] = value;
|
||||
selectDataSourceId.value = dsId;
|
||||
selectFieldsId.value = fields;
|
||||
@ -140,7 +168,7 @@ watch(
|
||||
);
|
||||
|
||||
const fieldsOptions = computed(() => {
|
||||
const ds = dataSources.value.find((ds) => ds.id === removeDataSourceFieldPrefix(selectDataSourceId.value));
|
||||
const ds = allDataSources.value.find((ds) => ds.id === removeDataSourceFieldPrefix(selectDataSourceId.value));
|
||||
|
||||
if (!ds) return [];
|
||||
|
||||
@ -163,8 +191,13 @@ const dsChangeHandler = (v: string) => {
|
||||
};
|
||||
|
||||
const fieldChangeHandler = (v: string[] = []) => {
|
||||
if (props.dataSourceId) {
|
||||
modelValue.value = v;
|
||||
emit('change', v);
|
||||
} else {
|
||||
modelValue.value = [selectDataSourceId.value, ...v];
|
||||
emit('change', modelValue.value);
|
||||
}
|
||||
};
|
||||
|
||||
const onChangeHandler = (v: string[] = []) => {
|
||||
|
||||
@ -8,6 +8,7 @@
|
||||
:value="config.value"
|
||||
:checkStrictly="checkStrictly"
|
||||
:dataSourceFieldType="config.dataSourceFieldType"
|
||||
:dataSourceId="config.dataSourceId"
|
||||
@change="onChangeHandler"
|
||||
></FieldSelect>
|
||||
|
||||
@ -98,7 +99,9 @@ const dataSources = computed(() => dataSourceService.get('dataSources') || []);
|
||||
const disabledDataSource = computed(() => propsService.getDisabledDataSource());
|
||||
|
||||
const type = computed((): string => {
|
||||
let type = props.config.fieldConfig?.type;
|
||||
if (!props.config.fieldConfig) return '';
|
||||
|
||||
let type = 'type' in props.config.fieldConfig ? props.config.fieldConfig.type : '';
|
||||
if (typeof type === 'function') {
|
||||
type = type(mForm, {
|
||||
model: props.model,
|
||||
@ -106,13 +109,18 @@ const type = computed((): string => {
|
||||
}
|
||||
if (type === 'form') return '';
|
||||
if (type === 'container') return '';
|
||||
return type?.replace(/([A-Z])/g, '-$1').toLowerCase() || (props.config.items ? '' : 'text');
|
||||
return (
|
||||
type?.replace(/([A-Z])/g, '-$1').toLowerCase() ||
|
||||
(props.config.fieldConfig && 'items' in props.config.fieldConfig ? '' : 'text')
|
||||
);
|
||||
});
|
||||
|
||||
const tagName = computed(() => {
|
||||
const component =
|
||||
getFormField(type.value || 'container') ||
|
||||
resolveComponent(`m-${props.config.items ? 'form' : 'fields'}-${type.value}`);
|
||||
resolveComponent(
|
||||
`m-${props.config.fieldConfig && 'items' in props.config.fieldConfig ? 'form' : 'fields'}-${type.value}`,
|
||||
);
|
||||
if (typeof component !== 'string') return component;
|
||||
return 'm-fields-text';
|
||||
});
|
||||
|
||||
@ -52,12 +52,15 @@ import { inject, Ref, ref } from 'vue';
|
||||
import type { DataSchema } from '@tmagic/core';
|
||||
import { TMagicButton, tMagicMessage, tMagicMessageBox } from '@tmagic/design';
|
||||
import {
|
||||
type CodeConfig,
|
||||
type ContainerChangeEventData,
|
||||
type DataSourceFieldsConfig,
|
||||
type FieldProps,
|
||||
type FormConfig,
|
||||
type FormState,
|
||||
MFormBox,
|
||||
type NumberConfig,
|
||||
type TextConfig,
|
||||
} from '@tmagic/form';
|
||||
import { type ColumnConfig, MagicTable } from '@tmagic/table';
|
||||
import { getDefaultValueFromFields } from '@tmagic/utils';
|
||||
@ -247,7 +250,7 @@ const dataSourceFieldsConfig: FormConfig = [
|
||||
{ text: 'true', value: true },
|
||||
{ text: 'false', value: false },
|
||||
],
|
||||
},
|
||||
} as unknown as CodeConfig | NumberConfig | TextConfig,
|
||||
{
|
||||
name: 'enable',
|
||||
text: '是否可用',
|
||||
|
||||
@ -48,15 +48,18 @@ import {
|
||||
type DataSourceMethodSelectConfig,
|
||||
type FieldProps,
|
||||
filterFunction,
|
||||
type FormItemConfig,
|
||||
type FormState,
|
||||
MCascader,
|
||||
} from '@tmagic/form';
|
||||
import { DATA_SOURCE_SET_DATA_METHOD_NAME } from '@tmagic/utils';
|
||||
|
||||
import CodeParams from '@editor/components/CodeParams.vue';
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
import type { CodeParamStatement, EventBus } from '@editor/type';
|
||||
import { SideItemKey } from '@editor/type';
|
||||
import { getFieldType } from '@editor/utils';
|
||||
|
||||
defineOptions({
|
||||
name: 'MFieldsDataSourceMethodSelect',
|
||||
@ -91,6 +94,43 @@ const isCustomMethod = computed(() => {
|
||||
const getParamItemsConfig = ([dataSourceId, methodName]: [Id, string] = ['', '']): CodeParamStatement[] => {
|
||||
if (!dataSourceId) return [];
|
||||
|
||||
if (methodName === DATA_SOURCE_SET_DATA_METHOD_NAME) {
|
||||
return [
|
||||
{
|
||||
name: 'field',
|
||||
text: '字段',
|
||||
type: 'data-source-field-select',
|
||||
dataSourceId,
|
||||
checkStrictly: true,
|
||||
},
|
||||
{
|
||||
name: 'data',
|
||||
text: '数据',
|
||||
type: (_formState, { model }) => {
|
||||
const fieldType = getFieldType(dataSourceService.getDataSourceById(`${dataSourceId}`), model.field);
|
||||
|
||||
let type = 'vs-code';
|
||||
|
||||
if (fieldType === 'number') {
|
||||
type = 'number';
|
||||
} else if (fieldType === 'string') {
|
||||
type = 'text';
|
||||
} else if (fieldType === 'boolean') {
|
||||
type = 'switch';
|
||||
}
|
||||
|
||||
return type;
|
||||
},
|
||||
language: 'javascript',
|
||||
options: inject('codeOptions', {}),
|
||||
autosize: {
|
||||
minRows: 1,
|
||||
maxRows: 10,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const paramStatements = dataSources.value
|
||||
?.find((item) => item.id === dataSourceId)
|
||||
?.methods?.find((item) => item.name === methodName)?.params;
|
||||
@ -107,12 +147,14 @@ const paramsConfig = ref<CodeParamStatement[]>(getParamItemsConfig(props.model[p
|
||||
|
||||
const methodsOptions = computed(
|
||||
() =>
|
||||
dataSources.value
|
||||
?.filter((ds) => ds.methods?.length || dataSourceService.getFormMethod(ds.type).length)
|
||||
?.map((ds) => ({
|
||||
dataSources.value?.map((ds) => ({
|
||||
label: ds.title || ds.id,
|
||||
value: ds.id,
|
||||
children: [
|
||||
{
|
||||
label: '设置数据',
|
||||
value: DATA_SOURCE_SET_DATA_METHOD_NAME,
|
||||
},
|
||||
...(dataSourceService?.getFormMethod(ds.type) || []),
|
||||
...(ds.methods || []).map((method) => ({
|
||||
label: method.name,
|
||||
@ -142,7 +184,9 @@ const onChangeHandler = (value: any) => {
|
||||
|
||||
changeRecords.push({
|
||||
propPath: props.prop.replace(`${props.name}`, 'params'),
|
||||
value: paramsConfig.value.length ? createValues(mForm, paramsConfig.value, {}, props.model.params) : {},
|
||||
value: paramsConfig.value.length
|
||||
? createValues(mForm, paramsConfig.value as unknown as FormItemConfig[], {}, props.model.params)
|
||||
: {},
|
||||
});
|
||||
|
||||
emit('change', value, {
|
||||
|
||||
@ -42,7 +42,7 @@ const props = withDefaults(defineProps<FieldProps<DataSourceMethodsConfig>>(), {
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
const codeConfig = ref<CodeBlockContent>();
|
||||
const codeConfig = ref<Omit<CodeBlockContent, 'content'> & { content: string }>();
|
||||
const codeBlockEditorRef = useTemplateRef<InstanceType<typeof CodeBlockEditor>>('codeBlockEditor');
|
||||
|
||||
let editIndex = -1;
|
||||
@ -72,10 +72,14 @@ const methodColumns: ColumnConfig[] = [
|
||||
{
|
||||
text: '编辑',
|
||||
handler: (method: CodeBlockContent, index: number) => {
|
||||
let codeContent = method.content || '({ params, dataSource, app }) => {\n // place your code here\n}';
|
||||
let codeContent: string = '({ params, dataSource, app }) => {\n // place your code here\n}';
|
||||
|
||||
if (typeof codeContent !== 'string') {
|
||||
codeContent = codeContent.toString();
|
||||
if (method.content) {
|
||||
if (typeof method.content !== 'string') {
|
||||
codeContent = method.content.toString();
|
||||
} else {
|
||||
codeContent = method.content;
|
||||
}
|
||||
}
|
||||
|
||||
codeConfig.value = {
|
||||
|
||||
@ -59,18 +59,18 @@ import { ActionType } from '@tmagic/core';
|
||||
import { TMagicButton } from '@tmagic/design';
|
||||
import type {
|
||||
CascaderOption,
|
||||
ChildConfig,
|
||||
CodeSelectColConfig,
|
||||
ContainerChangeEventData,
|
||||
DataSourceMethodSelectConfig,
|
||||
DynamicTypeConfig,
|
||||
EventSelectConfig,
|
||||
FieldProps,
|
||||
FormState,
|
||||
OnChangeHandlerData,
|
||||
PanelConfig,
|
||||
TableConfig,
|
||||
UISelectConfig,
|
||||
} from '@tmagic/form';
|
||||
import { MContainer as MFormContainer, MPanel, MTable } from '@tmagic/form';
|
||||
import { defineFormItem, MContainer as MFormContainer, MPanel, MTable } from '@tmagic/form';
|
||||
import { DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX, traverseNode } from '@tmagic/utils';
|
||||
|
||||
import { useServices } from '@editor/hooks/use-services';
|
||||
@ -90,10 +90,10 @@ const { editorService, dataSourceService, eventsService, codeBlockService, props
|
||||
|
||||
// 事件名称下拉框表单配置
|
||||
const eventNameConfig = computed(() => {
|
||||
const defaultEventNameConfig: ChildConfig = {
|
||||
const defaultEventNameConfig = {
|
||||
name: 'name',
|
||||
text: '事件',
|
||||
type: (mForm, { formValue }: any) => {
|
||||
type: (mForm: FormState | undefined, { formValue }: any) => {
|
||||
if (
|
||||
props.config.src !== 'component' ||
|
||||
(formValue.type === 'page-fragment-container' && formValue.pageFragmentId)
|
||||
@ -213,12 +213,12 @@ const actionTypeConfig = computed(() => {
|
||||
|
||||
// 联动组件配置
|
||||
const targetCompConfig = computed(() => {
|
||||
const defaultTargetCompConfig = {
|
||||
const defaultTargetCompConfig: UISelectConfig = {
|
||||
name: 'to',
|
||||
text: '联动组件',
|
||||
type: 'ui-select',
|
||||
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.actionType === ActionType.COMP,
|
||||
onChange: (MForm: FormState, v: string, { setModel }: OnChangeHandlerData) => {
|
||||
display: (_mForm, { model }) => model.actionType === ActionType.COMP,
|
||||
onChange: (_MForm, _v, { setModel }) => {
|
||||
setModel('method', '');
|
||||
},
|
||||
};
|
||||
@ -227,10 +227,10 @@ const targetCompConfig = computed(() => {
|
||||
|
||||
// 联动组件动作配置
|
||||
const compActionConfig = computed(() => {
|
||||
const defaultCompActionConfig: ChildConfig = {
|
||||
const defaultCompActionConfig: DynamicTypeConfig = {
|
||||
name: 'method',
|
||||
text: '动作',
|
||||
type: (mForm, { model }: any) => {
|
||||
type: (mForm: FormState | undefined, { model }: any) => {
|
||||
const to = editorService.getNodeById(model.to);
|
||||
|
||||
if (to && to.type === 'page-fragment-container' && to.pageFragmentId) {
|
||||
@ -240,7 +240,7 @@ const compActionConfig = computed(() => {
|
||||
return 'select';
|
||||
},
|
||||
checkStrictly: () => props.config.src !== 'component',
|
||||
display: (mForm, { model }: any) => model.actionType === ActionType.COMP,
|
||||
display: (mForm: FormState | undefined, { model }: any) => model.actionType === ActionType.COMP,
|
||||
options: (mForm: FormState, { model }: any) => {
|
||||
const node = editorService.getNodeById(model.to);
|
||||
if (!node?.type) return [];
|
||||
@ -305,7 +305,9 @@ const dataSourceActionConfig = computed(() => {
|
||||
});
|
||||
|
||||
// 兼容旧的数据格式
|
||||
const tableConfig = computed<TableConfig>(() => ({
|
||||
const tableConfig = computed(
|
||||
() =>
|
||||
defineFormItem({
|
||||
type: 'table',
|
||||
name: 'events',
|
||||
items: [
|
||||
@ -339,10 +341,13 @@ const tableConfig = computed<TableConfig>(() => ({
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
}) as TableConfig,
|
||||
);
|
||||
|
||||
// 组件动作组表单配置
|
||||
const actionsConfig = computed<PanelConfig>(() => ({
|
||||
const actionsConfig = computed(
|
||||
() =>
|
||||
defineFormItem({
|
||||
type: 'panel',
|
||||
items: [
|
||||
{
|
||||
@ -360,7 +365,8 @@ const actionsConfig = computed<PanelConfig>(() => ({
|
||||
],
|
||||
},
|
||||
],
|
||||
}));
|
||||
}) as PanelConfig,
|
||||
);
|
||||
|
||||
// 是否为旧的数据格式
|
||||
const isOldVersion = computed(() => {
|
||||
|
||||
@ -26,7 +26,7 @@ import type { StyleSchema } from '@tmagic/schema';
|
||||
|
||||
import MIcon from '@editor/components/Icon.vue';
|
||||
|
||||
import { Background, Border, Font, Layout, Position } from './pro/';
|
||||
import { Background, Border, Font, Layout, Position, Transform } from './pro/';
|
||||
|
||||
defineOptions({
|
||||
name: 'MFieldsStyleSetter',
|
||||
@ -60,6 +60,10 @@ const list = [
|
||||
title: '边框与圆角',
|
||||
component: Border,
|
||||
},
|
||||
{
|
||||
title: '变形',
|
||||
component: Transform,
|
||||
},
|
||||
];
|
||||
|
||||
const collapseValue = shallowRef(
|
||||
|
||||
@ -28,10 +28,10 @@
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { TMagicButton, TMagicInput } from '@tmagic/design';
|
||||
import type { FieldProps, FormItem } from '@tmagic/form';
|
||||
import type { FieldProps, StyleSetterConfig } from '@tmagic/form';
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
defineProps<FieldProps<{ type: 'style-setter' } & FormItem>>();
|
||||
defineProps<FieldProps<StyleSetterConfig>>();
|
||||
|
||||
const horizontalList = [
|
||||
{
|
||||
|
||||
@ -39,12 +39,13 @@
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import type { ContainerChangeEventData, FormValue } from '@tmagic/form';
|
||||
import { MContainer } from '@tmagic/form';
|
||||
import { defineFormItem, MContainer } from '@tmagic/form';
|
||||
import type { StyleSchema } from '@tmagic/schema';
|
||||
|
||||
const direction = ref('');
|
||||
|
||||
const config = computed(() => ({
|
||||
const config = computed(() =>
|
||||
defineFormItem({
|
||||
items: [
|
||||
{
|
||||
name: `border${direction.value}Width`,
|
||||
@ -78,7 +79,8 @@ const config = computed(() => ({
|
||||
},
|
||||
},
|
||||
],
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
const selectDirection = (d?: string) => (direction.value = d || '');
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
import { ContainerChangeEventData, MContainer } from '@tmagic/form';
|
||||
import { type ContainerChangeEventData, defineFormItem, MContainer } from '@tmagic/form';
|
||||
import type { StyleSchema } from '@tmagic/schema';
|
||||
|
||||
import BackgroundPosition from '../components/BackgroundPosition.vue';
|
||||
@ -21,7 +21,7 @@ const emit = defineEmits<{
|
||||
change: [v: StyleSchema, eventData: ContainerChangeEventData];
|
||||
}>();
|
||||
|
||||
const config = {
|
||||
const config = defineFormItem({
|
||||
items: [
|
||||
{
|
||||
name: 'backgroundColor',
|
||||
@ -39,7 +39,7 @@ const config = {
|
||||
type: 'data-source-field-select',
|
||||
fieldConfig: {
|
||||
type: 'img-upload',
|
||||
},
|
||||
} as any,
|
||||
},
|
||||
{
|
||||
name: 'backgroundSize',
|
||||
@ -74,7 +74,7 @@ const config = {
|
||||
labelWidth: '68px',
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
|
||||
emit('change', value, eventData);
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ContainerChangeEventData, MContainer } from '@tmagic/form';
|
||||
import { type ContainerChangeEventData, defineFormItem, MContainer } from '@tmagic/form';
|
||||
import type { StyleSchema } from '@tmagic/schema';
|
||||
|
||||
import Border from '../components/Border.vue';
|
||||
@ -19,7 +19,7 @@ const emit = defineEmits<{
|
||||
change: [v: StyleSchema, eventData: ContainerChangeEventData];
|
||||
}>();
|
||||
|
||||
const config = {
|
||||
const config = defineFormItem({
|
||||
items: [
|
||||
{
|
||||
labelWidth: '68px',
|
||||
@ -31,7 +31,7 @@ const config = {
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
|
||||
emit('change', value, eventData);
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
import { ContainerChangeEventData, MContainer } from '@tmagic/form';
|
||||
import { type ContainerChangeEventData, defineFormItem, MContainer } from '@tmagic/form';
|
||||
import type { StyleSchema } from '@tmagic/schema';
|
||||
|
||||
import { AlignCenter, AlignLeft, AlignRight } from '../icons/text-align';
|
||||
@ -20,7 +20,7 @@ const emit = defineEmits<{
|
||||
change: [v: StyleSchema, eventData: ContainerChangeEventData];
|
||||
}>();
|
||||
|
||||
const config = {
|
||||
const config = defineFormItem({
|
||||
items: [
|
||||
{
|
||||
type: 'row',
|
||||
@ -86,7 +86,7 @@ const config = {
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
|
||||
emit('change', value, eventData);
|
||||
|
||||
@ -12,8 +12,8 @@
|
||||
<script lang="ts" setup>
|
||||
import { markRaw } from 'vue';
|
||||
|
||||
import type { ContainerChangeEventData, FormState } from '@tmagic/form';
|
||||
import { MContainer } from '@tmagic/form';
|
||||
import type { ChildConfig, ContainerChangeEventData } from '@tmagic/form';
|
||||
import { defineFormItem, MContainer } from '@tmagic/form';
|
||||
import type { StyleSchema } from '@tmagic/schema';
|
||||
|
||||
import Box from '../components/Box.vue';
|
||||
@ -42,7 +42,7 @@ const emit = defineEmits<{
|
||||
change: [v: string | StyleSchema, eventData: ContainerChangeEventData];
|
||||
}>();
|
||||
|
||||
const config = {
|
||||
const config = defineFormItem({
|
||||
items: [
|
||||
{
|
||||
name: 'display',
|
||||
@ -74,7 +74,7 @@ const config = {
|
||||
tooltip: '垂直方向 起点在下沿 column-reverse',
|
||||
},
|
||||
],
|
||||
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
},
|
||||
{
|
||||
name: 'justifyContent',
|
||||
@ -89,7 +89,7 @@ const config = {
|
||||
{ value: 'space-between', icon: markRaw(JustifyContentSpaceBetween), tooltip: '两端对齐 space-between' },
|
||||
{ value: 'space-around', icon: markRaw(JustifyContentSpaceAround), tooltip: '横向平分 space-around' },
|
||||
],
|
||||
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
},
|
||||
{
|
||||
name: 'alignItems',
|
||||
@ -104,7 +104,7 @@ const config = {
|
||||
{ value: 'space-between', icon: markRaw(JustifyContentSpaceBetween), tooltip: '两端对齐 space-between' },
|
||||
{ value: 'space-around', icon: markRaw(JustifyContentSpaceAround), tooltip: '横向平分 space-around' },
|
||||
],
|
||||
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
},
|
||||
{
|
||||
name: 'flexWrap',
|
||||
@ -117,7 +117,7 @@ const config = {
|
||||
{ value: 'wrap', text: '正换行', tooltip: '第一行在上方 wrap' },
|
||||
{ value: 'wrap-reverse', text: '逆换行', tooltip: '第一行在下方 wrap-reverse' },
|
||||
],
|
||||
display: (mForm: FormState, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
display: (_mForm, { model }: { model: Record<any, any> }) => model.display === 'flex',
|
||||
},
|
||||
{
|
||||
type: 'row',
|
||||
@ -180,7 +180,7 @@ const config = {
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}) as ChildConfig;
|
||||
|
||||
const change = (value: string | StyleSchema, eventData: ContainerChangeEventData) => {
|
||||
emit('change', value, eventData);
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ContainerChangeEventData, MContainer } from '@tmagic/form';
|
||||
import { type ContainerChangeEventData, defineFormItem, MContainer } from '@tmagic/form';
|
||||
import type { StyleSchema } from '@tmagic/schema';
|
||||
|
||||
const props = defineProps<{
|
||||
@ -24,7 +24,7 @@ const positionText: Record<string, string> = {
|
||||
sticky: '粘性定位',
|
||||
};
|
||||
|
||||
const config = {
|
||||
const config = defineFormItem({
|
||||
items: [
|
||||
{
|
||||
name: 'position',
|
||||
@ -95,7 +95,7 @@ const config = {
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const change = (value: string | StyleSchema, eventData: ContainerChangeEventData) => {
|
||||
emit('change', value, eventData);
|
||||
|
||||
54
packages/editor/src/fields/StyleSetter/pro/Transform.vue
Normal file
54
packages/editor/src/fields/StyleSetter/pro/Transform.vue
Normal file
@ -0,0 +1,54 @@
|
||||
<template>
|
||||
<MContainer :config="config" :model="values" :size="size" :disabled="disabled" @change="change"></MContainer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { type ContainerChangeEventData, defineFormItem, MContainer } from '@tmagic/form';
|
||||
import type { StyleSchema } from '@tmagic/schema';
|
||||
|
||||
defineProps<{
|
||||
values: Partial<StyleSchema>;
|
||||
disabled?: boolean;
|
||||
size?: 'large' | 'default' | 'small';
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
change: [v: StyleSchema, eventData: ContainerChangeEventData];
|
||||
}>();
|
||||
|
||||
const config = defineFormItem({
|
||||
items: [
|
||||
{
|
||||
name: 'transform',
|
||||
items: [
|
||||
{
|
||||
name: 'rotate',
|
||||
text: '旋转角度',
|
||||
labelWidth: '68px',
|
||||
type: 'data-source-field-select',
|
||||
checkStrictly: false,
|
||||
dataSourceFieldType: ['string', 'number'],
|
||||
fieldConfig: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'scale',
|
||||
text: '缩放',
|
||||
labelWidth: '68px',
|
||||
type: 'data-source-field-select',
|
||||
checkStrictly: false,
|
||||
dataSourceFieldType: ['string', 'number'],
|
||||
fieldConfig: {
|
||||
type: 'text',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const change = (value: StyleSchema, eventData: ContainerChangeEventData) => {
|
||||
emit('change', value, eventData);
|
||||
};
|
||||
</script>
|
||||
@ -3,3 +3,4 @@ export { default as Font } from './Font.vue';
|
||||
export { default as Layout } from './Layout.vue';
|
||||
export { default as Position } from './Position.vue';
|
||||
export { default as Border } from './Border.vue';
|
||||
export { default as Transform } from './Transform.vue';
|
||||
|
||||
@ -8,7 +8,7 @@ import CodeBlockEditor from '@editor/components/CodeBlockEditor.vue';
|
||||
import type { Services } from '@editor/type';
|
||||
|
||||
export const useCodeBlockEdit = (codeBlockService: Services['codeBlockService']) => {
|
||||
const codeConfig = ref<CodeBlockContent>();
|
||||
const codeConfig = ref<Omit<CodeBlockContent, 'content'> & { content: string }>();
|
||||
const codeId = ref<string>();
|
||||
const codeBlockEditorRef = useTemplateRef<InstanceType<typeof CodeBlockEditor>>('codeBlockEditor');
|
||||
|
||||
@ -36,10 +36,14 @@ export const useCodeBlockEdit = (codeBlockService: Services['codeBlockService'])
|
||||
return;
|
||||
}
|
||||
|
||||
let codeContent = codeBlock.content;
|
||||
let codeContent = '';
|
||||
|
||||
if (typeof codeContent !== 'string') {
|
||||
codeContent = codeContent.toString();
|
||||
if (codeBlock.content) {
|
||||
if (typeof codeBlock.content !== 'string') {
|
||||
codeContent = codeBlock.content.toString();
|
||||
} else {
|
||||
codeContent = codeBlock.content;
|
||||
}
|
||||
}
|
||||
|
||||
codeConfig.value = {
|
||||
|
||||
@ -233,7 +233,7 @@ export const initServiceEvents = (
|
||||
((event: 'update:modelValue', value: MApp | null) => void),
|
||||
{ editorService, codeBlockService, dataSourceService, depService }: Services,
|
||||
) => {
|
||||
let getTMagicAppPrimise: Promise<TMagicCore | undefined> | null = null;
|
||||
let getTMagicAppPromise: Promise<TMagicCore | undefined> | null = null;
|
||||
|
||||
const getTMagicApp = async (): Promise<TMagicCore | undefined> => {
|
||||
const stage = await getStage();
|
||||
@ -246,11 +246,11 @@ export const initServiceEvents = (
|
||||
return renderer.runtime.getApp?.();
|
||||
}
|
||||
|
||||
if (getTMagicAppPrimise) {
|
||||
return getTMagicAppPrimise;
|
||||
if (getTMagicAppPromise) {
|
||||
return getTMagicAppPromise;
|
||||
}
|
||||
|
||||
getTMagicAppPrimise = new Promise<TMagicCore | undefined>((resolve) => {
|
||||
getTMagicAppPromise = new Promise<TMagicCore | undefined>((resolve) => {
|
||||
// 设置 10s 超时
|
||||
const timeout = globalThis.setTimeout(() => {
|
||||
resolve(void 0);
|
||||
@ -264,7 +264,7 @@ export const initServiceEvents = (
|
||||
});
|
||||
});
|
||||
|
||||
return getTMagicAppPrimise;
|
||||
return getTMagicAppPromise;
|
||||
};
|
||||
|
||||
const updateStageNodes = (nodes: MComponent[]) => {
|
||||
@ -560,7 +560,7 @@ export const initServiceEvents = (
|
||||
depService.clear(nodes);
|
||||
};
|
||||
|
||||
// 由于历史记录变化是更新整个page,所以历史记录变化时,需要重新收集依赖
|
||||
// 历史记录变化时,需要重新收集依赖
|
||||
const historyChangeHandler = (page: MPage | MPageFragment) => {
|
||||
collectIdle([page], true).then(() => {
|
||||
updateStageNode(page);
|
||||
|
||||
@ -80,7 +80,7 @@ const props = withDefaults(
|
||||
let stage: StageCore | null = null;
|
||||
let runtime: Runtime | null = null;
|
||||
|
||||
const { editorService, uiService, keybindingService } = useServices();
|
||||
const { editorService, uiService, keybindingService, stageOverlayService } = useServices();
|
||||
|
||||
const stageLoading = computed(() => editorService.get('stageLoading'));
|
||||
|
||||
@ -97,6 +97,60 @@ const page = computed(() => editorService.get('page'));
|
||||
const zoom = computed(() => uiService.get('zoom'));
|
||||
const node = computed(() => editorService.get('node'));
|
||||
|
||||
/**
|
||||
* 判断元素是否被非页面级的滚动容器裁剪(未完整显示)
|
||||
*
|
||||
* 从元素向上遍历祖先节点,跳过页面/页面片容器,
|
||||
* 检查是否存在设置了 overflow 的滚动容器将该元素裁剪,
|
||||
* 只有元素未被完整显示时才需要打开 overlay 以展示完整内容
|
||||
*/
|
||||
const isClippedByScrollContainer = (el: HTMLElement): boolean => {
|
||||
const win = el.ownerDocument.defaultView;
|
||||
if (!win) return false;
|
||||
|
||||
// 收集所有页面和页面片的 id
|
||||
const root = editorService.get('root');
|
||||
const pageIds = new Set(root?.items?.map((item) => `${item.id}`) ?? []);
|
||||
|
||||
// el 本身就是页面或页面片,无需判断
|
||||
const elId = getIdFromEl()(el);
|
||||
if (elId && pageIds.has(elId)) return false;
|
||||
|
||||
let parent = el.parentElement;
|
||||
|
||||
while (parent && parent !== el.ownerDocument.documentElement) {
|
||||
const parentId = getIdFromEl()(parent);
|
||||
|
||||
// 到达页面或页面片层级,不再继续向上查找
|
||||
if (parentId && pageIds.has(parentId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { overflowX, overflowY } = win.getComputedStyle(parent);
|
||||
|
||||
if (
|
||||
['auto', 'scroll', 'hidden'].includes(overflowX) ||
|
||||
['auto', 'scroll', 'hidden'].includes(overflowY) ||
|
||||
parent.scrollWidth > parent.clientWidth ||
|
||||
parent.scrollHeight > parent.clientHeight
|
||||
) {
|
||||
// 比较元素与容器的可视区域,判断元素是否被裁剪
|
||||
const elRect = el.getBoundingClientRect();
|
||||
const containerRect = parent.getBoundingClientRect();
|
||||
if (
|
||||
elRect.top < containerRect.top ||
|
||||
elRect.left < containerRect.left ||
|
||||
elRect.bottom > containerRect.bottom ||
|
||||
elRect.right > containerRect.right
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
parent = parent.parentElement;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (stage || !page.value) return;
|
||||
|
||||
@ -109,6 +163,40 @@ watchEffect(() => {
|
||||
stageWrapRef.value?.container?.focus();
|
||||
});
|
||||
|
||||
stage.on('dblclick', async (event: MouseEvent) => {
|
||||
if (props.stageOptions.beforeDblclick) {
|
||||
const result = await props.stageOptions.beforeDblclick(event);
|
||||
if (result === false) return;
|
||||
}
|
||||
|
||||
const el = (await stage?.actionManager?.getElementFromPoint(event)) || null;
|
||||
if (!el) return;
|
||||
|
||||
const id = getIdFromEl()(el);
|
||||
|
||||
if (id) {
|
||||
const node = editorService.getNodeById(id);
|
||||
if (node?.type === 'page-fragment-container' && node.pageFragmentId) {
|
||||
await editorService.select(node.pageFragmentId);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!props.disabledStageOverlay && isClippedByScrollContainer(el)) {
|
||||
stageOverlayService.openOverlay(el);
|
||||
return;
|
||||
}
|
||||
|
||||
const nextEl = (await stage?.actionManager?.getNextElementFromPoint(event)) || null;
|
||||
if (nextEl) {
|
||||
const nextId = getIdFromEl()(nextEl);
|
||||
if (nextId) {
|
||||
await editorService.select(nextId);
|
||||
editorService.get('stage')?.select(nextId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
editorService.set('stage', markRaw(stage));
|
||||
|
||||
stage.mount(stageContainerEl.value);
|
||||
|
||||
@ -46,12 +46,7 @@ const style = computed(() => ({
|
||||
}));
|
||||
|
||||
watch(stage, (stage) => {
|
||||
if (stage) {
|
||||
stage.on('dblclick', async (event: MouseEvent) => {
|
||||
const el = (await stage.actionManager?.getElementFromPoint(event)) || null;
|
||||
stageOverlayService.openOverlay(el);
|
||||
});
|
||||
} else {
|
||||
if (!stage) {
|
||||
stageOverlayService.closeOverlay();
|
||||
}
|
||||
});
|
||||
|
||||
@ -17,23 +17,13 @@
|
||||
*/
|
||||
|
||||
import { reactive, toRaw } from 'vue';
|
||||
import { cloneDeep, get, isObject, mergeWith, uniq } from 'lodash-es';
|
||||
import type { Writable } from 'type-fest';
|
||||
import { cloneDeep, isObject, mergeWith, uniq } from 'lodash-es';
|
||||
|
||||
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
|
||||
import { NodeType, Target, Watcher } from '@tmagic/core';
|
||||
import { NodeType } from '@tmagic/core';
|
||||
import type { ChangeRecord } from '@tmagic/form';
|
||||
import { isFixed } from '@tmagic/stage';
|
||||
import {
|
||||
calcValueByFontsize,
|
||||
getElById,
|
||||
getNodeInfo,
|
||||
getNodePath,
|
||||
isNumber,
|
||||
isPage,
|
||||
isPageFragment,
|
||||
isPop,
|
||||
} from '@tmagic/utils';
|
||||
import { getNodeInfo, getNodePath, isPage, isPageFragment } from '@tmagic/utils';
|
||||
|
||||
import BaseService from '@editor/services//BaseService';
|
||||
import propsService from '@editor/services//props';
|
||||
@ -42,69 +32,39 @@ import storageService, { Protocol } from '@editor/services/storage';
|
||||
import type {
|
||||
AddMNode,
|
||||
AsyncHookPlugin,
|
||||
AsyncMethodName,
|
||||
EditorEvents,
|
||||
EditorNodeInfo,
|
||||
HistoryOpType,
|
||||
PastePosition,
|
||||
StepValue,
|
||||
StoreState,
|
||||
StoreStateKey,
|
||||
} from '@editor/type';
|
||||
import { LayerOffset, Layout } from '@editor/type';
|
||||
import { canUsePluginMethods, LayerOffset, Layout } from '@editor/type';
|
||||
import {
|
||||
change2Fixed,
|
||||
calcAlignCenterStyle,
|
||||
calcLayerTargetIndex,
|
||||
calcMoveStyle,
|
||||
classifyDragSources,
|
||||
collectRelatedNodes,
|
||||
COPY_STORAGE_KEY,
|
||||
Fixed2Other,
|
||||
editorNodeMergeCustomizer,
|
||||
fixNodePosition,
|
||||
getInitPositionStyle,
|
||||
getNodeIndex,
|
||||
getPageFragmentList,
|
||||
getPageList,
|
||||
moveItemsInContainer,
|
||||
resolveSelectedNode,
|
||||
setChildrenLayout,
|
||||
setLayout,
|
||||
toggleFixedPosition,
|
||||
} from '@editor/utils/editor';
|
||||
import type { HistoryOpContext } from '@editor/utils/editor-history';
|
||||
import { applyHistoryAddOp, applyHistoryRemoveOp, applyHistoryUpdateOp } from '@editor/utils/editor-history';
|
||||
import { beforePaste, getAddParent } from '@editor/utils/operator';
|
||||
|
||||
export interface EditorEvents {
|
||||
'root-change': [value: StoreState['root'], preValue?: StoreState['root']];
|
||||
select: [node: MNode | null];
|
||||
add: [nodes: MNode[]];
|
||||
remove: [nodes: MNode[]];
|
||||
update: [nodes: { newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }[]];
|
||||
'move-layer': [offset: number | LayerOffset];
|
||||
'drag-to': [data: { targetIndex: number; configs: MNode | MNode[]; targetParent: MContainer }];
|
||||
'history-change': [data: MPage | MPageFragment];
|
||||
}
|
||||
|
||||
const canUsePluginMethods = {
|
||||
async: [
|
||||
'getLayout',
|
||||
'highlight',
|
||||
'select',
|
||||
'multiSelect',
|
||||
'doAdd',
|
||||
'add',
|
||||
'doRemove',
|
||||
'remove',
|
||||
'doUpdate',
|
||||
'update',
|
||||
'sort',
|
||||
'copy',
|
||||
'paste',
|
||||
'doPaste',
|
||||
'doAlignCenter',
|
||||
'alignCenter',
|
||||
'moveLayer',
|
||||
'moveToContainer',
|
||||
'dragTo',
|
||||
'undo',
|
||||
'redo',
|
||||
'move',
|
||||
] as const,
|
||||
sync: [],
|
||||
};
|
||||
|
||||
type AsyncMethodName = Writable<(typeof canUsePluginMethods)['async']>;
|
||||
|
||||
class Editor extends BaseService {
|
||||
public state: StoreState = reactive({
|
||||
root: null,
|
||||
@ -121,6 +81,7 @@ class Editor extends BaseService {
|
||||
disabledMultiSelect: false,
|
||||
});
|
||||
private isHistoryStateChange = false;
|
||||
private selectionBeforeOp: Id[] | null = null;
|
||||
|
||||
constructor() {
|
||||
super(
|
||||
@ -390,6 +351,8 @@ class Editor extends BaseService {
|
||||
* @returns 添加后的节点
|
||||
*/
|
||||
public async add(addNode: AddMNode | MNode[], parent?: MContainer | null): Promise<MNode | MNode[]> {
|
||||
this.captureSelectionBeforeOp();
|
||||
|
||||
const stage = this.get('stage');
|
||||
|
||||
// 新增多个组件只存在于粘贴多个组件,粘贴的是一个完整的config,所以不再需要getPropsValue
|
||||
@ -435,7 +398,21 @@ class Editor extends BaseService {
|
||||
}
|
||||
|
||||
if (!(isPage(newNodes[0]) || isPageFragment(newNodes[0]))) {
|
||||
this.pushHistoryState();
|
||||
const pageForOp = this.getNodeInfo(newNodes[0].id, false).page;
|
||||
this.pushOpHistory(
|
||||
'add',
|
||||
{
|
||||
nodes: newNodes.map((n) => cloneDeep(toRaw(n))),
|
||||
parentId: (this.getParentById(newNodes[0].id, false) ?? this.get('root'))!.id,
|
||||
indexMap: Object.fromEntries(
|
||||
newNodes.map((n) => {
|
||||
const p = this.getParentById(n.id, false) as MContainer;
|
||||
return [n.id, p ? getNodeIndex(n.id, p) : -1];
|
||||
}),
|
||||
),
|
||||
},
|
||||
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
||||
);
|
||||
}
|
||||
|
||||
this.emit('add', newNodes);
|
||||
@ -498,13 +475,33 @@ class Editor extends BaseService {
|
||||
* @param {Object} node
|
||||
*/
|
||||
public async remove(nodeOrNodeList: MNode | MNode[]): Promise<void> {
|
||||
this.captureSelectionBeforeOp();
|
||||
|
||||
const nodes = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList];
|
||||
|
||||
const removedItems: { node: MNode; parentId: Id; index: number }[] = [];
|
||||
let pageForOp: { name: string; id: Id } | null = null;
|
||||
if (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) {
|
||||
for (const n of nodes) {
|
||||
const { parent, node: curNode, page } = this.getNodeInfo(n.id, false);
|
||||
if (parent && curNode) {
|
||||
if (!pageForOp && page) {
|
||||
pageForOp = { name: page.name || '', id: page.id };
|
||||
}
|
||||
const idx = getNodeIndex(curNode.id, parent);
|
||||
removedItems.push({
|
||||
node: cloneDeep(toRaw(curNode)),
|
||||
parentId: parent.id,
|
||||
index: typeof idx === 'number' ? idx : -1,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(nodes.map((node) => this.doRemove(node)));
|
||||
|
||||
if (!(isPage(nodes[0]) || isPageFragment(nodes[0]))) {
|
||||
// 更新历史记录
|
||||
this.pushHistoryState();
|
||||
if (removedItems.length > 0 && pageForOp) {
|
||||
this.pushOpHistory('remove', { removedItems }, pageForOp);
|
||||
}
|
||||
|
||||
this.emit('remove', nodes);
|
||||
@ -525,21 +522,9 @@ class Editor extends BaseService {
|
||||
|
||||
const node = toRaw(info.node);
|
||||
|
||||
let newConfig = await this.toggleFixedPosition(toRaw(config), node, root);
|
||||
let newConfig = await toggleFixedPosition(toRaw(config), node, root, this.getLayout);
|
||||
|
||||
newConfig = mergeWith(cloneDeep(node), newConfig, (objValue, srcValue, key, object: any, source: any) => {
|
||||
if (typeof srcValue === 'undefined' && Object.hasOwn(source, key)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isObject(srcValue) && Array.isArray(objValue)) {
|
||||
// 原来的配置是数组,新的配置是对象,则直接使用新的值
|
||||
return srcValue;
|
||||
}
|
||||
if (Array.isArray(srcValue)) {
|
||||
return srcValue;
|
||||
}
|
||||
});
|
||||
newConfig = mergeWith(cloneDeep(node), newConfig, editorNodeMergeCustomizer);
|
||||
|
||||
if (!newConfig.type) throw new Error('配置缺少type值');
|
||||
|
||||
@ -597,12 +582,28 @@ class Editor extends BaseService {
|
||||
config: MNode | MNode[],
|
||||
data: { changeRecords?: ChangeRecord[] } = {},
|
||||
): Promise<MNode | MNode[]> {
|
||||
this.captureSelectionBeforeOp();
|
||||
|
||||
const nodes = Array.isArray(config) ? config : [config];
|
||||
|
||||
const updateData = await Promise.all(nodes.map((node) => this.doUpdate(node, data)));
|
||||
|
||||
if (updateData[0].oldNode?.type !== NodeType.ROOT) {
|
||||
this.pushHistoryState();
|
||||
const curNodes = this.get('nodes');
|
||||
if (!this.isHistoryStateChange && curNodes.length) {
|
||||
const pageForOp = this.getNodeInfo(nodes[0].id, false).page;
|
||||
this.pushOpHistory(
|
||||
'update',
|
||||
{
|
||||
updatedItems: updateData.map((d) => ({
|
||||
oldNode: cloneDeep(d.oldNode),
|
||||
newNode: cloneDeep(toRaw(d.newNode)),
|
||||
})),
|
||||
},
|
||||
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
||||
);
|
||||
}
|
||||
this.isHistoryStateChange = false;
|
||||
}
|
||||
|
||||
this.emit('update', updateData);
|
||||
@ -616,6 +617,8 @@ class Editor extends BaseService {
|
||||
* @returns void
|
||||
*/
|
||||
public async sort(id1: Id, id2: Id): Promise<void> {
|
||||
this.captureSelectionBeforeOp();
|
||||
|
||||
const root = this.get('root');
|
||||
if (!root) throw new Error('root为空');
|
||||
|
||||
@ -640,9 +643,6 @@ class Editor extends BaseService {
|
||||
parentId: parent.id,
|
||||
root: cloneDeep(root),
|
||||
});
|
||||
|
||||
this.addModifiedNodeId(parent.id);
|
||||
this.pushHistoryState();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -664,31 +664,8 @@ class Editor extends BaseService {
|
||||
public copyWithRelated(config: MNode | MNode[], collectorOptions?: TargetOptions): void {
|
||||
const copyNodes: MNode[] = Array.isArray(config) ? config : [config];
|
||||
|
||||
// 初始化复制组件相关的依赖收集器
|
||||
if (collectorOptions && typeof collectorOptions.isTarget === 'function') {
|
||||
const customTarget = new Target({
|
||||
...collectorOptions,
|
||||
});
|
||||
|
||||
const coperWatcher = new Watcher();
|
||||
|
||||
coperWatcher.addTarget(customTarget);
|
||||
|
||||
coperWatcher.collect(copyNodes, {}, true, collectorOptions.type);
|
||||
Object.keys(customTarget.deps).forEach((nodeId: Id) => {
|
||||
const node = this.getNodeById(nodeId);
|
||||
if (!node) return;
|
||||
customTarget!.deps[nodeId].keys.forEach((key) => {
|
||||
const relateNodeId = get(node, key);
|
||||
const isExist = copyNodes.find((node) => node.id === relateNodeId);
|
||||
if (!isExist) {
|
||||
const relateNode = this.getNodeById(relateNodeId);
|
||||
if (relateNode) {
|
||||
copyNodes.push(relateNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
collectRelatedNodes(copyNodes, collectorOptions, (id) => this.getNodeById(id));
|
||||
}
|
||||
|
||||
storageService.setItem(COPY_STORAGE_KEY, copyNodes, {
|
||||
@ -733,32 +710,16 @@ class Editor extends BaseService {
|
||||
|
||||
public async doAlignCenter(config: MNode): Promise<MNode> {
|
||||
const parent = this.getParentById(config.id);
|
||||
|
||||
if (!parent) throw new Error('找不到父节点');
|
||||
|
||||
const node = cloneDeep(toRaw(config));
|
||||
const layout = await this.getLayout(parent, node);
|
||||
if (layout === Layout.RELATIVE) {
|
||||
return config;
|
||||
}
|
||||
const doc = this.get('stage')?.renderer?.contentWindow?.document;
|
||||
const newStyle = calcAlignCenterStyle(node, parent, layout, doc);
|
||||
|
||||
if (!node.style) return config;
|
||||
|
||||
const stage = this.get('stage');
|
||||
const doc = stage?.renderer?.contentWindow?.document;
|
||||
|
||||
if (doc) {
|
||||
const el = getElById()(doc, node.id);
|
||||
const parentEl = layout === Layout.FIXED ? doc.body : el?.offsetParent;
|
||||
if (parentEl && el) {
|
||||
node.style.left = calcValueByFontsize(doc, (parentEl.clientWidth - el.clientWidth) / 2);
|
||||
node.style.right = '';
|
||||
}
|
||||
} else if (parent.style && isNumber(parent.style?.width) && isNumber(node.style?.width)) {
|
||||
node.style.left = (parent.style.width - node.style.width) / 2;
|
||||
node.style.right = '';
|
||||
}
|
||||
if (!newStyle) return config;
|
||||
|
||||
node.style = newStyle;
|
||||
return node;
|
||||
}
|
||||
|
||||
@ -789,6 +750,8 @@ class Editor extends BaseService {
|
||||
* @param offset 偏移量
|
||||
*/
|
||||
public async moveLayer(offset: number | LayerOffset): Promise<void> {
|
||||
this.captureSelectionBeforeOp();
|
||||
|
||||
const root = this.get('root');
|
||||
if (!root) throw new Error('root为空');
|
||||
|
||||
@ -801,22 +764,16 @@ class Editor extends BaseService {
|
||||
const brothers: MNode[] = parent.items || [];
|
||||
const index = brothers.findIndex((item) => `${item.id}` === `${node?.id}`);
|
||||
|
||||
// 流式布局与绝对定位布局操作的相反的
|
||||
const layout = await this.getLayout(parent, node);
|
||||
const isRelative = layout === Layout.RELATIVE;
|
||||
|
||||
let offsetIndex: number;
|
||||
if (offset === LayerOffset.TOP) {
|
||||
offsetIndex = isRelative ? 0 : brothers.length;
|
||||
} else if (offset === LayerOffset.BOTTOM) {
|
||||
offsetIndex = isRelative ? brothers.length : 0;
|
||||
} else {
|
||||
offsetIndex = index + (isRelative ? -offset : offset);
|
||||
}
|
||||
const offsetIndex = calcLayerTargetIndex(index, offset, brothers.length, isRelative);
|
||||
|
||||
if ((offsetIndex > 0 && offsetIndex > brothers.length) || offsetIndex < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const oldParent = cloneDeep(toRaw(parent));
|
||||
|
||||
brothers.splice(index, 1);
|
||||
brothers.splice(offsetIndex, 0, node);
|
||||
|
||||
@ -829,7 +786,14 @@ class Editor extends BaseService {
|
||||
});
|
||||
|
||||
this.addModifiedNodeId(parent.id);
|
||||
this.pushHistoryState();
|
||||
const pageForOp = this.getNodeInfo(node.id, false).page;
|
||||
this.pushOpHistory(
|
||||
'update',
|
||||
{
|
||||
updatedItems: [{ oldNode: oldParent, newNode: cloneDeep(toRaw(parent)) }],
|
||||
},
|
||||
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
||||
);
|
||||
|
||||
this.emit('move-layer', offset);
|
||||
}
|
||||
@ -840,12 +804,17 @@ class Editor extends BaseService {
|
||||
* @param targetId 容器ID
|
||||
*/
|
||||
public async moveToContainer(config: MNode, targetId: Id): Promise<MNode | undefined> {
|
||||
this.captureSelectionBeforeOp();
|
||||
|
||||
const root = this.get('root');
|
||||
const { node, parent } = this.getNodeInfo(config.id, false);
|
||||
const { node, parent, page: pageForOp } = this.getNodeInfo(config.id, false);
|
||||
const target = this.getNodeById(targetId, false) as MContainer;
|
||||
|
||||
const stage = this.get('stage');
|
||||
if (root && node && parent && stage) {
|
||||
const oldSourceParent = cloneDeep(toRaw(parent));
|
||||
const oldTarget = cloneDeep(toRaw(target));
|
||||
|
||||
const index = getNodeIndex(node.id, parent);
|
||||
parent.items?.splice(index, 1);
|
||||
|
||||
@ -876,62 +845,60 @@ class Editor extends BaseService {
|
||||
|
||||
this.addModifiedNodeId(target.id);
|
||||
this.addModifiedNodeId(parent.id);
|
||||
this.pushHistoryState();
|
||||
this.pushOpHistory(
|
||||
'update',
|
||||
{
|
||||
updatedItems: [
|
||||
{ oldNode: oldSourceParent, newNode: cloneDeep(toRaw(parent)) },
|
||||
{ oldNode: oldTarget, newNode: cloneDeep(toRaw(target)) },
|
||||
],
|
||||
},
|
||||
{ name: pageForOp?.name || '', id: pageForOp!.id },
|
||||
);
|
||||
|
||||
return newConfig;
|
||||
}
|
||||
}
|
||||
|
||||
public async dragTo(config: MNode | MNode[], targetParent: MContainer, targetIndex: number) {
|
||||
this.captureSelectionBeforeOp();
|
||||
|
||||
if (!targetParent || !Array.isArray(targetParent.items)) return;
|
||||
|
||||
const configs = Array.isArray(config) ? config : [config];
|
||||
|
||||
const sourceIndicesInTargetParent: number[] = [];
|
||||
const sourceOutTargetParent: MNode[] = [];
|
||||
const beforeSnapshots = new Map<string, MNode>();
|
||||
for (const cfg of configs) {
|
||||
const { parent } = this.getNodeInfo(cfg.id, false);
|
||||
if (parent && !beforeSnapshots.has(`${parent.id}`)) {
|
||||
beforeSnapshots.set(`${parent.id}`, cloneDeep(toRaw(parent)));
|
||||
}
|
||||
}
|
||||
if (!beforeSnapshots.has(`${targetParent.id}`)) {
|
||||
beforeSnapshots.set(`${targetParent.id}`, cloneDeep(toRaw(targetParent)));
|
||||
}
|
||||
|
||||
const newLayout = await this.getLayout(targetParent);
|
||||
const { sameParentIndices, crossParentConfigs, aborted } = classifyDragSources(configs, targetParent, (id, raw) =>
|
||||
this.getNodeInfo(id, raw),
|
||||
);
|
||||
if (aborted) return;
|
||||
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
forConfigs: for (const config of configs) {
|
||||
const { parent, node: curNode } = this.getNodeInfo(config.id, false);
|
||||
if (!parent || !curNode) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const path = getNodePath(curNode.id, parent.items);
|
||||
|
||||
for (const node of path) {
|
||||
if (targetParent.id === node.id) {
|
||||
continue forConfigs;
|
||||
}
|
||||
}
|
||||
|
||||
const index = getNodeIndex(curNode.id, parent);
|
||||
|
||||
if (parent.id === targetParent.id) {
|
||||
if (typeof index !== 'number' || index === -1) {
|
||||
return;
|
||||
}
|
||||
sourceIndicesInTargetParent.push(index);
|
||||
} else {
|
||||
for (const { config: crossConfig, parent } of crossParentConfigs) {
|
||||
const layout = await this.getLayout(parent);
|
||||
|
||||
if (newLayout !== layout) {
|
||||
setLayout(config, newLayout);
|
||||
setLayout(crossConfig, newLayout);
|
||||
}
|
||||
|
||||
const index = getNodeIndex(crossConfig.id, parent);
|
||||
parent.items?.splice(index, 1);
|
||||
sourceOutTargetParent.push(config);
|
||||
this.addModifiedNodeId(parent.id);
|
||||
}
|
||||
}
|
||||
|
||||
moveItemsInContainer(sourceIndicesInTargetParent, targetParent, targetIndex);
|
||||
moveItemsInContainer(sameParentIndices, targetParent, targetIndex);
|
||||
|
||||
sourceOutTargetParent.forEach((config, index) => {
|
||||
targetParent.items?.splice(targetIndex + index, 0, config);
|
||||
this.addModifiedNodeId(config.id);
|
||||
crossParentConfigs.forEach(({ config: crossConfig }, index) => {
|
||||
targetParent.items?.splice(targetIndex + index, 0, crossConfig);
|
||||
this.addModifiedNodeId(crossConfig.id);
|
||||
});
|
||||
|
||||
const page = this.get('page');
|
||||
@ -946,28 +913,40 @@ class Editor extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
this.pushHistoryState();
|
||||
const updatedItems: { oldNode: MNode; newNode: MNode }[] = [];
|
||||
for (const oldNode of beforeSnapshots.values()) {
|
||||
const newNode = this.getNodeById(oldNode.id, false);
|
||||
if (newNode) {
|
||||
updatedItems.push({ oldNode, newNode: cloneDeep(toRaw(newNode)) });
|
||||
}
|
||||
}
|
||||
const pageForOp = this.getNodeInfo(configs[0].id, false).page;
|
||||
this.pushOpHistory('update', { updatedItems }, { name: pageForOp?.name || '', id: pageForOp!.id });
|
||||
|
||||
this.emit('drag-to', { targetIndex, configs, targetParent });
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤销当前操作
|
||||
* @returns 上一次数据
|
||||
* @returns 被撤销的操作
|
||||
*/
|
||||
public async undo(): Promise<StepValue | null> {
|
||||
const value = historyService.undo();
|
||||
await this.changeHistoryState(value);
|
||||
if (value) {
|
||||
await this.applyHistoryOp(value, true);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复到下一步
|
||||
* @returns 下一步数据
|
||||
* @returns 被恢复的操作
|
||||
*/
|
||||
public async redo(): Promise<StepValue | null> {
|
||||
const value = historyService.redo();
|
||||
await this.changeHistoryState(value);
|
||||
if (value) {
|
||||
await this.applyHistoryOp(value, false);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@ -975,47 +954,10 @@ class Editor extends BaseService {
|
||||
const node = toRaw(this.get('node'));
|
||||
if (!node || isPage(node)) return;
|
||||
|
||||
const { style, id, type } = node;
|
||||
if (!style || !['absolute', 'fixed'].includes(style.position)) return;
|
||||
const newStyle = calcMoveStyle(node.style || {}, left, top);
|
||||
if (!newStyle) return;
|
||||
|
||||
const update = (style: { [key: string]: any }) =>
|
||||
this.update({
|
||||
id,
|
||||
type,
|
||||
style,
|
||||
});
|
||||
|
||||
if (top) {
|
||||
if (isNumber(style.top)) {
|
||||
update({
|
||||
...style,
|
||||
top: Number(style.top) + Number(top),
|
||||
bottom: '',
|
||||
});
|
||||
} else if (isNumber(style.bottom)) {
|
||||
update({
|
||||
...style,
|
||||
bottom: Number(style.bottom) - Number(top),
|
||||
top: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (left) {
|
||||
if (isNumber(style.left)) {
|
||||
update({
|
||||
...style,
|
||||
left: Number(style.left) + Number(left),
|
||||
right: '',
|
||||
});
|
||||
} else if (isNumber(style.right)) {
|
||||
update({
|
||||
...style,
|
||||
right: Number(style.right) - Number(left),
|
||||
left: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
await this.update({ id: node.id, type: node.type, style: newStyle });
|
||||
}
|
||||
|
||||
public resetState() {
|
||||
@ -1068,70 +1010,89 @@ class Editor extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private pushHistoryState() {
|
||||
const curNode = cloneDeep(toRaw(this.get('node')));
|
||||
const page = this.get('page');
|
||||
if (!this.isHistoryStateChange && curNode && page) {
|
||||
historyService.push({
|
||||
data: cloneDeep(toRaw(page)),
|
||||
modifiedNodeIds: this.get('modifiedNodeIds'),
|
||||
nodeId: curNode.id,
|
||||
});
|
||||
private captureSelectionBeforeOp() {
|
||||
if (this.isHistoryStateChange || this.selectionBeforeOp) return;
|
||||
this.selectionBeforeOp = this.get('nodes').map((n) => n.id);
|
||||
}
|
||||
|
||||
private pushOpHistory(opType: HistoryOpType, extra: Partial<StepValue>, pageData: { name: string; id: Id }) {
|
||||
if (this.isHistoryStateChange) {
|
||||
this.selectionBeforeOp = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const step: StepValue = {
|
||||
data: pageData,
|
||||
opType,
|
||||
selectedBefore: this.selectionBeforeOp ?? [],
|
||||
selectedAfter: this.get('nodes').map((n) => n.id),
|
||||
modifiedNodeIds: new Map(this.get('modifiedNodeIds')),
|
||||
...extra,
|
||||
};
|
||||
historyService.push(step);
|
||||
this.selectionBeforeOp = null;
|
||||
this.isHistoryStateChange = false;
|
||||
}
|
||||
|
||||
private async changeHistoryState(value: StepValue | null) {
|
||||
if (!value) return;
|
||||
|
||||
/**
|
||||
* 应用历史操作(撤销 / 重做)
|
||||
* @param step 操作记录
|
||||
* @param reverse true = 撤销,false = 重做
|
||||
*/
|
||||
private async applyHistoryOp(step: StepValue, reverse: boolean) {
|
||||
this.isHistoryStateChange = true;
|
||||
await this.update(value.data);
|
||||
this.set('modifiedNodeIds', value.modifiedNodeIds);
|
||||
|
||||
const root = this.get('root');
|
||||
const stage = this.get('stage');
|
||||
if (!root) return;
|
||||
|
||||
const ctx: HistoryOpContext = {
|
||||
root,
|
||||
stage,
|
||||
getNodeById: (id, raw) => this.getNodeById(id, raw),
|
||||
getNodeInfo: (id, raw) => this.getNodeInfo(id, raw),
|
||||
setRoot: (r) => this.set('root', r),
|
||||
setPage: (p) => this.set('page', p),
|
||||
getPage: () => this.get('page'),
|
||||
};
|
||||
|
||||
switch (step.opType) {
|
||||
case 'add':
|
||||
await applyHistoryAddOp(step, reverse, ctx);
|
||||
break;
|
||||
case 'remove':
|
||||
await applyHistoryRemoveOp(step, reverse, ctx);
|
||||
break;
|
||||
case 'update':
|
||||
await applyHistoryUpdateOp(step, reverse, ctx);
|
||||
break;
|
||||
}
|
||||
|
||||
this.set('modifiedNodeIds', step.modifiedNodeIds);
|
||||
|
||||
const page = toRaw(this.get('page'));
|
||||
if (page) {
|
||||
const selectIds = reverse ? step.selectedBefore : step.selectedAfter;
|
||||
setTimeout(() => {
|
||||
if (!value.nodeId) return;
|
||||
this.select(value.nodeId).then(() => {
|
||||
this.get('stage')?.select(value.nodeId);
|
||||
});
|
||||
if (!selectIds.length) return;
|
||||
|
||||
if (selectIds.length > 1) {
|
||||
this.multiSelect(selectIds);
|
||||
stage?.multiSelect(selectIds);
|
||||
} else {
|
||||
this.select(selectIds[0])
|
||||
.then(() => stage?.select(selectIds[0]))
|
||||
.catch(() => {});
|
||||
}
|
||||
}, 0);
|
||||
this.emit('history-change', value.data);
|
||||
this.emit('history-change', page as MPage | MPageFragment);
|
||||
}
|
||||
|
||||
private async toggleFixedPosition(dist: MNode, src: MNode, root: MApp) {
|
||||
const newConfig = cloneDeep(dist);
|
||||
|
||||
if (!isPop(src) && newConfig.style?.position) {
|
||||
if (isFixed(newConfig.style) && !isFixed(src.style || {})) {
|
||||
newConfig.style = change2Fixed(newConfig, root);
|
||||
} else if (!isFixed(newConfig.style) && isFixed(src.style || {})) {
|
||||
newConfig.style = await Fixed2Other(newConfig, root, this.getLayout);
|
||||
}
|
||||
}
|
||||
|
||||
return newConfig;
|
||||
this.isHistoryStateChange = false;
|
||||
}
|
||||
|
||||
private selectedConfigExceptionHandler(config: MNode | Id): EditorNodeInfo {
|
||||
let id: Id;
|
||||
if (typeof config === 'string' || typeof config === 'number') {
|
||||
id = config;
|
||||
} else {
|
||||
id = config.id;
|
||||
}
|
||||
if (!id) {
|
||||
throw new Error('没有ID,无法选中');
|
||||
}
|
||||
|
||||
const { node, parent, page } = this.getNodeInfo(id);
|
||||
if (!node) throw new Error('获取不到组件信息');
|
||||
|
||||
if (node.id === this.state.root?.id) {
|
||||
throw new Error('不能选根节点');
|
||||
}
|
||||
return {
|
||||
node,
|
||||
parent,
|
||||
page,
|
||||
};
|
||||
return resolveSelectedNode(config, (id) => this.getNodeInfo(id), this.state.root?.id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -56,15 +56,7 @@ class History extends BaseService {
|
||||
this.state.pageId = page.id;
|
||||
|
||||
if (!this.state.pageSteps[this.state.pageId]) {
|
||||
const undoRedo = new UndoRedo<StepValue>();
|
||||
|
||||
undoRedo.pushElement({
|
||||
data: page,
|
||||
modifiedNodeIds: new Map(),
|
||||
nodeId: page.id,
|
||||
});
|
||||
|
||||
this.state.pageSteps[this.state.pageId] = undoRedo;
|
||||
this.state.pageSteps[this.state.pageId] = new UndoRedo<StepValue>();
|
||||
}
|
||||
|
||||
this.setCanUndoRedo();
|
||||
|
||||
@ -102,9 +102,11 @@ class Props extends BaseService {
|
||||
}
|
||||
|
||||
public async setPropsConfig(type: string, config: FormConfig | PropsFormConfigFunction) {
|
||||
let c = config;
|
||||
let c: FormConfig;
|
||||
if (typeof config === 'function') {
|
||||
c = config({ editorService });
|
||||
} else {
|
||||
c = config;
|
||||
}
|
||||
|
||||
this.state.propsConfigMap[toLine(type)] = await this.fillConfig(Array.isArray(c) ? c : [c]);
|
||||
|
||||
@ -20,10 +20,10 @@ import type { Component } from 'vue';
|
||||
import type EventEmitter from 'events';
|
||||
import type * as Monaco from 'monaco-editor';
|
||||
import type { default as Sortable, Options, SortableEvent } from 'sortablejs';
|
||||
import type { PascalCasedProperties } from 'type-fest';
|
||||
import type { PascalCasedProperties, Writable } from 'type-fest';
|
||||
|
||||
import type { CodeBlockContent, CodeBlockDSL, Id, MApp, MContainer, MNode, MPage, MPageFragment } from '@tmagic/core';
|
||||
import type { FormConfig, TableColumnConfig } from '@tmagic/form';
|
||||
import type { ChangeRecord, FormConfig, TableColumnConfig, TypeFunction } from '@tmagic/form';
|
||||
import type StageCore from '@tmagic/stage';
|
||||
import type {
|
||||
ContainerHighlightType,
|
||||
@ -164,6 +164,8 @@ export interface StageOptions {
|
||||
disabledMultiSelect?: boolean;
|
||||
disabledRule?: boolean;
|
||||
zoom?: number;
|
||||
/** 画布双击前的钩子函数,返回 false 则阻止默认的双击行为 */
|
||||
beforeDblclick?: (event: MouseEvent) => Promise<boolean | void> | boolean | void;
|
||||
}
|
||||
|
||||
export interface StoreState {
|
||||
@ -541,14 +543,31 @@ export interface CodeParamStatement {
|
||||
/** 参数名称 */
|
||||
name: string;
|
||||
/** 参数类型 */
|
||||
type?: string;
|
||||
type?: string | TypeFunction<string>;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export type HistoryOpType = 'add' | 'remove' | 'update';
|
||||
|
||||
export interface StepValue {
|
||||
data: MPage | MPageFragment;
|
||||
/** 页面信息 */
|
||||
data: { name: string; id: Id };
|
||||
opType: HistoryOpType;
|
||||
/** 操作前选中的节点 ID,用于撤销后恢复选择状态 */
|
||||
selectedBefore: Id[];
|
||||
/** 操作后选中的节点 ID,用于重做后恢复选择状态 */
|
||||
selectedAfter: Id[];
|
||||
modifiedNodeIds: Map<Id, Id>;
|
||||
nodeId: Id;
|
||||
/** opType 'add': 新增的节点 */
|
||||
nodes?: MNode[];
|
||||
/** opType 'add': 父节点 ID */
|
||||
parentId?: Id;
|
||||
/** opType 'add': 每个新增节点在父节点 items 中的索引 */
|
||||
indexMap?: Record<string, number>;
|
||||
/** opType 'remove': 被删除的节点及其位置信息 */
|
||||
removedItems?: { node: MNode; parentId: Id; index: number }[];
|
||||
/** opType 'update': 变更前后的节点快照 */
|
||||
updatedItems?: { oldNode: MNode; newNode: MNode }[];
|
||||
}
|
||||
|
||||
export interface HistoryState {
|
||||
@ -712,3 +731,44 @@ export type CustomContentMenuFunction = (
|
||||
menus: (MenuButton | MenuComponent)[],
|
||||
type: 'layer' | 'data-source' | 'viewer' | 'code-block',
|
||||
) => (MenuButton | MenuComponent)[];
|
||||
|
||||
export interface EditorEvents {
|
||||
'root-change': [value: StoreState['root'], preValue?: StoreState['root']];
|
||||
select: [node: MNode | null];
|
||||
add: [nodes: MNode[]];
|
||||
remove: [nodes: MNode[]];
|
||||
update: [nodes: { newNode: MNode; oldNode: MNode; changeRecords?: ChangeRecord[] }[]];
|
||||
'move-layer': [offset: number | LayerOffset];
|
||||
'drag-to': [data: { targetIndex: number; configs: MNode | MNode[]; targetParent: MContainer }];
|
||||
'history-change': [data: MPage | MPageFragment];
|
||||
}
|
||||
|
||||
export const canUsePluginMethods = {
|
||||
async: [
|
||||
'getLayout',
|
||||
'highlight',
|
||||
'select',
|
||||
'multiSelect',
|
||||
'doAdd',
|
||||
'add',
|
||||
'doRemove',
|
||||
'remove',
|
||||
'doUpdate',
|
||||
'update',
|
||||
'sort',
|
||||
'copy',
|
||||
'paste',
|
||||
'doPaste',
|
||||
'doAlignCenter',
|
||||
'alignCenter',
|
||||
'moveLayer',
|
||||
'moveToContainer',
|
||||
'dragTo',
|
||||
'undo',
|
||||
'redo',
|
||||
'move',
|
||||
] as const,
|
||||
sync: [],
|
||||
};
|
||||
|
||||
export type AsyncMethodName = Writable<(typeof canUsePluginMethods)['async']>;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { defineFormConfig } from '@tmagic/form';
|
||||
import { defineFormConfig, type FormConfig } from '@tmagic/form';
|
||||
|
||||
export default () =>
|
||||
export default (): FormConfig =>
|
||||
defineFormConfig([
|
||||
{
|
||||
name: 'id',
|
||||
|
||||
@ -1,14 +1,11 @@
|
||||
import { DataSchema, DataSourceFieldType, DataSourceSchema } from '@tmagic/core';
|
||||
import { CascaderOption, FormConfig, FormState } from '@tmagic/form';
|
||||
import type { DataSchema, DataSourceFieldType, DataSourceSchema } from '@tmagic/core';
|
||||
import { type CascaderOption, defineFormItem, type FormConfig } from '@tmagic/form';
|
||||
import { dataSourceTemplateRegExp, getKeysArray, isNumber } from '@tmagic/utils';
|
||||
|
||||
import BaseFormConfig from './formConfigs/base';
|
||||
import HttpFormConfig from './formConfigs/http';
|
||||
|
||||
const fillConfig = (config: FormConfig): FormConfig => [
|
||||
...BaseFormConfig(),
|
||||
...config,
|
||||
{
|
||||
const dataSourceFormConfig = defineFormItem({
|
||||
type: 'tab',
|
||||
items: [
|
||||
{
|
||||
@ -53,7 +50,7 @@ const fillConfig = (config: FormConfig): FormConfig => [
|
||||
},
|
||||
{
|
||||
title: '请求参数裁剪',
|
||||
display: (_formState: FormState, { model }: any) => model.type === 'http',
|
||||
display: (_formState, { model }) => model.type === 'http',
|
||||
items: [
|
||||
{
|
||||
name: 'beforeRequest',
|
||||
@ -65,7 +62,7 @@ const fillConfig = (config: FormConfig): FormConfig => [
|
||||
},
|
||||
{
|
||||
title: '响应数据裁剪',
|
||||
display: (_formState: FormState, { model }: any) => model.type === 'http',
|
||||
display: (_formStat, { model }) => model.type === 'http',
|
||||
items: [
|
||||
{
|
||||
name: 'afterResponse',
|
||||
@ -76,8 +73,9 @@ const fillConfig = (config: FormConfig): FormConfig => [
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
});
|
||||
|
||||
const fillConfig = (config: FormConfig): FormConfig => [...BaseFormConfig(), ...config, dataSourceFormConfig];
|
||||
|
||||
export const getFormConfig = (type: string, configs: Record<string, FormConfig>): FormConfig => {
|
||||
switch (type) {
|
||||
|
||||
138
packages/editor/src/utils/editor-history.ts
Normal file
138
packages/editor/src/utils/editor-history.ts
Normal file
@ -0,0 +1,138 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2025 Tencent. All rights reserved.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { toRaw } from 'vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment } from '@tmagic/core';
|
||||
import { NodeType } from '@tmagic/core';
|
||||
import type StageCore from '@tmagic/stage';
|
||||
import { isPage, isPageFragment } from '@tmagic/utils';
|
||||
|
||||
import type { EditorNodeInfo, StepValue } from '@editor/type';
|
||||
import { getNodeIndex } from '@editor/utils/editor';
|
||||
|
||||
export interface HistoryOpContext {
|
||||
root: MApp;
|
||||
stage: StageCore | null;
|
||||
getNodeById(id: Id, raw?: boolean): MNode | null;
|
||||
getNodeInfo(id: Id, raw?: boolean): EditorNodeInfo;
|
||||
setRoot(root: MApp): void;
|
||||
setPage(page: MPage | MPageFragment): void;
|
||||
getPage(): MPage | MPageFragment | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用 add 类型的历史操作
|
||||
* reverse=true(撤销):从父节点中移除已添加的节点
|
||||
* reverse=false(重做):重新添加节点到父节点中
|
||||
*/
|
||||
export async function applyHistoryAddOp(step: StepValue, reverse: boolean, ctx: HistoryOpContext): Promise<void> {
|
||||
const { root, stage } = ctx;
|
||||
|
||||
if (reverse) {
|
||||
for (const node of step.nodes ?? []) {
|
||||
const parent = ctx.getNodeById(step.parentId!, false) as MContainer;
|
||||
if (!parent?.items) continue;
|
||||
const idx = getNodeIndex(node.id, parent);
|
||||
if (typeof idx === 'number' && idx !== -1) {
|
||||
parent.items.splice(idx, 1);
|
||||
}
|
||||
await stage?.remove({ id: node.id, parentId: parent.id, root: cloneDeep(root) });
|
||||
}
|
||||
} else {
|
||||
const parent = ctx.getNodeById(step.parentId!, false) as MContainer;
|
||||
if (parent?.items) {
|
||||
for (const node of step.nodes ?? []) {
|
||||
const idx = step.indexMap?.[node.id] ?? parent.items.length;
|
||||
parent.items.splice(idx, 0, cloneDeep(node));
|
||||
await stage?.add({
|
||||
config: cloneDeep(node),
|
||||
parent: cloneDeep(parent),
|
||||
parentId: parent.id,
|
||||
root: cloneDeep(root),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用 remove 类型的历史操作
|
||||
* reverse=true(撤销):将已删除的节点按原位置重新插入
|
||||
* reverse=false(重做):再次删除节点
|
||||
*/
|
||||
export async function applyHistoryRemoveOp(step: StepValue, reverse: boolean, ctx: HistoryOpContext): Promise<void> {
|
||||
const { root, stage } = ctx;
|
||||
|
||||
if (reverse) {
|
||||
const sorted = [...(step.removedItems ?? [])].sort((a, b) => a.index - b.index);
|
||||
for (const { node, parentId, index } of sorted) {
|
||||
const parent = ctx.getNodeById(parentId, false) as MContainer;
|
||||
if (!parent?.items) continue;
|
||||
parent.items.splice(index, 0, cloneDeep(node));
|
||||
await stage?.add({ config: cloneDeep(node), parent: cloneDeep(parent), parentId, root: cloneDeep(root) });
|
||||
}
|
||||
} else {
|
||||
for (const { node, parentId } of step.removedItems ?? []) {
|
||||
const parent = ctx.getNodeById(parentId, false) as MContainer;
|
||||
if (!parent?.items) continue;
|
||||
const idx = getNodeIndex(node.id, parent);
|
||||
if (typeof idx === 'number' && idx !== -1) {
|
||||
parent.items.splice(idx, 1);
|
||||
}
|
||||
await stage?.remove({ id: node.id, parentId, root: cloneDeep(root) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用 update 类型的历史操作
|
||||
* reverse=true(撤销):将节点恢复为 oldNode
|
||||
* reverse=false(重做):将节点更新为 newNode
|
||||
*/
|
||||
export async function applyHistoryUpdateOp(step: StepValue, reverse: boolean, ctx: HistoryOpContext): Promise<void> {
|
||||
const { root, stage } = ctx;
|
||||
const items = step.updatedItems ?? [];
|
||||
|
||||
for (const { oldNode, newNode } of items) {
|
||||
const config = reverse ? oldNode : newNode;
|
||||
if (config.type === NodeType.ROOT) {
|
||||
ctx.setRoot(cloneDeep(config) as MApp);
|
||||
continue;
|
||||
}
|
||||
const info = ctx.getNodeInfo(config.id, false);
|
||||
if (!info.parent) continue;
|
||||
const idx = getNodeIndex(config.id, info.parent);
|
||||
if (typeof idx !== 'number' || idx === -1) continue;
|
||||
info.parent.items![idx] = cloneDeep(config);
|
||||
|
||||
if (isPage(config) || isPageFragment(config)) {
|
||||
ctx.setPage(config as MPage | MPageFragment);
|
||||
}
|
||||
}
|
||||
|
||||
const curPage = ctx.getPage();
|
||||
if (stage && curPage) {
|
||||
await stage.update({
|
||||
config: cloneDeep(toRaw(curPage)),
|
||||
parentId: root.id,
|
||||
root: cloneDeep(toRaw(root)),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -17,12 +17,13 @@
|
||||
*/
|
||||
|
||||
import { detailedDiff } from 'deep-object-diff';
|
||||
import { isObject } from 'lodash-es';
|
||||
import { cloneDeep, get, isObject } from 'lodash-es';
|
||||
import serialize from 'serialize-javascript';
|
||||
|
||||
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment } from '@tmagic/core';
|
||||
import { NODE_CONDS_KEY, NodeType } from '@tmagic/core';
|
||||
import type { Id, MApp, MContainer, MNode, MPage, MPageFragment, TargetOptions } from '@tmagic/core';
|
||||
import { NODE_CONDS_KEY, NodeType, Target, Watcher } from '@tmagic/core';
|
||||
import type StageCore from '@tmagic/stage';
|
||||
import { isFixed } from '@tmagic/stage';
|
||||
import {
|
||||
calcValueByFontsize,
|
||||
getElById,
|
||||
@ -34,7 +35,8 @@ import {
|
||||
isValueIncludeDataSource,
|
||||
} from '@tmagic/utils';
|
||||
|
||||
import { Layout } from '@editor/type';
|
||||
import type { EditorNodeInfo } from '@editor/type';
|
||||
import { LayerOffset, Layout } from '@editor/type';
|
||||
|
||||
export const COPY_STORAGE_KEY = '$MagicEditorCopyData';
|
||||
export const COPY_CODE_STORAGE_KEY = '$MagicEditorCopyCode';
|
||||
@ -436,3 +438,246 @@ export const buildChangeRecords = (value: any, basePath: string) => {
|
||||
|
||||
return changeRecords;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据节点配置或ID解析出选中节点信息,并进行合法性校验
|
||||
* @param config 节点配置或节点ID
|
||||
* @param getNodeInfoFn 获取节点信息的回调函数
|
||||
* @param rootId 根节点ID,用于排除根节点被选中
|
||||
* @returns 选中节点的完整信息(node、parent、page)
|
||||
*/
|
||||
export const resolveSelectedNode = (
|
||||
config: MNode | Id,
|
||||
getNodeInfoFn: (id: Id) => EditorNodeInfo,
|
||||
rootId?: Id,
|
||||
): EditorNodeInfo => {
|
||||
const id: Id = typeof config === 'string' || typeof config === 'number' ? config : config.id;
|
||||
|
||||
if (!id) {
|
||||
throw new Error('没有ID,无法选中');
|
||||
}
|
||||
|
||||
const { node, parent, page } = getNodeInfoFn(id);
|
||||
if (!node) throw new Error('获取不到组件信息');
|
||||
if (node.id === rootId) throw new Error('不能选根节点');
|
||||
|
||||
return { node, parent, page };
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理节点在 fixed 定位与其他定位之间的切换
|
||||
* 当节点从非 fixed 变为 fixed 时,根据节点路径累加偏移量;反之则还原偏移量
|
||||
* @param dist 更新后的节点配置(目标状态)
|
||||
* @param src 更新前的节点配置(原始状态)
|
||||
* @param root 根节点配置,用于计算节点路径上的偏移量
|
||||
* @param getLayoutFn 获取父节点布局方式的回调函数
|
||||
* @returns 处理后的节点配置(深拷贝)
|
||||
*/
|
||||
export const toggleFixedPosition = async (
|
||||
dist: MNode,
|
||||
src: MNode,
|
||||
root: MApp,
|
||||
getLayoutFn: (parent: MNode, node?: MNode | null) => Promise<Layout>,
|
||||
): Promise<MNode> => {
|
||||
const newConfig = cloneDeep(dist);
|
||||
|
||||
if (!isPop(src) && newConfig.style?.position) {
|
||||
if (isFixed(newConfig.style) && !isFixed(src.style || {})) {
|
||||
newConfig.style = change2Fixed(newConfig, root);
|
||||
} else if (!isFixed(newConfig.style) && isFixed(src.style || {})) {
|
||||
newConfig.style = await Fixed2Other(newConfig, root, getLayoutFn);
|
||||
}
|
||||
}
|
||||
|
||||
return newConfig;
|
||||
};
|
||||
|
||||
/**
|
||||
* 根据键盘移动的偏移量计算节点的新样式
|
||||
* 仅对 absolute 或 fixed 定位的节点生效;优先修改 top/left,若未设置则修改 bottom/right
|
||||
* @param style 节点当前样式
|
||||
* @param left 水平方向偏移量(正值向右,负值向左)
|
||||
* @param top 垂直方向偏移量(正值向下,负值向上)
|
||||
* @returns 计算后的新样式对象,若节点不支持移动则返回 null
|
||||
*/
|
||||
export const calcMoveStyle = (style: Record<string, any>, left: number, top: number): Record<string, any> | null => {
|
||||
if (!style || !['absolute', 'fixed'].includes(style.position)) return null;
|
||||
|
||||
const newStyle: Record<string, any> = { ...style };
|
||||
|
||||
if (top) {
|
||||
if (isNumber(style.top)) {
|
||||
newStyle.top = Number(style.top) + Number(top);
|
||||
newStyle.bottom = '';
|
||||
} else if (isNumber(style.bottom)) {
|
||||
newStyle.bottom = Number(style.bottom) - Number(top);
|
||||
newStyle.top = '';
|
||||
}
|
||||
}
|
||||
|
||||
if (left) {
|
||||
if (isNumber(style.left)) {
|
||||
newStyle.left = Number(style.left) + Number(left);
|
||||
newStyle.right = '';
|
||||
} else if (isNumber(style.right)) {
|
||||
newStyle.right = Number(style.right) - Number(left);
|
||||
newStyle.left = '';
|
||||
}
|
||||
}
|
||||
|
||||
return newStyle;
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算节点水平居中对齐后的样式
|
||||
* 流式布局(relative)下不做处理;优先通过 DOM 元素实际宽度计算,回退到配置中的 width 值
|
||||
* @param node 需要居中的节点配置
|
||||
* @param parent 父容器节点配置
|
||||
* @param layout 当前布局方式
|
||||
* @param doc 画布 document 对象,用于获取 DOM 元素实际宽度
|
||||
* @returns 计算后的新样式对象,若不支持居中则返回 null
|
||||
*/
|
||||
export const calcAlignCenterStyle = (
|
||||
node: MNode,
|
||||
parent: MContainer,
|
||||
layout: Layout,
|
||||
doc?: Document,
|
||||
): Record<string, any> | null => {
|
||||
if (layout === Layout.RELATIVE || !node.style) return null;
|
||||
|
||||
const style = { ...node.style };
|
||||
|
||||
if (doc) {
|
||||
const el = getElById()(doc, node.id);
|
||||
const parentEl = layout === Layout.FIXED ? doc.body : el?.offsetParent;
|
||||
if (parentEl && el) {
|
||||
style.left = calcValueByFontsize(doc, (parentEl.clientWidth - el.clientWidth) / 2);
|
||||
style.right = '';
|
||||
}
|
||||
} else if (parent.style && isNumber(parent.style?.width) && isNumber(node.style?.width)) {
|
||||
style.left = (parent.style.width - node.style.width) / 2;
|
||||
style.right = '';
|
||||
}
|
||||
|
||||
return style;
|
||||
};
|
||||
|
||||
/**
|
||||
* 计算图层移动后的目标索引
|
||||
* 流式布局与绝对定位布局的移动方向相反:流式布局中"上移"对应索引减小,绝对定位中"上移"对应索引增大
|
||||
* @param currentIndex 节点当前在兄弟列表中的索引
|
||||
* @param offset 移动偏移量,支持数值或 LayerOffset.TOP / LayerOffset.BOTTOM
|
||||
* @param brothersLength 兄弟节点总数
|
||||
* @param isRelative 是否为流式布局
|
||||
* @returns 目标索引位置
|
||||
*/
|
||||
export const calcLayerTargetIndex = (
|
||||
currentIndex: number,
|
||||
offset: number | LayerOffset,
|
||||
brothersLength: number,
|
||||
isRelative: boolean,
|
||||
): number => {
|
||||
if (offset === LayerOffset.TOP) {
|
||||
return isRelative ? 0 : brothersLength;
|
||||
}
|
||||
if (offset === LayerOffset.BOTTOM) {
|
||||
return isRelative ? brothersLength : 0;
|
||||
}
|
||||
return currentIndex + (isRelative ? -(offset as number) : (offset as number));
|
||||
};
|
||||
|
||||
/**
|
||||
* 节点配置合并策略:用于 mergeWith 的自定义回调
|
||||
* - undefined 且 source 拥有该 key 时返回空字符串
|
||||
* - 原来是数组而新值是对象时,使用新值
|
||||
* - 新值是数组时,直接替换而非逐元素合并
|
||||
*/
|
||||
export const editorNodeMergeCustomizer = (objValue: any, srcValue: any, key: string, _object: any, source: any) => {
|
||||
if (typeof srcValue === 'undefined' && Object.hasOwn(source, key)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (isObject(srcValue) && Array.isArray(objValue)) {
|
||||
return srcValue;
|
||||
}
|
||||
if (Array.isArray(srcValue)) {
|
||||
return srcValue;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 收集复制节点关联的依赖节点,将关联节点追加到 copyNodes 数组中
|
||||
* @param copyNodes 待复制的节点列表(会被就地修改)
|
||||
* @param collectorOptions 依赖收集器配置
|
||||
* @param getNodeById 根据 ID 获取节点的回调函数
|
||||
*/
|
||||
export const collectRelatedNodes = (
|
||||
copyNodes: MNode[],
|
||||
collectorOptions: TargetOptions,
|
||||
getNodeById: (id: Id) => MNode | null,
|
||||
): void => {
|
||||
const customTarget = new Target({ ...collectorOptions });
|
||||
const coperWatcher = new Watcher();
|
||||
|
||||
coperWatcher.addTarget(customTarget);
|
||||
coperWatcher.collect(copyNodes, {}, true, collectorOptions.type);
|
||||
|
||||
Object.keys(customTarget.deps).forEach((nodeId: Id) => {
|
||||
const node = getNodeById(nodeId);
|
||||
if (!node) return;
|
||||
customTarget.deps[nodeId].keys.forEach((key) => {
|
||||
const relateNodeId = get(node, key);
|
||||
const isExist = copyNodes.find((n) => n.id === relateNodeId);
|
||||
if (!isExist) {
|
||||
const relateNode = getNodeById(relateNodeId);
|
||||
if (relateNode) {
|
||||
copyNodes.push(relateNode);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
export interface DragClassification {
|
||||
sameParentIndices: number[];
|
||||
crossParentConfigs: { config: MNode; parent: MContainer }[];
|
||||
/** 当同父容器节点索引异常时置为 true,调用方应中止拖拽操作 */
|
||||
aborted: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 对拖拽的节点进行分类:同父容器内移动 vs 跨容器移动
|
||||
* @param configs 被拖拽的节点列表
|
||||
* @param targetParent 目标父容器
|
||||
* @param getNodeInfo 获取节点信息的回调
|
||||
* @returns 分类结果,包含同容器索引列表和跨容器节点列表
|
||||
*/
|
||||
export const classifyDragSources = (
|
||||
configs: MNode[],
|
||||
targetParent: MContainer,
|
||||
getNodeInfo: (id: Id, raw?: boolean) => EditorNodeInfo,
|
||||
): DragClassification => {
|
||||
const sameParentIndices: number[] = [];
|
||||
const crossParentConfigs: { config: MNode; parent: MContainer }[] = [];
|
||||
|
||||
for (const config of configs) {
|
||||
const { parent, node: curNode } = getNodeInfo(config.id, false);
|
||||
if (!parent || !curNode) continue;
|
||||
|
||||
const path = getNodePath(curNode.id, parent.items);
|
||||
if (path.some((node) => `${targetParent.id}` === `${node.id}`)) continue;
|
||||
|
||||
const index = getNodeIndex(curNode.id, parent);
|
||||
|
||||
if (`${parent.id}` === `${targetParent.id}`) {
|
||||
if (typeof index !== 'number' || index === -1) {
|
||||
return { sameParentIndices, crossParentConfigs, aborted: true };
|
||||
}
|
||||
sameParentIndices.push(index);
|
||||
} else {
|
||||
crossParentConfigs.push({ config, parent });
|
||||
}
|
||||
}
|
||||
|
||||
return { sameParentIndices, crossParentConfigs, aborted: false };
|
||||
};
|
||||
|
||||
@ -24,7 +24,7 @@ import {
|
||||
NODE_DISABLE_DATA_SOURCE_KEY,
|
||||
} from '@tmagic/core';
|
||||
import { tMagicMessage } from '@tmagic/design';
|
||||
import type { FormConfig, FormState, TabConfig, TabPaneConfig } from '@tmagic/form';
|
||||
import type { ChildConfig, DisplayCondsConfig, FormConfig, TabConfig, TabPaneConfig } from '@tmagic/form';
|
||||
|
||||
export const arrayOptions = [
|
||||
{ text: '包含', value: 'include' },
|
||||
@ -107,6 +107,10 @@ export const styleTabConfig: TabPaneConfig = {
|
||||
'borderStyle',
|
||||
'borderColor',
|
||||
],
|
||||
} as unknown as ChildConfig,
|
||||
{
|
||||
name: 'transform',
|
||||
defaultValue: () => ({}),
|
||||
},
|
||||
],
|
||||
},
|
||||
@ -168,9 +172,9 @@ export const advancedTabConfig: TabPaneConfig = {
|
||||
],
|
||||
};
|
||||
|
||||
export const displayTabConfig: TabPaneConfig = {
|
||||
export const displayTabConfig: TabPaneConfig<DisplayCondsConfig> = {
|
||||
title: '显示条件',
|
||||
display: (_state: FormState, { model }: any) => model.type !== 'page',
|
||||
display: (_state, { model }) => model.type !== 'page',
|
||||
items: [
|
||||
{
|
||||
name: NODE_CONDS_RESULT_KEY,
|
||||
@ -209,7 +213,7 @@ export const fillConfig = (
|
||||
const propsConfig: FormConfig = [];
|
||||
|
||||
// 组件类型,必须要有
|
||||
if (!config.find((item) => item.name === 'type')) {
|
||||
if (!config.find((item) => 'name' in item && item.name === 'type')) {
|
||||
propsConfig.push({
|
||||
text: 'type',
|
||||
name: 'type',
|
||||
@ -217,7 +221,7 @@ export const fillConfig = (
|
||||
});
|
||||
}
|
||||
|
||||
if (!config.find((item) => item.name === 'id')) {
|
||||
if (!config.find((item) => 'name' in item && item.name === 'id')) {
|
||||
// 组件id,必须要有
|
||||
propsConfig.push({
|
||||
name: 'id',
|
||||
@ -241,14 +245,16 @@ export const fillConfig = (
|
||||
});
|
||||
}
|
||||
|
||||
if (!config.find((item) => item.name === 'name')) {
|
||||
if (!config.find((item) => 'name' in item && item.name === 'name')) {
|
||||
propsConfig.push({
|
||||
name: 'name',
|
||||
text: '组件名称',
|
||||
});
|
||||
}
|
||||
|
||||
const noCodeAdvancedTabItems = advancedTabConfig.items.filter((item) => item.type !== 'code-select');
|
||||
const noCodeAdvancedTabItems = advancedTabConfig.items.filter(
|
||||
(item) => 'type' in item && item.type !== 'code-select',
|
||||
);
|
||||
|
||||
if (noCodeAdvancedTabItems.length > 0 && disabledCodeBlock) {
|
||||
advancedTabConfig.items = noCodeAdvancedTabItems;
|
||||
|
||||
@ -23,7 +23,7 @@ export class UndoRedo<T = any> {
|
||||
private listCursor: number;
|
||||
private listMaxSize: number;
|
||||
|
||||
constructor(listMaxSize = 20) {
|
||||
constructor(listMaxSize = 100) {
|
||||
const minListMaxSize = 2;
|
||||
this.elementList = [];
|
||||
this.listCursor = 0;
|
||||
@ -42,29 +42,30 @@ export class UndoRedo<T = any> {
|
||||
}
|
||||
|
||||
public canUndo(): boolean {
|
||||
return this.listCursor > 1;
|
||||
return this.listCursor > 0;
|
||||
}
|
||||
|
||||
// 返回undo后的当前元素
|
||||
/** 返回被撤销的操作 */
|
||||
public undo(): T | null {
|
||||
if (!this.canUndo()) {
|
||||
return null;
|
||||
}
|
||||
this.listCursor -= 1;
|
||||
return this.getCurrentElement();
|
||||
return cloneDeep(this.elementList[this.listCursor]);
|
||||
}
|
||||
|
||||
public canRedo() {
|
||||
return this.elementList.length > this.listCursor;
|
||||
}
|
||||
|
||||
// 返回redo后的当前元素
|
||||
/** 返回被重做的操作 */
|
||||
public redo(): T | null {
|
||||
if (!this.canRedo()) {
|
||||
return null;
|
||||
}
|
||||
const element = cloneDeep(this.elementList[this.listCursor]);
|
||||
this.listCursor += 1;
|
||||
return this.getCurrentElement();
|
||||
return element;
|
||||
}
|
||||
|
||||
public getCurrentElement(): T | null {
|
||||
|
||||
245
packages/editor/tests/unit/utils/editor-history.spec.ts
Normal file
245
packages/editor/tests/unit/utils/editor-history.spec.ts
Normal file
@ -0,0 +1,245 @@
|
||||
/*
|
||||
* Tencent is pleased to support the open source community by making TMagicEditor available.
|
||||
*
|
||||
* Copyright (C) 2025 Tencent. All rights reserved.
|
||||
*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { MApp, MContainer, MNode } from '@tmagic/core';
|
||||
import { NodeType } from '@tmagic/core';
|
||||
|
||||
import type { StepValue } from '@editor/type';
|
||||
import type { HistoryOpContext } from '@editor/utils/editor-history';
|
||||
import { applyHistoryAddOp, applyHistoryRemoveOp, applyHistoryUpdateOp } from '@editor/utils/editor-history';
|
||||
|
||||
const makePage = (): MContainer => ({
|
||||
id: 'page_1',
|
||||
type: NodeType.PAGE,
|
||||
items: [
|
||||
{ id: 'n1', type: 'text' },
|
||||
{ id: 'n2', type: 'button' },
|
||||
],
|
||||
});
|
||||
|
||||
const makeRoot = (page: MContainer): MApp => ({
|
||||
id: 'app_1',
|
||||
type: NodeType.ROOT,
|
||||
items: [page],
|
||||
});
|
||||
|
||||
const makeCtx = (root: MApp): HistoryOpContext => {
|
||||
const page = root.items[0] as MContainer;
|
||||
return {
|
||||
root,
|
||||
stage: {
|
||||
add: vi.fn(),
|
||||
remove: vi.fn(),
|
||||
update: vi.fn(),
|
||||
} as any,
|
||||
getNodeById: (id: any) => {
|
||||
if (`${id}` === `${root.id}`) return root as unknown as MNode;
|
||||
if (`${id}` === `${page.id}`) return page as unknown as MNode;
|
||||
return page.items.find((n) => `${n.id}` === `${id}`) ?? null;
|
||||
},
|
||||
getNodeInfo: (id: any) => {
|
||||
if (`${id}` === `${page.id}`) {
|
||||
return { node: page as unknown as MNode, parent: root as unknown as MContainer, page: page as any };
|
||||
}
|
||||
const node = page.items.find((n) => `${n.id}` === `${id}`);
|
||||
return { node: node ?? null, parent: node ? page : null, page: page as any };
|
||||
},
|
||||
setRoot: vi.fn(),
|
||||
setPage: vi.fn(),
|
||||
getPage: () => page as any,
|
||||
};
|
||||
};
|
||||
|
||||
describe('applyHistoryAddOp', () => {
|
||||
test('撤销 add:从父节点移除已添加的节点', async () => {
|
||||
const page = makePage();
|
||||
const root = makeRoot(page);
|
||||
const ctx = makeCtx(root);
|
||||
|
||||
const step: StepValue = {
|
||||
opType: 'add',
|
||||
selectedBefore: [],
|
||||
selectedAfter: ['n1'],
|
||||
modifiedNodeIds: new Map(),
|
||||
nodes: [{ id: 'n1', type: 'text' }],
|
||||
parentId: 'page_1',
|
||||
};
|
||||
|
||||
expect(page.items).toHaveLength(2);
|
||||
await applyHistoryAddOp(step, true, ctx);
|
||||
expect(page.items).toHaveLength(1);
|
||||
expect(page.items[0].id).toBe('n2');
|
||||
expect(ctx.stage!.remove).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('重做 add:重新添加节点到父节点', async () => {
|
||||
const page: MContainer = { id: 'page_1', type: NodeType.PAGE, items: [] };
|
||||
const root = makeRoot(page);
|
||||
const ctx = makeCtx(root);
|
||||
|
||||
const step: StepValue = {
|
||||
opType: 'add',
|
||||
selectedBefore: [],
|
||||
selectedAfter: ['new1'],
|
||||
modifiedNodeIds: new Map(),
|
||||
nodes: [{ id: 'new1', type: 'text' }],
|
||||
parentId: 'page_1',
|
||||
indexMap: { new1: 0 },
|
||||
};
|
||||
|
||||
await applyHistoryAddOp(step, false, ctx);
|
||||
expect(page.items).toHaveLength(1);
|
||||
expect(page.items[0].id).toBe('new1');
|
||||
expect(ctx.stage!.add).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyHistoryRemoveOp', () => {
|
||||
test('撤销 remove:将已删除节点按原位置重新插入', async () => {
|
||||
const page: MContainer = { id: 'page_1', type: NodeType.PAGE, items: [{ id: 'n2', type: 'button' }] };
|
||||
const root = makeRoot(page);
|
||||
const ctx = makeCtx(root);
|
||||
|
||||
const step: StepValue = {
|
||||
opType: 'remove',
|
||||
selectedBefore: ['n1'],
|
||||
selectedAfter: [],
|
||||
modifiedNodeIds: new Map(),
|
||||
removedItems: [{ node: { id: 'n1', type: 'text' }, parentId: 'page_1', index: 0 }],
|
||||
};
|
||||
|
||||
await applyHistoryRemoveOp(step, true, ctx);
|
||||
expect(page.items).toHaveLength(2);
|
||||
expect(page.items[0].id).toBe('n1');
|
||||
expect(ctx.stage!.add).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('重做 remove:再次删除节点', async () => {
|
||||
const page = makePage();
|
||||
const root = makeRoot(page);
|
||||
const ctx = makeCtx(root);
|
||||
|
||||
const step: StepValue = {
|
||||
opType: 'remove',
|
||||
selectedBefore: [],
|
||||
selectedAfter: [],
|
||||
modifiedNodeIds: new Map(),
|
||||
removedItems: [{ node: { id: 'n1', type: 'text' }, parentId: 'page_1', index: 0 }],
|
||||
};
|
||||
|
||||
expect(page.items).toHaveLength(2);
|
||||
await applyHistoryRemoveOp(step, false, ctx);
|
||||
expect(page.items).toHaveLength(1);
|
||||
expect(page.items[0].id).toBe('n2');
|
||||
expect(ctx.stage!.remove).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyHistoryUpdateOp', () => {
|
||||
test('撤销 update:将节点恢复为 oldNode', async () => {
|
||||
const page = makePage();
|
||||
const root = makeRoot(page);
|
||||
const ctx = makeCtx(root);
|
||||
|
||||
const step: StepValue = {
|
||||
opType: 'update',
|
||||
selectedBefore: [],
|
||||
selectedAfter: [],
|
||||
modifiedNodeIds: new Map(),
|
||||
updatedItems: [
|
||||
{
|
||||
oldNode: { id: 'n1', type: 'text', text: 'before' },
|
||||
newNode: { id: 'n1', type: 'text', text: 'after' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await applyHistoryUpdateOp(step, true, ctx);
|
||||
expect(page.items[0].text).toBe('before');
|
||||
expect(ctx.stage!.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('重做 update:将节点更新为 newNode', async () => {
|
||||
const page = makePage();
|
||||
const root = makeRoot(page);
|
||||
const ctx = makeCtx(root);
|
||||
|
||||
const step: StepValue = {
|
||||
opType: 'update',
|
||||
selectedBefore: [],
|
||||
selectedAfter: [],
|
||||
modifiedNodeIds: new Map(),
|
||||
updatedItems: [
|
||||
{
|
||||
oldNode: { id: 'n1', type: 'text', text: 'before' },
|
||||
newNode: { id: 'n1', type: 'text', text: 'after' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await applyHistoryUpdateOp(step, false, ctx);
|
||||
expect(page.items[0].text).toBe('after');
|
||||
});
|
||||
|
||||
test('update ROOT 类型调用 setRoot', async () => {
|
||||
const page = makePage();
|
||||
const root = makeRoot(page);
|
||||
const ctx = makeCtx(root);
|
||||
|
||||
const step: StepValue = {
|
||||
opType: 'update',
|
||||
selectedBefore: [],
|
||||
selectedAfter: [],
|
||||
modifiedNodeIds: new Map(),
|
||||
updatedItems: [
|
||||
{
|
||||
oldNode: { id: 'app_1', type: NodeType.ROOT, items: [] } as any,
|
||||
newNode: { id: 'app_1', type: NodeType.ROOT, items: [page] } as any,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await applyHistoryUpdateOp(step, true, ctx);
|
||||
expect(ctx.setRoot).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('update 页面节点调用 setPage', async () => {
|
||||
const page = makePage();
|
||||
const root = makeRoot(page);
|
||||
const ctx = makeCtx(root);
|
||||
|
||||
const updatedPage = { ...page, name: 'renamed' };
|
||||
const step: StepValue = {
|
||||
opType: 'update',
|
||||
selectedBefore: [],
|
||||
selectedAfter: [],
|
||||
modifiedNodeIds: new Map(),
|
||||
updatedItems: [
|
||||
{
|
||||
oldNode: page as any,
|
||||
newNode: updatedPage as any,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await applyHistoryUpdateOp(step, false, ctx);
|
||||
expect(ctx.setPage).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@ -17,8 +17,11 @@
|
||||
*/
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import type { MApp, MContainer, MNode } from '@tmagic/core';
|
||||
import { NodeType } from '@tmagic/core';
|
||||
|
||||
import type { EditorNodeInfo } from '@editor/type';
|
||||
import { LayerOffset, Layout } from '@editor/type';
|
||||
import * as editor from '@editor/utils/editor';
|
||||
|
||||
describe('util form', () => {
|
||||
@ -305,3 +308,452 @@ describe('buildChangeRecords', () => {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
// ===== 以下为新提取的工具函数测试 =====
|
||||
|
||||
const mockRoot: MApp = {
|
||||
id: 'app_1',
|
||||
type: NodeType.ROOT,
|
||||
items: [
|
||||
{
|
||||
id: 'page_1',
|
||||
type: NodeType.PAGE,
|
||||
name: 'index',
|
||||
style: { position: 'relative', width: 375 },
|
||||
items: [
|
||||
{
|
||||
id: 'node_1',
|
||||
type: 'text',
|
||||
style: { position: 'absolute', top: 10, left: 20, width: 100 },
|
||||
},
|
||||
{
|
||||
id: 'node_2',
|
||||
type: 'button',
|
||||
style: { position: 'absolute', bottom: 50, right: 30 },
|
||||
},
|
||||
{
|
||||
id: 'node_3',
|
||||
type: 'image',
|
||||
style: { position: 'relative', top: 0, left: 0 },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const mockGetNodeInfo = (id: string | number): EditorNodeInfo => {
|
||||
const page = mockRoot.items[0];
|
||||
if (`${id}` === `${mockRoot.id}`) {
|
||||
return { node: mockRoot as unknown as MNode, parent: null, page: null };
|
||||
}
|
||||
if (`${id}` === `${page.id}`) {
|
||||
return { node: page, parent: mockRoot as unknown as MContainer, page: page as any };
|
||||
}
|
||||
const items = (page as MContainer).items || [];
|
||||
const node = items.find((n: MNode) => `${n.id}` === `${id}`);
|
||||
if (node) {
|
||||
return { node, parent: page as MContainer, page: page as any };
|
||||
}
|
||||
return { node: null, parent: null, page: null };
|
||||
};
|
||||
|
||||
describe('resolveSelectedNode', () => {
|
||||
test('传入数字ID,正常返回节点信息', () => {
|
||||
const result = editor.resolveSelectedNode('node_1', mockGetNodeInfo, mockRoot.id);
|
||||
expect(result.node?.id).toBe('node_1');
|
||||
expect(result.parent?.id).toBe('page_1');
|
||||
expect(result.page?.id).toBe('page_1');
|
||||
});
|
||||
|
||||
test('传入节点配置对象,正常返回节点信息', () => {
|
||||
const config: MNode = { id: 'node_2', type: 'button' };
|
||||
const result = editor.resolveSelectedNode(config, mockGetNodeInfo, mockRoot.id);
|
||||
expect(result.node?.id).toBe('node_2');
|
||||
});
|
||||
|
||||
test('传入页面ID,正常返回页面信息', () => {
|
||||
const result = editor.resolveSelectedNode('page_1', mockGetNodeInfo, mockRoot.id);
|
||||
expect(result.node?.id).toBe('page_1');
|
||||
});
|
||||
|
||||
test('传入空ID,抛出错误', () => {
|
||||
expect(() => editor.resolveSelectedNode({ id: '', type: 'text' }, mockGetNodeInfo)).toThrow('没有ID,无法选中');
|
||||
});
|
||||
|
||||
test('传入不存在的ID,抛出错误', () => {
|
||||
expect(() => editor.resolveSelectedNode('not_exist', mockGetNodeInfo)).toThrow('获取不到组件信息');
|
||||
});
|
||||
|
||||
test('传入根节点ID,抛出错误', () => {
|
||||
expect(() => editor.resolveSelectedNode('app_1', mockGetNodeInfo, mockRoot.id)).toThrow('不能选根节点');
|
||||
});
|
||||
|
||||
test('不传rootId时,不校验根节点', () => {
|
||||
const result = editor.resolveSelectedNode('app_1', mockGetNodeInfo);
|
||||
expect(result.node?.id).toBe('app_1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleFixedPosition', () => {
|
||||
const getLayoutFn = async () => Layout.ABSOLUTE;
|
||||
|
||||
test('非fixed变为fixed,调用change2Fixed', async () => {
|
||||
const src: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 10, left: 20 } };
|
||||
const dist: MNode = { id: 'node_1', type: 'text', style: { position: 'fixed', top: 10, left: 20 } };
|
||||
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
|
||||
expect(result.style?.position).toBe('fixed');
|
||||
expect(result).not.toBe(dist);
|
||||
});
|
||||
|
||||
test('fixed变为非fixed,调用Fixed2Other', async () => {
|
||||
const src: MNode = { id: 'node_1', type: 'text', style: { position: 'fixed', top: 10, left: 20 } };
|
||||
const dist: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 10, left: 20 } };
|
||||
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
|
||||
expect(result.style?.position).toBe('absolute');
|
||||
});
|
||||
|
||||
test('定位未变化,不修改样式', async () => {
|
||||
const src: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 10, left: 20 } };
|
||||
const dist: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 30, left: 40 } };
|
||||
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
|
||||
expect(result.style?.top).toBe(30);
|
||||
expect(result.style?.left).toBe(40);
|
||||
});
|
||||
|
||||
test('pop类型节点不做处理', async () => {
|
||||
const src: MNode = {
|
||||
id: 'node_1',
|
||||
type: 'pop',
|
||||
style: { position: 'absolute', top: 10, left: 20 },
|
||||
name: 'pop',
|
||||
};
|
||||
const dist: MNode = { id: 'node_1', type: 'pop', style: { position: 'fixed', top: 10, left: 20 }, name: 'pop' };
|
||||
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
|
||||
expect(result.style?.position).toBe('fixed');
|
||||
});
|
||||
|
||||
test('目标节点无position属性,不做处理', async () => {
|
||||
const src: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute' } };
|
||||
const dist: MNode = { id: 'node_1', type: 'text', style: { width: 100 } };
|
||||
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
|
||||
expect(result.style?.position).toBeUndefined();
|
||||
});
|
||||
|
||||
test('返回深拷贝,不修改原对象', async () => {
|
||||
const src: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 10 } };
|
||||
const dist: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 20 } };
|
||||
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
|
||||
expect(result).not.toBe(dist);
|
||||
expect(dist.style?.top).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calcMoveStyle', () => {
|
||||
test('absolute定位,向下移动', () => {
|
||||
const style = { position: 'absolute', top: 10, left: 20 };
|
||||
const result = editor.calcMoveStyle(style, 0, 5);
|
||||
expect(result).toEqual({ position: 'absolute', top: 15, left: 20, bottom: '' });
|
||||
});
|
||||
|
||||
test('absolute定位,向右移动', () => {
|
||||
const style = { position: 'absolute', top: 10, left: 20 };
|
||||
const result = editor.calcMoveStyle(style, 5, 0);
|
||||
expect(result).toEqual({ position: 'absolute', top: 10, left: 25, right: '' });
|
||||
});
|
||||
|
||||
test('absolute定位,同时向下和向右移动', () => {
|
||||
const style = { position: 'absolute', top: 10, left: 20 };
|
||||
const result = editor.calcMoveStyle(style, 3, 7);
|
||||
expect(result).toEqual({ position: 'absolute', top: 17, left: 23, bottom: '', right: '' });
|
||||
});
|
||||
|
||||
test('fixed定位,正常移动', () => {
|
||||
const style = { position: 'fixed', top: 100, left: 200 };
|
||||
const result = editor.calcMoveStyle(style, -10, -20);
|
||||
expect(result).toEqual({ position: 'fixed', top: 80, left: 190, bottom: '', right: '' });
|
||||
});
|
||||
|
||||
test('使用bottom定位时,向下移动减小bottom', () => {
|
||||
const style = { position: 'absolute', bottom: 50, left: 20 };
|
||||
const result = editor.calcMoveStyle(style, 0, 10);
|
||||
expect(result?.bottom).toBe(40);
|
||||
expect(result?.top).toBe('');
|
||||
});
|
||||
|
||||
test('使用right定位时,向右移动减小right', () => {
|
||||
const style = { position: 'absolute', top: 10, right: 30 };
|
||||
const result = editor.calcMoveStyle(style, 10, 0);
|
||||
expect(result?.right).toBe(20);
|
||||
expect(result?.left).toBe('');
|
||||
});
|
||||
|
||||
test('relative定位,返回null', () => {
|
||||
const style = { position: 'relative', top: 0, left: 0 };
|
||||
const result = editor.calcMoveStyle(style, 10, 10);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('无position属性,返回null', () => {
|
||||
const style = { width: 100 };
|
||||
const result = editor.calcMoveStyle(style, 10, 10);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('空样式对象,返回null', () => {
|
||||
const result = editor.calcMoveStyle({}, 10, 10);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('偏移量为0,不修改样式', () => {
|
||||
const style = { position: 'absolute', top: 10, left: 20 };
|
||||
const result = editor.calcMoveStyle(style, 0, 0);
|
||||
expect(result).toEqual({ position: 'absolute', top: 10, left: 20 });
|
||||
});
|
||||
|
||||
test('不修改原对象', () => {
|
||||
const style = { position: 'absolute', top: 10, left: 20 };
|
||||
editor.calcMoveStyle(style, 5, 5);
|
||||
expect(style.top).toBe(10);
|
||||
expect(style.left).toBe(20);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calcAlignCenterStyle', () => {
|
||||
test('absolute布局,通过配置中的width计算居中', () => {
|
||||
const node: MNode = { id: 'n1', type: 'text', style: { width: 100, left: 0 } };
|
||||
const parent = { id: 'p1', type: NodeType.PAGE, style: { width: 375 }, items: [] } as unknown as MContainer;
|
||||
const result = editor.calcAlignCenterStyle(node, parent, Layout.ABSOLUTE);
|
||||
expect(result?.left).toBe(137.5);
|
||||
expect(result?.right).toBe('');
|
||||
});
|
||||
|
||||
test('relative布局,返回null', () => {
|
||||
const node: MNode = { id: 'n1', type: 'text', style: { width: 100 } };
|
||||
const parent = { id: 'p1', type: NodeType.PAGE, style: { width: 375 }, items: [] } as unknown as MContainer;
|
||||
const result = editor.calcAlignCenterStyle(node, parent, Layout.RELATIVE);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('节点无style,返回null', () => {
|
||||
const node: MNode = { id: 'n1', type: 'text' };
|
||||
const parent = { id: 'p1', type: NodeType.PAGE, style: { width: 375 }, items: [] } as unknown as MContainer;
|
||||
const result = editor.calcAlignCenterStyle(node, parent, Layout.ABSOLUTE);
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('父节点无style,不修改', () => {
|
||||
const node: MNode = { id: 'n1', type: 'text', style: { width: 100, left: 10 } };
|
||||
const parent = { id: 'p1', type: NodeType.PAGE, items: [] } as unknown as MContainer;
|
||||
const result = editor.calcAlignCenterStyle(node, parent, Layout.ABSOLUTE);
|
||||
expect(result?.left).toBe(10);
|
||||
});
|
||||
|
||||
test('父节点width非数字,不修改left', () => {
|
||||
const node: MNode = { id: 'n1', type: 'text', style: { width: 100, left: 10 } };
|
||||
const parent = {
|
||||
id: 'p1',
|
||||
type: NodeType.PAGE,
|
||||
style: { width: '100%' },
|
||||
items: [],
|
||||
} as unknown as MContainer;
|
||||
const result = editor.calcAlignCenterStyle(node, parent, Layout.ABSOLUTE);
|
||||
expect(result?.left).toBe(10);
|
||||
});
|
||||
|
||||
test('不修改原节点style', () => {
|
||||
const node: MNode = { id: 'n1', type: 'text', style: { width: 100, left: 0 } };
|
||||
const parent = { id: 'p1', type: NodeType.PAGE, style: { width: 375 }, items: [] } as unknown as MContainer;
|
||||
editor.calcAlignCenterStyle(node, parent, Layout.ABSOLUTE);
|
||||
expect(node.style?.left).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('calcLayerTargetIndex', () => {
|
||||
test('绝对定位,向上移动1层', () => {
|
||||
const result = editor.calcLayerTargetIndex(2, 1, 5, false);
|
||||
expect(result).toBe(3);
|
||||
});
|
||||
|
||||
test('绝对定位,向下移动1层', () => {
|
||||
const result = editor.calcLayerTargetIndex(2, -1, 5, false);
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
|
||||
test('流式布局,向上移动1层(索引减小)', () => {
|
||||
const result = editor.calcLayerTargetIndex(2, 1, 5, true);
|
||||
expect(result).toBe(1);
|
||||
});
|
||||
|
||||
test('流式布局,向下移动1层(索引增大)', () => {
|
||||
const result = editor.calcLayerTargetIndex(2, -1, 5, true);
|
||||
expect(result).toBe(3);
|
||||
});
|
||||
|
||||
test('绝对定位,置顶', () => {
|
||||
const result = editor.calcLayerTargetIndex(2, LayerOffset.TOP, 5, false);
|
||||
expect(result).toBe(5);
|
||||
});
|
||||
|
||||
test('绝对定位,置底', () => {
|
||||
const result = editor.calcLayerTargetIndex(2, LayerOffset.BOTTOM, 5, false);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
test('流式布局,置顶(索引最小)', () => {
|
||||
const result = editor.calcLayerTargetIndex(3, LayerOffset.TOP, 5, true);
|
||||
expect(result).toBe(0);
|
||||
});
|
||||
|
||||
test('流式布局,置底(索引最大)', () => {
|
||||
const result = editor.calcLayerTargetIndex(1, LayerOffset.BOTTOM, 5, true);
|
||||
expect(result).toBe(5);
|
||||
});
|
||||
|
||||
test('偏移量为0,索引不变', () => {
|
||||
const result = editor.calcLayerTargetIndex(2, 0, 5, false);
|
||||
expect(result).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('editorNodeMergeCustomizer', () => {
|
||||
test('undefined 且 source 拥有该 key 时返回空字符串', () => {
|
||||
const source = { name: undefined };
|
||||
const result = editor.editorNodeMergeCustomizer('old', undefined, 'name', {}, source);
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
test('source 不拥有该 key 时返回 undefined(使用默认合并)', () => {
|
||||
const result = editor.editorNodeMergeCustomizer('old', undefined, 'name', {}, {});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
|
||||
test('原来是数组,新值是对象,使用新值', () => {
|
||||
const srcValue = { a: 1 };
|
||||
const result = editor.editorNodeMergeCustomizer([1, 2], srcValue, 'key', {}, {});
|
||||
expect(result).toBe(srcValue);
|
||||
});
|
||||
|
||||
test('新值是数组,直接替换', () => {
|
||||
const srcValue = [3, 4];
|
||||
const result = editor.editorNodeMergeCustomizer([1, 2], srcValue, 'key', {}, {});
|
||||
expect(result).toBe(srcValue);
|
||||
});
|
||||
|
||||
test('都是普通值,返回 undefined(使用默认合并)', () => {
|
||||
const result = editor.editorNodeMergeCustomizer('old', 'new', 'key', {}, {});
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('classifyDragSources', () => {
|
||||
const makeTree = (): { root: MApp; getNodeInfo: (id: any, raw?: boolean) => EditorNodeInfo } => {
|
||||
const child1: MNode = { id: 'c1', type: 'text' };
|
||||
const child2: MNode = { id: 'c2', type: 'text' };
|
||||
const child3: MNode = { id: 'c3', type: 'text' };
|
||||
const container1: MContainer = {
|
||||
id: 'cont1',
|
||||
type: NodeType.CONTAINER,
|
||||
items: [child1, child2],
|
||||
};
|
||||
const container2: MContainer = {
|
||||
id: 'cont2',
|
||||
type: NodeType.CONTAINER,
|
||||
items: [child3],
|
||||
};
|
||||
const page: any = {
|
||||
id: 'page_1',
|
||||
type: NodeType.PAGE,
|
||||
items: [container1, container2],
|
||||
};
|
||||
const root: MApp = { id: 'app', type: NodeType.ROOT, items: [page] };
|
||||
|
||||
const getNodeInfo = (id: any): EditorNodeInfo => {
|
||||
if (`${id}` === 'c1' || `${id}` === 'c2') {
|
||||
return {
|
||||
node: container1.items.find((n) => `${n.id}` === `${id}`) ?? null,
|
||||
parent: container1,
|
||||
page,
|
||||
};
|
||||
}
|
||||
if (`${id}` === 'c3') {
|
||||
return { node: child3, parent: container2, page };
|
||||
}
|
||||
if (`${id}` === 'cont1') {
|
||||
return { node: container1, parent: page, page };
|
||||
}
|
||||
if (`${id}` === 'cont2') {
|
||||
return { node: container2, parent: page, page };
|
||||
}
|
||||
return { node: null, parent: null, page: null };
|
||||
};
|
||||
|
||||
return { root, getNodeInfo };
|
||||
};
|
||||
|
||||
test('同父容器内拖拽,返回 sameParentIndices', () => {
|
||||
const { getNodeInfo } = makeTree();
|
||||
const targetParent = getNodeInfo('cont1').node as MContainer;
|
||||
const result = editor.classifyDragSources([{ id: 'c1', type: 'text' }], targetParent, getNodeInfo);
|
||||
expect(result.aborted).toBe(false);
|
||||
expect(result.sameParentIndices).toEqual([0]);
|
||||
expect(result.crossParentConfigs).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('跨容器拖拽,返回 crossParentConfigs', () => {
|
||||
const { getNodeInfo } = makeTree();
|
||||
const targetParent = getNodeInfo('cont1').node as MContainer;
|
||||
const result = editor.classifyDragSources([{ id: 'c3', type: 'text' }], targetParent, getNodeInfo);
|
||||
expect(result.aborted).toBe(false);
|
||||
expect(result.sameParentIndices).toHaveLength(0);
|
||||
expect(result.crossParentConfigs).toHaveLength(1);
|
||||
expect(result.crossParentConfigs[0].config.id).toBe('c3');
|
||||
});
|
||||
|
||||
test('混合拖拽:同容器+跨容器', () => {
|
||||
const { getNodeInfo } = makeTree();
|
||||
const targetParent = getNodeInfo('cont1').node as MContainer;
|
||||
const result = editor.classifyDragSources(
|
||||
[
|
||||
{ id: 'c1', type: 'text' },
|
||||
{ id: 'c3', type: 'text' },
|
||||
],
|
||||
targetParent,
|
||||
getNodeInfo,
|
||||
);
|
||||
expect(result.aborted).toBe(false);
|
||||
expect(result.sameParentIndices).toEqual([0]);
|
||||
expect(result.crossParentConfigs).toHaveLength(1);
|
||||
});
|
||||
|
||||
test('节点不存在时跳过', () => {
|
||||
const { getNodeInfo } = makeTree();
|
||||
const targetParent = getNodeInfo('cont1').node as MContainer;
|
||||
const result = editor.classifyDragSources([{ id: 'nonexistent', type: 'text' }], targetParent, getNodeInfo);
|
||||
expect(result.aborted).toBe(false);
|
||||
expect(result.sameParentIndices).toHaveLength(0);
|
||||
expect(result.crossParentConfigs).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('目标容器在节点路径上时跳过(防止循环嵌套)', () => {
|
||||
const { getNodeInfo } = makeTree();
|
||||
const targetParent = getNodeInfo('cont1').node as MContainer;
|
||||
const result = editor.classifyDragSources([{ id: 'c1', type: 'text' }], targetParent, (id: any) => {
|
||||
if (`${id}` === 'c1') {
|
||||
return {
|
||||
node: { id: 'c1', type: 'text' },
|
||||
parent: targetParent,
|
||||
page: { id: 'page_1', type: NodeType.PAGE, items: [] } as any,
|
||||
};
|
||||
}
|
||||
return { node: null, parent: null, page: null };
|
||||
});
|
||||
expect(result.sameParentIndices).toEqual([0]);
|
||||
expect(result.crossParentConfigs).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
@ -21,60 +21,66 @@ import { UndoRedo } from '@editor/utils/undo-redo';
|
||||
|
||||
describe('undo', () => {
|
||||
let undoRedo: UndoRedo;
|
||||
const element = { a: 1 };
|
||||
|
||||
beforeEach(() => {
|
||||
undoRedo = new UndoRedo();
|
||||
undoRedo.pushElement(element);
|
||||
});
|
||||
|
||||
test('can no undo: empty list', () => {
|
||||
test('can not undo: empty list', () => {
|
||||
expect(undoRedo.canUndo()).toBe(false);
|
||||
expect(undoRedo.undo()).toEqual(null);
|
||||
});
|
||||
|
||||
test('can undo', () => {
|
||||
test('can undo after one push', () => {
|
||||
undoRedo.pushElement({ a: 1 });
|
||||
expect(undoRedo.canUndo()).toBe(true);
|
||||
expect(undoRedo.undo()).toEqual({ a: 1 });
|
||||
expect(undoRedo.canUndo()).toBe(false);
|
||||
});
|
||||
|
||||
test('can undo returns the operation being undone', () => {
|
||||
undoRedo.pushElement({ a: 1 });
|
||||
undoRedo.pushElement({ a: 2 });
|
||||
expect(undoRedo.canUndo()).toBe(true);
|
||||
expect(undoRedo.undo()).toEqual(element);
|
||||
expect(undoRedo.undo()).toEqual({ a: 2 });
|
||||
expect(undoRedo.canUndo()).toBe(true);
|
||||
expect(undoRedo.undo()).toEqual({ a: 1 });
|
||||
expect(undoRedo.canUndo()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('redo', () => {
|
||||
let undoRedo: UndoRedo;
|
||||
const element = { a: 1 };
|
||||
|
||||
beforeEach(() => {
|
||||
undoRedo = new UndoRedo();
|
||||
undoRedo.pushElement(element);
|
||||
});
|
||||
|
||||
test('can no redo: empty list', () => {
|
||||
test('can not redo: empty list', () => {
|
||||
expect(undoRedo.canRedo()).toBe(false);
|
||||
expect(undoRedo.redo()).toBe(null);
|
||||
});
|
||||
|
||||
test('can no redo: no undo', () => {
|
||||
test('can not redo: no undo', () => {
|
||||
for (let i = 0; i < 5; i++) {
|
||||
undoRedo.pushElement(element);
|
||||
undoRedo.pushElement({ a: i });
|
||||
expect(undoRedo.canRedo()).toBe(false);
|
||||
expect(undoRedo.redo()).toBe(null);
|
||||
}
|
||||
});
|
||||
|
||||
test('can no redo: undo and push', () => {
|
||||
undoRedo.pushElement(element);
|
||||
test('can not redo: undo and push', () => {
|
||||
undoRedo.pushElement({ a: 1 });
|
||||
undoRedo.pushElement({ a: 2 });
|
||||
undoRedo.undo();
|
||||
undoRedo.pushElement(element);
|
||||
undoRedo.pushElement({ a: 3 });
|
||||
expect(undoRedo.canRedo()).toBe(false);
|
||||
expect(undoRedo.redo()).toEqual(null);
|
||||
});
|
||||
|
||||
test('can no redo: redo end', () => {
|
||||
const element1 = { a: 1 };
|
||||
const element2 = { a: 2 };
|
||||
undoRedo.pushElement(element1);
|
||||
undoRedo.pushElement(element2);
|
||||
test('can not redo: redo end', () => {
|
||||
undoRedo.pushElement({ a: 1 });
|
||||
undoRedo.pushElement({ a: 2 });
|
||||
undoRedo.undo();
|
||||
undoRedo.undo();
|
||||
undoRedo.redo();
|
||||
@ -85,23 +91,20 @@ describe('redo', () => {
|
||||
});
|
||||
|
||||
test('can redo', () => {
|
||||
const element1 = { a: 1 };
|
||||
const element2 = { a: 2 };
|
||||
undoRedo.pushElement(element1);
|
||||
undoRedo.pushElement(element2);
|
||||
undoRedo.pushElement({ a: 1 });
|
||||
undoRedo.pushElement({ a: 2 });
|
||||
undoRedo.undo();
|
||||
undoRedo.undo();
|
||||
|
||||
expect(undoRedo.canRedo()).toBe(true);
|
||||
expect(undoRedo.redo()).toEqual(element1);
|
||||
expect(undoRedo.redo()).toEqual({ a: 1 });
|
||||
expect(undoRedo.canRedo()).toBe(true);
|
||||
expect(undoRedo.redo()).toEqual(element2);
|
||||
expect(undoRedo.redo()).toEqual({ a: 2 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('get current element', () => {
|
||||
let undoRedo: UndoRedo;
|
||||
const element = { a: 1 };
|
||||
|
||||
beforeEach(() => {
|
||||
undoRedo = new UndoRedo();
|
||||
@ -112,44 +115,38 @@ describe('get current element', () => {
|
||||
});
|
||||
|
||||
test('has element', () => {
|
||||
undoRedo.pushElement(element);
|
||||
expect(undoRedo.getCurrentElement()).toEqual(element);
|
||||
undoRedo.pushElement({ a: 1 });
|
||||
expect(undoRedo.getCurrentElement()).toEqual({ a: 1 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('list max size', () => {
|
||||
let undoRedo: UndoRedo;
|
||||
const listMaxSize = 100;
|
||||
const element = { a: 1 };
|
||||
|
||||
beforeEach(() => {
|
||||
undoRedo = new UndoRedo(listMaxSize);
|
||||
undoRedo.pushElement(element);
|
||||
});
|
||||
|
||||
test('reach max size', () => {
|
||||
for (let i = 0; i < listMaxSize; i++) {
|
||||
for (let i = 0; i <= listMaxSize; i++) {
|
||||
undoRedo.pushElement({ a: i });
|
||||
}
|
||||
undoRedo.pushElement({ a: listMaxSize }); // 这个元素使得list达到maxSize,触发数据删除
|
||||
|
||||
expect(undoRedo.getCurrentElement()).toEqual({ a: listMaxSize });
|
||||
expect(undoRedo.canRedo()).toBe(false);
|
||||
expect(undoRedo.canUndo()).toBe(true);
|
||||
});
|
||||
|
||||
test('reach max size, then undo', () => {
|
||||
for (let i = 0; i < listMaxSize + 1; i++) {
|
||||
test('reach max size, then undo all', () => {
|
||||
for (let i = 0; i <= listMaxSize; i++) {
|
||||
undoRedo.pushElement({ a: i });
|
||||
}
|
||||
for (let i = 0; i < listMaxSize - 1; i++) {
|
||||
for (let i = 0; i < listMaxSize; i++) {
|
||||
undoRedo.undo();
|
||||
}
|
||||
const ele = undoRedo.getCurrentElement();
|
||||
undoRedo.undo();
|
||||
|
||||
expect(ele?.a).toBe(1); // 经过超过maxSize被删元素之后,原本a值为0的第一个元素已经被删除,现在第一个元素a值为1
|
||||
expect(undoRedo.canUndo()).toBe(false);
|
||||
expect(undoRedo.getCurrentElement()).toEqual(element);
|
||||
expect(undoRedo.getCurrentElement()).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/element-plus-adapter",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/form-schema",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -91,12 +91,10 @@ export interface FormItem {
|
||||
/** vnode的key值,默认是遍历数组时的index */
|
||||
__key?: string | number;
|
||||
/** 表单域标签的的宽度,例如 '50px'。支持 auto。 */
|
||||
labelWidth?: string;
|
||||
labelWidth?: string | number;
|
||||
/** label 标签的title属性 */
|
||||
labelTitle?: string;
|
||||
className?: string;
|
||||
/** 表单组件类型 */
|
||||
type?: string | TypeFunction;
|
||||
/** 字段名 */
|
||||
name?: string | number;
|
||||
/** 额外的提示信息,和 help 类似,当提示文案同时出现时,可以使用这个。 */
|
||||
@ -129,11 +127,16 @@ export interface FormItem {
|
||||
expand?: boolean;
|
||||
style?: Record<string, any>;
|
||||
fieldStyle?: Record<string, any>;
|
||||
labelPosition?: 'top' | 'left' | 'right';
|
||||
}
|
||||
|
||||
export interface DynamicTypeConfig extends FormItem {
|
||||
type: TypeFunction;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface ContainerCommonConfig {
|
||||
items: FormConfig;
|
||||
export interface ContainerCommonConfig<T = never> extends FormItem {
|
||||
items: FormConfig<T>;
|
||||
onInitValue?: (
|
||||
mForm: FormState | undefined,
|
||||
data: {
|
||||
@ -182,12 +185,12 @@ export interface Input {
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
export type TypeFunction = (
|
||||
export type TypeFunction<T extends string = string> = (
|
||||
mForm: FormState | undefined,
|
||||
data: {
|
||||
model: FormValue;
|
||||
},
|
||||
) => string;
|
||||
) => T;
|
||||
|
||||
export type FilterFunction<T = boolean> = (
|
||||
mForm: FormState | undefined,
|
||||
@ -208,6 +211,7 @@ export type FilterFunction<T = boolean> = (
|
||||
*/
|
||||
export interface SelectConfigOption {
|
||||
/** 选项的标签 */
|
||||
label?: string | SelectOptionTextFunction;
|
||||
text: string | SelectOptionTextFunction;
|
||||
/** 选项的值 */
|
||||
value: any | SelectOptionValueFunction;
|
||||
@ -344,13 +348,15 @@ export interface HtmlField extends FormItem {
|
||||
export interface DisplayConfig extends FormItem {
|
||||
type: 'display';
|
||||
initValue?: string | number | boolean;
|
||||
displayText: FilterFunction<string> | string;
|
||||
displayText?: FilterFunction<string> | string;
|
||||
}
|
||||
|
||||
/** 文本输入框 */
|
||||
export interface TextConfig extends FormItem, Input {
|
||||
type?: 'text';
|
||||
tooltip?: string;
|
||||
/** 是否可清空 */
|
||||
clearable?: boolean;
|
||||
prepend?: string;
|
||||
/** 后置元素,一般为标签或按钮 */
|
||||
append?:
|
||||
@ -436,6 +442,17 @@ export interface TimeConfig extends FormItem, Input {
|
||||
valueFormat?: 'HH:mm:ss' | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间范围选择器
|
||||
*/
|
||||
export interface TimerangeConfig extends FormItem {
|
||||
type: 'timerange';
|
||||
names?: string[];
|
||||
defaultTime?: Date[];
|
||||
format?: 'HH:mm:ss' | string;
|
||||
valueFormat?: 'HH:mm:ss' | string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 单个多选框
|
||||
*/
|
||||
@ -459,11 +476,11 @@ export interface SwitchConfig extends FormItem {
|
||||
* 单选框
|
||||
*/
|
||||
export interface RadioGroupConfig extends FormItem {
|
||||
type: 'radio-group';
|
||||
type: 'radio-group' | 'radioGroup';
|
||||
childType?: 'default' | 'button';
|
||||
options: {
|
||||
value: string | number | boolean;
|
||||
text: string;
|
||||
text?: string;
|
||||
icon?: any;
|
||||
tooltip?: string;
|
||||
}[];
|
||||
@ -486,7 +503,7 @@ export interface CheckboxGroupOption {
|
||||
* 多选框组
|
||||
*/
|
||||
export interface CheckboxGroupConfig extends FormItem {
|
||||
type: 'checkbox-group';
|
||||
type: 'checkbox-group' | 'checkboxGroup';
|
||||
options: CheckboxGroupOption[] | FilterFunction<CheckboxGroupOption[]>;
|
||||
}
|
||||
|
||||
@ -533,7 +550,7 @@ export interface SelectConfig extends FormItem, Input {
|
||||
/**
|
||||
* 链接
|
||||
*/
|
||||
export interface LinkConfig extends FormItem {
|
||||
export interface LinkConfig<T = never> extends FormItem {
|
||||
type: 'link';
|
||||
href?: string | ((model: Record<string, any>) => string);
|
||||
css?: {
|
||||
@ -553,7 +570,7 @@ export interface LinkConfig extends FormItem {
|
||||
) => string)
|
||||
| string;
|
||||
form:
|
||||
| FormConfig
|
||||
| FormConfig<T>
|
||||
| ((
|
||||
mForm: FormState | undefined,
|
||||
data: {
|
||||
@ -561,7 +578,7 @@ export interface LinkConfig extends FormItem {
|
||||
values?: Readonly<FormValue> | null;
|
||||
formValue?: FormValue;
|
||||
},
|
||||
) => FormConfig);
|
||||
) => FormConfig<T>);
|
||||
fullscreen?: boolean;
|
||||
}
|
||||
|
||||
@ -602,7 +619,7 @@ export interface CascaderConfig extends FormItem, Input {
|
||||
}
|
||||
|
||||
export interface DynamicFieldConfig extends FormItem {
|
||||
type: 'dynamic-field';
|
||||
type: 'dynamic-field' | 'dynamicField';
|
||||
returnFields: (
|
||||
config: DynamicFieldConfig,
|
||||
model: Record<any, any>,
|
||||
@ -618,32 +635,38 @@ export interface DynamicFieldConfig extends FormItem {
|
||||
/**
|
||||
* 分组容器
|
||||
*/
|
||||
export interface RowConfig extends FormItem {
|
||||
export interface RowConfig<T = never> extends FormItem {
|
||||
type: 'row';
|
||||
span: number;
|
||||
items: ({ span?: number } & (ChildConfig | EditorChildConfig))[];
|
||||
items: ({ span?: number } & (ChildConfig<T> | EditorChildConfig | T))[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 标签页容器
|
||||
*/
|
||||
export interface TabPaneConfig {
|
||||
export interface TabPaneConfig<T = never> {
|
||||
status?: string;
|
||||
/** 标签页名称,用于关联 model 中的数据 */
|
||||
name?: string | number;
|
||||
title: string;
|
||||
lazy?: boolean;
|
||||
labelWidth?: string;
|
||||
items: FormConfig;
|
||||
items: FormConfig<T>;
|
||||
display?: boolean | 'expand' | FilterFunction<boolean | 'expand'>;
|
||||
onTabClick?: (mForm: FormState | undefined, tab: any, data: any) => void;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export interface TabConfig extends FormItem, ContainerCommonConfig {
|
||||
export interface TabConfig<T = never> extends FormItem, ContainerCommonConfig<T> {
|
||||
type: 'tab' | 'dynamic-tab';
|
||||
tabType?: string;
|
||||
editable?: boolean;
|
||||
dynamic?: boolean;
|
||||
tabPosition?: 'top' | 'right' | 'bottom' | 'left';
|
||||
items: TabPaneConfig[];
|
||||
/** 当前激活的标签页,可以是固定值或动态函数 */
|
||||
active?:
|
||||
| string
|
||||
| ((mForm: FormState | undefined, data: { model: FormValue; formValue?: FormValue; prop: string }) => string);
|
||||
items: TabPaneConfig<T>[];
|
||||
onChange?: (mForm: FormState | undefined, data: any) => void;
|
||||
onTabAdd?: (mForm: FormState | undefined, data: any) => void;
|
||||
onTabRemove?: (mForm: FormState | undefined, tabName: string, data: any) => void;
|
||||
@ -654,7 +677,7 @@ export interface TabConfig extends FormItem, ContainerCommonConfig {
|
||||
/**
|
||||
* 分组
|
||||
*/
|
||||
export interface FieldsetConfig extends FormItem, ContainerCommonConfig {
|
||||
export interface FieldsetConfig<T = never> extends FormItem, ContainerCommonConfig<T> {
|
||||
type: 'fieldset';
|
||||
checkbox?:
|
||||
| boolean
|
||||
@ -671,7 +694,7 @@ export interface FieldsetConfig extends FormItem, ContainerCommonConfig {
|
||||
/**
|
||||
* 面板容器
|
||||
*/
|
||||
export interface PanelConfig extends FormItem, ContainerCommonConfig {
|
||||
export interface PanelConfig<T = never> extends FormItem, ContainerCommonConfig<T> {
|
||||
type: 'panel';
|
||||
expand?: boolean;
|
||||
title?: string;
|
||||
@ -683,7 +706,10 @@ export interface TableColumnConfig extends FormItem {
|
||||
label: string;
|
||||
width?: string | number;
|
||||
sortable?: boolean;
|
||||
[key: string]: any;
|
||||
items?: FormConfig;
|
||||
itemsFunction?: (row: any) => FormConfig;
|
||||
titleTip?: FilterFunction<string>;
|
||||
type?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -715,10 +741,11 @@ export interface TableConfig extends FormItem {
|
||||
importable?: (mForm: FormState | undefined, data: any) => boolean | 'undefined' | boolean;
|
||||
/** 是否显示checkbox */
|
||||
selection?: (mForm: FormState | undefined, data: any) => boolean | boolean | 'single';
|
||||
/** 新增的默认行 */
|
||||
defaultAdd?: (mForm: FormState | undefined, data: any) => any;
|
||||
/** 新增的默认行,可以是函数动态生成或静态对象 */
|
||||
defaultAdd?: ((mForm: FormState | undefined, data: any) => any) | Record<string, any>;
|
||||
copyHandler?: (mForm: FormState | undefined, data: any) => any;
|
||||
onSelect?: (mForm: FormState | undefined, data: any) => any;
|
||||
/** @deprecated 请使用 defaultSort */
|
||||
defautSort?: SortProp;
|
||||
defaultSort?: SortProp;
|
||||
/** 是否支持拖拽排序 */
|
||||
@ -739,15 +766,17 @@ export interface TableConfig extends FormItem {
|
||||
props?: Record<string, any>;
|
||||
text?: string;
|
||||
};
|
||||
sort?: boolean;
|
||||
sortKey?: string;
|
||||
}
|
||||
|
||||
export interface GroupListConfig extends FormItem {
|
||||
export interface GroupListConfig<T = never> extends FormItem {
|
||||
type: 'table' | 'groupList' | 'group-list';
|
||||
span?: number;
|
||||
enableToggleMode?: boolean;
|
||||
items: FormConfig;
|
||||
groupItems?: FormConfig;
|
||||
tableItems?: FormConfig;
|
||||
items: FormConfig<T>;
|
||||
groupItems?: FormConfig<T>;
|
||||
tableItems?: FormConfig<T>;
|
||||
titleKey?: string;
|
||||
titlePrefix?: string;
|
||||
title?: string | FilterFunction<string>;
|
||||
@ -760,7 +789,8 @@ export interface GroupListConfig extends FormItem {
|
||||
*/
|
||||
defaultExpandQuantity?: number;
|
||||
addable?: (mForm: FormState | undefined, data: any) => boolean | 'undefined' | boolean;
|
||||
defaultAdd?: (mForm: FormState | undefined, data: any) => any;
|
||||
/** 新增的默认值,可以是函数动态生成或静态对象 */
|
||||
defaultAdd?: ((mForm: FormState | undefined, data: any) => any) | Record<string, any>;
|
||||
delete?: (model: any, index: number | string | symbol, values: any) => boolean | boolean;
|
||||
copyable?: FilterFunction<boolean>;
|
||||
movable?: (
|
||||
@ -774,45 +804,50 @@ export interface GroupListConfig extends FormItem {
|
||||
props?: Record<string, any>;
|
||||
text?: string;
|
||||
};
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
interface StepItemConfig extends FormItem, ContainerCommonConfig {
|
||||
interface StepItemConfig<T = never> extends FormItem, ContainerCommonConfig<T> {
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface StepConfig extends FormItem {
|
||||
export interface StepConfig<T = never> extends FormItem {
|
||||
type: 'step';
|
||||
/** 每个 step 的间距,不填写将自适应间距。支持百分比。 */
|
||||
space?: string | number;
|
||||
items: StepItemConfig[];
|
||||
items: StepItemConfig<T>[];
|
||||
}
|
||||
|
||||
export interface ComponentConfig extends FormItem {
|
||||
type: 'component';
|
||||
id: string;
|
||||
extend: any;
|
||||
display: any;
|
||||
extend?: any;
|
||||
display?: any;
|
||||
component?: any;
|
||||
}
|
||||
|
||||
export interface FlexLayoutConfig extends FormItem, ContainerCommonConfig {
|
||||
export interface FlexLayoutConfig<T = never> extends FormItem, ContainerCommonConfig<T> {
|
||||
type: 'flex-layout';
|
||||
/** flex 子项间距,默认 '16px' */
|
||||
gap?: string;
|
||||
}
|
||||
|
||||
export type ChildConfig =
|
||||
| FormItem
|
||||
| TabConfig
|
||||
| RowConfig
|
||||
| FieldsetConfig
|
||||
| PanelConfig
|
||||
export type ChildConfig<T = never> =
|
||||
| ContainerCommonConfig<T>
|
||||
| TabConfig<T>
|
||||
| RowConfig<T>
|
||||
| FieldsetConfig<T>
|
||||
| PanelConfig<T>
|
||||
| TableConfig
|
||||
| GroupListConfig
|
||||
| StepConfig
|
||||
| GroupListConfig<T>
|
||||
| StepConfig<T>
|
||||
| DisplayConfig
|
||||
| TextConfig
|
||||
| NumberConfig
|
||||
| NumberRangeConfig
|
||||
| HiddenConfig
|
||||
| LinkConfig
|
||||
| LinkConfig<T>
|
||||
| DaterangeConfig
|
||||
| TimerangeConfig
|
||||
| SelectConfig
|
||||
| CascaderConfig
|
||||
| HtmlField
|
||||
@ -823,8 +858,12 @@ export type ChildConfig =
|
||||
| CheckboxConfig
|
||||
| SwitchConfig
|
||||
| RadioGroupConfig
|
||||
| CheckboxGroupConfig
|
||||
| TextareaConfig
|
||||
| DynamicFieldConfig
|
||||
| ComponentConfig;
|
||||
| ComponentConfig
|
||||
| FlexLayoutConfig<T>;
|
||||
|
||||
export type FormConfig = (ChildConfig | EditorChildConfig)[];
|
||||
export type FormItemConfig<T = never> = ChildConfig<T> | DynamicTypeConfig | EditorChildConfig<T> | T;
|
||||
|
||||
export type FormConfig<T = never> = FormItemConfig<T>[];
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { DataSourceFieldType, DataSourceSchema } from '@tmagic/schema';
|
||||
|
||||
import type { ChildConfig, FilterFunction, FormItem, FormState, Input } from './base';
|
||||
import type { FilterFunction, FormItem, FormItemConfig, FormState, Input } from './base';
|
||||
|
||||
export interface DataSourceFieldSelectConfig extends FormItem {
|
||||
export interface DataSourceFieldSelectConfig<T = never> extends FormItem {
|
||||
type: 'data-source-field-select';
|
||||
/**
|
||||
* 是否要编译成数据源的data。
|
||||
@ -26,13 +26,15 @@ export interface DataSourceFieldSelectConfig extends FormItem {
|
||||
},
|
||||
) => boolean);
|
||||
dataSourceFieldType?: DataSourceFieldType[];
|
||||
fieldConfig?: ChildConfig;
|
||||
fieldConfig?: FormItemConfig<T>;
|
||||
/** 是否可以编辑数据源,disable表示的是是否可以选择数据源 */
|
||||
notEditable?: boolean | FilterFunction;
|
||||
|
||||
dataSourceId?: string;
|
||||
}
|
||||
|
||||
export interface CodeConfig extends FormItem {
|
||||
type: 'code';
|
||||
type: 'vs-code';
|
||||
language?: string;
|
||||
options?: {
|
||||
[key: string]: any;
|
||||
@ -104,6 +106,7 @@ export interface DataSourceSelect extends FormItem, Input {
|
||||
}
|
||||
|
||||
export interface DisplayCondsConfig extends FormItem {
|
||||
type: 'display-conds';
|
||||
titlePrefix?: string;
|
||||
parentFields?: string[] | FilterFunction<string[]>;
|
||||
}
|
||||
@ -140,8 +143,12 @@ export interface UISelectConfig extends FormItem {
|
||||
type: 'ui-select';
|
||||
}
|
||||
|
||||
export type EditorChildConfig =
|
||||
| DataSourceFieldSelectConfig
|
||||
export interface StyleSetterConfig extends FormItem {
|
||||
type: 'style-setter';
|
||||
}
|
||||
|
||||
export type EditorChildConfig<T = never> =
|
||||
| DataSourceFieldSelectConfig<T>
|
||||
| CodeConfig
|
||||
| CodeLinkConfig
|
||||
| CodeSelectConfig
|
||||
@ -157,4 +164,5 @@ export type EditorChildConfig =
|
||||
| EventSelectConfig
|
||||
| KeyValueConfig
|
||||
| PageFragmentSelectConfig
|
||||
| UISelectConfig;
|
||||
| UISelectConfig
|
||||
| StyleSetterConfig;
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { FormConfig } from './base';
|
||||
import type { FormConfig, FormItemConfig } from './base';
|
||||
|
||||
export * from './base';
|
||||
export * from './editor';
|
||||
|
||||
export const defineFormConfig = <T = FormConfig>(config: T): T => config;
|
||||
export const defineFormConfig = <T = never>(config: FormConfig<T>): FormConfig<T> => config;
|
||||
|
||||
export const defineFormItem = <T = never>(config: FormItemConfig<T>): FormItemConfig<T> => config;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/form",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
|
||||
@ -218,13 +218,13 @@ const getTextByName = (name: string, config: FormConfig = props.config): string
|
||||
return typeof item.text === 'string' ? item.text : undefined;
|
||||
}
|
||||
|
||||
if (item.items && Array.isArray(item.items)) {
|
||||
if ('items' in item && Array.isArray(item.items)) {
|
||||
const result = findInConfig(item.items, remainingParts);
|
||||
if (result !== undefined) return result;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.items && Array.isArray(item.items)) {
|
||||
if ('items' in item && Array.isArray(item.items)) {
|
||||
const result = findInConfig(item.items, parts);
|
||||
if (result !== undefined) return result;
|
||||
}
|
||||
|
||||
@ -117,19 +117,25 @@ const stepActive = ref(1);
|
||||
const bodyHeight = ref(`${document.body.clientHeight - 194}px`);
|
||||
|
||||
const stepCount = computed(() => {
|
||||
const { length } = props.config;
|
||||
for (let index = 0; index < length; index++) {
|
||||
if (props.config[index].type === 'step') {
|
||||
return (props.config[index] as StepConfig).items.length;
|
||||
if (!Array.isArray(props.config)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (const item of props.config) {
|
||||
if ('type' in item && item.type === 'step') {
|
||||
return (item as StepConfig).items.length;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const hasStep = computed(() => {
|
||||
const { length } = props.config;
|
||||
for (let index = 0; index < length; index++) {
|
||||
if (props.config[index].type === 'step') {
|
||||
if (!Array.isArray(props.config)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const item of props.config) {
|
||||
if ('type' in item && item.type === 'step') {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<TMagicCol v-show="display && config.type !== 'hidden'" :span="span">
|
||||
<TMagicCol v-show="display && type !== 'hidden'" :span="span">
|
||||
<Container
|
||||
:model="model"
|
||||
:lastValues="lastValues"
|
||||
@ -21,7 +21,7 @@ import { computed, inject } from 'vue';
|
||||
|
||||
import { TMagicCol } from '@tmagic/design';
|
||||
|
||||
import type { ChildConfig, ContainerChangeEventData, FormState } from '../schema';
|
||||
import type { ContainerChangeEventData, FormItemConfig, FormState } from '../schema';
|
||||
import { display as displayFunction } from '../utils/form';
|
||||
|
||||
import Container from './Container.vue';
|
||||
@ -34,8 +34,8 @@ const props = defineProps<{
|
||||
model: any;
|
||||
lastValues?: any;
|
||||
isCompare?: boolean;
|
||||
config: ChildConfig;
|
||||
labelWidth?: string;
|
||||
config: FormItemConfig;
|
||||
labelWidth?: string | number;
|
||||
expandMore?: boolean;
|
||||
span?: number;
|
||||
size?: string;
|
||||
@ -52,4 +52,6 @@ const mForm = inject<FormState | undefined>('mForm');
|
||||
const display = computed(() => displayFunction(mForm, props.config.display, props));
|
||||
const changeHandler = (v: any, eventData: ContainerChangeEventData) => emit('change', v, eventData);
|
||||
const onAddDiffCount = () => emit('addDiffCount');
|
||||
|
||||
const type = computed(() => (props.config as any).type);
|
||||
</script>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div
|
||||
:data-tmagic-id="config.id"
|
||||
:data-tmagic-id="(config as Record<string, any>).id"
|
||||
:data-tmagic-form-item-prop="itemProp"
|
||||
:class="`m-form-container m-container-${type || ''} ${config.className || ''}${config.tip ? ' has-tip' : ''}`"
|
||||
:style="config.style"
|
||||
@ -28,7 +28,7 @@
|
||||
<FormLabel
|
||||
:tip="config.tip"
|
||||
:type="type"
|
||||
:use-label="config.useLabel"
|
||||
:use-label="(config as CheckboxConfig).useLabel"
|
||||
:label-title="config.labelTitle"
|
||||
:text="text"
|
||||
></FormLabel>
|
||||
@ -61,7 +61,7 @@
|
||||
></component>
|
||||
</TMagicFormItem>
|
||||
|
||||
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !config.useLabel" placement="top">
|
||||
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel" placement="top">
|
||||
<TMagicIcon style="line-height: 40px; margin-left: 5px"><warning-filled /></TMagicIcon>
|
||||
<template #content>
|
||||
<div v-html="config.tip"></div>
|
||||
@ -80,7 +80,7 @@
|
||||
<FormLabel
|
||||
:tip="config.tip"
|
||||
:type="type"
|
||||
:use-label="config.useLabel"
|
||||
:use-label="(config as CheckboxConfig).useLabel"
|
||||
:label-title="config.labelTitle"
|
||||
:text="text"
|
||||
></FormLabel>
|
||||
@ -95,7 +95,7 @@
|
||||
<component v-else v-bind="fieldsProps" :is="tagName" :model="lastValues" @change="onChangeHandler"></component>
|
||||
</TMagicFormItem>
|
||||
|
||||
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !config.useLabel" placement="top">
|
||||
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel" placement="top">
|
||||
<TMagicIcon style="line-height: 40px; margin-left: 5px"><warning-filled /></TMagicIcon>
|
||||
<template #content>
|
||||
<div v-html="config.tip"></div>
|
||||
@ -112,7 +112,7 @@
|
||||
<FormLabel
|
||||
:tip="config.tip"
|
||||
:type="type"
|
||||
:use-label="config.useLabel"
|
||||
:use-label="(config as CheckboxConfig).useLabel"
|
||||
:label-title="config.labelTitle"
|
||||
:text="text"
|
||||
></FormLabel>
|
||||
@ -127,7 +127,7 @@
|
||||
<component v-else v-bind="fieldsProps" :is="tagName" :model="model" @change="onChangeHandler"></component>
|
||||
</TMagicFormItem>
|
||||
|
||||
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !config.useLabel" placement="top">
|
||||
<TMagicTooltip v-if="config.tip && type === 'checkbox' && !(config as CheckboxConfig).useLabel" placement="top">
|
||||
<TMagicIcon style="line-height: 40px; margin-left: 5px"><warning-filled /></TMagicIcon>
|
||||
<template #content>
|
||||
<div v-html="config.tip"></div>
|
||||
@ -174,9 +174,11 @@ import { getValueByKeyPath } from '@tmagic/utils';
|
||||
|
||||
import MHidden from '../fields/Hidden.vue';
|
||||
import type {
|
||||
ChildConfig,
|
||||
CheckboxConfig,
|
||||
ComponentConfig,
|
||||
ContainerChangeEventData,
|
||||
ContainerCommonConfig,
|
||||
FormItemConfig,
|
||||
FormState,
|
||||
FormValue,
|
||||
ToolTipConfigType,
|
||||
@ -196,10 +198,10 @@ const props = withDefaults(
|
||||
model: FormValue;
|
||||
/** 需对比的值(开启对比模式时传入) */
|
||||
lastValues?: FormValue;
|
||||
config: ChildConfig;
|
||||
config: FormItemConfig;
|
||||
prop?: string;
|
||||
disabled?: boolean;
|
||||
labelWidth?: string;
|
||||
labelWidth?: string | number;
|
||||
expandMore?: boolean;
|
||||
stepActive?: string | number;
|
||||
size?: string;
|
||||
@ -251,7 +253,7 @@ const itemProp = computed(() => {
|
||||
});
|
||||
|
||||
const type = computed((): string => {
|
||||
let { type } = props.config;
|
||||
let type = 'type' in props.config ? props.config.type : '';
|
||||
type = type && filterFunction<string>(mForm, type, props);
|
||||
if (type === 'form') return '';
|
||||
if (type === 'container') return '';
|
||||
@ -259,13 +261,10 @@ const type = computed((): string => {
|
||||
});
|
||||
|
||||
const tagName = computed(() => {
|
||||
if (type.value === 'component' && props.config.component) {
|
||||
return props.config.component;
|
||||
if (type.value === 'component' && (props.config as ComponentConfig).component) {
|
||||
return (props.config as ComponentConfig).component;
|
||||
}
|
||||
|
||||
if (!getField(type.value || 'container')) {
|
||||
console.log(type.value, 'type.value');
|
||||
}
|
||||
return getField(type.value || 'container') || `m-${items.value ? 'form' : 'fields'}-${type.value}`;
|
||||
});
|
||||
|
||||
@ -305,7 +304,7 @@ const fieldsProps = computed(() => ({
|
||||
name: name.value,
|
||||
disabled: disabled.value,
|
||||
prop: itemProp.value,
|
||||
key: props.config[mForm?.keyProps],
|
||||
key: (props.config as Record<string, any>)[mForm?.keyProps],
|
||||
style: props.config.fieldStyle,
|
||||
}));
|
||||
|
||||
|
||||
@ -144,7 +144,9 @@ const rowConfig = computed(() => ({
|
||||
span: props.config.span || 24,
|
||||
items: props.config.items,
|
||||
labelWidth: props.config.labelWidth,
|
||||
[mForm?.keyProp || '__key']: `${props.config[mForm?.keyProp || '__key']}${String(props.index)}`,
|
||||
[mForm?.keyProp || '__key']: `${(props.config as Record<string, any>)[mForm?.keyProp || '__key']}${String(
|
||||
props.index,
|
||||
)}`,
|
||||
}));
|
||||
|
||||
const title = computed(() => {
|
||||
|
||||
@ -10,7 +10,7 @@
|
||||
<TMagicTooltip :disabled="!Boolean(option.tooltip)" placement="top-start" :content="option.tooltip">
|
||||
<div>
|
||||
<TMagicIcon v-if="option.icon" :size="iconSize"><component :is="option.icon"></component></TMagicIcon>
|
||||
<span>{{ option.text }}</span>
|
||||
<span v-if="option.text">{{ option.text }}</span>
|
||||
</div>
|
||||
</TMagicTooltip>
|
||||
</component>
|
||||
|
||||
@ -21,7 +21,7 @@ import dayjs from 'dayjs';
|
||||
|
||||
import { TMagicTimePicker } from '@tmagic/design';
|
||||
|
||||
import type { ChangeRecord, DaterangeConfig, FieldProps } from '../schema';
|
||||
import type { ChangeRecord, FieldProps, TimerangeConfig } from '../schema';
|
||||
import { datetimeFormatter } from '../utils/form';
|
||||
import { useAddField } from '../utils/useAddField';
|
||||
|
||||
@ -29,7 +29,7 @@ defineOptions({
|
||||
name: 'MFormTimeRange',
|
||||
});
|
||||
|
||||
const props = defineProps<FieldProps<DaterangeConfig>>();
|
||||
const props = defineProps<FieldProps<TimerangeConfig>>();
|
||||
|
||||
const emit = defineEmits(['change']);
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { computed, inject } from 'vue';
|
||||
|
||||
import { tMagicMessage } from '@tmagic/design';
|
||||
import type { FormState } from '@tmagic/form-schema';
|
||||
import type { FormConfig, FormState } from '@tmagic/form-schema';
|
||||
|
||||
import { initValue } from '../utils/form';
|
||||
|
||||
@ -86,7 +86,7 @@ export const useAdd = (
|
||||
}
|
||||
|
||||
inputs = await initValue(mForm, {
|
||||
config: columns,
|
||||
config: columns as FormConfig,
|
||||
initValues: inputs,
|
||||
});
|
||||
}
|
||||
|
||||
@ -3,7 +3,7 @@ import { WarningFilled } from '@element-plus/icons-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { type TableColumnOptions, TMagicIcon, TMagicTooltip } from '@tmagic/design';
|
||||
import type { FormState, TableColumnConfig } from '@tmagic/form-schema';
|
||||
import type { FormItemConfig, FormState, TableColumnConfig } from '@tmagic/form-schema';
|
||||
|
||||
import Container from '../containers/Container.vue';
|
||||
import type { ContainerChangeEventData } from '../schema';
|
||||
@ -68,7 +68,7 @@ export const useTableColumns = (
|
||||
return `${props.prop}${props.prop ? '.' : ''}${index + 1 + currentPage.value * pageSize.value - 1}`;
|
||||
};
|
||||
|
||||
const makeConfig = (config: TableColumnConfig, row: any) => {
|
||||
const makeConfig = (config: TableColumnConfig, row: any): TableColumnConfig => {
|
||||
const newConfig = cloneDeep(config);
|
||||
if (typeof config.itemsFunction === 'function') {
|
||||
newConfig.items = config.itemsFunction(row);
|
||||
@ -199,7 +199,7 @@ export const useTableColumns = (
|
||||
disabled: props.disabled,
|
||||
prop: getProp($index),
|
||||
rules: column.rules,
|
||||
config: makeConfig(column, row),
|
||||
config: makeConfig(column, row) as FormItemConfig,
|
||||
model: row,
|
||||
lastValues: lastData.value[$index],
|
||||
isCompare: props.isCompare,
|
||||
|
||||
@ -23,7 +23,7 @@ import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import { getValueByKeyPath } from '@tmagic/utils';
|
||||
|
||||
import {
|
||||
import type {
|
||||
ChildConfig,
|
||||
ContainerCommonConfig,
|
||||
DaterangeConfig,
|
||||
@ -34,6 +34,7 @@ import {
|
||||
HtmlField,
|
||||
Rule,
|
||||
SortProp,
|
||||
TableConfig,
|
||||
TabPaneConfig,
|
||||
TypeFunction,
|
||||
} from '../schema';
|
||||
@ -118,7 +119,8 @@ const initValueItem = function (
|
||||
) {
|
||||
const { items } = item as ContainerCommonConfig;
|
||||
const { names } = item as DaterangeConfig;
|
||||
const { type, name } = item as ChildConfig;
|
||||
const type = 'type' in item ? item.type : '';
|
||||
const { name } = item;
|
||||
|
||||
if (isTableSelect(type) && name) {
|
||||
value[name] = initValue[name] ?? '';
|
||||
@ -148,14 +150,15 @@ const initValueItem = function (
|
||||
setValue(mForm, value, initValue, item);
|
||||
|
||||
if (type === 'table') {
|
||||
if (item.defautSort) {
|
||||
sortChange(value[name], item.defautSort);
|
||||
} else if (item.defaultSort) {
|
||||
sortChange(value[name], item.defaultSort);
|
||||
const tableConfig = item as TableConfig;
|
||||
if (tableConfig.defautSort) {
|
||||
sortChange(value[name], tableConfig.defautSort);
|
||||
} else if (tableConfig.defaultSort) {
|
||||
sortChange(value[name], tableConfig.defaultSort);
|
||||
}
|
||||
|
||||
if (item.sort && item.sortKey) {
|
||||
value[name].sort((a: any, b: any) => b[item.sortKey] - a[item.sortKey]);
|
||||
if (tableConfig.sort && tableConfig.sortKey) {
|
||||
value[name].sort((a: any, b: any) => b[tableConfig.sortKey!] - a[tableConfig.sortKey!]);
|
||||
}
|
||||
}
|
||||
|
||||
@ -169,8 +172,8 @@ export const createValues = function (
|
||||
value: FormValue = {},
|
||||
) {
|
||||
if (Array.isArray(config)) {
|
||||
config.forEach((item: ChildConfig | TabPaneConfig) => {
|
||||
initValueItem(mForm, item, initValue, value);
|
||||
config.forEach((item) => {
|
||||
initValueItem(mForm, item as ChildConfig | TabPaneConfig, initValue, value);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/schema",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -186,7 +186,7 @@ export interface CodeBlockContent {
|
||||
/** 代码块名称 */
|
||||
name: string;
|
||||
/** 代码块内容 */
|
||||
content: ((...args: any[]) => any) | string;
|
||||
content: ((...args: any[]) => any) | Function;
|
||||
/** 参数定义 */
|
||||
params: CodeParam[] | [];
|
||||
/** 注释 */
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/stage",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
@ -31,6 +31,7 @@
|
||||
"dependencies": {
|
||||
"@scena/guides": "^0.29.2",
|
||||
"events": "^3.3.0",
|
||||
"@zumer/snapdom": "^2.8.0",
|
||||
"keycon": "^1.4.0",
|
||||
"lodash-es": "^4.17.21",
|
||||
"moveable": "^0.53.0",
|
||||
|
||||
@ -236,6 +236,30 @@ export default class ActionManager extends EventEmitter {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取鼠标下方第二个可选中元素(跳过最上层),用于双击穿透选中下方元素
|
||||
* @param event 鼠标事件
|
||||
* @returns 鼠标下方第二个可选中元素,不存在时返回 null
|
||||
*/
|
||||
public async getNextElementFromPoint(event: MouseEvent): Promise<HTMLElement | null> {
|
||||
const els = this.getElementsFromPoint(event as Point);
|
||||
|
||||
let stopped = false;
|
||||
const stop = () => (stopped = true);
|
||||
let skippedFirst = false;
|
||||
for (const el of els) {
|
||||
if (!getIdFromEl()(el)?.startsWith(GHOST_EL_ID_PREFIX) && (await this.isElCanSelect(el, event, stop))) {
|
||||
if (stopped) break;
|
||||
if (!skippedFirst) {
|
||||
skippedFirst = true;
|
||||
continue;
|
||||
}
|
||||
return el;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断一个元素能否在当前场景被选中
|
||||
* @param el 被判断的元素
|
||||
|
||||
@ -124,6 +124,7 @@ export default class Rule extends EventEmitter {
|
||||
this.hGuides?.off('changeGuides', this.hGuidesChangeGuidesHandler);
|
||||
this.vGuides?.off('changeGuides', this.vGuidesChangeGuidesHandler);
|
||||
this.containerResizeObserver?.disconnect();
|
||||
this.container = undefined;
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
@ -137,7 +138,6 @@ export default class Rule extends EventEmitter {
|
||||
|
||||
this.hGuides = undefined;
|
||||
this.vGuides = undefined;
|
||||
this.container = undefined;
|
||||
}
|
||||
|
||||
private getGuidesStyle = (type: GuidesType) => ({
|
||||
|
||||
@ -18,6 +18,7 @@
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { SnapdomOptions } from '@zumer/snapdom';
|
||||
import type { MoveableOptions, OnDragStart } from 'moveable';
|
||||
|
||||
import type { Id } from '@tmagic/core';
|
||||
@ -88,7 +89,38 @@ export default class StageCore extends EventEmitter {
|
||||
* @param id 选中的id
|
||||
*/
|
||||
public async select(id: Id, event?: MouseEvent): Promise<void> {
|
||||
const el = this.renderer?.getTargetElement(id) || null;
|
||||
if (!this.renderer) {
|
||||
return;
|
||||
}
|
||||
|
||||
let el = this.renderer.getTargetElement(id) || null;
|
||||
|
||||
if (!el) {
|
||||
el = await new Promise<HTMLElement | null>((resolve) => {
|
||||
const observer = new MutationObserver(() => {
|
||||
const target = this.renderer?.getTargetElement(id);
|
||||
if (target) {
|
||||
observer.disconnect();
|
||||
clearTimeout(timer);
|
||||
resolve(target);
|
||||
}
|
||||
});
|
||||
|
||||
const body = this.renderer?.getDocument()?.body;
|
||||
if (!body) {
|
||||
resolve(null);
|
||||
return;
|
||||
}
|
||||
|
||||
observer.observe(body, { childList: true, subtree: true });
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
observer.disconnect();
|
||||
resolve(this.renderer?.getTargetElement(id) || null);
|
||||
}, 1000);
|
||||
});
|
||||
}
|
||||
|
||||
if (el === this.actionManager?.getSelectedEl()) return;
|
||||
|
||||
await this.renderer?.select([id]);
|
||||
@ -241,6 +273,21 @@ export default class StageCore extends EventEmitter {
|
||||
this.renderer?.reloadIframe(url);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定id的dom元素生成为图片
|
||||
*/
|
||||
public async getElementImage(
|
||||
id: Id,
|
||||
type: 'download' | 'raw' | 'svg' | 'canvas' | 'png' | 'jpeg' | 'webp' | 'blob' = 'png',
|
||||
options: SnapdomOptions = {},
|
||||
) {
|
||||
if (!this.renderer) {
|
||||
throw new Error('Renderer is not initialized');
|
||||
}
|
||||
|
||||
return this.renderer.getElementImage(id, type, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* 销毁实例
|
||||
*/
|
||||
|
||||
@ -18,6 +18,8 @@
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
import { snapdom, SnapdomOptions } from '@zumer/snapdom';
|
||||
|
||||
import type { Id } from '@tmagic/core';
|
||||
import { getElById, getHost, guid, injectStyle, isSameDomain } from '@tmagic/core';
|
||||
|
||||
@ -162,6 +164,35 @@ export default class StageRender extends EventEmitter {
|
||||
return getElById()(this.getDocument(), id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 将指定id的dom元素生成为图片
|
||||
* @param id 目标元素的id
|
||||
* @param options 图片生成选项
|
||||
* @returns data URL 格式的图片数据
|
||||
*/
|
||||
public async getElementImage(
|
||||
id: Id,
|
||||
type: 'download' | 'raw' | 'svg' | 'canvas' | 'png' | 'jpeg' | 'webp' | 'blob' = 'png',
|
||||
options: SnapdomOptions = {},
|
||||
) {
|
||||
const el = this.getTargetElement(id);
|
||||
if (!el) {
|
||||
throw new Error(`Element with id "${id}" not found`);
|
||||
}
|
||||
|
||||
el.scrollIntoView();
|
||||
|
||||
const toFunc = `to${type.charAt(0).toUpperCase() + type.slice(1)}`;
|
||||
|
||||
const result = await snapdom(el, options);
|
||||
|
||||
if (toFunc in result) {
|
||||
return result[toFunc]();
|
||||
}
|
||||
|
||||
throw new Error(`Invalid type: ${type}`);
|
||||
}
|
||||
|
||||
public postTmagicRuntimeReady() {
|
||||
this.contentWindow = this.iframe?.contentWindow as RuntimeWindow;
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/table",
|
||||
"type": "module",
|
||||
"sideEffects": [
|
||||
|
||||
@ -5,7 +5,7 @@
|
||||
<MForm
|
||||
v-else-if="(config.type || config.editInlineFormConfig) && editState[index]"
|
||||
label-width="0"
|
||||
:config="config.editInlineFormConfig ?? [config]"
|
||||
:config="config.editInlineFormConfig ?? [config as FormItemConfig]"
|
||||
:init-values="editState[index]"
|
||||
@change="formChangeHandler"
|
||||
></MForm>
|
||||
@ -46,7 +46,7 @@
|
||||
<script lang="ts" setup>
|
||||
import { TMagicButton, TMagicTag, TMagicTooltip } from '@tmagic/design';
|
||||
import { type ContainerChangeEventData, MForm } from '@tmagic/form';
|
||||
import type { FormValue } from '@tmagic/form-schema';
|
||||
import type { FormItemConfig, FormValue } from '@tmagic/form-schema';
|
||||
import { setValueByKeyPath } from '@tmagic/utils';
|
||||
|
||||
import { ColumnConfig } from './schema';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/tdesign-vue-next-adapter",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
{
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"name": "@tmagic/utils",
|
||||
"type": "module",
|
||||
"sideEffects": false,
|
||||
|
||||
5
packages/utils/src/const.ts
Normal file
5
packages/utils/src/const.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export const DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX = 'ds-field::';
|
||||
|
||||
export const DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX = 'ds-field-changed';
|
||||
|
||||
export const DATA_SOURCE_SET_DATA_METHOD_NAME = 'setDataFromEvent';
|
||||
@ -35,8 +35,12 @@ import { NodeType } from '@tmagic/schema';
|
||||
|
||||
import type { EditorNodeInfo } from '@editor/type';
|
||||
|
||||
import { DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX } from './const';
|
||||
|
||||
export * from './dom';
|
||||
|
||||
export * from './const';
|
||||
|
||||
// for typeof global checks without @types/node
|
||||
declare let global: {};
|
||||
|
||||
@ -542,10 +546,6 @@ export const getDefaultValueFromFields = (fields: DataSchema[]) => {
|
||||
return data;
|
||||
};
|
||||
|
||||
export const DATA_SOURCE_FIELDS_SELECT_VALUE_PREFIX = 'ds-field::';
|
||||
|
||||
export const DATA_SOURCE_FIELDS_CHANGE_EVENT_PREFIX = 'ds-field-changed';
|
||||
|
||||
export const getKeys = Object.keys as <T extends object>(obj: T) => Array<keyof T>;
|
||||
|
||||
export const calculatePercentage = (value: number, percentageStr: string) => {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tmagic-playground",
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@ -12,11 +12,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-plus/icons-vue": "^2.3.2",
|
||||
"@tmagic/core": "1.7.8-beta.1",
|
||||
"@tmagic/design": "1.7.8-beta.1",
|
||||
"@tmagic/editor": "1.7.8-beta.1",
|
||||
"@tmagic/element-plus-adapter": "1.7.8-beta.1",
|
||||
"@tmagic/tdesign-vue-next-adapter": "1.7.8-beta.1",
|
||||
"@tmagic/core": "1.7.10",
|
||||
"@tmagic/design": "1.7.10",
|
||||
"@tmagic/editor": "1.7.10",
|
||||
"@tmagic/element-plus-adapter": "1.7.10",
|
||||
"@tmagic/tdesign-vue-next-adapter": "1.7.10",
|
||||
"@tmagic/tmagic-form-runtime": "1.1.3",
|
||||
"element-plus": "^2.11.8",
|
||||
"lodash-es": "^4.17.21",
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "..",
|
||||
},
|
||||
"exclude": [
|
||||
"**/dist/**/*"
|
||||
|
||||
1424
pnpm-lock.yaml
generated
1424
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -1,14 +1,13 @@
|
||||
packages:
|
||||
- 'packages/*'
|
||||
- 'playground'
|
||||
- 'runtime/*'
|
||||
- 'vue-components/*'
|
||||
- 'react-components/*'
|
||||
- 'eslint-config'
|
||||
- "packages/*"
|
||||
- "playground"
|
||||
- "runtime/*"
|
||||
- "vue-components/*"
|
||||
- "react-components/*"
|
||||
- "eslint-config"
|
||||
|
||||
catalog:
|
||||
vue: ^3.5.24
|
||||
'@vue/compiler-sfc': ^3.5.24
|
||||
vite: ^8.0.0
|
||||
typescript: "^5.9.3"
|
||||
|
||||
"@vue/compiler-sfc": ^3.5.24
|
||||
vite: ^8.0.8
|
||||
typescript: "^6.0.2"
|
||||
|
||||
@ -35,7 +35,16 @@ function aliasPlugin() {
|
||||
resolveId(source) {
|
||||
for (const { find, replacement } of aliasEntries) {
|
||||
if (find.test(source)) {
|
||||
return source.replace(find, replacement);
|
||||
let resolved = source.replace(find, replacement);
|
||||
resolved = resolved.replace(/\/\//g, '/');
|
||||
resolved = resolved.replace(/\.js$/, '');
|
||||
if (existsSync(`${resolved}.d.ts`)) {
|
||||
return `${resolved}.d.ts`;
|
||||
}
|
||||
if (existsSync(resolved)) {
|
||||
return resolved;
|
||||
}
|
||||
return `${resolved}.d.ts`;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import type { ChangeEvent, MNode } from '@tmagic/core';
|
||||
import type TMagicApp from '@tmagic/core';
|
||||
import type { ChangeEvent, MNode } from '@tmagic/core';
|
||||
import { isPage, replaceChildNode } from '@tmagic/core';
|
||||
|
||||
export const useDsl = (app: TMagicApp | undefined) => {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import type { Id, MApp } from '@tmagic/core';
|
||||
import type TMagicApp from '@tmagic/core';
|
||||
import type { Id, MApp } from '@tmagic/core';
|
||||
import { getElById, replaceChildNode } from '@tmagic/core';
|
||||
import type { Magic, RemoveData, SortEventData, UpdateData } from '@tmagic/stage';
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "runtime-react",
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"engines": {
|
||||
@ -16,16 +16,16 @@
|
||||
"build:playground": "node scripts/build.mjs --type=playground"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tmagic/core": "1.7.8-beta.1",
|
||||
"@tmagic/core": "1.7.10",
|
||||
"@tmagic/react-runtime-help": "0.2.2",
|
||||
"@tmagic/stage": "1.7.8-beta.1",
|
||||
"@tmagic/stage": "1.7.10",
|
||||
"axios": "^1.13.2",
|
||||
"qrcode": "^1.5.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tmagic/cli": "1.7.8-beta.1",
|
||||
"@tmagic/cli": "1.7.10",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"baseUrl": "./",
|
||||
"jsx": "react",
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node"],
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "runtime-vue",
|
||||
"version": "1.7.8-beta.1",
|
||||
"version": "1.7.10",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"engines": {
|
||||
@ -16,14 +16,14 @@
|
||||
"build:playground": "node scripts/build.mjs --type=playground"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tmagic/core": "1.7.8-beta.1",
|
||||
"@tmagic/stage": "1.7.8-beta.1",
|
||||
"@tmagic/core": "1.7.10",
|
||||
"@tmagic/stage": "1.7.10",
|
||||
"@tmagic/vue-runtime-help": "^2.0.0",
|
||||
"axios": "^1.13.2",
|
||||
"vue": "catalog:"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tmagic/cli": "1.7.8-beta.1",
|
||||
"@tmagic/cli": "1.7.10",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/node": "^24.0.10",
|
||||
"@vitejs/plugin-legacy": "^8.0.0",
|
||||
|
||||
@ -20,9 +20,10 @@ if (args.package) {
|
||||
const pkgRoot = path.resolve(packagesDir, args.package);
|
||||
if (fs.statSync(pkgRoot).isDirectory()) {
|
||||
rimraf.sync(path.resolve(packagesDir, `./${args.package}/dist`));
|
||||
const pkg = createRequire(import.meta.url)(`../packages/${args.package}/package.json`);
|
||||
|
||||
build({ packageName: args.package, format: 'es' });
|
||||
build({ packageName: args.package, format: 'umd' });
|
||||
build({ packageName: args.package, format: 'es', pkg, packagesDir });
|
||||
build({ packageName: args.package, format: 'umd', pkg, packagesDir });
|
||||
}
|
||||
} else {
|
||||
const packages = getPackageNames(packagesDir);
|
||||
@ -45,6 +46,30 @@ if (args.package) {
|
||||
}
|
||||
}
|
||||
|
||||
// rolldown 在 UMD 输出顶部会注入
|
||||
// Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
||||
// 当内联的依赖(如 lodash-es 的 _Symbol.js)声明 `var Symbol = root.Symbol;`
|
||||
// 时,由于 var hoisting,该局部 `Symbol` 会把上面一行引用到的全局 `Symbol`
|
||||
// 遮蔽掉(此时局部变量还未赋值),运行时抛出
|
||||
// TypeError: Cannot read properties of undefined (reading 'toStringTag')
|
||||
// 这里通过后处理把该引用改为 `globalThis.Symbol.toStringTag`,绕开被 hoist
|
||||
// 的局部绑定。rolldown 修好前先用此 workaround。
|
||||
function fixUmdSymbolShadow() {
|
||||
return {
|
||||
name: 'tmagic:fix-umd-symbol-shadow',
|
||||
generateBundle(outputOptions, bundle) {
|
||||
if (outputOptions.format !== 'umd') return;
|
||||
for (const file of Object.values(bundle)) {
|
||||
if (file.type !== 'chunk' || typeof file.code !== 'string') continue;
|
||||
file.code = file.code.replace(
|
||||
/Object\.defineProperty\(exports,\s*Symbol\.toStringTag,/g,
|
||||
'Object.defineProperty(exports, globalThis.Symbol.toStringTag,',
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function build({ packageName, format, pkg, packagesDir }) {
|
||||
await buildVite({
|
||||
root: path.resolve(packagesDir, `./${packageName}`),
|
||||
@ -69,6 +94,7 @@ async function build({ packageName, format, pkg, packagesDir }) {
|
||||
},
|
||||
|
||||
rolldownOptions: {
|
||||
plugins: [fixUmdSymbolShadow()],
|
||||
// 确保外部化处理那些你不想打包进库的依赖
|
||||
external(id) {
|
||||
if (format === 'umd' && id === 'lodash-es') {
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"outDir": "temp",
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user