fix: 修复 amis-editor 本地开发代码编辑器 json-schema 加载失败问题 & 移动端编辑预览方式调整无需提供 iframeUrl (#7776)

* fix: 修复 amis-editor 本地开发代码编辑器 json-schema 加载失败问题

* 调整 iframe 预览逻辑
This commit is contained in:
liaoxuezhi 2023-08-14 12:08:24 +08:00 committed by GitHub
parent 78c7141e1e
commit 5301cac8ee
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 271 additions and 600 deletions

View File

@ -42,7 +42,6 @@ render() {
- `preview?: boolean` 是否为预览模式。
- `autoFocus?: boolean` 是否自动聚焦第一个可编辑的组件。
- `isMobile?: boolean` 是否为移动端模式,当为移动模式时,将采用 iframe 来预览,需要配置 `iframeUrl`
- `iframeUrl?: string` 这个和 `isMobile` 搭配使用。具体看下面的说明。
- `$schemaUrl?: string` 提供 amis 产出的 schema.json 的访问路径。主要用来给代码编辑模式提供属性提示信息。
- `className?: string` 额外加个 css 类名,辅助样式定义。
- `schemas?: JSONSchemaObject` 用来定义有哪些全局变量,辅助编辑器格式化绑定全局数据。
@ -80,21 +79,6 @@ render() {
- `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 渲染器,然后再添加编辑器插件,让这个自定义渲染器可以在编辑器中可编辑。

View File

@ -54,6 +54,7 @@
"mobx": "^4.5.0",
"mobx-react": "^6.3.1",
"mobx-state-tree": "^3.17.3",
"react-frame-component": "^5.2.6",
"react-json-view": "^1.21.3",
"sortablejs": "^1.14.0"
},

View File

@ -192,7 +192,7 @@
.ae-Preview-inner {
position: relative;
// display: flex;
min-height: 100%;
min-height: calc(100% - 40px);
background: #fff;
box-shadow: 0 2px 6px 0 rgba(211, 211, 211, 0.5);
border-radius: 4px;
@ -201,7 +201,7 @@
display: flex;
flex-direction: column;
> *:first-child {
> *:not(iframe):first-child {
// min-width: 100%;
position: relative;
// min-height: calc(100vh - 131px); // 确保能撑开画布区
@ -228,7 +228,7 @@
&.is-mobile {
position: relative;
border-width: 64px 22px;
border-width: 24px 22px 36px;
border-color: #ddd;
border-style: solid;
border-radius: 40px;
@ -241,19 +241,35 @@
padding: 0;
overflow: visible;
// 这里覆盖 padding 没用
&:before {
content: none;
}
> .ae-Preview-inner {
min-height: 100%;
height: 100%;
width: 100%;
background: transparent;
overflow-x: hidden;
overflow-y: auto;
padding: 0;
transform: scale(1);
transform-origin: center top;
height: 100%;
display: block;
position: relative;
&::-webkit-scrollbar {
display: none;
}
&::before {
content: '';
height: 40px;
position: sticky;
top: 0;
width: 100%;
background: #ddd;
flex-shrink: 0;
display: block;
}
}
// 移动端预览设备装饰
@ -269,14 +285,14 @@
}
// 音响
.mobile-sound {
top: -35px; // 29px - 64px;
top: 5px;
left: 142px; // 164px - 22px;
width: 58px;
height: 6px;
}
// 听筒
.mobile-receiver {
top: -37px; // 27px - 64px;
top: 3px;
left: 224px; // 246px - 22px;
width: 10px;
height: 10px;
@ -333,17 +349,25 @@
width: 100%;
}
.ae-IframeMask {
position: absolute;
z-index: 100;
top: 0;
left: 0;
right: 0;
bottom: 0;
.ae-PreviewIFrame {
pointer-events: all !important;
border: 0 !important;
min-height: calc(100% - 46px);
width: 100%;
}
.ae-IFramePreview {
> *:first-child {
&,
& > .frame-content {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
flex: 1;
}
> .frame-content > *:first-child {
flex: 1;
min-width: 100%;
position: relative;
min-height: 100%;
@ -1294,14 +1318,16 @@
}
}
// 优化画布区页面展示效果
[data-region='aside'][data-renderer='page'] {
min-height: calc(100vh - 131px);
}
// 移动端有问题先注释了
[data-region='body'][data-renderer='page'] {
min-height: calc(100vh - 165px);
}
// 优化画布区页面展示效果
// [data-region='aside'][data-renderer='page'] {
// min-height: calc(100vh - 131px);
// }
// [data-region='body'][data-renderer='page'] {
// min-height: calc(100vh - 165px);
// }
.ae-Editor-rhlbox {
position: absolute;

View File

@ -5,7 +5,7 @@ import {autobind} from '../util';
import {MainStore, EditorStoreType} from '../store/editor';
import {EditorManager, EditorManagerConfig, PluginClass} from '../manager';
import {reaction} from 'mobx';
import {RenderOptions, toast} from 'amis';
import {RenderOptions, closeContextMenus, toast} from 'amis';
import {PluginEventListener, RendererPluginAction} from '../plugin';
import {reGenerateID} from '../util';
import {SubEditor} from './SubEditor';
@ -82,10 +82,6 @@ export interface EditorProps extends PluginEventListener {
*/
previewProps?: any;
// 如果配置了,编辑器变成 iframe 模式。
// 需要自己写代码去建立连接。
iframeUrl?: string;
isHiddenProps?: (key: string) => boolean;
/**
@ -408,6 +404,7 @@ export default class Editor extends Component<EditorProps> {
// 右键菜单
@autobind
handleContextMenu(e: React.MouseEvent<HTMLElement>) {
closeContextMenus();
let targetId: string = '';
let region = '';
@ -449,11 +446,20 @@ export default class Editor extends Component<EditorProps> {
e.preventDefault();
e.stopPropagation();
const manager = this.manager;
let offsetX = 0;
let offsetY = 0;
// 说明是 iframe 里面
if ((e.target as HTMLElement).ownerDocument !== document) {
const rect = manager.store.getIframe()!.getBoundingClientRect();
offsetX = rect.left;
offsetY = rect.top;
}
manager.openContextMenu(targetId, region, {
x: window.scrollX + e.clientX,
y: window.scrollY + e.clientY
x: window.scrollX + e.clientX + offsetX,
y: window.scrollY + e.clientY + offsetY
});
}
@ -540,7 +546,6 @@ export default class Editor extends Component<EditorProps> {
theme,
appLocale,
data,
iframeUrl,
previewProps,
autoFocus,
isSubEditor,
@ -573,7 +578,6 @@ export default class Editor extends Component<EditorProps> {
)}
<Preview
{...previewProps}
iframeUrl={iframeUrl}
editable={!preview}
isMobile={isMobile}
store={this.store}

View File

@ -137,7 +137,7 @@ export default class HighlightBox extends React.Component<HighlightBoxProps> {
if (ref) {
ref.addEventListener('mousedown', this.handleWResizerMouseDown);
} else {
this.wResizerDom?.addEventListener(
this.wResizerDom?.removeEventListener(
'mousedown',
this.handleWResizerMouseDown
);
@ -153,7 +153,7 @@ export default class HighlightBox extends React.Component<HighlightBoxProps> {
if (ref) {
ref.addEventListener('mousedown', this.handleHResizerMouseDown);
} else {
this.hResizerDom?.addEventListener(
this.hResizerDom?.removeEventListener(
'mousedown',
this.handleHResizerMouseDown
);
@ -169,7 +169,7 @@ export default class HighlightBox extends React.Component<HighlightBoxProps> {
if (ref) {
ref.addEventListener('mousedown', this.handleResizerMouseDown);
} else {
this.resizerDom?.addEventListener(
this.resizerDom?.removeEventListener(
'mousedown',
this.handleResizerMouseDown
);

View File

@ -1,175 +0,0 @@
import {observer} from 'mobx-react';
import {isAlive} from 'mobx-state-tree';
import React from 'react';
import {unmountComponentAtNode} from 'react-dom';
// import {createRoot} from 'react-dom/client';
import {EditorManager} from '../manager';
import {EditorStoreType} from '../store/editor';
import {autobind, guid} from '../util';
import IFramePreview from './IFramePreview';
export interface IFrameBridgeProps {
className?: string;
url: string;
editable?: boolean;
isMobile?: boolean;
store: EditorStoreType;
manager: EditorManager;
theme?: string;
data?: any;
env?: any;
autoFocus?: boolean;
}
export interface BridgeApi {
update: (props: any) => void;
}
@observer
export default class IFrameBridge extends React.PureComponent<IFrameBridgeProps> {
bridgeFnName: string;
bridge?: BridgeApi;
schema: any;
constructor(props: IFrameBridgeProps) {
super(props);
const bridgeName = `__amis_editor_bridge_fn_${guid()}`;
(window as any)[bridgeName] = (innerApi: BridgeApi) => {
delete (window as any)[bridgeName];
this.bridge = innerApi;
this.update(props);
return props.manager;
};
this.bridgeFnName = bridgeName;
}
componentDidUpdate() {
this.update();
}
componentWillUnmount() {
const store = this.props.store;
isAlive(store) && store.setDoc(document);
}
@autobind
iframeRef(iframe: any) {
const store = this.props.store;
isAlive(store) && store.setIframe(iframe);
}
update(props = this.props) {
const {editable, store} = props;
this.bridge?.update({
...props,
schema: editable ? store.filteredSchema : store.filteredSchemaForPreview
});
}
render() {
const {url, manager, className, editable, store} = this.props;
// 没啥用,纯粹是为了监控。
this.schema = editable
? store.filteredSchema
: store.filteredSchemaForPreview;
return (
<iframe
ref={this.iframeRef}
className={className}
id={manager.id}
src={`${url}#${this.bridgeFnName}`}
/>
);
}
}
class PreviewWrapper extends React.Component<{
bridgeName: string;
envCreator?: any;
}> {
readonly manager: EditorManager;
state: any = {};
inited = false;
constructor(props: {bridgeName: string}) {
super(props);
const bridgeName = props.bridgeName;
const bridge = (parent as any)[bridgeName];
if (typeof bridge !== 'function') {
throw new Error('调用错误,或者存在跨域。');
}
const manager: EditorManager = bridge({
update: (props: any) =>
this.inited ? this.setState({...props}) : (this.state = {...props})
});
if (!manager) {
throw new Error('调用错误');
}
manager.store.setDoc(document);
const subManager = new EditorManager(
manager.config,
manager.store,
manager
);
this.manager = subManager;
this.inited = true;
}
componentWillUnmount() {
// 销毁,不能调用 manager.dispose 因为它会把 dnd 也销毁了。
const manager = this.manager;
manager.toDispose.forEach(fn => fn());
manager.toDispose = [];
manager.listeners.splice(0, manager.listeners.length);
manager.lazyPatchSchema.cancel();
}
render() {
return (
<IFramePreview
{...this.state}
manager={this.manager}
store={this.manager.store}
envCreator={this.props.envCreator}
></IFramePreview>
);
}
}
export function mountInIframe(
dom: HTMLElement,
reactDom: any,
envCreator?: any
) {
if (!location.hash || parent === window) {
throw new Error('只能在 Iframe 里面调用');
}
const bridgeName = location.hash.substring(1);
if (reactDom.render.length === 1) {
// react 18 版本以下的 render方法
reactDom.render(
<PreviewWrapper bridgeName={bridgeName} envCreator={envCreator} />
);
} else {
reactDom.render(
<PreviewWrapper bridgeName={bridgeName} envCreator={envCreator} />,
dom
);
}
window.onunload = function () {
unmountComponentAtNode(dom);
// root.unmount(); // react18
};
}

