Co-authored-by: 肖林 <xiaolin@dushu365.com>
27 KiB
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 项目中这样使用:
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 来预览。$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 说明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 渲染器,然后再添加编辑器插件,让这个自定义渲染器可以在编辑器中可编辑。
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 注册全局插件。
- 不注册,调用 的时候时候通过 plugins 属性传入。
效果都一样,重点还是怎么写个 Plugin,示例:
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 后,可以有两种方式启用。
// 方式 1,注册默认插件,所有编辑器实例都会自动实例话。
import {registerEditorPlugin} from 'amis-editor';
registerEditorPlugin(MyRendererPlugin);
// 方式2,只让某些编辑器启用
() => <Editor plugins={[MyRendererPlugin]} />;
工作原理
编辑器在渲染 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
的,则会自动完成注册逻辑。
/**
* 如果配置里面有 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
来插入右侧面板。
/**
* 配置了 panelControls 自动生成配置面板
* @param context
* @param panels
*/
buildEditorPanel(
context: BuildPanelEventContext,
panels: Array<BasicPanelItem>
) {
panels.push({
key: 'xxxx',
title: '设置',
render: () => {
return <div>面板内容</div>
}
})
}
通常右侧面板都是表单配置,使用 amis 配置就可以完成。所以推荐的做法是,直接在这个插件上面定义 panelBody
或者 panelBodyCreator
即可。
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: []
}
]
}
];
panelBodyCreator
相对于 panelBody
的区别是,可以基于一些上下文信息来构建不同的表单。比如在表单里面的按钮,和在表单外面的按钮配置项不一样。
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)
来实现的容器功能。
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。
regions: Array<RegionConfig> = [
{
key: 'body',
label: '内容区'
}
];
插件内部会根据这个信息,自动在 render('body', body as any, {disabled})
的地方包裹个 RegionWrapper
。这种方式主要是通过篡改 this.props.render
方法实现的。
再看个复杂点的情况如 Form
的 actions
区块输出。
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
里面,没办法通过第一种方式实现。所以第二种配置方式是:
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 直接包一层 RegionrenderMethod?: 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 里面的 renderBodywrapper?: React.ComponentType<RegionWrapperProps>
用来指定用什么组件包裹,默认是 RegionWrapperwrapperResolve?: (dom: HTMLElement) => HTMLElement
返回需要添加 data-region 的 dom 节点。modifyGhost?: (ghost: HTMLElement, schema?: any) => void
当拖入到这个容器时,是否需要修改一下 ghost 结构?dndMode?: string
dnd 拖拽模式。比如 table 那种需要配置成 position-haccept?: (json: any) => boolean
可以用来判断是否允许拖入当前节点。
如何定义编辑器脚手架
如果希望拖入组件的时候,弹出个配置框,基于用户不同的配置,生成不同的初始数据。则这里需要用到 scaffoldForm
配置。
scaffoldForm = {
title: '标题',
body: [
{
name: 'target',
label: 'Target',
type: 'input-text'
}
]
};
可用配置
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
方法即可添加点选框顶部菜单
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
方法即可添加上下文功能菜单
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
改成如下代码:
@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>
);
}
}
然后插件中加入以下代码即可完成拖拽调整宽高
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();
});
}
});
}
如何开启快速配置
直接配置 popOverBody
即可
popOverBody = [
{
name: 'target',
label: 'Target',
type: 'input-text'
}
];
MiniEditor
除了暴露 Editor
外,还有一个简单的编辑器 MiniEditor
。与 Editor
的区别主要是,MiniEditor
只有编辑器区,没有左右两侧面板,像爱速搭的模型页面设计器就是基于此实现的。