feat(core): implement figma controls
Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
<script lang="ts" setup>
|
||||
import UserSelection from '../../components/UserSelection/UserSelection.vue'
|
||||
import NodesSelection from '../../components/NodesSelection/NodesSelection.vue'
|
||||
import { SelectionMode } from '../../types'
|
||||
import { getMousePosition } from './utils'
|
||||
|
||||
const { isSelecting } = defineProps<{ isSelecting: boolean }>()
|
||||
|
||||
const {
|
||||
id,
|
||||
vueFlowRef,
|
||||
getNodes,
|
||||
getEdges,
|
||||
viewport,
|
||||
emits,
|
||||
userSelectionActive,
|
||||
removeSelectedElements,
|
||||
panOnDrag,
|
||||
userSelectionRect,
|
||||
elementsSelectable,
|
||||
nodesSelectionActive,
|
||||
addSelectedElements,
|
||||
selectionMode,
|
||||
} = useVueFlow()
|
||||
|
||||
const container = ref<HTMLDivElement | null>(null)
|
||||
|
||||
const prevSelectedNodesCount = ref(0)
|
||||
|
||||
const prevSelectedEdgesCount = ref(0)
|
||||
|
||||
const containerBounds = ref<DOMRect>()
|
||||
|
||||
const hasActiveSelection = computed(() => elementsSelectable.value && (isSelecting || userSelectionActive.value))
|
||||
|
||||
const resetUserSelection = () => {
|
||||
userSelectionActive.value = false
|
||||
userSelectionRect.value = null
|
||||
|
||||
prevSelectedNodesCount.value = 0
|
||||
prevSelectedEdgesCount.value = 0
|
||||
}
|
||||
|
||||
function onClick(event: MouseEvent) {
|
||||
if (event.target !== container.value || hasActiveSelection.value) return
|
||||
|
||||
console.log('click')
|
||||
|
||||
emits.paneClick(event)
|
||||
|
||||
removeSelectedElements()
|
||||
|
||||
nodesSelectionActive.value = false
|
||||
}
|
||||
|
||||
function onContextMenu(event: MouseEvent) {
|
||||
if (event.target !== container.value) return
|
||||
|
||||
if (Array.isArray(panOnDrag.value) && panOnDrag.value?.includes(2)) {
|
||||
event.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
emits.paneContextMenu(event)
|
||||
}
|
||||
|
||||
function onWheel(event: WheelEvent) {
|
||||
if (event.target !== container.value) return
|
||||
|
||||
emits.paneScroll(event)
|
||||
}
|
||||
|
||||
function onMouseDown(event: MouseEvent) {
|
||||
containerBounds.value = vueFlowRef.value!.getBoundingClientRect()
|
||||
|
||||
if (
|
||||
!hasActiveSelection.value ||
|
||||
!elementsSelectable ||
|
||||
!isSelecting ||
|
||||
event.button !== 0 ||
|
||||
event.target !== container.value ||
|
||||
!containerBounds.value
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const { x, y } = getMousePosition(event, containerBounds.value)
|
||||
|
||||
removeSelectedElements()
|
||||
|
||||
userSelectionRect.value = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
startX: x,
|
||||
startY: y,
|
||||
x,
|
||||
y,
|
||||
}
|
||||
|
||||
userSelectionActive.value = true
|
||||
|
||||
emits.selectionStart(event)
|
||||
}
|
||||
|
||||
function onMouseMove(event: MouseEvent) {
|
||||
if (!isSelecting || !containerBounds.value || !userSelectionRect.value) return
|
||||
if (!hasActiveSelection.value) return emits.paneMouseMove(event)
|
||||
|
||||
if (!userSelectionActive.value) userSelectionActive.value = true
|
||||
if (nodesSelectionActive.value) nodesSelectionActive.value = false
|
||||
|
||||
const mousePos = getMousePosition(event, containerBounds.value)
|
||||
const startX = userSelectionRect.value.startX ?? 0
|
||||
const startY = userSelectionRect.value.startY ?? 0
|
||||
|
||||
const nextUserSelectRect = {
|
||||
...userSelectionRect.value,
|
||||
x: mousePos.x < startX ? mousePos.x : startX,
|
||||
y: mousePos.y < startY ? mousePos.y : startY,
|
||||
width: Math.abs(mousePos.x - startX),
|
||||
height: Math.abs(mousePos.y - startY),
|
||||
}
|
||||
|
||||
const selectedNodes = getNodesInside(
|
||||
getNodes.value,
|
||||
userSelectionRect.value,
|
||||
viewport.value,
|
||||
selectionMode.value === SelectionMode.Partial,
|
||||
)
|
||||
|
||||
const selectedEdges = getConnectedEdges(selectedNodes, getEdges.value)
|
||||
|
||||
prevSelectedNodesCount.value = selectedNodes.length
|
||||
prevSelectedEdgesCount.value = selectedEdges.length
|
||||
|
||||
userSelectionRect.value = nextUserSelectRect
|
||||
|
||||
addSelectedElements([...selectedNodes, ...selectedEdges])
|
||||
}
|
||||
|
||||
function onMouseUp(event: MouseEvent) {
|
||||
if (!hasActiveSelection.value) return
|
||||
|
||||
// We only want to trigger click functions when in selection mode if
|
||||
// the user did not move the mouse.
|
||||
if (!userSelectionActive.value && userSelectionRect.value && event.target === container.value) {
|
||||
onClick(event)
|
||||
}
|
||||
|
||||
nodesSelectionActive.value = prevSelectedNodesCount.value > 0
|
||||
|
||||
resetUserSelection()
|
||||
|
||||
emits.selectionEnd(event)
|
||||
}
|
||||
|
||||
function onMouseLeave(event: MouseEvent) {
|
||||
console.log('mouseleave')
|
||||
if (!hasActiveSelection.value) return emits.paneMouseLeave(event)
|
||||
|
||||
if (userSelectionActive.value) {
|
||||
nodesSelectionActive.value = prevSelectedNodesCount.value > 0
|
||||
emits.selectionEnd?.(event)
|
||||
}
|
||||
|
||||
resetUserSelection()
|
||||
}
|
||||
|
||||
function onMouseEnter(event: MouseEvent) {
|
||||
if (hasActiveSelection.value) return
|
||||
|
||||
emits.paneMouseEnter(event)
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'Pane',
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="container"
|
||||
:key="`pane-${id}`"
|
||||
class="vue-flow__pane vue-flow__container"
|
||||
:class="[{ selection: isSelecting }]"
|
||||
@click="onClick"
|
||||
@contextmenu="onContextMenu"
|
||||
@wheel="onWheel"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mousedown="onMouseDown"
|
||||
@mousemove="onMouseMove"
|
||||
@mouseup="onMouseUp"
|
||||
@mouseleave="onMouseLeave"
|
||||
>
|
||||
<slot />
|
||||
<UserSelection v-if="userSelectionActive && userSelectionRect" />
|
||||
<NodesSelection v-if="nodesSelectionActive" />
|
||||
</div>
|
||||
</template>
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -1,99 +0,0 @@
|
||||
<script lang="ts" setup>
|
||||
import type { GraphNode } from '../../types'
|
||||
import { NodesSelection, UserSelection } from '../../components'
|
||||
|
||||
const {
|
||||
id,
|
||||
deleteKeyCode,
|
||||
selectionKeyCode,
|
||||
multiSelectionKeyCode,
|
||||
emits,
|
||||
nodesSelectionActive,
|
||||
userSelectionActive,
|
||||
multiSelectionActive,
|
||||
elementsSelectable,
|
||||
getNodes,
|
||||
getSelectedEdges,
|
||||
removeSelectedElements,
|
||||
removeNodes,
|
||||
removeEdges,
|
||||
} = useVueFlow()
|
||||
|
||||
const onClick = (event: MouseEvent) => {
|
||||
emits.paneClick(event)
|
||||
nodesSelectionActive.value = false
|
||||
|
||||
removeSelectedElements()
|
||||
}
|
||||
|
||||
const onContextMenu = (event: MouseEvent) => emits.paneContextMenu(event)
|
||||
|
||||
const onWheel = (event: WheelEvent) => emits.paneScroll(event)
|
||||
|
||||
const onMouseEnter = (event: MouseEvent) => emits.paneMouseEnter(event)
|
||||
|
||||
const onMouseLeave = (event: MouseEvent) => emits.paneMouseLeave(event)
|
||||
|
||||
const onMouseMove = (event: MouseEvent) => emits.paneMouseMove(event)
|
||||
|
||||
useKeyPress(deleteKeyCode, (keyPressed) => {
|
||||
if (!keyPressed) return
|
||||
|
||||
const nodesToRemove = getNodes.value.reduce<GraphNode[]>((res, node) => {
|
||||
if (!node.selected && node.parentNode && res.find((n) => n.id === node.parentNode)) {
|
||||
res.push(node)
|
||||
} else if (node.selected) {
|
||||
res.push(node)
|
||||
}
|
||||
|
||||
return res
|
||||
}, [])
|
||||
|
||||
if (nodesToRemove || getSelectedEdges.value) {
|
||||
if (getSelectedEdges.value.length > 0) {
|
||||
removeEdges(getSelectedEdges.value)
|
||||
}
|
||||
|
||||
if (nodesToRemove.length > 0) {
|
||||
removeNodes(nodesToRemove)
|
||||
}
|
||||
|
||||
nodesSelectionActive.value = false
|
||||
|
||||
removeSelectedElements()
|
||||
}
|
||||
})
|
||||
|
||||
useKeyPress(multiSelectionKeyCode, (keyPressed) => {
|
||||
multiSelectionActive.value = keyPressed
|
||||
})
|
||||
|
||||
const selectionKeyPressed = useKeyPress(selectionKeyCode, (keyPressed) => {
|
||||
if (userSelectionActive.value && keyPressed) return
|
||||
|
||||
userSelectionActive.value = keyPressed && elementsSelectable.value
|
||||
})
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
export default {
|
||||
name: 'SelectionPane',
|
||||
inheritAttrs: false,
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UserSelection v-if="selectionKeyPressed && userSelectionActive" />
|
||||
<NodesSelection v-if="nodesSelectionActive" />
|
||||
<div
|
||||
:key="`pane-${id}`"
|
||||
v-bind="$attrs"
|
||||
class="vue-flow__pane vue-flow__container"
|
||||
@click="onClick"
|
||||
@contextmenu="onContextMenu"
|
||||
@wheel="onWheel"
|
||||
@mouseenter="onMouseEnter"
|
||||
@mousemove="onMouseMove"
|
||||
@mouseleave="onMouseLeave"
|
||||
/>
|
||||
</template>
|
||||
@@ -56,6 +56,7 @@ export default {
|
||||
<div class="vue-flow__edge-labels" />
|
||||
|
||||
<NodeRenderer />
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -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<HTMLDivElement>('viewport', null)
|
||||
const viewportEl = ref<HTMLDivElement>()
|
||||
|
||||
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<HTMLDivElement, any>().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<HTMLDivElement, any>) => {
|
||||
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
|
||||
})
|
||||
})
|
||||
</script>
|
||||
@@ -243,10 +286,13 @@ export default {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="viewport" :key="`viewport-${id}`" class="vue-flow__viewport vue-flow__container">
|
||||
<Transform>
|
||||
<slot />
|
||||
</Transform>
|
||||
<SelectionPane :class="{ connecting: isConnecting, dragging: isDragging, draggable: panOnDrag }" />
|
||||
<div ref="viewportEl" :key="`viewport-${id}`" class="vue-flow__viewport vue-flow__container">
|
||||
<Pane :is-selecting="isSelecting" :class="{ connecting: isConnecting, dragging: paneDragging, draggable: !!panOnDrag }">
|
||||
<Transform>
|
||||
<slot name="zoom-pane" />
|
||||
</Transform>
|
||||
</Pane>
|
||||
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -42,6 +42,7 @@ const props = withDefaults(defineProps<FlowProps>(), {
|
||||
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 {
|
||||
<slot name="connection-line" />
|
||||
</template>
|
||||
|
||||
<slot name="zoom-pane" />
|
||||
</Viewport>
|
||||
<template #zoom-pane>
|
||||
<slot name="zoom-pane" />
|
||||
</template>
|
||||
|
||||
<slot />
|
||||
<slot />
|
||||
</Viewport>
|
||||
|
||||
<A11yDescriptions />
|
||||
</div>
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user