refactor(minimap): move minimap to separate package

Signed-off-by: braks <78412429+bcakmakoglu@users.noreply.github.com>
This commit is contained in:
braks
2022-12-13 14:17:40 +01:00
committed by Braks
parent e44b683b74
commit 281279044c
13 changed files with 594 additions and 10 deletions

View File

@@ -104,16 +104,6 @@
border-radius: 100%;
}
.vue-flow__minimap {
background-color: #fff;
}
.vue-flow__minimap-mask {
&.pannable {
cursor: grab;
}
}
.vue-flow__controls {
box-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.08);

View File

@@ -0,0 +1,8 @@
module.exports = {
rules: {
'no-use-before-define': 0,
'vue/no-setup-props-destructure': 0,
},
extends: ['../../.eslintrc.js'],
ignorePatterns: ['!**/*'],
}

View File

@@ -0,0 +1,39 @@
# Vue Flow: MiniMap
This is a minimap component for Vue Flow.
It can be used to add a minimap to the canvas, which will show a smaller version of the canvas with your nodes.
The minimap can also be used to pan and zoom the main canvas.
## 🛠 Setup
```bash
# install
$ yarn add @vue-flow/minimap
# or
$ npm i --save @vue-flow/minimap
```
## 🎮 Quickstart
```vue
<script setup>
import { VueFlow } from '@vue-flow/core'
import { MiniMap } from '@vue-flow/minimap'
// import minimap styles
import '@vue-flow/minimap/dist/style.css'
import initialElements from './initial-elements'
// some nodes and edges
const elements = ref(initialElements)
</script>
<template>
<VueFlow v-model="elements" fit-view-on-init class="vue-flow-basic-example">
<MiniMap />
</VueFlow>
</template>
```

View File

@@ -0,0 +1,53 @@
{
"name": "@vue-flow/minimap",
"version": "0.0.1",
"private": false,
"license": "MIT",
"author": "Burak Cakmakoglu<78412429+bcakmakoglu@users.noreply.github.com>",
"repository": {
"type": "git",
"url": "git+https://github.com/bcakmakoglu/vue-flow/packages/plugins/minimap"
},
"homepage": "https://github.com/bcakmakoglu/vue-flow#readme",
"bugs": {
"url": "https://github.com/bcakmakoglu/vue-flow/issues"
},
"main": "./dist/vue-flow-plugin-minimap.js",
"module": "./dist/vue-flow-minimap.mjs",
"types": "./dist/index.d.ts",
"unpkg": "./dist/vue-flow-minimap.iife.js",
"jsdelivr": "./dist/vue-flow-minimap.iife.js",
"files": [
"dist"
],
"sideEffects": false,
"scripts": {
"build": "vite build",
"types": "vue-tsc --declaration --emitDeclarationOnly && pnpm lint:dist",
"lint": "eslint --ext \".js,.jsx,.ts,.tsx\" --fix --ignore-path ../../.gitignore .",
"lint:dist": "eslint --ext \".ts,.tsx\" -c .eslintrc.js --fix ./dist",
"test": "exit 0"
},
"peerDependencies": {
"@vue-flow/core": "^1.0.0",
"vue": "^3.2.37"
},
"dependencies": {
"d3-selection": "^3.0.0",
"d3-zoom": "^3.0.0"
},
"devDependencies": {
"@types/d3-selection": "^3.0.3",
"@types/d3-zoom": "^3.0.1",
"@vue-flow/core": "workspace:*",
"@vitejs/plugin-vue": "^3.2.0",
"unplugin-auto-import": "^0.12.0",
"vite": "^3.2.5",
"vite-plugin-vue-type-imports": "0.2.0",
"vue-tsc": "^1.0.11"
},
"publishConfig": {
"access": "public",
"registry": "https://registry.npmjs.org/"
}
}

View File

