├── .github └── workflows │ └── deploy-docs.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── docs ├── .vitepress │ ├── components │ │ ├── Container.vue │ │ ├── DnDDemo.vue │ │ ├── DnDList.vue │ │ ├── Draggable.vue │ │ ├── DraggableList.vue │ │ ├── Droppable.vue │ │ ├── ExampleContainer.vue │ │ ├── Examples │ │ │ ├── Animation │ │ │ │ ├── CustomContainer.vue │ │ │ │ ├── Draggable.vue │ │ │ │ └── Example.vue │ │ │ ├── ChangingLayers │ │ │ │ ├── Draggable.vue │ │ │ │ ├── Example.vue │ │ │ │ └── Layer.vue │ │ │ ├── ChangingOverlay │ │ │ │ ├── Container.vue │ │ │ │ └── ExampleOverlay.vue │ │ │ ├── CustomDragOverlayContainer │ │ │ │ ├── Container.vue │ │ │ │ └── Draggable.vue │ │ │ ├── CustomSensor │ │ │ │ └── Example.vue │ │ │ ├── DragHandle.vue │ │ │ ├── DragToZone.vue │ │ │ ├── Groups │ │ │ │ ├── Draggable.vue │ │ │ │ ├── Droppable.vue │ │ │ │ └── Example.vue │ │ │ ├── KeyboardSupport.vue │ │ │ ├── MorphSvg │ │ │ │ ├── Draggable.vue │ │ │ │ └── Example.vue │ │ │ ├── MultiSelection │ │ │ │ ├── Draggable.vue │ │ │ │ └── Example.vue │ │ │ ├── NestingZones │ │ │ │ ├── Draggable.vue │ │ │ │ ├── Example.vue │ │ │ │ └── Zone.vue │ │ │ ├── PromiseDrop │ │ │ │ └── Example.vue │ │ │ ├── ReorderingItems │ │ │ │ ├── Draggable.vue │ │ │ │ └── List.vue │ │ │ ├── SimpleDrag.vue │ │ │ ├── SortingLists │ │ │ │ ├── Draggable.vue │ │ │ │ └── Example.vue │ │ │ └── Tree │ │ │ │ ├── Draggable.vue │ │ │ │ ├── Droppable.vue │ │ │ │ ├── Example.vue │ │ │ │ └── Tree.vue │ │ └── Skeleton.vue │ ├── config.mts │ └── theme │ │ └── index.ts ├── about.md ├── examples │ ├── advanced │ │ ├── changing-layers.md │ │ ├── changing-overlay.md │ │ ├── custom-sensor.md │ │ ├── grouping.md │ │ └── nesting-zones.md │ ├── basic │ │ ├── drag-handle.md │ │ ├── drag-overlay.md │ │ ├── drag-to-zone.md │ │ ├── keyboard-support.md │ │ ├── multi-selection.md │ │ └── simple-drag.md │ └── real-world │ │ ├── animating.md │ │ ├── morph-svg.md │ │ ├── promise-drop.md │ │ ├── reordering-items.md │ │ ├── sorting-lists.md │ │ └── tree.md ├── guide │ ├── core │ │ ├── collision-detection.md │ │ ├── dnd-operations.md │ │ ├── use-dnd-store.md │ │ ├── use-drag-container.md │ │ ├── use-draggable.md │ │ ├── use-droppable.md │ │ └── use-selection.md │ ├── devtools │ │ └── usage.md │ ├── installation.md │ ├── introduction.md │ └── quick-start.md └── index.md ├── package.json ├── packages ├── components │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── Draggable.vue │ │ │ ├── Droppable.vue │ │ │ ├── Kanban │ │ │ │ ├── Kanban.vue │ │ │ │ ├── KanbanColumn.vue │ │ │ │ └── KanbanItem.vue │ │ │ └── Table │ │ │ │ ├── Table.vue │ │ │ │ ├── TableBody.vue │ │ │ │ └── TableHead.vue │ │ ├── index.ts │ │ ├── shims-vue.d.ts │ │ ├── types │ │ │ └── index.d.ts │ │ ├── utils │ │ │ └── classNames.ts │ │ ├── vite-env.d.ts │ │ └── vue-shims.d.ts │ ├── tsconfig.json │ ├── tsconfig.node.json │ └── vite.config.ts ├── core │ ├── LICENSE │ ├── README.md │ ├── package.json │ ├── src │ │ ├── components │ │ │ ├── DefaultOverlay.vue │ │ │ └── DragOverlay.vue │ │ ├── composables │ │ │ ├── useDnDStore.ts │ │ │ ├── useDragContainer.ts │ │ │ ├── useDraggable.ts │ │ │ ├── useDroppable.ts │ │ │ ├── useKeyboard.ts │ │ │ ├── usePointer.ts │ │ │ ├── useSelection.ts │ │ │ └── useSensor.ts │ │ ├── index.ts │ │ ├── managers │ │ │ ├── useElementManager.ts │ │ │ ├── useEventManager.ts │ │ │ └── useZoneManager.ts │ │ ├── plugin.ts │ │ ├── types │ │ │ └── index.d.ts │ │ ├── utils │ │ │ ├── dom.ts │ │ │ ├── events.ts │ │ │ ├── geometry.ts │ │ │ ├── namespaces.ts │ │ │ ├── operations.ts │ │ │ └── sensor.ts │ │ └── vue-shims.d.ts │ ├── tsconfig.json │ └── vite.config.ts ├── devtools │ ├── README.md │ ├── package.json │ ├── rollup.config.js │ ├── src │ │ ├── devtools.ts │ │ ├── global.d.ts │ │ ├── index.ts │ │ └── utils │ │ │ ├── container.ts │ │ │ ├── elements.ts │ │ │ ├── hovered.ts │ │ │ ├── pointer.ts │ │ │ ├── store.ts │ │ │ └── zones.ts │ └── tsconfig.json └── utilities │ ├── README.md │ ├── package.json │ ├── src │ ├── composables │ │ ├── useAutoScroll.ts │ │ └── useGeometry.ts │ ├── index.ts │ ├── types │ │ └── index.d.ts │ └── utils │ │ └── index.ts │ ├── tsconfig.json │ └── vite.config.ts ├── playground ├── .gitignore ├── README.md ├── index.html ├── package.json ├── public │ └── vite.svg ├── src │ ├── App.vue │ ├── assets │ │ └── vue.svg │ ├── components │ │ ├── CustomContainer.vue │ │ ├── Drag.vue │ │ └── Draggable.vue │ ├── main.ts │ ├── pages │ │ ├── Animation.vue │ │ ├── Kanban.vue │ │ ├── LazyDrop.vue │ │ ├── Promises.vue │ │ ├── SortableList.vue │ │ └── Table.vue │ ├── router │ │ └── index.ts │ ├── shims.d.ts │ ├── style.css │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json ├── tsconfig.tsbuildinfo ├── vite.config.ts └── yarn.lock ├── public ├── docs │ └── images │ │ └── devtools-screenshot.png └── logo.svg └── yarn.lock /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Documentation 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: write 10 | pages: write 11 | 12 | jobs: 13 | build-and-deploy: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v3 18 | 19 | - name: Setup Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: 18 23 | cache: 'yarn' 24 | 25 | - name: Install dependencies 26 | run: yarn install 27 | 28 | - name: Build documentation 29 | run: yarn docs:build 30 | 31 | - name: Deploy to GitHub Pages 32 | uses: JamesIves/github-pages-deploy-action@v4 33 | with: 34 | folder: docs/.vitepress/dist 35 | branch: gh-pages 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist-ssr 12 | dist 13 | packages/*/dist 14 | 15 | 16 | *.local 17 | lib 18 | docs/.vitepress/dist 19 | docs/.vitepress/cache 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | .DS_Store 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | All notable changes to the Vue DnD Kit project are documented in this file. 4 | 5 | ## 2025-05-19 6 | 7 | ### Core Package 8 | 9 | - Added lazyAllowed for all composables 10 | - Added promise support for dropzone 11 | 12 | ## 2025-05-13 13 | 14 | ### Core Package 15 | 16 | - Added options for plugin, now u can cutomize default overlay styles 17 | 18 | ## 2025-05-01 19 | 20 | ### Core Package 21 | 22 | - Added DragOverlay transition detection (need to handle it, Example in docs) 23 | - fix bug with isDragging on Draggable element in useDraggable hook 24 | 25 | ## 2025-04-30 Added payload to all DnD Events 26 | 27 | ### Core Package 28 | 29 | - now onDrop, onHover, and other events return payload (it's array DraggingItems array) 30 | 31 | ## 2025-04-28 Added transition support for useDragContainer 32 | 33 | ### Core Package 34 | 35 | - added transition support for usedragcontainer 36 | 37 | ## 2025-04-27 - DevTools Package Release 38 | 39 | ### DevTools Package 40 | 41 | - Initial release of the DevTools package 42 | - Integration with Vue DevTools 43 | - Real-time visualization of drag and drop state 44 | - Store state inspection and monitoring 45 | - Elements and drop zones visualization 46 | - Pointer position tracking 47 | - Container and hover state debugging 48 | 49 | ## 2025-04-19 - Core Package Stable Release 50 | 51 | ### Core Package 52 | 53 | - Stable API release 54 | - Full drag and drop functionality 55 | - Touch and mouse input support 56 | - Keyboard navigation 57 | - Custom drag handles 58 | - Multiple draggable items support 59 | - TypeScript support with complete type definitions 60 | - ARIA attributes for accessibility 61 | - Sortable list implementation 62 | - Grid layout support 63 | - Auto-scrolling when dragging near viewport edges 64 | - Drop zone validation 65 | - Event hooks for all drag phases 66 | 67 | --- 68 | 69 | This CHANGELOG will be updated as new features and improvements are added to the Vue DnD Kit ecosystem. 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ZiZiGY 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Vue DnD Kit Logo 3 |

4 | 5 |

6 | A modern, lightweight drag and drop library for Vue 3 applications 7 |

8 | 9 |

10 | 11 | Documentation 12 | 13 | Status: ready 14 | License: MIT 15 |

16 | 17 | ## 📋 About 18 | 19 | This repository contains the source code for Vue DnD Kit - a lightweight, high-performance drag and drop toolkit for Vue 3 applications. Inspired by the popular [React DnD Kit](https://dndkit.com/), but specifically designed for the Vue.js ecosystem. 20 | 21 | ## 🚀 Features 22 | 23 | - **Composable API**: Simple composables like `useDraggable` and `useDroppable` for creating drag and drop interfaces 24 | - **High Performance**: Built with performance in mind using efficient algorithms and optimizations 25 | - **TypeScript Support**: Written in TypeScript for better developer experience and code reliability 26 | - **Accessibility**: Designed with accessibility in mind, supporting keyboard navigation and screen readers 27 | - **Flexible**: Create custom drag overlays, handles, and complex nested interfaces 28 | 29 | ## 📦 Repository Structure 30 | 31 | The repository is organized into several packages: 32 | 33 | - **packages/core**: The main package with core drag and drop functionality 34 | - **packages/components**: (Coming soon) Ready-to-use components built on top of the core library 35 | - **packages/utils**: (Coming soon) Utility functions for advanced customization 36 | - **packages/devtools**: (Coming soon) Developer tools for debugging and optimizing 37 | - **docs**: Documentation site built with VitePress 38 | 39 | ## 📖 Documentation 40 | 41 | Visit our [documentation site](https://zizigy.github.io/vue-dnd-kit/) for guides, API references, and examples. 42 | 43 | ## 🔧 Dependencies 44 | 45 | - Vue 3 46 | - VueUse 47 | 48 | ## 🤝 Contributing 49 | 50 | Contributions are welcome! Please feel free to submit a Pull Request. 51 | 52 | ## 📄 License 53 | 54 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. 55 | 56 |

Made with ❤️ for the Vue.js community

57 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Container.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 43 | 44 | 55 | -------------------------------------------------------------------------------- /docs/.vitepress/components/DnDDemo.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 35 | 36 | 54 | -------------------------------------------------------------------------------- /docs/.vitepress/components/DnDList.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 42 | 43 | 54 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Draggable.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 35 | 36 | 65 | -------------------------------------------------------------------------------- /docs/.vitepress/components/DraggableList.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 39 | 40 | 60 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Droppable.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 23 | 24 | 75 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/Animation/CustomContainer.vue: -------------------------------------------------------------------------------- 1 | 72 | 73 | 92 | 93 | 116 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/Animation/Draggable.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 43 | 44 | 86 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/Animation/Example.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 54 | 55 | 117 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/ChangingLayers/Draggable.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 39 | 40 | 63 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/ChangingLayers/Example.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/ChangingLayers/Layer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/ChangingOverlay/Container.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 38 | 39 | 87 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/CustomDragOverlayContainer/Container.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 29 | 30 | 49 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/CustomDragOverlayContainer/Draggable.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 23 | 24 | 48 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/CustomSensor/Example.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 47 | 48 | 85 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/DragHandle.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 81 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/DragToZone.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 42 | 43 | 82 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/Groups/Draggable.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 30 | 31 | 42 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/Groups/Droppable.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 42 | 43 | 71 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/Groups/Example.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 44 | 45 | 57 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/KeyboardSupport.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 30 | 31 | 84 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/MorphSvg/Draggable.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 36 | 37 | 50 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/MorphSvg/Example.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 57 | 58 | 78 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/MultiSelection/Draggable.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 32 | 33 | 83 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/MultiSelection/Example.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 15 | 16 | 23 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/NestingZones/Draggable.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 25 | 26 | 37 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/NestingZones/Example.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 46 | 47 | 54 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/NestingZones/Zone.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 24 | 25 | 39 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/PromiseDrop/Example.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 44 | 45 | 75 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/ReorderingItems/Draggable.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 33 | 34 | 96 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/ReorderingItems/List.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 60 | 61 | 102 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/SimpleDrag.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 21 | 22 | 31 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/SortingLists/Draggable.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 31 | 32 | 69 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/SortingLists/Example.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 74 | 75 | 119 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/Tree/Draggable.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 36 | 37 | 61 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/Tree/Droppable.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 28 | 29 | 45 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/Tree/Example.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 69 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Examples/Tree/Tree.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 33 | 34 | 50 | -------------------------------------------------------------------------------- /docs/.vitepress/components/Skeleton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 22 | 23 | 57 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from 'vitepress/theme'; 2 | import VueDnDKitDevtools from '@vue-dnd-kit/devtools'; 3 | import VueDnDKitPlugin from '@vue-dnd-kit/core'; 4 | 5 | export default { 6 | ...DefaultTheme, 7 | enhanceApp({ app }) { 8 | app.use(VueDnDKitPlugin); 9 | app.use(VueDnDKitDevtools); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /docs/about.md: -------------------------------------------------------------------------------- 1 | # About Vue DnD Kit 2 | 3 | ## Project Origin 4 | 5 | Vue DnD Kit was born out of necessity when I needed to build a WYSIWYG editor with advanced drag and drop functionality. After searching for existing solutions, I couldn't find any library that met all my requirements: 6 | 7 | - Support for keyboard-based drag and drop for accessibility 8 | - Multi-drag capability to move multiple items simultaneously 9 | - Dynamic layer/z-index changes when hovering over drop zones 10 | - Custom animations during drag operations 11 | - Lightweight yet powerful API out of the box 12 | 13 | Facing these challenges, I decided to create my own comprehensive solution from scratch. This project is the evolution of my previous work with Vue DnD Hooks, completely reimagined for Vue 3's Composition API with a focus on modularity and extensibility. 14 | 15 | ## Vision 16 | 17 | My vision for Vue DnD Kit is to create the most developer-friendly drag and drop ecosystem for Vue applications. I believe that: 18 | 19 | 1. **Developers shouldn't have to compromise** between ease of use and flexibility 20 | 2. **Performance should be a priority**, not an afterthought 21 | 3. **Accessibility should be built-in**, not added later 22 | 4. **Documentation matters** as much as the code itself 23 | 24 | ## Roadmap 25 | 26 | Here's what I'm planning for future releases: 27 | 28 | ### Short-term 29 | 30 | - Expand component library with more specialized components 31 | - Add more examples and recipes for common patterns 32 | - Improve testing coverage and performance benchmarks 33 | - Create comprehensive documentation with interactive examples 34 | 35 | ### Mid-term 36 | 37 | - Add specialized modules for specific use cases (form builders, page builders) 38 | - Create ecosystem plugins for popular UI frameworks 39 | - Implement advanced animation capabilities 40 | - Add support for extremely large datasets 41 | 42 | ### Long-term 43 | 44 | - Explore Vue DnD Kit Plus with premium features for enterprise users 45 | - Create a visual builder for drag and drop interfaces 46 | - Expand the ecosystem based on community feedback 47 | 48 | ## Contributing 49 | 50 | While Vue DnD Kit is currently a solo project, I welcome contributions from the community! Whether you're fixing a bug, improving documentation, or proposing a new feature, your help is appreciated. 51 | 52 | If you're interested in contributing, please feel free to open an issue or submit a pull request on GitHub. 53 | 54 | ## About the Creator 55 | 56 | Vue DnD Kit is created and maintained by [ZIZIGY](https://github.com/zizigy). 57 | 58 | I built this library to solve real-world problems I encountered in my own projects, and I'm sharing it with the community in hopes that it helps others facing similar challenges. 59 | 60 | ## License 61 | 62 | Vue DnD Kit is released under the [MIT License](https://github.com/zizigy/vue-dnd-kit/blob/main/LICENSE). 63 | 64 | ## Support the Project 65 | 66 | If you find Vue DnD Kit useful, please consider: 67 | 68 | - Starring the [GitHub repository](https://github.com/zizigy/vue-dnd-kit) 69 | - Sharing your experience on social media 70 | - Contributing to the codebase or documentation 71 | - Reporting bugs or suggesting features 72 | 73 | Your support helps me continue improving and maintaining this library! 74 | -------------------------------------------------------------------------------- /docs/examples/basic/drag-handle.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Drag Handle 6 | 7 | This example demonstrates how to create elements with drag handles - specific parts of an element that can be used to initiate dragging. 8 | 9 | ## Overview 10 | 11 | The Drag Handle example shows how to use the `useDraggable` composable with accessibility features. Instead of making the entire element draggable, we designate a specific "handle" that users can interact with to initiate drag operations. 12 | 13 | 14 | 15 | ## Key Concepts 16 | 17 | - Creating draggable elements with dedicated drag handles 18 | - Styling drag handles to provide visual cues 19 | 20 | ## Basic Usage 21 | 22 | ```vue 23 | 30 | 31 | 49 | 50 | 101 | ``` 102 | 103 | ## Accessibility Considerations 104 | 105 | When implementing drag handles: 106 | 107 | - Use `role="button"` for the handle 108 | - Add appropriate `aria-label` to describe the purpose of the handle 109 | -------------------------------------------------------------------------------- /docs/examples/basic/drag-to-zone.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Drag to Zone 6 | 7 | This example demonstrates how to create an element that can be dragged into a designated drop zone. 8 | 9 | ## Overview 10 | 11 | The Drag to Zone example shows how to use both `useDraggable` and `useDroppable` composables together to create drag and drop interactions. When the draggable element is dropped onto the drop zone, a visual confirmation appears. 12 | 13 | 14 | 15 | ## Key Concepts 16 | 17 | - Creating a draggable element with `useDraggable` 18 | - Defining a drop zone with `useDroppable` 19 | - Handling drop events 20 | - Providing visual feedback for user interactions 21 | 22 | ## Basic Usage 23 | 24 | ```vue 25 | 41 | 42 | 63 | 64 | 104 | ``` 105 | 106 | ## Next Steps 107 | 108 | After mastering how to drag to a drop zone, you can explore: 109 | 110 | - Creating sortable lists 111 | - Multiple drop zones 112 | - Conditional drop acceptance 113 | - Custom drag overlays 114 | -------------------------------------------------------------------------------- /docs/examples/basic/keyboard-support.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Keyboard Support 6 | 7 | This example demonstrates how to make your drag and drop interface accessible using keyboard controls, making it usable for people who cannot or prefer not to use a mouse. 8 | 9 | ## Overview 10 | 11 | The Keyboard Support example shows how to enhance the `useDraggable` composable with keyboard event handlers, allowing users to initiate and control drag operations using only their keyboard. 12 | 13 | 14 | 15 | ## Key Concepts 16 | 17 | - Making drag and drop operations accessible via keyboard 18 | - Adding proper keyboard event handlers 19 | - Using ARIA attributes to enhance screen reader compatibility 20 | - Providing visual feedback for keyboard focus states 21 | 22 | ## Basic Usage 23 | 24 | ```vue 25 | 32 | 33 | 51 | 52 | 80 | ``` 81 | 82 | ## Accessibility Best Practices 83 | 84 | When implementing keyboard support for drag and drop: 85 | 86 | 1. Make draggable elements focusable with `tabindex="0"` 87 | 2. Use appropriate ARIA roles and labels 88 | 3. Add keyboard event handler for Space key to start dragging (Note: Enter is reserved for drop operations) 89 | 4. Once in drag mode, use WASD keys for directional movement 90 | 5. Provide clear visual focus indicators 91 | 6. Include instructions or visual cues about keyboard commands 92 | 93 | Supporting keyboard interactions ensures your drag and drop interface is accessible to all users, including those with motor disabilities or those who prefer keyboard navigation. 94 | -------------------------------------------------------------------------------- /docs/examples/basic/simple-drag.md: -------------------------------------------------------------------------------- 1 | 4 | 5 | # Simple Drag 6 | 7 | This example demonstrates the most basic usage of Vue DnD Kit - making an element draggable. 8 | 9 | ## Overview 10 | 11 | The Simple Drag example shows how to create draggable elements using the `useDraggable` composable. This is the foundation for all drag and drop interactions in Vue DnD Kit. 12 | 13 | 14 | 15 | ## Key Concepts 16 | 17 | - Using the `useDraggable` composable 18 | - Handling drag start events 19 | - Tracking the dragging state 20 | - Styling draggable elements 21 | - Supporting mobile devices with proper touch handling 22 | 23 | ## Basic Usage 24 | 25 | ```vue 26 | 33 | 34 | 44 | 45 | 54 | ``` 55 | 56 | ## Mobile Support 57 | 58 | To ensure proper drag behavior on mobile devices, it's crucial to add `touch-action: none` to your draggable elements. This CSS property prevents the browser's default touch actions (like scrolling) from interfering with your drag operations. 59 | 60 | ```css 61 | .draggable { 62 | touch-action: none; 63 | } 64 | ``` 65 | 66 | Without this property, mobile users might experience issues where attempting to drag an element results in page scrolling instead of the expected drag behavior. 67 | 68 | ## Next Steps 69 | 70 | Once you understand how to make elements draggable, you can: 71 | 72 | - Create drop zones with `useDroppable` 73 | - Implement drag-to-reorder functionality 74 | - Add custom drag handles 75 | - Create a drag overlay 76 | -------------------------------------------------------------------------------- /docs/guide/core/use-dnd-store.md: -------------------------------------------------------------------------------- 1 | # useDnDStore 2 | 3 | `useDnDStore` provides access to the global drag and drop state store. This is a singleton store (similar to Pinia but lightweight and specialized) that manages all draggable elements, drop zones, and the current drag state. 4 | 5 | While the basic drag and drop functionality works without directly accessing this store, it's useful for advanced customization scenarios, such as implementing custom animations, creating complex drag visualizations, or adding specialized behaviors. 6 | 7 | ## Usage 8 | 9 | ```ts 10 | import { useDnDStore } from '@vue-dnd-kit/core'; 11 | 12 | const store = useDnDStore(); 13 | ``` 14 | 15 | ## API Reference 16 | 17 | ### Store Properties 18 | 19 | | Property | Type | Description | 20 | | ---------------- | ------------------------------------- | ------------------------------------------------------- | 21 | | draggingElements | `Ref>` | Map of currently dragging elements | 22 | | isDragging | `ComputedRef` | Whether any element is currently being dragged | 23 | | elementsMap | `Ref>` | Map of all registered draggable elements | 24 | | selectedElements | `Ref` | Map of all selected elements (for multi-drag) | 25 | | zonesMap | `Ref>` | Map of all registered drop zones | 26 | | visibleZones | `Ref>` | Set of drop zones currently visible in viewport | 27 | | visibleElements | `Ref>` | Set of draggable elements currently visible in viewport | 28 | | pointerPosition | `Object` | Current and initial pointer positions | 29 | | keyboard | `Object` | State of keyboard keys (using VueUse's useMagicKeys) | 30 | | hovered | `Object` | Currently hovered zone and element | 31 | | activeContainer | `Object` | Active overlay container component and reference | 32 | 33 | ### Pointer Position Object 34 | 35 | ```ts 36 | pointerPosition: { 37 | start: Ref, // Initial position when drag started 38 | current: Ref, // Current position during drag 39 | offset: { 40 | percent: Ref, // Offset as percentage of element size 41 | pixel: Ref, // Offset in pixels 42 | } 43 | } 44 | ``` 45 | 46 | ### Hovered Object 47 | 48 | ```ts 49 | hovered: { 50 | zone: Ref, // Currently hovered drop zone 51 | element: Ref // Currently hovered draggable element 52 | } 53 | ``` 54 | 55 | ### Keyboard Object 56 | 57 | ```ts 58 | keyboard: { 59 | w: Ref, // Whether 'w' key is pressed 60 | s: Ref, // Whether 's' key is pressed 61 | a: Ref, // Whether 'a' key is pressed 62 | d: Ref, // Whether 'd' key is pressed 63 | ctrl: Ref, // Whether Control key is pressed 64 | shift: Ref, // Whether Shift key is pressed 65 | alt: Ref, // Whether Alt key is pressed 66 | meta: Ref // Whether Meta key is pressed 67 | } 68 | ``` 69 | 70 | ## Important Notes 71 | 72 | 1. The store is automatically used by `useDraggable` and `useDroppable` - you don't need to access it directly for basic drag and drop operations. 73 | 2. The store follows a singleton pattern - all calls to `useDnDStore()` return the same instance. 74 | 3. All properties are reactive, allowing you to create computed properties or watch for changes. 75 | 4. The store uses IntersectionObserver internally to track which elements are visible in the viewport. 76 | 5. You can access the store from any component without additional setup or provider components. 77 | 6. The store is primarily intended for advanced customization and extension of the drag and drop system. 78 | 7. Internal methods like `handleDragElementIntersection` and `handleDropZoneIntersection` are not recommended for direct use as they're part of the library's internal infrastructure. 79 | -------------------------------------------------------------------------------- /docs/guide/core/use-drag-container.md: -------------------------------------------------------------------------------- 1 | # useDragContainer 2 | 3 | `useDragContainer` is a core composable that allows registering a custom container where dragged elements will be displayed. This composable gives users complete freedom to customize how dragged elements appear during drag operations. 4 | 5 | ## API Reference 6 | 7 | ### Usage 8 | 9 | ```ts 10 | const result = useDragContainer(); 11 | ``` 12 | 13 | The `useDragContainer` composable doesn't accept any parameters. 14 | 15 | ### Return Value 16 | 17 | `useDragContainer` returns an object with the following properties: 18 | 19 | | Property | Type | Description | 20 | | ---------------- | -------------------------- | ----------------------------------------------------- | 21 | | elementRef | `Ref` | Template ref to attach to the container element | 22 | | draggingElements | `Map` | Map of all currently dragged elements | 23 | | pointerPosition | `Object` | Current and initial pointer positions during dragging | 24 | | isDragging | `ComputedRef` | Whether a drag operation is currently active | 25 | 26 | #### pointerPosition Object 27 | 28 | The `pointerPosition` object includes: 29 | 30 | | Property | Type | Description | 31 | | -------------- | --------------------- | ------------------------------------------ | 32 | | start | `Ref` | Starting position when drag began | 33 | | current | `Ref` | Current pointer position during dragging | 34 | | offset.percent | `Ref` | Offset as percentage of element dimensions | 35 | | offset.pixel | `Ref` | Offset in pixels | 36 | 37 | ## Important Notes 38 | 39 | 1. The `elementRef` must be bound to your container element in the template. 40 | 41 | 2. The container is automatically registered with the DnD system on mount and unregistered on unmount. 42 | 43 | 3. For proper positioning of dragged elements, use the values from `pointerPosition`. 44 | 45 | 4. If you create multiple drag containers, the most recently mounted one will be active. 46 | 47 | 5. Containers typically use `pointer-events: none` to prevent interfering with normal interactions. 48 | 49 | 6. To handle multi-select effectively, check `draggingElements.size` to determine how many elements are being dragged. 50 | 51 | 7. The container should typically be positioned fixed or absolute and cover the entire viewport to ensure dragged elements are visible anywhere. 52 | 53 | 8. For performance reasons, apply `will-change: transform` to elements that will move during dragging. 54 | 55 | 9. You may want to conditionally show the container only when `isDragging` is true to improve performance. 56 | -------------------------------------------------------------------------------- /docs/guide/core/use-selection.md: -------------------------------------------------------------------------------- 1 | # useSelection 2 | 3 | `useSelection` is a specialized composable for managing multi-select and multi-drag operations. It's extracted into a separate utility to keep the core `useDraggable` implementation lean while providing powerful selection management when needed. 4 | 5 | ## Usage 6 | 7 | ```ts 8 | import { useDraggable, useSelection } from '@vue-dnd-kit/core'; 9 | import { ref } from 'vue'; 10 | 11 | // First create a draggable element 12 | const { elementRef } = useDraggable({ 13 | /* options */ 14 | }); 15 | 16 | // Then add selection functionality to it 17 | const { isSelected, handleSelect, handleToggleSelect } = 18 | useSelection(elementRef); 19 | ``` 20 | 21 | ## API Reference 22 | 23 | ### Parameters 24 | 25 | ```ts 26 | useSelection(elementRef: Ref) 27 | ``` 28 | 29 | | Parameter | Type | Description | 30 | | ---------- | -------------------------- | ----------------------------------------- | 31 | | elementRef | `Ref` | Reference to the element to be selectable | 32 | 33 | ### Return Value 34 | 35 | `useSelection` returns an object with the following properties: 36 | 37 | | Property | Type | Description | 38 | | ------------------ | ---------------------- | ----------------------------------------- | 39 | | isSelected | `ComputedRef` | Whether the element is currently selected | 40 | | isParentOfSelected | `ComputedRef` | Whether any child elements are selected | 41 | | handleSelect | `() => void` | Function to select this element | 42 | | handleUnselect | `() => void` | Function to unselect this element | 43 | | handleToggleSelect | `() => void` | Function to toggle selection state | 44 | 45 | ## Selection Behavior 46 | 47 | `useSelection` implements smart selection logic that handles parent-child relationships: 48 | 49 | 1. When selecting an element that has selected children, the children are automatically unselected 50 | 2. When selecting an element that has a selected parent, the parent is automatically unselected 51 | 3. This prevents conflicting selection states within the same DOM hierarchy 52 | 53 | ## Important Notes 54 | 55 | 1. `useSelection` provides only the basic selection management functions and does not handle keyboard modifiers by itself. 56 | 57 | 2. For implementing advanced selection behaviors that respond to keyboard modifiers (Shift, Ctrl, etc.), you'll need to combine `useSelection` with `useDnDStore`: 58 | 59 | ```ts 60 | import { useDnDStore } from '@vue-dnd-kit/core'; 61 | 62 | const store = useDnDStore(); 63 | 64 | // Now you can check keyboard states in your event handlers 65 | // store.keyboard.ctrl.value, store.keyboard.shift.value, etc. 66 | ``` 67 | 68 | 3. Selected elements can be dragged together when using the appropriate drag operations. 69 | 70 | 4. Selection state is preserved between drag operations. 71 | 72 | 5. The composable automatically handles parent-child selection conflicts to maintain a consistent selection state. 73 | -------------------------------------------------------------------------------- /docs/guide/devtools/usage.md: -------------------------------------------------------------------------------- 1 | # DevTools Usage 2 | 3 | Vue DnD Kit includes a DevTools package that helps you monitor your drag and drop state in real-time. This integration works directly with Vue DevTools to provide visibility into your application's DnD state. 4 | 5 | ## Installation 6 | 7 | To use DevTools, first install the package: 8 | 9 | ```bash 10 | #yarn 11 | yarn add @vue-dnd-kit/devtools 12 | 13 | #npm 14 | npm install @vue-dnd-kit/devtools 15 | 16 | #pnpm 17 | pnpm add @vue-dnd-kit/devtools 18 | ``` 19 | 20 | If you haven't installed the core package yet, you'll need both: 21 | 22 | ```bash 23 | yarn add @vueuse/core @vue-dnd-kit/core @vue-dnd-kit/devtools 24 | ``` 25 | 26 | ## Integration 27 | 28 | Add the DevTools to your Vue application: 29 | 30 | ```js 31 | // main.js or main.ts 32 | import { createApp } from 'vue'; 33 | import App from './App.vue'; 34 | import VueDnDKitPlugin from '@vue-dnd-kit/core'; 35 | import VueDnDKitDevtools from '@vue-dnd-kit/devtools'; 36 | 37 | const app = createApp(App); 38 | 39 | // Register the core plugin 40 | app.use(VueDnDKitPlugin); 41 | 42 | // Register DevTools 43 | app.use(VueDnDKitDevtools); 44 | 45 | app.mount('#app'); 46 | ``` 47 | 48 | The DevTools integration automatically works only in development mode. 49 | 50 | ## Using DevTools 51 | 52 | After installing and registering the DevTools plugin, follow these steps to access the state viewer: 53 | 54 | 1. Open your application in a development environment 55 | 2. Open browser DevTools (F12 or right-click → Inspect) 56 | 3. Navigate to the Vue panel in DevTools 57 | 4. Look for the "DnD Store" section in the Vue DevTools sidebar 58 | 59 | ![Vue DnD Kit DevTools](https://raw.githubusercontent.com/ZiZIGY/vue-dnd-kit/refs/heads/master/public/docs/images/devtools-screenshot.png) 60 | 61 | ## Available Information 62 | 63 | The DevTools panel shows the current state of your DnD system: 64 | 65 | - **Elements**: All registered draggable elements 66 | - **Zones**: All registered drop zones 67 | - **Pointer**: Current pointer position and state 68 | - **Container**: Container information 69 | - **Hovered**: Currently hovered elements and zones 70 | 71 | This information updates in real-time as you interact with your drag and drop interface, making it easier to debug issues and understand the current state of your application. 72 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-dnd-kit", 3 | "private": true, 4 | "description": "Drag and Drop toolkit for Vue 3", 5 | "scripts": { 6 | "update-submodules": "git submodule update --init --recursive", 7 | "build-all": "npm run build --workspaces", 8 | "dev": "npm run dev --workspaces", 9 | "test": "npm run test --workspaces", 10 | "docs:dev": "vitepress dev docs", 11 | "docs:build": "vitepress build docs", 12 | "docs:preview": "vitepress preview docs" 13 | }, 14 | "workspaces": [ 15 | "packages/*", 16 | "playground" 17 | ], 18 | "keywords": [ 19 | "vue", 20 | "vue3", 21 | "drag-and-drop", 22 | "dnd", 23 | "draggable" 24 | ], 25 | "author": "ZiZIGY", 26 | "license": "MIT", 27 | "devDependencies": { 28 | "typescript": "^4.9.5", 29 | "vue": "^3.2.47" 30 | }, 31 | "dependencies": { 32 | "gsap": "^3.13.0" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/components/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ZiZiGY 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-dnd-kit/components", 3 | "version": "0.2.22", 4 | "description": "Components for Vue DnD Kit", 5 | "author": "ZiZIGY", 6 | "license": "MIT", 7 | "keywords": [ 8 | "vue", 9 | "dnd", 10 | "drag", 11 | "drop", 12 | "drag and drop", 13 | "drag and drop library", 14 | "vue dnd", 15 | "vue drag and drop", 16 | "vue drag and drop library", 17 | "vue dnd kit", 18 | "vue dnd kit components", 19 | "dnd-kit", 20 | "dnd-kit components", 21 | "vue-dnd-kit", 22 | "vue-dnd-kit components", 23 | "@vue-dnd-kit/components" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/zizigy/vue-dnd-kit.git", 28 | "directory": "packages/components" 29 | }, 30 | "scripts": { 31 | "dev": "vite build --watch", 32 | "start": "vite build --watch", 33 | "build": "rm -rf dist && vite build", 34 | "test": "vitest run", 35 | "lint": "eslint src --ext .ts,.vue", 36 | "prepublishOnly": "npm run build" 37 | }, 38 | "main": "./dist/vue-dnd-kit-components.umd.js", 39 | "module": "./dist/vue-dnd-kit-components.es.js", 40 | "unpkg": "./dist/vue-dnd-kit-components.umd.js", 41 | "types": "./dist/types/index.d.ts", 42 | "browser": { 43 | "./sfc": "./src/components/" 44 | }, 45 | "files": [ 46 | "README.md", 47 | "CHANGELOG.md", 48 | "LICENSE", 49 | "dist", 50 | "src/components/*.vue" 51 | ], 52 | "exports": { 53 | ".": { 54 | "types": "./dist/types/index.d.ts", 55 | "import": "./dist/vue-dnd-kit-components.es.js", 56 | "require": "./dist/vue-dnd-kit-components.umd.js" 57 | }, 58 | "./sfc": "./src/components/", 59 | "./styles": "./dist/style.css" 60 | }, 61 | "peerDependencies": { 62 | "@vue-dnd-kit/core": "^0.5.0", 63 | "@vueuse/core": "^10.0.0 || ^13.0.0", 64 | "vue": "^3.3.0 || ^3.4.0 || ^3.5.0" 65 | }, 66 | "devDependencies": { 67 | "@types/node": "^22.13.14", 68 | "@vitejs/plugin-vue": "^4.2.3", 69 | "@vueuse/core": "^13.1.0", 70 | "eslint": "^8.38.0", 71 | "typescript": "^4.9.5", 72 | "vite": "^6.0.5", 73 | "vite-plugin-dts": "^4.5.0", 74 | "vitepress": "^1.6.3", 75 | "vue": "^3.5.13", 76 | "vue-tsc": "^1.8.27" 77 | }, 78 | "publishConfig": { 79 | "access": "public", 80 | "registry": "https://registry.npmjs.org/" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /packages/components/src/components/Draggable.vue: -------------------------------------------------------------------------------- 1 | 64 | 65 | 96 | -------------------------------------------------------------------------------- /packages/components/src/components/Droppable.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 52 | -------------------------------------------------------------------------------- /packages/components/src/components/Kanban/Kanban.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /packages/components/src/components/Kanban/KanbanColumn.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 127 | -------------------------------------------------------------------------------- /packages/components/src/components/Kanban/KanbanItem.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 53 | -------------------------------------------------------------------------------- /packages/components/src/components/Table/Table.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /packages/components/src/components/Table/TableBody.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 56 | -------------------------------------------------------------------------------- /packages/components/src/components/Table/TableHead.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 42 | -------------------------------------------------------------------------------- /packages/components/src/index.ts: -------------------------------------------------------------------------------- 1 | // Экспортируем утилиты 2 | import * as classNames from './utils/classNames'; 3 | 4 | // Экспортируем компоненты по отдельности 5 | import Draggable from './components/Draggable.vue'; 6 | import Droppable from './components/Droppable.vue'; 7 | import Kanban from './components/Kanban/Kanban.vue'; 8 | import KanbanColumn from './components/Kanban/KanbanColumn.vue'; 9 | import KanbanItem from './components/Kanban/KanbanItem.vue'; 10 | import Table from './components/Table/Table.vue'; 11 | import TableBody from './components/Table/TableBody.vue'; 12 | import TableHead from './components/Table/TableHead.vue'; 13 | 14 | export { 15 | Draggable, 16 | Droppable, 17 | Kanban, 18 | KanbanColumn, 19 | KanbanItem, 20 | Table, 21 | TableBody, 22 | TableHead, 23 | classNames, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/components/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue'; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | -------------------------------------------------------------------------------- /packages/components/src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | export interface IKanbanColumn { 2 | title?: string; 3 | [key: string]: any; 4 | items: any[]; 5 | } 6 | 7 | export interface ITableColumn { 8 | key: string; 9 | name: string; 10 | } 11 | -------------------------------------------------------------------------------- /packages/components/src/utils/classNames.ts: -------------------------------------------------------------------------------- 1 | export enum DraggableClassNames { 2 | DRAGGABLE = 'dnd-kit-draggable', 3 | DRAGGABLE_DISABLED = 'dnd-kit-draggable-disabled', 4 | DRAGGABLE_ACTIVE = 'dnd-kit-draggable-active', 5 | DRAGGABLE_SELECTED = 'dnd-kit-draggable-selected', 6 | DRAGGABLE_ALLOWED = 'dnd-kit-draggable-allowed', 7 | DRAGGABLE_OVERED = 'dnd-kit-draggable-overed', 8 | } 9 | 10 | export enum DroppableClassNames { 11 | DROPPABLE = 'dnd-kit-droppable', 12 | DROPPABLE_DISABLED = 'dnd-kit-droppable-disabled', 13 | DROPPABLE_ACTIVE = 'dnd-kit-droppable-active', 14 | } 15 | -------------------------------------------------------------------------------- /packages/components/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue'; 5 | 6 | const component: DefineComponent<{}, {}, any>; 7 | export default component; 8 | } 9 | -------------------------------------------------------------------------------- /packages/components/src/vue-shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue'; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | -------------------------------------------------------------------------------- /packages/components/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "node", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | 14 | /* Важно: добавляем настройки для генерации .d.ts файлов */ 15 | "declaration": true, 16 | "declarationDir": "./dist/types", 17 | "emitDeclarationOnly": true, 18 | 19 | "jsx": "preserve", 20 | "esModuleInterop": true, 21 | 22 | /* Linting */ 23 | "strict": true, 24 | "noUnusedLocals": true, 25 | "noUnusedParameters": true, 26 | "noFallthroughCasesInSwitch": true 27 | }, 28 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"], 29 | "exclude": ["node_modules", "dist", "**/*.test.ts"], 30 | "references": [{ "path": "./tsconfig.node.json" }] 31 | } 32 | -------------------------------------------------------------------------------- /packages/components/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/components/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import dts from 'vite-plugin-dts'; 3 | import { resolve } from 'path'; 4 | import vue from '@vitejs/plugin-vue'; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | vue(), 9 | dts({ 10 | include: ['src/**/*.ts', 'src/**/*.vue'], 11 | outDir: 'dist/types', 12 | cleanVueFileName: true, 13 | staticImport: true, 14 | insertTypesEntry: true, 15 | copyDtsFiles: false, 16 | beforeWriteFile: (filePath, content) => { 17 | let newPath = filePath 18 | .replace('/src/components/', '/components/') 19 | .replace('/src/', '/'); 20 | 21 | return { 22 | filePath: newPath, 23 | content: content, 24 | }; 25 | }, 26 | 27 | }), 28 | ], 29 | build: { 30 | emptyOutDir: true, 31 | lib: { 32 | entry: resolve(__dirname, 'src/index.ts'), 33 | name: 'VueDndKitComponents', 34 | formats: ['es', 'umd'], 35 | fileName: (format) => `vue-dnd-kit-components.${format}.js`, 36 | }, 37 | rollupOptions: { 38 | external: ['vue', '@vue-dnd-kit/core', '@vueuse/core'], 39 | output: { 40 | globals: { 41 | vue: 'Vue', 42 | '@vue-dnd-kit/core': 'VueDndKitCore', 43 | '@vueuse/core': 'VueUse', 44 | }, 45 | exports: 'named', 46 | }, 47 | }, 48 | cssCodeSplit: false, 49 | cssMinify: true, 50 | sourcemap: true, 51 | }, 52 | resolve: { 53 | alias: { 54 | '@vue-dnd-kit/core': resolve(__dirname, '../core/src'), 55 | '@vue-dnd-kit/components': resolve(__dirname, './src'), 56 | '@': resolve(__dirname, './src'), 57 | }, 58 | dedupe: ['vue'], 59 | }, 60 | optimizeDeps: { 61 | include: ['vue'], 62 | exclude: ['@vue-dnd-kit/core'], 63 | }, 64 | }); 65 | 66 | // export default defineConfig({ 67 | // build: { 68 | // emptyOutDir: false, 69 | // lib: { 70 | // entry: path.resolve(__dirname, 'src/main.ts'), 71 | // formats: ['es'], 72 | // name: 'UiLib', 73 | // }, 74 | // rollupOptions: { 75 | // external: ['vue'], 76 | // output: { 77 | // globals: { 78 | // Vue: 'vue', 79 | // }, 80 | // }, 81 | // }, 82 | // }, 83 | // plugins: [vue(), dts()], 84 | // }); 85 | -------------------------------------------------------------------------------- /packages/core/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ZiZiGY 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-dnd-kit/core", 3 | "version": "1.5.0", 4 | "description": "Core functionality for Vue DnD Kit - a lightweight Vue 3 library for building performant and accessible drag and drop interfaces", 5 | "author": "ZiZIGY", 6 | "license": "MIT", 7 | "keywords": [ 8 | "vue", 9 | "dnd", 10 | "drag", 11 | "drop", 12 | "drag and drop", 13 | "drag and drop library", 14 | "vue dnd", 15 | "vue drag and drop", 16 | "vue drag and drop library", 17 | "vue dnd kit", 18 | "vue dnd kit core", 19 | "dnd-kit", 20 | "dnd-kit core", 21 | "vue-dnd-kit", 22 | "vue-dnd-kit core", 23 | "@vue-dnd-kit/core" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/zizigy/vue-dnd-kit.git", 28 | "directory": "packages/core" 29 | }, 30 | "scripts": { 31 | "dev": "yarn build --watch", 32 | "start": "vite build --watch", 33 | "build": "vite build", 34 | "test": "vitest run", 35 | "lint": "eslint src --ext .ts,.vue", 36 | "prepublishOnly": "vite build" 37 | }, 38 | "main": "dist/vue-dnd-kit-core.cjs.js", 39 | "module": "dist/vue-dnd-kit-core.es.js", 40 | "types": "dist/index.d.ts", 41 | "exports": { 42 | ".": { 43 | "types": "./dist/index.d.ts", 44 | "import": "./dist/vue-dnd-kit-core.es.js", 45 | "require": "./dist/vue-dnd-kit-core.cjs.js" 46 | } 47 | }, 48 | "files": [ 49 | "README.md", 50 | "CHANGELOG.md", 51 | "LICENSE", 52 | "dist" 53 | ], 54 | "peerDependencies": { 55 | "@vueuse/core": "^13.1.0", 56 | "vue": "^3.5.13" 57 | }, 58 | "devDependencies": { 59 | "@types/node": "^22.13.14", 60 | "@vitejs/plugin-vue": "^4.2.3", 61 | "@vueuse/core": "^13.1.0", 62 | "eslint": "^8.38.0", 63 | "typescript": "^4.9.5", 64 | "vite": "^6.0.5", 65 | "vite-plugin-dts": "^4.5.0", 66 | "vitepress": "^1.6.3", 67 | "vue": "^3.5.13" 68 | }, 69 | "publishConfig": { 70 | "access": "public", 71 | "registry": "https://registry.npmjs.org/" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /packages/core/src/components/DefaultOverlay.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 58 | -------------------------------------------------------------------------------- /packages/core/src/components/DragOverlay.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 58 | -------------------------------------------------------------------------------- /packages/core/src/composables/useDnDStore.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | IDragElement, 3 | IDraggingElement, 4 | IDropZone, 5 | IPoint, 6 | } from '../types'; 7 | import { 8 | computed, 9 | ref, 10 | shallowRef, 11 | type Component, 12 | type TransitionProps, 13 | } from 'vue'; 14 | import { createGlobalState, useMagicKeys } from '@vueuse/core'; 15 | 16 | export const useDnDStore = createGlobalState(() => { 17 | const isPending = shallowRef(false); 18 | const draggingElements = ref>( 19 | new Map() 20 | ); 21 | const isDragging = computed(() => draggingElements.value.size > 0); 22 | 23 | const activeContainer = { 24 | component: ref(null), 25 | ref: shallowRef(null), 26 | options: shallowRef(null), 27 | animating: { 28 | enter: shallowRef(false), 29 | leave: shallowRef(false), 30 | appear: shallowRef(false), 31 | }, 32 | }; 33 | 34 | const elementsMap = ref>(new Map()); 35 | const selectedElements = ref>(new Set()); 36 | const zonesMap = ref>(new Map()); 37 | const visibleZones = shallowRef>(new Set()); 38 | const visibleElements = shallowRef>(new Set()); 39 | 40 | const hasIntersectionObserver = 41 | typeof window !== 'undefined' && 'IntersectionObserver' in window; 42 | 43 | const elementObserver = hasIntersectionObserver 44 | ? new IntersectionObserver((entries) => { 45 | entries.forEach((entry) => 46 | visibleElements.value[entry.isIntersecting ? 'add' : 'delete']( 47 | entry.target 48 | ) 49 | ); 50 | }) 51 | : null; 52 | 53 | const zoneObserver = hasIntersectionObserver 54 | ? new IntersectionObserver((entries) => { 55 | entries.forEach((entry) => 56 | visibleZones.value[entry.isIntersecting ? 'add' : 'delete']( 57 | entry.target 58 | ) 59 | ); 60 | }) 61 | : null; 62 | 63 | const handleDragElementIntersection = ( 64 | action: 'add' | 'remove', 65 | element: HTMLElement | Element 66 | ) => { 67 | if (!elementObserver) return; 68 | 69 | if (action === 'add') { 70 | elementObserver.observe(element); 71 | } else { 72 | elementObserver.unobserve(element); 73 | visibleElements.value.delete(element); 74 | } 75 | }; 76 | 77 | const handleDropZoneIntersection = ( 78 | action: 'add' | 'remove', 79 | element: HTMLElement | Element 80 | ) => { 81 | if (!zoneObserver) return; 82 | 83 | if (action === 'add') { 84 | zoneObserver.observe(element); 85 | } else { 86 | zoneObserver.unobserve(element); 87 | visibleZones.value.delete(element); 88 | } 89 | }; 90 | 91 | const pointerPosition = { 92 | start: shallowRef(null), 93 | current: shallowRef(null), 94 | offset: { 95 | percent: shallowRef(null), 96 | pixel: shallowRef(null), 97 | }, 98 | }; 99 | 100 | const { w, s, a, d, ctrl, shift, alt, meta } = useMagicKeys(); 101 | 102 | const hovered = { 103 | zone: shallowRef(null), 104 | element: shallowRef(null), 105 | }; 106 | 107 | return { 108 | draggingElements, 109 | isDragging, 110 | activeContainer, 111 | elementsMap, 112 | selectedElements, 113 | zonesMap, 114 | visibleZones, 115 | visibleElements, 116 | pointerPosition, 117 | keyboard: { 118 | w, 119 | s, 120 | a, 121 | d, 122 | ctrl, 123 | shift, 124 | alt, 125 | meta, 126 | }, 127 | hovered, 128 | isPending, 129 | handleDragElementIntersection, 130 | handleDropZoneIntersection, 131 | }; 132 | }); 133 | -------------------------------------------------------------------------------- /packages/core/src/composables/useDragContainer.ts: -------------------------------------------------------------------------------- 1 | import { onUnmounted, onMounted, ref, type TransitionProps } from 'vue'; 2 | 3 | import { useDnDStore } from './useDnDStore'; 4 | 5 | export const useDragContainer = (options?: Omit) => { 6 | const elementRef = ref(null); 7 | 8 | const { draggingElements, pointerPosition, isDragging, activeContainer } = 9 | useDnDStore(); 10 | 11 | onMounted(() => { 12 | activeContainer.ref = elementRef; 13 | activeContainer.options.value = options ?? null; 14 | }); 15 | 16 | onUnmounted(() => { 17 | activeContainer.ref.value = null; 18 | activeContainer.options.value = null; 19 | }); 20 | 21 | return { 22 | elementRef, 23 | draggingElements, 24 | pointerPosition, 25 | isDragging, 26 | animating: activeContainer.animating, 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /packages/core/src/composables/useDraggable.ts: -------------------------------------------------------------------------------- 1 | import { onBeforeUnmount, onMounted } from 'vue'; 2 | 3 | import type { IUseDragOptions } from '../types'; 4 | import { useDnDStore } from './useDnDStore'; 5 | import { useElementManager } from '../managers/useElementManager'; 6 | import { useEventManager } from '../managers/useEventManager'; 7 | 8 | export const useDraggable = (options?: IUseDragOptions) => { 9 | const { 10 | id, 11 | elementRef, 12 | isDragging, 13 | isOvered, 14 | isAllowed, 15 | isLazyAllowed, 16 | registerElement, 17 | unregisterElement, 18 | } = useElementManager(options); 19 | 20 | const { pointerPosition } = useDnDStore(); 21 | const { handleDragStart: start } = useEventManager(); 22 | 23 | const handleDragStart = (event: PointerEvent | KeyboardEvent) => 24 | start(event, elementRef, options); 25 | 26 | onMounted(registerElement); 27 | onBeforeUnmount(unregisterElement); 28 | 29 | return { 30 | pointerPosition, 31 | elementRef, 32 | isDragging, 33 | isOvered, 34 | isAllowed, 35 | isLazyAllowed, 36 | handleDragStart, 37 | id, 38 | }; 39 | }; 40 | -------------------------------------------------------------------------------- /packages/core/src/composables/useDroppable.ts: -------------------------------------------------------------------------------- 1 | import { onBeforeUnmount, onMounted } from 'vue'; 2 | 3 | import type { IUseDropOptions } from '../types'; 4 | import { useZoneManager } from '../managers/useZoneManager'; 5 | 6 | export const useDroppable = (options?: IUseDropOptions) => { 7 | const { 8 | elementRef, 9 | registerZone, 10 | unregisterZone, 11 | isOvered, 12 | isAllowed, 13 | isLazyAllowed, 14 | } = useZoneManager(options); 15 | 16 | // Register/unregister drop zone with store 17 | onMounted(registerZone); 18 | onBeforeUnmount(unregisterZone); 19 | 20 | return { elementRef, isOvered, isAllowed, isLazyAllowed }; 21 | }; 22 | -------------------------------------------------------------------------------- /packages/core/src/composables/useKeyboard.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue'; 2 | import { getBoundingBox } from '../utils/geometry'; 3 | import { preventEvent } from '../utils/events'; 4 | import { useDnDStore } from './useDnDStore'; 5 | 6 | export interface IKeyboardOptions { 7 | moveStep?: number; 8 | } 9 | 10 | export const useKeyboard = ( 11 | elementRef: Ref, 12 | options?: IKeyboardOptions 13 | ) => { 14 | const { pointerPosition, keyboard } = useDnDStore(); 15 | const moveStep = options?.moveStep || 10; 16 | 17 | const onKeyboardStart = (event: KeyboardEvent) => { 18 | preventEvent(event); 19 | 20 | const rect = getBoundingBox(elementRef.value); 21 | 22 | elementRef.value?.blur(); 23 | 24 | const centerX = rect.x + rect.width / 2; 25 | const centerY = rect.y + rect.height / 2; 26 | 27 | pointerPosition.start.value = { 28 | x: centerX, 29 | y: centerY, 30 | }; 31 | 32 | pointerPosition.current.value = { 33 | x: centerX, 34 | y: centerY, 35 | }; 36 | 37 | pointerPosition.offset.pixel.value = { 38 | x: rect.width / 2, 39 | y: rect.height / 2, 40 | }; 41 | 42 | pointerPosition.offset.percent.value = { 43 | x: 50, 44 | y: 50, 45 | }; 46 | }; 47 | 48 | const onKeyboardMove = () => { 49 | if (!pointerPosition.current.value) return; 50 | 51 | const currentX = pointerPosition.current.value.x; 52 | const currentY = pointerPosition.current.value.y; 53 | 54 | let newX = currentX; 55 | let newY = currentY; 56 | 57 | if (keyboard.w.value) newY -= moveStep; 58 | if (keyboard.s.value) newY += moveStep; 59 | if (keyboard.a.value) newX -= moveStep; 60 | if (keyboard.d.value) newX += moveStep; 61 | 62 | pointerPosition.current.value = { 63 | x: newX, 64 | y: newY, 65 | }; 66 | }; 67 | 68 | const onKeyboardEnd = () => { 69 | pointerPosition.current.value = null; 70 | pointerPosition.start.value = null; 71 | pointerPosition.offset.pixel.value = null; 72 | pointerPosition.offset.percent.value = null; 73 | }; 74 | 75 | return { 76 | onKeyboardStart, 77 | onKeyboardMove, 78 | onKeyboardEnd, 79 | }; 80 | }; 81 | -------------------------------------------------------------------------------- /packages/core/src/composables/usePointer.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue'; 2 | import { getOffset } from '../utils/geometry'; 3 | import { useDnDStore } from './useDnDStore'; 4 | 5 | export const usePointer = (elementRef: Ref) => { 6 | const store = useDnDStore(); 7 | 8 | const onPointerStart = (event: PointerEvent) => { 9 | store.pointerPosition.start.value = { x: event.clientX, y: event.clientY }; 10 | store.pointerPosition.current.value = { 11 | x: event.clientX, 12 | y: event.clientY, 13 | }; 14 | 15 | const { pixel, percent } = getOffset(elementRef.value, { 16 | x: event.clientX, 17 | y: event.clientY, 18 | }); 19 | 20 | store.pointerPosition.offset.pixel.value = pixel; 21 | store.pointerPosition.offset.percent.value = percent; 22 | }; 23 | 24 | const onPointerMove = (event: PointerEvent | WheelEvent) => { 25 | if (store.isPending.value) return; 26 | 27 | store.pointerPosition.current.value = { 28 | x: event.clientX, 29 | y: event.clientY, 30 | }; 31 | }; 32 | 33 | const onPointerEnd = () => { 34 | store.pointerPosition.current.value = null; 35 | store.pointerPosition.start.value = null; 36 | store.pointerPosition.offset.pixel.value = null; 37 | store.pointerPosition.offset.percent.value = null; 38 | }; 39 | 40 | return { 41 | onPointerStart, 42 | onPointerMove, 43 | onPointerEnd, 44 | }; 45 | }; 46 | -------------------------------------------------------------------------------- /packages/core/src/composables/useSelection.ts: -------------------------------------------------------------------------------- 1 | import { computed, type Ref } from 'vue'; 2 | import { useDnDStore } from './useDnDStore'; 3 | import { isDescendant } from '../utils/dom'; 4 | 5 | export const useSelection = (elementRef: Ref) => { 6 | const { selectedElements, elementsMap } = useDnDStore(); 7 | 8 | const isSelected = computed(() => 9 | elementRef.value ? selectedElements.value.has(elementRef.value) : false 10 | ); 11 | 12 | const isParentOfSelected = computed(() => { 13 | if (!elementRef.value) return false; 14 | 15 | for (const node of selectedElements.value) { 16 | if ( 17 | node && 18 | isDescendant(node as HTMLElement, elementRef.value as HTMLElement) 19 | ) { 20 | return true; 21 | } 22 | } 23 | return false; 24 | }); 25 | 26 | const hasSelectedParent = computed(() => { 27 | if (!elementRef.value) return false; 28 | 29 | for (const node of selectedElements.value) { 30 | if ( 31 | node && 32 | isDescendant(elementRef.value as HTMLElement, node as HTMLElement) 33 | ) { 34 | return true; 35 | } 36 | } 37 | return false; 38 | }); 39 | 40 | const handleUnselect = () => { 41 | if (!elementRef.value) return; 42 | selectedElements.value.delete(elementRef.value); 43 | }; 44 | 45 | const handleSelect = () => { 46 | if (!elementRef.value) return; 47 | const element = elementsMap.value.get(elementRef.value); 48 | if (!element) return; 49 | 50 | if (isParentOfSelected.value) { 51 | // Удаляем все дочерние выбранные элементы 52 | for (const node of selectedElements.value) { 53 | if ( 54 | node && 55 | isDescendant(node as HTMLElement, elementRef.value as HTMLElement) 56 | ) { 57 | selectedElements.value.delete(node); 58 | } 59 | } 60 | } 61 | 62 | if (hasSelectedParent.value) { 63 | // Удаляем все родительские выбранные элементы 64 | for (const node of selectedElements.value) { 65 | if ( 66 | node && 67 | isDescendant(elementRef.value as HTMLElement, node as HTMLElement) 68 | ) { 69 | selectedElements.value.delete(node); 70 | } 71 | } 72 | } 73 | 74 | selectedElements.value.add(elementRef.value); 75 | }; 76 | 77 | const handleToggleSelect = () => { 78 | if (!elementRef.value) return; 79 | 80 | selectedElements.value.has(elementRef.value) 81 | ? handleUnselect() 82 | : handleSelect(); 83 | }; 84 | 85 | return { 86 | handleUnselect, 87 | handleSelect, 88 | handleToggleSelect, 89 | isSelected, 90 | isParentOfSelected, 91 | }; 92 | }; 93 | -------------------------------------------------------------------------------- /packages/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import { DnDOperations } from './utils/operations'; 2 | import { VueDndKitPlugin } from './plugin'; 3 | import { getBoundingBox } from './utils/geometry'; 4 | import { useDnDStore } from './composables/useDnDStore'; 5 | import { useDragContainer } from './composables/useDragContainer'; 6 | import { useDraggable } from './composables/useDraggable'; 7 | import { useDroppable } from './composables/useDroppable'; 8 | import { useSelection } from './composables/useSelection'; 9 | 10 | export default VueDndKitPlugin; 11 | 12 | export { 13 | useDraggable, 14 | useDroppable, 15 | useDnDStore, 16 | useSelection, 17 | getBoundingBox, 18 | DnDOperations, 19 | useDragContainer, 20 | }; 21 | 22 | export type { 23 | IDnDStore, 24 | IActiveContainer, 25 | IDragElement, 26 | IDraggingElement, 27 | IDropZone, 28 | TLayerProps, 29 | IPluginOptions, 30 | } from './types'; 31 | -------------------------------------------------------------------------------- /packages/core/src/managers/useElementManager.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref, shallowRef, useId } from 'vue'; 2 | 3 | import type { IUseDragOptions } from '../types'; 4 | import { draggableDataName } from '../utils/namespaces'; 5 | import { preventEvent } from '../utils/events'; 6 | import { useDnDStore } from '../composables/useDnDStore'; 7 | 8 | export const useElementManager = (options?: IUseDragOptions) => { 9 | const { 10 | elementsMap, 11 | draggingElements, 12 | hovered, 13 | selectedElements, 14 | isDragging: isDragStarted, 15 | visibleElements, 16 | handleDragElementIntersection, 17 | } = useDnDStore(); 18 | 19 | const elementRef = ref(null); 20 | const isOvered = computed( 21 | () => 22 | visibleElements.value.has(elementRef.value as HTMLElement) && 23 | hovered.element.value === elementRef.value 24 | ); 25 | 26 | const id = shallowRef(options?.id || useId()); 27 | 28 | const isDragging = computed(() => { 29 | if (!elementRef.value) return false; 30 | if (!elementsMap.value.has(elementRef.value)) return false; 31 | return draggingElements.value.has(elementRef.value); 32 | }); 33 | 34 | const isAllowed = computed(() => { 35 | if (!elementRef.value || !isDragStarted.value) return false; 36 | if (!visibleElements.value.has(elementRef.value)) return false; 37 | 38 | const currentElement = elementsMap.value.get(elementRef.value); 39 | if (!currentElement?.groups.length) return true; 40 | 41 | return !Array.from(draggingElements.value.entries()).some( 42 | ([_, draggingElement]) => { 43 | if (!draggingElement.groups.length) return false; 44 | return !draggingElement.groups.some((group) => 45 | currentElement.groups.includes(group) 46 | ); 47 | } 48 | ); 49 | }); 50 | 51 | const isLazyAllowed = computed(() => { 52 | if (!elementRef.value || !isDragStarted.value) return false; 53 | if (!visibleElements.value.has(elementRef.value)) return false; 54 | if (hovered.element.value !== elementRef.value) return false; 55 | 56 | const currentElement = elementsMap.value.get(elementRef.value); 57 | if (!currentElement?.groups.length) return true; 58 | 59 | return !Array.from(draggingElements.value.entries()).some( 60 | ([_, draggingElement]) => { 61 | if (!draggingElement.groups.length) return false; 62 | return !draggingElement.groups.some((group) => 63 | currentElement.groups.includes(group) 64 | ); 65 | } 66 | ); 67 | }); 68 | 69 | const registerElement = () => { 70 | if (!elementRef.value) throw new Error('ElementRef is not set'); 71 | 72 | elementsMap.value.set(elementRef.value, { 73 | node: elementRef.value, 74 | groups: options?.groups ?? [], 75 | layer: options?.layer ?? null, 76 | defaultLayer: options?.layer ?? null, 77 | events: options?.events ?? {}, 78 | data: options?.data ?? null, 79 | id: id.value, 80 | }); 81 | 82 | handleDragElementIntersection('add', elementRef.value); 83 | 84 | elementRef.value.addEventListener('dragstart', preventEvent); 85 | elementRef.value.addEventListener('drag', preventEvent); 86 | elementRef.value.setAttribute(draggableDataName, 'true'); 87 | elementRef.value.setAttribute('draggable', 'false'); 88 | }; 89 | 90 | const unregisterElement = () => { 91 | if (!elementRef.value) return; 92 | 93 | elementsMap.value.delete(elementRef.value); 94 | selectedElements.value.delete(elementRef.value); 95 | 96 | elementRef.value.removeEventListener('dragstart', preventEvent); 97 | elementRef.value.removeEventListener('drag', preventEvent); 98 | elementRef.value.removeAttribute(draggableDataName); 99 | elementRef.value.removeAttribute('draggable'); 100 | }; 101 | 102 | return { 103 | elementRef, 104 | registerElement, 105 | unregisterElement, 106 | isDragging, 107 | isOvered, 108 | isAllowed, 109 | isLazyAllowed, 110 | id, 111 | }; 112 | }; 113 | -------------------------------------------------------------------------------- /packages/core/src/managers/useZoneManager.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref } from 'vue'; 2 | 3 | import type { IUseDropOptions } from '../types'; 4 | import { useDnDStore } from '../composables/useDnDStore'; 5 | 6 | export const useZoneManager = (options?: IUseDropOptions) => { 7 | const { 8 | zonesMap, 9 | hovered, 10 | draggingElements, 11 | isDragging, 12 | handleDropZoneIntersection, 13 | } = useDnDStore(); 14 | 15 | const elementRef = ref(null); 16 | 17 | const isOvered = computed( 18 | () => hovered.zone.value === elementRef.value 19 | ); 20 | 21 | const isAllowed = computed(() => { 22 | if (!elementRef.value || !isDragging.value) return false; 23 | 24 | const currentZone = zonesMap.value.get(elementRef.value); 25 | if (!currentZone?.groups.length) return true; 26 | 27 | return !Array.from(draggingElements.value.values()).some((element) => { 28 | if (!element.groups.length) return false; 29 | return !element.groups.some((group) => 30 | currentZone.groups.includes(group) 31 | ); 32 | }); 33 | }); 34 | 35 | const isLazyAllowed = computed(() => { 36 | if (!elementRef.value || !isDragging.value) return false; 37 | if (hovered.zone.value !== elementRef.value) return false; 38 | 39 | const currentZone = zonesMap.value.get(elementRef.value); 40 | if (!currentZone?.groups.length) return true; 41 | 42 | return !Array.from(draggingElements.value.values()).some((element) => { 43 | return !element.groups.some((group) => 44 | currentZone.groups.includes(group) 45 | ); 46 | }); 47 | }); 48 | 49 | const registerZone = () => { 50 | if (!elementRef.value) throw new Error('elementRef is not set'); 51 | 52 | handleDropZoneIntersection('add', elementRef.value); 53 | 54 | zonesMap.value.set(elementRef.value, { 55 | node: elementRef.value, 56 | groups: options?.groups ?? [], 57 | events: options?.events ?? {}, 58 | data: options?.data ?? undefined, 59 | }); 60 | 61 | elementRef.value.setAttribute('data-dnd-droppable', 'true'); 62 | }; 63 | 64 | const unregisterZone = () => { 65 | if (!elementRef.value) return; 66 | 67 | handleDropZoneIntersection('remove', elementRef.value); 68 | 69 | zonesMap.value.delete(elementRef.value); 70 | }; 71 | 72 | return { 73 | elementRef, 74 | registerZone, 75 | unregisterZone, 76 | isOvered, 77 | isAllowed, 78 | isLazyAllowed, 79 | }; 80 | }; 81 | -------------------------------------------------------------------------------- /packages/core/src/plugin.ts: -------------------------------------------------------------------------------- 1 | import { createVNode, render } from 'vue'; 2 | 3 | import type { App } from 'vue'; 4 | import DragOverlay from './components/DragOverlay.vue'; 5 | import { IPluginOptions } from './types'; 6 | import { useDnDStore } from './composables/useDnDStore'; 7 | 8 | export const VueDndKitPlugin = { 9 | install(app: App, options?: IPluginOptions) { 10 | app.component('DragOverlay', DragOverlay); 11 | const originalMount = app.mount; 12 | app.mount = function (rootContainer) { 13 | const instance = originalMount.call(this, rootContainer); 14 | 15 | const rootEl = 16 | typeof rootContainer === 'string' 17 | ? document.querySelector(rootContainer) 18 | : rootContainer; 19 | 20 | if (rootEl && rootEl instanceof Element) { 21 | if (!rootEl.querySelector('#vue-dnd-kit-overlay')) { 22 | const overlayContainer = document.createElement('div'); 23 | 24 | overlayContainer.id = 'vue-dnd-kit-overlay'; 25 | overlayContainer.style.pointerEvents = 'none'; 26 | 27 | rootEl.appendChild(overlayContainer); 28 | 29 | // Передаем опции напрямую в компонент 30 | const vnode = createVNode(DragOverlay, { 31 | styles: options?.defaultOverlay?.styles, 32 | }); 33 | render(vnode, overlayContainer); 34 | 35 | app.__VUE_DND_KIT_OVERLAY__ = { 36 | container: overlayContainer, 37 | vnode, 38 | options: options?.defaultOverlay || {}, 39 | }; 40 | 41 | const store = useDnDStore(); 42 | app.__VUE_DND_KIT_STORE__ = store; 43 | } 44 | } 45 | 46 | return instance; 47 | }; 48 | 49 | // Модифицируем метод unmount для очистки 50 | const originalUnmount = app.unmount; 51 | app.unmount = function () { 52 | if (app.__VUE_DND_KIT_OVERLAY__) { 53 | render(null, app.__VUE_DND_KIT_OVERLAY__.container); 54 | delete app.__VUE_DND_KIT_OVERLAY__; 55 | } 56 | return originalUnmount.call(this); 57 | }; 58 | }, 59 | }; 60 | 61 | declare module '@vue/runtime-core' { 62 | export interface App { 63 | __VUE_DND_KIT_OVERLAY__?: { 64 | container: HTMLElement; 65 | vnode: any; 66 | options: IPluginOptions['defaultOverlay']; 67 | }; 68 | __VUE_DND_KIT_STORE__?: ReturnType; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/core/src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Component, 3 | ComputedRef, 4 | CSSProperties, 5 | Ref, 6 | ShallowRef, 7 | } from 'vue'; 8 | import { useDnDStore } from '../composables/useDnDStore'; 9 | 10 | export interface IDnDStore extends ReturnType {} 11 | 12 | export interface IActiveContainer { 13 | component: Ref; 14 | ref: Ref; 15 | } 16 | 17 | export interface IPluginOptions { 18 | defaultOverlay?: { 19 | styles?: CSSProperties; 20 | }; 21 | } 22 | 23 | export interface IPointerPosition { 24 | start: Ref; 25 | current: Ref; 26 | offset: { 27 | percent: Ref; 28 | pixel: Ref; 29 | }; 30 | } 31 | 32 | export interface IDragElement { 33 | id: string | number; 34 | node: HTMLElement | Element | null; 35 | groups: string[]; 36 | layer: Component | null; 37 | defaultLayer: Component | null; 38 | data: { 39 | source?: any[]; 40 | index?: number; 41 | [key: string]: any; 42 | } | null; 43 | events: { 44 | onHover?: (store: IDnDStore, payload: IDnDPayload) => void; 45 | onLeave?: (store: IDnDStore, payload: IDnDPayload) => void; 46 | onEnd?: (store: IDnDStore, payload: IDnDPayload) => void; 47 | onStart?: (store: IDnDStore, payload: IDnDPayload) => void; 48 | onMove?: (store: IDnDStore, payload: IDnDPayload) => void; 49 | }; 50 | } 51 | 52 | export type TLayerProps = Pick; 53 | 54 | export interface IDnDPayload { 55 | items: IDraggingElement[]; 56 | } 57 | 58 | export interface IDraggingElement extends IDragElement { 59 | initialHTML: string; 60 | initialRect?: DOMRect; 61 | } 62 | 63 | export interface IDropZone { 64 | node: HTMLElement | Element | null; 65 | groups: string[]; 66 | data?: { 67 | source?: any[]; 68 | [key: string]: any; 69 | }; 70 | events: { 71 | onHover?: (store: IDnDStore, payload: IDnDPayload) => void; 72 | onLeave?: (store: IDnDStore, payload: IDnDPayload) => void; 73 | onDrop?: ( 74 | store: IDnDStore, 75 | payload: IDnDPayload 76 | ) => void | Promise; 77 | }; 78 | } 79 | 80 | export interface IPoint { 81 | x: number; 82 | y: number; 83 | } 84 | 85 | export interface IUseDropOptions { 86 | groups?: string[]; 87 | events?: { 88 | onDrop?: ( 89 | store: IDnDStore, 90 | payload: IDnDPayload 91 | ) => void | Promise; 92 | onHover?: (store: IDnDStore, payload: IDnDPayload) => void; 93 | onLeave?: (store: IDnDStore, payload: IDnDPayload) => void; 94 | }; 95 | data?: { 96 | source?: any[]; 97 | [key: string]: any; 98 | }; 99 | } 100 | export interface IUseDragOptions extends IUseSensorOptions { 101 | id?: string | number; 102 | groups?: string[]; 103 | events?: { 104 | onEnd?: (store: IDnDStore, payload: IDnDPayload) => void; 105 | onStart?: (store: IDnDStore, payload: IDnDPayload) => void; 106 | onMove?: (store: IDnDStore, payload: IDnDPayload) => void; 107 | onHover?: (store: IDnDStore, payload: IDnDPayload) => void; 108 | onLeave?: (store: IDnDStore, payload: IDnDPayload) => void; 109 | }; 110 | keyboard?: { 111 | moveStep?: number; 112 | }; 113 | data?: { 114 | source?: any[]; 115 | index?: number; 116 | [key: string]: any; 117 | }; 118 | layer?: Component | null; 119 | container?: Component; 120 | } 121 | 122 | export type ISensor = ( 123 | store: IDnDStore 124 | ) => HTMLElement | HTMLElement[] | Element | Element[] | null; 125 | 126 | export interface IBoundingBox { 127 | x: number; 128 | y: number; 129 | width: number; 130 | height: number; 131 | bottom: number; 132 | left: number; 133 | right: number; 134 | top: number; 135 | } 136 | 137 | export interface IUseSensorOptions { 138 | sensor?: { 139 | throttle?: number; 140 | setup?: ISensor; 141 | }; 142 | } 143 | 144 | export interface ICollisionDetectionResult { 145 | element: HTMLElement | Element | null; 146 | zone: HTMLElement | Element | null; 147 | } 148 | -------------------------------------------------------------------------------- /packages/core/src/utils/dom.ts: -------------------------------------------------------------------------------- 1 | export const isDescendant = ( 2 | element: HTMLElement | Element | null, 3 | container: HTMLElement | Element 4 | ): boolean => { 5 | if (!element) return false; 6 | return container.contains(element); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/core/src/utils/events.ts: -------------------------------------------------------------------------------- 1 | import { IDnDPayload, IDnDStore } from '../types'; 2 | 3 | export const preventEvent = (event: Event) => { 4 | event.preventDefault(); 5 | }; 6 | 7 | export const createPayload = (store: IDnDStore): IDnDPayload => { 8 | return { 9 | items: Array.from(store.draggingElements.value.values()), 10 | }; 11 | }; 12 | -------------------------------------------------------------------------------- /packages/core/src/utils/geometry.ts: -------------------------------------------------------------------------------- 1 | import type { IBoundingBox, IPoint } from '../types'; 2 | 3 | export const checkCollision = ( 4 | boxA: IBoundingBox, 5 | boxB: IBoundingBox 6 | ): boolean => { 7 | return ( 8 | boxA.x < boxB.x + boxB.width && 9 | boxA.x + boxA.width > boxB.x && 10 | boxA.y < boxB.y + boxB.height && 11 | boxA.y + boxA.height > boxB.y 12 | ); 13 | }; 14 | 15 | export const getBoundingBox = (element: HTMLElement | null): IBoundingBox => { 16 | if (!element) 17 | return { 18 | x: 0, 19 | y: 0, 20 | width: 0, 21 | height: 0, 22 | bottom: 0, 23 | left: 0, 24 | right: 0, 25 | top: 0, 26 | }; 27 | 28 | const rect = element.getBoundingClientRect(); 29 | 30 | return { 31 | bottom: rect.bottom, 32 | left: rect.left, 33 | right: rect.right, 34 | top: rect.top, 35 | x: rect.x, 36 | y: rect.y, 37 | width: rect.width, 38 | height: rect.height, 39 | }; 40 | }; 41 | 42 | export const getCenter = (box: IBoundingBox): IPoint => ({ 43 | x: box.x + box.width / 2, 44 | y: box.y + box.height / 2, 45 | }); 46 | 47 | export const getOffset = (element: HTMLElement | null, pointer: IPoint) => { 48 | const rect = getBoundingBox(element); 49 | return { 50 | pixel: { 51 | x: pointer.x - rect.x, 52 | y: pointer.y - rect.y, 53 | }, 54 | percent: { 55 | x: ((pointer.x - rect.x) / rect.width) * 100, 56 | y: ((pointer.y - rect.y) / rect.height) * 100, 57 | }, 58 | }; 59 | }; 60 | 61 | export const getDistance = (pointA: IPoint, pointB: IPoint): number => { 62 | const dx = pointB.x - pointA.x; 63 | const dy = pointB.y - pointA.y; 64 | return Math.sqrt(dx * dx + dy * dy); 65 | }; 66 | 67 | export const getOverlapPercent = ( 68 | boxA: IBoundingBox, 69 | boxB: IBoundingBox 70 | ): number => { 71 | const xOverlap = Math.max( 72 | 0, 73 | Math.min(boxA.x + boxA.width, boxB.x + boxB.width) - 74 | Math.max(boxA.x, boxB.x) 75 | ); 76 | const yOverlap = Math.max( 77 | 0, 78 | Math.min(boxA.y + boxA.height, boxB.y + boxB.height) - 79 | Math.max(boxA.y, boxB.y) 80 | ); 81 | 82 | const overlapArea = xOverlap * yOverlap; 83 | 84 | const boxAArea = boxA.width * boxA.height; 85 | const boxBArea = boxB.width * boxB.height; 86 | 87 | // Возвращаем среднее значение процентов перекрытия относительно обоих элементов 88 | return ((overlapArea / boxAArea) * 100 + (overlapArea / boxBArea) * 100) / 2; 89 | }; 90 | -------------------------------------------------------------------------------- /packages/core/src/utils/namespaces.ts: -------------------------------------------------------------------------------- 1 | export const draggableDataName = 'data-vue-dnd-kit-draggable'; 2 | export const droppableDataName = 'data-vue-dnd-kit-droppable'; 3 | 4 | export const VUE_DND_KIT_OPTIONS = Symbol('vue-dnd-kit-options'); 5 | -------------------------------------------------------------------------------- /packages/core/src/vue-shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue'; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | "moduleResolution": "node", 9 | "allowSyntheticDefaultImports": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "strict": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "declaration": true, 17 | "declarationDir": "./dist", 18 | "outDir": "./dist" 19 | }, 20 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"], 21 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import dts from 'vite-plugin-dts'; 3 | import { resolve } from 'path'; 4 | import vue from '@vitejs/plugin-vue'; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | vue(), 9 | dts({ 10 | include: ['src/**/*.ts', 'src/**/*.vue', 'src/types/**/*.ts'], 11 | exclude: ['node_modules/**', 'src/**/*.spec.ts'], 12 | rollupTypes: true, 13 | insertTypesEntry: true, 14 | // Оставляем copyDtsFiles: true для копирования .d.ts файлов 15 | copyDtsFiles: true, 16 | }), 17 | ], 18 | build: { 19 | lib: { 20 | entry: resolve(__dirname, 'src/index.ts'), 21 | name: 'VueDndKitCore', 22 | fileName: (format) => `vue-dnd-kit-core.${format}.js`, 23 | formats: ['es', 'cjs'], 24 | }, 25 | rollupOptions: { 26 | external: ['vue', '@vueuse/core'], 27 | output: { 28 | globals: { 29 | vue: 'Vue', 30 | '@vueuse/core': 'VueUse', 31 | }, 32 | // Удаляем preserveModules для создания единого бандла 33 | // preserveModules: true, 34 | // preserveModulesRoot: 'src', 35 | }, 36 | }, 37 | sourcemap: true, 38 | emptyOutDir: true, 39 | }, 40 | resolve: { 41 | alias: { 42 | '@vue-dnd-kit/core': resolve(__dirname, './src'), 43 | '@': resolve(__dirname, './src'), 44 | './types': resolve(__dirname, './src/types'), 45 | }, 46 | dedupe: ['vue'], 47 | }, 48 | optimizeDeps: { 49 | include: ['vue'], 50 | exclude: ['@vueuse/core'], 51 | }, 52 | }); 53 | -------------------------------------------------------------------------------- /packages/devtools/README.md: -------------------------------------------------------------------------------- 1 | # Vue Drag & Drop Library - DevTools Package 2 | 3 | [![Release](https://img.shields.io/badge/status-release-green.svg)](https://github.com/zizigy/vue-dnd-kit) 4 | 5 |

