feat(editor): 画布添加滚动条

fix #262
This commit is contained in:
roymondchen 2022-08-26 21:58:31 +08:00 committed by jia000
parent dd3075be56
commit de8ef8dc58
8 changed files with 559 additions and 433 deletions

View File

@ -0,0 +1,147 @@
<template>
<div ref="bar" class="m-editor-scroll-bar" :class="isHorizontal ? 'horizontal' : 'vertical'">
<div ref="thumb" class="m-editor-scroll-bar-thumb" :style="thumbStyle"></div>
</div>
</template>
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
import Gesto from 'gesto';
const props = defineProps<{
size: number;
scrollSize: number;
isHorizontal?: boolean;
pos: number;
}>();
const emit = defineEmits(['scroll']);
const bar = ref<HTMLDivElement>();
const thumb = ref<HTMLDivElement>();
const thumbSize = computed(() => props.size * (props.size / props.scrollSize));
const thumbPos = computed(() => (props.pos / props.scrollSize) * props.size);
const thumbStyle = computed(() => ({
[props.isHorizontal ? 'width' : 'height']: `${thumbSize.value}px`,
transform: `translate${props.isHorizontal ? 'X' : 'Y'}(${thumbPos.value}px)`,
}));
let gesto: Gesto;
onMounted(() => {
if (!thumb.value) return;
const thumbEl = thumb.value;
gesto = new Gesto(thumbEl, {
container: window,
});
gesto
.on('dragStart', (e) => {
e.inputEvent.stopPropagation();
e.inputEvent.preventDefault();
})
.on('drag', (e) => {
scrollBy(getDelta(e));
});
bar.value?.addEventListener('wheel', wheelHandler, false);
});
onUnmounted(() => {
if (gesto) gesto.off();
bar.value?.removeEventListener('wheel', wheelHandler, false);
});
const wheelHandler = (e: WheelEvent) => {
const delta = props.isHorizontal ? e.deltaX : e.deltaY;
if (delta) {
e.preventDefault();
}
scrollBy(delta);
};
const getDelta = (e: any) => {
const ratio = (props.isHorizontal ? e.deltaX : e.deltaY) / props.size;
return props.scrollSize * ratio;
};
const scrollBy = (delta: number) => {
if (delta < 0) {
if (props.pos <= 0) {
emit('scroll', 0);
} else {
emit('scroll', -Math.min(-delta, props.pos));
}
} else {
const leftPos = props.size - (thumbSize.value + thumbPos.value);
if (leftPos <= 0) {
emit('scroll', 0);
} else {
emit('scroll', Math.min(delta, leftPos));
}
}
};
</script>
<style lang="scss">
.m-editor-scroll-bar {
position: absolute;
background-color: transparent;
opacity: 0.3;
transition: background-color 0.2s linear, opacity 0.2s linear;
.m-editor-scroll-bar-thumb {
background-color: #aaa;
border-radius: 6px;
position: absolute;
}
&.horizontal {
width: 100%;
height: 15px;
bottom: 0;
.m-editor-scroll-bar-thumb {
height: 6px;
transition: background-color 0.2s linear, height 0.2s ease-in-out;
bottom: 2px;
}
}
&.vertical {
height: 100%;
width: 15px;
right: 5px;
.m-editor-scroll-bar-thumb {
width: 6px;
transition: background-color 0.2s linear, width 0.2s ease-in-out;
right: 2px;
}
}
&:hover,
&:focus {
background-color: #eee;
opacity: 0.9;
.m-editor-scroll-bar-thumb {
background-color: #999;
}
&.horizontal {
.m-editor-scroll-bar-thumb {
height: 11px;
}
}
&.vertical {
.m-editor-scroll-bar-thumb {
width: 11px;
}
}
}
}
</style>

View File

