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, containerHighlightDuration: props.containerHighlightDuration,
containerHighlightType: props.containerHighlightType, containerHighlightType: props.containerHighlightType,
disabledDragStart: props.disabledDragStart, disabledDragStart: props.disabledDragStart,
renderType: props.renderType,
}), }),
); );

View File

@ -7,6 +7,7 @@ import StageCore, {
ContainerHighlightType, ContainerHighlightType,
CustomizeMoveableOptionsCallbackConfig, CustomizeMoveableOptionsCallbackConfig,
MoveableOptions, MoveableOptions,
RenderType,
UpdateDragEl, UpdateDragEl,
} from '@tmagic/stage'; } from '@tmagic/stage';
@ -39,6 +40,8 @@ export interface EditorProps {
render?: (stage: StageCore) => HTMLDivElement | Promise<HTMLDivElement>; render?: (stage: StageCore) => HTMLDivElement | Promise<HTMLDivElement>;
/** 中间工作区域中画布通过iframe渲染时的页面url */ /** 中间工作区域中画布通过iframe渲染时的页面url */
runtimeUrl?: string; runtimeUrl?: string;
/** 是用iframe渲染还是直接渲染 */
renderType?: RenderType;
/** 选中时是否自动滚动到可视区域 */ /** 选中时是否自动滚动到可视区域 */
autoScrollIntoView?: boolean; autoScrollIntoView?: boolean;
/** 组件的属性配置表单的dsl */ /** 组件的属性配置表单的dsl */
@ -87,4 +90,5 @@ export const defaultEditorProps = {
containerHighlightDuration: 800, containerHighlightDuration: 800,
containerHighlightType: ContainerHighlightType.DEFAULT, containerHighlightType: ContainerHighlightType.DEFAULT,
codeOptions: () => ({}), codeOptions: () => ({}),
renderType: RenderType.IFRAME,
}; };

View File

