├── .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 | [](https://circleci.com/gh/SortableJS/Vue.Draggable)
5 | [](https://codecov.io/gh/SortableJS/Vue.Draggable)
6 | [](https://codebeat.co/projects/github-com-sortablejs-vue-draggable-master)
7 | [](https://github.com/SortableJS/Vue.Draggable/issues?q=is%3Aopen+is%3Aissue)
8 | [](https://www.npmjs.com/package/vuedraggable)
9 | [](https://www.npmjs.com/package/vuedraggable)
10 | [](https://www.npmjs.com/package/vuedraggable)
11 | [](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 | 
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 | [](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 | Add
124 |
125 | ```
126 | ### With header slot:
127 | ``` html
128 |
129 |
130 | {{element.name}}
131 |
132 | Add
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 | Add
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 | Add
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 |
12 | We're sorry but vuedraggable doesn't work properly without JavaScript enabled. Please enable it to continue.
13 |
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 |
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 |
2 |
3 |
4 |
Draggable 1
5 |
12 |
13 | {{ element.name }}
14 |
15 |
16 |
17 |
18 |
19 |
Draggable 2
20 |
21 |
22 | {{ element.name }}
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
72 |
73 |
--------------------------------------------------------------------------------
/example/components/clone.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Draggable 1
5 |
11 |
16 | {{ element.name }}
17 |
18 |
19 |
20 |
21 |
22 |
Draggable 2
23 |
29 |
34 | {{ element.name }}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
76 |
77 |
--------------------------------------------------------------------------------
/example/components/custom-clone.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Draggable 1
5 |
12 |
13 | {{ element.name }}
14 |
15 |
16 |
17 |
18 |
19 |
Draggable 2
20 |
26 |
27 | {{ element.name }}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
76 |
77 |
--------------------------------------------------------------------------------
/example/components/footerslot.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Draggable with footer
5 |
6 |
16 |
21 | {{ element.name }}
22 |
23 |
24 |
31 | Add
32 | Replace
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
77 |
86 |
--------------------------------------------------------------------------------
/example/components/functional.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Draggable
5 |
11 |
19 |
25 |
32 | {{ item.title }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
86 |
101 |
--------------------------------------------------------------------------------
/example/components/handle.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Add
5 |
6 |
7 |
8 |
Draggable {{ draggingInfo }}
9 |
10 |
11 |
16 |
17 |
18 | {{ element.name }}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
68 |
93 |
--------------------------------------------------------------------------------
/example/components/headerslot.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Draggable with header
5 |
6 |
13 |
18 | {{ element.name }}
19 |
20 |
21 |
27 | Add
28 | Replace
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
67 |
68 |
--------------------------------------------------------------------------------
/example/components/infra/nested.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ el.name }}
5 |
6 |
7 |
8 |
9 |
25 |
31 |
--------------------------------------------------------------------------------
/example/components/infra/raw-displayer.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
{{ title }}
4 |
{{ valueString }}
5 |
6 |
7 |
27 |
32 |
--------------------------------------------------------------------------------
/example/components/nested-example.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Nested draggable
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
51 |
52 |
--------------------------------------------------------------------------------
/example/components/nested-with-vmodel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
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 |
18 |
26 |
27 |
{{ el.name }}
28 |
29 |
30 |
31 |
32 |
33 |
74 |
--------------------------------------------------------------------------------
/example/components/simple.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
25 |
26 |
27 |
Draggable {{ draggingInfo }}
28 |
29 |
38 |
43 | {{ element.name }}
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
91 |
101 |
--------------------------------------------------------------------------------
/example/components/table-column-example.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Draggable table
5 |
6 |
7 |
8 |
9 |
10 | {{ header }}
11 |
12 |
13 |
14 |
15 |
16 | {{ item[header] }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
51 |
56 |
--------------------------------------------------------------------------------
/example/components/table-example.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Draggable table
5 |
6 |
7 |
8 |
9 | Id
10 | Name
11 | Sport
12 |
13 |
14 |
15 |
16 | {{ item.id }}
17 | {{ item.name }}
18 | {{ item.sport }}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
50 |
55 |
--------------------------------------------------------------------------------
/example/components/third-party.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
18 |
24 | {{ lign }}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
99 |
100 |
--------------------------------------------------------------------------------
/example/components/transition-example-2.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | To original order
6 |
7 |
8 |
9 |
10 |
Transition
11 |
19 |
20 |
25 |
32 | {{ element.name }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
88 |
89 |
119 |
--------------------------------------------------------------------------------
/example/components/transition-example.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | To original order
6 |
7 |
8 |
9 |
10 |
Transition
11 |
19 |
20 |
25 |
32 | {{ element.name }}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
86 |
87 |
117 |
--------------------------------------------------------------------------------
/example/components/two-list-headerslots.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
First draggable with header
5 |
6 |
14 |
19 | {{ element.name }}
20 |
21 |
22 |
28 | Add
29 | Replace
30 |
31 |
32 |
33 |
34 |
35 |
Second draggable with header
36 |
37 |
38 |
43 | {{ element.name }}
44 |
45 |
46 |
52 | Add
53 | Replace
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
100 |
101 |
--------------------------------------------------------------------------------
/example/components/two-lists.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
Draggable 1
5 |
6 |
11 | {{ element.name }} {{ index }}
12 |
13 |
14 |
15 |
16 |
17 |
Draggable 2
18 |
19 |
24 | {{ element.name }} {{ index }}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
77 |
--------------------------------------------------------------------------------
/example/debug-components/future-index.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
25 |
26 |
27 |
Draggable
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
68 |
78 |
--------------------------------------------------------------------------------
/example/debug-components/nested/draggable-list.vue:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 | {{ element.name }}
13 |
14 |
15 |
16 |
17 |
58 |
64 |
--------------------------------------------------------------------------------
/example/debug-components/slot-example.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
17 |
18 |
Draggable
19 |
20 |
26 |
31 | {{ element.name }}
32 |
33 |
34 |
35 |
36 | header slot
37 |
38 |
39 |
40 |
41 |
42 | footer slot
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
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 |
12 | We're sorry but vuedraggable doesn't work properly without JavaScript enabled. Please enable it to continue.
13 |
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 |
2 |
3 | {{item}}
7 |
8 |
9 |
23 |
--------------------------------------------------------------------------------
/tests/unit/helper/DraggableWithModel.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{item}}
7 |
8 |
9 |
23 |
--------------------------------------------------------------------------------
/tests/unit/helper/DraggableWithTransition.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{item}}
8 |
9 |
10 |
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("")}${wrapper}>`;
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 | "";
24 | const initialRenderRaw = "";
25 | const initialRenderTransition =
26 | "";
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: "",
63 | },
64 | });
65 | vm = wrapper.vm;
66 | props = vm.$options.props;
67 | element = wrapper.element;
68 | });
69 |
70 | describe("when initialized with incorrect props", () => {
71 | const { error } = console;
72 | const { warn } = console;
73 |
74 | beforeEach(() => {
75 | console.error = jest.fn();
76 | console.warn = jest.fn();
77 | });
78 |
79 | afterEach(() => {
80 | console.error = error;
81 | console.warn = warn;
82 | });
83 |
84 | it("log an error when list and value are both not null", () => {
85 | wrapper = shallowMount(draggable, {
86 | propsData: {
87 | list: [],
88 | value: [],
89 | },
90 | slots: {
91 | default: "",
92 | },
93 | });
94 | expect(console.error).toBeCalledWith(
95 | "Value and list props are mutually exclusive! Please set one or another."
96 | );
97 | });
98 |
99 | it("warns when options is used", () => {
100 | wrapper = shallowMount(draggable, {
101 | propsData: {
102 | options: {
103 | group: "led zeppelin",
104 | },
105 | },
106 | slots: {
107 | default: "",
108 | },
109 | });
110 | expect(console.warn).toBeCalledWith(
111 | "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"
112 | );
113 | });
114 |
115 | it("warns when element is used", () => {
116 | wrapper = shallowMount(draggable, {
117 | propsData: {
118 | element: "li",
119 | },
120 | slots: {
121 | default: "",
122 | },
123 | });
124 | expect(console.warn).toBeCalledWith(
125 | "Element props is deprecated please use tag props instead. See https://github.com/SortableJS/Vue.Draggable/blob/master/documentation/migrate.md#element-props"
126 | );
127 | });
128 | });
129 |
130 | it("instantiate without error", () => {
131 | expect(wrapper).not.toBeUndefined();
132 | });
133 |
134 | it("has draggable name", () => {
135 | expect(vm.name).not.toBe("draggable");
136 | });
137 |
138 | test.each([
139 | ["options", { type: Object }],
140 | [
141 | "list",
142 | {
143 | type: Array,
144 | required: false,
145 | default: null,
146 | },
147 | ],
148 | [
149 | "value",
150 | {
151 | type: Array,
152 | required: false,
153 | default: null,
154 | },
155 | ],
156 | [
157 | "noTransitionOnDrag",
158 | {
159 | type: Boolean,
160 | default: false,
161 | },
162 | ],
163 | [
164 | "element",
165 | {
166 | type: String,
167 | default: "div",
168 | },
169 | ],
170 | [
171 | "tag",
172 | {
173 | type: String,
174 | default: null,
175 | },
176 | ],
177 | [
178 | "move",
179 | {
180 | type: Function,
181 | default: null,
182 | },
183 | ],
184 | [
185 | "componentData",
186 | {
187 | type: Object,
188 | required: false,
189 | default: null,
190 | },
191 | ],
192 | ])("should have props %s equal to %o", (name, value) => {
193 | const propsValue = props[name];
194 | expect(propsValue).toEqual(value);
195 | });
196 |
197 | it("has a clone props, defaulting with identity function", () => {
198 | const expected = {};
199 | const { clone } = props;
200 | expect(clone.type).toBe(Function);
201 | expect(clone.default(expected)).toBe(expected);
202 | });
203 |
204 | it("renders root element correctly", () => {
205 | expect(normalizeHTML(wrapper)).toMatch(/^.*<\/div>$/);
206 | });
207 |
208 | it("renders footer slot element correctly", () => {
209 | expect(normalizeHTML(wrapper)).toMatch(/
<\/footer><\/div>$/);
210 | });
211 |
212 | it("renders header slot element correctly", () => {
213 | expect(normalizeHTML(wrapper)).toMatch(/^<\/header>/);
214 | });
215 |
216 | it("renders default slot element correctly", () => {
217 | expect(normalizeHTML(wrapper)).toContain(
218 | "a
b
c
"
219 | );
220 | });
221 |
222 | it("renders correctly", () => {
223 | expectHTML(wrapper, initialRender);
224 | });
225 |
226 | describe.each(["ul", "span", "div"])("considering a tag %s", (tag) => {
227 | beforeEach(() => {
228 | wrapper = shallowMount(draggable, {
229 | propsData: { tag },
230 | });
231 | });
232 |
233 | it("renders tag as root element", () => {
234 | const expectedRegex = new RegExp(`^<${tag}>.*<\/${tag}>$`);
235 | expect(wrapper.html()).toMatch(expectedRegex);
236 | });
237 |
238 | it("set noneFunctionalComponentMode to false ", () => {
239 | const { noneFunctionalComponentMode } = vm;
240 | expect(noneFunctionalComponentMode).toBe(false);
241 | });
242 | });
243 |
244 | it("computes indexes", async () => {
245 | await Vue.nextTick();
246 | expect(vm.visibleIndexes).toEqual([-1, 0, 1, 2, 3]);
247 | });
248 |
249 | it("update indexes", async () => {
250 | await Vue.nextTick();
251 | const computeIndexes = jest.fn();
252 | wrapper.setMethods({ computeIndexes });
253 | wrapper.setProps({ list: ["c", "d", "e", "f", "g"] });
254 | await Vue.nextTick();
255 | expect(computeIndexes).toHaveBeenCalled();
256 | });
257 |
258 | it("set realList", () => {
259 | expect(vm.realList).toEqual(["a", "b", "c"]);
260 | });
261 |
262 | describe("when using component as tag", () => {
263 | beforeEach(() => {
264 | input = jest.fn();
265 | wrapper = mount(draggable, {
266 | propsData: {
267 | tag: "child",
268 | componentData: {
269 | on: {
270 | input,
271 | },
272 | attrs: {
273 | attribute1: "value1",
274 | },
275 | props: {
276 | prop1: "info",
277 | prop2: true,
278 | },
279 | },
280 | },
281 | stubs: {
282 | child: Fake,
283 | },
284 | });
285 | });
286 |
287 | it("instantiate child component", async () => {
288 | const child = wrapper.find(Fake);
289 | expect(child).not.toBeNull();
290 | });
291 |
292 | it("pass data to tag child", async () => {
293 | const fakeChild = wrapper.find(Fake);
294 | expect(fakeChild.props("prop1")).toEqual("info");
295 | });
296 |
297 | it("pass event listener to tag child", async () => {
298 | const child = wrapper.find(Fake);
299 | const evt = { data: 33 };
300 | child.vm.$emit("input", evt);
301 | expect(input).toHaveBeenCalledWith(evt);
302 | });
303 |
304 | it("pass attributes to tag child", async () => {
305 | const child = wrapper.find(Fake);
306 | const attrValue = child.attributes("attribute1");
307 | expect(attrValue).toEqual("value1");
308 | });
309 | });
310 |
311 | test.each([[Fake, true], [FakeFunctional, false]])(
312 | "when using component as tag",
313 | (component, expectedNoneFunctionalComponentMode) => {
314 | wrapper = mount(draggable, {
315 | propsData: {
316 | tag: "child",
317 | },
318 | stubs: {
319 | child: component,
320 | },
321 | });
322 | const {
323 | vm: { noneFunctionalComponentMode },
324 | } = wrapper;
325 | expect(noneFunctionalComponentMode).toBe(
326 | expectedNoneFunctionalComponentMode
327 | );
328 | }
329 | );
330 |
331 | it("keeps a reference to Sortable instance", () => {
332 | expect(vm._sortable).toBe(SortableFake);
333 | });
334 |
335 | it("creates sortable instance with options", () => {
336 | expect(Sortable.mock.calls.length).toBe(1);
337 | const parameters = Sortable.mock.calls[0];
338 | expect(parameters[0]).toBe(element);
339 | expect(parameters[1]).toMatchObject({
340 | draggable: ">*",
341 | sortableOption: "value",
342 | toBeCamelized: true,
343 | });
344 | });
345 |
346 | test.each([
347 | ["onChoose", "choose"],
348 | ["onUnchoose", "unchoose"],
349 | ["onSort", "sort"],
350 | ["onFilter", "filter"],
351 | ["onClone", "clone"],
352 | ])("when event %s is emitted from sortable", async (evt, vueEvt) => {
353 | const callBack = getEvent(evt);
354 | const evtInfo = {
355 | data: {},
356 | };
357 | callBack(evtInfo);
358 | await Vue.nextTick();
359 | expect(wrapper.emitted()).toEqual({
360 | [vueEvt]: [[evtInfo]],
361 | });
362 | });
363 |
364 | it("creates sortable instance with options", () => {
365 | expect(Sortable.mock.calls.length).toBe(1);
366 | const parameters = Sortable.mock.calls[0];
367 | expect(parameters[0]).toBe(element);
368 | expect(parameters[1]).toMatchObject({
369 | draggable: ">*",
370 | sortableOption: "value",
371 | toBeCamelized: true,
372 | });
373 | });
374 |
375 | describe("when add is called", () => {
376 | let newItem;
377 | beforeEach(async () => {
378 | await Vue.nextTick();
379 | newItem = document.createElement("div");
380 | const newContent = document.createTextNode("d");
381 | newItem.appendChild(newContent);
382 | newItem._underlying_vm_ = "d";
383 | const last = element.children[3];
384 | element.insertBefore(newItem, last);
385 | const add = getEvent("onAdd");
386 | add({
387 | item: newItem,
388 | newIndex: 3,
389 | });
390 | });
391 |
392 | it("DOM changes should be reverted", async () => {
393 | await Vue.nextTick();
394 | expectHTML(wrapper, initialRender);
395 | });
396 |
397 | it("list should be updated", async () => {
398 | await Vue.nextTick();
399 | expect(vm.list).toEqual(["a", "b", "d", "c"]);
400 | });
401 |
402 | it("sends a update event", async () => {
403 | await Vue.nextTick();
404 | const expectedEvt = {
405 | item: newItem,
406 | newIndex: 3,
407 | };
408 | expect(wrapper.emitted().add).toEqual([[expectedEvt]]);
409 | });
410 |
411 | it("sends a change event", async () => {
412 | await Vue.nextTick();
413 | const expectedEvt = { added: { element: "d", newIndex: 2 } };
414 | expect(wrapper.emitted().change).toEqual([[expectedEvt]]);
415 | });
416 | });
417 |
418 | describe("when initiating a drag operation", () => {
419 | let evt;
420 | beforeEach(() => {
421 | item = element.children[2];
422 | evt = { item };
423 | const start = getEvent("onStart");
424 | start(evt);
425 | });
426 |
427 | it("sends a start event", async () => {
428 | await Vue.nextTick();
429 | expect(wrapper.emitted()).toEqual({
430 | start: [[evt]],
431 | });
432 | });
433 |
434 | it("sets context", async () => {
435 | await Vue.nextTick();
436 | expect(vm.context).toEqual({
437 | element: "b",
438 | index: 1,
439 | });
440 | });
441 |
442 | describe("when calling onMove", () => {
443 | let originalEvt;
444 | let move;
445 | let doMove;
446 |
447 | beforeEach(() => {
448 | evt = {
449 | to: element,
450 | related: element.children[1],
451 | willInsertAfter: false,
452 | };
453 | originalEvt = {
454 | domInfo: true,
455 | };
456 | move = getEvent("onMove");
457 | doMove = () => move(evt, originalEvt);
458 | });
459 |
460 | it("returns true when move props is null", () => {
461 | const actual = doMove();
462 | expect(actual).toBe(true);
463 | });
464 |
465 | describe("when move is set", () => {
466 | let move;
467 | beforeEach(() => {
468 | move = jest.fn();
469 | wrapper.setProps({ move });
470 | });
471 |
472 | it("calls move with list information", () => {
473 | const expectedEvt = {
474 | draggedContext: {
475 | element: "b",
476 | futureIndex: 0,
477 | index: 1,
478 | },
479 | relatedContext: {
480 | component: vm,
481 | element: "a",
482 | index: 0,
483 | list: ["a", "b", "c"],
484 | },
485 | to: element,
486 | related: element.children[1],
487 | willInsertAfter: false,
488 | };
489 | doMove();
490 | expect(move.mock.calls.length).toBe(1);
491 | expect(move).toHaveBeenCalledWith(expectedEvt, originalEvt);
492 | });
493 |
494 | test.each([
495 | [1, false, 0, { element: "a", index: 0 }],
496 | [2, false, 1, { element: "b", index: 1 }],
497 | [3, false, 2, { element: "c", index: 2 }],
498 |
499 | // Will insert after is not taken into account if the dragging
500 | // element is in the target list
501 | [1, true, 0, { element: "a", index: 0 }],
502 | [2, true, 1, { element: "b", index: 1 }],
503 | [3, true, 2, { element: "c", index: 2 }],
504 | ])(
505 | "when context is of index %n with insert after %o has futureIndex: %n and context: %o",
506 | (index, willInsertAfter, futureIndex, context) => {
507 | evt.willInsertAfter = willInsertAfter;
508 | evt.related = element.children[index];
509 |
510 | const expectedEvt = {
511 | draggedContext: {
512 | element: "b",
513 | futureIndex,
514 | index: 1,
515 | },
516 | relatedContext: {
517 | component: vm,
518 | element: context.element,
519 | index: context.index,
520 | list: ["a", "b", "c"],
521 | },
522 | to: element,
523 | related: element.children[index],
524 | willInsertAfter,
525 | };
526 |
527 | doMove();
528 | expect(move.mock.calls.length).toBe(1);
529 | expect(move).toHaveBeenCalledWith(expectedEvt, originalEvt);
530 | }
531 | );
532 |
533 | test.each([true, false])("returns move result %o", (result) => {
534 | move.mockImplementation(() => result);
535 | const actual = doMove();
536 | expect(actual).toBe(result);
537 | });
538 | });
539 | });
540 |
541 | describe("when remove is called", () => {
542 | beforeEach(() => {
543 | element.removeChild(item);
544 | const remove = getEvent("onRemove");
545 | remove({
546 | item,
547 | oldIndex: 2,
548 | });
549 | });
550 |
551 | it("DOM changes should be reverted", async () => {
552 | await Vue.nextTick();
553 | expectHTML(wrapper, initialRender);
554 | });
555 |
556 | it("list should be updated", async () => {
557 | await Vue.nextTick();
558 | expect(vm.list).toEqual(["a", "c"]);
559 | });
560 |
561 | it("sends a remove event", async () => {
562 | await Vue.nextTick();
563 | const expectedEvt = { item, oldIndex: 2 };
564 | expect(wrapper.emitted().remove).toEqual([[expectedEvt]]);
565 | });
566 |
567 | it("sends a change event", async () => {
568 | await Vue.nextTick();
569 | const expectedEvt = { removed: { element: "b", oldIndex: 1 } };
570 | expect(wrapper.emitted().change).toEqual([[expectedEvt]]);
571 | });
572 | });
573 |
574 | describe.each([[1, ["b", "a", "c"]], [3, ["a", "c", "b"]]])(
575 | "when update is called with new index being %i",
576 | (index, expectedList) => {
577 | beforeEach(() => {
578 | const firstDraggable = element.children[index];
579 | element.removeChild(item);
580 | element.insertBefore(item, firstDraggable);
581 | const update = getEvent("onUpdate");
582 | update({
583 | item,
584 | oldIndex: 2,
585 | newIndex: index,
586 | from: element,
587 | });
588 | });
589 |
590 | it("DOM changes should be reverted", async () => {
591 | await Vue.nextTick();
592 | expectHTML(wrapper, initialRender);
593 | });
594 |
595 | it("list should be updated", async () => {
596 | await Vue.nextTick();
597 | expect(vm.list).toEqual(expectedList);
598 | });
599 |
600 | it("sends a update event", async () => {
601 | await Vue.nextTick();
602 | const expectedEvt = {
603 | item,
604 | oldIndex: 2,
605 | newIndex: index,
606 | from: element,
607 | };
608 | expect(wrapper.emitted().update).toEqual([[expectedEvt]]);
609 | });
610 |
611 | it("sends a change event", async () => {
612 | await Vue.nextTick();
613 | const expectedEvt = {
614 | moved: { element: "b", oldIndex: 1, newIndex: index - 1 },
615 | };
616 | expect(wrapper.emitted().change).toEqual([[expectedEvt]]);
617 | });
618 | }
619 | );
620 |
621 | describe("when sending DragEnd", () => {
622 | let endEvt;
623 | beforeEach(() => {
624 | endEvt = {
625 | data: "data",
626 | };
627 | const onEnd = getEvent("onEnd");
628 | onEnd(endEvt);
629 | });
630 |
631 | it("sends a update event", async () => {
632 | await Vue.nextTick();
633 | expect(wrapper.emitted().end).toEqual([[endEvt]]);
634 | });
635 | });
636 | });
637 |
638 | describe("when initiating a drag operation in clone context", () => {
639 | let evt;
640 | beforeEach(() => {
641 | resetMocks();
642 | wrapper = shallowMount(draggable, {
643 | propsData: {
644 | list: items,
645 | },
646 | slots: {
647 | default: items.map((item) => `${item}
`),
648 | },
649 | });
650 | vm = wrapper.vm;
651 | element = wrapper.element;
652 | item = element.children[1];
653 | evt = { item };
654 | const start = getEvent("onStart");
655 | start(evt);
656 | });
657 |
658 | describe("when remove is called", () => {
659 | beforeEach(() => {
660 | var clone = item.cloneNode(true);
661 | wrapper.element.insertBefore(clone, item);
662 | wrapper.element.removeChild(item);
663 | const remove = getEvent("onRemove");
664 | remove({
665 | item,
666 | clone,
667 | pullMode: "clone",
668 | oldIndex: 1,
669 | });
670 | });
671 |
672 | it("DOM changes should be reverted", async () => {
673 | await Vue.nextTick();
674 | expectHTML(wrapper, initialRenderRaw);
675 | });
676 |
677 | it("list should be not updated", async () => {
678 | await Vue.nextTick();
679 | expect(vm.list).toEqual(["a", "b", "c"]);
680 | });
681 |
682 | it("sends a remove event", async () => {
683 | await Vue.nextTick();
684 | expect(wrapper.emitted().remove).toEqual([
685 | [
686 | {
687 | item,
688 | clone: item,
689 | pullMode: "clone",
690 | oldIndex: 1,
691 | },
692 | ],
693 | ]);
694 | });
695 |
696 | it("does not send a change event", async () => {
697 | await Vue.nextTick();
698 | expect(wrapper.emitted().change).toBeUndefined();
699 | });
700 | });
701 | });
702 |
703 | describe("when initiating a drag operation in clone context using a pull function", () => {
704 | let evt;
705 | beforeEach(() => {
706 | resetMocks();
707 | wrapper = shallowMount(draggable, {
708 | propsData: {
709 | list: items,
710 | },
711 | attrs: {
712 | group: { pull: () => "clone" },
713 | },
714 | slots: {
715 | default: items.map((item) => `${item}
`),
716 | },
717 | });
718 | vm = wrapper.vm;
719 | element = wrapper.element;
720 | item = element.children[1];
721 | evt = { item };
722 | const start = getEvent("onStart");
723 | start(evt);
724 | });
725 |
726 | describe("when remove is called", () => {
727 | beforeEach(() => {
728 | var clone = item.cloneNode(true);
729 | wrapper.element.insertBefore(clone, item);
730 | wrapper.element.removeChild(item);
731 | const remove = getEvent("onRemove");
732 | remove({
733 | item,
734 | clone,
735 | pullMode: "clone",
736 | oldIndex: 1,
737 | });
738 | });
739 |
740 | it("DOM changes should be reverted", async () => {
741 | await Vue.nextTick();
742 | expectHTML(wrapper, initialRenderRaw);
743 | });
744 |
745 | it("list should be not updated", async () => {
746 | await Vue.nextTick();
747 | expect(vm.list).toEqual(["a", "b", "c"]);
748 | });
749 |
750 | it("does not send a remove event", async () => {
751 | await Vue.nextTick();
752 | expect(wrapper.emitted().remove).toEqual([
753 | [
754 | {
755 | item,
756 | clone: item,
757 | pullMode: "clone",
758 | oldIndex: 1,
759 | },
760 | ],
761 | ]);
762 | });
763 |
764 | it("does not send a change event", async () => {
765 | await Vue.nextTick();
766 | expect(wrapper.emitted().change).toBeUndefined();
767 | });
768 | });
769 | });
770 |
771 | describe("when attribute changes:", () => {
772 | const { error } = console;
773 | beforeEach(() => {
774 | console.error = () => {};
775 | });
776 | afterEach(() => {
777 | console.error = error;
778 | });
779 |
780 | test.each([
781 | ["sortableOption", "newValue", "sortableOption"],
782 | ["to-be-camelized", 1, "toBeCamelized"],
783 | ])(
784 | "attribute %s change for value %o, calls sortable option with %s attribute",
785 | async (attribute, value, sortableAttribute) => {
786 | vm.$attrs = { [attribute]: value };
787 | await Vue.nextTick();
788 | expect(SortableFake.option).toHaveBeenCalledWith(
789 | sortableAttribute,
790 | value
791 | );
792 | }
793 | );
794 |
795 | test.each([
796 | "Start",
797 | "Add",
798 | "Remove",
799 | "Update",
800 | "End",
801 | "Choose",
802 | "Unchoose",
803 | "Sort",
804 | "Filter",
805 | "Clone",
806 | "Move",
807 | ])("do not call option when updating option on%s", (callBack) => {
808 | vm.$attrs = { [`on${callBack}`]: jest.fn() };
809 | expect(SortableFake.option).not.toHaveBeenCalled();
810 | });
811 | });
812 |
813 | test.each([
814 | ["sortableOption", "newValue", "sortableOption"],
815 | ["to-be-camelized", 1, "toBeCamelized"],
816 | ])(
817 | "when option %s change for value %o, calls sortable option with %s attribute",
818 | async (attribute, value, sortableAttribute) => {
819 | wrapper.setProps({ options: { [attribute]: value } });
820 | await Vue.nextTick();
821 | expect(SortableFake.option).toHaveBeenCalledWith(
822 | sortableAttribute,
823 | value
824 | );
825 | }
826 | );
827 |
828 | it("does calls Sortable destroy when mounted", () => {
829 | expect(SortableFake.destroy.mock.calls.length).toBe(0);
830 | });
831 |
832 | it("calls Sortable destroy when destroyed", () => {
833 | wrapper.destroy();
834 | expect(SortableFake.destroy).toHaveBeenCalled();
835 | expect(SortableFake.destroy.mock.calls.length).toBe(1);
836 | });
837 |
838 | it("does not throw on destroy when sortable is not set", () => {
839 | delete vm._sortable;
840 | expect(() => wrapper.destroy()).not.toThrow();
841 | });
842 |
843 | it("renders id as html attribute", () => {
844 | wrapper = shallowMount(draggable, {
845 | propsData: {
846 | list: [],
847 | },
848 | attrs: {
849 | id: "my-id",
850 | },
851 | slots: {
852 | default: "",
853 | },
854 | });
855 |
856 | const element = wrapper.find("#my-id");
857 | expect(element.is("div")).toBe(true);
858 | expect(element.html()).toEqual(wrapper.html());
859 | });
860 |
861 | test.each([
862 | ["data-valor", "a"],
863 | ["data-valor2", "bd"],
864 | ["data-attribute", "efg"],
865 | ])(
866 | "renders attribute %s with value %s as html attribute",
867 | (attribute, value) => {
868 | wrapper = shallowMount(draggable, {
869 | propsData: {
870 | list: [],
871 | },
872 | attrs: {
873 | [attribute]: value,
874 | },
875 | slots: {
876 | default: "",
877 | },
878 | });
879 | const element = wrapper.find(`[${attribute}='${value}']`);
880 | expect(element.is("div")).toBe(true);
881 | expect(element.html()).toEqual(wrapper.html());
882 | }
883 | );
884 | });
885 |
886 | describe("draggable.vue when initialized with value", () => {
887 | beforeEach(() => {
888 | Sortable.mockClear();
889 | items = ["a", "b", "c"];
890 | wrapper = shallowMount(draggable, {
891 | propsData: {
892 | value: items,
893 | },
894 | slots: {
895 | default: items.map((item) => `${item}
`),
896 | },
897 | });
898 | vm = wrapper.vm;
899 | props = vm.$options.props;
900 | element = wrapper.element;
901 | });
902 |
903 | it("computes indexes", async () => {
904 | await Vue.nextTick();
905 | expect(vm.visibleIndexes).toEqual([0, 1, 2]);
906 | });
907 |
908 | it("renders correctly", () => {
909 | expectHTML(wrapper, initialRenderRaw);
910 | });
911 |
912 | it("update indexes", async () => {
913 | await Vue.nextTick();
914 | const computeIndexes = jest.fn();
915 | wrapper.setMethods({ computeIndexes });
916 | wrapper.setProps({ value: ["c", "d", "e", "f", "g"] });
917 | await Vue.nextTick();
918 | expect(computeIndexes).toHaveBeenCalled();
919 | });
920 |
921 | it("set realList", () => {
922 | expect(vm.realList).toEqual(["a", "b", "c"]);
923 | });
924 |
925 | it("transition mode should be false", () => {
926 | expect(vm.transitionMode).toBe(false);
927 | });
928 |
929 | describe("when initiating a drag operation", () => {
930 | let evt;
931 | beforeEach(() => {
932 | item = element.children[1];
933 | evt = { item };
934 | const start = getEvent("onStart");
935 | start(evt);
936 | });
937 |
938 | it("sends a start event", async () => {
939 | await Vue.nextTick();
940 | expect(wrapper.emitted()).toEqual({
941 | start: [[evt]],
942 | });
943 | });
944 |
945 | it("sets context", async () => {
946 | await Vue.nextTick();
947 | expect(vm.context).toEqual({
948 | element: "b",
949 | index: 1,
950 | });
951 | });
952 |
953 | describe("when remove is called", () => {
954 | beforeEach(() => {
955 | element.removeChild(item);
956 | const remove = getEvent("onRemove");
957 | remove({
958 | item,
959 | oldIndex: 1,
960 | });
961 | });
962 |
963 | it("DOM changes should be reverted", async () => {
964 | await Vue.nextTick();
965 | expectHTML(wrapper, initialRenderRaw);
966 | });
967 |
968 | it("input should with updated value", async () => {
969 | await Vue.nextTick();
970 | const expected = ["a", "c"];
971 | expect(wrapper.emitted().input).toEqual([[expected]]);
972 | });
973 |
974 | it("sends a remove event", async () => {
975 | await Vue.nextTick();
976 | const expectedEvt = { item, oldIndex: 1 };
977 | expect(wrapper.emitted().remove).toEqual([[expectedEvt]]);
978 | });
979 |
980 | it("sends a change event", async () => {
981 | await Vue.nextTick();
982 | const expectedEvt = { removed: { element: "b", oldIndex: 1 } };
983 | expect(wrapper.emitted().change).toEqual([[expectedEvt]]);
984 | });
985 | });
986 |
987 | describe("when update is called", () => {
988 | beforeEach(() => {
989 | const firstDraggable = element.children[0];
990 | element.removeChild(item);
991 | element.insertBefore(item, firstDraggable);
992 | const update = getEvent("onUpdate");
993 | update({
994 | item,
995 | oldIndex: 1,
996 | newIndex: 0,
997 | from: element,
998 | });
999 | });
1000 |
1001 | it("DOM changes should be reverted", async () => {
1002 | await Vue.nextTick();
1003 | expectHTML(wrapper, initialRenderRaw);
1004 | });
1005 |
1006 | it("send an input event", async () => {
1007 | await Vue.nextTick();
1008 | const expected = ["b", "a", "c"];
1009 | expect(wrapper.emitted().input).toEqual([[expected]]);
1010 | });
1011 |
1012 | it("sends a update event", async () => {
1013 | await Vue.nextTick();
1014 | const expectedEvt = {
1015 | item,
1016 | oldIndex: 1,
1017 | newIndex: 0,
1018 | from: element,
1019 | };
1020 | expect(wrapper.emitted().update).toEqual([[expectedEvt]]);
1021 | });
1022 |
1023 | it("sends a change event", async () => {
1024 | await Vue.nextTick();
1025 | const expectedEvt = {
1026 | moved: { element: "b", oldIndex: 1, newIndex: 0 },
1027 | };
1028 | expect(wrapper.emitted().change).toEqual([[expectedEvt]]);
1029 | });
1030 | });
1031 |
1032 | describe("when sending DragEnd", () => {
1033 | let endEvt;
1034 | beforeEach(() => {
1035 | endEvt = {
1036 | data: "data",
1037 | };
1038 | const onEnd = getEvent("onEnd");
1039 | onEnd(endEvt);
1040 | });
1041 |
1042 | it("sends a update event", async () => {
1043 | await Vue.nextTick();
1044 | expect(wrapper.emitted().end).toEqual([[endEvt]]);
1045 | });
1046 | });
1047 | });
1048 | });
1049 |
1050 | describe("draggable.vue when initialized with a transition group", () => {
1051 | beforeEach(() => {
1052 | Sortable.mockClear();
1053 | items = ["a", "b", "c"];
1054 | const inside = items.map((item) => `${item}
`).join("");
1055 | const template = `${inside} `;
1056 | wrapper = shallowMount(draggable, {
1057 | propsData: {
1058 | value: items,
1059 | },
1060 | slots: {
1061 | default: template,
1062 | },
1063 | });
1064 | vm = wrapper.vm;
1065 | props = vm.$options.props;
1066 | element = wrapper.element;
1067 | });
1068 |
1069 | it("computes indexes", async () => {
1070 | await Vue.nextTick();
1071 | expect(vm.visibleIndexes).toEqual([0, 1, 2]);
1072 | });
1073 |
1074 | it("set realList", () => {
1075 | expect(vm.realList).toEqual(["a", "b", "c"]);
1076 | });
1077 |
1078 | it("transition mode should be false", () => {
1079 | expect(vm.transitionMode).toBe(true);
1080 | });
1081 |
1082 | it("enders correctly", () => {
1083 | expectHTML(wrapper, initialRenderTransition);
1084 | });
1085 |
1086 | it("creates sortable instance with options on transition root", () => {
1087 | expect(Sortable.mock.calls.length).toBe(1);
1088 | const parameters = Sortable.mock.calls[0];
1089 | expect(parameters[0]).toBe(element.children[0]);
1090 | });
1091 |
1092 | describe("when initiating a drag operation", () => {
1093 | let evt;
1094 | beforeEach(() => {
1095 | item = element.children[0].children[1];
1096 | evt = { item };
1097 | const start = getEvent("onStart");
1098 | start(evt);
1099 | });
1100 |
1101 | it("sends a start event", async () => {
1102 | await Vue.nextTick();
1103 | expect(wrapper.emitted()).toEqual({
1104 | start: [[evt]],
1105 | });
1106 | });
1107 |
1108 | it("sets context", async () => {
1109 | await Vue.nextTick();
1110 | expect(vm.context).toEqual({
1111 | element: "b",
1112 | index: 1,
1113 | });
1114 | });
1115 |
1116 | describe("when remove is called", () => {
1117 | beforeEach(() => {
1118 | element.children[0].removeChild(item);
1119 | const remove = getEvent("onRemove");
1120 | remove({
1121 | item,
1122 | oldIndex: 1,
1123 | });
1124 | });
1125 |
1126 | it("DOM changes should be reverted", async () => {
1127 | await Vue.nextTick();
1128 | expectHTML(wrapper, initialRenderTransition);
1129 | });
1130 |
1131 | it("input should with updated value", async () => {
1132 | await Vue.nextTick();
1133 | const expected = ["a", "c"];
1134 | expect(wrapper.emitted().input).toEqual([[expected]]);
1135 | });
1136 |
1137 | it("sends a remove event", async () => {
1138 | await Vue.nextTick();
1139 | const expectedEvt = { item, oldIndex: 1 };
1140 | expect(wrapper.emitted().remove).toEqual([[expectedEvt]]);
1141 | });
1142 |
1143 | it("sends a change event", async () => {
1144 | await Vue.nextTick();
1145 | const expectedEvt = { removed: { element: "b", oldIndex: 1 } };
1146 | expect(wrapper.emitted().change).toEqual([[expectedEvt]]);
1147 | });
1148 | });
1149 |
1150 | describe("when update is called", () => {
1151 | beforeEach(() => {
1152 | const transitionRoot = element.children[0];
1153 | const firstDraggable = transitionRoot.children[0];
1154 | transitionRoot.removeChild(item);
1155 | transitionRoot.insertBefore(item, firstDraggable);
1156 | const update = getEvent("onUpdate");
1157 | update({
1158 | item,
1159 | oldIndex: 1,
1160 | newIndex: 0,
1161 | from: transitionRoot,
1162 | });
1163 | });
1164 |
1165 | it("DOM changes should be reverted", async () => {
1166 | await Vue.nextTick();
1167 | expectHTML(wrapper, initialRenderTransition);
1168 | });
1169 |
1170 | it("send an input event", async () => {
1171 | await Vue.nextTick();
1172 | const expected = ["b", "a", "c"];
1173 | expect(wrapper.emitted().input).toEqual([[expected]]);
1174 | });
1175 |
1176 | it("sends a update event", async () => {
1177 | await Vue.nextTick();
1178 | const expectedEvt = {
1179 | item,
1180 | oldIndex: 1,
1181 | newIndex: 0,
1182 | from: element.children[0],
1183 | };
1184 | expect(wrapper.emitted().update).toEqual([[expectedEvt]]);
1185 | });
1186 |
1187 | it("sends a change event", async () => {
1188 | await Vue.nextTick();
1189 | const expectedEvt = {
1190 | moved: { element: "b", oldIndex: 1, newIndex: 0 },
1191 | };
1192 | expect(wrapper.emitted().change).toEqual([[expectedEvt]]);
1193 | });
1194 | });
1195 |
1196 | describe("when calling onMove", () => {
1197 | let originalEvt;
1198 | let move;
1199 | let doMove;
1200 |
1201 | beforeEach(() => {
1202 | move = jest.fn();
1203 | wrapper.setProps({ move });
1204 | evt = {
1205 | to: element.children[0],
1206 | related: element.children[0].children[1],
1207 | willInsertAfter: false,
1208 | };
1209 | originalEvt = {
1210 | domInfo: true,
1211 | };
1212 | doMove = () => getEvent("onMove")(evt, originalEvt);
1213 | });
1214 |
1215 | it("calls move with list information", () => {
1216 | const expectedEvt = {
1217 | draggedContext: {
1218 | element: "b",
1219 | futureIndex: 1,
1220 | index: 1,
1221 | },
1222 | relatedContext: {
1223 | component: vm,
1224 | element: "b",
1225 | index: 1,
1226 | list: ["a", "b", "c"],
1227 | },
1228 | to: element.children[0],
1229 | related: element.children[0].children[1],
1230 | willInsertAfter: false,
1231 | };
1232 | doMove();
1233 | expect(move.mock.calls).toEqual([[expectedEvt, originalEvt]]);
1234 | });
1235 | });
1236 |
1237 | describe("when sending DragEnd", () => {
1238 | let endEvt;
1239 | beforeEach(() => {
1240 | endEvt = {
1241 | data: "data",
1242 | };
1243 | const onEnd = getEvent("onEnd");
1244 | onEnd(endEvt);
1245 | });
1246 |
1247 | it("sends a update event", async () => {
1248 | await Vue.nextTick();
1249 | expect(wrapper.emitted().end).toEqual([[endEvt]]);
1250 | });
1251 | });
1252 | });
1253 |
1254 | describe("draggable.vue when initialized with header and footer scoped slots", () => {
1255 | beforeEach(() => {
1256 | resetMocks();
1257 | items = ["a", "b", "c"];
1258 | wrapper = shallowMount(draggable, {
1259 | propsData: {
1260 | list: items,
1261 | },
1262 | attrs: {
1263 | sortableOption: "value",
1264 | "to-be-camelized": true,
1265 | },
1266 | slots: {
1267 | default: items.map((item) => `${item}
`),
1268 | },
1269 | scopedSlots: {
1270 | header: "",
1271 | footer: "",
1272 | },
1273 | });
1274 | vm = wrapper.vm;
1275 | props = vm.$options.props;
1276 | element = wrapper.element;
1277 | });
1278 |
1279 | it("renders correctly", () => {
1280 | expectHTML(wrapper, initialRender);
1281 | });
1282 | });
1283 | });
1284 |
--------------------------------------------------------------------------------
/tests/unit/vuedraggable.ssr.spec.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @jest-environment node
3 | */
4 |
5 | const Vue = require('vue');
6 | const renderer = require('vue-server-renderer').createRenderer();
7 | const draggable = require("@/vuedraggable").default;
8 | Vue.component('draggable', draggable);
9 | const app = new Vue({
10 | name: "test-app",
11 | template: `{{item}}
`,
12 | data:{
13 | items:["a","b","c"]
14 | }
15 | });
16 |
17 | let html;
18 |
19 | describe("vuedraggable in a SSR context", () => {
20 | beforeEach(async () => {
21 | html = await renderer.renderToString(app);
22 | });
23 |
24 | it("can be rendered", () => {
25 | const expected = '';
26 | expect(html).toEqual(expected);
27 | })
28 | })
--------------------------------------------------------------------------------
/vue.config.js:
--------------------------------------------------------------------------------
1 | const config = {
2 | publicPath: "./",
3 | configureWebpack: {
4 | output: {
5 | libraryExport: 'default'
6 | }
7 | }
8 | }
9 |
10 | if (process.env.NODE_ENV === "production") {
11 | config.configureWebpack.externals = {
12 | sortablejs: {
13 | commonjs: "sortablejs",
14 | commonjs2: "sortablejs",
15 | amd: "sortablejs",
16 | root: "Sortable"
17 | }
18 | };
19 | };
20 |
21 | module.exports = config;
--------------------------------------------------------------------------------