├── .gitignore
├── LICENSE
├── README.md
├── babel.config.js
├── package-lock.json
├── package.json
├── public
└── index.html
├── src
├── Sample.vue
├── components
│ ├── DraggableContainer.vue
│ └── DraggableItem.vue
├── composables
│ └── draggable.ts
├── index.js
├── main.ts
├── shims-vue.d.ts
├── types
│ └── draggable-item.interface.ts
└── utils
│ ├── change-order.ts
│ ├── id-generator.ts
│ ├── throttle.ts
│ └── to-draggable-items.ts
├── tsconfig.json
└── vue.config.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Kyle Kim
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 | # vue3-draggable
2 |
3 | list-based drag&drop component for vue 3.x, with no dependencies
4 |
5 | 
6 |
7 | # Features
8 |
9 | - support v-model
10 | - support transition
11 | - customizable draggable component
12 |
13 | Nested useage is currently not supported
14 |
15 | # Installation
16 |
17 | ```
18 | npm i vue3-draggable
19 | ```
20 |
21 | # Try Sample
22 |
23 | ```bash
24 | git clone https://github.com/shkilo/vue3-draggable.git
25 |
26 | npm i
27 | npm run serve
28 | ```
29 |
30 | # Usage
31 |
32 | import component:
33 |
34 | ```javascript
35 | import Draggable from "vue3-draggable";
36 |
37 | export default {
38 | components: {
39 | Draggable,
40 | },
41 | };
42 | ```
43 |
44 | template:
45 |
46 | ```vue
47 |
48 |
49 |
50 |
51 | {{item.title}}
52 |
53 |
54 |
55 |
56 | ```
57 |
58 | This componet is implemented based on [v-slot](https://v3.vuejs.org/guide/component-slots.html#slots)
59 |
60 | ### Props
61 |
62 | | Name | Required | Type | Description |
63 | | :--------- | :------- | :----- | :------------------------------- |
64 | | modelValue | REQUIRED | ARRAY | v-model value, items to be bound |
65 | | transition | OPTIONAL | STRING | transition delay in ms |
66 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | presets: ["@vue/cli-plugin-babel/preset"]
3 | };
4 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue3-draggable",
3 | "version": "2.0.8",
4 | "private": false,
5 | "description": "simple drag&drop component for vue 3.x",
6 | "author": {
7 | "name": "Kyle Kim",
8 | "email": "kyle.hwan.k@gmail.com"
9 | },
10 | "scripts": {
11 | "serve": "vue-cli-service serve",
12 | "build": "vue-cli-service build",
13 | "lint": "vue-cli-service lint",
14 | "build-library": "vue-cli-service build --target lib --name vue3-draggable ./src/index.js"
15 | },
16 | "main": "dist/vue3-draggable.umd.min.js",
17 | "files": [
18 | "dist/*.umd.min.*"
19 | ],
20 | "devDependencies": {
21 | "@typescript-eslint/eslint-plugin": "^2.33.0",
22 | "@typescript-eslint/parser": "^2.33.0",
23 | "@vue/cli-plugin-babel": "~4.5.0",
24 | "@vue/cli-plugin-eslint": "~4.5.0",
25 | "@vue/cli-plugin-typescript": "^4.5.13",
26 | "@vue/cli-service": "~4.5.0",
27 | "@vue/compiler-sfc": "^3.0.0",
28 | "@vue/eslint-config-prettier": "^6.0.0",
29 | "@vue/eslint-config-typescript": "^5.0.2",
30 | "babel-eslint": "^10.1.0",
31 | "core-js": "^3.6.5",
32 | "eslint": "^6.7.2",
33 | "eslint-plugin-prettier": "^3.1.3",
34 | "eslint-plugin-vue": "^7.0.0-0",
35 | "prettier": "^1.19.1",
36 | "typescript": "~4.1.5",
37 | "vue": "^3.0.0"
38 | },
39 | "peerDependencies": {
40 | "vue": "^3.0.0"
41 | },
42 | "eslintConfig": {
43 | "root": true,
44 | "env": {
45 | "node": true
46 | },
47 | "extends": [
48 | "plugin:vue/vue3-essential",
49 | "eslint:recommended",
50 | "@vue/typescript"
51 | ],
52 | "parserOptions": {
53 | "parser": "@typescript-eslint/parser"
54 | },
55 | "rules": {}
56 | },
57 | "browserslist": [
58 | "> 1%",
59 | "last 2 versions",
60 | "not dead"
61 | ],
62 | "bugs": "https://github.com/shkilo/vue3-draggable/issues",
63 | "keywords": [
64 | "draggable",
65 | "vue3",
66 | "vue",
67 | "drag",
68 | "drop",
69 | "drag and drop"
70 | ],
71 | "license": "MIT",
72 | "repository": "https://github.com/shkilo/vue3-draggable"
73 | }
74 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/Sample.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
11 | {{ item.title }}
12 |
13 |
14 |
15 |
16 |
{{ JSON.stringify(items1, undefined, 4) }}
17 |
18 |
19 |
20 |
25 |
26 |
27 | {{ item.title }}
28 |
29 |
30 |
31 |
32 |
{{ JSON.stringify(items2, undefined, 4) }}
33 |
34 |
35 |
36 |
37 |
54 |
55 |
91 |
--------------------------------------------------------------------------------
/src/components/DraggableContainer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
54 |
55 |
60 |
--------------------------------------------------------------------------------
/src/components/DraggableItem.vue:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
15 |
16 |
17 |
52 |
53 |
58 |
--------------------------------------------------------------------------------
/src/composables/draggable.ts:
--------------------------------------------------------------------------------
1 | import { ref, onMounted, onUpdated, watch, SetupContext, Ref } from "vue";
2 | import { DraggableItem } from "../types/draggable-item.interface";
3 | import { changeArrayOrder } from "../utils/change-order";
4 | import { getIdGenerator } from "../utils/id-generator";
5 | import { throttle } from "../utils/throttle";
6 | import { toOriginalArray, toDraggableItems } from "../utils/to-draggable-items";
7 |
8 | let itemCurrentlyDragging = ref(null);
9 | let containerIdCurrentlyDraggedOver = ref(null);
10 | let transitioning = false;
11 | const containerIdGenerator = getIdGenerator();
12 |
13 | const useDraggableContainer = (
14 | originalItems: Ref>,
15 | context: SetupContext
16 | ) => {
17 | const id = containerIdGenerator();
18 | const items = ref>(
19 | toDraggableItems(originalItems.value)
20 | );
21 |
22 | // update v-model when dropped
23 | watch(itemCurrentlyDragging, () => {
24 | if (itemCurrentlyDragging.value) {
25 | return;
26 | }
27 | context.emit("update:modelValue", toOriginalArray(items.value));
28 | });
29 |
30 | // case when an item is being dragged to another container
31 | watch(containerIdCurrentlyDraggedOver, () => {
32 | if (containerIdCurrentlyDraggedOver.value === id) {
33 | return;
34 | }
35 | items.value = items.value.filter(
36 | (item) => item.id !== itemCurrentlyDragging.value.id
37 | );
38 | });
39 |
40 | // when an item is moved to an empty container
41 | const onDragOver = () => {
42 | if (
43 | transitioning ||
44 | !itemCurrentlyDragging.value ||
45 | containerIdCurrentlyDraggedOver.value === id
46 | ) {
47 | return;
48 | }
49 |
50 | if (items.value.length > 0) {
51 | return;
52 | }
53 |
54 | containerIdCurrentlyDraggedOver.value = id;
55 | items.value = [itemCurrentlyDragging.value];
56 | };
57 |
58 | // handle event emitted from draggableItem
59 | const onItemDragOver = ({ position }: { position: number }) => {
60 | if (transitioning || !itemCurrentlyDragging.value) {
61 | return;
62 | }
63 | items.value = changeArrayOrder(
64 | items.value,
65 | itemCurrentlyDragging.value,
66 | position
67 | );
68 | };
69 |
70 | return {
71 | id,
72 | items,
73 | onDragOver,
74 | onItemDragOver,
75 | };
76 | };
77 |
78 | const useDraggableItem = (
79 | item: Ref,
80 | position: Ref,
81 | containerId: Ref,
82 | context: SetupContext
83 | ) => {
84 | const draggableItemEl = ref(null);
85 | const isDragging = ref(
86 | item.value.id === itemCurrentlyDragging.value?.id ? true : false
87 | );
88 | const middleY = ref(null);
89 |
90 | onMounted(async () => {
91 | const box = draggableItemEl.value.getBoundingClientRect();
92 | middleY.value = box.top + box.height / 2;
93 | });
94 |
95 | onUpdated(() => {
96 | const box = draggableItemEl.value.getBoundingClientRect();
97 | middleY.value = box.top + box.height / 2;
98 | });
99 |
100 | const onDragStart = () => {
101 | itemCurrentlyDragging.value = item.value;
102 | containerIdCurrentlyDraggedOver.value = containerId.value;
103 | isDragging.value = true;
104 | };
105 |
106 | const onDragEnd = () => {
107 | itemCurrentlyDragging.value = null;
108 | };
109 |
110 | const onDragOver = throttle((e: DragEvent) => {
111 | if (item.value.id === itemCurrentlyDragging.value.id) {
112 | return;
113 | }
114 |
115 | if (containerIdCurrentlyDraggedOver.value !== containerId.value) {
116 | containerIdCurrentlyDraggedOver.value = containerId.value;
117 | }
118 |
119 | const offset = middleY.value - e.clientY;
120 |
121 | context.emit("itemDragOver", {
122 | position: offset > 0 ? position.value : position.value + 1,
123 | });
124 | }, 50);
125 |
126 | const transitionStart = () => {
127 | transitioning = true;
128 | };
129 |
130 | const transitionEnd = () => {
131 | transitioning = false;
132 | };
133 |
134 | watch(itemCurrentlyDragging, () => {
135 | if (itemCurrentlyDragging.value) {
136 | return;
137 | }
138 | isDragging.value = false;
139 | });
140 |
141 | return {
142 | draggableItemEl,
143 | isDragging,
144 | onDragStart,
145 | onDragOver,
146 | onDragEnd,
147 | transitionStart,
148 | transitionEnd,
149 | };
150 | };
151 |
152 | export { useDraggableContainer, useDraggableItem };
153 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import Draggable from "./components/DraggableContainer";
2 |
3 | export default Draggable;
4 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from "vue";
2 | import Sample from "./Sample.vue";
3 |
4 | createApp(Sample).mount("#app");
5 |
--------------------------------------------------------------------------------
/src/shims-vue.d.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | declare module '*.vue' {
3 | import type { DefineComponent } from 'vue'
4 | const component: DefineComponent<{}, {}, any>
5 | export default component
6 | }
7 |
--------------------------------------------------------------------------------
/src/types/draggable-item.interface.ts:
--------------------------------------------------------------------------------
1 | export interface DraggableItem {
2 | id: number;
3 | data: object;
4 | }
5 |
--------------------------------------------------------------------------------
/src/utils/change-order.ts:
--------------------------------------------------------------------------------
1 | import { DraggableItem } from "../types/draggable-item.interface";
2 |
3 | export const changeArrayOrder = (
4 | arr: Array,
5 | target: DraggableItem,
6 | newIndexOfTarget: number
7 | ): Array => {
8 | let newArr = arr.filter((e) => e.id !== target.id);
9 | newArr.splice(newIndexOfTarget, 0, { ...target });
10 | return newArr;
11 | };
12 |
--------------------------------------------------------------------------------
/src/utils/id-generator.ts:
--------------------------------------------------------------------------------
1 | export const getIdGenerator = () => {
2 | let num = 0;
3 | return () => {
4 | return num++;
5 | };
6 | };
7 |
--------------------------------------------------------------------------------
/src/utils/throttle.ts:
--------------------------------------------------------------------------------
1 | export const throttle = (callback: Function, limit: number) => {
2 | let waiting = false;
3 | return (...args: any[]) => {
4 | if (waiting) {
5 | return;
6 | }
7 | callback(...args);
8 | waiting = true;
9 | setTimeout(() => {
10 | waiting = false;
11 | }, limit);
12 | return;
13 | };
14 | };
15 |
--------------------------------------------------------------------------------
/src/utils/to-draggable-items.ts:
--------------------------------------------------------------------------------
1 | import { DraggableItem } from "../types/draggable-item.interface";
2 | import { getIdGenerator } from "./id-generator";
3 |
4 | const draggableItemIdGenrator = getIdGenerator();
5 |
6 | export const toDraggableItems = (arr: Array): Array => {
7 | return arr.map((e) => ({
8 | id: draggableItemIdGenrator(),
9 | data: e,
10 | }));
11 | };
12 |
13 | export const toOriginalArray = (arr: Array): Array => {
14 | return arr.map((e) => e.data);
15 | };
16 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "esnext",
4 | "module": "esnext",
5 | "strict": true,
6 | "strictNullChecks": false,
7 | "jsx": "preserve",
8 | "importHelpers": true,
9 | "moduleResolution": "node",
10 | "allowJs": true,
11 | "skipLibCheck": true,
12 | "esModuleInterop": true,
13 | "allowSyntheticDefaultImports": true,
14 | "sourceMap": true,
15 | "baseUrl": ".",
16 | "types": [
17 | "webpack-env"
18 | ],
19 | "paths": {
20 | "@/*": [
21 | "src/*"
22 | ]
23 | },
24 | "lib": [
25 | "esnext",
26 | "dom",
27 | "dom.iterable",
28 | "scripthost"
29 | ]
30 | },
31 | "include": [
32 | "src/**/*.ts",
33 | "src/**/*.tsx",
34 | "src/**/*.vue",
35 | "tests/**/*.ts",
36 | "tests/**/*.tsx"
37 | , "src/index.js" ],
38 | "exclude": [
39 | "node_modules"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | css: { extract: false }
3 | };
4 |
--------------------------------------------------------------------------------