mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-09-11 13:39:47 +08:00
feat(editor): 画布拖动
This commit is contained in:
parent
ab3e113904
commit
de9d7d340a
65
packages/editor/src/components/ScrollViewer.vue
Normal file
65
packages/editor/src/components/ScrollViewer.vue
Normal 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>
|
@ -1,15 +1,21 @@
|
||||
<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
|
||||
class="m-editor-stage-container"
|
||||
ref="stageContainer"
|
||||
:style="stageStyle"
|
||||
@contextmenu="contextmenuHandler"
|
||||
:style="`transform: scale(${zoom})`"
|
||||
></div>
|
||||
<teleport to="body">
|
||||
<viewer-menu ref="menu" :style="menuStyle"></viewer-menu>
|
||||
</teleport>
|
||||
</div>
|
||||
</scroll-viewer>
|
||||
</template>
|
||||
|
||||
<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 StageCore from '@tmagic/stage';
|
||||
|
||||
import ScrollViewer from '@editor/components/ScrollViewer.vue';
|
||||
import type { Services, StageRect } from '@editor/type';
|
||||
|
||||
import ViewerMenu from './ViewerMenu.vue';
|
||||
@ -80,6 +87,7 @@ export default defineComponent({
|
||||
|
||||
components: {
|
||||
ViewerMenu,
|
||||
ScrollViewer,
|
||||
},
|
||||
|
||||
props: {
|
||||
@ -107,7 +115,7 @@ export default defineComponent({
|
||||
setup(props, { emit }) {
|
||||
const services = inject<Services>('services');
|
||||
|
||||
const stageWrap = ref<HTMLDivElement>();
|
||||
const stageWrap = ref<InstanceType<typeof ScrollViewer>>();
|
||||
const stageContainer = ref<HTMLDivElement>();
|
||||
|
||||
const stageRect = computed(() => services?.uiService.get<StageRect>('stageRect'));
|
||||
@ -116,11 +124,6 @@ export default defineComponent({
|
||||
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(() => ({
|
||||
width: `${stageRect.value?.width}px`,
|
||||
height: `${stageRect.value?.height}px`,
|
||||
transform: `scale(${zoom.value})`,
|
||||
}));
|
||||
|
||||
let stage: StageCore | null = null;
|
||||
let runtime: Runtime | null = null;
|
||||
@ -209,7 +212,7 @@ export default defineComponent({
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
stageWrap.value && resizeObserver.observe(stageWrap.value);
|
||||
stageWrap.value?.container && resizeObserver.observe(stageWrap.value.container);
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@ -221,7 +224,8 @@ export default defineComponent({
|
||||
return {
|
||||
stageWrap,
|
||||
stageContainer,
|
||||
stageStyle,
|
||||
stageRect,
|
||||
zoom,
|
||||
...useMenu(),
|
||||
};
|
||||
},
|
||||
|
@ -9,10 +9,12 @@
|
||||
}
|
||||
|
||||
.m-editor-stage-container {
|
||||
transition: transform 0.3s;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 0;
|
||||
position: absolute;
|
||||
position: relative;
|
||||
border: 1px solid $--border-color;
|
||||
transition: transform 0.3s;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 0 !important;
|
||||
|
@ -241,3 +241,7 @@ export enum Layout {
|
||||
RELATIVE = 'relative',
|
||||
ABSOLUTE = 'absolute',
|
||||
}
|
||||
|
||||
export enum Keys {
|
||||
ESCAPE = 'Space',
|
||||
}
|
||||
|
213
packages/editor/src/utils/scroll-viewer.ts
Normal file
213
packages/editor/src/utils/scroll-viewer.ts
Normal 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;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user