feat(stage, editor): runtime支持直接渲染模式不用iframe

This commit is contained in:
roymondchen 2023-12-12 15:09:33 +08:00
parent 0c994f1e23
commit ba2f1e5ac5
11 changed files with 102 additions and 35 deletions

View File

@ -175,6 +175,7 @@ provide(
containerHighlightDuration: props.containerHighlightDuration,
containerHighlightType: props.containerHighlightType,
disabledDragStart: props.disabledDragStart,
renderType: props.renderType,
}),
);

View File

@ -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,
};

View File

@ -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

View File

@ -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) || {

View File

@ -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 {

View File

@ -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]);
});
}
};

View File

@ -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 || ''}`"
>

View File

@ -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);

View File

@ -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();
}

View File

@ -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;

View File

@ -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>;
}