diff --git a/docs/src/.vuepress/config.ts b/docs/src/.vuepress/config.ts index c22f38d9..4f0c7d0e 100644 --- a/docs/src/.vuepress/config.ts +++ b/docs/src/.vuepress/config.ts @@ -121,6 +121,7 @@ const sidebar = { children: [ '/tutorial/hello-world', '/tutorial/runtime', + '/tutorial/render', ] }, ] diff --git a/docs/src/api/editor/editor.md b/docs/src/api/editor/editor.md index 4a14da2f..1e9e80e6 100644 --- a/docs/src/api/editor/editor.md +++ b/docs/src/api/editor/editor.md @@ -80,6 +80,13 @@ import { FolderOpened, SwitchButton, Tickets } from '@element-plus/icons'; ::: tip icon使用的是[element-plus icon](https://element-plus.org/zh-CN/component/icon.html) + +也可直接使用url,例如 +```js +{ + icon: 'https://vfiles.gtimg.cn/vupload/20220614/9cc3091655207317835.png' +} +``` ::: ::: warning @@ -119,6 +126,17 @@ import ModListPanel from '../components/sidebars/ModListPanel.vue'; } ``` +::: tip +icon使用的是[element-plus icon](https://element-plus.org/zh-CN/component/icon.html) + +也可直接使用url,例如 +```js +{ + icon: 'https://vfiles.gtimg.cn/vupload/20220614/9cc3091655207317835.png' +} +``` +::: + ### menu - **类型:** [MenuBarData](https://github.com/Tencent/tmagic-editor/blob/master/packages/editor/src/type.ts) @@ -177,6 +195,17 @@ import { ArrowLeft, Coin } from '@element-plus/icons'; } ``` +::: tip +icon使用的是[element-plus icon](https://element-plus.org/zh-CN/component/icon.html) + +也可直接使用url,例如 +```js +{ + icon: 'https://vfiles.gtimg.cn/vupload/20220614/9cc3091655207317835.png' +} +``` +::: + ### render - **类型:** Function diff --git a/docs/src/guide/advanced/coupling.md b/docs/src/guide/advanced/coupling.md index 11c406bc..4fad1de5 100644 --- a/docs/src/guide/advanced/coupling.md +++ b/docs/src/guide/advanced/coupling.md @@ -82,40 +82,26 @@ export default { - ``` -::: tip -在用 vue 实现的 组件中,我们通过 inject 方式来提供核心 app 和高阶组件 hoc。调用联动事件方法时,tmagic-editor是通过组件的 ref,并直接调用当前组件的方法。 -::: - #### react 版本实现 在 react 的实现中,由于tmagic-editor提供的 @tmagic/ui-react 版本是用 hook 实现的。所以组件开发我们也相应的需要使用 hook 方式。 @@ -125,10 +111,8 @@ import React from 'react'; import { useApp } from '@tmagic/ui-react'; function Test({ config }) { - // react 和 vue 实现不同,我们通过 useApp 这个 hook 来提供 app, ref 等核心内容 - // 其中 ref 需要绑定到你的组件上作为 ref。因为一些公共事件会需要使用到你的组件 dom - // 同时这个 ref 也会在tmagic-editor的高级函数钩子中,将你的组件 dom 作为参数提供给自定义钩子 - const { app, ref } = useApp({ + // react 和 vue 实现不同,我们通过 useApp 这个 hook 来提供 app 等核心内容 + const { app } = useApp({ config, // 此处实现事件动作 // 通过向 useApp 这个 hook 提供 methods 方法 @@ -147,7 +131,6 @@ function Test({ config }) { return (
{ const root = window.document.createElement('div'); @@ -268,10 +270,10 @@ const render = async ({ renderer }: StageCore) => { } `; - renderer.iframe.contentDocument.head.appendChild(style); + renderer.iframe?.contentDocument?.head.appendChild(style); - renderer.iframe.contentWindow.magic?.onPageElUpdate(root); - renderer.iframe.contentWindow.magic?.onRuntimeReady({}); + renderer.contentWindow?.magic?.onPageElUpdate(root); + renderer.contentWindow?.magic?.onRuntimeReady({}); }); return root; diff --git a/docs/src/tutorial/render.md b/docs/src/tutorial/render.md new file mode 100644 index 00000000..01731572 --- /dev/null +++ b/docs/src/tutorial/render.md @@ -0,0 +1,282 @@ +# 3.[DSL](../guide/conception.md#dsl) 解析渲染 + +tmagic 提供了 vue3/vue2/react 三个版本的解析渲染组件,可以直接使用 + +[@tmagic/ui](https://www.npmjs.com/package/@tmagic/ui) + +[@tmagic/ui-vue2](https://www.npmjs.com/package/@tmagic/ui-vue2) + +[@tmagic/ui-react](https://www.npmjs.com/package/@tmagic/ui-react) + +接下来是已vue3为基础,来讲述如何实现一个[@tmagic/ui](https://www.npmjs.com/package/@tmagic/ui) + +## 准备工作 + +### 创建项目 + +将[上一教程](./runtime.md)中的[editor-runtime](https://github.com/jia000/tmagic-tutorial/tree/master/course2/editor-runtime)和[hello-editor](https://github.com/jia000/tmagic-tutorial/tree/master/course2/hellow-editor)复制过来 + +## 基础概念 + +### 节点(Node) + +每一个组件最终都是由一个节点来描述,每个节点至少拥有id,type两个属性 + +id: 节点的唯一标识,不可重复 + +type: 节点的类型,有业务自行定义 + +### 容器(Container) + +容器也是节点的一种,容器可以包含多个节点并且是保存在items属性下 + +items: 容器下包含的节点组成的数组,items中不能有page,app + +### 页面(Page) + +页面是容器的一种,type固定为page,items中不能有page + +### 根(Root) + +根节点也是一个容器,type固定为app,items只能是page + +## 实现 + +创建hello-ui目录 + +``` +. +└─editor-runtime +└─hello-editor +└─hello-ui +``` + +### 渲染节点 + +在hello-ui下创建 Component.vue 文件 + +由于节点的type是由业务自行定义的,所以需要使用动态组件渲染,在vue下可以使用[component](https://cn.vuejs.org/v2/api/#component)组件来实现 + +[component](https://cn.vuejs.org/v2/api/#component) 是通过is参数来决定哪个组件被渲染,所以将type与组件做绑定 + +例如有组件 HelloWorld,可以将组件全局注册 + +```js +app.component('hello-world', HelloWorld); +``` + +然后将'hello-world'作为type,那么is="hello-world"就会渲染 HelloWorld 组件 + +为了让组件渲染出来的dom能被编辑器识别到,还需要将节点的id作为dom的id + +```vue + + + +``` + +接下来就需要解析节点的样式,在tmagic/editor中默认会将样式配置保存到节点的style属性中,如果自行定义到了其他属性,则已实际为准 + +解析style需要注意几个地方 + +1. 数字 + +css中的数值有些是需要单位的,例如px,有些是不需要的,例如opacity + +在tmagic/editor中,默认都是不带单位的,所以需要将需要单位的地方补齐单位 + +这里做补齐px处理,如果需要做屏幕大小适应, 可以使用rem或者vw,这个可以根据自身需求处理。 + +2. url + +css中的[url](https://developer.mozilla.org/zh-CN/docs/Web/CSS/url)需要是用url(),所以当值为url时,需要转为url(xxx) + +3. transform + +[transform](https://developer.mozilla.org/zh-CN/docs/Web/CSS/transform)属性可以指定为关键字值none 或一个或多个transform-function值。 + +```ts +const fillBackgroundImage = (value: string) => { + if (value && !/^url/.test(value) && !/^linear-gradient/.test(value)) { + return `url(${value})`; + } + return value; +}; + +const style = computed(() => { + if (!props.config.style) { + return {}; + } + + const results: Record = {}; + + const whiteList = ['zIndex', 'opacity', 'fontWeight']; + Object.entries(props.config.style).forEach(([key, value]) => { + if (key === 'backgroundImage') { + value && (results[key] = fillBackgroundImage(value)); + } else if (key === 'transform' && typeof value !== 'string') { + results[key] = Object.entries(value as Record) + .map(([transformKey, transformValue]) => { + let defaultValue = 0; + if (transformKey === 'scale') { + defaultValue = 1; + } + return `${transformKey}(${transformValue || defaultValue})`; + }) + .join(' '); + } else if (!whiteList.includes(key) && value && /^[-]?[0-9]*[.]?[0-9]*$/.test(value)) { + results[key] = `${value}px`; + } else { + results[key] = value; + } + }); + + return results; +}); +``` + +### 渲染容器 + +容器与普通节点的区别,就是需要多一个items的解析 + +新增Container.vue文件 + +```vue + + + +``` + +### 渲染页面 + +页面就是容器,之所以单独存在,是页面会自己的方法,例如reload等 + +Page.vue文件 + +```vue + + + +``` + +## 在runtime中使用 hello-ui + +删除editor-runtime/src/ui-page.vue + +将App.vue中的ui-page改成hello-ui中的Page + +```vue + + + +``` + +在editor-runtime main.ts中注册HelloWorld + +```ts +import { createApp } from 'vue'; + +import type { Magic } from '@tmagic/stage'; + +// eslint-disable-next-line +import { HelloWorld } from 'hello-ui'; + +import App from './App.vue'; + +declare global { + interface Window { + magic?: Magic; + } +} + +const app = createApp(App); + +app.component('hello-world', HelloWorld); + +app.mount('#app'); + +``` + +[源码](https://github.com/jia000/tmagic-tutorial/tree/master/course3) diff --git a/docs/src/tutorial/runtime.md b/docs/src/tutorial/runtime.md index da1d3455..dc6fba22 100644 --- a/docs/src/tutorial/runtime.md +++ b/docs/src/tutorial/runtime.md @@ -19,6 +19,12 @@ cd editor-runtime 删除src/components/HelloWorld.vue +按钮需要用的ts types依赖 + +```bash +npm install --save @tmagic/schema @tmagic/stage +``` + ## 实现runtime 将hello-editor中的render函数实现移植到runtime项目中 @@ -145,40 +151,58 @@ devServer: { 在App.vue中通过监听message,来准备获取magic注入时机,然后调用magic.onRuntimeReady,示例代码如下 +> 这里可能会出现editor抛出message的时候,runtime还没有执行到监听message的情况 +> 编辑器只在iframe onload事件中抛出message +> 如果出现runtime中接收不到message的情况,可以尝试在onMounted的时候调用magic.onRuntimeReady + ```ts -const root = ref(); +import type { Magic } from '@tmagic/stage'; + +declare global { + interface Window { + magic?: Magic; + } +} +``` + + +```ts +import type { RemoveData, UpdateData } from '@tmagic/stage'; +import type { Id, MApp, MNode } from '@tmagic/schema'; + +const root = ref(); window.addEventListener('message', ({ data }) => { if (!data.tmagicRuntimeReady) { return; } - (window as any).magic?.onRuntimeReady({ + window.magic?.onRuntimeReady({ /** 当编辑器的dsl对象变化时会调用 */ - updateRootConfig(config: any) { + updateRootConfig(config: MApp) { root.value = config; }, /** 当编辑器的切换页面时会调用 */ - updatePageId(id: string) { - page.value = root.value?.items?.find((item: any) => item.id === id); + updatePageId(id: Id) { + page.value = root.value?.items?.find((item) => item.id === id); }, /** 新增组件时调用 */ - add({ config }: any) { + add({ config }: UpdateData) { const parent = config.type === 'page' ? root.value : page.value; parent.items?.push(config); }, /** 更新组件时调用 */ - update({ config }: any) { - const index = page.value.items?.findIndex((child: any) => child.id === config.id); + update({ config }: UpdateData) { + const index = page.value.items?.findIndex((child: MNode) => child.id === config.id); page.value.items.splice(index, 1, reactive(config)); }, /** 删除组件时调用 */ - remove({ id }: any) { - const index = page.value.items?.findIndex((child: any) => child.id === id); + remove({ id }: RemoveData) { + const index = page.value.items?.findIndex((child: MNode) => child.id === id); page.value.items.splice(index, 1); }, }); @@ -194,11 +218,11 @@ window.addEventListener('message', ({ data }) => { watch(page, async () => { // page配置变化后,需要等dom更新 await nextTick(); - (window as any).magic.onPageElUpdate(pageComp.value?.$el); + window?.magic.onPageElUpdate(pageComp.value?.$el); }); ``` -以上就是一个简单runtime实现,以及与编辑的交互,这是一个不完善的实现,但是其中已经几乎覆盖所有需要关心的内容 +以上就是一个简单runtime实现,以及与编辑的交互,这是一个不完善的实现(会发现组件再画布中无法自由拖动,是因为没有完整的解析style),但是其中已经几乎覆盖所有需要关心的内容 当前教程中实现了一个简单的page,tmagic提供了一个比较完善的实现,将在下一节介绍