diff --git a/playground/package.json b/playground/package.json index 368c7718..c5facf20 100644 --- a/playground/package.json +++ b/playground/package.json @@ -18,6 +18,7 @@ "@tmagic/editor": "1.3.9", "@tmagic/element-plus-adapter": "1.3.9", "@tmagic/form": "1.3.9", + "@tmagic/tmagic-form-runtime": "1.0.0", "@tmagic/schema": "1.3.9", "@tmagic/stage": "1.3.9", "@tmagic/utils": "1.3.9", diff --git a/playground/src/configs/form-config/checkbox.ts b/playground/src/configs/form-config/checkbox.ts deleted file mode 100644 index 1902bf2f..00000000 --- a/playground/src/configs/form-config/checkbox.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { FormConfig } from '@tmagic/form'; - -export default [] as FormConfig; diff --git a/playground/src/configs/form-config/display.ts b/playground/src/configs/form-config/display.ts deleted file mode 100644 index 1902bf2f..00000000 --- a/playground/src/configs/form-config/display.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { FormConfig } from '@tmagic/form'; - -export default [] as FormConfig; diff --git a/playground/src/configs/form-config/switch.ts b/playground/src/configs/form-config/switch.ts deleted file mode 100644 index 1902bf2f..00000000 --- a/playground/src/configs/form-config/switch.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { FormConfig } from '@tmagic/form'; - -export default [] as FormConfig; diff --git a/playground/src/pages/FormEditor.vue b/playground/src/pages/FormEditor.vue index 63de6dcd..6d08d181 100644 --- a/playground/src/pages/FormEditor.vue +++ b/playground/src/pages/FormEditor.vue @@ -7,6 +7,7 @@ :props-configs="propsConfigs" :render="render" :can-select="canSelect" + :disabled-page-fragment="true" :stage-rect="{ width: 'calc(100% - 70px)', height: '100%' }" :moveable-options="{ resizable: false }" > @@ -17,33 +18,26 @@ diff --git a/playground/vite.config.ts b/playground/vite.config.ts index c352feed..e8a09d5e 100644 --- a/playground/vite.config.ts +++ b/playground/vite.config.ts @@ -54,6 +54,10 @@ export default defineConfig({ { find: /^@tmagic\/editor/, replacement: path.join(__dirname, '../packages/editor/src/index.ts') }, { find: /^@tmagic\/schema/, replacement: path.join(__dirname, '../packages/schema/src/index.ts') }, { find: /^@tmagic\/form/, replacement: path.join(__dirname, '../packages/form/src/index.ts') }, + { + find: /^@tmagic\/tmagic-form-runtime/, + replacement: path.join(__dirname, '../runtime/tmagic-form/src/index.ts'), + }, { find: /^@tmagic\/table/, replacement: path.join(__dirname, '../packages/table/src/index.ts') }, { find: /^@tmagic\/stage/, replacement: path.join(__dirname, '../packages/stage/src/index.ts') }, { find: /^@tmagic\/utils/, replacement: path.join(__dirname, '../packages/utils/src/index.ts') }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a9e057ad..808cac07 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -761,6 +761,9 @@ importers: '@tmagic/stage': specifier: 1.3.9 version: link:../packages/stage + '@tmagic/tmagic-form-runtime': + specifier: 1.0.0 + version: link:../runtime/tmagic-form '@tmagic/utils': specifier: 1.3.9 version: link:../packages/utils @@ -890,6 +893,55 @@ importers: specifier: ^5.0.7 version: 5.0.7(@types/node@18.19.3)(sass@1.35.1)(terser@5.14.2) + runtime/tmagic-form: + dependencies: + '@tmagic/core': + specifier: ^1.3.9 + version: link:../../packages/core + '@tmagic/editor': + specifier: ^1.3.9 + version: link:../../packages/editor + '@tmagic/form': + specifier: ^1.3.9 + version: link:../../packages/form + '@tmagic/schema': + specifier: ^1.3.9 + version: link:../../packages/schema + '@tmagic/utils': + specifier: ^1.3.9 + version: link:../../packages/utils + element-plus: + specifier: ^2.4.3 + version: 2.4.3(vue@3.3.8) + vue: + specifier: ^3.3.8 + version: 3.3.8(typescript@5.0.4) + devDependencies: + '@tmagic/stage': + specifier: ^1.3.9 + version: link:../../packages/stage + '@types/node': + specifier: ^18.19.0 + version: 18.19.3 + '@vitejs/plugin-vue': + specifier: ^4.5.2 + version: 4.5.2(vite@5.0.7)(vue@3.3.8) + '@vue/compiler-sfc': + specifier: ^3.3.8 + version: 3.3.8 + rimraf: + specifier: ^3.0.2 + version: 3.0.2 + typescript: + specifier: ^5.0.4 + version: 5.0.4 + vite: + specifier: ^5.0.7 + version: 5.0.7(@types/node@18.19.3)(sass@1.35.1)(terser@5.14.2) + vue-tsc: + specifier: ^1.8.25 + version: 1.8.25(typescript@5.0.4) + runtime/vue2: dependencies: '@tmagic/cli': @@ -8936,7 +8988,7 @@ packages: dependencies: hosted-git-info: 4.1.0 is-core-module: 2.11.0 - semver: 7.5.1 + semver: 7.5.4 validate-npm-package-license: 3.0.4 dev: true diff --git a/runtime/tmagic-form/README.md b/runtime/tmagic-form/README.md new file mode 100644 index 00000000..a562f960 --- /dev/null +++ b/runtime/tmagic-form/README.md @@ -0,0 +1,38 @@ +# TMagicFormRuntime +TMagicFormRuntime 基于@tmagic/form的编辑器runtime + +## 环境准备 + +先基于[tmagic-editor](https://tencent.github.io/tmagic-editor/docs/guide/)将编辑器搭建起来 + +按住依赖 + +```bash +pnpm add @tmagic/tmagic-form-runtime +``` + +```html + + +``` + +```ts +import { + canSelect, + COMPONENT_GROUP_LIST as componentGroupList, + propsConfigs, + useRuntime, +} from '@tmagic/tmagic-form-runtime'; + +const { render } = useRuntime(); +``` \ No newline at end of file diff --git a/runtime/tmagic-form/package.json b/runtime/tmagic-form/package.json new file mode 100644 index 00000000..c1995794 --- /dev/null +++ b/runtime/tmagic-form/package.json @@ -0,0 +1,58 @@ +{ + "version": "1.0.0", + "name": "@tmagic/tmagic-form-runtime", + "type": "module", + "sideEffects": [ + "dist/*", + "src/theme/*" + ], + "main": "dist/tmagic-form-runtime.umd.cjs", + "module": "dist/tmagic-form-runtime.js", + "types": "types/index.d.ts", + "exports": { + ".": { + "types": "./types/index.d.ts", + "import": "./dist/tmagic-form-runtime.js", + "require": "./dist/tmagic-form.umd-runtime.cjs" + }, + "./*": "./*" + }, + "license": "Apache-2.0", + "scripts": { + "build": "npm run build:type && vite build", + "build:type": "npm run clear:type && vue-tsc --declaration --emitDeclarationOnly --project tsconfig.build.json", + "clear:type": "rimraf ./types" + }, + "engines": { + "node": ">=18" + }, + "repository": { + "type": "git", + "url": "https://github.com/Tencent/tmagic-editor.git" + }, + "dependencies": { + "@tmagic/core": "^1.3.9", + "@tmagic/editor": "^1.3.9", + "@tmagic/form": "^1.3.9", + "@tmagic/utils": "^1.3.9", + "@tmagic/schema": "^1.3.9", + "element-plus": "^2.4.3" + }, + "peerDependencies": { + "@tmagic/editor": "^1.3.9", + "@tmagic/form": "^1.3.9", + "@tmagic/schema": "^1.3.9", + "element-plus": "^2.4.3", + "vue": "^3.3.8" + }, + "devDependencies": { + "@tmagic/stage": "^1.3.9", + "@types/node": "^18.19.0", + "@vitejs/plugin-vue": "^4.5.2", + "@vue/compiler-sfc": "^3.3.8", + "rimraf": "^3.0.2", + "typescript": "^5.0.4", + "vite": "^5.0.7", + "vue-tsc": "^1.8.25" + } +} diff --git a/runtime/tmagic-form/src/App.vue b/runtime/tmagic-form/src/App.vue new file mode 100644 index 00000000..eb4ac281 --- /dev/null +++ b/runtime/tmagic-form/src/App.vue @@ -0,0 +1,25 @@ + + + diff --git a/runtime/tmagic-form/src/component-group-list.ts b/runtime/tmagic-form/src/component-group-list.ts new file mode 100644 index 00000000..7bb6b0f5 --- /dev/null +++ b/runtime/tmagic-form/src/component-group-list.ts @@ -0,0 +1,161 @@ +import type { ComponentGroup } from '@tmagic/editor'; + +export const COMPONENT_GROUP_LIST: ComponentGroup[] = [ + { + title: '容器', + items: [ + { + text: '普通容器', + type: 'container', + data: { + items: [], + }, + }, + { + text: '表格', + type: 'table', + data: { + items: [], + }, + }, + { + text: '组列表', + type: 'group-list', + data: { + items: [], + }, + }, + { + text: '面板', + type: 'panel', + data: { + items: [], + }, + }, + { + text: '行', + type: 'row', + data: { + items: [], + }, + }, + ], + }, + { + title: '表单组件', + items: [ + { + text: '输入框', + type: 'text', + data: { + text: '输入框', + name: 'text', + }, + }, + { + text: '数字输入框', + type: 'number', + data: { + text: '数字输入框', + name: 'number', + }, + }, + { + text: '文本域', + type: 'textarea', + data: { + text: '文本域', + name: 'textarea', + }, + }, + { + text: '链接', + type: 'link', + data: { + text: '链接', + name: 'link', + }, + }, + { + text: '日期', + type: 'datetime', + data: { + text: '日期', + name: 'datetime', + }, + }, + { + text: '时间', + type: 'time', + data: { + text: '时间', + name: 'time', + }, + }, + { + text: '选中器', + type: 'select', + data: { + text: '选中器', + name: 'select', + }, + }, + { + text: '级联选择器', + type: 'cascader', + data: { + text: '级联选择器', + name: 'cascader', + }, + }, + { + text: '开关', + type: 'switch', + data: { + text: '开关', + name: 'switch', + }, + }, + { + text: '多选框', + type: 'checkbox', + data: { + text: '多选框', + name: 'checkbox', + }, + }, + { + text: '多选组', + type: 'checkboxGroup', + data: { + text: '多选组', + name: 'checkboxGroup', + }, + }, + { + text: '单选框', + type: 'radio', + data: { + text: '单选框', + name: 'radio', + }, + }, + { + text: '单选组', + type: 'radioGroup', + data: { + text: '单选组', + name: 'radioGroup', + }, + }, + { + text: '取色器', + type: 'colorPicker', + data: { + text: '取色器', + name: 'colorPicker', + }, + }, + ], + }, +]; diff --git a/runtime/tmagic-form/src/form-config/checkbox.ts b/runtime/tmagic-form/src/form-config/checkbox.ts new file mode 100644 index 00000000..17d9ed7d --- /dev/null +++ b/runtime/tmagic-form/src/form-config/checkbox.ts @@ -0,0 +1,14 @@ +import { createForm } from '@tmagic/form'; + +export default createForm([ + { + name: 'activeValue', + text: '选中时的值', + defaultValue: true, + }, + { + name: 'inactiveValue', + text: '没有选中时的值', + defaultValue: false, + }, +]); diff --git a/playground/src/configs/form-config/common.ts b/runtime/tmagic-form/src/form-config/common.ts similarity index 68% rename from playground/src/configs/form-config/common.ts rename to runtime/tmagic-form/src/form-config/common.ts index 49af25fe..99833d35 100644 --- a/playground/src/configs/form-config/common.ts +++ b/runtime/tmagic-form/src/form-config/common.ts @@ -1,6 +1,6 @@ -import type { FormConfig } from '@tmagic/form'; +import { createForm } from '@tmagic/form'; -export default [ +export default createForm([ { name: 'id', type: 'hidden', @@ -24,4 +24,10 @@ export default [ text: '标签宽度', extra: '表单域标签的的宽度,例如 "50px"。支持 auto。', }, -] as FormConfig; + { + name: 'disabled', + text: '是否禁用', + type: 'switch', + defaultValue: false, + }, +]); diff --git a/runtime/tmagic-form/src/form-config/display.ts b/runtime/tmagic-form/src/form-config/display.ts new file mode 100644 index 00000000..08805f20 --- /dev/null +++ b/runtime/tmagic-form/src/form-config/display.ts @@ -0,0 +1,3 @@ +import { createForm } from '@tmagic/form'; + +export default createForm([]); diff --git a/playground/src/configs/form-config/index.ts b/runtime/tmagic-form/src/form-config/index.ts similarity index 65% rename from playground/src/configs/form-config/index.ts rename to runtime/tmagic-form/src/form-config/index.ts index 439c6a66..fee18134 100644 --- a/playground/src/configs/form-config/index.ts +++ b/runtime/tmagic-form/src/form-config/index.ts @@ -1,13 +1,17 @@ +import type { FormConfig } from '@tmagic/form'; + import checkbox from './checkbox'; import display from './display'; import number from './number'; import switchConfig from './switch'; import text from './text'; -export default { +const configs: Record = { text, checkbox, display, number, switch: switchConfig, }; + +export default configs; diff --git a/playground/src/configs/form-config/number.ts b/runtime/tmagic-form/src/form-config/number.ts similarity index 76% rename from playground/src/configs/form-config/number.ts rename to runtime/tmagic-form/src/form-config/number.ts index 3cbf5e6d..a033c78e 100644 --- a/playground/src/configs/form-config/number.ts +++ b/runtime/tmagic-form/src/form-config/number.ts @@ -1,6 +1,6 @@ -import type { FormConfig } from '@tmagic/form'; +import { createForm } from '@tmagic/form'; -export default [ +export default createForm([ { type: 'number', name: 'min', @@ -20,4 +20,4 @@ export default [ name: 'placeholder', text: 'placeholder', }, -] as FormConfig; +]); diff --git a/runtime/tmagic-form/src/form-config/switch.ts b/runtime/tmagic-form/src/form-config/switch.ts new file mode 100644 index 00000000..08805f20 --- /dev/null +++ b/runtime/tmagic-form/src/form-config/switch.ts @@ -0,0 +1,3 @@ +import { createForm } from '@tmagic/form'; + +export default createForm([]); diff --git a/playground/src/configs/form-config/text.ts b/runtime/tmagic-form/src/form-config/text.ts similarity index 63% rename from playground/src/configs/form-config/text.ts rename to runtime/tmagic-form/src/form-config/text.ts index 4bd3e2ea..725c754f 100644 --- a/playground/src/configs/form-config/text.ts +++ b/runtime/tmagic-form/src/form-config/text.ts @@ -1,6 +1,6 @@ -import type { FormConfig } from '@tmagic/form'; +import { createForm } from '@tmagic/form'; -export default [ +export default createForm([ { name: 'placeholder', text: 'placeholder', @@ -10,7 +10,14 @@ export default [ legend: '后置按钮', type: 'fieldset', labelWidth: '80px', + checkbox: true, + expand: true, items: [ + { + name: 'type', + type: 'hidden', + defaultValue: 'button', + }, { name: 'text', text: '按钮文案', @@ -23,4 +30,4 @@ export default [ }, ], }, -] as FormConfig; +]); diff --git a/runtime/tmagic-form/src/index.ts b/runtime/tmagic-form/src/index.ts new file mode 100644 index 00000000..ae702bd6 --- /dev/null +++ b/runtime/tmagic-form/src/index.ts @@ -0,0 +1,86 @@ +import { createApp, onBeforeUnmount } from 'vue'; +import cssStyle from 'element-plus/dist/index.css?raw'; + +import { editorService, Layout, propsService, uiService } from '@tmagic/editor'; +import MagicForm, { type FormConfig } from '@tmagic/form'; +import type StageCore from '@tmagic/stage'; +import { injectStyle } from '@tmagic/utils'; + +import commonConfig from './form-config/common'; +import App from './App.vue'; +import formConfigs from './form-config'; + +export * from './component-group-list'; + +export const propsConfigs = formConfigs; + +export const canSelect = (el: HTMLElement) => Boolean(el.dataset.magicId); + +export const useRuntime = () => { + const render = (stage: StageCore) => { + injectStyle(stage.renderer.getDocument()!, cssStyle); + injectStyle( + stage.renderer.getDocument()!, + ` + html, + body, + #app { + width: 100%; + height: 100%; + margin: 0; + } + ::-webkit-scrollbar { + width: 0; + } + `, + ); + + const el: HTMLDivElement = globalThis.document.createElement('div'); + el.id = 'app'; + el.style.overflow = 'auto'; + + createApp(App, { + stage, + }) + .use(MagicForm) + .mount(el); + + setTimeout(() => { + uiService.set('showRule', false); + }); + + return el; + }; + + propsService.usePlugin({ + afterFillConfig(config: FormConfig, itemConfig: FormConfig) { + return [ + { + type: 'tab', + items: [ + { + title: '属性', + labelWidth: '80px', + items: [...commonConfig, ...itemConfig], + }, + ], + }, + ]; + }, + }); + + editorService.usePlugin({ + afterGetLayout() { + return Layout.RELATIVE; + }, + }); + + onBeforeUnmount(() => { + propsService.removeAllPlugins(); + editorService.removeAllPlugins(); + }); + + return { + render, + }; +}; diff --git a/runtime/tmagic-form/src/useFormConfig.ts b/runtime/tmagic-form/src/useFormConfig.ts new file mode 100644 index 00000000..60660832 --- /dev/null +++ b/runtime/tmagic-form/src/useFormConfig.ts @@ -0,0 +1,143 @@ +import { computed, nextTick, onBeforeUnmount, reactive, ref } from 'vue'; + +import Core from '@tmagic/core'; +import { type FormConfig, initValue, MForm } from '@tmagic/form'; +import type { Id, MApp, MNode } from '@tmagic/schema'; +import type { RemoveData, RuntimeWindow, UpdateData } from '@tmagic/stage'; +import { getNodePath, replaceChildNode } from '@tmagic/utils'; + +export const useFormConfig = (contentWindow: RuntimeWindow | null) => { + const mForm = ref>(); + + const root = ref(); + const values = ref({}); + const curPageId = ref(); + const selectedId = ref(); + + const config = computed( + () => root.value?.items?.find((item: MNode) => item.id === curPageId.value) || root.value?.items?.[0], + ); + + const formConfig = computed(() => (config.value?.items || []) as FormConfig); + + const app = new Core({ + ua: contentWindow?.navigator.userAgent, + platform: 'editor', + }); + + const resetValues = () => { + initValue(mForm.value?.formState, { + initValues: {}, + config: formConfig.value, + }).then((value) => { + values.value = value; + }); + }; + + const runtimeReadyHandler = ({ data }: any) => { + if (!data.tmagicRuntimeReady) { + return; + } + + contentWindow?.magic?.onRuntimeReady({ + getApp() { + return app; + }, + + updateRootConfig(config: MApp) { + root.value = config; + app?.setConfig(config, curPageId.value); + }, + + updatePageId(id: Id) { + curPageId.value = id; + app?.setPage(id); + }, + + select(id: Id) { + selectedId.value = id; + + if (app?.getPage(id)) { + this.updatePageId?.(id); + } + + const el = document.getElementById(`${id}`); + if (el) return el; + // 未在当前文档下找到目标元素,可能是还未渲染,等待渲染完成后再尝试获取 + return nextTick().then(() => document.getElementById(`${id}`) as HTMLElement); + }, + + add({ config, parentId }: UpdateData) { + if (!root.value) throw new Error('error'); + if (!selectedId.value) throw new Error('error'); + if (!parentId) throw new Error('error'); + + const parent = getNodePath(parentId, [root.value]).pop(); + if (!parent) throw new Error('未找到父节点'); + + if (config.type !== 'page') { + const parentNode = app?.page?.getNode(parent.id); + parentNode && app?.page?.initNode(config, parentNode); + } + + if (parent.id !== selectedId.value) { + const index = parent.items?.findIndex((child: MNode) => child.id === selectedId.value); + parent.items?.splice(index + 1, 0, config); + } else { + // 新增节点添加到配置中 + parent.items?.push(config); + } + + resetValues(); + }, + + update({ config, parentId }: UpdateData) { + if (!root.value || !app) throw new Error('error'); + + const newNode = app.dataSourceManager?.compiledNode(config) || config; + replaceChildNode(reactive(newNode), [root.value], parentId); + + const nodeInstance = app.page?.getNode(config.id); + if (nodeInstance) { + nodeInstance.setData(config); + } + + resetValues(); + }, + + remove({ id, parentId }: RemoveData) { + if (!root.value) throw new Error('error'); + + const node = getNodePath(id, [root.value]).pop(); + if (!node) throw new Error('未找到目标元素'); + + const parent = getNodePath(parentId, [root.value]).pop(); + if (!parent) throw new Error('未找到父元素'); + + if (node.type === 'page') { + app?.deletePage(); + } else { + app?.page?.deleteNode(node.id); + } + + const index = parent.items?.findIndex((child: MNode) => child.id === node.id); + parent.items.splice(index, 1); + + resetValues(); + }, + }); + }; + + contentWindow?.addEventListener('message', runtimeReadyHandler); + + onBeforeUnmount(() => { + contentWindow?.removeEventListener('message', runtimeReadyHandler); + }); + + return { + mForm, + config, + formConfig, + values, + }; +}; diff --git a/runtime/tmagic-form/tsconfig.build.json b/runtime/tmagic-form/tsconfig.build.json new file mode 100644 index 00000000..c9476221 --- /dev/null +++ b/runtime/tmagic-form/tsconfig.build.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": ".", + "declaration": true, + "declarationDir": "types", + "forceConsistentCasingInFileNames": true, + "paths": {}, + }, + "include": [ + "src" + ], +} diff --git a/runtime/tmagic-form/tsconfig.json b/runtime/tmagic-form/tsconfig.json new file mode 100644 index 00000000..6c40cf1e --- /dev/null +++ b/runtime/tmagic-form/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "../..", + }, +} diff --git a/runtime/tmagic-form/vite.config.ts b/runtime/tmagic-form/vite.config.ts new file mode 100644 index 00000000..725c7409 --- /dev/null +++ b/runtime/tmagic-form/vite.config.ts @@ -0,0 +1,45 @@ +/* + * Tencent is pleased to support the open source community by making TMagicEditor available. + * + * Copyright (C) 2023 THL A29 Limited, a Tencent company. 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 { defineConfig } from 'vite'; +import vue from '@vitejs/plugin-vue'; + +import pkg from './package.json'; + +export default defineConfig({ + plugins: [vue()], + + build: { + cssCodeSplit: false, + sourcemap: true, + minify: false, + target: 'esnext', + + lib: { + entry: 'src/index.ts', + name: 'TMagicFormRuntime', + fileName: 'tmagic-form-runtime', + }, + + rollupOptions: { + // 确保外部化处理那些你不想打包进库的依赖 + external(id: string) { + return Object.keys(pkg.dependencies).some((k) => new RegExp(`^${k}`).test(id)); + }, + }, + }, +}); diff --git a/tsconfig.json b/tsconfig.json index 61836b24..98a2511a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,6 +19,7 @@ "paths": { // 内部模块都指向 src/index.ts, 会有更好的代码跳转体验. "@tmagic/*": ["packages/*/src"], + "@tmagic/tmagic-form-runtime": ["runtime/tmagic-form/src"], "@editor/*": ["packages/editor/src/*"], "@form/*": ["packages/form/src/*"], "@data-source/*": ["packages/data-source/src/*"],