├── .editorconfig
├── .gitignore
├── .prettierrc.json
├── .storybook
├── main.ts
└── preview.ts
├── .vscode
└── extensions.json
├── LICENCE.txt
├── README.md
├── convert-tailwind-to-css.js
├── eslint.config.js
├── package-lock.json
├── package.json
├── postcss.config.js
├── src
├── assets
│ ├── alien.gif
│ ├── alien_dragging.gif
│ ├── astronaut.gif
│ ├── astronaut_dragging.gif
│ ├── index.css
│ ├── main.css
│ ├── moon.png
│ ├── rocket.gif
│ ├── saturn.png
│ └── star.gif
├── components
│ ├── VueDragPlayground.stories.js
│ └── VueDragPlayground.vue
├── index.d.ts
└── index.ts
├── tailwind.config.js
├── tsconfig.json
└── vite.config.ts
/.editorconfig:
--------------------------------------------------------------------------------
1 | [*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
2 | charset = utf-8
3 | indent_size = 2
4 | indent_style = space
5 | insert_final_newline = true
6 | trim_trailing_whitespace = true
7 |
--------------------------------------------------------------------------------
/.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 | .DS_Store
12 | dist
13 | dist-ssr
14 | coverage
15 | *.local
16 | .npmrc
17 | /cypress/videos/
18 | /cypress/screenshots/
19 |
20 | # Editor directories and files
21 | .vscode/*
22 | !.vscode/extensions.json
23 | .idea
24 | *.suo
25 | *.ntvs*
26 | *.njsproj
27 | *.sln
28 | *.sw?
29 | storybook-static
30 | storybook-static.zip
31 | *.tsbuildinfo
32 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "$schema": "https://json.schemastore.org/prettierrc",
4 | "semi": false,
5 | "singleQuote": true,
6 | "printWidth": 100
7 | }
8 |
--------------------------------------------------------------------------------
/.storybook/main.ts:
--------------------------------------------------------------------------------
1 | import type { StorybookConfig } from '@storybook/vue3-vite'
2 |
3 | const config: StorybookConfig = {
4 | stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
5 | staticDirs: ['../src/assets'],
6 | addons: [
7 | '@storybook/addon-links',
8 | '@storybook/addon-essentials',
9 | '@storybook/addon-interactions',
10 | ],
11 | framework: {
12 | name: '@storybook/vue3-vite',
13 | options: {},
14 | },
15 | }
16 | export default config
17 |
--------------------------------------------------------------------------------
/.storybook/preview.ts:
--------------------------------------------------------------------------------
1 | import type { Preview } from '@storybook/vue3'
2 | import '../src/assets/main.css'
3 | const preview: Preview = {
4 | parameters: {
5 | controls: {
6 | matchers: {
7 | color: /(background|color)$/i,
8 | date: /Date$/i,
9 | },
10 | },
11 | },
12 | }
13 |
14 | export default preview
15 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "Vue.volar",
4 | "dbaeumer.vscode-eslint",
5 | "EditorConfig.EditorConfig",
6 | "esbenp.prettier-vscode"
7 | ]
8 | }
9 |
--------------------------------------------------------------------------------
/LICENCE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [year] [your name]
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
21 | THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # VueDragPlayground
2 |
3 | VueDragPlayground is a versatile Vue 3 library designed to create dynamic and interactive user interfaces with drag, resize, and rotate functionalities. It allows developers to easily implement draggable, resizable, and rotatable elements in their applications, offering a smooth user experience powered by modern tooling like Vue 3 and Vite.
4 |
5 | ## Quick Links
6 |
7 | - 🌐 [Live Demo](https://vuedragplayground.actechworld.com/)
8 | Explore a live example of the component in action.
9 | - 📚 [Storybook Documentation](https://vuedragplayground.storybook.actechworld.com/?path=/story/lib-components-vuedragplayground--default)
10 | - 📦 [Npm package](https://www.npmjs.com/package/vue-drag-playground)
11 | Here is the npm package
12 | Dive into detailed component stories and configurations.
13 | - 💼 [LinkedIn](https://www.linkedin.com/in/antoine-canard/)
14 | Here is my linkedin if you want to contact, me I'm always open for new challenges !
15 |
16 | ## Features
17 |
18 | - **Drag**: Seamlessly move elements across the screen with intuitive drag gestures.
19 | - **Resize**: Change element dimensions via resize handles.
20 | - **Rotate**: Rotate elements freely to achieve precise orientations.
21 | - **Copy and Delete**: Duplicate or remove elements dynamically.
22 | - **Event Handling**: Capture and respond to drag, resize, and rotate actions.
23 | - **Multi Interaction (CTRL Click)**: Hold `CTRL` and click on multiple elements to interact with them simultaneously for drag, resize, rotate, copy, or delete actions.
24 |
25 | ## Getting Started
26 |
27 | ### Installation
28 |
29 | #### Package
30 |
31 | To install VueDragPlayground, run:
32 |
33 | ```bash
34 | npm install vue-drag-playground
35 | ```
36 |
37 | #### Global Registration
38 |
39 | In `main.ts` (Vue 3) or `main.js` (Vue 2), register the plugin globally:
40 |
41 | ##### For Vue 3:
42 |
43 | ```typescript
44 | import { createApp } from 'vue'
45 | import App from './App.vue'
46 | import VueDragPlayground from 'vue-drag-playground'
47 |
48 | const app = createApp(App)
49 | app.use(VueDragPlayground)
50 | app.mount('#app')
51 | ```
52 |
53 | ##### For Vue 2:
54 |
55 | ```javascript
56 | import Vue from 'vue'
57 | import App from './App.vue'
58 | import VueDragPlayground from 'vue-drag-playground'
59 |
60 | Vue.use(VueDragPlayground)
61 |
62 | new Vue({
63 | render: (h) => h(App),
64 | }).$mount('#app')
65 | ```
66 |
67 | #### Local Registration
68 |
69 | Alternatively, register the component locally in a Vue file:
70 |
71 | ```vue
72 |
73 |
74 |
75 |
76 |
85 | ```
86 |
87 | For Vue 2, you will need to adjust the script as follows:
88 |
89 | ```vue
90 |
91 |
92 |
93 |
94 |
119 | ```
120 |
121 | ## Documentation
122 |
123 | ### Props
124 |
125 | The component supports the following props to customize its behavior:
126 |
127 | - **v-model** (`Array`): A list of draggable items. Each item contains properties like `id`, `html`, `x`, `y`, `width`, `height`, and `rotation`.
128 |
129 | - Each item should have this structure:
130 | ```typescript
131 | {
132 | id?: number, // Optional unique identifier for the item
133 | name?: string, // Optional name for the item
134 | html: string, // HTML string to render inside the item
135 | x: number, // X-coordinate for position
136 | y: number, // Y-coordinate for position
137 | width?: number, // Optional width of the item (recommended to set, especially for images)
138 | height?: number, // Optional height of the item (recommended to set, especially for images)
139 | rotation?: number // Optional rotation angle in degrees
140 | static?: boolean // Optional a static item will be displayed on the playground but you cannot interact with it (drag, resize, rotate etc ...)
141 | }
142 | ```
143 |
144 | **Note**:
145 | While `width` and `height` are optional, it is highly recommended to set them initially, especially for images or elements that may have specific dimensions for proper rendering. If not set, the component may not render or position the item as expected, particularly for elements like images that rely on these properties.
146 |
147 | - **isDrag** (`Boolean`): Enables/disables dragging functionality. Default: `true`.
148 | - **isResize** (`Boolean`): Enables/disables resizing functionality. Default: `false`.
149 | - **isRotate** (`Boolean`): Enables/disables rotation functionality. Default: `false`.
150 | - **isCopy** (`Boolean`): Enables/disables item duplication. Default: `false`.
151 | - **isDelete** (`Boolean`): Enables/disables item removal. Default: `false`.
152 | - **isMultiSelect** (`Boolean`): Enables/disables multi-selection functionality with `CTRL` click. Default: `true`.
153 | - **throttleDelay** (`Number`): Delay (in ms) to throttle events like dragging, resizing, and rotating. Default: `1`.
154 | - **maxNumberOfItems** (`Number | undefined`): Maximum number of items allowed. If undefined, there is no limit (i.e., infinite items are allowed). Default: undefined.
155 | - **multiRotationMode** (`String`): Specifies rotation behavior for grouped items when multiple items are selected for rotation. This prop can have the following values:
156 | - **'proportional'**: When this mode is selected, the items will maintain their individual rotation angles relative to each other. Essentially, the rotation of each item remains in proportion to its original angle.
157 | - **'uniform'**: In this mode, all selected items will be rotated to the same angle. The rotation is aligned, so all items will be rotated together as if they share the same angle.
158 |
159 | ### Emits
160 |
161 | The following events can be emitted by the component to inform the parent about user interactions:
162 |
163 | - `drag-start`: Fired when dragging starts. **Emits the updated item.**
164 | - `dragging`: Fired while an item is being dragged. **Emits the updated item.**
165 | - `drag-end`: Fired when dragging stops. **Emits the updated item.**
166 | - `resize-start`: Fired when resizing starts. **Emits the updated item.**
167 | - `resizing`: Fired while an item is being resized. **Emits the updated item.**
168 | - `resize-end`: Fired when resizing stops. **Emits the updated item.**
169 | - `rotation-start`: Fired when rotation starts. **Emits the updated item.**
170 | - `rotating`: Fired while an item is being rotated. **Emits the updated item.**
171 | - `rotation-end`: Fired when rotation stops. **Emits the updated item.**
172 | - `copy-items`: Fired when items are copied. **Emits an object containing the copied items and the newly created items.**
173 | - `delete-items`: Fired when items are deleted. **Emits the list of deleted items.**
174 |
175 | Each event provides the entire updated item, including all properties such as its position (`x`, `y`), size (`width`, `height`), and rotation (`rotation`).
176 |
177 | ### Multi Interaction (CTRL Click)
178 |
179 | VueDragPlayground supports **multi-selection interaction** using the `CTRL` key. By holding the `CTRL` key while clicking on multiple items, users can select and interact with those items simultaneously. This works for the following actions:
180 |
181 | - **Drag**: Move multiple elements together.
182 | - **Resize**: Resize multiple elements together.
183 | - **Rotate**: Rotate multiple elements together.
184 | - **Copy**: Duplicate multiple selected elements.
185 | - **Delete**: Remove multiple selected elements.
186 |
187 | This functionality is controlled by the `isMultiSelect` prop, which is enabled by default (`true`). To disable multi-selection, set `isMultiSelect` to `false`.
188 |
189 | ## Example Usage
190 |
191 | Here’s how you can use VueDragPlayground to create an interactive UI, showcasing all the props:
192 |
193 | ```vue
194 |
195 |
218 |
219 |
220 |
259 | ```
260 |
261 | ## License
262 |
263 | This project is licensed under the MIT License.
264 |
--------------------------------------------------------------------------------
/convert-tailwind-to-css.js:
--------------------------------------------------------------------------------
1 |
2 | // convert-tailwind-to-css.js
3 |
4 | import fs from 'fs';
5 | import postcss from 'postcss';
6 | import tailwindcss from 'tailwindcss';
7 |
8 | // Generated CSS indent spaces count
9 | const indentSpaces = 2;
10 | // Generated CSS output file
11 | const outputCSS = './src/assets/index.css';
12 |
13 | // Convert Tailwind CSS to native CSS
14 | postcss([
15 | tailwindcss({
16 | content: ['./src/**/*.vue'],
17 | theme: {
18 | extend: {},
19 | },
20 | variants: {
21 | extend: {},
22 | },
23 | plugins: [],
24 | }),
25 | ])
26 | .process('@tailwind utilities; @tailwind components;', { from: undefined })
27 | .then((result) => {
28 | // Format and write the CSS output
29 | let formattedCSS = result.css
30 | .replaceAll(' '.repeat(4), ' '.repeat(indentSpaces)) // Handle indentation
31 | .replace(/([^{;\s]+:[^;}]+)(\s*?)\n(\s*})/g, '$1;\n$3'); // Insert semicolon before newline and closing brace, preserving indentation
32 |
33 | //Init all tailwind vars
34 | formattedCSS = `${formattedCSS}
35 | *, ::before, ::after {
36 | --tw-border-spacing-x: 0;
37 | --tw-border-spacing-y: 0;
38 | --tw-translate-x: 0;
39 | --tw-translate-y: 0;
40 | --tw-rotate: 0;
41 | --tw-skew-x: 0;
42 | --tw-skew-y: 0;
43 | --tw-scale-x: 1;
44 | --tw-scale-y: 1;
45 | --tw-pan-x: ;
46 | --tw-pan-y: ;
47 | --tw-pinch-zoom: ;
48 | --tw-scroll-snap-strictness: proximity;
49 | --tw-gradient-from-position: ;
50 | --tw-gradient-via-position: ;
51 | --tw-gradient-to-position: ;
52 | --tw-ordinal: ;
53 | --tw-slashed-zero: ;
54 | --tw-numeric-figure: ;
55 | --tw-numeric-spacing: ;
56 | --tw-numeric-fraction: ;
57 | --tw-ring-inset: ;
58 | --tw-ring-offset-width: 0px;
59 | --tw-ring-offset-color: #fff;
60 | --tw-ring-color: rgb(59 130 246 / 0.5);
61 | --tw-ring-offset-shadow: 0 0 #0000;
62 | --tw-ring-shadow: 0 0 #0000;
63 | --tw-shadow: 0 0 #0000;
64 | --tw-shadow-colored: 0 0 #0000;
65 | --tw-blur: ;
66 | --tw-brightness: ;
67 | --tw-contrast: ;
68 | --tw-grayscale: ;
69 | --tw-hue-rotate: ;
70 | --tw-invert: ;
71 | --tw-saturate: ;
72 | --tw-sepia: ;
73 | --tw-drop-shadow: ;
74 | --tw-backdrop-blur: ;
75 | --tw-backdrop-brightness: ;
76 | --tw-backdrop-contrast: ;
77 | --tw-backdrop-grayscale: ;
78 | --tw-backdrop-hue-rotate: ;
79 | --tw-backdrop-invert: ;
80 | --tw-backdrop-opacity: ;
81 | --tw-backdrop-saturate: ;
82 | --tw-backdrop-sepia: ;
83 | --tw-contain-size: ;
84 | --tw-contain-layout: ;
85 | --tw-contain-paint: ;
86 | --tw-contain-style: ;
87 | }`
88 |
89 | fs.writeFileSync(outputCSS, formattedCSS, 'utf8');
90 | console.log(`Native CSS generated: ${outputCSS}`);
91 | })
92 | .catch((err) => console.error('An error occurred:', err));
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import pluginVue from 'eslint-plugin-vue'
2 | import vueTsEslintConfig from '@vue/eslint-config-typescript'
3 | import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
4 |
5 | export default [
6 | {
7 | name: 'app/files-to-lint',
8 | files: ['**/*.{ts,mts,tsx,vue}'],
9 | },
10 |
11 | {
12 | name: 'app/files-to-ignore',
13 | ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
14 | },
15 |
16 | ...pluginVue.configs['flat/essential'],
17 | ...vueTsEslintConfig(),
18 | skipFormatting,
19 | ]
20 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vue-drag-playground",
3 | "version": "0.0.5-beta.4",
4 | "private": false,
5 | "license": "MIT",
6 | "type": "module",
7 | "types": "./dist/types/index.d.ts",
8 | "publishConfig": {
9 | "access": "public"
10 | },
11 | "exports": {
12 | ".": {
13 | "import": "./dist/vue-drag-playground.es.js",
14 | "require": "./dist/vue-drag-playground.umd.js",
15 | "types": "./dist/types/index.d.ts"
16 | }
17 | },
18 | "scripts": {
19 | "dev": "node convert-tailwind-to-css.js && vite",
20 | "build": "vue-tsc --declaration --emitDeclarationOnly && node convert-tailwind-to-css.js && vite build",
21 | "preview": "vite preview",
22 | "test:unit": "vitest --environment jsdom",
23 | "build-only": "vite build",
24 | "type-check": "vue-tsc --build --force",
25 | "lint": "eslint . --fix",
26 | "format": "prettier --write src/",
27 | "storybook": "storybook dev -p 6006",
28 | "build-storybook": "storybook build"
29 | },
30 | "dependencies": {
31 | "dompurify": "^3.2.1",
32 | "vue": "^3.5.12"
33 | },
34 | "devDependencies": {
35 | "@chromatic-com/storybook": "^3.2.2",
36 | "@storybook/addon-essentials": "^8.4.4",
37 | "@storybook/addon-interactions": "^8.4.4",
38 | "@storybook/blocks": "^8.4.4",
39 | "@storybook/test": "^8.4.4",
40 | "@storybook/vue3": "^8.4.4",
41 | "@storybook/vue3-vite": "^8.4.4",
42 | "@tsconfig/node22": "^22.0.0",
43 | "@types/node": "^22.9.0",
44 | "@vitejs/plugin-vue": "^5.1.4",
45 | "@vue/eslint-config-prettier": "^10.1.0",
46 | "@vue/eslint-config-typescript": "^14.1.3",
47 | "@vue/tsconfig": "^0.5.1",
48 | "autoprefixer": "^10.4.20",
49 | "cssnano": "^7.0.6",
50 | "eslint": "^9.14.0",
51 | "eslint-plugin-storybook": "^0.11.0",
52 | "eslint-plugin-vue": "^9.30.0",
53 | "npm-run-all2": "^7.0.1",
54 | "postcss": "^8.4.49",
55 | "prettier": "^3.3.3",
56 | "storybook": "^8.4.4",
57 | "tailwindcss": "^3.4.15",
58 | "typescript": "~5.6.3",
59 | "vite": "^5.4.10",
60 | "vite-plugin-css-injected-by-js": "^3.5.2",
61 | "vite-plugin-vue-devtools": "^7.5.4",
62 | "vue-tsc": "^2.1.10"
63 | },
64 | "eslintConfig": {
65 | "extends": [
66 | "plugin:storybook/recommended"
67 | ]
68 | },
69 | "files": [
70 | "dist",
71 | "README.md"
72 | ]
73 | }
74 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | cssnano: {},
6 | },
7 | }
8 |
--------------------------------------------------------------------------------
/src/assets/alien.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acTechWorld/vue-drag-playground/02ab14d20452e6a0862584812a7a680ba0e0cd01/src/assets/alien.gif
--------------------------------------------------------------------------------
/src/assets/alien_dragging.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acTechWorld/vue-drag-playground/02ab14d20452e6a0862584812a7a680ba0e0cd01/src/assets/alien_dragging.gif
--------------------------------------------------------------------------------
/src/assets/astronaut.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acTechWorld/vue-drag-playground/02ab14d20452e6a0862584812a7a680ba0e0cd01/src/assets/astronaut.gif
--------------------------------------------------------------------------------
/src/assets/astronaut_dragging.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acTechWorld/vue-drag-playground/02ab14d20452e6a0862584812a7a680ba0e0cd01/src/assets/astronaut_dragging.gif
--------------------------------------------------------------------------------
/src/assets/index.css:
--------------------------------------------------------------------------------
1 | .pointer-events-none {
2 | pointer-events: none;
3 | }
4 | .static {
5 | position: static;
6 | }
7 | .fixed {
8 | position: fixed;
9 | }
10 | .absolute {
11 | position: absolute;
12 | }
13 | .relative {
14 | position: relative;
15 | }
16 | .-bottom-1 {
17 | bottom: -0.25rem;
18 | }
19 | .-left-1 {
20 | left: -0.25rem;
21 | }
22 | .-right-1 {
23 | right: -0.25rem;
24 | }
25 | .-top-1 {
26 | top: -0.25rem;
27 | }
28 | .left-1\/2 {
29 | left: 50%;
30 | }
31 | .top-0 {
32 | top: 0px;
33 | }
34 | .top-2 {
35 | top: 0.5rem;
36 | }
37 | .z-0 {
38 | z-index: 0;
39 | }
40 | .z-\[1\] {
41 | z-index: 1;
42 | }
43 | .z-\[2\] {
44 | z-index: 2;
45 | }
46 | .z-\[3\] {
47 | z-index: 3;
48 | }
49 | .box-border {
50 | box-sizing: border-box;
51 | }
52 | .flex {
53 | display: flex;
54 | }
55 | .hidden {
56 | display: none;
57 | }
58 | .h-10 {
59 | height: 2.5rem;
60 | }
61 | .h-4 {
62 | height: 1rem;
63 | }
64 | .h-5 {
65 | height: 1.25rem;
66 | }
67 | .h-6 {
68 | height: 1.5rem;
69 | }
70 | .h-fit {
71 | height: fit-content;
72 | }
73 | .h-full {
74 | height: 100%;
75 | }
76 | .w-4 {
77 | width: 1rem;
78 | }
79 | .w-6 {
80 | width: 1.5rem;
81 | }
82 | .w-fit {
83 | width: fit-content;
84 | }
85 | .w-full {
86 | width: 100%;
87 | }
88 | .-translate-x-1\/2 {
89 | --tw-translate-x: -50%;
90 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
91 | }
92 | .-translate-y-full {
93 | --tw-translate-y: -100%;
94 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
95 | }
96 | .transform {
97 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
98 | }
99 | .cursor-grab {
100 | cursor: grab;
101 | }
102 | .cursor-grabbing {
103 | cursor: grabbing;
104 | }
105 | .cursor-none {
106 | cursor: none;
107 | }
108 | .cursor-pointer {
109 | cursor: pointer;
110 | }
111 | .select-none {
112 | user-select: none;
113 | }
114 | .resize {
115 | resize: both;
116 | }
117 | .gap-2 {
118 | gap: 0.5rem;
119 | }
120 | .rounded-\[50\%\] {
121 | border-radius: 50%;
122 | }
123 | .border-2 {
124 | border-width: 2px;
125 | }
126 | .border-dashed {
127 | border-style: dashed;
128 | }
129 | .border-black {
130 | --tw-border-opacity: 1;
131 | border-color: rgb(0 0 0 / var(--tw-border-opacity, 1));
132 | }
133 | .bg-black {
134 | --tw-bg-opacity: 1;
135 | background-color: rgb(0 0 0 / var(--tw-bg-opacity, 1));
136 | }
137 | .bg-white\/50 {
138 | background-color: rgb(255 255 255 / 0.5);
139 | }
140 | .fill-black {
141 | fill: #000;
142 | }
143 | .fill-green-500 {
144 | fill: #22c55e;
145 | }
146 | .fill-white {
147 | fill: #fff;
148 | }
149 | .p-1 {
150 | padding: 0.25rem;
151 | }
152 | .opacity-0 {
153 | opacity: 0;
154 | }
155 | .opacity-100 {
156 | opacity: 1;
157 | }
158 | .filter {
159 | filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
160 | }
161 | .transition-all {
162 | transition-property: all;
163 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
164 | transition-duration: 150ms;
165 | }
166 | .transition-opacity {
167 | transition-property: opacity;
168 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
169 | transition-duration: 150ms;
170 | }
171 | .duration-500 {
172 | transition-duration: 500ms;
173 | }
174 | .group:hover .group-hover\:pointer-events-auto {
175 | pointer-events: auto;
176 | }
177 | .group:hover .group-hover\:z-\[2\] {
178 | z-index: 2;
179 | }
180 | .group:hover .group-hover\:z-\[3\] {
181 | z-index: 3;
182 | }
183 | .group\/cursor:hover .group-hover\/cursor\:block {
184 | display: block;
185 | }
186 | .group:hover .group-hover\:opacity-100 {
187 | opacity: 1;
188 | }
189 | *, ::before, ::after {
190 | --tw-border-spacing-x: 0;
191 | --tw-border-spacing-y: 0;
192 | --tw-translate-x: 0;
193 | --tw-translate-y: 0;
194 | --tw-rotate: 0;
195 | --tw-skew-x: 0;
196 | --tw-skew-y: 0;
197 | --tw-scale-x: 1;
198 | --tw-scale-y: 1;
199 | --tw-pan-x: ;
200 | --tw-pan-y: ;
201 | --tw-pinch-zoom: ;
202 | --tw-scroll-snap-strictness: proximity;
203 | --tw-gradient-from-position: ;
204 | --tw-gradient-via-position: ;
205 | --tw-gradient-to-position: ;
206 | --tw-ordinal: ;
207 | --tw-slashed-zero: ;
208 | --tw-numeric-figure: ;
209 | --tw-numeric-spacing: ;
210 | --tw-numeric-fraction: ;
211 | --tw-ring-inset: ;
212 | --tw-ring-offset-width: 0px;
213 | --tw-ring-offset-color: #fff;
214 | --tw-ring-color: rgb(59 130 246 / 0.5);
215 | --tw-ring-offset-shadow: 0 0 #0000;
216 | --tw-ring-shadow: 0 0 #0000;
217 | --tw-shadow: 0 0 #0000;
218 | --tw-shadow-colored: 0 0 #0000;
219 | --tw-blur: ;
220 | --tw-brightness: ;
221 | --tw-contrast: ;
222 | --tw-grayscale: ;
223 | --tw-hue-rotate: ;
224 | --tw-invert: ;
225 | --tw-saturate: ;
226 | --tw-sepia: ;
227 | --tw-drop-shadow: ;
228 | --tw-backdrop-blur: ;
229 | --tw-backdrop-brightness: ;
230 | --tw-backdrop-contrast: ;
231 | --tw-backdrop-grayscale: ;
232 | --tw-backdrop-hue-rotate: ;
233 | --tw-backdrop-invert: ;
234 | --tw-backdrop-opacity: ;
235 | --tw-backdrop-saturate: ;
236 | --tw-backdrop-sepia: ;
237 | --tw-contain-size: ;
238 | --tw-contain-layout: ;
239 | --tw-contain-paint: ;
240 | --tw-contain-style: ;
241 | }
--------------------------------------------------------------------------------
/src/assets/main.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
--------------------------------------------------------------------------------
/src/assets/moon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acTechWorld/vue-drag-playground/02ab14d20452e6a0862584812a7a680ba0e0cd01/src/assets/moon.png
--------------------------------------------------------------------------------
/src/assets/rocket.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acTechWorld/vue-drag-playground/02ab14d20452e6a0862584812a7a680ba0e0cd01/src/assets/rocket.gif
--------------------------------------------------------------------------------
/src/assets/saturn.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acTechWorld/vue-drag-playground/02ab14d20452e6a0862584812a7a680ba0e0cd01/src/assets/saturn.png
--------------------------------------------------------------------------------
/src/assets/star.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/acTechWorld/vue-drag-playground/02ab14d20452e6a0862584812a7a680ba0e0cd01/src/assets/star.gif
--------------------------------------------------------------------------------
/src/components/VueDragPlayground.stories.js:
--------------------------------------------------------------------------------
1 | import VueDragPlayground from './VueDragPlayground.vue'
2 | export default {
3 | title: 'Lib/Components/VueDragPlayground',
4 |
5 | component: VueDragPlayground,
6 | argTypes: {
7 | // Event definitions
8 | onDragStart: { action: 'drag-start' },
9 | // onDragging: { action: 'dragging' },
10 | onDragEnd: { action: 'drag-end' },
11 | onResizeStart: { action: 'resize-start' },
12 | // onResizing: { action: 'resizing' },
13 | onResizeEnd: { action: 'resize-end' },
14 | onRotationStart: { action: 'rotation-start' },
15 | // onRotating: { action: 'rotating' },
16 | onRotationEnd: { action: 'rotation-end' },
17 | onCopyItems: { action: 'copy-items' },
18 | onDeleteItems: { action: 'delete-items' },
19 | },
20 | }
21 | import { ref } from 'vue'
22 |
23 | const DefaultTemplate = (args) => ({
24 | components: { VueDragPlayground },
25 | setup() {
26 | const refItems = ref(args.items)
27 | const handleDragStart = (item) => {
28 | refItems.value = refItems.value.map((it) =>
29 | it.name === item.name && it.id === item.id
30 | ? {
31 | ...it,
32 | html: ['astronaut', 'alien'].includes(it.name)
33 | ? it.html.replace(/(
]*src=')[^']*(')/i, `$1${it.name}_dragging.gif$2`)
34 | : it.html,
35 | }
36 | : it,
37 | )
38 | }
39 | const handleDragEnd = (item) => {
40 | refItems.value = refItems.value.map((it) =>
41 | it.name === item.name && it.id === item.id
42 | ? {
43 | ...it,
44 | html: ['astronaut', 'alien'].includes(it.name)
45 | ? it.html.replace(/(
]*src=')[^']*(')/i, `$1${it.name}.gif$2`)
46 | : it.html,
47 | }
48 | : it,
49 | )
50 | }
51 | return { args, refItems, handleDragStart, handleDragEnd }
52 | },
53 | template: `
54 |
55 |
56 |
57 |
58 |
59 | `,
60 | })
61 |
62 | export const Default = DefaultTemplate.bind({})
63 |
64 | Default.args = {
65 | items: [
66 | {
67 | name: 'block1',
68 | html: "Heading
",
69 | x: 50,
70 | y: 50,
71 | rotation: 50,
72 | },
73 | {
74 | name: 'block11',
75 | html: "Heading
",
76 | x: 150,
77 | y: 50,
78 | rotation: 50,
79 | },
80 | {
81 | name: 'block2',
82 | html: "Static Item
",
83 | x: 350,
84 | y: 300,
85 | static: true,
86 | },
87 | {
88 | name: 'block3',
89 | html: "Box
",
90 | x: 100,
91 | y: 600,
92 | rotation: -50,
93 | },
94 | {
95 | name: 'rocket',
96 | html: "
",
97 | x: 500,
98 | y: 500,
99 | width: 100,
100 | height: 100,
101 | },
102 | {
103 | name: 'moon',
104 | html: "
",
105 | x: 1000,
106 | y: 200,
107 | width: 200,
108 | height: 200,
109 | },
110 | {
111 | name: 'saturn',
112 | html: "
",
113 | x: 100,
114 | y: 500,
115 | width: 300,
116 | height: 300,
117 | },
118 | {
119 | name: 'alien',
120 | html: "
",
121 | x: 1050,
122 | y: 120,
123 | width: 100,
124 | height: 100,
125 | },
126 | {
127 | name: 'star',
128 | html: "
",
129 | x: 50,
130 | y: 900,
131 | width: 150,
132 | height: 150,
133 | },
134 | {
135 | name: 'astronaut',
136 | html: "
",
137 | x: 900,
138 | y: 700,
139 | width: 200,
140 | height: 200,
141 | rotation: 50,
142 | },
143 | {
144 | name: 'astronaut',
145 | html: "
",
146 | x: 1200,
147 | y: 400,
148 | width: 300,
149 | height: 300,
150 | rotation: -50,
151 | },
152 | ],
153 | isDrag: true,
154 | isResize: true,
155 | isRotate: true,
156 | isCopy: true,
157 | isDelete: true,
158 | isMultiSelect: true,
159 | maxNumberOfItems: 20,
160 | throttleDelay: 1,
161 | }
162 |
--------------------------------------------------------------------------------
/src/components/VueDragPlayground.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
17 |
18 |
24 |
35 |
46 |
47 |
64 |
65 |
66 |
73 |
86 |
87 |
94 |
107 |
108 |
115 |
128 |
129 |
136 |
149 |
150 |
151 |
171 |
172 |
173 |
174 |
175 |
176 |
1222 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | import { App, DefineComponent } from 'vue'
2 |
3 | // Define prop types
4 | export declare type DraggableItem = {
5 | id?: number
6 | name?: string
7 | html: string // HTML string to render
8 | x: number // X-coordinate for position
9 | y: number // Y-coordinate for position
10 | width?: number
11 | height?: number
12 | rotation?: number
13 | static?: boolean
14 | }
15 |
16 | export declare type DrapPlaygroundProps = {
17 | modelValue: DraggableItem[] // v-model binds to 'modelValue'
18 | isDrag?: boolean
19 | isResize?: boolean
20 | isRotate?: boolean
21 | isCopy?: boolean
22 | isMultiSelect?: boolean
23 | isDelete?: boolean
24 | throttleDelay?: number
25 | maxNumberOfItems?: number | undefined
26 | multiRotationMode?: 'proportional' | 'uniform' // "proportional" multirotation keep the based angle of item / "proportional" multirotation align items at the same angle"
27 | }
28 |
29 | // Define emits for custom events and v-model
30 | export declare type DrapPlaygroundEmits = {
31 | // v-model event: update the 'items' array
32 | 'update:modelValue': (value: DraggableItem[]) => void // v-model emit
33 | 'drag-start': (item: DraggableItem) => void
34 | dragging: (item: DraggableItem) => void
35 | 'drag-end': (item: DraggableItem) => void
36 | 'resize-start': (item: DraggableItem) => void
37 | resizing: (item: DraggableItem) => void
38 | 'resize-end': (item: DraggableItem) => void
39 | 'rotate-start': (item: DraggableItem) => void
40 | rotating: (item: DraggableItem) => void
41 | 'rotate-end': (item: DraggableItem) => void
42 | 'copy-items': (payload: { copiedItems: DraggableItem[]; createdItems: DraggableItem[] }) => void
43 | 'delete-items': (deletedItems: DraggableItem[]) => void
44 | }
45 |
46 | // Declare the Vue component itself
47 | declare const VueDragPlayground: DefineComponent<
48 | DrapPlaygroundProps,
49 | object,
50 | object,
51 | Record,
52 | Record,
53 | object,
54 | object,
55 | DrapPlaygroundEmits
56 | >
57 | // Declare the install function for the plugin system
58 | declare const _default: {
59 | install(app: App): void
60 | }
61 |
62 | // Export the default plugin object
63 | export default _default
64 |
65 | // Export the Vue component for direct use
66 | export { VueDragPlayground }
67 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /*!
2 | * Your Library Name v1.0.0
3 | * (c) [Year] [Your Name or Organization]
4 | * Released under the MIT License
5 | * https://opensource.org/licenses/MIT
6 | */
7 | import type { App } from 'vue'
8 | import VueDragPlayground from './components/VueDragPlayground.vue'
9 | import './assets/index.css'
10 |
11 | export { VueDragPlayground }
12 |
13 | // Install function for the plugin system
14 | export default {
15 | install(app: App) {
16 | app.component('VueDragPlayground', VueDragPlayground)
17 | },
18 | }
19 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: ['./src/**/*.{vue,js,ts,jsx,tsx}'],
4 | purge: ['./src/**/*.{vue,js,ts,jsx,tsx}'],
5 | theme: {
6 | extend: {},
7 | },
8 | plugins: [],
9 | }
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ESNext",
4 | "module": "ESNext",
5 | "moduleResolution": "node",
6 | "strict": true,
7 | "jsx": "preserve",
8 | "declaration": true, // Generate .d.ts files
9 | "declarationMap": false, // Disable source maps for .d.ts files
10 | "outDir": "dist", // Output directory for the build
11 | "esModuleInterop": true,
12 | "skipLibCheck": true,
13 | "allowJs": false,
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "noEmit": false,
17 | "lib": ["dom", "esnext"],
18 | "incremental": false // Disable incremental compilation to stop .tsbuildinfo
19 | },
20 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
21 | "exclude": ["node_modules", "dist"]
22 | }
23 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
2 | import fs from 'fs-extra'
3 | import { defineConfig } from 'vite'
4 | import vue from '@vitejs/plugin-vue'
5 | import path from 'path'
6 |
7 | // https://vite.dev/config/
8 | export default defineConfig({
9 | plugins: [
10 | vue(),
11 | cssInjectedByJsPlugin(),
12 | {
13 | name: 'copy-dts-file',
14 | writeBundle() {
15 | const dtsPath = path.resolve(__dirname, 'src/index.d.ts')
16 | const distDtsPath = path.resolve(__dirname, 'dist/types/index.d.ts')
17 |
18 | // Copy the .d.ts file to the dist/types directory
19 | fs.copySync(dtsPath, distDtsPath)
20 | },
21 | },
22 | ],
23 | resolve: {
24 | alias: {
25 | '@': path.resolve(__dirname, 'src'), // Alias '@' to your src directory
26 | },
27 | },
28 | build: {
29 | lib: {
30 | entry: path.resolve(__dirname, 'src/index.ts'), // Adjust path to index.js if needed
31 | name: 'VueDragPlayground',
32 | fileName: (format) => `vue-drag-playground.${format}.js`,
33 | formats: ['es', 'umd'],
34 | },
35 | rollupOptions: {
36 | // Ensure external dependencies are not bundled into your library
37 | external: ['vue'],
38 | output: {
39 | globals: {
40 | vue: 'Vue',
41 | },
42 | },
43 | },
44 | assetsInlineLimit: 0, // Ensure non-inline assets get copied
45 | copyPublicDir: true, // Ensure public files get copied to build folder
46 | },
47 | })
48 |
--------------------------------------------------------------------------------