diff --git a/packages/editor/src/components/ScrollViewer.vue b/packages/editor/src/components/ScrollViewer.vue index d4583e0f..f886560c 100644 --- a/packages/editor/src/components/ScrollViewer.vue +++ b/packages/editor/src/components/ScrollViewer.vue @@ -4,11 +4,13 @@ + + ) => { +const createPageNodeStatus = (nodeData: TreeNodeData[], initialLayerNodeStatus?: Map) => { const map = new Map(); nodeData.forEach((node: MNode) => traverseNode(node, (node) => { map.set( node.id, - initalLayerNodeStatus?.get(node.id) || { + initialLayerNodeStatus?.get(node.id) || { visible: true, expand: false, selected: false, diff --git a/packages/editor/src/hooks/use-stage-overlay.ts b/packages/editor/src/hooks/use-stage-overlay.ts new file mode 100644 index 00000000..01ba8cc2 --- /dev/null +++ b/packages/editor/src/hooks/use-stage-overlay.ts @@ -0,0 +1,158 @@ +import { computed, inject, nextTick, ref, watch } from 'vue'; + +import type StageCore from '@tmagic/stage'; + +import type { Services, StageOptions } from '@editor/type'; + +import { useStage } from './use-stage'; + +export const useStageOverlay = () => { + const services = inject('services'); + const stageOptions = inject('stageOptions'); + + const wrapWidth = ref(0); + const wrapHeight = ref(0); + const stageOverlayVisible = ref(false); + const stageOverlay = ref(); + + const stage = computed(() => services?.editorService.get('stage')); + + let subStage: StageCore | null = null; + + const div = document.createElement('div'); + let selectEl: HTMLElement | null = null; + + const render = () => { + if (!selectEl) return; + + const content = selectEl.cloneNode(true) as HTMLElement; + content.style.position = 'static'; + Array.from(div.children).forEach((element) => { + element.remove(); + }); + div.appendChild(content); + + subStage?.renderer.contentWindow?.magic.onPageElUpdate(div); + + subStage?.select(content); + }; + + const copyDocumentElement = () => { + const doc = subStage?.renderer.getDocument(); + const documentElement = stage.value?.renderer.getDocument()?.documentElement; + + if (doc && documentElement) { + doc.replaceChild(documentElement.cloneNode(true), doc.documentElement); + } + }; + + const updateOverlay = () => { + if (!selectEl) return; + + const { scrollWidth, scrollHeight } = selectEl; + + stageOverlay.value!.style.width = `${scrollWidth}px`; + stageOverlay.value!.style.height = `${scrollHeight}px`; + + wrapWidth.value = scrollWidth; + wrapHeight.value = scrollHeight; + }; + + const updateHandler = () => { + render(); + updateOverlay(); + }; + + const addHandler = () => { + render(); + updateOverlay(); + }; + + const removeHandler = () => { + render(); + updateOverlay(); + }; + + const openOverlay = async (el: HTMLElement) => { + selectEl = el; + + stageOverlayVisible.value = true; + + if (!stageOverlay.value) { + await nextTick(); + } + + if (!stageOptions) { + return; + } + + subStage = useStage({ + ...stageOptions, + runtimeUrl: '', + autoScrollIntoView: false, + render(stage: StageCore) { + copyDocumentElement(); + + const rootEl = stage.renderer.getDocument()?.getElementById('app'); + if (rootEl) { + rootEl.remove(); + } + + div.style.cssText = ` + width: ${el.scrollWidth}px; + height: ${el.scrollHeight}px; + background-color: #fff; + `; + + render(); + + return div; + }, + }); + subStage.mount(stageOverlay.value!); + + const { mask, renderer } = subStage; + + const { contentWindow } = renderer; + mask.showRule(false); + + updateOverlay(); + + contentWindow?.magic.onRuntimeReady({}); + + services?.editorService.on('update', updateHandler); + services?.editorService.on('add', addHandler); + services?.editorService.on('remove', removeHandler); + }; + + const closeOverlay = () => { + stageOverlayVisible.value = false; + subStage?.destroy(); + subStage = null; + + services?.editorService.off('update', updateHandler); + services?.editorService.off('add', addHandler); + services?.editorService.off('remove', removeHandler); + }; + + watch(stage, (stage) => { + if (stage) { + stage.on('dblclick', async (event: MouseEvent) => { + const el = await stage.actionManager.getElementFromPoint(event); + if (el) { + openOverlay(el); + } + }); + } else if (subStage) { + closeOverlay(); + } + }); + + return { + wrapWidth, + wrapHeight, + stageOverlayVisible, + stageOverlay, + closeOverlay, + }; +}; diff --git a/packages/editor/src/layouts/workspace/viewer/Stage.vue b/packages/editor/src/layouts/workspace/viewer/Stage.vue index 1e4d0081..3ce7d6f1 100644 --- a/packages/editor/src/layouts/workspace/viewer/Stage.vue +++ b/packages/editor/src/layouts/workspace/viewer/Stage.vue @@ -22,16 +22,21 @@ @drop="dropHandler" @dragover="dragoverHandler" > + - - - + @@ -49,6 +54,7 @@ import { getConfig } from '@editor/utils/config'; import { KeyBindingContainerKey } from '@editor/utils/keybinding-config'; import NodeListMenu from './NodeListMenu.vue'; +import StageOverlay from './StageOverlay.vue'; import ViewerMenu from './ViewerMenu.vue'; defineOptions({ @@ -93,10 +99,13 @@ watchEffect(() => { services?.editorService.set('stage', markRaw(stage)); - stage?.mount(stageContainer.value); + stage.mount(stageContainer.value); - if (!node.value?.id) return; - stage?.on('runtime-ready', (rt) => { + if (!node.value?.id) { + return; + } + + stage.on('runtime-ready', (rt) => { runtime = rt; // toRaw返回的值是一个引用而非快照,需要cloneDeep root.value && runtime?.updateRootConfig?.(cloneDeep(toRaw(root.value))); diff --git a/packages/editor/src/layouts/workspace/viewer/StageOverlay.vue b/packages/editor/src/layouts/workspace/viewer/StageOverlay.vue new file mode 100644 index 00000000..d4a086a4 --- /dev/null +++ b/packages/editor/src/layouts/workspace/viewer/StageOverlay.vue @@ -0,0 +1,16 @@ + + + diff --git a/packages/editor/src/theme/stage.scss b/packages/editor/src/theme/stage.scss index 9ae994b1..8f99244c 100644 --- a/packages/editor/src/theme/stage.scss +++ b/packages/editor/src/theme/stage.scss @@ -26,6 +26,30 @@ } } +.m-editor-stage-overlay { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.6); + display: flex; + z-index: 20; + overflow: auto; +} + +.m-editor-stage-overlay-container { + position: relative; + flex-shrink: 0; + margin: auto; +} + +.m-editor-stage-overlay-close.tmagic-design-icon { + position: fixed; + right: 10px; + top: 10px; +} + .m-editor-stage-float-button { cursor: pointer; transform: translateY(-50%); diff --git a/packages/stage/src/ActionManager.ts b/packages/stage/src/ActionManager.ts index e6b9b708..77d8c1ba 100644 --- a/packages/stage/src/ActionManager.ts +++ b/packages/stage/src/ActionManager.ts @@ -360,6 +360,7 @@ export default class ActionManager extends EventEmitter { this.container.removeEventListener('mousemove', this.mouseMoveHandler); this.container.removeEventListener('mouseleave', this.mouseLeaveHandler); this.container.removeEventListener('wheel', this.mouseWheelHandler); + this.container.removeEventListener('dblclick', this.dblclickHandler); this.dr.destroy(); this.multiDr?.destroy(); this.highlightLayer.destroy(); @@ -512,6 +513,7 @@ export default class ActionManager extends EventEmitter { this.container.addEventListener('mousemove', this.mouseMoveHandler); this.container.addEventListener('mouseleave', this.mouseLeaveHandler); this.container.addEventListener('wheel', this.mouseWheelHandler); + this.container.addEventListener('dblclick', this.dblclickHandler); } /** @@ -619,4 +621,8 @@ export default class ActionManager extends EventEmitter { private mouseWheelHandler = () => { this.clearHighlight(); }; + + private dblclickHandler = (event: MouseEvent) => { + this.emit('dblclick', event); + }; } diff --git a/packages/stage/src/StageCore.ts b/packages/stage/src/StageCore.ts index 6e556cb8..c3f87da3 100644 --- a/packages/stage/src/StageCore.ts +++ b/packages/stage/src/StageCore.ts @@ -46,8 +46,8 @@ export default class StageCore extends EventEmitter { public container?: HTMLDivElement; public renderer: StageRender; public mask: StageMask; + public actionManager: ActionManager; - private actionManager: ActionManager; private pageResizeObserver: ResizeObserver | null = null; private autoScrollIntoView: boolean | undefined; private customizedRender?: CustomizeRender; @@ -329,6 +329,9 @@ export default class StageCore extends EventEmitter { }) .on('multi-select', (selectedElList: HTMLElement[], event: MouseEvent) => { this.emit('multi-select', selectedElList, event); + }) + .on('dblclick', (event: MouseEvent) => { + this.emit('dblclick', event); }); } diff --git a/packages/stage/src/TargetShadow.ts b/packages/stage/src/TargetShadow.ts index a6a21d5e..b68ea718 100644 --- a/packages/stage/src/TargetShadow.ts +++ b/packages/stage/src/TargetShadow.ts @@ -15,6 +15,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { guid } from '@tmagic/utils'; + import { Mode, ZIndex } from './const'; import type { TargetElement as ShadowElement, TargetShadowConfig, UpdateDragEl } from './types'; import { getTargetElStyle, isFixedParent } from './util'; @@ -26,7 +28,7 @@ export default class TargetShadow { public el?: ShadowElement; public els: ShadowElement[] = []; - private idPrefix = 'target_calibrate_'; + private idPrefix = `target_calibrate_${guid()}`; private container: HTMLElement; private scrollLeft = 0; private scrollTop = 0; @@ -46,7 +48,7 @@ export default class TargetShadow { } if (config.idPrefix) { - this.idPrefix = config.idPrefix; + this.idPrefix = `${config.idPrefix}_${guid()}`; } this.container.addEventListener('customScroll', this.scrollHandler);