mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
parent
b9e0283b6b
commit
0303526739
747
docs/zh-CN/extend/editor.md
Normal file
747
docs/zh-CN/extend/editor.md
Normal file
@ -0,0 +1,747 @@
|
||||
---
|
||||
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`。
|
||||
- `iframeUrl?: string` 这个和 `isMobile` 搭配使用。具体看下面的说明。
|
||||
- `$schemaUrl?: string` 提供 amis 产出的 schema.json 的访问路径。主要用来给代码编辑模式提供属性提示信息。
|
||||
- `className?: string` 额外加个 css 类名,辅助样式定义。
|
||||
- `schemas?: JSONSchemaObject` 用来定义有哪些全局变量,辅助编辑器格式化绑定全局数据。
|
||||
- `theme?: string` amis 主题
|
||||
- `schemaFilter?: (schema: any) => 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` 时会触发变动事件
|
||||
|
||||
## 移动端编辑与预览
|
||||
|
||||
移动端预览,需要额外提供 iframe 页面,并且与编辑器建立连接。mountInIframe 前请确保自定义的 amis 渲染器已经加载了,否则会出现自定义渲染器无法编辑的问题。
|
||||
|
||||
```html
|
||||
<body>
|
||||
<div id="root" class="app-wrapper"></div>
|
||||
<script type="module">
|
||||
import reactDom from 'react-dom';
|
||||
import {mountInIframe} from 'amis-editor';
|
||||
mountInIframe(document.getElementById('root'), reactDom);
|
||||
</script>
|
||||
</body>
|
||||
```
|
||||
|
||||
## 自定义插件
|
||||
|
||||
开始之前,需要先自定义一个 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` 只有编辑器区,没有左右两侧面板,像爱速搭的模型页面设计器就是基于此实现的。
|
@ -216,6 +216,13 @@ export default [
|
||||
import('../../docs/zh-CN/extend/i18n.md').then(wrapDoc)
|
||||
)
|
||||
},
|
||||
{
|
||||
label: '可视化编辑器',
|
||||
path: '/zh-CN/docs/extend/editor',
|
||||
component: React.lazy(() =>
|
||||
import('../../docs/zh-CN/extend/editor.md').then(wrapDoc)
|
||||
)
|
||||
},
|
||||
{
|
||||
label: '如何贡献代码',
|
||||
path: '/zh-CN/docs/extend/contribute',
|
||||
|
BIN
examples/static/editor-inline-edit.png
Normal file
BIN
examples/static/editor-inline-edit.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
BIN
examples/static/editor-panel.png
Normal file
BIN
examples/static/editor-panel.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
BIN
examples/static/editor-panel2.png
Normal file
BIN
examples/static/editor-panel2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 50 KiB |
BIN
examples/static/editor-plugin.png
Normal file
BIN
examples/static/editor-plugin.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 109 KiB |
BIN
examples/static/editor-resize.png
Normal file
BIN
examples/static/editor-resize.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
BIN
examples/static/editor-scaffold-form.png
Normal file
BIN
examples/static/editor-scaffold-form.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 87 KiB |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amis-core",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.4-alpha.4",
|
||||
"description": "amis-core",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
@ -45,7 +45,7 @@
|
||||
"esm"
|
||||
],
|
||||
"dependencies": {
|
||||
"amis-formula": "^2.5.2",
|
||||
"amis-formula": "^2.5.4-alpha.4",
|
||||
"classnames": "2.3.1",
|
||||
"file-saver": "^2.0.2",
|
||||
"hoist-non-react-statics": "^3.3.2",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amis-formula",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.4-alpha.4",
|
||||
"description": "负责 amis 里面的表达式实现,内置公式,编辑器等",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
|
@ -3,7 +3,7 @@
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
"types": "lib/index.d.ts",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.4-alpha.4",
|
||||
"description": "",
|
||||
"scripts": {
|
||||
"build": "npm run clean-dist && NODE_ENV=production rollup -c ",
|
||||
@ -35,8 +35,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@rc-component/mini-decimal": "^1.0.1",
|
||||
"amis-core": "^2.5.2",
|
||||
"amis-formula": "^2.5.2",
|
||||
"amis-core": "^2.5.4-alpha.4",
|
||||
"amis-formula": "^2.5.4-alpha.4",
|
||||
"classnames": "2.3.1",
|
||||
"codemirror": "^5.63.0",
|
||||
"downshift": "6.1.12",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "amis",
|
||||
"version": "2.5.2",
|
||||
"version": "2.5.4-alpha.4",
|
||||
"description": "一种MIS页面生成工具",
|
||||
"main": "lib/index.js",
|
||||
"module": "esm/index.js",
|
||||
@ -41,8 +41,8 @@
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"amis-core": "^2.5.2",
|
||||
"amis-ui": "^2.5.2",
|
||||
"amis-core": "^2.5.4-alpha.4",
|
||||
"amis-ui": "^2.5.4-alpha.4",
|
||||
"attr-accept": "2.2.2",
|
||||
"blueimp-canvastoblob": "2.1.0",
|
||||
"classnames": "2.3.1",
|
||||
|
Loading…
Reference in New Issue
Block a user