feat(editor): 画布自适应大小

This commit is contained in:
roymondchen 2022-03-14 19:10:01 +08:00 committed by jia000
parent 3c7d756d19
commit ab3e113904
8 changed files with 122 additions and 135 deletions

View File

@ -55,7 +55,7 @@ import eventsService from '@editor/services/events';
import historyService from '@editor/services/history'; import historyService from '@editor/services/history';
import propsService from '@editor/services/props'; import propsService from '@editor/services/props';
import uiService from '@editor/services/ui'; import uiService from '@editor/services/ui';
import type { ComponentGroup, MenuBarData, Services, SideBarData } from '@editor/type'; import type { ComponentGroup, MenuBarData, Services, SideBarData, StageRect } from '@editor/type';
export default defineComponent({ export default defineComponent({
name: 'm-editor', name: 'm-editor',
@ -133,8 +133,8 @@ export default defineComponent({
type: Function as PropType<(el: HTMLElement) => boolean | Promise<boolean>>, type: Function as PropType<(el: HTMLElement) => boolean | Promise<boolean>>,
}, },
stageStyle: { stageRect: {
type: [String, Object] as PropType<Record<string, string | number>>, type: [String, Object] as PropType<StageRect>,
}, },
}, },
@ -208,8 +208,8 @@ export default defineComponent({
); );
watch( watch(
() => props.stageStyle, () => props.stageRect,
(stageStyle) => uiService.set('stageStyle', stageStyle), (stageRect) => stageRect && uiService.set('stageRect', stageRect),
{ {
immediate: true, immediate: true,
}, },

View File

@ -1,5 +1,5 @@
<template> <template>
<div class="m-editor-stage"> <div class="m-editor-stage" ref="stageWrap">
<div <div
class="m-editor-stage-container" class="m-editor-stage-container"
ref="stageContainer" ref="stageContainer"
@ -17,6 +17,7 @@ import {
computed, computed,
defineComponent, defineComponent,
inject, inject,
nextTick,
onMounted, onMounted,
onUnmounted, onUnmounted,
PropType, PropType,
@ -31,7 +32,7 @@ import type { MApp, MNode, MPage } from '@tmagic/schema';
import type { MoveableOptions, Runtime, SortEventData, UpdateEventData } from '@tmagic/stage'; import type { MoveableOptions, Runtime, SortEventData, UpdateEventData } from '@tmagic/stage';
import StageCore from '@tmagic/stage'; import StageCore from '@tmagic/stage';
import type { Services } from '@editor/type'; import type { Services, StageRect } from '@editor/type';
import ViewerMenu from './ViewerMenu.vue'; import ViewerMenu from './ViewerMenu.vue';
@ -88,26 +89,6 @@ export default defineComponent({
runtimeUrl: String, runtimeUrl: String,
root: {
type: Object as PropType<MApp>,
},
page: {
type: Object as PropType<MPage>,
},
node: {
type: Object as PropType<MNode>,
},
uiSelectMode: {
type: Boolean,
},
zoom: {
type: Number,
},
canSelect: { canSelect: {
type: Function as PropType<(el: HTMLElement) => boolean | Promise<boolean>>, type: Function as PropType<(el: HTMLElement) => boolean | Promise<boolean>>,
default: (el: HTMLElement) => Boolean(el.id), default: (el: HTMLElement) => Boolean(el.id),
@ -125,11 +106,20 @@ export default defineComponent({
setup(props, { emit }) { setup(props, { emit }) {
const services = inject<Services>('services'); const services = inject<Services>('services');
const stageWrap = ref<HTMLDivElement>();
const stageContainer = ref<HTMLDivElement>(); const stageContainer = ref<HTMLDivElement>();
const stageRect = computed(() => services?.uiService.get<StageRect>('stageRect'));
const uiSelectMode = computed(() => services?.uiService.get<boolean>('uiSelectMode'));
const root = computed(() => services?.editorService.get<MApp>('root'));
const page = computed(() => services?.editorService.get<MPage>('page'));
const zoom = computed(() => services?.uiService.get<number>('zoom'));
const node = computed(() => services?.editorService.get<MNode>('node'));
const stageStyle = computed(() => ({ const stageStyle = computed(() => ({
...services?.uiService.get<Record<string, string | number>>('stageStyle'), width: `${stageRect.value?.width}px`,
transform: `scale(${props.zoom}) translate3d(0, -50%, 0)`, height: `${stageRect.value?.height}px`,
transform: `scale(${zoom.value})`,
})); }));
let stage: StageCore | null = null; let stage: StageCore | null = null;
@ -139,16 +129,16 @@ export default defineComponent({
if (stage) return; if (stage) return;
if (!stageContainer.value) return; if (!stageContainer.value) return;
if (!(props.runtimeUrl || props.render) || !props.root) return; if (!(props.runtimeUrl || props.render) || !root.value) return;
stage = new StageCore({ stage = new StageCore({
render: props.render, render: props.render,
runtimeUrl: props.runtimeUrl, runtimeUrl: props.runtimeUrl,
zoom: props.zoom, zoom: zoom.value,
canSelect: (el, stop) => { canSelect: (el, stop) => {
const elCanSelect = props.canSelect(el); const elCanSelect = props.canSelect(el);
// ui-select // ui-select
if (props.uiSelectMode && elCanSelect) { if (uiSelectMode.value && elCanSelect) {
document.dispatchEvent(new CustomEvent('ui-select', { detail: el })); document.dispatchEvent(new CustomEvent('ui-select', { detail: el }));
return stop(); return stop();
} }
@ -176,45 +166,63 @@ export default defineComponent({
services?.uiService.set('showGuides', true); services?.uiService.set('showGuides', true);
}); });
if (!props.node?.id) return; if (!node.value?.id) return;
stage?.on('runtime-ready', (rt) => { stage?.on('runtime-ready', (rt) => {
runtime = rt; runtime = rt;
// toRawcloneDeep // toRawcloneDeep
props.root && runtime?.updateRootConfig(cloneDeep(toRaw(props.root))); root.value && runtime?.updateRootConfig(cloneDeep(toRaw(root.value)));
props.page?.id && runtime?.updatePageId?.(props.page.id); page.value?.id && runtime?.updatePageId?.(page.value.id);
setTimeout(() => { setTimeout(() => {
props.node && stage?.select(toRaw(props.node.id)); node.value && stage?.select(toRaw(node.value.id));
}); });
}); });
}); });
watch( watch(zoom, (zoom) => {
() => props.zoom,
(zoom) => {
if (!stage || !zoom) return; if (!stage || !zoom) return;
stage?.setZoom(zoom); stage.setZoom(zoom);
}, });
);
watch( watch(root, (root) => {
() => props.root,
(root) => {
if (runtime && root) { if (runtime && root) {
runtime.updateRootConfig(cloneDeep(toRaw(root))); runtime.updateRootConfig(cloneDeep(toRaw(root)));
} }
});
watch(
() => node.value?.id,
(id) => {
nextTick(() => {
// domselect
id && stage?.select(id);
});
}, },
); );
const resizeObserver = new ResizeObserver((entries) => {
for (const { contentRect } of entries) {
services?.uiService.set('stageContainerRect', {
width: contentRect.width,
height: contentRect.height,
});
}
});
onMounted(() => {
stageWrap.value && resizeObserver.observe(stageWrap.value);
});
onUnmounted(() => { onUnmounted(() => {
stage?.destroy(); stage?.destroy();
resizeObserver.disconnect();
services?.editorService.set('stage', null); services?.editorService.set('stage', null);
}); });
return { return {
stageWrap,
stageContainer,
stageStyle, stageStyle,
...useMenu(), ...useMenu(),
stageContainer,
}; };
}, },
}); });

View File

@ -4,11 +4,6 @@
:key="page?.id" :key="page?.id"
:runtime-url="runtimeUrl" :runtime-url="runtimeUrl"
:render="render" :render="render"
:ui-select-mode="uiSelectMode"
:root="root"
:page="page"
:node="node"
:zoom="zoom"
:moveable-options="moveableOptions" :moveable-options="moveableOptions"
:can-select="canSelect" :can-select="canSelect"
@select="selectHandler" @select="selectHandler"
@ -26,9 +21,9 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { computed, defineComponent, inject, nextTick, PropType, watch } from 'vue'; import { computed, defineComponent, inject, PropType } from 'vue';
import type { MApp, MComponent, MContainer, MNode, MPage } from '@tmagic/schema'; import type { MComponent, MContainer, MPage } from '@tmagic/schema';
import type { MoveableOptions, SortEventData } from '@tmagic/stage'; import type { MoveableOptions, SortEventData } from '@tmagic/stage';
import StageCore from '@tmagic/stage'; import StageCore from '@tmagic/stage';
@ -63,23 +58,9 @@ export default defineComponent({
setup() { setup() {
const services = inject<Services>('services'); const services = inject<Services>('services');
const node = computed(() => services?.editorService.get<MNode>('node'));
const stage = computed(() => services?.editorService.get<StageCore>('stage'));
watch([() => node.value?.id, stage], ([id, stage]) => {
nextTick(() => {
// domselect
id && stage?.select(id);
});
});
return { return {
uiSelectMode: computed(() => services?.uiService.get<boolean>('uiSelectMode')),
root: computed(() => services?.editorService.get<MApp>('root')),
page: computed(() => services?.editorService.get<MPage>('page')), page: computed(() => services?.editorService.get<MPage>('page')),
zoom: computed(() => services?.uiService.get<number>('zoom')),
node,
selectHandler(el: HTMLElement) { selectHandler(el: HTMLElement) {
services?.editorService.select(el.id); services?.editorService.select(el.id);

View File

@ -21,7 +21,7 @@ import { reactive, toRaw } from 'vue';
import type StageCore from '@tmagic/stage'; import type StageCore from '@tmagic/stage';
import editorService from '@editor/services/editor'; import editorService from '@editor/services/editor';
import { GetColumnWidth, SetColumnWidth, UiState } from '@editor/type'; import type { GetColumnWidth, SetColumnWidth, StageRect, UiState } from '@editor/type';
import BaseService from './BaseService'; import BaseService from './BaseService';
@ -29,7 +29,14 @@ const state = reactive<UiState>({
uiSelectMode: false, uiSelectMode: false,
showSrc: false, showSrc: false,
zoom: 1, zoom: 1,
stageStyle: {}, stageContainerRect: {
width: 0,
height: 0,
},
stageRect: {
width: 375,
height: 817,
},
columnWidth: { columnWidth: {
left: 310, left: 310,
center: globalThis.document.body.clientWidth - 310 - 400, center: globalThis.document.body.clientWidth - 310 - 400,
@ -57,6 +64,11 @@ class Ui extends BaseService {
return; return;
} }
if (name === 'stageRect') {
this.setStageRect(value as unknown as StageRect);
return;
}
if (name === 'showGuides') { if (name === 'showGuides') {
mask?.showGuides(value as unknown as boolean); mask?.showGuides(value as unknown as boolean);
} }
@ -66,6 +78,10 @@ class Ui extends BaseService {
} }
(state as any)[name] = value; (state as any)[name] = value;
if (name === 'stageContainerRect') {
state.zoom = this.calcZoom();
}
} }
public get<T>(name: keyof typeof state): T { public get<T>(name: keyof typeof state): T {
@ -94,6 +110,24 @@ class Ui extends BaseService {
state.columnWidth = columnWidth; state.columnWidth = columnWidth;
} }
private setStageRect(value: StageRect) {
state.stageRect = {
...state.stageRect,
...value,
};
state.zoom = this.calcZoom();
}
private calcZoom() {
const { stageRect, stageContainerRect } = state;
const { height, width } = stageContainerRect;
if (!width || !height) return 1;
if (width > stageRect.width && height > stageRect.height) {
return 1;
}
return Math.min((width - 100) / stageRect.width || 1, (height - 100) / stageRect.height || 1);
}
} }
export type UiService = Ui; export type UiService = Ui;

View File

@ -2,18 +2,16 @@
position: relative; position: relative;
width: 100%; width: 100%;
height: calc(100% - $--page-bar-height); height: calc(100% - $--page-bar-height);
overflow: auto; overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
} }
.m-editor-stage-container { .m-editor-stage-container {
transition: transform 0.3s; transition: transform 0.3s;
transform-origin: center -50%;
z-index: 0; z-index: 0;
top: 50%; position: absolute;
margin: 0 auto;
position: relative;
width: 375px;
height: 80%;
border: 1px solid $--border-color; border: 1px solid $--border-color;
&::-webkit-scrollbar { &::-webkit-scrollbar {

View File

@ -16,11 +16,11 @@
* limitations under the License. * limitations under the License.
*/ */
import { Component } from 'vue'; import type { Component } from 'vue';
import { FormConfig } from '@tmagic/form'; import type { FormConfig } from '@tmagic/form';
import { Id, MApp, MContainer, MNode, MPage } from '@tmagic/schema'; import type { Id, MApp, MContainer, MNode, MPage } from '@tmagic/schema';
import StageCore from '@tmagic/stage'; import type StageCore from '@tmagic/stage';
import type { ComponentListService } from '@editor/services/componentList'; import type { ComponentListService } from '@editor/services/componentList';
import type { EditorService } from '@editor/services/editor'; import type { EditorService } from '@editor/services/editor';
@ -75,6 +75,11 @@ export interface GetColumnWidth {
right: number; right: number;
} }
export interface StageRect {
width: number;
height: number;
}
export interface UiState { export interface UiState {
/** 当前点击画布是否触发选中true: 不触发false: 触发默认为false */ /** 当前点击画布是否触发选中true: 不触发false: 触发默认为false */
uiSelectMode: boolean; uiSelectMode: boolean;
@ -82,8 +87,10 @@ export interface UiState {
showSrc: boolean; showSrc: boolean;
/** 画布显示放大倍数,默认为 1 */ /** 画布显示放大倍数,默认为 1 */
zoom: number; zoom: number;
/** 画布顶层div的样式可用于改变画布的大小 */ /** 画布容器的宽高 */
stageStyle: Record<string, string | number>; stageContainerRect: StageRect;
/** 画布顶层div的宽高可用于改变画布的大小 */
stageRect: StageRect;
/** 编辑器列布局每一列的宽度,分为左中右三列 */ /** 编辑器列布局每一列的宽度,分为左中右三列 */
columnWidth: GetColumnWidth; columnWidth: GetColumnWidth;
/** 是否显示画布参考线true: 显示false: 不显示默认为true */ /** 是否显示画布参考线true: 显示false: 不显示默认为true */

View File

@ -1,49 +0,0 @@
/*
* Tencent is pleased to support the open source community by making TMagicEditor available.
*
* Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { mount } from '@vue/test-utils';
import Editor from '@editor/layouts/CodeEditor.vue';
/**
* @jest-environment jsdom
*/
describe('编辑器', () => {
it('初始化', () => {
const wrapper = mount(Editor, {
props: {
initValues: [
{
type: 'app',
id: 1,
name: 'app',
items: [
{
type: 'page',
id: 2,
name: 'page',
items: [],
},
],
},
],
},
});
expect(wrapper.exists()).toBe(true);
});
});

View File

@ -20,6 +20,14 @@ import { mount } from '@vue/test-utils';
import Stage from '@editor/layouts/workspace/Stage.vue'; import Stage from '@editor/layouts/workspace/Stage.vue';
globalThis.ResizeObserver =
globalThis.ResizeObserver ||
jest.fn().mockImplementation(() => ({
disconnect: jest.fn(),
observe: jest.fn(),
unobserve: jest.fn(),
}));
describe('Stage.vue', () => { describe('Stage.vue', () => {
(global as any).fetch = jest.fn(() => (global as any).fetch = jest.fn(() =>
Promise.resolve({ Promise.resolve({