feat(editor): 画布拖动

This commit is contained in:
roymondchen 2022-03-14 21:19:10 +08:00 committed by jia000
parent ab3e113904
commit de9d7d340a
5 changed files with 301 additions and 13 deletions

View File

@ -0,0 +1,65 @@
<template>
<div class="m-editor-scroll-viewer-container" ref="container">
<div ref="el" :style="style">
<slot></slot>
</div>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue';
import { ScrollViewer } from '@editor/utils/scroll-viewer';
export default defineComponent({
name: 'm-editor-scroll-viewer',
props: {
width: Number,
height: Number,
zoom: {
type: Number,
default: 1,
},
},
setup(props) {
const container = ref<HTMLDivElement>();
const el = ref<HTMLDivElement>();
let scrollViewer: ScrollViewer;
onMounted(() => {
if (!container.value || !el.value) return;
scrollViewer = new ScrollViewer({
container: container.value,
target: el.value,
zoom: props.zoom,
});
});
onUnmounted(() => {
scrollViewer.destroy();
});
watch(
() => props.zoom,
() => {
scrollViewer.setZoom(props.zoom);
},
);
return {
container,
el,
style: computed(
() => `
width: ${props.width}px;
height: ${props.height}px;
position: absolute;
`,
),
};
},
});
</script>

View File