View File

@ -1,59 +1,51 @@
import {observer} from 'mobx-react';
import {EditorStoreType} from '../store/editor';
import React from 'react';
import {EditorManager} from '../manager';
import {EditorStoreType} from '../store/editor';
import {render, toast, resolveRenderer, resizeSensor} from 'amis';
import {autobind} from '../util';
import {RendererConfig, RenderOptions} from 'amis-core';
import type {Schema} from 'amis';
import {ErrorRenderer} from './base/ErrorRenderer';
import cx from 'classnames';
import {findDOMNode} from 'react-dom';
import Frame, {useFrame} from 'react-frame-component';
import {
autobind,
closeContextMenus,
findTree,
render,
resizeSensor
} from 'amis';
import {isAlive} from 'mobx-state-tree';
import {findTree} from 'amis-core';
/**
* observer
*/
export interface IFramePreviewProps {
className: string;
editable?: boolean;
isMobile?: boolean;
schema: any;
theme?: string;
store: EditorStoreType;
manager: EditorManager;
autoFocus?: boolean;
store: EditorStoreType;
env: any;
data?: any;
envCreator?: (props: IFramePreviewProps) => any;
manager: EditorManager;
/** 应用语言类型 */
appLocale?: string;
}
// @observer
// 这个在 iframe 里面没办法做到。但是外面做了处理,所以不用加
@observer
export default class IFramePreview extends React.Component<IFramePreviewProps> {
env: RenderOptions = {
...this.props.manager.env,
notify: (type, msg) => {
if (this.props.editable) {
console.warn('[Notify]', type, msg);
return;
}
initialContent: string = '';
constructor(props: IFramePreviewProps) {
super(props);
toast[type]
? toast[type](msg, type === 'error' ? '系统错误' : '系统消息')
: console.warn('[Notify]', type, msg);
},
theme: this.props.theme,
session: `preview-${this.props.manager.id}`,
rendererResolver: this.rendererResolver,
...this.props.manager.env,
...this.props.envCreator?.(this.props)
};
const styles = [].slice
.call(document.querySelectorAll('link[rel="stylesheet"], style'))
.map((el: any) => {
return el.outerHTML;
});
styles.push(
`<style>body {height:auto !important;min-height:100%;display: flex;flex-direction: column;}</style>`
);
this.initialContent = `<!DOCTYPE html><html><head>${styles.join(
''
)}</head><body><div class="ae-IFramePreview"></div></body></html>`;
}
componentDidMount() {
const dom = findDOMNode(this) as HTMLElement;
dom.addEventListener('mouseleave', this.handleMouseLeave);
dom.addEventListener('mousemove', this.handleMouseMove);
dom.addEventListener('click', this.handleClick);
dom.addEventListener('mouseover', this.handeMouseOver);
if (this.props.autoFocus) {
// 一般弹框动画差不多 350ms
// 延时 350ms在弹框中展示编辑器效果要好点。
@ -73,208 +65,143 @@ export default class IFramePreview extends React.Component<IFramePreviewProps> {
}
}
componentWillUnmount() {
const dom = findDOMNode(this) as HTMLElement;
dom.removeEventListener('mouseleave', this.handleMouseLeave);
dom.removeEventListener('mousemove', this.handleMouseMove);
dom.removeEventListener('click', this.handleClick);
dom.removeEventListener('mouseover', this.handeMouseOver);
}
dom?: HTMLElement;
unSensor?: () => void;
@autobind
contentsRef(ref: HTMLDivElement | null) {
this.dom = ref!;
if (ref) {
// this.layer = ref!.querySelector('.ae-Preview-widgets') as HTMLDivElement;
this.syncIframeHeight();
this.unSensor = resizeSensor(ref, () => {
this.syncIframeHeight();
});
} else {
// delete this.layer;
this.unSensor?.();
delete this.unSensor;
}
}
// todo 优化这个
syncIframeHeight() {
const manager = this.props.manager;
if (this.dom) {
const iframe = manager.store.getIframe()!;
iframe.style.cssText += `height: ${this.dom.offsetHeight}px`;
}
}
@autobind
handleClick(e: MouseEvent) {
iframeRef(iframe: any) {
const store = this.props.store;
const target = (e.target as HTMLElement).closest(`[data-editor-id]`);
if (e.defaultPrevented) {
return;
}
if (target) {
store.setActiveId(target.getAttribute('data-editor-id')!);
}
if (this.props.editable) {
// 让渲染器不可点,只能点击选中。
const event = this.props.manager.trigger('prevent-click', {
data: e
});
if (!event.prevented && !event.stoped) {
e.preventDefault();
e.stopPropagation();
}
}
isAlive(store) && store.setIframe(iframe);
}
@autobind
handleMouseMove(e: MouseEvent) {
const store = this.props.store;
render() {
const {editable, store, appLocale, autoFocus, env, data, manager, ...rest} =
this.props;
return (
<Frame
className={`ae-PreviewIFrame`}
initialContent={this.initialContent}
ref={this.iframeRef}
>
<InnerComponent store={store} editable={editable} manager={manager} />
{render(
editable ? store.filteredSchema : store.filteredSchemaForPreview,
{
...rest,
key: editable ? 'edit-mode' : 'preview-mode',
theme: env.theme,
data: data ?? store.ctx,
locale: appLocale
},
env
)}
</Frame>
);
}
}
function InnerComponent({
store,
editable,
manager
}: {
store: EditorStoreType;
editable?: boolean;
manager: EditorManager;
}) {
// Hook returns iframe's window and document instances from Frame context
const {document: doc} = useFrame();
const handleMouseLeave = React.useCallback(() => {
store.setHoverId('');
}, []);
const handleMouseMove = React.useCallback((e: MouseEvent) => {
const dom = e.target as HTMLElement;
const target = dom.closest(`[data-editor-id]`);
if (target) {
store.setHoverId(target.getAttribute('data-editor-id')!);
}
}
}, []);
@autobind
handleMouseLeave() {
const store = this.props.store;
store.setHoverId('');
}
const handleBodyClick = React.useCallback(() => {
closeContextMenus();
}, []);
@autobind
handeMouseOver(e: MouseEvent) {
if (this.props.editable) {
e.preventDefault();
e.stopPropagation();
}
}
const handleClick = React.useCallback(
(e: MouseEvent) => {
const target = (e.target as HTMLElement).closest(`[data-editor-id]`);
@autobind
handleDragEnter(e: React.DragEvent) {
const manager = this.props.manager;
manager.dnd.dragEnter(e.nativeEvent);
}
@autobind
handleDragLeave(e: React.DragEvent) {
const manager = this.props.manager;
manager.dnd.dragLeave(e.nativeEvent);
}
@autobind
handleDragOver(e: React.DragEvent) {
const manager = this.props.manager;
manager.dnd.dragOver(e.nativeEvent);
}
@autobind
handleDrop(e: React.DragEvent) {
const manager = this.props.manager;
manager.dnd.drop(e.nativeEvent);
}
@autobind
handleContextMenu(e: React.MouseEvent<HTMLElement>) {
let targetId: string = (e.target as HTMLElement)
.closest('[data-editor-id]')
?.getAttribute('data-editor-id')!;
let region = '';
if (!targetId) {
const node = (e.target as HTMLElement).closest(
'[data-node-id]'
) as HTMLElement;
targetId = node?.getAttribute('data-node-id')!;
if (!targetId) {
if (e.defaultPrevented) {
return;
}
region = node.getAttribute('data-node-region')!;
}
if (target) {
store.setActiveId(target.getAttribute('data-editor-id')!);
}
e.preventDefault();
e.stopPropagation();
if (editable) {
// 让渲染器不可点,只能点击选中。
const event = manager.trigger('prevent-click', {
data: e
});
const manager = this.props.manager;
const rect = manager.store.getIframe()!.getBoundingClientRect();
manager.parent!.openContextMenu(targetId, region, {
x: window.scrollX + e.clientX + rect.left,
y: window.scrollY + e.clientY + rect.top
if (!event.prevented && !event.stoped) {
e.preventDefault();
e.stopPropagation();
}
}
},
[editable]
);
const handeMouseOver = React.useCallback(
(e: MouseEvent) => {
if (editable) {
e.preventDefault();
e.stopPropagation();
}
},
[editable]
);
const syncIframeHeight = React.useCallback(() => {
const iframe = manager.store.getIframe()!;
iframe.style.cssText += `height: ${doc!.body.offsetHeight}px`;
}, []);
React.useEffect(() => {
store.setDoc(doc);
const layer = doc?.querySelector('.frame-content') as HTMLElement;
doc!.addEventListener('click', handleBodyClick);
layer!.addEventListener('mouseleave', handleMouseLeave);
layer!.addEventListener('mousemove', handleMouseMove);
layer!.addEventListener('click', handleClick);
layer!.addEventListener('mouseover', handeMouseOver);
const unSensor = resizeSensor(doc!.body, () => {
syncIframeHeight();
});
}
syncIframeHeight();
@autobind
rendererResolver(path: string, schema: Schema, props: any) {
const {editable, manager} = this.props;
return () => {
doc!.removeEventListener('click', handleBodyClick);
layer!.removeEventListener('mouseleave', handleMouseLeave);
layer!.removeEventListener('mousemove', handleMouseMove);
layer!.removeEventListener('click', handleClick);
layer!.removeEventListener('mouseover', handeMouseOver);
let renderer: RendererConfig = resolveRenderer(path, schema)!;
if (editable === false) {
return renderer;
}
renderer = renderer || {
name: 'error',
test: () => true,
component: ErrorRenderer
store.setDoc(document);
unSensor();
};
}, [doc]);
let info = manager.getEditorInfo(renderer!, path, schema);
React.useEffect(() => {
doc
?.querySelector('body>div:first-child')
?.classList.toggle('is-edting', editable);
}, [editable]);
info &&
(renderer = {
...renderer,
component: manager.makeWrapper(info, renderer)
});
return renderer;
}
render() {
const {store, editable, manager, className, schema, data, ...rest} =
this.props;
return (
<div
ref={this.contentsRef}
onContextMenu={this.handleContextMenu}
onDragEnter={this.handleDragEnter}
onDragLeave={this.handleDragLeave}
onDragOver={this.handleDragOver}
onDrop={this.handleDrop}
className={cx(
'ae-IFramePreview',
className,
editable ? 'is-edting' : ''
)}
>
{render(
schema || {
type: 'tpl',
tpl: '渲染中...'
},
{
...rest,
key: editable ? 'edit-mode' : 'preview-mode',
theme: this.env.theme,
data: data ?? store.ctx
},
this.env
)}
</div>
);
}
return null;
}

View File

@ -11,11 +11,11 @@ import {EditorManager} from '../manager';
import HighlightBox from './HighlightBox';
import RegionHighlightBox from './RegionHLBox';
import {ErrorRenderer} from './base/ErrorRenderer';
import IFrameBridge from './IFrameBridge';
import {isAlive} from 'mobx-state-tree';
import {findTree} from 'amis-core';
import BackTop from './base/BackTop';
import type {RendererConfig} from 'amis-core';
import IFramePreview from './IFramePreview';
export interface PreviewProps {
// isEditorEnabled?: (
@ -35,7 +35,6 @@ export interface PreviewProps {
store: EditorStoreType;
manager: EditorManager;
data?: any;
iframeUrl?: string;
autoFocus?: boolean;
toolbarContainer?: () => any;
@ -461,7 +460,6 @@ export default class Preview extends Component<PreviewProps> {
amisEnv,
theme,
isMobile,
iframeUrl,
autoFocus,
toolbarContainer,
appLocale,
@ -495,7 +493,7 @@ export default class Preview extends Component<PreviewProps> {
)}
ref={this.contentsRef}
>
{iframeUrl && isMobile && (
{isMobile && (
<React.Fragment>
<div className="mobile-sound"></div>
<div className="mobile-receiver"></div>
@ -505,20 +503,17 @@ export default class Preview extends Component<PreviewProps> {
</React.Fragment>
)}
<div className="ae-Preview-inner">
{iframeUrl && isMobile ? (
<IFrameBridge
{isMobile ? (
<IFramePreview
{...rest}
key="mobile"
className="ae-PreviewFrame"
editable={editable}
isMobile={isMobile}
store={store}
env={env}
manager={manager}
url={iframeUrl}
theme={theme}
autoFocus={autoFocus}
></IFrameBridge>
appLocale={appLocale}
></IFramePreview>
) : (
<SmartPreview
{...rest}
@ -532,10 +527,6 @@ export default class Preview extends Component<PreviewProps> {
/>
)}
{iframeUrl && isMobile && store.contextId ? (
<span className="ae-IframeMask"></span>
) : null}
<div className="ae-Preview-widgets" id="aePreviewHighlightBox">
{store.highlightNodes.map(node => (
<HighlightBox

View File

@ -153,7 +153,6 @@ export class SubEditor extends React.Component<SubEditorProps> {
onBuildPanels={this.handleBuildPanels}
isMobile={store.isMobile}
isSubEditor={true}
iframeUrl={config.iframeUrl}
ctx={store.ctx}
schemas={manager.config?.schemas}
variables={variables}

View File

@ -28,7 +28,6 @@ import {BasicEditor, RendererEditor} from './compat';
import MiniEditor from './component/MiniEditor';
import CodeEditor from './component/Panel/AMisCodeEditor';
import IFramePreview from './component/IFramePreview';
import {mountInIframe} from './component/IFrameBridge';
import SearchPanel from './component/base/SearchPanel';
import {VRenderer} from './component/VRenderer';
import {RegionWrapper} from './component/RegionWrapper';
@ -52,7 +51,6 @@ export {
CodeEditor,
VRenderer,
RegionWrapper,
mountInIframe,
IFramePreview as IFrameEditor,
SearchPanel,
EditorNodeType,

View File

@ -206,18 +206,14 @@ export class EditorManager {
readonly pluginActions: PluginActions = {};
dataSchema: DataSchema;
readonly isInFrame: boolean = false;
/** 变量管理 */
readonly variableManager;
constructor(
readonly config: EditorManagerConfig,
readonly store: EditorStoreType,
readonly parent?: EditorManager
readonly store: EditorStoreType
) {
const isInFrame = !!parent;
this.isInFrame = isInFrame;
// 传给 amis 渲染器的默认 env
this.env = {
...env, // 默认的 env 中带 jumpTo
@ -228,78 +224,43 @@ export class EditorManager {
this,
this.env.beforeDispatchEvent
);
this.hackIn = parent?.hackIn || hackIn;
this.hackIn = hackIn;
// 自动加载预先注册的自定义组件
autoPreRegisterEditorCustomPlugins();
/** 在顶层对外部注册的Plugin和builtInPlugins合并去重 */
if (!parent?.plugins) {
(config?.plugins || []).forEach(external => {
if (
Array.isArray(external) ||
!external.priority ||
!Number.isInteger(external.priority)
) {
return;
this.plugins = (config.disableBultinPlugin ? [] : builtInPlugins) // 页面设计器注册的插件列表
.concat(this.normalizeScene(config?.plugins))
.filter(p => {
p = Array.isArray(p) ? p[0] : p;
return config.disablePluginList
? typeof config.disablePluginList === 'function'
? !config.disablePluginList(p.id || '', p)
: !config.disablePluginList.includes(p.id || 'unkown')
: true;
})
.map(Editor => {
let pluginOptions: Record<string, any> = {};
if (Array.isArray(Editor)) {
pluginOptions =
typeof Editor[1] === 'function' ? Editor[1]() : Editor[1];
Editor = Editor[0];
}
const idx = builtInPlugins.findIndex(
builtIn =>
!Array.isArray(builtIn) &&
!Array.isArray(external) &&
builtIn.id === external.id &&
builtIn?.prototype instanceof BasePlugin
);
const plugin = new Editor(this, pluginOptions); // 进行一次实例化
plugin.order = plugin.order ?? 0;
if (~idx) {
const current = builtInPlugins[idx] as PluginClass;
const currentPriority =
current.priority && Number.isInteger(current.priority)
? current.priority
: 0;
/** 同ID Plugin根据优先级决定是否替换掉Builtin中的Plugin */
if (external.priority > currentPriority) {
builtInPlugins.splice(idx, 1);
}
// 记录动作定义
if (plugin.rendererName) {
this.pluginEvents[plugin.rendererName] = plugin.events || [];
this.pluginActions[plugin.rendererName] = plugin.actions || [];
}
});
}
this.plugins =
parent?.plugins ||
(config.disableBultinPlugin ? [] : builtInPlugins) // 页面设计器注册的插件列表
.concat(this.normalizeScene(config?.plugins))
.filter(p => {
p = Array.isArray(p) ? p[0] : p;
return config.disablePluginList
? typeof config.disablePluginList === 'function'
? !config.disablePluginList(p.id || '', p)
: !config.disablePluginList.includes(p.id || 'unkown')
: true;
})
.map(Editor => {
let pluginOptions: Record<string, any> = {};
if (Array.isArray(Editor)) {
pluginOptions =
typeof Editor[1] === 'function' ? Editor[1]() : Editor[1];
Editor = Editor[0];
}
const plugin = new Editor(this, pluginOptions); // 进行一次实例化
plugin.order = plugin.order ?? 0;
// 记录动作定义
if (plugin.rendererName) {
this.pluginEvents[plugin.rendererName] = plugin.events || [];
this.pluginActions[plugin.rendererName] = plugin.actions || [];
}
return plugin;
})
.sort((a, b) => a.order! - b.order!); // 按order排序【升序】
return plugin;
})
.sort((a, b) => a.order! - b.order!); // 按order排序【升序】
this.hackRenderers();
this.dnd = parent?.dnd || new EditorDNDManager(this, store);
this.dataSchema =
parent?.dataSchema || new DataSchema(config.schemas || []);
this.dnd = new EditorDNDManager(this, store);
this.dataSchema = new DataSchema(config.schemas || []);
/** 初始化变量管理 */
this.variableManager = new VariableManager(
@ -308,10 +269,6 @@ export class EditorManager {
config?.variableOptions
);
if (isInFrame) {
return;
}
this.toDispose.push(
// 当前节点区域数量发生变化,重新构建孩子渲染器列表。
/*reaction(
@ -1811,9 +1768,7 @@ export class EditorManager {
* @param render
*/
makeWrapper(info: RendererInfo, render: RendererConfig): any {
return this.parent?.makeWrapper
? this.parent.makeWrapper(info, render)
: makeWrapper(this, info, render);
return makeWrapper(this, info, render);
}
/**
@ -1935,7 +1890,7 @@ export class EditorManager {
return [];
}
let scope: DataScope | void;
let scope: DataScope | void = undefined;
let from = node;
let region = node;
const trigger = node;
@ -2012,7 +1967,7 @@ export class EditorManager {
return;
}
let scope: DataScope | void;
let scope: DataScope | void = undefined;
let from = node;
let region = node;

View File

@ -1,51 +0,0 @@
<!-- htmlcs-disable -->
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8" />
<title>移动端编辑器</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1"
/>
<meta http-equiv="X-UA-Compatible" content="IE=Edge" />
<link
rel="stylesheet"
href="https://bce.bdstatic.com/iconfont/iconfont.css"
/>
<link
rel="stylesheet"
href="../../../node_modules/@fortawesome/fontawesome-free/css/all.css"
/>
<link
rel="stylesheet"
href="../../../node_modules/@fortawesome/fontawesome-free/css/v4-shims.css"
/>
<link rel="stylesheet" title="cxd" href="../amis-ui/scss/themes/cxd.scss" />
<link
rel="stylesheet"
type="text/css"
href="../amis-editor-core/scss/editor.scss"
/>
<style>
.app-wrapper {
position: relative;
width: 100%;
height: 100%;
}
</style>
</head>
<body>
<div id="root" class="app-wrapper"></div>
<script type="module">
import {createRoot} from 'react-dom/client';
import {mountInIframe} from 'amis-editor';
mountInIframe(
document.getElementById('root'),
createRoot(document.getElementById('root'))
);
</script>
</body>
</html>

View File

@ -643,7 +643,6 @@ export default class AMisSchemaEditor extends React.Component<any, any> {
theme={theme || 'cxd'}
showCustomRenderersPanel={true}
plugins={LayoutList} // 存放常见布局组件
iframeUrl={'/packages/amis-editor/editor.html'}
$schemaUrl={`${location.protocol}//${location.host}/schema.json`}
actionOptions={{
showOldEntry: false

View File

@ -313,7 +313,7 @@ export class ContextMenu extends React.Component<
export const ThemedContextMenu = themeable(ContextMenu);
export default ThemedContextMenu;
export function openContextMenus(
export async function openContextMenus(
info: Event | {x: number; y: number},
menus: Array<MenuItem | MenuDivider>,
onClose?: () => void
@ -322,3 +322,7 @@ export function openContextMenus(
instance.openContextMenus(info, menus, onClose)
);
}
export async function closeContextMenus() {
return ContextMenu.getInstance().then(instance => instance?.close());
}

View File

@ -12,7 +12,11 @@ import {
prompt,
setRenderSchemaFn
} from './Alert';
import {default as ContextMenu, openContextMenus} from './ContextMenu';
import {
default as ContextMenu,
openContextMenus,
closeContextMenus
} from './ContextMenu';
import AsideNav from './AsideNav';
import Avatar from './Avatar';
import Button from './Button';
@ -136,6 +140,7 @@ export {
setRenderSchemaFn,
ContextMenu,
openContextMenus,
closeContextMenus,
Alert2,
AsideNav,
Button,

View File

@ -77,6 +77,10 @@ export default defineConfig({
find: 'amis/lib',
replacement: path.resolve(__dirname, './packages/amis/src')
},
{
find: 'amis/schema.json',
replacement: path.resolve(__dirname, './packages/amis/schema.json')
},
{
find: 'amis',
replacement: path.resolve(__dirname, './packages/amis/src')