mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2025-06-20 12:49:19 +08:00
parent
dd3075be56
commit
de8ef8dc58
147
packages/editor/src/components/ScrollBar.vue
Normal file
147
packages/editor/src/components/ScrollBar.vue
Normal 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>
|
@ -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>
|
||||
|
@ -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;
|
||||
|
@ -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,148 +25,133 @@ 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,
|
||||
},
|
||||
const mouseenterHandler = () => {
|
||||
workspace.value?.focus();
|
||||
};
|
||||
|
||||
setup() {
|
||||
const services = inject<Services>('services');
|
||||
const workspace = ref<HTMLDivElement>();
|
||||
const nodes = computed(() => services?.editorService.get<MNode[]>('nodes'));
|
||||
let keycon: KeyController;
|
||||
const mouseleaveHandler = () => {
|
||||
workspace.value?.blur();
|
||||
};
|
||||
|
||||
const mouseenterHandler = () => {
|
||||
workspace.value?.focus();
|
||||
};
|
||||
let keycon: KeyController;
|
||||
|
||||
const mouseleaveHandler = () => {
|
||||
workspace.value?.blur();
|
||||
};
|
||||
onMounted(() => {
|
||||
workspace.value?.addEventListener('mouseenter', mouseenterHandler);
|
||||
workspace.value?.addEventListener('mouseleave', mouseleaveHandler);
|
||||
|
||||
onMounted(() => {
|
||||
workspace.value?.addEventListener('mouseenter', mouseenterHandler);
|
||||
workspace.value?.addEventListener('mouseleave', mouseleaveHandler);
|
||||
keycon = new KeyController(workspace.value);
|
||||
|
||||
keycon = new KeyController(workspace.value);
|
||||
const isMac = /mac os x/.test(navigator.userAgent.toLowerCase());
|
||||
|
||||
const isMac = /mac os x/.test(navigator.userAgent.toLowerCase());
|
||||
const ctrl = isMac ? 'meta' : 'ctrl';
|
||||
|
||||
const ctrl = isMac ? 'meta' : 'ctrl';
|
||||
|
||||
keycon
|
||||
.keyup('delete', (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
if (!nodes.value || isPage(nodes.value[0])) return;
|
||||
services?.editorService.remove(nodes.value);
|
||||
})
|
||||
.keyup('backspace', (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
if (!nodes.value || isPage(nodes.value[0])) return;
|
||||
services?.editorService.remove(nodes.value);
|
||||
})
|
||||
.keydown([ctrl, 'c'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
nodes.value && services?.editorService.copy(nodes.value);
|
||||
})
|
||||
.keydown([ctrl, 'v'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
nodes.value && services?.editorService.paste({ offsetX: 10, offsetY: 10 });
|
||||
})
|
||||
.keydown([ctrl, 'x'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
if (!nodes.value || isPage(nodes.value[0])) return;
|
||||
services?.editorService.copy(nodes.value);
|
||||
services?.editorService.remove(nodes.value);
|
||||
})
|
||||
.keydown([ctrl, 'z'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.undo();
|
||||
})
|
||||
.keydown([ctrl, 'shift', 'z'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.redo();
|
||||
})
|
||||
.keydown('up', (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(0, -1);
|
||||
})
|
||||
.keydown('down', (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(0, 1);
|
||||
})
|
||||
.keydown('left', (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(-1, 0);
|
||||
})
|
||||
.keydown('right', (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(1, 0);
|
||||
})
|
||||
.keydown([ctrl, 'up'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(0, -10);
|
||||
})
|
||||
.keydown([ctrl, 'down'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(0, 10);
|
||||
})
|
||||
.keydown([ctrl, 'left'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(-10, 0);
|
||||
})
|
||||
.keydown([ctrl, 'right'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(10, 0);
|
||||
})
|
||||
.keydown('tab', (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.selectNextNode();
|
||||
})
|
||||
.keydown([ctrl, 'tab'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.selectNextPage();
|
||||
})
|
||||
.keydown([ctrl, '='], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.uiService.zoom(0.1);
|
||||
})
|
||||
.keydown([ctrl, 'numpadplus'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.uiService.zoom(0.1);
|
||||
})
|
||||
.keydown([ctrl, '-'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.uiService.zoom(-0.1);
|
||||
})
|
||||
.keydown([ctrl, 'numpad-'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.uiService.zoom(-0.1);
|
||||
})
|
||||
.keydown([ctrl, '0'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.uiService.set('zoom', services.uiService.calcZoom());
|
||||
})
|
||||
.keydown([ctrl, '1'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.uiService.set('zoom', 1);
|
||||
});
|
||||
keycon
|
||||
.keyup('delete', (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
if (!nodes.value || isPage(nodes.value[0])) return;
|
||||
services?.editorService.remove(nodes.value);
|
||||
})
|
||||
.keyup('backspace', (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
if (!nodes.value || isPage(nodes.value[0])) return;
|
||||
services?.editorService.remove(nodes.value);
|
||||
})
|
||||
.keydown([ctrl, 'c'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
nodes.value && services?.editorService.copy(nodes.value);
|
||||
})
|
||||
.keydown([ctrl, 'v'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
nodes.value && services?.editorService.paste({ offsetX: 10, offsetY: 10 });
|
||||
})
|
||||
.keydown([ctrl, 'x'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
if (!nodes.value || isPage(nodes.value[0])) return;
|
||||
services?.editorService.copy(nodes.value);
|
||||
services?.editorService.remove(nodes.value);
|
||||
})
|
||||
.keydown([ctrl, 'z'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.undo();
|
||||
})
|
||||
.keydown([ctrl, 'shift', 'z'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.redo();
|
||||
})
|
||||
.keydown('up', (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(0, -1);
|
||||
})
|
||||
.keydown('down', (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(0, 1);
|
||||
})
|
||||
.keydown('left', (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(-1, 0);
|
||||
})
|
||||
.keydown('right', (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(1, 0);
|
||||
})
|
||||
.keydown([ctrl, 'up'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(0, -10);
|
||||
})
|
||||
.keydown([ctrl, 'down'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(0, 10);
|
||||
})
|
||||
.keydown([ctrl, 'left'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(-10, 0);
|
||||
})
|
||||
.keydown([ctrl, 'right'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.move(10, 0);
|
||||
})
|
||||
.keydown('tab', (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.selectNextNode();
|
||||
})
|
||||
.keydown([ctrl, 'tab'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.editorService.selectNextPage();
|
||||
})
|
||||
.keydown([ctrl, '='], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.uiService.zoom(0.1);
|
||||
})
|
||||
.keydown([ctrl, 'numpadplus'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.uiService.zoom(0.1);
|
||||
})
|
||||
.keydown([ctrl, '-'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.uiService.zoom(-0.1);
|
||||
})
|
||||
.keydown([ctrl, 'numpad-'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.uiService.zoom(-0.1);
|
||||
})
|
||||
.keydown([ctrl, '0'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.uiService.set('zoom', services.uiService.calcZoom());
|
||||
})
|
||||
.keydown([ctrl, '1'], (e) => {
|
||||
e.inputEvent.preventDefault();
|
||||
services?.uiService.set('zoom', 1);
|
||||
});
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
workspace.value?.removeEventListener('mouseenter', mouseenterHandler);
|
||||
workspace.value?.removeEventListener('mouseleave', mouseleaveHandler);
|
||||
keycon.destroy();
|
||||
});
|
||||
|
||||
return {
|
||||
workspace,
|
||||
|
||||
page: computed(() => services?.editorService.get<MPage>('page')),
|
||||
};
|
||||
},
|
||||
onUnmounted(() => {
|
||||
workspace.value?.removeEventListener('mouseenter', mouseenterHandler);
|
||||
workspace.value?.removeEventListener('mouseleave', mouseleaveHandler);
|
||||
keycon.destroy();
|
||||
});
|
||||
</script>
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -20,3 +20,4 @@ export * from './config';
|
||||
export * from './props';
|
||||
export * from './logger';
|
||||
export * from './editor';
|
||||
export * from './stage';
|
||||
|
@ -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;
|
||||
public scrollTo({ left, top }: { left?: number; top?: number }) {
|
||||
if (typeof left !== 'undefined') {
|
||||
this.scrollLeft = left;
|
||||
}
|
||||
|
||||
this.target.style.transform = `translate(${scrollLeft}px, ${scrollTop}px)`;
|
||||
}
|
||||
if (typeof top !== 'undefined') {
|
||||
this.scrollTop = top;
|
||||
}
|
||||
|
||||
private removeHandler() {
|
||||
this.target.style.cursor = '';
|
||||
this.target.removeEventListener('mousedown', this.mousedownHandler);
|
||||
document.removeEventListener('mousemove', this.mousemoveHandler);
|
||||
document.removeEventListener('mouseup', this.mouseupHandler);
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
return pos;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
let left: number | undefined;
|
||||
let top: number | undefined;
|
||||
if (this.scrollWidth < this.width) {
|
||||
left = 0;
|
||||
this.translateXCorrectionValue = 0;
|
||||
} else {
|
||||
this.translateXCorrectionValue = (this.scrollWidth - this.width) / 2;
|
||||
}
|
||||
if (this.scrollHeight < this.height) {
|
||||
top = 0;
|
||||
this.translateYCorrectionValue = 0;
|
||||
} else {
|
||||
this.translateYCorrectionValue = (this.scrollHeight - this.height) / 2;
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
91
packages/editor/src/utils/stage.ts
Normal file
91
packages/editor/src/utils/stage.ts
Normal 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;
|
||||
};
|
Loading…
x
Reference in New Issue
Block a user