@ -31,6 +31,7 @@ export const useStage = (stageOptions: StageOptions) => {
containerHighlightDuration: stageOptions.containerHighlightDuration, containerHighlightDuration: stageOptions.containerHighlightDuration,
containerHighlightType: stageOptions.containerHighlightType, containerHighlightType: stageOptions.containerHighlightType,
disabledDragStart: stageOptions.disabledDragStart, disabledDragStart: stageOptions.disabledDragStart,
renderType: stageOptions.renderType,
canSelect: (el, event, stop) => { canSelect: (el, event, stop) => {
const elCanSelect = stageOptions.canSelect(el); const elCanSelect = stageOptions.canSelect(el);
// 在组件联动过程中不能再往下选择,返回并触发 ui-select // 在组件联动过程中不能再往下选择,返回并触发 ui-select

View File

@ -18,7 +18,7 @@ const createPageNodeStatus = (page: MPage, initalLayerNodeStatus?: Map<Id, Layer
}); });
page.items.forEach((node: MNode) => page.items.forEach((node: MNode) =>
traverseNode(node, (node) => { traverseNode<MNode>(node, (node) => {
map.set( map.set(
node.id, node.id,
initalLayerNodeStatus?.get(node.id) || { initalLayerNodeStatus?.get(node.id) || {

View File

@ -25,6 +25,7 @@ import type {
ContainerHighlightType, ContainerHighlightType,
CustomizeMoveableOptionsCallbackConfig, CustomizeMoveableOptionsCallbackConfig,
MoveableOptions, MoveableOptions,
RenderType,
UpdateDragEl, UpdateDragEl,
} from '@tmagic/stage'; } from '@tmagic/stage';
@ -129,6 +130,7 @@ export interface StageOptions {
canSelect: (el: HTMLElement) => boolean | Promise<boolean>; canSelect: (el: HTMLElement) => boolean | Promise<boolean>;
isContainer: (el: HTMLElement) => boolean | Promise<boolean>; isContainer: (el: HTMLElement) => boolean | Promise<boolean>;
updateDragEl: UpdateDragEl; updateDragEl: UpdateDragEl;
renderType: RenderType;
} }
export interface StoreState { export interface StoreState {

View File

@ -257,13 +257,22 @@ export const serializeConfig = (config: any) =>
unsafe: true, unsafe: true,
}).replace(/"(\w+)":\s/g, '$1: '); }).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); cb(node, parents);
if (node.items?.length) { if (node.items?.length) {
parents.push(node); parents.push(node);
node.items.forEach((item: MNode) => { node.items.forEach((item) => {
traverseNode(item, cb, [...parents]); traverseNode(item as T, cb, [...parents]);
}); });
} }
}; };

View File

@ -1,6 +1,7 @@
<template> <template>
<div <div
v-if="config" v-if="config"
:id="config.id"
:style="config.tip ? 'display: flex;align-items: baseline;' : ''" :style="config.tip ? 'display: flex;align-items: baseline;' : ''"
:class="`m-form-container m-container-${type || ''} ${config.className || ''}`" :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({ this.renderer = new StageRender({
runtimeUrl: config.runtimeUrl, runtimeUrl: config.runtimeUrl,
zoom: config.zoom, zoom: config.zoom,
renderType: config.renderType,
customizedRender: async (): Promise<HTMLElement | null> => { customizedRender: async (): Promise<HTMLElement | null> => {
if (this?.customizedRender) { if (this?.customizedRender) {
return await this.customizedRender(this); return await this.customizedRender(this);

View File

@ -23,7 +23,15 @@ import { getHost, injectStyle, isSameDomain } from '@tmagic/utils';
import { DEFAULT_ZOOM } from './const'; import { DEFAULT_ZOOM } from './const';
import style from './style.css?raw'; 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'; import { addSelectedClassName, removeSelectedClassName } from './util';
export default class StageRender extends EventEmitter { export default class StageRender extends EventEmitter {
@ -31,28 +39,26 @@ export default class StageRender extends EventEmitter {
public contentWindow: RuntimeWindow | null = null; public contentWindow: RuntimeWindow | null = null;
public runtime: Runtime | null = null; public runtime: Runtime | null = null;
public iframe?: HTMLIFrameElement; public iframe?: HTMLIFrameElement;
public nativeContainer?: HTMLDivElement;
private runtimeUrl?: string; private runtimeUrl?: string;
private zoom = DEFAULT_ZOOM; private zoom = DEFAULT_ZOOM;
private renderType: RenderType;
private customizedRender?: () => Promise<HTMLElement | null>; private customizedRender?: () => Promise<HTMLElement | null>;
constructor({ runtimeUrl, zoom, customizedRender }: StageRenderConfig) { constructor({ runtimeUrl, zoom, customizedRender, renderType = RenderType.IFRAME }: StageRenderConfig) {
super(); super();
this.renderType = renderType;
this.runtimeUrl = runtimeUrl || ''; this.runtimeUrl = runtimeUrl || '';
this.customizedRender = customizedRender; this.customizedRender = customizedRender;
this.setZoom(zoom); this.setZoom(zoom);
this.iframe = globalThis.document.createElement('iframe'); if (this.renderType === RenderType.IFRAME) {
// 同源,直接加载 this.createIframe();
this.iframe.src = isSameDomain(this.runtimeUrl) ? this.runtimeUrl : ''; } else if (this.renderType === RenderType.NATIVE) {
this.iframe.style.cssText = ` this.createNativeContainer();
border: 0; }
width: 100%;
height: 100%;
`;
this.iframe.addEventListener('load', this.loadHandler);
} }
public getMagicApi = () => ({ public getMagicApi = () => ({
@ -102,10 +108,7 @@ export default class StageRender extends EventEmitter {
* @param el Dom节点上 * @param el Dom节点上
*/ */
public async mount(el: HTMLDivElement) { public async mount(el: HTMLDivElement) {
if (!this.iframe) { if (this.iframe) {
throw Error('mount 失败');
}
if (!isSameDomain(this.runtimeUrl) && this.runtimeUrl) { if (!isSameDomain(this.runtimeUrl) && this.runtimeUrl) {
// 不同域使用srcdoc发起异步请求需要目标地址支持跨域 // 不同域使用srcdoc发起异步请求需要目标地址支持跨域
let html = await fetch(this.runtimeUrl).then((res) => res.text()); let html = await fetch(this.runtimeUrl).then((res) => res.text());
@ -118,6 +121,9 @@ export default class StageRender extends EventEmitter {
el.appendChild<HTMLIFrameElement>(this.iframe); el.appendChild<HTMLIFrameElement>(this.iframe);
this.postTmagicRuntimeReady(); this.postTmagicRuntimeReady();
} else if (this.nativeContainer) {
el.appendChild(this.nativeContainer);
}
} }
public getRuntime = (): Promise<Runtime> => { public getRuntime = (): Promise<Runtime> => {
@ -150,6 +156,12 @@ export default class StageRender extends EventEmitter {
x = x - rect.left; x = x - rect.left;
y = y - rect.top; 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[]; 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 { public destroy(): void {
this.iframe?.removeEventListener('load', this.loadHandler); this.iframe?.removeEventListener('load', this.iframeLoadHandler);
this.contentWindow = null; this.contentWindow = null;
this.iframe?.remove(); this.iframe?.remove();
this.iframe = undefined; this.iframe = undefined;
this.removeAllListeners(); 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中对被选中的元素进行标记 * runtime中对被选中的元素进行标记
* @param el * @param el
@ -187,7 +228,7 @@ export default class StageRender extends EventEmitter {
} }
} }
private loadHandler = async () => { private iframeLoadHandler = async () => {
if (!this.contentWindow?.magic) { if (!this.contentWindow?.magic) {
this.postTmagicRuntimeReady(); this.postTmagicRuntimeReady();
} }

View File

@ -96,7 +96,7 @@ export default class TargetShadow {
el.style.cssText = getTargetElStyle(target, this.zIndex); el.style.cssText = getTargetElStyle(target, this.zIndex);
if (typeof this.updateDragEl === 'function') { if (typeof this.updateDragEl === 'function') {
this.updateDragEl(el, target); this.updateDragEl(el, target, this.container);
} }
const isFixed = isFixedParent(target); const isFixed = isFixedParent(target);
const mode = this.container.dataset.mode || Mode.ABSOLUTE; const mode = this.container.dataset.mode || Mode.ABSOLUTE;

View File

@ -52,7 +52,12 @@ export enum ContainerHighlightType {
ALT = 'alt', 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 { export interface StageCoreConfig {
/** 需要对齐的dom节点的CSS选择器字符串 */ /** 需要对齐的dom节点的CSS选择器字符串 */
@ -71,6 +76,7 @@ export interface StageCoreConfig {
autoScrollIntoView?: boolean; autoScrollIntoView?: boolean;
updateDragEl?: UpdateDragEl; updateDragEl?: UpdateDragEl;
disabledDragStart?: boolean; disabledDragStart?: boolean;
renderType?: RenderType;
} }
export interface ActionManagerConfig { export interface ActionManagerConfig {
@ -107,6 +113,7 @@ export interface CustomizeMoveableOptionsCallbackConfig {
export interface StageRenderConfig { export interface StageRenderConfig {
runtimeUrl?: string; runtimeUrl?: string;
zoom: number | undefined; zoom: number | undefined;
renderType?: RenderType;
customizedRender?: () => Promise<HTMLElement | null>; customizedRender?: () => Promise<HTMLElement | null>;
} }