mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
fix: 修复 amis-editor 本地开发代码编辑器 json-schema 加载失败问题 & 移动端编辑预览方式调整无需提供 iframeUrl (#7776)
* fix: 修复 amis-editor 本地开发代码编辑器 json-schema 加载失败问题 * 调整 iframe 预览逻辑
This commit is contained in:
parent
78c7141e1e
commit
5301cac8ee
@ -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 渲染器,然后再添加编辑器插件,让这个自定义渲染器可以在编辑器中可编辑。
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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;
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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}
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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>
|
@ -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
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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')
|
||||
|
Loading…
Reference in New Issue
Block a user