From de8ef8dc58d686adfd940f5be2de5b4285339606 Mon Sep 17 00:00:00 2001 From: roymondchen Date: Fri, 26 Aug 2022 21:58:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(editor):=20=E7=94=BB=E5=B8=83=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E6=BB=9A=E5=8A=A8=E6=9D=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix #262 --- packages/editor/src/components/ScrollBar.vue | 147 ++++++++++ .../editor/src/components/ScrollViewer.vue | 133 ++++++--- .../editor/src/layouts/workspace/Stage.vue | 98 +------ .../src/layouts/workspace/Workspace.vue | 261 +++++++++--------- packages/editor/src/type.ts | 7 + packages/editor/src/utils/index.ts | 1 + packages/editor/src/utils/scroll-viewer.ts | 254 +++++++---------- packages/editor/src/utils/stage.ts | 91 ++++++ 8 files changed, 559 insertions(+), 433 deletions(-) create mode 100644 packages/editor/src/components/ScrollBar.vue create mode 100644 packages/editor/src/utils/stage.ts diff --git a/packages/editor/src/components/ScrollBar.vue b/packages/editor/src/components/ScrollBar.vue new file mode 100644 index 00000000..6fe3e741 --- /dev/null +++ b/packages/editor/src/components/ScrollBar.vue @@ -0,0 +1,147 @@ + + + + + diff --git a/packages/editor/src/components/ScrollViewer.vue b/packages/editor/src/components/ScrollViewer.vue index 42ca7ffe..1af42bbd 100644 --- a/packages/editor/src/components/ScrollViewer.vue +++ b/packages/editor/src/components/ScrollViewer.vue @@ -3,64 +3,109 @@
+ + + - diff --git a/packages/editor/src/layouts/workspace/Stage.vue b/packages/editor/src/layouts/workspace/Stage.vue index 14465349..90e7c1bc 100644 --- a/packages/editor/src/layouts/workspace/Stage.vue +++ b/packages/editor/src/layouts/workspace/Stage.vue @@ -4,12 +4,14 @@ ref="stageWrap" :width="stageRect?.width" :height="stageRect?.height" + :wrap-width="stageContainerRect?.width" + :wrap-height="stageContainerRect?.height" :zoom="zoom" >
>(); const isMultiSelect = computed(() => services?.editorService.get('nodes')?.length > 1); const stageRect = computed(() => services?.uiService.get('stageRect')); -const uiSelectMode = computed(() => services?.uiService.get('uiSelectMode')); +const stageContainerRect = computed(() => services?.uiService.get('stageContainerRect')); const root = computed(() => services?.editorService.get('root')); const page = computed(() => services?.editorService.get('page')); const zoom = computed(() => services?.uiService.get('zoom') || 1); const node = computed(() => services?.editorService.get('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; diff --git a/packages/editor/src/layouts/workspace/Workspace.vue b/packages/editor/src/layouts/workspace/Workspace.vue index 6b39383a..41723206 100644 --- a/packages/editor/src/layouts/workspace/Workspace.vue +++ b/packages/editor/src/layouts/workspace/Workspace.vue @@ -1,20 +1,20 @@ - diff --git a/packages/editor/src/type.ts b/packages/editor/src/type.ts index e04318d0..c944686e 100644 --- a/packages/editor/src/type.ts +++ b/packages/editor/src/type.ts @@ -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; +} diff --git a/packages/editor/src/utils/index.ts b/packages/editor/src/utils/index.ts index 3f7120c4..320231bd 100644 --- a/packages/editor/src/utils/index.ts +++ b/packages/editor/src/utils/index.ts @@ -20,3 +20,4 @@ export * from './config'; export * from './props'; export * from './logger'; export * from './editor'; +export * from './stage'; diff --git a/packages/editor/src/utils/scroll-viewer.ts b/packages/editor/src/utils/scroll-viewer.ts index 4cdaff1b..2eb6df9e 100644 --- a/packages/editor/src/utils/scroll-viewer.ts +++ b/packages/editor/src/utils/scroll-viewer.ts @@ -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; + }; } diff --git a/packages/editor/src/utils/stage.ts b/packages/editor/src/utils/stage.ts new file mode 100644 index 00000000..0592b0ec --- /dev/null +++ b/packages/editor/src/utils/stage.ts @@ -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('root')); +const page = computed(() => editorService.get('page')); +const zoom = computed(() => uiService.get('zoom') || 1); +const uiSelectMode = computed(() => uiService.get('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; +};