From 500eecd22a6a8284d90e55ab48b7d178d308c02d Mon Sep 17 00:00:00 2001 From: dineug Date: Sun, 29 Oct 2023 17:03:28 +0900 Subject: [PATCH] feat: DragSelect --- packages/erd-editor/src/components/erd/Erd.ts | 33 ++++- .../components/erd/canvas/memo/Memo.styles.ts | 4 +- .../src/components/erd/canvas/memo/Memo.ts | 3 +- .../erd/canvas/table/Table.styles.ts | 32 ++--- .../src/components/erd/canvas/table/Table.ts | 49 ++++--- .../erd/canvas/table/column/Column.ts | 26 ++-- .../erd/drag-select/DragSelect.styles.ts | 8 ++ .../components/erd/drag-select/DragSelect.ts | 125 ++++++++++++++++++ .../components/primitives/kbd/Kbd.styles.ts | 3 +- packages/erd-editor/src/constants/layout.ts | 11 +- .../modules/editor/generator.actions.ts | 98 +++++++++++--- .../src/themes/radix-ui-theme.config.ts | 2 +- packages/erd-editor/src/utils/calcTable.ts | 4 +- packages/erd-editor/src/utils/dragSelect.ts | 60 +++++++++ 14 files changed, 377 insertions(+), 81 deletions(-) create mode 100644 packages/erd-editor/src/components/erd/drag-select/DragSelect.styles.ts create mode 100644 packages/erd-editor/src/components/erd/drag-select/DragSelect.ts create mode 100644 packages/erd-editor/src/utils/dragSelect.ts diff --git a/packages/erd-editor/src/components/erd/Erd.ts b/packages/erd-editor/src/components/erd/Erd.ts index 6b18a147..86019a5d 100644 --- a/packages/erd-editor/src/components/erd/Erd.ts +++ b/packages/erd-editor/src/components/erd/Erd.ts @@ -1,7 +1,8 @@ -import { createRef, FC, html, ref } from '@dineug/r-html'; +import { createRef, FC, html, observable, ref } from '@dineug/r-html'; import { useAppContext } from '@/components/context'; import Canvas from '@/components/erd/canvas/Canvas'; +import DragSelect from '@/components/erd/drag-select/DragSelect'; import ErdContextMenu, { ErdContextMenuType, } from '@/components/erd/erd-context-menu/ErdContextMenu'; @@ -12,6 +13,7 @@ import { streamZoomLevelAction, } from '@/engine/modules/settings/atom.actions'; import { drag$, DragMove } from '@/utils/globalEventObservable'; +import { isMod } from '@/utils/keyboard-shortcut'; import * as styles from './Erd.styles'; import { useErdShortcut } from './useErdShortcut'; @@ -22,6 +24,11 @@ const Erd: FC = (props, ctx) => { const contextMenu = useContextMenuRootProvider(ctx); const root = createRef(); const app = useAppContext(ctx); + const state = observable({ + dragSelect: false, + dragSelectX: 0, + dragSelectY: 0, + }); useErdShortcut(ctx); const resetScroll = () => { @@ -64,8 +71,18 @@ const Erd: FC = (props, ctx) => { const { store } = app.value; store.dispatch(unselectAllAction()); - // TODO: dragSelect - drag$.subscribe(handleMove); + if (event.type === 'mousedown' && isMod(event)) { + const { x, y } = root.value.getBoundingClientRect(); + state.dragSelect = true; + state.dragSelectX = event.clientX - x; + state.dragSelectY = event.clientY - y; + } else { + drag$.subscribe(handleMove); + } + }; + + const handleDragSelectEnd = () => { + state.dragSelect = false; }; return () => @@ -80,6 +97,16 @@ const Erd: FC = (props, ctx) => { @wheel=${handleWheel} > <${Canvas} /> + ${state.dragSelect + ? html` + <${DragSelect} + root=${root} + x=${state.dragSelectX} + y=${state.dragSelectY} + .onDragSelectEnd=${handleDragSelectEnd} + /> + ` + : null} <${ErdContextMenu} type=${ErdContextMenuType.ERD} root=${root} diff --git a/packages/erd-editor/src/components/erd/canvas/memo/Memo.styles.ts b/packages/erd-editor/src/components/erd/canvas/memo/Memo.styles.ts index 4d262322..1fab305c 100644 --- a/packages/erd-editor/src/components/erd/canvas/memo/Memo.styles.ts +++ b/packages/erd-editor/src/components/erd/canvas/memo/Memo.styles.ts @@ -56,12 +56,12 @@ export const headerButtonWrap = css` justify-content: flex-end; margin-bottom: ${HEADER_ICON_MARGIN_BOTTOM}px; - & > div { + & > .icon { margin-left: 4px; cursor: pointer; } - & > div:hover { + & > .icon:hover { fill: var(--active); color: var(--active); } diff --git a/packages/erd-editor/src/components/erd/canvas/memo/Memo.ts b/packages/erd-editor/src/components/erd/canvas/memo/Memo.ts index 7b25122a..650b62f7 100644 --- a/packages/erd-editor/src/components/erd/canvas/memo/Memo.ts +++ b/packages/erd-editor/src/components/erd/canvas/memo/Memo.ts @@ -38,6 +38,7 @@ const Memo: FC = (props, ctx) => { store.dispatch(selectMemoAction$(props.memo.id, isMod(event))); if ( + !el.closest('.memo-header-color') && !el.closest('.memo-textarea') && !el.closest('.icon') && !el.closest('.sash') @@ -88,7 +89,7 @@ const Memo: FC = (props, ctx) => {
.edit-input { - margin-right: ${INPUT_MARGIN_RIGHT}px; - } -`; - export const headerButtonWrap = css` display: flex; - height: 100%; - align-items: center; - margin-left: auto; + height: ${HEADER_ICON_HEIGHT}px; + justify-content: flex-end; + margin-bottom: ${TABLE_HEADER_ICON_MARGIN_BOTTOM}px; & > .icon { cursor: pointer; @@ -76,3 +67,14 @@ export const headerButtonWrap = css` color: var(--active); } `; + +export const headerInputWrap = css` + display: flex; + height: ${TABLE_HEADER_INPUT_HEIGHT}px; + align-items: center; + padding: ${TABLE_HEADER_PADDING}px 0; + + & > .edit-input { + margin-right: ${INPUT_MARGIN_RIGHT}px; + } +`; diff --git a/packages/erd-editor/src/components/erd/canvas/table/Table.ts b/packages/erd-editor/src/components/erd/canvas/table/Table.ts index f119f62c..17e04d91 100644 --- a/packages/erd-editor/src/components/erd/canvas/table/Table.ts +++ b/packages/erd-editor/src/components/erd/canvas/table/Table.ts @@ -41,7 +41,12 @@ const Table: FC = (props, ctx) => { const { store } = app.value; store.dispatch(selectTableAction$(props.table.id, isMod(event))); - if (!el.closest('.edit-input') && !el.closest('.icon')) { + if ( + !el.closest('.table-header-color') && + !el.closest('.column-row') && + !el.closest('.icon') && + !el.closest('.edit-input') + ) { drag$.subscribe(handleMove); } }; @@ -84,11 +89,31 @@ const Table: FC = (props, ctx) => { >
+
+ <${Icon} + size=${12} + name="plus" + title=${simpleShortcutToString( + keyBindingMap.addColumn[0]?.shortcut + )} + useTransition=${true} + .onClick=${handleAddColumn} + /> + <${Icon} + size=${12} + name="xmark" + title=${simpleShortcutToString( + keyBindingMap.removeTable[0]?.shortcut + )} + useTransition=${true} + .onClick=${handleRemoveTable} + /> +
<${EditInput} placeholder="table" @@ -104,26 +129,6 @@ const Table: FC = (props, ctx) => { /> ` : null} -
- <${Icon} - size=${12} - name="plus" - title=${simpleShortcutToString( - keyBindingMap.addColumn[0]?.shortcut - )} - useTransition=${true} - .onClick=${handleAddColumn} - /> - <${Icon} - size=${12} - name="xmark" - title=${simpleShortcutToString( - keyBindingMap.removeTable[0]?.shortcut - )} - useTransition=${true} - .onClick=${handleRemoveTable} - /> -
diff --git a/packages/erd-editor/src/components/erd/canvas/table/column/Column.ts b/packages/erd-editor/src/components/erd/canvas/table/column/Column.ts index 08afeef7..a7203f23 100644 --- a/packages/erd-editor/src/components/erd/canvas/table/column/Column.ts +++ b/packages/erd-editor/src/components/erd/canvas/table/column/Column.ts @@ -65,18 +65,6 @@ const Column: FC = (props, ctx) => { /> `; break; - case ColumnType.columnDataType: - template = bHas(settings.show, Show.columnDataType) - ? html` - <${EditInput} - class=${'column-col'} - placeholder="dataType" - width=${widthDataType} - value=${column.dataType} - /> - ` - : null; - break; case ColumnType.columnDefault: template = bHas(settings.show, Show.columnDefault) ? html` @@ -101,6 +89,18 @@ const Column: FC = (props, ctx) => { ` : null; break; + case ColumnType.columnDataType: + template = bHas(settings.show, Show.columnDataType) + ? html` + <${EditInput} + class=${'column-col'} + placeholder="dataType" + width=${widthDataType} + value=${column.dataType} + /> + ` + : null; + break; case ColumnType.columnNotNull: template = bHas(settings.show, Show.columnNotNull) ? html`<${ColumnNotNull} options=${column.options} />` @@ -151,7 +151,7 @@ const Column: FC = (props, ctx) => { const { column } = props; return html` -
+
<${ColumnKey} keys=${column.ui.keys} /> ${repeat( getColumnOrder(), diff --git a/packages/erd-editor/src/components/erd/drag-select/DragSelect.styles.ts b/packages/erd-editor/src/components/erd/drag-select/DragSelect.styles.ts new file mode 100644 index 00000000..1d594dd1 --- /dev/null +++ b/packages/erd-editor/src/components/erd/drag-select/DragSelect.styles.ts @@ -0,0 +1,8 @@ +import { css } from '@dineug/r-html'; + +export const dragSelect = css` + position: absolute; + stroke: var(--darg-select-border); + fill: var(--darg-select-background); + pointer-events: none; +`; diff --git a/packages/erd-editor/src/components/erd/drag-select/DragSelect.ts b/packages/erd-editor/src/components/erd/drag-select/DragSelect.ts new file mode 100644 index 00000000..df7da88a --- /dev/null +++ b/packages/erd-editor/src/components/erd/drag-select/DragSelect.ts @@ -0,0 +1,125 @@ +import { FC, observable, onBeforeMount, Ref, svg } from '@dineug/r-html'; +import { fromEvent } from 'rxjs'; + +import { useAppContext } from '@/components/context'; +import { dragSelectAction$ } from '@/engine/modules/editor/generator.actions'; +import { useUnmounted } from '@/hooks/useUnmounted'; +import { + getAbsolutePosition, + getOverlapPosition, + getZoomViewport, +} from '@/utils/dragSelect'; +import { mouseup$ } from '@/utils/globalEventObservable'; + +import * as styles from './DragSelect.styles'; + +export type DragSelectProps = { + x: number; + y: number; + root: Ref; + onDragSelectEnd: () => void; +}; + +const DragSelect: FC = (props, ctx) => { + const app = useAppContext(ctx); + const state = observable({ width: 0, height: 0, top: 0, left: 0 }); + const { addUnsubscribe } = useUnmounted(); + + onBeforeMount(() => { + const { store } = app.value; + const { settings } = store.state; + const $root = props.root.value; + + addUnsubscribe( + mouseup$.subscribe(props.onDragSelectEnd), + fromEvent($root, 'mousemove').subscribe(event => { + event.preventDefault(); + const rect = $root.getBoundingClientRect(); + const currentX = event.clientX - rect.x; + const currentY = event.clientY - rect.y; + const min = { + x: props.x < currentX ? props.x : currentX, + y: props.y < currentY ? props.y : currentY, + }; + const max = { + x: props.x > currentX ? props.x : currentX, + y: props.y > currentY ? props.y : currentY, + }; + + state.left = min.x; + state.width = max.x - min.x; + if (state.width < 0) { + state.width = 0; + } + + state.top = min.y; + state.height = max.y - min.y; + if (state.height < 0) { + state.height = 0; + } + + const ghostMin = Object.assign({}, min); + const ghostMax = Object.assign({}, max); + + ghostMin.x -= settings.scrollLeft; + ghostMin.y -= settings.scrollTop; + ghostMax.x -= settings.scrollLeft; + ghostMax.y -= settings.scrollTop; + + const zoomViewportRect = getZoomViewport( + settings.width, + settings.height, + settings.zoomLevel + ); + + const overlapPosition = getOverlapPosition( + { + ...ghostMin, + w: ghostMax.x - ghostMin.x, + h: ghostMax.y - ghostMin.y, + }, + zoomViewportRect + ); + + if (!overlapPosition) return; + + const absolutePosition = getAbsolutePosition( + overlapPosition, + zoomViewportRect, + settings.zoomLevel + ); + + ghostMin.x = absolutePosition.x1; + ghostMin.y = absolutePosition.y1; + ghostMax.x = absolutePosition.x2; + ghostMax.y = absolutePosition.y2; + + store.dispatch(dragSelectAction$(ghostMin, ghostMax)); + }) + ); + }); + + return () => svg` + + + + + `; +}; + +export default DragSelect; diff --git a/packages/erd-editor/src/components/primitives/kbd/Kbd.styles.ts b/packages/erd-editor/src/components/primitives/kbd/Kbd.styles.ts index 9ed82dec..b8298c20 100644 --- a/packages/erd-editor/src/components/primitives/kbd/Kbd.styles.ts +++ b/packages/erd-editor/src/components/primitives/kbd/Kbd.styles.ts @@ -7,7 +7,8 @@ export const root = css` `; export const kbd = css` - display: inline-block; + display: inline-flex; + align-items: center; padding-left: 0.5em; padding-right: 0.5em; diff --git a/packages/erd-editor/src/constants/layout.ts b/packages/erd-editor/src/constants/layout.ts index 51569789..7e421d02 100644 --- a/packages/erd-editor/src/constants/layout.ts +++ b/packages/erd-editor/src/constants/layout.ts @@ -10,17 +10,20 @@ export const DEFAULT_HEIGHT = (DEFAULT_WIDTH / RATIO_WIDTH) * RATIO_HEIGHT; export const INPUT_HEIGHT = 20; export const INPUT_MARGIN_RIGHT = 8; -export const HEADER_ICON_WIDTH = 12; export const HEADER_ICON_HEIGHT = 12; export const HEADER_ICON_MARGIN_BOTTOM = 4; export const TABLE_BORDER = 1; export const TABLE_PADDING = 8; export const TABLE_HEADER_PADDING = 2; -export const TABLE_HEADER_HEIGHT = INPUT_HEIGHT + TABLE_HEADER_PADDING * 2; +export const TABLE_HEADER_ICON_MARGIN_BOTTOM = 2; +export const TABLE_HEADER_INPUT_HEIGHT = + INPUT_HEIGHT + TABLE_HEADER_PADDING * 2; +export const TABLE_HEADER_HEIGHT = + HEADER_ICON_HEIGHT + + TABLE_HEADER_ICON_MARGIN_BOTTOM + + TABLE_HEADER_INPUT_HEIGHT; export const TABLE_HEADER_BUTTON_MARGIN_LEFT = 4; -export const TABLE_HEADER_BUTTONS_WIDTH = - HEADER_ICON_WIDTH + TABLE_HEADER_BUTTON_MARGIN_LEFT + HEADER_ICON_WIDTH; export const COLUMN_DELETE_WIDTH = 12; export const COLUMN_KEY_WIDTH = 12; diff --git a/packages/erd-editor/src/engine/modules/editor/generator.actions.ts b/packages/erd-editor/src/engine/modules/editor/generator.actions.ts index 55eb0467..685cbffa 100644 --- a/packages/erd-editor/src/engine/modules/editor/generator.actions.ts +++ b/packages/erd-editor/src/engine/modules/editor/generator.actions.ts @@ -1,13 +1,43 @@ +import { isEmpty } from 'lodash-es'; + import { GeneratorAction } from '@/engine/generator.actions'; import { clearAction, loadJsonAction, + selectAction, + unselectAllAction, } from '@/engine/modules/editor/atom.actions'; import { SelectType } from '@/engine/modules/editor/state'; import { moveMemoAction } from '@/engine/modules/memo/atom.actions'; import { removeMemoAction$ } from '@/engine/modules/memo/generator.actions'; import { moveTableAction } from '@/engine/modules/table/atom.actions'; import { removeTableAction$ } from '@/engine/modules/table/generator.actions'; +import { Point } from '@/internal-types'; +import { calcMemoHeight, calcMemoWidth } from '@/utils/calcMemo'; +import { calcTableHeight, calcTableWidths } from '@/utils/calcTable'; +import { query } from '@/utils/collection/query'; + +type SelectTypeIds = { + tableIds: string[]; + memoIds: string[]; +}; + +function getSelectTypeIds( + selectedMap: Record +): SelectTypeIds { + return Object.entries(selectedMap).reduce( + (acc, [id, type]) => { + if (type === SelectType.table) { + acc.tableIds.push(id); + } else if (type === SelectType.memo) { + acc.memoIds.push(id); + } + + return acc; + }, + { tableIds: [], memoIds: [] } + ); +} export const loadJsonAction$ = (value: string): GeneratorAction => function* () { @@ -20,22 +50,7 @@ export const moveAllAction$ = ( movementY: number ): GeneratorAction => function* ({ editor: { selectedMap }, settings: { zoomLevel } }) { - const { tableIds, memoIds } = Object.entries(selectedMap).reduce( - (acc, [id, type]) => { - if (type === SelectType.table) { - acc.tableIds.push(id); - } else if (type === SelectType.memo) { - acc.memoIds.push(id); - } - - return acc; - }, - { - tableIds: [] as string[], - memoIds: [] as string[], - } - ); - + const { tableIds, memoIds } = getSelectTypeIds(selectedMap); const newMovementX = movementX / zoomLevel; const newMovementY = movementY / zoomLevel; @@ -62,8 +77,59 @@ export const removeSelectedAction$ = (): GeneratorAction => yield removeMemoAction$(); }; +export const dragSelectAction$ = (min: Point, max: Point): GeneratorAction => + function* (state) { + const { + doc: { tableIds, memoIds }, + collections, + } = state; + + const inRange = (x: number, y: number) => + min.x <= x && max.x >= x && min.y <= y && max.y >= y; + + const selectedMap: Record = { + ...query(collections) + .collection('tableEntities') + .selectByIds(tableIds) + .reduce>((acc, table) => { + const width = calcTableWidths(table, state).width; + const height = calcTableHeight(table); + const centerX = table.ui.x + width / 2; + const centerY = table.ui.y + height / 2; + + if (inRange(centerX, centerY)) { + acc[table.id] = SelectType.table; + } + + return acc; + }, {}), + ...query(collections) + .collection('memoEntities') + .selectByIds(memoIds) + .reduce>((acc, memo) => { + const width = calcMemoWidth(memo); + const height = calcMemoHeight(memo); + const centerX = memo.ui.x + width / 2; + const centerY = memo.ui.y + height / 2; + + if (inRange(centerX, centerY)) { + acc[memo.id] = SelectType.memo; + } + + return acc; + }, {}), + }; + + yield unselectAllAction(); + + if (!isEmpty(selectedMap)) { + yield selectAction(selectedMap); + } + }; + export const actions$ = { loadJsonAction$, moveAllAction$, removeSelectedAction$, + dragSelectAction$, }; diff --git a/packages/erd-editor/src/themes/radix-ui-theme.config.ts b/packages/erd-editor/src/themes/radix-ui-theme.config.ts index 88757af7..bad3dc75 100644 --- a/packages/erd-editor/src/themes/radix-ui-theme.config.ts +++ b/packages/erd-editor/src/themes/radix-ui-theme.config.ts @@ -26,7 +26,7 @@ export const ThemeConfig: Theme = { contextMenuHover: 'accent-7', contextMenuBorder: 'gray-6', - dargSelectBackground: 'grayA-4', + dargSelectBackground: 'accent-5', dargSelectBorder: 'accent-8', scrollbarTrack: 'grayA-3', diff --git a/packages/erd-editor/src/utils/calcTable.ts b/packages/erd-editor/src/utils/calcTable.ts index d817cc85..bcc16a6f 100644 --- a/packages/erd-editor/src/utils/calcTable.ts +++ b/packages/erd-editor/src/utils/calcTable.ts @@ -10,7 +10,6 @@ import { COLUMN_UNIQUE_WIDTH, INPUT_MARGIN_RIGHT, TABLE_BORDER, - TABLE_HEADER_BUTTONS_WIDTH, TABLE_HEADER_HEIGHT, TABLE_PADDING, } from '@/constants/layout'; @@ -23,8 +22,7 @@ export function calcTableWidths( table: Table, { settings: { show }, collections }: RootState ): ColumnWidth { - let width = - table.ui.widthName + INPUT_MARGIN_RIGHT + TABLE_HEADER_BUTTONS_WIDTH; + let width = table.ui.widthName + INPUT_MARGIN_RIGHT; if (bHas(show, SchemaV3Constants.Show.tableComment)) { width += table.ui.widthComment + INPUT_MARGIN_RIGHT; } diff --git a/packages/erd-editor/src/utils/dragSelect.ts b/packages/erd-editor/src/utils/dragSelect.ts new file mode 100644 index 00000000..2c321900 --- /dev/null +++ b/packages/erd-editor/src/utils/dragSelect.ts @@ -0,0 +1,60 @@ +interface PointToPoint { + x1: number; + y1: number; + x2: number; + y2: number; +} + +interface Rect { + x: number; + y: number; + w: number; + h: number; +} + +export function getOverlapPosition( + dragRect: Rect, + rect: Rect +): PointToPoint | null { + if ( + dragRect.x > rect.x + rect.w || + dragRect.x + dragRect.w < rect.x || + dragRect.y > rect.y + rect.h || + dragRect.y + dragRect.h < rect.y + ) + return null; + + const target: PointToPoint = { x1: 0, y1: 0, x2: 0, y2: 0 }; + target.x1 = Math.max(dragRect.x, rect.x); + target.y1 = Math.max(dragRect.y, rect.y); + target.x2 = Math.min(dragRect.x + dragRect.w, rect.x + rect.w) - rect.x; + target.y2 = Math.min(dragRect.y + dragRect.h, rect.y + rect.h) - rect.y; + + return target; +} + +export function getZoomViewport( + width: number, + height: number, + zoomLevel: number +): Rect { + const viewport: Rect = { x: 0, y: 0, w: 0, h: 0 }; + + viewport.w = width * zoomLevel; + viewport.h = height * zoomLevel; + viewport.x = (width - viewport.w) / 2; + viewport.y = (height - viewport.h) / 2; + + return viewport; +} + +export const getAbsolutePosition = ( + overlapPosition: PointToPoint, + zoomViewport: Rect, + zoomLevel: number +): PointToPoint => ({ + x1: (overlapPosition.x1 - zoomViewport.x) / zoomLevel, + y1: (overlapPosition.y1 - zoomViewport.y) / zoomLevel, + x2: overlapPosition.x2 / zoomLevel, + y2: overlapPosition.y2 / zoomLevel, +});