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 propsService from '@editor/services/props';
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({
name: 'm-editor',
@ -133,8 +133,8 @@ export default defineComponent({
type: Function as PropType<(el: HTMLElement) => boolean | Promise<boolean>>,
},
stageStyle: {
type: [String, Object] as PropType<Record<string, string | number>>,
stageRect: {
type: [String, Object] as PropType<StageRect>,
},
},
@ -208,8 +208,8 @@ export default defineComponent({
);
watch(
() => props.stageStyle,
(stageStyle) => uiService.set('stageStyle', stageStyle),
() => props.stageRect,
(stageRect) => stageRect && uiService.set('stageRect', stageRect),
{
immediate: true,
},

View File

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

View File

@ -4,11 +4,6 @@
:key="page?.id"
:runtime-url="runtimeUrl"
:render="render"
:ui-select-mode="uiSelectMode"
:root="root"
:page="page"
:node="node"
:zoom="zoom"
:moveable-options="moveableOptions"
:can-select="canSelect"
@select="selectHandler"
@ -26,9 +21,9 @@
</template>
<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 StageCore from '@tmagic/stage';
@ -63,23 +58,9 @@ export default defineComponent({
setup() {
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 {
uiSelectMode: computed(() => services?.uiService.get<boolean>('uiSelectMode')),
root: computed(() => services?.editorService.get<MApp>('root')),
page: computed(() => services?.editorService.get<MPage>('page')),
zoom: computed(() => services?.uiService.get<number>('zoom')),
node,
selectHandler(el: HTMLElement) {
services?.editorService.select(el.id);

View File

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

View File

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

View File

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