├── .circleci └── config.yml ├── .codebeatignore ├── .env.development ├── .env.local ├── .github └── issue_template.md ├── .gitignore ├── .vscode └── launch.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── babel.config.js ├── dist ├── demo.html ├── vuedraggable.common.js ├── vuedraggable.common.js.map ├── vuedraggable.umd.js ├── vuedraggable.umd.js.map ├── vuedraggable.umd.min.js └── vuedraggable.umd.min.js.map ├── docs ├── app.js ├── favicon.ico ├── fonts │ ├── element-icons.2fad952a.woff │ ├── element-icons.6f0a7632.ttf │ ├── fontawesome-webfont.674f50d2.eot │ ├── fontawesome-webfont.af7ae505.woff2 │ ├── fontawesome-webfont.b06871f2.ttf │ └── fontawesome-webfont.fee66e71.woff ├── img │ ├── fontawesome-webfont.912ec66d.svg │ └── logo.c6a3753c.svg └── index.html ├── documentation ├── Vue.draggable.for.ReadME.md ├── legacy.options.md └── migrate.md ├── example.gif ├── example ├── App.vue ├── assets │ └── logo.svg ├── components │ ├── clone-on-control.vue │ ├── clone.vue │ ├── custom-clone.vue │ ├── footerslot.vue │ ├── functional.vue │ ├── handle.vue │ ├── headerslot.vue │ ├── infra │ │ ├── nested.vue │ │ └── raw-displayer.vue │ ├── nested-example.vue │ ├── nested-with-vmodel.vue │ ├── nested │ │ ├── nested-store.js │ │ └── nested-test.vue │ ├── simple.vue │ ├── table-column-example.vue │ ├── table-example.vue │ ├── third-party.vue │ ├── transition-example-2.vue │ ├── transition-example.vue │ ├── two-list-headerslots.vue │ └── two-lists.vue ├── debug-components │ ├── future-index.vue │ ├── nested │ │ └── draggable-list.vue │ └── slot-example.vue ├── main.js ├── route.js └── store.js ├── jest.config.js ├── logo.png ├── logo.svg ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── util │ └── helper.js ├── vuedraggable.d.ts └── vuedraggable.js ├── tests └── unit │ ├── .eslintrc.js │ ├── helper │ ├── DraggableWithList.vue │ ├── DraggableWithModel.vue │ ├── DraggableWithTransition.vue │ ├── FakeComponent.js │ └── FakeFunctionalComponent.js │ ├── util │ ├── helper.node.spec.js │ └── helper.spec.js │ ├── vuedraggable.integrated.spec.js │ ├── vuedraggable.script.tag.spec.js │ ├── vuedraggable.spec.js │ └── vuedraggable.ssr.spec.js ├── vue.config.js └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10-browsers 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: yarn test:coverage 38 | 39 | 40 | -------------------------------------------------------------------------------- /.codebeatignore: -------------------------------------------------------------------------------- 1 | docs/** -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VUE_APP_SHOW_ALL_EXAMPLES=false 2 | -------------------------------------------------------------------------------- /.env.local: -------------------------------------------------------------------------------- 1 | VUE_APP_SHOW_ALL_EXAMPLES=true 2 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | First check https://github.com/SortableJS/Vue.Draggable/blob/master/CONTRIBUTING.md 2 | 3 | ### Jsfiddle link 4 | 5 | ### Step by step scenario 6 | 7 | ### Actual Solution 8 | 9 | ### Expected Solution 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .tmp 3 | .sass-cache 4 | app/bower_components 5 | test/bower_components 6 | bower_components 7 | examples/src 8 | test/tmp 9 | /test/tmp 10 | /examples/src 11 | /examples/libs/ 12 | /coverage 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Jest All", 8 | "program": "${workspaceFolder}/node_modules/.bin/jest", 9 | "args": ["--runInBand"], 10 | "console": "integratedTerminal", 11 | "internalConsoleOptions": "neverOpen", 12 | "disableOptimisticBPs": true, 13 | "windows": { 14 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 15 | } 16 | }, 17 | { 18 | "type": "node", 19 | "request": "launch", 20 | "name": "Jest Current File", 21 | "program": "${workspaceFolder}/node_modules/.bin/jest", 22 | "args": [ 23 | "${relativeFile}", 24 | "--config", 25 | "jest.config.js" 26 | ], 27 | "console": "integratedTerminal", 28 | "internalConsoleOptions": "neverOpen", 29 | "disableOptimisticBPs": true, 30 | "windows": { 31 | "program": "${workspaceFolder}/node_modules/jest/bin/jest", 32 | } 33 | }, 34 | { 35 | "type": "node", 36 | "request": "launch", 37 | "name": "Jest Vue Current File", 38 | "program": "${workspaceFolder}/node_modules/@vue/cli-service/bin/vue-cli-service", 39 | "args": [ 40 | "test:unit", 41 | "${relativeFile}", 42 | ], 43 | "console": "integratedTerminal", 44 | "internalConsoleOptions": "neverOpen", 45 | "disableOptimisticBPs": true, 46 | "windows": { 47 | "program": "${workspaceFolder}/node_modules/@vue/cli-service/bin/vue-cli-service", 48 | } 49 | } 50 | ] 51 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute 2 | 3 | #### **Did you find a bug?** 4 | 5 | * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/SortableJS/Vue.Draggable/issues). 6 | 7 | * **Check if you are using the last version of vue.draggable and a compatible version of Sortable** (as indicated in the [README section](./README.md)) 8 | 9 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/SortableJS/Vue.Draggable/issues/new). Be sure to respect issue template including a **title and clear description**, as much relevant information as possible, and a [**jsfiddle**](http://jsfiddle.net/) (or similar online tool) containing an sample demonstrating the bug. Explain the **step by step scenario** as well as the **actual result** as opposed as the **expected result**. 10 | 11 | #### **Do you have questions about how to use vue.draggable?** 12 | 13 | * Check [README section](./README.md) section as well as [Issues](https://github.com/SortableJS/Vue.Draggable/issues) to see if a similar question has been asked and answered. 14 | 15 | * Check [Sortable](https://github.com/RubaXa/Sortable) documentation. 16 | 17 | * DO NOT OPEN ISSUE. Ask a question on [stackoverflow](https://stackoverflow.com) instead to get answer from the vue fantastic community. 18 | 19 | #### **Did you write a correction that fixes a bug?** 20 | 21 | * Open a new GitHub pull request with the code. 22 | 23 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 24 | 25 | #### **Do you intend to add a new feature or change an existing one?** 26 | 27 | * Open an issue proposing the enhancement explaining the rational and the added value. 28 | 29 | * Once agreed you may submit the corresponding PR. 30 | 31 | Thanks! 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2019 David Desmaisons 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |

Vue.Draggable

3 | 4 | [![CircleCI](https://circleci.com/gh/SortableJS/Vue.Draggable.svg?style=shield)](https://circleci.com/gh/SortableJS/Vue.Draggable) 5 | [![Coverage](https://codecov.io/gh/SortableJS/Vue.Draggable/branch/master/graph/badge.svg)](https://codecov.io/gh/SortableJS/Vue.Draggable) 6 | [![codebeat badge](https://codebeat.co/badges/7a6c27c8-2d0b-47b9-af55-c2eea966e713)](https://codebeat.co/projects/github-com-sortablejs-vue-draggable-master) 7 | [![GitHub open issues](https://img.shields.io/github/issues/SortableJS/Vue.Draggable.svg)](https://github.com/SortableJS/Vue.Draggable/issues?q=is%3Aopen+is%3Aissue) 8 | [![npm download](https://img.shields.io/npm/dt/vuedraggable.svg)](https://www.npmjs.com/package/vuedraggable) 9 | [![npm download per month](https://img.shields.io/npm/dm/vuedraggable.svg)](https://www.npmjs.com/package/vuedraggable) 10 | [![npm version](https://img.shields.io/npm/v/vuedraggable.svg)](https://www.npmjs.com/package/vuedraggable) 11 | [![MIT License](https://img.shields.io/github/license/SortableJS/Vue.Draggable.svg)](https://github.com/SortableJS/Vue.Draggable/blob/master/LICENSE) 12 | 13 | 14 | Vue component (Vue.js 2.0) or directive (Vue.js 1.0) allowing drag-and-drop and synchronization with view model array. 15 | 16 | Based on and offering all features of [Sortable.js](https://github.com/RubaXa/Sortable) 17 | 18 | 19 | ## For Vue 3 20 | See [vue.draggable.next](https://github.com/SortableJS/vue.draggable.next) 21 | 22 | ## Demo 23 | 24 | ![demo gif](https://raw.githubusercontent.com/SortableJS/Vue.Draggable/master/example.gif) 25 | 26 | ## Live Demos 27 | 28 | https://sortablejs.github.io/Vue.Draggable/ 29 | 30 | https://david-desmaisons.github.io/draggable-example/ 31 | 32 | ## Features 33 | 34 | * Full support of [Sortable.js](https://github.com/RubaXa/Sortable) features: 35 | * Supports touch devices 36 | * Supports drag handles and selectable text 37 | * Smart auto-scrolling 38 | * Support drag and drop between different lists 39 | * No jQuery dependency 40 | * Keeps in sync HTML and view model list 41 | * Compatible with Vue.js 2.0 transition-group 42 | * Cancellation support 43 | * Events reporting any changes when full control is needed 44 | * Reuse existing UI library components (such as [vuetify](https://vuetifyjs.com), [element](http://element.eleme.io/), or [vue material](https://vuematerial.io) etc...) and make them draggable using `tag` and `componentData` props 45 | 46 | ## Backers 47 | 48 | Looking for backers! 49 | 50 | ## Donate 51 | 52 | Find this project useful? You can buy me a :coffee: or a :beer: 53 | 54 | [![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donate_LG.gif)](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=GYAEKQZJ4FQT2¤cy_code=USD&source=url) 55 | 56 | 57 | ## Installation 58 | 59 | ### With npm or yarn 60 | 61 | ```bash 62 | yarn add vuedraggable 63 | 64 | npm i -S vuedraggable 65 | ``` 66 | 67 | **Beware it is vuedraggable for Vue 2.0 and not vue-draggable which is for version 1.0** 68 | 69 | ### with direct link 70 | ```html 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | ``` 79 | 80 | [cf example section](https://github.com/SortableJS/Vue.Draggable/tree/master/example) 81 | 82 | ## For Vue.js 2.0 83 | 84 | Use draggable component: 85 | 86 | ### Typical use: 87 | ``` html 88 | 89 |
{{element.name}}
90 |
91 | ``` 92 | .vue file: 93 | ``` js 94 | import draggable from 'vuedraggable' 95 | ... 96 | export default { 97 | components: { 98 | draggable, 99 | }, 100 | ... 101 | ``` 102 | 103 | ### With `transition-group`: 104 | ``` html 105 | 106 | 107 |
108 | {{element.name}} 109 |
110 |
111 |
112 | ``` 113 | 114 | Draggable component should directly wrap the draggable elements, or a `transition-component` containing the draggable elements. 115 | 116 | 117 | ### With footer slot: 118 | ``` html 119 | 120 |
121 | {{element.name}} 122 |
123 | 124 |
125 | ``` 126 | ### With header slot: 127 | ``` html 128 | 129 |
130 | {{element.name}} 131 |
132 | 133 |
134 | ``` 135 | 136 | ### With Vuex: 137 | 138 | ```html 139 | 140 | ``` 141 | 142 | ```javascript 143 | computed: { 144 | myList: { 145 | get() { 146 | return this.$store.state.myList 147 | }, 148 | set(value) { 149 | this.$store.commit('updateList', value) 150 | } 151 | } 152 | } 153 | ``` 154 | 155 | 156 | ### Props 157 | #### value 158 | Type: `Array`
159 | Required: `false`
160 | Default: `null` 161 | 162 | Input array to draggable component. Typically same array as referenced by inner element v-for directive.
163 | This is the preferred way to use Vue.draggable as it is compatible with Vuex.
164 | It should not be used directly but only though the `v-model` directive: 165 | ```html 166 | 167 | ``` 168 | 169 | #### list 170 | Type: `Array`
171 | Required: `false`
172 | Default: `null` 173 | 174 | Alternative to the `value` prop, list is an array to be synchronized with drag-and-drop.
175 | The main difference is that `list` prop is updated by draggable component using splice method, whereas `value` is immutable.
176 | **Do not use in conjunction with value prop.** 177 | 178 | #### All sortable options 179 | New in version 2.19 180 | 181 | Sortable options can be set directly as vue.draggable props since version 2.19. 182 | 183 | This means that all [sortable option](https://github.com/RubaXa/Sortable#options) are valid sortable props with the notable exception of all the method starting by "on" as draggable component expose the same API via events. 184 | 185 | kebab-case propery are supported: for example `ghost-class` props will be converted to `ghostClass` sortable option. 186 | 187 | Example setting handle, sortable and a group option: 188 | ```HTML 189 | 197 | 198 | 199 | ``` 200 | 201 | #### tag 202 | Type: `String`
203 | Default: `'div'` 204 | 205 | HTML node type of the element that draggable component create as outer element for the included slot.
206 | It is also possible to pass the name of vue component as element. In this case, draggable attribute will be passed to the create component.
207 | See also [componentData](#componentdata) if you need to set props or event to the created component. 208 | 209 | #### clone 210 | Type: `Function`
211 | Required: `false`
212 | Default: `(original) => { return original;}`
213 | 214 | Function called on the source component to clone element when clone option is true. The unique argument is the viewModel element to be cloned and the returned value is its cloned version.
215 | By default vue.draggable reuses the viewModel element, so you have to use this hook if you want to clone or deep clone it. 216 | 217 | #### move 218 | Type: `Function`
219 | Required: `false`
220 | Default: `null`
221 | 222 | If not null this function will be called in a similar way as [Sortable onMove callback](https://github.com/RubaXa/Sortable#move-event-object). 223 | Returning false will cancel the drag operation. 224 | 225 | ```javascript 226 | function onMoveCallback(evt, originalEvent){ 227 | ... 228 | // return false; — for cancel 229 | } 230 | ``` 231 | evt object has same property as [Sortable onMove event](https://github.com/RubaXa/Sortable#move-event-object), and 3 additional properties: 232 | - `draggedContext`: context linked to dragged element 233 | - `index`: dragged element index 234 | - `element`: dragged element underlying view model element 235 | - `futureIndex`: potential index of the dragged element if the drop operation is accepted 236 | - `relatedContext`: context linked to current drag operation 237 | - `index`: target element index 238 | - `element`: target element view model element 239 | - `list`: target list 240 | - `component`: target VueComponent 241 | 242 | HTML: 243 | ```HTML 244 | 245 | ``` 246 | javascript: 247 | ```javascript 248 | checkMove: function(evt){ 249 | return (evt.draggedContext.element.name!=='apple'); 250 | } 251 | ``` 252 | See complete example: [Cancel.html](https://github.com/SortableJS/Vue.Draggable/blob/master/examples/Cancel.html), [cancel.js](https://github.com/SortableJS/Vue.Draggable/blob/master/examples/script/cancel.js) 253 | 254 | #### componentData 255 | Type: `Object`
256 | Required: `false`
257 | Default: `null`
258 | 259 | This props is used to pass additional information to child component declared by [tag props](#tag).
260 | Value: 261 | * `props`: props to be passed to the child component 262 | * `attrs`: attrs to be passed to the child component 263 | * `on`: events to be subscribe in the child component 264 | 265 | Example (using [element UI library](http://element.eleme.io/#/en-US)): 266 | ```HTML 267 | 268 | 269 |
{{e.description}}
270 |
271 |
272 | ``` 273 | ```javascript 274 | methods: { 275 | handleChange() { 276 | console.log('changed'); 277 | }, 278 | inputChanged(value) { 279 | this.activeNames = value; 280 | }, 281 | getComponentData() { 282 | return { 283 | on: { 284 | change: this.handleChange, 285 | input: this.inputChanged 286 | }, 287 | attrs:{ 288 | wrap: true 289 | }, 290 | props: { 291 | value: this.activeNames 292 | } 293 | }; 294 | } 295 | } 296 | ``` 297 | 298 | ### Events 299 | 300 | * Support for Sortable events: 301 | 302 | `start`, `add`, `remove`, `update`, `end`, `choose`, `unchoose`, `sort`, `filter`, `clone`
303 | Events are called whenever onStart, onAdd, onRemove, onUpdate, onEnd, onChoose, onUnchoose, onSort, onClone are fired by Sortable.js with the same argument.
304 | [See here for reference](https://github.com/RubaXa/Sortable#event-object-demo) 305 | 306 | Note that SortableJS OnMove callback is mapped with the [move prop](https://github.com/SortableJS/Vue.Draggable/blob/master/README.md#move) 307 | 308 | HTML: 309 | ```HTML 310 | 311 | ``` 312 | 313 | * change event 314 | 315 | `change` event is triggered when list prop is not null and the corresponding array is altered due to drag-and-drop operation.
316 | This event is called with one argument containing one of the following properties: 317 | - `added`: contains information of an element added to the array 318 | - `newIndex`: the index of the added element 319 | - `element`: the added element 320 | - `removed`: contains information of an element removed from to the array 321 | - `oldIndex`: the index of the element before remove 322 | - `element`: the removed element 323 | - `moved`: contains information of an element moved within the array 324 | - `newIndex`: the current index of the moved element 325 | - `oldIndex`: the old index of the moved element 326 | - `element`: the moved element 327 | 328 | ### Slots 329 | 330 | Limitation: neither header or footer slot works in conjunction with transition-group. 331 | 332 | #### Header 333 | Use the `header` slot to add none-draggable element inside the vuedraggable component. 334 | Important: it should be used in conjunction with draggable option to tag draggable element. 335 | Note that header slot will always be added before the default slot regardless its position in the template. 336 | Ex: 337 | 338 | ``` html 339 | 340 |
341 | {{element.name}} 342 |
343 | 344 |
345 | ``` 346 | 347 | #### Footer 348 | Use the `footer` slot to add none-draggable element inside the vuedraggable component. 349 | Important: it should be used in conjunction with draggable option to tag draggable elements. 350 | Note that footer slot will always be added after the default slot regardless its position in the template. 351 | Ex: 352 | 353 | ``` html 354 | 355 |
356 | {{element.name}} 357 |
358 | 359 |
360 | ``` 361 | ### Gotchas 362 | 363 | - Vue.draggable children should always map the list or value prop using a v-for directive 364 | * You may use [header](https://github.com/SortableJS/Vue.Draggable#header) and [footer](https://github.com/SortableJS/Vue.Draggable#footer) slot to by-pass this limitation. 365 | 366 | - Children elements inside v-for should be keyed as any element in Vue.js. Be carefull to provide revelant key values in particular: 367 | * typically providing array index as keys won't work as key should be linked to the items content 368 | * cloned elements should provide updated keys, it is doable using the [clone props](#clone) for example 369 | 370 | 371 | ### Example 372 | * [Clone](https://sortablejs.github.io/Vue.Draggable/#/custom-clone) 373 | * [Handle](https://sortablejs.github.io/Vue.Draggable/#/handle) 374 | * [Transition](https://sortablejs.github.io/Vue.Draggable/#/transition-example-2) 375 | * [Nested](https://sortablejs.github.io/Vue.Draggable/#/nested-example) 376 | * [Table](https://sortablejs.github.io/Vue.Draggable/#/table-example) 377 | 378 | ### Full demo example 379 | 380 | [draggable-example](https://github.com/David-Desmaisons/draggable-example) 381 | 382 | ## For Vue.js 1.0 383 | 384 | [See here](documentation/Vue.draggable.for.ReadME.md) 385 | 386 | ``` 387 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [["@vue/app", { 3 | useBuiltIns: "usage" 4 | }] 5 | ] 6 | }; 7 | -------------------------------------------------------------------------------- /dist/demo.html: -------------------------------------------------------------------------------- 1 | 2 | vuedraggable demo 3 | 4 | 5 | 6 | 7 | 8 | 11 | -------------------------------------------------------------------------------- /dist/vuedraggable.umd.min.js: -------------------------------------------------------------------------------- 1 | (function(t,e){"object"===typeof exports&&"object"===typeof module?module.exports=e(require("sortablejs")):"function"===typeof define&&define.amd?define(["sortablejs"],e):"object"===typeof exports?exports["vuedraggable"]=e(require("sortablejs")):t["vuedraggable"]=e(t["Sortable"])})("undefined"!==typeof self?self:this,(function(t){return function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"===typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t["default"]}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s="fb15")}({"01f9":function(t,e,n){"use strict";var r=n("2d00"),o=n("5ca1"),i=n("2aba"),c=n("32e9"),u=n("84f2"),a=n("41a0"),s=n("7f20"),f=n("38fd"),l=n("2b4c")("iterator"),d=!([].keys&&"next"in[].keys()),p="@@iterator",h="keys",v="values",g=function(){return this};t.exports=function(t,e,n,b,m,y,x){a(n,e,b);var O,S,w,j=function(t){if(!d&&t in _)return _[t];switch(t){case h:return function(){return new n(this,t)};case v:return function(){return new n(this,t)}}return function(){return new n(this,t)}},M=e+" Iterator",C=m==v,T=!1,_=t.prototype,L=_[l]||_[p]||m&&_[m],I=L||j(m),E=m?C?j("entries"):I:void 0,P="Array"==e&&_.entries||L;if(P&&(w=f(P.call(new t)),w!==Object.prototype&&w.next&&(s(w,M,!0),r||"function"==typeof w[l]||c(w,l,g))),C&&L&&L.name!==v&&(T=!0,I=function(){return L.call(this)}),r&&!x||!d&&!T&&_[l]||c(_,l,I),u[e]=I,u[M]=g,m)if(O={values:C?I:j(v),keys:y?I:j(h),entries:E},x)for(S in O)S in _||i(_,S,O[S]);else o(o.P+o.F*(d||T),e,O);return O}},"02f4":function(t,e,n){var r=n("4588"),o=n("be13");t.exports=function(t){return function(e,n){var i,c,u=String(o(e)),a=r(n),s=u.length;return a<0||a>=s?t?"":void 0:(i=u.charCodeAt(a),i<55296||i>56319||a+1===s||(c=u.charCodeAt(a+1))<56320||c>57343?t?u.charAt(a):i:t?u.slice(a,a+2):c-56320+(i-55296<<10)+65536)}}},"0390":function(t,e,n){"use strict";var r=n("02f4")(!0);t.exports=function(t,e,n){return e+(n?r(t,e).length:1)}},"0bfb":function(t,e,n){"use strict";var r=n("cb7c");t.exports=function(){var t=r(this),e="";return t.global&&(e+="g"),t.ignoreCase&&(e+="i"),t.multiline&&(e+="m"),t.unicode&&(e+="u"),t.sticky&&(e+="y"),e}},"0d58":function(t,e,n){var r=n("ce10"),o=n("e11e");t.exports=Object.keys||function(t){return r(t,o)}},1495:function(t,e,n){var r=n("86cc"),o=n("cb7c"),i=n("0d58");t.exports=n("9e1e")?Object.defineProperties:function(t,e){o(t);var n,c=i(e),u=c.length,a=0;while(u>a)r.f(t,n=c[a++],e[n]);return t}},"214f":function(t,e,n){"use strict";n("b0c5");var r=n("2aba"),o=n("32e9"),i=n("79e5"),c=n("be13"),u=n("2b4c"),a=n("520a"),s=u("species"),f=!i((function(){var t=/./;return t.exec=function(){var t=[];return t.groups={a:"7"},t},"7"!=="".replace(t,"$")})),l=function(){var t=/(?:)/,e=t.exec;t.exec=function(){return e.apply(this,arguments)};var n="ab".split(t);return 2===n.length&&"a"===n[0]&&"b"===n[1]}();t.exports=function(t,e,n){var d=u(t),p=!i((function(){var e={};return e[d]=function(){return 7},7!=""[t](e)})),h=p?!i((function(){var e=!1,n=/a/;return n.exec=function(){return e=!0,null},"split"===t&&(n.constructor={},n.constructor[s]=function(){return n}),n[d](""),!e})):void 0;if(!p||!h||"replace"===t&&!f||"split"===t&&!l){var v=/./[d],g=n(c,d,""[t],(function(t,e,n,r,o){return e.exec===a?p&&!o?{done:!0,value:v.call(e,n,r)}:{done:!0,value:t.call(n,e,r)}:{done:!1}})),b=g[0],m=g[1];r(String.prototype,t,b),o(RegExp.prototype,d,2==e?function(t,e){return m.call(t,this,e)}:function(t){return m.call(t,this)})}}},"230e":function(t,e,n){var r=n("d3f4"),o=n("7726").document,i=r(o)&&r(o.createElement);t.exports=function(t){return i?o.createElement(t):{}}},"23c6":function(t,e,n){var r=n("2d95"),o=n("2b4c")("toStringTag"),i="Arguments"==r(function(){return arguments}()),c=function(t,e){try{return t[e]}catch(n){}};t.exports=function(t){var e,n,u;return void 0===t?"Undefined":null===t?"Null":"string"==typeof(n=c(e=Object(t),o))?n:i?r(e):"Object"==(u=r(e))&&"function"==typeof e.callee?"Arguments":u}},2621:function(t,e){e.f=Object.getOwnPropertySymbols},"2aba":function(t,e,n){var r=n("7726"),o=n("32e9"),i=n("69a8"),c=n("ca5a")("src"),u=n("fa5b"),a="toString",s=(""+u).split(a);n("8378").inspectSource=function(t){return u.call(t)},(t.exports=function(t,e,n,u){var a="function"==typeof n;a&&(i(n,"name")||o(n,"name",e)),t[e]!==n&&(a&&(i(n,c)||o(n,c,t[e]?""+t[e]:s.join(String(e)))),t===r?t[e]=n:u?t[e]?t[e]=n:o(t,e,n):(delete t[e],o(t,e,n)))})(Function.prototype,a,(function(){return"function"==typeof this&&this[c]||u.call(this)}))},"2aeb":function(t,e,n){var r=n("cb7c"),o=n("1495"),i=n("e11e"),c=n("613b")("IE_PROTO"),u=function(){},a="prototype",s=function(){var t,e=n("230e")("iframe"),r=i.length,o="<",c=">";e.style.display="none",n("fab2").appendChild(e),e.src="javascript:",t=e.contentWindow.document,t.open(),t.write(o+"script"+c+"document.F=Object"+o+"/script"+c),t.close(),s=t.F;while(r--)delete s[a][i[r]];return s()};t.exports=Object.create||function(t,e){var n;return null!==t?(u[a]=r(t),n=new u,u[a]=null,n[c]=t):n=s(),void 0===e?n:o(n,e)}},"2b4c":function(t,e,n){var r=n("5537")("wks"),o=n("ca5a"),i=n("7726").Symbol,c="function"==typeof i,u=t.exports=function(t){return r[t]||(r[t]=c&&i[t]||(c?i:o)("Symbol."+t))};u.store=r},"2d00":function(t,e){t.exports=!1},"2d95":function(t,e){var n={}.toString;t.exports=function(t){return n.call(t).slice(8,-1)}},"2fdb":function(t,e,n){"use strict";var r=n("5ca1"),o=n("d2c8"),i="includes";r(r.P+r.F*n("5147")(i),"String",{includes:function(t){return!!~o(this,t,i).indexOf(t,arguments.length>1?arguments[1]:void 0)}})},"32e9":function(t,e,n){var r=n("86cc"),o=n("4630");t.exports=n("9e1e")?function(t,e,n){return r.f(t,e,o(1,n))}:function(t,e,n){return t[e]=n,t}},"38fd":function(t,e,n){var r=n("69a8"),o=n("4bf8"),i=n("613b")("IE_PROTO"),c=Object.prototype;t.exports=Object.getPrototypeOf||function(t){return t=o(t),r(t,i)?t[i]:"function"==typeof t.constructor&&t instanceof t.constructor?t.constructor.prototype:t instanceof Object?c:null}},"41a0":function(t,e,n){"use strict";var r=n("2aeb"),o=n("4630"),i=n("7f20"),c={};n("32e9")(c,n("2b4c")("iterator"),(function(){return this})),t.exports=function(t,e,n){t.prototype=r(c,{next:o(1,n)}),i(t,e+" Iterator")}},"456d":function(t,e,n){var r=n("4bf8"),o=n("0d58");n("5eda")("keys",(function(){return function(t){return o(r(t))}}))},4588:function(t,e){var n=Math.ceil,r=Math.floor;t.exports=function(t){return isNaN(t=+t)?0:(t>0?r:n)(t)}},4630:function(t,e){t.exports=function(t,e){return{enumerable:!(1&t),configurable:!(2&t),writable:!(4&t),value:e}}},"4bf8":function(t,e,n){var r=n("be13");t.exports=function(t){return Object(r(t))}},5147:function(t,e,n){var r=n("2b4c")("match");t.exports=function(t){var e=/./;try{"/./"[t](e)}catch(n){try{return e[r]=!1,!"/./"[t](e)}catch(o){}}return!0}},"520a":function(t,e,n){"use strict";var r=n("0bfb"),o=RegExp.prototype.exec,i=String.prototype.replace,c=o,u="lastIndex",a=function(){var t=/a/,e=/b*/g;return o.call(t,"a"),o.call(e,"a"),0!==t[u]||0!==e[u]}(),s=void 0!==/()??/.exec("")[1],f=a||s;f&&(c=function(t){var e,n,c,f,l=this;return s&&(n=new RegExp("^"+l.source+"$(?!\\s)",r.call(l))),a&&(e=l[u]),c=o.call(l,t),a&&c&&(l[u]=l.global?c.index+c[0].length:e),s&&c&&c.length>1&&i.call(c[0],n,(function(){for(f=1;f1?arguments[1]:void 0)}}),n("9c6c")("includes")},6821:function(t,e,n){var r=n("626a"),o=n("be13");t.exports=function(t){return r(o(t))}},"69a8":function(t,e){var n={}.hasOwnProperty;t.exports=function(t,e){return n.call(t,e)}},"6a99":function(t,e,n){var r=n("d3f4");t.exports=function(t,e){if(!r(t))return t;var n,o;if(e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;if("function"==typeof(n=t.valueOf)&&!r(o=n.call(t)))return o;if(!e&&"function"==typeof(n=t.toString)&&!r(o=n.call(t)))return o;throw TypeError("Can't convert object to primitive value")}},7333:function(t,e,n){"use strict";var r=n("0d58"),o=n("2621"),i=n("52a7"),c=n("4bf8"),u=n("626a"),a=Object.assign;t.exports=!a||n("79e5")((function(){var t={},e={},n=Symbol(),r="abcdefghijklmnopqrst";return t[n]=7,r.split("").forEach((function(t){e[t]=t})),7!=a({},t)[n]||Object.keys(a({},e)).join("")!=r}))?function(t,e){var n=c(t),a=arguments.length,s=1,f=o.f,l=i.f;while(a>s){var d,p=u(arguments[s++]),h=f?r(p).concat(f(p)):r(p),v=h.length,g=0;while(v>g)l.call(p,d=h[g++])&&(n[d]=p[d])}return n}:a},7726:function(t,e){var n=t.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=n)},"77f1":function(t,e,n){var r=n("4588"),o=Math.max,i=Math.min;t.exports=function(t,e){return t=r(t),t<0?o(t+e,0):i(t,e)}},"79e5":function(t,e){t.exports=function(t){try{return!!t()}catch(e){return!0}}},"7f20":function(t,e,n){var r=n("86cc").f,o=n("69a8"),i=n("2b4c")("toStringTag");t.exports=function(t,e,n){t&&!o(t=n?t:t.prototype,i)&&r(t,i,{configurable:!0,value:e})}},8378:function(t,e){var n=t.exports={version:"2.6.5"};"number"==typeof __e&&(__e=n)},"84f2":function(t,e){t.exports={}},"86cc":function(t,e,n){var r=n("cb7c"),o=n("c69a"),i=n("6a99"),c=Object.defineProperty;e.f=n("9e1e")?Object.defineProperty:function(t,e,n){if(r(t),e=i(e,!0),r(n),o)try{return c(t,e,n)}catch(u){}if("get"in n||"set"in n)throw TypeError("Accessors not supported!");return"value"in n&&(t[e]=n.value),t}},"9b43":function(t,e,n){var r=n("d8e8");t.exports=function(t,e,n){if(r(t),void 0===e)return t;switch(n){case 1:return function(n){return t.call(e,n)};case 2:return function(n,r){return t.call(e,n,r)};case 3:return function(n,r,o){return t.call(e,n,r,o)}}return function(){return t.apply(e,arguments)}}},"9c6c":function(t,e,n){var r=n("2b4c")("unscopables"),o=Array.prototype;void 0==o[r]&&n("32e9")(o,r,{}),t.exports=function(t){o[r][t]=!0}},"9def":function(t,e,n){var r=n("4588"),o=Math.min;t.exports=function(t){return t>0?o(r(t),9007199254740991):0}},"9e1e":function(t,e,n){t.exports=!n("79e5")((function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a}))},a352:function(e,n){e.exports=t},a481:function(t,e,n){"use strict";var r=n("cb7c"),o=n("4bf8"),i=n("9def"),c=n("4588"),u=n("0390"),a=n("5f1b"),s=Math.max,f=Math.min,l=Math.floor,d=/\$([$&`']|\d\d?|<[^>]*>)/g,p=/\$([$&`']|\d\d?)/g,h=function(t){return void 0===t?t:String(t)};n("214f")("replace",2,(function(t,e,n,v){return[function(r,o){var i=t(this),c=void 0==r?void 0:r[e];return void 0!==c?c.call(r,i,o):n.call(String(i),r,o)},function(t,e){var o=v(n,t,this,e);if(o.done)return o.value;var l=r(t),d=String(this),p="function"===typeof e;p||(e=String(e));var b=l.global;if(b){var m=l.unicode;l.lastIndex=0}var y=[];while(1){var x=a(l,d);if(null===x)break;if(y.push(x),!b)break;var O=String(x[0]);""===O&&(l.lastIndex=u(d,i(l.lastIndex),m))}for(var S="",w=0,j=0;j=w&&(S+=d.slice(w,C)+E,w=C+M.length)}return S+d.slice(w)}];function g(t,e,r,i,c,u){var a=r+t.length,s=i.length,f=p;return void 0!==c&&(c=o(c),f=d),n.call(u,f,(function(n,o){var u;switch(o.charAt(0)){case"$":return"$";case"&":return t;case"`":return e.slice(0,r);case"'":return e.slice(a);case"<":u=c[o.slice(1,-1)];break;default:var f=+o;if(0===f)return n;if(f>s){var d=l(f/10);return 0===d?n:d<=s?void 0===i[d-1]?o.charAt(1):i[d-1]+o.charAt(1):n}u=i[f-1]}return void 0===u?"":u}))}}))},aae3:function(t,e,n){var r=n("d3f4"),o=n("2d95"),i=n("2b4c")("match");t.exports=function(t){var e;return r(t)&&(void 0!==(e=t[i])?!!e:"RegExp"==o(t))}},ac6a:function(t,e,n){for(var r=n("cadf"),o=n("0d58"),i=n("2aba"),c=n("7726"),u=n("32e9"),a=n("84f2"),s=n("2b4c"),f=s("iterator"),l=s("toStringTag"),d=a.Array,p={CSSRuleList:!0,CSSStyleDeclaration:!1,CSSValueList:!1,ClientRectList:!1,DOMRectList:!1,DOMStringList:!1,DOMTokenList:!0,DataTransferItemList:!1,FileList:!1,HTMLAllCollection:!1,HTMLCollection:!1,HTMLFormElement:!1,HTMLSelectElement:!1,MediaList:!0,MimeTypeArray:!1,NamedNodeMap:!1,NodeList:!0,PaintRequestList:!1,Plugin:!1,PluginArray:!1,SVGLengthList:!1,SVGNumberList:!1,SVGPathSegList:!1,SVGPointList:!1,SVGStringList:!1,SVGTransformList:!1,SourceBufferList:!1,StyleSheetList:!0,TextTrackCueList:!1,TextTrackList:!1,TouchList:!1},h=o(p),v=0;vf)if(u=a[f++],u!=u)return!0}else for(;s>f;f++)if((t||f in a)&&a[f]===n)return t||f||0;return!t&&-1}}},c649:function(t,e,n){"use strict";(function(t){n.d(e,"c",(function(){return s})),n.d(e,"a",(function(){return u})),n.d(e,"b",(function(){return o})),n.d(e,"d",(function(){return a}));n("a481");function r(){return"undefined"!==typeof window?window.console:t.console}var o=r();function i(t){var e=Object.create(null);return function(n){var r=e[n];return r||(e[n]=t(n))}}var c=/-(\w)/g,u=i((function(t){return t.replace(c,(function(t,e){return e?e.toUpperCase():""}))}));function a(t){null!==t.parentElement&&t.parentElement.removeChild(t)}function s(t,e,n){var r=0===n?t.children[0]:t.children[n-1].nextSibling;t.insertBefore(e,r)}}).call(this,n("c8ba"))},c69a:function(t,e,n){t.exports=!n("9e1e")&&!n("79e5")((function(){return 7!=Object.defineProperty(n("230e")("div"),"a",{get:function(){return 7}}).a}))},c8ba:function(t,e){var n;n=function(){return this}();try{n=n||new Function("return this")()}catch(r){"object"===typeof window&&(n=window)}t.exports=n},ca5a:function(t,e){var n=0,r=Math.random();t.exports=function(t){return"Symbol(".concat(void 0===t?"":t,")_",(++n+r).toString(36))}},cadf:function(t,e,n){"use strict";var r=n("9c6c"),o=n("d53b"),i=n("84f2"),c=n("6821");t.exports=n("01f9")(Array,"Array",(function(t,e){this._t=c(t),this._i=0,this._k=e}),(function(){var t=this._t,e=this._k,n=this._i++;return!t||n>=t.length?(this._t=void 0,o(1)):o(0,"keys"==e?n:"values"==e?t[n]:[n,t[n]])}),"values"),i.Arguments=i.Array,r("keys"),r("values"),r("entries")},cb7c:function(t,e,n){var r=n("d3f4");t.exports=function(t){if(!r(t))throw TypeError(t+" is not an object!");return t}},ce10:function(t,e,n){var r=n("69a8"),o=n("6821"),i=n("c366")(!1),c=n("613b")("IE_PROTO");t.exports=function(t,e){var n,u=o(t),a=0,s=[];for(n in u)n!=c&&r(u,n)&&s.push(n);while(e.length>a)r(u,n=e[a++])&&(~i(s,n)||s.push(n));return s}},d2c8:function(t,e,n){var r=n("aae3"),o=n("be13");t.exports=function(t,e,n){if(r(e))throw TypeError("String#"+n+" doesn't accept regex!");return String(o(t))}},d3f4:function(t,e){t.exports=function(t){return"object"===typeof t?null!==t:"function"===typeof t}},d53b:function(t,e){t.exports=function(t,e){return{value:e,done:!!t}}},d8e8:function(t,e){t.exports=function(t){if("function"!=typeof t)throw TypeError(t+" is not a function!");return t}},e11e:function(t,e){t.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},f559:function(t,e,n){"use strict";var r=n("5ca1"),o=n("9def"),i=n("d2c8"),c="startsWith",u=""[c];r(r.P+r.F*n("5147")(c),"String",{startsWith:function(t){var e=i(this,t,c),n=o(Math.min(arguments.length>1?arguments[1]:void 0,e.length)),r=String(t);return u?u.call(e,r,n):e.slice(n,n+r.length)===r}})},f6fd:function(t,e){(function(t){var e="currentScript",n=t.getElementsByTagName("script");e in t||Object.defineProperty(t,e,{get:function(){try{throw new Error}catch(r){var t,e=(/.*at [^\(]*\((.*):.+:.+\)$/gi.exec(r.stack)||[!1])[1];for(t in n)if(n[t].src==e||"interactive"==n[t].readyState)return n[t];return null}}})})(document)},f751:function(t,e,n){var r=n("5ca1");r(r.S+r.F,"Object",{assign:n("7333")})},fa5b:function(t,e,n){t.exports=n("5537")("native-function-to-string",Function.toString)},fab2:function(t,e,n){var r=n("7726").document;t.exports=r&&r.documentElement},fb15:function(t,e,n){"use strict";var r;(n.r(e),"undefined"!==typeof window)&&(n("f6fd"),(r=window.document.currentScript)&&(r=r.src.match(/(.+\/)[^/]+\.js(\?.*)?$/))&&(n.p=r[1]));n("f751"),n("f559"),n("ac6a"),n("cadf"),n("456d");function o(t){if(Array.isArray(t))return t}function i(t,e){if("undefined"!==typeof Symbol&&Symbol.iterator in Object(t)){var n=[],r=!0,o=!1,i=void 0;try{for(var c,u=t[Symbol.iterator]();!(r=(c=u.next()).done);r=!0)if(n.push(c.value),e&&n.length===e)break}catch(a){o=!0,i=a}finally{try{r||null==u["return"]||u["return"]()}finally{if(o)throw i}}return n}}function c(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,r=new Array(e);n=i?o.length:o.indexOf(t)}));return n?c.filter((function(t){return-1!==t})):c}function x(t,e){var n=this;this.$nextTick((function(){return n.$emit(t.toLowerCase(),e)}))}function O(t){var e=this;return function(n){null!==e.realList&&e["onDrag"+t](n),x.call(e,t,n)}}function S(t){return["transition-group","TransitionGroup"].includes(t)}function w(t){if(!t||1!==t.length)return!1;var e=s(t,1),n=e[0].componentOptions;return!!n&&S(n.tag)}function j(t,e,n){return t[n]||(e[n]?e[n]():void 0)}function M(t,e,n){var r=0,o=0,i=j(e,n,"header");i&&(r=i.length,t=t?[].concat(p(i),p(t)):p(i));var c=j(e,n,"footer");return c&&(o=c.length,t=t?[].concat(p(t),p(c)):p(c)),{children:t,headerOffset:r,footerOffset:o}}function C(t,e){var n=null,r=function(t,e){n=b(n,t,e)},o=Object.keys(t).filter((function(t){return"id"===t||t.startsWith("data-")})).reduce((function(e,n){return e[n]=t[n],e}),{});if(r("attrs",o),!e)return n;var i=e.on,c=e.props,u=e.attrs;return r("on",i),r("props",c),Object.assign(n.attrs,u),n}var T=["Start","Add","Remove","Update","End"],_=["Choose","Unchoose","Sort","Filter","Clone"],L=["Move"].concat(T,_).map((function(t){return"on"+t})),I=null,E={options:Object,list:{type:Array,required:!1,default:null},value:{type:Array,required:!1,default:null},noTransitionOnDrag:{type:Boolean,default:!1},clone:{type:Function,default:function(t){return t}},element:{type:String,default:"div"},tag:{type:String,default:null},move:{type:Function,default:null},componentData:{type:Object,required:!1,default:null}},P={name:"draggable",inheritAttrs:!1,props:E,data:function(){return{transitionMode:!1,noneFunctionalComponentMode:!1}},render:function(t){var e=this.$slots.default;this.transitionMode=w(e);var n=M(e,this.$slots,this.$scopedSlots),r=n.children,o=n.headerOffset,i=n.footerOffset;this.headerOffset=o,this.footerOffset=i;var c=C(this.$attrs,this.componentData);return t(this.getTag(),c,r)},created:function(){null!==this.list&&null!==this.value&&g["b"].error("Value and list props are mutually exclusive! Please set one or another."),"div"!==this.element&&g["b"].warn("Element props is deprecated please use tag props instead. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#element-props"),void 0!==this.options&&g["b"].warn("Options props is deprecated, add sortable options directly as vue.draggable item, or use v-bind. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#options-props")},mounted:function(){var t=this;if(this.noneFunctionalComponentMode=this.getTag().toLowerCase()!==this.$el.nodeName.toLowerCase()&&!this.getIsFunctional(),this.noneFunctionalComponentMode&&this.transitionMode)throw new Error("Transition-group inside component is not supported. Please alter tag value or remove transition-group. Current tag value: ".concat(this.getTag()));var e={};T.forEach((function(n){e["on"+n]=O.call(t,n)})),_.forEach((function(n){e["on"+n]=x.bind(t,n)}));var n=Object.keys(this.$attrs).reduce((function(e,n){return e[Object(g["a"])(n)]=t.$attrs[n],e}),{}),r=Object.assign({},this.options,n,e,{onMove:function(e,n){return t.onDragMove(e,n)}});!("draggable"in r)&&(r.draggable=">*"),this._sortable=new v.a(this.rootContainer,r),this.computeIndexes()},beforeDestroy:function(){void 0!==this._sortable&&this._sortable.destroy()},computed:{rootContainer:function(){return this.transitionMode?this.$el.children[0]:this.$el},realList:function(){return this.list?this.list:this.value}},watch:{options:{handler:function(t){this.updateOptions(t)},deep:!0},$attrs:{handler:function(t){this.updateOptions(t)},deep:!0},realList:function(){this.computeIndexes()}},methods:{getIsFunctional:function(){var t=this._vnode.fnOptions;return t&&t.functional},getTag:function(){return this.tag||this.element},updateOptions:function(t){for(var e in t){var n=Object(g["a"])(e);-1===L.indexOf(n)&&this._sortable.option(n,t[e])}},getChildrenNodes:function(){if(this.noneFunctionalComponentMode)return this.$children[0].$slots.default;var t=this.$slots.default;return this.transitionMode?t[0].child.$slots.default:t},computeIndexes:function(){var t=this;this.$nextTick((function(){t.visibleIndexes=y(t.getChildrenNodes(),t.rootContainer.children,t.transitionMode,t.footerOffset)}))},getUnderlyingVm:function(t){var e=m(this.getChildrenNodes()||[],t);if(-1===e)return null;var n=this.realList[e];return{index:e,element:n}},getUnderlyingPotencialDraggableComponent:function(t){var e=t.__vue__;return e&&e.$options&&S(e.$options._componentTag)?e.$parent:!("realList"in e)&&1===e.$children.length&&"realList"in e.$children[0]?e.$children[0]:e},emitChanges:function(t){var e=this;this.$nextTick((function(){e.$emit("change",t)}))},alterList:function(t){if(this.list)t(this.list);else{var e=p(this.value);t(e),this.$emit("input",e)}},spliceList:function(){var t=arguments,e=function(e){return e.splice.apply(e,p(t))};this.alterList(e)},updatePosition:function(t,e){var n=function(n){return n.splice(e,0,n.splice(t,1)[0])};this.alterList(n)},getRelatedContextFromMoveEvent:function(t){var e=t.to,n=t.related,r=this.getUnderlyingPotencialDraggableComponent(e);if(!r)return{component:r};var o=r.realList,i={list:o,component:r};if(e!==n&&o&&r.getUnderlyingVm){var c=r.getUnderlyingVm(n);if(c)return Object.assign(c,i)}return i},getVmIndex:function(t){var e=this.visibleIndexes,n=e.length;return t>n-1?n:e[t]},getComponent:function(){return this.$slots.default[0].componentInstance},resetTransitionData:function(t){if(this.noTransitionOnDrag&&this.transitionMode){var e=this.getChildrenNodes();e[t].data=null;var n=this.getComponent();n.children=[],n.kept=void 0}},onDragStart:function(t){this.context=this.getUnderlyingVm(t.item),t.item._underlying_vm_=this.clone(this.context.element),I=t.item},onDragAdd:function(t){var e=t.item._underlying_vm_;if(void 0!==e){Object(g["d"])(t.item);var n=this.getVmIndex(t.newIndex);this.spliceList(n,0,e),this.computeIndexes();var r={element:e,newIndex:n};this.emitChanges({added:r})}},onDragRemove:function(t){if(Object(g["c"])(this.rootContainer,t.item,t.oldIndex),"clone"!==t.pullMode){var e=this.context.index;this.spliceList(e,1);var n={element:this.context.element,oldIndex:e};this.resetTransitionData(e),this.emitChanges({removed:n})}else Object(g["d"])(t.clone)},onDragUpdate:function(t){Object(g["d"])(t.item),Object(g["c"])(t.from,t.item,t.oldIndex);var e=this.context.index,n=this.getVmIndex(t.newIndex);this.updatePosition(e,n);var r={element:this.context.element,oldIndex:e,newIndex:n};this.emitChanges({moved:r})},updateProperty:function(t,e){t.hasOwnProperty(e)&&(t[e]+=this.headerOffset)},computeFutureIndex:function(t,e){if(!t.element)return 0;var n=p(e.to.children).filter((function(t){return"none"!==t.style["display"]})),r=n.indexOf(e.related),o=t.component.getVmIndex(r),i=-1!==n.indexOf(I);return i||!e.willInsertAfter?o:o+1},onDragMove:function(t,e){var n=this.move;if(!n||!this.realList)return!0;var r=this.getRelatedContextFromMoveEvent(t),o=this.context,i=this.computeFutureIndex(r,t);Object.assign(o,{futureIndex:i});var c=Object.assign({},t,{relatedContext:r,draggedContext:o});return n(c,e)},onDragEnd:function(){this.computeIndexes(),I=null}}};"undefined"!==typeof window&&"Vue"in window&&window.Vue.component("draggable",P);var $=P;e["default"]=$}})["default"]})); 2 | //# sourceMappingURL=vuedraggable.umd.min.js.map -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SortableJS/Vue.Draggable/431db153bfdfe09e31f622f01e9b3220b77e6b56/docs/favicon.ico -------------------------------------------------------------------------------- /docs/fonts/element-icons.2fad952a.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SortableJS/Vue.Draggable/431db153bfdfe09e31f622f01e9b3220b77e6b56/docs/fonts/element-icons.2fad952a.woff -------------------------------------------------------------------------------- /docs/fonts/element-icons.6f0a7632.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SortableJS/Vue.Draggable/431db153bfdfe09e31f622f01e9b3220b77e6b56/docs/fonts/element-icons.6f0a7632.ttf -------------------------------------------------------------------------------- /docs/fonts/fontawesome-webfont.674f50d2.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SortableJS/Vue.Draggable/431db153bfdfe09e31f622f01e9b3220b77e6b56/docs/fonts/fontawesome-webfont.674f50d2.eot -------------------------------------------------------------------------------- /docs/fonts/fontawesome-webfont.af7ae505.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SortableJS/Vue.Draggable/431db153bfdfe09e31f622f01e9b3220b77e6b56/docs/fonts/fontawesome-webfont.af7ae505.woff2 -------------------------------------------------------------------------------- /docs/fonts/fontawesome-webfont.b06871f2.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SortableJS/Vue.Draggable/431db153bfdfe09e31f622f01e9b3220b77e6b56/docs/fonts/fontawesome-webfont.b06871f2.ttf -------------------------------------------------------------------------------- /docs/fonts/fontawesome-webfont.fee66e71.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SortableJS/Vue.Draggable/431db153bfdfe09e31f622f01e9b3220b77e6b56/docs/fonts/fontawesome-webfont.fee66e71.woff -------------------------------------------------------------------------------- /docs/img/logo.c6a3753c.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vuedraggable 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /documentation/Vue.draggable.for.ReadME.md: -------------------------------------------------------------------------------- 1 | ## For Vue.js 1.0 2 | 3 | Use it exactly as v-for directive, passing optional parameters using 'options' parameter. 4 | Options parameter can be json string or a full javascript object. 5 | 6 | ``` html 7 |
8 |

{{element.name}}

9 |
10 | ``` 11 | 12 | ### Limitation 13 | 14 | * This directive works only when applied to arrays and not to objects. 15 | * `onStart`, `onUpdate`, `onAdd`, `onRemove` Sortable.js options hooks are used by v-dragable-for to update VM. As such these four options are not usable with v-dragable-for. If you need to listen to re-order events, you can watch the underlying view model collection. For example: 16 | ``` js 17 | watch: { 18 | 'list1': function () { 19 | console.log('Collection updated!'); 20 | }, 21 | ``` 22 | ### fiddle 23 | Simple: 24 | https://jsfiddle.net/dede89/j62g58z7/ 25 | 26 | Two Lists: 27 | https://jsfiddle.net/dede89/hqxranrd/ 28 | 29 | Example with list clone: 30 | https://jsfiddle.net/dede89/u5ecgtsj/ 31 | 32 | ## Installation 33 | - Available through: 34 | ``` js 35 | npm install vuedraggable 36 | ``` 37 | ``` js 38 | Bower install vue.draggable 39 | ``` 40 | 41 | Version 1.0.9 is Vue.js 1.0 compatible
42 | 43 | - #### For Modules 44 | 45 | ``` js 46 | // ES6 47 | //For Vue.js 1.0 only 48 | import VueDraggable from 'vuedraggable' 49 | import Vue from 'vue' 50 | Vue.use(VueDraggable) 51 | 52 | // ES5 53 | //For Vue.js 1.0 54 | var Vue = require('vue') 55 | Vue.use(require('vuedraggable')) 56 | ``` 57 | 58 | - #### For ` 183 | 184 | 298 | -------------------------------------------------------------------------------- /example/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/components/clone-on-control.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 72 | 73 | -------------------------------------------------------------------------------- /example/components/clone.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 76 | 77 | -------------------------------------------------------------------------------- /example/components/custom-clone.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 76 | 77 | -------------------------------------------------------------------------------- /example/components/footerslot.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | 77 | 86 | -------------------------------------------------------------------------------- /example/components/functional.vue: -------------------------------------------------------------------------------- 1 | 42 | 43 | 86 | 101 | -------------------------------------------------------------------------------- /example/components/handle.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 68 | 93 | -------------------------------------------------------------------------------- /example/components/headerslot.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 67 | 68 | -------------------------------------------------------------------------------- /example/components/infra/nested.vue: -------------------------------------------------------------------------------- 1 | 9 | 25 | 31 | -------------------------------------------------------------------------------- /example/components/infra/raw-displayer.vue: -------------------------------------------------------------------------------- 1 | 7 | 27 | 32 | -------------------------------------------------------------------------------- /example/components/nested-example.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 51 | 52 | -------------------------------------------------------------------------------- /example/components/nested-with-vmodel.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 33 | -------------------------------------------------------------------------------- /example/components/nested/nested-store.js: -------------------------------------------------------------------------------- 1 | import Vuex from "vuex"; 2 | import Vue from "vue"; 3 | 4 | Vue.use(Vuex); 5 | 6 | export const nested = { 7 | namespaced: true, 8 | state: { 9 | elements: [ 10 | { 11 | id: 1, 12 | name: "Shrek", 13 | elements: [] 14 | }, 15 | { 16 | id: 2, 17 | name: "Fiona", 18 | elements: [ 19 | { 20 | id: 4, 21 | name: "Lord Farquad", 22 | elements: [] 23 | }, 24 | { 25 | id: 5, 26 | name: "Prince Charming", 27 | elements: [] 28 | } 29 | ] 30 | }, 31 | { 32 | id: 3, 33 | name: "Donkey", 34 | elements: [] 35 | } 36 | ] 37 | }, 38 | mutations: { 39 | updateElements: (state, payload) => { 40 | state.elements = payload; 41 | } 42 | }, 43 | actions: { 44 | updateElements: ({ commit }, payload) => { 45 | commit("updateElements", payload); 46 | } 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /example/components/nested/nested-test.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 32 | 33 | 74 | -------------------------------------------------------------------------------- /example/components/simple.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 91 | 101 | -------------------------------------------------------------------------------- /example/components/table-column-example.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 51 | 56 | -------------------------------------------------------------------------------- /example/components/table-example.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 50 | 55 | -------------------------------------------------------------------------------- /example/components/third-party.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 99 | 100 | -------------------------------------------------------------------------------- /example/components/transition-example-2.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 88 | 89 | 119 | -------------------------------------------------------------------------------- /example/components/transition-example.vue: -------------------------------------------------------------------------------- 1 | 41 | 42 | 86 | 87 | 117 | -------------------------------------------------------------------------------- /example/components/two-list-headerslots.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 100 | 101 | -------------------------------------------------------------------------------- /example/components/two-lists.vue: -------------------------------------------------------------------------------- 1 | 34 | 77 | -------------------------------------------------------------------------------- /example/debug-components/future-index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 68 | 78 | -------------------------------------------------------------------------------- /example/debug-components/nested/draggable-list.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 58 | 64 | -------------------------------------------------------------------------------- /example/debug-components/slot-example.vue: -------------------------------------------------------------------------------- 1 | 51 | 52 | 87 | 97 | -------------------------------------------------------------------------------- /example/main.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | import App from "./App.vue"; 3 | import VueRouter from "vue-router"; 4 | import routes from "./route"; 5 | import rawDisplayer from "./components/infra/raw-displayer.vue"; 6 | import ElementUI from "element-ui"; 7 | import store from "./store"; 8 | import "bootstrap/dist/css/bootstrap.min.css"; 9 | import "font-awesome/css/font-awesome.css"; 10 | 11 | require("bootstrap"); 12 | 13 | const router = new VueRouter({ 14 | routes 15 | }); 16 | 17 | Vue.config.productionTip = false; 18 | Vue.use(VueRouter); 19 | Vue.component("rawDisplayer", rawDisplayer); 20 | Vue.use(ElementUI); 21 | 22 | new Vue({ 23 | store, 24 | router, 25 | render: h => h(App) 26 | }).$mount("#app"); 27 | -------------------------------------------------------------------------------- /example/route.js: -------------------------------------------------------------------------------- 1 | const ctx = require.context("./components/", false, /\.vue$/); 2 | 3 | const routes = ctx.keys().map(key => ({ 4 | path: key 5 | })); 6 | 7 | routes.push({ 8 | path: "/", 9 | redirect: "./simple" 10 | }); 11 | export default routes; 12 | -------------------------------------------------------------------------------- /example/store.js: -------------------------------------------------------------------------------- 1 | import { nested } from "./components/nested/nested-store"; 2 | import Vuex from "vuex"; 3 | import Vue from "vue"; 4 | 5 | Vue.use(Vuex); 6 | 7 | export default new Vuex.Store({ 8 | namespaced: true, 9 | modules: { 10 | nested 11 | } 12 | }); 13 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleFileExtensions: [ 3 | "js", 4 | "jsx", 5 | "json", 6 | "vue" 7 | ], 8 | transform: { 9 | "^.+\\.vue$": "vue-jest", 10 | ".+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$": "jest-transform-stub", 11 | "^.+\\.jsx?$": "babel-jest" 12 | }, 13 | moduleNameMapper: { 14 | "^@/(.*)$": "/src/$1" 15 | }, 16 | snapshotSerializers: [ 17 | "jest-serializer-vue" 18 | ], 19 | testMatch: [ 20 | "**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)" 21 | ], 22 | testURL: "http://localhost/", 23 | collectCoverageFrom: [ 24 | "/src/vuedraggable.js", 25 | "/src/util/helper.js" 26 | ], 27 | // testEnvironment: "node", 28 | transformIgnorePatterns: [ 29 | "node_modules/(?!(babel-jest|jest-vue-preprocessor)/)" 30 | ], 31 | }; -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SortableJS/Vue.Draggable/431db153bfdfe09e31f622f01e9b3220b77e6b56/logo.png -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuedraggable", 3 | "version": "2.24.3", 4 | "description": "draggable component for vue", 5 | "license": "MIT", 6 | "main": "dist/vuedraggable.umd.min.js", 7 | "types": "src/vuedraggable.d.ts", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/SortableJS/Vue.Draggable.git" 11 | }, 12 | "private": false, 13 | "scripts": { 14 | "serve": "vue-cli-service serve ./example/main.js --open --mode local", 15 | "build:doc": "vue-cli-service build ./example/main.js --dest docs --mode development", 16 | "build": "vue-cli-service build --name vuedraggable --entry ./src/vuedraggable.js --target lib", 17 | "lint": "vue-cli-service lint src example", 18 | "prepublishOnly": "npm run lint && npm run test:unit && npm run build:doc && npm run build", 19 | "test:unit": "vue-cli-service test:unit --coverage", 20 | "test:coverage": "vue-cli-service test:unit --coverage --verbose && codecov" 21 | }, 22 | "keywords": [ 23 | "vue", 24 | "vuejs", 25 | "drag", 26 | "and", 27 | "drop", 28 | "list", 29 | "Sortable.js", 30 | "component", 31 | "nested" 32 | ], 33 | "dependencies": { 34 | "sortablejs": "1.10.2" 35 | }, 36 | "devDependencies": { 37 | "@vue/cli-plugin-babel": "^3.11.0", 38 | "@vue/cli-plugin-eslint": "^3.11.0", 39 | "@vue/cli-plugin-unit-jest": "^3.11.0", 40 | "@vue/cli-service": "^3.11.0", 41 | "@vue/eslint-config-prettier": "^4.0.1", 42 | "@vue/test-utils": "^1.1.0", 43 | "babel-core": "7.0.0-bridge.0", 44 | "babel-eslint": "^10.0.1", 45 | "babel-jest": "^23.6.0", 46 | "bootstrap": "^4.3.1", 47 | "codecov": "^3.2.0", 48 | "component-fixture": "^0.4.1", 49 | "element-ui": "^2.5.4", 50 | "eslint": "^5.8.0", 51 | "eslint-plugin-vue": "^5.0.0", 52 | "font-awesome": "^4.7.0", 53 | "jquery": "^3.5.1", 54 | "vue": "^2.6.12", 55 | "vue-cli-plugin-component": "^1.10.5", 56 | "vue-router": "^3.0.2", 57 | "vue-server-renderer": "^2.6.12", 58 | "vue-template-compiler": "^2.6.12", 59 | "vuetify": "^1.5.16", 60 | "vuex": "^3.1.1" 61 | }, 62 | "eslintConfig": { 63 | "root": true, 64 | "env": { 65 | "node": true 66 | }, 67 | "extends": [ 68 | "plugin:vue/essential", 69 | "@vue/prettier" 70 | ], 71 | "rules": {}, 72 | "parserOptions": { 73 | "parser": "babel-eslint" 74 | } 75 | }, 76 | "postcss": { 77 | "plugins": { 78 | "autoprefixer": {} 79 | } 80 | }, 81 | "browserslist": [ 82 | "> 1%", 83 | "last 2 versions", 84 | "not ie <= 8" 85 | ], 86 | "files": [ 87 | "dist/*.css", 88 | "dist/*.map", 89 | "dist/*.js", 90 | "src/*" 91 | ], 92 | "module": "dist/vuedraggable.umd.js" 93 | } 94 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SortableJS/Vue.Draggable/431db153bfdfe09e31f622f01e9b3220b77e6b56/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | vuedraggable 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/util/helper.js: -------------------------------------------------------------------------------- 1 | function getConsole() { 2 | if (typeof window !== "undefined") { 3 | return window.console; 4 | } 5 | return global.console; 6 | } 7 | const console = getConsole(); 8 | 9 | function cached(fn) { 10 | const cache = Object.create(null); 11 | return function cachedFn(str) { 12 | const hit = cache[str]; 13 | return hit || (cache[str] = fn(str)); 14 | }; 15 | } 16 | 17 | const regex = /-(\w)/g; 18 | const camelize = cached(str => 19 | str.replace(regex, (_, c) => (c ? c.toUpperCase() : "")) 20 | ); 21 | 22 | function removeNode(node) { 23 | if (node.parentElement !== null) { 24 | node.parentElement.removeChild(node); 25 | } 26 | } 27 | 28 | function insertNodeAt(fatherNode, node, position) { 29 | const refNode = 30 | position === 0 31 | ? fatherNode.children[0] 32 | : fatherNode.children[position - 1].nextSibling; 33 | fatherNode.insertBefore(node, refNode); 34 | } 35 | 36 | export { insertNodeAt, camelize, console, removeNode }; 37 | -------------------------------------------------------------------------------- /src/vuedraggable.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'vuedraggable' { 2 | import Vue, { VueConstructor } from 'vue'; 3 | 4 | type CombinedVueInstance< 5 | Instance extends Vue, 6 | Data, 7 | Methods, 8 | Computed, 9 | Props 10 | > = Data & Methods & Computed & Props & Instance; 11 | 12 | type ExtendedVue< 13 | Instance extends Vue, 14 | Data, 15 | Methods, 16 | Computed, 17 | Props 18 | > = VueConstructor< 19 | CombinedVueInstance & Vue 20 | >; 21 | 22 | export type DraggedContext = { 23 | index: number; 24 | futureIndex: number; 25 | element: T; 26 | }; 27 | 28 | export type DropContext = { 29 | index: number; 30 | component: Vue; 31 | element: T; 32 | }; 33 | 34 | export type Rectangle = { 35 | top: number; 36 | right: number; 37 | bottom: number; 38 | left: number; 39 | width: number; 40 | height: number; 41 | }; 42 | 43 | export type MoveEvent = { 44 | originalEvent: DragEvent; 45 | dragged: Element; 46 | draggedContext: DraggedContext; 47 | draggedRect: Rectangle; 48 | related: Element; 49 | relatedContext: DropContext; 50 | relatedRect: Rectangle; 51 | from: Element; 52 | to: Element; 53 | willInsertAfter: boolean; 54 | isTrusted: boolean; 55 | }; 56 | 57 | const draggable: ExtendedVue< 58 | Vue, 59 | {}, 60 | {}, 61 | {}, 62 | { 63 | options: any; 64 | list: any[]; 65 | value: any[]; 66 | noTransitionOnDrag?: boolean; 67 | clone: any; 68 | tag?: string | null; 69 | move: any; 70 | componentData: any; 71 | } 72 | >; 73 | 74 | export default draggable; 75 | } 76 | -------------------------------------------------------------------------------- /src/vuedraggable.js: -------------------------------------------------------------------------------- 1 | import Sortable from "sortablejs"; 2 | import { insertNodeAt, camelize, console, removeNode } from "./util/helper"; 3 | 4 | function buildAttribute(object, propName, value) { 5 | if (value === undefined) { 6 | return object; 7 | } 8 | object = object || {}; 9 | object[propName] = value; 10 | return object; 11 | } 12 | 13 | function computeVmIndex(vnodes, element) { 14 | return vnodes.map(elt => elt.elm).indexOf(element); 15 | } 16 | 17 | function computeIndexes(slots, children, isTransition, footerOffset) { 18 | if (!slots) { 19 | return []; 20 | } 21 | 22 | const elmFromNodes = slots.map(elt => elt.elm); 23 | const footerIndex = children.length - footerOffset; 24 | const rawIndexes = [...children].map((elt, idx) => 25 | idx >= footerIndex ? elmFromNodes.length : elmFromNodes.indexOf(elt) 26 | ); 27 | return isTransition ? rawIndexes.filter(ind => ind !== -1) : rawIndexes; 28 | } 29 | 30 | function emit(evtName, evtData) { 31 | this.$nextTick(() => this.$emit(evtName.toLowerCase(), evtData)); 32 | } 33 | 34 | function delegateAndEmit(evtName) { 35 | return evtData => { 36 | if (this.realList !== null) { 37 | this["onDrag" + evtName](evtData); 38 | } 39 | emit.call(this, evtName, evtData); 40 | }; 41 | } 42 | 43 | function isTransitionName(name) { 44 | return ["transition-group", "TransitionGroup"].includes(name); 45 | } 46 | 47 | function isTransition(slots) { 48 | if (!slots || slots.length !== 1) { 49 | return false; 50 | } 51 | const [{ componentOptions }] = slots; 52 | if (!componentOptions) { 53 | return false; 54 | } 55 | return isTransitionName(componentOptions.tag); 56 | } 57 | 58 | function getSlot(slot, scopedSlot, key) { 59 | return slot[key] || (scopedSlot[key] ? scopedSlot[key]() : undefined); 60 | } 61 | 62 | function computeChildrenAndOffsets(children, slot, scopedSlot) { 63 | let headerOffset = 0; 64 | let footerOffset = 0; 65 | const header = getSlot(slot, scopedSlot, "header"); 66 | if (header) { 67 | headerOffset = header.length; 68 | children = children ? [...header, ...children] : [...header]; 69 | } 70 | const footer = getSlot(slot, scopedSlot, "footer"); 71 | if (footer) { 72 | footerOffset = footer.length; 73 | children = children ? [...children, ...footer] : [...footer]; 74 | } 75 | return { children, headerOffset, footerOffset }; 76 | } 77 | 78 | function getComponentAttributes($attrs, componentData) { 79 | let attributes = null; 80 | const update = (name, value) => { 81 | attributes = buildAttribute(attributes, name, value); 82 | }; 83 | const attrs = Object.keys($attrs) 84 | .filter(key => key === "id" || key.startsWith("data-")) 85 | .reduce((res, key) => { 86 | res[key] = $attrs[key]; 87 | return res; 88 | }, {}); 89 | update("attrs", attrs); 90 | 91 | if (!componentData) { 92 | return attributes; 93 | } 94 | const { on, props, attrs: componentDataAttrs } = componentData; 95 | update("on", on); 96 | update("props", props); 97 | Object.assign(attributes.attrs, componentDataAttrs); 98 | return attributes; 99 | } 100 | 101 | const eventsListened = ["Start", "Add", "Remove", "Update", "End"]; 102 | const eventsToEmit = ["Choose", "Unchoose", "Sort", "Filter", "Clone"]; 103 | const readonlyProperties = ["Move", ...eventsListened, ...eventsToEmit].map( 104 | evt => "on" + evt 105 | ); 106 | var draggingElement = null; 107 | 108 | const props = { 109 | options: Object, 110 | list: { 111 | type: Array, 112 | required: false, 113 | default: null 114 | }, 115 | value: { 116 | type: Array, 117 | required: false, 118 | default: null 119 | }, 120 | noTransitionOnDrag: { 121 | type: Boolean, 122 | default: false 123 | }, 124 | clone: { 125 | type: Function, 126 | default: original => { 127 | return original; 128 | } 129 | }, 130 | element: { 131 | type: String, 132 | default: "div" 133 | }, 134 | tag: { 135 | type: String, 136 | default: null 137 | }, 138 | move: { 139 | type: Function, 140 | default: null 141 | }, 142 | componentData: { 143 | type: Object, 144 | required: false, 145 | default: null 146 | } 147 | }; 148 | 149 | const draggableComponent = { 150 | name: "draggable", 151 | 152 | inheritAttrs: false, 153 | 154 | props, 155 | 156 | data() { 157 | return { 158 | transitionMode: false, 159 | noneFunctionalComponentMode: false 160 | }; 161 | }, 162 | 163 | render(h) { 164 | const slots = this.$slots.default; 165 | this.transitionMode = isTransition(slots); 166 | const { children, headerOffset, footerOffset } = computeChildrenAndOffsets( 167 | slots, 168 | this.$slots, 169 | this.$scopedSlots 170 | ); 171 | this.headerOffset = headerOffset; 172 | this.footerOffset = footerOffset; 173 | const attributes = getComponentAttributes(this.$attrs, this.componentData); 174 | return h(this.getTag(), attributes, children); 175 | }, 176 | 177 | created() { 178 | if (this.list !== null && this.value !== null) { 179 | console.error( 180 | "Value and list props are mutually exclusive! Please set one or another." 181 | ); 182 | } 183 | 184 | if (this.element !== "div") { 185 | console.warn( 186 | "Element props is deprecated please use tag props instead. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#element-props" 187 | ); 188 | } 189 | 190 | if (this.options !== undefined) { 191 | console.warn( 192 | "Options props is deprecated, add sortable options directly as vue.draggable item, or use v-bind. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#options-props" 193 | ); 194 | } 195 | }, 196 | 197 | mounted() { 198 | this.noneFunctionalComponentMode = 199 | this.getTag().toLowerCase() !== this.$el.nodeName.toLowerCase() && 200 | !this.getIsFunctional(); 201 | if (this.noneFunctionalComponentMode && this.transitionMode) { 202 | throw new Error( 203 | `Transition-group inside component is not supported. Please alter tag value or remove transition-group. Current tag value: ${this.getTag()}` 204 | ); 205 | } 206 | const optionsAdded = {}; 207 | eventsListened.forEach(elt => { 208 | optionsAdded["on" + elt] = delegateAndEmit.call(this, elt); 209 | }); 210 | 211 | eventsToEmit.forEach(elt => { 212 | optionsAdded["on" + elt] = emit.bind(this, elt); 213 | }); 214 | 215 | const attributes = Object.keys(this.$attrs).reduce((res, key) => { 216 | res[camelize(key)] = this.$attrs[key]; 217 | return res; 218 | }, {}); 219 | 220 | const options = Object.assign({}, this.options, attributes, optionsAdded, { 221 | onMove: (evt, originalEvent) => { 222 | return this.onDragMove(evt, originalEvent); 223 | } 224 | }); 225 | !("draggable" in options) && (options.draggable = ">*"); 226 | this._sortable = new Sortable(this.rootContainer, options); 227 | this.computeIndexes(); 228 | }, 229 | 230 | beforeDestroy() { 231 | if (this._sortable !== undefined) this._sortable.destroy(); 232 | }, 233 | 234 | computed: { 235 | rootContainer() { 236 | return this.transitionMode ? this.$el.children[0] : this.$el; 237 | }, 238 | 239 | realList() { 240 | return this.list ? this.list : this.value; 241 | } 242 | }, 243 | 244 | watch: { 245 | options: { 246 | handler(newOptionValue) { 247 | this.updateOptions(newOptionValue); 248 | }, 249 | deep: true 250 | }, 251 | 252 | $attrs: { 253 | handler(newOptionValue) { 254 | this.updateOptions(newOptionValue); 255 | }, 256 | deep: true 257 | }, 258 | 259 | realList() { 260 | this.computeIndexes(); 261 | } 262 | }, 263 | 264 | methods: { 265 | getIsFunctional() { 266 | const { fnOptions } = this._vnode; 267 | return fnOptions && fnOptions.functional; 268 | }, 269 | 270 | getTag() { 271 | return this.tag || this.element; 272 | }, 273 | 274 | updateOptions(newOptionValue) { 275 | for (var property in newOptionValue) { 276 | const value = camelize(property); 277 | if (readonlyProperties.indexOf(value) === -1) { 278 | this._sortable.option(value, newOptionValue[property]); 279 | } 280 | } 281 | }, 282 | 283 | getChildrenNodes() { 284 | if (this.noneFunctionalComponentMode) { 285 | return this.$children[0].$slots.default; 286 | } 287 | const rawNodes = this.$slots.default; 288 | return this.transitionMode ? rawNodes[0].child.$slots.default : rawNodes; 289 | }, 290 | 291 | computeIndexes() { 292 | this.$nextTick(() => { 293 | this.visibleIndexes = computeIndexes( 294 | this.getChildrenNodes(), 295 | this.rootContainer.children, 296 | this.transitionMode, 297 | this.footerOffset 298 | ); 299 | }); 300 | }, 301 | 302 | getUnderlyingVm(htmlElt) { 303 | const index = computeVmIndex(this.getChildrenNodes() || [], htmlElt); 304 | if (index === -1) { 305 | //Edge case during move callback: related element might be 306 | //an element different from collection 307 | return null; 308 | } 309 | const element = this.realList[index]; 310 | return { index, element }; 311 | }, 312 | 313 | getUnderlyingPotencialDraggableComponent({ __vue__: vue }) { 314 | if ( 315 | !vue || 316 | !vue.$options || 317 | !isTransitionName(vue.$options._componentTag) 318 | ) { 319 | if ( 320 | !("realList" in vue) && 321 | vue.$children.length === 1 && 322 | "realList" in vue.$children[0] 323 | ) 324 | return vue.$children[0]; 325 | 326 | return vue; 327 | } 328 | return vue.$parent; 329 | }, 330 | 331 | emitChanges(evt) { 332 | this.$nextTick(() => { 333 | this.$emit("change", evt); 334 | }); 335 | }, 336 | 337 | alterList(onList) { 338 | if (this.list) { 339 | onList(this.list); 340 | return; 341 | } 342 | const newList = [...this.value]; 343 | onList(newList); 344 | this.$emit("input", newList); 345 | }, 346 | 347 | spliceList() { 348 | const spliceList = list => list.splice(...arguments); 349 | this.alterList(spliceList); 350 | }, 351 | 352 | updatePosition(oldIndex, newIndex) { 353 | const updatePosition = list => 354 | list.splice(newIndex, 0, list.splice(oldIndex, 1)[0]); 355 | this.alterList(updatePosition); 356 | }, 357 | 358 | getRelatedContextFromMoveEvent({ to, related }) { 359 | const component = this.getUnderlyingPotencialDraggableComponent(to); 360 | if (!component) { 361 | return { component }; 362 | } 363 | const list = component.realList; 364 | const context = { list, component }; 365 | if (to !== related && list && component.getUnderlyingVm) { 366 | const destination = component.getUnderlyingVm(related); 367 | if (destination) { 368 | return Object.assign(destination, context); 369 | } 370 | } 371 | return context; 372 | }, 373 | 374 | getVmIndex(domIndex) { 375 | const indexes = this.visibleIndexes; 376 | const numberIndexes = indexes.length; 377 | return domIndex > numberIndexes - 1 ? numberIndexes : indexes[domIndex]; 378 | }, 379 | 380 | getComponent() { 381 | return this.$slots.default[0].componentInstance; 382 | }, 383 | 384 | resetTransitionData(index) { 385 | if (!this.noTransitionOnDrag || !this.transitionMode) { 386 | return; 387 | } 388 | var nodes = this.getChildrenNodes(); 389 | nodes[index].data = null; 390 | const transitionContainer = this.getComponent(); 391 | transitionContainer.children = []; 392 | transitionContainer.kept = undefined; 393 | }, 394 | 395 | onDragStart(evt) { 396 | this.context = this.getUnderlyingVm(evt.item); 397 | evt.item._underlying_vm_ = this.clone(this.context.element); 398 | draggingElement = evt.item; 399 | }, 400 | 401 | onDragAdd(evt) { 402 | const element = evt.item._underlying_vm_; 403 | if (element === undefined) { 404 | return; 405 | } 406 | removeNode(evt.item); 407 | const newIndex = this.getVmIndex(evt.newIndex); 408 | this.spliceList(newIndex, 0, element); 409 | this.computeIndexes(); 410 | const added = { element, newIndex }; 411 | this.emitChanges({ added }); 412 | }, 413 | 414 | onDragRemove(evt) { 415 | insertNodeAt(this.rootContainer, evt.item, evt.oldIndex); 416 | if (evt.pullMode === "clone") { 417 | removeNode(evt.clone); 418 | return; 419 | } 420 | const oldIndex = this.context.index; 421 | this.spliceList(oldIndex, 1); 422 | const removed = { element: this.context.element, oldIndex }; 423 | this.resetTransitionData(oldIndex); 424 | this.emitChanges({ removed }); 425 | }, 426 | 427 | onDragUpdate(evt) { 428 | removeNode(evt.item); 429 | insertNodeAt(evt.from, evt.item, evt.oldIndex); 430 | const oldIndex = this.context.index; 431 | const newIndex = this.getVmIndex(evt.newIndex); 432 | this.updatePosition(oldIndex, newIndex); 433 | const moved = { element: this.context.element, oldIndex, newIndex }; 434 | this.emitChanges({ moved }); 435 | }, 436 | 437 | updateProperty(evt, propertyName) { 438 | evt.hasOwnProperty(propertyName) && 439 | (evt[propertyName] += this.headerOffset); 440 | }, 441 | 442 | computeFutureIndex(relatedContext, evt) { 443 | if (!relatedContext.element) { 444 | return 0; 445 | } 446 | const domChildren = [...evt.to.children].filter( 447 | el => el.style["display"] !== "none" 448 | ); 449 | const currentDOMIndex = domChildren.indexOf(evt.related); 450 | const currentIndex = relatedContext.component.getVmIndex(currentDOMIndex); 451 | const draggedInList = domChildren.indexOf(draggingElement) !== -1; 452 | return draggedInList || !evt.willInsertAfter 453 | ? currentIndex 454 | : currentIndex + 1; 455 | }, 456 | 457 | onDragMove(evt, originalEvent) { 458 | const onMove = this.move; 459 | if (!onMove || !this.realList) { 460 | return true; 461 | } 462 | 463 | const relatedContext = this.getRelatedContextFromMoveEvent(evt); 464 | const draggedContext = this.context; 465 | const futureIndex = this.computeFutureIndex(relatedContext, evt); 466 | Object.assign(draggedContext, { futureIndex }); 467 | const sendEvt = Object.assign({}, evt, { 468 | relatedContext, 469 | draggedContext 470 | }); 471 | return onMove(sendEvt, originalEvent); 472 | }, 473 | 474 | onDragEnd() { 475 | this.computeIndexes(); 476 | draggingElement = null; 477 | } 478 | } 479 | }; 480 | 481 | if (typeof window !== "undefined" && "Vue" in window) { 482 | window.Vue.component("draggable", draggableComponent); 483 | } 484 | 485 | export default draggableComponent; 486 | -------------------------------------------------------------------------------- /tests/unit/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | jest: true 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /tests/unit/helper/DraggableWithList.vue: -------------------------------------------------------------------------------- 1 | 9 | 23 | -------------------------------------------------------------------------------- /tests/unit/helper/DraggableWithModel.vue: -------------------------------------------------------------------------------- 1 | 9 | 23 | -------------------------------------------------------------------------------- /tests/unit/helper/DraggableWithTransition.vue: -------------------------------------------------------------------------------- 1 | 11 | 25 | -------------------------------------------------------------------------------- /tests/unit/helper/FakeComponent.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: "Fake", 3 | props: { 4 | prop1: { 5 | type: String, 6 | default: "string" 7 | } 8 | }, 9 | template: "
{{prop1}}
" 10 | } -------------------------------------------------------------------------------- /tests/unit/helper/FakeFunctionalComponent.js: -------------------------------------------------------------------------------- 1 | export default { 2 | name: "FakeFunctional", 3 | functional:true, 4 | props: { 5 | prop1: { 6 | type: String, 7 | default: "string" 8 | } 9 | }, 10 | render(createElement, context) { 11 | return createElement('button', 'Click me'); 12 | } 13 | } -------------------------------------------------------------------------------- /tests/unit/util/helper.node.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment node 3 | */ 4 | 5 | import { console } from "@/util/helper"; 6 | 7 | describe("console", () => { 8 | test.each([ 9 | ["log"], 10 | ["warn"], 11 | ["error"], 12 | ["info"], 13 | ])( 14 | "has %s function", 15 | (key) => { 16 | const actual = console[key]; 17 | expect(typeof actual).toEqual("function"); 18 | } 19 | ) 20 | }); 21 | -------------------------------------------------------------------------------- /tests/unit/util/helper.spec.js: -------------------------------------------------------------------------------- 1 | import { camelize, console } from "@/util/helper"; 2 | 3 | describe("camelize", () => { 4 | test.each([ 5 | ["MyProp", "MyProp"], 6 | ["MyProp", "MyProp"], 7 | ["kebab-case", "kebabCase"], 8 | ["multi-hyphen-string", "multiHyphenString"], 9 | ["drag-class", "dragClass"], 10 | ["test-", "test-"] 11 | ])( 12 | "transform %s into %s", 13 | (value, expected) =>{ 14 | const actual = camelize(value); 15 | expect(actual).toEqual(expected); 16 | } 17 | ) 18 | }); 19 | 20 | describe("console", () => { 21 | test.each([ 22 | ["log"], 23 | ["warn"], 24 | ["error"], 25 | ["info"], 26 | ])( 27 | "has %s function", 28 | (key) =>{ 29 | const actual = console[key]; 30 | expect(typeof actual).toEqual("function"); 31 | } 32 | ) 33 | }); -------------------------------------------------------------------------------- /tests/unit/vuedraggable.integrated.spec.js: -------------------------------------------------------------------------------- 1 | import { mount } from "@vue/test-utils"; 2 | import Sortable from "sortablejs"; 3 | jest.genMockFromModule("sortablejs"); 4 | jest.mock("sortablejs"); 5 | const SortableFake = { 6 | destroy: jest.fn(), 7 | option: jest.fn(), 8 | }; 9 | Sortable.mockImplementation(() => SortableFake); 10 | 11 | import Vue from "vue"; 12 | import DraggableWithList from "./helper/DraggableWithList"; 13 | import DraggableWithModel from "./helper/DraggableWithList"; 14 | import DraggableWithTransition from "./helper/DraggableWithTransition"; 15 | 16 | import draggable from "@/vuedraggable"; 17 | 18 | let wrapper; 19 | let element; 20 | let vm; 21 | 22 | function getEvent(name) { 23 | return Sortable.mock.calls[0][1][name]; 24 | } 25 | 26 | const expectedArray = [0, 1, 3, 4, 5, 6, 7, 2, 8, 9]; 27 | const expectedDomWithWrapper = (wrapper) => 28 | `<${wrapper}>${expectedArray 29 | .map((nu) => `
${nu}
`) 30 | .join("")}`; 31 | 32 | const expectedDomNoTransition = expectedDomWithWrapper("span"); 33 | const expectedDomTransition = `
${expectedDomWithWrapper( 34 | "transition-group-stub" 35 | )}
`; 36 | 37 | function normalizeHTML(wrapper) { 38 | return wrapper.html().replace(/(\r\n\t|\n|\r\t| )/gm, ""); 39 | } 40 | 41 | function expectHTML(wrapper, expected) { 42 | const htmlStripped = normalizeHTML(wrapper); 43 | expect(htmlStripped).toEqual(expected); 44 | } 45 | 46 | describe.each([ 47 | [DraggableWithList, "draggable with list", expectedDomNoTransition, "span"], 48 | [DraggableWithModel, "draggable with model", expectedDomNoTransition, "span"], 49 | [ 50 | DraggableWithTransition, 51 | "draggable with transition", 52 | expectedDomTransition, 53 | "transition-group-stub", 54 | ], 55 | ])( 56 | "should update list and DOM with component: %s %s", 57 | (component, _, expectedDom, expectWrapper) => { 58 | describe("when handling sort", () => { 59 | beforeEach(async () => { 60 | jest.resetAllMocks(); 61 | wrapper = mount(component); 62 | vm = wrapper.vm; 63 | element = wrapper.find(expectWrapper).element; 64 | 65 | const item = element.children[2]; 66 | const startEvt = { item }; 67 | getEvent("onStart")(startEvt); 68 | await Vue.nextTick(); 69 | 70 | const firstDraggable = element.children[1]; 71 | element.removeChild(item); 72 | element.insertBefore(item, firstDraggable); 73 | getEvent("onUpdate")({ 74 | item, 75 | oldIndex: 2, 76 | newIndex: 7, 77 | from: element, 78 | }); 79 | await Vue.nextTick(); 80 | }); 81 | 82 | it("sends a change event", async () => { 83 | const draggableWrapper = wrapper.findComponent(draggable); 84 | const expectedEvt = { moved: { element: 2, oldIndex: 2, newIndex: 7 } }; 85 | expect(draggableWrapper.emitted().change).toEqual([[expectedEvt]]); 86 | }); 87 | 88 | it("update list", async () => { 89 | expect(vm.array).toEqual(expectedArray); 90 | }); 91 | 92 | it("updates DOM", async () => { 93 | expectHTML(wrapper, expectedDom); 94 | }); 95 | }); 96 | } 97 | ); 98 | -------------------------------------------------------------------------------- /tests/unit/vuedraggable.script.tag.spec.js: -------------------------------------------------------------------------------- 1 | import Vue from "vue"; 2 | window.Vue = Vue; 3 | 4 | describe("draggable when used with script tag", () => { 5 | it("register draggable component", () => { 6 | const draggable = require("@/vuedraggable").default._Ctor[0]; 7 | const component = Vue.component("draggable"); 8 | expect(component).toBe(draggable); 9 | }); 10 | }); -------------------------------------------------------------------------------- /tests/unit/vuedraggable.spec.js: -------------------------------------------------------------------------------- 1 | import { mount, shallowMount } from "@vue/test-utils"; 2 | import Sortable from "sortablejs"; 3 | jest.genMockFromModule("sortablejs"); 4 | jest.mock("sortablejs"); 5 | const SortableFake = { 6 | destroy: jest.fn(), 7 | option: jest.fn(), 8 | }; 9 | Sortable.mockImplementation(() => SortableFake); 10 | import draggable from "@/vuedraggable"; 11 | import Vue from "vue"; 12 | import Fake from "./helper/FakeComponent.js"; 13 | import FakeFunctional from "./helper/FakeFunctionalComponent.js"; 14 | 15 | let wrapper; 16 | let vm; 17 | let props; 18 | let items; 19 | let item; 20 | let element; 21 | let input; 22 | const initialRender = 23 | "
a
b
c
"; 24 | const initialRenderRaw = "
a
b
c
"; 25 | const initialRenderTransition = 26 | "
a
b
c
"; 27 | 28 | function normalizeHTML(wrapper) { 29 | return wrapper.html().replace(/(\r\n\t|\n|\r\t| )/gm, ""); 30 | } 31 | 32 | function expectHTML(wrapper, expected) { 33 | const htmlStripped = normalizeHTML(wrapper); 34 | expect(htmlStripped).toEqual(expected); 35 | } 36 | 37 | function getEvent(name) { 38 | return Sortable.mock.calls[0][1][name]; 39 | } 40 | 41 | function resetMocks() { 42 | Sortable.mockClear(); 43 | SortableFake.destroy.mockClear(); 44 | SortableFake.option.mockClear(); 45 | } 46 | 47 | describe("draggable.vue when initialized with list", () => { 48 | beforeEach(() => { 49 | resetMocks(); 50 | items = ["a", "b", "c"]; 51 | wrapper = shallowMount(draggable, { 52 | propsData: { 53 | list: items, 54 | }, 55 | attrs: { 56 | sortableOption: "value", 57 | "to-be-camelized": true, 58 | }, 59 | slots: { 60 | default: items.map((item) => `
${item}
`), 61 | header: "
", 62 | footer: "