@@ -0,0 +1,242 @@
<script lang="ts" setup>
import type { CoordinateExtent, GraphNode } from '@vue-flow/core'
import { getBoundsofRects, getConnectedEdges, getRectOfNodes, useVueFlow } from '@vue-flow/core'
import { zoom, zoomIdentity } from 'd3-zoom'
import type { D3ZoomEvent } from 'd3-zoom'
import { pointer, select } from 'd3-selection'
import type { PanelPosition } from '../panel'
import { Panel } from '../panel'
import type { MiniMapNodeFunc, MiniMapProps, ShapeRendering } from './types'
import MiniMapNode from './MiniMapNode'
import { MiniMapSlots } from './types'
const {
width,
height,
nodeStrokeColor = 'transparent',
nodeColor = '#e2e2e2',
nodeClassName,
nodeBorderRadius = 5,
nodeStrokeWidth = 2,
maskColor = 'rgb(240, 240, 240, 0.6)',
position = 'bottom-right' as PanelPosition,
maskStrokeColor = 'none',
maskStrokeWidth = 1,
pannable = false,
zoomable = false,
} = defineProps<MiniMapProps>()
const emit = defineEmits(['click', 'nodeClick', 'nodeDblclick', 'nodeMouseenter', 'nodeMousemove', 'nodeMouseleave'])
const attrs: Record<string, any> = useAttrs()
const defaultWidth = 200
const defaultHeight = 150
const { id, edges, viewport, translateExtent, dimensions, emits, nodes, d3Selection, d3Zoom } = useVueFlow()
const el = ref<SVGElement>()
provide(MiniMapSlots, useSlots())
const elementWidth = computed(() => width ?? attrs.style?.width ?? defaultWidth)
const elementHeight = computed(() => height ?? attrs.style?.height ?? defaultHeight)
const shapeRendering: ShapeRendering = typeof window === 'undefined' || !!window.chrome ? 'crispEdges' : 'geometricPrecision'
const nodeColorFunc = computed<MiniMapNodeFunc>(() => (nodeColor instanceof Function ? nodeColor : () => nodeColor as string))
const nodeStrokeColorFunc = computed<MiniMapNodeFunc>(() =>
nodeStrokeColor instanceof Function ? nodeStrokeColor : () => nodeStrokeColor as string,
)
const nodeClassNameFunc = computed<MiniMapNodeFunc>(() =>
nodeClassName instanceof Function ? nodeClassName : ((() => nodeClassName) as MiniMapNodeFunc),
)
const bb = computed(() => getRectOfNodes(nodes.value))
const viewBB = computed(() => ({
x: -viewport.value.x / viewport.value.zoom,
y: -viewport.value.y / viewport.value.zoom,
width: dimensions.value.width / viewport.value.zoom,
height: dimensions.value.height / viewport.value.zoom,
}))
const boundingRect = computed(() => (nodes.value && nodes.value.length ? getBoundsofRects(bb.value, viewBB.value) : viewBB.value))
const viewScale = computed(() => {
const scaledWidth = boundingRect.value.width / elementWidth.value
const scaledHeight = boundingRect.value.height / elementHeight.value
return Math.max(scaledWidth, scaledHeight)
})
const viewBox = computed(() => {
const viewWidth = viewScale.value * elementWidth.value
const viewHeight = viewScale.value * elementHeight.value
const offset = 5 * viewScale.value
return {
offset,
x: boundingRect.value.x - (viewWidth - boundingRect.value.width) / 2 - offset,
y: boundingRect.value.y - (viewHeight - boundingRect.value.height) / 2 - offset,
width: viewWidth + offset * 2,
height: viewHeight + offset * 2,
}
})
const d = computed(() => {
if (!viewBox.value.x || !viewBox.value.y) return ''
return `
M${viewBox.value.x - viewBox.value.offset},${viewBox.value.y - viewBox.value.offset}
h${viewBox.value.width + viewBox.value.offset * 2}
v${viewBox.value.height + viewBox.value.offset * 2}
h${-viewBox.value.width - viewBox.value.offset * 2}z
M${viewBB.value.x},${viewBB.value.y}
h${viewBB.value.width}
v${viewBB.value.height}
h${-viewBB.value.width}z`
})
watchEffect(
(onCleanup) => {
if (el.value) {
const selection = select(el.value as Element)
const zoomHandler = (event: D3ZoomEvent<SVGSVGElement, any>) => {
if (event.sourceEvent.type !== 'wheel' || !d3Selection.value || !d3Zoom.value) {
return
}
const pinchDelta =
-event.sourceEvent.deltaY * (event.sourceEvent.deltaMode === 1 ? 0.05 : event.sourceEvent.deltaMode ? 1 : 0.002) * 10
const zoom = viewport.value.zoom * 2 ** pinchDelta
d3Zoom.value.scaleTo(d3Selection.value, zoom)
}
const panHandler = (event: D3ZoomEvent<HTMLDivElement, any>) => {
if (event.sourceEvent.type !== 'mousemove' || !d3Selection.value || !d3Zoom.value) {
return
}
const position = {
x: viewport.value.x - event.sourceEvent.movementX * viewScale.value * Math.max(1, viewport.value.zoom),
y: viewport.value.y - event.sourceEvent.movementY * viewScale.value * Math.max(1, viewport.value.zoom),
}
const extent: CoordinateExtent = [
[0, 0],
[dimensions.value.width, dimensions.value.height],
]
const nextTransform = zoomIdentity.translate(position.x, position.y).scale(viewport.value.zoom)
const constrainedTransform = d3Zoom.value.constrain()(nextTransform, extent, translateExtent.value)
d3Zoom.value.transform(d3Selection.value, constrainedTransform)
}
const zoomAndPanHandler = zoom()
.on('zoom', pannable ? panHandler : () => {})
.on('zoom.wheel', zoomable ? zoomHandler : () => {})
selection.call(zoomAndPanHandler)
onCleanup(() => {
selection.on('zoom', null)
})
}
},
{ flush: 'post' },
)
const onSvgClick = (event: MouseEvent) => {
const [x, y] = pointer(event)
emit('click', { event, position: { x, y } })
}
const onNodeClick = (event: MouseEvent, node: GraphNode) => {
const param = { event, node, connectedEdges: getConnectedEdges([node], edges.value) }
emits.miniMapNodeClick(param)
emit('nodeClick', param)
}
const onNodeDblClick = (event: MouseEvent, node: GraphNode) => {
const param = { event, node, connectedEdges: getConnectedEdges([node], edges.value) }
emits.miniMapNodeDoubleClick(param)
emit('nodeDblclick', param)
}
const onNodeMouseEnter = (event: MouseEvent, node: GraphNode) => {
const param = { event, node, connectedEdges: getConnectedEdges([node], edges.value) }
emits.miniMapNodeMouseEnter(param)
emit('nodeMouseenter', param)
}
const onNodeMouseMove = (event: MouseEvent, node: GraphNode) => {
const param = { event, node, connectedEdges: getConnectedEdges([node], edges.value) }
emits.miniMapNodeMouseMove(param)
emit('nodeMousemove', param)
}
const onNodeMouseLeave = (event: MouseEvent, node: GraphNode) => {
const param = { event, node, connectedEdges: getConnectedEdges([node], edges.value) }
emits.miniMapNodeMouseLeave(param)
emit('nodeMouseleave', param)
}
</script>
<script lang="ts">
export default {
name: 'MiniMap',
}
</script>
<template>
<Panel :position="position" class="vue-flow__minimap" :class="{ pannable, zoomable }">
<svg
ref="el"
:width="elementWidth"
:height="elementHeight"
:viewBox="[viewBox.x, viewBox.y, viewBox.width, viewBox.height].join(' ')"
role="img"
:aria-labelledby="`vue-flow__minimap-${id}`"
@click="onSvgClick"
>
<title :id="`vue-flow__minimap-${id}`">Vue Flow mini map {{ id }}</title>
<MiniMapNode
v-for="node of nodes"
:id="node.id"
:key="node.id"
:position="node.computedPosition"
:dimensions="node.dimensions"
:style="node.style"
:class="nodeClassNameFunc(node)"
:color="nodeColorFunc(node)"
:border-radius="nodeBorderRadius"
:stroke-color="nodeStrokeColorFunc(node)"
:stroke-width="nodeStrokeWidth"
:shape-rendering="shapeRendering"
:type="node.type"
@click="onNodeClick($event, node)"
@dblclick="onNodeDblClick($event, node)"
@mouseenter="onNodeMouseEnter($event, node)"
@mousemove="onNodeMouseMove($event, node)"
@mouseleave="onNodeMouseLeave($event, node)"
/>
<path
class="vue-flow__minimap-mask"
:d="d"
:fill="maskColor"
:stroke="maskStrokeColor"
:stroke-width="maskStrokeWidth"
fill-rule="evenodd"
/>
</svg>
</Panel>
</template>

