mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-04-06 03:57:56 +08:00
feat(stage, editor): runtime支持直接渲染模式不用iframe
This commit is contained in:
parent
0c994f1e23
commit
ba2f1e5ac5
@ -175,6 +175,7 @@ provide(
|
||||
containerHighlightDuration: props.containerHighlightDuration,
|
||||
containerHighlightType: props.containerHighlightType,
|
||||
disabledDragStart: props.disabledDragStart,
|
||||
renderType: props.renderType,
|
||||
}),
|
||||
);
|
||||
|
||||
|
@ -7,6 +7,7 @@ import StageCore, {
|
||||
ContainerHighlightType,
|
||||
CustomizeMoveableOptionsCallbackConfig,
|
||||
MoveableOptions,
|
||||
RenderType,
|
||||
UpdateDragEl,
|
||||
} from '@tmagic/stage';
|
||||
|
||||
@ -39,6 +40,8 @@ export interface EditorProps {
|
||||
render?: (stage: StageCore) => HTMLDivElement | Promise<HTMLDivElement>;
|
||||
/** 中间工作区域中画布通过iframe渲染时的页面url */
|
||||
runtimeUrl?: string;
|
||||
/** 是用iframe渲染还是直接渲染 */
|
||||
renderType?: RenderType;
|
||||
/** 选中时是否自动滚动到可视区域 */
|
||||
autoScrollIntoView?: boolean;
|
||||
/** 组件的属性配置表单的dsl */
|
||||
@ -87,4 +90,5 @@ export const defaultEditorProps = {
|
||||
containerHighlightDuration: 800,
|
||||
containerHighlightType: ContainerHighlightType.DEFAULT,
|
||||
codeOptions: () => ({}),
|
||||
renderType: RenderType.IFRAME,
|
||||
};
|
||||
|
@ -31,6 +31,7 @@ export const useStage = (stageOptions: StageOptions) => {
|
||||
containerHighlightDuration: stageOptions.containerHighlightDuration,
|
||||
containerHighlightType: stageOptions.containerHighlightType,
|
||||
disabledDragStart: stageOptions.disabledDragStart,
|
||||
renderType: stageOptions.renderType,
|
||||
canSelect: (el, event, stop) => {
|
||||
const elCanSelect = stageOptions.canSelect(el);
|
||||
// 在组件联动过程中不能再往下选择,返回并触发 ui-select
|
||||
|
@ -18,7 +18,7 @@ const createPageNodeStatus = (page: MPage, initalLayerNodeStatus?: Map<Id, Layer
|
||||
});
|
||||
|
||||
page.items.forEach((node: MNode) =>
|
||||
traverseNode(node, (node) => {
|
||||
traverseNode<MNode>(node, (node) => {
|
||||
map.set(
|
||||
node.id,
|
||||
initalLayerNodeStatus?.get(node.id) || {
|
||||
|
@ -25,6 +25,7 @@ import type {
|
||||
ContainerHighlightType,
|
||||
CustomizeMoveableOptionsCallbackConfig,
|
||||
MoveableOptions,
|
||||
RenderType,
|
||||
UpdateDragEl,
|
||||
} from '@tmagic/stage';
|
||||
|
||||
@ -129,6 +130,7 @@ export interface StageOptions {
|
||||
canSelect: (el: HTMLElement) => boolean | Promise<boolean>;
|
||||
isContainer: (el: HTMLElement) => boolean | Promise<boolean>;
|
||||
updateDragEl: UpdateDragEl;
|
||||
renderType: RenderType;
|
||||
}
|
||||
|
||||
export interface StoreState {
|
||||
|
@ -257,13 +257,22 @@ export const serializeConfig = (config: any) =>
|
||||
unsafe: true,
|
||||
}).replace(/"(\w+)":\s/g, '$1: ');
|
||||
|
||||
export const traverseNode = (node: MNode, cb: (node: MNode, parents: MNode[]) => void, parents: MNode[] = []) => {
|
||||
export interface NodeItem {
|
||||
items?: NodeItem[];
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export const traverseNode = <T extends NodeItem = NodeItem>(
|
||||
node: T,
|
||||
cb: (node: T, parents: T[]) => void,
|
||||
parents: T[] = [],
|
||||
) => {
|
||||
cb(node, parents);
|
||||
|
||||
if (node.items?.length) {
|
||||
parents.push(node);
|
||||
node.items.forEach((item: MNode) => {
|
||||
traverseNode(item, cb, [...parents]);
|
||||
node.items.forEach((item) => {
|
||||
traverseNode(item as T, cb, [...parents]);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="config"
|
||||
:id="config.id"
|
||||
:style="config.tip ? 'display: flex;align-items: baseline;' : ''"
|
||||
:class="`m-form-container m-container-${type || ''} ${config.className || ''}`"
|
||||
>
|
||||
|
@ -61,6 +61,7 @@ export default class StageCore extends EventEmitter {
|
||||
this.renderer = new StageRender({
|
||||
runtimeUrl: config.runtimeUrl,
|
||||
zoom: config.zoom,
|
||||
renderType: config.renderType,
|
||||
customizedRender: async (): Promise<HTMLElement | null> => {
|
||||
if (this?.customizedRender) {
|
||||
return await this.customizedRender(this);
|
||||
|
@ -23,7 +23,15 @@ import { getHost, injectStyle, isSameDomain } from '@tmagic/utils';
|
||||
|
||||
import { DEFAULT_ZOOM } from './const';
|
||||
import style from './style.css?raw';
|
||||
import type { Point, RemoveData, Runtime, RuntimeWindow, StageRenderConfig, UpdateData } from './types';
|
||||
import {
|
||||
type Point,
|
||||
type RemoveData,
|
||||
RenderType,
|
||||
type Runtime,
|
||||
type RuntimeWindow,
|
||||
type StageRenderConfig,
|
||||
type UpdateData,
|
||||
} from './types';
|
||||
import { addSelectedClassName, removeSelectedClassName } from './util';
|
||||
|
||||
export default class StageRender extends EventEmitter {
|
||||
@ -31,28 +39,26 @@ export default class StageRender extends EventEmitter {
|
||||
public contentWindow: RuntimeWindow | null = null;
|
||||
public runtime: Runtime | null = null;
|
||||
public iframe?: HTMLIFrameElement;
|
||||
public nativeContainer?: HTMLDivElement;
|
||||
|
||||
private runtimeUrl?: string;
|
||||
private zoom = DEFAULT_ZOOM;
|
||||
private renderType: RenderType;
|
||||
private customizedRender?: () => Promise<HTMLElement | null>;
|
||||
|
||||
constructor({ runtimeUrl, zoom, customizedRender }: StageRenderConfig) {
|
||||
constructor({ runtimeUrl, zoom, customizedRender, renderType = RenderType.IFRAME }: StageRenderConfig) {
|
||||
super();
|
||||
|
||||
this.renderType = renderType;
|
||||
this.runtimeUrl = runtimeUrl || '';
|
||||
this.customizedRender = customizedRender;
|
||||
this.setZoom(zoom);
|
||||
|
||||
this.iframe = globalThis.document.createElement('iframe');
|
||||
// 同源,直接加载
|
||||
this.iframe.src = isSameDomain(this.runtimeUrl) ? this.runtimeUrl : '';
|
||||
this.iframe.style.cssText = `
|
||||
border: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
this.iframe.addEventListener('load', this.loadHandler);
|
||||
if (this.renderType === RenderType.IFRAME) {
|
||||
this.createIframe();
|
||||
} else if (this.renderType === RenderType.NATIVE) {
|
||||
this.createNativeContainer();
|
||||
}
|
||||
}
|
||||
|
||||
public getMagicApi = () => ({
|
||||
@ -102,22 +108,22 @@ export default class StageRender extends EventEmitter {
|
||||
* @param el 将页面挂载到该Dom节点上
|
||||
*/
|
||||
public async mount(el: HTMLDivElement) {
|
||||
if (!this.iframe) {
|
||||
throw Error('mount 失败');
|
||||
if (this.iframe) {
|
||||
if (!isSameDomain(this.runtimeUrl) && this.runtimeUrl) {
|
||||
// 不同域,使用srcdoc发起异步请求,需要目标地址支持跨域
|
||||
let html = await fetch(this.runtimeUrl).then((res) => res.text());
|
||||
// 使用base, 解决相对路径或绝对路径的问题
|
||||
const base = `${location.protocol}//${getHost(this.runtimeUrl)}`;
|
||||
html = html.replace('<head>', `<head>\n<base href="${base}">`);
|
||||
this.iframe.srcdoc = html;
|
||||
}
|
||||
|
||||
el.appendChild<HTMLIFrameElement>(this.iframe);
|
||||
|
||||
this.postTmagicRuntimeReady();
|
||||
} else if (this.nativeContainer) {
|
||||
el.appendChild(this.nativeContainer);
|
||||
}
|
||||
|
||||
if (!isSameDomain(this.runtimeUrl) && this.runtimeUrl) {
|
||||
// 不同域,使用srcdoc发起异步请求,需要目标地址支持跨域
|
||||
let html = await fetch(this.runtimeUrl).then((res) => res.text());
|
||||
// 使用base, 解决相对路径或绝对路径的问题
|
||||
const base = `${location.protocol}//${getHost(this.runtimeUrl)}`;
|
||||
html = html.replace('<head>', `<head>\n<base href="${base}">`);
|
||||
this.iframe.srcdoc = html;
|
||||
}
|
||||
|
||||
el.appendChild<HTMLIFrameElement>(this.iframe);
|
||||
|
||||
this.postTmagicRuntimeReady();
|
||||
}
|
||||
|
||||
public getRuntime = (): Promise<Runtime> => {
|
||||
@ -150,6 +156,12 @@ export default class StageRender extends EventEmitter {
|
||||
x = x - rect.left;
|
||||
y = y - rect.top;
|
||||
}
|
||||
} else if (this.nativeContainer) {
|
||||
const rect = this.nativeContainer.getClientRects()[0];
|
||||
if (rect) {
|
||||
x = x - rect.left;
|
||||
y = y - rect.top;
|
||||
}
|
||||
}
|
||||
|
||||
return this.getDocument()?.elementsFromPoint(x / this.zoom, y / this.zoom) as HTMLElement[];
|
||||
@ -168,13 +180,42 @@ export default class StageRender extends EventEmitter {
|
||||
* 销毁实例
|
||||
*/
|
||||
public destroy(): void {
|
||||
this.iframe?.removeEventListener('load', this.loadHandler);
|
||||
this.iframe?.removeEventListener('load', this.iframeLoadHandler);
|
||||
this.contentWindow = null;
|
||||
this.iframe?.remove();
|
||||
this.iframe = undefined;
|
||||
this.removeAllListeners();
|
||||
}
|
||||
|
||||
private createIframe(): HTMLIFrameElement {
|
||||
this.iframe = globalThis.document.createElement('iframe');
|
||||
// 同源,直接加载
|
||||
this.iframe.src = this.runtimeUrl && isSameDomain(this.runtimeUrl) ? this.runtimeUrl : '';
|
||||
this.iframe.style.cssText = `
|
||||
border: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
`;
|
||||
|
||||
this.iframe.addEventListener('load', this.iframeLoadHandler);
|
||||
|
||||
return this.iframe;
|
||||
}
|
||||
|
||||
private async createNativeContainer() {
|
||||
this.contentWindow = globalThis as unknown as RuntimeWindow;
|
||||
this.nativeContainer = globalThis.document.createElement('div');
|
||||
|
||||
this.contentWindow.magic = this.getMagicApi();
|
||||
|
||||
if (this.customizedRender) {
|
||||
const el = await this.customizedRender();
|
||||
if (el) {
|
||||
this.nativeContainer.appendChild(el);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在runtime中对被选中的元素进行标记,部分组件有对选中态进行特殊显示的需求
|
||||
* @param el 被选中的元素
|
||||
@ -187,7 +228,7 @@ export default class StageRender extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
private loadHandler = async () => {
|
||||
private iframeLoadHandler = async () => {
|
||||
if (!this.contentWindow?.magic) {
|
||||
this.postTmagicRuntimeReady();
|
||||
}
|
||||
|
@ -96,7 +96,7 @@ export default class TargetShadow {
|
||||
el.style.cssText = getTargetElStyle(target, this.zIndex);
|
||||
|
||||
if (typeof this.updateDragEl === 'function') {
|
||||
this.updateDragEl(el, target);
|
||||
this.updateDragEl(el, target, this.container);
|
||||
}
|
||||
const isFixed = isFixedParent(target);
|
||||
const mode = this.container.dataset.mode || Mode.ABSOLUTE;
|
||||
|
@ -52,7 +52,12 @@ export enum ContainerHighlightType {
|
||||
ALT = 'alt',
|
||||
}
|
||||
|
||||
export type UpdateDragEl = (el: TargetElement, target: TargetElement) => void;
|
||||
export enum RenderType {
|
||||
IFRAME = 'iframe',
|
||||
NATIVE = 'native',
|
||||
}
|
||||
|
||||
export type UpdateDragEl = (el: TargetElement, target: TargetElement, container: HTMLElement) => void;
|
||||
|
||||
export interface StageCoreConfig {
|
||||
/** 需要对齐的dom节点的CSS选择器字符串 */
|
||||
@ -71,6 +76,7 @@ export interface StageCoreConfig {
|
||||
autoScrollIntoView?: boolean;
|
||||
updateDragEl?: UpdateDragEl;
|
||||
disabledDragStart?: boolean;
|
||||
renderType?: RenderType;
|
||||
}
|
||||
|
||||
export interface ActionManagerConfig {
|
||||
@ -107,6 +113,7 @@ export interface CustomizeMoveableOptionsCallbackConfig {
|
||||
export interface StageRenderConfig {
|
||||
runtimeUrl?: string;
|
||||
zoom: number | undefined;
|
||||
renderType?: RenderType;
|
||||
customizedRender?: () => Promise<HTMLElement | null>;
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user