6 | 7 | Vue Drag & Drop Logo 8 | 9 |

10 | 11 |

12 | DevTools package of the Vue Drag & Drop library with enhanced debugging capabilities. 13 |

14 | 15 |

16 | 17 | Documentation 18 | 19 |

20 | 21 |

22 | Extends Vue DevTools with specialized panels for debugging drag and drop operations 23 |

24 | 25 | ## Enhanced Debugging 26 | 27 | Vue DnD Kit DevTools provides specialized debugging tools for your drag and drop applications: 28 | 29 | - 🔍 **Store state visualization** - inspect the complete state of your DnD system 30 | - 📊 **Visual feedback** - see your drag elements, drop zones, and their relationships 31 | - 🔄 **Real-time updates** - observe state changes as they happen during interactions 32 | - 🧪 **Testing aid** - quickly identify issues in complex drag and drop scenarios 33 | 34 | ## Powerful Inspector Panels 35 | 36 | The DevTools package extends Vue DevTools with multiple specialized panels: 37 | 38 | - 📱 **Full Store Overview** 39 | 40 | - Complete visibility into the DnD store 41 | - Access to all internal state values 42 | - Reactive updates as operations occur 43 | 44 | - 🖱️ **Pointer Tracking** 45 | 46 | - Monitor cursor/touch position 47 | - View start and current positions 48 | - Inspect pixel and percentage offsets 49 | 50 | - 🧩 **Elements Inspector** 51 | 52 | - View all registered draggable elements 53 | - Track visibility status 54 | - Monitor selection state 55 | 56 | - 🎯 **Drop Zones Panel** 57 | 58 | - Inspect registered drop zones 59 | - Check visibility and hover states 60 | - Debug zone constraints 61 | 62 | - 📦 **Container Tools** 63 | 64 | - View active containers 65 | - Monitor component references 66 | - Debug container relationships 67 | 68 | - 👆 **Hover States** 69 | 70 | - Track elements under the cursor 71 | - Monitor intersection states 72 | - Debug hover-related issues 73 | 74 | ## Installation 75 | 76 | Choose your preferred package manager: 77 | 78 | ```bash 79 | npm install @vue-dnd-kit/devtools @vue-dnd-kit/core @vueuse/core 80 | ``` 81 | 82 | ```bash 83 | yarn add @vue-dnd-kit/devtools @vue-dnd-kit/core @vueuse/core 84 | ``` 85 | 86 | ```bash 87 | pnpm install @vue-dnd-kit/devtools @vue-dnd-kit/core @vueuse/core 88 | ``` 89 | 90 | ## Setup 91 | 92 | Register the DevTools extension in your main.ts/js file: 93 | 94 | ```typescript 95 | import { createApp } from 'vue'; 96 | import App from './App.vue'; 97 | import VueDnDKitPlugin from '@vue-dnd-kit/core'; 98 | import VueDnDKitDevtools from '@vue-dnd-kit/devtools'; 99 | 100 | const app = createApp(App); 101 | app.use(VueDnDKitPlugin); 102 | app.use(VueDnDKitDevtools); 103 | app.mount('#app'); 104 | ``` 105 | 106 | ## Usage 107 | 108 | Once installed and registered, the DevTools extension will automatically: 109 | 110 | 1. Register with Vue DevTools 111 | 2. Add a "DnD Kit" panel to the inspector 112 | 3. Display real-time state information as you interact with your application 113 | 114 | No additional code is required in your components to enable the debugging features. 115 | 116 | ## 📄 License 117 | 118 | [MIT](LICENSE) © [ZiZiGY](https://github.com/ZiZiGY) 119 | 120 | --- 121 | 122 |