View File

@@ -0,0 +1,40 @@
import type { CSSProperties, Slots } from 'vue'
import type { MiniMapNodeProps } from './types'
import { MiniMapSlots } from './types'
export default defineComponent({
name: 'MiniMapNode',
props: ['id', 'position', 'dimensions', 'strokeWidth', 'strokeColor', 'borderRadius', 'color', 'shapeRendering', 'type'],
emits: ['click', 'dblclick', 'mouseenter', 'mousemove', 'mouseleave'],
setup(props: MiniMapNodeProps, { attrs, emit }) {
const miniMapSlots: Slots = inject(MiniMapSlots)!
return () => {
const style = (attrs.style ?? {}) as CSSProperties
const slot = miniMapSlots[`node-${props.type}`]
if (slot) return slot!(props)
return h('rect', {
id: props.id,
class: ['vue-flow__minimap-node', attrs.class].join(' '),
style,
x: props.position.x,
y: props.position.y,
rx: props.borderRadius,
ry: props.borderRadius,
width: props.dimensions.width,
height: props.dimensions.height,
fill: props.color || (style.background as string) || style.backgroundColor,
stroke: props.strokeColor,
strokeWidth: props.strokeWidth,
shapeRendering: props.shapeRendering,
onClick: (e: MouseEvent) => emit('click', e),
onDblClick: (e: MouseEvent) => emit('dblclick', e),
onMouseenter: (e: MouseEvent) => emit('mouseenter', e),
onMousemove: (e: MouseEvent) => emit('mousemove', e),
onMouseleave: (e: MouseEvent) => emit('mouseleave', e),
})
}
},
})

