├── .editorconfig ├── .github └── workflows │ ├── cypress.yml │ ├── pages.yml │ ├── publish.yml │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── cypress.config.js ├── cypress └── support │ ├── commands.js │ ├── component-index.html │ └── component.js ├── histoire.config.js ├── index.html ├── package-lock.json ├── package.json ├── public └── favicon.ico ├── src ├── App.vue ├── components │ ├── vue-draggable-resizable.css │ └── vue-draggable-resizable.vue ├── install.js ├── main.js └── utils │ ├── dom.js │ └── fns.js ├── stories ├── advanced │ └── ParentResize.story.vue ├── aspect-ratio │ ├── CostrainedInParent.story.vue │ ├── CostrainedInParentWithGrid.story.vue │ ├── ForcedOnGridWithOffset.story.vue │ ├── LockAspectRatio.story.vue │ ├── MaxWidthMaxHeight.story.vue │ ├── MinWidthMinHeight.story.vue │ └── WithGrid.story.vue ├── basic │ ├── Active.story.vue │ ├── Axis.story.vue │ ├── Basic.story.vue │ ├── CancelHandle.story.vue │ ├── ControlledComponent.story.vue │ ├── DragHandle.story.vue │ ├── Form.story.vue │ ├── Handles.story.vue │ ├── MaxWidthMaxHeight.story.vue │ ├── MinWidthMinHeight.story.vue │ ├── NativeDragEnabled.story.vue │ ├── NotDraggable.story.vue │ ├── NotResizable.story.vue │ ├── PreventDeactivation.story.vue │ ├── Scale.story.vue │ ├── SizeAuto.story.vue │ └── ZIndex.story.vue ├── callback │ ├── OnDrag.story.vue │ ├── OnDragStart.story.vue │ ├── OnResize.story.vue │ └── OnResizeStart.story.vue ├── events │ ├── Activated.story.vue │ ├── Dragging.story.vue │ └── Resizing.story.vue ├── grid │ ├── Grid.story.vue │ ├── GridWithMaxWidthMaxHeight.story.vue │ └── GridWithMinWidthMinHeight.story.vue ├── histoire.setup.js ├── how-to │ ├── DragMultiple.story.vue │ ├── FrameSelection.story.vue │ └── HowTo.stories.js ├── parent │ ├── Basic.story.vue │ ├── ControlledComponent.story.vue │ ├── DisableUserSelect.story.vue │ ├── Grid.story.vue │ ├── ParentGridEvenOffset.story.vue │ ├── ParentGridMaxWidthMaxHeight.story.vue │ ├── ParentGridOffset.story.vue │ └── ParentMaxWidthMaxHeight.story.vue ├── styles.css └── styling │ ├── Active.story.vue │ ├── Component.story.vue │ ├── Dragging.story.vue │ ├── HandleSlots.story.vue │ ├── Handles.story.vue │ └── Resizing.story.vue ├── tests ├── cypress │ ├── base.cy.js │ ├── events.cy.js │ ├── props │ │ ├── active.cy.js │ │ ├── axis.cy.js │ │ ├── callbacks.cy.js │ │ ├── classes.cy.js │ │ ├── drag-cancel.cy.js │ │ ├── drag-handle.cy.js │ │ ├── draggable.cy.js │ │ ├── enable-native-drag.cy.js │ │ ├── grid.cy.js │ │ ├── handles.cy.js │ │ ├── lock-aspect-ratio.cy.js │ │ ├── max-width-max-height.cy.js │ │ ├── min-width-min-height.cy.js │ │ ├── parent-grid.cy.js │ │ ├── parent-position.cy.js │ │ ├── parent-size.cy.js │ │ ├── parent.cy.js │ │ ├── position.cy.js │ │ ├── prevent-deactivation.cy.js │ │ ├── resizable.cy.js │ │ ├── scale.cy.js │ │ ├── size.cy.js │ │ └── z-index.cy.js │ └── utils.js ├── fns.spec.js └── validation.spec.js └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/cypress.yml: -------------------------------------------------------------------------------- 1 | name: Component Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | workflow_dispatch: 9 | 10 | jobs: 11 | cypress-run: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | 17 | - name: Cypress run 18 | uses: cypress-io/github-action@v6 19 | with: 20 | component: true 21 | browser: chrome 22 | -------------------------------------------------------------------------------- /.github/workflows/pages.yml: -------------------------------------------------------------------------------- 1 | name: Publish GitHub pages 2 | 3 | on: 4 | release: 5 | types: [published] 6 | workflow_dispatch: 7 | 8 | permissions: 9 | contents: read 10 | pages: write 11 | id-token: write 12 | 13 | concurrency: 14 | group: "pages" 15 | cancel-in-progress: false 16 | 17 | jobs: 18 | build: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: 18 27 | cache: npm 28 | 29 | - name: Install dependencies 30 | run: npm ci 31 | 32 | - name: Build histoire 33 | run: npm run story:build 34 | 35 | - name: Upload artifact 36 | uses: actions/upload-pages-artifact@v3 37 | with: 38 | path: ./.histoire/dist 39 | 40 | deploy: 41 | runs-on: ubuntu-latest 42 | needs: build 43 | environment: 44 | name: github-pages 45 | url: ${{ steps.deployment.outputs.page_url }} 46 | 47 | steps: 48 | - name: Deploy to GitHub Pages 49 | id: deployment 50 | uses: actions/deploy-pages@v4 51 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Package to NPM 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | publish: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v4 13 | 14 | - name: Setup .npmrc file to publish to npm 15 | uses: actions/setup-node@v4 16 | with: 17 | node-version: 18 18 | cache: npm 19 | registry-url: 'https://registry.npmjs.org' 20 | 21 | - id: get_version 22 | uses: battila7/get-version-action@v2 23 | 24 | - run: npm --no-git-tag-version version ${{ steps.get_version.outputs.version-without-v }} 25 | 26 | - name: Install dependencies 27 | run: npm ci 28 | 29 | - name: Build 30 | run: npm run build 31 | 32 | - name: Publish the package with public visibility 33 | run: npm publish --access public 34 | env: 35 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Continuous tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | concurrency: 9 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 10 | cancel-in-progress: true 11 | 12 | jobs: 13 | build-and-test: 14 | runs-on: ubuntu-latest 15 | name: Build and test 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | 20 | - name: Setup node 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 18 24 | cache: 'npm' 25 | 26 | - name: Install 27 | run: npm install 28 | 29 | - name: Run Tests 30 | run: npm run test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | /tests/feature/coverage/* 5 | /docs 6 | .histoire 7 | # local env files 8 | .env.local 9 | .env.*.local 10 | 11 | # Log files 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw* 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `vue-draggable-resizable` will be documented in this file 4 | 5 | ## 3.0.0 - 2024-01-27 6 | - Migrates to Vue 3 7 | - Updates all packages 8 | - Adds Vitest instead of Mocha and Karma for testing 9 | - Adds Vite instead of Webpack 10 | - Adds Cypress to component-test the package in real browsers 11 | - Fix `drag-stop` and `resize-stop` events 12 | - And minor changes 13 | 14 | ## 2.3.0 - 2020-12-17 15 | - `scale` prop supports one or two values 16 | - component with `active` prop to `true` emits `activated` 17 | - upgrade vue-test-utils 18 | - small bugfixes 19 | 20 | ## 2.2.0 - 2020-05-03 21 | - `auto` height and width 22 | - `onDrag` callback 23 | - `onResize` callback 24 | 25 | ## 2.0.0 - 2019-09-09 26 | - released 2.0.0 27 | 28 | ## 1.7.0 - 2018-04-04 29 | - add touch events 30 | 31 | ## 1.6.0 - 2018-01-16 32 | - add `dragHandle` and `dragCancel` props 33 | 34 | ## 1.5.0 - 2017-09-19 35 | - implement :z prop and watcher 36 | 37 | ## 1.4.0 - 2017-09-17 38 | - add `active` synched prop 39 | 40 | ## 1.3.0 - 2017-09-11 41 | - add `resizing` and `dragging` css classes 42 | 43 | ## 1.2.0 - 2017-07-17 44 | - add `maximize` prop 45 | 46 | ## 1.1.0 - 2017-07-03 47 | - add `dragstop` and `resizestop` events 48 | 49 | ## 1.0.0 - 2017-06-08 50 | - Initial release 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Maurizio Bonani 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 |

logo

2 |

VueDraggableResizable 3

3 | 4 | [![Latest Version on NPM](https://img.shields.io/npm/v/vue-draggable-resizable.svg?style=flat-square)](https://npmjs.com/package/vue-draggable-resizable) 5 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 6 | [![npm](https://img.shields.io/npm/dt/vue-draggable-resizable.svg?style=flat-square)](https://www.npmjs.com/package/vue-draggable-resizable) 7 | 8 | > Vue Component for draggable and resizable elements. 9 | 10 | If you are looking for the version 1 of the component, it is available on the [v1 branch](https://github.com/mauricius/vue-draggable-resizable/tree/v1). 11 | 12 | ## Table of Contents 13 | 14 | * [Features](#features) 15 | * [Live Playground](#live-playground) 16 | * [Install and basic usage](#install-and-basic-usage) 17 | * [Props](#props) 18 | * [Events](#events) 19 | * [Styling](#styling) 20 | * [Contributing](#contributing) 21 | * [License](#license) 22 | 23 | ### Features 24 | 25 | * No dependencies 26 | * Use draggable, resizable or both 27 | * Define handles for resizing 28 | * Restrict size and movement to parent element 29 | * Snap element to custom grid 30 | * Restrict drag to vertical or horizontal axis 31 | * Maintain aspect ratio 32 | * Touch enabled 33 | * Use your own classes 34 | * Provide your own markup for handles 35 | 36 | ### Live Playground 37 | 38 | For examples of the component go to the [live playground](https://mauricius.github.io/vue-draggable-resizable/) 39 | 40 | Alternatively you can run the playground on your own computer: 41 | 42 | * Clone this repository 43 | * `npm install` 44 | * `npm run story:dev` 45 | * Visit [http://localhost:6006/](http://localhost:6006/) 46 | 47 | --- 48 | 49 | ## Install and basic usage 50 | 51 | ```bash 52 | $ npm install --save vue-draggable-resizable 53 | ``` 54 | 55 | 56 | Register the component globally 57 | 58 | ```js 59 | // main.js 60 | import { createApp } from 'vue' 61 | import VueDraggableResizable from 'vue-draggable-resizable' 62 | import App from './App.vue' 63 | 64 | createApp(App) 65 | .component("vue-draggable-resizable", VueDraggableResizable) 66 | .mount('#app') 67 | ``` 68 | 69 | You may now use the component in your markup 70 | 71 | ```vue 72 | // App.vue 73 | 80 | ``` 81 | 82 | The component itself does not include any CSS. You'll need to include it separately in your `App.vue`: 83 | 84 | ```vue 85 | 88 | ``` 89 | 90 | ### Props 91 | 92 | #### className 93 | Type: `String`
94 | Required: `false`
95 | Default: `vdr` 96 | 97 | Used to set the custom `class` of a draggable-resizable component. 98 | 99 | ```html 100 | 101 | ``` 102 | 103 | #### classNameDraggable 104 | Type: `String`
105 | Required: `false`
106 | Default: `draggable` 107 | 108 | Used to set the custom `class` of a draggable-resizable component when `draggable` is enable. 109 | 110 | ```html 111 | 112 | ``` 113 | 114 | #### classNameResizable 115 | Type: `String`
116 | Required: `false`
117 | Default: `resizable` 118 | 119 | Used to set the custom `class` of a draggable-resizable component when `resizable` is enable. 120 | 121 | ```html 122 | 123 | ``` 124 | 125 | #### classNameDragging 126 | Type: `String`
127 | Required: `false`
128 | Default: `dragging` 129 | 130 | Used to set the custom `class` of a draggable-resizable component when is dragging. 131 | 132 | ```html 133 | 134 | ``` 135 | 136 | #### classNameResizing 137 | Type: `String`
138 | Required: `false`
139 | Default: `resizing` 140 | 141 | Used to set the custom `class` of a draggable-resizable component when is resizing. 142 | 143 | ```html 144 | 145 | ``` 146 | 147 | #### classNameActive 148 | Type: `String`
149 | Required: `false`
150 | Default: `active` 151 | 152 | Used to set the custom `class` of a draggable-resizable component when is active. 153 | 154 | ```html 155 | 156 | ``` 157 | 158 | #### classNameHandle 159 | Type: `String`
160 | Required: `false`
161 | Default: `handle` 162 | 163 | Used to set the custom common `class` of each handle element. This way you can style each handle individually using the selector `-`, where `handle code` identifies one of the handles provided by the `handle` prop. 164 | 165 | So for example, this component: 166 | 167 | ```html 168 | 169 | ``` 170 | 171 | renders the following: 172 | 173 | ```html 174 |
175 |
176 |
177 |
178 | [...] 179 |
180 | ``` 181 | 182 | #### scale 183 | Type: `Number|Array`
184 | Required: `false`
185 | Default: `1` 186 | 187 | The `scale` prop controls the scale property when the CSS 3 [scale transformation](https://developer.mozilla.org/en-US/docs/Web/CSS/transform-function/scale) is applied to one of the parent elements. If not provided the default value is 1. 188 | 189 | ```html 190 | 191 | 192 | 193 | ``` 194 | 195 | #### disableUserSelect 196 | Type: `Boolean`
197 | Required: `false`
198 | Default: `true` 199 | 200 | By default, the component adds the style declaration `'user-select:none'` to itself to prevent text selection during drag. You can disable this behaviour by setting this prop to `false`. 201 | 202 | ```html 203 | 204 | ``` 205 | 206 | #### enableNativeDrag 207 | Type: `Boolean`
208 | Required: `false`
209 | Default: `false` 210 | 211 | By default, the browser's native drag and drop funcionality (usually used for images and some other elements) is disabled, as it may conflict with the one provided by the component. If you need, for whatever reason, to have this functionality back you can set this prop to `true`. 212 | 213 | ```html 214 | 215 | ``` 216 | 217 | #### active 218 | Type: `Boolean`
219 | Required: `false`
220 | Default: `false` 221 | 222 | Determines if the component should be active or not. The prop reacts to changes and also can be used with the `sync`[modifier](https://vuejs.org/v2/guide/components.html#sync-Modifier) to keep the state in sync with the parent. You can use along with the `preventDeactivation` prop in order to fully control the active behavior from outside the component. 223 | 224 | ```html 225 | 226 | ``` 227 | 228 | #### preventDeactivation 229 | Type: `Boolean`
230 | Required: `false`
231 | Default: `false` 232 | 233 | Determines if the component should be deactivated when the user clicks/taps outside it. 234 | 235 | ```html 236 | 237 | ``` 238 | 239 | #### draggable 240 | Type: `Boolean`
241 | Required: `false`
242 | Default: `true` 243 | 244 | Defines it the component should be draggable or not. 245 | 246 | ```html 247 | 248 | ``` 249 | 250 | #### resizable 251 | Type: `Boolean`
252 | Required: `false`
253 | Default: `true` 254 | 255 | Defines it the component should be resizable or not. 256 | 257 | ```html 258 | 259 | ``` 260 | 261 | #### w 262 | Type: `Number|String`
263 | Required: `false`
264 | Default: `200` 265 | 266 | Define the initial width of the element. It also supports `auto`, but when you start resizing the value will fallback to a number. 267 | 268 | ```html 269 | 270 | ``` 271 | 272 | #### h 273 | Type: `Number|String`
274 | Required: `false`
275 | Default: `200` 276 | 277 | Define the initial height of the element. It also supports `auto`, but when you start resizing the value will fallback to a number. 278 | 279 | ```html 280 | 281 | ``` 282 | 283 | #### minWidth 284 | Type: `Number`
285 | Required: `false`
286 | Default: `50` 287 | 288 | Define the minimal width of the element. 289 | 290 | ```html 291 | 292 | ``` 293 | 294 | #### minHeight 295 | Type: `Number`
296 | Required: `false`
297 | Default: `50` 298 | 299 | Define the minimal height of the element. 300 | 301 | ```html 302 | 303 | ``` 304 | 305 | #### maxWidth 306 | Type: `Number`
307 | Required: `false`
308 | Default: `null` 309 | 310 | Define the maximum width of the element. 311 | 312 | ```html 313 | 314 | ``` 315 | 316 | #### maxHeight 317 | Type: `Number`
318 | Required: `false`
319 | Default: `null` 320 | 321 | Define the maximum height of the element. 322 | 323 | ```html 324 | 325 | ``` 326 | 327 | #### x 328 | Type: `Number`
329 | Required: `false`
330 | Default: `0` 331 | 332 | Define the initial x position of the element. 333 | 334 | ```html 335 | 336 | ``` 337 | 338 | #### y 339 | Type: `Number`
340 | Required: `false`
341 | Default: `0` 342 | 343 | Define the initial y position of the element. 344 | 345 | ```html 346 | 347 | ``` 348 | 349 | #### z 350 | Type: `Number|String`
351 | Required: `false`
352 | Default: `auto` 353 | 354 | Define the z-index of the element. 355 | 356 | ```html 357 | 358 | ``` 359 | 360 | #### handles 361 | Type: `Array`
362 | Required: `false`
363 | Default: `['tl', 'tm', 'tr', 'mr', 'br', 'bm', 'bl', 'ml']` 364 | 365 | Define the array of handles to restrict the element resizing: 366 | * `tl` - Top left 367 | * `tm` - Top middle 368 | * `tr` - Top right 369 | * `mr` - Middle right 370 | * `br` - Bottom right 371 | * `bm` - Bottom middle 372 | * `bl` - Bottom left 373 | * `ml` - Middle left 374 | 375 | ```html 376 | 377 | ``` 378 | 379 | #### axis 380 | Type: `String`
381 | Required: `false`
382 | Default: `both` 383 | 384 | Define the axis on which the element is draggable. Available values are `x`, `y` or `both`. 385 | 386 | ```html 387 | 388 | ``` 389 | 390 | #### grid 391 | Type: `Array`
392 | Required: `false`
393 | Default: `[1,1]` 394 | 395 | Define the grid on which the element is snapped. 396 | 397 | ```html 398 | 399 | ``` 400 | 401 | #### parent 402 | Type: `Boolean`
403 | Required: `false`
404 | Default: `false` 405 | 406 | Restricts the movement and the dimensions of the component to the parent. 407 | 408 | ```html 409 | 410 | ``` 411 | 412 | #### dragHandle 413 | Type: `String`
414 | Required: `false` 415 | 416 | Defines the selector that should be used to drag the component. 417 | 418 | ```html 419 | 420 | ``` 421 | 422 | #### dragCancel 423 | Type: `String`
424 | Required: `false` 425 | 426 | Defines a selector that should be used to prevent drag initialization. 427 | 428 | ```html 429 | 430 | ``` 431 | 432 | #### lockAspectRatio 433 | Type: `Boolean`
434 | Required: `false`
435 | Default: `false` 436 | 437 | The `lockAspectRatio` property is used to lock aspect ratio. This property doesn't play well with `grid`, so make sure to use only one at a time. 438 | 439 | ```html 440 | 441 | ``` 442 | 443 | #### onDragStart 444 | Type: `Function`
445 | Required: `false`
446 | Default: `null` 447 | 448 | Called when dragging starts (element is clicked or touched). If `false` is returned by any handler, the action will cancel. You can use this function to prevent bubbling of events. 449 | 450 | ```html 451 | 452 | ``` 453 | 454 | ```js 455 | function onDragStartCallback(ev){ 456 | ... 457 | // return false; — for cancel 458 | } 459 | ``` 460 | 461 | #### onDrag 462 | Type: `Function`
463 | Required: `false`
464 | Default: `null` 465 | 466 | Called before the element is dragged. The function receives the next values of `x` and `y`. If `false` is returned by any handler, the action will cancel. 467 | 468 | ```html 469 | 470 | ``` 471 | 472 | ```js 473 | function onDragStartCallback(x, y){ 474 | ... 475 | // return false; — for cancel 476 | } 477 | ``` 478 | 479 | 480 | #### onResizeStart 481 | Type: `Function`
482 | Required: `false`
483 | Default: `null` 484 | 485 | Called when resizing starts (handle is clicked or touched). If `false` is returned by any handler, the action will cancel. 486 | 487 | ```html 488 | 489 | ``` 490 | 491 | ```js 492 | 493 | function onResizeStartCallback(handle, ev){ 494 | ... 495 | // return false; — for cancel 496 | } 497 | ``` 498 | 499 | #### onResize 500 | Type: `Function`
501 | Required: `false`
502 | Default: `null` 503 | 504 | Called before the element is resized. The function receives the handle and the next values of `x`, `y`, `width` and `height`. If `false` is returned by any handler, the action will cancel. 505 | 506 | ```html 507 | 508 | ``` 509 | 510 | ```js 511 | 512 | function onResizeStartCallback(handle, x, y, width, height){ 513 | ... 514 | // return false; — for cancel 515 | } 516 | ``` 517 | --- 518 | 519 | ### Events 520 | 521 | #### activated 522 | 523 | Parameters: `-` 524 | 525 | Called whenever the component gets clicked, in order to show handles. 526 | 527 | ```html 528 | 529 | ``` 530 | 531 | #### deactivated 532 | 533 | Parameters: `-` 534 | 535 | Called whenever the user clicks anywhere outside the component, in order to deactivate it. 536 | 537 | ```html 538 | 539 | ``` 540 | 541 | #### resizing 542 | 543 | Parameters: 544 | * `left` the X position of the element 545 | * `top` the Y position of the element 546 | * `width` the width of the element 547 | * `height` the height of the element 548 | 549 | Called whenever the component gets resized. 550 | 551 | ```html 552 | 553 | ``` 554 | 555 | #### resizestop 556 | 557 | Parameters: 558 | * `left` the X position of the element 559 | * `top` the Y position of the element 560 | * `width` the width of the element 561 | * `height` the height of the element 562 | 563 | Called whenever the component stops getting resized. 564 | 565 | ```html 566 | 567 | ``` 568 | 569 | #### dragging 570 | 571 | Parameters: 572 | * `left` the X position of the element 573 | * `top` the Y position of the element 574 | 575 | Called whenever the component gets dragged. 576 | 577 | ```html 578 | 579 | ``` 580 | 581 | #### dragstop 582 | 583 | Parameters: 584 | * `left` the X position of the element 585 | * `top` the Y position of the element 586 | 587 | Called whenever the component stops getting dragged. 588 | 589 | ```html 590 | 591 | ``` 592 | 593 | --- 594 | 595 | ### Styling 596 | 597 | You can style the component using appropriate class names passed as props to the component. Moreover you can replace the default styles for the handles, provided in the source file `vue-draggable-resizable.css`, but you should take care to define position and size for them. The default classes for handles are `handle` and `handle-tl`, `handle-br` and so on. 598 | 599 | The component also provides [named slots](https://vuejs.org/guide/components/slots.html#named-slots) for each handle, so you can use your markup inside each one. 600 | 601 | ## Thanks 602 | 603 | Thanks to @kirillmurashov for his work on [vue-drag-resize](https://github.com/kirillmurashov/vue-drag-resize) component. 604 | 605 | ## Security 606 | 607 | If you discover any security related issues, please email maurizio.bonani@gmail.com instead of using the issue tracker. 608 | 609 | ## Contributing 610 | 611 | Any contribution to the code or any part of the documentation and any idea and/or suggestion are very welcome. 612 | 613 | ``` bash 614 | # serve with hot reload at localhost:8080 615 | npm run dev 616 | 617 | # distribution build 618 | npm run build 619 | 620 | # build the histoire docs 621 | npm run story:build 622 | 623 | # run tests 624 | npm run test 625 | 626 | # run histoire at localhost:6006 627 | npm run story:dev 628 | ``` 629 | 630 | ## License 631 | 632 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 633 | -------------------------------------------------------------------------------- /cypress.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'cypress'; 2 | 3 | export default defineConfig({ 4 | component: { 5 | devServer: { 6 | framework: "vue", 7 | bundler: "vite", 8 | }, 9 | }, 10 | }); 11 | -------------------------------------------------------------------------------- /cypress/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | 11 | import '@4tw/cypress-drag-drop' 12 | -------------------------------------------------------------------------------- /cypress/support/component-index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Components App 8 | 9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /cypress/support/component.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/component.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | import '../../src/components/vue-draggable-resizable.css' 19 | 20 | // Alternatively you can use CommonJS syntax: 21 | // require('./commands') 22 | 23 | import { mount } from 'cypress/vue' 24 | 25 | Cypress.Commands.add('mount', (...args) => { 26 | return mount(...args).then(({ wrapper }) => { 27 | return cy.wrap(wrapper).as('vue') 28 | }) 29 | }) 30 | 31 | // Example use: 32 | // cy.mount(MyComponent) 33 | -------------------------------------------------------------------------------- /histoire.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'histoire' 2 | import { HstVue } from '@histoire/plugin-vue' 3 | 4 | export default defineConfig({ 5 | vite: { 6 | base: "", 7 | }, 8 | routerMode: 'hash', 9 | plugins: [ 10 | HstVue(), 11 | ], 12 | setupFile: './stories/histoire.setup.js', 13 | tree: { 14 | groups: [ 15 | { 16 | id: 'top', 17 | title: '', 18 | }, 19 | { 20 | title: 'Basic Stories', 21 | include: file => file.path.startsWith('stories/basic'), 22 | }, 23 | { 24 | title: 'Grid Stories', 25 | include: file => file.path.startsWith('stories/grid'), 26 | }, 27 | { 28 | title: 'Parent Stories', 29 | include: file => file.path.startsWith('stories/parent'), 30 | }, 31 | { 32 | title: 'Aspect Ratio Stories', 33 | include: file => file.path.startsWith('stories/aspect-ratio'), 34 | }, 35 | { 36 | title: 'Styling Stories', 37 | include: file => file.path.startsWith('stories/styling'), 38 | }, 39 | { 40 | title: 'Events Stories', 41 | include: file => file.path.startsWith('stories/events'), 42 | }, 43 | { 44 | title: 'Callback Stories', 45 | include: file => file.path.startsWith('stories/callback'), 46 | }, 47 | { 48 | title: 'Advanced Stories', 49 | include: file => file.path.startsWith('stories/advanced'), 50 | }, 51 | { 52 | title: 'How To', 53 | include: file => file.path.startsWith('stories/how-to'), 54 | }, 55 | ], 56 | }, 57 | }) 58 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | vue-draggable-resizable 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-draggable-resizable", 3 | "version": "3.0.0-beta.2", 4 | "author": "Maurizio Bonani ", 5 | "private": false, 6 | "description": "Vue3 Component for resizable and draggable elements", 7 | "type": "module", 8 | "files": [ 9 | "dist" 10 | ], 11 | "main": "dist/vue-draggable-resizable.umd.cjs", 12 | "module": "dist/vue-draggable-resizable.js", 13 | "unpkg": "dist/vue-draggable-resizable.umd.cjs", 14 | "browser": { 15 | "./sfc": "src/vue-draggable-resizable.vue" 16 | }, 17 | "exports": { 18 | ".": { 19 | "import": "./dist/vue-draggable-resizable.js", 20 | "require": "./dist/vue-draggable-resizable.umd.cjs" 21 | }, 22 | "./style.css": { 23 | "import": "./dist/style.css", 24 | "require": "./dist/style.css" 25 | } 26 | }, 27 | "scripts": { 28 | "dev": "vite", 29 | "build": "vite build", 30 | "test": "vitest run", 31 | "test:ui": "vitest --ui", 32 | "test:coverage": "vitest run --coverage", 33 | "test:watch": "vitest", 34 | "cypress": "npx cypress open", 35 | "story:dev": "histoire dev", 36 | "story:build": "histoire build", 37 | "story:preview": "histoire preview" 38 | }, 39 | "devDependencies": { 40 | "@4tw/cypress-drag-drop": "^2.2.5", 41 | "@babel/core": "^7.18.6", 42 | "@histoire/plugin-vue": "^0.17.6", 43 | "@vitejs/plugin-vue": "^4.3.4", 44 | "@vitest/ui": "^0.15.1", 45 | "@vue/test-utils": "^2.2.7", 46 | "babel-loader": "^8.2.5", 47 | "cypress": "^13.6.2", 48 | "histoire": "^0.17.6", 49 | "jsdom": "^19.0.0", 50 | "vite": "^4.4.9", 51 | "vitest": "^0.28.4", 52 | "vue": "^3.2.25", 53 | "vue-loader": "^16.8.3" 54 | }, 55 | "peerDependencies": { 56 | "vue": "^3.2.25" 57 | }, 58 | "repository": { 59 | "type": "git", 60 | "url": "https://github.com/mauricius/vue-draggable-resizable.git" 61 | }, 62 | "license": "MIT", 63 | "bugs": { 64 | "url": "https://github.com/mauricius/vue-draggable-resizable/issues" 65 | }, 66 | "homepage": "https://github.com/mauricius/vue-draggable-resizable", 67 | "keywords": [ 68 | "vue", 69 | "component", 70 | "dragabble", 71 | "resizable" 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mauricius/vue-draggable-resizable/72cda0844f765c85df7ae423de12bb54790bcc75/public/favicon.ico -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /src/components/vue-draggable-resizable.css: -------------------------------------------------------------------------------- 1 | .vdr { 2 | touch-action: none; 3 | position: absolute; 4 | box-sizing: border-box; 5 | border: 1px dashed black; 6 | } 7 | .handle { 8 | box-sizing: border-box; 9 | position: absolute; 10 | width: 10px; 11 | height: 10px; 12 | background: #EEE; 13 | border: 1px solid #333; 14 | } 15 | .handle-tl { 16 | top: -10px; 17 | left: -10px; 18 | cursor: nw-resize; 19 | } 20 | .handle-tm { 21 | top: -10px; 22 | left: 50%; 23 | margin-left: -5px; 24 | cursor: n-resize; 25 | } 26 | .handle-tr { 27 | top: -10px; 28 | right: -10px; 29 | cursor: ne-resize; 30 | } 31 | .handle-ml { 32 | top: 50%; 33 | margin-top: -5px; 34 | left: -10px; 35 | cursor: w-resize; 36 | } 37 | .handle-mr { 38 | top: 50%; 39 | margin-top: -5px; 40 | right: -10px; 41 | cursor: e-resize; 42 | } 43 | .handle-bl { 44 | bottom: -10px; 45 | left: -10px; 46 | cursor: sw-resize; 47 | } 48 | .handle-bm { 49 | bottom: -10px; 50 | left: 50%; 51 | margin-left: -5px; 52 | cursor: s-resize; 53 | } 54 | .handle-br { 55 | bottom: -10px; 56 | right: -10px; 57 | cursor: se-resize; 58 | } 59 | @media only screen and (max-width: 768px) { 60 | [class*="handle-"]:before { 61 | content: ''; 62 | left: -10px; 63 | right: -10px; 64 | bottom: -10px; 65 | top: -10px; 66 | position: absolute; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/install.js: -------------------------------------------------------------------------------- 1 | import './components/vue-draggable-resizable.css' 2 | 3 | import VueDraggableResizable from './components/vue-draggable-resizable.vue' 4 | 5 | export function install (Vue) { 6 | if (install.installed) return 7 | install.installed = true 8 | Vue.component('VueDraggableResizable', VueDraggableResizable) 9 | } 10 | 11 | const plugin = { 12 | install 13 | } 14 | 15 | let GlobalVue = null 16 | if (typeof window !== 'undefined') { 17 | GlobalVue = window.Vue 18 | } else if (typeof global !== 'undefined') { 19 | GlobalVue = global.Vue 20 | } 21 | if (GlobalVue) { 22 | GlobalVue.use(plugin) 23 | } 24 | 25 | export default VueDraggableResizable 26 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | 4 | const app = createApp(App) 5 | app.config.productionTip = false 6 | app.mount('#app') 7 | -------------------------------------------------------------------------------- /src/utils/dom.js: -------------------------------------------------------------------------------- 1 | import { isFunction } from './fns' 2 | 3 | export function matchesSelectorToParentElements (el, selector, baseNode) { 4 | let node = el 5 | 6 | const matchesSelectorFunc = [ 7 | 'matches', 8 | 'webkitMatchesSelector', 9 | 'mozMatchesSelector', 10 | 'msMatchesSelector', 11 | 'oMatchesSelector' 12 | ].find(func => isFunction(node[func])) 13 | 14 | if (!isFunction(node[matchesSelectorFunc])) return false 15 | 16 | do { 17 | if (node[matchesSelectorFunc](selector)) return true 18 | if (node === baseNode) return false 19 | node = node.parentNode 20 | } while (node) 21 | 22 | return false 23 | } 24 | 25 | export function getComputedSize ($el) { 26 | const style = window.getComputedStyle($el) 27 | 28 | return [ 29 | parseFloat(style.getPropertyValue('width'), 10), 30 | parseFloat(style.getPropertyValue('height'), 10) 31 | ] 32 | } 33 | 34 | export function addEvent (el, event, handler) { 35 | if (!el) { 36 | return 37 | } 38 | if (el.attachEvent) { 39 | el.attachEvent('on' + event, handler) 40 | } else if (el.addEventListener) { 41 | el.addEventListener(event, handler, true) 42 | } else { 43 | el['on' + event] = handler 44 | } 45 | } 46 | 47 | export function removeEvent (el, event, handler) { 48 | if (!el) { 49 | return 50 | } 51 | if (el.detachEvent) { 52 | el.detachEvent('on' + event, handler) 53 | } else if (el.removeEventListener) { 54 | el.removeEventListener(event, handler, true) 55 | } else { 56 | el['on' + event] = null 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/utils/fns.js: -------------------------------------------------------------------------------- 1 | export function isFunction (func) { 2 | return (typeof func === 'function' || Object.prototype.toString.call(func) === '[object Function]') 3 | } 4 | 5 | export function snapToGrid (grid, pendingX, pendingY, scale = 1) { 6 | const [scaleX, scaleY] = typeof scale === 'number' ? [scale, scale] : scale 7 | const x = Math.round((pendingX / scaleX) / grid[0]) * grid[0] 8 | const y = Math.round((pendingY / scaleY) / grid[1]) * grid[1] 9 | return [x, y] 10 | } 11 | 12 | export function getSize (el) { 13 | const rect = el.getBoundingClientRect() 14 | 15 | return [ 16 | parseInt(rect.width), 17 | parseInt(rect.height) 18 | ] 19 | } 20 | 21 | export function computeWidth (parentWidth, left, right) { 22 | return parentWidth - left - right 23 | } 24 | 25 | export function computeHeight (parentHeight, top, bottom) { 26 | return parentHeight - top - bottom 27 | } 28 | 29 | export function restrictToBounds (value, min, max) { 30 | if (min !== null && value < min) { 31 | return min 32 | } 33 | 34 | if (max !== null && max < value) { 35 | return max 36 | } 37 | 38 | return value 39 | } 40 | -------------------------------------------------------------------------------- /stories/advanced/ParentResize.story.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 15 | -------------------------------------------------------------------------------- /stories/aspect-ratio/CostrainedInParent.story.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 34 | 35 | 36 | ## Component with Aspect Ratio costrained in parent 37 | 38 | A component costrained in parent, with `:lock-aspect-ratio` prop to keep the aspect ratio of the component during resize. 39 | 40 | -------------------------------------------------------------------------------- /stories/aspect-ratio/CostrainedInParentWithGrid.story.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 54 | -------------------------------------------------------------------------------- /stories/aspect-ratio/ForcedOnGridWithOffset.story.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 53 | -------------------------------------------------------------------------------- /stories/aspect-ratio/LockAspectRatio.story.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 31 | 32 | ## Component with Aspect Ratio 33 | 34 | A component, with `:lock-aspect-ratio` prop to keep the aspect ratio of the component during resize. 35 | 36 | -------------------------------------------------------------------------------- /stories/aspect-ratio/MaxWidthMaxHeight.story.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 34 | 35 | 36 | ## Component with Aspect Ratio and maxWidth, maxHeight 37 | 38 | A component, with `:lock-aspect-ratio` prop and `:max-width` set to `300` and `:max-height` set to `250`. Notice that locking the aspect ratio also forces the max width to be 250. 39 | 40 | -------------------------------------------------------------------------------- /stories/aspect-ratio/MinWidthMinHeight.story.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 34 | 35 | 36 | ## Component with Aspect Ratio and minHeight, minWidth 37 | 38 | A component, with `:lock-aspect-ratio` prop and `:min-height` set to `100` and `:min-width` set to `50`. Notice that locking the aspect ratio also forces the Min Width to be 100 (Factor: 1 / 1). 39 | 40 | -------------------------------------------------------------------------------- /stories/aspect-ratio/WithGrid.story.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 51 | 52 | 53 | ## Component with Aspect Ratio costrained on 20x20 Grid 54 | 55 | Having a component costrained on a Grid doesn't play so well when using the `:lock-aspect-ratio` prop. You can notice that you have different results by dragging, for example, the right handle or the bottom handle. 56 | 57 | -------------------------------------------------------------------------------- /stories/basic/Active.story.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 31 | 32 | ## Basic component with active control 33 | 34 | A basic component, with `active` prop to control the active state from outside the component. 35 | 36 | -------------------------------------------------------------------------------- /stories/basic/Axis.story.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 43 | 44 | 45 | ## Basic component with axis prop 46 | 47 | A basic component, with `:axis` prop to control on which axis the component is draggable. Suitable values are `x`, `y` or `both`. 48 | 49 | -------------------------------------------------------------------------------- /stories/basic/Basic.story.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 15 | 16 | 17 | ## Basic component 18 | 19 | The most basic component, without any props, free to move even outside the parent element. 20 | 21 | -------------------------------------------------------------------------------- /stories/basic/CancelHandle.story.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 16 | ## Basic component with Drag Cancel 17 | 18 | A basic component, that cannot be dragged through a handle, specified by the prop `:drag-cancel` and a valid CSS selector. 19 | 20 | -------------------------------------------------------------------------------- /stories/basic/ControlledComponent.story.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 36 | 37 | 38 | ## Basic controlled component 39 | 40 | A basic controlled component, with `x`, `y`, `w` and `h` props to control the position and the size of the component. You should also provide callbacks to sync the state with the parent. 41 | 42 | -------------------------------------------------------------------------------- /stories/basic/DragHandle.story.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 16 | ## Basic component with Drag Handle 17 | 18 | A basic component, that cannot be dragged through a handle, specified by the prop `:drag-handle` and a valid CSS selector. 19 | 20 | -------------------------------------------------------------------------------- /stories/basic/Form.story.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 38 | 39 | 40 | ## Basic component with form 41 | 42 | A basic component, with a form inside. The input should be focusable and the button should be clickable. 43 | 44 | -------------------------------------------------------------------------------- /stories/basic/Handles.story.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 36 | 37 | 38 | ## Basic component with handles prop 39 | 40 | You can choose what handles to provide to the component using the `handles` prop, which accepts an array of handles. For example, if you want to costrain resizing only on horizontal axis you can provide only left and right handles `:handles="['ml','mr']"`. 41 | 42 | -------------------------------------------------------------------------------- /stories/basic/MaxWidthMaxHeight.story.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 32 | 33 | 34 | ## Basic component with Max Height and Max Width 35 | 36 | A basic component, with Max Height and Max Width provided respectively by `:max-height` and `:max-width` props 37 | 38 | -------------------------------------------------------------------------------- /stories/basic/MinWidthMinHeight.story.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | 32 | 33 | ## Basic component with Min Height and Min Width 34 | 35 | A basic component, with minimum height and minimum width provided respectively by `:min-width` and `:min-height` props. 36 | 37 | -------------------------------------------------------------------------------- /stories/basic/NativeDragEnabled.story.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 18 | 19 | 20 | ## Component with native drag enabled 21 | 22 | A basic component, with `:enable-native-drag` prop set to `true`, in order to allow native browser's drag behavior. You can see the difference by dragging each component using the ball. Default value is `false`. 23 | 24 | -------------------------------------------------------------------------------- /stories/basic/NotDraggable.story.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 31 | 32 | ## Component not draggable 33 | 34 | A basic component, with `:draggable` prop set to `false`, so it's not draggable. 35 | 36 | -------------------------------------------------------------------------------- /stories/basic/NotResizable.story.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 31 | 32 | ## Component not resizable 33 | 34 | A basic component, with `:resizable` prop set to `false`, so it's not resizable. 35 | 36 | -------------------------------------------------------------------------------- /stories/basic/PreventDeactivation.story.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 31 | 32 | ## Prevent deactivation 33 | 34 | A basic component, with `:prevent-deactivation` prop to avoid untoggling active state when clicking outside. 35 | 36 | -------------------------------------------------------------------------------- /stories/basic/Scale.story.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 33 | 34 | 35 | ## Basic component with scale prop 36 | 37 | A basic component, with `:scale` prop to control the scale property when the CSS 3 scale transformation is applied to one of the parent elements. If not provided the default value is `1`. 38 | 39 | -------------------------------------------------------------------------------- /stories/basic/SizeAuto.story.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 36 | 37 | 38 | ## Basic component with "auto" width and height 39 | 40 | A basic component with initial values for `w` and `h` props set to `auto`. As soon as the component is resized their values fall back to numbers. 41 | 42 | -------------------------------------------------------------------------------- /stories/basic/ZIndex.story.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 36 | 37 | 38 | ## Basic component with "auto" width and height 39 | 40 | A basic component with initial values for `w` and `h` props set to `auto`. As soon as the component is resized their values fall back to numbers. 41 | 42 | -------------------------------------------------------------------------------- /stories/callback/OnDrag.story.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 29 | 30 | 31 | ## onDrag callback 32 | 33 | A component with `:onDrag` prop that accepts a function that gets called on dragging. If the function returns `false`, the drag action is cancelled. 34 | 35 | -------------------------------------------------------------------------------- /stories/callback/OnDragStart.story.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 45 | 46 | 47 | ## onDragStart callback 48 | 49 | A component, with `:onDragStart` prop that accepts a function that gets called when dragging starts (element is clicked or touched). If the function returns `false`, the action is cancelled. You can use this function to prevent bubbling of events. 50 | 51 | ```js 52 | function onDragStartCallback(ev){ 53 | ... 54 | // return false; — for cancel 55 | } 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /stories/callback/OnResize.story.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 34 | 35 | 36 | ## onResize callback 37 | 38 | A component with `:onResize` prop that accepts a function that gets called on resizing. If the function returns `false`, the action is cancelled. 39 | 40 | -------------------------------------------------------------------------------- /stories/callback/OnResizeStart.story.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 45 | 46 | 47 | ## onResizeStart callback 48 | 49 | A component, with `:onResizeStart` prop that accepts a function that gets called when dragging starts (element is clicked or touched). If the function returns `false`, the resizing action is cancelled. You can use this function to prevent bubbling of events. 50 | 51 | ```js 52 | function onResizeStartCallback(handle, ev){ 53 | ... 54 | // return false; — for cancel 55 | } 56 | ``` 57 | 58 | -------------------------------------------------------------------------------- /stories/events/Activated.story.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 39 | 40 | 41 | ## Activated and deactivated events 42 | 43 | The `activated` event is emitted when the component gets activated. The `deactivated` event is emitted when the component gets deactivated. 44 | 45 | -------------------------------------------------------------------------------- /stories/events/Dragging.story.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 43 | 44 | 45 | ## Dragging and dragstop events 46 | 47 | The `dragging(x, y)` event is emitted when the component is dragged. The `dragstop(x, y)` event is emmitted when the dragging stops. 48 | 49 | -------------------------------------------------------------------------------- /stories/events/Resizing.story.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 48 | 49 | 50 | ## Resizing and resizestop events 51 | 52 | The `resizing(x, y, width, height)` event is emitted when the component is resized. The `resizestop(x, y, width, height)` event is emmitted when the resizing stops. 53 | 54 | -------------------------------------------------------------------------------- /stories/grid/Grid.story.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 49 | 50 | 51 | ## Basic component with Grid 52 | 53 | A basic component, with `:grid=[20,20]` prop to set the dimensions of grid cells (height and width in pixels). 54 | 55 | -------------------------------------------------------------------------------- /stories/grid/GridWithMaxWidthMaxHeight.story.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 49 | 50 | 51 | ## Grid 20x40 pixels with 10x10 offset and maxHeight, maxWidth values 52 | 53 | If you provide `:max-height` and `:max-width` values that are higer than the respective grid values, you can notice that resizing stops to the lower suitable value. For example on the x axis the lowest valid value that respects max width and grid[x] constraint is `80`. The same applies for the y axis. 54 | 55 | -------------------------------------------------------------------------------- /stories/grid/GridWithMinWidthMinHeight.story.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 49 | 50 | 51 | ## Grid 20x40 pixels with 10x10 offset and minHeight, minWidth values 52 | 53 | If you provide `:min-height` and `:min-width` values that are lower than the respective grid values, you can notice that resizing stops to the lowest suitable value. For example on the x axis the lowest valid value that respects the min width and grid[x] constraint is `40`. The same applies for the y axis. 54 | 55 | -------------------------------------------------------------------------------- /stories/histoire.setup.js: -------------------------------------------------------------------------------- 1 | import './styles.css' 2 | -------------------------------------------------------------------------------- /stories/how-to/DragMultiple.story.vue: -------------------------------------------------------------------------------- 1 | 94 | 95 | 117 | 118 | 119 | ## Drag Multiple Elements 120 | 121 | This is not a feature provided by the component by default, but it's an example of how you can build complex scenarios using existing features. 122 | 123 | Make sure to click anywhere inside the container before starting to drag elements, due to how the ctrl event handler is registered. 124 | 125 | -------------------------------------------------------------------------------- /stories/how-to/FrameSelection.story.vue: -------------------------------------------------------------------------------- 1 | 159 | 160 | 193 | 194 | 213 | 214 | 215 | ## Select and drag Multiple Elements 216 | 217 | This is not a feature provided by the component by default, but it's an example of how you can build complex scenarios using existing features. 218 | 219 | Make a selection with the mouse to include one or more elements so you can drag them together. 220 | 221 | -------------------------------------------------------------------------------- /stories/how-to/HowTo.stories.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '../../src/components/vue-draggable-resizable.vue'; 2 | 3 | export default { 4 | title: 'How To', 5 | component: VueDraggableResizable 6 | }; 7 | 8 | export const FrameSelection = () => ({ 9 | components: { VueDraggableResizable }, 10 | template: ` 11 |
16 | 29 |

{{ element.text }}

30 |
31 | 32 |
36 |
37 |
38 | Number of selected elements: {{selectedItemNum}} 39 |
40 |
41 | `, 42 | data() { 43 | return { 44 | draggingId: null, 45 | prevOffsetX: 0, 46 | prevOffsetY: 0, 47 | elements: [ 48 | {id: 1, x: 10, y: 10, w: 200, h: 200, isSelect: false, text: 'Element 1'}, 49 | {id: 2, x: 250, y: 200, w: 200, h: 200, isSelect: false, text: 'Element 2'}, 50 | {id: 3, x: 500, y: 300, w: 200, h: 200, isSelect: false, text: 'Element 3'} 51 | ], 52 | //frame selection 53 | selectedItemNum:0, 54 | startX: 0, //X of the mouse (begin to move) 55 | startY: 0, 56 | initX: 0, //X of the mouse (moving) 57 | initY: 0, 58 | scrollX: 0, 59 | scrollY: 0, 60 | rectX: 0, //X of the rectangle selection 61 | rectY: 0, 62 | rectWidth: 0, //width of the rectangle selection 63 | rectHeight: 0, 64 | rectShow: false, //weather to show the rectangle selection 65 | } 66 | }, 67 | methods: { 68 | dragging(id, left, top) { 69 | this.draggingId = id; 70 | 71 | const offsetX = left - this.draggingElement.x; 72 | const offsetY = top - this.draggingElement.y; 73 | 74 | const deltaX = this.deltaX(offsetX); 75 | const deltaY = this.deltaY(offsetY); 76 | 77 | // if the dragging element is not been selected,clear all selected elements 78 | if(this.draggingElement.isSelected===false){ 79 | this.elements.map((el) => { el.isSelected = false; }); 80 | this.selectedItemNum=0; 81 | return; 82 | }; 83 | this.elements.map(el => { 84 | if (el.id !== id && el.isSelected===true) { 85 | el.x += deltaX; 86 | el.y += deltaY; 87 | } 88 | return el; 89 | }); 90 | }, 91 | dragstop(id, left, top) { 92 | this.elements.map(el => { 93 | if (el.id === id) { 94 | el.x = left; 95 | el.y = top; 96 | } 97 | return el; 98 | }); 99 | 100 | this.draggingId = null; 101 | this.prevOffsetX = 0; 102 | this.prevOffsetY = 0; 103 | }, 104 | deltaX(offsetX) { 105 | const ret = offsetX - this.prevOffsetX; 106 | 107 | this.prevOffsetX = offsetX; 108 | 109 | return ret; 110 | }, 111 | deltaY(offsetY) { 112 | const ret = offsetY - this.prevOffsetY; 113 | this.prevOffsetY = offsetY; 114 | return ret; 115 | }, 116 | mouseDown(e) { 117 | if (e.target.id !== 'components-container') { 118 | return; 119 | } 120 | //clear all selected elements 121 | if (this.selectedItemNum > 0) { 122 | this.elements.map((el) => { el.isSelected = false; }); 123 | this.selectedItemNum=0; 124 | } 125 | this.rectSelect = true;//begin to draw the rectangle 126 | this.rectWidth =0; 127 | this.rectHeight=0; 128 | this.startX = e.offsetX; 129 | this.startY = e.offsetY; 130 | this.scrollX = e.clientX - e.offsetX; 131 | this.scrollY = e.clientY - e.offsetY; 132 | }, 133 | 134 | mouseMove(e) { 135 | if (!this.rectSelect) { 136 | return; 137 | } 138 | this.rectShow = true; 139 | this.initX = e.clientX - this.scrollX; 140 | this.initY = e.clientY - this.scrollY; 141 | this.rectX = Math.min(this.initX, this.startX); 142 | this.rectY = Math.min(this.initY, this.startY); 143 | this.rectWidth = Math.abs(this.initX - this.startX); 144 | this.rectHeight = Math.abs(this.initY - this.startY); 145 | this.clearBubble(e); 146 | }, 147 | mouseUpfn(e) { 148 | if (this.rectSelect) { 149 | if (this.rectWidth > 10 && this.rectHeight > 10) { 150 | this.elements.map((el)=>{ 151 | const elLeft=el.x; 152 | const elRight=el.x+el.w; 153 | const elTop=el.y; 154 | const elBottom=el.y+el.h; 155 | if (elLeft > this.rectX && elTop > this.rectY && (elRight < (this.rectX + this.rectWidth)) && (elBottom < (this.rectY + this.rectHeight))) { 156 | el.isSelected = true; 157 | this.selectedItemNum+=1; 158 | } 159 | }); 160 | } 161 | this.rectShow = false; 162 | this.rectSelect = false; 163 | this.clearBubble(e); 164 | } 165 | }, 166 | clearBubble(e) { 167 | if (e.stopPropagation) { 168 | e.stopPropagation(); 169 | } else { 170 | e.cancelBubble = true; 171 | } 172 | if (e.preventDefault) { 173 | e.preventDefault(); 174 | } else { 175 | e.returnValue = false; 176 | } 177 | }, 178 | selectStyle(isSelected) { 179 | return isSelected? "border:1px solid blue !important" :null; 180 | } 181 | }, 182 | computed: { 183 | draggingElement: function () { 184 | if (! this.draggingId) return; 185 | return this.elements.find(el => el.id === this.draggingId); 186 | }, 187 | } 188 | }) 189 | 190 | 191 | export const DragMultiple = () => ({ 192 | components: { VueDraggableResizable }, 193 | template: ` 194 |
195 | 207 |

{{ element.text }} {{ sync ? '(synchronized)' : null }}

208 |
209 |
210 | `, 211 | data() { 212 | return { 213 | sync: false, 214 | draggingId: null, 215 | prevOffsetX: 0, 216 | prevOffsetY: 0, 217 | elements: [ 218 | {id: 1, x: 0, y: 0, text: 'Element 1'}, 219 | {id: 2, x: 200, y: 200, text: 'Element 2'} 220 | ] 221 | } 222 | }, 223 | mounted() { 224 | window.addEventListener('keydown', ev => { 225 | if (ev.keyCode === 17) { 226 | this.sync = true; 227 | } 228 | }); 229 | window.addEventListener('keyup', ev => { 230 | if (ev.keyCode === 17) { 231 | this.sync = false; 232 | } 233 | }); 234 | }, 235 | methods: { 236 | dragging(id, left, top) { 237 | this.draggingId = id; 238 | 239 | if (! this.sync) return; 240 | 241 | const offsetX = left - this.draggingElement.x; 242 | const offsetY = top - this.draggingElement.y; 243 | 244 | const deltaX = this.deltaX(offsetX); 245 | const deltaY = this.deltaY(offsetY); 246 | 247 | this.elements.map(el => { 248 | if (el.id !== id) { 249 | el.x += deltaX; 250 | el.y += deltaY; 251 | } 252 | 253 | return el; 254 | }); 255 | }, 256 | dragstop(id, left, top) { 257 | this.elements.map(el => { 258 | if (el.id === id) { 259 | el.x = left; 260 | el.y = top; 261 | } 262 | 263 | return el; 264 | }); 265 | 266 | this.draggingId = null; 267 | this.prevOffsetX = 0; 268 | this.prevOffsetY = 0; 269 | }, 270 | deltaX(offsetX) { 271 | const ret = offsetX - this.prevOffsetX; 272 | 273 | this.prevOffsetX = offsetX; 274 | 275 | return ret; 276 | }, 277 | deltaY(offsetY) { 278 | const ret = offsetY - this.prevOffsetY; 279 | 280 | this.prevOffsetY = offsetY; 281 | 282 | return ret; 283 | } 284 | }, 285 | computed: { 286 | draggingElement: function () { 287 | if (! this.draggingId) return; 288 | 289 | return this.elements.find(el => el.id === this.draggingId); 290 | } 291 | } 292 | }) 293 | -------------------------------------------------------------------------------- /stories/parent/Basic.story.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 32 | 33 | 34 | ## Component costraind in parent 35 | 36 | Component that cannot be dragged or resized outside its parent element, defined with the prop `:parent="true"`. 37 | 38 | -------------------------------------------------------------------------------- /stories/parent/ControlledComponent.story.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 57 | 58 | 59 | ## Parent controlled Component with Grid 60 | 61 | A basic parent controlled component, with `:x`, `:y`, `:w` and `:h` props to control the position and the size of the component. Notice that using also the `:grid` prop, the component will react only with a valid multiple of grid values. 62 | 63 | -------------------------------------------------------------------------------- /stories/parent/DisableUserSelect.story.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 32 | 33 | 34 | ## Component with text selection disabled 35 | 36 | Component that disables text selection of an element using the prop `:disable-user-select="true"` prop. 37 | 38 | -------------------------------------------------------------------------------- /stories/parent/Grid.story.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 41 | 42 | 43 | ## Component with grid costraind in parent 44 | 45 | Component attached to a grid, that cannot be dragged or resized outside its parent element, defined with the prop `:parent="true"`. In this case the size of the parent matches perfectly the grid. 46 | 47 | -------------------------------------------------------------------------------- /stories/parent/ParentGridEvenOffset.story.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 43 | 44 | 45 | ## Component costraind in parent and on a grid with an even 17x17 offset 46 | 47 | Component attached to a grid with an even offset. 48 | 49 | -------------------------------------------------------------------------------- /stories/parent/ParentGridMaxWidthMaxHeight.story.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 52 | 53 | 54 | ## Component with grid costraind in parent with maxWidth and maxHeight 55 | 56 | Component attached to a grid, that cannot be dragged or resized outside its parent element, with `:max-width` and `:max-height` props to limit its size. Notice that using `20` as grid prop for the y axis, the maximum height of the element is `280` instead of `290`. 57 | 58 | -------------------------------------------------------------------------------- /stories/parent/ParentGridOffset.story.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 43 | 44 | 45 | ## Component costraind in parent and on a grid with 10x10 offset 46 | 47 | Component attached to a grid with a small offset. Its starting position is not perfectly aligned with the top-left corner of the parent. 48 | 49 | -------------------------------------------------------------------------------- /stories/parent/ParentMaxWidthMaxHeight.story.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 34 | 35 | 36 | ## Component costrained in parent with maxWidth and maxHeight 37 | 38 | Component that cannot be dragged or resized outside its parent element, with `:max-width` and `:max-height` props to limit its size. 39 | 40 | -------------------------------------------------------------------------------- /stories/styles.css: -------------------------------------------------------------------------------- 1 | .__histoire-render-story:not(.__histoire-render-custom-controls) { 2 | overflow: visible; 3 | } 4 | 5 | .drag-handle, 6 | .drag-cancel { 7 | padding: 6px; 8 | margin: 6px; 9 | background-color: #CCC; 10 | border: 2px solid; 11 | } 12 | 13 | .drag-handle:hover { 14 | cursor: move; 15 | } 16 | 17 | .drag-cancel:hover { 18 | cursor: not-allowed; 19 | } 20 | 21 | .my-class { 22 | background-color: green; 23 | border: 1px solid red; 24 | -webkit-transition: background-color 200ms linear; 25 | -ms-transition: background-color 200ms linear; 26 | transition: background-color 200ms linear; 27 | } 28 | 29 | .my-dragging-class { 30 | background-color: red; 31 | border: 1px solid black; 32 | } 33 | 34 | .my-resizing-class { 35 | background-color: blue; 36 | border: 1px solid black; 37 | color: white; 38 | } 39 | 40 | .my-active-class { 41 | border: 1px solid black; 42 | -webkit-box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.75); 43 | -moz-box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.75); 44 | box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.75); 45 | } 46 | 47 | .my-handle-class { 48 | position: absolute; 49 | border: 1px solid black; 50 | border-radius: 50%; 51 | height: 14px; 52 | width: 14px; 53 | font-size: 1em; 54 | line-height: 1em; 55 | box-sizing: border-box; 56 | -webkit-transition: all 300ms linear; 57 | -ms-transition: all 300ms linear; 58 | transition: all 300ms linear; 59 | } 60 | 61 | .my-handle-class-tl { 62 | top: -14px; 63 | left: -14px; 64 | cursor: nw-resize; 65 | } 66 | .my-handle-class-tm { 67 | top: -14px; 68 | left: 50%; 69 | margin-left: -7px; 70 | cursor: n-resize; 71 | } 72 | .my-handle-class-tr { 73 | top: -14px; 74 | right: -14px; 75 | cursor: ne-resize; 76 | } 77 | .my-handle-class-ml { 78 | top: 50%; 79 | margin-top: -7px; 80 | left: -14px; 81 | cursor: w-resize; 82 | } 83 | .my-handle-class-mr { 84 | top: 50%; 85 | margin-top: -7px; 86 | right: -14px; 87 | cursor: e-resize; 88 | } 89 | .my-handle-class-bl { 90 | bottom: -14px; 91 | left: -14px; 92 | cursor: sw-resize; 93 | } 94 | .my-handle-class-bm { 95 | bottom: -14px; 96 | left: 50%; 97 | margin-left: -7px; 98 | cursor: s-resize; 99 | } 100 | .my-handle-class-br { 101 | bottom: -14px; 102 | right: -14px; 103 | cursor: se-resize; 104 | } 105 | .my-handle-class-tl:hover, 106 | .my-handle-class-tm:hover, 107 | .my-handle-class-tr:hover, 108 | .my-handle-class-ml:hover, 109 | .my-handle-class-mr:hover, 110 | .my-handle-class-bl:hover, 111 | .my-handle-class-bm:hover, 112 | .my-handle-class-br:hover { 113 | transform: scale(1.4); 114 | } 115 | 116 | 117 | -------------------------------------------------------------------------------- /stories/styling/Active.story.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 31 | 32 | ## Component with custom class name on active state 33 | 34 | Component with a custom class name on active state, provided with the prop `:class-name-active`. 35 | 36 | ```css 37 | .my-class { 38 | background-color: green; 39 | border: 1px solid red; 40 | -webkit-transition: background-color 200ms linear; 41 | -ms-transition: background-color 200ms linear; 42 | transition: background-color 200ms linear; 43 | } 44 | 45 | .my-active-class { 46 | border: 1px solid black; 47 | -webkit-box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.75); 48 | -moz-box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.75); 49 | box-shadow: 10px 10px 5px 0px rgba(0,0,0,0.75); 50 | } 51 | ``` 52 | 53 | -------------------------------------------------------------------------------- /stories/styling/Component.story.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 31 | 32 | ## Component with custom class name 33 | 34 | Component with a custom class name, provided with the prop `:class-name`. 35 | 36 | ```css 37 | .my-class { 38 | background-color: green; 39 | border: 1px solid red; 40 | } 41 | ``` 42 | 43 | -------------------------------------------------------------------------------- /stories/styling/Dragging.story.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 31 | 32 | ## Component with custom class name on dragging 33 | 34 | Component with a custom class name on dragging, provided with the prop `:class-name-dragging`. 35 | 36 | ```css 37 | .my-class { 38 | background-color: green; 39 | border: 1px solid red; 40 | -webkit-transition: background-color 200ms linear; 41 | -ms-transition: background-color 200ms linear; 42 | transition: background-color 200ms linear; 43 | } 44 | 45 | .my-dragging-class { 46 | background-color: red; 47 | border: 1px solid black; 48 | } 49 | ``` 50 | 51 | -------------------------------------------------------------------------------- /stories/styling/HandleSlots.story.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 22 | 23 | 24 | ## Component with custom markup for handles 25 | 26 | Component with custom markup for handles, provided by VueJS named slots (e.g. slot="tl"). 27 | 28 | -------------------------------------------------------------------------------- /stories/styling/Handles.story.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 31 | 32 | ## Component with custom class name handles 33 | 34 | Component with a custom class for handle, provided with the prop `:class-name-handle`. In this way you can style each handle separately. 35 | 36 | **Remember to set `position: absolute` for the handle class** 37 | ```css 38 | .my-handle-class { 39 | position: absolute. 40 | border: 1px solid black; 41 | border-radius: 50%; 42 | height: 14px; 43 | width: 14px; 44 | box-model: border-box; 45 | -webkit-transition: all 300ms linear; 46 | -ms-transition: all 300ms linear; 47 | transition: all 300ms linear; 48 | } 49 | 50 | .my-handle-class-tl { 51 | top: -14px; 52 | left: -14px; 53 | cursor: nw-resize; 54 | } 55 | 56 | .my-handle-class-tm { 57 | top: -14px; 58 | left: 50%; 59 | margin-left: -7px; 60 | cursor: n-resize; 61 | } 62 | 63 | .my-handle-class-tr { 64 | top: -14px; 65 | right: -14px; 66 | cursor: ne-resize; 67 | } 68 | 69 | .my-handle-class-ml { 70 | top: 50%; 71 | margin-top: -7px; 72 | left: -14px; 73 | cursor: w-resize; 74 | } 75 | 76 | .my-handle-class-mr { 77 | top: 50%; 78 | margin-top: -7px; 79 | right: -14px; 80 | cursor: e-resize; 81 | } 82 | 83 | .my-handle-class-bl { 84 | bottom: -14px; 85 | left: -14px; 86 | cursor: sw-resize; 87 | } 88 | 89 | .my-handle-class-bm { 90 | bottom: -14px; 91 | left: 50%; 92 | margin-left: -7px; 93 | cursor: s-resize; 94 | } 95 | 96 | .my-handle-class-br { 97 | bottom: -14px; 98 | right: -14px; 99 | cursor: se-resize; 100 | } 101 | 102 | .my-handle-class-tl:hover, 103 | .my-handle-class-tr:hover, 104 | .my-handle-class-bl:hover, 105 | .my-handle-class-br:hover { 106 | background-color: red; 107 | transform: scale(1.4); 108 | } 109 | ``` 110 | 111 | -------------------------------------------------------------------------------- /stories/styling/Resizing.story.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 30 | 31 | 32 | ## Component with custom class name on resizing 33 | 34 | Component with a custom class name on resizing, provided with the prop `:class-name-resizing`. 35 | 36 | ```css 37 | .my-class { 38 | background-color: green; 39 | border: 1px solid red; 40 | -webkit-transition: background-color 200ms linear; 41 | -ms-transition: background-color 200ms linear; 42 | transition: background-color 200ms linear; 43 | } 44 | 45 | .my-resizing-class { 46 | background-color: blue; 47 | border: 1px solid black; 48 | color: white; 49 | } 50 | ``` 51 | 52 | -------------------------------------------------------------------------------- /tests/cypress/base.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | 3 | describe('VueDraggableResizable base tests', () => { 4 | 5 | it('should render correctly', () => { 6 | cy.mount(VueDraggableResizable) 7 | 8 | cy.get('div') 9 | .should('be.visible') 10 | .and('have.class', 'vdr') 11 | }) 12 | 13 | it('should render the elements in default slot', () => { 14 | cy.mount(VueDraggableResizable, { 15 | slots: { 16 | default: '

Resize Me

' 17 | } 18 | }) 19 | 20 | cy.get('div.vdr') 21 | .find('p') 22 | .should('be.visible') 23 | .and('have.text', 'Resize Me') 24 | }) 25 | 26 | it('should render the component with 8 handles by default', () => { 27 | cy.mount(VueDraggableResizable) 28 | 29 | cy.get('div.handle') 30 | .should('not.be.empty') 31 | .and('have.length', 8) 32 | .and($handles => { 33 | const classes = $handles.map((i, el) => { 34 | return Cypress.$(el).attr('class') 35 | }) 36 | 37 | expect(classes.get()).to.deep.eq([ 38 | 'handle handle-tl', 39 | 'handle handle-tm', 40 | 'handle handle-tr', 41 | 'handle handle-mr', 42 | 'handle handle-br', 43 | 'handle handle-bm', 44 | 'handle handle-bl', 45 | 'handle handle-ml', 46 | ]) 47 | }) 48 | }) 49 | 50 | it('should provide named slots for each one of the handles', () => { 51 | cy.mount(VueDraggableResizable, { 52 | slots: { 53 | tl: 'TL', 54 | tm: 'TM', 55 | tr: 'TR', 56 | mr: 'MR', 57 | br: 'BR', 58 | bm: 'BM', 59 | bl: 'BL', 60 | ml: 'ML' 61 | } 62 | }) 63 | 64 | cy.get('div.handle-tl').should('have.html', 'TL') 65 | cy.get('div.handle-tm').should('have.html', 'TM') 66 | cy.get('div.handle-tr').should('have.html', 'TR') 67 | cy.get('div.handle-mr').should('have.html', 'MR') 68 | cy.get('div.handle-br').should('have.html', 'BR') 69 | cy.get('div.handle-bm').should('have.html', 'BM') 70 | cy.get('div.handle-bl').should('have.html', 'BL') 71 | cy.get('div.handle-ml').should('have.html', 'ML') 72 | }) 73 | 74 | it('should not block event bubbling', () => { 75 | const onActivated = cy.spy().as('onActivatedSpy') 76 | 77 | cy.mount(VueDraggableResizable, { 78 | propsData: { 79 | onActivated 80 | }, 81 | slots: { 82 | default: '' 83 | } 84 | }) 85 | 86 | cy.get('input').click() 87 | 88 | cy.get('@onActivatedSpy') 89 | .should('have.been.called') 90 | 91 | cy.get('div.vdr').should('have.class', 'active') 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /tests/cypress/events.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | 3 | describe('VueDraggableResizable events', () => { 4 | 5 | it('should emit "activated" event when the component is clicked', () => { 6 | const onActivated = cy.spy().as('onActivatedSpy') 7 | 8 | cy.mount(VueDraggableResizable, { 9 | propsData: { 10 | active: false, 11 | onActivated 12 | } 13 | }) 14 | 15 | cy.get('div.vdr').click() 16 | 17 | cy.get('@onActivatedSpy') 18 | .should('have.been.called') 19 | }) 20 | 21 | it('should emit "dragging" event while dragging the component', () => { 22 | const onDragging = cy.spy().as('onDraggingSpy') 23 | 24 | cy.mount(VueDraggableResizable, { 25 | propsData: { 26 | w: 100, 27 | h: 100, 28 | active: true, 29 | onDragging 30 | } 31 | }) 32 | 33 | cy.get('div.vdr') 34 | .move({ deltaX: 50, deltaY: 50 }) 35 | 36 | cy.get('@onDraggingSpy') 37 | .should('have.been.calledWith', 50, 50) 38 | }) 39 | 40 | it('should emit "dragStop" event while stopping dragging the component', () => { 41 | const onDragStop = cy.spy().as('onDragStopSpy') 42 | 43 | cy.mount(VueDraggableResizable, { 44 | propsData: { 45 | w: 100, 46 | h: 100, 47 | active: true, 48 | onDragStop 49 | } 50 | }) 51 | 52 | cy.get('div.vdr') 53 | .move({ deltaX: 50, deltaY: 50 }) 54 | 55 | cy.get('@onDragStopSpy') 56 | .should('have.been.calledWith', 50, 50) 57 | }) 58 | 59 | it('should not emit "dragStop" when the component is activated', () => { 60 | const onDragStop = cy.spy().as('onDragStopSpy') 61 | 62 | cy.mount(VueDraggableResizable, { 63 | propsData: { 64 | w: 100, 65 | h: 100, 66 | onDragStop 67 | } 68 | }) 69 | 70 | cy.get('div.vdr').click() 71 | 72 | cy.get('@onDragStopSpy') 73 | .should('not.have.been.called') 74 | }) 75 | 76 | it('should emit "resizing" event while resizing the component', () => { 77 | const onResizing = cy.spy().as('onResizingSpy') 78 | 79 | cy.mount(VueDraggableResizable, { 80 | propsData: { 81 | w: 100, 82 | h: 100, 83 | active: true, 84 | onResizing 85 | } 86 | }) 87 | 88 | cy.get('div.handle-br') 89 | .move({ deltaX: 50, deltaY: 50 }) 90 | 91 | cy.get('@onResizingSpy') 92 | .should('have.been.calledWith', 0, 0, 150, 150) 93 | }) 94 | 95 | it('should emit "resizeStop" event while stopping resizing the component', () => { 96 | const onResizeStop = cy.spy().as('onResizeStopSpy') 97 | 98 | cy.mount(VueDraggableResizable, { 99 | propsData: { 100 | w: 100, 101 | h: 100, 102 | active: true, 103 | onResizeStop 104 | } 105 | }) 106 | 107 | cy.get('div.handle-br') 108 | .move({ deltaX: 50, deltaY: 50 }) 109 | 110 | cy.get('@onResizeStopSpy') 111 | .should('have.been.calledWith', 0, 0, 150, 150) 112 | }) 113 | 114 | it('should not emit "resizeStop" when a handle is clicked', () => { 115 | const onResizeStop = cy.spy().as('onResizeStopSpy') 116 | 117 | cy.mount(VueDraggableResizable, { 118 | propsData: { 119 | w: 100, 120 | h: 100, 121 | active: true, 122 | onResizeStop 123 | } 124 | }) 125 | 126 | cy.get('div.handle-br').click() 127 | 128 | cy.get('@onResizeStopSpy') 129 | .should('not.have.been.called') 130 | }) 131 | }) 132 | -------------------------------------------------------------------------------- /tests/cypress/props/active.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | 3 | describe('"active" prop', () => { 4 | 5 | it('should not have the "active" class if the component is not active', () => { 6 | cy.mount(VueDraggableResizable, { 7 | propsData: { 8 | active: false 9 | } 10 | }) 11 | 12 | cy.get('div.vdr') 13 | .should('not.have.class', 'active') 14 | }) 15 | 16 | it('should enable the component through "active" prop', () => { 17 | cy.mount(VueDraggableResizable, { 18 | propsData: { 19 | active: true 20 | } 21 | }) 22 | 23 | cy.get('div.vdr') 24 | .should('have.class', 'active') 25 | 26 | cy.get('@vue').should(wrapper => { 27 | expect(wrapper.props().active).to.be.true 28 | }) 29 | }) 30 | 31 | it('should not show handles if the component is not active', () => { 32 | cy.mount(VueDraggableResizable, { 33 | propsData: { 34 | active: false 35 | } 36 | }) 37 | 38 | cy.get('div.handle') 39 | .should('be.not.visible') 40 | }) 41 | 42 | it('should show handles if the component is active', () => { 43 | cy.mount(VueDraggableResizable, { 44 | propsData: { 45 | active: true 46 | } 47 | }) 48 | 49 | cy.get('div.handle') 50 | .should('be.visible') 51 | }) 52 | 53 | it('should watch to the "active" prop', () => { 54 | cy.mount(VueDraggableResizable, { 55 | propsData: { 56 | active: false 57 | } 58 | }) 59 | 60 | cy.get('@vue').then(wrapper => { 61 | wrapper.setProps({ active: true }) 62 | 63 | cy.get('div.vdr') 64 | .should('have.class', 'active') 65 | }) 66 | }) 67 | 68 | it('should activate the component when clicking on it', () => { 69 | const onActivated = cy.spy().as('onActivatedSpy') 70 | 71 | cy.mount(VueDraggableResizable, { 72 | propsData: { 73 | onActivated 74 | } 75 | }) 76 | 77 | cy.get('div.vdr').click() 78 | 79 | cy.get('@onActivatedSpy') 80 | .should('have.been.calledOnce') 81 | 82 | cy.get('@vue').should(wrapper => { 83 | expect(wrapper.emitted()).to.have.property('update:active') 84 | }) 85 | }) 86 | 87 | it('should emit "activated" event if the component is mounted with the "active" prop set true', () => { 88 | const onActivated = cy.spy().as('onActivatedSpy') 89 | 90 | cy.mount(VueDraggableResizable, { 91 | propsData: { 92 | active: true, 93 | onActivated 94 | } 95 | }) 96 | 97 | cy.get('@onActivatedSpy') 98 | .should('have.been.called') 99 | }) 100 | 101 | it('should deactivate the component when clicking outside it', () => { 102 | const onDeactivated = cy.spy().as('onDeactivatedSpy') 103 | 104 | cy.mount(VueDraggableResizable, { 105 | propsData: { 106 | active: true, 107 | onDeactivated 108 | } 109 | }) 110 | 111 | cy.get(document.documentElement).click() 112 | 113 | cy.get('@onDeactivatedSpy') 114 | .should('have.been.calledOnce') 115 | 116 | cy.get('div.vdr') 117 | .should('not.have.class', 'active') 118 | 119 | cy.get('@vue').should(wrapper => { 120 | expect(wrapper.emitted()).to.have.property('update:active') 121 | }) 122 | }) 123 | 124 | it('should not activate the component when right-clicking on it', () => { 125 | const onActivated = cy.spy().as('onActivatedSpy') 126 | 127 | cy.mount(VueDraggableResizable, { 128 | propsData: { 129 | active: false, 130 | onActivated 131 | } 132 | }) 133 | 134 | cy.get('div.vdr').rightclick() 135 | 136 | cy.get('@onActivatedSpy') 137 | .should('not.have.been.called') 138 | 139 | cy.get('div.vdr') 140 | .should('not.have.class', 'active') 141 | 142 | cy.get('@vue').should(wrapper => { 143 | expect(wrapper.emitted()).to.not.have.property('update:active') 144 | }) 145 | }) 146 | 147 | it('should resize the component also when it is activated using prop', () => { 148 | const onResizing = cy.spy().as('onResizingSpy') 149 | 150 | cy.mount(VueDraggableResizable, { 151 | propsData: { 152 | w: 100, 153 | h: 100, 154 | active: false, 155 | onResizing 156 | } 157 | }) 158 | 159 | cy.get('@vue') 160 | .then(wrapper => { 161 | wrapper.setProps({ active: true }) 162 | }) 163 | .then(() => { 164 | cy.get('div.handle-br').move({ deltaX: 100, deltaY: 100 }) 165 | 166 | cy.get('@onResizingSpy') 167 | .should('have.been.called.with', 0, 0, 200, 200) 168 | }) 169 | }) 170 | 171 | it('should activate the component when touching on it', () => { 172 | const onActivated = cy.spy().as('onActivatedSpy') 173 | 174 | cy.mount(VueDraggableResizable, { 175 | propsData: { 176 | onActivated 177 | } 178 | }) 179 | 180 | cy.get('div.vdr').trigger('touchstart') 181 | 182 | cy.get('@onActivatedSpy') 183 | .should('have.been.calledOnce') 184 | 185 | cy.get('@vue').should(wrapper => { 186 | expect(wrapper.emitted()).to.have.property('update:active') 187 | }) 188 | }) 189 | }) 190 | 191 | -------------------------------------------------------------------------------- /tests/cypress/props/axis.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | 3 | describe('axis prop', () => { 4 | it('should provide the draggable `axis` prop to the component', () => { 5 | cy.mount(VueDraggableResizable, { 6 | propsData: { 7 | axis: 'x' 8 | } 9 | }) 10 | 11 | cy.get('@vue').should(wrapper => { 12 | expect(wrapper.props().axis).to.equal('x') 13 | }) 14 | }) 15 | 16 | it('should drag the component only on the horizontal axis', () => { 17 | cy.mount(VueDraggableResizable, { 18 | propsData: { 19 | axis: 'x', 20 | x: 0, 21 | y: 0, 22 | w: 100, 23 | h: 100 24 | } 25 | }) 26 | 27 | cy.get('div.vdr') 28 | .trigger('mousedown', { button: 0 }) 29 | .trigger('mousemove', { 30 | pageX: 100, 31 | pageY: 100 32 | }) 33 | .trigger('mouseup') 34 | 35 | cy.get('@vue').should(wrapper => { 36 | const $el = wrapper.vm.$el 37 | 38 | expect($el.style.transform).to.be.oneOf([ 39 | 'translate(42px, 0px)', // Chrome 40 | 'translate(42px)', // Firefox 41 | ]) 42 | }) 43 | }) 44 | 45 | it('should effectively drag the component only on the vertical axis', () => { 46 | cy.mount(VueDraggableResizable, { 47 | propsData: { 48 | axis: 'y', 49 | x: 0, 50 | y: 0, 51 | w: 100, 52 | h: 100 53 | } 54 | }) 55 | 56 | cy.get('div.vdr') 57 | .trigger('mousedown', { button: 0 }) 58 | .trigger('mousemove', { 59 | pageX: 100, 60 | pageY: 100 61 | }) 62 | .trigger('mouseup') 63 | 64 | cy.get('@vue').should(wrapper => { 65 | const $el = wrapper.vm.$el 66 | 67 | expect($el.style.transform).to.equal('translate(0px, 42px)') 68 | }) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /tests/cypress/props/callbacks.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | import translateToMatrix from '../utils' 3 | 4 | describe('`onDragStart` and `onResizeStart` props', () => { 5 | 6 | it('should call `onDragStart` callback when the component is clicked', () => { 7 | const onDragStartSpy = cy.spy().as('onDragStartSpy') 8 | 9 | cy.mount(VueDraggableResizable, { 10 | propsData: { 11 | onDragStart: onDragStartSpy 12 | } 13 | }) 14 | 15 | cy.get('div.vdr').click() 16 | 17 | cy.get('@onDragStartSpy').should('have.been.calledOnce') 18 | }) 19 | 20 | it('should prevent activation of the component if the `onDragStart` callback returns false', () => { 21 | const onDragStart = () => false 22 | const onActivatedSpy = cy.spy().as('onActivatedSpy') 23 | 24 | cy.mount(VueDraggableResizable, { 25 | propsData: { 26 | onDragStart, 27 | onActivated: onActivatedSpy 28 | } 29 | }) 30 | 31 | cy.get('div.vdr') 32 | .click() 33 | .should('not.have.class', 'active') 34 | 35 | cy.get('@onActivatedSpy').should('not.have.been.called') 36 | }) 37 | 38 | it('should call `onResizeStart` callback when the component is resized', () => { 39 | const onResizeStartSpy = cy.spy().as('onResizeStartSpy') 40 | 41 | cy.mount(VueDraggableResizable, { 42 | propsData: { 43 | active: true, 44 | onResizeStart: onResizeStartSpy 45 | } 46 | }) 47 | 48 | cy.get('div.handle-br').move({ deltaX: 100, deltaY: 100 }) 49 | 50 | cy.get('@onResizeStartSpy').should('have.been.called') 51 | }) 52 | 53 | it('should prevent resizing the component if the `onResizeStart` callback returns false', () => { 54 | const onResizeStart = () => false 55 | 56 | cy.mount(VueDraggableResizable, { 57 | propsData: { 58 | active: true, 59 | w: 100, 60 | h: 100, 61 | onResizeStart: onResizeStart 62 | } 63 | }) 64 | 65 | cy.get('div.handle-br').move({ deltaX: 100, deltaY: 100 }) 66 | 67 | cy.get('div.vdr') 68 | .should('have.css', 'width', '100px') 69 | .should('have.css', 'height', '100px') 70 | }) 71 | }) 72 | 73 | 74 | describe('`onDrag` and `onResize` props', () => { 75 | 76 | it('should call `onDrag` callback when the component is dragged', () => { 77 | const onDragSpy = cy.spy().as('onDragSpy') 78 | 79 | cy.mount(VueDraggableResizable, { 80 | propsData: { 81 | active: true, 82 | w: 100, 83 | h: 100, 84 | onDrag: onDragSpy 85 | } 86 | }) 87 | 88 | cy.get('div.vdr').move({ deltaX: 100, deltaY: 100 }) 89 | 90 | cy.get('@onDragSpy').should('have.been.calledWith', 100, 100) 91 | }) 92 | 93 | it('should prevent dragging the component if the `onDrag` callback returns false', () => { 94 | const onDragCallback = (x, y) => { 95 | if (x > 10) return false 96 | if (y > 10) return false 97 | } 98 | 99 | cy.mount(VueDraggableResizable, { 100 | propsData: { 101 | active: true, 102 | w: 100, 103 | h: 100, 104 | onDrag: onDragCallback 105 | } 106 | }) 107 | 108 | cy.get('div.vdr') 109 | // intermediate step 110 | .move({ deltaX: 10, deltaY: 10 }) 111 | .should('have.css', 'transform', translateToMatrix('10', '10')) 112 | .should('have.css', 'width', '100px') 113 | .should('have.css', 'height', '100px') 114 | // final step 115 | .move({ deltaX: 100, deltaY: 100 }) 116 | .should('have.css', 'transform', translateToMatrix('10', '10')) 117 | }) 118 | 119 | it('should call `onResize` callback when the component is resized', () => { 120 | const onResizeSpy = cy.spy().as('onResizeSpy') 121 | 122 | cy.mount(VueDraggableResizable, { 123 | propsData: { 124 | active: true, 125 | w: 100, 126 | h: 100, 127 | onResize: onResizeSpy 128 | } 129 | }) 130 | 131 | cy.get('div.handle-br').move({ deltaX: 100, deltaY: 100 }) 132 | 133 | cy.get('@onResizeSpy').should('have.been.calledWith', 'br', 0, 0, 200, 200) 134 | }) 135 | 136 | it('should prevent resizing the component if the `onResize` callback returns false', () => { 137 | const onResizeCallback = (handle, x, y, w, h) => { 138 | if (w > 110) return false 139 | if (h > 110) return false 140 | } 141 | 142 | cy.mount(VueDraggableResizable, { 143 | propsData: { 144 | active: true, 145 | w: 100, 146 | h: 100, 147 | onResize: onResizeCallback 148 | } 149 | }) 150 | 151 | cy.get('div.handle-br').move({ deltaX: 10, deltaY: 10 }) 152 | 153 | cy.get('div.vdr') 154 | .should('have.css', 'width', '110px') 155 | .should('have.css', 'height', '110px') 156 | 157 | cy.get('div.handle-br').move({ deltaX: 100, deltaY: 100 }) 158 | 159 | cy.get('div.vdr') 160 | .should('have.css', 'width', '110px') 161 | .should('have.css', 'height', '110px') 162 | }) 163 | }) 164 | -------------------------------------------------------------------------------- /tests/cypress/props/classes.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | 3 | describe('classes props', () => { 4 | 5 | it('should provide the default class name as `class-name` prop', () => { 6 | cy.mount(VueDraggableResizable, { 7 | propsData: { 8 | className: 'my-class' 9 | } 10 | }) 11 | 12 | cy.get('div.my-class').should('have.class', 'my-class') 13 | 14 | cy.get('@vue').should(wrapper => { 15 | expect(wrapper.props().className).to.equal('my-class') 16 | }) 17 | }) 18 | 19 | it('should provide the active class name as `class-name-active` prop', () => { 20 | cy.mount(VueDraggableResizable, { 21 | propsData: { 22 | classNameActive: 'my-active-class', 23 | active: true 24 | } 25 | }) 26 | 27 | cy.get('div.vdr').should('have.class', 'my-active-class') 28 | 29 | cy.get('@vue').should(wrapper => { 30 | expect(wrapper.classes()).to.contain('my-active-class') 31 | }) 32 | }) 33 | 34 | it('should provide the dragging class name as `class-name-dragging` prop', () => { 35 | cy.mount(VueDraggableResizable, { 36 | propsData: { 37 | classNameDragging: 'my-dragging-class' 38 | } 39 | }) 40 | 41 | cy.get('@vue').should(wrapper => { 42 | expect(wrapper.props().classNameDragging).to.equal('my-dragging-class') 43 | }) 44 | }) 45 | 46 | it('should provide the resizing class name as `class-name-resizing` prop', () => { 47 | cy.mount(VueDraggableResizable, { 48 | propsData: { 49 | classNameResizing: 'my-resizing-class' 50 | } 51 | }) 52 | 53 | cy.get('@vue').should(wrapper => { 54 | expect(wrapper.props().classNameResizing).to.equal('my-resizing-class') 55 | }) 56 | }) 57 | 58 | it('should provide the handle class name as `class-name-handle` prop', () => { 59 | cy.mount(VueDraggableResizable, { 60 | propsData: { 61 | classNameHandle: 'my-handle-class' 62 | } 63 | }) 64 | 65 | cy.get('div.my-handle-class').should('have.length', 8) 66 | 67 | cy.get('@vue').should(wrapper => { 68 | expect(wrapper.props().classNameHandle).to.equal('my-handle-class') 69 | }) 70 | }) 71 | }) 72 | -------------------------------------------------------------------------------- /tests/cypress/props/drag-cancel.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | import translateToMatrix from '../utils' 3 | 4 | describe('`drag-cancel` prop', () => { 5 | 6 | it('should not activate the component from the selector identified by the `drag-cancel` prop', () => { 7 | const onActivated = cy.spy().as('onActivatedSpy') 8 | 9 | cy.mount(VueDraggableResizable, { 10 | propsData: { 11 | dragCancel: '.drag-cancel', 12 | onActivated 13 | }, 14 | slots: { 15 | default: '
Drag is not allowed here
' 16 | } 17 | }) 18 | 19 | cy.get('.drag-cancel').click() 20 | 21 | cy.get('@onActivatedSpy').should('not.have.been.called') 22 | 23 | cy.get('@vue').should(wrapper => { 24 | expect(wrapper.emitted()).to.not.have.property('update:active') 25 | }) 26 | }) 27 | 28 | it('should activate the component from outside the selector identified by the `drag-cancel` prop', () => { 29 | const onActivated = cy.spy().as('onActivatedSpy') 30 | 31 | cy.mount(VueDraggableResizable, { 32 | propsData: { 33 | dragCancel: '.drag-cancel', 34 | onActivated 35 | }, 36 | slots: { 37 | default: '
Drag is not allowed here
' 38 | } 39 | }) 40 | 41 | cy.get('div.vdr').click() 42 | 43 | cy.get('@onActivatedSpy').should('have.been.called') 44 | 45 | cy.get('@vue').should(wrapper => { 46 | expect(wrapper.emitted()).to.have.property('update:active') 47 | }) 48 | }) 49 | }) 50 | -------------------------------------------------------------------------------- /tests/cypress/props/drag-handle.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | import translateToMatrix from '../utils' 3 | 4 | describe('`drag-handle` prop', () => { 5 | 6 | it('should activate the component from the selector identified by the `drag-handle` prop', () => { 7 | const onActivated = cy.spy().as('onActivatedSpy') 8 | 9 | cy.mount(VueDraggableResizable, { 10 | propsData: { 11 | dragHandle: '.drag-handle', 12 | onActivated 13 | }, 14 | slots: { 15 | default: '
Drag only me
' 16 | } 17 | }) 18 | 19 | cy.get('.drag-handle').click() 20 | 21 | cy.get('@onActivatedSpy').should('have.been.called') 22 | 23 | cy.get('@vue').should(wrapper => { 24 | expect(wrapper.emitted()).to.have.property('update:active') 25 | }) 26 | }) 27 | 28 | it('should not activate the component from outside the selector identified by the `drag-handle` prop', () => { 29 | const onActivated = cy.spy().as('onActivatedSpy') 30 | 31 | cy.mount(VueDraggableResizable, { 32 | propsData: { 33 | dragHandle: '.drag-handle', 34 | onActivated 35 | }, 36 | slots: { 37 | default: '
Drag only me
' 38 | } 39 | }) 40 | 41 | cy.get('div.vdr').click() 42 | 43 | cy.get('@onActivatedSpy').should('not.have.been.called') 44 | 45 | cy.get('@vue').should(wrapper => { 46 | expect(wrapper.emitted()).to.not.have.property('update:active') 47 | }) 48 | }) 49 | 50 | it('should drag the component only from the selector identified by the `drag-handle` prop', () => { 51 | cy.mount(VueDraggableResizable, { 52 | propsData: { 53 | dragHandle: '.drag-handle', 54 | x: 0, 55 | y: 0, 56 | w: 100, 57 | h: 100 58 | }, 59 | slots: { 60 | default: '
Drag only me
' 61 | } 62 | }) 63 | 64 | cy.get('.drag-handle').move({ deltaX: 100, deltaY: 100 }) 65 | 66 | cy.get('div.vdr') 67 | .should('have.css', 'transform', translateToMatrix('100', '100')) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /tests/cypress/props/draggable.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | import translateToMatrix from '../utils' 3 | 4 | describe('component draggable', () => { 5 | 6 | it('should have the `draggable` class by default', () => { 7 | cy.mount(VueDraggableResizable) 8 | 9 | cy.get('div.vdr').should('have.class', 'draggable') 10 | }) 11 | 12 | it('should not have the `draggable` class if the `draggable` prop is false', () => { 13 | cy.mount(VueDraggableResizable, { 14 | propsData: { 15 | draggable: false 16 | } 17 | }) 18 | 19 | cy.get('div.vdr') 20 | .should('not.have.class', 'draggable') 21 | .should('have.class', 'resizable') 22 | 23 | cy.get('@vue').should(wrapper => { 24 | expect(wrapper.props().draggable).to.be.false 25 | }) 26 | }) 27 | 28 | it('should react to `draggable` prop changes', () => { 29 | cy.mount(VueDraggableResizable, { 30 | propsData: { 31 | draggable: false 32 | } 33 | }) 34 | 35 | cy.get('@vue').then(wrapper => { 36 | wrapper.setProps({ draggable: true }) 37 | 38 | cy.get('div.vdr').should('have.class', 'draggable') 39 | }) 40 | }) 41 | 42 | it('should not be draggable if `draggable` prop is false', () => { 43 | cy.mount(VueDraggableResizable, { 44 | propsData: { 45 | x: 0, 46 | y: 0, 47 | w: 100, 48 | h: 100, 49 | draggable: false 50 | } 51 | }) 52 | 53 | cy.get('div.vdr') 54 | .move({ deltaX: 100, deltaY: 100 }) 55 | .should('have.css', 'transform', translateToMatrix(0, 0)) 56 | .should('have.css', 'width', '100px') 57 | .should('have.css', 'height', '100px') 58 | }) 59 | }) 60 | -------------------------------------------------------------------------------- /tests/cypress/props/enable-native-drag.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | 3 | describe('native dragging', () => { 4 | 5 | it('should enable native drag by setting the `enable-native-drag` prop', () => { 6 | cy.mount(VueDraggableResizable, { 7 | propsData: { 8 | enableNativeDrag: true 9 | } 10 | }) 11 | 12 | cy.get('@vue').should(wrapper => { 13 | expect(wrapper.props().enableNativeDrag).to.be.true 14 | }) 15 | }) 16 | }) 17 | -------------------------------------------------------------------------------- /tests/cypress/props/grid.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | import translateToMatrix from '../utils' 3 | 4 | describe('`grid` prop', () => { 5 | 6 | it('should provide the `grid` as prop', () => { 7 | cy.mount(VueDraggableResizable, { 8 | propsData: { 9 | grid: [20, 40] 10 | } 11 | }) 12 | 13 | cy.get('@vue').should(wrapper => { 14 | expect(wrapper.props().grid[0]).to.equal(20) 15 | expect(wrapper.props().grid[1]).to.equal(40) 16 | }) 17 | }) 18 | 19 | it('should react to the changes of the `grid` prop', () => { 20 | cy.mount(VueDraggableResizable, { 21 | propsData: { 22 | grid: [20, 40] 23 | } 24 | }) 25 | 26 | cy.get('@vue') 27 | .then(wrapper => wrapper.setProps({ grid: [10, 30] })) 28 | .then(wrapper => { 29 | expect(wrapper.props().grid[0]).to.equal(10) 30 | expect(wrapper.props().grid[1]).to.equal(30) 31 | }) 32 | }) 33 | 34 | it('should not drag the component on the grid if the drag movement is smaller than the grid interval', () => { 35 | cy.mount(VueDraggableResizable, { 36 | propsData: { 37 | x: 0, 38 | y: 0, 39 | w: 100, 40 | h: 100, 41 | grid: [20, 40] 42 | } 43 | }) 44 | 45 | cy.get('div.vdr') 46 | .move({ deltaX: 9, deltaY: 19 }) 47 | .should('have.css', 'transform', translateToMatrix(0, 0)) 48 | }) 49 | 50 | it('should drag the component on the grid if the drag movement equals the grid interval', () => { 51 | cy.mount(VueDraggableResizable, { 52 | propsData: { 53 | x: 0, 54 | y: 0, 55 | w: 100, 56 | h: 100, 57 | grid: [20, 40] 58 | } 59 | }) 60 | 61 | cy.get('div.vdr') 62 | .move({ deltaX: 29, deltaY: 59 }) 63 | .should('have.css', 'transform', translateToMatrix(20, 40)) 64 | }) 65 | 66 | it('should not resize the component on the grid if the drag movement is smaller than the grid interval', () => { 67 | cy.mount(VueDraggableResizable, { 68 | propsData: { 69 | x: 0, 70 | y: 0, 71 | w: 100, 72 | h: 100, 73 | grid: [20, 40], 74 | active: true 75 | } 76 | }) 77 | 78 | cy.get('div.handle-br') 79 | .move({ deltaX: 9, deltaY: 19 }) 80 | 81 | cy.get('div.vdr') 82 | .should('have.css', 'transform', translateToMatrix(0, 0)) 83 | .should('have.css', 'width', '100px') 84 | .should('have.css', 'height', '100px') 85 | }) 86 | 87 | it('should resize the component on the grid if the resize movement equals the grid interval', () => { 88 | cy.mount(VueDraggableResizable, { 89 | propsData: { 90 | x: 0, 91 | y: 0, 92 | w: 100, 93 | h: 100, 94 | grid: [20, 40], 95 | active: true 96 | } 97 | }) 98 | 99 | cy.get('div.handle-br') 100 | .move({ deltaX: 20, deltaY: 40 }) 101 | 102 | cy.get('div.vdr') 103 | .should('have.css', 'transform', translateToMatrix(0, 0)) 104 | .should('have.css', 'width', '120px') 105 | .should('have.css', 'height', '140px') 106 | }) 107 | 108 | it('should not resize the component under lower grid values even if `minHeight` and `minWidth` props are lower', () => { 109 | cy.mount(VueDraggableResizable, { 110 | propsData: { 111 | x: 0, 112 | y: 0, 113 | w: 100, 114 | h: 120, 115 | minHeight: 30, 116 | minWidth: 30, 117 | grid: [20, 40], 118 | active: true 119 | } 120 | }) 121 | 122 | cy.get('div.handle-br') 123 | .move({ deltaX: -80, deltaY: -80 }) 124 | 125 | cy.get('div.vdr') 126 | .should('have.css', 'transform', translateToMatrix(0, 0)) 127 | .should('have.css', 'width', '40px') 128 | .should('have.css', 'height', '40px') 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /tests/cypress/props/handles.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | import translateToMatrix from '../utils' 3 | 4 | describe('`handles` prop', () => { 5 | 6 | it('should render only the handles passed with `handles` props', () => { 7 | cy.mount(VueDraggableResizable, { 8 | propsData: { 9 | handles: ['tl', 'tm', 'tr', 'bl', 'bm', 'br'] 10 | } 11 | }) 12 | 13 | cy.get('div.handle').should('have.length', 6) 14 | 15 | cy.get('@vue').should(wrapper => { 16 | expect(wrapper.props().handles).to.have.length(6) 17 | }) 18 | }) 19 | 20 | it('should not render the handles if `handles` props is empty', () => { 21 | cy.mount(VueDraggableResizable, { 22 | propsData: { 23 | handles: [] 24 | } 25 | }) 26 | 27 | cy.get('div.handle').should('have.length', 0) 28 | }) 29 | 30 | it('should react to `handles` prop changes', () => { 31 | cy.mount(VueDraggableResizable, { 32 | propsData: { 33 | handles: [] 34 | } 35 | }) 36 | 37 | cy.get('@vue') 38 | .then(wrapper => wrapper.setProps({ handles: ['tl', 'tm', 'tr', 'bl', 'bm', 'br'] })) 39 | .then(wrapper => { 40 | expect(wrapper.props().handles).to.have.length(6) 41 | 42 | cy.get('div.handle').should('have.length', 6) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /tests/cypress/props/lock-aspect-ratio.cy.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue/dist/vue.esm-bundler' 2 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 3 | import translateToMatrix from '../utils' 4 | 5 | describe('`lock-aspect-ratio` prop', () => { 6 | 7 | it('should provide the `lock-aspect-ratio` as prop', () => { 8 | cy.mount(VueDraggableResizable, { 9 | propsData: { 10 | lockAspectRatio: true 11 | } 12 | }) 13 | 14 | cy.get('@vue').should(wrapper => { 15 | expect(wrapper.props().lockAspectRatio).to.be.true 16 | }) 17 | }) 18 | 19 | it('should resize the component accordingly to its aspect ratio if `lock-aspect-ratio` is true', () => { 20 | cy.mount(VueDraggableResizable, { 21 | propsData: { 22 | w: 200, 23 | h: 100, 24 | active: true, 25 | lockAspectRatio: true 26 | } 27 | }) 28 | 29 | cy.get('div.handle-mr') 30 | .move({ deltaX: 100, deltaY: 0 }) 31 | 32 | cy.get('div.vdr') 33 | .should('have.css', 'transform', translateToMatrix(0, 0)) 34 | .should('have.css', 'width', '300px') 35 | .should('have.css', 'height', '150px') 36 | }) 37 | 38 | it('should not resize the component outside the parent node if `parent` prop is true and `lock-aspect-ratio` is set', () => { 39 | const ParentComponent = { 40 | template: `
41 | 42 |
`, 43 | components: { 44 | VueDraggableResizable 45 | } 46 | } 47 | 48 | cy.mount(ParentComponent) 49 | 50 | cy.get('div.handle-br') 51 | .move({ deltaX: 100, deltaY: 0 }) 52 | 53 | cy.get('div.vdr') 54 | .should('have.css', 'transform', translateToMatrix(0, 0)) 55 | .should('have.css', 'width', '375px') 56 | .should('have.css', 'height', '500px') 57 | }) 58 | }) 59 | -------------------------------------------------------------------------------- /tests/cypress/props/max-width-max-height.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | 3 | describe('`max-width` and `max-height` props', () => { 4 | 5 | it('should pass `max-width` and `max-height` as props', () => { 6 | cy.mount(VueDraggableResizable, { 7 | propsData: { 8 | maxWidth: 200, 9 | maxHeight: 300 10 | } 11 | }) 12 | 13 | cy.get('@vue').should(wrapper => { 14 | expect(wrapper.props().maxWidth).to.equal(200) 15 | expect(wrapper.props().maxHeight).to.equal(300) 16 | }) 17 | }) 18 | 19 | it('should react to `max-width` and `max-height` prop changes', () => { 20 | cy.mount(VueDraggableResizable, { 21 | propsData: { 22 | maxWidth: 200, 23 | maxHeight: 300 24 | } 25 | }) 26 | 27 | cy.get('@vue') 28 | .then(wrapper => wrapper.setProps({ maxWidth: 300, maxHeight: 200 })) 29 | .then(wrapper => { 30 | expect(wrapper.props().maxWidth).to.equal(300) 31 | expect(wrapper.props().maxHeight).to.equal(200) 32 | }) 33 | }) 34 | 35 | it('should not resize the component over `max-width` and `max-height` props', () => { 36 | cy.mount(VueDraggableResizable, { 37 | propsData: { 38 | maxWidth: 100, 39 | maxHeight: 100, 40 | w: 100, 41 | h: 100, 42 | active: true 43 | } 44 | }) 45 | 46 | cy.get('div.handle-br').move({ deltaX: 50, deltaY: 50 }) 47 | 48 | cy.get('div.vdr') 49 | .should('have.css', 'width', '100px') 50 | .should('have.css', 'height', '100px') 51 | }) 52 | }) 53 | -------------------------------------------------------------------------------- /tests/cypress/props/min-width-min-height.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | 3 | describe('`min-height` and `min-width` props', () => { 4 | 5 | it('should pass `min-height` and `min-width` as props', () => { 6 | cy.mount(VueDraggableResizable, { 7 | propsData: { 8 | minHeight: 100, 9 | minWidth: 200 10 | } 11 | }) 12 | 13 | cy.get('@vue').should(wrapper => { 14 | expect(wrapper.props().minHeight).to.equal(100) 15 | expect(wrapper.props().minWidth).to.equal(200) 16 | }) 17 | }) 18 | 19 | it('should react to `min-height` and `min-width` prop changes', () => { 20 | cy.mount(VueDraggableResizable, { 21 | propsData: { 22 | minHeight: 100, 23 | minWidth: 200 24 | } 25 | }) 26 | 27 | cy.get('@vue').then(wrapper => wrapper.setProps({ minHeight: 200, minWidth: 300 })) 28 | .then(wrapper => { 29 | expect(wrapper.props().minHeight).to.equal(200) 30 | expect(wrapper.props().minWidth).to.equal(300) 31 | }) 32 | }) 33 | 34 | it('should not resize the component under `min-height` and `min-width`', () => { 35 | cy.mount(VueDraggableResizable, { 36 | propsData: { 37 | minHeight: 100, 38 | minWidth: 100, 39 | w: 100, 40 | h: 100, 41 | active: true 42 | } 43 | }) 44 | 45 | cy.get('div.handle-br').move({ deltaX: -50, deltaY: -50 }) 46 | 47 | cy.get('div.vdr') 48 | .should('have.css', 'width', '100px') 49 | .should('have.css', 'height', '100px') 50 | }) 51 | }) 52 | -------------------------------------------------------------------------------- /tests/cypress/props/parent-grid.cy.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue/dist/vue.esm-bundler' 2 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 3 | import translateToMatrix from '../utils' 4 | 5 | describe('`parent` and `grid` props', () => { 6 | 7 | it('should not drag the component outside the parent node when grid is an exact dividend of the parent size', () => { 8 | const ParentComponent = { 9 | template: `
10 | 11 |
`, 12 | components: { 13 | VueDraggableResizable 14 | } 15 | } 16 | 17 | cy.mount(ParentComponent) 18 | 19 | cy.get('div.vdr') 20 | .move({ deltaX: 120, deltaY: 120 }) 21 | .should('have.css', 'transform', translateToMatrix(100, 100)) 22 | }) 23 | 24 | it('should not resize the component outside the parent node when the grid is an exact dividend of the parent size', () => { 25 | const ParentComponent = { 26 | template: `
27 | 28 |
`, 29 | components: { 30 | VueDraggableResizable 31 | } 32 | } 33 | 34 | cy.mount(ParentComponent) 35 | 36 | cy.get('div.handle-br') 37 | .move({ deltaX: 120, deltaY: 120 }) 38 | 39 | cy.get('div.vdr') 40 | .should('have.css', 'transform', translateToMatrix(0, 0)) 41 | .should('have.css', 'width', '400px') 42 | .should('have.css', 'height', '400px') 43 | }) 44 | 45 | it('should not drag the component outside the parent node when grid is not an exact dividend of the parent size', () => { 46 | const ParentComponent = { 47 | template: `
48 | 49 |
`, 50 | components: { 51 | VueDraggableResizable 52 | } 53 | } 54 | 55 | cy.mount(ParentComponent) 56 | 57 | cy.get('div.vdr') 58 | .move({ deltaX: 48, pageY: 48 }) 59 | .should('have.css', 'transform', translateToMatrix(24, 24)) 60 | }) 61 | 62 | it('should not resize the component outside the parent node when the grid is not an exact dividend of the parent size', () => { 63 | const ParentComponent = { 64 | template: `
65 | 66 |
`, 67 | components: { 68 | VueDraggableResizable 69 | } 70 | } 71 | 72 | cy.mount(ParentComponent) 73 | 74 | cy.get('div.handle-br') 75 | .move({ deltaX: 50, deltaY: 50 }) 76 | 77 | cy.get('div.vdr') 78 | .should('have.css', 'width', '384px') 79 | .should('have.css', 'height', '384px') 80 | }) 81 | 82 | it('should not drag the component outside the parent node when component has offset and grid is not an exact dividend of the parent size', () => { 83 | const ParentComponent = { 84 | template: `
85 | 86 |
`, 87 | components: { 88 | VueDraggableResizable 89 | } 90 | } 91 | 92 | cy.mount(ParentComponent) 93 | 94 | cy.get('div.vdr') 95 | .move({ deltaX: 48, deltaY: 48 }) 96 | .should('have.css', 'transform', translateToMatrix(34, 34)) 97 | }) 98 | 99 | it('should not resize the component outside the parent node when component has offset the grid is not an exact dividend of the parent size', () => { 100 | const ParentComponent = { 101 | template: `
102 | 103 |
`, 104 | components: { 105 | VueDraggableResizable 106 | } 107 | } 108 | 109 | cy.mount(ParentComponent) 110 | 111 | cy.get('div.handle-br') 112 | .move({ deltaX: 50, deltaY: 50 }) 113 | 114 | cy.get('div.vdr') 115 | .should('have.css', 'width', '384px') 116 | .should('have.css', 'height', '384px') 117 | }) 118 | }) 119 | -------------------------------------------------------------------------------- /tests/cypress/props/parent-position.cy.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue/dist/vue.esm-bundler' 2 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 3 | import translateToMatrix from '../utils' 4 | 5 | const ParentComponent = { 6 | props: { 7 | x: { 8 | type: Number 9 | }, 10 | y: { 11 | type: Number 12 | }, 13 | }, 14 | components: { 15 | VueDraggableResizable 16 | } 17 | } 18 | 19 | describe('`parent` and `position` props', () => { 20 | 21 | it('should set the component position outside the parent node if `parent` prop is false', () => { 22 | ParentComponent.template = `
23 | 24 |
` 25 | 26 | cy.mount(ParentComponent, { 27 | props: { 28 | x: 0, 29 | y: 0 30 | } 31 | }) 32 | 33 | cy.get('@vue') 34 | .then(wrapper => { 35 | wrapper.setProps({ x: 100, y: 100 }) 36 | }) 37 | 38 | cy.get('div.vdr') 39 | .should('have.css', 'transform', translateToMatrix(100, 100)) 40 | }) 41 | 42 | it('should not set the component position outside the parent node if `parent` prop is true', () => { 43 | ParentComponent.template = `
44 | 45 |
` 46 | 47 | cy.mount(ParentComponent, { 48 | props: { 49 | x: 0, 50 | y: 0 51 | } 52 | }) 53 | 54 | cy.get('@vue') 55 | .then(wrapper => { 56 | wrapper.setProps({ x: 100, y: 100 }) 57 | }) 58 | 59 | cy.get('div.vdr') 60 | .should('have.css', 'transform', translateToMatrix(0, 0)) 61 | }) 62 | }) 63 | -------------------------------------------------------------------------------- /tests/cypress/props/parent-size.cy.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue/dist/vue.esm-bundler' 2 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 3 | import translateToMatrix from '../utils' 4 | 5 | const ParentComponent = { 6 | props: { 7 | w: { 8 | type: Number 9 | }, 10 | h: { 11 | type: Number 12 | }, 13 | }, 14 | components: { 15 | VueDraggableResizable 16 | } 17 | } 18 | 19 | describe('`parent` and `size` props', () => { 20 | 21 | it('should set the component size outside the parent node if `parent` prop is false', () => { 22 | ParentComponent.template = `
23 | 24 |
` 25 | 26 | cy.mount(ParentComponent, { 27 | props: { 28 | w: 200, 29 | h: 200 30 | } 31 | }) 32 | 33 | cy.get('@vue') 34 | .then(wrapper => { 35 | wrapper.setProps({ w: 300, h: 300 }) 36 | }) 37 | 38 | cy.get('div.vdr') 39 | .should('have.css', 'transform', translateToMatrix(0, 0)) 40 | .should('have.css', 'width', '300px') 41 | .should('have.css', 'height', '300px') 42 | }) 43 | 44 | it('should not set the component size outside the parent node if `parent` prop is true', () => { 45 | ParentComponent.template = `
46 | 47 |
` 48 | 49 | cy.mount(ParentComponent, { 50 | props: { 51 | w: 200, 52 | h: 200 53 | } 54 | }) 55 | 56 | cy.get('@vue') 57 | .then(wrapper => { 58 | wrapper.setProps({ w: 300, h: 300 }) 59 | }) 60 | 61 | cy.get('div.vdr') 62 | .should('have.css', 'transform', translateToMatrix(0, 0)) 63 | .should('have.css', 'width', '200px') 64 | .should('have.css', 'height', '200px') 65 | }) 66 | }) 67 | -------------------------------------------------------------------------------- /tests/cypress/props/parent.cy.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue/dist/vue.esm-bundler' 2 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 3 | import translateToMatrix from '../utils' 4 | 5 | const ParentComponent = { 6 | components: { 7 | VueDraggableResizable 8 | } 9 | } 10 | 11 | describe('`parent` prop', () => { 12 | 13 | it('should drag the component outside the parent node if `parent` prop is false', () => { 14 | ParentComponent.template = `
15 | 16 |
` 17 | 18 | cy.mount(ParentComponent) 19 | 20 | cy.get('div.vdr') 21 | .move({ deltaX: 50, deltaY: 50 }) 22 | 23 | cy.get('div.vdr') 24 | .should('have.css', 'transform', translateToMatrix(50, 50)) 25 | }) 26 | 27 | it('should not drag the component outside the parent node if `parent` prop is true', () => { 28 | ParentComponent.template = `
29 | 30 |
` 31 | 32 | cy.mount(ParentComponent) 33 | 34 | cy.get('div.vdr') 35 | .move({ deltaX: 50, deltaY: 50 }) 36 | 37 | cy.get('div.vdr') 38 | .should('have.css', 'transform', translateToMatrix(0, 0)) 39 | }) 40 | 41 | it('should resize the component outside the parent node if `parent` prop is false', () => { 42 | ParentComponent.template = `
43 | 44 |
` 45 | 46 | cy.mount(ParentComponent) 47 | 48 | cy.get('div.handle-br') 49 | .move({ deltaX: 50, deltaY: 50 }) 50 | 51 | cy.get('div.vdr') 52 | .should('have.css', 'transform', translateToMatrix(0, 0)) 53 | .should('have.css', 'width', '250px') 54 | .should('have.css', 'height', '250px') 55 | }) 56 | 57 | it('should not resize the component outside the parent node if `parent` prop is true', () => { 58 | ParentComponent.template = `
59 | 60 |
` 61 | 62 | cy.mount(ParentComponent) 63 | 64 | cy.get('div.handle-br') 65 | .move({ deltaX: 50, deltaY: 50 }) 66 | 67 | cy.get('div.vdr') 68 | .should('have.css', 'transform', translateToMatrix(0, 0)) 69 | .should('have.css', 'width', '200px') 70 | .should('have.css', 'height', '200px') 71 | }) 72 | 73 | it('should resize correctly after the parent changes size to bigger values', () => { 74 | ParentComponent.template = `
75 | 76 |
` 77 | 78 | cy.mount(ParentComponent) 79 | 80 | cy.get('div.handle-br') 81 | .move({ deltaX: 50, deltaY: 50 }) 82 | 83 | cy.get('@vue').then(wrapper => { 84 | const $parent = wrapper.vm.$el 85 | 86 | $parent.style.width = '300px' 87 | $parent.style.height = '300px' 88 | 89 | window.dispatchEvent(new Event('resize')) 90 | 91 | cy.get('div.handle-br') 92 | .move({ deltaX: 50, deltaY: 50 }) 93 | 94 | cy.get('div.vdr') 95 | .should('have.css', 'transform', translateToMatrix(0, 0)) 96 | .should('have.css', 'width', '300px') 97 | .should('have.css', 'height', '300px') 98 | }) 99 | }) 100 | }) 101 | -------------------------------------------------------------------------------- /tests/cypress/props/position.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | import translateToMatrix from '../utils' 3 | 4 | describe('position props', () => { 5 | 6 | it('should set the initial position of the element using `x` and `y` props', () => { 7 | cy.mount(VueDraggableResizable, { 8 | propsData: { 9 | x: 200, 10 | y: 150 11 | } 12 | }) 13 | 14 | cy.get('@vue').should(wrapper => { 15 | expect(wrapper.props().x).to.equal(200) 16 | expect(wrapper.props().y).to.equal(150) 17 | }) 18 | 19 | cy.get('div.vdr') 20 | .should('have.css', 'transform', translateToMatrix(200, 150)) 21 | }) 22 | 23 | it('should react to position prop changes', async function () { 24 | cy.mount(VueDraggableResizable, { 25 | propsData: { 26 | x: 200, 27 | y: 150 28 | } 29 | }) 30 | 31 | cy.get('@vue').should(wrapper => { 32 | wrapper.setProps({ x: 250, y: 200 }) 33 | }) 34 | 35 | cy.get('div.vdr') 36 | .should('have.css', 'transform', translateToMatrix(250, 200)) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /tests/cypress/props/prevent-deactivation.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | 3 | describe('"prevent-deactivation" prop', () => { 4 | 5 | it('should not deactivate the component when clicking outside it if "prevent-deactivation" is set true', () => { 6 | const onDeactivated = cy.spy().as('onDeactivatedSpy') 7 | 8 | cy.mount(VueDraggableResizable, { 9 | propsData: { 10 | active: true, 11 | preventDeactivation: true, 12 | onDeactivated 13 | } 14 | }) 15 | 16 | cy.get(document.documentElement).click() 17 | 18 | cy.get('@onDeactivatedSpy').should('not.have.been.called') 19 | 20 | cy.get('div.vdr').should('have.class', 'active') 21 | }) 22 | 23 | it('should deactivate the component when clicking outside it if "prevent-deactivation" is set false', () => { 24 | const onDeactivated = cy.spy().as('onDeactivatedSpy') 25 | 26 | cy.mount(VueDraggableResizable, { 27 | propsData: { 28 | active: true, 29 | preventDeactivation: false, 30 | onDeactivated 31 | } 32 | }) 33 | 34 | cy.get(document.documentElement).click() 35 | 36 | cy.get('@onDeactivatedSpy').should('have.been.called') 37 | 38 | cy.get('div.vdr').should('not.have.class', 'active') 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /tests/cypress/props/resizable.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | import translateToMatrix from '../utils' 3 | 4 | describe('component resizable', () => { 5 | 6 | it('should have `resizable` class by default', () => { 7 | cy.mount(VueDraggableResizable) 8 | 9 | cy.get('div.vdr').should('have.class', 'resizable') 10 | }) 11 | 12 | it('should not have the `resizable` class if the `resizable` prop is false', () => { 13 | cy.mount(VueDraggableResizable, { 14 | propsData: { 15 | resizable: false 16 | } 17 | }) 18 | 19 | cy.get('div.vdr') 20 | .should('not.have.class', 'resizable') 21 | .should('have.class', 'draggable') 22 | 23 | cy.get('@vue').should(wrapper => { 24 | expect(wrapper.props().resizable).to.be.false 25 | }) 26 | }) 27 | 28 | it('should not render handles if the `resizable` prop is false', () => { 29 | cy.mount(VueDraggableResizable, { 30 | propsData: { 31 | active: true, 32 | resizable: false 33 | } 34 | }) 35 | 36 | cy.get('div.handle').should('have.length', 0) 37 | }) 38 | 39 | 40 | it('should react to `resizable` prop changes', () => { 41 | cy.mount(VueDraggableResizable, { 42 | propsData: { 43 | resizable: false 44 | } 45 | }) 46 | 47 | cy.get('@vue').then(wrapper => { 48 | wrapper.setProps({ resizable: true }) 49 | 50 | cy.get('div.vdr').should('have.class', 'resizable') 51 | }) 52 | }) 53 | }) 54 | -------------------------------------------------------------------------------- /tests/cypress/props/scale.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | import translateToMatrix from '../utils' 3 | 4 | describe('`scale` prop', () => { 5 | 6 | it('should drag the component accordingly to the `scale` prop', () => { 7 | cy.mount(VueDraggableResizable, { 8 | propsData: { 9 | w: 200, 10 | h: 200, 11 | scale: 0.5, 12 | active: true 13 | } 14 | }) 15 | 16 | cy.get('div.vdr') 17 | .move({ deltaX: 50, deltaY: 50 }) 18 | 19 | cy.get('div.vdr') 20 | .should('have.css', 'transform', translateToMatrix(100, 100)) 21 | }) 22 | 23 | it('should resize the component accordingly to the `scale` prop', () => { 24 | cy.mount(VueDraggableResizable, { 25 | propsData: { 26 | w: 200, 27 | h: 200, 28 | scale: 1.5, 29 | active: true 30 | } 31 | }) 32 | 33 | cy.get('div.handle-br') 34 | .move({ deltaX: 50, deltaY: 50 }) 35 | 36 | cy.get('div.vdr') 37 | .should('have.css', 'width', '233px') 38 | .should('have.css', 'height', '233px') 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /tests/cypress/props/size.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | 3 | describe('size props', () => { 4 | 5 | it('should set the initial size of the component using `w` and `h` props', () => { 6 | cy.mount(VueDraggableResizable, { 7 | propsData: { 8 | w: 200, 9 | h: 150 10 | } 11 | }) 12 | 13 | cy.get('div.vdr') 14 | .should('have.css', 'width', '200px') 15 | .should('have.css', 'height', '150px') 16 | 17 | cy.get('@vue').should(wrapper => { 18 | expect(wrapper.vm.$el.style.width).to.equal('200px') 19 | expect(wrapper.vm.$el.style.height).to.equal('150px') 20 | }) 21 | }) 22 | 23 | it('should react to size prop changes', () => { 24 | cy.mount(VueDraggableResizable, { 25 | propsData: { 26 | w: 200, 27 | h: 150 28 | } 29 | }) 30 | 31 | cy.get('@vue').then(wrapper => { 32 | wrapper.setProps({ w: 250, h: 200 }) 33 | 34 | cy.get('div.vdr') 35 | .should('have.css', 'width', '250px') 36 | .should('have.css', 'height', '200px') 37 | }) 38 | }) 39 | 40 | it('should allow auto value for `w` and `h` props', () => { 41 | cy.mount(VueDraggableResizable, { 42 | propsData: { 43 | w: 'auto', 44 | h: 'auto' 45 | }, 46 | slots: { 47 | default: '
' 48 | } 49 | }) 50 | 51 | cy.get('div.vdr') 52 | // Cypress tests computed styles (100px for the content, 1px for the border) 53 | .should('have.css', 'width', '102px') 54 | .should('have.css', 'height', '102px') 55 | 56 | cy.get('@vue').should(wrapper => { 57 | expect(wrapper.props().w).to.equal('auto') 58 | expect(wrapper.props().h).to.equal('auto') 59 | }) 60 | }) 61 | }) 62 | -------------------------------------------------------------------------------- /tests/cypress/props/z-index.cy.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | 3 | describe('z-index prop', () => { 4 | 5 | it('should set the z-index through the `z` prop', () => { 6 | cy.mount(VueDraggableResizable, { 7 | propsData: { 8 | z: 99 9 | } 10 | }) 11 | 12 | cy.get('div.vdr').should('have.css', 'z-index', '99') 13 | 14 | cy.get('@vue').should(wrapper => { 15 | expect(wrapper.props().z).to.equal(99) 16 | }) 17 | }) 18 | 19 | it('should set "auto" as defaul value for z-index if `z` prop is not provided', () => { 20 | cy.mount(VueDraggableResizable) 21 | 22 | cy.get('div.vdr').should('have.css', 'z-index', 'auto') 23 | }) 24 | 25 | it('should react to `z` prop changes', () => { 26 | cy.mount(VueDraggableResizable, { 27 | propsData: { 28 | z: 99 29 | } 30 | }) 31 | 32 | cy.get('@vue') 33 | .then(wrapper => { 34 | wrapper.setProps({ z: 999 }) 35 | }) 36 | .then(wrapper => { 37 | cy.get('div.vdr').should('have.css', 'z-index', '999') 38 | }) 39 | }) 40 | }) 41 | -------------------------------------------------------------------------------- /tests/cypress/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts translate(x, y) into corresponding matrix function 3 | * @param {int} x 4 | * @param {int} y 5 | * @returns 6 | */ 7 | export default function translateToMatrix(x, y) { 8 | return `matrix(1, 0, 0, 1, ${x}, ${y})`; 9 | } 10 | -------------------------------------------------------------------------------- /tests/fns.spec.js: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest' 2 | import { isFunction, snapToGrid } from '@/utils/fns' 3 | 4 | describe('fns', () => { 5 | 6 | describe('isFunction', () => { 7 | 8 | it('should return false if a String is provided', () => { 9 | expect(isFunction('test')).to.be.false 10 | }) 11 | 12 | it('should return false if a Number is provided', () => { 13 | expect(isFunction(123)).to.be.false 14 | }) 15 | 16 | it('should return false if a Boolean is provided', () => { 17 | expect(isFunction(true)).to.be.false 18 | }) 19 | 20 | it('should return false if a null is provided', () => { 21 | expect(isFunction(null)).to.be.false 22 | }) 23 | 24 | it('should return true if a function is provided', () => { 25 | expect(isFunction(() => {})).to.be.true 26 | }) 27 | }) 28 | 29 | describe('snapToGrid', () => { 30 | 31 | it('should snap exactly to the size of the grid', () => { 32 | expect(snapToGrid([1, 1], 0, 0)).to.deep.equal([0, 0]) 33 | expect(snapToGrid([1, 1], 1, 1)).to.deep.equal([1, 1]) 34 | }) 35 | 36 | it('should snap exactly to the size of the grid', () => { 37 | expect(snapToGrid([5, 5], 0, 0)).to.deep.equal([0, 0]) 38 | expect(snapToGrid([5, 5], 1, 1)).to.deep.equal([0, 0]) 39 | expect(snapToGrid([5, 5], 2, 2)).to.deep.equal([0, 0]) 40 | expect(snapToGrid([5, 5], 3, 3)).to.deep.equal([5, 5]) 41 | expect(snapToGrid([5, 5], 4, 4)).to.deep.equal([5, 5]) 42 | expect(snapToGrid([5, 5], 5, 5)).to.deep.equal([5, 5]) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /tests/validation.spec.js: -------------------------------------------------------------------------------- 1 | import VueDraggableResizable from '@/components/vue-draggable-resizable.vue' 2 | import { mount } from '@vue/test-utils' 3 | import { describe, it, expect, afterEach, beforeEach } from 'vitest' 4 | 5 | let wrapper 6 | 7 | const getProp = (prop) => { 8 | return wrapper.vm.$options.props[prop] 9 | } 10 | 11 | describe('props validation', () => { 12 | 13 | beforeEach(() => { 14 | wrapper = mount(VueDraggableResizable) 15 | }) 16 | 17 | it('should validate `className` prop', () => { 18 | const className = getProp('className') 19 | 20 | expect(className.required).to.be.undefined 21 | expect(className.type).to.equal(String) 22 | expect(className.default).to.equal('vdr') 23 | }) 24 | 25 | it('should validate `classNameDraggable` prop', () => { 26 | const classNameDraggable = getProp('classNameDraggable') 27 | 28 | expect(classNameDraggable.required).to.be.undefined 29 | expect(classNameDraggable.type).to.equal(String) 30 | expect(classNameDraggable.default).to.equal('draggable') 31 | }) 32 | 33 | it('should validate `classNameResizable` prop', () => { 34 | const classNameResizable = getProp('classNameResizable') 35 | 36 | expect(classNameResizable.required).to.be.undefined 37 | expect(classNameResizable.type).to.equal(String) 38 | expect(classNameResizable.default).to.equal('resizable') 39 | }) 40 | 41 | it('should validate `classNameDragging` prop', () => { 42 | const classNameDragging = getProp('classNameDragging') 43 | 44 | expect(classNameDragging.required).to.be.undefined 45 | expect(classNameDragging.type).to.equal(String) 46 | expect(classNameDragging.default).to.equal('dragging') 47 | }) 48 | 49 | it('should validate `classNameResizing` prop', () => { 50 | const classNameResizing = getProp('classNameResizing') 51 | 52 | expect(classNameResizing.required).to.be.undefined 53 | expect(classNameResizing.type).to.equal(String) 54 | expect(classNameResizing.default).to.equal('resizing') 55 | }) 56 | 57 | it('should validate `classNameActive` prop', () => { 58 | const classNameActive = getProp('classNameActive') 59 | 60 | expect(classNameActive.required).to.be.undefined 61 | expect(classNameActive.type).to.equal(String) 62 | expect(classNameActive.default).to.equal('active') 63 | }) 64 | 65 | it('should validate `classNameHandle` prop', () => { 66 | const classNameHandle = getProp('classNameHandle') 67 | 68 | expect(classNameHandle.required).to.be.undefined 69 | expect(classNameHandle.type).to.equal(String) 70 | expect(classNameHandle.default).to.equal('handle') 71 | }) 72 | 73 | it('should validate `disableUserSelect` prop', () => { 74 | const disableUserSelect = getProp('disableUserSelect') 75 | 76 | expect(disableUserSelect.required).to.be.undefined 77 | expect(disableUserSelect.type).to.equal(Boolean) 78 | expect(disableUserSelect.default).to.be.true 79 | }) 80 | 81 | it('should validate `enableNativeDrag` prop', () => { 82 | const enableNativeDrag = getProp('enableNativeDrag') 83 | 84 | expect(enableNativeDrag.required).to.be.undefined 85 | expect(enableNativeDrag.type).to.equal(Boolean) 86 | expect(enableNativeDrag.default).to.be.false 87 | }) 88 | 89 | it('should validate `preventDeactivation` prop', () => { 90 | const preventDeactivation = getProp('preventDeactivation') 91 | 92 | expect(preventDeactivation.required).to.be.undefined 93 | expect(preventDeactivation.type).to.equal(Boolean) 94 | expect(preventDeactivation.default).to.be.false 95 | }) 96 | 97 | it('should validate `active` prop', () => { 98 | const active = getProp('active') 99 | 100 | expect(active.required).to.be.undefined 101 | expect(active.type).to.equal(Boolean) 102 | expect(active.default).to.be.false 103 | }) 104 | 105 | it('should validate `draggable` prop', () => { 106 | const draggable = getProp('draggable') 107 | 108 | expect(draggable.required).to.be.undefined 109 | expect(draggable.type).to.equal(Boolean) 110 | expect(draggable.default).to.be.true 111 | }) 112 | 113 | it('should validate `resizable` prop', () => { 114 | const resizable = getProp('resizable') 115 | 116 | expect(resizable.required).to.be.undefined 117 | expect(resizable.type).to.equal(Boolean) 118 | expect(resizable.default).to.be.true 119 | }) 120 | 121 | it('should validate `lockAspectRatio` prop', () => { 122 | const lockAspectRatio = getProp('lockAspectRatio') 123 | 124 | expect(lockAspectRatio.required).to.be.undefined 125 | expect(lockAspectRatio.type).to.equal(Boolean) 126 | expect(lockAspectRatio.default).to.be.false 127 | }) 128 | 129 | it('should validate `w` prop', () => { 130 | const w = getProp('w') 131 | 132 | expect(w.required).to.be.undefined 133 | expect(w.type).to.contain(String) 134 | expect(w.type).to.contain(Number) 135 | expect(w.validator('auto')).to.be.true 136 | expect(w.validator(200)).to.be.true 137 | expect(w.validator('random')).to.be.false 138 | expect(w.validator(-200)).to.be.false 139 | }) 140 | 141 | it('should validate `h` prop', () => { 142 | const h = getProp('w') 143 | 144 | expect(h.required).to.be.undefined 145 | expect(h.type).to.contain(String) 146 | expect(h.type).to.contain(Number) 147 | expect(h.validator('auto')).to.be.true 148 | expect(h.validator(200)).to.be.true 149 | expect(h.validator('random')).to.be.false 150 | expect(h.validator(-200)).to.be.false 151 | }) 152 | 153 | it('should validate `minWidth` prop', () => { 154 | const minWidth = getProp('minWidth') 155 | 156 | expect(minWidth.required).to.be.undefined 157 | expect(minWidth.type).to.equal(Number) 158 | expect(minWidth.default).to.equal(0) 159 | expect(minWidth.validator(1)).to.be.true 160 | expect(minWidth.validator(-1)).to.be.false 161 | }) 162 | 163 | it('should validate `minHeight` prop', () => { 164 | const minHeight = getProp('minHeight') 165 | 166 | expect(minHeight.required).to.be.undefined 167 | expect(minHeight.type).to.equal(Number) 168 | expect(minHeight.default).to.equal(0) 169 | expect(minHeight.validator(1)).to.be.true 170 | expect(minHeight.validator(-1)).to.be.false 171 | }) 172 | 173 | it('should validate `maxWidth` prop', () => { 174 | const maxWidth = getProp('maxWidth') 175 | 176 | expect(maxWidth.required).to.be.undefined 177 | expect(maxWidth.type).to.equal(Number) 178 | expect(maxWidth.default).to.be.null 179 | expect(maxWidth.validator(1)).to.be.true 180 | expect(maxWidth.validator(-1)).to.be.false 181 | }) 182 | 183 | it('should validate `maxHeight` prop', () => { 184 | const maxHeight = getProp('maxHeight') 185 | 186 | expect(maxHeight.required).to.be.undefined 187 | expect(maxHeight.type).to.equal(Number) 188 | expect(maxHeight.default).to.be.null 189 | expect(maxHeight.validator(1)).to.be.true 190 | expect(maxHeight.validator(-1)).to.be.false 191 | }) 192 | 193 | it('should validate `x` prop', () => { 194 | const x = getProp('x') 195 | 196 | expect(x.required).to.be.undefined 197 | expect(x.type).to.equal(Number) 198 | expect(x.default).to.equal(0) 199 | }) 200 | 201 | it('should validate `y` prop', () => { 202 | const y = getProp('y') 203 | 204 | expect(y.required).to.be.undefined 205 | expect(y.type).to.equal(Number) 206 | expect(y.default).to.equal(0) 207 | }) 208 | 209 | it('should validate `z` prop', () => { 210 | const z = getProp('z') 211 | 212 | expect(z.required).to.be.undefined 213 | expect(z.type).to.contain(String) 214 | expect(z.type).to.contain(Number) 215 | expect(z.default).to.equal('auto') 216 | expect(z.validator(1)).to.be.true 217 | expect(z.validator(-1)).to.be.false 218 | expect(z.validator('auto')).to.be.true 219 | expect(z.validator('test')).to.be.false 220 | }) 221 | 222 | it('should validate `handles` prop', () => { 223 | const handles = getProp('handles') 224 | 225 | expect(handles.required).to.be.undefined 226 | expect(handles.type).to.equal(Array) 227 | expect(handles.default()).to.deep.equal(['tl', 'tm', 'tr', 'mr', 'br', 'bm', 'bl', 'ml']) 228 | expect(handles.validator([])).to.be.true 229 | expect(handles.validator(['cc', 'pp'])).to.be.false 230 | }) 231 | 232 | it('should validate `dragHandle` prop', () => { 233 | const dragHandle = getProp('dragHandle') 234 | 235 | expect(dragHandle.required).to.be.undefined 236 | expect(dragHandle.type).to.equal(String) 237 | expect(dragHandle.default).to.be.null 238 | }) 239 | 240 | it('should validate `dragCancel` prop', () => { 241 | const dragCancel = getProp('dragCancel') 242 | 243 | expect(dragCancel.required).to.be.undefined 244 | expect(dragCancel.type).to.equal(String) 245 | expect(dragCancel.default).to.be.null 246 | }) 247 | 248 | it('should validate `axis` prop', () => { 249 | const axis = getProp('axis') 250 | 251 | expect(axis.required).to.be.undefined 252 | expect(axis.type).to.equal(String) 253 | expect(axis.default).to.equal('both') 254 | expect(axis.validator('x')).to.be.true 255 | expect(axis.validator('y')).to.be.true 256 | expect(axis.validator('both')).to.be.true 257 | expect(axis.validator('test')).to.be.false 258 | }) 259 | 260 | it('should validate `grid` prop', () => { 261 | const grid = getProp('grid') 262 | 263 | expect(grid.required).to.be.undefined 264 | expect(grid.type).to.equal(Array) 265 | expect(grid.default()).to.deep.equal([1, 1]) 266 | }) 267 | 268 | it('should validate `parent` prop', () => { 269 | const parent = getProp('parent') 270 | 271 | expect(parent.required).to.be.undefined 272 | expect(parent.type).to.equal(Boolean) 273 | expect(parent.default).to.equal(false) 274 | }) 275 | 276 | it('should validate `scale` prop', () => { 277 | const scale = getProp('scale') 278 | 279 | expect(scale.required).to.be.undefined 280 | expect(scale.type).to.contain(Number) 281 | expect(scale.type).to.contain(Array) 282 | expect(scale.default).to.equal(1) 283 | expect(scale.validator(1)).to.be.true 284 | expect(scale.validator([0.5, 0.4])).to.be.true 285 | expect(scale.validator(0)).to.be.false 286 | expect(scale.validator(-1)).to.be.false 287 | expect(scale.validator([0, 0])).to.be.false 288 | expect(scale.validator([-1, -1])).to.be.false 289 | expect(scale.validator([1, 1, 1])).to.be.false 290 | }) 291 | 292 | it('should validate `onDragStart` prop', () => { 293 | const onDragStart = getProp('onDragStart') 294 | 295 | expect(onDragStart.required).to.be.undefined 296 | expect(onDragStart.type).to.equal(Function) 297 | expect(onDragStart.default()).to.equal(true) 298 | }) 299 | 300 | it('should validate `onDrag` prop', () => { 301 | const onDrag = getProp('onDrag') 302 | 303 | expect(onDrag.required).to.be.undefined 304 | expect(onDrag.type).to.equal(Function) 305 | expect(onDrag.default()).to.equal(true) 306 | }) 307 | 308 | it('should validate `onResizeStart` prop', () => { 309 | const onResizeStart = getProp('onResizeStart') 310 | 311 | expect(onResizeStart.required).to.be.undefined 312 | expect(onResizeStart.type).to.equal(Function) 313 | expect(onResizeStart.default()).to.equal(true) 314 | }) 315 | 316 | it('should validate `onResize` prop', () => { 317 | const onResize = getProp('onResize') 318 | 319 | expect(onResize.required).to.be.undefined 320 | expect(onResize.type).to.equal(Function) 321 | expect(onResize.default()).to.equal(true) 322 | }) 323 | 324 | afterEach(() => wrapper.unmount()) 325 | }) 326 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | /// 2 | import { defineConfig } from 'vite' 3 | import { resolve } from 'path' 4 | import vue from '@vitejs/plugin-vue' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | test: { 9 | environment: 'jsdom' 10 | }, 11 | build: { 12 | lib: { 13 | entry: resolve(__dirname, 'src/install.js'), 14 | name: 'VueDraggableResizable', 15 | fileName: 'vue-draggable-resizable' 16 | }, 17 | rollupOptions: { 18 | external: ['vue'], 19 | output: { 20 | exports: 'named', 21 | globals: { 22 | 'vue': 'Vue', 23 | } 24 | } 25 | }, 26 | }, 27 | server: { 28 | port: 8080 29 | }, 30 | resolve: { 31 | dedupe: ['vue'], 32 | alias: { 33 | '~': resolve(__dirname, './src'), 34 | '@': resolve(__dirname, './src') 35 | }, 36 | }, 37 | plugins: [vue()] 38 | }) 39 | --------------------------------------------------------------------------------