From 9157ae837b09e5cf6759810a554f1e2067ed1f8f Mon Sep 17 00:00:00 2001 From: dineug Date: Sat, 13 Jan 2024 02:07:33 +0900 Subject: [PATCH] feat: virtual scroll --- packages/erd-editor-vscode/package.json | 2 +- packages/erd-editor/package.json | 2 +- packages/erd-editor/src/components/erd/Erd.ts | 17 +- .../virtual-scroll/VirtualScroll.styles.ts | 51 ++++++ .../erd/virtual-scroll/VirtualScroll.ts | 150 +++++++++++++++++ .../erd/virtual-scroll/useVirtualScroll.ts | 159 ++++++++++++++++++ 6 files changed, 377 insertions(+), 4 deletions(-) create mode 100644 packages/erd-editor/src/components/erd/virtual-scroll/VirtualScroll.styles.ts create mode 100644 packages/erd-editor/src/components/erd/virtual-scroll/VirtualScroll.ts create mode 100644 packages/erd-editor/src/components/erd/virtual-scroll/useVirtualScroll.ts diff --git a/packages/erd-editor-vscode/package.json b/packages/erd-editor-vscode/package.json index 0940f052..e93655ec 100644 --- a/packages/erd-editor-vscode/package.json +++ b/packages/erd-editor-vscode/package.json @@ -1,6 +1,6 @@ { "name": "vuerd-vscode", - "version": "1.0.8", + "version": "1.0.9", "private": true, "description": "Entity-Relationship Diagram Editor VSCode Extension", "icon": "./assets/erd-editor.png", diff --git a/packages/erd-editor/package.json b/packages/erd-editor/package.json index 18cf494d..239deac8 100644 --- a/packages/erd-editor/package.json +++ b/packages/erd-editor/package.json @@ -1,6 +1,6 @@ { "name": "@dineug/erd-editor", - "version": "3.1.2", + "version": "3.1.3", "description": "Entity-Relationship Diagram Editor", "type": "module", "main": "./dist/erd-editor.js", diff --git a/packages/erd-editor/src/components/erd/Erd.ts b/packages/erd-editor/src/components/erd/Erd.ts index fc2022b3..95698c05 100644 --- a/packages/erd-editor/src/components/erd/Erd.ts +++ b/packages/erd-editor/src/components/erd/Erd.ts @@ -21,6 +21,7 @@ import ErdContextMenu, { import HideSign from '@/components/erd/hide-sign/HideSign'; import Minimap from '@/components/erd/minimap/Minimap'; import TableProperties from '@/components/erd/table-properties/TableProperties'; +import VirtualScroll from '@/components/erd/virtual-scroll/VirtualScroll'; import ColorPicker from '@/components/primitives/color-picker/ColorPicker'; import { useContextMenuRootProvider } from '@/components/primitives/context-menu/context-menu-root/contextMenuRootContext'; import { Open } from '@/constants/open'; @@ -109,8 +110,18 @@ const Erd: FC = (props, ctx) => { }; const handleWheel = (event: WheelEvent) => { + event.preventDefault(); + const $mod = isMod(event); const { store } = app.value; - store.dispatch(streamZoomLevelAction$(event.deltaY < 0 ? 0.1 : -0.1)); + + store.dispatch( + $mod + ? streamZoomLevelAction$(event.deltaY < 0 ? 0.1 : -0.1) + : streamScrollToAction({ + movementX: event.deltaX * -1, + movementY: event.deltaY * -1, + }) + ); }; const handleMove = ({ event, movementX, movementY }: DragMove) => { @@ -141,7 +152,8 @@ const Erd: FC = (props, ctx) => { canUnselectAll && canHideColorPicker && !el.closest('.minimap') && - !el.closest('.minimap-viewport'); + !el.closest('.minimap-viewport') && + !el.closest('.virtual-scroll'); if (canUnselectAll) { const { store } = app.value; @@ -336,6 +348,7 @@ const Erd: FC = (props, ctx) => { @wheel=${handleWheel} > <${Canvas} root=${root} canvas=${canvas} grabMove=${state.grabMove} /> + <${VirtualScroll} /> <${Minimap} /> <${HideSign} root=${root} /> ${state.dragSelect diff --git a/packages/erd-editor/src/components/erd/virtual-scroll/VirtualScroll.styles.ts b/packages/erd-editor/src/components/erd/virtual-scroll/VirtualScroll.styles.ts new file mode 100644 index 00000000..75a1f40f --- /dev/null +++ b/packages/erd-editor/src/components/erd/virtual-scroll/VirtualScroll.styles.ts @@ -0,0 +1,51 @@ +import { css } from '@dineug/r-html'; + +export const vertical = css` + position: absolute; + top: 0; + right: 0; + width: 8px; + height: calc(100% - 8px); + overflow: hidden; + padding-top: 4px; +`; + +export const horizontal = css` + position: absolute; + left: 0; + bottom: 0; + width: calc(100% - 8px); + height: 8px; + overflow: hidden; + padding-left: 4px; +`; + +export const ghostThumb = css` + will-change: transform; + cursor: pointer; + + &:hover > div { + background-color: var(--scrollbar-thumb-hover); + } + + &[data-selected] > div { + background-color: var(--scrollbar-thumb-hover); + } +`; + +const thumb = css` + background-color: var(--scrollbar-thumb); + border-radius: 4px; +`; + +export const verticalThumb = css` + width: 4px; + height: 100%; + ${thumb}; +`; + +export const horizontalThumb = css` + width: 100%; + height: 4px; + ${thumb}; +`; diff --git a/packages/erd-editor/src/components/erd/virtual-scroll/VirtualScroll.ts b/packages/erd-editor/src/components/erd/virtual-scroll/VirtualScroll.ts new file mode 100644 index 00000000..f94191b5 --- /dev/null +++ b/packages/erd-editor/src/components/erd/virtual-scroll/VirtualScroll.ts @@ -0,0 +1,150 @@ +import { createRef, FC, html, ref } from '@dineug/r-html'; + +import { useAppContext } from '@/components/appContext'; +import { scrollToAction } from '@/engine/modules/settings/atom.actions'; + +import { useVirtualScroll } from './useVirtualScroll'; +import * as styles from './VirtualScroll.styles'; + +export type VirtualScrollProps = {}; + +const VirtualScroll: FC = (props, ctx) => { + const app = useAppContext(ctx); + const { + state, + getWidthRatio, + getHeightRatio, + onScrollLeftStart, + onScrollTopStart, + } = useVirtualScroll(ctx); + const horizontal = createRef(); + const vertical = createRef(); + + const handleMoveLeft = (event: MouseEvent) => { + const el = event.target as HTMLElement | null; + if (!el) return; + + const canMove = !el.closest('.virtual-scroll-ghost-thumb'); + if (!canMove) return; + + const { store } = app.value; + const { + editor: { viewport }, + settings, + } = store.state; + const ratio = getWidthRatio(); + const $horizontal = horizontal.value; + const rect = $horizontal.getBoundingClientRect(); + const clientX = event.clientX; + + const x = clientX - rect.x; + const absoluteX = x / ratio; + const scrollLeft = absoluteX - viewport.width / 2; + + store.dispatch( + scrollToAction({ + scrollLeft: -1 * scrollLeft, + scrollTop: settings.scrollTop, + }) + ); + + onScrollLeftStart(event); + }; + + const handleMoveTop = (event: MouseEvent) => { + const el = event.target as HTMLElement | null; + if (!el) return; + + const canMove = !el.closest('.virtual-scroll-ghost-thumb'); + if (!canMove) return; + + const { store } = app.value; + const { + editor: { viewport }, + } = store.state; + const ratio = getHeightRatio(); + const $vertical = vertical.value; + const rect = $vertical.getBoundingClientRect(); + const clientY = event.clientY; + + const y = clientY - rect.y; + const absoluteY = y / ratio; + const scrollTop = absoluteY - viewport.height / 2; + + store.dispatch( + scrollToAction({ + scrollLeft: store.state.settings.scrollLeft, + scrollTop: -1 * scrollTop, + }) + ); + + onScrollTopStart(event); + }; + + return () => { + const { store } = app.value; + const { + editor: { viewport }, + settings: { width, height, scrollLeft, scrollTop }, + } = store.state; + + const wRatio = getWidthRatio(); + const hRatio = getHeightRatio(); + const w = viewport.width * wRatio; + const h = viewport.height * hRatio; + const left = -1 * scrollLeft * wRatio; + const top = -1 * scrollTop * hRatio; + + const showHorizontal = viewport.width < width; + const showVertical = viewport.height < height; + + return html` + ${showHorizontal + ? html` +
+
+
+
+
+ ` + : null} + ${showVertical + ? html` +
+
+
+
+
+ ` + : null} + `; + }; +}; + +export default VirtualScroll; diff --git a/packages/erd-editor/src/components/erd/virtual-scroll/useVirtualScroll.ts b/packages/erd-editor/src/components/erd/virtual-scroll/useVirtualScroll.ts new file mode 100644 index 00000000..dbf5f170 --- /dev/null +++ b/packages/erd-editor/src/components/erd/virtual-scroll/useVirtualScroll.ts @@ -0,0 +1,159 @@ +import { observable } from '@dineug/r-html'; + +import { useAppContext } from '@/components/appContext'; +import { streamScrollToAction } from '@/engine/modules/settings/atom.actions'; +import { Ctx } from '@/internal-types'; +import { DirectionName } from '@/utils/draw-relationship'; +import { drag$, DragMove } from '@/utils/globalEventObservable'; + +export function useVirtualScroll(ctx: Ctx) { + const app = useAppContext(ctx); + const state = observable({ + selected: null as null | 'horizontal' | 'vertical', + }); + + let clientX = 0; + let clientY = 0; + + const getWidthRatio = () => { + const { store } = app.value; + const { + editor: { viewport }, + settings: { width }, + } = store.state; + return viewport.width / width; + }; + + const getHeightRatio = () => { + const { store } = app.value; + const { + editor: { viewport }, + settings: { height }, + } = store.state; + return viewport.height / height; + }; + + const absoluteMovement = (movement: number, ratio: number) => { + return -1 * (movement / ratio); + }; + + const getMovementX = ({ movementX, x }: DragMove) => { + const { store } = app.value; + const { + settings, + editor: { viewport }, + } = store.state; + const scrollLeft = + settings.scrollLeft + absoluteMovement(movementX, getWidthRatio()); + const min = viewport.width - settings.width; + const max = 0; + const direction = movementX < 0 ? DirectionName.left : DirectionName.right; + let change = false; + + switch (direction) { + case DirectionName.left: + if (scrollLeft < max && x < clientX) { + clientX += movementX; + change = true; + } + break; + case DirectionName.right: + if (scrollLeft > min && x > clientX) { + clientX += movementX; + change = true; + } + break; + } + + return change ? movementX : 0; + }; + + const getMovementY = ({ movementY, y }: DragMove) => { + const { store } = app.value; + const { + settings, + editor: { viewport }, + } = store.state; + const scrollTop = + settings.scrollTop + absoluteMovement(movementY, getHeightRatio()); + const min = viewport.height - settings.height; + const max = 0; + const direction = movementY < 0 ? DirectionName.top : DirectionName.bottom; + let change = false; + + switch (direction) { + case DirectionName.top: + if (scrollTop < max && y < clientY) { + clientY += movementY; + change = true; + } + break; + case DirectionName.bottom: + if (scrollTop > min && y > clientY) { + clientY += movementY; + change = true; + } + break; + } + + return change ? movementY : 0; + }; + + const handleScroll = (dragMove: DragMove) => { + const { event } = dragMove; + event.type === 'mousemove' && event.preventDefault(); + const isVertical = state.selected === 'vertical'; + const isHorizontal = state.selected === 'horizontal'; + const movementX = getMovementX(dragMove); + const movementY = getMovementY(dragMove); + const { store } = app.value; + + if (isVertical && movementY !== 0) { + store.dispatch( + streamScrollToAction({ + movementX: 0, + movementY: absoluteMovement(movementY, getHeightRatio()), + }) + ); + } else if (isHorizontal && movementX !== 0) { + store.dispatch( + streamScrollToAction({ + movementX: absoluteMovement(movementX, getWidthRatio()), + movementY: 0, + }) + ); + } + }; + + const onScrollLeftStart = (event: MouseEvent) => { + state.selected = 'horizontal'; + clientX = event.clientX; + + drag$.subscribe({ + next: handleScroll, + complete: () => { + state.selected = null; + }, + }); + }; + + const onScrollTopStart = (event: MouseEvent) => { + state.selected = 'vertical'; + clientY = event.clientY; + + drag$.subscribe({ + next: handleScroll, + complete: () => { + state.selected = null; + }, + }); + }; + + return { + state, + onScrollLeftStart, + onScrollTopStart, + getWidthRatio, + getHeightRatio, + }; +}