@ -3,64 +3,109 @@
<div ref="el" :style="style">
<slot></slot>
</div>
<ScrollBar
v-if="scrollHeight > wrapHeight"
:scroll-size="scrollHeight"
:size="wrapHeight"
:pos="vOffset"
@scroll="vScrollHandler"
></ScrollBar>
<ScrollBar
v-if="scrollWidth > wrapWidth"
:is-horizontal="true"
:scroll-size="scrollWidth"
:pos="hOffset"
:size="wrapWidth"
@scroll="hScrollHandler"
></ScrollBar>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, onMounted, onUnmounted, ref, watch } from 'vue';
<script lang="ts" setup>
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
import { ScrollViewerEvent } from '../type';
import { ScrollViewer } from '../utils/scroll-viewer';
export default defineComponent({
name: 'm-editor-scroll-viewer',
import ScrollBar from './ScrollBar.vue';
props: {
width: Number,
height: Number,
zoom: {
type: Number,
default: 1,
},
const props = withDefaults(
defineProps<{
width?: number;
height?: number;
wrapWidth?: number;
wrapHeight?: number;
zoom?: number;
}>(),
{
width: 0,
height: 0,
wrapWidth: 0,
wrapHeight: 0,
zoom: 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(
const container = ref<HTMLDivElement>();
const el = ref<HTMLDivElement>();
const style = computed(
() => `
width: ${props.width}px;
height: ${props.height}px;
position: absolute;
margin-top: 30px;
`,
),
};
);
const scrollWidth = ref(0);
const scrollHeight = ref(0);
let scrollViewer: ScrollViewer;
onMounted(() => {
if (!container.value || !el.value) return;
scrollViewer = new ScrollViewer({
container: container.value,
target: el.value,
zoom: props.zoom,
});
scrollViewer.on('scroll', (data: ScrollViewerEvent) => {
hOffset.value = data.scrollLeft;
vOffset.value = data.scrollTop;
scrollWidth.value = data.scrollWidth;
scrollHeight.value = data.scrollHeight;
});
});
onUnmounted(() => {
scrollViewer.destroy();
});
watch(
() => props.zoom,
() => {
scrollViewer.setZoom(props.zoom);
},
);
const vOffset = ref(0);
const vScrollHandler = (delta: number) => {
vOffset.value += delta;
scrollViewer.scrollTo({
top: vOffset.value,
});
};
const hOffset = ref(0);
const hScrollHandler = (delta: number) => {
hOffset.value += delta;
scrollViewer.scrollTo({
left: hOffset.value,
});
};
defineExpose({
container,
});
</script>

View File

@ -4,12 +4,14 @@
ref="stageWrap"
:width="stageRect?.width"
:height="stageRect?.height"
:wrap-width="stageContainerRect?.width"
:wrap-height="stageContainerRect?.height"
:zoom="zoom"
>
<div
class="m-editor-stage-container"
ref="stageContainer"
:style="`transform: scale(${zoom})`"
:style="`transform: scale(${zoom});`"
@contextmenu="contextmenuHandler"
@drop="dropHandler"
@dragover="dragoverHandler"
@ -25,25 +27,11 @@ import { computed, inject, markRaw, onMounted, onUnmounted, ref, toRaw, watch, w
import { cloneDeep } from 'lodash-es';
import type { MApp, MContainer, MNode, MPage } from '@tmagic/schema';
import StageCore, {
calcValueByFontsize,
getOffset,
GuidesType,
Runtime,
SortEventData,
UpdateEventData,
} from '@tmagic/stage';
import StageCore, { calcValueByFontsize, getOffset, Runtime } from '@tmagic/stage';
import ScrollViewer from '../../components/ScrollViewer.vue';
import {
H_GUIDE_LINE_STORAGE_KEY,
Layout,
Services,
StageOptions,
StageRect,
V_GUIDE_LINE_STORAGE_KEY,
} from '../../type';
import { getGuideLineFromCache } from '../../utils';
import { Layout, Services, StageOptions, StageRect } from '../../type';
import { useStage } from '../../utils/stage';
import ViewerMenu from './ViewerMenu.vue';
@ -59,94 +47,24 @@ const menu = ref<InstanceType<typeof ViewerMenu>>();
const isMultiSelect = computed(() => services?.editorService.get('nodes')?.length > 1);
const stageRect = computed(() => services?.uiService.get<StageRect>('stageRect'));
const uiSelectMode = computed(() => services?.uiService.get<boolean>('uiSelectMode'));
const stageContainerRect = computed(() => services?.uiService.get<StageRect>('stageContainerRect'));
const root = computed(() => services?.editorService.get<MApp>('root'));
const page = computed(() => services?.editorService.get<MPage>('page'));
const zoom = computed(() => services?.uiService.get<number>('zoom') || 1);
const node = computed(() => services?.editorService.get<MNode>('node'));
const getGuideLineKey = (key: string) => `${key}_${root.value?.id}_${page.value?.id}`;
watchEffect(() => {
if (stage) return;
if (!stageContainer.value) return;
if (!(stageOptions?.runtimeUrl || stageOptions?.render) || !root.value) return;
stage = new StageCore({
render: stageOptions.render,
runtimeUrl: stageOptions.runtimeUrl,
zoom: zoom.value,
autoScrollIntoView: stageOptions.autoScrollIntoView,
isContainer: stageOptions.isContainer,
containerHighlightClassName: stageOptions.containerHighlightClassName,
containerHighlightDuration: stageOptions.containerHighlightDuration,
containerHighlightType: stageOptions.containerHighlightType,
canSelect: (el, event, stop) => {
const elCanSelect = stageOptions.canSelect(el);
// ui-select
if (uiSelectMode.value && elCanSelect && event.type === 'mousedown') {
document.dispatchEvent(new CustomEvent('ui-select', { detail: el }));
return stop();
}
return elCanSelect;
},
moveableOptions: stageOptions.moveableOptions,
updateDragEl: stageOptions.updateDragEl,
});
stage = useStage(stageOptions);
services?.editorService.set('stage', markRaw(stage));
stage?.mount(stageContainer.value);
stage.mask.setGuides([
getGuideLineFromCache(getGuideLineKey(H_GUIDE_LINE_STORAGE_KEY)),
getGuideLineFromCache(getGuideLineKey(V_GUIDE_LINE_STORAGE_KEY)),
]);
stage?.on('select', (el: HTMLElement) => {
services?.editorService.select(el.id);
});
stage?.on('highlight', (el: HTMLElement) => {
services?.editorService.highlight(el.id);
});
stage?.on('multiSelect', (els: HTMLElement[]) => {
services?.editorService.multiSelect(els.map((el) => el.id));
});
stage?.on('update', (ev: UpdateEventData) => {
if (ev.parentEl) {
for (const data of ev.data) {
services?.editorService.moveToContainer({ id: data.el.id, style: data.style }, ev.parentEl.id);
}
return;
}
services?.editorService.update(ev.data.map((data) => ({ id: data.el.id, style: data.style })));
});
stage?.on('sort', (ev: SortEventData) => {
services?.editorService.sort(ev.src, ev.dist);
});
stage?.on('changeGuides', (e) => {
services?.uiService.set('showGuides', true);
if (!root.value || !page.value) return;
const storageKey = getGuideLineKey(
e.type === GuidesType.HORIZONTAL ? H_GUIDE_LINE_STORAGE_KEY : V_GUIDE_LINE_STORAGE_KEY,
);
if (e.guides.length) {
globalThis.localStorage.setItem(storageKey, JSON.stringify(e.guides));
} else {
globalThis.localStorage.removeItem(storageKey);
}
});
if (!node.value?.id) return;
stage?.on('runtime-ready', (rt) => {
runtime = rt;

View File

@ -1,20 +1,20 @@
<template>
<div class="m-editor-workspace" tabindex="1" ref="workspace">
<slot name="stage">
<magic-stage :key="page?.id"></magic-stage>
<MagicStage :key="page?.id"></MagicStage>
</slot>
<slot name="workspace-content"></slot>
<page-bar>
<PageBar>
<template #page-bar-title="{ page }"><slot name="page-bar-title" :page="page"></slot></template>
<template #page-bar-popover="{ page }"><slot name="page-bar-popover" :page="page"></slot></template>
</page-bar>
</PageBar>
</div>
</template>
<script lang="ts">
import { computed, defineComponent, inject, onMounted, onUnmounted, ref } from 'vue';
<script lang="ts" setup>
import { computed, inject, onMounted, onUnmounted, ref } from 'vue';
import KeyController from 'keycon';
import type { MNode, MPage } from '@tmagic/schema';
@ -25,29 +25,22 @@ import type { Services } from '../../type';
import PageBar from './PageBar.vue';
import MagicStage from './Stage.vue';
export default defineComponent({
name: 'm-editor-workspace',
const services = inject<Services>('services');
const workspace = ref<HTMLDivElement>();
const nodes = computed(() => services?.editorService.get<MNode[]>('nodes'));
const page = computed(() => services?.editorService.get<MPage>('page'));
components: {
PageBar,
MagicStage,
},
setup() {
const services = inject<Services>('services');
const workspace = ref<HTMLDivElement>();
const nodes = computed(() => services?.editorService.get<MNode[]>('nodes'));
let keycon: KeyController;
const mouseenterHandler = () => {
const mouseenterHandler = () => {
workspace.value?.focus();
};
};
const mouseleaveHandler = () => {
const mouseleaveHandler = () => {
workspace.value?.blur();
};
};
onMounted(() => {
let keycon: KeyController;
onMounted(() => {
workspace.value?.addEventListener('mouseenter', mouseenterHandler);
workspace.value?.addEventListener('mouseleave', mouseleaveHandler);
@ -154,19 +147,11 @@ export default defineComponent({
e.inputEvent.preventDefault();
services?.uiService.set('zoom', 1);
});
});
});
onUnmounted(() => {
onUnmounted(() => {
workspace.value?.removeEventListener('mouseenter', mouseenterHandler);
workspace.value?.removeEventListener('mouseleave', mouseleaveHandler);
keycon.destroy();
});
return {
workspace,
page: computed(() => services?.editorService.get<MPage>('page')),
};
},
});
</script>

View File

@ -296,3 +296,10 @@ export enum Keys {
export const H_GUIDE_LINE_STORAGE_KEY = '$MagicStageHorizontalGuidelinesData';
export const V_GUIDE_LINE_STORAGE_KEY = '$MagicStageVerticalGuidelinesData';
export interface ScrollViewerEvent {
scrollLeft: number;
scrollTop: number;
scrollHeight: number;
scrollWidth: number;
}

View File

@ -20,3 +20,4 @@ export * from './config';
export * from './props';
export * from './logger';
export * from './editor';
export * from './stage';

View File

@ -1,4 +1,4 @@
import { Keys } from '../type';
import { EventEmitter } from 'events';
interface ScrollViewerOptions {
container: HTMLDivElement;
@ -6,10 +6,7 @@ interface ScrollViewerOptions {
zoom: number;
}
export class ScrollViewer {
private enter = false;
private targetEnter = false;
private keydown = false;
export class ScrollViewer extends EventEmitter {
private container: HTMLDivElement;
private target: HTMLDivElement;
private zoom = 1;
@ -17,200 +14,135 @@ export class ScrollViewer {
private scrollLeft = 0;
private scrollTop = 0;
private x = 0;
private y = 0;
private scrollHeight = 0;
private scrollWidth = 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 targetMarginTop = Number(this.target.style.marginTop) || 0;
const targetHeight = (targetRect.height + targetMarginTop) * this.zoom;
private width = 0;
private height = 0;
if (targetWidth < width) {
(this.target as any)._left = 0;
}
if (targetHeight < height) {
(this.target as any)._top = 0;
}
private translateXCorrectionValue = 0;
private translateYCorrectionValue = 0;
this.scroll();
}
private resizeObserver = new ResizeObserver(() => {
this.setSize();
this.setScrollSize();
});
constructor(options: ScrollViewerOptions) {
super();
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.container.addEventListener('wheel', this.wheelHandler, false);
this.setSize();
this.setScrollSize();
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);
this.container.removeEventListener('wheel', this.wheelHandler, false);
this.removeAllListeners();
}
public setZoom(zoom: number) {
this.zoom = zoom;
this.setScrollSize();
}
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)`;
public scrollTo({ left, top }: { left?: number; top?: number }) {
if (typeof left !== 'undefined') {
this.scrollLeft = left;
}
private removeHandler() {
this.target.style.cursor = '';
this.target.removeEventListener('mousedown', this.mousedownHandler);
document.removeEventListener('mousemove', this.mousemoveHandler);
document.removeEventListener('mouseup', this.mouseupHandler);
if (typeof top !== 'undefined') {
this.scrollTop = top;
}
const translateX = -this.scrollLeft + this.translateXCorrectionValue;
const translateY = -this.scrollTop + this.translateYCorrectionValue;
this.target.style.transform = `translate(${translateX}px, ${translateY}px)`;
}
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();
let top: number | undefined;
if (this.scrollHeight > this.height) {
top = this.scrollTop + this.getPos(deltaY, this.scrollTop, this.scrollHeight, this.height);
}
if (event.code !== Keys.ESCAPE || !this.enter || this.keydown) {
return;
let left: number | undefined;
if (this.scrollWidth > this.width) {
left = this.scrollLeft + this.getPos(deltaX, this.scrollLeft, this.scrollWidth, this.width);
}
this.keydown = true;
this.target.style.cursor = 'grab';
this.container.addEventListener('mousedown', this.mousedownHandler);
this.scrollTo({ left, top });
this.emit('scroll', {
scrollLeft: this.scrollLeft,
scrollTop: this.scrollTop,
scrollHeight: this.scrollHeight,
scrollWidth: this.scrollWidth,
});
};
private keyupHandler = (event: KeyboardEvent) => {
if (event.code !== Keys.ESCAPE || !this.keydown) {
return;
private getPos(delta: number, scrollPos: number, scrollSize: number, size: number) {
let pos = 0;
if (delta < 0) {
if (scrollPos > 0) {
pos = Math.max(delta, -scrollPos);
}
} else {
const leftPos = scrollSize - size - scrollPos;
if (leftPos > 0) {
pos = Math.min(delta, leftPos);
}
}
return pos;
}
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();
private setScrollSize = () => {
const targetRect = this.target.getBoundingClientRect();
this.scrollWidth = targetRect.width * this.zoom + 100;
const targetMarginTop = Number(this.target.style.marginTop) || 0;
this.scrollHeight = (targetRect.height + targetMarginTop) * this.zoom + 100;
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);
let left: number | undefined;
let top: number | undefined;
if (this.scrollWidth < this.width) {
left = 0;
this.translateXCorrectionValue = 0;
} else {
y = this.scrollTop + Math.max(-(targetHeight - height + this.scrollTop), deltaY);
this.translateXCorrectionValue = (this.scrollWidth - this.width) / 2;
}
}
let x = 0;
if (targetWidth > width) {
if (deltaX > 0) {
x = this.scrollLeft + Math.min(targetWidth - width - this.scrollLeft, deltaX);
if (this.scrollHeight < this.height) {
top = 0;
this.translateYCorrectionValue = 0;
} else {
x = this.scrollLeft + Math.max(-(targetWidth - width + this.scrollLeft), deltaX);
}
this.translateYCorrectionValue = (this.scrollHeight - this.height) / 2;
}
(this.target as any)._left = x;
(this.target as any)._top = y;
}
this.scrollTo({
left,
top,
});
this.emit('scroll', {
scrollLeft: this.scrollLeft,
scrollTop: this.scrollTop,
scrollHeight: this.scrollHeight,
scrollWidth: this.scrollWidth,
});
};
private setSize = () => {
const { width, height } = this.container.getBoundingClientRect();
this.width = width;
this.height = height;
};
}

View File

@ -0,0 +1,91 @@
import { computed } from 'vue';
import { MApp, MPage } from '@tmagic/schema';
import StageCore, { GuidesType, SortEventData, UpdateEventData } from '@tmagic/stage';
import editorService from '../services/editor';
import uiService from '../services/ui';
import { H_GUIDE_LINE_STORAGE_KEY, StageOptions, V_GUIDE_LINE_STORAGE_KEY } from '../type';
import { getGuideLineFromCache } from './editor';
const root = computed(() => editorService.get<MApp>('root'));
const page = computed(() => editorService.get<MPage>('page'));
const zoom = computed(() => uiService.get<number>('zoom') || 1);
const uiSelectMode = computed(() => uiService.get<boolean>('uiSelectMode'));
const getGuideLineKey = (key: string) => `${key}_${root.value?.id}_${page.value?.id}`;
export const useStage = (stageOptions: StageOptions) => {
const stage = new StageCore({
render: stageOptions.render,
runtimeUrl: stageOptions.runtimeUrl,
zoom: zoom.value,
autoScrollIntoView: stageOptions.autoScrollIntoView,
isContainer: stageOptions.isContainer,
containerHighlightClassName: stageOptions.containerHighlightClassName,
containerHighlightDuration: stageOptions.containerHighlightDuration,
containerHighlightType: stageOptions.containerHighlightType,
canSelect: (el, event, stop) => {
const elCanSelect = stageOptions.canSelect(el);
// 在组件联动过程中不能再往下选择,返回并触发 ui-select
if (uiSelectMode.value && elCanSelect && event.type === 'mousedown') {
document.dispatchEvent(new CustomEvent('ui-select', { detail: el }));
return stop();
}
return elCanSelect;
},
moveableOptions: stageOptions.moveableOptions,
updateDragEl: stageOptions.updateDragEl,
});
stage.mask.setGuides([
getGuideLineFromCache(getGuideLineKey(H_GUIDE_LINE_STORAGE_KEY)),
getGuideLineFromCache(getGuideLineKey(V_GUIDE_LINE_STORAGE_KEY)),
]);
stage.on('select', (el: HTMLElement) => {
editorService.select(el.id);
});
stage.on('highlight', (el: HTMLElement) => {
editorService.highlight(el.id);
});
stage.on('multiSelect', (els: HTMLElement[]) => {
editorService.multiSelect(els.map((el) => el.id));
});
stage.on('update', (ev: UpdateEventData) => {
if (ev.parentEl) {
for (const data of ev.data) {
editorService.moveToContainer({ id: data.el.id, style: data.style }, ev.parentEl.id);
}
return;
}
editorService.update(ev.data.map((data) => ({ id: data.el.id, style: data.style })));
});
stage.on('sort', (ev: SortEventData) => {
editorService.sort(ev.src, ev.dist);
});
stage.on('changeGuides', (e) => {
uiService.set('showGuides', true);
if (!root.value || !page.value) return;
const storageKey = getGuideLineKey(
e.type === GuidesType.HORIZONTAL ? H_GUIDE_LINE_STORAGE_KEY : V_GUIDE_LINE_STORAGE_KEY,
);
if (e.guides.length) {
globalThis.localStorage.setItem(storageKey, JSON.stringify(e.guides));
} else {
globalThis.localStorage.removeItem(storageKey);
}
});
return stage;
};