@ -1,15 +1,21 @@
<template> <template>
<div class="m-editor-stage" ref="stageWrap"> <scroll-viewer
class="m-editor-stage"
ref="stageWrap"
:width="stageRect?.width"
:height="stageRect?.height"
:zoom="zoom"
>
<div <div
class="m-editor-stage-container" class="m-editor-stage-container"
ref="stageContainer" ref="stageContainer"
:style="stageStyle"
@contextmenu="contextmenuHandler" @contextmenu="contextmenuHandler"
:style="`transform: scale(${zoom})`"
></div> ></div>
<teleport to="body"> <teleport to="body">
<viewer-menu ref="menu" :style="menuStyle"></viewer-menu> <viewer-menu ref="menu" :style="menuStyle"></viewer-menu>
</teleport> </teleport>
</div> </scroll-viewer>
</template> </template>
<script lang="ts"> <script lang="ts">
@ -32,6 +38,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 ScrollViewer from '@editor/components/ScrollViewer.vue';
import type { Services, StageRect } from '@editor/type'; import type { Services, StageRect } from '@editor/type';
import ViewerMenu from './ViewerMenu.vue'; import ViewerMenu from './ViewerMenu.vue';
@ -80,6 +87,7 @@ export default defineComponent({
components: { components: {
ViewerMenu, ViewerMenu,
ScrollViewer,
}, },
props: { props: {
@ -107,7 +115,7 @@ export default defineComponent({
setup(props, { emit }) { setup(props, { emit }) {
const services = inject<Services>('services'); const services = inject<Services>('services');
const stageWrap = ref<HTMLDivElement>(); const stageWrap = ref<InstanceType<typeof ScrollViewer>>();
const stageContainer = ref<HTMLDivElement>(); const stageContainer = ref<HTMLDivElement>();
const stageRect = computed(() => services?.uiService.get<StageRect>('stageRect')); const stageRect = computed(() => services?.uiService.get<StageRect>('stageRect'));
@ -116,11 +124,6 @@ export default defineComponent({
const page = computed(() => services?.editorService.get<MPage>('page')); const page = computed(() => services?.editorService.get<MPage>('page'));
const zoom = computed(() => services?.uiService.get<number>('zoom')); const zoom = computed(() => services?.uiService.get<number>('zoom'));
const node = computed(() => services?.editorService.get<MNode>('node')); const node = computed(() => services?.editorService.get<MNode>('node'));
const stageStyle = computed(() => ({
width: `${stageRect.value?.width}px`,
height: `${stageRect.value?.height}px`,
transform: `scale(${zoom.value})`,
}));
let stage: StageCore | null = null; let stage: StageCore | null = null;
let runtime: Runtime | null = null; let runtime: Runtime | null = null;
@ -209,7 +212,7 @@ export default defineComponent({
}); });
onMounted(() => { onMounted(() => {
stageWrap.value && resizeObserver.observe(stageWrap.value); stageWrap.value?.container && resizeObserver.observe(stageWrap.value.container);
}); });
onUnmounted(() => { onUnmounted(() => {
@ -221,7 +224,8 @@ export default defineComponent({
return { return {
stageWrap, stageWrap,
stageContainer, stageContainer,
stageStyle, stageRect,
zoom,
...useMenu(), ...useMenu(),
}; };
}, },

View File

@ -9,10 +9,12 @@
} }
.m-editor-stage-container { .m-editor-stage-container {
transition: transform 0.3s; width: 100%;
height: 100%;
z-index: 0; z-index: 0;
position: absolute; position: relative;
border: 1px solid $--border-color; border: 1px solid $--border-color;
transition: transform 0.3s;
&::-webkit-scrollbar { &::-webkit-scrollbar {
width: 0 !important; width: 0 !important;

View File

@ -241,3 +241,7 @@ export enum Layout {
RELATIVE = 'relative', RELATIVE = 'relative',
ABSOLUTE = 'absolute', ABSOLUTE = 'absolute',
} }
export enum Keys {
ESCAPE = 'Space',
}

View File

@ -0,0 +1,213 @@
import { Keys } from '@editor/type';
interface ScrollViewerOptions {
container: HTMLDivElement;
target: HTMLDivElement;
zoom: number;
}
export class ScrollViewer {
private enter = false;
private targetEnter = false;
private keydown = false;
private container: HTMLDivElement;
private target: HTMLDivElement;
private zoom = 1;
private scrollLeft = 0;
private scrollTop = 0;
private x = 0;
private y = 0;
private resizeObserver = new ResizeObserver((entries) => {
for (const { contentRect } of entries) {
const { width, height } = contentRect;
const targetRect = this.target.getBoundingClientRect();
const targetWidth = targetRect.width * this.zoom;
const targetHeight = targetRect.height * this.zoom;
if (targetWidth < width) {
(this.target as any)._left = 0;
}
if (targetHeight < height) {
(this.target as any)._top = 0;
}
this.scroll();
}
});
constructor(options: ScrollViewerOptions) {
this.container = options.container;
this.target = options.target;
this.zoom = options.zoom;
globalThis.addEventListener('keydown', this.keydownHandler);
globalThis.addEventListener('keyup', this.keyupHandler);
this.container.addEventListener('mouseenter', this.mouseEnterHandler);
this.container.addEventListener('mouseleave', this.mouseLeaveHandler);
this.target.addEventListener('mouseenter', this.targetMouseEnterHandler);
this.target.addEventListener('mouseleave', this.targetMouseLeaveHandler);
this.container.addEventListener('wheel', this.wheelHandler);
this.resizeObserver.observe(this.container);
}
public destroy() {
this.resizeObserver.disconnect();
this.container.removeEventListener('mouseenter', this.mouseEnterHandler);
this.container.removeEventListener('mouseleave', this.mouseLeaveHandler);
this.target.removeEventListener('mouseenter', this.targetMouseEnterHandler);
this.target.removeEventListener('mouseleave', this.targetMouseLeaveHandler);
globalThis.removeEventListener('keydown', this.keydownHandler);
globalThis.removeEventListener('keyup', this.keyupHandler);
}
public setZoom(zoom: number) {
this.zoom = zoom;
}
private scroll() {
const scrollLeft = (this.target as any)._left;
const scrollTop = (this.target as any)._top;
this.target.style.transform = `translate(${scrollLeft}px, ${scrollTop}px)`;
}
private removeHandler() {
this.target.style.cursor = '';
this.target.removeEventListener('mousedown', this.mousedownHandler);
document.removeEventListener('mousemove', this.mousemoveHandler);
document.removeEventListener('mouseup', this.mouseupHandler);
}
private wheelHandler = (event: WheelEvent) => {
if (this.targetEnter) return;
const { deltaX, deltaY, currentTarget } = event;
if (currentTarget !== this.container) return;
this.setScrollOffset(deltaX, deltaY);
this.scroll();
this.scrollLeft = (this.target as any)._left;
this.scrollTop = (this.target as any)._top;
};
private mouseEnterHandler = () => {
this.enter = true;
};
private mouseLeaveHandler = () => {
this.enter = false;
};
private targetMouseEnterHandler = () => {
this.targetEnter = true;
};
private targetMouseLeaveHandler = () => {
this.targetEnter = false;
};
private mousedownHandler = (event: MouseEvent) => {
if (!this.keydown) return;
event.stopImmediatePropagation();
event.stopPropagation();
this.target.style.cursor = 'grabbing';
this.x = event.clientX;
this.y = event.clientY;
document.addEventListener('mousemove', this.mousemoveHandler);
document.addEventListener('mouseup', this.mouseupHandler);
};
private mouseupHandler = () => {
this.x = 0;
this.y = 0;
this.scrollLeft = (this.target as any)._left;
this.scrollTop = (this.target as any)._top;
this.removeHandler();
};
private mousemoveHandler = (event: MouseEvent) => {
event.stopImmediatePropagation();
event.stopPropagation();
const deltaX = event.clientX - this.x;
const deltaY = event.clientY - this.y;
this.setScrollOffset(deltaX, deltaY);
this.scroll();
};
private keydownHandler = (event: KeyboardEvent) => {
if (event.code === Keys.ESCAPE && this.enter) {
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
} else if (this.keydown) return;
this.keydown = true;
event.preventDefault();
this.target.style.cursor = 'grab';
this.container.addEventListener('mousedown', this.mousedownHandler);
};
private keyupHandler = (event: KeyboardEvent) => {
if (event.code !== Keys.ESCAPE || !this.keydown) {
return;
}
event.preventDefault();
event.stopImmediatePropagation();
event.stopPropagation();
this.keydown = false;
event.preventDefault();
this.removeHandler();
};
private setScrollOffset(deltaX: number, deltaY: number) {
const { width, height } = this.container.getBoundingClientRect();
const targetRect = this.target.getBoundingClientRect();
const targetWidth = targetRect.width * this.zoom;
const targetHeight = targetRect.height * this.zoom;
let y = 0;
if (targetHeight > height) {
if (deltaY > 0) {
y = this.scrollTop + Math.min(targetHeight - height - this.scrollTop, deltaY);
} else {
y = this.scrollTop + Math.max(-(targetHeight - height + this.scrollTop), deltaY);
}
}
let x = 0;
if (targetWidth > width) {
if (deltaX > 0) {
x = this.scrollLeft + Math.min(targetWidth - width - this.scrollLeft, deltaX);
} else {
x = this.scrollLeft + Math.max(-(targetWidth - width + this.scrollLeft), deltaX);
}
}
(this.target as any)._left = x;
(this.target as any)._top = y;
}
}