63
packages/minimap/src/auto-imports.d.ts vendored Normal file
View File

@@ -0,0 +1,63 @@
// Generated by 'unplugin-auto-import'
export {}
declare global {
const $$: typeof import('vue/macros')['$$']
const $: typeof import('vue/macros')['$']
const $computed: typeof import('vue/macros')['$computed']
const $customRef: typeof import('vue/macros')['$customRef']
const $ref: typeof import('vue/macros')['$ref']
const $shallowRef: typeof import('vue/macros')['$shallowRef']
const $toRef: typeof import('vue/macros')['$toRef']
const EffectScope: typeof import('vue')['EffectScope']
const computed: typeof import('vue')['computed']
const createApp: typeof import('vue')['createApp']
const customRef: typeof import('vue')['customRef']
const defineAsyncComponent: typeof import('vue')['defineAsyncComponent']
const defineComponent: typeof import('vue')['defineComponent']
const effectScope: typeof import('vue')['effectScope']
const getCurrentInstance: typeof import('vue')['getCurrentInstance']
const getCurrentScope: typeof import('vue')['getCurrentScope']
const h: typeof import('vue')['h']
const inject: typeof import('vue')['inject']
const isProxy: typeof import('vue')['isProxy']
const isReactive: typeof import('vue')['isReactive']
const isReadonly: typeof import('vue')['isReadonly']
const isRef: typeof import('vue')['isRef']
const markRaw: typeof import('vue')['markRaw']
const nextTick: typeof import('vue')['nextTick']
const onActivated: typeof import('vue')['onActivated']
const onBeforeMount: typeof import('vue')['onBeforeMount']
const onBeforeUnmount: typeof import('vue')['onBeforeUnmount']
const onBeforeUpdate: typeof import('vue')['onBeforeUpdate']
const onDeactivated: typeof import('vue')['onDeactivated']
const onErrorCaptured: typeof import('vue')['onErrorCaptured']
const onMounted: typeof import('vue')['onMounted']
const onRenderTracked: typeof import('vue')['onRenderTracked']
const onRenderTriggered: typeof import('vue')['onRenderTriggered']
const onScopeDispose: typeof import('vue')['onScopeDispose']
const onServerPrefetch: typeof import('vue')['onServerPrefetch']
const onUnmounted: typeof import('vue')['onUnmounted']
const onUpdated: typeof import('vue')['onUpdated']
const provide: typeof import('vue')['provide']
const reactive: typeof import('vue')['reactive']
const readonly: typeof import('vue')['readonly']
const ref: typeof import('vue')['ref']
const resolveComponent: typeof import('vue')['resolveComponent']
const resolveDirective: typeof import('vue')['resolveDirective']
const shallowReactive: typeof import('vue')['shallowReactive']
const shallowReadonly: typeof import('vue')['shallowReadonly']
const shallowRef: typeof import('vue')['shallowRef']
const toRaw: typeof import('vue')['toRaw']
const toRef: typeof import('vue')['toRef']
const toRefs: typeof import('vue')['toRefs']
const triggerRef: typeof import('vue')['triggerRef']
const unref: typeof import('vue')['unref']
const useAttrs: typeof import('vue')['useAttrs']
const useCssModule: typeof import('vue')['useCssModule']
const useCssVars: typeof import('vue')['useCssVars']
const useSlots: typeof import('vue')['useSlots']
const watch: typeof import('vue')['watch']
const watchEffect: typeof import('vue')['watchEffect']
const watchPostEffect: typeof import('vue')['watchPostEffect']
const watchSyncEffect: typeof import('vue')['watchSyncEffect']
}

