amis/docs/zh-CN/extend/editor.md
RUNZE LU d27948f0f5
feat: CRUD & Form 脚手架构建优化, 列表可视化设计优化; chore: DSBuilder迁移至amis-editor (#8003)
* feat: CRUD & Form 脚手架构建优化; chore: DSBuilder迁移至amis-editor (#7531)

* tpl最大显示行数支持

* 编辑器组件通用假数据支持

* each循环次数支持

* 列表可视化设计优化

* fix: 数据源构造器 & Form & CRUD2 & Service相关问题修复 (#8002)

* chore: 修复正则报错

* chore: 修复正则报错-02

* chore: 更新 CRUD 组件 Card 模式单测快照

* chore: 优化假数据merge

---------

Co-authored-by: zhangtao07 <zhang.tao.1006@163.com>
2023-09-06 21:30:21 +08:00

732 lines
27 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
title: 可视化编辑器
---
目前 amis 可视化编辑器也作为单独的 npm 包发布了出来,可以通过 npm 安装使用。
在线体验https://aisuda.github.io/amis-editor-demo
示例代码https://github.com/aisuda/amis-editor-demo
## 使用
目前有两个 npm 包:`amis-editor` 和 `amis-editor-core`
- `amis-editor-core` 包含了少量底层必要的功能实现,里面没有包含 amis 内置渲染器插件的任何实现。
- `amis-editor` 基于 `amis-editor-core` 实现了 amis 内置的所有渲染器的可视化编辑器插件。
如果你没有使用 amis 内置渲染器,推荐只使用 `amis-editor-core`,否则推荐使用 `amis-editor`。这里主要介绍 `amis-editor`, `amis-editor-core` 的使用方式是一样的。
```
npm i amis-editor
```
通过 `npm` 安装完后,在 React 项目中这样使用:
```jsx
import {Editor} from 'amis-editor';
render() {
return (
<Editor
{...props}
/>
)
}
```
## 属性说明
- `value: Schema` amis json 配置,比如:`{type: 'page', body: 'contents...'}`
- `onChange: (value: Schema) => void` 当编辑器修改的时候会触发。
- `preview?: boolean` 是否为预览模式。
- `autoFocus?: boolean` 是否自动聚焦第一个可编辑的组件。
- `isMobile?: boolean` 是否为移动端模式,当为移动模式时,将采用 iframe 来预览,需要配置 `iframeUrl`
- `$schemaUrl?: string` 提供 amis 产出的 schema.json 的访问路径。主要用来给代码编辑模式提供属性提示信息。
- `className?: string` 额外加个 css 类名,辅助样式定义。
- `schemas?: JSONSchemaObject` 用来定义有哪些全局变量,辅助编辑器格式化绑定全局数据。
- `theme?: string` amis 主题
- `schemaFilter?: (schema: any, isPreview?: boolean) => any` 配置过滤器。可以用来实现 api proxy比如原始配置中请求地址是 `http://baidu.com` 如果直接给编辑器预览请求,很可能会报跨域,可以自动转成 `/api/proxy?_url=xxxx`,走 proxy 解决。
- `amisEnv?: any` 这是是给 amis 的 Env 对象,具体请前往 [env 说明](../start/getting-started#env)
- `disableBultinPlugin?: boolean` 是否禁用内置插件
- `disablePluginList?: Array<string> | (id: string, plugin: PluginClass) => boolean` 禁用插件列表
- `plugins?: Array<PluginClass>` 额外的自定义插件,具体看下面的说明。
- `isHiddenProps?: (key: string) => boolean` 是否为隐藏属性,隐藏属性是在配置中有,但是在代码编辑器中不可见。
- `actionOptions?: any` 事件动作面板相关配置
- `onInit?: ( event: PluginEvent<EventContext & {data: EditorManager;}>) => void` 初始化事件
- `onActive?: (event: PluginEvent<ActiveEventContext>) => void` 点选事件
- `beforeInsert?: (event: PluginEvent<InsertEventContext>) => false | void` 插入节点前事件
- `afterInsert?: (event: PluginEvent<InsertEventContext>) => void;` 插入节点后事件
- `beforeUpdate?: (event: PluginEvent<ChangeEventContext>) => false | void;` 面板里面编辑修改前的事件。可通过 event.preventDefault() 阻止。
- `afterUpdate?: (event: PluginEvent<ChangeEventContext>) => false | void;` 面板里面编辑修改后的事件。
- `beforeReplace?: (event: PluginEvent<ReplaceEventContext>) => false | void;` 更新渲染器前的事件,或者右键粘贴配置。可通过 event.preventDefault() 阻止。
- `afterReplace?: (event: PluginEvent<ReplaceEventContext>) => void` 更新渲染器后的事件,或者右键粘贴配置。
- `beforeMove?: (event: PluginEvent<MoveEventContext>) => false | void` 移动节点前触发,包括上移,下移。可通过 event.preventDefault() 阻止。
- `afterMove?: (event: PluginEvent<MoveEventContext>) => void` 移动节点后触发,包括上移,下移。
- `beforeDelete?: (event: PluginEvent<DeleteEventContext>) => false | void` 删除前触发。可通过 event.preventDefault() 阻止。
- `afterDelete?: (event: PluginEvent<DeleteEventContext>) => void` 删除后触发
- `beforeResolveEditorInfo?: ( event: PluginEvent<RendererInfoResolveEventContext> ) => false | void` 收集渲染器信息前触发。可通过 event.preventDefault() 阻止,如果阻止了,则目标组件不可编辑。
- `afterResolveEditorInfo?: ( event: PluginEvent<RendererInfoResolveEventContext> ) => void` 收集渲染器信息后触发
- `beforeResolveJsonSchema?: ( event: PluginEvent<RendererJSONSchemaResolveEventContext> ) => false | void` 基于渲染器获取配置的 jsonSchema 信息。可通过 event.preventDefault() 阻止。
- `afterResolveJsonSchema?: ( event: PluginEvent<RendererJSONSchemaResolveEventContext> ) => void` 基于渲染器获取配置的 jsonSchema 信息。
- `onDndAccept?: (event: PluginEvent<DragEventContext>) => false | void` 当将组件拖入某个容器时触发,用来决定接收不接收本次拖拽。
- `onBuildPanels?: (event: PluginEvent<BuildPanelEventContext>) => void` 构建右侧面板的事件,可以干预右侧面板的生成,可以新增面板。
- `onBuildContextMenus?: (event: PluginEvent<ContextMenuEventContext>) => void` 构建上下文菜单的事件
- `onBuildToolbars?: (event: PluginEvent<BaseEventContext>) => void` 构建点选框顶部 icon 按钮事件
- `onSelectionChange?: (event: PluginEvent<SelectionEventContext>) => void` 当点选发生变化的事件
- `onPreventClick?: ( event: PluginEvent<PreventClickEventContext> ) => false | void` 禁用内部点击事件的事件,可以用来控制是否禁用编辑态内置组件的一些点选能力。
- `onWidthChangeStart?: ` 当渲染器标记为 `widthMutable` 时会触发宽度变动事件
- `onHeightChangeStart?: ` 当渲染器标记为 `heightMutable` 时会触发宽度变动事件
- `onSizeChangeStart?: ` 当渲染器同时标记为 `widthMutable``heightMutable` 时会触发变动事件
## 自定义插件
开始之前,需要先自定义一个 amis 渲染器,然后再添加编辑器插件,让这个自定义渲染器可以在编辑器中可编辑。
```jsx
import React from 'react';
import {Renderer} from 'amis';
@Renderer({
type: 'my-renderer',
name: 'my-renderer'
})
export default class MyRenderer extends React.Component<MyRendererProps> {
static defaultProps = {
target: 'world'
};
render() {
const {target} = this.props;
return <p>Hello {target}!</p>;
}
}
```
通过以上代码amis 配置中通过 `type` 指定为 `my-renderer` 即可启用此组件。
接下来添加编辑器插件,添加插件的方式有两种。
- registerEditorPlugin 注册全局插件。
- 不注册,调用 <Editor> 的时候时候通过 plugins 属性传入。
效果都一样,重点还是怎么写个 Plugin示例
```jsx
import {BasePlugin} from 'amis-editor';
export class MyRendererPlugin extends BasePlugin {
// 这里要跟对应的渲染器名字对应上
// 注册渲染器的时候会要求指定渲染器名字
rendererName = 'my-renderer';
// 暂时只支持这个,配置后会开启代码编辑器
$schema = '/schemas/UnkownSchema.json';
// 用来配置名称和描述
name = '自定义渲染器';
description = '这只是个示例';
// tag决定会在哪个 tab 下面显示的
tags = ['自定义', '表单项'];
// 图标
icon = 'fa fa-user';
// 用来生成预览图的
previewSchema = {
type: 'my-renderer',
target: 'demo'
};
// 拖入组件里面时的初始数据
scaffold = {
type: 'my-renderer',
target: '233'
};
// 右侧面板相关
panelTitle = '自定义组件';
panelBody = [
{
type: 'tabs',
tabsMode: 'line',
className: 'm-t-n-xs',
contentClassName: 'no-border p-l-none p-r-none',
tabs: [
{
title: '常规',
body: [
{
name: 'target',
label: 'Target',
type: 'input-text'
}
]
},
{
title: '外观',
body: []
}
]
}
];
}
```
定义好 plugin 后,可以有两种方式启用。
```jsx
// 方式 1注册默认插件所有编辑器实例都会自动实例话。
import {registerEditorPlugin} from 'amis-editor';
registerEditorPlugin(MyRendererPlugin);
// 方式2只让某些编辑器启用
() => <Editor plugins={[MyRendererPlugin]} />;
```
![editor-plugin](../../../examples/static/editor-plugin.png)
## 工作原理
编辑器在渲染 amis 配置的时候,会把所有的 json配置 节点都自动加个 `$$id` 唯一 id。然后复写了 `rendererResolver` 方法。某个节点 {type: 'xxxx'} 在找到对应 amis 组件渲染前,都会调用这个方法。
这个方法会在渲染之前,基于 schema、渲染器信息通过插件去收集编辑器信息如果收集到了会额外的通过一个 `Wrapper` 包裹。这个 `Wrapper` 主要是自动把 `$$id` 写入到 dom 的属性上`data-editor-id="$$id"`。这样鼠标点击的时候,能够根据 dom 上的标记知道是哪个 json 节点,同时根据渲染器编辑器信息,能够生成对应的配置面板,并把对应 json 的节点做配置修改。
有些组件是带区域的,所以除了 dom 上标记节点信息外,还需要标记区域信息。节点能够通过 `Wrapper` 自动包裹来实现,但是区域则不能,这个要去分析组件本身是怎么实现。最终目的是要通过 `RegionWrapper` 去包裹对应 JSX.Element 来完成标记。这个 `RegionWrapper` 会自动完成 dom 的标记 `data-region="xxx" data-region-host="$$id"`,这样点击到这个 dom 的时候,能知道是哪个组件的哪个区域,这样就能往里面拖入新组件。
左侧的组件列表主要是将收集到的渲染器编辑器信息做个汇总展示,可拖入到指定区域内。
## 注册渲染器信息
如果想要渲染器在编辑器里面可点选,必须有插件提供这个渲染器的信息,这样才会被 `Wrapper` 包裹,才会在对应的 dom 上带上标记,才能点选。
在插件中可以通过实现 `getRendererInfo` 方法来注册渲染器信息,如果某个插件设置了 `rendererName``name` 属性,同时它继承 `BasePlugin` 的,则会自动完成注册逻辑。
```ts
/**
* 如果配置里面有 rendererName 自动返回渲染器信息。
* @param renderer
*/
getRendererInfo({
renderer,
schema
}: RendererInfoResolveEventContext): BasicRendererInfo | void {
const plugin: PluginInterface = this;
if (
schema.$$id &&
plugin.name &&
plugin.rendererName &&
plugin.rendererName === renderer.name // renderer.name 会从 renderer.type 中取值
) {
let curPluginName = plugin.name;
// 复制部分信息出去
return {
name: curPluginName,
regions: plugin.regions,
patchContainers: plugin.patchContainers,
// wrapper: plugin.wrapper,
vRendererConfig: plugin.vRendererConfig,
wrapperProps: plugin.wrapperProps,
wrapperResolve: plugin.wrapperResolve,
filterProps: plugin.filterProps,
$schema: plugin.$schema,
renderRenderer: plugin.renderRenderer,
multifactor: plugin.multifactor,
scaffoldForm: plugin.scaffoldForm,
disabledRendererPlugin: plugin.disabledRendererPlugin,
isBaseComponent: plugin.isBaseComponent,
rendererName: plugin.rendererName
};
}
}
```
`RendererInfoResolveEventContext` 主要包含以下信息:
- `schema` 渲染器的配置
- `schemaPath` 渲染器在整个配置中的路径信息
- `renderer` 渲染器信息,即注册 amis 渲染器的时候注册的渲染器信息
插件中可以基于这些信息来决定要不要注册编辑器插件,如果注册了则此渲染器可在编辑器中点选。可注册的信息主要包含:
- `name: string` 渲染器名字,决定点选高亮框的名称显示
- `searchKeywords?: string` 组件关键字,用来辅助组件列表搜索
- `description?: string` 在组件列表中展示有用
- `docLink?: string` 组件文档链接
- `previewSchema?: any` 用来生成预览图
- `tags ?:string | Array<string>` 分类, 决定会在哪个 tab 下面显示的
- `scaffold ?: any` 当编辑讲此组件拖入时,默认的配置项是啥
- `scaffolds ?: Array<any>` 脚手架也可以是多个,比如 Grid 组件,两栏,三栏组件都是用 grid 构建的,只是拖入时的初始配置不一样。
- `$schema?: string` json schema 定义。如: `/schemas/UnkownSchema.json` 目前这个不支持自定义,只有内置渲染器才有这些信息。
- `isBaseComponent?: boolean` 是否为内置渲染器,决定组建列表出现在内置 tab 下还是自定义 tab 下。
- `disabledRendererPlugin?: boolean` 新增属性,用于判断是否出现在组件面板中,默认为 false为 true 则不展示
- `regions?: Array<RegionConfig>` 定义这个组件一共有哪些区域比如页面组件包含的区域有aside、body、toolbar 等。
- `patchContainers?: Array<string>` 哪些容器属性需要自动转成数组的。如果不配置默认就从 regions 里面读取。
- `overrides?: { [propName: string]: Function;}` 用来复写渲染器原型链上的方法,通常不需要这个。下面单独的篇章介绍
- `vRendererConfig?: VRendererConfig` 虚拟渲染器的配置项,有时候需要给那些并不是渲染器的组件添加点选编辑功能。 比如: Tabs 下面的 Tab, 这个并不是个渲染器,但是需要可以点选修改内容。
- `wrapperResolve?: (dom: HTMLElement) => HTMLElement | Array<HTMLElement>` 返回哪些 dom 节点,需要自动加上 data-editor-id 属性, 目前只有 TableCell 里面用到了,就它需要同时给某一列下所有 td 都加上那个属性。
- `wrapperProps?: Record<string, any>` 默认下发哪些属性,如果要动态下发,请使用 filterProps, 比如table 渲染器,默认下发 resizeable: false, 这样编辑的时候就不会出现列的宽度可调整功能。这个是运行态的功能,不应该出现在编辑态里面。
- `filterProps?: (props: any, node: EditorNodeType) => Record<string, any>` 修改一些属性,一般用来干掉 $$id或者渲染假数据, 这样它的孩子节点就不能直接点选编辑了,比如 Combo。
- `renderRenderer?: (props: any, info: RendererInfo) => JSX.Element` 有些没有视图的组件,可以自己输出点内容,否则没办法点选编辑。
- `multifactor?: boolean` 是否有多重身份?比如 CRUD 即是 CRUD 又可能是 Table表格的列即是表格列也可能是其他文本框。 配置了这个后会自动添加多个 Panel 面板来编辑。
- `scaffoldForm?: ScaffoldForm` 右键的时候是否出现重新构建,靠这个。同时首次新增此渲染器的时候会出现一个脚手架弹窗。下面会有单独内容介绍。
## 如何定义右侧配置面板
当点选某个组件的时候,编辑器内部会触发面板构建动作,每个插件都可以通过实现 `buildEditorPanel` 来插入右侧面板。
```tsx
/**
* 配置了 panelControls 自动生成配置面板
* @param context
* @param panels
*/
buildEditorPanel(
context: BuildPanelEventContext,
panels: Array<BasicPanelItem>
) {
panels.push({
key: 'xxxx',
title: '设置',
render: () => {
return <div>面板内容</div>
}
})
}
```
![editor-panel](../../../examples/static/editor-panel.png)
通常右侧面板都是表单配置,使用 amis 配置就可以完成。所以推荐的做法是,直接在这个插件上面定义 `panelBody` 或者 `panelBodyCreator` 即可。
```js
panelBody = [
{
type: 'tabs',
tabsMode: 'line',
className: 'm-t-n-xs',
contentClassName: 'no-border p-l-none p-r-none',
tabs: [
{
title: '常规',
body: [
{
name: 'target',
label: 'Target',
type: 'input-text'
}
]
},
{
title: '外观',
body: []
}
]
}
];
```
![editor-panel2](../../../examples/static/editor-panel2.png)
`panelBodyCreator` 相对于 `panelBody` 的区别是,可以基于一些上下文信息来构建不同的表单。比如在表单里面的按钮,和在表单外面的按钮配置项不一样。
```js
panelBodyCreator = context => {
console.log(context);
return [
{
type: 'tabs',
tabsMode: 'line',
className: 'm-t-n-xs',
contentClassName: 'no-border p-l-none p-r-none',
tabs: [
{
title: '常规',
body: [
{
name: 'target',
label: 'Target',
type: 'input-text'
}
]
},
{
title: '外观',
body: []
}
]
}
];
};
```
`context` 中主要包含:
- `selections` 当前选中的渲染器,可能是多个
- `node` 节点信息
- `schema` 当前组件配置
- `info` 注册的渲染器编辑器信息
## 如何扩充渲染器容器配置
开始之前请先阅读 [工作原理](#工作原理),如果是容器组件,还需要在对应 React 虚拟 dom 前包裹 `RegionWrapper`, 来完成 dom 标记。如果在注册编辑器信息的时候定义了 `regions` 信息,则会根据这个信息,自动完成 `RegionWrapper` 包裹。
这里先看简单的情况,比如 `container` 组件。它在 amis 大概是这样实现的容器功能。通过 `this.props.render('body', schema)` 来实现的容器功能。
```tsx
renderBody(): JSX.Element | null {
const {
children,
body,
render,
classnames: cx,
bodyClassName,
disabled
} = this.props;
return (
<div className={cx('Container-body', bodyClassName)}>
{(render('body', body as any, {disabled}) as JSX.Element)}
</div>
);
}
```
在插件中像这样定义 `regions` 即可使得 `container` 有了 `body` 这个 region。
```ts
regions: Array<RegionConfig> = [
{
key: 'body',
label: '内容区'
}
];
```
插件内部会根据这个这个信息,自动在 `render('body', body as any, {disabled})` 的地方包裹个 `RegionWrapper`。这种方式主要是通过篡改 `this.props.render` 方法实现的。
再看个复杂点的情况如 `Form``actions` 区块输出。
```tsx
renderFooter() {
const actions = this.buildActions();
if (!actions || !actions.length) {
return null;
}
const {
store,
render,
classnames: cx,
showErrorMsg,
showLoading,
show
} = this.props;
return (
<div className={cx('Modal-footer')}>
{(showLoading !== false && store.loading) ||
(showErrorMsg !== false && store.error) ? (
<div className={cx('Dialog-info')} key="info">
{showLoading !== false ? (
<Spinner size="sm" key="info" show={store.loading} />
) : null}
{store.error && showErrorMsg !== false ? (
<span className={cx('Dialog-error')}>{store.msg}</span>
) : null}
</div>
) : null}
{actions.map((action, key) =>
render(`action/${key}`, action, {
data: store.formData,
onAction: this.handleAction,
key,
disabled: action.disabled || store.loading || !show
})
)}
</div>
);
}
```
像这个区域,它应该包裹在 `.Modal-footer` 里面,没办法通过第一种方式实现。所以第二种配置方式是:
```ts
regions: Array<RegionConfig> = [
{
key: 'actions',
label: '按钮组',
renderMethod: 'renderFooter',
wrapperResolve: dom => dom
}
];
```
通过 `renderMethod` 信息去篡改渲染器React Component的原型链在这个方法里面自动包裹 `RegionWrapper`。包裹也有多种策略,有时候要包裹在外面,有时要包裹在第一个虚拟 dom 里面。
更多配置信息请参考以下 `RegionConfig` 信息
- `key: string` 简单情况,如果区域直接用的 render('region', subSchema),这种只需要配置 key 就能简单插入 Region 节点。
- `label: string` 区域用来显示的名字。
- `placeholder?: string` 区域占位字符,用于提示
- `matchRegion?: (elem: JSX.Element | undefined | null, component: JSX.Element ) => boolean` 对于复杂的控件需要用到这个配置。如果配置了,则遍历 react dom 直到目标节点调换成 Region 节点,如果没有配置这个,但是又配置了 renderMethod 方法,那就直接将 renderMethod 里面返回的 react dom 直接包一层 Region
- `renderMethod?: string` 指定要覆盖哪个方法。
- `rendererName?: string` 通常是 hack 当前渲染器,单有时候当前渲染器其实是组合的别的渲染器。如果要篡改别的渲染器,则通过渲染器名字指定。
- `insertPosition?: 'outter' | 'inner'` 当配置 renderMethod 的时候会自动把 Region 插入进去。 默认是 outter 模式,有时候可能需要配置成 inner 比如 renderMethod 为 render 的时候。
- `optional?: boolean` 是否为可选容器,如果是可选容器,不会强制自动创建成员
- `renderMethodOverride?: (regions: Array<RegionConfig>, insertRegion: (component: JSX.Element, dom: JSX.Element, regions: Array<RegionConfig>, info: RendererInfo, manager: EditorManager) => JSX.Element ) => Function` 有时候有些包括是需要其他条件的,所以要自己写包裹逻辑。比如 Panel 里面的 renderBody
- `wrapper?: React.ComponentType<RegionWrapperProps>` 用来指定用什么组件包裹,默认是 RegionWrapper
- `wrapperResolve?: (dom: HTMLElement) => HTMLElement` 返回需要添加 data-region 的 dom 节点。
- `modifyGhost?: (ghost: HTMLElement, schema?: any) => void` 当拖入到这个容器时,是否需要修改一下 ghost 结构?
- `dndMode?: string` dnd 拖拽模式。比如 table 那种需要配置成 position-h
- `accept?: (json: any) => boolean` 可以用来判断是否允许拖入当前节点。
## 如何定义编辑器脚手架
如果希望拖入组件的时候,弹出个配置框,基于用户不同的配置,生成不同的初始数据。则这里需要用到 `scaffoldForm` 配置。
```tsx
scaffoldForm = {
title: '标题',
body: [
{
name: 'target',
label: 'Target',
type: 'input-text'
}
]
};
```
![editor-scaffold-form](../../../examples/static/editor-scaffold-form.png)
可用配置
- `title` 脚手架框的标题
- `body` 表单项配置,参考 amis 的 form 配置
- `mode` 表单默认展示方式,参考 amis 的 form 配置
- `size` 弹窗大小,参考 amis 的 dialog 配置
- `initApi` 初始化接口
- `api` 提交接口
- `validate` 整体验证钩子
- `pipeIn?: (value: any) => any` schema 配置转脚手架配置
- `pipeOut?: (value: any) => any` 脚手架配置转 schema 配置
- `canRebuild?: boolean` 是否允许重新构建
## 如何构建点选框顶部菜单
插件中定义 `buildEditorToolbar` 方法即可添加点选框顶部菜单
```tsx
buildEditorToolbar(context: BaseEventContext, toolbars: Array<BasicToolbarItem>) {
toolbars.push({
iconSvg: 'left-arrow-to-left',
tooltip: '向前插入组件',
// level: 'special',
placement: 'bottom',
// placement: vertical ? 'bottom' : 'right',
// className: vertical
// ? 'ae-InsertBefore is-vertical'
// : 'ae-InsertBefore',
onClick: () =>
this.manager.showInsertPanel(
regionNode.region,
regionNode.id,
regionNode.preferTag,
'insert',
undefined,
id
)
});
}
```
## 如何构建上下文功能菜单
插件中定义 `buildEditorContextMenu` 方法即可添加上下文功能菜单
```tsx
buildEditorContextMenu(
{id, schema, region, selections}: ContextMenuEventContext,
menus: Array<ContextMenuItem>
) {
menus.push({
label: '重复一份',
icon: 'copy-icon',
disabled: selections.some(item => !item.node.duplicatable),
onSelect: () => manager.duplicate(selections.map(item => item.id))
});
}
```
## 如何让渲染器可通过拖拽调整宽高
首先组件需要支持宽高设置,为了演示效果,将之前的 `my-renderer` 改成如下代码:
```tsx
@Renderer({
type: 'my-renderer',
name: 'my-renderer'
})
export class MyRenderer extends React.Component {
static defaultProps = {
target: 'world'
};
render() {
const {target, width, height} = this.props;
return (
<p style={{width: width || 'auto', height: height || 'auto'}}>
Hello {target}!
</p>
);
}
}
```
然后插件中加入以下代码即可完成拖拽调整宽高
```tsx
onActive(event: PluginEvent<ActiveEventContext>) {
const context = event.context;
if (context.info?.plugin !== this || !context.node) {
return;
}
const node = context.node!;
node.setHeightMutable(true);
node.setWidthMutable(true);
}
onWidthChangeStart(
event: PluginEvent<
ResizeMoveEventContext,
{
onMove(e: MouseEvent): void;
onEnd(e: MouseEvent): void;
}
>
) {
return this.onSizeChangeStart(event, 'horizontal');
}
onHeightChangeStart(
event: PluginEvent<
ResizeMoveEventContext,
{
onMove(e: MouseEvent): void;
onEnd(e: MouseEvent): void;
}
>
) {
return this.onSizeChangeStart(event, 'vertical');
}
onSizeChangeStart(
event: PluginEvent<
ResizeMoveEventContext,
{
onMove(e: MouseEvent): void;
onEnd(e: MouseEvent): void;
}
>,
direction: 'both' | 'vertical' | 'horizontal' = 'both'
) {
const context = event.context;
const node = context.node;
if (node.info?.plugin !== this) {
return;
}
const resizer = context.resizer;
const dom = context.dom;
const frameRect = dom.parentElement!.getBoundingClientRect();
const rect = dom.getBoundingClientRect();
const startX = context.nativeEvent.pageX;
const startY = context.nativeEvent.pageY;
event.setData({
onMove: (e: MouseEvent) => {
const dy = e.pageY - startY;
const dx = e.pageX - startX;
const height = Math.max(50, rect.height + dy);
const width = Math.max(100, Math.min(rect.width + dx, frameRect.width));
const state: any = {
width,
height
};
if (direction === 'both') {
resizer.setAttribute('data-value', `${width}px x ${height}px`);
} else if (direction === 'vertical') {
resizer.setAttribute('data-value', `${height}px`);
delete state.width;
} else {
resizer.setAttribute('data-value', `${width}px`);
delete state.height;
}
node.updateState(state);
requestAnimationFrame(() => {
node.calculateHighlightBox();
});
},
onEnd: (e: MouseEvent) => {
const dy = e.pageY - startY;
const dx = e.pageX - startX;
const height = Math.max(50, rect.height + dy);
const width = Math.max(100, Math.min(rect.width + dx, frameRect.width));
const state: any = {
width,
height
};
if (direction === 'vertical') {
delete state.width;
} else if (direction === 'horizontal') {
delete state.height;
}
resizer.removeAttribute('data-value');
node.updateSchema(state);
requestAnimationFrame(() => {
node.calculateHighlightBox();
});
}
});
}
```
![editor-resize](../../../examples/static/editor-resize.png)
## 如何开启快速配置
![editor-inline-edit](../../../examples/static/editor-inline-edit.png)
直接配置 `popOverBody` 即可
```tsx
popOverBody = [
{
name: 'target',
label: 'Target',
type: 'input-text'
}
];
```
## MiniEditor
除了暴露 `Editor` 外,还有一个简单的编辑器 `MiniEditor`。与 `Editor` 的区别主要是,`MiniEditor` 只有编辑器区,没有左右两侧面板,像爱速搭的模型页面设计器就是基于此实现的。