mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-09-12 06:19:49 +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>
|
<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(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -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;
|
||||||
|
@ -241,3 +241,7 @@ export enum Layout {
|
|||||||
RELATIVE = 'relative',
|
RELATIVE = 'relative',
|
||||||
ABSOLUTE = 'absolute',
|
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