From fd79ab141debba5def5e30e1a04df62c958cf5d9 Mon Sep 17 00:00:00 2001 From: braks <78412429+bcakmakoglu@users.noreply.github.com> Date: Sun, 25 Dec 2022 15:00:07 +0100 Subject: [PATCH] feat(core): implement figma controls Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com> --- .../UserSelection/SelectionRect.vue | 25 --- .../UserSelection/UserSelection.vue | 105 +-------- .../src/components/UserSelection/utils.ts | 13 -- packages/core/src/composables/useDrag.ts | 12 +- packages/core/src/container/Pane/Pane.vue | 201 ++++++++++++++++++ packages/core/src/container/Pane/utils.ts | 8 + .../container/SelectionPane/SelectionPane.vue | 99 --------- .../core/src/container/Viewport/Transform.vue | 1 + .../core/src/container/Viewport/Viewport.vue | 94 +++++--- .../core/src/container/VueFlow/VueFlow.vue | 14 +- packages/core/src/container/index.ts | 2 +- packages/core/src/store/hooks.ts | 5 + packages/core/src/store/state.ts | 8 +- packages/core/src/style.css | 8 +- packages/core/src/types/flow.ts | 15 +- packages/core/src/types/hooks.ts | 42 ++-- packages/core/src/types/node.ts | 2 +- packages/core/src/types/store.ts | 23 +- packages/core/src/types/zoom.ts | 7 - packages/core/src/utils/edge.ts | 4 +- packages/core/src/utils/graph.ts | 10 +- 21 files changed, 381 insertions(+), 317 deletions(-) delete mode 100644 packages/core/src/components/UserSelection/SelectionRect.vue delete mode 100644 packages/core/src/components/UserSelection/utils.ts create mode 100644 packages/core/src/container/Pane/Pane.vue create mode 100644 packages/core/src/container/Pane/utils.ts delete mode 100644 packages/core/src/container/SelectionPane/SelectionPane.vue diff --git a/packages/core/src/components/UserSelection/SelectionRect.vue b/packages/core/src/components/UserSelection/SelectionRect.vue deleted file mode 100644 index 1097dd54..00000000 --- a/packages/core/src/components/UserSelection/SelectionRect.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/packages/core/src/components/UserSelection/UserSelection.vue b/packages/core/src/components/UserSelection/UserSelection.vue index 75596822..38bb53bc 100644 --- a/packages/core/src/components/UserSelection/UserSelection.vue +++ b/packages/core/src/components/UserSelection/UserSelection.vue @@ -1,103 +1,14 @@ - - diff --git a/packages/core/src/components/UserSelection/utils.ts b/packages/core/src/components/UserSelection/utils.ts deleted file mode 100644 index ee2f5784..00000000 --- a/packages/core/src/components/UserSelection/utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { XYPosition } from '../../types' - -export function getMousePosition(event: MouseEvent): XYPosition | void { - const flowNode = (event.target as Element).closest('.vue-flow') - if (!flowNode) return - - const containerBounds = flowNode.getBoundingClientRect() - - return { - x: event.clientX - containerBounds.left, - y: event.clientY - containerBounds.top, - } -} diff --git a/packages/core/src/composables/useDrag.ts b/packages/core/src/composables/useDrag.ts index cd53c4fb..53c4e4da 100644 --- a/packages/core/src/composables/useDrag.ts +++ b/packages/core/src/composables/useDrag.ts @@ -3,7 +3,7 @@ import { drag } from 'd3-drag' import { select } from 'd3-selection' import type { Ref } from 'vue' import type { MaybeRef } from '@vueuse/core' -import type { NodeDragEvent, NodeDragItem, SnapGrid, XYPosition } from '~/types' +import type { NodeDragEvent, NodeDragItem, XYPosition } from '~/types' export type UseDragEvent = D3DragEvent @@ -22,7 +22,7 @@ function useDrag(params: UseDragParams) { const dragging = scope.run(() => { const { snapToGrid, - snapGrid: globalSnapGrid, + snapGrid, noDragClassName, nodes, nodeExtent, @@ -42,8 +42,6 @@ function useDrag(params: UseDragParams) { let lastPos = $ref>({ x: undefined, y: undefined }) let dragHandler = $ref() - const hasSnapGrid = (sg?: SnapGrid) => (sg ?? snapToGrid ? globalSnapGrid : undefined) - const getPointerPosition = useGetPointerPosition() watch([() => disabled, () => el], () => { @@ -66,7 +64,7 @@ function useDrag(params: UseDragParams) { handleNodeClick(node, multiSelectionActive, addSelectedNodes, removeSelectedElements, $$(nodesSelectionActive)) } - const mousePos = getPointerPosition(event, hasSnapGrid(node?.snapGrid) as SnapGrid) + const mousePos = getPointerPosition(event, snapToGrid ? snapGrid : undefined) dragItems = getDragItems(nodes, mousePos, getNode, id) if (onStart && dragItems) { @@ -79,9 +77,7 @@ function useDrag(params: UseDragParams) { } }) .on('drag', (event: UseDragEvent) => { - const snapGrid = hasSnapGrid(node?.snapGrid) as SnapGrid - - const mousePos = getPointerPosition(event, snapGrid) + const mousePos = getPointerPosition(event, snapToGrid ? snapGrid : undefined) let hasChange = false diff --git a/packages/core/src/container/Pane/Pane.vue b/packages/core/src/container/Pane/Pane.vue new file mode 100644 index 00000000..6b998b01 --- /dev/null +++ b/packages/core/src/container/Pane/Pane.vue @@ -0,0 +1,201 @@ + + + + + diff --git a/packages/core/src/container/Pane/utils.ts b/packages/core/src/container/Pane/utils.ts new file mode 100644 index 00000000..7466bc41 --- /dev/null +++ b/packages/core/src/container/Pane/utils.ts @@ -0,0 +1,8 @@ +import type { XYPosition } from '~/types' + +export function getMousePosition(event: MouseEvent, containerBounds: DOMRect): XYPosition { + return { + x: event.clientX - containerBounds.left, + y: event.clientY - containerBounds.top, + } +} diff --git a/packages/core/src/container/SelectionPane/SelectionPane.vue b/packages/core/src/container/SelectionPane/SelectionPane.vue deleted file mode 100644 index 5fdaeaca..00000000 --- a/packages/core/src/container/SelectionPane/SelectionPane.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - - - diff --git a/packages/core/src/container/Viewport/Transform.vue b/packages/core/src/container/Viewport/Transform.vue index e32966d2..e7a70dfa 100644 --- a/packages/core/src/container/Viewport/Transform.vue +++ b/packages/core/src/container/Viewport/Transform.vue @@ -56,6 +56,7 @@ export default {
+
diff --git a/packages/core/src/container/Viewport/Viewport.vue b/packages/core/src/container/Viewport/Viewport.vue index 3d66605f..d81b051e 100644 --- a/packages/core/src/container/Viewport/Viewport.vue +++ b/packages/core/src/container/Viewport/Viewport.vue @@ -2,9 +2,9 @@ import type { D3ZoomEvent, ZoomTransform } from 'd3-zoom' import { zoom, zoomIdentity } from 'd3-zoom' import { pointer, select } from 'd3-selection' -import type { CoordinateExtent, ViewportTransform } from '../../types' +import type { CoordinateExtent, FlowOptions, ViewportTransform } from '../../types' import { PanOnScrollMode } from '../../types' -import SelectionPane from '../SelectionPane/SelectionPane.vue' +import Pane from '../Pane/Pane.vue' import Transform from './Transform.vue' const { @@ -16,6 +16,7 @@ const { dimensions, zoomActivationKeyCode, selectionKeyCode, + panActivationKeyCode, panOnScroll, panOnScrollMode, panOnScrollSpeed, @@ -29,16 +30,30 @@ const { setState, emits, connectionStartHandle, + userSelectionActive, + paneDragging, + selectionOnDrag, } = $(useVueFlow()) -const viewportEl = templateRef('viewport', null) +const viewportEl = ref() + +let selectionKeyPressed = $ref(false) let isZoomingOrPanning = $ref(false) -let isDragging = $ref(false) +let zoomedWithRightMouseButton = $ref(false) + +const isRightClickPan = (pan: FlowOptions['panOnDrag'], usedButton: number) => + usedButton === 2 && Array.isArray(pan) && pan.includes(2) + +const panKeyPressed = useKeyPress(panActivationKeyCode) const isConnecting = $computed(() => !!connectionStartHandle) +const shouldPanOnDrag = computed(() => !selectionKeyPressed && panOnDrag && panKeyPressed.value) + +const isSelecting = computed(() => selectionKeyPressed || (selectionOnDrag && shouldPanOnDrag.value !== true)) + const viewChanged = (prevViewport: ViewportTransform, eventTransform: ZoomTransform): boolean => (prevViewport.x !== eventTransform.x && !isNaN(eventTransform.x)) || (prevViewport.y !== eventTransform.y && !isNaN(eventTransform.y)) || @@ -74,9 +89,10 @@ onMounted(() => { }) onMounted(() => { - const bbox = viewportEl.value.getBoundingClientRect() + const viewportElement = viewportEl.value! + const bbox = viewportElement.getBoundingClientRect() const d3Zoom = zoom().scaleExtent([minZoom, maxZoom]).translateExtent(translateExtent) - const d3Selection = select(viewportEl.value).call(d3Zoom) + const d3Selection = select(viewportElement).call(d3Zoom) const d3ZoomHandler = d3Selection.on('wheel.zoom') const updatedTransform = zoomIdentity @@ -96,24 +112,30 @@ onMounted(() => { d3Selection, d3ZoomHandler, viewport: { x: updatedTransform.x, y: updatedTransform.y, zoom: updatedTransform.k }, - viewportRef: viewportEl.value, + viewportRef: viewportElement, }) const onKeyPress = (keyPress: boolean) => { - if (keyPress && !isZoomingOrPanning) { + selectionKeyPressed = keyPress + + if (keyPress && userSelectionActive && !isZoomingOrPanning) { d3Zoom.on('zoom', null) - } else if (!keyPress) { + } else if (!keyPress && !userSelectionActive) { d3Zoom.on('zoom', (event: D3ZoomEvent) => { setState({ viewport: { x: event.transform.x, y: event.transform.y, zoom: event.transform.k } }) const flowTransform = eventToFlowTransform(event.transform) + zoomedWithRightMouseButton = isRightClickPan(panOnDrag, event.sourceEvent?.button) + + emits.viewportChange(flowTransform) emits.move({ event, flowTransform }) }) } } - const selectionKeyPressed = useKeyPress(selectionKeyCode, onKeyPress) + useKeyPress(selectionKeyCode, onKeyPress) + // initialize onKeyPress(false) @@ -127,11 +149,12 @@ onMounted(() => { const flowTransform = eventToFlowTransform(event.transform) if (event.sourceEvent?.type === 'mousedown') { - isDragging = true + setState({ paneDragging: true }) } prevTransform = flowTransform + emits.viewportChangeStart(flowTransform) emits.moveStart({ event, flowTransform }) }) @@ -139,22 +162,30 @@ onMounted(() => { if (!event.sourceEvent) return null isZoomingOrPanning = false - isDragging = false + + setState({ paneDragging: false }) + + if (isRightClickPan(panOnDrag, event.sourceEvent?.button) && !zoomedWithRightMouseButton) { + emits.paneContextMenu(event.sourceEvent) + } + + zoomedWithRightMouseButton = false if (viewChanged(prevTransform, event.transform)) { const flowTransform = eventToFlowTransform(event.transform) prevTransform = flowTransform + emits.viewportChangeEnd(flowTransform) emits.moveEnd({ event, flowTransform }) } }) watchEffect(() => { - if (panOnScroll && !zoomKeyPressed.value) { + if (panOnScroll && !zoomKeyPressed.value && !userSelectionActive) { d3Selection .on('wheel', (event: WheelEvent) => { - if (isWrappedWithClass(event, noWheelClassName?.value)) { + if (isWrappedWithClass(event, noWheelClassName)) { return false } @@ -185,7 +216,7 @@ onMounted(() => { } else if (typeof d3ZoomHandler !== 'undefined') { d3Selection .on('wheel', (event: WheelEvent) => { - if (!preventScrolling || isWrappedWithClass(event, noWheelClassName?.value)) { + if (!preventScrolling || isWrappedWithClass(event, noWheelClassName)) { return null } @@ -211,16 +242,16 @@ onMounted(() => { if (!panOnDrag && !zoomScroll && !panOnScroll && !zoomOnDoubleClick && !zoomOnPinch) return false // during a selection we prevent all other interactions - if (selectionKeyPressed.value && selectionKeyCode !== true) return false + if (userSelectionActive) return false // if zoom on double click is disabled, we prevent the double click event if (!zoomOnDoubleClick && event.type === 'dblclick') return false // if the target element is inside an element with the nowheel class, we prevent zooming - if (isWrappedWithClass(event, noWheelClassName as any) && event.type === 'wheel') return false + if (isWrappedWithClass(event, noWheelClassName) && event.type === 'wheel') return false // if the target element is inside an element with the nopan class, we prevent panning - if (isWrappedWithClass(event, noPanClassName as any) && event.type !== 'wheel') return false + if (isWrappedWithClass(event, noPanClassName) && event.type !== 'wheel') return false if (!zoomOnPinch && event.ctrlKey && event.type === 'wheel') return false @@ -230,8 +261,20 @@ onMounted(() => { // if the pane is not movable, we prevent dragging it with mousestart or touchstart if (!panOnDrag && (event.type === 'mousedown' || event.type === 'touchstart')) return false + // if the pane is only movable using allowed clicks + if ( + Array.isArray(panOnDrag) && + !panOnDrag.includes(event.button) && + (event.type === 'mousedown' || event.type === 'touchstart') + ) { + return false + } + + // We only allow right clicks if pan on drag is set to right-click + const buttonAllowed = (Array.isArray(panOnDrag) && panOnDrag.includes(event.button)) || !event.button || event.button <= 1 + // default filter for d3-zoom - return (!event.ctrlKey || event.type === 'wheel') && (!event.button || event.button <= 1) + return (!event.ctrlKey || event.type === 'wheel') && buttonAllowed }) }) @@ -243,10 +286,13 @@ export default { diff --git a/packages/core/src/container/VueFlow/VueFlow.vue b/packages/core/src/container/VueFlow/VueFlow.vue index 4ae9084b..7400e244 100644 --- a/packages/core/src/container/VueFlow/VueFlow.vue +++ b/packages/core/src/container/VueFlow/VueFlow.vue @@ -42,6 +42,7 @@ const props = withDefaults(defineProps(), { disableKeyboardA11y: undefined, edgesFocusable: undefined, nodesFocusable: undefined, + selectionOnDrag: undefined, }) const emit = defineEmits<{ @@ -77,6 +78,11 @@ const emit = defineEmits<{ (event: 'selectionDrag', selectionEvent: NodeDragEvent): void (event: 'selectionDragStop', selectionEvent: NodeDragEvent): void (event: 'selectionContextMenu', selectionEvent: { event: MouseEvent; nodes: GraphNode[] }): void + (event: 'selectionStart', selectionEvent: MouseEvent): void + (event: 'selectionEnd', selectionEvent: MouseEvent): void + (event: 'viewportChangeStart', viewport: ViewportTransform): void + (event: 'viewportChange', viewport: ViewportTransform): void + (event: 'viewportChangeEnd', viewport: ViewportTransform): void (event: 'paneReady', paneEvent: VueFlowStore): void (event: 'paneScroll', paneEvent: WheelEvent | undefined): void (event: 'paneClick', paneEvent: MouseEvent): void @@ -167,10 +173,12 @@ export default { - - + - + + diff --git a/packages/core/src/container/index.ts b/packages/core/src/container/index.ts index c000fc78..8a86e667 100644 --- a/packages/core/src/container/index.ts +++ b/packages/core/src/container/index.ts @@ -4,4 +4,4 @@ export { default as Marker } from './EdgeRenderer/Marker.vue' export { default as MarkerDefinitions } from './EdgeRenderer/MarkerDefinitions.vue' export { default as NodeRenderer } from './NodeRenderer/NodeRenderer.vue' export { default as Viewport } from './Viewport/Viewport.vue' -export { default as SelectionPane } from './SelectionPane/SelectionPane.vue' +export { default as SelectionPane } from './Pane/SelectionPane.vue' diff --git a/packages/core/src/store/hooks.ts b/packages/core/src/store/hooks.ts index fc26d2bb..807afbc8 100644 --- a/packages/core/src/store/hooks.ts +++ b/packages/core/src/store/hooks.ts @@ -31,6 +31,11 @@ export const createHooks = (): FlowHooks => ({ selectionDrag: createEventHook(), selectionDragStop: createEventHook(), selectionContextMenu: createEventHook(), + selectionStart: createEventHook(), + selectionEnd: createEventHook(), + viewportChangeStart: createEventHook(), + viewportChange: createEventHook(), + viewportChangeEnd: createEventHook(), paneScroll: createEventHook(), paneClick: createEventHook(), paneContextMenu: createEventHook(), diff --git a/packages/core/src/store/state.ts b/packages/core/src/store/state.ts index 9e6c6507..7fb5f114 100644 --- a/packages/core/src/store/state.ts +++ b/packages/core/src/store/state.ts @@ -1,5 +1,5 @@ import type { DefaultEdgeTypes, DefaultNodeTypes, FlowOptions, State } from '~/types' -import { ConnectionLineType, ConnectionMode, PanOnScrollMode } from '~/types' +import { ConnectionLineType, ConnectionMode, PanOnScrollMode, SelectionMode } from '~/types' import { BezierEdge, DefaultNode, @@ -56,6 +56,8 @@ const defaultState = (): State => ({ [Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY], ], + selectionMode: SelectionMode.Full, + paneDragging: false, preventScrolling: true, zoomOnScroll: true, zoomOnPinch: true, @@ -71,6 +73,8 @@ const defaultState = (): State => ({ nodesSelectionActive: false, userSelectionActive: false, + userSelectionRect: null, + defaultMarkerColor: '#b1b1b7', connectionLineStyle: {}, connectionLineType: ConnectionLineType.Bezier, @@ -94,11 +98,13 @@ const defaultState = (): State => ({ nodesDraggable: true, elementsSelectable: true, selectNodesOnDrag: true, + selectionOnDrag: false, multiSelectionActive: false, selectionKeyCode: 'Shift', multiSelectionKeyCode: 'Meta', zoomActivationKeyCode: 'Meta', deleteKeyCode: 'Backspace', + panActivationKeyCode: 'Space', hooks: createHooks(), diff --git a/packages/core/src/style.css b/packages/core/src/style.css index 1cdfa51a..494752a0 100644 --- a/packages/core/src/style.css +++ b/packages/core/src/style.css @@ -24,6 +24,10 @@ &.dragging { cursor: grabbing; } + + &.selection { + cursor: pointer; + } } .vue-flow__transformationpane { @@ -36,8 +40,8 @@ z-index: 4; } -.vue-flow__selectionpane { - z-index: 5; +.vue-flow__selection { + z-index: 6; } .vue-flow__edge-labels { diff --git a/packages/core/src/types/flow.ts b/packages/core/src/types/flow.ts index e7b7da03..84707de6 100644 --- a/packages/core/src/types/flow.ts +++ b/packages/core/src/types/flow.ts @@ -3,7 +3,7 @@ import type { KeyFilter } from '@vueuse/core' import type { DefaultEdgeOptions, Edge, EdgeUpdatable, GraphEdge } from './edge' import type { CoordinateExtent, GraphNode, Node } from './node' import type { ConnectionLineOptions, ConnectionLineType, ConnectionMode, Connector } from './connection' -import type { PanOnScrollMode, Viewport } from './zoom' +import type { PanOnScrollMode, ViewportTransform } from './zoom' import type { EdgeTypesObject, NodeTypesObject } from './components' import type { CustomEvent } from './hooks' @@ -88,7 +88,11 @@ export type SnapGrid = [number, number] export interface SelectionRect extends Rect { startX: number startY: number - draw: boolean +} + +export enum SelectionMode { + Partial = 'partial', + Full = 'full', } export interface FlowExportObject { @@ -117,6 +121,7 @@ export interface FlowProps { selectionKeyCode?: KeyFilter | null multiSelectionKeyCode?: KeyFilter | null zoomActivationKeyCode?: KeyFilter | null + panActivationKeyCode?: KeyFilter | null snapToGrid?: boolean snapGrid?: SnapGrid onlyRenderVisibleElements?: boolean @@ -126,10 +131,10 @@ export interface FlowProps { elementsSelectable?: boolean selectNodesOnDrag?: boolean /** move pane on drag, replaced prop `paneMovable` */ - panOnDrag?: boolean + panOnDrag?: boolean | number[] minZoom?: number maxZoom?: number - defaultViewport?: Viewport + defaultViewport?: ViewportTransform translateExtent?: CoordinateExtent nodeExtent?: CoordinateExtent defaultMarkerColor?: string @@ -141,6 +146,8 @@ export interface FlowProps { zoomOnDoubleClick?: boolean /** enable this to prevent vue flow from scrolling inside the container, i.e. allow for the page to scroll */ preventScrolling?: boolean + selectionOnDrag?: boolean + selectionMode?: SelectionMode edgeUpdaterRadius?: number fitViewOnInit?: boolean /** allow connection with click handlers, i.e. support touch devices */ diff --git a/packages/core/src/types/hooks.ts b/packages/core/src/types/hooks.ts index 915dd970..d0bba340 100644 --- a/packages/core/src/types/hooks.ts +++ b/packages/core/src/types/hooks.ts @@ -65,6 +65,11 @@ export interface FlowEvents { selectionDrag: NodeDragEvent selectionDragStop: NodeDragEvent selectionContextMenu: { event: MouseEvent; nodes: GraphNode[] } + selectionStart: MouseEvent + selectionEnd: MouseEvent + viewportChangeStart: ViewportTransform + viewportChange: ViewportTransform + viewportChangeEnd: ViewportTransform paneScroll: WheelEvent | undefined paneClick: MouseEvent paneContextMenu: MouseEvent @@ -82,21 +87,6 @@ export interface FlowEvents { edgeUpdateEnd: EdgeMouseEvent } -export type FlowHooks = Readonly<{ - [key in keyof FlowEvents]: EventHook -}> - -export type FlowHooksOn = Readonly<{ - [key in keyof FlowEvents as `on${Capitalize}`]: EventHookOn -}> - -export type FlowHooksEmit = Readonly<{ - [key in keyof FlowEvents]: EventHookTrigger -}> - -/** - * Vue Flow component event emitter definitions - */ export interface Emits { (event: 'nodesChange', changes: NodeChange[]): void (event: 'edgesChange', changes: EdgeChange[]): void @@ -119,10 +109,10 @@ export interface Emits { ( event: 'connectStart', connectionEvent: { - event: MouseEvent + event?: MouseEvent } & OnConnectStartParams, ): void - (event: 'connectEnd', connectionEvent: MouseEvent): void + (event: 'connectEnd', connectionEvent?: MouseEvent): void (event: 'moveStart', moveEvent: { event: D3ZoomEvent; flowTransform: ViewportTransform }): void (event: 'move', moveEvent: { event: D3ZoomEvent; flowTransform: ViewportTransform }): void (event: 'moveEnd', moveEvent: { event: D3ZoomEvent; flowTransform: ViewportTransform }): void @@ -130,6 +120,11 @@ export interface Emits { (event: 'selectionDrag', selectionEvent: NodeDragEvent): void (event: 'selectionDragStop', selectionEvent: NodeDragEvent): void (event: 'selectionContextMenu', selectionEvent: { event: MouseEvent; nodes: GraphNode[] }): void + (event: 'selectionStart', selectionEvent: MouseEvent): void + (event: 'selectionEnd', selectionEvent: MouseEvent): void + (event: 'viewportChangeStart', viewport: ViewportTransform): void + (event: 'viewportChange', viewport: ViewportTransform): void + (event: 'viewportChangeEnd', viewport: ViewportTransform): void (event: 'paneReady', paneEvent: VueFlowStore): void (event: 'paneScroll', paneEvent: WheelEvent | undefined): void (event: 'paneClick', paneEvent: MouseEvent): void @@ -146,6 +141,7 @@ export interface Emits { (event: 'edgeUpdateStart', edgeMouseEvent: EdgeMouseEvent): void (event: 'edgeUpdate', edgeUpdateEvent: EdgeUpdateEvent): void (event: 'edgeUpdateEnd', edgeMouseEvent: EdgeMouseEvent): void + (event: 'updateNodeInternals'): void /** v-model event definitions */ (event: 'update:modelValue', value: FlowElements): void @@ -153,6 +149,18 @@ export interface Emits { (event: 'update:edges', value: GraphEdge[]): void } +export type FlowHooks = Readonly<{ + [key in keyof FlowEvents]: EventHook +}> + +export type FlowHooksOn = Readonly<{ + [key in keyof FlowEvents as `on${Capitalize}`]: EventHookOn +}> + +export type FlowHooksEmit = Readonly<{ + [key in keyof FlowEvents]: EventHookTrigger +}> + /** * To type `Args` (the event callback arguments) pass an array as argument list as first generic type * To type `Return` (the event callback return value) pass a value to the second generic type diff --git a/packages/core/src/types/node.ts b/packages/core/src/types/node.ts index 772b185b..93ddc202 100644 --- a/packages/core/src/types/node.ts +++ b/packages/core/src/types/node.ts @@ -1,5 +1,5 @@ import type { Component, VNode } from 'vue' -import type { ClassFunc, Dimensions, ElementData, Position, SnapGrid, StyleFunc, Styles, XYPosition, XYZPosition } from './flow' +import type { ClassFunc, Dimensions, ElementData, Position, StyleFunc, Styles, XYPosition, XYZPosition } from './flow' import type { DefaultNodeTypes, NodeComponent } from './components' import type { HandleConnectable, HandleElement, ValidConnectionFunc } from './handle' import type { CustomEvent, NodeEventsHandler, NodeEventsOn } from './hooks' diff --git a/packages/core/src/types/store.ts b/packages/core/src/types/store.ts index 7dfef020..80913085 100644 --- a/packages/core/src/types/store.ts +++ b/packages/core/src/types/store.ts @@ -8,6 +8,8 @@ import type { FlowExportObject, FlowOptions, Rect, + SelectionMode, + SelectionRect, SnapGrid, XYPosition, } from './flow' @@ -15,7 +17,7 @@ import type { DefaultEdgeTypes, DefaultNodeTypes, EdgeComponent, NodeComponent } import type { Connection, ConnectionLineOptions, ConnectionLineType, ConnectionMode, Connector } from './connection' import type { DefaultEdgeOptions, Edge, EdgeUpdatable, GraphEdge } from './edge' import type { CoordinateExtent, GraphNode, Node } from './node' -import type { D3Selection, D3Zoom, D3ZoomHandler, PanOnScrollMode, Viewport, ViewportFunctions } from './zoom' +import type { D3Selection, D3Zoom, D3ZoomHandler, PanOnScrollMode, ViewportFunctions, ViewportTransform } from './zoom' import type { CustomEvent, FlowHooks, FlowHooksEmit, FlowHooksOn } from './hooks' import type { EdgeChange, NodeChange, NodeDragItem } from './changes' import type { StartHandle } from './handle' @@ -48,7 +50,7 @@ export interface State extends Omit { minZoom: number /** use setMaxZoom action to change maxZoom */ maxZoom: number - defaultViewport: Viewport + defaultViewport: ViewportTransform /** use setTranslateExtent action to change translateExtent */ translateExtent: CoordinateExtent nodeExtent: CoordinateExtent @@ -56,7 +58,7 @@ export interface State extends Omit { /** viewport dimensions - do not change! */ readonly dimensions: Dimensions /** viewport transform x, y, z - do not change! */ - readonly viewport: Viewport + readonly viewport: ViewportTransform /** if true will skip rendering any elements currently not inside viewport until they become visible */ onlyRenderVisibleElements: boolean nodesSelectionActive: boolean @@ -67,6 +69,7 @@ export interface State extends Omit { selectionKeyCode: KeyFilter | null multiSelectionKeyCode: KeyFilter | null zoomActivationKeyCode: KeyFilter | null + panActivationKeyCode: KeyFilter | null connectionMode: ConnectionMode connectionLineOptions: ConnectionLineOptions @@ -95,7 +98,10 @@ export interface State extends Omit { elementsSelectable: boolean selectNodesOnDrag: boolean - panOnDrag: boolean + userSelectionRect: SelectionRect | null + selectionOnDrag: boolean + selectionMode: SelectionMode + panOnDrag: boolean | number[] zoomOnScroll: boolean zoomOnPinch: boolean panOnScroll: boolean @@ -103,16 +109,17 @@ export interface State extends Omit { panOnScrollMode: PanOnScrollMode zoomOnDoubleClick: boolean preventScrolling: boolean + paneDragging: boolean initialized: boolean applyDefault: boolean autoConnect: boolean | Connector - fitViewOnInit?: boolean + fitViewOnInit: boolean - noDragClassName?: 'nodrag' | string - noWheelClassName?: 'nowheel' | string - noPanClassName?: 'nopan' | string + noDragClassName: 'nodrag' | string + noWheelClassName: 'nowheel' | string + noPanClassName: 'nopan' | string defaultEdgeOptions?: DefaultEdgeOptions diff --git a/packages/core/src/types/zoom.ts b/packages/core/src/types/zoom.ts index a030b85f..8d09c9fc 100644 --- a/packages/core/src/types/zoom.ts +++ b/packages/core/src/types/zoom.ts @@ -5,13 +5,6 @@ export type D3Zoom = ZoomBehavior export type D3Selection = Selection export type D3ZoomHandler = (this: HTMLDivElement, event: any, d: unknown) => void -/** Transform x, y, z */ -export interface Viewport { - x: number - y: number - zoom: number -} - export enum PanOnScrollMode { Free = 'free', Vertical = 'vertical', diff --git a/packages/core/src/utils/edge.ts b/packages/core/src/utils/edge.ts index 4eb50d84..21f0e07e 100644 --- a/packages/core/src/utils/edge.ts +++ b/packages/core/src/utils/edge.ts @@ -1,4 +1,4 @@ -import type { EdgePositions, Getters, GraphEdge, GraphNode, HandleElement, Rect, Viewport, XYPosition } from '~/types' +import type { EdgePositions, Getters, GraphEdge, GraphNode, HandleElement, Rect, ViewportTransform, XYPosition } from '~/types' import { Position } from '~/types' export const getHandlePosition = (position: Position, rect: Rect, handle?: HandleElement): XYPosition => { @@ -83,7 +83,7 @@ interface IsEdgeVisibleParams { targetHeight: number width: number height: number - viewport: Viewport + viewport: ViewportTransform } export function isEdgeVisible({ diff --git a/packages/core/src/utils/graph.ts b/packages/core/src/utils/graph.ts index b8aaf09b..b7cfd127 100644 --- a/packages/core/src/utils/graph.ts +++ b/packages/core/src/utils/graph.ts @@ -15,7 +15,7 @@ import type { GraphNode, Node, Rect, - Viewport, + ViewportTransform, XYPosition, XYZPosition, } from '~/types' @@ -222,7 +222,7 @@ export const updateEdge = (oldEdge: Edge, newConnection: Connection, elements: E export const pointToRendererPoint = ( { x, y }: XYPosition, - { x: tx, y: ty, zoom: tScale }: Viewport, + { x: tx, y: ty, zoom: tScale }: ViewportTransform, snapToGrid: boolean, [snapX, snapY]: [number, number], ) => { @@ -280,7 +280,7 @@ export const getRectOfNodes = (nodes: GraphNode[]) => { return boxToRect(box) } -export const graphPosToZoomedPos = ({ x, y }: XYPosition, { x: tx, y: ty, zoom: tScale }: Viewport): XYPosition => ({ +export const graphPosToZoomedPos = ({ x, y }: XYPosition, { x: tx, y: ty, zoom: tScale }: ViewportTransform): XYPosition => ({ x: x * tScale + tx, y: y * tScale + ty, }) @@ -288,7 +288,7 @@ export const graphPosToZoomedPos = ({ x, y }: XYPosition, { x: tx, y: ty, zoom: export const getNodesInside = ( nodes: GraphNode[], rect: Rect, - { x: tx, y: ty, zoom: tScale }: Viewport = { x: 0, y: 0, zoom: 1 }, + { x: tx, y: ty, zoom: tScale }: ViewportTransform = { x: 0, y: 0, zoom: 1 }, partially = false, // set excludeNonSelectableNodes if you want to pay attention to the nodes "selectable" attribute excludeNonSelectableNodes = false, @@ -337,7 +337,7 @@ export const getTransformForBounds = ( x?: number y?: number } = { x: 0, y: 0 }, -): Viewport => { +): ViewportTransform => { const xZoom = width / (bounds.width * (1 + padding)) const yZoom = height / (bounds.height * (1 + padding)) const zoom = Math.min(xZoom, yZoom)