diff --git a/.changeset/config.json b/.changeset/config.json index 2df85f405..312fde6c2 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -6,7 +6,7 @@ "repo": "bcakmakoglu/vue-flow" } ], - "fixed": [], + "fixed": [["@vue-flow/*"]], "updateInternalDependencies": "patch", "snapshot": { "useCalculatedVersion": true diff --git a/.changeset/cool-coats-yawn.md b/.changeset/cool-coats-yawn.md new file mode 100644 index 000000000..ce54313ec --- /dev/null +++ b/.changeset/cool-coats-yawn.md @@ -0,0 +1,5 @@ +--- +'@vue-flow/plugin-drag-n-drop': major +--- + +Add drag and drop plugin pkg to help simplify implementing a dnd UI for vue flow diff --git a/.changeset/cyan-melons-unite.md b/.changeset/cyan-melons-unite.md new file mode 100644 index 000000000..b967de82a --- /dev/null +++ b/.changeset/cyan-melons-unite.md @@ -0,0 +1,5 @@ +--- +"@vue-flow/core": major +--- + +Remove experimental features flag diff --git a/.changeset/fluffy-pans-boil.md b/.changeset/fluffy-pans-boil.md new file mode 100644 index 000000000..7357194ab --- /dev/null +++ b/.changeset/fluffy-pans-boil.md @@ -0,0 +1,5 @@ +--- +'@vue-flow/core': major +--- + +Use `GraphNode` or `GraphEdge` as optional generic types for `findNode` or `findEdge` actions diff --git a/.changeset/hip-melons-peel.md b/.changeset/hip-melons-peel.md new file mode 100644 index 000000000..80e55c5b8 --- /dev/null +++ b/.changeset/hip-melons-peel.md @@ -0,0 +1,5 @@ +--- +'@vue-flow/core': major +--- + +Update handle styles and avoid using fixed pixel positions to offset handle position and instead use transform to align handles diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 000000000..fac142e7f --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,27 @@ +{ + "mode": "pre", + "tag": "next", + "initialVersions": { + "@vue-flow/docs": "1.0.0", + "@vue-flow/examples-nuxt3": "0.0.0", + "@vue-flow/examples-quasar": "0.0.0", + "@vue-flow/examples-vite": "0.0.0", + "@vue-flow/background": "1.0.5", + "@vue-flow/controls": "1.0.4", + "@vue-flow/core": "1.16.5", + "@vue-flow/minimap": "1.0.4", + "@vue-flow/node-resizer": "1.3.0", + "@vue-flow/node-toolbar": "1.0.6", + "@vue-flow/pathfinding-edge": "1.0.5", + "vueflow": "1.0.0", + "@vue-flow/tests": "0.0.0", + "@tooling/eslint-config": "0.0.0", + "@tooling/tsconfig": "0.0.0", + "@tooling/vite-config": "0.0.0" + }, + "changesets": [ + "cyan-melons-unite.md", + "fluffy-pans-boil.md", + "hip-melons-peel.md" + ] +} diff --git a/.changeset/slimy-seas-check.md b/.changeset/slimy-seas-check.md new file mode 100644 index 000000000..9f3050230 --- /dev/null +++ b/.changeset/slimy-seas-check.md @@ -0,0 +1,5 @@ +--- +'@vue-flow/plugin-layout': major +--- + +Add dagre layout plugin pkg diff --git a/.changeset/weak-points-whisper.md b/.changeset/weak-points-whisper.md new file mode 100644 index 000000000..c8cdaf9d1 --- /dev/null +++ b/.changeset/weak-points-whisper.md @@ -0,0 +1,71 @@ +--- +'@vue-flow/core': minor + +--- + +Add plugin API to Vue Flow App + +## What's changed? + +- Allow setting global store config that is applied as default to all stores that are created +- A simple API to extend the vue flow store (⚠️ *NOT* a Vue Plugin) +- Provide hooks to hook into store lifecycle (`beforeCreate`, `created`, `beforeDestroy`, `destroyed`) +- Add some simple plugins to start off + - DnD + - Layout + - Screenshot + +## Plugin + +```ts +interface PluginHooks { + beforeCreate: EventHookOn<[string, FlowOptions | undefined]> + created: EventHookOn<[VueFlowStore, (plugin: Partial) => void]> + beforeDestroy: EventHookOn + destroyed: EventHookOn +} + +type Plugin = (hooks: PluginHooks) => void +``` + +## Example + +```ts +// main.ts or your App entry point +import { createVueFlow, Plugin } from '@vue-flow/core' + +const plugin: Plugin = (hooks) => { + hooks.beforeCreate(([id, preloadedState]) => { + // do something before a store instance is created + }) + + // after a store has been created + hooks.created(([storeInstance, extendFn]) => {}) + + // before a store is destroyed + hooks.beforeDestroy((storeInstance) => {}) + + // after a store is destroyed + hooks.destroyed(() => {}) +} + +// You can pass a factory function to set default values to each new store instance +const vueFlowApp = createVueFlow(() => ({ + fitViewOnInit: true, +})) + +vueFlowApp.use(plugin) +``` + +## Extending Store Type (TypeScript) + +```ts +// my-shims.d.ts +declare module '@vue-flow/core' { + // the StoreBase interface can be used to extend the VueFlowStore type + interface StoreBase { + myProperty: boolean + myRefProperty: Ref + } +} +``` diff --git a/.changeset/wild-carrots-occur.md b/.changeset/wild-carrots-occur.md new file mode 100644 index 000000000..c668e355f --- /dev/null +++ b/.changeset/wild-carrots-occur.md @@ -0,0 +1,5 @@ +--- +'@vue-flow/plugin-screenshot': major +--- + +Add screenshot plugin pkg to create a downloadable image of the graph diff --git a/docs/src/.vitepress/config.ts b/docs/src/.vitepress/config.ts index 671c01335..10b205ac9 100644 --- a/docs/src/.vitepress/config.ts +++ b/docs/src/.vitepress/config.ts @@ -1,5 +1,4 @@ import { resolve } from 'node:path' -import { readdirSync, statSync } from 'node:fs' import type { DefaultTheme, HeadConfig } from 'vitepress' import { defineConfigWithTheme } from 'vitepress' import WindiCSS from 'vite-plugin-windicss' @@ -9,56 +8,11 @@ import Components from 'unplugin-vue-components/vite' import AutoImport from 'unplugin-auto-import/vite' import { useVueFlow } from '@vue-flow/core' import head from './head' -import { copyVueFlowPlugin, files } from './plugins' +import { copyVueFlowPlugin } from './plugins' +import { changelogSidebarEntries, pluginSidebarEntries, typedocSidebarEntries } from './utils' const { vueFlowVersion } = useVueFlow() -function capitalize(str: string) { - return str.charAt(0).toUpperCase() + str.slice(1) -} - -function typedocSidebarEntries() { - const filePath = resolve(__dirname, '../typedocs') - - const docsModules = readdirSync(filePath).filter((name) => statSync(`${filePath}/${name}`).isDirectory()) - - return docsModules.map((module) => { - let children = readdirSync(`${filePath}/${module}/`).map((entry) => ({ - text: entry.replace('.md', ''), - link: `/typedocs/${module}/${entry.replace('.md', '')}`, - })) - - if (module === 'variables') { - children = children.filter((child) => { - return child.link.includes('default') - }) - } - - return { text: capitalize(module), collapsed: false, items: children } as DefaultTheme.SidebarItem - }) -} - -function changelogSidebarEntries(): DefaultTheme.SidebarItem[] { - return [ - { - text: 'CHANGELOG', - collapsed: true, - items: files.map((file) => { - const name = file.pkgName.replace('.md', '') - const isCore = name === 'core' - - return { - text: name - .split('-') - .map((s) => capitalize(s)) - .join(' '), - link: `/changelog/${isCore ? '' : name}`, - } - }), - }, - ] -} - export default defineConfigWithTheme({ title: 'Vue Flow', description: 'Visualize your ideas with Vue Flow, a highly customizable Vue3 Flowchart library.', @@ -130,6 +84,11 @@ export default defineConfigWithTheme({ link: '/examples/', activeMatch: '^/examples/', }, + { + text: 'Plugins', + link: '/plugins/', + activeMatch: '^/plugins/', + }, { text: 'Migration', link: '/migration/', @@ -244,6 +203,7 @@ export default defineConfigWithTheme({ ], '/typedocs/': typedocSidebarEntries(), '/changelog/': changelogSidebarEntries(), + '/plugins/': pluginSidebarEntries(), }, }, }) diff --git a/docs/src/.vitepress/plugins/changelog.ts b/docs/src/.vitepress/plugins/changelog.ts index 325a53542..e9778b095 100644 --- a/docs/src/.vitepress/plugins/changelog.ts +++ b/docs/src/.vitepress/plugins/changelog.ts @@ -26,7 +26,7 @@ const getAllFiles = function (dirPath: string, needle?: string, arrayOfFiles: Ch return arrayOfFiles } -export const files = getAllFiles(resolve(__dirname, '../../../../packages'), 'CHANGELOG') +export const changelogFiles = getAllFiles(resolve(__dirname, '../../../../packages'), 'CHANGELOG') const changelogDirPath = resolve(__dirname, `../../changelog/`) @@ -34,7 +34,7 @@ if (!existsSync(changelogDirPath)) { mkdirSync(changelogDirPath) } -files.forEach(({ path, pkgName }) => { +changelogFiles.forEach(({ path, pkgName }) => { const isCore = pkgName === 'core' const filePath = resolve(__dirname, `${path}`) diff --git a/docs/src/.vitepress/plugins/index.ts b/docs/src/.vitepress/plugins/index.ts index e43dde54a..a65795010 100644 --- a/docs/src/.vitepress/plugins/index.ts +++ b/docs/src/.vitepress/plugins/index.ts @@ -1,2 +1,3 @@ export * from './changelog' export * from './copy' +export * from './plugins' diff --git a/docs/src/.vitepress/plugins/plugins.ts b/docs/src/.vitepress/plugins/plugins.ts new file mode 100644 index 000000000..22d51c3eb --- /dev/null +++ b/docs/src/.vitepress/plugins/plugins.ts @@ -0,0 +1,41 @@ +import { copyFile, existsSync, mkdirSync, readdirSync, statSync } from 'node:fs' +import { resolve } from 'node:path' + +interface PluginFile { + path: string + pkgName: string +} + +const skip = ['node_modules', 'dist', 'turbo'] + +const getAllFiles = function (dirPath: string, needle?: string, arrayOfFiles: PluginFile[] = [], pkgName?: string) { + readdirSync(dirPath).forEach((file) => { + if (skip.includes(file)) return + + if (statSync(`${dirPath}/${file}`).isDirectory()) { + getAllFiles(`${dirPath}/${file}`, needle, arrayOfFiles, file) + } else { + if (file.includes('README')) { + arrayOfFiles.push({ path: `${dirPath}/${file}`, pkgName }) + } + } + }) + + return arrayOfFiles +} + +export const pluginFiles = getAllFiles(resolve(__dirname, '../../../../packages/plugins'), 'README') + +const pluginDirPath = resolve(__dirname, `../../plugins/`) + +if (!existsSync(pluginDirPath)) { + mkdirSync(pluginDirPath) +} + +pluginFiles.forEach(({ path, pkgName }) => { + const filePath = resolve(__dirname, `${path}`) + + copyFile(filePath, `${pluginDirPath}/${pkgName}.md`, () => {}) + + console.log(`Copied ${filePath} to ${pluginDirPath}/${pkgName}.md`) +}) diff --git a/docs/src/.vitepress/utils.ts b/docs/src/.vitepress/utils.ts new file mode 100644 index 000000000..91eaa75d1 --- /dev/null +++ b/docs/src/.vitepress/utils.ts @@ -0,0 +1,70 @@ +import { resolve } from 'node:path' +import { readdirSync, statSync } from 'node:fs' +import type { DefaultTheme } from 'vitepress' +import { changelogFiles, pluginFiles } from './plugins' + +export function capitalize(str: string) { + return str.charAt(0).toUpperCase() + str.slice(1) +} + +export const typedocSidebarEntries = (): DefaultTheme.SidebarGroup[] => { + const filePath = resolve(__dirname, '../typedocs') + + const docsModules = readdirSync(filePath).filter((name) => statSync(`${filePath}/${name}`).isDirectory()) + + return docsModules.map((module) => { + let children = readdirSync(`${filePath}/${module}/`).map((entry) => ({ + text: entry.replace('.md', ''), + link: `/typedocs/${module}/${entry.replace('.md', '')}`, + })) + + if (module === 'variables') { + children = children.filter((child) => { + return child.link.includes('default') + }) + } + + return { text: capitalize(module), collapsible: true, items: children } as DefaultTheme.SidebarGroup + }) +} + +export const changelogSidebarEntries = (): DefaultTheme.SidebarGroup[] => { + return [ + { + text: 'CHANGELOG', + collapsible: true, + items: changelogFiles.map((file) => { + const name = file.pkgName.replace('.md', '') + const isCore = name === 'core' + + return { + text: name + .split('-') + .map((s) => capitalize(s)) + .join(' '), + link: `/changelog/${isCore ? '' : name}`, + } + }), + }, + ] +} + +export const pluginSidebarEntries = (): DefaultTheme.SidebarGroup[] => { + return [ + { + text: 'Plugins', + collapsible: true, + items: pluginFiles.map((file) => { + const name = file.pkgName.replace('.md', '') + + return { + text: name + .split('-') + .map((s) => capitalize(s)) + .join(' '), + link: `/plugins/${name}`, + } + }), + }, + ] +} diff --git a/docs/src/guide/index.md b/docs/src/guide/index.md index 7e3305164..e0700bb8e 100644 --- a/docs/src/guide/index.md +++ b/docs/src/guide/index.md @@ -19,18 +19,18 @@ Check out the [examples](/examples/) if you want to dive directly into the code. - 👶 __Easy setup__: Get started hassle-free - Built-in zoom- & pan features, element dragging, selection and much more -- 🎨 __Customizable__: Use your own custom nodes, edges, connection lines and expand on the Vue Flows functionality +- 🎨 __Customizable__: Use your own custom nodes, edges, connection-lines and a multitude of plugins to extend the functionality of Vue Flow - 🚀 __Fast__: Tracks changes reactively and only re-renders the appropriate elements -- 🧲 __Utils & Composition__: Comes with graph helper and state composable functions for advanced uses +- 🧲 __Utils & Composition__: Built for Composition API and comes with a set of useful utilities to make your life easier - 📦 __Additional Components__: - - 🖼 Background: With two built-in patterns and some configuration options like height, width or color. + - 🖼 Background: With two built-in patterns (grid & dots) and the ability to add your own custom background shapes - - 🧭 Minimap: Shows current nodes in a small map shape in the bottom right corner + - 🧭 Interactive Minimap: Shows current nodes in a small map shape and allows dragging or zooming the viewport using the mini map - - 🕹 Controls: Control zoom behavior from a panel on the bottom left + - 🕹 Controls: A panel with buttons to zoom in, zoom out, fit the view or lock interaction. You can also add your own custom controls to the panel. -- 🦾 __Reliable__: Fully written in TypeScript +- 🦾 __Reliable__: Fully written in TypeScript and tested with Cypress 10 diff --git a/docs/src/plugins/index.md b/docs/src/plugins/index.md new file mode 100644 index 000000000..17648797c --- /dev/null +++ b/docs/src/plugins/index.md @@ -0,0 +1,125 @@ +--- +title: Plugin API +--- + +# Introduction + +## What is a Plugin? + +A plugin is a function that takes Vue Flow app lifecycle hooks as an argument and returns void. +It allows you to extend on the core functionalities of Vue Flow by using the existing store and hooks. + +```ts +type Plugin = (hooks: PluginHooks) => void + +interface PluginHooks { + beforeCreate: EventHookOn<[string, FlowOptions | undefined]> + created: EventHookOn + beforeDestroy: EventHookOn + destroyed: EventHookOn +} +``` + +## How to use a Plugin? + +To use a plugin, you first need to create a new Vue Flow app and then pass the plugin to the `use` method. + +```ts +// main.ts +import { createApp } from 'vue' +import { createVueFlow } from '@vue-flow/core' + +import { MyPlugin } from './my-plugin' + +const app = createApp(App) + +const vueFlow = createVueFlow() + +vueFlow.use(MyPlugin) + +app.mount('#app') +``` + +## How to create a Plugin? + +To create a plugin, you need to create a function that takes the `PluginHooks` as an argument and returns void. + +```ts +// my-plugin.ts +import { PluginHooks } from '@vue-flow/core' + +export const MyPlugin: Plugin = (hooks) => { + hooks.created(([store, extend]) => { + console.log('Vue Flow App instance created') + + // use the extend fn to extend the store instance + extend({ + myTestPlugin: () => console.log('Hello from my plugin!') + }) + + }) +} +``` + +## Plugin Hooks + +### beforeCreate + +The `beforeCreate` hook is called before the store is created. It takes the `id` and `options` as arguments. + +```ts +hooks.beforeCreate(([id, options]) => { + console.log('store is about to be created') +}) +``` + +### created + +The `created` hook is called after the store is created. It takes the `store` as the first and an `extend` function as the second argument. +You can use the `extend` function to extend the store instance with your custom properties. + +```ts +hooks.created(([store, extend]) => { + console.log('store created') + + // Extend the store + extend({ myProp: () => {} }) + + // myProp is now accessable from store.myProp +}) +``` + +### beforeDestroy + +The `beforeDestroy` hook is called before the store is destroyed. It takes the `store` as an argument. + +```ts +hooks.beforeDestroy((store) => { + console.log('store is about to be destroyed') +}) +``` + +### destroyed + +The `destroyed` hook is called after the store is destroyed. It takes the `id` as an argument. + +```ts +hooks.destroyed((id) => { + console.log('store destroyed') +}) +``` + +## TypeScript + +You can create a type for your plugin so users have proper IDE support for your custom properties on their store instance. + +```ts +import type { UseTestPlugin } from './types' + +declare module '@vue-flow/core' { + // extend the StoreBase interface to add your custom properties + interface StoreBase { + myTestPlugin: UseTestPlugin + } +} +``` diff --git a/docs/tsconfig.docs.json b/docs/tsconfig.docs.json index 0a66de340..e515e1523 100644 --- a/docs/tsconfig.docs.json +++ b/docs/tsconfig.docs.json @@ -21,9 +21,11 @@ } }, "include": [ - "../packages/**/*" + "../packages/**/*", + "../packages/plugins/**/*" ], "exclude": [ - "node_modules" + "node_modules", + "../packages/plugins/**/dist" ] } diff --git a/docs/typedoc.json b/docs/typedoc.json index 5f71be864..451296d22 100644 --- a/docs/typedoc.json +++ b/docs/typedoc.json @@ -10,7 +10,10 @@ "../packages/controls/src/index.ts", "../packages/minimap/src/index.ts", "../packages/node-toolbar/src/index.ts", - "../packages/node-resizer/src/index.ts" + "../packages/node-resizer/src/index.ts", + "../packages/plugins/dagre/src/index.ts", + "../packages/plugins/drag-n-drop/src/index.ts", + "../packages/plugins/screenshot/src/index.ts" ], "categorizeByGroup": true, "darkHighlightTheme": "vitesse-dark", diff --git a/docs/typedoc.md.json b/docs/typedoc.md.json index 29884c383..54df7ffa5 100644 --- a/docs/typedoc.md.json +++ b/docs/typedoc.md.json @@ -11,7 +11,10 @@ "../packages/controls/src/index.ts", "../packages/minimap/src/index.ts", "../packages/node-toolbar/src/index.ts", - "../packages/node-resizer/src/index.ts" + "../packages/node-resizer/src/index.ts", + "../packages/plugins/dagre/src/index.ts", + "../packages/plugins/drag-n-drop/src/index.ts", + "../packages/plugins/screenshot/src/index.ts" ], "allReflectionsHaveOwnDocument": true, "categorizeByGroup": true, diff --git a/examples/vite/main.ts b/examples/vite/main.ts index 6d8604e29..1e67f21ab 100644 --- a/examples/vite/main.ts +++ b/examples/vite/main.ts @@ -1,10 +1,16 @@ import { createApp } from 'vue' +import { createVueFlow } from '@vue-flow/core' +import { PluginDragNDrop } from '@vue-flow/plugin-drag-n-drop' +import { PluginScreenshot } from '@vue-flow/plugin-screenshot' import './index.css' -import { createPinia } from 'pinia' import App from './App.vue' import { router } from './router' const app = createApp(App) +const vueFlowApp = createVueFlow() + +vueFlowApp.use(PluginDragNDrop) +vueFlowApp.use(PluginScreenshot({ defaultFileName: 'vue-flow-screenshot' })) app.config.performance = true app.use(router) diff --git a/examples/vite/package.json b/examples/vite/package.json index bee1a6e69..479889381 100644 --- a/examples/vite/package.json +++ b/examples/vite/package.json @@ -13,6 +13,9 @@ "@vue-flow/minimap": "workspace:*", "@vue-flow/node-resizer": "workspace:*", "@vue-flow/node-toolbar": "workspace:*", + "@vue-flow/plugin-layout": "workspace:*", + "@vue-flow/plugin-drag-n-drop": "workspace:*", + "@vue-flow/plugin-screenshot": "workspace:*", "pinia": "^2.0.35", "vueflow": "workspace:*" }, diff --git a/examples/vite/router.ts b/examples/vite/router.ts index 308fc59f2..6f68066c1 100644 --- a/examples/vite/router.ts +++ b/examples/vite/router.ts @@ -114,6 +114,10 @@ export const routes: RouterOptions['routes'] = [ path: '/nesting', component: () => import('./src/Nesting/Nesting.vue'), }, + { + path: '/screenshot', + component: () => import('./src/Screenshot/ScreenshotExample.vue'), + }, { path: '/rgb', component: () => import('./src/RGBFlow/RGBFlow.vue'), diff --git a/examples/vite/src/DragNDrop/DnD.vue b/examples/vite/src/DragNDrop/DnD.vue index 5489c7b6c..875640bd2 100644 --- a/examples/vite/src/DragNDrop/DnD.vue +++ b/examples/vite/src/DragNDrop/DnD.vue @@ -1,13 +1,12 @@ diff --git a/examples/vite/src/DragNDrop/Sidebar.vue b/examples/vite/src/DragNDrop/Sidebar.vue index 8636ae05c..ba1dfdeab 100644 --- a/examples/vite/src/DragNDrop/Sidebar.vue +++ b/examples/vite/src/DragNDrop/Sidebar.vue @@ -1,23 +1,14 @@ diff --git a/examples/vite/src/Layouting/LayoutingExample.vue b/examples/vite/src/Layouting/LayoutingExample.vue index d7421925d..578efdd31 100644 --- a/examples/vite/src/Layouting/LayoutingExample.vue +++ b/examples/vite/src/Layouting/LayoutingExample.vue @@ -1,56 +1,23 @@