View File

@@ -0,0 +1,5 @@
import './style.css'
export { default as MiniMap } from './MiniMap.vue'
export { default as MiniMapNode } from './MiniMapNode'
export * from './types'

View File

@@ -0,0 +1,7 @@
.vue-flow__minimap {
background-color: #fff;
}
.vue-flow__minimap-mask.pannable {
cursor: grab;
}

View File

@@ -0,0 +1,58 @@
import type { Dimensions, GraphNode, XYPosition } from '@vue-flow/core'
import type { CSSProperties, InjectionKey, Slots } from 'vue'
import type { PanelPosition } from '../panel'
/** expects a node and returns a color value */
export type MiniMapNodeFunc = (node: GraphNode) => string
// hack for vue-type imports
type MiniMapNodeFunc2 = (node: GraphNode) => string
type MiniMapNodeFunc3 = (node: GraphNode) => string
export type ShapeRendering = CSSProperties['shapeRendering']
export interface MiniMapProps {
/** Node color, can be either a string or a string func that receives the current node */
nodeColor?: string | MiniMapNodeFunc
/** Node stroke color, can be either a string or a string func that receives the current node */
nodeStrokeColor?: string | MiniMapNodeFunc2
/** Additional node class name, can be either a string or a string func that receives the current node */
nodeClassName?: string | MiniMapNodeFunc3
/** Node border radius */
nodeBorderRadius?: number
/** Node stroke width */
nodeStrokeWidth?: number
/** Background color of minimap mask */
maskColor?: string
/** Border color of minimap mask */
maskStrokeColor?: string
/** Border width of minimap mask */
maskStrokeWidth?: number
/** Position of the minimap {@link PanelPosition} */
position?: PanelPosition
/** Enable drag minimap to drag viewport */
pannable?: boolean
/** Enable zoom minimap to zoom viewport */
zoomable?: boolean
width?: number
height?: number
}
/** these props are passed to mini map node slots */
export interface MiniMapNodeProps {
id: string
type: string
parentNode?: string
selected?: boolean
dragging?: boolean
position: XYPosition
dimensions: Dimensions
borderRadius?: number
color?: string
shapeRendering?: ShapeRendering
strokeColor?: string
strokeWidth?: number
}
export const MiniMapSlots: InjectionKey<Slots> = Symbol('MiniMapSlots')

View File

@@ -0,0 +1,3 @@
declare interface Window {
chrome: any
}

View File

@@ -0,0 +1,31 @@
{
"compilerOptions": {
"baseUrl": ".",
"module": "ESNext",
"target": "es2017",
"lib": [
"DOM",
"ESNext"
],
"strict": true,
"esModuleInterop": true,
"incremental": false,
"skipLibCheck": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"noUnusedLocals": false,
"strictNullChecks": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationDir": "./dist",
"emitDeclarationOnly": true,
"types": [],
},
"include": [
"./src",
],
"exclude": [
"node_modules",
"dist"
]
}

View File

@@ -0,0 +1,45 @@
import { resolve } from 'path'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import vueTypes from 'vite-plugin-vue-type-imports'
// https://vitejs.dev/config/
export default defineConfig({
resolve: {
extensions: ['.ts', '.vue'],
},
build: {
emptyOutDir: false,
lib: {
formats: ['es', 'cjs', 'iife'],
entry: resolve(__dirname, 'src/index.ts'),
fileName: 'vue-flow-minimap',
name: 'vueFlowMiniMap',
},
rollupOptions: {
// make sure to externalize deps that shouldn't be bundled
// into your library
external: ['vue', '@vue-flow/core'],
output: {
dir: './dist',
// Provide global variables to use in the UMD build
// for externalized deps
globals: {
'vue': 'Vue',
'@vue-flow/core': 'VueFlow',
},
},
},
},
plugins: [
vue({
reactivityTransform: true,
}),
vueTypes(),
AutoImport({
imports: ['vue', 'vue/macros'],
dts: 'src/auto-imports.d.ts',
}),
],
})