import { shallowReactive } from 'vue'; import type { Writable } from 'type-fest'; import StageCore from '@tmagic/stage'; import { getIdFromEl } from '@tmagic/utils'; import { useStage } from '@editor/hooks/use-stage'; import BaseService from '@editor/services//BaseService'; import editorService from '@editor/services//editor'; import type { StageOptions, StageOverlayState, SyncHookPlugin } from '@editor/type'; const canUsePluginMethods = { async: [], sync: ['openOverlay', 'closeOverlay', 'updateOverlay', 'createStage'] as const, }; type SyncMethodName = Writable<(typeof canUsePluginMethods)['sync']>; class StageOverlay extends BaseService { private state: StageOverlayState = shallowReactive({ wrapDiv: document.createElement('div'), sourceEl: null, contentEl: null, stage: null, stageOptions: null, wrapWidth: 0, wrapHeight: 0, stageOverlayVisible: false, }); constructor() { super(canUsePluginMethods.sync.map((methodName) => ({ name: methodName, isAsync: false }))); this.get('wrapDiv').classList.add('tmagic-editor-sub-stage-wrap'); } public get(name: K): StageOverlayState[K] { return this.state[name]; } public set(name: K, value: T) { this.state[name] = value; } public openOverlay(el: HTMLElement | null) { const stageOptions = this.get('stageOptions'); if (!el || !stageOptions) return; this.set('sourceEl', el); this.createContentEl(); this.set('stageOverlayVisible', true); editorService.on('update', this.updateHandler); editorService.on('add', this.addHandler); editorService.on('remove', this.removeHandler); editorService.on('drag-to', this.updateHandler); editorService.on('move-layer', this.updateHandler); } public closeOverlay() { this.set('stageOverlayVisible', false); const subStage = this.get('stage'); const wrapDiv = this.get('wrapDiv'); subStage?.destroy(); for (let i = 0, l = wrapDiv.children.length; i < l; i++) { const child = wrapDiv.children[i]; child.remove(); } wrapDiv.remove(); this.set('stage', null); this.set('sourceEl', null); this.set('contentEl', null); editorService.off('update', this.updateHandler); editorService.off('add', this.addHandler); editorService.off('remove', this.removeHandler); editorService.off('drag-to', this.updateHandler); editorService.off('move-layer', this.updateHandler); } public updateOverlay() { const sourceEl = this.get('sourceEl'); if (!sourceEl) return; const { scrollWidth, scrollHeight } = sourceEl; this.set('wrapWidth', scrollWidth); this.set('wrapHeight', scrollHeight); } public createStage(stageOptions: StageOptions = {}) { return useStage({ ...stageOptions, runtimeUrl: '', autoScrollIntoView: false, disabledRule: true, render: async (stage: StageCore) => { this.copyDocumentElement(); const rootEls = stage.renderer?.getDocument()?.body.children; if (rootEls) { Array.from(rootEls).forEach((element) => { if (['SCRIPT', 'STYLE'].includes(element.tagName)) { return; } element.remove(); }); } const wrapDiv = this.get('wrapDiv'); await this.render(); return wrapDiv; }, }); } public usePlugin(options: SyncHookPlugin): void { super.usePlugin(options); } private createContentEl() { const sourceEl = this.get('sourceEl'); if (!sourceEl) return; const contentEl = sourceEl.cloneNode(true) as HTMLElement; this.set('contentEl', contentEl); contentEl.style.position = 'static'; contentEl.style.overflow = 'visible'; } private copyDocumentElement() { const subStage = this.get('stage'); const stage = editorService.get('stage'); const doc = subStage?.renderer?.getDocument(); const documentElement = stage?.renderer?.getDocument()?.documentElement; if (doc && documentElement) { doc.replaceChild(documentElement.cloneNode(true), doc.documentElement); } } private async render() { this.createContentEl(); const contentEl = this.get('contentEl'); const sourceEl = this.get('sourceEl'); const wrapDiv = this.get('wrapDiv'); const subStage = this.get('stage'); const stageOptions = this.get('stageOptions'); if (!contentEl) return; wrapDiv.style.cssText = ` width: ${sourceEl?.scrollWidth}px; height: ${sourceEl?.scrollHeight}px; background-color: #fff; `; for (let i = 0, l = wrapDiv.children.length; i < l; i++) { const child = wrapDiv.children[i]; child.remove(); } wrapDiv.appendChild(contentEl); setTimeout(() => { subStage?.renderer?.contentWindow?.magic.onPageElUpdate(wrapDiv); }); if (await stageOptions?.canSelect?.(contentEl)) { const id = getIdFromEl()(contentEl); id && subStage?.select(id); } } private updateHandler = () => { setTimeout(() => { this.render(); this.updateOverlay(); this.updateSelectStatus(); }); }; private addHandler = () => { this.render(); this.updateOverlay(); this.updateSelectStatus(); }; private removeHandler = () => { this.render(); this.updateOverlay(); this.updateSelectStatus(); }; private updateSelectStatus() { const subStage = this.get('stage'); const nodes = editorService.get('nodes'); if (nodes.length > 1) { subStage?.multiSelect(nodes.map((n) => n.id)); } else { subStage?.select(nodes[0].id); } } } export type StageOverlayService = StageOverlay; export default new StageOverlay();