🔍 Enhance your debugging experience!

123 |

Made with ❤️ for the Vue.js community

124 | -------------------------------------------------------------------------------- /packages/devtools/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-dnd-kit/devtools", 3 | "version": "1.1.0", 4 | "description": "Vue DnD Kit DevTools Plugin", 5 | "main": "dist/vue-dnd-kit-devtools.cjs.js", 6 | "module": "dist/vue-dnd-kit-devtools.esm.js", 7 | "types": "dist/types/index.d.ts", 8 | "files": [ 9 | "dist" 10 | ], 11 | "scripts": { 12 | "build": "rollup -c", 13 | "dev": "rollup -c -w" 14 | }, 15 | "peerDependencies": { 16 | "vue": "^3.0.0", 17 | "@vue-dnd-kit/core": "workspace:*" 18 | }, 19 | "dependencies": { 20 | "@rollup/plugin-terser": "^0.4.4", 21 | "@vue/devtools-api": "^6.0.0" 22 | }, 23 | "devDependencies": { 24 | "@rollup/plugin-commonjs": "^28.0.3", 25 | "@rollup/plugin-node-resolve": "^16.0.1", 26 | "@rollup/plugin-terser": "^0.4.4", 27 | "@vue/devtools-api": "^6.0.0", 28 | "rollup": "^4.40.0", 29 | "rollup-plugin-terser": "^7.0.2", 30 | "rollup-plugin-typescript2": "^0.36.0", 31 | "typescript": "^4.5.2", 32 | "vue": "^3.2.0" 33 | }, 34 | "publishConfig": { 35 | "access": "public", 36 | "registry": "https://registry.npmjs.org/" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/devtools/rollup.config.js: -------------------------------------------------------------------------------- 1 | const typescript = require('rollup-plugin-typescript2'); 2 | const terser = require('@rollup/plugin-terser'); 3 | const resolve = require('@rollup/plugin-node-resolve'); 4 | const commonjs = require('@rollup/plugin-commonjs'); 5 | 6 | const pkg = require('./package.json'); 7 | 8 | module.exports = [ 9 | { 10 | input: 'src/index.ts', 11 | output: [ 12 | { 13 | file: pkg.main, 14 | format: 'cjs', 15 | exports: 'named', 16 | sourcemap: true, 17 | }, 18 | { 19 | file: pkg.module, 20 | format: 'esm', 21 | exports: 'named', 22 | sourcemap: true, 23 | }, 24 | { 25 | file: 'dist/vue-dnd-kit-devtools.global.js', 26 | format: 'iife', 27 | name: 'VueDndKitDevtools', 28 | sourcemap: true, 29 | globals: { 30 | vue: 'Vue', 31 | '@vue/devtools-api': 'VueDevtoolsApi', 32 | }, 33 | }, 34 | { 35 | file: 'dist/vue-dnd-kit-devtools.global.min.js', 36 | format: 'iife', 37 | name: 'VueDndKitDevtools', 38 | sourcemap: true, 39 | globals: { 40 | vue: 'Vue', 41 | '@vue/devtools-api': 'VueDevtoolsApi', 42 | }, 43 | plugins: [terser()], 44 | }, 45 | ], 46 | external: ['vue', '@vue/devtools-api'], 47 | plugins: [ 48 | typescript({ 49 | tsconfigOverride: { 50 | compilerOptions: { 51 | declaration: true, 52 | declarationDir: 'dist/types', 53 | }, 54 | include: ['src/**/*.ts'], 55 | exclude: ['node_modules', 'dist'], 56 | }, 57 | abortOnError: false, 58 | }), 59 | resolve(), 60 | commonjs(), 61 | ], 62 | }, 63 | ]; 64 | -------------------------------------------------------------------------------- /packages/devtools/src/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | __VUE__?: any; 3 | __VUE_DEVTOOLS_GLOBAL_HOOK__?: { 4 | Vue?: any; 5 | enabled?: boolean; 6 | plugins?: Array; 7 | emit?: (event: string, ...args: any[]) => void; 8 | on?: (event: string, fn: Function) => void; 9 | once?: (event: string, fn: Function) => void; 10 | off?: (event: string, fn: Function) => void; 11 | }; 12 | } 13 | -------------------------------------------------------------------------------- /packages/devtools/src/index.ts: -------------------------------------------------------------------------------- 1 | import { App } from 'vue'; 2 | import { setupDevtools } from './devtools'; 3 | 4 | function setupDndKitDevtools(app: App) { 5 | if (process.env.NODE_ENV === 'development' || __VUE_PROD_DEVTOOLS__) { 6 | return setupDevtools(app); 7 | } 8 | return {}; 9 | } 10 | 11 | export default setupDndKitDevtools; 12 | 13 | declare global { 14 | var __VUE_PROD_DEVTOOLS__: boolean; 15 | } 16 | -------------------------------------------------------------------------------- /packages/devtools/src/utils/container.ts: -------------------------------------------------------------------------------- 1 | import { CustomInspectorState } from '@vue/devtools-api'; 2 | import { useDnDStore } from '@vue-dnd-kit/core'; 3 | 4 | export const createContainer = ( 5 | store: ReturnType 6 | ): CustomInspectorState => ({ 7 | state: [ 8 | { 9 | key: 'component', 10 | value: store.activeContainer.component.value, 11 | objectType: 'ref', 12 | }, 13 | { 14 | key: 'ref', 15 | value: store.activeContainer.ref.value, 16 | objectType: 'ref', 17 | }, 18 | { 19 | key: 'animating', 20 | value: { 21 | appear: store.activeContainer.animating.appear.value, 22 | enter: store.activeContainer.animating.enter.value, 23 | leave: store.activeContainer.animating.leave.value, 24 | }, 25 | objectType: 'ref', 26 | }, 27 | { 28 | key: 'options', 29 | value: store.activeContainer.options.value, 30 | objectType: 'ref', 31 | }, 32 | ], 33 | }); 34 | -------------------------------------------------------------------------------- /packages/devtools/src/utils/elements.ts: -------------------------------------------------------------------------------- 1 | import { CustomInspectorState } from '@vue/devtools-api'; 2 | import { useDnDStore } from '@vue-dnd-kit/core'; 3 | 4 | export const createElements = ( 5 | store: ReturnType 6 | ): CustomInspectorState => ({ 7 | state: [ 8 | { 9 | key: 'elementsMap', 10 | value: store.elementsMap.value, 11 | objectType: 'ref', 12 | }, 13 | { 14 | key: 'visibleElements', 15 | value: store.visibleElements.value, 16 | objectType: 'ref', 17 | }, 18 | { 19 | key: 'selectedElements', 20 | value: store.selectedElements.value, 21 | objectType: 'ref', 22 | }, 23 | { 24 | key: 'draggingElements', 25 | value: store.draggingElements.value, 26 | objectType: 'ref', 27 | }, 28 | ], 29 | }); 30 | -------------------------------------------------------------------------------- /packages/devtools/src/utils/hovered.ts: -------------------------------------------------------------------------------- 1 | import { CustomInspectorState } from '@vue/devtools-api'; 2 | import { useDnDStore } from '@vue-dnd-kit/core'; 3 | 4 | export const createHovered = ( 5 | store: ReturnType 6 | ): CustomInspectorState => ({ 7 | state: [ 8 | { 9 | key: 'element', 10 | value: store.hovered.element.value, 11 | }, 12 | { 13 | key: 'zone', 14 | value: store.hovered.zone.value, 15 | }, 16 | ], 17 | }); 18 | -------------------------------------------------------------------------------- /packages/devtools/src/utils/pointer.ts: -------------------------------------------------------------------------------- 1 | import type { CustomInspectorState } from '@vue/devtools-api'; 2 | import { useDnDStore } from '@vue-dnd-kit/core'; 3 | 4 | export const createPointer = ( 5 | store: ReturnType 6 | ): CustomInspectorState => ({ 7 | state: [ 8 | { 9 | key: 'current', 10 | value: store.pointerPosition.current.value, 11 | objectType: 'ref', 12 | }, 13 | { 14 | key: 'start', 15 | value: store.pointerPosition.start.value, 16 | objectType: 'ref', 17 | }, 18 | { 19 | key: 'offset', 20 | value: { 21 | percent: store.pointerPosition.offset.percent.value, 22 | pixel: store.pointerPosition.offset.pixel.value, 23 | }, 24 | }, 25 | ], 26 | }); 27 | -------------------------------------------------------------------------------- /packages/devtools/src/utils/store.ts: -------------------------------------------------------------------------------- 1 | import { CustomInspectorState } from '@vue/devtools-api'; 2 | import { useDnDStore } from '@vue-dnd-kit/core'; 3 | 4 | export const createStore = ( 5 | store: ReturnType 6 | ): CustomInspectorState => ({ 7 | state: [ 8 | { 9 | key: 'isDragging', 10 | value: store.isDragging.value, 11 | objectType: 'computed', 12 | }, 13 | { 14 | key: 'elementsMap', 15 | value: store.elementsMap.value, 16 | objectType: 'ref', 17 | }, 18 | { 19 | key: 'zonesMap', 20 | value: store.zonesMap.value, 21 | objectType: 'ref', 22 | }, 23 | { 24 | key: 'visibleElements', 25 | value: store.visibleElements.value, 26 | objectType: 'ref', 27 | }, 28 | { 29 | key: 'activeContainer', 30 | value: { 31 | component: store.activeContainer.component.value, 32 | ref: store.activeContainer.ref.value, 33 | animating: { 34 | appear: store.activeContainer.animating.appear.value, 35 | enter: store.activeContainer.animating.enter.value, 36 | leave: store.activeContainer.animating.leave.value, 37 | }, 38 | options: store.activeContainer.options.value, 39 | }, 40 | }, 41 | { 42 | key: 'visibleZones', 43 | value: store.visibleZones.value, 44 | objectType: 'ref', 45 | }, 46 | { 47 | key: 'draggingElements', 48 | value: store.draggingElements.value, 49 | objectType: 'ref', 50 | }, 51 | { 52 | key: 'pointerPosition', 53 | value: { 54 | start: store.pointerPosition.start.value, 55 | current: store.pointerPosition.current.value, 56 | offset: { 57 | percent: store.pointerPosition.offset.percent.value, 58 | pixel: store.pointerPosition.offset.pixel.value, 59 | }, 60 | }, 61 | }, 62 | 63 | { 64 | key: 'selectedElements', 65 | value: store.selectedElements.value, 66 | objectType: 'ref', 67 | }, 68 | { 69 | key: 'keyboard', 70 | editable: true, 71 | value: { 72 | w: store.keyboard.w.value, 73 | s: store.keyboard.s.value, 74 | a: store.keyboard.a.value, 75 | d: store.keyboard.d.value, 76 | }, 77 | }, 78 | { 79 | key: 'hovered', 80 | value: { 81 | zone: store.hovered.zone.value, 82 | element: store.hovered.element.value, 83 | }, 84 | }, 85 | ], 86 | }); 87 | -------------------------------------------------------------------------------- /packages/devtools/src/utils/zones.ts: -------------------------------------------------------------------------------- 1 | import { CustomInspectorState } from '@vue/devtools-api'; 2 | import { useDnDStore } from '@vue-dnd-kit/core'; 3 | 4 | export const createZones = ( 5 | store: ReturnType 6 | ): CustomInspectorState => ({ 7 | state: [ 8 | { 9 | key: 'zonesMap', 10 | value: store.zonesMap.value, 11 | objectType: 'ref', 12 | }, 13 | { 14 | key: 'visibleZones', 15 | value: store.visibleZones.value, 16 | objectType: 'ref', 17 | }, 18 | ], 19 | }); 20 | -------------------------------------------------------------------------------- /packages/devtools/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/global.d.ts", "src/**/*.ts", "__tests__/**/*.ts"], 3 | "compilerOptions": { 4 | "outDir": "dist", 5 | "sourceMap": false, 6 | 7 | "target": "esnext", 8 | "module": "esnext", 9 | "moduleResolution": "node", 10 | "allowJs": false, 11 | "skipLibCheck": true, 12 | 13 | "noUnusedLocals": true, 14 | "strictNullChecks": true, 15 | "noImplicitAny": true, 16 | "noImplicitThis": true, 17 | "noImplicitReturns": false, 18 | "strict": true, 19 | "isolatedModules": true, 20 | 21 | "experimentalDecorators": true, 22 | "resolveJsonModule": true, 23 | "esModuleInterop": true, 24 | "removeComments": false, 25 | "jsx": "preserve", 26 | "lib": ["esnext", "dom"], 27 | "types": ["node"] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/utilities/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@vue-dnd-kit/utilities", 3 | "version": "0.0.7", 4 | "description": "Utilities for Vue DnD Kit - a lightweight Vue 3 library for building performant and accessible drag and drop interfaces", 5 | "author": "ZiZIGY", 6 | "license": "MIT", 7 | "keywords": [ 8 | "vue", 9 | "dnd", 10 | "drag", 11 | "drop", 12 | "drag and drop", 13 | "drag and drop library", 14 | "drag and drop component", 15 | "vue dnd", 16 | "vue drag and drop", 17 | "vue drag and drop library", 18 | "vue drag and drop component", 19 | "vue dnd kit", 20 | "vue dnd kit utilities", 21 | "dnd-kit", 22 | "dnd-kit utilities" 23 | ], 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/zizigy/vue-dnd-kit.git", 27 | "directory": "packages/core" 28 | }, 29 | "scripts": { 30 | "start": "vite build --watch", 31 | "build": "vite build", 32 | "test": "vitest run", 33 | "lint": "eslint src --ext .ts,.vue", 34 | "prepublish": "npm run build" 35 | }, 36 | "main": "dist/vue-dnd-kit-utilities.cjs.js", 37 | "module": "dist/vue-dnd-kit-utilities.es.js", 38 | "types": "dist/index.d.ts", 39 | "exports": { 40 | ".": { 41 | "types": "./dist/index.d.ts", 42 | "import": "./dist/vue-dnd-kit-utilities.es.js", 43 | "require": "./dist/vue-dnd-kit-utilities.cjs.js" 44 | } 45 | }, 46 | "files": [ 47 | "README.md", 48 | "CHANGELOG.md", 49 | "LICENSE", 50 | "dist" 51 | ], 52 | "peerDependencies": { 53 | "vue": "^3.5.13", 54 | "@vue-dnd-kit/core": "workspace:*" 55 | }, 56 | "devDependencies": { 57 | "@vitejs/plugin-vue": "^4.2.3", 58 | "eslint": "^8.38.0", 59 | "typescript": "^4.9.5", 60 | "vite": "^6.0.5", 61 | "vite-plugin-dts": "^4.5.0", 62 | "vitepress": "^1.6.3", 63 | "vue": "^3.5.13" 64 | }, 65 | "publishConfig": { 66 | "access": "public", 67 | "registry": "https://registry.npmjs.org/" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/utilities/src/composables/useGeometry.ts: -------------------------------------------------------------------------------- 1 | import { computed, type Ref } from 'vue'; 2 | import type { IPoint } from '@vue-dnd-kit/core'; 3 | import { getDirection as _getDirection } from '../utils'; 4 | 5 | /** 6 | * Hook for calculating geometric relationships between two points. 7 | * Independent utility that can be used for any geometric calculations, 8 | * commonly used in drag and drop but not limited to it. 9 | * 10 | * @param pointA - Reference to the first point coordinates 11 | * @param pointB - Reference to the second point coordinates 12 | * @returns Object containing geometric calculations: 13 | * - delta: difference between points 14 | * - direction: cardinal direction from pointA to pointB 15 | * - distance: euclidean distance between points 16 | * - angle: angle in degrees between points 17 | * 18 | * @example 19 | * ```ts 20 | * const start = ref({ x: 0, y: 0 }); 21 | * const end = ref({ x: 100, y: 100 }); 22 | * const { delta, direction, distance, angle } = useGeometry(start, end); 23 | * ``` 24 | */ 25 | export const useGeometry = ( 26 | pointA: Ref, 27 | pointB: Ref 28 | ) => { 29 | /** Vector between two points */ 30 | const delta = computed(() => ({ 31 | x: (pointB.value?.x ?? 0) - (pointA.value?.x ?? 0), 32 | y: (pointB.value?.y ?? 0) - (pointA.value?.y ?? 0), 33 | })); 34 | 35 | /** Cardinal direction from pointA to pointB */ 36 | const direction = computed(() => _getDirection(delta.value)); 37 | 38 | /** Euclidean distance between points */ 39 | const distance = computed(() => { 40 | const dx = (pointB.value?.x ?? 0) - (pointA.value?.x ?? 0); 41 | const dy = (pointB.value?.y ?? 0) - (pointA.value?.y ?? 0); 42 | return Math.sqrt(dx * dx + dy * dy); 43 | }); 44 | 45 | /** Angle in degrees between points (0° is right, 90° is down) */ 46 | const angle = computed(() => { 47 | const dx = (pointB.value?.x ?? 0) - (pointA.value?.x ?? 0); 48 | const dy = (pointB.value?.y ?? 0) - (pointA.value?.y ?? 0); 49 | return Math.atan2(dy, dx) * (180 / Math.PI); 50 | }); 51 | 52 | return { 53 | delta, 54 | direction, 55 | distance, 56 | angle, 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /packages/utilities/src/index.ts: -------------------------------------------------------------------------------- 1 | export { useGeometry } from './composables/useGeometry'; 2 | export { useAutoScroll } from './composables/useAutoScroll'; 3 | 4 | export type { IAutoScrollOptions } from './types'; 5 | -------------------------------------------------------------------------------- /packages/utilities/src/types/index.d.ts: -------------------------------------------------------------------------------- 1 | /** Auto-scroll configuration options */ 2 | export interface IAutoScrollOptions { 3 | /** Distance from edge to start auto-scrolling (px) */ 4 | threshold?: number; 5 | /** Scrolling speed (px/frame) */ 6 | speed?: number; 7 | /** Whether auto-scroll is disabled */ 8 | disabled?: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /packages/utilities/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import type { IPoint } from '@vue-dnd-kit/core'; 2 | 3 | export const getDelta = (pointA: IPoint, pointB: IPoint): IPoint => ({ 4 | x: pointB.x - pointA.x, 5 | y: pointB.y - pointA.y, 6 | }); 7 | 8 | export const getDirection = ( 9 | delta: IPoint 10 | ): 'up' | 'right' | 'down' | 'left' => { 11 | const angle = Math.atan2(delta.y, delta.x); 12 | const deg = angle * (180 / Math.PI); 13 | 14 | if (deg >= -45 && deg <= 45) return 'right'; 15 | if (deg > 45 && deg < 135) return 'down'; 16 | if (deg >= 135 || deg <= -135) return 'left'; 17 | return 'up'; 18 | }; 19 | 20 | export const getAngle = (pointA: IPoint, pointB: IPoint): number => { 21 | const dx = pointB.x - pointA.x; 22 | const dy = pointB.y - pointA.y; 23 | return Math.atan2(dy, dx) * (180 / Math.PI); 24 | }; 25 | -------------------------------------------------------------------------------- /packages/utilities/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | "moduleResolution": "node", 9 | "allowSyntheticDefaultImports": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "strict": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "declaration": true, 17 | "declarationDir": "./dist", 18 | "outDir": "./dist", 19 | "baseUrl": ".", 20 | "paths": { 21 | "@vue-dnd-kit/core": ["../core/src"], 22 | "@vue-dnd-kit/core/*": ["../core/src/*"] 23 | } 24 | }, 25 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.vue"], 26 | "exclude": ["node_modules", "dist", "**/*.test.ts"] 27 | } 28 | -------------------------------------------------------------------------------- /packages/utilities/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import dts from 'vite-plugin-dts'; 3 | import { resolve } from 'path'; 4 | import vue from '@vitejs/plugin-vue'; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | vue(), 9 | dts({ 10 | include: ['src/**/*.ts', 'src/**/*.vue'], 11 | beforeWriteFile: (filePath, content) => { 12 | const fixedContent = content.replace( 13 | /from ['"]\.\.\/types['"];/g, 14 | 'from "../types";' 15 | ); 16 | return { 17 | filePath: filePath.replace('/src/', '/'), 18 | content: fixedContent, 19 | }; 20 | }, 21 | copyDtsFiles: true, 22 | insertTypesEntry: true, 23 | cleanVueFileName: true, 24 | }), 25 | ], 26 | build: { 27 | lib: { 28 | entry: resolve(__dirname, 'src/index.ts'), 29 | name: 'VueDndKitUtilities', 30 | formats: ['es', 'cjs'], 31 | fileName: (format) => `vue-dnd-kit-utilities.${format}.js`, 32 | }, 33 | rollupOptions: { 34 | external: ['vue', '@vue-dnd-kit/core'], 35 | output: { 36 | globals: { 37 | vue: 'Vue', 38 | '@vue-dnd-kit/core': 'VueDndKitCore', 39 | }, 40 | exports: 'named', 41 | }, 42 | }, 43 | cssCodeSplit: false, 44 | cssMinify: true, 45 | }, 46 | css: { 47 | modules: { 48 | localsConvention: 'camelCase', 49 | }, 50 | }, 51 | resolve: { 52 | alias: { 53 | '@vue-dnd-kit/core': resolve(__dirname, '../core/src'), 54 | }, 55 | dedupe: ['vue'], 56 | }, 57 | optimizeDeps: { 58 | include: ['vue'], 59 | exclude: ['@vue-dnd-kit/core'], 60 | }, 61 | server: { 62 | fs: { 63 | allow: ['..'], 64 | }, 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /playground/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /playground/README.md: -------------------------------------------------------------------------------- 1 | # Vue 3 + TypeScript + Vite 2 | 3 | This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` 12 | 13 | 14 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "playground", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vue-tsc -b && vite build", 9 | "preview": "vite preview" 10 | }, 11 | "dependencies": { 12 | "vue": "^3.5.13", 13 | "vue-router": "^4.5.0" 14 | }, 15 | "devDependencies": { 16 | "@vitejs/plugin-vue": "^5.2.1", 17 | "@vue/tsconfig": "^0.7.0", 18 | "typescript": "~5.7.2", 19 | "vite": "^6.2.0", 20 | "vue-tsc": "^2.2.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /playground/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/src/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 38 | 39 | 131 | -------------------------------------------------------------------------------- /playground/src/assets/vue.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /playground/src/components/CustomContainer.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 90 | 91 | 104 | -------------------------------------------------------------------------------- /playground/src/components/Drag.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 41 | 42 | 47 | -------------------------------------------------------------------------------- /playground/src/components/Draggable.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 40 | 41 | 66 | -------------------------------------------------------------------------------- /playground/src/main.ts: -------------------------------------------------------------------------------- 1 | import './style.css'; 2 | 3 | import App from './App.vue'; 4 | import VueDnDKitDevtools from '@vue-dnd-kit/devtools'; 5 | import VueDnDKitPlugin, { type IPluginOptions } from '@vue-dnd-kit/core'; 6 | import { createApp } from 'vue'; 7 | import router from './router'; 8 | 9 | const app = createApp(App); 10 | app.use(VueDnDKitPlugin, { 11 | defaultOverlay: { 12 | styles: { 13 | opacity: 0.8, 14 | boxShadow: '0 2px 8px rgba(0,0,0,0.1)', 15 | transition: 'none', 16 | }, 17 | }, 18 | } as IPluginOptions); 19 | app.use(router); 20 | app.use(VueDnDKitDevtools); 21 | app.mount('#app'); 22 | -------------------------------------------------------------------------------- /playground/src/pages/Animation.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 45 | 46 | 69 | -------------------------------------------------------------------------------- /playground/src/pages/LazyDrop.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 41 | 42 | 47 | -------------------------------------------------------------------------------- /playground/src/pages/Promises.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 34 | -------------------------------------------------------------------------------- /playground/src/pages/SortableList.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 37 | 38 | 50 | -------------------------------------------------------------------------------- /playground/src/pages/Table.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 56 | 57 | 64 | -------------------------------------------------------------------------------- /playground/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHistory } from 'vue-router'; 2 | 3 | import Animation from '../pages/Animation.vue'; 4 | import Kanban from '../pages/Kanban.vue'; 5 | import LazyDrop from 'src/pages/LazyDrop.vue'; 6 | import Promises from 'src/pages/Promises.vue'; 7 | import SortableList from '../pages/SortableList.vue'; 8 | import Table from '../pages/Table.vue'; 9 | 10 | const routes = [ 11 | { 12 | path: '/', 13 | redirect: '/table', 14 | }, 15 | { 16 | path: '/table', 17 | name: 'Table', 18 | component: Table, 19 | }, 20 | { 21 | path: '/kanban', 22 | name: 'Kanban', 23 | component: Kanban, 24 | }, 25 | { 26 | path: '/sortable-list', 27 | name: 'SortableList', 28 | component: SortableList, 29 | }, 30 | { 31 | path: '/animation', 32 | name: 'Animation', 33 | component: Animation, 34 | }, 35 | { 36 | path: '/lazy-drop', 37 | name: 'LazyDrop', 38 | component: LazyDrop, 39 | }, 40 | { 41 | path: '/promise', 42 | name: 'Promise', 43 | component: Promises, 44 | }, 45 | ]; 46 | 47 | const router = createRouter({ 48 | history: createWebHistory(), 49 | routes, 50 | }); 51 | 52 | export default router; 53 | -------------------------------------------------------------------------------- /playground/src/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import type { DefineComponent } from 'vue'; 3 | const component: DefineComponent<{}, {}, any>; 4 | export default component; 5 | } 6 | -------------------------------------------------------------------------------- /playground/src/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; 3 | line-height: 1.5; 4 | font-weight: 400; 5 | 6 | color-scheme: light dark; 7 | color: rgba(255, 255, 255, 0.87); 8 | background-color: #242424; 9 | 10 | font-synthesis: none; 11 | text-rendering: optimizeLegibility; 12 | -webkit-font-smoothing: antialiased; 13 | -moz-osx-font-smoothing: grayscale; 14 | } 15 | 16 | a { 17 | font-weight: 500; 18 | color: #646cff; 19 | text-decoration: inherit; 20 | } 21 | a:hover { 22 | color: #535bf2; 23 | } 24 | 25 | body { 26 | margin: 0; 27 | display: flex; 28 | place-items: center; 29 | min-width: 320px; 30 | min-height: 100vh; 31 | } 32 | 33 | h1 { 34 | font-size: 3.2em; 35 | line-height: 1.1; 36 | } 37 | 38 | button { 39 | border-radius: 8px; 40 | border: 1px solid transparent; 41 | padding: 0.6em 1.2em; 42 | font-size: 1em; 43 | font-weight: 500; 44 | font-family: inherit; 45 | background-color: #1a1a1a; 46 | cursor: pointer; 47 | transition: border-color 0.25s; 48 | } 49 | button:hover { 50 | border-color: #646cff; 51 | } 52 | button:focus, 53 | button:focus-visible { 54 | outline: 4px auto -webkit-focus-ring-color; 55 | } 56 | 57 | .card { 58 | padding: 2em; 59 | } 60 | 61 | #app { 62 | max-width: 1280px; 63 | margin: 0 auto; 64 | padding: 2rem; 65 | text-align: center; 66 | } 67 | 68 | @media (prefers-color-scheme: light) { 69 | :root { 70 | color: #213547; 71 | background-color: #ffffff; 72 | } 73 | a:hover { 74 | color: #747bff; 75 | } 76 | button { 77 | background-color: #f9f9f9; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /playground/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /playground/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "compilerOptions": { 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | 6 | /* Linting */ 7 | "strict": true, 8 | "noUnusedLocals": true, 9 | "noUnusedParameters": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noUncheckedSideEffectImports": true 12 | }, 13 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"] 14 | } 15 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "noEmit": true, 15 | "jsx": "preserve", 16 | 17 | /* Linting */ 18 | "strict": true, 19 | "noUnusedLocals": true, 20 | "noUnusedParameters": true, 21 | "noFallthroughCasesInSwitch": true, 22 | 23 | /* Paths */ 24 | "baseUrl": ".", 25 | "paths": { 26 | "@vue-dnd-kit/core": ["../packages/core/src"], 27 | "@vue-dnd-kit/core/*": ["../packages/core/src/*"], 28 | "@vue-dnd-kit/components": ["../packages/components/src"], 29 | "@vue-dnd-kit/components/*": ["../packages/components/src/*"], 30 | "@vue-dnd-kit/devtools": ["../packages/devtools/src"] 31 | } 32 | }, 33 | "include": [ 34 | "src/**/*.ts", 35 | "src/**/*.d.ts", 36 | "src/**/*.tsx", 37 | "src/**/*.vue", 38 | "shims.d.ts" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /playground/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 4 | "target": "ES2022", 5 | "lib": ["ES2023"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "isolatedModules": true, 13 | "moduleDetection": "force", 14 | "noEmit": true, 15 | 16 | /* Linting */ 17 | "strict": true, 18 | "noUnusedLocals": true, 19 | "noUnusedParameters": true, 20 | "noFallthroughCasesInSwitch": true, 21 | "noUncheckedSideEffectImports": true 22 | }, 23 | "include": ["vite.config.ts"] 24 | } 25 | -------------------------------------------------------------------------------- /playground/tsconfig.tsbuildinfo: -------------------------------------------------------------------------------- 1 | {"root":["./src/main.ts","./src/shims.d.ts","./src/vite-env.d.ts","./src/router/index.ts","./src/App.vue","./src/components/Drag.vue","./src/pages/BigKanban.vue","./src/pages/Kanban.vue","./src/pages/Table.vue"],"version":"5.7.3"} -------------------------------------------------------------------------------- /playground/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { resolve } from 'path'; 3 | import vue from '@vitejs/plugin-vue'; 4 | 5 | // https://vite.dev/config/ 6 | export default defineConfig({ 7 | plugins: [vue()], 8 | resolve: { 9 | alias: { 10 | '@vue-dnd-kit/core': resolve(__dirname, '../packages/core/src'), 11 | '@vue-dnd-kit/components': resolve( 12 | __dirname, 13 | '../packages/components/src' 14 | ), 15 | src: resolve(__dirname, 'src'), 16 | }, 17 | dedupe: ['vue'], 18 | }, 19 | optimizeDeps: { 20 | include: ['vue'], 21 | exclude: ['@vue-dnd-kit/core', '@vue-dnd-kit/components'], 22 | }, 23 | server: { 24 | fs: { 25 | allow: ['..'], 26 | }, 27 | }, 28 | build: { 29 | sourcemap: true, 30 | }, 31 | }); 32 | -------------------------------------------------------------------------------- /public/docs/images/devtools-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZiZIGY/vue-dnd-kit/1daf116d2382715ecb7be12ee96d97db56bcf9a5/public/docs/images/devtools-screenshot.png --------------------------------------------------------------------------------